feat: User Management

This commit is contained in:
fit2cloud-chenyw
2025-05-06 18:15:35 +08:00
parent 0179b3d371
commit 2de7ece73b
12 changed files with 530 additions and 18 deletions
+9
View File
@@ -0,0 +1,9 @@
import { request } from '@/utils/request'
export const userApi = {
pager: (pageNumber: number, pageSize: number) => request.get(`/user/pager/${pageNumber}/${pageSize}`),
add: (data: any) => request.post('/settings/terminology', data),
edit: (data: any) => request.put('/settings/terminology', data),
delete: (id: number) => request.delete(`/settings/terminology/${id}`),
query: (id: number) => request.get(`/settings/terminology/${id}`)
}
+56 -6
View File
@@ -45,7 +45,7 @@
<h1>{{ currentPageTitle }}</h1>
<div class="header-actions">
<el-tooltip content="System manage" placement="bottom">
<div class="header-icon-btn">
<div class="header-icon-btn" @click="toSystem">
<el-icon><iconsystem /></el-icon>
<span>System manage</span>
</div>
@@ -77,8 +77,27 @@
</div>
</div>
<div v-if="sysRouterList.length && showHead" class="sys-setting-container">
<el-menu
:default-active="activeMenu"
class="el-menu-demo"
mode="horizontal"
>
<el-menu-item v-for="item in sysRouterList" :key="item.path" :index="item.path" @click="menuSelect">
<el-icon v-if="item.meta.icon">
<component :is="resolveIcon(item.meta.icon)" />
</el-icon>
<span>{{ item.meta.title }}</span>
</el-menu-item>
</el-menu>
</div>
<div class="page-content">
<div v-if="sysRouterList.length && showHead" class="sys-page-content">
<div class="sys-inner-container">
<router-view />
</div>
</div>
<div v-else class="page-content">
<router-view />
</div>
</div>
@@ -105,19 +124,31 @@ const route = useRoute()
const userStore = useUserStore()
const name = ref('admin')
const activeMenu = computed(() => route.path)
const routerList = computed(() => {
return router.getRoutes().filter(route => {
return route.path !== '/login' && !route.redirect && route.path !== '/:pathMatch(.*)*'
return route.path !== '/login' && !route.path.includes('/system') && !route.redirect && route.path !== '/:pathMatch(.*)*'
})
})
const sysRouterList = computed(() => {
return router.getRoutes().filter(route => route.path.includes('/system'))
})
const showHead = computed(() => {
return route.path.includes('/system')
})
const workspace = ref('1')
const options = [
{ value: '1', label: 'Default workspace' },
{ value: '2', label: 'Workspace 2' },
{ value: '3', label: 'Workspace 3' }
]
const currentPageTitle = computed(() => route.meta.title || 'Dashboard')
const currentPageTitle = computed(() => {
if (route.path.includes('/system')) {
return 'System Settings'
}
return route.meta.title || 'Dashboard'
})
const resolveIcon = (iconName: any) => {
const icons: Record<string, any> = {
'ds': ds,
@@ -129,13 +160,15 @@ const resolveIcon = (iconName: any) => {
}
const menuSelect = (e: any) => {
console.log(routerList.value)
router.push(e.index)
}
const logout = () => {
userStore.logout()
router.push('/login')
}
const toSystem = () => {
router.push('/system')
}
</script>
<style lang="less" scoped>
@@ -260,6 +293,23 @@ const logout = () => {
flex: 1;
overflow-y: auto;
}
.sys-page-content {
background-color: var(--white);
border-radius: var(--border-radius);
padding: 24px;
box-shadow: var(--shadow);
margin-top: 24px;
.sys-inner-container {
background: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
.sys-setting-container {
overflow: hidden;
border-radius: 8px;
}
}
}
</style>
+19
View File
@@ -66,6 +66,25 @@ const router = createRouter({
meta: { title: 'Setting', icon: 'setting' }
}
]
},
{
path: '/system',
component: Layout,
redirect: '/system/user',
children: [
{
path: 'user',
name: 'user',
component: () => import('@/views/system/user/index.vue'),
meta: { title: 'User Management', icon: 'setting' }
},
{
path: 'model',
name: 'model',
component: () => import('@/views/system/model/index.vue'),
meta: { title: 'AI Model Configuration', icon: 'setting' }
}
]
}
]
})
+32
View File
@@ -0,0 +1,32 @@
export function formatTimestamp(timestamp: number, format: string = 'YYYY-MM-DD'): string {
if (!timestamp) {
return '-'
}
const date = new Date(timestamp)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return format.replace(/YYYY|MM|DD|HH|mm|ss/g, (match) => {
switch (match) {
case 'YYYY':
return String(year)
case 'MM':
return month
case 'DD':
return day
case 'HH':
return hours
case 'mm':
return minutes
case 'ss':
return seconds
default:
return match
}
})
}
+1 -1
View File
@@ -71,7 +71,7 @@ class HttpService {
}
// Request logging
console.log(`[Request] ${config.method?.toUpperCase()} ${config.url}`)
// console.log(`[Request] ${config.method?.toUpperCase()} ${config.url}`)
return config
},
+1 -1
View File
@@ -11,7 +11,7 @@
<div class="login-form">
<el-card class="login-card">
<h2 class="login-title">Login</h2>
<el-form :model="loginForm" :rules="rules" ref="loginFormRef">
<el-form :model="loginForm" :rules="rules" ref="loginFormRef" @keyup.enter="submitForm">
<el-form-item prop="username">
<el-input v-model="loginForm.username" placeholder="username" prefix-icon="user"></el-input>
</el-form-item>
@@ -0,0 +1,3 @@
<template>
<h2>this is ai model page</h2>
</template>
+366
View File
@@ -0,0 +1,366 @@
<template>
<div class="sqlbot-table-container professional-container">
<div class="sqlbot-tool">
<div class="tool-left">
<el-input
v-model="keyword"
class="sqlbot-search"
placeholder="Search"
prefix-icon="el-icon-search"
@keyup.enter.native="handleSearch"
>
<template #prefix>
<el-icon class="el-input__icon"><search /></el-icon>
</template>
</el-input>
</div>
<!-- <div class="tool-right">
<div class="tool-btn secondary-btn">
<el-icon><IconOpeUpload /></el-icon>
<span>Import Terminology</span>
</div>
<div class="tool-btn secondary-btn">
<el-icon><IconOpeDownload /></el-icon>
<span>Export Terminology</span>
</div>
<div class="tool-btn primary-btn" @click="openDialog">
<el-icon><IconOpeAdd /></el-icon>
<span>Add Terminology</span>
</div>
</div> -->
</div>
<div class="sqlbot-table">
<el-table :data="state.tableData" style="width: 100%">
<el-table-column prop="account" label="Account" width="280" />
<el-table-column prop="name" label="Name" width="280" />
<el-table-column prop="email" label="Email" width="280" />
<el-table-column prop="status" label="Status" width="180">
<template #default="scope">
<div class="user-status-container" :class="[ scope.row.status ? 'active' : 'disabled' ]">
<el-icon>
<SuccessFilled v-if="scope.row.status"/>
<CircleCloseFilled v-else />
</el-icon>
<span>{{ scope.row.status ? 'Activated' : 'Disabled' }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="create_time" label="Create time">
<template #default="scope">
<span>{{ formatTimestamp(scope.row.create_time) }}</span>
</template>
</el-table-column>
<el-table-column prop="id" label="Actions" width="120" fixed="right">
<template #default="scope">
<div class="table-operate">
<div class="opt-btn" @click="editHandler(scope.row.id)">
<el-icon><IconOpeEdit /></el-icon>
</div>
<div class="opt-btn" @click="deleteHandler(scope.row.id)">
<el-icon><IconOpeDelete /></el-icon>
</div>
</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
<el-dialog v-model="dialogFormVisible" title="Add Terminology" destroy-on-close width="500" @close="onFormClose" class="sqlbot-dialog">
<el-form :model="state.form"
label-width="180px"
label-position="top"
class="sqlbot-form"
size="large"
ref="termFormRef"
>
<el-form-item label="Account" >
<el-input v-model="state.form.term" autocomplete="off" />
</el-form-item>
<el-form-item label="Definition" >
<el-input type="textarea" v-model="state.form.definition" />
</el-form-item>
<el-form-item label="Domain">
<el-select v-model="state.form.domain" placeholder="Please select a domain">
<el-option label="domain1" value="domain1" />
<el-option label="domain2" value="domain2" />
<el-option label="domain3" value="domain3" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeForm">Cancel</el-button>
<el-button type="primary" @click="saveHandler">
save
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
/* import IconOpeUpload from '@/assets/svg/operate/ope-upload.svg';
import IconOpeDownload from '@/assets/svg/operate/ope-download.svg';
import IconOpeAdd from '@/assets/svg/operate/ope-add.svg'; */
import { SuccessFilled, CircleCloseFilled } from '@element-plus/icons-vue'
import IconOpeEdit from '@/assets/svg/operate/ope-edit.svg';
import IconOpeDelete from '@/assets/svg/operate/ope-delete.svg';
import { Search } from '@element-plus/icons-vue'
import { userApi } from '@/api/auth'
import { formatTimestamp } from '@/utils/date'
const keyword = ref('')
const dialogFormVisible = ref(false)
const termFormRef = ref()
const state = reactive({
tableData: [
],
form: {
id: '',
term: '',
definition: '',
domain: ''
},
})
const handleSearch = (e: any) => {
console.log('search', e);
}
const editHandler = (id: any) => {
/* userApi.query(id).then((res: any) => {
console.log('term detail', res)
state.form = res
dialogFormVisible.value = true
}) */
}
const deleteHandler = (id: any) => {
ElMessageBox.confirm(
'Are you sure to delete?',
'Warning',
{
confirmButtonText: 'OK',
cancelButtonText: 'Cancel',
type: 'warning',
}
)
.then(() => {
/* userApi.delete(id).then(() => {
ElMessage({
type: 'success',
message: 'Delete completed',
})
search()
}) */
})
.catch(() => {
ElMessage({
type: 'info',
message: 'Delete canceled',
})
})
}
const openDialog = () => {
dialogFormVisible.value = true
}
const resetForm = () => {
if (!termFormRef) return
Object.keys(state.form).forEach((key) => {
state.form[key as keyof typeof state.form] = ''
})
}
const closeForm = () => {
dialogFormVisible.value = false
}
const onFormClose = () => {
resetForm()
dialogFormVisible.value = false
}
const search = () => {
userApi.pager(1, 20).then((res: any) => {
state.tableData = res.items
})
}
const addTerm = () => {
userApi.add(state.form).then(() => {
dialogFormVisible.value = false
search()
ElMessage({
type: 'success',
message: 'Add completed',
})
})
}
const editTerm = () => {
userApi.edit(state.form).then((res: any) => {
console.log('edit term', res)
dialogFormVisible.value = false
search()
ElMessage({
type: 'success',
message: 'Edit completed',
})
})
}
const saveHandler = () => {
if (state.form.id) {
editTerm()
} else {
addTerm()
}
}
onMounted(() => {
search()
})
</script>
<style lang="less" scoped>
.sqlbot-table-container {
width: 100%;
height: 100%;
.sqlbot-tool {
height: 42px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.tool-left {
display: flex;
align-items: center;
.sqlbot-search {
width: 300px;
--el-input-inner-height: 40px !important;
--el-input-border-radius: 24px;
--el-input-bg-color: #f5f7fa;
--el-input-border-color: #e5e8ed;
}
}
.tool-right {
display: flex;
align-items: center;
column-gap: 12px;
height: 35px;
.secondary-btn {
background: #f5f5f5;
color: #333;
border: 1px solid #ddd;
}
.primary-btn {
background: #1890ff;
color: white;
}
.tool-btn {
display: flex;
align-items: center;
padding: 6px 16px;
border-radius: 4px;
cursor: pointer;
outline: none;
font-weight: 500;
transition: all 0.3s;
font-size: 14px;
gap: 6px;
&:hover {
background-color: #f1f3f4;
}
&.default {
color: #2d2e31;
font-weight: 500;
font-size: 14px;
i {
width: 20px;
height: 20px;
}
}
}
}
}
.sqlbot-table {
border-radius: 6px;
:deep(.el-table) {
--el-table-header-bg-color: #f5f7fa;
--el-table-border-color: #ebeef5;
--el-table-header-text-color: #606266;
th {
font-weight: 600;
height: 48px;
}
td {
height: 52px;
}
}
.table-operate {
display: flex;
justify-content: flex-end;
gap: 8px;
.opt-btn {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #5f6368;
cursor: pointer;
transition: all 0.2s;
background-color: #f1f3f4;
border: none;
font-size: 18px;
&:hover {
background-color: #e8f0fe;
color: #4285f4;
}
}
}
}
}
.sqlbot-form {
width: 100%;
.el-form-item {
margin-bottom: 16px;
text-align: left;
label {
font-size: 14px;
color: #606266;
}
}
}
.sqlbot-dialog {
width: 500px;
:deep(.el-dialog__header) {
text-align: left !important;
font-size: 16px;
background-color: #f5f7fa;
border-bottom: 1px solid #ebeef5;
color: #606266;
font-size: 16px;
}
}
.user-status-container {
display: flex;
align-items: center;
column-gap: 8px;
font-size: 14px;
color: #606266;
padding: 4px 8px;
width: fit-content;
border-radius: 4px;
&.active {
background-color: #e6f7ee;
color: #389e0d;
}
&.disabled {
background-color: #fff1f0;
color: #cf1322;
}
}
</style>