games.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. <script setup>
  2. import axios from 'axios';
  3. import dayjs from 'dayjs';
  4. import { ref, reactive, computed, watch, onMounted, onUnmounted } from 'vue';
  5. import { message } from 'ant-design-vue';
  6. import { ReloadOutlined, PlusOutlined, LinkOutlined, DisconnectOutlined } from '@ant-design/icons-vue';
  7. const search = ref('');
  8. const games = ref(null);
  9. const gamesRelations = ref([]);
  10. const selectedGames = reactive({
  11. polymarket: null,
  12. pinnacle: null,
  13. });
  14. const onSearch = (value) => {
  15. search.value = value;
  16. };
  17. const updateGames = async () => {
  18. return axios.get('/api/games/get_games').then(res => {
  19. if (res.data.statusCode === 200) {
  20. games.value = res.data.data;
  21. }
  22. else {
  23. throw new Error(res.data.message);
  24. }
  25. })
  26. .catch(err => {
  27. message.error(err.response?.data?.message ?? err.message);
  28. console.error(err);
  29. });
  30. };
  31. const selectGame = (type, game) => {
  32. const { id, leagueId, leagueName, teamHomeName, teamAwayName, timestamp } = game;
  33. const currentGames = { id, leagueId, leagueName, teamHomeName, teamAwayName, timestamp };
  34. if (type === 'polymarket') {
  35. if (selectedGames.polymarket?.id === id) {
  36. selectedGames.polymarket = null;
  37. }
  38. else {
  39. selectedGames.polymarket = currentGames;
  40. }
  41. }
  42. else if (type === 'pinnacle') {
  43. if (selectedGames.pinnacle?.id === id) {
  44. selectedGames.pinnacle = null;
  45. }
  46. else {
  47. selectedGames.pinnacle = currentGames;
  48. }
  49. }
  50. else {
  51. throw new Error('invalid type');
  52. }
  53. }
  54. const resetSelectedGames = () => {
  55. selectedGames.polymarket = null;
  56. selectedGames.pinnacle = null;
  57. }
  58. const setGamesRelation = () => {
  59. if (!selectedGames.polymarket || !selectedGames.pinnacle) {
  60. message.error('请选择要添加的比赛');
  61. return;
  62. }
  63. const gameRelation = {
  64. id: selectedGames.polymarket.id,
  65. platforms: selectedGames,
  66. timestamp: selectedGames.polymarket.timestamp,
  67. };
  68. axios.post('/api/games/set_relation', gameRelation)
  69. .then(res => {
  70. if (res.data.statusCode === 200) {
  71. message.success('添加成功');
  72. }
  73. else {
  74. throw new Error(res.data.message);
  75. }
  76. resetSelectedGames();
  77. return refresh();
  78. })
  79. .then(() => {
  80. if (search.value.trim() && (!polymarketGames.value.length || !pinnacleGames.value.length)) {
  81. search.value = '';
  82. }
  83. })
  84. .catch(err => {
  85. message.error(err.response?.data?.message ?? err.message);
  86. console.error(err);
  87. });
  88. };
  89. const removeGamesRelation = (relation) => {
  90. axios.post('/api/games/remove_relation', { id: relation.id })
  91. .then(res => {
  92. if (res.data.statusCode === 200) {
  93. message.success('删除成功');
  94. }
  95. else {
  96. throw new Error(res.data.message);
  97. }
  98. return refresh();
  99. })
  100. .catch(err => {
  101. message.error(err.response?.data?.message ?? err.message);
  102. console.error(err);
  103. });
  104. }
  105. const updateGamesRelations = async () => {
  106. return axios.get('/api/games/get_relations')
  107. .then(res => {
  108. if (res.data.statusCode === 200) {
  109. gamesRelations.value = res.data.data;
  110. }
  111. else {
  112. throw new Error(res.data.message);
  113. }
  114. })
  115. .catch(err => {
  116. message.error(err.response?.data?.message ?? err.message);
  117. console.error(err);
  118. });
  119. };
  120. const refresh = async () => {
  121. return Promise.all([updateGames(), updateGamesRelations()]);
  122. }
  123. const getGames = (platform) => {
  124. const searchValue = search.value.trim().toLowerCase();
  125. return games.value?.[platform].map(item => {
  126. const { id, timestamp } = item;
  127. let selected = false;
  128. let disabeld = false;
  129. if (selectedGames[platform]?.id === id) {
  130. selected = true;
  131. }
  132. if (gamesRelations.value.some(relation => relation.platforms[platform]?.id === id)) {
  133. disabeld = true;
  134. }
  135. const dateTime = dayjs(timestamp).format('MM-DD HH:mm');
  136. return { ...item, selected, disabeld, dateTime };
  137. }).filter(item => {
  138. const { disabeld, leagueName, teamHomeName, teamAwayName, localesLeagueName, localesTeamHomeName, localesTeamAwayName } = item;
  139. return !disabeld && (!searchValue
  140. || leagueName?.toLowerCase().includes(searchValue)
  141. || localesLeagueName?.toLowerCase().includes(searchValue)
  142. || teamHomeName?.toLowerCase().includes(searchValue)
  143. || localesTeamHomeName?.toLowerCase().includes(searchValue)
  144. || teamAwayName?.toLowerCase().includes(searchValue)
  145. || localesTeamAwayName?.toLowerCase().includes(searchValue));
  146. }) ?? [];
  147. }
  148. const polymarketGames = computed(() => {
  149. return getGames('polymarket');
  150. });
  151. const pinnacleGames = computed(() => {
  152. return getGames('pinnacle');
  153. });
  154. const gamesRelationsFiltered = computed(() => {
  155. const searchValue = search.value.trim().toLowerCase();
  156. return gamesRelations.value.filter(item => {
  157. const { platforms: { polymarket, pinnacle } } = item;
  158. return !searchValue
  159. || polymarket.leagueName?.toLowerCase().includes(searchValue)
  160. || polymarket.localesLeagueName?.toLowerCase().includes(searchValue)
  161. || polymarket.teamHomeName?.toLowerCase().includes(searchValue)
  162. || polymarket.localesTeamHomeName?.toLowerCase().includes(searchValue)
  163. || polymarket.teamAwayName?.toLowerCase().includes(searchValue)
  164. || polymarket.localesTeamAwayName?.toLowerCase().includes(searchValue)
  165. || pinnacle.leagueName?.toLowerCase().includes(searchValue)
  166. || pinnacle.teamHomeName?.toLowerCase().includes(searchValue)
  167. || pinnacle.teamAwayName?.toLowerCase().includes(searchValue);
  168. }) ?? [];
  169. });
  170. onMounted(() => {
  171. // console.log('games mounted');
  172. refresh();
  173. });
  174. onUnmounted(() => {
  175. // console.log('games unmounted');
  176. });
  177. </script>
  178. <template>
  179. <a-page-header title="比赛">
  180. <template #extra>
  181. <a-input-search
  182. v-model:value="search"
  183. placeholder="搜索"
  184. :allowClear="true"
  185. @search="onSearch"
  186. />
  187. <a-button @click="refresh">
  188. <template #icon>
  189. <reload-outlined />
  190. </template>
  191. 刷新
  192. </a-button>
  193. <a-button type="primary" @click="setGamesRelation">
  194. <template #icon>
  195. <plus-outlined />
  196. </template>
  197. 添加
  198. </a-button>
  199. </template>
  200. </a-page-header>
  201. <div class="games-container">
  202. <div class="games-column polymarket">
  203. <h3>Polymarket 比赛 ({{ polymarketGames.length }})</h3>
  204. <a-list item-layout="horizontal" :data-source="polymarketGames" size="small">
  205. <template #renderItem="{ item }">
  206. <a-list-item :class="{ selected: item.selected, disabled: item.disabeld }" @click="!item.disabeld && selectGame('polymarket', item)">
  207. <div class="game-info">
  208. <div class="game-league-name">{{ item.localesLeagueName }} <em>{{ item.leagueName }}</em></div>
  209. <div class="game-team-name home-team-name">{{ item.localesTeamHomeName }} <em>{{ item.teamHomeName }}</em></div>
  210. <div class="game-team-name away-team-name">{{ item.localesTeamAwayName }} <em>{{ item.teamAwayName }}</em></div>
  211. </div>
  212. <div class="game-date-time">{{ item.dateTime }}</div>
  213. </a-list-item>
  214. </template>
  215. </a-list>
  216. </div>
  217. <div class="games-column pinnacle">
  218. <h3>Pinnacle 比赛 ({{ pinnacleGames.length }})</h3>
  219. <a-list item-layout="horizontal" :data-source="pinnacleGames" size="small">
  220. <template #renderItem="{ item }">
  221. <a-list-item :class="{ selected: item.selected, disabled: item.disabeld }" @click="!item.disabeld && selectGame('pinnacle', item)">
  222. <div class="game-info">
  223. <div class="game-league-name">{{ item.leagueName }}</div>
  224. <div class="game-team-name home-team-name">{{ item.teamHomeName }}</div>
  225. <div class="game-team-name away-team-name">{{ item.teamAwayName }}</div>
  226. </div>
  227. <div class="game-date-time">{{ item.dateTime }}</div>
  228. </a-list-item>
  229. </template>
  230. </a-list>
  231. </div>
  232. <div class="games-column relations">
  233. <h3>已添加的比赛 ({{ gamesRelationsFiltered.length }})</h3>
  234. <a-list item-layout="horizontal" :data-source="gamesRelationsFiltered" size="small">
  235. <template #renderItem="{ item }">
  236. <a-list-item>
  237. <div class="game-info">
  238. <div class="game-league-name">{{ item.platforms.polymarket.leagueName }}</div>
  239. <div class="game-team-name home-team-name">{{ item.platforms.polymarket.teamHomeName }}</div>
  240. <div class="game-team-name away-team-name">{{ item.platforms.polymarket.teamAwayName }}</div>
  241. </div>
  242. <div class="game-info">
  243. <div class="game-league-name">{{ item.platforms.pinnacle?.leagueName }}</div>
  244. <div class="game-team-name home-team-name">{{ item.platforms.pinnacle?.teamHomeName }}</div>
  245. <div class="game-team-name away-team-name">{{ item.platforms.pinnacle?.teamAwayName }}</div>
  246. </div>
  247. <div class="game-info">
  248. <div class="game-league-name">{{ item.platforms.huangguan?.leagueName }}</div>
  249. <div class="game-team-name home-team-name">{{ item.platforms.huangguan?.teamHomeName }}</div>
  250. <div class="game-team-name away-team-name">{{ item.platforms.huangguan?.teamAwayName }}</div>
  251. </div>
  252. <div class="game-info">
  253. <div class="game-league-name">{{ item.platforms.obsports?.leagueName }}</div>
  254. <div class="game-team-name home-team-name">{{ item.platforms.obsports?.teamHomeName }}</div>
  255. <div class="game-team-name away-team-name">{{ item.platforms.obsports?.teamAwayName }}</div>
  256. </div>
  257. <a-button class="remove-button" type="link" @click="removeGamesRelation(item)">
  258. <template #icon>
  259. <link-outlined />
  260. <disconnect-outlined />
  261. </template>
  262. </a-button>
  263. <div class="game-date-time">{{ dayjs(item.timestamp).format('MM-DD HH:mm') }}</div>
  264. </a-list-item>
  265. </template>
  266. </a-list>
  267. </div>
  268. </div>
  269. </template>
  270. <style lang="scss" scoped>
  271. .games-container {
  272. display: flex;
  273. flex-direction: row;
  274. justify-content: space-between;
  275. align-items: stretch;
  276. height: calc(100vh - 126px);
  277. padding: 15px;
  278. gap: 15px;
  279. border-top: 1px solid rgba(5, 5, 5, 0.06);
  280. }
  281. .games-column {
  282. overflow: auto;
  283. flex: 1;
  284. h3 {
  285. height: 39px;
  286. margin: 0;
  287. line-height: 39px;
  288. text-align: center;
  289. font-size: 14px;
  290. font-weight: 600;
  291. border: 1px solid rgba(5, 5, 5, 0.06);
  292. border-bottom: none;
  293. border-radius: 8px 8px 0 0;
  294. background-color: #fafafa;
  295. }
  296. }
  297. .games-column.relations {
  298. flex: 2.5;
  299. border-right: 0 none;
  300. }
  301. .ant-list {
  302. max-height: calc(100vh - 196px);
  303. overflow: auto;
  304. border: 1px solid rgba(5, 5, 5, 0.06);
  305. border-radius: 0 0 8px 8px;
  306. }
  307. .ant-list-item {
  308. &:hover {
  309. background-color: #fafafa;
  310. .remove-button {
  311. visibility: visible;
  312. }
  313. }
  314. &.selected {
  315. background-color: #e6f7ff;
  316. }
  317. &.disabled {
  318. opacity: 0.5;
  319. cursor: not-allowed;
  320. }
  321. .remove-button {
  322. visibility: hidden;
  323. .anticon-disconnect {
  324. display: none;
  325. }
  326. &:hover {
  327. color: #f5222d;
  328. .anticon-link {
  329. display: none;
  330. }
  331. .anticon-disconnect {
  332. margin-inline-start: 0;
  333. display: inline-block;
  334. }
  335. }
  336. }
  337. }
  338. .game-info {
  339. flex: 1;
  340. em {
  341. font-style: normal;
  342. font-size: 12px;
  343. }
  344. }
  345. .game-league-name {
  346. font-size: 16px;
  347. }
  348. .game-team-name {
  349. color: #666;
  350. &.home-team-name {
  351. color: rgb(0, 107, 230);
  352. }
  353. &.away-team-name {
  354. color: rgb(255, 56, 96);
  355. }
  356. }
  357. .game-date-time {
  358. color: #666;
  359. }
  360. </style>