| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664 |
- <script setup>
- import { requestClient } from '#/api/request';
- import { Button, message, Form, InputNumber, RadioGroup, Radio, Drawer } from 'ant-design-vue';
- import { ref, reactive, computed, watch, 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 markCount = ref({ all: 0, rollball: 0, today: 0, early: 0 });
- const selectedSolutions = reactive([]);
- const totalProfit = ref({});
- const loopActive = ref(false);
- const loopTimer = ref(null);
- const updateTimer = ref(null);
- const minProfitRate = ref(2);
- const marketType = ref(-1);
- const updateLoaderHide = ref(null);
- 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 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 { sol: { inner_base, win_average } } = item;
- const win_average_rate = fixFloat(win_average / inner_base * 100);
- const currentSol = { ...item.sol, win_average_rate };
- return { ...item, sol: currentSol, selected, disabled };
- });
- });
- const getSolutions = async () => {
- try {
- const win_min = minProfitRate.value * 100;
- const mk = marketType.value;
- const with_events = true;
- const data = await requestClient.get('/pstery/get_solutions', { params: { win_min, mk, with_events } });
- 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]);
- 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'],
- ['ot_1', '-', 'ior_ot_1', '-'],
- ['ot_2', '-', 'ior_ot_2', '-'],
- ['ot_3', '-', 'ior_ot_3', '-'],
- ['ot_4', '-', 'ior_ot_4', '-'],
- ['ot_5', '-', 'ior_ot_5', '-'],
- ['ot_6', '-', 'ior_ot_6', '-'],
- ['ot_7', '-', 'ior_ot_7', '-'],
- ];
- const formatPsEvents = (events) => {
- return PS_IOR_KEYS.map(([label, ...keys]) => {
- const match = keys.map(key => ({
- key,
- value: events[key]?.v ?? 0,
- origin: events[key]?.r
- }));
- 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') {
- ratioKey = `ot_${ratio}`;
- index = 1;
- // 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]?.v ?? 0;
- const origin = events[key]?.r;
- eventsMap[ratioKey][index] = { key, value, origin };
- });
- 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 (showLoading=false) => {
- clearTimeout(loopTimer.value);
- if (showLoading && !updateLoaderHide.value) {
- updateLoaderHide.value = message.loading('数据加载中...', 0);
- }
- getSolutions()
- .then(({ solutions: solutionsList, gamesEvents: eventsList, mkCount: mkCountData }) => {
- solutions.value = solutionsList?.map(solution => formatSolution(solution, eventsList)) ?? [];
- markCount.value = mkCountData;
- })
- .catch(error => {
- console.error('Failed to update solutions:', error);
- message.error('获取中单方案失败');
- })
- .finally(() => {
- updateLoaderHide.value?.();
- updateLoaderHide.value = null;
- if (loopActive.value) {
- loopTimer.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();
- }
- }
- const setLocalStorage = (key, value) => {
- localStorage.setItem(key, JSON.stringify(value));
- }
- const getLocalStorage = (key) => {
- const value = localStorage.getItem(key);
- return value ? JSON.parse(value) : null;
- }
- watch(minProfitRate, (newVal) => {
- clearTimeout(updateTimer.value);
- updateTimer.value = setTimeout(() => {
- setLocalStorage('minProfitRate', newVal);
- updateSolutions();
- }, 1000);
- });
- watch(marketType, (newVal) => {
- if (!updateLoaderHide.value) {
- updateLoaderHide.value = message.loading('数据更新中...', 0);
- }
- clearTimeout(updateTimer.value);
- updateTimer.value = setTimeout(() => {
- setLocalStorage('marketType', newVal);
- updateSolutions();
- }, 1000);
- });
- onMounted(() => {
- loopActive.value = true;
- const min_win_rate = getLocalStorage('minProfitRate');
- const mk = getLocalStorage('marketType');
- if (min_win_rate !== null) {
- minProfitRate.value = min_win_rate;
- }
- if (mk !== null) {
- marketType.value = mk;
- }
- setTimeout(() => {
- updateSolutions(true);
- }, 100);
- });
- 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" class="sol-opt-container">
- <Form.Item label="盘口类型" class="sol-opt-item">
- <RadioGroup v-model:value="marketType">
- <Radio :value="-1">全部({{ markCount.all ?? 0 }})</Radio>
- <Radio :value="2">滚球({{ markCount.rollball ?? 0 }})</Radio>
- <Radio :value="1">今日({{ markCount.today ?? 0 }})</Radio>
- <Radio :value="0">早盘({{ markCount.early ?? 0 }})</Radio>
- </RadioGroup>
- </Form.Item>
- <Form.Item label="最小利润率(%)" class="sol-opt-item">
- <InputNumber style="width: 60px" size="small" max="100" min="-100" step="0.1" placeholder="最小利润率(%)" v-model:value="minProfitRate"/>
- </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_rate, 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 ?? []" :stage="ps.stage" :retime="ps.retime" :score="ps.score" />
- <MatchCard platform="ob" :eventId="ob.eventId" :leagueName="ob.leagueName" :teamHomeName="ob.teamHomeName"
- :teamAwayName="ob.teamAwayName" :dateTime="ob.dateTime" :events="ob.events ?? []"
- :selected="ob.selected ?? []" :stage="ob.stage" :retime="ob.retime" :score="ob.score" />
- <MatchCard platform="hg" :eventId="hg.eventId" :leagueName="hg.leagueName" :teamHomeName="hg.teamHomeName"
- :teamAwayName="hg.teamAwayName" :dateTime="hg.dateTime" :events="hg.events ?? []"
- :selected="hg.selected ?? []" :stage="hg.stage" :retime="hg.retime" :score="hg.score" />
- <div class="solution-profit" @click="!disabled && toggleSolution(sid, ps.timestamp)">
- <p>{{ win_average_rate }}%</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));
- }
- .sol-opt-container {
- flex-grow: 1;
- justify-content: space-between;
- }
- .sol-opt-item:last-child {
- margin-inline-end: 0 !important;
- }
- .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>
|