Browse Source

弹出层更新

main
jiangAB 1 week ago
parent
commit
16c5dd6271
  1. 69
      api/httpSetting.js
  2. 11
      api/index.js
  3. 278
      components/popup-drawer/popup-drawer.vue
  4. 34
      components/recruitment/recruitment.vue
  5. 7
      pages.json
  6. 465
      pages/index/index.vue
  7. 8
      pages/login/login.vue
  8. 215
      pages/my/editProfile.vue
  9. 47
      pages/my/my.vue
  10. 73
      pages/searchPositions/searchPositions.vue
  11. BIN
      static/default-avatar.png
  12. 14
      stores/user.js
  13. 38
      utils/index.js

69
api/httpSetting.js

@ -22,7 +22,7 @@ const BASE_URL = baseApi[env] || 'http://110.40.156.216:30005/api';
function request(url, method, data = {}) { function request(url, method, data = {}) {
const userInfo = uni.getStorageSync('loginInfo') || {}; const loginRes = uni.getStorageSync('loginRes') || {};
const UTCOffset = new Date().getTimezoneOffset(); const UTCOffset = new Date().getTimezoneOffset();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
uni.request({ uni.request({
@ -30,12 +30,9 @@ function request(url, method, data = {}) {
method: method, method: method,
data: data, data: data,
header: { header: {
'AccessToken': userInfo.accessToken || '-1', // 请求头,可根据需要调整 'AccessToken': loginRes.accessToken || '-1', // 请求头,可根据需要调整
'UserId': userInfo.userId || '1', // 请求头,可根据需要调整 'UserId': loginRes.appUserId || '-1', // 请求头,可根据需要调整
'LanguageType': 0, 'LanguageType': 0,
'LoginName': userInfo.loginName || '',
'CompanyId': userInfo.companyId || '',
'UTCOffset': UTCOffset,
}, },
success: (res) => { success: (res) => {
if (res.statusCode === 200) { if (res.statusCode === 200) {
@ -45,16 +42,52 @@ function request(url, method, data = {}) {
} else { } else {
reject(res.data) reject(res.data)
} }
/* if (res.data.code === 500) { } else {
reject(res);
}
},
fail: (err) => {
uni.showToast({ uni.showToast({
title: '系统错误', // 提示的文本内容 title: '请求错误', // 提示的文本内容
icon: 'none', // 提示的图标,可选值:'success' | 'loading' | 'none' icon: 'none', // 提示的图标,可选值:'success' | 'loading' | 'none'
duration: 2000 // 提示框的显示时间,单位为毫秒,默认1500ms duration: 2000 // 提示框的显示时间,单位为毫秒,默认1500ms
}); });
reject(err); // 请求失败
} }
if (res.data.code === 401) { });
});
}
// GET 请求
export function get(url, params = {}) {
return request(url, 'GET', params);
}
// POST 请求
export function post(url, data = {}) {
return request(url, 'POST', data);
}
export function uploadFile(url, filePath = '', data = {}) {
const loginRes = uni.getStorageSync('loginRes') || {};
return new Promise((resolve, reject) => {
uni.uploadFile({
url: BASE_URL + url, // 完整的URL
filePath: filePath,
name: 'image',
header: {
'AccessToken': loginRes.accessToken || '-1', // 请求头,可根据需要调整
'UserId': loginRes.appUserId || '-1', // 请求头,可根据需要调整
LanguageType: 0,
// 其他需要的请求头信息(如Token)
},
formData: data,
success: (res) => {
if (res.statusCode === 200) {
if (res.data.code === 500) {
uni.showToast({ uni.showToast({
title: '登录过期,重新登录中', // 提示的文本内容 title: '系统错误', // 提示的文本内容
icon: 'none', // 提示的图标,可选值:'success' | 'loading' | 'none' icon: 'none', // 提示的图标,可选值:'success' | 'loading' | 'none'
duration: 2000 // 提示框的显示时间,单位为毫秒,默认1500ms duration: 2000 // 提示框的显示时间,单位为毫秒,默认1500ms
}); });
@ -62,11 +95,10 @@ function request(url, method, data = {}) {
if (res.data.code === 20001) { if (res.data.code === 20001) {
reject(res.data); reject(res.data);
} }
resolve(res.data) */ resolve(res.data); // 返回数据
} else { } else {
reject(res); reject(res.data); // 返回错误信息
} }
}, },
fail: (err) => { fail: (err) => {
uni.showToast({ uni.showToast({
@ -77,15 +109,6 @@ function request(url, method, data = {}) {
reject(err); // 请求失败 reject(err); // 请求失败
} }
}); });
}); })
}
// GET 请求
export function get(url, params = {}) {
return request(url, 'GET', params);
}
// POST 请求
export function post(url, data = {}) {
return request(url, 'POST', data);
} }

11
api/index.js

@ -1,4 +1,4 @@
import { get, post } from "./httpSetting"; import { get, post, uploadFile } from "./httpSetting";
/** /**
* 登录 * 登录
@ -6,3 +6,12 @@ import { get, post } from "./httpSetting";
export const userLogin = (params ={}) => post('/app/auth/login', params); export const userLogin = (params ={}) => post('/app/auth/login', params);
export const getAppJobVO = (params ={}) => get('/app/job/getAppJobVO', params); export const getAppJobVO = (params ={}) => get('/app/job/getAppJobVO', params);
export const appUserQuery = (params ={}) => get('/app/user/query', params);
export const appUserEditAvatar = (filePath, params ={}) => uploadFile('/app/user/editAvatar', filePath, params);
export const appserEditUser = (params ={}) => post('/app/user/editUser', params);
export const factoryMapGetListPage = (params ={}) => get('/app/factoryMap/getListPage', params);

278
components/popup-drawer/popup-drawer.vue

@ -0,0 +1,278 @@
<template>
<view class="drawer-container">
<!-- 蒙层 -->
<!-- <view
class="mask"
:class="{ show: visibleInner && isFullScreen }"
:style="{ top: isFullScreen ? '0' : halfY + 'px' }"
@click="close"
/> -->
<!-- 抽屉 -->
<view
class="drawer"
:class="{
dragging: isDragging
}"
:style="{ transform: `translateY(${translateY}px)` }"
@touchstart="onStart"
@touchmove.stop.prevent="onMove"
@touchend="onEnd"
>
<!-- 顶部拖拽条 -->
<view class="header" @touchstart.stop="onStart">
<view class="bar"></view>
</view>
<!-- 内容 -->
<view class="content">
<slot></slot>
</view>
</view>
</view>
</template>
<script>
export default {
name: "PopupDrawer",
props: {
visible: {
type: Boolean,
default: false
}
},
data() {
return {
visibleInner: false,
//
isDragging: false,
startY: 0,
currentY: 0,
//
windowHeight: 0,
halfY: 0,
fullY: 0,
translateY: 0,
startTranslateY: 0,
//
isFullScreen: false
};
},
watch: {
visible: {
immediate: true,
handler(v) {
if (v) {
this.open();
} else {
this.close();
}
}
}
},
created() {
const res = uni.getSystemInfoSync();
this.windowHeight = res.windowHeight;
this.halfY = this.windowHeight * 0.5;
this.fullY = 0;
this.translateY = this.halfY;
},
methods: {
/** 打开 */
open() {
this.visibleInner = true;
this.isFullScreen = false; //
this.$nextTick(() => {
this.animateTo(this.halfY);
});
},
/** 关闭 */
close() {
this.isFullScreen = false;
this.animateTo(this.windowHeight, () => {
this.visibleInner = false;
this.$emit("update:visible", false);
this.$emit("close");
});
},
/** 手势开始 */
onStart(e) {
//
e.preventDefault();
e.stopPropagation();
this.isDragging = true;
this.startY = e.touches[0].clientY;
this.startTranslateY = this.translateY;
},
/** 手势移动 */
onMove(e) {
//
e.preventDefault();
e.stopPropagation();
if (!this.isDragging) return;
const moveY = e.touches[0].clientY - this.startY;
let newY = this.startTranslateY + moveY;
//
if (newY < 0) newY = 0;
if (newY > this.windowHeight) newY = this.windowHeight;
this.translateY = newY;
},
/** 手势结束(吸附逻辑) */
onEnd(e) {
e.preventDefault();
e.stopPropagation();
this.isDragging = false;
const moveY = e.changedTouches[0].clientY - this.startY;
// ---------------------------
// 1.
// ---------------------------
if (this.translateY === this.fullY) { // translateY = 0
// 80
if (moveY > 80) {
this.isFullScreen = false;
return this.animateTo(this.halfY);
}
//
if (moveY < -50) {
this.isFullScreen = true;
return this.animateTo(this.fullY);
}
}
// ---------------------------
// 2.
// ---------------------------
if (this.translateY >= this.halfY - 10 && this.translateY <= this.halfY + 10) {
//
if (moveY < -80) {
this.isFullScreen = true;
return this.animateTo(this.fullY);
}
//
if (moveY > 0 && moveY < 120) {
this.isFullScreen = false;
return this.animateTo(this.halfY);
}
}
// ---------------------------
// 3.
// ---------------------------
if (moveY > 120) {
this.isFullScreen = false;
return this.close();
}
// ---------------------------
// 4.
// ---------------------------
const points = [this.fullY, this.halfY];
let nearest = points.reduce((prev, curr) =>
Math.abs(curr - this.translateY) < Math.abs(prev - this.translateY) ? curr : prev
);
this.isFullScreen = (nearest === this.fullY);
this.animateTo(nearest);
},
/** 动画过渡(使用 RAF,不卡顿) */
animateTo(targetY, callback) {
//
const animation = uni.createAnimation({
duration: 250,
timingFunction: "cubic-bezier(0.25, 0.1, 0.25, 1)"
});
animation.translateY(targetY).step();
this.animationData = animation.export();
this.translateY = targetY;
if (callback) {
setTimeout(callback, 260);
}
}
}
};
</script>
<style lang="scss" scoped>
.drawer-container {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
pointer-events: none; /* 默认不阻挡交互 */
z-index: 9999;
.mask {
position: absolute;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.45);
opacity: 0;
transition: opacity 0.25s;
pointer-events: auto; /* 蒙层接收交互 */
&.show {
opacity: 1;
}
}
.drawer {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: #fff;
border-radius: 24rpx 24rpx 0 0;
transition: transform 0.25s ease-out;
pointer-events: auto; /* 抽屉部分接收交互 */
&.dragging {
transition: none !important;
}
.header {
width: 100%;
height: 80rpx;
display: flex;
justify-content: center;
align-items: center;
.bar {
width: 120rpx;
height: 12rpx;
border-radius: 6rpx;
background: #ccc;
}
}
.content {
height: calc(100% - 80rpx);
overflow-y: auto;
}
}
}
</style>

34
components/recruitment/recruitment.vue

@ -4,7 +4,7 @@
<view class="card-head"> <view class="card-head">
<div class="factory-info"> <div class="factory-info">
<h3 class="factory-name">{{dataSource.factoryName || '默认工厂'}}</h3> <h3 class="factory-name">{{dataSource.factoryName || '默认工厂'}}</h3>
<span class="factory-type">{{dataSource.factoryType || '电子厂'}}</span> <!-- <span class="factory-type">{{dataSource.factoryType || '电子厂'}}</span> -->
</div> </div>
<div class="card-actions"> <div class="card-actions">
<!-- <view class="favorite-btn" @click="toggleFavorite"> <!-- <view class="favorite-btn" @click="toggleFavorite">
@ -21,9 +21,10 @@
<!-- 工作时间和内容 --> <!-- 工作时间和内容 -->
<view class="work-info"> <view class="work-info">
<text class="work-time">{{dataSource.workTime || '8:00-20:00'}}</text> <text class="work-time">{{dataSource.workTime || '8:00-20:00'}}</text>
<text class="work-type">{{dataSource.workType || '两班倒'}}</text> <text class="work-type">{{dataSource.gender}}</text>
<text class="work-content">{{dataSource.workContent || '主营汽车线束'}}</text> <text class="work-content">{{dataSource.jobName || '主营汽车线束'}}</text>
<span class="distance">{{dataSource.distance || '距你2.5km'}}</span> <span v-if="!!dataSource.distanceMeters" class="distance">{{`距你${Math.round(dataSource.distanceMeters)}`}}</span>
<text v-for="item in dataSource.tags" class="work-type">{{item.tagName}}</text>
</view> </view>
<view class="card-bm"> <view class="card-bm">
@ -55,36 +56,38 @@
}, },
data() { data() {
return { return {
isFavorite: false, isFavorite: false
companyData: [{ };
},
computed: {
companyData() {
return [
{
label: '薪资', label: '薪资',
value: '20', value: this.dataSource.hourlyWage || '无', // 使25
unit: '', unit: '',
exLabel: '元/小时' exLabel: '元/小时'
}, },
{ {
label: '年龄', label: '年龄',
value: '20-42', value: this.dataSource.ageRange || '无',
unit: '', unit: '',
exLabel: '元/小时' exLabel: '元/小时'
}, },
{ {
label: '吃饭', label: '吃饭',
value: '2', value: this.dataSource.mealPlan || '无',
unit: '餐', unit: '餐',
exLabel: '白-夜' exLabel: '白-夜'
}, },
{ {
label: '住宿', label: '住宿',
value: '20', value: this.dataSource.dorm || '无',
unit: '', unit: '',
exLabel: '人间' exLabel: '人间'
}, },
], ];
};
}, },
computed: {
processedCompanyData() { processedCompanyData() {
return this.dataSource.companyData || this.companyData; return this.dataSource.companyData || this.companyData;
} }
@ -94,7 +97,7 @@
makeCall(e) { makeCall(e) {
e.stopPropagation(); e.stopPropagation();
uni.makePhoneCall({ uni.makePhoneCall({
phoneNumber: '400-123-4567' phoneNumber: this.dataSource.phoneNumber
}); });
}, },
// //
@ -197,6 +200,7 @@
font-size: 12px; font-size: 12px;
color: #666; color: #666;
margin-bottom: 12px; margin-bottom: 12px;
flex-wrap: wrap;
} }
.card-bm { .card-bm {

7
pages.json

@ -18,6 +18,13 @@
"navigationBarTitleText": "个人中心" "navigationBarTitleText": "个人中心"
} }
}, },
{
"path" : "pages/my/editProfile",
"style" :
{
"navigationBarTitleText" : "编辑资料"
}
},
{ {
"path" : "pages/map/map", "path" : "pages/map/map",
"style" : "style" :

465
pages/index/index.vue

@ -1,92 +1,323 @@
<template> <template>
<view class="index-page"> <view class="index-page">
<map style="height: 100vh; width: 100vw;" id="myMap" :markers="markers" show-location show-compass <map style="height: 100vh; width: 100vw;" id="myMap" :markers="markers" show-location show-compass
@updated="handleMapLoad"></map> @updated="handleMapLoad" @markertap="onMarkerTap"></map>
<view class="selectors-group">
<view class="distance-selector" @click="openDistancePicker">
<text class="distance-text">{{ selectedDistance }}km </text>
</view>
<view class="region-selector" @click="openRegionPicker">
<text class="region-text">{{ selectedRegion }} </text>
</view>
</view>
<!-- 省份选择器 -->
<view class="region-picker-modal" v-if="showRegionPicker">
<view class="picker-header">
<text class="cancel-btn" @click="showRegionPicker = false">取消</text>
<text class="confirm-btn" @click="confirmRegion">确定</text>
</view>
<picker-view class="region-picker" :value="regionValue" immediate-change @change="onRegionChange">
<picker-view-column>
<view class="picker-item" v-for="(province, index) in regionData" :key="index">
{{ province.label }}
</view>
</picker-view-column>
</picker-view>
</view>
<!-- 距离范围选择器 -->
<view class="distance-picker-modal" v-if="showDistancePicker">
<view class="picker-header">
<text class="cancel-btn" @click="showDistancePicker = false">取消</text>
<text class="confirm-btn" @click="confirmDistance">确定</text>
</view>
<picker-view class="distance-picker" :value="distanceValue" immediate-change @change="onDistanceChange">
<picker-view-column>
<view class="picker-item" v-for="(distance, index) in distanceData" :key="index">
{{ distance.label }}km
</view>
</picker-view-column>
</picker-view>
</view>
<view class="rec-swiper"> <view class="rec-swiper">
<recruitment></recruitment> <recruitment></recruitment>
</view> </view>
<!-- 弹出层示例 -->
<popup-drawer :visible="showPopup" @update:visible="showPopup = $event" @close="onPopupClose">
<view class="popup-content">
<view class="popup-title">弹出层标题</view>
<view class="popup-body">
<p>这是弹出层的内容区域</p>
<p>你可以在这里放置任何内容</p>
<p>按住顶部拖拽区域可以上拉至全屏</p>
<p>向下拉动可以关闭弹出层</p>
</view>
</view>
</popup-drawer>
<!-- 触发按钮 -->
<view class="trigger-button" @click="showPopup = true">
打开弹出层
</view>
</view> </view>
</template> </template>
<script> <script setup>
export default { import {
data() { ref,
return { computed
companyData: [{ } from 'vue';
label: '薪资', import {
value: '20', onLoad
unit: '', } from '@dcloudio/uni-app'
exLabel: '元/小时' import {
factoryMapGetListPage
} from '../../api';
import {
delay,
getLatLng
} from '../../utils';
import PopupDrawer from '../../components/popup-drawer/popup-drawer.vue';
//
const showRegionPicker = ref(false);
const selectedRegion = ref('江苏省');
const regionValue = ref(0);
//
const showDistancePicker = ref(false);
const selectedDistance = ref(1); // 1km
const distanceValue = ref(0);
//
const showPopup = ref(false);
//
const distanceData = ref([{
label: '1',
value: 1
}, },
{ {
label: '年龄', label: '5',
value: '20-42', value: 5
unit: '',
exLabel: '元/小时'
}, },
{ {
label: '吃饭', label: '10',
value: '2', value: 10
unit: '餐',
exLabel: '白-夜'
}, },
{ {
label: '住宿', label: '50',
value: '20', value: 50
unit: '', },
exLabel: '人间' {
label: '100',
value: 100
},
{
label: '500',
value: 500
}
]);
//
const regionData = ref([{
label: '江苏省',
value: '江苏省'
}, },
], {
markers: [{ label: '浙江省',
id: 1, value: '浙江省'
latitude: 39.90923, }
longitude: 116.397428, ]);
title: '模拟点位1',
//
const getStoredRegionInfo = () => {
try {
const storedRegion = uni.getStorageSync('selectedRegionInfo');
if (storedRegion) {
return JSON.parse(storedRegion);
}
} catch (e) {
console.error('读取本地存储的地区信息失败', e);
}
return null;
};
//
const setStoredRegionInfo = (province, label) => {
try {
const regionInfo = {
province,
label
};
uni.setStorageSync('selectedRegionInfo', JSON.stringify(regionInfo));
} catch (e) {
console.error('保存地区信息到本地存储失败', e);
}
};
const openRegionPicker = () => {
showRegionPicker.value = true;
};
const openDistancePicker = () => {
showDistancePicker.value = true;
};
//
const onRegionChange = (e) => {
const values = e.detail.value;
console.log(values)
regionValue.value = values;
};
//
const onDistanceChange = (e) => {
const values = e.detail.value;
distanceValue.value = values;
};
//
const confirmRegion = () => {
const provinceIndex = regionValue.value[0];
const province = regionData.value[provinceIndex];
selectedRegion.value = province.label;
console.log(selectedRegion.value)
console.log(selectedRegion.value)
showRegionPicker.value = false;
//
setStoredRegionInfo(province.label);
//
handleGetFactoryList(province.label);
};
//
const confirmDistance = () => {
const distanceIndex = distanceValue.value[0];
const distance = distanceData.value[distanceIndex];
selectedDistance.value = distance.value;
showDistancePicker.value = false;
//
handleGetFactoryList(selectedRegion.value);
};
let userLatLng = {
lat: null,
lng: null
}
const markers = ref([
{
id: 'default1',
latitude: 31.230416,
longitude: 121.473701,
title: '默认点位1',
iconPath: '/static/position-icon.png', iconPath: '/static/position-icon.png',
width: 30, width: 30,
height: 30, height: 30,
label: { label: {
content: '模拟点位1', content: '默认点位1',
textAlign: 'center', textAlign: 'center',
fontSize: 18, fontSize: 18,
} }
}, },
{ {
id: 2, id: 'default2',
latitude: 39.8, latitude: 31.230416,
longitude: 116.3, longitude: 121.473701,
title: '模拟点位1', title: '默认点位2',
iconPath: '/static/position-icon.png', iconPath: '/static/position-icon.png',
width: 30, width: 30,
height: 30, height: 30,
label: { label: {
content: '模拟点位1', content: '默认点位2',
textAlign: 'center', textAlign: 'center',
fontSize: 18, fontSize: 18,
} }
},
]
} }
}, ])
onLoad() { const handleGetFactoryList = async (province = '江苏省') => {
// uni.showLoading({
}, title: '加载中'
methods: {
handleMapLoad() {
console.log('地图加载完成');
//
const mapContext = uni.createMapContext('myMap');
//
mapContext.includePoints({
points: this.markers.map(marker => ({
latitude: marker.latitude,
longitude: marker.longitude
})),
padding: [50, 50, 200, 50] //
}); });
if (!(userLatLng.lat && userLatLng.lng)) {
userLatLng = await getLatLng()
} }
try {
const params = {
...(userLatLng.lat && userLatLng.lng ? userLatLng : {}),
province: province,
maxDistance: selectedDistance.value * 1000 //
} }
await delay(500)
const {
data
} = await factoryMapGetListPage(params)
// markers
markers.value = markers.value.slice(0, 2);
data.records.map(item => markers.value.push({
id: item.id,
latitude: item.lat,
longitude: item.lng,
title: item.factoryName,
iconPath: '/static/position-icon.png',
width: 30,
height: 30,
label: {
content: item.factoryName,
textAlign: 'center',
fontSize: 18,
}
}))
console.log(await markers.value)
} catch (err) {
if (!(userLatLng.lat && userLatLng.lng)) {
uni.showToast({
title: '经纬度获取失败',
icon: 'none'
})
} else {
uni.showToast({
title: '位置获取失败',
icon: 'none'
})
}
} finally {
uni.hideLoading()
}
}
//
const onPopupClose = () => {
showPopup.value = false;
}
const onMarkerTap = (e) => {
const markerId = e.markerId;
console.log(markerId)
} }
onLoad(() => {
//
const storedRegion = getStoredRegionInfo();
if (storedRegion) {
// 使
selectedRegion.value = storedRegion.label;
handleGetFactoryList(storedRegion.province);
} else {
// 使
handleGetFactoryList();
}
})
</script> </script>
<style lang="scss"> <style lang="scss">
@ -95,6 +326,102 @@
overflow: hidden; overflow: hidden;
position: relative; position: relative;
.selectors-group {
position: absolute;
top: 25rpx;
right: 20rpx;
display: flex;
flex-direction: row-reverse;
gap: 6rpx;
z-index: 999;
}
.region-selector {
background-color: rgba(255, 255, 255, 0.9);
padding: 15rpx 25rpx;
border-radius: 15rpx;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
.region-text {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
}
.distance-selector {
background-color: rgba(255, 255, 255, 0.9);
padding: 15rpx 25rpx;
border-radius: 15rpx;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
.distance-text {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
}
/* 省市区选择器样式 */
.region-picker-modal {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 500rpx;
background-color: #fff;
z-index: 1000;
}
/* 距离范围选择器样式 */
.distance-picker-modal {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 500rpx;
background-color: #fff;
z-index: 1000;
}
.picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 30rpx;
border-bottom: 1rpx solid #eee;
}
.cancel-btn,
.confirm-btn {
font-size: 32rpx;
padding: 10rpx 20rpx;
}
.cancel-btn {
color: #999;
}
.confirm-btn {
color: #007AFF;
}
.region-picker,
.distance-picker {
height: 400rpx;
width: 100%;
}
.picker-item {
display: flex;
align-items: center;
justify-content: center;
height: 80rpx;
line-height: 80rpx;
text-align: center;
font-size: 30rpx;
}
.rec-swiper { .rec-swiper {
width: calc(100% - 16px); width: calc(100% - 16px);
// height: 200px; // height: 200px;
@ -107,5 +434,39 @@
border-radius: 16px; border-radius: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
} }
.trigger-button {
position: fixed;
bottom: 120rpx;
left: 50%;
transform: translateX(-50%);
background-color: #007AFF;
color: white;
padding: 20rpx 40rpx;
border-radius: 50rpx;
font-size: 28rpx;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
z-index: 998;
}
.popup-content {
padding: 20rpx;
}
.popup-title {
font-size: 36rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
}
.popup-body {
font-size: 28rpx;
line-height: 1.6;
}
.popup-body p {
margin-bottom: 20rpx;
}
} }
</style> </style>

8
pages/login/login.vue

@ -38,7 +38,7 @@
<script setup> <script setup>
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { userLogin } from '../../api'; import { appUserQuery, userLogin } from '../../api';
import { useUserStore } from '../../stores/user.js'; import { useUserStore } from '../../stores/user.js';
// //
const agreeChecked = ref(false); const agreeChecked = ref(false);
@ -68,23 +68,21 @@ const onWechatLogin = (e) => {
provider: 'weixin', provider: 'weixin',
success: async (wxLoginRes) => { success: async (wxLoginRes) => {
try { try {
console.log('微信登录成功', wxLoginRes);
const code = wxLoginRes.code; const code = wxLoginRes.code;
const loginRes = await userLogin({ code }); const loginRes = await userLogin({ code });
console.log('登录接口返回:', loginRes);
// //
uni.hideLoading(); uni.hideLoading();
// 使 Pinia loginRes // 使 Pinia loginRes
userStore.setLoginRes(loginRes.data); userStore.setLoginRes(loginRes.data);
console.log('loginRes 已存储到 Pinia:', loginRes.data);
// //
uni.showToast({ uni.showToast({
title: '登录成功', title: '登录成功',
icon: 'success' icon: 'success'
}); });
const userProfileRes = await appUserQuery()
userStore.setUserInfo(userProfileRes.data);
// //
setTimeout(() => { setTimeout(() => {
uni.switchTab({ uni.switchTab({

215
pages/my/editProfile.vue

@ -0,0 +1,215 @@
<template>
<view class="edit-profile-page">
<!-- 头像编辑区域 -->
<view class="profile-section">
<view class="profile-item" @click="changeAvatar">
<text class="item-label">头像</text>
<view class="item-value avatar-container">
<button class="avatar-btn" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
<image class="avatar" :src="userInfo.avatarUrl || '/static/default-avatar.png'" mode="aspectFill"></image>
</button>
<!-- <text class="arrow">></text> -->
</view>
</view>
<view class="profile-item" @click="editNickname">
<text class="item-label">昵称</text>
<view class="item-value">
<text class="nickname">{{ userInfo.nickname || '未设置' }}</text>
<!-- <text class="arrow">></text> -->
</view>
</view>
<!-- <view class="profile-item">
<text class="item-label">性别</text>
<view class="item-value">
<text>{{ userInfo.gender === 1 ? '男' : userInfo.gender === 2 ? '女' : '未设置' }}</text>
</view>
</view> -->
</view>
<!-- 保存按钮 -->
<!-- <view class="save-section">
<button class="save-btn" @click="saveProfile">保存</button>
</view> -->
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
import { useUserStore } from '../../stores/user.js';
import { appUserEditAvatar, appserEditUser } from '../../api/index.js';
const userStore = useUserStore();
const userInfo = computed(() => userStore.userInfo || {});
//
const onChooseAvatar = async (e) => {
const { avatarUrl } = e.detail
console.log(avatarUrl)
try {
await appUserEditAvatar(avatarUrl)
// store
if (avatarUrl) {
userStore.updateUserInfoField('avatarUrl', avatarUrl);
}
} catch(err) {
console.log(err)
}
}
//
const changeAvatar = () => {
//
console.log('点击更换头像');
};
//
const editNickname = () => {
uni.showModal({
title: '编辑昵称',
editable: true,
placeholderText: '请输入昵称',
success: async (res) => {
if (res.confirm) {
const nickname = res.content;
if (nickname) {
/* userStore.updateUserInfoField('nickName', nickname);
uni.showToast({
title: '昵称已更新',
icon: 'success'
}); */
try {
await appserEditUser({ nickname })
userStore.updateUserInfoField('nickname', nickname);
} catch(err) {
console.log(err)
}
}
}
}
});
};
//
const saveProfile = () => {
uni.showToast({
title: '资料保存成功',
icon: 'success'
});
//
setTimeout(() => {
uni.navigateBack();
}, 1000);
};
</script>
<style lang="scss">
.edit-profile-page {
background-color: #f5f5f5;
min-height: 100vh;
padding: 0;
margin: 0;
.page-header {
background-color: #fff;
padding: 16px;
text-align: center;
position: relative;
border-bottom: 1px solid #eee;
.page-title {
font-size: 18px;
font-weight: bold;
color: #333;
}
}
.profile-section {
background-color: #fff;
margin-top: 12px;
padding: 0 16px;
.profile-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid #eee;
&:last-child {
border-bottom: none;
}
.item-label {
font-size: 16px;
color: #333;
}
.item-value {
display: flex;
align-items: center;
color: #999;
.nickname {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.arrow {
margin-left: 8px;
font-size: 18px;
}
.avatar-container {
display: flex;
align-items: center;
}
.avatar-btn {
background: none;
border: none;
padding: 0;
margin: 0;
width: 50px;
height: 50px;
border-radius: 25px;
overflow: hidden;
&::after {
border: none;
}
.avatar {
width: 50px;
height: 50px;
border-radius: 25px;
}
}
}
}
}
.save-section {
padding: 24px 16px;
.save-btn {
width: 100%;
height: 48px;
background-color: #07c160;
color: #fff;
font-size: 16px;
border-radius: 24px;
border: none;
&::after {
border: none;
}
}
}
}
</style>

47
pages/my/my.vue

@ -1,11 +1,19 @@
<template> <template>
<view class="my-page"> <view class="my-page">
<!-- 登录区域 --> <!-- 登录区域 -->
<view class="login-section" @click="loginRegister"> <view class="login-section" @click="goToProfile">
<!-- <button class="avatar" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
<image :src="userInfo && userInfo.avatarUrl || '/static/default-avatar.png'" mode="aspectFit"></image>
</button> -->
<view class="avatar"> <view class="avatar">
<text class="avatar-icon">😊</text> <!-- <image class="avatar" src="{{avatarUrl}}"></image> -->
<image :src="userInfo && userInfo.avatarUrl || '/static/default-avatar.png'" mode="aspectFit"></image>
</view> </view>
<view class="login-text"> <view v-if="loginRes && loginRes.accessToken" class="login-text">
<text class="login-title">{{userInfo.nickname || '默认昵称'}}</text>
<!-- <text class="login-subtitle">登录查看更多消息</text> -->
</view>
<view v-else class="login-text">
<text class="login-title">点击登录/注册</text> <text class="login-title">点击登录/注册</text>
<text class="login-subtitle">登录查看更多消息</text> <text class="login-subtitle">登录查看更多消息</text>
</view> </view>
@ -33,8 +41,7 @@
</view> </view>
<text class="feature-name">咨询客服</text> <text class="feature-name">咨询客服</text>
</view> </view>
</view>123 </view>
{{loginRes}}
<!-- 底部占位避免内容被底部导航栏遮挡 --> <!-- 底部占位避免内容被底部导航栏遮挡 -->
<view class="bottom-space"></view> <view class="bottom-space"></view>
</view> </view>
@ -43,9 +50,32 @@
<script setup> <script setup>
import { computed } from 'vue'; import { computed } from 'vue';
import { useUserStore } from '../../stores/user.js'; import { useUserStore } from '../../stores/user.js';
import { appUserEditAvatar } from '../../api/index.js';
const userStore = useUserStore(); const userStore = useUserStore();
const loginRes = computed(() => userStore.loginRes); const loginRes = computed(() => userStore.loginRes);
const userInfo = computed(() => userStore.userInfo);
const loginRegister = () => {
if (loginRes.value && loginRes.value.accessToken) {
return;
}
uni.navigateTo({
url: '/pages/login/login'
})
}
const goToProfile = () => {
if (loginRes.value && loginRes.value.accessToken) {
//
uni.navigateTo({
url: '/pages/my/editProfile'
})
} else {
//
uni.navigateTo({
url: '/pages/login/login'
})
}
}
</script> </script>
<style lang="scss"> <style lang="scss">
@ -65,14 +95,17 @@ const loginRes = computed(() => userStore.loginRes);
.avatar { .avatar {
width: 60px; width: 60px;
height: 60px; height: 60px;
border-radius: 30px; margin: 0;
padding: 0;
border-radius: 4px;
background-color: #e6f7ff; background-color: #e6f7ff;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
overflow: hidden;
margin-right: 16px; margin-right: 16px;
.avatar-icon { .avatar-image {
font-size: 30px; font-size: 30px;
} }
} }

73
pages/searchPositions/searchPositions.vue

@ -16,7 +16,8 @@
</view> </view>
</view> </view>
<view class="search-list" :refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh" @scrolltolower="onLoadMore"> <scroll-view class="search-list" scroll-y :refresher-enabled="true" :refresher-triggered="refreshing"
@refresherrefresh="onRefresh" @scrolltolower="onLoadMore">
<template v-for="item in list.records" :key="item.jobId"> <template v-for="item in list.records" :key="item.jobId">
<recruitment :dataSource="item"></recruitment> <recruitment :dataSource="item"></recruitment>
<view class="driver"></view> <view class="driver"></view>
@ -25,7 +26,7 @@
<text v-if="loadingMore">加载中...</text> <text v-if="loadingMore">加载中...</text>
<text v-else-if="noMoreData">没有更多数据了</text> <text v-else-if="noMoreData">没有更多数据了</text>
</view> </view>
</view> </scroll-view>
</view> </view>
</template> </template>
@ -39,45 +40,75 @@
import { import {
ref ref
} from 'vue'; } from 'vue';
import {
delay, getLatLng
} from '../../utils';
let currentPage = 1
const list = ref([]) const list = ref([])
const searchKeyword = ref('') const searchKeyword = ref('')
const refreshing = ref(false) const refreshing = ref(false)
const loadingMore = ref(false) const loadingMore = ref(false)
const noMoreData = ref(false) const noMoreData = ref(false)
const currentPage = ref(1)
const pageSize = ref(10) const pageSize = ref(10)
let userLatLng = {
lat: null,
lng: null
}
const hanleGetJobList = async (params = {}) => { const hanleGetJobList = async (params = {}) => {
if (!(userLatLng.lat && userLatLng.lng)) {
userLatLng = await getLatLng()
}
const res = await getAppJobVO({ const res = await getAppJobVO({
pageNum: currentPage.value, pageNum: currentPage,
pageSize: pageSize.value, pageSize: pageSize.value,
...(userLatLng.lat && userLatLng.lng ? userLatLng : {}),
...params ...params
}) })
console.log(res) console.log(res)
if (currentPage.value === 1) { if (currentPage === 1) {
// //
list.value = res.data list.value = res.data
} else { } else {
// //
if (res.data.records.length > 0) {
list.value.records = [...list.value.records, ...res.data.records] list.value.records = [...list.value.records, ...res.data.records]
currentPage++
} else {
noMoreData.value = true
} }
//
if (res.data.records.length < pageSize.value) {
noMoreData.value = true
} else {
noMoreData.value = false
} }
} }
onLoad(() => { onLoad(() => {
currentPage.value = 1 currentPage = 1
hanleGetJobList() hanleGetJobList()
}) })
const onRefresh = async () => { const onRefresh = async () => {
console.log("刷新")
refreshing.value = true refreshing.value = true
loadingMore.value = false
noMoreData.value = false
try {
await delay(1000)
currentPage = 1
await hanleGetJobList()
} catch (err) {
console.log(err)
uni.showToast({
title: '刷新失败',
icon: 'none'
})
} finally {
refreshing.value = false
}
/* refreshing.value = true
currentPage.value = 1 currentPage.value = 1
noMoreData.value = false noMoreData.value = false
try { try {
@ -93,24 +124,26 @@
}) })
} finally { } finally {
refreshing.value = false refreshing.value = false
} } */
} }
const onLoadMore = async () => { const onLoadMore = async () => {
// //
console.log("dao dibule")
if (loadingMore.value || noMoreData.value) return if (loadingMore.value || noMoreData.value) return
loadingMore.value = true loadingMore.value = true
currentPage.value++ currentPage++
try { try {
await delay(1000)
await hanleGetJobList() await hanleGetJobList()
} catch (error) { } catch (error) {
uni.showToast({ uni.showToast({
title: '加载失败', title: '加载失败',
icon: 'none' icon: 'none'
}) })
currentPage.value-- currentPage--
} finally { } finally {
loadingMore.value = false loadingMore.value = false
} }
@ -119,10 +152,10 @@
const onSearch = () => { const onSearch = () => {
console.log(searchKeyword.value) console.log(searchKeyword.value)
const params = { const params = {
jobName: searchKeyword.value keyword: searchKeyword.value
} }
// //
currentPage.value = 1 currentPage = 1
noMoreData.value = false noMoreData.value = false
hanleGetJobList(params) hanleGetJobList(params)
} }
@ -167,6 +200,8 @@
padding: 12px 16px; padding: 12px 16px;
background-color: #fff; background-color: #fff;
z-index: 10; z-index: 10;
box-sizing: border-box;
height: 15vh;
// //
.search-input-wrapper { .search-input-wrapper {
@ -233,9 +268,11 @@
// - // -
.search-list { .search-list {
height: 85vh;
box-sizing: border-box;
padding: 4px; padding: 4px;
flex: 1; // flex: 1;
overflow-y: auto; // overflow-y: auto;
// //
::-webkit-scrollbar { ::-webkit-scrollbar {

BIN
static/default-avatar.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

14
stores/user.js

@ -59,6 +59,20 @@ export const useUserStore = defineStore('user', {
updateUserInfo(updates) { updateUserInfo(updates) {
this.userInfo = { ...this.userInfo, ...updates }; this.userInfo = { ...this.userInfo, ...updates };
uni.setStorageSync('userInfo', this.userInfo); uni.setStorageSync('userInfo', this.userInfo);
},
/**
* 更新单个用户信息字段
* @param {String} field - 要更新的字段名
* @param {*} value - 要更新的字段值
*/
updateUserInfoField(field, value) {
if (this.userInfo) {
this.userInfo = { ...this.userInfo, [field]: value };
} else {
this.userInfo = { [field]: value };
}
uni.setStorageSync('userInfo', this.userInfo);
} }
} }
}); });

38
utils/index.js

@ -0,0 +1,38 @@
/**
* 延时函数
* @param {number} ms - 延迟时间毫秒
* @returns {Promise} - 返回一个 Promise在指定时间后 resolve
*/
export function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
export function getLatLng() {
return new Promise((reslove) => {
uni.getLocation({
type: 'gcj02',
success: function(res) {
console.log('当前位置的经度:' + res.longitude);
console.log('当前位置的纬度:' + res.latitude);
reslove({
lat: res.latitude,
lng: res.longitude
})
},
fail: function(res) {
reslove({
lat: null,
lng: null
})
}
});
});
}
// 使用示例:
// async function example() {
// console.log('开始');
// await delay(2000); // 延时 2 秒
// console.log('2秒后执行');
// }
Loading…
Cancel
Save