Quellcode durchsuchen

init: 初始化项目

jiaxing.liao vor 1 Monat
Commit
78d66ffd96

+ 7 - 0
.dockerignore

@@ -0,0 +1,7 @@
+   node_modules
+   dist
+   .DS_Store
+   .env
+   .git
+   .gitignore
+   *.md

+ 11 - 0
.editorconfig

@@ -0,0 +1,11 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+max_line_length = 100
+quote_type = single
+trim_trailing_whitespace = true

+ 2 - 0
.env.development

@@ -0,0 +1,2 @@
+# 服务器地址
+VITE_APP_SERVER=''

+ 2 - 0
.env.production

@@ -0,0 +1,2 @@
+# 服务器地址
+VITE_APP_SERVER='/api'

+ 28 - 0
.gitignore

@@ -0,0 +1,28 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+.sonar
+.scannerwork
+.sonarlint
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 1 - 0
.npmrc

@@ -0,0 +1 @@
+registry=https://registry.npmmirror.com

+ 3 - 0
.vscode/extensions.json

@@ -0,0 +1,3 @@
+{
+  "recommendations": ["Vue.volar"]
+}

+ 22 - 0
README.md

@@ -0,0 +1,22 @@
+# Vue 3 + TypeScript + Vite
+
+## Project Setup
+
+```bash
+# Install dependencies
+pnpm install
+
+# Start development server
+pnpm run dev
+
+# Build for production
+pnpm run build
+
+# Preview production build
+pnpm run preview
+```
+
+## Requirements
+
+- Node.js 18.x or higher
+- pnpm 10.x or higher

+ 10 - 0
auto-imports.d.ts

@@ -0,0 +1,10 @@
+/* eslint-disable */
+/* prettier-ignore */
+// @ts-nocheck
+// noinspection JSUnusedGlobalSymbols
+// Generated by unplugin-auto-import
+// biome-ignore lint: disable
+export {}
+declare global {
+
+}

+ 19 - 0
components.d.ts

@@ -0,0 +1,19 @@
+/* eslint-disable */
+// @ts-nocheck
+// Generated by unplugin-vue-components
+// Read more: https://github.com/vuejs/core/pull/3399
+// biome-ignore lint: disable
+export {}
+
+/* prettier-ignore */
+declare module 'vue' {
+  export interface GlobalComponents {
+    Card: typeof import('./src/components/Card.vue')['default']
+    CardTitle: typeof import('./src/components/CardTitle.vue')['default']
+    Chart: typeof import('./src/components/Chart/index.vue')['default']
+    ElTable: typeof import('element-plus/es')['ElTable']
+    ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
+    RouterLink: typeof import('vue-router')['RouterLink']
+    RouterView: typeof import('vue-router')['RouterView']
+  }
+}

+ 11 - 0
eslint.config.mjs

@@ -0,0 +1,11 @@
+// eslint.config.mjs
+import { base } from 'eslint-config-ali';
+import prettier from 'eslint-plugin-prettier/recommended';
+
+export default [
+  ...base,
+  prettier,
+  {
+    ignores: ['auto-imports.d.ts', 'components.d.ts', '**/useAutoScroll.ts', 'uno.config.ts'],
+  },
+];

+ 17 - 0
index.html

@@ -0,0 +1,17 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>智擎智慧工地看板</title>
+  </head>
+  <style>
+    body {
+      background: #011C37;
+    }
+  </style>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 49 - 0
package.json

@@ -0,0 +1,49 @@
+{
+  "name": "Smart Construction Dashboard",
+  "private": true,
+  "version": "1.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build:dev": "vue-tsc -b && vite build --mode development",
+    "build:prod": "vue-tsc -b && vite build --mode production",
+    "preview": "vite preview",
+    "lint": "eslint .",
+    "lint:fix": "prettier --write . && eslint --fix ."
+  },
+  "dependencies": {
+    "@element-plus/icons-vue": "^2.3.2",
+    "@visactor/vchart": "^2.0.3",
+    "dayjs": "^1.11.13",
+    "echarts": "^6.0.0",
+    "element-plus": "^2.10.6",
+    "less": "^4.4.0",
+    "less-loader": "^12.3.0",
+    "lodash-es": "^4.17.21",
+    "normalize.css": "^8.0.1",
+    "vue": "^3.5.18",
+    "vue-router": "^4.5.1"
+  },
+  "devDependencies": {
+    "@types/lodash-es": "^4.17.12",
+    "@types/node": "^24.2.1",
+    "@unocss/preset-wind3": "^66.4.2",
+    "@vitejs/plugin-vue": "^6.0.1",
+    "@vue/tsconfig": "^0.7.0",
+    "eslint": "^9.33.0",
+    "eslint-config-ali": "^16.5.0",
+    "eslint-config-prettier": "^10.1.2",
+    "eslint-plugin-prettier": "^5.2.6",
+    "prettier": "^3.5.3",
+    "prettier-config-ali": "^1.3.2",
+    "sharp": "^0.34.3",
+    "svgo": "^4.0.0",
+    "typescript": "~5.8.3",
+    "unocss": "^66.3.3",
+    "unplugin-auto-import": "^20.0.0",
+    "unplugin-vue-components": "^29.0.0",
+    "vite": "^7.1.0",
+    "vite-plugin-image-optimizer": "^2.0.2",
+    "vue-tsc": "^3.0.5"
+  }
+}

Datei-Diff unterdrückt, da er zu groß ist
+ 7089 - 0
pnpm-lock.yaml


+ 63 - 0
src/App.vue

@@ -0,0 +1,63 @@
+<template>
+  <div class="page-container" :style="styles">
+    <router-view />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onBeforeUnmount } from 'vue';
+// 设计稿尺寸
+const baseWidth = 1920;
+const baseHeight = 1080;
+
+// 动态样式
+const styles = ref({
+  width: `${baseWidth}px`,
+  height: `${baseHeight}px`,
+  transform: `scale(1) translate(-50%, -50%)`,
+});
+
+// 计算缩放比例
+function calcScale() {
+  const scaleW = window.innerWidth / baseWidth;
+  const scaleH = window.innerHeight / baseHeight;
+
+  styles.value = {
+    width: `${baseWidth}px`,
+    height: `${baseHeight}px`,
+    transform: `scale(${Math.min(scaleW, scaleH)}) translate(-50%, -50%)`,
+  };
+}
+
+calcScale();
+
+// 监听窗口大小变化
+onMounted(() => {
+  window.addEventListener('resize', calcScale);
+});
+
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', calcScale);
+});
+</script>
+
+<style lang="less">
+.page-container {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  transform-origin: 0 0;
+  font-family: 'Microsoft YaHei', sans-serif;
+  font-size: 14px;
+  color: #fff;
+  user-select: none;
+}
+
+/* 隐藏滚动条 */
+::-webkit-scrollbar {
+  display: none;
+}
+</style>

+ 1 - 0
src/api/index.ts

@@ -0,0 +1 @@
+// import { get, post } from '@/utils/request';

+ 6 - 0
src/assets/images/arrow.svg

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="18px" height="18px" xmlns="http://www.w3.org/2000/svg">
+  <g transform="matrix(1 0 0 1 -70 -123 )">
+    <path d="M 17.8853809931507 17.8938234060403  L 9.30821917808219 0.00660654362416107  L 0.092947345890411 17.5894505033557  L 9.04575128424658 13.2078963926175  L 17.8853809931507 17.8938234060403  Z " fill-rule="nonzero" fill="#e8e525" stroke="none" transform="matrix(1 0 0 1 70 123 )" />
+  </g>
+</svg>

BIN
src/assets/images/bg.png


BIN
src/assets/images/header.png


+ 17 - 0
src/assets/images/menu-active.svg

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="143px" height="48px" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <linearGradient gradientUnits="userSpaceOnUse" x1="156.863372093023" y1="42.2340425531915" x2="20.7623546511628" y2="42.2340425531915" id="LinearGradient372">
+      <stop id="Stop373" stop-color="#10396a" stop-opacity="0.113725490196078" offset="0" />
+      <stop id="Stop374" stop-color="#10396a" stop-opacity="0.447058823529412" offset="1" />
+    </linearGradient>
+    <linearGradient gradientUnits="userSpaceOnUse" x1="13" y1="46" x2="153.1543" y2="46" id="LinearGradient375">
+      <stop id="Stop376" stop-color="#2387b2" offset="0" />
+      <stop id="Stop377" stop-color="#378ed4" stop-opacity="0.12156862745098" offset="1" />
+    </linearGradient>
+  </defs>
+  <g transform="matrix(1 0 0 1 -13 -22 )">
+    <path d="M 14.9875387583177 68.5  L 28.4875387583177 23.5  L 154.012461241682 23.5  L 140.512461241682 68.5  L 14.9875387583177 68.5  Z " fill-rule="nonzero" fill="url(#LinearGradient372)" stroke="none" />
+    <path d="M 13 70  L 27.4 22  L 156 22  L 141.6 70  L 13 70  Z M 28.8500516777569 24  L 15.6500516777569 68  L 140.149948322243 68  L 153.349948322243 24  L 28.8500516777569 24  Z " fill-rule="nonzero" fill="url(#LinearGradient375)" stroke="none" />
+  </g>
+</svg>

+ 12 - 0
src/assets/images/menu.svg

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="143px" height="48px" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <linearGradient gradientUnits="userSpaceOnUse" x1="299.863372093023" y1="42.2340425531915" x2="163.762354651163" y2="42.2340425531915" id="LinearGradient378">
+      <stop id="Stop379" stop-color="#10396a" stop-opacity="0.113725490196078" offset="0" />
+      <stop id="Stop380" stop-color="#10396a" stop-opacity="0.447058823529412" offset="1" />
+    </linearGradient>
+  </defs>
+  <g transform="matrix(1 0 0 1 -156 -22 )">
+    <path d="M 156 70  L 170.4 22  L 299 22  L 284.6 70  L 156 70  Z " fill-rule="nonzero" fill="url(#LinearGradient378)" stroke="none" />
+  </g>
+</svg>

+ 13 - 0
src/components/Card.vue

@@ -0,0 +1,13 @@
+<template>
+  <div>
+
+  </div>
+</template>
+
+<script setup lang="ts">
+
+</script>
+
+<style scoped>
+
+</style>

+ 37 - 0
src/components/CardTitle.vue

@@ -0,0 +1,37 @@
+<template>
+  <div class="card-title">
+    <img :src="icon === 'circle' ? circle : diamond" alt=""></img>
+    <slot></slot>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { defineProps, withDefaults } from 'vue';
+import diamond from '@/assets/images/diamond.png';
+import circle from '@/assets/images/circle.png';
+
+withDefaults(defineProps<{ icon?: 'circle' | 'diamond' }>(), { icon: 'diamond' });
+</script>
+
+<style lang="less" scoped>
+.card-title {
+  display: flex;
+  align-items: center;
+  gap: 15px;
+  font-family: Microsoft YaHei;
+  font-weight: bold;
+  font-size: 20px;
+  margin-top: 4px;
+  line-height: 42px;
+  color: #FFFFFF;
+  background: linear-gradient(0deg, #B2FEFF 0%, #FFFFFF 100%);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+}
+
+img {
+  width: 26px;
+  height: 26px;
+  margin-left: 18px;
+}
+</style>

+ 58 - 0
src/components/Chart/index.vue

@@ -0,0 +1,58 @@
+<template>
+  <div ref="chart" class="w-full h-full"></div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, defineProps, watch } from 'vue';
+// 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口。
+import * as echarts from 'echarts/core';
+// 引入图表
+import { BarChart, LineChart, PieChart } from 'echarts/charts';
+// 引入标题,提示框,直角坐标系,数据集,内置数据转换器组件,组件后缀都为 Component
+import {
+  TitleComponent,
+  TooltipComponent,
+  GridComponent,
+  DatasetComponent,
+  TransformComponent,
+} from 'echarts/components';
+// 标签自动布局、全局过渡动画等特性
+import { LabelLayout, UniversalTransition } from 'echarts/features';
+// 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
+import { CanvasRenderer } from 'echarts/renderers';
+import type { EChartsOption } from 'echarts';
+
+// 注册必须的组件
+echarts.use([
+  TitleComponent,
+  TooltipComponent,
+  GridComponent,
+  DatasetComponent,
+  TransformComponent,
+  LabelLayout,
+  UniversalTransition,
+  CanvasRenderer,
+  LineChart,
+  PieChart,
+  BarChart,
+]);
+
+const props = defineProps<{
+  options: EChartsOption;
+}>();
+
+const chart = ref<HTMLDivElement>();
+const instance = ref<echarts.ECharts>();
+
+watch(
+  () => props.options,
+  () => {
+    instance.value?.setOption(props.options, true);
+  },
+);
+
+onMounted(() => {
+  instance.value = echarts.init(chart.value);
+  instance.value.setOption(props.options);
+});
+</script>

+ 169 - 0
src/hooks/useAutoScroll.ts

@@ -0,0 +1,169 @@
+// useAutoScroll.ts
+import { ref, onMounted, onUnmounted, watch, type Ref } from 'vue';
+
+interface AutoScrollOptions {
+  speed?: number; // 滚动速度(像素/秒)
+  direction?: 'up' | 'down' | 'left' | 'right'; // 滚动方向
+  loop?: boolean; // 是否循环
+  delay?: number; // 开始滚动前的延迟(毫秒)
+  pauseOnHover?: boolean; // 鼠标悬停时是否暂停
+}
+
+export function useAutoScroll(
+  containerRef: Ref<HTMLElement | null>,
+  contentRef: Ref<HTMLElement | null>,
+  options: AutoScrollOptions = {},
+) {
+  const { speed = 50, direction = 'up', loop = true, delay = 0, pauseOnHover = true } = options;
+
+  const el = containerRef.value;
+  const isPlaying = ref(false);
+  const animationFrameId = ref<number | null>(null);
+  const startTime = ref<number | null>(null);
+  const scrollPosition = ref(0);
+  const containerSize = ref(0);
+  const contentSize = ref(0);
+
+  // 初始化尺寸
+  const initSizes = () => {
+    if (!el || !contentRef.value) return;
+
+    if (direction === 'up' || direction === 'down') {
+      containerSize.value = el.clientHeight;
+      contentSize.value = contentRef.value.scrollHeight;
+    } else {
+      containerSize.value = el.clientWidth;
+      contentSize.value = contentRef.value.scrollWidth;
+    }
+  };
+
+  // 设置滚动位置
+  const setScrollPosition = (position: number) => {
+    if (!el) return;
+
+    if (direction === 'up' || direction === 'down') {
+      el.scrollTop = position;
+    } else {
+      el.scrollLeft = position;
+    }
+  };
+
+  // 动画循环
+  const animate = (timestamp: number) => {
+    if (!startTime.value) startTime.value = timestamp;
+    const elapsed = timestamp - startTime.value;
+
+    // 计算滚动位置
+    const newPosition = (elapsed / 1000) * speed;
+
+    if (loop) {
+      // 循环模式
+      const loopSize = contentSize.value - containerSize.value + 500;
+      scrollPosition.value = newPosition % loopSize;
+
+      // 反向滚动时调整位置
+      if (direction === 'up' || direction === 'left') {
+        setScrollPosition(contentSize.value - (scrollPosition.value % contentSize.value));
+      } else {
+        setScrollPosition(scrollPosition.value % contentSize.value);
+      }
+    } else {
+      // 非循环模式
+      scrollPosition.value = newPosition;
+
+      // 检查是否滚动到底部
+      if (scrollPosition.value >= contentSize.value - containerSize.value) {
+        scrollPosition.value = contentSize.value - containerSize.value;
+        setScrollPosition(scrollPosition.value);
+        stop();
+        return;
+      }
+
+      setScrollPosition(scrollPosition.value);
+    }
+
+    if (isPlaying.value) {
+      animationFrameId.value = requestAnimationFrame(animate);
+    }
+  };
+
+  // 开始滚动
+  const play = () => {
+    if (!isPlaying.value) {
+      isPlaying.value = true;
+      animationFrameId.value = requestAnimationFrame(animate);
+    }
+  };
+
+  // 暂停滚动
+  const pause = () => {
+    isPlaying.value = false;
+    if (animationFrameId.value) {
+      cancelAnimationFrame(animationFrameId.value);
+      animationFrameId.value = null;
+    }
+  };
+
+  // 停止滚动并重置位置
+  const stop = () => {
+    pause();
+    startTime.value = null;
+    scrollPosition.value = 0;
+    setScrollPosition(0);
+  };
+
+  // 重置并重新开始
+  const restart = () => {
+    stop();
+    setTimeout(play, delay);
+  };
+
+  // 监听尺寸变化
+  const resizeObserver = new ResizeObserver(() => {
+    initSizes();
+  });
+
+  // 初始化
+  onMounted(() => {
+    if (!el || !contentRef.value) return;
+
+    initSizes();
+    resizeObserver.observe(el);
+    resizeObserver.observe(contentRef.value);
+
+    // 设置延迟
+    setTimeout(play, delay);
+
+    // 鼠标悬停暂停
+    if (pauseOnHover) {
+      el.addEventListener('mouseenter', pause);
+      el.addEventListener('mouseleave', play);
+    }
+  });
+
+  // 清理
+  onUnmounted(() => {
+    if (el) {
+      el.removeEventListener('mouseenter', pause);
+      el.removeEventListener('mouseleave', play);
+    }
+    resizeObserver.disconnect();
+    pause();
+  });
+
+  // 监听选项变化
+  watch(
+    () => [speed, direction, loop, delay],
+    () => {
+      restart();
+    },
+  );
+
+  return {
+    isPlaying,
+    play,
+    pause,
+    stop,
+    restart,
+  };
+}

+ 128 - 0
src/layout/index.vue

@@ -0,0 +1,128 @@
+<template>
+  <div class="layout">
+    <div class="mask"></div>
+    <div class="header">
+      <img class="header-bg" src="@/assets/images/header.png" alt="">
+      <div class="menu">
+        <div class="menu-item" :class="{ active: route.path === '/home' }">
+          <router-link to="/home"><span>项目看板</span></router-link>
+        </div>
+        <div class="menu-item" :class="{ active: route.path === '/quality' }">
+          <router-link to="/quality"><span>质量管理</span></router-link>
+        </div>
+      </div>
+      <div class="title">智慧工地大屏看板</div>
+      <div class="menu">
+        <div class="menu-item" :class="{ active: route.path === '/safety' }">
+          <router-link to="/safety"><span>安全管理</span></router-link>
+        </div>
+        <div class="menu-item" :class="{ active: route.path === '/process' }">
+          <router-link to="/process"><span>进度管理</span></router-link>
+        </div>
+      </div>
+    </div>
+    <div class="content">
+      <router-view></router-view>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { useRoute } from 'vue-router';
+import { ref, onBeforeUnmount } from 'vue';
+import dayjs from 'dayjs';
+
+const route = useRoute();
+const time = ref(dayjs().format('YYYY-MM-DD dddd HH:mm:ss'));
+
+const timer = setInterval(() => {
+  time.value = dayjs().format('YYYY-MM-DD dddd HH:mm:ss');
+}, 1000);
+
+onBeforeUnmount(() => {
+  clearInterval(timer);
+});
+</script>
+
+<style lang="less">
+.layout {
+  width: 100%;
+  height: 100%;
+  background: url(@/assets/images/bg.png) no-repeat center;
+  background-size: cover;
+}
+
+.mask {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-color: rgba(36, 50, 99, 0.6);
+}
+
+.header {
+  position: relative;
+  display: flex;
+  height: 83px;
+  align-items: center;
+  justify-content: center;
+  z-index: 9;
+  background-color: rgba(4, 50, 99, 0.4);
+  &-bg {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 73px;
+    z-index: 1;
+  }
+  .title {
+    width: 680px;
+    text-align: center;
+    font-family: Microsoft YaHei;
+    font-weight: bold;
+    font-size: 40px;
+    color: #fff;
+    margin-top: -18px;
+    z-index: 9;
+  }
+  .menu {
+    display: flex;
+    align-items: center;
+    z-index: 9;
+    margin-top: 16px;
+    a {
+      color: #c1d4e3;
+      text-decoration: none;
+    }
+    &-item {
+      width: 143px;
+      height: 48px;
+      text-align: center;
+      font-family: 'Arial Bold Oblique', 'Arial Bold', 'Arial Normal', 'Arial', sans-serif;
+      font-weight: 700;
+      font-style: oblique;
+      font-size: 22px;
+      color: #ebf9f9;
+      line-height: 48px;
+      background: url(@/assets/images/menu.svg) no-repeat center;
+    }
+    .active {
+      background: url(@/assets/images/menu-active.svg) no-repeat center;
+      span {
+        color: #56efef;
+      }
+    }
+  }
+}
+
+.content {
+  position: relative;
+  width: 100%;
+  height: calc(100% - 83px);
+  padding: 20px;
+  box-sizing: border-box;
+  z-index: 1;
+}
+</style>

+ 13 - 0
src/main.ts

@@ -0,0 +1,13 @@
+import 'normalize.css';
+import { createApp } from 'vue';
+import App from './App.vue';
+import router from './router/index';
+import 'virtual:uno.css';
+import 'element-plus/theme-chalk/el-message.css';
+import './style.less';
+
+const app = createApp(App);
+
+app.use(router);
+
+app.mount('#app');

+ 40 - 0
src/router/index.ts

@@ -0,0 +1,40 @@
+import { createRouter, createWebHashHistory } from 'vue-router';
+import type { RouteRecordRaw } from 'vue-router';
+
+const routes: RouteRecordRaw[] = [
+  {
+    path: '/',
+    name: 'Layout',
+    component: () => import('../layout/index.vue'),
+    redirect: '/home',
+    children: [
+      {
+        path: '/home',
+        name: 'Home',
+        component: () => import('../views/home/index.vue'),
+      },
+      {
+        path: '/quality',
+        name: 'Quality',
+        component: () => import('../views/quality/index.vue'),
+      },
+      {
+        path: '/safety',
+        name: 'Safety',
+        component: () => import('../views/safety/index.vue'),
+      },
+      {
+        path: '/process',
+        name: 'Process',
+        component: () => import('../views/process/index.vue'),
+      },
+    ],
+  },
+];
+
+const router = createRouter({
+  history: createWebHashHistory(),
+  routes,
+});
+
+export default router;

+ 13 - 0
src/style.less

@@ -0,0 +1,13 @@
+.el-table {
+    --el-table-border-color: none !important;
+    --el-table-border: none !important;
+    --el-table-text-color: #FFFFFF !important;
+    --el-table-header-text-color: #CFFDFF !important;
+    --el-table-row-hover-bg-color: none !important;
+    --el-table-header-bg-color: rgba(24, 146, 240, .3) !important;
+    --el-table-bg-color: transparent !important;
+    --el-table-tr-bg-color: transparent !important;
+    --el-table-expanded-cell-bg-color: none !important;
+    --el-fill-color-lighter: rgba(24, 146, 240, .1) !important;
+    font-weight: bold !important;
+}

+ 246 - 0
src/utils/autofit.ts

@@ -0,0 +1,246 @@
+export interface IgnoreOption {
+  el: string;
+  height?: string;
+  width?: string;
+  scale?: number;
+  fontSize?: number;
+}
+export interface AutofitOption {
+  el?: string;
+  dw?: number;
+  dh?: number;
+  resize?: boolean;
+  ignore?: (IgnoreOption | string)[];
+  transition?: number;
+  delay?: number;
+  limit?: number;
+  cssMode?: "scale" | "zoom";
+  allowScroll?: boolean;
+}
+
+declare interface Autofit {
+  /**
+   * 参数列表
+   * 对象:
+   *
+   * @param {AutofitOption|String|undefined} options
+   * @param {boolean|undefined} isShowInitTip
+   * - 传入对象,对象中的属性如下:
+   * - el(可选):渲染的元素,默认是 "body"
+   * - dw(可选):设计稿的宽度,默认是 1920
+   * - dh(可选):设计稿的高度,默认是 1080
+   * - resize(可选):是否监听resize事件,默认是 true
+   * - ignore(可选):忽略缩放的元素(该元素将反向缩放),参数见readme.md
+   * - transition(可选):过渡时间,默认是 0
+   * - delay(可选):延迟,默认是 0
+   * - limit(可选):缩放限制,默认是 0.1
+   * - cssMode(可选):缩放模式,默认是 scale,可选值有 scale 和 zoom, zoom 模式可能对事件偏移有利
+   */
+  init(options?: AutofitOption | string, isShowInitTip?: boolean): void;
+  /**
+   * @param {String} id
+   * 关闭autofit.js造成的影响
+   *
+   */
+  off(id?: string): void;
+  /**
+   * 检查autofit.js是否正在运行
+   */
+  isAutofitRunning: boolean;
+  /**
+   * @param {string} el - 待处理的元素选择器
+   * @param {boolean} isKeepRatio - 是否保持纵横比(可选,默认为true,false时将充满父元素)
+   * @param {number|undefined} level - 缩放等级,用于手动调整缩放程度(可选,默认为 1)
+   */
+  elRectification: typeof elRectification;
+
+  /**
+   * 当前缩放比例
+   */
+  scale: number
+}
+
+// type Ignore = Array<{ height: number, width: number, fontSize: number, scale: number, el: HTMLElement, dom: HTMLElement }>;
+
+let currRenderDom: string | HTMLElement = null!;
+let currelRectification = "";
+let currelRectificationLevel: string | number = "";
+let currelRectificationIsKeepRatio: string | boolean = "";
+let resizeListener: EventListenerOrEventListenerObject = null!;
+let timer: number = null!;
+let currScale: string | number = 1;
+let isElRectification = false;
+const autofit: Autofit = {
+  isAutofitRunning: false,
+  init(options = {}, isShowInitTip = true) {
+    if (isShowInitTip) {
+      console.log(`autofit.js is running`);
+    }
+    const {
+      dw = 1920,
+      dh = 1080,
+      el = typeof options === "string" ? options : "body",
+      resize = true,
+      ignore = [],
+      transition = "none",
+      delay = 0,
+      limit = 0.1,
+      cssMode = "scale",
+      allowScroll = false,
+    } = options as AutofitOption;
+    currRenderDom = el;
+    const dom = document.querySelector<HTMLElement>(el);
+    if (!dom) {
+      console.error(`autofit: '${el}' is not exist`);
+      return;
+    }
+    const style = document.createElement("style");
+    const ignoreStyle = document.createElement("style");
+    style.lang = "text/css";
+    ignoreStyle.lang = "text/css";
+    style.id = "autofit-style";
+    ignoreStyle.id = "ignoreStyle";
+    !allowScroll && (style.innerHTML = `body {overflow: hidden;}`);
+    const bodyEl = document.querySelector("body")!;
+    bodyEl.appendChild(style);
+    bodyEl.appendChild(ignoreStyle);
+    dom.style.height = `${dh}px`;
+    dom.style.width = `${dw}px`;
+    dom.style.transformOrigin = `0 0`;
+    !allowScroll && (dom.style.overflow = "hidden");
+    keepFit(dw, dh, dom, ignore, limit, cssMode);
+    resizeListener = () => {
+      clearTimeout(timer);
+      if (delay != 0)
+        timer = setTimeout(() => {
+          keepFit(dw, dh, dom, ignore, limit, cssMode);
+          isElRectification &&
+            elRectification(currelRectification, currelRectificationIsKeepRatio, currelRectificationLevel);
+        }, delay) as unknown as number;
+      else {
+        keepFit(dw, dh, dom, ignore, limit, cssMode);
+        isElRectification &&
+          elRectification(currelRectification, currelRectificationIsKeepRatio, currelRectificationLevel);
+      }
+    };
+    resize && window.addEventListener("resize", resizeListener);
+    this.isAutofitRunning = true;
+    setTimeout(() => {
+      dom.style.transition = `${transition}s`;
+    });
+  },
+  off(el = "body") {
+    try {
+      window.removeEventListener("resize", resizeListener);
+      const autofitStyle = document.querySelector("#autofit-style");
+      autofitStyle && autofitStyle.remove();
+      const ignoreStyleDOM = document.querySelector("#ignoreStyle");
+      ignoreStyleDOM && ignoreStyleDOM.remove();
+      const temp = document.querySelector<HTMLDivElement>(currRenderDom ? currRenderDom as string : el);
+      temp && (temp.style.cssText = "")
+      isElRectification && offelRectification();
+    } catch (error) {
+      console.error(`autofit: Failed to remove normally`, error);
+    }
+    this.isAutofitRunning = false;
+    console.log(`autofit.js is off`);
+  },
+  elRectification: null!,
+  scale: currScale
+};
+
+function elRectification(el: string, isKeepRatio: string | boolean = true, level: string | number = 1) {
+  if (!autofit.isAutofitRunning) {
+    console.error("autofit.js:(elRectification): autofit has not been initialized yet");
+    return;
+  }
+  offelRectification();
+  !el && console.error(`autofit.js:elRectification bad selector: ${el}`);
+  currelRectification = el;
+  currelRectificationLevel = level;
+  currelRectificationIsKeepRatio = isKeepRatio;
+  const currEl = Array.from(document.querySelectorAll<HTMLElement & { originalWidth: number, originalHeight: number }>(el));
+  if (currEl.length == 0) {
+    console.error(`autofit.js:elRectification found no element by selector: "${el}"`);
+    return;
+  }
+  for (const item of currEl) {
+    const rectification = currScale == 1 ? 1 : Number(currScale) * Number(level);
+    if (!isElRectification) {
+      item.originalWidth = item.clientWidth;
+      item.originalHeight = item.clientHeight;
+    }
+    if (isKeepRatio) {
+      item.style.width = `${item.originalWidth * rectification}px`;
+      item.style.height = `${item.originalHeight * rectification}px`;
+    } else {
+      item.style.width = `${100 * rectification}%`;
+      item.style.height = `${100 * rectification}%`;
+    }
+    item.style.transform = `translateZ(0) scale(${1 / Number(currScale)})`;
+    item.style.transformOrigin = `0 0`;
+  }
+  isElRectification = true;
+}
+
+function offelRectification() {
+  if (!currelRectification) return;
+  isElRectification = false;
+  for (const item of Array.from(document.querySelectorAll<HTMLElement>(currelRectification))) {
+    item.style.width = ``;
+    item.style.height = ``;
+    item.style.transform = ``;
+  }
+}
+function keepFit(
+  dw: number,
+  dh: number,
+  dom: HTMLElement,
+  ignore: AutofitOption['ignore'],
+  limit: number,
+  cssMode: AutofitOption['cssMode'] = "scale"
+) {
+  const clientHeight = document.documentElement.clientHeight;
+  const clientWidth = document.documentElement.clientWidth;
+  currScale =
+    clientWidth / clientHeight < dw / dh ? clientWidth / dw : clientHeight / dh;
+  currScale = Math.abs(1 - currScale) > limit ? currScale : 1;
+  autofit.scale = +currScale;
+  const height = Math.round(clientHeight / Number(currScale));
+  const width = Math.round(clientWidth / Number(currScale));
+  dom.style.height = `${height}px`;
+  dom.style.width = `${width}px`;
+  if (cssMode === "zoom") {
+    (dom.style as any).zoom = `${currScale}`;
+  } else {
+    dom.style.transform = `translateZ(0) scale(${currScale})`;
+  }
+  const ignoreStyleDOM = document.querySelector("#ignoreStyle")!;
+  ignoreStyleDOM.innerHTML = "";
+  for (const temp of ignore!) {
+    const item = temp as IgnoreOption & { dom: string };
+    let itemEl = item.el || item.dom;
+    typeof item == "string" && (itemEl = item);
+    if (!itemEl || (typeof itemEl === "object" && !Object.keys(itemEl).length)) {
+      console.error(`autofit: found invalid or empty selector/object: ${itemEl}`);
+      continue;
+    }
+    const realScale: number = item.scale ? item.scale : 1 / Number(currScale);
+    const realFontSize = realScale != currScale && item.fontSize;
+    const realWidth = realScale != currScale && item.width;
+    const realHeight = realScale != currScale && item.height;
+    ignoreStyleDOM.innerHTML += `\n${itemEl} { 
+      transform: scale(${realScale})!important;
+      transform-origin: 0 0;
+      ${realWidth ? `width: ${realWidth}!important;` : ''}
+      ${realHeight ? `height: ${realHeight}!important;` : ''}
+    }`;
+    if (realFontSize) {
+      ignoreStyleDOM.innerHTML += `\n${itemEl} div ,${itemEl} span,${itemEl} a,${itemEl} * {
+        font-size: ${realFontSize}px;
+      }`;
+    }
+  }
+}
+autofit.elRectification = elRectification;
+export { autofit as default, elRectification };

+ 39 - 0
src/utils/index.ts

@@ -0,0 +1,39 @@
+/**
+ * 数字格式化
+ * @param num
+ * @param options 配置项(可选)
+ * @param options.decimals 小数位数
+ * @param options.separator 分隔符
+ * @param options.decimalPoint 小数点
+ * @returns
+ */
+export function formatNumber(
+  num?: number | string,
+  options?: {
+    decimals?: number;
+    separator?: string;
+    decimalPoint?: string;
+  },
+): string {
+  // 处理参数
+  const separator = options?.separator || ',';
+  const decimalPoint = options?.decimalPoint || '.';
+  const decimals = options?.decimals;
+
+  // 转换输入为数字
+  const number = typeof num === 'string' ? parseFloat(num) : Number(num);
+
+  if (isNaN(number)) return '-';
+
+  // 处理小数位数
+  const numStr = decimals !== undefined ? number.toFixed(decimals) : number.toString();
+
+  // 分割整数和小数部分
+  const parts = numStr.split('.');
+
+  // 处理整数部分
+  parts[0] = parts[0].replace(/\d(?=(\d{3})+(?!\d))/g, '$&' + separator);
+
+  // 重新组合
+  return parts.length > 1 ? `${parts[0]}${decimalPoint}${parts[1]}` : parts[0];
+}

+ 121 - 0
src/utils/request.ts

@@ -0,0 +1,121 @@
+interface RequestOptions extends RequestInit {
+  timeout?: number;
+  // 忽略存储
+  ignoreStorage?: boolean;
+  // 解析返回结果
+  parseResult?: boolean;
+}
+
+// 请求返回数据格式
+interface ResponseData<T = any> {
+  code: number;
+  status: boolean;
+  data: T;
+  message: string;
+}
+
+class RequestError extends Error {
+  constructor(
+    public status: number,
+    message: string,
+  ) {
+    super(message);
+    this.name = 'RequestError';
+  }
+}
+
+const DEFAULT_TIMEOUT = 1000 * 60 * 30;
+const BASE_URL = process.env.NODE_ENV === 'production' ? import.meta.env.VITE_APP_SERVER : '/api';
+
+export async function request<T = any>(url: string, options: RequestOptions = {}): Promise<T> {
+  const { timeout = DEFAULT_TIMEOUT, parseResult = true, ignoreStorage, ...fetchOptions } = options;
+
+  // 增加默认headers
+  const headers = new Headers(fetchOptions.headers);
+  if (!headers.has('Content-Type')) {
+    headers.set('Content-Type', 'application/json');
+  }
+
+  // 增加token
+  const token = localStorage.getItem('token');
+  if (token) {
+    headers.set('Authorization', `Bearer ${token}`);
+  }
+
+  // 创建超时终止
+  const controller = new AbortController();
+  const timeoutId = setTimeout(() => controller.abort(), timeout);
+  // Storage key 根据请求方法、url、参数生成key 获取到数据更新到本地
+  // 数据接口返回异常则使用本地存储数据 (数据异常或数据空)
+  let key = `${fetchOptions.method}_${url}`;
+  if (fetchOptions.body) {
+    key += `_${JSON.stringify(fetchOptions.body)}`;
+  }
+
+  try {
+    const response = await fetch(BASE_URL + url, {
+      ...fetchOptions,
+      headers,
+      signal: controller.signal,
+    });
+
+    clearTimeout(timeoutId);
+
+    if (!response.ok) {
+      throw new RequestError(response.status, response.statusText);
+    }
+
+    const data: ResponseData<T> = await response.json();
+
+    // 401处理
+    if (data.code === 401) {
+      localStorage.removeItem('token');
+      window.location.href = '/#/login';
+      throw new Error('Unauthorized');
+    }
+
+    // 返回解析的数据
+    if (!parseResult) {
+      return data as unknown as T;
+    }
+
+    if (data.code !== 200) {
+      throw new Error(data.message);
+    }
+
+    return data?.data;
+  } catch (error) {
+    if (error instanceof DOMException && error.name === 'AbortError') {
+      throw new Error(`Request timeout after ${timeout}ms`);
+    }
+    throw error;
+  }
+}
+
+const buildUrl = (url: string, data?: any) => {
+  if (!data) return url;
+  return `${url}?${new URLSearchParams(data as Record<string, string>).toString()}`;
+};
+
+// Helper methods
+export const get = <T>(url: string, data?: any, options?: RequestOptions) => {
+  const newUrl = buildUrl(url, data);
+  return request<T>(newUrl, { ...options, method: 'GET' });
+};
+
+export const post = <T>(url: string, data?: any, options?: RequestOptions) =>
+  request<T>(url, {
+    ...options,
+    method: 'POST',
+    body: JSON.stringify(data),
+  });
+
+export const put = <T>(url: string, data?: any, options?: RequestOptions) =>
+  request<T>(url, {
+    ...options,
+    method: 'PUT',
+    body: JSON.stringify(data),
+  });
+
+export const del = <T>(url: string, options?: RequestOptions) =>
+  request<T>(url, { ...options, method: 'DELETE' });

+ 13 - 0
src/views/home/index.vue

@@ -0,0 +1,13 @@
+<template>
+  <div>
+    首页
+  </div>
+</template>
+
+<script setup lang="ts">
+
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 13 - 0
src/views/process/index.vue

@@ -0,0 +1,13 @@
+<template>
+  <div>
+    进度
+  </div>
+</template>
+
+<script setup lang="ts">
+
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 13 - 0
src/views/quality/index.vue

@@ -0,0 +1,13 @@
+<template>
+  <div>
+    质量
+  </div>
+</template>
+
+<script setup lang="ts">
+
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 13 - 0
src/views/safety/index.vue

@@ -0,0 +1,13 @@
+<template>
+  <div>
+    安全
+  </div>
+</template>
+
+<script setup lang="ts">
+
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 61 - 0
src/vite-env.d.ts

@@ -0,0 +1,61 @@
+/// <reference types="vite/client" />
+
+declare module 'd3-cloud';
+
+declare module 'd3-cloud' {
+  interface Word {
+    text: string;
+    size: number;
+    value?: number;
+    x?: number;
+    y?: number;
+    rotate?: number;
+    [key: string]: any;
+  }
+
+  interface Cloud {
+    size: (size: [number, number]) => Cloud;
+    words: (words: Word[]) => Cloud;
+    padding: (padding: number) => Cloud;
+    rotate: (rotate: (d: Word) => number) => Cloud;
+    fontSize: (fontSize: (d: Word) => number) => Cloud;
+    random: (random: () => number) => Cloud;
+    spiral: (spiral: string) => Cloud;
+    on: (type: string, callback: (words: Word[]) => void) => Cloud;
+    start: () => void;
+  }
+
+  export default function (): Cloud;
+}
+
+declare module 'd3-scale' {
+  interface ScaleOrdinal<Range> {
+    (value: string | number): Range;
+    domain: (domain: Array<string | number>) => ScaleOrdinal<Range>;
+    range: (range: Range[]) => ScaleOrdinal<Range>;
+  }
+
+  export function scaleOrdinal<Range = string>(): ScaleOrdinal<Range>;
+}
+
+declare module 'd3-scale-chromatic' {
+  // 定义所有颜色方案
+  export const schemeCategory10: string[];
+  export const schemeAccent: string[];
+  export const schemeDark2: string[];
+  export const schemePaired: string[];
+  export const schemePastel1: string[];
+  export const schemePastel2: string[];
+  export const schemeSet1: string[];
+  export const schemeSet2: string[];
+  export const schemeSet3: string[];
+  export const schemeTableau10: string[];
+
+  // 其他颜色方案函数
+  export function interpolateBlues(t: number): string;
+  export function interpolateGreens(t: number): string;
+  export function interpolateGreys(t: number): string;
+  export function interpolateOranges(t: number): string;
+  export function interpolatePurples(t: number): string;
+  export function interpolateReds(t: number): string;
+}

+ 22 - 0
tsconfig.app.json

@@ -0,0 +1,22 @@
+{
+  "extends": "@vue/tsconfig/tsconfig.dom.json",
+  "compilerOptions": {
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+
+    /* Linting */
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "erasableSyntaxOnly": false,
+    "noFallthroughCasesInSwitch": true,
+    "noUncheckedSideEffectImports": true,
+
+    "types": ["node"],
+
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["src/*"]
+    }
+  },
+  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
+}

+ 4 - 0
tsconfig.json

@@ -0,0 +1,4 @@
+{
+  "files": [],
+  "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
+}

+ 25 - 0
tsconfig.node.json

@@ -0,0 +1,25 @@
+{
+  "compilerOptions": {
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+    "target": "ES2023",
+    "lib": ["ES2023"],
+    "module": "ESNext",
+    "skipLibCheck": true,
+
+    /* Bundler mode */
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "verbatimModuleSyntax": true,
+    "moduleDetection": "force",
+    "noEmit": true,
+
+    /* Linting */
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "erasableSyntaxOnly": true,
+    "noFallthroughCasesInSwitch": true,
+    "noUncheckedSideEffectImports": true
+  },
+  "include": ["vite.config.ts"]
+}

+ 17 - 0
uno.config.ts

@@ -0,0 +1,17 @@
+import { defineConfig } from 'unocss';
+import presetWind3 from '@unocss/preset-wind3';
+
+export default defineConfig({
+  // ...UnoCSS options
+  presets: [presetWind3()],
+  theme: {
+    colors: {
+      primary: '#082653',
+    },
+  },
+  shortcuts: {
+  },
+  rules: [
+    
+  ],
+});

+ 83 - 0
vite.config.ts

@@ -0,0 +1,83 @@
+import { defineConfig } from 'vite';
+import vue from '@vitejs/plugin-vue';
+import { resolve } from 'path';
+import UnoCSS from 'unocss/vite';
+import AutoImport from 'unplugin-auto-import/vite';
+import Components from 'unplugin-vue-components/vite';
+import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
+import { ViteImageOptimizer } from 'vite-plugin-image-optimizer';
+
+// https://vite.dev/config/
+export default defineConfig({
+  base: './',
+  plugins: [
+    vue(),
+    UnoCSS(),
+    // 自动引入组件
+    AutoImport({
+      resolvers: [ElementPlusResolver()],
+    }),
+    Components({
+      resolvers: [ElementPlusResolver()],
+    }),
+    ViteImageOptimizer({
+      logStats: true,
+      ansiColors: true,
+      test: /\.(jpe?g|png|gif|tiff|webp|svg|avif)$/i,
+      includePublic: true,
+      svg: {
+        multipass: true,
+        plugins: [
+          {
+            name: 'preset-default',
+            params: {
+              overrides: {
+                cleanupNumericValues: false,
+                cleanupIds: {
+                  minify: false,
+                  remove: false,
+                },
+                convertPathData: false,
+              },
+            },
+          },
+          'sortAttrs',
+          {
+            name: 'addAttributesToSVGElement',
+            params: {
+              attributes: [{ xmlns: 'http://www.w3.org/2000/svg' }],
+            },
+          },
+        ],
+      },
+      png: {
+        quality: 80,
+      },
+      jpeg: {
+        quality: 80,
+      },
+      jpg: {
+        quality: 80,
+      },
+      webp: {
+        lossless: true,
+      },
+    }),
+  ],
+  resolve: {
+    alias: {
+      '@': resolve(__dirname, 'src'),
+    },
+  },
+  server: {
+    proxy: {
+      '/api': {
+        // target: 'http://172.18.50.86:8080',
+        // target: 'http://172.26.28.188:8080',
+        target: 'http://localhost:8080',
+        changeOrigin: true,
+        rewrite: (path) => path.replace('/api', ''),
+      },
+    },
+  },
+});