init5
235
app/.vscode/css_custom_data.json
vendored
Normal file
@@ -0,0 +1,235 @@
|
||||
{
|
||||
"version": 1.1,
|
||||
"properties": [
|
||||
{
|
||||
"name": "nine-slice",
|
||||
"description": "九宫格边框类 - 支持基础到高级功能",
|
||||
"references": [
|
||||
{
|
||||
"name": "MDN Reference",
|
||||
"url": "https://developer.mozilla.org/en-US/docs/Web/CSS/border-image"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "nine-slice-no-fill",
|
||||
"description": "九宫格边框类 - 不填充中心区域"
|
||||
},
|
||||
{
|
||||
"name": "nine-slice-animated",
|
||||
"description": "九宫格边框类 - 支持动画过渡"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-container",
|
||||
"description": "大屏布局容器 - 全屏深色背景"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-grid",
|
||||
"description": "大屏网格布局"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-grid-2x2",
|
||||
"description": "2x2 大屏网格布局"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-grid-3x3",
|
||||
"description": "3x3 大屏网格布局"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-grid-4x4",
|
||||
"description": "4x4 大屏网格布局"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-card",
|
||||
"description": "大屏卡片容器 - 半透明背景"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-card-highlight",
|
||||
"description": "高亮大屏卡片 - 蓝色边框"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-card-warning",
|
||||
"description": "警告大屏卡片 - 橙色边框"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-card-danger",
|
||||
"description": "危险大屏卡片 - 红色边框"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-title",
|
||||
"description": "大屏标题样式 - 大号白色文字"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-subtitle",
|
||||
"description": "大屏副标题样式 - 中号浅色文字"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-metric-title",
|
||||
"description": "指标标题样式 - 小号灰色文字"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-metric-large",
|
||||
"description": "大号数值显示 - 6xl 字体"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-metric-medium",
|
||||
"description": "中号数值显示 - 5xl 字体"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-metric-small",
|
||||
"description": "小号数值显示 - 3xl 字体"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-metric-trend-up",
|
||||
"description": "上升趋势指标 - 绿色文字"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-metric-trend-down",
|
||||
"description": "下降趋势指标 - 红色文字"
|
||||
},
|
||||
{
|
||||
"name": "chart-container",
|
||||
"description": "图表容器 - 中等尺寸"
|
||||
},
|
||||
{
|
||||
"name": "chart-container-large",
|
||||
"description": "大尺寸图表容器"
|
||||
},
|
||||
{
|
||||
"name": "chart-container-small",
|
||||
"description": "小尺寸图表容器"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-progress",
|
||||
"description": "进度条容器"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-progress-bar",
|
||||
"description": "进度条 - 蓝色渐变"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-progress-bar-success",
|
||||
"description": "进度条 - 绿色渐变"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-progress-bar-warning",
|
||||
"description": "进度条 - 橙色渐变"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-progress-bar-danger",
|
||||
"description": "进度条 - 红色渐变"
|
||||
},
|
||||
{
|
||||
"name": "status-indicator",
|
||||
"description": "状态指示器容器"
|
||||
},
|
||||
{
|
||||
"name": "status-online",
|
||||
"description": "在线状态 - 绿色"
|
||||
},
|
||||
{
|
||||
"name": "status-offline",
|
||||
"description": "离线状态 - 红色"
|
||||
},
|
||||
{
|
||||
"name": "status-warning",
|
||||
"description": "警告状态 - 橙色"
|
||||
},
|
||||
{
|
||||
"name": "status-dot",
|
||||
"description": "状态点 - 基础样式"
|
||||
},
|
||||
{
|
||||
"name": "status-dot-online",
|
||||
"description": "在线状态点 - 绿色脉冲动画"
|
||||
},
|
||||
{
|
||||
"name": "status-dot-offline",
|
||||
"description": "离线状态点 - 红色"
|
||||
},
|
||||
{
|
||||
"name": "status-dot-warning",
|
||||
"description": "警告状态点 - 橙色"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-fade-in",
|
||||
"description": "淡入动画"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-slide-up",
|
||||
"description": "从下往上滑入动画"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-scale-in",
|
||||
"description": "缩放进入动画"
|
||||
},
|
||||
{
|
||||
"name": "animate-fade-in",
|
||||
"description": "淡入动画 - 自定义关键帧"
|
||||
},
|
||||
{
|
||||
"name": "animate-slide-up",
|
||||
"description": "滑入动画 - 自定义关键帧"
|
||||
},
|
||||
{
|
||||
"name": "animate-scale-in",
|
||||
"description": "缩放动画 - 自定义关键帧"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-table",
|
||||
"description": "大屏数据表格样式"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-scrollbar",
|
||||
"description": "自定义滚动条样式"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-fullscreen",
|
||||
"description": "全屏模式样式"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-split-horizontal",
|
||||
"description": "水平分屏布局"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-split-vertical",
|
||||
"description": "垂直分屏布局"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-align-top",
|
||||
"description": "顶部对齐"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-align-bottom",
|
||||
"description": "底部对齐"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-align-left",
|
||||
"description": "左侧对齐"
|
||||
},
|
||||
{
|
||||
"name": "dashboard-align-right",
|
||||
"description": "右侧对齐"
|
||||
},
|
||||
{
|
||||
"name": "bg-flex-center",
|
||||
"description": "背景图居中覆盖 - 支持自定义padding控制"
|
||||
},
|
||||
{
|
||||
"name": "crud-container",
|
||||
"description": "CRUD 容器样式"
|
||||
},
|
||||
{
|
||||
"name": "crud-header",
|
||||
"description": "CRUD 头部样式"
|
||||
},
|
||||
{
|
||||
"name": "crud-table",
|
||||
"description": "CRUD 表格样式"
|
||||
},
|
||||
{
|
||||
"name": "crud-form",
|
||||
"description": "CRUD 表单样式"
|
||||
}
|
||||
]
|
||||
}
|
||||
25
app/app.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div class="h-screen w-screen overflow-hidden bg-black/0">
|
||||
<NuxtRouteAnnouncer />
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* 导入 Tailwind CSS */
|
||||
@import './assets/css/main.css';
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
font-family:
|
||||
'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑',
|
||||
Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
573
app/assets/css/main.css
Normal file
@@ -0,0 +1,573 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
/* 背景图居中 */
|
||||
/* 背景图居中覆盖 - 支持自定义padding控制 */
|
||||
.bg-flex-center {
|
||||
@apply flex items-center justify-center bg-center bg-no-repeat bg-contain;
|
||||
}
|
||||
|
||||
/* 自定义样式 */
|
||||
.crud-container {
|
||||
@apply max-w-7xl mx-auto p-6;
|
||||
}
|
||||
|
||||
.crud-header {
|
||||
@apply mb-6 flex justify-between items-center;
|
||||
}
|
||||
|
||||
.crud-table {
|
||||
@apply bg-white rounded-lg shadow-sm;
|
||||
}
|
||||
|
||||
.crud-form {
|
||||
@apply bg-white p-6 rounded-lg shadow-sm;
|
||||
}
|
||||
|
||||
/*
|
||||
九宫格(9-slice)图片边框 Utility
|
||||
|
||||
目标:
|
||||
- 传入一张九宫格图片的 URL,保持4个角按上下左右的参数不拉伸
|
||||
- 中间和4个角中间的区域拉伸
|
||||
- 支持自定义重复模式、填充行为、边框扩展等高级选项
|
||||
|
||||
核心参数:
|
||||
- 图片URL:--nine-src
|
||||
- 切片参数:--nine-top/right/bottom/left(基于图片实际像素)
|
||||
- 快速设置:--nine-all(一次性设置四个角相等)
|
||||
- 重复模式:--nine-repeat(stretch/repeat/round/space)
|
||||
- 填充控制:--nine-fill(1 填充/0 不填充)
|
||||
- 边框扩展:--nine-outset(向外扩展距离)
|
||||
- 边框宽度:--nine-width(边框图像宽度)
|
||||
|
||||
使用说明(Tailwind 原子类 + CSS 变量):
|
||||
1) 在元素上添加基础类:`nine-slice`
|
||||
2) 通过 Tailwind 任意值设置 CSS 变量:
|
||||
- 设置图片 URL:`[--nine-src:url('/public/images/frame-9.png')]`
|
||||
- 设置切片大小:`[--nine-top:10] [--nine-right:10] [--nine-bottom:10] [--nine-left:10]`
|
||||
- 快速设置(四个角相等):`[--nine-all:15]`(等效于设置四个角都为15)
|
||||
- 可选高级参数:
|
||||
- 重复模式:`[--nine-repeat:repeat]`(默认 stretch)
|
||||
- 向外扩展:`[--nine-outset:5]`(默认 0)
|
||||
- 边框宽度:`[--nine-width:2]`(默认 1)
|
||||
3) 填充控制:
|
||||
- 默认:填充中心区域
|
||||
- 添加 `nine-slice-no-fill` 类:不填充中心,露出背景
|
||||
|
||||
示例:
|
||||
<!-- 基础使用 -->
|
||||
<div class="nine-slice [--nine-src:url('/public/images/frame-9.png')] [--nine-top:10] [--nine-right:10] [--nine-bottom:10] [--nine-left:10] p-4">
|
||||
内容区域
|
||||
</div>
|
||||
|
||||
<!-- 快速设置(四个角相等) -->
|
||||
<div class="nine-slice [--nine-src:url('/public/images/frame-9.png')] [--nine-all:15] p-4">
|
||||
快速设置 - 四个角都是15px
|
||||
</div>
|
||||
|
||||
<!-- 重复图案 -->
|
||||
<div class="nine-slice [--nine-src:url('/public/images/pattern.png')] [--nine-top:15] [--nine-right:15] [--nine-bottom:15] [--nine-left:15] [--nine-repeat:round] p-6">
|
||||
重复图案边框
|
||||
</div>
|
||||
|
||||
<!-- 不填充中心 -->
|
||||
<div class="nine-slice nine-slice-no-fill [--nine-src:url('/public/images/frame.png')] [--nine-top:20] [--nine-right:20] [--nine-bottom:20] [--nine-left:20] p-8">
|
||||
不填充中心
|
||||
</div>
|
||||
|
||||
关键点:
|
||||
- 使用 CSS border-image 实现精确的9宫格切片
|
||||
- 4个角保持不拉伸,边缘和中心区域可配置拉伸或重复
|
||||
- 支持 fill/unfill 控制中心区域填充
|
||||
- 兼容所有现代浏览器
|
||||
*/
|
||||
@layer utilities {
|
||||
.H2 {
|
||||
@apply text-2xl font-bold;
|
||||
}
|
||||
.H3 {
|
||||
@apply text-xl font-bold;
|
||||
}
|
||||
.H4 {
|
||||
@apply text-base font-light;
|
||||
}
|
||||
.frame1 {
|
||||
@apply nine-slice [--nine-src:url('/public/images/Frame1.png')] [--nine-all:10] [--nine-fill:1];
|
||||
}
|
||||
.frame-border2 {
|
||||
@apply absolute inset-0 nine-slice [--nine-src:url('/public/images/frame-border2.png')] [--nine-all:50] [--nine-fill:1];
|
||||
}
|
||||
/* 九宫格边框类 - 支持基础到高级功能 */
|
||||
.nine-slice {
|
||||
border-style: solid;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* 边框厚度:基于独立的切片值或快速设置 */
|
||||
border-width: calc(var(--nine-top, var(--nine-all, 24)) * 1px)
|
||||
calc(var(--nine-right, var(--nine-all, 24)) * 1px)
|
||||
calc(var(--nine-bottom, var(--nine-all, 24)) * 1px)
|
||||
calc(var(--nine-left, var(--nine-all, 24)) * 1px);
|
||||
|
||||
/* 边框图像 */
|
||||
border-image-source: var(--nine-src);
|
||||
|
||||
/* 边框图像宽度 */
|
||||
border-image-width: var(--nine-width, 1);
|
||||
|
||||
/* 边框图像向外扩展 */
|
||||
border-image-outset: calc(var(--nine-outset, 0) * 1px);
|
||||
|
||||
/* 重复模式 - 支持 stretch, repeat, round, space */
|
||||
border-image-repeat: var(--nine-repeat, stretch);
|
||||
|
||||
/* 默认切片设置(填充中心) */
|
||||
border-image-slice: var(--nine-top, var(--nine-all, 24)) var(--nine-right, var(--nine-all, 24))
|
||||
var(--nine-bottom, var(--nine-all, 24)) var(--nine-left, var(--nine-all, 24)) fill;
|
||||
}
|
||||
|
||||
/* 不填充中心的变体 */
|
||||
.nine-slice.nine-slice-no-fill {
|
||||
border-image-slice: var(--nine-top, var(--nine-all, 24)) var(--nine-right, var(--nine-all, 24))
|
||||
var(--nine-bottom, var(--nine-all, 24)) var(--nine-left, var(--nine-all, 24));
|
||||
}
|
||||
|
||||
/* 动画支持 - 平滑过渡 */
|
||||
.nine-slice-animated {
|
||||
transition:
|
||||
border-image-source 0.3s ease,
|
||||
border-width 0.3s ease,
|
||||
border-image-slice 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
大屏看板专用 TailwindCSS Utilities
|
||||
|
||||
设计目标:
|
||||
- 适配4K大屏分辨率 (3840x2160)
|
||||
- 支持多种布局模式(网格、弹性、绝对定位)
|
||||
- 提供专业的数据可视化容器
|
||||
- 优化文字可读性和对比度
|
||||
- 支持动态主题切换
|
||||
*/
|
||||
|
||||
@layer utilities {
|
||||
/* 大屏布局容器 */
|
||||
.dashboard-container {
|
||||
@apply w-full h-screen overflow-hidden bg-slate-900 text-white;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
@apply grid gap-4 p-6 h-full;
|
||||
}
|
||||
|
||||
.dashboard-grid-2x2 {
|
||||
@apply grid grid-cols-2 grid-rows-2 gap-6 p-8;
|
||||
}
|
||||
|
||||
.dashboard-grid-3x3 {
|
||||
@apply grid grid-cols-3 grid-rows-3 gap-4 p-6;
|
||||
}
|
||||
|
||||
.dashboard-grid-4x4 {
|
||||
@apply grid grid-cols-4 grid-rows-4 gap-3 p-4;
|
||||
}
|
||||
|
||||
/* 大屏卡片容器 */
|
||||
.dashboard-card {
|
||||
@apply bg-slate-800/80 backdrop-blur-sm rounded-xl border border-slate-700 p-6 shadow-2xl;
|
||||
}
|
||||
|
||||
.dashboard-card-highlight {
|
||||
@apply dashboard-card border-blue-500/50 bg-slate-800/90;
|
||||
}
|
||||
|
||||
.dashboard-card-warning {
|
||||
@apply dashboard-card border-amber-500/50 bg-orange-900/20;
|
||||
}
|
||||
|
||||
.dashboard-card-danger {
|
||||
@apply dashboard-card border-red-500/50 bg-red-900/20;
|
||||
}
|
||||
|
||||
/* 大屏标题文字 */
|
||||
.dashboard-title {
|
||||
@apply text-4xl font-bold text-white tracking-wide mb-4;
|
||||
}
|
||||
|
||||
.dashboard-subtitle {
|
||||
@apply text-2xl font-semibold text-slate-300 mb-3;
|
||||
}
|
||||
|
||||
.dashboard-metric-title {
|
||||
@apply text-lg font-medium text-slate-400 mb-2;
|
||||
}
|
||||
|
||||
/* 大屏数值显示 */
|
||||
.dashboard-metric-large {
|
||||
@apply text-6xl font-bold text-white tabular-nums;
|
||||
}
|
||||
|
||||
.dashboard-metric-medium {
|
||||
@apply text-5xl font-semibold text-white tabular-nums;
|
||||
}
|
||||
|
||||
.dashboard-metric-small {
|
||||
@apply text-3xl font-medium text-slate-200 tabular-nums;
|
||||
}
|
||||
|
||||
.dashboard-metric-trend-up {
|
||||
@apply text-green-400 text-2xl font-semibold;
|
||||
}
|
||||
|
||||
.dashboard-metric-trend-down {
|
||||
@apply text-red-400 text-2xl font-semibold;
|
||||
}
|
||||
|
||||
/* 图表容器 */
|
||||
.chart-container {
|
||||
@apply w-full h-80 bg-slate-800/50 rounded-lg p-4 border border-slate-700;
|
||||
}
|
||||
|
||||
.chart-container-large {
|
||||
@apply w-full h-96 bg-slate-800/50 rounded-lg p-4 border border-slate-700;
|
||||
}
|
||||
|
||||
.chart-container-small {
|
||||
@apply w-full h-48 bg-slate-800/50 rounded-lg p-3 border border-slate-700;
|
||||
}
|
||||
|
||||
/* 进度条 */
|
||||
.dashboard-progress {
|
||||
@apply w-full h-3 bg-slate-700 rounded-full overflow-hidden;
|
||||
}
|
||||
|
||||
.dashboard-progress-bar {
|
||||
@apply h-full bg-gradient-to-r from-blue-500 to-cyan-400 rounded-full transition-all duration-500;
|
||||
}
|
||||
|
||||
.dashboard-progress-bar-success {
|
||||
@apply bg-gradient-to-r from-green-500 to-emerald-400;
|
||||
}
|
||||
|
||||
.dashboard-progress-bar-warning {
|
||||
@apply bg-gradient-to-r from-amber-500 to-yellow-400;
|
||||
}
|
||||
|
||||
.dashboard-progress-bar-danger {
|
||||
@apply bg-gradient-to-r from-red-500 to-rose-400;
|
||||
}
|
||||
|
||||
/* 状态指示器 */
|
||||
.status-indicator {
|
||||
@apply inline-flex items-center px-3 py-1 rounded-full text-sm font-medium;
|
||||
}
|
||||
|
||||
.status-online {
|
||||
@apply status-indicator bg-green-500/20 text-green-300 border border-green-500/30;
|
||||
}
|
||||
|
||||
.status-offline {
|
||||
@apply status-indicator bg-red-500/20 text-red-300 border border-red-500/30;
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
@apply status-indicator bg-amber-500/20 text-amber-300 border border-amber-500/30;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
@apply w-3 h-3 rounded-full mr-2;
|
||||
}
|
||||
|
||||
.status-dot-online {
|
||||
@apply status-dot bg-green-400 animate-pulse;
|
||||
}
|
||||
|
||||
.status-dot-offline {
|
||||
@apply status-dot bg-red-400;
|
||||
}
|
||||
|
||||
.status-dot-warning {
|
||||
@apply status-dot bg-amber-400;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
.dashboard-fade-in {
|
||||
@apply animate-fade-in duration-1000;
|
||||
}
|
||||
|
||||
.dashboard-slide-up {
|
||||
@apply animate-slide-up duration-700;
|
||||
}
|
||||
|
||||
.dashboard-scale-in {
|
||||
@apply animate-scale-in duration-500;
|
||||
}
|
||||
|
||||
/* 自定义动画 */
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scale-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fade-in ease-out;
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slide-up ease-out;
|
||||
}
|
||||
|
||||
.animate-scale-in {
|
||||
animation: scale-in ease-out;
|
||||
}
|
||||
|
||||
/* 数据表格样式 */
|
||||
.dashboard-table {
|
||||
@apply w-full border-collapse bg-slate-800/50 rounded-lg overflow-hidden;
|
||||
}
|
||||
|
||||
.dashboard-table th {
|
||||
@apply px-6 py-4 text-left text-sm font-semibold text-slate-300 border-b border-slate-700 bg-slate-800/80;
|
||||
}
|
||||
|
||||
.dashboard-table td {
|
||||
@apply px-6 py-4 text-sm text-slate-200 border-b border-slate-700/50;
|
||||
}
|
||||
|
||||
.dashboard-table tr:hover {
|
||||
@apply bg-slate-700/30;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.dashboard-scrollbar {
|
||||
overflow: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #475569 #1e293b;
|
||||
}
|
||||
|
||||
.dashboard-scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.dashboard-scrollbar::-webkit-scrollbar-track {
|
||||
background: #1e293b;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.dashboard-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: #475569;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.dashboard-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: #64748b;
|
||||
}
|
||||
|
||||
/* 全屏模式 */
|
||||
.dashboard-fullscreen {
|
||||
@apply fixed inset-0 z-50 bg-slate-900;
|
||||
}
|
||||
|
||||
/* 分屏布局 */
|
||||
.dashboard-split-horizontal {
|
||||
@apply grid grid-cols-2 gap-0 h-full;
|
||||
}
|
||||
|
||||
.dashboard-split-vertical {
|
||||
@apply grid grid-rows-2 gap-0 h-full;
|
||||
}
|
||||
|
||||
/* 边缘对齐 */
|
||||
.dashboard-align-top {
|
||||
@apply flex items-start justify-center;
|
||||
}
|
||||
|
||||
.dashboard-align-bottom {
|
||||
@apply flex items-end justify-center;
|
||||
}
|
||||
|
||||
.dashboard-align-left {
|
||||
@apply flex items-center justify-start;
|
||||
}
|
||||
|
||||
.dashboard-align-right {
|
||||
@apply flex items-center justify-end;
|
||||
}
|
||||
|
||||
/* 页面过渡动画 - 淡入淡出效果 */
|
||||
.page-transition {
|
||||
@apply transition-all duration-1000 ease-out;
|
||||
}
|
||||
|
||||
.page-enter {
|
||||
@apply opacity-0 scale-95;
|
||||
}
|
||||
|
||||
.page-enter-active {
|
||||
@apply transition-all duration-1000 ease-out;
|
||||
}
|
||||
|
||||
.page-enter-to {
|
||||
@apply opacity-100 scale-100;
|
||||
}
|
||||
|
||||
.page-leave {
|
||||
@apply opacity-100 scale-100;
|
||||
}
|
||||
|
||||
.page-leave-active {
|
||||
@apply transition-all duration-500 ease-in;
|
||||
}
|
||||
|
||||
.page-leave-to {
|
||||
@apply opacity-0 scale-95;
|
||||
}
|
||||
|
||||
/* 元素淡入动画 */
|
||||
.fade-in {
|
||||
animation: fade-in 1s ease-out forwards;
|
||||
opacity: 0; /* 初始状态设为透明,避免闪烁 */
|
||||
}
|
||||
|
||||
.fade-in-left {
|
||||
animation: fade-in-left 1s ease-out forwards;
|
||||
opacity: 0; /* 初始状态设为透明,避免闪烁 */
|
||||
}
|
||||
|
||||
.fade-in-right {
|
||||
animation: fade-in-right 1s ease-out forwards;
|
||||
opacity: 0; /* 初始状态设为透明,避免闪烁 */
|
||||
}
|
||||
|
||||
.fade-in-up {
|
||||
animation: fade-in-up 1s ease-out forwards;
|
||||
}
|
||||
|
||||
.fade-in-down {
|
||||
animation: fade-in-down 1s ease-out forwards;
|
||||
}
|
||||
|
||||
/* 淡入动画关键帧 */
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in-left {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in-right {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in-down {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画延迟工具类 */
|
||||
.delay-100 {
|
||||
animation-delay: 100ms;
|
||||
}
|
||||
|
||||
.delay-200 {
|
||||
animation-delay: 200ms;
|
||||
}
|
||||
|
||||
.delay-300 {
|
||||
animation-delay: 300ms;
|
||||
}
|
||||
|
||||
.delay-400 {
|
||||
animation-delay: 400ms;
|
||||
}
|
||||
|
||||
.delay-500 {
|
||||
animation-delay: 500ms;
|
||||
}
|
||||
|
||||
.delay-600 {
|
||||
animation-delay: 600ms;
|
||||
}
|
||||
|
||||
.delay-700 {
|
||||
animation-delay: 700ms;
|
||||
}
|
||||
|
||||
.delay-800 {
|
||||
animation-delay: 800ms;
|
||||
}
|
||||
|
||||
.delay-900 {
|
||||
animation-delay: 900ms;
|
||||
}
|
||||
|
||||
.delay-1000 {
|
||||
animation-delay: 1000ms;
|
||||
}
|
||||
}
|
||||
743
app/components/UniversalQueryBuilder.vue
Normal file
@@ -0,0 +1,743 @@
|
||||
<template>
|
||||
<div class="universal-query-builder bg-white rounded-lg shadow-lg p-6">
|
||||
<!-- 表选择 -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2"> 选择表 </label>
|
||||
<select
|
||||
v-model="selectedTable"
|
||||
@change="onTableChange"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">请选择表</option>
|
||||
<option v-for="table in availableTables" :key="table.key" :value="table.key">
|
||||
{{ table.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 查询条件构造器 -->
|
||||
<div v-if="selectedTable && tableInfo" class="mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-medium text-gray-900">查询条件</h3>
|
||||
<button
|
||||
@click="addCondition"
|
||||
class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
添加条件
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 条件列表 -->
|
||||
<div v-if="queryOptions.where && queryOptions.where.length > 0" class="space-y-4">
|
||||
<div
|
||||
v-for="(condition, index) in queryOptions.where"
|
||||
:key="index"
|
||||
class="flex items-center space-x-4 p-4 bg-gray-50 rounded-lg"
|
||||
>
|
||||
<!-- 逻辑操作符 -->
|
||||
<select
|
||||
v-if="index > 0"
|
||||
v-model="condition.logicalOperator"
|
||||
class="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="AND">AND</option>
|
||||
<option value="OR">OR</option>
|
||||
</select>
|
||||
|
||||
<!-- 字段选择 -->
|
||||
<select
|
||||
v-model="condition.field"
|
||||
@change="onFieldChange(condition)"
|
||||
class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">选择字段</option>
|
||||
<option v-for="field in tableInfo.fields" :key="field.name" :value="field.name">
|
||||
{{ field.name }} ({{ field.type }})
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<!-- 操作符选择 -->
|
||||
<select
|
||||
v-model="condition.operator"
|
||||
@change="onOperatorChange(condition)"
|
||||
class="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">选择操作符</option>
|
||||
<option
|
||||
v-for="operator in getAvailableOperators(condition.field)"
|
||||
:key="operator.value"
|
||||
:value="operator.value"
|
||||
>
|
||||
{{ operator.label }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<!-- 值输入 -->
|
||||
<div class="flex-1">
|
||||
<!-- 单值输入 -->
|
||||
<input
|
||||
v-if="
|
||||
!isMultiValueOperator(condition.operator) && !isNullOperator(condition.operator)
|
||||
"
|
||||
v-model="condition.value"
|
||||
:type="getInputType(condition.field, condition.operator)"
|
||||
:placeholder="getInputPlaceholder(condition.field, condition.operator)"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
|
||||
<!-- 多值输入 -->
|
||||
<div v-else-if="isMultiValueOperator(condition.operator)" class="space-y-2">
|
||||
<input
|
||||
v-if="condition.operator === 'between' || condition.operator === 'date_between'"
|
||||
v-model="condition.value[0]"
|
||||
:type="getInputType(condition.field, condition.operator)"
|
||||
placeholder="起始值"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<input
|
||||
v-if="condition.operator === 'between' || condition.operator === 'date_between'"
|
||||
v-model="condition.value[1]"
|
||||
:type="getInputType(condition.field, condition.operator)"
|
||||
placeholder="结束值"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<textarea
|
||||
v-if="condition.operator === 'in' || condition.operator === 'not_in'"
|
||||
v-model="condition.valueText"
|
||||
@input="onMultiValueInput(condition)"
|
||||
placeholder="输入多个值,用逗号分隔"
|
||||
rows="2"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 空值操作符不需要输入 -->
|
||||
<span v-else-if="isNullOperator(condition.operator)" class="text-gray-500 italic">
|
||||
无需输入值
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 删除按钮 -->
|
||||
<button
|
||||
@click="removeCondition(index)"
|
||||
class="px-3 py-2 bg-red-500 text-white rounded-md hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="text-gray-500 text-center py-8">
|
||||
暂无查询条件,点击"添加条件"开始构建查询
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<div v-if="selectedTable && tableInfo" class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2"> 全文搜索 </label>
|
||||
<input
|
||||
v-model="queryOptions.search"
|
||||
type="text"
|
||||
placeholder="输入搜索关键词..."
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<p class="text-sm text-gray-500 mt-1">搜索字段: {{ tableInfo.searchFields.join(', ') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 排序设置 -->
|
||||
<div v-if="selectedTable && tableInfo" class="mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-medium text-gray-900">排序设置</h3>
|
||||
<button
|
||||
@click="addSortCondition"
|
||||
class="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||
>
|
||||
添加排序
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="queryOptions.orderBy && queryOptions.orderBy.length > 0" class="space-y-2">
|
||||
<div
|
||||
v-for="(sort, index) in queryOptions.orderBy"
|
||||
:key="index"
|
||||
class="flex items-center space-x-4 p-3 bg-gray-50 rounded-lg"
|
||||
>
|
||||
<select
|
||||
v-model="sort.field"
|
||||
class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">选择字段</option>
|
||||
<option v-for="field in tableInfo.sortableFields" :key="field" :value="field">
|
||||
{{ field }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
v-model="sort.direction"
|
||||
class="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="ASC">升序</option>
|
||||
<option value="DESC">降序</option>
|
||||
</select>
|
||||
|
||||
<button
|
||||
@click="removeSortCondition(index)"
|
||||
class="px-3 py-2 bg-red-500 text-white rounded-md hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页设置 -->
|
||||
<div v-if="selectedTable" class="mb-6">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">分页设置</h3>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">页码</label>
|
||||
<input
|
||||
v-model.number="queryOptions.pagination.page"
|
||||
type="number"
|
||||
min="1"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">每页数量</label>
|
||||
<select
|
||||
v-model.number="queryOptions.pagination.limit"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option :value="10">10</option>
|
||||
<option :value="20">20</option>
|
||||
<option :value="50">50</option>
|
||||
<option :value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div v-if="selectedTable" class="flex space-x-4">
|
||||
<button
|
||||
@click="executeQuery"
|
||||
:disabled="loading"
|
||||
class="flex-1 px-6 py-3 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50"
|
||||
>
|
||||
{{ loading ? '查询中...' : '执行查询' }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="executeAggregateQuery"
|
||||
:disabled="loading"
|
||||
class="px-6 py-3 bg-purple-500 text-white rounded-md hover:bg-purple-600 focus:outline-none focus:ring-2 focus:ring-purple-500 disabled:opacity-50"
|
||||
>
|
||||
聚合查询
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="clearQuery"
|
||||
class="px-6 py-3 bg-gray-500 text-white rounded-md hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-500"
|
||||
>
|
||||
清空
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 查询结果 -->
|
||||
<div v-if="queryResult" class="mt-8">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">查询结果</h3>
|
||||
|
||||
<!-- 结果统计 -->
|
||||
<div class="mb-4 p-4 bg-blue-50 rounded-lg">
|
||||
<p class="text-sm text-blue-800">
|
||||
<span v-if="queryResult.pagination">
|
||||
第 {{ queryResult.pagination.page }} 页,共 {{ queryResult.pagination.totalPages }} 页,
|
||||
总计 {{ queryResult.total }} 条记录
|
||||
</span>
|
||||
<span v-else> 总计 {{ queryResult.total }} 条记录 </span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
v-for="field in getDisplayFields()"
|
||||
:key="field"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
{{ field }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<tr v-for="(item, index) in queryResult.data" :key="index">
|
||||
<td
|
||||
v-for="field in getDisplayFields()"
|
||||
:key="field"
|
||||
class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"
|
||||
>
|
||||
{{ formatValue(item[field]) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 聚合结果 -->
|
||||
<div v-if="aggregateResult" class="mt-8">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">聚合结果</h3>
|
||||
<div class="bg-gray-50 rounded-lg p-4">
|
||||
<pre class="text-sm">{{ JSON.stringify(aggregateResult, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { AdvancedQueryOptions, QueryCondition, SortCondition } from '@nuxt4crud/shared/types'
|
||||
|
||||
/**
|
||||
* 组件属性
|
||||
* Component props
|
||||
*/
|
||||
interface Props {
|
||||
modelValue?: AdvancedQueryOptions
|
||||
table?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件事件
|
||||
* Component events
|
||||
*/
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: AdvancedQueryOptions): void
|
||||
(e: 'query', result: any): void
|
||||
(e: 'aggregate', result: any): void
|
||||
(e: 'search', options: AdvancedQueryOptions): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: () => ({
|
||||
where: [],
|
||||
orderBy: [],
|
||||
pagination: { page: 1, limit: 20 },
|
||||
}),
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// 响应式数据
|
||||
const selectedTable = ref(props.table || '')
|
||||
const availableTables = ref<any[]>([])
|
||||
const tableInfo = ref<any>(null)
|
||||
const loading = ref(false)
|
||||
const queryResult = ref<any>(null)
|
||||
const aggregateResult = ref<any>(null)
|
||||
|
||||
// 监听table属性变化
|
||||
watch(() => props.table, (newTable) => {
|
||||
if (newTable) {
|
||||
selectedTable.value = newTable
|
||||
}
|
||||
})
|
||||
|
||||
// 查询选项
|
||||
const queryOptions = ref<AdvancedQueryOptions>({
|
||||
where: [],
|
||||
orderBy: [],
|
||||
search: '',
|
||||
pagination: { page: 1, limit: 20 },
|
||||
})
|
||||
|
||||
/**
|
||||
* 监听查询选项变化
|
||||
* Watch query options changes
|
||||
*/
|
||||
watch(
|
||||
queryOptions,
|
||||
newValue => {
|
||||
emit('update:modelValue', newValue)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
/**
|
||||
* 监听外部传入的值
|
||||
* Watch external value changes
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
newValue => {
|
||||
if (newValue && JSON.stringify(newValue) !== JSON.stringify(queryOptions.value)) {
|
||||
queryOptions.value = { ...newValue }
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取可用表列表
|
||||
* Get available tables
|
||||
*/
|
||||
async function loadAvailableTables() {
|
||||
try {
|
||||
const response = await $fetch('/api/table-info')
|
||||
if (response.success) {
|
||||
availableTables.value = response.data.tables
|
||||
} else {
|
||||
// 提供默认的表列表,确保能够正确加载用户表和文章表
|
||||
availableTables.value = [
|
||||
{
|
||||
key: 'User',
|
||||
name: '用户表'
|
||||
},
|
||||
{
|
||||
key: 'Post',
|
||||
name: '文章表'
|
||||
}
|
||||
]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load tables:', error)
|
||||
// 错误时提供默认的表列表
|
||||
availableTables.value = [
|
||||
{
|
||||
key: 'User',
|
||||
name: '用户表'
|
||||
},
|
||||
{
|
||||
key: 'Post',
|
||||
name: '文章表'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 表选择变化处理
|
||||
* Handle table selection change
|
||||
*/
|
||||
async function onTableChange() {
|
||||
if (!selectedTable.value) {
|
||||
tableInfo.value = null
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await $fetch(`/api/table-info?table=${selectedTable.value}`)
|
||||
if (response.success) {
|
||||
tableInfo.value = response.data.table
|
||||
// 清空之前的查询条件
|
||||
clearQuery()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load table info:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加查询条件
|
||||
* Add query condition
|
||||
*/
|
||||
function addCondition() {
|
||||
const condition: QueryCondition & { logicalOperator?: string; valueText?: string } = {
|
||||
field: '',
|
||||
operator: 'equals' as any,
|
||||
value: '',
|
||||
logicalOperator: queryOptions.value.where!.length > 0 ? 'AND' : undefined,
|
||||
valueText: '',
|
||||
}
|
||||
|
||||
if (!queryOptions.value.where) {
|
||||
queryOptions.value.where = []
|
||||
}
|
||||
|
||||
queryOptions.value.where.push(condition as any)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除查询条件
|
||||
* Remove query condition
|
||||
*/
|
||||
function removeCondition(index: number) {
|
||||
if (queryOptions.value.where) {
|
||||
queryOptions.value.where.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 字段变化处理
|
||||
* Handle field change
|
||||
*/
|
||||
function onFieldChange(condition: any) {
|
||||
// 重置操作符和值
|
||||
condition.operator = 'equals'
|
||||
condition.value = ''
|
||||
condition.valueText = ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作符变化处理
|
||||
* Handle operator change
|
||||
*/
|
||||
function onOperatorChange(condition: any) {
|
||||
// 根据操作符类型初始化值
|
||||
if (isMultiValueOperator(condition.operator)) {
|
||||
if (condition.operator === 'between' || condition.operator === 'date_between') {
|
||||
condition.value = ['', '']
|
||||
} else {
|
||||
condition.value = []
|
||||
condition.valueText = ''
|
||||
}
|
||||
} else if (isNullOperator(condition.operator)) {
|
||||
condition.value = null
|
||||
} else {
|
||||
condition.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 多值输入处理
|
||||
* Handle multi-value input
|
||||
*/
|
||||
function onMultiValueInput(condition: any) {
|
||||
if (condition.valueText) {
|
||||
condition.value = condition.valueText
|
||||
.split(',')
|
||||
.map((v: string) => v.trim())
|
||||
.filter(Boolean)
|
||||
} else {
|
||||
condition.value = []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用操作符
|
||||
* Get available operators for field
|
||||
*/
|
||||
function getAvailableOperators(fieldName: string) {
|
||||
if (!tableInfo.value || !fieldName) return []
|
||||
|
||||
const field = tableInfo.value.fields.find((f: any) => f.name === fieldName)
|
||||
if (!field) return []
|
||||
|
||||
return tableInfo.value.operators.filter((op: any) => op.types.includes(field.type))
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为多值操作符
|
||||
* Check if operator requires multiple values
|
||||
*/
|
||||
function isMultiValueOperator(operator: string): boolean {
|
||||
return ['in', 'not_in', 'between', 'date_between'].includes(operator)
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为空值操作符
|
||||
* Check if operator is null operator
|
||||
*/
|
||||
function isNullOperator(operator: string): boolean {
|
||||
return ['is_null', 'is_not_null'].includes(operator)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取输入框类型
|
||||
* Get input type for field and operator
|
||||
*/
|
||||
function getInputType(fieldName: string, operator: string): string {
|
||||
if (!tableInfo.value || !fieldName) return 'text'
|
||||
|
||||
const field = tableInfo.value.fields.find((f: any) => f.name === fieldName)
|
||||
if (!field) return 'text'
|
||||
|
||||
if (operator.startsWith('date_') || field.type === 'datetime') {
|
||||
return 'datetime-local'
|
||||
} else if (field.type === 'number') {
|
||||
return 'number'
|
||||
}
|
||||
|
||||
return 'text'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取输入框占位符
|
||||
* Get input placeholder
|
||||
*/
|
||||
function getInputPlaceholder(fieldName: string, operator: string): string {
|
||||
if (!tableInfo.value || !fieldName) return ''
|
||||
|
||||
const field = tableInfo.value.fields.find((f: any) => f.name === fieldName)
|
||||
if (!field) return ''
|
||||
|
||||
return `请输入${field.name}的值`
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加排序条件
|
||||
* Add sort condition
|
||||
*/
|
||||
function addSortCondition() {
|
||||
const sortCondition: SortCondition = {
|
||||
field: '',
|
||||
direction: 'ASC',
|
||||
}
|
||||
|
||||
if (!queryOptions.value.orderBy) {
|
||||
queryOptions.value.orderBy = []
|
||||
}
|
||||
|
||||
queryOptions.value.orderBy.push(sortCondition)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除排序条件
|
||||
* Remove sort condition
|
||||
*/
|
||||
function removeSortCondition(index: number) {
|
||||
if (queryOptions.value.orderBy) {
|
||||
queryOptions.value.orderBy.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行查询
|
||||
* Execute query
|
||||
*/
|
||||
async function executeQuery() {
|
||||
if (!selectedTable.value) return
|
||||
|
||||
loading.value = true
|
||||
queryResult.value = null
|
||||
aggregateResult.value = null
|
||||
|
||||
// 发出搜索事件,传递查询选项
|
||||
emit('search', queryOptions.value)
|
||||
|
||||
try {
|
||||
const response = await $fetch('/api/universal-query', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
table: selectedTable.value,
|
||||
options: queryOptions.value,
|
||||
mode: 'query',
|
||||
},
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
queryResult.value = response.data
|
||||
emit('query', response.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Query failed:', error)
|
||||
alert('查询失败: ' + (error as any).data?.message || '未知错误')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行聚合查询
|
||||
* Execute aggregate query
|
||||
*/
|
||||
async function executeAggregateQuery() {
|
||||
if (!selectedTable.value) return
|
||||
|
||||
loading.value = true
|
||||
queryResult.value = null
|
||||
aggregateResult.value = null
|
||||
|
||||
try {
|
||||
// 构建聚合选项
|
||||
const aggregateOptions = {
|
||||
...queryOptions.value,
|
||||
aggregate: {
|
||||
count: true,
|
||||
sum: tableInfo.value.fields.filter((f: any) => f.type === 'number').map((f: any) => f.name),
|
||||
avg: tableInfo.value.fields.filter((f: any) => f.type === 'number').map((f: any) => f.name),
|
||||
min: tableInfo.value.fields.filter((f: any) => f.type === 'number').map((f: any) => f.name),
|
||||
max: tableInfo.value.fields.filter((f: any) => f.type === 'number').map((f: any) => f.name),
|
||||
},
|
||||
}
|
||||
|
||||
const response = await $fetch('/api/universal-query', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
table: selectedTable.value,
|
||||
options: aggregateOptions,
|
||||
mode: 'aggregate',
|
||||
},
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
aggregateResult.value = response.data.result
|
||||
emit('aggregate', response.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Aggregate query failed:', error)
|
||||
alert('聚合查询失败: ' + (error as any).data?.message || '未知错误')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空查询
|
||||
* Clear query
|
||||
*/
|
||||
function clearQuery() {
|
||||
queryOptions.value = {
|
||||
where: [],
|
||||
orderBy: tableInfo.value?.defaultOrderBy || [],
|
||||
search: '',
|
||||
pagination: { page: 1, limit: 20 },
|
||||
}
|
||||
queryResult.value = null
|
||||
aggregateResult.value = null
|
||||
// 发出重置事件
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取显示字段
|
||||
* Get display fields
|
||||
*/
|
||||
function getDisplayFields(): string[] {
|
||||
if (!queryResult.value?.data || queryResult.value.data.length === 0) return []
|
||||
return Object.keys(queryResult.value.data[0])
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化值显示
|
||||
* Format value for display
|
||||
*/
|
||||
function formatValue(value: any): string {
|
||||
if (value === null || value === undefined) return '-'
|
||||
if (typeof value === 'object') return JSON.stringify(value)
|
||||
if (typeof value === 'string' && value.includes('T')) {
|
||||
// 可能是日期时间
|
||||
try {
|
||||
return new Date(value).toLocaleString()
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
|
||||
// 组件挂载时加载表列表
|
||||
onMounted(() => {
|
||||
loadAvailableTables()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.universal-query-builder {
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
25
app/composables/powerStation/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* PowerStation 模块入口
|
||||
* 统一导出所有组合式函数和类型定义
|
||||
*/
|
||||
|
||||
// 类型导出
|
||||
export type {
|
||||
BreadcrumbItem, HighlightConfig, InteractionCallbacks,
|
||||
// 交互系统类型
|
||||
InteractionState, LightingMode,
|
||||
// 模型管理系统类型
|
||||
ModelHierarchyNode, ModelModeContext, RaycasterConfig, RotateDirection,
|
||||
// 视图控制类型
|
||||
StandardViewType
|
||||
} from './types'
|
||||
|
||||
// 组合式函数导出
|
||||
export { useInteraction } from './useInteraction'
|
||||
export { useModelManager } from './useModelManager'
|
||||
export { useThreeScene } from './useThreeScene'
|
||||
|
||||
// 模式导出
|
||||
export { useFullMode } from './modes/useFullMode'
|
||||
export { useSimplifiedMode } from './modes/useSimplifiedMode'
|
||||
|
||||
363
app/composables/powerStation/modes/useFullMode.ts
Normal file
@@ -0,0 +1,363 @@
|
||||
/**
|
||||
* 完整模式 - 模型加载
|
||||
* 支持完整的模型层级结构和合并优化
|
||||
*/
|
||||
import { ElMessage } from 'element-plus'
|
||||
import * as THREE from 'three'
|
||||
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
||||
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
||||
import { markRaw } from 'vue'
|
||||
import type { ModelModeContext } from '../types'
|
||||
|
||||
// ============================================================
|
||||
// 配置常量
|
||||
// ============================================================
|
||||
|
||||
/** DRACO解码器路径 */
|
||||
const DRACO_DECODER_PATH = 'https://www.gstatic.com/draco/v1/decoders/'
|
||||
|
||||
// ============================================================
|
||||
// 辅助函数
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 从URL提取文件名(不含路径和扩展名)
|
||||
* @param url - 文件URL
|
||||
* @returns 文件名
|
||||
*/
|
||||
const extractFileName = (url: string): string => {
|
||||
const urlParts = url.split('/')
|
||||
const fileNameWithExt = urlParts[urlParts.length - 1]
|
||||
const fileName = fileNameWithExt.split('.').slice(0, -1).join('.')
|
||||
return fileName || '模型'
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归打印模型中所有物体的路径(用于调试)
|
||||
* @param object - Three.js 对象
|
||||
* @param path - 当前路径
|
||||
*/
|
||||
const printModelObjectPaths = (object: THREE.Object3D, path: string = '') => {
|
||||
const fullPath = path ? `${path}/${object.name}` : object.name
|
||||
object.children.forEach(child => {
|
||||
printModelObjectPaths(child, fullPath)
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 几何体合并
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 合并子树中的所有网格
|
||||
* 用于优化渲染性能
|
||||
* @param root - 要合并的根对象
|
||||
*/
|
||||
const mergeSubTree = (root: THREE.Object3D) => {
|
||||
// 收集所有网格
|
||||
const meshes: THREE.Mesh[] = []
|
||||
root.traverse(child => {
|
||||
if ((child as THREE.Mesh).isMesh) {
|
||||
meshes.push(child as THREE.Mesh)
|
||||
}
|
||||
})
|
||||
|
||||
// 检查是否需要合并
|
||||
if (meshes.length === 0) return
|
||||
if (meshes.length === 1 && root === meshes[0] && root.children.length === 0) return
|
||||
|
||||
const geometries: THREE.BufferGeometry[] = []
|
||||
const materials: THREE.Material[] = []
|
||||
const materialIndexMap = new Map<string, number>()
|
||||
|
||||
// 更新世界矩阵
|
||||
root.updateMatrixWorld(true)
|
||||
const rootInverse = root.matrixWorld.clone().invert()
|
||||
|
||||
// 检查UV属性兼容性
|
||||
const hasUVAttribute = meshes.some(mesh => mesh.geometry.attributes.uv)
|
||||
const allHaveUV = meshes.every(mesh => mesh.geometry.attributes.uv)
|
||||
|
||||
if (hasUVAttribute && !allHaveUV) {
|
||||
console.warn(`节点 ${root.name} 的几何体UV属性不一致,将移除UV属性以确保合并成功`)
|
||||
}
|
||||
|
||||
// 处理每个网格
|
||||
meshes.forEach(mesh => {
|
||||
const geom = mesh.geometry.clone()
|
||||
mesh.updateMatrixWorld(true)
|
||||
const matrix = mesh.matrixWorld.clone().premultiply(rootInverse)
|
||||
geom.applyMatrix4(matrix)
|
||||
|
||||
// 重新计算法线
|
||||
if (geom.attributes.normal) {
|
||||
delete geom.attributes.normal
|
||||
}
|
||||
geom.computeVertexNormals()
|
||||
|
||||
// 处理UV属性兼容性
|
||||
if (hasUVAttribute && !allHaveUV && geom.attributes.uv) {
|
||||
delete geom.attributes.uv
|
||||
}
|
||||
|
||||
const meshMaterials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
|
||||
|
||||
// 处理几何体组
|
||||
if (!geom.groups || geom.groups.length === 0) {
|
||||
geom.clearGroups()
|
||||
if (geom.attributes.position) {
|
||||
geom.addGroup(0, geom.attributes.position.count, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// 重新映射材质索引
|
||||
const newGroups = []
|
||||
for (const group of geom.groups) {
|
||||
const originalMat = meshMaterials[group.materialIndex || 0]
|
||||
if (!originalMat) continue
|
||||
|
||||
const matUuid = originalMat.uuid
|
||||
let newMatIndex = materialIndexMap.get(matUuid)
|
||||
|
||||
if (newMatIndex === undefined) {
|
||||
newMatIndex = materials.length
|
||||
materials.push(originalMat)
|
||||
materialIndexMap.set(matUuid, newMatIndex)
|
||||
}
|
||||
|
||||
newGroups.push({
|
||||
start: group.start,
|
||||
count: group.count,
|
||||
materialIndex: newMatIndex,
|
||||
})
|
||||
}
|
||||
|
||||
geom.groups = newGroups
|
||||
geometries.push(geom)
|
||||
})
|
||||
|
||||
// 执行合并
|
||||
if (geometries.length > 0) {
|
||||
try {
|
||||
const mergedGeometry = BufferGeometryUtils.mergeGeometries(geometries, true)
|
||||
|
||||
// 重新计算法线
|
||||
if (mergedGeometry.attributes.normal) {
|
||||
mergedGeometry.computeVertexNormals()
|
||||
}
|
||||
|
||||
const mergedMesh = new THREE.Mesh(mergedGeometry, materials)
|
||||
mergedMesh.name = root.name
|
||||
mergedMesh.userData = { ...root.userData }
|
||||
|
||||
// 替换原对象
|
||||
const parent = root.parent
|
||||
if (parent) {
|
||||
mergedMesh.position.copy(root.position)
|
||||
mergedMesh.rotation.copy(root.rotation)
|
||||
mergedMesh.scale.copy(root.scale)
|
||||
parent.remove(root)
|
||||
parent.add(mergedMesh)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('合并节点失败:', root.name, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 主钩子函数
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 完整模式组合式函数
|
||||
* @param context - 模型模式上下文
|
||||
*/
|
||||
export function useFullMode(context: ModelModeContext) {
|
||||
const {
|
||||
store,
|
||||
isLoading,
|
||||
archetypeModel,
|
||||
modelGroup,
|
||||
objectMap,
|
||||
modelCache,
|
||||
replaceWithPBRMaterial,
|
||||
generateHierarchy,
|
||||
initEnvironment,
|
||||
applyLightingMode,
|
||||
fitView,
|
||||
findNodeById,
|
||||
rebuildObjectMap,
|
||||
generateBreadcrumbs,
|
||||
} = context
|
||||
|
||||
// ============================================================
|
||||
// 模型加载
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 加载模型
|
||||
* @param url - 模型URL
|
||||
* @param onLoaded - 加载完成回调
|
||||
*/
|
||||
const loadModel = (url: string, onLoaded?: () => void) => {
|
||||
isLoading.value = true
|
||||
store.isModelLoading = true
|
||||
|
||||
const fileName = extractFileName(url)
|
||||
|
||||
// 检查缓存
|
||||
if (store.modelCacheEnabled && modelCache.has(fileName)) {
|
||||
console.log('从缓存加载模型:', fileName)
|
||||
const cachedModel = modelCache.get(fileName)
|
||||
|
||||
// 使用缓存的模型
|
||||
const clonedModel = cachedModel!.clone()
|
||||
replaceWithPBRMaterial(clonedModel)
|
||||
archetypeModel.value = markRaw(clonedModel)
|
||||
|
||||
// 生成层级结构
|
||||
const rootPath = store.currentNodeId
|
||||
const hierarchy = generateHierarchy(archetypeModel.value, rootPath)
|
||||
store.modelHierarchy = [hierarchy]
|
||||
|
||||
initEnvironment()
|
||||
applyLightingMode()
|
||||
|
||||
isLoading.value = false
|
||||
store.isModelLoading = false
|
||||
|
||||
if (onLoaded) onLoaded()
|
||||
return
|
||||
}
|
||||
|
||||
// 创建加载器
|
||||
const loader = new GLTFLoader()
|
||||
const dracoLoader = new DRACOLoader()
|
||||
dracoLoader.setDecoderPath(DRACO_DECODER_PATH)
|
||||
loader.setDRACOLoader(dracoLoader)
|
||||
|
||||
loader.load(
|
||||
url,
|
||||
gltf => {
|
||||
replaceWithPBRMaterial(gltf.scene)
|
||||
|
||||
// 缓存模型
|
||||
if (store.modelCacheEnabled) {
|
||||
modelCache.set(fileName, gltf.scene.clone())
|
||||
console.log('模型已缓存:', fileName)
|
||||
}
|
||||
|
||||
// 设置原型模型
|
||||
archetypeModel.value = markRaw(gltf.scene)
|
||||
|
||||
// 生成层级结构
|
||||
const rootPath = store.currentNodeId
|
||||
const hierarchy = generateHierarchy(archetypeModel.value, rootPath)
|
||||
store.modelHierarchy = [hierarchy]
|
||||
|
||||
initEnvironment()
|
||||
applyLightingMode()
|
||||
|
||||
isLoading.value = false
|
||||
store.isModelLoading = false
|
||||
dracoLoader.dispose()
|
||||
|
||||
if (onLoaded) onLoaded()
|
||||
},
|
||||
undefined,
|
||||
error => {
|
||||
console.error('模型加载错误:', error)
|
||||
ElMessage.error('模型加载失败,请检查网络连接或模型文件是否存在')
|
||||
isLoading.value = false
|
||||
store.isModelLoading = false
|
||||
dracoLoader.dispose()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 模型处理
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 处理模型(核心逻辑)
|
||||
* 合并子节点并更新场景
|
||||
* @param nodeId - 节点ID
|
||||
* @param shouldFitView - 是否适配视图
|
||||
*/
|
||||
const processModel = (nodeId: string, shouldFitView: boolean = false) => {
|
||||
if (!archetypeModel.value) return
|
||||
|
||||
// 克隆模型
|
||||
const newModel = archetypeModel.value.clone()
|
||||
newModel.name = store.currentNodeId || '模型'
|
||||
|
||||
// 查找目标节点
|
||||
let targetNode = findNodeById(newModel, nodeId)
|
||||
if (!targetNode) {
|
||||
console.warn(`节点 ${nodeId} 未找到,回退到根节点`)
|
||||
targetNode = newModel
|
||||
}
|
||||
|
||||
// 合并子节点
|
||||
const childrenToMerge = [...targetNode.children]
|
||||
childrenToMerge.forEach(child => {
|
||||
mergeSubTree(child)
|
||||
})
|
||||
|
||||
// 清空模型组
|
||||
modelGroup.clear()
|
||||
|
||||
// 计算世界变换
|
||||
newModel.updateMatrixWorld(true)
|
||||
const worldPosition = new THREE.Vector3()
|
||||
const worldQuaternion = new THREE.Quaternion()
|
||||
const worldScale = new THREE.Vector3()
|
||||
|
||||
targetNode.getWorldPosition(worldPosition)
|
||||
targetNode.getWorldQuaternion(worldQuaternion)
|
||||
targetNode.getWorldScale(worldScale)
|
||||
|
||||
// 添加到模型组
|
||||
modelGroup.add(targetNode)
|
||||
|
||||
// 应用世界变换
|
||||
targetNode.position.copy(worldPosition)
|
||||
targetNode.quaternion.copy(worldQuaternion)
|
||||
targetNode.scale.copy(worldScale)
|
||||
|
||||
// 重建物体映射
|
||||
objectMap.clear()
|
||||
rebuildObjectMap(targetNode)
|
||||
|
||||
// 更新层级结构
|
||||
const newHierarchy = generateHierarchy(targetNode, targetNode.userData.id)
|
||||
store.modelHierarchy = [newHierarchy]
|
||||
|
||||
// 生成面包屑(返回供调用者处理)
|
||||
const breadcrumbs = generateBreadcrumbs(nodeId)
|
||||
|
||||
// 适配视图
|
||||
if (shouldFitView) {
|
||||
fitView(targetNode)
|
||||
}
|
||||
|
||||
// 调试:打印物体路径
|
||||
printModelObjectPaths(newModel)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 导出接口
|
||||
// ============================================================
|
||||
|
||||
return {
|
||||
/** 加载状态(只读) */
|
||||
isLoading: readonly(isLoading),
|
||||
/** 加载模型 */
|
||||
loadModel,
|
||||
/** 处理模型 */
|
||||
processModel,
|
||||
}
|
||||
}
|
||||
219
app/composables/powerStation/modes/useSimplifiedMode.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* 简化模式 - 模型加载
|
||||
* 按文件名直接加载模型,支持按需加载和自动回退
|
||||
*/
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type * as THREE from 'three'
|
||||
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
||||
import { markRaw } from 'vue'
|
||||
import type { ModelModeContext } from '../types'
|
||||
|
||||
// ============================================================
|
||||
// 配置常量
|
||||
// ============================================================
|
||||
|
||||
/** DRACO解码器路径 */
|
||||
const DRACO_DECODER_PATH = 'https://www.gstatic.com/draco/v1/decoders/'
|
||||
|
||||
/** 简化模式配置 */
|
||||
const SIMPLIFIED_MODE_CONFIG = {
|
||||
/** 层级显示的最大深度 */
|
||||
maxHierarchyDepth: 2,
|
||||
/** 模型文件扩展名 */
|
||||
fileExtension: '.glb',
|
||||
} as const
|
||||
|
||||
// ============================================================
|
||||
// 主钩子函数
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 简化模式组合式函数
|
||||
* @param context - 模型模式上下文
|
||||
*/
|
||||
export function useSimplifiedMode(context: ModelModeContext) {
|
||||
const {
|
||||
store,
|
||||
isLoading,
|
||||
archetypeModel,
|
||||
modelGroup,
|
||||
objectMap,
|
||||
modelCache,
|
||||
replaceWithPBRMaterial,
|
||||
generateHierarchy,
|
||||
initEnvironment,
|
||||
applyLightingMode,
|
||||
fitView,
|
||||
rebuildObjectMap,
|
||||
generateBreadcrumbs,
|
||||
} = context
|
||||
|
||||
// ============================================================
|
||||
// 模型设置
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 从缓存设置模型
|
||||
* @param model - 缓存的模型
|
||||
* @param fileName - 文件名
|
||||
*/
|
||||
const setupModelFromCache = (model: THREE.Object3D, fileName: string) => {
|
||||
const clonedModel = model.clone()
|
||||
const nameWithoutExt = fileName.replace(SIMPLIFIED_MODE_CONFIG.fileExtension, '')
|
||||
|
||||
// 设置模型名称
|
||||
clonedModel.name = nameWithoutExt
|
||||
|
||||
// 更新原型模型
|
||||
archetypeModel.value = markRaw(clonedModel.clone())
|
||||
|
||||
// 清空并添加到模型组
|
||||
modelGroup.clear()
|
||||
modelGroup.add(clonedModel)
|
||||
|
||||
// 生成层级结构(只显示指定深度的节点)
|
||||
let hierarchy = []
|
||||
if (clonedModel.children.length > 0) {
|
||||
// 如果模型有子节点,将每个子节点作为一级节点
|
||||
clonedModel.children.forEach((child, index) => {
|
||||
const childPath = index === 0 ? nameWithoutExt : `${nameWithoutExt}~child${index}`
|
||||
const childHierarchy = generateHierarchy(
|
||||
child,
|
||||
childPath,
|
||||
1,
|
||||
SIMPLIFIED_MODE_CONFIG.maxHierarchyDepth
|
||||
)
|
||||
hierarchy.push(childHierarchy)
|
||||
})
|
||||
} else {
|
||||
// 如果模型没有子节点,使用模型本身作为一级节点
|
||||
const modelHierarchy = generateHierarchy(
|
||||
clonedModel,
|
||||
nameWithoutExt,
|
||||
1,
|
||||
SIMPLIFIED_MODE_CONFIG.maxHierarchyDepth
|
||||
)
|
||||
hierarchy = [modelHierarchy]
|
||||
}
|
||||
|
||||
store.modelHierarchy = hierarchy
|
||||
|
||||
// 生成面包屑(返回供调用者处理)
|
||||
const breadcrumbs = generateBreadcrumbs(nameWithoutExt)
|
||||
|
||||
// 重建物体映射
|
||||
objectMap.clear()
|
||||
rebuildObjectMap(clonedModel)
|
||||
|
||||
// 初始化环境和光照
|
||||
initEnvironment()
|
||||
applyLightingMode()
|
||||
|
||||
// 适配视图
|
||||
fitView(clonedModel)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 模型加载
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 根据命名约定构建模型URL
|
||||
* @param fileName - 文件名
|
||||
* @returns 完整URL路径
|
||||
*/
|
||||
const buildModelUrl = (fileName: string): string => {
|
||||
// 格式: Root~Parent~Child.glb -> /3DModels/Root/Root~Parent~Child.glb
|
||||
const nameWithoutExt = fileName.slice(0, -4)
|
||||
const rootFolder = nameWithoutExt.split('~')[0]
|
||||
return `/3DModels/${rootFolder}/${fileName}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 按文件名加载模型
|
||||
* @param fileName - 文件名(可带或不带.glb扩展名)
|
||||
* @param onLoaded - 加载完成回调
|
||||
*/
|
||||
const loadModelByFileName = (fileName: string, onLoaded?: () => void) => {
|
||||
// 确保有.glb扩展名
|
||||
if (!fileName.endsWith(SIMPLIFIED_MODE_CONFIG.fileExtension)) {
|
||||
fileName += SIMPLIFIED_MODE_CONFIG.fileExtension
|
||||
}
|
||||
|
||||
// 检查缓存
|
||||
if (store.modelCacheEnabled && modelCache.has(fileName)) {
|
||||
const cached = modelCache.get(fileName)!
|
||||
setupModelFromCache(cached, fileName)
|
||||
if (onLoaded) onLoaded()
|
||||
return
|
||||
}
|
||||
|
||||
// 开始加载
|
||||
isLoading.value = true
|
||||
store.isModelLoading = true
|
||||
|
||||
const url = buildModelUrl(fileName)
|
||||
console.log(`从 ${url} 加载模型`)
|
||||
|
||||
// 创建加载器
|
||||
const loader = new GLTFLoader()
|
||||
const dracoLoader = new DRACOLoader()
|
||||
dracoLoader.setDecoderPath(DRACO_DECODER_PATH)
|
||||
loader.setDRACOLoader(dracoLoader)
|
||||
|
||||
loader.load(
|
||||
url,
|
||||
gltf => {
|
||||
const rawModel = gltf.scene
|
||||
replaceWithPBRMaterial(rawModel)
|
||||
|
||||
// 缓存模型
|
||||
if (store.modelCacheEnabled) {
|
||||
modelCache.set(fileName, rawModel.clone())
|
||||
}
|
||||
|
||||
// 设置模型
|
||||
setupModelFromCache(rawModel, fileName)
|
||||
|
||||
isLoading.value = false
|
||||
store.isModelLoading = false
|
||||
dracoLoader.dispose()
|
||||
|
||||
if (onLoaded) onLoaded()
|
||||
},
|
||||
undefined,
|
||||
error => {
|
||||
console.warn('加载模型失败:', fileName, error)
|
||||
ElMessage.error(`模型加载失败: ${fileName},请检查网络连接或模型文件是否存在`)
|
||||
|
||||
// 回退逻辑:尝试加载父级模型
|
||||
const nameWithoutExt = fileName.slice(0, -4)
|
||||
const parts = nameWithoutExt.split('~')
|
||||
|
||||
if (parts.length > 1) {
|
||||
// 移除最后一级,尝试加载父级
|
||||
parts.pop()
|
||||
const parentName = parts.join('~')
|
||||
console.log('回退到父级:', parentName)
|
||||
loadModelByFileName(parentName, onLoaded)
|
||||
} else {
|
||||
// 没有父级可回退
|
||||
console.error('没有父级可以回退,或根节点加载失败。')
|
||||
isLoading.value = false
|
||||
store.isModelLoading = false
|
||||
dracoLoader.dispose()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 导出接口
|
||||
// ============================================================
|
||||
|
||||
return {
|
||||
/** 按文件名加载模型 */
|
||||
loadModelByFileName,
|
||||
}
|
||||
}
|
||||
156
app/composables/powerStation/types.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* PowerStation 模块的共享类型定义
|
||||
* 用于 Three.js 场景、模型管理和交互系统
|
||||
*/
|
||||
|
||||
import type * as THREE from 'three'
|
||||
import type { ShallowRef } from 'vue'
|
||||
|
||||
// ============================================================
|
||||
// 交互系统类型
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 交互状态
|
||||
*/
|
||||
export interface InteractionState {
|
||||
/** 当前悬停的对象 */
|
||||
hoveredObject: THREE.Object3D | null
|
||||
/** 当前选中的对象 */
|
||||
selectedObject: THREE.Object3D | null
|
||||
/** 是否正在拖拽 */
|
||||
isDragging: boolean
|
||||
/** 拖拽起始位置 */
|
||||
dragStart: THREE.Vector2
|
||||
/** 拖拽阈值 */
|
||||
dragThreshold: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 交互回调函数
|
||||
*/
|
||||
export interface InteractionCallbacks {
|
||||
onHover?: (object: THREE.Object3D | null) => void
|
||||
onSelect?: (object: THREE.Object3D | null) => void
|
||||
onDoubleClick?: (object: THREE.Object3D) => void
|
||||
onDragStart?: (object: THREE.Object3D, point: THREE.Vector3) => void
|
||||
onDragMove?: (object: THREE.Object3D, point: THREE.Vector3) => void
|
||||
onDragEnd?: (object: THREE.Object3D) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 高亮配置
|
||||
*/
|
||||
export interface HighlightConfig {
|
||||
/** 高亮颜色 */
|
||||
color: THREE.Color
|
||||
/** 高亮强度 */
|
||||
intensity: number
|
||||
/** 动画持续时间(毫秒) */
|
||||
duration: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 射线检测配置
|
||||
*/
|
||||
export interface RaycasterConfig {
|
||||
/** 是否递归检测子节点 */
|
||||
recursive: boolean
|
||||
/** 检测阈值 */
|
||||
threshold: number
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 模型管理系统类型
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 模型层级节点
|
||||
*/
|
||||
export interface ModelHierarchyNode {
|
||||
/** 节点ID */
|
||||
id: string
|
||||
/** 节点标签 */
|
||||
label: string
|
||||
/** 子节点 */
|
||||
children: ModelHierarchyNode[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 面包屑导航项
|
||||
*/
|
||||
export interface BreadcrumbItem {
|
||||
/** 显示文本 */
|
||||
text: string
|
||||
/** 跳转链接 */
|
||||
to?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 模型加载模式上下文
|
||||
* 用于在不同加载模式间共享数据和方法
|
||||
*/
|
||||
export interface ModelModeContext {
|
||||
/** 状态存储 */
|
||||
store: any
|
||||
/** 加载状态 */
|
||||
isLoading: globalThis.Ref<boolean>
|
||||
/** 原型模型 */
|
||||
archetypeModel: ShallowRef<THREE.Object3D | null>
|
||||
/** 模型组 */
|
||||
modelGroup: THREE.Group
|
||||
/** 物体ID映射 */
|
||||
objectMap: Map<string, THREE.Object3D>
|
||||
/** 模型缓存 */
|
||||
modelCache: Map<string, THREE.Object3D>
|
||||
|
||||
// 方法
|
||||
/** 替换为PBR材质 */
|
||||
replaceWithPBRMaterial: (object: THREE.Object3D) => void
|
||||
/** 生成层级结构 */
|
||||
generateHierarchy: (
|
||||
object: THREE.Object3D,
|
||||
currentPath: string,
|
||||
depth?: number,
|
||||
maxDepth?: number
|
||||
) => ModelHierarchyNode
|
||||
/** 初始化环境 */
|
||||
initEnvironment: () => void
|
||||
/** 应用光照模式 */
|
||||
applyLightingMode: () => void
|
||||
/** 适配视图 */
|
||||
fitView: (targetObj?: THREE.Object3D, keepOrientation?: boolean) => void
|
||||
/** 通过ID查找节点 */
|
||||
findNodeById: (root: THREE.Object3D, id: string) => THREE.Object3D | null
|
||||
/** 重建物体映射 */
|
||||
rebuildObjectMap: (root: THREE.Object3D) => void
|
||||
/** 生成面包屑 */
|
||||
generateBreadcrumbs: (nodeId: string) => BreadcrumbItem[]
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 视图控制类型
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 标准视图类型
|
||||
*/
|
||||
export type StandardViewType = 'top' | 'bottom' | 'front' | 'back' | 'left' | 'right' | 'iso'
|
||||
|
||||
/**
|
||||
* 视图旋转方向
|
||||
*/
|
||||
export type RotateDirection =
|
||||
| 'up'
|
||||
| 'down'
|
||||
| 'left'
|
||||
| 'right'
|
||||
| 'upleft'
|
||||
| 'upright'
|
||||
| 'downleft'
|
||||
| 'downright'
|
||||
|
||||
/**
|
||||
* 光照模式
|
||||
*/
|
||||
export type LightingMode = 'basic' | 'advanced'
|
||||
408
app/composables/powerStation/useInteraction.ts
Normal file
@@ -0,0 +1,408 @@
|
||||
/**
|
||||
* 三维场景交互系统
|
||||
* 处理鼠标悬停、点击、双击等交互事件
|
||||
* 提供物体高亮和轮廓效果
|
||||
*/
|
||||
import { navigateTo } from 'nuxt/app'
|
||||
import * as THREE from 'three'
|
||||
import type { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
|
||||
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass.js'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { usePowerStationStore } from '~/stores/powerStation'
|
||||
import type { HighlightConfig, RaycasterConfig } from './types'
|
||||
|
||||
// ============================================================
|
||||
// 配置常量
|
||||
// ============================================================
|
||||
|
||||
/** 悬停轮廓颜色 */
|
||||
const HOVER_OUTLINE_COLOR = '#435c9d'
|
||||
|
||||
/** 点击轮廓颜色 */
|
||||
const CLICK_OUTLINE_COLOR = '#9d7e43'
|
||||
|
||||
/** 轮廓通道配置 */
|
||||
const OUTLINE_PASS_CONFIG = {
|
||||
edgeStrength: 2,
|
||||
edgeGlow: 1,
|
||||
edgeThickness: 1,
|
||||
} as const
|
||||
|
||||
/** 默认高亮配置 */
|
||||
const DEFAULT_HIGHLIGHT_CONFIG: HighlightConfig = {
|
||||
color: new THREE.Color(0x00ff00),
|
||||
intensity: 0.5,
|
||||
duration: 200,
|
||||
}
|
||||
|
||||
/** 默认射线检测配置 */
|
||||
const DEFAULT_RAYCASTER_CONFIG: RaycasterConfig = {
|
||||
recursive: true,
|
||||
threshold: 0.1,
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 材质管理
|
||||
// ============================================================
|
||||
|
||||
/** 存储原始材质用于恢复 */
|
||||
const originalMaterials = new Map<string, THREE.Material | THREE.Material[]>()
|
||||
|
||||
/**
|
||||
* 高亮物体(使用线框材质)
|
||||
* @param object - 要高亮的物体
|
||||
* @param config - 高亮配置
|
||||
*/
|
||||
const highlightObject = (
|
||||
object: THREE.Object3D,
|
||||
config: HighlightConfig = DEFAULT_HIGHLIGHT_CONFIG
|
||||
) => {
|
||||
object.traverse(child => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
const uuid = child.uuid
|
||||
|
||||
// 存储原始材质
|
||||
if (!originalMaterials.has(uuid)) {
|
||||
originalMaterials.set(uuid, child.material)
|
||||
}
|
||||
|
||||
// 创建高亮材质
|
||||
const highlightMaterial = new THREE.MeshBasicMaterial({
|
||||
color: config.color,
|
||||
transparent: true,
|
||||
opacity: config.intensity,
|
||||
wireframe: true,
|
||||
})
|
||||
|
||||
child.material = highlightMaterial
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消高亮物体(恢复原始材质)
|
||||
* @param object - 要取消高亮的物体
|
||||
*/
|
||||
const unhighlightObject = (object: THREE.Object3D) => {
|
||||
object.traverse(child => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
const uuid = child.uuid
|
||||
const originalMaterial = originalMaterials.get(uuid)
|
||||
|
||||
if (originalMaterial) {
|
||||
child.material = originalMaterial
|
||||
originalMaterials.delete(uuid)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 主交互钩子
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 交互系统组合式函数
|
||||
* @param getScene - 获取场景
|
||||
* @param getCamera - 获取相机
|
||||
* @param getComposer - 获取后期处理合成器
|
||||
* @param getContainer - 获取容器元素
|
||||
* @param modelGroup - 模型组
|
||||
* @param getArchetypeModel - 获取原型模型
|
||||
*/
|
||||
export function useInteraction(
|
||||
getScene: () => THREE.Scene,
|
||||
getCamera: () => THREE.PerspectiveCamera,
|
||||
getComposer: () => EffectComposer,
|
||||
getContainer: () => HTMLElement | undefined,
|
||||
modelGroup: THREE.Group,
|
||||
getArchetypeModel: () => THREE.Object3D | null
|
||||
) {
|
||||
const store = usePowerStationStore()
|
||||
const route = useRoute()
|
||||
|
||||
// 射线检测器
|
||||
const raycaster = new THREE.Raycaster()
|
||||
const mouse = new THREE.Vector2()
|
||||
|
||||
// 轮廓通道
|
||||
let hoverOutlinePass: OutlinePass
|
||||
let clickOutlinePass: OutlinePass
|
||||
|
||||
// 当前交互状态
|
||||
let currentHoverObject: THREE.Object3D | null = null
|
||||
let currentClickObject: THREE.Object3D | null = null
|
||||
|
||||
// ============================================================
|
||||
// 射线检测
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 获取鼠标与场景物体的交点
|
||||
* @param event - 鼠标事件
|
||||
* @returns 交点数组
|
||||
*/
|
||||
const getIntersects = (event: MouseEvent): THREE.Intersection[] => {
|
||||
const container = getContainer()
|
||||
const camera = getCamera()
|
||||
|
||||
if (!container || !camera) return []
|
||||
|
||||
const rect = container.getBoundingClientRect()
|
||||
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
|
||||
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
|
||||
|
||||
raycaster.setFromCamera(mouse, camera)
|
||||
|
||||
return raycaster.intersectObjects(modelGroup.children, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找具有用户数据ID的父级物体
|
||||
* @param object - 起始物体
|
||||
* @returns 具有ID的物体或null
|
||||
*/
|
||||
const findObjectWithId = (object: THREE.Object3D): THREE.Object3D | null => {
|
||||
let target = object
|
||||
|
||||
while (target && !target.userData.id && target !== modelGroup) {
|
||||
if (target.parent) {
|
||||
target = target.parent
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return target?.userData.id ? target : null
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 高亮效果
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 高亮悬停物体
|
||||
* @param object - 要高亮的物体(null清除高亮)
|
||||
*/
|
||||
const highlightHover = (object: THREE.Object3D | null) => {
|
||||
if (!hoverOutlinePass) return
|
||||
|
||||
if (object && object !== currentClickObject) {
|
||||
if (currentHoverObject !== object) {
|
||||
hoverOutlinePass.selectedObjects = [object]
|
||||
currentHoverObject = object
|
||||
}
|
||||
} else {
|
||||
hoverOutlinePass.selectedObjects = []
|
||||
currentHoverObject = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 高亮点击物体
|
||||
* @param object - 要高亮的物体(null清除高亮)
|
||||
*/
|
||||
const highlightClick = (object: THREE.Object3D | null) => {
|
||||
if (!clickOutlinePass) return
|
||||
|
||||
if (object) {
|
||||
// 清除相同物体的悬停高亮
|
||||
if (currentHoverObject === object) {
|
||||
hoverOutlinePass.selectedObjects = []
|
||||
currentHoverObject = null
|
||||
}
|
||||
|
||||
clickOutlinePass.selectedObjects = [object]
|
||||
currentClickObject = object
|
||||
} else {
|
||||
clickOutlinePass.selectedObjects = []
|
||||
currentClickObject = null
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 事件处理
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 点击事件处理
|
||||
*/
|
||||
const onClick = (event: MouseEvent) => {
|
||||
const intersects = getIntersects(event)
|
||||
|
||||
if (intersects.length > 0 && intersects[0]) {
|
||||
const target = findObjectWithId(intersects[0].object)
|
||||
|
||||
if (target && target.userData.id) {
|
||||
const id = target.userData.id
|
||||
const currentRouteNodeId = route.query.currentNodeId as string
|
||||
const shouldHighlight = id !== currentRouteNodeId
|
||||
|
||||
// 更新store中的选中节点
|
||||
store.selectNode({ id, label: target.name || `节点 ${id}` })
|
||||
|
||||
// 只有ID不一致时才高亮
|
||||
if (shouldHighlight) {
|
||||
highlightClick(target)
|
||||
} else {
|
||||
highlightClick(null)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 点击空白区域,恢复到路由参数值
|
||||
const currentRouteNodeId = route.query.currentNodeId as string
|
||||
if (currentRouteNodeId) {
|
||||
store.selectNode({ id: currentRouteNodeId, label: currentRouteNodeId })
|
||||
} else {
|
||||
store.selectNode({ id: '' })
|
||||
}
|
||||
highlightClick(null)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 双击事件处理
|
||||
*/
|
||||
const onDoubleClick = async (event: MouseEvent) => {
|
||||
const intersects = getIntersects(event)
|
||||
|
||||
if (intersects.length > 0 && intersects[0]) {
|
||||
const target = findObjectWithId(intersects[0].object)
|
||||
|
||||
if (target && target.userData.id) {
|
||||
const id = target.userData.id
|
||||
await navigateTo({
|
||||
query: {
|
||||
...route.query,
|
||||
currentNodeId: id,
|
||||
},
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 双击空白区域,返回父级
|
||||
await handleNavigateToParent()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导航到父级节点
|
||||
*/
|
||||
const handleNavigateToParent = async () => {
|
||||
const currentRouteNodeId = route.query.currentNodeId as string
|
||||
|
||||
// 如果是根节点,不操作
|
||||
if (!currentRouteNodeId || !currentRouteNodeId.includes('~')) {
|
||||
return
|
||||
}
|
||||
|
||||
// 删除最后一个"~"之后的内容
|
||||
const lastUnderlineIndex = currentRouteNodeId.lastIndexOf('~')
|
||||
if (lastUnderlineIndex > 0) {
|
||||
const newCurrentNodeId = currentRouteNodeId.substring(0, lastUnderlineIndex)
|
||||
|
||||
await navigateTo({
|
||||
query: {
|
||||
...route.query,
|
||||
currentNodeId: newCurrentNodeId,
|
||||
},
|
||||
})
|
||||
|
||||
store.selectNode({ id: newCurrentNodeId, label: newCurrentNodeId })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 鼠标移动事件处理
|
||||
*/
|
||||
const onMouseMove = (event: MouseEvent) => {
|
||||
const intersects = getIntersects(event)
|
||||
|
||||
if (intersects.length > 0 && intersects[0]) {
|
||||
const target = findObjectWithId(intersects[0].object)
|
||||
highlightHover(target)
|
||||
} else {
|
||||
highlightHover(null)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 初始化与清理
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 初始化交互系统
|
||||
*/
|
||||
const initInteraction = () => {
|
||||
const scene = getScene()
|
||||
const camera = getCamera()
|
||||
const composer = getComposer()
|
||||
const container = getContainer()
|
||||
|
||||
if (!scene || !camera || !composer || !container) return
|
||||
|
||||
const width = container.clientWidth
|
||||
const height = container.clientHeight
|
||||
|
||||
// 创建悬停轮廓通道
|
||||
hoverOutlinePass = new OutlinePass(new THREE.Vector2(width, height), scene, camera)
|
||||
hoverOutlinePass.edgeStrength = OUTLINE_PASS_CONFIG.edgeStrength
|
||||
hoverOutlinePass.edgeGlow = OUTLINE_PASS_CONFIG.edgeGlow
|
||||
hoverOutlinePass.edgeThickness = OUTLINE_PASS_CONFIG.edgeThickness
|
||||
hoverOutlinePass.visibleEdgeColor.set(HOVER_OUTLINE_COLOR)
|
||||
hoverOutlinePass.hiddenEdgeColor.set(HOVER_OUTLINE_COLOR)
|
||||
composer.addPass(hoverOutlinePass)
|
||||
|
||||
// 创建点击轮廓通道
|
||||
clickOutlinePass = new OutlinePass(new THREE.Vector2(width, height), scene, camera)
|
||||
clickOutlinePass.edgeStrength = OUTLINE_PASS_CONFIG.edgeStrength
|
||||
clickOutlinePass.edgeGlow = OUTLINE_PASS_CONFIG.edgeGlow
|
||||
clickOutlinePass.edgeThickness = OUTLINE_PASS_CONFIG.edgeThickness
|
||||
clickOutlinePass.visibleEdgeColor.set(CLICK_OUTLINE_COLOR)
|
||||
clickOutlinePass.hiddenEdgeColor.set(CLICK_OUTLINE_COLOR)
|
||||
composer.addPass(clickOutlinePass)
|
||||
|
||||
// 绑定事件
|
||||
container.addEventListener('click', onClick)
|
||||
container.addEventListener('dblclick', onDoubleClick)
|
||||
container.addEventListener('mousemove', onMouseMove)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新轮廓通道尺寸
|
||||
* @param width - 新宽度
|
||||
* @param height - 新高度
|
||||
*/
|
||||
const updatePassSize = (width: number, height: number) => {
|
||||
if (hoverOutlinePass) hoverOutlinePass.setSize(width, height)
|
||||
if (clickOutlinePass) clickOutlinePass.setSize(width, height)
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
const dispose = () => {
|
||||
const container = getContainer()
|
||||
if (container) {
|
||||
container.removeEventListener('click', onClick)
|
||||
container.removeEventListener('dblclick', onDoubleClick)
|
||||
container.removeEventListener('mousemove', onMouseMove)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 导出接口
|
||||
// ============================================================
|
||||
|
||||
return {
|
||||
/** 初始化交互系统 */
|
||||
initInteraction,
|
||||
/** 高亮点击物体 */
|
||||
highlightClick,
|
||||
/** 高亮悬停物体 */
|
||||
highlightHover,
|
||||
/** 更新轮廓通道尺寸 */
|
||||
updatePassSize,
|
||||
/** 清理资源 */
|
||||
dispose,
|
||||
}
|
||||
}
|
||||
551
app/composables/powerStation/useModelManager.ts
Normal file
@@ -0,0 +1,551 @@
|
||||
/**
|
||||
* 模型管理系统
|
||||
* 处理3D模型的加载、层级结构生成、材质处理和相机适配
|
||||
*/
|
||||
import * as TWEEN from '@tweenjs/tween.js'
|
||||
import * as THREE from 'three'
|
||||
import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js'
|
||||
import { usePowerStationStore } from '~/stores/powerStation'
|
||||
import { useFullMode } from './modes/useFullMode'
|
||||
import { useSimplifiedMode } from './modes/useSimplifiedMode'
|
||||
import type { BreadcrumbItem, LightingMode, ModelHierarchyNode } from './types'
|
||||
|
||||
// ============================================================
|
||||
// 配置常量
|
||||
// ============================================================
|
||||
|
||||
/** 光源配置 */
|
||||
const LIGHT_CONFIG = {
|
||||
/** 环境光颜色 */
|
||||
ambientColor: 0xffffff,
|
||||
/** 环境光强度 */
|
||||
ambientIntensity: 0.6,
|
||||
/** 方向光颜色 */
|
||||
directionalColor: 0xffffff,
|
||||
/** 方向光强度 */
|
||||
directionalIntensity: 1,
|
||||
/** 方向光位置 */
|
||||
directionalPosition: new THREE.Vector3(5, 3, 5),
|
||||
} as const
|
||||
|
||||
/** PBR材质配置 */
|
||||
const PBR_MATERIAL_CONFIG = {
|
||||
/** 金属度 */
|
||||
metalness: 0.6,
|
||||
/** 粗糙度 */
|
||||
roughness: 0.6,
|
||||
/** 环境贴图强度 */
|
||||
envMapIntensity: 1.5,
|
||||
} as const
|
||||
|
||||
/** 相机动画配置 */
|
||||
const CAMERA_ANIMATION_CONFIG = {
|
||||
/** 动画时长(毫秒) */
|
||||
duration: 900,
|
||||
/** 距离系数 */
|
||||
distanceMultiplier: 1.2,
|
||||
} as const
|
||||
|
||||
// ============================================================
|
||||
// 主钩子函数
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 模型管理组合式函数
|
||||
* @param getScene - 获取场景
|
||||
* @param getCamera - 获取相机
|
||||
* @param getControls - 获取控制器
|
||||
* @param getRenderer - 获取渲染器
|
||||
*/
|
||||
export function useModelManager(
|
||||
getScene: () => THREE.Scene,
|
||||
getCamera: () => THREE.PerspectiveCamera,
|
||||
getControls: () => any, // OrbitControls
|
||||
getRenderer: () => THREE.WebGLRenderer
|
||||
) {
|
||||
const store = usePowerStationStore()
|
||||
const isLoading = ref(true)
|
||||
|
||||
// ============================================================
|
||||
// 模型数据
|
||||
// ============================================================
|
||||
|
||||
// 使用 markRaw 标记 Three.js 对象为非响应式,防止 Vue DevTools 追踪导致性能问题
|
||||
const modelGroup = markRaw(new THREE.Group())
|
||||
const archetypeModel = shallowRef<THREE.Object3D | null>(null)
|
||||
|
||||
// 使用普通 Map(Three.js 对象已是 markRaw,Map 本身不需要响应式)
|
||||
const objectMap = new Map<string, THREE.Object3D>()
|
||||
const modelCache = new Map<string, THREE.Object3D>()
|
||||
|
||||
// 环境贴图
|
||||
let envTexture: THREE.Texture | null = null
|
||||
|
||||
// 光照模式
|
||||
const lightingMode = ref<LightingMode>('basic')
|
||||
|
||||
// ============================================================
|
||||
// 初始化
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 初始化模型系统
|
||||
* 添加模型组和基础光源到场景
|
||||
*/
|
||||
const initModelSystem = () => {
|
||||
const scene = getScene()
|
||||
if (!scene) return
|
||||
|
||||
// 添加模型组
|
||||
scene.add(modelGroup)
|
||||
|
||||
// 添加基础光源
|
||||
const ambientLight = new THREE.AmbientLight(
|
||||
LIGHT_CONFIG.ambientColor,
|
||||
LIGHT_CONFIG.ambientIntensity
|
||||
)
|
||||
scene.add(ambientLight)
|
||||
|
||||
const dirLight = new THREE.DirectionalLight(
|
||||
LIGHT_CONFIG.directionalColor,
|
||||
LIGHT_CONFIG.directionalIntensity
|
||||
)
|
||||
dirLight.position.copy(LIGHT_CONFIG.directionalPosition)
|
||||
scene.add(dirLight)
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化环境贴图
|
||||
*/
|
||||
const initEnvironment = () => {
|
||||
const renderer = getRenderer()
|
||||
const scene = getScene()
|
||||
if (!renderer || !scene) return
|
||||
|
||||
// 避免重复创建
|
||||
if (envTexture) return
|
||||
|
||||
const pmremGenerator = new THREE.PMREMGenerator(renderer)
|
||||
pmremGenerator.compileEquirectangularShader()
|
||||
|
||||
const roomEnvironment = new RoomEnvironment()
|
||||
envTexture = pmremGenerator.fromScene(roomEnvironment).texture
|
||||
|
||||
if (lightingMode.value === 'advanced') {
|
||||
scene.environment = envTexture
|
||||
}
|
||||
|
||||
// 清理临时对象
|
||||
roomEnvironment.dispose()
|
||||
pmremGenerator.dispose()
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 层级结构生成
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 生成模型层级结构
|
||||
* @param object - Three.js 对象
|
||||
* @param currentPath - 当前路径
|
||||
* @param depth - 当前深度
|
||||
* @param maxDepth - 最大深度
|
||||
* @returns 层级节点
|
||||
*/
|
||||
const generateHierarchy = (
|
||||
object: THREE.Object3D,
|
||||
currentPath: string,
|
||||
depth: number = 1,
|
||||
maxDepth: number = Infinity
|
||||
): ModelHierarchyNode => {
|
||||
const isRoot = object === archetypeModel.value
|
||||
const nodeId = isRoot ? store.currentNodeId || currentPath : currentPath
|
||||
|
||||
// 设置 userData.id
|
||||
object.userData.id = nodeId
|
||||
|
||||
const node: ModelHierarchyNode = {
|
||||
id: nodeId,
|
||||
label: isRoot
|
||||
? store.currentNodeId || object.name || `节点 ${nodeId}`
|
||||
: object.name || `节点 ${nodeId}`,
|
||||
children: [],
|
||||
}
|
||||
|
||||
// 检查是否为完整物体(有名称且包含多个子Mesh)
|
||||
const isCompleteObject =
|
||||
object.name && object.children.filter(child => child.type === 'Mesh').length > 1
|
||||
|
||||
// 达到最大深度则停止递归
|
||||
if (depth >= maxDepth) {
|
||||
return node
|
||||
}
|
||||
|
||||
// 递归处理子节点
|
||||
object.children.forEach(child => {
|
||||
if (child.type === 'Mesh' || child.type === 'Group' || child.type === 'Object3D') {
|
||||
const childName = child.name || '子对象'
|
||||
const childPath = `${currentPath}~${childName}`
|
||||
|
||||
// 跳过完整物体中的无名Mesh子节点
|
||||
if (isCompleteObject && child.type === 'Mesh' && !child.name) {
|
||||
return
|
||||
}
|
||||
|
||||
const childNode = generateHierarchy(child, childPath, depth + 1, maxDepth)
|
||||
node.children.push(childNode)
|
||||
}
|
||||
})
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 节点查找
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 通过ID查找节点
|
||||
* @param root - 根对象
|
||||
* @param id - 节点ID
|
||||
* @returns 找到的节点或null
|
||||
*/
|
||||
const findNodeById = (root: THREE.Object3D, id: string): THREE.Object3D | null => {
|
||||
if (root.userData.id === id) return root
|
||||
|
||||
for (const child of root.children) {
|
||||
const found = findNodeById(child, id)
|
||||
if (found) return found
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 重建物体映射表
|
||||
* @param root - 根对象
|
||||
*/
|
||||
const rebuildObjectMap = (root: THREE.Object3D) => {
|
||||
if (root.userData.id) {
|
||||
objectMap.set(root.userData.id, root)
|
||||
}
|
||||
root.children.forEach(child => rebuildObjectMap(child))
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 面包屑生成
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 生成面包屑导航数据
|
||||
* @param nodeId - 节点ID
|
||||
* @returns 面包屑数组
|
||||
*/
|
||||
const generateBreadcrumbs = (nodeId: string): BreadcrumbItem[] => {
|
||||
if (!archetypeModel.value) return []
|
||||
|
||||
const breadcrumbsList: BreadcrumbItem[] = []
|
||||
const nodeIdParts = nodeId.split('~')
|
||||
|
||||
if (nodeIdParts.length > 0) {
|
||||
let currentPath = ''
|
||||
|
||||
for (let i = 0; i < nodeIdParts.length; i++) {
|
||||
const part = nodeIdParts[i]
|
||||
currentPath = i === 0 ? part : `${currentPath}~${part}`
|
||||
|
||||
const currentObject = findNodeById(archetypeModel.value, currentPath)
|
||||
const displayText = currentObject
|
||||
? currentObject.name
|
||||
: part || findNodeById(archetypeModel.value, currentPath)?.name || part
|
||||
|
||||
const item: BreadcrumbItem = { text: displayText }
|
||||
|
||||
if (currentPath !== nodeId) {
|
||||
item.to = `?currentNodeId=${currentPath}`
|
||||
}
|
||||
|
||||
breadcrumbsList.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
// 使用当前节点ID的第一部分作为面包屑首项
|
||||
if (breadcrumbsList.length > 0 && store.currentNodeId) {
|
||||
const firstPart = store.currentNodeId.split('~')[0]
|
||||
breadcrumbsList[0].text = firstPart
|
||||
}
|
||||
|
||||
return breadcrumbsList
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 相机适配
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 适配视图到目标对象
|
||||
* @param targetObj - 目标对象(默认为modelGroup)
|
||||
* @param keepOrientation - 是否保持当前相机朝向
|
||||
*/
|
||||
const fitView = (targetObj?: THREE.Object3D, keepOrientation: boolean = true) => {
|
||||
const obj = targetObj || modelGroup
|
||||
const camera = getCamera()
|
||||
const controls = getControls()
|
||||
|
||||
console.log('[fitView] 开始执行:', {
|
||||
hasTargetObj: !!targetObj,
|
||||
objName: obj?.name,
|
||||
objChildrenCount: obj?.children?.length,
|
||||
hasCamera: !!camera,
|
||||
hasControls: !!controls,
|
||||
})
|
||||
|
||||
if (!obj || !camera || !controls) {
|
||||
console.warn('[fitView] 提前返回: obj/camera/controls 缺失')
|
||||
return
|
||||
}
|
||||
|
||||
// 计算包围盒
|
||||
const box = new THREE.Box3().setFromObject(obj)
|
||||
console.log('[fitView] 包围盒:', {
|
||||
isEmpty: box.isEmpty(),
|
||||
min: box.min.toArray(),
|
||||
max: box.max.toArray(),
|
||||
})
|
||||
|
||||
if (box.isEmpty()) {
|
||||
console.warn('[fitView] 提前返回: 包围盒为空')
|
||||
return
|
||||
}
|
||||
|
||||
const size = box.getSize(new THREE.Vector3())
|
||||
const center = box.getCenter(new THREE.Vector3())
|
||||
|
||||
// 计算适配距离
|
||||
const maxSize = Math.max(size.x, size.y, size.z)
|
||||
const fitHeightDistance = maxSize / (2 * Math.atan((Math.PI * camera.fov) / 360))
|
||||
const fitWidthDistance = fitHeightDistance / camera.aspect
|
||||
const distance = CAMERA_ANIMATION_CONFIG.distanceMultiplier * Math.max(fitHeightDistance, fitWidthDistance)
|
||||
|
||||
// 计算相机方向
|
||||
let direction: THREE.Vector3
|
||||
if (keepOrientation) {
|
||||
direction = new THREE.Vector3().subVectors(camera.position, controls.target).normalize()
|
||||
if (direction.lengthSq() < 0.0001) {
|
||||
direction = new THREE.Vector3(1, 1, 1).normalize()
|
||||
}
|
||||
} else {
|
||||
direction = new THREE.Vector3(1, 1, 1).normalize()
|
||||
}
|
||||
|
||||
const targetPosition = center.clone().add(direction.multiplyScalar(distance))
|
||||
|
||||
console.log('[fitView] 动画参数:', {
|
||||
size: size.toArray(),
|
||||
center: center.toArray(),
|
||||
distance,
|
||||
currentCameraPos: camera.position.toArray(),
|
||||
targetCameraPos: targetPosition.toArray(),
|
||||
})
|
||||
|
||||
// 相机位置动画
|
||||
new TWEEN.Tween(camera.position)
|
||||
.to({ x: targetPosition.x, y: targetPosition.y, z: targetPosition.z }, CAMERA_ANIMATION_CONFIG.duration)
|
||||
.easing(TWEEN.Easing.Quadratic.InOut)
|
||||
.onStart(() => console.log('[fitView] 相机位置动画开始'))
|
||||
.onComplete(() => console.log('[fitView] 相机位置动画完成'))
|
||||
.start()
|
||||
|
||||
// 控制目标动画
|
||||
new TWEEN.Tween(controls.target)
|
||||
.to({ x: center.x, y: center.y, z: center.z }, CAMERA_ANIMATION_CONFIG.duration)
|
||||
.easing(TWEEN.Easing.Quadratic.InOut)
|
||||
.onStart(() => console.log('[fitView] 控制目标动画开始'))
|
||||
.onUpdate(() => {
|
||||
controls.update()
|
||||
})
|
||||
.onComplete(() => console.log('[fitView] 控制目标动画完成'))
|
||||
.start()
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 材质处理
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 替换为PBR材质
|
||||
* @param object - 要处理的对象
|
||||
*/
|
||||
const replaceWithPBRMaterial = (object: THREE.Object3D) => {
|
||||
object.traverse(child => {
|
||||
if ((child as THREE.Mesh).isMesh) {
|
||||
const mesh = child as THREE.Mesh
|
||||
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
|
||||
const newMaterials: THREE.MeshStandardMaterial[] = []
|
||||
|
||||
materials.forEach(mat => {
|
||||
const color =
|
||||
(mat as THREE.MeshBasicMaterial).color instanceof THREE.Color
|
||||
? (mat as THREE.MeshBasicMaterial).color.clone()
|
||||
: new THREE.Color(0xaaaaaa)
|
||||
|
||||
// 获取原始贴图
|
||||
const originalMap = (mat as THREE.MeshBasicMaterial).map || null
|
||||
const alphaMap = originalMap || null
|
||||
const useAlpha = originalMap !== null
|
||||
|
||||
const pbrMaterial = new THREE.MeshStandardMaterial({
|
||||
color: color,
|
||||
metalness: PBR_MATERIAL_CONFIG.metalness,
|
||||
roughness: PBR_MATERIAL_CONFIG.roughness,
|
||||
transparent: useAlpha || ((mat as THREE.MeshBasicMaterial).transparent ?? false),
|
||||
opacity: useAlpha ? 1 : ((mat as THREE.MeshBasicMaterial).opacity ?? 1),
|
||||
side: mat.side || THREE.FrontSide,
|
||||
map: originalMap,
|
||||
alphaMap: alphaMap,
|
||||
normalMap: (mat as THREE.MeshStandardMaterial).normalMap || null,
|
||||
envMapIntensity: PBR_MATERIAL_CONFIG.envMapIntensity,
|
||||
})
|
||||
|
||||
newMaterials.push(pbrMaterial)
|
||||
})
|
||||
|
||||
if (newMaterials.length > 0) {
|
||||
mesh.material = (Array.isArray(mesh.material) ? newMaterials : newMaterials[0]) as
|
||||
| THREE.Material
|
||||
| THREE.Material[]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新材质模式
|
||||
* @param root - 根对象
|
||||
* @param mode - 光照模式
|
||||
*/
|
||||
const updateMaterials = (root: THREE.Object3D, mode: LightingMode) => {
|
||||
root.traverse(child => {
|
||||
if ((child as THREE.Mesh).isMesh) {
|
||||
const mesh = child as THREE.Mesh
|
||||
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
|
||||
|
||||
const newMaterials = materials.map(mat => {
|
||||
if (mode === 'basic') {
|
||||
// 切换到基础模式
|
||||
if (mat.type === 'MeshLambertMaterial') return mat
|
||||
if (mat.userData.lambertPartner) return mat.userData.lambertPartner
|
||||
|
||||
const source = mat as THREE.MeshStandardMaterial
|
||||
const lambert = new THREE.MeshLambertMaterial({
|
||||
color: source.color,
|
||||
map: source.map,
|
||||
transparent: source.transparent,
|
||||
opacity: source.opacity,
|
||||
side: source.side,
|
||||
alphaMap: source.alphaMap,
|
||||
aoMap: source.aoMap,
|
||||
})
|
||||
|
||||
mat.userData.lambertPartner = lambert
|
||||
lambert.userData.pbrPartner = mat
|
||||
return lambert
|
||||
} else {
|
||||
// 切换到高级模式
|
||||
if (mat.type === 'MeshStandardMaterial') return mat
|
||||
if (mat.userData.pbrPartner) return mat.userData.pbrPartner
|
||||
return mat
|
||||
}
|
||||
})
|
||||
|
||||
mesh.material = Array.isArray(mesh.material) ? newMaterials : newMaterials[0]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 光照模式控制
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 切换光照模式
|
||||
*/
|
||||
const toggleLighting = () => {
|
||||
lightingMode.value = lightingMode.value === 'basic' ? 'advanced' : 'basic'
|
||||
applyLightingMode()
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用当前光照模式
|
||||
*/
|
||||
const applyLightingMode = () => {
|
||||
const scene = getScene()
|
||||
if (!scene) return
|
||||
|
||||
if (lightingMode.value === 'advanced') {
|
||||
if (envTexture) scene.environment = envTexture
|
||||
} else {
|
||||
scene.environment = null
|
||||
}
|
||||
|
||||
if (modelGroup) updateMaterials(modelGroup, lightingMode.value)
|
||||
if (archetypeModel.value) updateMaterials(archetypeModel.value, lightingMode.value)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 模式上下文
|
||||
// ============================================================
|
||||
|
||||
/** 供加载模式使用的共享上下文 */
|
||||
const context = {
|
||||
store,
|
||||
isLoading,
|
||||
archetypeModel,
|
||||
modelGroup,
|
||||
objectMap,
|
||||
modelCache,
|
||||
replaceWithPBRMaterial,
|
||||
generateHierarchy,
|
||||
initEnvironment,
|
||||
applyLightingMode,
|
||||
fitView,
|
||||
findNodeById,
|
||||
rebuildObjectMap,
|
||||
generateBreadcrumbs,
|
||||
}
|
||||
|
||||
// 实例化加载模式
|
||||
const { loadModel, processModel } = useFullMode(context)
|
||||
const { loadModelByFileName } = useSimplifiedMode(context)
|
||||
|
||||
// ============================================================
|
||||
// 导出接口
|
||||
// ============================================================
|
||||
|
||||
return {
|
||||
// 数据
|
||||
modelGroup,
|
||||
isLoading,
|
||||
lightingMode,
|
||||
objectMap,
|
||||
archetypeModel,
|
||||
|
||||
// 初始化
|
||||
initModelSystem,
|
||||
|
||||
// 模型加载
|
||||
loadModel,
|
||||
processModel,
|
||||
loadModelByFileName,
|
||||
|
||||
// 视图控制
|
||||
fitView,
|
||||
|
||||
// 光照控制
|
||||
toggleLighting,
|
||||
|
||||
// 工具函数
|
||||
findNodeById,
|
||||
generateBreadcrumbs,
|
||||
}
|
||||
}
|
||||
469
app/composables/powerStation/useThreeScene.ts
Normal file
@@ -0,0 +1,469 @@
|
||||
/**
|
||||
* Three.js 场景管理系统
|
||||
* 提供场景、相机、渲染器、控制器的初始化和管理
|
||||
* 支持投影模式切换、标准视图和视角旋转
|
||||
*/
|
||||
import * as TWEEN from '@tweenjs/tween.js'
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
||||
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
|
||||
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
|
||||
import type { RotateDirection, StandardViewType } from './types'
|
||||
|
||||
// ============================================================
|
||||
// 配置常量
|
||||
// ============================================================
|
||||
|
||||
/** 相机配置 */
|
||||
const CAMERA_CONFIG = {
|
||||
/** 透视相机视角 */
|
||||
fov: 45,
|
||||
/** 近裁剪面 */
|
||||
near: 0.01,
|
||||
/** 远裁剪面 */
|
||||
far: 10000,
|
||||
/** 初始位置 */
|
||||
position: new THREE.Vector3(10, 10, 10),
|
||||
} as const
|
||||
|
||||
/** 正交相机视锥体大小 */
|
||||
const ORTHOGRAPHIC_FRUSTUM_SIZE = 20
|
||||
|
||||
/** 渲染器配置 */
|
||||
const RENDERER_CONFIG = {
|
||||
/** 抗锯齿 */
|
||||
antialias: true,
|
||||
/** 透明背景 */
|
||||
alpha: true,
|
||||
/** 阴影映射 */
|
||||
shadowMap: false,
|
||||
/** 色调映射 */
|
||||
toneMapping: THREE.ACESFilmicToneMapping,
|
||||
/** 曝光度 */
|
||||
toneMappingExposure: 1.0,
|
||||
} as const
|
||||
|
||||
/** 控制器配置 */
|
||||
const CONTROLS_CONFIG = {
|
||||
/** 阻尼效果 */
|
||||
enableDamping: true,
|
||||
/** 阻尼系数 */
|
||||
dampingFactor: 0.05,
|
||||
/** 允许缩放 */
|
||||
enableZoom: true,
|
||||
/** 允许平移 */
|
||||
enablePan: true,
|
||||
} as const
|
||||
|
||||
/** 动画配置 */
|
||||
const ANIMATION_CONFIG = {
|
||||
/** 视图切换动画时长(毫秒) */
|
||||
viewTransitionDuration: 800,
|
||||
/** 旋转动画时长(毫秒) */
|
||||
rotationDuration: 500,
|
||||
/** 旋转角度(弧度) */
|
||||
rotateAngle: Math.PI / 8, // 22.5度
|
||||
} as const
|
||||
|
||||
/** 场景背景色 */
|
||||
const SCENE_BACKGROUND_COLOR = 0x1a1a1a
|
||||
|
||||
// ============================================================
|
||||
// 主钩子函数
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Three.js 场景组合式函数
|
||||
* 管理Three.js的完整生命周期
|
||||
*/
|
||||
export function useThreeScene() {
|
||||
// 容器引用
|
||||
const containerRef = ref<HTMLElement>()
|
||||
|
||||
// Three.js 核心实例
|
||||
let scene: THREE.Scene
|
||||
let camera: THREE.PerspectiveCamera | THREE.OrthographicCamera
|
||||
let renderer: THREE.WebGLRenderer
|
||||
let controls: OrbitControls
|
||||
let composer: EffectComposer
|
||||
let animationId: number
|
||||
let resizeObserver: ResizeObserver
|
||||
let renderPass: RenderPass
|
||||
|
||||
// 响应式状态
|
||||
const isOrthographic = ref(false)
|
||||
|
||||
// 回调钩子
|
||||
const onAnimateCallbacks: (() => void)[] = []
|
||||
const onResizeCallbacks: (() => void)[] = []
|
||||
|
||||
// ============================================================
|
||||
// 初始化
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 初始化Three.js场景
|
||||
*/
|
||||
const initThree = () => {
|
||||
if (!containerRef.value) return
|
||||
|
||||
const width = containerRef.value.clientWidth
|
||||
const height = containerRef.value.clientHeight
|
||||
|
||||
// 创建场景
|
||||
scene = new THREE.Scene()
|
||||
scene.background = new THREE.Color(SCENE_BACKGROUND_COLOR)
|
||||
|
||||
// 创建透视相机
|
||||
camera = new THREE.PerspectiveCamera(
|
||||
CAMERA_CONFIG.fov,
|
||||
width / height,
|
||||
CAMERA_CONFIG.near,
|
||||
CAMERA_CONFIG.far
|
||||
)
|
||||
camera.position.copy(CAMERA_CONFIG.position)
|
||||
|
||||
// 创建渲染器
|
||||
renderer = new THREE.WebGLRenderer({
|
||||
antialias: RENDERER_CONFIG.antialias,
|
||||
alpha: RENDERER_CONFIG.alpha,
|
||||
})
|
||||
renderer.setSize(width, height)
|
||||
renderer.setPixelRatio(window.devicePixelRatio)
|
||||
renderer.shadowMap.enabled = RENDERER_CONFIG.shadowMap
|
||||
renderer.toneMapping = RENDERER_CONFIG.toneMapping
|
||||
renderer.toneMappingExposure = RENDERER_CONFIG.toneMappingExposure
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace
|
||||
containerRef.value.appendChild(renderer.domElement)
|
||||
|
||||
// 创建控制器
|
||||
controls = new OrbitControls(camera, renderer.domElement)
|
||||
controls.enableDamping = CONTROLS_CONFIG.enableDamping
|
||||
controls.dampingFactor = CONTROLS_CONFIG.dampingFactor
|
||||
controls.enableZoom = CONTROLS_CONFIG.enableZoom
|
||||
controls.enablePan = CONTROLS_CONFIG.enablePan
|
||||
|
||||
// 创建后期处理合成器
|
||||
composer = new EffectComposer(renderer)
|
||||
composer.setPixelRatio(window.devicePixelRatio)
|
||||
renderPass = new RenderPass(scene, camera)
|
||||
composer.addPass(renderPass)
|
||||
|
||||
// 创建尺寸观察器
|
||||
resizeObserver = new ResizeObserver(() => onWindowResize())
|
||||
resizeObserver.observe(containerRef.value)
|
||||
|
||||
// 启动渲染循环
|
||||
animate()
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 渲染循环
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 渲染循环
|
||||
*/
|
||||
const animate = () => {
|
||||
animationId = requestAnimationFrame(animate)
|
||||
|
||||
// 更新TWEEN动画
|
||||
TWEEN.update()
|
||||
|
||||
// 更新控制器
|
||||
if (controls) controls.update()
|
||||
|
||||
// 执行外部回调
|
||||
onAnimateCallbacks.forEach(cb => cb())
|
||||
|
||||
// 渲染
|
||||
if (composer) {
|
||||
composer.render()
|
||||
} else if (renderer && scene && camera) {
|
||||
renderer.render(scene, camera)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 尺寸调整
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 窗口尺寸变化处理
|
||||
*/
|
||||
const onWindowResize = () => {
|
||||
if (!containerRef.value || !camera || !renderer || !composer) return
|
||||
|
||||
const width = containerRef.value.clientWidth
|
||||
const height = containerRef.value.clientHeight
|
||||
|
||||
// 根据相机类型更新投影矩阵
|
||||
if (camera instanceof THREE.PerspectiveCamera) {
|
||||
camera.aspect = width / height
|
||||
camera.updateProjectionMatrix()
|
||||
} else if (camera instanceof THREE.OrthographicCamera) {
|
||||
const aspect = width / height
|
||||
camera.left = (-ORTHOGRAPHIC_FRUSTUM_SIZE * aspect) / 2
|
||||
camera.right = (ORTHOGRAPHIC_FRUSTUM_SIZE * aspect) / 2
|
||||
camera.top = ORTHOGRAPHIC_FRUSTUM_SIZE / 2
|
||||
camera.bottom = -ORTHOGRAPHIC_FRUSTUM_SIZE / 2
|
||||
camera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
// 更新渲染器和合成器尺寸
|
||||
renderer.setSize(width, height)
|
||||
composer.setSize(width, height)
|
||||
composer.setPixelRatio(window.devicePixelRatio)
|
||||
|
||||
// 执行外部回调
|
||||
onResizeCallbacks.forEach(cb => cb())
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 投影模式切换
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 切换投影模式(透视/正交)
|
||||
*/
|
||||
const toggleProjection = () => {
|
||||
if (!containerRef.value || !camera || !controls) return
|
||||
|
||||
const width = containerRef.value.clientWidth
|
||||
const height = containerRef.value.clientHeight
|
||||
const aspect = width / height
|
||||
|
||||
// 保存当前状态
|
||||
const target = controls.target.clone()
|
||||
const position = camera.position.clone()
|
||||
|
||||
if (isOrthographic.value) {
|
||||
// 切换到透视相机
|
||||
camera = new THREE.PerspectiveCamera(
|
||||
CAMERA_CONFIG.fov,
|
||||
aspect,
|
||||
CAMERA_CONFIG.near,
|
||||
CAMERA_CONFIG.far
|
||||
)
|
||||
camera.position.copy(position)
|
||||
} else {
|
||||
// 切换到正交相机
|
||||
camera = new THREE.OrthographicCamera(
|
||||
(-ORTHOGRAPHIC_FRUSTUM_SIZE * aspect) / 2,
|
||||
(ORTHOGRAPHIC_FRUSTUM_SIZE * aspect) / 2,
|
||||
ORTHOGRAPHIC_FRUSTUM_SIZE / 2,
|
||||
-ORTHOGRAPHIC_FRUSTUM_SIZE / 2,
|
||||
CAMERA_CONFIG.near,
|
||||
CAMERA_CONFIG.far
|
||||
)
|
||||
camera.position.copy(position)
|
||||
camera.zoom = 1
|
||||
}
|
||||
|
||||
camera.lookAt(target)
|
||||
|
||||
// 更新控制器和渲染通道
|
||||
controls.object = camera
|
||||
controls.update()
|
||||
|
||||
if (renderPass) {
|
||||
renderPass.camera = camera
|
||||
}
|
||||
|
||||
isOrthographic.value = !isOrthographic.value
|
||||
onWindowResize() // 确保投影矩阵正确
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 视图控制
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 设置标准视图
|
||||
* @param view - 视图类型
|
||||
*/
|
||||
const setStandardView = (view: StandardViewType) => {
|
||||
if (!camera || !controls) return
|
||||
|
||||
const target = controls.target.clone()
|
||||
const distance = camera.position.distanceTo(target)
|
||||
const newPosition = target.clone()
|
||||
|
||||
// 根据视图类型计算新位置
|
||||
switch (view) {
|
||||
case 'top':
|
||||
newPosition.y += distance
|
||||
break
|
||||
case 'bottom':
|
||||
newPosition.y -= distance
|
||||
break
|
||||
case 'front':
|
||||
newPosition.z += distance
|
||||
break
|
||||
case 'back':
|
||||
newPosition.z -= distance
|
||||
break
|
||||
case 'left':
|
||||
newPosition.x -= distance
|
||||
break
|
||||
case 'right':
|
||||
newPosition.x += distance
|
||||
break
|
||||
case 'iso': {
|
||||
const isoDist = distance / Math.sqrt(3)
|
||||
newPosition.set(target.x + isoDist, target.y + isoDist, target.z + isoDist)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 动画过渡
|
||||
new TWEEN.Tween(camera.position)
|
||||
.to(newPosition, ANIMATION_CONFIG.viewTransitionDuration)
|
||||
.easing(TWEEN.Easing.Cubic.Out)
|
||||
.onUpdate(() => {
|
||||
camera.lookAt(target)
|
||||
controls.update()
|
||||
})
|
||||
.start()
|
||||
}
|
||||
|
||||
/**
|
||||
* 旋转视图
|
||||
* @param direction - 旋转方向
|
||||
*/
|
||||
const rotateView = (direction: RotateDirection) => {
|
||||
if (!controls) return
|
||||
|
||||
const offset = new THREE.Vector3().subVectors(camera.position, controls.target)
|
||||
const spherical = new THREE.Spherical().setFromVector3(offset)
|
||||
const rotateAngle = ANIMATION_CONFIG.rotateAngle
|
||||
|
||||
// 根据方向调整球坐标
|
||||
switch (direction) {
|
||||
case 'left':
|
||||
spherical.theta -= rotateAngle
|
||||
break
|
||||
case 'right':
|
||||
spherical.theta += rotateAngle
|
||||
break
|
||||
case 'up':
|
||||
spherical.phi -= rotateAngle
|
||||
break
|
||||
case 'down':
|
||||
spherical.phi += rotateAngle
|
||||
break
|
||||
case 'upleft':
|
||||
spherical.theta -= rotateAngle
|
||||
spherical.phi -= rotateAngle
|
||||
break
|
||||
case 'upright':
|
||||
spherical.theta += rotateAngle
|
||||
spherical.phi -= rotateAngle
|
||||
break
|
||||
case 'downleft':
|
||||
spherical.theta -= rotateAngle
|
||||
spherical.phi += rotateAngle
|
||||
break
|
||||
case 'downright':
|
||||
spherical.theta += rotateAngle
|
||||
spherical.phi += rotateAngle
|
||||
break
|
||||
}
|
||||
|
||||
// 限制phi范围,避免翻转
|
||||
spherical.phi = Math.max(0.01, Math.min(Math.PI - 0.01, spherical.phi))
|
||||
|
||||
const newPosition = new THREE.Vector3().setFromSpherical(spherical).add(controls.target)
|
||||
|
||||
// 动画过渡
|
||||
new TWEEN.Tween(camera.position)
|
||||
.to(newPosition, ANIMATION_CONFIG.rotationDuration)
|
||||
.easing(TWEEN.Easing.Cubic.Out)
|
||||
.onUpdate(() => {
|
||||
camera.lookAt(controls.target)
|
||||
controls.update()
|
||||
})
|
||||
.start()
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 回调注册
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 添加渲染循环回调
|
||||
* @param cb - 回调函数
|
||||
*/
|
||||
const addAnimateCallback = (cb: () => void) => {
|
||||
onAnimateCallbacks.push(cb)
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加尺寸变化回调
|
||||
* @param cb - 回调函数
|
||||
*/
|
||||
const addResizeCallback = (cb: () => void) => {
|
||||
onResizeCallbacks.push(cb)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 清理
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
const dispose = () => {
|
||||
cancelAnimationFrame(animationId)
|
||||
|
||||
if (resizeObserver) resizeObserver.disconnect()
|
||||
|
||||
if (renderer) {
|
||||
renderer.dispose()
|
||||
if (containerRef.value && renderer.domElement) {
|
||||
containerRef.value.removeChild(renderer.domElement)
|
||||
}
|
||||
}
|
||||
|
||||
if (composer) composer.dispose()
|
||||
}
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
dispose()
|
||||
})
|
||||
|
||||
// ============================================================
|
||||
// 导出接口
|
||||
// ============================================================
|
||||
|
||||
return {
|
||||
// 引用
|
||||
containerRef,
|
||||
|
||||
// 初始化
|
||||
initThree,
|
||||
|
||||
// 获取器
|
||||
getScene: () => scene,
|
||||
getCamera: () => camera,
|
||||
getRenderer: () => renderer,
|
||||
getControls: () => controls,
|
||||
getComposer: () => composer,
|
||||
|
||||
// 回调注册
|
||||
addAnimateCallback,
|
||||
addResizeCallback,
|
||||
|
||||
// 尺寸调整
|
||||
onWindowResize,
|
||||
|
||||
// 投影控制
|
||||
isOrthographic,
|
||||
toggleProjection,
|
||||
|
||||
// 视图控制
|
||||
setStandardView,
|
||||
rotateView,
|
||||
}
|
||||
}
|
||||
5
app/eslint.config.mjs
Normal file
@@ -0,0 +1,5 @@
|
||||
// @ts-check
|
||||
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||
|
||||
export default withNuxt()
|
||||
// Your custom configs here
|
||||
156
app/nuxt.config.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
import { config } from 'dotenv'
|
||||
|
||||
// 加载环境变量
|
||||
config({ path: '../.env' })
|
||||
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2025-07-15',
|
||||
devtools: { enabled: false },
|
||||
|
||||
// 开发服务器配置 - 允许外网访问
|
||||
devServer: {
|
||||
host: '0.0.0.0', // 改为0.0.0.0以允许外网访问
|
||||
port: process.env.NUXT_DEV_PORT || 8080,
|
||||
},
|
||||
|
||||
// 禁用默认布局,确保每个页面使用独立布局
|
||||
app: {
|
||||
layoutTransition: false,
|
||||
pageTransition: false,
|
||||
},
|
||||
|
||||
// 启用 SSR 以获得更好的构建结果
|
||||
ssr: false,
|
||||
|
||||
// 路径别名配置 - 使用 pnpm workspace 包引用
|
||||
alias: {
|
||||
'@nuxt4crud/shared': '@nuxt4crud/shared',
|
||||
},
|
||||
|
||||
// Vite 配置
|
||||
vite: {
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
'element-plus': ['element-plus'],
|
||||
'vue-vendor': ['vue'],
|
||||
},
|
||||
},
|
||||
},
|
||||
// 确保以 ES 模块模式构建
|
||||
commonjsOptions: {
|
||||
transformMixedEsModules: true,
|
||||
},
|
||||
},
|
||||
// 解决 CSS 中 import.meta 的问题
|
||||
css: {
|
||||
devSourcemap: true,
|
||||
// 确保 CSS 预处理器正确工作
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
api: 'modern-compiler', // 使用现代编译器 API
|
||||
},
|
||||
},
|
||||
},
|
||||
// 配置 ES 模块处理
|
||||
esbuild: {
|
||||
target: 'es2020',
|
||||
},
|
||||
optimizeDeps: {
|
||||
esbuildOptions: {
|
||||
target: 'es2020',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// 构建优化
|
||||
build: {
|
||||
transpile: ['element-plus/es'],
|
||||
},
|
||||
|
||||
modules: [
|
||||
'@nuxt/eslint',
|
||||
'@nuxt/image',
|
||||
'@element-plus/nuxt',
|
||||
'@nuxtjs/tailwindcss',
|
||||
'@pinia/nuxt',
|
||||
],
|
||||
|
||||
// Pinia配置
|
||||
pinia: {
|
||||
autoImports: [
|
||||
// 自动导入defineStore和storeToRefs
|
||||
'defineStore',
|
||||
'storeToRefs',
|
||||
],
|
||||
},
|
||||
|
||||
// 指定 TailwindCSS 的全局样式入口文件
|
||||
tailwindcss: {
|
||||
cssPath: '~/assets/css/main.css',
|
||||
},
|
||||
|
||||
// PostCSS 配置 - 替代 postcss.config.js
|
||||
postcss: {
|
||||
plugins: {
|
||||
'tailwindcss/nesting': {},
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
},
|
||||
|
||||
elementPlus: {
|
||||
/** Options */
|
||||
// 自动导入样式
|
||||
importStyle: 'scss',
|
||||
// 启用组件和指令缓存(仅在开发模式下有效)
|
||||
cache: true,
|
||||
// 手动配置需要安装的方法
|
||||
installMethods: ['ElLoading', 'ElMessage', 'ElMessageBox', 'ElNotification'],
|
||||
// 全局配置
|
||||
config: {
|
||||
size: 'default',
|
||||
zIndex: 3000,
|
||||
},
|
||||
},
|
||||
|
||||
// API 代理配置,移除Express代理,使用Nitro内置API
|
||||
nitro: {
|
||||
scanDirs: ['../server/api', '../server/routes'],
|
||||
srcDir: '../server',
|
||||
unstorage: {
|
||||
driver: 'fs',
|
||||
base: '../.data/db',
|
||||
},
|
||||
output: {
|
||||
serverDir: '.output/server',
|
||||
},
|
||||
ignore: ['../scripts/**'],
|
||||
prerender: {
|
||||
crawlLinks: false,
|
||||
routes: [],
|
||||
},
|
||||
},
|
||||
|
||||
// 运行时配置
|
||||
runtimeConfig: {
|
||||
// 私有配置,只在服务端可用
|
||||
feishuAppId: process.env.FEISHU_APP_ID || 'cli_a62aaf3d9d385013',
|
||||
feishuAppSecret: process.env.FEISHU_APP_SECRET || 'VH46dJI4T4bbUD7rI9J6Se2EI5rWVpBf',
|
||||
cozeApiToken:
|
||||
process.env.COZE_API_TOKEN ||
|
||||
'pat_7VvJA4an8IbsjmID9SasNBZyVDCJ3NamxwBOBdDxfUQEnGOYGaQf3odBUoibO0aF',
|
||||
cozeClientId: process.env.COZE_CLIENT_ID || '27608009841040583568491680329534.app.coze',
|
||||
cozeClientSecret:
|
||||
process.env.COZE_CLIENT_SECRET || 'Bx7ctHOQlzWVdoaQnSYJgRPwlK64IUuhi4oPnRO3OyJ5K4yc',
|
||||
|
||||
// 公共配置,客户端和服务端都可用
|
||||
public: {
|
||||
// 更新API基础路径,不再指向Express服务器
|
||||
apiBase: process.env.NUXT_PUBLIC_API_BASE || '',
|
||||
cozeDefaultWorkflowId: process.env.COZE_DEFAULT_WORKFLOW_ID || '7554970254934605859',
|
||||
},
|
||||
},
|
||||
})
|
||||
60
app/package.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "@nuxt4crud/app",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@8",
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"pnpm": ">=8.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare",
|
||||
"type-check": "nuxt typecheck",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babylonjs/core": "8.36.1",
|
||||
"@babylonjs/gui": "^8.36.1",
|
||||
"@babylonjs/gui-editor": "^8.36.1",
|
||||
"@babylonjs/inspector": "^8.36.1",
|
||||
"@babylonjs/loaders": "8.36.1",
|
||||
"@babylonjs/serializers": "8.36.1",
|
||||
"@coze/api": "^1.3.8",
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@element-plus/nuxt": "^1.1.4",
|
||||
"@kjgl77/datav-vue3": "^1.7.4",
|
||||
"@nuxt/eslint": "1.10.0",
|
||||
"@nuxt/fonts": "0.11.4",
|
||||
"@nuxt/icon": "2.1.0",
|
||||
"@nuxt/image": "1.11.0",
|
||||
"@nuxt4crud/shared": "file:../shared",
|
||||
"poseidon-engine-core": "file:./supreium_viewer/package",
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
"@tweenjs/tween.js": "^18.6.4",
|
||||
"@types/three": "^0.151.0",
|
||||
"chart.js": "^4.5.1",
|
||||
"echarts": "^6.0.0",
|
||||
"element-plus": "^2.11.7",
|
||||
"eslint": "^9.39.0",
|
||||
"nuxt": "^4.2.1",
|
||||
"three": "^0.151.3",
|
||||
"valibot": "^1.1.0",
|
||||
"vue": "^3.5.22",
|
||||
"vue-chartjs": "^5.3.3",
|
||||
"vue-echarts": "^8.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@prisma/client": "^6.19.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"magicast": "^0.3.5",
|
||||
"sass-embedded": "^1.93.3",
|
||||
"tsx": "^4.20.6",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
}
|
||||
}
|
||||
20
app/pages/index.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center">
|
||||
<div class="text-center">
|
||||
<h1 class="text-4xl font-bold text-gray-800 mb-4">Nuxt4CRUD 应用</h1>
|
||||
<p class="text-lg text-gray-600 mb-8">Prisma 6.x 升级成功!</p>
|
||||
<div class="space-x-4">
|
||||
<NuxtLink to="/posts" class="inline-block px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
|
||||
查看帖子
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/dashboard-demo" class="inline-block px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors">
|
||||
仪表板演示
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 首页组件
|
||||
</script>
|
||||
BIN
app/public/Cad/ScreenShot_2025-12-04_171313_794.png
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
app/public/Cad/一级再热器出口连接管.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
app/public/Cad/一级再热器管排.png
Normal file
|
After Width: | Height: | Size: 195 KiB |
BIN
app/public/Cad/一级再热器进口连接管.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
app/public/Cad/一级过热器出口连接管.png
Normal file
|
After Width: | Height: | Size: 219 KiB |
BIN
app/public/Cad/二级再热器出口连接管.png
Normal file
|
After Width: | Height: | Size: 179 KiB |
BIN
app/public/Cad/二级再热器管排.png
Normal file
|
After Width: | Height: | Size: 195 KiB |
BIN
app/public/Cad/二级再热器进口连接管.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
app/public/Cad/二级过热器出口连接管.png
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
app/public/Cad/二级过热器管排.png
Normal file
|
After Width: | Height: | Size: 177 KiB |
BIN
app/public/Cad/二级过热器进口连接管.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
app/public/Cad/水冷壁进口连接管屏.png
Normal file
|
After Width: | Height: | Size: 215 KiB |
BIN
app/public/Cad/燃烧孔.png
Normal file
|
After Width: | Height: | Size: 287 KiB |
BIN
app/public/Cad/燃烧孔1.png
Normal file
|
After Width: | Height: | Size: 315 KiB |
BIN
app/public/Cad_Preview/ScreenShot_2025-12-04_171313_794.png
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
app/public/Cad_Preview/一级再热器出口连接管.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
app/public/Cad_Preview/一级再热器管排.png
Normal file
|
After Width: | Height: | Size: 195 KiB |
BIN
app/public/Cad_Preview/一级再热器进口连接管.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
app/public/Cad_Preview/一级过热器出口连接管.png
Normal file
|
After Width: | Height: | Size: 219 KiB |
BIN
app/public/Cad_Preview/二级再热器出口连接管.png
Normal file
|
After Width: | Height: | Size: 179 KiB |
BIN
app/public/Cad_Preview/二级再热器管排.png
Normal file
|
After Width: | Height: | Size: 195 KiB |
BIN
app/public/Cad_Preview/二级再热器进口连接管.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
app/public/Cad_Preview/二级过热器出口连接管.png
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
app/public/Cad_Preview/二级过热器管排.png
Normal file
|
After Width: | Height: | Size: 177 KiB |
BIN
app/public/Cad_Preview/二级过热器进口连接管.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
app/public/Cad_Preview/水冷壁进口连接管屏.png
Normal file
|
After Width: | Height: | Size: 215 KiB |
BIN
app/public/Cad_Preview/燃烧孔.png
Normal file
|
After Width: | Height: | Size: 287 KiB |
BIN
app/public/Cad_Preview/燃烧孔1.png
Normal file
|
After Width: | Height: | Size: 315 KiB |
BIN
app/public/assets/ball.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
app/public/assets/cross.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
app/public/assets/leftTop.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
app/public/assets/rightTop.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
1
app/public/assets/toDown.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1656572456155" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="47431" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M141.84448 228.97664a25.6 25.6 0 0 0-8.6528 35.15392l330.61376 546.51392a56.32 56.32 0 0 0 19.03616 19.03616c26.61376 16.1024 61.24032 7.5776 77.34272-19.03616l330.61376-546.51392A25.6 25.6 0 0 0 868.8896 225.28H155.09504a25.6 25.6 0 0 0-13.25056 3.69664zM868.8896 174.08c42.4192 0 76.8 34.38592 76.8 76.8a76.8 76.8 0 0 1-11.0848 39.75168l-330.61376 546.51392c-30.73536 50.81088-96.83968 67.08224-147.65056 36.34688a107.52 107.52 0 0 1-36.34176-36.34688L89.38496 290.6368c-21.95456-36.3008-10.33216-83.51232 25.9584-105.472A76.8 76.8 0 0 1 155.09504 174.08h713.79456z" fill="#bfbfbf" p-id="47432"></path></svg>
|
||||
|
After Width: | Height: | Size: 988 B |
1
app/public/assets/toLeft.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1656572420938" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="46589" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M795.02336 141.85472a25.6 25.6 0 0 0-35.15392-8.6528L213.35552 463.81568a56.32 56.32 0 0 0-19.03616 19.03616c-16.1024 26.61376-7.5776 61.24032 19.03616 77.34272l546.51392 330.60864a25.6 25.6 0 0 0 38.85056-21.90336V155.10528a25.6 25.6 0 0 0-3.69664-13.25056zM849.92 868.89984c0 42.4192-34.38592 76.8-76.8 76.8a76.8 76.8 0 0 1-39.75168-11.0848L186.8544 604.00128c-50.81088-30.73536-67.08224-96.8448-36.34688-147.65056a107.52 107.52 0 0 1 36.34688-36.34176l546.5088-330.61376c36.3008-21.95456 83.51232-10.33216 105.472 25.9584A76.8 76.8 0 0 1 849.92 155.10528v713.79456z" fill="#bfbfbf" p-id="46590"></path></svg>
|
||||
|
After Width: | Height: | Size: 989 B |
1
app/public/assets/toRight.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1656572433880" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="46874" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M228.97664 141.85472A25.6 25.6 0 0 0 225.28 155.10528v713.79456a25.6 25.6 0 0 0 38.85056 21.90336l546.51392-330.60864c26.61376-16.1024 35.13856-50.72896 19.03616-77.34272a56.32 56.32 0 0 0-19.03616-19.03616L264.13056 133.20192a25.6 25.6 0 0 0-35.15392 8.6528zM174.08 868.89984V155.10528a76.8 76.8 0 0 1 11.08992-39.75168c21.95456-36.29056 69.1712-47.91296 105.46176-25.9584l546.51392 330.61376a107.52 107.52 0 0 1 36.34688 36.34176c30.73536 50.80576 14.464 116.9152-36.34688 147.65056l-546.5088 330.61376A76.8 76.8 0 0 1 250.88 945.69984c-42.41408 0-76.8-34.3808-76.8-76.8z" fill="#bfbfbf" p-id="46875"></path></svg>
|
||||
|
After Width: | Height: | Size: 994 B |
1
app/public/assets/toTop.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1656572448124" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="47179" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M882.12992 795.01824a25.6 25.6 0 0 0 8.6528-35.15392L560.16896 213.34528a56.32 56.32 0 0 0-19.03616-19.03616c-26.61376-16.09728-61.2352-7.5776-77.3376 19.03616L133.18144 759.86432a25.6 25.6 0 0 0 21.90336 38.85056h713.79456a25.6 25.6 0 0 0 13.25056-3.69664zM155.0848 849.91488c-42.41408 0-76.8-34.38592-76.8-76.8a76.8 76.8 0 0 1 11.08992-39.75168L419.98848 186.84416c30.73536-50.80576 96.83968-67.07712 147.64544-36.34176a107.52 107.52 0 0 1 36.34688 36.34176l330.61376 546.51904c21.95456 36.29056 10.32704 83.5072-25.9584 105.46176a76.8 76.8 0 0 1-39.7568 11.08992H155.0848z" fill="#bfbfbf" p-id="47180"></path></svg>
|
||||
|
After Width: | Height: | Size: 996 B |
254754
app/public/example.json
Normal file
BIN
app/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
app/public/icons/close.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
app/public/icons/fit-all.png
Normal file
|
After Width: | Height: | Size: 941 B |
BIN
app/public/icons/view-at.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
1
app/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
48
app/start-all-simple.bat
Normal file
@@ -0,0 +1,48 @@
|
||||
@echo off
|
||||
REM Nuxt Production Server Startup Script (BAT Version)
|
||||
REM Author: AI Assistant
|
||||
REM Version: 1.0
|
||||
|
||||
REM Change to script directory
|
||||
cd /d "%~dp0"
|
||||
|
||||
echo =======================================
|
||||
echo Nuxt4Curd Production Server Startup
|
||||
echo =======================================
|
||||
echo.
|
||||
|
||||
REM 1. Check Node.js environment
|
||||
echo 1. Checking Node.js environment...
|
||||
where node >nul 2>nul
|
||||
if %ERRORLEVEL% NEQ 0 (
|
||||
echo ERROR: Node.js is not installed!
|
||||
echo Please install Node.js v18+ and try again.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Get Node.js version
|
||||
for /f "tokens=*" %%i in ('node --version') do set NODE_VERSION=%%i
|
||||
echo SUCCESS: Node.js version: %NODE_VERSION%
|
||||
echo.
|
||||
|
||||
REM 2. Check current directory
|
||||
echo 2. Checking current directory...
|
||||
if not exist "server\index.mjs" (
|
||||
echo ERROR: Server files not found!
|
||||
echo Please ensure server files are in the output directory.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo SUCCESS: Server files found in current directory.
|
||||
echo.
|
||||
|
||||
REM 3. Start server
|
||||
echo 3. Starting Nuxt production server...
|
||||
echo.
|
||||
echo Press Ctrl+C to stop the server at any time.
|
||||
echo.
|
||||
|
||||
REM Run server command
|
||||
node server/index.mjs
|
||||
48
app/stores/powerStation.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const usePowerStationStore = defineStore('powerStation', () => {
|
||||
const currentNodeId = ref<string>('')
|
||||
const modelHierarchy = ref<any[]>([])
|
||||
const isModelLoading = ref(false)
|
||||
const loadMode = ref<boolean>(false) // false: 分批加载, true: 完整加载
|
||||
const modelCacheEnabled = ref<boolean>(false) // false: 不使用缓存, true: 使用缓存
|
||||
|
||||
// 聚焦事件监听器
|
||||
const focusListeners = ref<((nodeId: string) => void)[]>([])
|
||||
|
||||
// 操作方法 - 仅处理节点选择
|
||||
const selectNode = (node: any) => {
|
||||
currentNodeId.value = node.id
|
||||
}
|
||||
|
||||
// 触发聚焦事件
|
||||
const triggerFocus = (nodeId: string) => {
|
||||
focusListeners.value.forEach(listener => listener(nodeId))
|
||||
}
|
||||
|
||||
// 添加聚焦监听器
|
||||
const addFocusListener = (listener: (nodeId: string) => void) => {
|
||||
focusListeners.value.push(listener)
|
||||
}
|
||||
|
||||
// 移除聚焦监听器
|
||||
const removeFocusListener = (listener: (nodeId: string) => void) => {
|
||||
const index = focusListeners.value.indexOf(listener)
|
||||
if (index > -1) {
|
||||
focusListeners.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
currentNodeId,
|
||||
modelHierarchy,
|
||||
isModelLoading,
|
||||
loadMode,
|
||||
modelCacheEnabled,
|
||||
selectNode,
|
||||
triggerFocus,
|
||||
addFocusListener,
|
||||
removeFocusListener,
|
||||
}
|
||||
})
|
||||
22
app/supreium_viewer/package/Readme.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# poseidon-engine-core
|
||||
|
||||
This is the core of rendering engine. shared by all rendering project.
|
||||
|
||||
use can use submodule to reference this project,
|
||||
or use package created by poseidon-engine-core-wrap project.
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
1. 2022-12-29. Avoid export classes as default export, user need to change the way of import.
|
||||
2. 2022-12-29. to run in node, user need to import 'canvas-gl' and invoke init() function to register the font for canvas before create NodeWebGLRenderer.
|
||||
3. 2022-12-20. import the classes from 'poseidon-engine-core'
|
||||
4. 2022-12-20. rename file MeshLineExMaterial, rename MeLineColorMapMaterialParameters to LineColorMapMaterialParameters
|
||||
5. 2023-1-28. move after-projection-matrix from materials into cameras. do not need to update shaders any more.
|
||||
6. 2023-2-9. support color mapped materials. use ColorMapExMaterials instead. TODO: support filters later.
|
||||
|
||||
## Version History
|
||||
|
||||
### v0.2.0
|
||||
|
||||
1. 修复ScreenHelper.focusCamera连续多次调用的bug
|
||||
2. 修改Legend拼写错误
|
||||
BIN
app/supreium_viewer/package/dist/assets/ball.png
vendored
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
app/supreium_viewer/package/dist/assets/cross.png
vendored
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
app/supreium_viewer/package/dist/assets/leftTop.png
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
app/supreium_viewer/package/dist/assets/rightTop.png
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
1
app/supreium_viewer/package/dist/assets/toDown.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1656572456155" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="47431" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M141.84448 228.97664a25.6 25.6 0 0 0-8.6528 35.15392l330.61376 546.51392a56.32 56.32 0 0 0 19.03616 19.03616c26.61376 16.1024 61.24032 7.5776 77.34272-19.03616l330.61376-546.51392A25.6 25.6 0 0 0 868.8896 225.28H155.09504a25.6 25.6 0 0 0-13.25056 3.69664zM868.8896 174.08c42.4192 0 76.8 34.38592 76.8 76.8a76.8 76.8 0 0 1-11.0848 39.75168l-330.61376 546.51392c-30.73536 50.81088-96.83968 67.08224-147.65056 36.34688a107.52 107.52 0 0 1-36.34176-36.34688L89.38496 290.6368c-21.95456-36.3008-10.33216-83.51232 25.9584-105.472A76.8 76.8 0 0 1 155.09504 174.08h713.79456z" fill="#bfbfbf" p-id="47432"></path></svg>
|
||||
|
After Width: | Height: | Size: 988 B |
1
app/supreium_viewer/package/dist/assets/toLeft.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1656572420938" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="46589" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M795.02336 141.85472a25.6 25.6 0 0 0-35.15392-8.6528L213.35552 463.81568a56.32 56.32 0 0 0-19.03616 19.03616c-16.1024 26.61376-7.5776 61.24032 19.03616 77.34272l546.51392 330.60864a25.6 25.6 0 0 0 38.85056-21.90336V155.10528a25.6 25.6 0 0 0-3.69664-13.25056zM849.92 868.89984c0 42.4192-34.38592 76.8-76.8 76.8a76.8 76.8 0 0 1-39.75168-11.0848L186.8544 604.00128c-50.81088-30.73536-67.08224-96.8448-36.34688-147.65056a107.52 107.52 0 0 1 36.34688-36.34176l546.5088-330.61376c36.3008-21.95456 83.51232-10.33216 105.472 25.9584A76.8 76.8 0 0 1 849.92 155.10528v713.79456z" fill="#bfbfbf" p-id="46590"></path></svg>
|
||||
|
After Width: | Height: | Size: 989 B |
1
app/supreium_viewer/package/dist/assets/toRight.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1656572433880" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="46874" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M228.97664 141.85472A25.6 25.6 0 0 0 225.28 155.10528v713.79456a25.6 25.6 0 0 0 38.85056 21.90336l546.51392-330.60864c26.61376-16.1024 35.13856-50.72896 19.03616-77.34272a56.32 56.32 0 0 0-19.03616-19.03616L264.13056 133.20192a25.6 25.6 0 0 0-35.15392 8.6528zM174.08 868.89984V155.10528a76.8 76.8 0 0 1 11.08992-39.75168c21.95456-36.29056 69.1712-47.91296 105.46176-25.9584l546.51392 330.61376a107.52 107.52 0 0 1 36.34688 36.34176c30.73536 50.80576 14.464 116.9152-36.34688 147.65056l-546.5088 330.61376A76.8 76.8 0 0 1 250.88 945.69984c-42.41408 0-76.8-34.3808-76.8-76.8z" fill="#bfbfbf" p-id="46875"></path></svg>
|
||||
|
After Width: | Height: | Size: 994 B |
1
app/supreium_viewer/package/dist/assets/toTop.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1656572448124" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="47179" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M882.12992 795.01824a25.6 25.6 0 0 0 8.6528-35.15392L560.16896 213.34528a56.32 56.32 0 0 0-19.03616-19.03616c-26.61376-16.09728-61.2352-7.5776-77.3376 19.03616L133.18144 759.86432a25.6 25.6 0 0 0 21.90336 38.85056h713.79456a25.6 25.6 0 0 0 13.25056-3.69664zM155.0848 849.91488c-42.41408 0-76.8-34.38592-76.8-76.8a76.8 76.8 0 0 1 11.08992-39.75168L419.98848 186.84416c30.73536-50.80576 96.83968-67.07712 147.64544-36.34176a107.52 107.52 0 0 1 36.34688 36.34176l330.61376 546.51904c21.95456 36.29056 10.32704 83.5072-25.9584 105.46176a76.8 76.8 0 0 1-39.7568 11.08992H155.0848z" fill="#bfbfbf" p-id="47180"></path></svg>
|
||||
|
After Width: | Height: | Size: 996 B |
143
app/supreium_viewer/package/dist/browser.mjs
vendored
Normal file
143
app/supreium_viewer/package/dist/index.mjs
vendored
Normal file
1
app/supreium_viewer/package/dist/types/index.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export * from './src';
|
||||
16
app/supreium_viewer/package/dist/types/src/cad/brep/BRepAssembly.d.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
import { BRepBody, IBRepCompObj } from './BRepBody';
|
||||
export interface IAssemblyProps {
|
||||
name: string;
|
||||
}
|
||||
export interface IAssembly {
|
||||
subAssemblies: IAssembly[];
|
||||
bodies: BRepBody[];
|
||||
props?: IAssemblyProps;
|
||||
}
|
||||
export declare class BRepAssembly implements IAssembly {
|
||||
readonly subAssemblies: IAssembly[];
|
||||
readonly bodies: BRepBody[];
|
||||
props?: IAssemblyProps;
|
||||
name(): string | undefined;
|
||||
static fromCompound(comObj: IBRepCompObj): BRepAssembly | null;
|
||||
}
|
||||
127
app/supreium_viewer/package/dist/types/src/cad/brep/BRepBody.d.ts
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
import { SupBox } from '../types';
|
||||
import { BRepEdge } from './BRepEdge';
|
||||
import { BRepFace } from './BRepFace';
|
||||
import { BRepObject, IBRepProps } from './BRepObject';
|
||||
import { BRepShell } from './BRepShell';
|
||||
import { BRepSolid } from './BRepSolid';
|
||||
import { BodyType, ShapeType } from './BRepType';
|
||||
import { BRepVertex } from './BRepVertex';
|
||||
import { BRepWire } from './BRepWire';
|
||||
/**
|
||||
* @brief BRep body基类
|
||||
*
|
||||
*/
|
||||
export declare abstract class BRepBody {
|
||||
/**
|
||||
* @brief body类型
|
||||
*
|
||||
* @return
|
||||
* -<em>body 类型</em>
|
||||
*/
|
||||
abstract bodyType(): BodyType;
|
||||
/**
|
||||
* @brief 转换为特定body类型
|
||||
* @param
|
||||
*
|
||||
* @return
|
||||
* -<em>相应类型body</em>
|
||||
*/
|
||||
toSolid(): BRepSolidBody;
|
||||
toShell(): BRepShellBody;
|
||||
toWire(): BRepWireBody;
|
||||
toAcorn(): BRepAcornBody;
|
||||
toCompound(): BRepCompoundBody;
|
||||
findVertex(name: string | string[]): BRepVertex | undefined;
|
||||
findEdge(name: string | string[]): BRepEdge | undefined;
|
||||
findFace(name: string | string[]): BRepFace | undefined;
|
||||
findWire(name: string | string[]): BRepWire | undefined;
|
||||
findShell(name: string | string[]): BRepShell | undefined;
|
||||
findSolid(name: string | string[]): BRepSolid | undefined;
|
||||
findBRepObject(name: string | string[]): BRepObject | undefined;
|
||||
abstract bndBox(): SupBox;
|
||||
protected static calcuBox(obj: BRepObject): SupBox;
|
||||
}
|
||||
/**
|
||||
* @brief 实体类型body
|
||||
*
|
||||
*/
|
||||
export declare class BRepSolidBody extends BRepBody {
|
||||
readonly solid: BRepSolid;
|
||||
constructor(solid: BRepSolid);
|
||||
bodyType(): BodyType;
|
||||
toSolid(): BRepSolidBody;
|
||||
findVertex(name: string | string[]): BRepVertex | undefined;
|
||||
findEdge(name: string | string[]): BRepEdge | undefined;
|
||||
findFace(name: string | string[]): BRepFace | undefined;
|
||||
findSolid(name: string | string[]): BRepSolid | undefined;
|
||||
bndBox(): SupBox;
|
||||
}
|
||||
/**
|
||||
* @brief 壳类型body
|
||||
*
|
||||
*/
|
||||
export declare class BRepShellBody extends BRepBody {
|
||||
readonly shell: BRepShell;
|
||||
constructor(shell: BRepShell);
|
||||
bodyType(): BodyType;
|
||||
toShell(): BRepShellBody;
|
||||
findVertex(name: string | string[]): BRepVertex | undefined;
|
||||
findEdge(name: string | string[]): BRepEdge | undefined;
|
||||
findFace(name: string | string[]): BRepFace | undefined;
|
||||
findShell(name: string | string[]): BRepShell | undefined;
|
||||
bndBox(): SupBox;
|
||||
}
|
||||
/**
|
||||
* @brief wire 类型body
|
||||
*
|
||||
*/
|
||||
export declare class BRepWireBody extends BRepBody {
|
||||
readonly wire: BRepWire;
|
||||
constructor(wire: BRepWire);
|
||||
toWire(): BRepWireBody;
|
||||
bodyType(): BodyType;
|
||||
findVertex(name: string | string[]): BRepVertex | undefined;
|
||||
findEdge(name: string | string[]): BRepEdge | undefined;
|
||||
findWire(name: string | string[]): BRepWire | undefined;
|
||||
bndBox(): SupBox;
|
||||
}
|
||||
/**
|
||||
* @brief acorn类型body
|
||||
*
|
||||
*/
|
||||
export declare class BRepAcornBody extends BRepBody {
|
||||
readonly point: BRepVertex;
|
||||
constructor(point: BRepVertex);
|
||||
bodyType(): BodyType;
|
||||
toAcorn(): BRepAcornBody;
|
||||
findVertex(name: string | string[]): BRepVertex | undefined;
|
||||
bndBox(): SupBox;
|
||||
}
|
||||
/**
|
||||
* @brief compound类型body
|
||||
*
|
||||
*/
|
||||
export declare class BRepCompoundBody extends BRepBody {
|
||||
readonly solids: BRepSolid[];
|
||||
readonly shells: BRepShell[];
|
||||
readonly wires: BRepWire[];
|
||||
readonly acorns: BRepVertex[];
|
||||
bodyType(): BodyType;
|
||||
toCompound(): BRepCompoundBody;
|
||||
addSubShape(obj: IBRepObj): boolean;
|
||||
findVertex(name: string | string[]): BRepVertex | undefined;
|
||||
findEdge(name: string | string[]): BRepEdge | undefined;
|
||||
findFace(name: string | string[]): BRepFace | undefined;
|
||||
findSolid(name: string | string[]): BRepSolid | undefined;
|
||||
findShell(name: string | string[]): BRepShell | undefined;
|
||||
findWire(name: string | string[]): BRepWire | undefined;
|
||||
bndBox(): SupBox;
|
||||
}
|
||||
export interface IBRepObj {
|
||||
type: ShapeType;
|
||||
props?: IBRepProps;
|
||||
}
|
||||
export interface IBRepCompObj extends IBRepObj {
|
||||
subShapes: IBRepObj[];
|
||||
}
|
||||
export declare const createBRepBody: (obj: IBRepObj) => BRepBody | null;
|
||||
20
app/supreium_viewer/package/dist/types/src/cad/brep/BRepEdge.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
import { CurveType } from '../geom/CurveType';
|
||||
import { Polygon } from '../geom/Polygon';
|
||||
import { BRepObject } from './BRepObject';
|
||||
import { ShapeType } from './BRepType';
|
||||
import { BRepVertex } from './BRepVertex';
|
||||
/**
|
||||
* @brief BRep边对象
|
||||
*
|
||||
*/
|
||||
export declare class BRepEdge extends BRepObject {
|
||||
readonly geomType: CurveType;
|
||||
readonly polygon: Polygon;
|
||||
readonly start: BRepVertex;
|
||||
readonly end: BRepVertex;
|
||||
constructor(obj: any, parent: BRepObject);
|
||||
isGeom(): boolean;
|
||||
getAllEdges(): BRepEdge[];
|
||||
getAllVertices(): BRepVertex[];
|
||||
shapeType(): ShapeType;
|
||||
}
|
||||
23
app/supreium_viewer/package/dist/types/src/cad/brep/BRepFace.d.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
import { SurfaceType } from '../geom/SurfaceType';
|
||||
import { Triangulation } from '../geom/Triangulation';
|
||||
import { BRepEdge } from './BRepEdge';
|
||||
import { BRepLoop } from './BRepLoop';
|
||||
import { BRepObject } from './BRepObject';
|
||||
import { ShapeType } from './BRepType';
|
||||
import { BRepVertex } from './BRepVertex';
|
||||
/**
|
||||
* @brief 边界面类型
|
||||
*
|
||||
*/
|
||||
export declare class BRepFace extends BRepObject {
|
||||
readonly geomType: SurfaceType;
|
||||
readonly triangulation: Triangulation;
|
||||
readonly loops: BRepLoop[];
|
||||
constructor(obj: any, parent: BRepObject);
|
||||
hasInnerLoop(): boolean;
|
||||
isGeom(): boolean;
|
||||
getAllFaces(): BRepFace[];
|
||||
getAllEdges(): BRepEdge[];
|
||||
getAllVertices(): BRepVertex[];
|
||||
shapeType(): ShapeType;
|
||||
}
|
||||
17
app/supreium_viewer/package/dist/types/src/cad/brep/BRepHalfEdge.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
import { BRepEdge } from './BRepEdge';
|
||||
import { BRepObject } from './BRepObject';
|
||||
import { ShapeType } from './BRepType';
|
||||
import { BRepVertex } from './BRepVertex';
|
||||
/**
|
||||
* @brief 半边对象
|
||||
*
|
||||
*/
|
||||
export declare class BRepHalfEdge extends BRepObject {
|
||||
readonly edge: BRepEdge;
|
||||
readonly reversed: boolean;
|
||||
constructor(parent: BRepObject, obj: any);
|
||||
shapeType(): ShapeType;
|
||||
isGeom(): boolean;
|
||||
getAllEdges(): BRepEdge[];
|
||||
getAllVertices(): BRepVertex[];
|
||||
}
|
||||
18
app/supreium_viewer/package/dist/types/src/cad/brep/BRepLoop.d.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
import { BRepEdge } from './BRepEdge';
|
||||
import { BRepHalfEdge } from './BRepHalfEdge';
|
||||
import { BRepObject } from './BRepObject';
|
||||
import { ShapeType } from './BRepType';
|
||||
import { BRepVertex } from './BRepVertex';
|
||||
/**
|
||||
* @brief BRep环对象,用于限定Face的边界
|
||||
*
|
||||
*/
|
||||
export declare class BRepLoop extends BRepObject {
|
||||
readonly halfEdges: BRepHalfEdge[];
|
||||
readonly isOut: boolean;
|
||||
constructor(parent: BRepObject | null, obj: any);
|
||||
shapeType(): ShapeType;
|
||||
isGeom(): boolean;
|
||||
getAllEdges(): BRepEdge[];
|
||||
getAllVertices(): BRepVertex[];
|
||||
}
|
||||
29
app/supreium_viewer/package/dist/types/src/cad/brep/BRepObject.d.ts
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
import { BRepBody } from './BRepBody';
|
||||
import { BRepEdge } from './BRepEdge';
|
||||
import { BRepFace } from './BRepFace';
|
||||
import { ShapeType } from './BRepType';
|
||||
import { BRepVertex } from './BRepVertex';
|
||||
export interface IBRepProps {
|
||||
name: string | string[];
|
||||
color?: number;
|
||||
}
|
||||
/**
|
||||
* @brief BRep对象接口
|
||||
*
|
||||
*/
|
||||
export declare abstract class BRepObject {
|
||||
parent: BRepObject | null;
|
||||
props?: IBRepProps;
|
||||
constructor(parent: BRepObject | null, obj: any);
|
||||
name(): string | string[] | undefined;
|
||||
abstract shapeType(): ShapeType;
|
||||
abstract isGeom(): boolean;
|
||||
findEdge(_name: string | string[]): BRepEdge | undefined;
|
||||
findFace(_name: string | string[]): BRepFace | undefined;
|
||||
findVertex(_name: string | string[]): BRepVertex | undefined;
|
||||
getAllFaces(): BRepFace[];
|
||||
getAllEdges(): BRepEdge[];
|
||||
getAllVertices(): BRepVertex[];
|
||||
static findBRep(name: string | string[], array: BRepObject[]): BRepObject | undefined;
|
||||
body(): BRepBody;
|
||||
}
|
||||
27
app/supreium_viewer/package/dist/types/src/cad/brep/BRepShell.d.ts
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
import { BRepBody } from './BRepBody';
|
||||
import { BRepEdge } from './BRepEdge';
|
||||
import { BRepFace } from './BRepFace';
|
||||
import { BRepObject } from './BRepObject';
|
||||
import { ShapeType } from './BRepType';
|
||||
import { BRepVertex } from './BRepVertex';
|
||||
/**
|
||||
* @brief BRep壳对象,壳对象包含面、边和顶点
|
||||
*
|
||||
*/
|
||||
export declare class BRepShell extends BRepObject {
|
||||
readonly faces: BRepFace[];
|
||||
readonly edges: BRepEdge[];
|
||||
readonly vertices: BRepVertex[];
|
||||
private wrappedBody;
|
||||
constructor(obj: any, parent?: BRepObject | null);
|
||||
findVertex(name: string | string[]): BRepVertex | undefined;
|
||||
findEdge(name: string | string[]): BRepEdge | undefined;
|
||||
findFace(name: string | string[]): BRepFace | undefined;
|
||||
shapeType(): ShapeType;
|
||||
isGeom(): boolean;
|
||||
getAllFaces(): BRepFace[];
|
||||
getAllEdges(): BRepEdge[];
|
||||
getAllVertices(): BRepVertex[];
|
||||
body(): BRepBody;
|
||||
setBody(body: BRepBody): void;
|
||||
}
|
||||
26
app/supreium_viewer/package/dist/types/src/cad/brep/BRepSolid.d.ts
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
import { BRepBody } from './BRepBody';
|
||||
import { BRepEdge } from './BRepEdge';
|
||||
import { BRepFace } from './BRepFace';
|
||||
import { BRepObject } from './BRepObject';
|
||||
import { BRepShell } from './BRepShell';
|
||||
import { ShapeType } from './BRepType';
|
||||
import { BRepVertex } from './BRepVertex';
|
||||
/**
|
||||
* @brief BRep实体对象
|
||||
*
|
||||
*/
|
||||
export declare class BRepSolid extends BRepObject {
|
||||
readonly shells: BRepShell[];
|
||||
private wrappedBody;
|
||||
constructor(obj: any);
|
||||
shapeType(): ShapeType;
|
||||
isGeom(): boolean;
|
||||
findVertex(name: string | string[]): BRepVertex | undefined;
|
||||
findEdge(name: string | string[]): BRepEdge | undefined;
|
||||
findFace(name: string | string[]): BRepFace | undefined;
|
||||
getAllFaces(): BRepFace[];
|
||||
getAllEdges(): BRepEdge[];
|
||||
getAllVertices(): BRepVertex[];
|
||||
body(): BRepBody;
|
||||
setBody(body: BRepBody): void;
|
||||
}
|
||||
27
app/supreium_viewer/package/dist/types/src/cad/brep/BRepType.d.ts
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* @brief BRep对象类型
|
||||
* @note 这里定义的BRep对象主要用于显示,这里采用Parasolid/ACIS的拓扑表示方式
|
||||
*
|
||||
*/
|
||||
export declare enum ShapeType {
|
||||
COMPOUND = 0,//! 复合类型
|
||||
SOLID = 2,//! 实体类型
|
||||
SHELL = 3,//! 壳类型
|
||||
FACE = 4,//! 面类型
|
||||
WIRE = 5,//! 线框类型
|
||||
EDGE = 6,//! 边类型
|
||||
VERTEX = 7,//! 顶点类型
|
||||
LOOP = 9,//! 环类型,与a/p对应
|
||||
HALFEDGE = 10
|
||||
}
|
||||
/**
|
||||
* @brief BRep body类型,使用brep对象时应该以此为单位进行使用
|
||||
*
|
||||
*/
|
||||
export declare enum BodyType {
|
||||
BT_SolidBody = 0,//! 实体对象
|
||||
BT_ShellBody = 1,//! 壳对象
|
||||
BT_WireBody = 2,//! 线框对象
|
||||
BT_AcornBody = 3,//! 点对象
|
||||
BT_CompoundBody = 4
|
||||
}
|
||||
18
app/supreium_viewer/package/dist/types/src/cad/brep/BRepVertex.d.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Point } from '../geom/Point';
|
||||
import { BRepBody } from './BRepBody';
|
||||
import { BRepObject } from './BRepObject';
|
||||
import { ShapeType } from './BRepType';
|
||||
/**
|
||||
* @brief BRep顶点对象
|
||||
*
|
||||
*/
|
||||
export declare class BRepVertex extends BRepObject {
|
||||
readonly point: Point;
|
||||
private wrappedBody;
|
||||
constructor(obj: any, parent?: BRepObject | null);
|
||||
isGeom(): boolean;
|
||||
shapeType(): ShapeType;
|
||||
getAllVertices(): BRepVertex[];
|
||||
body(): BRepBody;
|
||||
setBody(body: BRepBody): void;
|
||||
}
|
||||
24
app/supreium_viewer/package/dist/types/src/cad/brep/BRepWire.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
import { BRepBody } from './BRepBody';
|
||||
import { BRepEdge } from './BRepEdge';
|
||||
import { BRepObject } from './BRepObject';
|
||||
import { ShapeType } from './BRepType';
|
||||
import { BRepVertex } from './BRepVertex';
|
||||
/**
|
||||
* @brief BRep环对象
|
||||
*
|
||||
*/
|
||||
export declare class BRepWire extends BRepObject {
|
||||
readonly edges: BRepEdge[];
|
||||
readonly vertices: BRepVertex[];
|
||||
private wrappedBody;
|
||||
constructor();
|
||||
constructor(obj: any);
|
||||
findVertex(name: string | string[]): BRepVertex | undefined;
|
||||
findEdge(name: string | string[]): BRepEdge | undefined;
|
||||
isGeom(): boolean;
|
||||
shapeType(): ShapeType;
|
||||
getAllEdges(): BRepEdge[];
|
||||
getAllVertices(): BRepVertex[];
|
||||
body(): BRepBody;
|
||||
setBody(body: BRepBody): void;
|
||||
}
|
||||
12
app/supreium_viewer/package/dist/types/src/cad/brep/index.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
export * from './BRepBody';
|
||||
export * from './BRepEdge';
|
||||
export * from './BRepFace';
|
||||
export * from './BRepHalfEdge';
|
||||
export * from './BRepLoop';
|
||||
export * from './BRepObject';
|
||||
export * from './BRepShell';
|
||||
export * from './BRepSolid';
|
||||
export * from './BRepType';
|
||||
export * from './BRepVertex';
|
||||
export * from './BRepWire';
|
||||
export * from './BRepAssembly';
|
||||
43
app/supreium_viewer/package/dist/types/src/cad/geom/AABB/AABB.d.ts
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Vector3, Vector2 } from 'three';
|
||||
export declare class AABB {
|
||||
readonly lowPoint: Vector3 | Vector2;
|
||||
readonly highPoint: Vector3 | Vector2;
|
||||
/**
|
||||
* @remarks
|
||||
* If the min is higher than the max, the bounding box calculations will break
|
||||
*
|
||||
* @param minX the minimum value in the X axis
|
||||
* @param minY the minimum value in the Y axis
|
||||
* @param minZ the minimum value in the Z axis
|
||||
* @param maxX the maximum value in the X axis
|
||||
* @param maxY the maximum value in the Y axis
|
||||
* @param maxZ the maximum value in the Z axis
|
||||
*/
|
||||
constructor(minX: number, minY: number, minZ: number | null, maxX: number, maxY: number, maxZ: number | null);
|
||||
/**
|
||||
* Merges the two bounding boxes to create a larger one containing the two
|
||||
*
|
||||
* @param other - The other bounding box to use for merging
|
||||
* @returns The new bounding box containing both bounding boxes
|
||||
*/
|
||||
merge(other: AABB): AABB;
|
||||
/**
|
||||
* Check if the other bounding box is overlapping with this one
|
||||
*
|
||||
* @param other - The bounding box to check overlap against
|
||||
* @returns `true` if the other bounding box is colling, `false` otherwise
|
||||
*/
|
||||
overlaps(other: AABB): boolean;
|
||||
/**
|
||||
* Check if the point is contained within the bounding box
|
||||
*
|
||||
* @param point - The point to check
|
||||
* @returns `true` if the point is inside the bounding box, `false` otherwise
|
||||
*/
|
||||
containsPoint(point: Vector3 | Vector2): boolean;
|
||||
get is3D(): boolean;
|
||||
get width(): number;
|
||||
get height(): number;
|
||||
get depth(): number;
|
||||
get space(): number;
|
||||
}
|
||||
38
app/supreium_viewer/package/dist/types/src/cad/geom/AABB/AABBNode.d.ts
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
import { IAABBShape } from './IAABBShape';
|
||||
import { AABB } from './AABB';
|
||||
/**
|
||||
* Axis Aligned Bouding Box Node
|
||||
*
|
||||
* @remark
|
||||
* The bounding box on this nodes either fully contains the shape or the two child nodes.
|
||||
*/
|
||||
interface IAABBNode {
|
||||
Aabb: AABB;
|
||||
Shape?: IAABBShape;
|
||||
ParentNode?: IAABBNode;
|
||||
LeftNode?: IAABBNode;
|
||||
RightNode?: IAABBNode;
|
||||
readonly IsLeaf: boolean;
|
||||
}
|
||||
export declare class AABBNode implements IAABBNode {
|
||||
Aabb: AABB;
|
||||
Shape?: IAABBShape;
|
||||
ParentNode?: IAABBNode;
|
||||
LeftNode?: IAABBNode;
|
||||
RightNode?: IAABBNode;
|
||||
/**
|
||||
* @param aabb - A bounding box containing the shape or the two child nodes
|
||||
* @param shape - A reference to a shape implementation of IAABBShape
|
||||
* @param parentNode - The node parenting this node
|
||||
* @param leftNode - The first child node
|
||||
* @param rightNode - The second child node
|
||||
*/
|
||||
constructor(Aabb: AABB, shape?: IAABBShape, parentNode?: IAABBNode, leftNode?: IAABBNode, rightNode?: IAABBNode);
|
||||
/**
|
||||
* @remark
|
||||
* The node is considered a leaf when is has no reference to any child nodes.
|
||||
* Only leaf nodes should contain shapes
|
||||
*/
|
||||
get IsLeaf(): boolean;
|
||||
}
|
||||
export {};
|
||||
61
app/supreium_viewer/package/dist/types/src/cad/geom/AABB/AABBTree.d.ts
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Ray, Vector3, Vector2 } from 'three';
|
||||
import { IAABBShape } from './IAABBShape';
|
||||
import { AABBNode } from './AABBNode';
|
||||
/**
|
||||
* Axis Aligned Bounding Box Tree
|
||||
*/
|
||||
export declare class AABBTree {
|
||||
private rootNode?;
|
||||
private shapeToNodeMap;
|
||||
constructor();
|
||||
/**
|
||||
* Adds a new shape to the tree.
|
||||
* @param shape The shape to add to the tree (Must be an implementation of IAABBShape interface)
|
||||
*/
|
||||
addShape(shape: IAABBShape): void;
|
||||
/**
|
||||
* Removes the shape and balances the tree
|
||||
* @remarks The method will gracefully exit if the shape doesn't exist in the tree
|
||||
* @param shape - The shape to remove from the tree
|
||||
*/
|
||||
removeShape(shape: IAABBShape): void;
|
||||
/**
|
||||
* Finds all the shape overlapping with the point
|
||||
* @remarks if the tree is empty this function will always return an empty array
|
||||
* @param shape - The shape to check overlaps against
|
||||
* @returns An array containing all the shape overlapping with the point
|
||||
*/
|
||||
getShapesOverlappingWith(point: Vector3 | Vector2): IAABBShape[];
|
||||
/**
|
||||
* Returns all the node in the tree
|
||||
*
|
||||
* @remarks
|
||||
* The nodes that serves as parent containers are also returned. Do not expect every node to contain a shape
|
||||
*
|
||||
* @returns An array containing all the nodes the tree contains
|
||||
*/
|
||||
getAllNodes(): AABBNode[];
|
||||
/**
|
||||
* Iterates recursivly over the entire three and all the node to the array
|
||||
*
|
||||
* @param node - The node to iterate over
|
||||
* @param array - Reference to the array that will contain all the nodes in the tree
|
||||
*/
|
||||
private nodeIterator;
|
||||
/**
|
||||
* Remove a node from the tree and balances the tree if needed
|
||||
*
|
||||
* @param node - The node to remove from the tree
|
||||
*/
|
||||
private removeNode;
|
||||
/**
|
||||
* @remarks
|
||||
* When the parent node intersects the ray, the child node is found separately.
|
||||
* If a Boundbox for a leaf node intersects a ray, the leaf node is returned
|
||||
*
|
||||
* @notion two dimensional ray intersection is not achieved
|
||||
* @param ray A ray type from threejs
|
||||
* @returns All shapes that intersect the Ray
|
||||
*/
|
||||
ray2Tree(ray: Ray): IAABBShape[] | null;
|
||||
}
|
||||
7
app/supreium_viewer/package/dist/types/src/cad/geom/AABB/IAABBShape.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Vector3, Vector2 } from 'three';
|
||||
import { AABB } from './AABB';
|
||||
export interface IAABBShape {
|
||||
getAABB(): AABB;
|
||||
containsPoint(point: Vector3 | Vector2): boolean;
|
||||
getTypeName(): string;
|
||||
}
|
||||
13
app/supreium_viewer/package/dist/types/src/cad/geom/AABB/LineAABBShape.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Vector3, Vector2 } from 'three';
|
||||
import { IAABBShape } from './IAABBShape';
|
||||
import { AABB } from './AABB';
|
||||
export declare class LineAABBShape implements IAABBShape {
|
||||
private aabb;
|
||||
private VectorList;
|
||||
private type;
|
||||
constructor(A: Vector3 | Vector2, B: Vector3 | Vector2);
|
||||
getAABB(): AABB;
|
||||
containsPoint(point: Vector3 | Vector2): boolean;
|
||||
getTypeName(): string;
|
||||
points(): (Vector3 | Vector2)[];
|
||||
}
|
||||
13
app/supreium_viewer/package/dist/types/src/cad/geom/AABB/TriAABBShape.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Vector3, Vector2 } from 'three';
|
||||
import { IAABBShape } from './IAABBShape';
|
||||
import { AABB } from './AABB';
|
||||
export declare class TriAABBShape implements IAABBShape {
|
||||
private aabb;
|
||||
private VectorList;
|
||||
private type;
|
||||
constructor(A: Vector3 | Vector2, B: Vector3 | Vector2, C: Vector3 | Vector2);
|
||||
getAABB(): AABB;
|
||||
containsPoint(point: Vector3 | Vector2): boolean;
|
||||
getTypeName(): string;
|
||||
points(): (Vector3 | Vector2)[];
|
||||
}
|
||||
6
app/supreium_viewer/package/dist/types/src/cad/geom/AABB/index.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from './AABB';
|
||||
export * from './AABBNode';
|
||||
export * from './AABBTree';
|
||||
export * from './IAABBShape';
|
||||
export * from './LineAABBShape';
|
||||
export * from './TriAABBShape';
|
||||
17
app/supreium_viewer/package/dist/types/src/cad/geom/Coord.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
import { SupVector } from '../types';
|
||||
/**
|
||||
* @brief 坐标对象
|
||||
* @note 坐标对象目前使用threejs的Vector3实现
|
||||
*
|
||||
*/
|
||||
export declare class Coord {
|
||||
readonly xyz: SupVector;
|
||||
constructor(x: number, y: number, z: number);
|
||||
setCoord(x: number, y: number, z: number): void;
|
||||
x(): number;
|
||||
y(): number;
|
||||
z(): number;
|
||||
static toFloat32Array(coords: Coord[]): Float32Array;
|
||||
static toArray(coords: Coord[]): number[];
|
||||
static toArrayVec(coords: SupVector[]): number[];
|
||||
}
|
||||
10
app/supreium_viewer/package/dist/types/src/cad/geom/Coordinate.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
import { SupMat4, SupVector } from '../types';
|
||||
export declare class Coordinate {
|
||||
readonly pos: SupVector;
|
||||
readonly zDir: SupVector;
|
||||
readonly xDir: SupVector;
|
||||
constructor(pos?: SupVector, zDir?: SupVector, xDir?: SupVector);
|
||||
get yDir(): SupVector;
|
||||
private normalize;
|
||||
toMatrix4(): SupMat4;
|
||||
}
|
||||
15
app/supreium_viewer/package/dist/types/src/cad/geom/CurveType.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @brief 曲线类型枚举
|
||||
*
|
||||
*/
|
||||
export declare enum CurveType {
|
||||
Line = 0,//! 直线
|
||||
Circle = 1,//! 圆
|
||||
Ellipse = 2,//! 椭圆
|
||||
Parabola = 3,//! 抛物线
|
||||
Hyperbola = 4,//! 双曲线
|
||||
OffsetCurve = 5,//! 偏置曲线
|
||||
BezierCurve = 6,//! 贝塞尔曲线
|
||||
BSplineCurve = 7,//! Nurbs曲线
|
||||
Other = 8
|
||||
}
|
||||
10
app/supreium_viewer/package/dist/types/src/cad/geom/Direct.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Coord } from './Coord';
|
||||
import { SupMat4 } from '../types';
|
||||
/**
|
||||
* @brief 方向对象
|
||||
*
|
||||
*/
|
||||
export declare class Direct extends Coord {
|
||||
constructor(x: number, y: number, z: number);
|
||||
applayMatrix4(mat: SupMat4): void;
|
||||
}
|
||||
174
app/supreium_viewer/package/dist/types/src/cad/geom/GeometryBuilder.d.ts
vendored
Normal file
@@ -0,0 +1,174 @@
|
||||
import { Coordinate } from './Coordinate';
|
||||
import { PolyGeometry } from './PolyGeometry';
|
||||
import { Triangulation } from './Triangulation';
|
||||
import { SupBox, SupVector } from '../types';
|
||||
type NumberArray4 = [number, number, number, number];
|
||||
export type CRModeType = 'Chamfer' | 'Fillet';
|
||||
export type CRModeTypeArray4 = [CRModeType, CRModeType, CRModeType, CRModeType];
|
||||
export declare class GeometryBuilder {
|
||||
static angleTol: number;
|
||||
static zeroTol: number;
|
||||
private static floatEqual;
|
||||
static cone(r1: number, r2: number, l: number, csys?: Coordinate): Triangulation | null;
|
||||
static arrow(cylR: number, cylL: number, coneR: number, coneL: number, csys?: Coordinate): Triangulation | null;
|
||||
static cutPlaneByBox(box: SupBox, csys: Coordinate): PolyGeometry | null;
|
||||
static box(min: SupVector, max: SupVector, csys?: Coordinate): PolyGeometry | null;
|
||||
static sphere(radius: number, center: SupVector, csys?: Coordinate, segments?: number): Triangulation;
|
||||
/**
|
||||
* 计算一段圆弧上的离散的点
|
||||
* @param center 圆心
|
||||
* @param radius 半径
|
||||
* @param startAngle 开始角度
|
||||
* @param endAngle 结束角度
|
||||
* @param segments 线段数量
|
||||
* @param returnMode 1, 2, 3, 4 返回模式
|
||||
* @param xy true 在 xy 平面上 false 在 xz平面上
|
||||
* @returns
|
||||
* returnMode | length | means
|
||||
* ------------------------------------------------
|
||||
* 1 | segments | start + paths
|
||||
* 2 | segments | paths + end
|
||||
* 3 | segments + 1 | start + paths + end
|
||||
* 4 | segments - 1 | paths
|
||||
*/
|
||||
private static computeArcNodes;
|
||||
/**
|
||||
* geneate line with connecting start index to end index
|
||||
* for example:
|
||||
* input: 0, 3
|
||||
* output: [0,1,1,2,2,3]
|
||||
* @param startIndex start index
|
||||
* @param endIndex end index
|
||||
* @returns line array connect start index to end index
|
||||
*/
|
||||
private static generateArcLine;
|
||||
/**
|
||||
* 使用多个圆环创建面和圆环线
|
||||
* loops length | first loop length | result surface
|
||||
* --------------------------------------------------
|
||||
* 1 | 1 | point -> error
|
||||
* 1 | >= 3 | plane -> circle plane
|
||||
* 2 | 1 | circle plane or cone surface
|
||||
* 2 | === another | torus plane or cone plane
|
||||
* >2 | === others | torus surface
|
||||
* @param loops 圆环数组
|
||||
* @param poly 添加到的目标PolyGeometry
|
||||
* @param lineMode
|
||||
* 0: don't add lines
|
||||
* 1: add first loop lines
|
||||
* 2: add last loop lines
|
||||
* 3. add first and last loop lines
|
||||
* @param close 是否形成闭环,头尾相接
|
||||
* @param z true: the surface normal direction is z positive; false: the surface normal direction is z negative
|
||||
*/
|
||||
private static addSurfaceByLoops;
|
||||
/**
|
||||
* 球或空心球的2D草图
|
||||
* @param r1 大圆半径
|
||||
* @param r2 小圆半径 为0时球,大于0时空心球 大于等于r1时报错
|
||||
* @param center 圆心
|
||||
* @param quarter false: full ; true: 1/4 circle
|
||||
* @param segments 一个圆的分段数
|
||||
* @param csys 坐标系
|
||||
*/
|
||||
static sphere2D(r1: number, r2: number, center?: SupVector, quarter?: boolean, segments?: number, csys?: Coordinate | undefined): PolyGeometry;
|
||||
/**
|
||||
* 构造圆环的草图
|
||||
* @param radius 截面半径
|
||||
* @param center 截面圆心
|
||||
* @param quarter false: full quarter: 半圆
|
||||
* @param segments 整圆分段数
|
||||
* @param csys 坐标系
|
||||
* @returns
|
||||
*/
|
||||
static torus2D(radius: number, center: SupVector, quarter?: boolean, segments?: number, csys?: Coordinate | undefined): PolyGeometry;
|
||||
/**
|
||||
* 根据点 和 anchors 创建 2D 轮廓图形
|
||||
* @param poly 目标PolyGeometry
|
||||
* @param nodes 一些点坐标
|
||||
* @param anchors 锚点,创建线的起止点
|
||||
*/
|
||||
private static create2DShape;
|
||||
/**
|
||||
* R1
|
||||
* |<---> __ ←-
|
||||
* | r2 / \ r1 p1↑ 点创建顺序
|
||||
* | | |
|
||||
* | | |
|
||||
* | | |
|
||||
* | r3 \__/ r4
|
||||
* |<--------->
|
||||
* pos R2
|
||||
*
|
||||
* @param R1 小圆半径
|
||||
* @param R2 大圆半径
|
||||
* @param height 高度
|
||||
* @param position 旋转轴位置
|
||||
* @param radii 倒角半径 4 个
|
||||
* @param modes 倒角类型 4个
|
||||
* @param quarter 是否 1/4
|
||||
* @param segments 倒圆角的时候圆弧的分辨率
|
||||
* @returns nodes 和 anchors 和 middle( 上 与 下 的 分界点)
|
||||
*/
|
||||
private static cylinder2DNodes;
|
||||
static cylinder(R1: number, R2: number, height: number, position?: SupVector, radii?: NumberArray4, modes?: CRModeTypeArray4, segments?: number, revolution?: boolean, range?: [number, number], quarter?: boolean, csys?: Coordinate | undefined): PolyGeometry;
|
||||
/**
|
||||
* |
|
||||
* | _________ ——
|
||||
* | r2 / \ r1 p0 ↑
|
||||
* | | ·p | y
|
||||
* | r3 \_________/ r4 ↓
|
||||
* |pos |<--------->| ——
|
||||
* | x
|
||||
* |————————————————————————
|
||||
* @param x box x direction length
|
||||
* @param y box y direction length
|
||||
* @param position box center position
|
||||
* @param radii r1, r2, r3, r4
|
||||
* @param modes m1, m2, m3, m4
|
||||
* @param segments one arc segments( Π / 2)
|
||||
*/
|
||||
private static box2DNodes;
|
||||
/**
|
||||
* 创建的基本体的顶面或者底面的平面
|
||||
* @param poly 目标PolyGeometry
|
||||
* @param nodes 一圈平面点
|
||||
* @param anchors 锚点,创建线的起止点
|
||||
* @param z true: 法向z正方形 false: 法向为z的负方向
|
||||
*/
|
||||
private static addOneTopOrBottomPlane;
|
||||
/**
|
||||
* 添加一个侧面,可能是一个平面或者一个圆柱面,平面由4个点,2个三角形组成,圆柱面由2n个点,2n个三角形组成
|
||||
* @param poly 目标PolyGeometry
|
||||
* @param nodes1 底面的点
|
||||
* @param nodes2 顶面的点
|
||||
*/
|
||||
private static addOneSideSurface;
|
||||
/**
|
||||
* 复杂的box, 可对4个角进行倒角
|
||||
* @param x x方向长度
|
||||
* @param y y方向长度
|
||||
* @param z z方向长度
|
||||
* @param position 底面圆心位置
|
||||
* @param radii 4个角的倒角半径
|
||||
* @param modes 4个角的倒角模式
|
||||
* @param segments Math.PI / 4 的分段数
|
||||
* @param extrude 是否extrude , false返回2D图形
|
||||
* @param csys 坐标系
|
||||
* @returns PolyGeometry
|
||||
*/
|
||||
static complexBox(x: number, y: number, z: number, position?: SupVector, radii?: NumberArray4, modes?: CRModeTypeArray4, segments?: number, extrude?: boolean, csys?: Coordinate | undefined): PolyGeometry;
|
||||
/**
|
||||
* 构造正多边形棱柱
|
||||
* @param radius 外接圆半径
|
||||
* @param height 高度
|
||||
* @param edgeNumber 边数量
|
||||
* @param startAngle 起始点位置
|
||||
* @param bottomCenter 底面圆心位置
|
||||
* @param extrude true: 拉伸的3D模型 false: 未拉伸的2D轮廓
|
||||
* @param csys 坐标系
|
||||
* @returns poly geometry
|
||||
*/
|
||||
static regularPolygonalPrism(radius: number, height: number, edgeNumber: number, startAngle?: number, bottomCenter?: SupVector, extrude?: boolean, csys?: Coordinate | undefined): PolyGeometry;
|
||||
}
|
||||
export {};
|
||||
9
app/supreium_viewer/package/dist/types/src/cad/geom/Point.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Coord } from './Coord';
|
||||
import { SupMat4 } from '../types';
|
||||
/**
|
||||
* @brief 点对象
|
||||
*
|
||||
*/
|
||||
export declare class Point extends Coord {
|
||||
applayMatrix4(mat: SupMat4): void;
|
||||
}
|
||||
49
app/supreium_viewer/package/dist/types/src/cad/geom/PolyGeometry.d.ts
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Ray } from 'three';
|
||||
import { Triangle } from './Triangulation';
|
||||
import { SupBox, SupVector } from '../types';
|
||||
import { AABBTree } from './AABB/AABBTree';
|
||||
export type PolyLine = {
|
||||
indices: number[];
|
||||
isSegment: boolean;
|
||||
props?: object;
|
||||
};
|
||||
export type PolySurf = {
|
||||
triangles: Triangle[];
|
||||
props?: object;
|
||||
};
|
||||
export interface IPolyGeometry {
|
||||
nodes: Array<[number, number, number]>;
|
||||
surfs?: Array<{
|
||||
props?: object;
|
||||
triangles: Array<[number, number, number]>;
|
||||
}>;
|
||||
lines?: Array<{
|
||||
props?: object;
|
||||
indices: Array<[number, number]> | number[];
|
||||
}>;
|
||||
}
|
||||
/**
|
||||
* @brief poly几何,可以包含多个多边形面和多边形边
|
||||
*
|
||||
*/
|
||||
export declare class PolyGeometry {
|
||||
readonly nodes: SupVector[];
|
||||
readonly lines: PolyLine[];
|
||||
readonly surfs: PolySurf[];
|
||||
constructor(nodes?: SupVector[]);
|
||||
static createIndex(poly: PolyLine): number[];
|
||||
bndBox(): SupBox;
|
||||
static createFromIPoly(data: IPolyGeometry): PolyGeometry;
|
||||
static ray2Tree(ray: Ray, tree: AABBTree): number;
|
||||
/**
|
||||
* AABB Tree is used to calculate the distance between two PolyGeometrys
|
||||
* @param geomFrom PolyGeometry
|
||||
* @param geomTo PolyGeometry
|
||||
* @param direction The direction from geomFrom to geomTo
|
||||
* @returns The distance from geomFrom to geomTo calculated along the direction.
|
||||
* If the return value is negative, it represents the distance from geomFrom to geomTo in the opposite direction.
|
||||
* If the return value is positive, it represents the distance from geomFrom to geomTo in the positive direction.
|
||||
* If the return value is null, it means that geomFrom and geomTo cannot calculate the distance in this direction.
|
||||
*/
|
||||
static calDistancewithAABB(geomFrom: PolyGeometry, geomTo: PolyGeometry, direction: SupVector): number | null;
|
||||
}
|
||||
14
app/supreium_viewer/package/dist/types/src/cad/geom/Polygon.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Point } from './Point';
|
||||
import { SupBox } from '../types';
|
||||
/**
|
||||
* @brief 多边形对象
|
||||
*
|
||||
*/
|
||||
export declare class Polygon {
|
||||
protected nodes: Point[];
|
||||
constructor();
|
||||
get nodeList(): Point[];
|
||||
set nodeList(nodes: Point[]);
|
||||
static createIndex(size: number, offset: number): number[];
|
||||
bndBox(): SupBox;
|
||||
}
|
||||
17
app/supreium_viewer/package/dist/types/src/cad/geom/SurfaceType.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @brief 曲面类型
|
||||
*
|
||||
*/
|
||||
export declare enum SurfaceType {
|
||||
Plane = 0,//! 平面
|
||||
Cylinder = 1,//! 圆柱面
|
||||
Cone = 2,//! 圆锥面
|
||||
Sphere = 3,//! 球面
|
||||
Torus = 4,//! 圆环面
|
||||
BezierSurface = 5,//! 贝塞尔曲面
|
||||
BSplineSurface = 6,//! Nurbs曲面
|
||||
SurfaceOfRevolution = 7,//! 回转面
|
||||
SurfaceOfExtrusion = 8,//! 拉伸面
|
||||
OffsetSurface = 9,//! 偏执面
|
||||
OtherSurface = 10
|
||||
}
|
||||