1. 提交代码
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import type { App } from 'vue'
|
import type { App } from 'vue'
|
||||||
|
import { toRaw } from 'vue'
|
||||||
import { useUserStore } from '@/store/modules/user'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
|
|
||||||
const { t } = useI18n() // 国际化
|
const { t } = useI18n() // 国际化
|
||||||
@@ -24,8 +25,8 @@ export function hasPermi(app: App<Element>) {
|
|||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const all_permission = '*:*:*'
|
const all_permission = '*:*:*'
|
||||||
export const hasPermission = (permission: string[]) => {
|
export const hasPermission = (permission: string[]) => {
|
||||||
return (
|
// Vue 3.5+ 对响应式 Set 存字符串时,直接 .has 可能触发 WeakMap keys must be objects
|
||||||
userStore.permissions.has(all_permission) ||
|
const raw = toRaw(userStore.permissions) as Set<string> | undefined
|
||||||
permission.some((permission) => userStore.permissions.has(permission))
|
if (!raw?.size) return false
|
||||||
)
|
return raw.has(all_permission) || permission.some((p) => raw.has(p))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,17 +15,20 @@ export const useTagsView = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const closeLeft = (callback?: Fn) => {
|
const closeLeft = (callback?: Fn) => {
|
||||||
tagsViewStore.delLeftViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
|
const tag = unref(selectedTag)
|
||||||
|
if (tag) tagsViewStore.delLeftViews(tag as RouteLocationNormalizedLoaded)
|
||||||
callback?.()
|
callback?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeRight = (callback?: Fn) => {
|
const closeRight = (callback?: Fn) => {
|
||||||
tagsViewStore.delRightViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
|
const tag = unref(selectedTag)
|
||||||
|
if (tag) tagsViewStore.delRightViews(tag as RouteLocationNormalizedLoaded)
|
||||||
callback?.()
|
callback?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeOther = (callback?: Fn) => {
|
const closeOther = (callback?: Fn) => {
|
||||||
tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
|
const tag = unref(selectedTag)
|
||||||
|
if (tag) tagsViewStore.delOthersViews(tag as RouteLocationNormalizedLoaded)
|
||||||
callback?.()
|
callback?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, nextTick, onMounted, ref, unref, watch } from 'vue'
|
import { computed, nextTick, onMounted, ref, unref, watch } from 'vue'
|
||||||
import type { RouteLocationNormalizedLoaded, RouterLinkProps } from 'vue-router'
|
import type {
|
||||||
|
RouteLocationNormalizedLoaded,
|
||||||
|
RouteLocationRaw,
|
||||||
|
RouterLinkProps
|
||||||
|
} from 'vue-router'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { usePermissionStore } from '@/store/modules/permission'
|
import { usePermissionStore } from '@/store/modules/permission'
|
||||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||||
@@ -83,17 +87,17 @@ const toLastView = () => {
|
|||||||
const visitedViews = tagsViewStore.getVisitedViews
|
const visitedViews = tagsViewStore.getVisitedViews
|
||||||
const latestView = visitedViews.slice(-1)[0]
|
const latestView = visitedViews.slice(-1)[0]
|
||||||
if (latestView) {
|
if (latestView) {
|
||||||
push(latestView)
|
// 使用 fullPath 字符串,避免 push(整段 RouteLocation) 在部分环境下触发 WeakMap 相关运行时错误
|
||||||
|
push(latestView.fullPath)
|
||||||
} else {
|
} else {
|
||||||
if (
|
const first = permissionStore.getAddRouters[0]
|
||||||
unref(currentRoute).path === permissionStore.getAddRouters[0].path ||
|
if (!first?.path) return
|
||||||
unref(currentRoute).path === permissionStore.getAddRouters[0].redirect
|
const curPath = unref(currentRoute).path
|
||||||
) {
|
if (curPath === first.path || curPath === first.redirect) {
|
||||||
addTags()
|
addTags()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// You can set another route
|
push(first.path)
|
||||||
push(permissionStore.getAddRouters[0].path)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,8 +141,20 @@ const moveToCurrentTag = async () => {
|
|||||||
|
|
||||||
const tagLinksRefs = useTemplateRefsList<RouterLinkProps>()
|
const tagLinksRefs = useTemplateRefsList<RouterLinkProps>()
|
||||||
|
|
||||||
|
/** router-link 的 to 可能是 fullPath 字符串或对象,统一成路径字符串再比较 */
|
||||||
|
function resolveRouterLinkToPath(to: RouteLocationRaw | undefined): string | undefined {
|
||||||
|
if (to == null) return undefined
|
||||||
|
if (typeof to === 'string') return to
|
||||||
|
if (typeof to === 'object' && 'fullPath' in to && typeof (to as RouteLocationNormalizedLoaded).fullPath === 'string') {
|
||||||
|
return (to as RouteLocationNormalizedLoaded).fullPath
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
const moveToTarget = (currentTag: RouteLocationNormalizedLoaded) => {
|
const moveToTarget = (currentTag: RouteLocationNormalizedLoaded) => {
|
||||||
const wrap$ = unref(scrollbarRef)?.wrapRef
|
const wrap$ = unref(scrollbarRef)?.wrapRef
|
||||||
|
if (!wrap$) return
|
||||||
|
|
||||||
let firstTag: Nullable<RouterLinkProps> = null
|
let firstTag: Nullable<RouterLinkProps> = null
|
||||||
let lastTag: Nullable<RouterLinkProps> = null
|
let lastTag: Nullable<RouterLinkProps> = null
|
||||||
|
|
||||||
@@ -148,33 +164,37 @@ const moveToTarget = (currentTag: RouteLocationNormalizedLoaded) => {
|
|||||||
firstTag = tagList[0]
|
firstTag = tagList[0]
|
||||||
lastTag = tagList[tagList.length - 1]
|
lastTag = tagList[tagList.length - 1]
|
||||||
}
|
}
|
||||||
if ((firstTag?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath) {
|
const cur = currentTag.fullPath
|
||||||
|
if (resolveRouterLinkToPath(firstTag?.to) === cur) {
|
||||||
// 直接滚动到0的位置
|
// 直接滚动到0的位置
|
||||||
const { start } = useScrollTo({
|
const { start } = useScrollTo({
|
||||||
el: wrap$!,
|
el: wrap$,
|
||||||
position: 'scrollLeft',
|
position: 'scrollLeft',
|
||||||
to: 0,
|
to: 0,
|
||||||
duration: 500
|
duration: 500
|
||||||
})
|
})
|
||||||
start()
|
start()
|
||||||
} else if ((lastTag?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath) {
|
} else if (resolveRouterLinkToPath(lastTag?.to) === cur) {
|
||||||
// 滚动到最后的位置
|
// 滚动到最后的位置
|
||||||
const { start } = useScrollTo({
|
const { start } = useScrollTo({
|
||||||
el: wrap$!,
|
el: wrap$,
|
||||||
position: 'scrollLeft',
|
position: 'scrollLeft',
|
||||||
to: wrap$!.scrollWidth - wrap$!.offsetWidth,
|
to: wrap$.scrollWidth - wrap$.offsetWidth,
|
||||||
duration: 500
|
duration: 500
|
||||||
})
|
})
|
||||||
start()
|
start()
|
||||||
} else {
|
} else {
|
||||||
// find preTag and nextTag
|
// find preTag and nextTag
|
||||||
const currentIndex: number = tagList.findIndex(
|
const currentIndex: number = tagList.findIndex(
|
||||||
(item) => (item?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath
|
(item) => resolveRouterLinkToPath(item?.to) === cur
|
||||||
)
|
)
|
||||||
|
if (currentIndex < 0) return
|
||||||
|
|
||||||
const tgsRefs = document.getElementsByClassName(`${prefixCls}__item`)
|
const tgsRefs = document.getElementsByClassName(`${prefixCls}__item`)
|
||||||
|
|
||||||
const prevTag = tgsRefs[currentIndex - 1] as HTMLElement
|
const prevTag = tgsRefs[currentIndex - 1] as HTMLElement | undefined
|
||||||
const nextTag = tgsRefs[currentIndex + 1] as HTMLElement
|
const nextTag = tgsRefs[currentIndex + 1] as HTMLElement | undefined
|
||||||
|
if (!nextTag || !prevTag) return
|
||||||
|
|
||||||
// the tag's offsetLeft after of nextTag
|
// the tag's offsetLeft after of nextTag
|
||||||
const afterNextTagOffsetLeft = nextTag.offsetLeft + nextTag.offsetWidth + 4
|
const afterNextTagOffsetLeft = nextTag.offsetLeft + nextTag.offsetWidth + 4
|
||||||
@@ -182,17 +202,17 @@ const moveToTarget = (currentTag: RouteLocationNormalizedLoaded) => {
|
|||||||
// the tag's offsetLeft before of prevTag
|
// the tag's offsetLeft before of prevTag
|
||||||
const beforePrevTagOffsetLeft = prevTag.offsetLeft - 4
|
const beforePrevTagOffsetLeft = prevTag.offsetLeft - 4
|
||||||
|
|
||||||
if (afterNextTagOffsetLeft > unref(scrollLeftNumber) + wrap$!.offsetWidth) {
|
if (afterNextTagOffsetLeft > unref(scrollLeftNumber) + wrap$.offsetWidth) {
|
||||||
const { start } = useScrollTo({
|
const { start } = useScrollTo({
|
||||||
el: wrap$!,
|
el: wrap$,
|
||||||
position: 'scrollLeft',
|
position: 'scrollLeft',
|
||||||
to: afterNextTagOffsetLeft - wrap$!.offsetWidth,
|
to: afterNextTagOffsetLeft - wrap$.offsetWidth,
|
||||||
duration: 500
|
duration: 500
|
||||||
})
|
})
|
||||||
start()
|
start()
|
||||||
} else if (beforePrevTagOffsetLeft < unref(scrollLeftNumber)) {
|
} else if (beforePrevTagOffsetLeft < unref(scrollLeftNumber)) {
|
||||||
const { start } = useScrollTo({
|
const { start } = useScrollTo({
|
||||||
el: wrap$!,
|
el: wrap$,
|
||||||
position: 'scrollLeft',
|
position: 'scrollLeft',
|
||||||
to: beforePrevTagOffsetLeft,
|
to: beforePrevTagOffsetLeft,
|
||||||
duration: 500
|
duration: 500
|
||||||
@@ -364,7 +384,7 @@ watch(
|
|||||||
@visible-change="visibleChange"
|
@visible-change="visibleChange"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<router-link :ref="tagLinksRefs.set" v-slot="{ navigate }" :to="{ ...item }" custom>
|
<router-link :ref="tagLinksRefs.set" v-slot="{ navigate }" :to="item.fullPath" custom>
|
||||||
<div
|
<div
|
||||||
:class="`h-full flex items-center justify-center whitespace-nowrap pl-15px ${prefixCls}__item--label`"
|
:class="`h-full flex items-center justify-center whitespace-nowrap pl-15px ${prefixCls}__item--label`"
|
||||||
@click="navigate"
|
@click="navigate"
|
||||||
|
|||||||
@@ -74,7 +74,8 @@ export const generateRoute = (routes: AppCustomRouteRecordRaw[]): AppRouteRecord
|
|||||||
alwaysShow:
|
alwaysShow:
|
||||||
route.children &&
|
route.children &&
|
||||||
route.children.length > 0 &&
|
route.children.length > 0 &&
|
||||||
(route.alwaysShow !== undefined ? route.alwaysShow : true)
|
(route.alwaysShow !== undefined ? route.alwaysShow : true),
|
||||||
|
...(route.id != null ? { menuId: Number(route.id) } : {})
|
||||||
} as any
|
} as any
|
||||||
// 特殊逻辑:如果后端配置的 MenuDO.component 包含 ?,则表示需要传递参数
|
// 特殊逻辑:如果后端配置的 MenuDO.component 包含 ?,则表示需要传递参数
|
||||||
// 此时,我们需要解析参数,并且将参数放到 meta.query 中
|
// 此时,我们需要解析参数,并且将参数放到 meta.query 中
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="head-container">
|
<div class="head-container">
|
||||||
<el-input v-model="deptName" class="mb-20px" clearable placeholder="请输入部门名称">
|
<el-input
|
||||||
|
v-model="deptName"
|
||||||
|
class="mb-20px"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入部门名称,回车查询"
|
||||||
|
@keyup.enter="applyDeptFilter"
|
||||||
|
@clear="applyDeptFilter"
|
||||||
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<Icon icon="ep:search" />
|
<Icon icon="ep:search" />
|
||||||
</template>
|
</template>
|
||||||
@@ -45,6 +52,11 @@ const filterNode = (name: string, data: Tree) => {
|
|||||||
return data.name.includes(name)
|
return data.name.includes(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 输入完成后回车(或点清空)再筛选,避免每键都过滤 */
|
||||||
|
const applyDeptFilter = () => {
|
||||||
|
treeRef.value?.filter(deptName.value)
|
||||||
|
}
|
||||||
|
|
||||||
/** 处理部门被点击 */
|
/** 处理部门被点击 */
|
||||||
let currentNode: any = {}
|
let currentNode: any = {}
|
||||||
const handleNodeClick = async (row: { [key: string]: any }, treeNode: any) => {
|
const handleNodeClick = async (row: { [key: string]: any }, treeNode: any) => {
|
||||||
@@ -67,11 +79,6 @@ const handleNodeClick = async (row: { [key: string]: any }, treeNode: any) => {
|
|||||||
}
|
}
|
||||||
const emits = defineEmits(['node-click'])
|
const emits = defineEmits(['node-click'])
|
||||||
|
|
||||||
/** 监听deptName */
|
|
||||||
watch(deptName, (val) => {
|
|
||||||
treeRef.value!.filter(val)
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 初始化 */
|
/** 初始化 */
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await getTree()
|
await getTree()
|
||||||
|
|||||||
@@ -828,7 +828,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed, watch, onMounted, onUnmounted, onBeforeUnmount, onDeactivated, nextTick } from 'vue'
|
import {
|
||||||
|
ref,
|
||||||
|
reactive,
|
||||||
|
computed,
|
||||||
|
watch,
|
||||||
|
onMounted,
|
||||||
|
onUnmounted,
|
||||||
|
onBeforeUnmount,
|
||||||
|
onActivated,
|
||||||
|
onDeactivated,
|
||||||
|
nextTick
|
||||||
|
} from 'vue'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import type { EChartsOption } from 'echarts'
|
import type { EChartsOption } from 'echarts'
|
||||||
import Echart from '@/components/Echart/src/Echart.vue'
|
import Echart from '@/components/Echart/src/Echart.vue'
|
||||||
@@ -844,7 +855,7 @@ import { useUserStore } from '@/store/modules/user'
|
|||||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Icon } from '@/components/Icon'
|
import { Icon } from '@/components/Icon'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import type { CategoryDiagnosticData } from './components/categoryCardListComponents.vue'
|
import type { CategoryDiagnosticData } from './components/categoryCardListComponents.vue'
|
||||||
import { useAiAssistant } from '@/components/AiAssistant/useAiAssistant'
|
import { useAiAssistant } from '@/components/AiAssistant/useAiAssistant'
|
||||||
|
|
||||||
@@ -1264,7 +1275,26 @@ const loadingPie = ref(false)
|
|||||||
/** 商品明细列表加载状态 */
|
/** 商品明细列表加载状态 */
|
||||||
const loadingProductList = ref(false)
|
const loadingProductList = ref(false)
|
||||||
|
|
||||||
const { setPageLoading, setPageModuleName, setPageModuleCode, setScreenshotTarget } = useAiAssistant()
|
const {
|
||||||
|
setPageLoading,
|
||||||
|
setPageModuleName,
|
||||||
|
setPageModuleCode,
|
||||||
|
setScreenshotTarget,
|
||||||
|
setAiAssistantFabEnabled
|
||||||
|
} = useAiAssistant()
|
||||||
|
const route = useRoute()
|
||||||
|
/** 每日汇报模块名称:与后台菜单名称一致(meta.title),缺省为「商品大盘」 */
|
||||||
|
const aiMenuModuleName = computed(() => {
|
||||||
|
const t = route.meta?.title
|
||||||
|
return typeof t === 'string' && t.trim() ? t.trim() : '商品大盘'
|
||||||
|
})
|
||||||
|
watch(
|
||||||
|
aiMenuModuleName,
|
||||||
|
(name) => {
|
||||||
|
setPageModuleName(name)
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
const reportPageRootRef = ref<HTMLElement | null>(null)
|
const reportPageRootRef = ref<HTMLElement | null>(null)
|
||||||
watch(
|
watch(
|
||||||
[loadingKpi, loadingCategory, loadingPie, loadingProductList],
|
[loadingKpi, loadingCategory, loadingPie, loadingProductList],
|
||||||
@@ -1272,6 +1302,7 @@ watch(
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
setAiAssistantFabEnabled(false)
|
||||||
setPageModuleName(null)
|
setPageModuleName(null)
|
||||||
setPageModuleCode(null)
|
setPageModuleCode(null)
|
||||||
setScreenshotTarget(null)
|
setScreenshotTarget(null)
|
||||||
@@ -2222,9 +2253,22 @@ const handleQuery = async () => {
|
|||||||
name: 'YDY_AI_GET_SPDP1'
|
name: 'YDY_AI_GET_SPDP1'
|
||||||
} as any)
|
} as any)
|
||||||
.then((kpiRes: any) => {
|
.then((kpiRes: any) => {
|
||||||
const data = Array.isArray(kpiRes?.data) ? kpiRes.data : (Array.isArray(kpiRes) ? kpiRes : null)
|
let raw: any[] | null = null
|
||||||
if (data && data.length > 0) {
|
if (kpiRes != null) {
|
||||||
kpiList.value = data.map((item: any) => mapApiRowToKpi(item))
|
if (kpiRes.code != null && kpiRes.data != null) {
|
||||||
|
raw = Array.isArray(kpiRes.data) ? kpiRes.data : null
|
||||||
|
} else if (Array.isArray(kpiRes)) {
|
||||||
|
raw = kpiRes
|
||||||
|
} else if (kpiRes.data != null) {
|
||||||
|
raw = Array.isArray(kpiRes.data) ? kpiRes.data : null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 前几行常为列配置(含 keys/orders、无 code);数据行含供货商 code
|
||||||
|
const dataRows = (raw || []).filter(
|
||||||
|
(row) => row != null && row.code != null && String(row.code).trim() !== ''
|
||||||
|
)
|
||||||
|
if (dataRows.length > 0) {
|
||||||
|
kpiList.value = dataRows.map((item: any) => mapApiRowToKpi(item))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
@@ -2330,20 +2374,55 @@ const handleQuery = async () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 颜色字段 up/down/flat → 趋势 */
|
||||||
|
function trendFromColorField(c: unknown): 'up' | 'down' | 'flat' {
|
||||||
|
if (c == null || String(c).trim() === '') return 'flat'
|
||||||
|
const s = String(c).toLowerCase()
|
||||||
|
if (s.includes('up')) return 'up'
|
||||||
|
if (s.includes('down')) return 'down'
|
||||||
|
return 'flat'
|
||||||
|
}
|
||||||
|
|
||||||
// 将接口返回的一行数据映射为 KPI 卡片格式(按后端字段名适配,无则保留默认)
|
// 将接口返回的一行数据映射为 KPI 卡片格式(按后端字段名适配,无则保留默认)
|
||||||
function mapApiRowToKpi(row: any): KPIData {
|
function mapApiRowToKpi(row: any): KPIData {
|
||||||
const trend = row.trend === 'up' || row.trend === 1 ? 'up' : row.trend === 'down' || row.trend === -1 ? 'down' : 'flat'
|
const trend =
|
||||||
|
row.trend === 'up' || row.trend === 1
|
||||||
|
? 'up'
|
||||||
|
: row.trend === 'down' || row.trend === -1
|
||||||
|
? 'down'
|
||||||
|
: trendFromColorField(row.value1color ?? row.value2color ?? row.value3color ?? row.tagcolor)
|
||||||
|
|
||||||
const rawTrendText = row.trendText ?? row.rateText
|
const rawTrendText = row.trendText ?? row.rateText
|
||||||
const trendText =
|
let trendText: number | null = null
|
||||||
rawTrendText == null ? null : typeof rawTrendText === 'number' ? rawTrendText : Number(rawTrendText) || 0
|
if (rawTrendText != null) {
|
||||||
|
const n = typeof rawTrendText === 'number' ? rawTrendText : Number(rawTrendText)
|
||||||
|
trendText = Number.isFinite(n) ? n : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const v1 = row.value1 != null && String(row.value1).trim() !== '' ? String(row.value1) : ''
|
||||||
|
const v2 = row.value2 != null && String(row.value2).trim() !== '' ? String(row.value2) : ''
|
||||||
|
const v3 = row.value3 != null && String(row.value3).trim() !== '' ? String(row.value3) : ''
|
||||||
|
const legacyVal = String(row.value ?? row.mainValue ?? row.val ?? '').trim()
|
||||||
|
const combinedMetrics =
|
||||||
|
[v1 && `毛利率 ${v1}`, v2 && `动销 ${v2}`, v3 && `销售额 ${v3}`].filter(Boolean).join(' · ') || ''
|
||||||
|
|
||||||
|
const descFromApi = row.descs ?? row.desc ?? row.compareText
|
||||||
|
const descFromTags = [row.tag, row.value1date, row.value2date, row.value3date].filter(
|
||||||
|
(x) => x != null && String(x).trim() !== ''
|
||||||
|
)
|
||||||
|
const descs =
|
||||||
|
(descFromApi != null && String(descFromApi).trim() !== ''
|
||||||
|
? String(descFromApi)
|
||||||
|
: descFromTags.join(' | ')) ?? ''
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: row.title ?? row.mainTitle ?? row.name ?? '',
|
title: row.title ?? row.mainTitle ?? row.name ?? '',
|
||||||
unit: row.unit ?? '¥',
|
unit: row.unit != null && String(row.unit).trim() !== '' ? String(row.unit) : '',
|
||||||
value: String(row.value ?? row.mainValue ?? row.val ?? ''),
|
value: legacyVal || combinedMetrics,
|
||||||
valueSuffix: row.valueSuffix ?? row.suffix ?? '',
|
valueSuffix: row.valueSuffix ?? row.suffix ?? '',
|
||||||
trendText,
|
trendText,
|
||||||
trend,
|
trend,
|
||||||
descs: row.descs ?? row.desc ?? row.compareText ?? '',
|
descs,
|
||||||
bottomText: row.bottomText,
|
bottomText: row.bottomText,
|
||||||
bottomTextClass: row.bottomTextClass,
|
bottomTextClass: row.bottomTextClass,
|
||||||
progress: row.progress,
|
progress: row.progress,
|
||||||
@@ -2636,7 +2715,7 @@ function applyQueryDefaults() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
setPageModuleName('商品驾驶舱')
|
setAiAssistantFabEnabled(true)
|
||||||
setPageModuleCode('ProductDashboard:main')
|
setPageModuleCode('ProductDashboard:main')
|
||||||
await nextTick()
|
await nextTick()
|
||||||
if (reportPageRootRef.value) setScreenshotTarget(reportPageRootRef.value)
|
if (reportPageRootRef.value) setScreenshotTarget(reportPageRootRef.value)
|
||||||
@@ -2752,7 +2831,11 @@ onBeforeUnmount(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// keep-alive 下切走时不会触发 onBeforeUnmount,只触发 onDeactivated,离开时也要保存缓存
|
// keep-alive 下切走时不会触发 onBeforeUnmount,只触发 onDeactivated,离开时也要保存缓存
|
||||||
|
onActivated(() => {
|
||||||
|
setAiAssistantFabEnabled(true)
|
||||||
|
})
|
||||||
onDeactivated(() => {
|
onDeactivated(() => {
|
||||||
|
setAiAssistantFabEnabled(false)
|
||||||
saveDataCache()
|
saveDataCache()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
2365
src/views/ydoyun/report/productDaily/index.vue
Normal file
2365
src/views/ydoyun/report/productDaily/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
4
types/router.d.ts
vendored
4
types/router.d.ts
vendored
@@ -36,6 +36,8 @@ import { defineComponent } from 'vue'
|
|||||||
**/
|
**/
|
||||||
declare module 'vue-router' {
|
declare module 'vue-router' {
|
||||||
interface RouteMeta extends Record<string | number | symbol, unknown> {
|
interface RouteMeta extends Record<string | number | symbol, unknown> {
|
||||||
|
/** 后台菜单 id(动态路由由 generateRoute 从菜单节点写入,用于 Dify 知识库命名等) */
|
||||||
|
menuId?: number
|
||||||
hidden?: boolean
|
hidden?: boolean
|
||||||
alwaysShow?: boolean
|
alwaysShow?: boolean
|
||||||
title?: string
|
title?: string
|
||||||
@@ -68,6 +70,8 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface AppCustomRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {
|
interface AppCustomRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {
|
||||||
|
/** 菜单编号,与后端 MenuVO.id 一致 */
|
||||||
|
id?: number
|
||||||
icon: any
|
icon: any
|
||||||
name: string
|
name: string
|
||||||
meta: RouteMeta
|
meta: RouteMeta
|
||||||
|
|||||||
143
日志汇总统计.html
Normal file
143
日志汇总统计.html
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
<!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><</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">></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user