提交 9bff4700 authored 作者: lidongxu's avatar lidongxu

feat(examine): 稽核任务

同上
上级 9ce234cc
import request from '@/utils/request'
const VITE_APP_PROMOTION = import.meta.env.VITE_APP_PROMOTION
// 获取稽核任务详情
export function getExamineDetail(id) {
return request({
baseURL: VITE_APP_PROMOTION,
url: `/exa/query/${id}`
})
}
// 创建稽核任务
export function createExamine(data) {
return request({
baseURL: VITE_APP_PROMOTION,
url: `/exa/core/save`,
method: 'post',
data: data
})
}
\ No newline at end of file
...@@ -3,6 +3,7 @@ export * from './common/menu' ...@@ -3,6 +3,7 @@ export * from './common/menu'
export * from './common/openQuery' export * from './common/openQuery'
export * from './common/region' export * from './common/region'
export * from './common/upload' export * from './common/upload'
export * from './examine/index'
export * from './bi/competitor' export * from './bi/competitor'
export * from './bi/finance' export * from './bi/finance'
export * from './bi/livecate' export * from './bi/livecate'
......
<template>
<div>
<van-nav-bar left-text="返回"
left-arrow
@click-left="clickBack()"
fixed
placeholder />
<!-- 店铺门头照组 -->
<div class="group-container bg-white">
<!-- <h2 class="group-title">
店铺门头照
<div class="title-divider"></div>
</h2> -->
<div class="form-item">
<label>店铺门头照:</label>
</div>
<div class="form-item">
<van-uploader :max-count="1"
accept="image/*"
v-model="form.storePicture"
:after-read="storePictureRead"
preview-size="120">
</van-uploader>
</div>
</div>
<!-- 现场考核组 -->
<div class="group-container">
<h2 class="group-title">
现场考核
<div class="title-divider"></div>
</h2>
<div class="section">
<div class="form-item"
style="display: flex; align-items: center;">
<label>是否执行:</label>
<van-switch v-model="form.planStatus"
style="transform: scale(0.8);"
@change="planStatusChange" />
</div>
<div class="form-item"
style="display: flex; align-items: center;">
<label>促销员数量:</label>
<div class="custom-counter">
<button @click="decreaseCount">-</button>
<span>{{ form.temNum }}</span>
<button @click="increaseCount">+</button>
</div>
</div>
<div class="form-item">
<label>地堆:</label>
<van-radio-group v-model="form.storeDd"
@change="changeStoreDd">
<van-radio name="是"></van-radio>
<van-radio name="否"></van-radio>
</van-radio-group>
</div>
</div>
</div>
<!-- 促销员考核组 -->
<div class="group-container bg-white">
<h2 class="group-title">
促销员考核
<div class="title-divider"></div>
</h2>
<div class="section">
<div class="form-item">
<label>促销员是否在岗:</label>
<van-radio-group v-model="form.temOnWork"
@change="changeTemOnWork">
<van-radio name="在岗">在岗</van-radio>
<van-radio name="离岗">离岗 <span v-show="form.temOnWork === '离岗'">{{ temOnWorkTimeRange }}</span></van-radio>
</van-radio-group>
</div>
<div class="form-item">
<label>话述是否达标:</label>
<van-radio-group v-model="form.temHs"
@change="changeTemHs">
<van-radio name="达标">达标</van-radio>
<van-radio name="未达标">未达标</van-radio>
</van-radio-group>
</div>
<div class="form-item">
<label>物料是否齐全:</label>
<van-radio-group v-model="form.temWl"
@change="changeTemWl">
<van-radio name="齐全">齐全</van-radio>
<van-radio name="缺少">缺少</van-radio>
</van-radio-group>
</div>
<div class="form-item">
<label>着装是否达标:</label>
<van-radio-group v-model="form.temZz"
@change="changeTemZz">
<van-radio name="达标">达标</van-radio>
<van-radio name="未达标">未达标</van-radio>
</van-radio-group>
</div>
</div>
</div>
<!-- 在/离岗取证组 -->
<div class="group-container">
<h2 class="group-title">
在/离岗取证
<div class="title-divider"></div>
</h2>
<div class="section">
<div class="form-item">
<label>在/离岗取证(最少2张照片):</label>
</div>
<div class="form-item">
<van-uploader :max-count="2"
accept="image/*"
v-model="form.temWorkPhotos"
:after-read="temWorkPhotosRead"
preview-size="120">
</van-uploader>
</div>
<div class="form-item">
<label>特陈照片(1张):</label>
</div>
<div class="form-item">
<van-uploader :max-count="1"
accept="image/*"
v-model="form.storeTcPhoto"
:after-read="storeTcPhotoRead"
preview-size="120">
</van-uploader>
</div>
<div class="form-item">
<label>主货架照片(1张):</label>
</div>
<div class="form-item">
<van-uploader :max-count="1"
accept="image/*"
v-model="form.storeZhjPhoto"
:after-read="temOnWorkPictureRead"
preview-size="120">
</van-uploader>
</div>
<div class="form-item">
<label>POS照片(两张):</label>
</div>
<div class="form-item">
<van-uploader :max-count="2"
accept="image/*"
v-model="form.posPhotos"
:after-read="posPhotosRead"
preview-size="120">
</van-uploader>
</div>
<div class="form-item">
<label>POS金额:</label>
<van-field v-model="form.posRmb"
type="number"
:controls="true"
placeholder="请输入POS金额"
style="margin-top: 10px;"
@input="posRmbChange" />
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router'; // 导入 useRouter
import { getExamineDetail, createExamine, uploadFileToOSSAPI } from '@/api'
import userStore from '@/store/modules/user'
import { v4 as uuidv4 } from 'uuid';
import { parseTime } from '@/utils'
const route = useRoute();
const router = useRouter();
const form = ref({
storePicture: [], // 店铺门头照
temNum: null, // 促销员数量
storeDd: '否', // 是否有地堆
temWorkPhotos: [], // 在/离岗取证照片
storeTcPhoto: [], // 特陈照
storeZhjPhoto: [], // 主货架照
posPhotos: [], // POS 照片
posRmb: null, // POS 金额
})
const planId = ref() // 计划 ID
const storeName = ref('') // 计划-店铺名称
// 现场考核相关
const isExecute = ref(false);
const promoterCount = ref(1);
const isStack = ref('1');
// 促销员考核相关
const isPromoterOnDuty = ref('0');
const onDutyTimeRange = '16:36 - 16:41';
const isSpeechStandard = ref('1');
const isMaterialsComplete = ref('1');
const isDressStandard = ref('0');
const temOnWorkTimeRange = ref('') // 离岗时间
// 右侧部分相关
const posAmount = ref(1);
const clickBack = () => {
router.back()
}
const decreaseCount = async () => {
if (form.value.temNum > 0) {
await createExamine({
id: form.value.id,
temNum: form.value.temNum - 1
})
form.value.temNum--;
}
};
const increaseCount = async () => {
await createExamine({
id: form.value.id,
temNum: form.value.temNum + 1
})
form.value.temNum++;
};
// 获取考核详情/创建稽核任务
const getDetail = async () => {
const res = await getExamineDetail(route.params.examineId)
planId.value = res.data.planId
storeName.value = res.data.storeName
form.value = res.data
form.value.storePicture = res.data.storePicture ? [{
url: res.data.storePicture
}] : []
form.value.planStatus = Boolean(res.data.planStatus * 1)
form.value.temNum = res.data.temNum || 0
form.value.storeDd = res.data.storeDd === null ? '否' : res.data.storeDd
form.value.temOnWork = res.data.temOnWork === null ? '离岗' : res.data.temOnWork
form.value.temHs = res.data.temHs === null ? '未达标' : res.data.temHs
form.value.temWl = res.data.temWl === null ? '缺少' : res.data.temWl
form.value.temZz = res.data.temZz === null ? '未达标' : res.data.temZz
form.value.temWorkPhotos = res.data?.temWorkPhotos ? (res.data.temWorkPhotos.map(o => {
return {
url: o
}
})) : []
form.value.storeTcPhoto = res.data?.storeTcPhoto ? [{
url: res.data.storeTcPhoto
}] : []
form.value.storeZhjPhoto = res.data?.storeZhjPhoto ? [{
url: res.data.storeZhjPhoto
}] : []
form.value.posPhotos = res.data?.posPhotos ? (res.data.posPhotos.map(o => {
return {
url: o
}
})) : []
form.value.posRmb = res.data.posRmb || 0
}
getDetail()
// 店铺门头照上传
const storePictureRead = async (file) => {
// 处理上传的文件
const date = new Date()
const month = date.getMonth() + 1
const theDate = date.getDate()
const pictureUrl = await uploadFileToOSSAPI(`examine/${date.getFullYear()}-${month}/${theDate}/${planId.value}/${userStore().getEmployeeNo}/${uuidv4()}.png`, file.file)
await createExamine({
id: form.value.id,
storePicture: pictureUrl
})
}
// 执行状态改变
const planStatusChange = async (val) => {
form.value.planStatus = val
await createExamine({
id: form.value.id,
planStatus: val ? 1 : 0
})
}
// 改变地堆状态
const changeStoreDd = async () => {
await createExamine({
id: form.value.id,
storeDd: form.value.storeDd
})
}
// 改变在岗状态
const changeTemOnWork = async () => {
if (form.value.temOnWork === '离岗') {
changeTemOnWorkTimeRange()
}
// 保存给后台状态
await createExamine({
id: form.value.id,
temOnWork: form.value.temOnWork
})
}
const changeTemOnWorkTimeRange = () => {
// temOnWorkTimeRange
// 从当前时间到 15 分钟以后,用 parseTime 只显示时和分
const currentTime = new Date()
const futureTime = new Date(currentTime.getTime() + 15 * 60000)
const formatteCurrentTime = parseTime(currentTime, '{h}:{i}')
const formattedTime = parseTime(futureTime, '{h}:{i}')
temOnWorkTimeRange.value = `${formatteCurrentTime} - ${formattedTime}`
}
changeTemOnWorkTimeRange()
// 改变话术物料和着装时把参数传给后台
const changeTemHs = async () => {
await createExamine({
id: form.value.id,
temHs: form.value.temHs
})
}
const changeTemWl = async () => {
await createExamine({
id: form.value.id,
temWl: form.value.temWl
})
}
const changeTemZz = async () => {
await createExamine({
id: form.value.id,
temZz: form.value.temZz
})
}
// 在/离岗照片上传
const temWorkPhotosRead = async (file) => {
// 处理上传的文件
const date = new Date()
const month = date.getMonth() + 1
const theDate = date.getDate()
const pictureUrl = await uploadFileToOSSAPI(`examine/${date.getFullYear()}-${month}/${theDate}/${planId.value}/${userStore().getEmployeeNo}/${uuidv4()}.png`, file.file)
// 如果当前对象包含 objectUrl 则是组件上传的,替换当前元素的对象
const index = form.value.temWorkPhotos.findIndex(o => o.objectUrl)
form.value.temWorkPhotos[index] = {
url: pictureUrl
}
await createExamine({
id: form.value.id,
temWorkPhotos: form.value.temWorkPhotos.map(o => o.url)
})
}
// 特陈照片上传
const storeTcPhotoRead = async (file) => {
// 处理上传的文件
const date = new Date()
const month = date.getMonth() + 1
const theDate = date.getDate()
const pictureUrl = await uploadFileToOSSAPI(`examine/${date.getFullYear()}-${month}/${theDate}/${planId.value}/${userStore().getEmployeeNo}/${uuidv4()}.png`, file.file)
await createExamine({
id: form.value.id,
storeTcPhoto: pictureUrl
})
}
// 主货架照片
const temOnWorkPictureRead = async (file) => {
// 处理上传的文件
const date = new Date()
const month = date.getMonth() + 1
const theDate = date.getDate()
const pictureUrl = await uploadFileToOSSAPI(`examine/${date.getFullYear()}-${month}/${theDate}/${planId.value}/${userStore().getEmployeeNo}/${uuidv4()}.png`, file.file)
await createExamine({
id: form.value.id,
storeZhjPhoto: pictureUrl
})
}
// POS 两张照片
const posPhotosRead = async (file) => {
// 处理上传的文件
const date = new Date()
const month = date.getMonth() + 1
const theDate = date.getDate()
const pictureUrl = await uploadFileToOSSAPI(`examine/${date.getFullYear()}-${month}/${theDate}/${planId.value}/${userStore().getEmployeeNo}/${uuidv4()}.png`, file.file)
// 判断 objectUrl
const index = form.value.posPhotos.findIndex(o => o.objectUrl)
form.value.posPhotos[index] = {
url: pictureUrl
}
await createExamine({
id: form.value.id,
posPhotos: form.value.posPhotos.map(o => o.url)
})
}
// POS 金额修改
const posRmbChange = async () => {
await createExamine({
id: form.value.id,
posRmb: form.value.posRmb || 0
})
}
</script>
<style scoped>
html,
body {
margin: 0;
padding: 0;
}
.container {
padding: 16px;
}
.group-container {
margin-bottom: 0;
padding: 16px;
}
.section {
margin-bottom: 20px;
}
.form-item {
margin-bottom: 12px;
}
h2 {
font-size: 18px;
margin-bottom: 8px;
}
.uploader-preview img {
width: 100%;
height: auto;
}
label {
font-size: 0.373rem;
}
:deep(.van-radio__label) {
font-size: 0.373rem;
}
.bg-white {
background-color: #fff;
}
.bg-gray-100 {
background-color: transparent;
/* 去掉灰色背景 */
}
.group-title {
/* 减小组标题的字体大小 */
font-size: 14px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 8px;
}
.group-title .title-divider {
width: 100%;
height: 1px;
background-color: #ccc;
margin-top: 4px;
}
/* 让具体 item 的标题不加粗 */
label {
font-size: 0.373rem;
font-weight: normal;
}
.custom-counter {
display: flex;
align-items: center;
margin-left: 10px;
}
.custom-counter button {
width: 25px;
height: 25px;
border: 1px solid #ccc;
background-color: #f0f0f0;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
/* 缩小按钮内文本的字体大小 */
font-size: 14px;
}
.custom-counter span {
width: 30px;
text-align: center;
border-top: 1px solid #ccc;
border-bottom: 1px solid #ccc;
padding: 3px 0;
font-size: 14px;
}
/* 给单选框添加上下间距 */
:deep(.van-radio) {
margin-top: 8px;
margin-bottom: 8px;
}
/* 调整预览图片的大小 */
.uploader-preview img {
width: 100%;
height: 100%;
object-fit: cover;
/* 确保图片覆盖整个容器 */
}
</style>
<style scoped>
/* 自定义促销员数量输入框的样式 */
.custom-promoter-count-field .van-field__control {
/* 调整输入框宽度 */
width: 80px;
}
.custom-promoter-count-field .van-field__button {
/* 调整 + - 按钮的颜色 */
color: #007aff;
}
:deep(.van-field) {
height: 30px;
/* 可以根据需要调整高度 */
line-height: 30px;
/* 确保文本垂直居中 */
padding: 0;
/* 外部 padding 覆盖成 0 */
}
/* 使用样式穿透为 POS 金额输入框添加背景色 */
:deep(.van-field__control) {
background-color: #f5f5f5;
/* 可根据需要调整背景色 */
padding-left: 16px;
}
</style>
\ No newline at end of file
<template> <template>
<div class="mobile-container"> <div class="mobile-container">
<van-nav-bar left-text="返回" <van-nav-bar left-text="返回"
:right-text="(examined ? '已' : '未') + '稽查'"
left-arrow left-arrow
@click-left="clickBack()" /> @click-left="clickBack()"
@click-right="clickExamine()"
fixed
placeholder />
<van-cell-group> <van-cell-group>
<van-cell> <van-cell>
<template #title> <template #title>
...@@ -17,7 +21,7 @@ ...@@ -17,7 +21,7 @@
<p class="employee">活动日期:&emsp;{{ parseTime(planDetail.date, '{y}-{m}-{d} (周{a})') }}</p> <p class="employee">活动日期:&emsp;{{ parseTime(planDetail.date, '{y}-{m}-{d} (周{a})') }}</p>
<p class="employee">归属人:&emsp;&emsp;{{ planDetail.employeeName }}{{ planDetail.employeeNo }}</p> <p class="employee">归属人:&emsp;&emsp;{{ planDetail.employeeName }}{{ planDetail.employeeNo }}</p>
<p class="employee">战区:&emsp;&emsp;&emsp;{{ planDetail.orgName }}</p> <p class="employee">战区:&emsp;&emsp;&emsp;{{ planDetail.orgName }}</p>
<p class="employee">系统名称:&emsp;{{ planDetail.lineName }}</p> <p class="employee">系统名称:&emsp;{{ planDetail.lineName }}</p>
<p class="employee">店铺编码:&emsp;{{ planDetail.storeCode }}</p> <p class="employee">店铺编码:&emsp;{{ planDetail.storeCode }}</p>
<p class="employee">上班时间:&emsp;{{ parseTime(planDetail.clockInTime, "{h}:{i}:{s}") }}</p> <p class="employee">上班时间:&emsp;{{ parseTime(planDetail.clockInTime, "{h}:{i}:{s}") }}</p>
...@@ -41,7 +45,7 @@ ...@@ -41,7 +45,7 @@
finished-text="没有更多了" finished-text="没有更多了"
@load="onLoad"> @load="onLoad">
<van-cell-group inset <van-cell-group inset
v-if="planList.length > 0"> v-if="planList?.length > 0">
<van-cell v-for="item in planList" <van-cell v-for="item in planList"
:key="item.id" :key="item.id"
label-class="image-cell"> label-class="image-cell">
...@@ -81,19 +85,23 @@ ...@@ -81,19 +85,23 @@
</template> </template>
<script setup> <script setup>
import { getPlanDetailAPI } from '@/api' import { getPlanDetailAPI, createExamine } from '@/api'
import { parseTime } from '@/utils' import { parseTime } from '@/utils'
// 获取路由路径上的 id 参数 // 获取路由路径上的 id 参数
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const id = route.params.id;
// 稽查
const examined = ref(0) // 获取默认的
const newExamined = ref(0) // 没有默认创建的新的
// 获取计划详情 // 获取计划详情
const planDetail = ref({}) const planDetail = ref({})
const planList = ref([]) const planList = ref([])
const getPlanDetail = async () => { const getPlanDetail = async () => {
const res = await getPlanDetailAPI(id) const res = await getPlanDetailAPI(route.params.id)
planDetail.value = res.data.planInfo planDetail.value = res.data.planInfo
examined.value = res.data.examine?.id
// 循环多人打卡记录 // 循环多人打卡记录
planList.value = res.data.reporteds?.map(o => { planList.value = res.data.reporteds?.map(o => {
...@@ -185,6 +193,20 @@ const getPlanDetail = async () => { ...@@ -185,6 +193,20 @@ const getPlanDetail = async () => {
} }
getPlanDetail() getPlanDetail()
// 点击稽查按钮
const clickExamine = async () => {
// 如果稽查 ID 是空则创建一个
if (!examined.value) {
const result = await createExamine({
...planDetail.value,
id: '',
planId: planDetail.value.id
})
newExamined.value = result.data.id
}
router.push({ path: `/examine/${examined?.value || newExamined.value}` })
}
const refreshLoading = ref(false) const refreshLoading = ref(false)
const onRefresh = () => { const onRefresh = () => {
refreshLoading.value = true refreshLoading.value = true
...@@ -219,6 +241,8 @@ const previewImage = (list, ind) => { ...@@ -219,6 +241,8 @@ const previewImage = (list, ind) => {
const onChange = (ind) => { const onChange = (ind) => {
index.value = ind index.value = ind
} }
</script> </script>
<style scoped> <style scoped>
...@@ -242,12 +266,12 @@ p { ...@@ -242,12 +266,12 @@ p {
margin-top: 20px; margin-top: 20px;
min-height: 100vh; min-height: 100vh;
::v-deep(.van-cell:nth-child(n+2)){ ::v-deep(.van-cell:nth-child(n+2)) {
margin-top: 20px; margin-top: 20px;
} }
::v-deep(.van-cell) { ::v-deep(.van-cell) {
.item{ .item {
margin-top: 20px; margin-top: 20px;
} }
} }
...@@ -305,11 +329,11 @@ p { ...@@ -305,11 +329,11 @@ p {
} }
.gray_title{ .gray_title {
color: gray; color: gray;
} }
.black_title{ .black_title {
color: black; color: black;
} }
</style> </style>
\ No newline at end of file
...@@ -105,6 +105,12 @@ export const constantMobileRoutes = [ ...@@ -105,6 +105,12 @@ export const constantMobileRoutes = [
component: () => import('@/mobile_views/promotion/detail'), component: () => import('@/mobile_views/promotion/detail'),
name: 'Detail', name: 'Detail',
hidden: true, hidden: true,
},
{
path: '/examine/:examineId',
component: () => import('@/mobile_views/examine'),
name: 'Examine',
hidden: true,
} }
] ]
......
...@@ -598,7 +598,7 @@ const columns = ref([ ...@@ -598,7 +598,7 @@ const columns = ref([
{ {
label: '店铺名称', label: '店铺名称',
prop: 'storeName', prop: 'storeName',
width: 150 width: 240
}, },
{ {
label: '活动日期', label: '活动日期',
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论