|
|
@@ -0,0 +1,599 @@
|
|
|
+<style scoped lang="scss">
|
|
|
+.remote-panel {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.device-status-bar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 12px 16px;
|
|
|
+ background: #1a1a2e;
|
|
|
+ border-radius: 8px;
|
|
|
+ color: #00ff88;
|
|
|
+ font-family: 'Courier New', monospace;
|
|
|
+ font-size: 14px;
|
|
|
+
|
|
|
+ .status-left {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .status-dot {
|
|
|
+ width: 10px;
|
|
|
+ height: 10px;
|
|
|
+ border-radius: 50%;
|
|
|
+ display: inline-block;
|
|
|
+ margin-right: 6px;
|
|
|
+
|
|
|
+ &.busy { background: #ff4444; animation: blink 1s infinite; }
|
|
|
+ &.idle { background: #00ff88; }
|
|
|
+ &.init { background: #ffaa00; }
|
|
|
+ &.fault { background: #ff0000; }
|
|
|
+ &.maintenance { background: #ff8800; }
|
|
|
+ &.sleep { background: #888888; }
|
|
|
+ }
|
|
|
+
|
|
|
+ @keyframes blink {
|
|
|
+ 0%, 100% { opacity: 1; }
|
|
|
+ 50% { opacity: 0.3; }
|
|
|
+ }
|
|
|
+
|
|
|
+ .status-label {
|
|
|
+ color: #aaa;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .status-value {
|
|
|
+ color: #fff;
|
|
|
+ font-weight: bold;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.func-button-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(4, 1fr);
|
|
|
+ gap: 10px;
|
|
|
+
|
|
|
+ .func-btn {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 16px 8px;
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ border-radius: 10px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s;
|
|
|
+ background: #f8f9fa;
|
|
|
+ user-select: none;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background: #e8f4fd;
|
|
|
+ border-color: #409eff;
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
|
|
|
+ }
|
|
|
+
|
|
|
+ &:active {
|
|
|
+ transform: translateY(0);
|
|
|
+ background: #d9ecff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .func-icon {
|
|
|
+ font-size: 24px;
|
|
|
+ margin-bottom: 6px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .func-name {
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .func-price {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #f56c6c;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.system-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ padding: 12px 0;
|
|
|
+ border-top: 1px solid #ebeef5;
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
+
|
|
|
+ .action-btn {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 100px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.order-info-card {
|
|
|
+ background: #fffbf0;
|
|
|
+ border: 1px solid #f0d78c;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 12px 16px;
|
|
|
+ margin-top: 8px;
|
|
|
+
|
|
|
+ .order-title {
|
|
|
+ font-weight: 600;
|
|
|
+ color: #e6a23c;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .order-row {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #666;
|
|
|
+ margin-bottom: 4px;
|
|
|
+
|
|
|
+ span:last-child {
|
|
|
+ color: #333;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.config-editor {
|
|
|
+ margin-top: 8px;
|
|
|
+ padding: 12px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ border-radius: 8px;
|
|
|
+
|
|
|
+ .config-title {
|
|
|
+ font-weight: 600;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ color: #333;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-form-item) {
|
|
|
+ margin-bottom: 12px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.price-tag {
|
|
|
+ display: inline-block;
|
|
|
+ padding: 2px 8px;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 12px;
|
|
|
+ cursor: pointer;
|
|
|
+ margin-left: 6px;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ opacity: 0.8;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <div class="system-dialog-container">
|
|
|
+ <el-dialog
|
|
|
+ :title="'远程控制 — ' + deviceName"
|
|
|
+ v-model="state.dialog.isShowDialog"
|
|
|
+ width="800px"
|
|
|
+ draggable
|
|
|
+ destroy-on-close
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ align-center
|
|
|
+ @close="onClose">
|
|
|
+
|
|
|
+ <div v-if="!state.ready" style="text-align:center;padding:60px 0;">
|
|
|
+ <el-icon class="is-loading" style="font-size:32px;color:#409eff;"><Loading /></el-icon>
|
|
|
+ <p style="color:#999;margin-top:12px;">正在连接设备...</p>
|
|
|
+ </div>
|
|
|
+ <div class="remote-panel" v-else>
|
|
|
+ <!-- 设备状态栏 -->
|
|
|
+ <div class="device-status-bar">
|
|
|
+ <div class="status-left">
|
|
|
+ <span>
|
|
|
+ <span class="status-dot" :class="statusClass"></span>
|
|
|
+ {{ statusLabel }}
|
|
|
+ </span>
|
|
|
+ <span><span class="status-label">运行时长</span> <span class="status-value">{{ uptimeDisplay }}</span></span>
|
|
|
+ <span><span class="status-label">温度</span> <span class="status-value">{{ temperatureDisplay }}</span></span>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <el-button size="small" text type="primary" @click="refreshState" :loading="state.loading">
|
|
|
+ <el-icon><Refresh /></el-icon> 刷新
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 功能按钮面板 -->
|
|
|
+ <div class="func-button-grid">
|
|
|
+ <div class="func-btn" v-for="fn in functionButtons" :key="fn.key" @click="editPrice(fn)">
|
|
|
+ <span class="func-icon">{{ fn.icon }}</span>
|
|
|
+ <span class="func-name">{{ fn.name }}</span>
|
|
|
+ <span class="func-price">{{ fn.price != null ? (fn.price / 100).toFixed(2) + '元/分' : '--' }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 系统操作按钮 -->
|
|
|
+ <div class="system-actions">
|
|
|
+ <el-button class="action-btn" type="primary" plain @click="handleReadConfig" :loading="state.loading">
|
|
|
+ <el-icon><Reading /></el-icon> 读取配置
|
|
|
+ </el-button>
|
|
|
+ <el-button class="action-btn" type="warning" plain @click="handleShowMsgbox" :loading="state.loading">
|
|
|
+ <el-icon><ChatDotSquare /></el-icon> 屏幕消息
|
|
|
+ </el-button>
|
|
|
+ <el-button class="action-btn" type="success" plain @click="handleHideMsgbox" :loading="state.loading">
|
|
|
+ <el-icon><CloseBold /></el-icon> 清除消息
|
|
|
+ </el-button>
|
|
|
+ <el-button class="action-btn" type="danger" plain @click="handleReboot" :loading="state.loading">
|
|
|
+ <el-icon><SwitchButton /></el-icon> 重启设备
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 订单信息(仅忙碌时显示) -->
|
|
|
+ <div class="order-info-card" v-if="state.deviceState === 'busy' && state.orderInfo">
|
|
|
+ <div class="order-title">当前订单</div>
|
|
|
+ <div class="order-row"><span>订单号</span><span>{{ state.orderInfo.order_id || '--' }}</span></div>
|
|
|
+ <div class="order-row"><span>消费金额</span><span>{{ state.orderInfo.amount ? (state.orderInfo.amount / 100).toFixed(2) + '元' : '--' }}</span></div>
|
|
|
+ <div class="order-row"><span>操作剩余</span><span>{{ state.orderInfo.operation_remain_time }}秒</span></div>
|
|
|
+ <div class="order-row"><span>空闲剩余</span><span>{{ state.orderInfo.idle_remain_time }}秒</span></div>
|
|
|
+ <div style="margin-top: 10px;">
|
|
|
+ <el-button size="small" type="danger" @click="handleForceCloseOrder">
|
|
|
+ 强制结算
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 配置编辑区 -->
|
|
|
+ <div class="config-editor" v-if="state.showConfigEditor">
|
|
|
+ <div class="config-title">设备配置编辑</div>
|
|
|
+ <el-form :model="state.configForm" label-width="140px" size="small" inline>
|
|
|
+ <el-form-item label="清水单价(分/分)">
|
|
|
+ <el-input-number v-model="state.configForm.priceWater" :min="0" size="small" controls-position="right"/>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="泡沫单价(分/分)">
|
|
|
+ <el-input-number v-model="state.configForm.priceFoam" :min="0" size="small" controls-position="right"/>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="吸尘单价(分/分)">
|
|
|
+ <el-input-number v-model="state.configForm.priceCleaner" :min="0" size="small" controls-position="right"/>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="洗手单价(分/分)">
|
|
|
+ <el-input-number v-model="state.configForm.priceTap" :min="0" size="small" controls-position="right"/>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="扩展单价(分/分)">
|
|
|
+ <el-input-number v-model="state.configForm.priceUserExt" :min="0" size="small" controls-position="right"/>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="镀膜单价(分/分)">
|
|
|
+ <el-input-number v-model="state.configForm.priceCoat" :min="0" size="small" controls-position="right"/>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="吹气单价(分/分)">
|
|
|
+ <el-input-number v-model="state.configForm.priceBlow" :min="0" size="small" controls-position="right"/>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="场地费(分/分)">
|
|
|
+ <el-input-number v-model="state.configForm.priceSpace" :min="0" size="small" controls-position="right"/>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="空闲超时(秒)">
|
|
|
+ <el-input-number v-model="state.configForm.idleTimeout" :min="0" size="small" controls-position="right"/>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="操作超时(秒)">
|
|
|
+ <el-input-number v-model="state.configForm.operationTimeout" :min="0" size="small" controls-position="right"/>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="提示音音量">
|
|
|
+ <el-input-number v-model="state.configForm.soundVolume" :min="0" :max="100" size="small" controls-position="right"/>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="屏幕左下文本">
|
|
|
+ <el-input v-model="state.configForm.userMessage1" size="small" style="width:180px"/>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="屏幕右下文本">
|
|
|
+ <el-input v-model="state.configForm.userMessage2" size="small" style="width:180px"/>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <div style="margin-top: 10px;">
|
|
|
+ <el-button type="primary" size="small" @click="handleWriteConfig" :loading="state.loading">应用配置</el-button>
|
|
|
+ <el-button size="small" @click="state.showConfigEditor = false">收起</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 价格快捷编辑弹窗 -->
|
|
|
+ <el-dialog v-model="state.priceDialog.visible" :title="'修改单价 — ' + state.priceDialog.name" width="350px"
|
|
|
+ :close-on-click-modal="false" append-to-body>
|
|
|
+ <div style="text-align:center; padding: 20px 0;">
|
|
|
+ <div style="font-size: 14px; color: #666; margin-bottom: 12px;">当前单价:{{ (state.priceDialog.currentPrice / 100).toFixed(2) }} 元/分钟</div>
|
|
|
+ <div style="display: flex; align-items: center; justify-content: center; gap: 8px;">
|
|
|
+ <span>新单价:</span>
|
|
|
+ <el-input-number v-model="state.priceDialog.newPriceYuan" :min="0" :precision="2" :step="0.1" size="default"/>
|
|
|
+ <span>元/分钟</span>
|
|
|
+ </div>
|
|
|
+ <div style="font-size: 12px; color: #999; margin-top: 6px;">({{ Math.round(state.priceDialog.newPriceYuan * 100) }} 分)</div>
|
|
|
+ </div>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="state.priceDialog.visible = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="confirmPriceEdit" :loading="state.loading">确认修改</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 屏幕消息弹窗 -->
|
|
|
+ <el-dialog v-model="state.msgDialog.visible" title="发送屏幕消息" width="450px"
|
|
|
+ :close-on-click-modal="false" append-to-body>
|
|
|
+ <el-form label-width="70px" size="default">
|
|
|
+ <el-form-item label="标题">
|
|
|
+ <el-input v-model="state.msgDialog.title" placeholder="消息标题"/>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="内容">
|
|
|
+ <el-input v-model="state.msgDialog.content" type="textarea" :rows="3" placeholder="消息内容"/>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="显示时长">
|
|
|
+ <el-input-number v-model="state.msgDialog.seconds" :min="1" :max="300"/> 秒
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="state.msgDialog.visible = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="confirmShowMsgbox" :loading="state.loading">发送</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts" name="WashDeviceRemoteDialog">
|
|
|
+import {reactive, computed, watch} from 'vue';
|
|
|
+import {Msg} from "/@/utils/message";
|
|
|
+import {$body} from "/@/utils/request";
|
|
|
+import {ElMessage} from 'element-plus';
|
|
|
+import {Refresh, Reading, ChatDotSquare, CloseBold, SwitchButton, Loading} from '@element-plus/icons-vue';
|
|
|
+
|
|
|
+const emit = defineEmits(['refresh']);
|
|
|
+
|
|
|
+const initState = () => ({
|
|
|
+ ready: false,
|
|
|
+ loading: false,
|
|
|
+ deviceState: '',
|
|
|
+ temperatureChip: null as number | null,
|
|
|
+ uptimeMs: '',
|
|
|
+ orderInfo: null as any,
|
|
|
+ showConfigEditor: false,
|
|
|
+ configForm: {} as any,
|
|
|
+ priceDialog: {
|
|
|
+ visible: false,
|
|
|
+ key: '',
|
|
|
+ name: '',
|
|
|
+ currentPrice: 0,
|
|
|
+ newPriceYuan: 0,
|
|
|
+ },
|
|
|
+ msgDialog: {
|
|
|
+ visible: false,
|
|
|
+ title: '',
|
|
|
+ content: '',
|
|
|
+ seconds: 10,
|
|
|
+ },
|
|
|
+ dialog: {
|
|
|
+ isShowDialog: false,
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+const state = reactive(initState());
|
|
|
+
|
|
|
+let productKey = '';
|
|
|
+let deviceName = '';
|
|
|
+
|
|
|
+const functionButtons = [
|
|
|
+ {key: 'priceWater', name: '清水', icon: '🚿', price: null as number | null},
|
|
|
+ {key: 'priceFoam', name: '泡沫', icon: '🫧', price: null as number | null},
|
|
|
+ {key: 'priceCleaner', name: '吸尘', icon: '🌀', price: null as number | null},
|
|
|
+ {key: 'priceTap', name: '洗手', icon: '🖐️', price: null as number | null},
|
|
|
+ {key: 'priceUserExt', name: '扩展', icon: '🔧', price: null as number | null},
|
|
|
+ {key: 'priceCoat', name: '镀膜', icon: '✨', price: null as number | null},
|
|
|
+ {key: 'priceBlow', name: '吹气', icon: '💨', price: null as number | null},
|
|
|
+ {key: 'priceSpace', name: '场地费', icon: '🅿️', price: null as number | null},
|
|
|
+];
|
|
|
+
|
|
|
+const statusClass = computed(() => {
|
|
|
+ const map: Record<string, string> = {busy: 'busy', idle: 'idle', init: 'init', fault: 'fault', maintenance: 'maintenance', sleep: 'sleep'};
|
|
|
+ return map[state.deviceState] || '';
|
|
|
+});
|
|
|
+
|
|
|
+const statusLabel = computed(() => {
|
|
|
+ const map: Record<string, string> = {busy: '忙碌', idle: '空闲', init: '初始化', fault: '故障', maintenance: '维护', sleep: '休眠'};
|
|
|
+ return map[state.deviceState] || state.deviceState || '未知';
|
|
|
+});
|
|
|
+
|
|
|
+const uptimeDisplay = computed(() => {
|
|
|
+ if (!state.uptimeMs) return '--';
|
|
|
+ const ms = parseInt(state.uptimeMs);
|
|
|
+ const h = Math.floor(ms / 3600000);
|
|
|
+ const m = Math.floor((ms % 3600000) / 60000);
|
|
|
+ return h > 0 ? `${h}时${m}分` : `${m}分`;
|
|
|
+});
|
|
|
+
|
|
|
+const temperatureDisplay = computed(() => {
|
|
|
+ return state.temperatureChip != null ? `${state.temperatureChip}°C` : '--';
|
|
|
+});
|
|
|
+
|
|
|
+const open = (row: any) => {
|
|
|
+ productKey = row.productKey;
|
|
|
+ deviceName = row.deviceName;
|
|
|
+ state.dialog.isShowDialog = true;
|
|
|
+ loadDeviceState();
|
|
|
+};
|
|
|
+
|
|
|
+const onClose = () => {
|
|
|
+ state.dialog.isShowDialog = false;
|
|
|
+ Object.assign(state, initState());
|
|
|
+};
|
|
|
+
|
|
|
+const api = (url: string, data: any = {}) => {
|
|
|
+ return $body(`/washDevice/remote/${url}`, {productKey, deviceName, ...data});
|
|
|
+};
|
|
|
+
|
|
|
+const loadDeviceState = async () => {
|
|
|
+ state.loading = true;
|
|
|
+ try {
|
|
|
+ const res: any = await api('queryState');
|
|
|
+ state.deviceState = res?.device_state?.state || '';
|
|
|
+ state.uptimeMs = res?.device_state?.uptime_ms || '';
|
|
|
+ state.temperatureChip = res?.device_state?.temperature_chip ?? null;
|
|
|
+ state.orderInfo = res?.order_info || null;
|
|
|
+ } catch (e) {
|
|
|
+ Msg.message('查询设备状态失败', 'error');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 自动加载设备配置,填充功能按钮价格
|
|
|
+ try {
|
|
|
+ const config: any = await api('readConfig');
|
|
|
+ state.configForm = {...config};
|
|
|
+ for (const fn of functionButtons) {
|
|
|
+ fn.price = config?.[fn.key] ?? null;
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ // 配置加载失败不影响面板使用,功能按钮显示 "--"
|
|
|
+ for (const fn of functionButtons) {
|
|
|
+ fn.price = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ state.ready = true;
|
|
|
+ state.loading = false;
|
|
|
+};
|
|
|
+
|
|
|
+const refreshState = () => loadDeviceState();
|
|
|
+
|
|
|
+const handleReadConfig = async () => {
|
|
|
+ state.loading = true;
|
|
|
+ try {
|
|
|
+ const res: any = await api('readConfig');
|
|
|
+ state.configForm = {...res};
|
|
|
+ // 同步价格到功能按钮
|
|
|
+ for (const fn of functionButtons) {
|
|
|
+ fn.price = res?.[fn.key] ?? null;
|
|
|
+ }
|
|
|
+ state.showConfigEditor = true;
|
|
|
+ Msg.message('配置读取成功', 'success');
|
|
|
+ } catch (e) {
|
|
|
+ Msg.message('读取配置失败', 'error');
|
|
|
+ } finally {
|
|
|
+ state.loading = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const handleWriteConfig = async () => {
|
|
|
+ state.loading = true;
|
|
|
+ try {
|
|
|
+ await api('writeConfig', {config: state.configForm});
|
|
|
+ // 同步更新按钮显示的价格
|
|
|
+ for (const fn of functionButtons) {
|
|
|
+ fn.price = state.configForm[fn.key] ?? null;
|
|
|
+ }
|
|
|
+ Msg.message('配置写入成功', 'success');
|
|
|
+ } catch (e) {
|
|
|
+ Msg.message('写入配置失败', 'error');
|
|
|
+ } finally {
|
|
|
+ state.loading = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const editPrice = (fn: any) => {
|
|
|
+ if (fn.price == null) {
|
|
|
+ Msg.message('配置加载失败,无法修改价格,请点击「读取配置」重试', 'warning');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ state.priceDialog.key = fn.key;
|
|
|
+ state.priceDialog.name = fn.name;
|
|
|
+ state.priceDialog.currentPrice = fn.price;
|
|
|
+ state.priceDialog.newPriceYuan = parseFloat((fn.price / 100).toFixed(2));
|
|
|
+ state.priceDialog.visible = true;
|
|
|
+};
|
|
|
+
|
|
|
+const confirmPriceEdit = async () => {
|
|
|
+ const newPriceFen = Math.round(state.priceDialog.newPriceYuan * 100);
|
|
|
+ state.loading = true;
|
|
|
+ try {
|
|
|
+ // 先读当前完整配置,修改单个价格后写回
|
|
|
+ const fullConfig: any = await api('readConfig');
|
|
|
+ fullConfig[state.priceDialog.key] = newPriceFen;
|
|
|
+ await api('writeConfig', {config: fullConfig});
|
|
|
+ // 更新显示
|
|
|
+ const fn = functionButtons.find(f => f.key === state.priceDialog.key);
|
|
|
+ if (fn) fn.price = newPriceFen;
|
|
|
+ state.configForm[state.priceDialog.key] = newPriceFen;
|
|
|
+ state.priceDialog.visible = false;
|
|
|
+ Msg.message(`${state.priceDialog.name}价格已更新`, 'success');
|
|
|
+ } catch (e) {
|
|
|
+ Msg.message('修改价格失败', 'error');
|
|
|
+ } finally {
|
|
|
+ state.loading = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const handleShowMsgbox = () => {
|
|
|
+ state.msgDialog.title = '';
|
|
|
+ state.msgDialog.content = '';
|
|
|
+ state.msgDialog.seconds = 10;
|
|
|
+ state.msgDialog.visible = true;
|
|
|
+};
|
|
|
+
|
|
|
+const confirmShowMsgbox = async () => {
|
|
|
+ state.loading = true;
|
|
|
+ try {
|
|
|
+ await api('showMsgbox', {
|
|
|
+ title: state.msgDialog.title,
|
|
|
+ content: state.msgDialog.content,
|
|
|
+ seconds: state.msgDialog.seconds,
|
|
|
+ });
|
|
|
+ state.msgDialog.visible = false;
|
|
|
+ Msg.message('消息已发送', 'success');
|
|
|
+ } catch (e) {
|
|
|
+ Msg.message('发送消息失败', 'error');
|
|
|
+ } finally {
|
|
|
+ state.loading = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const handleHideMsgbox = async () => {
|
|
|
+ state.loading = true;
|
|
|
+ try {
|
|
|
+ await api('hideMsgbox');
|
|
|
+ Msg.message('消息已清除', 'success');
|
|
|
+ } catch (e) {
|
|
|
+ Msg.message('清除消息失败', 'error');
|
|
|
+ } finally {
|
|
|
+ state.loading = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const handleReboot = () => {
|
|
|
+ Msg.confirm('确定要重启设备吗?重启有3-5秒延迟。').then(async () => {
|
|
|
+ state.loading = true;
|
|
|
+ try {
|
|
|
+ await api('reboot');
|
|
|
+ Msg.message('重启命令已发送', 'success');
|
|
|
+ } catch (e) {
|
|
|
+ Msg.message('重启失败', 'error');
|
|
|
+ } finally {
|
|
|
+ state.loading = false;
|
|
|
+ }
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const handleForceCloseOrder = () => {
|
|
|
+ Msg.confirm('确定要强制结算当前订单吗?此操作将立即关闭订单并释放设备。').then(async () => {
|
|
|
+ state.loading = true;
|
|
|
+ try {
|
|
|
+ await api('forceCloseOrder');
|
|
|
+ Msg.message('强制结算命令已发送', 'success');
|
|
|
+ emit('refresh');
|
|
|
+ setTimeout(() => loadDeviceState(), 2000);
|
|
|
+ } catch (e) {
|
|
|
+ Msg.message('强制结算失败', 'error');
|
|
|
+ } finally {
|
|
|
+ state.loading = false;
|
|
|
+ }
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+defineExpose({open});
|
|
|
+</script>
|