|
|
@@ -1,5 +1,9 @@
|
|
|
<template>
|
|
|
<view class="operation">
|
|
|
+ <!-- 右上角设置入口:打开设置弹窗 -->
|
|
|
+ <view class="operation__setting" @tap.stop="openSetting">
|
|
|
+ <u-icon name="setting" size="36" color="#909399"></u-icon>
|
|
|
+ </view>
|
|
|
<view class="operation__header">
|
|
|
<image class="operation__icon" :src="iconSrc" mode="aspectFit" />
|
|
|
<view class="operation__name">{{
|
|
|
@@ -7,16 +11,34 @@
|
|
|
}}</view>
|
|
|
</view>
|
|
|
<view class="operation__body">
|
|
|
- <!-- 三状态:按钮形式(0 停止 1 正向 2 反向) -->
|
|
|
+ <!-- 三状态:按钮形式(0 停止 1 正向 2 反向)+ 行进进度条 -->
|
|
|
<block v-if="hasPos">
|
|
|
- <view
|
|
|
- class="operation__btn"
|
|
|
- :class="btnClass(opt.value)"
|
|
|
- v-for="opt in options"
|
|
|
- :key="opt.value"
|
|
|
- @click="handleClick(opt.value)"
|
|
|
- >{{ opt.label }}</view
|
|
|
- >
|
|
|
+ <view class="operation__btns">
|
|
|
+ <view
|
|
|
+ class="operation__btn"
|
|
|
+ :class="btnClass(opt.value)"
|
|
|
+ v-for="opt in options"
|
|
|
+ :key="opt.value"
|
|
|
+ @click="handleClick(opt.value)"
|
|
|
+ >{{ opt.label }}</view
|
|
|
+ >
|
|
|
+ </view>
|
|
|
+ <!-- 行进进度条:0 / 当前值 / 100 刻度在条上方,当前值 0/100 时与端点刻度重合隐藏 -->
|
|
|
+ <view class="operation__progress">
|
|
|
+ <view class="operation__scale">
|
|
|
+ <view class="operation__num">0</view>
|
|
|
+ <view
|
|
|
+ v-if="posValue > 0 && posValue < 100"
|
|
|
+ class="operation__num operation__num--pos"
|
|
|
+ :style="{ left: posLeft }"
|
|
|
+ >{{ posValue }}</view
|
|
|
+ >
|
|
|
+ <view class="operation__num operation__num--end">100</view>
|
|
|
+ </view>
|
|
|
+ <view class="operation__track">
|
|
|
+ <view class="operation__fill" :style="{ width: posWidth }"></view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
</block>
|
|
|
<!-- 两状态:单个开关(1 开 0 关) -->
|
|
|
<view v-else class="operation__switch-row">
|
|
|
@@ -34,6 +56,74 @@
|
|
|
></u-switch>
|
|
|
</view>
|
|
|
</view>
|
|
|
+ <!-- 设置弹窗:参数设置(运行/间隔时间,走 config 接口)+ 行程设置(TargetPos,走 devctl 接口) -->
|
|
|
+ <u-popup v-model="settingVisible" mode="bottom" border-radius="24" :safe-area-inset-bottom="true">
|
|
|
+ <view class="setting">
|
|
|
+ <view class="setting__head">
|
|
|
+ <view class="setting__title">{{ settingName }}设置</view>
|
|
|
+ <view class="setting__close" @click="settingVisible = false">
|
|
|
+ <u-icon name="close" size="32" color="#909399"></u-icon>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="setting__section">
|
|
|
+ <view class="setting__section-title">参数设置</view>
|
|
|
+ <view class="setting__item">
|
|
|
+ <view class="setting__label">运行时间</view>
|
|
|
+ <view class="setting__input-box">
|
|
|
+ <input
|
|
|
+ class="setting__input"
|
|
|
+ type="number"
|
|
|
+ maxlength="5"
|
|
|
+ :value="settingForm.runTime"
|
|
|
+ placeholder="0"
|
|
|
+ @input="filterNum('runTime', $event)"
|
|
|
+ />
|
|
|
+ <view class="setting__unit">秒</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="setting__item">
|
|
|
+ <view class="setting__label">间隔时间</view>
|
|
|
+ <view class="setting__input-box">
|
|
|
+ <input
|
|
|
+ class="setting__input"
|
|
|
+ type="number"
|
|
|
+ maxlength="5"
|
|
|
+ :value="settingForm.intervalTime"
|
|
|
+ placeholder="0"
|
|
|
+ @input="filterNum('intervalTime', $event)"
|
|
|
+ />
|
|
|
+ <view class="setting__unit">秒</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="setting__acts">
|
|
|
+ <view class="setting__btn" @click="settingVisible = false">取消</view>
|
|
|
+ <view class="setting__btn setting__btn--primary" @click="saveSetting('params')">保存</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="setting__divider"></view>
|
|
|
+ <view class="setting__section">
|
|
|
+ <view class="setting__section-title">行程设置</view>
|
|
|
+ <view class="setting__item">
|
|
|
+ <view class="setting__label">行程设置</view>
|
|
|
+ <view class="setting__input-box">
|
|
|
+ <input
|
|
|
+ class="setting__input"
|
|
|
+ type="number"
|
|
|
+ maxlength="5"
|
|
|
+ :value="settingForm.stroke"
|
|
|
+ placeholder="0"
|
|
|
+ @input="filterNum('stroke', $event)"
|
|
|
+ />
|
|
|
+ <view class="setting__unit">度</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="setting__acts">
|
|
|
+ <view class="setting__btn" @click="settingVisible = false">取消</view>
|
|
|
+ <view class="setting__btn setting__btn--primary" @click="saveSetting('stroke')">保存</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </u-popup>
|
|
|
</view>
|
|
|
</template>
|
|
|
<script>
|
|
|
@@ -59,7 +149,22 @@ export default {
|
|
|
default: () => ({}),
|
|
|
},
|
|
|
},
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ settingVisible: false,
|
|
|
+ // 设置弹窗回显:LimitRun 运行时间 / StepIdle 间隔时间 / TargetPos 行程
|
|
|
+ settingForm: {
|
|
|
+ runTime: '',
|
|
|
+ intervalTime: '',
|
|
|
+ stroke: '',
|
|
|
+ },
|
|
|
+ };
|
|
|
+ },
|
|
|
computed: {
|
|
|
+ settingName() {
|
|
|
+ const d = this.dourceData;
|
|
|
+ return d.display_name || d.name || d.code || '设备';
|
|
|
+ },
|
|
|
// 存在 CurrentPos 字段即为三状态(0 停止 1 正向 2 反向),否则两状态(0 停止 1 开启)
|
|
|
// CurrentPos 可能是明文字段或 ChannelParamSetXX:CurrentPos 平铺属性,只用于判定形态
|
|
|
hasPos() {
|
|
|
@@ -100,6 +205,52 @@ export default {
|
|
|
const val = this.statusKey ? d[this.statusKey] : d.value;
|
|
|
return val == null ? 0 : Number(val);
|
|
|
},
|
|
|
+ // 行进位置遥测 key:ChannelParamSetXX:CurrentPos 或明文 CurrentPos,仅三态有
|
|
|
+ posKey() {
|
|
|
+ const d = this.dourceData;
|
|
|
+ if ('CurrentPos' in d) {
|
|
|
+ return 'CurrentPos';
|
|
|
+ }
|
|
|
+ return (
|
|
|
+ Object.keys(d).find((k) => /^ChannelParamSet\d+:CurrentPos$/.test(k)) ||
|
|
|
+ ''
|
|
|
+ );
|
|
|
+ },
|
|
|
+ // 当前行进位置(0~100),无效值按 0 处理
|
|
|
+ posValue() {
|
|
|
+ const d = this.dourceData;
|
|
|
+ const num = Number(this.posKey ? d[this.posKey] : 0);
|
|
|
+ return Number.isFinite(num) ? num : 0;
|
|
|
+ },
|
|
|
+ posWidth() {
|
|
|
+ return `${Math.min(Math.max(this.posValue, 0), 100)}%`;
|
|
|
+ },
|
|
|
+ // 当前值数字跟随填充末端,钳制在 8%~92% 防贴边
|
|
|
+ posLeft() {
|
|
|
+ return `${Math.min(Math.max(this.posValue, 8), 92)}%`;
|
|
|
+ },
|
|
|
+ // 设置弹窗回显/下发 key:ChannelParamSetXX:LimitRun / StepIdle / TargetPos 平铺属性
|
|
|
+ limitRunKey() {
|
|
|
+ return (
|
|
|
+ Object.keys(this.dourceData).find((k) =>
|
|
|
+ /^ChannelParamSet\d+:LimitRun$/.test(k)
|
|
|
+ ) || ''
|
|
|
+ );
|
|
|
+ },
|
|
|
+ stepIdleKey() {
|
|
|
+ return (
|
|
|
+ Object.keys(this.dourceData).find((k) =>
|
|
|
+ /^ChannelParamSet\d+:StepIdle$/.test(k)
|
|
|
+ ) || ''
|
|
|
+ );
|
|
|
+ },
|
|
|
+ targetPosKey() {
|
|
|
+ return (
|
|
|
+ Object.keys(this.dourceData).find((k) =>
|
|
|
+ /^ChannelParamSet\d+:TargetPos$/.test(k)
|
|
|
+ ) || ''
|
|
|
+ );
|
|
|
+ },
|
|
|
// 按通道名匹配图标,随运行状态切换 on/off 变体
|
|
|
iconSrc() {
|
|
|
const d = this.dourceData;
|
|
|
@@ -123,6 +274,65 @@ export default {
|
|
|
},
|
|
|
},
|
|
|
methods: {
|
|
|
+ // 打开设置弹窗:回显通道配置(LimitRun 运行时间 / StepIdle 间隔时间 / TargetPos 行程)
|
|
|
+ openSetting() {
|
|
|
+ const d = this.dourceData;
|
|
|
+ const pick = (key) => (key && d[key] != null ? d[key] : 0);
|
|
|
+ this.settingForm = {
|
|
|
+ runTime: String(pick(this.limitRunKey)),
|
|
|
+ intervalTime: String(pick(this.stepIdleKey)),
|
|
|
+ stroke: String(pick(this.targetPosKey)),
|
|
|
+ };
|
|
|
+ this.settingVisible = true;
|
|
|
+ },
|
|
|
+ // 运行 / 间隔 / 行程只保留数字,最多 5 位(99999),滤掉其余输入
|
|
|
+ filterNum(field, e) {
|
|
|
+ this.settingForm[field] = String(e.detail.value)
|
|
|
+ .replace(/[^\d]/g, '')
|
|
|
+ .slice(0, 5);
|
|
|
+ },
|
|
|
+ // 按分区保存:抛给父级接接口,弹窗保持打开可继续存另一分区
|
|
|
+ saveSetting(section) {
|
|
|
+ const keys = {
|
|
|
+ limitRunKey: this.limitRunKey,
|
|
|
+ stepIdleKey: this.stepIdleKey,
|
|
|
+ targetPosKey: this.targetPosKey,
|
|
|
+ };
|
|
|
+ if (section === 'stroke') {
|
|
|
+ if (!this.targetPosKey) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '该设备不支持行程设置',
|
|
|
+ icon: 'none',
|
|
|
+ duration: 3000,
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.$emit('setting-save', {
|
|
|
+ dourceData: this.dourceData,
|
|
|
+ section,
|
|
|
+ data: { stroke: this.settingForm.stroke },
|
|
|
+ keys,
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!this.limitRunKey && !this.stepIdleKey) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '该设备不支持参数设置',
|
|
|
+ icon: 'none',
|
|
|
+ duration: 3000,
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.$emit('setting-save', {
|
|
|
+ dourceData: this.dourceData,
|
|
|
+ section,
|
|
|
+ data: {
|
|
|
+ runTime: this.settingForm.runTime,
|
|
|
+ intervalTime: this.settingForm.intervalTime,
|
|
|
+ },
|
|
|
+ keys,
|
|
|
+ });
|
|
|
+ },
|
|
|
// 三状态按钮高亮:正/反向绿色,停止橙色
|
|
|
btnClass(value) {
|
|
|
if (Number(value) !== this.statusValue) {
|
|
|
@@ -149,15 +359,24 @@ export default {
|
|
|
</script>
|
|
|
<style scoped lang="scss">
|
|
|
.operation {
|
|
|
+ position: relative;
|
|
|
// 卡片浅灰底,与头部渐变底色衔接;白色按钮直接落在灰底上
|
|
|
background: #eff2fa;
|
|
|
border-radius: 16rpx;
|
|
|
margin-bottom: 24rpx;
|
|
|
overflow: hidden;
|
|
|
+ // 右上角设置按钮(padding 扩大点击热区)
|
|
|
+ &__setting {
|
|
|
+ position: absolute;
|
|
|
+ top: 8rpx;
|
|
|
+ right: 12rpx;
|
|
|
+ z-index: 2;
|
|
|
+ padding: 10rpx;
|
|
|
+ }
|
|
|
&__header {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- padding: 20rpx 24rpx;
|
|
|
+ padding: 20rpx 72rpx 20rpx 24rpx;
|
|
|
background: linear-gradient(180deg, #eff4ff 20.24%, #eff2fa 100%);
|
|
|
}
|
|
|
&__icon {
|
|
|
@@ -177,9 +396,14 @@ export default {
|
|
|
}
|
|
|
&__body {
|
|
|
display: flex;
|
|
|
- align-items: center;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: stretch;
|
|
|
padding: 20rpx 24rpx 24rpx;
|
|
|
}
|
|
|
+ &__btns {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
&__btn {
|
|
|
flex: 1;
|
|
|
height: 72rpx;
|
|
|
@@ -211,8 +435,45 @@ export default {
|
|
|
font-weight: 500;
|
|
|
}
|
|
|
}
|
|
|
+ // 行进进度条:绿色填充 + 0 / 当前值 / 100 刻度(刻度在条上方)
|
|
|
+ &__progress {
|
|
|
+ margin-top: 20rpx;
|
|
|
+ }
|
|
|
+ &__scale {
|
|
|
+ position: relative;
|
|
|
+ height: 30rpx;
|
|
|
+ margin-bottom: 4rpx;
|
|
|
+ color: #909399;
|
|
|
+ font-family: 'Source Han Sans CN VF';
|
|
|
+ font-size: 22rpx;
|
|
|
+ line-height: 30rpx;
|
|
|
+ }
|
|
|
+ &__num {
|
|
|
+ &--pos {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ color: #0db02b;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+ &--end {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ right: 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ &__track {
|
|
|
+ height: 12rpx;
|
|
|
+ border-radius: 6rpx;
|
|
|
+ background: #c3cad8;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+ &__fill {
|
|
|
+ display: block;
|
|
|
+ height: 100%;
|
|
|
+ background: #0bbc58;
|
|
|
+ }
|
|
|
&__switch-row {
|
|
|
- flex: 1;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
@@ -227,4 +488,94 @@ export default {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+// 设置弹窗(底部弹出)
|
|
|
+.setting {
|
|
|
+ padding: 32rpx;
|
|
|
+ &__head {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 32rpx;
|
|
|
+ }
|
|
|
+ &__title {
|
|
|
+ color: #042118;
|
|
|
+ font-family: 'Source Han Sans CN VF';
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 700;
|
|
|
+ }
|
|
|
+ &__close {
|
|
|
+ padding: 8rpx;
|
|
|
+ }
|
|
|
+ &__section-title {
|
|
|
+ color: #042118;
|
|
|
+ font-family: 'Source Han Sans CN VF';
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+ }
|
|
|
+ &__item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+ }
|
|
|
+ &__label {
|
|
|
+ flex-shrink: 0;
|
|
|
+ width: 150rpx;
|
|
|
+ color: #666666;
|
|
|
+ font-family: 'Source Han Sans CN VF';
|
|
|
+ font-size: 26rpx;
|
|
|
+ }
|
|
|
+ &__input-box {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ height: 72rpx;
|
|
|
+ padding: 0 20rpx;
|
|
|
+ background: #eff2fa;
|
|
|
+ border-radius: 12rpx;
|
|
|
+ }
|
|
|
+ &__input {
|
|
|
+ flex: 1;
|
|
|
+ height: 100%;
|
|
|
+ color: #333333;
|
|
|
+ font-size: 26rpx;
|
|
|
+ }
|
|
|
+ &__unit {
|
|
|
+ margin-left: 12rpx;
|
|
|
+ color: #999999;
|
|
|
+ font-size: 26rpx;
|
|
|
+ }
|
|
|
+ &__acts {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: flex-end;
|
|
|
+ }
|
|
|
+ &__btn {
|
|
|
+ min-width: 140rpx;
|
|
|
+ height: 64rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 0 28rpx;
|
|
|
+ background: #ffffff;
|
|
|
+ border: 2rpx solid #e3e6ee;
|
|
|
+ border-radius: 12rpx;
|
|
|
+ box-sizing: border-box;
|
|
|
+ color: #666666;
|
|
|
+ font-family: 'Source Han Sans CN VF';
|
|
|
+ font-size: 26rpx;
|
|
|
+ margin-left: 20rpx;
|
|
|
+ &--primary {
|
|
|
+ background: #0bbc58;
|
|
|
+ border-color: #0bbc58;
|
|
|
+ color: #ffffff;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ &__divider {
|
|
|
+ height: 2rpx;
|
|
|
+ background: #eef0f5;
|
|
|
+ margin: 32rpx 0;
|
|
|
+ }
|
|
|
+}
|
|
|
</style>
|