This commit is contained in:
suguo 2026-03-19 17:35:05 +08:00
parent 446f4475f1
commit abbc1dd7a6
10 changed files with 726 additions and 3 deletions

View File

View File

0
src/pages/org/person.vue Normal file
View File

View File

View File

View File

View File

716
src/pages/system/user.vue Normal file
View File

@ -0,0 +1,716 @@
<script setup>
import { ref, onMounted } from 'vue'
import { useUserStore } from '../../stores/user'
import { apiFetch } from '../../lib/api'
const userStore = useUserStore()
const users = ref([])
const loading = ref(false)
const error = ref('')
//
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
//
const showAddModal = ref(false)
const showEditModal = ref(false)
const currentUser = ref(null)
//
const form = ref({
userName: '',
password: '',
roleId: '',
name: '',
email: '',
phone: ''
})
//
const formErrors = ref({})
//
async function loadUsers() {
loading.value = true
error.value = ''
try {
const res = await apiFetch(`/api/users?page=${page.value}&pageSize=${pageSize.value}`)
if (res?.data) {
users.value = res.data.users || []
total.value = res.data.total || 0
}
} catch (err) {
error.value = err.message || '加载用户列表失败'
} finally {
loading.value = false
}
}
//
function resetForm() {
form.value = {
userName: '',
password: '',
roleId: '',
mobile: '',
orgId: ''
}
formErrors.value = {}
}
//
function openAddModal() {
resetForm()
showAddModal.value = true
}
//
function openEditModal(user) {
currentUser.value = user
form.value = {
userName: user.userName || '',
password: '', //
roleId: user.roleId || '',
mobile: user.mobile || '',
orgId: user.orgId || ''
}
formErrors.value = {}
showEditModal.value = true
}
//
function validateForm() {
formErrors.value = {}
let isValid = true
if (!form.value.userName.trim()) {
formErrors.value.userName = '用户名不能为空'
isValid = false
}
if (!form.value.password.trim() && !currentUser.value) {
formErrors.value.password = '密码不能为空'
isValid = false
}
if (!form.value.roleId) {
formErrors.value.roleId = '角色不能为空'
isValid = false
}
if (!form.value.orgId) {
formErrors.value.orgId = '机构不能为空'
isValid = false
}
return isValid
}
//
async function addUser() {
if (!validateForm()) return
loading.value = true
error.value = ''
try {
await apiFetch('/api/users', {
method: 'POST',
body: JSON.stringify(form.value)
})
showAddModal.value = false
await loadUsers()
} catch (err) {
error.value = err.message || '添加用户失败'
} finally {
loading.value = false
}
}
//
async function updateUser() {
if (!validateForm() || !currentUser.value) return
loading.value = true
error.value = ''
try {
const updateData = { ...form.value }
//
if (!updateData.password.trim()) {
delete updateData.password
}
await apiFetch(`/api/users/${currentUser.value.id}`, {
method: 'PUT',
body: JSON.stringify(updateData)
})
showEditModal.value = false
await loadUsers()
} catch (err) {
error.value = err.message || '更新用户失败'
} finally {
loading.value = false
}
}
//
async function deleteUser(userId) {
if (!confirm('确定要删除该用户吗?')) return
loading.value = true
error.value = ''
try {
await apiFetch(`/api/users/${userId}`, {
method: 'DELETE'
})
await loadUsers()
} catch (err) {
error.value = err.message || '删除用户失败'
} finally {
loading.value = false
}
}
//
onMounted(() => {
loadUsers()
})
</script>
<template>
<div class="user-page">
<div class="page-header">
<h1>用户管理</h1>
<button class="btn-primary" @click="openAddModal">添加用户</button>
</div>
<div v-if="error" class="error-message">{{ error }}</div>
<div class="user-list">
<table class="user-table">
<thead>
<tr>
<th>用户名</th>
<th>手机</th>
<th>角色ID</th>
<th>机构ID</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td>{{ user.userName }}</td>
<td>{{ user.mobile || '-' }}</td>
<td>{{ user.roleId || '-' }}</td>
<td>{{ user.orgId || '-' }}</td>
<td class="actions">
<button class="btn-edit" @click="openEditModal(user)">编辑</button>
<button class="btn-delete" @click="deleteUser(user.id)">删除</button>
</td>
</tr>
</tbody>
</table>
<div v-if="loading" class="loading">加载中...</div>
<div v-else-if="users.length === 0" class="empty">暂无用户数据</div>
</div>
<!-- 分页组件 -->
<div v-if="total > 0" class="pagination">
<div class="pagination-info">
{{ total }} 条记录每页 {{ pageSize }} {{ page }} / {{ Math.ceil(total / pageSize) }}
</div>
<div class="pagination-controls">
<button
class="pagination-btn"
@click="page = 1; loadUsers()"
:disabled="page === 1"
>
首页
</button>
<button
class="pagination-btn"
@click="page--; loadUsers()"
:disabled="page === 1"
>
上一页
</button>
<button
class="pagination-btn"
@click="page++; loadUsers()"
:disabled="page >= Math.ceil(total / pageSize)"
>
下一页
</button>
<button
class="pagination-btn"
@click="page = Math.ceil(total / pageSize); loadUsers()"
:disabled="page >= Math.ceil(total / pageSize)"
>
末页
</button>
<select v-model="pageSize" @change="page = 1; loadUsers()" class="pagination-select">
<option value="10">10/</option>
<option value="20">20/</option>
<option value="50">50/</option>
</select>
</div>
</div>
<!-- 添加用户模态框 -->
<div v-if="showAddModal" class="modal" @click.self="showAddModal = false">
<div class="modal-content">
<div class="modal-header">
<h2>添加用户</h2>
<button class="modal-close" @click="showAddModal = false">×</button>
</div>
<div class="modal-body">
<form @submit.prevent="addUser">
<div class="form-group">
<label>用户名</label>
<input v-model="form.userName" type="text" class="form-input" />
<div v-if="formErrors.userName" class="form-error">{{ formErrors.userName }}</div>
</div>
<div class="form-group">
<label>密码</label>
<input v-model="form.password" type="password" class="form-input" />
<div v-if="formErrors.password" class="form-error">{{ formErrors.password }}</div>
</div>
<div class="form-group">
<label>角色ID</label>
<input v-model="form.roleId" type="text" class="form-input" />
<div v-if="formErrors.roleId" class="form-error">{{ formErrors.roleId }}</div>
</div>
<div class="form-group">
<label>手机</label>
<input v-model="form.mobile" type="tel" class="form-input" />
</div>
<div class="form-group">
<label>机构ID</label>
<input v-model="form.orgId" type="text" class="form-input" />
<div v-if="formErrors.orgId" class="form-error">{{ formErrors.orgId }}</div>
</div>
<div class="form-actions">
<button type="button" class="btn-cancel" @click="showAddModal = false">取消</button>
<button type="submit" class="btn-primary" :disabled="loading">
{{ loading ? '提交中...' : '提交' }}
</button>
</div>
</form>
</div>
</div>
</div>
<!-- 编辑用户模态框 -->
<div v-if="showEditModal" class="modal" @click.self="showEditModal = false">
<div class="modal-content">
<div class="modal-header">
<h2>编辑用户</h2>
<button class="modal-close" @click="showEditModal = false">×</button>
</div>
<div class="modal-body">
<form @submit.prevent="updateUser">
<div class="form-group">
<label>用户名</label>
<input v-model="form.userName" type="text" class="form-input" />
<div v-if="formErrors.userName" class="form-error">{{ formErrors.userName }}</div>
</div>
<div class="form-group">
<label>密码留空则不修改</label>
<input v-model="form.password" type="password" class="form-input" />
<div v-if="formErrors.password" class="form-error">{{ formErrors.password }}</div>
</div>
<div class="form-group">
<label>角色ID</label>
<input v-model="form.roleId" type="text" class="form-input" />
<div v-if="formErrors.roleId" class="form-error">{{ formErrors.roleId }}</div>
</div>
<div class="form-group">
<label>手机</label>
<input v-model="form.mobile" type="tel" class="form-input" />
</div>
<div class="form-group">
<label>机构ID</label>
<input v-model="form.orgId" type="text" class="form-input" />
<div v-if="formErrors.orgId" class="form-error">{{ formErrors.orgId }}</div>
</div>
<div class="form-actions">
<button type="button" class="btn-cancel" @click="showEditModal = false">取消</button>
<button type="submit" class="btn-primary" :disabled="loading">
{{ loading ? '提交中...' : '提交' }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.user-page {
padding: 24px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.page-header h1 {
font-size: 20px;
font-weight: 900;
margin: 0;
}
.btn-primary {
height: 36px;
padding: 0 16px;
border-radius: 8px;
border: none;
background: linear-gradient(135deg, #b48040, #205c40);
color: white;
font-weight: 800;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-primary:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-edit {
height: 28px;
padding: 0 12px;
border-radius: 6px;
border: 1px solid #b48040;
background: white;
color: #b48040;
font-weight: 800;
cursor: pointer;
margin-right: 8px;
transition: all 0.2s ease;
}
.btn-edit:hover {
background: #f9f0e6;
}
.btn-delete {
height: 28px;
padding: 0 12px;
border-radius: 6px;
border: 1px solid #e53e3e;
background: white;
color: #e53e3e;
font-weight: 800;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-delete:hover {
background: #fed7d7;
}
.btn-cancel {
height: 36px;
padding: 0 16px;
border-radius: 8px;
border: 1px solid #d1d5db;
background: white;
color: #374151;
font-weight: 800;
cursor: pointer;
margin-right: 12px;
transition: all 0.2s ease;
}
.btn-cancel:hover {
background: #f3f4f6;
}
.error-message {
background: #fed7d7;
color: #c53030;
padding: 12px;
border-radius: 8px;
margin-bottom: 16px;
font-weight: 800;
}
.user-list {
background: white;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.user-table {
width: 100%;
border-collapse: collapse;
}
.user-table th {
background: #f9fafb;
padding: 12px 16px;
text-align: left;
font-weight: 800;
font-size: 14px;
color: #374151;
border-bottom: 2px solid #e5e7eb;
}
.user-table td {
padding: 12px 16px;
border-bottom: 1px solid #e5e7eb;
font-size: 14px;
}
.user-table tr:last-child td {
border-bottom: none;
}
.actions {
display: flex;
align-items: center;
}
.loading {
padding: 48px;
text-align: center;
color: #6b7280;
font-weight: 800;
}
.empty {
padding: 48px;
text-align: center;
color: #6b7280;
font-weight: 800;
}
.modal {
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;
padding: 20px;
}
.modal-content {
background: white;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #e5e7eb;
}
.modal-header h2 {
font-size: 18px;
font-weight: 900;
margin: 0;
}
.modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #6b7280;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
transition: all 0.2s ease;
}
.modal-close:hover {
background: #f3f4f6;
color: #374151;
}
.modal-body {
padding: 20px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
font-size: 14px;
font-weight: 800;
color: #374151;
margin-bottom: 6px;
}
.form-input {
width: 100%;
height: 40px;
padding: 0 12px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
transition: all 0.2s ease;
}
.form-input:focus {
outline: none;
border-color: #b48040;
box-shadow: 0 0 0 3px rgba(180, 128, 64, 0.1);
}
.form-error {
font-size: 12px;
color: #e53e3e;
margin-top: 4px;
font-weight: 800;
}
.form-actions {
display: flex;
justify-content: flex-end;
margin-top: 24px;
}
/* 分页样式 */
.pagination {
margin-top: 24px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
}
.pagination-info {
font-size: 14px;
color: #6b7280;
font-weight: 800;
}
.pagination-controls {
display: flex;
align-items: center;
gap: 8px;
}
.pagination-btn {
height: 32px;
padding: 0 12px;
border-radius: 6px;
border: 1px solid #d1d5db;
background: white;
color: #374151;
font-weight: 800;
cursor: pointer;
transition: all 0.2s ease;
}
.pagination-btn:hover:not(:disabled) {
background: #f3f4f6;
border-color: #b48040;
}
.pagination-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.pagination-select {
height: 32px;
padding: 0 8px;
border-radius: 6px;
border: 1px solid #d1d5db;
background: white;
color: #374151;
font-weight: 800;
cursor: pointer;
}
.pagination-select:focus {
outline: none;
border-color: #b48040;
box-shadow: 0 0 0 3px rgba(180, 128, 64, 0.1);
}
@media (max-width: 768px) {
.user-page {
padding: 16px;
}
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.user-table {
font-size: 12px;
}
.user-table th,
.user-table td {
padding: 8px 12px;
}
.actions {
flex-direction: column;
gap: 4px;
}
.btn-edit,
.btn-delete {
margin-right: 0;
width: 100%;
}
.pagination {
flex-direction: column;
align-items: flex-start;
}
.pagination-controls {
width: 100%;
justify-content: space-between;
}
.pagination-btn {
flex: 1;
min-width: 80px;
}
}
</style>

0
src/pages/task/list.vue Normal file
View File

View File

@ -50,9 +50,16 @@ const router = createRouter({
{ path: 'home', component: AdminHome },
{ path: 'archives', name: 'admin-archives', component: Archives },
{ path: 'inspections', name: 'admin-inspections', component: Inspections },
{ path: 'projects', name: 'admin-projects', component: Projects },
{ path: 'system', name: 'admin-analytics', component: Analytics },
{ path: 'org/list', name: 'admin-org', component: () => import('../pages/org/list.vue') },
{ path: 'system/user', name: 'admin-system-usr', component: () => import('../pages/system/user.vue') },
{ path: 'system/role', name: 'admin-system-role', component: () => import('../pages/system/role.vue') },
{ path: 'system/option', name: 'admin-system-option', component: () => import('../pages/system/option.vue') },
{ path: 'org/list', name: 'admin-org-list', component: () => import('../pages/org/list.vue') },
{ path: 'org/person', name: 'admin-org-person', component: () => import('../pages/org/person.vue') },
{ path: 'device/summary', name: 'admin-device-summary', component: () => import('../pages/device/summary.vue') },
{ path: 'device/list', name: 'admin-device-list', component: () => import('../pages/device/list.vue') },
{ path: 'project/list', name: 'admin-project-list', component: () => import('../pages/project/list.vue') },
{ path: 'project/detail', name: 'admin-project-detail', component: () => import('../pages/project/detail.vue') },
{path:'task/list',name:'admin-task-list',component:()=> import('../pages/task/list.vue')}
],
},
],