19 Commits

Author SHA1 Message Date
FengHui
55c0b1fd22 11 2026-04-14 18:05:04 +08:00
FengHui
cec340da43 111 2026-04-14 13:37:14 +08:00
FengHui
183d43b664 样式优化 2026-04-14 13:26:23 +08:00
FengHui
3e408d5740 111 2026-04-14 11:39:36 +08:00
FengHui
855deb4643 11 2026-04-14 11:23:41 +08:00
FengHui
5a9d111d4c 删除测试代码 2026-04-13 17:52:29 +08:00
FengHui
9ec42f18f3 删除无用代码 2026-04-13 17:32:05 +08:00
FengHui
321e686d68 bug修复 2026-04-13 12:29:47 +08:00
FengHui
3d8e13c665 登录流程bug修改 2026-04-10 19:46:42 +08:00
FengHui
3fe4dbe47f 11111 2026-04-10 13:18:15 +08:00
FengHui
c742a65aa0 删除调试代码 2026-04-10 12:27:10 +08:00
FengHui
7c409e8528 登录验证提示 2026-04-10 12:17:53 +08:00
FengHui
4d8403609f Merge branch 'main' of http://124.243.245.42:3000/sdz/ks-app-employment-service 2026-04-10 11:25:36 +08:00
FengHui
769bc23edb 页面效果优化 2026-04-10 11:25:34 +08:00
1f8b24c0bf 添加生成安全随机标识符功能以改进文件上传逻辑 2026-04-10 11:15:25 +08:00
FengHui
8bfc1dc683 登录流程改版 2026-04-10 01:01:03 +08:00
FengHui
9b2cdecc16 文件上传功开发 2026-04-09 21:14:25 +08:00
FengHui
b1942e90e0 登陆失败,弹出后端返回的msg 2026-04-07 17:51:22 +08:00
FengHui
15668dc769 文件上传区域展开bug修复 2026-04-03 19:22:20 +08:00
26 changed files with 2618 additions and 323 deletions

View File

@@ -83,3 +83,15 @@ export function getInfo() {
method: 'get'
})
}
// 重新发送验证码
export function sendSmsAgain(data) {
return request({
method: 'post',
url: '/app/sendSmsAgain',
data,
headers: {
isToken: false
}
})
}

View File

@@ -417,6 +417,136 @@ html {
background-color: #ffffff !important;
}
/* #ifdef H5 */
/* H5端字体和边距调整 - 放大1.5倍 */
html, body {
font-size: 150% !important;
}
/* 调整页面基础字体大小 */
page {
font-size: 42rpx !important;
}
/* 调整特定类的字体大小 */
.fs_10 {
font-size: 30rpx !important;
}
.fs_12 {
font-size: 36rpx !important;
}
.fs_14 {
font-size: 42rpx !important;
}
.fs_16 {
font-size: 48rpx !important;
}
.fs_18 {
font-size: 54rpx !important;
}
.fs_20 {
font-size: 60rpx !important;
}
.fs_22 {
font-size: 66rpx !important;
}
.fs_24 {
font-size: 72rpx !important;
}
.fs_26 {
font-size: 78rpx !important;
}
.fs_28 {
font-size: 84rpx !important;
}
.fs_30 {
font-size: 90rpx !important;
}
.fs_32 {
font-size: 96rpx !important;
}
/* 调整边距类 */
.mar_le30 {
margin-left: 90rpx !important;
}
.mar_le25 {
margin-left: 75rpx !important;
}
.mar_le20 {
margin-left: 60rpx !important;
}
.mar_le15 {
margin-left: 45rpx !important;
}
.mar_le10 {
margin-left: 30rpx !important;
}
.mar_le5 {
margin-left: 15rpx !important;
}
.mar_ri5 {
margin-right: 15rpx !important;
}
.mar_ri10 {
margin-right: 30rpx !important;
}
.mar_ri15 {
margin-right: 45rpx !important;
}
.mar_ri20 {
margin-right: 60rpx !important;
}
.mar_ri25 {
margin-right: 75rpx !important;
}
.mar_top0 {
margin-top: 0 !important;
}
.mar_top5 {
margin-top: 15rpx !important;
}
.mar_top10 {
margin-top: 30rpx !important;
}
.mar_top15 {
margin-top: 45rpx !important;
}
.mar_top20 {
margin-top: 60rpx !important;
}
.mar_top25 {
margin-top: 75rpx !important;
}
/* #endif */
/* 弹性布局 */

View File

@@ -584,6 +584,86 @@ function isInWechatMiniProgramWebview() {
function isEmptyObject(obj) {
return obj && typeof obj === 'object' && !Array.isArray(obj) && Object.keys(obj).length === 0;
}
/**
* 农历日期工具
* 提供公历转农历日期的中文表示
*/
export const LunarUtil = {
/**
* 获取农历日期的中文表示
* @param {number} year - 公历年
* @param {number} month - 公历月 (1-12)
* @param {number} day - 公历日
* @returns {string} 农历日期的中文表示(初一、初二...三十)
*/
getLunarDayInChinese(year, month, day) {
// 这里使用简化的农历转换算法
// 实际项目中可以根据需要扩展更完整的农历功能
const lunarDays = this.solarToLunar(year, month, day);
return this.getDayInChinese(lunarDays.day);
},
/**
* 简化的公历转农历日期算法
* 注意:这是一个简化版,实际农历计算更复杂
* 仅用于生成初一到三十的日期表示
*/
solarToLunar(year, month, day) {
// 这里使用简化算法,实际项目中可以使用更精确的算法
// 对于本项目,我们只需要生成初一到三十的序列
// 实际的农历计算需要考虑节气、闰月等因素
const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
// 处理闰年
if ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0) {
daysInMonth[1] = 29;
}
// 计算当年已过天数
let dayOfYear = 0;
for (let i = 0; i < month - 1; i++) {
dayOfYear += daysInMonth[i];
}
dayOfYear += day;
// 简化的农历月份计算(实际农历月份计算更复杂)
const lunarMonth = Math.ceil(dayOfYear / 29.5);
const lunarDay = (dayOfYear % 29) || 29;
return {
month: lunarMonth,
day: lunarDay
};
},
/**
* 将农历日期转换为中文表示
* @param {number} day - 农历日
* @returns {string} 中文表示(初一、初二...三十)
*/
getDayInChinese(day) {
if (day === 1) {
return '初一';
} else if (day === 15) {
return '十五';
} else if (day === 30) {
return '三十';
} else {
const digits = ['', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
if (day <= 10) {
return '初' + digits[day];
} else if (day <= 19) {
return '十' + (day === 10 ? '' : digits[day - 10]);
} else if (day <= 29) {
return '廿' + (day === 20 ? '' : digits[day - 20]);
} else {
return '三十';
}
}
}
};
/**
* 身份证号码校验工具
* 支持15位和18位身份证号码校验
@@ -941,6 +1021,7 @@ export default {
cloneDeep,
formatDate,
IdCardValidator,
LunarUtil,
getdeviceInfo,
checkingPhoneRegExp,
checkingEmailRegExp,

View File

@@ -8,7 +8,7 @@
</template>
<script setup>
import { ref, watch, nextTick } from 'vue';
import { ref, watch, nextTick, getCurrentInstance } from 'vue';
const props = defineProps({
show: Boolean,
@@ -26,11 +26,18 @@ const wrapStyle = ref({
});
const contentRef = ref(null);
const instance = getCurrentInstance();
// 获取高度(兼容 H5 + 小程序)
function getContentHeight() {
return new Promise((resolve) => {
const query = uni.createSelectorQuery().in(this ? this : undefined);
let query;
// #ifdef MP-WEIXIN
query = uni.createSelectorQuery().in(instance);
// #endif
// #ifndef MP-WEIXIN
query = uni.createSelectorQuery();
// #endif
query
.select('.content-inner')
.boundingClientRect((data) => {

View File

@@ -277,7 +277,8 @@ const getPhoneNumber = (e) => {
$api.msg('获取用户信息失败');
});
} else {
$api.msg('登录失败,请重试');
// $api.msg('登录失败,请重试');
$api.msg(resData.msg || '登录失败,请重试');
}
}).catch((err) => {
uni.hideLoading();

View File

@@ -37,15 +37,8 @@ export function useColumnCount(onChange = () => {}) {
count = 2
// #endif
// #ifndef H5
if (width >= 1000) {
count = 5
} else if (width >= 750) {
count = 4
} else if (width >= 500) {
count = 3
} else {
count = 2
}
// 小程序端固定显示1列
count = 1
// #endif
if (count !== columnCount.value) {

File diff suppressed because one or more lines are too long

View File

@@ -3,8 +3,7 @@
<!-- 自定义tabbar -->
<CustomTabBar :currentPage="2" />
<!-- 微信授权登录弹窗 -->
<WxAuthLogin ref="wxAuthLoginRef" @success="handleLoginSuccess"></WxAuthLogin>
<!-- 抽屉遮罩层 -->
<view v-if="isDrawerOpen" class="overlay" @click="toggleDrawer"></view>
@@ -89,7 +88,7 @@ import useUserStore from '@/stores/useUserStore';
import { tabbarManager } from '@/utils/tabbarManager';
import aiPaging from './components/ai-paging.vue';
import { storeToRefs } from 'pinia';
import WxAuthLogin from '@/components/WxAuthLogin/WxAuthLogin.vue';
const { isTyping, tabeList, chatSessionID } = storeToRefs(useChatGroupDBStore());
const { userInfo } = storeToRefs(useUserStore());
const isDrawerOpen = ref(false);
@@ -97,7 +96,7 @@ const scrollIntoView = ref(false);
const searchText = ref('');
const paging = ref(null);
const wxAuthLoginRef = ref(null);
// 实时过滤
const filteredList = computed(() => {
@@ -123,7 +122,7 @@ onShow(() => {
onMounted(() => {
// 监听退出登录事件,显示微信登录弹窗
uni.$on('showLoginModal', () => {
wxAuthLoginRef.value?.open();
uni.navigateTo({ url: '/pages/login/wx-login' });
});
});

View File

@@ -101,8 +101,19 @@
v-for="(file, vInex) in msg.files"
:key="vInex"
@click="jumpUrl(file)"
:class="{ 'msg-image-file': isImage(file.type, file.name) }"
>
<image class="msg-file-icon" src="/static/icon/Vector2.png"></image>
<image
v-if="isImage(file.type, file.name)"
class="msg-image-thumbnail"
:src="file.url"
mode="aspectFill"
></image>
<image
v-else
class="msg-file-icon"
src="/static/icon/Vector2.png"
></image>
<text class="msg-file-text">{{ file.name || '附件' }}</text>
</view>
</view>
@@ -218,7 +229,7 @@
@confirm="sendMessage"
:disabled="isTyping"
:adjust-position="true"
placeholder="请输入您的职位名称、薪资要求、岗位地址"
placeholder="请输入您想找的岗位信息或就政策信息【比如:设计师、10000-12000、广州】【比如:今年喀什地区高校毕业生有什么就业政策】"
v-show="!isVoice"
/>
<view
@@ -255,8 +266,8 @@
</view>
</view>
<view class="ai-tips">本服务为AI生成内容结果仅供参考</view>
<!-- btn -->
<CollapseTransition :show="showfile">
<!-- 文件上传区域 - 使用绝对定位向上展开避免被tabbar遮挡 -->
<view class="file-upload-popup" :class="{ show: showfile }">
<view class="area-tips">
<uni-icons type="info-filled" color="#ADADAD" size="15"></uni-icons>
上传后自动解析简历内容
@@ -275,7 +286,7 @@
<text>文件上传</text>
</view>
</view>
</CollapseTransition>
</view>
<!-- filelist -->
<view class="area-uploadfiles" v-if="filesList.length">
@@ -283,27 +294,48 @@
<view class="uploadfiles-list">
<view
class="file-uploadsend"
:class="{ 'file-border': isImage(file.type) }"
:class="{ 'file-border': isImage(file.type, file.name) }"
v-for="(file, index) in filesList"
:key="index"
>
<view class="file-thumbnail" @click="jumpUrl(file)">
<image
class="file-iconImg"
@click="jumpUrl(file)"
v-if="isImage(file.type)"
:src="file.url"
v-if="isImage(file.type, file.name) && !file.error"
:src="file.tempPath || file.url"
mode="aspectFill"
@load="onImageLoad(file)"
@error="onImageError(file)"
></image>
<view class="file-doc" @click="jumpUrl(file)" v-else>
<view class="file-doc" v-else-if="!isImage(file.type, file.name)">
<FileIcon class="doc-icon" :type="file.type"></FileIcon>
<view class="doc-con">
</view>
<view class="image-loading" v-if="isImage(file.type, file.name) && file.loading">
<uni-icons type="spinner-cycle" size="30" color="#999"></uni-icons>
<text class="loading-text">加载中...</text>
</view>
<view class="image-error" v-if="isImage(file.type, file.name) && file.error" @click="retryImageLoad(file)">
<uni-icons type="info" size="30" color="#ff4444"></uni-icons>
<text class="error-text">加载失败</text>
<text class="retry-text">点击重试</text>
</view>
<!-- 图片数量标签 -->
<view class="image-count-badge" v-if="isImage(file.type, file.name) && filesList.filter(f => isImage(f.type, f.name)).length > 1">
{{ filesList.filter(f => isImage(f.type, f.name)).indexOf(file) + 1 }}/{{ filesList.filter(f => isImage(f.type, f.name)).length }}
</view>
</view>
<view class="file-info">
<view class="filename-text">{{ file.name }}</view>
<view class="filerow">
<view class="filerow" v-if="!isImage(file.type, file.name)">
<FileText :type="file.type"></FileText>
<view class="row-x"></view>
<view class="filename-size">{{ file.size }}</view>
</view>
<view class="filerow" v-else>
<text class="file-type-tag">图片</text>
<view class="row-x"></view>
<view class="filename-size">{{ file.size }}</view>
</view>
<!-- <image class="file-icon" @click="jumpUrl(file)" :src=""></image> -->
</view>
<view class="file-del" catchtouchmove="true" @click="delfile(file)">
<uni-icons type="closeempty" color="#FFFFFF" size="10"></uni-icons>
@@ -335,7 +367,6 @@ import { storeToRefs } from 'pinia';
// 移除重复导入使用从globalFunction注入的config
import useChatGroupDBStore from '@/stores/userChatGroupStore';
import MdRender from '@/components/md-render/md-render.vue';
import CollapseTransition from '@/components/CollapseTransition/CollapseTransition.vue';
import PopupFeeBack from './popupbadFeeback.vue';
import AudioWave from './AudioWave.vue';
import WaveDisplay from './WaveDisplay.vue';
@@ -653,8 +684,93 @@ function getGuess() {
});
}
function isImage(type) {
return new RegExp('image').test(type);
// 从文件路径提取文件名
function getFileNameFromPath(filePath) {
if (!filePath) return '';
// 处理反斜杠和正斜杠
const path = filePath.replace(/\\/g, '/');
const lastSlashIndex = path.lastIndexOf('/');
if (lastSlashIndex !== -1) {
return path.substring(lastSlashIndex + 1);
}
return path;
}
// 从文件名推断文件类型
function getFileTypeFromName(fileName) {
if (!fileName) return '';
const lowerName = fileName.toLowerCase();
if (lowerName.endsWith('.jpg') || lowerName.endsWith('.jpeg')) return 'image/jpeg';
if (lowerName.endsWith('.png')) return 'image/png';
if (lowerName.endsWith('.gif')) return 'image/gif';
if (lowerName.endsWith('.bmp')) return 'image/bmp';
if (lowerName.endsWith('.webp')) return 'image/webp';
if (lowerName.endsWith('.svg')) return 'image/svg+xml';
if (lowerName.endsWith('.pdf')) return 'application/pdf';
if (lowerName.endsWith('.doc')) return 'application/msword';
if (lowerName.endsWith('.docx')) return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
if (lowerName.endsWith('.ppt')) return 'application/vnd.ms-powerpoint';
if (lowerName.endsWith('.pptx')) return 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
if (lowerName.endsWith('.xls')) return 'application/vnd.ms-excel';
if (lowerName.endsWith('.xlsx')) return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
if (lowerName.endsWith('.txt')) return 'text/plain';
if (lowerName.endsWith('.md')) return 'text/markdown';
if (lowerName.endsWith('.html') || lowerName.endsWith('.htm')) return 'text/html';
if (lowerName.endsWith('.csv')) return 'text/csv';
return '';
}
function isImage(type, name) {
if (!type && !name) return false;
const debug = false;
if (debug) console.log('isImage检查:', { type, name });
if (type) {
const lowerType = type.toLowerCase();
if (lowerType.includes('image/')) {
if (debug) console.log('通过MIME类型识别为图片:', lowerType);
return true;
}
if (lowerType.includes('image')) {
if (debug) console.log('通过"image"关键词识别为图片:', lowerType);
return true;
}
const imageExtensionsInType = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'heic', 'heif', 'tiff', 'tif'];
for (const ext of imageExtensionsInType) {
if (lowerType.includes(ext)) {
if (debug) console.log('通过类型中的扩展名识别为图片:', ext);
return true;
}
}
}
if (name) {
const lowerName = name.toLowerCase();
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.heic', '.heif', '.tiff', '.tif', '.jfif', '.pjpeg', '.pjp'];
for (const ext of imageExtensions) {
if (lowerName.endsWith(ext)) {
if (debug) console.log('通过文件扩展名识别为图片:', ext);
return true;
}
}
const imageExtensionsWithoutDot = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'heic', 'heif', 'tiff', 'tif', 'jfif', 'pjpeg', 'pjp'];
for (const ext of imageExtensionsWithoutDot) {
if (lowerName.endsWith('.' + ext)) {
if (debug) console.log('通过带点的扩展名识别为图片:', ext);
return true;
}
}
if (debug) console.log('未识别为图片:', lowerName);
}
if (debug) console.log('不是图片文件');
return false;
}
function isFile(type) {
@@ -666,71 +782,504 @@ function isFile(type) {
}
function jumpUrl(file) {
if (file.url) {
window.open(file.url);
} else {
if (!file.url && !file.tempPath) {
$api.msg('文件地址丢失');
return;
}
if (isImage(file.type, file.name)) {
const imageUrl = file.tempPath || file.url;
uni.previewImage({ urls: [imageUrl], current: 0 });
} else {
window.open(file.url);
}
}
function VerifyNumberFiles(num) {
if (filesList.value.length >= config.allowedFileNumber) {
$api.msg(`最大上传文件数量 ${config.allowedFileNumber}`);
function VerifyNumberFiles(additionalCount = 1) {
const currentCount = filesList.value.length;
const maxCount = config.allowedFileNumber || 9;
if (currentCount + additionalCount > maxCount) {
$api.msg(`最多只能上传${maxCount}个文件,当前已有${currentCount}`);
return true;
} else {
return false;
}
}
function onImageLoad(file) {
const index = filesList.value.findIndex(f => f.url === file.url);
if (index !== -1) {
filesList.value[index].loading = false;
filesList.value[index].error = false;
}
}
function onImageError(file) {
const index = filesList.value.findIndex(f => f.url === file.url);
if (index !== -1) {
filesList.value[index].loading = false;
filesList.value[index].error = true;
}
}
function retryImageLoad(file) {
const index = filesList.value.findIndex(f => f.url === file.url);
if (index !== -1) {
filesList.value[index].loading = true;
filesList.value[index].error = false;
// 触发重新加载
setTimeout(() => {
if (filesList.value[index]) {
filesList.value[index].loading = false;
}
}, 100);
}
}
function uploadCamera(type = 'camera') {
if (VerifyNumberFiles()) return;
uni.chooseImage({
count: 1, //默认9
sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
sourceType: [type], //从相册选择
success: function (res) {
const tempFilePaths = res.tempFilePaths;
const file = res.tempFiles[0];
// 继续上传
$api.uploadFile(tempFilePaths[0], true).then((resData) => {
resData = JSON.parse(resData);
if (isImage(file.type)) {
filesList.value.push({
url: resData.msg,
type: file.type,
name: file.name,
// 在微信小程序中检查权限
// #ifdef MP-WEIXIN
const scope = type === 'camera' ? 'scope.camera' : 'scope.writePhotosAlbum';
uni.getSetting({
success: (res) => {
if (!res.authSetting[scope]) {
// 未授权,发起授权请求
uni.authorize({
scope: scope,
success: () => {
// 授权成功,执行选择图片
chooseImageAfterAuth();
},
fail: (err) => {
console.error('授权失败:', err);
if (err.errMsg && err.errMsg.includes('auth deny')) {
uni.showModal({
title: '权限提示',
content: `需要${type === 'camera' ? '相机' : '相册'}权限才能上传图片,请在设置中开启权限`,
showCancel: false,
confirmText: '确定'
});
textInput.value = state.uploadFileTips;
} else {
$api.msg('授权失败,请重试');
}
}
});
} else {
// 已授权,直接执行选择图片
chooseImageAfterAuth();
}
},
fail: (err) => {
console.error('获取设置失败:', err);
chooseImageAfterAuth(); // 失败时也尝试选择图片
}
});
// #endif
// #ifndef MP-WEIXIN
// 非微信小程序环境直接选择图片
chooseImageAfterAuth();
// #endif
function chooseImageAfterAuth() {
uni.chooseImage({
count: 9, //支持多选最多9张
sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
// sourceType: [type], // 移除 sourceType 参数,解决微信 "unknown scene" 错误
success: function (res) {
const tempFilePaths = res.tempFilePaths;
const files = res.tempFiles;
// 检查文件数量限制
if (VerifyNumberFiles(files.length)) return;
// 显示上传提示
$api.msg(`开始上传${files.length}张图片...`);
// 上传所有图片
const uploadPromises = tempFilePaths.map((filePath, index) => {
return $api.uploadFile(filePath, true).then((resData) => {
resData = JSON.parse(resData);
if (resData.code === 200 && resData.filePath) {
// 从文件对象获取文件信息
const fileInfo = files[index] || {};
const fileName = getFileNameFromPath(fileInfo.name || filePath);
const fileType = fileInfo.type || getFileTypeFromName(fileName);
const fileSize = fileInfo.size || 0;
// 使用统一的文件路径处理函数
const processedFile = processFilePath(
resData.filePath, // 原始服务器路径
fileName,
fileType,
fileSize,
tempFilePaths[index] // 临时路径用于预览
);
// 添加图片加载状态字段
processedFile.loading = false;
processedFile.error = false;
return processedFile;
} else {
throw new Error(resData.msg || '上传失败');
}
});
});
Promise.all(uploadPromises).then((uploadedFiles) => {
// 将所有上传成功的文件添加到filesList并设置图片加载状态
uploadedFiles.forEach(file => {
if (isImage(file.type, file.name)) {
file.loading = true; // 图片开始加载
}
filesList.value.push(file);
});
textInput.value = state.uploadFileTips;
showfile.value = false; // 关闭上传面板
$api.msg(`成功上传${uploadedFiles.length}张图片`);
}).catch((error) => {
$api.msg(error.message || '图片上传失败');
});
},
fail: function (err) {
console.error('选择图片失败:', err);
// 专门处理微信 "unknown scene" 错误
if (err.errMsg && err.errMsg.includes('unknown scene')) {
$api.msg('选择图片失败,请尝试重新选择或检查权限设置');
} else if (err.errMsg && err.errMsg.includes('auth deny')) {
$api.msg('没有相机或相册权限,请在设置中开启');
} else {
$api.msg('选择图片失败: ' + (err.errMsg || '未知错误'));
}
}
});
}
}
function getUploadFile(type = 'camera') {
if (VerifyNumberFiles()) return;
uni.chooseFile({
count: 1,
// 检测是否是微信小程序环境
const isWeixinMiniProgram = typeof wx !== 'undefined' && wx.chooseMessageFile;
console.log('当前环境检测:', {
isWeixinMiniProgram,
wxExists: typeof wx !== 'undefined',
hasChooseMessageFile: typeof wx !== 'undefined' && wx.chooseMessageFile
});
if (isWeixinMiniProgram) {
console.log('微信小程序环境使用uni.chooseMessageFile');
// 微信小程序环境使用uni.chooseMessageFile
uni.chooseMessageFile({
count: 9,
type: 'file',
success: (res) => {
const tempFilePaths = res.tempFilePaths;
const file = res.tempFiles[0];
const allowedTypes = config.allowedFileTypes || [];
const size = $api.formatFileSize(file.size);
if (!allowedTypes.includes(file.type)) {
return $api.msg('仅支持 txt md html word pdf ppt csv excel 格式类型');
}
// 继续上传
$api.uploadFile(tempFilePaths[0], true).then((resData) => {
resData = JSON.parse(resData);
filesList.value.push({
url: resData.msg,
type: file.type,
console.log('uni.chooseMessageFile返回的res:', res);
// uni.chooseMessageFile返回的数据结构不同
// tempFiles数组中每个对象有path、name、size等属性
const tempFiles = res.tempFiles || [];
const tempFilePaths = tempFiles.map(file => file.path);
const files = tempFiles;
console.log('tempFilePaths:', tempFilePaths);
console.log('tempFiles:', tempFiles);
if (files && files.length > 0) {
console.log('第一个文件详情:', files[0]);
console.log('第一个文件的所有属性:', Object.keys(files[0]));
// 详细记录每个文件的所有属性
files.forEach((file, index) => {
console.log(`文件 ${index} 完整对象:`, JSON.stringify(file));
console.log(`文件 ${index} 关键属性:`, {
name: file.name,
size: size,
type: file.type,
path: file.path,
size: file.size,
hasType: 'type' in file,
typeIsString: typeof file.type === 'string',
typeLength: file.type ? file.type.length : 0
});
});
}
// 调用公共的文件处理函数
handleSelectedFiles(tempFilePaths, files);
},
fail: (err) => {
console.error('uni.chooseMessageFile失败:', err);
// 微信小程序特定的错误处理
if (err.errMsg && err.errMsg.includes('cancel')) {
// 用户取消选择,不提示错误
console.log('用户取消选择文件');
} else {
$api.msg('选择文件失败,请重试');
}
}
});
} else {
console.log('非微信小程序环境使用uni.chooseFile');
// 其他平台使用uni.chooseFile
uni.chooseFile({
count: 9,
success: (res) => {
console.log('uni.chooseFile返回的res:', res);
const tempFilePaths = res.tempFilePaths;
const files = res.tempFiles;
console.log('tempFilePaths:', tempFilePaths);
console.log('tempFiles:', files);
if (files && files.length > 0) {
console.log('第一个文件详情:', files[0]);
console.log('第一个文件的所有属性:', Object.keys(files[0]));
// 详细记录每个文件的所有属性
files.forEach((file, index) => {
console.log(`文件 ${index} 完整对象:`, JSON.stringify(file));
console.log(`文件 ${index} 关键属性:`, {
name: file.name,
type: file.type,
path: file.path,
size: file.size,
hasType: 'type' in file,
typeIsString: typeof file.type === 'string',
typeLength: file.type ? file.type.length : 0
});
});
}
// 调用公共的文件处理函数
handleSelectedFiles(tempFilePaths, files);
},
fail: (err) => {
console.error('uni.chooseFile失败:', err);
if (err.errMsg && !err.errMsg.includes('cancel')) {
$api.msg('选择文件失败,请重试');
}
}
});
}
}
// 处理服务器返回的文件路径生成正确的显示URL和服务器路径
function processFilePath(originalFilePath, fileName, fileType, fileSize, tempPath) {
let displayUrl = originalFilePath; // 用于缩略图显示的URL外网域名
const serverPath = originalFilePath; // 原始服务器路径(内网地址,用于对话接口)
console.log('processFilePath原始fileUrl:', originalFilePath);
// 如果filePath包含IP地址替换为配置的域名用于显示
if (displayUrl && displayUrl.includes('://')) {
// 检查是否是本地临时路径如http://tmp/),如果是则保持原样
if (displayUrl.startsWith('http://tmp/')) {
// 本地临时路径,保持原样
console.log('本地临时路径,保持原样:', displayUrl);
} else {
// 方法1直接替换域名部分保持路径结构- 使用兼容的方法
// 例如http://10.98.80.146/file/xxx.jpg -> https://www.xjksly.cn/file/xxx.jpg
const originalUrl = displayUrl;
// 提取协议、域名和路径部分
let protocol = '';
let domain = '';
let path = '';
// 分离协议和剩余部分
const protocolIndex = originalUrl.indexOf('://');
if (protocolIndex !== -1) {
protocol = originalUrl.substring(0, protocolIndex + 3);
const rest = originalUrl.substring(protocolIndex + 3);
const slashIndex = rest.indexOf('/');
if (slashIndex !== -1) {
domain = rest.substring(0, slashIndex);
path = rest.substring(slashIndex);
} else {
domain = rest;
path = '/';
}
}
console.log('解析结果:', { protocol, domain, path });
// 使用config.baseUrl或config.imgBaseUrl作为新域名
let baseUrl = config.baseUrl || config.imgBaseUrl;
// 如果baseUrl以/api/ks结尾去掉这个部分
if (baseUrl.endsWith('/api/ks')) {
baseUrl = baseUrl.substring(0, baseUrl.length - 7);
}
// 构建新URL使用新域名 + 原路径
// 确保baseUrl不以/结尾path以/开头
let cleanBaseUrl = baseUrl;
if (cleanBaseUrl.endsWith('/')) {
cleanBaseUrl = cleanBaseUrl.substring(0, cleanBaseUrl.length - 1);
}
if (!path.startsWith('/')) {
path = '/' + path;
}
displayUrl = cleanBaseUrl + path;
console.log('替换域名后的displayUrl用于显示:', displayUrl);
}
} else if (displayUrl && !displayUrl.startsWith('http')) {
// 如果filePath不是完整的URL直接添加图片基础URL前缀
displayUrl = config.imgBaseUrl + displayUrl;
console.log('非完整URL添加imgBaseUrl前缀:', displayUrl);
}
return {
url: displayUrl, // 使用处理后的完整URL用于缩略图显示
serverPath: serverPath, // 原始服务器路径(内网地址,用于对话接口)
type: fileType,
name: fileName,
size: fileSize > 0 ? $api.formatFileSize(fileSize) : '',
tempPath: tempPath // 保存临时路径用于预览
};
}
// 处理选择的文件(公共逻辑)
function handleSelectedFiles(tempFilePaths, files) {
// 检查文件数量限制
if (VerifyNumberFiles(files.length)) return;
const allowedTypes = config.allowedFileTypes || [];
// 调试:在文件类型检查之前记录详细信息
console.log('=== handleSelectedFiles 文件类型检查开始 ===');
console.log('allowedTypes配置:', allowedTypes);
console.log('files数组长度:', files.length);
files.forEach((file, index) => {
console.log(`文件 ${index} 检查前详情:`, {
name: file.name,
type: file.type,
hasType: 'type' in file,
typeValue: file.type,
typeIsString: typeof file.type === 'string',
typeLength: file.type ? file.type.length : 0,
size: file.size,
path: file.path
});
// 尝试从文件名推断类型
const inferredType = getFileTypeFromName(file.name);
console.log(`文件 ${index} 从文件名推断的类型:`, inferredType);
// 检查是否在allowedTypes中
const isAllowedByType = allowedTypes.includes(file.type);
const isAllowedByInferred = allowedTypes.includes(inferredType);
const isImageFile = isImage(file.type, file.name);
console.log(`文件 ${index} 检查结果:`, {
isAllowedByType,
isAllowedByInferred,
isImageFile,
finalAllowed: isAllowedByType || isAllowedByInferred || isImageFile
});
});
console.log('=== handleSelectedFiles 文件类型检查结束 ===');
// 检查所有文件类型 - 增强版优先使用file.type回退到文件名推断
const invalidFiles = files.filter(file => {
// 如果文件类型存在且在允许的类型列表中,则通过
if (file.type && allowedTypes.includes(file.type)) {
return false;
}
// 尝试从文件名推断类型
const inferredType = getFileTypeFromName(file.name);
if (inferredType && allowedTypes.includes(inferredType)) {
return false;
}
// 如果是图片,也通过
if (isImage(file.type, file.name)) {
return false;
}
// 否则,文件无效
return true;
});
if (invalidFiles.length > 0) {
// 调试:查看不支持的文件类型
console.log('不支持的文件类型:', invalidFiles.map(f => ({name: f.name, type: f.type})));
// 更详细地显示为什么这些文件不被支持
invalidFiles.forEach((file, index) => {
const inferredType = getFileTypeFromName(file.name);
console.log(`无效文件 ${index} 分析:`, {
name: file.name,
type: file.type,
inferredType,
isImage: isImage(file.type, file.name),
isInAllowedTypes: allowedTypes.includes(file.type),
isInferredInAllowedTypes: allowedTypes.includes(inferredType)
});
});
return $api.msg('仅支持 txt md html word doc docx pdf ppt pptx csv excel xlsx 和图片格式');
}
// 显示上传提示
const imageCount = files.filter(file => isImage(file.type, file.name)).length;
const otherFileCount = files.length - imageCount;
let tip = '开始上传';
if (imageCount > 0) tip += `${imageCount}张图片`;
if (otherFileCount > 0) {
if (imageCount > 0) tip += '和';
tip += `${otherFileCount}个文件`;
}
$api.msg(tip + '...');
// 上传所有文件
const uploadPromises = tempFilePaths.map((filePath, index) => {
return $api.uploadFile(filePath, true).then((resData) => {
resData = JSON.parse(resData);
console.log('服务器返回的resData:', resData);
if (resData.code === 200 && resData.filePath) {
// 从文件对象或文件路径获取文件信息
const fileInfo = files[index] || {};
const fileName = fileInfo.name || getFileNameFromPath(tempFilePaths[index]);
const fileType = fileInfo.type || getFileTypeFromName(fileName);
const fileSize = fileInfo.size || 0;
// 使用统一的文件路径处理函数
const processedFile = processFilePath(
resData.filePath, // 原始服务器路径
fileName,
fileType,
fileSize,
tempFilePaths[index] // 临时路径用于预览
);
// 调试:记录文件信息
console.log('上传文件信息:', {
fileInfo: fileInfo,
type: fileType,
name: fileName,
size: fileSize,
isImage: isImage(fileType, fileName),
originalFilePath: resData.filePath,
displayUrl: processedFile.url,
serverPath: processedFile.serverPath,
imgBaseUrl: config.imgBaseUrl
});
return processedFile;
} else {
throw new Error(resData.msg || '上传失败');
}
});
});
Promise.all(uploadPromises).then((uploadedFiles) => {
// 将所有上传成功的文件添加到filesList
uploadedFiles.forEach(file => {
if (isImage(file.type, file.name)) {
file.loading = true; // 图片开始加载
file.error = false;
}
filesList.value.push(file);
});
textInput.value = state.uploadFileTips;
});
},
showfile.value = false; // 关闭上传面板
$api.msg(`成功上传${uploadedFiles.length}个文件`);
}).catch((error) => {
$api.msg(error.message || '文件上传失败');
});
}
@@ -1096,6 +1645,25 @@ defineExpose({ scrollToBottom, closeGuess, closeFile, changeQueries, handleTouch
font-size: 24rpx
.msg-files:active
background: #e9e9e9
// 消息中的图片文件样式
.msg-image-file
height: 120rpx
width: 120rpx
padding: 4rpx
flex-direction: column
justify-content: center
.msg-image-thumbnail
width: 100%
height: 80rpx
border-radius: 8rpx
margin-bottom: 8rpx
.msg-file-text
font-size: 20rpx
text-align: center
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
.guess
padding: 5rpx 0 10rpx 0
.guess-list
@@ -1374,6 +1942,29 @@ image-margin-top = 40rpx
color: #434343;
margin-top: 18rpx
}
/* 文件上传弹窗 - 向上展开避免被tabbar遮挡 */
.file-upload-popup {
position: absolute;
bottom: 100%;
left: 0;
right: 0;
background: #FFFFFF;
padding: 0 28rpx 24rpx 28rpx;
box-shadow: 0rpx -4rpx 10rpx 0rpx rgba(11,44,112,0.06);
transform: translateY(20rpx);
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
z-index: 1002;
}
.file-upload-popup.show {
transform: translateY(0);
opacity: 1;
visibility: visible;
}
.area-file
display: grid
width: 100%
@@ -1400,12 +1991,13 @@ image-margin-top = 40rpx
.area-uploadfiles
position: absolute
top: -180rpx
bottom: 100%
width: calc(100% - 30rpx)
background: #FFFFFF
left: 0
padding: 10rpx 0 10rpx 30rpx
box-shadow: 0rpx -4rpx 10rpx 0rpx rgba(11,44,112,0.06);
z-index: 1003
.uploadfiles-scroll
height: 100%
.uploadfiles-list
@@ -1431,11 +2023,17 @@ image-margin-top = 40rpx
height: 100%
font-size: 24rpx
position: relative
min-width: 460rpx;
height: 160rpx;
border-radius: 12rpx 12rpx 12rpx 12rpx;
min-width: 200rpx;
max-width: 200rpx;
height: 200rpx;
border-radius: 12rpx;
border: 2rpx solid #E2E2E2;
overflow: hidden
display: flex
flex-direction: column
&.file-border
border: 2rpx solid #4A90E2;
background: #F5F9FF;
.file-del
position: absolute
right: 25rpx
@@ -1467,9 +2065,61 @@ image-margin-top = 40rpx
white-space: nowrap
color: #7B7B7B;
max-width: 100%
.file-thumbnail
flex: 1
display: flex
align-items: center
justify-content: center
overflow: hidden
border-radius: 10rpx 10rpx 0 0
background: #F8F9FA
.file-iconImg
height: 100%
width: 100%
object-fit: cover
.file-info
padding: 12rpx 16rpx
background: white
border-top: 1rpx solid #F0F0F0
.file-type-tag
font-size: 20rpx
color: #4A90E2
background: #F0F7FF
padding: 4rpx 8rpx
border-radius: 4rpx
.image-loading, .image-error
position: absolute
top: 0
left: 0
right: 0
bottom: 0
display: flex
flex-direction: column
align-items: center
justify-content: center
background: rgba(255, 255, 255, 0.9)
.loading-text, .error-text
font-size: 24rpx
margin-top: 10rpx
.loading-text
color: #999
.error-text
color: #ff4444
.retry-text
font-size: 22rpx
color: #4A90E2
margin-top: 5rpx
text-decoration: underline
.image-count-badge
position: absolute
top: 10rpx
right: 10rpx
background: rgba(0, 0, 0, 0.6)
color: white
font-size: 20rpx
padding: 4rpx 8rpx
border-radius: 12rpx
z-index: 2
.filerow
display: flex
align-items: center

View File

@@ -46,12 +46,10 @@
<script setup>
import { reactive, inject, watch, ref, onMounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
const { $api, navTo, navBack } = inject('globalFunction');
const { $api, navTo, navBack, LunarUtil } = inject('globalFunction');
const weekMap = ['日', '一', '二', '三', '四', '五', '六'];
const calendarData = ref([]);
const current = ref({});
import lunarModule from '@/packageA/lib/lunar-javascript@1.7.2.js';
const { Solar, Lunar } = lunarModule;
const isRecord = ref(false);
const recordNum = ref(4);
@@ -162,12 +160,11 @@ function getMonthCalendarData({ year, month, selectableDates = [] }) {
const d = prevLastDate - i;
const prevMonth = month - 1 <= 0 ? 12 : month - 1;
const prevYear = month - 1 <= 0 ? year - 1 : year;
const solar = Solar.fromYmd(prevYear, prevMonth, d);
const lunar = Lunar.fromSolar(solar);
const nl = LunarUtil.getLunarDayInChinese(prevYear, prevMonth, d);
const dateStr = `${prevYear}-${String(prevMonth).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
list.push({
day: d,
nl: lunar.getDayInChinese(),
nl: nl,
isThisMonth: false,
isToday: dateStr === todayStr,
isSelectable: isSelected(prevYear, prevMonth, d),
@@ -179,12 +176,11 @@ function getMonthCalendarData({ year, month, selectableDates = [] }) {
// 当前月天数
for (let d = 1; d <= lastDate; d++) {
const solar = Solar.fromYmd(year, month, d);
const lunar = Lunar.fromSolar(solar);
const nl = LunarUtil.getLunarDayInChinese(year, month, d);
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
list.push({
day: d,
nl: lunar.getDayInChinese(),
nl: nl,
isThisMonth: true,
isToday: dateStr === todayStr,
isSelectable: isSelected(year, month, d),
@@ -199,12 +195,11 @@ function getMonthCalendarData({ year, month, selectableDates = [] }) {
for (let d = 1; d <= remaining; d++) {
const nextMonth = month + 1 > 12 ? 1 : month + 1;
const nextYear = month + 1 > 12 ? year + 1 : year;
const solar = Solar.fromYmd(nextYear, nextMonth, d);
const lunar = Lunar.fromSolar(solar);
const nl = LunarUtil.getLunarDayInChinese(nextYear, nextMonth, d);
const dateStr = `${nextYear}-${String(nextMonth).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
list.push({
day: d,
nl: lunar.getDayInChinese(),
nl: nl,
isThisMonth: false,
isToday: dateStr === todayStr,
isSelectable: isSelected(nextYear, nextMonth, d),

View File

@@ -566,15 +566,42 @@ export default {
this.serviceForm.practicalSolutionTime = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
},
// 使用密码学安全随机源生成标识符
generateSecureId(length = 16) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const charsLength = chars.length;
const randomBytes = new Uint8Array(length);
// H5/现代浏览器
if (typeof globalThis !== 'undefined' && globalThis.crypto && typeof globalThis.crypto.getRandomValues === 'function') {
globalThis.crypto.getRandomValues(randomBytes);
}
// 微信小程序环境
else if (typeof wx !== 'undefined' && typeof wx.getRandomValues === 'function') {
wx.getRandomValues(randomBytes);
}
// 兜底:若运行环境不支持 CSPRNG显式报错避免继续使用可预测随机数
else {
throw new Error('当前环境不支持密码学安全随机数,请在服务端生成标识符');
}
let id = '';
for (let i = 0; i < length; i++) {
id += chars[randomBytes[i] % charsLength];
}
return id;
},
// 触发文件上传
triggerFileUpload() {
if (this.serviceForm.fileUrl.length < 6) {
// 模拟添加文件
const mockFiles = ['文档.pdf', '图片.jpg', '表格.xlsx', '报告.docx'];
const randomFile = mockFiles[Math.floor(Math.random() * mockFiles.length)];
const secureId = this.generateSecureId(20);
const randomFile = mockFiles[Date.now() % mockFiles.length];
this.serviceForm.fileUrl.push({
name: randomFile,
url: `mock-url/${Date.now()}/${randomFile}`
url: `mock-url/${secureId}/${randomFile}`
});
} else {
uni.showToast({ title: '最多只能上传6个文件', icon: 'none' });

View File

@@ -400,11 +400,7 @@
<!-- 筛选 -->
<select-filter ref="selectFilterModel"></select-filter>
<!-- 微信授权登录弹窗 -->
<WxAuthLogin
ref="wxAuthLoginRef"
@success="handleLoginSuccess"
></WxAuthLogin>
<!-- <view class="maskFristEntry" v-if="maskFristEntry">
<view class="entry-content">
@@ -531,7 +527,7 @@ import {
} from "@/stores/useRecommedIndexedDBStore.js";
import { useScrollDirection } from "@/hook/useScrollDirection";
import { useColumnCount } from "@/hook/useColumnCount";
import WxAuthLogin from "@/components/WxAuthLogin/WxAuthLogin.vue";
import IconfontIcon from "@/components/IconfontIcon/IconfontIcon.vue";
// 企业卡片组件已内联到模板中
// 滚动状态管理
@@ -584,7 +580,7 @@ const loadmoreRef = ref(null);
const conditionSearch = ref({});
const waterfallcolumn = ref(2);
const maskFristEntry = ref(false);
const wxAuthLoginRef = ref(null);
const state = reactive({
tabIndex: "all",
});
@@ -684,7 +680,7 @@ onMounted(() => {
// 监听退出登录事件,显示微信登录弹窗
uni.$on("showLoginModal", () => {
wxAuthLoginRef.value?.open();
uni.navigateTo({ url: '/pages/login/wx-login' });
});
});
@@ -712,8 +708,8 @@ watch(
const checkLogin = () => {
const tokenValue = uni.getStorageSync("token") || "";
if (!tokenValue || !hasLogin.value) {
// 未登录,打开授权弹窗
wxAuthLoginRef.value?.open();
// 未登录,跳转到登录页面
uni.navigateTo({ url: '/pages/login/wx-login' });
return false;
}
return true;

View File

@@ -3,7 +3,10 @@
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "喀什智慧就业平台"
"navigationBarTitleText": "喀什智慧就业平台",
// #ifdef H5
"navigationStyle": "custom"
// #endif
}
},
{
@@ -76,6 +79,18 @@
"navigationStyle": "custom"
}
},
{
"path": "pages/login/wx-login",
"style": {
"navigationBarTitleText": "登录"
}
},
{
"path": "pages/login/sms-verify",
"style": {
"navigationBarTitleText": "短信验证"
}
},
{
"path": "pages/resume-guide/resume-guide",
"style": {

View File

@@ -161,7 +161,7 @@
import {
tabbarManager
} from "@/utils/tabbarManager";
import WxAuthLogin from "@/components/WxAuthLogin/WxAuthLogin.vue";
import config from "@/config.js";
import CryptoJS from 'crypto-js'
const {

View File

@@ -307,7 +307,8 @@
<view class="item btn-feel" v-if="!job.recommend">
<view class="falls-card" :class="{ 'disabled-card': Number(job.jobStatus) === 1 }" @click="nextDetail(job)">
<view class="falls-card-pay">
<view class="pay-text">
<view class="fl_1 falls-card-title">{{ job.jobTitle }}</view>
<view class="fr_1 pay-text">
<Salary-Expectation
:max-salary="job.maxSalary"
:min-salary="job.minSalary"
@@ -316,8 +317,10 @@
</view>
<image v-if="job.isHot" class="flame" src="/static/icon/flame.png"></image>
</view>
<view class="falls-card-title">{{ job.jobTitle }}</view>
<view class="fl_box fl_warp">
<view>
<!-- <view class="fl_1 falls-card-title">{{ job.jobTitle }}</view> -->
<view class="fr_1 fl_box fl_warp">
<view class="falls-card-education mar_ri10" v-if="job.education">
<dict-Label dictType="education" :value="job.education"></dict-Label>
</view>
@@ -325,22 +328,24 @@
<dict-Label dictType="experience" :value="job.experience"></dict-Label>
</view>
</view>
<view class="falls-card-company" v-show="isShowJw !== 3">
{{ config.appInfo.areaName }}
<!-- {{ job.jobLocation }} -->
<dict-Label dictType="jobLocationAreaCode" :value="job.jobLocationAreaCode"></dict-Label>
</view>
<!-- <view class="falls-card-company" v-show="isShowJw !== 3">
{{ config.appInfo.areaName }}
{{ job.jobLocation }}
<dict-Label dictType="jobLocationAreaCode" :value="job.jobLocationAreaCode"></dict-Label>
</view> -->
<view class="falls-card-pepleNumber">
<view>
<image class="point2" src="/static/icon/pintDate.png"></image>
<view class="fl_1">
{{ job.postingDate || '发布日期' }}
发布日期{{ job.postingDate || '暂无数据' }}
</view>
</view>
<view>
<image class="point3" src="/static/icon/pointpeople.png"></image>
<view class="fl_1">
{{ vacanciesTo(job.vacancies) }}
招聘人数{{ vacanciesTo(job.vacancies) }}
</view>
</view>
</view>
@@ -349,6 +354,10 @@
<view class="fl_1">
{{ job.companyName }}
</view>
<view class="fr-1">
地区{{ config.appInfo.areaName }}
</view>
</view>
<!-- 招聘者显示上下架开关 -->
<view class="falls-card-actions" v-if="isRecruiter">
@@ -395,7 +404,8 @@
<view class="item btn-feel" v-if="!job.recommend">
<view class="falls-card" :class="{ 'disabled-card': Number(job.jobStatus) === 1 }" @click="nextDetail(job)">
<view class="falls-card-pay">
<view class="pay-text">
<view class="fl_1 falls-card-title">{{ job.jobTitle }}</view>
<view class="fr_1 pay-text">
<Salary-Expectation
:max-salary="job.maxSalary"
:min-salary="job.minSalary"
@@ -404,7 +414,7 @@
</view>
<image v-if="job.isHot" class="flame" src="/static/icon/flame.png"></image>
</view>
<view class="falls-card-title">{{ job.jobTitle }}</view>
<view class="fl_box fl_warp">
<view class="falls-card-education mar_ri10" v-if="job.education">
<dict-Label dictType="education" :value="job.education"></dict-Label>
@@ -413,16 +423,16 @@
<dict-Label dictType="experience" :value="job.experience"></dict-Label>
</view>
</view>
<view class="falls-card-company" v-show="isShowJw !== 3">
{{ config.appInfo.areaName }}
<!-- {{ job.jobLocation }} -->
<!-- <view class="falls-card-company" v-show="isShowJw !== 3">
地区{{ config.appInfo.areaName }}
地址{{ job.jobLocation }}
<dict-Label dictType="jobLocationAreaCode" :value="job.jobLocationAreaCode"></dict-Label>
</view>
</view> -->
<view class="falls-card-pepleNumber">
<view>
<image class="point2" src="/static/icon/pintDate.png"></image>
<view class="fl_1">
{{ job.postingDate || '发布日期' }}
发布日期{{ job.postingDate || '暂无数据' }}
</view>
</view>
<view>
@@ -437,6 +447,10 @@
<view class="fl_1">
{{ job.companyName }}
</view>
<view class="fr-1">
地区{{ config.appInfo.areaName }}
</view>
</view>
<!-- 招聘者显示上下架开关 -->
<view class="falls-card-actions" v-if="isRecruiter">
@@ -486,8 +500,7 @@
@update:show="(value) => showNewFilter = value"
/>
<!-- 微信授权登录弹窗 -->
<WxAuthLogin ref="wxAuthLoginRef" @success="handleLoginSuccess" />
<!-- <view class="maskFristEntry" v-if="maskFristEntry">
<view class="entry-content">
@@ -612,7 +625,7 @@ import newFilterPage from '@/components/new-filter-page/new-filter-page.vue';
import { useRecommedIndexedDBStore, jobRecommender } from '@/stores/useRecommedIndexedDBStore.js';
import { useScrollDirection } from '@/hook/useScrollDirection';
import { useColumnCount } from '@/hook/useColumnCount';
import WxAuthLogin from '@/components/WxAuthLogin/WxAuthLogin.vue';
import IconfontIcon from '@/components/IconfontIcon/IconfontIcon.vue'
// 企业卡片组件已内联到模板中
// 滚动状态管理
@@ -703,7 +716,7 @@ const loadmoreRef = ref(null);
const conditionSearch = ref({});
const waterfallcolumn = ref(2);
const maskFristEntry = ref(false);
const wxAuthLoginRef = ref(null);
const state = reactive({
tabIndex: 'all',
});
@@ -818,7 +831,7 @@ onMounted(() => {
getCompanyInfo();
// pageNull.value = 0;
uni.$on('showLoginModal', () => {
wxAuthLoginRef.value?.open();
uni.navigateTo({ url: '/pages/login/wx-login' });
pageNull.value = 0;
});
});
@@ -832,7 +845,7 @@ onMounted(() => {
// 绑定新的监听
uni.$on('showLoginModal', () => {
console.log('收到showLoginModal事件打开登录弹窗');
wxAuthLoginRef.value?.open();
uni.navigateTo({ url: '/pages/login/wx-login' });
pageNull.value = 0;
});

View File

@@ -1,6 +1,16 @@
<template>
<view class="app-custom-root">
<view class="app-container">
<!-- #ifdef H5 -->
<!-- 自定义导航栏 -->
<view class="custom-nav" :style="{paddingTop: statusBarHeight + 'px'}">
<view class="nav-content">
<view class="nav-back" @click="back"><text class="nav-back-text"></text></view>
<view class="nav-title">喀什智慧就业平台</view>
<view class="nav-placeholder"></view>
</view>
</view>
<!-- #endif -->
<!-- 主体内容区域 -->
<view class="container-main">
<IndexOne @onShowTabbar="changeShowTabbar" />
@@ -26,7 +36,13 @@ const userStore = useUserStore();
onLoad((options) => {
// useReadMsg().fetchMessages();
});
// 返回按钮功能
function back() {
// uni.navigateBack({
// delta: 1
// });
window.location.href = 'https://www.xjksly.cn/mechine-single-vue/';
}
onShow(() => {
// 更新自定义tabbar选中状态
tabbarManager.updateSelected(0);
@@ -191,4 +207,39 @@ onShow(() => {
background: #FFFFFF
width: 4rpx
height: 20rpx
/* 自定义导航栏样式 */
.custom-nav
background: linear-gradient(135deg, #8a9bf0, #c3cafa, #e7ebfe)
width: 100%
box-shadow: 0 2rpx 10rpx rgba(138, 155, 240, 0.2)
border-radius: 0
.nav-content
height: 80rpx
display: flex
align-items: center
justify-content: space-between
padding: 0 40rpx
.nav-back
width: 120rpx
height: 80rpx
display: flex
align-items: center
justify-content: flex-start
color: #2f56e8
font-weight: 400
transition: all 0.3s ease
&:hover
transform: translateX(-5rpx)
.nav-back-text
font-size: 56rpx
line-height: 1
text-shadow: 1rpx 1rpx 2rpx rgba(0, 0, 0, 0.1)
.nav-title
color: #4a55b0
font-size: 36rpx
font-weight: 600
text-shadow: 1rpx 1rpx 2rpx rgba(255, 255, 255, 0.8)
letter-spacing: 2rpx
.nav-placeholder
width: 120rpx
</style>

634
pages/login/sms-verify.vue Normal file
View File

@@ -0,0 +1,634 @@
<template>
<view class="sms-verify-page">
<!-- 顶部导航栏 -->
<!-- <view class="nav-bar">
<view class="nav-back" @click="goBack">
<uni-icons type="arrowleft" size="24" color="#333"></uni-icons>
</view>
<view class="nav-title">短信验证</view>
<view class="nav-placeholder"></view>
</view> -->
<!-- 页面内容 -->
<view class="page-content">
<!-- 安全提示 -->
<view class="security-tip">
<view class="tip-icon">
<uni-icons type="auth-filled" size="36" color="#256BFA"></uni-icons>
</view>
<view class="tip-text">为了您的账户安全需要您进行验证</view>
</view>
<!-- 手机号显示 -->
<view class="phone-display">
<view class="phone-label">验证码将发送至</view>
<view class="phone-number">{{ formattedPhone }}</view>
</view>
<!-- 验证码输入区域 -->
<view class="sms-input-area">
<view class="input-label">请输入6位数字验证码</view>
<view class="single-input-container">
<input
ref="smsInput"
class="single-input"
type="tel"
inputmode="numeric"
pattern="[0-9]*"
autocomplete="one-time-code"
maxlength="6"
:value="smsCode"
@input="onCodeInput"
@paste="onPaste"
placeholder="请输入验证码"
:focus="autoFocus"
/>
</view>
</view>
<!-- 倒计时和重发 -->
<view class="countdown-section">
<view v-if="countdown > 0" class="countdown-text">
{{ countdown }}秒后重新获取
</view>
<view v-else class="resend-text" @click="resendSms">
重新获取验证码
</view>
</view>
<!-- 提交按钮 -->
<button class="submit-btn" :disabled="!canSubmit" @click="submitVerification">
<text v-if="!loading">确认</text>
<view v-else class="loading-spinner"></view>
</button>
</view>
</view>
</template>
<script setup>
import { ref, inject, computed, onMounted, onUnmounted } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import useUserStore from '@/stores/useUserStore';
import { tabbarManager } from '@/utils/tabbarManager';
const { $api } = inject('globalFunction');
const userStore = useUserStore();
// 从页面参数获取数据
const phone = ref('');
const openid = ref('');
const unionid = ref('');
const userType = ref('');
const orgType = ref('');
// 验证码相关
const smsCode = ref(''); // 单个字符串验证码
const smsInput = ref(null); // 输入框引用
const countdown = ref(60); // 倒计时
const timer = ref(null);
const loading = ref(false);
const autoFocus = ref(true); // 自动聚焦
// 格式化手机号:只显示前三位和后四位,中间用星号代替
const formattedPhone = computed(() => {
if (!phone.value || phone.value.trim() === '') {
return '未知号码';
}
// 清理手机号,移除空格和特殊字符
const cleanPhone = phone.value.replace(/\D/g, '');
if (cleanPhone.length < 11) {
return phone.value;
}
const prefix = cleanPhone.substring(0, 3);
const suffix = cleanPhone.substring(cleanPhone.length - 4);
return `${prefix}****${suffix}`;
});
// 是否可以提交
const canSubmit = computed(() => {
return smsCode.value.length === 6 && !loading.value;
});
// 页面加载时获取参数
onLoad(async (options) => {
// 尝试多种可能的手机号字段名(按优先级)
const possiblePhoneFields = [
'phone', 'mobile', 'phoneNumber', 'tel',
'phoneNum', 'userPhone', 'telephone', 'mobilePhone', 'cellphone',
'userTel', 'userMobile', 'contactPhone'
];
let foundPhone = '';
for (const field of possiblePhoneFields) {
if (options[field]) {
foundPhone = options[field];
break;
}
}
// 如果没找到尝试从URL参数中解析
if (!foundPhone) {
const currentPages = getCurrentPages();
if (currentPages.length > 0) {
const currentPage = currentPages[currentPages.length - 1];
// 尝试从页面路由参数中获取
if (currentPage.$page && currentPage.$page.fullPath) {
const urlParams = new URLSearchParams(currentPage.$page.fullPath.split('?')[1] || '');
for (const field of possiblePhoneFields) {
const value = urlParams.get(field);
if (value) {
foundPhone = value;
break;
}
}
}
}
}
phone.value = foundPhone || '';
openid.value = options.openid || '';
unionid.value = options.unionid || '';
userType.value = options.userType || '';
orgType.value = options.orgType || '';
// 如果手机号仍然为空,显示错误信息
if (!phone.value) {
uni.showToast({ title: '无法获取手机号,请返回重试', icon: 'none' });
}
// 开始倒计时
startCountdown();
// 自动聚焦到输入框
setTimeout(() => {
if (smsInput.value && typeof smsInput.value.focus === 'function') {
smsInput.value.focus();
}
}, 300);
});
// 开始倒计时
const startCountdown = () => {
clearInterval(timer.value);
countdown.value = 60;
timer.value = setInterval(() => {
if (countdown.value > 0) {
countdown.value--;
} else {
clearInterval(timer.value);
}
}, 1000);
};
// 重发验证码
const resendSms = async () => {
if (countdown.value > 0) return;
// 检查手机号是否为空
if (!phone.value) {
uni.showToast({ title: '手机号获取失败,请返回重试', icon: 'none' });
return;
}
uni.showLoading({ title: '发送中...' });
try {
// 调用重新发送验证码接口
const requestParams = { phone: phone.value };
// 只有单位用户才传递机构类型
if (userType.value === '0') {
requestParams.orgType = orgType.value;
requestParams.userType = userType.value;
}
const res = await $api.createRequest('/app/sendSmsAgain', requestParams, 'post');
// 检查状态码
if (res.code === 200 ) {
uni.hideLoading();
uni.showToast({ title: '验证码已发送', icon: 'success' });
startCountdown();
} else {
uni.hideLoading();
uni.showToast({ title: res.msg || '发送失败', icon: 'none' });
}
} catch (error) {
uni.hideLoading();
uni.showToast({ title: error.msg || '发送失败,请重试', icon: 'none' });
}
};
// 验证码输入处理
const onCodeInput = (e) => {
let value = e.detail.value.replace(/\D/g, ''); // 只保留数字
// 限制为6位数字
if (value.length > 6) {
value = value.substring(0, 6);
}
smsCode.value = value;
};
// 粘贴事件处理
const onPaste = (e) => {
// #ifdef MP-WEIXIN
// 微信小程序中,使用异步方式获取剪贴板
setTimeout(() => {
pasteFromClipboard();
}, 50);
// 尝试阻止默认行为
if (e && e.preventDefault) {
e.preventDefault();
}
if (e && e.stopPropagation) {
e.stopPropagation();
}
// #endif
// #ifndef MP-WEIXIN
// 其他平台H5/App使用标准粘贴处理
e.preventDefault();
e.stopPropagation();
let pasteText = '';
if (e.clipboardData && e.clipboardData.getData) {
pasteText = e.clipboardData.getData('text');
}
if (pasteText) {
// 处理粘贴的文本
handlePaste(pasteText);
} else {
// 使用异步方式获取剪贴板
setTimeout(() => {
pasteFromClipboard();
}, 100);
}
return false;
// #endif
};
// 处理粘贴的文本
const handlePaste = (text) => {
// 提取数字
const digits = text.replace(/\D/g, '');
if (digits.length === 0) {
uni.showToast({ title: '剪贴板中没有找到验证码', icon: 'none' });
return;
}
// 限制为6位数字
const code = digits.substring(0, 6);
smsCode.value = code;
uni.showToast({ title: '已粘贴验证码', icon: 'success' });
};
// 从剪贴板粘贴验证码
const pasteFromClipboard = () => {
uni.getClipboardData({
success: (res) => {
const text = res.data || '';
handlePaste(text);
},
fail: () => {
uni.showToast({ title: '读取剪贴板失败', icon: 'none' });
}
});
};
// 提交验证
const submitVerification = async () => {
if (!canSubmit.value) {
return;
}
loading.value = true;
const code = smsCode.value;
// 检查手机号是否为空
if (!phone.value) {
uni.showToast({ title: '手机号获取失败,请返回重试', icon: 'none' });
loading.value = false;
return;
}
try {
// 调用接口 /app/appLoginPhone
const requestParams = {
smsCode: code,
phone: phone.value,
openid: openid.value,
unionid: unionid.value,
userType: userType.value
};
// 只有单位用户才传递机构类型
if (userType.value === '0') {
requestParams.orgType = orgType.value;
}
const res = await $api.createRequest('/app/appLoginPhone', requestParams, 'post');
if (res.token && res.code === 200) {
// 登录成功存储token
await userStore.loginSetToken(res.token).then((resume) => {
// 更新用户类型到缓存
if (res.isCompanyUser !== undefined) {
const userInfo = uni.getStorageSync('userInfo') || {};
userInfo.isCompanyUser = Number(res.isCompanyUser); // 0-企业用户1-求职者
uni.setStorageSync('userInfo', userInfo);
}
uni.showToast({ title: '登录成功', icon: 'success' });
// 刷新tabbar以显示正确的用户类型
tabbarManager.refreshTabBar();
console.log(userType.value , res.isCompanyUser);
console.log('用户登录成功,简历信息-resume:', resume);
console.log('用户登录成功,简历信息-res:', res);
if (!resume?.data?.jobTitleId) {
if (!res.idCard) {
console.log('用户登录成功,没有身份证号');
if (userType.value == '1') {
// 求职者跳转到个人信息补全页面
uni.reLaunch({
url: '/packageA/pages/complete-info/complete-info?step=1'
});
} else if (userType.value == '0') {
// 招聘者跳转到企业信息补全页面
uni.reLaunch({
url: '/packageA/pages/complete-info/company-info'
});
}
} else {
// 跳转到首页
uni.reLaunch({
url: '/pages/index/index'
});
}
} else {
console.log('用户登录成功,有简历信息--');
// 跳转到首页
uni.reLaunch({
url: '/pages/index/index'
});
}
}).catch((error) => {
// 只有在非企业用户且确实需要简历信息时才显示错误
// 企业用户可能没有简历信息,这是正常的
if (userType.value !== '0') {
uni.showToast({ title: '获取用户信息失败', icon: 'none' });
} else {
console.log('企业用户登录成功,简历信息可能为空');
}
});
} else {
uni.showToast({ title: res.msg || '验证失败', icon: 'none' });
}
} catch (error) {
uni.showToast({ title: error || '验证失败,请重试', icon: 'none' });
} finally {
loading.value = false;
}
};
// 返回上一页
const goBack = () => {
uni.navigateBack();
};
// 组件销毁时清除定时器
onUnmounted(() => {
clearInterval(timer.value);
});
</script>
<style lang="stylus" scoped>
.sms-verify-page
min-height: 100vh
background: linear-gradient(135deg, #F7F9FF 0%, #FFFFFF 100%)
.nav-bar
display: flex
align-items: center
justify-content: space-between
height: 88rpx
padding: 0 32rpx
background: #FFFFFF
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04)
.nav-back
width: 60rpx
height: 60rpx
display: flex
align-items: center
justify-content: center
border-radius: 50%
transition: background-color 0.2s ease
&:active
background-color: #F5F5F5
.nav-title
font-size: 32rpx
font-weight: 600
color: #333333
.nav-placeholder
width: 60rpx
.page-content
padding: 80rpx 48rpx 48rpx
.security-tip
display: flex
flex-direction: column
align-items: center
justify-content: center
margin-bottom: 60rpx
padding: 32rpx
background: linear-gradient(135deg, rgba(37, 107, 250, 0.08) 0%, rgba(30, 91, 255, 0.04) 100%)
border-radius: 24rpx
border: 1rpx solid rgba(37, 107, 250, 0.1)
.tip-icon
margin-bottom: 20rpx
width: 64rpx
height: 64rpx
display: flex
align-items: center
justify-content: center
background: #FFFFFF
border-radius: 50%
box-shadow: 0 4rpx 16rpx rgba(37, 107, 250, 0.15)
.tip-text
font-size: 28rpx
font-weight: 500
color: #256BFA
text-align: center
line-height: 1.4
.phone-display
text-align: center
margin-bottom: 80rpx
.phone-label
font-size: 28rpx
color: #666666
margin-bottom: 16rpx
opacity: 0.8
.phone-number
font-size: 44rpx
font-weight: 700
color: #333333
letter-spacing: 2rpx
background: linear-gradient(135deg, #256BFA 0%, #1E5BFF 100%)
-webkit-background-clip: text
-webkit-text-fill-color: transparent
background-clip: text
.sms-input-area
margin-bottom: 60rpx
.input-label
font-size: 28rpx
color: #666666
text-align: center
margin-bottom: 24rpx
font-weight: 500
.single-input-container
margin-bottom: 16rpx
.single-input
width: 100%
height: 120rpx
border: 3rpx solid #E8ECF4
border-radius: 20rpx
font-size: 48rpx
font-weight: 700
color: #333333
background: #FFFFFF
text-align: center
padding: 0 32rpx
box-sizing: border-box
outline: none
caret-color: #256BFA
transition: all 0.3s ease
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05)
&:focus
border-color: #256BFA
background: #FFFFFF
box-shadow: 0 8rpx 32rpx rgba(37, 107, 250, 0.2)
transform: translateY(-2rpx)
&::placeholder
color: #CCCCCC
font-size: 32rpx
font-weight: normal
.countdown-section
text-align: center
margin-bottom: 80rpx
.countdown-text
font-size: 28rpx
color: #999999
font-weight: 500
.resend-text
font-size: 28rpx
color: #256BFA
font-weight: 600
cursor: pointer
padding: 12rpx 24rpx
border-radius: 24rpx
background: rgba(37, 107, 250, 0.1)
display: inline-block
transition: all 0.2s ease
&:active
background: rgba(37, 107, 250, 0.2)
transform: scale(0.98)
.submit-btn
width: 100%
height: 96rpx
background: linear-gradient(135deg, #256BFA 0%, #1E5BFF 100%)
border-radius: 48rpx
color: #FFFFFF
font-size: 34rpx
font-weight: 600
border: none
display: flex
align-items: center
justify-content: center
box-shadow: 0 8rpx 32rpx rgba(37, 107, 250, 0.3)
transition: all 0.3s ease
position: relative
overflow: hidden
&::before
content: ''
position: absolute
top: 0
left: -100%
width: 100%
height: 100%
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent)
transition: left 0.5s ease
&:not(:disabled):hover::before
left: 100%
&:not(:disabled):active
transform: translateY(2rpx)
box-shadow: 0 4rpx 16rpx rgba(37, 107, 250, 0.3)
&:disabled
opacity: 0.5
cursor: not-allowed
box-shadow: none
.loading-spinner
width: 48rpx
height: 48rpx
border: 4rpx solid rgba(255, 255, 255, 0.3)
border-radius: 50%
border-top: 4rpx solid #FFFFFF
animation: spin 1s linear infinite
// 按钮重置样式
button::after
border: none
// 动画定义
@keyframes spin
0%
transform: rotate(0deg)
100%
transform: rotate(360deg)
// 页面进入动画
.sms-verify-page
animation: fadeIn 0.4s ease-out
@keyframes fadeIn
from
opacity: 0
transform: translateY(20rpx)
to
opacity: 1
transform: translateY(0)
</style>

697
pages/login/wx-login.vue Normal file
View File

@@ -0,0 +1,697 @@
<template>
<view class="wx-login-page">
<!-- 顶部导航栏 -->
<!-- <view class="nav-bar">
<view class="nav-back" @click="goBack">
<uni-icons type="arrowleft" size="24" color="#333"></uni-icons>
</view>
<view class="nav-title">登录</view>
<view class="nav-placeholder"></view>
</view> -->
<!-- 页面内容 -->
<view class="page-content">
<!-- Logo和标题 -->
<view class="auth-header">
<image class="auth-logo" src="@/static/logo2-S.png" mode="aspectFit"></image>
<view class="auth-title">欢迎使用就业服务</view>
</view>
<!-- 角色选择 -->
<view class="role-select">
<view class="role-title">请选择您的角色</view>
<view class="role-options">
<view
class="role-item"
:class="{ active: userType === 1 }"
@click="selectRole(1)"
>
<view class="role-icon">
<uni-icons type="person" size="32" :color="userType === 1 ? '#256BFA' : '#999'"></uni-icons>
</view>
<view class="role-text">个人</view>
</view>
<view
class="role-item"
:class="{ active: userType === 0 }"
@click="selectRole(0)"
>
<view class="role-icon">
<uni-icons type="shop" size="32" :color="userType === 0 ? '#256BFA' : '#999'"></uni-icons>
</view>
<view class="role-text">单位</view>
</view>
</view>
</view>
<!-- 机构类型选择仅单位角色显示 -->
<view v-if="userType === 0" class="org-type-select">
<view class="org-type-title">请选择机构类型</view>
<view class="org-type-options">
<view
v-for="option in orgTypeOptions"
:key="option.value"
class="org-type-item"
:class="{ active: orgType === option.value }"
@click="selectOrgType(option.value)"
>
<view class="org-type-text">{{ option.label }}</view>
</view>
</view>
</view>
<!-- 授权说明 -->
<view class="auth-tips">
<view class="tip-item">
<uni-icons type="checkmarkempty" size="16" color="#256BFA"></uni-icons>
<text>保护您的个人信息安全</text>
</view>
<view class="tip-item">
<uni-icons type="checkmarkempty" size="16" color="#256BFA"></uni-icons>
<text>为您推荐更合适的岗位</text>
</view>
<view class="tip-item">
<uni-icons type="checkmarkempty" size="16" color="#256BFA"></uni-icons>
<text>享受完整的就业服务</text>
</view>
</view>
<!-- 授权按钮 -->
<view class="auth-actions">
<!-- 微信小程序使用 open-type="getPhoneNumber" -->
<!-- #ifdef MP-WEIXIN -->
<button
class="auth-btn primary"
open-type="getPhoneNumber"
@getphonenumber="onWxGetPhoneNumber"
>
<uni-icons type="phone" size="20" color="#FFFFFF"></uni-icons>
<text>手机号快捷登录</text>
</button>
<!-- #endif -->
<!-- H5和App使用普通按钮 -->
<!-- #ifndef MP-WEIXIN -->
<button class="auth-btn primary" @click="wxLogin">
<uni-icons type="phone" size="20" color="#FFFFFF"></uni-icons>
<text>手机号快捷登录</text>
</button>
<!-- #endif -->
<!-- 测试登录按钮仅开发环境 -->
<!-- #ifdef APP-PLUS || H5 -->
<button class="auth-btn secondary" @click="testLogin">
<text>测试账号登录</text>
</button>
<!-- #endif -->
</view>
<!-- 用户协议 -->
<view class="auth-agreement">
<view class="agreement-checkbox" @click="toggleAgreement">
<uni-icons
:type="agreedToAgreement ? 'checkbox-filled' : 'circle'"
size="20"
:color="agreedToAgreement ? '#256BFA' : '#999'"
></uni-icons>
<text class="agreement-text">我已阅读并同意</text>
</view>
<text class="link" @click="openAgreement('user')">隐私协议</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, inject, onMounted, computed } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import useUserStore from '@/stores/useUserStore';
import useDictStore from '@/stores/useDictStore';
const { $api } = inject('globalFunction');
const userStore = useUserStore();
const dictStore = useDictStore();
const userType = ref(null); // 用户角色1-求职者0-企业
const orgType = ref(null); // 机构类型
const orgTypeOptions = ref([]); // 机构类型选项
const agreedToAgreement = ref(false); // 是否同意用户协议
// 计算是否可提交
const canSubmit = computed(() => {
if (userType.value === null) return false;
if (userType.value === 0 && orgType.value === null) return false;
if (!agreedToAgreement.value) return false;
return true;
});
// 获取机构类型字典
const getOrgTypeDict = async () => {
try {
const options = await dictStore.getDictSelectOption('org_type');
orgTypeOptions.value = options;
} catch (error) {
console.error('获取机构类型字典失败:', error);
// 使用备用数据
orgTypeOptions.value = [
{ label: '有限责任公司', value: '1' },
{ label: '股份有限公司', value: '2' },
{ label: '个人独资企业', value: '3' },
{ label: '合伙企业', value: '4' },
{ label: '外商投资企业', value: '5' },
{ label: '其他', value: '6' }
];
}
};
// 选择角色
const selectRole = (type) => {
userType.value = type;
orgType.value = null; // 切换角色时重置机构类型选择
};
// 选择机构类型
const selectOrgType = (type) => {
orgType.value = type;
};
// 切换协议同意状态
const toggleAgreement = () => {
agreedToAgreement.value = !agreedToAgreement.value;
};
// 打开用户协议
const openAgreement = (type) => {
const urls = {
user: '/packageA/pages/agreement/user',
privacy: '/packageA/pages/agreement/privacy'
};
uni.navigateTo({
url: urls[type]
});
};
// 返回上一页
const goBack = () => {
uni.navigateBack();
};
// 通用的验证函数
const validateForm = () => {
if (userType.value === null) {
uni.showToast({
title: '请先选择您的角色(个人或单位)',
icon: 'none',
duration: 2000
});
return false;
}
if (userType.value === 0 && orgType.value === null) {
uni.showToast({
title: '请选择机构类型',
icon: 'none',
duration: 2000
});
return false;
}
if (!agreedToAgreement.value) {
uni.showToast({
title: '请先阅读并同意隐私协议',
icon: 'none',
duration: 2000
});
return false;
}
return true;
};
// 微信小程序授权前的检查
const checkBeforeWxAuth = (e) => {
// 验证表单
if (!validateForm()) {
// 阻止微信授权流程
if (e && e.preventDefault) {
e.preventDefault();
}
return false;
} else {
console.log('Validation passed, allowing wx auth');
}
};
// 微信获取手机号
const onWxGetPhoneNumber = async (e) => {
const { encryptedData, iv } = e.detail;
// 使用通用验证函数
if (!validateForm()) {
return;
}
if (e.detail.errMsg === 'getPhoneNumber:ok') {
uni.login({
provider: 'weixin',
success: (loginRes) => {
const code = loginRes.code;
// 调用接口 /app/appWxphoneSmsCode
uni.showLoading({ title: '获取验证码中...' });
// 根据用户类型构建参数
const requestParams = {
code,
encryptedData,
iv,
userType: userType.value
};
// 只有单位用户才传递机构类型
if (userType.value === 0) {
requestParams.orgType = orgType.value;
}
$api.createRequest('/app/appWxphoneSmsCode', requestParams, 'post').then((resData) => {
uni.hideLoading();
// 检查可能的手机号字段
const possiblePhoneFields = ['phone', 'mobile', 'phoneNumber', 'tel', 'mobilePhone'];
let phoneValue = '';
for (const field of possiblePhoneFields) {
if (resData[field]) {
phoneValue = resData[field];
console.log(`从接口返回中找到手机号字段 "${field}": ${phoneValue}`);
break;
}
}
if (resData.code === 200 || resData.success) {
// 跳转到短信验证页面,传递参数
const params = {
phone: phoneValue || '', // 接口返回的手机号
openid: resData.openid || '',
unionid: resData.unionid || '',
userType: userType.value
};
// 只有单位用户才传递机构类型
if (userType.value === 0) {
params.orgType = orgType.value;
}
uni.navigateTo({
url: '/pages/login/sms-verify?' + Object.keys(params)
.map(key => `${key}=${encodeURIComponent(params[key])}`)
.join('&')
});
} else {
$api.msg(resData.msg || '获取验证码失败');
}
}).catch((err) => {
uni.hideLoading();
$api.msg(err.msg || '获取验证码失败,请重试');
});
},
fail: (err) => {
uni.showToast({
title: '获取登录信息失败,请重试',
icon: 'none',
duration: 2000
});
}
});
} else if (e.detail.errMsg === 'getPhoneNumber:fail user deny') {
uni.showToast({
title: '您取消了授权',
icon: 'none',
duration: 2000
});
} else {
uni.showToast({
title: '获取手机号失败',
icon: 'none',
duration: 2000
});
}
};
// H5/App 微信登录(暂保持原有逻辑,后续可调整)
const wxLogin = () => {
// 使用通用验证函数
if (!validateForm()) {
console.log('Validation failed in wxLogin');
return;
}
// #ifdef H5
// H5端跳转到H5登录页面
uni.navigateTo({
url: '/pages/login/h5-login'
});
return;
// #endif
// #ifdef APP-PLUS
// App微信登录逻辑暂保持原有逻辑
uni.getProvider({
service: 'oauth',
success: (res) => {
if (~res.provider.indexOf('weixin')) {
uni.login({
provider: 'weixin',
success: (loginRes) => {
console.log('微信登录成功:', loginRes);
// 根据用户类型构建参数
const loginParams = {
code: loginRes.code,
userType: userType.value
};
// 只有单位用户才传递机构类型
if (userType.value === 0) {
loginParams.orgType = orgType.value;
}
// 调用后端接口进行登录
$api.createRequest('/app/appLogin', loginParams, 'post').then((resData) => {
if (resData.token) {
userStore.loginSetToken(resData.token).then((resume) => {
// 更新用户类型到缓存
if (resData.isCompanyUser !== undefined) {
const userInfo = uni.getStorageSync('userInfo') || {};
userInfo.isCompanyUser = resData.isCompanyUser ? 0 : 1;
uni.setStorageSync('userInfo', userInfo);
}
uni.showToast({
title: '登录成功',
icon: 'success',
duration: 2000
});
// 登录成功后返回上一页
uni.navigateBack();
}).catch((error) => {
// 只有在非企业用户且确实需要简历信息时才显示错误
// 企业用户可能没有简历信息,这是正常的
if (userType.value !== 0) {
uni.showToast({
title: '获取用户信息失败',
icon: 'none',
duration: 2000
});
} else {
console.log('企业用户登录成功,简历信息可能为空');
uni.showToast({
title: '登录成功',
icon: 'success',
duration: 2000
});
uni.navigateBack();
}
});
}
});
},
fail: (err) => {
console.error('微信登录失败:', err);
uni.showToast({
title: '微信登录失败',
icon: 'none',
duration: 2000
});
}
});
}
}
});
// #endif
};
// 测试账号登录(仅开发环境)
const testLogin = () => {
// 使用通用验证函数
if (!validateForm()) {
return;
}
uni.showLoading({ title: '登录中...' });
const params = {
username: 'test',
password: 'test',
};
$api.createRequest('/app/login', params, 'post').then((resData) => {
uni.hideLoading();
userStore.loginSetToken(resData.token).then((resume) => {
// 更新用户类型到缓存
if (resData.isCompanyUser !== undefined) {
const userInfo = uni.getStorageSync('userInfo') || {};
userInfo.isCompanyUser = resData.isCompanyUser ? 0 : 1;
uni.setStorageSync('userInfo', userInfo);
}
uni.showToast({
title: '测试登录成功',
icon: 'success',
duration: 2000
});
// 登录成功后返回上一页
uni.navigateBack();
}).catch((error) => {
// 只有在非企业用户且确实需要简历信息时才显示错误
// 企业用户可能没有简历信息,这是正常的
if (userType.value !== 0) {
uni.showToast({
title: '获取用户信息失败',
icon: 'none',
duration: 2000
});
} else {
console.log('企业用户测试登录成功,简历信息可能为空');
uni.showToast({
title: '测试登录成功',
icon: 'success',
duration: 2000
});
uni.navigateBack();
}
});
}).catch((err) => {
uni.hideLoading();
uni.showToast({
title: err.msg || '登录失败',
icon: 'none',
duration: 2000
});
});
};
// 页面加载时获取字典数据
onLoad(() => {
getOrgTypeDict();
});
</script>
<style lang="stylus" scoped>
.wx-login-page
min-height: 100vh
background: #FFFFFF
.nav-bar
display: flex
align-items: center
justify-content: space-between
height: 88rpx
padding: 0 32rpx
border-bottom: 1rpx solid #f5f5f5
.nav-back
width: 60rpx
height: 60rpx
display: flex
align-items: center
justify-content: center
.nav-title
font-size: 32rpx
font-weight: 600
color: #333333
.nav-placeholder
width: 60rpx
.page-content
padding: 40rpx 40rpx 60rpx
.auth-header
text-align: center
margin-bottom: 40rpx
.auth-logo
width: 120rpx
height: 120rpx
margin: 0 auto 24rpx
.auth-title
font-size: 36rpx
font-weight: 600
color: #333333
margin-bottom: 12rpx
.role-select
margin-bottom: 32rpx
.role-title
font-size: 28rpx
font-weight: 500
color: #333333
margin-bottom: 20rpx
text-align: center
.role-options
display: flex
justify-content: space-between
gap: 20rpx
.role-item
flex: 1
background: #F7F8FA
border: 2rpx solid #E5E5E5
border-radius: 16rpx
padding: 32rpx 20rpx
display: flex
flex-direction: column
align-items: center
position: relative
transition: all 0.3s ease
cursor: pointer
&.active
background: #F0F5FF
border-color: #256BFA
box-shadow: 0 4rpx 12rpx rgba(37, 107, 250, 0.15)
.role-icon
margin-bottom: 16rpx
.role-text
font-size: 28rpx
color: #333333
font-weight: 500
.org-type-select
margin-bottom: 22rpx
.org-type-title
font-size: 28rpx
font-weight: 500
color: #333333
margin-bottom: 20rpx
text-align: center
.org-type-options
display: flex
flex-wrap: wrap
gap: 16rpx
.org-type-item
display:inline-block
background: #F7F8FA
border: 2rpx solid #E5E5E5
border-radius: 10rpx
padding: 10rpx 10rpx
transition: all 0.3s ease
cursor: pointer
text-align: center
&.active
background: #F0F5FF
border-color: #256BFA
box-shadow: 0 4rpx 12rpx rgba(37, 107, 250, 0.15)
.org-type-text
font-size: 22rpx
color: #333333
font-weight: 500
.auth-tips
background: #F7F8FA
border-radius: 16rpx
padding: 24rpx
margin-bottom: 40rpx
.tip-item
display: flex
align-items: center
margin-bottom: 16rpx
font-size: 26rpx
color: #666666
&:last-child
margin-bottom: 0
text
margin-left: 12rpx
.auth-actions
margin-bottom: 32rpx
.auth-btn
width: 100%
height: 88rpx
border-radius: 44rpx
display: flex
align-items: center
justify-content: center
font-size: 32rpx
font-weight: 500
border: none
margin-bottom: 20rpx
&:last-child
margin-bottom: 0
&.primary
background: linear-gradient(135deg, #256BFA 0%, #1E5BFF 100%)
color: #FFFFFF
box-shadow: 0 8rpx 20rpx rgba(37, 107, 250, 0.3)
&.secondary
background: #F7F8FA
color: #666666
&:disabled
opacity: 0.5
cursor: not-allowed
background: #CCCCCC !important
box-shadow: none !important
text
margin-left: 12rpx
.auth-agreement
display: flex
align-items: center
justify-content: center
font-size: 24rpx
color: #999999
line-height: 1.6
flex-wrap: wrap
gap: 8rpx
.agreement-checkbox
display: flex
align-items: center
cursor: pointer
.agreement-text
margin-left: 8rpx
color: #666666
.link
color: #256BFA
text-decoration: underline
// 按钮重置样式
button::after
border: none
</style>

View File

@@ -144,10 +144,7 @@ function close() {
}
function confirm() {
// 调用退出登录
useUserStore().logOut();
// 关闭弹窗
popup.value.close();
useUserStore().logOut(false); // 不显示登录弹窗
// 跳转到首页
uni.reLaunch({
url: '/pages/index/index'
@@ -163,9 +160,9 @@ onShow(() => {
onMounted(() => {
uni.$on('showLoginModal', () => {
// 这里可以显示微信登录弹窗
// 由于这个页面没有 WxAuthLogin 组件,我们跳转到首页让首页处理
uni.reLaunch({
url: '/pages/index/index'
// 跳转到微信登录页面
uni.navigateTo({
url: '/pages/login/wx-login'
});
});
});

View File

@@ -175,9 +175,9 @@ onShow(() => {
onMounted(() => {
uni.$on('showLoginModal', () => {
// 这里可以显示微信登录弹窗
// 由于这个页面没有 WxAuthLogin 组件,我们跳转到首页让首页处理
uni.reLaunch({
url: '/pages/index/index'
// 跳转到微信登录页面
uni.navigateTo({
url: '/pages/login/wx-login'
});
});
});
@@ -191,7 +191,11 @@ function close() {
}
function confirm() {
useUserStore().logOut();
useUserStore().logOut(false); // 不显示登录弹窗
// 跳转到首页
uni.reLaunch({
url: '/pages/index/index'
});
}
const isAbove90 = (percent) => parseFloat(percent) < 90;

View File

@@ -31,8 +31,7 @@
<!-- 自定义tabbar -->
<CustomTabBar :currentPage="3" />
<!-- 微信授权登录弹窗 -->
<WxAuthLogin ref="wxAuthLoginRef" @success="handleLoginSuccess"></WxAuthLogin>
<!-- 统一使用系统tabBar -->
</view>
@@ -46,14 +45,14 @@ import Tabbar from '@/components/tabbar/midell-box.vue';
import ReadComponent from './read.vue';
import UnreadComponent from './unread.vue';
import { tabbarManager } from '@/utils/tabbarManager';
import WxAuthLogin from '@/components/WxAuthLogin/WxAuthLogin.vue';
const loadedMap = reactive([false, false]);
const swiperRefs = [ref(null), ref(null)];
const components = [ReadComponent, UnreadComponent];
import { storeToRefs } from 'pinia';
import { useReadMsg } from '@/stores/useReadMsg';
const { unreadCount } = storeToRefs(useReadMsg());
const wxAuthLoginRef = ref(null);
onShow(() => {
// 获取消息列表
@@ -71,7 +70,7 @@ onMounted(() => {
// 监听退出登录事件,显示微信登录弹窗
uni.$on('showLoginModal', () => {
wxAuthLoginRef.value?.open();
uni.navigateTo({ url: '/pages/login/wx-login' });
});
});

View File

@@ -74,10 +74,17 @@ const useUserStore = defineStore("user", () => {
uni.removeStorageSync('token')
uni.removeStorageSync('Padmin-Token')
// 如果需要显示登录弹窗,则通过事件通知页面显示微信登录弹窗
if (showLoginModal) {
// 通过 uni.$emit 发送全局事件,通知页面显示登录弹窗
uni.$emit('showLoginModal');
}
// if (showLoginModal) {
// // 通过 uni.$emit 发送全局事件,通知页面显示登录弹窗
// uni.$emit('showLoginModal');
// }
//#ifdef H5
// 跳转到首页
window.location.href = 'https://www.xjksly.cn/mechine-single-vue/';
//#endif
uni.reLaunch({
url: '/pages/index/index'
});
}
const getUserInfo = () => {
@@ -96,7 +103,13 @@ const useUserStore = defineStore("user", () => {
similarityJobs.setUserInfo(resume.data)
setUserInfo(resume);
reslove(resume)
}).catch(() => reject());
}).catch((error) => {
// 对于企业用户,简历接口可能失败,但这不应该阻止登录流程
// 记录错误但不reject让登录流程继续
console.warn('获取简历信息失败,可能是企业用户或无简历信息:', error);
// 返回一个空的简历对象,让登录流程继续
reslove({ data: {} });
});
})
}

View File

@@ -125,7 +125,7 @@ const useChatGroupDBStore = defineStore("messageGroup", () => {
dataId: customDataID
};
if (fileUrls && fileUrls.length) {
params['fileUrl'] = fileUrls.map((item) => item.url);
params['fileUrl'] = fileUrls.map((item) => item.serverPath || item.url);
}
// ------>
const MsgData = {

View File

@@ -100,7 +100,7 @@ export default {
return {
data: {
list: this.value ? this.value : [],
column: this.column < 2 ? 2 : this.column,
column: this.column < 1 ? 1 : this.column,
columnSpace: this.columnSpace <= 5 ? this.columnSpace : 5,
imageKey: this.imageKey,
seat: this.seat,
@@ -179,7 +179,7 @@ export default {
this.isRefresh = true;
this.adds = [];
this.data.list = this.value ? this.value : [];
this.data.column = this.column < 2 ? 2 : this.column >= this.maxColumn ? this.maxColumn : this.column;
this.data.column = this.column < 1 ? 1 : this.column >= this.maxColumn ? this.maxColumn : this.column;
this.data.columnSpace = this.columnSpace <= 5 ? this.columnSpace : 5;
this.data.imageKey = this.imageKey;
this.data.seat = this.seat;

View File

@@ -44,22 +44,11 @@ export function navigateToLoginPage(options = {}) {
switch (platform) {
case 'mp-weixin':
// 小程序端使用微信授权登录,直接显示微信授权弹窗
uni.$emit('showLoginModal', { loginType: 'wechat' });
return;
case 'h5':
uni.showToast({
title: '请先登录',
icon: 'none',
duration: 2000
});
return;
case 'app':
// App端使用微信授权登录
uni.$emit('showLoginModal', { loginType: 'wechat' });
return;
case 'h5':
// 跳转到微信登录页面
loginPage = '/pages/login/wx-login';
break;
default:
loginPage = '/pages/login/h5-login';

View File

@@ -155,7 +155,7 @@ export function uploadFile(tempFilePaths, loading = false) {
header["Authorization"] = encodeURIComponent(Authorization);
return new Promise((resolve, reject) => {
uni.uploadFile({
url: config.baseUrl + '/app/file/upload',
url: config.baseUrl + '/app/file/uploadFile',
filePath: tempFilePaths,
name: 'file',
header,