This commit is contained in:
史典卓
2025-03-28 15:19:42 +08:00
parent ad4eb162a5
commit 0216f6053a
396 changed files with 18278 additions and 9899 deletions

BIN
components/.DS_Store vendored

Binary file not shown.

View 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>

View File

@@ -0,0 +1,45 @@
<template>
<view v-show="internalShow" :style="fadeStyle" class="fade-wrapper">
<slot />
</view>
</template>
<script setup>
import { ref, watch } from 'vue';
const props = defineProps({
show: { type: Boolean, default: false },
duration: { type: Number, default: 300 }, // ms
});
const internalShow = ref(props.show);
const fadeStyle = ref({
opacity: props.show ? 1 : 0,
transition: `opacity ${props.duration}ms ease`,
});
watch(
() => props.show,
(val) => {
if (val) {
internalShow.value = true;
requestAnimationFrame(() => {
fadeStyle.value.opacity = 1;
});
} else {
fadeStyle.value.opacity = 0;
// 动画结束后隐藏 DOM
setTimeout(() => {
internalShow.value = false;
}, props.duration);
}
}
);
</script>
<style scoped>
.fade-wrapper {
width: 100%;
height: 100%;
}
</style>

View 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>

View 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>

Binary file not shown.

View File

@@ -0,0 +1,23 @@
<template>
<span style="padding-left: 16rpx">{{ tofixedAndKmM(distance) }}</span>
</template>
<script setup>
import { inject } from 'vue';
const { haversine, getDistanceFromLatLonInKm } = inject('globalFunction');
const { alat, along, blat, blong } = defineProps(['alat', 'along', 'blat', 'blong']);
const distance = getDistanceFromLatLonInKm(alat, along, blat, blong);
function tofixedAndKmM(data) {
const { km, m } = data;
if (!alat && !along) {
return '--km';
}
if (km > 1) {
return km.toFixed(2) + 'km';
} else {
return m.toFixed(2) + 'm';
}
}
</script>
<style></style>

View File

@@ -0,0 +1,134 @@
<template>
<view
v-if="visible"
class="tianditu-popop"
:style="{ height: winHeight + 'px', width: winWidth + 'px', top: winTop + 'px' }"
>
<view v-if="header" class="popup-header" @click="close">
<slot name="header"></slot>
</view>
<view :style="{ minHeight: contentHeight + 'vh' }" class="popup-content fadeInUp animated">
<slot></slot>
</view>
</view>
</template>
<script>
export default {
name: 'custom-popup',
data() {
return {
winWidth: 0,
winHeight: 0,
winTop: 0,
contentHeight: 30,
};
},
props: {
visible: {
type: Boolean,
require: true,
default: false,
},
hide: {
type: Number,
default: 0,
},
header: {
type: Boolean,
default: true,
},
contentH: {
type: Number,
default: 30,
},
},
created() {
var that = this;
if (this.contentH) {
this.contentHeight = this.contentH;
}
uni.getSystemInfo({
success: function (res) {
if (that.hide === 0) {
that.winWidth = res.screenWidth;
that.winHeight = res.screenHeight;
that.winTop = 0;
} else {
that.winWidth = res.windowWidth;
that.winHeight = res.windowHeight;
that.winTop = res.windowTop;
}
},
});
},
methods: {
close(e) {
this.$emit('onClose');
},
},
};
</script>
<style scoped>
.tianditu-popop {
position: fixed;
left: 0;
z-index: 999;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
flex-direction: column;
}
.popup-header {
flex: 1;
}
.popup-content {
background-color: #ffffff;
min-height: 300px;
width: 100%;
/* position: absolute;
bottom: 0;
left: 0; */
}
/*base code*/
.animated {
-webkit-animation-duration: 1s;
animation-duration: 1s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
.animated.infinite {
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
.animated.hinge {
-webkit-animation-duration: 2s;
animation-duration: 2s;
}
@keyframes fadeInUp {
0% {
opacity: 0;
-webkit-transform: translate3d(0, 100%, 0);
-ms-transform: translate3d(0, 100%, 0);
transform: translate3d(0, 100%, 0);
}
100% {
opacity: 1;
-webkit-transform: none;
-ms-transform: none;
transform: none;
}
}
.fadeInUp {
-webkit-animation-name: fadeInUp;
animation-name: fadeInUp;
}
</style>

View 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>

View 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>

View File

@@ -0,0 +1,226 @@
<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-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: [],
};
},
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;
},
addItem(item) {
let titiles = [];
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}`);
firstLayer.checkednumber++; // 累加计数器
}
}
}
}
titiles = titiles.join(',');
this.$emit('onChange', titiles);
},
},
};
</script>
<style lang="stylus" scoped>
.secondary-title{
font-weight: bold;
padding: 40rpx 0 10rpx 30rpx;
}
.expected-station{
width: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.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: 250rpx;
.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
.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;
flex: 1;
.grid-sex
display: grid;
grid-template-columns: 50% 50%;
place-items: center;
padding: 0 0 40rpx 0;
.sex-right-btn
width: 211rpx;
height: 84rpx;
font-size: 32rpx;
line-height: 41rpx;
text-align: center;
display: grid;
place-items: center;
background: #D9D9D9;
border-radius: 20rpx;
margin-top:30rpx;
color: #606060;
.sex-right-btned
color: #FFFFFF;
background: #4778EC;
</style>

View File

@@ -0,0 +1,27 @@
<template>
<view>
<picker range-key="text" @change="changeLatestHotestStatus" :value="rangeVal" :range="rangeOptions">
<view class="uni-input">{{ rangeOptions[rangeVal].text }}</view>
</picker>
</view>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted, getCurrentInstance } from 'vue';
const rangeVal = ref(0);
const emit = defineEmits(['confirm', 'close']);
const rangeOptions = ref([
{ value: 0, text: '推荐' },
{ value: 1, text: '最热' },
{ value: 2, text: '最新发布' },
]);
function changeLatestHotestStatus(e) {
const id = e.detail.value;
rangeVal.value = id;
const obj = rangeOptions.value.filter((item) => item.value === id)[0];
emit('confirm', obj);
}
</script>
<style lang="stylus"></style>

View 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>

View File

@@ -0,0 +1,17 @@
<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>

View File

@@ -0,0 +1,178 @@
<template>
<view class="markdown-body">
<rich-text class="markdownRich" id="markdown-content" :nodes="renderedHtml" @itemclick="handleItemClick" />
</view>
</template>
<script setup>
import { computed } from 'vue';
import { parseMarkdown, codeDataList } from '@/utils/markdownParser';
const props = defineProps({
content: {
type: String,
default: '',
},
});
const renderedHtml = computed(() => parseMarkdown(props.content));
const handleItemClick = (e) => {
let { attrs } = e.detail.node;
let { 'data-copy-index': codeDataIndex, class: className, href } = attrs;
if (href) {
window.open(href);
return;
}
if (className == 'copy-btn') {
uni.setClipboardData({
data: codeDataList[codeDataIndex],
showToast: false,
success() {
uni.showToast({
title: '复制成功',
icon: 'none',
});
},
});
}
};
</script>
<style lang="scss">
.markdown-body {
h2,
h3,
h4,
h5,
h6 {
// line-height: 1;
}
ul {
// display: block;
padding-inline-start: 40rpx;
li {
margin-bottom: -30rpx;
}
li:nth-child(1) {
margin-top: -40rpx;
}
}
}
.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;
}
.code-container {
position: relative;
border-radius: 10rpx;
overflow: hidden;
// background: #0d1117;
padding: 8rpx;
color: #c9d1d9;
font-size: 28rpx;
height: fit-content;
margin-top: -140rpx;
margin-bottom: -140rpx;
}
.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: #f8f8f8;
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;
}
</style>

View File

@@ -0,0 +1,323 @@
<template>
<view v-if="show" class="popup-container">
<view class="popup-content">
<!-- 标题 -->
<view class="title">岗位推荐</view>
<!-- 圆环 -->
<view class="circle-content" :style="{ height: contentHeight * 2 + 'rpx' }">
<!-- 渲染岗位标签 -->
<view class="tabs">
<!-- 动画 -->
<view
class="circle"
:style="{ height: circleDiameter * 2 + 'rpx', width: circleDiameter * 2 + 'rpx' }"
@click="serchforIt"
>
搜一搜
</view>
<view
v-for="(item, index) in jobList"
:key="index"
class="tab"
:style="getLabelStyle(index)"
@click="selectTab(item)"
>
{{ item.name }}
</view>
</view>
</view>
<!-- 关闭按钮 -->
<button class="close-btn" @click="closePopup">完成</button>
</view>
<!-- piker -->
<custom-popup :content-h="100" :visible="state.visible" :header="false">
<view class="popContent">
<view class="s-header">
<view class="heade-lf" @click="state.visible = false">取消</view>
<view class="heade-ri" @click="confimPopup">确认</view>
</view>
<view class="sex-content fl_1">
<expected-station
:search="false"
@onChange="changeJobTitleId"
:station="state.stations"
:max="5"
></expected-station>
</view>
</view>
</custom-popup>
</view>
</template>
<script setup>
import { ref, inject, computed, onMounted, defineProps, defineEmits, reactive } from 'vue';
import useUserStore from '@/stores/useUserStore';
const { $api, navTo, setCheckedNodes } = inject('globalFunction');
const { getUserResume } = useUserStore();
const props = defineProps({
show: Boolean, // 是否显示弹窗
jobList: Array, // 职位列表
});
const contentHeight = ref(373);
const circleDiameter = ref(113);
const screenWidth = ref(375); // 默认值,避免初始化报错
const screenHeight = ref(667);
const centerX = ref(187.5); // 圆心X
const centerY = ref(333.5); // 圆心Y
const radius = ref(120); // 圆半径
const tabPositions = ref([]); // 存储计算好的随机坐标
const emit = defineEmits(['update:show']);
const userInfo = ref({});
const state = reactive({
jobTitleId: '',
stations: [],
visible: false,
});
const closePopup = () => {
emit('update:show', false);
};
const updateScreenSize = () => {
const systemInfo = uni.getSystemInfoSync();
screenWidth.value = systemInfo.windowWidth;
screenHeight.value = systemInfo.windowHeight;
centerX.value = screenWidth.value / 2;
centerY.value = screenHeight.value / 2 - contentHeight.value / 2; // 让圆心稍微上移
};
function serchforIt() {
if (state.stations.length) {
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));
setCheckedNodes(resData.data, ids);
}
state.jobTitleId = userInfo.value.jobTitleId;
state.stations = resData.data;
state.visible = true;
});
}
function confimPopup() {
$api.createRequest('/app/user/resume', { jobTitleId: state.jobTitleId }, 'post').then((resData) => {
$api.msg('完成');
state.visible = false;
getUserResume().then(() => {
initload();
});
});
}
function selectTab(item) {
console.log(item);
}
function changeJobTitleId(ids) {
state.jobTitleId = ids;
}
function getLabelStyle(index) {
// 基础半径(根据标签数量动态调整)
const baseRadius = Math.min(Math.max(props.jobList.length * 15, 130), screenWidth.value * 0.4);
// 基础角度间隔
const angleStep = 360 / props.jobList.length;
// 随机扰动参数
const randomRadius = baseRadius + Math.random() * 60 - 50;
const randomAngle = angleStep * index + Math.random() * 20 - 10;
// 极坐标转笛卡尔坐标
const radians = (randomAngle * Math.PI) / 180;
const x = Math.cos(radians) * randomRadius;
const y = Math.sin(radians) * randomRadius;
return {
left: `calc(50% + ${x}px)`,
top: `calc(50% + ${y}px)`,
transform: 'translate(-50%, -50%)',
};
}
onMounted(() => {
userInfo.value = useUserStore().userInfo;
updateScreenSize();
});
</script>
<style scoped>
/* 全屏弹窗 */
.popup-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
overflow: hidden;
z-index: 999;
}
/* 弹窗内容 */
.popup-content {
position: relative;
width: 100%;
height: 100%;
background: linear-gradient(to bottom, #007aff, #005bbb);
text-align: center;
flex-direction: column;
display: flex;
justify-content: space-around;
align-items: center;
}
.title {
padding-left: 20rpx;
width: calc(100% - 20rpx);
height: 68rpx;
font-family: Inter, Inter;
font-weight: 400;
font-size: 56rpx;
color: #ffffff;
line-height: 65rpx;
text-align: left;
font-style: normal;
text-transform: none;
}
.circle-content {
width: 731rpx;
height: 747rpx;
position: relative;
}
.circle {
width: 225rpx;
height: 225rpx;
background: linear-gradient(145deg, #13c57c 0%, #8dc5ae 100%);
border-radius: 50%;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
font-weight: 400;
font-size: 35rpx;
color: #ffffff;
text-align: center;
line-height: 225rpx;
}
.circle::before,
.circle::after {
width: 225rpx;
height: 225rpx;
background: linear-gradient(145deg, #13c57c 0%, #8dc5ae 100%);
border-radius: 50%;
position: absolute;
left: 0;
top: 0;
z-index: -1;
content: '';
}
@keyframes larger2 {
0% {
transform: scale(1);
}
100% {
transform: scale(2.5);
opacity: 0;
}
}
.circle::after {
animation: larger1 2s infinite;
}
@keyframes larger1 {
0% {
transform: scale(1);
}
100% {
transform: scale(3.5);
opacity: 0;
}
}
.circle::before {
animation: larger2 2s infinite;
}
/* 岗位标签 */
.tabs {
position: relative;
width: 100%;
height: 100%;
border-radius: 50%;
z-index: 3;
}
.tab {
position: absolute;
white-space: nowrap;
padding: 8rpx 16rpx;
background: #fff;
border-radius: 40rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.15);
transition: all 0.5s ease;
}
/* 关闭按钮 */
.close-btn {
width: 549rpx;
line-height: 85rpx;
height: 85rpx;
background: #2ecc71;
color: #ffffff;
}
/* popup */
.popContent {
padding: 24rpx;
background: #4778ec;
height: calc(100% - 49rpx);
.sex-content {
border-radius: 20rpx;
width: 100%;
margin-top: 20rpx;
margin-bottom: 40rpx;
display: flex;
overflow: hidden;
height: calc(100% - 100rpx);
border: 1px solid #4778ec;
}
.s-header {
display: flex;
justify-content: space-between;
text-align: center;
font-size: 16px;
.heade-lf {
line-height: 30px;
width: 50px;
height: 30px;
border-radius: 4px;
border: 1px solid #666666;
color: #666666;
background: #ffffff;
}
.heade-ri {
line-height: 30px;
width: 50px;
height: 30px;
border-radius: 4px;
border: 1px solid #1b66ff;
background-color: #1b66ff;
color: #ffffff;
}
}
}
</style>

View File

@@ -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>

View File

@@ -1,16 +0,0 @@
<template>
<view class="zhuo-tabs"></view>
</template>
<script lang="ts">
export default {
name: 'zhuo-tabs',
data() {
return {};
},
};
</script>
<style lang="stylus">
.zhuo-tabs
</style>