flyzto 2 тижнів тому
батько
коміт
7bb6ee0ad3

+ 34 - 1
server/models/Clients.js

@@ -87,6 +87,7 @@ const recordRequest = (req) => {
   CLIENTS.Items[key] = {
     key,
     ...clientFields,
+    deviceId: current.deviceId,
     route,
     firstRequestTime: current.firstRequestTime ?? now,
     lastRequestTime: now,
@@ -102,6 +103,36 @@ const getClients = () => {
   .sort((a, b) => (b.lastRequestTime ?? 0) - (a.lastRequestTime ?? 0));
 }
 
+const updateClient = ({ key, deviceId } = {}) => {
+  if (!key || !CLIENTS.Items[key]) {
+    return Promise.reject(new Error('CLIENT_NOT_FOUND'));
+  }
+  if (deviceId === undefined || deviceId === null || deviceId === '') {
+    delete CLIENTS.Items[key].deviceId;
+    saveClientsToCache();
+    return Promise.resolve(CLIENTS.Items[key]);
+  }
+  const parsedDeviceId = Number(deviceId);
+  if (!Number.isInteger(parsedDeviceId)) {
+    return Promise.reject(new Error('DEVICE_ID_INVALID'));
+  }
+  CLIENTS.Items[key] = {
+    ...CLIENTS.Items[key],
+    deviceId: parsedDeviceId,
+  };
+  saveClientsToCache();
+  return Promise.resolve(CLIENTS.Items[key]);
+}
+
+const deleteClient = (key) => {
+  if (!key || !CLIENTS.Items[key]) {
+    return Promise.reject(new Error('CLIENT_NOT_FOUND'));
+  }
+  delete CLIENTS.Items[key];
+  saveClientsToCache();
+  return Promise.resolve();
+}
+
 function saveClientsToCache() {
   if (saveTimer) {
     clearTimeout(saveTimer);
@@ -134,7 +165,9 @@ process.on('SIGUSR2', () => {
 const Clients = {
   recordRequest,
   getClients,
+  updateClient,
+  deleteClient,
 };
 
-export { recordRequest, getClients };
+export { recordRequest, getClients, updateClient, deleteClient };
 export default Clients;

+ 34 - 1
server/routes/pstery.js

@@ -55,7 +55,7 @@ router.post('/update_base_events', (req, res) => {
  */
 router.post('/update_leagues_list', (req, res) => {
   const { mk, leagues, platform } = req.body ?? {};
-  // Clients.recordRequest(req);
+  Clients.recordRequest(req);
   const updateCount = Games.updateLeaguesList({ mk, leagues, platform });
   res.sendSuccess({ updateCount });
 });
@@ -67,6 +67,39 @@ router.get('/get_clients', (req, res) => {
   res.sendSuccess(Clients.getClients());
 });
 
+/**
+ * 更新数据同步客户端信息
+ */
+router.post('/update_client', (req, res) => {
+  Clients.updateClient(req.body)
+  .then(client => {
+    res.sendSuccess(client);
+  })
+  .catch(err => {
+    if (err.message === 'CLIENT_NOT_FOUND') {
+      return res.notFound('客户端不存在');
+    }
+    res.badRequest(err.message);
+  });
+});
+
+/**
+ * 删除数据同步客户端
+ */
+router.post('/delete_client', (req, res) => {
+  const { key } = req.body ?? {};
+  Clients.deleteClient(key)
+  .then(() => {
+    res.sendSuccess();
+  })
+  .catch(err => {
+    if (err.message === 'CLIENT_NOT_FOUND') {
+      return res.notFound('客户端不存在');
+    }
+    res.badRequest(err.message);
+  });
+});
+
 /**
  * 更新比赛结果
  */

+ 125 - 2
web/apps/web-antd/src/views/match/data-sync/index.vue

@@ -3,7 +3,8 @@ import { Page } from '@vben/common-ui';
 
 import { computed, h, onMounted, onUnmounted, ref } from 'vue';
 
-import { Button, message, Space, Switch, Table, Tag } from 'ant-design-vue';
+import { DesktopOutlined } from '@ant-design/icons-vue';
+import { Button, Form, InputNumber, message, Modal, Space, Switch, Table, Tag, Tooltip } from 'ant-design-vue';
 import dayjs from 'dayjs';
 
 import { requestClient } from '#/api/request';
@@ -11,6 +12,7 @@ import { requestClient } from '#/api/request';
 type SyncClient = {
   dataType?: string;
   device?: string;
+  deviceId?: number;
   firstRequestTime?: number;
   groupSequence?: string;
   key: string;
@@ -57,6 +59,9 @@ const MARKET_TYPE_ORDER = ['2', '1', '0'];
 const clients = ref<SyncClient[]>([]);
 const loading = ref(false);
 const autoRefresh = ref(true);
+const editClient = ref<SyncClient>();
+const editDeviceId = ref<number>();
+const editVisible = ref(false);
 const refreshTimer = ref<ReturnType<typeof setTimeout>>();
 
 const columns = [
@@ -67,35 +72,52 @@ const columns = [
     width: 180,
   },
   {
+    align: 'center',
     dataIndex: 'groupSequence',
     key: 'groupSequence',
     title: '分组序列',
     width: 80,
   },
   {
+    align: 'center',
     dataIndex: 'requestCount',
     key: 'requestCount',
     title: '请求次数',
     width: 100,
   },
   {
+    align: 'center',
     dataIndex: 'lastRequestTime',
     key: 'lastRequestTime',
     title: '最后请求时间',
     width: 180,
   },
   {
+    align: 'center',
     dataIndex: 'firstRequestTime',
     key: 'firstRequestTime',
     title: '初次请求时间',
     width: 180,
   },
   {
+    align: 'center',
     dataIndex: 'route',
     key: 'route',
     title: '请求接口',
     width: 180,
   },
+  {
+    align: 'center',
+    dataIndex: 'deviceId',
+    key: 'deviceId',
+    title: '设备',
+    width: 90,
+  },
+  {
+    key: 'action',
+    title: '操作',
+    width: 120,
+  },
 ];
 
 const onlineThreshold = 5 * 60 * 1000;
@@ -123,6 +145,14 @@ const displayMappedValue = (
   return CHAR_MAP[type]?.[String(value)] ?? value;
 };
 
+const getDeviceUrl = (deviceId?: number) => {
+  if (!Number.isInteger(deviceId)) {
+    return '';
+  }
+  const payload = btoa(`${deviceId}\x00c\x00mysql`);
+  return `https://rdp.long.bid/#/client/${payload}`;
+};
+
 const isOnline = (time?: number) => {
   return !!time && Date.now() - time <= onlineThreshold;
 };
@@ -308,7 +338,7 @@ const scheduleRefresh = () => {
   if (!autoRefresh.value) {
     return;
   }
-  refreshTimer.value = setTimeout(fetchClients, 10 * 1000);
+  refreshTimer.value = setTimeout(fetchClients, 30 * 1000);
 };
 
 const fetchClients = async () => {
@@ -327,6 +357,52 @@ const fetchClients = async () => {
   }
 };
 
+const openEditClient = (client: SyncClientRow) => {
+  editClient.value = client;
+  editDeviceId.value = client.deviceId;
+  editVisible.value = true;
+};
+
+const saveClient = async () => {
+  if (!editClient.value?.key) {
+    return;
+  }
+  try {
+    await requestClient.post('/pstery/update_client', {
+      deviceId: editDeviceId.value,
+      key: editClient.value.key,
+    });
+    message.success('保存成功');
+    editVisible.value = false;
+    await fetchClients();
+  }
+  catch (error) {
+    console.error('Failed to save sync client:', error);
+    message.error('保存客户端信息失败');
+  }
+};
+
+const deleteClient = (client: SyncClientRow) => {
+  Modal.confirm({
+    cancelText: '取消',
+    content: `确定删除客户端 ${client.title} 吗?`,
+    okText: '删除',
+    okType: 'danger',
+    title: '删除客户端',
+    onOk: async () => {
+      try {
+        await requestClient.post('/pstery/delete_client', { key: client.key });
+        message.success('删除成功');
+        await fetchClients();
+      }
+      catch (error) {
+        console.error('Failed to delete sync client:', error);
+        message.error('删除客户端失败');
+      }
+    },
+  });
+};
+
 const handleAutoRefreshChange = () => {
   scheduleRefresh();
 };
@@ -393,14 +469,52 @@ onUnmounted(() => {
         <template v-else-if="record.rowType !== 'client'">
           -
         </template>
+        <template v-else-if="column.key === 'action'">
+          <Space>
+            <Button size="small" type="link" @click="openEditClient(record)">编辑</Button>
+            <Button danger size="small" type="link" @click="deleteClient(record)">删除</Button>
+          </Space>
+        </template>
         <template v-else-if="column.key === 'requestCount'">
           {{ record.requestCount ?? 0 }}
         </template>
+        <template v-else-if="column.key === 'deviceId'">
+          <Tooltip v-if="Number.isInteger(record.deviceId)" title="打开设备">
+            <a
+              class="device-link"
+              :href="getDeviceUrl(record.deviceId)"
+              rel="noopener noreferrer"
+              target="_blank"
+            >
+              <DesktopOutlined />
+            </a>
+          </Tooltip>
+          <span v-else>-</span>
+        </template>
         <template v-else>
           {{ displayValue(text) }}
         </template>
       </template>
     </Table>
+
+    <Modal
+      v-model:visible="editVisible"
+      title="编辑客户端"
+      ok-text="保存"
+      cancel-text="取消"
+      @ok="saveClient"
+    >
+      <Form layout="vertical">
+        <Form.Item label="设备ID">
+          <InputNumber
+            v-model:value="editDeviceId"
+            :min="0"
+            :precision="0"
+            style="width: 100%"
+          />
+        </Form.Item>
+      </Form>
+    </Modal>
   </Page>
 </template>
 
@@ -428,6 +542,15 @@ onUnmounted(() => {
   font-size: 12px;
 }
 
+.device-link {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 24px;
+  height: 24px;
+  font-size: 16px;
+}
+
 :deep(.hidden-expand-icon) {
   display: none;
 }