Appearance
前端架构
本文引用的文件
- web/src/main.js
- web/src/App.vue
- web/src/api.js
- web/src/adminApi.js
- web/src/session.js
- web/src/skinModel.js
- web/src/i18n.js
- web/src/net.js
- web/src/errors.js
- web/vite.config.js
- web/viewer-src.js
- web/package.json
- web/src/components/AdminPanel.vue
- web/src/pages/DashboardPage.vue
- web/src/components/plugin/PluginPage.vue
- web/src/components/ModalHost.vue
- web/src/components/ToastHub.vue
目录
引言
本文面向维护 SkinNexus Web 前端的工程师,系统说明 web/src 下的应用入口、路由组织、组件树、状态管理、API 客户端分层、国际化与 Vite 构建配置,并给出新增页面、组件和 API 的完整流程。该前端使用 Vue 3 + Composition API、vue-i18n 和 Vite;后端 Go 服务同时提供 SSR 页面、Web API、管理 API、静态资源与插件桥接。
项目结构
web 子工程是一个 SPA 应用,但通过 Vite 多入口同时产出固定名称的 viewer.js,供后端渲染的纹理详情页增强。
图示来源
关键目录职责
| 路径 | 职责 |
|---|---|
web/src/main.js | 创建 Vue 应用、挂载根组件、注册 i18n、加载全局样式 |
web/src/App.vue | 应用外壳:登录态、站点能力、导航、视图分发、全局上下文注入 |
web/src/pages | 玩家侧页面:仪表盘、衣柜、角色、资料、通知、授权应用等 |
web/src/components/admin | 管理面板各功能视图 |
web/src/components/plugin | 插件 iframe 宿主与字段容器 |
web/src/components | 全局组件:弹窗、提示、皮肤预览、分页器、表单等 |
web/src/api.js | 玩家 Web API 与上传封装 |
web/src/adminApi.js | 管理 API 共享 fetch 与令牌持久化 |
web/src/session.js | 全局 401 未授权回调与守卫 |
web/src/i18n.js | vue-i18n 初始化与语言 cookie/localStorage 同步 |
web/src/net.js | 带超时的 fetch 包装 |
web/src/errors.js | 后端错误码到用户消息的统一翻译 |
web/viewer-src.js | 纹理详情页渐进增强的 3D 预览与交互 |
web/vite.config.js | Vite 开发服务器、代理、多入口与产物命名 |
章节来源
核心组件
- App.vue:应用根组件。负责启动引导、会话读取、站点能力拉取、导航分组、异步页面懒加载、插件 UI 注册、以及向子组件注入用户、站点配置和跳转函数。
- AdminPanel.vue:管理面板外壳,包含侧边栏、动态可见项(邀请码、Union)、URL 深链同步、插件页嵌入与 token 输入区。
- DashboardPage.vue:玩家仪表盘,承载公告 Markdown 渲染、上传、纹理列表、衣柜操作、进度与确认对话框。
- PluginPage.vue / PluginFrame.vue:插件 iframe 页面宿主;当前版本支持
iframe渲染类型,schema 渲染标记为暂不支持。 - ModalHost.vue:全局弹窗宿主,由
dialog.js驱动,支持提示、确认和文本输入三种形态。 - ToastHub.vue:全局 Toast 通知中心,监听自定义
skin-toast事件,最多保留最近三条,自动消失。
章节来源
- web/src/App.vue:1-120
- web/src/components/AdminPanel.vue:1-60
- web/src/pages/DashboardPage.vue:1-40
- web/src/components/plugin/PluginPage.vue:1-39
- web/src/components/ModalHost.vue:1-53
- web/src/components/ToastHub.vue:1-79
架构总览
应用采用“单壳 + 异步页面”的架构:main.js 只挂载 App.vue,所有页面和 admin 面板都通过 defineAsyncComponent 按需加载,避免登录卡片或小型页面下载无关代码。
图示来源
- web/src/App.vue:1-40
- web/src/components/AdminPanel.vue:1-60
- web/src/api.js:1-40
- web/src/adminApi.js:1-32
- web/src/net.js:1-23
- web/src/errors.js:1-35
详细组件分析
应用外壳与视图组合:App.vue
App.vue 是整站控制面。它做以下事情:
- 定义异步页面加载器,为每个页面设置
PageLoading占位与延迟。 - 通过
provide('sessionUser', user)和provide('siteConfig', siteConf)将会话与站点能力注入全树。 - 根据
/api/v1/me返回的permission判断是否为管理员,决定是否显示管理面板。 - 计算导航分组,包括内置页面、通知徽标、Union 入口、插件菜单和管理面板入口。
- 处理邮箱验证门、邀请码、邮件配置、上传限制等站点能力。
- 管理移动端抽屉菜单、认证锁屏倒计时、外部插件路径。
图示来源
章节来源
管理面板:AdminPanel.vue
管理面板以侧边栏为主,按模块切换视图,并通过 URL 段同步状态,使刷新、分享和后退前进保持激活项。其关键行为:
- 根据站点能力隐藏邀请码、Union 等可选模块。
- 区分“会话管理员”(
permission > 0)与“机器令牌管理员”。 - 当会话不可用时允许输入
X-Admin-Token,成功后进入面板。 - 管理面板内嵌插件页面,复用
navPages('admin')的清单。
图示来源
章节来源
玩家侧页面与仪表盘:DashboardPage.vue
仪表盘承担大量玩家日常操作:公告渲染、上传材质、查看纹理列表、入柜、重命名、删除、选择穿戴目标等。它体现的常见模式是:
- 服务端分页 + 客户端搜索筛选。
- 行级并发保护:用
rowBusySet 防止重复提交。 - 结合
detectSkinModel在上传前预览手臂模型。 - 通过
guard401统一处理未授权。 - 使用
confirmDialog触发全局弹窗。
图示来源
章节来源
插件 iframe 宿主:PluginPage.vue 与 PluginFrame.vue
插件 UI 当前以 iframe 方式嵌入。主机通过 manifest 声明页面路径、渲染类型和导航标签,PluginPage.vue 根据 pageByPath 解析后渲染 <PluginFrame>。若渲染类型不是 iframe,则显示“暂不支持”提示。
图示来源
章节来源
全局弹窗宿主:ModalHost.vue
全局弹窗由 dialog.js 的状态驱动,ModalHost.vue 只负责展示三种形态:
- alert:仅关闭按钮。
- prompt:带输入框。
- confirm:确认/取消。
图示来源
章节来源
全局通知:ToastHub.vue
ToastHub 订阅 window.skin-toast 事件,最多保留最近三条通知,每条默认 4.2 秒后自动移除。它提供 role="status" 和 aria-live="polite",便于屏幕阅读器播报。
图示来源
章节来源
API 客户端分层
玩家 API:api.js
api.js 暴露三类能力:
- 通用请求函数
req:设置 JSON 头、处理 204、解析 JSON、包装非 2xx 错误、透传status和结构化data。 - 玩家接口集合:登录、注册、登出、修改密码、找回/重置密码、邮箱验证、通知、衣柜、角色、头像、个人档案、皮肤库点赞/收藏/编辑等。
- OIDC 设备流程与 Union 成员接口。
- 独立的
uploadTexture:使用XMLHttpRequest上传FormData,支持进度回调、超时和凭据发送。
图示来源
章节来源
管理 API:adminApi.js
管理 API 单独封装,用于 AdminPanel.vue 及其子视图。特点:
- 从
localStorage读取admin_token,并在请求头附加X-Admin-Token。 - 401/403 时设置
needToken.value = true,让外壳显示令牌输入卡。 - 所有错误携带
status和data,调用方可区分“模块未启用”“会话失效”“资源冲突”等情况。 - 提供
persistToken()把令牌落盘。
图示来源
章节来源
会话与未授权处理:session.js
session.js 提供:
setUnauthorizedHandler(fn):注册全局未授权处理器,App.vue在启动时设置为回到登录页。notifyUnauthorized():通知所有消费者。guard401(e):页面级 catch 中快速判断 401,避免重复提示。
图示来源
章节来源
领域模型:skinModel.js
skinModel.js 实现客户端手臂模型检测,逻辑对齐后端 server/internal/texture/model.go:
- 仅对宽高相等且宽度为 64 倍数的图片进行分析。
- 使用
OffscreenCanvas采样右臂第一层的两个透明区域。 - 发现透明像素即判定为
slim,否则回退为default。 - 浏览器不支持时直接降级为
default,不影响存储侧决策。
图示来源
章节来源
国际化:i18n.js 与 locales
i18n.js 使用 vue-i18n,语言优先级为:
skin_langcookielocalStorage.skin_langnavigator.language- 默认
zh-CN
语言切换会同步更新:
document.documentElement.langlocalStorage.skin_langskin_langcookie(SameSite=Lax,HTTPS 下加 Secure,一年有效期)
后端 SSR 也读写同一 cookie,因此 SPA 与 SSR 的语言选择保持一致。本地语言包位于 web/src/locales/en.json 和 web/src/locales/zh-CN.json。
图示来源
章节来源
3D 预览集成:viewer-src.js
viewer-src.js 是 Vite 固定入口 viewer,产出 /assets/viewer.js,专供后端 SSR 纹理详情页增强:
- 渐进增强:先渲染静态
<img>,JS 可用时再升级为skinview3dcanvas。 - WebGL 探测失败时保留静态内容,不破坏 SEO。
- 动态导入
skinview3d,仅在需要且环境支持时加载约 500KB 的代码块。 - 文案来自后端注入的
window.__SKIN_I18N__,保证前后端语言一致。 - 处理点赞、举报、编辑等交互,401 时跳转到
/login?next=...保留上下文。
图示来源
章节来源
依赖关系分析
图示来源
- web/src/main.js:1-6
- web/src/App.vue:1-40
- web/src/api.js:1-40
- web/src/adminApi.js:1-32
- web/src/pages/DashboardPage.vue:1-40
- web/viewer-src.js:1-20
- web/vite.config.js:1-20
章节来源
- web/src/main.js:1-6
- web/src/App.vue:1-40
- web/src/api.js:1-40
- web/src/adminApi.js:1-32
- web/viewer-src.js:1-20
性能与构建
- 代码分割与懒加载:所有页面和 admin 面板都通过
defineAsyncComponent懒加载,减少首屏体积。 - CSS 拆分关闭:
cssCodeSplit: false保证主题样式表优先于异步页面样式注入,避免覆盖顺序问题。 - 固定 viewer 入口:
viewer输出文件名固定为assets/viewer.js,满足后端 SSR 的契约。 - skinview3d 动态导入:只在 canvas 存在且 WebGL 可用时才加载 3D 库。
- 开发代理:Vite 将
/api和/textures代理到http://localhost:8080,方便本地联调。 - 网络超时:
fetchWithTimeout使用AbortController,将长时间无响应的 CDN 边缘节点转为可重试的网络错误。
图示来源
章节来源
开发工作流
- 安装依赖后运行
npm run dev启动 Vite,端口 5173。 - 后端服务需在
http://localhost:8080运行,以便代理/api和/textures。 - 构建产物输出到
web/dist,预览使用npm run preview。 - 新增页面或组件后无需改动
main.js,只需在App.vue中添加异步页面映射并加入导航。
章节来源
指南:新增页面、组件与 API
新增页面
- 在
web/src/pages新建 Vue 组件,例如NewFeaturePage.vue。 - 在
web/src/App.vue中使用asyncPage(() => import('./pages/NewFeaturePage.vue'))注册异步组件。 - 在导航分组中加入新项,确保 labelKey 指向 i18n 文案。
- 如果页面需要用户数据,使用
inject('sessionUser');如果需要站点配置,使用inject('siteConfig')。 - 如需跳转,使用
inject('gotoView')或直接操作view状态所在的父作用域约定。
章节来源
新增组件
- 在
web/src/components下创建组件文件。 - 如果是全局通知,发布
new CustomEvent('skin-toast', { detail }),由ToastHub.vue消费。 - 如果需要用户确认,调用
dialog.js提供的confirmDialog,由ModalHost.vue渲染。 - 如果组件依赖用户会话,使用
inject('sessionUser'),避免从 props 层层传递。
章节来源
新增 API
- 在
web/src/api.js中新增方法,遵循现有req风格:设置 method、path、body,并返回 Promise。 - 如果接口属于管理后台,放在
web/src/adminApi.js并通过fetchJSON调用。 - 如果涉及文件上传,参考
uploadTexture使用FormData和XMLHttpRequest,带上withCredentials和进度回调。 - 在页面中引入并使用,捕获错误时使用
guard401统一处理未授权。 - 如需后端新增路由,请同步更新后端
server/internal/webapi或server/internal/adminapi路由与校验逻辑。
章节来源
可访问性与响应式布局
- 可访问性
ToastHub.vue使用role="status"与aria-live="polite",并提供关闭按钮的aria-label。ModalHost.vue在 prompt 打开后聚焦输入框,支持回车确认。- 错误消息通过
errors.js统一翻译,避免硬编码字符串导致语言错乱。
- 响应式布局
App.vue维护移动端顶部抽屉菜单开关menuOpen,在窄屏使用汉堡菜单。ToastHub.vue对触控设备放大关闭按钮尺寸,并限制最大宽度。- 导航图标使用一致的 SVG 线宽,保持视觉一致性。
- 动画偏好
- Toast 动画遵循
prefers-reduced-motion: reduce,减少动效对用户的影响。
- Toast 动画遵循
章节来源
故障排查
| 现象 | 可能原因 | 处理建议 |
|---|---|---|
| 登录后立即回到登录页 | 会话过期或 401 | 检查 session.js 的 guard401,确认 App.vue 设置了未授权处理器 |
| 管理面板一直要求输入 token | 会话无管理权限或后端禁用模块 | 确认 permission > 0,或使用 X-Admin-Token |
| 上传进度不更新 | 未传入 onProgress 或后端未返回长度 | 检查 uploadTexture 的 meta.onProgress 和响应体 |
| 纹理详情页无 3D 预览 | 无 WebGL 或 canvas 不存在 | 检查 viewer-src.js 的 WebGL 探测逻辑 |
| 语言切换无效 | cookie/localStorage 被阻止或拼写错误 | 检查 skin_lang cookie 与 KNOW_LOCALES |
| 网络请求永久等待 | CDN 或后端无响应 | 查看 net.js 的 AbortController 超时时间 |
| 错误消息中文出现在英文界面 | 未在 errors.js 或 i18n 字典中注册 code | 补充 errors.* 翻译或后端返回已知 code |
章节来源
- web/src/session.js:1-13
- web/src/adminApi.js:1-32
- web/src/api.js:120-161
- web/viewer-src.js:1-60
- web/src/i18n.js:1-68
- web/src/net.js:1-23
- web/src/errors.js:1-35
结论
SkinNexus 前端以 App.vue 为单一控制点,配合异步页面、全局弹窗、Toast、会话守卫和双层 API 客户端,形成清晰的分层结构。玩家侧与管理侧通过权限隔离,插件 UI 通过 iframe 沙箱嵌入,3D 预览通过固定入口与渐进增强兼容旧环境。国际化在 cookie、localStorage、SSR 与 SPA 之间保持一致,Vite 构建在保证主题样式顺序的同时,对大体积 3D 库进行懒加载。新增功能应优先扩展页面、组件和 API 层,避免绕过统一的错误、会话和国际化机制。