|
|
@@ -0,0 +1,718 @@
|
|
|
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
|
+import styles from './index.module.scss'
|
|
|
+import { Button, Select, TreeSelect } from 'antd'
|
|
|
+import { iconUrl } from '@/utils/http'
|
|
|
+import { useDispatch, useSelector } from 'react-redux'
|
|
|
+import { RootState } from '@/store'
|
|
|
+import { E1_APIgetTree } from '@/store/action/Eculture/E1tag'
|
|
|
+import type { TypeI5Tree } from '@/pages/Isystem/I5organization/data'
|
|
|
+import * as xlsx from 'xlsx'
|
|
|
+import * as echarts from 'echarts'
|
|
|
+import {
|
|
|
+ A1_APIworkCount,
|
|
|
+ A1_APIgetDictById,
|
|
|
+ A1_APIgetDataTextureByTagId,
|
|
|
+ A1_APIgetDataTagByTagId,
|
|
|
+ A1_APIgetTotal,
|
|
|
+ A1_APIgetDataTagLevel
|
|
|
+} from '@/store/action/Abench/A1'
|
|
|
+
|
|
|
+// 生成年度选项:2025 到今年
|
|
|
+const currentYear = new Date().getFullYear()
|
|
|
+const yearOptions = Array.from({ length: currentYear - 2025 + 1 }, (_, i) => {
|
|
|
+ const year = 2025 + i
|
|
|
+ return { value: year, label: `${year}年度` }
|
|
|
+})
|
|
|
+
|
|
|
+const DEFAULT_COLORS = ['#c11b2d', '#243220', '#24664b', '#8acfb2', '#806e4c', '#988364', '#972d00']
|
|
|
+
|
|
|
+// 先写一些静态的
|
|
|
+type DictItem = { name: string; id: number }
|
|
|
+type TotalItem = { pcs: number | null; level: string | null }
|
|
|
+
|
|
|
+function A1statistics() {
|
|
|
+ const [yearCountYear, setYearCountYear] = useState(currentYear)
|
|
|
+ const [yearCount, setYearCount] = useState<number | null>(null)
|
|
|
+
|
|
|
+ // 藏品总数、藏品总数量、定级文物数量
|
|
|
+ const [totalList, setTotalList] = useState<TotalItem[] | null>(null)
|
|
|
+
|
|
|
+ // 材质统计
|
|
|
+ const [textureOptions, setTextureOptions] = useState<DictItem[]>([])
|
|
|
+ const [textureTagId, setTextureTagId] = useState<number | null>(null)
|
|
|
+ const [textureData, setTextureData] = useState<{ name: string; value: number }[]>([])
|
|
|
+
|
|
|
+ // 品类统计(使用 cms/tag/getTree 树状结构)
|
|
|
+ const [categoryTagId, setCategoryTagId] = useState<string | null>(null)
|
|
|
+ const [categoryData, setCategoryData] = useState<{ name: string; value: number }[]>([])
|
|
|
+
|
|
|
+ const dispatch = useDispatch()
|
|
|
+ const categoryTreeData = useSelector((state: RootState) => state.E1tag.treeData)
|
|
|
+
|
|
|
+ // 一级/二级分类
|
|
|
+ const [level1Data, setLevel1Data] = useState<{ name: string; value: number }[]>([])
|
|
|
+ const [level2Data, setLevel2Data] = useState<{ name: string; value: number }[]>([])
|
|
|
+
|
|
|
+ const textureInitialized = useRef(false)
|
|
|
+ const categoryInitialized = useRef(false)
|
|
|
+
|
|
|
+ // 将 TypeI5Tree 转为 TreeSelect 的 treeData 格式
|
|
|
+ const categoryTreeSelectData = useMemo(() => {
|
|
|
+ const convert = (nodes: TypeI5Tree[]): { value: string; label: string; children?: any[] }[] =>
|
|
|
+ nodes.map(node => ({
|
|
|
+ value: node.id,
|
|
|
+ label: node.name,
|
|
|
+ children: node.children?.length ? convert(node.children) : undefined
|
|
|
+ }))
|
|
|
+ return convert(categoryTreeData || [])
|
|
|
+ }, [categoryTreeData])
|
|
|
+
|
|
|
+ // 从树中根据 id 查找 name(用于导出)
|
|
|
+ const getCategoryNameById = useCallback(
|
|
|
+ (id: string | null) => {
|
|
|
+ if (!id) return ''
|
|
|
+ const find = (nodes: TypeI5Tree[]): string | undefined => {
|
|
|
+ for (const n of nodes) {
|
|
|
+ if (n.id === id) return n.name
|
|
|
+ if (n.children?.length) {
|
|
|
+ const found = find(n.children)
|
|
|
+ if (found) return found
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return undefined
|
|
|
+ }
|
|
|
+ return find(categoryTreeData || []) ?? ''
|
|
|
+ },
|
|
|
+ [categoryTreeData]
|
|
|
+ )
|
|
|
+
|
|
|
+ // 获取一级/二级分类数据
|
|
|
+ useEffect(() => {
|
|
|
+ const fetchTagLevel = async () => {
|
|
|
+ try {
|
|
|
+ const res = await A1_APIgetDataTagLevel()
|
|
|
+ if (res?.code === 0 && res.data) {
|
|
|
+ const list1 = (res.data.level_1 || []).map((it: { name: string; count: number }) => ({
|
|
|
+ name: it.name,
|
|
|
+ value: it.count
|
|
|
+ }))
|
|
|
+ const list2 = (res.data.level_2 || []).map((it: { name: string; count: number }) => ({
|
|
|
+ name: it.name,
|
|
|
+ value: it.count
|
|
|
+ }))
|
|
|
+ setLevel1Data(list1)
|
|
|
+ setLevel2Data(list2)
|
|
|
+ } else {
|
|
|
+ setLevel1Data([])
|
|
|
+ setLevel2Data([])
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ setLevel1Data([])
|
|
|
+ setLevel2Data([])
|
|
|
+ }
|
|
|
+ }
|
|
|
+ fetchTagLevel()
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ // 获取藏品总数统计
|
|
|
+ useEffect(() => {
|
|
|
+ const fetchTotal = async () => {
|
|
|
+ try {
|
|
|
+ const res = await A1_APIgetTotal()
|
|
|
+ if (res?.code === 0 && Array.isArray(res.data)) {
|
|
|
+ setTotalList(res.data as TotalItem[])
|
|
|
+ } else {
|
|
|
+ setTotalList(null)
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ setTotalList(null)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ fetchTotal()
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ // 藏品总数、藏品总数量、定级文物按级别统计
|
|
|
+ const totalStats = totalList
|
|
|
+ ? (() => {
|
|
|
+ const totalCount = totalList.length
|
|
|
+ const totalPcs = totalList.reduce((sum, it) => sum + (it.pcs ?? 0), 0)
|
|
|
+ const withLevel = totalList.filter(it => it.level != null && String(it.level).trim() !== '')
|
|
|
+ const level1 = totalList.filter(it => it.level === '一级').length
|
|
|
+ const level2 = totalList.filter(it => it.level === '二级').length
|
|
|
+ const level3 = totalList.filter(it => it.level === '三级').length
|
|
|
+ const level4 = totalList.filter(it => it.level === '一般').length
|
|
|
+ return {
|
|
|
+ totalCount,
|
|
|
+ totalPcs,
|
|
|
+ leveledCount: withLevel.length,
|
|
|
+ level1,
|
|
|
+ level2,
|
|
|
+ level3,
|
|
|
+ level4
|
|
|
+ }
|
|
|
+ })()
|
|
|
+ : null
|
|
|
+
|
|
|
+ // 获取品类树(cms/tag/getTree)
|
|
|
+ useEffect(() => {
|
|
|
+ dispatch(E1_APIgetTree())
|
|
|
+ }, [dispatch])
|
|
|
+
|
|
|
+ // 品类树加载后默认选中第一个
|
|
|
+ useEffect(() => {
|
|
|
+ if (categoryTreeData?.length > 0 && !categoryInitialized.current) {
|
|
|
+ categoryInitialized.current = true
|
|
|
+ setCategoryTagId(categoryTreeData[0].id)
|
|
|
+ }
|
|
|
+ }, [categoryTreeData])
|
|
|
+
|
|
|
+ // 获取筛选字典(材质)
|
|
|
+ useEffect(() => {
|
|
|
+ const fetchDict = async () => {
|
|
|
+ try {
|
|
|
+ const res = await A1_APIgetDictById()
|
|
|
+ if (res?.code === 0 && res.data) {
|
|
|
+ const list3 = res.data.texture || []
|
|
|
+ setTextureOptions(list3)
|
|
|
+ if (list3.length > 0 && !textureInitialized.current) {
|
|
|
+ textureInitialized.current = true
|
|
|
+ setTextureTagId(list3[0].id)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ setTextureOptions([])
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ setTextureOptions([])
|
|
|
+ }
|
|
|
+ }
|
|
|
+ fetchDict()
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ // 材质 tagId 变化时获取数据
|
|
|
+ useEffect(() => {
|
|
|
+ if (textureTagId == null) {
|
|
|
+ setTextureData([])
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const fetch = async () => {
|
|
|
+ try {
|
|
|
+ const res = await A1_APIgetDataTextureByTagId(textureTagId)
|
|
|
+ if (
|
|
|
+ res?.code === 0 &&
|
|
|
+ res.data &&
|
|
|
+ typeof res.data === 'object' &&
|
|
|
+ !Array.isArray(res.data)
|
|
|
+ ) {
|
|
|
+ setTextureData(
|
|
|
+ Object.entries(res.data as Record<string, number>).map(([name, value]) => ({
|
|
|
+ name,
|
|
|
+ value
|
|
|
+ }))
|
|
|
+ )
|
|
|
+ } else {
|
|
|
+ setTextureData([])
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ setTextureData([])
|
|
|
+ }
|
|
|
+ }
|
|
|
+ fetch()
|
|
|
+ }, [textureTagId])
|
|
|
+
|
|
|
+ // 品类 tagId 变化时获取数据
|
|
|
+ useEffect(() => {
|
|
|
+ if (categoryTagId == null) {
|
|
|
+ setCategoryData([])
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const fetch = async () => {
|
|
|
+ try {
|
|
|
+ const res = await A1_APIgetDataTagByTagId(Number(categoryTagId))
|
|
|
+ if (res?.code === 0 && res.data && Array.isArray(res.data)) {
|
|
|
+ setCategoryData(
|
|
|
+ res.data.map(i => ({
|
|
|
+ name: i.name,
|
|
|
+ value: i.count
|
|
|
+ }))
|
|
|
+ )
|
|
|
+ } else {
|
|
|
+ setCategoryData([])
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ setCategoryData([])
|
|
|
+ }
|
|
|
+ }
|
|
|
+ fetch()
|
|
|
+ }, [categoryTagId])
|
|
|
+
|
|
|
+ // 获取年度新增产品数量
|
|
|
+ useEffect(() => {
|
|
|
+ const fetchYearCount = async () => {
|
|
|
+ try {
|
|
|
+ const res = await A1_APIworkCount(yearCountYear)
|
|
|
+ if (res?.code === 0) {
|
|
|
+ const count = typeof res.data === 'number' ? res.data : (res.data?.count ?? 0)
|
|
|
+ setYearCount(count)
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ setYearCount(null)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ fetchYearCount()
|
|
|
+ }, [yearCountYear])
|
|
|
+
|
|
|
+ // 生成柱状图
|
|
|
+ const initEchFu = useCallback((data: any[], dom: any) => {
|
|
|
+ if (!dom) return
|
|
|
+
|
|
|
+ const myChart = echarts.getInstanceByDom(dom) || echarts.init(dom)
|
|
|
+
|
|
|
+ // 计算总数用于百分比(total 为 0 时避免 NaN)
|
|
|
+ const total = data.reduce((sum, item) => sum + item.value, 0)
|
|
|
+ const pct = (val: number) => (total > 0 ? ((val / total) * 100).toFixed(1) + '%' : '0%')
|
|
|
+ const dataMax = Math.max(...data.map(d => d.value))
|
|
|
+ const yMax = Math.max(5, dataMax)
|
|
|
+ const yInterval = Math.max(1, Math.ceil(yMax / 5))
|
|
|
+
|
|
|
+ const option: echarts.EChartsOption = {
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ axisPointer: {
|
|
|
+ type: 'shadow'
|
|
|
+ },
|
|
|
+ formatter: (params: any) => {
|
|
|
+ const param = params[0]
|
|
|
+ return `
|
|
|
+ <div style="font-weight: bold;">${param.name}</div>
|
|
|
+ <div>数量: ${param.value}</div>
|
|
|
+ <div>占比: ${pct(param.value)}</div>
|
|
|
+ `
|
|
|
+ },
|
|
|
+ backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
|
|
+ borderColor: '#ddd',
|
|
|
+ textStyle: {
|
|
|
+ color: '#000'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ grid: {
|
|
|
+ left: '5%',
|
|
|
+ right: '5%',
|
|
|
+ // 减小底部距离 - 调整这个值可以让柱子更靠近底部
|
|
|
+ bottom: '5%',
|
|
|
+ top: '5%',
|
|
|
+ containLabel: true
|
|
|
+ },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: data.map(item => item.name),
|
|
|
+ axisLabel: {
|
|
|
+ interval: 0,
|
|
|
+ rotate: 30,
|
|
|
+ fontSize: 12,
|
|
|
+ color: '#000',
|
|
|
+ formatter: (value: string) => {
|
|
|
+ return value.length > 4 ? value.slice(0, 5) + '...' : value
|
|
|
+ },
|
|
|
+ margin: 15 // 调整标签与柱子的距离
|
|
|
+ },
|
|
|
+ axisLine: {
|
|
|
+ lineStyle: {
|
|
|
+ color: '#999'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ axisTick: {
|
|
|
+ show: true,
|
|
|
+ alignWithLabel: true,
|
|
|
+ length: 4
|
|
|
+ },
|
|
|
+ // 调整X轴位置,使其更靠近柱子底部
|
|
|
+ offset: 10
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value',
|
|
|
+ name: '',
|
|
|
+ nameTextStyle: {
|
|
|
+ fontSize: 14,
|
|
|
+ color: '#000',
|
|
|
+ padding: [0, 0, 0, 10]
|
|
|
+ },
|
|
|
+ axisLabel: {
|
|
|
+ fontSize: 12,
|
|
|
+ color: '#000',
|
|
|
+ formatter: (value: number) => {
|
|
|
+ if (value >= 1000) {
|
|
|
+ return `${value / 1000}k`
|
|
|
+ }
|
|
|
+ return value.toString()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ splitLine: {
|
|
|
+ lineStyle: {
|
|
|
+ type: 'dashed',
|
|
|
+ color: '#e0e0e0'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ max: yMax,
|
|
|
+ min: 0,
|
|
|
+ interval: yInterval
|
|
|
+ },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: '数量',
|
|
|
+ type: 'bar',
|
|
|
+ data: data.map((item, index) => ({
|
|
|
+ value: item.value,
|
|
|
+ percentage: pct(item.value),
|
|
|
+ // 为每个柱子设置独立的渐变
|
|
|
+ itemStyle: {
|
|
|
+ // 关键修改:使用线性渐变实现单个柱子的渐变效果
|
|
|
+ color: new echarts.graphic.LinearGradient(
|
|
|
+ 0,
|
|
|
+ 0,
|
|
|
+ 0,
|
|
|
+ 1, // 0,0表示起点,0,1表示终点(垂直方向渐变)
|
|
|
+ [
|
|
|
+ { offset: 0, color: '#bb1e2e' }, // 顶部颜色
|
|
|
+ { offset: 1, color: '#29644a' } // 底部颜色(更浅/白色)
|
|
|
+ ]
|
|
|
+ ),
|
|
|
+ borderRadius: [10, 10, 10, 10],
|
|
|
+ // 添加阴影效果增强立体感
|
|
|
+ shadowColor: 'rgba(0, 0, 0, 0.1)',
|
|
|
+ shadowBlur: 4,
|
|
|
+ shadowOffsetY: 2
|
|
|
+ }
|
|
|
+ })),
|
|
|
+ barWidth: '50%', // 稍微调窄柱子
|
|
|
+ // 调整标签位置和样式
|
|
|
+ label: {
|
|
|
+ show: true,
|
|
|
+ position: 'top',
|
|
|
+ formatter: (params: any) => {
|
|
|
+ const itemData = params.data
|
|
|
+ return `${itemData.value}\n${pct(itemData.value)}`
|
|
|
+ },
|
|
|
+ fontSize: 10,
|
|
|
+ fontWeight: 'bold',
|
|
|
+ color: '#000',
|
|
|
+ lineHeight: 16
|
|
|
+ },
|
|
|
+
|
|
|
+ emphasis: {
|
|
|
+ itemStyle: {
|
|
|
+ shadowColor: 'rgba(0, 0, 0, 0.3)',
|
|
|
+ shadowBlur: 10,
|
|
|
+ shadowOffsetY: 3
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+
|
|
|
+ myChart.setOption(option)
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ // 生成饼图
|
|
|
+ const binInitFu = useCallback((data: any[], dom: any) => {
|
|
|
+ if (!dom) return
|
|
|
+
|
|
|
+ const myChart = echarts.getInstanceByDom(dom) || echarts.init(dom)
|
|
|
+ if (!data || data.length === 0) {
|
|
|
+ myChart.clear()
|
|
|
+ myChart.setOption({
|
|
|
+ graphic: [
|
|
|
+ {
|
|
|
+ type: 'text',
|
|
|
+ left: 'center',
|
|
|
+ top: 'center',
|
|
|
+ style: { text: '暂无数据', fontSize: 14, fill: '#999', textAlign: 'center' }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 有数据时先清除,避免从「暂无数据」切换后 graphic 残留
|
|
|
+ myChart.clear()
|
|
|
+ // 计算总数用于百分比(total 为 0 时避免 NaN)
|
|
|
+ const total = data.reduce((sum, item) => sum + item.value, 0)
|
|
|
+ const pct = (val: number) => (total > 0 ? ((val / total) * 100).toFixed(1) : '0')
|
|
|
+
|
|
|
+ const option = {
|
|
|
+ color: DEFAULT_COLORS, // 设置颜色方案
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'item',
|
|
|
+ formatter: (params: any) => `${params.name}: ${params.value} (${pct(params.value)}%)`
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ type: 'scroll',
|
|
|
+ orient: 'vertical',
|
|
|
+ left: '52%',
|
|
|
+ top: 'center',
|
|
|
+ itemGap: 15,
|
|
|
+ textStyle: {
|
|
|
+ fontSize: 12
|
|
|
+ },
|
|
|
+ formatter: function (name: string) {
|
|
|
+ const item = data.find(d => d.name === name)
|
|
|
+ if (item) {
|
|
|
+ return `${name} ${item.value} ${pct(item.value)}%`
|
|
|
+ }
|
|
|
+ return name
|
|
|
+ }
|
|
|
+ },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: '品类分布',
|
|
|
+ type: 'pie',
|
|
|
+ center: ['25%', '50%'], // 设置饼图中心位置
|
|
|
+ radius: ['50%', '90%'],
|
|
|
+ avoidLabelOverlap: false,
|
|
|
+ itemStyle: {
|
|
|
+ borderRadius: 0,
|
|
|
+ borderColor: '#fff',
|
|
|
+ borderWidth: 2
|
|
|
+ },
|
|
|
+ label: {
|
|
|
+ show: false
|
|
|
+ },
|
|
|
+ labelLine: {
|
|
|
+ show: false
|
|
|
+ },
|
|
|
+ emphasis: {
|
|
|
+ itemStyle: {
|
|
|
+ shadowBlur: 10,
|
|
|
+ shadowOffsetX: 0,
|
|
|
+ shadowColor: 'rgba(0, 0, 0, 0.5)'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ data: data.map(item => ({
|
|
|
+ name: item.name,
|
|
|
+ value: item.value
|
|
|
+ }))
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ myChart.setOption(option)
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ initEchFu(level1Data, document.querySelector('#echBox1'))
|
|
|
+ }, [initEchFu, level1Data])
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ initEchFu(level2Data, document.querySelector('#echBox2'))
|
|
|
+ }, [initEchFu, level2Data])
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ binInitFu(textureData, document.querySelector('#echBox3'))
|
|
|
+ }, [binInitFu, textureData])
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ binInitFu(categoryData, document.querySelector('#echBox4'))
|
|
|
+ }, [binInitFu, categoryData])
|
|
|
+
|
|
|
+ // 数据导出
|
|
|
+ const dataExport = useCallback(() => {
|
|
|
+ const wb = xlsx.utils.book_new()
|
|
|
+
|
|
|
+ // 汇总数据
|
|
|
+ const summaryData = [
|
|
|
+ { 指标: '藏品总数', 数值: totalStats?.totalCount ?? '-', 单位: '件/套' },
|
|
|
+ { 指标: '藏品总数量', 数值: totalStats?.totalPcs ?? '-', 单位: '个' },
|
|
|
+ { 指标: '定级文物数量', 数值: totalStats?.leveledCount ?? '-', 单位: '件/套' },
|
|
|
+ { 指标: '一级文物', 数值: totalStats?.level1 ?? 0, 单位: '件/套' },
|
|
|
+ { 指标: '二级文物', 数值: totalStats?.level2 ?? 0, 单位: '件/套' },
|
|
|
+ { 指标: '三级文物', 数值: totalStats?.level3 ?? 0, 单位: '件/套' },
|
|
|
+ { 指标: '一般文物', 数值: totalStats?.level4 ?? 0, 单位: '件/套' },
|
|
|
+ { 指标: `年度新增产品数量(${yearCountYear}年)`, 数值: yearCount ?? '-', 单位: '件/套' }
|
|
|
+ ]
|
|
|
+ const wsSummary = xlsx.utils.json_to_sheet(summaryData)
|
|
|
+ wsSummary['!cols'] = [{ wpx: 180 }, { wpx: 100 }, { wpx: 80 }]
|
|
|
+ xlsx.utils.book_append_sheet(wb, wsSummary, '汇总')
|
|
|
+
|
|
|
+ // 一级分类
|
|
|
+ const level1SheetData = level1Data.map((it, i) => ({
|
|
|
+ 序号: i + 1,
|
|
|
+ 分类名称: it.name,
|
|
|
+ 数量: it.value
|
|
|
+ }))
|
|
|
+ if (level1SheetData.length) {
|
|
|
+ const ws1 = xlsx.utils.json_to_sheet(level1SheetData)
|
|
|
+ ws1['!cols'] = [{ wpx: 60 }, { wpx: 150 }, { wpx: 80 }]
|
|
|
+ xlsx.utils.book_append_sheet(wb, ws1, '一级分类')
|
|
|
+ }
|
|
|
+
|
|
|
+ // 二级分类
|
|
|
+ const level2SheetData = level2Data.map((it, i) => ({
|
|
|
+ 序号: i + 1,
|
|
|
+ 分类名称: it.name,
|
|
|
+ 数量: it.value
|
|
|
+ }))
|
|
|
+ if (level2SheetData.length) {
|
|
|
+ const ws2 = xlsx.utils.json_to_sheet(level2SheetData)
|
|
|
+ ws2['!cols'] = [{ wpx: 60 }, { wpx: 150 }, { wpx: 80 }]
|
|
|
+ xlsx.utils.book_append_sheet(wb, ws2, '二级分类')
|
|
|
+ }
|
|
|
+
|
|
|
+ // 材质统计(当前筛选)
|
|
|
+ const textureSheetData = textureData.map((it, i) => ({
|
|
|
+ 序号: i + 1,
|
|
|
+ 材质: it.name,
|
|
|
+ 数量: it.value
|
|
|
+ }))
|
|
|
+ if (textureSheetData.length) {
|
|
|
+ const ws3 = xlsx.utils.json_to_sheet(textureSheetData)
|
|
|
+ ws3['!cols'] = [{ wpx: 60 }, { wpx: 150 }, { wpx: 80 }]
|
|
|
+ const tagName = (textureOptions.find(t => t.id === textureTagId)?.name ?? '').replace(
|
|
|
+ /[\\/*?:[\]]/g,
|
|
|
+ ''
|
|
|
+ )
|
|
|
+ xlsx.utils.book_append_sheet(wb, ws3, tagName ? `材质_${tagName.slice(0, 20)}` : '材质统计')
|
|
|
+ }
|
|
|
+
|
|
|
+ // 品类统计(当前筛选)
|
|
|
+ const categorySheetData = categoryData.map((it, i) => ({
|
|
|
+ 序号: i + 1,
|
|
|
+ 品类: it.name,
|
|
|
+ 数量: it.value
|
|
|
+ }))
|
|
|
+ if (categorySheetData.length) {
|
|
|
+ const ws4 = xlsx.utils.json_to_sheet(categorySheetData)
|
|
|
+ ws4['!cols'] = [{ wpx: 60 }, { wpx: 150 }, { wpx: 80 }]
|
|
|
+ const tagName = getCategoryNameById(categoryTagId).replace(/[\\/*?:[\]]/g, '')
|
|
|
+ xlsx.utils.book_append_sheet(wb, ws4, tagName ? `品类_${tagName.slice(0, 20)}` : '品类统计')
|
|
|
+ }
|
|
|
+
|
|
|
+ const fileName = `数据统计_${new Date().toISOString().slice(0, 10)}.xlsx`
|
|
|
+ xlsx.writeFile(wb, fileName)
|
|
|
+ }, [
|
|
|
+ totalStats,
|
|
|
+ yearCount,
|
|
|
+ yearCountYear,
|
|
|
+ level1Data,
|
|
|
+ level2Data,
|
|
|
+ textureData,
|
|
|
+ textureTagId,
|
|
|
+ textureOptions,
|
|
|
+ categoryData,
|
|
|
+ categoryTagId,
|
|
|
+ getCategoryNameById
|
|
|
+ ])
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className={styles.A1statistics}>
|
|
|
+ <div className={styles.A1header}>
|
|
|
+ <div className='pageTitle'>数据统计</div>
|
|
|
+ <Button className={styles.A1export} type='primary' onClick={dataExport}>
|
|
|
+ 数据导出
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className='A1_1'>
|
|
|
+ <div>
|
|
|
+ <div>
|
|
|
+ <img src={iconUrl + '/a11.png'} alt='' />
|
|
|
+ <h3>藏品总数</h3>
|
|
|
+ </div>
|
|
|
+ <p>
|
|
|
+ <span>{totalStats?.totalCount ?? 'empty'}</span>(件/套)
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <div>
|
|
|
+ <img src={iconUrl + '/a22.png'} alt='' />
|
|
|
+ <h3>藏品总数量</h3>
|
|
|
+ </div>
|
|
|
+ <p>
|
|
|
+ <span>{totalStats?.totalPcs ?? 'empty'}</span>(个)
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className='A1_1_3'>
|
|
|
+ <div>
|
|
|
+ <img src={iconUrl + '/a33.png'} alt='' />
|
|
|
+ <h3>定级文物数量</h3>
|
|
|
+ </div>
|
|
|
+ <p>
|
|
|
+ <span>{totalStats?.leveledCount ?? 'empty'}</span>(件/套)
|
|
|
+ <i>
|
|
|
+ 一级<i>{totalStats?.level1 ?? 0}</i>
|
|
|
+ </i>
|
|
|
+ <i>
|
|
|
+ 二级<i>{totalStats?.level2 ?? 0}</i>
|
|
|
+ </i>
|
|
|
+ <i>
|
|
|
+ 三级<i>{totalStats?.level3 ?? 0}</i>
|
|
|
+ </i>
|
|
|
+ <i>
|
|
|
+ 一般<i>{totalStats?.level4 ?? 0}</i>
|
|
|
+ </i>
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className='A1_1_4'>
|
|
|
+ <div>
|
|
|
+ <img src={iconUrl + '/a44.png'} alt='' />
|
|
|
+ <h3>年度新增产品数量</h3>
|
|
|
+ <div className='A1_1_4_1'>
|
|
|
+ <Select value={yearCountYear} onChange={setYearCountYear} options={yearOptions} />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <p>
|
|
|
+ <span>{yearCount ?? 'empty'}</span>(件/套)
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className='A1_box'>
|
|
|
+ <div className='A1_2'>
|
|
|
+ <div className='A1_2row'>
|
|
|
+ <div className='A1tit'>一级分类</div>
|
|
|
+ <div className='A1_2ech' id='echBox1'></div>
|
|
|
+ </div>
|
|
|
+ <div className='A1_2row'>
|
|
|
+ <div className='A1tit'>二级分类</div>
|
|
|
+ <div className='A1_2ech' id='echBox2'></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div className='A1_3'>
|
|
|
+ <div className='A1_3row'>
|
|
|
+ <div className='A1tit2'>
|
|
|
+ <div>材质统计</div>
|
|
|
+ <Select
|
|
|
+ value={textureTagId ?? undefined}
|
|
|
+ onChange={v => setTextureTagId(v ?? null)}
|
|
|
+ options={textureOptions.map(({ id, name }) => ({ value: id, label: name }))}
|
|
|
+ placeholder='请选择'
|
|
|
+ allowClear
|
|
|
+ style={{ width: 200 }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div className='A1_3ech' id='echBox3'></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className='A1_3row'>
|
|
|
+ <div className='A1tit2'>
|
|
|
+ <div>品类统计</div>
|
|
|
+ <TreeSelect
|
|
|
+ value={categoryTagId ?? undefined}
|
|
|
+ onChange={v => setCategoryTagId(v ?? null)}
|
|
|
+ treeData={categoryTreeSelectData}
|
|
|
+ placeholder='请选择'
|
|
|
+ allowClear
|
|
|
+ style={{ width: 200 }}
|
|
|
+ treeDefaultExpandAll
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div className='A1_3ech' id='echBox4'></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+const MemoA1statistics = React.memo(A1statistics)
|
|
|
+
|
|
|
+export default MemoA1statistics
|