- 生产 nginx 三路径: ry-vue3 /hg/, ry-h5 /camera/, API /hg-api, .env 分环境 - 签约链接/摄像头 base-url 走 yml, 不再硬编码 /hg 与 localhost - 新增 biz_project_executor_assign (镜像 sponsor_assign) 执行方项目级分配 - 会议费用后台汇总 FeeCalcScheduler + 结算回写项目金额 + 状态流转调度 - 签到表拍照高斯模糊脱敏 extra_oss_url, 新增 PosterService/StageDeriver - 清理 biz_support_intent 旧表 (6 Java/XML 删除) - ry-h5 相机黑屏修复: 显式 video.play() + 动态 apiBase 上传 - 资源: simhei 字体 / logo.png / qrcode_1.png / favicon.ico Co-Authored-By: Claude <noreply@anthropic.com>
151 lines
4.9 KiB
Vue
151 lines
4.9 KiB
Vue
<!--
|
|
身份证正反面上传控件 (仿 hwt-code gr-id-card, 适配 PC 后台)
|
|
单 v-model, 内部以 CSV "frontUrl,backUrl" 存到一个字段:
|
|
<id-card-uploader v-model="form.idCardAttachments" />
|
|
|
|
readonly=true 时不显示上传按钮, 点击 thumb 弹 el-image 预览
|
|
上传走 OSS 直传 (utils/oss.js 的 uploadToOss)
|
|
-->
|
|
<template>
|
|
<div class="ht-image-upload-content" :class="{ 'has-file': frontUrl }" @click="handleClick('front')">
|
|
<div v-if="!frontUrl && !readonly" class="ht-image-placeholder-wrap">
|
|
<img class="ht-image-placeholder" :src="frontPlaceholder" />
|
|
</div>
|
|
<div v-else-if="!frontUrl && readonly" style="width: 90px;height: 60px; background: #f2f4f5;"></div>
|
|
<img v-else-if="!readonly" class="ht-image" :src="frontUrl" />
|
|
<el-image v-else :src="frontUrl" style="width: 90px;height: 60px; display: flex; justify-content: center; align-items: center; background: #f2f4f5;"
|
|
:preview-src-list="[frontUrl]"></el-image>
|
|
</div>
|
|
<div class="ht-image-upload-content" :class="{ 'has-file': backUrl }" style="margin-left:20px" @click="handleClick('back')">
|
|
<div v-if="!backUrl && !readonly" class="ht-image-placeholder-wrap">
|
|
<img class="ht-image-placeholder" :src="backPlaceholder" />
|
|
</div>
|
|
<div v-else-if="!backUrl && readonly" style="width: 90px;height: 60px; background: #f2f4f5;"></div>
|
|
<img v-else-if="!readonly" class="ht-image" :src="backUrl" />
|
|
<el-image v-else :src="backUrl" style="width: 90px;height: 60px; display: flex; justify-content: center; align-items: center; background: #f2f4f5;"
|
|
:preview-src-list="[backUrl]"></el-image>
|
|
</div>
|
|
<!-- hidden file input triggered by thumb click -->
|
|
<input
|
|
type="file"
|
|
ref="fileInputFront"
|
|
accept="image/*"
|
|
style="display:none"
|
|
@change="(e) => onFileChange(e, 'front')"
|
|
/>
|
|
<input
|
|
type="file"
|
|
ref="fileInputBack"
|
|
accept="image/*"
|
|
style="display:none"
|
|
@change="(e) => onFileChange(e, 'back')"
|
|
/>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed } from 'vue'
|
|
import { ElMessage } from 'element-plus'
|
|
import { uploadToOss } from '@/utils/oss'
|
|
|
|
/** CSV 拆分: 允许任意空段, 返回 [front, back], 空段视为 '' */
|
|
function parseCsv(str) {
|
|
if (!str || typeof str !== 'string') return ['', '']
|
|
const parts = str.split(',')
|
|
return [parts[0] || '', parts[1] || '']
|
|
}
|
|
|
|
/** 拼回 CSV: 始终 2 段 (空字符串保留位), 避免 "a" 被读成 ["a",""] 没问题但 "a," 被读成 ["a",""] */
|
|
function joinCsv(front, back) {
|
|
return [front || '', back || ''].join(',')
|
|
}
|
|
|
|
const props = defineProps({
|
|
/** CSV 字符串, "frontUrl,backUrl", 任意段为空保留位 */
|
|
modelValue: { type: String, default: '' },
|
|
dir: { type: String, default: 'ry8080/idcard/' },
|
|
readonly: { type: Boolean, default: false },
|
|
frontPlaceholder: { type: String, default: import.meta.env.BASE_URL + 'images/id-front.png' },
|
|
backPlaceholder: { type: String, default: import.meta.env.BASE_URL + 'images/id-back.png' }
|
|
})
|
|
|
|
const emit = defineEmits(['update:modelValue'])
|
|
|
|
const fileInputFront = ref(null)
|
|
const fileInputBack = ref(null)
|
|
const uploading = ref({ front: false, back: false })
|
|
|
|
// 内部 front/back URL 由 modelValue 派生
|
|
const frontUrl = computed(() => parseCsv(props.modelValue)[0])
|
|
const backUrl = computed(() => parseCsv(props.modelValue)[1])
|
|
|
|
function handleClick(side) {
|
|
if (props.readonly) return
|
|
if (uploading.value[side]) return
|
|
const el = side === 'front' ? fileInputFront.value : fileInputBack.value
|
|
el?.click()
|
|
}
|
|
|
|
async function onFileChange(e, side) {
|
|
const file = e.target.files?.[0]
|
|
e.target.value = ''
|
|
if (!file) return
|
|
if (!file.type.startsWith('image/')) {
|
|
ElMessage.warning('只能上传图片文件')
|
|
return
|
|
}
|
|
if (file.size / 1024 / 1024 > 10) {
|
|
ElMessage.warning('图片大小不能超过 10MB')
|
|
return
|
|
}
|
|
uploading.value[side] = true
|
|
try {
|
|
const url = await uploadToOss(file, props.dir)
|
|
const [f, b] = parseCsv(props.modelValue)
|
|
const next = side === 'front' ? joinCsv(url, b) : joinCsv(f, url)
|
|
emit('update:modelValue', next)
|
|
ElMessage.success(side === 'front' ? '身份证正面已上传' : '身份证反面已上传')
|
|
} catch (err) {
|
|
ElMessage.error(err.message || '上传失败')
|
|
} finally {
|
|
uploading.value[side] = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.ht-image-upload-content {
|
|
display: flex;
|
|
justify-content: flex-start;
|
|
border: 1px dashed #d9d9d9;
|
|
border-radius: 4px;
|
|
width: 90px;
|
|
height: 60px;
|
|
box-sizing: border-box;
|
|
overflow: hidden;
|
|
cursor: pointer;
|
|
}
|
|
.ht-image-upload-content:hover {
|
|
border-color: var(--brand-primary);
|
|
}
|
|
.ht-image-upload-content.has-file {
|
|
border-style: solid;
|
|
border-color: #52c41a;
|
|
}
|
|
.ht-image-placeholder-wrap {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
.ht-image-placeholder {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
.ht-image {
|
|
width: 100%;
|
|
height: 100%;
|
|
background: #f2f4f5;
|
|
object-fit: cover;
|
|
display: block;
|
|
}
|
|
</style>
|