Browse Source

优化 Game 结构

flyzto 6 months ago
parent
commit
36bf13d0e6

+ 2 - 1
server/models/Relation.js

@@ -7,7 +7,8 @@ const gameSchema = new Schema({
   leagueName: { type: String, required: true },
   teamHomeName: { type: String, required: true },
   teamAwayName: { type: String, required: true },
-  timestamp: { type: Number, required: true }
+  timestamp: { type: Number, required: true },
+  matchNumStr: { type: String, required: false }
 }, { _id: false });
 
 const relSchema = new Schema({

+ 4 - 4
web/apps/web-antd/src/views/dashboard/analytics/analytics-visits-source.vue

@@ -24,10 +24,10 @@ onMounted(() => {
         avoidLabelOverlap: false,
         color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
         data: [
-          { name: '搜索引擎', value: 1048 },
-          { name: '直接访问', value: 735 },
-          { name: '邮件营销', value: 580 },
-          { name: '联盟广告', value: 484 },
+          // { name: '搜索引擎', value: 1048 },
+          // { name: '直接访问', value: 735 },
+          // { name: '邮件营销', value: 580 },
+          // { name: '联盟广告', value: 484 },
         ],
         emphasis: {
           label: {

+ 174 - 174
web/apps/web-antd/src/views/dashboard/workspace/index.vue

@@ -29,189 +29,189 @@ const userStore = useUserStore();
 // url 也可以是内部路由,在 navTo 方法中识别处理,进行内部跳转
 // 例如:url: /dashboard/workspace
 const projectItems: WorkbenchProjectItem[] = [
-  {
-    color: '',
-    content: '不要等待机会,而要创造机会。',
-    date: '2021-04-01',
-    group: '开源组',
-    icon: 'carbon:logo-github',
-    title: 'Github',
-    url: 'https://github.com',
-  },
-  {
-    color: '#3fb27f',
-    content: '现在的你决定将来的你。',
-    date: '2021-04-01',
-    group: '算法组',
-    icon: 'ion:logo-vue',
-    title: 'Vue',
-    url: 'https://vuejs.org',
-  },
-  {
-    color: '#e18525',
-    content: '没有什么才能比努力更重要。',
-    date: '2021-04-01',
-    group: '上班摸鱼',
-    icon: 'ion:logo-html5',
-    title: 'Html5',
-    url: 'https://developer.mozilla.org/zh-CN/docs/Web/HTML',
-  },
-  {
-    color: '#bf0c2c',
-    content: '热情和欲望可以突破一切难关。',
-    date: '2021-04-01',
-    group: 'UI',
-    icon: 'ion:logo-angular',
-    title: 'Angular',
-    url: 'https://angular.io',
-  },
-  {
-    color: '#00d8ff',
-    content: '健康的身体是实现目标的基石。',
-    date: '2021-04-01',
-    group: '技术牛',
-    icon: 'bx:bxl-react',
-    title: 'React',
-    url: 'https://reactjs.org',
-  },
-  {
-    color: '#EBD94E',
-    content: '路是走出来的,而不是空想出来的。',
-    date: '2021-04-01',
-    group: '架构组',
-    icon: 'ion:logo-javascript',
-    title: 'Js',
-    url: 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript',
-  },
+  // {
+  //   color: '',
+  //   content: '不要等待机会,而要创造机会。',
+  //   date: '2021-04-01',
+  //   group: '开源组',
+  //   icon: 'carbon:logo-github',
+  //   title: 'Github',
+  //   url: 'https://github.com',
+  // },
+  // {
+  //   color: '#3fb27f',
+  //   content: '现在的你决定将来的你。',
+  //   date: '2021-04-01',
+  //   group: '算法组',
+  //   icon: 'ion:logo-vue',
+  //   title: 'Vue',
+  //   url: 'https://vuejs.org',
+  // },
+  // {
+  //   color: '#e18525',
+  //   content: '没有什么才能比努力更重要。',
+  //   date: '2021-04-01',
+  //   group: '上班摸鱼',
+  //   icon: 'ion:logo-html5',
+  //   title: 'Html5',
+  //   url: 'https://developer.mozilla.org/zh-CN/docs/Web/HTML',
+  // },
+  // {
+  //   color: '#bf0c2c',
+  //   content: '热情和欲望可以突破一切难关。',
+  //   date: '2021-04-01',
+  //   group: 'UI',
+  //   icon: 'ion:logo-angular',
+  //   title: 'Angular',
+  //   url: 'https://angular.io',
+  // },
+  // {
+  //   color: '#00d8ff',
+  //   content: '健康的身体是实现目标的基石。',
+  //   date: '2021-04-01',
+  //   group: '技术牛',
+  //   icon: 'bx:bxl-react',
+  //   title: 'React',
+  //   url: 'https://reactjs.org',
+  // },
+  // {
+  //   color: '#EBD94E',
+  //   content: '路是走出来的,而不是空想出来的。',
+  //   date: '2021-04-01',
+  //   group: '架构组',
+  //   icon: 'ion:logo-javascript',
+  //   title: 'Js',
+  //   url: 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript',
+  // },
 ];
 
 // 同样,这里的 url 也可以使用以 http 开头的外部链接
 const quickNavItems: WorkbenchQuickNavItem[] = [
-  {
-    color: '#1fdaca',
-    icon: 'ion:home-outline',
-    title: '首页',
-    url: '/',
-  },
-  {
-    color: '#bf0c2c',
-    icon: 'ion:grid-outline',
-    title: '仪表盘',
-    url: '/dashboard',
-  },
-  {
-    color: '#e18525',
-    icon: 'ion:layers-outline',
-    title: '组件',
-    url: '/demos/features/icons',
-  },
-  {
-    color: '#3fb27f',
-    icon: 'ion:settings-outline',
-    title: '系统管理',
-    url: '/demos/features/login-expired', // 这里的 URL 是示例,实际项目中需要根据实际情况进行调整
-  },
-  {
-    color: '#4daf1bc9',
-    icon: 'ion:key-outline',
-    title: '权限管理',
-    url: '/demos/access/page-control',
-  },
-  {
-    color: '#00d8ff',
-    icon: 'ion:bar-chart-outline',
-    title: '图表',
-    url: '/analytics',
-  },
+  // {
+  //   color: '#1fdaca',
+  //   icon: 'ion:home-outline',
+  //   title: '首页',
+  //   url: '/',
+  // },
+  // {
+  //   color: '#bf0c2c',
+  //   icon: 'ion:grid-outline',
+  //   title: '仪表盘',
+  //   url: '/dashboard',
+  // },
+  // {
+  //   color: '#e18525',
+  //   icon: 'ion:layers-outline',
+  //   title: '组件',
+  //   url: '/demos/features/icons',
+  // },
+  // {
+  //   color: '#3fb27f',
+  //   icon: 'ion:settings-outline',
+  //   title: '系统管理',
+  //   url: '/demos/features/login-expired', // 这里的 URL 是示例,实际项目中需要根据实际情况进行调整
+  // },
+  // {
+  //   color: '#4daf1bc9',
+  //   icon: 'ion:key-outline',
+  //   title: '权限管理',
+  //   url: '/demos/access/page-control',
+  // },
+  // {
+  //   color: '#00d8ff',
+  //   icon: 'ion:bar-chart-outline',
+  //   title: '图表',
+  //   url: '/analytics',
+  // },
 ];
 
 const todoItems = ref<WorkbenchTodoItem[]>([
-  {
-    completed: false,
-    content: `审查最近提交到Git仓库的前端代码,确保代码质量和规范。`,
-    date: '2024-07-30 11:00:00',
-    title: '审查前端代码提交',
-  },
-  {
-    completed: true,
-    content: `检查并优化系统性能,降低CPU使用率。`,
-    date: '2024-07-30 11:00:00',
-    title: '系统性能优化',
-  },
-  {
-    completed: false,
-    content: `进行系统安全检查,确保没有安全漏洞或未授权的访问。 `,
-    date: '2024-07-30 11:00:00',
-    title: '安全检查',
-  },
-  {
-    completed: false,
-    content: `更新项目中的所有npm依赖包,确保使用最新版本。`,
-    date: '2024-07-30 11:00:00',
-    title: '更新项目依赖',
-  },
-  {
-    completed: false,
-    content: `修复用户报告的页面UI显示问题,确保在不同浏览器中显示一致。 `,
-    date: '2024-07-30 11:00:00',
-    title: '修复UI显示问题',
-  },
+  // {
+  //   completed: false,
+  //   content: `审查最近提交到Git仓库的前端代码,确保代码质量和规范。`,
+  //   date: '2024-07-30 11:00:00',
+  //   title: '审查前端代码提交',
+  // },
+  // {
+  //   completed: true,
+  //   content: `检查并优化系统性能,降低CPU使用率。`,
+  //   date: '2024-07-30 11:00:00',
+  //   title: '系统性能优化',
+  // },
+  // {
+  //   completed: false,
+  //   content: `进行系统安全检查,确保没有安全漏洞或未授权的访问。 `,
+  //   date: '2024-07-30 11:00:00',
+  //   title: '安全检查',
+  // },
+  // {
+  //   completed: false,
+  //   content: `更新项目中的所有npm依赖包,确保使用最新版本。`,
+  //   date: '2024-07-30 11:00:00',
+  //   title: '更新项目依赖',
+  // },
+  // {
+  //   completed: false,
+  //   content: `修复用户报告的页面UI显示问题,确保在不同浏览器中显示一致。 `,
+  //   date: '2024-07-30 11:00:00',
+  //   title: '修复UI显示问题',
+  // },
 ]);
 const trendItems: WorkbenchTrendItem[] = [
-  {
-    avatar: 'svg:avatar-1',
-    content: `在 <a>开源组</a> 创建了项目 <a>Vue</a>`,
-    date: '刚刚',
-    title: '威廉',
-  },
-  {
-    avatar: 'svg:avatar-2',
-    content: `关注了 <a>威廉</a> `,
-    date: '1个小时前',
-    title: '艾文',
-  },
-  {
-    avatar: 'svg:avatar-3',
-    content: `发布了 <a>个人动态</a> `,
-    date: '1天前',
-    title: '克里斯',
-  },
-  {
-    avatar: 'svg:avatar-4',
-    content: `发表文章 <a>如何编写一个Vite插件</a> `,
-    date: '2天前',
-    title: 'Vben',
-  },
-  {
-    avatar: 'svg:avatar-1',
-    content: `回复了 <a>杰克</a> 的问题 <a>如何进行项目优化?</a>`,
-    date: '3天前',
-    title: '皮特',
-  },
-  {
-    avatar: 'svg:avatar-2',
-    content: `关闭了问题 <a>如何运行项目</a> `,
-    date: '1周前',
-    title: '杰克',
-  },
-  {
-    avatar: 'svg:avatar-3',
-    content: `发布了 <a>个人动态</a> `,
-    date: '1周前',
-    title: '威廉',
-  },
-  {
-    avatar: 'svg:avatar-4',
-    content: `推送了代码到 <a>Github</a>`,
-    date: '2021-04-01 20:00',
-    title: '威廉',
-  },
-  {
-    avatar: 'svg:avatar-4',
-    content: `发表文章 <a>如何编写使用 Admin Vben</a> `,
-    date: '2021-03-01 20:00',
-    title: 'Vben',
-  },
+  // {
+  //   avatar: 'svg:avatar-1',
+  //   content: `在 <a>开源组</a> 创建了项目 <a>Vue</a>`,
+  //   date: '刚刚',
+  //   title: '威廉',
+  // },
+  // {
+  //   avatar: 'svg:avatar-2',
+  //   content: `关注了 <a>威廉</a> `,
+  //   date: '1个小时前',
+  //   title: '艾文',
+  // },
+  // {
+  //   avatar: 'svg:avatar-3',
+  //   content: `发布了 <a>个人动态</a> `,
+  //   date: '1天前',
+  //   title: '克里斯',
+  // },
+  // {
+  //   avatar: 'svg:avatar-4',
+  //   content: `发表文章 <a>如何编写一个Vite插件</a> `,
+  //   date: '2天前',
+  //   title: 'Vben',
+  // },
+  // {
+  //   avatar: 'svg:avatar-1',
+  //   content: `回复了 <a>杰克</a> 的问题 <a>如何进行项目优化?</a>`,
+  //   date: '3天前',
+  //   title: '皮特',
+  // },
+  // {
+  //   avatar: 'svg:avatar-2',
+  //   content: `关闭了问题 <a>如何运行项目</a> `,
+  //   date: '1周前',
+  //   title: '杰克',
+  // },
+  // {
+  //   avatar: 'svg:avatar-3',
+  //   content: `发布了 <a>个人动态</a> `,
+  //   date: '1周前',
+  //   title: '威廉',
+  // },
+  // {
+  //   avatar: 'svg:avatar-4',
+  //   content: `推送了代码到 <a>Github</a>`,
+  //   date: '2021-04-01 20:00',
+  //   title: '威廉',
+  // },
+  // {
+  //   avatar: 'svg:avatar-4',
+  //   content: `发表文章 <a>如何编写使用 Admin Vben</a> `,
+  //   date: '2021-03-01 20:00',
+  //   title: 'Vben',
+  // },
 ];
 
 const router = useRouter();

+ 42 - 1
web/apps/web-antd/src/views/demos/antd/index.vue

@@ -1,7 +1,20 @@
 <script lang="ts" setup>
 import { Page } from '@vben/common-ui';
+import { Button, Card, message, notification, Space, Form, Input, Drawer } from 'ant-design-vue';
+import { ref } from 'vue';
 
-import { Button, Card, message, notification, Space } from 'ant-design-vue';
+import { useContentsPositionStore } from '@vben/stores';
+const contentsPositionStore = useContentsPositionStore();
+
+const drawerVisible = ref(false);
+
+const showDrawer = () => {
+  drawerVisible.value = true;
+};
+
+const closeDrawer = () => {
+  drawerVisible.value = false;
+};
 
 type NotificationType = 'error' | 'info' | 'success' | 'warning';
 
@@ -37,6 +50,16 @@ function notify(type: NotificationType) {
     description="支持多语言,主题功能集成切换等"
     title="Ant Design Vue组件使用演示"
   >
+    <Card class="mb-5" title="设置">
+      <Form layout="inline">
+        <Form.Item label="JC 投注">
+          <Input placeholder="请输入JC投注" />
+        </Form.Item>
+        <Form.Item label="JC 返点">
+          <Input placeholder="请输入JC返点" />
+        </Form.Item>
+      </Form>
+    </Card>
     <Card class="mb-5" title="按钮">
       <Space>
         <Button>Default</Button>
@@ -62,5 +85,23 @@ function notify(type: NotificationType) {
         <Button @click="notify('success')"> 成功 </Button>
       </Space>
     </Card>
+
+    <Card class="mb-5" title="底部抽屉">
+      <Space>
+        <Button type="primary" @click="showDrawer">打开底部抽屉</Button>
+      </Space>
+    </Card>
+
+    <Drawer
+      title="底部抽屉"
+      placement="bottom"
+      :visible="drawerVisible"
+      @close="closeDrawer"
+    >
+      <div style="padding: 20px">
+        <h3>抽屉内容</h3>
+        <p>这里可以放置任何内容</p>
+      </div>
+    </Drawer>
   </Page>
 </template>

+ 6 - 0
web/apps/web-antd/src/views/match/components/match_card.vue

@@ -92,6 +92,9 @@ defineProps({
   display: flex;
   align-items: center;
   font-size: 14px;
+  .disabled & {
+    display: none;
+  }
   span, em {
     display: block;
   }
@@ -121,6 +124,9 @@ defineProps({
 }
 .events-list {
   margin-top: 10px;
+  .disabled & {
+    display: none;
+  }
   table {
     width: 100%;
     border-collapse: collapse;

+ 167 - 51
web/apps/web-antd/src/views/match/solutions/index.vue

@@ -1,22 +1,79 @@
 <script setup>
 import { Page } from '@vben/common-ui';
 import { requestClient } from '#/api/request';
-import { Button, message } from 'ant-design-vue';
+import { Button, message, Form, InputNumber, Drawer } from 'ant-design-vue';
 import { ref, reactive, computed, onMounted, onUnmounted, useTemplateRef } 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 jcOptions = reactive({
+  bet: 10000,
+  rebate: 12,
+});
+
+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 = {};
+  const jcScale = jcOptions.bet / totalProfit.value.jc_base;
+  const jcRebate = jcOptions.bet * jcOptions.rebate / 100;
+
+  Object.keys(totalProfit.value).forEach(key => {
+    if (key == 'win_diff') {
+      return;
+    }
+    if (key.startsWith('gold')) {
+      profit[key] = fixFloat(totalProfit.value[key] * jcScale);
+    }
+    else if (key.startsWith('win_')) {
+      profit[key] = fixFloat(totalProfit.value[key] * jcScale + jcRebate);
+    }
+  });
+  return profit;
+});
 
 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 = !selected && (item.info.jc.timestamp < startTimestamp + 1000 * 60 * 60 * 2);
+    const currentSol = { ...item.sol };
+
+    const jcScale = jcOptions.bet / currentSol.jc_base;
+    const jcRebate = jcOptions.bet * jcOptions.rebate / 100;
+
+    Object.keys(currentSol).forEach(key => {
+      if (key.startsWith('gold_')) {
+        currentSol[key] = fixFloat(currentSol[key] * jcScale);
+      }
+      else if (key.startsWith('win_')) {
+        currentSol[key] = fixFloat(currentSol[key] * jcScale + jcRebate);
+      }
+    });
     // console.log(new Date(item.info.jc.timestamp), new Date(startTimestamp), disabled);
-    return { ...item, selected, disabled };
+    return { ...item, sol: currentSol, selected, disabled };
   })
   // .filter(item => !item.disabled);
 });
@@ -100,7 +157,7 @@ const formatJcEvents = (events) => {
     eventsMap[ratioKey][index] = { key, value };
 
   });
-  return Object.keys(eventsMap).sort((a, b) =>  b.localeCompare(a)).map(key => {
+  return Object.keys(eventsMap).sort((a, b) => b.localeCompare(a)).map(key => {
     return [key, ...eventsMap[key]];
   });
 }
@@ -155,10 +212,10 @@ const formatEvents = (events, cprKeys) => {
         values[0] = { key, value: events[key] ?? 0 };
       }
       else if (side === 'c') {
-        values[2] = { key, value: events[key] ?? 0 }  ;
+        values[2] = { key, value: events[key] ?? 0 };
       }
     }
-    if (typeof(ratioKey) == 'number') {
+    if (typeof (ratioKey) == 'number') {
       if (ratioKey > 0) {
         ratioKey = `+${ratioKey}`;
       }
@@ -171,14 +228,14 @@ const formatEvents = (events, cprKeys) => {
     }
     eventsMap[ratioKey] = values;
   });
-  return Object.keys(eventsMap).sort((a, b) =>  a.localeCompare(b)).map(key => {
+  return Object.keys(eventsMap).sort((a, b) => a.localeCompare(b)).map(key => {
     return [key, ...eventsMap[key]];
   });
 }
 
 const updateSolutions = async () => {
   const { solutions: solutionsList, gamesEvents: eventsList } = await getSolutions();
-  solutions.value = solutionsList.map(item => {
+  solutions.value = solutionsList?.map(item => {
     const { cpr, info, sol: { cross_type, jc_index, gold_side_a, gold_side_b, gold_side_m, win_side_a, win_side_b, win_side_m, win_average } } = item;
     const cprKeys = cpr.map(item => item.k);
 
@@ -199,9 +256,20 @@ const updateSolutions = async () => {
     });
 
     return item;
-  });
+  }) ?? [];
 }
 
+const showTotalProfit = async () => {
+  totalProfit.value = await calcTotalProfit();
+  totalProfitVisible.value = true;
+};
+
+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) {
@@ -214,7 +282,7 @@ const toggleSolution = (sid, timestamp) => {
     selectedSolutions.splice(1, 1, { sid, timestamp });
   }
   if (selectedSolutions.length == 2) {
-    calcTotalProfit();
+    showTotalProfit();
   }
 }
 
@@ -228,7 +296,6 @@ onMounted(() => {
 });
 
 onUnmounted(() => {
-  console.log('unmounted');
   clearInterval(updateTimer);
 });
 
@@ -237,95 +304,137 @@ onUnmounted(() => {
 
 <template>
   <div class="solution-container">
-    <div class="solution-header">
-      <span>JC</span>
-      <span>PS</span>
-      <span>OB</span>
-      <em>利润</em>
+
+    <div class="contents-header transition-all duration-200" :style="headerStyle">
+      <div class="solution-options">
+        <Form layout="inline">
+          <Form.Item label="JC 投注">
+            <InputNumber size="small" placeholder="JC投注" min="1000" v-model:value="jcOptions.bet" />
+          </Form.Item>
+          <Form.Item label="JC 返点">
+            <InputNumber size="small" placeholder="JC返点" min="0" v-model:value="jcOptions.rebate" />
+          </Form.Item>
+        </Form>
+      </div>
+      <div class="solution-header">
+        <span>JC</span>
+        <span>PS</span>
+        <span>OB</span>
+        <em>利润</em>
+      </div>
     </div>
+
     <div class="solution-list">
       <div class="solution-item"
-        v-for="{ sid, sol: {win_average}, info: {jc, ps, ob}, selected, disabled } in solutionsList" :key="sid"
-        :class="{'selected': selected, 'disabled': disabled}"
-      >
-        <MatchCard platform="jc"
-          :eventId="jc.eventId"
-          :leagueName="jc.leagueName"
-          :teamHomeName="jc.teamHomeName"
-          :teamAwayName="jc.teamAwayName"
-          :dateTime="jc.dateTime"
-          :eventInfo="jc.eventInfo"
-          :events="jc.events ?? []"
-          :selected="jc.selected ?? []"/>
-
-        <MatchCard platform="ps"
-          :eventId="ps.eventId"
-          :leagueName="ps.leagueName"
-          :teamHomeName="ps.teamHomeName"
-          :teamAwayName="ps.teamAwayName"
-          :dateTime="ps.dateTime"
-          :events="ps.events ?? []"
-          :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 ?? []"/>
+        v-for="{ sid, sol: { win_average }, info: { jc, ps, ob }, selected, disabled } in solutionsList" :key="sid"
+        :class="{ 'selected': selected, 'disabled': disabled }">
+        <MatchCard platform="jc" :eventId="jc.eventId" :leagueName="jc.leagueName" :teamHomeName="jc.teamHomeName"
+          :teamAwayName="jc.teamAwayName" :dateTime="jc.dateTime" :eventInfo="jc.eventInfo" :events="jc.events ?? []"
+          :selected="jc.selected ?? []" />
+
+        <MatchCard platform="ps" :eventId="ps.eventId" :leagueName="ps.leagueName" :teamHomeName="ps.teamHomeName"
+          :teamAwayName="ps.teamAwayName" :dateTime="ps.dateTime" :events="ps.events ?? []"
+          :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 ?? []" />
 
         <div class="solution-profit" @click="!disabled && toggleSolution(sid, jc.timestamp)">{{ win_average }}</div>
       </div>
     </div>
+    <div class="list-empty" v-if="!solutionsList.length">暂无数据</div>
+
+    <Drawer
+      title="综合利润方案"
+      placement="bottom"
+      :visible="totalProfitVisible"
+      @close="closeTotalProfit"
+    >
+      <div class="solution-total-profit">
+        <ul style="text-align: center;">
+          <li v-for="key in Object.keys(totalProfitValue)" :key="key">
+            <span>{{ key }}:</span>
+            <span>{{ totalProfitValue[key] }}</span>
+          </li>
+        </ul>
+      </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;
-  z-index: 11;
   display: flex;
   align-items: center;
-  height: 40px;
+  height: 30px;
   padding: 0 20px;
-  background-color: hsl(var(--background));
-  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
-  span, em {
+  span,
+  em {
     display: block;
     text-align: center;
   }
+
   span {
     flex: 1;
   }
+
   em {
     width: 80px;
     font-style: normal;
   }
 }
+
+.solution-container {
+  padding-top: 74px;
+}
+
 .solution-list {
   padding: 20px;
   overflow: hidden;
 }
+
 .solution-item {
   display: flex;
   border-radius: 10px;
   background-color: hsl(var(--card));
+
   &.selected {
     background-color: hsl(var(--primary) / 0.15);
   }
+
   &.disabled {
-    filter: blur(15px);
+    opacity: 0.5;
     cursor: not-allowed;
   }
+
   &:not(:last-child) {
     margin-bottom: 20px;
   }
+
   .match-card {
     flex: 1;
     border-right: 1px solid hsl(var(--border));
   }
+
   .solution-profit {
     display: flex;
     width: 80px;
@@ -333,4 +442,11 @@ onUnmounted(() => {
     justify-content: center;
   }
 }
+
+.list-empty {
+  text-align: center;
+  padding: 10px;
+  font-size: 18px;
+  color: hsl(var(--foreground) / 0.7);
+}
 </style>

+ 12 - 2
web/packages/@core/ui-kit/layout-ui/src/vben-layout.vue

@@ -16,6 +16,9 @@ import { ELEMENT_ID_MAIN_CONTENT } from '@vben-core/shared/constants';
 
 import { useMouse, useScroll, useThrottleFn } from '@vueuse/core';
 
+import { useContentsPositionStore } from '../../../../stores';
+const contentsPositionStore = useContentsPositionStore();
+
 import {
   LayoutContent,
   LayoutFooter,
@@ -286,6 +289,8 @@ const tabbarStyle = computed((): CSSProperties => {
     width = '100%';
   }
 
+  contentsPositionStore.setPaddingLeft(`${marginLeft}px`);
+
   return {
     marginLeft: `${marginLeft}px`,
     width,
@@ -316,7 +321,7 @@ const headerZIndex = computed(() => {
 
 const headerWrapperStyle = computed((): CSSProperties => {
   const fixed = headerFixed.value;
-  return {
+  const wrapperStyle: CSSProperties = {
     height: isFullContent.value ? '0' : `${headerWrapperHeight.value}px`,
     left: isMixedNav.value ? 0 : mainStyle.value.sidebarAndExtraWidth,
     position: fixed ? 'fixed' : 'static',
@@ -326,7 +331,12 @@ const headerWrapperStyle = computed((): CSSProperties => {
         : 0,
     width: mainStyle.value.width,
     'z-index': headerZIndex.value,
-  };
+  }
+  contentsPositionStore.setPosition(wrapperStyle.position);
+  contentsPositionStore.setTop(wrapperStyle.height);
+  contentsPositionStore.setLeft(wrapperStyle.left);
+  contentsPositionStore.setWidth(wrapperStyle.width);
+  return wrapperStyle;
 });
 
 /**

+ 62 - 0
web/packages/stores/src/modules/contents-pos.ts

@@ -0,0 +1,62 @@
+import { acceptHMRUpdate, defineStore } from 'pinia';
+
+interface ContentsPosition {
+  [key: string]: any;
+  /**
+   * 定位方式
+   */
+  position: 'static' | 'fixed' | 'absolute' | 'relative' | 'sticky';
+  /**
+   * 顶部位置
+   */
+  top: string;
+  /**
+   * 左侧位置
+   */
+  left: string;
+  /**
+   * 宽
+   */
+  width: string;
+  /**
+   * 左边距
+   */
+  paddingLeft: string;
+
+}
+
+/**
+ * @zh_CN 用户信息相关
+ */
+export const useContentsPositionStore = defineStore('contents-position', {
+  actions: {
+    setPosition(position: 'static' | 'fixed' | 'absolute' | 'relative' | 'sticky') {
+      this.position = position;
+    },
+    setTop(top: string) {
+      this.top = top;
+    },
+    setLeft(left: string) {
+      this.left = left;
+    },
+    setWidth(width: string) {
+      this.width = width;
+    },
+    setPaddingLeft(paddingLeft: string) {
+      this.paddingLeft = paddingLeft;
+    },
+  },
+  state: (): ContentsPosition => ({
+    position: 'static',
+    top: 'unset',
+    left: 'unset',
+    width: '100%',
+    paddingLeft: '0',
+  }),
+});
+
+// 解决热更新问题
+const hot = import.meta.hot;
+if (hot) {
+  hot.accept(acceptHMRUpdate(useContentsPositionStore, hot));
+}

+ 1 - 0
web/packages/stores/src/modules/index.ts

@@ -1,3 +1,4 @@
 export * from './access';
 export * from './tabbar';
 export * from './user';
+export * from './contents-pos';