index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  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. ];
  147. const formatPsEvents = (events) => {
  148. return PS_IOR_KEYS.map(([label, ...keys]) => {
  149. const match = keys.map(key => ({
  150. key,
  151. value: events[key] ?? 0
  152. }));
  153. return {
  154. label,
  155. match
  156. };
  157. })
  158. // .filter(item => item.match.every(entry => entry.value !== 0))
  159. .map(({label, match}) => [label, ...match]);
  160. }
  161. // const rivalIor = (ior) => {
  162. // const map = {
  163. // "ior_rh": "ior_rac",
  164. // "ior_rc": "ior_rah",
  165. // "ior_rac": "ior_rh",
  166. // "ior_rah": "ior_rc",
  167. // "ior_wmh": "ior_wmc",
  168. // "ior_wmc": "ior_wmh",
  169. // "ior_wmh_2": "ior_wmc_2",
  170. // "ior_wmc_2": "ior_wmh_2"
  171. // };
  172. // const iorInfos = ior.split('_');
  173. // const iorStart = iorInfos.slice(0, 2).join('_');
  174. // if (!map[iorStart]) {
  175. // return ior;
  176. // }
  177. // return `${map[iorStart]}_${iorInfos[2]}`;
  178. // }
  179. const formatEvents = (events, cprKeys) => {
  180. const eventsMap = {};
  181. Object.keys(events).forEach(key => {
  182. const { type, side, ratio } = parseIorKey(key);
  183. let ratioKey, index;
  184. if (type === 'r') {
  185. if (side === 'h') {
  186. ratioKey = ratio;
  187. index = 0;
  188. }
  189. else if (side === 'c') {
  190. ratioKey = -ratio;
  191. index = 2;
  192. }
  193. }
  194. else if (type === 'm') {
  195. ratioKey = 'm';
  196. if (side == 'h') {
  197. index = 0;
  198. }
  199. else if (side == 'c') {
  200. index = 2;
  201. }
  202. else {
  203. index = 1;
  204. }
  205. }
  206. else if (type === 'wm') {
  207. ratioKey = `wm_${Math.abs(ratio)}`;
  208. if (side === 'h') {
  209. index = 0;
  210. }
  211. else if (side === 'c') {
  212. index = 2;
  213. }
  214. }
  215. if (typeof (ratioKey) == 'number') {
  216. if (ratioKey > 0) {
  217. ratioKey = `+${ratioKey}`;
  218. }
  219. // else if (ratioKey === 0) {
  220. // ratioKey = '-0';
  221. // }
  222. else {
  223. ratioKey = `${ratioKey}`;
  224. }
  225. }
  226. if (!ratioKey) {
  227. return;
  228. }
  229. if (!eventsMap[ratioKey]) {
  230. eventsMap[ratioKey] = new Array(3).fill(undefined);
  231. }
  232. const value = events[key] ?? 0;
  233. eventsMap[ratioKey][index] = { key, value };
  234. });
  235. return Object.keys(eventsMap).sort((a, b) => a.localeCompare(b)).map(key => {
  236. return [key, ...eventsMap[key]];
  237. });
  238. }
  239. const formatSolution = (solution, eventsList) => {
  240. const { cpr, info } = solution;
  241. if (!cpr || !info) {
  242. return null;
  243. }
  244. const cprKeys = cpr.map(item => item.k);
  245. const psEvents = eventsList.ps?.[info.ps.eventId] ?? {};
  246. const obEvents = eventsList.ob?.[info.ob.eventId] ?? {};
  247. const hgEvents = eventsList.hg?.[info.hg.eventId] ?? {};
  248. info.ps.events = formatPsEvents(psEvents);
  249. info.ob.events = formatEvents(obEvents, cprKeys);
  250. info.hg.events = formatEvents(hgEvents, cprKeys);
  251. info.ps.dateTime = dayjs(info.ps.timestamp).format('YYYY-MM-DD HH:mm:ss');
  252. info.ob.dateTime = dayjs(info.ob.timestamp).format('YYYY-MM-DD HH:mm:ss');
  253. info.hg.dateTime = dayjs(info.hg.timestamp).format('YYYY-MM-DD HH:mm:ss');
  254. cpr.forEach(item => {
  255. const { k, p } = item;
  256. if (!info[p]['selected']) {
  257. info[p]['selected'] = [];
  258. }
  259. info[p]['selected'].push(k);
  260. });
  261. return solution;
  262. }
  263. const updateSolutions = async () => {
  264. const { solutions: solutionsList, gamesEvents: eventsList } = await getSolutions();
  265. solutions.value = solutionsList?.map(solution => formatSolution(solution, eventsList)) ?? [];
  266. }
  267. const showTotalProfit = async () => {
  268. totalProfit.value = await calcTotalProfit();
  269. totalProfitVisible.value = true;
  270. };
  271. const closeTotalProfit = () => {
  272. totalProfitVisible.value = false;
  273. selectedSolutions.length = 0;
  274. totalProfit.value = {};
  275. };
  276. const toggleSolution = (sid, timestamp) => {
  277. const findIndex = selectedSolutions.findIndex(item => item.sid === sid);
  278. if (findIndex >= 0) {
  279. selectedSolutions.splice(findIndex, 1);
  280. }
  281. else if (selectedSolutions.length < 2) {
  282. selectedSolutions.push({ sid, timestamp });
  283. }
  284. else {
  285. selectedSolutions.splice(1, 1, { sid, timestamp });
  286. }
  287. if (selectedSolutions.length == 2) {
  288. showTotalProfit();
  289. }
  290. }
  291. let updateTimer = null;
  292. onMounted(() => {
  293. updateSolutions();
  294. updateTimer = setInterval(() => {
  295. updateSolutions();
  296. }, 1000 * 10);
  297. });
  298. onUnmounted(() => {
  299. clearInterval(updateTimer);
  300. });
  301. </script>
  302. <template>
  303. <div class="solution-container">
  304. <div class="contents-header transition-all duration-200" :style="headerStyle">
  305. <div class="solution-options">
  306. <Form layout="inline">
  307. <Form.Item label="PS 投注">
  308. <InputNumber size="small" placeholder="PS投注" min="1000" v-model:value="psOptions.bet" />
  309. </Form.Item>
  310. <!-- <Form.Item label="PS 返点">
  311. <InputNumber size="small" placeholder="PS返点" min="0" v-model:value="psOptions.rebate" />
  312. </Form.Item> -->
  313. </Form>
  314. </div>
  315. <div class="solution-header">
  316. <span>PS</span>
  317. <span>OB</span>
  318. <span>HG</span>
  319. <em>利润</em>
  320. </div>
  321. </div>
  322. <div class="solution-list">
  323. <div class="solution-item"
  324. v-for="{ sid, sol: { win_average, cross_type }, info: { ps, ob, hg }, selected, disabled } in solutionsList" :key="sid"
  325. :class="{ 'selected': selected, 'disabled': disabled }">
  326. <MatchCard platform="ps" :eventId="ps.eventId" :leagueName="ps.leagueName" :teamHomeName="ps.teamHomeName"
  327. :teamAwayName="ps.teamAwayName" :dateTime="ps.dateTime" :eventInfo="ps.eventInfo" :events="ps.events ?? []"
  328. :matchNumStr="ps.matchNumStr" :selected="ps.selected ?? []" />
  329. <MatchCard platform="ob" :eventId="ob.eventId" :leagueName="ob.leagueName" :teamHomeName="ob.teamHomeName"
  330. :teamAwayName="ob.teamAwayName" :dateTime="ob.dateTime" :events="ob.events ?? []"
  331. :selected="ob.selected ?? []" />
  332. <MatchCard platform="hg" :eventId="hg.eventId" :leagueName="hg.leagueName" :teamHomeName="hg.teamHomeName"
  333. :teamAwayName="hg.teamAwayName" :dateTime="hg.dateTime" :events="hg.events ?? []"
  334. :selected="hg.selected ?? []" />
  335. <div class="solution-profit" @click="!disabled && toggleSolution(sid, ps.timestamp)">
  336. <p>{{ win_average }}</p>
  337. <p>{{ cross_type }}</p>
  338. </div>
  339. </div>
  340. </div>
  341. <div class="list-empty" v-if="!solutionsList.length">暂无数据</div>
  342. <!-- <Drawer
  343. title="综合利润方案"
  344. placement="bottom"
  345. height="600"
  346. :visible="totalProfitVisible"
  347. @close="closeTotalProfit"
  348. >
  349. <div class="solution-total-profit" v-if="totalProfitValue.solutions.length">
  350. <div class="solution-item"
  351. v-for="{ sid, info: { ps, ob, hg } } in totalProfitValue.solutions" :key="sid">
  352. <MatchCard platform="ps" :eventId="ps.eventId" :leagueName="ps.leagueName" :teamHomeName="ps.teamHomeName"
  353. :teamAwayName="ps.teamAwayName" :dateTime="ps.dateTime" :eventInfo="ps.eventInfo" :events="ps.events ?? []"
  354. :matchNumStr="ps.matchNumStr" :selected="ps.selected ?? []" />
  355. <MatchCard platform="ob" :eventId="ob.eventId" :leagueName="ob.leagueName" :teamHomeName="ob.teamHomeName"
  356. :teamAwayName="ob.teamAwayName" :dateTime="ob.dateTime" :events="ob.events ?? []"
  357. :selected="ob.selected ?? []" />
  358. <MatchCard platform="hg" :eventId="hg.eventId" :leagueName="hg.leagueName" :teamHomeName="hg.teamHomeName"
  359. :teamAwayName="hg.teamAwayName" :dateTime="hg.dateTime" :events="hg.events ?? []"
  360. :selected="hg.selected ?? []" />
  361. </div>
  362. </div>
  363. <div class="profit-info">
  364. <table>
  365. <tr>
  366. <th></th>
  367. <td>PS</td>
  368. <td colspan="2">第一场</td>
  369. <td colspan="2">第二场</td>
  370. </tr>
  371. <tr>
  372. <th>赔率</th>
  373. <td>{{ totalProfitValue.psInfo[0]?.v }}: {{ totalProfitValue.psInfo[1]?.v }}</td>
  374. <td>{{ totalProfitValue.outPreSol[0]?.p }}: {{ totalProfitValue.outPreSol[0]?.v }}</td>
  375. <td>{{ totalProfitValue.outPreSol[1]?.p }}: {{ totalProfitValue.outPreSol[1]?.v }}</td>
  376. <td>{{ totalProfitValue.outSubSol[0]?.p }}: {{ totalProfitValue.outSubSol[0]?.v }}</td>
  377. <td>{{ totalProfitValue.outSubSol[1]?.p }}: {{ totalProfitValue.outSubSol[1]?.v }}</td>
  378. </tr>
  379. <tr>
  380. <th>下注</th>
  381. <td>{{ psOptions.bet }}</td>
  382. <td>{{ totalProfitValue.outPreSol[0]?.g }}</td>
  383. <td>{{ totalProfitValue.outPreSol[1]?.g }}</td>
  384. <td>{{ totalProfitValue.outSubSol[0]?.g }}</td>
  385. <td>{{ totalProfitValue.outSubSol[1]?.g }}</td>
  386. </tr>
  387. <tr>
  388. <th>利润</th>
  389. <td>{{ totalProfitValue.profit.win_ps }}</td>
  390. <td colspan="2">{{ totalProfitValue.profit.win_target }}</td>
  391. <td colspan="2">{{ totalProfitValue.profit.win_target }}</td>
  392. </tr>
  393. </table>
  394. </div>
  395. </Drawer> -->
  396. </div>
  397. </template>
  398. <style lang="scss" scoped>
  399. .contents-header {
  400. position: fixed;
  401. top: 0;
  402. left: 0;
  403. z-index: 201;
  404. width: 100%;
  405. border-bottom: 1px solid hsl(var(--border));
  406. background-color: hsl(var(--background));
  407. }
  408. .solution-options {
  409. position: relative;
  410. display: flex;
  411. align-items: center;
  412. padding: 5px 20px;
  413. border-bottom: 1px solid hsl(var(--border));
  414. }
  415. .solution-header {
  416. position: relative;
  417. display: flex;
  418. align-items: center;
  419. height: 30px;
  420. padding: 0 20px;
  421. span,
  422. em {
  423. display: block;
  424. text-align: center;
  425. }
  426. span {
  427. flex: 1;
  428. }
  429. em {
  430. width: 80px;
  431. font-style: normal;
  432. }
  433. }
  434. .solution-container {
  435. padding-top: 74px;
  436. }
  437. .solution-item {
  438. display: flex;
  439. .match-card {
  440. flex: 1;
  441. }
  442. }
  443. .solution-list {
  444. padding: 20px;
  445. overflow: hidden;
  446. .solution-item {
  447. border-radius: 10px;
  448. background-color: hsl(var(--card));
  449. &.selected {
  450. background-color: hsl(var(--primary) / 0.15);
  451. }
  452. &.disabled {
  453. opacity: 0.5;
  454. cursor: not-allowed;
  455. }
  456. &:not(:last-child) {
  457. margin-bottom: 20px;
  458. }
  459. .match-card {
  460. border-right: 1px solid hsl(var(--border));
  461. }
  462. .solution-profit {
  463. display: flex;
  464. flex-direction: column;
  465. width: 80px;
  466. align-items: center;
  467. justify-content: center;
  468. }
  469. }
  470. }
  471. .profit-info {
  472. table {
  473. width: 100%;
  474. border-collapse: collapse;
  475. border-spacing: 0;
  476. table-layout: fixed;
  477. th, td {
  478. height: 30px;
  479. border: 1px solid hsl(var(--border));
  480. text-align: center;
  481. }
  482. th {
  483. width: 64px;
  484. font-weight: normal;
  485. }
  486. }
  487. }
  488. .list-empty {
  489. text-align: center;
  490. padding: 10px;
  491. font-size: 18px;
  492. color: hsl(var(--foreground) / 0.7);
  493. }
  494. </style>