feat: 实现拨动开关的数字孪生

This commit is contained in:
2025-08-16 16:01:10 +08:00
parent a2ac1bcb3b
commit b6720d867d
4 changed files with 481 additions and 30 deletions

View File

@@ -6936,6 +6936,255 @@ export class ResourceClient {
}
}
export class SwitchClient {
protected instance: AxiosInstance;
protected baseUrl: string;
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
constructor(baseUrl?: string, instance?: AxiosInstance) {
this.instance = instance || axios.create();
this.baseUrl = baseUrl ?? "http://127.0.0.1:5000";
}
/**
* 启用或禁用 Switch 外设
* @param enable (optional) 是否启用
* @return 操作结果
*/
setEnable(enable: boolean | undefined, cancelToken?: CancelToken): Promise<boolean> {
let url_ = this.baseUrl + "/api/Switch/enable?";
if (enable === null)
throw new Error("The parameter 'enable' cannot be null.");
else if (enable !== undefined)
url_ += "enable=" + encodeURIComponent("" + enable) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: AxiosRequestConfig = {
method: "POST",
url: url_,
headers: {
"Accept": "application/json"
},
cancelToken
};
return this.instance.request(options_).catch((_error: any) => {
if (isAxiosError(_error) && _error.response) {
return _error.response;
} else {
throw _error;
}
}).then((_response: AxiosResponse) => {
return this.processSetEnable(_response);
});
}
protected processSetEnable(response: AxiosResponse): Promise<boolean> {
const status = response.status;
let _headers: any = {};
if (response.headers && typeof response.headers === "object") {
for (const k in response.headers) {
if (response.headers.hasOwnProperty(k)) {
_headers[k] = response.headers[k];
}
}
}
if (status === 200) {
const _responseText = response.data;
let result200: any = null;
let resultData200 = _responseText;
result200 = resultData200 !== undefined ? resultData200 : <any>null;
return Promise.resolve<boolean>(result200);
} else if (status === 500) {
const _responseText = response.data;
let result500: any = null;
let resultData500 = _responseText;
result500 = Exception.fromJS(resultData500);
return throwException("A server side error occurred.", status, _responseText, _headers, result500);
} else if (status === 401) {
const _responseText = response.data;
let result401: any = null;
let resultData401 = _responseText;
result401 = ProblemDetails.fromJS(resultData401);
return throwException("A server side error occurred.", status, _responseText, _headers, result401);
} else if (status !== 200 && status !== 204) {
const _responseText = response.data;
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}
return Promise.resolve<boolean>(null as any);
}
/**
* 控制指定编号的 Switch 开关
* @param num (optional) 开关编号
* @param onOff (optional) 开/关
* @return 操作结果
*/
setSwitchOnOff(num: number | undefined, onOff: boolean | undefined, cancelToken?: CancelToken): Promise<boolean> {
let url_ = this.baseUrl + "/api/Switch/switch?";
if (num === null)
throw new Error("The parameter 'num' cannot be null.");
else if (num !== undefined)
url_ += "num=" + encodeURIComponent("" + num) + "&";
if (onOff === null)
throw new Error("The parameter 'onOff' cannot be null.");
else if (onOff !== undefined)
url_ += "onOff=" + encodeURIComponent("" + onOff) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: AxiosRequestConfig = {
method: "POST",
url: url_,
headers: {
"Accept": "application/json"
},
cancelToken
};
return this.instance.request(options_).catch((_error: any) => {
if (isAxiosError(_error) && _error.response) {
return _error.response;
} else {
throw _error;
}
}).then((_response: AxiosResponse) => {
return this.processSetSwitchOnOff(_response);
});
}
protected processSetSwitchOnOff(response: AxiosResponse): Promise<boolean> {
const status = response.status;
let _headers: any = {};
if (response.headers && typeof response.headers === "object") {
for (const k in response.headers) {
if (response.headers.hasOwnProperty(k)) {
_headers[k] = response.headers[k];
}
}
}
if (status === 200) {
const _responseText = response.data;
let result200: any = null;
let resultData200 = _responseText;
result200 = resultData200 !== undefined ? resultData200 : <any>null;
return Promise.resolve<boolean>(result200);
} else if (status === 400) {
const _responseText = response.data;
let result400: any = null;
let resultData400 = _responseText;
result400 = ArgumentException.fromJS(resultData400);
return throwException("A server side error occurred.", status, _responseText, _headers, result400);
} else if (status === 500) {
const _responseText = response.data;
let result500: any = null;
let resultData500 = _responseText;
result500 = Exception.fromJS(resultData500);
return throwException("A server side error occurred.", status, _responseText, _headers, result500);
} else if (status === 401) {
const _responseText = response.data;
let result401: any = null;
let resultData401 = _responseText;
result401 = ProblemDetails.fromJS(resultData401);
return throwException("A server side error occurred.", status, _responseText, _headers, result401);
} else if (status !== 200 && status !== 204) {
const _responseText = response.data;
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}
return Promise.resolve<boolean>(null as any);
}
/**
* 控制 Switch 开关
* @param keyStatus 开关状态
* @return 操作结果
*/
setMultiSwitchsOnOff(keyStatus: boolean[], cancelToken?: CancelToken): Promise<boolean> {
let url_ = this.baseUrl + "/api/Switch/MultiSwitch";
url_ = url_.replace(/[?&]$/, "");
const content_ = JSON.stringify(keyStatus);
let options_: AxiosRequestConfig = {
data: content_,
method: "POST",
url: url_,
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
cancelToken
};
return this.instance.request(options_).catch((_error: any) => {
if (isAxiosError(_error) && _error.response) {
return _error.response;
} else {
throw _error;
}
}).then((_response: AxiosResponse) => {
return this.processSetMultiSwitchsOnOff(_response);
});
}
protected processSetMultiSwitchsOnOff(response: AxiosResponse): Promise<boolean> {
const status = response.status;
let _headers: any = {};
if (response.headers && typeof response.headers === "object") {
for (const k in response.headers) {
if (response.headers.hasOwnProperty(k)) {
_headers[k] = response.headers[k];
}
}
}
if (status === 200) {
const _responseText = response.data;
let result200: any = null;
let resultData200 = _responseText;
result200 = resultData200 !== undefined ? resultData200 : <any>null;
return Promise.resolve<boolean>(result200);
} else if (status === 400) {
const _responseText = response.data;
let result400: any = null;
let resultData400 = _responseText;
result400 = ArgumentException.fromJS(resultData400);
return throwException("A server side error occurred.", status, _responseText, _headers, result400);
} else if (status === 500) {
const _responseText = response.data;
let result500: any = null;
let resultData500 = _responseText;
result500 = Exception.fromJS(resultData500);
return throwException("A server side error occurred.", status, _responseText, _headers, result500);
} else if (status === 401) {
const _responseText = response.data;
let result401: any = null;
let resultData401 = _responseText;
result401 = ProblemDetails.fromJS(resultData401);
return throwException("A server side error occurred.", status, _responseText, _headers, result401);
} else if (status !== 200 && status !== 204) {
const _responseText = response.data;
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}
return Promise.resolve<boolean>(null as any);
}
}
export class TutorialClient {
protected instance: AxiosInstance;
protected baseUrl: string;

View File

@@ -3,7 +3,7 @@
xmlns="http://www.w3.org/2000/svg"
:width="width"
:height="height"
:viewBox="`4 6 ${props.switchCount + 2} 4`"
:viewBox="`4 6 ${switchCount + 2} 4`"
class="dip-switch"
>
<defs>
@@ -38,7 +38,7 @@
</defs>
<g>
<rect
:width="props.switchCount + 2"
:width="switchCount + 2"
height="4"
x="4"
y="6"
@@ -55,7 +55,7 @@
ON
</text>
<g>
<template v-for="(_, index) in Array(props.switchCount)" :key="index">
<template v-for="(_, index) in Array(switchCount)" :key="index">
<rect
class="glow interactive"
@click="toggleBtnStatus(index)"
@@ -101,27 +101,36 @@
</template>
<script lang="ts" setup>
import { SwitchClient } from "@/APIClient";
import { AuthManager } from "@/utils/AuthManager";
import { isUndefined } from "lodash";
import { ref, computed, watch, onMounted } from "vue";
interface Props {
size?: number;
enableDigitalTwin?: boolean;
switchCount?: number;
initialValues?: boolean[] | string;
initialValues?: string;
showLabels?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
size: 1,
enableDigitalTwin: false,
switchCount: 6,
initialValues: () => [],
initialValues: "",
showLabels: true,
});
const emit = defineEmits(["change"]);
const switchCount = computed(() => {
if (props.enableDigitalTwin) return 5;
else return props.switchCount;
});
// 解析初始值
function parseInitialValues(): boolean[] {
if (Array.isArray(props.initialValues)) {
return [...props.initialValues].slice(0, props.switchCount);
return [...props.initialValues].slice(0, switchCount.value);
}
if (
typeof props.initialValues === "string" &&
@@ -133,14 +142,14 @@ function parseInitialValues(): boolean[] {
while (arr.length < props.switchCount) arr.push(false);
return arr.slice(0, props.switchCount);
}
return Array(props.switchCount).fill(false);
return Array(switchCount.value).fill(false);
}
// 状态唯一真相
const btnStatus = ref<boolean[]>(parseInitialValues());
// 计算宽高
const width = computed(() => (props.switchCount * 25 + 20) * props.size);
const width = computed(() => (switchCount.value * 25 + 20) * props.size);
const height = computed(() => 85 * props.size);
// 按钮位置
@@ -150,13 +159,14 @@ const btnLocation = computed(() =>
// 状态变更统一处理
function updateStatus(newStates: boolean[], index?: number) {
btnStatus.value = newStates.slice(0, props.switchCount);
SwitchClient.setStates(btnStatus.value); // 同步后端
emit("change", {
index,
value: index !== undefined ? btnStatus.value[index] : undefined,
states: [...btnStatus.value],
});
btnStatus.value = newStates.slice(0, switchCount.value);
if (props.enableDigitalTwin) {
try {
const client = AuthManager.createClient(SwitchClient);
if (!isUndefined(index)) client.setSwitchOnOff(index, newStates[index]);
else client.setMultiSwitchsOnOff(btnStatus.value);
} catch (error: any) {}
}
}
// 切换单个
@@ -167,11 +177,6 @@ function toggleBtnStatus(idx: number) {
updateStatus(newStates, idx);
}
// 一次性设置全部
function setAllStates(states: boolean[]) {
updateStatus(states);
}
// 单个设置
function setBtnStatus(idx: number, isOn: boolean) {
if (idx < 0 || idx >= btnStatus.value.length) return;
@@ -182,19 +187,12 @@ function setBtnStatus(idx: number, isOn: boolean) {
// 监听 props 变化只同步一次
watch(
() => [props.switchCount, props.initialValues],
() => [switchCount.value, props.initialValues],
() => {
btnStatus.value = parseInitialValues();
SwitchClient.setStates(btnStatus.value);
updateStatus(btnStatus.value);
},
);
// 监听后端推送
onMounted(() => {
SwitchClient.onStateChange((states: boolean[]) => {
btnStatus.value = states.slice(0, props.switchCount);
});
});
</script>
<style scoped lang="postcss">
@@ -214,3 +212,15 @@ rect {
cursor: pointer;
}
</style>
<script lang="ts">
export function getDefaultProps() {
return {
size: 1,
enableDigitalTwin: false,
switchCount: 6,
initialValues: "",
showLabels: true,
};
}
</script>