|
- {% extends "../layout.html" %}
-
- {% block title %}用户管理{% endblock %}
-
- {% block content %}
- <div id="userApp" v-cloak>
- <!-- 搜索栏 -->
- <el-card shadow="never" class="mb-4">
- <el-form :inline="true" @submit.prevent="loadList()" class="flex items-center flex-wrap gap-2">
- <el-form-item label="关键词" class="!mb-0">
- <el-input v-model="keyword" placeholder="用户名/姓名..." clearable style="width:180px;" />
- </el-form-item>
- <el-form-item label="角色" class="!mb-0">
- <el-select v-model="roleFilter" placeholder="全部" clearable style="width:120px;">
- <el-option v-for="role in roles" :key="role.id" :label="role.name" :value="role.id" />
- </el-select>
- </el-form-item>
- <el-form-item label="状态" class="!mb-0">
- <el-select v-model="statusFilter" placeholder="全部" clearable style="width:100px;">
- <el-option label="启用" :value="1"></el-option>
- <el-option label="禁用" :value="0"></el-option>
- </el-select>
- </el-form-item>
- <el-form-item class="!mb-0">
- <el-button type="primary" @click="loadList()">搜索</el-button>
- <el-button @click="resetFilter">重置</el-button>
- </el-form-item>
- </el-form>
- </el-card>
-
- <!-- 用户列表 -->
- <el-card shadow="never">
- <template #header>
- <el-button type="primary" @click="showAddModal">+ 新增用户</el-button>
- </template>
-
- <el-table :data="tableData" v-loading="loading" stripe border>
- <el-table-column prop="id" label="ID" width="60" ></el-table-column>
- <el-table-column prop="username" label="用户名" ></el-table-column>
- <el-table-column prop="nickname" label="姓名">
- <template #default="{ row }">${ row.nickname || '-' }</template>
- </el-table-column>
- <el-table-column prop="role_name" label="角色" width="120">
- <template #default="{ row }">
- <el-tag type="primary" size="small">${ row.role_name || '-' }</el-tag>
- </template>
- </el-table-column>
- <el-table-column prop="email" label="邮箱" width="180">
- <template #default="{ row }">${ row.email || '-' }</template>
- </el-table-column>
- <el-table-column prop="status" label="状态" width="120" align="center">
- <template #default="{ row }">
- <el-tag v-if="row.is_locked" type="danger" size="small">已锁定</el-tag>
- <el-tag v-else-if="row.status === 1" type="success" size="small">启用</el-tag>
- <el-tag v-else type="info" size="small">禁用</el-tag>
- </template>
- </el-table-column>
- <el-table-column prop="last_login_time" label="最后登录" width="160">
- <template #default="{ row }">${ row.last_login_time || '-' }</template>
- </el-table-column>
- <el-table-column label="操作" width="260" align="center">
- <template #default="{ row }">
- <el-button type="primary" link @click="editUser(row)">编辑</el-button>
- <el-button type="primary" link @click="showResetModal(row)">重置密码</el-button>
- <el-button v-if="row.is_locked" type="warning" link @click="unlockUser(row)">解锁</el-button>
- <el-button :type="row.status === 1 ? 'danger' : 'success'" link @click="toggleStatus(row)">
- ${ row.status === 1 ? '禁用' : '启用' }
- </el-button>
- </template>
- </el-table-column>
- </el-table>
-
- <div class="flex justify-end mt-4">
- <el-pagination
- v-model:current-page="pagination.page"
- :page-size="pagination.pageSize"
- :total="pagination.total"
- layout="total, prev, pager, next"
- @current-change="loadList"
- />
- </div>
- </el-card>
-
- <!-- 新增/编辑弹窗 -->
- <el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px" destroy-on-close draggable :close-on-click-modal="false">
- <el-form :model="form" label-width="80px">
- <el-form-item label="用户名" required>
- <el-input v-model="form.username" placeholder="请输入用户名" :disabled="!!form.id" />
- </el-form-item>
- <el-form-item v-if="!form.id" label="密码" required>
- <el-input v-model="form.password" type="password" placeholder="至少8位,含大小写字母/数字中两种" show-password />
- <div v-if="form.password" style="margin-top:4px;">
- <div style="display:flex;gap:4px;margin-bottom:2px;">
- <span :style="{flex:1,height:'4px',borderRadius:'2px',background: pwdStrength>=1?pwdStrengthColor:'#e4e7ed'}"></span>
- <span :style="{flex:1,height:'4px',borderRadius:'2px',background: pwdStrength>=2?pwdStrengthColor:'#e4e7ed'}"></span>
- <span :style="{flex:1,height:'4px',borderRadius:'2px',background: pwdStrength>=3?pwdStrengthColor:'#e4e7ed'}"></span>
- </div>
- <span :style="{fontSize:'12px',color:pwdStrengthColor}">${ pwdStrengthText }</span>
- </div>
- </el-form-item>
- <el-form-item label="姓名">
- <el-input v-model="form.nickname" placeholder="请输入姓名" />
- </el-form-item>
- <el-form-item label="邮箱">
- <el-input v-model="form.email" type="email" placeholder="请输入邮箱" />
- </el-form-item>
- <el-form-item label="手机号">
- <el-input v-model="form.phone" placeholder="请输入手机号" />
- </el-form-item>
- <el-form-item label="角色">
- <el-select v-model="form.role_id" placeholder="请选择角色" style="width:100%;">
- <el-option v-for="role in roles" :key="role.id" :label="role.name" :value="role.id" />
- </el-select>
- </el-form-item>
- <el-form-item label="状态">
- <el-switch v-model="form.status" :active-value="1" :inactive-value="0" />
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="dialogVisible = false">取消</el-button>
- <el-button type="primary" @click="saveUser" :loading="saving">确定</el-button>
- </template>
- </el-dialog>
-
- <!-- 重置密码弹窗 -->
- <el-dialog v-model="resetPwdVisible" title="重置密码" width="400px" destroy-on-close draggable :close-on-click-modal="false">
- <el-form label-width="80px">
- <el-form-item label="新密码" required>
- <el-input v-model="newPassword" type="password" placeholder="至少8位,含大小写字母/数字中两种" show-password />
- <div v-if="newPassword" style="margin-top:4px;">
- <div style="display:flex;gap:4px;margin-bottom:2px;">
- <span :style="{flex:1,height:'4px',borderRadius:'2px',background: resetPwdStrength>=1?resetPwdStrengthColor:'#e4e7ed'}"></span>
- <span :style="{flex:1,height:'4px',borderRadius:'2px',background: resetPwdStrength>=2?resetPwdStrengthColor:'#e4e7ed'}"></span>
- <span :style="{flex:1,height:'4px',borderRadius:'2px',background: resetPwdStrength>=3?resetPwdStrengthColor:'#e4e7ed'}"></span>
- </div>
- <span :style="{fontSize:'12px',color:resetPwdStrengthColor}">${ resetPwdStrengthText }</span>
- </div>
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="resetPwdVisible = false">取消</el-button>
- <el-button type="primary" @click="resetPassword" :loading="resetPwdSaving">确定</el-button>
- </template>
- </el-dialog>
- </div>
- {% endblock %}
-
- {% block js %}
- <script>
- const rolesData = {{ roles | dump | safe }};
-
- const { createApp, ref, reactive, computed, onMounted } = Vue;
-
- const app = createApp({
- delimiters: ['${', '}'],
- setup() {
- const keyword = ref('');
- const roleFilter = ref('');
- const statusFilter = ref('');
- const loading = ref(false);
- const tableData = ref([]);
- const pagination = reactive({ page: 1, pageSize: 10, total: 0 });
- const roles = ref(rolesData || []);
-
- // 弹窗
- const dialogVisible = ref(false);
- const dialogTitle = ref('新增用户');
- const saving = ref(false);
- const form = reactive({ id: null, username: '', password: '', nickname: '', email: '', phone: '', role_id: '', status: 1 });
-
- // 重置密码弹窗
- const resetPwdVisible = ref(false);
- const resetPwdSaving = ref(false);
- const newPassword = ref('');
- const resetUserId = ref(null);
-
- // 密码强度计算
- function calcPwdStrength(pwd) {
- if (!pwd) return 0;
- var s = 0;
- if (pwd.length >= 8) s++;
- var types = 0;
- if (/[a-z]/.test(pwd)) types++;
- if (/[A-Z]/.test(pwd)) types++;
- if (/\d/.test(pwd)) types++;
- if (types >= 2) s++;
- if (types >= 3 && pwd.length >= 10) s++;
- return s;
- }
- const pwdStrength = computed(function() { return calcPwdStrength(form.password); });
- const pwdStrengthColor = computed(function() { return pwdStrength.value <= 1 ? '#f56c6c' : pwdStrength.value === 2 ? '#e6a23c' : '#67c23a'; });
- const pwdStrengthText = computed(function() { return pwdStrength.value <= 1 ? '弱' : pwdStrength.value === 2 ? '中' : '强'; });
- const resetPwdStrength = computed(function() { return calcPwdStrength(newPassword.value); });
- const resetPwdStrengthColor = computed(function() { return resetPwdStrength.value <= 1 ? '#f56c6c' : resetPwdStrength.value === 2 ? '#e6a23c' : '#67c23a'; });
- const resetPwdStrengthText = computed(function() { return resetPwdStrength.value <= 1 ? '弱' : resetPwdStrength.value === 2 ? '中' : '强'; });
-
- // 前端密码校验
- function validatePassword(pwd) {
- if (!pwd || pwd.length < 8) return '密码长度不能少于8位';
- var types = 0;
- if (/[a-z]/.test(pwd)) types++;
- if (/[A-Z]/.test(pwd)) types++;
- if (/\d/.test(pwd)) types++;
- if (types < 2) return '密码需包含大写字母、小写字母、数字中的至少两种';
- return null;
- }
-
- // 加载列表
- async function loadList(page) {
- if (typeof page === 'number') pagination.page = page;
- loading.value = true;
- try {
- const params = new URLSearchParams({
- page: pagination.page,
- pageSize: pagination.pageSize,
- keyword: keyword.value,
- role_id: roleFilter.value,
- status: statusFilter.value
- });
- const res = await fetch('/admin/system/user/list?' + params).then(r => r.json());
- if (res.code === 0) {
- var today = new Date().toISOString().slice(0, 10);
- (res.data.data || []).forEach(function(item) {
- var failDate = item.login_fail_date ? item.login_fail_date.slice(0, 10) : null;
- item.is_locked = (failDate === today && item.login_fail_count >= 5);
- });
- tableData.value = res.data.data || [];
- pagination.total = res.data.count || 0;
- } else {
- ElementPlus.ElMessage.error(res.msg || '加载失败');
- }
- } finally {
- loading.value = false;
- }
- }
-
- // 重置筛选
- function resetFilter() {
- keyword.value = '';
- roleFilter.value = '';
- statusFilter.value = '';
- pagination.page = 1;
- loadList();
- }
-
- // 显示新增弹窗
- function showAddModal() {
- dialogTitle.value = '新增用户';
- Object.assign(form, { id: null, username: '', password: '', nickname: '', email: '', phone: '', role_id: '', status: 1 });
- dialogVisible.value = true;
- }
-
- // 编辑用户
- async function editUser(row) {
- const res = await fetch('/admin/system/user/detail?id=' + row.id).then(r => r.json());
- if (res.code !== 0) {
- ElementPlus.ElMessage.error(res.msg || '获取失败');
- return;
- }
- const user = res.data;
- dialogTitle.value = '编辑用户';
- Object.assign(form, {
- id: user.id,
- username: user.username,
- password: '',
- nickname: user.nickname || '',
- email: user.email || '',
- phone: user.phone || '',
- role_id: user.role_id || '',
- status: user.status
- });
- dialogVisible.value = true;
- }
-
- // 保存用户
- async function saveUser() {
- if (!form.username.trim()) {
- ElementPlus.ElMessage.warning('用户名不能为空');
- return;
- }
- if (!form.id && !form.password) {
- ElementPlus.ElMessage.warning('密码不能为空');
- return;
- }
- if (!form.id) {
- var pwdErr = validatePassword(form.password);
- if (pwdErr) { ElementPlus.ElMessage.warning(pwdErr); return; }
- }
- saving.value = true;
- try {
- const url = form.id ? '/admin/system/user/edit' : '/admin/system/user/add';
- const body = {
- username: form.username,
- password: form.password,
- nickname: form.nickname,
- email: form.email,
- phone: form.phone,
- role_id: form.role_id,
- status: form.status
- };
- if (form.id) body.id = form.id;
-
- const res = await fetch(url, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(body)
- }).then(r => r.json());
-
- if (res.code === 0) {
- ElementPlus.ElMessage.success('保存成功');
- dialogVisible.value = false;
- loadList();
- } else {
- ElementPlus.ElMessage.error(res.msg || '保存失败');
- }
- } finally {
- saving.value = false;
- }
- }
-
- // 显示重置密码弹窗
- function showResetModal(row) {
- resetUserId.value = row.id;
- newPassword.value = '';
- resetPwdVisible.value = true;
- }
-
- // 重置密码
- async function resetPassword() {
- if (!newPassword.value) {
- ElementPlus.ElMessage.warning('请输入新密码');
- return;
- }
- var pwdErr = validatePassword(newPassword.value);
- if (pwdErr) { ElementPlus.ElMessage.warning(pwdErr); return; }
- resetPwdSaving.value = true;
- try {
- const res = await fetch('/admin/system/user/resetPassword', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ id: resetUserId.value, password: newPassword.value })
- }).then(r => r.json());
-
- if (res.code === 0) {
- ElementPlus.ElMessage.success('密码重置成功');
- resetPwdVisible.value = false;
- } else {
- ElementPlus.ElMessage.error(res.msg || '重置失败');
- }
- } finally {
- resetPwdSaving.value = false;
- }
- }
-
- // 解锁用户
- async function unlockUser(row) {
- try {
- await ElementPlus.ElMessageBox.confirm('确定要解锁该用户吗?', '提示', { type: 'warning' });
- const res = await fetch('/admin/system/user/unlock', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ id: row.id })
- }).then(r => r.json());
-
- if (res.code === 0) {
- ElementPlus.ElMessage.success('解锁成功');
- loadList();
- } else {
- ElementPlus.ElMessage.error(res.msg || '解锁失败');
- }
- } catch {}
- }
-
- // 切换状态
- async function toggleStatus(row) {
- try {
- await ElementPlus.ElMessageBox.confirm('确定要切换该用户状态吗?', '提示', { type: 'warning' });
- const res = await fetch('/admin/system/user/toggleStatus', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ id: row.id })
- }).then(r => r.json());
-
- if (res.code === 0) {
- ElementPlus.ElMessage.success('操作成功');
- loadList();
- } else {
- ElementPlus.ElMessage.error(res.msg || '操作失败');
- }
- } catch {}
- }
-
- onMounted(() => loadList());
-
- return {
- keyword, roleFilter, statusFilter, loading, tableData, pagination, roles,
- dialogVisible, dialogTitle, saving, form,
- resetPwdVisible, resetPwdSaving, newPassword,
- pwdStrength, pwdStrengthColor, pwdStrengthText,
- resetPwdStrength, resetPwdStrengthColor, resetPwdStrengthText,
- loadList, resetFilter, showAddModal, editUser, saveUser,
- showResetModal, resetPassword, unlockUser, toggleStatus
- };
- }
- });
-
- app.use(ElementPlus, { locale: ElementPlusLocaleZhCn });
- app.mount('#userApp');
- </script>
- {% endblock %}
|