Merge remote-tracking branch 'origin/bin' into media-css

This commit is contained in:
2025-12-09 11:07:41 +08:00
110 changed files with 2160 additions and 954 deletions

View File

@@ -14,8 +14,10 @@ onLaunch((options) => {
useDictStore().getDictData();
try {
getUserInfo();
useUserStore().changMiniProgramAppStatus(false);
} catch {
console.log('不是爱山东平台,使用测试登陆');
useUserStore().changMiniProgramAppStatus(true);
useUserStore().initSeesionId(); //更新
let token = uni.getStorageSync('token') || ''; // 同步获取 缓存信息
if (token) {

View File

@@ -33,12 +33,17 @@ html {
overflow-x: hidden;
}
li {
list-style: none;
}
/* 布局调整 */
/* 点击动效 */
/* 缩小 */
.button-click {
transition: transform 0.1s ease;
/* transition: transform 0.1s ease; */
transition: transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.button-click:active {
@@ -67,7 +72,8 @@ html {
}
.btn-feel {
transition: transform 0.15s ease;
transition: transform 0.5s ease;
/* transition: transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); */
transform-style: preserve-3d;
}
@@ -84,7 +90,8 @@ html {
border: none;
border-radius: 6px;
cursor: pointer;
transition: transform 0.1s ease, box-shadow 0.1s ease;
/* transition: transform 0.4s ease, box-shadow 0.1s ease; */
transition: transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
/* box-shadow: 0 4px 0 #2C3E50; */
}
@@ -473,3 +480,158 @@ html {
.height-100 {
height: 100%;
}
.pointEveNone {
pointer-events: none;
}
/*
*
* TransitionGroup 动画
* stagger 错峰
* fade-up-stagger --i 淡入上滑(经典错峰)
* pop-in-stagger --i 旋转缩放弹入(活泼)
* slide-left-stagger 从左侧滑入(消息流风格)
* blur-fade-stagger --i 渐显 + 模糊(毛玻璃感)
* bounce-up-stagger 从底部弹跳入场(物理感)
*
*/
/* 淡入上滑(经典错峰) */
.fade-up-stagger-enter-active {
transition: all 0.4s cubic-bezier(0.18, 0.89, 0.32, 1.28);
transition-delay: calc(var(--i) * 0.1s);
}
.fade-up-stagger-enter-from {
opacity: 0;
transform: translateY(20px);
}
.fade-up-stagger-leave-active {
transition: opacity 0.25s ease;
}
.fade-up-stagger-leave-to {
opacity: 0;
}
/* */
.pop-in-stagger-enter-active {
transition: all 0.45s cubic-bezier(0.68, -0.55, 0.27, 1.55);
transition-delay: calc(var(--i) * 0.08s);
}
.pop-in-stagger-enter-from {
opacity: 0;
transform: scale(0.7) rotate(-10deg);
}
.pop-in-stagger-leave-active {
transition: all 0.2s ease;
}
.pop-in-stagger-leave-to {
opacity: 0;
transform: scale(0.9) rotate(5deg);
}
.slide-left-stagger-enter-active {
transition: all 0.35s ease-out;
transition-delay: calc(var(--i) * 0.07s);
}
.slide-left-stagger-enter-from {
opacity: 0;
transform: translateX(-100%);
}
.slide-left-stagger-leave-active {
transition: all 0.25s ease;
}
.slide-left-stagger-leave-to {
opacity: 0;
transform: translateX(50%);
}
/* 错峰动画 */
.stagger-enter-active {
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
transition-delay: calc(var(--i) * 0.42s);
/* 关键120ms 间隔 */
}
.stagger-enter-from {
opacity: 0;
transform: translateY(30px) scale(0.95);
/* 更大位移 + 轻微缩放 */
}
.stagger-leave-active {
transition: all 0.25s ease;
}
.stagger-leave-to {
opacity: 0;
transform: translateX(20px);
}
.stagger-move {
transition: transform 0.4s ease;
}
.blur-fade-stagger-enter-active {
transition: all 0.5s ease;
transition-delay: calc(var(--i) * 0.09s);
}
.blur-fade-stagger-enter-from {
opacity: 0;
filter: blur(8px);
transform: scale(1.02);
}
.blur-fade-stagger-leave-active {
transition: all 0.2s ease;
}
.blur-fade-stagger-leave-to {
opacity: 0;
filter: blur(4px);
}
.bounce-up-stagger-enter-active {
animation: stagger-bounce 0.6s forwards;
animation-delay: calc(var(--i) * 0.12s);
}
.bounce-up-stagger-leave-active {
transition: all 0.25s ease;
}
.bounce-up-stagger-leave-to {
opacity: 0;
transform: translateY(30px);
}
@keyframes stagger-bounce {
0% {
opacity: 0;
transform: translateY(100px) scale(0.8);
}
60% {
transform: translateY(-10px) scale(1.05);
}
80% {
transform: translateY(5px) scale(0.98);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}

View File

@@ -285,9 +285,9 @@ function deg2rad(deg) {
}
function vacanciesTo(vacancies) {
if(!vacancies){
return '不限人数'
}else if (vacancies >= 0) {
if (vacancies === null) {
return '-人'
} else if (vacancies >= 0) {
return vacancies + "人"
} else {
return '不限人数'

View File

@@ -1,48 +1,52 @@
<template>
<view v-for="job in listData" :key="job.id">
<view class="cards" @click="nextDetail(job)">
<view class="card-company">
<text class="company line_1">{{ job.gsmc }}</text>
</view>
<view class="card-bottom" >
<view class="fl_box" >
<!-- <dict-tree-Label class="mar_ri10" dictType="industry" :value="job.industry"></dict-tree-Label>
<transition-group name="blur-fade-stagger" tag="view">
<view v-for="(job, index) in dataSource" :key="job.id" :style="{ '--i': 2 }">
<view class="cards" @click="nextDetail(job)">
<view class="card-company">
<text class="company line_1">{{ job.gsmc }}</text>
</view>
<view class="card-bottom">
<view class="fl_box fs_14">
<!-- <dict-tree-Label class="mar_ri10" dictType="industry" :value="job.industry"></dict-tree-Label>
<dict-Label dictType="scale" :value="job.scale"></dict-Label> -->
<view>{{job.gsxy}}</view>
<view>{{ job.gsxy }}</view>
</view>
<view class="ris">
<text class="fs_14">
在招职位·
<text class="color_256BFA">{{ job.zzgwsl || '-' }}</text>
</text>
</view>
</view>
<view class="ris" >
<text>
在招职位·
<text class="color_256BFA">{{ job.zzgwsl || '-' }}</text>
</text>
</view>
</view>
<view class="card-tags">
<view class="tag" v-if="job.nature">
<dict-Label dictType="nature" :value="job.nature"></dict-Label>
<dict-Label dictType="nature" :value="job.nature"></dict-Label>
</view>
<view class="tag">
{{ vacanciesTo(job.vacancies) }}
</view>
<view class="tag" v-if="job.qyxz">
{{job.qyxz}}
<view class="card-tags">
<view class="tag" v-if="job.nature">
<dict-Label dictType="nature" :value="job.nature"></dict-Label>
<dict-Label dictType="nature" :value="job.nature"></dict-Label>
</view>
<view class="tag">
{{ vacanciesTo(job.vacancies) }}
</view>
<view class="tag" v-if="job.qyxz">
{{ job.qyxz }}
</view>
</view>
</view>
</view>
</view>
</transition-group>
</template>
<script setup>
import { inject, computed, toRaw } from 'vue';
import { inject, computed, toRaw, watch, ref, nextTick } from 'vue';
const { insertSortData, navTo, vacanciesTo } = inject('globalFunction');
import { useRecommedIndexedDBStore } from '@/stores/useRecommedIndexedDBStore.js';
const recommedIndexDb = useRecommedIndexedDBStore();
const dataSource = ref([]);
const props = defineProps({
list: {
type: Array,
default: '标题',
default: () => [],
},
longitude: {
type: Number,
@@ -61,13 +65,28 @@ const props = defineProps({
default: '',
},
});
const listData = computed(() => {
return props.list;
});
const processedIds = new Set();
watch(
() => props.list,
(newList) => {
if (!Array.isArray(newList)) return;
const newItems = newList.filter((item) => !processedIds.has(item.id));
if (newItems.length === 0) return;
newItems.forEach((item) => processedIds.add(item.id));
const delay = 50;
newItems.forEach((item, index) => {
setTimeout(() => {
dataSource.value.push(item);
}, index * delay);
});
},
{ immediate: true }
);
function nextDetail(company) {
navTo(`/packageA/pages/UnitDetails/UnitDetails?companyId=${company.gsID}&companyName=${company.gsmc}&zphId=${props.zphId}&dataType=2`);
navTo(
`/packageA/pages/UnitDetails/UnitDetails?companyId=${company.gsID}&companyName=${company.gsmc}&zphId=${props.zphId}&dataType=2`
);
}
</script>

View File

@@ -1,48 +1,54 @@
<template>
<view v-for="job in listData" :key="job.id">
<view v-if="!job.isTitle" class="cards" @click="nextDetail(job)">
<view class="card-company">
<text class="company">{{ job.jobTitle }}</text>
<view class="salary">
<Salary-Expectation :max-salary="job.maxSalary" :min-salary="job.minSalary"></Salary-Expectation>
<transition-group name="blur-fade-stagger" tag="view">
<view v-for="job in dataSource" :key="job.id || job.jobId" :style="{ '--i': 2 }">
<view v-if="!job.isTitle" class="cards" @click="nextDetail(job)">
<view class="card-company">
<text class="company">{{ job.jobTitle }}</text>
<view class="salary">
<Salary-Expectation
:max-salary="job.maxSalary"
:min-salary="job.minSalary"
></Salary-Expectation>
</view>
</view>
<view class="card-companyName">{{ job.gwmc }}</view>
<view class="card-tags">
<view class="tag">
<dict-Label dictType="education" :value="job.education"></dict-Label>
</view>
<view class="tag">
<dict-Label dictType="experience" :value="job.experience"></dict-Label>
</view>
<view class="tag">
{{ vacanciesTo(job.vacancies) }}
</view>
</view>
<view class="card-bottom">
<view>{{ job.postingDate }}</view>
<view>
<convert-distance
:alat="job.latitude"
:along="job.longitude"
:blat="latitude"
:blong="longitude"
></convert-distance>
<dict-Label class="mar_le10" dictType="area" :value="job.jobLocationAreaCode"></dict-Label>
</view>
</view>
</view>
<view class="card-companyName">{{ job.gwmc }}</view>
<view class="card-tags">
<view class="tag">
<dict-Label dictType="education" :value="job.education"></dict-Label>
</view>
<view class="tag">
<dict-Label dictType="experience" :value="job.experience"></dict-Label>
</view>
<view class="tag">
{{ vacanciesTo(job.vacancies) }}
</view>
</view>
<view class="card-bottom">
<view>{{ job.postingDate }}</view>
<view>
<convert-distance
:alat="job.latitude"
:along="job.longitude"
:blat="latitude"
:blong="longitude"
></convert-distance>
<dict-Label class="mar_le10" dictType="area" :value="job.jobLocationAreaCode"></dict-Label>
</view>
<view class="date-jobTitle" v-else>
{{ job.title }}
</view>
</view>
<view class="date-jobTitle" v-else>
{{ job.title }}
</view>
</view>
</transition-group>
</template>
<script setup>
import { inject, computed, toRaw } from 'vue';
import { inject, computed, toRaw, ref, watch } from 'vue';
const { insertSortData, navTo, vacanciesTo } = inject('globalFunction');
import { useRecommedIndexedDBStore } from '@/stores/useRecommedIndexedDBStore.js';
const recommedIndexDb = useRecommedIndexedDBStore();
const dataSource = ref([]);
const props = defineProps({
list: {
type: Array,
@@ -71,6 +77,24 @@ const listData = computed(() => {
return props.list;
});
const processedIds = new Set();
watch(
() => props.list,
(newList) => {
if (!Array.isArray(newList)) return;
const newItems = newList.filter((item) => !processedIds.has(item.id));
if (newItems.length === 0) return;
newItems.forEach((item) => processedIds.add(item.id));
const delay = 50;
newItems.forEach((item, index) => {
setTimeout(() => {
dataSource.value.push(item);
}, index * delay);
});
},
{ immediate: true }
);
function nextDetail(job) {
// 记录岗位类型,用作数据分析
if (job.jobCategory) {

View File

@@ -1,48 +1,54 @@
<template>
<view v-for="job in listData" :key="job.id">
<view v-if="!job.isTitle" class="cards" @click="nextDetail(job)">
<view class="card-company">
<text class="company">{{ job.gwmc }}</text>
<view class="salary">
<Salary-Expectation :max-salary="job.maxSalary" :min-salary="job.minSalary"></Salary-Expectation>
<transition-group name="blur-fade-stagger" tag="view">
<view v-for="job in dataSource" :key="job.id" :style="{ '--i': 2 }">
<view v-if="!job.isTitle" class="cards" @click="nextDetail(job)">
<view class="card-company">
<text class="company">{{ job.gwmc }}</text>
<view class="salary">
<Salary-Expectation
:max-salary="job.maxSalary"
:min-salary="job.minSalary"
></Salary-Expectation>
</view>
</view>
</view>
<view class="card-companyName">{{ job.gsmc }}</view>
<view class="card-tags">
<view class="tag">
{{job.xlyq == '不限' ? '学历不限' : job.xlyq}}
<view class="card-companyName">{{ job.gsmc }}</view>
<view class="card-tags">
<view class="tag">
{{ job.xlyq == '不限' ? '学历不限' : job.xlyq }}
</view>
<view class="tag">
{{ job.gwgzjy == '不限' ? '经验不限' : job.gwgzjy }}
</view>
<view class="tag">
{{ vacanciesTo(job.zprs) }}
</view>
</view>
<view class="tag">
{{job.gwgzjy == '不限' ? '经验不限' : job.gwgzjy}}
</view>
<view class="tag">
{{ vacanciesTo(job.zprs) }}
</view>
</view>
<view class="card-bottom">
<view>{{ parseDateTime(job.createTime).date }}</view>
<view>
<!-- <convert-distance
<view class="card-bottom">
<view>{{ parseDateTime(job.createTime).date }}</view>
<view>
<!-- <convert-distance
:alat="job.latitude"
:along="job.longitude"
:blat="latitude"
:blong="longitude"
></convert-distance>
<dict-Label class="mar_le10" dictType="area" :value="job.jobLocationAreaCode"></dict-Label> -->
</view>
</view>
</view>
<view class="date-jobTitle" v-else>
{{ job.title }}
</view>
</view>
<view class="date-jobTitle" v-else>
{{ job.title }}
</view>
</view>
</transition-group>
</template>
<script setup>
import { inject, computed, toRaw } from 'vue';
import { inject, computed, toRaw, ref, watch } from 'vue';
const { insertSortData, navTo, vacanciesTo } = inject('globalFunction');
import { useRecommedIndexedDBStore } from '@/stores/useRecommedIndexedDBStore.js';
const recommedIndexDb = useRecommedIndexedDBStore();
const dataSource = ref([]);
const props = defineProps({
list: {
type: Array,
@@ -71,6 +77,24 @@ const listData = computed(() => {
return props.list;
});
const processedIds = new Set();
watch(
() => props.list,
(newList) => {
if (!Array.isArray(newList)) return;
const newItems = newList.filter((item) => !processedIds.has(item.id));
if (newItems.length === 0) return;
newItems.forEach((item) => processedIds.add(item.id));
const delay = 50;
newItems.forEach((item, index) => {
setTimeout(() => {
dataSource.value.push(item);
}, index * delay);
});
},
{ immediate: true }
);
function nextDetail(job) {
// 记录岗位类型,用作数据分析
if (job.jobCategory) {

View File

@@ -1,17 +1,24 @@
export default {
// baseUrl: 'https://fw.rc.qingdao.gov.cn/rgpp-api/api', // 内网
baseUrl: 'https://qd.zhaopinzao8dian.com/api', // 测试
baseUrl: 'https://fw.rc.qingdao.gov.cn/rgpp-api/api', // 内网
// baseUrl: 'https://qd.zhaopinzao8dian.com/api', // 测试
// baseUrl: 'http://192.168.3.29:8081',
// baseUrl: 'http://10.213.6.207:19010/api',
// 语音转文字
// vioceBaseURl: 'ws://39.98.44.136:8080/speech-recognition',
vioceBaseURl: 'wss://qd.zhaopinzao8dian.com/api/speech-recognition',
// vioceBaseURl: 'wss://qd.zhaopinzao8dian.com/api/system/asr/connect', // 自定义
vioceBaseURl: 'wss://fw.rc.qingdao.gov.cn/rgpp-api/api/system/asr/connect', // 内网
// 语音合成
speechSynthesis: 'wss://qd.zhaopinzao8dian.com/api/speech-synthesis',
speechSynthesis2: 'wss://resource.zhuoson.com/synthesis/', //直接替换即可
// indexedDB
DBversion: 2,
DBversion: 3,
// 只使用本地缓寸的数据
OnlyUseCachedDB: false,
// 素质测评URL
Quality_assessment_URL: 'https://web1.isdapp.shandong.gov.cn/jmopen_files/unzip/49ee8533b31b46238906b31c27c5dfc9/zycpvhyjw/index.html#/pages/evaluation_record/evaluation_record?uuid=2',
// 职业指导
Career_guidance: 'https://web1.isdapp.shandong.gov.cn/jmopen_files/unzip/2626f6e3c899445db8639a873d172d73/zyzd/index.html',
// ai 模拟面试
mock_interview: 'https://web1.isdapp.shandong.gov.cn/jmopen_files/unzip/08c660be20b74e15acd8763001db5fd1/szjx-rgzn-xnsc/#/pages/mine/interview/index',
// 使用模拟定位
UsingSimulatedPositioning: true,
// 应用信息
@@ -77,5 +84,12 @@ export default {
mode: 'ECB', // default
iv: 'UISwD9fW6cFh9SNS', // default is null
cipherType: 'base64' // default is base64
}
},
animations: [ //动画
'fade-up-stagger',
'pop-in-stagger',
'slide-left-stagger',
'blur-fade-stagger',
'bounce-up-stagger'
]
}

214
hook/piper-sdk.js Normal file
View File

@@ -0,0 +1,214 @@
/**
* PiperTTS SDK - 兼容移动端的流式语音合成客户端
* 特性:
* 1. Web Audio API 实时调度,解决移动端不支持 MSE 的问题
* 2. 头部注入 (Header Injection) 技术,解决分片解码错误
* 3. 自动状态管理与事件回调
*/
export class PiperTTS {
constructor(config = {}) {
this.baseUrl = config.baseUrl || 'http://localhost:5001';
this.audioCtx = config.audioCtx || new(window.AudioContext || window.webkitAudioContext)();
this.onStatus = config.onStatus || ((msg, type) => console.log(`[Piper] ${msg}`));
this.onStart = config.onStart || (() => {});
this.onEnd = config.onEnd || (() => {});
// 内部状态
this.ws = null;
this.nextTime = 0; // 下一段音频的预定播放时间
this.audioHeader = null; // 保存WAV/MP3头部
this.chunkQueue = []; // 数据缓冲队列
this.queueSize = 0; // 当前缓冲区字节数
this.analyser = null; // 可视化分析器节点
// 配置参数
this.flushThreshold = 8 * 1024; // 8KB 阈值
}
/**
* [重要] 初始化音频引擎
* 必须在用户点击事件click/touch中调用一次否则手机上没声音
*/
async init() {
if (this.audioCtx.state === 'suspended') {
await this.audioCtx.resume();
this.onStatus('音频引擎已激活', 'success');
}
}
/**
* 绑定可视化分析器
* @param {AnalyserNode} analyserNode - Web Audio Analyser节点
*/
attachVisualizer(analyserNode) {
this.analyser = analyserNode;
}
/**
* 开始合成并播放
* @param {string} text - 要合成的文本
* @param {object} options - 可选参数 {speaker_id, noise_scale, etc.}
*/
speak(text, options = {}) {
if (!text) return;
this.stop(); // 清理上一次播放
this.onStatus('正在建立连接...', 'processing');
try {
const wsUrl = this.baseUrl.replace(/^http/, 'ws') + '/ws/synthesize';
this.ws = new WebSocket(wsUrl);
this.ws.binaryType = 'arraybuffer';
this.ws.onopen = () => {
this.onStatus('连接成功,请求生成...', 'processing');
// 初始化时间轴:当前时间 + 缓冲延迟
this.nextTime = this.audioCtx.currentTime + 0.1;
this.onStart();
this.ws.send(
JSON.stringify({
text: text,
speaker_id: options.speakerId || null,
length_scale: options.lengthScale || 1.0,
noise_scale: options.noiseScale || 0.667,
})
);
};
this.ws.onmessage = (event) => this._handleMessage(event);
this.ws.onclose = async () => {
// 处理剩余残余数据
if (this.chunkQueue.length > 0) {
await this._processQueue(true);
}
this.onStatus('播放结束', 'success');
this.onEnd();
};
this.ws.onerror = (err) => {
console.error(err);
this.onStatus('连接发生错误', 'error');
};
} catch (e) {
this.onStatus(`启动失败: ${e.message}`, 'error');
}
}
/**
* 停止播放并重置状态
*/
stop() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
// 重置缓冲
this.chunkQueue = [];
this.queueSize = 0;
this.audioHeader = null;
// 注意Web Audio API 很难"立即停止"已经在 flight 中的 node
// 除非我们追踪所有的 sourceNode 并调用 .stop()。
// 简单实现suspend 再 resume 或者关闭 context (不推荐频繁关闭)。
// 这里的 stop 主要停止数据接收。
}
// --- 内部私有方法 ---
async _handleMessage(event) {
if (!(event.data instanceof ArrayBuffer)) return;
const chunk = event.data;
// 1. 捕获头部 (Header Injection 核心)
if (!this.audioHeader) {
// 截取前100字节作为通用头
this.audioHeader = chunk.slice(0, 100);
}
// 2. 入队
this.chunkQueue.push(chunk);
this.queueSize += chunk.byteLength;
// 3. 达到阈值则解码播放
if (this.queueSize >= this.flushThreshold) {
await this._processQueue();
}
}
async _processQueue(isLast = false) {
if (this.chunkQueue.length === 0) return;
// 1. 合并 Buffer
const rawData = new Uint8Array(this.queueSize);
let offset = 0;
for (const chunk of this.chunkQueue) {
rawData.set(new Uint8Array(chunk), offset);
offset += chunk.byteLength;
}
// 清空队列
this.chunkQueue = [];
this.queueSize = 0;
try {
// 2. 构造带头部的 Buffer
let decodeTarget;
// 简单的头部检测逻辑,如果没有头,就拼上去
if (this.audioHeader && !this._hasHeader(rawData)) {
const newBuffer = new Uint8Array(this.audioHeader.byteLength + rawData.byteLength);
newBuffer.set(new Uint8Array(this.audioHeader), 0);
newBuffer.set(rawData, this.audioHeader.byteLength);
decodeTarget = newBuffer.buffer;
} else {
decodeTarget = rawData.buffer;
}
// 3. 解码
const decodedBuffer = await this.audioCtx.decodeAudioData(decodeTarget);
// 4. 播放调度
this._scheduleBuffer(decodedBuffer);
} catch (err) {
// 解码失败处理:如果是中间数据,放回队列头部等待拼接
if (!isLast) {
this.chunkQueue.unshift(rawData);
this.queueSize += rawData.byteLength;
} else {
console.warn('最后一段数据解码失败,丢弃', err);
}
}
}
_scheduleBuffer(decodedBuffer) {
const source = this.audioCtx.createBufferSource();
source.buffer = decodedBuffer;
// 连接可视化
if (this.analyser) {
source.connect(this.analyser);
this.analyser.connect(this.audioCtx.destination);
} else {
source.connect(this.audioCtx.destination);
}
// 计算播放时间:如果发生卡顿,立即播放;否则无缝衔接
const scheduleTime = Math.max(this.audioCtx.currentTime, this.nextTime);
source.start(scheduleTime);
// 更新下一段的开始时间
this.nextTime = scheduleTime + decodedBuffer.duration;
}
_hasHeader(uint8Arr) {
if (uint8Arr.byteLength < 4) return false;
// Check "RIFF" (WAV)
if (uint8Arr[0] === 82 && uint8Arr[1] === 73 && uint8Arr[2] === 70) return true;
// Check "ID3" (MP3)
if (uint8Arr[0] === 73 && uint8Arr[1] === 68 && uint8Arr[2] === 51) return true;
// Check MP3 Sync Word (Simplify)
if (uint8Arr[0] === 0xff && (uint8Arr[1] & 0xe0) === 0xe0) return true;
return false;
}
}

View File

@@ -9,6 +9,7 @@ import {
import config from '@/config'
// Alibaba Cloud
export function useAudioRecorder() {
const isRecording = ref(false)
const isStopping = ref(false)

View File

@@ -0,0 +1,348 @@
import {
ref,
onUnmounted
} from 'vue'
import {
$api
} from '../common/globalFunction'; // 你的请求封装
import config from '@/config'
// 开源
export function useAudioRecorder() {
// --- 状态定义 ---
const isRecording = ref(false)
const isSocketConnected = ref(false)
const recordingDuration = ref(0)
const volumeLevel = ref(0) // 0-100
const recognizedText = ref('')
// --- 内部变量 ---
let socketTask = null
let durationTimer = null
// --- APP/小程序 变量 ---
let recorderManager = null;
// --- H5 变量 ---
let audioContext = null;
let scriptProcessor = null;
let mediaStreamSource = null;
let h5Stream = null;
// --- 配置项 ---
const RECORD_CONFIG = {
duration: 600000,
sampleRate: 16000,
numberOfChannels: 1,
format: 'pcm',
frameSize: 4096
}
/**
* 获取 WebSocket 地址 (含 Token)
*/
const getWsUrl = async () => {
let wsUrl = config.vioceBaseURl
// 拼接 Token
const token = uni.getStorageSync('token') || '';
if (token) {
const separator = wsUrl.includes('?') ? '&' : '?';
wsUrl = `${wsUrl}${separator}token=${encodeURIComponent(token)}`;
}
return wsUrl;
}
/**
* 开始录音 (入口)
*/
const startRecording = async () => {
if (isRecording.value) return
try {
recognizedText.value = ''
volumeLevel.value = 0
// #ifdef H5
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
uni.showToast({
title: 'H5录音需要HTTPS环境',
icon: 'none'
});
return;
}
// #endif
const url = await getWsUrl()
console.log('正在连接 ASR:', url)
await connectSocket(url);
} catch (err) {
console.error('启动失败:', err);
uni.showToast({
title: '启动失败: ' + (err.message || ''),
icon: 'none'
});
cleanup();
}
}
/**
* 连接 WebSocket
*/
const connectSocket = (url) => {
return new Promise((resolve, reject) => {
socketTask = uni.connectSocket({
url: url,
success: () => console.log('Socket 连接请求发送'),
fail: (err) => reject(err)
});
socketTask.onOpen((res) => {
console.log('WebSocket 已连接');
isSocketConnected.value = true;
// #ifdef H5
startH5Recording().then(() => resolve()).catch(err => {
socketTask.close();
reject(err);
});
// #endif
// #ifndef H5
startAppRecording();
resolve();
// #endif
});
socketTask.onMessage((res) => {
// 接收文本结果
if (res.data) {
recognizedText.value = res.data;
}
});
socketTask.onError((err) => {
console.error('Socket 错误:', err);
isSocketConnected.value = false;
stopRecording();
});
socketTask.onClose(() => {
isSocketConnected.value = false;
console.log('Socket 已关闭');
});
})
}
const startH5Recording = async () => {
try {
// 1. 获取麦克风流
const stream = await navigator.mediaDevices.getUserMedia({
audio: true
});
h5Stream = stream;
// 2. 创建 AudioContext
const AudioContext = window.AudioContext || window.webkitAudioContext;
audioContext = new AudioContext({
sampleRate: 16000
});
mediaStreamSource = audioContext.createMediaStreamSource(stream);
scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1);
scriptProcessor.onaudioprocess = (event) => {
if (!isSocketConnected.value || !socketTask) return;
const inputData = event.inputBuffer.getChannelData(0);
calculateVolume(inputData, true);
const buffer = new ArrayBuffer(inputData.length * 2);
const view = new DataView(buffer);
for (let i = 0; i < inputData.length; i++) {
let s = Math.max(-1, Math.min(1, inputData[i]));
view.setInt16(i * 2, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
}
socketTask.send({
data: buffer,
fail: (e) => console.error('发送音频失败', e)
});
};
mediaStreamSource.connect(scriptProcessor);
scriptProcessor.connect(audioContext.destination);
isRecording.value = true;
recordingDuration.value = 0;
durationTimer = setInterval(() => recordingDuration.value++, 1000);
console.log('H5 录音已启动');
} catch (err) {
console.error('H5 录音启动失败:', err);
throw err;
}
}
const stopH5Resources = () => {
if (scriptProcessor) scriptProcessor.disconnect();
if (mediaStreamSource) mediaStreamSource.disconnect();
if (audioContext) audioContext.close();
if (h5Stream) h5Stream.getTracks().forEach(track => track.stop());
scriptProcessor = null;
mediaStreamSource = null;
audioContext = null;
h5Stream = null;
}
const startAppRecording = () => {
recorderManager = uni.getRecorderManager();
recorderManager.onFrameRecorded((res) => {
const {
frameBuffer
} = res;
calculateVolume(frameBuffer, false);
if (isSocketConnected.value && socketTask) {
socketTask.send({
data: frameBuffer
});
}
});
recorderManager.onStart(() => {
console.log('APP 录音已开始');
isRecording.value = true;
recordingDuration.value = 0;
durationTimer = setInterval(() => recordingDuration.value++, 1000);
});
recorderManager.onError((err) => {
console.error('APP 录音报错:', err);
cleanup();
});
recorderManager.start(RECORD_CONFIG);
}
const stopHardwareResource = () => {
// APP/小程序停止
if (recorderManager) {
recorderManager.stop();
}
// H5停止
// #ifdef H5
if (scriptProcessor) scriptProcessor.disconnect();
if (mediaStreamSource) mediaStreamSource.disconnect();
if (audioContext) audioContext.close();
if (h5Stream) h5Stream.getTracks().forEach(track => track.stop());
scriptProcessor = null;
mediaStreamSource = null;
audioContext = null;
h5Stream = null;
// #endif
}
/**
* 停止录音 (通用)
*/
const stopRecording = () => {
// 停止 APP 录音
if (recorderManager) {
recorderManager.stop();
}
// 停止 H5 录音资源
// #ifdef H5
stopH5Resources();
// #endif
// 关闭 Socket
if (socketTask) {
socketTask.close();
}
cleanup();
}
const cancelRecording = () => {
if (!isRecording.value) return;
console.log('取消录音 - 丢弃结果');
// 1. 停止硬件录音
stopHardwareResource();
// 2. 强制关闭 Socket
if (socketTask) {
socketTask.close();
}
// 3. 关键:清空已识别的文本
recognizedText.value = '';
// 4. 清理资源
cleanup();
}
/**
* 清理状态
*/
const cleanup = () => {
clearInterval(durationTimer);
isRecording.value = false;
isSocketConnected.value = false;
socketTask = null;
recorderManager = null;
volumeLevel.value = 0;
}
/**
* 计算音量 (兼容 Float32 和 Int16/ArrayBuffer)
*/
const calculateVolume = (data, isFloat32) => {
let sum = 0;
let length = 0;
if (isFloat32) {
length = data.length;
for (let i = 0; i < length; i += 10) {
sum += Math.abs(data[i]);
}
volumeLevel.value = Math.min(100, Math.floor((sum / (length / 10)) * 100 * 3));
} else {
const int16Data = new Int16Array(data);
length = int16Data.length;
for (let i = 0; i < length; i += 10) {
sum += Math.abs(int16Data[i]);
}
const avg = sum / (length / 10);
volumeLevel.value = Math.min(100, Math.floor((avg / 10000) * 100));
}
}
onUnmounted(() => {
if (isRecording.value) {
stopRecording();
}
})
return {
isRecording,
isSocketConnected,
recordingDuration,
volumeLevel,
recognizedText,
startRecording,
stopRecording,
cancelRecording
}
}

View File

@@ -9,8 +9,9 @@ import {
onUnload
} from '@dcloudio/uni-app'
import WavDecoder from '@/lib/wav-decoder@1.3.0.js'
import config from '@/config'
export function useTTSPlayer(wsUrl) {
export function useTTSPlayer() {
const isSpeaking = ref(false)
const isPaused = ref(false)
const isComplete = ref(false)
@@ -89,12 +90,13 @@ export function useTTSPlayer(wsUrl) {
const initWebSocket = () => {
const thisPlayId = currentPlayId
socket = new WebSocket(wsUrl)
socket = new WebSocket(config.speechSynthesis)
socket.binaryType = 'arraybuffer'
socket.onopen = () => {
if (pendingText && thisPlayId === activePlayId) {
const seepdText = extractSpeechText(pendingText)
console.log(seepdText)
socket.send(seepdText)
pendingText = null
}

216
hook/useTTSPlayer2.js Normal file
View File

@@ -0,0 +1,216 @@
import {
ref,
onUnmounted,
onMounted,
watch
} from 'vue'
import {
onHide,
onUnload
} from '@dcloudio/uni-app'
import config from '@/config'
// 请确保 piper-sdk.js 已经正确 export class PiperTTS
import {
PiperTTS
} from './piper-sdk.js'
export function useTTSPlayer() {
// UI 状态
const isSpeaking = ref(false)
const isPaused = ref(false)
const isLoading = ref(false)
// SDK 实例
let piper = null
/**
* 初始化 SDK 实例
* 每次 stop 后 piper 会被置空,这里会重新创建
*/
const initPiper = () => {
if (piper) return
let baseUrl = config.speechSynthesis2 || ''
baseUrl = baseUrl.replace(/\/$/, '')
piper = new PiperTTS({
baseUrl: baseUrl,
onStatus: (msg, type) => {
if (type === 'error') {
console.error('[TTS Error]', msg)
// 出错时不重置状态,交给用户手动处理或结束事件处理
resetState()
}
},
onStart: () => {
isLoading.value = false
isSpeaking.value = true
isPaused.value = false
},
onEnd: () => {
resetState()
}
})
}
/**
* 核心朗读方法
*/
const speak = async (text) => {
if (!text) return
const processedText = extractSpeechText(text)
if (!processedText) return
// 1. 【关键修改】先彻底停止并销毁旧实例
// 这会断开 socket 并且 close AudioContext确保上一个声音立即消失
await stop()
// 2. 初始化新实例 (因为 stop() 把 piper 设为了 null)
initPiper()
// 3. 更新 UI 为加载中
isLoading.value = true
isPaused.value = false
isSpeaking.value = true // 预先设为 true防止按钮闪烁
try {
// 4. 激活音频引擎 (移动端防静音关键)
await piper.init()
// 5. 发送请求
piper.speak(processedText, {
speakerId: 0,
noiseScale: 0.667,
lengthScale: 1.0
})
} catch (e) {
console.error('TTS Speak Error:', e)
resetState()
}
}
/**
* 暂停
*/
const pause = async () => {
if (piper && piper.audioCtx && piper.audioCtx.state === 'running') {
await piper.audioCtx.suspend()
isPaused.value = true
}
}
/**
* 恢复
*/
const resume = async () => {
if (piper && piper.audioCtx && piper.audioCtx.state === 'suspended') {
await piper.audioCtx.resume()
isPaused.value = false
isSpeaking.value = true
}
}
/**
* 停止并重置 (核打击模式)
*/
const stop = async () => {
if (piper) {
// 1. 断开 WebSocket
piper.stop()
// 2. 【关键】关闭 AudioContext
// Web Audio API 中,已经 schedule 的 buffer 很难单独取消
// 最直接的方法是关闭整个 Context
if (piper.audioCtx && piper.audioCtx.state !== 'closed') {
try {
await piper.audioCtx.close()
} catch (e) {
console.warn('AudioContext close failed', e)
}
}
// 3. 销毁实例引用
piper = null
}
resetState()
}
// UI 状态重置
const resetState = () => {
isSpeaking.value = false
isPaused.value = false
isLoading.value = false
}
// === 生命周期 ===
onMounted(() => {
// 预初始化可以不做,等到点击时再做,避免空闲占用 AudioContext 资源
// initPiper()
})
onUnmounted(() => {
stop()
})
// Uniapp 生命周期
if (typeof onHide === 'function') onHide(stop)
if (typeof onUnload === 'function') onUnload(stop)
return {
speak,
pause,
resume,
stop,
cancelAudio: stop,
isSpeaking,
isPaused,
isLoading
}
}
/**
* 提取文本逻辑 (保持不变)
*/
function extractSpeechText(markdown) {
if (!markdown || markdown.indexOf('job-json') === -1) {
return markdown;
}
const jobRegex = /``` job-json\s*({[\s\S]*?})\s*```/g;
const jobs = [];
let match;
let lastJobEndIndex = 0;
let firstJobStartIndex = -1;
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);
}
}
const guideText = firstJobStartIndex > 0 ?
markdown.slice(0, firstJobStartIndex).trim() : '';
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

@@ -22,11 +22,11 @@
<script>
eruda.init();
</script> -->
<!-- <script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>
// VConsole 默认会挂载到 `window.VConsole` 上
var vConsole = new window.VConsole();
</script> -->
</script>
<!-- 爱山东jssdk 本sdk存在性能问题 -->
<script type="text/javascript" src="https://isdapp.shandong.gov.cn/jmopen/jssdk/index.js"></script>
<!-- 只在内网有效 -->

View File

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

View File

@@ -21,6 +21,7 @@ import { storeToRefs } from 'pinia';
import useLocationStore from '@/stores/useLocationStore';
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
const userStore = useUserStore();
const { isMiniProgram } = storeToRefs(useUserStore());
const state = reactive({});
const pageState = reactive({
page: 0,
@@ -37,6 +38,15 @@ onLoad(() => {
getJobList();
});
onMounted(() => {
// #ifdef H5
if (!isMiniProgram.value) {
const a = document.getElementsByClassName('uni-page-head-hd')[0];
a.style.display = 'none';
}
// #endif
});
onReachBottom(() => {
getJobList();
});

View File

@@ -1,6 +1,6 @@
<template>
<AppLayout title="" :use-scroll-view="false">
<template #headerleft>
<template #headerleft v-if="isMiniProgram">
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
@@ -75,6 +75,8 @@ import { reactive, inject, watch, ref, onMounted, computed } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import dictLabel from '@/components/dict-Label/dict-Label.vue';
import { storeToRefs } from 'pinia';
const { isMiniProgram } = storeToRefs(useUserStore());
import useUserStore from '@/stores/useUserStore';
import useLocationStore from '@/stores/useLocationStore';
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
const { $api, navTo, vacanciesTo, navBack } = inject('globalFunction');
@@ -220,7 +222,8 @@ function handleJobsListResponse(type, rows, total, pageKey) {
const str = pageState.pageSize * (pageState[pageKey] - 1);
const end = pageState.list.length;
const reslist = rows;
pageState.list.splice(str, end, ...reslist);
pageState.list = [...pageState.list, ...rows];
// pageState.list.splice(str, end, ...reslist);
} else {
pageState.list = rows;
}

View File

@@ -1,6 +1,6 @@
<template>
<AppLayout title="添加岗位">
<template #headerleft>
<template #headerleft v-if="isMiniProgram">
<view class="btn">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
@@ -74,7 +74,7 @@ const { $api, navBack } = inject('globalFunction');
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
const { getUserResume } = useUserStore();
const { userInfo } = storeToRefs(useUserStore());
const { userInfo, isMiniProgram } = storeToRefs(useUserStore());
const popup = ref(null);
const selectJobsModel = ref(null);

View File

@@ -1,6 +1,6 @@
<template>
<AppLayout title="我的浏览" :show-bg-image="false" :use-scroll-view="false">
<template #headerleft>
<template #headerleft v-if="isMiniProgram">
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
@@ -46,7 +46,7 @@ import { onLoad, onShow } from '@dcloudio/uni-app';
import useUserStore from '@/stores/useUserStore';
const { $api, navTo, navBack } = inject('globalFunction');
import useLocationStore from '@/stores/useLocationStore';
const { userInfo } = storeToRefs(useUserStore());
const { userInfo, isMiniProgram } = storeToRefs(useUserStore());
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
const userStore = useUserStore();
const browseDate = ref('');
@@ -123,6 +123,7 @@ function getJobList(type = 'add', loading = true) {
reslist.shift();
}
}
// pageState.list = [...pageState.list, ...rows];
pageState.list.splice(str, end, ...reslist);
pageState.lastDate = lastDate;
} else {

View File

@@ -1,6 +1,6 @@
<template>
<AppLayout title="精选企业">
<template #headerleft>
<template #headerleft v-if="isMiniProgram">
<view class="btn">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
@@ -28,7 +28,7 @@ const { $api, navBack, navTo } = inject('globalFunction');
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
const { getUserResume } = useUserStore();
const { userInfo } = storeToRefs(useUserStore());
const { userInfo, isMiniProgram } = storeToRefs(useUserStore());
const list = ref([]);
onLoad(() => {

View File

@@ -1,6 +1,6 @@
<template>
<AppLayout :title="title" :show-bg-image="false" @onScrollBottom="getDataList('add')">
<template #headerleft>
<template #headerleft v-if="isMiniProgram">
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
@@ -39,6 +39,8 @@ import { storeToRefs } from 'pinia';
const { $api, navTo, navBack } = inject('globalFunction');
import useLocationStore from '@/stores/useLocationStore';
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
import useUserStore from '@/stores/useUserStore';
const { isMiniProgram } = storeToRefs(useUserStore());
// state
const title = ref('事业单位');
const cardInfo = ref({});

View File

@@ -1,6 +1,6 @@
<template>
<AppLayout title="我的收藏" :show-bg-image="false" :use-scroll-view="false">
<template #headerleft>
<template #headerleft v-if="isMiniProgram">
<view class="btn">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
@@ -61,6 +61,8 @@ import { storeToRefs } from 'pinia';
import useLocationStore from '@/stores/useLocationStore';
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
const { $api, navBack } = inject('globalFunction');
import useUserStore from '@/stores/useUserStore';
const { isMiniProgram } = storeToRefs(useUserStore());
const type = ref(0);
const pageState = reactive({

View File

@@ -1,6 +1,6 @@
<template>
<AppLayout title="" :use-scroll-view="false">
<template #headerleft>
<template #headerleft v-if="isMiniProgram">
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
@@ -83,7 +83,7 @@
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderCompanysOutData>
<empty v-else is-position></empty>
<empty v-if="!pageState.list.length" is-position></empty>
</view>
</scroll-view>
</view>
@@ -110,6 +110,8 @@ import useLocationStore from '@/stores/useLocationStore';
const { $api, navTo, vacanciesTo, navBack } = inject('globalFunction');
import { storeToRefs } from 'pinia';
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
import useUserStore from '@/stores/useUserStore';
const { isMiniProgram } = storeToRefs(useUserStore());
const isExpanded = ref(false);
const fairInfo = ref({});
@@ -121,7 +123,7 @@ const pageState = reactive({
maxPage: 1,
pageSize: 10,
});
const hasnext = ref(true);
const hasnext = ref(false);
const zphId = ref('');
const pageOptions = ref({});
@@ -163,7 +165,8 @@ function getCompanyList(type = 'add') {
const str = pageState.pageSize * (pageState.current - 1);
const end = pageState.list.length;
const reslist = rows;
pageState.list.splice(str, end, ...reslist);
pageState.list = [...pageState.list, ...rows];
// pageState.list.splice(str, end, ...reslist);
} else {
pageState.list = rows;
}

View File

@@ -1,6 +1,6 @@
<template>
<AppLayout title="我的简历" title-color="#FFFFFF" back-gorund-color="#F4F4F4">
<template #headerleft>
<template #headerleft v-if="isMiniProgram">
<view class="btn">
<image src="@/static/icon/back-white.png" @click="navBack"></image>
</view>
@@ -42,7 +42,7 @@
</view>
<view class="subName">
<dict-Label class="mar_ri10" dictType="sex" :value="userInfo.sex"></dict-Label>
<text class="mar_ri10">{{ userInfo.age }}</text>
<text class="mar_ri10">{{ userInfo.age || '-' }}</text>
<dict-Label class="mar_ri10" dictType="education" :value="userInfo.education"></dict-Label>
<dict-Label
class="mar_ri10"
@@ -50,7 +50,7 @@
:value="userInfo.politicalAffiliation"
></dict-Label>
</view>
<view class="subName">{{ userInfo.phone }}</view>
<view class="subName">{{ userInfo.phone || '未知手机号' }}</view>
</view>
<view class="tops-right">
<view class="right-imghead">
@@ -137,7 +137,7 @@ 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 { userInfo, isMiniProgram } = storeToRefs(useUserStore());
const { getUserResume } = useUserStore();
const { getDictData, oneDictData } = useDictStore();
import config from '@/config.js';
@@ -186,7 +186,8 @@ function uploadResume(tempFilePath, loading) {
header['Authorization'] = encodeURIComponent(Authorization);
return new Promise((resolve, reject) => {
uni.uploadFile({
url: config.baseUrl + '/app/oss/uploadToObs',
url: config.baseUrl + '/app/user/resume/recognition',
// url: config.baseUrl + '/app/oss/uploadToObs',
filePath: tempFilePath,
name: 'file',
header,

View File

@@ -1,267 +1,295 @@
<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-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" />
</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 pointEveNone"
v-model="fromValue.birthDate"
disabled
placeholder="请选择您的出生年月"
/>
</view>
<view class="content-input" @click="changeEducation">
<view class="input-titile">学历</view>
<input
class="input-con triangle pointEveNone"
v-model="state.educationText"
disabled
placeholder="请选择您的学历"
/>
</view>
<view class="content-input" @click="changePoliticalAffiliation">
<view class="input-titile">政治面貌</view>
<input
class="input-con triangle pointEveNone"
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: '',
avatar: '',
});
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;
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;
}
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();
let defaultIndex = [0, 0, 0];
if (fromValue.birthDate) {
const defaultIndex = getDatePickerIndexes(fromValue.birthDate);
}
console.log(defaultIndex);
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];
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) => {},
});
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) => {},
});
}
</script>

View File

@@ -1,6 +1,6 @@
<template>
<AppLayout title="" backGorundColor="#F4F4F4">
<template #headerleft>
<template #headerleft v-if="isMiniProgram">
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
@@ -183,6 +183,9 @@ import { reactive, inject, watch, ref, onMounted, computed } from 'vue';
import { onLoad, onShow, onHide } from '@dcloudio/uni-app';
import dictLabel from '@/components/dict-Label/dict-Label.vue';
import RadarMap from './component/radarMap.vue';
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
const { isMiniProgram } = storeToRefs(useUserStore());
const { $api, navTo, getLenPx, parseQueryParams, navBack, isEmptyObject } = inject('globalFunction');
import config from '@/config.js';

View File

@@ -60,7 +60,7 @@ import Countdown from './component/countdown.vue';
import { storeToRefs } from 'pinia';
import useLocationStore from '@/stores/useLocationStore';
import useUserStore from '@/stores/useUserStore';
const { userInfo } = storeToRefs(useUserStore());
const { userInfo, isMiniProgram } = storeToRefs(useUserStore());
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
const pageState = reactive({
@@ -92,6 +92,15 @@ onLoad(() => {
getList();
});
onMounted(() => {
// #ifdef H5
if (!isMiniProgram.value) {
const a = document.getElementsByClassName('uni-page-head-hd')[0];
a.style.display = 'none';
}
// #endif
});
function chnageRanOption(item) {
ranItem.value = item;
getList();

View File

@@ -1,7 +1,7 @@
<template>
<AppLayout title="选择日期" :use-scroll-view="false" back-gorund-color="#FAFAFA">
<template #headerleft>
<view class="btn">
<view class="btn" v-if="isMiniProgram">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
</template>
@@ -51,6 +51,9 @@ const { $api, navTo, navBack } = inject('globalFunction');
const weekMap = ['日', '一', '二', '三', '四', '五', '六'];
const calendarData = ref([]);
const current = ref({});
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
const { isMiniProgram } = storeToRefs(useUserStore());
import { Solar, Lunar } from '@/lib/lunar-javascript@1.7.2.js';
const isRecord = ref(false);
@@ -62,7 +65,6 @@ const pages = reactive({
const hasZphDateArray = ref([]);
onLoad((options) => {
updateDateArray();
if (options.date) {
current.value = {
date: options?.date || null,

View File

@@ -95,7 +95,7 @@ const change = (e) => {
position: absolute;
left: 24rpx;
right: 24rpx;
bottom: 30rpx;
bottom: calc( var(--window-bottom));
color: #fff;
.title{
font-weight: 500;

View File

@@ -1,267 +1,279 @@
<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>
<AppLayout title="电子名片" title-color="#FFFFFF" back-gorund-color="#F4F4F4">
<template #headerleft v-if="isMiniProgram">
<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></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 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></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>
</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>
<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>
<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>
</view>
</view>
</AppLayout>
</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());
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, isMiniProgram } = 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 {
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;
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%;
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;
}
}
.content {
margin-top: 24rpx;
font-size: 30rpx;
color: #666;
}
.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 {
background: #e8e8e8;
border-radius: 50%;
overflow: hidden;
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,246 +1,250 @@
<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>
<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>
</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>
<!-- 时间选择器组件 -->
<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";
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,
position: '',
company: '',
startTime: '',
endTime: '',
duty: '',
id: undefined,
});
// 获取时间选择器组件的引用
const datePicker = ref();
onLoad((e) => {
initLoad(e?.id);
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");
// 验证必填字段
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('保存失败');
}
$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();
});
}
},
});
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);
});
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;
},
});
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("结束时间不能小于开始时间!");
}
},
});
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;
color: #1677ff;
}
.content {
padding: 28rpx;
display: flex;
flex-direction: column;
justify-content: flex-start;
height: calc(100% - 120rpx);
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;
}
.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;
}
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;
}
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

@@ -45,56 +45,59 @@
<view class="container-main">
<scroll-view scroll-y class="main-scroll" @scrolltolower="handleScrollToLower">
<view class="cards">
<view
class="card press-button"
v-for="(item, index) in fairList"
:key="index"
@click="
navTo(
'/packageA/pages/exhibitors/exhibitors?jobFairId=' +
item.zphID +
'&jobFairName=' +
item.zphmc
)
"
>
<view class="card-title">{{ item.zphmc }}</view>
<view class="card-row">
<text class="">{{ item.jbf }}</text>
<text class="">
<!-- <convert-distance
<transition-group name="stagger" tag="view" :css="true">
<view
class="card press-button"
v-for="(item, index) in fairList"
:key="item.zphID"
:style="{ '--i': index }"
@click="
navTo(
'/packageA/pages/exhibitors/exhibitors?jobFairId=' +
item.zphID +
'&jobFairName=' +
item.zphmc
)
"
>
<view class="card-title">{{ item.zphmc }}</view>
<view class="card-row">
<text class="">{{ item.jbf }}</text>
<text class="">
<!-- <convert-distance
:alat="item.latitude"
:along="item.longitude"
:blat="latitudeVal"
:blong="longitudeVal"
></convert-distance> -->
</text>
</view>
<view class="card-times">
<view class="time-left">
<view class="left-date">{{ parseDateTime(item.zphjbsj).time }}</view>
<view class="left-dateDay">{{ parseDateTime(item.zphjbsj).date }}</view>
</text>
</view>
<view class="line"></view>
<view class="time-center">
<view class="center-date">
{{ getTimeStatus(item.zphjbsj, item.zphjzsj).statusText }}
<view class="card-times">
<view class="time-left">
<view class="left-date">{{ parseDateTime(item.zphjbsj).time }}</view>
<view class="left-dateDay">{{ parseDateTime(item.zphjbsj).date }}</view>
</view>
<view class="center-dateDay">
{{ getHoursBetween(item.zphjbsj, item.zphjzsj) }}小时
<view class="line"></view>
<view class="time-center">
<view class="center-date">
{{ getTimeStatus(item.zphjbsj, item.zphjzsj).statusText }}
</view>
<view class="center-dateDay">
{{ getHoursBetween(item.zphjbsj, item.zphjzsj) }}小时
</view>
</view>
<view class="line"></view>
<view class="time-right">
<view class="left-date">{{ parseDateTime(item.zphjzsj).time }}</view>
<view class="left-dateDay">{{ parseDateTime(item.zphjzsj).date }}</view>
</view>
</view>
<view class="line"></view>
<view class="time-right">
<view class="left-date">{{ parseDateTime(item.zphjzsj).time }}</view>
<view class="left-dateDay">{{ parseDateTime(item.zphjzsj).date }}</view>
</view>
<view class="recommend-card-line"></view>
<view class="card-footer">内容简介{{ item.zphjj }}</view>
</view>
<view class="recommend-card-line"></view>
<view class="card-footer">内容简介{{ item.zphjj }}</view>
</view>
</transition-group>
</view>
<empty v-if="!fairList.length"></empty>
<empty v-if="!fairList.length" content="暂时没有结果,下一天也许就有惊喜"></empty>
</scroll-view>
</view>
<!-- <Tabbar :currentpage="1"></Tabbar> -->

View File

@@ -111,16 +111,16 @@ onHide(() => {
paging.value?.handleTouchCancel();
if (isDrawerOpen.value) {
isDrawerOpen.value = false;
// uni.showTabBar();
uni.showTabBar();
}
});
const toggleDrawer = () => {
isDrawerOpen.value = !isDrawerOpen.value;
if (isDrawerOpen.value) {
// uni.hideTabBar();
uni.hideTabBar();
} else {
// uni.showTabBar();
uni.showTabBar();
}
};

View File

@@ -240,7 +240,7 @@
</scroll-view>
</view>
</view>
<PopupFeeBack ref="feeback" @onSend="confirmFeeBack"></PopupFeeBack>
<PopupFeeBack ref="feeback" @onClose="colseFeeBack" @onSend="confirmFeeBack"></PopupFeeBack>
<MsgTips ref="feeBackTips" content="已收到反馈,感谢您的关注" title="反馈成功" :icon="successIcon"></MsgTips>
</view>
</template>
@@ -268,9 +268,9 @@ 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/useRealtimeRecorder2.js';
// import { useAudioRecorder } from '@/hook/useSystemSpeechReader.js';
import { useTTSPlayer } from '@/hook/useTTSPlayer.js';
import { useTTSPlayer } from '@/hook/useTTSPlayer2.js';
// import { useTTSPlayer } from '@/hook/useSystemPlayer.js';
// 全局
const { $api, navTo, throttle } = inject('globalFunction');
@@ -612,17 +612,23 @@ function userGoodFeedback(msg) {
// $api.msg('该功能正在开发中,敬请期待后续更新!');
feeback.value?.open();
feebackData.value = msg;
uni.hideTabBar()
}
function confirmFeeBack(value) {
useChatGroupDBStore()
.badFeedback(feebackData.value, value)
.then(() => {
uni.showTabBar()
feeback.value?.close();
feeBackTips.value?.open();
});
}
function colseFeeBack() {
uni.showTabBar()
}
function readMarkdown(value, index) {
speechIndex.value = index;
if (speechIndex.value !== index) {

View File

@@ -1,5 +1,5 @@
<template>
<uni-popup ref="popup" type="bottom" borderRadius="12px 12px 0 0" background-color="#F6F6F6">
<uni-popup ref="popup" type="bottom" borderRadius="12px 12px 0 0" @change="changePopup" background-color="#F6F6F6">
<view class="feeback">
<view class="titile">反馈</view>
<view class="pop-h3">针对问题</view>
@@ -38,7 +38,7 @@
<script setup>
import { ref, inject } from 'vue';
const emit = defineEmits(['onSend']);
const emit = defineEmits(['onSend', 'onClose']);
const { $api } = inject('globalFunction');
const popup = ref(null);
const inputText = ref('');
@@ -66,6 +66,13 @@ function close() {
popup.value.close();
}
function changePopup(e) {
if (e.show) {
} else {
emit('onClose');
}
}
function send() {
const text = getLabel();
if (text) {

View File

@@ -1,64 +1,70 @@
<template>
<scroll-view :scroll-y="true" class="app-container" :scroll-top="scrollTop" @scroll="checkStickyStatus" @scrolltolower="scrollBottom">
<view class="nav-hidden">
<view class="container-search">
<image class="bg-text" mode="widthFix" src="@/static/icon/index-text-bg.png"></image>
<view class="search-inner">
<view class="inner-left">
<image class="bg-text2" mode="widthFix" src="@/static/icon/index-text-bg2.png"></image>
<view class="search-input button-click" @click="navTo('/pages/search/search')">
<image class="icon" src="@/static/icon/index-search.png"></image>
<text class="inpute">请告诉我想找什么工作</text>
</view>
</view>
<image class="bg-robot button-click" mode="widthFix" src="@/static/icon/index-robot.png"></image>
</view>
</view>
<view class="ai-card-out">
<view class="ai-card">
<image class="ai-card-bg" src="@/static/icon/ai-card-bg.png" />
<view class="ai-card-inner">
<view class="title">人工智能专区</view>
<view class="item-box">
<view class="box-l">
<view class="item1 button-click" @click="handleItemClick">
<view class="title">AI素质测评</view>
<view class="des">提高个人素质</view>
<image class="bg-text" mode="widthFix" src="@/static/icon/item-bg-text.png"></image>
<image class="bg-img" mode="widthFix" src="@/static/icon/item-bg-img1.png"></image>
<scroll-view
:scroll-y="true"
class="app-container"
:scroll-top="scrollTop"
@scroll="checkStickyStatus"
@scrolltolower="scrollBottom"
>
<view class="nav-hidden">
<view class="container-search">
<image class="bg-text" mode="widthFix" src="@/static/icon/index-text-bg.png"></image>
<view class="search-inner">
<view class="inner-left">
<image class="bg-text2" mode="widthFix" src="@/static/icon/index-text-bg2.png"></image>
<view class="search-input button-click" @click="navTo('/pages/search/search')">
<image class="icon" src="@/static/icon/index-search.png"></image>
<text class="inpute">请告诉我想找什么工作</text>
</view>
</view>
<image class="bg-robot button-click" mode="widthFix" src="@/static/icon/index-robot.png"></image>
</view>
</view>
<view class="box-r">
<view class="item2 button-click" @click="handleItemClick">
<view class="title">就业指导</view>
<view class="des">根据个人建议</view>
<image class="bg" src="@/static/icon/top-card-bg.png" />
<image class="bg-img" mode="widthFix" src="@/static/icon/item-bg-img2.png"></image>
</view>
<view class="item3 button-click" @click="handleItemClick">
<view class="title">AI模拟面试</view>
<view class="des">提高面试成功率</view>
<image class="bg" src="@/static/icon/bottom-card-bg.png" />
<image class="bg-img" mode="widthFix" src="@/static/icon/item-bg-img3.png"></image>
</view>
</view>
</view>
<view class="tip" @click="navTo('/packageA/pages/myResume/myResume')">
<image class="icon" src="@/static/icon/leart-gold.png" />
<view class="text">使用人工智能需要先完成素质测评完善简历 ></view>
<view class="ai-card-out">
<view class="ai-card">
<image class="ai-card-bg" src="@/static/icon/ai-card-bg.png" />
<view class="ai-card-inner">
<view class="title">人工智能专区</view>
<view class="item-box">
<view class="box-l">
<view class="item1 button-click" @click="handleItemClick('素质测评')">
<view class="title">AI素质测评</view>
<view class="des">提高个人素质</view>
<image class="bg-text" mode="widthFix" src="@/static/icon/item-bg-text.png"></image>
<image class="bg-img" src="@/static/icon/item-bg-img1.png"></image>
</view>
</view>
<view class="box-r">
<view class="item2 button-click" @click="handleItemClick('就业指导')">
<view class="title">就业指导</view>
<view class="des">根据个人建议</view>
<image class="bg" src="@/static/icon/top-card-bg.png" />
<image class="bg-img" src="@/static/icon/item-bg-img2.png"></image>
</view>
<view class="item3 button-click" @click="handleItemClick('模拟面试')">
<view class="title">AI模拟面试</view>
<view class="des">提高面试成功率</view>
<image class="bg" src="@/static/icon/bottom-card-bg.png" />
<image class="bg-img" src="@/static/icon/item-bg-img3.png"></image>
</view>
</view>
</view>
<view class="tip button-click" @click="navTo('/packageA/pages/myResume/myResume')">
<image class="icon" src="@/static/icon/leart-gold.png" />
<view class="text">使用人工智能需要先完成素质测评完善简历 ></view>
</view>
</view>
</view>
</view>
<view class="match-card-out">
<view class="match-card">
<image class="match-card-bg" src="@/static/icon/match-card-bg.png" />
<view class="title">简历匹配职位</view>
<view class="match-item-container">
<AIMatch @tag-click="handleTagClick"></AIMatch>
</view>
</view>
</view>
</view>
</view>
</view>
<view class="match-card-out">
<view class="match-card">
<image class="match-card-bg" src="@/static/icon/match-card-bg.png" />
<view class="title">简历匹配职位</view>
<view class="match-item-container">
<AIMatch @tag-click="handleTagClick"></AIMatch>
</view>
</view>
</view>
<view class="cards">
<view class="card card1 press-button" @click="navTo('/pages/nearby/nearby')">
@@ -173,30 +179,32 @@
<view class=""><matchingDegree :job="job"></matchingDegree></view>
<uni-icons type="star" size="30"></uni-icons>
</view> -->
</view>
<view v-if="!job.education" class="recommend-card" :class="{ isBut: job.isBut }">
<view class="card-content">
<view class="recommend-card-title"> 在找岗位{{ job.jobCategory }}的工作吗 </view>
<!-- <view class="recommend-card-tip">{{ job.tip }}</view> -->
<view class="recommend-card-tip">确认您的兴趣为您推荐更多合适的岗位</view>
<!-- <view class="recommend-card-line"></view> -->
<view class="recommend-card-controll">
<view class="controll-no" @click="clearfindJob(job)">不是</view>
<view class="controll-yes" @click="findJob(job)">是的</view>
</view>
</view>
<view v-if="!job.education" class="recommend-card" :class="{ isBut: job.isBut }">
<view class="card-content">
<view class="recommend-card-title">
在找岗位{{ job.jobCategory }}的工作吗
</view>
<!-- <view class="recommend-card-tip">{{ job.tip }}</view> -->
<view class="recommend-card-tip">确认您的兴趣为您推荐更多合适的岗位</view>
<!-- <view class="recommend-card-line"></view> -->
<view class="recommend-card-controll">
<view class="controll-no" @click="clearfindJob(job)">不是</view>
<view class="controll-yes" @click="findJob(job)">是的</view>
</view>
</view>
<view class="card-bottom"></view>
</view>
</view>
</template>
</custom-waterfalls-flow>
<loadmore ref="loadmoreRef"></loadmore>
<view v-if="showScrollBottom" style="height: 200px"></view>
</view>
<view class="card-bottom"></view>
</view>
<empty v-else is-position></empty>
</view>
</template>
</custom-waterfalls-flow>
<loadmore ref="loadmoreRef"></loadmore>
<view v-if="showScrollBottom" style="height: 200px"></view>
</view>
<empty v-else pdTop="200"></empty>
</view>
</view>
</view>
</view>
<!-- 筛选 -->
<select-filter ref="selectFilterModel"></select-filter>
@@ -229,7 +237,8 @@ import { useScrollDirection } from "@/hook/useScrollDirection";
import { useColumnCount } from "@/hook/useColumnCount";
const { isScrollingDown, handleScroll } = useScrollDirection();
const recommedIndexDb = useRecommedIndexedDBStore();
import AIMatch from "./AIMatch.vue";
import config from '@/config';
import AIMatch from './AIMatch.vue';
const { proxy } = getCurrentInstance();
@@ -304,12 +313,30 @@ function closeVideoTip() {
}
const handleTagClick = (tagInfo) => {
console.log("点击的标签信息:", tagInfo);
$api.msg("暂未开放");
console.log('点击的标签信息:', tagInfo);
$api.msg('暂未开放');
};
const handleItemClick = () => {
$api.msg("暂未开放");
const handleItemClick = (item) => {
switch (item) {
case '素质测评':
lightAppJssdk.navigation.hide({
url: config.Quality_assessment_URL,
});
break;
case '就业指导':
lightAppJssdk.navigation.hide({
url: config.Career_guidance,
});
break;
case '模拟面试':
lightAppJssdk.navigation.hide({
url: config.mock_interview,
});
break;
default:
$api.msg('暂未开放');
}
};
const hexToRgba = (hex, opacity) => {
@@ -1115,6 +1142,8 @@ defineExpose({ loadData });
.falls-scroll
width: 100%
height: 100%
min-height: 100vh;
position: relative
.falls
padding: 28rpx 28rpx;
.item,.m-height{

View File

@@ -1,51 +1,73 @@
<template>
<view class="app-container">
<view class="nav-filter">
<view class="filter-top" @touchmove.stop.prevent>
<scroll-view :scroll-x="true" :show-scrollbar="false" class="tab-scroll">
<view class="jobs-left">
<view class="job button-click" :class="{ active: state.tabIndex === 'all' }" @click="choosePosition('all')"> 全部 </view>
<view class="job button-click" :class="{ active: state.tabIndex === index }" v-for="(item, index) in userInfo.jobTitle" :key="index" @click="choosePosition(index)">
{{ item }}
<view class="app-container">
<view class="nav-filter">
<view class="filter-top" @touchmove.stop.prevent>
<scroll-view :scroll-x="true" :show-scrollbar="false" class="tab-scroll">
<view class="jobs-left">
<view
class="job button-click"
:class="{ active: state.tabIndex === 'all' }"
@click="choosePosition('all')"
>
全部
</view>
<view
class="job button-click"
:class="{ active: state.tabIndex === index }"
v-for="(item, index) in userInfo.jobTitle"
:key="index"
@click="choosePosition(index)"
>
{{ item }}
</view>
</view>
</scroll-view>
<view class="jobs-add button-click" @click="navTo('/pages/search/search')">
<uni-icons class="iconsearch" color="#666D7F" type="search" size="18"></uni-icons>
<text>搜索</text>
</view>
</view>
</view>
</scroll-view>
<view class="jobs-add button-click" @click="navTo('/pages/search/search')">
<uni-icons class="iconsearch" color="#666D7F" type="search" size="18"></uni-icons>
<text>搜索</text>
</view>
</view>
</view>
<view class="cards">
<scroll-view :scroll-y="true" class="tab-scroll" @scrolltolower="scrollBottom">
<view class="scroll-content">
<custom-waterfalls-flow ref="waterfallsFlowRef" :column="columnCount" :columnSpace="columnSpace" :value="list">
<template v-slot:default="job">
<view class="slot-item">
<view class="job-image btn-feel" @click="nextVideo(job)">
<image class="cover-image" :src="job.cover" mode="aspectFill"></image>
<view class="cover-triangle"></view>
<view class="cards">
<scroll-view :scroll-y="true" class="tab-scroll" @scrolltolower="scrollBottom">
<view class="scroll-content">
<custom-waterfalls-flow
ref="waterfallsFlowRef"
:column="columnCount"
:columnSpace="columnSpace"
:value="list"
>
<template v-slot:default="job">
<view class="slot-item">
<view class="job-image btn-feel" @click="nextVideo(job)">
<image class="cover-image" :src="job.cover" mode="aspectFill"></image>
<view class="cover-triangle"></view>
</view>
<view class="job-info" @click="nextDetail(job)">
<view class="salary">
<Salary-Expectation
:max-salary="job.maxSalary"
:min-salary="job.minSalary"
:is-month="true"
></Salary-Expectation>
<image v-if="job.isHot" class="flame" src="/static/icon/flame.png"></image>
</view>
<view class="title">{{ job.jobTitle }}</view>
<view class="desc">
<image class="point3" src="/static/icon/point3.png"></image>
<!-- <uni-icons type="location" size="14"></uni-icons> -->
<view class="descText">{{ job.companyName }}</view>
</view>
</view>
</view>
</template>
</custom-waterfalls-flow>
<empty v-if="!list.length"></empty>
<loadmore v-if="list.length >= pageSize" ref="loadmoreRef"></loadmore>
</view>
<view class="job-info" @click="nextDetail(job)">
<view class="salary">
<Salary-Expectation :max-salary="job.maxSalary" :min-salary="job.minSalary" :is-month="true"></Salary-Expectation>
<image v-if="job.isHot" class="flame" src="/static/icon/flame.png"></image>
</view>
<view class="title">{{ job.jobTitle }}</view>
<view class="desc">
<image class="point3" src="/static/icon/point3.png"></image>
<!-- <uni-icons type="location" size="14"></uni-icons> -->
<view class="descText">{{ job.companyName }}</view>
</view>
</view>
</view>
</template>
</custom-waterfalls-flow>
<loadmore ref="loadmoreRef"></loadmore>
</scroll-view>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup>
@@ -188,6 +210,7 @@ defineExpose({ loadData });
text-overflow: clip;
.scroll-content{
padding: 24rpx
height: calc(100% - 48rpx)
}
.nav-filter

View File

@@ -91,7 +91,9 @@ onLoad(() => {
uni.preloadPage({ url: '/pages/nearby/nearby' });
uni.preloadPage({ url: '/pages/chat/chat' });
uni.preloadPage({ url: '/packageA/pages/choiceness/choiceness' });
}, 3000);
uni.preloadPage({ url: '/packageA/pages/reservation/reservation' });
uni.preloadPage({ url: '/packageA/pages/Intendedposition/Intendedposition' });
}, 1000);
});
onShow(() => {

View File

@@ -312,8 +312,8 @@ function complete() {
<style lang="stylus" scoped>
.backdoor{
position: fixed;
left: 0;
bottom: 200rpx;
left: 24rpx;
top: 10rpx;
}
.input-nx
position: relative

View File

@@ -280,6 +280,7 @@ function chooseFileUploadTest(pam) {}
overflow: hidden
border-radius: 50%
margin-right: 22rpx;
background: #e8e8e8;
.userindo-head-img
width: 100%;
height: 100%;

View File

@@ -15,14 +15,15 @@
></image>
<image
class="card-img-flame"
v-if="item.title === '职位上新'"
v-else-if="item.title === '职位上新'"
src="/static/icon/msgtype2.png"
></image>
<image
class="card-img-flame"
v-if="item.title === '系统通知'"
v-else-if="item.title === '系统通知'"
src="/static/icon/msgtype3.png"
></image>
<image class="card-img-flame" v-else src="/static/icon/msgtype3.png"></image>
<view class="subscript" v-if="item.notReadCount || !item.isRead">
{{ item.notReadCount || '' }}
</view>
@@ -72,6 +73,9 @@ function seeDetail(item, index) {
case '系统通知':
navTo('/packageA/pages/systemNotification/systemNotification');
break;
default:
useReadMsg().markAsRead(item, index);
navTo('/packageA/pages/newJobPosition/newJobPosition');
}
}

View File

@@ -15,14 +15,15 @@
></image>
<image
class="card-img-flame"
v-if="item.title === '职位上新'"
v-else-if="item.title === '职位上新'"
src="/static/icon/msgtype2.png"
></image>
<image
class="card-img-flame"
v-if="item.title === '系统通知'"
v-else-if="item.title === '系统通知'"
src="/static/icon/msgtype3.png"
></image>
<image class="card-img-flame" v-else src="/static/icon/msgtype3.png"></image>
<view class="subscript" v-if="item.notReadCount">{{ item.notReadCount }}</view>
</view>
<view class="card-info">
@@ -59,6 +60,19 @@ async function loadData() {
function seeDetail(item) {
console.log(item);
switch (item.title) {
case '职位上新':
useReadMsg().markAsRead(item, index);
navTo('/packageA/pages/newJobPosition/newJobPosition');
break;
case '招聘会预约提醒':
useReadMsg().markAsRead(item, index);
navTo('/packageA/pages/reservation/reservation');
break;
case '系统通知':
navTo('/packageA/pages/systemNotification/systemNotification');
break;
}
}
defineExpose({ loadData });

View File

@@ -1,6 +1,6 @@
<template>
<AppLayout title="附近" :use-scroll-view="false" :show-bg-image="false">
<template #headerleft>
<template #headerleft v-if="isMiniProgram">
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
@@ -50,6 +50,9 @@ import threeComponent from './components/three.vue';
import fourComponent from './components/four.vue';
import { reactive, inject, watch, ref, onMounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
const { isMiniProgram } = storeToRefs(useUserStore());
const { $api, debounce, throttle, navBack } = inject('globalFunction');
const loadedMap = reactive([false, false, false, false]);
const swiperRefs = [ref(null), ref(null), ref(null), ref(null)];

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 995 B

After

Width:  |  Height:  |  Size: 984 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 469 B

After

Width:  |  Height:  |  Size: 414 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 515 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 B

After

Width:  |  Height:  |  Size: 209 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 543 B

After

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 B

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 B

After

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 B

After

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 751 B

After

Width:  |  Height:  |  Size: 739 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 B

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 826 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 826 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 843 B

After

Width:  |  Height:  |  Size: 834 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 542 B

After

Width:  |  Height:  |  Size: 514 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 508 B

After

Width:  |  Height:  |  Size: 504 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 443 B

After

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 522 B

After

Width:  |  Height:  |  Size: 500 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 564 B

After

Width:  |  Height:  |  Size: 557 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 734 B

After

Width:  |  Height:  |  Size: 730 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 987 B

After

Width:  |  Height:  |  Size: 764 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 740 B

After

Width:  |  Height:  |  Size: 736 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 683 B

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 775 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 778 B

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 514 B

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 B

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1008 B

After

Width:  |  Height:  |  Size: 983 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 776 B

After

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 209 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 555 B

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 339 B

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 B

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 391 B

After

Width:  |  Height:  |  Size: 382 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 627 B

After

Width:  |  Height:  |  Size: 609 B

Some files were not shown because too many files have changed in this diff Show More