Browse Source

Merge remote-tracking branch 'origin/master'

master
修宁 7 months ago
parent
commit
99d8e38548
  1. 17
      .editorconfig
  2. 10
      src/components/Model3DView.vue
  3. 283
      src/components/data-form/DataForm.vue
  4. 69
      src/components/data-form/DataFormConstant.ts
  5. 276
      src/components/data-form/DataFormTypes.ts
  6. 37
      src/components/data-form/DataFormUtils.tsx
  7. 1
      src/core/base/IMeta.ts
  8. 83
      src/editor/widgets/property/PropertyView.vue
  9. 63
      src/modules/gstore/GstoreRenderer.ts
  10. 11
      src/pages/DataForm01.vue
  11. 52
      src/router/index.ts
  12. 227
      src/utils/Utils.ts
  13. 11
      tsconfig.app.json
  14. 14
      types/basics.d.ts

17
.editorconfig

@ -0,0 +1,17 @@
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 240
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

10
src/components/Model3DView.vue

@ -17,6 +17,7 @@
</el-upload>
<el-button @click="addConveyor">添加输送线</el-button>
<el-button @click="createShelf">添加货架</el-button>
<el-button @click="createGroundStore">添加地堆</el-button>
<div class="demo-color-block">
<span class="demonstration">材质颜色</span>
<el-color-picker v-model="restate.targetColor" />
@ -378,6 +379,15 @@ function createShelf(){//创建货架
})
}
function createGroundStore() {
const planeGeometry = new THREE.PlaneGeometry(1, 1);
const material = new THREE.MeshBasicMaterial({
color: 0x00ff00,
side: THREE.DoubleSide // :ml-citation{ref="5,8" data="citationList"}
});
const planeMesh = new THREE.Mesh(planeGeometry, material);
scene.add(planeMesh);
}
function initThree() {
const viewerDom = canvasContainer.value

283
src/components/data-form/DataForm.vue

@ -0,0 +1,283 @@
<script setup lang="ts">
import lodash from "lodash";
import { getCurrentInstance, reactive } from "vue";
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
import { Expression, Typeof } from "@ease-forge/shared";
import { calcBreakpointValue, toVNode } from "@/utils/Utils.ts";
import { type DataFormData, type DataFormItem, type DataFormProps, type DataFormState, type FormData, type FormField } from "./DataFormTypes.ts";
import { dataFormInputComponents } from "./DataFormConstant.ts";
import { dataPathToNamePath } from "./DataFormUtils.tsx";
defineOptions({
name: 'DataForm',
});
//
const emit = defineEmits<{
/** loading变化事件 */
loadingChange: [loading: boolean];
/** 表单字段变化 */
// fieldsChange: [changedFields: Array<FieldData>, allFields: Array<FieldData>];
/** 表单值变化 */
valuesChange: [changedValues: any, values: any];
}>();
//
const slots = defineSlots<{
/** 提交区域插槽 */
submit?: (props?: any) => Array<VueNode>;
}>();
//
const instance = getCurrentInstance();
// props
const props = withDefaults(defineProps<DataFormProps>(), {
name: () => lodash.uniqueId("data_form_"),
defColumnCount: 2,
layout: "onlyLabelFixed",
labelWidth: "120px",
formItemHeightMode: "mini",
components: () => dataFormInputComponents,
});
// state
const state = reactive<DataFormState>({
loading: false,
data: props.data,
columnCount: calcColumnCount(props.columnCount),
dataFormItems: toDataFormItems(props.formFields, props.data),
});
//
const data: DataFormData = {
firstDataChange: true,
oldData: {},
formRef: null,
inputRefs: {},
ctxData: {},
};
//
const breakpoints = useBreakpoints(props.breakpoints ?? breakpointsTailwind);
/** bothFixed:label和input都固定宽度布局 */
function isBothFixed() {
return props.layout === "bothFixed";
}
/**
* Array<FormField> 装换成 Array<DataFormItem>
* @param formFields 表单字段配置
* @param formData 表单数据
*/
function toDataFormItems(formFields?: Array<FormField>, formData?: FormData) {
if (!formFields) return [];
return formFields.map(formField => toDataFormItem(formField, formData));
}
/**
* FormField 装换成 DataFormItem
* @param formField 表单字段配置
* @param formData 表单数据
*/
function toDataFormItem(formField: FormField, formData?: FormData) {
const { dataPath, widthCount, label, input, inputProps, format, transform, extInputs, hidden, watchValues, rawProps } = formField;
const item: DataFormItem = {
widthCount: widthCount ?? 1,
inputRef: dataPath?.replaceAll(/[.\[\]]/g, '_') + '_' + lodash.uniqueId(),
formItemProps: { ...rawProps },
dataPath,
format,
transform,
hidden,
};
if (dataPath) {
item.formItemProps.name = dataPathToNamePath(dataPath);
} else {
item.formItemProps.autoLink = false;
}
if (dataPath && Typeof.isFunction(label)) {
const dataValue = Expression.getKeyPathValue(dataPath, formData);
const vnode = label(dataValue, dataPath, formData);
item.formItemProps.label = toVNode(vnode);
} else if (label) {
item.formItemProps.label = label;
}
if (input) {
item.input = resolveInputComponent(input);
if (item.input && inputProps) {
item.inputProps = inputProps;
}
}
if (Typeof.isArray(watchValues)) {
item.watchValues = watchValues;
} else if (watchValues) {
item.watchValues = [watchValues];
}
// formItemProps
const { style, class: className, labelWidth, labelPosition, showMessage, inlineMessage, rules, required } = formField;
if (Typeof.hasValue(style)) item.formItemProps.style = style;
if (Typeof.hasValue(className)) item.formItemProps.class = className;
if (Typeof.hasValue(labelWidth)) item.formItemProps.labelWidth = labelWidth;
if (Typeof.hasValue(labelPosition)) item.formItemProps.labelPosition = labelPosition;
if (Typeof.hasValue(showMessage)) item.formItemProps.showMessage = showMessage;
if (Typeof.hasValue(inlineMessage)) item.formItemProps.inlineMessage = inlineMessage;
if (Typeof.hasValue(rules)) item.formItemProps.rules = rules;
if (Typeof.hasValue(required)) item.formItemProps.required = required;
// TODO bothFixed widthCount > 1
if (widthCount && widthCount > 1 && isBothFixed()) {
// if (!item.formItemProps.wrapperCol) item.formItemProps.wrapperCol = {};
// if (!item.formItemProps.wrapperCol.style) item.formItemProps.wrapperCol.style = {};
// const width = `calc((${props.labelWidth} + ${props.inputWidth}) * ${widthCount - 1} + ${props.inputWidth})`;
// if (!item.formItemProps.wrapperCol.style.width) item.formItemProps.wrapperCol.style.width = width;
// if (!item.formItemProps.wrapperCol.style.flex) item.formItemProps.wrapperCol.style.flex = `0 0 ${width}`;
}
// extInputs
if (Typeof.isArray(extInputs)) {
item.extInputs = extInputs.map(extInput => toDataFormItem(extInput, formData));
}
return item;
}
/** 获取 input vue组件 */
function resolveInputComponent(input: any) {
if (!Typeof.isStr(input)) return input;
const components = props.components ?? {};
const cmp = components[input];
if (!cmp) console.warn("input组件未注册:", input);
return cmp;
}
/**
* 根据响应式断点配置计算一行显示的字段数量
* @param columnCount 响应式断点配置
* @param current 当前响应式断点值
*/
function calcColumnCount(columnCount?: DataFormProps["columnCount"], current?: Array<string>): number {
return Math.min(calcBreakpointValue(columnCount, current, props.defColumnCount ?? 2), 24);
}
interface DataFormExpose {
state: DataFormState;
data: DataFormData;
}
const expose: DataFormExpose = {
state,
data,
};
//
defineExpose(expose);
export type {
DataFormProps,
DataFormState,
}
</script>
<template>
<div class="data-form">
</div>
</template>
<style scoped>
.data-form-item.data-form-item-flex-1 {
flex: 1 1 0;
}
.data-form-item.data-form-item-flex-2 {
flex: 2 1 0;
}
.data-form-item.data-form-item-flex-3 {
flex: 3 1 0;
}
.data-form-item.data-form-item-flex-4 {
flex: 4 1 0;
}
.data-form-item.data-form-item-flex-5 {
flex: 5 1 0;
}
.data-form-item.data-form-item-flex-6 {
flex: 6 1 0;
}
.data-form-item.data-form-item-flex-7 {
flex: 7 1 0;
}
.data-form-item.data-form-item-flex-8 {
flex: 8 1 0;
}
.data-form-item.data-form-item-flex-9 {
flex: 9 1 0;
}
.data-form-item.data-form-item-flex-10 {
flex: 10 1 0;
}
.data-form-item.data-form-item-flex-11 {
flex: 11 1 0;
}
.data-form-item.data-form-item-flex-12 {
flex: 12 1 0;
}
.data-form-item.data-form-item-flex-13 {
flex: 13 1 0;
}
.data-form-item.data-form-item-flex-14 {
flex: 14 1 0;
}
.data-form-item.data-form-item-flex-15 {
flex: 15 1 0;
}
.data-form-item.data-form-item-flex-16 {
flex: 16 1 0;
}
.data-form-item.data-form-item-flex-17 {
flex: 17 1 0;
}
.data-form-item.data-form-item-flex-18 {
flex: 18 1 0;
}
.data-form-item.data-form-item-flex-19 {
flex: 19 1 0;
}
.data-form-item.data-form-item-flex-20 {
flex: 20 1 0;
}
.data-form-item.data-form-item-flex-21 {
flex: 21 1 0;
}
.data-form-item.data-form-item-flex-22 {
flex: 22 1 0;
}
.data-form-item.data-form-item-flex-23 {
flex: 23 1 0;
}
.data-form-item.data-form-item-flex-24 {
flex: 24 1 0;
}
</style>

69
src/components/data-form/DataFormConstant.ts

@ -0,0 +1,69 @@
import { markRaw } from "vue";
import {
ElAutocomplete,
ElCascader,
ElCheckbox,
ElColorPicker,
ElDatePicker,
ElInput,
ElInputNumber,
ElInputTag,
ElMention,
ElRadio,
ElRate,
ElSelect,
ElSelectV2,
ElSlider,
ElSwitch,
ElTimePicker,
ElTimeSelect,
ElTransfer,
ElTreeSelect,
ElUpload,
} from "element-plus";
import type { DisplayMode } from "./DataFormTypes.ts";
/** 内建的表单输入组件 */
const builtInInputComponents = {
Autocomplete: markRaw<any>(ElAutocomplete),
Cascader: markRaw<any>(ElCascader),
Checkbox: markRaw<any>(ElCheckbox),
ColorPicker: markRaw<any>(ElColorPicker),
DatePicker: markRaw<any>(ElDatePicker),
Input: markRaw<any>(ElInput),
InputNumber: markRaw<any>(ElInputNumber),
InputTag: markRaw<any>(ElInputTag),
Mention: markRaw<any>(ElMention),
Radio: markRaw<any>(ElRadio),
Rate: markRaw<any>(ElRate),
Select: markRaw<any>(ElSelect),
SelectV2: markRaw<any>(ElSelectV2),
Slider: markRaw<any>(ElSlider),
Switch: markRaw<any>(ElSwitch),
TimePicker: markRaw<any>(ElTimePicker),
TimeSelect: markRaw<any>(ElTimeSelect),
Transfer: markRaw<any>(ElTransfer),
TreeSelect: markRaw<any>(ElTreeSelect),
Upload: markRaw<any>(ElUpload),
};
type BuiltInInput = keyof typeof builtInInputComponents;
const dataFormInputComponents: Record<BuiltInInput | string, any> = markRaw<any>(builtInInputComponents);
/** DisplayMode 的默认值 */
const defDisplayMode: DisplayMode = {};
export type {
BuiltInInput,
};
export default {
dataFormInputComponents,
defDisplayMode,
}
export {
dataFormInputComponents,
defDisplayMode,
}

276
src/components/data-form/DataFormTypes.ts

@ -0,0 +1,276 @@
import { type ComponentInternalInstance, type CSSProperties } from "vue";
import { type Breakpoints, breakpointsTailwind } from "@vueuse/core";
import { type FormItemProps, type FormProps } from "element-plus";
/** 表单数据类型 */
type FormData = Record<string, any> | Array<any>;
/** 字段标题 */
type Label = VueNode | ((value: any, dataPath: string, formData?: FormData) => VueNode);
/** 表单数据绑定到表单组件上时的格式化操作 */
type ValueFormat = DictGroup | ((value: any, dataPath: string, formData?: FormData) => any);
/** 表单组件值更新到表单数据时的数据转换 */
type ValueTransform = DictGroup | ((value: any, dataPath: string, formData?: FormData) => any);
/** 前缀、后缀的自定义组件配置 */
interface PrefixSuffixComponent {
/** 禁用 */
disabled?: boolean;
/** 组件名或组件对象 */
component?: any;
/** 组件props */
props?: Record<string, any>;
/** 自定义渲染逻辑 */
render?: (value: any) => VueNode;
}
/** 显示模式 */
interface DisplayMode {
/** 禁用 */
disabled?: boolean;
/** 定义样式 */
style?: CSSProperties;
/** 自定义class样式 */
class?: string;
/** 数据格式显示 */
format?: (value: any) => string;
/** 自定义渲染逻辑 */
render?: (value: any) => VueNode;
}
/** 表单字段输入组件通用的props */
interface FormInputBaseProps {
/** 定义样式 */
style?: CSSProperties;
/** 自定义class样式 */
class?: string;
/** 占位符 */
placeholder?: string;
/** 允许清空 */
allowClear?: boolean;
/** 只读 */
readonly?: boolean;
/** 禁用 */
disabled?: boolean;
/** 隐藏 */
hidden?: boolean;
/** 前缀区域的自定义组件 */
prefixConfig?: PrefixSuffixComponent;
/** 后缀区域的自定义组件 */
suffixConfig?: PrefixSuffixComponent;
/** 前缀区域已经后缀区域的包装容器的样式 */
preSufWrapStyle?: CSSProperties;
/** 显示模式 */
displayMode?: DisplayMode | true;
// onFocus
// onBlur
// onChange
// onPressEnter
[key: string]: any;
}
/** 监听表单字段值 */
interface WatchFormFieldValue<V = any> {
/** 数据路径,如: "userName"、"user.age"、"user.hobby.[1].name"、"[2].id"*/
dataPath: string
/**
*
* @param oldValue
* @param newValue
* @param data
* @param formItem (ui更新)
* @param input
* @param from Form组件实例
* @param ctxData
*/
onChange: (
oldValue: V,
newValue: V,
formData: any,
formItem: Pick<DataFormItem, "widthCount" | "inputProps" | "formItemProps" | "hidden">,
input: any,
from: any,
ctxData: DataFormData["ctxData"],
) => void
/** 在开始监听时立即触发回调 */
immediate?: boolean
}
/** 表单字段,一般包含一个 label 和一个 input */
interface FormField {
// 核心配置
/** 数据路径,如: "userName"、"user.age"、"user.hobby.[1].name"、"[2].id" */
dataPath?: string;
/** 宽度占用列数 */
widthCount?: number;
/** 字段标题 */
label?: Label;
/** 字段输入组件,组件名或者组件对象 */
input?: any;
/** 字段输入组件props */
inputProps?: FormInputBaseProps;
/** 表单数据绑定到表单组件上时的格式化操作 */
format?: ValueFormat;
/** 表单组件值更新到表单数据时的数据转换 */
transform?: ValueTransform;
/** 当字段输入组件有多个时,扩展的输入组件 */
extInputs?: Array<Omit<FormField, "extInputs">>;
/** 是否隐藏当前字段 */
hidden?: boolean;
/** 监听表单字段值变化,修改当前表单字段状态 */
watchValues?: WatchFormFieldValue | Array<WatchFormFieldValue>;
// 扩展配置
/** 定义样式 */
style?: CSSProperties;
/** 自定义class样式 */
class?: string;
/** label宽度 */
labelWidth?: string | number;
/** label标签的位置 */
labelPosition?: "left" | "right"
/** 是否显示校验错误信息 */
showMessage?: boolean;
/** 是否在行内显示校验信息 */
inlineMessage?: string | boolean;
/** 表单验证规则 */
rules?: FormItemProps["rules"];
/** 是否必填,如不设置,则会根据校验规则自动生成 */
required?: boolean;
/** 原始的 Form.Item 属性 */
rawProps?: FormItemProps;
}
/** 运行时的表单项 */
interface DataFormItem {
/** 表单项占用列数 */
widthCount: number;
/** 输入组件ref */
inputRef: string;
/** 输入组件 */
input?: VueComponent;
/** 输入组件props */
inputProps?: FormInputBaseProps;
/** 扩展的输入组件 */
extInputs?: Array<Omit<DataFormItem, "extInputs">>;
/** 需要使用的 FormItem 属性 */
formItemProps: Record<keyof FormItemProps | string, any>;
/** 数据路径,如: "userName"、"user.age"、"user.hobby.[1].name"、"[2].id" */
dataPath?: string;
/** 表单数据绑定到表单组件上时的格式化操作 */
format?: ValueFormat;
/** 表单组件值更新到表单数据时的数据转换 */
transform?: ValueTransform;
/** 是否隐藏当前表单项 */
hidden?: boolean;
/** 监听表单字段值变化 */
watchValues?: Array<WatchFormFieldValue>;
}
// 定义 Props 类型
interface DataFormProps {
// 核心配置
/** 表单名称,会作为表单字段 id 前缀使用 */
name?: string;
/** 表单数据 */
data?: FormData;
/** 服务端数据API */
dataApi?: HttpRequestConfig<FormData>;
/** 自动使用"dataApi"配置加载服务端数据 */
autoLoadData?: boolean;
/** 表单字段 */
formFields?: Array<FormField>;
/** 响应式断点配置 */
breakpoints?: Breakpoints;
/** 默认一行显示的字段数量 */
defColumnCount?: number;
/** 一行显示的字段数量,支持响应式断点配置,最大值:24 */
columnCount?: number | Record<(keyof typeof breakpointsTailwind) | string, number>;
/** 表单布局:bothFixed:label和input都固定宽度;onlyLabelFixed:仅label固定宽度 */
layout?: "bothFixed" | "onlyLabelFixed";
/** label标签的位置 */
labelPosition?: "left" | "right";
/** 表单域标签的后缀 */
labelSuffix?: string;
/** label宽度 */
labelWidth?: string | number;
/** value宽度,仅layout=bothFixed时有效 */
inputWidth?: string | number;
/** submit 区域 FormItem 的 Props 配置 */
submitFormItemProps?: Record<keyof FormItemProps | string, any>;
// 扩展配置
/** 表单字段大小 */
size?: "" | "small" | "default" | "large";
/** 是否显示校验错误信息 */
showMessage?: boolean;
/** 是否在行内显示校验信息 */
inlineMessage?: boolean;
/** 是否显示校验错误信息 */
requireAsteriskPosition?: "left" | "right";
/** 隐藏所有表单项的必选标记 */
hideRequiredAsterisk?: boolean;
/** 表单验证规则 */
rules?: FormProps["rules"];
/** 是否在 rules 属性改变后立即触发一次验证 */
validateOnRuleChange?: boolean;
/** 设置表单组件禁用 */
disabled?: boolean;
/** 当校验失败时,滚动到第一个错误表单项 */
scrollToError?: boolean;
/** 当校验有失败结果时,滚动到第一个失败的表单项目 可通过 scrollIntoView 配置 */
scrollIntoViewOptions?: FormProps["scrollIntoViewOptions"];
/** 注册内置使用的组件 */
components?: Record<string, any>;
/** 原始的 Form 属性 */
rawProps?: FormProps;
}
// 定义 State 类型
interface DataFormState {
/** 是否是加载中 */
loading: boolean;
/** 数据值 */
data?: FormData;
/** 表格列数 */
columnCount: number;
/** 表单项数组 */
dataFormItems: Array<DataFormItem>;
}
// 定义 Data 类型
interface DataFormData {
/** 表单数据是否是第一次变化 */
firstDataChange: boolean;
/** 表单旧数据,Record<dataPath, dataValue> */
oldData: Record<string, any>;
/** 表单组件的引用 */
formRef: any;
/** 输入组件的引用 */
inputRefs: Record<string, any>;
/** 表单上下文数据 */
ctxData: {
/** 当前 DataForm 组件实例 */
instance?: ComponentInternalInstance;
/** 其它属性 */
[key: string]: any;
};
}
export type {
FormData,
Label,
ValueFormat,
ValueTransform,
PrefixSuffixComponent,
DisplayMode,
FormInputBaseProps,
WatchFormFieldValue,
FormField,
DataFormItem,
DataFormProps,
DataFormState,
DataFormData,
}

37
src/components/data-form/DataFormUtils.tsx

@ -0,0 +1,37 @@
import lodash from "lodash";
import { Typeof } from "@ease-forge/shared";
/**
* antd NamePath
* @param dataPath : "userName""user.age""user.hobby.[1].name""[2].id"
*/
function dataPathToNamePath(dataPath: string): string | number | Array<string | number> {
dataPath = lodash.trim(dataPath);
const paths = dataPath.split(".");
const namePath: Array<string | number> = [];
for (let path of paths) {
path = lodash.trim(path);
let name: string | number = path;
if (path.startsWith("[") && path.endsWith("]")) {
name = lodash.toInteger(path.substring(1, path.length - 1));
if (!Typeof.isValidNumber(name)) {
name = path;
}
}
namePath.push(name);
}
if (namePath.length === 0) {
return dataPath;
} else if (namePath.length === 1) {
return namePath[0];
}
return namePath;
}
export default {
dataPathToNamePath,
}
export {
dataPathToNamePath,
}

1
src/core/base/IMeta.ts

@ -44,6 +44,7 @@ export type IMeta = MetaItem[]
export interface MetaItem {
field?: string;
editor: string;
editorProps?: any;
label?: string;
readonly?: boolean;
category?: string;

83
src/editor/widgets/property/PropertyView.vue

@ -11,36 +11,48 @@
</span>
</div>
<div class="calc-right-panel">
<el-form label-position="right" label-width="60" class="property-panel-form" size="default" @submit.native.prevent>
<el-form
label-position="right"
label-width="60"
class="property-panel-form"
size="default"
@submit.native.prevent
>
<template v-for="(itemMeta, idx) in selectedObjectMeta">
<el-divider v-if="itemMeta.editor === '-'" />
<TextInput v-else-if="itemMeta.editor === 'TextInput'"
:prop="itemMeta" :viewport="viewport" />
<TextInput
v-else-if="itemMeta.editor === 'TextInput'"
:prop="itemMeta"
:viewport="viewport"
/>
<Transform v-else-if="itemMeta.editor === 'TransformEditor'"
:prop="itemMeta" :viewport="viewport" />
<Transform
v-else-if="itemMeta.editor === 'TransformEditor'"
:prop="itemMeta"
:viewport="viewport"
/>
<SwitchItem v-else-if="itemMeta.editor === 'Switch'"
:prop="itemMeta" :viewport="viewport" />
<SwitchItem
v-else-if="itemMeta.editor === 'Switch'"
:prop="itemMeta"
:viewport="viewport"
/>
<ColorItem v-else-if="itemMeta.editor === 'Color'"
:prop="itemMeta" :viewport="viewport" />
<ColorItem v-else-if="itemMeta.editor === 'Color'" :prop="itemMeta" :viewport="viewport" />
<UUIDItem v-else-if="itemMeta.editor === 'UUID'"
:prop="itemMeta" :viewport="viewport" />
<UUIDItem v-else-if="itemMeta.editor === 'UUID'" :prop="itemMeta" :viewport="viewport" />
<NumberInput v-else-if="itemMeta.editor === 'Number'"
:prop="itemMeta" :viewport="viewport" />
<NumberInput
v-else-if="itemMeta.editor === 'Number'"
:prop="itemMeta"
:viewport="viewport"
/>
<template v-else>
未知编辑器: {{ itemMeta.editor }}
</template>
<template v-else> 未知编辑器: {{ itemMeta.editor }} </template>
</template>
</el-form>
</div>
</template>
<script>
import IWidgets from '../IWidgets.js'
@ -50,6 +62,7 @@ import SwitchItem from '../../propEditors/SwitchItem.vue'
import ColorItem from '../../propEditors/ColorItem.vue'
import UUIDItem from '../../propEditors/UUIDItem.vue'
import NumberInput from '../../propEditors/NumberInput.vue'
import EventBus from '../../../runtime/EventBus.js'
export default {
name: 'PropertyView',
@ -75,17 +88,35 @@ export default {
selectedObjectMeta() {
return this.state?.selectedObjectMeta
}
}
},
methods: {
selectedObjectChanged(state) {
const data = state.selectedItem;
console.log("selectedObjectChanged data", data)
this.viewport.stateManager.beginStateUpdate()
const item = _.find(this.viewport.stateManager.vdata.items, item=>item.id === data.id)
// item.tf[0][0] = item.tf[0][0] / 2;
console.log("selectedObjectChanged item", item)
// _.extend(item, data)
this.viewport.stateManager.endStateUpdate()
},
},
mounted() {
EventBus.on("selectedObjectChanged", this.selectedObjectChanged);
},
unmounted() {
EventBus.off("selectedObjectChanged", this.selectedObjectChanged);
},
}
</script>
<style lang="less">
.property-panel-form{
.property-panel-form {
margin: 0;
font-size: 14px;
color:#606266;
.el-form-item--default{
margin:5px 3px 0 0;
.el-form-item__label{
color: #606266;
.el-form-item--default {
margin: 5px 3px 0 0;
.el-form-item__label {
height: 20px;
line-height: 22px;
}
@ -129,13 +160,13 @@ export default {
.el-input__wrapper {
background-color: #efefef;
box-shadow: none
box-shadow: none;
}
}
}
}
}
.el-divider{
.el-divider {
margin: 5px 0;
}
}

63
src/modules/gstore/GstoreRenderer.ts

@ -1,15 +1,14 @@
import * as THREE from 'three'
import BaseRenderer from '@/core/base/BaseRenderer.ts'
import { Text } from 'troika-three-text'
import MoveLinePointPng from '@/assets/images/moveline_point.png'
import SimSunTTF from '@/assets/fonts/simsunb.ttf'
import { getLineId } from '@/core/ModelUtils.ts'
import { Line2 } from 'three/examples/jsm/lines/Line2.js'
import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry.js'
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js'
/**
*
*
*/
export default class GstoreRenderer extends BaseRenderer {
static POINT_NAME = 'way_point'
static POINT_NAME = 'ground_store'
pointMaterial: THREE.Material
@ -19,6 +18,7 @@ export default class GstoreRenderer extends BaseRenderer {
readonly defulePositionY: number = 0.5 // 默认点的高度, 0.01, 防止和地面重合
readonly defaultScale: THREE.Vector3 = new THREE.Vector3(1.5, 1.2, 0.1)
readonly defaultRotation: THREE.Vector3 = new THREE.Vector3(0, 0, 0)
readonly defaultLineWidth: number = 0.05
constructor(itemTypeName: string) {
super(itemTypeName)
@ -50,13 +50,56 @@ export default class GstoreRenderer extends BaseRenderer {
}
createPointBasic(item: ItemJson, option?: RendererCudOption): THREE.Object3D[] {
const obj = new THREE.Sprite(this.pointMaterial as THREE.SpriteMaterial)
obj.name = GstoreRenderer.POINT_NAME
return [obj]
// 创建平面几何体
const group = new THREE.Group()
group.name = GstoreRenderer.POINT_NAME
// 绘制背景矩形框
const planeGeometry = new THREE.PlaneGeometry(item.dt.storeWidth, item.dt.storeDepth);
planeGeometry.rotateX(-Math.PI / 2)
const planeMaterial = new THREE.MeshBasicMaterial({
color: 'white',
transparent: true, // 启用透明
opacity: 0.2, // 50%透明度
depthWrite: false, // 防止深度冲突
side: THREE.DoubleSide // 双面渲染:ml-citation{ref="5,8" data="citationList"}
});
const planeMesh = new THREE.Mesh(planeGeometry, planeMaterial);
group.add(planeMesh)
if (!item.dt.storeWidth || !item.dt.storeDepth) {
return [group]
}
// 绘制边框
const lineXLen = item.dt.storeWidth - this.defaultLineWidth
const lineYLen = item.dt.storeDepth - this.defaultLineWidth
const lineGeometry = new LineGeometry().setPositions([
-(lineXLen/2),-(lineYLen/2),0,
lineXLen/2,-(lineYLen/2),0,
lineXLen/2,lineYLen/2,0,
-(lineXLen/2),lineYLen/2,0,
-(lineXLen/2),-(lineYLen/2),0
]);
lineGeometry.rotateX(-Math.PI / 2)
const lineMaterial = new LineMaterial({
color: 0x00ff00,
linewidth: 0.05,
worldUnits: true,
resolution: new THREE.Vector2(window.innerWidth, window.innerHeight),
side: THREE.DoubleSide
});
//
const line = new Line2(lineGeometry, lineMaterial);
group.add(line as THREE.Object3D)
return [group]
}
dispose() {
super.dispose()
this.pointMaterial.dispose()
}
}
}

11
src/pages/DataForm01.vue

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
DataForm01.vue
</template>
<style scoped lang="less">
</style>

52
src/router/index.ts

@ -1,29 +1,35 @@
import { createRouter, createWebHashHistory } from 'vue-router'
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
// 自动引导到 /editor
redirect: '/editor'
},
{
path: '/editor',
name: 'editor',
// component: HomeView,
component: () => import('../editor/ModelMain.vue')
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/AboutView.vue')
}
]
history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
// 自动引导到 /editor
redirect: '/editor'
},
{
path: '/editor',
name: 'editor',
// component: HomeView,
component: () => import('../editor/ModelMain.vue')
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/AboutView.vue')
},
{
path: '/DataForm01',
name: 'DataForm01',
component: () => import('@/pages/DataForm01.vue'),
},
]
})
export default router

227
src/utils/Utils.ts

@ -0,0 +1,227 @@
import lodash from "lodash";
import { createTextVNode, createVNode, Fragment, isVNode, onMounted, onUnmounted, onUpdated, toRaw, type VNode } from "vue";
import { Format, Typeof } from "@ease-forge/shared";
import { type AnyFunction, type BaseProps } from "@ease-forge/runtime";
interface CreateVNode {
/**
* @param type html标签
* @param props
* @param children
*/
(type: any, props?: BaseProps, children?: Array<any> | Record<string, AnyFunction<any, VNode>>): VNode;
}
/** createVNode函数 */
const h: CreateVNode = createVNode as any;
/**
*
*/
function debugRender(tag: string = "", ...params: any[]) {
if (APP_INFO.mode === "production") return;
if (tag) tag = `[${tag}]: `;
const nowTime = () => Format.numberFormat(new Date().getTime() / 1000.0, "0.000");
onMounted(() => console.log(`${nowTime()} | ${tag}加载 onMounted`, ...params));
onUpdated(() => console.log(`${nowTime()} | ${tag}渲染 onUpdated`, ...params));
onUnmounted(() => console.log(`${nowTime()} | ${tag}卸载 onUnmounted`, ...params));
}
/**
* VNode使 <component :is="VNode"/>
*/
function toVNode(vueNode: VueNode, key?: any) {
let props: any = null;
if (Typeof.hasValue(key)) {
props = {};
props.key = lodash.toString(key);
}
if (isVNode(vueNode)) {
// VNode
return vueNode;
} else if (Typeof.noValue(vueNode)) {
// 空值
return createTextVNode();
} else if (Typeof.isArray(vueNode)) {
// 数组
const children = vueNode.map((node, idx) => toVNode(node, `idx_${idx}`));
return createVNode(Fragment, props, children);
} else if (Typeof.isFunction(vueNode)) {
// 函数
return toVNode(vueNode(), key);
} else if (Typeof.isBool(vueNode)) {
// 布尔值
return createTextVNode(lodash.toString(vueNode));
} else if (!Typeof.isObj(vueNode)) {
// 非对象
return createTextVNode(lodash.toString(vueNode));
} else {
// 其它值
return createVNode(Fragment, props, [vueNode]);
}
}
interface MergePropsObjOptions {
/** 表示空值 */
nullVal: any;
/** 表示未定义的值 */
unSetVal: any;
}
const defMergePropsObjOptions: MergePropsObjOptions = {
nullVal: "none",
unSetVal: "unset",
};
/**
* props
* @param propName prop
* @param props props
* @param res props
* @param options
*/
function mergePropsObj<Props = any>(propName: keyof Props, props: Props, res: any, options?: Partial<MergePropsObjOptions>) {
const ops: MergePropsObjOptions = lodash.defaultsDeep({}, options, defMergePropsObjOptions);
const propValue = props[propName];
// props 未配置
if (Typeof.noValue(propValue) || propValue === ops.unSetVal) return;
// props 配置空值
if (propValue === ops.nullVal) {
res[propName] = undefined;
return;
}
// 如果是对象就合并配置,否则直接覆盖
if (Typeof.isPlainObj(propValue) && !Typeof.isArray(propValue) && !Typeof.isDate(propValue)) {
res[propName] = lodash.defaultsDeep({}, propValue, res[propName]);
} else {
res[propName] = propValue;
}
}
/**
*
* @param obj
* @param funName
* @param fun
*/
function mergeFunction<T, K extends keyof T>(obj: T, funName: K, fun: T[K]) {
if (!Typeof.isFun(fun)) return;
const rawFun = obj[funName] as Function;
if (!rawFun) {
obj[funName] = fun;
return;
}
// const func:Function = fun;
if (Typeof.isFunction(rawFun)) {
obj[funName] = (function (this: any, ...args: any[]) {
if (Typeof.isAsyncFunction(fun)) {
fun.apply(this, args).then(() => rawFun.apply(this, args));
} else {
fun.apply(this, args);
return rawFun.apply(this, args);
}
}) as any;
} else if (Typeof.isAsyncFunction(rawFun)) {
obj[funName] = (async function (this: any, ...args: any[]) {
if (Typeof.isAsyncFunction(fun)) {
await fun.apply(this, args);
} else {
fun.apply(this, args);
}
return await rawFun.apply(this, args);
}) as any;
} else {
console.warn("mergeFunction 失败不支持的函数类型:", rawFun);
}
}
/**
* vue组件的expose对象sources中存在且expose中不存在的属性合并到expose对象
* @param expose
* @param sources
*/
function mergeExpose(expose: Record<string, any>, sources: any) {
if (!expose) return;
sources = toRaw(sources);
if (!sources || Typeof.isDate(sources) || Typeof.isArray(sources) || !Typeof.isObj(sources)) return;
// sources 是 dom 对象
if (sources instanceof HTMLElement || (sources.constructor?.name?.includes("HTML") && sources.constructor?.name?.includes("Element"))) {
expose.$el = sources;
return;
}
// 压制警告[Vue warn] Object.keys(sources)
const rawConsoleWarn = console.warn;
console.warn = _emptyFun;
let keys = Object.keys(sources);
console.warn = rawConsoleWarn;
// 处理 expose
for (let key of keys) {
const newValue = sources[key];
if (Typeof.noValue(newValue)) continue;
expose[key] = newValue;
}
// 处理 vue 组件内置属性
keys = ["$data", "$props", "$attrs", "$slots", "$refs", "$emit", "$on", "$off", "$once", "$forceUpdate", "$nextTick", "$watch", "$el", "$options", "$parent", "$root"];
for (let key of keys) {
const newValue = sources[key];
if (Typeof.noValue(newValue)) continue;
expose[key] = newValue;
}
}
// 空函数
function _emptyFun() {
}
/**
*
* @param breakpointsConfig
* @param current 当前响应式断点: useBreakpoints().current()
* @param defValue
*/
function calcBreakpointValue(breakpointsConfig: number | Record<string, number> | undefined, current: Array<string> | undefined, defValue: number): number {
if (Typeof.noValue(breakpointsConfig)) return defValue;
if (Typeof.isNum(breakpointsConfig)) return breakpointsConfig;
let minValue: number | undefined = undefined;
for (let key in breakpointsConfig) {
const value = breakpointsConfig[key];
if (minValue === undefined) {
minValue = value;
} else if (value < minValue) {
minValue = value;
}
}
if (minValue === undefined) return defValue;
if (!current || current.length <= 0) return minValue;
const breakpoint = current[current.length - 1];
if (!breakpoint) return defValue;
return breakpointsConfig?.[breakpoint] ?? defValue;
}
export type {
MergePropsObjOptions,
}
export default {
createVNode: h,
h,
debugRender,
toVNode,
defMergePropsObjOptions,
mergePropsObj,
mergeFunction,
mergeExpose,
calcBreakpointValue,
}
export {
createVNode,
h,
debugRender,
toVNode,
defMergePropsObjOptions,
mergePropsObj,
mergeFunction,
mergeExpose,
calcBreakpointValue,
}

11
tsconfig.app.json

@ -4,6 +4,7 @@
"module": "esnext",
"moduleResolution": "node",
"jsx": "preserve",
"jsxImportSource": "vue",
"strict": false,
"allowJs": true,
"checkJs": true,
@ -22,13 +23,8 @@
"baseUrl": ".",
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"lib": [
"es2018",
"es2017",
"es2016",
"es2015.promise",
"dom",
"scripthost",
"es5"
"DOM",
"ESNext"
],
"rootDirs": [
"./src"
@ -46,6 +42,7 @@
"include": [
"env.d.ts",
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"types/*.d.ts"
],

14
types/basics.d.ts

@ -0,0 +1,14 @@
import type { Component, DefineComponent, VNode } from 'vue'
declare global {
/** vue node 原子类型 */
type VueNodeAtom = VNode | string | number | boolean | Date | null | undefined | (() => VueNode)
/** vue node 类型,可使用 ComponentsUtils.toVNode(VueNode) 转换成 VNode 后直接渲染 */
type VueNode = VueNodeAtom | Array<VueNode | VueNodeAtom>
/** vue组件 */
type VueComponent = Component | DefineComponent
}
export {}
Loading…
Cancel
Save