- ① 头像上传失败(超限/非图片)不再挡保存,改为 flash 提示「已加进名册。头像没有保存:…」 ——这是「添加不了」的真根因(错误被塞进 errors.avatar 把整条新增打回) - ② 表单顺序:生日整块下移(身份证能读出生日时无需常改) - ③ 裁剪缩放语义反转:框在屏幕上的位置与大小不动、动它底下的图; 落库归一化矩形随倍率缩小(放大=同一块区域看得更清楚) - ④ 取消应用层 5MB 兆数上限;按像素动态上调 memory_limit(px*6+64M,封顶 768M); 头像输出恒为 512x512 - ⑤ 成员信息对齐 vCard:person_contacts 表(phone/email/address/url 可多行, value 加密落库)+ persons.org(工作单位)/title(职务职称);均可留空 Schema v5→v6:person_contacts 建表 + persons.org/title 加列
85 lines
3.3 KiB
PHP
85 lines
3.3 KiB
PHP
<?php
|
||
/**
|
||
* 上传位置(「槽位」)的声明式配置。
|
||
*
|
||
* 为什么要有这个类:**比例是由「这张图显示出来是什么形状」决定的**,不是由上传者决定的。
|
||
* 头像在页面上永远是圆的/圆角方的 → 1:1;宽幅题图是横的 → 16:9。
|
||
* 把这些写在一处,前端拿它去做裁剪框的锁定比例、服务端拿它去做最终裁切,
|
||
* 两边就不会各写一个数字然后慢慢漂移。
|
||
*
|
||
* 加一个新位置(比如题图)只需要在 SLOTS 里加一行,前端与服务端都不用改:
|
||
* 'cover' => ['label' => '题图', 'ratio' => '16:9', 'w' => 1920, 'h' => 1080,
|
||
* 'dir' => 'covers', 'hint' => '宽屏横幅,显示在详情页顶部'],
|
||
*/
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App;
|
||
|
||
final class ImageSlot
|
||
{
|
||
// ⚠️ 应用层不再设「兆数」上限(用户原话:上传照片不要有大小限制)。
|
||
// 服务器 ini(upload_max_filesize / post_max_size,线上是 50M)是唯一的天花板;
|
||
// 像素与内存的守卫在 Upload::image() 里按图动态算。
|
||
// 输出尺寸恒为槽位的 w×h(头像 512×512)——原图再大,落库的都是小 JPEG。
|
||
|
||
/**
|
||
* 位置 → 比例与输出尺寸。
|
||
*
|
||
* `dir` 是**存储前缀**:本地就是 uploads/<dir>/,云端就是桶里的 <prefix>/<dir>/。
|
||
* `w`/`h` 是**输出**尺寸(不是要求上传者提供的尺寸)——
|
||
* 裁剪之后统一缩到这个大小,所以原图多大都不影响页面上的加载量。
|
||
*
|
||
* ratio 写成字符串是为了让人一眼看懂,也方便直接拿去当 CSS 的 aspect-ratio。
|
||
*/
|
||
public const SLOTS = [
|
||
'avatar' => [
|
||
'label' => '头像',
|
||
'ratio' => '1:1',
|
||
'w' => 512,
|
||
'h' => 512,
|
||
'dir' => 'avatars',
|
||
'hint' => '正方形。页面上显示成圆形或圆角方形,所以按 1:1 裁。',
|
||
],
|
||
// 题图按 16:9 —— 宽屏横幅。现在还没有位置用它,需要时把它启用即可:
|
||
// 'cover' => ['label' => '题图', 'ratio' => '16:9', 'w' => 1920, 'h' => 1080,
|
||
// 'dir' => 'covers', 'hint' => '宽屏横幅,显示在详情页顶部。'],
|
||
];
|
||
|
||
public static function exists(string $key): bool
|
||
{
|
||
return isset(self::SLOTS[$key]);
|
||
}
|
||
|
||
/** @return array<string,mixed>|null */
|
||
public static function get(string $key): ?array
|
||
{
|
||
return self::SLOTS[$key] ?? null;
|
||
}
|
||
|
||
/** 宽高比(宽 / 高)。'1:1' → 1.0,'16:9' → 1.777… */
|
||
public static function ratio(array $slot): float
|
||
{
|
||
[$a, $b] = self::parseRatio((string)($slot['ratio'] ?? '1:1'));
|
||
return $b > 0 ? $a / $b : 1.0;
|
||
}
|
||
|
||
/** 给前端用的 CSS aspect-ratio 写法:'1 / 1'、'16 / 9' */
|
||
public static function ratioCss(array $slot): string
|
||
{
|
||
[$a, $b] = self::parseRatio((string)($slot['ratio'] ?? '1:1'));
|
||
return $a . ' / ' . $b;
|
||
}
|
||
|
||
/** @return array{0:int,1:int} */
|
||
private static function parseRatio(string $ratio): array
|
||
{
|
||
if (preg_match('/^\s*(\d+)\s*[:/]\s*(\d+)\s*$/', $ratio, $m)) {
|
||
$a = max(1, (int)$m[1]);
|
||
$b = max(1, (int)$m[2]);
|
||
return [$a, $b];
|
||
}
|
||
return [1, 1];
|
||
}
|
||
}
|