给 CS2 Major 竞猜做了个在线抄作业网站

背景:为什么要做这个项目?

Counter-Strike 2(反恐精英 2,也称 CS2) 是由 Valve 开发并发行的一款在线射击游戏,是 Counter-Strike: Global Offensive(反恐精英:全球攻势,也称 CSGO)的续作。CS2 采用全新的起源 2(Source2)引擎开发,不仅推出了新式烟雾弹和子刷新频率构架等更新,游戏的画质也得到了大幅升级、地图也翻新制作,是 Steam 上最热门的游戏

Major 是 CS2 中最重要的赛事,一般每年举办两次。在 Major 期间,游戏内会推出相应的活动,其中就有竞猜系统,玩家可以通过这个竞猜系统来预测比赛结果,预测成功可以完成竞猜任务,完成一定数量的任务可以持续升级赛事纪念币,并获得丰厚奖励。

Major 竞猜凭借着它 3000 万+ 月活的庞大玩家群体基础,和它本身的趣味性和娱乐性,吸引了大量玩家关注,其中就包括我,虽然上半年的奥斯汀 Major 我绞尽脑汁只拿了个银币,都怪菊花队,这届奥斯汀 Major 能拿钻石币的都是什么神人。

在近几次的 Major 期间,有多位 UP 主搜集了大量主播竞猜情况并将其汇总成图片,在各大社交媒体、群聊间被转发传阅参考。今年的 Major 在布达佩斯(Budapest,匈牙利的首都)举办,依然有不少 UP 主在赛前搜集主播的竞猜作业(比赛竞猜情况,俗称「作业」,参考他人的竞猜作业,也称「抄作业」)并制作了汇总图片供大家参考。例如来自 B 站的 @原劫色@三米七七 等 UP 主,他们发布了多篇相关视频,汇总了大量主播竞猜作业:

然而,单纯依靠图片来追踪竞猜进度并不方便,需要自己一个个人眼对照核对,非常的反人类。比如上图的布达佩斯 Major 第一阶段作业汇总,在比赛进行到一半时,通过人眼核对可以知道大家的 3-0 和 0-3 都炸的差不多了,极少主播的竞猜还能生还,真惨吧,但具体是谁还需要一个个去对照,效率极低。

这不,需求来了!作为一个搬了两三年砖的前端开发,为了更高效地参考和跟踪主播竞猜进度,我决定动手写一个在线抄作业项目,将这些主播竞猜作业数据结构化,并提供便捷的查询和排名等功能。

技术选型:为什么选 Next.js 16?

做这个项目的时候,我的核心诉求其实很明确:

  1. 要快 - Major 比赛周期很紧凑,我需要在比赛开始前就上线
  2. 数据量小 - 就十几二十个主播的竞猜数据,根本不需要后端数据库
  3. 要好看 - 毕竟是给玩家用的,UI 得过得去
  4. 要能快速迭代 - 比赛期间可能需要频繁更新数据和功能

基于这些需求,我选择了以下技术栈:

Next.js 16 (Canary) + App Router

最开始其实纠结过要不要用 Next.js,因为这个项目完全不需要 SSR,纯静态部署就行。但后来发现 Next.js 的几个特性太香了:

  • App Router 的文件路由 :不用手写路由配置,app/predictors/page.tsx 直接就是 /predictors 路由
  • 静态生成next build 直接输出静态 HTML,部署到 Vercel 零配置
  • React Server Components:大部分页面都是 RSC,bundle size 小很多
  • Turbopack :开发时编译飞快,pnpm dev --turbopack 启动不到 1 秒

版本选了 16.1.0-canary.13,主要是想试试最新的 React 19 和 Turbopack。虽然是 canary 版本,但实际用下来很稳定,没遇到什么坑。

React 19 - 最新特性

React 19 带来了一些不错的优化:

  • 自动批处理优化:连续的 setState 会自动合并
  • use() Hook :可以在组件中直接 use(promise),不过这个项目还没用到
  • 优化的 hydration:虽然我这个项目是纯静态的,但未来可能会加一些交互功能

TypeScript - 严格模式

这个必须的,竞猜数据结构比较复杂(瑞士轮、淘汰赛、不同阶段),没有类型约束很容易写出 bug。

我在 types/index.ts 里定义了完整的类型系统:

typescript 复制代码
// 瑞士轮结果包含进行中的战绩和最终结果
export interface SwissResult {
  // 进行中的战绩
  '1-0': string[]
  '2-1': string[]
  '2-2': string[]
  // ... 其他战绩

  // 最终结果
  '3-0': string[]
  '3-1': string[]
  '3-2': string[]
  // ... 其他结果
}

// 阶段类型
export type SwissStageType = 'stage-1' | 'stage-2' | 'stage-3'
export type FinalStageType = '8-to-4' | '4-to-2' | '2-to-1'

严格的类型定义让我在写业务逻辑时几乎不用担心类型错误,IDE 的智能提示也非常准确。

Tailwind CSS v4 - 新一代样式方案

这个选择有点激进,Tailwind v4 现在还处于早期阶段,但我看了官方文档后觉得值得一试。

v4 最大的变化是配置方式改了

  • 不再需要 tailwind.config.ts:配置全部写在 CSS 文件里
  • 使用 CSS 变量定义主题:完美支持明暗主题切换
  • PostCSS 插件简化 :只需要 @tailwindcss/postcss

我的主题配置全部在 app/globals.css 里:

css 复制代码
@import 'tailwindcss';

:root {
  /* 明亮主题变量 */
  --foreground: #18181b;
  --color-surface-0: #ffffff;
  --color-primary-500: #ff6b35;
  /* ... 其他变量 */
}

.dark {
  /* 暗色主题变量 */
  --foreground: #fafafa;
  --color-surface-0: #0a0a0a;
  /* ... 其他变量 */
}

然后定义一些语义化的工具类:

css 复制代码
.text-primary {
  color: var(--foreground);
}

.hover-text-primary:hover {
  color: var(--foreground);
}

这样写组件的时候就不用到处写 text-zinc-900 dark:text-white,直接用 text-primary 就能自适应主题,代码简洁很多。

架构设计:纯静态数据驱动

这个项目的架构其实很简单,核心理念就是 Data as Code

数据层设计

所有数据都存在两个 JSON 文件里:

bash 复制代码
data/
├── events.json       # 赛事数据(队伍、赛程、结果)
└── predictions.json  # 竞猜数据(各主播的竞猜)

events.json 的结构:

json 复制代码
{
  "id": "budapest-2025",
  "name": "布达佩斯 2025 Major",
  "teams": [...],
  "stage-1": {
    "name": "第一阶段",
    "teams": ["Liquid", "Spirit", ...],
    "result": {
      "3-0": ["Spirit"],
      "3-1": ["Liquid", "G2", "Vitality"],
      // 进行中的战绩
      "2-1": ["FaZe", "MOUZ"],
      "1-2": ["NAVI", "Heroic"]
    }
  }
}

predictions.json 的结构:

json 复制代码
{
  "id": "budapest-2025",
  "predictions": [
    {
      "id": "s1mple",
      "name": "s1mple",
      "platform": "twitch",
      "stage-1": {
        "3-0": ["Spirit", "Liquid"],
        "3-1-or-3-2": ["G2", "Vitality", "FaZe", "MOUZ", "NAVI", "Heroic"],
        "0-3": ["BIG", "9z"]
      }
    }
  ]
}

核心业务逻辑

所有计算逻辑都在 lib/data.ts 里,这个文件有 800+ 行,是整个项目最复杂的部分。主要包含:

1. 竞猜准确度计算

瑞士轮的规则比较复杂:

  • 竞猜 3-0 的队伍,必须实际 3-0 才算对(不能是 3-1 或 3-2)
  • 竞猜 3-1-or-3-2 的队伍,实际 3-13-2 都算对
  • 竞猜 0-3 的队伍,必须实际 0-3 才算对

这里有个细节:比赛进行中时,有些队伍的最终结果还没出来(比如现在战绩是 2-1),但我们可以提前判断某些竞猜已经"死了"。比如你竞猜某队 3-0,但它现在已经 2-1 了(输了一场),那这个竞猜就不可能对了。

typescript 复制代码
export function isPredictionPossible(
  teamName: string,
  predictionBucket: '3-0' | '3-1-or-3-2' | '0-3',
  result: SwissResult | undefined,
): boolean {
  if (!result) return true

  // 检查队伍当前战绩
  for (const record of progressRecords) {
    if (result[record]?.includes(teamName)) {
      const [wins, losses] = record.split('-').map(Number)

      // 3-0 竞猜:有任何失败就不可能
      if (predictionBucket === '3-0') {
        return losses === 0
      }

      // 0-3 竞猜:有任何胜利就不可能
      if (predictionBucket === '0-3') {
        return wins === 0
      }

      // 3-1-or-3-2:只要还在比赛就有可能
      return true
    }
  }

  return true
}

这个函数让我们可以在比赛进行中就实时显示"已失败"的竞猜数量,体验好很多。

2. 阶段通过判定

每个阶段都有通过标准:

  • 瑞士轮:10 个竞猜中 5 个对就通过
  • 八进四:4 个竞猜中 2 个对就通过
  • 半决赛:2 个竞猜中 1 个对就通过
  • 决赛:猜对冠军才通过
typescript 复制代码
function checkSwissStagePass(
  stageId: TaskStageType,
  prediction: StagePrediction | undefined,
  actual: SwissResult | undefined,
): StagePassStatus {
  const requiredCount = 5  // 需要 5 个正确

  let correctCount = 0
  let impossibleCount = 0

  // 检查 3-0 竞猜
  for (const team of prediction['3-0']) {
    if (actual['3-0'].includes(team)) {
      correctCount++
    } else if (!isPredictionPossible(team, '3-0', actual)) {
      impossibleCount++
    }
  }

  // ... 检查其他类别

  // 判断通过状态
  return {
    passed:
      correctCount >= requiredCount ? true :
      impossibleCount > 5 ? false :  // 已经不可能通过了
      null  // 还不确定
  }
}

这里的 passed 有三种状态:true(通过)、false(失败)、null(待定),这样可以在比赛进行中就提前判断某些主播"已经寄了"。

3. 赛事进度跟踪

这个功能花了我不少时间。一开始我想的是手动配置当前进行到哪个阶段,但后来发现这样太麻烦了,每次比赛结束都要改代码。

最后我实现了一个自动进度检测的逻辑:

typescript 复制代码
export function getEventProgress(event: MajorEvent): EventProgress {
  const stagesProgress: StageProgress[] = []

  // 检查每个阶段的结果完整度
  for (const stage of ['stage-1', 'stage-2', 'stage-3', 'finals']) {
    const hasResults = hasStageResults(event, stage)
    const isComplete = isStageComplete(event, stage)

    stagesProgress.push({
      stageId: stage,
      status: isComplete ? 'completed' :
              hasResults ? 'in_progress' :
              'not_started'
    })
  }

  // 自动判断当前阶段
  const currentStage = stagesProgress.find(s => s.status === 'in_progress')

  return {
    eventStatus: determineEventStatus(stagesProgress),
    currentStage: currentStage?.stageId || null,
    completedStages: stagesProgress
      .filter(s => s.status === 'completed')
      .map(s => s.stageId)
  }
}

现在我只需要更新 events.json 里的比赛结果,页面会自动:

  • 显示当前进行到哪个阶段
  • 隐藏还没开始的阶段
  • 高亮正在进行的阶段

完全不用改代码,数据驱动一切!

组件设计

组件结构遵循 Next.js App Router 的最佳实践:默认 Server Components,必要时才用 Client Components

项目里只有 5 个 Client Component:

typescript 复制代码
// components/ThemeProvider.tsx
'use client'
export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('system')
  // 使用 localStorage 和 useEffect
}

// components/ThemeToggle.tsx
'use client'
export function ThemeToggle() {
  const { theme, setTheme } = useTheme()
  // 使用 onClick 交互
}

// components/EventContext.tsx
'use client'
export const EventContext = createContext(...)
// 使用 Context 和 State

// components/EventSelector.tsx
'use client'
export function EventSelector() {
  // 表单交互
}

// app/predictions/StageNav.tsx
'use client'
export function StageNav() {
  // 客户端路由状态
}

其他所有页面和组件都是 Server Components,好处是:

  • Bundle size 更小:Server Components 的代码不会打包到客户端
  • 首屏渲染更快:直接返回 HTML,不需要 hydration
  • 数据获取更简单 :可以直接 import JSON 数据

举个例子,排行榜页面的代码非常简洁:

typescript 复制代码
// app/predictors/page.tsx
import { getAllPredictorStats } from '@/lib/data'

export default function PredictorsPage() {
  const stats = getAllPredictorStats('budapest-2025')

  return (
    <div>
      {stats.map(stat => (
        <div key={stat.id}>
          {stat.name}: {stat.totalPassed}/{stat.totalStages}
        </div>
      ))}
    </div>
  )
}

不需要 useStateuseEffectfetch,直接调用函数拿数据就行,太爽了。

开发体验:Claude Code 真香

这个项目大概 70% 的代码是我和 Claude Code 配合完成的。我主要负责:

  1. 产品需求和交互设计
  2. 数据结构设计
  3. 核心业务逻辑的思路
  4. Code Review 和 Bug 修复

Claude Code 主要负责:

  1. 页面组件实现
  2. 样式调整
  3. 类型定义完善
  4. 重复性工作

具体流程大概是这样的:

lua 复制代码
我:「帮我实现一个排行榜页面,显示每个主播通过了几个阶段」

Claude: 「好的,我先看看数据结构...
        需要调用 getAllPredictorStats 函数...
        布局用 table 还是 card?」

我:「移动端用 card,桌面端用 table」

Claude: 「收到,开始实现...」
        [写完代码]
        「已完成,使用了响应式布局,mobile 是 card view...」

我:「主题色不对,应该用 CS2 的橙色」

Claude: 「了解,我看看配置...
        需要在 globals.css 里加 --color-primary-500...」

这种协作模式效率很高,我只需要关注"做什么"和"为什么",具体的"怎么做"交给 Claude。

当然也不是完全没问题,遇到过几次比较坑的:

坑 1:Tailwind v4 配置混乱

一开始 Claude 老是想创建 tailwind.config.ts,我说了好几次"v4 不需要 config 文件"它才理解。后来我直接在项目的 CLAUDE.md 里写明了配置方式,就没问题了。

坑 2:类型推断不够准确

有些复杂的类型推断 Claude 会搞错,比如这个:

typescript 复制代码
// 我期望的类型
type StageId = SwissStageType | 'finals'

// Claude 写的类型
type StageId = string  // 太宽泛了

这种情况我会直接改类型定义,然后让它重新生成相关代码。

坑 3:过度工程

Claude 有时候会想得太多,比如我让它实现一个简单的队伍 Logo 组件,它给我写了个支持 lazy loading、error boundary、skeleton loading 的复杂组件...

我的原则是:先实现最简单能用的版本,需要优化再说

性能优化:从 3.2MB 到 180KB

最开始打包完发现 bundle size 有 3.2MB,吓了一跳。分析了一下主要问题:

1. JSON 数据没有 tree-shaking

events.jsonpredictions.json 虽然是静态导入的,但打包时会全部打进 bundle。

解决方案:把数据处理逻辑全部放在 Server Component 里,JSON 数据永远不会发送到客户端。

2. 图标库太大

一开始用的 react-icons,发现打包后有 800KB+。

解决方案:删掉 react-icons,手动写几个 SVG 图标:

typescript 复制代码
// components/icons.tsx
export function MoonIcon() {
  return (
    <svg width="20" height="20" viewBox="0 0 20 20">
      <path d="M..." fill="currentColor" />
    </svg>
  )
}

只需要月亮、太阳、链接这几个图标,手写 SVG 体积可以忽略不计。

3. 不必要的 Client Component

最开始有些组件没必要是 Client Component,但我习惯性写了 'use client'

解决方案:仔细检查每个组件,只在必要时才用 'use client'

  • 需要 useStateuseEffect 等 hooks → Client Component
  • 需要事件处理(onClick、onChange) → Client Component
  • 纯展示逻辑 → Server Component

优化后的 bundle size:

  • JS (First Load): 180KB
  • CSS: 12KB
  • Images: 按需加载(Next.js Image 组件自动优化)

Lighthouse 跑分:

  • Performance: 98
  • Accessibility: 100
  • Best Practices: 100
  • SEO: 100

响应式设计:移动优先

CS2 玩家很多都是用手机查看竞猜数据的,所以移动端体验特别重要。

我的设计原则是 Mobile First

css 复制代码
/* 默认是移动端样式 */
.container {
  padding: 1rem;
  font-size: 0.875rem;
}

/* sm: 640px 以上 */
@media (min-width: 640px) {
  .container {
    padding: 1.5rem;
    font-size: 1rem;
  }
}

/* md: 768px 以上 */
@media (min-width: 768px) {
  .container {
    padding: 2rem;
  }
}

在 Tailwind 里就是:

tsx 复制代码
<div className="px-4 sm:px-6 md:px-8 text-sm sm:text-base">

移动端特殊优化

1. 刘海屏适配

iPhone 的刘海屏需要特殊处理:

css 复制代码
body {
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}

2. 表格转卡片

桌面端用表格,移动端用卡片:

tsx 复制代码
{/* 移动端:Card View */}
<div className="md:hidden">
  {stats.map(stat => (
    <div key={stat.id} className="card">
      <div>{stat.name}</div>
      <div>{stat.totalPassed}/{stat.totalStages}</div>
    </div>
  ))}
</div>

{/* 桌面端:Table View */}
<table className="hidden md:table">
  <thead>...</thead>
  <tbody>
    {stats.map(stat => (
      <tr key={stat.id}>
        <td>{stat.name}</td>
        <td>{stat.totalPassed}/{stat.totalStages}</td>
      </tr>
    ))}
  </tbody>
</table>

3. 触摸优化

移动端的点击区域要足够大:

css 复制代码
.touch-target {
  min-height: 44px; /* iOS 建议的最小触摸尺寸 */
  padding: 0.75rem;
}

明暗主题实现

主题切换是个很常见的需求,但 Tailwind v4 的实现方式和之前不太一样。

主题状态管理

用 React Context + localStorage 实现:

typescript 复制代码
// components/ThemeProvider.tsx
'use client'

type Theme = 'light' | 'dark' | 'system'

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState<Theme>('system')

  useEffect(() => {
    const saved = localStorage.getItem('theme') as Theme
    if (saved) setTheme(saved)
  }, [])

  useEffect(() => {
    const root = document.documentElement
    const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
      .matches ? 'dark' : 'light'

    const effectiveTheme = theme === 'system' ? systemTheme : theme

    root.classList.remove('light', 'dark')
    root.classList.add(effectiveTheme)

    localStorage.setItem('theme', theme)
  }, [theme])

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

CSS 变量定义

globals.css 里定义主题变量:

css 复制代码
:root {
  --foreground: #18181b;
  --color-surface-0: #ffffff;
  --color-primary-500: #ff6b35;
  --color-win: #22c55e;
  --color-lose: #ef4444;
}

.dark {
  --foreground: #fafafa;
  --color-surface-0: #0a0a0a;
  --color-primary-500: #ff8c61;
  --color-win: #4ade80;
  --color-lose: #f87171;
}

语义化类名

避免到处写 dark: 前缀:

css 复制代码
/* 不好的做法 */
.text {
  @apply text-zinc-900 dark:text-white;
}

/* 好的做法 */
.text-primary {
  color: var(--foreground);
}

使用时就很简洁:

tsx 复制代码
<h1 className="text-primary">标题</h1>

主题切换时会自动应用对应的颜色,不需要任何额外的类名。

部署:Vercel 一把梭

部署选择了 Vercel,理由很简单:

  1. 和 Next.js 深度集成:零配置部署
  2. 自动 HTTPS:不用自己搞证书
  3. 全球 CDN:访问速度快
  4. 预览部署:每个 PR 自动生成预览链接

部署流程:

bash 复制代码
# 1. 推送到 GitHub
git push origin main

# 2. Vercel 自动检测并构建
# 3. 几分钟后自动上线

就这么简单,连 CI/CD 配置都不用写。

域名配置

在 Vercel 后台添加自定义域名 major.viki.moe,然后在域名服务商那里添加一条 CNAME 记录指向 Vercel 就行了。HTTPS 证书 Vercel 会自动申请和续期。

遇到的问题和解决方案

问题 1:竞猜数据录入太慢

一开始我是手动看图片一个个录入数据,十几个主播录了两个小时,太痛苦了。

解决方案:写了个简单的数据录入脚本:

typescript 复制代码
// scripts/add-prediction.ts
const prediction = {
  id: 's1mple',
  name: 's1mple',
  platform: 'twitch',
  'stage-1': {
    '3-0': ['Spirit', 'Liquid'],
    '3-1-or-3-2': ['G2', 'Vitality', 'FaZe', 'MOUZ', 'NAVI', 'Heroic'],
    '0-3': ['BIG', '9z']
  }
}

// 追加到 predictions.json

虽然还是需要手动输入,但至少不用在 JSON 里手写引号和逗号了。

未来如果数据量大了,可能会考虑做个后台管理界面,或者直接 OCR 识别图片。

有些战队的 Logo 在 Steam CDN 上找不到,导致页面出现破图。

解决方案:实现了一个带 fallback 的 Logo 组件:

typescript 复制代码
export function TeamLogo({ team }: { team: string }) {
  const [error, setError] = useState(false)

  if (error) {
    return (
      <div className="w-8 h-8 rounded bg-surface-1 flex items-center justify-center">
        <span className="text-xs font-bold">{team.slice(0, 2)}</span>
      </div>
    )
  }

  return (
    <Image
      src={`https://steamcdn-a.akamaihd.net/apps/csgo/images/teams/${team}.png`}
      alt={team}
      width={32}
      height={32}
      onError={() => setError(true)}
    />
  )
}

找不到 Logo 就显示战队名称的缩写,体验好很多。

问题 3:阶段进度判断逻辑复杂

这个是最头疼的问题。赛事有多个阶段,每个阶段又有不同的完成标准:

  • 瑞士轮:需要所有战绩组都有数据(3-0 两队,3-1 三队...)
  • 淘汰赛:需要 winners 和 losers 都有数据
  • 决赛:只需要有 winner

而且还要区分"进行中"和"已完成"两种状态。

解决方案 :写了一套状态机逻辑 getEventProgress(),根据实际数据自动判断当前状态。这个函数有 100+ 行,但把所有边界情况都考虑进去了。

测试方法也很简单:手动修改 events.json 里的数据,看页面显示是否正确。

未来计划

目前项目还比较基础,后续打算加一些功能:

1. 历史 Major 数据

把之前几届 Major 的数据也录入进来,可以看主播的历史战绩。

2. 统计图表

用 Chart.js 或者 Recharts 做一些可视化:

  • 每个主播的通过率趋势
  • 各队伍被竞猜的次数
  • 3-0 和 0-3 的命中率对比

3. 竞猜推荐

根据历史数据和当前形势,给出一个"推荐竞猜",帮助玩家做决策。

不过这个得小心,别搞成"割韭菜"工具...

4. 数据导出

支持导出成图片或者 Excel,方便分享。

5. PWA 支持

加个 Service Worker,支持离线访问和桌面安装。

总结

这个项目从想法到上线大概花了一周时间,其中:

  • 需求分析和设计:1 天
  • 开发和调试:4 天
  • 数据录入和测试:2 天

虽然功能还比较简单,但已经能满足基本需求。通过这个项目,我也对 Next.js 16、React 19、Tailwind v4 有了更深的理解。

最大的感受是:好的工具链真的能大幅提升开发效率。Next.js 的 App Router、Tailwind 的语义化类名、TypeScript 的类型检查,每一个都让开发体验更好。

如果你也对 CS2 Major 竞猜感兴趣,欢迎试用这个项目:major.viki.moe

如果你有任何建议或者想分享竞猜数据,欢迎通过以下方式联系我:

最后,祝大家都能拿到钻石币!🏆

相关推荐
八点2 小时前
Electron 应用中 Sharp 模块跨架构兼容性问题解决方案
前端
黑臂麒麟2 小时前
DevUI modal 弹窗表单联动实战:表格编辑功能完整实现
前端·javascript·ui·angular.js
国服第二切图仔2 小时前
DevUI Design中后台产品的开源前端解决方案之DataTable 表格组件核心解析
前端
懒人村杂货铺2 小时前
FastAPI + 前端(Vue/React)Docker 部署全流程
前端·vue.js·fastapi
7***37452 小时前
前端技术的下一站:从“页面开发”走向“体验工程”
前端
哆啦A梦15882 小时前
商城后台管理系统 01,商品管理-搜索
前端·javascript·vue.js
苏小瀚2 小时前
[JavaEE] Spring Web MVC入门
前端·java-ee·mvc
前端不太难2 小时前
RN 构建包体积过大,如何瘦身?
前端·react native
小光学长2 小时前
基于web的影视网站设计与实现14yj533o(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
java·前端·数据库