Skip to content

242. 综合实战:开放世界(四十三)

Published:

上节实现了注册的接口。

这节在前端代码里集成一下。

创建 src/api/register.js

/**
 * 用户注册 API。开发环境通过 Vite 代理访问 localhost:3000;生产可设 VITE_API_BASE。
 */
function getApiBase() {
  const base = import.meta.env.VITE_API_BASE;
  if (base) {
    return String(base).replace(/\/$/, '');
  }
  return '/api';
}

/**
 * POST /user/register
 * @returns {Promise<{ id: number, username: string, createdAt: string }>}
 */
export async function registerUser(username, password) {
  const url = `${getApiBase()}/user/register`;
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ username, password })
  });
  let data = {};
  try {
    data = await res.json();
  } catch {
    /* ignore */
  }
  if (!res.ok) {
    const msg =
      data.message ||
      data.error ||
      (typeof data === 'string' ? data : null) ||
      `请求失败 (${res.status})`;
    throw new Error(msg);
  }
  return data;
}

这里的 base url 在 vite.config.js 里配置:

import { defineConfig } from 'vite'

export default defineConfig({
  // base: '/threejs-open-world/',
  server: {
    proxy: {
      // 开发时前端请求 /api/* 转发到后端,路径去掉 /api 前缀 → 与 curl localhost:3000/user/register 一致
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  },
  build: {
    outDir: 'dist',
    assetsDir: 'assets',
    minify: false,
  }
})

然后在界面加一下这部分 html:

image.png

<!-- 注册(结构在 HTML;显示/隐藏仅用 hidden 属性 + CSS,不由 JS 创建节点) -->
    <div id="registerPanel" class="register-panel" hidden>
      <div class="settings-content register-content">
        <div class="settings-header">
          <h2>注册</h2>
          <button id="closeRegisterBtn" type="button" class="close-btn">关闭 (ESC)</button>
        </div>
        <div class="settings-body">
          <form id="registerForm" class="register-form">
            <div class="settings-item register-field">
              <label for="registerUsername">用户名</label>
              <input id="registerUsername" name="username" type="text" autocomplete="username" required minlength="1" />
            </div>
            <div class="settings-item register-field">
              <label for="registerPassword">密码</label>
              <input id="registerPassword" name="password" type="password" autocomplete="new-password" required minlength="1" />
            </div>
            <div class="register-actions">
              <button type="submit" id="registerSubmitBtn" class="save-btn">注册</button>
            </div>
            <p id="registerMessage" class="register-message" role="status" hidden></p>
          </form>
        </div>
      </div>
    </div>
    <!-- 设置按钮 -->
    <button id="registerBtn" class="register-btn" type="button" title="注册">注册</button>

在 main.js 写一下这部分逻辑:

代码比较多,你可以直接从 github 仓库复制代码:

image.png

import { registerUser } from './api/register.js';

image.png

export let isRegisterOpen = false;

image.png

if (isRegisterOpen) {
    isRegisterOpen = false;
    const registerPanel = document.getElementById('registerPanel');
    if (registerPanel) registerPanel.hidden = true;
    const registerMsg = document.getElementById('registerMessage');
    if (registerMsg) {
      registerMsg.hidden = true;
      registerMsg.textContent = '';
      registerMsg.className = 'register-message';
    }
}

image.png

function toggleRegister() {
  const registerPanel = document.getElementById('registerPanel');
  if (!registerPanel) return;

  const willOpen = !isRegisterOpen;
  if (willOpen) {
    if (isSettingsOpen) {
      isSettingsOpen = false;
      const settingsPanel = document.getElementById('settingsPanel');
      if (settingsPanel) settingsPanel.style.display = 'none';
    }
    if (isManualOpen) {
      isManualOpen = false;
      const manualPanel = document.getElementById('manualPanel');
      if (manualPanel) manualPanel.style.display = 'none';
    }
  }

  isRegisterOpen = !isRegisterOpen;
  registerPanel.hidden = !isRegisterOpen;

  if (isRegisterOpen && document.pointerLockElement) {
    document.exitPointerLock();
  }

  const registerMsg = document.getElementById('registerMessage');
  if (!isRegisterOpen && registerMsg) {
    registerMsg.hidden = true;
    registerMsg.textContent = '';
    registerMsg.className = 'register-message';
  }
}

image.png

const registerPanel = document.getElementById('registerPanel');
  const registerBtn = document.getElementById('registerBtn');
  const closeRegisterBtn = document.getElementById('closeRegisterBtn');
  const registerForm = document.getElementById('registerForm');
  if (registerPanel) {
    registerPanel.addEventListener('mousedown', (e) => e.stopPropagation());
    registerPanel.addEventListener('click', (e) => e.stopPropagation());
  }
  if (registerBtn) {
    registerBtn.addEventListener('click', (e) => {
      e.stopPropagation();
      toggleRegister();
    });
  }
  if (closeRegisterBtn) {
    closeRegisterBtn.addEventListener('click', (e) => {
      e.stopPropagation();
      toggleRegister();
    });
  }
  if (registerForm) {
    registerForm.addEventListener('submit', async (e) => {
      e.preventDefault();
      const usernameEl = document.getElementById('registerUsername');
      const passwordEl = document.getElementById('registerPassword');
      const msgEl = document.getElementById('registerMessage');
      const submitBtn = document.getElementById('registerSubmitBtn');
      const username = usernameEl ? usernameEl.value.trim() : '';
      const password = passwordEl ? passwordEl.value : '';
      if (!username || !password) {
        if (msgEl) {
          msgEl.hidden = false;
          msgEl.textContent = '请填写用户名和密码';
          msgEl.className = 'register-message register-error';
        }
        return;
      }
      if (msgEl) {
        msgEl.hidden = true;
        msgEl.textContent = '';
        msgEl.className = 'register-message';
      }
      if (submitBtn) submitBtn.disabled = true;
      try {
        const data = await registerUser(username, password);
        if (msgEl) {
          msgEl.hidden = false;
          const t = data.createdAt
            ? new Date(data.createdAt).toLocaleString('zh-CN')
            : '';
          msgEl.textContent = t
            ? `注册成功:${data.username}(id: ${data.id})· ${t}`
            : `注册成功:${data.username}(id: ${data.id})`;
          msgEl.className = 'register-message register-success';
        }
      } catch (err) {
        if (msgEl) {
          msgEl.hidden = false;
          msgEl.textContent = err instanceof Error ? err.message : '注册失败';
          msgEl.className = 'register-message register-error';
        }
      } finally {
        if (submitBtn) submitBtn.disabled = false;
      }
    });
  }

试下效果:

2026-04-05 22.30.48.gif

注册成功!

去数据库看一下:

image.png

这样,注册功能就完成了。

总结

这节我们加上了注册的前端界面。

代码比较多,可以直接从仓库复制。

现在可以注册账号,下节来做登录的功能。

评论