init
This commit is contained in:
148
components/AppLayout/AppLayout.vue
Normal file
148
components/AppLayout/AppLayout.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<view class="app-custom-root">
|
||||
<view
|
||||
class="app-container"
|
||||
:style="{ backgroundColor: backGorundColor, backgroundImage: showBgImage && `url(${img})` }"
|
||||
>
|
||||
<!-- 顶部头部区域 -->
|
||||
<view
|
||||
class="container-header"
|
||||
:style="border ? { borderBottom: `2rpx solid ${borderColor}` } : { borderBottom: 'none' }"
|
||||
>
|
||||
<view class="header-btnLf">
|
||||
<slot name="headerleft"></slot>
|
||||
</view>
|
||||
<view class="header-title">
|
||||
<view>{{ title }}</view>
|
||||
<view v-show="subTitle" class="subtitle-text">{{ subTitle }}</view>
|
||||
</view>
|
||||
<view class="header-btnRi">
|
||||
<slot name="headerright"></slot>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 主体不可滚动 headContent -->
|
||||
<view class="container-headContent">
|
||||
<slot name="headContent"></slot>
|
||||
</view>
|
||||
|
||||
<!-- 主体内容区域 -->
|
||||
<view class="container-main">
|
||||
<scroll-view v-if="useScrollView" scroll-y class="main-scroll" @scrolltolower="handleScrollToLower">
|
||||
<slot></slot>
|
||||
</scroll-view>
|
||||
<view class="main-scroll" v-else><slot></slot></view>
|
||||
</view>
|
||||
|
||||
<!-- 底部 footer -->
|
||||
<view class="container-footer">
|
||||
<slot name="footer"></slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import img from '@/static/icon/background2.png';
|
||||
const emit = defineEmits(['onScrollBottom']);
|
||||
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '标题',
|
||||
},
|
||||
border: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
borderColor: {
|
||||
type: String,
|
||||
default: '#F4F4F4',
|
||||
},
|
||||
subTitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
backGorundColor: {
|
||||
type: String,
|
||||
default: '#ffffff',
|
||||
},
|
||||
useScrollView: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showBgImage: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const handleScrollToLower = () => {
|
||||
emit('onScrollBottom');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-custom-root {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
width: 100vw;
|
||||
height: calc(100% - var(--window-bottom));
|
||||
overflow: hidden;
|
||||
}
|
||||
.app-container {
|
||||
// background-image: url('@/static/icon/background2.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 0;
|
||||
background-size: 100% 728rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
.container-header {
|
||||
min-height: calc(88rpx - 14rpx);
|
||||
text-align: center;
|
||||
line-height: calc(88rpx - 14rpx);
|
||||
font-size: 32rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 7rpx 3rpx;
|
||||
.header-title {
|
||||
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
|
||||
color: #000000;
|
||||
font-weight: bold;
|
||||
.subtitle-text {
|
||||
font-family: 'PingFangSC-Regular', 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
line-height: 45rpx;
|
||||
margin-top: -16rpx;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
}
|
||||
.header-btnLf,
|
||||
.header-btnRi {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
width: calc(60rpx * 3);
|
||||
}
|
||||
.header-btnRi {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container-main {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-scroll {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
92
components/CollapseTransition/CollapseTransition.vue
Normal file
92
components/CollapseTransition/CollapseTransition.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<!-- CollapseTransition.vue -->
|
||||
<template>
|
||||
<view :style="wrapStyle" class="collapse-wrapper">
|
||||
<view ref="contentRef" class="content-inner">
|
||||
<slot />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, nextTick } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
show: Boolean,
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 300,
|
||||
},
|
||||
});
|
||||
|
||||
const wrapStyle = ref({
|
||||
height: '0rpx',
|
||||
opacity: 0,
|
||||
overflow: 'hidden',
|
||||
transition: `all ${props.duration}ms ease`,
|
||||
});
|
||||
|
||||
const contentRef = ref(null);
|
||||
|
||||
// 获取高度(兼容 H5 + 小程序)
|
||||
function getContentHeight() {
|
||||
return new Promise((resolve) => {
|
||||
const query = uni.createSelectorQuery().in(this ? this : undefined);
|
||||
query
|
||||
.select('.content-inner')
|
||||
.boundingClientRect((data) => {
|
||||
resolve(data?.height || 0);
|
||||
})
|
||||
.exec();
|
||||
});
|
||||
}
|
||||
|
||||
// 动画执行
|
||||
async function expand() {
|
||||
const height = await getContentHeight();
|
||||
wrapStyle.value = {
|
||||
height: height + 'px',
|
||||
opacity: 1,
|
||||
overflow: 'hidden',
|
||||
transition: `all ${props.duration}ms ease`,
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
wrapStyle.value.height = 'auto';
|
||||
}, props.duration);
|
||||
}
|
||||
|
||||
async function collapse() {
|
||||
const height = await getContentHeight();
|
||||
wrapStyle.value = {
|
||||
height: height + 'px',
|
||||
opacity: 1,
|
||||
overflow: 'hidden',
|
||||
transition: 'none',
|
||||
};
|
||||
|
||||
// 等待下一帧开始收起动画
|
||||
await nextTick();
|
||||
requestAnimationFrame(() => {
|
||||
wrapStyle.value = {
|
||||
height: '0rpx',
|
||||
opacity: 0,
|
||||
overflow: 'hidden',
|
||||
transition: `all ${props.duration}ms ease`,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(val) => {
|
||||
if (val) expand();
|
||||
else collapse();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.collapse-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
140
components/MsgTips/MsgTips.vue
Normal file
140
components/MsgTips/MsgTips.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<uni-popup
|
||||
ref="popup"
|
||||
type="center"
|
||||
borderRadius="10px 10px 10px 10px"
|
||||
background-color="#F6F6F6"
|
||||
:mask-click="maskClick"
|
||||
>
|
||||
<view class="popup-content">
|
||||
<view class="text-h2">
|
||||
<image v-if="icon" class="text-h2-icon" :src="icon"></image>
|
||||
{{ title }}
|
||||
</view>
|
||||
<text class="text-content button-click">{{ content }}</text>
|
||||
<template v-if="showButton">
|
||||
<uni-button class="popup-button button-click" v-if="isTip" @click="close">{{ buttonText }}</uni-button>
|
||||
<view v-else class="confirm-btns">
|
||||
<uni-button class="popup-button button-click" @click="close">{{ cancelText }}</uni-button>
|
||||
<uni-button class="popup-button button-click" @click="confirm">{{ confirmText }}</uni-button>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'MsgTips',
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
default: '', // 如:'/static/success.png'
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '提示',
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: '这是提示内容',
|
||||
},
|
||||
buttonText: {
|
||||
type: String,
|
||||
default: '我知道了',
|
||||
},
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: '取消',
|
||||
},
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: '保存并退出',
|
||||
},
|
||||
showButton: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
maskClosable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
isTip: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
maskClick: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
// mounted() {
|
||||
// this.$refs.popup.open('center');
|
||||
// },
|
||||
methods: {
|
||||
open() {
|
||||
this.$refs.popup.open('center');
|
||||
},
|
||||
close() {
|
||||
this.$refs.popup.close('center');
|
||||
},
|
||||
confirm() {},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.popup-content {
|
||||
display: flex;
|
||||
padding: 40rpx;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
width: calc(630rpx - 80rpx);
|
||||
.text-h2 {
|
||||
font-weight: 500;
|
||||
font-size: 36rpx;
|
||||
color: #333333;
|
||||
line-height: 42rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.text-h2-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
.text-content {
|
||||
margin-top: 12rpx;
|
||||
font-weight: 400;
|
||||
font-size: 28rpx;
|
||||
color: #6c7282;
|
||||
line-height: 33rpx;
|
||||
text-align: justified;
|
||||
}
|
||||
.popup-button {
|
||||
background-color: #256bfa;
|
||||
color: white;
|
||||
border-radius: 30px;
|
||||
text-align: center;
|
||||
height: 90rpx;
|
||||
line-height: 90rpx;
|
||||
border-radius: 12rpx 12rpx 12rpx 12rpx;
|
||||
margin-top: 48rpx;
|
||||
width: 100%;
|
||||
}
|
||||
.confirm-btns {
|
||||
display: flex;
|
||||
.popup-button {
|
||||
width: 260rpx;
|
||||
}
|
||||
.popup-button:first-child {
|
||||
background-color: #e8eaee;
|
||||
margin-right: 30rpx;
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
26
components/NoBouncePage/NoBouncePage.vue
Normal file
26
components/NoBouncePage/NoBouncePage.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<!-- components/NoBouncePage.vue -->
|
||||
<template>
|
||||
<view class="no-bounce-page">
|
||||
<scroll-view scroll-y :show-scrollbar="false" class="scroll-area">
|
||||
<slot />
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style scoped>
|
||||
.no-bounce-page {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
overscroll-behavior: none; /* 禁止页面级回弹 */
|
||||
}
|
||||
|
||||
.scroll-area {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain; /* 禁止滚动内容回弹 */
|
||||
-webkit-overflow-scrolling: touch; /* 保留 iOS 惯性滚动 */
|
||||
}
|
||||
</style>
|
||||
17
components/Salary-Expectation/Salary-Expectation.vue
Normal file
17
components/Salary-Expectation/Salary-Expectation.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<view>{{ salaryText }}</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject, computed } from 'vue';
|
||||
import useDictStore from '../../stores/useDictStore';
|
||||
const { minSalary, maxSalary, isMonth } = defineProps(['minSalary', 'maxSalary', 'isMonth']);
|
||||
|
||||
const salaryText = computed(() => {
|
||||
if (!minSalary || !maxSalary) return '面议';
|
||||
if (isMonth) {
|
||||
return `${minSalary}-${maxSalary}/月`;
|
||||
}
|
||||
return `${minSalary / 1000}k-${maxSalary / 1000}k`;
|
||||
});
|
||||
</script>
|
||||
312
components/TikTok/TikTok.vue
Normal file
312
components/TikTok/TikTok.vue
Normal file
@@ -0,0 +1,312 @@
|
||||
<template>
|
||||
<swiper
|
||||
class="m-tiktok-video-swiper"
|
||||
circular
|
||||
@change="swiperChange"
|
||||
:current="state.current"
|
||||
:vertical="true"
|
||||
duration="300"
|
||||
>
|
||||
<swiper-item v-for="(item, index) in state.displaySwiperList" :key="index">
|
||||
<view class="swiper-item" @click="(e) => handleClick(index, e)">
|
||||
<video
|
||||
:id="`video__${index}`"
|
||||
:controls="controls"
|
||||
:autoplay="false"
|
||||
:loop="loop"
|
||||
@ended="ended"
|
||||
@controlstoggle="controlstoggle"
|
||||
@play="onPlay"
|
||||
@error="emits('error')"
|
||||
class="m-tiktok-video-player"
|
||||
:src="item.src || item.explainUrl"
|
||||
v-if="index === 0 || !state.isFirstLoad"
|
||||
></video>
|
||||
<view class="cover-triangle" v-if="pause"></view>
|
||||
<image
|
||||
v-if="item.poster && state.displayIndex != index"
|
||||
:src="item.poster"
|
||||
class="m-tiktok-video-poster"
|
||||
mode="aspectFit"
|
||||
></image>
|
||||
<slot :item="item"></slot>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, getCurrentInstance, watch, nextTick } from 'vue';
|
||||
import { onLoad, onUnload } from '@dcloudio/uni-app';
|
||||
const _this = getCurrentInstance();
|
||||
const emits = defineEmits(['play', 'error', 'loadMore', 'change', 'controlstoggle', 'click', 'ended']);
|
||||
|
||||
const lastTapTime = ref(0);
|
||||
const pause = ref(false);
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 视频列表
|
||||
*/
|
||||
videoList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
/**
|
||||
* 是否循环播放一个视频
|
||||
*/
|
||||
loop: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/**
|
||||
* 显示原生控制栏
|
||||
*/
|
||||
controls: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/**
|
||||
* 是否自动播放
|
||||
*/
|
||||
autoplay: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/**
|
||||
* 是否自动滚动播放
|
||||
*/
|
||||
autoChange: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* 滚动加载阈值(即播放到剩余多少个之后触发加载更多
|
||||
*/
|
||||
loadMoreOffsetCount: {
|
||||
type: Number,
|
||||
default: 2,
|
||||
},
|
||||
/**
|
||||
* 暂停 1 单机暂停; 2 双击暂停; 0关闭
|
||||
*/
|
||||
pauseType: {
|
||||
type: Number,
|
||||
default: 2,
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
originList: [], // 源数据
|
||||
displaySwiperList: [], // swiper需要的数据
|
||||
displayIndex: 0, // 用于显示swiper的真正的下标数值只有:0,1,2。
|
||||
originIndex: 0, // 记录源数据的下标
|
||||
current: 0,
|
||||
oid: 0,
|
||||
showControls: '',
|
||||
toggleShow: true, // 显示面板
|
||||
videoContexts: [],
|
||||
isFirstLoad: true,
|
||||
});
|
||||
|
||||
const initVideoContexts = () => {
|
||||
state.videoContexts = [
|
||||
uni.createVideoContext('video__0', _this),
|
||||
uni.createVideoContext('video__1', _this),
|
||||
uni.createVideoContext('video__2', _this),
|
||||
];
|
||||
};
|
||||
|
||||
const onPlay = (e) => {
|
||||
emits('play', e);
|
||||
};
|
||||
|
||||
const setVideoRef = (el, index) => {
|
||||
if (el) {
|
||||
videoRefs.value[index] = el;
|
||||
}
|
||||
};
|
||||
|
||||
function handleClick(index, e) {
|
||||
const now = Date.now();
|
||||
switch (props.pauseType) {
|
||||
case 1:
|
||||
if (pause.value) {
|
||||
state.videoContexts[index].play();
|
||||
pause.value = false;
|
||||
} else {
|
||||
state.videoContexts[index].pause();
|
||||
pause.value = true;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
if (now - lastTapTime.value < 300) {
|
||||
if (pause.value) {
|
||||
state.videoContexts[index].play();
|
||||
pause.value = false;
|
||||
} else {
|
||||
state.videoContexts[index].pause();
|
||||
pause.value = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
lastTapTime.value = now;
|
||||
state.toggleShow = !state.toggleShow;
|
||||
emits('click', e);
|
||||
}
|
||||
function ended() {
|
||||
// 自动切换下一个视频
|
||||
if (props.autoChange) {
|
||||
if (state.displayIndex < 2) {
|
||||
state.current = state.displayIndex + 1;
|
||||
} else {
|
||||
state.current = 0;
|
||||
}
|
||||
}
|
||||
emits('ended');
|
||||
}
|
||||
/**
|
||||
* 初始一个显示的swiper数据
|
||||
* @originIndex 从源数据的哪个开始显示默认0,如从其他页面跳转进来,要显示第n个,这个参数就是他的下标
|
||||
*/
|
||||
function initSwiperData(originIndex = state.originIndex) {
|
||||
const originListLength = state.originList.length; // 源数据长度
|
||||
const displayList = [];
|
||||
displayList[state.displayIndex] = state.originList[originIndex];
|
||||
displayList[state.displayIndex - 1 == -1 ? 2 : state.displayIndex - 1] =
|
||||
state.originList[originIndex - 1 == -1 ? originListLength - 1 : originIndex - 1];
|
||||
displayList[state.displayIndex + 1 == 3 ? 0 : state.displayIndex + 1] =
|
||||
state.originList[originIndex + 1 == originListLength ? 0 : originIndex + 1];
|
||||
state.displaySwiperList = displayList;
|
||||
|
||||
if (state.oid >= state.originList.length) {
|
||||
state.oid = 0;
|
||||
}
|
||||
if (state.oid < 0) {
|
||||
state.oid = state.originList.length - 1;
|
||||
}
|
||||
// 暂停所有视频
|
||||
state.videoContexts.map((item) => item?.stop());
|
||||
setTimeout(() => {
|
||||
// 当前视频
|
||||
if (props.autoplay) {
|
||||
uni.createVideoContext(`video__${state.displayIndex}`, _this).play();
|
||||
}
|
||||
}, 500);
|
||||
// 数据改变
|
||||
emits('change', {
|
||||
index: originIndex,
|
||||
detail: state.originList[originIndex],
|
||||
});
|
||||
// 加载更多
|
||||
var pCount = state.originList.length - props.loadMoreOffsetCount;
|
||||
if (originIndex == pCount) {
|
||||
emits('loadMore');
|
||||
}
|
||||
}
|
||||
/**
|
||||
* swiper滑动时候
|
||||
*/
|
||||
function swiperChange(event) {
|
||||
const { current } = event.detail;
|
||||
state.isFirstLoad = false;
|
||||
const originListLength = state.originList.length; // 源数据长度
|
||||
// 向后滚动
|
||||
if (state.displayIndex - current == 2 || state.displayIndex - current == -1) {
|
||||
state.originIndex = state.originIndex + 1 == originListLength ? 0 : state.originIndex + 1;
|
||||
state.displayIndex = state.displayIndex + 1 == 3 ? 0 : state.displayIndex + 1;
|
||||
state.oid = state.originIndex - 1;
|
||||
initSwiperData(state.originIndex);
|
||||
}
|
||||
// 如果两者的差为-2或者1则是向前滑动
|
||||
else if (state.displayIndex - current == -2 || state.displayIndex - current == 1) {
|
||||
state.originIndex = state.originIndex - 1 == -1 ? originListLength - 1 : state.originIndex - 1;
|
||||
state.displayIndex = state.displayIndex - 1 == -1 ? 2 : state.displayIndex - 1;
|
||||
state.oid = state.originIndex + 1;
|
||||
initSwiperData(state.originIndex);
|
||||
}
|
||||
state.toggleShow = true;
|
||||
}
|
||||
|
||||
function controlstoggle(e) {
|
||||
state.showControls = e.detail.show;
|
||||
emits('controlstoggle', e);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.videoList,
|
||||
() => {
|
||||
if (props.videoList?.length) {
|
||||
state.originList = props.videoList;
|
||||
if (state.isFirstLoad || !state.videoContexts?.length) {
|
||||
initSwiperData();
|
||||
initVideoContexts();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
let loadTimer = null;
|
||||
onLoad(() => {
|
||||
// 为了首次只加载一条视频(提高首次加载性能),延迟加载后续视频
|
||||
loadTimer = setTimeout(() => {
|
||||
state.isFirstLoad = false;
|
||||
clearTimeout(loadTimer);
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
onUnload(() => {
|
||||
clearTimeout(loadTimer);
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
initSwiperData,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.m-tiktok-video-swiper,
|
||||
.m-tiktok-video-player {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #000;
|
||||
}
|
||||
.m-tiktok-video-swiper {
|
||||
.swiper-item {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
.m-tiktok-video-poster {
|
||||
background-color: #000;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.cover-triangle{
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%)
|
||||
width: 132rpx
|
||||
height: 132rpx
|
||||
border-radius: 50%;
|
||||
background: rgba(0,0,0,0.3)
|
||||
}
|
||||
.cover-triangle::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-40%, -50%) rotate(90deg);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 40rpx solid transparent;
|
||||
border-right: 40rpx solid transparent;
|
||||
border-bottom: 60rpx solid #fff;
|
||||
}
|
||||
</style>
|
||||
26
components/convert-distance/convert-distance.vue
Normal file
26
components/convert-distance/convert-distance.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<span style="padding-left: 16rpx">{{ distance }}</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject, computed, watch } from 'vue';
|
||||
const { haversine, getDistanceFromLatLonInKm } = inject('globalFunction');
|
||||
const props = defineProps(['alat', 'along', 'blat', 'blong']);
|
||||
|
||||
const distance = computed(() => {
|
||||
const distance2 = getDistanceFromLatLonInKm(props.alat, props.along, props.blat, props.blong);
|
||||
// console.log(distance2, props.alat, props.along, props.blat, props.blong);
|
||||
const { km, m } = distance2;
|
||||
if (!props.alat && !props.along) {
|
||||
return '--km';
|
||||
}
|
||||
if (km > 1) {
|
||||
return km.toFixed(2) + 'km';
|
||||
} else {
|
||||
return m.toFixed(2) + 'm';
|
||||
}
|
||||
return '';
|
||||
});
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
11
components/dict-Label/dict-Label.vue
Normal file
11
components/dict-Label/dict-Label.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<span>{{ dictLabel(dictType, value) }}</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import useDictStore from '../../stores/useDictStore';
|
||||
const { dictType, value } = defineProps(['value', 'dictType']);
|
||||
const { complete, dictLabel } = useDictStore();
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
11
components/dict-tree-Label/dict-tree-Label.vue
Normal file
11
components/dict-tree-Label/dict-tree-Label.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<span>{{ industryLabel(dictType, value) }}</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import useDictStore from '../../stores/useDictStore';
|
||||
const { dictType, value } = defineProps(['value', 'dictType']);
|
||||
const { complete, industryLabel } = useDictStore();
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
84
components/empty/empty.vue
Normal file
84
components/empty/empty.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<view class="empty" :style="{ background: bgcolor, marginTop: mrTop + 'rpx' }">
|
||||
<view class="ty_content" :style="{ paddingTop: pdTop + 'rpx' }">
|
||||
<view class="content_top btn-shaky">
|
||||
<image v-if="pictrue" :src="pictrue" mode=""></image>
|
||||
<image v-else src="@/static/icon/empty.png" mode=""></image>
|
||||
</view>
|
||||
<view class="content_c">{{ content }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
props: {
|
||||
content: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '暂时没有结果,下一秒也许就有惊喜',
|
||||
},
|
||||
bgcolor: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'transparent',
|
||||
},
|
||||
pdTop: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '80',
|
||||
},
|
||||
mrTop: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '20',
|
||||
},
|
||||
pictrue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
methods: {},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.empty {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
.ty_content {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
transform: translate(-50%, 0);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.content_top {
|
||||
width: 450rpx;
|
||||
height: 322rpx;
|
||||
}
|
||||
.content_c {
|
||||
margin-top: 32rpx;
|
||||
color: #6a707c;
|
||||
font-size: 28rpx;
|
||||
width: 512rpx;
|
||||
height: 44rpx;
|
||||
font-weight: 400;
|
||||
font-size: 32rpx;
|
||||
color: #000000;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
254
components/expected-station/expected-station.vue
Normal file
254
components/expected-station/expected-station.vue
Normal file
@@ -0,0 +1,254 @@
|
||||
<template>
|
||||
<view class="expected-station">
|
||||
<view class="sex-search" v-if="search">
|
||||
<uni-icons class="iconsearch" type="search" size="20"></uni-icons>
|
||||
<input class="uni-input searchinput" confirm-type="search" />
|
||||
</view>
|
||||
<view class="sex-content">
|
||||
<scroll-view :show-scrollbar="false" :scroll-y="true" class="sex-content-left">
|
||||
<view
|
||||
v-for="item in copyTree"
|
||||
:key="item.id"
|
||||
class="left-list-btn"
|
||||
:class="{ 'left-list-btned': item.id === leftValue.id }"
|
||||
@click="changeStationLog(item)"
|
||||
>
|
||||
{{ item.label }}
|
||||
<!-- <view class="positionNum" v-show="item.checkednumber">
|
||||
{{ item.checkednumber }}
|
||||
</view> -->
|
||||
</view>
|
||||
</scroll-view>
|
||||
<scroll-view
|
||||
:show-scrollbar="false"
|
||||
:scroll-top="scrollTop"
|
||||
@scroll="scrollTopBack"
|
||||
:scroll-y="true"
|
||||
class="sex-content-right"
|
||||
>
|
||||
<view v-for="item in rightValue" :key="item.id">
|
||||
<view class="secondary-title">{{ item.label }}</view>
|
||||
<view class="grid-sex">
|
||||
<view
|
||||
v-for="item in item.children"
|
||||
:key="item.id"
|
||||
:class="{ 'sex-right-btned': item.checked }"
|
||||
class="sex-right-btn"
|
||||
@click="addItem(item)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</view>
|
||||
<!-- <view class="sex-right-btn sex-right-btned" @click="addItem()">客户经理</view>
|
||||
<view class="sex-right-btn" @click="addItem()">客户经理</view> -->
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'expected-station',
|
||||
data() {
|
||||
return {
|
||||
leftValue: {},
|
||||
rightValue: [],
|
||||
stationCateLog: 0,
|
||||
copyTree: [],
|
||||
scrollTop: 0,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
station: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
search: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.copyTree = this.station;
|
||||
if (this.copyTree.length) {
|
||||
this.leftValue = this.copyTree[0];
|
||||
this.rightValue = this.copyTree[0].children;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
station(newVal) {
|
||||
this.copyTree = this.station;
|
||||
if (this.copyTree.length) {
|
||||
this.leftValue = this.copyTree[0];
|
||||
this.rightValue = this.copyTree[0].children;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
changeStationLog(item) {
|
||||
this.leftValue = item;
|
||||
this.rightValue = item.children;
|
||||
this.scrollTop = 0;
|
||||
},
|
||||
scrollTopBack(e) {
|
||||
this.scrollTop = e.detail.scrollTop;
|
||||
},
|
||||
addItem(item) {
|
||||
let titiles = [];
|
||||
let labels = [];
|
||||
let count = 0;
|
||||
|
||||
// 先统计已选中的职位数量
|
||||
for (const firstLayer of this.copyTree) {
|
||||
for (const secondLayer of firstLayer.children) {
|
||||
for (const thirdLayer of secondLayer.children) {
|
||||
if (thirdLayer.checked) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const firstLayer of this.copyTree) {
|
||||
firstLayer.checkednumber = 0; // 初始化当前层级的 checked 计数
|
||||
for (const secondLayer of firstLayer.children) {
|
||||
for (const thirdLayer of secondLayer.children) {
|
||||
// **如果是当前点击的职位**
|
||||
if (thirdLayer.id === item.id) {
|
||||
if (!thirdLayer.checked && count >= 5) {
|
||||
// 如果已经选了 5 个,并且点击的是未选中的职位,则禁止选择
|
||||
uni.showToast({
|
||||
title: `最多选择5个职位`,
|
||||
icon: 'none',
|
||||
});
|
||||
continue; // 跳过后续逻辑,继续循环
|
||||
}
|
||||
// 切换选中状态
|
||||
thirdLayer.checked = !thirdLayer.checked;
|
||||
}
|
||||
// 统计被选中的第三层节点
|
||||
if (thirdLayer.checked) {
|
||||
titiles.push(`${thirdLayer.id}`);
|
||||
labels.push(`${thirdLayer.label}`);
|
||||
firstLayer.checkednumber++; // 累加计数器
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
titiles = titiles.join(',');
|
||||
labels = labels.join(',');
|
||||
this.$emit('onChange', {
|
||||
ids: titiles,
|
||||
labels,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.secondary-title{
|
||||
color: #333333
|
||||
font-size: 28rpx
|
||||
padding: 40rpx 0 10rpx 30rpx;
|
||||
}
|
||||
.expected-station{
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%
|
||||
}
|
||||
.sex-search
|
||||
width: calc(100% - 28rpx - 28rpx);
|
||||
padding: 10rpx 28rpx;
|
||||
display: grid;
|
||||
// grid-template-columns: 50rpx auto;
|
||||
position: relative;
|
||||
.iconsearch
|
||||
position: absolute;
|
||||
left: 40rpx;
|
||||
top: 20rpx;
|
||||
.searchinput
|
||||
border-radius: 10rpx;
|
||||
background: #FFFFFF;
|
||||
padding: 10rpx 0 10rpx 58rpx;
|
||||
.sex-content
|
||||
background: #FFFFFF;
|
||||
// border-radius: 20rpx;
|
||||
width: 100%;
|
||||
margin-top: 20rpx;
|
||||
display: flex;
|
||||
border-bottom: 2px solid #D9D9D9;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
.sex-content-left
|
||||
width: 198rpx;
|
||||
padding: 20rpx 0 0 0;
|
||||
.left-list-btn
|
||||
padding: 0 40rpx 0 24rpx;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
height: 100rpx;
|
||||
text-align: center;
|
||||
color: #606060;
|
||||
font-size: 28rpx;
|
||||
position: relative
|
||||
margin-top: 60rpx
|
||||
.left-list-btn:first-child
|
||||
margin-top: 0
|
||||
// .positionNum
|
||||
// position: absolute
|
||||
// right: 0
|
||||
// top: 50%;
|
||||
// transform: translate(0, -50%)
|
||||
// color: #FFFFFF
|
||||
// background: #4778EC
|
||||
// border-radius: 50%
|
||||
// width: 36rpx;
|
||||
// height: 36rpx;
|
||||
.left-list-btned
|
||||
color: #4778EC;
|
||||
position: relative;
|
||||
// .left-list-btned::after
|
||||
// position: absolute;
|
||||
// left: 20rpx;
|
||||
// content: '';
|
||||
// width: 7rpx;
|
||||
// height: 38rpx;
|
||||
// background: #4778EC;
|
||||
// border-radius: 0rpx 0rpx 0rpx 0rpx;
|
||||
|
||||
.sex-content-right
|
||||
// border-left: 2px solid #D9D9D9;
|
||||
background: #F6F6F6;
|
||||
flex: 1;
|
||||
.grid-sex
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
place-items: center;
|
||||
padding: 0 20rpx 40rpx 20rpx;
|
||||
.sex-right-btn
|
||||
width: 228rpx;
|
||||
height: 80rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 41rpx;
|
||||
text-align: center;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 12rpx;
|
||||
margin-top: 30rpx;
|
||||
background: #E8EAEE;
|
||||
color: #606060;
|
||||
.sex-right-btned
|
||||
font-weight: 500
|
||||
width: 224rpx;
|
||||
height: 76rpx;
|
||||
background: rgba(37,107,250,0.06);
|
||||
border: 2rpx solid #256BFA;
|
||||
color: #256BFA
|
||||
</style>
|
||||
51
components/loadmore/loadmore.vue
Normal file
51
components/loadmore/loadmore.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<view class="more">
|
||||
<uni-load-more iconType="circle" :status="status" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'loadmore',
|
||||
data() {
|
||||
return {
|
||||
status: 'more',
|
||||
statusTypes: [
|
||||
{
|
||||
value: 'more',
|
||||
text: '加载前',
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
value: 'loading',
|
||||
text: '加载中',
|
||||
checked: false,
|
||||
},
|
||||
{
|
||||
value: 'noMore',
|
||||
text: '没有更多',
|
||||
checked: false,
|
||||
},
|
||||
],
|
||||
contentText: {
|
||||
contentdown: '查看更多',
|
||||
contentrefresh: '加载中',
|
||||
contentnomore: '没有更多',
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
change(state) {
|
||||
this.status = state;
|
||||
},
|
||||
clickLoadMore(e) {
|
||||
uni.showToast({
|
||||
icon: 'none',
|
||||
title: '当前状态:' + e.detail.status,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
18
components/matchingDegree/matchingDegree.vue
Normal file
18
components/matchingDegree/matchingDegree.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<view>{{ matchingText }}</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 匹配度
|
||||
import { inject, computed } from 'vue';
|
||||
const { job } = defineProps(['job']);
|
||||
const { similarityJobs, throttle } = inject('globalFunction');
|
||||
|
||||
const matchingText = computed(() => {
|
||||
if (!job) return '';
|
||||
const matching = similarityJobs.calculationMatchingDegree(job);
|
||||
return matching ? '匹配度 ' + matching.overallMatch : '';
|
||||
});
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
357
components/md-render/md-render.vue
Normal file
357
components/md-render/md-render.vue
Normal file
@@ -0,0 +1,357 @@
|
||||
<template>
|
||||
<view class="markdown-body">
|
||||
<rich-text class="markdownRich" id="markdown-content" :nodes="renderedHtml" @itemclick="handleItemClick" />
|
||||
<!-- <view class="markdown-body" v-html="renderedHtml"></view> -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, inject } from 'vue';
|
||||
import { parseMarkdown, codeDataList } from '@/utils/markdownParser';
|
||||
const { navTo } = inject('globalFunction');
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
typing: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const renderedHtml = computed(() => parseMarkdown(props.content));
|
||||
|
||||
const handleItemClick = (e) => {
|
||||
let { attrs } = e.detail.node;
|
||||
console.log(attrs);
|
||||
let { 'data-copy-index': codeDataIndex, 'data-job-id': jobId, class: className } = attrs;
|
||||
switch (className) {
|
||||
case 'custom-card':
|
||||
return navTo('/packageA/pages/post/post?jobId=' + jobId);
|
||||
case 'custom-more':
|
||||
return navTo('/packageA/pages/moreJobs/moreJobs?jobId=' + jobId);
|
||||
case 'copy-btn':
|
||||
uni.setClipboardData({
|
||||
data: codeDataList[codeDataIndex],
|
||||
showToast: false,
|
||||
success() {
|
||||
uni.showToast({
|
||||
title: '复制成功',
|
||||
icon: 'none',
|
||||
});
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.cursor-blink {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 1.2em;
|
||||
background-color: black;
|
||||
animation: blink 1s step-start infinite;
|
||||
margin-left: 2px;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.markdown-body {
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
// line-height: 1;
|
||||
}
|
||||
ul {
|
||||
// display: block;
|
||||
padding-inline-start: 40rpx;
|
||||
li {
|
||||
margin-bottom: -30rpx;
|
||||
display: list-item;
|
||||
list-style-position: outside; /* 确保数字/点在左侧 */
|
||||
word-break: break-word;
|
||||
p {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
li:nth-child(1) {
|
||||
margin-top: -20rpx;
|
||||
}
|
||||
}
|
||||
ol {
|
||||
li {
|
||||
display: list-item;
|
||||
list-style-position: outside; /* 确保数字/点在左侧 */
|
||||
word-break: break-word;
|
||||
p {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
li:nth-child(1) {
|
||||
margin-top: -20rpx;
|
||||
}
|
||||
}
|
||||
p {
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
.markdown-body {
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* 表格 */
|
||||
table {
|
||||
// display: block; /* 让表格可滚动 */
|
||||
// width: 100%;
|
||||
// overflow-x: auto;
|
||||
// white-space: nowrap; /* 防止单元格内容换行 */
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 16rpx;
|
||||
border: 2rpx solid #ddd;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 隔行变色 */
|
||||
tr:nth-child(even) {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
/* 鼠标悬停效果 */
|
||||
tr:hover {
|
||||
background-color: #f1f1f1;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
/* 代码块 */
|
||||
pre,
|
||||
code {
|
||||
user-select: text;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
pre code {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
min-height: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
pre code {
|
||||
white-space: pre-wrap; /* 保证换行处理 */
|
||||
}
|
||||
|
||||
/* #ifndef MP-WEIXIN */
|
||||
pre code:empty,
|
||||
pre code:not(:has(*)):not(:has(text)) {
|
||||
display: none;
|
||||
}
|
||||
/* #endif */
|
||||
/* #ifdef MP-WEIXIN */
|
||||
pre code:empty {
|
||||
display: none;
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
.code-container {
|
||||
position: relative;
|
||||
border-radius: 10rpx;
|
||||
overflow: hidden;
|
||||
font-size: 28rpx;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.code-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10rpx 20rpx;
|
||||
background: #161b22;
|
||||
color: #888;
|
||||
font-size: 24rpx;
|
||||
border-radius: 10rpx 10rpx 0 0;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #ddd;
|
||||
border: none;
|
||||
padding: 6rpx 16rpx;
|
||||
font-size: 24rpx;
|
||||
cursor: pointer;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
.copy-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #259939;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
pre.hljs {
|
||||
padding: 0 24rpx;
|
||||
margin: 0;
|
||||
border-radius: 0 0 16rpx 16rpx;
|
||||
background-color: #ffffff;
|
||||
padding: 20rpx;
|
||||
overflow-x: auto;
|
||||
font-size: 24rpx;
|
||||
margin-top: 10rpx;
|
||||
margin-top: -66rpx;
|
||||
}
|
||||
pre code {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: block;
|
||||
white-space: pre-wrap; /* 允许自动换行 */
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: decimal-leading-zero;
|
||||
padding-left: 60rpx;
|
||||
}
|
||||
|
||||
.line-num {
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
color: #666;
|
||||
margin-right: 20rpx;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#markdown-content ::v-deep div > pre:first-of-type {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
#markdown-content ::v-deep > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.markdownRich > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="stylus">
|
||||
.custom-more{
|
||||
display: flex
|
||||
justify-content: flex-end
|
||||
color: #256BFA
|
||||
padding-top: 5rpx
|
||||
padding-bottom: 14rpx
|
||||
.more-icon{
|
||||
width: 60rpx;
|
||||
height: 40rpx;
|
||||
background: url('@/static/svg/seemore.svg') center center no-repeat;
|
||||
background-size: 100% 100%
|
||||
}
|
||||
}
|
||||
.custom-card
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0rpx 0rpx 8rpx 0rpx rgba(0,0,0,0.04);
|
||||
border-radius: 20rpx 20rpx 20rpx 20rpx;
|
||||
padding: 28rpx 24rpx;
|
||||
font-weight: 400;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
margin-bottom: 20rpx;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column
|
||||
.card-title
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between
|
||||
.title-text
|
||||
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
|
||||
max-width: calc(100% - 160rpx);
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
font-size: 30rpx
|
||||
.card-salary
|
||||
font-family: DIN-Medium;
|
||||
font-size: 28rpx;
|
||||
color: #FF6E1C;
|
||||
|
||||
.card-company
|
||||
margin-top: 16rpx;
|
||||
max-width: calc(100%);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis
|
||||
color: #6C7282;
|
||||
.card-info
|
||||
margin-top: 22rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-right: 40rpx;
|
||||
.info-item
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
color: #256BFA;
|
||||
font-size: 28rpx;
|
||||
padding-right: 10rpx
|
||||
.position-nav
|
||||
position: absolute;
|
||||
right: -10rpx;
|
||||
top: 50%;
|
||||
.position-nav::before
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: -4rpx;
|
||||
content: '';
|
||||
width: 4rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 2rpx
|
||||
background: #256BFA;
|
||||
transform: translate(0, -50%) rotate(-45deg) ;
|
||||
.position-nav::after
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: -4rpx;
|
||||
content: '';
|
||||
width: 4rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 2rpx
|
||||
background: #256BFA;
|
||||
transform: rotate(45deg)
|
||||
.card-tag
|
||||
font-weight: 500;
|
||||
font-size: 24rpx;
|
||||
color: #333333;
|
||||
width: fit-content;
|
||||
background: #F4F4F4;
|
||||
border-radius: 4rpx 4rpx 4rpx 4rpx;
|
||||
padding: 4rpx 20rpx;
|
||||
margin-right: 16rpx;
|
||||
</style>
|
||||
133
components/renderCompanys/renderCompanys.vue
Normal file
133
components/renderCompanys/renderCompanys.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<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.name }}</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>
|
||||
<view class="ris">
|
||||
<text class="fs_14">
|
||||
在招职位·
|
||||
<text class="color_256BFA">{{ job.totalRecruitment || '-' }}</text>
|
||||
个
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-tags">
|
||||
<view class="tag" v-if="job.nature">
|
||||
<dict-Label dictType="nature" :value="job.nature"></dict-Label>
|
||||
</view>
|
||||
<view class="tag">
|
||||
{{ vacanciesTo(job.vacancies) }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject, computed, toRaw } from 'vue';
|
||||
const { insertSortData, navTo, vacanciesTo } = inject('globalFunction');
|
||||
import { useRecommedIndexedDBStore } from '@/stores/useRecommedIndexedDBStore.js';
|
||||
const recommedIndexDb = useRecommedIndexedDBStore();
|
||||
const props = defineProps({
|
||||
list: {
|
||||
type: Array,
|
||||
default: '标题',
|
||||
},
|
||||
longitude: {
|
||||
type: Number,
|
||||
default: 120.382665,
|
||||
},
|
||||
latitude: {
|
||||
type: Number,
|
||||
default: 36.066938,
|
||||
},
|
||||
seeDate: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const listData = computed(() => {
|
||||
return props.list;
|
||||
});
|
||||
|
||||
function nextDetail(company) {
|
||||
navTo(`/packageA/pages/UnitDetails/UnitDetails?companyId=${company.companyId}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.date-jobTitle{
|
||||
font-weight: 400;
|
||||
font-size: 28rpx;
|
||||
color: #495265;
|
||||
padding: 28rpx 0 0 20rpx
|
||||
}
|
||||
.cards{
|
||||
padding: 32rpx;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0rpx 0rpx 8rpx 0rpx rgba(0,0,0,0.04);
|
||||
border-radius: 20rpx 20rpx 20rpx 20rpx;
|
||||
margin-top: 22rpx;
|
||||
.card-company{
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
align-items: flex-start
|
||||
.company{
|
||||
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: 32rpx;
|
||||
color: #333333;
|
||||
}
|
||||
.salary{
|
||||
font-weight: 500;
|
||||
font-size: 28rpx;
|
||||
color: #4C6EFB;
|
||||
white-space: nowrap
|
||||
line-height: 48rpx
|
||||
}
|
||||
}
|
||||
.card-companyName{
|
||||
font-weight: 400;
|
||||
font-size: 28rpx;
|
||||
color: #6C7282;
|
||||
}
|
||||
.card-tags{
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
.tag{
|
||||
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
|
||||
width: fit-content;
|
||||
height: 30rpx;
|
||||
background: #F4F4F4;
|
||||
border-radius: 4rpx;
|
||||
padding: 6rpx 20rpx;
|
||||
line-height: 30rpx;
|
||||
font-weight: 400;
|
||||
font-size: 24rpx;
|
||||
color: #6C7282;
|
||||
text-align: center;
|
||||
margin-top: 14rpx;
|
||||
white-space: nowrap
|
||||
margin-right: 20rpx
|
||||
}
|
||||
}
|
||||
.card-bottom{
|
||||
margin-top: 4rpx
|
||||
margin-bottom: 10rpx
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
font-size: 28rpx;
|
||||
color: #6C7282;
|
||||
}
|
||||
}
|
||||
.ris{
|
||||
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
|
||||
}
|
||||
</style>
|
||||
149
components/renderJobs/renderJobs.vue
Normal file
149
components/renderJobs/renderJobs.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<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>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-companyName">{{ job.companyName }}</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="date-jobTitle" v-else>
|
||||
{{ job.title }}
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject, computed, toRaw } from 'vue';
|
||||
const { insertSortData, navTo, vacanciesTo } = inject('globalFunction');
|
||||
import { useRecommedIndexedDBStore } from '@/stores/useRecommedIndexedDBStore.js';
|
||||
const recommedIndexDb = useRecommedIndexedDBStore();
|
||||
const props = defineProps({
|
||||
list: {
|
||||
type: Array,
|
||||
default: '标题',
|
||||
},
|
||||
longitude: {
|
||||
type: Number,
|
||||
default: 120.382665,
|
||||
},
|
||||
latitude: {
|
||||
type: Number,
|
||||
default: 36.066938,
|
||||
},
|
||||
seeDate: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const listData = computed(() => {
|
||||
if (props.seeDate && props.list.length) {
|
||||
const ulist = toRaw(props.list);
|
||||
const [reslist, lastDate] = insertSortData(ulist, props.seeDate);
|
||||
return reslist;
|
||||
}
|
||||
return props.list;
|
||||
});
|
||||
|
||||
function nextDetail(job) {
|
||||
// 记录岗位类型,用作数据分析
|
||||
if (job.jobCategory) {
|
||||
const recordData = recommedIndexDb.JobParameter(job);
|
||||
recommedIndexDb.addRecord(recordData);
|
||||
}
|
||||
navTo(`/packageA/pages/post/post?jobId=${btoa(job.jobId)}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.date-jobTitle{
|
||||
font-weight: 400;
|
||||
font-size: 28rpx;
|
||||
color: #495265;
|
||||
padding: 28rpx 0 0 20rpx
|
||||
}
|
||||
.cards{
|
||||
padding: 32rpx;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0rpx 0rpx 8rpx 0rpx rgba(0,0,0,0.04);
|
||||
border-radius: 20rpx 20rpx 20rpx 20rpx;
|
||||
margin-top: 22rpx;
|
||||
.card-company{
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
align-items: flex-start
|
||||
.company{
|
||||
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: 32rpx;
|
||||
color: #333333;
|
||||
}
|
||||
.salary{
|
||||
font-family: DIN-Medium;
|
||||
font-weight: 500;
|
||||
font-size: 28rpx;
|
||||
color: #4C6EFB;
|
||||
white-space: nowrap
|
||||
line-height: 48rpx
|
||||
}
|
||||
}
|
||||
.card-companyName{
|
||||
font-weight: 400;
|
||||
font-size: 28rpx;
|
||||
color: #6C7282;
|
||||
}
|
||||
.card-tags{
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
.tag{
|
||||
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
|
||||
width: fit-content;
|
||||
height: 30rpx;
|
||||
background: #F4F4F4;
|
||||
border-radius: 4rpx;
|
||||
padding: 6rpx 20rpx;
|
||||
line-height: 30rpx;
|
||||
font-weight: 400;
|
||||
font-size: 24rpx;
|
||||
color: #6C7282;
|
||||
text-align: center;
|
||||
margin-top: 14rpx;
|
||||
white-space: nowrap
|
||||
margin-right: 20rpx
|
||||
}
|
||||
}
|
||||
.card-bottom{
|
||||
margin-top: 32rpx
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
font-size: 28rpx;
|
||||
color: #6C7282;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,261 @@
|
||||
<template>
|
||||
<view v-if="show" class="modal-mask">
|
||||
<view class="modal-container">
|
||||
<!-- 头部 -->
|
||||
<view class="modal-header">
|
||||
<text class="back-btn" @click="handleClose">
|
||||
<uni-icons type="left" size="24"></uni-icons>
|
||||
</text>
|
||||
<text class="modal-title">{{ title }}</text>
|
||||
<view class="back-btn"></view>
|
||||
</view>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view class="content-wrapper">
|
||||
<!-- 左侧筛选类别 -->
|
||||
<scroll-view class="filter-nav" scroll-y>
|
||||
<view
|
||||
v-for="(item, index) in filterOptions"
|
||||
:key="index"
|
||||
class="nav-item"
|
||||
:class="{ active: activeTab === item.key }"
|
||||
@click="scrollTo(item.key)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 右侧筛选内容 -->
|
||||
<scroll-view class="filter-content" :scroll-into-view="activeTab" scroll-y>
|
||||
<template v-for="(item, index) in filterOptions" :key="index">
|
||||
<view class="content-item">
|
||||
<view class="item-title" :id="item.key">{{ item.label }}</view>
|
||||
<checkbox-group class="check-content" @change="(e) => handleSelect(item.key, e)">
|
||||
<label
|
||||
v-for="option in item.options"
|
||||
:key="option.value"
|
||||
class="checkbox-item"
|
||||
:class="{ checkedstyle: selectedValues[item.key]?.includes(String(option.value)) }"
|
||||
>
|
||||
<checkbox
|
||||
style="display: none"
|
||||
:value="String(option.value)"
|
||||
:checked="selectedValues[item.key]?.includes(String(option.value))"
|
||||
/>
|
||||
<text class="option-label">{{ option.label }}</text>
|
||||
</label>
|
||||
</checkbox-group>
|
||||
</view>
|
||||
</template>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="modal-footer">
|
||||
<button class="footer-btn" type="default" @click="handleClear">清除</button>
|
||||
<button class="footer-btn" type="primary" @click="handleConfirm">确认</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onBeforeMount } from 'vue';
|
||||
import useDictStore from '@/stores/useDictStore';
|
||||
const { getTransformChildren } = useDictStore();
|
||||
const props = defineProps({
|
||||
show: Boolean,
|
||||
title: {
|
||||
type: String,
|
||||
default: '筛选',
|
||||
},
|
||||
area: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['confirm', 'close', 'update:show']);
|
||||
|
||||
// 当前激活的筛选类别
|
||||
const activeTab = ref('');
|
||||
|
||||
// 存储已选中的值 {key: [selectedValues]}
|
||||
const selectedValues = reactive({});
|
||||
|
||||
const filterOptions = ref([]);
|
||||
|
||||
onBeforeMount(() => {
|
||||
const arr = [
|
||||
getTransformChildren('education', '学历要求'),
|
||||
getTransformChildren('experience', '工作经验'),
|
||||
getTransformChildren('scale', '公司规模'),
|
||||
];
|
||||
if (props.area) {
|
||||
arr.push(getTransformChildren('area', '区域'));
|
||||
}
|
||||
filterOptions.value = arr;
|
||||
activeTab.value = 'education';
|
||||
});
|
||||
|
||||
// 处理选项选择
|
||||
const handleSelect = (key, e) => {
|
||||
selectedValues[key] = e.detail.value.map(String);
|
||||
};
|
||||
const scrollTo = (key) => {
|
||||
activeTab.value = key;
|
||||
};
|
||||
// 清除所有选择
|
||||
const handleClear = () => {
|
||||
Object.keys(selectedValues).forEach((key) => {
|
||||
selectedValues[key] = [];
|
||||
});
|
||||
};
|
||||
|
||||
// 确认筛选
|
||||
function handleConfirm() {
|
||||
emit('confirm', selectedValues);
|
||||
handleClose();
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
emit('update:show', false);
|
||||
emit('close');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: #fff;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 32rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
position: relative;
|
||||
|
||||
.back-btn {
|
||||
font-size: 36rpx;
|
||||
width: 48rpx;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.filter-nav {
|
||||
width: 200rpx;
|
||||
background-color: #f5f5f5;
|
||||
|
||||
.nav-item {
|
||||
height: 100rpx;
|
||||
padding: 0 20rpx;
|
||||
line-height: 100rpx;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
|
||||
&.active {
|
||||
background-color: #fff;
|
||||
color: #007aff;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
flex: 1;
|
||||
padding: 20rpx;
|
||||
|
||||
.content-item {
|
||||
margin-top: 40rpx;
|
||||
.item-title {
|
||||
width: 281rpx;
|
||||
height: 52rpx;
|
||||
font-family: Inter, Inter;
|
||||
font-weight: 400;
|
||||
font-size: 35rpx;
|
||||
color: #000000;
|
||||
line-height: 41rpx;
|
||||
text-align: left;
|
||||
font-style: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
}
|
||||
.content-item:first-child {
|
||||
margin-top: 0rpx;
|
||||
}
|
||||
|
||||
.check-content {
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
place-items: center;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 228rpx;
|
||||
height: 65rpx;
|
||||
margin: 20rpx 20rpx 0 0;
|
||||
text-align: center;
|
||||
background-color: #d9d9d9;
|
||||
|
||||
.option-label {
|
||||
font-size: 28rpx;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.checkedstyle {
|
||||
background-color: #007aff;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
height: 100rpx;
|
||||
display: flex;
|
||||
border-top: 1rpx solid #eee;
|
||||
|
||||
.footer-btn {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
line-height: 100rpx;
|
||||
|
||||
&:first-child {
|
||||
border-right: 1rpx solid #eee;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
374
components/selectFilter/selectFilter.vue
Normal file
374
components/selectFilter/selectFilter.vue
Normal file
@@ -0,0 +1,374 @@
|
||||
<template>
|
||||
<uni-popup
|
||||
ref="popup"
|
||||
type="bottom"
|
||||
borderRadius="10px 10px 0 0"
|
||||
background-color="#FFFFFF"
|
||||
@maskClick="maskClickFn"
|
||||
:mask-click="maskClick"
|
||||
class="popup-fix"
|
||||
>
|
||||
<view class="popup-content">
|
||||
<view class="popup-header">
|
||||
<view class="btn-cancel" @click="cancel">取消</view>
|
||||
<view class="title">
|
||||
<text>{{ title }}</text>
|
||||
<text style="color: #256bfa">·{{ count }}</text>
|
||||
</view>
|
||||
<view class="btn-confirm" @click="confirm"> </view>
|
||||
</view>
|
||||
<view class="popup-list">
|
||||
<view class="content-wrapper">
|
||||
<!-- 左侧筛选类别 -->
|
||||
<scroll-view class="filter-nav" scroll-y>
|
||||
<view
|
||||
v-for="(item, index) in filterOptions"
|
||||
:key="index"
|
||||
class="nav-item button-click"
|
||||
:class="{ active: activeTab === item.key }"
|
||||
@click="scrollTo(item.key)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 右侧筛选内容 -->
|
||||
<scroll-view class="filter-content" :scroll-into-view="activeTab" scroll-y>
|
||||
<template v-for="(item, index) in filterOptions" :key="index">
|
||||
<view class="content-item">
|
||||
<view class="item-title" :id="item.key">{{ item.label }}</view>
|
||||
<checkbox-group class="check-content" @change="(e) => handleSelect(item.key, e)">
|
||||
<label
|
||||
v-for="option in item.options"
|
||||
:key="option.value"
|
||||
class="checkbox-item button-click"
|
||||
:class="{
|
||||
checkedstyle: selectedValues[item.key]?.includes(String(option.value)),
|
||||
}"
|
||||
>
|
||||
<checkbox
|
||||
style="display: none"
|
||||
:value="String(option.value)"
|
||||
:checked="selectedValues[item.key]?.includes(String(option.value))"
|
||||
/>
|
||||
<text class="option-label">{{ option.label }}</text>
|
||||
</label>
|
||||
</checkbox-group>
|
||||
</view>
|
||||
</template>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="popup-bottom">
|
||||
<view class="btn-cancel btn-feel" @click="cleanup">清除</view>
|
||||
<view class="btn-confirm btn-feel" @click="confirm">确认</view>
|
||||
</view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, nextTick, onBeforeMount } from 'vue';
|
||||
import useDictStore from '@/stores/useDictStore';
|
||||
const { getTransformChildren } = useDictStore();
|
||||
|
||||
const area = ref(true);
|
||||
const maskClick = ref(false);
|
||||
const maskClickFn = ref(null);
|
||||
|
||||
const title = ref('标题');
|
||||
const confirmCallback = ref(null);
|
||||
const cancelCallback = ref(null);
|
||||
const changeCallback = ref(null);
|
||||
const popup = ref(null);
|
||||
const selectedValues = reactive({});
|
||||
const count = ref(0);
|
||||
// 当前激活的筛选类别
|
||||
const activeTab = ref('');
|
||||
const filterOptions = ref([]);
|
||||
|
||||
const open = (newConfig = {}) => {
|
||||
const { title: configTitle, success, cancel, change, data, maskClick: configMaskClick = false } = newConfig;
|
||||
|
||||
// reset();
|
||||
|
||||
if (configTitle) title.value = configTitle;
|
||||
if (typeof success === 'function') confirmCallback.value = success;
|
||||
if (typeof cancel === 'function') cancelCallback.value = cancel;
|
||||
if (typeof change === 'function') changeCallback.value = change;
|
||||
|
||||
if (configMaskClick) {
|
||||
maskClick.value = configMaskClick;
|
||||
maskClickFn.value = cancel;
|
||||
}
|
||||
|
||||
getoptions();
|
||||
|
||||
nextTick(() => {
|
||||
popup.value?.open();
|
||||
});
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
popup.value?.close();
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
handleClick(cancelCallback.value);
|
||||
};
|
||||
|
||||
const confirm = () => {
|
||||
handleClick(confirmCallback.value);
|
||||
};
|
||||
|
||||
const handleClick = async (callback) => {
|
||||
if (typeof callback !== 'function') {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await callback(selectedValues);
|
||||
if (result !== false) close();
|
||||
} catch (error) {
|
||||
console.error('confirmCallback 执行出错:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理选项选择
|
||||
const handleSelect = (key, e) => {
|
||||
selectedValues[key] = e.detail.value.map(String);
|
||||
let va = 0;
|
||||
for (const [key, value] of Object.entries(selectedValues)) {
|
||||
va += value.length;
|
||||
}
|
||||
count.value = va;
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
Object.keys(selectedValues).forEach((key) => {
|
||||
delete selectedValues[key];
|
||||
});
|
||||
};
|
||||
|
||||
const scrollTo = (key) => {
|
||||
activeTab.value = key;
|
||||
};
|
||||
|
||||
function getoptions() {
|
||||
const arr = [
|
||||
getTransformChildren('education', '学历要求'),
|
||||
getTransformChildren('experience', '工作经验'),
|
||||
getTransformChildren('scale', '公司规模'),
|
||||
];
|
||||
if (area.value) {
|
||||
arr.push(getTransformChildren('area', '区域'));
|
||||
}
|
||||
filterOptions.value = arr;
|
||||
activeTab.value = 'education';
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
maskClick.value = false;
|
||||
confirmCallback.value = null;
|
||||
cancelCallback.value = null;
|
||||
changeCallback.value = null;
|
||||
Object.keys(selectedValues).forEach((key) => delete selectedValues[key]);
|
||||
};
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
open,
|
||||
close,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.popup-fix {
|
||||
position: fixed !important;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
z-index: 9999;
|
||||
}
|
||||
.popup-content {
|
||||
color: #000000;
|
||||
height: 80vh;
|
||||
}
|
||||
.popup-bottom {
|
||||
padding: 40rpx 28rpx 20rpx 28rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.btn-cancel {
|
||||
font-weight: 400;
|
||||
font-size: 32rpx;
|
||||
color: #666d7f;
|
||||
line-height: 90rpx;
|
||||
width: 33%;
|
||||
min-width: 222rpx;
|
||||
height: 90rpx;
|
||||
background: #f5f5f5;
|
||||
border-radius: 12rpx 12rpx 12rpx 12rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.btn-confirm {
|
||||
font-weight: 400;
|
||||
font-size: 32rpx;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
width: 67%;
|
||||
height: 90rpx;
|
||||
margin-left: 28rpx;
|
||||
line-height: 90rpx;
|
||||
background: #256bfa;
|
||||
min-width: 444rpx;
|
||||
border-radius: 12rpx 12rpx 12rpx 12rpx;
|
||||
}
|
||||
}
|
||||
.popup-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
height: calc(80vh - 100rpx - 150rpx);
|
||||
overflow: hidden;
|
||||
.picker-view {
|
||||
width: 100%;
|
||||
height: 500rpx;
|
||||
margin-top: 20rpx;
|
||||
.uni-picker-view-mask {
|
||||
background: rgba(0, 0, 0, 0);
|
||||
}
|
||||
.item {
|
||||
line-height: 84rpx;
|
||||
height: 84rpx;
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
font-size: 32rpx;
|
||||
color: #cccccc;
|
||||
}
|
||||
.item-active {
|
||||
color: #333333;
|
||||
}
|
||||
.uni-picker-view-indicator:after {
|
||||
border-color: #e3e3e3;
|
||||
}
|
||||
.uni-picker-view-indicator:before {
|
||||
border-color: #e3e3e3;
|
||||
}
|
||||
}
|
||||
// .list {
|
||||
// .row {
|
||||
// font-weight: 400;
|
||||
// font-size: 32rpx;
|
||||
// color: #333333;
|
||||
// line-height: 84rpx;
|
||||
// text-align: center;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
.popup-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 40rpx 40rpx 10rpx 40rpx;
|
||||
.title {
|
||||
font-weight: 500;
|
||||
font-size: 36rpx;
|
||||
color: #333333;
|
||||
text-align: center;
|
||||
}
|
||||
.btn-cancel {
|
||||
font-weight: 400;
|
||||
font-size: 32rpx;
|
||||
color: #666d7f;
|
||||
line-height: 38rpx;
|
||||
}
|
||||
.btn-confirm {
|
||||
font-weight: 400;
|
||||
font-size: 32rpx;
|
||||
color: #256bfa;
|
||||
}
|
||||
}
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.filter-nav {
|
||||
width: 200rpx;
|
||||
background-color: #ffffff;
|
||||
|
||||
.nav-item {
|
||||
height: 100rpx;
|
||||
line-height: 100rpx;
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
font-size: 28rpx;
|
||||
color: #666d7f;
|
||||
&.active {
|
||||
font-weight: 500;
|
||||
font-size: 28rpx;
|
||||
color: #256bfa;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
flex: 1;
|
||||
padding: 20rpx;
|
||||
background-color: #f6f6f6;
|
||||
|
||||
.content-item {
|
||||
margin-top: 30rpx;
|
||||
.item-title {
|
||||
font-weight: 400;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
}
|
||||
.content-item:first-child {
|
||||
margin-top: 0rpx;
|
||||
}
|
||||
|
||||
.check-content {
|
||||
display: grid;
|
||||
gap: 16rpx;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180rpx, 1fr));
|
||||
place-items: stretch;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
background-color: #d9d9d9;
|
||||
|
||||
min-width: 0;
|
||||
padding: 0 10rpx;
|
||||
height: 80rpx;
|
||||
background: #e8eaee;
|
||||
border-radius: 12rpx 12rpx 12rpx 12rpx;
|
||||
|
||||
.option-label {
|
||||
font-size: 28rpx;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
.checkedstyle {
|
||||
height: 76rpx;
|
||||
background: rgba(37, 107, 250, 0.06);
|
||||
border-radius: 12rpx 12rpx 12rpx 12rpx;
|
||||
border: 2rpx solid #256bfa;
|
||||
color: #256bfa;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
289
components/selectJobs/selectJobs.vue
Normal file
289
components/selectJobs/selectJobs.vue
Normal file
@@ -0,0 +1,289 @@
|
||||
<template>
|
||||
<uni-popup
|
||||
ref="popup"
|
||||
type="bottom"
|
||||
borderRadius="10px 10px 0 0"
|
||||
background-color="#FFFFFF"
|
||||
:mask-click="maskClick"
|
||||
>
|
||||
<view class="popup-content">
|
||||
<view class="popup-header">
|
||||
<view class="btn-cancel" @click="cancel">取消</view>
|
||||
<view class="title">
|
||||
<text>{{ title }}</text>
|
||||
<text style="color: #256bfa">·{{ count }}</text>
|
||||
</view>
|
||||
<view class="btn-confirm" @click="confirm"> </view>
|
||||
</view>
|
||||
<view class="popup-list">
|
||||
<expected-station
|
||||
:search="false"
|
||||
@onChange="changeJobTitleId"
|
||||
:station="state.stations"
|
||||
:max="5"
|
||||
></expected-station>
|
||||
</view>
|
||||
<view class="popup-bottom">
|
||||
<view class="btn-cancel" @click="cleanup">清除</view>
|
||||
<view class="btn-confirm" @click="confirm">确认</view>
|
||||
</view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, inject, nextTick, defineExpose, onMounted } from 'vue';
|
||||
const { $api, navTo, setCheckedNodes, cloneDeep } = inject('globalFunction');
|
||||
import useUserStore from '@/stores/useUserStore';
|
||||
import { storeToRefs } from 'pinia';
|
||||
const { userInfo } = storeToRefs(useUserStore());
|
||||
const maskClick = ref(false);
|
||||
const title = ref('标题');
|
||||
const confirmCallback = ref(null);
|
||||
const cancelCallback = ref(null);
|
||||
const changeCallback = ref(null);
|
||||
const listData = ref([]);
|
||||
const selectedIndex = ref([0, 0, 0]);
|
||||
const rowLabel = ref('label');
|
||||
const rowKey = ref('value');
|
||||
const selectedItems = ref([]);
|
||||
const popup = ref(null);
|
||||
const count = ref(0);
|
||||
const JobsIdsValue = ref('');
|
||||
const JobsLabelValue = ref('');
|
||||
|
||||
const state = reactive({
|
||||
jobTitleId: '',
|
||||
stations: [],
|
||||
visible: false,
|
||||
});
|
||||
|
||||
// onMounted(() => {
|
||||
// serchforIt();
|
||||
// });
|
||||
|
||||
// 统一处理二维数组格式
|
||||
const processedListData = computed(() => {
|
||||
return listData.value.map((column) => {
|
||||
if (!Array.isArray(column)) return [];
|
||||
return column.map((item) => {
|
||||
return typeof item === 'object' ? item : { [rowLabel.value]: item, [rowKey.value]: item };
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const open = (newConfig = {}) => {
|
||||
const {
|
||||
title: configTitle,
|
||||
success,
|
||||
cancel,
|
||||
change,
|
||||
data,
|
||||
rowLabel: configRowLabel = 'label',
|
||||
rowKey: configRowKey = 'value',
|
||||
maskClick: configMaskClick = false,
|
||||
defaultId = '',
|
||||
} = newConfig;
|
||||
|
||||
reset();
|
||||
serchforIt(defaultId);
|
||||
|
||||
if (configTitle) title.value = configTitle;
|
||||
if (typeof success === 'function') confirmCallback.value = success;
|
||||
if (typeof cancel === 'function') cancelCallback.value = cancel;
|
||||
if (typeof change === 'function') changeCallback.value = change;
|
||||
if (Array.isArray(data)) listData.value = data;
|
||||
|
||||
rowLabel.value = configRowLabel;
|
||||
rowKey.value = configRowKey;
|
||||
maskClick.value = configMaskClick;
|
||||
nextTick(() => {
|
||||
popup.value?.open();
|
||||
});
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
popup.value?.close();
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
handleClick(cancelCallback.value);
|
||||
};
|
||||
|
||||
const confirm = () => {
|
||||
if (JobsIdsValue.value) {
|
||||
handleClick(confirmCallback.value);
|
||||
} else {
|
||||
$api.msg('请选择期望岗位');
|
||||
}
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
setCheckedNodes(state.stations, []);
|
||||
count.value = 0;
|
||||
reset();
|
||||
};
|
||||
|
||||
const changeJobTitleId = (e) => {
|
||||
const ids = e.ids.split(',').map((id) => Number(id));
|
||||
count.value = ids.length;
|
||||
JobsIdsValue.value = e.ids;
|
||||
JobsLabelValue.value = e.labels;
|
||||
};
|
||||
const handleClick = async (callback) => {
|
||||
if (typeof callback !== 'function') {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await callback(JobsIdsValue.value, JobsLabelValue.value);
|
||||
if (result !== false) close();
|
||||
} catch (error) {
|
||||
console.error('confirmCallback 执行出错:', error);
|
||||
}
|
||||
};
|
||||
function serchforIt(defaultId) {
|
||||
if (state.stations.length) {
|
||||
const ids = defaultId
|
||||
? defaultId.split(',').map((id) => Number(id))
|
||||
: userInfo.value.jobTitleId.split(',').map((id) => Number(id));
|
||||
count.value = ids.length;
|
||||
state.jobTitleId = defaultId ? defaultId : userInfo.value.jobTitleId;
|
||||
setCheckedNodes(state.stations, ids);
|
||||
state.visible = true;
|
||||
return;
|
||||
}
|
||||
$api.createRequest('/app/common/jobTitle/treeselect', {}, 'GET').then((resData) => {
|
||||
if (userInfo.value.jobTitleId) {
|
||||
const ids = userInfo.value.jobTitleId.split(',').map((id) => Number(id));
|
||||
count.value = ids.length;
|
||||
setCheckedNodes(resData.data, ids);
|
||||
}
|
||||
state.jobTitleId = userInfo.value.jobTitleId;
|
||||
state.stations = resData.data;
|
||||
state.visible = true;
|
||||
});
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
maskClick.value = false;
|
||||
changeCallback.value = null;
|
||||
listData.value = [];
|
||||
selectedIndex.value = [0, 0, 0];
|
||||
rowLabel.value = 'label';
|
||||
rowKey.value = 'value';
|
||||
selectedItems.value = [];
|
||||
JobsIdsValue.value = '';
|
||||
JobsLabelValue.value = '';
|
||||
};
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
open,
|
||||
close,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.popup-content {
|
||||
color: #000000;
|
||||
height: 80vh;
|
||||
}
|
||||
.popup-bottom {
|
||||
padding: 40rpx 28rpx 20rpx 28rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.btn-cancel {
|
||||
font-weight: 400;
|
||||
font-size: 32rpx;
|
||||
color: #666d7f;
|
||||
line-height: 90rpx;
|
||||
width: 33%;
|
||||
min-width: 222rpx;
|
||||
height: 90rpx;
|
||||
background: #f5f5f5;
|
||||
border-radius: 12rpx 12rpx 12rpx 12rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.btn-confirm {
|
||||
font-weight: 400;
|
||||
font-size: 32rpx;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
width: 67%;
|
||||
height: 90rpx;
|
||||
margin-left: 28rpx;
|
||||
line-height: 90rpx;
|
||||
background: #256bfa;
|
||||
min-width: 444rpx;
|
||||
border-radius: 12rpx 12rpx 12rpx 12rpx;
|
||||
}
|
||||
}
|
||||
.popup-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
height: calc(80vh - 100rpx - 150rpx);
|
||||
overflow: hidden;
|
||||
.picker-view {
|
||||
width: 100%;
|
||||
height: 500rpx;
|
||||
margin-top: 20rpx;
|
||||
.uni-picker-view-mask {
|
||||
background: rgba(0, 0, 0, 0);
|
||||
}
|
||||
.item {
|
||||
line-height: 84rpx;
|
||||
height: 84rpx;
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
font-size: 32rpx;
|
||||
color: #cccccc;
|
||||
}
|
||||
.item-active {
|
||||
color: #333333;
|
||||
}
|
||||
.uni-picker-view-indicator:after {
|
||||
border-color: #e3e3e3;
|
||||
}
|
||||
.uni-picker-view-indicator:before {
|
||||
border-color: #e3e3e3;
|
||||
}
|
||||
}
|
||||
// .list {
|
||||
// .row {
|
||||
// font-weight: 400;
|
||||
// font-size: 32rpx;
|
||||
// color: #333333;
|
||||
// line-height: 84rpx;
|
||||
// text-align: center;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
.popup-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 40rpx 40rpx 10rpx 40rpx;
|
||||
.title {
|
||||
font-weight: 500;
|
||||
font-size: 36rpx;
|
||||
color: #333333;
|
||||
text-align: center;
|
||||
}
|
||||
.btn-cancel {
|
||||
font-weight: 400;
|
||||
font-size: 32rpx;
|
||||
color: #666d7f;
|
||||
line-height: 38rpx;
|
||||
}
|
||||
.btn-confirm {
|
||||
font-weight: 400;
|
||||
font-size: 32rpx;
|
||||
color: #256bfa;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
233
components/selectPopup/selectPopup.vue
Normal file
233
components/selectPopup/selectPopup.vue
Normal file
@@ -0,0 +1,233 @@
|
||||
<template>
|
||||
<uni-popup
|
||||
ref="popup"
|
||||
type="bottom"
|
||||
borderRadius="10px 10px 0 0"
|
||||
background-color="#FFFFFF"
|
||||
:mask-click="maskClick"
|
||||
>
|
||||
<view class="popup-content">
|
||||
<view class="popup-header">
|
||||
<view class="btn-cancel" @click="cancel">取消</view>
|
||||
<view class="title">{{ title }}</view>
|
||||
<view class="btn-confirm" @click="confirm">确认</view>
|
||||
</view>
|
||||
<view class="popup-list">
|
||||
<picker-view
|
||||
indicator-style="height: 84rpx;"
|
||||
:value="selectedIndex"
|
||||
@change="bindChange"
|
||||
class="picker-view"
|
||||
>
|
||||
<template v-for="(list, lsIndex) in processedListData" :key="lsIndex">
|
||||
<picker-view-column>
|
||||
<view
|
||||
v-for="(item, index) in list"
|
||||
:key="index"
|
||||
class="item"
|
||||
:class="{ 'item-active': selectedIndex[lsIndex] === index }"
|
||||
>
|
||||
<text>{{ getLabel(item) }}</text>
|
||||
<text>{{ unit }}</text>
|
||||
</view>
|
||||
</picker-view-column>
|
||||
</template>
|
||||
</picker-view>
|
||||
</view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'selectPopup',
|
||||
data() {
|
||||
return {
|
||||
maskClick: false,
|
||||
title: '标题',
|
||||
confirmCallback: null,
|
||||
cancelCallback: null,
|
||||
changeCallback: null,
|
||||
listData: [],
|
||||
selectedIndex: [0, 0, 0],
|
||||
rowLabel: 'label',
|
||||
rowKey: 'value',
|
||||
selectedItems: [],
|
||||
unit: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 统一处理二维数组格式
|
||||
processedListData() {
|
||||
return this.listData.map((column) => {
|
||||
if (!Array.isArray(column)) return [];
|
||||
return column.map((item) => {
|
||||
return typeof item === 'object' ? item : { [this.rowLabel]: item, [this.rowKey]: item };
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
open(newConfig = {}) {
|
||||
const {
|
||||
title,
|
||||
success,
|
||||
cancel,
|
||||
change,
|
||||
data,
|
||||
unit = '',
|
||||
rowLabel = 'label',
|
||||
rowKey = 'value',
|
||||
maskClick = false,
|
||||
defaultIndex = [],
|
||||
} = newConfig;
|
||||
this.reset();
|
||||
if (title) this.title = title;
|
||||
if (typeof success === 'function') this.confirmCallback = success;
|
||||
if (typeof cancel === 'function') this.cancelCallback = cancel;
|
||||
if (typeof change === 'function') this.changeCallback = change;
|
||||
if (Array.isArray(data)) this.listData = data;
|
||||
|
||||
this.rowLabel = rowLabel;
|
||||
this.rowKey = rowKey;
|
||||
this.maskClick = maskClick;
|
||||
this.unit = unit;
|
||||
|
||||
this.selectedIndex =
|
||||
defaultIndex.length === this.listData.length ? defaultIndex : new Array(this.listData.length).fill(0);
|
||||
this.selectedItems = this.selectedIndex.map((val, index) => this.processedListData[index][val]);
|
||||
this.$nextTick(() => {
|
||||
this.$refs.popup.open();
|
||||
});
|
||||
},
|
||||
close() {
|
||||
this.$refs.popup.close();
|
||||
},
|
||||
bindChange(e) {
|
||||
this.selectedIndex = e.detail.value;
|
||||
this.selectedItems = this.selectedIndex.map((val, index) => this.processedListData[index][val]);
|
||||
this.changeCallback && this.changeCallback(e, this.selectedIndex, this.selectedItems);
|
||||
},
|
||||
cancel() {
|
||||
this.clickCallback(this.cancelCallback);
|
||||
},
|
||||
confirm() {
|
||||
this.clickCallback(this.confirmCallback);
|
||||
},
|
||||
getLabel(item) {
|
||||
return item?.[this.rowLabel] ?? '';
|
||||
},
|
||||
setColunm(index, list) {
|
||||
if (index > this.listData.length) {
|
||||
return console.warn('最长' + this.listData.length);
|
||||
}
|
||||
if (!list.length) {
|
||||
return console.warn(list + '不能为空');
|
||||
}
|
||||
this.listData[index] = list;
|
||||
this.selectedIndex[index] = 0;
|
||||
this.selectedItems = this.selectedIndex.map((val, index) => this.processedListData[index][val]);
|
||||
},
|
||||
async clickCallback(callback) {
|
||||
if (typeof callback !== 'function') {
|
||||
this.$refs.popup.close();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await callback(this.selectedIndex, this.selectedItems); // 无论是 async 还是返回 Promise 的函数都可以 await
|
||||
if (result !== false) {
|
||||
this.$refs.popup.close();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('confirmCallback 执行出错:', error);
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
this.maskClick = false;
|
||||
this.confirmCallback = null;
|
||||
this.cancelCallback = null;
|
||||
this.changeCallback = null;
|
||||
this.listData = [];
|
||||
this.selectedIndex = [0, 0, 0];
|
||||
this.rowLabel = 'label';
|
||||
this.rowKey = 'value';
|
||||
this.selectedItems = [];
|
||||
this.unit = '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.popup-content {
|
||||
color: #000000;
|
||||
height: 50vh;
|
||||
}
|
||||
.popup-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
.picker-view {
|
||||
width: 100%;
|
||||
height: calc(50vh - 100rpx);
|
||||
margin-top: 20rpx;
|
||||
.uni-picker-view-mask {
|
||||
background: rgba(0, 0, 0, 0);
|
||||
}
|
||||
.item {
|
||||
line-height: 84rpx;
|
||||
height: 84rpx;
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
font-size: 32rpx;
|
||||
color: #cccccc;
|
||||
}
|
||||
.item-active {
|
||||
color: #333333;
|
||||
}
|
||||
.uni-picker-view-indicator:after {
|
||||
border-color: #e3e3e3;
|
||||
}
|
||||
.uni-picker-view-indicator:before {
|
||||
border-color: #e3e3e3;
|
||||
}
|
||||
}
|
||||
// .list {
|
||||
// .row {
|
||||
// font-weight: 400;
|
||||
// font-size: 32rpx;
|
||||
// color: #333333;
|
||||
// line-height: 84rpx;
|
||||
// text-align: center;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
.popup-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 40rpx 40rpx 10rpx 40rpx;
|
||||
.title {
|
||||
font-weight: 500;
|
||||
font-size: 36rpx;
|
||||
color: #333333;
|
||||
text-align: center;
|
||||
}
|
||||
.btn-cancel {
|
||||
font-weight: 400;
|
||||
font-size: 32rpx;
|
||||
color: #666d7f;
|
||||
line-height: 38rpx;
|
||||
}
|
||||
.btn-confirm {
|
||||
font-weight: 400;
|
||||
font-size: 32rpx;
|
||||
color: #256bfa;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
26
components/selectPopup/selectPopupPlugin.js
Normal file
26
components/selectPopup/selectPopupPlugin.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// plugins/selectPopup.js
|
||||
import {
|
||||
createApp
|
||||
} from 'vue';
|
||||
import SelectPopup from './selectPopup.vue';
|
||||
|
||||
export default {
|
||||
install(app) {
|
||||
const popupApp = createApp(SelectPopup);
|
||||
// #ifdef H5
|
||||
const popupInstance = popupApp.mount(document.createElement('div'));
|
||||
document.body.appendChild(popupInstance.$el);
|
||||
|
||||
// 提供 open 方法
|
||||
const openPopup = (config) => {
|
||||
popupInstance.open(config);
|
||||
};
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
const openPopup = (config) => {};
|
||||
// #endif
|
||||
// 提供给所有组件使用
|
||||
app.provide('openSelectPopup', openPopup);
|
||||
}
|
||||
};
|
||||
157
components/tabbar/midell-box.vue
Normal file
157
components/tabbar/midell-box.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<view class="tabbar_container">
|
||||
<view class="tabbar_item" v-for="(item, index) in tabbarList" :key="index" @click="changeItem(item)">
|
||||
<view
|
||||
class="item-top"
|
||||
:class="[
|
||||
item.centerItem ? 'center-item-img ' : '',
|
||||
item.centerItem && currentItem === item.id ? 'rubberBand animated' : '',
|
||||
]"
|
||||
>
|
||||
<image :src="currentItem == item.id ? item.selectedIconPath : item.iconPath"></image>
|
||||
</view>
|
||||
<view class="badge" v-if="item.badge">{{ item.badge }}</view>
|
||||
<view class="item-bottom" :class="[currentItem == item.id ? 'item-active' : '']">
|
||||
<text>{{ item.text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps, onMounted, computed } from 'vue';
|
||||
import { useReadMsg } from '@/stores/useReadMsg';
|
||||
const props = defineProps({
|
||||
currentpage: {
|
||||
type: Number,
|
||||
required: true,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
const readMsg = useReadMsg();
|
||||
const currentItem = ref(0);
|
||||
const tabbarList = computed(() => [
|
||||
{
|
||||
id: 0,
|
||||
text: '首页',
|
||||
path: '/pages/index/index',
|
||||
iconPath: '../../static/tabbar/calendar.png',
|
||||
selectedIconPath: '../../static/tabbar/calendared.png',
|
||||
centerItem: false,
|
||||
badge: readMsg.badges[0].count,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
text: '招聘会',
|
||||
path: '/pages/careerfair/careerfair',
|
||||
iconPath: '../../static/tabbar/post.png',
|
||||
selectedIconPath: '../../static/tabbar/posted.png',
|
||||
centerItem: false,
|
||||
badge: readMsg.badges[1].count,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
text: '',
|
||||
path: '/pages/chat/chat',
|
||||
iconPath: '../../static/tabbar/logo3.png',
|
||||
selectedIconPath: '../../static/tabbar/logo3.png',
|
||||
centerItem: true,
|
||||
badge: readMsg.badges[2].count,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
text: '消息',
|
||||
path: '/pages/msglog/msglog',
|
||||
iconPath: '../../static/tabbar/chat4.png',
|
||||
selectedIconPath: '../../static/tabbar/chat4ed.png',
|
||||
centerItem: false,
|
||||
badge: readMsg.badges[3].count,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
text: '我的',
|
||||
path: '/pages/mine/mine',
|
||||
iconPath: '../../static/tabbar/mine.png',
|
||||
selectedIconPath: '../../static/tabbar/mined.png',
|
||||
centerItem: false,
|
||||
badge: readMsg.badges[4].count,
|
||||
},
|
||||
]);
|
||||
|
||||
onMounted(() => {
|
||||
uni.hideTabBar();
|
||||
currentItem.value = props.currentpage;
|
||||
});
|
||||
|
||||
const changeItem = (item) => {
|
||||
uni.switchTab({
|
||||
url: item.path,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: 4rpx;
|
||||
right: 20rpx;
|
||||
min-width: 30rpx;
|
||||
height: 30rpx;
|
||||
background-color: red;
|
||||
color: #fff;
|
||||
font-size: 18rpx;
|
||||
border-radius: 15rpx;
|
||||
text-align: center;
|
||||
line-height: 30rpx;
|
||||
padding: 0 10rpx;
|
||||
}
|
||||
.tabbar_container {
|
||||
background-color: #ffffff;
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5rpx 0;
|
||||
overflow: hidden;
|
||||
// position: fixed;
|
||||
// bottom: 0rpx;
|
||||
// left: 0rpx;
|
||||
// box-shadow: 0 0 5px #999;
|
||||
// padding-bottom: env(safe-area-inset-bottom);
|
||||
// z-index: 998;
|
||||
.tabbar_item {
|
||||
width: 33.33%;
|
||||
height: 100rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
color: #5e5f60;
|
||||
.item-top {
|
||||
width: 44.44rpx;
|
||||
height: 44.44rpx;
|
||||
image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.item-bottom {
|
||||
font-weight: 500;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
.center-item-img {
|
||||
// position: absolute;
|
||||
// top: 0rpx;
|
||||
// left: 50%;
|
||||
// transform: translate(-50%, 0);
|
||||
width: 108rpx !important;
|
||||
height: 98rpx !important;
|
||||
}
|
||||
.item-active {
|
||||
color: #256bfa;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user