index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. <template>
  2. <div class="user-manage">
  3. <Block class="header-block tw-flex tw-items-center">
  4. <a-form layout="inline">
  5. <a-form-item label="学校名称">
  6. <a-select
  7. v-model:value="query.schoolId"
  8. show-search
  9. :filterOption="false"
  10. @search="(name:string) => querySchoolList(name,'list')"
  11. placeholder="学校名称"
  12. >
  13. <a-select-option
  14. v-for="school in schoolTableData.result"
  15. :key="school.id"
  16. :value="school.id"
  17. >{{ school.name }}</a-select-option
  18. >
  19. </a-select>
  20. </a-form-item>
  21. <a-form-item label="登录名">
  22. <a-input
  23. v-model:value="query.loginName"
  24. placeholder="登录手机号"
  25. ></a-input>
  26. </a-form-item>
  27. <a-form-item label="角色">
  28. <a-select v-model:value="query.role" placeholder="用户角色">
  29. <a-select-option :value="void 0">全部</a-select-option>
  30. <a-select-option value="SCHOOL_ADMIN">{{
  31. ROLE.SCHOOL_ADMIN
  32. }}</a-select-option>
  33. <a-select-option value="SECTION_LEADER">{{
  34. ROLE.SECTION_LEADER
  35. }}</a-select-option>
  36. </a-select>
  37. </a-form-item>
  38. <a-form-item>
  39. <a-button class="search-button" type="primary" @click="queryUserList"
  40. >查询</a-button
  41. >
  42. </a-form-item>
  43. </a-form>
  44. <a-button
  45. type="primary"
  46. class="tw-flex tw-items-center operation-button"
  47. @click="toggleAddUserModal"
  48. >
  49. <template #icon>
  50. <PlusCircleOutlined />
  51. </template>
  52. 新增
  53. </a-button>
  54. <a-button
  55. type="primary"
  56. class="tw-flex tw-items-center operation-button"
  57. @click="importUserList"
  58. >
  59. <template #icon>
  60. <DownloadOutlined />
  61. </template>
  62. 导入
  63. </a-button>
  64. </Block>
  65. <Block class="user-table">
  66. <a-table
  67. :columns="columns"
  68. :data-source="userTableData.result"
  69. :pagination="{
  70. total: userTableData.totalCount,
  71. pageSize: query.pageSize,
  72. }"
  73. @change="currentPageChange"
  74. :row-class-name="
  75. (_:any, index:number) => (index % 2 === 1 ? 'table-striped' : null)
  76. "
  77. >
  78. <template #bodyCell="{ column, record, index }">
  79. <template v-if="column.dataIndex === 'index'">
  80. {{ index + 1 }}
  81. </template>
  82. <template v-else-if="column.dataIndex === 'enable'">
  83. <template v-if="record.enable">
  84. <CheckCircleFilled style="color: #30bf78" />
  85. </template>
  86. <template v-else>
  87. <CloseCircleFilled style="color: #f4664a" />
  88. </template>
  89. </template>
  90. <template v-else-if="column.dataIndex === 'operation'">
  91. <div
  92. class="tw-flex tw-items-center"
  93. v-if="record.role !== 'SUPER_ADMIN'"
  94. >
  95. <span
  96. class="tw-cursor-pointer tw-p-2"
  97. @click="updateUserStatus(record)"
  98. >{{ record.enable ? "禁用" : "启用" }}</span
  99. >
  100. <span
  101. class="tw-cursor-pointer tw-p-2 tw-ml-1"
  102. @click="onEdit(record)"
  103. >编辑</span
  104. >
  105. <span
  106. class="tw-cursor-pointer tw-p-2 tw-ml-1"
  107. @click="onResetPwd(record)"
  108. >重置密码
  109. </span>
  110. </div>
  111. </template>
  112. </template>
  113. </a-table>
  114. </Block>
  115. <a-modal
  116. v-model:visible="showModal"
  117. :title="`${!userInfo.id ? '新增' : '编辑'}用户`"
  118. okText="确定"
  119. cancelText="取消"
  120. :maskClosable="false"
  121. @ok="onPutUser"
  122. :afterClose="resetUserFields"
  123. >
  124. <a-form :labelCol="{ span: 6 }">
  125. <a-form-item label="学校" v-bind="validateInfos.schoolId">
  126. <a-select
  127. v-model:value="userInfo.schoolId"
  128. show-search
  129. :disabled="!!userInfo.id"
  130. :filterOption="false"
  131. @search="(name:string) => querySchoolList(name,'form')"
  132. placeholder="学校名称"
  133. >
  134. <a-select-option
  135. v-for="school in userInfo.schoolTableData.result"
  136. :key="school.id"
  137. :value="school.id"
  138. >{{ school.name }}</a-select-option
  139. >
  140. </a-select>
  141. </a-form-item>
  142. <a-form-item label="姓名" v-bind="validateInfos.name">
  143. <a-input
  144. v-model:value="userInfo.name"
  145. placeholder="请输入姓名"
  146. ></a-input>
  147. </a-form-item>
  148. <a-form-item label="登录名" v-bind="validateInfos.loginName">
  149. <a-input
  150. :disabled="!!userInfo.id"
  151. v-model:value="userInfo.loginName"
  152. maxlength="11"
  153. :placeholder="
  154. userInfo.role === 'SECTION_LEADER'
  155. ? '请输入登录手机号'
  156. : '请输入登录名'
  157. "
  158. ></a-input>
  159. </a-form-item>
  160. <a-form-item
  161. v-if="!userInfo.id"
  162. label="密码"
  163. v-bind="validateInfos.passwd"
  164. >
  165. <a-input-password
  166. v-model:value="userInfo.passwd"
  167. placeholder="请输入密码"
  168. ></a-input-password>
  169. </a-form-item>
  170. <a-form-item label="角色" v-bind="validateInfos.role">
  171. <a-select v-model:value="userInfo.role" placeholder="请选择角色">
  172. <a-select-option value="SCHOOL_ADMIN">{{
  173. ROLE.SCHOOL_ADMIN
  174. }}</a-select-option>
  175. <a-select-option value="SECTION_LEADER">{{
  176. ROLE.SECTION_LEADER
  177. }}</a-select-option>
  178. </a-select>
  179. </a-form-item>
  180. <a-form-item
  181. v-if="userInfo.role === 'SECTION_LEADER'"
  182. label="所属科目"
  183. v-bind="validateInfos.course"
  184. >
  185. <a-textarea
  186. v-model:value="userInfo.course"
  187. placeholder="请填写所属科目代码, 以','分隔;"
  188. ></a-textarea>
  189. </a-form-item>
  190. </a-form>
  191. </a-modal>
  192. <a-modal
  193. v-model:visible="showResetPwdModal"
  194. title="密码设置"
  195. okText="确定"
  196. cancelText="取消"
  197. :maskClosable="false"
  198. @ok="onUpdateUserPwd"
  199. :afterClose="resetPwdFields"
  200. >
  201. <a-form :labelCol="{ span: 3 }">
  202. <a-form-item label="密码" v-bind="validatePwdInfos.passwd">
  203. <a-input-password v-model:value="resetPwd.passwd"></a-input-password>
  204. </a-form-item>
  205. </a-form>
  206. </a-modal>
  207. <a-modal
  208. v-model:visible="showImportModal"
  209. :maskClosable="false"
  210. title="导入用户"
  211. okText="确认上传"
  212. cancelText="取消"
  213. @ok="onImportUserList"
  214. :afterClose="resetImportFields"
  215. >
  216. <a-form :labelCol="{ span: 6 }">
  217. <a-form-item label="学校名称" v-bind="validateImportInfos.schoolId">
  218. <a-select
  219. v-model:value="importUserForm.schoolId"
  220. show-search
  221. :filterOption="false"
  222. @search="(name: string) => querySchoolList(name,'import')"
  223. placeholder="学校名称"
  224. >
  225. <a-select-option
  226. v-for="school in importUserForm.schoolTableData.result"
  227. :key="school.id"
  228. :value="school.id"
  229. >{{ school.name }}</a-select-option
  230. >
  231. </a-select>
  232. </a-form-item>
  233. <a-form-item label="用户导入文件" v-bind="validateImportInfos.fileList">
  234. <a-upload
  235. :file-list="importUserForm.fileList"
  236. :before-upload="beforeUpload"
  237. @remove="handleRemove"
  238. :max-count="1"
  239. type="primary"
  240. >
  241. <a-button>
  242. <upload-outlined></upload-outlined>
  243. 选择文件
  244. </a-button>
  245. <a class="tw-ml-4 tw-align-bottom" @click.stop="downloadTemplate">
  246. 下载导入模板
  247. </a>
  248. </a-upload>
  249. </a-form-item>
  250. </a-form>
  251. </a-modal>
  252. </div>
  253. </template>
  254. <script setup lang="ts" name="PageUsers">
  255. import { nextTick, reactive, ref, watch, markRaw } from "vue";
  256. import {
  257. PlusCircleOutlined,
  258. CheckCircleFilled,
  259. CloseCircleFilled,
  260. DownloadOutlined,
  261. } from "@ant-design/icons-vue";
  262. import {
  263. getUserListHttp,
  264. updateUserStatusHttp,
  265. editUserInfoHttp,
  266. resetUserPwdHttp,
  267. importUserHttp,
  268. downloadImportUserHttp,
  269. } from "@/apis/user";
  270. import Block from "@/components/block/index.vue";
  271. import { message } from "ant-design-vue";
  272. import { Form } from "ant-design-vue";
  273. import { getSchoolListHttp } from "@/apis/school";
  274. import type { UploadProps, TableColumnType } from "ant-design-vue";
  275. import { useMainStore } from "@/store/main";
  276. import { throttle } from "lodash-es";
  277. import { ROLE } from "@/constants/dicts";
  278. const mainStore = useMainStore();
  279. const showModal = ref(false);
  280. const showResetPwdModal = ref(false);
  281. const showImportModal = ref(false);
  282. const importUserForm = reactive<{
  283. schoolId: string;
  284. fileList: UploadProps["fileList"];
  285. schoolTableData: MultiplePageData<SchoolListInfo>;
  286. }>({
  287. schoolId: "",
  288. fileList: [],
  289. schoolTableData: { totalCount: 0, result: [] },
  290. });
  291. const importRules = {
  292. schoolId: [{ required: true, message: "请选择学校" }],
  293. fileList: [
  294. { required: true, type: "array", len: 1, message: "请选择导入文件" },
  295. ],
  296. };
  297. const userInfo = reactive<
  298. EditUserInfo & { schoolTableData: MultiplePageData<SchoolListInfo> }
  299. >({
  300. schoolId: "",
  301. name: "",
  302. loginName: "",
  303. course: "",
  304. passwd: void 0,
  305. role: void 0,
  306. id: void 0,
  307. schoolTableData: { totalCount: 0, result: [] },
  308. });
  309. const resetPwd = reactive({
  310. passwd: "",
  311. userId: "",
  312. });
  313. const editUserRules = () => ({
  314. name: [{ required: true, message: "请填写用户姓名" }],
  315. role: [{ required: true, message: "请选择用户角色" }],
  316. });
  317. const addUserRules = () => ({
  318. schoolId: [{ required: true, message: "请选择用户所属学校" }],
  319. loginName: [{ required: true, message: "请填写登录名" }],
  320. passwd: [
  321. { required: true, message: "请填写登录密码" },
  322. {
  323. pattern: /^[a-zA-Z0-9]{6,18}$/,
  324. message: "密码只能由数字、字母组成,长度6-18个字符",
  325. },
  326. ],
  327. });
  328. const userRules = reactive({
  329. ...addUserRules(),
  330. ...editUserRules(),
  331. });
  332. const pwdRules = {
  333. passwd: [
  334. { required: true, message: "请填写登录密码" },
  335. {
  336. pattern: /^[a-zA-Z0-9]{6,18}$/,
  337. message: "密码只能由数字、字母组成,长度6-18个字符",
  338. },
  339. ],
  340. };
  341. const {
  342. validate,
  343. validateInfos,
  344. resetFields: resetUserFields,
  345. } = Form.useForm(userInfo, userRules);
  346. const {
  347. validate: validatePwd,
  348. validateInfos: validatePwdInfos,
  349. resetFields: resetPwdFields,
  350. } = Form.useForm(resetPwd, pwdRules);
  351. const {
  352. validate: validateImport,
  353. validateInfos: validateImportInfos,
  354. resetFields: resetImportFields,
  355. } = Form.useForm(importUserForm, importRules);
  356. /** 请求参数 */
  357. const query = reactive<FetchUserListQuery>({
  358. loginName: "",
  359. schoolId: mainStore.systemUserInfo?.schoolId || "",
  360. role: void 0,
  361. pageNumber: 1,
  362. pageSize: 10,
  363. });
  364. /** table配置 */
  365. const columns: TableColumnType[] = [
  366. { title: "序号", dataIndex: "index", align: "center" },
  367. { title: "ID", dataIndex: "id" },
  368. { title: "姓名", dataIndex: "name" },
  369. { title: "登录名", dataIndex: "loginName" },
  370. { title: "学校", dataIndex: "schoolName", align: "center" },
  371. { title: "角色", dataIndex: "roleName" },
  372. { title: "更新时间", dataIndex: "updateTime" },
  373. { title: "状态", dataIndex: "enable" },
  374. { title: "操作", dataIndex: "operation" },
  375. ];
  376. /** 用户列表信息 */
  377. const userTableData = reactive<MultiplePageData<UserInfo>>({
  378. totalCount: 0,
  379. result: [],
  380. });
  381. /** 学校列表信息 */
  382. const schoolTableData = reactive<MultiplePageData<SchoolListInfo>>({
  383. totalCount: 0,
  384. result: [],
  385. });
  386. /** 查询学校列表 */
  387. const querySchoolList = throttle(
  388. async (name: string = "", type: "list" | "form" | "import" = "list") => {
  389. const isList = type === "list";
  390. const isForm = type === "form";
  391. try {
  392. const { result = [], totalCount } = await getSchoolListHttp({
  393. name,
  394. pageNumber: 1,
  395. pageSize: 10,
  396. });
  397. Object.assign(
  398. isList
  399. ? schoolTableData
  400. : isForm
  401. ? userInfo.schoolTableData
  402. : importUserForm.schoolTableData,
  403. {
  404. result,
  405. totalCount,
  406. }
  407. );
  408. } catch (error) {
  409. return Promise.reject(error);
  410. }
  411. },
  412. 100
  413. );
  414. /** 显示新增用户弹窗 */
  415. const toggleAddUserModal = (show: boolean = true) => {
  416. if (show) {
  417. if (userInfo.id) {
  418. Object.assign(userRules, { schoolId: [], loginName: [], passwd: [] });
  419. } else {
  420. Object.assign(userInfo, { schoolId: query.schoolId || mainStore.systemUserInfo?.schoolId });
  421. Object.assign(userRules, { ...addUserRules() });
  422. }
  423. querySchoolList("", "form");
  424. }
  425. nextTick(() => {
  426. showModal.value = show;
  427. });
  428. };
  429. /** 查询用户列表 */
  430. const queryUserList = async () => {
  431. try {
  432. const { result = [], totalCount } = await getUserListHttp(query);
  433. Object.assign(userTableData, { result, totalCount });
  434. } catch (error) {
  435. console.error(error);
  436. }
  437. };
  438. watch(() => query.pageNumber, queryUserList);
  439. /* 启用/禁用 */
  440. const updateUserStatus = (record: UserInfo) => {
  441. updateUserStatusHttp({ enable: !record.enable, ids: [record.id] }).then(
  442. queryUserList
  443. );
  444. };
  445. /** 编辑用户 */
  446. const onEdit = (record: UserInfo) => {
  447. Object.assign(userInfo, {
  448. course: record.courseCodes?.join(","),
  449. id: record.id,
  450. schoolId: record.schoolId,
  451. name: record.name,
  452. loginName: record.loginName,
  453. role: record.role,
  454. });
  455. toggleAddUserModal(true);
  456. };
  457. /** 重置密码 */
  458. const onResetPwd = (record: UserInfo) => {
  459. Object.assign(resetPwd, { passwd: "", userId: `${record.id}` });
  460. showResetPwdModal.value = true;
  461. };
  462. /** 新增用户 */
  463. const onPutUser = () => {
  464. const role = userInfo.role;
  465. const isEdit = !!userInfo.id;
  466. if (!isEdit) {
  467. if (role === "SECTION_LEADER") {
  468. Object.assign(userRules, {
  469. loginName: [
  470. { required: true, message: "请填写登录手机号" },
  471. // @ts-ignore
  472. { pattern: /\d{11}/, message: "请填写正确的手机号" },
  473. ],
  474. });
  475. } else {
  476. Object.assign(userRules, {
  477. loginName: [{ required: true, message: "请填写登录名" }],
  478. });
  479. }
  480. }
  481. validate().then((valid) => {
  482. if (valid) {
  483. const { role, course, schoolTableData, ...info } = userInfo;
  484. editUserInfoHttp({
  485. ...info,
  486. role,
  487. course: role === "SECTION_LEADER" ? course : "",
  488. }).then(() => {
  489. message.success(`${isEdit ? "修改" : "添加"}成功`);
  490. queryUserList();
  491. toggleAddUserModal(false);
  492. });
  493. }
  494. });
  495. };
  496. /** 导入用户 */
  497. const importUserList = () => {
  498. showImportModal.value = true;
  499. Object.assign(importUserForm, {
  500. schoolId: query.schoolId || mainStore.systemUserInfo?.schoolId,
  501. });
  502. querySchoolList("", "import");
  503. };
  504. const handleRemove: UploadProps["onRemove"] = (file) => {
  505. const index = importUserForm.fileList!.indexOf(file);
  506. const newFileList = importUserForm.fileList!.slice();
  507. newFileList.splice(index, 1);
  508. importUserForm.fileList = newFileList;
  509. };
  510. const beforeUpload: UploadProps["beforeUpload"] = (file) => {
  511. importUserForm.fileList = [file];
  512. return false;
  513. };
  514. /** 下载导入模板 */
  515. const downloadTemplate = async () => {
  516. try {
  517. await downloadImportUserHttp();
  518. } catch (error) {
  519. return Promise.reject(error);
  520. }
  521. };
  522. const onImportUserList = async () => {
  523. try {
  524. const valid = await validateImport();
  525. if (valid) {
  526. const formData = new FormData();
  527. formData.append("schoolId", `${importUserForm.schoolId}`);
  528. importUserForm.fileList?.forEach((file: any) => {
  529. formData.append("file", file);
  530. });
  531. await importUserHttp(formData);
  532. queryUserList();
  533. showImportModal.value = false;
  534. }
  535. } catch (error) {
  536. return Promise.reject(error);
  537. }
  538. };
  539. /** 修改密码 */
  540. const onUpdateUserPwd = () => {
  541. validatePwd().then((valid) => {
  542. if (valid) {
  543. resetUserPwdHttp(resetPwd).then(() => {
  544. message.success(`重置成功`);
  545. showResetPwdModal.value = false;
  546. });
  547. }
  548. });
  549. };
  550. const currentPageChange = ({ current }: { current: number }) => {
  551. query.pageNumber = current;
  552. };
  553. querySchoolList();
  554. /** effect */
  555. if (query.schoolId) {
  556. queryUserList();
  557. }
  558. </script>
  559. <style scoped lang="less">
  560. .user-manage {
  561. .header-block {
  562. .ant-input {
  563. width: 160px;
  564. }
  565. :deep(.ant-select-selector) {
  566. width: 160px;
  567. }
  568. .search-button {
  569. background-color: @font-color;
  570. color: @white;
  571. border: none;
  572. width: 56px;
  573. padding: 0;
  574. &:after {
  575. display: none;
  576. opacity: 0;
  577. }
  578. }
  579. .operation-button {
  580. width: 72px;
  581. padding: 0;
  582. margin-left: auto;
  583. & ~ .operation-button {
  584. margin-left: 8px;
  585. }
  586. }
  587. }
  588. .user-table {
  589. }
  590. }
  591. </style>