|
|
@@ -0,0 +1,591 @@
|
|
|
+<script setup lang="ts">
|
|
|
+import { reactive, computed } from "vue";
|
|
|
+import { ElMessage, ElMessageBox } from "element-plus";
|
|
|
+import { Refresh, Reading, ChatDotSquare, CloseBold, SwitchButton, Loading } from "@element-plus/icons-vue";
|
|
|
+import { deviceRemoteApi } from "@/api/station";
|
|
|
+
|
|
|
+defineOptions({ name: "DeviceRemoteDialog" });
|
|
|
+
|
|
|
+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 api = (url: string, data: any = {}) => {
|
|
|
+ return deviceRemoteApi(url, { productKey, deviceName, ...data });
|
|
|
+};
|
|
|
+
|
|
|
+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 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) {
|
|
|
+ ElMessage.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;
|
|
|
+ ElMessage.success("配置读取成功");
|
|
|
+ } catch (e) {
|
|
|
+ ElMessage.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;
|
|
|
+ }
|
|
|
+ ElMessage.success("配置写入成功");
|
|
|
+ } catch (e) {
|
|
|
+ ElMessage.error("写入配置失败");
|
|
|
+ } finally {
|
|
|
+ state.loading = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const editPrice = (fn: any) => {
|
|
|
+ if (fn.price == null) {
|
|
|
+ ElMessage.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;
|
|
|
+ ElMessage.success(`${state.priceDialog.name}价格已更新`);
|
|
|
+ } catch (e) {
|
|
|
+ ElMessage.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;
|
|
|
+ ElMessage.success("消息已发送");
|
|
|
+ } catch (e) {
|
|
|
+ ElMessage.error("发送消息失败");
|
|
|
+ } finally {
|
|
|
+ state.loading = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const handleHideMsgbox = async () => {
|
|
|
+ state.loading = true;
|
|
|
+ try {
|
|
|
+ await api("hideMsgbox");
|
|
|
+ ElMessage.success("消息已清除");
|
|
|
+ } catch (e) {
|
|
|
+ ElMessage.error("清除消息失败");
|
|
|
+ } finally {
|
|
|
+ state.loading = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const handleReboot = () => {
|
|
|
+ ElMessageBox.confirm("确定要重启设备吗?重启有3-5秒延迟。", "提示", {
|
|
|
+ confirmButtonText: "确定",
|
|
|
+ cancelButtonText: "取消",
|
|
|
+ type: "warning"
|
|
|
+ }).then(async () => {
|
|
|
+ state.loading = true;
|
|
|
+ try {
|
|
|
+ await api("reboot");
|
|
|
+ ElMessage.success("重启命令已发送");
|
|
|
+ } catch (e) {
|
|
|
+ ElMessage.error("重启失败");
|
|
|
+ } finally {
|
|
|
+ state.loading = false;
|
|
|
+ }
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const handleForceCloseOrder = () => {
|
|
|
+ ElMessageBox.confirm("确定要强制结算当前订单吗?此操作将立即关闭订单并释放设备。", "提示", {
|
|
|
+ confirmButtonText: "确定",
|
|
|
+ cancelButtonText: "取消",
|
|
|
+ type: "warning"
|
|
|
+ }).then(async () => {
|
|
|
+ state.loading = true;
|
|
|
+ try {
|
|
|
+ await api("forceCloseOrder");
|
|
|
+ ElMessage.success("强制结算命令已发送");
|
|
|
+ emit("refresh");
|
|
|
+ setTimeout(() => loadDeviceState(), 2000);
|
|
|
+ } catch (e) {
|
|
|
+ ElMessage.error("强制结算失败");
|
|
|
+ } finally {
|
|
|
+ state.loading = false;
|
|
|
+ }
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+defineExpose({ open });
|
|
|
+</script>
|
|
|
+
|
|
|
+<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>
|
|
|
+
|
|
|
+<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;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|