瀏覽代碼

添加as机器人管理

DESKTOP-NE65RNK\Citrus_limon 1 年之前
父節點
當前提交
ee4b526ef8

+ 14 - 0
src/api/index.ts

@@ -150,6 +150,20 @@ export const set_robot_auto = (params: any, callback: any) => {
   });
 };
 
+// as机器人
+// 机器人管理-as列表
+export const get_as_robot_list = (params: any, callback: any) => {
+  return http.request("/api/robot/getStdPage", "get", params).then((data) => {
+    if (data) callback && callback(data);
+  });
+};
+// 机器人管理-机器人详情
+export const get_as_robot_detail = (params: any, callback: any) => {
+  return http.request("/api/robot/predictorState", "get", params).then((data) => {
+    if (data) callback && callback(data);
+  });
+};
+
 // 套利机器人
 // 机器人管理-套利列表
 export const get_arbitrage_robot_list = (params: any, callback: any) => {

+ 12 - 0
src/router/routes.ts

@@ -34,6 +34,12 @@ const routes: Array<RouteRecordRaw> = [
         component: () => import("@/views/bot/manage/index.vue"),
         meta: { title: "机器人管理", keepAlive: true },
       },
+      {
+        path: "/bot/as",
+        name: "BotAs",
+        component: () => import("@/views/bot/as/index.vue"),
+        meta: { title: "AS机器人管理", keepAlive: true },
+      },
       {
         path: "/bot/arbitrage",
         name: "BotArbitrage",
@@ -184,6 +190,12 @@ const routes: Array<RouteRecordRaw> = [
     component: () => import("@/views/bot/manage/detail.vue"),
     meta: { title: "机器人详情", keepAlive: false },
   },
+  {
+    path: "/bot/as/detail/:id",
+    name: "BotAsDetail",
+    component: () => import("@/views/bot/as/detail.vue"),
+    meta: { title: "AS机器人详情", keepAlive: true },
+  },
   {
     path: "/bot/arbitrage/detail/:id",
     name: "BotArbitrageDetail",

+ 145 - 0
src/views/bot/as/components/Automate.vue

@@ -0,0 +1,145 @@
+<template>
+  <lay-layer :title="modelConfig.title" v-model="modelConfig.visible" area="auto" :btn="operator">
+    <div class="width-500 custom-layer" style="padding: 20px">
+      <lay-form :model="modelParams" ref="modelFormRef" required>
+        <lay-form-item label="是否自动调参" prop="isAuto">
+          <lay-switch v-model="modelParams.isAuto" :onswitch-value="1" :unswitch-value="0" />
+          <div class="tips">开启后会自动开关机机器,自动修改参数</div>
+        </lay-form-item>
+        <lay-form-item label="是否参考币安" prop="isRefBinance">
+          <lay-switch v-model="modelParams.isRefBinance" />
+          <div class="tips">开启后如果币安交易所存在该币对就会优先参考币安交易所</div>
+        </lay-form-item>
+        <lay-form-item label="调参间隔时间" prop="timeInterval">
+          <lay-input v-model="modelParams.timeInterval" placeholder="调参间隔时间(分钟)" />
+        </lay-form-item>
+        <lay-form-item label="取第几个" prop="coinIndex">
+          <lay-input v-model="modelParams.coinIndex" placeholder="设置排行榜排名第几的币对" />
+        </lay-form-item>
+        <lay-form-item label="开仓系数" prop="openRatio">
+          <lay-input v-model="modelParams.openRatio" placeholder="排行榜开仓值乘以系数" />
+        </lay-form-item>
+        <lay-form-item label="最小总分数" prop="scoreLimit">
+          <lay-input v-model="modelParams.scoreLimit" placeholder="排行榜总分数最小值" />
+        </lay-form-item>
+        <lay-form-item label="最小波动分数" prop="fluctuationScore">
+          <lay-input v-model="modelParams.fluctuationScore" placeholder="排行榜最小波动分数最小值" />
+        </lay-form-item>
+        <lay-form-item label="最小频次分数" prop="frequencyScore">
+          <lay-input v-model="modelParams.frequencyScore" placeholder="排行榜最小交易频次分数最小值" />
+        </lay-form-item>
+        <lay-form-item label="最小量分数" prop="transactionVolumeScore">
+          <lay-input v-model="modelParams.transactionVolumeScore" placeholder="排行榜最小交易量分数最小值" />
+        </lay-form-item>
+        <lay-form-item label="调参等级" prop="dataHourNum">
+          <lay-select v-model="modelParams.dataHourNum">
+            <lay-select-option :value="1" label="LV1(1小时)" />
+            <lay-select-option :value="2" label="LV2(2小时)" />
+          </lay-select>
+        </lay-form-item>
+      </lay-form>
+    </div>
+  </lay-layer>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, getCurrentInstance } from "vue";
+import { set_robot_auto } from "@/api";
+
+const { proxy }: any = getCurrentInstance();
+
+const modelFormRef = ref();
+
+interface ModelConfig {
+  title: string;
+  visible: boolean;
+  isUpdate: boolean;
+  loading: boolean;
+}
+interface ModelParams {
+  id?: string;
+  exchange?: string;
+  isAuto?: number;
+  isRefBinance?: boolean;
+  timeInterval?: string;
+  coinIndex?: string;
+  openRatio?: string;
+  scoreLimit?: string;
+  fluctuationScore?: string;
+  frequencyScore?: string;
+  transactionVolumeScore?: string;
+  dataHourNum?: number;
+}
+
+let modelParams = ref<ModelParams>({});
+let modelConfig: ModelConfig = reactive({ title: "", visible: false, isUpdate: false, loading: false });
+
+let handleResult = reactive<{ resolve?: any; reject?: any }>({});
+
+const show = (params?: any) => {
+  modelConfig.visible = true;
+  modelConfig.title = "设置自动调参";
+  let exchange = params.configList.find((item: any) => item.code == "exchange")?.val || undefined;
+  let refExchange = params.configList.find((item: any) => item.code == "ref_exchange")?.val || undefined;
+  modelParams.value = { id: params.id, dataHourNum: 1, isAuto: params.isAuto || 0, isRefBinance: false, ...params.autoConfig, exchange, refExchange };
+  return new Promise(async (resolve, reject) => {
+    handleResult.resolve = resolve;
+    handleResult.reject = reject;
+  });
+};
+
+const operator = reactive([
+  {
+    text: "确认",
+    callback: () => {
+      modelFormRef.value.validate((isValidate: boolean) => {
+        if (isValidate) {
+          modelConfig.loading = true;
+          const params = {
+            robotId: modelParams.value.id,
+            exchange: modelParams.value.exchange,
+            isAuto: modelParams.value.isAuto,
+            isRefBinance: modelParams.value.isRefBinance,
+            timeInterval: modelParams.value.timeInterval,
+            coinIndex: modelParams.value.coinIndex,
+            openRatio: modelParams.value.openRatio,
+            scoreLimit: modelParams.value.scoreLimit,
+            fluctuationScore: modelParams.value.fluctuationScore,
+            frequencyScore: modelParams.value.frequencyScore,
+            transactionVolumeScore: modelParams.value.transactionVolumeScore,
+            dataHourNum: modelParams.value.dataHourNum,
+          };
+          set_robot_auto(params, (data: any) => {
+            modelConfig.loading = false;
+            if (data.code == 200) {
+              proxy.$message("设置成功!");
+              modelConfig.visible = false;
+              handleResult.resolve(true);
+            }
+          });
+        }
+      });
+    },
+  },
+  {
+    text: "取消",
+    callback: () => {
+      modelConfig.visible = false;
+      handleResult.resolve(false);
+    },
+  },
+]);
+defineExpose({ show });
+</script>
+<style lang="scss" scoped>
+.tips {
+  font-size: 12px;
+  color: #2c2c2c;
+  &.danger {
+    color: var(--button-danger-color);
+  }
+}
+.split-text {
+  padding: 10px 0 30px;
+}
+</style>

+ 319 - 0
src/views/bot/as/components/BatchUpdate.vue

@@ -0,0 +1,319 @@
+<template>
+  <lay-layer :title="modelConfig.title" v-model="modelConfig.visible" area="auto" :btn="operator">
+    <div class="width-800 custom-layer" style="padding: 20px">
+      <lay-form :model="modelParams" ref="modelFormRef">
+        <lay-row space="10">
+          <lay-col md="12" sm="12" xs="24">
+            <lay-form-item label="名称" prop="name">
+              <lay-input v-model="modelParams.name" placeholder="机器人名称" />
+            </lay-form-item>
+          </lay-col>
+          <lay-col md="12" sm="12" xs="24">
+            <lay-form-item label="初始金额" prop="startAmount">
+              <lay-input v-model="modelParams.startAmount" placeholder="初始金额" />
+            </lay-form-item>
+          </lay-col>
+          <lay-col md="12" sm="12" xs="24">
+            <lay-form-item label="服务器" prop="serverId">
+              <lay-select v-model="modelParams.serverId" :show-search="true">
+                <lay-select-option v-for="item in serverList" :value="item.id" :label="item.value" placeholder="选择服务器" />
+              </lay-select>
+            </lay-form-item>
+          </lay-col>
+          <lay-col md="12" sm="12" xs="24">
+            <lay-form-item label="通讯端口" prop="callPort">
+              <lay-input v-model="modelParams.callPort" placeholder="通讯端口(1001、1002)" />
+            </lay-form-item>
+          </lay-col>
+          <lay-col md="12" sm="12" xs="24">
+            <lay-form-item label="策略" prop="strategyId">
+              <lay-select v-model="modelParams.strategyId" @change="strategyHandleChange">
+                <lay-select-option v-for="item in strategyList" :value="item.id" :label="item.value" placeholder="选择策略" />
+              </lay-select>
+            </lay-form-item>
+          </lay-col>
+          <lay-col md="12" sm="12" xs="24">
+            <lay-form-item label="策略版本" prop="strategyProgramId">
+              <lay-select v-model="modelParams.strategyProgramId" @change="strategyProgramHandleChange">
+                <lay-select-option v-for="item in strategyProgramList" :value="item.id" :label="item.value" placeholder="选择策略版本" />
+              </lay-select>
+            </lay-form-item>
+          </lay-col>
+          <div>
+            <template v-for="item in strategyParameterList">
+              <lay-col md="12" sm="12" xs="24">
+                <lay-form-item :label="item.name" :prop="`robotConfigs.${item.code}`">
+                  <lay-input v-if="item.valType == 0" v-model="modelParams.robotConfigs[item.code]" @input="handleStringInput(item.code, $event)" :placeholder="PARAMS_ENUM[item.code]" />
+                  <lay-input
+                    v-if="item.valType == 1 && item.code != 'colo'"
+                    v-model="modelParams.robotConfigs[item.code]"
+                    @input="handleNmberInput(item.code, $event)"
+                    :placeholder="PARAMS_ENUM[item.code]"
+                  />
+                  <lay-switch v-if="item.valType == 1 && item.code == 'colo'" onswitch-value="1" unswitch-value="0" v-model="modelParams.robotConfigs[item.code]" />
+                  <lay-switch v-if="item.valType == 2" v-model="modelParams.robotConfigs[item.code]" />
+                  <lay-select v-if="item.valType == 3" v-model="modelParams.robotConfigs[item.code]" :show-search="true" :placeholder="PARAMS_ENUM[item.code]">
+                    <lay-select-option v-for="items in item.contentVal.split(',')" :value="items" :label="items" />
+                  </lay-select>
+                  <lay-select v-if="item.valType == 5" v-model="modelParams.robotConfigs[item.code]" :options="apikeyList" :show-search="true" :placeholder="PARAMS_ENUM[item.code]" />
+                </lay-form-item>
+                <lay-form-item v-if="item.code == 'open'" label="开仓递增" :prop="`robotConfigs.openIntervals`" :placeholder="PARAMS_ENUM[item.code]">
+                  <lay-input v-model="modelParams.robotConfigs.openIntervals" />
+                </lay-form-item>
+                <lay-form-item v-if="item.code == 'close'" label="平仓递增" :prop="`robotConfigs.closeIntervals`" :placeholder="PARAMS_ENUM[item.code]">
+                  <lay-input v-model="modelParams.robotConfigs.closeIntervals" />
+                </lay-form-item>
+              </lay-col>
+            </template>
+          </div>
+        </lay-row>
+      </lay-form>
+      <div class="operator-wp">
+        <lay-button :loading="modelConfig.loading" type="primary" size="sm" @click="handleSaveAndStart">保存并开机</lay-button>
+      </div>
+    </div>
+  </lay-layer>
+</template>
+
+<script lang="ts" setup>
+import Decimal from "decimal.js";
+import { ref, reactive, getCurrentInstance } from "vue";
+import { update_robot_batch, get_server_select, get_strategy_select, get_strategy_program_select, get_strategy_parameter_select, get_apikey_select, set_robot_status } from "@/api";
+
+const PARAMS_ENUM: any = {
+  open: "挂单距离百分比",
+  close: "平仓距离百分比",
+  lever_rate: "杠杆倍数",
+  stop_loss: "百分比最大回撤",
+  pair: "不带USDT会自动补齐",
+  exchange: "交易盘口",
+  ref_pair: "不带USDT会自动补齐",
+  ref_exchange: "参考盘口",
+  grid: "网格数量",
+  hold_coin: "现货底仓",
+  account: "账户",
+};
+
+const { proxy }: any = getCurrentInstance();
+
+const modelFormRef = ref();
+
+interface ModelConfig {
+  title: string;
+  visible: boolean;
+  isUpdate: boolean;
+  loading: boolean;
+}
+interface ModelParams {
+  id?: number;
+  name?: string;
+  serverId?: number;
+  strategyId?: number;
+  strategyProgramId?: number;
+  startAmount?: number;
+  callPort?: number;
+  remark?: string;
+  robotConfigs?: any;
+}
+
+let modelParams = ref<ModelParams>({});
+let modelConfig: ModelConfig = reactive({ title: "", visible: false, isUpdate: false, loading: false });
+
+let handleResult = reactive<{ resolve?: any; reject?: any }>({});
+
+let serverList = ref();
+let strategyList = ref();
+let strategyProgramList = ref();
+let strategyParameterList = ref();
+let apikeyList = ref();
+
+let botDetailList = ref();
+
+const show = async (params?: any) => {
+  initData();
+  get_server_list();
+  get_strategy_list();
+  get_apikey();
+  modelConfig.visible = true;
+  modelConfig.title = "批量设置机器人";
+
+  modelParams.value = { strategyId: params[0].strategyId, strategyProgramId: params[0].strategyProgramId, robotConfigs: { openIntervals: "0.00", closeIntervals: "0.00" } };
+  botDetailList.value = params;
+  strategyHandleChange(modelParams.value.strategyId);
+  strategyProgramHandleChange(modelParams.value.strategyProgramId);
+  return new Promise(async (resolve, reject) => {
+    handleResult.resolve = resolve;
+    handleResult.reject = reject;
+  });
+};
+// 获取服务器列表
+const get_server_list = () => {
+  const params = {};
+  get_server_select(params, (data: any) => {
+    serverList.value = data.data;
+  });
+};
+// 获取策略列表
+const get_strategy_list = () => {
+  const params = {};
+  get_strategy_select(params, (data: any) => {
+    strategyList.value = data.data;
+  });
+};
+// 获取策略版本列表
+const get_strategy_program_list = (value: number) => {
+  const params = { strategyId: value };
+  get_strategy_program_select(params, (data: any) => {
+    strategyProgramList.value = data.data;
+  });
+};
+// 获取策略版本参数下拉
+const get_strategy_parameter = (value: number) => {
+  const params = { programId: value };
+  get_strategy_parameter_select(params, (data: any) => {
+    strategyParameterList.value = data.data;
+  });
+};
+// 获取APIKEY参数下拉
+const get_apikey = () => {
+  const params = {};
+  get_apikey_select(params, (data: any) => {
+    apikeyList.value = data.data.map((item: any) => ({ label: item.value, value: item.id }));
+  });
+};
+
+// 选择策略操作
+const strategyHandleChange = (e: any) => {
+  get_strategy_program_list(e);
+};
+// 选择策略版本操作
+const strategyProgramHandleChange = (e: any) => {
+  get_strategy_parameter(e);
+};
+
+const handleStringInput = (code: string, value: any) => {
+  if (code == "pair") modelParams.value.robotConfigs.ref_pair = value.trim();
+};
+const handleNmberInput = (code: string, value: any) => {
+  if (code == "lever_rate") {
+    modelParams.value.robotConfigs.stop_loss = new Decimal(value.trim()).dividedBy(100) > new Decimal(0.06) ? 0.06 : new Decimal(value.trim()).dividedBy(100);
+  }
+};
+
+const handleParams = (value: any) => {
+  let openIntervals = new Decimal(0);
+  let closeIntervals = new Decimal(0);
+  return botDetailList.value.map((item: any) => {
+    const configList = item.configList || [];
+    let configs = configList.map((item: any) => {
+      delete item.deleted;
+      delete item.id;
+      delete item.robotId;
+      let result = { ...item, val: value.robotConfigs[item.code] || item.val };
+      if (result.code == "open") {
+        result.val = new Decimal(result.val || 0).plus(openIntervals || 0);
+        openIntervals = new Decimal(openIntervals).plus(value.robotConfigs.openIntervals || 0);
+      }
+      if (result.code == "close") {
+        result.val = new Decimal(result.val || 0).plus(closeIntervals || 0);
+        closeIntervals = new Decimal(closeIntervals).plus(value.robotConfigs.closeIntervals || 0);
+      }
+      if (["pair", "ref_pair"].includes(result.code)) {
+        if (!result.val.toLowerCase().endsWith("_usdt")) result.val = result.val + "_usdt";
+      }
+      return result;
+    });
+    return {
+      id: item.id,
+      name: value.name || item.name,
+      serverId: value.serverId || item.serverId,
+      strategyId: value.strategyId || item.strategyId,
+      strategyProgramId: value.strategyProgramId || item.strategyProgramId,
+      callPort: value.callPort || item.callPort,
+      startAmount: value.startAmount || item.startAmount,
+      remark: item.remark,
+      robotConfigs: configs,
+    };
+  });
+};
+
+const initData = () => {
+  serverList.value = [];
+  strategyList.value = [];
+  strategyProgramList.value = [];
+  strategyParameterList.value = [];
+  apikeyList.value = [];
+};
+
+const handleStatus = (ids: any, status: String) => {
+  const params = { robotIds: ids, status };
+  modelConfig.loading = true;
+  set_robot_status(params, (data: any) => {
+    modelConfig.loading = false;
+    if (data.code == 200) {
+      proxy.$message("执行成功!");
+      modelConfig.visible = false;
+      handleResult.resolve(true);
+    }
+  });
+};
+
+const hanldeUpdate = () => {
+  return new Promise(async (resolve) => {
+    const params = handleParams(modelParams.value);
+    modelConfig.loading = true;
+    update_robot_batch(params, (data: any) => {
+      modelConfig.loading = false;
+      if (data.code == 200) {
+        resolve(true);
+      }
+    });
+  });
+};
+
+const handleSaveAndStart = () => {
+  modelFormRef.value.validate(async (isValidate: boolean) => {
+    if (isValidate) {
+      const result = await hanldeUpdate();
+      const ids = botDetailList.value.map((item: any) => item.id);
+      if (result) handleStatus(ids, "RESTART");
+    }
+  });
+};
+
+const operator = reactive([
+  {
+    text: "确认",
+    callback: () => {
+      modelFormRef.value.validate((isValidate: boolean) => {
+        if (isValidate) {
+          const params = handleParams(modelParams.value);
+          modelConfig.loading = true;
+          update_robot_batch(params, (data: any) => {
+            modelConfig.loading = false;
+            if (data.code == 200) {
+              proxy.$message("批量修改成功!");
+              modelConfig.visible = false;
+              handleResult.resolve(true);
+            }
+          });
+        }
+      });
+    },
+  },
+  {
+    text: "取消",
+    callback: () => {
+      modelConfig.visible = false;
+      handleResult.resolve(false);
+    },
+  },
+]);
+defineExpose({ show });
+</script>
+
+<style lang="scss" scoped>
+.operator-wp {
+  padding-left: 110px;
+}
+</style>

+ 69 - 0
src/views/bot/as/components/Copy.vue

@@ -0,0 +1,69 @@
+<template>
+  <lay-layer :title="modelConfig.title" v-model="modelConfig.visible" area="auto" :btn="operator">
+    <div class="width-500 custom-layer" style="padding: 20px">
+      <lay-form :model="modelParams" ref="modelFormRef" required>
+        <lay-form-item label="复制数量" prop="copyNum">
+          <lay-input v-model="modelParams.copyNum" type="number" :max="50" :min="1" />
+        </lay-form-item>
+      </lay-form>
+    </div>
+  </lay-layer>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, getCurrentInstance } from "vue";
+import { copy_robot } from "@/api";
+
+const { proxy }: any = getCurrentInstance();
+
+interface ModelConfig {
+  title: string;
+  visible: boolean;
+  loading: boolean;
+}
+interface ModelParams {
+  robotId?: string;
+  copyNum?: number;
+}
+
+let modelParams = ref<ModelParams>({});
+let modelConfig: ModelConfig = reactive({ title: "", visible: false, loading: false });
+
+let handleResult = reactive<{ resolve?: any; reject?: any }>({});
+
+const show = (params?: any) => {
+  modelConfig.visible = true;
+  modelConfig.title = "复制机器人";
+  modelParams.value = { robotId: params, copyNum: 1 };
+  return new Promise(async (resolve, reject) => {
+    handleResult.resolve = resolve;
+    handleResult.reject = reject;
+  });
+};
+
+const operator = reactive([
+  {
+    text: "确认",
+    callback: () => {
+      modelConfig.loading = true;
+      const params = { ...modelParams.value };
+      copy_robot(params, (data: any) => {
+        modelConfig.loading = false;
+        if (data.code == 200) {
+          proxy.$message("复制成功!");
+          modelConfig.visible = false;
+          handleResult.resolve(true);
+        }
+      });
+    },
+  },
+  {
+    text: "取消",
+    callback: () => {
+      modelConfig.visible = false;
+      handleResult.resolve(false);
+    },
+  },
+]);
+defineExpose({ show });
+</script>

+ 40 - 0
src/views/bot/as/components/LogText.vue

@@ -0,0 +1,40 @@
+<template>
+  <lay-layer :title="modelConfig.title" v-model="modelConfig.visible" area="auto">
+    <div class="width-1000 custom-layer" style="padding: 20px">
+      <div class="content">
+        {{ modelParams.text }}
+      </div>
+    </div>
+  </lay-layer>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive } from "vue";
+
+interface ModelConfig {
+  title: string;
+  visible: boolean;
+  isUpdate: boolean;
+  loading: boolean;
+}
+interface ModelParams {
+  text?: string;
+}
+
+let modelParams = ref<ModelParams>({});
+let modelConfig: ModelConfig = reactive({ title: "", visible: false, isUpdate: false, loading: false });
+
+const show = (params?: any) => {
+  modelConfig.visible = true;
+  modelConfig.title = "日志详情";
+  modelParams.value = { text: params };
+};
+
+defineExpose({ show });
+</script>
+<style lang="scss" scoped>
+.content {
+  word-wrap: break-word;
+  padding: 10px;
+}
+</style>

+ 88 - 0
src/views/bot/as/components/Transfers.vue

@@ -0,0 +1,88 @@
+<template>
+  <lay-layer :title="modelConfig.title" v-model="modelConfig.visible" area="auto" :btn="operator">
+    <div class="width-450 custom-layer" style="padding: 20px">
+      <lay-form :model="modelParams" ref="modelFormRef" required>
+        <lay-form-item label="移交用户" prop="userId">
+          <lay-select v-model="modelParams.userId">
+            <lay-select-option v-for="item in userList" :value="item.id" :label="item.value" />
+          </lay-select>
+        </lay-form-item>
+      </lay-form>
+    </div>
+  </lay-layer>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, getCurrentInstance } from "vue";
+import { transfers_robot, get_user_list_all } from "@/api";
+
+const { proxy }: any = getCurrentInstance();
+
+const modelFormRef = ref();
+
+interface ModelConfig {
+  title: string;
+  visible: boolean;
+  isUpdate: boolean;
+  loading: boolean;
+}
+interface ModelParams {
+  userId?: string;
+  robotIds?: Array<string>;
+}
+
+let modelParams = ref<ModelParams>({});
+let modelConfig: ModelConfig = reactive({ title: "", visible: false, isUpdate: false, loading: false });
+
+let handleResult = reactive<{ resolve?: any; reject?: any }>({});
+
+let userList = ref();
+
+const show = async (params?: any) => {
+  get_user_list();
+  modelConfig.visible = true;
+  modelConfig.title = "移交机器人";
+  modelParams.value = { robotIds: params };
+  return new Promise(async (resolve, reject) => {
+    handleResult.resolve = resolve;
+    handleResult.reject = reject;
+  });
+};
+// 获取用户列表
+const get_user_list = () => {
+  const params = {};
+  get_user_list_all(params, (data: any) => {
+    userList.value = data.data;
+  });
+};
+
+const operator = reactive([
+  {
+    text: "确认",
+    callback: () => {
+      modelFormRef.value.validate((isValidate: boolean) => {
+        if (isValidate) {
+          modelConfig.loading = true;
+          const params = modelParams.value;
+          transfers_robot(params, (data: any) => {
+            modelConfig.loading = false;
+            if (data.code == 200) {
+              proxy.$message("移交成功!");
+              modelConfig.visible = false;
+              handleResult.resolve(true);
+            }
+          });
+        }
+      });
+    },
+  },
+  {
+    text: "取消",
+    callback: () => {
+      modelConfig.visible = false;
+      handleResult.resolve(false);
+    },
+  },
+]);
+defineExpose({ show });
+</script>

+ 337 - 0
src/views/bot/as/components/Update.vue

@@ -0,0 +1,337 @@
+<template>
+  <lay-layer :title="modelConfig.title" v-model="modelConfig.visible" area="auto" :btn="operator">
+    <div class="width-800 custom-layer" style="padding: 20px">
+      <lay-form :model="modelParams" ref="modelFormRef" required>
+        <lay-row space="10">
+          <lay-col md="12" sm="12" xs="24">
+            <lay-form-item label="名称" prop="name">
+              <lay-input v-model="modelParams.name" placeholder="机器人名称" />
+            </lay-form-item>
+          </lay-col>
+          <lay-col md="12" sm="12" xs="24">
+            <lay-form-item label="初始金额" prop="startAmount">
+              <lay-input v-model="modelParams.startAmount" placeholder="初始金额" />
+            </lay-form-item>
+          </lay-col>
+          <lay-col md="12" sm="12" xs="24">
+            <lay-form-item label="服务器" prop="serverId">
+              <lay-select v-model="modelParams.serverId" :show-search="true">
+                <lay-select-option v-for="item in serverList" :value="item.id" :label="item.value" placeholder="选择服务器" />
+              </lay-select>
+            </lay-form-item>
+          </lay-col>
+          <lay-col md="12" sm="12" xs="24">
+            <lay-form-item label="通讯端口" prop="callPort">
+              <lay-input v-model="modelParams.callPort" placeholder="通讯端口(1001、1002)" />
+            </lay-form-item>
+          </lay-col>
+          <lay-col md="12" sm="12" xs="24">
+            <lay-form-item label="策略" prop="strategyId">
+              <lay-select v-model="modelParams.strategyId" @change="strategyHandleChange">
+                <lay-select-option v-for="item in strategyList" :value="item.id" :label="item.value" placeholder="选择策略" />
+              </lay-select>
+            </lay-form-item>
+          </lay-col>
+          <lay-col md="12" sm="12" xs="24">
+            <lay-form-item label="策略版本" prop="strategyProgramId">
+              <lay-select v-model="modelParams.strategyProgramId" @change="strategyProgramHandleChange">
+                <lay-select-option v-for="item in strategyProgramList" :value="item.id" :label="item.value" placeholder="选择策略版本" />
+              </lay-select>
+            </lay-form-item>
+          </lay-col>
+          <div>
+            <template v-for="item in strategyParameterList">
+              <lay-col md="12" sm="12" xs="24">
+                <lay-form-item :label="item.name" :prop="`robotConfigs.${item.code}`">
+                  <lay-input v-if="item.valType == 0" v-model="modelParams.robotConfigs[item.code]" @input="handleStringInput(item.code, $event)" :placeholder="PARAMS_ENUM[item.code]" />
+                  <lay-input
+                    v-if="item.valType == 1 && item.code != 'colo'"
+                    v-model="modelParams.robotConfigs[item.code]"
+                    @input="handleNmberInput(item.code, $event)"
+                    :placeholder="PARAMS_ENUM[item.code]"
+                  />
+                  <lay-switch v-if="item.valType == 1 && item.code == 'colo'" onswitch-value="1" unswitch-value="0" v-model="modelParams.robotConfigs[item.code]" />
+                  <lay-switch v-if="item.valType == 2" v-model="modelParams.robotConfigs[item.code]" />
+                  <lay-select v-if="item.valType == 3" v-model="modelParams.robotConfigs[item.code]" :show-search="true">
+                    <lay-select-option v-for="items in item.contentVal.split(',')" :value="items" :label="items" :placeholder="PARAMS_ENUM[item.code]" />
+                  </lay-select>
+                  <lay-select v-if="item.valType == 5" v-model="modelParams.robotConfigs[item.code]" :options="apikeyList" :show-search="true" :placeholder="PARAMS_ENUM[item.code]" />
+                </lay-form-item>
+              </lay-col>
+            </template>
+          </div>
+        </lay-row>
+      </lay-form>
+      <div class="operator-wp" v-if="modelConfig.isUpdate">
+        <lay-button :loading="modelConfig.loading" type="primary" size="sm" @click="handleSaveAndStart">保存并重启</lay-button>
+      </div>
+    </div>
+  </lay-layer>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, getCurrentInstance } from "vue";
+import Decimal from "decimal.js";
+import { add_robot, update_robot, get_server_select, get_strategy_select, get_strategy_program_select, get_strategy_parameter_select, get_apikey_select, set_robot_status } from "@/api";
+
+const PARAMS_ENUM: any = {
+  open: "挂单距离百分比",
+  close: "平仓距离百分比",
+  lever_rate: "杠杆倍数",
+  stop_loss: "百分比最大回撤",
+  pair: "不带USDT会自动补齐",
+  exchange: "交易盘口",
+  ref_pair: "不带USDT会自动补齐",
+  ref_exchange: "参考盘口",
+  grid: "网格数量",
+  hold_coin: "现货底仓",
+  account: "账户",
+};
+
+const { proxy }: any = getCurrentInstance();
+
+const modelFormRef = ref();
+
+interface ModelConfig {
+  title: string;
+  visible: boolean;
+  isUpdate: boolean;
+  loading: boolean;
+}
+interface ModelParams {
+  id?: number;
+  name?: string;
+  serverId?: number;
+  strategyId?: number;
+  strategyProgramId?: number;
+  startAmount?: number;
+  callPort?: number;
+  remark?: string;
+  robotConfigs?: any;
+}
+
+let modelParams = ref<ModelParams>({});
+let modelConfig: ModelConfig = reactive({ title: "", visible: false, isUpdate: false, loading: false });
+
+let handleResult = reactive<{ resolve?: any; reject?: any }>({});
+
+let serverList = ref();
+let strategyList = ref();
+let strategyProgramList = ref();
+let strategyParameterList = ref();
+let apikeyList = ref();
+
+const show = async (params?: any) => {
+  const accountId = params.configList?.find((item: any) => item.code == "account")?.val;
+  initData();
+  get_server_list();
+  get_strategy_list();
+  get_apikey(accountId);
+  modelConfig.visible = true;
+  modelConfig.isUpdate = !!params;
+  modelConfig.title = !!params ? "编辑机器人" : "添加机器人";
+  if (modelConfig.isUpdate) {
+    const configList = params.configList || [];
+    let robotConfigs: any = {};
+    configList.map((item: any) => {
+      if (item.valType == 2) {
+        robotConfigs[item.code] = item.val == "true" ? true : false;
+      } else if (item.valType == 5) {
+        robotConfigs[item.code] = item.val ? parseInt(item.val) : undefined;
+      } else {
+        robotConfigs[item.code] = item.val;
+      }
+    });
+
+    modelParams.value = { ...params, robotConfigs };
+    strategyHandleChange(modelParams.value.strategyId);
+    strategyProgramHandleChange(modelParams.value.strategyProgramId);
+  } else {
+    modelParams.value = { robotConfigs: {} };
+  }
+  return new Promise(async (resolve, reject) => {
+    handleResult.resolve = resolve;
+    handleResult.reject = reject;
+  });
+};
+// 获取服务器列表
+const get_server_list = () => {
+  const params = {};
+  get_server_select(params, (data: any) => {
+    serverList.value = data.data;
+  });
+};
+// 获取策略列表
+const get_strategy_list = () => {
+  const params = {};
+  get_strategy_select(params, (data: any) => {
+    strategyList.value = data.data;
+  });
+};
+// 获取策略版本列表
+const get_strategy_program_list = (value: number) => {
+  const params = { strategyId: value };
+  get_strategy_program_select(params, (data: any) => {
+    strategyProgramList.value = data.data;
+  });
+};
+// 获取策略版本参数下拉
+const get_strategy_parameter = (value: number) => {
+  const params = { programId: value };
+  get_strategy_parameter_select(params, (data: any) => {
+    strategyParameterList.value = data.data;
+
+    let defaultValObj: any = {};
+    strategyParameterList.value.map((item: any) => {
+      if (item.valType == 2) {
+        defaultValObj[item.code] = item.defaultVal == "true" ? true : false;
+      } else if (item.valType == 5) {
+        defaultValObj[item.code] = item.defaultVal ? parseInt(item.defaultVal) : undefined;
+      } else {
+        defaultValObj[item.code] = item.defaultVal;
+      }
+    });
+    modelParams.value = { ...modelParams.value, robotConfigs: { ...defaultValObj, ...modelParams.value.robotConfigs } };
+  });
+};
+// 获取APIKEY参数下拉
+const get_apikey = (id: any) => {
+  const params = {};
+  get_apikey_select(params, (data: any) => {
+    apikeyList.value = data.data.filter((item: any) => item.inUseNum < 1 || item.id == id).map((item: any) => ({ label: item.value, value: item.id }));
+  });
+};
+
+// 选择策略操作
+const strategyHandleChange = (e: any) => {
+  get_strategy_program_list(e);
+};
+// 选择策略版本操作
+const strategyProgramHandleChange = (e: any) => {
+  get_strategy_parameter(e);
+};
+
+const handleStringInput = (code: string, value: any) => {
+  if (code == "pair") modelParams.value.robotConfigs.ref_pair = value.trim();
+};
+const handleNmberInput = (code: string, value: any) => {
+  if (code == "lever_rate") {
+    modelParams.value.robotConfigs.stop_loss = new Decimal(value.trim()).dividedBy(100) > new Decimal(0.06) ? 0.06 : new Decimal(value.trim()).dividedBy(100);
+  }
+};
+
+const handleStatus = (ids: any, status: String) => {
+  const params = { robotIds: ids, status };
+  modelConfig.loading = true;
+  set_robot_status(params, (data: any) => {
+    modelConfig.loading = false;
+    if (data.code == 200) {
+      proxy.$message("执行成功!");
+      modelConfig.visible = false;
+      handleResult.resolve(true);
+    }
+  });
+};
+
+const hanldeUpdate = () => {
+  return new Promise(async (resolve) => {
+    const params = handleParams(modelParams.value);
+    modelConfig.loading = true;
+    update_robot(params, (data: any) => {
+      modelConfig.loading = false;
+      if (data.code == 200) {
+        resolve(true);
+      }
+    });
+  });
+};
+
+const handleSaveAndStart = () => {
+  modelFormRef.value.validate(async (isValidate: boolean) => {
+    if (isValidate) {
+      const result = await hanldeUpdate();
+      const ids = [modelParams.value.id];
+      if (result) handleStatus(ids, "RESTART");
+    }
+  });
+};
+
+const handleParams = (value: any) => {
+  const params = {
+    id: value.id,
+    name: value.name,
+    serverId: value.serverId,
+    strategyId: value.strategyId,
+    strategyProgramId: value.strategyProgramId,
+    startAmount: value.startAmount,
+    callPort: value.callPort,
+    remark: value.remark,
+  };
+
+  const robotConfigs = strategyParameterList.value.map((item: any) => {
+    let configs = { name: item.name, code: item.code, val: value.robotConfigs[item.code], valType: item.valType };
+    if (["pair", "ref_pair"].includes(configs.code)) {
+      if (!configs.val.toLowerCase().endsWith("_usdt")) {
+        configs.val = configs.val + "_usdt";
+      }
+    }
+    return configs;
+  });
+  return { ...params, robotConfigs };
+};
+
+const initData = () => {
+  serverList.value = [];
+  strategyList.value = [];
+  strategyProgramList.value = [];
+  strategyParameterList.value = [];
+  apikeyList.value = [];
+};
+
+const operator = reactive([
+  {
+    text: "确认",
+    callback: () => {
+      modelFormRef.value.validate((isValidate: boolean) => {
+        if (isValidate) {
+          modelConfig.loading = true;
+          const params = handleParams(modelParams.value);
+          if (modelConfig.isUpdate) {
+            update_robot(params, (data: any) => {
+              modelConfig.loading = false;
+              if (data.code == 200) {
+                proxy.$message("编辑成功!");
+                modelConfig.visible = false;
+                handleResult.resolve(true);
+              }
+            });
+          } else {
+            add_robot(params, (data: any) => {
+              modelConfig.loading = false;
+              if (data.code == 200) {
+                proxy.$message("添加成功!");
+                modelConfig.visible = false;
+                handleResult.resolve(true);
+              }
+            });
+          }
+        }
+      });
+    },
+  },
+  {
+    text: "取消",
+    callback: () => {
+      modelConfig.visible = false;
+      handleResult.resolve(false);
+    },
+  },
+]);
+defineExpose({ show });
+</script>
+
+<style lang="scss" scoped>
+.operator-wp {
+  padding-left: 110px;
+}
+</style>

+ 594 - 0
src/views/bot/as/detail.vue

@@ -0,0 +1,594 @@
+<template>
+  <div class="container-wp">
+    <div class="robot-info-header">
+      <lay-space>
+        <span class="robot-name">{{ robotDetail.name }}</span>
+        <lay-tag size="sm">
+          <span class="robot-status" v-if="robotDetail.status == 'RUNNING'">
+            <lay-badge type="dot" theme="blue" ripple />
+            <span>{{ ROBOT_STATUS[robotDetail.status] }}</span>
+          </span>
+          <span class="robot-status" v-else-if="robotDetail.status == 'ERROR'">
+            <lay-badge type="dot" ripple />
+            <span>{{ ROBOT_STATUS[robotDetail.status] }}</span>
+          </span>
+          <span class="robot-status" v-else>
+            <lay-badge type="dot" theme="orange" ripple />
+            <span>{{ ROBOT_STATUS[robotDetail.status] }}</span>
+          </span>
+        </lay-tag>
+      </lay-space>
+    </div>
+
+    <lay-card v-show="apiList?.includes('/remaining/list')" class="custom-card">
+      <template v-slot:title>
+        <span class="card-title">净值图</span>
+      </template>
+      <template v-slot:body>
+        <div class="profit-chart" ref="balanceChartRef"></div>
+      </template>
+    </lay-card>
+
+    <lay-card class="custom-card">
+      <template v-slot:title>
+        <span class="card-title">内部数据</span>
+      </template>
+      <template v-slot:body>
+        <div class="predictor-chart" ref="predictorChartRef" @mouseleave="startPredictorInterval" @mouseenter="stopPredictorInterval"></div>
+      </template>
+    </lay-card>
+
+    <lay-card v-if="apiList?.includes('/robot/getRobotLog')" class="custom-card">
+      <template v-slot:title>
+        <span class="card-title">运行日志</span>
+      </template>
+
+      <template v-slot:body>
+        <lay-table :columns="columns" size="sm" resize :data-source="logsList">
+          <template v-slot:text="{ row }">
+            <span class="ellipsis-2" @click="showLog(row.text)">{{ row.text }}</span>
+          </template>
+        </lay-table>
+      </template>
+    </lay-card>
+  </div>
+  <LogText ref="logtextRef" />
+</template>
+<script lang="ts" setup name="BotManageDetail">
+import { ref, reactive, onMounted, onUnmounted, shallowRef } from "vue";
+import { useRoute } from "vue-router";
+import * as echarts from "echarts";
+import dayjs from "dayjs";
+import LogText from "./components/LogText.vue";
+import { get_as_robot_detail, get_robot_detail, get_robot_logs, get_remaining_detail } from "@/api";
+
+const ROBOT_STATUS: any = reactive({
+  STOPPED: "已停止",
+  STOP_PENDING: "停止中",
+  RUNNING: "运行中",
+  START_PENDING: "启动中",
+  RESTART_PENDING: "重启中",
+  DOWNLOADING: "下载中",
+  ERROR: "错误",
+});
+
+const apiList = ref(window.sessionStorage.getItem("_4L_API_LIST"));
+
+const balanceChartRef = ref();
+const predictorChartRef = ref();
+const logtextRef = ref();
+
+const route = useRoute();
+
+interface PageConfig {
+  loading: boolean;
+}
+interface Logs {
+  time?: string;
+  text?: string;
+}
+
+let pageConfig: PageConfig = reactive({
+  loading: false,
+});
+
+const columns = ref([
+  { title: "时间", key: "time", width: 150 },
+  { title: "日志", key: "text", customSlot: "text" },
+]);
+let logsList = ref<Array<Logs>>();
+let robotDetail = ref<any>({});
+let balanceList = ref([]);
+let balanceChart = shallowRef();
+let predictorChart = shallowRef();
+let predictorInterval = ref();
+let timer = ref();
+
+const getPredictorState = () => {
+  const params = { botId: route.params.id, startTime: +new Date() - 7 * 24 * 60 * 60 * 1000, endTime: +new Date() };
+  if (pageConfig.loading) return;
+  pageConfig.loading = true;
+  get_as_robot_detail(params, (data: any) => {
+    pageConfig.loading = false;
+
+    if (data.code == 200) {
+      !predictorChart.value ? initPredictorChart(data.data) : updatePredictorChart(data.data);
+    }
+  });
+};
+getPredictorState();
+
+const initPredictorChart = (data: any) => {
+  if (predictorChart.value != null && !predictorChart.value.isDisposed()) echarts.dispose(predictorChart.value);
+  predictorChart.value = echarts.init(predictorChartRef.value);
+
+  window.removeEventListener("resize", () => predictorChart.value.resize());
+  window.addEventListener("resize", () => predictorChart.value.resize());
+  updatePredictorChart(data);
+};
+const updatePredictorChart = (data: any) => {
+  predictorChart.value.clear();
+  const xData = data.map((item: any) => dayjs(item.update_time * 1).format("MM-DD HH:mm:ss:SSS"));
+  const option = {
+    dataZoom: [
+      {
+        type: "inside",
+        xAxisIndex: [0, 1, 2, 3, 4, 5],
+        start: 0,
+        end: 100,
+      },
+      {
+        xAxisIndex: [0, 1, 2, 3, 4, 5],
+        start: 0,
+        end: 100,
+      },
+    ],
+    tooltip: {
+      trigger: "axis",
+      formatter: (value: any) => {
+        return `时间:${value[0].name}<br />${value.map((item: any) => `${item.marker}${item.seriesName}:${item.value}`).join("<br/>")}`;
+      },
+    },
+    toolbox: {
+      feature: {
+        dataZoom: {},
+        brush: {
+          type: ["rect", "clear"],
+        },
+      },
+    },
+    legend: [
+      {
+        data: ["mid_price", "ask_price", "bid_price", "optimal_ask_price", "optimal_bid_price", "ref_price"],
+        top: "230px",
+      },
+      {
+        data: ["inventory"],
+        top: "380px",
+      },
+      {
+        data: ["spread", "delta_plus", "spread_max", "spread_min"],
+        top: "630px",
+      },
+      {
+        data: ["sigma_square"],
+        top: "780px",
+      },
+      {
+        data: ["gamma"],
+        top: "930px",
+      },
+      {
+        data: ["kappa"],
+        top: "1080px",
+      },
+    ],
+
+    grid: [
+      {
+        top: "50px",
+        left: "60px",
+        right: "60px",
+        height: "150px", // 主图高度
+      },
+      {
+        top: "300px",
+        left: "60px",
+        right: "60px",
+        height: "50px",
+      },
+      {
+        top: "450px",
+        left: "60px",
+        right: "60px",
+        height: "150px",
+      },
+      {
+        top: "700px",
+        left: "60px",
+        right: "60px",
+        height: "50px",
+      },
+      {
+        top: "850px",
+        left: "60px",
+        right: "60px",
+        height: "50px",
+      },
+      {
+        top: "1000px",
+        left: "60px",
+        right: "60px",
+        height: "50px",
+      },
+    ],
+    xAxis: [
+      {
+        type: "category",
+        data: xData,
+      },
+      {
+        gridIndex: 1, // 第1个网格的 x 轴
+        type: "category",
+        data: xData,
+      },
+      {
+        gridIndex: 2, // 第2个网格的 x 轴
+        type: "category",
+        data: xData,
+      },
+      {
+        gridIndex: 3, // 第3个网格的 x 轴
+        type: "category",
+        data: xData,
+      },
+      {
+        gridIndex: 4, // 第4个网格的 x 轴
+        type: "category",
+        data: xData,
+      },
+      {
+        gridIndex: 5, // 第5个网格的 x 轴
+        type: "category",
+        data: xData,
+      },
+    ],
+    yAxis: [
+      {
+        type: "value",
+        min: "dataMin",
+      },
+      {
+        gridIndex: 1, // 第1个网格的 y 轴
+        type: "value",
+        min: "dataMin",
+      },
+      {
+        gridIndex: 2, // 第2个网格的 y 轴
+        type: "value",
+        min: "dataMin",
+      },
+      {
+        gridIndex: 3, // 第3个网格的 y 轴
+        type: "value",
+        min: "dataMin",
+      },
+      {
+        gridIndex: 4, // 第4个网格的 y 轴
+        type: "value",
+        min: "dataMin",
+      },
+      {
+        gridIndex: 5, // 第5个网格的 y 轴
+        type: "value",
+        min: "dataMin",
+      },
+    ],
+    series: [
+      {
+        name: "mid_price",
+        type: "line",
+        data: data.map((item: any) => item.mid_price),
+      },
+      {
+        name: "ask_price",
+        type: "line",
+        data: data.map((item: any) => item.ask_price),
+      },
+      {
+        name: "bid_price",
+        type: "line",
+        data: data.map((item: any) => item.bid_price),
+      },
+      {
+        name: "optimal_ask_price",
+        type: "line",
+        data: data.map((item: any) => item.optimal_ask_price),
+        lineStyle: {
+          type: "dashed",
+        },
+      },
+      {
+        name: "optimal_bid_price",
+        type: "line",
+        data: data.map((item: any) => item.optimal_bid_price),
+        lineStyle: {
+          type: "dashed",
+        },
+      },
+      {
+        name: "ref_price",
+        type: "line",
+        data: data.map((item: any) => item.ref_price),
+      },
+
+      {
+        name: "inventory",
+        type: "line",
+        xAxisIndex: 1,
+        yAxisIndex: 1,
+        data: data.map((item: any) => item.inventory),
+      },
+
+      {
+        name: "spread",
+        type: "line",
+        xAxisIndex: 2,
+        yAxisIndex: 2,
+        data: data.map((item: any) => item.spread),
+      },
+      {
+        name: "delta_plus",
+        type: "line",
+        xAxisIndex: 2,
+        yAxisIndex: 2,
+        data: data.map((item: any) => item.delta_plus),
+      },
+      {
+        name: "spread_max",
+        type: "line",
+        xAxisIndex: 2,
+        yAxisIndex: 2,
+        data: data.map((item: any) => item.spread_max),
+      },
+      {
+        name: "spread_min",
+        type: "line",
+        xAxisIndex: 2,
+        yAxisIndex: 2,
+        data: data.map((item: any) => item.spread_min),
+      },
+
+      {
+        name: "sigma_square",
+        type: "line",
+        xAxisIndex: 3,
+        yAxisIndex: 3,
+        data: data.map((item: any) => item.sigma_square),
+      },
+
+      {
+        name: "gamma",
+        type: "line",
+        xAxisIndex: 4,
+        yAxisIndex: 4,
+        data: data.map((item: any) => item.gamma),
+      },
+
+      {
+        name: "kappa",
+        type: "line",
+        xAxisIndex: 5,
+        yAxisIndex: 5,
+        data: data.map((item: any) => item.kappa),
+      },
+    ],
+  };
+  predictorChart.value.setOption(option, true);
+};
+
+const startPredictorInterval = () => {
+  if (!predictorInterval.value) {
+    predictorInterval.value = setInterval(() => {
+      getPredictorState();
+    }, 5000);
+  }
+};
+
+// 停止定时请求
+const stopPredictorInterval = () => {
+  if (predictorInterval.value) {
+    clearInterval(predictorInterval.value);
+    predictorInterval.value = null; // 释放定时器
+  }
+};
+
+// 获取机器人详情
+const getRobotDetail = () => {
+  const params = { id: route.params.id };
+  pageConfig.loading = true;
+  get_robot_detail(params, (data: any) => {
+    pageConfig.loading = false;
+    if (data.code == 200) {
+      robotDetail.value = data.data;
+      document.title = data.data.name;
+      getBalanceInfo(data.data.accId);
+    }
+  });
+};
+const getBalanceInfo = (id: number) => {
+  const params = { id: id };
+  pageConfig.loading = true;
+  get_remaining_detail(params, (data: any) => {
+    pageConfig.loading = false;
+    if (data.code == 200) {
+      balanceList.value = data.data;
+
+      const xData = data.data.map((item: any) => item.creationTime);
+      const sData = data.data.map((item: any) => [item.creationTime, item.afterU, item.changeU, item.pair, item.openNum, item.closeNum]);
+      const yMinData = Math.min(sData.map((item: any) => item[1]));
+
+      !balanceChart.value
+        ? initBalanceChart(data.data)
+        : balanceChart.value.setOption({
+            xAxis: {
+              type: "category",
+              boundaryGap: false,
+              data: xData,
+            },
+            yAxis: {
+              type: "value",
+              boundaryGap: [0, "100%"],
+              min: yMinData,
+              interval: 1,
+            },
+            series: {
+              name: "Balance",
+              type: "line",
+              areaStyle: {},
+              lineStyle: {
+                width: 1,
+              },
+              data: sData,
+            },
+          });
+    }
+  });
+};
+
+// 请求机器人日志
+const getLogsInfo = () => {
+  pageConfig.loading = true;
+  const params = { id: route.params.id, n: 500 };
+  pageConfig.loading = true;
+  get_robot_logs(params, (data: any) => {
+    pageConfig.loading = false;
+    if (data.code == 200) {
+      logsList.value = handlePageInfo(data.data);
+    }
+  });
+};
+getLogsInfo();
+
+const showLog = (data: any) => {
+  logtextRef.value.show(data);
+};
+
+const handlePageInfo = (data: any) => {
+  let infoList = data;
+  let result = infoList.map((item: string) => {
+    return { time: item.slice(0, 18), text: item.slice(19).trim() };
+  });
+  return result;
+};
+
+const initBalanceChart = (data: any) => {
+  if (balanceChart.value != null && !balanceChart.value.isDisposed()) echarts.dispose(balanceChart.value);
+
+  balanceChart.value = echarts.init(balanceChartRef.value);
+
+  window.removeEventListener("resize", () => balanceChart.value.resize());
+  window.addEventListener("resize", () => balanceChart.value.resize());
+
+  const xData = data.map((item: any) => item.creationTime);
+  const sData = data.map((item: any) => [item.creationTime, item.afterU, item.changeU, item.pair, item.openNum, item.closeNum]);
+  const yMinData = Math.min(sData);
+
+  const balanceChartOption = {
+    tooltip: {
+      trigger: "axis",
+      axisPointer: {
+        type: "cross",
+      },
+      formatter: (params: any) => {
+        let info = params[0];
+        return `${info.marker}${info.seriesName}<br/>时间:${info.value[0]} <br/>余额:${info.value[1]}<br/>收益:${info.value[2]}<br/>币对:${info.value[3]}<br/>开仓:${info.value[4]}<br/>平仓:${info.value[5]}`;
+      },
+    },
+    dataZoom: [
+      {
+        start: 0,
+        end: 100,
+      },
+      {
+        type: "inside",
+        start: 0,
+        end: 100,
+      },
+    ],
+    xAxis: {
+      type: "category",
+      boundaryGap: false,
+      data: xData,
+    },
+    yAxis: {
+      type: "value",
+      boundaryGap: [0, "100%"],
+      min: yMinData,
+      interval: 1,
+    },
+    series: {
+      name: "Balance",
+      type: "line",
+      areaStyle: {},
+      data: sData,
+    },
+  };
+  balanceChart.value.setOption(balanceChartOption);
+};
+
+timer.value = setInterval(() => {
+  getBalanceInfo(robotDetail.value.accId);
+  getLogsInfo();
+}, 5000);
+
+getLogsInfo();
+
+onMounted(() => {
+  startPredictorInterval();
+  getRobotDetail();
+});
+
+onUnmounted(() => {
+  window.removeEventListener("resize", () => balanceChart.value.resize());
+  clearInterval(timer.value);
+  predictorChart.value.dispose();
+  stopPredictorInterval();
+});
+</script>
+
+<style lang="scss" scoped>
+.ellipsis-2 {
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  overflow: hidden;
+  line-break: anywhere;
+  -webkit-line-clamp: 2; /* 控制显示的行数 */
+}
+.container-wp {
+  min-height: 100%;
+  padding: 20px 40px;
+  background-color: rgb(244, 246, 247);
+}
+.profit-chart {
+  height: 300px;
+}
+.predictor-chart {
+  height: 1200px;
+}
+.robot-info-header {
+  background-color: white;
+  padding: 16px 24px;
+  margin-bottom: 20px;
+  .robot-name {
+    font-size: 14px;
+    font-weight: bold;
+  }
+  .robot-status {
+    span {
+      font-size: 12px;
+      padding-left: 4px;
+    }
+  }
+}
+</style>

+ 507 - 0
src/views/bot/as/index.vue

@@ -0,0 +1,507 @@
+<template>
+  <lay-card class="custom-card">
+    <template v-slot:title>
+      <span class="card-title">机器人管理</span>
+    </template>
+    <template v-slot:extra>
+      <lay-button class="card-button" v-if="apiList?.includes('/robot/transfers')" @click="handleTransfersRobot(selectedKeys)">移交机器人</lay-button>
+      <lay-button class="card-button" v-if="apiList?.includes('/robot/copy')" @click="handleCopyRobot(selectedKeys)">复制机器人</lay-button>
+      <lay-button class="card-button" v-if="apiList?.includes('/robot/save')" @click="handleUpdate(0)">添加</lay-button>
+    </template>
+
+    <template v-slot:body>
+      <div class="custom-form-layout">
+        <lay-form class="form-wp" :model="pageParams" mode="inline" size="sm">
+          <lay-form-item label="关键字" prop="textContent">
+            <lay-input v-model="pageParams.textContent" />
+          </lay-form-item>
+          <lay-form-item label="币对" prop="symbol">
+            <lay-input v-model="pageParams.symbol" />
+          </lay-form-item>
+          <lay-form-item label="状态" prop="robotState">
+            <lay-select v-model="pageParams.robotState" :show-search="true" allowClear>
+              <lay-select-option v-for="(value, key) of ROBOT_STATUS" :value="key" :label="value" />
+            </lay-select>
+          </lay-form-item>
+          <lay-form-item label="盘口" prop="exchange">
+            <lay-select v-model="pageParams.exchange" :show-search="true" allowClear>
+              <lay-select-option v-for="item in exchangeList" :value="item.code" :label="item.code" />
+            </lay-select>
+          </lay-form-item>
+          <lay-form-item label="所属人" prop="userName">
+            <lay-select v-model="pageParams.userName" :show-search="true" allowClear>
+              <lay-select-option v-for="item in userList" :value="item.value" :label="item.value" />
+            </lay-select>
+          </lay-form-item>
+          <div class="form-button-wp">
+            <lay-button @click="getPageInfo(true)">搜索</lay-button>
+          </div>
+        </lay-form>
+      </div>
+      <div class="custom-operator-wp padding-bottom-10">
+        <div class="custom-group collect-wp" v-html="collectInfo" />
+        <div class="custom-group">
+          <lay-space>
+            <lay-button v-if="apiList?.includes('/robot/submitStatus')" :border="'green'" size="xs" @click="handleStatus(selectedKeys, 'RUN')">开机</lay-button>
+            <lay-button v-if="apiList?.includes('/robot/submitStatus')" :border="'green'" size="xs" @click="handleStatus(selectedKeys, 'RESTART')">重启</lay-button>
+            <lay-button v-if="apiList?.includes('/robot/submitStatus')" :border="'red'" size="xs" @click="handleStatus(selectedKeys, 'STOP')">停机</lay-button>
+            <lay-button v-if="apiList?.includes('/robot/resetCapital')" :border="'green'" size="xs" @click="handleResetCapital(selectedKeys)">复位本金</lay-button>
+          </lay-space>
+        </div>
+        <div class="custom-group">
+          <lay-space>
+            <lay-button v-if="apiList?.includes('/robot/update')" :border="'green'" size="xs" @click="handleBatchUpdate(selectedKeys)">批量设置</lay-button>
+            <lay-button v-if="apiList?.includes('/robot/update')" :border="'green'" size="xs" @click="handleCopyParams(selectedKeys)">复制参数</lay-button>
+          </lay-space>
+        </div>
+        <div class="custom-group">
+          <lay-space>
+            <lay-tooltip position="bottom" content="把所有机器人杠杆调整成0.1,这样停机可以防止遗留仓位。">
+              <lay-button :border="'red'" size="xs" @click="handleSurviveRobot()"> 保命 </lay-button>
+            </lay-tooltip>
+            <lay-tooltip position="bottom" content="还原杠杆,可能会有的机器人没有开机成功,要检查下。">
+              <lay-button :border="'red'" size="xs" @click="handleRestoreRobot()"> 还原 </lay-button>
+            </lay-tooltip>
+          </lay-space>
+        </div>
+      </div>
+      <div>
+        <lay-table :page="tablePage" :columns="columns" resize id="id" :data-source="dataSource" v-model:selected-keys="selectedKeys" :loading="pageConfig.loading" @change="handleCurrentChange">
+          <template v-slot:name="{ row }">
+            <span v-if="apiList?.includes('/robot/findById')" class="normal-color" @click="jumpDetail(row)">{{ row.name }}</span>
+            <span v-else>{{ row.name }}</span>
+          </template>
+          <template v-slot:account="{ row }">
+            <span>{{ JSON.parse(row.configsJson || "{}").account }}</span>
+          </template>
+          <template v-slot:pair="{ row }">
+            <span>{{ JSON.parse(row.configsJson || "{}").pair }}</span>
+          </template>
+          <template v-slot:earningRate="{ row }">
+            <span :class="{ 'primary-color': row.earningRate > 0, 'danger-color': row.earningRate < 0 }">{{ row.earningRate || 0 }}%</span>
+          </template>
+          <template v-slot:status="{ row }">
+            <lay-space v-if="row.status == 'RUNNING'">
+              <lay-badge type="dot" theme="blue" ripple />
+              <span>{{ ROBOT_STATUS[row.status] }}</span>
+            </lay-space>
+            <lay-space v-else-if="row.status == 'ERROR'">
+              <lay-badge type="dot" />
+              <span>{{ ROBOT_STATUS[row.status] }}</span>
+            </lay-space>
+            <lay-space v-else>
+              <lay-badge type="dot" theme="orange" :ripple="row.status == 'STOPPED' ? false : true" />
+              <span>{{ ROBOT_STATUS[row.status] }}</span>
+            </lay-space>
+          </template>
+          <template v-slot:ip="{ row }">
+            {{ `${row.serverIp}:${row.callPort}` }}
+          </template>
+          <template v-slot:configs="{ row }">
+            <lay-tooltip :content="row.configs || ''">
+              <div class="ellipsis-2" @click="handleUpdate(row)">
+                <span class="danger-color" v-if="apiList?.includes('/robot/setAuto') && row.isAuto == 1">[自动调参]</span>{{ `${row.configs || ""}` }}
+              </div>
+            </lay-tooltip>
+          </template>
+          <template v-slot:lastReportTime="{ row }">
+            <span :class="{ 'danger-color': timeConverts(row.lastReportTime).indexOf('秒') == -1 }">{{ row.lastReportTime ? timeConverts(row.lastReportTime) : "未通讯" }}</span>
+          </template>
+          <template v-slot:posNum="{ row }">
+            <span :class="{ 'danger-color': row.posNum > 0 }">{{ row.posNum > 0 ? row.posNum : 0 }}</span>
+          </template>
+          <template v-slot:updateTime="{ row }">
+            <span>{{ timeConverts(row.updateTime) }}</span>
+          </template>
+          <template v-slot:operator="{ row }">
+            <lay-space>
+              <TableButton v-if="apiList?.includes('/robot/submitStatus') && ['STOPPED', 'STOP_PENDING', 'ERROR'].includes(row.status)" text="开机" @click="handleStatus([row.id], 'RUN')" />
+              <TableButton
+                v-if="apiList?.includes('/robot/submitStatus') && ['RUNNING', 'START_PENDING', 'RESTART_PENDING'].includes(row.status)"
+                text="停机"
+                @click="handleStatus([row.id], 'STOP')"
+              />
+              <TableButton v-if="apiList?.includes('/robot/update')" text="编辑" @click="handleUpdate(row)" />
+              <TableButton
+                v-if="apiList?.includes('/robot/submitStatus') && ['RUNNING', 'START_PENDING', 'RESTART_PENDING'].includes(row.status)"
+                text="重启"
+                @click="handleStatus([row.id], 'RESTART')"
+              />
+              <TableButton v-if="apiList?.includes('/robot/delete') && ['STOPPED', 'STOP_PENDING', 'ERROR'].includes(row.status)" text="删除" @click="handleDelete(row)" />
+
+            </lay-space>
+          </template>
+        </lay-table>
+      </div>
+      <div class="custom-operator-wp padding-top-10">
+        <div class="custom-group collect-wp" v-html="collectInfo" />
+        <div class="custom-group">
+          <lay-space>
+            <lay-button v-if="apiList?.includes('/robot/submitStatus')" :border="'green'" size="xs" @click="handleStatus(selectedKeys, 'RUN')">开机</lay-button>
+            <lay-button v-if="apiList?.includes('/robot/submitStatus')" :border="'green'" size="xs" @click="handleStatus(selectedKeys, 'RESTART')">重启</lay-button>
+            <lay-button v-if="apiList?.includes('/robot/submitStatus')" :border="'red'" size="xs" @click="handleStatus(selectedKeys, 'STOP')">停机</lay-button>
+            <lay-button v-if="apiList?.includes('/robot/resetCapital')" :border="'green'" size="xs" @click="handleResetCapital(selectedKeys)">复位本金</lay-button>
+          </lay-space>
+        </div>
+        <div class="custom-group">
+          <lay-space>
+            <lay-button v-if="apiList?.includes('/robot/update')" :border="'green'" size="xs" @click="handleBatchUpdate(selectedKeys)">批量设置</lay-button>
+            <lay-button v-if="apiList?.includes('/robot/update')" :border="'green'" size="xs" @click="handleCopyParams(selectedKeys)">复制参数</lay-button>
+          </lay-space>
+        </div>
+        <div class="custom-group">
+          <lay-space>
+            <lay-tooltip position="bottom" content="把所有机器人杠杆调整成0.1,这样停机可以防止遗留仓位。">
+              <lay-button :border="'red'" size="xs" @click="handleSurviveRobot()"> 保命 </lay-button>
+            </lay-tooltip>
+            <lay-tooltip position="bottom" content="还原杠杆,可能会有的机器人没有开机成功,要检查下。">
+              <lay-button :border="'red'" size="xs" @click="handleRestoreRobot()"> 还原 </lay-button>
+            </lay-tooltip>
+          </lay-space>
+        </div>
+      </div>
+    </template>
+  </lay-card>
+  <BatchUpdate ref="batchUpdateRef" />
+  <Update ref="updateRef" />
+  <Copy ref="copyRef" />
+  <Transfers ref="transfersRef" />
+  <Automate ref="automateRef" />
+</template>
+
+<script lang="ts" setup name="BotManage">
+import { ref, reactive, getCurrentInstance, onBeforeUnmount } from "vue";
+import BatchUpdate from "./components/BatchUpdate.vue";
+import Update from "./components/Update.vue";
+import Transfers from "./components/Transfers.vue";
+import Copy from "./components/Copy.vue";
+import Automate from "./components/Automate.vue";
+import TableButton from "@/components/TableButton.vue";
+import { timeConverts } from "@/utils/index";
+import { get_as_robot_list, delete_robot, set_robot_status, set_robot_reset_capital, update_robot_batch, survive_robot, restore_robot, get_user_list_all, get_exchange_list_all } from "@/api";
+
+const ROBOT_STATUS: any = reactive({
+  STOPPED: "已停止",
+  STOP_PENDING: "停止中",
+  RUNNING: "运行中",
+  START_PENDING: "启动中",
+  RESTART_PENDING: "重启中",
+  DOWNLOADING: "下载中",
+  ERROR: "错误",
+});
+
+const { proxy }: any = getCurrentInstance();
+const batchUpdateRef = ref();
+const updateRef = ref();
+const copyRef = ref();
+const transfersRef = ref();
+const automateRef = ref();
+
+const apiList = ref(window.sessionStorage.getItem("_4L_API_LIST"));
+
+interface PageConfig {
+  loading: boolean;
+}
+
+let pageConfig: PageConfig = reactive({
+  loading: false,
+});
+
+interface FormItem {
+  pageNum?: Number;
+  pageSize?: Number;
+  textContent?: String;
+  symbol?: String;
+  robotState?: String;
+  exchange?: String;
+  userName?: String;
+}
+const pageParams: FormItem = reactive({ pageNum: 1, pageSize: 50 });
+
+interface TablePage {
+  current: number;
+  limit: number;
+  total: number;
+}
+const tablePage: TablePage = reactive({ current: 1, limit: 50, total: 0, limits: [20, 50, 100, 200, 500] });
+const columns = ref([
+  { title: "选项", width: "44px", type: "checkbox" },
+  { title: "ID", width: "50px", key: "id" },
+  { title: "名称", width: "90px", key: "name", customSlot: "name", ellipsisTooltip: true },
+  { title: "账户", width: "90px", key: "account", customSlot: "account", ellipsisTooltip: true },
+  { title: "币对", width: "90px", key: "pair", customSlot: "pair", align: "center", ellipsisTooltip: true },
+  { title: "起始", width: "90px", key: "startAmount", align: "center" },
+  { title: "收益", width: "70px", key: "earningRate", customSlot: "earningRate", align: "center" },
+  { title: "状态", width: "90px", key: "status", customSlot: "status", align: "center" },
+  { title: "参数", key: "configs", customSlot: "configs" },
+  { title: "IP:编号", width: "120px", key: "ip", customSlot: "ip", ellipsisTooltip: true },
+  { title: "通讯", width: "80px", key: "lastReportTime", customSlot: "lastReportTime", align: "center" },
+  { title: "持仓信息", width: "80px", key: "posNum", customSlot: "posNum", align: "center", ellipsisTooltip: true },
+  { title: "修改", width: "90px", key: "updateTime", customSlot: "updateTime", align: "center" },
+  { title: "所属人", width: "80px", key: "userName", align: "center", ellipsisTooltip: true },
+  {
+    title: "操作",
+    width: "160px",
+    customSlot: "operator",
+    key: "operator",
+    ignoreExport: true,
+  },
+]);
+
+let dataSource = ref([]);
+let selectedKeys = ref([]);
+let collectInfo = ref(`[0/0] 利润:<span class="primary-color">0(0%)</span> 初始:<span class="primary-color">0</span> 现有:<span class="primary-color">0</span>`);
+let userList = ref();
+let exchangeList = ref();
+
+// 请求机器人列表
+const getPageInfo = (isSearch?: boolean) => {
+  if (isSearch) {
+    pageParams.pageNum = 1;
+    selectedKeys.value = [];
+  }
+  pageConfig.loading = true;
+  get_as_robot_list(pageParams, (data: any) => {
+    pageConfig.loading = false;
+    if (data.code == 200) {
+      dataSource.value = data.data.list;
+      tablePage.total = data.data.total;
+      handleShowInfo(data.data);
+    }
+  });
+};
+getPageInfo();
+
+// 获取用户列表
+const get_user_list = () => {
+  const params = {};
+  get_user_list_all(params, (data: any) => {
+    userList.value = data.data;
+  });
+};
+get_user_list();
+
+// 获取交易所列表
+const get_exchange_list = () => {
+  const params = {};
+  get_exchange_list_all(params, (data: any) => {
+    exchangeList.value = data.data;
+  });
+};
+get_exchange_list();
+
+let refreshInterval = setInterval(() => {
+  get_as_robot_list(pageParams, (data: any) => {
+    if (data.code == 200) {
+      dataSource.value = data.data.list;
+      tablePage.total = data.data.total;
+      handleShowInfo(data.data);
+    }
+  });
+}, 2000);
+
+const jumpDetail = (info: any) => {
+  window.open(`/bot/as/detail/${info.id}`);
+};
+
+const handleShowInfo = (info: any) => {
+  document.title = `[${info.runNum || 0}/${info.total || 0}] 利润:${info.income || 0}(${info.incomeRate || 0}%)
+    初始:${info.startAmount || 0} 现有:${info.nowAmount || 0}`;
+  collectInfo.value = `[${info.runNum || 0}/${info.total || 0}] 利润:<span class="${info.income >= 0 ? "primary-color" : "danger-color"}">${info.income || 0}(${info.incomeRate || 0}%)</span>
+    初始:<span class="primary-color">${info.startAmount || 0}</span> 现有:<span class="primary-color">${info.nowAmount || 0}</span>`;
+};
+
+const handleTransfersRobot = async (ids: any) => {
+  if (ids.length == 0) return proxy.$message(`请先选择要移交机器!`, 7);
+  const result = await transfersRef.value.show(ids);
+  if (result) getPageInfo(true);
+};
+
+const handleCopyRobot = async (ids: any) => {
+  if (ids.length > 1 || ids.length == 0) return proxy.$message(`请勾选1个要复制的机器!`, 7);
+  const result = await copyRef.value.show(ids[0]);
+  if (result) {
+    selectedKeys.value = [];
+    getPageInfo();
+  }
+};
+// 批量修改
+const handleBatchUpdate = async (ids: any) => {
+  if (ids.length == 0) return proxy.$message(`请先选择要设置机器!`, 7);
+  const botList = dataSource.value.filter((item: any) => ids.includes(item.id));
+  const runningBotList = botList.filter((item: any) => ["RUNNING", "START_PENDING"].includes(item.status));
+  if (runningBotList.length > 0) return proxy.$message(`请先停止正在运行的机器!`, 7);
+  const result = await batchUpdateRef.value.show(botList);
+  if (result) getPageInfo();
+};
+// 复制参数
+const handleCopyParams = async (ids: any) => {
+  if (ids.length == 0) return proxy.$message(`请先选择要复制参数机器!`, 7);
+  if (ids.length == 1) return proxy.$message(`请先至少选择两台机器!`, 7);
+  const firstBot: any = dataSource.value.find((item: any) => item.id == ids[0]);
+  const otherBot = dataSource.value.filter((item: any) => ids.includes(item.id) && item.id != ids[0]);
+  const result = await proxy.$waitingConfirm(`是否确认要把机器"${firstBot.name}"参数复制给"${otherBot.map((item: any) => item.name).join(",")}"?`);
+  if (!result) return;
+  const configs =
+    firstBot.configList?.filter((item: any) => {
+      delete item.deleted;
+      delete item.id;
+      delete item.robotId;
+      return ![4, 5].includes(item.valType);
+    }) || [];
+  const params = otherBot.map((item: any) => {
+    const botConfigs: any =
+      item.configList?.filter((item: any) => {
+        delete item.deleted;
+        delete item.id;
+        delete item.robotId;
+        return [4, 5].includes(item.valType);
+      }) || [];
+    return {
+      id: item.id,
+      name: item.name,
+      serverId: item.serverId,
+      strategyId: item.strategyId,
+      strategyProgramId: item.strategyProgramId,
+      callPort: item.callPort,
+      startAmount: item.startAmount,
+      remark: item.remark,
+      robotConfigs: [...configs, ...botConfigs],
+    };
+  });
+  update_robot_batch(params, (data: any) => {
+    pageConfig.loading = false;
+    if (data.code == 200) {
+      proxy.$message(`复制成功!`);
+      getPageInfo();
+    }
+  });
+};
+
+// 保命方法
+const handleSurviveRobot = async () => {
+  let result = await proxy.$waitingConfirm(`<div>保命把<span class="danger-color">所有机器人</span>杠杆调整成<span class="danger-color">0.1</span>,是否确认要执行保命操作?</div>`);
+  if (!result) return;
+
+  const params = {};
+  pageConfig.loading = true;
+  survive_robot(params, (data: any) => {
+    pageConfig.loading = false;
+    if (data.code == 200) {
+      proxy.$message(`执行成功!`);
+      getPageInfo();
+    }
+  });
+};
+
+// 还原方法
+const handleRestoreRobot = async () => {
+  let result = await proxy.$waitingConfirm("是否确认要执行还原操作?");
+  if (!result) return;
+
+  const params = {};
+  pageConfig.loading = true;
+  restore_robot(params, (data: any) => {
+    pageConfig.loading = false;
+    if (data.code == 200) {
+      proxy.$message(`执行成功!`);
+      getPageInfo();
+    }
+  });
+};
+
+const handleResetCapital = async (ids: any) => {
+  if (ids.length == 0) return proxy.$message(`请先选择要复位本金机器!`, 7);
+  let result = await proxy.$waitingConfirm("是否确认要复位机器本金?");
+  if (!result) return;
+  const params = ids;
+  set_robot_reset_capital(params, (data: any) => {
+    if (data.code == 200) {
+      proxy.$message(`执行成功!`);
+    }
+  });
+};
+
+const handleStatus = (ids: any, status: String) => {
+  if (ids.length == 0) return proxy.$message(`请先选择要执行命令机器!`, 7);
+  const params = { robotIds: ids, status };
+  set_robot_status(params, (data: any) => {
+    if (data.code == 200) {
+      proxy.$message(`执行成功!`);
+    }
+  });
+};
+
+const handleUpdate = async (value?: any) => {
+  const result = await updateRef.value.show(value);
+  if (result) getPageInfo();
+};
+
+// 删除机器人
+const handleDelete = async (value: any) => {
+  let result = await proxy.$waitingConfirm("是否确认删除该机器人?");
+  if (!result) return;
+  let params = [value.id];
+  pageConfig.loading = true;
+  delete_robot(params, (data: any) => {
+    pageConfig.loading = false;
+    if (data.code == 200) {
+      proxy.$message(`删除成功!`);
+      getPageInfo();
+    }
+  });
+};
+
+// 分页设置
+const handleCurrentChange = (val: any) => {
+  pageParams.pageNum = val.current;
+  pageParams.pageSize = val.limit;
+  getPageInfo();
+};
+
+onBeforeUnmount(() => {
+  document.title = "4L CAPITAL";
+  clearInterval(refreshInterval);
+});
+</script>
+
+<style lang="scss" scoped>
+.custom-operator-wp {
+  .custom-group {
+    margin-right: 14px;
+    display: inline-block;
+    vertical-align: middle;
+  }
+  .collect-wp {
+    line-height: 24px;
+    padding: 0 4px;
+    border: 1px solid var(--normal-color);
+    color: var(--normal-color);
+    :deep(.primary-color) {
+      color: var(--primary-color);
+    }
+    :deep(.danger-color) {
+      color: var(--danger-color);
+    }
+  }
+}
+.ellipsis-2 {
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  overflow: hidden;
+  line-break: anywhere;
+  -webkit-line-clamp: 2; /* 控制显示的行数 */
+}
+.custom-form-layout {
+  font-size: 12px !important;
+}
+.primary-color {
+  color: var(--primary-color);
+}
+.normal-color {
+  color: var(--normal-color);
+}
+.danger-color {
+  color: var(--danger-color);
+}
+</style>

+ 6 - 0
src/views/indicator/symbol_filter/index.vue

@@ -78,6 +78,11 @@
                 <div v-for="(value, key) of row.tc">{{ key }}:{{ value }}次</div>
               </div>
             </template>
+            <template v-slot:vdpd="{ row }">
+              <div>
+                <div v-for="(value, key) of row.vdpd">{{ key }}:{{ value }}</div>
+              </div>
+            </template>
             <template v-slot:operator="{ row }">
               <div>
                 <div>
@@ -138,6 +143,7 @@ const columns = ref([
   { title: "振幅", key: "amp", customSlot: "amp", sort: "desc" },
   { title: "交易量", key: "volume", customSlot: "volume", sort: "desc" },
   { title: "交易次数", key: "tc", customSlot: "tc", sort: "desc" },
+  { title: "波动/价格变化", key: "vdpd", customSlot: "vdpd", sort: "desc" },
   { title: "操作", width: "220", key: "operator", customSlot: "operator" },
 ]);
 let dataSource = ref<any>([]);