v6:名册联系信息对齐 vCard + 头像与裁剪四项修正
- ① 头像上传失败(超限/非图片)不再挡保存,改为 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 加列
This commit is contained in:
+131
-76
@@ -29,6 +29,27 @@ if ($lunVal === '') {
|
||||
$lunVal = $lv;
|
||||
}
|
||||
$idBd = trim((string)($person['birthday_id_card'] ?? ''));
|
||||
// 补充联系方式(v6 起):新增时是空数组;编辑时是库里解密后的行;
|
||||
// 校验失败重画时是用户刚提交的那批(连同填错的也要原样带回)。
|
||||
$contacts = $contacts ?? [];
|
||||
foreach (['phone', 'email', 'address', 'url'] as $ctKind) {
|
||||
if (!isset($contacts[$ctKind]) || !is_array($contacts[$ctKind])) {
|
||||
$contacts[$ctKind] = [];
|
||||
}
|
||||
}
|
||||
$ctEmpty = ['label' => '', 'value' => ''];
|
||||
$ctTitles = [
|
||||
'phone' => '补充电话',
|
||||
'email' => '补充邮箱',
|
||||
'address' => '其他地址',
|
||||
'url' => '网址',
|
||||
];
|
||||
$ctHints = [
|
||||
'phone' => '主手机号在「手机号」一栏(开通登录用);这里放工作号、老人机、家里座机等',
|
||||
'email' => '主邮箱在「邮箱」一栏;这里放其他邮箱',
|
||||
'address' => '住址在「住址」一栏;这里放单位地址、老家地址等',
|
||||
'url' => '主页、博客、店铺链接……',
|
||||
];
|
||||
?>
|
||||
|
||||
<div class="page-head">
|
||||
@@ -80,7 +101,116 @@ $idBd = trim((string)($person['birthday_id_card'] ?? ''));
|
||||
写进这张(全家人共用的)资料表单里是错的:我把他归成"同事"
|
||||
不该把妈妈那边的归类也改掉。 */ ?>
|
||||
|
||||
<!-- ==================== 三个生日 ==================== -->
|
||||
<!-- 生日块已挪到表单最底部:大多数情况下生日由身份证号读出、三个都对,
|
||||
不需要动(用户原话:生日什么的应该放下面)。 -->
|
||||
|
||||
<div class="field full">
|
||||
<label for="slogan">这个人的一句话</label>
|
||||
<input class="input" id="slogan" name="slogan" maxlength="200"
|
||||
value="<?= e(old('slogan', $person['slogan'] ?? '')) ?>"
|
||||
placeholder="可留空;留空时用家庭统一的那句">
|
||||
<span class="tip">显示在这个人自己的首页标题下面。本人也可以在自己的资料页里改。</span>
|
||||
<?php if (!empty($errors['slogan'])): ?><span class="err"><?= e($errors['slogan']) ?></span><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($canEditAll): ?>
|
||||
<div class="field">
|
||||
<label for="phone">手机号</label>
|
||||
<input class="input" id="phone" name="phone" inputmode="numeric"
|
||||
value="<?= e(old('phone', $phoneValue ?? '')) ?>" placeholder="可留空">
|
||||
<span class="tip">开通登录后用它登录;加密存储,列表里只显示部分。其他号码放下面的「补充电话」。</span>
|
||||
<?php if (!empty($errors['phone'])): ?><span class="err"><?= e($errors['phone']) ?></span><?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="field">
|
||||
<label for="email">邮箱</label>
|
||||
<input class="input" id="email" name="email" type="email"
|
||||
value="<?= e(old('email', $person['email'] ?? '')) ?>" placeholder="可留空">
|
||||
<?php if (!empty($errors['email'])): ?><span class="err"><?= e($errors['email']) ?></span><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="org">工作单位</label>
|
||||
<input class="input" id="org" name="org"
|
||||
value="<?= e(old('org', $person['org'] ?? '')) ?>" placeholder="可留空">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="title">职务 / 职称</label>
|
||||
<input class="input" id="title" name="title"
|
||||
value="<?= e(old('title', $person['title'] ?? '')) ?>" placeholder="可留空">
|
||||
</div>
|
||||
|
||||
<?php if ($canEditAll): ?>
|
||||
<div class="field full">
|
||||
<label for="id_card">身份证号</label>
|
||||
<input class="input mono" id="id_card" name="id_card"
|
||||
value="<?= e(old('id_card', $idCardValue ?? '')) ?>" placeholder="可留空">
|
||||
<span class="tip">
|
||||
加密存储,页面上一律只显示前 6 后 4 位。
|
||||
<b>填了它,最下面的「身份证生日」会自动读出来</b>(当前:<span id="idCardBirthday"><?= $idBd !== '' ? e($idBd) : '' ?></span>),
|
||||
这个日期系统永远不会改动。家庭场景里真正需要它的场合很少 —— 能不填就别填。
|
||||
</span>
|
||||
<?php if (!empty($errors['id_card'])): ?><span class="err"><?= e($errors['id_card']) ?></span><?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="field full">
|
||||
<label for="address">住址</label>
|
||||
<input class="input" id="address" name="address"
|
||||
value="<?= e(old('address', $person['address'] ?? '')) ?>" placeholder="可留空">
|
||||
</div>
|
||||
|
||||
<div class="field full">
|
||||
<label for="note">备注</label>
|
||||
<textarea class="input" id="note" name="note" placeholder="爱吃辣、对芒果过敏、怕坐过山车……"><?= e(old('note', $person['note'] ?? '')) ?></textarea>
|
||||
</div>
|
||||
|
||||
<!-- ==================== 补充联系方式(可多条,对齐 vCard) ==================== -->
|
||||
<?php foreach (['phone', 'email', 'address', 'url'] as $ctKind): ?>
|
||||
<div class="field full ct-block" data-ct-kind="<?= $ctKind ?>">
|
||||
<label><?= $ctTitles[$ctKind] ?></label>
|
||||
<p class="small muted" style="margin:0 0 8px"><?= $ctHints[$ctKind] ?></p>
|
||||
<?php foreach (array_merge($contacts[$ctKind], [$ctEmpty]) as $ctRow): ?>
|
||||
<div class="ct-row" data-ct-row>
|
||||
<input class="input ct-label" name="ct_<?= $ctKind ?>_label[]" maxlength="20"
|
||||
value="<?= e((string)($ctRow['label'] ?? '')) ?>" placeholder="标注(如:工作)">
|
||||
<input class="input" name="ct_<?= $ctKind ?>_value[]"
|
||||
value="<?= e((string)($ctRow['value'] ?? '')) ?>"
|
||||
placeholder="<?= $ctKind === 'phone' ? '号码' : ($ctKind === 'url' ? 'https://…' : '内容') ?>">
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<button type="button" class="btn btn-ghost btn-sm ct-add">再加一个</button>
|
||||
<?php if (!empty($errors['contacts'])): ?><span class="err"><?= e($errors['contacts']) ?></span><?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php
|
||||
// 裁剪比例不是拍脑袋定的,而是由「这个位置显示成什么形状」决定(见 App\ImageSlot)。
|
||||
// 前端拿这些 data-* 去生成裁剪框,服务端拿同一个槽位去最终裁切 —— 一处定义、两处使用。
|
||||
$avSlot = \App\ImageSlot::get('avatar');
|
||||
?>
|
||||
<div class="field">
|
||||
<label for="avatar">头像</label>
|
||||
<input class="input" id="avatar" name="avatar" type="file" accept="image/*"
|
||||
data-crop-ratio="<?= e((string)$avSlot['ratio']) ?>"
|
||||
data-crop-w="<?= (int)$avSlot['w'] ?>"
|
||||
data-crop-h="<?= (int)$avSlot['h'] ?>"
|
||||
data-crop-label="<?= e((string)$avSlot['label']) ?>"
|
||||
data-crop-hint="<?= e((string)$avSlot['hint']) ?>"
|
||||
data-crop-preview="#avatar-preview">
|
||||
<?php /* 用户挑的裁剪框(归一化的,相对原图 0..1);不裁就留空 = 居中裁切 */ ?>
|
||||
<input type="hidden" name="avatar_crop" value="">
|
||||
<span class="tip">选好图片后会弹出裁剪框,把要留的部分框住。<b>比例按它在页面上的形状定</b>(头像是 1:1 正方形)。多大都能传,保存时自动缩放成 <?= (int)$avSlot['w'] ?>×<?= (int)$avSlot['h'] ?>。</span>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>当前头像</label>
|
||||
<img id="avatar-preview" src="<?= e($avatarUrl) ?>" alt=""
|
||||
style="width:88px;height:88px;border-radius:12px;object-fit:cover;border:1px solid var(--line)">
|
||||
</div>
|
||||
|
||||
<!-- ==================== 三个生日(放在最后:多数情况不用动) ==================== -->
|
||||
<div class="field full">
|
||||
<label>生日</label>
|
||||
<p class="small muted" style="margin:0 0 12px">
|
||||
@@ -134,81 +264,6 @@ $idBd = trim((string)($person['birthday_id_card'] ?? ''));
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field full">
|
||||
<label for="slogan">这个人的一句话</label>
|
||||
<input class="input" id="slogan" name="slogan" maxlength="200"
|
||||
value="<?= e(old('slogan', $person['slogan'] ?? '')) ?>"
|
||||
placeholder="可留空;留空时用家庭统一的那句">
|
||||
<span class="tip">显示在这个人自己的首页标题下面。本人也可以在自己的资料页里改。</span>
|
||||
<?php if (!empty($errors['slogan'])): ?><span class="err"><?= e($errors['slogan']) ?></span><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($canEditAll): ?>
|
||||
<div class="field">
|
||||
<label for="phone">手机号</label>
|
||||
<input class="input" id="phone" name="phone" inputmode="numeric"
|
||||
value="<?= e(old('phone', $phoneValue ?? '')) ?>" placeholder="可留空">
|
||||
<span class="tip">开通登录后用它登录;加密存储,列表里只显示部分。</span>
|
||||
<?php if (!empty($errors['phone'])): ?><span class="err"><?= e($errors['phone']) ?></span><?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="field">
|
||||
<label for="email">邮箱</label>
|
||||
<input class="input" id="email" name="email" type="email"
|
||||
value="<?= e(old('email', $person['email'] ?? '')) ?>">
|
||||
<?php if (!empty($errors['email'])): ?><span class="err"><?= e($errors['email']) ?></span><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($canEditAll): ?>
|
||||
<div class="field full">
|
||||
<label for="id_card">身份证号</label>
|
||||
<input class="input mono" id="id_card" name="id_card"
|
||||
value="<?= e(old('id_card', $idCardValue ?? '')) ?>" placeholder="可留空">
|
||||
<span class="tip">
|
||||
加密存储,页面上一律只显示前 6 后 4 位。
|
||||
<b>填了它,上面的「身份证生日」会自动读出来</b>(当前:<span id="idCardBirthday"><?= $idBd !== '' ? e($idBd) : '' ?></span>),
|
||||
这个日期系统永远不会改动。家庭场景里真正需要它的场合很少 —— 能不填就别填。
|
||||
</span>
|
||||
<?php if (!empty($errors['id_card'])): ?><span class="err"><?= e($errors['id_card']) ?></span><?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="field full">
|
||||
<label for="address">住址</label>
|
||||
<input class="input" id="address" name="address"
|
||||
value="<?= e(old('address', $person['address'] ?? '')) ?>">
|
||||
</div>
|
||||
|
||||
<div class="field full">
|
||||
<label for="note">备注</label>
|
||||
<textarea class="input" id="note" name="note" placeholder="爱吃辣、对芒果过敏、怕坐过山车……"><?= e(old('note', $person['note'] ?? '')) ?></textarea>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// 裁剪比例不是拍脑袋定的,而是由「这个位置显示成什么形状」决定(见 App\ImageSlot)。
|
||||
// 前端拿这些 data-* 去生成裁剪框,服务端拿同一个槽位去最终裁切 —— 一处定义、两处使用。
|
||||
$avSlot = \App\ImageSlot::get('avatar');
|
||||
?>
|
||||
<div class="field">
|
||||
<label for="avatar">头像</label>
|
||||
<input class="input" id="avatar" name="avatar" type="file" accept="image/*"
|
||||
data-crop-ratio="<?= e((string)$avSlot['ratio']) ?>"
|
||||
data-crop-w="<?= (int)$avSlot['w'] ?>"
|
||||
data-crop-h="<?= (int)$avSlot['h'] ?>"
|
||||
data-crop-label="<?= e((string)$avSlot['label']) ?>"
|
||||
data-crop-hint="<?= e((string)$avSlot['hint']) ?>"
|
||||
data-crop-preview="#avatar-preview">
|
||||
<?php /* 用户挑的裁剪框(归一化的,相对原图 0..1);不裁就留空 = 居中裁切 */ ?>
|
||||
<input type="hidden" name="avatar_crop" value="">
|
||||
<span class="tip">选好图片后会弹出裁剪框,把要留的部分框住。<b>比例按它在页面上的形状定</b>(头像是 1:1 正方形)。上限 5MB。</span>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>当前头像</label>
|
||||
<img id="avatar-preview" src="<?= e($avatarUrl) ?>" alt=""
|
||||
style="width:88px;height:88px;border-radius:12px;object-fit:cover;border:1px solid var(--line)">
|
||||
</div>
|
||||
|
||||
<?php if ($canEditAll): ?>
|
||||
<div class="field full">
|
||||
<label class="check-item">
|
||||
|
||||
@@ -157,6 +157,12 @@ $shortKind = ['id' => '身份证生日', 'solar' => '阳历生日', 'lunar' =
|
||||
<dt>手机号</dt>
|
||||
<dd class="mono"><?= e(\App\Crypto::maskPhone((string)($p['phone_encrypted'] ?? ''))) ?></dd>
|
||||
</div>
|
||||
<?php foreach (($contacts['phone'] ?? []) as $r): ?>
|
||||
<div class="kv-row">
|
||||
<dt>电话<?= (string)$r['label'] !== '' ? '(' . e((string)$r['label']) . ')' : '' ?></dt>
|
||||
<dd class="mono"><?= e(\App\Crypto::maskPhone((string)$r['value'])) ?></dd>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<div class="kv-row">
|
||||
<dt>身份证号</dt>
|
||||
<dd class="mono"><?= e(\App\Crypto::maskIdCard((string)($p['id_card_encrypted'] ?? ''))) ?></dd>
|
||||
@@ -164,9 +170,33 @@ $shortKind = ['id' => '身份证生日', 'solar' => '阳历生日', 'lunar' =
|
||||
<?php if (!empty($p['email'])): ?>
|
||||
<div class="kv-row"><dt>邮箱</dt><dd><?= e($p['email']) ?></dd></div>
|
||||
<?php endif; ?>
|
||||
<?php foreach (($contacts['email'] ?? []) as $r): ?>
|
||||
<div class="kv-row">
|
||||
<dt>邮箱<?= (string)$r['label'] !== '' ? '(' . e((string)$r['label']) . ')' : '' ?></dt>
|
||||
<dd><?= e((string)$r['value']) ?></dd>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php if (!empty($p['org']) || !empty($p['title'])): ?>
|
||||
<div class="kv-row">
|
||||
<dt>工作</dt>
|
||||
<dd><?= e(trim((string)$p['org'] . (($p['org'] ?? '') !== '' && ($p['title'] ?? '') !== '' ? ' · ' : '') . (string)$p['title'])) ?></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($p['address'])): ?>
|
||||
<div class="kv-row"><dt>住址</dt><dd><?= e($p['address']) ?></dd></div>
|
||||
<?php endif; ?>
|
||||
<?php foreach (($contacts['address'] ?? []) as $r): ?>
|
||||
<div class="kv-row">
|
||||
<dt>地址<?= (string)$r['label'] !== '' ? '(' . e((string)$r['label']) . ')' : '' ?></dt>
|
||||
<dd><?= e((string)$r['value']) ?></dd>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php foreach (($contacts['url'] ?? []) as $r): ?>
|
||||
<div class="kv-row">
|
||||
<dt>网址<?= (string)$r['label'] !== '' ? '(' . e((string)$r['label']) . ')' : '' ?></dt>
|
||||
<dd class="mono"><?= e((string)$r['value']) ?></dd>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php if (!empty($p['note'])): ?>
|
||||
<div class="kv-row"><dt>备注</dt><dd><?= nl2br(e($p['note'])) ?></dd></div>
|
||||
<?php endif; ?>
|
||||
|
||||
Reference in New Issue
Block a user