Compare commits
8
Commits
45bc324f70
...
fix-audio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88febb92e5 | ||
|
|
c8533de5cf | ||
|
|
74bfd7a327 | ||
|
|
bbfa35e871 | ||
|
|
469487ba73 | ||
|
|
b9bbaba77f | ||
|
|
11eeb98b74 | ||
|
|
96b64e4187 |
+3
-1
@@ -376,9 +376,11 @@ export const readyGameAPI = (battleId) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const simulShootAPI = (device_id, x, y) => {
|
||||
export const simulShootAPI = (device_id, x, y, targetType = 40) => {
|
||||
const data = {
|
||||
device_id,
|
||||
// 模拟射箭仅支持 20cm、40cm 靶纸,未传或传入无效值时默认使用 40cm。
|
||||
targetType: Number(targetType) === 20 ? 20 : 40,
|
||||
};
|
||||
if (x !== undefined && y !== undefined) {
|
||||
data.x = x;
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
export const AUDIO_INTERRUPTION_BEGIN_EVENT = "audio-interruption-begin";
|
||||
export const AUDIO_INTERRUPTION_END_EVENT = "audio-interruption-end";
|
||||
export const RHYTHM_SHOOT_WINDOW_AUDIO_KEY = "请射箭";
|
||||
export const STABILITY_START_AUDIO_KEY =
|
||||
"请在计时结束前能量条达到百分百";
|
||||
export const STABILITY_ENERGY_50_AUDIO_KEY = "加油!到达50%啦";
|
||||
export const STABILITY_ENERGY_70_AUDIO_KEY =
|
||||
"70%啦!还差一点,胜利就在眼前";
|
||||
|
||||
const TRAINING_START_AUDIO_KEY_MAP = Object.freeze({
|
||||
base: "请于计时前完成指定环数对应箭量",
|
||||
endurance: "请于计时前完成累计环数和箭量",
|
||||
precision: "请射箭命中高亮区域",
|
||||
rhythm: "请在读条推进到标记区间内射箭",
|
||||
stability: "请在计时结束前能量条达到百分百",
|
||||
});
|
||||
|
||||
export const getTrainingStartAudioKey = (trainingType, fallback = "") =>
|
||||
@@ -41,6 +47,16 @@ export const getRhythmShotAudioKeys = (shootData) => {
|
||||
return [ringAudioKey, shootData.ok === true ? "Perfect" : "miss"];
|
||||
};
|
||||
|
||||
export const getStabilityShotAudioKeys = (shootData) => {
|
||||
if (!shootData || typeof shootData !== "object") return [];
|
||||
const ring = Number(shootData.ring);
|
||||
const ringAudioKey =
|
||||
Number.isFinite(ring) && ring > 0
|
||||
? `${shootData.ringX ? "X" : ring}环`
|
||||
: "未上靶";
|
||||
return [ringAudioKey];
|
||||
};
|
||||
|
||||
export const audioFils = {
|
||||
tententen: "https://static.shelingxingqiu.com/shootmini/static/audio/tententen.mp3",
|
||||
点击按钮: "https://static.shelingxingqiu.com/shootmini/static/audio/%E7%82%B9%E5%87%BB%E6%8C%89%E9%92%AE.mp3",
|
||||
@@ -159,6 +175,12 @@ export const audioFils = {
|
||||
"https://static.shelingxingqiu.com/attachment/2025-11-13/de7kzzllq0futwynso.mp3",
|
||||
练习开始:
|
||||
"https://static.shelingxingqiu.com/attachment/2025-11-14/de88w0lmmt43nnfmoi.mp3",
|
||||
[STABILITY_START_AUDIO_KEY]:
|
||||
"https://static.shelingxingqiu.com/shootaudio/%E8%AF%B7%E5%9C%A8%E8%AE%A1%E6%97%B6%E7%BB%93%E6%9D%9F%E5%89%8D%E8%83%BD%E9%87%8F%E6%9D%A1%E8%BE%BE%E5%88%B0%E7%99%BE%E5%88%86%E7%99%BE.MP3",
|
||||
[STABILITY_ENERGY_50_AUDIO_KEY]:
|
||||
"https://static.shelingxingqiu.com/shootmini/static/audio0820/%E5%8A%A0%E6%B2%B9%EF%BC%81%E5%88%B0%E8%BE%BE50%25%E5%95%A6.MP3",
|
||||
[STABILITY_ENERGY_70_AUDIO_KEY]:
|
||||
"https://static.shelingxingqiu.com/shootmini/static/audio0820/70%25%E5%95%A6%EF%BC%81%E8%BF%98%E5%B7%AE%E4%B8%80%E7%82%B9%EF%BC%8C%E8%83%9C%E5%88%A9%E5%B0%B1%E5%9C%A8%E7%9C%BC%E5%89%8D.MP3",
|
||||
请于计时前完成指定环数对应箭量:
|
||||
"https://static.shelingxingqiu.com/shootmini/static/audio0820/%E8%AF%B7%E4%BA%8E%E8%AE%A1%E6%97%B6%E5%89%8D%E5%AE%8C%E6%88%90%E6%8C%87%E5%AE%9A%E7%8E%AF%E6%95%B0%E5%AF%B9%E5%BA%94%E7%AE%AD%E9%87%8F.MP3",
|
||||
请于计时前完成累计环数和箭量:
|
||||
|
||||
@@ -51,6 +51,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
targetType: {
|
||||
type: [Number, String],
|
||||
default: 40,
|
||||
},
|
||||
targetRadius: {
|
||||
type: Number,
|
||||
default: 20,
|
||||
@@ -426,12 +430,19 @@ function getExperienceTipStyle(shot) {
|
||||
);
|
||||
}
|
||||
const simulShoot = async () => {
|
||||
if (device.value.deviceId) await simulShootAPI(device.value.deviceId);
|
||||
if (device.value.deviceId) {
|
||||
await simulShootAPI(
|
||||
device.value.deviceId,
|
||||
undefined,
|
||||
undefined,
|
||||
props.targetType
|
||||
);
|
||||
}
|
||||
};
|
||||
const simulShoot2 = async () => {
|
||||
if (device.value.deviceId) {
|
||||
const r1 = Math.random() > 0.5 ? 0.01 : 0.02;
|
||||
await simulShootAPI(device.value.deviceId, r1, r1);
|
||||
await simulShootAPI(device.value.deviceId, r1, r1, props.targetType);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -29,6 +29,10 @@ const props = defineProps({
|
||||
type: Number,
|
||||
default: 15,
|
||||
},
|
||||
targetType: {
|
||||
type: [Number, String],
|
||||
default: 40,
|
||||
},
|
||||
autoStart: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -105,7 +109,14 @@ async function onReceiveMessage(msg) {
|
||||
}
|
||||
|
||||
const simulShoot = async () => {
|
||||
if (device.value.deviceId) await simulShootAPI(device.value.deviceId);
|
||||
if (device.value.deviceId) {
|
||||
await simulShootAPI(
|
||||
device.value.deviceId,
|
||||
undefined,
|
||||
undefined,
|
||||
props.targetType
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
@@ -144,7 +155,6 @@ onBeforeUnmount(() => {
|
||||
</button>
|
||||
<view class="warnning-text">
|
||||
<block v-if="statusText">
|
||||
<text v-if="distance > 0">当前距离{{ distance }}米</text>
|
||||
<text>{{ statusText }}</text>
|
||||
</block>
|
||||
<block v-else>
|
||||
|
||||
@@ -24,7 +24,7 @@ const props = defineProps({
|
||||
|
||||
const getDisplayText = (arrow = {}) => {
|
||||
if (!arrow) return "";
|
||||
if (!arrow.ring) return "0";
|
||||
if (!arrow.ring) return props.trainingType === "stability" ? "-" : "0";
|
||||
return arrow.ringX ? "X" : String(arrow.ring);
|
||||
};
|
||||
|
||||
@@ -33,7 +33,8 @@ const isFailed = (arrow = {}) => {
|
||||
if (
|
||||
props.recordMode &&
|
||||
props.trainingType !== "precision" &&
|
||||
props.trainingType !== "rhythm"
|
||||
props.trainingType !== "rhythm" &&
|
||||
props.trainingType !== "stability"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,10 @@ import {
|
||||
import {
|
||||
getPrecisionShotAudioKeys,
|
||||
getRhythmShotAudioKeys,
|
||||
getStabilityShotAudioKeys,
|
||||
getTrainingStartAudioKey,
|
||||
STABILITY_ENERGY_50_AUDIO_KEY,
|
||||
STABILITY_ENERGY_70_AUDIO_KEY,
|
||||
} from "@/audioManager";
|
||||
import {
|
||||
normalizeId,
|
||||
@@ -220,6 +223,13 @@ function buildPracticeSyncMessage(message) {
|
||||
if (currentContext && practiceInfo.trainingType) {
|
||||
currentContext.trainingType = String(practiceInfo.trainingType).trim();
|
||||
}
|
||||
updateStabilityEnergyContext(practiceInfo, {
|
||||
fullSnapshot: true,
|
||||
detectCrossing: false,
|
||||
});
|
||||
updateStabilityShotContext(practiceInfo, {
|
||||
detectShot: false,
|
||||
});
|
||||
const matchId = normalizeId(
|
||||
pickField(message, "matchId", "match_id") ||
|
||||
practiceInfo.id ||
|
||||
@@ -378,12 +388,171 @@ function getShootResultAudioKeys(shootData) {
|
||||
return keys;
|
||||
}
|
||||
|
||||
function normalizeScoreSlot(value) {
|
||||
const scoreSlot = Number(value);
|
||||
return Number.isFinite(scoreSlot) && scoreSlot > 0 ? scoreSlot : 0;
|
||||
}
|
||||
|
||||
function clampEnergy(value, scoreSlot = 0) {
|
||||
const energy = Number(value);
|
||||
if (!Number.isFinite(energy)) return 0;
|
||||
const maxEnergy = normalizeScoreSlot(scoreSlot);
|
||||
return maxEnergy > 0
|
||||
? Math.min(maxEnergy, Math.max(0, energy))
|
||||
: Math.max(0, energy);
|
||||
}
|
||||
|
||||
function getEnergyPercent(energy, scoreSlot) {
|
||||
const maxEnergy = normalizeScoreSlot(scoreSlot);
|
||||
if (!maxEnergy) return 0;
|
||||
return Math.min(100, (clampEnergy(energy, maxEnergy) / maxEnergy) * 100);
|
||||
}
|
||||
|
||||
function getStabilityArrowCount(source = {}) {
|
||||
const candidates = [
|
||||
Array.isArray(source.details) ? source.details.length : NaN,
|
||||
Number(source.currentArrows),
|
||||
Number(source.totalArrows),
|
||||
].filter((value) => Number.isFinite(value) && value >= 0);
|
||||
return candidates.length > 0 ? Math.max(...candidates) : null;
|
||||
}
|
||||
|
||||
function updateStabilityShotContext(
|
||||
source = {},
|
||||
{ detectShot = false } = {}
|
||||
) {
|
||||
const trainingType = String(
|
||||
source.trainingType || currentContext?.trainingType || ""
|
||||
).trim();
|
||||
if (trainingType !== "stability" || !currentContext) return false;
|
||||
|
||||
const nextArrowCount = getStabilityArrowCount(source);
|
||||
if (nextArrowCount === null) return false;
|
||||
|
||||
const storedArrowCount = Number(currentContext.stabilityArrowCount);
|
||||
const previousArrowCount = Number.isFinite(storedArrowCount)
|
||||
? storedArrowCount
|
||||
: 0;
|
||||
const hasNewShot =
|
||||
detectShot && nextArrowCount > previousArrowCount;
|
||||
|
||||
if (nextArrowCount >= previousArrowCount) {
|
||||
currentContext.stabilityArrowCount = nextArrowCount;
|
||||
}
|
||||
return hasNewShot;
|
||||
}
|
||||
|
||||
// 稳定训练的阈值语音必须和 ACK 使用同一份判定结果,避免页面播放了
|
||||
// 里程碑语音但比赛服提前收到 ACK。能量跌破阈值后再次向上跨越可重复触发。
|
||||
function updateStabilityEnergyContext(
|
||||
source = {},
|
||||
{ fullSnapshot = false, detectCrossing = false } = {}
|
||||
) {
|
||||
const trainingType = String(
|
||||
source.trainingType || currentContext?.trainingType || ""
|
||||
).trim();
|
||||
if (trainingType !== "stability" || !currentContext) return "";
|
||||
|
||||
const hasCurrentEnergy = Object.prototype.hasOwnProperty.call(
|
||||
source,
|
||||
"currentEnergy"
|
||||
);
|
||||
if (!hasCurrentEnergy && !fullSnapshot) return "";
|
||||
|
||||
const previousScoreSlot = normalizeScoreSlot(
|
||||
currentContext.stabilityScoreSlot
|
||||
);
|
||||
const nextScoreSlot =
|
||||
normalizeScoreSlot(source.scoreSlot) || previousScoreSlot;
|
||||
const previousEnergy = clampEnergy(
|
||||
currentContext.stabilityEnergy,
|
||||
previousScoreSlot
|
||||
);
|
||||
const nextEnergy = clampEnergy(
|
||||
hasCurrentEnergy ? source.currentEnergy : 0,
|
||||
nextScoreSlot
|
||||
);
|
||||
const previousPercent = getEnergyPercent(
|
||||
previousEnergy,
|
||||
previousScoreSlot || nextScoreSlot
|
||||
);
|
||||
const nextPercent = getEnergyPercent(nextEnergy, nextScoreSlot);
|
||||
|
||||
currentContext.stabilityEnergy = nextEnergy;
|
||||
currentContext.stabilityScoreSlot = nextScoreSlot;
|
||||
|
||||
if (!detectCrossing) {
|
||||
currentContext.stabilityEnergy50Armed = nextPercent < 50;
|
||||
currentContext.stabilityEnergy70Armed = nextPercent < 70;
|
||||
return "";
|
||||
}
|
||||
|
||||
let energy50Armed =
|
||||
typeof currentContext.stabilityEnergy50Armed === "boolean"
|
||||
? currentContext.stabilityEnergy50Armed
|
||||
: previousPercent < 50;
|
||||
let energy70Armed =
|
||||
typeof currentContext.stabilityEnergy70Armed === "boolean"
|
||||
? currentContext.stabilityEnergy70Armed
|
||||
: previousPercent < 70;
|
||||
|
||||
if (nextPercent < 50) energy50Armed = true;
|
||||
if (nextPercent < 70) energy70Armed = true;
|
||||
|
||||
let milestoneAudioKey = "";
|
||||
if (energy70Armed && nextPercent >= 70) {
|
||||
milestoneAudioKey = STABILITY_ENERGY_70_AUDIO_KEY;
|
||||
energy70Armed = false;
|
||||
// 同一次直接跨过两个阈值时只播70%,不能在后续补播50%。
|
||||
energy50Armed = false;
|
||||
} else if (energy50Armed && nextPercent >= 50) {
|
||||
milestoneAudioKey = STABILITY_ENERGY_50_AUDIO_KEY;
|
||||
energy50Armed = false;
|
||||
}
|
||||
|
||||
currentContext.stabilityEnergy50Armed = energy50Armed;
|
||||
currentContext.stabilityEnergy70Armed = energy70Armed;
|
||||
return milestoneAudioKey;
|
||||
}
|
||||
|
||||
function getTestDistanceAudioKeys(shootData) {
|
||||
if (!shootData) return [];
|
||||
return [getDistanceCheckAudioKey(shootData)];
|
||||
}
|
||||
|
||||
function attachStabilityAudioContext(message, businessMessage) {
|
||||
const fullStabilitySnapshot = [
|
||||
ServerMessageType.SERVER_MSG_MATCH_START,
|
||||
ServerMessageType.SERVER_MSG_SHOT,
|
||||
ServerMessageType.SERVER_MSG_MATCH_END,
|
||||
ServerMessageType.SERVER_MSG_TIMEOUT,
|
||||
ServerMessageType.SERVER_MSG_PRACTICE_END,
|
||||
].includes(message.type);
|
||||
const stabilityMilestoneAudioKey = updateStabilityEnergyContext(
|
||||
businessMessage,
|
||||
{
|
||||
fullSnapshot: fullStabilitySnapshot,
|
||||
detectCrossing: message.type === ServerMessageType.SERVER_MSG_SHOT,
|
||||
}
|
||||
);
|
||||
const stabilityHasNewShot = updateStabilityShotContext(businessMessage, {
|
||||
detectShot: message.type === ServerMessageType.SERVER_MSG_SHOT,
|
||||
});
|
||||
if (businessMessage && stabilityMilestoneAudioKey) {
|
||||
businessMessage.stabilityMilestoneAudioKey = stabilityMilestoneAudioKey;
|
||||
}
|
||||
if (businessMessage) {
|
||||
businessMessage.stabilityHasNewShot = stabilityHasNewShot;
|
||||
}
|
||||
}
|
||||
|
||||
function getAckAudioKeys(message, businessMessage) {
|
||||
const stabilityMilestoneAudioKey = String(
|
||||
businessMessage?.stabilityMilestoneAudioKey || ""
|
||||
).trim();
|
||||
const stabilityHasNewShot =
|
||||
businessMessage?.stabilityHasNewShot === true;
|
||||
|
||||
switch (message.type) {
|
||||
case ServerMessageType.SERVER_MSG_MATCH_START:
|
||||
if (businessMessage?.isSecondHalfStart) return ["下半场开始"];
|
||||
@@ -419,6 +588,14 @@ function getAckAudioKeys(message, businessMessage) {
|
||||
if (trainingType === "rhythm") {
|
||||
return getRhythmShotAudioKeys(businessMessage?.shootData);
|
||||
}
|
||||
if (trainingType === "stability") {
|
||||
return [
|
||||
...(stabilityHasNewShot
|
||||
? getStabilityShotAudioKeys(businessMessage?.shootData)
|
||||
: []),
|
||||
stabilityMilestoneAudioKey,
|
||||
].filter(Boolean);
|
||||
}
|
||||
return getShootResultAudioKeys(businessMessage?.shootData);
|
||||
case ServerMessageType.SERVER_MSG_MATCH_END:
|
||||
return ["比赛结束"];
|
||||
@@ -880,6 +1057,8 @@ function handleMessage(data) {
|
||||
if (businessMessage?.matchId && currentContext) {
|
||||
currentContext.matchId = businessMessage.matchId;
|
||||
}
|
||||
// 阈值事件必须先于 ACK 判断生成,确保无 sequence 的状态推送也能播放。
|
||||
attachStabilityAudioContext(message, businessMessage);
|
||||
queueAckAfterAudio(message, businessMessage);
|
||||
emitBusinessMessage(businessMessage);
|
||||
}
|
||||
@@ -991,6 +1170,21 @@ export function connectMatchWebSocket(options = {}) {
|
||||
trainingType:
|
||||
normalizedTrainingType ||
|
||||
(isSameContext ? currentContext?.trainingType || "" : ""),
|
||||
stabilityEnergy: isSameContext
|
||||
? currentContext?.stabilityEnergy
|
||||
: undefined,
|
||||
stabilityScoreSlot: isSameContext
|
||||
? currentContext?.stabilityScoreSlot
|
||||
: undefined,
|
||||
stabilityEnergy50Armed: isSameContext
|
||||
? currentContext?.stabilityEnergy50Armed
|
||||
: undefined,
|
||||
stabilityEnergy70Armed: isSameContext
|
||||
? currentContext?.stabilityEnergy70Armed
|
||||
: undefined,
|
||||
stabilityArrowCount: isSameContext
|
||||
? currentContext?.stabilityArrowCount
|
||||
: undefined,
|
||||
meleeHalfRest: isSameContext
|
||||
? currentContext?.meleeHalfRest === true
|
||||
: false,
|
||||
|
||||
@@ -114,7 +114,7 @@ const createPractise = async (arrows) => {
|
||||
const result = await createPractiseAPI(
|
||||
arrows,
|
||||
120,
|
||||
1,
|
||||
2,
|
||||
device.value.deviceId
|
||||
);
|
||||
if (result) practiseId.value = result.id;
|
||||
@@ -384,7 +384,7 @@ const getResultTipSrc = (result = {}) => {
|
||||
<text>请完成以上步骤校准智能弓</text>
|
||||
</view>
|
||||
<ShootProgress v-if="step === 5" tips="请开始连续射箭" :start="start" />
|
||||
<TestDistance v-if="step === 3" :guide="false" />
|
||||
<TestDistance v-if="step === 3" :guide="false" :targetType="40" />
|
||||
<view
|
||||
class="user-row"
|
||||
v-if="step === 5"
|
||||
@@ -399,6 +399,8 @@ const getResultTipSrc = (result = {}) => {
|
||||
:totalRound="step === 5 ? total : 0"
|
||||
:scores="scores"
|
||||
:isSvip="isSvip"
|
||||
:targetType="40"
|
||||
stable-shot-effect
|
||||
/>
|
||||
<ScorePanel
|
||||
v-if="step === 5"
|
||||
|
||||
@@ -29,6 +29,7 @@ const { user, online } = storeToRefs(store);
|
||||
const title = ref("");
|
||||
const start = ref(null);
|
||||
const battleId = ref("");
|
||||
const targetType = ref(20);
|
||||
/** 对战模式:1=好友约战 2=排位赛,用于结算页跳转判断 */
|
||||
const way = ref(0);
|
||||
const currentRound = ref(1);
|
||||
@@ -254,6 +255,8 @@ watch(online, (newVal, oldVal) => {
|
||||
function recoverData(battleInfo, { force = false } = {}) {
|
||||
battleInfo = normalizeBattleInfo(battleInfo);
|
||||
if (!battleInfo) return;
|
||||
const nextTargetType = Number(battleInfo.targetType ?? battleInfo.target_type);
|
||||
if ([20, 40].includes(nextTargetType)) targetType.value = nextTargetType;
|
||||
if (battleInfo.status !== undefined) {
|
||||
battleEnded = [2, 4].includes(Number(battleInfo.status));
|
||||
}
|
||||
@@ -374,6 +377,7 @@ function onMatchSocketState(event) {
|
||||
}
|
||||
|
||||
onLoad(async (options) => {
|
||||
targetType.value = 20;
|
||||
const returnSnapshot = options.fromReturn ? takeMatchReturnSnapshot() : null;
|
||||
skipNextRestoreOnShow = false;
|
||||
if (returnSnapshot?.matchId) battleId.value = returnSnapshot.matchId;
|
||||
@@ -468,6 +472,7 @@ onShow(() => {
|
||||
:guide="false"
|
||||
:isBattle="true"
|
||||
:count="readyTime"
|
||||
:targetType="targetType"
|
||||
/>
|
||||
<ShootProgress
|
||||
:show="start"
|
||||
@@ -490,6 +495,7 @@ onShow(() => {
|
||||
:totalRound="12"
|
||||
:scores="playersScores.map((r) => r[user.id]).flat()"
|
||||
:isSvip="isCurrentUserSvip"
|
||||
:targetType="targetType"
|
||||
:stop="halfRest"
|
||||
stable-shot-effect
|
||||
/>
|
||||
|
||||
@@ -13,19 +13,10 @@ const { user } = storeToRefs(store);
|
||||
const arrows = ref([]);
|
||||
const isSvip = ref(false);
|
||||
const practiseDetail = ref({});
|
||||
const trainingTypeNameMap = Object.freeze({
|
||||
base: "基础训练",
|
||||
endurance: "耐力训练",
|
||||
precision: "精准训练",
|
||||
rhythm: "节奏训练",
|
||||
});
|
||||
|
||||
const trainingType = computed(() =>
|
||||
String(practiseDetail.value.trainingType || "").trim().toLowerCase()
|
||||
);
|
||||
const trainingTypeName = computed(
|
||||
() => trainingTypeNameMap[trainingType.value] || "自由训练"
|
||||
);
|
||||
const targetTypeText = computed(() => {
|
||||
const targetType = Number(practiseDetail.value.targetType);
|
||||
return targetType > 0 ? `${targetType}CM靶` : "";
|
||||
@@ -82,7 +73,7 @@ onLoad(async (options) => {
|
||||
</view> -->
|
||||
<view v-if="practiseDetail.id" class="practice-meta">
|
||||
<text v-if="targetTypeText">{{ targetTypeText }}</text>
|
||||
<text>{{ trainingTypeName }}</text>
|
||||
<text>{{ practiseDetail.trainingTypeText }}</text>
|
||||
<text v-if="difficultyText">{{ difficultyText }}</text>
|
||||
</view>
|
||||
<view :style="{ marginBottom: '20px' }">
|
||||
|
||||
+1
-12
@@ -15,17 +15,6 @@ const selectedIndex = ref(0);
|
||||
const matchList = ref([]);
|
||||
const battleList = ref([]);
|
||||
const practiseList = ref([]);
|
||||
const trainingTypeNameMap = Object.freeze({
|
||||
base: "基础训练",
|
||||
endurance: "耐力训练",
|
||||
precision: "精准训练",
|
||||
rhythm: "节奏训练",
|
||||
});
|
||||
|
||||
const getTrainingTypeName = (trainingType) => {
|
||||
const normalizedType = String(trainingType || "").trim().toLowerCase();
|
||||
return trainingTypeNameMap[normalizedType] || "自由训练";
|
||||
};
|
||||
|
||||
const formatPractiseTime = (value) => {
|
||||
const normalizedTime = String(value || "").trim();
|
||||
@@ -172,7 +161,7 @@ onLoad((options) => {
|
||||
@click="() => getPractiseDetail(item.id)"
|
||||
>
|
||||
<text
|
||||
>{{ getTrainingTypeName(item.trainingType) }}
|
||||
>{{ item.trainingTypeText }}
|
||||
{{ formatPractiseTime(item.createTime) }}</text
|
||||
>
|
||||
<image src="../static/back.png" mode="widthFix" />
|
||||
|
||||
@@ -240,9 +240,11 @@ onLoad((options) => {
|
||||
}
|
||||
|
||||
.login-title {
|
||||
width: 600rpx;
|
||||
margin: 28rpx 0 60rpx;
|
||||
font-size: 40rpx;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.avatar-wrap {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, nextTick, onMounted, onBeforeUnmount } from "vue";
|
||||
import { computed, ref, nextTick, onMounted, onBeforeUnmount } from "vue";
|
||||
import { onHide, onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import Container from "@/components/Container.vue";
|
||||
import ShootProgress from "@/components/ShootProgress.vue";
|
||||
@@ -47,6 +47,9 @@ const serverAddr = ref("");
|
||||
const showGuide = ref(false);
|
||||
const tips = ref("");
|
||||
const targetType = ref(1);
|
||||
const simulatorTargetType = computed(() =>
|
||||
[2, 40].includes(Number(targetType.value)) ? 40 : 20
|
||||
);
|
||||
const sharing = ref(false);
|
||||
const exiting = ref(false);
|
||||
const hiddenWhileActive = ref(false);
|
||||
@@ -447,6 +450,7 @@ onBeforeUnmount(() => {
|
||||
<view>
|
||||
<TestDistance
|
||||
v-if="!start && !practiseResult.id"
|
||||
:targetType="simulatorTargetType"
|
||||
:autoStart="true"
|
||||
@passed="onReady"
|
||||
/>
|
||||
@@ -477,6 +481,7 @@ onBeforeUnmount(() => {
|
||||
:currentRound="scores.length % 3"
|
||||
:scores="scores"
|
||||
:isSvip="isSvip"
|
||||
:targetType="simulatorTargetType"
|
||||
stable-shot-effect
|
||||
/>
|
||||
<ScorePanel2 :arrows="scores" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, nextTick, onMounted, onBeforeUnmount } from "vue";
|
||||
import { computed, ref, nextTick, onMounted, onBeforeUnmount } from "vue";
|
||||
import { onHide, onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import Container from "@/components/Container.vue";
|
||||
import ShootProgress from "@/components/ShootProgress.vue";
|
||||
@@ -46,6 +46,9 @@ const practiseId = ref("");
|
||||
const serverAddr = ref("");
|
||||
const showGuide = ref(false);
|
||||
const targetType = ref(1);
|
||||
const simulatorTargetType = computed(() =>
|
||||
[2, 40].includes(Number(targetType.value)) ? 40 : 20
|
||||
);
|
||||
const sharing = ref(false);
|
||||
const exiting = ref(false);
|
||||
const hiddenWhileActive = ref(false);
|
||||
@@ -461,6 +464,7 @@ onBeforeUnmount(() => {
|
||||
<view>
|
||||
<TestDistance
|
||||
v-if="!start && !practiseResult.id"
|
||||
:targetType="simulatorTargetType"
|
||||
:autoStart="true"
|
||||
@passed="onReady"
|
||||
/>
|
||||
@@ -486,6 +490,7 @@ onBeforeUnmount(() => {
|
||||
:totalRound="start ? total : 0"
|
||||
:scores="scores"
|
||||
:isSvip="isSvip"
|
||||
:targetType="simulatorTargetType"
|
||||
/>
|
||||
<ScorePanel
|
||||
v-if="start"
|
||||
|
||||
@@ -55,6 +55,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
targetType: {
|
||||
type: [Number, String],
|
||||
default: 20,
|
||||
},
|
||||
targetRadius: {
|
||||
type: Number,
|
||||
default: 20,
|
||||
@@ -422,12 +426,19 @@ function getExperienceTipStyle(shot) {
|
||||
);
|
||||
}
|
||||
const simulShoot = async () => {
|
||||
if (device.value.deviceId) await simulShootAPI(device.value.deviceId);
|
||||
if (device.value.deviceId) {
|
||||
await simulShootAPI(
|
||||
device.value.deviceId,
|
||||
undefined,
|
||||
undefined,
|
||||
props.targetType
|
||||
);
|
||||
}
|
||||
};
|
||||
const simulShoot2 = async () => {
|
||||
if (device.value.deviceId) {
|
||||
const r1 = Math.random() > 0.5 ? 0.01 : 0.02;
|
||||
await simulShootAPI(device.value.deviceId, r1, r1);
|
||||
await simulShootAPI(device.value.deviceId, r1, r1, props.targetType);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -24,6 +24,10 @@ const props = defineProps({
|
||||
type: Number,
|
||||
default: 15,
|
||||
},
|
||||
targetType: {
|
||||
type: [Number, String],
|
||||
default: 20,
|
||||
},
|
||||
});
|
||||
const arrow = ref({});
|
||||
const distance = ref(0);
|
||||
@@ -63,7 +67,14 @@ async function onReceiveMessage(msg) {
|
||||
}
|
||||
|
||||
const simulShoot = async () => {
|
||||
if (device.value.deviceId) await simulShootAPI(device.value.deviceId);
|
||||
if (device.value.deviceId) {
|
||||
await simulShootAPI(
|
||||
device.value.deviceId,
|
||||
undefined,
|
||||
undefined,
|
||||
props.targetType
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
@@ -102,7 +113,6 @@ onBeforeUnmount(() => {
|
||||
</button>
|
||||
<view class="warnning-text">
|
||||
<block v-if="statusText">
|
||||
<text v-if="distance > 0">当前距离{{ distance }}米</text>
|
||||
<text>{{ statusText }}</text>
|
||||
</block>
|
||||
<block v-else>
|
||||
|
||||
@@ -67,6 +67,7 @@ const COUNTDOWN_READY_EVENT = "team-battle-countdown-ready";
|
||||
const start = ref(null);
|
||||
const tips = ref("");
|
||||
const battleId = ref("");
|
||||
const targetType = ref(20);
|
||||
const currentRound = ref(0);
|
||||
const roundTipRound = ref(0);
|
||||
const goldenRound = ref(0);
|
||||
@@ -654,6 +655,8 @@ function applyBattleBase(battleInfo) {
|
||||
battleInfo = normalizeBattleInfo(battleInfo);
|
||||
if (!battleInfo) return;
|
||||
if (battleInfo.matchId) battleId.value = battleInfo.matchId;
|
||||
const nextTargetType = Number(battleInfo.targetType ?? battleInfo.target_type);
|
||||
if ([20, 40].includes(nextTargetType)) targetType.value = nextTargetType;
|
||||
if (battleInfo.status !== undefined) {
|
||||
start.value = battleInfo.status !== 0;
|
||||
matchStatus.value = battleInfo.status;
|
||||
@@ -1265,6 +1268,7 @@ onLoad((options) => {
|
||||
start.value = null;
|
||||
tips.value = "";
|
||||
battleId.value = returnSnapshot?.matchId || options.battleId || "";
|
||||
targetType.value = 20;
|
||||
currentRound.value = 0;
|
||||
roundTipRound.value = 0;
|
||||
goldenRound.value = 0;
|
||||
@@ -1398,6 +1402,7 @@ onShow(() => {
|
||||
:guide="false"
|
||||
:isBattle="true"
|
||||
:count="readyTime"
|
||||
:targetType="targetType"
|
||||
/>
|
||||
<!-- 比赛进行中显示:左右队伍、进度条、靶面和底部比分。 -->
|
||||
<view v-if="start" class="players-row">
|
||||
@@ -1423,6 +1428,7 @@ onShow(() => {
|
||||
:latestShotFlash="latestShotFlash"
|
||||
:redTeam="redTeam"
|
||||
:blueTeam="blueTeam"
|
||||
:targetType="targetType"
|
||||
stable-shot-effect
|
||||
/>
|
||||
<BattleFooter
|
||||
|
||||
@@ -56,6 +56,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
targetType: {
|
||||
type: [Number, String],
|
||||
default: 40,
|
||||
},
|
||||
coordinateRadius: {
|
||||
type: Number,
|
||||
default: 20,
|
||||
@@ -433,12 +437,19 @@ watch(
|
||||
);
|
||||
|
||||
const simulShoot = async () => {
|
||||
if (device.value.deviceId) await simulShootAPI(device.value.deviceId);
|
||||
if (device.value.deviceId) {
|
||||
await simulShootAPI(
|
||||
device.value.deviceId,
|
||||
undefined,
|
||||
undefined,
|
||||
props.targetType
|
||||
);
|
||||
}
|
||||
};
|
||||
const simulShoot2 = async () => {
|
||||
if (device.value.deviceId) {
|
||||
const r1 = Math.random() > 0.5 ? 0.01 : 0.02;
|
||||
await simulShootAPI(device.value.deviceId, r1, r1);
|
||||
await simulShootAPI(device.value.deviceId, r1, r1, props.targetType);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -39,7 +39,9 @@ const getDisplayText = (arrow) => {
|
||||
|
||||
const isLowScore = (arrow) => {
|
||||
if (!arrow) return false;
|
||||
if (props.trainingType === "rhythm") return arrow.ok !== true;
|
||||
if (["rhythm", "stability"].includes(props.trainingType)) {
|
||||
return arrow.ok !== true;
|
||||
}
|
||||
if (arrow.ringX) return false;
|
||||
const ring = Number(arrow.ring);
|
||||
return Number.isFinite(ring) && ring < 6;
|
||||
|
||||
@@ -231,6 +231,13 @@ const metricConfigs = {
|
||||
deltaKeys: ["deltaStability", "delta_stability"],
|
||||
deltaUnit: "",
|
||||
},
|
||||
{
|
||||
label: "十环次数",
|
||||
valueKeys: ["tenRingCount", "ten_ring_count"],
|
||||
unit: "次",
|
||||
deltaKeys: ["deltaTenRingCount", "delta_ten_ring_count"],
|
||||
deltaUnit: "次",
|
||||
},
|
||||
],
|
||||
rhythm: [
|
||||
{
|
||||
@@ -255,6 +262,32 @@ const metricConfigs = {
|
||||
deltaUnit: "次",
|
||||
},
|
||||
],
|
||||
stability: [
|
||||
{
|
||||
label: "能量最高值",
|
||||
valueKeys: ["maxEnergyPercent", "max_energy_percent"],
|
||||
unit: "%",
|
||||
deltaKeys: [
|
||||
"deltaMaxEnergyPercent",
|
||||
"delta_max_energy_percent",
|
||||
],
|
||||
deltaUnit: "%",
|
||||
},
|
||||
{
|
||||
label: "达标箭数",
|
||||
valueKeys: ["qualifiedArrows", "qualified_arrows"],
|
||||
unit: "箭",
|
||||
deltaKeys: ["deltaQualifiedArrows", "delta_qualified_arrows"],
|
||||
deltaUnit: "箭",
|
||||
},
|
||||
{
|
||||
label: "达标率",
|
||||
valueKeys: ["qualifiedRate", "qualified_rate"],
|
||||
unit: "%",
|
||||
deltaKeys: ["deltaQualifiedRate", "delta_qualified_rate"],
|
||||
deltaUnit: "%",
|
||||
},
|
||||
],
|
||||
endurance: [
|
||||
{
|
||||
label: "完成箭数",
|
||||
@@ -270,41 +303,43 @@ const metricConfigs = {
|
||||
deltaKeys: ["deltaTotalRings", "delta_total_rings"],
|
||||
deltaUnit: "环",
|
||||
},
|
||||
{
|
||||
label: "平均环数",
|
||||
valueKeys: ["averageRing", "average_ring"],
|
||||
unit: "环",
|
||||
deltaKeys: ["deltaAverageRing", "delta_average_ring"],
|
||||
deltaUnit: "环",
|
||||
},
|
||||
],
|
||||
precision: [
|
||||
{
|
||||
label: "共命中目标",
|
||||
label: "命中目标数",
|
||||
valueKeys: ["totalHits", "total_hits"],
|
||||
fallback: () => 0,
|
||||
unit: "次",
|
||||
deltaKeys: [
|
||||
"deltaTotalHits",
|
||||
"delta_total_hits",
|
||||
"hitCompare",
|
||||
"hitDiff",
|
||||
"hitDelta",
|
||||
],
|
||||
deltaKeys: ["deltaTotalHits", "delta_total_hits"],
|
||||
deltaUnit: "次",
|
||||
},
|
||||
{
|
||||
label: "用时",
|
||||
valueKeys: ["duration", "usedTime", "shootTime", "time"],
|
||||
unit: "",
|
||||
deltaKeys: [
|
||||
"deltaDuration",
|
||||
"delta_duration",
|
||||
"timeCompare",
|
||||
"timeDiff",
|
||||
"durationDiff",
|
||||
],
|
||||
deltaUnit: "",
|
||||
duration: true,
|
||||
label: "射箭数",
|
||||
valueKeys: ["totalArrows", "total_arrows"],
|
||||
unit: "箭",
|
||||
deltaKeys: ["deltaTotalArrows", "delta_total_arrows"],
|
||||
deltaUnit: "箭",
|
||||
},
|
||||
{
|
||||
label: "命中率",
|
||||
valueKeys: ["hitRate", "hit_rate"],
|
||||
unit: "%",
|
||||
deltaKeys: ["deltaHitRate", "delta_hit_rate"],
|
||||
deltaUnit: "%",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const resultRows = computed(() => {
|
||||
const configs = metricConfigs[resultTrainingType.value] || metricConfigs.precision;
|
||||
const configs =
|
||||
metricConfigs[resultTrainingType.value] || metricConfigs.precision;
|
||||
return configs.map((config) => {
|
||||
const fallback = config.fallback ? config.fallback() : 0;
|
||||
const value = readMetricNumber(config.valueKeys, fallback);
|
||||
@@ -333,9 +368,6 @@ const handlePrimary = () => {
|
||||
retryPractice();
|
||||
};
|
||||
|
||||
const calories = computed(
|
||||
() => formatMetricNumber(readMetricNumber(["calories", "calorie", "kcal"]))
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -371,25 +403,6 @@ const calories = computed(
|
||||
<view v-else class="stat-value">--</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="resultTrainingType !== 'rhythm'" class="stat-row">
|
||||
<image class="stat-bg" src="https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/result-c-bg.png" mode="scaleToFill" />
|
||||
<view class="stat-cell">
|
||||
<text class="stat-label">消耗卡路里</text>
|
||||
<view class="stat-value">
|
||||
<text>{{ calories }}卡</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="stat-equal">≈</text>
|
||||
<!-- <view class="stat-divider"></view> -->
|
||||
<view class="stat-cell stat-cell--compare">
|
||||
<view class="stat-value">
|
||||
<image v-for="index in 3" :key="index" class="rice-icon"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/result-rice.png" mode="widthFix" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
<view class="actions">
|
||||
<view class="action-item" @click="() => (showBowData = true)">
|
||||
<image class="action-icon" src="https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/result-icon-1.png" mode="widthFix" />
|
||||
@@ -633,14 +646,6 @@ const calories = computed(
|
||||
transform: skewX(12deg);
|
||||
}
|
||||
|
||||
.stat-equal{
|
||||
width: 30rpx;
|
||||
height: 40rpx;
|
||||
color: #F3E0B9;
|
||||
font-size: 30rpx;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
|
||||
.trend-icon {
|
||||
width: 28rpx;
|
||||
height: 42rpx;
|
||||
@@ -659,18 +664,6 @@ const calories = computed(
|
||||
height: 62rpx;
|
||||
}
|
||||
|
||||
.rice-list {
|
||||
width: 160rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rice-icon {
|
||||
width: 36rpx;
|
||||
height: 34rpx;
|
||||
margin-right: 14rpx;
|
||||
}
|
||||
|
||||
.oper-box {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
@@ -73,6 +73,14 @@ const props = defineProps({
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
energyPercent: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
energyReqPercent: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
isVip: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -137,6 +145,9 @@ let rhythmServerClockOffsetMs = 0;
|
||||
let rhythmSyncGeneration = 0;
|
||||
|
||||
const isRhythmTraining = computed(() => props.trainingType === "rhythm");
|
||||
const isStabilityTraining = computed(
|
||||
() => props.trainingType === "stability"
|
||||
);
|
||||
|
||||
const normalizePositiveInteger = (value) => {
|
||||
const numberValue = Number(value);
|
||||
@@ -176,6 +187,18 @@ const progressPercent = computed(() => {
|
||||
return Math.max(0, Math.min(100, (remain.value / props.total) * 100));
|
||||
});
|
||||
|
||||
const stabilityEnergyPercent = computed(() =>
|
||||
Math.max(0, Math.min(100, Number(props.energyPercent) || 0))
|
||||
);
|
||||
const stabilityEnergyReqPercent = computed(() =>
|
||||
Math.max(0, Math.min(100, Number(props.energyReqPercent) || 0))
|
||||
);
|
||||
const stabilityEnergyReached = computed(
|
||||
() =>
|
||||
stabilityEnergyReqPercent.value > 0 &&
|
||||
stabilityEnergyPercent.value >= stabilityEnergyReqPercent.value
|
||||
);
|
||||
|
||||
const rhythmMarkerPercent = computed(() => {
|
||||
if (!validRhythmRoundTime.value) return 0;
|
||||
return Math.max(
|
||||
@@ -536,9 +559,34 @@ onBeforeUnmount(() => {
|
||||
<template>
|
||||
<view
|
||||
v-if="show"
|
||||
:class="isRhythmTraining ? 'rhythm-progress' : 'progress-card'"
|
||||
:class="
|
||||
isRhythmTraining
|
||||
? 'rhythm-progress'
|
||||
: isStabilityTraining
|
||||
? 'stability-progress'
|
||||
: 'progress-card'
|
||||
"
|
||||
>
|
||||
<template v-if="isRhythmTraining">
|
||||
<template v-if="isStabilityTraining">
|
||||
<text class="stability-progress__time">{{ remain }}秒</text>
|
||||
<view class="stability-progress__track">
|
||||
<view
|
||||
class="stability-progress__fill"
|
||||
:class="{
|
||||
'stability-progress__fill--reached': stabilityEnergyReached,
|
||||
}"
|
||||
:style="{ width: `${stabilityEnergyPercent}%` }"
|
||||
/>
|
||||
<view
|
||||
class="stability-progress__marker"
|
||||
:style="{ left: `${stabilityEnergyReqPercent}%` }"
|
||||
/>
|
||||
<text class="stability-progress__value">
|
||||
{{ Math.round(stabilityEnergyPercent) }}%
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<template v-else-if="isRhythmTraining">
|
||||
<text class="rhythm-progress__title">{{ rhythmTitle }}</text>
|
||||
<view class="rhythm-progress__track">
|
||||
<view
|
||||
@@ -758,6 +806,68 @@ onBeforeUnmount(() => {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stability-progress {
|
||||
box-sizing: border-box;
|
||||
margin: 32rpx 84rpx 0;
|
||||
}
|
||||
|
||||
.stability-progress__time {
|
||||
display: block;
|
||||
margin-bottom: 20rpx;
|
||||
color: #ffffff;
|
||||
font-size: 34rpx;
|
||||
font-weight: 500;
|
||||
line-height: 48rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stability-progress__track {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 24rpx;
|
||||
overflow: hidden;
|
||||
border-radius: 18rpx;
|
||||
background: #444444;
|
||||
}
|
||||
|
||||
.stability-progress__fill {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
border-radius: 18rpx;
|
||||
background: linear-gradient(90deg, #87f1df 0%, #5ba8e8 100%);
|
||||
transition: width 240ms linear, background 240ms ease;
|
||||
}
|
||||
|
||||
.stability-progress__fill--reached {
|
||||
background: linear-gradient(90deg, #a5df62 0%, #61c787 100%);
|
||||
}
|
||||
|
||||
.stability-progress__marker {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 2;
|
||||
width: 4rpx;
|
||||
transform: translateX(-2rpx);
|
||||
background: rgba(26, 24, 22, 0.92);
|
||||
}
|
||||
|
||||
.stability-progress__value {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #eefaff;
|
||||
font-size: 18rpx;
|
||||
line-height: 24rpx;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.rhythm-progress {
|
||||
box-sizing: border-box;
|
||||
margin: 32rpx 84rpx 0;
|
||||
|
||||
@@ -109,7 +109,14 @@ async function onReceiveMessage(msg) {
|
||||
}
|
||||
|
||||
const simulShoot = async () => {
|
||||
if (device.value.deviceId) await simulShootAPI(device.value.deviceId);
|
||||
if (device.value.deviceId) {
|
||||
await simulShootAPI(
|
||||
device.value.deviceId,
|
||||
undefined,
|
||||
undefined,
|
||||
props.targetType
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
@@ -143,7 +150,6 @@ onBeforeUnmount(() => {
|
||||
<view class="warnning-text">
|
||||
<view class="target-tip">当前靶纸为<text class="text-yellow">{{ targetType }}cm</text>全环靶</view>
|
||||
<block v-if="statusText">
|
||||
<text v-if="distance > 0">当前距离<text class="text-yellow">{{ distance }}</text>米</text>
|
||||
<text>{{ statusText }}</text>
|
||||
</block>
|
||||
<block v-else>
|
||||
|
||||
@@ -13,7 +13,20 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const previewLines = computed(() => {
|
||||
return props.lines.map((line) => String(line || "").trim()).filter(Boolean);
|
||||
return props.lines
|
||||
.map((line) => {
|
||||
const rawParts = Array.isArray(line?.parts)
|
||||
? line.parts
|
||||
: [{ text: line }];
|
||||
const parts = rawParts
|
||||
.map((part) => ({
|
||||
text: String(part?.text ?? part ?? "").trim(),
|
||||
}))
|
||||
.filter((part) => part.text);
|
||||
|
||||
return { parts };
|
||||
})
|
||||
.filter((line) => line.parts.length > 0);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -27,13 +40,16 @@ const previewLines = computed(() => {
|
||||
<view class="difficulty-preview__content">
|
||||
<text class="difficulty-preview__title">{{ title }}</text>
|
||||
<view class="difficulty-preview__copy">
|
||||
<text
|
||||
<view
|
||||
v-for="(line, index) in previewLines"
|
||||
:key="`${line}-${index}`"
|
||||
:key="index"
|
||||
class="difficulty-preview__line"
|
||||
>
|
||||
{{ line }}
|
||||
</text>
|
||||
<text
|
||||
v-for="(part, partIndex) in line.parts"
|
||||
:key="partIndex"
|
||||
>{{ part.text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -86,6 +102,4 @@ const previewLines = computed(() => {
|
||||
.difficulty-preview__line {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from "@/apis";
|
||||
|
||||
// 难度页接口数据源:
|
||||
// 1. 接口:GET /training/difficulty/list?type=base/endurance/precision/rhythm
|
||||
// 1. 接口:GET /training/difficulty/list?type=base/endurance/precision/rhythm/stability
|
||||
// 2. 当前进度:接口 user_levels / list.completed,路由参数可覆盖选中难度
|
||||
const trainingDifficultyStorageKey = "training-selection";
|
||||
const defaultTrainingType = "precision";
|
||||
@@ -35,6 +35,10 @@ const trainingTypeMetaMap = {
|
||||
key: "rhythm",
|
||||
title: "节奏训练",
|
||||
},
|
||||
stability: {
|
||||
key: "stability",
|
||||
title: "稳定训练",
|
||||
},
|
||||
};
|
||||
const routeModeTypeMap = {
|
||||
basic: "base",
|
||||
@@ -42,6 +46,9 @@ const routeModeTypeMap = {
|
||||
endurance: "endurance",
|
||||
precision: "precision",
|
||||
rhythm: "rhythm",
|
||||
stability: "stability",
|
||||
// 兼容历史上已经分享出去的 power 深链,实际创建仍统一传 stability。
|
||||
power: "stability",
|
||||
};
|
||||
const defaultTargetType = 1;
|
||||
|
||||
@@ -103,6 +110,7 @@ const createDifficultySummary = (item = {}) => {
|
||||
const timeLimit = toNumber(item.time_limit);
|
||||
const hitReq = toNumber(item.hit_req);
|
||||
const totalReq = toNumber(item.total_req);
|
||||
const energyReqPercent = toNumber(item.energy_req_percent);
|
||||
const shootingTimeText =
|
||||
timeLimit > 0 ? `在${timeLimit}秒内进行射箭` : "不限时进行射箭";
|
||||
const enduranceTimeText =
|
||||
@@ -129,6 +137,26 @@ const createDifficultySummary = (item = {}) => {
|
||||
: "需要在指定时间节点射箭",
|
||||
hitReq > 0 ? `且每箭命中${hitReq}环内` : "且每箭命中指定区域",
|
||||
],
|
||||
stability: [
|
||||
{
|
||||
parts: [
|
||||
{ text: timeLimit > 0 ? "在" : "" },
|
||||
{
|
||||
text: timeLimit > 0 ? `${timeLimit}秒` : "不限时",
|
||||
highlight: true,
|
||||
},
|
||||
{ text: timeLimit > 0 ? "内射箭,命中" : "射箭,命中" },
|
||||
{ text: `${hitReq}环`, highlight: true },
|
||||
{ text: "可获得能量" },
|
||||
],
|
||||
},
|
||||
{
|
||||
parts: [
|
||||
{ text: "计时结束能量需要大于" },
|
||||
{ text: `${energyReqPercent}%`, highlight: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (summaryMap[type] || [desc]).filter(Boolean);
|
||||
@@ -604,6 +632,21 @@ const createPracticeQuery = (difficulty) => {
|
||||
roundTime: toNumber(difficulty.round_time ?? difficulty.roundTime),
|
||||
shootTime: toNumber(difficulty.shoot_time ?? difficulty.shootTime),
|
||||
},
|
||||
stability: {
|
||||
hitReq: toNumber(difficulty.hit_req),
|
||||
scoreSlot: toNumber(
|
||||
difficulty.score_slot ?? difficulty.scoreSlot
|
||||
),
|
||||
energyPerHit: toNumber(
|
||||
difficulty.energy_per_hit ?? difficulty.energyPerHit
|
||||
),
|
||||
energyCostPerSec: toNumber(
|
||||
difficulty.energy_cost_per_sec ?? difficulty.energyCostPerSec
|
||||
),
|
||||
energyReqPercent: toNumber(
|
||||
difficulty.energy_req_percent ?? difficulty.energyReqPercent
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -638,6 +681,19 @@ const saveTrainingContext = (practice = {}) => {
|
||||
targetPaperType: difficulty.targetPaperType,
|
||||
roundTime: toNumber(difficulty.round_time ?? difficulty.roundTime),
|
||||
shootTime: toNumber(difficulty.shoot_time ?? difficulty.shootTime),
|
||||
hitReq: toNumber(difficulty.hit_req ?? difficulty.hitReq),
|
||||
scoreSlot: toNumber(
|
||||
difficulty.score_slot ?? difficulty.scoreSlot
|
||||
),
|
||||
energyPerHit: toNumber(
|
||||
difficulty.energy_per_hit ?? difficulty.energyPerHit
|
||||
),
|
||||
energyCostPerSec: toNumber(
|
||||
difficulty.energy_cost_per_sec ?? difficulty.energyCostPerSec
|
||||
),
|
||||
energyReqPercent: toNumber(
|
||||
difficulty.energy_req_percent ?? difficulty.energyReqPercent
|
||||
),
|
||||
practiceId: practice.id || "",
|
||||
serverAddr: practice.serverAddr || "",
|
||||
createdAt: practice.id ? Date.now() : 0,
|
||||
|
||||
@@ -15,16 +15,16 @@ const trainingModeRouteMap = {
|
||||
endurance: "endurance",
|
||||
precision: "precision",
|
||||
rhythm: "rhythm",
|
||||
stability: "power",
|
||||
stability: "stability",
|
||||
};
|
||||
const unavailableTrainingIds = new Set(["stability"]);
|
||||
const unavailableTrainingIds = new Set();
|
||||
// 训练项目卡片右侧主图标。
|
||||
const trainingModeIconMap = {
|
||||
base_bow:
|
||||
"https://static.shelingxingqiu.com/shootmini/static/training-home/img_3.png",
|
||||
bow: "https://static.shelingxingqiu.com/shootmini/static/training-home/img_4.png",
|
||||
bow: "https://static.shelingxingqiu.com/shootmini/static/training-home/img_5.png",
|
||||
target:
|
||||
"https://static.shelingxingqiu.com/shootmini/static/training-home/img_5.png",
|
||||
"https://static.shelingxingqiu.com/shootmini/static/training-home/img_4.png",
|
||||
wave: "https://static.shelingxingqiu.com/shootmini/static/training-home/img_6.png",
|
||||
muscle:
|
||||
"https://static.shelingxingqiu.com/shootmini/static/training-home/img_7.png",
|
||||
|
||||
@@ -13,8 +13,11 @@ import BubbleTip from "./components/BubbleTip.vue";
|
||||
import audioManager, {
|
||||
getPrecisionShotAudioKeys,
|
||||
getRhythmShotAudioKeys,
|
||||
getStabilityShotAudioKeys,
|
||||
getTrainingStartAudioKey,
|
||||
RHYTHM_SHOOT_WINDOW_AUDIO_KEY,
|
||||
STABILITY_ENERGY_50_AUDIO_KEY,
|
||||
STABILITY_ENERGY_70_AUDIO_KEY,
|
||||
} from "@/audioManager";
|
||||
|
||||
import {
|
||||
@@ -88,6 +91,11 @@ const tips = ref("");
|
||||
const targetType = ref(defaultTargetType);
|
||||
const trainingParams = ref({});
|
||||
const practiceInfo = ref({});
|
||||
const simulatorTargetType = computed(() => {
|
||||
const currentTargetType = Number(practiceInfo.value.targetType);
|
||||
if ([20, 40].includes(currentTargetType)) return currentTargetType;
|
||||
return [2, 40].includes(Number(targetType.value)) ? 40 : 20;
|
||||
});
|
||||
const rhythmServerTimestamp = ref(0);
|
||||
const rhythmFallbackWindowStart = ref(0);
|
||||
const rhythmFallbackTimestamp = ref(0);
|
||||
@@ -265,6 +273,9 @@ const loadNextDifficultyState = (result = {}) => {
|
||||
const timeLimit = computed(() => getPositiveInteger(practiceInfo.value.timeLimit));
|
||||
const hasTimeLimit = computed(() => timeLimit.value > 0);
|
||||
const isRhythmTraining = computed(() => trainingType.value === "rhythm");
|
||||
const isStabilityTraining = computed(
|
||||
() => trainingType.value === "stability"
|
||||
);
|
||||
const rhythmRoundTime = computed(() =>
|
||||
getPositiveInteger(practiceInfo.value.roundTime) ||
|
||||
getPositiveInteger(trainingParams.value.roundTime)
|
||||
@@ -297,6 +308,44 @@ const rhythmHitReq = computed(
|
||||
getPositiveInteger(trainingParams.value.hitReq)
|
||||
);
|
||||
|
||||
const stabilityEnergyReqPercent = computed(() =>
|
||||
Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
100,
|
||||
getPracticeNumber(
|
||||
practiceInfo.value.energyReqPercent,
|
||||
trainingParams.value.energyReqPercent
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
const stabilityScoreSlot = computed(() =>
|
||||
Math.max(
|
||||
0,
|
||||
getPracticeNumber(
|
||||
practiceInfo.value.scoreSlot,
|
||||
trainingParams.value.scoreSlot
|
||||
)
|
||||
)
|
||||
);
|
||||
const stabilityCurrentEnergy = computed(() => {
|
||||
const currentEnergy = Math.max(
|
||||
0,
|
||||
getPracticeNumber(practiceInfo.value.currentEnergy)
|
||||
);
|
||||
return stabilityScoreSlot.value > 0
|
||||
? Math.min(stabilityScoreSlot.value, currentEnergy)
|
||||
: currentEnergy;
|
||||
});
|
||||
const stabilityEnergyPercent = computed(() => {
|
||||
if (stabilityScoreSlot.value <= 0) return 0;
|
||||
return Math.min(
|
||||
100,
|
||||
(stabilityCurrentEnergy.value / stabilityScoreSlot.value) * 100
|
||||
);
|
||||
});
|
||||
|
||||
const initializeRhythmFirstRoundCountdown = () => {
|
||||
rhythmHasActiveServerAnchor.value = false;
|
||||
rhythmFallbackWindowStart.value = 0;
|
||||
@@ -444,6 +493,35 @@ const trainingCopy = computed(() => {
|
||||
};
|
||||
}
|
||||
|
||||
if (trainingType.value === "stability") {
|
||||
const hitReq = getPracticeNumber(
|
||||
practiceInfo.value.hitReq,
|
||||
trainingParams.value.hitReq
|
||||
);
|
||||
const stabilityTimeLimit = getPracticeNumber(
|
||||
practiceInfo.value.timeLimit,
|
||||
trainingParams.value.timeLimit
|
||||
);
|
||||
|
||||
return {
|
||||
inline: true,
|
||||
details: [
|
||||
{ text: stabilityTimeLimit > 0 ? "在" : "" },
|
||||
{
|
||||
text: stabilityTimeLimit > 0 ? `${stabilityTimeLimit}秒` : "不限时",
|
||||
highlight: true,
|
||||
},
|
||||
{ text: stabilityTimeLimit > 0 ? "内射箭,命中" : "射箭,命中" },
|
||||
{ text: `${hitReq}环`, highlight: true },
|
||||
{ text: "可获得能量,计时结束能量需要大于" },
|
||||
{
|
||||
text: `${stabilityEnergyReqPercent.value}%`,
|
||||
highlight: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -481,6 +559,18 @@ const practiceInfoFields = [
|
||||
"shootTime",
|
||||
"shootWindowStart",
|
||||
"inShootWindow",
|
||||
"scoreSlot",
|
||||
"currentEnergy",
|
||||
"energyCostPerSec",
|
||||
"energyPerHit",
|
||||
"energyReqPercent",
|
||||
"deltaCurrentEnergy",
|
||||
"maxEnergyPercent",
|
||||
"qualifiedArrows",
|
||||
"qualifiedRate",
|
||||
"deltaMaxEnergyPercent",
|
||||
"deltaQualifiedArrows",
|
||||
"deltaQualifiedRate",
|
||||
"timeLimit",
|
||||
"completed",
|
||||
"totalArrows",
|
||||
@@ -489,8 +579,10 @@ const practiceInfoFields = [
|
||||
"stability",
|
||||
"maxCombo",
|
||||
"totalHits",
|
||||
"hitRate",
|
||||
"deltaTotalHits",
|
||||
"deltaDuration",
|
||||
"deltaHitRate",
|
||||
"deltaMaxCombo",
|
||||
"deltaTotalRings",
|
||||
"deltaTotalArrows",
|
||||
@@ -518,9 +610,23 @@ const practiceResultFields = [
|
||||
"stability",
|
||||
"maxCombo",
|
||||
"totalHits",
|
||||
"hitRate",
|
||||
"currentRings",
|
||||
"scoreSlot",
|
||||
"currentEnergy",
|
||||
"energyCostPerSec",
|
||||
"energyPerHit",
|
||||
"energyReqPercent",
|
||||
"deltaCurrentEnergy",
|
||||
"maxEnergyPercent",
|
||||
"qualifiedArrows",
|
||||
"qualifiedRate",
|
||||
"deltaMaxEnergyPercent",
|
||||
"deltaQualifiedArrows",
|
||||
"deltaQualifiedRate",
|
||||
"deltaTotalHits",
|
||||
"deltaDuration",
|
||||
"deltaHitRate",
|
||||
"deltaMaxCombo",
|
||||
"deltaTotalRings",
|
||||
"deltaTotalArrows",
|
||||
@@ -600,6 +706,9 @@ const cacheRhythmTargetArrows = (message = {}) => {
|
||||
const syncPracticeInfo = (message = {}) => {
|
||||
cacheRhythmTrainingConfig(message);
|
||||
cacheRhythmTargetArrows(message);
|
||||
const messageTrainingType = String(
|
||||
message.trainingType ?? message.training_type ?? trainingType.value
|
||||
).trim();
|
||||
const nextInfo = practiceInfoFields.reduce((result, field) => {
|
||||
if (Object.prototype.hasOwnProperty.call(message, field)) {
|
||||
result[field] = message[field];
|
||||
@@ -607,6 +716,24 @@ const syncPracticeInfo = (message = {}) => {
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
const isStabilityEnergySnapshot =
|
||||
messageTrainingType === "stability" &&
|
||||
(Object.prototype.hasOwnProperty.call(message, "currentEnergy") ||
|
||||
message.type === undefined ||
|
||||
[
|
||||
MESSAGETYPESV2.BattleStart,
|
||||
MESSAGETYPESV2.ShootResult,
|
||||
MESSAGETYPESV2.BattleEnd,
|
||||
].includes(message.type));
|
||||
|
||||
if (isStabilityEnergySnapshot) {
|
||||
// BattleStart/ShootResult/PracticeEnd/同步消息都是稳定训练完整快照。
|
||||
// current_energy 为 0 时 protobuf 不编码,不能沿用上一份非零能量。
|
||||
if (!Object.prototype.hasOwnProperty.call(nextInfo, "currentEnergy")) {
|
||||
nextInfo.currentEnergy = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const isPrecisionSnapshot =
|
||||
(message.type === MESSAGETYPESV2.BattleStart ||
|
||||
message.type === MESSAGETYPESV2.ShootResult) &&
|
||||
@@ -1216,7 +1343,10 @@ onLoad((options = {}) => {
|
||||
toRouteNumber(trainingContext.difficultyLevel)
|
||||
),
|
||||
recordId: options.recordId || "",
|
||||
hitReq: toRouteNumber(options.hitReq),
|
||||
hitReq: toRouteNumber(
|
||||
options.hitReq,
|
||||
toRouteNumber(trainingContext.hitReq)
|
||||
),
|
||||
totalReq: toRouteNumber(options.totalReq),
|
||||
blocks: toRouteNumber(options.blocks),
|
||||
mode: toRouteNumber(options.mode),
|
||||
@@ -1228,16 +1358,37 @@ onLoad((options = {}) => {
|
||||
options.shootTime,
|
||||
toRouteNumber(trainingContext.shootTime)
|
||||
),
|
||||
energyPerHit: toRouteNumber(
|
||||
options.energyPerHit,
|
||||
toRouteNumber(trainingContext.energyPerHit)
|
||||
),
|
||||
energyCostPerSec: toRouteNumber(
|
||||
options.energyCostPerSec,
|
||||
toRouteNumber(trainingContext.energyCostPerSec)
|
||||
),
|
||||
energyReqPercent: toRouteNumber(
|
||||
options.energyReqPercent,
|
||||
toRouteNumber(trainingContext.energyReqPercent)
|
||||
),
|
||||
scoreSlot: toRouteNumber(
|
||||
options.scoreSlot,
|
||||
toRouteNumber(trainingContext.scoreSlot)
|
||||
),
|
||||
};
|
||||
practiseId.value = trainingContext.practiceId || "";
|
||||
serverAddr.value = trainingContext.serverAddr || "";
|
||||
|
||||
const startAudioKey = getTrainingStartAudioKey(trainingParams.value.type);
|
||||
const warmupKeys = [startAudioKey];
|
||||
const warmupKeys = [startAudioKey, "胜利", "失败"];
|
||||
if (trainingParams.value.type === "precision") {
|
||||
warmupKeys.push("Bingo命中目标", "未命中");
|
||||
} else if (trainingParams.value.type === "rhythm") {
|
||||
warmupKeys.push(RHYTHM_SHOOT_WINDOW_AUDIO_KEY, "Perfect", "miss");
|
||||
} else if (trainingParams.value.type === "stability") {
|
||||
warmupKeys.push(
|
||||
STABILITY_ENERGY_50_AUDIO_KEY,
|
||||
STABILITY_ENERGY_70_AUDIO_KEY
|
||||
);
|
||||
}
|
||||
void audioManager.warmKeys(warmupKeys.filter(Boolean));
|
||||
});
|
||||
@@ -1307,6 +1458,7 @@ const enterPracticeResult = (result = {}) => {
|
||||
clearPracticeRuntimeContext();
|
||||
closePracticeConnection("training-practice-result");
|
||||
pageStage.value = pageStages.RESULT;
|
||||
audioManager.play(result.completed === true ? "胜利" : "失败");
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -1406,6 +1558,30 @@ async function onReceiveMessage(msg) {
|
||||
if (hasNewShot) {
|
||||
shotEffectToken.value += 1;
|
||||
}
|
||||
} else if (trainingType.value === "stability") {
|
||||
const hasStabilityShot =
|
||||
msg.stabilityHasNewShot === true || hasNewShot;
|
||||
const latestDetail =
|
||||
Array.isArray(msg.details) && msg.details.length > 0
|
||||
? msg.details[msg.details.length - 1]
|
||||
: null;
|
||||
const arrow = msg.shootData || latestDetail;
|
||||
const audioKeys = hasStabilityShot
|
||||
? getStabilityShotAudioKeys(arrow)
|
||||
: [];
|
||||
const milestoneAudioKey = String(
|
||||
msg.stabilityMilestoneAudioKey || ""
|
||||
).trim();
|
||||
|
||||
// WebSocket 管理器负责阈值判定并把同一语音 key 用于 ACK;这里按
|
||||
// 环数/最高跨越阈值的顺序一次入队。
|
||||
if (milestoneAudioKey) audioKeys.push(milestoneAudioKey);
|
||||
if (audioKeys.length > 0) {
|
||||
audioManager.play(audioKeys, false);
|
||||
}
|
||||
if (hasStabilityShot) {
|
||||
shotEffectToken.value += 1;
|
||||
}
|
||||
} else if (hasNewShot) {
|
||||
shotEffectToken.value += 1;
|
||||
}
|
||||
@@ -1579,15 +1755,25 @@ onBeforeUnmount(() => {
|
||||
:showBottom="isDistanceStage"
|
||||
:scroll="!isShootingStage"
|
||||
:onBack="exitPractice"
|
||||
:title="isShootingStage && isRhythmTraining ? '节奏训练' : ''"
|
||||
:title="
|
||||
isShootingStage
|
||||
? isRhythmTraining
|
||||
? '节奏训练'
|
||||
: isStabilityTraining
|
||||
? '稳定训练'
|
||||
: ''
|
||||
: ''
|
||||
"
|
||||
:titleStyle="
|
||||
isShootingStage && isRhythmTraining ? rhythmHeaderTitleStyle : {}
|
||||
isShootingStage && (isRhythmTraining || isStabilityTraining)
|
||||
? rhythmHeaderTitleStyle
|
||||
: {}
|
||||
"
|
||||
>
|
||||
<view class="practise-content">
|
||||
<TestDistance
|
||||
v-if="isDistanceStage"
|
||||
:targetType="practiceInfo.targetType"
|
||||
:targetType="simulatorTargetType"
|
||||
:autoStart="true"
|
||||
@passed="onReady"
|
||||
/>
|
||||
@@ -1604,10 +1790,14 @@ onBeforeUnmount(() => {
|
||||
:inShootWindow="rhythmInShootWindow"
|
||||
:serverTimestamp="rhythmCountdownTimestamp"
|
||||
:hitReq="rhythmHitReq"
|
||||
:energyPercent="stabilityEnergyPercent"
|
||||
:energyReqPercent="stabilityEnergyReqPercent"
|
||||
:isVip="isVip"
|
||||
:isSvip="isSvip"
|
||||
:externalShootResultAudio="
|
||||
trainingType === 'precision' || trainingType === 'rhythm'
|
||||
trainingType === 'precision' ||
|
||||
trainingType === 'rhythm' ||
|
||||
trainingType === 'stability'
|
||||
"
|
||||
:onStop="onTimeLimitReached"
|
||||
/>
|
||||
@@ -1624,6 +1814,7 @@ onBeforeUnmount(() => {
|
||||
:currentRound="scores.length % 3"
|
||||
:scores="scores"
|
||||
:isSvip="isSvip"
|
||||
:targetType="simulatorTargetType"
|
||||
:shotEffectToken="shotEffectToken"
|
||||
:showCrosshair="false"
|
||||
:sectorCount="precisionBlocks"
|
||||
|
||||
+17
-1
@@ -4,6 +4,7 @@ import Container from "@/components/Container.vue";
|
||||
import UserHeader from "@/components/UserHeader.vue";
|
||||
import UserItem from "@/components/UserItem.vue";
|
||||
import Avatar from "@/components/Avatar.vue";
|
||||
import { getMyGoldAPI } from "@/apis";
|
||||
import { canEenter } from "@/util";
|
||||
import useStore from "@/store";
|
||||
import { storeToRefs } from "pinia";
|
||||
@@ -29,6 +30,16 @@ const toCoinPage = () => {
|
||||
url: "/pages/coin/nearby-stores",
|
||||
});
|
||||
};
|
||||
const showCoinEntry = ref(false);
|
||||
const loadCoinEntryVisibility = async () => {
|
||||
showCoinEntry.value = false;
|
||||
try {
|
||||
const result = await getMyGoldAPI();
|
||||
showCoinEntry.value = result?.hasTempBind === true;
|
||||
} catch (error) {
|
||||
console.error("加载金币入口状态失败", error);
|
||||
}
|
||||
};
|
||||
const toBeVipPage = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/member/be-vip",
|
||||
@@ -74,6 +85,7 @@ const logout = () => {
|
||||
updateUser();
|
||||
};
|
||||
onMounted(() => {
|
||||
loadCoinEntryVisibility();
|
||||
const accountInfo = uni.getAccountInfoSync();
|
||||
const envVersion = accountInfo.miniProgram.envVersion;
|
||||
if (envVersion !== "release") showLogout.value = true;
|
||||
@@ -113,7 +125,11 @@ const buildVersion = typeof __BUILD_TIME__ !== 'undefined' ? __BUILD_TIME__ : ''
|
||||
<text v-if="user.trio > 0" :style="{ color: '#259249' }">已完成</text>
|
||||
<text v-else :style="{ color: '#CC311F' }">未完成</text>
|
||||
</UserItem>
|
||||
<UserItem title="我的金币" :onClick="toCoinPage" />
|
||||
<UserItem
|
||||
v-if="showCoinEntry"
|
||||
title="我的金币"
|
||||
:onClick="toCoinPage"
|
||||
/>
|
||||
<UserItem title="会员" :onClick="toBeVipPage">
|
||||
<view
|
||||
v-if="user.sVip === true"
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -7,8 +7,8 @@ const { Reader, Writer } = protobuf;
|
||||
// 所以这里使用 minimal Reader/Writer 做静态字段解码和客户端消息编码。
|
||||
// <match-schema-generated>
|
||||
// 此区块由 scripts/generate-match-schema.mjs 自动生成,请勿手动修改。
|
||||
// 来源:src/utils/match.min.js(sha256: fbf97fd3d08ae314)
|
||||
// 协议命名空间:rpc;消息数:12;字段数:155
|
||||
// 来源:src/utils/match.min.js(sha256: bece92f31bde3072)
|
||||
// 协议命名空间:rpc;消息数:12;字段数:163
|
||||
|
||||
export const ServerMessageType = {
|
||||
SERVER_MSG_UNKNOWN: 0,
|
||||
@@ -198,6 +198,14 @@ const SCHEMAS = {
|
||||
53: { name: "shoot_time", kind: "int32" },
|
||||
54: { name: "ten_ring_count", kind: "int32" },
|
||||
55: { name: "delta_ten_ring_count", kind: "int32" },
|
||||
56: { name: "max_energy_percent", kind: "int32" },
|
||||
57: { name: "qualified_arrows", kind: "int32" },
|
||||
58: { name: "qualified_rate", kind: "int32" },
|
||||
59: { name: "delta_max_energy_percent", kind: "int32" },
|
||||
60: { name: "delta_qualified_arrows", kind: "int32" },
|
||||
61: { name: "delta_qualified_rate", kind: "int32" },
|
||||
62: { name: "hit_rate", kind: "int32" },
|
||||
63: { name: "delta_hit_rate", kind: "int32" },
|
||||
},
|
||||
MatchInfo: {
|
||||
1: { name: "match_id", kind: "string" },
|
||||
|
||||
Reference in New Issue
Block a user