diff --git a/app/Http/Controllers/Auth/Admin/Department.php b/app/Http/Controllers/Auth/Admin/Department.php index f376825..cf97e28 100644 --- a/app/Http/Controllers/Auth/Admin/Department.php +++ b/app/Http/Controllers/Auth/Admin/Department.php @@ -85,7 +85,7 @@ class Department extends Controller { $validated = $request->validate([ 'name' => 'required|string|max:50', - 'parent_id' => 'nullable|integer|exists:auth_departments,id', + 'parent_id' => 'nullable|integer', 'leader' => 'nullable|string|max:50', 'phone' => 'nullable|string|max:20', 'sort' => 'nullable|integer|min:0', @@ -108,7 +108,7 @@ class Department extends Controller { $validated = $request->validate([ 'name' => 'nullable|string|max:50', - 'parent_id' => 'nullable|integer|exists:auth_departments,id', + 'parent_id' => 'nullable|integer', 'leader' => 'nullable|string|max:50', 'phone' => 'nullable|string|max:20', 'sort' => 'nullable|integer|min:0', diff --git a/app/Http/Controllers/System/Admin/Log.php b/app/Http/Controllers/System/Admin/Log.php index 16d634f..adb689d 100644 --- a/app/Http/Controllers/System/Admin/Log.php +++ b/app/Http/Controllers/System/Admin/Log.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\System\Admin; use App\Http\Controllers\Controller; use App\Http\Requests\LogRequest; use App\Services\System\LogService; +use Illuminate\Http\Request; use Maatwebsite\Excel\Facades\Excel; use App\Exports\GenericExport; @@ -30,39 +31,9 @@ class Log extends Controller public function export(LogRequest $request) { $params = $request->validated(); - $pageSize = $params['page_size'] ?? 10000; // 导出时默认获取更多数据 + $filePath = $this->logService->export($params); - // 获取所有符合条件的日志(不分页) - $query = $this->logService->getListQuery($params); - $logs = $query->limit($pageSize)->get(); - - // 准备导出数据 - $headers = [ - 'ID', '用户名', '模块', '操作', '请求方法', 'URL', 'IP地址', - '状态码', '状态', '错误信息', '执行时间(ms)', '创建时间' - ]; - - $data = []; - foreach ($logs as $log) { - $data[] = [ - $log->id, - $log->username, - $log->module, - $log->action, - $log->method, - $log->url, - $log->ip, - $log->status_code, - $log->status === 'success' ? '成功' : '失败', - $log->error_message ?? '-', - $log->execution_time, - $log->created_at->format('Y-m-d H:i:s'), - ]; - } - - $filename = '系统操作日志_' . date('YmdHis') . '.xlsx'; - - return Excel::download(new GenericExport($headers, $data), $filename); + return response()->download($filePath)->deleteFileAfterSend(true); } public function show(int $id) @@ -93,7 +64,7 @@ class Log extends Controller ]); } - public function batchDelete(Request $request) + public function batchDelete(\Illuminate\Http\Request $request) { $this->logService->batchDelete($request->input('ids', [])); return response()->json([ diff --git a/app/Http/Requests/LogRequest.php b/app/Http/Requests/LogRequest.php index 7abafb1..cd46a5d 100644 --- a/app/Http/Requests/LogRequest.php +++ b/app/Http/Requests/LogRequest.php @@ -9,7 +9,7 @@ use Illuminate\Http\Exceptions\HttpResponseException; class LogRequest extends FormRequest { /** - * Determine if the user is authorized to make this request. + * Determine if user is authorized to make this request. * * @return bool */ @@ -19,7 +19,7 @@ class LogRequest extends FormRequest } /** - * Get the validation rules that apply to the request. + * Get validation rules that apply to request. * * @return array */ @@ -34,6 +34,7 @@ class LogRequest extends FormRequest 'username' => 'nullable|string|max:50', 'module' => 'nullable|string|max:50', 'action' => 'nullable|string|max:100', + 'method' => 'nullable|in:GET,POST,PUT,DELETE,PATCH', 'status' => 'nullable|in:success,error', 'start_date' => 'nullable|date', 'end_date' => 'nullable|date|after_or_equal:start_date', @@ -77,6 +78,7 @@ class LogRequest extends FormRequest 'username.max' => '用户名最多50个字符', 'module.max' => '模块名最多50个字符', 'action.max' => '操作名最多100个字符', + 'method.in' => '请求方式必须是 GET、POST、PUT、DELETE 或 PATCH', 'status.in' => '状态值必须是 success 或 error', 'start_date.date' => '开始日期格式不正确', 'end_date.date' => '结束日期格式不正确', @@ -121,7 +123,7 @@ class LogRequest extends FormRequest } /** - * Prepare the data for validation. + * Prepare for validation. * * @return void */ diff --git a/app/Services/System/LogService.php b/app/Services/System/LogService.php index 575d57c..e27b201 100644 --- a/app/Services/System/LogService.php +++ b/app/Services/System/LogService.php @@ -48,7 +48,7 @@ class LogService */ protected function buildQuery(array $params) { - $query = Log::query()->with('user:id,name,username'); + $query = Log::query()->with('user:id,username'); if (!empty($params['user_id'])) { $query->where('user_id', $params['user_id']); @@ -108,6 +108,14 @@ class LogService { $query = Log::query(); + if (!empty($params['method'])) { + $query->where('method', $params['method']); + } + + if (!empty($params['status'])) { + $query->where('status', $params['status']); + } + if (!empty($params['start_date']) && !empty($params['end_date'])) { $query->whereBetween('created_at', [$params['start_date'], $params['end_date']]); } @@ -116,10 +124,74 @@ class LogService $successCount = (clone $query)->where('status', 'success')->count(); $errorCount = (clone $query)->where('status', 'error')->count(); + // 计算平均响应时间 + $avgTime = (clone $query)->avg('execution_time'); + $avgTime = $avgTime ? round($avgTime, 2) : 0; + return [ 'total' => $total, 'success' => $successCount, 'error' => $errorCount, + 'avg_time' => $avgTime, ]; } + + /** + * 导出日志 + * + * @param array $params + * @return string + */ + public function export(array $params): string + { + $query = $this->buildQuery($params); + $query->orderBy('created_at', 'desc'); + + $logs = $query->limit(10000)->get(); + + // 创建导出数据 + $data = []; + foreach ($logs as $log) { + $data[] = [ + 'ID' => $log->id, + '用户名' => $log->username, + '模块' => $log->module, + '操作' => $log->action, + '请求方式' => $log->method, + 'URL' => $log->url, + 'IP地址' => $log->ip, + '状态码' => $log->status_code, + '状态' => $log->status === 'success' ? '成功' : '失败', + '执行时间' => $log->execution_time . 'ms', + '创建时间' => $log->created_at, + ]; + } + + // 生成CSV文件 + $filename = '系统日志_' . date('YmdHis') . '.csv'; + $filepath = storage_path('app/exports/' . $filename); + + // 确保目录存在 + if (!file_exists(dirname($filepath))) { + mkdir(dirname($filepath), 0755, true); + } + + $file = fopen($filepath, 'w'); + // 添加BOM以支持Excel中文显示 + fprintf($file, chr(0xEF) . chr(0xBB) . chr(0xBF)); + + // 写入表头 + if (!empty($data)) { + fputcsv($file, array_keys($data[0])); + + // 写入数据 + foreach ($data as $row) { + fputcsv($file, $row); + } + } + + fclose($file); + + return $filepath; + } } diff --git a/resources/admin/src/api/auth.js b/resources/admin/src/api/auth.js index 97956e6..793f0c9 100644 --- a/resources/admin/src/api/auth.js +++ b/resources/admin/src/api/auth.js @@ -23,6 +23,17 @@ export default { }, }, + // 文件上传 + upload: { + post: async function (file) { + const formData = new FormData() + formData.append('file', file) + return await request.post('upload', formData, { + headers: { 'Content-Type': 'multipart/form-data' } + }) + }, + }, + // 用户管理 users: { list: { diff --git a/resources/admin/src/api/system.js b/resources/admin/src/api/system.js index c7c6beb..602c1f4 100644 --- a/resources/admin/src/api/system.js +++ b/resources/admin/src/api/system.js @@ -52,37 +52,45 @@ export default { // 操作日志管理 logs: { - list: { - get: async function (params) { - return await request.get('logs', { params }) + list: { + get: async function (params) { + return await request.get('logs', { params }) + }, + }, + detail: { + get: async function (id) { + return await request.get(`logs/${id}`) + }, + }, + delete: { + delete: async function (id) { + return await request.delete(`logs/${id}`) + }, + }, + batchDelete: { + post: async function (params) { + return await request.post('logs/batch-delete', params) + }, + }, + clear: { + post: async function (params) { + return await request.post('logs/clear', params) + }, + }, + export: { + get: async function (params) { + return await request.get('logs/export', { + params, + responseType: 'blob' + }) + }, + }, + statistics: { + get: async function (params) { + return await request.get('logs/statistics', { params }) + }, }, }, - detail: { - get: async function (id) { - return await request.get(`logs/${id}`) - }, - }, - delete: { - delete: async function (id) { - return await request.delete(`logs/${id}`) - }, - }, - batchDelete: { - post: async function (params) { - return await request.post('logs/batch-delete', params) - }, - }, - clear: { - post: async function (params) { - return await request.post('logs/clear', params) - }, - }, - statistics: { - get: async function (params) { - return await request.get('logs/statistics', { params }) - }, - }, - }, // 数据字典管理 dictionaries: { diff --git a/resources/admin/src/pages/auth/roles/components/CopyDialog.vue b/resources/admin/src/pages/auth/roles/components/CopyDialog.vue new file mode 100644 index 0000000..da464eb --- /dev/null +++ b/resources/admin/src/pages/auth/roles/components/CopyDialog.vue @@ -0,0 +1,125 @@ + + + diff --git a/resources/admin/src/pages/auth/roles/components/PermissionDialog.vue b/resources/admin/src/pages/auth/roles/components/PermissionDialog.vue index a934971..4c58c75 100644 --- a/resources/admin/src/pages/auth/roles/components/PermissionDialog.vue +++ b/resources/admin/src/pages/auth/roles/components/PermissionDialog.vue @@ -1,5 +1,5 @@ diff --git a/resources/admin/src/pages/auth/roles/components/SaveDialog.vue b/resources/admin/src/pages/auth/roles/components/SaveDialog.vue index 14eb025..9381ee0 100644 --- a/resources/admin/src/pages/auth/roles/components/SaveDialog.vue +++ b/resources/admin/src/pages/auth/roles/components/SaveDialog.vue @@ -1,6 +1,5 @@ diff --git a/resources/admin/src/pages/auth/roles/index.vue b/resources/admin/src/pages/auth/roles/index.vue index 0cd05a7..4fe1089 100644 --- a/resources/admin/src/pages/auth/roles/index.vue +++ b/resources/admin/src/pages/auth/roles/index.vue @@ -72,13 +72,28 @@ @@ -100,6 +115,9 @@ + + + + + diff --git a/resources/admin/src/pages/home/components/Welcome.vue b/resources/admin/src/pages/home/components/Welcome.vue new file mode 100644 index 0000000..6d5e125 --- /dev/null +++ b/resources/admin/src/pages/home/components/Welcome.vue @@ -0,0 +1,157 @@ + + + + + diff --git a/resources/admin/src/pages/home/index.vue b/resources/admin/src/pages/home/index.vue index b0244fc..dd04c90 100644 --- a/resources/admin/src/pages/home/index.vue +++ b/resources/admin/src/pages/home/index.vue @@ -1,444 +1,50 @@ diff --git a/resources/admin/src/pages/ucenter/components/BasicInfo.vue b/resources/admin/src/pages/ucenter/components/BasicInfo.vue index 157dbb4..e21315b 100644 --- a/resources/admin/src/pages/ucenter/components/BasicInfo.vue +++ b/resources/admin/src/pages/ucenter/components/BasicInfo.vue @@ -25,41 +25,48 @@ const loading = ref(false) // 表单初始值 const initialValues = computed(() => ({ username: props.userInfo.username || '', - nickname: props.userInfo.nickname || '', - mobile: props.userInfo.mobile || '', + real_name: props.userInfo.real_name || '', + phone: props.userInfo.phone || '', email: props.userInfo.email || '', - gender: props.userInfo.gender || 0, - birthday: props.userInfo.birthday || null, - bio: props.userInfo.bio || '', })) // 表单项配置 const formItems = [ - { field: 'username', label: '用户名', type: 'input' }, { - field: 'nickname', label: '昵称', type: 'input', required: true, + field: 'username', + label: '用户名', + type: 'input', rules: [ - { required: true, message: '请输入昵称', trigger: 'blur' }, - { min: 2, max: 20, message: '昵称长度在 2 到 20 个字符', trigger: 'blur' }, + { required: true, message: '请输入用户名', trigger: 'blur' }, + { min: 3, max: 20, message: '用户名长度在 3 到 20 个字符', trigger: 'blur' }, ], }, { - field: 'phone', label: '手机号', type: 'input', - rules: [{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }], - }, - { - field: 'email', label: '邮箱', type: 'input', - rules: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }], - }, - { - field: 'gender', label: '性别', type: 'radio', - options: [ - { label: '男', value: 1 }, - { label: '女', value: 2 }, - { label: '保密', value: 0 }, + field: 'real_name', + label: '真实姓名', + type: 'input', + required: true, + rules: [ + { required: true, message: '请输入真实姓名', trigger: 'blur' }, + { min: 2, max: 20, message: '姓名长度在 2 到 20 个字符', trigger: 'blur' }, + ], + }, + { + field: 'phone', + label: '手机号', + type: 'input', + rules: [ + { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }, + ], + }, + { + field: 'email', + label: '邮箱', + type: 'input', + rules: [ + { type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }, ], }, - { field: 'remark', label: '个人简介', type: 'textarea', rows: 4, maxLength: 200, showCount: true, }, ] // 表单提交 @@ -67,27 +74,32 @@ const handleFinish = async (values) => { try { loading.value = true - // 调用更新当前用户信息接口 - let res = await api.users.edit.post({ + // 获取当前用户ID + const userId = userStore.userInfo?.id + + if (!userId) { + throw new Error('用户信息不存在,请重新登录') + } + + // 调用更新用户信息接口 + const res = await api.users.edit.put(userId, { username: values.username, - nickname: values.nickname, - mobile: values.mobile, + real_name: values.real_name, + phone: values.phone, email: values.email, - gender: values.gender, - remark: values.remark, }) - if (!res || res.code !== 1) { + if (!res || res.code !== 200) { throw new Error(res.message || '保存失败,请重试') } + // 重新获取用户信息 - const response = await api.user.get() + const response = await api.me.get() if (response && response.data) { userStore.setUserInfo(response.data) + emit('update', response.data) } - // 通知父组件更新 - emit('update', values) message.success('保存成功') } catch (error) { message.error(error.message || '保存失败,请重试') diff --git a/resources/admin/src/pages/ucenter/components/Password.vue b/resources/admin/src/pages/ucenter/components/Password.vue index 427132c..ef86bf4 100644 --- a/resources/admin/src/pages/ucenter/components/Password.vue +++ b/resources/admin/src/pages/ucenter/components/Password.vue @@ -6,30 +6,36 @@ diff --git a/resources/admin/src/pages/ucenter/index.vue b/resources/admin/src/pages/ucenter/index.vue index ae0dbc3..84e4889 100644 --- a/resources/admin/src/pages/ucenter/index.vue +++ b/resources/admin/src/pages/ucenter/index.vue @@ -87,7 +87,7 @@ const initUserInfo = async () => { userInfo.value = storeUserInfo } else { // 如果 store 中没有用户信息,则从接口获取 - const response = await api.user.get() + const response = await api.me.get() if (response && response.data) { userStore.setUserInfo(response.data) userInfo.value = response.data @@ -140,21 +140,52 @@ const handleAvatarChange = ({ fileList }) => { } // 上传头像 -const handleAvatarUpload = () => { +const handleAvatarUpload = async () => { if (avatarFileList.value.length === 0) { message.warning('请先选择头像') return } - loading.value = true - // 模拟上传 - setTimeout(() => { - const file = avatarFileList.value[0] - userInfo.value.avatar = URL.createObjectURL(file.originFileObj) + + try { + loading.value = true + const file = avatarFileList.value[0].originFileObj + + // 调用上传接口 + const uploadRes = await api.upload.post(file) + if (!uploadRes || uploadRes.code !== 200) { + throw new Error(uploadRes.message || '头像上传失败') + } + + // 获取当前用户ID + const userId = userStore.userInfo?.id + if (!userId) { + throw new Error('用户信息不存在,请重新登录') + } + + // 更新用户头像 + const updateRes = await api.users.edit.put(userId, { + avatar: uploadRes.data.url, + }) + + if (!updateRes || updateRes.code !== 200) { + throw new Error(updateRes.message || '头像更新失败') + } + + // 重新获取用户信息 + const response = await api.me.get() + if (response && response.data) { + userStore.setUserInfo(response.data) + userInfo.value = response.data + } + message.success('头像更新成功') showAvatarModal.value = false avatarFileList.value = [] + } catch (error) { + message.error(error.message || '头像上传失败,请重试') + } finally { loading.value = false - }, 1000) + } } onMounted(() => { diff --git a/routes/admin.php b/routes/admin.php index 86f52f0..7f01fda 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -101,12 +101,12 @@ Route::middleware(['auth.check:admin', 'log.request'])->group(function () { // 系统操作日志 Route::prefix('logs')->group(function () { Route::get('/', [\App\Http\Controllers\System\Admin\Log::class, 'index']); + Route::get('/export', [\App\Http\Controllers\System\Admin\Log::class, 'export']); Route::get('/statistics', [\App\Http\Controllers\System\Admin\Log::class, 'getStatistics']); Route::get('/{id}', [\App\Http\Controllers\System\Admin\Log::class, 'show']); Route::delete('/{id}', [\App\Http\Controllers\System\Admin\Log::class, 'destroy']); Route::post('/batch-delete', [\App\Http\Controllers\System\Admin\Log::class, 'batchDelete']); Route::post('/clear', [\App\Http\Controllers\System\Admin\Log::class, 'clearLogs']); - Route::post('/export', [\App\Http\Controllers\System\Admin\Log::class, 'export']); }); // 数据字典管理