| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613 |
- <script setup>
- import { requestClient } from '#/api/request';
- import { Button, message, Form, InputNumber, Drawer } from 'ant-design-vue';
- import { ref, reactive, computed, onMounted, onUnmounted } from 'vue';
- import dayjs from 'dayjs';
- import MatchCard from '../components/match_card.vue';
- import { useContentsPositionStore } from '@vben/stores';
- const contentsPositionStore = useContentsPositionStore();
- const solutions = ref([]);
- const selectedSolutions = reactive([]);
- const totalProfit = ref({});
- const loopActive = ref(false);
- const psOptions = reactive({
- bet: 10000,
- rebate: 0,
- });
- const totalProfitVisible = ref(false);
- const fixFloat = (number, x = 2) => {
- return parseFloat(number.toFixed(x));
- }
- const headerStyle = computed(() => {
- return {
- position: contentsPositionStore.position,
- top: contentsPositionStore.top,
- left: contentsPositionStore.left,
- width: contentsPositionStore.width,
- paddingLeft: contentsPositionStore.paddingLeft,
- }
- });
- const totalProfitValue = computed(() => {
- const { profit = {}, preSolution = {}, subSolution = {}, gamesEvents = {} } = totalProfit.value;
- const sol1 = formatSolution(preSolution, gamesEvents);
- const sol2 = formatSolution(subSolution, gamesEvents);
- const psScale = psOptions.bet / profit.ps_base ?? 10000;
- const psRebate = psOptions.bet * psOptions.rebate / 100;
- const profitInfo = {};
- Object.keys(profit).forEach(key => {
- if (key == 'win_diff') {
- return;
- }
- if (key.startsWith('gold')) {
- profitInfo[key] = fixFloat(profit[key] * psScale);
- }
- else if (key.startsWith('win_')) {
- profitInfo[key] = fixFloat(profit[key] * psScale + psRebate);
- }
- });
- const psInfo = [];
- const outPreSol = [];
- const outSubSol = [];
- const solutions = [sol1, sol2].filter(item => item);
- solutions.forEach((item, index) => {
- const { sol: { ps_index }, cpr } = item;
- const newCpr = [...cpr];
- const ps_info = newCpr.splice(ps_index, 1);
- psInfo.push({ ...ps_info[0] });
- newCpr.forEach((c, i) => {
- let side = '';
- if (ps_index == 0) {
- if (i == 0) {
- side = "B"
- }
- else {
- side = "M";
- }
- }
- else if (ps_index == 1) {
- if (i == 0) {
- side = "A";
- }
- else {
- side = "M";
- }
- }
- else {
- if (i == 0) {
- side = "A";
- }
- else {
- side = "B";
- }
- }
- if (index == 0) {
- outPreSol.push({ ...c, g: profitInfo[`gold${side}${index+1}`] });
- }
- else {
- outSubSol.push({ ...c, g: profitInfo[`gold${side}${index+1}`] });
- }
- })
- });
- return { solutions, profit: profitInfo, psInfo, outPreSol, outSubSol };
- });
- const solutionsList = computed(() => {
- const startTimestamp = selectedSolutions[0]?.timestamp ?? 0;
- return solutions.value.map(item => {
- const selected = selectedSolutions.findIndex(sol => sol.sid === item.sid) >= 0;
- const disabled = false && !selected && (item.info.ps.timestamp < startTimestamp + 1000 * 60 * 60 * 2);
- const currentSol = { ...item.sol };
- const psScale = psOptions.bet / currentSol.inner_base;
- const psRebate = psOptions.bet * psOptions.rebate / 100;
- Object.keys(currentSol).forEach(key => {
- if (key.startsWith('gold_')) {
- currentSol[key] = fixFloat(currentSol[key] * psScale);
- }
- else if (key.startsWith('win_')) {
- currentSol[key] = fixFloat(currentSol[key] * psScale + psRebate);
- }
- });
- return { ...item, sol: currentSol, selected, disabled };
- });
- });
- const getSolutions = async () => {
- try {
- const data = await requestClient.get('/pstery/get_solutions');
- return data;
- }
- catch (error) {
- console.error('Failed to fetch solutions:', error);
- message.error('获取中单方案失败');
- return [];
- }
- }
- const calcTotalProfit = async () => {
- const sids = selectedSolutions.map(item => item.sid);
- try {
- const totalProfit = await requestClient.post('/pstery/calc_total_profit', [...sids, psOptions.bet]);
- return totalProfit;
- }
- catch (error) {
- console.error('Failed to calc total profit:', error);
- message.error('计算综合利润失败');
- return {};
- }
- }
- const parseIorKey = (iorKey) => {
- const [, type, accept, side, , ratioString] = iorKey.match(/^ior_(r|ou|m|wm|ot|os)(a?)(h|c|n)?(_([\d-]+))?$/);
- let ratio = 0;
- if (type === 'ot' || type === 'os') {
- ratio = ratioString;
- }
- else if (ratioString) {
- ratio = `${ratioString[0]}.${ratioString.slice(1)}` * (accept ? 1 : -1);
- }
- return { type, side, ratio };
- }
- const PS_IOR_KEYS = [
- ['0', 'ior_mh', 'ior_mn', 'ior_mc'],
- ['-1', 'ior_rh_15', 'ior_wmh_1', 'ior_rac_05'],
- ['-2', 'ior_rh_25', 'ior_wmh_2', 'ior_rac_15'],
- ['+1', 'ior_rah_05', 'ior_wmc_1', 'ior_rc_15'],
- ['+2', 'ior_rah_15', 'ior_wmc_2', 'ior_rc_25'],
- ['0-1', 'ior_ot_0', 'ior_os_0-1', 'ior_ot_1'],
- ['2-3', 'ior_ot_2', 'ior_os_2-3', 'ior_ot_3'],
- ];
- const formatPsEvents = (events) => {
- return PS_IOR_KEYS.map(([label, ...keys]) => {
- const match = keys.map(key => ({
- key,
- value: events[key] ?? 0
- }));
- return {
- label,
- match
- };
- })
- // .filter(item => item.match.every(entry => entry.value !== 0))
- .map(({label, match}) => [label, ...match]);
- }
- // const rivalIor = (ior) => {
- // const map = {
- // "ior_rh": "ior_rac",
- // "ior_rc": "ior_rah",
- // "ior_rac": "ior_rh",
- // "ior_rah": "ior_rc",
- // "ior_wmh": "ior_wmc",
- // "ior_wmc": "ior_wmh",
- // "ior_wmh_2": "ior_wmc_2",
- // "ior_wmc_2": "ior_wmh_2"
- // };
- // const iorInfos = ior.split('_');
- // const iorStart = iorInfos.slice(0, 2).join('_');
- // if (!map[iorStart]) {
- // return ior;
- // }
- // return `${map[iorStart]}_${iorInfos[2]}`;
- // }
- const formatEvents = (events, cprKeys) => {
- const eventsMap = {};
- Object.keys(events).forEach(key => {
- const { type, side, ratio } = parseIorKey(key);
- let ratioKey, index;
- if (type === 'r') {
- if (side === 'h') {
- ratioKey = ratio;
- index = 0;
- }
- else if (side === 'c') {
- ratioKey = -ratio;
- index = 2;
- }
- }
- else if (type === 'm') {
- ratioKey = 'm';
- if (side == 'h') {
- index = 0;
- }
- else if (side == 'c') {
- index = 2;
- }
- else {
- index = 1;
- }
- }
- else if (type === 'wm') {
- ratioKey = `wm_${Math.abs(ratio)}`;
- if (side === 'h') {
- index = 0;
- }
- else if (side === 'c') {
- index = 2;
- }
- }
- else if (type === 'ou') {
- ratioKey = `ou_${Math.abs(ratio)}`;
- if (side === 'c') {
- index = 0;
- }
- else if (side === 'h') {
- index = 2;
- }
- }
- else if (type === 'os') {
- ratioKey = ratio;
- index = 1;
- }
- else if (type === 'ot') {
- switch (ratio) {
- case '0':
- ratioKey = '0-1';
- index = 0;
- break;
- case '1':
- ratioKey = '0-1';
- index = 2;
- break;
- case '2':
- ratioKey = '2-3';
- index = 0;
- break;
- case '3':
- ratioKey = '2-3';
- index = 2;
- break;
- }
- }
- if (typeof (ratioKey) == 'number') {
- if (ratioKey > 0) {
- ratioKey = `+${ratioKey}`;
- }
- // else if (ratioKey === 0) {
- // ratioKey = '-0';
- // }
- else {
- ratioKey = `${ratioKey}`;
- }
- }
- if (!ratioKey) {
- return;
- }
- if (!eventsMap[ratioKey]) {
- eventsMap[ratioKey] = new Array(3).fill(undefined);
- }
- const value = events[key] ?? 0;
- eventsMap[ratioKey][index] = { key, value };
- });
- return Object.keys(eventsMap).sort((a, b) => a.localeCompare(b)).map(key => {
- return [key, ...eventsMap[key]];
- });
- }
- const formatSolution = (solution, eventsList) => {
- const { cpr, info } = solution;
- if (!cpr || !info) {
- return null;
- }
- const cprKeys = cpr.map(item => item.k);
- const psEvents = eventsList.ps?.[info.ps.eventId] ?? {};
- const obEvents = eventsList.ob?.[info.ob.eventId] ?? {};
- const hgEvents = eventsList.hg?.[info.hg.eventId] ?? {};
- info.ps.events = formatPsEvents(psEvents);
- info.ob.events = formatEvents(obEvents, cprKeys);
- info.hg.events = formatEvents(hgEvents, cprKeys);
- info.ps.dateTime = dayjs(info.ps.timestamp).format('YYYY-MM-DD HH:mm:ss');
- info.ob.dateTime = dayjs(info.ob.timestamp).format('YYYY-MM-DD HH:mm:ss');
- info.hg.dateTime = dayjs(info.hg.timestamp).format('YYYY-MM-DD HH:mm:ss');
- cpr.forEach(item => {
- const { k, p } = item;
- if (!info[p]['selected']) {
- info[p]['selected'] = [];
- }
- info[p]['selected'].push(k);
- });
- return solution;
- }
- const updateSolutions = async () => {
- getSolutions()
- .then(({ solutions: solutionsList, gamesEvents: eventsList }) => {
- solutions.value = solutionsList?.map(solution => formatSolution(solution, eventsList)) ?? [];
- })
- .catch(error => {
- console.error('Failed to update solutions:', error);
- message.error('获取中单方案失败');
- })
- .finally(() => {
- if (loopActive.value) {
- setTimeout(() => {
- updateSolutions();
- }, 1000 * 10);
- }
- });
- }
- const showTotalProfit = async () => {
- totalProfit.value = await calcTotalProfit();
- totalProfitVisible.value = true;
- const { profit } = totalProfit.value;
- console.log('profit', profit);
- };
- const closeTotalProfit = () => {
- totalProfitVisible.value = false;
- selectedSolutions.length = 0;
- totalProfit.value = {};
- };
- const toggleSolution = (sid, timestamp) => {
- const findIndex = selectedSolutions.findIndex(item => item.sid === sid);
- if (findIndex >= 0) {
- selectedSolutions.splice(findIndex, 1);
- }
- else if (selectedSolutions.length < 2) {
- selectedSolutions.push({ sid, timestamp });
- }
- else {
- selectedSolutions.splice(1, 1, { sid, timestamp });
- }
- if (selectedSolutions.length == 2) {
- showTotalProfit();
- }
- }
- onMounted(() => {
- loopActive.value = true;
- updateSolutions();
- });
- onUnmounted(() => {
- loopActive.value = false;
- });
- </script>
- <template>
- <div class="solution-container">
- <div class="contents-header transition-all duration-200" :style="headerStyle">
- <div class="solution-options">
- <Form layout="inline">
- <Form.Item label="PS 投注">
- <InputNumber size="small" placeholder="PS投注" min="1000" v-model:value="psOptions.bet" />
- </Form.Item>
- <!-- <Form.Item label="PS 返点">
- <InputNumber size="small" placeholder="PS返点" min="0" v-model:value="psOptions.rebate" />
- </Form.Item> -->
- </Form>
- </div>
- <div class="solution-header">
- <span>PS</span>
- <span>OB</span>
- <span>HG</span>
- <em>利润</em>
- </div>
- </div>
- <div class="solution-list">
- <div class="solution-item"
- v-for="{ sid, sol: { win_average, win_profit_rate, cross_type }, info: { ps, ob, hg }, selected, disabled } in solutionsList" :key="sid"
- :class="{ 'selected': selected, 'disabled': disabled }">
- <MatchCard platform="ps" :eventId="ps.eventId" :leagueName="ps.leagueName" :teamHomeName="ps.teamHomeName"
- :teamAwayName="ps.teamAwayName" :dateTime="ps.dateTime" :eventInfo="ps.eventInfo" :events="ps.events ?? []"
- :matchNumStr="ps.matchNumStr" :selected="ps.selected ?? []" />
- <MatchCard platform="ob" :eventId="ob.eventId" :leagueName="ob.leagueName" :teamHomeName="ob.teamHomeName"
- :teamAwayName="ob.teamAwayName" :dateTime="ob.dateTime" :events="ob.events ?? []"
- :selected="ob.selected ?? []" />
- <MatchCard platform="hg" :eventId="hg.eventId" :leagueName="hg.leagueName" :teamHomeName="hg.teamHomeName"
- :teamAwayName="hg.teamAwayName" :dateTime="hg.dateTime" :events="hg.events ?? []"
- :selected="hg.selected ?? []" />
- <div class="solution-profit" @click="!disabled && toggleSolution(sid, ps.timestamp)">
- <p>{{ win_average }}</p>
- <p>{{ win_profit_rate }}%</p>
- <p>{{ cross_type }}</p>
- </div>
- </div>
- </div>
- <div class="list-empty" v-if="!solutionsList.length">暂无数据</div>
- <!-- <Drawer
- title="综合利润方案"
- placement="bottom"
- height="600"
- :visible="totalProfitVisible"
- @close="closeTotalProfit"
- >
- <div class="solution-total-profit" v-if="totalProfitValue.solutions.length">
- <div class="solution-item"
- v-for="{ sid, info: { ps, ob, hg } } in totalProfitValue.solutions" :key="sid">
- <MatchCard platform="ps" :eventId="ps.eventId" :leagueName="ps.leagueName" :teamHomeName="ps.teamHomeName"
- :teamAwayName="ps.teamAwayName" :dateTime="ps.dateTime" :eventInfo="ps.eventInfo" :events="ps.events ?? []"
- :matchNumStr="ps.matchNumStr" :selected="ps.selected ?? []" />
- <MatchCard platform="ob" :eventId="ob.eventId" :leagueName="ob.leagueName" :teamHomeName="ob.teamHomeName"
- :teamAwayName="ob.teamAwayName" :dateTime="ob.dateTime" :events="ob.events ?? []"
- :selected="ob.selected ?? []" />
- <MatchCard platform="hg" :eventId="hg.eventId" :leagueName="hg.leagueName" :teamHomeName="hg.teamHomeName"
- :teamAwayName="hg.teamAwayName" :dateTime="hg.dateTime" :events="hg.events ?? []"
- :selected="hg.selected ?? []" />
- </div>
- </div>
- <div class="profit-info">
- <table>
- <tr>
- <th></th>
- <td>PS</td>
- <td colspan="2">第一场</td>
- <td colspan="2">第二场</td>
- </tr>
- <tr>
- <th>赔率</th>
- <td>{{ totalProfitValue.psInfo[0]?.v }}: {{ totalProfitValue.psInfo[1]?.v }}</td>
- <td>{{ totalProfitValue.outPreSol[0]?.p }}: {{ totalProfitValue.outPreSol[0]?.v }}</td>
- <td>{{ totalProfitValue.outPreSol[1]?.p }}: {{ totalProfitValue.outPreSol[1]?.v }}</td>
- <td>{{ totalProfitValue.outSubSol[0]?.p }}: {{ totalProfitValue.outSubSol[0]?.v }}</td>
- <td>{{ totalProfitValue.outSubSol[1]?.p }}: {{ totalProfitValue.outSubSol[1]?.v }}</td>
- </tr>
- <tr>
- <th>下注</th>
- <td>{{ psOptions.bet }}</td>
- <td>{{ totalProfitValue.outPreSol[0]?.g }}</td>
- <td>{{ totalProfitValue.outPreSol[1]?.g }}</td>
- <td>{{ totalProfitValue.outSubSol[0]?.g }}</td>
- <td>{{ totalProfitValue.outSubSol[1]?.g }}</td>
- </tr>
- <tr>
- <th>利润</th>
- <td>{{ totalProfitValue.profit.win_ps }}</td>
- <td colspan="2">{{ totalProfitValue.profit.win_target }}</td>
- <td colspan="2">{{ totalProfitValue.profit.win_target }}</td>
- </tr>
- </table>
- </div>
- </Drawer> -->
- </div>
- </template>
- <style lang="scss" scoped>
- .contents-header {
- position: fixed;
- top: 0;
- left: 0;
- z-index: 201;
- width: 100%;
- border-bottom: 1px solid hsl(var(--border));
- background-color: hsl(var(--background));
- }
- .solution-options {
- position: relative;
- display: flex;
- align-items: center;
- padding: 5px 20px;
- border-bottom: 1px solid hsl(var(--border));
- }
- .solution-header {
- position: relative;
- display: flex;
- align-items: center;
- height: 30px;
- padding: 0 20px;
- span,
- em {
- display: block;
- text-align: center;
- }
- span {
- flex: 1;
- }
- em {
- width: 80px;
- font-style: normal;
- }
- }
- .solution-container {
- padding-top: 74px;
- }
- .solution-item {
- display: flex;
- .match-card {
- flex: 1;
- }
- }
- .solution-list {
- padding: 20px;
- overflow: hidden;
- .solution-item {
- border-radius: 10px;
- background-color: hsl(var(--card));
- &.selected {
- background-color: hsl(var(--primary) / 0.15);
- }
- &.disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
- &:not(:last-child) {
- margin-bottom: 20px;
- }
- .match-card {
- border-right: 1px solid hsl(var(--border));
- }
- .solution-profit {
- display: flex;
- flex-direction: column;
- width: 80px;
- align-items: center;
- justify-content: center;
- }
- }
- }
- .profit-info {
- table {
- width: 100%;
- border-collapse: collapse;
- border-spacing: 0;
- table-layout: fixed;
- th, td {
- height: 30px;
- border: 1px solid hsl(var(--border));
- text-align: center;
- }
- th {
- width: 64px;
- font-weight: normal;
- }
- }
- }
- .list-empty {
- text-align: center;
- padding: 10px;
- font-size: 18px;
- color: hsl(var(--foreground) / 0.7);
- }
- </style>
|