1.加入ai对话框 模块日报功能

This commit is contained in:
2026-03-25 08:40:33 +08:00
parent bb5ab23430
commit e762188322
8 changed files with 184 additions and 183 deletions

View File

@@ -4,8 +4,8 @@ NODE_ENV=production
VITE_DEV=true
# 请求路径
VITE_BASE_URL='http://localhost:48080'
#VITE_BASE_URL='http://118.253.178.8:48080'
#VITE_BASE_URL='http://localhost:48080'
VITE_BASE_URL='http://118.253.178.8:48080'
# 文件上传类型server - 后端上传, client - 前端直连上传仅支持S3服务
VITE_UPLOAD_TYPE=server

View File

@@ -4,8 +4,8 @@ NODE_ENV=development
VITE_DEV=true
# 请求路径
#VITE_BASE_URL='http://118.253.178.8:48080'
VITE_BASE_URL='http://localhost:48080'
VITE_BASE_URL='http://118.253.178.8:48080'
#VITE_BASE_URL='http://localhost:48080'
# 文件上传类型server - 后端上传, client - 前端直连上传,仅支持 S3 服务
VITE_UPLOAD_TYPE=server

View File

@@ -14,6 +14,7 @@ export interface TenantVO {
accountCount: number
websites: string[]
createTime: Date
tenantPrompt?: string
}
export interface TenantPageReqVO extends PageParam {

View File

@@ -4,6 +4,7 @@ import {
BarChart,
FunnelChart,
GaugeChart,
HeatmapChart,
LineChart,
MapChart,
PictorialBarChart,
@@ -13,6 +14,7 @@ import {
import {
AriaComponent,
CalendarComponent,
DataZoomComponent,
GridComponent,
LegendComponent,
@@ -37,6 +39,7 @@ echarts.use([
AriaComponent,
ParallelComponent,
VisualMapComponent,
CalendarComponent,
BarChart,
LineChart,
PieChart,
@@ -45,7 +48,8 @@ echarts.use([
PictorialBarChart,
RadarChart,
GaugeChart,
FunnelChart
FunnelChart,
HeatmapChart
])
export default echarts

View File

@@ -792,7 +792,6 @@ const remainingRouter: AppRouteRecordRaw[] = [
name: 'SupplierPerformance',
meta: {
title: '供应商详情',
noCache: true,
hidden: true,
canTo: true
},

View File

@@ -61,6 +61,9 @@
class="w-full"
/>
</el-form-item>
<el-form-item label="租户提示词" prop="tenantPrompt">
<el-input v-model="formData.tenantPrompt" type="textarea" :rows="3" placeholder="请输入租户提示词" />
</el-form-item>
<el-form-item label="租户状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
@@ -103,13 +106,15 @@ const formData = ref({
expireTime: undefined,
websites: [],
status: CommonStatusEnum.ENABLE,
tenantPrompt: '',
// 新增专属
username: undefined,
password: undefined
})
const formRules = reactive({
name: [{ required: true, message: '租户名不能为空', trigger: 'blur' }],
packageId: [{ required: true, message: '租户套<EFBFBD><EFBFBD><EFBFBD>不能为空', trigger: 'blur' }],
packageId: [{ required: true, message: '租户套不能为空', trigger: 'blur' }],
contactName: [{ required: true, message: '联系人不能为空', trigger: 'blur' }],
status: [{ required: true, message: '租户状态不能为空', trigger: 'blur' }],
accountCount: [{ required: true, message: '账号额度不能为空', trigger: 'blur' }],
@@ -178,6 +183,7 @@ const resetForm = () => {
expireTime: undefined,
websites: [],
status: CommonStatusEnum.ENABLE,
tenantPrompt: '',
username: undefined,
password: undefined
}

View File

@@ -1,5 +1,5 @@
<template>
<div class="supplier-performance-page">
<div ref="pageRootRef" class="supplier-performance-page">
<!-- 查询条件区域与主页一致 -->
<el-card class="query-card" shadow="never">
<div class="query-form">
@@ -265,7 +265,7 @@
</div>
<!-- 指标表格三列网格布局 -->
<div v-if="getColumnsWithDate(row).length > 0" class="metric-section">
<div v-if="getColumnsWithDate().length > 0" class="metric-section">
<div class="grid-layout metric-header">
<span>检测指标</span>
<span>实际结果</span>
@@ -313,10 +313,10 @@
</div>
</div>
<!-- 列表形式不需要悬浮卡片 -->
<!-- 列表形式每个字段独立一列铺开展示 -->
<div v-show="displayMode === 'table'" class="table-wrap">
<el-table :data="visibleRows" v-loading="loading" border stripe style="width: 100%">
<el-table-column label="供应商" min-width="220" show-overflow-tooltip>
<el-table-column label="供应商" min-width="180" fixed="left" show-overflow-tooltip>
<template #default="{ row }">
<div class="table-title">
<div class="table-title-main">{{ String((row as any)?.title ?? (row as any)?.name ?? '-') }}</div>
@@ -333,24 +333,21 @@
</div>
</template>
</el-table-column>
<el-table-column label="核心指标" width="160" align="center">
<template #default="{ row }">
<div class="table-core">
<div class="core-name">{{ getTitleName(row) || '-' }}</div>
<div class="core-value">{{ getTitleValue(row) || '-' }}</div>
</div>
</template>
<el-table-column label="核心指标名称" min-width="120" align="center" show-overflow-tooltip>
<template #default="{ row }">{{ getTitleName(row) || '-' }}</template>
</el-table-column>
<el-table-column label="检测指标明细" min-width="420">
<el-table-column label="核心指标值" min-width="100" align="center" show-overflow-tooltip>
<template #default="{ row }">{{ getTitleValue(row) || '-' }}</template>
</el-table-column>
<template v-for="(col, ci) in metricColumns" :key="col.key || ci">
<el-table-column :label="col.title + '(实际)'" min-width="110" align="center" show-overflow-tooltip>
<template #default="{ row }">
<div class="metric-cell">
<div
v-for="(item, mi) in getTableData(row)"
:key="mi"
class="metric-cell-row"
v-for="(item, mi) in getMetricItemAt(row, ci)"
:key="item.metricName + '-' + mi"
class="table-metric-actual"
:class="item.valueColorClass"
>
<div class="metric-cell-name">{{ item.metricName }}</div>
<div class="metric-cell-actual" :class="item.valueColorClass">
{{ item.actualValue }}
<span v-if="item.arrow" class="trend-arrow" :class="item.arrowClass">{{ item.arrow }}</span>
<template v-if="item.labelPairs && item.labelPairs.length > 0">
@@ -364,11 +361,16 @@
</span>
</template>
</div>
<div class="metric-cell-ref">{{ item.paramValue }}</div>
</div>
</template>
</el-table-column>
<el-table-column :label="col.title + '(基准)'" min-width="100" align="center" show-overflow-tooltip>
<template #default="{ row }">
<div v-for="(item, mi) in getMetricItemAt(row, ci)" :key="item.metricName + '-' + mi" class="table-metric-ref">
{{ item.paramValue }}
</div>
</template>
</el-table-column>
</template>
</el-table>
</div>
</el-card>
@@ -384,7 +386,9 @@
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
defineOptions({ name: 'SupplierPerformance' })
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import dayjs from 'dayjs'
@@ -410,10 +414,24 @@ const SUPPLIER_PERFORMANCE_MODULE_KEY = `${SUPPLIER_PERFORMANCE_COMPONENT}:main`
const REPORT_ID = 6
const route = useRoute()
const userStore = useUserStore()
const { openWithScreenshot, setPageLoading } = useAiAssistant()
const { openWithScreenshot, setPageLoading, setPageModuleName, setPageModuleCode, setScreenshotTarget } =
useAiAssistant()
const pageRootRef = ref<HTMLElement | null>(null)
const loading = ref(false)
watch(loading, (v) => setPageLoading(v), { immediate: true })
onUnmounted(() => setPageLoading(false))
onMounted(() => {
setPageModuleName('供应商表现')
setPageModuleCode(SUPPLIER_PERFORMANCE_MODULE_KEY)
nextTick(() => {
if (pageRootRef.value) setScreenshotTarget(pageRootRef.value)
})
})
onUnmounted(() => {
setPageLoading(false)
setPageModuleName(null)
setPageModuleCode(null)
setScreenshotTarget(null)
})
const promptMap = ref<Record<string, string>>({})
const promptEditVisible = ref(false)
@@ -436,7 +454,11 @@ function onCardAiAnalysis(e: MouseEvent) {
const card = (e.currentTarget as HTMLElement).closest('.supplier-card')
if (card instanceof HTMLElement) {
const prompt = promptMap.value[SUPPLIER_PERFORMANCE_MODULE_KEY]
openWithScreenshot(card, { moduleName: '供应商表现', prompt })
openWithScreenshot(card, {
moduleName: '供应商表现',
moduleCode: SUPPLIER_PERFORMANCE_MODULE_KEY,
prompt
})
}
}
@@ -447,10 +469,6 @@ const rows = ref<any[]>([])
/** 标签分类筛选(与主页供应商表现一致) */
const labelFilter = ref<string>('all')
const username = computed(() => {
return userStore.user?.username || ''
})
// 查询参数(与主页一致)
const queryParams = reactive({
rq: dayjs().subtract(6, 'day').format('YYYY-MM-DD'),
@@ -657,51 +675,35 @@ function mapKehuToOptions(list: any): { label: string; value: string }[] {
if (!Array.isArray(list)) return []
return list.map((item: any) => {
const label = item?.khmc ?? item?.CKMC ?? item?.KHMC ?? item?.DLMC ?? item?.label ?? item?.name ?? ''
const value = item?.khdm != null ? String(item.khdm) : item?.CKDM != null ? String(item.CKDM) : item?.KHDM != null ? String(item.KHDM) : item?.DLDM != null ? String(item.DLDM) : item?.value != null ? String(item.value) : item?.code != null ? String(item.code) : ''
const rawVal = item?.khdm ?? item?.CKDM ?? item?.KHDM ?? item?.DLDM ?? item?.value ?? item?.code
const value = rawVal != null ? String(rawVal) : ''
return { label, value }
}).filter((o) => o.label && o.value)
}
// 拉取下拉选项
async function fetchBrandOptions() {
try {
const res = await ReportApi.executeTable({ reportId: REPORT_ID, tableName: 'pinpai' })
brandOptions.value = mapPinpaiToOptions(resolveTableList(res))
} catch (_) {
brandOptions.value = []
}
}
// 拉取下拉选项(通用)
const OPTION_TABLE_MAP: Array<{
tableName: string
target: ReturnType<typeof ref<{ label: string; value: string }[]>>
mapper: (list: any) => { label: string; value: string }[]
}> = [
{ tableName: 'pinpai', target: brandOptions, mapper: mapPinpaiToOptions },
{ tableName: 'jijie', target: seasonOptions, mapper: mapJijieToOptions },
{ tableName: 'dalei', target: categoryOptions, mapper: mapDaleiToOptions },
{ tableName: 'kehu', target: storeOptions, mapper: mapKehuToOptions }
]
async function fetchSeasonOptions() {
async function fetchAllOptions() {
await Promise.all(
OPTION_TABLE_MAP.map(async ({ tableName, target, mapper }) => {
try {
const res = await ReportApi.executeTable({ reportId: REPORT_ID, tableName: 'jijie' })
seasonOptions.value = mapJijieToOptions(resolveTableList(res))
} catch (_) {
seasonOptions.value = []
const res = await ReportApi.executeTable({ reportId: REPORT_ID, tableName })
target.value = mapper(resolveTableList(res))
} catch {
target.value = []
}
}
async function fetchCategoryOptions() {
try {
const res = await ReportApi.executeTable({ reportId: REPORT_ID, tableName: 'dalei' })
categoryOptions.value = mapDaleiToOptions(resolveTableList(res))
} catch (_) {
categoryOptions.value = []
}
}
async function fetchStoreOptions() {
try {
const res = await ReportApi.executeTable({ reportId: REPORT_ID, tableName: 'kehu' })
storeOptions.value = mapKehuToOptions(resolveTableList(res))
} catch (_) {
storeOptions.value = []
}
}
/** 线路选项:暂不查后端,使用空默认值 */
function fetchLineOptions() {
lineOptions.value = []
})
)
}
// 查询条件默认值:门店默认全选,品牌有数据且含 1 则默认 1
@@ -721,6 +723,9 @@ const columnsSorted = computed(() => {
return cols.sort((a, b) => (a.order ?? 999) - (b.order ?? 999))
})
/** 列表表格:检测指标列(排除第一列 title */
const metricColumns = computed(() => columnsSorted.value.slice(1))
/** 从 rows 中收集所有 label/tag按逗号拆分去重后作为筛选项首项为「全部」 */
const labelOpts = computed(() => {
const labels = new Set<string>()
@@ -770,9 +775,8 @@ function parseLabelColorPairs(labelStr: unknown, colorStr: unknown): { label: st
return labels.map((label, i) => ({ label, color: colors[i] ?? 'flat' }))
}
function formatVal(val: unknown, key: string): string {
function formatVal(val: unknown): string {
if (val == null) return '-'
// 后端 value1/value2/value3 多为字符串(含 %),直接展示;数值则按千分位
if (typeof val === 'number') return val.toLocaleString('zh-CN')
return String(val)
}
@@ -824,16 +828,16 @@ function getTitleValue(row: any): string {
return v != null ? String(v).trim() : '-'
}
/** 获取数据列(排除第一列 title:按接口列配置全部展示,不要求 date 非空 */
function getColumnsWithDate(row: any): ColumnCfg[] {
/** 获取数据列(排除第一列 title */
function getColumnsWithDate(): ColumnCfg[] {
return columnsSorted.value.slice(1)
}
/** 生成表格数据:按列配置渲染,实际结果取 row[key],基准参考取 row[key+date] 或 '-' */
function getTableData(row: any): Array<{ metricName: string; actualValue: string; paramValue: string; valueColorClass?: string; arrow?: string; arrowClass?: string; labelPairs?: Array<{ label: string; color: string }> }> {
const colsWithDate = getColumnsWithDate(row)
const colsWithDate = getColumnsWithDate()
return colsWithDate.map((col) => {
const actualVal = formatVal((row as any)?.[col.key], col.key)
const actualVal = formatVal((row as any)?.[col.key])
const dateVal = (row as any)?.[col.key + 'date']
const paramVal = dateVal != null && String(dateVal).trim() !== '' ? String(dateVal).trim() : '-'
@@ -882,6 +886,13 @@ function getTableData(row: any): Array<{ metricName: string; actualValue: string
})
}
/** 获取某行第 ci 个指标项(用于列表表格按列展示),返回数组供 v-for 使用 */
function getMetricItemAt(row: any, ci: number) {
const data = getTableData(row)
const item = data[ci]
return item ? [item] : []
}
function parseResponseData(raw: unknown): { columns: ColumnCfg[]; list: any[] } {
if (Array.isArray(raw)) {
// 二维数组 [[列], [list]]
@@ -915,8 +926,6 @@ function parseResponseData(raw: unknown): { columns: ColumnCfg[]; list: any[] }
const handleQuery = async () => {
loading.value = true
try {
// 确保 username 有值:优先使用 computed如果为空则直接从 userStore 获取
const currentUsername = username.value || userStore.user?.username || ''
const res = await ReportApi.getCategoryPerformance({
reportId: REPORT_ID,
name: 'YDY_AI_GET_GHSZX',
@@ -927,7 +936,7 @@ const handleQuery = async () => {
dalei: arrToQuery(queryParams.category),
jj: arrToQuery(queryParams.season),
p: '123',
username: currentUsername,
username: userStore.user?.username || '',
ghsdm: supplierCode.value || undefined
})
const raw = (res as any)?.data ?? res
@@ -961,28 +970,9 @@ const handleReset = () => {
}
onMounted(async () => {
await loadPromptMap()
// 等待下一个 tick确保路由参数完全加载
await nextTick()
// 先初始化路由参数
initQueryParamsFromRoute()
// 并行获取下拉选项数据(这些不依赖路由参数,用于填充下拉框)
// 注意:这些调用会立即执行,但它们是获取下拉选项数据,不是主查询
await Promise.all([
fetchBrandOptions(),
fetchSeasonOptions(),
fetchCategoryOptions(),
fetchLineOptions(),
fetchStoreOptions()
])
// 设置默认值
await Promise.all([loadPromptMap(), fetchAllOptions()])
applyQueryDefaults()
// 最后执行主查询(此时路由参数已经读取完成,包括 code 参数)
// 主查询会使用 supplierCode.value从路由参数中读取的 code作为 ghsdm 参数
handleQuery()
})
</script>
@@ -1208,44 +1198,9 @@ onMounted(async () => {
}
}
.table-core {
display: flex;
flex-direction: column;
gap: 4px;
.core-name {
.table-metric-actual,
.table-metric-ref {
font-size: 12px;
color: var(--el-text-color-secondary);
}
.core-value {
font-weight: 700;
color: var(--el-text-color-primary);
}
}
.metric-cell {
display: flex;
flex-direction: column;
gap: 10px;
padding: 2px 0;
}
.metric-cell-row {
display: grid;
grid-template-columns: 160px 1fr 120px;
gap: 12px;
align-items: start;
}
.metric-cell-name {
font-size: 12px;
color: var(--el-text-color-regular);
}
.metric-cell-actual {
font-size: 12px;
color: var(--el-text-color-primary);
line-height: 1.4;
display: flex;
flex-wrap: wrap;
@@ -1272,21 +1227,18 @@ onMounted(async () => {
font-weight: 700;
display: inline-block;
&.badge-danger,
&.label-trend-up {
background: #fee2e2;
color: #ef4444;
border: 1px solid #fecaca;
}
&.badge-warn,
&.label-trend-flat {
background: #fef3c7;
color: #d97706;
border: 1px solid #fde68a;
}
&.badge-success,
&.label-trend-down {
background: #dcfce7;
color: #22c55e;
@@ -1295,12 +1247,22 @@ onMounted(async () => {
}
}
.metric-cell-ref {
font-size: 12px;
.table-metric-actual {
&.label-trend-up {
color: #ef4444;
}
&.label-trend-down {
color: #22c55e;
}
&.label-trend-flat {
color: #d97706;
}
}
.table-metric-ref {
color: var(--el-text-color-secondary);
text-align: right;
line-height: 1.4;
white-space: pre-line;
}
.empty {

View File

@@ -553,7 +553,7 @@
row-class-name="product-list-row-clickable"
@row-click="handleProductRowClick"
>
<el-table-column prop="productInfo" label="商品信息" width="280">
<el-table-column prop="productInfo" label="商品信息" width="200" fixed="left">
<template #default="{ row }">
<el-popover
placement="right"
@@ -564,25 +564,19 @@
@show="handleProductHoverShow(row)"
>
<template #reference>
<div class="product-info">
<div class="product-info product-info-compact">
<div class="product-image">
<img v-if="row.imageUrl" :src="row.imageUrl" alt="商品图片" class="product-img" />
<el-icon v-else><Picture /></el-icon>
</div>
<div class="product-details">
<div class="product-code">
款号:
<span
<div class="product-name">{{ row.name || '-' }}</div>
<div
class="product-code-link"
@click.stop="handleProductCodeClick(row)"
>
{{ getProductCode(row) }}
</span>
</div>
<div class="product-code">条码: {{ row.barcode || '-' }}</div>
<div class="product-code">颜色: {{ row.color }}</div>
<div class="product-code">进价: ¥{{ formatNumber(row.purchasePrice || 0) }}</div>
<div class="product-code">售价: ¥{{ formatNumber(row.sellingPrice || 0) }}</div>
</div>
</div>
</template>
@@ -718,13 +712,37 @@
</el-popover>
</template>
</el-table-column>
<!-- 第二列商品属性 -->
<el-table-column prop="category" label="商品属性" width="200" show-overflow-tooltip>
<!-- 商品属性拆开列展示 -->
<el-table-column prop="code" label="款号" min-width="100" show-overflow-tooltip>
<template #default="{ row }">
<div>{{ row.category }}</div>
<span
class="product-code-link"
@click.stop="handleProductCodeClick(row)"
>
{{ getProductCode(row) }}
</span>
</template>
</el-table-column>
<!-- 第三列销售数据 -->
<el-table-column prop="barcode" label="条码" min-width="120" show-overflow-tooltip>
<template #default="{ row }">{{ row.barcode || '-' }}</template>
</el-table-column>
<el-table-column prop="color" label="颜色" min-width="80" show-overflow-tooltip>
<template #default="{ row }">{{ row.color || '-' }}</template>
</el-table-column>
<el-table-column prop="category" label="类目" min-width="120" show-overflow-tooltip>
<template #default="{ row }">{{ row.category || '-' }}</template>
</el-table-column>
<el-table-column prop="purchasePrice" label="进价" min-width="90" align="right">
<template #default="{ row }">
{{ row.purchasePrice != null ? '¥' + formatNumber(row.purchasePrice) : '-' }}
</template>
</el-table-column>
<el-table-column prop="sellingPrice" label="售价" min-width="90" align="right">
<template #default="{ row }">
{{ row.sellingPrice != null ? '¥' + formatNumber(row.sellingPrice) : '-' }}
</template>
</el-table-column>
<!-- 销售数据 -->
<el-table-column prop="ls" label="销售数据" align="right" width="150">
<template #default="{ row }">
<div class="font-bold">{{ row.lsRaw || '-' }}</div>
@@ -3586,6 +3604,17 @@ onDeactivated(() => {
gap: 12px;
align-items: flex-start;
&.product-info-compact .product-details {
.product-name {
font-size: 13px;
font-weight: 600;
margin-bottom: 2px;
}
.product-code-link {
font-size: 12px;
}
}
.product-image {
width: 48px;
height: 56px;