kllay hai 4 días
pai
achega
288eae45a9

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 41 - 935
package-lock.json


+ 1 - 0
package.json

@@ -60,6 +60,7 @@
     "vue": "^3.4.38",
     "vue-clipboard3": "^2.0.0",
     "vue-cropper": "^1.0.8",
+    "vue-draggable-next": "^2.2.1",
     "vue-echarts": "^7.0.3",
     "vue-grid-layout": "^3.0.0-beta1",
     "vue-i18n": "^9.14.0",

+ 57 - 0
src/views/system/screenconsole/api.ts

@@ -8,3 +8,60 @@ export function GetList() {
 		method: 'get',
 	});
 }
+
+export function GetOverviewList() {
+	return request({
+		url: apiPrefix+'overview_stats/',
+		method: 'get',
+	});
+}
+
+export function GetDeviceRankList() {
+	return request({
+		url: apiPrefix+'device_rankings/',
+		method: 'get',
+	});
+}
+
+
+export function GetBorrowRanking(params:object) {
+	return request({
+		url: apiPrefix+'device_rankings/',
+		method: 'get',
+		params:params
+	});
+}
+
+
+
+
+export function GetUtilizationList() {
+	return request({
+		url: apiPrefix+'utilization_trend/',
+		method: 'get',
+	});
+}
+
+
+export function GetBorrowTrends() {
+	return request({
+		url: apiPrefix+'borrow_trends/',
+		method: 'get',
+	});
+}
+
+export function GetActiveUsers() {
+	return request({
+		url: apiPrefix+'active_users/',
+		method: 'get',
+	});
+}
+
+
+
+
+
+
+
+
+

+ 110 - 0
src/views/system/screenconsole/component/ActiveUsersPie.vue

@@ -0,0 +1,110 @@
+<template>
+    <div class="chart-container">
+      <h3>活跃用户 Top10 借用次数分布</h3>
+  
+      <div v-if="hasData">
+        <v-chart :option="chartOption" autoresize style="height: 400px;" />
+      </div>
+      <div v-else class="empty-tip">暂无数据</div>
+    </div>
+  </template>
+  
+  <script setup lang="ts">
+  import { ref, computed, onMounted } from 'vue'
+  import VChart from 'vue-echarts'
+  import { use } from 'echarts/core'
+  import {
+    CanvasRenderer
+  } from 'echarts/renderers'
+  import {
+    PieChart
+  } from 'echarts/charts'
+  import {
+    TitleComponent,
+    TooltipComponent,
+    LegendComponent
+  } from 'echarts/components'
+  import * as api from '../api'
+  
+  use([
+    CanvasRenderer,
+    PieChart,
+    TitleComponent,
+    TooltipComponent,
+    LegendComponent
+  ])
+  
+  interface ActiveUser {
+    user_id: number
+    user_name: string
+    user_code: string
+    borrow_count: number
+    total_quantity: number
+  }
+  
+  const rawData = ref<ActiveUser[]>([])
+  
+  const fetchData = async () => {
+    const res = await api.GetActiveUsers()
+    if (res.code === 2000) {
+      rawData.value = res.data.active_users
+    } else {
+      rawData.value = []
+    }
+  }
+  
+  onMounted(fetchData)
+  
+  const hasData = computed(() => rawData.value.length > 0)
+  
+  const chartOption = computed(() => {
+    const pieData = rawData.value.map(user => ({
+      name: user.user_name,
+      value: user.borrow_count
+    }))
+  
+    return {
+      title: {
+        text: '借用次数分布',
+        left: 'center'
+      },
+      tooltip: {
+        trigger: 'item',
+        formatter: '{b}: {c}次 ({d}%)'
+      },
+      legend: {
+        orient: 'vertical',
+        left: 'left',
+        data: pieData.map(item => item.name)
+      },
+      series: [
+        {
+          name: '借用次数',
+          type: 'pie',
+          radius: '60%',
+          data: pieData,
+          emphasis: {
+            itemStyle: {
+              shadowBlur: 10,
+              shadowOffsetX: 0,
+              shadowColor: 'rgba(0, 0, 0, 0.5)'
+            }
+          }
+        }
+      ]
+    }
+  })
+  </script>
+  
+  <style scoped>
+  .chart-container {
+    width: 100%;
+    margin: 0 auto;
+  }
+  .empty-tip {
+    text-align: center;
+    color: #999;
+    padding: 20px;
+  }
+  </style>
+  

+ 106 - 0
src/views/system/screenconsole/component/BorrowRankingList.vue

@@ -0,0 +1,106 @@
+<template>
+    <div class="ranking-panel">
+      <div class="panel-header">
+        <el-select v-model="borrowType" placeholder="借用类型" style="width: 140px">
+          <el-option label="全部" value="all" />
+          <el-option label="常规借用" value="regular" />
+          <el-option label="课堂借用" value="classroom" />
+          <el-option label="特殊借用" value="special" />
+        </el-select>
+  
+        <el-date-picker
+          v-model="dateRange"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          style="margin-left: 10px"
+        />
+  
+        <el-input
+          v-model="keyword"
+          placeholder="搜索用户/设备"
+          clearable
+          style="margin-left: 10px; width: 200px"
+        />
+  
+        <el-button type="primary" @click="fetchRanking" style="margin-left: 10px">
+          查询
+        </el-button>
+      </div>
+  
+      <el-table
+        v-if="rankingData.length"
+        :data="rankingData"
+        style="width: 100%; margin-top: 20px"
+        border
+      >
+        <el-table-column type="index" label="排名" width="60" />
+        <el-table-column prop="device_name" label="设备名" />
+        <el-table-column prop="device_code" label="设备编号" />
+        <el-table-column prop="borrow_count" label="借用次数" />
+        <!-- <el-table-column prop="total_quantity" label="累计借用册数" /> -->
+      </el-table>
+  
+      <div v-else class="empty-tip">暂无数据</div>
+    </div>
+  </template>
+  
+  <script setup lang="ts">
+  import { ref, onMounted } from 'vue'
+  import * as api from '../api'
+  
+  interface RankingItem {
+    user_id: number
+    user_name: string
+    user_code: string
+    borrow_count: number
+    total_quantity: number
+  }
+  
+  const borrowType = ref('all')
+  const dateRange = ref<[string, string] | null>(null)
+  const keyword = ref('')
+  const rankingData = ref<RankingItem[]>([])
+  
+  const fetchRanking = async () => {
+    const params: Record<string, any> = {
+      type: borrowType.value
+    }
+  
+    if (dateRange.value) {
+      params.start = dateRange.value[0]
+      params.end = dateRange.value[1]
+    }
+  
+    if (keyword.value) {
+      params.keyword = keyword.value
+    }
+  
+    const res = await api.GetBorrowRanking(params)
+    if (res.code === 2000) {
+      rankingData.value = res.data.borrow_count_ranking
+    } else {
+      rankingData.value = []
+    }
+  }
+  
+  onMounted(fetchRanking)
+  </script>
+  
+  <style scoped>
+  .ranking-panel {
+    width: 100%;
+    padding: 10px;
+  }
+  .panel-header {
+    display: flex;
+    align-items: center;
+    flex-wrap: wrap;
+  }
+  .empty-tip {
+    text-align: center;
+    color: #999;
+    padding: 20px;
+  }
+  </style>
+  

+ 143 - 0
src/views/system/screenconsole/component/BorrowTrendsChart.vue

@@ -0,0 +1,143 @@
+<template>
+    <div class="chart-container">
+      <h3>整体设备借用次数趋势</h3>
+  
+      <div class="filter-bar">
+        <button
+          v-for="range in ranges"
+          :key="range.value"
+          :class="{ active: selectedRange === range.value }"
+          @click="selectedRange = range.value"
+        >
+          {{ range.label }}
+        </button>
+      </div>
+  
+      <div v-if="hasData">
+        <v-chart :option="chartOption" autoresize style="height: 300px;" />
+      </div>
+      <div v-else class="empty-tip">暂无数据</div>
+    </div>
+  </template>
+  
+  <script setup lang="ts">
+  import { ref, computed, watch, onMounted } from 'vue'
+  import VChart from 'vue-echarts'
+  import { use } from 'echarts/core'
+  import {
+    CanvasRenderer
+  } from 'echarts/renderers'
+  import {
+    LineChart
+  } from 'echarts/charts'
+  import {
+    TitleComponent,
+    TooltipComponent,
+    GridComponent,
+    LegendComponent
+  } from 'echarts/components'
+  import * as api from '../api'
+  
+  use([
+    CanvasRenderer,
+    LineChart,
+    TitleComponent,
+    TooltipComponent,
+    GridComponent,
+    LegendComponent
+  ])
+  
+  interface BorrowTrend {
+    date: string
+    borrow_count: number
+    total_duration: number
+  }
+  
+  const ranges = [
+    { label: '近一周', value: 'week' },
+    { label: '近一个月', value: 'month' },
+    { label: '近一年', value: 'year' }
+  ]
+  
+  const selectedRange = ref('week')
+  const rawData = ref<BorrowTrend[]>([])
+  
+  const fetchData = async () => {
+    // const res = await api.GetBorrowTrends(selectedRange.value)
+    const res = await api.GetBorrowTrends()
+    if (res.code === 2000) {
+      rawData.value = res.data.trends
+    } else {
+      rawData.value = []
+    }
+  }
+  
+  watch(selectedRange, fetchData)
+  onMounted(fetchData)
+  
+  const hasData = computed(() => rawData.value.length > 0)
+  
+  const chartOption = computed(() => {
+    const dates = rawData.value.map(item => item.date)
+    const borrowCounts = rawData.value.map(item => item.borrow_count)
+    const durations = rawData.value.map(item => item.total_duration)
+  
+    return {
+      tooltip: {
+        trigger: 'axis'
+      },
+      legend: {
+        data: ['借用次数', '使用时长']
+      },
+      xAxis: {
+        type: 'category',
+        data: dates
+      },
+      yAxis: {
+        type: 'value',
+        name: '数量'
+      },
+      series: [
+        {
+          name: '借用次数',
+          type: 'line',
+          data: borrowCounts,
+          smooth: true
+        },
+        {
+          name: '使用时长',
+          type: 'line',
+          data: durations,
+          smooth: true
+        }
+      ]
+    }
+  })
+  </script>
+  
+  <style scoped>
+  .chart-container {
+    width: 100%;
+    margin: 0 auto;
+  }
+  .filter-bar {
+    margin-bottom: 10px;
+  }
+  .filter-bar button {
+    margin-right: 8px;
+    padding: 6px 12px;
+    border: none;
+    background: #eee;
+    cursor: pointer;
+  }
+  .filter-bar button.active {
+    background: #409eff;
+    color: white;
+  }
+  .empty-tip {
+    text-align: center;
+    color: #999;
+    padding: 20px;
+  }
+  </style>
+  

+ 59 - 0
src/views/system/screenconsole/component/DeviceBorrowCountChart.vue

@@ -0,0 +1,59 @@
+
+<template>
+    <div><h2>Top20单台设备累计借用次数排名</h2></div>
+    <div>
+      <el-table :data="sortedData" style="width: 100%">
+        <el-table-column label="排名" type="index" width="60" />
+        <el-table-column prop="device_name" label="设备名称" />
+        <el-table-column prop="device_code" label="设备编号" />
+        <el-table-column prop="borrow_count" label="借用次数" />
+      </el-table>
+    </div>
+  </template>
+  
+  <script setup lang="ts">
+  import { onMounted, ref, watch,computed } from 'vue'
+  import * as echarts from 'echarts'
+  import { useDeviceRanking } from '../composables/useDeviceRanking'
+  
+
+  const chartRef = ref<HTMLElement | null>(null)
+  const selectedTag = ref('')
+  const tags = ['全部', '高值设备', '摄影设备'] // 示例标签
+  const { borrowCountData, fetchRanking } = useDeviceRanking()
+  
+
+
+  interface DeviceItem {
+    device_id: number
+    device_name: string
+    device_code: string
+    borrow_count: number
+  }
+
+  const rawData = ref<DeviceItem[]>([])
+  
+  const loadData = async () => {
+      await fetchRanking()
+      rawData.value = borrowCountData.value
+
+  }
+
+  const sortedData = computed(() =>
+    [...rawData.value].sort((a, b) => b.borrow_count - a.borrow_count)
+  )
+
+
+  
+
+
+    
+  onMounted(loadData)
+  
+//   watch(borrowCountData, renderChart)
+  </script>
+  
+
+
+
+  

+ 47 - 0
src/views/system/screenconsole/component/DeviceBorrowDurationChart.vue

@@ -0,0 +1,47 @@
+<template>
+    <div><h2>Top20单台设备累计借用时长排名</h2></div>
+    <div>
+      <el-table :data="sortedDurationData" style="width: 100%">
+        <el-table-column label="排名" type="index" width="60" />
+        <el-table-column prop="device_name" label="设备名称" />
+        <el-table-column prop="device_code" label="设备编号" />
+        <el-table-column prop="total_duration" label="借用时长(小时)" />
+      </el-table>
+      
+      <!-- <template>
+        <draggable v-model="sortedDurationData" item-key="device_id" tag="el-table">
+            <template #item="{ element, index }">
+            <el-table-row>
+                <el-table-column>{{ index + 1 }}</el-table-column>
+                <el-table-column>{{ element }}</el-table-column>
+                <el-table-column>{{ element }}</el-table-column>
+                <el-table-column>{{ element }}</el-table-column>
+            </el-table-row>
+            </template>
+        </draggable>
+        </template> -->
+
+
+    </div>
+  </template>
+  
+  <script setup lang="ts">
+  import { ref, computed, onMounted } from 'vue'
+  import { useDeviceRanking } from '../composables/useDeviceRanking'
+ import { VueDraggableNext as draggable} from 'vue-draggable-next'
+  
+  const selectedTag = ref('')
+  const tags = ['全部', '高值设备', '摄影设备']
+  const { borrowDurationData, fetchRanking } = useDeviceRanking()
+  
+  const loadData = async () => {
+    await fetchRanking(selectedTag.value)
+  }
+  
+  const sortedDurationData = computed(() =>
+    [...borrowDurationData.value].sort((a, b) => Number(b.total_duration) - Number(a.total_duration))
+  )
+  
+  onMounted(loadData)
+  </script>
+  

+ 202 - 0
src/views/system/screenconsole/component/DeviceRanking.vue

@@ -0,0 +1,202 @@
+
+<template>
+    <div class="device-ranking">
+      <div class="header">
+        <h2>设备排行榜</h2>
+        <div class="tabs">
+          <button 
+            :class="{ active: activeTab === 'count' }"
+            @click="activeTab = 'count'"
+          >
+            借用次数排名
+          </button>
+          <button
+            :class="{ active: activeTab === 'duration' }"
+            @click="activeTab = 'duration'"
+          >
+            借用时长排名
+          </button>
+        </div>
+      </div>
+  
+      <div class="chart-container">
+        <div ref="chartRef" style="width: 100%; height: 400px;"></div>
+      </div>
+    </div>
+  </template>
+  
+  <script lang="ts">
+  import { defineComponent, ref, onMounted, watch } from 'vue';
+  import * as echarts from 'echarts';
+  import type { ECharts } from 'echarts';
+  
+  interface DeviceRanking {
+    device_id: number;
+    device_name: string;
+    device_code: string;
+    borrow_count?: number;
+    total_duration?: string;
+  }
+  
+  interface ApiResponse {
+    code: number;
+    data: {
+      borrow_count_ranking: DeviceRanking[];
+      borrow_duration_ranking: DeviceRanking[];
+    };
+    msg: string;
+  }
+  
+  export default defineComponent({
+    name: 'DeviceRanking',
+    props: {
+      apiData: {
+        type: Object as () => ApiResponse,
+        required: true
+      },
+      minValue: {
+        type: Number,
+        default: 5
+      }
+    },
+    setup(props) {
+      const activeTab = ref<'count' | 'duration'>('count');
+      const chartRef = ref<HTMLElement>();
+      const chartInstance = ref<ECharts>();
+  
+      const initChart = () => {
+        if (!chartRef.value) return;
+        
+        chartInstance.value = echarts.init(chartRef.value);
+        updateChart();
+      };
+  
+      const getFilteredData = () => {
+        const sourceData = activeTab.value === 'count' 
+          ? props.apiData.data.borrow_count_ranking 
+          : props.apiData.data.borrow_duration_ranking;
+  
+        return sourceData.filter(item => 
+          activeTab.value === 'count'
+            ? (item.borrow_count || 0) >= props.minValue
+            : parseInt(item.total_duration || '0') >= props.minValue
+        ).slice(0, 20);
+      };
+  
+      const updateChart = () => {
+        if (!chartInstance.value) return;
+  
+        const filteredData = getFilteredData();
+        const xAxisData = filteredData.map(item => item.device_name);
+        const seriesData = filteredData.map(item => 
+          activeTab.value === 'count' 
+            ? item.borrow_count 
+            : parseInt(item.total_duration || '0')
+        );
+  
+        const option = {
+          tooltip: {
+            trigger: 'axis',
+            axisPointer: {
+              type: 'shadow'
+            }
+          },
+          grid: {
+            left: '3%',
+            right: '4%',
+            bottom: '3%',
+            containLabel: true
+          },
+          xAxis: {
+            type: 'value',
+            name: activeTab.value === 'count' ? '借用次数' : '借用时长(小时)'
+          },
+          yAxis: {
+            type: 'category',
+            data: xAxisData,
+            axisLabel: {
+              interval: 0,
+              rotate: 30
+            }
+          },
+          series: [
+            {
+              name: activeTab.value === 'count' ? '借用次数' : '借用时长',
+              type: 'bar',
+              data: seriesData,
+              itemStyle: {
+                color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+                  { offset: 0, color: '#83bff6' },
+                  { offset: 0.5, color: '#188df0' },
+                  { offset: 1, color: '#0078ff' }
+                ])
+              },
+              emphasis: {
+                itemStyle: {
+                  color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+                    { offset: 0, color: '#2378f7' },
+                    { offset: 0.7, color: '#2378f7' },
+                    { offset: 1, color: '#83bff6' }
+                  ])
+                }
+              }
+            }
+          ]
+        };
+  
+        chartInstance.value.setOption(option);
+      };
+  
+      onMounted(() => {
+        initChart();
+      });
+  
+      watch(activeTab, () => {
+        updateChart();
+      });
+  
+      return {
+        activeTab,
+        chartRef
+      };
+    }
+  });
+  </script>
+  
+  <style scoped>
+  .device-ranking {
+    width: 100%;
+    padding: 20px;
+    background: #fff;
+    border-radius: 8px;
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+  }
+  
+  .header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 20px;
+  }
+  
+  .tabs button {
+    padding: 8px 16px;
+    margin-left: 10px;
+    border: 1px solid #dcdfe6;
+    background: #fff;
+    border-radius: 4px;
+    cursor: pointer;
+    transition: all 0.3s;
+  }
+  
+  .tabs button.active {
+    color: #fff;
+    background: #409eff;
+    border-color: #409eff;
+  }
+  
+  .chart-container {
+    width: 100%;
+  }
+  </style>
+  

+ 5 - 5
src/views/system/screenconsole/component/InfoCards.vue

@@ -39,11 +39,11 @@ const props = defineProps({
 
 const infoList = computed(()=>[
   { label: '设备总数', value: props.summaryData.total_devices, icon: Coin },
-  { label: '在借数', value: props.summaryData.total_borrows, icon: Box },
-  { label: '在库数', value: props.summaryData.total_warehouses, icon: Document },
-  { label: '维修中', value: props.summaryData.total_maintenances, icon: Avatar },
-  { label: '用户数', value: props.summaryData.total_students, icon: Folder },
-  { label: '借用件次数', value: props.summaryData.total_applications, icon: Monitor },
+  { label: '在借数', value: props.summaryData.borrowed_devices, icon: Box },
+  { label: '在库数', value: props.summaryData.available_devices, icon: Document },
+  { label: '今日借出', value: props.summaryData.today_borrowed, icon: Avatar },
+  { label: '今日归还', value: props.summaryData.today_returned, icon: Folder },
+  { label: '累计借用次数', value: props.summaryData.total_borrows, icon: Monitor },
 ]);
 function handleClick(item: any) {
   console.log('点击信息卡片:', item.label);

+ 123 - 0
src/views/system/screenconsole/component/UtilizationTrend.vue

@@ -0,0 +1,123 @@
+<template>
+    <div class="chart-container">
+      <h3>设备月度利用率变化趋势</h3>
+         <v-chart :option="chartOption" autoresize  />
+    </div>
+  </template>
+  
+  <script setup lang="ts">
+  import { ref, computed, onMounted } from 'vue'
+  import { use } from 'echarts/core'
+  import VChart from 'vue-echarts'
+  import * as api from '../api';
+  import {
+    CanvasRenderer
+  } from 'echarts/renderers'
+  import {
+    LineChart
+  } from 'echarts/charts'
+  import {
+    TitleComponent,
+    TooltipComponent,
+    GridComponent,
+    LegendComponent
+  } from 'echarts/components'
+  
+  use([
+    CanvasRenderer,
+    LineChart,
+    TitleComponent,
+    TooltipComponent,
+    GridComponent,
+    LegendComponent
+  ])
+
+
+
+  interface MonthlyUtilization {
+    month: string
+    rate: number
+  }
+  
+  const rawData = ref<MonthlyUtilization[]>([])
+  
+  const fetchData = async () => {
+    const res = await api.GetUtilizationList()
+    if (res.code === 2000) {
+        rawData.value = res.data.monthly_utilization
+
+
+    }
+
+  }
+
+
+  
+  
+  onMounted(fetchData)
+  
+  const chartOption = computed(() => {
+    const months = rawData.value.map(item => item.month)
+    const rates = rawData.value.map(item => item.rate)
+    console.log("rates:::",rates);
+    const diffs = rawData.value.map((item, index, arr) => {
+      if (index === 0) return 0
+      return +(item.rate - arr[index - 1].rate).toFixed(2)
+    })
+  
+    return {
+      title: {
+        text: '设备利用率提升幅度',
+        left: 'center'
+      },
+      tooltip: {
+        trigger: 'axis',
+        formatter: (params: any) => {
+          const rate = params[0].data
+          const diff = diffs[params[0].dataIndex]
+          const sign = diff > 0 ? '+' : ''
+          return `
+            月份:${params[0].name}<br/>
+            利用率:${rate}<br/>
+            提升幅度:${sign}${diff}
+          `
+        }
+      },
+      xAxis: {
+        type: 'category',
+        data: months
+      },
+      yAxis: {
+        type: 'value',
+        name: '利用率'
+      },
+      series: [
+        {
+          name: '月度利用率',
+          type: 'line',
+          data: rates,
+          smooth: true,
+          label: {
+            show: true,
+            formatter: '{c}'
+          },
+          lineStyle: {
+            color: '#5470C6'
+          },
+          itemStyle: {
+            color: '#91cc75'
+          }
+        }
+      ]
+    }
+  })
+  </script>
+  
+  <style scoped>
+  .chart-container {
+    width: 100%;
+    height: 400px;
+    margin: 0 auto;
+  }
+  </style>
+  

+ 26 - 0
src/views/system/screenconsole/composables/useDeviceRanking.ts

@@ -0,0 +1,26 @@
+// composables/useDeviceRanking.ts
+import { ref } from 'vue'
+import { request } from '/@/utils/service';
+
+export function useDeviceRanking() {
+  const borrowCountData = ref([])
+  const borrowDurationData = ref([])
+
+  const fetchRanking = async (tag?: string) => {
+    const res = await request({
+		url: '/api/system/dashboard/device_rankings/',
+		method: 'get',
+	});
+
+    if (res.code === 2000) {
+      borrowCountData.value = res.data.borrow_count_ranking
+      borrowDurationData.value = res.data.borrow_duration_ranking
+    }
+  }
+
+  return {
+    borrowCountData,
+    borrowDurationData,
+    fetchRanking
+  }
+}

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 253 - 201
src/views/system/screenconsole/index.vue


Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio