1. 提交代码
This commit is contained in:
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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?.()
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 中
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
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' {
|
||||
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
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