flat: 暂存
This commit is contained in:
BIN
components/.DS_Store
vendored
BIN
components/.DS_Store
vendored
Binary file not shown.
@@ -12,7 +12,7 @@
|
||||
<view class="header-btnLf">
|
||||
<slot name="headerleft"></slot>
|
||||
</view>
|
||||
<view class="header-title">
|
||||
<view class="header-title" :style="{ color: titleColor }">
|
||||
<view>{{ title }}</view>
|
||||
<view v-show="subTitle" class="subtitle-text">{{ subTitle }}</view>
|
||||
</view>
|
||||
@@ -45,12 +45,15 @@
|
||||
<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,
|
||||
@@ -110,11 +113,13 @@ 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;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<view class="empty" :style="{ background: bgcolor, marginTop: mrTop + 'rpx' }">
|
||||
<view class="ty_content" :style="{ paddingTop: pdTop + 'rpx' }">
|
||||
<view class="content_top btn-shaky">
|
||||
<view class="content_top">
|
||||
<!-- <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>
|
||||
|
||||
@@ -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: 32rpx;
|
||||
font-size: 30rpx;
|
||||
color: #333333;
|
||||
}
|
||||
.salary{
|
||||
font-family: DIN-Medium;
|
||||
font-weight: 500;
|
||||
font-size: 28rpx;
|
||||
font-size: 26rpx;
|
||||
color: #4C6EFB;
|
||||
white-space: nowrap
|
||||
line-height: 48rpx
|
||||
|
||||
@@ -149,6 +149,7 @@ const cleanup = () => {
|
||||
Object.keys(selectedValues).forEach((key) => {
|
||||
delete selectedValues[key];
|
||||
});
|
||||
count.value = 0;
|
||||
};
|
||||
|
||||
const scrollTo = (key) => {
|
||||
@@ -161,6 +162,7 @@ function getoptions() {
|
||||
getTransformChildren('experience', '工作经验'),
|
||||
getTransformChildren('scale', '公司规模'),
|
||||
];
|
||||
console.log(arr);
|
||||
if (area.value) {
|
||||
arr.push(getTransformChildren('area', '区域'));
|
||||
}
|
||||
|
||||
345
components/selectFilter/selectFilter2col.vue
Normal file
345
components/selectFilter/selectFilter2col.vue
Normal file
@@ -0,0 +1,345 @@
|
||||
<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>
|
||||
@@ -1,136 +0,0 @@
|
||||
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');
|
||||
}
|
||||
157
hook/useSystemPlayer.js
Normal file
157
hook/useSystemPlayer.js
Normal file
@@ -0,0 +1,157 @@
|
||||
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.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');
|
||||
}
|
||||
203
hook/useSystemSpeechReader.js
Normal file
203
hook/useSystemSpeechReader.js
Normal file
@@ -0,0 +1,203 @@
|
||||
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, // 导出支持状态
|
||||
};
|
||||
}
|
||||
@@ -23,6 +23,8 @@
|
||||
var vConsole = new window.VConsole();
|
||||
vConsole.destroy();
|
||||
</script> -->
|
||||
<!-- 爱山东jssdk -->
|
||||
<script type="text/javascript" src="https://isdapp.shandong.gov.cn/jmopen/jssdk/index.js"></script>
|
||||
</head>
|
||||
<!-- <body> -->
|
||||
<div id="app"><!--app-html--></div>
|
||||
|
||||
@@ -1,75 +1,116 @@
|
||||
<template>
|
||||
<view class="mys-container">
|
||||
<!-- 个人信息 -->
|
||||
<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>
|
||||
<AppLayout title="我的简历" title-color="#FFFFFF" back-gorund-color="#F4F4F4">
|
||||
<template #headerleft>
|
||||
<view class="btn">
|
||||
<image src="@/static/icon/back.png" @click="navBack"></image>
|
||||
</view>
|
||||
</template>
|
||||
<view class="mys-container">
|
||||
<!-- 个人信息 -->
|
||||
<view class="card-top" style="margin-top: 12rpx; padding: 0">
|
||||
<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>
|
||||
<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="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>
|
||||
<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="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>期望薪资:</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 class="subName">{{ userInfo.phone }}</view>
|
||||
</view>
|
||||
<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 class="card-top" style="margin-top: 24rpx">
|
||||
<view class="mys-info" style="padding: 0">
|
||||
<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="mar_top10" v-for="item in userInfo.workExp" :key="item.id">
|
||||
<view class="fl_box fl_justbet mar_top10">
|
||||
<text class="fs_16">{{ item.company }}</text>
|
||||
<text class="datetext">{{ item.startTime }}--{{ item.endTime || '至今' }}</text>
|
||||
</view>
|
||||
<view class="mys-text">
|
||||
<text class="color_333333 fs_14">{{ item.position }}</text>
|
||||
<text></text>
|
||||
</view>
|
||||
<view class="mys-text color_333333">
|
||||
<text>{{ item.duty }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 求职期望 -->
|
||||
<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>
|
||||
<template #footer>
|
||||
<view class="footer-container">
|
||||
<view class="footer-button">上传简历</view>
|
||||
</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>
|
||||
</template>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, inject, watch, ref, onMounted, computed } from 'vue';
|
||||
const { $api, navTo } = inject('globalFunction');
|
||||
const { $api, navTo, navBack } = inject('globalFunction');
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import useUserStore from '@/stores/useUserStore';
|
||||
@@ -80,20 +121,52 @@ 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{
|
||||
.card-top{
|
||||
background: #FFFFFF;
|
||||
margin: 0 28rpx;
|
||||
border-radius: 8rpx;
|
||||
padding: 24rpx
|
||||
}
|
||||
.mys-tops{
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
padding: 52rpx 48rpx
|
||||
padding: 38rpx 44rpx
|
||||
.tops-left{
|
||||
.name{
|
||||
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 44rpx;
|
||||
font-size: 36rpx;
|
||||
color: #333333;
|
||||
display: flex
|
||||
align-items: center
|
||||
@@ -108,8 +181,8 @@ image{
|
||||
.subName{
|
||||
margin-top: 12rpx
|
||||
font-weight: 400;
|
||||
font-size: 32rpx;
|
||||
color: #333333;
|
||||
font-size: 26rpx;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -143,7 +216,7 @@ image{
|
||||
.mys-h4{
|
||||
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 32rpx;
|
||||
font-size: 30rpx;
|
||||
color: #000000;
|
||||
margin-bottom: 8rpx
|
||||
display: flex;
|
||||
@@ -155,10 +228,15 @@ image{
|
||||
height: 40rpx
|
||||
}
|
||||
}
|
||||
.datetext{
|
||||
font-weight: 400;
|
||||
font-size: 26rpx;
|
||||
color: #999999;
|
||||
}
|
||||
.mys-text{
|
||||
font-weight: 400;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
font-size: 26rpx;
|
||||
color: #999999;
|
||||
margin-top: 16rpx
|
||||
}
|
||||
.mys-list{
|
||||
|
||||
@@ -106,7 +106,8 @@
|
||||
"path": "pages/myResume/myResume",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的简历",
|
||||
"navigationBarBackgroundColor": "#FFFFFF"
|
||||
"navigationBarBackgroundColor": "#FFFFFF",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}, {
|
||||
"path": "pages/Intendedposition/Intendedposition",
|
||||
|
||||
@@ -113,7 +113,8 @@
|
||||
</view>
|
||||
</view>
|
||||
<view class="chat-item self" v-if="isRecording">
|
||||
<view class="message">{{ recognizedText }} {{ lastFinalText }}</view>
|
||||
<!-- <view class="message">{{ recognizedText }} {{ lastFinalText }}</view> -->
|
||||
<view class="message">{{ recognizedText }}</view>
|
||||
</view>
|
||||
<view v-if="isTyping" class="self">
|
||||
<text class="message msg-loading">
|
||||
@@ -268,14 +269,18 @@ import AudioWave from './AudioWave.vue';
|
||||
import WaveDisplay from './WaveDisplay.vue';
|
||||
import FileIcon from './fileIcon.vue';
|
||||
import FileText from './fileText.vue';
|
||||
import { useAudioRecorder } from '@/hook/useRealtimeRecorder.js';
|
||||
import { useTTSPlayer } from '@/hook/useTTSPlayer.js';
|
||||
// 系统功能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';
|
||||
// 全局
|
||||
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,
|
||||
@@ -286,7 +291,7 @@ const {
|
||||
recognizedText,
|
||||
lastFinalText,
|
||||
} = useAudioRecorder();
|
||||
|
||||
// 语音合成
|
||||
const { speak, pause, resume, isSpeaking, isPaused, cancelAudio } = useTTSPlayer(config.speechSynthesis);
|
||||
|
||||
// state
|
||||
@@ -629,6 +634,7 @@ function readMarkdown(value, index) {
|
||||
if (isPaused.value) {
|
||||
resume();
|
||||
} else {
|
||||
console.log(value, speechIndex.value, index, isPaused.value)
|
||||
speak(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')">
|
||||
@@ -563,7 +563,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 +703,7 @@ defineExpose({ loadData });
|
||||
text-align: left;
|
||||
word-break:break-all
|
||||
font-weight: 500;
|
||||
font-size: 32rpx;
|
||||
font-size: 30rpx;
|
||||
color: #333333;
|
||||
margin-top: 10rpx
|
||||
.falls-card-pay
|
||||
|
||||
@@ -327,7 +327,8 @@ function complete() {
|
||||
width: 100%;
|
||||
height: calc(100vh - var(--window-top) - var(--status-bar-height) - var(--window-bottom));
|
||||
position: fixed;
|
||||
background: url('@/static/icon/background2.png') 0 0 no-repeat;
|
||||
// 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-size: 100% 728rpx;
|
||||
display: flex;
|
||||
flex-direction: column
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
<template>
|
||||
<scroll-view :scroll-y="true" class="nearby-scroll" @scrolltolower="scrollBottom">
|
||||
<view class="two-head">
|
||||
<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 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>
|
||||
<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>
|
||||
@@ -70,11 +81,12 @@
|
||||
</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 } from 'vue';
|
||||
import { reactive, inject, watch, ref, onMounted, onBeforeUnmount, computed } from 'vue';
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||
const { $api, navTo, debounce, customSystem } = inject('globalFunction');
|
||||
import { storeToRefs } from 'pinia';
|
||||
@@ -87,6 +99,7 @@ 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({
|
||||
@@ -96,12 +109,15 @@ 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,
|
||||
@@ -114,6 +130,18 @@ 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: '最热' },
|
||||
@@ -149,7 +177,6 @@ function openFilter() {
|
||||
pageState.search[key] = value.join(',');
|
||||
}
|
||||
showFilter.value = false;
|
||||
console.log(pageState.search);
|
||||
getJobList('refresh');
|
||||
},
|
||||
cancel: () => {
|
||||
@@ -201,7 +228,7 @@ function changeArea(area, item) {
|
||||
}
|
||||
|
||||
function getBusinessDistrict() {
|
||||
$api.createRequest(`/app/common/commercialArea`).then((resData) => {
|
||||
$api.createRequest(`/app/common/commercialArea/getAllData`).then((resData) => {
|
||||
if (resData.data.length) {
|
||||
state.comlist = resData.data;
|
||||
state.areaInfo = resData.data[0];
|
||||
@@ -266,10 +293,49 @@ 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
|
||||
@@ -277,18 +343,30 @@ defineExpose({ loadData, handleFilterConfirm });
|
||||
.two-head
|
||||
margin: 22rpx;
|
||||
display: flex;
|
||||
flex-wrap: wrap
|
||||
flex-direction: column
|
||||
flex-wrap: no-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;
|
||||
@@ -369,4 +447,4 @@ defineExpose({ loadData, handleFilterConfirm });
|
||||
height: 26rpx;
|
||||
.active
|
||||
transform: rotate(180deg)
|
||||
</style>
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<AppLayout title="附近" :use-scroll-view="false">
|
||||
<AppLayout title="附近" :use-scroll-view="false" :show-bg-image="false">
|
||||
<template #headerleft>
|
||||
<view class="btnback">
|
||||
<image src="@/static/icon/back.png" @click="navBack"></image>
|
||||
|
||||
BIN
static/.DS_Store
vendored
BIN
static/.DS_Store
vendored
Binary file not shown.
BIN
static/icon/.DS_Store
vendored
BIN
static/icon/.DS_Store
vendored
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 34 KiB |
BIN
static/icon/background3.png
Normal file
BIN
static/icon/background3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.9 KiB |
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"hash": "5cf4c8aa",
|
||||
"configHash": "0279f4bc",
|
||||
"hash": "8cb75570",
|
||||
"configHash": "01aaac4c",
|
||||
"lockfileHash": "5d26acb0",
|
||||
"browserHash": "5520a35c",
|
||||
"browserHash": "4b09b340",
|
||||
"optimized": {},
|
||||
"chunks": {}
|
||||
}
|
||||
Reference in New Issue
Block a user