init
This commit is contained in:
@@ -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
@@ -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>
|
||||
`
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user