index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. <script setup>
  2. import { requestClient } from '#/api/request';
  3. import { Button, message, Form, InputNumber, Drawer } from 'ant-design-vue';
  4. import { ref, reactive, computed, onMounted, onUnmounted } from 'vue';
  5. import dayjs from 'dayjs';
  6. import MatchCard from '../components/match_card.vue';
  7. import { useContentsPositionStore } from '@vben/stores';
  8. const contentsPositionStore = useContentsPositionStore();
  9. const solutions = ref([]);
  10. const selectedSolutions = reactive([]);
  11. const totalProfit = ref({});
  12. const loopActive = ref(false);
  13. const psOptions = reactive({
  14. bet: 10000,
  15. rebate: 0,
  16. });
  17. const totalProfitVisible = ref(false);
  18. const fixFloat = (number, x = 2) => {
  19. return parseFloat(number.toFixed(x));
  20. }
  21. const headerStyle = computed(() => {
  22. return {
  23. position: contentsPositionStore.position,
  24. top: contentsPositionStore.top,
  25. left: contentsPositionStore.left,
  26. width: contentsPositionStore.width,
  27. paddingLeft: contentsPositionStore.paddingLeft,
  28. }
  29. });
  30. const totalProfitValue = computed(() => {
  31. const { profit = {}, preSolution = {}, subSolution = {}, gamesEvents = {} } = totalProfit.value;
  32. const sol1 = formatSolution(preSolution, gamesEvents);
  33. const sol2 = formatSolution(subSolution, gamesEvents);
  34. const psScale = psOptions.bet / profit.ps_base ?? 10000;
  35. const psRebate = psOptions.bet * psOptions.rebate / 100;
  36. const profitInfo = {};
  37. Object.keys(profit).forEach(key => {
  38. if (key == 'win_diff') {
  39. return;
  40. }
  41. if (key.startsWith('gold')) {
  42. profitInfo[key] = fixFloat(profit[key] * psScale);
  43. }
  44. else if (key.startsWith('win_')) {
  45. profitInfo[key] = fixFloat(profit[key] * psScale + psRebate);
  46. }
  47. });
  48. const psInfo = [];
  49. const outPreSol = [];
  50. const outSubSol = [];
  51. const solutions = [sol1, sol2].filter(item => item);
  52. solutions.forEach((item, index) => {
  53. const { sol: { ps_index }, cpr } = item;
  54. const newCpr = [...cpr];
  55. const ps_info = newCpr.splice(ps_index, 1);
  56. psInfo.push({ ...ps_info[0] });
  57. newCpr.forEach((c, i) => {
  58. let side = '';
  59. if (ps_index == 0) {
  60. if (i == 0) {
  61. side = "B"
  62. }
  63. else {
  64. side = "M";
  65. }
  66. }
  67. else if (ps_index == 1) {
  68. if (i == 0) {
  69. side = "A";
  70. }
  71. else {
  72. side = "M";
  73. }
  74. }
  75. else {
  76. if (i == 0) {
  77. side = "A";
  78. }
  79. else {
  80. side = "B";
  81. }
  82. }
  83. if (index == 0) {
  84. outPreSol.push({ ...c, g: profitInfo[`gold${side}${index+1}`] });
  85. }
  86. else {
  87. outSubSol.push({ ...c, g: profitInfo[`gold${side}${index+1}`] });
  88. }
  89. })
  90. });
  91. return { solutions, profit: profitInfo, psInfo, outPreSol, outSubSol };
  92. });
  93. const solutionsList = computed(() => {
  94. const startTimestamp = selectedSolutions[0]?.timestamp ?? 0;
  95. return solutions.value.map(item => {
  96. const selected = selectedSolutions.findIndex(sol => sol.sid === item.sid) >= 0;
  97. const disabled = false && !selected && (item.info.ps.timestamp < startTimestamp + 1000 * 60 * 60 * 2);
  98. const currentSol = { ...item.sol };
  99. const psScale = psOptions.bet / currentSol.inner_base;
  100. const psRebate = psOptions.bet * psOptions.rebate / 100;
  101. Object.keys(currentSol).forEach(key => {
  102. if (key.startsWith('gold_')) {
  103. currentSol[key] = fixFloat(currentSol[key] * psScale);
  104. }
  105. else if (key.startsWith('win_')) {
  106. currentSol[key] = fixFloat(currentSol[key] * psScale + psRebate);
  107. }
  108. });
  109. return { ...item, sol: currentSol, selected, disabled };
  110. });
  111. });
  112. const getSolutions = async () => {
  113. try {
  114. const data = await requestClient.get('/pstery/get_solutions');
  115. return data;
  116. }
  117. catch (error) {
  118. console.error('Failed to fetch solutions:', error);
  119. message.error('获取中单方案失败');
  120. return [];
  121. }
  122. }
  123. const calcTotalProfit = async () => {
  124. const sids = selectedSolutions.map(item => item.sid);
  125. try {
  126. const totalProfit = await requestClient.post('/pstery/calc_total_profit', [...sids, psOptions.bet]);
  127. return totalProfit;
  128. }
  129. catch (error) {
  130. console.error('Failed to calc total profit:', error);
  131. message.error('计算综合利润失败');
  132. return {};
  133. }
  134. }
  135. const parseIorKey = (iorKey) => {
  136. const [, type, accept, side, , ratioString] = iorKey.match(/^ior_(r|ou|m|wm|ot|os)(a?)(h|c|n)?(_([\d-]+))?$/);
  137. let ratio = 0;
  138. if (type === 'ot' || type === 'os') {
  139. ratio = ratioString;
  140. }
  141. else if (ratioString) {
  142. ratio = `${ratioString[0]}.${ratioString.slice(1)}` * (accept ? 1 : -1);
  143. }
  144. return { type, side, ratio };
  145. }
  146. const PS_IOR_KEYS = [
  147. ['0', 'ior_mh', 'ior_mn', 'ior_mc'],
  148. ['-1', 'ior_rh_15', 'ior_wmh_1', 'ior_rac_05'],
  149. ['-2', 'ior_rh_25', 'ior_wmh_2', 'ior_rac_15'],
  150. ['+1', 'ior_rah_05', 'ior_wmc_1', 'ior_rc_15'],
  151. ['+2', 'ior_rah_15', 'ior_wmc_2', 'ior_rc_25'],
  152. ['0-1', 'ior_ot_0', 'ior_os_0-1', 'ior_ot_1'],
  153. ['2-3', 'ior_ot_2', 'ior_os_2-3', 'ior_ot_3'],
  154. ];
  155. const formatPsEvents = (events) => {
  156. return PS_IOR_KEYS.map(([label, ...keys]) => {
  157. const match = keys.map(key => ({
  158. key,
  159. value: events[key] ?? 0
  160. }));
  161. return {
  162. label,
  163. match
  164. };
  165. })
  166. // .filter(item => item.match.every(entry => entry.value !== 0))
  167. .map(({label, match}) => [label, ...match]);
  168. }
  169. // const rivalIor = (ior) => {
  170. // const map = {
  171. // "ior_rh": "ior_rac",
  172. // "ior_rc": "ior_rah",
  173. // "ior_rac": "ior_rh",
  174. // "ior_rah": "ior_rc",
  175. // "ior_wmh": "ior_wmc",
  176. // "ior_wmc": "ior_wmh",
  177. // "ior_wmh_2": "ior_wmc_2",
  178. // "ior_wmc_2": "ior_wmh_2"
  179. // };
  180. // const iorInfos = ior.split('_');
  181. // const iorStart = iorInfos.slice(0, 2).join('_');
  182. // if (!map[iorStart]) {
  183. // return ior;
  184. // }
  185. // return `${map[iorStart]}_${iorInfos[2]}`;
  186. // }
  187. const formatEvents = (events, cprKeys) => {
  188. const eventsMap = {};
  189. Object.keys(events).forEach(key => {
  190. const { type, side, ratio } = parseIorKey(key);
  191. let ratioKey, index;
  192. if (type === 'r') {
  193. if (side === 'h') {
  194. ratioKey = ratio;
  195. index = 0;
  196. }
  197. else if (side === 'c') {
  198. ratioKey = -ratio;
  199. index = 2;
  200. }
  201. }
  202. else if (type === 'm') {
  203. ratioKey = 'm';
  204. if (side == 'h') {
  205. index = 0;
  206. }
  207. else if (side == 'c') {
  208. index = 2;
  209. }
  210. else {
  211. index = 1;
  212. }
  213. }
  214. else if (type === 'wm') {
  215. ratioKey = `wm_${Math.abs(ratio)}`;
  216. if (side === 'h') {
  217. index = 0;
  218. }
  219. else if (side === 'c') {
  220. index = 2;
  221. }
  222. }
  223. else if (type === 'ou') {
  224. ratioKey = `ou_${Math.abs(ratio)}`;
  225. if (side === 'c') {
  226. index = 0;
  227. }
  228. else if (side === 'h') {
  229. index = 2;
  230. }
  231. }
  232. else if (type === 'os') {
  233. ratioKey = ratio;
  234. index = 1;
  235. }
  236. else if (type === 'ot') {
  237. switch (ratio) {
  238. case '0':
  239. ratioKey = '0-1';
  240. index = 0;
  241. break;
  242. case '1':
  243. ratioKey = '0-1';
  244. index = 2;
  245. break;
  246. case '2':
  247. ratioKey = '2-3';
  248. index = 0;
  249. break;
  250. case '3':
  251. ratioKey = '2-3';
  252. index = 2;
  253. break;
  254. }
  255. }
  256. if (typeof (ratioKey) == 'number') {
  257. if (ratioKey > 0) {
  258. ratioKey = `+${ratioKey}`;
  259. }
  260. // else if (ratioKey === 0) {
  261. // ratioKey = '-0';
  262. // }
  263. else {
  264. ratioKey = `${ratioKey}`;
  265. }
  266. }
  267. if (!ratioKey) {
  268. return;
  269. }
  270. if (!eventsMap[ratioKey]) {
  271. eventsMap[ratioKey] = new Array(3).fill(undefined);
  272. }
  273. const value = events[key] ?? 0;
  274. eventsMap[ratioKey][index] = { key, value };
  275. });
  276. return Object.keys(eventsMap).sort((a, b) => a.localeCompare(b)).map(key => {
  277. return [key, ...eventsMap[key]];
  278. });
  279. }
  280. const formatSolution = (solution, eventsList) => {
  281. const { cpr, info } = solution;
  282. if (!cpr || !info) {
  283. return null;
  284. }
  285. const cprKeys = cpr.map(item => item.k);
  286. const psEvents = eventsList.ps?.[info.ps.eventId] ?? {};
  287. const obEvents = eventsList.ob?.[info.ob.eventId] ?? {};
  288. const hgEvents = eventsList.hg?.[info.hg.eventId] ?? {};
  289. info.ps.events = formatPsEvents(psEvents);
  290. info.ob.events = formatEvents(obEvents, cprKeys);
  291. info.hg.events = formatEvents(hgEvents, cprKeys);
  292. info.ps.dateTime = dayjs(info.ps.timestamp).format('YYYY-MM-DD HH:mm:ss');
  293. info.ob.dateTime = dayjs(info.ob.timestamp).format('YYYY-MM-DD HH:mm:ss');
  294. info.hg.dateTime = dayjs(info.hg.timestamp).format('YYYY-MM-DD HH:mm:ss');
  295. cpr.forEach(item => {
  296. const { k, p } = item;
  297. if (!info[p]['selected']) {
  298. info[p]['selected'] = [];
  299. }
  300. info[p]['selected'].push(k);
  301. });
  302. return solution;
  303. }
  304. const updateSolutions = async () => {
  305. getSolutions()
  306. .then(({ solutions: solutionsList, gamesEvents: eventsList }) => {
  307. solutions.value = solutionsList?.map(solution => formatSolution(solution, eventsList)) ?? [];
  308. })
  309. .catch(error => {
  310. console.error('Failed to update solutions:', error);
  311. message.error('获取中单方案失败');
  312. })
  313. .finally(() => {
  314. if (loopActive.value) {
  315. setTimeout(() => {
  316. updateSolutions();
  317. }, 1000 * 10);
  318. }
  319. });
  320. }
  321. const showTotalProfit = async () => {
  322. totalProfit.value = await calcTotalProfit();
  323. totalProfitVisible.value = true;
  324. const { profit } = totalProfit.value;
  325. console.log('profit', profit);
  326. };
  327. const closeTotalProfit = () => {
  328. totalProfitVisible.value = false;
  329. selectedSolutions.length = 0;
  330. totalProfit.value = {};
  331. };
  332. const toggleSolution = (sid, timestamp) => {
  333. const findIndex = selectedSolutions.findIndex(item => item.sid === sid);
  334. if (findIndex >= 0) {
  335. selectedSolutions.splice(findIndex, 1);
  336. }
  337. else if (selectedSolutions.length < 2) {
  338. selectedSolutions.push({ sid, timestamp });
  339. }
  340. else {
  341. selectedSolutions.splice(1, 1, { sid, timestamp });
  342. }
  343. if (selectedSolutions.length == 2) {
  344. showTotalProfit();
  345. }
  346. }
  347. onMounted(() => {
  348. loopActive.value = true;
  349. updateSolutions();
  350. });
  351. onUnmounted(() => {
  352. loopActive.value = false;
  353. });
  354. </script>
  355. <template>
  356. <div class="solution-container">
  357. <div class="contents-header transition-all duration-200" :style="headerStyle">
  358. <div class="solution-options">
  359. <Form layout="inline">
  360. <Form.Item label="PS 投注">
  361. <InputNumber size="small" placeholder="PS投注" min="1000" v-model:value="psOptions.bet" />
  362. </Form.Item>
  363. <!-- <Form.Item label="PS 返点">
  364. <InputNumber size="small" placeholder="PS返点" min="0" v-model:value="psOptions.rebate" />
  365. </Form.Item> -->
  366. </Form>
  367. </div>
  368. <div class="solution-header">
  369. <span>PS</span>
  370. <span>OB</span>
  371. <span>HG</span>
  372. <em>利润</em>
  373. </div>
  374. </div>
  375. <div class="solution-list">
  376. <div class="solution-item"
  377. v-for="{ sid, sol: { win_average, win_profit_rate, cross_type }, info: { ps, ob, hg }, selected, disabled } in solutionsList" :key="sid"
  378. :class="{ 'selected': selected, 'disabled': disabled }">
  379. <MatchCard platform="ps" :eventId="ps.eventId" :leagueName="ps.leagueName" :teamHomeName="ps.teamHomeName"
  380. :teamAwayName="ps.teamAwayName" :dateTime="ps.dateTime" :eventInfo="ps.eventInfo" :events="ps.events ?? []"
  381. :matchNumStr="ps.matchNumStr" :selected="ps.selected ?? []" />
  382. <MatchCard platform="ob" :eventId="ob.eventId" :leagueName="ob.leagueName" :teamHomeName="ob.teamHomeName"
  383. :teamAwayName="ob.teamAwayName" :dateTime="ob.dateTime" :events="ob.events ?? []"
  384. :selected="ob.selected ?? []" />
  385. <MatchCard platform="hg" :eventId="hg.eventId" :leagueName="hg.leagueName" :teamHomeName="hg.teamHomeName"
  386. :teamAwayName="hg.teamAwayName" :dateTime="hg.dateTime" :events="hg.events ?? []"
  387. :selected="hg.selected ?? []" />
  388. <div class="solution-profit" @click="!disabled && toggleSolution(sid, ps.timestamp)">
  389. <p>{{ win_average }}</p>
  390. <p>{{ win_profit_rate }}%</p>
  391. <p>{{ cross_type }}</p>
  392. </div>
  393. </div>
  394. </div>
  395. <div class="list-empty" v-if="!solutionsList.length">暂无数据</div>
  396. <!-- <Drawer
  397. title="综合利润方案"
  398. placement="bottom"
  399. height="600"
  400. :visible="totalProfitVisible"
  401. @close="closeTotalProfit"
  402. >
  403. <div class="solution-total-profit" v-if="totalProfitValue.solutions.length">
  404. <div class="solution-item"
  405. v-for="{ sid, info: { ps, ob, hg } } in totalProfitValue.solutions" :key="sid">
  406. <MatchCard platform="ps" :eventId="ps.eventId" :leagueName="ps.leagueName" :teamHomeName="ps.teamHomeName"
  407. :teamAwayName="ps.teamAwayName" :dateTime="ps.dateTime" :eventInfo="ps.eventInfo" :events="ps.events ?? []"
  408. :matchNumStr="ps.matchNumStr" :selected="ps.selected ?? []" />
  409. <MatchCard platform="ob" :eventId="ob.eventId" :leagueName="ob.leagueName" :teamHomeName="ob.teamHomeName"
  410. :teamAwayName="ob.teamAwayName" :dateTime="ob.dateTime" :events="ob.events ?? []"
  411. :selected="ob.selected ?? []" />
  412. <MatchCard platform="hg" :eventId="hg.eventId" :leagueName="hg.leagueName" :teamHomeName="hg.teamHomeName"
  413. :teamAwayName="hg.teamAwayName" :dateTime="hg.dateTime" :events="hg.events ?? []"
  414. :selected="hg.selected ?? []" />
  415. </div>
  416. </div>
  417. <div class="profit-info">
  418. <table>
  419. <tr>
  420. <th></th>
  421. <td>PS</td>
  422. <td colspan="2">第一场</td>
  423. <td colspan="2">第二场</td>
  424. </tr>
  425. <tr>
  426. <th>赔率</th>
  427. <td>{{ totalProfitValue.psInfo[0]?.v }}: {{ totalProfitValue.psInfo[1]?.v }}</td>
  428. <td>{{ totalProfitValue.outPreSol[0]?.p }}: {{ totalProfitValue.outPreSol[0]?.v }}</td>
  429. <td>{{ totalProfitValue.outPreSol[1]?.p }}: {{ totalProfitValue.outPreSol[1]?.v }}</td>
  430. <td>{{ totalProfitValue.outSubSol[0]?.p }}: {{ totalProfitValue.outSubSol[0]?.v }}</td>
  431. <td>{{ totalProfitValue.outSubSol[1]?.p }}: {{ totalProfitValue.outSubSol[1]?.v }}</td>
  432. </tr>
  433. <tr>
  434. <th>下注</th>
  435. <td>{{ psOptions.bet }}</td>
  436. <td>{{ totalProfitValue.outPreSol[0]?.g }}</td>
  437. <td>{{ totalProfitValue.outPreSol[1]?.g }}</td>
  438. <td>{{ totalProfitValue.outSubSol[0]?.g }}</td>
  439. <td>{{ totalProfitValue.outSubSol[1]?.g }}</td>
  440. </tr>
  441. <tr>
  442. <th>利润</th>
  443. <td>{{ totalProfitValue.profit.win_ps }}</td>
  444. <td colspan="2">{{ totalProfitValue.profit.win_target }}</td>
  445. <td colspan="2">{{ totalProfitValue.profit.win_target }}</td>
  446. </tr>
  447. </table>
  448. </div>
  449. </Drawer> -->
  450. </div>
  451. </template>
  452. <style lang="scss" scoped>
  453. .contents-header {
  454. position: fixed;
  455. top: 0;
  456. left: 0;
  457. z-index: 201;
  458. width: 100%;
  459. border-bottom: 1px solid hsl(var(--border));
  460. background-color: hsl(var(--background));
  461. }
  462. .solution-options {
  463. position: relative;
  464. display: flex;
  465. align-items: center;
  466. padding: 5px 20px;
  467. border-bottom: 1px solid hsl(var(--border));
  468. }
  469. .solution-header {
  470. position: relative;
  471. display: flex;
  472. align-items: center;
  473. height: 30px;
  474. padding: 0 20px;
  475. span,
  476. em {
  477. display: block;
  478. text-align: center;
  479. }
  480. span {
  481. flex: 1;
  482. }
  483. em {
  484. width: 80px;
  485. font-style: normal;
  486. }
  487. }
  488. .solution-container {
  489. padding-top: 74px;
  490. }
  491. .solution-item {
  492. display: flex;
  493. .match-card {
  494. flex: 1;
  495. }
  496. }
  497. .solution-list {
  498. padding: 20px;
  499. overflow: hidden;
  500. .solution-item {
  501. border-radius: 10px;
  502. background-color: hsl(var(--card));
  503. &.selected {
  504. background-color: hsl(var(--primary) / 0.15);
  505. }
  506. &.disabled {
  507. opacity: 0.5;
  508. cursor: not-allowed;
  509. }
  510. &:not(:last-child) {
  511. margin-bottom: 20px;
  512. }
  513. .match-card {
  514. border-right: 1px solid hsl(var(--border));
  515. }
  516. .solution-profit {
  517. display: flex;
  518. flex-direction: column;
  519. width: 80px;
  520. align-items: center;
  521. justify-content: center;
  522. }
  523. }
  524. }
  525. .profit-info {
  526. table {
  527. width: 100%;
  528. border-collapse: collapse;
  529. border-spacing: 0;
  530. table-layout: fixed;
  531. th, td {
  532. height: 30px;
  533. border: 1px solid hsl(var(--border));
  534. text-align: center;
  535. }
  536. th {
  537. width: 64px;
  538. font-weight: normal;
  539. }
  540. }
  541. }
  542. .list-empty {
  543. text-align: center;
  544. padding: 10px;
  545. font-size: 18px;
  546. color: hsl(var(--foreground) / 0.7);
  547. }
  548. </style>