您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 
 
 

330 行
12 KiB

  1. {% extends "../layout.html" %}
  2. {% block title %}用户管理{% endblock %}
  3. {% block content %}
  4. <div id="userApp">
  5. <!-- 搜索栏 -->
  6. <el-card shadow="never" class="mb-4">
  7. <el-form :inline="true" @submit.prevent="loadList()" class="flex items-center flex-wrap gap-2">
  8. <el-form-item label="关键词" class="!mb-0">
  9. <el-input v-model="keyword" placeholder="用户名/姓名..." clearable style="width:180px;" />
  10. </el-form-item>
  11. <el-form-item label="角色" class="!mb-0">
  12. <el-select v-model="roleFilter" placeholder="全部" clearable style="width:120px;">
  13. <el-option v-for="role in roles" :key="role.id" :label="role.name" :value="role.id" />
  14. </el-select>
  15. </el-form-item>
  16. <el-form-item label="状态" class="!mb-0">
  17. <el-select v-model="statusFilter" placeholder="全部" clearable style="width:100px;">
  18. <el-option label="启用" :value="1" />
  19. <el-option label="禁用" :value="0" />
  20. </el-select>
  21. </el-form-item>
  22. <el-form-item class="!mb-0">
  23. <el-button type="primary" @click="loadList()">搜索</el-button>
  24. <el-button @click="resetFilter">重置</el-button>
  25. </el-form-item>
  26. </el-form>
  27. </el-card>
  28. <!-- 用户列表 -->
  29. <el-card shadow="never">
  30. <template #header>
  31. <el-button type="primary" @click="showAddModal">+ 新增用户</el-button>
  32. </template>
  33. <el-table :data="tableData" v-loading="loading" stripe border>
  34. <el-table-column prop="id" label="ID" width="60" ></el-table-column>
  35. <el-table-column prop="username" label="用户名" ></el-table-column>
  36. <el-table-column prop="nickname" label="姓名">
  37. <template #default="{ row }">${ row.nickname || '-' }</template>
  38. </el-table-column>
  39. <el-table-column prop="role_name" label="角色" width="120">
  40. <template #default="{ row }">
  41. <el-tag type="primary" size="small">${ row.role_name || '-' }</el-tag>
  42. </template>
  43. </el-table-column>
  44. <el-table-column prop="email" label="邮箱" width="180">
  45. <template #default="{ row }">${ row.email || '-' }</template>
  46. </el-table-column>
  47. <el-table-column prop="status" label="状态" width="80" align="center">
  48. <template #default="{ row }">
  49. <el-tag v-if="row.status === 1" type="success" size="small">启用</el-tag>
  50. <el-tag v-else type="info" size="small">禁用</el-tag>
  51. </template>
  52. </el-table-column>
  53. <el-table-column prop="last_login_time" label="最后登录" width="160">
  54. <template #default="{ row }">${ row.last_login_time || '-' }</template>
  55. </el-table-column>
  56. <el-table-column label="操作" width="200" align="center">
  57. <template #default="{ row }">
  58. <el-button type="primary" link @click="editUser(row)">编辑</el-button>
  59. <el-button type="primary" link @click="showResetModal(row)">重置密码</el-button>
  60. <el-button :type="row.status === 1 ? 'danger' : 'success'" link @click="toggleStatus(row)">
  61. ${ row.status === 1 ? '禁用' : '启用' }
  62. </el-button>
  63. </template>
  64. </el-table-column>
  65. </el-table>
  66. <div class="flex justify-end mt-4">
  67. <el-pagination
  68. v-model:current-page="pagination.page"
  69. :page-size="pagination.pageSize"
  70. :total="pagination.total"
  71. layout="total, prev, pager, next"
  72. @current-change="loadList"
  73. />
  74. </div>
  75. </el-card>
  76. <!-- 新增/编辑弹窗 -->
  77. <el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px" destroy-on-close>
  78. <el-form :model="form" label-width="80px">
  79. <el-form-item label="用户名" required>
  80. <el-input v-model="form.username" placeholder="请输入用户名" :disabled="!!form.id" />
  81. </el-form-item>
  82. <el-form-item v-if="!form.id" label="密码" required>
  83. <el-input v-model="form.password" type="password" placeholder="请输入密码" show-password />
  84. </el-form-item>
  85. <el-form-item label="姓名">
  86. <el-input v-model="form.nickname" placeholder="请输入姓名" />
  87. </el-form-item>
  88. <el-form-item label="邮箱">
  89. <el-input v-model="form.email" type="email" placeholder="请输入邮箱" />
  90. </el-form-item>
  91. <el-form-item label="手机号">
  92. <el-input v-model="form.phone" placeholder="请输入手机号" />
  93. </el-form-item>
  94. <el-form-item label="角色">
  95. <el-select v-model="form.role_id" placeholder="请选择角色" style="width:100%;">
  96. <el-option v-for="role in roles" :key="role.id" :label="role.name" :value="role.id" />
  97. </el-select>
  98. </el-form-item>
  99. <el-form-item label="状态">
  100. <el-switch v-model="form.status" :active-value="1" :inactive-value="0" />
  101. </el-form-item>
  102. </el-form>
  103. <template #footer>
  104. <el-button @click="dialogVisible = false">取消</el-button>
  105. <el-button type="primary" @click="saveUser" :loading="saving">确定</el-button>
  106. </template>
  107. </el-dialog>
  108. <!-- 重置密码弹窗 -->
  109. <el-dialog v-model="resetPwdVisible" title="重置密码" width="400px" destroy-on-close>
  110. <el-form label-width="80px">
  111. <el-form-item label="新密码" required>
  112. <el-input v-model="newPassword" type="password" placeholder="请输入新密码" show-password />
  113. </el-form-item>
  114. </el-form>
  115. <template #footer>
  116. <el-button @click="resetPwdVisible = false">取消</el-button>
  117. <el-button type="primary" @click="resetPassword" :loading="resetPwdSaving">确定</el-button>
  118. </template>
  119. </el-dialog>
  120. </div>
  121. {% endblock %}
  122. {% block js %}
  123. <script>
  124. const rolesData = {{ roles | dump | safe }};
  125. const { createApp, ref, reactive, onMounted } = Vue;
  126. const app = createApp({
  127. delimiters: ['${', '}'],
  128. setup() {
  129. const keyword = ref('');
  130. const roleFilter = ref('');
  131. const statusFilter = ref('');
  132. const loading = ref(false);
  133. const tableData = ref([]);
  134. const pagination = reactive({ page: 1, pageSize: 10, total: 0 });
  135. const roles = ref(rolesData || []);
  136. // 弹窗
  137. const dialogVisible = ref(false);
  138. const dialogTitle = ref('新增用户');
  139. const saving = ref(false);
  140. const form = reactive({ id: null, username: '', password: '', nickname: '', email: '', phone: '', role_id: '', status: 1 });
  141. // 重置密码弹窗
  142. const resetPwdVisible = ref(false);
  143. const resetPwdSaving = ref(false);
  144. const newPassword = ref('');
  145. const resetUserId = ref(null);
  146. // 加载列表
  147. async function loadList(page) {
  148. if (typeof page === 'number') pagination.page = page;
  149. loading.value = true;
  150. try {
  151. const params = new URLSearchParams({
  152. page: pagination.page,
  153. pageSize: pagination.pageSize,
  154. keyword: keyword.value,
  155. role_id: roleFilter.value,
  156. status: statusFilter.value
  157. });
  158. const res = await fetch('/admin/system/user/list?' + params).then(r => r.json());
  159. if (res.code === 0) {
  160. tableData.value = res.data.data || [];
  161. pagination.total = res.data.count || 0;
  162. } else {
  163. ElementPlus.ElMessage.error(res.msg || '加载失败');
  164. }
  165. } finally {
  166. loading.value = false;
  167. }
  168. }
  169. // 重置筛选
  170. function resetFilter() {
  171. keyword.value = '';
  172. roleFilter.value = '';
  173. statusFilter.value = '';
  174. pagination.page = 1;
  175. loadList();
  176. }
  177. // 显示新增弹窗
  178. function showAddModal() {
  179. dialogTitle.value = '新增用户';
  180. Object.assign(form, { id: null, username: '', password: '', nickname: '', email: '', phone: '', role_id: '', status: 1 });
  181. dialogVisible.value = true;
  182. }
  183. // 编辑用户
  184. async function editUser(row) {
  185. const res = await fetch('/admin/system/user/detail?id=' + row.id).then(r => r.json());
  186. if (res.code !== 0) {
  187. ElementPlus.ElMessage.error(res.msg || '获取失败');
  188. return;
  189. }
  190. const user = res.data;
  191. dialogTitle.value = '编辑用户';
  192. Object.assign(form, {
  193. id: user.id,
  194. username: user.username,
  195. password: '',
  196. nickname: user.nickname || '',
  197. email: user.email || '',
  198. phone: user.phone || '',
  199. role_id: user.role_id || '',
  200. status: user.status
  201. });
  202. dialogVisible.value = true;
  203. }
  204. // 保存用户
  205. async function saveUser() {
  206. if (!form.username.trim()) {
  207. ElementPlus.ElMessage.warning('用户名不能为空');
  208. return;
  209. }
  210. if (!form.id && !form.password) {
  211. ElementPlus.ElMessage.warning('密码不能为空');
  212. return;
  213. }
  214. saving.value = true;
  215. try {
  216. const url = form.id ? '/admin/system/user/edit' : '/admin/system/user/add';
  217. const body = {
  218. username: form.username,
  219. password: form.password,
  220. nickname: form.nickname,
  221. email: form.email,
  222. phone: form.phone,
  223. role_id: form.role_id,
  224. status: form.status
  225. };
  226. if (form.id) body.id = form.id;
  227. const res = await fetch(url, {
  228. method: 'POST',
  229. headers: { 'Content-Type': 'application/json' },
  230. body: JSON.stringify(body)
  231. }).then(r => r.json());
  232. if (res.code === 0) {
  233. ElementPlus.ElMessage.success('保存成功');
  234. dialogVisible.value = false;
  235. loadList();
  236. } else {
  237. ElementPlus.ElMessage.error(res.msg || '保存失败');
  238. }
  239. } finally {
  240. saving.value = false;
  241. }
  242. }
  243. // 显示重置密码弹窗
  244. function showResetModal(row) {
  245. resetUserId.value = row.id;
  246. newPassword.value = '';
  247. resetPwdVisible.value = true;
  248. }
  249. // 重置密码
  250. async function resetPassword() {
  251. if (!newPassword.value) {
  252. ElementPlus.ElMessage.warning('请输入新密码');
  253. return;
  254. }
  255. resetPwdSaving.value = true;
  256. try {
  257. const res = await fetch('/admin/system/user/resetPassword', {
  258. method: 'POST',
  259. headers: { 'Content-Type': 'application/json' },
  260. body: JSON.stringify({ id: resetUserId.value, password: newPassword.value })
  261. }).then(r => r.json());
  262. if (res.code === 0) {
  263. ElementPlus.ElMessage.success('密码重置成功');
  264. resetPwdVisible.value = false;
  265. } else {
  266. ElementPlus.ElMessage.error(res.msg || '重置失败');
  267. }
  268. } finally {
  269. resetPwdSaving.value = false;
  270. }
  271. }
  272. // 切换状态
  273. async function toggleStatus(row) {
  274. try {
  275. await ElementPlus.ElMessageBox.confirm('确定要切换该用户状态吗?', '提示', { type: 'warning' });
  276. const res = await fetch('/admin/system/user/toggleStatus', {
  277. method: 'POST',
  278. headers: { 'Content-Type': 'application/json' },
  279. body: JSON.stringify({ id: row.id })
  280. }).then(r => r.json());
  281. if (res.code === 0) {
  282. ElementPlus.ElMessage.success('操作成功');
  283. loadList();
  284. } else {
  285. ElementPlus.ElMessage.error(res.msg || '操作失败');
  286. }
  287. } catch {}
  288. }
  289. onMounted(() => loadList());
  290. return {
  291. keyword, roleFilter, statusFilter, loading, tableData, pagination, roles,
  292. dialogVisible, dialogTitle, saving, form,
  293. resetPwdVisible, resetPwdSaving, newPassword,
  294. loadList, resetFilter, showAddModal, editUser, saveUser,
  295. showResetModal, resetPassword, toggleStatus
  296. };
  297. }
  298. });
  299. app.use(ElementPlus, { locale: ElementPlusLocaleZhCn });
  300. app.mount('#userApp');
  301. </script>
  302. {% endblock %}