upload project

This commit is contained in:
unknown 2025-12-27 17:16:03 +08:00
commit 06961cae04
422 changed files with 110626 additions and 0 deletions

View file

@ -0,0 +1,152 @@
Page({
data:{
photoPath: '',
livenessSteps: ['请正对镜头', '请张张嘴', '请左右摇头', '请眨眨眼'],
stepIndex: 0,
isCapturing: false,
isUploading: false
},
onLoad(){
// 页面加载时检查并申请摄像头权限
this.checkCameraPermission();
},
async checkCameraPermission(){
try{
const setting = await new Promise((resolve) => {
wx.getSetting({ success: resolve, fail: () => resolve({}) });
});
// 检查是否已授权
if(!setting.authSetting || !setting.authSetting['scope.camera']){
// 未授权,主动申请
const result = await new Promise((resolve) => {
wx.authorize({
scope: 'scope.camera',
success: () => resolve({ granted: true }),
fail: () => resolve({ granted: false })
});
});
if(!result.granted){
// 用户拒绝了授权,需要引导去设置页
wx.showModal({
title: '需要摄像头权限',
content: '为了完成实名认证,需要访问您的摄像头。请在设置中开启摄像头权限。',
confirmText: '去设置',
success: (res) => {
if(res.confirm){
wx.openSetting({
success: (settingRes) => {
if(settingRes.authSetting && settingRes.authSetting['scope.camera']){
wx.showToast({ title: '授权成功', icon: 'success' });
} else {
wx.showToast({ title: '授权失败,无法使用摄像头', icon: 'none' });
}
}
});
}
}
});
}
}
}catch(err){
console.error('检查摄像头权限失败:', err);
}
},
onCameraError(e){
console.error('camera error', e);
let msg = '摄像头打开失败';
if(e.detail && e.detail.errMsg){
if(e.detail.errMsg.includes('permission')){
msg = '摄像头权限被拒绝,请前往设置开启';
} else if(e.detail.errMsg.includes('not available')){
msg = '摄像头不可用,请检查设备';
}
}
wx.showModal({
title: '摄像头错误',
content: msg,
confirmText: '去设置',
success: (res) => {
if(res.confirm){
wx.openSetting();
}
}
});
},
get currentHint(){
const { livenessSteps, stepIndex } = this.data;
return livenessSteps[Math.min(stepIndex, livenessSteps.length - 1)] || '';
},
canCapture(){
const { stepIndex, livenessSteps } = this.data;
return stepIndex >= livenessSteps.length;
},
onNextAction(){
const { stepIndex, livenessSteps } = this.data;
if (stepIndex < livenessSteps.length) {
const nextIndex = stepIndex + 1;
this.setData({ stepIndex: nextIndex }, () => {
// 若所有动作完成,自动拍照
if (nextIndex >= livenessSteps.length) {
setTimeout(() => {
this.onCapture();
}, 600);
}
});
}
},
async onCapture(){
if (!this.canCapture()){
wx.showToast({ title: '请先完成动作提示', icon: 'none' });
return;
}
if(this.data.isCapturing){
return; // 防止重复点击
}
this.setData({ isCapturing: true });
wx.showLoading({ title: '拍照中...', mask: true });
try{
const ctx = wx.createCameraContext();
const res = await new Promise((resolve, reject)=>{
ctx.takePhoto({
quality: 'high',
success: resolve,
fail: reject
});
});
// 临时文件需要及时处理,这里先保存路径
// 注意tempImagePath 仅在当前会话有效,需要尽快上传
this.setData({
photoPath: res.tempImagePath,
isCapturing: false
});
wx.hideLoading();
wx.showToast({ title: '拍照成功', icon: 'success', duration: 1500 });
}catch(err){
this.setData({ isCapturing: false });
wx.hideLoading();
console.error('拍照失败:', err);
let msg = '拍照失败,请重试';
if(err.errMsg){
if(err.errMsg.includes('permission')){
msg = '摄像头权限被拒绝,请前往设置开启';
} else if(err.errMsg.includes('not available')){
msg = '摄像头不可用';
}
}
wx.showToast({ title: msg, icon: 'none', duration: 2000 });
}
},
onRetake(){
this.setData({ photoPath: '' });
},
onSubmit(){
if(!this.data.photoPath){
wx.showToast({ title: '请先拍照', icon: 'none' });
return;
}
// TODO: 上传并提交认证
wx.showToast({ title: '已提交', icon: 'success' });
}
});

View file

@ -0,0 +1,12 @@
{
"navigationBarTitleText": "摄像头认证",
"navigationBarBackgroundColor": "#000000",
"navigationBarTextStyle": "white",
"permission": {
"scope.camera": {
"desc": "需要使用您的摄像头进行实名认证,以核实您的真实身份"
}
}
}

View file

@ -0,0 +1,38 @@
<view class="camera-verify-container">
<view class="header">
<text class="title">摄像头实名认证</text>
<text class="desc">请正对镜头,确保光线充足,按提示完成拍摄</text>
</view>
<!-- 顶部动作说明(始终显示当前动作,强调眨眨眼) -->
<view class="top-hint">
<text class="top-hint-text">动作说明:{{livenessSteps[stepIndex] || '请眨眨眼'}}</text>
</view>
<view class="camera-box">
<camera device-position="front" flash="off" class="camera" mode="normal" frame-size="medium" binderror="onCameraError"></camera>
<view class="liveness-hint">
<text class="hint-text">{{livenessSteps[stepIndex]}}</text>
<view class="hint-actions">
<button class="hint-btn" bindtap="onNextAction">我已完成</button>
</view>
<view class="hint-dots">
<block wx:for="{{livenessSteps}}" wx:key="index">
<view class="dot {{index <= stepIndex-1 ? 'done' : ''}}"></view>
</block>
</view>
</view>
</view>
<view class="actions">
<button class="btn capture {{stepIndex >= livenessSteps.length && !isCapturing ? 'enabled' : 'disabled'}}" bindtap="onCapture" disabled="{{isCapturing}}">{{isCapturing ? '拍照中...' : '拍照'}}</button>
<button class="btn retake" wx:if="{{photoPath}}" bindtap="onRetake" disabled="{{isUploading}}">重拍</button>
<button class="btn submit {{photoPath && !isUploading ? 'enabled' : 'disabled'}}" bindtap="onSubmit" disabled="{{isUploading}}">{{isUploading ? '提交中...' : '提交认证'}}</button>
</view>
<view class="preview" wx:if="{{photoPath}}">
<image class="preview-image" src="{{photoPath}}" mode="aspectFit" />
</view>
</view>

View file

@ -0,0 +1,176 @@
.camera-verify-container {
min-height: 100vh;
background: #000;
color: #fff;
box-sizing: border-box;
padding: 32rpx;
display: flex;
flex-direction: column;
}
.header {
margin-bottom: 24rpx;
width: 100%;
}
.title {
font-size: 36rpx;
font-weight: 600;
color: #fff;
display: block;
}
.desc {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
color: #b0b0b0;
}
.top-hint {
margin: 8rpx 0 20rpx 0;
width: 100%;
}
.top-hint-text {
font-size: 26rpx;
color: #ffd666;
display: block;
}
.camera-box {
width: 560rpx;
height: 560rpx;
border: 4rpx solid #333;
border-radius: 50%;
overflow: hidden;
background: #111;
margin: 0 auto 32rpx auto;
position: relative;
z-index: 1;
}
.camera {
width: 100%;
height: 100%;
display: block;
}
.liveness-hint {
position: absolute;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
padding: 16rpx 20rpx;
box-sizing: border-box;
z-index: 2;
}
.hint-text {
display: block;
font-size: 28rpx;
color: #fff;
margin-bottom: 12rpx;
}
.hint-actions {
display: flex;
justify-content: flex-end;
}
.hint-btn {
height: 64rpx;
line-height: 64rpx;
padding: 0 24rpx;
border-radius: 12rpx;
background: #2eadfb;
color: #fff;
border: none;
font-size: 24rpx;
}
.hint-btn::after {
border: none;
}
.hint-dots {
margin-top: 10rpx;
display: flex;
gap: 8rpx;
justify-content: center;
}
.dot {
width: 10rpx;
height: 10rpx;
border-radius: 50%;
background: #666;
}
.dot.done {
background: #2eadfb;
}
.actions {
display: flex;
gap: 16rpx;
justify-content: space-between;
margin: 20rpx 0;
width: 100%;
z-index: 10;
position: relative;
}
.btn {
flex: 1;
height: 88rpx;
line-height: 88rpx;
text-align: center;
border-radius: 16rpx;
border: none;
color: #fff;
font-size: 28rpx;
}
.btn.capture {
background: #2b8a3e;
}
.btn.capture.enabled {
background: #2b8a3e;
}
.btn.capture.disabled {
background: #555;
opacity: 0.5;
}
.btn.retake {
background: #6c757d;
}
.btn.submit {
background: #494949;
}
.btn.submit.enabled {
background: linear-gradient(124deg, #ff6460 1.58%, #ec42c8 34.28%, #435cff 54%, #00d5ff 84.05%);
}
.btn.submit.disabled {
background: #494949;
opacity: 0.5;
}
.preview {
margin-top: 20rpx;
width: 100%;
}
.preview-image {
width: 100%;
border-radius: 12rpx;
display: block;
}

View file

@ -0,0 +1,42 @@
Page({
data:{ url: '' },
onLoad(options){
if(options && options.url){
try{
const decoded = decodeURIComponent(options.url);
this.setData({ url: decoded });
}catch(e){
this.setData({ url: options.url });
}
}
else{
// 如果没有URL直接跳转到验证成功页面
wx.redirectTo({
url: '/subpackages/realname/verify-success'
});
}
},
// 监听网页传来的消息
onWebViewMessage(e){
console.log('收到网页消息:', e.detail.data);
const data = e.detail.data || {};
// 检查是否是认证成功消息
// 根据腾讯云 FaceID 的实际回调格式调整判断条件
if(data.type === 'verify_success' || data.success === true || data.status === 'success' || data.verifySuccess){
// 跳转到验证成功页面
setTimeout(() => {
wx.redirectTo({
url: '/subpackages/realname/verify-success'
});
}, 500); // 延迟500ms确保网页消息处理完成
}
},
// 页面显示时检查认证状态
onShow(){
// 如果认证已完成,可以直接跳转
// 注意:实际逻辑可能需要根据腾讯云 FaceID 的回调方式调整
}
});

View file

@ -0,0 +1,7 @@
{
"navigationBarTitleText": "实人认证",
"navigationBarBackgroundColor": "#000000",
"navigationBarTextStyle": "white"
}

View file

@ -0,0 +1,5 @@
<view class="faceid-webview-container">
<web-view src="{{url}}" bindmessage="onWebViewMessage"></web-view>
</view>

View file

@ -0,0 +1 @@
/* subpackages/realname/faceid-webview.wxss */

View file

@ -0,0 +1,114 @@
Page({
data: {
realName: '',
idNumber: '',
canProceed: false,
idError: '',
showConfirm: false
},
// 简易中国二代身份证校验:格式+生日+校验位
isValidChineseID(id) {
if (!id || typeof id !== 'string') return false;
const upper = id.trim().toUpperCase();
if (!/^\d{17}[\dX]$/.test(upper)) return false;
// 校验生日是否合法
const y = parseInt(upper.slice(6, 10), 10);
const m = parseInt(upper.slice(10, 12), 10);
const d = parseInt(upper.slice(12, 14), 10);
const date = new Date(y, m - 1, d);
if (!(date.getFullYear() === y && date.getMonth() + 1 === m && date.getDate() === d)) return false;
// 校验位
const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
const parity = ['1','0','X','9','8','7','6','5','4','3','2'];
let sum = 0;
for (let i = 0; i < 17; i++) {
sum += parseInt(upper[i], 10) * weights[i];
}
const code = parity[sum % 11];
return code === upper[17];
},
onRealNameInput(e) {
const realName = e.detail.value || '';
this.setData({ realName }, this.updateProceedState);
},
onIdNumberInput(e) {
// 仅保留数字与X统一为大写
const raw = (e.detail.value || '').toString();
const cleaned = raw.replace(/[^0-9xX]/g, '').toUpperCase();
const idNumber = cleaned;
this.setData({ idNumber }, () => {
// 输入中先不显示错误,但实时控制按钮可用
this.updateProceedState(false);
});
},
updateProceedState(showError = true) {
const { realName, idNumber } = this.data;
const trimmed = (idNumber || '').trim();
const valid = this.isValidChineseID(trimmed);
const ok = realName.trim().length > 0 && valid;
const idError = !trimmed ? '' : (valid ? '' : '身份证格式不正确请输入18位二代身份证');
this.setData({ canProceed: ok, idError: showError ? idError : '' });
},
onIdBlur() {
// 失去焦点时显示错误
this.updateProceedState(true);
},
onNext() {
const { realName, idNumber, canProceed } = this.data;
if (!realName.trim()) {
wx.showToast({ title: '请输入真实姓名', icon: 'none' });
return;
}
if (!this.isValidChineseID((idNumber || '').trim())) {
this.setData({ idError: '身份证格式不正确请输入18位二代身份证' });
wx.showToast({ title: '身份证格式不正确', icon: 'none' });
return;
}
if (!canProceed) {
wx.showToast({ title: '请完善信息', icon: 'none' });
return;
}
// 打开确认弹框
this.setData({ showConfirm: true });
},
onConfirmCancel() {
this.setData({ showConfirm: false });
},
async onConfirmAgree() {
this.setData({ showConfirm: false });
wx.navigateTo({
url: `/subpackages/realname/faceid-webview?url=${encodeURIComponent('')}`
});
return;
//
try {
const apiClient = require('../../../utils/api-client.js');
const response = await apiClient.post('/api/faceid/create', {
realName: this.data.realName,
idNumber: this.data.idNumber,
redirectUrl: encodeURIComponent('/subpackages/realname/faceid-webview')
});
// 期望后端返回 { code:0, data: { certUrl: 'https://...' } }
const certUrl = response?.data?.certUrl || response?.certUrl;
if (certUrl) {
wx.navigateTo({
url: `/subpackages/realname/faceid-webview?url=${encodeURIComponent(certUrl)}`
});
} else {
wx.showToast({
title: response?.message || '创建认证会话失败',
icon: 'none'
});
}
} catch (error) {
console.error('创建FaceID会话失败:', error);
wx.showToast({
title: error?.message || '网络异常,请稍后重试',
icon: 'none'
});
}
}
});

View file

@ -0,0 +1,7 @@
{
"navigationBarTitleText": "实名认证",
"navigationBarBackgroundColor": "#000000",
"navigationBarTextStyle": "white"
}

View file

@ -0,0 +1,44 @@
<view class="realname-container">
<view class="top-image-box">
<image class="check-image" src="/images/self/check.png" mode="widthFix" />
</view>
<view class="form-row">
<text class="label">真实姓名</text>
<input class="input" placeholder="请输入真实姓名" value="{{realName}}" bindinput="onRealNameInput" />
</view>
<view class="form-row">
<text class="label">身份证号</text>
<input class="input {{idError ? 'error' : ''}}" placeholder="请输入身份证号" value="{{idNumber}}" bindinput="onIdNumberInput" bindblur="onIdBlur" />
<text wx:if="{{idError}}" class="error-text">{{idError}}</text>
</view>
<view class="action-row">
<button class="next-btn {{canProceed ? 'enabled' : 'disabled'}}" bindtap="onNext">下一步</button>
</view>
<view class="tips-box">
<text class="tips-title">温馨提示:</text>
<view class="tips-list">
<text class="tips-item">1. 目前仅支持二代身份证认证</text>
<text class="tips-item">2. 请您填写正确的身份信息实名认证,以核实本人身份,保障账户安全</text>
<text class="tips-item">3. 您所提交的身份信息仅仅被用于身份核验,会严格保密</text>
</view>
</view>
<!-- 确认弹框 -->
<view wx:if="{{showConfirm}}" class="confirm-mask" bindtap="onConfirmCancel"></view>
<view wx:if="{{showConfirm}}" class="confirm-modal">
<view class="confirm-title">提示您注意</view>
<view class="confirm-content">
<text>为实现核实您真实身份并完成实名认证的目的,您需要向我们提供您的真实姓名、身份证号码。我们承诺此类信息将不会用于任何其他您未授权的场景,并且会对此类信息采取加密等安全保护技术措施。点击【同意】则表示本人同意我们根据以上的方式和目的收集、使用及储存您提供的本人身份信息用于实名认证。</text>
</view>
<view class="confirm-actions">
<button class="confirm-btn cancel" bindtap="onConfirmCancel">取消</button>
<button class="confirm-btn ok" bindtap="onConfirmAgree">同意</button>
</view>
</view>
</view>

View file

@ -0,0 +1,157 @@
.realname-container {
width: 100vw;
min-height: 100vh;
background: #000000;
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
padding: 40rpx 32rpx 80rpx 32rpx;
}
.top-image-box {
width: 100%;
display: flex;
justify-content: center;
margin-bottom: 60rpx;
}
.check-image {
width: 420rpx;
height: 150rpx;
margin: 100rpx 0;
}
.form-row {
width: 100%;
display: flex;
flex-direction: column;
margin-bottom: 42rpx;
}
.label {
font-size: 28rpx;
color: #bfbfbf;
margin-bottom: 16rpx;
}
.input {
width: 100%;
height: 84rpx;
border-radius: 12rpx;
background: #1a1a1a;
color: #ffffff;
font-size: 28rpx;
padding: 0 28rpx;
box-sizing: border-box;
border: 1rpx solid #333333;
}
.input.error {
border: 1rpx solid #ff4d4f;
}
.error-text{
margin-top: 10rpx;
font-size: 24rpx;
color: #ff4d4f;
}
.action-row {
width: 100%;
margin-top: 40rpx;
}
.next-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
text-align: center;
border-radius: 44rpx;
color: #ffffff;
font-size: 32rpx;
border: none;
}
.next-btn.disabled {
background: #494949;
}
.next-btn.enabled {
background: linear-gradient(124deg, #ff6460 1.58%, #ec42c8 34.28%, #435cff 54%, #00d5ff 84.05%);
}
.next-btn::after {
border: none;
}
.tips-box{
width: 100%;
margin-top: 58rpx;
padding: 24rpx;
}
.tips-title{
display: block;
font-size: 28rpx;
color: #555;
margin-bottom: 12rpx;
}
.tips-list{display:flex;flex-direction:column;gap:10rpx;}
.tips-item{font-size:24rpx;color:#555;line-height:1.6;}
/* 自定义确认弹框 */
.confirm-mask{
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.6);
z-index: 999;
}
.confirm-modal{
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 86%;
max-width: 640rpx;
background: #0e0e0e;
color: #ffffff;
border-radius: 24rpx;
padding: 32rpx 28rpx;
z-index: 1000;
box-sizing: border-box;
border: 1rpx solid #222;
}
.confirm-title{
font-size: 32rpx;
font-weight: 600;
margin-bottom: 16rpx;
}
.confirm-content{
font-size: 26rpx;
line-height: 1.8;
color: #e0e0e0;
}
.confirm-actions{
margin-top: 28rpx;
display: flex;
justify-content: space-between;
gap: 24rpx;
}
.confirm-btn{
width: 200rpx;
height: 80rpx;
line-height: 80rpx;
text-align: center;
border-radius: 16rpx;
color: #ffffff;
font-size: 28rpx;
border: none;
}
.confirm-btn::after{border:none;}
.confirm-btn.cancel{
background: linear-gradient(135deg, #242424 0%, #2eadfb 100%);
}
.confirm-btn.ok{
background: linear-gradient(135deg, #c38eff 0%, #559cff 100%);
}

View file

@ -0,0 +1,8 @@
Page({
data: {},
onLoad(options) {
// 可以接收认证结果参数
console.log('实名认证成功页面', options);
}
});

View file

@ -0,0 +1,6 @@
{
"navigationBarTitleText": "认证成功",
"navigationBarBackgroundColor": "#000000",
"navigationBarTextStyle": "white"
}

View file

@ -0,0 +1,10 @@
<view class="verify-success-container">
<view class="badge-box">
<image class="badge-image" src="/images/self/approve_badge.png" mode="aspectFit" />
</view>
<view class="success-text-box">
<text class="success-text">恭喜你,实名认证成功</text>
</view>
</view>

View file

@ -0,0 +1,37 @@
.verify-success-container {
width: 100vw;
min-height: 100vh;
background: #000000;
display: flex;
flex-direction: column;
align-items: center;
/* justify-content: center; */
padding: 40rpx 32rpx;
box-sizing: border-box;
}
.badge-box {
width: 100%;
display: flex;
justify-content: center;
margin-top: 120rpx;
}
.badge-image {
width: 320rpx;
height: 320rpx;
}
.success-text-box {
width: 100%;
display: flex;
justify-content: center;
padding-top: 160rpx;
}
.success-text {
font-size: 64rpx;
color: #ffffff;
text-align: center;
}