Compare commits

1 Commits

Author SHA1 Message Date
史典卓
af3ff2db18 flat: 菜单 2025-07-09 16:48:37 +08:00
90 changed files with 1287 additions and 4683 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

21
.gitignore vendored
View File

@@ -1,22 +1 @@
# 编译/打包输出目录
/unpackage/
# 依赖包目录
/node_modules/
# IDE/编辑器配置
.vscode/
.idea/
# macOS 系统文件
.DS_Store
# Windows 系统文件
Thumbs.db
# 日志文件
npm-debug.log
yarn-debug.log
# HBuilderX 运行时生成的文件
.hbuilderx

16
.hbuilderx/launch.json Normal file
View File

@@ -0,0 +1,16 @@
{ // launch.json 配置了启动调试时相关设置configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
// launchtype项可配置值为local或remote, local代表前端连本地云函数remote代表前端连云端云函数
"version": "0.0",
"configurations": [{
"default" :
{
"launchtype" : "local"
},
"mp-weixin" :
{
"launchtype" : "local"
},
"type" : "uniCloud"
}
]
}

136
App.vue
View File

@@ -3,35 +3,40 @@ import { reactive, inject, onMounted } from 'vue';
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app';
import useUserStore from './stores/useUserStore';
import useDictStore from './stores/useDictStore';
const { $api, navTo, appendScriptTagElement, aes_Decrypt, sm2_Decrypt } = inject('globalFunction');
import config from '@/config.js';
const appword = 'aKd20dbGdFvmuwrt'; // 固定值
const { $api, navTo, appendScriptTagElement } = inject('globalFunction');
onLaunch((options) => {
uni.hideTabBar();
useDictStore().getDictData();
try {
getUserInfo();
} catch {
console.log('不是爱山东平台,使用测试登陆');
useUserStore().initSeesionId(); //更新
let token = uni.getStorageSync('token') || ''; // 同步获取 缓存信息
if (token) {
useUserStore()
.loginSetToken(token)
.then(() => {
$api.msg('登录成功');
});
} else {
uni.redirectTo({
url: '/pages/login/login',
// uni.hideTabBar();
// 登录
let token = uni.getStorageSync('token') || ''; // 同步获取 缓存信息
if (token) {
useUserStore()
.loginSetToken(token)
.then(() => {
$api.msg('登录成功');
});
}
} else {
uni.redirectTo({
url: '/pages/login/login',
});
}
});
onMounted(() => {});
onMounted(() => {
// #ifndef MP-WEIXIN
if (process.env.NODE_ENV === 'development') {
appendScriptTagElement('./static/js/jweixin-1.4.0.js').then(() => {
console.log('✅ 微信 JSSDK 加载完成');
});
} else {
appendScriptTagElement('/static/js/jweixin-1.4.0.js').then(() => {
console.log('✅ 微信 JSSDK 加载完成');
});
}
// #endif
});
onShow(() => {
console.log('App Show');
@@ -40,87 +45,6 @@ onShow(() => {
onHide(() => {
console.log('App Hide');
});
function getUserInfo() {
lightAppJssdk.user.getUserInfoWithEncryptedParamByAppId({
appId: 'qdsrgznrgpp', // 接入方在成功创建应用后自动生成
success: function (data) {
if (data == '未登录') onLoginApp();
else {
if (typeof data == 'string') data = JSON.parse(data);
const sm2_privateKey = '7e14966df4ecd4241ed082ef716d82b52113cb5899ebdc704a98844d0a32b0dc';
let sm2_encrypt_result = data.data;
let sm2_decrypt_result = sm2_Decrypt(sm2_encrypt_result, sm2_privateKey);
if (typeof sm2_decrypt_result == 'string') sm2_decrypt_result = JSON.parse(sm2_decrypt_result);
// 其次,对sm2解密后的结果进行 aes解密
// aes解密需要用到 appword , 为固定值,使用示例代码中的即可
let aes_encrypt_result = sm2_decrypt_result.data;
let aes_decrypt_result = aes_Decrypt(aes_encrypt_result, appword);
// 加密
loginCallback(aes_decrypt_result);
}
},
fail: function (data) {
console.log('err', data);
},
});
}
/**
* 使用jssdk调用登录页面
*/
function onLoginApp() {
lightAppJssdk.user.loginapp({
success: function (data) {
if (data == '未登录') {
//取消登录或登录失败,关闭页面
oncloseWindow();
} else {
getUserInfo();
}
},
fail: function (data) {
//关闭页面
oncloseWindow();
},
});
}
/**
* 关闭容器
*/
function oncloseWindow() {
lightAppJssdk.navigation.close({
success: function (data) {},
fail: function (data) {},
});
}
function loginCallback(userInfo) {
let params = {
username: userInfo,
};
$api.createRequest('/app/login', params, 'post').then((resData) => {
useUserStore()
.loginSetToken(resData.token)
.then((resume) => {
if (resume.data.jobTitleId) {
useUserStore().initSeesionId();
uni.reLaunch({
url: '/pages/index/index',
});
} else {
uni.redirectTo({
url: '/pages/login/login',
});
}
});
});
}
</script>
<style>
@@ -157,19 +81,19 @@ uni-modal,
@font-face {
font-family: PingFangSC-Regular;
src: url('https://qd.zhaopinzao8dian.com/file/csn/PingFangSC-Regular.woff2') format('woff2');
src: url('/static/font/PingFangSC-Regular.woff2') format('woff2');
font-display: swap;
}
@font-face {
font-family: PingFangSC-Medium;
src: url('https://qd.zhaopinzao8dian.com/file/csn/PingFangSC-Medium.woff2') format('woff2');
src: url('/static/font/PingFangSC-Medium.woff2') format('woff2');
font-display: swap;
}
@font-face {
font-family: DIN-Medium;
src: url('https://qd.zhaopinzao8dian.com/file/csn/DIN-Medium.woff2') format('woff2');
src: url('/static/font/DIN-Medium.woff2') format('woff2');
font-display: swap;
}

BIN
common/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -67,7 +67,7 @@ html {
}
.btn-feel {
transition: transform 0.15s ease;
transition: transform 0.2s ease;
transform-style: preserve-3d;
}

View File

@@ -1,5 +1,6 @@
import useUserStore from "../stores/useUserStore";
import {
request,
createRequest,
uploadFile
} from "../utils/request";
@@ -7,9 +8,6 @@ import streamRequest, {
chatRequest
} from "../utils/streamRequest.js";
const sm4 = typeof window.sm4 !== 'undefined' ? window.sm4 :
(typeof window.smCrypto !== 'undefined' ? window.smCrypto.sm4 : null);
export const CloneDeep = (props) => {
if (typeof props !== 'object' || props === null) {
return props
@@ -53,7 +51,6 @@ const prePage = () => {
/**
* 页面跳转封装,支持 query 参数传递和返回回调
* @param {string} url - 跳转路径
@@ -62,22 +59,17 @@ const prePage = () => {
* @param {object} options.query - 携带参数
* @param {function} options.onBack - 页面返回时的回调(目标页调用 uni.navigateBack 时传递数据)
*/
let isJumping = false
export const navTo = function(url, {
needLogin = false,
query = {},
onBack = null
} = {}) {
const userStore = useUserStore();
if (isJumping) return
isJumping = true
if (needLogin && !userStore.hasLogin) {
setTimeout(() => {
uni.navigateTo({
url: '/pages/login/login'
});
isJumping = false
}, 170);
uni.navigateTo({
url: '/pages/login/login'
});
return;
}
@@ -92,12 +84,9 @@ export const navTo = function(url, {
currentPage.__onBackCallback__ = onBack;
}
setTimeout(() => {
uni.navigateTo({
url: finalUrl
});
isJumping = false
}, 170);
uni.navigateTo({
url: finalUrl
});
};
export const navBack = function({
@@ -554,71 +543,11 @@ function isEmptyObject(obj) {
return obj && typeof obj === 'object' && !Array.isArray(obj) && Object.keys(obj).length === 0;
}
function aes_Decrypt(word, key) {
var key = CryptoJS.enc.Utf8.parse(key) //转为128bit
var srcs = CryptoJS.enc.Hex.parse(word) //转为16进制
var str = CryptoJS.enc.Base64.stringify(srcs) //变为Base64编码的字符串
var decrypt = CryptoJS.AES.decrypt(str, key, {
mode: CryptoJS.mode.ECB,
spadding: CryptoJS.pad.Pkcs7
})
return decrypt.toString(CryptoJS.enc.Utf8)
}
export function sm2_Decrypt(word, key) {
return SM.decrypt(word, key);
}
export function sm2_Encrypt(word, key) {
return SM.encrypt(word, key);
}
export function sm4Decrypt(key, value, mode = "hex") {
try {
if (key.length !== 32) {
alert('密钥必须是32位16进制字符串128位');
return;
}
const decrypted = sm4.decrypt(value, key, {
mode: 'ecb',
cipherType: mode === 'hex' ? 'hex' : 'base64',
padding: 'pkcs#5'
});
return decrypted
} catch (e) {
console.log('解密失败')
}
}
export function sm4Encrypt(key, value, mode = "hex") {
try {
if (key.length !== 32) {
alert('密钥必须是32位16进制字符串128位');
return;
}
const encrypted = sm4.encrypt(value, key, {
mode: 'ecb',
cipherType: mode === 'hex' ? 'hex' : 'base64',
padding: 'pkcs#5'
});
return encrypted
} catch (e) {
console.log('加密失败')
}
}
export const $api = {
msg,
prePage,
sleep,
request,
createRequest,
streamRequest,
chatRequest,
@@ -626,8 +555,7 @@ export const $api = {
uploadFile,
formatFileSize,
sendingMiniProgramMessage,
copyText,
aes_Decrypt,
copyText
}
@@ -656,9 +584,5 @@ export default {
appendScriptTagElement,
insertSortData,
isInWechatMiniProgramWebview,
isEmptyObject,
sm4Decrypt,
aes_Decrypt,
sm2_Decrypt,
sm2_Encrypt
isEmptyObject
}

BIN
components/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -12,7 +12,7 @@
<view class="header-btnLf">
<slot name="headerleft"></slot>
</view>
<view class="header-title" :style="{ color: titleColor }">
<view class="header-title">
<view>{{ title }}</view>
<view v-show="subTitle" class="subtitle-text">{{ subTitle }}</view>
</view>
@@ -45,15 +45,12 @@
<script setup>
import img from '@/static/icon/background2.png';
const emit = defineEmits(['onScrollBottom']);
defineProps({
title: {
type: String,
default: '标题',
},
titleColor: {
type: String,
default: '#333333',
},
border: {
type: Boolean,
default: false,
@@ -113,13 +110,11 @@ const handleScrollToLower = () => {
align-items: center;
padding: 7rpx 3rpx;
.header-title {
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei',
sans-serif;
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
color: #000000;
font-weight: bold;
.subtitle-text {
font-family: 'PingFangSC-Regular', 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial,
sans-serif;
font-family: 'PingFangSC-Regular', 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-weight: 400;
font-size: 28rpx;
color: #333333;

View File

@@ -1,327 +0,0 @@
<template>
<uni-popup
ref="popup"
type="bottom"
borderRadius="10px 10px 0 0"
background-color="#FFFFFF"
:mask-click="maskClick"
>
<view class="popup-content">
<view class="popup-header">
<view class="btn-cancel" @click="cancel">取消</view>
<view class="title">{{ title }}</view>
<view class="btn-confirm" @click="confirm">确认</view>
</view>
<view class="popup-list">
<picker-view
indicator-style="height: 84rpx;"
:value="selectedIndex"
@change="bindChange"
class="picker-view"
>
<picker-view-column>
<view
v-for="(year, index) in years"
:key="index"
class="item"
:class="{ 'item-active': selectedIndex[0] === index }"
>
<text>{{ year }}</text>
<text></text>
</view>
</picker-view-column>
<picker-view-column>
<view
v-for="(month, index) in months"
:key="index"
class="item"
:class="{ 'item-active': selectedIndex[1] === index }"
>
<text>{{ month }}</text>
<text></text>
</view>
</picker-view-column>
<picker-view-column>
<view
v-for="(day, index) in days"
:key="index"
class="item"
:class="{ 'item-active': selectedIndex[2] === index }"
>
<text>{{ day }}</text>
<text></text>
</view>
</picker-view-column>
</picker-view>
</view>
</view>
</uni-popup>
</template>
<script>
export default {
name: 'datePicker',
data() {
return {
maskClick: false,
title: '选择日期',
confirmCallback: null,
cancelCallback: null,
changeCallback: null,
selectedIndex: [0, 0, 0],
selectedDate: '',
// 日期数据
years: [],
months: [],
days: [],
// 配置
startYear: 0,
endYear: 0,
};
},
created() {
this.initDateData();
},
methods: {
// 初始化日期数据
initDateData() {
const currentYear = new Date().getFullYear();
this.startYear = currentYear - 50; // 往前50年
this.endYear = currentYear + 10; // 往后10年
// 生成年份
this.years = [];
for (let i = this.startYear; i <= this.endYear; i++) {
this.years.push(i);
}
// 生成月份
this.months = [];
for (let i = 1; i <= 12; i++) {
this.months.push(i);
}
// 初始天数(默认当前年月)
this.updateDays(this.years[0], this.months[0]);
},
// 根据年月更新天数
updateDays(year, month) {
const daysInMonth = new Date(year, month, 0).getDate();
this.days = [];
for (let i = 1; i <= daysInMonth; i++) {
this.days.push(i);
}
},
open(config = {}) {
const {
title = '选择日期',
success,
cancel,
change,
maskClick = false,
defaultDate = '',
} = config;
this.reset();
this.title = title;
if (typeof success === 'function') this.confirmCallback = success;
if (typeof cancel === 'function') this.cancelCallback = cancel;
if (typeof change === 'function') this.changeCallback = change;
this.maskClick = maskClick;
// 设置默认选中
this.setDefaultDate(defaultDate);
this.$nextTick(() => {
this.$refs.popup.open();
});
},
close() {
this.$refs.popup.close();
},
// 设置默认日期
setDefaultDate(dateStr) {
if (!dateStr) {
// 没有默认日期,使用当前日期
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1;
const day = now.getDate();
this.selectedIndex = [
this.years.findIndex(y => y === year),
this.months.findIndex(m => m === month),
this.days.findIndex(d => d === day)
];
} else {
// 解析日期字符串 (支持 YYYY-MM-DD 格式)
const [year, month, day] = dateStr.split('-').map(Number);
this.selectedIndex = [
this.years.findIndex(y => y === year),
this.months.findIndex(m => m === month),
this.days.findIndex(d => d === day)
];
}
// 确保索引有效
this.selectedIndex = this.selectedIndex.map((index, i) =>
index === -1 ? 0 : index
);
this.updateSelectedDate();
},
bindChange(e) {
this.selectedIndex = e.detail.value;
// 检查是否需要更新天数
const oldDaysLength = this.days.length;
const selectedYear = this.years[this.selectedIndex[0]];
const selectedMonth = this.months[this.selectedIndex[1]];
this.updateDays(selectedYear, selectedMonth);
// 如果天数变化且当前选择的日期超过新月份的天数,调整日期索引
if (this.days.length !== oldDaysLength && this.selectedIndex[2] >= this.days.length) {
this.selectedIndex[2] = this.days.length - 1;
}
this.updateSelectedDate();
// 触发change回调
this.changeCallback && this.changeCallback(this.selectedDate, this.selectedIndex);
},
// 更新选中的日期字符串
updateSelectedDate() {
const year = this.years[this.selectedIndex[0]];
const month = this.months[this.selectedIndex[1]].toString().padStart(2, '0');
const day = this.days[this.selectedIndex[2]].toString().padStart(2, '0');
this.selectedDate = `${year}-${month}-${day}`;
},
cancel() {
this.clickCallback(this.cancelCallback);
},
confirm() {
this.clickCallback(this.confirmCallback);
},
async clickCallback(callback) {
if (typeof callback !== 'function') {
this.$refs.popup.close();
return;
}
try {
const result = await callback(this.selectedDate, this.selectedIndex);
if (result !== false) {
this.$refs.popup.close();
}
} catch (error) {
console.error('callback 执行出错:', error);
}
},
reset() {
this.maskClick = false;
this.confirmCallback = null;
this.cancelCallback = null;
this.changeCallback = null;
this.selectedIndex = [0, 0, 0];
this.selectedDate = '';
this.title = '选择日期';
},
// 设置日期范围
setDateRange(startYear, endYear) {
this.startYear = startYear;
this.endYear = endYear;
// 重新生成年份
this.years = [];
for (let i = this.startYear; i <= this.endYear; i++) {
this.years.push(i);
}
// 重置选中索引
this.selectedIndex[0] = 0;
this.updateDays(this.years[0], this.months[0]);
},
},
};
</script>
<style lang="scss" scoped>
.popup-content {
color: #000000;
height: 50vh;
}
.popup-list {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
justify-content: space-evenly;
flex: 1;
overflow: hidden;
.picker-view {
width: 100%;
height: calc(50vh - 100rpx);
margin-top: 20rpx;
.uni-picker-view-mask {
background: rgba(0, 0, 0, 0);
}
.item {
line-height: 84rpx;
height: 84rpx;
text-align: center;
font-weight: 400;
font-size: 32rpx;
color: #cccccc;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
}
.item-active {
color: #333333;
}
.uni-picker-view-indicator:after {
border-color: #e3e3e3;
}
.uni-picker-view-indicator:before {
border-color: #e3e3e3;
}
}
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 40rpx 40rpx 10rpx 40rpx;
.title {
font-weight: 500;
font-size: 36rpx;
color: #333333;
text-align: center;
}
.btn-cancel {
font-weight: 400;
font-size: 32rpx;
color: #666d7f;
line-height: 38rpx;
}
.btn-confirm {
font-weight: 400;
font-size: 32rpx;
color: #256bfa;
}
}
</style>

View File

@@ -1,8 +1,7 @@
<template>
<view class="empty" :style="{ background: bgcolor, marginTop: mrTop + 'rpx' }">
<view class="ty_content" :style="{ paddingTop: pdTop + 'rpx' }">
<view class="content_top">
<!-- <view class="content_top btn-shaky"> -->
<view class="content_top btn-shaky">
<image v-if="pictrue" :src="pictrue" mode=""></image>
<image v-else src="@/static/icon/empty.png" mode=""></image>
</view>

View File

@@ -24,13 +24,11 @@ const renderedHtml = computed(() => parseMarkdown(props.content));
const handleItemClick = (e) => {
let { attrs } = e.detail.node;
console.log(attrs);
let { 'data-copy-index': codeDataIndex, 'data-job-id': jobId, class: className } = attrs;
switch (className) {
case 'custom-card':
return navTo('/packageA/pages/post/post?jobId=' + jobId);
case 'custom-more':
return navTo('/packageA/pages/moreJobs/moreJobs?jobId=' + jobId);
navTo('/packageA/pages/post/post?jobId=' + jobId);
return;
case 'copy-btn':
uni.setClipboardData({
data: codeDataList[codeDataIndex],
@@ -42,7 +40,6 @@ const handleItemClick = (e) => {
});
},
});
break;
}
};
</script>
@@ -261,19 +258,6 @@ ol {
</style>
<style lang="stylus">
.custom-more{
display: flex
justify-content: flex-end
color: #256BFA
padding-top: 5rpx
padding-bottom: 14rpx
.more-icon{
width: 60rpx;
height: 40rpx;
background: url('@/static/svg/seemore.svg') center center no-repeat;
background-size: 100% 100%
}
}
.custom-card
background: #FFFFFF;
box-shadow: 0rpx 0rpx 8rpx 0rpx rgba(0,0,0,0.04);

View File

@@ -101,13 +101,13 @@ function nextDetail(job) {
.company{
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 500;
font-size: 30rpx;
font-size: 32rpx;
color: #333333;
}
.salary{
font-family: DIN-Medium;
font-weight: 500;
font-size: 26rpx;
font-size: 28rpx;
color: #4C6EFB;
white-space: nowrap
line-height: 48rpx

View File

@@ -149,7 +149,6 @@ const cleanup = () => {
Object.keys(selectedValues).forEach((key) => {
delete selectedValues[key];
});
count.value = 0;
};
const scrollTo = (key) => {
@@ -162,7 +161,6 @@ function getoptions() {
getTransformChildren('experience', '工作经验'),
getTransformChildren('scale', '公司规模'),
];
console.log(arr);
if (area.value) {
arr.push(getTransformChildren('area', '区域'));
}

View File

@@ -1,345 +0,0 @@
<template>
<uni-popup
ref="popup"
type="bottom"
borderRadius="10px 10px 0 0"
background-color="#FFFFFF"
@maskClick="maskClickFn"
:mask-click="maskClick"
class="popup-fix"
>
<view class="popup-content">
<view class="popup-header">
<view class="btn-cancel" @click="cancel">取消</view>
<view class="title">
<text>{{ title }}</text>
</view>
<view class="btn-confirm"></view>
</view>
<view class="popup-list">
<view class="content-wrapper">
<scroll-view class="filter-nav" scroll-y>
<view
v-for="(item, index) in filterOptions"
:key="index"
class="nav-item button-click"
:class="{ active: activeTab === item.key }"
@click="scrollTo(item.key)"
>
{{ item.label }}
</view>
</scroll-view>
<scroll-view class="filter-content" :scroll-into-view="activeTab" scroll-y>
<template v-for="(item, index) in filterOptions" :key="index">
<view class="content-item">
<view class="item-title" :id="item.key">{{ item.label }}</view>
<view class="check-content">
<view
v-for="option in item.options"
:key="option.value"
class="checkbox-item button-click"
:class="{
checkedstyle: activeValue === option.value,
}"
@click="handleItemClick(option)"
>
<text class="option-label">{{ option.label }}</text>
</view>
</view>
</view>
</template>
</scroll-view>
</view>
</view>
</view>
</uni-popup>
</template>
<script setup>
import { ref, reactive, nextTick, onBeforeMount } from 'vue';
import useDictStore from '@/stores/useDictStore';
const { getTransformChildren } = useDictStore();
const area = ref(true);
const maskClick = ref(false);
const maskClickFn = ref(null);
const title = ref('标题');
const confirmCallback = ref(null);
const cancelCallback = ref(null);
const changeCallback = ref(null);
const popup = ref(null);
// MODIFIED: 新增 ref用于存储当前激活的选项值
const activeValue = ref(null);
const activeTab = ref('');
const filterOptions = ref([]);
const listData = ref([]);
// MODIFIED: open 方法增加一个 currentValue 参数
const open = (newConfig = {}) => {
const {
title: configTitle,
success,
cancel,
change,
data,
maskClick: configMaskClick = false,
currentValue, // MODIFIED: 接收父组件传入的当前值
} = newConfig;
// reset();
if (configTitle) title.value = configTitle;
if (typeof success === 'function') confirmCallback.value = success;
if (typeof cancel === 'function') cancelCallback.value = cancel;
if (typeof change === 'function') changeCallback.value = change;
if (Array.isArray(data)) listData.value = data;
// MODIFIED: 将父组件传入的值
activeValue.value = currentValue;
if (configMaskClick) {
maskClick.value = configMaskClick;
maskClickFn.value = cancel;
}
getoptions();
nextTick(() => {
popup.value?.open();
});
};
const close = () => {
popup.value?.close();
};
const cancel = () => {
handleClick(cancelCallback.value, null);
};
const handleClick = async (callback, selectedItem) => {
if (typeof callback !== 'function') {
close();
return;
}
try {
const result = await callback(selectedItem);
if (result !== false) close();
} catch (error) {
console.error('Callback execution error:', error);
}
};
const handleItemClick = (option) => {
// MODIFIED: 点击时,更新本地的 activeValue
activeValue.value = option.value;
// 立即调用回调并传递所选的完整 option
handleClick(confirmCallback.value, option);
};
const scrollTo = (key) => {
activeTab.value = key;
};
function getoptions() {
filterOptions.value = transformRegionalData(listData.value);
activeTab.value = listData.value[0].key;
}
const reset = () => {
maskClick.value = false;
confirmCallback.value = null;
cancelCallback.value = null;
changeCallback.value = null;
// MODIFIED: 重置时也清空 activeValue
activeValue.value = null;
};
function transformRegionalData(sourceData) {
const options = sourceData.map((region, index) => {
const ls = region.areaList.map((commercial) => ({
...commercial,
text: commercial.commercialAreaName,
label: commercial.commercialAreaName,
value: commercial.commercialAreaId,
key: commercial.commercialAreaId,
listClass: 'default',
status: 'default',
}));
return {
label: region.regionalName,
key: 'lx' + region.regionalId,
options: ls,
};
});
return options;
}
// 暴露方法给父组件
defineExpose({
open,
close,
});
</script>
<style lang="scss" scoped>
/* 样式表无变化,保持原样即可 */
.popup-fix {
position: fixed !important;
left: 0;
right: 0;
bottom: 0;
top: 0;
height: 100vh;
z-index: 9999;
}
.popup-content {
color: #000000;
height: 80vh;
}
.popup-bottom {
display: none;
}
.popup-list {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
justify-content: space-evenly;
height: calc(80vh - 100rpx);
overflow: hidden;
.picker-view {
width: 100%;
height: 500rpx;
margin-top: 20rpx;
.uni-picker-view-mask {
background: rgba(0, 0, 0, 0);
}
.item {
line-height: 84rpx;
height: 84rpx;
text-align: center;
font-weight: 400;
font-size: 32rpx;
color: #cccccc;
}
.item-active {
color: #333333;
}
.uni-picker-view-indicator:after {
border-color: #e3e3e3;
}
.uni-picker-view-indicator:before {
border-color: #e3e3e3;
}
}
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 40rpx 40rpx 10rpx 40rpx;
.title {
font-weight: 500;
font-size: 36rpx;
color: #333333;
text-align: center;
}
.btn-cancel {
font-weight: 400;
font-size: 32rpx;
color: #666d7f;
line-height: 38rpx;
}
.btn-confirm {
font-weight: 400;
font-size: 32rpx;
color: #256bfa;
min-width: 60rpx;
}
}
.content-wrapper {
flex: 1;
display: flex;
overflow: hidden;
height: 100%;
}
.filter-nav {
width: 200rpx;
background-color: #ffffff;
.nav-item {
height: 100rpx;
line-height: 100rpx;
text-align: center;
font-weight: 400;
font-size: 28rpx;
color: #666d7f;
&.active {
font-weight: 500;
font-size: 28rpx;
color: #256bfa;
}
}
}
.filter-content {
flex: 1;
padding: 20rpx;
background-color: #f6f6f6;
.content-item {
margin-top: 30rpx;
.item-title {
font-weight: 400;
font-size: 28rpx;
color: #333333;
margin-bottom: 15rpx;
}
}
.content-item:first-child {
margin-top: 0rpx;
}
.check-content {
display: grid;
gap: 16rpx;
grid-template-columns: repeat(auto-fill, minmax(180rpx, 1fr));
place-items: stretch;
.checkbox-item {
display: flex;
align-items: center;
text-align: center;
background-color: #d9d9d9;
min-width: 0;
padding: 0 10rpx;
height: 80rpx;
background: #e8eaee;
border-radius: 12rpx 12rpx 12rpx 12rpx;
.option-label {
font-size: 28rpx;
width: 100%;
white-space: nowrap;
overflow: hidden;
}
}
/* 这个样式现在会根据 activeValue 动态应用 */
.checkedstyle {
height: 76rpx;
background: rgba(37, 107, 250, 0.06);
border-radius: 12rpx 12rpx 12rpx 12rpx;
border: 2rpx solid #256bfa;
color: #256bfa;
}
}
}
</style>

View File

@@ -32,7 +32,7 @@
</template>
<script setup>
import { ref, reactive, computed, inject, nextTick, onMounted } from 'vue';
import { ref, reactive, computed, inject, nextTick, defineExpose, onMounted } from 'vue';
const { $api, navTo, setCheckedNodes, cloneDeep } = inject('globalFunction');
import useUserStore from '@/stores/useUserStore';
import { storeToRefs } from 'pinia';
@@ -58,9 +58,9 @@ const state = reactive({
visible: false,
});
// onMounted(() => {
// serchforIt();
// });
onMounted(() => {
serchforIt();
});
// 统一处理二维数组格式
const processedListData = computed(() => {
@@ -82,11 +82,11 @@ const open = (newConfig = {}) => {
rowLabel: configRowLabel = 'label',
rowKey: configRowKey = 'value',
maskClick: configMaskClick = false,
defaultId = '',
defaultIndex = [],
} = newConfig;
reset();
serchforIt(defaultId);
serchforIt();
if (configTitle) title.value = configTitle;
if (typeof success === 'function') confirmCallback.value = success;
@@ -143,13 +143,11 @@ const handleClick = async (callback) => {
console.error('confirmCallback 执行出错:', error);
}
};
function serchforIt(defaultId) {
function serchforIt() {
if (state.stations.length) {
const ids = defaultId
? defaultId.split(',').map((id) => Number(id))
: userInfo.value.jobTitleId.split(',').map((id) => Number(id));
const ids = userInfo.value.jobTitleId.split(',').map((id) => Number(id));
count.value = ids.length;
state.jobTitleId = defaultId ? defaultId : userInfo.value.jobTitleId;
state.jobTitleId = userInfo.value.jobTitleId;
setCheckedNodes(state.stations, ids);
state.visible = true;
return;
@@ -168,14 +166,14 @@ function serchforIt(defaultId) {
const reset = () => {
maskClick.value = false;
confirmCallback.value = null;
cancelCallback.value = null;
changeCallback.value = null;
listData.value = [];
selectedIndex.value = [0, 0, 0];
rowLabel.value = 'label';
rowKey.value = 'value';
selectedItems.value = [];
JobsIdsValue.value = '';
JobsLabelValue.value = '';
};
// 暴露方法给父组件

View File

@@ -10,7 +10,6 @@
>
<image :src="currentItem == item.id ? item.selectedIconPath : item.iconPath"></image>
</view>
<view class="badge" v-if="item.badge">{{ item.badge }}</view>
<view class="item-bottom" :class="[currentItem == item.id ? 'item-active' : '']">
<text>{{ item.text }}</text>
</view>
@@ -18,93 +17,69 @@
</view>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue';
import { useReadMsg } from '@/stores/useReadMsg';
const props = defineProps({
currentpage: {
type: Number,
required: true,
default: 0,
<script>
export default {
data() {
return {
currentItem: 0,
tabbarList: [
{
id: 0,
text: '首页',
path: '/pages/index/index',
iconPath: '../../static/tabbar/calendar.png',
selectedIconPath: '../../static/tabbar/calendared.png',
centerItem: false,
},
{
id: 1,
text: '精选',
path: '/pages/careerfair/careerfair',
iconPath: '../../static/tabbar/post.png',
selectedIconPath: '../../static/tabbar/posted.png',
centerItem: false,
},
{
id: 2,
text: '消息',
path: '/pages/msglog/msglog',
iconPath: '../../static/tabbar/chat4.png',
selectedIconPath: '../../static/tabbar/chat4ed.png',
centerItem: false,
},
{
id: 3,
text: '我的',
path: '/pages/mine/mine',
iconPath: '../../static/tabbar/mine.png',
selectedIconPath: '../../static/tabbar/mined.png',
centerItem: false,
},
],
};
},
});
const readMsg = useReadMsg();
const currentItem = ref(0);
const tabbarList = computed(() => [
{
id: 0,
text: '首页',
path: '/pages/index/index',
iconPath: '../../static/tabbar/calendar.png',
selectedIconPath: '../../static/tabbar/calendared.png',
centerItem: false,
badge: readMsg.badges[0].count,
props: {
currentpage: {
type: Number,
required: true,
default: 0,
},
},
{
id: 1,
text: '招聘会',
path: '/pages/careerfair/careerfair',
iconPath: '../../static/tabbar/post.png',
selectedIconPath: '../../static/tabbar/posted.png',
centerItem: false,
badge: readMsg.badges[1].count,
mounted() {
this.currentItem = this.currentpage;
uni.hideTabBar();
},
{
id: 2,
text: '',
path: '/pages/chat/chat',
iconPath: '../../static/tabbar/logo3.png',
selectedIconPath: '../../static/tabbar/logo3.png',
centerItem: true,
badge: readMsg.badges[2].count,
methods: {
changeItem(item) {
uni.switchTab({
url: item.path,
});
},
},
{
id: 3,
text: '消息',
path: '/pages/msglog/msglog',
iconPath: '../../static/tabbar/chat4.png',
selectedIconPath: '../../static/tabbar/chat4ed.png',
centerItem: false,
badge: readMsg.badges[3].count,
},
{
id: 4,
text: '我的',
path: '/pages/mine/mine',
iconPath: '../../static/tabbar/mine.png',
selectedIconPath: '../../static/tabbar/mined.png',
centerItem: false,
badge: readMsg.badges[4].count,
},
]);
onMounted(() => {
uni.hideTabBar();
currentItem.value = props.currentpage;
});
const changeItem = (item) => {
uni.switchTab({
url: item.path,
});
};
</script>
<style lang="scss" scoped>
.badge {
position: absolute;
top: 4rpx;
right: 20rpx;
min-width: 30rpx;
height: 30rpx;
background-color: red;
color: #fff;
font-size: 18rpx;
border-radius: 15rpx;
text-align: center;
line-height: 30rpx;
padding: 0 10rpx;
}
.tabbar_container {
background-color: #ffffff;
width: 100%;

View File

@@ -1,8 +1,6 @@
export default {
baseUrl: 'https://fw.rc.qingdao.gov.cn/rgpp-api/api', // 内网
// baseUrl: 'https://qd.zhaopinzao8dian.com/api', // 测试
// baseUrl: "http://192.168.98.110:18181",
// baseUrl: "http://192.168.3.19:8080",
// baseUrl: 'http://39.98.44.136:8080', // 测试
baseUrl: 'https://qd.zhaopinzao8dian.com/api', // 测试
// sseAI+
// StreamBaseURl: 'http://39.98.44.136:8000',
StreamBaseURl: 'https://qd.zhaopinzao8dian.com/ai',
@@ -15,9 +13,9 @@ export default {
// indexedDB
DBversion: 2,
// 只使用本地缓寸的数据
OnlyUseCachedDB: false,
OnlyUseCachedDB: true,
// 使用模拟定位
UsingSimulatedPositioning: true,
UsingSimulatedPositioning: false,
// 应用信息
appInfo: {
// 应用名称
@@ -65,17 +63,5 @@ export default {
experience: 0.3, //经验
salary: 0.5, // 薪资
areas: 0.5 // 区域
},
shareConfig: {
baseUrl: 'https://qd.zhaopinzao8dian.com',
title: '找工作,用 AI 更高效|青岛市智能求职平台',
desc: '融合海量岗位、智能简历匹配、竞争力分析,助你精准锁定理想职位!',
imgUrl: 'https://qd.zhaopinzao8dian.com/file/csn/qd_shareLogo.jpg',
},
sm4Config: {
key: '86C63180C1306ABC4D8F989E0A0BC9F3',
mode: 'ECB', // default
iv: 'UISwD9fW6cFh9SNS', // default is null
cipherType: 'base64' // default is base64
}
}

BIN
hook/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -49,7 +49,7 @@ export function usePagination(
const fetchData = async (type = 'refresh') => {
if (loading.value) return Promise.resolve()
console.log(type)
loading.value = true
error.value = false
@@ -77,17 +77,17 @@ export function usePagination(
}
const params = {
...pageState.search,
[pageField]: pageState.page,
[sizeField]: pageState.pageSize,
...pageState.search
}
try {
const res = await requestFn(params)
const rawData = res[dataKey]
const total = res[totalKey] || 99999999
console.log(total, rawData)
const total = res[totalKey]
const data = typeof transformFn === 'function' ? transformFn(rawData) : rawData
if (type === 'refresh') {
@@ -137,10 +137,10 @@ export function usePagination(
if (autoWatchSearch && isRef(search)) {
watch(search, (newVal) => {
pageState.search = newVal
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
refresh()
}, debounceTime)
// clearTimeout(debounceTimer)
// debounceTimer = setTimeout(() => {
// refresh()
// }, debounceTime)
}, {
deep: true
})

View File

@@ -1,246 +1,387 @@
import {
ref,
onUnmounted
} from 'vue'
import {
$api,
} from 'vue';
} from '../common/globalFunction';
import config from '@/config'
export function useAudioRecorder() {
const isRecording = ref(false)
const isStopping = ref(false)
const isSocketConnected = ref(false)
const recordingDuration = ref(0)
const audioDataForDisplay = ref(new Array(16).fill(0))
const volumeLevel = ref(0)
const recognizedText = ref('')
const lastFinalText = ref('')
let audioStream = null
let audioContext = null
let audioInput = null
let scriptProcessor = null
let websocket = null
let durationTimer = null
const generateUUID = () => {
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11)
.replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
).replace(/-/g, '')
function mergeText(prevText, newText) {
if (newText.startsWith(prevText)) {
return newText; // 直接替换,避免重复拼接
}
return prevText + newText; // 兼容意外情况
}
const fetchWsUrl = async () => {
const res = await $api.createRequest('/app/speech/getToken')
if (res.code !== 200) throw new Error('无法获取语音识别 wsUrl')
const wsUrl = res.msg
return wsUrl
}
export function useAudioRecorder(wsUrl) {
// 状态变量
const isRecording = ref(false);
const isStopping = ref(false);
const isSocketConnected = ref(false);
const recordingDuration = ref(0);
const audioDataForDisplay = ref(new Array(16).fill(0.01));
const volumeLevel = ref(0);
function extractWsParams(wsUrl) {
const url = new URL(wsUrl)
const appkey = url.searchParams.get('appkey')
const token = url.searchParams.get('token')
return {
appkey,
token
// 音频相关
const audioContext = ref(null);
const mediaStream = ref(null);
const workletNode = ref(null);
const analyser = ref(null);
// 网络相关
const socket = ref(null);
// 配置常量
const SAMPLE_RATE = 16000;
const SILENCE_THRESHOLD = 0.02; // 静音阈值 (0-1)
const SILENCE_DURATION = 100; // 静音持续时间(ms)后切片
const MIN_SOUND_DURATION = 200; // 最小有效声音持续时间(ms)
// 音频处理变量
const lastSoundTime = ref(0);
const audioChunks = ref([]);
const currentChunkStartTime = ref(0);
const silenceStartTime = ref(0);
// 语音识别结果
const recognizedText = ref('');
const lastFinalText = ref(''); // 保存最终确认的文本
// AudioWorklet处理器代码
const workletProcessorCode = `
class AudioProcessor extends AudioWorkletProcessor {
constructor(options) {
super();
this.silenceThreshold = options.processorOptions.silenceThreshold;
this.sampleRate = options.processorOptions.sampleRate;
this.samplesPerChunk = Math.floor(this.sampleRate * 0.05); // 50ms的块
this.buffer = new Int16Array(this.samplesPerChunk);
this.index = 0;
this.lastUpdate = 0;
}
calculateVolume(inputs) {
const input = inputs[0];
if (!input || input.length === 0) return 0;
let sum = 0;
const inputChannel = input[0];
for (let i = 0; i < inputChannel.length; i++) {
sum += inputChannel[i] * inputChannel[i];
}
return Math.sqrt(sum / inputChannel.length);
}
process(inputs) {
const now = currentTime;
const volume = this.calculateVolume(inputs);
// 每50ms发送一次分析数据
if (now - this.lastUpdate > 0.05) {
this.lastUpdate = now;
// 简单的频率分析 (模拟16个频段)
const simulatedFreqData = [];
for (let i = 0; i < 16; i++) {
simulatedFreqData.push(
Math.min(1, volume * 10 + (Math.random() * 0.2 - 0.1))
);
}
this.port.postMessage({
type: 'analysis',
volume: volume,
frequencyData: simulatedFreqData,
isSilent: volume < this.silenceThreshold,
timestamp: now
});
}
// 原始音频处理
const input = inputs[0];
if (input && input.length > 0) {
const inputChannel = input[0];
for (let i = 0; i < inputChannel.length; i++) {
this.buffer[this.index++] = Math.max(-32768, Math.min(32767, inputChannel[i] * 32767));
if (this.index >= this.samplesPerChunk) {
this.port.postMessage({
type: 'audio',
audioData: this.buffer.buffer,
timestamp: now
}, [this.buffer.buffer]);
this.buffer = new Int16Array(this.samplesPerChunk);
this.index = 0;
}
}
}
return true;
}
}
registerProcessor('audio-processor', AudioProcessor);
`;
const connectWebSocket = async () => {
const wsUrl = await fetchWsUrl()
const {
appkey,
token
} = extractWsParams(wsUrl)
// 初始化WebSocket连接
const initSocket = (wsUrl) => {
return new Promise((resolve, reject) => {
websocket = new WebSocket(wsUrl)
websocket.binaryType = 'arraybuffer'
socket.value = new WebSocket(wsUrl);
websocket.onopen = () => {
isSocketConnected.value = true
socket.value.onopen = () => {
console.log('open')
isSocketConnected.value = true;
resolve();
};
// 发送 StartTranscription 消息(参考 demo.html
const startTranscriptionMessage = {
header: {
appkey: appkey, // 不影响使用,可留空或由 wsUrl 带入
namespace: 'SpeechTranscriber',
name: 'StartTranscription',
task_id: generateUUID(),
message_id: generateUUID()
},
payload: {
format: 'pcm',
sample_rate: 16000,
enable_intermediate_result: true,
enable_punctuation_prediction: true,
enable_inverse_text_normalization: true
}
}
websocket.send(JSON.stringify(startTranscriptionMessage))
resolve()
}
socket.value.onerror = (error) => {
reject(error);
};
websocket.onerror = (e) => {
isSocketConnected.value = false
reject(e)
}
socket.value.onclose = () => {
isSocketConnected.value = false;
};
websocket.onclose = () => {
isSocketConnected.value = false
}
socket.value.onmessage = handleMessage;
});
};
websocket.onmessage = (e) => {
const msg = JSON.parse(e.data)
const name = msg?.header?.name
const payload = msg?.payload
switch (name) {
case 'TranscriptionResultChanged': {
// 中间识别文本(可选:使用 stash_result.unfixedText 更精确)
const text = payload?.unfixed_result || payload?.result || ''
lastFinalText.value = text
break
}
case 'SentenceBegin': {
// 可选:开始新的一句,重置状态
// console.log('开始新的句子识别')
break
}
case 'SentenceEnd': {
const text = payload?.result || ''
const confidence = payload?.confidence || 0
if (text && confidence > 0.5) {
recognizedText.value += text
lastFinalText.value = ''
// console.log('识别完成:', {
// text,
// confidence
// })
}
break
}
case 'TranscriptionStarted': {
// console.log('识别任务已开始')
break
}
case 'TranscriptionCompleted': {
lastFinalText.value = ''
// console.log('识别全部完成')
break
}
case 'TaskFailed': {
console.error('识别失败:', msg?.header?.status_text)
break
}
default:
console.log('未知消息类型:', name, msg)
break
}
}
})
}
const startRecording = async () => {
if (isRecording.value) return
const handleMessage = (values) => {
try {
recognizedText.value = ''
lastFinalText.value = ''
await connectWebSocket()
audioStream = await navigator.mediaDevices.getUserMedia({
audio: true
})
audioContext = new(window.AudioContext || window.webkitAudioContext)({
sampleRate: 16000
})
audioInput = audioContext.createMediaStreamSource(audioStream)
scriptProcessor = audioContext.createScriptProcessor(2048, 1, 1)
scriptProcessor.onaudioprocess = (event) => {
const input = event.inputBuffer.getChannelData(0)
const pcm = new Int16Array(input.length)
let sum = 0
for (let i = 0; i < input.length; ++i) {
const s = Math.max(-1, Math.min(1, input[i]))
pcm[i] = s * 0x7FFF
sum += s * s
}
volumeLevel.value = Math.sqrt(sum / input.length)
audioDataForDisplay.value = Array(16).fill(volumeLevel.value)
if (websocket?.readyState === WebSocket.OPEN) {
websocket.send(pcm.buffer)
const data = JSON.parse(event.data);
if (data.text) {
const {
asrEnd,
text
} = data
if (asrEnd === 'true') {
recognizedText.value += data.text;
} else {
lastFinalText.value = '';
}
}
audioInput.connect(scriptProcessor)
scriptProcessor.connect(audioContext.destination)
isRecording.value = true
recordingDuration.value = 0
durationTimer = setInterval(() => recordingDuration.value++, 1000)
} catch (err) {
console.error('启动失败:', err)
cleanup()
} catch (error) {
console.error('解析识别结果失败:', error);
}
}
const stopRecording = () => {
if (!isRecording.value || isStopping.value) return
isStopping.value = true
// 处理音频切片
const processAudioChunk = (isSilent) => {
const now = Date.now();
if (websocket?.readyState === WebSocket.OPEN) {
websocket.send(JSON.stringify({
header: {
namespace: 'SpeechTranscriber',
name: 'StopTranscription',
message_id: generateUUID()
if (!isSilent) {
// 检测到声音
lastSoundTime.value = now;
if (silenceStartTime.value > 0) {
// 从静音恢复到有声音
silenceStartTime.value = 0;
}
} else {
// 静音状态
if (silenceStartTime.value === 0) {
silenceStartTime.value = now;
}
// 检查是否达到静音切片条件
if (now - silenceStartTime.value >= SILENCE_DURATION &&
now - currentChunkStartTime.value >= MIN_SOUND_DURATION) {
sendCurrentChunk();
}
}
};
// 发送当前音频块
const sendCurrentChunk = () => {
if (audioChunks.value.length === 0 || !socket.value || socket.value.readyState !== WebSocket.OPEN) {
return;
}
try {
// 合并所有块
const totalBytes = audioChunks.value.reduce((total, chunk) => total + chunk.byteLength, 0);
const combined = new Int16Array(totalBytes / 2);
let offset = 0;
audioChunks.value.forEach(chunk => {
const samples = new Int16Array(chunk);
combined.set(samples, offset);
offset += samples.length;
});
// 发送合并后的数据
socket.value.send(combined.buffer);
audioChunks.value = [];
// 记录新块的开始时间
currentChunkStartTime.value = Date.now();
silenceStartTime.value = 0;
} catch (error) {
console.error('发送音频数据时出错:', error);
}
};
// 开始录音
const startRecording = async () => {
if (isRecording.value) return;
try {
// 重置状态
recognizedText.value = '';
lastFinalText.value = '';
// 重置状态
recordingDuration.value = 0;
audioChunks.value = [];
lastSoundTime.value = 0;
currentChunkStartTime.value = Date.now();
silenceStartTime.value = 0;
// 初始化WebSocket
await initSocket(wsUrl);
// 获取音频流
mediaStream.value = await navigator.mediaDevices.getUserMedia({
audio: {
sampleRate: SAMPLE_RATE,
channelCount: 1,
echoCancellation: true,
noiseSuppression: true,
autoGainControl: false
},
video: false
});
// 创建音频上下文
audioContext.value = new(window.AudioContext || window.webkitAudioContext)({
sampleRate: SAMPLE_RATE
});
// 注册AudioWorklet
const blob = new Blob([workletProcessorCode], {
type: 'application/javascript'
});
const workletUrl = URL.createObjectURL(blob);
await audioContext.value.audioWorklet.addModule(workletUrl);
URL.revokeObjectURL(workletUrl);
// 创建AudioWorkletNode
workletNode.value = new AudioWorkletNode(audioContext.value, 'audio-processor', {
processorOptions: {
silenceThreshold: SILENCE_THRESHOLD,
sampleRate: SAMPLE_RATE
}
}))
websocket.close()
});
// 处理音频数据
workletNode.value.port.onmessage = (e) => {
if (e.data.type === 'audio') {
audioChunks.value.push(e.data.audioData);
} else if (e.data.type === 'analysis') {
audioDataForDisplay.value = e.data.frequencyData;
volumeLevel.value = e.data.volume;
processAudioChunk(e.data.isSilent);
}
};
// 连接音频节点
const source = audioContext.value.createMediaStreamSource(mediaStream.value);
source.connect(workletNode.value);
workletNode.value.connect(audioContext.value.destination);
isRecording.value = true;
} catch (error) {
console.error('启动录音失败:', error);
cleanup();
throw error;
}
};
// 停止录音
const stopRecording = async () => {
if (!isRecording.value || isStopping.value) return;
isStopping.value = true;
try {
// 发送最后一个音频块(无论是否静音)
sendCurrentChunk();
// 发送结束标记
if (socket.value?.readyState === WebSocket.OPEN) {
socket.value.send(JSON.stringify({
action: 'end',
duration: recordingDuration.value
}));
await new Promise(resolve => {
if (socket.value.bufferedAmount === 0) {
resolve();
} else {
const timer = setInterval(() => {
if (socket.value.bufferedAmount === 0) {
clearInterval(timer);
resolve();
}
}, 50);
}
});
socket.value.close();
}
cleanup();
} catch (error) {
console.error('停止录音时出错:', error);
throw error;
} finally {
isStopping.value = false;
}
};
// 清理资源
const cleanup = () => {
if (mediaStream.value) {
mediaStream.value.getTracks().forEach(track => track.stop());
mediaStream.value = null;
}
cleanup()
isStopping.value = false
}
if (workletNode.value) {
workletNode.value.disconnect();
workletNode.value = null;
}
const cancelRecording = () => {
if (!isRecording.value || isStopping.value) return
isStopping.value = true
websocket?.close()
cleanup()
isStopping.value = false
}
if (audioContext.value && audioContext.value.state !== 'closed') {
audioContext.value.close();
audioContext.value = null;
}
const cleanup = () => {
clearInterval(durationTimer)
audioChunks.value = [];
isRecording.value = false;
isSocketConnected.value = false;
};
scriptProcessor?.disconnect()
audioInput?.disconnect()
audioStream?.getTracks().forEach(track => track.stop())
audioContext?.close()
audioStream = null
audioContext = null
audioInput = null
scriptProcessor = null
websocket = null
isRecording.value = false
isSocketConnected.value = false
}
/// 取消录音
const cancelRecording = async () => {
if (!isRecording.value || isStopping.value) return;
isStopping.value = true;
try {
if (socket.value?.readyState === WebSocket.OPEN) {
console.log('发送结束标记...');
socket.value.send(JSON.stringify({
action: 'cancel'
}));
socket.value.close();
}
cleanup()
} catch (error) {
console.error('取消录音时出错:', error);
throw error;
} finally {
isStopping.value = false;
}
};
onUnmounted(() => {
if (isRecording.value) stopRecording()
})
if (isRecording.value) {
stopRecording();
}
});
return {
isRecording,
@@ -249,10 +390,10 @@ export function useAudioRecorder() {
recordingDuration,
audioDataForDisplay,
volumeLevel,
recognizedText,
lastFinalText,
startRecording,
stopRecording,
recognizedText,
lastFinalText,
cancelRecording
}
};
}

136
hook/useSpeechReader.js Normal file
View File

@@ -0,0 +1,136 @@
import {
ref,
onBeforeUnmount,
onMounted
} from 'vue'
import {
onHide,
onUnload
} from '@dcloudio/uni-app'
export function useSpeechReader() {
const isSpeaking = ref(false)
const isPaused = ref(false)
let utterance = null
const cleanMarkdown = (text) => {
return formatTextForSpeech(text)
}
const speak = (text, options = {
lang: 'zh-CN',
rate: 0.9,
pitch: 1.2
}) => {
cancelAudio() // 重置之前的
// const voices = speechSynthesis.getVoices()
// const chineseVoices = voices.filter(v => v.lang.includes('zh'))
const speechText = extractSpeechText(text);
utterance = new SpeechSynthesisUtterance(speechText)
// utterance.lang = options.lang || 'zh'
utterance.rate = options.rate || 1
utterance.pitch = options.pitch || 1.1 // 音调0 - 2偏高比较柔和
utterance.onend = () => {
isSpeaking.value = false
isPaused.value = false
}
speechSynthesis.speak(utterance)
isSpeaking.value = true
isPaused.value = false
}
const pause = () => {
if (isSpeaking.value && !isPaused.value) {
speechSynthesis.pause()
isPaused.value = true
}
}
const resume = () => {
if (isSpeaking.value && isPaused.value) {
speechSynthesis.resume()
isPaused.value = false
}
}
const cancelAudio = () => {
speechSynthesis.cancel()
isSpeaking.value = false
isPaused.value = false
}
// 页面刷新/关闭时
onMounted(() => {
if (typeof window !== 'undefined') {
window.addEventListener('beforeunload', cancelAudio)
}
})
onBeforeUnmount(() => {
cancelAudio()
if (typeof window !== 'undefined') {
window.removeEventListener('beforeunload', cancelAudio)
}
})
onHide(cancelAudio)
onUnload(cancelAudio)
return {
speak,
pause,
resume,
cancelAudio,
isSpeaking,
isPaused,
}
}
function extractSpeechText(markdown) {
const jobRegex = /``` job-json\s*({[\s\S]*?})\s*```/g;
const jobs = [];
let match;
let lastJobEndIndex = 0;
let firstJobStartIndex = -1;
// 提取岗位 json 数据及前后位置
while ((match = jobRegex.exec(markdown)) !== null) {
const jobStr = match[1];
try {
const job = JSON.parse(jobStr);
jobs.push(job);
if (firstJobStartIndex === -1) {
firstJobStartIndex = match.index;
}
lastJobEndIndex = jobRegex.lastIndex;
} catch (e) {
console.warn('JSON 解析失败', e);
}
}
// 提取引导语(第一个 job-json 之前的文字)
const guideText = firstJobStartIndex > 0 ?
markdown.slice(0, firstJobStartIndex).trim() :
'';
// 提取结束语(最后一个 job-json 之后的文字)
const endingText = lastJobEndIndex < markdown.length ?
markdown.slice(lastJobEndIndex).trim() :
'';
// 岗位信息格式化为语音文本
const jobTexts = jobs.map((job, index) => {
return `${index + 1} 个岗位,岗位名称是:${job.jobTitle},公司是:${job.companyName},薪资:${job.salary},地点:${job.location},学历要求:${job.education},经验要求:${job.experience}`;
});
// 拼接总语音内容
const finalTextParts = [];
if (guideText) finalTextParts.push(guideText);
finalTextParts.push(...jobTexts);
if (endingText) finalTextParts.push(endingText);
return finalTextParts.join('\n');
}

View File

@@ -1,158 +0,0 @@
import {
ref,
onUnmounted,
readonly
} from 'vue';
const defaultExtractSpeechText = (text) => text;
export function useTTSPlayer() {
const synth = window.speechSynthesis;
const isSpeaking = ref(false);
const isPaused = ref(false);
const utteranceRef = ref(null);
const cleanup = () => {
isSpeaking.value = false;
isPaused.value = false;
utteranceRef.value = null;
};
/**
* @param {string} text - The text to be spoken.
* @param {object} [options] - Optional settings for the speech.
* @param {string} [options.lang] - Language (e.g., 'en-US', 'es-ES').
* @param {number} [options.rate] - Speed (0.1 to 10, default 1).
* @param {number} [options.pitch] - Pitch (0 to 2, default 1).
* @param {SpeechSynthesisVoice} [options.voice] - A specific voice object.
* @param {function(string): string} [options.extractSpeechText] - A function to filter/clean the text before speaking.
*/
const speak = (text, options = {}) => {
if (!synth) {
console.error('SpeechSynthesis API is not supported in this browser.');
return;
}
if (isSpeaking.value) {
synth.cancel();
}
const filteredText = extractSpeechText(text);
if (!filteredText || typeof filteredText !== 'string' || filteredText.trim() === '') {
console.warn('Text to speak is empty after filtering.');
cleanup(); // Ensure state is clean
return;
}
const newUtterance = new SpeechSynthesisUtterance(filteredText); // Use filtered text
utteranceRef.value = newUtterance;
newUtterance.lang = 'zh-CN';
newUtterance.rate = options.rate || 1;
newUtterance.pitch = options.pitch || 1;
if (options.voice) {
newUtterance.voice = options.voice;
}
newUtterance.onstart = () => {
isSpeaking.value = true;
isPaused.value = false;
};
newUtterance.onpause = () => {
isPaused.value = true;
};
newUtterance.onresume = () => {
isPaused.value = false;
};
newUtterance.onend = () => {
cleanup();
};
newUtterance.onerror = (event) => {
console.error('SpeechSynthesis Error:', event.error);
cleanup();
};
synth.speak(newUtterance);
};
const pause = () => {
if (synth && isSpeaking.value && !isPaused.value) {
synth.pause();
}
};
const resume = () => {
if (synth && isPaused.value) {
synth.resume();
}
};
const cancelAudio = () => {
if (synth) {
synth.cancel();
}
cleanup();
};
onUnmounted(() => {
cancelAudio();
});
return {
speak,
pause,
resume,
cancelAudio,
isSpeaking: readonly(isSpeaking),
isPaused: readonly(isPaused),
};
}
function extractSpeechText(markdown) {
const jobRegex = /``` job-json\s*({[\s\S]*?})\s*```/g;
const jobs = [];
let match;
let lastJobEndIndex = 0;
let firstJobStartIndex = -1;
// 提取岗位 json 数据及前后位置
while ((match = jobRegex.exec(markdown)) !== null) {
const jobStr = match[1];
try {
const job = JSON.parse(jobStr);
jobs.push(job);
if (firstJobStartIndex === -1) {
firstJobStartIndex = match.index;
}
lastJobEndIndex = jobRegex.lastIndex;
} catch (e) {
console.warn('JSON 解析失败', e);
}
}
// 提取引导语(第一个 job-json 之前的文字)
const guideText = firstJobStartIndex > 0 ?
markdown.slice(0, firstJobStartIndex).trim() :
'';
// 提取结束语(最后一个 job-json 之后的文字)
const endingText = lastJobEndIndex < markdown.length ?
markdown.slice(lastJobEndIndex).trim() :
'';
// 岗位信息格式化为语音文本
const jobTexts = jobs.map((job, index) => {
return `${index + 1} 个岗位,岗位名称是:${job.jobTitle},公司是:${job.companyName},薪资:${job.salary},地点:${job.location},学历要求:${job.education},经验要求:${job.experience}`;
});
// 拼接总语音内容
const finalTextParts = [];
if (guideText) finalTextParts.push(guideText);
finalTextParts.push(...jobTexts);
if (endingText) finalTextParts.push(endingText);
return finalTextParts.join('\n');
}

View File

@@ -1,203 +0,0 @@
import {
ref,
readonly,
onUnmounted
} from 'vue';
// 检查 API 兼容性
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const isApiSupported = !!SpeechRecognition && !!navigator.mediaDevices && !!window.AudioContext;
/**
* @param {object} [options]
* @param {string} [options.lang] - Language code (e.g., 'zh-CN', 'en-US')
* @returns {object}
*/
export function useAudioRecorder(options = {}) {
const lang = options.lang || 'zh-CN'; // 默认使用中文
const isRecording = ref(false);
const recognizedText = ref(''); // 完整的识别文本(包含临时的)
const lastFinalText = ref(''); // 最后一段已确定的文本
const volumeLevel = ref(0); // 音量 (0-100)
const audioDataForDisplay = ref(new Uint8Array()); // 波形数据
let recognition = null;
let audioContext = null;
let analyser = null;
let mediaStreamSource = null;
let mediaStream = null;
let dataArray = null; // 用于音量和波形
let animationFrameId = null;
if (!isApiSupported) {
console.warn(
'此浏览器不支持Web语音API或Web音频API。钩子无法正常工作。'
);
return {
isRecording: readonly(isRecording),
startRecording: () => console.error('Audio recording not supported.'),
stopRecording: () => {},
cancelRecording: () => {},
audioDataForDisplay: readonly(audioDataForDisplay),
volumeLevel: readonly(volumeLevel),
recognizedText: readonly(recognizedText),
lastFinalText: readonly(lastFinalText),
};
}
const setupRecognition = () => {
recognition = new SpeechRecognition();
recognition.lang = lang;
recognition.continuous = true; // 持续识别
recognition.interimResults = true; // 返回临时结果
recognition.onstart = () => {
isRecording.value = true;
};
recognition.onend = () => {
isRecording.value = false;
stopAudioAnalysis(); // 语音识别停止时,也停止音频分析
};
recognition.onerror = (event) => {
console.error('SpeechRecognition Error:', event.error);
isRecording.value = false;
stopAudioAnalysis();
};
recognition.onresult = (event) => {
let interim = '';
let final = '';
for (let i = 0; i < event.results.length; i++) {
const transcript = event.results[i][0].transcript;
if (event.results[i].isFinal) {
final += transcript;
lastFinalText.value = transcript; // 存储最后一段确定的文本
} else {
interim += transcript;
}
}
recognizedText.value = final + interim; // 组合为完整文本
};
};
const startAudioAnalysis = async () => {
try {
mediaStream = await navigator.mediaDevices.getUserMedia({
audio: true
});
audioContext = new AudioContext();
analyser = audioContext.createAnalyser();
mediaStreamSource = audioContext.createMediaStreamSource(mediaStream);
// 设置 Analyser
analyser.fftSize = 512; // 必须是 2 的幂
const bufferLength = analyser.frequencyBinCount;
dataArray = new Uint8Array(bufferLength); // 用于波形
// 连接节点
mediaStreamSource.connect(analyser);
// 开始循环分析
updateAudioData();
} catch (err) {
console.error('Failed to get media stream or setup AudioContext:', err);
if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') {
alert('麦克风权限被拒绝。请在浏览器设置中允许访问麦克风。');
}
}
};
const updateAudioData = () => {
if (!isRecording.value) return; // 如果停止了就退出循环
// 获取时域数据 (波形)
analyser.getByteTimeDomainData(dataArray);
audioDataForDisplay.value = new Uint8Array(dataArray); // 复制数组以触发响应式
// 计算音量 (RMS)
let sumSquares = 0.0;
for (const amplitude of dataArray) {
const normalized = (amplitude / 128.0) - 1.0; // 转换为 -1.0 到 1.0
sumSquares += normalized * normalized;
}
const rms = Math.sqrt(sumSquares / dataArray.length);
volumeLevel.value = Math.min(100, Math.floor(rms * 250)); // 放大 RMS 值到 0-100 范围
animationFrameId = requestAnimationFrame(updateAudioData);
};
const stopAudioAnalysis = () => {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
// 停止麦克风轨道
mediaStream?.getTracks().forEach((track) => track.stop());
// 关闭 AudioContext
audioContext?.close().catch((e) => console.error('Error closing AudioContext', e));
mediaStream = null;
audioContext = null;
analyser = null;
mediaStreamSource = null;
volumeLevel.value = 0;
audioDataForDisplay.value = new Uint8Array();
};
const startRecording = async () => {
if (isRecording.value) return;
// 重置状态
recognizedText.value = '';
lastFinalText.value = '';
try {
// 必须先启动音频分析以获取麦克风权限
await startAudioAnalysis();
// 如果音频启动成功 (mediaStream 存在),则启动语音识别
if (mediaStream) {
setupRecognition();
recognition.start();
}
} catch (error) {
console.error("Error starting recording:", error);
}
};
const stopRecording = () => {
if (!isRecording.value || !recognition) return;
recognition.stop(); // 这将触发 onend 事件,自动停止音频分析
};
const cancelRecording = () => {
if (!recognition) return;
isRecording.value = false; // 立即设置状态
recognition.abort(); // 这也会触发 onend
recognizedText.value = '';
lastFinalText.value = '';
};
onUnmounted(() => {
if (recognition) {
recognition.abort();
}
stopAudioAnalysis();
});
return {
isRecording: readonly(isRecording),
startRecording,
stopRecording,
cancelRecording,
audioDataForDisplay: readonly(audioDataForDisplay),
volumeLevel: readonly(volumeLevel),
recognizedText: readonly(recognizedText),
lastFinalText: readonly(lastFinalText),
isApiSupported, // 导出支持状态
};
}

View File

@@ -17,23 +17,15 @@
})();
</script>
<title></title>
<!-- eruda -->
<script src="https://cdn.jsdelivr.net/npm/eruda"></script>
<!-- vconsole -->
<!-- <script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>
eruda.init();
</script>
<!-- 爱山东jssdk 本sdk存在性能问题 -->
<script type="text/javascript" src="https://isdapp.shandong.gov.cn/jmopen/jssdk/index.js"></script>
<script type="module" src="./static/js/sm4.min.js"></script>
<script type="module" src="./static/js/SM.js"></script>
<script type="module" src="./static/js/aes.js"></script>
var vConsole = new window.VConsole();
vConsole.destroy();
</script> -->
</head>
<!-- <body> -->
<div id="app"><!--app-html--></div>
<script type="module" src="/main.js"></script>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/main.js"></script>
</body>
</html>

BIN
lib/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -80,7 +80,7 @@
"locale": "zh-Hans",
"h5": {
"router": {
"base": "./",
"base": "/app/",
"mode": "hash"
},
"title": "青岛智慧就业服务",
@@ -97,9 +97,6 @@
"serviceHost": ""
}
}
},
"devServer": {
"https": false
}
}
}

View File

@@ -1,7 +1,7 @@
<template>
<AppLayout title="" :use-scroll-view="false">
<template #headerleft>
<view class="btnback">
<view class="btn">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
</template>
@@ -143,16 +143,12 @@ function expand() {
</script>
<style lang="stylus" scoped>
.btnback{
width: 64rpx;
height: 64rpx;
}
.btn {
display: flex;
justify-content: space-between;
align-items: center;
width: 52rpx;
height: 52rpx;
width: 60rpx;
height: 60rpx;
}
image {
height: 100%;

View File

@@ -1,7 +1,7 @@
<template>
<AppLayout title="我的浏览" :show-bg-image="false" :use-scroll-view="false">
<template #headerleft>
<view class="btnback">
<view class="btn">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
</template>
@@ -152,16 +152,12 @@ function getPreviousDay(dateStr) {
</script>
<style lang="stylus" scoped>
.btnback{
width: 64rpx;
height: 64rpx;
}
.btn {
display: flex;
justify-content: space-between;
align-items: center;
width: 52rpx;
height: 52rpx;
width: 60rpx;
height: 60rpx;
}
image {
height: 100%;

View File

@@ -1,7 +1,7 @@
<template>
<AppLayout :title="title" :show-bg-image="false" @onScrollBottom="getDataList('add')">
<template #headerleft>
<view class="btnback">
<view class="btn">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
</template>
@@ -101,16 +101,12 @@ function getDataList(type = 'add') {
</script>
<style lang="stylus" scoped>
.btnback{
width: 64rpx;
height: 64rpx;
}
.btn {
display: flex;
justify-content: space-between;
align-items: center;
width: 52rpx;
height: 52rpx;
width: 60rpx;
height: 60rpx;
}
image {
height: 100%;

View File

@@ -1,7 +1,7 @@
<template>
<AppLayout title="" :use-scroll-view="false">
<template #headerleft>
<view class="btnback">
<view class="btn">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
</template>
@@ -219,16 +219,12 @@ function getHoursBetween(startTimeStr, endTimeStr) {
</script>
<style lang="stylus" scoped>
.btnback{
width: 64rpx;
height: 64rpx;
}
.btn {
display: flex;
justify-content: space-between;
align-items: center;
width: 52rpx;
height: 52rpx;
width: 60rpx;
height: 60rpx;
}
image {
height: 100%;

View File

@@ -145,17 +145,10 @@ function changeArea() {
function changeJobs() {
selectJobsModel.value?.open({
title: '添加岗位',
defaultId: fromValue.jobTitleId,
success: (ids, labels) => {
console.log(ids, labels);
fromValue.jobTitleId = ids;
state.jobsText = labels.split(',');
},
cancel: (ids, labels) => {
console.log(ids, labels);
// fromValue.jobTitleId = ids;
// state.jobsText = labels.split(',');
},
});
}
@@ -206,9 +199,6 @@ function getFormCompletionPercent(form) {
height: 80rpx;
border-bottom: 2rpx solid #EBEBEB
position: relative;
.triangle {
pointer-events: none;
}
.triangle::before
position: absolute;
right: 20rpx;
@@ -272,9 +262,10 @@ function getFormCompletionPercent(form) {
display: flex
flex-wrap: wrap
.nx-item
margin: 12rpx 12rpx 0 0;
padding: 12rpx 25rpx;
padding: 20rpx 28rpx
width: fit-content
border-radius: 12rpx 12rpx 12rpx 12rpx;
border: 2rpx solid #E8EAEE;
margin-right: 24rpx
margin-top: 24rpx
</style>

View File

@@ -1,73 +0,0 @@
<template>
<view class="collection-content">
<renderJobs :list="list" :longitude="longitudeVal" :latitude="latitudeVal"></renderJobs>
<loadmore ref="loadmoreRef"></loadmore>
</view>
</template>
<script setup>
import dictLabel from '@/components/dict-Label/dict-Label.vue';
import { reactive, inject, watch, ref, onMounted } from 'vue';
import { onLoad, onShow, onReachBottom } from '@dcloudio/uni-app';
import useUserStore from '@/stores/useUserStore';
const { $api, navTo, navBack, vacanciesTo } = inject('globalFunction');
import { storeToRefs } from 'pinia';
import useLocationStore from '@/stores/useLocationStore';
import { usePagination } from '@/hook/usePagination';
import { jobMoreMap } from '@/utils/markdownParser';
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
const loadmoreRef = ref(null);
// 响应式搜索条件(可以被修改)
const searchParams = ref({});
const pageSize = ref(10);
const { list, loading, refresh, loadMore } = usePagination(
(params) => $api.createRequest('/app/job/list', params, 'GET', true),
null, // 转换函数
{
pageSize: pageSize,
search: searchParams,
autoWatchSearch: true,
onBeforeRequest: () => {
loadmoreRef.value?.change('loading');
},
onAfterRequest: () => {
loadmoreRef.value?.change('more');
},
}
);
onLoad((options) => {
let params = jobMoreMap.get(options.jobId);
if (params) {
uni.setStorageSync('jobMoreMap', params);
} else {
params = uni.getStorageSync('jobMoreMap');
}
const objs = removeNullProperties(params);
searchParams.value = objs;
refresh();
});
function removeNullProperties(obj) {
for (const key in obj) {
if (obj.hasOwnProperty(key) && obj[key] === null) {
delete obj[key]; // 删除值为 null 的属性
}
}
return obj;
}
onReachBottom(() => {
loadMore();
});
</script>
<style lang="stylus" scoped>
.collection-content{
padding: 1rpx 28rpx 20rpx 28rpx;
background: #F4F4F4;
height: 100%
min-height: calc(100vh - var(--window-top) - var(--status-bar-height) - var(--window-bottom));
}
</style>

View File

@@ -1,161 +1,99 @@
<template>
<AppLayout title="我的简历" title-color="#FFFFFF" back-gorund-color="#F4F4F4">
<template #headerleft>
<view class="btn">
<image src="@/static/icon/back-white.png" @click="navBack"></image>
</view>
</template>
<view class="mys-container">
<!-- 个人信息 -->
<view class="card-top" style="margin-top: 12rpx; padding: 0; background: none">
<!-- 个人信息 -->
<view class="mys-tops btn-feel">
<view class="tops-left">
<view class="name">
<text>{{ userInfo.name || "编辑用户名" }}</text>
<view class="edit-icon mar_le10">
<image class="button-click" src="@/static/icon/edit1.png" @click="navTo('/packageA/pages/personalInfo/personalInfo')"></image>
</view>
<view class="tops-left">
<view class="name">
<text>{{ userInfo.name || '编辑用户名' }}</text>
<view class="edit-icon mar_le10">
<image
class="button-click"
src="@/static/icon/edit1.png"
@click="navTo('/packageA/pages/personalInfo/personalInfo')"
></image>
</view>
</view>
<view class="subName">
<dict-Label class="mar_ri10" dictType="sex" :value="userInfo.sex"></dict-Label>
<text class="mar_ri10">{{ userInfo.age }}</text>
<dict-Label class="mar_ri10" dictType="education" :value="userInfo.education"></dict-Label>
<dict-Label
class="mar_ri10"
dictType="affiliation"
:value="userInfo.politicalAffiliation"
></dict-Label>
</view>
<view class="subName">{{ userInfo.phone }}</view>
</view>
<view class="subName">
<dict-Label class="mar_ri10" dictType="sex" :value="userInfo.sex"></dict-Label>
<text class="mar_ri10">{{ userInfo.age }}</text>
<dict-Label class="mar_ri10" dictType="education" :value="userInfo.education"></dict-Label>
<dict-Label class="mar_ri10" dictType="affiliation" :value="userInfo.politicalAffiliation"></dict-Label>
<view class="tops-right">
<view class="right-imghead">
<image v-if="userInfo.sex === '0'" src="@/static/icon/boy.png"></image>
<image v-else src="@/static/icon/girl.png"></image>
</view>
<view class="right-sex">
<image v-if="userInfo.sex === '0'" src="@/static/icon/boy1.png"></image>
<image v-else src="@/static/icon/girl1.png"></image>
</view>
</view>
<view class="subName">{{ userInfo.phone }}</view>
</view>
<view class="tops-right">
<view class="right-imghead">
<image v-if="userInfo.avatar" :src="userInfo.avatar"></image>
<image v-else-if="userInfo.sex == '0'" src="@/static/icon/boy.png"></image>
<image v-else src="@/static/icon/girl.png"></image>
</view>
<view class="right-sex">
<image v-if="userInfo.sex === '0'" src="@/static/icon/boy1.png"></image>
<image v-else src="@/static/icon/girl1.png"></image>
</view>
</view>
</view>
<!-- 求职期望 -->
<view class="mys-line">
<view class="line"></view>
</view>
<view class="mys-info btn-feel">
<view class="mys-h4">
<view>求职期望</view>
<image class="icon" src="@/static/icon/edit1.png" @click="navTo('/packageA/pages/jobExpect/jobExpect')"></image>
</view>
<view class="mys-text">
<text>期望薪资</text>
<text>{{ userInfo.salaryMin / 1000 }}k-{{ userInfo.salaryMax / 1000 }}k</text>
</view>
<view class="mys-text">
<text>期望工作地</text>
<text>青岛市-</text>
<dict-Label dictType="area" :value="Number(userInfo.area)"></dict-Label>
</view>
<view class="mys-list">
<view class="cards button-click" v-for="(title, index) in userInfo.jobTitle" :key="index">
{{ title }}
</view>
</view>
</view>
</view>
<view class="card-top" style="margin-top: 24rpx">
<view class="mys-info" style="padding: 0">
<view class="mys-h4 btn-feel">
<text>工作经历</text>
<view class="mys-edit-icon btn-feel" @click="navTo('/packageA/pages/workExp/workExp')">
<image class="icon button-click btn-feel" src="@/static/icon/plus.png"></image>
<view class="txt">添加</view>
</view>
</view>
<view class="exp-item btn-feel" v-for="item in userInfo.workExp" :key="item.id">
<view class="fl_box fl_justbet mar_top15">
<view class="fs_16">{{ item.company }}</view>
<image class="icon btn-feel" src="@/static/icon/edit1.png" @click="navTo(`/packageA/pages/workExp/workExp?id=${item.id}`)"></image>
</view>
<view class="mys-text fl_box fl_justbet">
<text class="color_333333 fs_14">{{ item.position }}</text>
<text class="datetext">{{ item.startTime }}--{{ item.endTime || "至今" }}</text>
<view class="mys-line"></view>
<view class="mys-info">
<view class="mys-h4">
<text>求职期望</text>
<view class="mys-edit-icon">
<image
class="button-click"
src="@/static/icon/edit1.png"
@click="navTo('/packageA/pages/jobExpect/jobExpect')"
></image>
</view>
</view>
<view class="mys-text">
<text>{{ item.duty }}</text>
<text>期望薪资</text>
<text>{{ userInfo.salaryMin / 1000 }}k-{{ userInfo.salaryMax / 1000 }}k</text>
</view>
<view class="mys-text">
<text>期望工作地</text>
<text>青岛市-</text>
<dict-Label dictType="area" :value="Number(userInfo.area)"></dict-Label>
</view>
<view class="mys-list">
<view class="cards button-click" v-for="(title, index) in userInfo.jobTitle" :key="index">
{{ title }}
</view>
</view>
</view>
</view>
</view>
</view>
<template #footer>
<view class="footer-container">
<view class="footer-button btn-feel">上传简历</view>
</view>
</template>
</AppLayout>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted, computed } from "vue";
const { $api, navTo, navBack } = inject("globalFunction");
import { onLoad, onShow } from "@dcloudio/uni-app";
import { storeToRefs } from "pinia";
import useUserStore from "@/stores/useUserStore";
import useDictStore from "@/stores/useDictStore";
import { reactive, inject, watch, ref, onMounted, computed } from 'vue';
const { $api, navTo } = inject('globalFunction');
import { onLoad, onShow } from '@dcloudio/uni-app';
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
import useDictStore from '@/stores/useDictStore';
const { userInfo } = storeToRefs(useUserStore());
const { getUserResume } = useUserStore();
const { getDictData, oneDictData } = useDictStore();
</script>
<style lang="stylus" scoped>
.btn {
display: flex;
justify-content: space-between;
align-items: center;
width: 60rpx;
height: 60rpx;
image {
height: 100%;
width: 100%;
}
}
.footer-container{
background: #FFFFFF;
box-shadow: 0rpx -4rpx 24rpx 0rpx rgba(11,44,112,0.12);
border-radius: 0rpx 0rpx 0rpx 0rpx;
padding: 40rpx 28rpx 20rpx 28rpx
.footer-button{
width: 100%;
height: 90rpx;
background: #1677FF;
border-radius: 8rpx;
color: #FFFFFF;
line-height: 90rpx;
text-align: center;
}
}
image{
width: 100%;
height: 100%
}
.mys-container{
padding-bottom:20rpx;
.card-top{
background: #FFFFFF;
margin: 0 28rpx;
border-radius: 8rpx;
padding: 24rpx
}
.mys-tops{
display: flex
justify-content: space-between
padding: 38rpx 44rpx
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.8) , rgba(255, 255, 255, 1));
border-radius: 8rpx 8rpx 0 0 ;
padding: 52rpx 48rpx
.tops-left{
.name{
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 600;
font-size: 36rpx;
font-size: 44rpx;
color: #333333;
display: flex
align-items: center
@@ -170,8 +108,8 @@ image{
.subName{
margin-top: 12rpx
font-weight: 400;
font-size: 26rpx;
color: #999999;
font-size: 32rpx;
color: #333333;
}
}
@@ -194,55 +132,33 @@ image{
}
}
.mys-line{
background: #ffffff;
padding: 0 24rpx
.line{
border: 2rpx dashed #eeeeee;
}
margin: 0 28rpx
height: 0rpx;
border-radius: 0rpx 0rpx 0rpx 0rpx;
border: 2rpx dashed #000000;
opacity: 0.16;
}
.mys-info{
padding: 28rpx
background: #ffffff;
border-radius: 0 0 8rpx 8rpx ;
.mys-h4{
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 500;
font-size: 30rpx;
font-weight: 600;
font-size: 32rpx;
color: #000000;
margin-bottom: 8rpx
display: flex;
justify-content: space-between
align-items: center
.icon{
.mys-edit-icon{
display: inline-block
width: 40rpx;
height: 40rpx
}
.mys-edit-icon{
display: flex;
align-items: center;
.txt{
font-size: 26rpx;
color: #444;
font-weight: 400;
}
.icon{
width: 28rpx;
height: 28rpx
margin-right: 5rpx;
margin-top: 2rpx
vertical-align: bottom;
}
}
}
.datetext{
font-weight: 400;
font-size: 26rpx;
color: #999999;
}
.mys-text{
font-weight: 400;
font-size: 26rpx;
color: #999999;
font-size: 28rpx;
color: #333333;
margin-top: 16rpx
}
.mys-list{
@@ -250,27 +166,17 @@ image{
align-items: center
flex-wrap: wrap;
.cards{
margin: 12rpx 12rpx 0 0
padding: 12rpx 25rpx;
margin: 28rpx 28rpx 0 0
height: 80rpx;
padding: 0 38rpx;
width: fit-content
display: flex
align-items: center
justify-content: center
border-radius:10rpx;
border-radius: 12rpx 12rpx 12rpx 12rpx;
border: 2rpx solid #E8EAEE;
}
}
.exp-item{
padding-bottom: 28rpx;
border-bottom: 2rpx dashed #EEEEEE;
.icon{
width 40rpx;
height 40rpx
}
}
.exp-item:nth-last-child(1){
border-bottom: none;
}
}
}
</style>

View File

@@ -1,267 +1,257 @@
<template>
<AppLayout title="个人信息" :sub-title="`完成度${percent}`" border back-gorund-color="#ffffff" :show-bg-image="false">
<template #headerleft>
<view class="btn mar_le20 button-click" @click="navBack">取消</view>
</template>
<template #headerright>
<view class="btn mar_ri20 button-click" @click="confirm">确认</view>
</template>
<view class="content">
<view class="content-avatar">
<view class="avatar-title">编辑头像</view>
<view @click="selectAvatar">
<image class="avatar" v-if="fromValue.avatar" :src="fromValue.avatar" />
<image class="avatar" v-else-if="fromValue.sex == '0'" src="@/static/icon/boy.png" />
<image class="avatar" v-else="fromValue.sex == '1'" src="@/static/icon/girl.png" />
<AppLayout
title="个人信息"
:sub-title="`完成度${percent}`"
border
back-gorund-color="#ffffff"
:show-bg-image="false"
>
<template #headerleft>
<view class="btn mar_le20 button-click" @click="navBack">取消</view>
</template>
<template #headerright>
<view class="btn mar_ri20 button-click" @click="confirm">确认</view>
</template>
<view class="content">
<view class="content-input">
<view class="input-titile">姓名</view>
<input class="input-con" v-model="fromValue.name" placeholder="请输入您的姓名" />
</view>
<view class="content-sex">
<view class="sex-titile">性别</view>
<view class="sext-ri">
<view class="sext-box" :class="{ 'sext-boxactive': fromValue.sex === 0 }" @click="changeSex(0)">
</view>
<view class="sext-box" :class="{ 'sext-boxactive': fromValue.sex === 1 }" @click="changeSex(1)">
</view>
</view>
</view>
<view class="content-input" @click="changeDateBirt">
<view class="input-titile">出生年月</view>
<input
class="input-con triangle"
v-model="fromValue.birthDate"
disabled
placeholder="请选择您的出生年月"
/>
</view>
<view class="content-input" @click="changeEducation">
<view class="input-titile">学历</view>
<input class="input-con triangle" v-model="state.educationText" disabled placeholder="请选择您的学历" />
</view>
<view class="content-input" @click="changePoliticalAffiliation">
<view class="input-titile">政治面貌</view>
<input
class="input-con triangle"
v-model="state.politicalAffiliationText"
disabled
placeholder="请选择您的政治面貌"
/>
</view>
<view class="content-input">
<view class="input-titile">手机号码</view>
<input class="input-con" v-model="fromValue.phone" placeholder="请输入您的手机号码" />
</view>
</view>
</view>
<view class="content-input">
<view class="input-titile">姓名</view>
<input class="input-con" v-model="fromValue.name" placeholder="请输入您的姓名" />
</view>
<view class="content-sex">
<view class="sex-titile">性别</view>
<view class="sext-ri">
<view class="sext-box" :class="{ 'sext-boxactive': fromValue.sex === 0 }" @click="changeSex(0)"> </view>
<view class="sext-box" :class="{ 'sext-boxactive': fromValue.sex === 1 }" @click="changeSex(1)"> </view>
</view>
</view>
<view class="content-input" @click="changeDateBirt">
<view class="input-titile">出生年月</view>
<input class="input-con triangle" v-model="fromValue.birthDate" disabled placeholder="请选择您的出生年月" />
</view>
<view class="content-input" @click="changeEducation">
<view class="input-titile">学历</view>
<input class="input-con triangle" v-model="state.educationText" disabled placeholder="请选择您的学历" />
</view>
<view class="content-input" @click="changePoliticalAffiliation">
<view class="input-titile">政治面貌</view>
<input class="input-con triangle" v-model="state.politicalAffiliationText" disabled placeholder="请选择您的政治面貌" />
</view>
<view class="content-input">
<view class="input-titile">手机号码</view>
<input class="input-con" v-model="fromValue.phone" placeholder="请输入您的手机号码" />
</view>
</view>
</AppLayout>
</AppLayout>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
const { $api, navTo, navBack, checkingPhoneRegExp } = inject("globalFunction");
import { storeToRefs } from "pinia";
import useUserStore from "@/stores/useUserStore";
import useDictStore from "@/stores/useDictStore";
import { reactive, inject, watch, ref, onMounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
const { $api, navTo, navBack, checkingPhoneRegExp } = inject('globalFunction');
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
import useDictStore from '@/stores/useDictStore';
const { userInfo } = storeToRefs(useUserStore());
const { getUserResume } = useUserStore();
const { dictLabel, oneDictData } = useDictStore();
const openSelectPopup = inject("openSelectPopup");
const openSelectPopup = inject('openSelectPopup');
const percent = ref("0%");
const percent = ref('0%');
const state = reactive({
educationText: "",
politicalAffiliationText: "",
educationText: '',
politicalAffiliationText: '',
});
const fromValue = reactive({
name: "",
sex: 0,
birthDate: "",
education: "",
politicalAffiliation: "",
avatar: "",
name: '',
sex: 0,
birthDate: '',
education: '',
politicalAffiliation: '',
});
onLoad(() => {
initLoad();
// setTimeout(() => {
// const { age, birthDate } = useUserStore().userInfo;
// const newAge = calculateAge(birthDate);
// // 计算年龄是否对等
// if (age != newAge) {
// completeResume();
// }
// }, 1000);
initLoad();
// setTimeout(() => {
// const { age, birthDate } = useUserStore().userInfo;
// const newAge = calculateAge(birthDate);
// // 计算年龄是否对等
// if (age != newAge) {
// completeResume();
// }
// }, 1000);
});
function initLoad() {
fromValue.name = userInfo.value.name;
fromValue.sex = Number(userInfo.value.sex);
fromValue.phone = userInfo.value.phone;
fromValue.birthDate = userInfo.value.birthDate;
fromValue.education = userInfo.value.education;
fromValue.politicalAffiliation = userInfo.value.politicalAffiliation;
fromValue.avatar = userInfo.value.avatar;
// 回显
state.educationText = dictLabel("education", userInfo.value.education);
state.politicalAffiliationText = dictLabel("affiliation", userInfo.value.politicalAffiliation);
const result = getFormCompletionPercent(fromValue);
percent.value = result;
fromValue.name = userInfo.value.name;
fromValue.sex = Number(userInfo.value.sex);
fromValue.phone = userInfo.value.phone;
fromValue.birthDate = userInfo.value.birthDate;
fromValue.education = userInfo.value.education;
fromValue.politicalAffiliation = userInfo.value.politicalAffiliation;
// 回显
state.educationText = dictLabel('education', userInfo.value.education);
state.politicalAffiliationText = dictLabel('affiliation', userInfo.value.politicalAffiliation);
const result = getFormCompletionPercent(fromValue);
percent.value = result;
}
const confirm = () => {
if (!fromValue.name) {
return $api.msg("请输入姓名");
}
if (!fromValue.birthDate) {
return $api.msg("请选择出生年月");
}
if (!fromValue.education) {
return $api.msg("请选择学历");
}
if (!fromValue.politicalAffiliation) {
return $api.msg("请选择政治面貌");
}
// if (!checkingPhoneRegExp(fromValue.phone)) {
// return $api.msg('请输入正确手机号');
// }
const params = {
...fromValue,
age: calculateAge(fromValue.birthDate),
};
$api.createRequest("/app/user/resume", params, "post").then((resData) => {
$api.msg("完成");
state.disbleDate = true;
getUserResume().then(() => {
navBack();
if (!fromValue.name) {
return $api.msg('请输入姓名');
}
if (!fromValue.birthDate) {
return $api.msg('请选择出生年月');
}
if (!fromValue.education) {
return $api.msg('请选择学历');
}
if (!fromValue.politicalAffiliation) {
return $api.msg('请选择政治面貌');
}
if (!checkingPhoneRegExp(fromValue.phone)) {
return $api.msg('请输入正确手机号');
}
const params = {
...fromValue,
age: calculateAge(fromValue.birthDate),
};
$api.createRequest('/app/user/resume', params, 'post').then((resData) => {
$api.msg('完成');
state.disbleDate = true;
getUserResume().then(() => {
navBack();
});
});
});
};
const changeDateBirt = () => {
const datearray = generateDatePickerArrays();
const defaultIndex = getDatePickerIndexes(fromValue.birthDate);
openSelectPopup({
title: "年龄段",
maskClick: true,
data: datearray,
defaultIndex,
success: (_, value) => {
const [year, month, day] = value;
const dateStr = `${year.value}-${month.value}-${day.value}`;
if (isValidDate(dateStr)) {
fromValue.birthDate = dateStr;
} else {
$api.msg("没有这一天");
}
},
});
const datearray = generateDatePickerArrays();
const defaultIndex = getDatePickerIndexes(fromValue.birthDate);
openSelectPopup({
title: '年龄段',
maskClick: true,
data: datearray,
defaultIndex,
success: (_, value) => {
const [year, month, day] = value;
const dateStr = `${year.value}-${month.value}-${day.value}`;
if (isValidDate(dateStr)) {
fromValue.birthDate = dateStr;
} else {
$api.msg('没有这一天');
}
},
});
};
const changeEducation = () => {
openSelectPopup({
title: "学历",
maskClick: true,
data: [oneDictData("education")],
success: (_, [value]) => {
fromValue.education = value.value;
state.educationText = value.label;
},
});
openSelectPopup({
title: '学历',
maskClick: true,
data: [oneDictData('education')],
success: (_, [value]) => {
fromValue.education = value.value;
state.educationText = value.label;
},
});
};
const changeSex = (sex) => {
fromValue.sex = sex;
fromValue.sex = sex;
};
const changePoliticalAffiliation = () => {
openSelectPopup({
title: "政治面貌",
maskClick: true,
data: [oneDictData("affiliation")],
success: (_, [value]) => {
fromValue.politicalAffiliation = value.value;
state.politicalAffiliationText = value.label;
},
});
openSelectPopup({
title: '政治面貌',
maskClick: true,
data: [oneDictData('affiliation')],
success: (_, [value]) => {
fromValue.politicalAffiliation = value.value;
state.politicalAffiliationText = value.label;
},
});
};
function generateDatePickerArrays(startYear = 1975, endYear = new Date().getFullYear()) {
const years = [];
const months = [];
const days = [];
const years = [];
const months = [];
const days = [];
for (let y = startYear; y <= endYear; y++) {
years.push(y.toString());
}
for (let m = 1; m <= 12; m++) {
months.push(m.toString().padStart(2, "0"));
}
for (let d = 1; d <= 31; d++) {
days.push(d.toString().padStart(2, "0"));
}
for (let y = startYear; y <= endYear; y++) {
years.push(y.toString());
}
for (let m = 1; m <= 12; m++) {
months.push(m.toString().padStart(2, '0'));
}
for (let d = 1; d <= 31; d++) {
days.push(d.toString().padStart(2, '0'));
}
return [years, months, days];
return [years, months, days];
}
function isValidDate(dateString) {
const [year, month, day] = dateString.split("-").map(Number);
const [year, month, day] = dateString.split('-').map(Number);
const date = new Date(year, month - 1, day); // 月份从0开始
return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day;
const date = new Date(year, month - 1, day); // 月份从0开始
return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day;
}
const calculateAge = (birthDate) => {
const birth = new Date(birthDate);
const today = new Date();
let age = today.getFullYear() - birth.getFullYear();
const monthDiff = today.getMonth() - birth.getMonth();
const dayDiff = today.getDate() - birth.getDate();
const birth = new Date(birthDate);
const today = new Date();
let age = today.getFullYear() - birth.getFullYear();
const monthDiff = today.getMonth() - birth.getMonth();
const dayDiff = today.getDate() - birth.getDate();
// 如果生日的月份还没到,或者刚到生日月份但当天还没过,则年龄减 1
if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) {
age--;
}
// 如果生日的月份还没到,或者刚到生日月份但当天还没过,则年龄减 1
if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) {
age--;
}
return age;
return age;
};
function getFormCompletionPercent(form) {
let total = Object.keys(form).length;
let filled = 0;
let total = Object.keys(form).length;
let filled = 0;
for (const key in form) {
const value = form[key];
if (value !== "" && value !== null && value !== undefined) {
if (typeof value === "number") {
filled += 1;
} else if (typeof value === "string" && value.trim() !== "") {
filled += 1;
}
for (const key in form) {
const value = form[key];
if (value !== '' && value !== null && value !== undefined) {
if (typeof value === 'number') {
filled += 1;
} else if (typeof value === 'string' && value.trim() !== '') {
filled += 1;
}
}
}
}
if (total === 0) return "0%";
const percent = (filled / total) * 100;
return percent.toFixed(0) + "%"; // 取整,不要小数点
if (total === 0) return '0%';
const percent = (filled / total) * 100;
return percent.toFixed(0) + '%'; // 取整,不要小数点
}
// 主函数
function getDatePickerIndexes(dateStr) {
const [year, month, day] = dateStr.split("-");
const [year, month, day] = dateStr.split('-');
const [years, months, days] = generateDatePickerArrays();
const [years, months, days] = generateDatePickerArrays();
const yearIndex = years.indexOf(year);
const monthIndex = months.indexOf(month);
const dayIndex = days.indexOf(day);
const yearIndex = years.indexOf(year);
const monthIndex = months.indexOf(month);
const dayIndex = days.indexOf(day);
return [yearIndex, monthIndex, dayIndex];
}
function selectAvatar() {
uni.chooseImage({
sizeType: ["original", "compressed"],
sourceType: ["album", "camera"],
count: 1,
success: ({ tempFilePaths, tempFiles }) => {
$api
.uploadFile(tempFilePaths[0], true)
.then((res) => {
res = JSON.parse(res);
if (res.msg) fromValue.avatar = res.msg;
})
.catch((err) => {
$api.msg("上传失败");
});
},
fail: (error) => {},
});
return [yearIndex, monthIndex, dayIndex];
}
</script>
@@ -277,23 +267,6 @@ function selectAvatar() {
height: calc(100% - 120rpx)
}
.content-avatar{
margin-bottom: 52rpx;
padding-bottom: 28rpx
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2rpx solid #EBEBEB
.avatar-title{
font-size: 30rpx;
color #333;
}
.avatar{
width:110rpx;
height: 110rpx;
border-radius: 50%;
}
}
.content-input
margin-bottom: 52rpx
.input-titile
@@ -329,12 +302,12 @@ function selectAvatar() {
background: #697279;
transform: rotate(45deg)
.content-sex
height: 110rpx;
display: flex
justify-content: space-between;
align-items: flex-start;
border-bottom: 2rpx solid #EBEBEB
margin-bottom: 52rpx
padding-bottom: 28rpx
.sex-titile
line-height: 80rpx;
.sext-ri

BIN
packageA/pages/post/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -1,14 +1,11 @@
<template>
<AppLayout title="" backGorundColor="#F4F4F4">
<template #headerleft>
<view class="btnback">
<view class="btn">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
</template>
<template #headerright>
<!-- <view class="btnshare">
<image src="@/static/icon/share.png" @click="shareJob"></image>
</view> -->
<view class="btn mar_ri10">
<image src="@/static/icon/collect3.png" v-if="!jobInfo.isCollection" @click="jobCollection"></image>
<image src="@/static/icon/collect2.png" v-else @click="jobCollection"></image>
@@ -140,11 +137,10 @@
import point from '@/static/icon/point.png';
import VideoPlayer from './component/videoPlayer.vue';
import { reactive, inject, watch, ref, onMounted, computed } from 'vue';
import { onLoad, onShow, onHide } from '@dcloudio/uni-app';
import { onLoad, onShow } from '@dcloudio/uni-app';
import dictLabel from '@/components/dict-Label/dict-Label.vue';
import RadarMap from './component/radarMap.vue';
const { $api, navTo, getLenPx, parseQueryParams, navBack, isEmptyObject } = inject('globalFunction');
import config from '@/config.js';
import RadarMap from './component/radarMap.vue';
const matchingDegree = ref(['一般', '良好', '优秀', '极好']);
const currentStep = ref(1);
const companyCount = ref(0);
@@ -186,33 +182,30 @@ function seeExplain() {
}
function getDetail(jobId) {
return new Promise((reslove, reject) => {
$api.createRequest(`/app/job/${jobId}`).then((resData) => {
const { latitude, longitude, companyName, companyId } = resData.data;
jobInfo.value = resData.data;
reslove(resData.data);
getCompanyIsAJobs(companyId);
getCompetivetuveness(jobId);
if (latitude && longitude) {
mapCovers.value = [
{
latitude: latitude,
longitude: longitude,
iconPath: point,
label: {
content: companyName,
textAlign: 'center',
padding: 3,
fontSize: 12,
bgColor: '#FFFFFF',
anchorX: getTextWidth(companyName), // X 轴调整,负数向左
borderRadius: 5,
},
width: 34,
$api.createRequest(`/app/job/${jobId}`).then((resData) => {
const { latitude, longitude, companyName, companyId } = resData.data;
jobInfo.value = resData.data;
getCompanyIsAJobs(companyId);
getCompetivetuveness(jobId);
if (latitude && longitude) {
mapCovers.value = [
{
latitude: latitude,
longitude: longitude,
iconPath: point,
label: {
content: companyName,
textAlign: 'center',
padding: 3,
fontSize: 12,
bgColor: '#FFFFFF',
anchorX: getTextWidth(companyName), // X 轴调整,负数向左
borderRadius: 5,
},
];
}
});
width: 34,
},
];
}
});
}
@@ -285,21 +278,12 @@ function getClass(index) {
</script>
<style lang="stylus" scoped>
.btnback{
width: 64rpx;
height: 64rpx;
}
.btn {
display: flex;
justify-content: space-between;
align-items: center;
width: 52rpx;
height: 52rpx;
}
.btnshare {
width: 48rpx;
height: 48rpx;
margin-right: 46rpx;
width: 60rpx;
height: 60rpx;
}
image {
height: 100%;

View File

@@ -1,265 +0,0 @@
<template>
<AppLayout title="电子名片" title-color="#FFFFFF" back-gorund-color="#F4F4F4">
<template #headerleft>
<view class="btn">
<image src="@/static/icon/back-white.png" @click="navBack"></image>
</view>
</template>
<view class="mys-container">
<!-- 个人信息 -->
<view class="card-top btn-feel">
<view class="info">
<view class="avatar">
<image v-if="userInfo.avatar" :src="userInfo.avatar"></image>
<image v-else-if="userInfo.sex == '0'" src="@/static/icon/boy.png"></image>
<image v-else src="@/static/icon/girl.png"></image>
</view>
<view class="info-right">
<view class="name">{{ userInfo.name || "编辑用户名" }}</view>
<view class="des">
<dict-Label class="mar_ri10" dictType="sex" :value="userInfo.sex"></dict-Label>
<text class="mar_ri10">|</text>
<text class="mar_ri10">{{ userInfo.age }}</text>
<text class="mar_ri10">|</text>
<dict-Label class="mar_ri10" dictType="education" :value="userInfo.education"></dict-Label>
<!-- <text class="mar_ri10">|</text>
<dict-Label class="mar_ri10" dictType="affiliation" :value="userInfo.politicalAffiliation"></dict-Label> -->
</view>
<view class="phone">
<image class="call-icon" src="@/static/icon/call.png" />
<view>{{ userInfo.phone }}</view>
</view>
</view>
</view>
<view class="info-bottom">
<view>到岗2025-11-02</view>
<view>地点青岛市-<dict-Label dictType="area" :value="Number(userInfo.area)"></dict-Label></view>
</view>
<view class="des-card" style="margin-top: 24rpx">
<view class="fl_box fl_justbet">
<view style="white-space:nowrap">求职意向岗位</view>
<view class="line_1" style="padding-left:40rpx" >{{ userInfo.jobIntention || userInfo.jobTitle?.join(',') || '-' }}</view>
</view>
<view class="fl_box fl_justbet">
<view>毕业学校</view>
<view>{{ userInfo.graduationSchool || "-" }}</view>
</view>
<view class="fl_box fl_justbet">
<view>当前状态</view>
<view>在职 看工作机会</view>
</view>
</view>
</view>
<view class="card btn-feel">
<view class="title">
<image class="bg" src="@/static/icon/title-bg.png" />
<view class="text">个人技能</view>
</view>
<view class="skill-box">
<view class="skill-item" v-for="item in userInfo?.skillList" :key="item.id">{{ item.skill }}</view>
</view>
<view v-if="!userInfo?.skillList?.length" class="empty-box">
<image class="img" src="@/static/icon/empty.png" mode="widthFix"></image>
<view class="content">暂无个人技能</view>
</view>
</view>
<view class="card btn-feel">
<view class="title">
<image class="bg" src="@/static/icon/title-bg.png" />
<view class="text">关键经历</view>
</view>
<view class="exp-box">
<view class="exp-item" v-for="(item, index) in userInfo?.workExp" :key="item.id">{{ index + 1 + "." }}{{ item.duty }}</view>
</view>
<view v-if="!userInfo?.workExp?.length" class="empty-box">
<image class="img" src="@/static/icon/empty.png" mode="widthFix"></image>
<view class="content">暂无关键经历</view>
</view>
</view>
<view class="card btn-feel">
<view class="title">
<image class="bg" src="@/static/icon/title-bg.png" />
<view class="text">荣誉及证书情况</view>
</view>
<ul class="certificate-box">
<li class="certificate-item" v-for="(item, index) in userInfo?.certificateList" :key="item.id">{{ item.name }}</li>
</ul>
<view v-if="!userInfo?.certificateList?.length" class="empty-box">
<image class="img" src="@/static/icon/empty.png" mode="widthFix"></image>
<view class="content">暂无荣誉证书</view>
</view>
</view>
</view>
</AppLayout>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted, computed } from "vue";
const { $api, navTo, navBack } = inject("globalFunction");
import { onLoad, onShow } from "@dcloudio/uni-app";
import { storeToRefs } from "pinia";
import useUserStore from "@/stores/useUserStore";
import useDictStore from "@/stores/useDictStore";
const { userInfo } = storeToRefs(useUserStore());
const { getUserResume } = useUserStore();
const { getDictData, oneDictData } = useDictStore();
</script>
<style lang="scss" scoped>
.btn {
display: flex;
justify-content: space-between;
align-items: center;
width: 60rpx;
height: 60rpx;
image {
height: 100%;
width: 100%;
}
}
.card-top {
width: 100%;
border-radius: 8rpx;
box-sizing: border-box;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.9));
}
.card {
width: 100%;
border-radius: 8rpx;
box-sizing: border-box;
background: #fff;
padding: 24rpx;
margin-top: 24rpx;
.title {
position: relative;
.bg {
width: 108rpx;
height: 16rpx;
position: absolute;
left: 0;
bottom: 0;
}
.text {
margin-left: 20rpx;
font-size: 30rpx;
font-weight: bold;
color: #333;
}
}
}
.skill-box {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-top: 24rpx;
.skill-item {
padding: 8rpx 20rpx;
font-size: 24rpx;
color: #333;
background: #e7f1ff;
border-radius: 8rpx;
}
}
.exp-box {
display: flex;
flex-wrap: wrap;
gap: 24rpx;
margin-top: 24rpx;
.exp-item {
font-size: 26rpx;
color: #333;
}
}
.certificate-box {
margin-top: 24rpx;
padding-inline-start: 40rpx !important;
.certificate-item {
font-size: 26rpx;
color: #333;
margin-bottom: 24rpx;
}
}
image {
width: 100%;
height: 100%;
}
.mys-container {
padding: 28rpx;
.info {
display: flex;
align-items: center;
padding: 24rpx;
.avatar {
width: 160rpx;
height: 160rpx;
margin-right: 24rpx;
image{
border-radius: 50%;
}
}
.info-right {
height: 160rpx;
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: space-between;
.name {
font-size: 40rpx;
font-weight: bold;
}
.des {
font-size: 26rpx;
color: #999999;
}
.phone {
display: flex;
align-items: center;
font-size: 26rpx;
color: #999999;
.call-icon {
width: 24rpx;
height: 24rpx;
margin-right: 5rpx;
}
}
}
}
.info-bottom {
background: linear-gradient(to right, #91b6ff, #87afff, #b7adff);
border-radius: 0 0 8rpx 8rpx;
padding: 12rpx 24rpx;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 26rpx;
color: #333;
}
.des-card {
padding: 24rpx;
font-size: 26rpx;
color: #666;
display: flex;
flex-direction: column;
gap: 12rpx;
}
.empty-box {
padding: 80rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.img {
width: 100%;
}
.content {
margin-top: 24rpx;
font-size: 30rpx;
color: #666;
}
}
}
</style>

View File

@@ -1,245 +0,0 @@
<template>
<AppLayout title="工作经历" border back-gorund-color="#ffffff" :show-bg-image="false">
<template #headerleft>
<view class="btn mar_le20 button-click" @click="navBack">取消</view>
</template>
<template #headerright>
<view class="btn mar_ri20 button-click blue" @click="confirm">确认</view>
</template>
<view class="content">
<view class="content-input">
<view class="input-titile">公司</view>
<input class="input-con" v-model="fromValue.company" placeholder="请输入公司名称" />
</view>
<view class="content-input">
<view class="input-titile">岗位</view>
<input class="input-con" v-model="fromValue.position" placeholder="请输入岗位" />
</view>
<view class="content-input">
<view class="input-titile">时间</view>
<view class="flex-box">
<view class="input-box btn-feel" @click="changestartTime">
<input v-model="fromValue.startTime" class="input-con triangle" disabled placeholder="开始时间" />
<image class="icon" src="@/static/icon/arrow-down.png" />
</view>
<view class="gap">-</view>
<view class="input-box btn-feel" @click="changeendTime">
<input v-model="fromValue.endTime" class="input-con triangle" disabled placeholder="至今" />
<image class="icon" src="@/static/icon/arrow-down.png" />
</view>
</view>
</view>
<view class="content-input">
<view class="input-titile">工作内容</view>
<textarea class="text-area" placeholder="请输入工作内容" v-model="fromValue.duty"></textarea>
</view>
</view>
<!-- 时间选择器组件 -->
<DatePicker ref="datePicker" />
<template #footer v-if="fromValue.id">
<view class="footer-container">
<view class="footer-button btn-feel" @click="delCurrent">删除该工作经历</view>
</view>
</template>
</AppLayout>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted, computed } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
const { $api, navTo, navBack } = inject("globalFunction");
import { storeToRefs } from "pinia";
import useUserStore from "@/stores/useUserStore";
import useDictStore from "@/stores/useDictStore";
import DatePicker from "@/components/DatePicker/DatePicker.vue";
const { userInfo } = storeToRefs(useUserStore());
const { getUserResume } = useUserStore();
const { dictLabel, oneDictData } = useDictStore();
// 初始化数据
const fromValue = reactive({
position: "",
company: "",
startTime: "",
endTime: "",
duty: "",
id: undefined,
});
// 获取时间选择器组件的引用
const datePicker = ref();
onLoad((e) => {
initLoad(e?.id);
});
const confirm = async () => {
// 验证必填字段
if (!fromValue.company) {
return $api.msg("请输入公司名称");
}
if (!fromValue.position) {
return $api.msg("请输入岗位");
}
if (!fromValue.startTime) {
return $api.msg("请选择开始时间");
}
// 验证时间逻辑:结束时间不能早于开始时间
if (fromValue.endTime && new Date(fromValue.endTime) < new Date(fromValue.startTime)) {
return $api.msg("结束时间不能早于开始时间");
}
let res;
try {
if (fromValue.id) {
res = await $api.createRequest("/app/user/experience/edit", fromValue, "post");
} else {
res = await $api.createRequest("/app/user/experience/save", fromValue, "post");
}
$api.msg("保存成功");
getUserResume().then(() => {
navBack();
});
} catch (error) {
$api.msg("保存失败");
}
};
function delCurrent() {
uni.showModal({
title: "提示",
content: "确认要删除此条工作经历吗?",
showCancel: true,
success: async ({ confirm, cancel }) => {
if (confirm) {
await $api.createRequest("/app/user/experience/delete", { id: fromValue.id }, "post");
$api.msg("删除成功");
getUserResume().then(() => {
navBack();
});
}
},
});
}
function initLoad(id) {
if (!id) return;
$api
.createRequest(`/app/user/experience/getSingle/${id}`, {}, "get")
.then((res) => {
Object.assign(fromValue, res.data);
})
.catch((err) => {
console.error("获取工作经历失败:", err);
});
}
// 选择开始时间
const changestartTime = () => {
console.log(1);
datePicker.value.open({
title: "选择开始时间",
defaultDate: fromValue.startTime,
success: (selectedDate) => {
fromValue.startTime = selectedDate;
},
});
};
// 选择结束时间
const changeendTime = () => {
datePicker.value.open({
title: "选择结束时间",
defaultDate: fromValue.endTime,
success: (selectedDate) => {
fromValue.endTime = selectedDate;
// 如果结束时间早于新的开始时间,清空结束时间
if (fromValue.startTime && new Date(fromValue.startTime) > new Date(selectedDate)) {
fromValue.endTime = "";
$api.msg("结束时间不能小于开始时间!");
}
},
});
};
</script>
<style lang="scss" scoped>
.btn.blue {
color: #1677ff;
}
.content {
padding: 28rpx;
display: flex;
flex-direction: column;
justify-content: flex-start;
height: calc(100% - 120rpx);
}
.flex-box {
display: flex;
align-items: center;
.gap {
font-size: 40rpx;
font-weight: bold;
flex: 0.25;
text-align: center;
}
.icon {
width: 75rpx;
height: 50rpx;
}
.input-box {
flex: 0.375;
display: flex;
align-items: center;
}
}
.content-input {
margin-bottom: 48rpx;
padding-bottom: 20rpx;
border-bottom: 2rpx solid #ebebeb;
&:nth-last-of-type(1) {
border-bottom: none;
}
.input-titile {
font-weight: 400;
font-size: 28rpx;
color: #6a6a6a;
margin-bottom: 10rpx;
}
.input-con {
font-weight: 400;
font-size: 32rpx;
color: #333333;
line-height: 80rpx;
height: 80rpx;
position: relative;
}
.triangle {
pointer-events: none;
}
.text-area {
width: 100%;
height: 700rpx;
background: #f5f5f5;
padding: 20rpx;
box-sizing: border-box;
}
}
.footer-container {
background: #ffffff;
box-shadow: 0rpx -4rpx 24rpx 0rpx rgba(11, 44, 112, 0.12);
border-radius: 0rpx 0rpx 0rpx 0rpx;
padding: 40rpx 28rpx 20rpx 28rpx;
.footer-button {
width: 100%;
height: 90rpx;
background: #f93a4a;
border-radius: 8rpx;
color: #ffffff;
line-height: 90rpx;
text-align: center;
}
}
</style>

View File

@@ -1,13 +1,12 @@
{
"pages": [
//pages数组中第一项表示应用启动页参考https://uniapp.dcloud.io/collocation/pages
"pages": [ //pages数组中第一项表示应用启动页参考https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "青岛智慧就业平台",
// #ifdef H5
// #ifdef H5
"navigationStyle": "custom"
// #endif
// #endif
}
},
{
@@ -48,18 +47,18 @@
"navigationStyle": "custom"
}
},
{
"path": "pages/chat/chat",
"style": {
"navigationBarTitleText": "AI+",
"navigationBarBackgroundColor": "#4778EC",
"navigationBarTextStyle": "white",
"enablePullDownRefresh": false,
// #ifdef H5
"navigationStyle": "custom"
//#endif
}
},
// {
// "path": "pages/chat/chat",
// "style": {
// "navigationBarTitleText": "AI+",
// "navigationBarBackgroundColor": "#4778EC",
// "navigationBarTextStyle": "white",
// "enablePullDownRefresh": false,
// // #ifdef H5
// "navigationStyle": "custom"
// //#endif
// }
// },
{
"path": "pages/search/search",
"style": {
@@ -67,6 +66,7 @@
"navigationStyle": "custom"
}
}
],
"subpackages": [{
"root": "packageA",
@@ -78,8 +78,7 @@
"navigationBarTextStyle": "white",
"navigationStyle": "custom"
}
},
{
}, {
"path": "pages/post/post",
"style": {
"navigationBarTitleText": "职位详情",
@@ -87,8 +86,7 @@
"navigationBarTextStyle": "white",
"navigationStyle": "custom"
}
},
{
}, {
"path": "pages/UnitDetails/UnitDetails",
"style": {
"navigationBarTitleText": "单位详情",
@@ -96,8 +94,7 @@
"navigationBarTextStyle": "white",
"navigationStyle": "custom"
}
},
{
}, {
"path": "pages/exhibitors/exhibitors",
"style": {
"navigationBarTitleText": "参展单位",
@@ -105,31 +102,19 @@
"navigationBarTextStyle": "white",
"navigationStyle": "custom"
}
},
{
}, {
"path": "pages/myResume/myResume",
"style": {
"navigationBarTitleText": "我的简历",
"navigationBarBackgroundColor": "#FFFFFF",
"navigationStyle": "custom"
"navigationBarBackgroundColor": "#FFFFFF"
}
},
{
"path": "pages/vCard/vCard",
"style": {
"navigationBarTitleText": "点子名片",
"navigationBarBackgroundColor": "#FFFFFF",
"navigationStyle": "custom"
}
},
{
}, {
"path": "pages/Intendedposition/Intendedposition",
"style": {
"navigationBarTitleText": "投递记录",
"navigationBarBackgroundColor": "#FFFFFF"
}
},
{
}, {
"path": "pages/collection/collection",
"style": {
"navigationBarTitleText": "我的收藏",
@@ -173,13 +158,6 @@
"navigationStyle": "custom"
}
},
{
"path": "pages/workExp/workExp",
"style": {
"navigationBarTitleText": "工作经历",
"navigationStyle": "custom"
}
},
{
"path": "pages/reservation/reservation",
"style": {
@@ -216,13 +194,6 @@
"navigationBarBackgroundColor": "#FFFFFF",
"navigationStyle": "custom"
}
},
{
"path": "pages/moreJobs/moreJobs",
"style": {
"navigationBarTitleText": "更多岗位",
"navigationBarBackgroundColor": "#FFFFFF"
}
}
]
}],
@@ -250,11 +221,11 @@
"selectedIconPath": "static/tabbar/posted.png",
"text": "招聘会"
},
{
"pagePath": "pages/chat/chat",
"iconPath": "static/tabbar/logo3.png",
"selectedIconPath": "static/tabbar/logo3.png"
},
// {
// "pagePath": "pages/chat/chat",
// "iconPath": "static/tabbar/logo3.png",
// "selectedIconPath": "static/tabbar/logo3.png"
// },
{
"pagePath": "pages/msglog/msglog",
"iconPath": "static/tabbar/chat4.png",

BIN
pages/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -113,8 +113,7 @@
</view>
</view>
<view class="chat-item self" v-if="isRecording">
<!-- <view class="message">{{ recognizedText }} {{ lastFinalText }}</view> -->
<view class="message">{{ recognizedText }}</view>
<view class="message">{{ recognizedText }} {{ lastFinalText }}</view>
</view>
<view v-if="isTyping" class="self">
<text class="message msg-loading">
@@ -250,6 +249,8 @@ import {
ref,
inject,
nextTick,
defineProps,
defineEmits,
onMounted,
onUnmounted,
toRaw,
@@ -267,18 +268,14 @@ import AudioWave from './AudioWave.vue';
import WaveDisplay from './WaveDisplay.vue';
import FileIcon from './fileIcon.vue';
import FileText from './fileText.vue';
// 系统功能hook和阿里云hook
// import { useAudioRecorder } from '@/hook/useRealtimeRecorder.js';
import { useAudioRecorder } from '@/hook/useSystemSpeechReader.js';
// import { useTTSPlayer } from '@/hook/useTTSPlayer.js';
import { useTTSPlayer } from '@/hook/useSystemPlayer.js';
import { useAudioRecorder } from '@/hook/useRealtimeRecorder.js';
import { useTTSPlayer } from '@/hook/useTTSPlayer.js';
// 全局
const { $api, navTo, throttle } = inject('globalFunction');
const emit = defineEmits(['onConfirm']);
const { messages, isTyping, textInput, chatSessionID } = storeToRefs(useChatGroupDBStore());
import successIcon from '@/static/icon/success.png';
// hook
// 语音识别
const {
isRecording,
startRecording,
@@ -288,9 +285,9 @@ const {
volumeLevel,
recognizedText,
lastFinalText,
} = useAudioRecorder();
// 语音合成
const { speak, pause, resume, isSpeaking, isPaused, cancelAudio } = useTTSPlayer(config.speechSynthesis);
} = useAudioRecorder(config.vioceBaseURl);
const { speak, pause, resume, isSpeaking, isPaused, cancelAudio, audioUrl } = useTTSPlayer(config.speechSynthesis);
// state
const queries = ref([]);
@@ -446,7 +443,7 @@ const scrollToBottom = throttle(function () {
}, 500);
function getGuess() {
$api.chatRequest('/app/chat/guest', { sessionId: chatSessionID.value }, 'POST').then((res) => {
$api.chatRequest('/guest', { sessionId: chatSessionID.value }, 'POST').then((res) => {
guessList.value = res.data;
showGuess.value = true;
nextTick(() => {
@@ -632,7 +629,6 @@ function readMarkdown(value, index) {
if (isPaused.value) {
resume();
} else {
console.log(value, speechIndex.value, index, isPaused.value)
speak(value);
}
}

View File

@@ -37,7 +37,7 @@
</template>
<script setup>
import { ref, inject } from 'vue';
import { ref, inject, defineEmits } from 'vue';
const emit = defineEmits(['onSend']);
const { $api } = inject('globalFunction');
const popup = ref(null);

View File

@@ -6,7 +6,7 @@
<uni-icons class="iconsearch" color="#666666" type="search" size="18"></uni-icons>
<text class="inpute">职位名称薪资要求等</text>
</view>
<!-- <view class="chart button-click">职业图谱</view> -->
<view class="chart button-click">职业图谱</view>
</view>
<view class="cards">
<view class="card press-button" @click="navTo('/pages/nearby/nearby')">
@@ -390,12 +390,10 @@ function getJobRecommend(type = 'add') {
list.value = dataToImg(data);
}
// 切换状态
if (loadmoreRef.value && typeof loadmoreRef.value.change === 'function') {
if (data.length < pageState.pageSize) {
loadmoreRef.value.change('noMore');
} else {
loadmoreRef.value.change('more');
}
if (data.length < pageState.pageSize) {
loadmoreRef.value.change('noMore');
} else {
loadmoreRef.value.change('more');
}
// 当没有岗位刷新sessionId重新啦
if (!data.length) {
@@ -412,8 +410,7 @@ function getJobList(type = 'add') {
list.value = [];
pageState.page = 1;
pageState.maxPage = 2;
// waterfallsFlowRef.value.refresh();
if (waterfallsFlowRef.value) waterfallsFlowRef.value.refresh();
waterfallsFlowRef.value.refresh();
}
let params = {
current: pageState.page,
@@ -434,13 +431,10 @@ function getJobList(type = 'add') {
}
pageState.total = resData.total;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
// 切换状态
if (loadmoreRef.value && typeof loadmoreRef.value.change === 'function') {
if (rows.length < pageState.pageSize) {
loadmoreRef.value?.change('noMore');
} else {
loadmoreRef.value?.change('more');
}
if (rows.length < pageState.pageSize) {
loadmoreRef.value?.change('noMore');
} else {
loadmoreRef.value?.change('more');
}
});
}
@@ -563,7 +557,7 @@ defineExpose({ loadData });
width: 100%
height: 80rpx;
line-height: 80rpx
// margin-right: 24rpx
margin-right: 24rpx
background: #FFFFFF;
border-radius: 75rpx 75rpx 75rpx 75rpx;
.iconsearch
@@ -703,7 +697,7 @@ defineExpose({ loadData });
text-align: left;
word-break:break-all
font-weight: 500;
font-size: 30rpx;
font-size: 32rpx;
color: #333333;
margin-top: 10rpx
.falls-card-pay
@@ -753,22 +747,20 @@ defineExpose({ loadData });
.falls-card-pepleNumber
display: flex;
justify-content: space-between;
flex-wrap: wrap
margin-top: 10rpx;
align-items: center;
margin-top: 20rpx;
font-weight: 400;
font-size: 24rpx;
color: #999999;
line-height: 46rpx
view
display:flex
align-items: center
white-space: nowrap;
.point2
margin: 0rpx 6rpx 0 2rpx
margin: 4rpx 6rpx 0 2rpx
height: 22rpx
width: 22rpx
.point3
margin: 0rpx 4rpx 0 0
margin: 4rpx 4rpx 0 0
height: 28rpx
width: 28rpx
.falls-card-matchingrate
@@ -780,7 +772,7 @@ defineExpose({ loadData });
color: #4778EC;
text-align: left;
.falls-card-company2
margin-top: 4rpx;
margin-top: 8rpx;
font-size: 24rpx;
color: #999999;
text-align: left;

View File

@@ -60,14 +60,14 @@ const maskFristEntry = ref(false);
onLoad(() => {
// 判断浏览器是否有 fristEntry 第一次进入
let fristEntry = uni.getStorageSync('fristEntry') === false ? false : true; // 默认未读
maskFristEntry.value = fristEntry;
// maskFristEntry.value = true;
// let fristEntry = uni.getStorageSync('fristEntry') === false ? false : true; // 默认未读
// maskFristEntry.value = fristEntry ;
maskFristEntry.value = true;
});
onShow(() => {
// 获取消息列表
// useReadMsg().fetchMessages();
useReadMsg().fetchMessages();
});
const state = reactive({

View File

@@ -109,7 +109,6 @@
</template>
</tabcontrolVue>
<SelectJobs ref="selectJobsModel"></SelectJobs>
<view class="backdoor" @click="loginbackdoor">后门</view>
</AppLayout>
</template>
@@ -126,7 +125,7 @@ const { getDictSelectOption, oneDictData } = useDictStore();
const openSelectPopup = inject('openSelectPopup');
// status
const selectJobsModel = ref();
const tabCurrent = ref(1);
const tabCurrent = ref(0);
const salay = [2, 5, 10, 15, 20, 25, 30, 50, 80, 100];
const state = reactive({
station: [],
@@ -244,23 +243,6 @@ function getTreeselect() {
});
}
function loginbackdoor() {
$api.createRequest('/app/mock/login', {}, 'post').then((resData) => {
$api.msg('模拟帐号密码测试登录成功');
loginSetToken(resData.token).then((resume) => {
if (resume.data.jobTitleId) {
// 设置推荐列表,每次退出登录都需要更新
useUserStore().initSeesionId();
uni.reLaunch({
url: '/pages/index/index',
});
} else {
nextStep();
}
});
});
}
// 登录
function loginTest() {
// uni.share({
@@ -307,12 +289,6 @@ function complete() {
</script>
<style lang="stylus" scoped>
.backdoor{
position: fixed;
left: 0;
top: 500rpx;
background: red
}
.input-nx
position: relative
border-bottom: 2rpx solid #EBEBEB
@@ -320,10 +296,12 @@ function complete() {
display: flex
flex-wrap: wrap
.nx-item
margin: 12rpx 12rpx 0 0;
padding: 12rpx 25rpx;
padding: 20rpx 28rpx
width: fit-content
border-radius: 12rpx 12rpx 12rpx 12rpx;
border: 2rpx solid #E8EAEE;
margin-right: 24rpx
margin-top: 24rpx
.nx-item::before
position: absolute;
right: 20rpx;
@@ -349,8 +327,7 @@ function complete() {
width: 100%;
height: calc(100vh - var(--window-top) - var(--status-bar-height) - var(--window-bottom));
position: fixed;
// background: linear-gradient( 180deg, #1677FF 0%, rgba(22,119,255,0) 54%, rgba(22,119,255,0) 100%);
// background: url('@/static/icon/background2.png') 0 0 no-repeat;
background: url('@/static/icon/background2.png') 0 0 no-repeat;
background-size: 100% 728rpx;
display: flex;
flex-direction: column
@@ -488,4 +465,4 @@ function complete() {
color: #FFFFFF;
text-align: center;
line-height: 90rpx
</style>
</style>

View File

@@ -2,8 +2,7 @@
<AppLayout title="我的" back-gorund-color="#F4F4F4">
<view class="mine-userinfo btn-feel" @click="navTo('/packageA/pages/myResume/myResume')">
<view class="userindo-head">
<image class="userindo-head-img" v-if="userInfo.avatar" :src="userInfo.avatar"></image>
<image class="userindo-head-img" v-else-if="userInfo.sex === '0'" src="/static/icon/boy.png"></image>
<image class="userindo-head-img" v-if="userInfo.sex === '0'" src="/static/icon/boy.png"></image>
<image class="userindo-head-img" v-else src="/static/icon/girl.png"></image>
</view>
<view class="userinfo-ls">
@@ -33,7 +32,7 @@
</view>
</view>
<view class="mini-cards">
<view class="card-top btn-feel" @click="navTo('/packageA/pages/vCard/vCard')">
<view class="card-top btn-feel">
<view class="top-title line_1">
<text>{{ userInfo.name || '暂无用户名' }}</text>
&nbsp;|&nbsp;
@@ -45,7 +44,9 @@
<text v-if="userInfo.jobTitle.length - 1 !== index">|</text>
</text>
</view>
<view class="top-btn button-click">电子名片</view>
<view class="top-btn button-click" @click="navTo('/packageA/pages/personalInfo/personalInfo')">
修改简历
</view>
</view>
<view class="card-main">
<view class="main-title">服务专区</view>
@@ -130,6 +131,7 @@ const isAbove90 = (percent) => parseFloat(percent) < 90;
function getUserstatistics() {
$api.createRequest('/app/user/statistics').then((resData) => {
console.log(resData);
counts.value = resData.data;
});
}

View File

@@ -1,26 +1,15 @@
<template>
<scroll-view :scroll-y="true" class="nearby-scroll" @scrolltolower="scrollBottom">
<view class="two-head">
<view class="head-all">
<text>热门商圈</text>
<text class="color_333333 button-click" @click="handleOpenBusinessDistrict">
更多
<uni-icons type="forward" color="#333333" size="14"></uni-icons>
</text>
<view
class="head-item"
:class="{ active: state.comId === item.commercialAreaId }"
v-for="(item, index) in state.comlist"
:key="item.commercialAreaName"
@click="clickCommercialArea(item)"
>
{{ item.commercialAreaName }}
</view>
<scroll-view class="scroll-head" :scroll-x="true" :scroll-into-view="activeTab" :show-scrollbar="false">
<view class="head-item-content">
<view
class="head-item"
:class="{ active: state.comId === item.commercialAreaId }"
v-for="(item, index) in comlistPuted"
:key="item.commercialAreaName"
@click="clickCommercialArea(item)"
>
{{ item.commercialAreaName }}
</view>
</view>
</scroll-view>
</view>
<view class="nearby-list">
<view class="nav-filter" @touchmove.stop.prevent>
@@ -81,12 +70,11 @@
</view>
<!-- 筛选 -->
<select-filter ref="selectFilterModel"></select-filter>
<select-filter2-col ref="selectFilter2ColModel"></select-filter2-col>
</scroll-view>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted, onBeforeUnmount, computed } from 'vue';
import { reactive, inject, watch, ref, onMounted, onBeforeUnmount } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
const { $api, navTo, debounce, customSystem } = inject('globalFunction');
import { storeToRefs } from 'pinia';
@@ -99,7 +87,6 @@ const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
import point2 from '@/static/icon/point2.png';
import LocationPng from '@/static/icon/Location.png';
import selectFilter from '@/components/selectFilter/selectFilter.vue';
import selectFilter2Col from '@/components/selectFilter/selectFilter2Col.vue';
const emit = defineEmits(['onFilter']);
const state = reactive({
@@ -109,15 +96,12 @@ const state = reactive({
comId: 0,
areaInfo: {},
});
const commercialAreaList = ref([]);
const isLoaded = ref(false);
const showFilter = ref(false);
const selectFilterModel = ref();
const selectFilter2ColModel = ref();
const fromValue = reactive({
area: 0,
});
const activeTab = ref('');
const loadmoreRef = ref(null);
const pageState = reactive({
page: 0,
@@ -130,18 +114,6 @@ const pageState = reactive({
});
const list = ref([]);
const comlistPuted = computed(() => {
// const commercialArea = state.comlist.find((item) => item.commercialAreaId === state.comId);
// if (commercialArea) {
// const otherItems = state.comlist.filter((item) => item.commercialAreaId !== state.comId);
// return [commercialArea, ...otherItems];
// } else {
// return [state.areaInfo, ...state.comlist];
// }
// activeTab.value = state.areaInfo.commercialAreaId;
return state.comlist;
});
const rangeOptions = ref([
{ value: 0, text: '推荐' },
{ value: 1, text: '最热' },
@@ -177,6 +149,7 @@ function openFilter() {
pageState.search[key] = value.join(',');
}
showFilter.value = false;
console.log(pageState.search);
getJobList('refresh');
},
cancel: () => {
@@ -228,7 +201,7 @@ function changeArea(area, item) {
}
function getBusinessDistrict() {
$api.createRequest(`/app/common/commercialArea/getAllData`).then((resData) => {
$api.createRequest(`/app/common/commercialArea`).then((resData) => {
if (resData.data.length) {
state.comlist = resData.data;
state.areaInfo = resData.data[0];
@@ -268,12 +241,10 @@ function getJobList(type = 'add') {
}
pageState.total = resData.total;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
if (loadmoreRef.value && typeof loadmoreRef.value.change === 'function') {
if (rows.length < pageState.pageSize) {
loadmoreRef.value.change('noMore');
} else {
loadmoreRef.value.change('more');
}
if (rows.length < pageState.pageSize) {
loadmoreRef.value.change('noMore');
} else {
loadmoreRef.value.change('more');
}
});
}
@@ -293,49 +264,10 @@ function handleFilterConfirm(val) {
getJobList('refresh');
}
function handleOpenBusinessDistrict() {
if (commercialAreaList.value.length) {
openFilter2Col();
} else {
getBusinessDistrictList();
}
}
function getBusinessDistrictList() {
$api.createRequest(`/app/common/commercialArea`).then((resData) => {
if (resData.data.length) {
commercialAreaList.value = resData.data;
openFilter2Col();
}
});
}
function openFilter2Col() {
selectFilter2ColModel.value?.open({
data: commercialAreaList.value,
title: '商圈',
currentValue: state.comId,
maskClick: true,
success: (values) => {
pageState.search = {
...pageState.search,
latitude: values.latitude,
longitude: values.longitude,
};
state.areaInfo = values;
state.comId = values.value;
getJobList('refresh');
},
});
}
defineExpose({ loadData, handleFilterConfirm });
</script>
<style lang="stylus" scoped>
.scroll-head
width: 100%;
overflow: hidden;
.tabchecked
color: #4778EC !important
.nearby-scroll
@@ -343,30 +275,18 @@ defineExpose({ loadData, handleFilterConfirm });
.two-head
margin: 22rpx;
display: flex;
flex-direction: column
flex-wrap: no-wrap
flex-wrap: wrap
// grid-template-columns: repeat(4, 1fr);
// grid-column-gap: 10rpx;
// grid-row-gap: 24rpx;
border-radius: 17rpx 17rpx 17rpx 17rpx;
.head-all{
display: flex;
justify-content: space-between;
align-items: center
margin-bottom: 16rpx
}
.head-item-content{
display: flex
flex-wrap: nowrap
}
.head-item
padding: 0 10rpx
margin: 10rpx
white-space: nowrap
// min-width: 156rpx
min-width: 156rpx
line-height: 64rpx
text-align: center;
// width: fit-content;
width: fit-content;
font-size: 21rpx;
font-weight: 400;
font-size: 28rpx;
@@ -447,4 +367,4 @@ defineExpose({ loadData, handleFilterConfirm });
height: 26rpx;
.active
transform: rotate(180deg)
</style>
</style>

View File

@@ -292,12 +292,10 @@ function getJobList(type = 'add') {
}
pageState.total = resData.total;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
if (loadmoreRef.value && typeof loadmoreRef.value.change === 'function') {
if (rows.length < pageState.pageSize) {
loadmoreRef.value.change('noMore');
} else {
loadmoreRef.value.change('more');
}
if (rows.length < pageState.pageSize) {
loadmoreRef.value.change('noMore');
} else {
loadmoreRef.value.change('more');
}
});
}

View File

@@ -317,12 +317,10 @@ function getJobList(type = 'add') {
}
pageState.total = resData.total;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
if (loadmoreRef.value && typeof loadmoreRef.value.change === 'function') {
if (rows.length < pageState.pageSize) {
loadmoreRef.value.change('noMore');
} else {
loadmoreRef.value.change('more');
}
if (rows.length < pageState.pageSize) {
loadmoreRef.value.change('noMore');
} else {
loadmoreRef.value.change('more');
}
});
}

View File

@@ -222,12 +222,10 @@ function getJobList(type = 'add') {
}
pageState.total = resData.total;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
if (loadmoreRef.value && typeof loadmoreRef.value.change === 'function') {
if (rows.length < pageState.pageSize) {
loadmoreRef.value.change('noMore');
} else {
loadmoreRef.value.change('more');
}
if (rows.length < pageState.pageSize) {
loadmoreRef.value.change('noMore');
} else {
loadmoreRef.value.change('more');
}
});
}

View File

@@ -1,7 +1,7 @@
<template>
<AppLayout title="附近" :use-scroll-view="false" :show-bg-image="false">
<AppLayout title="附近" :use-scroll-view="false">
<template #headerleft>
<view class="btnback">
<view class="btn">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
</template>
@@ -83,16 +83,12 @@ function handleTabChange(index) {
</script>
<style lang="stylus" scoped>
.btnback{
width: 64rpx;
height: 64rpx;
}
.btn {
display: flex;
justify-content: space-between;
align-items: center;
width: 52rpx;
height: 52rpx;
width: 60rpx;
height: 60rpx;
}
image {
height: 100%;

View File

@@ -112,7 +112,7 @@ const searchParams = ref({});
const pageSize = ref(10);
const { list, loading, refresh, loadMore } = usePagination(
(params) => $api.createRequest('/app/job/littleVideo', params, 'GET', true),
(params) => $api.createRequest('/app/job/littleVideo', params),
dataToImg, // 转换函数
{
pageSize: pageSize,
@@ -122,9 +122,6 @@ const { list, loading, refresh, loadMore } = usePagination(
onBeforeRequest: () => {
loadmoreRef.value?.change('loading');
},
onAfterRequest: () => {
loadmoreRef.value?.change('more');
},
}
);
@@ -247,7 +244,7 @@ function getJobList(type = 'add') {
jobTitle: searchValue.value,
};
$api.createRequest('/app/job/list', params, 'GET', true).then((resData) => {
$api.createRequest('/app/job/list', params).then((resData) => {
const { rows, total } = resData;
if (type === 'add') {
const str = pageState.pageSize * (pageState.page - 1);

BIN
static/.DS_Store vendored Normal file

Binary file not shown.

BIN
static/font/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
static/gif/.DS_Store vendored Normal file

Binary file not shown.

BIN
static/icon/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,36 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>找工作,用 AI 更高效|青岛市智能求职平台</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- 微信分享卡片标签(动态填充) -->
<meta property="og:title" content="找工作,用 AI 更高效|青岛市智能求职平台" />
<meta property="og:description" content="融合海量岗位、智能简历匹配、竞争力分析,助你精准锁定理想职位!" />
<meta property="og:image" content="https://qd.zhaopinzao8dian.com/file/csn/qd_shareLogo.jpg" />
<meta property="og:url" content="https://qd.zhaopinzao8dian.com" />
<meta name="description" content="融合海量岗位、智能简历匹配、竞争力分析,助你精准锁定理想职位!">
<script>
const params = new URLSearchParams(location.search)
const jobId = params.get('jobId')
// document.querySelector('meta[property="og:url"]').setAttribute('content', location.href)
// 延迟跳转到 Vue 页面
setTimeout(() => {
if (jobId) {
window.location.href = `/#/packageA/pages/post/post?jobId=${jobId}`
} else {
window.location.href = '/#/'
}
}, 300)
// 测试使用 分享等形式打开
// http://localhost:5173/app/static/share.html?jobId=MTE4MzQ4NTE4
// http://localhost:5173/app/static/share.html?jobId=MTE4MzQ4NTE4&_t=1752221704007#/
</script>
</head>
<body>
<p>正在加载中...</p>
</body>
</html>

View File

@@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1753846081356" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1008" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M256 384c38.272 0 72.576 16.768 96 43.392C332.16 449.92 320 479.552 320 512c0 32.448 12.096 62.08 32 84.608A128 128 0 1 1 256 384zM512 384c38.272 0 72.576 16.768 96 43.392C588.16 449.92 576 479.552 576 512c0 32.448 12.096 62.08 32 84.608A128 128 0 1 1 512 384z" fill="#256BFA" p-id="1009"></path><path d="M768 512m-128 0a128 128 0 1 0 256 0 128 128 0 1 0-256 0Z" fill="#256BFA" p-id="1010"></path></svg>

Before

Width:  |  Height:  |  Size: 736 B

BIN
static/tabbar/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
stores/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -15,22 +15,6 @@ import {
// 控制消息
export const useReadMsg = defineStore('readMsg', () => {
const msgList = ref([])
const badges = ref([{
count: 0
},
{
count: 0
},
{
count: 0
},
{
count: 0
},
{
count: 0
},
])
// 计算总未读数量,基于 notReadCount 字段
const unreadCount = computed(() =>
@@ -46,22 +30,14 @@ export const useReadMsg = defineStore('readMsg', () => {
// 设置 TabBar 角标
function updateTabBarBadge() {
const count = unreadCount.value
const index = 3
const countVal = count > 99 ? '99+' : String(count)
if (count === 0) {
uni.removeTabBarBadge({
index
index: 3
}) // 替换为你消息页面的 TabBar index
badges.value[index] = {
count: 0
}
} else {
badges.value[index] = {
count: countVal
}
uni.setTabBarBadge({
index,
text: countVal
index: 3,
text: count > 99 ? '99+' : String(count)
})
}
}
@@ -100,7 +76,6 @@ export const useReadMsg = defineStore('readMsg', () => {
}
return {
badges,
msgList,
unreadMsgList,
unreadCount,

View File

@@ -18,9 +18,6 @@ import {
UUID
} from '../lib/uuid-min';
import config from '../config';
import {
clearJobMoreMap
} from '@/utils/markdownParser';
const useChatGroupDBStore = defineStore("messageGroup", () => {
const tableName = ref('messageGroup')
@@ -60,7 +57,6 @@ const useChatGroupDBStore = defineStore("messageGroup", () => {
if (!baseDB.isDBReady) await baseDB.initDB();
chatSessionID.value = sessionId
const list = await baseDB.db.queryByField(massageName.value, 'parentGroupId', sessionId);
clearJobMoreMap() // 清空对话 加载更多参数
if (list.length) {
console.log('本地数据库存在该对话数据', list)
messages.value = list
@@ -180,7 +176,7 @@ const useChatGroupDBStore = defineStore("messageGroup", () => {
resolve();
}
$api.streamRequest('/app/chat/chat', params, onDataReceived, onError, onComplete);
$api.streamRequest('/chat', params, onDataReceived, onError, onComplete);
} catch (err) {
console.log(err);
reject(err);
@@ -230,7 +226,7 @@ const useChatGroupDBStore = defineStore("messageGroup", () => {
// 云端数据
function getHistory() {
$api.chatRequest('/app/chat/getHistory').then((res) => {
$api.chatRequest('/getHistory').then((res) => {
if (!res.data.list.length) return
let tabel = parseHistory(res.data.list)
if (tabel && tabel.length) {
@@ -250,7 +246,7 @@ const useChatGroupDBStore = defineStore("messageGroup", () => {
const params = {
sessionId: chatSessionID.value
}
$api.chatRequest('/app/chat/detail', params, 'GET', loading).then((res) => {
$api.chatRequest('/detail', params, 'GET', loading).then((res) => {
let list = parseHistoryDetail(res.data.list, chatSessionID.value)
if (list.length) {
baseDB.db.add(massageName.value, list).then((ids) => {

BIN
uni_modules/.DS_Store vendored Normal file

Binary file not shown.

BIN
unpackage/.DS_Store vendored Normal file

Binary file not shown.

BIN
unpackage/dist/.DS_Store vendored Normal file

Binary file not shown.

BIN
unpackage/dist/build/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,8 @@
{
"hash": "ab0eb594",
"configHash": "554cced5",
"lockfileHash": "5d26acb0",
"browserHash": "86a09ddc",
"optimized": {},
"chunks": {}
}

View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

View File

@@ -0,0 +1,36 @@
{
"description": "项目配置文件。",
"packOptions": {
"ignore": []
},
"setting": {
"urlCheck": false,
"es6": true,
"postcss": true,
"minified": true,
"newFeature": true,
"bigPackageSizeSupport": true
},
"compileType": "miniprogram",
"libVersion": "",
"appid": "touristappid",
"projectname": "qingdao-employment-service",
"condition": {
"search": {
"current": -1,
"list": []
},
"conversation": {
"current": -1,
"list": []
},
"game": {
"current": -1,
"list": []
},
"miniprogram": {
"current": -1,
"list": []
}
}
}

View File

@@ -0,0 +1,8 @@
{
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
"projectname": "qingdao-employment-service",
"setting": {
"compileHotReLoad": true,
"autoAudits": false
}
}

View File

@@ -4,7 +4,6 @@ import parseHtml from '@/lib/html-parser.js';
// import DOMPurify from '@/lib/dompurify@3.2.4es.js';
export let codeDataList = []
export let jobMoreMap = new Map()
const md = new MarkdownIt({
html: true, // 允许 HTML 标签
@@ -17,31 +16,23 @@ const md = new MarkdownIt({
highlight: function(str, lang) {
if (lang === 'job-json') {
const result = safeExtractJson(str);
if (result) { // json解析成功
const jobId = result.appJobUrl.split('jobId=')[1]
let domContext = `
<a class="custom-card" data-job-id="${jobId}">
<div class="card-title">
<span class="title-text" >${result.jobTitle}</span>
<div class="card-salary">${result.salary}</div>
const jobId = result.appJobUrl.split('jobId=')[1]
return `
<a class="custom-card" data-job-id="${jobId}">
<div class="card-title">
<span class="title-text" >${result.jobTitle}</span>
<div class="card-salary">${result.salary}</div>
</div>
<div class="card-company">${result.location}·${result.companyName}</div>
<div class="card-info">
<div class="info-item">
<div class="card-tag">${result.education}</div>
<div class="card-tag">${result.experience}</div>
</div>
<div class="card-company">${result.location}·${result.companyName}</div>
<div class="card-info">
<div class="info-item">
<div class="card-tag">${result.education}</div>
<div class="card-tag">${result.experience}</div>
</div>
<div class="info-item">查看详情<div class="position-nav"></div></div>
</div>
</a>
`
if (result.data) {
jobMoreMap.set(jobId, result.data)
domContext +=
`<a class="custom-more" data-job-id="${jobId}">查看更多岗位<div class="more-icon"></div></a>`
}
return domContext
}
<div class="info-item">查看详情<div class="position-nav"></div></div>
</div>
</a>
`
}
// <div class="card-tag">${result.location}</div>
// <div class="info-item">${result.salary}</div>
@@ -77,54 +68,18 @@ const md = new MarkdownIt({
}
})
function extractFirstJson(text) {
let stack = [];
let startIndex = -1;
let endIndex = -1;
for (let i = 0; i < text.length; i++) {
const char = text[i];
if (char === '{') {
if (stack.length === 0) startIndex = i; // 记录第一个 '{' 的位置
stack.push(char);
} else if (char === '}') {
stack.pop();
if (stack.length === 0) {
endIndex = i; // 找到配对的 '}'
break;
}
}
}
if (startIndex !== -1 && endIndex !== -1) {
const jsonString = text.slice(startIndex, endIndex + 1);
try {
const jsonObject = JSON.parse(jsonString);
return jsonObject;
} catch (e) {
return null; // 如果不是有效的 JSON
}
}
return null; // 如果没有找到有效的 JSON 对象
}
function safeExtractJson(text) {
try {
const jsonObject = extractFirstJson(text);
return jsonObject
const match = text.match(/\{[\s\S]*?\}/); // 提取第一个完整的 JSON 块
if (match) {
return JSON.parse(match[0]);
}
} catch (e) {
console.error('JSON 解析失败:', e);
}
return null;
}
export function clearJobMoreMap() { // 切换对话清空
jobMoreMap.clear()
}
export function parseMarkdown(content) {
if (!content) {
return //处理特殊情况,比如网络异常导致的响应的 content 的值为空

View File

@@ -1,24 +1,69 @@
import config from "@/config.js"
import {
sm2_Decrypt,
sm2_Encrypt
} from '@/common/globalFunction';
import useUserStore from '@/stores/useUserStore';
import {
sm4Decrypt,
sm4Encrypt
} from '../common/globalFunction';
export function request({
url,
method = 'GET',
data = {},
load = false,
header = {}
} = {}) {
return new Promise((resolve, reject) => {
if (load) {
uni.showLoading({
title: '请稍候',
mask: true
});
}
let Authorization = ''
if (useUserStore().token) {
Authorization = `${useUserStore().userInfo.token}${useUserStore().token}`
}
uni.request({
url: config.baseUrl + url,
method,
data: data,
header: {
'Authorization': Authorization || '',
},
success: resData => {
// 响应拦截
if (resData.statusCode === 200) {
const {
code,
msg
} = resData.data
if (code === 200) {
resolve(resData.data)
return
}
uni.showToast({
title: msg,
icon: 'none'
})
}
if (resData.data?.code === 401 || resData.data?.code === 402) {
useUserStore().logOut()
uni.showToast({
title: '登录过期,请重新登录',
icon: 'none'
})
return
}
const err = new Error('请求出现异常,请联系工作人员')
err.error = resData
reject(err)
},
fail: err => reject(err),
complete() {
if (load) {
uni.hideLoading();
}
}
})
})
}
const needToEncrypt = [
["post", "/app/login"],
["get", "/app/user/resume"],
["post", "/app/user/resume"],
["post", "/app/user/experience/edit"],
["post", "/app/user/experience/delete"],
["get", "/app/user/experience/getSingle/{value}"],
["get", "/app/user/experience/list"]
]
/**
* @param url String请求的地址默认none
@@ -42,44 +87,15 @@ export function createRequest(url, data = {}, method = 'GET', loading = false, h
const header = headers || {};
header["Authorization"] = encodeURIComponent(Authorization);
// ------------------------------------------------------------------
// 检查当前请求是否需要加密
const isEncrypt = needToEncrypt.some(item => {
const matchMethod = item[0].toLowerCase() === method.toLowerCase();
const matchUrl = item[1].includes('{') ?
url.startsWith(item[1].split('/{')[0]) // 检查动态路径的前缀
:
item[1] === url; // 检查静态路径
return matchMethod && matchUrl;
});
let requestData = data;
if (isEncrypt) {
const jsonData = JSON.stringify(data);
const encryptedBody = sm4Encrypt(config.sm4Config.key, jsonData);
requestData = {
encrypted: true,
encryptedData: encryptedBody,
timestamp: Date.now()
};
}
// ------------------------------------------------------------------
return new Promise((resolve, reject) => {
uni.request({
url: config.baseUrl + url,
method: method,
data: requestData,
data: data,
header,
success: resData => {
// 响应拦截
if (resData.statusCode === 200) {
if (resData.data.encrypted) {
const decryptedData = sm4Decrypt(config
.sm4Config.key, resData.data.encryptedData)
resData.data = JSON.parse(decryptedData)
}
const {
code,
msg

View File

@@ -17,7 +17,7 @@ export default function StreamRequest(url, data = {}, onDataReceived, onError, o
};
return new Promise(async (resolve, reject) => {
try {
const response = await fetch(config.baseUrl + url, {
const response = await fetch(config.StreamBaseURl + url, {
method: "POST",
headers,
body: JSON.stringify(data)
@@ -46,7 +46,6 @@ export default function StreamRequest(url, data = {}, onDataReceived, onError, o
let lines = buffer.split("\n");
buffer = lines.pop(); // 可能是不完整的 JSON 片段,留待下次解析
for (let line of lines) {
line = line.slice(5).trim()
if (line.startsWith("data: ")) {
const jsonData = line.slice(6).trim();
if (jsonData === "[DONE]") {
@@ -105,7 +104,7 @@ export function chatRequest(url, data = {}, method = 'GET', loading = false, hea
header["Authorization"] = encodeURIComponent(Authorization);
return new Promise((resolve, reject) => {
uni.request({
url: config.baseUrl + url,
url: config.StreamBaseURl + url,
method: method,
data: data,
header,

63
utils/wechatShare.js Normal file
View File

@@ -0,0 +1,63 @@
import wx from 'weixin-js-sdk'
import config from "@/config.js"
export function setupWechatShare({
title,
desc,
link,
imgUrl
}) {
// 通过后端接口获取签名(必须)
fetch(`${config.baseUrl}/wechat-signature?url=${encodeURIComponent(location.href.split('#')[0])}`)
.then(res => res.json())
.then(({
appId,
timestamp,
nonceStr,
signature
}) => {
wx.config({
debug: false,
appId,
timestamp,
nonceStr,
signature,
jsApiList: ['updateAppMessageShareData', 'updateTimelineShareData']
})
wx.ready(() => {
// 分享给好友
wx.updateAppMessageShareData({
title,
desc,
link,
imgUrl,
success: () => {
console.log('分享配置成功')
}
})
// 分享到朋友圈
wx.updateTimelineShareData({
title,
link,
imgUrl,
success: () => {
console.log('朋友圈分享配置成功')
}
})
})
})
}
// 使用
// import { setupWechatShare } from '@/utils/wechatShare.js'
// onMounted(() => {
// setupWechatShare({
// title: '职位推荐:高级前端工程师',
// desc: '某知名互联网公司年薪40W点击查看详情',
// link: location.href,
// imgUrl: 'https://yourcdn.com/job-thumbnail.png'
// })
// })