1. 提交代码

This commit is contained in:
2026-04-27 15:25:12 +08:00
parent 02ff9e4e65
commit 78ff26cb20
6 changed files with 252 additions and 515 deletions

View File

@@ -14,6 +14,13 @@ export interface ExecuteProcedureParams {
rq_e?: string // 结束日期格式YYYY-MM-DD可选
p?: string // 密码(可选)
username?: string // 用户名(可选)
/** 存储过程入参 @authVARCHAR(MAX),如 YDY_GET_TAG 第一个参数) */
auth?: string
/**
* 存储过程入参 @paramsVARCHAR(MAX)JSON 字符串,如 YDY_GET_TAG 第二个参数)
* 注意与 axios 的 `params` 配置重名,此处为查询参数字段名
*/
params?: string
}
/** 日报表-当日总销售数据 */

View File

@@ -1,9 +1,13 @@
<template>
<el-drawer
v-model="visible"
:size="isFullscreen ? '100%' : dialogWidth"
:size="drawerSize"
direction="rtl"
:class="['ai-image-chat-dialog', 'ai-chat-drawer', { 'is-fullscreen': isFullscreen }]"
:class="[
'ai-image-chat-dialog',
'ai-chat-drawer',
{ 'is-fullscreen': isFullscreen, 'is-with-daily-report': reportDocVisible }
]"
:modal-class="'ai-chat-overlay'"
destroy-on-close
:close-on-click-modal="true"
@@ -62,7 +66,11 @@
</div>
</div>
</template>
<div class="ai-chat-dialog-body">
<div class="ai-chat-dialog-body" :class="{ 'ai-chat-dialog-body--split': reportDocVisible }">
<div
class="ai-chat-panel ai-chat-panel--convo"
:style="reportDocVisible ? splitConvoStyle : undefined"
>
<div class="chat-main">
<div
ref="messageListRef"
@@ -153,7 +161,7 @@
<button
class="daily-report-entry"
type="button"
:class="{ 'is-module-required': !hasReportModuleContext }"
:class="{ 'is-module-required': !hasReportModuleContext, 'is-open': reportDocVisible }"
:disabled="loading"
@click="onDailyReportClick"
>
@@ -235,12 +243,14 @@
</div>
</div>
</div>
<!-- 报告单据弹窗 -->
<div v-if="reportDocVisible" class="ai-chat-panel ai-chat-panel--report">
<ReportDocumentModal
v-model="reportDocVisible"
:module-name="effectiveReportModuleName"
:module-code="effectiveReportModuleCode"
/>
</div>
</div>
<!-- Markdown 内图片大图预览 -->
<ElImageViewer
v-if="imageViewerVisible"
@@ -325,7 +335,10 @@ function onDailyReportClick() {
ElMessage.warning('请进入一个模块进行每日汇报')
return
}
reportDocVisible.value = true
const open = !reportDocVisible.value
reportDocVisible.value = open
// 打开每日汇报时直接全屏抽屉,收起时恢复窄条宽度
isFullscreen.value = open
}
const emit = defineEmits<{
@@ -376,8 +389,22 @@ const streamAbortController = ref<AbortController | null>(null)
const streamingText = ref('')
/** 多轮对话会话 ID由 Dify 返回,后续请求携带以保持上下文 */
const conversationId = ref<string>('')
const dialogWidth = ref('420px')
const isFullscreen = ref(false)
/** 仅聊天时抽屉宽度 = 分栏时左侧对话区宽度(与全屏/宽屏无联动) */
const NARROW_DRAWER_PX = 380
const splitConvoStyle = computed(() => ({
flex: `0 1 ${NARROW_DRAWER_PX}px`,
width: `${NARROW_DRAWER_PX}px`,
maxWidth: `${NARROW_DRAWER_PX}px`,
minWidth: '240px',
boxSizing: 'border-box' as const
}))
/** 全屏或每日汇报为宽分栏时调整宽度;每日汇报打开时会同步 isFullscreen=100% */
const drawerSize = computed(() => {
if (isFullscreen.value) return '100%'
if (reportDocVisible.value) return 'min(100%, 1280px)'
return `${NARROW_DRAWER_PX}px`
})
const displayStreamingHtml = computed(() => renderMarkdown(streamingText.value))
const imageViewerVisible = ref(false)
const imageViewerList = ref<string[]>([])
@@ -593,6 +620,7 @@ const handleClosed = () => {
streamAbortController.value?.abort()
}
isFullscreen.value = false
reportDocVisible.value = false
if (picPreviewUrl.value) {
URL.revokeObjectURL(picPreviewUrl.value)
picPreviewUrl.value = ''
@@ -760,6 +788,31 @@ function getGreeting(): string {
height: max(calc(100vh - 120px), calc(100vh - var(--left-menu-max-width, 200px) - 80px));
max-height: max(calc(100vh - 120px), calc(100vh - var(--left-menu-max-width, 200px) - 80px));
}
.ai-image-chat-dialog .ai-chat-dialog-body--split {
flex-direction: row;
align-items: stretch;
gap: 0;
}
.ai-image-chat-dialog .ai-chat-panel--convo {
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
}
.ai-image-chat-dialog .ai-chat-panel--report {
flex: 1;
min-width: 0;
min-height: 0;
position: relative;
display: flex;
flex-direction: column;
border-left: 1px solid #e2e8f0;
}
.ai-image-chat-dialog .ai-chat-panel--report :deep(.el-dialog) {
--el-dialog-margin-top: 0;
margin: 0;
}
.ai-image-chat-dialog.is-fullscreen .ai-chat-dialog-body {
height: calc(100vh - 120px) !important;
max-height: calc(100vh - 120px) !important;
@@ -1208,6 +1261,11 @@ function getGreeting(): string {
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.16);
transform: translateY(-1px);
}
.daily-report-entry.is-open {
border-color: var(--el-color-primary);
background: linear-gradient(135deg, #ecf5ff 0%, #d9ecff 100%);
box-shadow: inset 0 0 0 1px rgba(64, 158, 255, 0.25);
}
.daily-report-entry:disabled {
opacity: 0.6;
cursor: not-allowed;

View File

@@ -1,41 +1,13 @@
<template>
<el-dialog
v-model="modalVisible"
width="92vw"
class="report-document-modal report-document-modal--no-title"
body-class="report-document-modal-body"
align-center
:show-close="false"
:fullscreen="reportModalFullscreen"
:close-on-click-modal="true"
destroy-on-close
@closed="handleClosed"
@opened="loadAllData"
<!-- AI 决策助手同屏时不用 el-dialogfixed/teleport 会浮出侧栏外用普通块级容器填满父级 -->
<div
v-show="modalVisible"
class="report-document-surface"
>
<div
class="modal-body"
style="min-height: min(70vh, 760px); box-sizing: border-box"
:style="surfaceBodyStyle"
>
<div class="report-modal-topbar">
<h2 class="report-modal-title">每日汇报</h2>
<div class="report-modal-header-actions" @click.stop>
<el-tooltip :content="reportModalFullscreen ? '退出全屏' : '全屏查看'" placement="bottom">
<button
type="button"
class="report-modal-fs"
:aria-label="reportModalFullscreen ? '退出全屏' : '全屏查看'"
@click="reportModalFullscreen = !reportModalFullscreen"
>
<Icon :icon="reportModalFullscreen ? 'ep:copy-document' : 'ep:full-screen'" />
</button>
</el-tooltip>
<el-tooltip content="关闭" placement="bottom">
<button type="button" class="report-modal-close" aria-label="关闭" @click="modalVisible = false">
<el-icon><Close /></el-icon>
</button>
</el-tooltip>
</div>
</div>
<el-tabs v-model="activeTab" class="main-tabs" :lazy="false">
<!-- Tab1仅报告内容 -->
<el-tab-pane label="撰写报告" name="write">
@@ -318,7 +290,7 @@
</el-tab-pane>
</el-tabs>
</div>
</el-dialog>
</div>
<!-- 日历当日多条汇报时先选一条再打开与历史列表相同的全屏报告详情 -->
<el-dialog
@@ -443,8 +415,7 @@ import {
} from '@/api/ydoyun/aiAssistantReport'
import { useUserStore } from '@/store/modules/user'
import { ElMessage, ElMessageBox, ElLoading, ElNotification } from 'element-plus'
import { ArrowLeft, ArrowRight, Close, Plus } from '@element-plus/icons-vue'
import { Icon } from '@/components/Icon'
import { ArrowLeft, ArrowRight, Plus } from '@element-plus/icons-vue'
import { AI_ASSISTANT_REPORT_CONTEXT_KEY } from './useAiAssistant'
import * as FileApi from '@/api/infra/file'
import Echart from '@/components/Echart/src/Echart.vue'
@@ -461,6 +432,18 @@ const props = defineProps<{
moduleCode?: string
}>()
/** 与 AI 助手侧栏同屏时占满父级,内部 tabs 可滚动 */
const surfaceBodyStyle = {
minHeight: 0,
flex: 1,
height: '100%',
maxHeight: '100%',
boxSizing: 'border-box' as const,
display: 'flex',
flexDirection: 'column' as const,
overflow: 'hidden' as const
}
const emit = defineEmits<{
'update:modelValue': [value: boolean]
}>()
@@ -558,7 +541,6 @@ async function loadDetailRows(reportId: number | null) {
resetDetailRows()
}
}
const reportModalFullscreen = ref(false)
const optimizing = ref(false)
const saving = ref(false)
/** 历史列表中单条详情弹窗(全屏:正文 + AI 详表 + 截图) */
@@ -668,14 +650,23 @@ function filterReportRowsByScope<T extends AiAssistantReportItem>(rows: T[]): T[
})
}
watch(modalVisible, (v) => {
if (v && !hasModuleScope.value) {
ElMessage.warning('请进入一个模块进行每日汇报')
watch(
() => props.modelValue,
(open, wasOpen) => {
if (open) {
nextTick(() => {
if (!hasModuleScope.value) {
ElMessage.warning('请进入一个模块进行每日汇报')
modalVisible.value = false
})
return
}
})
loadAllData()
})
} else if (wasOpen) {
handleClosed()
}
}
)
/** 截图地址补全(相对路径拼 API 根) */
function resolveMediaUrl(url?: string | null): string {
@@ -1204,7 +1195,6 @@ function handleClosed() {
reportContent.value = ''
currentReportId.value = null
resetDetailRows()
reportModalFullscreen.value = false
historyRecordDetailVisible.value = false
historyRecordDetail.value = null
historyList.value = []
@@ -1330,30 +1320,11 @@ function truncateContent(text?: string, maxLen = 60): string {
</script>
<style scoped>
/*
* 默认弹窗 margin-top: 15vh 会在视口里留出很大空白align-center 改为垂直居中margin: auto
* 同时收紧 .el-dialog 与 body 的上内边距,避免「每日汇报」上方堆叠过多 padding。
*/
.report-document-modal.report-document-modal--no-title {
/* 与 AI 助手同屏:无 el-dialog侧栏内边距由本类控制 */
.report-document-surface {
padding: 8px 16px 16px;
box-sizing: border-box;
}
/*
* 弹窗 teleport 到 body 后scoped 无法稳定命中 EP 渲染的 `.el-dialog__body`
* 固定高度与非 scoped 块配合,见文末 `.el-dialog__body.report-document-modal-body`。
*/
.report-document-modal.el-dialog:not(.is-fullscreen) {
width: min(1000px, 92vw) !important;
max-width: min(1000px, calc(100vw - 24px));
box-sizing: border-box;
}
.report-document-modal.el-dialog.is-fullscreen {
display: flex;
flex-direction: column;
margin: 0 !important;
height: 100vh;
max-height: 100vh;
}
.modal-body {
display: flex;
flex-direction: column;
@@ -1364,50 +1335,6 @@ function truncateContent(text?: string, maxLen = 60): string {
max-height: 100%;
overflow: hidden;
}
.report-modal-topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-shrink: 0;
padding-bottom: 10px;
margin-bottom: 4px;
border-bottom: 1px solid var(--el-border-color-lighter);
}
.report-modal-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
line-height: 1.35;
letter-spacing: 0.02em;
}
.report-modal-header-actions {
display: flex;
align-items: center;
gap: 2px;
flex-shrink: 0;
}
.report-modal-fs,
.report-modal-close {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
margin: 0;
border: none;
border-radius: 4px;
background: transparent;
color: var(--el-text-color-secondary);
cursor: pointer;
}
.report-modal-fs:hover,
.report-modal-close:hover {
color: var(--el-color-primary);
background: var(--el-fill-color-light);
}
.section-title-hint {
margin-left: 8px;
font-size: 12px;
@@ -2521,45 +2448,24 @@ function truncateContent(text?: string, maxLen = 60): string {
}
</style>
<!--
Teleport body scoped 无法命中 EP 渲染的 .el-dialog / .el-dialog__body无父组件 data-v
此处用唯一类名做全局样式避免主内容区高度为 0只剩标题栏
-->
<!-- AI 助手同屏时主内容已非 el-dialog .report-document-surface 填满侧栏避免主区域高度为 0 -->
<style lang="scss">
.el-dialog.report-document-modal.report-document-modal--no-title > .el-dialog__header {
display: none !important;
height: 0 !important;
min-height: 0 !important;
padding: 0 !important;
margin: 0 !important;
border: none !important;
overflow: hidden !important;
line-height: 0 !important;
}
/* 主弹窗内容区:固定高度 + flex供内部 .modal-body / .main-tabs 分配剩余空间 */
.el-dialog__body.report-document-modal-body {
min-height: min(800px, calc(100vh - 72px)) !important;
height: min(800px, calc(100vh - 72px)) !important;
max-height: min(800px, calc(100vh - 72px)) !important;
padding: 0 0 4px !important;
box-sizing: border-box;
.report-document-surface {
min-height: 0;
flex: 1 1 0;
width: 100%;
height: 100%;
max-height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
display: flex !important;
flex-direction: column !important;
box-sizing: border-box;
background: var(--el-bg-color);
border-radius: 8px;
scrollbar-gutter: stable;
}
.el-dialog.is-fullscreen .el-dialog__body.report-document-modal-body {
flex: 1 1 0 !important;
min-height: 0 !important;
height: auto !important;
max-height: none !important;
overflow: hidden;
}
/* 槽位内容铺满 body避免用 > 以防 EP 插入包裹层) */
.el-dialog__body.report-document-modal-body .modal-body {
.report-document-surface .modal-body {
flex: 1 1 0;
min-height: 0;
overflow: hidden;

View File

@@ -832,15 +832,15 @@ const remainingRouter: AppRouteRecordRaw[] = [
component: () => import('@/views/ydoyun/report/lijun/reportpage6/detail.vue')
},
{
path: 'product-splb',
path: 'wbl/product-splb',
name: 'ProductSplbReport',
meta: {
title: '商品报表',
title: '商品列表报表',
noCache: true,
hidden: true,
canTo: true
},
component: () => import('@/views/ydoyun/report/productSplb/index.vue')
component: () => import('@/views/ydoyun/report/wangbuliao/productSplb/index.vue')
}
]
},

View File

@@ -198,26 +198,49 @@
<!-- KPI 卡片区域 -->
<div class="kpi-section">
<div class="module-header">
<span class="module-title">KPI 指标</span>
<div class="kb-card-header kb-card-header--title-style">
<div class="kb-title-wrapper">
<el-icon class="kb-title-icon"><Odometer /></el-icon>
<h3 class="kb-card-title">KPI 指标</h3>
</div>
</div>
<div class="kpi-section-body">
<div
class="kpi-grid"
v-loading="loadingKpi"
element-loading-text="加载中..."
element-loading-custom-class="kpi-loading"
>
<el-card v-for="(kpi, index) in kpiList" :key="index" class="kpi-card" shadow="never">
<el-empty
v-if="!loadingKpi && kpiList.length === 0"
class="kpi-grid-empty"
description="暂无数据"
:image-size="120"
/>
<template v-else>
<el-card
v-for="(kpi, index) in kpiList"
:key="'kpi-' + index"
class="kpi-card"
shadow="never"
>
<div class="kpi-header">
<span class="kpi-title">{{ kpi.title }}</span>
<span v-if="kpi.unit != null && kpi.unit !== ''" class="kpi-unit">{{ kpi.unit }}</span>
<span class="kpi-unit">{{ kpi.unit }}</span>
</div>
<div v-if="kpi.value != null && kpi.value !== ''" class="kpi-value">{{ kpi.value }}</div>
<div v-if="(formatTrendText(kpi) || (kpi.descs != null && kpi.descs !== ''))" class="kpi-footer">
<span v-if="formatTrendText(kpi)" class="kpi-trend" :class="kpi.trend">{{ formatTrendText(kpi) }}</span>
<el-tooltip v-if="kpi.descs != null && kpi.descs !== ''" :content="kpi.descs" placement="top" :show-after="300" class="kpi-desc-tooltip">
<div class="kpi-value">{{ kpi.value }}</div>
<div class="kpi-footer">
<span class="kpi-trend" :class="kpi.trend">{{ formatTrendText(kpi) || '-' }}</span>
<el-tooltip
v-if="kpi.descs != null && String(kpi.descs).trim() !== ''"
:content="kpi.descs"
placement="top"
:show-after="300"
class="kpi-desc-tooltip"
>
<span class="kpi-desc">{{ kpi.descs }}</span>
</el-tooltip>
<span v-else class="kpi-desc kpi-desc--empty">-</span>
</div>
<!-- 迷你柱状图 -->
<div v-if="kpi.miniBars" class="mini-bars">
@@ -243,6 +266,8 @@
/>
</div>
</el-card>
</template>
</div>
</div>
</div>
@@ -845,6 +870,7 @@ import type { EChartsOption } from 'echarts'
import Echart from '@/components/Echart/src/Echart.vue'
import {
StarFilled,
Odometer,
Search,
Document,
Picture,
@@ -2263,13 +2289,16 @@ const handleQuery = async () => {
raw = Array.isArray(kpiRes.data) ? kpiRes.data : null
}
}
// 前几行常为列配置(含 keys/orders、无 code数据行含供货商 code
const dataRows = (raw || []).filter(
// 前几行常为列配置(含 keys/orders、无 code优先用含 code 的数据行
const rawArr = raw || []
let rowsToMap = rawArr.filter(
(row) => row != null && row.code != null && String(row.code).trim() !== ''
)
if (dataRows.length > 0) {
kpiList.value = dataRows.map((item: any) => mapApiRowToKpi(item))
if (rowsToMap.length === 0) {
// 无 code 时仍可能返回 KPI 行(仅有 title/指标等),避免整块区域无卡片
rowsToMap = rawArr.filter((row) => isLikelyKpiDataRow(row))
}
kpiList.value = rowsToMap.map((item: any) => mapApiRowToKpi(item))
})
.catch(() => {})
.finally(() => {
@@ -2383,6 +2412,18 @@ function trendFromColorField(c: unknown): 'up' | 'down' | 'flat' {
return 'flat'
}
/** 排除明显为列配置的行;其余有标题或主值或 code 的视为 KPI 数据行 */
function isLikelyKpiDataRow(row: any): boolean {
if (row == null || typeof row !== 'object') return false
if (row.keys != null || row.orders != null) return false
const hasCode = row.code != null && String(row.code).trim() !== ''
const hasTitle =
String(row.title ?? row.mainTitle ?? row.name ?? '').trim() !== ''
const hasMainValue =
String(row.value ?? row.mainValue ?? row.val ?? row.value1 ?? '').trim() !== ''
return hasCode || hasTitle || hasMainValue
}
// 将接口返回的一行数据映射为 KPI 卡片格式(按后端字段名适配,无则保留默认)
function mapApiRowToKpi(row: any): KPIData {
const trend =
@@ -2950,6 +2991,60 @@ onDeactivated(() => {
}
}
// KPI 指标模块:白底容器(与整页灰底区分;标题栏与「供应商表现」同一样式)
.kpi-section {
background-color: var(--el-bg-color);
border: 1px solid var(--el-border-color);
border-radius: 4px;
padding: 0;
margin-bottom: 10px;
overflow: hidden;
.kpi-section-body {
padding: 0 12px 12px;
}
}
// 模块标题栏KPI、供应商表现等共用与 kb-card-header--title-style 一致)
.kb-card-header {
display: flex;
align-items: center;
justify-content: space-between;
.more-button {
font-size: 12px;
}
&.kb-card-header--title-style {
padding: 16px;
border-bottom: 1px solid var(--el-border-color-lighter);
background: var(--el-fill-color-lighter);
margin-bottom: 0;
}
.kb-title-wrapper {
display: flex;
align-items: center;
gap: 8px;
padding-left: 12px;
border-left: 4px solid var(--el-color-primary);
flex: 1;
}
.kb-title-icon {
color: var(--el-color-primary);
font-size: 18px;
}
.kb-card-title {
font-size: 16px;
font-weight: 700;
color: var(--el-text-color-primary);
margin: 0;
letter-spacing: 0.5px;
}
}
// KPI 网格(含加载中特效)
.kpi-grid {
position: relative;
@@ -2957,7 +3052,14 @@ onDeactivated(() => {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 10px;
margin-bottom: 0;
.kpi-grid-empty {
grid-column: 1 / -1;
justify-self: center;
width: 100%;
padding: 16px 0 8px;
}
.kpi-card {
height: 160px;
@@ -3134,46 +3236,6 @@ onDeactivated(() => {
padding: 0;
}
.kb-card-header {
display: flex;
align-items: center;
justify-content: space-between;
.more-button {
font-size: 12px;
}
/* 与中类销售排名 Top 5 标题样式一致 */
&.kb-card-header--title-style {
padding: 16px;
border-bottom: 1px solid var(--el-border-color-lighter);
background: var(--el-fill-color-lighter);
margin-bottom: 0;
}
.kb-title-wrapper {
display: flex;
align-items: center;
gap: 8px;
padding-left: 12px;
border-left: 4px solid var(--el-color-primary);
flex: 1;
}
.kb-title-icon {
color: var(--el-color-primary);
font-size: 18px;
}
.kb-card-title {
font-size: 16px;
font-weight: 700;
color: var(--el-text-color-primary);
margin: 0;
letter-spacing: 0.5px;
}
}
.kb-card-content {
padding: 0 12px 12px;
}

File diff suppressed because one or more lines are too long