Files
guoju0808/ry-h5/pages/signin/index.uvue
T
郭庆泰 5a0a892574 feat: 邀请函邮件发送 + 首次设密/密码为空 + 姓名单一可信源
邀请发送 (菜单暂隐藏, 待确认参数见 发送邀请-待确认参数.md):
- 后端: BizInvite / BizInviteRecipient + Controller/Service/Mapper/XML
- 邮件: InviteMailSender (spring-boot-starter-mail SMTP) + application*.yml 邮件配置
- 上传进度: UploadProgressRegistry + UploadProgressController
- 前端: InviteList / InviteNew / InviteDetail / InviteView + api/business/invite.js
- 原型: proto/html/components/invite-detail / new-invitation / send-invitation

登录/账号:
- 首次设密: /getInfo 返回 isPasswordEmpty, SysProfileController 密码为空时跳过旧密码校验, ForcePasswordDialog 强制弹窗
- 姓名单一可信源 resolveDisplayName: doctor→biz_expert.name, sponsor/executor→biz_person.name, 其余回退 nick_name
- OA compliance 门禁改为按手机号查 ecology 视图 (不再限定 manager/leader)

其它:
- OSS zip 在线查看 (列清单+取单文件, 公开只读) + SecurityConfig permitAll
- doctor 项目详情 ProjectDetail.vue
- 数据库/测试/设计文档 (md) 入库
2026-09-10 20:40:49 +08:00

413 lines
7.8 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="camera-page">
<!-- 顶部导航 -->
<view class="topbar">
<view class="back-btn" @click="goBack">
<text class="back-icon"></text>
</view>
<text class="topbar-title">签到表取景 (A4)</text>
<view class="placeholder" />
</view>
<!-- 视频 + overlay -->
<view class="viewport">
<video
id="signin-video"
class="video"
muted
playsinline
/>
<image
class="frame signin-frame"
src="/static/overlay/signin.svg"
mode="widthFix"
/>
<view v-if="errorMsg" class="error-layer">
<text class="error-text">{{ errorMsg }}</text>
<button class="retry-btn" @click="startCamera">重试</button>
</view>
</view>
<!-- 连拍已选照片条 -->
<view v-if="shots.length" class="shot-strip">
<view class="shot-scroll">
<view v-for="(s, i) in shots" :key="i" class="shot-thumb">
<image class="shot-img" :src="s" mode="aspectFill" />
<view class="shot-del" @click="removeShot(i)">
<text>×</text>
</view>
</view>
</view>
<view class="shot-bar">
<text class="shot-count">已拍 {{ shots.length }} 张</text>
<button class="submit-btn" @click="submitAll">
<text>提交全部</text>
</button>
</view>
</view>
<!-- 底部控制条 -->
<view class="controls">
<button class="ctrl-btn" @click="flipCamera">
<text class="ctrl-icon">⟲</text>
<text class="ctrl-label">翻转</text>
</button>
<view class="shutter" @click="takePhoto">
<view class="shutter-inner" />
</view>
<view class="ctrl-btn-placeholder" />
</view>
<!-- 拍照预览层 -->
<view v-if="captured" class="preview">
<image
class="preview-img"
:src="captured"
mode="aspectFit"
/>
<view class="preview-actions">
<button class="preview-btn preview-btn-cancel" @click="retake">
<text>重拍</text>
</button>
<button class="preview-btn preview-btn-confirm" @click="addShot">
<text>加入</text>
</button>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { useCamera } from '@/composables/useCamera'
const videoId = 'signin-video'
// A4 纸: 从顶边到底边的一竖条, 水平位置在 60%~70% 宽度 (签到人手机号/身份证号所在列) 做高斯模糊脱敏
// 签到表支持连拍多张: shots 攒批 + 提交全部
const { captured, errorMsg, startCamera, flipCamera, takePhoto, retake, shots, addShot, removeShot, submitAll } =
useCamera(videoId, { start: 0.6, end: 0.7, radius: 20 })
function goBack() {
uni.navigateBack()
}
</script>
<style>
.camera-page {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: transparent;
display: flex;
flex-direction: column;
overflow: hidden;
z-index: 3;
}
/* ========== 顶部导航 ========== */
.topbar {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 20rpx 30rpx;
padding-top: calc(20rpx + env(safe-area-inset-top));
background: rgba(0, 0, 0, 0.7);
}
.back-btn {
width: 80rpx;
height: 80rpx;
display: flex;
justify-content: center;
align-items: center;
}
.back-icon {
color: #fff;
font-size: 60rpx;
font-weight: bold;
}
.topbar-title {
color: #fff;
font-size: 32rpx;
font-weight: bold;
}
.placeholder {
width: 80rpx;
}
/* ========== 视频取景区 ========== */
.viewport {
position: relative;
flex: 1;
overflow: hidden;
background: transparent;
display: flex;
justify-content: center;
align-items: center;
}
.video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 0;
}
.frame {
position: relative;
z-index: 1;
pointer-events: none;
}
/* A4 框: 宽度 90% 视口,SVG 内置 1:√2 比例 */
.signin-frame {
width: 90%;
height: auto;
}
/* ========== 错误层 ========== */
.error-layer {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
padding: 40rpx;
background: rgba(0, 0, 0, 0.75);
border-radius: 16rpx;
max-width: 80%;
z-index: 2;
}
.error-text {
color: #fff;
font-size: 28rpx;
text-align: center;
margin-bottom: 24rpx;
line-height: 1.5;
}
.retry-btn {
padding: 12rpx 48rpx;
background: #FFD600;
color: #000;
border-radius: 32rpx;
font-size: 26rpx;
font-weight: bold;
}
/* ========== 底部控制 ========== */
.controls {
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: center;
padding: 40rpx 60rpx;
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
background: rgba(0, 0, 0, 0.7);
}
.ctrl-btn {
display: flex;
flex-direction: column;
align-items: center;
background: transparent;
min-width: 120rpx;
}
.ctrl-icon {
color: #fff;
font-size: 40rpx;
line-height: 1;
margin-bottom: 8rpx;
}
.ctrl-label {
color: #ccc;
font-size: 22rpx;
}
.ctrl-btn-placeholder {
min-width: 120rpx;
}
.shutter {
width: 144rpx;
height: 144rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.25);
border: 6rpx solid #fff;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 0 0 4rpx rgba(0, 0, 0, 0.3);
}
.shutter:active {
background: rgba(255, 255, 255, 0.4);
transform: scale(0.95);
}
.shutter-inner {
width: 110rpx;
height: 110rpx;
border-radius: 50%;
background: #fff;
}
/* ========== 预览层 ========== */
.preview {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #000;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 100;
}
.preview-img {
width: 100%;
flex: 1;
}
.preview-actions {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
width: 100%;
padding: 40rpx 60rpx;
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
}
.preview-btn {
/* flex item: 在 .preview-actions 里 flex:1 平分宽度 */
flex: 1 1 0%;
min-width: 0;
box-sizing: border-box;
/* flex container: 让内部 <text> 真正居中 */
display: flex;
justify-content: center;
align-items: center;
/* 重置浏览器 / uni-app H5 给 <button> 的默认外观 (边框/appearance 是按钮不对称的常见根因) */
margin: 0 16rpx;
padding: 0;
border: none;
outline: none;
appearance: none;
-webkit-appearance: none;
border-radius: 48rpx;
font-size: 30rpx;
font-weight: bold;
line-height: 1;
min-height: 88rpx;
}
.preview-btn-cancel {
background: rgba(255, 255, 255, 0.2);
color: #fff;
}
.preview-btn-confirm {
background: #FFD600;
color: #000;
}
/* ========== 连拍已选照片条 ========== */
.shot-strip {
background: rgba(0, 0, 0, 0.7);
padding: 20rpx 30rpx;
}
.shot-scroll {
display: flex;
flex-direction: row;
overflow-x: auto;
}
.shot-thumb {
position: relative;
width: 120rpx;
height: 160rpx;
margin-right: 16rpx;
flex-shrink: 0;
border: 2rpx solid #fff;
border-radius: 8rpx;
overflow: hidden;
}
.shot-img {
width: 100%;
height: 100%;
}
.shot-del {
position: absolute;
top: 0;
right: 0;
width: 40rpx;
height: 40rpx;
background: rgba(0, 0, 0, 0.6);
border-radius: 0 0 0 12rpx;
display: flex;
justify-content: center;
align-items: center;
}
.shot-del text {
color: #fff;
font-size: 30rpx;
line-height: 1;
}
.shot-bar {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-top: 16rpx;
}
.shot-count {
color: #ccc;
font-size: 26rpx;
}
.submit-btn {
margin: 0;
padding: 0 40rpx;
min-height: 68rpx;
border: none;
outline: none;
appearance: none;
-webkit-appearance: none;
border-radius: 34rpx;
background: #FFD600;
color: #000;
font-size: 28rpx;
font-weight: bold;
line-height: 68rpx;
}
</style>