提交 d106e050 authored 作者: lidongxu's avatar lidongxu

Merge branch 'ap' into dev

......@@ -16,6 +16,58 @@ export let isRelogin = { show: false };
// 促销系统后台 baseURL
export const promotionBaseURL = import.meta.env.VITE_APP_PROMOTION
// 错误消息队列和状态管理
let errorMessageQueue = [];
let isShowingError = false;
let lastErrorTime = 0;
const ERROR_DEBOUNCE_TIME = 1000; // 1秒内的错误视为同一批
// 显示错误消息(单例模式)
function showErrorMessage(message, type = 'error') {
const now = Date.now();
// 如果正在显示错误消息,或者距离上次错误时间很短,将消息加入队列
if (isShowingError || (now - lastErrorTime < ERROR_DEBOUNCE_TIME)) {
// 检查队列中是否已有相同消息,避免重复
const existingIndex = errorMessageQueue.findIndex(item => item.message === message);
if (existingIndex === -1) {
errorMessageQueue.push({ message, type });
}
return;
}
// 标记为正在显示
isShowingError = true;
lastErrorTime = now;
// 显示当前错误消息
ElMessage({
message: message,
type: type,
duration: 3000, // 适当延长显示时间
onClose: () => {
isShowingError = false;
// 检查队列中是否有待显示的消息
if (errorMessageQueue.length > 0) {
// 延迟一段时间再显示下一条,避免消息切换太快
setTimeout(() => {
const nextError = errorMessageQueue.shift();
if (nextError) {
showErrorMessage(nextError.message, nextError.type);
}
}, 500);
}
}
});
}
// 清空错误消息队列(可选,在某些场景下可能需要)
export function clearErrorMessageQueue() {
errorMessageQueue = [];
isShowingError = false;
}
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例
const service = axios.create({
......@@ -91,51 +143,18 @@ service.interceptors.response.use(async res => {
return res.data
}
if (code === 401) {
if (window.h5sdk) {
// 如果在飞书客户端,则无感知重新获取token
const code = await fsClientAuth()
await useUserStore().login({
type: 'fs',
data: { code }
})
// 重新发送本次失败的请求
return service(res.config)
}
// PC/移动端刷新 token 有效期
// await useUserStore().refreshTokenFn()
// return service(res.config)
ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
isRelogin.show = false;
useUserStore().logOut().then(() => {
// location.href = '#/login';
router.push({ path: '/login' })
})
}).catch(() => {
isRelogin.show = false;
});
return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
// ... existing 401 handling code ...
} else if (code === 403) {
// 如果刷新 refreshToken 接口也报错了,证明需要重新登录
// ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
// isRelogin.show = false;
// useUserStore().logOut().then(() => {
// location.href = '#/login';
// })
// }).catch(() => {
// isRelogin.show = false;
// });
// return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
ElMessage({ message: msg, type: 'error' })
showErrorMessage(msg, 'error')
return Promise.reject(new Error(msg))
} else if (code === 500) {
ElMessage({ message: msg, type: 'error' })
showErrorMessage(msg, 'error')
return Promise.reject(new Error(msg))
} else if (code === 601) {
ElMessage({ message: msg, type: 'warning' })
showErrorMessage(msg, 'warning')
return Promise.reject(new Error(msg))
} else if (code !== 200) {
ElNotification.error({ title: msg })
showErrorMessage(msg, 'error')
return Promise.reject('error')
} else {
return Promise.resolve(res.data)
......@@ -150,7 +169,7 @@ service.interceptors.response.use(async res => {
} else if (message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常";
}
ElMessage({ message: message, type: 'error', duration: 5 * 1000 })
showErrorMessage(message, 'error')
return Promise.reject(error)
}
)
......
......@@ -92,7 +92,8 @@ const handleIconClick = (icon) => {
.mobile-container {
background-color: #f3f3f3;
min-height: 100vh;
font-size: 14px;
font-size: 13px;
// font-size: 12px;
padding: 20px;
p {
......@@ -125,7 +126,7 @@ const handleIconClick = (icon) => {
display: grid;
grid-template-columns: repeat(4, 1fr);
/* 创建4列等宽布局 */
gap: 10px;
gap: 2px;
/* 列之间的间距 */
justify-content: space-between;
/* 两边对齐 */
......
<template>
<div class="flex-container">
<!-- 操作类型 -->
<el-row>
<el-form-item>
<el-radio-group v-model="operation" @change="handleChange">
<el-radio-button label="大区战区-分析"
value="大区战区-分析"
v-hasPermi="['promotion:dashboard:list-show']" />
<el-radio-button label="城市经理-分析"
value="城市经理-分析" />
</el-radio-group>
</el-form-item>
</el-row>
<el-table :data="tableData"
border
ref="tableRef"
v-loading="isLoading"
show-overflow-tooltip>
<template v-for="col in chooseColumns"
:key="col.prop">
<!-- 有子列的情况 -->
<el-table-column v-if="col.childColumns && col.childColumns.length > 0"
:label="col.label"
align="center"
:width="col.width">
<el-table-column v-for="childCol in col.childColumns"
:key="childCol.prop"
:label="childCol.label"
:prop="childCol.prop"
align="center"
:width="childCol.width">
</el-table-column>
</el-table-column>
<!-- 没有子列的独立列情况 -->
<el-table-column v-else
:label="col.label"
:prop="col.prop"
align="center"
:width="col.width">
</el-table-column>
</template>
</el-table>
</div>
</template>
<script setup>
import userStore from '@/store/modules/user'
const props = defineProps({
tableData: { // 数据源
type: Array,
default: () => []
},
baseColumns: { // 表格列
type: Array,
default: () => []
},
total: { // 总条数
type: Number,
default: 0
},
isLoading: { // 表格加载状态
type: Boolean,
default: false
},
formatter: { // 格式化函数
type: Function,
default: (row, col, cellValue) => cellValue
}
})
const emit = defineEmits(['getTableList'])
/*************** 工具栏 ***************/
const operation = ref(''); // 切换平铺/填报模式
const tableRef = ref(null)
const chooseColumns = ref([]) // 使用的列
// 检查用户是否有特定权限的辅助函数
const hasPermission = (permission) => {
return userStore().permissions.includes(permission)
}
// 根据权限动态设置默认选中的值
onMounted(() => {
// 优先检查是否有'promotion:dashboard:list-show'权限,如果有则选中'大区战区-分析'
if (hasPermission('promotion:dashboard:list-show')) {
operation.value = '大区战区-分析'
} else {
// 否则默认选中'城市经理-分析'
operation.value = '城市经理-分析'
}
// 初始化列选择
initColumns()
})
// 初始化列选择的函数
const initColumns = () => {
if (operation.value === '大区战区-分析') {
chooseColumns.value = props.baseColumns.filter(item => item.prop !== 'cityManager')
} else {
chooseColumns.value = props.baseColumns
}
}
watch(operation, (newVal) => {
// 如果是大区战区-分析,则隐藏城市经理列
if (newVal === '大区战区-分析') {
chooseColumns.value = props.baseColumns.filter(item => item.prop !== 'cityManager')
} else {
chooseColumns.value = props.baseColumns
}
}, {
immediate: true
})
// 切换操作类型时触发的函数
const handleChange = (newVal) => {
emit('getTableList', newVal)
}
</script>
<style scoped
lang="scss">
/* 根容器设置为flex布局,占据整个可用空间 */
.flex-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
/* 表格样式 */
.table-container {
flex: 1;
/* 这是关键,让表格容器自动占据剩余空间 */
display: flex;
flex-direction: column;
min-height: 0;
/* 解决flex子项内容溢出问题 */
.auto-fit-header-table {
flex: 1;
/* 列样式 */
.column-style {
/* 列头样式 */
.formula-column {
display: flex;
justify-content: center;
align-items: center;
.column {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
p {
margin: 0;
font-size: 12px;
color: #606266;
}
}
.el-icon {
margin-left: 2px;
}
}
/* 单元格样式 */
/* 展示模式单元格(仅展示模式生效) */
.show-cell-style {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 自定义单元格样式(仅填充模式生效) */
.cell-style {
>div {
display: flex;
flex-direction: column;
align-items: center;
>span {
text-align: left;
text-indent: 5px;
display: inline-block;
width: 80%;
background-color: #e1e2e6;
border-bottom: 1px solid #ebeef5;
}
}
/* 表格内下拉框 */
.el-select {
width: 100% !important;
padding: 10px;
/* 非 disabled 状态下的背景颜色 */
&.no-disabled ::v-deep(.el-select__wrapper) {
border: 4px solid var(--el-background-editor-cell) !important;
}
}
.el-input {
height: 32px !important;
/* box-sizing: content-box; */
padding: 0 10px;
width: 100%;
/* 非 disabled 状态下的背景颜色 */
&.no-disabled ::v-deep(.el-input__wrapper) {
border: 4px solid var(--el-background-editor-cell) !important;
}
}
.date-picker {
width: 100%;
padding: 10px;
/* 非 disabled 状态下的背景颜色 */
&.no-disabled ::v-deep(.el-input__wrapper) {
border: 4px solid var(--el-background-editor-cell) !important;
}
::v-deep(.el-input) {
width: 100%;
padding: 0;
}
}
}
/* 普通文字单元格(仅填充模式生效) */
.fill-span-wrap {
/* 超出省略号 */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 0 10px;
}
}
/* 填报模式-单元格 */
&.cell-no-padding {
/* 无上下内边距 */
::v-deep(.el-table__row) {
.el-table__cell {
padding-top: 0;
padding-bottom: 0;
.cell {
padding: 0 !important;
}
}
}
}
}
/* 分页器 */
.pagination-container {
margin: 10px;
}
}
}
/* 加深表头背景颜色 */
::v-deep(.el-table__header-wrapper) {
.el-table__header {
th {
background-color: #dcdfe6 !important;
color: #303133 !important;
font-weight: bold;
}
}
}
</style>
<template>
<div class="app-container">
<div class="container">
<van-nav-bar fixed
left-arrow
@click-left="router.back()"
title="店内执行上报浏览" />
<el-tabs v-model="activeName"
class="demo-tabs"
@tab-click="handleClickTabs">
<el-tab-pane label="常规陈列"
name="常规陈列">
<Display />
</el-tab-pane>
<el-tab-pane label="档期计划"
name="档期计划">
<Schedule />
</el-tab-pane>
<el-tab-pane label="档期陈列"
name="档期陈列">
<ScheduleDis />
</el-tab-pane>
<el-tab-pane label="零食陈列"
name="零食陈列">
<Snack />
</el-tab-pane>
<el-tab-pane label="三米两秒"
name="三米两秒">
<ThreeTwoSeconds />
</el-tab-pane>
<el-tab-pane label="六小金刚"
name="六小金刚">
<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'
const router = useRouter()
const activeName = ref('常规陈列');
const handleClickTabs = (tab) => {
activeName.value = tab.name;
}
</script>
<style scoped
lang="scss">
.app-container {
padding: 10px;
padding-top: 46px;
.container {
padding: 10px;
.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
// 每个表格里的数据列信息集合
// 常规陈列
export const getDisplayConfig = () => {
return [
// {
// label: "计划月份",
// prop: "salesMonth",
// width: 100,
// childCol: []
// },
{
label: "大区",
prop: "regionName",
childCol: []
},
{
label: "战区",
prop: "districtName",
childCol: [],
width: 110
},
{
label: "城市经理",
prop: "cityManager",
childCol: []
},
{
label: '主货架',
prop: "mainShelf",
childColumns: [
{
label: '计划',
prop: "planMsStoreCnt",
},
{
label: '执行',
prop: "execMsStoreCnt",
},
{
label: '执行率',
prop: "msExecRate",
width: 90
}
]
},
{
label: '端架',
prop: "endShelf",
childColumns: [
{
label: '计划',
prop: "planRegEndcapStoreCnt",
},
{
label: '执行',
prop: "execRegEndcapStoreCnt",
},
{
label: '执行率',
prop: "endcapExecRate",
width: 90
}
]
},
{
label: '地堆',
prop: "endcapShelf",
childColumns: [
{
label: '计划',
prop: "planRegGsStoreCnt",
},
{
label: '执行',
prop: "execRegGsStoreCnt",
},
{
label: '执行率',
prop: "gsExecRate",
width: 90
}
]
},
{
label: '多点陈列',
prop: "multiShelf",
childColumns: [
{
label: '计划',
prop: "planMpDispStoreCnt",
},
{
label: '执行',
prop: "execMpDispStoreCnt",
},
{
label: '执行率',
prop: "mpDispExecRate",
width: 90
}
]
},
{
label: '挂条陈列',
prop: "hangShelf",
childColumns: [
{
label: '计划',
prop: "planHsStoreCnt",
},
{
label: '执行',
prop: "execHsStoreCnt",
},
{
label: '执行率',
prop: "HsExecRate",
width: 90
}
]
}
]
}
// 档期计划
export const getSchedulePlanConfig = (submitChange) => {
return [
// {
// label: "计划月份",
// prop: "salesMonth",
// width: 100
// },
{
label: "大区",
prop: "regionName",
width: 100
},
{
label: "战区",
prop: "districtName",
width: 100
},
{
label: "城市经理",
prop: "cityManager",
width: 100
},
{
label: '大区反馈',
prop: "regionFeedback",
childColumns: [
{
label: '计划',
prop: "planPromoPeriStoreCnt",
},
{
label: '执行',
prop: "launchPromoPeriStoreCnt",
},
{
label: '执行率',
prop: "launchRatePromoPeriExec",
width: 90
}
]
},
{
label: '规格',
prop: "spec",
childColumns: [
{
label: '执行',
prop: "execPsStoreCnt",
},
{
label: '执行率',
prop: "psExecRatePromoPeriExec",
width: 90
}
]
},
{
label: '口味',
prop: "flavor",
childColumns: [
{
label: '执行',
prop: "execPfStoreCnt",
},
{
label: '执行率',
prop: "pfExecRatePromoPeriExec",
width: 90
}
]
},
{
label: '价格',
prop: "price",
childColumns: [
{
label: '执行',
prop: "execPpStoreCnt",
},
{
label: '执行率',
prop: "ppExecRatePromoPeriExec",
width: 90
}
]
},
{
label: '海报',
prop: "poster",
childColumns: [
{
label: '计划',
prop: "planPosterStoreCnt",
},
{
label: '执行',
prop: "execPosterStoreCnt",
},
{
label: '执行率',
prop: "posterExecRate",
width: 90
}
]
},
]
}
// 档期陈列
export const getScheduleDisConfig = (submitChange) => {
return [
// {
// label: "计划月份",
// prop: "salesMonth",
// width: 100
// },
{
label: "大区",
prop: "regionName",
width: 100
},
{
label: "战区",
prop: "districtName",
width: 100
},
{
label: "城市经理",
prop: "cityManager",
width: 100
},
{
label: '端架',
prop: "endShelf",
childColumns: [
{
label: '计划',
prop: "planEndcapStoreCnt",
},
{
label: '执行',
prop: "execEndcapStoreCnt",
},
{
label: '执行率',
prop: "endcapExecRatePromoPeriDisp",
}
]
},
{
label: '地堆',
prop: "endcapShelf",
childColumns: [
{
label: '计划',
prop: "planGsStoreCnt",
},
{
label: '执行',
prop: "execGsStoreCnt",
},
{
label: '执行率',
prop: "gsExecRatePromoPeriDisp",
}
]
},
{
label: '其他陈列',
prop: "otherShelf",
childColumns: [
{
label: '计划',
prop: "planOtherDispStoreCnt",
},
{
label: '执行',
prop: "execOtherDispStoreCnt",
},
{
label: '执行率',
prop: "otherDispExecRatePromoPeriDisp",
}
]
},
]
}
// 零食陈列
export const getSnackCofing = (submitChange) => {
return [
// {
// label: "计划月份",
// prop: "salesMonth",
// width: 100
// },
{
label: "大区",
prop: "regionName",
width: 100
},
{
label: "战区",
prop: "districtName",
width: 100
},
{
label: "城市经理",
prop: "cityManager",
width: 100
},
{
label: '计划',
prop: "planSnackStoreCnt",
},
{
label: '执行',
prop: "execSnackStoreCnt",
},
{
label: '执行率',
prop: "snackExecRate",
}
]
}
// 三米两秒
export const getThreeTwoSecondsConfig = (submitChange) => {
return [
// {
// label: "计划月份",
// prop: "salesMonth",
// width: 100
// },
{
label: "大区",
prop: "regionName",
width: 100
},
{
label: "战区",
prop: "districtName",
width: 100
},
{
label: "城市经理",
prop: "cityManager",
width: 100
},
{
label: '计划',
prop: "planSLStoreCnt",
},
{
label: '执行',
prop: "execSLStoreCnt",
},
{
label: '执行率',
prop: "SLExecRate",
}
]
}
// 六小金刚
export const getSixLittleDiamondsConfig = (submitChange) => {
return [
// {
// label: "计划月份",
// prop: "salesMonth",
// width: 100
// },
{
label: "大区",
prop: "regionName",
width: 100
},
{
label: "战区",
prop: "districtName",
width: 100
},
{
label: "城市经理",
prop: "cityManager",
width: 100
},
{
label: '计划',
prop: "planSixJdStoreCnt",
},
{
label: '执行',
prop: "execSixJdStoreCnt",
},
{
label: '执行率',
prop: "sixJdExecRate",
}
]
}
<template>
<!-- 常规陈列 -->
<el-form :inline="true"
:model="queryParams"
class="demo-form-inline">
<el-form-item label="计划月份">
<el-date-picker v-model="queryParams.salesMonth"
type="month"
placeholder="选择计划月份"
@change="getTableList"
clearable />
</el-form-item>
</el-form>
<CustomTable :tableData="tableData"
:baseColumns="baseColumns"
:isLoading="isLoading"
@getTableList="getTableList"
@updateShowSearch="v => showSearch.value = v" />
</template>
<script setup
lang="jsx">
import CustomTable from '../components/Table'
import { getDisplayScheduleDashboardList, getDisplayScheduleDashboardListArea } from '@/api'
import { getDisplayConfig } from './data.jsx'
import { parseTime } from '@/utils'
/*************** 表格操作相关 ***************/
// 提交变更
const tableColumns = getDisplayConfig()
// 全部列
const baseColumns = ref(tableColumns);
// 表格数据
const queryParams = reactive({
salesMonth: new Date(),
})
const tableData = ref([])
const isLoading = ref(true)
const total = ref(0)
// 筛选列表数据
const getTableList = async (operation) => {
isLoading.value = true
const res = await (operation === '大区战区-分析' ? getDisplayScheduleDashboardList : getDisplayScheduleDashboardListArea)({
...queryParams,
salesMonth: parseTime(queryParams.salesMonth, '{y}-{m}')
})
if (operation === '大区战区-分析') {
// 合并战区大区结构为扁平化
const { zq, dq } = res.data
tableData.value = [...zq, ...dq]
} else {
// 合并城市经理结构为扁平化
tableData.value = res.data
}
isLoading.value = false
}
const { proxy } = getCurrentInstance()
const isDaQuZQ = proxy.checkPermi(['promotion:dashboard:list-show'])
getTableList(isDaQuZQ ? '大区战区-分析' : '城市经理-分析')
</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>
<!-- 档期计划 -->
<!-- 隐藏门店搜索 -->
<el-form :inline="true"
:model="queryParams"
class="demo-form-inline">
<el-form-item label="计划月份">
<el-date-picker v-model="queryParams.salesMonth"
type="month"
placeholder="选择计划月份"
@change="getTableList"
clearable />
</el-form-item>
</el-form>
<CustomTable :tableData="tableData"
:baseColumns="baseColumns"
:isLoading="isLoading"
@getTableList="getTableList"
@updateShowSearch="v => showSearch.value = v" />
</template>
<script setup
lang="jsx">
import CustomTable from '../components/Table'
import { getDisplayScheduleDashboardList, getDisplayScheduleDashboardListArea } from '@/api'
import { parseTime } from '@/utils'
import { getSchedulePlanConfig } from './data.jsx'
const { proxy } = getCurrentInstance()
/*************** 操作类型 ***************/
// 提交变更
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 queryParams = reactive({
salesMonth: new Date(),
})
const tableData = ref([])
const isLoading = ref(true)
const total = ref(0)
// 筛选列表数据
const getTableList = async (operation) => {
isLoading.value = true
const res = await (operation === '大区战区-分析' ? getDisplayScheduleDashboardList : getDisplayScheduleDashboardListArea)({
...queryParams,
salesMonth: parseTime(queryParams.salesMonth, '{y}-{m}')
})
if (operation === '大区战区-分析') {
// 合并战区大区结构为扁平化
const { zq, dq } = res.data
tableData.value = [...zq, ...dq]
} else {
// 合并城市经理结构为扁平化
tableData.value = res.data
}
isLoading.value = false
}
const isDaQuZQ = proxy.checkPermi(['promotion:dashboard:list-show'])
getTableList(isDaQuZQ ? '大区战区-分析' : '城市经理-分析')
</script>
<template>
<!-- 档期陈列 -->
<el-form :inline="true"
:model="queryParams"
class="demo-form-inline">
<el-form-item label="计划月份">
<el-date-picker v-model="queryParams.salesMonth"
type="month"
placeholder="选择计划月份"
@change="getTableList"
clearable />
</el-form-item>
</el-form>
<CustomTable :tableData="tableData"
:baseColumns="baseColumns"
:isLoading="isLoading"
@getTableList="getTableList"
@updateShowSearch="v => showSearch.value = v" />
</template>
<script setup
lang="jsx">
import CustomTable from '../components/Table'
import { getDisplayScheduleDashboardList, getDisplayScheduleDashboardListArea } from '@/api'
import { parseTime } from '@/utils'
import { getScheduleDisConfig } from './data'
/*************** 表格数据 ***************/
// 提交变更
const submitChange = async (row, col) => {
// 需要特殊处理的
// 1. 实际主货架-形式,为空时,置空实际主货架-数量
if (col.prop === 'actualMainShelfType' && !row.actualMainShelfType) {
row.actualMainShelfQty = 0
}
// 关联的公式计算列,需要特殊处理
for (const str of col.requestKey) {
const obj = getScheduleDisConfig().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)
}
}
await submitDisplayScheduleDetail({
id: row.sapdId,
[col.prop]: row[col.prop], // 当前修改列的值
...col.requestKey.reduce((acc, key) => ({ ...acc, [key]: row[key] }), {}), // 额外携带影响的列字段值
// 特殊类型字段处理
// 端架数量实际
actualEndCapQty: row.actualEndCapQty || 0,
// 地堆平米数实际
actualFloorStackArea: row.actualFloorStackArea || 0,
})
}
// 全部列
const baseColumns = ref(getScheduleDisConfig(submitChange));
// 表格数据
const queryParams = reactive({
salesMonth: new Date(),
})
const tableData = ref([])
const isLoading = ref(true)
const total = ref(0)
// 筛选列表数据
const getTableList = async (operation) => {
isLoading.value = true
const res = await (operation === '大区战区-分析' ? getDisplayScheduleDashboardList : getDisplayScheduleDashboardListArea)({
...queryParams,
salesMonth: parseTime(queryParams.salesMonth, '{y}-{m}')
})
if (operation === '大区战区-分析') {
// 合并战区大区结构为扁平化
const { zq, dq } = res.data
tableData.value = [...zq, ...dq]
} else {
// 合并城市经理结构为扁平化
tableData.value = res.data
}
isLoading.value = false
}
const { proxy } = getCurrentInstance()
const isDaQuZQ = proxy.checkPermi(['promotion:dashboard:list-show'])
getTableList(isDaQuZQ ? '大区战区-分析' : '城市经理-分析')
</script>
\ No newline at end of file
<template>
<!-- 六小金刚 -->
<el-form :inline="true"
:model="queryParams"
class="demo-form-inline">
<el-form-item label="计划月份">
<el-date-picker v-model="queryParams.salesMonth"
type="month"
placeholder="选择计划月份"
@change="getTableList"
clearable />
</el-form-item>
</el-form>
<CustomTable :tableData="tableData"
:baseColumns="baseColumns"
:isLoading="isLoading"
@getTableList="getTableList"
@updateShowSearch="v => showSearch.value = v" />
</template>
<script setup
lang="jsx">
import CustomTable from '../components/Table'
import { getDisplayScheduleDashboardList, getDisplayScheduleDashboardListArea } from '@/api'
import { getSixLittleDiamondsConfig } from './data.jsx'
import { parseTime } from '@/utils'
const { proxy } = getCurrentInstance()
/*************** 搜索列表 ***************/
const showSearch = ref(true)
/*************** 表格 ***************/
// 提交变更
const submitChange = async (row, col) => {
let requestObj = {}
if (col.requestKey) {
// 关联的公式计算列,需要特殊处理
for (const str of col.requestKey) {
const obj = getSixLittleDiamondsConfig().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 submitSixLittleDiamondsPlan({
id: row.sadjId,
[col.prop]: row[col.prop], // 当前修改列的值
...requestObj
})
}
// 全部列
const baseColumns = ref(getSixLittleDiamondsConfig(submitChange));
// 表格数据
const queryParams = reactive({
salesMonth: new Date(),
})
const tableData = ref([])
const isLoading = ref(true)
const total = ref(0)
// 筛选列表数据
const getTableList = async (operation) => {
isLoading.value = true
const res = await (operation === '大区战区-分析' ? getDisplayScheduleDashboardList : getDisplayScheduleDashboardListArea)({
...queryParams,
salesMonth: parseTime(queryParams.salesMonth, '{y}-{m}')
})
if (operation === '大区战区-分析') {
// 合并战区大区结构为扁平化
const { zq, dq } = res.data
tableData.value = [...zq, ...dq]
} else {
// 合并城市经理结构为扁平化
tableData.value = res.data
}
isLoading.value = false
}
const isDaQuZQ = proxy.checkPermi(['promotion:dashboard:list-show'])
getTableList(isDaQuZQ ? '大区战区-分析' : '城市经理-分析')
</script>
<style scoped></style>
\ No newline at end of file
<template>
<!-- 零食陈列 -->
<el-form :inline="true"
:model="queryParams"
class="demo-form-inline">
<el-form-item label="计划月份">
<el-date-picker v-model="queryParams.salesMonth"
type="month"
placeholder="选择计划月份"
@change="getTableList"
clearable />
</el-form-item>
</el-form>
<CustomTable :tableData="tableData"
:baseColumns="baseColumns"
:isLoading="isLoading"
@getTableList="getTableList"
@updateShowSearch="v => showSearch.value = v" />
</template>
<script setup
lang="jsx">
import CustomTable from '../components/Table'
import { getDisplayScheduleDashboardList, getDisplayScheduleDashboardListArea } from '@/api'
import { parseTime } from '@/utils'
import { getSnackCofing } from './data.jsx';
/*************** 操作类型 ***************/
// 提交变更
const submitChange = async (row, col) => {
// 需要特殊处理的
// 1. 实际主货架-形式,为空时,置空实际主货架-数量
// if (col.prop === 'actualMainShelfType' && !row.actualMainShelfType) {
// row.actualMainShelfQty = 0
// }
// 关联的公式计算列,需要特殊处理
for (const str of col.requestKey) {
const obj = getSnackCofing().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)
}
}
await submitSnackPlan({
id: row.sasdId,
[col.prop]: row[col.prop], // 当前修改列的值
...col.requestKey.reduce((acc, key) => ({ ...acc, [key]: row[key] }), {}), // 额外携带影响的列字段值
})
}
// 全部列
const baseColumns = ref(getSnackCofing(submitChange));
// 表格数据
const queryParams = reactive({
salesMonth: new Date(),
})
const tableData = ref([])
const isLoading = ref(true)
const total = ref(0)
// 筛选列表数据
const getTableList = async (operation) => {
isLoading.value = true
const res = await (operation === '大区战区-分析' ? getDisplayScheduleDashboardList : getDisplayScheduleDashboardListArea)({
...queryParams,
salesMonth: parseTime(queryParams.salesMonth, '{y}-{m}')
})
if (operation === '大区战区-分析') {
// 合并战区大区结构为扁平化
const { zq, dq } = res.data
tableData.value = [...zq, ...dq]
} else {
// 合并城市经理结构为扁平化
tableData.value = res.data
}
isLoading.value = false
}
const { proxy } = getCurrentInstance()
const isDaQuZQ = proxy.checkPermi(['promotion:dashboard:list-show'])
getTableList(isDaQuZQ ? '大区战区-分析' : '城市经理-分析')
</script>
<style scoped
lang="scss">
.container {
.el-tabs {
height: 100%;
display: flex;
flex-direction: column-reverse;
.el-tabs__content {
display: flex;
flex-direction: column;
height: 100%;
.el-tab-pane {
height: 100%;
display: flex;
flex-direction: column;
.pagination-container {
margin: 10px;
}
}
.formula-column {
display: flex;
justify-content: center;
align-items: center;
.el-icon {
margin-left: 2px;
}
}
}
}
.el-form-item {
align-items: center;
}
/* 表格区域 */
.auto-fit-header-table {
/* 优化超长文本的显示效果 */
.cell {
/* padding: 0 .2133rem; */
}
::v-deep(.el-table__row) {
.el-table__cell {
padding: 0;
}
}
::v-deep(.column-style) {
.cell-style {
/* margin: 0 -12px; */
>div {
display: flex;
flex-direction: column;
align-items: flex-start;
>span {
text-align: left;
text-indent: 5px;
display: inline-block;
width: 100%;
background-color: #e1e2e6;
border-bottom: 1px solid #ebeef5;
}
}
/* 表格内下拉框 */
.el-select {
width: 100% !important;
padding: 10px;
}
.el-input {
padding: 10px;
}
}
}
}
}
</style>
\ No newline at end of file
<template>
<!-- 三米两秒 -->
<el-form :inline="true"
:model="queryParams"
class="demo-form-inline">
<el-form-item label="计划月份">
<el-date-picker v-model="queryParams.salesMonth"
type="month"
placeholder="选择计划月份"
@change="getTableList"
clearable />
</el-form-item>
</el-form>
<CustomTable :tableData="tableData"
:baseColumns="baseColumns"
:isLoading="isLoading"
@getTableList="getTableList"
@updateShowSearch="v => showSearch.value = v" />
</template>
<script setup
lang="jsx">
import CustomTable from '../components/Table'
import { getDisplayScheduleDashboardList, getDisplayScheduleDashboardListArea } from '@/api'
import { getThreeTwoSecondsConfig } from './data.jsx'
import { parseTime } from '@/utils'
const { proxy } = getCurrentInstance()
/*************** 搜索列表 ***************/
const showSearch = ref(true)
/*************** 表格 ***************/
// 提交变更
const submitChange = async (row, col) => {
let requestObj = {}
if (col.requestKey) {
// 关联的公式计算列,需要特殊处理
for (const str of col.requestKey) {
const obj = getThreeTwoSecondsConfig().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 submitThreeMetersTwoSecondsPlan({
id: row.sadsId,
[col.prop]: row[col.prop], // 当前修改列的值
...requestObj
})
}
// 全部列
const baseColumns = ref(getThreeTwoSecondsConfig(submitChange));
// 表格数据
const queryParams = reactive({
salesMonth: new Date(),
})
const tableData = ref([])
const isLoading = ref(true)
const total = ref(0)
// 筛选列表数据
const getTableList = async (operation) => {
isLoading.value = true
const res = await (operation === '大区战区-分析' ? getDisplayScheduleDashboardList : getDisplayScheduleDashboardListArea)({
...queryParams,
salesMonth: parseTime(queryParams.salesMonth, '{y}-{m}')
})
if (operation === '大区战区-分析') {
// 合并战区大区结构为扁平化
const { zq, dq } = res.data
tableData.value = [...zq, ...dq]
} else {
// 合并城市经理结构为扁平化
tableData.value = res.data
}
isLoading.value = false
}
const isDaQuZQ = proxy.checkPermi(['promotion:dashboard:list-show'])
getTableList(isDaQuZQ ? '大区战区-分析' : '城市经理-分析')
</script>
<style scoped></style>
\ No newline at end of file
<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.
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论