feat: 支持上传及查看高清原图自定义头像

This commit is contained in:
2026-04-02 17:07:24 +08:00
parent caf4742dd8
commit b4d6e0e23b
5 changed files with 44 additions and 9 deletions
+12 -7
View File
@@ -714,7 +714,7 @@ class ChatController extends Controller
public function uploadAvatar(Request $request): JsonResponse public function uploadAvatar(Request $request): JsonResponse
{ {
$request->validate([ $request->validate([
'file' => 'required|image|mimes:jpeg,png,jpg,gif,webp|max:2048', 'file' => 'required|image|mimes:jpeg,png,jpg,gif,webp|max:6144',
]); ]);
$user = Auth::user(); $user = Auth::user();
@@ -726,17 +726,22 @@ class ChatController extends Controller
try { try {
$manager = new ImageManager(new Driver); $manager = new ImageManager(new Driver);
$image = $manager->read($file);
// 裁剪正方形并压缩为 112x112
$image->cover(112, 112);
// 生成相对路径 // 生成相对路径
$filename = 'custom_'.$user->id.'_'.time().'.'.$file->extension(); $filename = 'custom_'.$user->id.'_'.time().'.'.$file->extension();
$originalFilename = 'custom_'.$user->id.'_'.time().'_original.'.$file->extension();
$path = 'avatars/'.$filename; $path = 'avatars/'.$filename;
$originalPath = 'avatars/'.$originalFilename;
// 保存以高质量 JPG 或原格式 // 1. 处理原图:限制最大宽度为 1280 以免过大,保存原比例高清大图
Storage::disk('public')->put($path, (string) $image->encode()); $originalImage = $manager->read($file);
$originalImage->scaleDown(width: 1280);
Storage::disk('public')->put($originalPath, (string) $originalImage->encode());
// 2. 处理缩略图:裁剪正方形并压缩为 112x112
$thumbImage = $manager->read($file);
$thumbImage->cover(112, 112);
Storage::disk('public')->put($path, (string) $thumbImage->encode());
$dbValue = 'storage/'.$path; $dbValue = 'storage/'.$path;
+11
View File
@@ -40,6 +40,16 @@ class UserController extends Controller
$targetUser = User::where('username', $username)->firstOrFail(); $targetUser = User::where('username', $username)->firstOrFail();
$operator = Auth::user(); $operator = Auth::user();
// 探测原图
$headfaceOriginal = $targetUser->headfaceUrl;
if (str_starts_with((string) $targetUser->headface, 'storage/')) {
$info = pathinfo($targetUser->headface);
$origPath = $info['dirname'].'/'.$info['filename'].'_original.'.($info['extension'] ?? 'jpg');
if (\Illuminate\Support\Facades\Storage::disk('public')->exists(substr($origPath, 8))) {
$headfaceOriginal = '/'.$origPath;
}
}
// 基础公开信息 // 基础公开信息
$activePosition = $targetUser->activePosition?->load('position.department')->position; $activePosition = $targetUser->activePosition?->load('position.department')->position;
$data = [ $data = [
@@ -48,6 +58,7 @@ class UserController extends Controller
1 => '男', 2 => '女', default => '' 1 => '男', 2 => '女', default => ''
}, },
'headface' => $targetUser->headface, 'headface' => $targetUser->headface,
'headface_original' => $headfaceOriginal,
'usersf' => $targetUser->usersf, 'usersf' => $targetUser->usersf,
'user_level' => $targetUser->user_level, 'user_level' => $targetUser->user_level,
'qianming' => $targetUser->qianming, 'qianming' => $targetUser->qianming,
+4 -1
View File
@@ -141,7 +141,10 @@ class User extends Authenticatable
$hf = (string) $this->usersf; $hf = (string) $this->usersf;
if (str_starts_with($hf, 'storage/')) { if (str_starts_with($hf, 'storage/')) {
$path = substr($hf, 8); // 去除 'storage/' 前缀 $path = substr($hf, 8); // 去除 'storage/' 前缀
\Illuminate\Support\Facades\Storage::disk('public')->delete($path); $info = pathinfo($path);
$origPath = $info['dirname'].'/'.$info['filename'].'_original.'.($info['extension'] ?? 'jpg');
\Illuminate\Support\Facades\Storage::disk('public')->delete([$path, $origPath]);
} }
} }
+1
View File
@@ -56,6 +56,7 @@
'username' => $botUser->username, 'username' => $botUser->username,
'level' => $botUser->user_level, 'level' => $botUser->user_level,
'sex' => $botUser->sex, 'sex' => $botUser->sex,
'headface' => $botUser->headface,
'headfaceUrl' => $botUser->headfaceUrl, 'headfaceUrl' => $botUser->headfaceUrl,
'vip_icon' => $botUser->vipIcon(), 'vip_icon' => $botUser->vipIcon(),
'vip_name' => $botUser->vipName(), 'vip_name' => $botUser->vipName(),
@@ -87,6 +87,7 @@
function userCardComponent() { function userCardComponent() {
return { return {
showUserModal: false, showUserModal: false,
showOriginalLightbox: false,
userInfo: { userInfo: {
position_history: [] position_history: []
}, },
@@ -605,7 +606,12 @@
<div class="profile-row"> <div class="profile-row">
<img class="profile-avatar" x-show="userInfo.headface" <img class="profile-avatar" x-show="userInfo.headface"
:src="(userInfo.headface || '1.gif').startsWith('storage/') ? '/' + (userInfo.headface || '1.gif') : '/images/headface/' + (userInfo.headface || '1.gif')" :src="(userInfo.headface || '1.gif').startsWith('storage/') ? '/' + (userInfo.headface || '1.gif') : '/images/headface/' + (userInfo.headface || '1.gif')"
x-on:error="$el.style.display='none'"> x-on:error="$el.style.display='none'"
style="cursor: pointer; transition: transform 0.2s;"
x-on:click="if(userInfo.headface) showOriginalLightbox = true"
title="点击查看大图"
onmouseover="this.style.transform='scale(1.05)'"
onmouseout="this.style.transform='scale(1)'">
<div class="profile-info"> <div class="profile-info">
<h4> <h4>
<span x-text="userInfo.username"></span> <span x-text="userInfo.username"></span>
@@ -1071,6 +1077,15 @@
</div> </div>
</div> </div>
</div> </div>
{{-- 头像原图全屏大图预览灯箱 --}}
<template x-if="userInfo">
<div x-show="showOriginalLightbox" style="display: none; z-index: 10000; background: rgba(0,0,0,0.85); backdrop-filter: blur(5px);" class="modal-overlay" x-on:click.self="showOriginalLightbox = false" x-transition.opacity>
<div style="position: absolute; top: 20px; right: 26px; color: rgba(255,255,255,0.7); font-size: 36px; cursor: pointer; transition: color 0.2s;" x-on:click="showOriginalLightbox = false" onmouseover="this.style.color='white'" onmouseout="this.style.color='rgba(255,255,255,0.7)'">&times;</div>
<img :src="userInfo.headface_original ? userInfo.headface_original : ((userInfo.headface || '1.gif').startsWith('storage/') ? '/' + (userInfo.headface || '1.gif') : '/images/headface/' + (userInfo.headface || '1.gif'))"
style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); max-width: 90vw; max-height: 90vh; object-fit: contain; border-radius: 8px; box-shadow: 0 10px 40px rgba(0,0,0,0.5);">
</div>
</template>
</div> </div>
{{-- ═══════════ 奖励金币独立弹窗 ═══════════ --}} {{-- ═══════════ 奖励金币独立弹窗 ═══════════ --}}