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) 入库
This commit is contained in:
@@ -16,7 +16,7 @@ import {
|
||||
describeCameraError,
|
||||
type Facing,
|
||||
} from '@/utils/camera'
|
||||
import { uploadCameraPhoto } from '@/utils/upload'
|
||||
import { uploadCameraPhoto, uploadCameraPhotos } from '@/utils/upload'
|
||||
|
||||
/** 条带高斯模糊配置 (签到表脱敏用): 对截图竖直 [start, end] 比例区间做高斯模糊 */
|
||||
export type BlurConfig = {
|
||||
@@ -29,6 +29,9 @@ export function useCamera(videoId: string, blur?: BlurConfig) {
|
||||
const facing = ref<Facing>('environment')
|
||||
const captured = ref<string>('')
|
||||
const blurred = ref<string>('')
|
||||
// 连拍多张 (签到表): 每张 = sharp + 对应的高斯模糊版, 攒批后一次提交
|
||||
const shots = ref<string[]>([])
|
||||
const blurredShots = ref<string[]>([])
|
||||
const errorMsg = ref<string>('')
|
||||
|
||||
function getVideoEl(): HTMLVideoElement | null {
|
||||
@@ -125,6 +128,47 @@ export function useCamera(videoId: string, blur?: BlurConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 连拍: 把当前这张加入批次, 清空取景回相机 (继续拍下一张) */
|
||||
function addShot() {
|
||||
if (!captured.value) return
|
||||
shots.value.push(captured.value)
|
||||
blurredShots.value.push(blurred.value || '')
|
||||
captured.value = ''
|
||||
blurred.value = ''
|
||||
}
|
||||
|
||||
/** 连拍: 删除批次里第 index 张 */
|
||||
function removeShot(index: number) {
|
||||
shots.value.splice(index, 1)
|
||||
blurredShots.value.splice(index, 1)
|
||||
}
|
||||
|
||||
/** 连拍: 把批次里所有照片一次性直传 OSS + 回传后端 */
|
||||
async function submitAll() {
|
||||
if (!shots.value.length) return
|
||||
uni.showLoading({ title: '上传中...' })
|
||||
try {
|
||||
await uploadCameraPhotos(
|
||||
shots.value.map((s, i) => ({ sharp: s, blurred: blurredShots.value[i] || '' }))
|
||||
)
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '已上传',
|
||||
icon: 'success',
|
||||
duration: 1500,
|
||||
})
|
||||
shots.value = []
|
||||
blurredShots.value = []
|
||||
} catch (err) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: (err as Error).message || '上传失败',
|
||||
icon: 'none',
|
||||
duration: 2500,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
startCamera()
|
||||
})
|
||||
@@ -145,5 +189,9 @@ export function useCamera(videoId: string, blur?: BlurConfig) {
|
||||
takePhoto,
|
||||
retake,
|
||||
confirm,
|
||||
shots,
|
||||
addShot,
|
||||
removeShot,
|
||||
submitAll,
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,24 @@
|
||||
</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">
|
||||
@@ -54,8 +72,8 @@
|
||||
<button class="preview-btn preview-btn-cancel" @click="retake">
|
||||
<text>重拍</text>
|
||||
</button>
|
||||
<button class="preview-btn preview-btn-confirm" @click="confirm">
|
||||
<text>使用</text>
|
||||
<button class="preview-btn preview-btn-confirm" @click="addShot">
|
||||
<text>加入</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -67,7 +85,8 @@ import { useCamera } from '@/composables/useCamera'
|
||||
|
||||
const videoId = 'signin-video'
|
||||
// A4 纸: 从顶边到底边的一竖条, 水平位置在 60%~70% 宽度 (签到人手机号/身份证号所在列) 做高斯模糊脱敏
|
||||
const { captured, errorMsg, startCamera, flipCamera, takePhoto, retake, confirm } =
|
||||
// 签到表支持连拍多张: shots 攒批 + 提交全部
|
||||
const { captured, errorMsg, startCamera, flipCamera, takePhoto, retake, shots, addShot, removeShot, submitAll } =
|
||||
useCamera(videoId, { start: 0.6, end: 0.7, radius: 20 })
|
||||
|
||||
function goBack() {
|
||||
@@ -315,4 +334,80 @@ function goBack() {
|
||||
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>
|
||||
+32
-6
@@ -71,24 +71,27 @@ async function uploadBase64ToOss(apiBase: string, base64: string, dir: string):
|
||||
return sign.host + '/' + key
|
||||
}
|
||||
|
||||
/** 回传 OSS URL 到后端 (POST /business/meetingMaterial/cameraUpload) */
|
||||
/** 回传 OSS URL 数组到后端 (POST /business/meetingMaterial/cameraUpload) */
|
||||
async function cameraUpload(
|
||||
apiBase: string,
|
||||
meetingId: string,
|
||||
subType: string,
|
||||
ossUrl: string,
|
||||
extraOssUrl: string
|
||||
ossUrls: string[],
|
||||
extraOssUrls: string[]
|
||||
): Promise<void> {
|
||||
const resp = await fetch(`${apiBase}/business/meetingMaterial/cameraUpload`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ meetingId: Number(meetingId), subType, ossUrl, extraOssUrl }),
|
||||
body: JSON.stringify({ meetingId: Number(meetingId), subType, ossUrls, extraOssUrls }),
|
||||
})
|
||||
const data = (await resp.json()) as { code: number; msg: string }
|
||||
if (data.code !== 200) throw new Error(data.msg || '照片回传失败')
|
||||
}
|
||||
|
||||
/** 拍照后调用: 直传 OSS + 回传 URL 存库. 签到表额外上传高斯模糊版作为 extraOssUrl. */
|
||||
/** 连拍多张: 每张 sharp + 可选高斯模糊版 (签到表). 批量直传 OSS 后一次性回传数组存库. */
|
||||
export type CameraShot = { sharp: string; blurred: string }
|
||||
|
||||
/** 拍照后调用 (单张): 直传 OSS + 回传 URL 存库. 签到表额外上传高斯模糊版作为 extraOssUrl. */
|
||||
export async function uploadCameraPhoto(base64: string, blurredBase64: string): Promise<void> {
|
||||
const p = getCameraParams()
|
||||
if (!p.apiBase) throw new Error('缺少 apiBase 参数')
|
||||
@@ -104,5 +107,28 @@ export async function uploadCameraPhoto(base64: string, blurredBase64: string):
|
||||
extraOssUrl = await uploadBase64ToOss(p.apiBase, blurredBase64, dir + 'masked/')
|
||||
}
|
||||
|
||||
await cameraUpload(p.apiBase, p.meetingId, p.subType, ossUrl, extraOssUrl)
|
||||
await cameraUpload(p.apiBase, p.meetingId, p.subType, [ossUrl], extraOssUrl ? [extraOssUrl] : [])
|
||||
}
|
||||
|
||||
/** 拍照后调用 (连拍多张): 每张直传 OSS + 签到表额外上传高斯模糊版, 一次性回传数组存库. */
|
||||
export async function uploadCameraPhotos(list: CameraShot[]): Promise<void> {
|
||||
const p = getCameraParams()
|
||||
if (!p.apiBase) throw new Error('缺少 apiBase 参数')
|
||||
if (!p.meetingId) throw new Error('缺少 meetingId 参数')
|
||||
if (!p.subType) throw new Error('缺少 subType 参数')
|
||||
if (!list || !list.length) throw new Error('没有可上传的照片')
|
||||
|
||||
const dir = `ry8080/meeting/${p.meetingId}/camera/`
|
||||
const ossUrls: string[] = []
|
||||
const extraOssUrls: string[] = []
|
||||
for (const s of list) {
|
||||
ossUrls.push(await uploadBase64ToOss(p.apiBase, s.sharp, dir))
|
||||
if (p.subType === 'L_SIGN_IN' && s.blurred) {
|
||||
extraOssUrls.push(await uploadBase64ToOss(p.apiBase, s.blurred, dir + 'masked/'))
|
||||
} else {
|
||||
extraOssUrls.push('')
|
||||
}
|
||||
}
|
||||
|
||||
await cameraUpload(p.apiBase, p.meetingId, p.subType, ossUrls, extraOssUrls)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user