提交 017cd04d authored 作者: lidongxu's avatar lidongxu

refactor(mobile/pages/storeexecution/report): 新增:勤策App移动端_新增店内执行上报预览功能

上级 8fb7776d
......@@ -89,7 +89,8 @@ const handleIconClick = (icon) => {
.mobile-container {
background-color: #f3f3f3;
min-height: 100vh;
font-size: 14px;
font-size: 13px;
// font-size: 12px;
padding: 20px;
p {
......@@ -122,7 +123,7 @@ const handleIconClick = (icon) => {
display: grid;
grid-template-columns: repeat(4, 1fr);
/* 创建4列等宽布局 */
gap: 10px;
gap: 2px;
/* 列之间的间距 */
justify-content: space-between;
/* 两边对齐 */
......
<template>
<!-- 抽屉搜索组件 -->
<van-popup v-model:show="showDrawer"
position="right"
class="search-drawer"
:style="{ width: '85%', height: '100%' }"
teleport="body"
@close="handleClose">
<!-- 抽屉头部 -->
<van-nav-bar title="筛选条件"
left-text="关闭"
left-arrow
@click-left="handleClose" />
<!-- 搜索表单内容 -->
<div class="search-content">
<van-cell-group inset>
<!-- 计划月份 -->
<van-field :modelValue="parseTime(queryParams.salesMonth, '{y}-{m}')"
label="计划月份"
placeholder="选择计划月份"
readonly
is-link
@click="showMonthPicker = true">
<template #input>
<span>{{ queryParams.salesMonth || '请选择' }}</span>
</template>
</van-field>
<!-- 大区/战区 -->
<van-field v-model="queryParams.deptName"
label="大区/战区"
placeholder="请输入大区/战区"
@update:model-value="handleChange"
clearable />
<!-- 经销商编码/名称 -->
<van-field v-model="queryParams.dealerCN"
label="经销商编码/名称"
placeholder="请输入经销商编码/名称"
@update:model-value="handleChange"
clearable />
<!-- 系统名称 -->
<van-field v-model="queryParams.lineNameLike"
label="系统名称"
placeholder="请输入系统名称"
@update:model-value="handleChange"
clearable />
<!-- 门店编码/名称 -->
<van-field v-if="showStoreSearch"
v-model="queryParams.storeCN"
label="门店编码/名称"
placeholder="请输入门店编码/名称"
@update:model-value="handleChange"
clearable />
<!-- 数据筛选 -->
<van-field v-model="queryParams.rqStatus"
label="数据筛选"
placeholder="请选择筛选状态"
readonly
is-link
@click="showStatusPicker = true">
<template #input>
<span>{{ getStatusText(queryParams.rqStatus) }}</span>
</template>
</van-field>
</van-cell-group>
<!-- 月份选择器 -->
<van-popup v-model:show="showMonthPicker"
position="bottom"
teleport="body">
<van-date-picker v-model="currentMonth"
title="选择计划月份"
:columns-type="['year', 'month']"
:min-date="minDate"
:max-date="maxDate"
@confirm="confirmMonth"
@cancel="showMonthPicker = false" />
</van-popup>
<!-- 状态选择器 -->
<van-popup v-model:show="showStatusPicker"
position="bottom"
teleport="body">
<van-picker :columns="statusOptions"
@confirm="confirmStatus"
@cancel="showStatusPicker = false" />
</van-popup>
<!-- 操作按钮 -->
<div class="action-buttons">
<van-button type="default"
size="large"
@click="handleReset"
class="reset-btn">
<van-icon name="replay" /> 重置
</van-button>
<van-button type="primary"
size="large"
@click="handleConfirm"
class="confirm-btn">
<van-icon name="success" /> 确定
</van-button>
</div>
</div>
</van-popup>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { parseTime } from '@/utils'
const { proxy } = getCurrentInstance()
const props = defineProps({
showSearch: {
type: Boolean,
default: false
},
showStoreSearch: {
type: Boolean,
default: true
},
queryParams: {
type: Object,
default: () => ({})
}
})
const emits = defineEmits(['change', 'update:showSearch'])
// 抽屉显示状态
const showDrawer = computed({
get: () => props.showSearch,
set: (value) => emits('update:showSearch', value)
})
// 选择器状态
const showMonthPicker = ref(false)
const showStatusPicker = ref(false)
const currentMonth = ref([])
// 日期范围
const minDate = new Date(2016, 0, 1)
const maxDate = new Date(new Date().getFullYear(), new Date().getMonth() + 1, 31)
// 状态选项
const statusOptions = [
{ text: '全部', value: '' },
{ text: '未执行', value: '未执行' }
]
// 获取状态文本
const getStatusText = (value) => {
const option = statusOptions.find(item => item.value === value)
return option ? option.text : '全部'
}
// 确认月份选择
const confirmMonth = ({ selectedValues: value }) => {
// value 是 [2025, 2] 这种格式
const year = value[0]
const month = String(value[1]).padStart(2, '0')
props.queryParams.salesMonth = `${year}-${month}`
showMonthPicker.value = false
handleChange()
}
// 确认状态选择
const confirmStatus = (value) => {
props.queryParams.rqStatus = value.selectedOptions[0]?.value || ''
showStatusPicker.value = false
handleChange()
}
// 处理变化
const handleChange = () => {
emits('change')
}
// 关闭抽屉
const handleClose = () => {
showDrawer.value = false
}
// 重置表单
const handleReset = () => {
// 重置所有查询参数
Object.keys(props.queryParams).forEach(key => {
if (key !== 'pageNum' && key !== 'pageSize') {
props.queryParams[key] = ''
}
})
handleChange()
}
// 确定搜索
const handleConfirm = () => {
handleChange()
handleClose()
}
// 监听外部showSearch变化
watch(() => props.showSearch, (newVal) => {
if (newVal) {
// 打开抽屉时初始化当前月份
if (props.queryParams.salesMonth) {
// 如果是日期类型转成字符串
if (props.queryParams.salesMonth instanceof Date) {
props.queryParams.salesMonth = props.queryParams.salesMonth.toISOString().split('T')[0].substring(0, 7)
}
const [year, month] = props.queryParams.salesMonth.split('-')
// currentMonth = [2025, 02] 这种格式
currentMonth.value = [parseInt(year), parseInt(month)]
} else {
// 当前年月日
currentMonth.value = [new Date().getFullYear(), new Date().getMonth()]
}
}
})
</script>
<style scoped
lang="scss">
.search-drawer {
.search-content {
height: calc(100% - 46px); // 减去导航栏高度
overflow-y: auto;
padding: 16px;
.van-cell-group {
margin-bottom: 20px;
}
.action-buttons {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
padding: 16px;
display: flex;
gap: 12px;
border-top: 1px solid #ebedf0;
.reset-btn,
.confirm-btn {
flex: 1;
:deep(.van-button__content) {
display: flex;
align-items: center;
gap: 4px;
}
}
}
}
}
</style>
\ No newline at end of file
<template>
<div class="app-container">
<div class="container">
<van-nav-bar fixed
left-arrow
@click-left="router.back()"
title="店内执行上报浏览"
right-text="筛选"
@click-right="handleClickRight" />
<SearchList v-model:showSearch="showSearch"
:queryParams="params"
@change="getTableList" />
<el-tabs v-model="activeName"
class="demo-tabs"
@tab-click="handleClickTabs">
<el-tab-pane label="常规陈列"
name="常规陈列">
<Display :params="params" ref="display"/>
</el-tab-pane>
<el-tab-pane label="档期计划"
name="档期计划">
<Schedule :params="params" ref="schedule"/>
</el-tab-pane>
<el-tab-pane label="档期陈列"
name="档期陈列">
<ScheduleDis :params="params" ref="scheduleDis"/>
</el-tab-pane>
<el-tab-pane label="零食陈列"
name="零食陈列">
<Snack :params="params" ref="snack"/>
</el-tab-pane>
<el-tab-pane label="三米两秒"
name="三米两秒">
<ThreeTwoSeconds :params="params" ref="threeTwoSeconds"/>
</el-tab-pane>
<el-tab-pane label="六小金刚"
name="六小金刚">
<SixLittleDiamonds :params="params" ref="sixLittleDiamonds"/>
</el-tab-pane>
</el-tabs>
</div>
</div>
</template>
<script setup>
import Display from './tabs/display.vue'
import Schedule from './tabs/schedule.vue'
import ScheduleDis from './tabs/schedule_dis.vue'
import Snack from './tabs/snack.vue'
import ThreeTwoSeconds from './tabs/three_two_seconds.vue'
import SixLittleDiamonds from './tabs/six_little_diamonds.vue'
import SearchList from './components/SearchList'
const router = useRouter()
const activeName = ref('常规陈列');
const handleClickTabs = (tab) => {
activeName.value = tab.name;
}
const showSearch = ref(false)
const handleClickRight = () => {
showSearch.value = true;
}
const params = ref({
pageNum: 1,
pageSize: 50,
salesMonth: new Date(),
deptName: '',
dealerCN: '',
lineNameLike: '',
storeCN: ''
})
const display = ref(null)
const schedule = ref(null)
const scheduleDis = ref(null)
const snack = ref(null)
const threeTwoSeconds = ref(null)
const sixLittleDiamonds = ref(null)
const getTableList = () => {
display.value.getTableList()
schedule.value.getTableList()
scheduleDis.value.getTableList()
snack.value.getTableList()
threeTwoSeconds.value.getTableList()
sixLittleDiamonds.value.getTableList()
}
</script>
<style scoped
lang="scss">
.app-container {
padding: 10px;
padding-top: 46px;
.container {
padding: 0px;
.el-tabs {
height: 100%;
display: flex;
flex-direction: column-reverse;
::v-deep(.el-tabs__header) {
// 确保底部边框线完整显示
border-bottom: 1px solid var(--el-border-color-light);
width: 100%;
}
::v-deep(.el-tabs__nav-wrap) {
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch; // iOS平滑滚动
scrollbar-width: none; // Firefox隐藏滚动条
&::after {
// 隐藏el-tabs默认的底部伪元素边框
display: none;
}
&::-webkit-scrollbar {
display: none; // Chrome/Safari隐藏滚动条
}
}
::v-deep(.el-tabs__nav-scroll) {
overflow: visible;
width: max-content;
}
::v-deep(.el-tabs__content) {
display: flex;
flex-direction: column;
overflow-y: scroll;
// height: 100%;
.el-tab-pane {
// height: 100%;
display: flex;
flex-direction: column;
}
}
}
}
}
</style>
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
<template>
<!-- 常规陈列 -->
<CustomTable :tableData="tableData"
:baseColumns="baseColumns"
:total="total"
:params="params"
:isLoading="isLoading"
@getTableList="getTableList"
@updateShowSearch="v => showSearch.value = v" />
</template>
<script setup
lang="jsx">
import CustomTable from '../components/Table'
import SearchList from '../components/SearchList'
import { getDisplayList, submitDisplayPlan } from '@/api'
import { parseTime } from '@/utils'
import { getDisplayConfig } from './data.jsx'
/*************** 表格操作相关 ***************/
const props = defineProps({
params: {
type: Object,
default: () => ({})
}
})
// 提交变更
const tableColumns = getDisplayConfig(async (row, col) => {
// 需要特殊处理的
// 实际主货架-形式,为空时,置空实际主货架-数量
if (col.prop === 'actualMainShelfType' && !row.actualMainShelfType) {
row.actualMainShelfQty = ''
}
let requestObj = {}
if (col.requestKey) {
// 关联的公式计算列,需要特殊处理
for (const str of col.requestKey) {
// 找到目标列的列对象,调用列对象自己的公式函数进行计算
const obj = getDisplayConfig().flatMap(item => {
if (item.children) {
return item.children.filter(child => !child.onlyFill);
}
return [];
}).find(item => item.prop === str)
if (obj && obj.type === 'formula') {
obj.func(row)
}
}
requestObj = col.requestKey.reduce((acc, key) => ({ ...acc, [key]: row[key] }), {})
}
await submitDisplayPlan({
id: row.sadId,
[col.prop]: row[col.prop], // 当前修改列的值
...requestObj,
// 特殊类型数据处理(比如前端要字符串'',后端要数字0)
actualMainShelfQty: row.actualMainShelfQty || 0,
actualEndCapQty: row.actualEndCapQty || 0,
actualFloorStackArea: row.actualFloorStackArea || 0,
actualFloorStackQty: row.actualFloorStackQty || 0,
})
})
// 全部列
const baseColumns = ref(tableColumns);
// 表格数据
const tableData = ref([])
const isLoading = ref(true)
// const params = ref({
// pageNum: 1,
// pageSize: 50,
// salesMonth: new Date(),
// deptName: '',
// dealerCN: '',
// lineNameLike: '',
// storeCN: ''
// })
const total = ref(0)
// 筛选列表数据
const getTableList = async () => {
isLoading.value = true
const res = await getDisplayList({
...props.params,
salesMonth: parseTime(props.params.salesMonth, '{y}-{m}')
})
res.data.rows.forEach(item => {
// 计划月份
item.salesMonth = parseTime(item.salesMonth, '{y}-{m}')
// 开户日期
item.openingDate = parseTime(item.openingDate, '{y}-{m}-{d}')
// 闭户日期
item.closingDate = parseTime(item.closingDate, '{y}-{m}-{d}')
// 修改时间
item.updateTime = parseTime(item.updateTime, '{y}-{m}-{d} {h}:{i}:{s}')
// 动态新增列:门店名称+门店编码+经销山名称(填报模式下,合并到一起)
item.storeNameCodeDealerName = item.storeName + '\n(' + item.storeCode + ')' + '\n(' + item.dealerName + ')'
// 特殊类型数据处理,前端要字符串'',后端要数字0
// item.actualMainShelfQty = item.actualMainShelfQty || ''
// item.actualEndCapQty = item.actualEndCapQty || ''
// item.actualFloorStackArea = item.actualFloorStackArea || ''
// item.actualFloorStackQty = item.actualFloorStackQty || ''
})
tableData.value = res.data.rows
total.value = res.data.total
isLoading.value = false
}
getTableList()
defineExpose({
getTableList
})
/*************** 筛选 ***************/
const showSearch = ref(true)
</script>
<style lang="scss">
// 动态列内容的 render 内样式
// 操作提示列
.operation_tip_cell {
display: flex;
flex-direction: column;
align-items: flex-start;
p {
width: 100%;
margin: 0;
}
p:first-child {
background-color: #e1e2e6;
border-bottom: 1px solid #ebeef5;
}
p:last-child {
padding: 15px 0;
}
}
// 只在填报模式出现的门店名称+门店编码+经销山名称(合并到一起)
.store-name-render-cell {
display: flex;
flex-direction: column;
align-items: flex-start;
p {
width: 100%;
margin: 0;
}
}
</style>
\ No newline at end of file
<template>
<!-- 档期计划 -->
<!-- 隐藏门店搜索 -->
<CustomTable :tableData="tableData"
:baseColumns="baseColumns"
:total="total"
:params="params"
:isLoading="isLoading"
:formatter="formatterFn"
@getTableList="getTableList"
@updateShowSearch="v => showSearch.value = v" />
<!-- 弹窗:实际与计划不一致,让用户补充实际内容 -->
<el-dialog v-model="showPromotionDialog"
title="请输入实际促销机制内容"
width="500%"
:before-close="handleDialogClose">
<el-form :model="dialogForm"
label-width="80px">
<el-form-item label="实际内容">
<el-input v-model="dialogForm.actualPromotionContent"
type="textarea"
:rows="4"
placeholder="请输入实际促销机制内容" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleDialogClose">取消</el-button>
<el-button type="primary"
@click="handleDialogConfirm">确认</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup
lang="jsx">
import CustomTable from '../components/Table'
import SearchList from '../components/SearchList'
import { getDisplayScheduleList, submitDisplaySchedulePlan } from '@/api'
import { parseTime } from '@/utils'
import { getSchedulePlanConfig } from './data.jsx'
const { proxy } = getCurrentInstance()
/*************** 操作类型 ***************/
const props = defineProps({
params: {
type: Object,
default: () => ({})
}
})
// 提交变更
const submitChange = async (row, col) => {
// 需要特殊处理的
// 实际主货架-形式,为空时,置空实际主货架-数量
if (col.prop === 'actualMainShelfType' && !row.actualMainShelfType) {
row.actualMainShelfQty = 0
}
let requestObj = {}
if (col.requestKey) {
// 关联的公式计算列,需要特殊处理
for (const str of col.requestKey) {
const obj = getSchedulePlanConfig().flatMap(item => {
if (item.children) {
return item.children.filter(child => !child.onlyFill);
}
return [];
}).find(item => item.prop == str)
if (obj && obj.type === 'formula') {
obj.func(row)
}
}
requestObj = col.requestKey.reduce((acc, key) => ({ ...acc, [key]: row[key] }), {})
}
await submitDisplaySchedulePlan({
id: row.sapId,
[col.prop]: row[col.prop], // 当前修改列的值
...requestObj,
actualPromotionFlavor: Array.isArray(row.actualPromotionFlavor) ? row.actualPromotionFlavor.join(',') : '-', // 档期执行-促销口味,为数组时,转成字符串
actualPromotionStartDate: row.actualPromotionStartDate ? parseTime(row.actualPromotionStartDate, '{y}-{m}-{d}') : '', // 档期执行-促销开始日期,为字符串时,转成日期格式
})
}
// 基础列配置
const baseColumns = ref(getSchedulePlanConfig(submitChange));
// 表格数据
const tableData = ref([])
const isLoading = ref(true)
const params = ref({
pageNum: 1,
pageSize: 50,
salesMonth: new Date(),
deptName: '',
dealerCN: '',
lineNameLike: '',
storeCN: ''
})
const total = ref(0)
// 获取表格数据
const getTableList = async () => {
isLoading.value = true
const res = await getDisplayScheduleList({
...props.params,
salesMonth: parseTime(props.params.salesMonth, '{y}-{m}')
})
// 处理日期格式
res.data.rows.forEach(item => {
item.salesMonth = parseTime(item.salesMonth, '{y}-{m}')
item.openingDate = parseTime(item.openingDate, '{y}-{m}-{d}')
item.plannedAdjustmentStartDate = parseTime(item.plannedAdjustmentStartDate, '{y}-{m}-{d}')
item.plannedAdjustmentEndDate = parseTime(item.plannedAdjustmentEndDate, '{y}-{m}-{d}')
item.plannedPromotionStartDate = parseTime(item.plannedPromotionStartDate, '{y}-{m}-{d}')
item.actualPromotionStartDate = parseTime(item.actualPromotionStartDate, '{y}-{m}-{d}')
item.plannedPromotionEndDate = parseTime(item.plannedPromotionEndDate, '{y}-{m}-{d}')
item.actualPromotionEndDate = parseTime(item.actualPromotionEndDate, '{y}-{m}-{d}')
item.totalCostRate = item.totalCostRate ? `${(item.totalCostRate).toFixed(2)}%` : '-'
item.adjustmentCostRatio = item.adjustmentCostRatio ? `${(item.adjustmentCostRatio).toFixed(2)}%` : '-'
// 档期执行-促销口味从字符串转成数组(回显用)
item.actualPromotionFlavor = item.actualPromotionFlavor ? item.actualPromotionFlavor.split(',') : []
// 修改时间
item.updateTime = parseTime(item.updateTime, '{y}-{m}-{d} {h}:{i}:{s}')
})
tableData.value = res.data.rows
total.value = res.data.total
isLoading.value = false
}
defineExpose({
getTableList
})
// 表格格式化指定列的数据内容
const formatterFn = (row, col, cellValue) => {
if (col.prop === 'actualPromotionFlavor') {
// 对数组内容格式化,为空时,显示'-'
return cellValue.length ? cellValue.join(',') : '-'
}
return cellValue
}
onMounted(() => {
getTableList()
})
const showSearch = ref(true)
/*************** 弹窗 ***************/
const showPromotionDialog = ref(false)
const nowEnterRow = ref({}) // 当前弹窗弹出时,要填写哪行的数据对象
const nowEnterRowProp = ref('') // 当前弹窗弹出时,要填写哪行的数据对象的属性
const dialogForm = reactive({
actualPromotionContent: '',
})
// 弹窗关闭时,清空表单数据
const handleDialogClose = () => {
showPromotionDialog.value = false
dialogForm.actualPromotionContent = ''
nowEnterRow.value = {}
nowEnterRowProp.value = ''
}
// 确定
const handleDialogConfirm = async () => {
if (!dialogForm.actualPromotionContent) {
proxy.$modal.msgError('请输入实际促销机制内容')
return
}
// 赋值
nowEnterRow.value[nowEnterRowProp.value] = dialogForm.actualPromotionContent
showPromotionDialog.value = false
}
</script>
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论