1. 新增模块品类穿透
This commit is contained in:
2
.env.dev
2
.env.dev
@@ -5,7 +5,7 @@ VITE_DEV=true
|
|||||||
|
|
||||||
# 请求路径
|
# 请求路径
|
||||||
#VITE_BASE_URL='http://localhost:48080'
|
#VITE_BASE_URL='http://localhost:48080'
|
||||||
VITE_BASE_URL='http://118.253.178.8:48080'
|
VITE_BASE_URL='http://118.253.178.8:58080'
|
||||||
|
|
||||||
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
|
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
|
||||||
VITE_UPLOAD_TYPE=server
|
VITE_UPLOAD_TYPE=server
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ NODE_ENV=development
|
|||||||
VITE_DEV=true
|
VITE_DEV=true
|
||||||
|
|
||||||
# 请求路径
|
# 请求路径
|
||||||
VITE_BASE_URL='http://118.253.178.8:48080'
|
VITE_BASE_URL='http://118.253.178.8:58080'
|
||||||
#VITE_BASE_URL='http://localhost:48080'
|
#VITE_BASE_URL='http://localhost:48080'
|
||||||
|
|
||||||
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
|
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ NODE_ENV=production
|
|||||||
VITE_DEV=true
|
VITE_DEV=true
|
||||||
|
|
||||||
# 请求路径
|
# 请求路径
|
||||||
VITE_BASE_URL='http://118.253.178.8:48080'
|
VITE_BASE_URL='http://118.253.178.8:58080'
|
||||||
|
|
||||||
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
|
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
|
||||||
VITE_UPLOAD_TYPE=server
|
VITE_UPLOAD_TYPE=server
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<!-- 全局右下角悬浮按钮(参考 AIRBT.html 样式);登录页不展示 -->
|
<!-- Teleport 到 body:避免父級 transform/overflow 讓 position:fixed 參考錯容器而被裁切 -->
|
||||||
|
<Teleport to="body">
|
||||||
<div
|
<div
|
||||||
v-if="showAssistantFab"
|
v-if="showAssistantFab"
|
||||||
ref="fabRef"
|
ref="fabRef"
|
||||||
@@ -15,6 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<span class="fab-text">AI 决策助手</span>
|
<span class="fab-text">AI 决策助手</span>
|
||||||
</div>
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
|
||||||
<AiImageChatDialog
|
<AiImageChatDialog
|
||||||
v-model="dialogVisible"
|
v-model="dialogVisible"
|
||||||
@@ -34,6 +36,7 @@ import {
|
|||||||
} from './useAiAssistant'
|
} from './useAiAssistant'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
import { routeShouldShowAiAssistantFab } from './aiFabRoute'
|
||||||
|
|
||||||
defineOptions({ name: 'AiAssistantProvider' })
|
defineOptions({ name: 'AiAssistantProvider' })
|
||||||
|
|
||||||
@@ -212,6 +215,20 @@ function setAiAssistantFabEnabled(enabled: boolean) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
|
/** 依當前路由同步浮層(後台動態路由 name 常與組件 defineOptions 不一致,僅靠業務頁 setFab 易失效) */
|
||||||
|
watch(
|
||||||
|
() => route.fullPath,
|
||||||
|
() => {
|
||||||
|
if (isHideAssistantFabPath(route.path)) {
|
||||||
|
fabEnabled.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fabEnabled.value = routeShouldShowAiAssistantFab(route)
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
const showAssistantFab = computed(
|
const showAssistantFab = computed(
|
||||||
() => !isHideAssistantFabPath(route.path) && fabEnabled.value
|
() => !isHideAssistantFabPath(route.path) && fabEnabled.value
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -865,7 +865,6 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
|||||||
name: 'SalesDailyCategoryPenetration',
|
name: 'SalesDailyCategoryPenetration',
|
||||||
meta: {
|
meta: {
|
||||||
title: '品类穿透看板',
|
title: '品类穿透看板',
|
||||||
noCache: true,
|
|
||||||
hidden: true,
|
hidden: true,
|
||||||
canTo: true
|
canTo: true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -884,6 +884,7 @@ import { Icon } from '@/components/Icon'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import type { CategoryDiagnosticData } from './components/categoryCardListComponents.vue'
|
import type { CategoryDiagnosticData } from './components/categoryCardListComponents.vue'
|
||||||
import { useAiAssistant } from '@/components/AiAssistant/useAiAssistant'
|
import { useAiAssistant } from '@/components/AiAssistant/useAiAssistant'
|
||||||
|
import { routeKeepsAiAssistantFab } from '@/components/AiAssistant/aiFabRoute'
|
||||||
|
|
||||||
defineOptions({ name: 'ProductDashboard' })
|
defineOptions({ name: 'ProductDashboard' })
|
||||||
|
|
||||||
@@ -2876,7 +2877,11 @@ onActivated(() => {
|
|||||||
setAiAssistantFabEnabled(true)
|
setAiAssistantFabEnabled(true)
|
||||||
})
|
})
|
||||||
onDeactivated(() => {
|
onDeactivated(() => {
|
||||||
|
void nextTick(() => {
|
||||||
|
if (!routeKeepsAiAssistantFab(router.currentRoute.value)) {
|
||||||
setAiAssistantFabEnabled(false)
|
setAiAssistantFabEnabled(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
saveDataCache()
|
saveDataCache()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -501,6 +501,7 @@ import { useAiModulePromptEditor } from '@/hooks/web/useAiModulePromptEditor'
|
|||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { AiModulePromptApi } from '@/api/ydoyun/aiModulePrompt'
|
import { AiModulePromptApi } from '@/api/ydoyun/aiModulePrompt'
|
||||||
import AiPromptEditDialog from '../lijun/reportpage6/components/AiPromptEditDialog.vue'
|
import AiPromptEditDialog from '../lijun/reportpage6/components/AiPromptEditDialog.vue'
|
||||||
|
import { routeKeepsAiAssistantFab } from '@/components/AiAssistant/aiFabRoute'
|
||||||
|
|
||||||
defineOptions({ name: 'ProductDaily' })
|
defineOptions({ name: 'ProductDaily' })
|
||||||
|
|
||||||
@@ -925,7 +926,11 @@ onActivated(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onDeactivated(() => {
|
onDeactivated(() => {
|
||||||
|
void nextTick(() => {
|
||||||
|
if (!routeKeepsAiAssistantFab(router.currentRoute.value)) {
|
||||||
setAiAssistantFabEnabled(false)
|
setAiAssistantFabEnabled(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="plct-page">
|
<div ref="plctPageRootRef" class="plct-page">
|
||||||
<div class="plct-toolbar card">
|
<div class="plct-toolbar card">
|
||||||
<el-form :model="queryForm" class="plct-query-form" inline @submit.prevent="handleQuery">
|
<el-form :model="queryForm" class="plct-query-form" inline @submit.prevent="handleQuery">
|
||||||
<el-form-item label="日期">
|
<el-form-item label="日期">
|
||||||
@@ -209,15 +209,6 @@
|
|||||||
min-width="68"
|
min-width="68"
|
||||||
show-overflow-tooltip
|
show-overflow-tooltip
|
||||||
/>
|
/>
|
||||||
<el-table-column
|
|
||||||
label="销量"
|
|
||||||
align="right"
|
|
||||||
width="64"
|
|
||||||
label-class-name="plct-jj-hd-sl"
|
|
||||||
class-name="plct-jj-col-sl"
|
|
||||||
>
|
|
||||||
<template #default="{ row }">{{ formatJjInt(row.sl) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column
|
<el-table-column
|
||||||
label="库存"
|
label="库存"
|
||||||
align="right"
|
align="right"
|
||||||
@@ -227,6 +218,15 @@
|
|||||||
>
|
>
|
||||||
<template #default="{ row }">{{ formatJjInt(row.kc) }}</template>
|
<template #default="{ row }">{{ formatJjInt(row.kc) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
label="销量"
|
||||||
|
align="right"
|
||||||
|
width="64"
|
||||||
|
label-class-name="plct-jj-hd-sl"
|
||||||
|
class-name="plct-jj-col-sl"
|
||||||
|
>
|
||||||
|
<template #default="{ row }">{{ formatJjInt(row.sl) }}</template>
|
||||||
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -276,14 +276,14 @@
|
|||||||
<span class="plct3-size-dot plct3-size-dot--muted" aria-hidden="true"></span>
|
<span class="plct3-size-dot plct3-size-dot--muted" aria-hidden="true"></span>
|
||||||
尺码
|
尺码
|
||||||
</span>
|
</span>
|
||||||
<span class="plct3-size-legend-i">
|
|
||||||
<span class="plct3-size-dot plct3-size-dot--kc" aria-hidden="true"></span>
|
|
||||||
库存
|
|
||||||
</span>
|
|
||||||
<span class="plct3-size-legend-i">
|
<span class="plct3-size-legend-i">
|
||||||
<span class="plct3-size-dot plct3-size-dot--ls" aria-hidden="true"></span>
|
<span class="plct3-size-dot plct3-size-dot--ls" aria-hidden="true"></span>
|
||||||
零售
|
零售
|
||||||
</span>
|
</span>
|
||||||
|
<span class="plct3-size-legend-i">
|
||||||
|
<span class="plct3-size-dot plct3-size-dot--kc" aria-hidden="true"></span>
|
||||||
|
库存
|
||||||
|
</span>
|
||||||
<span class="plct3-size-legend-i">
|
<span class="plct3-size-legend-i">
|
||||||
<span class="plct3-size-dot plct3-size-dot--zt" aria-hidden="true"></span>
|
<span class="plct3-size-dot plct3-size-dot--zt" aria-hidden="true"></span>
|
||||||
状态
|
状态
|
||||||
@@ -294,8 +294,8 @@
|
|||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<template v-if="col.type === 'sku'">
|
<template v-if="col.type === 'sku'">
|
||||||
<div class="plct3-sku-merged">
|
<div class="plct3-sku-merged">
|
||||||
<div class="plct3-sku-code">{{ plct3FormatCell(row[col.spdmKey]) }}</div>
|
|
||||||
<div class="plct3-sku-name">{{ plct3FormatCell(row[col.spmcKey]) }}</div>
|
<div class="plct3-sku-name">{{ plct3FormatCell(row[col.spmcKey]) }}</div>
|
||||||
|
<div class="plct3-sku-code">{{ plct3FormatCell(row[col.spdmKey]) }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="col.type === 'sizeSku'">
|
<template v-else-if="col.type === 'sizeSku'">
|
||||||
@@ -319,9 +319,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="plct3-size-chip-vals">
|
<div class="plct3-size-chip-vals">
|
||||||
<template v-if="t.kc != null || t.ls != null">
|
<template v-if="t.kc != null || t.ls != null">
|
||||||
<span v-if="t.kc != null" class="plct3-size-kc">{{ t.kc }}</span>
|
|
||||||
<span v-if="t.kc != null && t.ls != null" class="plct3-size-sep">|</span>
|
|
||||||
<span v-if="t.ls != null" class="plct3-size-ls">{{ t.ls }}</span>
|
<span v-if="t.ls != null" class="plct3-size-ls">{{ t.ls }}</span>
|
||||||
|
<span v-if="t.kc != null && t.ls != null" class="plct3-size-sep">|</span>
|
||||||
|
<span v-if="t.kc != null" class="plct3-size-kc">{{ t.kc }}</span>
|
||||||
</template>
|
</template>
|
||||||
<span v-else class="plct3-size-dash">—</span>
|
<span v-else class="plct3-size-dash">—</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -369,16 +369,42 @@
|
|||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import type { EChartsOption } from 'echarts'
|
import type { EChartsOption } from 'echarts'
|
||||||
import { useCssVar } from '@vueuse/core'
|
import { useCssVar } from '@vueuse/core'
|
||||||
import { computed, onActivated, onMounted, reactive, ref, watch } from 'vue'
|
import {
|
||||||
import { useRoute } from 'vue-router'
|
computed,
|
||||||
|
nextTick,
|
||||||
|
onActivated,
|
||||||
|
onBeforeUnmount,
|
||||||
|
onDeactivated,
|
||||||
|
onMounted,
|
||||||
|
reactive,
|
||||||
|
ref,
|
||||||
|
watch
|
||||||
|
} from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ReportApi, type ExecuteProcedureParams } from '@/api/ydoyun/report/reportpage'
|
import { ReportApi, type ExecuteProcedureParams } from '@/api/ydoyun/report/reportpage'
|
||||||
import { useAppStore } from '@/store/modules/app'
|
import { useAppStore } from '@/store/modules/app'
|
||||||
|
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||||
import { useUserStore } from '@/store/modules/user'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
import Echart from '@/components/Echart/src/Echart.vue'
|
import Echart from '@/components/Echart/src/Echart.vue'
|
||||||
|
import { useAiAssistant } from '@/components/AiAssistant/useAiAssistant'
|
||||||
|
import { routeKeepsAiAssistantFab } from '@/components/AiAssistant/aiFabRoute'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
defineOptions({ name: 'SalesDailyCategoryPenetration' })
|
defineOptions({ name: 'SalesDailyCategoryPenetration' })
|
||||||
|
|
||||||
|
/** 與 ProductDashboard 一致:SPA 內是否已做過初次初始化;配合 window 快取,不依賴 keep-alive */
|
||||||
|
const INITIAL_QUERY_FLAG_KEY = '__SalesDailyPlct_initialQueryDone'
|
||||||
|
const DATA_CACHE_KEY = '__SalesDailyPlct_dataCache'
|
||||||
|
|
||||||
|
/** 關閉分頁或刷新時清除,下次打開重新完整查詢(SPA 內切路由不會觸發 beforeunload) */
|
||||||
|
function clearPlctFlagsOnLeave() {
|
||||||
|
const win = window as unknown as Record<string, unknown>
|
||||||
|
try {
|
||||||
|
delete win[INITIAL_QUERY_FLAG_KEY]
|
||||||
|
delete win[DATA_CACHE_KEY]
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
const REPORT_ID = 1
|
const REPORT_ID = 1
|
||||||
const PROCEDURE_SECRET = '123'
|
const PROCEDURE_SECRET = '123'
|
||||||
const PROC_QX_KEHU = 'YDY_AI_GET_QX_KEHU'
|
const PROC_QX_KEHU = 'YDY_AI_GET_QX_KEHU'
|
||||||
@@ -390,6 +416,27 @@ const PROC_PLCT3 = 'YDY_AI_GET_PLCT3'
|
|||||||
const PLCT3_HIDDEN_KEYS = /^(ysdm)$/i
|
const PLCT3_HIDDEN_KEYS = /^(ysdm)$/i
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
/** AI 決策助手:須在本頁顯式開啟;從銷售日報跳入時上一頁 onDeactivated 會關閉懸浮按鈕,此處需重新打開 */
|
||||||
|
const PLCT_AI_MODULE_KEY = 'SalesDailyCategoryPenetration:main'
|
||||||
|
const aiAssistant = useAiAssistant()
|
||||||
|
const { setAiAssistantFabEnabled } = aiAssistant
|
||||||
|
const plctPageRootRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
const aiMenuModuleName = computed(() => {
|
||||||
|
const t = route.meta?.title
|
||||||
|
return typeof t === 'string' && t.trim() ? t.trim() : '品类穿透看板'
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
aiMenuModuleName,
|
||||||
|
(name) => {
|
||||||
|
aiAssistant.setPageModuleName(name)
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
const username = computed(() => userStore.user.username || '')
|
const username = computed(() => userStore.user.username || '')
|
||||||
@@ -488,6 +535,77 @@ function syncFormFromRoute() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 路由穿透條件快照:用於與上次成功拉數後對比,避免 keep-alive 切回無條件重查(對齊 ProductDashboard 思路) */
|
||||||
|
function snapshotRouteQueryKey(): string {
|
||||||
|
return [
|
||||||
|
pickRouteQuery(route.query.rq),
|
||||||
|
pickRouteQuery(route.query.ckdm),
|
||||||
|
pickRouteQuery(route.query.pp),
|
||||||
|
pickRouteQuery(route.query.ppmc)
|
||||||
|
].join('|')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 最近一次 loadDetail 完成後對應的路由 query 快照(切回分頁時若與當前路由一致則不重查) */
|
||||||
|
const lastPlctRouteKeyAfterFetch = ref('')
|
||||||
|
|
||||||
|
const plctBootstrapDone = ref(false)
|
||||||
|
|
||||||
|
function isPlctCacheRestorable(data: unknown): data is Record<string, any> {
|
||||||
|
if (!data || typeof data !== 'object') return false
|
||||||
|
const d = data as Record<string, any>
|
||||||
|
if (String(d.routeQueryKey || '') !== snapshotRouteQueryKey()) return false
|
||||||
|
return d.queried === true
|
||||||
|
}
|
||||||
|
|
||||||
|
function savePlctDataCache() {
|
||||||
|
const win = window as unknown as Record<string, any>
|
||||||
|
try {
|
||||||
|
win[DATA_CACHE_KEY] = {
|
||||||
|
routeQueryKey: snapshotRouteQueryKey(),
|
||||||
|
queried: queried.value,
|
||||||
|
detail: detail.value ? JSON.parse(JSON.stringify(detail.value)) : null,
|
||||||
|
plct2Rows: JSON.parse(JSON.stringify(plct2Rows.value)),
|
||||||
|
plct3Rows: JSON.parse(JSON.stringify(plct3Rows.value)),
|
||||||
|
plct3SkuSizeLines: JSON.parse(JSON.stringify(plct3SkuSizeLines.value)),
|
||||||
|
queryForm: JSON.parse(JSON.stringify(queryForm)),
|
||||||
|
brandOptions: JSON.parse(JSON.stringify(brandOptions.value)),
|
||||||
|
userStoreRows: JSON.parse(JSON.stringify(userStoreRows.value))
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function restorePlctFromWindowCache(data: Record<string, any>) {
|
||||||
|
if (Array.isArray(data.userStoreRows)) userStoreRows.value = data.userStoreRows
|
||||||
|
if (Array.isArray(data.brandOptions)) brandOptions.value = data.brandOptions
|
||||||
|
if (data.queryForm && typeof data.queryForm === 'object') {
|
||||||
|
Object.assign(queryForm, data.queryForm)
|
||||||
|
}
|
||||||
|
queried.value = !!data.queried
|
||||||
|
detail.value = data.detail && typeof data.detail === 'object' ? data.detail : null
|
||||||
|
plct2Rows.value = Array.isArray(data.plct2Rows) ? data.plct2Rows : []
|
||||||
|
plct3Rows.value = Array.isArray(data.plct3Rows) ? data.plct3Rows : []
|
||||||
|
plct3SkuSizeLines.value = Array.isArray(data.plct3SkuSizeLines) ? data.plct3SkuSizeLines : []
|
||||||
|
lastPlctRouteKeyAfterFetch.value = String(data.routeQueryKey || snapshotRouteQueryKey())
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRefetchPlctFromRouteIfChanged() {
|
||||||
|
const cur = snapshotRouteQueryKey()
|
||||||
|
const rqQ = pickRouteQuery(route.query.rq)
|
||||||
|
const ckdmQ = pickRouteQuery(route.query.ckdm)
|
||||||
|
const ppQ = pickRouteQuery(route.query.pp)
|
||||||
|
const ppmcQ = pickRouteQuery(route.query.ppmc)
|
||||||
|
if (!rqQ || !ckdmQ || !(ppQ || ppmcQ)) return
|
||||||
|
if (cur === lastPlctRouteKeyAfterFetch.value) return
|
||||||
|
syncFormFromRoute()
|
||||||
|
ensureSelectionsAfterSync()
|
||||||
|
const rq = String(queryForm.rq || '').trim()
|
||||||
|
const ckdm = String(queryForm.ckdm || '').trim()
|
||||||
|
const pp = String(queryForm.pp || '').trim()
|
||||||
|
if (rq && ckdm && pp) {
|
||||||
|
void handleQuery()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function normalizePercent(v: any): number {
|
function normalizePercent(v: any): number {
|
||||||
const n = toFiniteNumber(v)
|
const n = toFiniteNumber(v)
|
||||||
if (!Number.isFinite(n)) return NaN as unknown as number
|
if (!Number.isFinite(n)) return NaN as unknown as number
|
||||||
@@ -2084,12 +2202,15 @@ async function loadDetail() {
|
|||||||
plct3SkuSizeLines.value = []
|
plct3SkuSizeLines.value = []
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
if (rq && ckdm && pp) {
|
||||||
|
lastPlctRouteKeyAfterFetch.value = snapshotRouteQueryKey()
|
||||||
|
savePlctDataCache()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 初次 / 路由同步後:路由帶齊 rq+ckdm+品牌(pp 或 ppmc)且表單已解析則自動查詢 */
|
/** 同步路由條件到表單;路徑參數齊全且表單可解析時自動查詢(僅在需要打接口時調用) */
|
||||||
async function bootstrapPenetrationFromRoute() {
|
async function syncFormAndAutoQueryIfRouteReady() {
|
||||||
await Promise.all([loadUserStores(), loadBrandOptions()])
|
|
||||||
syncFormFromRoute()
|
syncFormFromRoute()
|
||||||
ensureSelectionsAfterSync()
|
ensureSelectionsAfterSync()
|
||||||
|
|
||||||
@@ -2107,30 +2228,73 @@ async function bootstrapPenetrationFromRoute() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 與 ProductDashboard 相同套路:首次進入完整拉選項+可能自動查;再次進入實例時若有 window 快取且與當前 URL 一致則還原不查 */
|
||||||
* 與 SalesDailyAiReport 一致:從 keep-alive 再次進入時有完整條件則重拉穿透數據。
|
|
||||||
* 首次掛載已由 onMounted 走 bootstrap,此處跳過一次避免雙次請求。
|
|
||||||
*/
|
|
||||||
let skipPlctActivateRefetchOnce = true
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await bootstrapPenetrationFromRoute()
|
const win = window as unknown as Record<string, any>
|
||||||
|
const isFirstEnter = !win[INITIAL_QUERY_FLAG_KEY]
|
||||||
|
window.addEventListener('beforeunload', clearPlctFlagsOnLeave)
|
||||||
|
|
||||||
|
if (isFirstEnter) {
|
||||||
|
await Promise.all([loadUserStores(), loadBrandOptions()])
|
||||||
|
win[INITIAL_QUERY_FLAG_KEY] = true
|
||||||
|
await syncFormAndAutoQueryIfRouteReady()
|
||||||
|
} else {
|
||||||
|
const dataCache = win[DATA_CACHE_KEY]
|
||||||
|
if (isPlctCacheRestorable(dataCache)) {
|
||||||
|
restorePlctFromWindowCache(dataCache)
|
||||||
|
} else {
|
||||||
|
await Promise.all([loadUserStores(), loadBrandOptions()])
|
||||||
|
await syncFormAndAutoQueryIfRouteReady()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plctBootstrapDone.value = true
|
||||||
|
|
||||||
|
setAiAssistantFabEnabled(true)
|
||||||
|
aiAssistant.setPageModuleCode(PLCT_AI_MODULE_KEY)
|
||||||
|
await nextTick()
|
||||||
|
if (plctPageRootRef.value) aiAssistant.setScreenshotTarget(plctPageRootRef.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
onActivated(() => {
|
onBeforeUnmount(() => {
|
||||||
if (skipPlctActivateRefetchOnce) {
|
window.removeEventListener('beforeunload', clearPlctFlagsOnLeave)
|
||||||
skipPlctActivateRefetchOnce = false
|
savePlctDataCache()
|
||||||
return
|
setAiAssistantFabEnabled(false)
|
||||||
}
|
aiAssistant.setPageModuleName(null)
|
||||||
syncFormFromRoute()
|
aiAssistant.setPageModuleCode(null)
|
||||||
ensureSelectionsAfterSync()
|
aiAssistant.setScreenshotTarget(null)
|
||||||
const rq = String(queryForm.rq || '').trim()
|
const tagsViewStore = useTagsViewStore()
|
||||||
const ckdm = String(queryForm.ckdm || '').trim()
|
if (tagsViewStore.getVisitedViews.length <= 1) {
|
||||||
const pp = String(queryForm.pp || '').trim()
|
clearPlctFlagsOnLeave()
|
||||||
if (rq && ckdm && pp) {
|
|
||||||
void handleQuery()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onDeactivated(() => {
|
||||||
|
savePlctDataCache()
|
||||||
|
void nextTick(() => {
|
||||||
|
if (!routeKeepsAiAssistantFab(router.currentRoute.value)) {
|
||||||
|
setAiAssistantFabEnabled(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/** keep-alive 切回:僅當 URL 與上次拉數不一致時再同步並查詢(與 ProductDashboard 的 onActivated 不重查相輔) */
|
||||||
|
onActivated(() => {
|
||||||
|
setAiAssistantFabEnabled(true)
|
||||||
|
void nextTick(() => {
|
||||||
|
if (plctPageRootRef.value) aiAssistant.setScreenshotTarget(plctPageRootRef.value)
|
||||||
|
})
|
||||||
|
maybeRefetchPlctFromRouteIfChanged()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => snapshotRouteQueryKey(),
|
||||||
|
(_key, oldKey) => {
|
||||||
|
if (!plctBootstrapDone.value) return
|
||||||
|
if (oldKey === undefined || oldKey === _key) return
|
||||||
|
maybeRefetchPlctFromRouteIfChanged()
|
||||||
|
}
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -2250,6 +2414,9 @@ onActivated(() => {
|
|||||||
/** 與左欄「當日指標」外框等高 */
|
/** 與左欄「當日指標」外框等高 */
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: var(--plct-section-gap);
|
gap: var(--plct-section-gap);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plct-kpi-col,
|
.plct-kpi-col,
|
||||||
@@ -2263,6 +2430,8 @@ onActivated(() => {
|
|||||||
|
|
||||||
.plct-charts-col {
|
.plct-charts-col {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plct-kpi-col > .plct-upper-pane {
|
.plct-kpi-col > .plct-upper-pane {
|
||||||
@@ -2510,15 +2679,32 @@ onActivated(() => {
|
|||||||
@media (max-width: 1180px) {
|
@media (max-width: 1180px) {
|
||||||
.plct-content-split {
|
.plct-content-split {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直向堆疊時勿再對兩塊使用 flex:1 1 0 均分父級高度,否則在矮視窗下會把「當日指標」與
|
||||||
|
* 圖表區壓扁並被 overflow:hidden 裁掉,窄屏看起來像「兩塊都沒了」。
|
||||||
|
*/
|
||||||
|
.plct-kpi-col,
|
||||||
|
.plct-charts-col {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plct-charts-col {
|
.plct-charts-col {
|
||||||
height: auto;
|
height: auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plct-charts-shell.plct-upper-pane {
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plct-kpi-col .plct-upper-pane--kpi {
|
.plct-kpi-col .plct-upper-pane--kpi {
|
||||||
min-height: 340px;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plct-chart-grid {
|
.plct-chart-grid {
|
||||||
@@ -2527,6 +2713,18 @@ onActivated(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 平板寬度:四宮格改單欄,避免兩列餅圖擠在過窄寬度內被裁切 */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.plct-chart-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: repeat(4, minmax(160px, auto));
|
||||||
|
height: auto;
|
||||||
|
max-height: none;
|
||||||
|
min-height: 0;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.plct-chart-grid {
|
.plct-chart-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -2536,6 +2734,43 @@ onActivated(() => {
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.plct-page {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plct-field-rq {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plct-field-ckdm,
|
||||||
|
.plct-field-pp {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plct-query-form {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
|
||||||
|
:deep(.el-form--inline .el-form-item) {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-form--inline .el-form-item__label) {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-form--inline .el-form-item__content) {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.plct-bullet-row {
|
.plct-bullet-row {
|
||||||
|
|||||||
@@ -373,6 +373,7 @@ import { useAiModulePromptEditor } from '@/hooks/web/useAiModulePromptEditor'
|
|||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { AiModulePromptApi } from '@/api/ydoyun/aiModulePrompt'
|
import { AiModulePromptApi } from '@/api/ydoyun/aiModulePrompt'
|
||||||
import AiPromptEditDialog from '../lijun/reportpage6/components/AiPromptEditDialog.vue'
|
import AiPromptEditDialog from '../lijun/reportpage6/components/AiPromptEditDialog.vue'
|
||||||
|
import { routeKeepsAiAssistantFab } from '@/components/AiAssistant/aiFabRoute'
|
||||||
|
|
||||||
defineOptions({ name: 'SalesDailyAiReport' })
|
defineOptions({ name: 'SalesDailyAiReport' })
|
||||||
|
|
||||||
@@ -758,22 +759,17 @@ watch(weatherPrimaryStackRef, () => nextTick(bindWeatherPrimaryStackResizeObserv
|
|||||||
flush: 'post'
|
flush: 'post'
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 與 onMounted 一致:從 keep-alive 再次進入時重拉報表(避免分頁切回仍為舊數據);首次掛載已由 onMounted 查詢,此處跳過一次 */
|
/** 與 ProductDashboard 一致:keep-alive 切回本頁不重拉接口,由緩存實例保留表單與報表數據 */
|
||||||
let skipSalesDailyActivateRefetchOnce = true
|
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
setAiAssistantFabEnabled(true)
|
setAiAssistantFabEnabled(true)
|
||||||
if (skipSalesDailyActivateRefetchOnce) {
|
|
||||||
skipSalesDailyActivateRefetchOnce = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (queryParams.ckdm && queryParams.rq) {
|
|
||||||
void handleQuery()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onDeactivated(() => {
|
onDeactivated(() => {
|
||||||
|
void nextTick(() => {
|
||||||
|
if (!routeKeepsAiAssistantFab(router.currentRoute.value)) {
|
||||||
setAiAssistantFabEnabled(false)
|
setAiAssistantFabEnabled(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user