mirror of https://github.com/veypi/OneAuth.git
refactor: 重构用户/组织/OAuth页面UI
parent
d85cb6ae84
commit
4101daeed3
@ -1,128 +1,241 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta name="description" content="OAuth Apps">
|
||||
<title>{{ $t('nav.oauth') }}</title>
|
||||
<style>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.app-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
.app-card {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.app-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.app-name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.app-id {
|
||||
font-size: 12px;
|
||||
color: var(--text-color-secondary);
|
||||
font-family: monospace;
|
||||
background: var(--bg-color-tertiary);
|
||||
padding: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
.app-redirect {
|
||||
font-size: 14px;
|
||||
color: var(--text-color-secondary);
|
||||
word-break: break-all;
|
||||
}
|
||||
.btn-create {
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius-md);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-delete {
|
||||
background-color: var(--color-danger);
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
<meta name="description" content="OAuth Apps">
|
||||
<title>{{ $t('nav.oauth') }}</title>
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-lg);
|
||||
padding: var(--spacing-lg);
|
||||
box-sizing: border-box;
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-md);
|
||||
padding-bottom: var(--spacing-md);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.app-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: var(--spacing-lg);
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.app-card {
|
||||
background: var(--bg-color-secondary);
|
||||
padding: var(--spacing-lg);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
border: 1px solid var(--border-color);
|
||||
transition: all var(--transition-base);
|
||||
}
|
||||
|
||||
.app-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.app-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-color-tertiary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-color-secondary);
|
||||
font-family: monospace;
|
||||
background: var(--bg-color-tertiary);
|
||||
padding: 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-sm);
|
||||
margin-top: auto;
|
||||
padding-top: var(--spacing-md);
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="page-header">
|
||||
<h1>{{ $t('nav.oauth') }}</h1>
|
||||
<button class="btn-create" @click="createApp">New App</button>
|
||||
<div class="page-header">
|
||||
<div class="page-title">
|
||||
<i class="fas fa-key" style="color: var(--color-primary);"></i>
|
||||
{{ $t('nav.oauth') }}
|
||||
</div>
|
||||
|
||||
<div class="app-grid">
|
||||
<div class="app-card" v-for="app in apps">
|
||||
<div class="app-header">
|
||||
<div class="app-name">{{ app.name }}</div>
|
||||
<button class="btn-delete" @click="deleteApp(app.id)">Delete</button>
|
||||
</div>
|
||||
<div class="app-id">ID: {{ app.client_id }}</div>
|
||||
<div class="app-redirect">Callback: {{ app.redirect_uri }}</div>
|
||||
</div>
|
||||
<v-btn color="primary" :click="openCreateModal">
|
||||
<i class="fas fa-plus"></i>
|
||||
{{ $t('oauth.create_app') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div class="app-grid">
|
||||
<div class="app-card" v-for="app in apps">
|
||||
<div class="app-header">
|
||||
<div class="app-name">{{ app.name }}</div>
|
||||
<div class="info-value" style="font-size: 10px; background: transparent; padding: 0;">{{ app.id }}</div>
|
||||
</div>
|
||||
|
||||
<div class="info-row">
|
||||
<span class="info-label">Client ID</span>
|
||||
<div class="info-value">{{ app.client_id }}</div>
|
||||
</div>
|
||||
|
||||
<div class="info-row">
|
||||
<span class="info-label">Redirect URI</span>
|
||||
<div class="info-value">{{ app.redirect_uri }}</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<v-btn icon size="sm" variant="outline" :click="() => openEditModal(app)" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</v-btn>
|
||||
<v-btn icon size="sm" color="danger" variant="outline" :click="() => deleteApp(app)" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create/Edit Dialog -->
|
||||
<v-dialog v:visible="showModal"
|
||||
:title="isEdit ? $t('oauth.edit') : $t('oauth.create')">
|
||||
<form @submit.prevent="saveApp" style="display: grid; gap: 16px;">
|
||||
<v-input label="App Name" required v:value="formData.name" placeholder="e.g. My Awesome App"></v-input>
|
||||
<v-input label="Redirect URI" required v:value="formData.redirect_uri"
|
||||
placeholder="e.g. http://localhost:3000/callback"></v-input>
|
||||
</form>
|
||||
<div vslot="footer">
|
||||
<v-btn variant="outline" :click="closeModal">{{ $t('common.cancel') }}</v-btn>
|
||||
<v-btn color="primary" :click="saveApp">
|
||||
{{ isEdit ? $t('common.save') : $t('common.create') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-dialog>
|
||||
</body>
|
||||
<script setup>
|
||||
apps = [];
|
||||
|
||||
loadApps = async () => {
|
||||
try {
|
||||
const res = await $axios.get('/api/oauth/clients');
|
||||
apps = res || [];
|
||||
} catch (e) {
|
||||
$message.error(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
createApp = () => {
|
||||
$message.prompt("Enter App Name", "New App").then(async (name) => {
|
||||
if (!name) return;
|
||||
const uri = await $message.prompt("Enter Redirect URI", "http://localhost:3000/callback");
|
||||
if (!uri) return;
|
||||
|
||||
try {
|
||||
await $axios.post('/api/oauth/clients', { name: name, redirect_uri: uri });
|
||||
$message.success("Created");
|
||||
loadApps();
|
||||
} catch (e) {
|
||||
$message.error(e.message);
|
||||
}
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
deleteApp = async (id) => {
|
||||
try {
|
||||
await $message.confirm("Delete this app?");
|
||||
await $axios.delete(`/api/oauth/clients/${id}`);
|
||||
$message.success("Deleted");
|
||||
loadApps();
|
||||
} catch (e) {
|
||||
// Cancelled
|
||||
}
|
||||
};
|
||||
apps = [];
|
||||
showModal = false;
|
||||
isEdit = false;
|
||||
formData = {
|
||||
id: null,
|
||||
name: "",
|
||||
redirect_uri: ""
|
||||
};
|
||||
|
||||
loadApps = async () => {
|
||||
try {
|
||||
const res = await $axios.get('/api/oauth/clients');
|
||||
apps = res || [];
|
||||
} catch (e) {
|
||||
$message.error(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
openCreateModal = () => {
|
||||
isEdit = false;
|
||||
formData = { id: null, name: "", redirect_uri: "http://localhost:3000/callback" };
|
||||
showModal = true;
|
||||
};
|
||||
|
||||
openEditModal = (app) => {
|
||||
isEdit = true;
|
||||
formData = { ...app };
|
||||
showModal = true;
|
||||
};
|
||||
|
||||
closeModal = () => {
|
||||
showModal = false;
|
||||
};
|
||||
|
||||
saveApp = async () => {
|
||||
if (!formData.name || !formData.redirect_uri) {
|
||||
$message.error("Name and Redirect URI are required");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (isEdit) {
|
||||
await $axios.patch(`/api/oauth/clients/${formData.id}`, {
|
||||
name: formData.name,
|
||||
redirect_uri: formData.redirect_uri
|
||||
});
|
||||
$message.success("App updated");
|
||||
} else {
|
||||
await $axios.post('/api/oauth/clients', {
|
||||
name: formData.name,
|
||||
redirect_uri: formData.redirect_uri
|
||||
});
|
||||
$message.success("App created");
|
||||
}
|
||||
closeModal();
|
||||
loadApps();
|
||||
} catch (e) {
|
||||
$message.error(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
deleteApp = async (app) => {
|
||||
try {
|
||||
await $message.confirm(`Delete app "${app.name}"?`);
|
||||
await $axios.delete(`/api/oauth/clients/${app.id}`);
|
||||
$message.success("Deleted");
|
||||
loadApps();
|
||||
} catch (e) {
|
||||
// Cancelled
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<script>
|
||||
$data.loadApps();
|
||||
$data.loadApps();
|
||||
</script>
|
||||
|
||||
</html>
|
||||
|
||||
@ -1,110 +1,280 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta name="description" content="User Management">
|
||||
<title>{{ $t('nav.users') }}</title>
|
||||
<style>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: #fff;
|
||||
box-shadow: var(--shadow-sm);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
th, td {
|
||||
text-align: left;
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
th {
|
||||
background-color: var(--bg-color-tertiary);
|
||||
font-weight: 600;
|
||||
}
|
||||
.btn-action {
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.btn-edit {
|
||||
background-color: var(--color-info);
|
||||
color: white;
|
||||
}
|
||||
.btn-delete {
|
||||
background-color: var(--color-danger);
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
<meta name="description" content="User Management">
|
||||
<title>{{ $t('nav.users') }}</title>
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-lg);
|
||||
padding: var(--spacing-lg);
|
||||
box-sizing: border-box;
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-md);
|
||||
padding-bottom: var(--spacing-md);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
background: var(--bg-color-secondary);
|
||||
padding: 0 var(--spacing-md);
|
||||
border-radius: var(--radius-full);
|
||||
border: 1px solid var(--border-color);
|
||||
min-width: 320px;
|
||||
height: 40px;
|
||||
transition: all var(--transition-base);
|
||||
}
|
||||
|
||||
.search-box:focus-within {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary), transparent 85%);
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
border: none;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
font-size: var(--font-size-md);
|
||||
color: var(--text-color);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
background: var(--bg-color-secondary);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
text-align: left;
|
||||
padding: var(--spacing-md);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: var(--bg-color-tertiary);
|
||||
font-weight: 600;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
tr:hover td {
|
||||
background-color: var(--bg-color-tertiary);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
background-color: color-mix(in srgb, var(--color-success), transparent 85%);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.status-inactive {
|
||||
background-color: color-mix(in srgb, var(--text-color-disabled), transparent 85%);
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="page-header">
|
||||
<h1>{{ $t('nav.users') }}</h1>
|
||||
<button class="btn-action btn-edit" @click="createUser">Create User</button>
|
||||
<div class="page-header">
|
||||
<div class="page-title">
|
||||
<i class="fas fa-users" style="color: var(--color-primary);"></i>
|
||||
{{ $t('nav.users') }}
|
||||
</div>
|
||||
<div style="display: flex; gap: var(--spacing-md); align-items: center;">
|
||||
<div class="search-box">
|
||||
<i class="fas fa-search" style="color: var(--text-color-tertiary);"></i>
|
||||
<input type="text" v:value="searchQuery" :placeholder="$t('user.search_placeholder')">
|
||||
</div>
|
||||
<v-btn color="primary" :click="openCreateModal">
|
||||
<i class="fas fa-plus"></i>
|
||||
{{ $t('user.create') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Username</th>
|
||||
<th>Email</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="u in users">
|
||||
<td>{{ u.id }}</td>
|
||||
<td>{{ u.username }}</td>
|
||||
<td>{{ u.email }}</td>
|
||||
<td>{{ u.status || 'Active' }}</td>
|
||||
<td>
|
||||
<button class="btn-action btn-edit" @click="editUser(u)">Edit</button>
|
||||
<button class="btn-action btn-delete" @click="deleteUser(u.id)">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Username</th>
|
||||
<th>Email</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="u in filteredUsers">
|
||||
<td>{{ u.id }}</td>
|
||||
<td>{{ u.username }}</td>
|
||||
<td>{{ u.email }}</td>
|
||||
<td>
|
||||
<span class="status-badge" :class="u.status === 1 ? 'status-active' : 'status-inactive'">
|
||||
{{ u.status === 1 ? 'Active' : 'Inactive' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<v-btn icon size="sm" variant="outline" :click="() => openEditModal(u)" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</v-btn>
|
||||
<v-btn icon size="sm" color="danger" variant="outline" :click="() => deleteUser(u)" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</v-btn>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Create/Edit Dialog -->
|
||||
<v-dialog v:visible="showModal"
|
||||
:title="isEdit ? $t('user.edit') : $t('user.create')">
|
||||
<form @submit.prevent="saveUser" style="display: grid; gap: 16px;">
|
||||
<v-input label="Username" required v:value="formData.username" :disabled="isEdit"></v-input>
|
||||
<v-input label="Email" type="email" required v:value="formData.email"></v-input>
|
||||
<v-input label="Password" type="password" :required="!isEdit" v:value="formData.password"
|
||||
:placeholder="isEdit ? 'Leave blank to keep unchanged' : 'Enter password'"></v-input>
|
||||
</form>
|
||||
<div vslot="footer">
|
||||
<v-btn variant="outline" :click="closeModal">{{ $t('common.cancel') }}</v-btn>
|
||||
<v-btn color="primary" :click="saveUser">
|
||||
{{ isEdit ? $t('common.save') : $t('common.create') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-dialog>
|
||||
</body>
|
||||
<script setup>
|
||||
users = [];
|
||||
|
||||
loadUsers = async () => {
|
||||
try {
|
||||
const res = await $axios.get('/api/users');
|
||||
users = res || [];
|
||||
} catch (e) {
|
||||
$message.error(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
createUser = () => {
|
||||
$message.info("Create User Modal coming soon");
|
||||
};
|
||||
|
||||
editUser = (u) => {
|
||||
$message.info("Edit User " + u.username);
|
||||
};
|
||||
|
||||
deleteUser = async (id) => {
|
||||
try {
|
||||
await $message.confirm("Delete user?");
|
||||
await $axios.delete(`/api/users/${id}`);
|
||||
$message.success("Deleted");
|
||||
loadUsers();
|
||||
} catch (e) {
|
||||
// Cancelled
|
||||
users = [];
|
||||
searchQuery = "";
|
||||
showModal = false;
|
||||
isEdit = false;
|
||||
formData = {
|
||||
id: null,
|
||||
username: "",
|
||||
email: "",
|
||||
password: ""
|
||||
};
|
||||
|
||||
loadUsers = async () => {
|
||||
try {
|
||||
const res = await $axios.get('/api/users');
|
||||
users = res.items || []; // Adjust based on actual API response structure (array or {items: []})
|
||||
} catch (e) {
|
||||
$message.error(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
filteredUsers = () => {
|
||||
if (!searchQuery) return users;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return users.filter(u =>
|
||||
u.username.toLowerCase().includes(query) ||
|
||||
u.email.toLowerCase().includes(query)
|
||||
);
|
||||
};
|
||||
|
||||
openCreateModal = () => {
|
||||
isEdit = false;
|
||||
formData = { id: null, username: "", email: "", password: "" };
|
||||
showModal = true;
|
||||
};
|
||||
|
||||
openEditModal = (u) => {
|
||||
isEdit = true;
|
||||
formData = { ...u, password: "" };
|
||||
showModal = true;
|
||||
};
|
||||
|
||||
closeModal = () => {
|
||||
showModal = false;
|
||||
};
|
||||
|
||||
saveUser = async () => {
|
||||
if (!formData.username) {
|
||||
$message.error("Username is required");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (isEdit) {
|
||||
const payload = { email: formData.email };
|
||||
if (formData.password) payload.password = formData.password;
|
||||
await $axios.patch(`/api/users/${formData.id}`, payload);
|
||||
$message.success("User updated");
|
||||
} else {
|
||||
if (!formData.password) {
|
||||
$message.error("Password is required for new users");
|
||||
return;
|
||||
}
|
||||
};
|
||||
await $axios.post('/api/users', formData);
|
||||
$message.success("User created");
|
||||
}
|
||||
closeModal();
|
||||
loadUsers();
|
||||
} catch (e) {
|
||||
$message.error(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
deleteUser = async (u) => {
|
||||
try {
|
||||
await $message.confirm(`Delete user "${u.username}"?`);
|
||||
await $axios.delete(`/api/users/${u.id}`);
|
||||
$message.success("Deleted");
|
||||
loadUsers();
|
||||
} catch (e) {
|
||||
// Cancelled
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<script>
|
||||
$data.loadUsers();
|
||||
$data.loadUsers();
|
||||
</script>
|
||||
|
||||
</html>
|
||||
|
||||
@ -1,133 +1,119 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta name="description" content="User Profile">
|
||||
<title>{{ $t('user.profile') }}</title>
|
||||
<style>
|
||||
.profile-container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.avatar-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.avatar {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-primary-light);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32px;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
.btn-save {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
.btn-logout {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background-color: var(--color-danger);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
<meta name="description" content="User Profile">
|
||||
<title>{{ $t('user.profile') }}</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: var(--spacing-xl);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.profile-container {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
padding: var(--spacing-xl);
|
||||
background: var(--bg-color-secondary);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-lg);
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.avatar-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32px;
|
||||
color: var(--color-primary-text);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.form-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="profile-container">
|
||||
<h2 style="text-align: center; margin-bottom: 30px;">{{ $t('user.profile') }}</h2>
|
||||
|
||||
<div class="avatar-section">
|
||||
<div class="avatar">
|
||||
{{ user.username ? user.username.charAt(0).toUpperCase() : 'U' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="updateProfile">
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ $t('auth.username') }}</label>
|
||||
<input type="text" v:value="user.username" class="form-input" disabled>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ $t('auth.email') }}</label>
|
||||
<input type="email" v:value="user.email" class="form-input">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Phone</label>
|
||||
<input type="tel" v:value="user.phone" class="form-input">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-save">{{ $t('common.save') }}</button>
|
||||
</form>
|
||||
|
||||
<button class="btn-logout" @click="handleLogout">{{ $t('auth.logout') }}</button>
|
||||
<div class="profile-container">
|
||||
<h2 style="text-align: center;">{{ $t('user.profile') }}</h2>
|
||||
|
||||
<div class="avatar-section">
|
||||
<div class="avatar">
|
||||
{{ user.username ? user.username.charAt(0).toUpperCase() : 'U' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="form-content" @submit.prevent="updateProfile">
|
||||
<v-input label="Username" v:value="user.username" disabled></v-input>
|
||||
<v-input label="Email" type="email" v:value="user.email" required></v-input>
|
||||
<v-input label="Phone" type="tel" v:value="user.phone"></v-input>
|
||||
|
||||
<v-btn type="submit" color="primary" block style="margin-top: var(--spacing-sm);">
|
||||
{{ $t('common.save') }}
|
||||
</v-btn>
|
||||
</form>
|
||||
|
||||
<div style="border-top: 1px solid var(--border-color); padding-top: var(--spacing-lg);">
|
||||
<v-btn color="danger" variant="outline" block :click="handleLogout">
|
||||
{{ $t('auth.logout') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script setup>
|
||||
user = $env.$vbase.user || {};
|
||||
|
||||
// Fetch fresh data
|
||||
loadUser = async () => {
|
||||
try {
|
||||
user = await $env.$vbase.fetchUser();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
updateProfile = async () => {
|
||||
try {
|
||||
await $axios.patch('/api/auth/me', {
|
||||
email: user.email,
|
||||
phone: user.phone
|
||||
});
|
||||
$message.success("Profile updated");
|
||||
loadUser();
|
||||
} catch (e) {
|
||||
$message.error(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
handleLogout = async () => {
|
||||
await $env.$vbase.logout('/login');
|
||||
};
|
||||
user = $env.$vbase.user || {};
|
||||
|
||||
// Fetch fresh data
|
||||
loadUser = async () => {
|
||||
try {
|
||||
user = await $env.$vbase.fetchUser();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
updateProfile = async () => {
|
||||
try {
|
||||
await $axios.patch('/api/auth/me', {
|
||||
email: user.email,
|
||||
phone: user.phone
|
||||
});
|
||||
$message.success("Profile updated");
|
||||
loadUser();
|
||||
} catch (e) {
|
||||
$message.error(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
handleLogout = async () => {
|
||||
await $env.$vbase.logout('/login');
|
||||
};
|
||||
</script>
|
||||
<script>
|
||||
$data.loadUser();
|
||||
$data.loadUser();
|
||||
</script>
|
||||
|
||||
</html>
|
||||
|
||||
Loading…
Reference in New Issue