This commit is contained in:
2026-03-04 01:08:00 +08:00
parent 006a30ea0c
commit 24c799f08e
6 changed files with 960 additions and 214 deletions
+140
View File
@@ -870,6 +870,141 @@ a:hover {
color: var(--text-secondary);
}
/* ================================
模态框
================================ */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
animation: fadeIn 0.2s ease;
}
.modal-content {
background: var(--bg-primary);
border-radius: var(--radius-lg);
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow: auto;
animation: slideUp 0.3s ease;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-6);
border-bottom: 1px solid var(--border-light);
}
.modal-header h2 {
font-size: var(--text-xl);
font-weight: 600;
}
.modal-close {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border: none;
background: transparent;
color: var(--text-secondary);
font-size: 24px;
cursor: pointer;
border-radius: var(--radius-md);
transition: all 0.2s;
}
.modal-close:hover {
background: var(--gray-100);
color: var(--text-primary);
}
.modal-body {
padding: var(--space-6);
}
.modal-footer {
display: flex;
gap: var(--space-3);
justify-content: flex-end;
padding: var(--space-6);
border-top: 1px solid var(--border-light);
}
/* ================================
复选框组
================================ */
.checkbox-group {
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
}
.checkbox-label {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
background: var(--gray-100);
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s;
}
.checkbox-label:hover {
background: var(--gray-200);
}
.checkbox-label input[type="checkbox"] {
width: 16px;
height: 16px;
cursor: pointer;
}
/* ================================
按钮变体
================================ */
.btn-error {
background: var(--error);
color: white;
border: none;
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-md);
font-size: var(--text-sm);
cursor: pointer;
transition: all 0.2s;
}
.btn-error:hover {
background: #dc2626;
}
.btn-secondary {
background: var(--gray-200);
color: var(--text-primary);
border: none;
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-md);
font-size: var(--text-sm);
cursor: pointer;
transition: all 0.2s;
}
.btn-secondary:hover {
background: var(--gray-300);
}
/* ================================
进度条
================================ */
@@ -1489,6 +1624,11 @@ a:hover {
color: var(--primary-700);
}
.auth-tip {
color: var(--text-tertiary);
font-size: var(--text-sm);
}
.page-container {
max-width: 1200px;
margin: 0 auto;
+174 -107
View File
@@ -271,11 +271,8 @@ const LoginView = {
setup() {
const router = useRouter();
const state = reactive({
isLogin: true,
username: '',
password: '',
email: '',
full_name: '',
loading: false,
error: ''
});
@@ -296,51 +293,24 @@ const LoginView = {
state.error = '';
try {
if (state.isLogin) {
const res = await fetch('/api/auth/login/json', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: state.username,
password: state.password
})
});
if (!res.ok) {
const error = await res.json();
throw new Error(error.detail || '登录失败');
}
const data = await res.json();
saveAuth(data.access_token, data.user);
addNotification('登录成功', 'success');
router.push('/');
} else {
if (!state.email) {
state.error = '请填写邮箱';
state.loading = false;
return;
}
const res = await fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: state.username,
password: state.password,
email: state.email,
full_name: state.full_name || null
})
});
if (!res.ok) {
const error = await res.json();
throw new Error(error.detail || '注册失败');
}
addNotification('注册成功,请登录', 'success');
state.isLogin = true;
const res = await fetch('/api/auth/login/json', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: state.username,
password: state.password
})
});
if (!res.ok) {
const error = await res.json();
throw new Error(error.detail || '登录失败');
}
const data = await res.json();
saveAuth(data.access_token, data.user);
addNotification('登录成功', 'success');
router.push('/');
} catch (e) {
state.error = e.message;
addNotification(e.message, 'error');
@@ -356,8 +326,8 @@ const LoginView = {
<div class="auth-card">
<div class="auth-header">
<div class="auth-logo">◆</div>
<h1>{{ state.isLogin ? '登录' : '注册' }}</h1>
<p>{{ state.isLogin ? '登录到 Gemold 系统' : '创建新账户' }}</p>
<h1>登录</h1>
<p>登录到 Gemold 系统</p>
</div>
<form @submit.prevent="handleSubmit" class="auth-form">
@@ -371,47 +341,25 @@ const LoginView = {
/>
</div>
<div class="form-group" v-if="!state.isLogin">
<label>邮箱</label>
<input
v-model="state.email"
type="email"
placeholder="请输入邮箱"
autocomplete="email"
/>
</div>
<div class="form-group" v-if="!state.isLogin">
<label>姓名</label>
<input
v-model="state.full_name"
type="text"
placeholder="请输入姓名(可选)"
/>
</div>
<div class="form-group">
<label>密码</label>
<input
v-model="state.password"
type="password"
placeholder="请输入密码"
:autocomplete="state.isLogin ? 'current-password' : 'new-password'"
autocomplete="current-password"
/>
</div>
<div v-if="state.error" class="error-message">{{ state.error }}</div>
<button type="submit" class="btn-primary btn-full" :disabled="state.loading">
{{ state.loading ? '处理中...' : (state.isLogin ? '登录' : '注册') }}
{{ state.loading ? '登录中...' : '登录' }}
</button>
</form>
<div class="auth-footer">
<span>{{ state.isLogin ? '没有账户?' : '已有账户?' }}</span>
<a @click="state.isLogin = !state.isLogin; state.error = ''">
{{ state.isLogin ? '立即注册' : '立即登录' }}
</a>
<p class="auth-tip">如需开通账号,请联系管理员</p>
</div>
</div>
</div>
@@ -566,7 +514,17 @@ const UsersView = {
const router = useRouter();
const state = reactive({
users: [],
loading: true
roles: [],
loading: true,
showUserModal: false,
editingUser: null,
userForm: {
username: '',
email: '',
password: '',
full_name: '',
role_ids: []
}
});
const loadUsers = async () => {
@@ -579,43 +537,123 @@ const UsersView = {
}
};
const toggleActive = async (user) => {
const loadRoles = async () => {
try {
const updated = await apiRequest(`/api/auth/users/${user.id}/toggle-active`, { method: 'PUT' });
const index = state.users.findIndex(u => u.id === user.id);
if (index > -1) state.users[index] = updated;
addNotification(`用户 ${user.username} 已${updated.is_active ? '启用' : '禁用'}`, 'success');
state.roles = await apiRequest('/api/auth/roles');
} catch (e) {
handleApiError(e, '切换用户状态');
handleApiError(e, '加载角色列表');
}
};
const toggleAdmin = async (user) => {
const openUserModal = (user = null) => {
state.editingUser = user;
if (user) {
state.userForm = {
username: user.username,
email: user.email,
password: '',
full_name: user.full_name || '',
role_ids: user.roles.map(r => {
const role = state.roles.find(role => role.code === r);
return role ? role.id : null;
}).filter(id => id !== null)
};
} else {
state.userForm = {
username: '',
email: '',
password: '',
full_name: '',
role_ids: []
};
}
state.showUserModal = true;
};
const saveUser = async () => {
if (!state.userForm.username || !state.userForm.email) {
addNotification('请填写用户名和邮箱', 'error');
return;
}
if (!state.editingUser && !state.userForm.password) {
addNotification('请填写密码', 'error');
return;
}
try {
const updated = await apiRequest(`/api/auth/users/${user.id}/toggle-admin`, { method: 'PUT' });
const index = state.users.findIndex(u => u.id === user.id);
if (index > -1) state.users[index] = updated;
addNotification(`用户 ${user.username} ${updated.is_superuser ? '已设为管理员' : '已取消管理员'}`, 'success');
if (state.editingUser) {
await apiRequest(`/api/auth/users/${state.editingUser.id}`, {
method: 'PUT',
body: JSON.stringify({
email: state.userForm.email,
full_name: state.userForm.full_name || null,
role_ids: state.userForm.role_ids
})
});
addNotification('用户更新成功', 'success');
} else {
await apiRequest('/api/auth/users', {
method: 'POST',
body: JSON.stringify(state.userForm)
});
addNotification('用户创建成功', 'success');
}
state.showUserModal = false;
loadUsers();
} catch (e) {
handleApiError(e, '切换管理员权限');
handleApiError(e, '保存用户');
}
};
onMounted(() => {
const deleteUser = async (user) => {
if (!confirm(`确定要删除用户 ${user.username} 吗?`)) return;
try {
await apiRequest(`/api/auth/users/${user.id}`, { method: 'DELETE' });
addNotification('用户已删除', 'success');
loadUsers();
} catch (e) {
handleApiError(e, '删除用户');
}
};
const resetPassword = async (user) => {
const newPassword = prompt(`请输入 ${user.username} 的新密码:`);
if (!newPassword || newPassword.length < 6) {
addNotification('密码长度至少6位', 'error');
return;
}
try {
await apiRequest(`/api/auth/users/${user.id}/reset-password`, {
method: 'PUT',
body: JSON.stringify(newPassword)
});
addNotification('密码已重置', 'success');
} catch (e) {
handleApiError(e, '重置密码');
}
};
onMounted(async () => {
if (!appState.user?.is_superuser) {
router.push('/');
return;
}
await loadRoles();
loadUsers();
});
return { state, appState, toggleActive, toggleAdmin, formatDateTime };
return { state, appState, openUserModal, saveUser, deleteUser, resetPassword, formatDateTime };
},
template: `
<div class="page-container">
<div class="page-header">
<h1>用户管理</h1>
<p>管理系统用户和权限</p>
<div>
<h1>用户管理</h1>
<p>管理系统用户和权限</p>
</div>
<button class="btn-primary" @click="openUserModal()">+ 添加用户</button>
</div>
<div v-if="state.loading" class="loading-state">
@@ -647,33 +685,62 @@ const UsersView = {
</span>
</td>
<td>
<span :class="['badge', user.is_superuser ? 'badge-primary' : 'badge-neutral']">
{{ user.is_superuser ? '管理员' : '普通用户' }}
<span v-for="role in user.roles" :key="role" class="badge badge-info" style="margin-right: 4px;">
{{ role }}
</span>
</td>
<td>{{ formatDateTime(user.created_at) }}</td>
<td>
<div class="action-buttons">
<button
v-if="user.id !== appState.user?.id"
:class="['btn-sm', user.is_active ? 'btn-warning' : 'btn-success']"
@click="toggleActive(user)"
>
{{ user.is_active ? '禁用' : '启用' }}
</button>
<button
v-if="user.id !== appState.user?.id"
:class="['btn-sm', user.is_superuser ? 'btn-warning' : 'btn-primary']"
@click="toggleAdmin(user)"
>
{{ user.is_superuser ? '取消管理员' : '设为管理员' }}
</button>
<button class="btn-sm btn-primary" @click="openUserModal(user)">编辑</button>
<button class="btn-sm btn-warning" @click="resetPassword(user)">重置密码</button>
<button v-if="user.id !== appState.user?.id" class="btn-sm btn-error" @click="deleteUser(user)">删除</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="state.showUserModal" class="modal-overlay" @click.self="state.showUserModal = false">
<div class="modal-content">
<div class="modal-header">
<h2>{{ state.editingUser ? '编辑用户' : '添加用户' }}</h2>
<button class="modal-close" @click="state.showUserModal = false">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>用户名</label>
<input v-model="state.userForm.username" type="text" :disabled="!!state.editingUser" />
</div>
<div class="form-group">
<label>邮箱</label>
<input v-model="state.userForm.email" type="email" />
</div>
<div class="form-group" v-if="!state.editingUser">
<label>密码</label>
<input v-model="state.userForm.password" type="password" />
</div>
<div class="form-group">
<label>姓名</label>
<input v-model="state.userForm.full_name" type="text" />
</div>
<div class="form-group">
<label>角色</label>
<div class="checkbox-group">
<label v-for="role in state.roles" :key="role.id" class="checkbox-label">
<input type="checkbox" :value="role.id" v-model="state.userForm.role_ids" />
{{ role.name }}
</label>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" @click="state.showUserModal = false">取消</button>
<button class="btn-primary" @click="saveUser">保存</button>
</div>
</div>
</div>
</div>
`
};