1. 新增模块品类穿透

This commit is contained in:
2026-05-08 08:27:39 +08:00
parent 9d7ac679a0
commit 1967e97074
7 changed files with 352 additions and 322 deletions

View File

@@ -1,148 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 决策通</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.ai-drawer { transition: all 0.5s cubic-bezier(0.16, 1, 0.3, 1); transform: translateX(110%); }
.ai-drawer.active { transform: translateX(0); }
.ai-drawer.fullscreen { width: 85vw !important; height: 80vh !important; right: 7.5vw !important; top: 10vh !important; border-radius: 1.5rem !important; transform: translateX(0); }
.typing::after { content: '|'; animation: blink 1s infinite; margin-left: 2px; color: #3B82F6; }
@keyframes blink { 50% { opacity: 0; } }
.custom-scroll::-webkit-scrollbar { width: 5px; }
.custom-scroll::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
#overlay { display: none; position: fixed; inset: 0; background: rgba(15, 23, 42, 0.4); backdrop-filter: blur(4px); z-index: 55; }
#overlay.active { display: block; }
#fileInput, #imageInput { display: none; }
</style>
</head>
<body class="bg-[#F1F5F9]">
<div id="overlay" onclick="closeAI()"></div>
<div class="fixed bottom-[85px] right-6 z-50" id="aiFab">
<button onclick="openAI()" class="flex items-center gap-2 bg-white border border-blue-100 px-5 py-3 rounded-full shadow-2xl hover:scale-105 transition-all group">
<div class="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center text-white shadow-lg shadow-blue-200">
<i class="fas fa-robot text-sm"></i>
</div>
<span class="text-blue-600 font-bold text-sm tracking-wide">AI 决策助手</span>
</button>
</div>
<div id="aiPanel" class="fixed top-0 right-0 h-full w-[420px] bg-white shadow-2xl z-[60] ai-drawer flex flex-col overflow-hidden border-l border-slate-100">
<div class="px-6 py-4 border-b flex items-center justify-between bg-white shrink-0">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-blue-600 rounded-xl flex items-center justify-center text-white shadow-md shadow-blue-100">
<i class="fas fa-robot text-lg"></i>
</div>
<div>
<h3 class="font-bold text-slate-800 text-sm">AI 决策助手</h3>
</div>
</div>
<div class="flex items-center gap-1">
<button onclick="toggleFullscreen()" class="w-9 h-9 rounded-lg hover:bg-slate-100 flex items-center justify-center text-slate-500">
<i class="fas fa-expand-alt" id="expandIcon"></i>
</button>
<button onclick="closeAI()" class="w-9 h-9 rounded-lg hover:bg-red-50 flex items-center justify-center text-slate-400 hover:text-red-500">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<div id="chatBox" class="flex-1 overflow-y-auto p-6 space-y-6 bg-[#FBFCFE] custom-scroll">
<div class="flex gap-4 items-start">
<div class="w-8 h-8 bg-blue-50 text-blue-600 rounded-lg flex items-center justify-center shrink-0 mt-1 border border-blue-100">
<i class="fas fa-robot text-xs"></i>
</div>
<div class="bg-white border border-slate-200 p-4 rounded-2xl rounded-tl-none shadow-sm text-sm text-slate-700">
您好!请发送数据或输入指令。
</div>
</div>
</div>
<div class="p-4 bg-white border-t border-slate-100 shrink-0">
<div class="max-w-4xl mx-auto flex flex-col">
<div class="flex gap-1 mb-2">
<input type="file" id="fileInput" accept=".xls,.xlsx,.csv" onchange="handleFileUpload(event, 'excel')">
<input type="file" id="imageInput" accept="image/*" onchange="handleFileUpload(event, 'image')">
<button onclick="triggerInput('image')" class="w-8 h-8 flex items-center justify-center text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded-md transition-all">
<i class="fas fa-image"></i>
</button>
<button onclick="triggerInput('excel')" class="w-8 h-8 flex items-center justify-center text-slate-400 hover:text-emerald-600 hover:bg-emerald-50 rounded-md transition-all">
<i class="fas fa-file-excel"></i>
</button>
</div>
<div class="flex items-center gap-3 bg-slate-100 rounded-2xl px-5 py-1.5 border border-transparent focus-within:bg-white focus-within:border-blue-500 transition-all">
<input id="chatInput" type="text" placeholder="输入指令..."
class="flex-1 bg-transparent py-2.5 text-sm outline-none text-slate-700 font-medium"
onkeypress="if(event.key==='Enter') sendMsg()">
<button onclick="sendMsg()" class="w-9 h-9 bg-blue-600 text-white rounded-xl flex items-center justify-center hover:bg-blue-700 shadow-lg shadow-blue-100 active:scale-95 transition-all">
<i class="fas fa-paper-plane text-xs"></i>
</button>
</div>
</div>
</div>
</div>
<script>
const panel = document.getElementById('aiPanel');
const chatBox = document.getElementById('chatBox');
const input = document.getElementById('chatInput');
const overlay = document.getElementById('overlay');
const fab = document.getElementById('aiFab');
const icon = document.getElementById('expandIcon');
function openAI() { panel.classList.add('active'); overlay.classList.add('active'); fab.style.opacity = '0'; }
function closeAI() { panel.classList.remove('active'); panel.classList.remove('fullscreen'); overlay.classList.remove('active'); fab.style.opacity = '1'; icon.className = 'fas fa-expand-alt'; }
function toggleFullscreen() { panel.classList.toggle('fullscreen'); icon.className = panel.classList.contains('fullscreen') ? 'fas fa-compress-alt' : 'fas fa-expand-alt'; }
function triggerInput(type) { document.getElementById(type === 'image' ? 'imageInput' : 'fileInput').click(); }
function handleFileUpload(event, type) {
const file = event.target.files[0];
if (!file) return;
const contentHtml = type === 'image' ? `<img src="${URL.createObjectURL(file)}" class="rounded-lg max-w-full">` : `<div class="flex items-center gap-3 bg-white/10 p-3 rounded-xl border border-white/20"><div class="w-8 h-8 bg-emerald-500 rounded flex items-center justify-center text-white shrink-0"><i class="fas fa-file-excel text-xs"></i></div><p class="text-xs font-bold truncate">${file.name}</p></div>`;
const msgHtml = `<div class="flex justify-end"><div class="bg-blue-600 text-white p-3 rounded-2xl rounded-tr-none text-sm max-w-[85%] shadow-lg">${contentHtml}</div></div>`;
chatBox.insertAdjacentHTML('beforeend', msgHtml);
chatBox.scrollTop = chatBox.scrollHeight;
setTimeout(() => {
const aiId = 'ai-' + Date.now();
const aiHtml = `<div class="flex gap-4 items-start"><div class="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center text-white shrink-0 mt-1 shadow-sm"><i class="fas fa-robot text-xs"></i></div><div class="bg-white border border-slate-200 p-4 rounded-2xl rounded-tl-none shadow-sm text-sm text-slate-700 flex-1"><p id="${aiId}" class="typing leading-relaxed"></p></div></div>`;
chatBox.insertAdjacentHTML('beforeend', aiHtml);
chatBox.scrollTop = chatBox.scrollHeight;
typeWriter(`正在分析您的文件数据...`, aiId);
}, 800);
}
function sendMsg() {
const text = input.value.trim();
if(!text) return;
const userHtml = `<div class="flex justify-end"><div class="bg-blue-600 text-white p-4 rounded-2xl rounded-tr-none text-sm max-w-[80%] shadow-lg shadow-blue-100/50">${text}</div></div>`;
chatBox.insertAdjacentHTML('beforeend', userHtml);
input.value = '';
chatBox.scrollTop = chatBox.scrollHeight;
setTimeout(() => {
const aiId = 'ai-' + Date.now();
const aiHtml = `<div class="flex gap-4 items-start"><div class="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center text-white shrink-0 mt-1 border border-blue-100"><i class="fas fa-robot text-xs"></i></div><div class="bg-white border border-slate-200 p-4 rounded-2xl rounded-tl-none shadow-sm text-sm text-slate-700 flex-1"><p id="${aiId}" class="typing leading-relaxed"></p></div></div>`;
chatBox.insertAdjacentHTML('beforeend', aiHtml);
chatBox.scrollTop = chatBox.scrollHeight;
typeWriter(`正在处理您的指令...`, aiId);
}, 600);
}
function typeWriter(text, elementId) {
let i = 0;
const el = document.getElementById(elementId);
function type() { if (i < text.length) { el.innerHTML += text.charAt(i); i++; chatBox.scrollTop = chatBox.scrollHeight; setTimeout(type, 30); } else { el.classList.remove('typing'); } }
type();
}
</script>
</body>
</html>

View File

@@ -21,6 +21,8 @@ export interface ExecuteProcedureParams {
* 注意与 axios 的 `params` 配置重名,此处为查询参数字段名 * 注意与 axios 的 `params` 配置重名,此处为查询参数字段名
*/ */
params?: string params?: string
/** 存储过程入参 @ztj如 YDY_AI_GET_SPTPQ 正特价维度) */
ztj?: string
} }
/** 日报表-当日总销售数据 */ /** 日报表-当日总销售数据 */

View File

@@ -56,7 +56,9 @@ const styles = computed(() => {
return { return {
width, width,
height height,
/** 嵌套 flex 時避免高度鏈斷裂導致圖表區變矮、模組下半截空白 */
minHeight: height === '100%' ? '100%' : undefined
} }
}) })

View File

@@ -775,7 +775,10 @@ const remainingRouter: AppRouteRecordRaw[] = [
hidden: true, hidden: true,
canTo: true canTo: true
}, },
component: () => import('@/views/ydoyun/report/lijun/reportpage6/components/categoryCardListComponents.vue') component: () =>
import(
'@/views/ydoyun/report/lijun/reportpage6/components/categoryCardListComponents.vue'
)
}, },
{ {
path: 'lijun/supplier-ranking', path: 'lijun/supplier-ranking',
@@ -786,7 +789,8 @@ const remainingRouter: AppRouteRecordRaw[] = [
hidden: true, hidden: true,
canTo: true canTo: true
}, },
component: () => import('@/views/ydoyun/report/lijun/reportpage6/components/SupplierRanking.vue') component: () =>
import('@/views/ydoyun/report/lijun/reportpage6/components/SupplierRanking.vue')
}, },
{ {
path: 'lijun/supplier-performance', path: 'lijun/supplier-performance',
@@ -796,7 +800,8 @@ const remainingRouter: AppRouteRecordRaw[] = [
hidden: true, hidden: true,
canTo: true canTo: true
}, },
component: () => import('@/views/ydoyun/report/lijun/reportpage6/components/SupplierPerformancePage.vue') component: () =>
import('@/views/ydoyun/report/lijun/reportpage6/components/SupplierPerformancePage.vue')
}, },
{ {
path: 'lijun/middle-class-ranking', path: 'lijun/middle-class-ranking',
@@ -807,7 +812,8 @@ const remainingRouter: AppRouteRecordRaw[] = [
hidden: true, hidden: true,
canTo: true canTo: true
}, },
component: () => import('@/views/ydoyun/report/lijun/reportpage6/components/MiddleClassRanking.vue') component: () =>
import('@/views/ydoyun/report/lijun/reportpage6/components/MiddleClassRanking.vue')
}, },
{ {
path: 'lijun/product-cards', path: 'lijun/product-cards',
@@ -818,7 +824,8 @@ const remainingRouter: AppRouteRecordRaw[] = [
hidden: true, hidden: true,
canTo: true canTo: true
}, },
component: () => import('@/views/ydoyun/report/lijun/reportpage6/components/ProductCardsPage.vue') component: () =>
import('@/views/ydoyun/report/lijun/reportpage6/components/ProductCardsPage.vue')
}, },
{ {
path: 'lijun/reportpage6/detail', path: 'lijun/reportpage6/detail',
@@ -841,6 +848,28 @@ const remainingRouter: AppRouteRecordRaw[] = [
canTo: true canTo: true
}, },
component: () => import('@/views/ydoyun/report/wangbuliao/productSplb/index.vue') component: () => import('@/views/ydoyun/report/wangbuliao/productSplb/index.vue')
},
{
path: 'product-daily/top-wall-more',
name: 'ProductDailyTopWallMore',
meta: {
title: 'TOP图片墙 - 更多',
noCache: true,
hidden: true,
canTo: true
},
component: () => import('@/views/ydoyun/report/productDaily/TopWallMore.vue')
},
{
path: 'sales-daily/category-penetration',
name: 'SalesDailyCategoryPenetration',
meta: {
title: '品类穿透看板',
noCache: true,
hidden: true,
canTo: true
},
component: () => import('@/views/ydoyun/report/salesdaily/CategoryPenetrationBoard.vue')
} }
] ]
}, },
@@ -860,7 +889,8 @@ const remainingRouter: AppRouteRecordRaw[] = [
noCache: true, noCache: true,
canTo: true canTo: true
}, },
component: () => import(/* webpackChunkName: "custom-tag" */ '@/views/ydoyun/customtag/index.vue') component: () =>
import(/* webpackChunkName: "custom-tag" */ '@/views/ydoyun/customtag/index.vue')
}, },
{ {
path: 'product-custom-tag', path: 'product-custom-tag',
@@ -870,7 +900,10 @@ const remainingRouter: AppRouteRecordRaw[] = [
noCache: true, noCache: true,
canTo: true canTo: true
}, },
component: () => import(/* webpackChunkName: "product-custom-tag" */ '@/views/ydoyun/productcustomtag/index.vue') component: () =>
import(
/* webpackChunkName: "product-custom-tag" */ '@/views/ydoyun/productcustomtag/index.vue'
)
}, },
{ {
path: 'store-custom-tag', path: 'store-custom-tag',
@@ -880,7 +913,10 @@ const remainingRouter: AppRouteRecordRaw[] = [
noCache: true, noCache: true,
canTo: true canTo: true
}, },
component: () => import(/* webpackChunkName: "store-custom-tag" */ '@/views/ydoyun/storecustomtag/index.vue') component: () =>
import(
/* webpackChunkName: "store-custom-tag" */ '@/views/ydoyun/storecustomtag/index.vue'
)
}, },
{ {
path: 'supplier-custom-tag', path: 'supplier-custom-tag',
@@ -890,7 +926,10 @@ const remainingRouter: AppRouteRecordRaw[] = [
noCache: true, noCache: true,
canTo: true canTo: true
}, },
component: () => import(/* webpackChunkName: "supplier-custom-tag" */ '@/views/ydoyun/suppliercustomtag/index.vue') component: () =>
import(
/* webpackChunkName: "supplier-custom-tag" */ '@/views/ydoyun/suppliercustomtag/index.vue'
)
} }
] ]
} }

View File

@@ -207,7 +207,7 @@
<div class="value">{{ rawCellValue(summary.ZTJ ?? summary.ztj ?? summary.zjzb) }}</div> <div class="value">{{ rawCellValue(summary.ZTJ ?? summary.ztj ?? summary.zjzb) }}</div>
</div> </div>
<!-- 3 同比业绩同比毛利率库存量 --> <!-- 3 同比业绩同比毛利率库存量周转天 -->
<div class="stat-box"> <div class="stat-box">
<span class="label">同比业绩</span> <span class="label">同比业绩</span>
<div class="value">{{ formatMoney(summary.TBYJ) }}</div> <div class="value">{{ formatMoney(summary.TBYJ) }}</div>
@@ -220,6 +220,10 @@
<span class="label">库存量</span> <span class="label">库存量</span>
<div class="value">{{ formatNumber(summary.kcl) }}</div> <div class="value">{{ formatNumber(summary.kcl) }}</div>
</div> </div>
<div class="stat-box">
<span class="label">周转天</span>
<div class="value">{{ formatNumber(summary.zzt ?? summary.ZZT) }}</div>
</div>
</div> </div>
</div> </div>
@@ -230,7 +234,7 @@
> >
<div class="card-title trend-title"> <div class="card-title trend-title">
<span class="trend-title-main">TOP图片墙</span> <span class="trend-title-main">TOP图片墙</span>
<div class="trend-title-sub trend-inline-filters"> <div class="trend-title-sub trend-inline-filters trend-image-wall-filters">
<el-select <el-select
v-model="imageFilter.storeValues" v-model="imageFilter.storeValues"
multiple multiple
@@ -293,6 +297,16 @@
/> />
</el-select> </el-select>
</div> </div>
<el-button
v-if="isProductDailyReport"
link
type="primary"
size="small"
class="trend-more-link"
@click="goTopWallMorePage"
>
更多
</el-button>
</div> </div>
<div <div
class="trend-chart-wrap trend-product-wrap" class="trend-chart-wrap trend-product-wrap"
@@ -442,12 +456,33 @@
<el-dialog <el-dialog
v-model="imagePreviewVisible" v-model="imagePreviewVisible"
title="图片详情" title="图片详情"
width="680px" width="720px"
append-to-body append-to-body
destroy-on-close destroy-on-close
class="image-detail-dialog"
@closed="resetImagePreviewState"
> >
<div class="image-preview-wrap"> <div class="image-preview-body">
<img v-if="currentPreviewImage" :src="currentPreviewImage" class="image-preview-main" alt="图片详情" /> <div class="image-preview-wrap">
<img
v-if="currentPreviewImage"
:src="currentPreviewImage"
class="image-preview-main"
alt="商品图片"
/>
</div>
<div v-if="previewImageDetailEntries.length" class="image-preview-detail">
<div class="image-preview-detail-title">全部信息</div>
<el-descriptions :column="2" border size="small" class="image-preview-descriptions">
<el-descriptions-item
v-for="ent in previewImageDetailEntries"
:key="ent.key"
:label="ent.label"
>
<span class="image-preview-detail-value">{{ ent.value }}</span>
</el-descriptions-item>
</el-descriptions>
</div>
</div> </div>
</el-dialog> </el-dialog>
@@ -457,7 +492,7 @@
<script setup lang="ts"> <script setup lang="ts">
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { computed, nextTick, onActivated, onDeactivated, onMounted, onUnmounted, reactive, ref, watch } from 'vue' import { computed, nextTick, onActivated, onDeactivated, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
import { useRoute } from 'vue-router' 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 { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import { Icon } from '@/components/Icon' import { Icon } from '@/components/Icon'
@@ -470,6 +505,7 @@ import AiPromptEditDialog from '../lijun/reportpage6/components/AiPromptEditDial
defineOptions({ name: 'ProductDaily' }) defineOptions({ name: 'ProductDaily' })
const route = useRoute() const route = useRoute()
const router = useRouter()
const reportScene = computed(() => String((route.meta as any)?.query?.scene || '').trim().toLowerCase()) const reportScene = computed(() => String((route.meta as any)?.query?.scene || '').trim().toLowerCase())
const isProductDailyReport = computed(() => reportScene.value === 'product') const isProductDailyReport = computed(() => reportScene.value === 'product')
/** 与 setPageModuleCode、AiPromptEditDialog 保存逻辑一致component = moduleKey 冒号前一段 */ /** 与 setPageModuleCode、AiPromptEditDialog 保存逻辑一致component = moduleKey 冒号前一段 */
@@ -593,6 +629,31 @@ const daleiOptions = ref<Array<{ label: string; value: string }>>([])
const ztjOptions = ref<Array<{ label: string; value: string }>>([]) const ztjOptions = ref<Array<{ label: string; value: string }>>([])
const zhongleiOptions = ref<Array<{ label: string; value: string }>>([]) const zhongleiOptions = ref<Array<{ label: string; value: string }>>([])
/** TOP 明细页 ckdm与图片墙「门店」大图筛选一致cangku 模式用选中门店,否则主表门店) */
const topWallMoreCkdmParam = computed(() => {
if (imageFilter.activeType === 'cangku' && imageFilter.storeValues.length) {
return imageFilter.storeValues.map((v) => String(v).trim()).filter(Boolean).join(',')
}
return ckdmQueryParam.value
})
/** 跳转 TOP图片墙明细携带门店 / 日期 / 品牌 / 正特价 */
function goTopWallMorePage() {
const rqStr = String(queryParams.rq || '').trim()
router.push({
path: '/reports/product-daily/top-wall-more',
query: {
fromProductDaily: '1',
pp: String(queryParams.brandCode || ''),
ckdm: topWallMoreCkdmParam.value,
rq: rqStr,
rq_s: rqStr,
rq_e: rqStr,
ztj: String(imageFilter.ztjValue || '').trim()
}
})
}
const routeSelectOptions = computed(() => { const routeSelectOptions = computed(() => {
const seen = new Set<string>() const seen = new Set<string>()
return userStoreRows.value return userStoreRows.value
@@ -895,11 +956,20 @@ const summary = reactive<any>({})
const categoryRowsRaw = ref<any[]>([]) const categoryRowsRaw = ref<any[]>([])
/** 仓库节点是否展开(默认折叠,点击箭头展开子行) */ /** 仓库节点是否展开(默认折叠,点击箭头展开子行) */
const warehouseExpanded = ref<Record<string, boolean>>({}) const warehouseExpanded = ref<Record<string, boolean>>({})
const productImageRows = ref<Array<{ spdm: string; pic: string }>>([]) /** TOP 图片墙行:含接口完整 raw便于详情弹窗展示全部字段 */
interface ProductImageWallItem {
spdm: string
pic: string
raw: Record<string, any>
}
const productImageRows = ref<ProductImageWallItem[]>([])
const topProductImageRows = computed(() => productImageRows.value.slice(0, 10)) const topProductImageRows = computed(() => productImageRows.value.slice(0, 10))
const imageWallLoading = ref(false) const imageWallLoading = ref(false)
const imagePreviewVisible = ref(false) const imagePreviewVisible = ref(false)
const currentPreviewImage = ref('') const currentPreviewImage = ref('')
/** 图片详情:与 SPRB2 返回行一致,用于列出全部键值 */
const currentPreviewRow = ref<Record<string, any> | null>(null)
function clearReactiveObject(obj: Record<string, any>) { function clearReactiveObject(obj: Record<string, any>) {
Object.keys(obj).forEach((k) => delete obj[k]) Object.keys(obj).forEach((k) => delete obj[k])
@@ -997,6 +1067,7 @@ async function loadSummary() {
if (anySum.PJZ == null && anySum.pjz != null) anySum.PJZ = anySum.pjz if (anySum.PJZ == null && anySum.pjz != null) anySum.PJZ = anySum.pjz
if (anySum.zjzb == null && anySum.ZJZB != null) anySum.zjzb = anySum.ZJZB if (anySum.zjzb == null && anySum.ZJZB != null) anySum.zjzb = anySum.ZJZB
if (anySum.kcl == null && anySum.KCL != null) anySum.kcl = anySum.KCL if (anySum.kcl == null && anySum.KCL != null) anySum.kcl = anySum.KCL
if (anySum.zzt == null && anySum.ZZT != null) anySum.zzt = anySum.ZZT
// 毛利率YDY_AI_GET_SXRB 可能返回 mll / MLL / ML // 毛利率YDY_AI_GET_SXRB 可能返回 mll / MLL / ML
if (anySum.mll == null && anySum.MLL != null) anySum.mll = anySum.MLL if (anySum.mll == null && anySum.MLL != null) anySum.mll = anySum.MLL
if (anySum.mll == null && anySum.ML != null) anySum.mll = anySum.ML if (anySum.mll == null && anySum.ML != null) anySum.mll = anySum.ML
@@ -1178,10 +1249,14 @@ async function loadProductImageList() {
p: PROCEDURE_SECRET, p: PROCEDURE_SECRET,
username: username.value username: username.value
} satisfies ExecuteProcedureParams) } satisfies ExecuteProcedureParams)
productImageRows.value = extractProcedureList(res).map((row: any) => ({ productImageRows.value = extractProcedureList(res).map((row: any) => {
spdm: String(row.spdm ?? row.SPDM ?? '').trim(), const r = row && typeof row === 'object' ? row : {}
pic: normalizeWeatherIconUrl(String(row.pic ?? row.PIC ?? '').trim()) return {
})) spdm: String(r.spdm ?? r.SPDM ?? '').trim(),
pic: normalizeWeatherIconUrl(String(r.pic ?? r.PIC ?? '').trim()),
raw: r as Record<string, any>
}
})
} catch (e) { } catch (e) {
console.warn('商品图片查询加载失败:', e) console.warn('商品图片查询加载失败:', e)
productImageRows.value = [] productImageRows.value = []
@@ -1207,12 +1282,58 @@ function handleImageScopeChange(type: 'cangku' | 'dalei' | 'fjsx4' | 'fjsx1') {
handleImageFilterChange() handleImageFilterChange()
} }
function openImagePreview(item: { pic: string }) { function openImagePreview(item: ProductImageWallItem) {
if (!item?.pic) return if (!item?.pic) return
currentPreviewImage.value = item.pic currentPreviewImage.value = item.pic
currentPreviewRow.value = item.raw && typeof item.raw === 'object' ? item.raw : {}
imagePreviewVisible.value = true imagePreviewVisible.value = true
} }
function resetImagePreviewState() {
currentPreviewImage.value = ''
currentPreviewRow.value = null
}
/** 图片详情弹窗:字段中文标签(未知字段仍显示键名) */
function imageDetailFieldLabel(key: string): string {
const k = key.toLowerCase()
const map: Record<string, string> = {
pic: '图片地址',
spdmt: '商品代码(拓展)',
spdm: '商品代码',
spmc: '商品名称',
ppmc: '品牌名称',
sl: '数量',
ckdm: '仓库/门店代码',
ckmc: '仓库/门店名称'
}
return map[k] ?? key
}
function formatImageDetailValue(v: any): string {
if (v == null || v === '') return '—'
if (typeof v === 'object') {
try {
return JSON.stringify(v)
} catch {
return String(v)
}
}
const s = String(v).trim()
return s === '' ? '—' : s
}
/** 弹窗中按接口原字段顺序展示全部信息 */
const previewImageDetailEntries = computed(() => {
const row = currentPreviewRow.value
if (!row || typeof row !== 'object') return [] as { key: string; label: string; value: string }[]
return Object.keys(row).map((key) => ({
key,
label: imageDetailFieldLabel(key),
value: formatImageDetailValue(row[key])
}))
})
// ========= 字段归一/展示 ========= // ========= 字段归一/展示 =========
function toFiniteNumber(v: any): number { function toFiniteNumber(v: any): number {
if (v == null || v === '') return NaN if (v == null || v === '') return NaN
@@ -1930,8 +2051,22 @@ function formatAmountSlashQuantity(v: any): string {
.trend-title { .trend-title {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: baseline; align-items: center;
gap: 4px 10px; gap: 6px 10px;
}
/* 中部筛选项占满中间区域,「更多」自然靠最右 */
.trend-image-wall-filters {
flex: 1 1 auto;
min-width: 0;
}
.trend-more-link {
flex-shrink: 0;
margin-left: auto;
padding: 0 2px;
font-size: 13px;
font-weight: 500;
} }
.trend-title-main { .trend-title-main {
@@ -2087,19 +2222,52 @@ function formatAmountSlashQuantity(v: any): string {
font-size: 13px; font-size: 13px;
} }
.image-preview-body {
display: flex;
flex-direction: column;
gap: 16px;
max-height: 85vh;
}
.image-preview-wrap { .image-preview-wrap {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
min-height: 260px; min-height: 200px;
flex-shrink: 0;
} }
.image-preview-main { .image-preview-main {
max-width: 100%; max-width: 100%;
max-height: 70vh; max-height: 46vh;
object-fit: contain; object-fit: contain;
} }
.image-preview-detail {
min-height: 0;
max-height: 38vh;
overflow: auto;
}
.image-preview-detail-title {
font-size: 13px;
font-weight: 600;
color: var(--el-text-color-primary);
margin-bottom: 8px;
}
.image-preview-descriptions {
:deep(.el-descriptions__label) {
width: 120px;
font-weight: 500;
}
}
.image-preview-detail-value {
word-break: break-all;
white-space: pre-wrap;
}
.trend-chart-inner { .trend-chart-inner {
flex: 1; flex: 1;
min-height: 160px; min-height: 160px;

View File

@@ -256,8 +256,13 @@
element-loading-text="加载中..." element-loading-text="加载中..."
> >
<div class="card-title category-title"> <div class="card-title category-title">
<span class="category-title-main">品类全维度分析</span> <div class="category-title-left">
<span class="category-title-sub">实收/标准/正价/特价/对比/目标</span> <span class="category-title-main">品类全维度分析</span>
<span class="category-title-sub">实收/标准/正价/特价/对比/目标</span>
</div>
<el-button type="primary" link class="category-image-wall-link" @click="goProductImageWall">
商品图片墙
</el-button>
</div> </div>
<div class="table-scroll"> <div class="table-scroll">
@@ -293,7 +298,9 @@
<tr v-for="(row, idx) in categoryRows" :key="row.PP || idx"> <tr v-for="(row, idx) in categoryRows" :key="row.PP || idx">
<td class="sxrb-td-sticky sxrb-col-brand"> <td class="sxrb-td-sticky sxrb-col-brand">
<div class="sxrb-brand-cell"> <div class="sxrb-brand-cell">
<span class="sxrb-brand-name">{{ row.PP || '-' }}</span> <el-button link type="primary" class="sxrb-brand-btn" @click="goCategoryPenetration(row)">
<span class="sxrb-brand-name">{{ row.PP || '-' }}</span>
</el-button>
<span class="sxrb-brand-zb">{{ formatCategoryPpzb(row.PPZB) }}</span> <span class="sxrb-brand-zb">{{ formatCategoryPpzb(row.PPZB) }}</span>
</div> </div>
</td> </td>
@@ -354,7 +361,7 @@ 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, nextTick, onActivated, onDeactivated, onMounted, onUnmounted, reactive, ref, watch } from 'vue' import { computed, nextTick, onActivated, onDeactivated, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
import { useRoute } from 'vue-router' 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 { getWeatherAggregate } from '@/api/ydoyun/weather' import { getWeatherAggregate } from '@/api/ydoyun/weather'
import { useAppStore } from '@/store/modules/app' import { useAppStore } from '@/store/modules/app'
@@ -374,6 +381,69 @@ const SALES_DAILY_COMPONENT = 'SalesDaily'
const SALES_DAILY_MODULE_KEY = `${SALES_DAILY_COMPONENT}:main` const SALES_DAILY_MODULE_KEY = `${SALES_DAILY_COMPONENT}:main`
const route = useRoute() const route = useRoute()
const router = useRouter()
/** 品类行品牌代号(存储过程 YDY_AI_GET_PLCT1 的 @pp优先 PPDM */
function resolveCategoryRowPpCode(row: Record<string, any>): string {
return String(
row?.PPDM ??
row?.ppdm ??
row?.PPBH ??
row?.ppbh ??
row?.BH ??
row?.bh ??
row?.CODE ??
row?.code ??
row?.PP ??
row?.pp ??
''
).trim()
}
/** 跳转品类全维度穿透看板 */
function goCategoryPenetration(row: Record<string, any>) {
const pp = resolveCategoryRowPpCode(row)
if (!pp) {
ElMessage.warning('该行缺少品牌代号,无法穿透')
return
}
if (!String(queryParams.ckdm || '').trim() || !String(queryParams.rq || '').trim()) {
ElMessage.warning('请先选择门店与日期并完成查询')
return
}
router.push({
path: '/reports/sales-daily/category-penetration',
query: {
rq: String(queryParams.rq).trim(),
ckdm: String(queryParams.ckdm).trim(),
pp,
ppmc: String(row.PP ?? row.pp ?? '').trim()
}
})
}
/** 跳转商品图片墙明细(与商品日报「更多」同源页,携带门店/日期) */
function goProductImageWall() {
if (!String(queryParams.ckdm || '').trim()) {
ElMessage.warning('请先选择门店')
return
}
if (!String(queryParams.rq || '').trim()) {
ElMessage.warning('请先选择日期')
return
}
const rqStr = String(queryParams.rq).trim()
router.push({
path: '/reports/product-daily/top-wall-more',
query: {
fromSalesDaily: '1',
ckdm: String(queryParams.ckdm).trim(),
rq: rqStr,
rq_s: rqStr,
rq_e: rqStr
}
})
}
/** 每日汇报模块名称与后台菜单名称一致meta.title缺省为「销售日报」 */ /** 每日汇报模块名称与后台菜单名称一致meta.title缺省为「销售日报」 */
const aiMenuModuleName = computed(() => { const aiMenuModuleName = computed(() => {
const t = route.meta?.title const t = route.meta?.title
@@ -688,8 +758,18 @@ watch(weatherPrimaryStackRef, () => nextTick(bindWeatherPrimaryStackResizeObserv
flush: 'post' flush: 'post'
}) })
/** 與 onMounted 一致:從 keep-alive 再次進入時重拉報表(避免分頁切回仍為舊數據);首次掛載已由 onMounted 查詢,此處跳過一次 */
let skipSalesDailyActivateRefetchOnce = true
onActivated(() => { onActivated(() => {
setAiAssistantFabEnabled(true) setAiAssistantFabEnabled(true)
if (skipSalesDailyActivateRefetchOnce) {
skipSalesDailyActivateRefetchOnce = false
return
}
if (queryParams.ckdm && queryParams.rq) {
void handleQuery()
}
}) })
onDeactivated(() => { onDeactivated(() => {
@@ -1901,10 +1981,25 @@ const timeSlotBarChartOptions = computed<EChartsOption>(() => {
} }
.category-title { .category-title {
display: flex;
flex-wrap: nowrap;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
}
.category-title-left {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: baseline; align-items: baseline;
gap: 4px 10px; gap: 4px 10px;
min-width: 0;
}
.category-image-wall-link {
flex-shrink: 0;
font-weight: 600;
} }
.category-title-main { .category-title-main {
@@ -2028,9 +2123,24 @@ const timeSlotBarChartOptions = computed<EChartsOption>(() => {
min-width: 0; min-width: 0;
} }
.sxrb-brand-btn {
max-width: 100%;
min-width: 0;
flex: 1 1 auto;
padding: 0 !important;
height: auto !important;
}
.sxrb-brand-btn :deep(.el-button__text) {
display: inline-flex;
align-items: center;
min-width: 0;
max-width: 100%;
}
.sxrb-brand-name { .sxrb-brand-name {
font-weight: 500; font-weight: 500;
color: var(--el-text-color-primary); color: inherit;
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;

View File

@@ -1,143 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>日志填报明细 - 已填报</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
body { background-color: #f0f2f5; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial; }
.card { background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.sidebar-active { border-right: 3px solid #1890ff; background: #e6f7ff; color: #1890ff; font-weight: bold; }
/* 限制内容列宽度并处理长文本溢出 */
.content-cell {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
</style>
</head>
<body class="p-6">
<div class="flex justify-between items-center mb-6">
<div>
<h1 class="text-xl font-bold text-gray-800">日志填报明细</h1>
<p class="text-sm text-gray-500">查看已完成提交的员工日志内容</p>
</div>
<div class="flex gap-3">
<input type="date" value="2026-04-12" class="border rounded px-3 py-1.5 text-sm outline-none focus:border-blue-500 shadow-sm bg-white text-gray-600">
<button class="bg-[#1890ff] text-white px-4 py-1.5 rounded text-sm hover:bg-blue-600 transition shadow-sm font-medium">导出已报明细</button>
</div>
</div>
<div class="flex gap-6">
<div class="w-64 flex-shrink-0 card p-4 h-fit">
<h3 class="font-bold mb-4 text-gray-700 text-sm flex items-center gap-2">
<i class="fas fa-sitemap text-blue-500"></i> 部门结构
</h3>
<ul class="space-y-1 text-sm">
<li class="p-3 sidebar-active cursor-pointer rounded flex justify-between">
<span>全公司</span> <span class="opacity-60 font-normal">128</span>
</li>
<li class="p-3 hover:bg-gray-50 cursor-pointer rounded text-gray-600 flex justify-between transition">
<span>店铺日报</span> <span class="text-gray-400 font-normal">45</span>
</li>
<li class="p-3 hover:bg-gray-50 cursor-pointer rounded text-gray-600 flex justify-between transition">
<span>采购日报</span> <span class="text-gray-400 font-normal">32</span>
</li>
<li class="p-3 hover:bg-gray-50 cursor-pointer rounded text-gray-600 flex justify-between transition">
<span>商品日报</span> <span class="text-gray-400 font-normal">51</span>
</li>
</ul>
</div>
<div class="flex-1 card overflow-hidden">
<div class="border-b px-6 py-4 flex justify-between items-center bg-gray-50/50">
<div class="flex gap-8">
<button class="text-sm text-gray-400 pb-2 hover:text-gray-600 transition-all font-medium">未填报名单 (16)</button>
<button class="text-sm font-bold border-b-2 border-blue-500 pb-2 text-blue-600 transition-all">已填报明细 (112)</button>
</div>
<div class="relative">
<input type="text" placeholder="搜索姓名或内容..." class="text-xs border rounded-full px-4 py-1.5 w-64 outline-none focus:border-blue-500 shadow-sm">
<i class="fas fa-search absolute right-3 top-2 text-gray-300"></i>
</div>
</div>
<table class="w-full text-left text-sm table-fixed">
<thead class="bg-gray-50 text-gray-500 border-b">
<tr>
<th class="px-6 py-4 font-bold w-40">姓名</th>
<th class="px-6 py-4 font-bold w-56">部门</th>
<th class="px-6 py-4 font-bold">填报内容</th>
<th class="px-6 py-4 font-bold text-center w-28 uppercase text-[10px] tracking-wider">提交状态</th>
</tr>
</thead>
<tbody class="divide-y text-gray-700">
<tr class="hover:bg-blue-50/30 transition group">
<td class="px-6 py-4 flex items-center gap-3">
<span class="w-8 h-8 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center text-xs font-bold shadow-sm"></span>
<span class="font-medium text-gray-800">王小明</span>
</td>
<td class="px-6 py-4">
<div class="text-gray-700 font-medium">零售事业部</div>
<div class="text-[10px] text-gray-400 uppercase tracking-tighter mt-0.5">Chenzhou Store · Manager</div>
</td>
<td class="px-6 py-4">
<div class="bg-gray-50 p-2.5 rounded border border-transparent group-hover:border-gray-200 group-hover:bg-white transition relative">
<p class="text-xs text-gray-600 leading-relaxed content-cell">
今日郴州店客流量较昨日回升成交额达到预期目标。主推的春季新款T恤表现优异转化率约12%。下午对新入职导购进行了系统培训。
</p>
<div class="absolute right-2 bottom-1 text-[10px] text-blue-500 opacity-0 group-hover:opacity-100 transition font-bold">详情 ></div>
</div>
</td>
<td class="px-6 py-4 text-center">
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-bold bg-green-50 text-green-600 border border-green-100">
<i class="fas fa-check-circle mr-1"></i> 已提交
</span>
</td>
</tr>
<tr class="hover:bg-blue-50/30 transition group">
<td class="px-6 py-4 flex items-center gap-3">
<span class="w-8 h-8 rounded-full bg-indigo-100 text-indigo-600 flex items-center justify-center text-xs font-bold shadow-sm"></span>
<span class="font-medium text-gray-800">李华</span>
</td>
<td class="px-6 py-4">
<div class="text-gray-700 font-medium">采购部</div>
<div class="text-[10px] text-gray-400 uppercase tracking-tighter mt-0.5">Supply Chain · Buyer</div>
</td>
<td class="px-6 py-4">
<div class="bg-gray-50 p-2.5 rounded border border-transparent group-hover:border-gray-200 group-hover:bg-white transition relative">
<p class="text-xs text-gray-600 leading-relaxed content-cell">
完成了与面料供应商的本周对账工作。确认了下周到货的夏季轻薄款面料数量及颜色。注意到原材料价格有小幅波动,已汇报给商品中心。
</p>
<div class="absolute right-2 bottom-1 text-[10px] text-blue-500 opacity-0 group-hover:opacity-100 transition font-bold">详情 ></div>
</div>
</td>
<td class="px-6 py-4 text-center">
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-bold bg-green-50 text-green-600 border border-green-100">
<i class="fas fa-check-circle mr-1"></i> 已提交
</span>
</td>
</tr>
</tbody>
</table>
<div class="px-6 py-4 flex justify-between items-center border-t bg-gray-50/50 text-xs text-gray-500">
<span>显示 1 到 10 条,共 112 条已填报记录</span>
<div class="flex gap-2">
<button class="p-1.5 border rounded bg-white hover:bg-gray-100 disabled:opacity-50" disabled>&lt;</button>
<button class="p-1.5 border rounded bg-blue-500 text-white min-w-[30px] font-bold shadow-sm">1</button>
<button class="p-1.5 border rounded bg-white hover:bg-gray-100 min-w-[30px]">2</button>
<button class="p-1.5 border rounded bg-white hover:bg-gray-100 min-w-[30px]">3</button>
<button class="p-1.5 border rounded bg-white hover:bg-gray-100">&gt;</button>
</div>
</div>
</div>
</div>
</body>
</html>