UserManageModal.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. <template>
  2. <Modal :open="open" title="用户管理" :width="920" :footer="null" destroy-on-close @cancel="handleClose">
  3. <div class="user-manage__toolbar">
  4. <InputSearch
  5. v-model:value="keyword"
  6. placeholder="搜索用户名 / 显示名"
  7. allow-clear
  8. style="width: 240px"
  9. @search="handleSearch"
  10. />
  11. <Button type="primary" @click="openCreate">新建用户</Button>
  12. </div>
  13. <Table
  14. :data-source="records"
  15. :columns="columns"
  16. :loading="loading"
  17. :pagination="pagination"
  18. row-key="id"
  19. size="small"
  20. @change="handleTableChange"
  21. >
  22. <template #bodyCell="{ column, record }">
  23. <template v-if="column.key === 'isAdmin'">
  24. <Tag :color="Number(record.isAdmin) === 1 ? 'blue' : 'default'">
  25. {{ Number(record.isAdmin) === 1 ? '管理员' : '普通用户' }}
  26. </Tag>
  27. </template>
  28. <template v-else-if="column.key === 'status'">
  29. <Tag :color="Number(record.status) === 1 ? 'success' : 'error'">
  30. {{ Number(record.status) === 1 ? '启用' : '停用' }}
  31. </Tag>
  32. </template>
  33. <template v-else-if="column.key === 'action'">
  34. <a class="user-manage__link" @click="openEdit(record)">编辑</a>
  35. <Divider type="vertical" />
  36. <a class="user-manage__link" @click="openResetPwd(record)">重置密码</a>
  37. <Divider type="vertical" />
  38. <a class="user-manage__link" @click="handleKickout(record)">强制下线</a>
  39. <Divider type="vertical" />
  40. <a class="user-manage__link user-manage__link--danger" @click="handleDelete(record)">删除</a>
  41. </template>
  42. </template>
  43. </Table>
  44. </Modal>
  45. <Modal
  46. v-model:open="formVisible"
  47. :title="form.id ? '编辑用户' : '新建用户'"
  48. :width="460"
  49. :confirm-loading="submitting"
  50. destroy-on-close
  51. @ok="handleSubmit"
  52. >
  53. <Form layout="vertical">
  54. <FormItem label="用户名" required>
  55. <Input v-model:value="form.username" :disabled="!!form.id" placeholder="登录名,创建后不可修改" />
  56. </FormItem>
  57. <FormItem v-if="!form.id" label="初始密码" required>
  58. <InputPassword v-model:value="form.password" placeholder="至少 6 位" autocomplete="new-password" />
  59. </FormItem>
  60. <FormItem label="显示名">
  61. <Input v-model:value="form.displayName" placeholder="不填则与用户名一致" />
  62. </FormItem>
  63. <FormItem label="角色">
  64. <RadioGroup v-model:value="form.isAdmin">
  65. <Radio :value="0">普通用户</Radio>
  66. <Radio :value="1">管理员</Radio>
  67. </RadioGroup>
  68. </FormItem>
  69. <FormItem v-if="form.id" label="状态">
  70. <RadioGroup v-model:value="form.status">
  71. <Radio :value="1">启用</Radio>
  72. <Radio :value="0">停用</Radio>
  73. </RadioGroup>
  74. </FormItem>
  75. </Form>
  76. </Modal>
  77. <Modal
  78. v-model:open="pwdVisible"
  79. title="重置密码"
  80. :width="420"
  81. :confirm-loading="submitting"
  82. destroy-on-close
  83. @ok="handleResetPwd"
  84. >
  85. <p class="user-manage__hint">为「{{ pwdTarget?.username }}」设置新密码,重置后该用户会被强制下线。</p>
  86. <InputPassword v-model:value="newPassword" placeholder="至少 6 位" autocomplete="new-password" />
  87. </Modal>
  88. </template>
  89. <script lang="ts">
  90. import { defineComponent, reactive, ref, watch } from 'vue';
  91. import {
  92. Button,
  93. Divider,
  94. Form,
  95. FormItem,
  96. Input,
  97. InputPassword,
  98. InputSearch,
  99. Modal,
  100. Radio,
  101. RadioGroup,
  102. Table,
  103. Tag,
  104. } from 'ant-design-vue';
  105. import { useMessage } from '@/core/hooks/web/useMessage';
  106. import {
  107. createUserApi,
  108. deleteUserApi,
  109. kickoutUserApi,
  110. listUsersApi,
  111. resetUserPwdApi,
  112. updateUserApi,
  113. type UserInfo,
  114. } from '@/core/api/auth/authApi';
  115. export default defineComponent({
  116. name: 'UserManageModal',
  117. components: {
  118. Button,
  119. Divider,
  120. Form,
  121. FormItem,
  122. Input,
  123. InputPassword,
  124. InputSearch,
  125. Modal,
  126. Radio,
  127. RadioGroup,
  128. Table,
  129. Tag,
  130. },
  131. props: {
  132. open: {
  133. type: Boolean,
  134. default: false,
  135. },
  136. },
  137. emits: ['update:open'],
  138. setup(props, { emit }) {
  139. const { createMessage, createConfirm } = useMessage();
  140. const loading = ref(false);
  141. const submitting = ref(false);
  142. const keyword = ref('');
  143. const records = ref<UserInfo[]>([]);
  144. const pagination = reactive({
  145. current: 1,
  146. pageSize: 10,
  147. total: 0,
  148. showSizeChanger: true,
  149. showTotal: (total: number) => `共 ${total} 条`,
  150. });
  151. const columns = [
  152. { title: '用户名', dataIndex: 'username', key: 'username', width: 140 },
  153. { title: '显示名', dataIndex: 'displayName', key: 'displayName', width: 140 },
  154. { title: '角色', key: 'isAdmin', width: 100 },
  155. { title: '状态', key: 'status', width: 90 },
  156. { title: '最后登录', dataIndex: 'lastLoginTime', key: 'lastLoginTime', width: 170 },
  157. { title: '操作', key: 'action', width: 260 },
  158. ];
  159. const formVisible = ref(false);
  160. const form = reactive<{
  161. id?: string;
  162. username: string;
  163. password: string;
  164. displayName: string;
  165. isAdmin: number;
  166. status: number;
  167. }>({ username: '', password: '', displayName: '', isAdmin: 0, status: 1 });
  168. const pwdVisible = ref(false);
  169. const newPassword = ref('');
  170. const pwdTarget = ref<UserInfo | null>(null);
  171. async function loadList() {
  172. loading.value = true;
  173. try {
  174. const res = await listUsersApi({
  175. keyword: keyword.value || undefined,
  176. page: pagination.current,
  177. size: pagination.pageSize,
  178. });
  179. records.value = res?.records || [];
  180. pagination.total = Number(res?.total || 0);
  181. } catch {
  182. records.value = [];
  183. } finally {
  184. loading.value = false;
  185. }
  186. }
  187. function handleSearch() {
  188. pagination.current = 1;
  189. void loadList();
  190. }
  191. function handleTableChange(pag: { current?: number; pageSize?: number }) {
  192. pagination.current = pag?.current || 1;
  193. pagination.pageSize = pag?.pageSize || 10;
  194. void loadList();
  195. }
  196. function openCreate() {
  197. Object.assign(form, { id: undefined, username: '', password: '', displayName: '', isAdmin: 0, status: 1 });
  198. formVisible.value = true;
  199. }
  200. function openEdit(record: Record<string, any>) {
  201. Object.assign(form, {
  202. id: record.id,
  203. username: record.username,
  204. password: '',
  205. displayName: record.displayName,
  206. isAdmin: Number(record.isAdmin),
  207. status: Number(record.status),
  208. });
  209. formVisible.value = true;
  210. }
  211. async function handleSubmit() {
  212. const username = form.username?.trim();
  213. if (!form.id && !username) {
  214. createMessage.warning('请填写用户名');
  215. return;
  216. }
  217. if (!form.id && (!form.password || form.password.length < 6)) {
  218. createMessage.warning('初始密码不能少于 6 位');
  219. return;
  220. }
  221. submitting.value = true;
  222. try {
  223. if (form.id) {
  224. await updateUserApi({
  225. id: form.id,
  226. displayName: form.displayName,
  227. isAdmin: form.isAdmin,
  228. status: form.status,
  229. });
  230. } else {
  231. await createUserApi({
  232. username,
  233. password: form.password,
  234. displayName: form.displayName,
  235. isAdmin: form.isAdmin,
  236. });
  237. }
  238. formVisible.value = false;
  239. await loadList();
  240. } catch {
  241. // 错误提示由 defHttp 统一弹出
  242. } finally {
  243. submitting.value = false;
  244. }
  245. }
  246. function openResetPwd(record: Record<string, any>) {
  247. pwdTarget.value = record as unknown as UserInfo;
  248. newPassword.value = '';
  249. pwdVisible.value = true;
  250. }
  251. async function handleResetPwd() {
  252. if (!pwdTarget.value || !newPassword.value || newPassword.value.length < 6) {
  253. createMessage.warning('新密码不能少于 6 位');
  254. return;
  255. }
  256. submitting.value = true;
  257. try {
  258. await resetUserPwdApi({ id: pwdTarget.value.id, newPassword: newPassword.value });
  259. pwdVisible.value = false;
  260. createMessage.success('密码已重置');
  261. } catch {
  262. // 错误提示由 defHttp 统一弹出
  263. } finally {
  264. submitting.value = false;
  265. }
  266. }
  267. function handleKickout(record: Record<string, any>) {
  268. createConfirm({
  269. iconType: 'warning',
  270. title: '提示',
  271. content: `确认强制「${record.username}」下线?`,
  272. onOk: async () => {
  273. await kickoutUserApi(record.id);
  274. createMessage.success('已强制下线');
  275. },
  276. });
  277. }
  278. function handleDelete(record: Record<string, any>) {
  279. createConfirm({
  280. iconType: 'warning',
  281. title: '提示',
  282. content: `确认删除用户「${record.username}」?该操作不可恢复。`,
  283. onOk: async () => {
  284. await deleteUserApi(record.id);
  285. createMessage.success('已删除');
  286. await loadList();
  287. },
  288. });
  289. }
  290. function handleClose() {
  291. emit('update:open', false);
  292. }
  293. watch(
  294. () => props.open,
  295. (val) => {
  296. if (val) {
  297. pagination.current = 1;
  298. keyword.value = '';
  299. void loadList();
  300. }
  301. },
  302. { immediate: true },
  303. );
  304. return {
  305. loading,
  306. submitting,
  307. keyword,
  308. records,
  309. pagination,
  310. columns,
  311. formVisible,
  312. form,
  313. pwdVisible,
  314. newPassword,
  315. pwdTarget,
  316. handleSearch,
  317. handleTableChange,
  318. openCreate,
  319. openEdit,
  320. handleSubmit,
  321. openResetPwd,
  322. handleResetPwd,
  323. handleKickout,
  324. handleDelete,
  325. handleClose,
  326. };
  327. },
  328. });
  329. </script>
  330. <style lang="less" scoped>
  331. .user-manage {
  332. &__toolbar {
  333. display: flex;
  334. align-items: center;
  335. justify-content: space-between;
  336. margin-bottom: 12px;
  337. }
  338. &__link {
  339. color: @primary-color;
  340. &--danger {
  341. color: @error-color;
  342. }
  343. }
  344. &__hint {
  345. margin-bottom: 12px;
  346. color: @cb-text-secondary;
  347. }
  348. }
  349. </style>