update:优化个人训练图片
@@ -114,3 +114,133 @@ git push origin test
|
|||||||
3. **体验版发布前确认代码已提交**,避免遗漏
|
3. **体验版发布前确认代码已提交**,避免遗漏
|
||||||
4. **开发分支命名建议**:`feature/姓名-功能名`,如 `feature/zhangsan-login`
|
4. **开发分支命名建议**:`feature/姓名-功能名`,如 `feature/zhangsan-login`
|
||||||
5. **删除已合并的开发分支**:`git branch -d feature/your-name-work`
|
5. **删除已合并的开发分支**:`git branch -d feature/your-name-work`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、比赛服 Proto 更新流程
|
||||||
|
|
||||||
|
### 1. 新开发人员快速接手
|
||||||
|
|
||||||
|
首次拉取项目后,先安装依赖并确认当前协议与运行时解码器一致:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run proto:check
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
如果 `proto:check` 提示协议不同步,先执行 `npm run proto:generate`,不要直接修改生成区块。
|
||||||
|
|
||||||
|
### 2. 相关文件职责
|
||||||
|
|
||||||
|
| 文件 | 职责 | 修改规则 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `src/utils/match.min.js` | 后端比赛服协议描述,是生成字段表的输入文件 | 后端协议更新时整体替换;禁止在小程序运行时代码中导入 |
|
||||||
|
| `scripts/generate-match-schema.mjs` | 解析协议、校验字段并生成运行时字段表 | 只有新增通用协议能力时才修改 |
|
||||||
|
| `src/utils/matchProtocol.js` | 小程序实际使用的 protobuf 编解码器 | `<match-schema-generated>` 标记区间内禁止手动修改 |
|
||||||
|
| `src/matchWebsocket.js` | 服务端消息路由、ACK 和业务事件分发 | 新增消息业务行为时人工接入 |
|
||||||
|
| `src/utils/matchAdapter.js` | 将解码结果从 snake_case 统一转换为 camelCase | 页面继续使用适配后的字段名 |
|
||||||
|
|
||||||
|
`match.min.js` 不是小程序运行时解码器。运行时仍使用 `protobufjs/minimal.js` 的 `Reader/Writer`,`int64` 字段统一保留为字符串,避免大整数精度丢失。
|
||||||
|
|
||||||
|
### 3. 日常协议更新步骤
|
||||||
|
|
||||||
|
```text
|
||||||
|
替换 src/utils/match.min.js
|
||||||
|
↓
|
||||||
|
执行 npm run proto:update
|
||||||
|
↓
|
||||||
|
检查生成差异和业务接入范围
|
||||||
|
↓
|
||||||
|
执行 git diff --check
|
||||||
|
↓
|
||||||
|
微信开发者工具检查包体并上传
|
||||||
|
```
|
||||||
|
|
||||||
|
具体操作:
|
||||||
|
|
||||||
|
1. 从后端获取最新的 `match.min.js`,整体覆盖 `src/utils/match.min.js`。
|
||||||
|
2. 执行推荐命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run proto:update
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 该命令会依次完成:
|
||||||
|
- 解析 `match.min.js`
|
||||||
|
- 更新 `matchProtocol.js` 内的生成区块
|
||||||
|
- 校验生成区块与协议源文件一致
|
||||||
|
- 执行微信小程序正式构建
|
||||||
|
4. 检查 Git 差异。普通字段更新通常只应涉及:
|
||||||
|
- `src/utils/match.min.js`
|
||||||
|
- `src/utils/matchProtocol.js` 的生成区块
|
||||||
|
5. 如果新增了消息业务行为,再单独检查 `src/matchWebsocket.js`、页面或组件的接入改动。
|
||||||
|
6. 执行空白和换行检查:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
7. 打开微信开发者工具,导入 `dist/build/mp-weixin`,检查代码依赖分析和主包体积后再上传。
|
||||||
|
|
||||||
|
### 4. Proto 命令说明
|
||||||
|
|
||||||
|
| 命令 | 用途 | 是否修改文件 |
|
||||||
|
|------|------|--------------|
|
||||||
|
| `npm run proto:generate` | 根据 `match.min.js` 更新生成区块 | 是 |
|
||||||
|
| `npm run proto:check` | 检查生成区块是否与协议同步 | 否 |
|
||||||
|
| `npm run proto:update` | 执行生成,然后运行正式构建 | 是,日常更新推荐使用 |
|
||||||
|
| `npm run dev` | 启动开发构建,启动前自动执行 `proto:check` | 协议不同步时直接中止 |
|
||||||
|
| `npm run build` | 执行正式构建,构建前自动执行 `proto:check` | 协议不同步时直接中止 |
|
||||||
|
|
||||||
|
`npm run dev` 和 `npm run build` 只负责检查,不会自动改写协议;发现不同步时应执行 `npm run proto:generate` 或 `npm run proto:update`。
|
||||||
|
|
||||||
|
### 5. 自动生成与人工接入边界
|
||||||
|
|
||||||
|
以下内容由生成器自动处理:
|
||||||
|
|
||||||
|
- 服务端和客户端消息枚举
|
||||||
|
- 普通 scalar 字段
|
||||||
|
- 嵌套 message
|
||||||
|
- repeated message
|
||||||
|
- 当前解码器支持的 map
|
||||||
|
- snake_case 字段名和 oneof 兼容信息
|
||||||
|
|
||||||
|
以下情况仍需人工处理:
|
||||||
|
|
||||||
|
- **新增服务端消息类型**:枚举会自动生成,但 `matchWebsocket.js` 的路由、ACK、音频或页面事件仍需接入。
|
||||||
|
- **新增客户端指令**:枚举会自动生成,但发送函数、字段编码和业务调用入口仍需接入。
|
||||||
|
- **修改既有字段编号或类型**:属于协议兼容性变更,必须先与后端确认,不能只看构建是否通过。
|
||||||
|
- **新增解码器不支持的类型**:生成器会直接报错,需要同时扩展生成器和 `matchProtocol.js` 的通用解码能力。
|
||||||
|
|
||||||
|
当前生成器会拒绝不支持的 scalar、map key、字段规则以及 packed scalar repeated,避免继续构建后静默丢字段。
|
||||||
|
|
||||||
|
### 6. 常见报错处理
|
||||||
|
|
||||||
|
| 报错或现象 | 原因 | 处理方式 |
|
||||||
|
|------------|------|----------|
|
||||||
|
| 生成区块与 `match.min.js` 不同步 | 替换协议后没有重新生成 | 执行 `npm run proto:update` |
|
||||||
|
| 找不到 `Root.create(...)` | 后端提供的文件格式发生变化或文件不完整 | 停止更新,确认协议文件来源和生成格式 |
|
||||||
|
| 使用了不支持的类型、规则或 map key | 新协议超出当前通用解码器能力 | 不要手补生成区块,先扩展生成器和解码器并补充验证 |
|
||||||
|
| packed scalar repeated 不支持 | protobuf 默认可能使用 packed 编码,当前通用解码器未覆盖 | 增加 packed 解码能力后再重新生成 |
|
||||||
|
| `matchProtocol.js` 缺少生成区块标记 | 标记被误删或生成区块被手改 | 恢复 `<match-schema-generated>` 标记,重新执行生成 |
|
||||||
|
| 协议生成成功但构建失败 | 问题位于项目构建或业务代码,不是字段表同步 | 保留生成结果,按构建错误继续定位 |
|
||||||
|
|
||||||
|
### 7. 禁止事项与验收清单
|
||||||
|
|
||||||
|
禁止:
|
||||||
|
|
||||||
|
- 禁止在运行时代码中导入 `match.min.js`。
|
||||||
|
- 禁止手动修改 `<match-schema-generated>` 与 `</match-schema-generated>` 之间的内容。
|
||||||
|
- 禁止绕过 `proto:check` 后直接上传。
|
||||||
|
- 禁止把 `int64` 字段直接转换为普通 `Number`。
|
||||||
|
- 禁止把“枚举已经生成”等同于“业务消息已经完成接入”。
|
||||||
|
|
||||||
|
每次协议更新至少确认:
|
||||||
|
|
||||||
|
1. `npm run proto:check` 通过。
|
||||||
|
2. `npm run build` 通过,或直接确认 `npm run proto:update` 已完整通过。
|
||||||
|
3. `git diff --check` 通过。
|
||||||
|
4. 生成区块之外没有意外改动。
|
||||||
|
5. 新消息已完成必要的 WebSocket 路由和页面验证。
|
||||||
|
6. 微信开发者工具中的主包、分包体积符合上传限制。
|
||||||
|
|||||||
@@ -3,7 +3,12 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"proto:generate": "node scripts/generate-match-schema.mjs",
|
||||||
|
"proto:check": "node scripts/generate-match-schema.mjs --check",
|
||||||
|
"proto:update": "npm run proto:generate && npm run build",
|
||||||
|
"predev": "npm run proto:check",
|
||||||
"dev": "uni -p mp-weixin",
|
"dev": "uni -p mp-weixin",
|
||||||
|
"prebuild": "npm run proto:check",
|
||||||
"build": "uni build -p mp-weixin"
|
"build": "uni build -p mp-weixin"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -235,6 +235,14 @@
|
|||||||
{
|
{
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"value": "static/tab-mall.png"
|
"value": "static/tab-mall.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "folder",
|
||||||
|
"value": "static/training-home"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "folder",
|
||||||
|
"value": "static/training-difficulty-design"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"include": []
|
"include": []
|
||||||
|
|||||||
@@ -0,0 +1,440 @@
|
|||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import { readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { dirname, resolve } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import vm from "node:vm";
|
||||||
|
|
||||||
|
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const projectRoot = resolve(scriptDirectory, "..");
|
||||||
|
const sourcePath = resolve(projectRoot, "src/utils/match.min.js");
|
||||||
|
const runtimePath = resolve(projectRoot, "src/utils/matchProtocol.js");
|
||||||
|
const generatedStartMarker = "// <match-schema-generated>";
|
||||||
|
const generatedEndMarker = "// </match-schema-generated>";
|
||||||
|
|
||||||
|
const supportedScalarKinds = new Set([
|
||||||
|
"int32",
|
||||||
|
"int64",
|
||||||
|
"float",
|
||||||
|
"double",
|
||||||
|
"bool",
|
||||||
|
"string",
|
||||||
|
"bytes",
|
||||||
|
]);
|
||||||
|
const supportedMapKeyKinds = new Set(["int32", "int64", "bool", "string"]);
|
||||||
|
// 新版描述文件未携带旧协议的 oneof 元数据,保留现有解码结果中的 payload 标识。
|
||||||
|
const compatibilityOneofs = {
|
||||||
|
ServerMessage: {
|
||||||
|
match_info: "payload",
|
||||||
|
shoot_data: "payload",
|
||||||
|
practice_info: "payload",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function isRecord(value) {
|
||||||
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractRootCreateArgument(source) {
|
||||||
|
const match = /(?:\$protobuf\.)?Root\.create\s*\(/.exec(source);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error("match.min.js 中未找到 Root.create(...) 协议描述");
|
||||||
|
}
|
||||||
|
|
||||||
|
const openIndex = source.indexOf("(", match.index);
|
||||||
|
let depth = 0;
|
||||||
|
let quote = "";
|
||||||
|
let escaped = false;
|
||||||
|
let lineComment = false;
|
||||||
|
let blockComment = false;
|
||||||
|
|
||||||
|
for (let index = openIndex; index < source.length; index += 1) {
|
||||||
|
const char = source[index];
|
||||||
|
const next = source[index + 1];
|
||||||
|
|
||||||
|
if (lineComment) {
|
||||||
|
if (char === "\n") lineComment = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (blockComment) {
|
||||||
|
if (char === "*" && next === "/") {
|
||||||
|
blockComment = false;
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (quote) {
|
||||||
|
if (escaped) {
|
||||||
|
escaped = false;
|
||||||
|
} else if (char === "\\") {
|
||||||
|
escaped = true;
|
||||||
|
} else if (char === quote) {
|
||||||
|
quote = "";
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (char === '"' || char === "'" || char === "`") {
|
||||||
|
quote = char;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (char === "/" && next === "/") {
|
||||||
|
lineComment = true;
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (char === "/" && next === "*") {
|
||||||
|
blockComment = true;
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (char === "(") {
|
||||||
|
depth += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (char === ")") {
|
||||||
|
depth -= 1;
|
||||||
|
if (depth === 0) {
|
||||||
|
return source.slice(openIndex + 1, index).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("match.min.js 中的 Root.create(...) 括号不完整");
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripQuotedText(source) {
|
||||||
|
let output = "";
|
||||||
|
let quote = "";
|
||||||
|
let escaped = false;
|
||||||
|
|
||||||
|
for (const char of source) {
|
||||||
|
if (quote) {
|
||||||
|
output += " ";
|
||||||
|
if (escaped) {
|
||||||
|
escaped = false;
|
||||||
|
} else if (char === "\\") {
|
||||||
|
escaped = true;
|
||||||
|
} else if (char === quote) {
|
||||||
|
quote = "";
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (char === '"' || char === "'") {
|
||||||
|
quote = char;
|
||||||
|
output += " ";
|
||||||
|
} else {
|
||||||
|
output += char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDescriptor(argumentSource) {
|
||||||
|
if (!argumentSource.startsWith("{") || !argumentSource.endsWith("}")) {
|
||||||
|
throw new Error("Root.create(...) 参数不是静态对象字面量");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(argumentSource);
|
||||||
|
} catch {
|
||||||
|
// 兼容旧版 pbjs 生成的未加引号对象键;只允许静态对象语法。
|
||||||
|
const syntaxOnly = stripQuotedText(argumentSource);
|
||||||
|
if (/[();`=]/.test(syntaxOnly)) {
|
||||||
|
throw new Error("旧版协议描述包含非静态表达式,已拒绝执行");
|
||||||
|
}
|
||||||
|
const descriptor = vm.runInNewContext(
|
||||||
|
`(${argumentSource})`,
|
||||||
|
Object.create(null),
|
||||||
|
{ timeout: 1000 }
|
||||||
|
);
|
||||||
|
return JSON.parse(JSON.stringify(descriptor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findProtocolNamespace(node, path = []) {
|
||||||
|
if (!isRecord(node)) return null;
|
||||||
|
const entries = isRecord(node.nested) ? node.nested : node;
|
||||||
|
|
||||||
|
if (isRecord(entries.ServerMessage?.fields) && isRecord(entries.ClientMessage?.fields)) {
|
||||||
|
return { entries, path };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [name, child] of Object.entries(entries)) {
|
||||||
|
if (!isRecord(child) || isRecord(child.fields)) continue;
|
||||||
|
const found = findProtocolNamespace(child, [...path, name]);
|
||||||
|
if (found) return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEnumValues(definition) {
|
||||||
|
if (!isRecord(definition)) return null;
|
||||||
|
const candidate = isRecord(definition.values) ? definition.values : definition;
|
||||||
|
const entries = Object.entries(candidate);
|
||||||
|
if (entries.length === 0 || entries.some(([, value]) => !Number.isInteger(value))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Object.fromEntries(entries.sort((left, right) => left[1] - right[1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTypeName(type) {
|
||||||
|
return String(type || "")
|
||||||
|
.replace(/^\./, "")
|
||||||
|
.split(".")
|
||||||
|
.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOneofByField(definition) {
|
||||||
|
const result = new Map();
|
||||||
|
if (!isRecord(definition.oneofs)) return result;
|
||||||
|
|
||||||
|
for (const [groupName, groupDefinition] of Object.entries(definition.oneofs)) {
|
||||||
|
const fieldNames = Array.isArray(groupDefinition)
|
||||||
|
? groupDefinition
|
||||||
|
: groupDefinition?.oneof;
|
||||||
|
if (!Array.isArray(fieldNames)) continue;
|
||||||
|
for (const fieldName of fieldNames) result.set(fieldName, groupName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSchema({ messageName, definition, messages, enumNames }) {
|
||||||
|
const schema = {};
|
||||||
|
const usedIds = new Set();
|
||||||
|
const oneofByField = getOneofByField(definition);
|
||||||
|
|
||||||
|
for (const [fieldKey, fieldDefinition] of Object.entries(definition.fields)) {
|
||||||
|
const id = Number(fieldDefinition.id);
|
||||||
|
if (!Number.isInteger(id) || id <= 0) {
|
||||||
|
throw new Error(`${messageName}.${fieldKey} 的字段编号无效`);
|
||||||
|
}
|
||||||
|
if (usedIds.has(id)) {
|
||||||
|
throw new Error(`${messageName} 存在重复字段编号 ${id}`);
|
||||||
|
}
|
||||||
|
usedIds.add(id);
|
||||||
|
|
||||||
|
const rule = fieldDefinition.rule;
|
||||||
|
if (rule && !["optional", "required", "repeated", "map"].includes(rule)) {
|
||||||
|
throw new Error(`${messageName}.${fieldKey} 使用了不支持的规则 ${rule}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldName = fieldDefinition.protoName || fieldKey;
|
||||||
|
const typeName = normalizeTypeName(fieldDefinition.type);
|
||||||
|
const keyTypeName = normalizeTypeName(
|
||||||
|
fieldDefinition.keyType ?? fieldDefinition.keytype
|
||||||
|
);
|
||||||
|
const isMap = rule === "map" || Boolean(keyTypeName);
|
||||||
|
|
||||||
|
if (isMap) {
|
||||||
|
if (!supportedMapKeyKinds.has(keyTypeName)) {
|
||||||
|
throw new Error(
|
||||||
|
`${messageName}.${fieldKey} 使用了不支持的 map key 类型 ${keyTypeName}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const valueIsMessage = messages.has(typeName);
|
||||||
|
const valueKind = enumNames.has(typeName) ? "int32" : typeName;
|
||||||
|
if (!valueIsMessage && !supportedScalarKinds.has(valueKind)) {
|
||||||
|
throw new Error(
|
||||||
|
`${messageName}.${fieldKey} 使用了不支持的 map value 类型 ${typeName}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
schema[id] = valueIsMessage
|
||||||
|
? {
|
||||||
|
name: fieldName,
|
||||||
|
kind: "map",
|
||||||
|
keyKind: keyTypeName,
|
||||||
|
valueKind: "message",
|
||||||
|
valueType: typeName,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
name: fieldName,
|
||||||
|
kind: "map",
|
||||||
|
keyKind: keyTypeName,
|
||||||
|
valueKind,
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMessage = messages.has(typeName);
|
||||||
|
const kind = enumNames.has(typeName) ? "int32" : typeName;
|
||||||
|
if (!isMessage && !supportedScalarKinds.has(kind)) {
|
||||||
|
throw new Error(`${messageName}.${fieldKey} 使用了不支持的类型 ${typeName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const repeated = rule === "repeated";
|
||||||
|
if (repeated && !isMessage && !["string", "bytes"].includes(kind)) {
|
||||||
|
throw new Error(
|
||||||
|
`${messageName}.${fieldKey} 是 packed scalar repeated,当前通用解码器尚不支持`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const field = isMessage
|
||||||
|
? { name: fieldName, kind: "message", type: typeName }
|
||||||
|
: { name: fieldName, kind };
|
||||||
|
if (repeated) field.repeated = true;
|
||||||
|
|
||||||
|
const oneof =
|
||||||
|
fieldDefinition.oneof ||
|
||||||
|
oneofByField.get(fieldKey) ||
|
||||||
|
compatibilityOneofs[messageName]?.[fieldName];
|
||||||
|
if (oneof) field.oneof = oneof;
|
||||||
|
schema[id] = field;
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPropertyKey(key) {
|
||||||
|
return /^(?:[A-Za-z_$][\w$]*|\d+)$/.test(key) ? key : JSON.stringify(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatJsValue(value, depth = 0) {
|
||||||
|
if (!isRecord(value)) return JSON.stringify(value);
|
||||||
|
|
||||||
|
const entries = Object.entries(value);
|
||||||
|
if (entries.length === 0) return "{}";
|
||||||
|
|
||||||
|
const indent = " ".repeat(depth);
|
||||||
|
const primitiveEntries = entries.every(([, child]) => !isRecord(child));
|
||||||
|
if (primitiveEntries && value.kind !== "map") {
|
||||||
|
const singleLine = `{ ${entries
|
||||||
|
.map(([key, child]) => `${formatPropertyKey(key)}: ${JSON.stringify(child)}`)
|
||||||
|
.join(", ")} }`;
|
||||||
|
if (indent.length + singleLine.length <= 100) return singleLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
const childIndent = " ".repeat(depth + 1);
|
||||||
|
const lines = entries.map(
|
||||||
|
([key, child]) =>
|
||||||
|
`${childIndent}${formatPropertyKey(key)}: ${formatJsValue(child, depth + 1)},`
|
||||||
|
);
|
||||||
|
return `{\n${lines.join("\n")}\n${indent}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGeneratedSource(source) {
|
||||||
|
const descriptor = parseDescriptor(extractRootCreateArgument(source));
|
||||||
|
const namespace = findProtocolNamespace(descriptor);
|
||||||
|
if (!namespace) {
|
||||||
|
throw new Error("协议描述中未找到 ServerMessage 和 ClientMessage");
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = new Map();
|
||||||
|
const enums = new Map();
|
||||||
|
for (const [name, definition] of Object.entries(namespace.entries)) {
|
||||||
|
if (isRecord(definition?.fields)) {
|
||||||
|
messages.set(name, definition);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const values = getEnumValues(definition);
|
||||||
|
if (values) enums.set(name, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverMessageType = enums.get("ServerMessageType");
|
||||||
|
const clientMessageType = enums.get("ClientMessageType");
|
||||||
|
if (!serverMessageType || !clientMessageType) {
|
||||||
|
throw new Error("协议描述缺少 ServerMessageType 或 ClientMessageType");
|
||||||
|
}
|
||||||
|
|
||||||
|
const enumNames = new Set(enums.keys());
|
||||||
|
const schemaByName = new Map();
|
||||||
|
for (const [messageName, definition] of messages) {
|
||||||
|
schemaByName.set(
|
||||||
|
messageName,
|
||||||
|
buildSchema({ messageName, definition, messages, enumNames })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reachable = new Set();
|
||||||
|
function collectReachable(messageName) {
|
||||||
|
if (reachable.has(messageName)) return;
|
||||||
|
const schema = schemaByName.get(messageName);
|
||||||
|
if (!schema) throw new Error(`找不到消息定义 ${messageName}`);
|
||||||
|
reachable.add(messageName);
|
||||||
|
for (const field of Object.values(schema)) {
|
||||||
|
if (field.kind === "message") collectReachable(field.type);
|
||||||
|
if (field.kind === "map" && field.valueKind === "message") {
|
||||||
|
collectReachable(field.valueType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
collectReachable("ServerMessage");
|
||||||
|
|
||||||
|
const schemas = {};
|
||||||
|
for (const messageName of messages.keys()) {
|
||||||
|
if (reachable.has(messageName)) schemas[messageName] = schemaByName.get(messageName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceHash = createHash("sha256").update(source).digest("hex").slice(0, 16);
|
||||||
|
const namespaceName = namespace.path.join(".") || "root";
|
||||||
|
const fieldCount = Object.values(schemas).reduce(
|
||||||
|
(total, schema) => total + Object.keys(schema).length,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const generatedBlock = [
|
||||||
|
generatedStartMarker,
|
||||||
|
"// 此区块由 scripts/generate-match-schema.mjs 自动生成,请勿手动修改。",
|
||||||
|
`// 来源:src/utils/match.min.js(sha256: ${sourceHash})`,
|
||||||
|
`// 协议命名空间:${namespaceName};消息数:${Object.keys(schemas).length};字段数:${fieldCount}`,
|
||||||
|
"",
|
||||||
|
`export const ServerMessageType = ${formatJsValue(serverMessageType)};`,
|
||||||
|
"",
|
||||||
|
`export const ClientMessageType = ${formatJsValue(clientMessageType)};`,
|
||||||
|
"",
|
||||||
|
`const SCHEMAS = ${formatJsValue(schemas)};`,
|
||||||
|
generatedEndMarker,
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
return {
|
||||||
|
generatedBlock,
|
||||||
|
messageCount: Object.keys(schemas).length,
|
||||||
|
fieldCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const unknownArgs = args.filter((arg) => arg !== "--check");
|
||||||
|
if (unknownArgs.length > 0) {
|
||||||
|
throw new Error(`未知参数:${unknownArgs.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = readFileSync(sourcePath, "utf8");
|
||||||
|
const { generatedBlock, messageCount, fieldCount } = createGeneratedSource(source);
|
||||||
|
const runtimeSource = readFileSync(runtimePath, "utf8");
|
||||||
|
const startIndex = runtimeSource.indexOf(generatedStartMarker);
|
||||||
|
const endMarkerIndex = runtimeSource.indexOf(generatedEndMarker);
|
||||||
|
if (startIndex < 0 || endMarkerIndex < startIndex) {
|
||||||
|
throw new Error("matchProtocol.js 缺少协议生成区块标记");
|
||||||
|
}
|
||||||
|
const endIndex = endMarkerIndex + generatedEndMarker.length;
|
||||||
|
const currentBlock = runtimeSource.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
if (args.includes("--check")) {
|
||||||
|
if (currentBlock !== generatedBlock) {
|
||||||
|
throw new Error(
|
||||||
|
"matchProtocol.js 的生成区块与 match.min.js 不同步,请运行 npm run proto:generate"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log(`[match-schema] 同步校验通过:${messageCount} 个消息,${fieldCount} 个字段`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentBlock === generatedBlock) {
|
||||||
|
console.log(`[match-schema] 无需更新:${messageCount} 个消息,${fieldCount} 个字段`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextRuntimeSource =
|
||||||
|
runtimeSource.slice(0, startIndex) + generatedBlock + runtimeSource.slice(endIndex);
|
||||||
|
writeFileSync(runtimePath, nextRuntimeSource, "utf8");
|
||||||
|
console.log(`[match-schema] 已生成:${messageCount} 个消息,${fieldCount} 个字段`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
main();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[match-schema] ${error.message}`);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
@@ -286,6 +286,14 @@
|
|||||||
{
|
{
|
||||||
"type" : "file",
|
"type" : "file",
|
||||||
"value" : "static/tab-mall.png"
|
"value" : "static/tab-mall.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type" : "folder",
|
||||||
|
"value" : "static/training-home"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type" : "folder",
|
||||||
|
"value" : "static/training-difficulty-design"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,43 +4,43 @@ export const trainingHomeWeekSchedule = [
|
|||||||
key: "mon",
|
key: "mon",
|
||||||
label: "周一",
|
label: "周一",
|
||||||
status: "done",
|
status: "done",
|
||||||
icon: "/pages/training/static/training-home/done.png",
|
icon: "https://static.shelingxingqiu.com/shootmini/static/training-home/done.png",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "tue",
|
key: "tue",
|
||||||
label: "周二",
|
label: "周二",
|
||||||
status: "done",
|
status: "done",
|
||||||
icon: "/pages/training/static/training-home/done.png",
|
icon: "https://static.shelingxingqiu.com/shootmini/static/training-home/done.png",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "wed",
|
key: "wed",
|
||||||
label: "周三",
|
label: "周三",
|
||||||
status: "missed",
|
status: "missed",
|
||||||
icon: "/pages/training/static/training-home/missed.png",
|
icon: "https://static.shelingxingqiu.com/shootmini/static/training-home/missed.png",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "thu",
|
key: "thu",
|
||||||
label: "周四",
|
label: "周四",
|
||||||
status: "missed",
|
status: "missed",
|
||||||
icon: "/pages/training/static/training-home/missed.png",
|
icon: "https://static.shelingxingqiu.com/shootmini/static/training-home/missed.png",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "fri",
|
key: "fri",
|
||||||
label: "周五",
|
label: "周五",
|
||||||
status: "done",
|
status: "done",
|
||||||
icon: "/pages/training/static/training-home/done.png",
|
icon: "https://static.shelingxingqiu.com/shootmini/static/training-home/done.png",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "sat",
|
key: "sat",
|
||||||
label: "周六",
|
label: "周六",
|
||||||
status: "done",
|
status: "done",
|
||||||
icon: "/pages/training/static/training-home/done.png",
|
icon: "https://static.shelingxingqiu.com/shootmini/static/training-home/done.png",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "sun",
|
key: "sun",
|
||||||
label: "周日",
|
label: "周日",
|
||||||
status: "missed",
|
status: "missed",
|
||||||
icon: "/pages/training/static/training-home/missed.png",
|
icon: "https://static.shelingxingqiu.com/shootmini/static/training-home/missed.png",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ export const trainingHomeModes = [
|
|||||||
key: "endurance",
|
key: "endurance",
|
||||||
title: "耐力训练",
|
title: "耐力训练",
|
||||||
progressText: "当前进度 LV5 >",
|
progressText: "当前进度 LV5 >",
|
||||||
icon: "/pages/training/static/training-home/img_3.png",
|
icon: "https://static.shelingxingqiu.com/shootmini/static/training-home/img_3.png",
|
||||||
recommended: true,
|
recommended: true,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
@@ -81,7 +81,7 @@ export const trainingHomeModes = [
|
|||||||
key: "precision",
|
key: "precision",
|
||||||
title: "精准训练",
|
title: "精准训练",
|
||||||
progressText: "当前进度 LV3 >",
|
progressText: "当前进度 LV3 >",
|
||||||
icon: "/pages/training/static/training-home/img_4.png",
|
icon: "https://static.shelingxingqiu.com/shootmini/static/training-home/img_4.png",
|
||||||
recommended: false,
|
recommended: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
@@ -89,7 +89,7 @@ export const trainingHomeModes = [
|
|||||||
key: "rhythm",
|
key: "rhythm",
|
||||||
title: "节奏训练",
|
title: "节奏训练",
|
||||||
progressText: "当前进度 LV6 >",
|
progressText: "当前进度 LV6 >",
|
||||||
icon: "/pages/training/static/training-home/img_5.png",
|
icon: "https://static.shelingxingqiu.com/shootmini/static/training-home/img_5.png",
|
||||||
recommended: false,
|
recommended: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
@@ -97,7 +97,7 @@ export const trainingHomeModes = [
|
|||||||
key: "power",
|
key: "power",
|
||||||
title: "力量训练",
|
title: "力量训练",
|
||||||
progressText: "Coming! LV10",
|
progressText: "Coming! LV10",
|
||||||
icon: "/pages/training/static/training-home/img_6.png",
|
icon: "https://static.shelingxingqiu.com/shootmini/static/training-home/img_6.png",
|
||||||
recommended: false,
|
recommended: false,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const props = defineProps({
|
|||||||
<Avatar :src="user.avatar" :rankLvl="user.rankLvl" :size="45" />
|
<Avatar :src="user.avatar" :rankLvl="user.rankLvl" :size="45" />
|
||||||
<view>
|
<view>
|
||||||
<text>{{ user.nickName }}</text>
|
<text>{{ user.nickName }}</text>
|
||||||
<text>{{ user.lvlName }}</text>
|
<!-- <text>{{ user.lvlName }}</text> -->
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view @click="onClose">
|
<view @click="onClose">
|
||||||
|
|||||||
@@ -85,8 +85,8 @@ onBeforeUnmount(() => {
|
|||||||
class="score-item-bg"
|
class="score-item-bg"
|
||||||
:src="
|
:src="
|
||||||
isLowScore(arrows[index])
|
isLowScore(arrows[index])
|
||||||
? '../static/training-difficulty-design/block-gray.png'
|
? 'https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/block-gray.png'
|
||||||
: '../static/training-difficulty-design/block-gold.png'
|
: 'https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/block-gold.png'
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<text
|
<text
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const displayArrows = computed(() => {
|
|||||||
:key="index"
|
:key="index"
|
||||||
class="score-card"
|
class="score-card"
|
||||||
>
|
>
|
||||||
<image class="score-card-bg" :src="isLowScore(arrow)?'../static/training-difficulty-design/block-gray.png':'../static/training-difficulty-design/block-gold.png'"></image>
|
<image class="score-card-bg" :src="isLowScore(arrow)?'https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/block-gray.png':'https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/block-gold.png'"></image>
|
||||||
<text
|
<text
|
||||||
class="score-value"
|
class="score-value"
|
||||||
:class="{ 'score-value--low': isLowScore(arrow) }"
|
:class="{ 'score-value--low': isLowScore(arrow) }"
|
||||||
|
|||||||
@@ -319,9 +319,9 @@ const calories = computed(
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view :class="['result-mask', showPanel ? 'result-mask--show' : 'result-mask--hide']">
|
<view :class="['result-mask', showPanel ? 'result-mask--show' : 'result-mask--hide']">
|
||||||
<image class="hero-glow" src="../static/training-difficulty-design/result-bg.png" mode="widthFix" />
|
<image class="hero-glow" src="https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/result-bg.png" mode="widthFix" />
|
||||||
<view class="result-title">
|
<view class="result-title">
|
||||||
<image class="result-title-bg" src="../static/training-difficulty-design/result-t-bg.png" mode="widthFix" />
|
<image class="result-title-bg" src="https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/result-t-bg.png" mode="widthFix" />
|
||||||
<view class="result-title-text">Lv{{ resultDifficultyLevel }}</view>
|
<view class="result-title-text">Lv{{ resultDifficultyLevel }}</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -330,7 +330,7 @@ const calories = computed(
|
|||||||
<view class="line-bottom"></view>
|
<view class="line-bottom"></view>
|
||||||
<view class="stats">
|
<view class="stats">
|
||||||
<view v-for="row in resultRows" :key="row.label" class="stat-row">
|
<view v-for="row in resultRows" :key="row.label" class="stat-row">
|
||||||
<image class="stat-bg" src="../static/training-difficulty-design/result-c-bg.png" mode="scaleToFill" />
|
<image class="stat-bg" src="https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/result-c-bg.png" mode="scaleToFill" />
|
||||||
<view class="stat-cell">
|
<view class="stat-cell">
|
||||||
<text class="stat-label">{{ row.label }}</text>
|
<text class="stat-label">{{ row.label }}</text>
|
||||||
<view class="stat-value">
|
<view class="stat-value">
|
||||||
@@ -345,13 +345,13 @@ const calories = computed(
|
|||||||
<text>{{ row.delta > 0 ? "+" : "-" }}{{ row.deltaText }}</text>
|
<text>{{ row.delta > 0 ? "+" : "-" }}{{ row.deltaText }}</text>
|
||||||
<text v-if="row.deltaUnit" class="stat-unit">{{ row.deltaUnit }}</text>
|
<text v-if="row.deltaUnit" class="stat-unit">{{ row.deltaUnit }}</text>
|
||||||
<image class="trend-icon" :class="{ 'trend-icon--down': row.delta < 0 }"
|
<image class="trend-icon" :class="{ 'trend-icon--down': row.delta < 0 }"
|
||||||
src="../static/training-difficulty-design/result-up.png" mode="widthFix" />
|
src="https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/result-up.png" mode="widthFix" />
|
||||||
</view>
|
</view>
|
||||||
<view v-else class="stat-value">--</view>
|
<view v-else class="stat-value">--</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="stat-row">
|
<view class="stat-row">
|
||||||
<image class="stat-bg" src="../static/training-difficulty-design/result-c-bg.png" mode="scaleToFill" />
|
<image class="stat-bg" src="https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/result-c-bg.png" mode="scaleToFill" />
|
||||||
<view class="stat-cell">
|
<view class="stat-cell">
|
||||||
<text class="stat-label">消耗卡路里</text>
|
<text class="stat-label">消耗卡路里</text>
|
||||||
<view class="stat-value">
|
<view class="stat-value">
|
||||||
@@ -363,7 +363,7 @@ const calories = computed(
|
|||||||
<view class="stat-cell stat-cell--compare">
|
<view class="stat-cell stat-cell--compare">
|
||||||
<view class="stat-value">
|
<view class="stat-value">
|
||||||
<image v-for="index in 3" :key="index" class="rice-icon"
|
<image v-for="index in 3" :key="index" class="rice-icon"
|
||||||
src="../static/training-difficulty-design/result-rice.png" mode="widthFix" />
|
src="https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/result-rice.png" mode="widthFix" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -371,15 +371,15 @@ const calories = computed(
|
|||||||
|
|
||||||
<view class="actions">
|
<view class="actions">
|
||||||
<view class="action-item" @click="() => (showBowData = true)">
|
<view class="action-item" @click="() => (showBowData = true)">
|
||||||
<image class="action-icon" src="../static/training-difficulty-design/result-icon-1.png" mode="widthFix" />
|
<image class="action-icon" src="https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/result-icon-1.png" mode="widthFix" />
|
||||||
<text>查看靶纸</text>
|
<text>查看靶纸</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="action-item" @click="() => (showComment = true)">
|
<view class="action-item" @click="() => (showComment = true)">
|
||||||
<image class="action-icon" src="../static/training-difficulty-design/result-icon-2.png" mode="widthFix" />
|
<image class="action-icon" src="https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/result-icon-2.png" mode="widthFix" />
|
||||||
<text>教练点评</text>
|
<text>教练点评</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="action-item" @click="onClickShare">
|
<view class="action-item" @click="onClickShare">
|
||||||
<image class="action-icon" src="../static/training-difficulty-design/result-icon-3.png" mode="widthFix" />
|
<image class="action-icon" src="https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/result-icon-3.png" mode="widthFix" />
|
||||||
<text>分享成绩</text>
|
<text>分享成绩</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -54,10 +54,14 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const trainingTitleIconMap = Object.freeze({
|
const trainingTitleIconMap = Object.freeze({
|
||||||
base: "../static/training-difficulty-design/text-icon-jcxl.png",
|
base:
|
||||||
precision: "../static/training-difficulty-design/text-icon-jingzxl.png",
|
"https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/text-icon-jcxl.png",
|
||||||
rhythm: "../static/training-difficulty-design/text-icon-jzxl.png",
|
precision:
|
||||||
endurance: "../static/training-difficulty-design/text-icon-nlxl.png",
|
"https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/text-icon-jingzxl.png",
|
||||||
|
rhythm:
|
||||||
|
"https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/text-icon-jzxl.png",
|
||||||
|
endurance:
|
||||||
|
"https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/text-icon-nlxl.png",
|
||||||
});
|
});
|
||||||
const trainingTitleIcon = computed(
|
const trainingTitleIcon = computed(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ onBeforeUnmount(() => {
|
|||||||
<view class="test-area">
|
<view class="test-area">
|
||||||
<image
|
<image
|
||||||
class="text-bg"
|
class="text-bg"
|
||||||
src="../static/training-difficulty-design/par-bg.png"
|
src="https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/par-bg.png"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
|
||||||
const lockedBadgeBackground =
|
const lockedBadgeBackground =
|
||||||
"../static/training-difficulty-design/unlock.svg";
|
"https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/unlock.svg";
|
||||||
const unlockedBadgeBackground =
|
const unlockedBadgeBackground =
|
||||||
"../static/training-difficulty-design/lock.svg";
|
"https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/lock.svg";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
node: {
|
node: {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const previewLines = computed(() => {
|
|||||||
<view class="difficulty-preview">
|
<view class="difficulty-preview">
|
||||||
<image
|
<image
|
||||||
class="difficulty-preview__bg"
|
class="difficulty-preview__bg"
|
||||||
src="../static/training-difficulty-design/text.png"
|
src="https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/text.png"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
<view class="difficulty-preview__content">
|
<view class="difficulty-preview__content">
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const handleClick = () => {
|
|||||||
>
|
>
|
||||||
<image
|
<image
|
||||||
class="difficulty-start__button"
|
class="difficulty-start__button"
|
||||||
src="../static/training-difficulty-design/btn.png"
|
src="https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/btn.png"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -784,7 +784,7 @@ onUnload(() => {
|
|||||||
v-for="connector in difficultyConnectors"
|
v-for="connector in difficultyConnectors"
|
||||||
:key="connector.id"
|
:key="connector.id"
|
||||||
class="difficulty-page__connector"
|
class="difficulty-page__connector"
|
||||||
src="./static/training-difficulty-design/jiantou.png"
|
src="https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/jiantou.png"
|
||||||
mode="aspectFit"
|
mode="aspectFit"
|
||||||
:style="connector"
|
:style="connector"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import Container from "@/components/Container.vue";
|
|||||||
import TargetPicker from "@/components/TargetPicker.vue";
|
import TargetPicker from "@/components/TargetPicker.vue";
|
||||||
import { getPersonalTrainingAPI } from "@/apis";
|
import { getPersonalTrainingAPI } from "@/apis";
|
||||||
|
|
||||||
const checkedIcon = "./static/training-home/done.png";
|
const checkedIcon =
|
||||||
const missedIcon = "./static/training-home/missed.png";
|
"https://static.shelingxingqiu.com/shootmini/static/training-home/done.png";
|
||||||
|
const missedIcon =
|
||||||
|
"https://static.shelingxingqiu.com/shootmini/static/training-home/missed.png";
|
||||||
// 后端训练项目 id 与难度页 mode 参数的映射关系。
|
// 后端训练项目 id 与难度页 mode 参数的映射关系。
|
||||||
const trainingModeRouteMap = {
|
const trainingModeRouteMap = {
|
||||||
base: "basic",
|
base: "basic",
|
||||||
@@ -17,18 +19,25 @@ const trainingModeRouteMap = {
|
|||||||
};
|
};
|
||||||
// 训练项目卡片右侧主图标。
|
// 训练项目卡片右侧主图标。
|
||||||
const trainingModeIconMap = {
|
const trainingModeIconMap = {
|
||||||
base_bow: "./static/training-home/img_22.png",
|
base_bow:
|
||||||
bow: "./static/training-home/img_3.png",
|
"https://static.shelingxingqiu.com/shootmini/static/training-home/img_22.png",
|
||||||
target: "./static/training-home/img_4.png",
|
bow: "https://static.shelingxingqiu.com/shootmini/static/training-home/img_3.png",
|
||||||
wave: "./static/training-home/img_5.png",
|
target:
|
||||||
muscle: "./static/training-home/img_6.png",
|
"https://static.shelingxingqiu.com/shootmini/static/training-home/img_4.png",
|
||||||
|
wave: "https://static.shelingxingqiu.com/shootmini/static/training-home/img_5.png",
|
||||||
|
muscle:
|
||||||
|
"https://static.shelingxingqiu.com/shootmini/static/training-home/img_6.png",
|
||||||
};
|
};
|
||||||
// 训练项目卡片标题图,按接口 id 映射本地资源。
|
// 训练项目卡片标题图,按接口 id 映射 CDN 资源。
|
||||||
const trainingModeTitleImageMap = {
|
const trainingModeTitleImageMap = {
|
||||||
endurance: "./static/training-home/nailixunlian.png",
|
endurance:
|
||||||
precision: "./static/training-home/jingzhunxunlian.png",
|
"https://static.shelingxingqiu.com/shootmini/static/training-home/nailixunlian.png",
|
||||||
rhythm: "./static/training-home/jiezouxunlian.png",
|
precision:
|
||||||
strength: "./static/training-home/liliangxulian.png",
|
"https://static.shelingxingqiu.com/shootmini/static/training-home/jingzhunxunlian.png",
|
||||||
|
rhythm:
|
||||||
|
"https://static.shelingxingqiu.com/shootmini/static/training-home/jiezouxunlian.png",
|
||||||
|
strength:
|
||||||
|
"https://static.shelingxingqiu.com/shootmini/static/training-home/liliangxulian.png",
|
||||||
};
|
};
|
||||||
const defaultWeekDays = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
|
const defaultWeekDays = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
|
||||||
const defaultRadarDimensions = [
|
const defaultRadarDimensions = [
|
||||||
@@ -360,12 +369,12 @@ onShow(async () => {
|
|||||||
<view class="stats-card-bg"></view>
|
<view class="stats-card-bg"></view>
|
||||||
<image
|
<image
|
||||||
class="stats-quote stats-quote-left"
|
class="stats-quote stats-quote-left"
|
||||||
src="./static/training-home/img_17.png"
|
src="https://static.shelingxingqiu.com/shootmini/static/training-home/img_17.png"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
<image
|
<image
|
||||||
class="stats-quote stats-quote-right"
|
class="stats-quote stats-quote-right"
|
||||||
src="./static/training-home/img_16.png"
|
src="https://static.shelingxingqiu.com/shootmini/static/training-home/img_16.png"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
<view class="stats-grid">
|
<view class="stats-grid">
|
||||||
@@ -440,7 +449,7 @@ onShow(async () => {
|
|||||||
<view class="record-bubble" @click="openTrainingRecord">
|
<view class="record-bubble" @click="openTrainingRecord">
|
||||||
<image
|
<image
|
||||||
class="record-bubble-bg"
|
class="record-bubble-bg"
|
||||||
src="./static/training-home/img_28.png"
|
src="https://static.shelingxingqiu.com/shootmini/static/training-home/img_28.png"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
<view class="record-bubble-copy">
|
<view class="record-bubble-copy">
|
||||||
@@ -451,7 +460,7 @@ onShow(async () => {
|
|||||||
<text class="record-sub-text">我的训练记录</text>
|
<text class="record-sub-text">我的训练记录</text>
|
||||||
<image
|
<image
|
||||||
class="record-arrow"
|
class="record-arrow"
|
||||||
src="./static/training-home/img_7.png"
|
src="https://static.shelingxingqiu.com/shootmini/static/training-home/img_7.png"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
@@ -479,7 +488,7 @@ onShow(async () => {
|
|||||||
<image
|
<image
|
||||||
class="radar-grid-image"
|
class="radar-grid-image"
|
||||||
:style="radarFigureStyle"
|
:style="radarFigureStyle"
|
||||||
src="./static/training-home/img_19.png"
|
src="https://static.shelingxingqiu.com/shootmini/static/training-home/img_19.png"
|
||||||
/>
|
/>
|
||||||
<canvas
|
<canvas
|
||||||
:canvas-id="trainingRadarCanvasId"
|
:canvas-id="trainingRadarCanvasId"
|
||||||
@@ -491,7 +500,7 @@ onShow(async () => {
|
|||||||
/>
|
/>
|
||||||
<image
|
<image
|
||||||
class="radar-mascot"
|
class="radar-mascot"
|
||||||
src="./static/training-home/img_21.png"
|
src="https://static.shelingxingqiu.com/shootmini/static/training-home/img_21.png"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
@@ -501,7 +510,7 @@ onShow(async () => {
|
|||||||
<view class="featured-card" @click="openRoutineTraining">
|
<view class="featured-card" @click="openRoutineTraining">
|
||||||
<image
|
<image
|
||||||
class="featured-card-bg"
|
class="featured-card-bg"
|
||||||
src="./static/training-home/img_22.png"
|
src="https://static.shelingxingqiu.com/shootmini/static/training-home/img_22.png"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
<view class="featured-card-mask"></view>
|
<view class="featured-card-mask"></view>
|
||||||
|
|||||||
@@ -953,7 +953,7 @@ onBeforeUnmount(() => {
|
|||||||
<view class="bat-text-big-box">
|
<view class="bat-text-big-box">
|
||||||
<image
|
<image
|
||||||
class="dao-icon"
|
class="dao-icon"
|
||||||
src="./static/training-difficulty-design/dao-icon.png"
|
src="https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/dao-icon.png"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
<view v-if="trainingCopy" class="bat-text-box">
|
<view v-if="trainingCopy" class="bat-text-box">
|
||||||
@@ -998,7 +998,7 @@ onBeforeUnmount(() => {
|
|||||||
<view class="btn-box">
|
<view class="btn-box">
|
||||||
<image
|
<image
|
||||||
class="btn-box-bg"
|
class="btn-box-bg"
|
||||||
src="./static/training-difficulty-design/par-star.png"
|
src="https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/par-star.png"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
<button class="btn" @click="onReady">准备好了,开始练习</button>
|
<button class="btn" @click="onReady">准备好了,开始练习</button>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 838 B |
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="55px" height="55px" viewBox="0 0 55 55" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>编组 11</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<g id="训练难度展示" transform="translate(-160.000000, -478.000000)">
|
|
||||||
<g id="编组-2备份" transform="translate(154.000000, 472.000000)">
|
|
||||||
<g id="编组-11" transform="translate(6.000000, 6.000000)">
|
|
||||||
<circle id="椭圆形" fill="#CACACA" cx="27.5" cy="27.5" r="27.5"></circle>
|
|
||||||
<circle id="椭圆形" fill="#FFFFFF" cx="27.5" cy="26" r="25"></circle>
|
|
||||||
<circle id="椭圆形" fill="#5E5E5E" cx="27.5" cy="26" r="22"></circle>
|
|
||||||
<circle id="椭圆形" fill="#17B6F2" cx="27.5" cy="26" r="19.5"></circle>
|
|
||||||
<circle id="椭圆形" fill="#FFC2C2" cx="27.5" cy="26" r="17"></circle>
|
|
||||||
<circle id="椭圆形" fill="#FF1F33" opacity="0.800000012" cx="27.5" cy="26" r="14.5"></circle>
|
|
||||||
<circle id="椭圆形" fill="#FED847" cx="27.5" cy="26" r="13"></circle>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 338 KiB |
|
Before Width: | Height: | Size: 849 B |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 719 B |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="56px" height="55px" viewBox="0 0 56 55" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>编组 9</title>
|
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<g id="训练难度展示" transform="translate(-60.000000, -313.000000)">
|
|
||||||
<g id="编组-2备份" transform="translate(54.500000, 307.000000)">
|
|
||||||
<g id="编组-9" transform="translate(5.500000, 6.000000)">
|
|
||||||
<circle id="椭圆形" fill="#CACACA" cx="28" cy="27.5" r="27.5"></circle>
|
|
||||||
<circle id="椭圆形" fill="#FFFFFF" cx="28" cy="26" r="25"></circle>
|
|
||||||
<circle id="椭圆形" fill="#5E5E5E" cx="28" cy="26" r="22"></circle>
|
|
||||||
<circle id="椭圆形" fill="#808080" cx="28" cy="26" r="19.5"></circle>
|
|
||||||
<circle id="椭圆形" fill="#FFFFFF" cx="28" cy="26" r="17"></circle>
|
|
||||||
<circle id="椭圆形" fill="#8C8C8C" cx="28" cy="26" r="14.5"></circle>
|
|
||||||
<circle id="椭圆形" fill="#DBDBDB" cx="28" cy="26" r="13"></circle>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1011 B |
|
Before Width: | Height: | Size: 184 B |
|
Before Width: | Height: | Size: 192 B |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 468 B |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 173 B |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 351 B |
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -5,6 +5,11 @@ const { Reader, Writer } = protobuf;
|
|||||||
// 比赛服 protobuf 协议适配层:
|
// 比赛服 protobuf 协议适配层:
|
||||||
// 小程序环境不支持 protobufjs 反射模式里的动态 Function codegen,
|
// 小程序环境不支持 protobufjs 反射模式里的动态 Function codegen,
|
||||||
// 所以这里使用 minimal Reader/Writer 做静态字段解码和客户端消息编码。
|
// 所以这里使用 minimal Reader/Writer 做静态字段解码和客户端消息编码。
|
||||||
|
// <match-schema-generated>
|
||||||
|
// 此区块由 scripts/generate-match-schema.mjs 自动生成,请勿手动修改。
|
||||||
|
// 来源:src/utils/match.min.js(sha256: b4f272aad40fb951)
|
||||||
|
// 协议命名空间:rpc;消息数:12;字段数:137
|
||||||
|
|
||||||
export const ServerMessageType = {
|
export const ServerMessageType = {
|
||||||
SERVER_MSG_UNKNOWN: 0,
|
SERVER_MSG_UNKNOWN: 0,
|
||||||
SERVER_MSG_MATCH_READY: 1,
|
SERVER_MSG_MATCH_READY: 1,
|
||||||
@@ -21,6 +26,7 @@ export const ServerMessageType = {
|
|||||||
SERVER_MSG_HEARTBEAT: 12,
|
SERVER_MSG_HEARTBEAT: 12,
|
||||||
SERVER_MSG_PRACTICE_END: 13,
|
SERVER_MSG_PRACTICE_END: 13,
|
||||||
SERVER_MSG_SYNC_PRACTICE_INFO: 14,
|
SERVER_MSG_SYNC_PRACTICE_INFO: 14,
|
||||||
|
SERVER_MSG_SYNC_MATCH_INFO: 15,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ClientMessageType = {
|
export const ClientMessageType = {
|
||||||
@@ -30,18 +36,9 @@ export const ClientMessageType = {
|
|||||||
CLIENT_MSG_ACK: 3,
|
CLIENT_MSG_ACK: 3,
|
||||||
CLIENT_MSG_LEAVE: 4,
|
CLIENT_MSG_LEAVE: 4,
|
||||||
CLIENT_MSG_SYNC_PRACTICE_INFO: 5,
|
CLIENT_MSG_SYNC_PRACTICE_INFO: 5,
|
||||||
|
CLIENT_MSG_SYNC_MATCH_INFO: 6,
|
||||||
};
|
};
|
||||||
|
|
||||||
// protobufjs 的 enum 默认是 name -> value,这里反转成 value -> name 用于日志打印。
|
|
||||||
const ServerMessageTypeNameByValue = Object.keys(ServerMessageType).reduce(
|
|
||||||
(result, name) => {
|
|
||||||
result[ServerMessageType[name]] = name;
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 当前只需要解码比赛服下发的字段并打印,字段表按后端 proto 定义维护。
|
|
||||||
const SCHEMAS = {
|
const SCHEMAS = {
|
||||||
MatchShoot: {
|
MatchShoot: {
|
||||||
1: { name: "player_id", kind: "int64" },
|
1: { name: "player_id", kind: "int64" },
|
||||||
@@ -210,12 +207,7 @@ const SCHEMAS = {
|
|||||||
17: { name: "win_team", kind: "int32" },
|
17: { name: "win_team", kind: "int32" },
|
||||||
18: { name: "mvp", kind: "message", type: "PlayerFull" },
|
18: { name: "mvp", kind: "message", type: "PlayerFull" },
|
||||||
19: { name: "room_id", kind: "string" },
|
19: { name: "room_id", kind: "string" },
|
||||||
20: {
|
20: { name: "result_list", kind: "message", type: "PlayerMatchResult", repeated: true },
|
||||||
name: "result_list",
|
|
||||||
kind: "message",
|
|
||||||
type: "PlayerMatchResult",
|
|
||||||
repeated: true,
|
|
||||||
},
|
|
||||||
21: { name: "timeout_time", kind: "int64" },
|
21: { name: "timeout_time", kind: "int64" },
|
||||||
22: { name: "target_type", kind: "int32" },
|
22: { name: "target_type", kind: "int32" },
|
||||||
23: { name: "event_type", kind: "int32" },
|
23: { name: "event_type", kind: "int32" },
|
||||||
@@ -233,6 +225,16 @@ const SCHEMAS = {
|
|||||||
7: { name: "sequence", kind: "int64" },
|
7: { name: "sequence", kind: "int64" },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
// </match-schema-generated>
|
||||||
|
|
||||||
|
// protobufjs 的 enum 默认是 name -> value,这里反转成 value -> name 用于日志打印。
|
||||||
|
const ServerMessageTypeNameByValue = Object.keys(ServerMessageType).reduce(
|
||||||
|
(result, name) => {
|
||||||
|
result[ServerMessageType[name]] = name;
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
// 小程序 websocket 收到的 data 可能是 ArrayBuffer、TypedArray 或字符串,这里统一成 Uint8Array。
|
// 小程序 websocket 收到的 data 可能是 ArrayBuffer、TypedArray 或字符串,这里统一成 Uint8Array。
|
||||||
function toUint8Array(data) {
|
function toUint8Array(data) {
|
||||||
|
|||||||