1.加入ai对话框 模块日报功能
This commit is contained in:
4
.env.dev
4
.env.dev
@@ -4,8 +4,8 @@ NODE_ENV=production
|
|||||||
VITE_DEV=true
|
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:48080'
|
||||||
|
|
||||||
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
|
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
|
||||||
VITE_UPLOAD_TYPE=server
|
VITE_UPLOAD_TYPE=server
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ 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:48080'
|
||||||
VITE_BASE_URL='http://localhost:48080'
|
#VITE_BASE_URL='http://localhost:48080'
|
||||||
|
|
||||||
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
|
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
|
||||||
VITE_UPLOAD_TYPE=server
|
VITE_UPLOAD_TYPE=server
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface TenantVO {
|
|||||||
accountCount: number
|
accountCount: number
|
||||||
websites: string[]
|
websites: string[]
|
||||||
createTime: Date
|
createTime: Date
|
||||||
|
tenantPrompt?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TenantPageReqVO extends PageParam {
|
export interface TenantPageReqVO extends PageParam {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
BarChart,
|
BarChart,
|
||||||
FunnelChart,
|
FunnelChart,
|
||||||
GaugeChart,
|
GaugeChart,
|
||||||
|
HeatmapChart,
|
||||||
LineChart,
|
LineChart,
|
||||||
MapChart,
|
MapChart,
|
||||||
PictorialBarChart,
|
PictorialBarChart,
|
||||||
@@ -13,6 +14,7 @@ import {
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
AriaComponent,
|
AriaComponent,
|
||||||
|
CalendarComponent,
|
||||||
DataZoomComponent,
|
DataZoomComponent,
|
||||||
GridComponent,
|
GridComponent,
|
||||||
LegendComponent,
|
LegendComponent,
|
||||||
@@ -37,6 +39,7 @@ echarts.use([
|
|||||||
AriaComponent,
|
AriaComponent,
|
||||||
ParallelComponent,
|
ParallelComponent,
|
||||||
VisualMapComponent,
|
VisualMapComponent,
|
||||||
|
CalendarComponent,
|
||||||
BarChart,
|
BarChart,
|
||||||
LineChart,
|
LineChart,
|
||||||
PieChart,
|
PieChart,
|
||||||
@@ -45,7 +48,8 @@ echarts.use([
|
|||||||
PictorialBarChart,
|
PictorialBarChart,
|
||||||
RadarChart,
|
RadarChart,
|
||||||
GaugeChart,
|
GaugeChart,
|
||||||
FunnelChart
|
FunnelChart,
|
||||||
|
HeatmapChart
|
||||||
])
|
])
|
||||||
|
|
||||||
export default echarts
|
export default echarts
|
||||||
|
|||||||
@@ -792,7 +792,6 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
|||||||
name: 'SupplierPerformance',
|
name: 'SupplierPerformance',
|
||||||
meta: {
|
meta: {
|
||||||
title: '供应商详情',
|
title: '供应商详情',
|
||||||
noCache: true,
|
|
||||||
hidden: true,
|
hidden: true,
|
||||||
canTo: true
|
canTo: true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -61,6 +61,9 @@
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</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-form-item label="租户状态" prop="status">
|
||||||
<el-radio-group v-model="formData.status">
|
<el-radio-group v-model="formData.status">
|
||||||
<el-radio
|
<el-radio
|
||||||
@@ -103,13 +106,15 @@ const formData = ref({
|
|||||||
expireTime: undefined,
|
expireTime: undefined,
|
||||||
websites: [],
|
websites: [],
|
||||||
status: CommonStatusEnum.ENABLE,
|
status: CommonStatusEnum.ENABLE,
|
||||||
|
tenantPrompt: '',
|
||||||
// 新增专属
|
// 新增专属
|
||||||
username: undefined,
|
username: undefined,
|
||||||
password: undefined
|
password: undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
const formRules = reactive({
|
const formRules = reactive({
|
||||||
name: [{ required: true, message: '租户名不能为空', trigger: 'blur' }],
|
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' }],
|
contactName: [{ required: true, message: '联系人不能为空', trigger: 'blur' }],
|
||||||
status: [{ required: true, message: '租户状态不能为空', trigger: 'blur' }],
|
status: [{ required: true, message: '租户状态不能为空', trigger: 'blur' }],
|
||||||
accountCount: [{ required: true, message: '账号额度不能为空', trigger: 'blur' }],
|
accountCount: [{ required: true, message: '账号额度不能为空', trigger: 'blur' }],
|
||||||
@@ -178,6 +183,7 @@ const resetForm = () => {
|
|||||||
expireTime: undefined,
|
expireTime: undefined,
|
||||||
websites: [],
|
websites: [],
|
||||||
status: CommonStatusEnum.ENABLE,
|
status: CommonStatusEnum.ENABLE,
|
||||||
|
tenantPrompt: '',
|
||||||
username: undefined,
|
username: undefined,
|
||||||
password: undefined
|
password: undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="supplier-performance-page">
|
<div ref="pageRootRef" class="supplier-performance-page">
|
||||||
<!-- 查询条件区域:与主页一致 -->
|
<!-- 查询条件区域:与主页一致 -->
|
||||||
<el-card class="query-card" shadow="never">
|
<el-card class="query-card" shadow="never">
|
||||||
<div class="query-form">
|
<div class="query-form">
|
||||||
@@ -265,7 +265,7 @@
|
|||||||
</div>
|
</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">
|
<div class="grid-layout metric-header">
|
||||||
<span>检测指标</span>
|
<span>检测指标</span>
|
||||||
<span>实际结果</span>
|
<span>实际结果</span>
|
||||||
@@ -313,10 +313,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 列表形式(不需要悬浮卡片) -->
|
<!-- 列表形式:每个字段独立一列铺开展示 -->
|
||||||
<div v-show="displayMode === 'table'" class="table-wrap">
|
<div v-show="displayMode === 'table'" class="table-wrap">
|
||||||
<el-table :data="visibleRows" v-loading="loading" border stripe style="width: 100%">
|
<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 }">
|
<template #default="{ row }">
|
||||||
<div class="table-title">
|
<div class="table-title">
|
||||||
<div class="table-title-main">{{ String((row as any)?.title ?? (row as any)?.name ?? '-') }}</div>
|
<div class="table-title-main">{{ String((row as any)?.title ?? (row as any)?.name ?? '-') }}</div>
|
||||||
@@ -333,42 +333,44 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="核心指标" width="160" align="center">
|
<el-table-column label="核心指标名称" min-width="120" align="center" show-overflow-tooltip>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">{{ getTitleName(row) || '-' }}</template>
|
||||||
<div class="table-core">
|
|
||||||
<div class="core-name">{{ getTitleName(row) || '-' }}</div>
|
|
||||||
<div class="core-value">{{ getTitleValue(row) || '-' }}</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
</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 }">
|
<template #default="{ row }">{{ getTitleValue(row) || '-' }}</template>
|
||||||
<div class="metric-cell">
|
</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
|
<div
|
||||||
v-for="(item, mi) in getTableData(row)"
|
v-for="(item, mi) in getMetricItemAt(row, ci)"
|
||||||
:key="mi"
|
:key="item.metricName + '-' + mi"
|
||||||
class="metric-cell-row"
|
class="table-metric-actual"
|
||||||
|
:class="item.valueColorClass"
|
||||||
>
|
>
|
||||||
<div class="metric-cell-name">{{ item.metricName }}</div>
|
{{ item.actualValue }}
|
||||||
<div class="metric-cell-actual" :class="item.valueColorClass">
|
<span v-if="item.arrow" class="trend-arrow" :class="item.arrowClass">{{ item.arrow }}</span>
|
||||||
{{ item.actualValue }}
|
<template v-if="item.labelPairs && item.labelPairs.length > 0">
|
||||||
<span v-if="item.arrow" class="trend-arrow" :class="item.arrowClass">{{ item.arrow }}</span>
|
<span
|
||||||
<template v-if="item.labelPairs && item.labelPairs.length > 0">
|
v-for="(pair, pi) in item.labelPairs"
|
||||||
<span
|
:key="pi"
|
||||||
v-for="(pair, pi) in item.labelPairs"
|
class="badge-small"
|
||||||
:key="pi"
|
:class="getTrendClassFromColor(pair.color)"
|
||||||
class="badge-small"
|
>
|
||||||
:class="getTrendClassFromColor(pair.color)"
|
{{ pair.label }}
|
||||||
>
|
</span>
|
||||||
{{ pair.label }}
|
</template>
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div class="metric-cell-ref">{{ item.paramValue }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
</el-table-column>
|
||||||
</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>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -384,7 +386,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { useRoute } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
@@ -410,10 +414,24 @@ const SUPPLIER_PERFORMANCE_MODULE_KEY = `${SUPPLIER_PERFORMANCE_COMPONENT}:main`
|
|||||||
const REPORT_ID = 6
|
const REPORT_ID = 6
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const { openWithScreenshot, setPageLoading } = useAiAssistant()
|
const { openWithScreenshot, setPageLoading, setPageModuleName, setPageModuleCode, setScreenshotTarget } =
|
||||||
|
useAiAssistant()
|
||||||
|
const pageRootRef = ref<HTMLElement | null>(null)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
watch(loading, (v) => setPageLoading(v), { immediate: true })
|
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 promptMap = ref<Record<string, string>>({})
|
||||||
const promptEditVisible = ref(false)
|
const promptEditVisible = ref(false)
|
||||||
@@ -436,7 +454,11 @@ function onCardAiAnalysis(e: MouseEvent) {
|
|||||||
const card = (e.currentTarget as HTMLElement).closest('.supplier-card')
|
const card = (e.currentTarget as HTMLElement).closest('.supplier-card')
|
||||||
if (card instanceof HTMLElement) {
|
if (card instanceof HTMLElement) {
|
||||||
const prompt = promptMap.value[SUPPLIER_PERFORMANCE_MODULE_KEY]
|
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 labelFilter = ref<string>('all')
|
||||||
|
|
||||||
const username = computed(() => {
|
|
||||||
return userStore.user?.username || ''
|
|
||||||
})
|
|
||||||
|
|
||||||
// 查询参数(与主页一致)
|
// 查询参数(与主页一致)
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
rq: dayjs().subtract(6, 'day').format('YYYY-MM-DD'),
|
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 []
|
if (!Array.isArray(list)) return []
|
||||||
return list.map((item: any) => {
|
return list.map((item: any) => {
|
||||||
const label = item?.khmc ?? item?.CKMC ?? item?.KHMC ?? item?.DLMC ?? item?.label ?? item?.name ?? ''
|
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 }
|
return { label, value }
|
||||||
}).filter((o) => o.label && o.value)
|
}).filter((o) => o.label && o.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 拉取下拉选项
|
// 拉取下拉选项(通用)
|
||||||
async function fetchBrandOptions() {
|
const OPTION_TABLE_MAP: Array<{
|
||||||
try {
|
tableName: string
|
||||||
const res = await ReportApi.executeTable({ reportId: REPORT_ID, tableName: 'pinpai' })
|
target: ReturnType<typeof ref<{ label: string; value: string }[]>>
|
||||||
brandOptions.value = mapPinpaiToOptions(resolveTableList(res))
|
mapper: (list: any) => { label: string; value: string }[]
|
||||||
} catch (_) {
|
}> = [
|
||||||
brandOptions.value = []
|
{ 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() {
|
||||||
try {
|
await Promise.all(
|
||||||
const res = await ReportApi.executeTable({ reportId: REPORT_ID, tableName: 'jijie' })
|
OPTION_TABLE_MAP.map(async ({ tableName, target, mapper }) => {
|
||||||
seasonOptions.value = mapJijieToOptions(resolveTableList(res))
|
try {
|
||||||
} catch (_) {
|
const res = await ReportApi.executeTable({ reportId: REPORT_ID, tableName })
|
||||||
seasonOptions.value = []
|
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
|
// 查询条件默认值:门店默认全选,品牌有数据且含 1 则默认 1
|
||||||
@@ -721,6 +723,9 @@ const columnsSorted = computed(() => {
|
|||||||
return cols.sort((a, b) => (a.order ?? 999) - (b.order ?? 999))
|
return cols.sort((a, b) => (a.order ?? 999) - (b.order ?? 999))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** 列表表格:检测指标列(排除第一列 title) */
|
||||||
|
const metricColumns = computed(() => columnsSorted.value.slice(1))
|
||||||
|
|
||||||
/** 从 rows 中收集所有 label/tag(按逗号拆分)去重后作为筛选项,首项为「全部」 */
|
/** 从 rows 中收集所有 label/tag(按逗号拆分)去重后作为筛选项,首项为「全部」 */
|
||||||
const labelOpts = computed(() => {
|
const labelOpts = computed(() => {
|
||||||
const labels = new Set<string>()
|
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' }))
|
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 '-'
|
if (val == null) return '-'
|
||||||
// 后端 value1/value2/value3 多为字符串(含 %),直接展示;数值则按千分位
|
|
||||||
if (typeof val === 'number') return val.toLocaleString('zh-CN')
|
if (typeof val === 'number') return val.toLocaleString('zh-CN')
|
||||||
return String(val)
|
return String(val)
|
||||||
}
|
}
|
||||||
@@ -824,16 +828,16 @@ function getTitleValue(row: any): string {
|
|||||||
return v != null ? String(v).trim() : '-'
|
return v != null ? String(v).trim() : '-'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取数据列(排除第一列 title):按接口列配置全部展示,不要求 date 非空 */
|
/** 获取数据列(排除第一列 title) */
|
||||||
function getColumnsWithDate(row: any): ColumnCfg[] {
|
function getColumnsWithDate(): ColumnCfg[] {
|
||||||
return columnsSorted.value.slice(1)
|
return columnsSorted.value.slice(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 生成表格数据:按列配置渲染,实际结果取 row[key],基准参考取 row[key+date] 或 '-' */
|
/** 生成表格数据:按列配置渲染,实际结果取 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 }> }> {
|
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) => {
|
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 dateVal = (row as any)?.[col.key + 'date']
|
||||||
const paramVal = dateVal != null && String(dateVal).trim() !== '' ? String(dateVal).trim() : '-'
|
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[] } {
|
function parseResponseData(raw: unknown): { columns: ColumnCfg[]; list: any[] } {
|
||||||
if (Array.isArray(raw)) {
|
if (Array.isArray(raw)) {
|
||||||
// 二维数组 [[列], [list]]
|
// 二维数组 [[列], [list]]
|
||||||
@@ -915,8 +926,6 @@ function parseResponseData(raw: unknown): { columns: ColumnCfg[]; list: any[] }
|
|||||||
const handleQuery = async () => {
|
const handleQuery = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
// 确保 username 有值:优先使用 computed,如果为空则直接从 userStore 获取
|
|
||||||
const currentUsername = username.value || userStore.user?.username || ''
|
|
||||||
const res = await ReportApi.getCategoryPerformance({
|
const res = await ReportApi.getCategoryPerformance({
|
||||||
reportId: REPORT_ID,
|
reportId: REPORT_ID,
|
||||||
name: 'YDY_AI_GET_GHSZX',
|
name: 'YDY_AI_GET_GHSZX',
|
||||||
@@ -927,7 +936,7 @@ const handleQuery = async () => {
|
|||||||
dalei: arrToQuery(queryParams.category),
|
dalei: arrToQuery(queryParams.category),
|
||||||
jj: arrToQuery(queryParams.season),
|
jj: arrToQuery(queryParams.season),
|
||||||
p: '123',
|
p: '123',
|
||||||
username: currentUsername,
|
username: userStore.user?.username || '',
|
||||||
ghsdm: supplierCode.value || undefined
|
ghsdm: supplierCode.value || undefined
|
||||||
})
|
})
|
||||||
const raw = (res as any)?.data ?? res
|
const raw = (res as any)?.data ?? res
|
||||||
@@ -961,28 +970,9 @@ const handleReset = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadPromptMap()
|
|
||||||
// 等待下一个 tick,确保路由参数完全加载
|
|
||||||
await nextTick()
|
|
||||||
|
|
||||||
// 先初始化路由参数
|
|
||||||
initQueryParamsFromRoute()
|
initQueryParamsFromRoute()
|
||||||
|
await Promise.all([loadPromptMap(), fetchAllOptions()])
|
||||||
// 并行获取下拉选项数据(这些不依赖路由参数,用于填充下拉框)
|
|
||||||
// 注意:这些调用会立即执行,但它们是获取下拉选项数据,不是主查询
|
|
||||||
await Promise.all([
|
|
||||||
fetchBrandOptions(),
|
|
||||||
fetchSeasonOptions(),
|
|
||||||
fetchCategoryOptions(),
|
|
||||||
fetchLineOptions(),
|
|
||||||
fetchStoreOptions()
|
|
||||||
])
|
|
||||||
|
|
||||||
// 设置默认值
|
|
||||||
applyQueryDefaults()
|
applyQueryDefaults()
|
||||||
|
|
||||||
// 最后执行主查询(此时路由参数已经读取完成,包括 code 参数)
|
|
||||||
// 主查询会使用 supplierCode.value(从路由参数中读取的 code)作为 ghsdm 参数
|
|
||||||
handleQuery()
|
handleQuery()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -1208,44 +1198,9 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-core {
|
.table-metric-actual,
|
||||||
display: flex;
|
.table-metric-ref {
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
|
|
||||||
.core-name {
|
|
||||||
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;
|
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;
|
line-height: 1.4;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -1272,21 +1227,18 @@ onMounted(async () => {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
&.badge-danger,
|
|
||||||
&.label-trend-up {
|
&.label-trend-up {
|
||||||
background: #fee2e2;
|
background: #fee2e2;
|
||||||
color: #ef4444;
|
color: #ef4444;
|
||||||
border: 1px solid #fecaca;
|
border: 1px solid #fecaca;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.badge-warn,
|
|
||||||
&.label-trend-flat {
|
&.label-trend-flat {
|
||||||
background: #fef3c7;
|
background: #fef3c7;
|
||||||
color: #d97706;
|
color: #d97706;
|
||||||
border: 1px solid #fde68a;
|
border: 1px solid #fde68a;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.badge-success,
|
|
||||||
&.label-trend-down {
|
&.label-trend-down {
|
||||||
background: #dcfce7;
|
background: #dcfce7;
|
||||||
color: #22c55e;
|
color: #22c55e;
|
||||||
@@ -1295,12 +1247,22 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-cell-ref {
|
.table-metric-actual {
|
||||||
font-size: 12px;
|
&.label-trend-up {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.label-trend-down {
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.label-trend-flat {
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-metric-ref {
|
||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
text-align: right;
|
|
||||||
line-height: 1.4;
|
|
||||||
white-space: pre-line;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
|
|||||||
@@ -553,7 +553,7 @@
|
|||||||
row-class-name="product-list-row-clickable"
|
row-class-name="product-list-row-clickable"
|
||||||
@row-click="handleProductRowClick"
|
@row-click="handleProductRowClick"
|
||||||
>
|
>
|
||||||
<el-table-column prop="productInfo" label="商品信息" width="280">
|
<el-table-column prop="productInfo" label="商品信息" width="200" fixed="left">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-popover
|
<el-popover
|
||||||
placement="right"
|
placement="right"
|
||||||
@@ -564,25 +564,19 @@
|
|||||||
@show="handleProductHoverShow(row)"
|
@show="handleProductHoverShow(row)"
|
||||||
>
|
>
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<div class="product-info">
|
<div class="product-info product-info-compact">
|
||||||
<div class="product-image">
|
<div class="product-image">
|
||||||
<img v-if="row.imageUrl" :src="row.imageUrl" alt="商品图片" class="product-img" />
|
<img v-if="row.imageUrl" :src="row.imageUrl" alt="商品图片" class="product-img" />
|
||||||
<el-icon v-else><Picture /></el-icon>
|
<el-icon v-else><Picture /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
<div class="product-details">
|
<div class="product-details">
|
||||||
<div class="product-code">
|
<div class="product-name">{{ row.name || '-' }}</div>
|
||||||
款号:
|
<div
|
||||||
<span
|
class="product-code-link"
|
||||||
class="product-code-link"
|
@click.stop="handleProductCodeClick(row)"
|
||||||
@click.stop="handleProductCodeClick(row)"
|
>
|
||||||
>
|
{{ getProductCode(row) }}
|
||||||
{{ getProductCode(row) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -718,13 +712,37 @@
|
|||||||
</el-popover>
|
</el-popover>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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 }">
|
<template #default="{ row }">
|
||||||
<div>{{ row.category }}</div>
|
<span
|
||||||
|
class="product-code-link"
|
||||||
|
@click.stop="handleProductCodeClick(row)"
|
||||||
|
>
|
||||||
|
{{ getProductCode(row) }}
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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">
|
<el-table-column prop="ls" label="销售数据" align="right" width="150">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="font-bold">{{ row.lsRaw || '-' }}</div>
|
<div class="font-bold">{{ row.lsRaw || '-' }}</div>
|
||||||
@@ -3586,6 +3604,17 @@ onDeactivated(() => {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
align-items: flex-start;
|
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 {
|
.product-image {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
|
|||||||
Reference in New Issue
Block a user