This commit is contained in:
2025-12-25 15:09:11 +08:00
6 changed files with 317 additions and 75 deletions

View File

@@ -101,10 +101,6 @@ export class FaceLoginService {
this._retryTimer = null; this._retryTimer = null;
} }
/**
* 【新增】生命周期 - 开始
* 统一入口,通常在页面加载 (onLoad/onMounted) 时调用
*/
start(scope = null) { start(scope = null) {
// 启动前先清理可能存在的旧状态 // 启动前先清理可能存在的旧状态
this.close(); this.close();
@@ -113,10 +109,6 @@ export class FaceLoginService {
this.init(scope); this.init(scope);
} }
/**
* 【新增】生命周期 - 关闭
* 统一出口,通常在页面卸载 (onUnload/onUnmounted) 时调用
*/
close() { close() {
// 1. 清除正在等待执行的重试定时器 // 1. 清除正在等待执行的重试定时器
if (this._retryTimer) { if (this._retryTimer) {
@@ -131,10 +123,6 @@ export class FaceLoginService {
console.log("[FaceLogin] 服务已关闭"); console.log("[FaceLogin] 服务已关闭");
} }
/**
* 1. 初始化刷脸服务
* (已修改:增加了对定时器的管理)
*/
async init(scope = null, retryCount = 1) { async init(scope = null, retryCount = 1) {
const params = { const params = {
action: "initFace", action: "initFace",
@@ -154,7 +142,6 @@ export class FaceLoginService {
return true; return true;
} catch (err) { } catch (err) {
console.error(`[FaceLogin] 初始化失败: ${err.message}`); console.error(`[FaceLogin] 初始化失败: ${err.message}`);
// 如果还有重试次数,且服务没有被手动 close 关闭
if (retryCount > 0) { if (retryCount > 0) {
console.log("[FaceLogin] 3秒后尝试重新初始化..."); console.log("[FaceLogin] 3秒后尝试重新初始化...");
this._retryTimer = setTimeout(() => { this._retryTimer = setTimeout(() => {
@@ -162,8 +149,7 @@ export class FaceLoginService {
this.init(scope, retryCount - 1); // 递归重试 this.init(scope, retryCount - 1); // 递归重试
}, 3000); }, 3000);
} else { } else {
// 重试耗尽,抛出错误 // throw err; // 视业务逻辑决定是否阻断
// throw err; // 可选:视业务逻辑决定是否阻断
} }
} }
} }
@@ -172,11 +158,9 @@ export class FaceLoginService {
* 2. 唤起 1:N 刷脸并获取 AuthCode * 2. 唤起 1:N 刷脸并获取 AuthCode
*/ */
async startFaceLogin() { async startFaceLogin() {
// 防御性编程:确保已初始化
if (!this.isInitialized) { if (!this.isInitialized) {
console.warn("[FaceLogin] 服务未初始化,尝试自动补救初始化..."); console.warn("[FaceLogin] 服务未初始化,尝试自动补救初始化...");
try { try {
// 尝试一次即时初始化(不重试)
await this.init(null, 0); await this.init(null, 0);
if (!this.isInitialized) throw new Error("初始化未完成"); if (!this.isInitialized) throw new Error("初始化未完成");
} catch (e) { } catch (e) {
@@ -218,9 +202,6 @@ export class FaceLoginService {
} }
} }
/**
* 基础 Bridge 调用封装
*/
_bridgeCall(params) { _bridgeCall(params) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (typeof hh === 'undefined' || !hh.call) { if (typeof hh === 'undefined' || !hh.call) {

View File

@@ -64,7 +64,7 @@
<view v-if="hasLogin" :class="{ 'match-move-top': isMachineEnv }" class="match-card-out"> <view v-if="hasLogin" :class="{ 'match-move-top': isMachineEnv }" class="match-card-out">
<view class="match-card"> <view class="match-card">
<image class="match-card-bg" src="@/static/icon/match-card-bg.png" /> <image class="match-card-bg" src="@/static/icon/match-card-bg.png" />
<view class="title">简历匹配职位</view> <view class="title">为您匹配职位</view>
<view class="match-item-container"> <view class="match-item-container">
<AIMatch :tags="matchTags" :loading="matchLoading" @tag-click="handleTagClick"></AIMatch> <AIMatch :tags="matchTags" :loading="matchLoading" @tag-click="handleTagClick"></AIMatch>
</view> </view>

View File

@@ -1,15 +1,8 @@
<template> <template>
<AppLayout title="就业服务程序"> <AppLayout :title="pageTitle">
<view v-if="isMachineEnv && !hasLogin" class="alipay-login-container"> <view v-if="isMachineEnv && !hasLogin" class="alipay-login-container">
<!-- 切换 --> <!-- 切换 -->
<view class="login-method-switch"> <view class="login-method-switch">
<view
class="method-item"
:class="{ active: loginMethod === 'face' }"
@click="switchLoginMethod('face')"
>
扫脸登录
</view>
<view <view
class="method-item" class="method-item"
:class="{ active: loginMethod === 'qrcode' }" :class="{ active: loginMethod === 'qrcode' }"
@@ -17,6 +10,13 @@
> >
扫码登录 扫码登录
</view> </view>
<view
class="method-item"
:class="{ active: loginMethod === 'face' }"
@click="switchLoginMethod('face')"
>
扫脸登录
</view>
</view> </view>
<view class="login-scan-area"> <view class="login-scan-area">
@@ -64,7 +64,8 @@
</view> </view>
<view class="countdown-container"> <view class="countdown-container">
<view class="countdown-wrapper"> <!-- 刷脸不限时间 -->
<view class="countdown-wrapper" v-if="loginMethod === 'qrcode'">
<text class="countdown-number">{{ countdown }}</text> <text class="countdown-number">{{ countdown }}</text>
<text class="countdown-text">秒后自动返回</text> <text class="countdown-text">秒后自动返回</text>
</view> </view>
@@ -236,7 +237,7 @@ let scanInterval = null;
const countdown = ref(60); const countdown = ref(60);
let countdownTimer = null; let countdownTimer = null;
const loginMethod = ref('qrcode'); // 'qrcode' / 'face' const loginMethod = ref('qrcode'); // 'qrcode' / 'face'
const pageTitle = ref('就享家服务程序');
onLoad((parmas) => { onLoad((parmas) => {
getTreeselect(); getTreeselect();
if (!isMachineEnv.value) { if (!isMachineEnv.value) {
@@ -264,6 +265,7 @@ onMounted(() => {
}, 1000); }, 1000);
} }
}); });
onUnmounted(() => { onUnmounted(() => {
stopScanAnimation(); stopScanAnimation();
stopCountdown(); stopCountdown();
@@ -274,7 +276,7 @@ const startCountdown = () => {
countdown.value = 60; countdown.value = 60;
countdownTimer = setInterval(() => { countdownTimer = setInterval(() => {
countdown.value--; countdown.value--;
if (countdown.value <= 0) { if (countdown.value <= 0 && loginMethod.value === 'qrcode') {
returnToHome(); returnToHome();
} }
}, 1000); }, 1000);
@@ -316,6 +318,7 @@ const switchLoginMethod = (method) => {
faceService.close(); faceService.close();
qrHandler.start(); qrHandler.start();
playTextDirectly('扫码登录'); playTextDirectly('扫码登录');
resetCountdown();
break; break;
case 'face': case 'face':
qrHandler.close(); qrHandler.close();
@@ -323,15 +326,32 @@ const switchLoginMethod = (method) => {
playTextDirectly('扫脸登录'); playTextDirectly('扫脸登录');
break; break;
} }
resetCountdown();
} }
}; };
async function handleFaceLogin() { async function handleFaceLogin() {
try { try {
const authCode = await faceService.startFaceLogin(); const authCode = await faceService.startFaceLogin();
console.log('拿到 AuthCode:', authCode); console.log('authCode获取:', authCode);
// 调用后端登录接口... if (authCode.name) {
pageTitle.value = `就享家服务程序(${authCode.name})`;
}
$api.createRequest('/app/alipay/scanLogin', authCode, 'POST').then((resData) => {
loginSetToken(resData.token).then((resume) => {
if (resume.data.jobTitleId) {
useUserStore().initSeesionId();
uni.reLaunch({
url: '/pages/index/index',
});
} else {
if (resume.data.sex) {
pageTitle.value = `就享家服务程序(${name})`;
fromValue.sex = resume.data.sex === '男' ? 0 : 1;
}
playTextDirectly('登录成功,请完善简历信息');
}
});
});
} catch (err) { } catch (err) {
this.$api.msg(err.message); this.$api.msg(err.message);
} }

View File

@@ -1,6 +1,6 @@
<template> <template>
<view class="container"> <view class="container">
<view v-show="searchFocus" class="search-mask" ></view> <view v-show="searchFocus" class="search-mask"></view>
<view> <view>
<view class="top"> <view class="top">
<image <image
@@ -40,8 +40,12 @@
@confirm="searchBtn" @confirm="searchBtn"
/> />
<!-- 联想搜索下拉列表 --> <!-- 联想搜索下拉列表 -->
<scroll-view scroll-y class="search-suggestions" v-show="showSuggestions && filteredSuggestions.length"> <scroll-view
<view scroll-y
class="search-suggestions"
v-show="showSuggestions && filteredSuggestions.length"
>
<view
class="suggestion-item" class="suggestion-item"
v-for="(item, index) in filteredSuggestions" v-for="(item, index) in filteredSuggestions"
:key="index" :key="index"
@@ -144,13 +148,10 @@ const filteredSuggestions = computed(() => {
const searchText = searchValue.value.toLowerCase(); const searchText = searchValue.value.toLowerCase();
try { try {
return majorDataSource.value.filter(item => return majorDataSource.value.filter((item) => item.toLowerCase().includes(searchText));
item.toLowerCase().includes(searchText)
);
} catch (error) { } catch (error) {
return [] return [];
} }
}); });
const pageState = reactive({ const pageState = reactive({
@@ -207,26 +208,28 @@ const { columnCount, columnSpace } = useColumnCount(() => {
}); });
onLoad((options) => { onLoad((options) => {
if(options.keyWord){ if (options.keyWord) {
searchValue.value = decodeURIComponent(options.keyWord) searchValue.value = decodeURIComponent(options.keyWord);
searchBtn() searchBtn();
} }
let arr = uni.getStorageSync('searchList'); let arr = uni.getStorageSync('searchList');
if (arr) { if (arr) {
historyList.value = uni.getStorageSync('searchList'); historyList.value = uni.getStorageSync('searchList');
} }
getMajorDataSource() getMajorDataSource();
}); });
function getMajorDataSource() { function getMajorDataSource() {
const LoadCache = (resData) => { const LoadCache = (resData) => {
if (resData.code === 200) { if (resData.code === 200) {
majorDataSource.value = resData.data; majorDataSource.value = resData.data;
console.log(majorDataSource.value) console.log(majorDataSource.value);
} }
}; };
$api.createRequestWithCache('/app/common/majorManagement/getMajorList', {}, 'GET', false, LoadCache).then(LoadCache); $api.createRequestWithCache('/app/common/majorManagement/getMajorList', {}, 'GET', false, LoadCache).then(
LoadCache
);
} }
// 设置搜索类型并触发搜索 // 设置搜索类型并触发搜索
@@ -247,7 +250,7 @@ function setSearchType(type) {
pageState.page = 0; pageState.page = 0;
// 触发搜索 // 触发搜索
playTextDirectly('正在为您查找岗位') playTextDirectly('正在为您查找岗位');
// 根据搜索类型设置不同的参数 // 根据搜索类型设置不同的参数
if (searchType.value === 'job') { if (searchType.value === 'job') {
@@ -332,7 +335,7 @@ function searchBtn() {
if (!searchValue.value) { if (!searchValue.value) {
return; return;
} }
playTextDirectly('正在为您查找岗位') playTextDirectly('正在为您查找岗位');
// 保存到历史记录(仅当搜索成功时保存) // 保存到历史记录(仅当搜索成功时保存)
historyList.value.unshift(searchValue.value); historyList.value.unshift(searchValue.value);
@@ -443,6 +446,16 @@ function getJobList(type = 'add') {
listCom.value = [...listCom.value, ...reslist]; listCom.value = [...listCom.value, ...reslist];
} else { } else {
listCom.value = [...rows]; listCom.value = [...rows];
// 一体机语音提示
if (searchType.value === 'major') {
if (rows.length) {
$api.msg('为您找到相关专业的职位');
playTextDirectly(`为您找到相关专业的职位`);
} else {
$api.msg('未找到相关专业的职位,请尝试其他关键词');
playTextDirectly(`未找到相关专业的职位,请尝试其他关键词`);
}
}
} }
pageState.total = resData.total; pageState.total = resData.total;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize); pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);

227
utils/IDCardParser.js Normal file
View File

@@ -0,0 +1,227 @@
// 使用示例:
// import IDCardParser from '@/utils/IDCardParser';
// const handleCheck = (idStr) => {
// const parser = new IDCardParser(idStr);
// const result = parser.getInfo();
// if(result.valid) {
// // 填充表单
// console.log(result.age, result.gender);
// } else {
// alert(result.message);
// }
// }
class IDCardParser {
constructor(idNumber) {
this.idNumber = idNumber ? idNumber.trim().toUpperCase() : "";
this.provinceMap = {
11: "北京市",
12: "天津市",
13: "河北省",
14: "山西省",
15: "内蒙古自治区",
21: "辽宁省",
22: "吉林省",
23: "黑龙江省",
31: "上海市",
32: "江苏省",
33: "浙江省",
34: "安徽省",
35: "福建省",
36: "江西省",
37: "山东省",
41: "河南省",
42: "湖北省",
43: "湖南省",
44: "广东省",
45: "广西壮族自治区",
46: "海南省",
50: "重庆市",
51: "四川省",
52: "贵州省",
53: "云南省",
54: "西藏自治区",
61: "陕西省",
62: "甘肃省",
63: "青海省",
64: "宁夏回族自治区",
65: "新疆维吾尔自治区",
71: "台湾省",
81: "香港特别行政区",
82: "澳门特别行政区"
};
// 初始化验证结果
const validation = this._validate();
this.isValid = validation.isValid;
this.errorMsg = validation.msg;
// 如果合法,预先解析数据
if (this.isValid) {
this._parseData();
}
}
/**
* 核心校验逻辑
*/
_validate() {
// 1. 基础正则 (18位末位数字或X)
if (!/^\d{17}[\d|X]$/.test(this.idNumber)) {
return {
isValid: false,
msg: "格式错误必须是18位末位为数字或X"
};
}
// 2. 省份校验
const provinceCode = parseInt(this.idNumber.substring(0, 2));
if (!this.provinceMap[provinceCode]) {
return {
isValid: false,
msg: "省份代码错误"
};
}
// 3. 日期合法性严格校验
const year = parseInt(this.idNumber.substring(6, 10));
const month = parseInt(this.idNumber.substring(10, 12));
const day = parseInt(this.idNumber.substring(12, 14));
const date = new Date(year, month - 1, day);
// JS Date会自动纠错(例如2月30会变成3月),所以必须反向比对
if (
date.getFullYear() !== year ||
date.getMonth() + 1 !== month ||
date.getDate() !== day
) {
return {
isValid: false,
msg: "出生日期无效"
};
}
// 检查年份范围可选例如限制在1900年以后
if (year < 1900 || date > new Date()) {
return {
isValid: false,
msg: "出生日期不在合理范围内"
};
}
// 4. 校验码计算 (ISO 7064:1983.MOD 11-2)
if (!this._checkSum()) {
return {
isValid: false,
msg: "校验码错误(身份证可能无效)"
};
}
return {
isValid: true,
msg: "验证通过"
};
}
/**
* 校验码算法
*/
_checkSum() {
const factors = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
let sum = 0;
for (let i = 0; i < 17; i++) {
sum += parseInt(this.idNumber[i]) * factors[i];
}
const mod = sum % 11;
return checkCodes[mod] === this.idNumber[17];
}
/**
* 解析内部数据
*/
_parseData() {
const year = parseInt(this.idNumber.substring(6, 10));
const month = parseInt(this.idNumber.substring(10, 12));
const day = parseInt(this.idNumber.substring(12, 14));
this.birthDate = new Date(year, month - 1, day);
this.province = this.provinceMap[parseInt(this.idNumber.substring(0, 2))];
// 性别第17位奇数男偶数女
const genderCode = parseInt(this.idNumber.substring(16, 17));
this.gender = genderCode % 2 !== 0 ? "男" : "女";
}
/**
* 获取所有信息
*/
getInfo() {
if (!this.isValid) {
return {
valid: false,
message: this.errorMsg
};
}
return {
valid: true,
idNumber: this.idNumber,
province: this.province,
birthday: this._formatDate(this.birthDate),
gender: this.gender,
age: this._calculateAge(),
zodiac: this._getZodiac(), // 生肖
constellation: this._getConstellation() // 星座
};
}
_formatDate(date) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}
_calculateAge() {
const today = new Date();
let age = today.getFullYear() - this.birthDate.getFullYear();
const m = today.getMonth() - this.birthDate.getMonth();
// 如果没过生日年龄减1
if (m < 0 || (m === 0 && today.getDate() < this.birthDate.getDate())) {
age--;
}
return age;
}
_getZodiac() {
// 猴鸡狗猪鼠牛虎兔龙蛇马羊 (对应余数 0-11)
const zodiacs = "猴鸡狗猪鼠牛虎兔龙蛇马羊";
return zodiacs.charAt(this.birthDate.getFullYear() % 12);
}
_getConstellation() {
const month = this.birthDate.getMonth() + 1;
const day = this.birthDate.getDate();
// 星座分割日
const dates = [20, 19, 21, 20, 21, 22, 23, 23, 23, 24, 23, 22];
const constellations = [
"摩羯座", "水瓶座", "双鱼座", "白羊座", "金牛座", "双子座",
"巨蟹座", "狮子座", "处女座", "天秤座", "天蝎座", "射手座", "摩羯座"
];
if (day < dates[month - 1]) {
return constellations[month - 1];
} else {
return constellations[month];
}
}
}
export default IDCardParser;

View File

@@ -18,7 +18,8 @@ const needToEncrypt = [
["post", "/app/user/experience/edit"], ["post", "/app/user/experience/edit"],
["post", "/app/user/experience/delete"], ["post", "/app/user/experience/delete"],
["get", "/app/user/experience/getSingle/{value}"], ["get", "/app/user/experience/getSingle/{value}"],
["get", "/app/user/experience/list"] ["get", "/app/user/experience/list"],
["post", "/app/alipay/scanLogin"]
] ]
/** /**