index_bak.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664
  1. <script setup>
  2. import { requestClient } from '#/api/request';
  3. import { Button, message, Form, InputNumber, RadioGroup, Radio, Drawer } 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 { useContentsPositionStore } from '@vben/stores';
  8. const contentsPositionStore = useContentsPositionStore();
  9. const solutions = ref([]);
  10. const markCount = ref({ all: 0, rollball: 0, today: 0, early: 0 });
  11. const selectedSolutions = reactive([]);
  12. const totalProfit = ref({});
  13. const loopActive = ref(false);
  14. const loopTimer = ref(null);
  15. const updateTimer = ref(null);
  16. const minProfitRate = ref(2);
  17. const marketType = ref(-1);
  18. const updateLoaderHide = ref(null);
  19. const totalProfitVisible = ref(false);
  20. const fixFloat = (number, x = 2) => {
  21. return parseFloat(number.toFixed(x));
  22. }
  23. const headerStyle = computed(() => {
  24. return {
  25. position: contentsPositionStore.position,
  26. top: contentsPositionStore.top,
  27. left: contentsPositionStore.left,
  28. width: contentsPositionStore.width,
  29. paddingLeft: contentsPositionStore.paddingLeft,
  30. }
  31. });
  32. // const totalProfitValue = computed(() => {
  33. // const { profit = {}, preSolution = {}, subSolution = {}, gamesEvents = {} } = totalProfit.value;
  34. // const sol1 = formatSolution(preSolution, gamesEvents);
  35. // const sol2 = formatSolution(subSolution, gamesEvents);
  36. // const psInfo = [];
  37. // const outPreSol = [];
  38. // const outSubSol = [];
  39. // const solutions = [sol1, sol2].filter(item => item);
  40. // solutions.forEach((item, index) => {
  41. // const { sol: { ps_index }, cpr } = item;
  42. // const newCpr = [...cpr];
  43. // const ps_info = newCpr.splice(ps_index, 1);
  44. // psInfo.push({ ...ps_info[0] });
  45. // newCpr.forEach((c, i) => {
  46. // let side = '';
  47. // if (ps_index == 0) {
  48. // if (i == 0) {
  49. // side = "B"
  50. // }
  51. // else {
  52. // side = "M";
  53. // }
  54. // }
  55. // else if (ps_index == 1) {
  56. // if (i == 0) {
  57. // side = "A";
  58. // }
  59. // else {
  60. // side = "M";
  61. // }
  62. // }
  63. // else {
  64. // if (i == 0) {
  65. // side = "A";
  66. // }
  67. // else {
  68. // side = "B";
  69. // }
  70. // }
  71. // if (index == 0) {
  72. // outPreSol.push({ ...c, g: profitInfo[`gold${side}${index+1}`] });
  73. // }
  74. // else {
  75. // outSubSol.push({ ...c, g: profitInfo[`gold${side}${index+1}`] });
  76. // }
  77. // })
  78. // });
  79. // return { solutions, profit: profitInfo, psInfo, outPreSol, outSubSol };
  80. // });
  81. const solutionsList = computed(() => {
  82. const startTimestamp = selectedSolutions[0]?.timestamp ?? 0;
  83. return solutions.value.map(item => {
  84. const selected = selectedSolutions.findIndex(sol => sol.sid === item.sid) >= 0;
  85. const disabled = false && !selected && (item.info.ps.timestamp < startTimestamp + 1000 * 60 * 60 * 2);
  86. const { sol: { inner_base, win_average } } = item;
  87. const win_average_rate = fixFloat(win_average / inner_base * 100);
  88. const currentSol = { ...item.sol, win_average_rate };
  89. return { ...item, sol: currentSol, selected, disabled };
  90. });
  91. });
  92. const getSolutions = async () => {
  93. try {
  94. const win_min = minProfitRate.value * 100;
  95. const mk = marketType.value;
  96. const with_events = true;
  97. const data = await requestClient.get('/pstery/get_solutions', { params: { win_min, mk, with_events } });
  98. return data;
  99. }
  100. catch (error) {
  101. console.error('Failed to fetch solutions:', error);
  102. message.error('获取中单方案失败');
  103. return [];
  104. }
  105. }
  106. const calcTotalProfit = async () => {
  107. const sids = selectedSolutions.map(item => item.sid);
  108. try {
  109. const totalProfit = await requestClient.post('/pstery/calc_total_profit', [...sids]);
  110. return totalProfit;
  111. }
  112. catch (error) {
  113. console.error('Failed to calc total profit:', error);
  114. message.error('计算综合利润失败');
  115. return {};
  116. }
  117. }
  118. const parseIorKey = (iorKey) => {
  119. const [, type, accept, side, , ratioString] = iorKey.match(/^ior_(r|ou|m|wm|ot|os)(a?)(h|c|n)?(_([\d-]+))?$/);
  120. let ratio = 0;
  121. if (type === 'ot' || type === 'os') {
  122. ratio = ratioString;
  123. }
  124. else if (ratioString) {
  125. ratio = `${ratioString[0]}.${ratioString.slice(1)}` * (accept ? 1 : -1);
  126. }
  127. return { type, side, ratio };
  128. }
  129. const PS_IOR_KEYS = [
  130. ['0', 'ior_mh', 'ior_mn', 'ior_mc'],
  131. ['-1', 'ior_rh_15', 'ior_wmh_1', 'ior_rac_05'],
  132. ['-2', 'ior_rh_25', 'ior_wmh_2', 'ior_rac_15'],
  133. ['+1', 'ior_rah_05', 'ior_wmc_1', 'ior_rc_15'],
  134. ['+2', 'ior_rah_15', 'ior_wmc_2', 'ior_rc_25'],
  135. // ['0-1', 'ior_ot_0', 'ior_os_0-1', 'ior_ot_1'],
  136. // ['2-3', 'ior_ot_2', 'ior_os_2-3', 'ior_ot_3'],
  137. ['ot_1', '-', 'ior_ot_1', '-'],
  138. ['ot_2', '-', 'ior_ot_2', '-'],
  139. ['ot_3', '-', 'ior_ot_3', '-'],
  140. ['ot_4', '-', 'ior_ot_4', '-'],
  141. ['ot_5', '-', 'ior_ot_5', '-'],
  142. ['ot_6', '-', 'ior_ot_6', '-'],
  143. ['ot_7', '-', 'ior_ot_7', '-'],
  144. ];
  145. const formatPsEvents = (events) => {
  146. return PS_IOR_KEYS.map(([label, ...keys]) => {
  147. const match = keys.map(key => ({
  148. key,
  149. value: events[key]?.v ?? 0,
  150. origin: events[key]?.r
  151. }));
  152. return {
  153. label,
  154. match
  155. };
  156. })
  157. // .filter(item => item.match.every(entry => entry.value !== 0))
  158. .map(({label, match}) => [label, ...match]);
  159. }
  160. // const rivalIor = (ior) => {
  161. // const map = {
  162. // "ior_rh": "ior_rac",
  163. // "ior_rc": "ior_rah",
  164. // "ior_rac": "ior_rh",
  165. // "ior_rah": "ior_rc",
  166. // "ior_wmh": "ior_wmc",
  167. // "ior_wmc": "ior_wmh",
  168. // "ior_wmh_2": "ior_wmc_2",
  169. // "ior_wmc_2": "ior_wmh_2"
  170. // };
  171. // const iorInfos = ior.split('_');
  172. // const iorStart = iorInfos.slice(0, 2).join('_');
  173. // if (!map[iorStart]) {
  174. // return ior;
  175. // }
  176. // return `${map[iorStart]}_${iorInfos[2]}`;
  177. // }
  178. const formatEvents = (events, cprKeys) => {
  179. const eventsMap = {};
  180. Object.keys(events).forEach(key => {
  181. const { type, side, ratio } = parseIorKey(key);
  182. let ratioKey, index;
  183. if (type === 'r') {
  184. if (side === 'h') {
  185. ratioKey = ratio;
  186. index = 0;
  187. }
  188. else if (side === 'c') {
  189. ratioKey = -ratio;
  190. index = 2;
  191. }
  192. }
  193. else if (type === 'm') {
  194. ratioKey = 'm';
  195. if (side == 'h') {
  196. index = 0;
  197. }
  198. else if (side == 'c') {
  199. index = 2;
  200. }
  201. else {
  202. index = 1;
  203. }
  204. }
  205. else if (type === 'wm') {
  206. ratioKey = `wm_${Math.abs(ratio)}`;
  207. if (side === 'h') {
  208. index = 0;
  209. }
  210. else if (side === 'c') {
  211. index = 2;
  212. }
  213. }
  214. else if (type === 'ou') {
  215. ratioKey = `ou_${Math.abs(ratio)}`;
  216. if (side === 'c') {
  217. index = 0;
  218. }
  219. else if (side === 'h') {
  220. index = 2;
  221. }
  222. }
  223. // else if (type === 'os') {
  224. // ratioKey = ratio;
  225. // index = 1;
  226. // }
  227. else if (type === 'ot') {
  228. ratioKey = `ot_${ratio}`;
  229. index = 1;
  230. // switch (ratio) {
  231. // case '0':
  232. // ratioKey = '0-1';
  233. // index = 0;
  234. // break;
  235. // case '1':
  236. // ratioKey = '0-1';
  237. // index = 2;
  238. // break;
  239. // case '2':
  240. // ratioKey = '2-3';
  241. // index = 0;
  242. // break;
  243. // case '3':
  244. // ratioKey = '2-3';
  245. // index = 2;
  246. // break;
  247. // }
  248. }
  249. if (typeof (ratioKey) == 'number') {
  250. if (ratioKey > 0) {
  251. ratioKey = `+${ratioKey}`;
  252. }
  253. // else if (ratioKey === 0) {
  254. // ratioKey = '-0';
  255. // }
  256. else {
  257. ratioKey = `${ratioKey}`;
  258. }
  259. }
  260. if (!ratioKey) {
  261. return;
  262. }
  263. if (!eventsMap[ratioKey]) {
  264. eventsMap[ratioKey] = new Array(3).fill(undefined);
  265. }
  266. const value = events[key]?.v ?? 0;
  267. const origin = events[key]?.r;
  268. eventsMap[ratioKey][index] = { key, value, origin };
  269. });
  270. return Object.keys(eventsMap).sort((a, b) => a.localeCompare(b)).map(key => {
  271. return [key, ...eventsMap[key]];
  272. });
  273. }
  274. const formatSolution = (solution, eventsList) => {
  275. const { cpr, info } = solution;
  276. if (!cpr || !info) {
  277. return null;
  278. }
  279. const cprKeys = cpr.map(item => item.k);
  280. const psEvents = eventsList.ps?.[info.ps.eventId] ?? {};
  281. const obEvents = eventsList.ob?.[info.ob.eventId] ?? {};
  282. const hgEvents = eventsList.hg?.[info.hg.eventId] ?? {};
  283. info.ps.events = formatPsEvents(psEvents);
  284. info.ob.events = formatEvents(obEvents, cprKeys);
  285. info.hg.events = formatEvents(hgEvents, cprKeys);
  286. info.ps.dateTime = dayjs(info.ps.timestamp).format('YYYY-MM-DD HH:mm:ss');
  287. info.ob.dateTime = dayjs(info.ob.timestamp).format('YYYY-MM-DD HH:mm:ss');
  288. info.hg.dateTime = dayjs(info.hg.timestamp).format('YYYY-MM-DD HH:mm:ss');
  289. cpr.forEach(item => {
  290. const { k, p } = item;
  291. if (!info[p]['selected']) {
  292. info[p]['selected'] = [];
  293. }
  294. info[p]['selected'].push(k);
  295. });
  296. return solution;
  297. }
  298. const updateSolutions = async (showLoading=false) => {
  299. clearTimeout(loopTimer.value);
  300. if (showLoading && !updateLoaderHide.value) {
  301. updateLoaderHide.value = message.loading('数据加载中...', 0);
  302. }
  303. getSolutions()
  304. .then(({ solutions: solutionsList, gamesEvents: eventsList, mkCount: mkCountData }) => {
  305. solutions.value = solutionsList?.map(solution => formatSolution(solution, eventsList)) ?? [];
  306. markCount.value = mkCountData;
  307. })
  308. .catch(error => {
  309. console.error('Failed to update solutions:', error);
  310. message.error('获取中单方案失败');
  311. })
  312. .finally(() => {
  313. updateLoaderHide.value?.();
  314. updateLoaderHide.value = null;
  315. if (loopActive.value) {
  316. loopTimer.value = setTimeout(() => {
  317. updateSolutions();
  318. }, 1000 * 10);
  319. }
  320. });
  321. }
  322. const showTotalProfit = async () => {
  323. totalProfit.value = await calcTotalProfit();
  324. totalProfitVisible.value = true;
  325. const { profit } = totalProfit.value;
  326. console.log('profit', profit);
  327. };
  328. const closeTotalProfit = () => {
  329. totalProfitVisible.value = false;
  330. selectedSolutions.length = 0;
  331. totalProfit.value = {};
  332. };
  333. const toggleSolution = (sid, timestamp) => {
  334. const findIndex = selectedSolutions.findIndex(item => item.sid === sid);
  335. if (findIndex >= 0) {
  336. selectedSolutions.splice(findIndex, 1);
  337. }
  338. else if (selectedSolutions.length < 2) {
  339. selectedSolutions.push({ sid, timestamp });
  340. }
  341. else {
  342. selectedSolutions.splice(1, 1, { sid, timestamp });
  343. }
  344. if (selectedSolutions.length == 2) {
  345. showTotalProfit();
  346. }
  347. }
  348. const setLocalStorage = (key, value) => {
  349. localStorage.setItem(key, JSON.stringify(value));
  350. }
  351. const getLocalStorage = (key) => {
  352. const value = localStorage.getItem(key);
  353. return value ? JSON.parse(value) : null;
  354. }
  355. watch(minProfitRate, (newVal) => {
  356. clearTimeout(updateTimer.value);
  357. updateTimer.value = setTimeout(() => {
  358. setLocalStorage('minProfitRate', newVal);
  359. updateSolutions();
  360. }, 1000);
  361. });
  362. watch(marketType, (newVal) => {
  363. if (!updateLoaderHide.value) {
  364. updateLoaderHide.value = message.loading('数据更新中...', 0);
  365. }
  366. clearTimeout(updateTimer.value);
  367. updateTimer.value = setTimeout(() => {
  368. setLocalStorage('marketType', newVal);
  369. updateSolutions();
  370. }, 1000);
  371. });
  372. onMounted(() => {
  373. loopActive.value = true;
  374. const min_win_rate = getLocalStorage('minProfitRate');
  375. const mk = getLocalStorage('marketType');
  376. if (min_win_rate !== null) {
  377. minProfitRate.value = min_win_rate;
  378. }
  379. if (mk !== null) {
  380. marketType.value = mk;
  381. }
  382. setTimeout(() => {
  383. updateSolutions(true);
  384. }, 100);
  385. });
  386. onUnmounted(() => {
  387. loopActive.value = false;
  388. });
  389. </script>
  390. <template>
  391. <div class="solution-container">
  392. <div class="contents-header transition-all duration-200" :style="headerStyle">
  393. <div class="solution-options">
  394. <Form layout="inline" class="sol-opt-container">
  395. <Form.Item label="盘口类型" class="sol-opt-item">
  396. <RadioGroup v-model:value="marketType">
  397. <Radio :value="-1">全部({{ markCount.all ?? 0 }})</Radio>
  398. <Radio :value="2">滚球({{ markCount.rollball ?? 0 }})</Radio>
  399. <Radio :value="1">今日({{ markCount.today ?? 0 }})</Radio>
  400. <Radio :value="0">早盘({{ markCount.early ?? 0 }})</Radio>
  401. </RadioGroup>
  402. </Form.Item>
  403. <Form.Item label="最小利润率(%)" class="sol-opt-item">
  404. <InputNumber style="width: 60px" size="small" max="100" min="-100" step="0.1" placeholder="最小利润率(%)" v-model:value="minProfitRate"/>
  405. </Form.Item>
  406. </Form>
  407. </div>
  408. <div class="solution-header">
  409. <span>PS</span>
  410. <span>OB</span>
  411. <span>HG</span>
  412. <em>利润</em>
  413. </div>
  414. </div>
  415. <div class="solution-list">
  416. <div class="solution-item"
  417. v-for="{ sid, sol: { win_average_rate, win_profit_rate, cross_type }, info: { ps, ob, hg }, selected, disabled } in solutionsList" :key="sid"
  418. :class="{ 'selected': selected, 'disabled': disabled }">
  419. <MatchCard platform="ps" :eventId="ps.eventId" :leagueName="ps.leagueName" :teamHomeName="ps.teamHomeName"
  420. :teamAwayName="ps.teamAwayName" :dateTime="ps.dateTime" :eventInfo="ps.eventInfo" :events="ps.events ?? []"
  421. :matchNumStr="ps.matchNumStr" :selected="ps.selected ?? []" :stage="ps.stage" :retime="ps.retime" :score="ps.score" />
  422. <MatchCard platform="ob" :eventId="ob.eventId" :leagueName="ob.leagueName" :teamHomeName="ob.teamHomeName"
  423. :teamAwayName="ob.teamAwayName" :dateTime="ob.dateTime" :events="ob.events ?? []"
  424. :selected="ob.selected ?? []" :stage="ob.stage" :retime="ob.retime" :score="ob.score" />
  425. <MatchCard platform="hg" :eventId="hg.eventId" :leagueName="hg.leagueName" :teamHomeName="hg.teamHomeName"
  426. :teamAwayName="hg.teamAwayName" :dateTime="hg.dateTime" :events="hg.events ?? []"
  427. :selected="hg.selected ?? []" :stage="hg.stage" :retime="hg.retime" :score="hg.score" />
  428. <div class="solution-profit" @click="!disabled && toggleSolution(sid, ps.timestamp)">
  429. <p>{{ win_average_rate }}%</p>
  430. <p>{{ win_profit_rate }}%</p>
  431. <p>{{ cross_type }}</p>
  432. </div>
  433. </div>
  434. </div>
  435. <div class="list-empty" v-if="!solutionsList.length">暂无数据</div>
  436. <!-- <Drawer
  437. title="综合利润方案"
  438. placement="bottom"
  439. height="600"
  440. :visible="totalProfitVisible"
  441. @close="closeTotalProfit"
  442. >
  443. <div class="solution-total-profit" v-if="totalProfitValue.solutions.length">
  444. <div class="solution-item"
  445. v-for="{ sid, info: { ps, ob, hg } } in totalProfitValue.solutions" :key="sid">
  446. <MatchCard platform="ps" :eventId="ps.eventId" :leagueName="ps.leagueName" :teamHomeName="ps.teamHomeName"
  447. :teamAwayName="ps.teamAwayName" :dateTime="ps.dateTime" :eventInfo="ps.eventInfo" :events="ps.events ?? []"
  448. :matchNumStr="ps.matchNumStr" :selected="ps.selected ?? []" />
  449. <MatchCard platform="ob" :eventId="ob.eventId" :leagueName="ob.leagueName" :teamHomeName="ob.teamHomeName"
  450. :teamAwayName="ob.teamAwayName" :dateTime="ob.dateTime" :events="ob.events ?? []"
  451. :selected="ob.selected ?? []" />
  452. <MatchCard platform="hg" :eventId="hg.eventId" :leagueName="hg.leagueName" :teamHomeName="hg.teamHomeName"
  453. :teamAwayName="hg.teamAwayName" :dateTime="hg.dateTime" :events="hg.events ?? []"
  454. :selected="hg.selected ?? []" />
  455. </div>
  456. </div>
  457. <div class="profit-info">
  458. <table>
  459. <tr>
  460. <th></th>
  461. <td>PS</td>
  462. <td colspan="2">第一场</td>
  463. <td colspan="2">第二场</td>
  464. </tr>
  465. <tr>
  466. <th>赔率</th>
  467. <td>{{ totalProfitValue.psInfo[0]?.v }}: {{ totalProfitValue.psInfo[1]?.v }}</td>
  468. <td>{{ totalProfitValue.outPreSol[0]?.p }}: {{ totalProfitValue.outPreSol[0]?.v }}</td>
  469. <td>{{ totalProfitValue.outPreSol[1]?.p }}: {{ totalProfitValue.outPreSol[1]?.v }}</td>
  470. <td>{{ totalProfitValue.outSubSol[0]?.p }}: {{ totalProfitValue.outSubSol[0]?.v }}</td>
  471. <td>{{ totalProfitValue.outSubSol[1]?.p }}: {{ totalProfitValue.outSubSol[1]?.v }}</td>
  472. </tr>
  473. <tr>
  474. <th>下注</th>
  475. <td>{{ psOptions.bet }}</td>
  476. <td>{{ totalProfitValue.outPreSol[0]?.g }}</td>
  477. <td>{{ totalProfitValue.outPreSol[1]?.g }}</td>
  478. <td>{{ totalProfitValue.outSubSol[0]?.g }}</td>
  479. <td>{{ totalProfitValue.outSubSol[1]?.g }}</td>
  480. </tr>
  481. <tr>
  482. <th>利润</th>
  483. <td>{{ totalProfitValue.profit.win_ps }}</td>
  484. <td colspan="2">{{ totalProfitValue.profit.win_target }}</td>
  485. <td colspan="2">{{ totalProfitValue.profit.win_target }}</td>
  486. </tr>
  487. </table>
  488. </div>
  489. </Drawer> -->
  490. </div>
  491. </template>
  492. <style lang="scss" scoped>
  493. .contents-header {
  494. position: fixed;
  495. top: 0;
  496. left: 0;
  497. z-index: 201;
  498. width: 100%;
  499. border-bottom: 1px solid hsl(var(--border));
  500. background-color: hsl(var(--background));
  501. }
  502. .solution-options {
  503. position: relative;
  504. display: flex;
  505. align-items: center;
  506. padding: 5px 20px;
  507. border-bottom: 1px solid hsl(var(--border));
  508. }
  509. .sol-opt-container {
  510. flex-grow: 1;
  511. justify-content: space-between;
  512. }
  513. .sol-opt-item:last-child {
  514. margin-inline-end: 0 !important;
  515. }
  516. .solution-header {
  517. position: relative;
  518. display: flex;
  519. align-items: center;
  520. height: 30px;
  521. padding: 0 20px;
  522. span,
  523. em {
  524. display: block;
  525. text-align: center;
  526. }
  527. span {
  528. flex: 1;
  529. }
  530. em {
  531. width: 80px;
  532. font-style: normal;
  533. }
  534. }
  535. .solution-container {
  536. padding-top: 74px;
  537. }
  538. .solution-item {
  539. display: flex;
  540. .match-card {
  541. flex: 1;
  542. }
  543. }
  544. .solution-list {
  545. padding: 20px;
  546. overflow: hidden;
  547. .solution-item {
  548. border-radius: 10px;
  549. background-color: hsl(var(--card));
  550. &.selected {
  551. background-color: hsl(var(--primary) / 0.15);
  552. }
  553. &.disabled {
  554. opacity: 0.5;
  555. cursor: not-allowed;
  556. }
  557. &:not(:last-child) {
  558. margin-bottom: 20px;
  559. }
  560. .match-card {
  561. border-right: 1px solid hsl(var(--border));
  562. }
  563. .solution-profit {
  564. display: flex;
  565. flex-direction: column;
  566. width: 80px;
  567. align-items: center;
  568. justify-content: center;
  569. }
  570. }
  571. }
  572. .profit-info {
  573. table {
  574. width: 100%;
  575. border-collapse: collapse;
  576. border-spacing: 0;
  577. table-layout: fixed;
  578. th, td {
  579. height: 30px;
  580. border: 1px solid hsl(var(--border));
  581. text-align: center;
  582. }
  583. th {
  584. width: 64px;
  585. font-weight: normal;
  586. }
  587. }
  588. }
  589. .list-empty {
  590. text-align: center;
  591. padding: 10px;
  592. font-size: 18px;
  593. color: hsl(var(--foreground) / 0.7);
  594. }
  595. </style>