index.vue 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. <script setup>
  2. import { requestClient } from '#/api/request';
  3. import { Button, message, Form, InputNumber, RadioGroup, Radio, Checkbox, Drawer, Input, Switch } from 'ant-design-vue';
  4. import { ref, reactive, computed, watch, onMounted, onUnmounted } from 'vue';
  5. import dayjs from 'dayjs';
  6. // import MatchCard from '../components/match_card.vue';
  7. import SolutionItem from '../components/solution_item.vue';
  8. import { useContentsPositionStore } from '@vben/stores';
  9. const contentsPositionStore = useContentsPositionStore();
  10. const solutions = ref([]);
  11. const markCount = ref({ all: 0, rollball: 0, today: 0, early: 0 });
  12. const selectedSolutions = reactive([]);
  13. const totalProfit = ref({});
  14. const loopActive = ref(false);
  15. const loopTimer = ref(null);
  16. const updateTimer = ref(null);
  17. const minProfitRate = ref(2);
  18. const marketType = ref(-1);
  19. const dataType = ref(0);
  20. const showLower = ref(false);
  21. const searchValue = ref('');
  22. const updateLoaderHide = ref(null);
  23. const totalProfitVisible = ref(false);
  24. const fixFloat = (number, x = 2) => {
  25. return parseFloat(number.toFixed(x));
  26. }
  27. const headerStyle = computed(() => {
  28. return {
  29. position: contentsPositionStore.position,
  30. top: contentsPositionStore.top,
  31. left: contentsPositionStore.left,
  32. width: contentsPositionStore.width,
  33. paddingLeft: contentsPositionStore.paddingLeft,
  34. }
  35. });
  36. const solutionsList = computed(() => {
  37. return solutions.value.map(item => {
  38. const selected = selectedSolutions.findIndex(sol => sol.id == item.id) >= 0;
  39. const topSolutions = item.solutions.slice(0, 10);
  40. return { ...item, solutions: topSolutions, selected };
  41. });
  42. });
  43. const getSolutions = async () => {
  44. try {
  45. const mk = marketType.value;
  46. const tp = dataType.value;
  47. const sk = searchValue.value.trim();
  48. const win_min = !!sk ? -99999 : minProfitRate.value * 100;
  49. const with_events = true;
  50. const show_lower = showLower.value;
  51. const data = await requestClient.get('/pstery/get_games_solutions', { params: { win_min, mk, tp, sk, with_events, show_lower } });
  52. return data;
  53. }
  54. catch (error) {
  55. console.error('Failed to fetch solutions:', error);
  56. message.error('获取中单方案失败');
  57. return [];
  58. }
  59. }
  60. const calcTotalProfit = async () => {
  61. const sids = selectedSolutions.map(item => item.sid);
  62. try {
  63. const totalProfit = await requestClient.post('/pstery/calc_total_profit', {sids});
  64. return totalProfit;
  65. }
  66. catch (error) {
  67. console.error('Failed to calc total profit:', error);
  68. message.error('计算综合利润失败');
  69. return {};
  70. }
  71. }
  72. const updateSolutions = async (showLoading=false) => {
  73. clearTimeout(loopTimer.value);
  74. if (showLoading && !updateLoaderHide.value) {
  75. updateLoaderHide.value = message.loading('数据加载中...', 0);
  76. }
  77. getSolutions()
  78. .then(({ gamesSolutions, mkCount }) => {
  79. solutions.value = gamesSolutions ?? [];
  80. markCount.value = mkCount;
  81. })
  82. .catch(error => {
  83. console.error('Failed to update solutions:', error);
  84. message.error('获取中单方案失败');
  85. })
  86. .finally(() => {
  87. updateLoaderHide.value?.();
  88. updateLoaderHide.value = null;
  89. if (loopActive.value) {
  90. loopTimer.value = setTimeout(() => {
  91. updateSolutions();
  92. }, 1000 * 10);
  93. }
  94. });
  95. }
  96. const showTotalProfit = async () => {
  97. totalProfit.value = await calcTotalProfit();
  98. totalProfitVisible.value = true;
  99. const { profit } = totalProfit.value;
  100. console.log('profit', profit);
  101. };
  102. const toggleSolution = (data) => {
  103. // console.log('toggleSolution', data);
  104. const { id, sid } = data;
  105. const findIndex = selectedSolutions.findIndex(item => item.id == id);
  106. if (findIndex >= 0) {
  107. if (selectedSolutions[findIndex].sid == sid) {
  108. selectedSolutions.splice(findIndex, 1);
  109. }
  110. else {
  111. selectedSolutions.splice(findIndex, 1, data);
  112. }
  113. }
  114. else if (selectedSolutions.length < 2) {
  115. selectedSolutions.push(data);
  116. }
  117. else {
  118. selectedSolutions.splice(1, 1, data);
  119. }
  120. if (selectedSolutions.length == 2) {
  121. showTotalProfit();
  122. }
  123. }
  124. const setLocalStorage = (key, value) => {
  125. localStorage.setItem(key, JSON.stringify(value));
  126. }
  127. const getLocalStorage = (key) => {
  128. const value = localStorage.getItem(key);
  129. return value ? JSON.parse(value) : null;
  130. }
  131. watch(searchValue, (newVal, oldVal) => {
  132. if (newVal.trim() == oldVal.trim()) {
  133. return;
  134. }
  135. clearTimeout(updateTimer.value);
  136. updateTimer.value = setTimeout(() => {
  137. updateSolutions();
  138. }, 1000);
  139. });
  140. watch(minProfitRate, (newVal) => {
  141. clearTimeout(updateTimer.value);
  142. updateTimer.value = setTimeout(() => {
  143. setLocalStorage('minProfitRate', newVal);
  144. updateSolutions();
  145. }, 1000);
  146. });
  147. watch(marketType, (newVal) => {
  148. if (!updateLoaderHide.value) {
  149. updateLoaderHide.value = message.loading('数据更新中...', 0);
  150. }
  151. clearTimeout(updateTimer.value);
  152. updateTimer.value = setTimeout(() => {
  153. setLocalStorage('marketType', newVal);
  154. updateSolutions();
  155. }, 1000);
  156. });
  157. watch(dataType, (newVal) => {
  158. if (!updateLoaderHide.value) {
  159. updateLoaderHide.value = message.loading('数据更新中...', 0);
  160. }
  161. clearTimeout(updateTimer.value);
  162. updateTimer.value = setTimeout(() => {
  163. setLocalStorage('dataType', newVal);
  164. updateSolutions();
  165. }, 1000);
  166. });
  167. watch(showLower, (newVal) => {
  168. if (!updateLoaderHide.value) {
  169. updateLoaderHide.value = message.loading('数据更新中...', 0);
  170. }
  171. clearTimeout(updateTimer.value);
  172. updateTimer.value = setTimeout(() => {
  173. setLocalStorage('showLower', newVal);
  174. updateSolutions();
  175. }, 1000);
  176. });
  177. onMounted(() => {
  178. loopActive.value = true;
  179. const min_win_rate = getLocalStorage('minProfitRate');
  180. const mk = getLocalStorage('marketType');
  181. const tp = getLocalStorage('dataType');
  182. const show_lower = getLocalStorage('showLower');
  183. if (min_win_rate !== null) {
  184. minProfitRate.value = min_win_rate;
  185. }
  186. if (mk !== null) {
  187. marketType.value = mk;
  188. }
  189. if (tp !== null) {
  190. dataType.value = tp;
  191. }
  192. if (show_lower !== null) {
  193. showLower.value = show_lower;
  194. }
  195. setTimeout(() => {
  196. updateSolutions(true);
  197. }, 100);
  198. });
  199. onUnmounted(() => {
  200. loopActive.value = false;
  201. });
  202. </script>
  203. <template>
  204. <div class="solution-container">
  205. <div class="contents-header transition-all duration-200" :style="headerStyle">
  206. <div class="solution-options">
  207. <Form layout="inline" class="sol-opt-container">
  208. <Form.Item label="比赛类型" class="sol-opt-item">
  209. <RadioGroup v-model:value="marketType">
  210. <Radio :value="-1">全部({{ markCount?.all ?? 0 }})</Radio>
  211. <Radio :value="2">滚球({{ markCount?.rollball ?? 0 }})</Radio>
  212. <Radio :value="1">今日({{ markCount?.today ?? 0 }})</Radio>
  213. <Radio :value="0">早盘({{ markCount?.early ?? 0 }})</Radio>
  214. </RadioGroup>
  215. </Form.Item>
  216. <Form.Item label="盘口类型" class="sol-opt-item">
  217. <RadioGroup v-model:value="dataType">
  218. <Radio :value="0">全部</Radio>
  219. <Radio :value="1">让球</Radio>
  220. <Radio :value="2">大小</Radio>
  221. </RadioGroup>
  222. </Form.Item>
  223. <Form.Item label="最小利润率(%)" class="sol-opt-item input-item" :class="{ 'disabled': !!searchValue.trim() }">
  224. <InputNumber class="number-input" size="small" max="100" min="-100" step="0.1" placeholder="最小利润率(%)" v-model:value="minProfitRate"/>
  225. </Form.Item>
  226. <Form.Item label="显示低赔盘" class="sol-opt-item input-item">
  227. <Switch v-model:checked="showLower" />
  228. </Form.Item>
  229. <Form.Item class="sol-opt-item input-item">
  230. <Input class="search-input" placeholder="搜索联赛/球队" :allowClear="true" v-model:value="searchValue"/>
  231. </Form.Item>
  232. </Form>
  233. </div>
  234. <div class="solution-header">
  235. <span>PS</span>
  236. <span><Checkbox>OB</Checkbox></span>
  237. <span><Checkbox>HG</Checkbox></span>
  238. <span><Checkbox>IM</Checkbox></span>
  239. <!-- <em>利润</em> -->
  240. </div>
  241. </div>
  242. <div class="solution-list" v-if="solutionsList.length">
  243. <SolutionItem v-for="(solution, index) in solutionsList"
  244. :key="solution.id"
  245. :serial="index+1"
  246. :id="solution.id"
  247. :mk="solution.mk"
  248. :rel="solution.rel"
  249. :selected="solution.selected"
  250. :solutions="solution.solutions"
  251. @toggle="toggleSolution"
  252. />
  253. </div>
  254. <div class="list-empty" v-else>暂无数据</div>
  255. </div>
  256. </template>
  257. <style lang="scss" scoped>
  258. .contents-header {
  259. position: fixed;
  260. top: 0;
  261. left: 0;
  262. z-index: 201;
  263. width: 100%;
  264. border-bottom: 1px solid hsl(var(--border));
  265. background-color: hsl(var(--background));
  266. }
  267. .solution-options {
  268. position: relative;
  269. display: flex;
  270. align-items: center;
  271. padding: 5px 20px;
  272. border-bottom: 1px solid hsl(var(--border));
  273. }
  274. .sol-opt-container {
  275. flex-grow: 1;
  276. // justify-content: flex-end;
  277. }
  278. .sol-opt-item {
  279. margin-inline-end: 0 !important;
  280. &:nth-child(2) {
  281. margin-inline-start: auto;
  282. }
  283. &:nth-child(n+3) {
  284. padding-inline-start: 15px;
  285. border-left: 1px solid hsl(var(--border));
  286. }
  287. &.input-item:not(:last-child) {
  288. padding-inline-end: 15px;
  289. }
  290. &.disabled {
  291. opacity: 0.5;
  292. * {
  293. text-decoration: line-through;
  294. }
  295. }
  296. .search-input, .number-input {
  297. height: 28px;
  298. }
  299. .search-input {
  300. width: 150px;
  301. }
  302. .number-input {
  303. display: inline-flex;
  304. width: 60px;
  305. align-items: center;
  306. }
  307. }
  308. .solution-header {
  309. position: relative;
  310. display: flex;
  311. align-items: center;
  312. height: 30px;
  313. padding: 0 20px;
  314. span,
  315. em {
  316. display: block;
  317. text-align: center;
  318. }
  319. span {
  320. flex: 1;
  321. }
  322. // em {
  323. // width: 80px;
  324. // font-style: normal;
  325. // }
  326. }
  327. .solution-container {
  328. padding-top: 74px;
  329. }
  330. .solution-list {
  331. padding: 20px;
  332. overflow: hidden;
  333. }
  334. .list-empty {
  335. text-align: center;
  336. padding: 10px;
  337. font-size: 18px;
  338. color: hsl(var(--foreground) / 0.7);
  339. }
  340. </style>