1. 提交代码

This commit is contained in:
2026-04-17 09:46:21 +08:00
parent dd379b1e8f
commit 5210a140ba
9 changed files with 2675 additions and 48 deletions

View File

@@ -1,4 +1,5 @@
import type { App } from 'vue'
import { toRaw } from 'vue'
import { useUserStore } from '@/store/modules/user'
const { t } = useI18n() // 国际化
@@ -24,8 +25,8 @@ export function hasPermi(app: App<Element>) {
const userStore = useUserStore()
const all_permission = '*:*:*'
export const hasPermission = (permission: string[]) => {
return (
userStore.permissions.has(all_permission) ||
permission.some((permission) => userStore.permissions.has(permission))
)
// Vue 3.5+ 对响应式 Set 存字符串时,直接 .has 可能触发 WeakMap keys must be objects
const raw = toRaw(userStore.permissions) as Set<string> | undefined
if (!raw?.size) return false
return raw.has(all_permission) || permission.some((p) => raw.has(p))
}

View File

@@ -15,17 +15,20 @@ export const useTagsView = () => {
}
const closeLeft = (callback?: Fn) => {
tagsViewStore.delLeftViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
const tag = unref(selectedTag)
if (tag) tagsViewStore.delLeftViews(tag as RouteLocationNormalizedLoaded)
callback?.()
}
const closeRight = (callback?: Fn) => {
tagsViewStore.delRightViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
const tag = unref(selectedTag)
if (tag) tagsViewStore.delRightViews(tag as RouteLocationNormalizedLoaded)
callback?.()
}
const closeOther = (callback?: Fn) => {
tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
const tag = unref(selectedTag)
if (tag) tagsViewStore.delOthersViews(tag as RouteLocationNormalizedLoaded)
callback?.()
}

View File

@@ -1,6 +1,10 @@
<script lang="ts" setup>
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 { usePermissionStore } from '@/store/modules/permission'
import { useTagsViewStore } from '@/store/modules/tagsView'
@@ -83,17 +87,17 @@ const toLastView = () => {
const visitedViews = tagsViewStore.getVisitedViews
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
push(latestView)
// 使用 fullPath 字符串,避免 push(整段 RouteLocation) 在部分环境下触发 WeakMap 相关运行时错误
push(latestView.fullPath)
} else {
if (
unref(currentRoute).path === permissionStore.getAddRouters[0].path ||
unref(currentRoute).path === permissionStore.getAddRouters[0].redirect
) {
const first = permissionStore.getAddRouters[0]
if (!first?.path) return
const curPath = unref(currentRoute).path
if (curPath === first.path || curPath === first.redirect) {
addTags()
return
}
// You can set another route
push(permissionStore.getAddRouters[0].path)
push(first.path)
}
}
@@ -137,8 +141,20 @@ const moveToCurrentTag = async () => {
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 wrap$ = unref(scrollbarRef)?.wrapRef
if (!wrap$) return
let firstTag: Nullable<RouterLinkProps> = null
let lastTag: Nullable<RouterLinkProps> = null
@@ -148,33 +164,37 @@ const moveToTarget = (currentTag: RouteLocationNormalizedLoaded) => {
firstTag = tagList[0]
lastTag = tagList[tagList.length - 1]
}
if ((firstTag?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath) {
const cur = currentTag.fullPath
if (resolveRouterLinkToPath(firstTag?.to) === cur) {
// 直接滚动到0的位置
const { start } = useScrollTo({
el: wrap$!,
el: wrap$,
position: 'scrollLeft',
to: 0,
duration: 500
})
start()
} else if ((lastTag?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath) {
} else if (resolveRouterLinkToPath(lastTag?.to) === cur) {
// 滚动到最后的位置
const { start } = useScrollTo({
el: wrap$!,
el: wrap$,
position: 'scrollLeft',
to: wrap$!.scrollWidth - wrap$!.offsetWidth,
to: wrap$.scrollWidth - wrap$.offsetWidth,
duration: 500
})
start()
} else {
// find preTag and nextTag
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 prevTag = tgsRefs[currentIndex - 1] as HTMLElement
const nextTag = tgsRefs[currentIndex + 1] as HTMLElement
const prevTag = tgsRefs[currentIndex - 1] as HTMLElement | undefined
const nextTag = tgsRefs[currentIndex + 1] as HTMLElement | undefined
if (!nextTag || !prevTag) return
// the tag's offsetLeft after of nextTag
const afterNextTagOffsetLeft = nextTag.offsetLeft + nextTag.offsetWidth + 4
@@ -182,17 +202,17 @@ const moveToTarget = (currentTag: RouteLocationNormalizedLoaded) => {
// the tag's offsetLeft before of prevTag
const beforePrevTagOffsetLeft = prevTag.offsetLeft - 4
if (afterNextTagOffsetLeft > unref(scrollLeftNumber) + wrap$!.offsetWidth) {
if (afterNextTagOffsetLeft > unref(scrollLeftNumber) + wrap$.offsetWidth) {
const { start } = useScrollTo({
el: wrap$!,
el: wrap$,
position: 'scrollLeft',
to: afterNextTagOffsetLeft - wrap$!.offsetWidth,
to: afterNextTagOffsetLeft - wrap$.offsetWidth,
duration: 500
})
start()
} else if (beforePrevTagOffsetLeft < unref(scrollLeftNumber)) {
const { start } = useScrollTo({
el: wrap$!,
el: wrap$,
position: 'scrollLeft',
to: beforePrevTagOffsetLeft,
duration: 500
@@ -364,7 +384,7 @@ watch(
@visible-change="visibleChange"
>
<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
:class="`h-full flex items-center justify-center whitespace-nowrap pl-15px ${prefixCls}__item--label`"
@click="navigate"

View File

@@ -74,7 +74,8 @@ export const generateRoute = (routes: AppCustomRouteRecordRaw[]): AppRouteRecord
alwaysShow:
route.children &&
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
// 特殊逻辑:如果后端配置的 MenuDO.component 包含 ?,则表示需要传递参数
// 此时,我们需要解析参数,并且将参数放到 meta.query 中

View File

@@ -1,6 +1,13 @@
<template>
<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>
<Icon icon="ep:search" />
</template>
@@ -45,6 +52,11 @@ const filterNode = (name: string, data: Tree) => {
return data.name.includes(name)
}
/** 输入完成后回车(或点清空)再筛选,避免每键都过滤 */
const applyDeptFilter = () => {
treeRef.value?.filter(deptName.value)
}
/** 处理部门被点击 */
let currentNode: 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'])
/** 监听deptName */
watch(deptName, (val) => {
treeRef.value!.filter(val)
})
/** 初始化 */
onMounted(async () => {
await getTree()

View File

@@ -828,7 +828,18 @@
</template>
<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 type { EChartsOption } from 'echarts'
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 { ElMessage } from 'element-plus'
import { Icon } from '@/components/Icon'
import { useRouter } from 'vue-router'
import { useRoute, useRouter } from 'vue-router'
import type { CategoryDiagnosticData } from './components/categoryCardListComponents.vue'
import { useAiAssistant } from '@/components/AiAssistant/useAiAssistant'
@@ -1264,7 +1275,26 @@ const loadingPie = 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)
watch(
[loadingKpi, loadingCategory, loadingPie, loadingProductList],
@@ -1272,6 +1302,7 @@ watch(
{ immediate: true }
)
onUnmounted(() => {
setAiAssistantFabEnabled(false)
setPageModuleName(null)
setPageModuleCode(null)
setScreenshotTarget(null)
@@ -2222,9 +2253,22 @@ const handleQuery = async () => {
name: 'YDY_AI_GET_SPDP1'
} as any)
.then((kpiRes: any) => {
const data = Array.isArray(kpiRes?.data) ? kpiRes.data : (Array.isArray(kpiRes) ? kpiRes : null)
if (data && data.length > 0) {
kpiList.value = data.map((item: any) => mapApiRowToKpi(item))
let raw: any[] | null = null
if (kpiRes != null) {
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(() => {})
@@ -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 卡片格式(按后端字段名适配,无则保留默认)
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 trendText =
rawTrendText == null ? null : typeof rawTrendText === 'number' ? rawTrendText : Number(rawTrendText) || 0
let trendText: number | null = null
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 {
title: row.title ?? row.mainTitle ?? row.name ?? '',
unit: row.unit ?? '¥',
value: String(row.value ?? row.mainValue ?? row.val ?? ''),
unit: row.unit != null && String(row.unit).trim() !== '' ? String(row.unit) : '',
value: legacyVal || combinedMetrics,
valueSuffix: row.valueSuffix ?? row.suffix ?? '',
trendText,
trend,
descs: row.descs ?? row.desc ?? row.compareText ?? '',
descs,
bottomText: row.bottomText,
bottomTextClass: row.bottomTextClass,
progress: row.progress,
@@ -2636,7 +2715,7 @@ function applyQueryDefaults() {
}
onMounted(async () => {
setPageModuleName('商品驾驶舱')
setAiAssistantFabEnabled(true)
setPageModuleCode('ProductDashboard:main')
await nextTick()
if (reportPageRootRef.value) setScreenshotTarget(reportPageRootRef.value)
@@ -2752,7 +2831,11 @@ onBeforeUnmount(() => {
})
// keep-alive 下切走时不会触发 onBeforeUnmount只触发 onDeactivated离开时也要保存缓存
onActivated(() => {
setAiAssistantFabEnabled(true)
})
onDeactivated(() => {
setAiAssistantFabEnabled(false)
saveDataCache()
})
</script>

File diff suppressed because it is too large Load Diff

4
types/router.d.ts vendored
View File

@@ -36,6 +36,8 @@ import { defineComponent } from 'vue'
**/
declare module 'vue-router' {
interface RouteMeta extends Record<string | number | symbol, unknown> {
/** 后台菜单 id动态路由由 generateRoute 从菜单节点写入,用于 Dify 知识库命名等) */
menuId?: number
hidden?: boolean
alwaysShow?: boolean
title?: string
@@ -68,6 +70,8 @@ declare global {
}
interface AppCustomRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {
/** 菜单编号,与后端 MenuVO.id 一致 */
id?: number
icon: any
name: string
meta: RouteMeta

143
日志汇总统计.html Normal file
View 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>&lt;</button>
<button class="p-1.5 border rounded bg-blue-500 text-white min-w-[30px] font-bold shadow-sm">1</button>
<button class="p-1.5 border rounded bg-white hover:bg-gray-100 min-w-[30px]">2</button>
<button class="p-1.5 border rounded bg-white hover:bg-gray-100 min-w-[30px]">3</button>
<button class="p-1.5 border rounded bg-white hover:bg-gray-100">&gt;</button>
</div>
</div>
</div>
</div>
</body>
</html>