Browse Source

feat: 点详细信息抽屉。

main
betaqi 4 weeks ago
parent
commit
9e15449eb9
  1. 2
      .env
  2. 13
      .idea/workspace.xml
  3. 200
      README.md
  4. 1
      global.types/auto-imports.d.ts
  5. 1
      global.types/components.d.ts
  6. 2
      package.json
  7. 28
      pnpm-lock.yaml
  8. 8
      src/api/module/device/category.ts
  9. 3
      src/main.ts
  10. 5
      src/utils/validate.ts
  11. 150
      src/views/engineering/components/category-point-drawer.vue
  12. 11
      src/views/engineering/config/components/StepDeviceCategory.vue

2
.env

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
VITE_BASE_API = '/remoteServer/admin-api/'
VITE_BASE_API_SYSTEM = '/remoteServer/admin-api/system/'
VITE_SHOW_ONLINE_DEVICE = true
VITE_BASE_URL = 'http://192.168.1.63:48089'
VITE_BASE_URL = 'http://43.140.245.32:48089'

13
.idea/workspace.xml

@ -4,7 +4,16 @@ @@ -4,7 +4,16 @@
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="1a5a317b-2539-4ca3-a13a-6db181725745" name="Changes" comment="feat: 功能调整" />
<list default="true" id="1a5a317b-2539-4ca3-a13a-6db181725745" name="Changes" comment="feat: 功能调整">
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/global.types/auto-imports.d.ts" beforeDir="false" afterPath="$PROJECT_DIR$/global.types/auto-imports.d.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/global.types/components.d.ts" beforeDir="false" afterPath="$PROJECT_DIR$/global.types/components.d.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/package.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pnpm-lock.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/pnpm-lock.yaml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/main.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/utils/validate.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/utils/validate.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/views/engineering/config/components/StepDeviceCategory.vue" beforeDir="false" afterPath="$PROJECT_DIR$/src/views/engineering/config/components/StepDeviceCategory.vue" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
@ -185,6 +194,8 @@ @@ -185,6 +194,8 @@
<workItem from="1765187147072" duration="460000" />
<workItem from="1765264069898" duration="42000" />
<workItem from="1765270558674" duration="670000" />
<workItem from="1765953826742" duration="1253000" />
<workItem from="1766036981199" duration="3226000" />
</task>
<task id="LOCAL-00001" summary="fix: 一些调整">
<option name="closed" value="true" />

200
README.md

@ -1,33 +1,193 @@ @@ -1,33 +1,193 @@
# vue-project
# AI 与 编程范式 (AI & Trends)
This template should help get you started developing with Vue 3 in Vite.
- **怎么理解现在的 AI 对程序员的影响?**
- **AI 辅助开发实践:**
- 如果用 AI 来写虚拟列表,提示词(Prompt)该怎么设计?
- 如何对 AI 生成的代码进行 Code Review?
## Recommended IDE Setup
---
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
# 前端工程化 (Engineering)
## Type Support for `.vue` Imports in TS
- **构建工具:** Webpack 的打包流程、常用 Loaders 和 Plugins 的作用、性能优化技巧(如懒加载、代码分割)。Vite
的构建原理(如 ESBuild 预编译、HMR 机制)及与 Webpack 的区别。
1. **Loader:**
- **作用:** Loader用于处理模块中的资源文件,将它们转换为Webpack可以理解的模块。
- **资源处理:** Loader处理各种资源文件,如JavaScript、CSS、图片、字体等,执行加载、转换、编译等任务。
- **模块级别:** Loader工作在模块级别,通常用于处理单个文件或模块,它们直接与模块的内用容交互。
- **配置:** Loader通过 `module.rules` 进行配置。示例:Babel Loader用于将ES6+ 转换为ES5,CSS
Loader用于加载CSS文件等。
2. **Plugin:**
- **作用:** Plugin用于扩展Webpack的功能,执行各种自定义构建任务和优化。
- **构建过程控制:** Plugin可以介入Webpack的构建过程,在不同的生命周期阶段执行任务,如代码压缩、文件生成、HTML注入等。
- **应用级别:** Plugin工作在应用程序级别,可以操作整个构建过程,包括资源文件的加载和输出。
- **配置:** Plugin通过 `plugins` 进行配置。
- **示例:** HtmlWebpackPlugin用于生成HTML文件,UglifyJSPlugin用于代码压缩等。
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
- **工程化优化:** Tree Shaking 移除未引用代码、按需加载第三方库。
## Customize configuration
- **代码规范:** ESLint、Prettier、Husky 等在项目中的应用。
See [Vite Configuration Reference](https://vite.dev/config/).
- **Git:** 常用工作流、代码合并与冲突解决。
## Project Setup
- **SSR/SSG:** 项目如何支持 SSG,并做 SEO?(你 Nuxt 项目相关)
```sh
npm install
```
---
### Compile and Hot-Reload for Development
# JavaScript 核心与基础 (JS Core & Basics)
```sh
npm run dev
```
- **数据类型:** - JS 有哪些数据类型?
- `undefined``null` 的区别是什么?
- `typeof(undefined)``typeof(null)` 相等吗?(注意 `typeof null === 'object'` 的历史遗留问题)。
### Type-Check, Compile and Minify for Production
- **运行机制:**
- JS 为什么是单线程的?
- **事件循环机制 (Event Loop):** 宏任务与微任务的执行顺序。
- **函数缓存 (Memoization):** 如何实现?有什么应用场景?
- **异步编程:** 怎么实现异步编程?
```sh
npm run build
```
- **Promise 深入:**
- 讲一讲 Promise 的原理。
- `Promise.race``Promise.all` 的区别。
- **场景题:** 三个相同的数组三次传入 `Promise.race` 会出现什么结果?
- Promise 的执行时机(微任务队列)。
- Promise 控制、并发限制。
- `async/await` 的底层原理。
- **对象与操作:**
- **深比较:** 如何实现两个对象的深比较?
- **深拷贝与浅拷贝**
- **高级特性:**
- EventEmitter 的实现。
- Proxy 与 Object.defineProperty 的区别(Proxy 优势:监听数组、无需递归遍历、拦截方式多)。
---
# CSS 与 响应式 (CSS & Responsive)
- **单位辨析:** `px`、`rem`、`vw` 的区别与适用场景。
- **多端适配:** 手机端和 PC 端用同一套 HTML 要注意什么细节?(媒体查询、视口设置、交互差异)。
---
# 前端框架 (React / Vue)
- **组件设计:**
- **组件封装能力:** 怎么样去封装一个组件(例如 el-dialog 弹窗)?
- *追问方向:* API 设计理念、Props/Events 定义、插槽 (Slots) 使用、状态管理(v-model)、生命周期处理、Teleport
的使用等。
- **框架原理:**
- React / Vue 的 diff 机制对比?
- Vue 组合式 API (Composition API):与 Options API 的对比,`ref` vs `reactive`,生命周期映射。
- 虚拟 DOM (VDOM) 原理与性能优化。
- 响应式原理(Vue 2 vs Vue 3)。
- **状态管理:** Redux / Mobx / Vuex / Pinia 的设计思想和适用场景。
- **高阶能力:**
- 如何构建一个可拓展的 schema-form(表单系统)?
- 动态路由、微前端架构。
---
# 浏览器与网络 (Browser & Network)
- **可视区域判断:** 判断一个元素是否在窗口内(`getBoundingClientRect` vs `IntersectionObserver`)。
- **列表与滚动优化:**
- **虚拟列表 (Virtual List):** 怎么实现?复杂度是多少?(如果是 AI 写的代码如何 Review)。
- **加载机制:** 下拉加载(Pull-to-refresh)和滚动加载(Infinite Scroll)怎么实现?
- **文件处理:**
- **大文件上传:** 分片上传、断点续传、秒传的实现原理。
- 前端如何处理视频编码/解码?(ffmpeg.wasm)。
- **性能优化:**
- **加载性能:** 减少 HTTP 请求、压缩资源、浏览器缓存、代码分割、懒加载、Gzip、CDN。
- **运行时性能:** 避免频繁 DOM 操作、防抖 (Debounce)与节流 (Throttle)、Web Worker、重绘与回流优化。
- **OffscreenCanvas** 的使用。
- **缓存策略:**
- **强缓存 (`Cache-Control`):** - `max-age`: 有效期。
- `no-cache`: 不走强缓存,走协商缓存。
- `no-store`: 彻底不缓存。
- **协商缓存 (304):** - `Last-Modified` / `If-Modified-Since` (基于时间,有秒级误差)。
- `ETag` / `If-None-Match` (基于内容指纹,更精准)。
- **网络协议:**
- **HTTP/1.1 vs HTTP/2 vs HTTP/3:** 多路复用、头部压缩、QUIC 协议。
- **跨域:** CORS、JSONP、Nginx 反代理。
- **通信方式:** REST vs RPC;WebSocket vs SSE vs EventSource。
---
# 算法 (Algorithms)
- **几何算法:** 判断一个点是否在一个三角形内部(面积法、向量叉积法、重心坐标法)。
- **常用手写:** 手写 Promise、手写防抖节流、深拷贝。
---
# 库与工具 (Tools & Libs)
- **Redis:** 用途(缓存、消息队列、分布式锁)。
- **消息队列:** Kafka / RabbitMQ / MQTT / ZMQ 的区别。
- **微服务通信:** 服务间如何交互。
---
# 前端安全 (Security)
- **XSS (跨站脚本攻击):** 原理(注入恶意脚本)、防范(转义、CSP)。
- **CSRF (跨站请求伪造):** 原理(利用用户凭证)、防范(SameSite Cookie、Token/Referer 验证)。
---
# 框架对比 (Framework Comparison)
- React 和 Vue 的区别,以及你对它们的理解。
- 微服务、实时流、渲染机制等差异理解。
- 微调 (Fine-tuning) 和 RAG (检索增强生成) 在 AI 应用中的概念。
---
# WebSocket 场景
- **WebSocket 连接在弱网环境下不稳定如何处理?心跳机制如何设计?**
1. **智能重连机制 (Reconnection Strategy)**
- **指数退避算法 (Exponential Backoff):** 重连间隔逐步递增 (1s -> 2s -> 4s -> ...)。
- **随机抖动 (Jitter):** 增加随机数,避免拥塞。
- **最大重试次数:** 避免无限空耗。
2. **可靠消息保证 (Message Reliability)**
- **消息队列:** 前端维护发送缓冲队列 (Buffer Queue)。
- **ACK 机制与序列号:** 消息带 `id`/`seq`,收到 Server ACK 后才从队列移除。
- **重连补发:** 连接恢复后,检查队列补发未 ACK 消息。
- **高并发消息处理(聊天室 200 -> 500 人):**
1. **逻辑层:** Set 防重、消息缓冲池。
2. **渲染层:** 虚拟列表 (Virtual List)、requestAnimationFrame 分批渲染/节流刷帧。
3. **视觉层:** 同用户消息合并。
- **多 Tab 共享连接(避免服务器压力):**
- **SharedWorker (最佳实践):** 所有 Tab 连接同一个 Worker,由 Worker 维持唯一的 WS 连接并分发消息。
- **BroadcastChannel:** 主 Tab 维护连接,广播给其他 Tab。
- **WebSocket 消息顺序如何保证?**
- **后端:** 发送严格递增的 `seqId`
- **前端:** 维护 `nextExpectedSeqId`**排序缓冲池**
1. **命中:** `seqId === nextExpectedSeqId` -> 渲染,`next++`。
2. **超前:** `seqId > nextExpectedSeqId` -> 放入缓冲池暂存,由于可能丢包,开启定时器尝试“补洞”。
3. **过期:** `seqId < nextExpectedSeqId` -> 丢弃。
---
# Web Worker 相关
- **Worker 场景:** 复杂 JSON 解析、路径规划、图像滤镜等 CPU 密集型任务。
- **通信与性能:**
- `postMessage` (结构化克隆): 默认行为,数据拷贝,有性能开销 ($O(n)$)。
- **`Transferable` (零拷贝):** 适用于 `ArrayBuffer` 等二进制数据。
- **特点:** 瞬间完成 ($O(1)$),但原对象在发送方会失效 (Neutering)。
- **注意:** 只能转移 `buffer` (如 `myUint8Array.buffer`)。

1
global.types/auto-imports.d.ts vendored

@ -8,6 +8,7 @@ export {} @@ -8,6 +8,7 @@ export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const ElLoading: typeof import('element-plus/es')['ElLoading']
const ElMessage: typeof import('element-plus/es')['ElMessage']
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp']

1
global.types/components.d.ts vendored

@ -23,6 +23,7 @@ declare module 'vue' { @@ -23,6 +23,7 @@ declare module 'vue' {
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']

2
package.json

@ -17,6 +17,8 @@ @@ -17,6 +17,8 @@
"@element-plus/icons-vue": "^2.3.2",
"@types/qs": "^6.9.18",
"@unocss/reset": "^66.0.0",
"ag-grid-community": "^35.0.0",
"ag-grid-vue3": "^35.0.0",
"axios": "^1.8.4",
"dayjs": "^1.11.13",
"dexie": "^4.0.11",

28
pnpm-lock.yaml

@ -26,6 +26,12 @@ importers: @@ -26,6 +26,12 @@ importers:
'@unocss/reset':
specifier: ^66.0.0
version: 66.5.10
ag-grid-community:
specifier: ^35.0.0
version: 35.0.0
ag-grid-vue3:
specifier: ^35.0.0
version: 35.0.0(vue@3.5.25(typescript@5.7.3))
axios:
specifier: ^1.8.4
version: 1.13.2
@ -1174,6 +1180,17 @@ packages: @@ -1174,6 +1180,17 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
ag-charts-types@13.0.0:
resolution: {integrity: sha512-mqOmKS0q4s2tt/C+CBG2Z+HWrSKYvRUCAlQzcKXKfARE3v/KdnBuxfjafa2c8ivElTTywdVdOe0q52Cow2Oggw==}
ag-grid-community@35.0.0:
resolution: {integrity: sha512-Cz3MA98zZygPwvCi8OKIhP0nea+YXdx8r5MwIXqVqnQwb/BEL05nGxwovIpelL6spv3jNlHQrTVgt4lw9J+nyg==}
ag-grid-vue3@35.0.0:
resolution: {integrity: sha512-R4Y/Ru6Pjwjgw6hWBsm61cav5s/QskkSrwN1uoEeJVWVBM/JggZtDFGo9OhlCocwjn5O8KSFgA+TVnw6iplaSQ==}
peerDependencies:
vue: ^3.5.0
ajv-formats@2.1.1:
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
peerDependencies:
@ -3792,6 +3809,17 @@ snapshots: @@ -3792,6 +3809,17 @@ snapshots:
acorn@8.15.0: {}
ag-charts-types@13.0.0: {}
ag-grid-community@35.0.0:
dependencies:
ag-charts-types: 13.0.0
ag-grid-vue3@35.0.0(vue@3.5.25(typescript@5.7.3)):
dependencies:
ag-grid-community: 35.0.0
vue: 3.5.25(typescript@5.7.3)
ajv-formats@2.1.1(ajv@8.17.1):
optionalDependencies:
ajv: 8.17.1

8
src/api/module/device/category.ts

@ -38,3 +38,11 @@ export const downloadDeviceTypeFile = (params: { projectName: string, pointName: @@ -38,3 +38,11 @@ export const downloadDeviceTypeFile = (params: { projectName: string, pointName:
responseType: 'blob',
})
}
export const getPointFileList = (params: { projectName: string, fileName: string }) => {
return globalServer({
url: '/project/get',
method: 'get',
params,
})
}

3
src/main.ts

@ -2,7 +2,8 @@ import './assets/styles/main.css' @@ -2,7 +2,8 @@ import './assets/styles/main.css'
import 'virtual:uno.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { AllCommunityModule, ModuleRegistry } from 'ag-grid-community';
ModuleRegistry.registerModules([AllCommunityModule]);
import App from './App.vue'
import router from './router'

5
src/utils/validate.ts

@ -1,15 +1,12 @@ @@ -1,15 +1,12 @@
export function validateName(name: string): string | null {
if (!name) return '名称不能为空'
// Check for Chinese and special characters (allow alphanumeric, _, -)
const regex = /^[a-zA-Z0-9_-]+$/
if (!regex.test(name)) {
return '名称不能包含中文或特殊符号'
}
// Check byte length (max 32)
const byteLength = new Blob([name]).size
const byteLength = new TextEncoder().encode(name).length
if (byteLength > 32) {
return '名称长度不能超过32字节'
}

150
src/views/engineering/components/category-point-drawer.vue

@ -0,0 +1,150 @@ @@ -0,0 +1,150 @@
<template>
<el-drawer
v-model="visible"
title="点位详情"
direction="rtl"
size="70%"
:before-close="handleClose"
>
<div class="h-full flex flex-col" v-loading="loading">
<div class="mb-4 flex gap-2">
<el-input
v-model="searchKeyword"
placeholder="搜索"
style="width: 300px"
clearable
:prefix-icon="Search"
@input="handleSearch"
/>
</div>
<div class="flex-1">
<ag-grid-vue
class="ag-theme-quartz h-full w-full"
:rowData="processedRowData"
:columnDefs="computedColDefs"
:animateRows="false"
:rowHeight="40"
@grid-ready="onGridReady"
>
</ag-grid-vue>
</div>
</div>
</el-drawer>
</template>
<script setup lang="ts">
import { ref, shallowRef, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { AgGridVue } from 'ag-grid-vue3'
import { Search } from '@element-plus/icons-vue'
import { getPointFileList } from '@/api/module/device/category'
const visible = ref(false)
const loading = ref(false)
const pointList = ref<any[][]>([])
const searchKeyword = ref('')
const col = ref([
'点位地址',
'点数量',
'功能码',
'数据类型',
'点读取周期ms',
'点位段名称',
])
const processedRowData = computed(() => {
return pointList.value.map(row => {
const obj: any = {}
col.value.forEach((header, index) => {
obj[header] = row[index]
})
return obj
})
})
const computedColDefs = computed(() => {
const colDefs = col.value.map((header, index) => {
const isLast = index === col.value.length - 1
const def: any = {
field: header,
headerName: header,
minWidth: 120,
flex: isLast ? 1 : 0,
sortable: true,
}
// Hex sorting for Point Address and Function Code
if (header === '点位地址' || header === '功能码') {
def.comparator = (valueA: string, valueB: string) => {
const numA = parseInt(valueA, 16) || 0
const numB = parseInt(valueB, 16) || 0
return numA - numB
}
}
// Numeric sorting for Count and Period
if (header === '点数量' || header === '点读取周期ms') {
def.comparator = (valueA: string, valueB: string) => {
const numA = parseFloat(valueA) || 0
const numB = parseFloat(valueB) || 0
return numA - numB
}
}
return def
})
console.log(colDefs)
return colDefs
})
const gridApi = shallowRef()
const onGridReady = (params: any) => {
gridApi.value = params.api
}
const open = async (projectName: string, fileName: string) => {
visible.value = true
loading.value = true
pointList.value = []
searchKeyword.value = ''
try {
const res = await getPointFileList({ projectName, fileName })
if (res.code === 0 && Array.isArray(res.data?.point)) {
pointList.value = res.data.point
} else {
ElMessage.error(res.msg || '获取点位详情失败')
}
} catch (error) {
ElMessage.error('获取点位详情失败')
} finally {
loading.value = false
}
}
const handleClose = (done: () => void) => {
done()
}
const handleSearch = () => {
if (gridApi.value) {
gridApi.value.setGridOption('quickFilterText', searchKeyword.value)
}
}
defineExpose({
open,
})
</script>
<style scoped>
:deep(.el-drawer__body) {
padding: 20px;
height: 100%;
overflow: hidden;
}
</style>

11
src/views/engineering/config/components/StepDeviceCategory.vue

@ -41,6 +41,9 @@ @@ -41,6 +41,9 @@
>
类别名称{{ categoryName }}
</h3>
<el-button @click.stop="handleOpenPointDrawer(fileName)">
点位详情
</el-button>
<el-tag
v-if="!!status"
size="small"
@ -76,6 +79,7 @@ @@ -76,6 +79,7 @@
{{ fileName || '-' }}
</span>
</el-tooltip>
<div
class="ml-2"
v-if="!!status"
@ -174,6 +178,7 @@ @@ -174,6 +178,7 @@
</span>
</template>
</el-dialog>
<CategoryPointDrawer ref="categoryPointDrawerRef" />
</div>
</template>
@ -192,6 +197,7 @@ import { @@ -192,6 +197,7 @@ import {
import { ChannelEnum } from '@/api/module/channel/index'
import type { IChannelOV } from '@/api/module/channel/index.d'
import { validateName } from '@/utils/validate'
import CategoryPointDrawer from '../../components/category-point-drawer.vue'
const props = defineProps<{
modelValue: IDeviceCategoryList
@ -207,6 +213,7 @@ const projectName = computed(() => route.query.name as string) @@ -207,6 +213,7 @@ const projectName = computed(() => route.query.name as string)
const dialogVisible = ref(false)
const formRef = ref<FormInstance>()
const categoryPointDrawerRef = ref<InstanceType<typeof CategoryPointDrawer>>()
const uploadControllers = reactive<{ [key: string]: AbortController }>({})
const uploadingStates = reactive<{ [key: string]: boolean }>({})
@ -371,6 +378,10 @@ async function handleDownload(pointName: string) { @@ -371,6 +378,10 @@ async function handleDownload(pointName: string) {
}
}
const handleOpenPointDrawer = (fileName: string) => {
categoryPointDrawerRef.value?.open(projectName.value, fileName)
}
// Check before leaving step
const checkBeforeLeave = async (): Promise<boolean> => {
if (!hasUploadingFiles()) {

Loading…
Cancel
Save