index.vue 9.1 KB

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