Initial commit: 积分兑换电商平台多商户版 MER-2.2
Made-with: Cursor
This commit is contained in:
332
mer_uniapp/pages/admin/workOrder_manage/checkin.vue
Normal file
332
mer_uniapp/pages/admin/workOrder_manage/checkin.vue
Normal file
@@ -0,0 +1,332 @@
|
||||
<template>
|
||||
<view class="relative" :style="cssVarStyle">
|
||||
<!-- #ifdef MP || APP-PLUS -->
|
||||
<NavBar navTitle="服务打卡" :iconColor="iconColor" :isBackgroundColor="false" :textColor="iconColor" :isScrolling="isScrolling" showBack>
|
||||
</NavBar>
|
||||
<!-- #endif -->
|
||||
<TopHeaderfixed></TopHeaderfixed>
|
||||
<view class="checkin-container" :style="{ height: screenHeight }">
|
||||
<view class="checkin-body">
|
||||
<OrderAddress :orderInfo="checkInConfig" v-if="checkInConfig" />
|
||||
<view class="checkin-box">
|
||||
<button class="checkin-btn" :disabled="!allowCheckin" @click="handleCheckin">
|
||||
<view class="checkin-btn-text">{{ checkinBtnText }}</view>
|
||||
<view class="checkin-btn-time">{{ formatTime }}</view>
|
||||
</button>
|
||||
<view class="checkin-box-tips">
|
||||
{{
|
||||
allowCheckin ? "您已进入服务打卡区域" : "您当前不在服务打卡区域"
|
||||
}}
|
||||
</view>
|
||||
<view class="checkin-address-info borderPad">
|
||||
<text class="iconfont icon-ic_location51"></text>
|
||||
<text class="overflow-text checkin-address-info__text">{{ addressInfo }}</text>
|
||||
<button class="checkin-address__refresh" @click="refreshLocation">刷新</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- takePhoto: checkInConfig.clockInPhotoSwitch拍照开关 true是开启-->
|
||||
<CheckinPopup v-if="checkinPopupVisible" :time="formatTime" :address="addressInfo" :workOrderNo="workOrderNo"
|
||||
@cancel="handleCheckinCancel" :takePhoto="checkInConfig && checkInConfig.clockInPhotoSwitch" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import OrderAddress from "../components/OrderAddress";
|
||||
import NavBar from "@/components/navBar.vue"
|
||||
import dayjs from "@/plugin/dayjs/dayjs.min.js";
|
||||
import {clockInfoApi, getCoordinateAddressApi} from "./workOrder";
|
||||
import { getDistanceFromLatLonInMeter} from "@/libs/order";
|
||||
import CheckinPopup from "./components/CheckinPopup.vue";
|
||||
import TopHeaderfixed from "../../../components/TopHeaderfixed";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TopHeaderfixed,
|
||||
NavBar,
|
||||
OrderAddress,
|
||||
CheckinPopup
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
iconColor: '#FFFFFF',
|
||||
isScrolling: false,
|
||||
workOrderNo: null,
|
||||
customerLocation: {}, //商户经纬度
|
||||
checkInConfig: {}, // 打卡配置
|
||||
formatTime: "00:00:00",
|
||||
inCheckinArea: false,
|
||||
addressInfo: "获取位置中...",
|
||||
location: null, // 当前定位经纬度
|
||||
checkinLocation: null,
|
||||
checkinPopupVisible: false,
|
||||
distance: ''
|
||||
};
|
||||
},
|
||||
onLoad(options) {
|
||||
this._cssVarsHandler = vars => { this.$forceUpdate(); };
|
||||
this.$eventHub.$on('css-vars:updated', this._cssVarsHandler);
|
||||
|
||||
this.workOrderNo = options.workOrderNo;
|
||||
this.handleGetWorkOrderDetail();
|
||||
this.getLocation(true);
|
||||
this.timeDisplayTimer();
|
||||
},
|
||||
onUnload() {
|
||||
this.$eventHub.$off('css-vars:updated', this._cssVarsHandler);
|
||||
},
|
||||
computed: {
|
||||
// 打卡文字展示
|
||||
checkinBtnText() {
|
||||
if (this.hasClockRecord) return "已打卡";
|
||||
if (!this.allowCheckin) return "无法打卡";
|
||||
return "点击打卡";
|
||||
},
|
||||
locationDiff() {
|
||||
return {
|
||||
location: this.location,
|
||||
customerLocation: this.customerLocation,
|
||||
checkInConfig: this.checkInConfig
|
||||
};
|
||||
},
|
||||
hasClockRecord() {
|
||||
if (!this.checkInConfig) return false;
|
||||
const { clock_in_info } = this.checkInConfig;
|
||||
if (clock_in_info && clock_in_info.clock_time) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
allowCheckin() {
|
||||
// 在这里判断是否需要在范围内进行定位打卡
|
||||
// 必须获取到打卡配置和订单信息之后再判断是否允许打卡
|
||||
if (!this.checkInConfig) return false;
|
||||
return this.inCheckinArea;
|
||||
},
|
||||
cssVarStyle() {
|
||||
return this.$getCssVarStyle();
|
||||
},
|
||||
screenHeight() {
|
||||
const vars = this.$getCssVarStyle();
|
||||
return vars['--screen-height'] || '100vh';
|
||||
},
|
||||
navBarHeight() {
|
||||
const vars = this.$getCssVarStyle();
|
||||
return vars['--nav-bar-height'] || '44px';
|
||||
},
|
||||
safeAreaBottom() {
|
||||
const vars = this.$getCssVarStyle();
|
||||
return vars['--safe-area-inset-bottom'] || '0px';
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
locationDiff({ location, customerLocation, checkInConfig }) {
|
||||
if (!checkInConfig) return;
|
||||
|
||||
// 如果不限制打卡地址,则设置为在范围内
|
||||
if (!checkInConfig.clockInAddressSwitch) {
|
||||
this.inCheckinArea = true;
|
||||
return;
|
||||
};
|
||||
if (!location || !customerLocation) return;
|
||||
// 单位是km
|
||||
const distance = getDistanceFromLatLonInMeter(customerLocation.latitude, customerLocation.longitude);
|
||||
this.distance = distance
|
||||
this.inCheckinArea = distance * 1000 < checkInConfig.clockInDistance;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// getMerStaffCheckinConfig() {
|
||||
// this.checkInConfig = uni.getStorageSync('reservationConfig');
|
||||
// },
|
||||
// async getLocationByAddress(address) {
|
||||
// try {
|
||||
// const result = await geocoding(address);
|
||||
// const { lng: longitude, lat: latitude } = result.data.location;
|
||||
// this.customerLocation = {
|
||||
// latitude: Number(latitude),
|
||||
// longitude: Number(longitude)
|
||||
// };
|
||||
// } catch (error) {
|
||||
// this.$util.Tips({
|
||||
// title: error,
|
||||
// icon: "none"
|
||||
// });
|
||||
// }
|
||||
// },
|
||||
// 打卡信息
|
||||
async handleGetWorkOrderDetail() {
|
||||
try {
|
||||
const { data } = await clockInfoApi(this.workOrderNo);
|
||||
this.checkInConfig = data;
|
||||
this.customerLocation = {
|
||||
latitude: Number(data.latitude),
|
||||
longitude: Number(data.longitude)
|
||||
};
|
||||
// this.getLocationByAddress(this.orderInfo.user_address);
|
||||
} catch (err) {
|
||||
this.$util.Tips({
|
||||
title: err,
|
||||
icon: "none"
|
||||
});
|
||||
}
|
||||
},
|
||||
handleCheckinCancel() {
|
||||
this.checkinPopupVisible = false;
|
||||
},
|
||||
//点击打卡
|
||||
async handleCheckin() {
|
||||
this.checkinPopupVisible = true;
|
||||
},
|
||||
async refreshLocation() {
|
||||
await this.getLocation();
|
||||
},
|
||||
// 获取当前定位
|
||||
async getLocation(firstCheck = false) {
|
||||
if (this._checkLocationLoading) return;
|
||||
this._checkLocationLoading = true;
|
||||
try {
|
||||
const { latitude, longitude } = await this.$util.$L.getLocation();
|
||||
this.location = {
|
||||
latitude,
|
||||
longitude
|
||||
};
|
||||
const geoCoderRes = await getCoordinateAddressApi({
|
||||
latitude: latitude,
|
||||
longitude: longitude
|
||||
});
|
||||
this.addressInfo = geoCoderRes.data.address;
|
||||
firstCheck !== true && this.$util.Tips({
|
||||
title: "刷新位置成功",
|
||||
icon: "none"
|
||||
});
|
||||
} catch (error) {
|
||||
this.$util.Tips({
|
||||
title: error,
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
this._checkLocationLoading = false;
|
||||
}
|
||||
},
|
||||
timeDisplayTimer() {
|
||||
const setCurrentTime = () => {
|
||||
this.formatTime = dayjs().format("HH:mm:ss");
|
||||
};
|
||||
setCurrentTime();
|
||||
this._displayTimer = setInterval(setCurrentTime, 1000);
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
clearInterval(this._displayTimer);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.checkin-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 256rpx; /* 44px + 212rpx */
|
||||
background-image: linear-gradient(90deg, #2291F8 0%, #1CD1DC 100%);
|
||||
}
|
||||
|
||||
.checkin-bg2 {
|
||||
position: absolute;
|
||||
top: 208rpx; /* 44px + 164rpx */
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 50rpx;
|
||||
background-image: linear-gradient(0deg, #F5F5F5 0%, rgba(245, 245, 245, 0) 100%);
|
||||
}
|
||||
|
||||
.checkin-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.checkin-body {
|
||||
padding: 36rpx 24rpx 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: 10px;
|
||||
|
||||
&.safe-bottom-env {
|
||||
padding-bottom: 0px; /* 移除 CSS 变量 */
|
||||
}
|
||||
}
|
||||
|
||||
.checkin-box {
|
||||
flex: 1;
|
||||
background-color: #fff;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.checkin-btn {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
width: 272rpx;
|
||||
height: 272rpx;
|
||||
border-radius: 50%;
|
||||
margin: 164rpx auto 0;
|
||||
|
||||
background: linear-gradient(159deg, #1CD1DC -13%, #2291F8 43%), linear-gradient(139deg, #47B5FF 12%, #0F86F5 86%);
|
||||
box-shadow: 0rpx 10rpx 32rpx 0rpx rgba(48, 139, 248, 0.5);
|
||||
|
||||
&[disabled] {
|
||||
background: linear-gradient(139deg, #D0D3D9 12%, #C3C7CE 85%);
|
||||
box-shadow: 0rpx 10rpx 32rpx 0rpx rgba(122, 140, 162, 0.3);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.checkin-btn-text {
|
||||
font-size: 40rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.checkin-btn-time {
|
||||
font-size: 32rpx;
|
||||
font-weight: 400;
|
||||
opacity: 0.7;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-family: initial;
|
||||
}
|
||||
|
||||
.checkin-box-tips {
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
margin-top: 60rpx;
|
||||
margin-bottom: 18rpx;
|
||||
}
|
||||
|
||||
.checkin-address-info {
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #999;
|
||||
justify-content: center;
|
||||
|
||||
.iconfont {
|
||||
font-size: 28rpx;
|
||||
margin-right: 4rpx;
|
||||
}
|
||||
|
||||
.checkin-address__refresh {
|
||||
flex-shrink: 0;
|
||||
margin-left: 12rpx;
|
||||
color: #308Bf8;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<view class="order-list-bottom-tabs">
|
||||
<view class="tab-item" v-for="item of tabs" :key="item.index" :class="{ active: item.index === value }"
|
||||
@click="$emit('input', item.index)">
|
||||
<view class="iconfont f-s-40" :class="item.icon"></view>
|
||||
<view class="tab-item-label">{{ item.label }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {ASSIGNED, HOMEPAGE, UNASSIGNED} from "../config";
|
||||
|
||||
export default {
|
||||
name: "BottomTabs",
|
||||
model: {
|
||||
prop: "value",
|
||||
event: "input"
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Number,
|
||||
default: 2
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tabs: [
|
||||
{
|
||||
index: HOMEPAGE,
|
||||
label: "工单",
|
||||
icon: "icon-gongdan-weixuanzhong"
|
||||
},
|
||||
{
|
||||
index: UNASSIGNED,
|
||||
label: "接单",
|
||||
icon: "icon-jiedan-weixuanzhong"
|
||||
},
|
||||
{
|
||||
index: ASSIGNED,
|
||||
label: "我的工单",
|
||||
icon: "icon-dingdan-weixuanzhong"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
//@import "@/static/iconfont/iconfont-next.css";
|
||||
|
||||
.order-list-bottom-tabs {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ffffff;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
display: flex;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100rpx;
|
||||
font-size: 20rpx;
|
||||
line-height: 28rpx;
|
||||
color: #333;
|
||||
|
||||
&.active {
|
||||
color: #2A7EFB;
|
||||
}
|
||||
|
||||
.iconfont-next {
|
||||
font-size: 38rpx;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
text-align: center;
|
||||
line-height: 40rpx;
|
||||
}
|
||||
|
||||
.tab-item-label {
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,380 @@
|
||||
<template>
|
||||
<view>
|
||||
<view v-if="stage === STAGE.directSubmission"></view>
|
||||
<view v-else-if="stage === STAGE.fillTheForm" class="popup-bg">
|
||||
<view class="popup-form-container" :style="{ 'backgroundImage': `url(${urlDomain}crmebimage/presets/checkin-bg.png)` }">
|
||||
<view class="popup-form-wrapper">
|
||||
<view class="popup-form-title">您当前已在服务区域,确定打卡?</view>
|
||||
<view class="popup-form-date">
|
||||
<text class="iconfont icon-ic_clock" />
|
||||
打卡时间
|
||||
{{ time }}
|
||||
</view>
|
||||
<view class="popup-form-location">
|
||||
<text class="iconfont icon-ic_location51" />
|
||||
{{ address }}
|
||||
</view>
|
||||
<view class="popup-form-input__wrapper">
|
||||
<view class="popup-form-input__inner_wrapper">
|
||||
<textarea class="popup-form-input" placeholder="请输入打卡备注" fixed :maxlength="maxLength"
|
||||
placeholder-style="color: #9e9e9e;" v-model="form.clockInRemark" />
|
||||
<view class="popup-form-input__count">
|
||||
<text>{{ form.clockInRemark.length }}/{{ maxLength }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="popup-form-image-list" @click="handleImageListClick">
|
||||
<view class="popup-form-image-item" v-for="(image, index) in form.images" :key="index">
|
||||
<image class="popup-form-image-item__image" mode="aspectFill" :src="image" />
|
||||
<button class="popup-form-image-item__delete" :data-event="EVENT.DELETE_IMAGE" :data-index="index">
|
||||
<text class="iconfont icon-ic_close" :data-event="EVENT.DELETE_IMAGE" :data-index="index" />
|
||||
</button>
|
||||
</view>
|
||||
<view class="popup-form-image-item popup-form-image-item__add"
|
||||
:style="{ 'background': `url(${urlDomain}crmebimage/presets/checkin-form-camera.png) no-repeat center / 52rpx` }"
|
||||
:data-event="EVENT.ADD_IMAGE" v-if="form.images.length < imageCountLimit">
|
||||
<text class="iconfont icon-ic_add" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="popup-form-button-wrapper">
|
||||
<button class="popup-form-button" @click="handleCheckinCancel">取消</button>
|
||||
<button class="popup-form-button confirm" @click="handleCheckinConfirm">确认打卡</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="popup-bg popup-bg-success">
|
||||
<view class="popup-result-container">
|
||||
<image :src="`${urlDomain}crmebimage/presets/checkin-succ.png`" v-if="stage === STAGE.success" mode="widthFill"
|
||||
class="popup-result-image" />
|
||||
<image :src="`${urlDomain}crmebimage/presets/checkin-fail.png`" v-else mode="widthFill" class="popup-result-image" />
|
||||
|
||||
<view class="popup-result-title">
|
||||
{{
|
||||
stage === STAGE.success ? `打卡成功 ${form.checkInTime}` : "打卡失败"
|
||||
}}
|
||||
</view>
|
||||
<view class="popup-result-description">
|
||||
{{ stage === STAGE.success ? "感谢您的辛苦付出~" : "打卡失败,请再试一次" }}
|
||||
</view>
|
||||
<button class="popup-result-btn" v-if="stage === STAGE.success" @click="handleCheckinSuccess">我知道了</button>
|
||||
<button class="popup-result-btn" v-else @click="handleRetryCheckin">重新打卡</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import dayjs from "@/plugin/dayjs/dayjs.min.js";
|
||||
// import { STAFF_CHECKIN_SUCC_EVENT } from "@/utils/constants";
|
||||
import {checkinStaffOrderApi} from "../workOrder";
|
||||
|
||||
const STAGE = {
|
||||
directSubmission: 0,
|
||||
fillTheForm: 1,
|
||||
success: 2,
|
||||
fail: 3
|
||||
};
|
||||
|
||||
const EVENT = {
|
||||
DELETE_IMAGE: 'delete-image',
|
||||
ADD_IMAGE: 'add-image',
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
time: String,
|
||||
address: String,
|
||||
workOrderNo: String,
|
||||
takePhoto: Boolean //拍照开关 true是开启
|
||||
},
|
||||
data() {
|
||||
const { takePhoto } = this;
|
||||
return {
|
||||
urlDomain: this.$Cache.get("imgHost"),
|
||||
STAGE,
|
||||
EVENT,
|
||||
maxLength: 100,
|
||||
stage: takePhoto ? STAGE.fillTheForm : STAGE.directSubmission,
|
||||
|
||||
imageCountLimit: 4,
|
||||
form: {
|
||||
checkInTime: this.time,
|
||||
clockInRemark: "",
|
||||
images: []
|
||||
}
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
if (!this.takePhoto) {
|
||||
const [err, { confirm }] = await uni.showModal({
|
||||
title: "提示",
|
||||
content: `确定要打卡吗?`,
|
||||
});
|
||||
if (err || !confirm) {
|
||||
this.handleCheckinCancel();
|
||||
} else {
|
||||
this.handleCheckinConfirm();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleCheckinCancel() {
|
||||
this.$emit("cancel");
|
||||
},
|
||||
async handleCheckinConfirm() {
|
||||
const { clockInRemark, images } = this.form;
|
||||
if( !images.length && this.stage === STAGE.fillTheForm ) return this.$util.Tips({ title: '请上传打卡照片', icon: "none" });
|
||||
uni.showLoading({
|
||||
mask: true
|
||||
});
|
||||
try {
|
||||
await checkinStaffOrderApi({
|
||||
workOrderNo: this.workOrderNo,
|
||||
address: this.address,
|
||||
clockInRemark,
|
||||
clockInPhoto: images.length ? images.join(',') : ''
|
||||
});
|
||||
uni.hideLoading();
|
||||
const checkinTime = dayjs().format("HH:mm:ss");
|
||||
this.form.checkInTime = checkinTime;
|
||||
this.stage = STAGE.success;
|
||||
// uni.$emit(STAFF_CHECKIN_SUCC_EVENT, this.orderId);
|
||||
} catch (err) {
|
||||
uni.hideLoading();
|
||||
this.stage = STAGE.fail;
|
||||
this.$util.Tips({
|
||||
title: err,
|
||||
icon: "none"
|
||||
});
|
||||
}
|
||||
},
|
||||
handleUploadImage() {
|
||||
this.$util.uploadImageOne({
|
||||
url: 'upload/image',
|
||||
name: 'multipart',
|
||||
model: "staff",
|
||||
pid: 1
|
||||
}, (res)=> {
|
||||
this.form.images.push(res);
|
||||
});
|
||||
},
|
||||
handleImageListClick(e) {
|
||||
const { event, index } = e.target.dataset;
|
||||
if (event === undefined) return;
|
||||
|
||||
switch (event) {
|
||||
case EVENT.DELETE_IMAGE:
|
||||
this.form.images.splice(index, 1);
|
||||
break;
|
||||
case EVENT.ADD_IMAGE:
|
||||
this.handleUploadImage();
|
||||
break;
|
||||
}
|
||||
},
|
||||
handleCheckinSuccess() {
|
||||
uni.navigateBack(-1);
|
||||
// uni.redirectTo({
|
||||
// url: `/pages/admin/workOrder_manage/workOrder_detail?workOrderNo=${this.workOrderNo}`
|
||||
// });
|
||||
},
|
||||
handleRetryCheckin() {
|
||||
this.stage = STAGE.fillTheForm;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.popup-bg {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.popup-bg-success {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.popup-form-container {
|
||||
width: 654rpx;
|
||||
border-radius: 12px;
|
||||
background: none no-repeat left top / 100% auto #fff;
|
||||
overflow: hidden;
|
||||
|
||||
|
||||
.popup-form-wrapper {
|
||||
padding: 40rpx 27rpx 40rpx;
|
||||
|
||||
.popup-form-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
margin-bottom: 14rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.popup-form-date,
|
||||
.popup-form-location {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 24rpx;
|
||||
|
||||
.iconfont {
|
||||
font-size: 28rpx;
|
||||
margin-right: 5rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.popup-form-date {
|
||||
color: #2291F8;
|
||||
}
|
||||
|
||||
.popup-form-location {
|
||||
margin-top: 12rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.popup-form-input__wrapper {
|
||||
margin-top: 28rpx;
|
||||
background-color: #F9F9F9;
|
||||
border-radius: 8px;
|
||||
padding: 28rpx 20rpx 20rpx;
|
||||
}
|
||||
|
||||
.popup-form-input__inner_wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.popup-form-input {
|
||||
font-size: 14px;
|
||||
line-height: 40rpx;
|
||||
height: 200rpx;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.popup-form-input__count {
|
||||
text-align: right;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.popup-form-image-list {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
--gap: 20rpx;
|
||||
--row-count: 4;
|
||||
gap: var(--gap);
|
||||
font-size: 0;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
|
||||
.popup-form-image-item {
|
||||
--size: calc((100% - var(--gap) * (var(--row-count) - 1)) / var(--row-count));
|
||||
width: var(--size);
|
||||
aspect-ratio: 1 / 1;
|
||||
border-radius: 6px;
|
||||
background-color: #F9F9F9;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.popup-form-image-item__image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.popup-form-image-item__delete {
|
||||
position: absolute;
|
||||
top: -16rpx;
|
||||
right: -12rpx;
|
||||
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
background-color: #ccc;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.iconfont {
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.popup-form-image-item__add {
|
||||
border: 1rpx solid #ddd;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.popup-form-button-wrapper {
|
||||
border-top: 1rpx solid #eee;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.popup-form-button {
|
||||
flex: 1;
|
||||
height: 90rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 30rpx;
|
||||
color: #666;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.popup-form-button.confirm {
|
||||
color: #2291F8;
|
||||
border-left: 1rpx solid #eee;
|
||||
}
|
||||
}
|
||||
|
||||
.popup-result-container {
|
||||
border-radius: 12px;
|
||||
background: #FFFFFF;
|
||||
width: 560rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 70rpx 0 86rpx;
|
||||
|
||||
.popup-result-image {
|
||||
width: 350rpx;
|
||||
height: 222rpx;
|
||||
margin-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.popup-result-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.popup-result-description {
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
margin: 16rpx 0 68rpx;
|
||||
}
|
||||
|
||||
.popup-result-btn {
|
||||
width: 340rpx;
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50rpx;
|
||||
background: #2291F8;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<view class="borderPad mt-28 mb20">
|
||||
<view class="borRadius14 bg--w111-fff px-24 py-32 color28 f-s-30 f-w-500 relative">
|
||||
<view class="mb-32">工单管理</view>
|
||||
<view class="borRadius14 word_order flex-between-center">
|
||||
<view class="list text-center">
|
||||
<view class="flex-center mb-20">
|
||||
<view class="w-36 h-36 rd-4rpx"><text class="iconfont icon-ic-gift3"></text></view>
|
||||
<view class="title">待服务工单</view>
|
||||
</view>
|
||||
<view class="f-s-40 semiBold">{{serviceStaffInfo.awaitWorkNum}}</view>
|
||||
</view>
|
||||
<view class="line"></view>
|
||||
<view class="list text-center">
|
||||
<view class="flex-center mb-20">
|
||||
<view class="w-36 h-36 rd-4rpx"><text class="iconfont icon-ic-gift3"></text></view>
|
||||
<view class="title">已服务工单</view>
|
||||
</view>
|
||||
<view class="f-s-40 semiBold">{{serviceStaffInfo.workedNum}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "statistics",
|
||||
props: {
|
||||
serviceStaffInfo:{
|
||||
type: Object,
|
||||
default: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.word_order{
|
||||
width: 662rpx;
|
||||
height: 182rpx;
|
||||
background: #F9F9F9;
|
||||
.list{
|
||||
width: 330rpx;
|
||||
}
|
||||
.title{
|
||||
font-size: 24rpx;
|
||||
margin-left: 16rpx;
|
||||
color: #999999FF;
|
||||
}
|
||||
.line{
|
||||
width: 2rpx;
|
||||
height: 72rpx;
|
||||
background-color: #D8D8D8FF;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
160
mer_uniapp/pages/admin/workOrder_manage/config.js
Normal file
160
mer_uniapp/pages/admin/workOrder_manage/config.js
Normal file
@@ -0,0 +1,160 @@
|
||||
// 已领取的工单
|
||||
import {checkinStaffOrderApi, workOrderReceiveApi, workOrderServiceEndApi} from "./workOrder";
|
||||
import util from "../../../utils/util";
|
||||
|
||||
export const ASSIGNED = 2;
|
||||
// 待领取工单
|
||||
export const UNASSIGNED = 1;
|
||||
// 工单主页
|
||||
export const HOMEPAGE = 3;
|
||||
|
||||
/**
|
||||
* 工单服务类型
|
||||
*/
|
||||
export const serviceStatusEnum = {
|
||||
Unabsorbed: 1, //未分配
|
||||
WaitingService: 2, //已分配 待服务
|
||||
inService: 3, // 服务中
|
||||
ServiceEnd: 4, //服务结束
|
||||
};
|
||||
|
||||
/**
|
||||
* 工单操作按钮判断
|
||||
* @type {{COMPLETE: string, SERVICE_RECORD: string, SIGN_IN: string, RUSH_ORDER: string, START: string}}
|
||||
*/
|
||||
const BTN_EVENT = {
|
||||
SERVICE_RECORD: "serviceRecord",
|
||||
SIGN_IN: "signIn",
|
||||
COMPLETE: 'complete',
|
||||
RUSH_ORDER: "rush_order",
|
||||
START: "start",
|
||||
}
|
||||
export function workOrderBottomBar(workOrderNoInfo) {
|
||||
const reservationConfig = uni.getStorageSync('reservationConfig'); // 商户预约设置
|
||||
// if (!this.orderInfo || !this.merServiceConfig) return [];
|
||||
const config = [];
|
||||
if( workOrderNoInfo.refundStatus > 0 ) return [];
|
||||
// 上门serviceType === 1,2到店
|
||||
if (workOrderNoInfo.serviceType === 1) {
|
||||
// allocateType 分配类型:0-未分配,1-派单,2-抢单
|
||||
if (workOrderNoInfo.allocateType === 0) {
|
||||
config.push({
|
||||
text: "领取工单",
|
||||
type: "lang",
|
||||
event: BTN_EVENT.RUSH_ORDER
|
||||
});
|
||||
}
|
||||
if (workOrderNoInfo.serviceStatus === serviceStatusEnum.WaitingService) {
|
||||
config.push({
|
||||
text: "上门打卡",
|
||||
type: "lang",
|
||||
event: BTN_EVENT.SIGN_IN
|
||||
});
|
||||
}
|
||||
if (workOrderNoInfo.serviceStatus === serviceStatusEnum.inService && reservationConfig
|
||||
.serviceEvidenceSwitch && !workOrderNoInfo.serviceEvidenceFormId) {
|
||||
config.push({
|
||||
text: "服务留凭",
|
||||
type: "lang",
|
||||
event: BTN_EVENT.SERVICE_RECORD
|
||||
});
|
||||
}
|
||||
}else{
|
||||
if (workOrderNoInfo.serviceStatus === serviceStatusEnum.WaitingService) {
|
||||
config.push({
|
||||
text: "服务开始",
|
||||
type: "lang",
|
||||
event: BTN_EVENT.START
|
||||
});
|
||||
}
|
||||
}
|
||||
if (workOrderNoInfo.serviceStatus === serviceStatusEnum.inService && ((workOrderNoInfo.serviceEvidenceFormId >0 || !reservationConfig.serviceEvidenceSwitch) || workOrderNoInfo.serviceType === 2)) {
|
||||
config.push({
|
||||
text: "服务完成",
|
||||
type: "lang",
|
||||
event: BTN_EVENT.COMPLETE
|
||||
});
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工单按钮操作
|
||||
* @param event 操作名称
|
||||
* @param workOrderNo 工单号
|
||||
* @returns {Promise<unknown>}
|
||||
*/
|
||||
export async function handleWorkOrderBarAction(event, workOrderNo) {
|
||||
switch (event) {
|
||||
case BTN_EVENT.SERVICE_RECORD:
|
||||
uni.navigateTo({
|
||||
url: `/pages/goods/service_record/index?workOrderNo=${workOrderNo}`
|
||||
});
|
||||
break;
|
||||
case BTN_EVENT.SIGN_IN:
|
||||
util.navigateTo(`/pages/admin/workOrder_manage/checkin?workOrderNo=${workOrderNo}`)
|
||||
break;
|
||||
case BTN_EVENT.COMPLETE:
|
||||
return new Promise(async (resolve) => {
|
||||
const result = await uni.showModal({
|
||||
content: "您确定要完成服务吗?",
|
||||
});
|
||||
if (result[0] || result[1].cancel) return;
|
||||
let data = await workOrderServiceEndApi(workOrderNo)
|
||||
if (data.code === 200) {
|
||||
util.Tips({
|
||||
title: '服务完成成功'
|
||||
});
|
||||
await resolve(BTN_EVENT.COMPLETE)
|
||||
}
|
||||
});
|
||||
break;
|
||||
case BTN_EVENT.RUSH_ORDER:
|
||||
return new Promise(async (resolve) => {
|
||||
const result = await uni.showModal({
|
||||
content: "您确定要领取此工单服务吗?",
|
||||
});
|
||||
if (result[0] || result[1].cancel) return;
|
||||
try {
|
||||
await workOrderReceiveApi(workOrderNo);
|
||||
util.Tips({
|
||||
title: '领取成功',
|
||||
icon: "success"
|
||||
});
|
||||
await resolve(BTN_EVENT.RUSH_ORDER)
|
||||
// uni.navigateTo({
|
||||
// url: `/pages/admin/workOrder_manage/workOrder_detail?workOrderNo=${workOrderNo}`
|
||||
// });
|
||||
} catch (err) {
|
||||
util.Tips({
|
||||
title: err,
|
||||
icon: "none"
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
case BTN_EVENT.START:
|
||||
return new Promise(async (resolve) => {
|
||||
const result = await uni.showModal({
|
||||
content: "您确定要开始服务吗?",
|
||||
});
|
||||
if (result[0] || result[1].cancel) return;
|
||||
try {
|
||||
await checkinStaffOrderApi({workOrderNo: workOrderNo});
|
||||
uni.hideLoading();
|
||||
util.Tips({
|
||||
title: '服务开始',
|
||||
icon: "none"
|
||||
});
|
||||
await resolve(BTN_EVENT.START)
|
||||
} catch (err) {
|
||||
uni.hideLoading();
|
||||
util.Tips({
|
||||
title: err,
|
||||
icon: "none"
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
189
mer_uniapp/pages/admin/workOrder_manage/index.vue
Normal file
189
mer_uniapp/pages/admin/workOrder_manage/index.vue
Normal file
@@ -0,0 +1,189 @@
|
||||
<template>
|
||||
<view :data-theme="theme">
|
||||
<nav-bar :isScrolling="isScrolling" :iconColor='iconColor' :isShowMenu="false" :isBackgroundColor="false" ref="navBarRef" :navTitle="serviceStaffInfo.merName" goBack="/pages/user/index">
|
||||
</nav-bar>
|
||||
<TopHeaderfixed></TopHeaderfixed>
|
||||
<view class="merchant-info relative borderPad mt-24">
|
||||
<view class="merchant-info-wrapper">
|
||||
<image :src="serviceStaffInfo.idPhoto" class="merchant-user-avatar" />
|
||||
<view class="merchant-user-name overflow-text">{{ serviceStaffInfo.name }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<Statistics :serviceStaffInfo="serviceStaffInfo"></Statistics>
|
||||
<view class="relative"> <OrderList :orderList="orderList" :orderType="orderType" /></view>
|
||||
<view v-if="!loadOptions.loading">
|
||||
<view class="order-list-empty text-center py-20 f-s-26 text--w111-ccc" v-if="loadOptions.loaded && orderList.length">
|
||||
暂无更多~
|
||||
</view>
|
||||
<view v-else-if="orderList.length === 0" class="nothing">
|
||||
<emptyPage title="暂无订单~" mTop="14%" :imgSrc="urlDomain+'crmebimage/presets/nodingdan.png'" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="list-bottom-tab-placeholder"></view>
|
||||
<BottomTabs v-model="orderType"/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NavBar from "@/components/navBar.vue"
|
||||
import {reservationConfigApi, staffInfoApi, staffLoginApi, workOrderAwaitListApi, workOrderListApi} from "./workOrder";
|
||||
import Cache from "@/utils/cache";
|
||||
import BottomTabs from "./components/BottomTabs";
|
||||
import { HOMEPAGE} from "./config";
|
||||
import OrderList from "../components/OrderList";
|
||||
import emptyPage from "@/components/emptyPage";
|
||||
import Statistics from "./components/Statistics";
|
||||
import TopHeaderfixed from "../../../components/TopHeaderfixed";
|
||||
const app = getApp();
|
||||
export default {
|
||||
components: {
|
||||
NavBar,
|
||||
BottomTabs,
|
||||
OrderList,
|
||||
emptyPage,
|
||||
Statistics,
|
||||
TopHeaderfixed
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
urlDomain: this.$Cache.get("imgHost"),
|
||||
theme: app.globalData.theme,
|
||||
orderType: HOMEPAGE,
|
||||
serviceStaffInfo: {},
|
||||
loadOptions: {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
loading: false,
|
||||
loaded: false,
|
||||
},
|
||||
orderList: [],
|
||||
iconColor: '#FFFFFF',
|
||||
isScrolling: false,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 底部tab选项卡
|
||||
orderType: {
|
||||
handler(newValue) {
|
||||
if (newValue === HOMEPAGE){
|
||||
uni.redirectTo({
|
||||
url: `/pages/admin/workOrder_manage/index`
|
||||
})
|
||||
}else{
|
||||
uni.redirectTo({
|
||||
url: `/pages/admin/workOrder_manage/workOrder_list?orderType=${newValue}`
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
async onPullDownRefresh() {
|
||||
await this.handleForceRefetch();
|
||||
uni.stopPullDownRefresh();
|
||||
},
|
||||
onPageScroll(e) {
|
||||
if (e.scrollTop > 50) {
|
||||
this.iconColor = '#333333';
|
||||
this.isScrolling = true;
|
||||
} else {
|
||||
this.iconColor = '#FFFFFF';
|
||||
this.isScrolling = false;
|
||||
}
|
||||
this.isScroll = e.scrollTop > 0;
|
||||
},
|
||||
onReachBottom() {
|
||||
const { loading, loaded } = this.loadOptions;
|
||||
if (loading || loaded) return;
|
||||
this.loadOptions.page++;
|
||||
this.handleGetOrderList();
|
||||
},
|
||||
onLoad() {
|
||||
if(!Cache.get('workOrderToken')){
|
||||
this.handleLogin();
|
||||
}else{
|
||||
this.getStaffInfo()
|
||||
this.getReservationConfig()
|
||||
this.handleGetOrderList()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleForceRefetch() {
|
||||
this.orderList = [];
|
||||
return this.handleGetOrderList();
|
||||
},
|
||||
async handleLogin(){
|
||||
try {
|
||||
const res = await staffLoginApi()
|
||||
this.$store.commit('SET_WORK_ORDER_TOKEN', res.data.token);
|
||||
await this.getReservationConfig()
|
||||
await this.getStaffInfo()
|
||||
await this.handleGetOrderList()
|
||||
} catch (err) {}
|
||||
},
|
||||
async getReservationConfig(){
|
||||
const { data } = await reservationConfigApi()
|
||||
uni.setStorageSync('reservationConfig', data);
|
||||
},
|
||||
// 用户信息
|
||||
async getStaffInfo(){
|
||||
const { data } = await staffInfoApi()
|
||||
this.serviceStaffInfo = data
|
||||
},
|
||||
// 列表
|
||||
async handleGetOrderList() {
|
||||
const { loading, loaded, page, pageSize } = this.loadOptions;
|
||||
if (loading || loaded) return;
|
||||
this.loadOptions.loading = true;
|
||||
try {
|
||||
const res = await workOrderListApi({
|
||||
status : 3,
|
||||
page,
|
||||
limit: pageSize
|
||||
})
|
||||
this.orderList.push(...res.data.list);
|
||||
this.loadOptions.total = res.data.total;
|
||||
this.loadOptions.loaded = this.orderList.length >= this.loadOptions.total;
|
||||
} catch (err) {
|
||||
this.$util.Tips({
|
||||
title: err,
|
||||
icon: "none"
|
||||
});
|
||||
}
|
||||
this.loadOptions.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.business-bg {
|
||||
height: calc(var(--nav-bar-height) + 290rpx);
|
||||
background: linear-gradient(180deg, #2291F8 0%, #2291F8 var(--nav-bar-height), rgba(34, 145, 248, 0) 100%);
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
.merchant-info {
|
||||
|
||||
&-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.merchant-user-avatar {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.merchant-user-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
max-width: 60 0rpx;
|
||||
margin: 0 16rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
111
mer_uniapp/pages/admin/workOrder_manage/workOrder.js
Normal file
111
mer_uniapp/pages/admin/workOrder_manage/workOrder.js
Normal file
@@ -0,0 +1,111 @@
|
||||
// +----------------------------------------------------------------------
|
||||
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: CRMEB Team <admin@crmeb.com>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
|
||||
import request from "@/utils/request.js";
|
||||
|
||||
/**
|
||||
* 服务员工登录
|
||||
*/
|
||||
export function staffLoginApi() {
|
||||
return request.post(`staff/login/index`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 待领取工单分页列表
|
||||
*/
|
||||
export function workOrderAwaitListApi(data) {
|
||||
return request.get(`staff/work/order/await/receive/page`,data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的工单分页列表
|
||||
* @data
|
||||
*/
|
||||
export function workOrderListApi(data) {
|
||||
return request.get(`staff/work/order/page`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 商户预约设置信息
|
||||
*/
|
||||
export function reservationConfigApi() {
|
||||
return request.get(`staff/merchant/reservation/config`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工单打卡页信息
|
||||
*/
|
||||
export function clockInfoApi(workOrderNo) {
|
||||
return request.get(`staff/work/order/clock/in/page/info/${workOrderNo}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 工单打卡
|
||||
*/
|
||||
export function checkinStaffOrderApi(data) {
|
||||
return request.post(`staff/work/order/clock/in`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过坐标获取地址
|
||||
*/
|
||||
export function getCoordinateAddressApi(data) {
|
||||
return request.get(`staff/address/get/coordinate/address`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 工单详情
|
||||
*/
|
||||
export function getWorkOrderInfoApi(workOrderNo) {
|
||||
return request.get(`staff/work/order/info/${workOrderNo}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 工单备注
|
||||
*/
|
||||
export function workOrderRemarkApi(data) {
|
||||
return request.post(`staff/work/order/remark`,data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 工单服务结束
|
||||
*/
|
||||
export function workOrderServiceEndApi(workOrderNo) {
|
||||
return request.post(`staff/work/order/service/end/${workOrderNo}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取工单
|
||||
*/
|
||||
export function workOrderReceiveApi(workOrderNo) {
|
||||
return request.post(`staff/work/order/receive/${workOrderNo}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 商户预约服务留凭表单信息
|
||||
*/
|
||||
export function serviceFormInfoApi() {
|
||||
return request.get(`staff/merchant/reservation/service/evidence/form/info`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 工单服务过程留凭
|
||||
*/
|
||||
export function serviceEvidenceApi(data) {
|
||||
return request.post(`staff/work/order/service/evidence`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取登录员工信息
|
||||
*/
|
||||
export function staffInfoApi() {
|
||||
return request.get(`staff/login/staff/info`);
|
||||
}
|
||||
201
mer_uniapp/pages/admin/workOrder_manage/workOrder_detail.vue
Normal file
201
mer_uniapp/pages/admin/workOrder_manage/workOrder_detail.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- #ifdef MP || APP-PLUS -->
|
||||
<NavBar navTitle="工单详情" :iconColor="iconColor" :isShowMenu="false" :isBackgroundColor="false"
|
||||
:textColor="iconColor" :isScrolling="isScrolling" showBack>
|
||||
</NavBar>
|
||||
<!-- #endif -->
|
||||
<!-- 状态、备注 -->
|
||||
<OrderHeader :info="workOrderNoInfo" :remark="workOrderNoInfo.remark" @changeRemark="handleRemark('1')" title="工单"></OrderHeader>
|
||||
|
||||
<view class="relative borderPad mt20" :class="btnConfig.length?'pb-80':'mb-20'">
|
||||
<!-- 地址信息 -->
|
||||
<OrderAddress v-if="workOrderNoInfo.serviceType === 1" :orderInfo="workOrderNoInfo"></OrderAddress>
|
||||
<!-- 商品信息、用户信息 -->
|
||||
<view class="borderPad bg--w111-fff borRadius14 mt20 pb-32">
|
||||
<OrderGoods :cartInfo="[workOrderNoInfo]" :orderInfo="workOrderNoInfo" :isShowBtn="false"></OrderGoods>
|
||||
<OrderTable :list="tableData" mini-gap :has-style="false"></OrderTable>
|
||||
</view>
|
||||
<!-- 系统表单信息 -->
|
||||
<view v-if="reservationFormData.length" class="bg--w111-fff borRadius14 mt20 pt-32">
|
||||
<view class="borderPad order-detail-table f-s-30 f-w-500" style="margin-bottom: -20rpx;">表单信息</view>
|
||||
<systemFromInfo :orderExtend="reservationFormData"></systemFromInfo>
|
||||
</view>
|
||||
<!-- 服务信息、打卡信息 -->
|
||||
<OrderTable :list="item.list" :title="item.title" v-for="(item, index) in tableList" :key="index">
|
||||
</OrderTable>
|
||||
<!-- 服务过程留凭信息 -->
|
||||
<view v-if="serviceEvidenceForm.length" class="bg--w111-fff borRadius14 mt20 pt-32 pb-32">
|
||||
<view class="borderPad order-detail-table f-s-30 f-w-500">服务过程留凭</view>
|
||||
<systemFromInfo :orderExtend="serviceEvidenceForm"></systemFromInfo>
|
||||
</view>
|
||||
|
||||
<view class="list-bottom-tab-placeholder"></view>
|
||||
</view>
|
||||
<OrderBottomBar v-if="btnConfig.length" :config="btnConfig" @action="handleBottomBarAction"></OrderBottomBar>
|
||||
<PriceChange :change="change" :orderInfo="workOrderNoInfo" v-on:closechange="changeClose($event)"
|
||||
v-on:savePrice="getRemark" :status="status" title="工单" :remark="workOrderNoInfo.remark"></PriceChange>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// #ifdef MP || APP-PLUS
|
||||
import NavBar from "@/components/navBar.vue";
|
||||
// #endif
|
||||
import PriceChange from "../components/PriceChange";
|
||||
import OrderHeader from "../components/OrderDetail/OrderHeader.vue";
|
||||
import OrderTable from "../components/OrderDetail/OrderTable.vue";
|
||||
import systemFromInfo from '@/components/systemFromInfo';
|
||||
import OrderGoods from '@/components/orderGoods'
|
||||
import {
|
||||
getWorkOrderInfoApi,
|
||||
workOrderRemarkApi
|
||||
} from "./workOrder";
|
||||
import OrderAddress from "../components/OrderAddress";
|
||||
import {
|
||||
getTableList
|
||||
} from "../order";
|
||||
import {
|
||||
handleWorkOrderBarAction,
|
||||
workOrderBottomBar
|
||||
} from "./config";
|
||||
import OrderBottomBar from "../components/OrderBottomBar";
|
||||
|
||||
const BTN_EVENT = {
|
||||
SERVICE_RECORD: "serviceRecord",
|
||||
SIGN_IN: "signIn",
|
||||
COMPLETE: 'complete'
|
||||
}
|
||||
export default {
|
||||
name: "workOrder_detail",
|
||||
components: {
|
||||
OrderBottomBar,
|
||||
OrderHeader,
|
||||
PriceChange,
|
||||
systemFromInfo,
|
||||
OrderAddress,
|
||||
OrderGoods,
|
||||
OrderTable,
|
||||
// #ifdef MP || APP-PLUS
|
||||
NavBar,
|
||||
// #endif
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
iconColor: '#FFFFFF',
|
||||
isScrolling: false,
|
||||
workOrderNo: '',
|
||||
workOrderNoInfo: {},
|
||||
status: '',
|
||||
change: false,
|
||||
tableData: [],
|
||||
reservationFormData: [], //系统表单数据
|
||||
serviceEvidenceForm: [] //服务留凭
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
tableList() {
|
||||
return getTableList(this.workOrderNoInfo);
|
||||
},
|
||||
// 底部操作按钮
|
||||
btnConfig() {
|
||||
return workOrderBottomBar(this.workOrderNoInfo)
|
||||
},
|
||||
},
|
||||
onShow(){
|
||||
this.handleGetWorkOrderDetail();
|
||||
},
|
||||
onPageScroll(e) {
|
||||
// #ifdef MP || APP-PLUS
|
||||
if (e.scrollTop > 50) {
|
||||
this.iconColor = '#333333';
|
||||
this.isScrolling = true;
|
||||
} else {
|
||||
this.iconColor = '#FFFFFF';
|
||||
this.isScrolling = false;
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
onLoad(options) {
|
||||
this.workOrderNo = options.workOrderNo;
|
||||
},
|
||||
methods: {
|
||||
// 工单详情
|
||||
async handleGetWorkOrderDetail() {
|
||||
uni.showLoading({
|
||||
title: '加载中...'
|
||||
});
|
||||
const {
|
||||
data
|
||||
} = await getWorkOrderInfoApi(this.workOrderNo);
|
||||
this.workOrderNoInfo = data;
|
||||
this.getTableData();
|
||||
this.reservationFormData = data.reservationFormData ? [JSON.parse(data.reservationFormData)] :
|
||||
[] // 表单信息
|
||||
this.serviceEvidenceForm = data.serviceEvidenceForm ? [JSON.parse(data.serviceEvidenceForm)] :
|
||||
[] // 服务留凭
|
||||
uni.hideLoading();
|
||||
},
|
||||
// 联系人信息
|
||||
getTableData() {
|
||||
this.tableData = [{
|
||||
label: "订单号",
|
||||
value: this.workOrderNoInfo.orderNo,
|
||||
copy: true,
|
||||
},
|
||||
{
|
||||
label: "留言",
|
||||
value: this.workOrderNoInfo.userRemark,
|
||||
overflow: true,
|
||||
}
|
||||
]
|
||||
},
|
||||
// 操作按钮回调
|
||||
async handleBottomBarAction(event) {
|
||||
let data = await handleWorkOrderBarAction(event, this.workOrderNo)
|
||||
// 服务完成 / 服务开始
|
||||
if (data === 'complete' || data === 'start' || 'rush_order') await this.handleGetWorkOrderDetail()
|
||||
},
|
||||
// 备注
|
||||
handleRemark: function(status) {
|
||||
this.change = true;
|
||||
this.status = status;
|
||||
},
|
||||
async getRemark(opt) {
|
||||
if (!opt.remark) {
|
||||
return this.$util.Tips({
|
||||
title: '请输入备注'
|
||||
})
|
||||
} else {
|
||||
this.toMark(opt.remark)
|
||||
}
|
||||
},
|
||||
//备注
|
||||
toMark(remark) {
|
||||
workOrderRemarkApi({
|
||||
workOrderNo: this.workOrderNo,
|
||||
remark
|
||||
}).then(res => {
|
||||
res.code == 200 && (this.change = false);
|
||||
this.handleGetWorkOrderDetail()
|
||||
return this.$util.Tips({
|
||||
title: '备注成功'
|
||||
})
|
||||
})
|
||||
},
|
||||
changeClose: function(msg) {
|
||||
this.change = msg;
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pb-80 {
|
||||
padding-bottom: 80rpx;
|
||||
}
|
||||
|
||||
.order-detail-table {
|
||||
color: #333333FF;
|
||||
}
|
||||
</style>
|
||||
332
mer_uniapp/pages/admin/workOrder_manage/workOrder_list.vue
Normal file
332
mer_uniapp/pages/admin/workOrder_manage/workOrder_list.vue
Normal file
@@ -0,0 +1,332 @@
|
||||
<template>
|
||||
<view :data-theme="theme">
|
||||
<!-- #ifdef MP || APP-PLUS -->
|
||||
<NavBar titleText="工单管理" bagColor="#f5f5f5" iconColor='#333333' textColor="#333333" :isScrolling="isScrolling" showBack goBack="pages/user/index"></NavBar>
|
||||
<!-- #endif -->
|
||||
<view class="order-top-bar">
|
||||
<view class="order-top-bar-content">
|
||||
<SearchBar @confirm="handleSearch" placeholder="请输入工单号/订单号/商品名称">
|
||||
<view v-show="orderType === ASSIGNED" class="calendar-btn" :style="{ 'background-image': `url(${urlDomain}crmebimage/presets/ic_calendar.png)` }">
|
||||
<uni-datetime-picker ref="daterange" type="daterange" @change="handleChangeDateRange">
|
||||
<view class="daterange-placeholder"></view>
|
||||
<template #header>
|
||||
<button hover-class="none" class="calendar-clear-btn" @click="handleClearDateRange">清空</button>
|
||||
</template>
|
||||
</uni-datetime-picker>
|
||||
</view>
|
||||
</SearchBar>
|
||||
</view>
|
||||
<view class="order-category-bg" v-if="orderType === ASSIGNED">
|
||||
<OrderCategory v-model="orderStatus" :orderType="orderType"/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="order-list-container" :class="{ 'is-assign-order': orderType === UNASSIGNED }">
|
||||
<OrderList :orderList="orderList" :orderType="orderType" />
|
||||
</view>
|
||||
<template v-if="!loadOptions.loading">
|
||||
<view class="order-list-empty" v-if="loadOptions.loaded && orderList.length">
|
||||
暂无更多~
|
||||
</view>
|
||||
<view v-else-if="orderList.length === 0" class="nothing">
|
||||
<emptyPage title="暂无订单~" mTop="25%" :imgSrc="urlDomain+'crmebimage/presets/nodingdan.png'" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view class="list-bottom-tab-placeholder"></view>
|
||||
<BottomTabs v-model="orderType" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// #ifdef MP || APP-PLUS
|
||||
import NavBar from '../components/NavBar.vue';
|
||||
// #endif
|
||||
import BottomTabs from "./components/BottomTabs";
|
||||
import {workOrderAwaitListApi, workOrderListApi} from "./workOrder";
|
||||
import {ASSIGNED, HOMEPAGE, serviceStatusEnum, UNASSIGNED} from "./config";
|
||||
import OrderCategory from "../components/OrderCategory";
|
||||
import emptyPage from "@/components/emptyPage";
|
||||
import OrderList from "../components/OrderList";
|
||||
import SearchBar from "../components/SearchBar";
|
||||
const app = getApp();
|
||||
export default {
|
||||
name: "workOrder_list",
|
||||
components: {
|
||||
SearchBar,
|
||||
// #ifdef MP || APP-PLUS
|
||||
NavBar,
|
||||
// #endif
|
||||
BottomTabs,
|
||||
OrderCategory,
|
||||
emptyPage,
|
||||
OrderList
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ASSIGNED,
|
||||
UNASSIGNED,
|
||||
serviceStatusEnum,
|
||||
urlDomain: this.$Cache.get("imgHost"),
|
||||
theme: app.globalData.theme,
|
||||
isScrolling: false,
|
||||
orderType: null,
|
||||
isScroll: false,
|
||||
orderStatus: 0,
|
||||
loadOptions: {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
loading: false,
|
||||
loaded: false,
|
||||
},
|
||||
orderList: [],
|
||||
dateLimit: '',
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
searchText: ''
|
||||
}
|
||||
},
|
||||
async onPullDownRefresh() {
|
||||
await this.handleForceRefetch();
|
||||
uni.stopPullDownRefresh();
|
||||
},
|
||||
onPageScroll(e) {
|
||||
this.isScroll = e.scrollTop > 0;
|
||||
},
|
||||
onReachBottom() {
|
||||
const { loading, loaded } = this.loadOptions;
|
||||
if (loading || loaded) return;
|
||||
this.loadOptions.page++;
|
||||
this.handleGetOrderList();
|
||||
},
|
||||
onLoad(options) {
|
||||
this.orderType = Number(options.orderType);
|
||||
},
|
||||
onUnload() {
|
||||
},
|
||||
onShow(){
|
||||
this.orderList = [];
|
||||
this.handleForceRefetch();
|
||||
},
|
||||
computed: {
|
||||
// 搜索条件
|
||||
queryParams() {
|
||||
const params = {
|
||||
keywords: this.searchText,
|
||||
status : this.orderStatus
|
||||
};
|
||||
|
||||
if (this.orderType === 1) {
|
||||
delete params.status
|
||||
}
|
||||
|
||||
if (this.dateLimit) {
|
||||
params.dateLimit = this.dateLimit
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 底部tab选项卡
|
||||
orderType: {
|
||||
handler(newValue, oldValue) {
|
||||
if (newValue === HOMEPAGE){
|
||||
uni.redirectTo({
|
||||
url: `/pages/admin/workOrder_manage/index`
|
||||
})
|
||||
}
|
||||
if(oldValue) this.handleForceRefetch()
|
||||
}
|
||||
},
|
||||
// 底部tab选项卡
|
||||
orderStatus() {
|
||||
this.handleForceRefetch()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleStaffCheckinSucc(orderId) {
|
||||
if (this.orderType === ASSIGNED) {
|
||||
const order = this.orderList.find(item => item.order_id === Number(orderId));
|
||||
if (order) {
|
||||
// 更新打卡后的订单状态为已上门打卡,待服务
|
||||
order.status = 20;
|
||||
}
|
||||
}
|
||||
},
|
||||
// 选择时间
|
||||
handleChangeDateRange(e) {
|
||||
const [before, after] = e;
|
||||
this.startDate = before;
|
||||
this.endDate = after;
|
||||
this.dateLimit = e.join(",")
|
||||
this.orderList = [];
|
||||
this.handleForceRefetch();
|
||||
},
|
||||
handleClearDateRange() {
|
||||
this.$refs.daterange.close();
|
||||
this.$refs.daterange.clear();
|
||||
this.startDate = "";
|
||||
this.endDate = "";
|
||||
this.handleForceRefetch()
|
||||
},
|
||||
handleForceRefetch() {
|
||||
this.handleResetLoadOptions();
|
||||
this.orderList = [];
|
||||
this.handleGetOrderList();
|
||||
},
|
||||
handleResetLoadOptions() {
|
||||
this.loadOptions.page = 1;
|
||||
this.loadOptions.total = 0;
|
||||
this.loadOptions.loaded = false;
|
||||
this.loadOptions.loading = false;
|
||||
},
|
||||
handleSearch(searchText) {
|
||||
this.searchText = searchText;
|
||||
this.handleForceRefetch();
|
||||
},
|
||||
// 列表
|
||||
async handleGetOrderList() {
|
||||
const { loading, loaded, page, pageSize } = this.loadOptions;
|
||||
if (loading || loaded) return;
|
||||
this.loadOptions.loading = true;
|
||||
try {
|
||||
const res = this.orderType === ASSIGNED ? await workOrderListApi({
|
||||
...this.queryParams,
|
||||
page,
|
||||
limit: pageSize
|
||||
}) : await workOrderAwaitListApi({
|
||||
...this.queryParams,
|
||||
page,
|
||||
limit: pageSize
|
||||
});
|
||||
// res.data.list.forEach(item => {
|
||||
// addBookingOrderType(item);
|
||||
// });
|
||||
this.orderList.push(...res.data.list);
|
||||
this.loadOptions.total = res.data.total;
|
||||
this.loadOptions.loaded = this.orderList.length >= this.loadOptions.total;
|
||||
} catch (err) {
|
||||
this.$util.Tips({
|
||||
title: err,
|
||||
icon: "none"
|
||||
});
|
||||
}
|
||||
this.loadOptions.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$bg-height: calc(238rpx + var(--safe-area-inset-top));
|
||||
.order-top-bar-content{
|
||||
padding: 20rpx 24rpx;
|
||||
}
|
||||
.body-bg1 {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: $bg-height;
|
||||
background-image: linear-gradient(90deg, #2291F8 0%, #1CD1DC 100%);
|
||||
}
|
||||
|
||||
.body-bg2 {
|
||||
position: absolute;
|
||||
bottom: -2rpx;
|
||||
left: 0;
|
||||
height: 48rpx;
|
||||
width: 100%;
|
||||
background: linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, #f5f5f5 100%);
|
||||
|
||||
&.unassign-order-and-scroll {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.order-category-bg {
|
||||
position: relative;
|
||||
|
||||
&::after,
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 48rpx;
|
||||
}
|
||||
|
||||
&::before {
|
||||
// background-image: linear-gradient(90deg, #2291F8 0%, #1CD1DC 100%);
|
||||
}
|
||||
|
||||
&::after {
|
||||
height: 50rpx;
|
||||
background: linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, #f5f5f5 100%);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.order-top-bar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background-color: #f5f5f5;
|
||||
//min-height: $bg-height;
|
||||
}
|
||||
|
||||
.list-bottom-tab-placeholder {
|
||||
height: calc(var(--safe-area-inset-bottom) + 100rpx);
|
||||
}
|
||||
|
||||
.order-list-empty {
|
||||
text-align: center;
|
||||
margin: 20rpx 0 70rpx;
|
||||
color: #CCCCCC;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.nothing {
|
||||
margin-top: 0;
|
||||
|
||||
/deep/ .empty-box {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-btn {
|
||||
height: var(--content-height);
|
||||
aspect-ratio: 1;
|
||||
margin-left: 16rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
background: none no-repeat center / 36rpx #fff;
|
||||
|
||||
.daterange-placeholder {
|
||||
height: var(--content-height);
|
||||
}
|
||||
|
||||
.calendar-clear-btn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin: 10px 25px 0;
|
||||
font-size: 26rpx;
|
||||
color: #737987;
|
||||
}
|
||||
}
|
||||
|
||||
.order-list-container {
|
||||
position: relative;
|
||||
|
||||
&.is-assign-order {
|
||||
//margin-top: -40rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user