index.vue 15 KB

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