Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53ac4473f9 | ||
|
|
419cefd00e | ||
|
|
14f9ccd1ef | ||
|
|
899a222e3f | ||
|
|
ca727ec1f7 | ||
|
|
cc8f997600 | ||
|
|
bac84536c7 | ||
|
|
a81494973c | ||
|
|
94c46bb7df | ||
|
|
f3fab17687 | ||
|
|
d22d958215 | ||
|
|
b4ed13d2b5 | ||
|
|
b8b0ab4005 | ||
|
|
05f73acf47 | ||
|
|
eca15178f4 | ||
|
|
9eb6ef7cf1 | ||
|
|
00f84fc3e4 | ||
|
|
5bb9df8a03 | ||
|
|
bad336c7d4 | ||
|
|
cee09cd7fa | ||
|
|
ae3ba60191 | ||
|
|
f4b37fba00 | ||
|
|
f8bf9fdb63 | ||
|
|
21ed8066aa | ||
|
|
78fd64c697 | ||
|
|
169e2e4a51 | ||
|
|
19a4631df3 | ||
|
|
9ad2c5e12e | ||
|
|
9de435a369 | ||
|
|
5357f131de | ||
|
|
7093d3e4e5 | ||
|
|
e98e5129d2 | ||
|
|
01e8f6c6da |
@@ -3,6 +3,9 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"i18n:check": "node scripts/check-localization.cjs",
|
||||
"rank:i18n:check": "node scripts/check-rank-i18n.cjs",
|
||||
"battle:display:check": "node scripts/check-battle-display.cjs",
|
||||
"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",
|
||||
|
||||
@@ -243,6 +243,14 @@
|
||||
{
|
||||
"type": "folder",
|
||||
"value": "static/training-difficulty-design"
|
||||
},
|
||||
{
|
||||
"type": "folder",
|
||||
"value": "static/coin"
|
||||
},
|
||||
{
|
||||
"type": "folder",
|
||||
"value": "static/home-device"
|
||||
}
|
||||
],
|
||||
"include": []
|
||||
|
||||
@@ -264,11 +264,6 @@ function buildSchema({ messageName, definition, messages, enumNames }) {
|
||||
}
|
||||
|
||||
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 }
|
||||
|
||||
+4
-2
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import {
|
||||
watch
|
||||
} from "vue";
|
||||
@@ -62,8 +64,8 @@
|
||||
uni.removeStorageSync(`${env}_token`);
|
||||
clearSessionState();
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "账号已在其他设备登录",
|
||||
title: t('notice'),
|
||||
content: t('your_account_has_been_signed_in_on_another_device'),
|
||||
showCancel: false,
|
||||
});
|
||||
}
|
||||
|
||||
+39
-16
@@ -1,3 +1,4 @@
|
||||
import { t } from "@/locales";
|
||||
import { normalizeBattleApiResult } from "@/utils/matchAdapter";
|
||||
|
||||
let BASE_URL = "https://api.shelingxingqiu.com/api/shoot"; // 默认正式版
|
||||
@@ -33,7 +34,8 @@ function request(method, url, data = {}, baseUrl = BASE_URL, successCodes = [0],
|
||||
const token = uni.getStorageSync(
|
||||
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
|
||||
);
|
||||
const header = {};
|
||||
// 英文版全部后端 HTTP 请求统一声明语言,保留原有认证与响应处理。
|
||||
const header = {"Accept-Language": "en-US"};
|
||||
if (token) header.Authorization = `Bearer ${token || ""}`;
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
@@ -100,7 +102,7 @@ function request(method, url, data = {}, baseUrl = BASE_URL, successCodes = [0],
|
||||
if (message === "BIND_FAILD") {
|
||||
uni.$emit("device-bind-invalid");
|
||||
uni.showToast({
|
||||
title: "设备绑定状态已失效,请重新绑定",
|
||||
title: t('your_device_binding_has_expired_please_bind_it_again'),
|
||||
icon: "none",
|
||||
});
|
||||
reject({type: "DEVICE_BIND_INVALID", message});
|
||||
@@ -108,7 +110,7 @@ function request(method, url, data = {}, baseUrl = BASE_URL, successCodes = [0],
|
||||
}
|
||||
if (message === "ERROR_ORDER_UNPAY") {
|
||||
uni.showToast({
|
||||
title: "当前有未支付订单",
|
||||
title: t('you_have_an_unpaid_order'),
|
||||
icon: "none",
|
||||
});
|
||||
resolve({});
|
||||
@@ -116,7 +118,7 @@ function request(method, url, data = {}, baseUrl = BASE_URL, successCodes = [0],
|
||||
}
|
||||
if (message === "ROOM_EMPTY") {
|
||||
return uni.showToast({
|
||||
title: "房间已过期",
|
||||
title: t('this_room_has_expired'),
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
@@ -147,26 +149,26 @@ function handleRequestError(err, url) {
|
||||
// 根据错误类型显示不同提示
|
||||
if (err.errMsg) {
|
||||
if (err.errMsg.includes("timeout")) {
|
||||
showCustomToast("请求超时,请稍后重试", "timeout");
|
||||
showCustomToast(t('request_timed_out_please_try_again'), "timeout");
|
||||
} else if (err.errMsg.includes("fail")) {
|
||||
// 检查网络状态
|
||||
uni.getNetworkType({
|
||||
success: (res) => {
|
||||
if (res.networkType === "none") {
|
||||
showCustomToast("网络连接已断开,请检查网络设置", "network");
|
||||
showCustomToast(t('network_disconnected_check_your_network_settings'), "network");
|
||||
} else {
|
||||
showCustomToast("服务器连接失败,请稍后重试", "server");
|
||||
showCustomToast(t('cannot_connect_to_the_server_please_try_again'), "server");
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
showCustomToast("网络异常,请检查网络连接", "unknown");
|
||||
showCustomToast(t('network_error_check_your_connection'), "unknown");
|
||||
},
|
||||
});
|
||||
} else {
|
||||
showCustomToast("请求失败,请稍后重试", "general");
|
||||
showCustomToast(t('request_failed_please_try_again'), "general");
|
||||
}
|
||||
} else {
|
||||
showCustomToast("网络异常,请稍后重试", "unknown");
|
||||
showCustomToast(t('network_error_please_try_again'), "unknown");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,7 +564,7 @@ export const aimRenewAPI = () => {
|
||||
const token = uni.getStorageSync(
|
||||
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
|
||||
);
|
||||
const header = {};
|
||||
const header = {"Accept-Language": "en-US"};
|
||||
if (token) header.Authorization = `Bearer ${token}`;
|
||||
|
||||
uni.request({
|
||||
@@ -592,14 +594,20 @@ export const connectDeviceWifiAPI = async (ssid, password) => {
|
||||
);
|
||||
};
|
||||
|
||||
// 获取硬件盒子版本信息,用于判断当前设备是否需要 OTA 升级。
|
||||
export const getHardwareBoxVersionAPI = async () => {
|
||||
return request("GET", "/user/hardwareBox/version");
|
||||
// 查询设备当前联网类型及已连接的 WiFi 名称。
|
||||
export const getDeviceWifiStatusAPI = async () => {
|
||||
return request("GET", "/user/device/wifiStatus", {}, BASE_URL, [0], {
|
||||
showErrorToast: false,
|
||||
});
|
||||
};
|
||||
|
||||
// 发送硬件盒子 OTA 更新指令,服务端会返回后续轮询使用的任务 ID。
|
||||
export const sendHardwareBoxUpdateAPI = async (data) => {
|
||||
return request("POST", "/user/hardwareBox/sendUpdate", data);
|
||||
export const sendHardwareBoxUpdateAPI = async ({versionNumber, wifiSsid, wifiPassword}) => {
|
||||
return request("POST", "/user/hardwareBox/sendUpdate", {
|
||||
versionNumber,
|
||||
wifiSsid,
|
||||
wifiPassword,
|
||||
});
|
||||
};
|
||||
|
||||
export const addNoteAPI = async (id, remark) => {
|
||||
@@ -764,6 +772,21 @@ export const getGiftDetailAPI = (id) => {
|
||||
);
|
||||
};
|
||||
|
||||
// 分页获取启用门店及当前用户在各店的会员状态。
|
||||
export const getStoreVipListAPI = ({page = 1, pageSize = 20} = {}) => {
|
||||
return request("GET", "/store/list", {page, pageSize});
|
||||
};
|
||||
|
||||
// 查询当前用户在指定门店的会员状态及权益剩余次数。
|
||||
export const getStoreBenefitAPI = (storeId) => {
|
||||
return request("GET", "/store/benefit", {storeId});
|
||||
};
|
||||
|
||||
// 获取体验中心 VIP 介绍纯文本,无需登录。
|
||||
export const getStoreVipIntroAPI = () => {
|
||||
return request("GET", "/store/vip-intro");
|
||||
};
|
||||
|
||||
// 根据用户定位分页获取附近门店。
|
||||
export const getNearbyStoresAPI = ({
|
||||
longitude,
|
||||
|
||||
@@ -18,6 +18,10 @@ const TRAINING_START_AUDIO_KEY_MAP = Object.freeze({
|
||||
export const getTrainingStartAudioKey = (trainingType, fallback = "") =>
|
||||
TRAINING_START_AUDIO_KEY_MAP[trainingType] || fallback;
|
||||
|
||||
// 与服务端通关判定保持一致,proto3 省略的 false 同样按失败处理。
|
||||
export const getTrainingEndAudioKey = (completed) =>
|
||||
completed === true ? "通关成功" : "通关失败";
|
||||
|
||||
export const getPrecisionShotAudioKeys = (shootData, directionText = "") => {
|
||||
if (!shootData || typeof shootData !== "object") return [];
|
||||
const ring = Number(shootData.ring);
|
||||
@@ -201,6 +205,10 @@ export const audioFils = {
|
||||
"https://static.shelingxingqiu.com/shootmini/static/audio0820/%E6%9C%AA%E5%91%BD%E4%B8%AD.MP3",
|
||||
练习结束:
|
||||
"https://static.shelingxingqiu.com/shootmini/static/audio/%E7%BB%83%E4%B9%A0%E7%BB%93%E6%9D%9F.mp3",
|
||||
通关成功:
|
||||
"https://static.shelingxingqiu.com/shootaudio/%E9%80%9A%E5%85%B3%E6%88%90%E5%8A%9F.mp3",
|
||||
通关失败:
|
||||
"https://static.shelingxingqiu.com/shootaudio/%E9%80%9A%E5%85%B3%E5%A4%B1%E8%B4%A5.mp3",
|
||||
射箭声音:
|
||||
"https://static.shelingxingqiu.com/shootaudio/v4/v4/%E7%AE%AD%E9%A3%9E%E8%A1%8C.mp3",
|
||||
命中:
|
||||
|
||||
+37
-39
@@ -1,3 +1,4 @@
|
||||
import { t as translate } from "@/locales";
|
||||
import { formatTimestamp } from "@/util";
|
||||
|
||||
const loadImage = (src) =>
|
||||
@@ -277,7 +278,7 @@ export const generateShareImage = async (canvasId, data) => {
|
||||
|
||||
renderText(ctx, data.user.name, 18, "#000", 84, 36);
|
||||
renderText(ctx, data.recordDate, 9, "#fff", 348, 23, "center", 39);
|
||||
renderText(ctx, "今日打卡", 14, "#fff", 336, 38, "center", 39);
|
||||
renderText(ctx, translate('today_s_check_in'), 9, "#fff", 336, 38, "center", 39);
|
||||
|
||||
// 文本标签
|
||||
let cursorX = 84;
|
||||
@@ -295,7 +296,7 @@ export const generateShareImage = async (canvasId, data) => {
|
||||
);
|
||||
cursorX = drawTextBoxesLine(
|
||||
ctx,
|
||||
String(pointBook.distance) + " 米",
|
||||
String(pointBook.distance) + translate('m'),
|
||||
cursorX + 6,
|
||||
58,
|
||||
9,
|
||||
@@ -318,9 +319,9 @@ export const generateShareImage = async (canvasId, data) => {
|
||||
2
|
||||
);
|
||||
|
||||
renderText(ctx, "落点稳定性", 13, "#999", 25, 110);
|
||||
renderText(ctx, "黄心率", 13, "#999", 145, 110);
|
||||
renderText(ctx, "10环数", 13, "#999", 262, 110);
|
||||
renderText(ctx, translate('shot_grouping'), 13, "#999", 25, 110);
|
||||
renderText(ctx, translate('gold_hit_rate'), 13, "#999", 145, 110);
|
||||
renderText(ctx, translate('10s'), 13, "#999", 262, 110);
|
||||
|
||||
renderText(
|
||||
ctx,
|
||||
@@ -340,8 +341,8 @@ export const generateShareImage = async (canvasId, data) => {
|
||||
);
|
||||
renderText(ctx, arrowData.tenRings, 15, "#000", 262, 133);
|
||||
|
||||
renderText(ctx, "平均环数", 13, "#999", 25, 175);
|
||||
renderText(ctx, "总环数", 13, "#999", 145, 175);
|
||||
renderText(ctx, translate('average_score'), 13, "#999", 25, 175);
|
||||
renderText(ctx, translate('total_score'), 13, "#999", 145, 175);
|
||||
renderText(
|
||||
ctx,
|
||||
Number(arrowData.averageRing.toFixed(2)),
|
||||
@@ -361,7 +362,7 @@ export const generateShareImage = async (canvasId, data) => {
|
||||
|
||||
if (hasPoint) {
|
||||
drawRoundedRect(ctx, 20, 230, 5, 15, 5, "#FED847");
|
||||
renderText(ctx, "落点分布", 13, "#999", 32, 242);
|
||||
renderText(ctx, translate('shot_distribution'), 13, "#999", 32, 242);
|
||||
|
||||
const bowSrc = await loadImage(pointBook.bowtargetType.iconPng);
|
||||
const bowImg = await loadCanvasImage(canvas, bowSrc);
|
||||
@@ -421,7 +422,7 @@ export const generateShareImage = async (canvasId, data) => {
|
||||
);
|
||||
});
|
||||
|
||||
renderText(ctx, "环值", 9, "#999", 15, ringBarHeight - 1);
|
||||
renderText(ctx, translate('score'), 9, "#999", 15, ringBarHeight - 1);
|
||||
ringNames.forEach((ring, index) => {
|
||||
renderText(
|
||||
ctx,
|
||||
@@ -441,17 +442,17 @@ export const generateShareImage = async (canvasId, data) => {
|
||||
const qrcodeImg = await loadCanvasImage(canvas, qrcodeSrc);
|
||||
ctx.drawImage(qrcodeImg, 40, hasPoint ? 715 : 358, 68, 68);
|
||||
|
||||
renderText(ctx, "射灵星球", 18, "#333", 120, hasPoint ? 734 : 380);
|
||||
renderText(ctx, translate('sheling_planet'), 18, "#333", 120, hasPoint ? 734 : 380);
|
||||
renderText(
|
||||
ctx,
|
||||
"高效记录每一箭,快来一起打卡吧!",
|
||||
translate('track_every_arrow_join_us'),
|
||||
13,
|
||||
"#999",
|
||||
120,
|
||||
hasPoint ? 756 : 402
|
||||
);
|
||||
renderText(ctx, "扫码打卡", 13, "#FFA118", 142, hasPoint ? 777 : 422);
|
||||
const pointImg = await loadCanvasImage(canvas, "../static/point.png");
|
||||
renderText(ctx, translate('scan_to_check_in'), 13, "#FFA118", 142, hasPoint ? 777 : 422);
|
||||
const pointImg = await loadCanvasImage(canvas, "https://static.shelingxingqiu.com/shootmini/static/point.png");
|
||||
ctx.drawImage(pointImg, 120, hasPoint ? 765 : 410, 18, 14);
|
||||
// 2D 即时绘制,无需 ctx.draw()
|
||||
} catch (e) {
|
||||
@@ -539,10 +540,10 @@ export const sharePointData = async (canvasId, data) => {
|
||||
const bubble2Img = await loadCanvasImage(canvas, bubble2Src);
|
||||
const bubble3Img = await loadCanvasImage(canvas, bubble3Src);
|
||||
ctx.drawImage(bubble1Img, 10, 88, 143, 87);
|
||||
renderText(ctx, "本周箭数", 14, "#FDA103", 84, 116, "center");
|
||||
renderText(ctx, translate('arrows_this_week'), 14, "#FDA103", 84, 116, "center");
|
||||
renderText(ctx, data.weekArrow, 36, "#FA2A2A", 84, 152, "center");
|
||||
ctx.drawImage(bubble2Img, 65, 220, 143, 87);
|
||||
renderText(ctx, "本周消耗", 14, "#FDA103", 139, 248, "center");
|
||||
renderText(ctx, translate('calories_this_week'), 14, "#FDA103", 139, 248, "center");
|
||||
renderText(
|
||||
ctx,
|
||||
Math.round(data.weekArrow * 1.6),
|
||||
@@ -553,7 +554,7 @@ export const sharePointData = async (canvasId, data) => {
|
||||
"center"
|
||||
);
|
||||
ctx.drawImage(bubble3Img, 255, 52, 114, 92);
|
||||
renderText(ctx, "我的名次", 14, "#FDA103", 312, 80, "center");
|
||||
renderText(ctx, translate('my_rank'), 14, "#FDA103", 312, 80, "center");
|
||||
renderText(ctx, data.rank, 36, "#FA2A2A", 312, 116, "center");
|
||||
|
||||
const qrcodeSrc = await loadImage(
|
||||
@@ -562,8 +563,8 @@ export const sharePointData = async (canvasId, data) => {
|
||||
const qrcodeImg = await loadCanvasImage(canvas, qrcodeSrc);
|
||||
ctx.drawImage(qrcodeImg, 40, 383, 68, 68);
|
||||
|
||||
renderText(ctx, "射灵星球", 18, "#333", 120, 412);
|
||||
renderText(ctx, "高效记录每一箭,快来一起打卡吧!", 13, "#999", 120, 435);
|
||||
renderText(ctx, translate('sheling_planet'), 18, "#333", 120, 412);
|
||||
renderText(ctx, translate('track_every_arrow_join_us'), 13, "#999", 120, 435);
|
||||
// 2D 即时绘制,无需 ctx.draw()
|
||||
} catch (e) {
|
||||
console.error("generateShareImage 绘制失败:", e);
|
||||
@@ -635,7 +636,7 @@ export function renderScores(ctx, arrows = [], bgImg) {
|
||||
);
|
||||
} else {
|
||||
ctx.drawImage(
|
||||
"/static/score-bg.png",
|
||||
"https://static.shelingxingqiu.com/shootmini/static/score-bg.png",
|
||||
16 + (i % 9) * 30,
|
||||
290 + Math.ceil((i + 1) / 9) * 30,
|
||||
27,
|
||||
@@ -657,7 +658,7 @@ export function renderScores(ctx, arrows = [], bgImg) {
|
||||
ctx.drawImage(bgImg, 24 + rowIndex * 42, i > 5 ? 362 : 320, 38, 38);
|
||||
} else {
|
||||
ctx.drawImage(
|
||||
"/static/score-bg.png",
|
||||
"https://static.shelingxingqiu.com/shootmini/static/score-bg.png",
|
||||
24 + rowIndex * 42,
|
||||
i > 5 ? 362 : 320,
|
||||
38,
|
||||
@@ -708,10 +709,7 @@ export async function sharePractiseData(canvasId, type, user, data) {
|
||||
|
||||
// 头像与段位框属于装饰图片,缺失时使用兜底或跳过,避免阻断分享。
|
||||
const loadProfileImage = async (src, fallbackSrc = "", label = "") => {
|
||||
const normalizedSrc =
|
||||
typeof src === "string" && src.startsWith("../static/")
|
||||
? src.slice(2)
|
||||
: src;
|
||||
const normalizedSrc = typeof src === "string" ? src : "";
|
||||
if (normalizedSrc) {
|
||||
try {
|
||||
const path = await loadImage(normalizedSrc);
|
||||
@@ -725,20 +723,20 @@ export async function sharePractiseData(canvasId, type, user, data) {
|
||||
|
||||
const avatarImgPromise = loadProfileImage(
|
||||
user?.avatar,
|
||||
"/static/user-icon.png",
|
||||
"https://static.shelingxingqiu.com/shootmini/static/user-icon.png",
|
||||
"avatar"
|
||||
);
|
||||
const lvlImgPromise = loadProfileImage(user?.lvlImage, "", "level");
|
||||
|
||||
let titleImageSrc = "/static/first-try-title.png";
|
||||
let titleImageSrc = "https://static.shelingxingqiu.com/shootmini/static/first-try-title.png";
|
||||
if (type == 2) {
|
||||
titleImageSrc = "/static/practise-one-title.png";
|
||||
titleImageSrc = "https://static.shelingxingqiu.com/shootmini/static/practise-one-title.png";
|
||||
} else if (type == 3) {
|
||||
titleImageSrc = "/static/practise-two-title.png";
|
||||
titleImageSrc = "https://static.shelingxingqiu.com/shootmini/static/practise-two-title.png";
|
||||
}
|
||||
const titleImgPromise = loadCanvasImage(canvas, titleImageSrc);
|
||||
const scoreBgImgPromise = loadCanvasImage(canvas, "/static/score-bg.png");
|
||||
const qrCodeImgPromise = loadCanvasImage(canvas, "/static/qr-code.png");
|
||||
const scoreBgImgPromise = loadCanvasImage(canvas, "https://static.shelingxingqiu.com/shootmini/static/score-bg.png");
|
||||
const qrCodeImgPromise = loadCanvasImage(canvas, "https://static.shelingxingqiu.com/shootmini/static/qr-code.png");
|
||||
|
||||
const [avatarImg, lvlImg, titleImg, scoreBgImg, qrCodeImg] =
|
||||
await Promise.all([
|
||||
@@ -761,9 +759,9 @@ export async function sharePractiseData(canvasId, type, user, data) {
|
||||
renderText(ctx, user.nickName, 13, "#fff", 58, 34);
|
||||
renderRankTitle(ctx, user.lvlName);
|
||||
|
||||
let subTitle = "正式开启弓箭手之路";
|
||||
let subTitle = translate('your_archery_journey_starts_here');
|
||||
if (type > 1) {
|
||||
subTitle = `今日弓箭练习打卡 ${formatTimestamp(data.startTime)}`;
|
||||
subTitle = translate('archery_check_in_value', { p0: formatTimestamp(data.startTime) });
|
||||
}
|
||||
|
||||
ctx.drawImage(titleImg, (width - 160) / 2, 160, 160, 40);
|
||||
@@ -771,10 +769,10 @@ export async function sharePractiseData(canvasId, type, user, data) {
|
||||
ctx.font = "18px sans-serif";
|
||||
renderText(ctx, subTitle, 18, "#fff", width / 2, 224, "center");
|
||||
|
||||
renderText(ctx, "共", 14, "#fff", 122, 300);
|
||||
renderText(ctx, translate('total'), 14, "#fff", 102, 300);
|
||||
const totalRing = data.details.reduce((last, next) => last + next.ring, 0);
|
||||
renderText(ctx, totalRing, 14, "#fed847", 148, 300, "center");
|
||||
renderText(ctx, "环", 14, "#fff", 161, 300);
|
||||
renderText(ctx, translate('pts'), 14, "#fff", 161, 300);
|
||||
|
||||
renderLine(ctx, 77);
|
||||
renderLine(ctx, 185);
|
||||
@@ -782,10 +780,10 @@ export async function sharePractiseData(canvasId, type, user, data) {
|
||||
renderScores(ctx, data.details, scoreBgImg);
|
||||
|
||||
ctx.drawImage(qrCodeImg, width * 0.06, height * 0.87, 52, 52);
|
||||
renderText(ctx, "射灵平台", 12, "#fff", width * 0.26, height * 0.9);
|
||||
renderText(ctx, translate('sheling'), 12, "#fff", width * 0.26, height * 0.9);
|
||||
renderText(
|
||||
ctx,
|
||||
"快加入我们一起玩吧~",
|
||||
translate('join_us_and_play'),
|
||||
9,
|
||||
"rgba(255, 255, 255, 0.5)",
|
||||
width * 0.26,
|
||||
@@ -793,7 +791,7 @@ export async function sharePractiseData(canvasId, type, user, data) {
|
||||
);
|
||||
renderText(
|
||||
ctx,
|
||||
"后羿就是这样练成的",
|
||||
translate('practice_makes_a_great_archer'),
|
||||
9,
|
||||
"rgba(255, 255, 255, 0.5)",
|
||||
width * 0.26,
|
||||
@@ -905,7 +903,7 @@ export const generateShareCard = (canvasId, date, actual, total) => {
|
||||
const totalWidth = ctx.measureText(`${t || ""}`).width || 0;
|
||||
renderText(
|
||||
ctx,
|
||||
"环",
|
||||
translate('pts'),
|
||||
20,
|
||||
"#FFFFFF",
|
||||
centerX + totalWidth + 5,
|
||||
@@ -997,7 +995,7 @@ export const generateShareCard = (canvasId, date, actual, total) => {
|
||||
const totalWidth = ctx.measureText(`${t || ""}`).width || 0;
|
||||
renderText(
|
||||
ctx,
|
||||
"环",
|
||||
translate('pts'),
|
||||
20,
|
||||
"#FFFFFF",
|
||||
centerX + totalWidth + 5,
|
||||
|
||||
@@ -85,7 +85,7 @@ const props = defineProps({
|
||||
<image
|
||||
class="bg-image"
|
||||
v-if="type === 12"
|
||||
src="../static/device-assets/my-device-unbound-background.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/device-assets/my-device-unbound-background.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<image
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
const tabs = [
|
||||
{ image: "../static/tab-vip.png" },
|
||||
{ image: "https://static.shelingxingqiu.com/shootmini/static/tab-vip.png" },
|
||||
{ image: "https://static.shelingxingqiu.com/shootmini/static/tab-point-book.png" },
|
||||
{ image: "https://static.shelingxingqiu.com/shootmini/static/tab-mall.png" },
|
||||
];
|
||||
|
||||
function handleTabClick(index) {
|
||||
if (index === 0) {
|
||||
uni.navigateTo({
|
||||
url: "/pages/member/be-vip",
|
||||
uni.showToast({
|
||||
title: t('coming_soon'),
|
||||
icon: "none",
|
||||
});
|
||||
// uni.navigateTo({
|
||||
// url: "/pages/member/be-vip",
|
||||
// });
|
||||
}
|
||||
if (index === 1) {
|
||||
uni.navigateTo({
|
||||
|
||||
@@ -77,25 +77,25 @@ watch(
|
||||
/>
|
||||
<image
|
||||
v-if="rank === 1"
|
||||
src="../static/champ1.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/champ1.png"
|
||||
mode="widthFix"
|
||||
class="avatar-rank"
|
||||
/>
|
||||
<image
|
||||
v-if="rank === 2"
|
||||
src="../static/champ2.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/champ2.png"
|
||||
mode="widthFix"
|
||||
class="avatar-rank"
|
||||
/>
|
||||
<image
|
||||
v-if="rank === 3"
|
||||
src="../static/champ3.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/champ3.png"
|
||||
mode="widthFix"
|
||||
class="avatar-rank"
|
||||
/>
|
||||
<view v-if="rank > 3" class="rank-view">{{ rank }}</view>
|
||||
<image
|
||||
:src="src || '../static/user-icon.png'"
|
||||
:src="src || 'https://static.shelingxingqiu.com/shootmini/static/user-icon.png'"
|
||||
:mode="imageMode"
|
||||
:style="avatarImageStyle"
|
||||
class="avatar-image"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
|
||||
@@ -99,12 +101,12 @@ onBeforeUnmount(() => {
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/back-to-game-bg.png" mode="widthFix" />
|
||||
<block v-if="game.inBattle">
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/pk-icon.png" mode="widthFix" />
|
||||
<text>返回进行中的对局</text>
|
||||
<text>{{ t('return_to_your_current_match') }}</text>
|
||||
</block>
|
||||
<block v-else-if="game.roomID">
|
||||
<text>返回房间</text>
|
||||
<text>{{ t('back_to_room') }}</text>
|
||||
</block>
|
||||
<image src="../static/back.png" mode="widthFix" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/back.png" mode="widthFix" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
=
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { computed } from "vue";
|
||||
import BowPower from "@/components/BowPower.vue";
|
||||
import { RoundImages } from "@/constants";
|
||||
@@ -43,8 +45,8 @@ const normalRounds = computed(() => {
|
||||
</view>
|
||||
<view>
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/battle-header-melee.png" mode="widthFix" />
|
||||
<text>蓝队({{ bluePoints }})</text>
|
||||
<text>红队({{ redPoints }})</text>
|
||||
<text>{{ t('blue') }}{{ bluePoints }})</text>
|
||||
<text>{{ t('red') }}{{ redPoints }})</text>
|
||||
</view>
|
||||
<view class="players">
|
||||
<view>
|
||||
@@ -66,18 +68,18 @@ const normalRounds = computed(() => {
|
||||
.reduce((last, next) => last + next, 0)
|
||||
: ""
|
||||
}}</text>
|
||||
<text>环</text>
|
||||
<text>{{ t('pts') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<block v-if="roundResults.length < 3">
|
||||
<view v-for="i in 3 - roundResults.length" :key="i">
|
||||
<view v-for="pendingRoundNumber in 3 - roundResults.length" :key="pendingRoundNumber">
|
||||
<image
|
||||
:src="RoundImages[`round${i + roundResults.length}`]"
|
||||
:src="RoundImages[`round${pendingRoundNumber + roundResults.length}`]"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<view>
|
||||
<text></text>
|
||||
<text>环</text>
|
||||
<text>{{ t('pts') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
@@ -101,18 +103,18 @@ const normalRounds = computed(() => {
|
||||
.reduce((last, next) => last + next, 0)
|
||||
: ""
|
||||
}}</text>
|
||||
<text>环</text>
|
||||
<text>{{ t('pts') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<block v-if="roundResults.length < 3">
|
||||
<view v-for="i in 3 - roundResults.length" :key="i">
|
||||
<view v-for="pendingRoundNumber in 3 - roundResults.length" :key="pendingRoundNumber">
|
||||
<image
|
||||
:src="RoundImages[`round${i + roundResults.length}`]"
|
||||
:src="RoundImages[`round${pendingRoundNumber + roundResults.length}`]"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<view>
|
||||
<text></text>
|
||||
<text>环</text>
|
||||
<text>{{ t('pts') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
@@ -72,7 +72,7 @@ const isMember = (player = {}) => player.vip === true || player.sVip === true;
|
||||
</view>
|
||||
<image
|
||||
v-if="winner === 1"
|
||||
src="../static/winner-badge.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/winner-badge.png"
|
||||
mode="widthFix"
|
||||
class="left-winner-badge"
|
||||
/>
|
||||
@@ -100,7 +100,7 @@ const isMember = (player = {}) => player.vip === true || player.sVip === true;
|
||||
</view>
|
||||
<image
|
||||
v-if="winner === 2"
|
||||
src="../static/winner-badge.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/winner-badge.png"
|
||||
mode="widthFix"
|
||||
class="right-winner-badge"
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { computed } from "vue";
|
||||
import AppBackground from "@/components/AppBackground.vue";
|
||||
import Avatar from "@/components/Avatar.vue";
|
||||
@@ -58,7 +60,7 @@ const props = defineProps({
|
||||
</view>
|
||||
</view>
|
||||
<view @click="onClose">
|
||||
<image src="../static/close-white.png" mode="widthFix" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/close-white.png" mode="widthFix" />
|
||||
</view>
|
||||
</view>
|
||||
<view :style="{ width: '100%', marginBottom: '20px' }">
|
||||
@@ -66,9 +68,9 @@ const props = defineProps({
|
||||
</view>
|
||||
<view class="desc">
|
||||
<text>{{ arrows.length }}</text>
|
||||
<text>支箭,共</text>
|
||||
<text>{{ t('arrows_total') }}</text>
|
||||
<text>{{ arrows.reduce((a, b) => a + (b.ring || 0), 0) }}</text>
|
||||
<text>环</text>
|
||||
<text>{{ t('pts') }}</text>
|
||||
</view>
|
||||
<ScorePanel
|
||||
:completeEffect="false"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import useStore from "@/store";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
@@ -8,8 +10,8 @@ const { deviceBattery: power } = storeToRefs(store);
|
||||
|
||||
<template>
|
||||
<view class="container">
|
||||
<image src="../static/b-power.png" mode="widthFix" />
|
||||
<view>{{ power === null ? "电量--" : `电量${power}%` }}</view>
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/b-power.png" mode="widthFix" />
|
||||
<view>{{ power === null ? t('battery') : t('battery_value', { p0: power }) }}</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import {
|
||||
ref,
|
||||
watch,
|
||||
@@ -97,9 +99,9 @@ const ROUND_TIP_OFFSET_Y = -32;
|
||||
const EXPERIENCE_TIP_OFFSET_Y = -68;
|
||||
|
||||
function getRoundText(shot) {
|
||||
if (shot?.ringX) return "X环";
|
||||
if (shot?.ring) return `${shot.ring}环`;
|
||||
return props.missAsZero ? "0环" : "未上靶";
|
||||
if (shot?.ringX) return t('x');
|
||||
if (shot?.ring) return t('value_pts', { p0: shot.ring });
|
||||
return props.missAsZero ? t('0_pts') : t('miss');
|
||||
}
|
||||
|
||||
function buildShotEffectKey(team, shot, index) {
|
||||
@@ -527,17 +529,15 @@ onBeforeUnmount(() => {
|
||||
<view :class="['target', { 'target--shake': targetShaking }]">
|
||||
<view v-if="angle !== null" class="arrow-dir" :style="arrowStyle">
|
||||
<view :style="{ background: circleColor }">
|
||||
<image src="../static/dot-circle.png" mode="widthFix" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/dot-circle.png" mode="widthFix" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="stop" class="stop-sign">中场休息</view>
|
||||
<view v-if="stop" class="stop-sign">{{ t('halftime') }}</view>
|
||||
<view
|
||||
v-if="latestOne && latestOne.ring && user.id === latestOne.playerId"
|
||||
class="e-value fade-in-out"
|
||||
:style="getExperienceTipStyle(latestOne)"
|
||||
>
|
||||
经验 +1
|
||||
</view>
|
||||
>{{ t('xp_1') }}</view>
|
||||
<view
|
||||
v-if="latestOne"
|
||||
class="round-tip fade-in-out"
|
||||
@@ -552,9 +552,7 @@ onBeforeUnmount(() => {
|
||||
"
|
||||
class="e-value fade-in-out"
|
||||
:style="getExperienceTipStyle(bluelatestOne)"
|
||||
>
|
||||
经验 +1
|
||||
</view>
|
||||
>{{ t('xp_1') }}</view>
|
||||
<view
|
||||
v-if="bluelatestOne"
|
||||
class="round-tip fade-in-out"
|
||||
@@ -565,7 +563,7 @@ onBeforeUnmount(() => {
|
||||
<image
|
||||
v-if="pMode && isSvip && bow.ring > 0 && !shouldHideRedHit(index)"
|
||||
class="svip-hit-bg"
|
||||
src="../static/vip/svip-xuan.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/vip/svip-xuan.png"
|
||||
:style="getSvipHitBgStyle(bow)"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
@@ -585,7 +583,7 @@ onBeforeUnmount(() => {
|
||||
<image
|
||||
v-if="pMode && isSvip && bow.ring > 0 && !shouldHideBlueHit(index)"
|
||||
class="svip-hit-bg"
|
||||
src="../static/vip/svip-xuan.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/vip/svip-xuan.png"
|
||||
:style="getSvipHitBgStyle(bow)"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
@@ -638,10 +636,10 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
</view>
|
||||
<view class="simul" v-if="env !== 'release'">
|
||||
<button @click="simulShoot">模拟</button>
|
||||
<button @click="simulShoot2">射箭</button>
|
||||
<button @click="openAim">开瞄</button>
|
||||
<button @click="closeAim">关瞄</button>
|
||||
<button @click="simulShoot">{{ t('simulate') }}</button>
|
||||
<button @click="simulShoot2">{{ t('shoot') }}</button>
|
||||
<button @click="openAim">{{ t('sight_on') }}</button>
|
||||
<button @click="closeAim">{{ t('sight_off') }}</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||
import { getElementRect, calcRing, capsuleHeight } from "@/util";
|
||||
|
||||
@@ -258,7 +260,7 @@ onBeforeUnmount(() => {
|
||||
fontSize: '16px',
|
||||
marginLeft: '2px',
|
||||
}"
|
||||
>环</text
|
||||
>{{ t('pts') }}</text
|
||||
>
|
||||
</view>
|
||||
<view
|
||||
@@ -266,16 +268,16 @@ onBeforeUnmount(() => {
|
||||
@touchstart.stop="confirmAdd"
|
||||
:style="{ ...getNewPos() }"
|
||||
>
|
||||
<image src="../static/arrow-edit-save.png" mode="widthFix" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/arrow-edit-save.png" mode="widthFix" />
|
||||
</view>
|
||||
<view class="edit-btn delete-btn" @touchstart.stop="deleteArrow">
|
||||
<image src="../static/arrow-edit-delete.png" mode="widthFix" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/arrow-edit-delete.png" mode="widthFix" />
|
||||
</view>
|
||||
<view
|
||||
class="edit-btn drag-btn"
|
||||
@touchstart.stop="startDrag($event)"
|
||||
>
|
||||
<image src="../static/arrow-edit-move.png" mode="widthFix" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/arrow-edit-move.png" mode="widthFix" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -29,14 +29,14 @@ const props = defineProps({
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.normal {
|
||||
background-image: url("../static/bubble-tip.png");
|
||||
background-image: url("https://static.shelingxingqiu.com/shootmini/static/bubble-tip.png");
|
||||
width: 157rpx;
|
||||
height: 105rpx;
|
||||
padding-top: 10px;
|
||||
padding-left: 30rpx;
|
||||
}
|
||||
.normal2 {
|
||||
background-image: url("../static/bubble-tip4.png");
|
||||
background-image: url("https://static.shelingxingqiu.com/shootmini/static/bubble-tip4.png");
|
||||
width: 190rpx;
|
||||
height: 105rpx;
|
||||
padding-top: 10px;
|
||||
@@ -46,14 +46,14 @@ const props = defineProps({
|
||||
z-index: 1;
|
||||
}
|
||||
.long {
|
||||
background-image: url("../static/bubble-tip2.png");
|
||||
background-image: url("https://static.shelingxingqiu.com/shootmini/static/bubble-tip2.png");
|
||||
width: 370rpx;
|
||||
height: 70rpx;
|
||||
top: -50%;
|
||||
left: 49%;
|
||||
}
|
||||
.short {
|
||||
background-image: url("../static/bubble-tip3.png");
|
||||
background-image: url("https://static.shelingxingqiu.com/shootmini/static/bubble-tip3.png");
|
||||
width: 300rpx;
|
||||
height: 70rpx;
|
||||
top: -50%;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref } from "vue";
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
import AppBackground from "@/components/AppBackground.vue";
|
||||
@@ -179,43 +181,35 @@ const goCalibration = async () => {
|
||||
</view>
|
||||
<ScreenHint :show="showHint">
|
||||
<view v-if="hintType === 1" class="tip-content">
|
||||
<text>完成进行中的对局才能开启新的。</text>
|
||||
<text>您有正在进行中的对局,是否进入?</text>
|
||||
<text>{{ t('finish_your_current_match_before_starting_a_new_one') }}</text>
|
||||
<text>{{ t('you_have_a_match_in_progress_join_it') }}</text>
|
||||
<view>
|
||||
<button hover-class="none" @click="() => (showHint = false)">
|
||||
不进入
|
||||
</button>
|
||||
<button hover-class="none" @click="() => (showHint = false)">{{ t('stay_out') }}</button>
|
||||
<button hover-class="none" @click="backToGame" :disabled="isLoading">
|
||||
{{ isLoading ? "加载中..." : "进入" }}
|
||||
{{ isLoading ? t('loading') : t('join') }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="hintType === 2" class="tip-content">
|
||||
<text>离开比赛可能会导致比赛失败,</text>
|
||||
<text>确认离开吗?</text>
|
||||
<text>{{ t('leaving_may_result_in_a_loss') }}</text>
|
||||
<text>{{ t('are_you_sure_you_want_to_leave') }}</text>
|
||||
<view>
|
||||
<button hover-class="none" @click="goBack">离开比赛</button>
|
||||
<button hover-class="none" @click="() => (showHint = false)">
|
||||
继续比赛
|
||||
</button>
|
||||
<button hover-class="none" @click="goBack">{{ t('leave_match') }}</button>
|
||||
<button hover-class="none" @click="() => (showHint = false)">{{ t('continue_match') }}</button>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="hintType === 3" class="tip-content">
|
||||
<text>今天不玩了吗?</text>
|
||||
<text>{{ t('done_playing_for_today') }}</text>
|
||||
<view>
|
||||
<button hover-class="none" @click="() => (showHint = false)">
|
||||
取消
|
||||
</button>
|
||||
<button hover-class="none" @click="$clickSound(cancelMatching)">确认</button>
|
||||
<button hover-class="none" @click="() => (showHint = false)">{{ t('cancel') }}</button>
|
||||
<button hover-class="none" @click="$clickSound(cancelMatching)">{{ t('confirm') }}</button>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="hintType === 4" class="tip-content">
|
||||
<text>完成智能弓校准,即可解锁全部功能</text>
|
||||
<text>{{ t('calibrate_your_smart_bow_to_unlock_all_features') }}</text>
|
||||
<view>
|
||||
<button hover-class="none" @click="() => (showHint = false)">
|
||||
取消
|
||||
</button>
|
||||
<button hover-class="none" @click="goCalibration">去校准</button>
|
||||
<button hover-class="none" @click="() => (showHint = false)">{{ t('cancel') }}</button>
|
||||
<button hover-class="none" @click="goCalibration">{{ t('calibrate') }}</button>
|
||||
</view>
|
||||
</view>
|
||||
</ScreenHint>
|
||||
@@ -228,7 +222,7 @@ const goCalibration = async () => {
|
||||
<view :style="{ width: '100%' }"></view>
|
||||
</view>
|
||||
<view>
|
||||
<text>{{ loadingText || "加载中..." }}</text>
|
||||
<text>{{ loadingText || t('loading') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<DeviceChargingDialog />
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref } from "vue";
|
||||
import SButton from "@/components/SButton.vue";
|
||||
|
||||
@@ -27,7 +29,7 @@ const roomNumber = ref("");
|
||||
const createRoom = debounce(async () => {
|
||||
// 校验必填项:对战模式与靶纸均必须选择
|
||||
if (!battleMode.value || !targetMode.value) {
|
||||
uni.showToast({ title: '请完善创建信息', icon: 'none' });
|
||||
uni.showToast({ title: t('please_complete_the_room_settings'), icon: 'none' });
|
||||
return;
|
||||
}
|
||||
if (game.value.inBattle) {
|
||||
@@ -77,30 +79,30 @@ const createRoom = debounce(async () => {
|
||||
:class="{ 'battle-btn': true, 'battle-choosen': battleMode === 1 }"
|
||||
@click="() => (battleMode = 1)"
|
||||
>
|
||||
<text>对抗模式(1V1)</text>
|
||||
<text>{{ t('versus_1v1') }}</text>
|
||||
</view>
|
||||
<view
|
||||
:class="{ 'battle-btn': true, 'battle-choosen': battleMode === 3 }"
|
||||
@click="() => (battleMode = 3)"
|
||||
>
|
||||
<text>对抗模式(2V2)</text>
|
||||
<text>{{ t('versus_2v2') }}</text>
|
||||
</view>
|
||||
<view
|
||||
:class="{ 'battle-btn': true, 'battle-choosen': battleMode === 4 }"
|
||||
@click="() => (battleMode = 4)"
|
||||
>
|
||||
<text>对抗模式(3V3)</text>
|
||||
<text>{{ t('versus_3v3') }}</text>
|
||||
</view>
|
||||
<view
|
||||
:class="{ 'battle-btn': true, 'battle-choosen': battleMode === 2 }"
|
||||
@click="() => (battleMode = 2)"
|
||||
>
|
||||
<text>乱斗模式(3-10人)</text>
|
||||
<text>{{ t('free_for_all_3_10_players') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="target-options-header">
|
||||
<view class="target-options-header-line-left"></view>
|
||||
<view class="target-options-header-title">选择靶纸</view>
|
||||
<view class="target-options-header-title">{{ t('select_target_face') }}</view>
|
||||
<view class="target-options-header-line-right"></view>
|
||||
</view>
|
||||
<view class="target-options">
|
||||
@@ -108,16 +110,16 @@ const createRoom = debounce(async () => {
|
||||
:class="{ 'battle-btn': true, 'battle-choosen': targetMode === 1 }"
|
||||
@click="() => (targetMode = 1)"
|
||||
>
|
||||
<text>20厘米全环靶</text>
|
||||
<text>{{ t('20_cm_full_ring_target') }}</text>
|
||||
</view>
|
||||
<view
|
||||
:class="{ 'battle-btn': true, 'battle-choosen': targetMode === 2 }"
|
||||
@click="() => (targetMode = 2)"
|
||||
>
|
||||
<text>40厘米全环靶</text>
|
||||
<text>{{ t('40_cm_full_ring_target') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<SButton :disabled="loading" :onClick="() => { $clickSound(); return createRoom(); }">创建房间</SButton>
|
||||
<SButton :disabled="loading" :onClick="() => { $clickSound(); return createRoom(); }">{{ t('create_room') }}</SButton>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { storeToRefs } from "pinia";
|
||||
import ModalDialog from "@/components/ModalDialog.vue";
|
||||
import useStore from "@/store";
|
||||
|
||||
const chargingNotice =
|
||||
"检测到智能弓箭已开始充电,为保护电池,设备即将自动关机。\n(注意:此时拔出充电线将会重启设备。)";
|
||||
t('your_smart_bow_is_charging_and_will_shut_down_automatically_to_p');
|
||||
|
||||
const store = useStore();
|
||||
const { deviceChargingDialogVisible } = storeToRefs(store);
|
||||
@@ -16,7 +18,7 @@ const { hideDeviceChargingDialog } = store;
|
||||
:show="deviceChargingDialogVisible"
|
||||
:content="chargingNotice"
|
||||
:show-cancel="false"
|
||||
confirm-text="我知道了"
|
||||
:confirm-text="t('got_it')"
|
||||
:on-confirm="hideDeviceChargingDialog"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
variant: {
|
||||
type: String,
|
||||
default: "home",
|
||||
validator: (value) => ["home", "detail"].includes(value),
|
||||
},
|
||||
});
|
||||
|
||||
// 固定粒子分布营造随机感,避免每次进入页面时因运行时随机数产生位置跳变。
|
||||
const sparkSeeds = [
|
||||
[1, "dot", 18, 98, -28, 5, 5, 2.3, 0.4],
|
||||
[2, "streak", 23, 90, -38, 4, 18, 2, 1.1, true],
|
||||
[3, "dot", 28, 112, -17, 6, 6, 2.8, 1.9, true],
|
||||
[4, "dot", 32, 104, -12, 12, 12, 2.6, 0.7, true, true],
|
||||
[5, "dot", 36, 88, -24, 4, 4, 1.8, 1.4],
|
||||
[6, "streak", 40, 116, -8, 4, 20, 2.4, 2.2, true],
|
||||
[7, "dot", 44, 96, -16, 7, 7, 2.1, 0.2],
|
||||
[8, "dot", 48, 120, -4, 5, 5, 2.9, 1.3, true],
|
||||
[9, "dot", 51, 110, 3, 14, 14, 3, 2.7, true, true],
|
||||
[10, "streak", 54, 92, 8, 4, 22, 1.9, 0.8],
|
||||
[11, "dot", 58, 105, 14, 6, 6, 2.5, 1.6, true],
|
||||
[12, "dot", 62, 86, 21, 4, 4, 2, 0.5],
|
||||
[13, "dot", 66, 114, 10, 7, 7, 2.7, 2.1, true],
|
||||
[14, "streak", 70, 96, 28, 4, 18, 2.2, 1],
|
||||
[15, "dot", 74, 108, 20, 11, 11, 2.8, 2.4, true, true],
|
||||
[16, "dot", 80, 90, 35, 5, 5, 2.3, 0.3, true],
|
||||
[17, "dot", 21, 118, -8, 4, 4, 2.6, 1.7],
|
||||
[18, "streak", 27, 84, -31, 3, 16, 1.8, 0.6, true],
|
||||
[19, "dot", 34, 122, -3, 6, 6, 2.9, 2.3, true],
|
||||
[20, "dot", 42, 102, 5, 5, 5, 2.2, 0.9],
|
||||
[21, "dot", 47, 88, -10, 13, 13, 2.7, 1.5, true, true],
|
||||
[22, "streak", 56, 120, 16, 4, 21, 2.5, 2.6, true],
|
||||
[23, "dot", 64, 100, 24, 7, 7, 2.4, 0.1],
|
||||
[24, "dot", 72, 84, 38, 5, 5, 2, 1.2, true],
|
||||
[25, "dot", 82, 116, 12, 4, 4, 2.8, 2],
|
||||
[26, "dot", 68, 124, 7, 10, 10, 3.1, 2.9, true, true],
|
||||
[27, "streak", 77, 98, 33, 4, 19, 2.1, 0.7, true],
|
||||
[28, "dot", 29, 94, -20, 7, 7, 2.5, 1.4],
|
||||
[29, "dot", 53, 126, 0, 5, 5, 2.3, 2.2, true],
|
||||
[30, "dot", 60, 108, 18, 6, 6, 2.6, 0.4],
|
||||
];
|
||||
|
||||
const deviceSparks = computed(() => {
|
||||
const isDetail = props.variant === "detail";
|
||||
const sizeScale = isDetail ? 1.25 : 1;
|
||||
const bottomOffset = isDetail ? 120 : 0;
|
||||
|
||||
return sparkSeeds.map(([
|
||||
id,
|
||||
type,
|
||||
left,
|
||||
bottom,
|
||||
angle,
|
||||
width,
|
||||
height,
|
||||
duration,
|
||||
delay,
|
||||
long = false,
|
||||
large = false,
|
||||
]) => ({
|
||||
id,
|
||||
type,
|
||||
long,
|
||||
large,
|
||||
trackStyle: {
|
||||
left: `${left}%`,
|
||||
bottom: `${bottom + bottomOffset}rpx`,
|
||||
transform: `rotate(${angle}deg)`,
|
||||
},
|
||||
sparkStyle: {
|
||||
width: `${width * sizeScale}rpx`,
|
||||
height: `${height * sizeScale}rpx`,
|
||||
marginLeft: `${-(width * sizeScale) / 2}rpx`,
|
||||
animationDuration: `${duration}s`,
|
||||
animationDelay: `-${delay}s`,
|
||||
},
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view
|
||||
class="device-online-effects"
|
||||
:class="`device-online-effects--${variant}`"
|
||||
>
|
||||
<view class="device-online-rays-stage">
|
||||
<view class="device-online-rays-orbit">
|
||||
<view class="device-online-ray device-online-ray--1" />
|
||||
<view class="device-online-ray device-online-ray--2" />
|
||||
<view class="device-online-ray device-online-ray--3" />
|
||||
<view class="device-online-ray device-online-ray--4" />
|
||||
<view class="device-online-ray device-online-ray--5" />
|
||||
<view class="device-online-ray device-online-ray--6" />
|
||||
<view class="device-online-ray device-online-ray--7" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="device-online-particles">
|
||||
<view
|
||||
v-for="spark in deviceSparks"
|
||||
:key="spark.id"
|
||||
class="device-online-spark-track"
|
||||
:style="spark.trackStyle"
|
||||
>
|
||||
<view
|
||||
class="device-online-spark"
|
||||
:class="[
|
||||
`device-online-spark--${spark.type}`,
|
||||
{
|
||||
'device-online-spark--long': spark.long,
|
||||
'device-online-spark--large': spark.large,
|
||||
}
|
||||
]"
|
||||
:style="spark.sparkStyle"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.device-online-effects {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 3;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 光束沿底座椭圆轨迹扫动,用位移、明暗和横向缩放表达前后纵深。 */
|
||||
.device-online-rays-stage {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 112rpx;
|
||||
width: 100%;
|
||||
height: 140rpx;
|
||||
overflow: hidden;
|
||||
transform-origin: 50% 100%;
|
||||
}
|
||||
|
||||
.device-online-rays-orbit {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
transform-origin: 50% 100%;
|
||||
animation: device-rays-rotate 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.device-online-ray {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
width: 48rpx;
|
||||
height: 132rpx;
|
||||
margin-left: -24rpx;
|
||||
transform-origin: 50% 100%;
|
||||
border-radius: 50% 50% 12% 12%;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 255, 244, 0) 0%,
|
||||
rgba(255, 254, 239, 0.1) 36%,
|
||||
rgba(255, 248, 215, 0.22) 72%,
|
||||
rgba(255, 232, 151, 0.36) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.device-online-ray--1 {
|
||||
opacity: 0.55;
|
||||
transform: rotate(-55deg);
|
||||
}
|
||||
|
||||
.device-online-ray--2 {
|
||||
width: 68rpx;
|
||||
margin-left: -34rpx;
|
||||
opacity: 0.72;
|
||||
transform: rotate(-36deg);
|
||||
}
|
||||
|
||||
.device-online-ray--3 {
|
||||
width: 42rpx;
|
||||
margin-left: -21rpx;
|
||||
opacity: 0.6;
|
||||
transform: rotate(-18deg);
|
||||
}
|
||||
|
||||
.device-online-ray--4 {
|
||||
width: 76rpx;
|
||||
margin-left: -38rpx;
|
||||
opacity: 0.82;
|
||||
}
|
||||
|
||||
.device-online-ray--5 {
|
||||
width: 46rpx;
|
||||
margin-left: -23rpx;
|
||||
opacity: 0.64;
|
||||
transform: rotate(19deg);
|
||||
}
|
||||
|
||||
.device-online-ray--6 {
|
||||
width: 64rpx;
|
||||
margin-left: -32rpx;
|
||||
opacity: 0.7;
|
||||
transform: rotate(38deg);
|
||||
}
|
||||
|
||||
.device-online-ray--7 {
|
||||
width: 44rpx;
|
||||
margin-left: -22rpx;
|
||||
opacity: 0.56;
|
||||
transform: rotate(56deg);
|
||||
}
|
||||
|
||||
.device-online-particles {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.device-online-spark-track {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
transform-origin: center bottom;
|
||||
}
|
||||
|
||||
.device-online-spark {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: #fff6bd;
|
||||
box-shadow: 0 0 6rpx rgba(255, 225, 112, 0.65);
|
||||
animation: device-spark-rise 2.4s linear infinite;
|
||||
}
|
||||
|
||||
.device-online-spark--dot {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.device-online-spark--streak {
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 255, 235, 0) 0%,
|
||||
rgba(255, 250, 205, 0.75) 45%,
|
||||
#ffe379 100%
|
||||
);
|
||||
}
|
||||
|
||||
.device-online-spark--long {
|
||||
animation-name: device-spark-rise-long;
|
||||
}
|
||||
|
||||
.device-online-spark--large {
|
||||
background: #fffbdc;
|
||||
box-shadow:
|
||||
0 0 10rpx rgba(255, 243, 174, 0.95),
|
||||
0 0 20rpx rgba(255, 207, 74, 0.72);
|
||||
}
|
||||
|
||||
.device-online-effects--detail .device-online-rays-stage {
|
||||
bottom: 215rpx;
|
||||
height: 190rpx;
|
||||
transform: scaleX(1.35);
|
||||
}
|
||||
|
||||
.device-online-effects--detail .device-online-ray {
|
||||
height: 180rpx;
|
||||
}
|
||||
|
||||
.device-online-effects--detail .device-online-rays-orbit {
|
||||
animation-name: device-rays-rotate-detail;
|
||||
}
|
||||
|
||||
.device-online-effects--detail .device-online-spark {
|
||||
animation-name: device-spark-rise-detail;
|
||||
}
|
||||
|
||||
.device-online-effects--detail .device-online-spark--long {
|
||||
animation-name: device-spark-rise-detail-long;
|
||||
}
|
||||
|
||||
@keyframes device-spark-rise {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(8rpx) scale(0.45);
|
||||
}
|
||||
16% {
|
||||
opacity: 1;
|
||||
}
|
||||
72% {
|
||||
opacity: 0.62;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(-148rpx) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes device-spark-rise-long {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(8rpx) scale(0.4);
|
||||
}
|
||||
16% {
|
||||
opacity: 1;
|
||||
}
|
||||
72% {
|
||||
opacity: 0.55;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(-176rpx) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes device-spark-rise-detail {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(12rpx) scale(0.45);
|
||||
}
|
||||
16% {
|
||||
opacity: 1;
|
||||
}
|
||||
72% {
|
||||
opacity: 0.62;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(-225rpx) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes device-spark-rise-detail-long {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(12rpx) scale(0.4);
|
||||
}
|
||||
16% {
|
||||
opacity: 1;
|
||||
}
|
||||
72% {
|
||||
opacity: 0.55;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(-270rpx) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes device-rays-rotate {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.48;
|
||||
transform: translate(-42rpx, 4rpx) skewX(-8deg) scaleX(0.82);
|
||||
}
|
||||
25% {
|
||||
opacity: 0.88;
|
||||
transform: translate(0, 0) skewX(0deg) scaleX(1.05);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.48;
|
||||
transform: translate(42rpx, 4rpx) skewX(8deg) scaleX(0.82);
|
||||
}
|
||||
75% {
|
||||
opacity: 0.24;
|
||||
transform: translate(0, 10rpx) skewX(0deg) scaleX(0.68);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes device-rays-rotate-detail {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.48;
|
||||
transform: translate(-62rpx, 6rpx) skewX(-8deg) scaleX(0.82);
|
||||
}
|
||||
25% {
|
||||
opacity: 0.88;
|
||||
transform: translate(0, 0) skewX(0deg) scaleX(1.05);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.48;
|
||||
transform: translate(62rpx, 6rpx) skewX(8deg) scaleX(0.82);
|
||||
}
|
||||
75% {
|
||||
opacity: 0.24;
|
||||
transform: translate(0, 14rpx) skewX(0deg) scaleX(0.68);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref, watch, onMounted, computed } from "vue";
|
||||
import { getPointBookConfigAPI } from "@/apis";
|
||||
const props = defineProps({
|
||||
@@ -27,7 +29,7 @@ const props = defineProps({
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
const itemTexts = ["选择弓种", "选择练习距离", "选择靶纸", "选择组/箭数"];
|
||||
const itemTexts = [t('select_bow_type'), t('select_shooting_distance'), t('select_target_face'), t('select_ends_arrows')];
|
||||
const distances = [5, 8, 10, 18, 25, 30, 50, 60, 70];
|
||||
const groupArrows = [3, 6, 12, 18];
|
||||
|
||||
@@ -157,11 +159,9 @@ const formatSetAndAmount = computed(() => {
|
||||
return itemTexts[props.itemIndex];
|
||||
if (secondSelectIndex.value === 99 && !arrowAmount.value)
|
||||
return itemTexts[props.itemIndex];
|
||||
return `${selectedIndex.value === 99 ? sets.value : selectedIndex.value}组/${
|
||||
secondSelectIndex.value === 99
|
||||
return t('value_ends_value_arrows', { p0: selectedIndex.value === 99 ? sets.value : selectedIndex.value, p1: secondSelectIndex.value === 99
|
||||
? arrowAmount.value
|
||||
: groupArrows[secondSelectIndex.value]
|
||||
}箭`;
|
||||
: groupArrows[secondSelectIndex.value] });
|
||||
});
|
||||
onMounted(async () => {
|
||||
const config = uni.getStorageSync("point-book-config");
|
||||
@@ -187,13 +187,13 @@ onMounted(async () => {
|
||||
<view></view>
|
||||
<block>
|
||||
<text v-if="expand" :style="{ color: '#999', fontWeight: 'normal' }">{{
|
||||
itemIndex !== 3 ? itemTexts[itemIndex] : "选择组"
|
||||
itemIndex !== 3 ? itemTexts[itemIndex] : t('select_ends')
|
||||
}}</text>
|
||||
<text v-if="!expand && itemIndex === 0">{{
|
||||
value || itemTexts[itemIndex]
|
||||
}}</text>
|
||||
<text v-if="!expand && itemIndex === 1">{{
|
||||
value && value > 0 ? value + "米" : itemTexts[itemIndex]
|
||||
value && value > 0 ? value + t('m_92') : itemTexts[itemIndex]
|
||||
}}</text>
|
||||
<text v-if="!expand && itemIndex === 2">{{
|
||||
value || itemTexts[itemIndex]
|
||||
@@ -203,7 +203,7 @@ onMounted(async () => {
|
||||
<button hover-class="none">
|
||||
<image
|
||||
v-if="!noArrow"
|
||||
src="../static/arrow-grey.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/arrow-grey.png"
|
||||
mode="widthFix"
|
||||
:style="{ transform: expand ? 'rotateX(180deg)' : 'rotateX(0deg)' }"
|
||||
/>
|
||||
@@ -232,7 +232,7 @@ onMounted(async () => {
|
||||
@click="onSelectItem(index)"
|
||||
>
|
||||
<text>{{ item }}</text>
|
||||
<text>米</text>
|
||||
<text>{{ t('m_92') }}</text>
|
||||
</view>
|
||||
<view
|
||||
:style="{
|
||||
@@ -242,12 +242,12 @@ onMounted(async () => {
|
||||
<input
|
||||
v-model="meter"
|
||||
type="number"
|
||||
placeholder="自定义"
|
||||
:placeholder="t('custom')"
|
||||
placeholder-style="color: #DDDDDD"
|
||||
@focus="() => (selectedIndex = 9)"
|
||||
@blur="onMeterChange"
|
||||
/>
|
||||
<text>米</text>
|
||||
<text>{{ t('m_92') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="itemIndex === 2" class="bowtarget-items">
|
||||
@@ -274,7 +274,7 @@ onMounted(async () => {
|
||||
@click="onSelectItem(i)"
|
||||
>
|
||||
<text>{{ i }}</text>
|
||||
<text>组</text>
|
||||
<text>{{ t('ends') }}</text>
|
||||
</view>
|
||||
<view
|
||||
:style="{
|
||||
@@ -289,7 +289,7 @@ onMounted(async () => {
|
||||
@focus="() => (selectedIndex = 99)"
|
||||
@blur="onSetsChange"
|
||||
/>
|
||||
<text>组</text>
|
||||
<text>{{ t('ends') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
@@ -299,7 +299,7 @@ onMounted(async () => {
|
||||
color: '#999999',
|
||||
textAlign: 'center',
|
||||
}"
|
||||
>选择每组的箭数</view
|
||||
>{{ t('select_arrows_per_end') }}</view
|
||||
>
|
||||
<view class="amount-items">
|
||||
<view
|
||||
@@ -311,7 +311,7 @@ onMounted(async () => {
|
||||
@click="onSelectSecondItem(index)"
|
||||
>
|
||||
<text>{{ item }}</text>
|
||||
<text>箭</text>
|
||||
<text>{{ t('arrows') }}</text>
|
||||
</view>
|
||||
<view
|
||||
:style="{
|
||||
@@ -327,7 +327,7 @@ onMounted(async () => {
|
||||
@focus="() => (secondSelectIndex = 99)"
|
||||
@blur="onArrowAmountChange"
|
||||
/>
|
||||
<text>箭</text>
|
||||
<text>{{ t('arrows') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -10,8 +10,8 @@ defineProps({
|
||||
},
|
||||
});
|
||||
const bubbleTypes = [
|
||||
"../static/long-bubble.png",
|
||||
"../static/long-bubble-middle.png",
|
||||
"https://static.shelingxingqiu.com/shootmini/static/long-bubble.png",
|
||||
"https://static.shelingxingqiu.com/shootmini/static/long-bubble-middle.png",
|
||||
"https://static.shelingxingqiu.com/shootmini/static/long-bubble-tall.png",
|
||||
];
|
||||
</script>
|
||||
|
||||
+14
-12
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t, translateStatic } from "@/locales";
|
||||
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
|
||||
import HeaderProgress from "@/components/HeaderProgress.vue";
|
||||
import Avatar from "@/components/Avatar.vue";
|
||||
@@ -115,10 +117,10 @@ onBeforeUnmount(() => {
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="back-btn" @click="onClick">
|
||||
<image v-if="whiteBackArrow" src="../static/back.png" mode="widthFix" />
|
||||
<image v-if="whiteBackArrow" src="https://static.shelingxingqiu.com/shootmini/static/back.png" mode="widthFix" />
|
||||
<image
|
||||
v-if="!whiteBackArrow"
|
||||
src="../static/back-black.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/back-black.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
</view>
|
||||
@@ -156,13 +158,13 @@ onBeforeUnmount(() => {
|
||||
<text v-else class="truncate">{{ user.nickName }}</text>
|
||||
<image
|
||||
v-if="heat"
|
||||
:src="`../static/hot${heat}.png`"
|
||||
:src="`https://static.shelingxingqiu.com/shootmini/static/hot${heat}.png`"
|
||||
mode="widthFix"
|
||||
/>
|
||||
</block>
|
||||
<block v-else>
|
||||
<image src="../static/user-icon.png" mode="widthFix" />
|
||||
<text>新来的弓箭手你好呀~</text>
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/user-icon.png" mode="widthFix" />
|
||||
<text>{{ t('welcome_new_archer') }}</text>
|
||||
</block>
|
||||
</view>
|
||||
<block
|
||||
@@ -171,7 +173,7 @@ onBeforeUnmount(() => {
|
||||
'-箭前准备-感知距离-小试牛刀'.indexOf(title) === 11
|
||||
"
|
||||
>
|
||||
<text>{{ title }}</text>
|
||||
<text>{{ ['-箭前准备', '-感知距离', '-小试牛刀', '小试牛刀'].includes(title) ? translateStatic(title) : title }}</text>
|
||||
</block>
|
||||
<block
|
||||
v-if="
|
||||
@@ -181,14 +183,14 @@ onBeforeUnmount(() => {
|
||||
"
|
||||
>
|
||||
<view class="first-try-steps">
|
||||
<text :class="title === '-箭前准备' ? 'current-step' : ''">箭前准备</text>
|
||||
<text :class="title === '-箭前准备' ? 'current-step' : ''">{{ t('get_ready') }}</text>
|
||||
<text>-</text>
|
||||
<text :class="title === '-感知距离' ? 'current-step' : ''"
|
||||
>感知距离</text
|
||||
>{{ t('find_your_distance') }}</text
|
||||
>
|
||||
<text>-</text>
|
||||
<text :class="title === '-小试牛刀' ? 'current-step' : ''"
|
||||
>小试牛刀</text
|
||||
>{{ t('first_challenge') }}</text
|
||||
>
|
||||
</view>
|
||||
</block>
|
||||
@@ -196,7 +198,7 @@ onBeforeUnmount(() => {
|
||||
</view>
|
||||
<view v-if="pointBook" class="point-book-info">
|
||||
<text>{{ pointBook.bowType.name }}</text>
|
||||
<text>{{ pointBook.distance }} 米</text>
|
||||
<text>{{ pointBook.distance }}{{ t('m') }}</text>
|
||||
<text
|
||||
>{{
|
||||
pointBook.bowtargetType.name.substring(
|
||||
@@ -228,8 +230,8 @@ onBeforeUnmount(() => {
|
||||
class="battle-room-number"
|
||||
:style="battleRoomBtnStyle"
|
||||
>
|
||||
<text class="battle-room-number__text">房号: {{ game.roomNumber }}</text>
|
||||
<image src="../static/share2.png" mode="widthFix" class="battle-room-number__icon" />
|
||||
<text class="battle-room-number__text">{{ t('room') }}{{ game.roomNumber }}</text>
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/share2.png" mode="widthFix" class="battle-room-number__icon" />
|
||||
</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup>
|
||||
import { formatBattleTip } from "@/locales";
|
||||
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
|
||||
import audioManager from "@/audioManager";
|
||||
import { MESSAGETYPESV2 } from "@/constants";
|
||||
@@ -143,10 +144,10 @@ onBeforeUnmount(() => {
|
||||
|
||||
<template>
|
||||
<view class="container">
|
||||
<text>{{ (tips || "").replace(/你/g, "").replace(/重回/g, "") }}</text>
|
||||
<text>{{ formatBattleTip(tips) }}</text>
|
||||
<text v-if="totalShot > 0"> ({{ currentShot }}/{{ totalShot }}) </text>
|
||||
<button v-if="!!tips" hover-class="none" @click="updateSound">
|
||||
<image :src="`../static/sound${sound ? '' : '-off'}-yellow.png`" mode="widthFix" />
|
||||
<image :src="`https://static.shelingxingqiu.com/shootmini/static/sound${sound ? '' : '-off'}-yellow.png`" mode="widthFix" />
|
||||
</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
+14
-12
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from "vue";
|
||||
const props = defineProps({
|
||||
stopMatch: {
|
||||
@@ -11,17 +13,17 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
const playerNames = [
|
||||
"乐正青山",
|
||||
"宇文玉兰",
|
||||
"岑思宇",
|
||||
"邬梓瑜",
|
||||
"范子衿",
|
||||
"旗鼓相当的对手",
|
||||
"乐子睿",
|
||||
"时春晓",
|
||||
"柏孤鸿",
|
||||
"东宫锦瑟",
|
||||
"段干流云",
|
||||
t('yuezheng_qingshan'),
|
||||
t('yuwen_yulan'),
|
||||
t('cen_siyu'),
|
||||
t('wu_ziyu'),
|
||||
t('fan_zijin'),
|
||||
t('a_worthy_opponent'),
|
||||
t('le_zirui'),
|
||||
t('shi_chunxiao'),
|
||||
t('bai_guhong'),
|
||||
t('donggong_jinse'),
|
||||
t('duangan_liuyun'),
|
||||
];
|
||||
const textStyles = [
|
||||
{
|
||||
@@ -123,7 +125,7 @@ onBeforeUnmount(() => {
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<button hover-class="none" @click="$clickSound(stopMatch)">取消匹配</button>
|
||||
<button hover-class="none" @click="$clickSound(stopMatch)">{{ t('cancel_matchmaking') }}</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import IconButton from "./IconButton.vue";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -16,11 +18,11 @@ const props = defineProps({
|
||||
},
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: "取消",
|
||||
default: t('cancel_133'),
|
||||
},
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: "确定",
|
||||
default: t('confirm_134'),
|
||||
},
|
||||
showCancel: {
|
||||
type: Boolean,
|
||||
@@ -118,7 +120,7 @@ const handleClose = () => {
|
||||
</view>
|
||||
<view v-if="showClose" class="dialog-close">
|
||||
<IconButton
|
||||
src="../static/close-gold-outline.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/close-gold-outline.png"
|
||||
:width="30"
|
||||
:onClick="handleClose"
|
||||
/>
|
||||
@@ -193,6 +195,9 @@ const handleClose = () => {
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
max-height: 45vh;
|
||||
overflow-y: auto;
|
||||
overflow-wrap: break-word;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
@@ -216,6 +221,7 @@ const handleClose = () => {
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
@@ -229,11 +235,19 @@ const handleClose = () => {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dialog-actions.single .dialog-button {
|
||||
flex: 0 1 232rpx;
|
||||
}
|
||||
|
||||
.dialog-button {
|
||||
display: flex;
|
||||
width: 232rpx;
|
||||
height: 70rpx;
|
||||
line-height: 70rpx;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 70rpx;
|
||||
padding: 12rpx 20rpx;
|
||||
box-sizing: border-box;
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
border-radius: 44rpx;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
+25
-14
@@ -1,9 +1,11 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { computed } from "vue";
|
||||
import useStore from "@/store";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
const OTA_OFFLINE_TEXT = "请先开启智能弓";
|
||||
const OTA_OFFLINE_TEXT = t('please_turn_on_your_smart_bow_first');
|
||||
const store = useStore();
|
||||
const { deviceStatus } = storeToRefs(store);
|
||||
|
||||
@@ -54,11 +56,11 @@ const isFailure = computed(() => props.state === "update_failure");
|
||||
const progressValue = computed(() => Math.min(100, Math.max(0, Number(props.progress) || 0)));
|
||||
const progressPhaseText = computed(() => {
|
||||
const phaseText = {
|
||||
started: "正在准备固件更新",
|
||||
downloading: "正在下载固件",
|
||||
installing: "正在安装固件",
|
||||
started: t('preparing_firmware_update'),
|
||||
downloading: t('downloading_firmware'),
|
||||
installing: t('installing_firmware'),
|
||||
};
|
||||
return phaseText[props.phase] || "正在进行固件更新";
|
||||
return phaseText[props.phase] || t('updating_firmware');
|
||||
});
|
||||
|
||||
// 点击立即更新前先校验设备在线状态。
|
||||
@@ -130,19 +132,19 @@ const handleUpdateClick = () => {
|
||||
<text v-if="changelog" class="changelog-text">{{ changelog }}</text>
|
||||
<view class="btn-group">
|
||||
<view class="primary-btn" @click="handleUpdateClick">
|
||||
<text class="primary-btn-text">立即更新</text>
|
||||
<text class="primary-btn-text">{{ t('update_now') }}</text>
|
||||
</view>
|
||||
<text v-if="!forceUpdate" class="skip-text" @click="emit('skip')">暂不更新</text>
|
||||
<text v-if="!forceUpdate" class="skip-text" @click="emit('skip')">{{ t('later') }}</text>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 更新成功:图片左边距 34rpx,文案左边距 44rpx,按钮浮动底部居中 -->
|
||||
<block v-else-if="isSuccess">
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/ota/update-ok.png" mode="aspectFit" class="result-title-img" style="width: 220rpx; height: 62rpx;" />
|
||||
<text class="dialog-desc">固件更新已完成</text>
|
||||
<text class="dialog-desc">{{ t('firmware_update_complete') }}</text>
|
||||
<view class="btn-group-result">
|
||||
<view class="primary-btn" @click="emit('done')">
|
||||
<text class="primary-btn-text">关闭</text>
|
||||
<text class="primary-btn-text">{{ t('close') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
@@ -158,18 +160,19 @@ const handleUpdateClick = () => {
|
||||
<view class="progress-track">
|
||||
<view class="progress-fill" :style="{ width: `${progressValue}%` }"></view>
|
||||
</view>
|
||||
<text class="progress-warning">{{ t('please_stay_on_this_page') }}</text>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 更新失败:图片左边距 34rpx,文案左对齐 44rpx,按钮浮动底部居中 -->
|
||||
<block v-else-if="isFailure">
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/ota/update-fail.png" mode="aspectFit" class="result-title-img" style="width: 222rpx; height: 62rpx;" />
|
||||
<text class="dialog-desc">请确保:</text>
|
||||
<text class="dialog-desc">1、智能弓已开启</text>
|
||||
<text class="dialog-desc">2、网路连接稳定</text>
|
||||
<text class="dialog-desc">{{ t('please_make_sure') }}</text>
|
||||
<text class="dialog-desc">{{ t('1_your_smart_bow_is_on') }}</text>
|
||||
<text class="dialog-desc">{{ t('2_your_network_connection_is_stable') }}</text>
|
||||
<view class="btn-group-result">
|
||||
<view class="primary-btn" @click="emit('retry')">
|
||||
<text class="primary-btn-text">重试</text>
|
||||
<text class="primary-btn-text">{{ t('retry') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
@@ -184,7 +187,7 @@ const handleUpdateClick = () => {
|
||||
class="ota-close-below"
|
||||
@click="emit('close')"
|
||||
>
|
||||
<image src="../static/sicon/close.png" mode="aspectFit" style="width: 56rpx; height: 56rpx;" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/sicon/close.png" mode="aspectFit" style="width: 56rpx; height: 56rpx;" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -439,6 +442,14 @@ const handleUpdateClick = () => {
|
||||
background-color: #FED847;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
.progress-warning {
|
||||
display: block;
|
||||
margin-top: 16rpx;
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
font-size: 22rpx;
|
||||
line-height: 32rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 关闭按钮(位于弹窗下方) */
|
||||
.ota-close-below {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import useStore from "@/store";
|
||||
import { storeToRefs } from "pinia";
|
||||
const { user } = storeToRefs(useStore());
|
||||
@@ -18,8 +20,8 @@ const rowCount = new Array(6).fill(0);
|
||||
|
||||
const getRingText = (arrow) => {
|
||||
if (!arrow) return "-";
|
||||
if (arrow.ringX && arrow.ring) return "X环";
|
||||
return `${Number(arrow.ring) || 0}环`;
|
||||
if (arrow.ringX && arrow.ring) return t('x');
|
||||
return t('value_pts', { p0: Number(arrow.ring) || 0 });
|
||||
};
|
||||
const isMember = (player = {}) => player.vip === true || player.sVip === true;
|
||||
|
||||
@@ -40,10 +42,10 @@ const getMemberNicknameClass = (player = {}) => [
|
||||
opacity:
|
||||
(scores[0] || []).length + (scores[1] || []).length === 12 ? 1 : 0,
|
||||
}"
|
||||
src="../static/checked-green.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/checked-green.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<image :src="player.avatar || '../static/user-icon.png'" mode="widthFix" />
|
||||
<image :src="player.avatar || 'https://static.shelingxingqiu.com/shootmini/static/user-icon.png'" mode="widthFix" />
|
||||
<view
|
||||
v-if="isMember(player)"
|
||||
:class="['player-score-name', ...getMemberNicknameClass(player)]"
|
||||
@@ -71,7 +73,7 @@ const getMemberNicknameClass = (player = {}) => [
|
||||
scores
|
||||
.map((s) => (s || []).reduce((last, next) => last + next.ring, 0))
|
||||
.reduce((last, next) => last + next, 0)
|
||||
}}环</text
|
||||
}}{{ t('pts') }}</text
|
||||
>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { topThreeColors } from "@/constants";
|
||||
defineProps({
|
||||
avatar: {
|
||||
@@ -30,25 +32,25 @@ const rowCount = new Array(6).fill(0);
|
||||
<view>
|
||||
<image
|
||||
v-if="rank === 1"
|
||||
src="../static/champ1.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/champ1.png"
|
||||
mode="widthFix"
|
||||
class="avatar-rank"
|
||||
/>
|
||||
<image
|
||||
v-if="rank === 2"
|
||||
src="../static/champ2.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/champ2.png"
|
||||
mode="widthFix"
|
||||
class="avatar-rank"
|
||||
/>
|
||||
<image
|
||||
v-if="rank === 3"
|
||||
src="../static/champ3.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/champ3.png"
|
||||
mode="widthFix"
|
||||
class="avatar-rank"
|
||||
/>
|
||||
<view v-if="rank > 3" class="rank-view">{{ rank }}</view>
|
||||
<image
|
||||
:src="avatar || '../static/user-icon.png'"
|
||||
:src="avatar || 'https://static.shelingxingqiu.com/shootmini/static/user-icon.png'"
|
||||
mode="widthFix"
|
||||
:style="{ borderColor: topThreeColors[rank - 1] || '#fff' }"
|
||||
/>
|
||||
@@ -56,20 +58,20 @@ const rowCount = new Array(6).fill(0);
|
||||
<view>
|
||||
<view>
|
||||
<view v-for="(_, index) in rowCount" :key="index">
|
||||
<text>{{ arrows[index] ? `${arrows[index].ring}环` : "-" }}</text>
|
||||
<text>{{ arrows[index] ? t('value_pts', { p0: arrows[index].ring }) : "-" }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view>
|
||||
<view v-for="(_, index) in rowCount" :key="index">
|
||||
<text>{{
|
||||
arrows[index + 6] ? `${arrows[index + 6].ring}环` : "-"
|
||||
arrows[index + 6] ? t('value_pts', { p0: arrows[index + 6].ring }) : "-"
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view>
|
||||
<text>{{ arrows.reduce((last, next) => last + next.ring, 0) }}环</text>
|
||||
<text>积分{{ totalScore }}</text>
|
||||
<text>{{ arrows.reduce((last, next) => last + next.ring, 0) }}{{ t('pts') }}</text>
|
||||
<text>{{ t('points') }}{{ totalScore }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import Avatar from "@/components/Avatar.vue";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -38,16 +40,16 @@ const seats = new Array(props.total).fill(1);
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/player-bg.png" mode="widthFix" />
|
||||
<view v-if="players[index] && players[index].name" class="avatar">
|
||||
<Avatar
|
||||
:src="players[index].avatar || '../static/user-icon.png'"
|
||||
:src="players[index].avatar || 'https://static.shelingxingqiu.com/shootmini/static/user-icon.png'"
|
||||
:size="40"
|
||||
/>
|
||||
<text
|
||||
:style="{ opacity: players[index] && !!players[index].state ? 1 : 0 }"
|
||||
>已准备</text
|
||||
>{{ t('ready') }}</text
|
||||
>
|
||||
</view>
|
||||
<view v-else class="player-unknow">
|
||||
<image src="../static/question-mark.png" mode="widthFix" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/question-mark.png" mode="widthFix" />
|
||||
</view>
|
||||
<view
|
||||
v-if="players[index] && players[index].name && isMember(players[index])"
|
||||
@@ -67,12 +69,10 @@ const seats = new Array(props.total).fill(1);
|
||||
>
|
||||
{{ players[index].name }}
|
||||
</text>
|
||||
<text v-else class="player-seat-name" :style="{ color: '#fff9' }">
|
||||
虚位以待
|
||||
</text>
|
||||
<view v-if="index === 0" class="founder">管理员</view>
|
||||
<text v-else class="player-seat-name" :style="{ color: '#fff9' }">{{ t('waiting_for_a_player') }}</text>
|
||||
<view v-if="index === 0" class="founder">{{ t('host') }}</view>
|
||||
<!-- <image
|
||||
:src="`../static/player-${index + 1}.png`"
|
||||
:src="`https://static.shelingxingqiu.com/shootmini/static/player-${index + 1}.png`"
|
||||
mode="widthFix"
|
||||
class="player-bg"
|
||||
/> -->
|
||||
@@ -83,7 +83,7 @@ const seats = new Array(props.total).fill(1);
|
||||
class="remove-player"
|
||||
@click="() => removePlayer(players[index])"
|
||||
>
|
||||
<image src="../static/close-white.png" mode="widthFix" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/close-white.png" mode="widthFix" />
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref, watch } from "vue";
|
||||
import Avatar from "@/components/Avatar.vue";
|
||||
|
||||
@@ -55,12 +57,12 @@ const onClick = async () => {
|
||||
|
||||
<template>
|
||||
<view class="rank-item" :style="{ borderWidth: borderWidth + 'rpx' }">
|
||||
<image v-if="data.rank === 1" src="../static/point-no1.png" />
|
||||
<image v-else-if="data.rank === 2" src="../static/point-no2.png" />
|
||||
<image v-else-if="data.rank === 3" src="../static/point-no3.png" />
|
||||
<image v-if="data.rank === 1" src="https://static.shelingxingqiu.com/shootmini/static/point-no1.png" />
|
||||
<image v-else-if="data.rank === 2" src="https://static.shelingxingqiu.com/shootmini/static/point-no2.png" />
|
||||
<image v-else-if="data.rank === 3" src="https://static.shelingxingqiu.com/shootmini/static/point-no3.png" />
|
||||
<text v-else>{{ data.rank || "" }}</text>
|
||||
<view>
|
||||
<Avatar :src="data.avatar || '../static/user-icon.png'" :size="36" />
|
||||
<Avatar :src="data.avatar || 'https://static.shelingxingqiu.com/shootmini/static/user-icon.png'" :size="36" />
|
||||
<view>
|
||||
<view v-if="isMember(data)" :class="getMemberNicknameClass(data)">
|
||||
<text class="member-nickname__text">{{ data.name }}</text>
|
||||
@@ -70,24 +72,24 @@ const onClick = async () => {
|
||||
</view>
|
||||
<text v-else class="truncate">{{ data.name }}</text>
|
||||
<view>
|
||||
<text>{{ data.totalDay }}天</text>
|
||||
<text>{{ data.totalDay }}{{ t('days') }}</text>
|
||||
<view />
|
||||
<text>平均{{ Number(data.averageRing.toFixed(1)) }}环</text>
|
||||
<text>{{ t('average') }}{{ Number(data.averageRing.toFixed(1)) }}{{ t('pts') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-info">
|
||||
<text>{{ data.weekArrow }}</text>
|
||||
<text>箭</text>
|
||||
<text>{{ t('arrows') }}</text>
|
||||
</view>
|
||||
<view class="item-info">
|
||||
<text>{{ Math.round(data.weekArrow * 1.6) }}</text>
|
||||
<text>千卡</text>
|
||||
<text>{{ t('kcal') }}</text>
|
||||
</view>
|
||||
<button hover-class="none" @click="onClick">
|
||||
<text>{{ likeCount }}</text>
|
||||
<image
|
||||
:src="`../static/like-${like ? 'on' : 'off'}.png`"
|
||||
:src="`https://static.shelingxingqiu.com/shootmini/static/like-${like ? 'on' : 'off'}.png`"
|
||||
mode="widthFix"
|
||||
/>
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -48,7 +50,7 @@ onMounted(() => {
|
||||
<text>{{
|
||||
bowOptions[data.bowType] ? bowOptions[data.bowType].name : ""
|
||||
}}</text>
|
||||
<text>{{ data.distance }} 米</text>
|
||||
<text>{{ data.distance }}{{ t('m') }}</text>
|
||||
<text>{{
|
||||
targetOptions[data.targetType]
|
||||
? targetOptions[data.targetType].name
|
||||
@@ -59,9 +61,9 @@ onMounted(() => {
|
||||
<text>{{ data.createAt }}</text>
|
||||
</view>
|
||||
<view>
|
||||
<text>黄心率:{{ Number((data.yellowRate * 100).toFixed(2)) }}%</text>
|
||||
<text>10环数:{{ data.tenRings }}</text>
|
||||
<text>平均:{{ data.averageRing }}</text>
|
||||
<text>{{ t('gold_hit_rate_156') }}{{ Number((data.yellowRate * 100).toFixed(2)) }}%</text>
|
||||
<text>{{ t('10s_157') }}{{ data.tenRings }}</text>
|
||||
<text>{{ t('average_158') }}{{ data.averageRing }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="right-part">
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -25,9 +27,9 @@ const onClick = () => {
|
||||
@click="onClick"
|
||||
:style="{ transform: 'translateX(' + (mode ? '-58' : '4') + 'rpx)' }"
|
||||
>
|
||||
<text>放大</text>
|
||||
<text>{{ t('zoom') }}</text>
|
||||
<view :style="{ background: mode ? '#D8D8D8' : '#53EF56' }"></view>
|
||||
<text>真实</text>
|
||||
<text>{{ t('actual_size') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref, reactive, watch, onMounted } from "vue";
|
||||
import { getAppConfig, donateAPI } from "@/apis";
|
||||
import useStore from "@/store";
|
||||
@@ -47,7 +49,7 @@ const onPay = async (index) => {
|
||||
signType: "RSA",
|
||||
async success(res) {
|
||||
uni.showToast({
|
||||
title: "感谢您的支持!",
|
||||
title: t('thank_you_for_your_support'),
|
||||
icon: "none",
|
||||
});
|
||||
props.onClose();
|
||||
@@ -55,7 +57,7 @@ const onPay = async (index) => {
|
||||
fail(res) {
|
||||
console.log("pay error", res);
|
||||
uni.showToast({
|
||||
title: "取消支付",
|
||||
title: t('payment_canceled'),
|
||||
icon: "none",
|
||||
});
|
||||
props.onClose();
|
||||
@@ -94,7 +96,7 @@ onMounted(async () => {
|
||||
<template>
|
||||
<view class="container">
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/donate.png" mode="widthFix" />
|
||||
<text>感谢您对我们公益项目的支持!</text>
|
||||
<text>{{ t('thank_you_for_supporting_our_community_project') }}</text>
|
||||
<view class="amounts">
|
||||
<button
|
||||
v-for="(item, index) in amounts"
|
||||
@@ -113,25 +115,25 @@ onMounted(async () => {
|
||||
@click="checked = !checked"
|
||||
:style="{ marginBottom: !checked ? '20rpx' : '0' }"
|
||||
>
|
||||
<image v-if="checked" src="../static/checked.png" mode="widthFix" />
|
||||
<image v-if="checked" src="https://static.shelingxingqiu.com/shootmini/static/checked.png" mode="widthFix" />
|
||||
<view v-else></view>
|
||||
<text>我想给建议(选填)</text>
|
||||
<text>{{ t('i_have_a_suggestion_optional') }}</text>
|
||||
</view>
|
||||
<view v-if="checked">
|
||||
<view>
|
||||
<text>您的姓名</text>
|
||||
<text>{{ t('your_name') }}</text>
|
||||
<input v-model="formData.name" />
|
||||
</view>
|
||||
<view>
|
||||
<text>手机/微信号</text>
|
||||
<text>{{ t('phone_wechat_id') }}</text>
|
||||
<input v-model="formData.account" />
|
||||
</view>
|
||||
<view>
|
||||
<text>所在机构</text>
|
||||
<text>{{ t('organization') }}</text>
|
||||
<input v-model="formData.organization" />
|
||||
</view>
|
||||
<view>
|
||||
<text>建议</text>
|
||||
<text>{{ t('suggestion') }}</text>
|
||||
<textarea v-model="formData.suggestion" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -56,7 +58,7 @@ const ringText = (ring) => {
|
||||
{{ b && b.ring !== undefined ? b.ring : "" }}
|
||||
</text>
|
||||
</view>
|
||||
<text>环值</text>
|
||||
<text>{{ t('score') }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||
const props = defineProps({
|
||||
isFinal: {
|
||||
@@ -47,64 +49,56 @@ onBeforeUnmount(() => {
|
||||
|
||||
<template>
|
||||
<view class="round-end-tip">
|
||||
<text>第{{ round }}轮射箭结束</text>
|
||||
<text>{{ t('round') }}{{ round }}{{ t('shooting_complete') }}</text>
|
||||
<block v-if="!isFinal">
|
||||
<view class="point-view1" v-if="bluePoint !== 0 || redPoint !== 0">
|
||||
<text>本轮蓝队</text>
|
||||
<text>{{ t('blue_team_this_round') }}</text>
|
||||
<text>{{
|
||||
(roundData.shoots[1] || []).reduce(
|
||||
(last, next) => last + next.ring,
|
||||
0
|
||||
)
|
||||
}}</text>
|
||||
<text>环,红队</text>
|
||||
<text>{{ t('pts_red_team') }}</text>
|
||||
<text>{{
|
||||
(roundData.shoots[2] || []).reduce(
|
||||
(last, next) => last + next.ring,
|
||||
0
|
||||
)
|
||||
}}</text>
|
||||
<text>环</text>
|
||||
<text>{{ t('pts') }}</text>
|
||||
</view>
|
||||
<text
|
||||
v-if="bluePoint === 0 && redPoint === 0"
|
||||
:style="{ marginTop: '20px' }"
|
||||
>
|
||||
连续3个来回双方均无人射箭,比赛取消。
|
||||
</text>
|
||||
<text v-if="bluePoint !== 0 && bluePoint === redPoint">
|
||||
红队、蓝队各得<text :style="{ color: '#fed847', margin: '0 5px' }">{{
|
||||
>{{ t('no_arrows_were_shot_by_either_team_for_3_consecutive_turns_match') }}</text>
|
||||
<text v-if="bluePoint !== 0 && bluePoint === redPoint">{{ t('red_and_blue_teams_each_earn') }}<text :style="{ color: '#fed847', margin: '0 5px' }">{{
|
||||
redPoint
|
||||
}}</text
|
||||
>分
|
||||
</text>
|
||||
<text v-if="bluePoint > redPoint">
|
||||
蓝队获胜,得<text :style="{ color: '#fed847', margin: '0 5px' }">{{
|
||||
>{{ t('pts_175') }}</text>
|
||||
<text v-if="bluePoint > redPoint">{{ t('blue_team_wins_and_earns') }}<text :style="{ color: '#fed847', margin: '0 5px' }">{{
|
||||
bluePoint
|
||||
}}</text
|
||||
>分
|
||||
</text>
|
||||
<text v-if="bluePoint < redPoint">
|
||||
红队获胜,得<text :style="{ color: '#fed847', margin: '0 5px' }">{{
|
||||
>{{ t('pts_175') }}</text>
|
||||
<text v-if="bluePoint < redPoint">{{ t('red_team_wins_and_earns') }}<text :style="{ color: '#fed847', margin: '0 5px' }">{{
|
||||
redPoint
|
||||
}}</text
|
||||
>分
|
||||
</text>
|
||||
>{{ t('pts_175') }}</text>
|
||||
</block>
|
||||
<block v-if="isFinal">
|
||||
<view class="point-view2">
|
||||
<text>蓝队</text>
|
||||
<text>{{ t('blue_team') }}</text>
|
||||
<text>{{ bluePoint }}</text>
|
||||
<text>分,红队</text>
|
||||
<text>{{ t('pts_red_team_179') }}</text>
|
||||
<text>{{ redPoint }}</text>
|
||||
<text>分</text>
|
||||
<text>{{ t('pts_180') }}</text>
|
||||
</view>
|
||||
<text>同分僵局!最后一箭定江山</text>
|
||||
<text>{{ t('a_tie_one_final_arrow_decides_the_winner') }}</text>
|
||||
<view class="final-shoot">
|
||||
<text>{{ count }}</text>
|
||||
<text>秒后蓝红双方</text>
|
||||
<text>决金箭</text>
|
||||
<text>一箭决胜负</text>
|
||||
<text>{{ t('s_until_both_teams_shoot') }}</text>
|
||||
<text>{{ t('golden_arrow') }}</text>
|
||||
<text>{{ t('one_arrow_decides_it_all') }}</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
@@ -69,7 +69,7 @@ const onBtnClick = debounce(async () => {
|
||||
<slot />
|
||||
</block>
|
||||
<block v-else>
|
||||
<image src="../static/btn-loading.png" mode="widthFix" class="loading" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/btn-loading.png" mode="widthFix" class="loading" />
|
||||
</block>
|
||||
</button>
|
||||
</template>
|
||||
@@ -77,8 +77,13 @@ const onBtnClick = debounce(async () => {
|
||||
<style scoped>
|
||||
.sbtn {
|
||||
margin: 0 auto;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
min-height: 88rpx;
|
||||
height: auto;
|
||||
line-height: 1.3;
|
||||
padding: 20rpx 24rpx;
|
||||
box-sizing: border-box;
|
||||
white-space: normal;
|
||||
overflow-wrap: break-word;
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
display: flex;
|
||||
|
||||
@@ -60,7 +60,7 @@ watch(
|
||||
mode="widthFix"
|
||||
/>
|
||||
<view class="close-btn" @click="onClose" v-if="!noBg">
|
||||
<image src="../static/close-yellow.png" mode="widthFix" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/close-yellow.png" mode="widthFix" />
|
||||
</view>
|
||||
<slot></slot>
|
||||
</view>
|
||||
|
||||
@@ -71,7 +71,7 @@ watch(
|
||||
margin: 100 / (total * 2) + 'px',
|
||||
}"
|
||||
>
|
||||
<image src="../static/score-bg.png" mode="widthFix" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/score-bg.png" mode="widthFix" />
|
||||
<text
|
||||
:style="{ fontWeight: arrows[index] !== undefined ? 'bold' : 'normal' }"
|
||||
>{{
|
||||
@@ -93,7 +93,7 @@ watch(
|
||||
padding: 1vw 0;
|
||||
}
|
||||
.score-item {
|
||||
/* background-image: url("../static/score-bg.png");
|
||||
/* background-image: url("https://static.shelingxingqiu.com/shootmini/static/score-bg.png");
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center; */
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
const props = defineProps({
|
||||
arrows: {
|
||||
type: Array,
|
||||
@@ -12,19 +14,19 @@ const getSum = (...arrows) => {
|
||||
(total, arrow) => total + (Number(arrow.ring) || 0),
|
||||
0
|
||||
);
|
||||
return `${sum}环`;
|
||||
return t('value_pts', { p0: sum });
|
||||
};
|
||||
const roundsName = ["第一轮", "第二轮", "第三轮", "第四轮"];
|
||||
const roundsName = [t('round_1'), t('round_2'), t('round_3'), t('round_4')];
|
||||
const getShowText = (arrow) => {
|
||||
if (!arrow) return "-";
|
||||
return arrow.ringX ? "X" : `${Number(arrow.ring) || 0}环`;
|
||||
return arrow.ringX ? "X" : t('value_pts', { p0: Number(arrow.ring) || 0 });
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<view class="container">
|
||||
<view>
|
||||
<text :style="{ transform: 'translateX(-10%)' }">总成绩</text>
|
||||
<text>{{ arrows.reduce((last, next) => last + next.ring, 0) }}环</text>
|
||||
<text :style="{ transform: 'translateX(-10%)' }">{{ t('final_score') }}</text>
|
||||
<text>{{ arrows.reduce((last, next) => last + next.ring, 0) }}{{ t('pts') }}</text>
|
||||
</view>
|
||||
<view
|
||||
v-for="(_, index) in new Array(
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
import IconButton from "@/components/IconButton.vue";
|
||||
import SButton from "@/components/SButton.vue";
|
||||
@@ -86,9 +88,9 @@ const openCoachComment = () => {
|
||||
<image :src="tipSrc" mode="widthFix" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/finish-frame.png" mode="widthFix" />
|
||||
<text
|
||||
>完成<text class="gold-text">{{ validArrows }}</text
|
||||
>箭,获得<text class="gold-text">{{ validArrows }}</text
|
||||
>点经验</text
|
||||
>{{ t('completed') }}<text class="gold-text">{{ validArrows }}</text
|
||||
>{{ t('arrows_earned') }}<text class="gold-text">{{ validArrows }}</text
|
||||
>{{ t('xp') }}</text
|
||||
>
|
||||
</view>
|
||||
<view
|
||||
@@ -96,11 +98,11 @@ const openCoachComment = () => {
|
||||
:style="{ transform: `translateY(${showPanel ? '0%' : '100%'})` }"
|
||||
>
|
||||
<view>
|
||||
<text>本局成绩(共{{ totalRing }}环):</text>
|
||||
<text>{{ t('score_this_match_total') }}{{ totalRing }}{{ t('pts_194') }}</text>
|
||||
<button @click="() => (showBowData = true)">
|
||||
<text>查看靶纸</text>
|
||||
<text>{{ t('view_target') }}</text>
|
||||
<image
|
||||
src="../static/enter-arrow-blue.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/enter-arrow-blue.png"
|
||||
mode="widthFix"
|
||||
:style="{ width: '20px' }"
|
||||
/>
|
||||
@@ -109,20 +111,20 @@ const openCoachComment = () => {
|
||||
<view :style="{ gridTemplateColumns: `repeat(${rowCount}, 1fr)` }">
|
||||
<view v-for="(_, index) in new Array(total).fill(0)" :key="index">
|
||||
{{ getRing(arrows[index])
|
||||
}}<text v-if="getRing(arrows[index]) !== '-'">环</text>
|
||||
}}<text v-if="getRing(arrows[index]) !== '-'">{{ t('pts') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view>
|
||||
<block v-if="validArrows === total">
|
||||
<IconButton
|
||||
name="分享"
|
||||
src="../static/share.png"
|
||||
:name="t('share')"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/share.png"
|
||||
:onClick="onClickShare"
|
||||
/>
|
||||
<IconButton
|
||||
v-if="isMember"
|
||||
name="教练点评"
|
||||
src="../static/review.png"
|
||||
:name="t('coach_feedback')"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/review.png"
|
||||
:onClick="openCoachComment"
|
||||
/>
|
||||
</block>
|
||||
@@ -130,7 +132,7 @@ const openCoachComment = () => {
|
||||
:width="validArrows === total ? '70vw' : 'calc(100vw - 20px)'"
|
||||
:rounded="30"
|
||||
:onClick="closePanel"
|
||||
>{{ validArrows === total ? "完成" : "返回" }}</SButton
|
||||
>{{ validArrows === total ? t('completed') : t('back') }}</SButton
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
@@ -140,17 +142,16 @@ const openCoachComment = () => {
|
||||
mode="tall"
|
||||
>
|
||||
<view class="coach-comment">
|
||||
<text>
|
||||
您本次练习取得了<text :style="{ color: '#fed847' }">{{
|
||||
<text>{{ t('your_score_for_this_session_is') }}<text :style="{ color: '#fed847' }">{{
|
||||
totalRing
|
||||
}}</text
|
||||
>环的成绩,所有箭支上靶后的平均点间距为<text
|
||||
>{{ t('pts_the_average_distance_between_your_arrow_impacts_is') }}<text
|
||||
:style="{ color: '#fed847' }"
|
||||
>{{ Number((result?.interpretation?.spreadStability || 0).toFixed(2)) }}</text
|
||||
>,{{
|
||||
result.spreadEvaluation === "Dispersed"
|
||||
? "还需要持续改进哦~"
|
||||
: "成绩优秀。"
|
||||
? t('keep_practicing')
|
||||
: t('excellent_performance')
|
||||
}}
|
||||
</text>
|
||||
<view>
|
||||
@@ -159,14 +160,12 @@ const openCoachComment = () => {
|
||||
mode="widthFix"
|
||||
/>
|
||||
<text :style="{ marginTop: '12px' }"
|
||||
>针对您本次的练习,{{
|
||||
>{{ t('for_this_session') }}{{
|
||||
result.spreadEvaluation === "Dispersed"
|
||||
? "我们建议您充分练习推弓、靠位以及撒放动作一致性。"
|
||||
? t('we_recommend_practicing_consistency_in_your_bow_arm_anchor_point')
|
||||
: totalRing >= 100
|
||||
? "我们建议您继续保持即可。"
|
||||
: `我们建议您将设备的瞄准器${
|
||||
directionAdjusts[result.adjustmentHint]
|
||||
}调整。`
|
||||
? t('we_recommend_keeping_up_the_good_work')
|
||||
: t('we_recommend_adjusting_your_sight_value', { p0: directionAdjusts[result.adjustmentHint] })
|
||||
}}</text
|
||||
>
|
||||
</view>
|
||||
|
||||
@@ -14,8 +14,13 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: "normal",
|
||||
},
|
||||
contentHeight: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
const getContentHeight = () => {
|
||||
if (props.contentHeight) return props.contentHeight;
|
||||
if (props.mode === "tall") return "50vw";
|
||||
if (props.mode === "square") return "74vw";
|
||||
return "36vw";
|
||||
@@ -49,7 +54,7 @@ const getContentHeight = () => {
|
||||
</view>
|
||||
<IconButton
|
||||
v-if="!!onClose"
|
||||
src="../static/close-gold-outline.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/close-gold-outline.png"
|
||||
:width="30"
|
||||
:onClick="onClose"
|
||||
/>
|
||||
@@ -82,8 +87,14 @@ const getContentHeight = () => {
|
||||
position: absolute;
|
||||
width: 80vw;
|
||||
left: -7%;
|
||||
bottom: -18vw;
|
||||
bottom: -8rpx;
|
||||
z-index: -1;
|
||||
transform: translateY(-75px);
|
||||
}
|
||||
|
||||
/* 英文教练点评较长,保持弹窗按钮可见。 */
|
||||
.container :deep(.coach-comment) {
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -25,7 +25,7 @@ const props = defineProps({
|
||||
</view>
|
||||
<IconButton
|
||||
v-if="!!onClose"
|
||||
src="../static/close-white-outline.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/close-white-outline.png"
|
||||
:width="30"
|
||||
:onClick="onClose"
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref } from "vue";
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
const props = defineProps({
|
||||
@@ -73,8 +75,8 @@ onShow(async () => {
|
||||
>
|
||||
<slot></slot>
|
||||
<view class="tips">
|
||||
<text v-if="loading">加载中...</text>
|
||||
<text v-if="noMore">{{ count === 0 ? "暂无数据" : "没有更多了" }}</text>
|
||||
<text v-if="loading">{{ t('loading') }}</text>
|
||||
<text v-if="noMore">{{ count === 0 ? t('no_data_yet') : t('no_more_results') }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t, formatBattleTip } from "@/locales";
|
||||
|
||||
import { ref, watch, onMounted, onBeforeUnmount, computed } from "vue";
|
||||
import audioManager from "@/audioManager";
|
||||
import { MESSAGETYPESV2 } from "@/constants";
|
||||
@@ -135,9 +137,9 @@ watch(
|
||||
|
||||
const tipContent = computed(() => {
|
||||
if (halfTime.value) {
|
||||
return props.battleId ? "中场休息" : `中场休息(${wait.value}秒)`;
|
||||
return props.battleId ? t('halftime') : t('halftime_values', { p0: wait.value });
|
||||
}
|
||||
return props.start && remain.value === 0 ? "时间到!" : props.tips;
|
||||
return props.start && remain.value === 0 ? t('time_s_up') : props.tips;
|
||||
});
|
||||
|
||||
const updateSound = () => {
|
||||
@@ -204,10 +206,10 @@ onBeforeUnmount(() => {
|
||||
<view class="container" :style="{ display: show ? 'block' : 'none' }">
|
||||
<view>
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/shooter.png" mode="widthFix" />
|
||||
<text>{{ tipContent }}</text>
|
||||
<text>{{ formatBattleTip(tipContent) }}</text>
|
||||
<button hover-class="none" @click="updateSound">
|
||||
<image
|
||||
:src="`../static/sound${sound ? '' : '-off'}-yellow.png`"
|
||||
:src="`https://static.shelingxingqiu.com/shootmini/static/sound${sound ? '' : '-off'}-yellow.png`"
|
||||
mode="widthFix"
|
||||
/>
|
||||
</button>
|
||||
@@ -221,7 +223,7 @@ onBeforeUnmount(() => {
|
||||
transition: transitionStyle,
|
||||
}"
|
||||
/>
|
||||
<text>剩余{{ remain }}秒</text>
|
||||
<text>{{ t('remaining') }}{{ remain }}{{ t('s') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import {ref, watch, onMounted, onBeforeUnmount} from "vue";
|
||||
import {RoundGoldImages} from "@/constants";
|
||||
|
||||
@@ -129,7 +131,7 @@ onBeforeUnmount(() => {
|
||||
transition: transitionStyle,
|
||||
}"
|
||||
/>
|
||||
<text v-if="!loading">剩余{{ remain }}秒</text>
|
||||
<text v-if="!loading">{{ t('remaining') }}{{ remain }}{{ t('s') }}</text>
|
||||
<text v-else>···</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
+22
-20
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref, watch } from "vue";
|
||||
import { onShow } from "@dcloudio/uni-app";
|
||||
import SModal from "@/components/SModal.vue";
|
||||
@@ -83,25 +85,25 @@ const handleLogin = async () => {
|
||||
if (loading.value) return;
|
||||
if (!phone.value) {
|
||||
return uni.showToast({
|
||||
title: "请获取手机号",
|
||||
title: t('please_authorize_your_phone_number'),
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
if (!avatarUrl.value) {
|
||||
return uni.showToast({
|
||||
title: "请选择头像",
|
||||
title: t('please_select_an_avatar'),
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
if (!nickName.value) {
|
||||
return uni.showToast({
|
||||
title: "请输入昵称",
|
||||
title: t('please_enter_a_nickname'),
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
if (!agree.value) {
|
||||
return uni.showToast({
|
||||
title: "请先同意协议",
|
||||
title: t('please_agree_to_the_agreements_first'),
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
@@ -165,7 +167,7 @@ onShow(() => {
|
||||
<SModal :show="show" :onClose="onClose" :noBg="noBg">
|
||||
<view class="container" :style="{ background: noBg ? '#fff' : 'none' }">
|
||||
<view class="avatar" :style="{ borderColor: noBg ? '#E3E3E3' : '#fff3' }">
|
||||
<text :style="{ color: noBg ? '#666' : '#fff' }">手机:</text>
|
||||
<text :style="{ color: noBg ? '#666' : '#fff' }">{{ t('phone') }}</text>
|
||||
<button
|
||||
:open-type="!phone ? 'getPhoneNumber' : ''"
|
||||
@getphonenumber="getphonenumber"
|
||||
@@ -176,13 +178,13 @@ onShow(() => {
|
||||
phone
|
||||
}}</text>
|
||||
<text v-else :style="{ color: noBg ? '#666' : '#fff9' }"
|
||||
>点击获取</text
|
||||
>{{ t('authorize') }}</text
|
||||
>
|
||||
<image src="../static/enter.png" mode="widthFix" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/enter.png" mode="widthFix" />
|
||||
</button>
|
||||
</view>
|
||||
<view class="avatar" :style="{ borderColor: noBg ? '#E3E3E3' : '#fff3' }">
|
||||
<text :style="{ color: noBg ? '#666' : '#fff' }">头像:</text>
|
||||
<text :style="{ color: noBg ? '#666' : '#fff' }">{{ t('avatar') }}</text>
|
||||
<button
|
||||
open-type="chooseAvatar"
|
||||
@chooseavatar="onChooseAvatar"
|
||||
@@ -191,20 +193,20 @@ onShow(() => {
|
||||
>
|
||||
<Avatar v-if="avatarUrl" :src="avatarUrl" :size="30" />
|
||||
<text v-else :style="{ color: noBg ? '#666' : '#fff9' }"
|
||||
>点击获取</text
|
||||
>{{ t('authorize') }}</text
|
||||
>
|
||||
<image src="../static/enter.png" mode="widthFix" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/enter.png" mode="widthFix" />
|
||||
</button>
|
||||
</view>
|
||||
<view
|
||||
class="nickname"
|
||||
:style="{ borderColor: noBg ? '#E3E3E3' : '#fff3' }"
|
||||
>
|
||||
<text :style="{ color: noBg ? '#666' : '#fff' }">昵称:</text>
|
||||
<text :style="{ color: noBg ? '#666' : '#fff' }">{{ t('nickname') }}</text>
|
||||
<input
|
||||
type="nickname"
|
||||
:value="nickName"
|
||||
placeholder="请输入昵称"
|
||||
:placeholder="t('please_enter_a_nickname')"
|
||||
:placeholder-style="`color: ${noBg ? '#666' : '#fff9'} `"
|
||||
@input="onNicknameChange"
|
||||
@change="onNicknameChange"
|
||||
@@ -213,11 +215,11 @@ onShow(() => {
|
||||
</view>
|
||||
<SButton :rounded="20" width="80vw" :onClick="handleLogin">
|
||||
<block v-if="!loading">
|
||||
<text :style="{ color: '#000' }">手机号快捷登录</text>
|
||||
<text :style="{ color: '#000' }">{{ t('sign_in_with_phone_number') }}</text>
|
||||
</block>
|
||||
<block v-else>
|
||||
<image
|
||||
src="../static/btn-loading.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/btn-loading.png"
|
||||
mode="widthFix"
|
||||
class="loading"
|
||||
/>
|
||||
@@ -228,21 +230,21 @@ onShow(() => {
|
||||
v-if="!agree"
|
||||
:style="{ borderColor: noBg ? '#E3E3E3' : '#fff' }"
|
||||
/>
|
||||
<image v-if="agree" src="../static/checked.png" mode="widthFix" />
|
||||
<image v-if="agree" src="https://static.shelingxingqiu.com/shootmini/static/checked.png" mode="widthFix" />
|
||||
<view>
|
||||
<text>已同意并阅读</text>
|
||||
<text>{{ t('i_have_read_and_agree_to_the') }}</text>
|
||||
<view
|
||||
@click.stop="openServiceLink"
|
||||
:style="{ color: noBg ? '#333' : '#ffffff99' }"
|
||||
>《用户协议》</view
|
||||
>{{ t('user_agreement') }}</view
|
||||
>
|
||||
<text>及</text>
|
||||
<text>{{ t('and') }}</text>
|
||||
<view
|
||||
@click.stop="openPrivacyLink"
|
||||
:style="{ color: noBg ? '#333' : '#ffffff99' }"
|
||||
>《隐私协议》</view
|
||||
>{{ t('privacy_policy') }}</view
|
||||
>
|
||||
<text>内容</text>
|
||||
<text>{{ t('terms') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -46,10 +46,10 @@ const props = defineProps({
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
// 指定环数,1 到 10;无效值表示高亮整个区域。
|
||||
activeRing: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
// 指定一个或多个环数,空数组表示高亮整个区域。
|
||||
activeRings: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
// 每次变化时以固定低帧数重新展开当前高亮扇区;默认关闭。
|
||||
highlightRefreshToken: {
|
||||
@@ -164,6 +164,8 @@ const defaultHighlightStyle = {
|
||||
color: "rgba(255, 228, 0, 0.6)",
|
||||
strokeColor: "rgba(254, 216, 71, 0.82)",
|
||||
lineWidthRatio: 0.003,
|
||||
// 在最小线宽计算后应用倍数,确保不同靶面尺寸下按比例加粗。
|
||||
lineWidthScale: 1,
|
||||
};
|
||||
|
||||
// 合并默认靶纸样式和外部传入样式,ringColors 单独深合并。
|
||||
@@ -192,6 +194,25 @@ const getPositiveInteger = (value) => {
|
||||
return Number.isInteger(numberValue) && numberValue > 0 ? numberValue : 0;
|
||||
};
|
||||
|
||||
// 过滤、去重并合并连续环,避免 3、4 环同时高亮时绘制中间描边。
|
||||
const getActiveRingRanges = (ringCount) => {
|
||||
const rings = [...new Set(
|
||||
(Array.isArray(props.activeRings) ? props.activeRings : [])
|
||||
.map(getPositiveInteger)
|
||||
.filter((ring) => ring >= 1 && ring <= ringCount)
|
||||
)].sort((first, second) => first - second);
|
||||
|
||||
return rings.reduce((ranges, ring) => {
|
||||
const lastRange = ranges[ranges.length - 1];
|
||||
if (lastRange && ring === lastRange.end + 1) {
|
||||
lastRange.end = ring;
|
||||
} else {
|
||||
ranges.push({ start: ring, end: ring });
|
||||
}
|
||||
return ranges;
|
||||
}, []);
|
||||
};
|
||||
|
||||
// 正上方作为第一区起始边界,Canvas 角度递增方向即为顺时针。
|
||||
const getSectorAngles = (sector, sectorCount) => {
|
||||
const count = getPositiveInteger(sectorCount);
|
||||
@@ -256,7 +277,7 @@ const drawTargetRings = (ctx, centerX, centerY, targetRadius, config) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 高亮后端指定区域;activeRing 有效时只高亮该区域内的单个环。
|
||||
// 高亮后端指定区域;activeRings 非空时高亮该区域内的一个或多个环。
|
||||
const drawSectorHighlight = (
|
||||
ctx,
|
||||
centerX,
|
||||
@@ -274,32 +295,35 @@ const drawSectorHighlight = (
|
||||
);
|
||||
if (safeRevealProgress <= 0) return;
|
||||
|
||||
const ring = getPositiveInteger(props.activeRing);
|
||||
const hasActiveRing = ring >= 1 && ring <= config.ringCount;
|
||||
const innerRadius = hasActiveRing
|
||||
? targetRadius * ((config.ringCount - ring) / config.ringCount)
|
||||
: 0;
|
||||
const outerRadius = hasActiveRing
|
||||
? targetRadius * ((config.ringCount + 1 - ring) / config.ringCount)
|
||||
: targetRadius;
|
||||
const ringRanges = getActiveRingRanges(config.ringCount);
|
||||
const highlightRanges = ringRanges.length > 0
|
||||
? ringRanges
|
||||
: [{ start: 1, end: config.ringCount }];
|
||||
const style = {
|
||||
...defaultHighlightStyle,
|
||||
...props.highlightStyle,
|
||||
};
|
||||
|
||||
drawAnnularSector(
|
||||
ctx,
|
||||
centerX,
|
||||
centerY,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
angles.startAngle,
|
||||
angles.startAngle +
|
||||
(angles.endAngle - angles.startAngle) * safeRevealProgress,
|
||||
style.color,
|
||||
style.strokeColor,
|
||||
Math.max(1, targetRadius * style.lineWidthRatio)
|
||||
);
|
||||
highlightRanges.forEach((range) => {
|
||||
const innerRadius =
|
||||
targetRadius * ((config.ringCount - range.end) / config.ringCount);
|
||||
const outerRadius =
|
||||
targetRadius * ((config.ringCount + 1 - range.start) / config.ringCount);
|
||||
|
||||
drawAnnularSector(
|
||||
ctx,
|
||||
centerX,
|
||||
centerY,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
angles.startAngle,
|
||||
angles.startAngle +
|
||||
(angles.endAngle - angles.startAngle) * safeRevealProgress,
|
||||
style.color,
|
||||
style.strokeColor,
|
||||
Math.max(1, targetRadius * style.lineWidthRatio) * style.lineWidthScale
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// 从正上方开始顺时针绘制所有区域边界。
|
||||
@@ -420,7 +444,7 @@ const getDrawKey = (width, height) => {
|
||||
showRingLabels: props.showRingLabels,
|
||||
sectorCount: props.sectorCount,
|
||||
activeSector: props.activeSector,
|
||||
activeRing: props.activeRing,
|
||||
activeRings: props.activeRings,
|
||||
showSectorLabels: props.showSectorLabels,
|
||||
targetStyleConfig: props.targetStyleConfig,
|
||||
crosshairStyle: props.crosshairStyle,
|
||||
@@ -591,7 +615,7 @@ watch(
|
||||
props.showRingLabels,
|
||||
props.sectorCount,
|
||||
props.activeSector,
|
||||
props.activeRing,
|
||||
props.activeRings,
|
||||
props.highlightRefreshToken,
|
||||
props.showSectorLabels,
|
||||
props.highlightOnly,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref, watch } from "vue";
|
||||
import SButton from "@/components/SButton.vue";
|
||||
import audioManager from "@/audioManager";
|
||||
@@ -77,11 +79,11 @@ const handleConfirm = () => {
|
||||
<view class="header">
|
||||
<view class="header-title">
|
||||
<view class="header-title-line-left"></view>
|
||||
<text>选择靶型</text>
|
||||
<text>{{ t('select_target_type') }}</text>
|
||||
<view class="header-title-line-right"></view>
|
||||
</view>
|
||||
<view class="close-btn" @click="onClose">
|
||||
<image src="../static/close-yellow.png" mode="widthFix" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/close-yellow.png" mode="widthFix" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="target-options">
|
||||
@@ -89,17 +91,17 @@ const handleConfirm = () => {
|
||||
:class="{ 'target-btn': true, 'target-choosen': selectedTarget === 1 }"
|
||||
@click="handleSelectTarget(1)"
|
||||
>
|
||||
<text>20厘米全环靶</text>
|
||||
<text>{{ t('20_cm_full_ring_target') }}</text>
|
||||
</view>
|
||||
<view style="width: 30rpx"></view>
|
||||
<view
|
||||
:class="{ 'target-btn': true, 'target-choosen': selectedTarget === 2 }"
|
||||
@click="handleSelectTarget(2)"
|
||||
>
|
||||
<text>40厘米全环靶</text>
|
||||
<text>{{ t('40_cm_full_ring_target') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<SButton width="694rpx" :onClick="handleConfirm">确定</SButton>
|
||||
<SButton width="694rpx" :onClick="handleConfirm">{{ t('confirm_134') }}</SButton>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref, watch } from "vue";
|
||||
const props = defineProps({
|
||||
isRed: {
|
||||
@@ -65,7 +67,7 @@ watch(
|
||||
<template>
|
||||
<view class="container">
|
||||
<image
|
||||
:src="isRed ? '../static/flag-red.png' : '../static/flag-blue.png'"
|
||||
:src="isRed ? 'https://static.shelingxingqiu.com/shootmini/static/flag-red.png' : 'https://static.shelingxingqiu.com/shootmini/static/flag-blue.png'"
|
||||
class="flag"
|
||||
:style="{
|
||||
[isRed ? 'left' : 'right']: '10rpx',
|
||||
@@ -86,11 +88,11 @@ watch(
|
||||
[isRed ? 'left' : 'right']: getPos(item.id) + 'rpx',
|
||||
}"
|
||||
>
|
||||
<image :src="item.avatar || '../static/user-icon.png'" mode="widthFix" />
|
||||
<image :src="item.avatar || 'https://static.shelingxingqiu.com/shootmini/static/user-icon.png'" mode="widthFix" />
|
||||
<text
|
||||
v-if="isFirst(item.id)"
|
||||
:style="{ backgroundColor: isRed ? '#ff6060' : '#5fadff' }"
|
||||
>{{ isRed ? "红队" : "蓝队" }}</text
|
||||
>{{ isRed ? t('red_team') : t('blue_team') }}</text
|
||||
>
|
||||
</view>
|
||||
<text
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||
import Guide from "@/components/Guide.vue";
|
||||
import BowPower from "@/components/BowPower.vue";
|
||||
@@ -135,8 +137,8 @@ onBeforeUnmount(() => {
|
||||
<view class="container">
|
||||
<Guide v-show="guide">
|
||||
<view class="guide-tips">
|
||||
<text>请确保站距达到5米</text>
|
||||
<text>低于5米的射箭无效</text>
|
||||
<text>{{ t('stand_at_least_5_m_from_the_target') }}</text>
|
||||
<text>{{ t('shots_from_under_5_m_do_not_count') }}</text>
|
||||
</view>
|
||||
</Guide>
|
||||
<view class="test-area">
|
||||
@@ -150,15 +152,13 @@ onBeforeUnmount(() => {
|
||||
@click="simulShoot"
|
||||
hover-class="none"
|
||||
v-if="showsimul"
|
||||
>
|
||||
模拟射箭
|
||||
</button>
|
||||
>{{ t('simulate_shot') }}</button>
|
||||
<view class="warnning-text">
|
||||
<block v-if="statusText">
|
||||
<text>{{ statusText }}</text>
|
||||
</block>
|
||||
<block v-else>
|
||||
<text>请射箭,测试站距与靶纸</text>
|
||||
<text>{{ t('shoot_to_check_distance_and_target_face') }}</text>
|
||||
</block>
|
||||
</view>
|
||||
<view class="user-row">
|
||||
@@ -169,11 +169,11 @@ onBeforeUnmount(() => {
|
||||
<view v-if="isBattle" class="ready-timer">
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/test-tip.png" mode="widthFix" />
|
||||
<view v-if="count >= 0">
|
||||
<text>距离正式比赛还有</text>
|
||||
<text>{{ t('match_starts_in') }}</text>
|
||||
<text>{{ count }}</text>
|
||||
<text>秒</text>
|
||||
<text>{{ t('s') }}</text>
|
||||
</view>
|
||||
<view v-else> 进入中... </view>
|
||||
<view v-else>{{ t('joining') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
|
||||
const props = defineProps({
|
||||
countdown: {
|
||||
@@ -42,11 +44,11 @@ onBeforeUnmount(() => {
|
||||
class="container"
|
||||
:style="{ transform: `translateX(${show ? '0' : '100'}%)` }"
|
||||
>
|
||||
<view>距离正式比赛</view>
|
||||
<view>{{ t('match') }}</view>
|
||||
<view>
|
||||
<text>开始还有</text>
|
||||
<text>{{ t('starts_in') }}</text>
|
||||
<text>{{ count }}</text>
|
||||
<text>秒</text>
|
||||
<text>{{ t('s') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { computed } from "vue";
|
||||
|
||||
const MAX_VISIBLE_SCORE_CARDS = 60;
|
||||
@@ -59,10 +61,7 @@ const displayArrows = computed(() => {
|
||||
|
||||
<template>
|
||||
<view v-if="displayArrows.length" class="score-panel">
|
||||
<text v-if="hiddenScoreCount" class="score-window-tip">
|
||||
已省略前 {{ hiddenScoreCount }} 支,仅显示最近
|
||||
{{ MAX_VISIBLE_SCORE_CARDS }} 支
|
||||
</text>
|
||||
<text v-if="hiddenScoreCount" class="score-window-tip">{{ t('earlier') }}{{ hiddenScoreCount }}{{ t('arrows_hidden_showing_the_latest') }}{{ MAX_VISIBLE_SCORE_CARDS }}{{ t('arrows_241') }}</text>
|
||||
<view class="score-grid">
|
||||
<view
|
||||
v-for="(arrow, index) in displayArrows"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref, computed, watch } from "vue";
|
||||
import Avatar from "@/components/Avatar.vue";
|
||||
import useStore from "@/store";
|
||||
@@ -95,12 +97,12 @@ watch(
|
||||
</view>
|
||||
<!-- <image
|
||||
class="user-name-image"
|
||||
src="../static/vip1.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/vip1.png"
|
||||
mode="widthFix"
|
||||
/> -->
|
||||
</view>
|
||||
<view class="user-stats">
|
||||
<text class="level-tag level-tag-first">段位积分</text>
|
||||
<text class="level-tag level-tag-first">{{ t('rank_points') }}</text>
|
||||
<view class="rank-tag">
|
||||
<view
|
||||
class="rank-tag-progress"
|
||||
@@ -117,31 +119,31 @@ watch(
|
||||
<view v-if="showRank === true" class="rank-info" @click="toRankListPage">
|
||||
<image
|
||||
class="rank-info-image"
|
||||
src="../static/global-rank.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/global-rank.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<block v-if="user.ranking > 0 && rankData.rank.length">
|
||||
<text>本赛季全国</text>
|
||||
<text>{{ t('national_rank_this_season') }}</text>
|
||||
<text class="rank-number"
|
||||
>第<text :style="{ color: '#ffd700' }"
|
||||
>{{ t('round') }}<text :style="{ color: '#ffd700' }"
|
||||
>{{ user.ranking }}/{{ rankData.rank.length }}</text
|
||||
>名</text
|
||||
>{{ t('empty') }}</text
|
||||
>
|
||||
</block>
|
||||
<block v-else>
|
||||
<text>本赛季</text>
|
||||
<text>暂未上榜</text>
|
||||
<text>{{ t('this_season') }}</text>
|
||||
<text>{{ t('unranked') }}</text>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
<block v-else>
|
||||
<view class="signin" @click="onSignin">
|
||||
<image src="../static/user-icon.png" mode="widthFix" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/user-icon.png" mode="widthFix" />
|
||||
<view>
|
||||
<text>新来的弓箭手你好呀~</text>
|
||||
<text>{{ t('welcome_new_archer') }}</text>
|
||||
<view>
|
||||
<text>登录</text>
|
||||
<image src="../static/enter-arrow-blue.png" mode="widthFix" />
|
||||
<text>{{ t('sign_in') }}</text>
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/enter-arrow-blue.png" mode="widthFix" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -25,7 +25,7 @@ defineProps({
|
||||
<view>
|
||||
<slot></slot>
|
||||
<view v-if="onClick !== null">
|
||||
<image src="../static/enter.png" mode="widthFix" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/enter.png" mode="widthFix" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||
import useStore from "@/store";
|
||||
import { storeToRefs } from "pinia";
|
||||
@@ -52,7 +54,7 @@ onBeforeUnmount(() => {
|
||||
<template>
|
||||
<view class="content" :style="{ display: show ? 'flex' : 'none' }">
|
||||
<view v-if="showRank" class="up-rank">
|
||||
<image :src="user.avatar || '../static/user-icon.png'" mode="widthFix" />
|
||||
<image :src="user.avatar || 'https://static.shelingxingqiu.com/shootmini/static/user-icon.png'" mode="widthFix" />
|
||||
<image :src="nextRankImage" mode="widthFix" />
|
||||
<image class="bg-effect" src="https://static.shelingxingqiu.com/shootmini/static/shining-bg.png" mode="widthFix" />
|
||||
<image
|
||||
@@ -75,9 +77,9 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
</view>
|
||||
<view class="text-content">
|
||||
<image src="../static/update-text-bg.png" />
|
||||
<text>恭喜你升级到</text>
|
||||
<text>{{ showRank ? nextRankTitle : `射灵${lvl}级` }}</text>
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/update-text-bg.png" />
|
||||
<text>{{ t('congratulations_you_reached') }}</text>
|
||||
<text>{{ showRank ? nextRankTitle : t('sheling_level_value', { p0: lvl }) }}</text>
|
||||
<text>!</text>
|
||||
</view>
|
||||
<!-- <button @click="onClose" hover-class="none">关闭</button> -->
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
import { t } from "@/locales";
|
||||
import { computed, ref } from "vue";
|
||||
import { sendHardwareBoxUpdateAPI } from "@/apis";
|
||||
import useStore from "@/store";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
const OTA_PROGRESS_EVENT = "/addons/shoot/otaProgress";
|
||||
const OTA_RESULT_EVENT = "/addons/shoot/otaResult";
|
||||
const UPDATE_TIMEOUT = 10 * 60 * 1000;
|
||||
|
||||
const phaseTextMap = {
|
||||
started: t('preparing_firmware_update'),
|
||||
downloading: t('downloading_firmware'),
|
||||
installing: t('installing_firmware'),
|
||||
};
|
||||
|
||||
// 设备详情中的 latestVersion 是升级目标,version 是当前固件版本。
|
||||
export const getOtaInfoFromDetail = (detail) => ({
|
||||
versionNumber: String(detail?.latestVersion ?? "").trim(),
|
||||
currentVersion: String(detail?.version ?? "").trim(),
|
||||
versionInfo: String(detail?.versionInfo ?? "").trim(),
|
||||
needUpdate: detail?.needUpdate === true || Number(detail?.needUpdate) === 1,
|
||||
forceUpdate: detail?.forceUpdate === true || Number(detail?.forceUpdate) === 1,
|
||||
});
|
||||
|
||||
// OTA 状态由首页、WiFi 设置页和我的设备页共享,页面切换后仍可继续接收进度。
|
||||
const updating = ref(false);
|
||||
const progress = ref(0);
|
||||
const phase = ref("started");
|
||||
const resultVisible = ref(false);
|
||||
const resultStatus = ref("");
|
||||
const resultReason = ref("");
|
||||
|
||||
let deviceRef = null;
|
||||
let updateRunId = 0;
|
||||
let targetVersion = "";
|
||||
let lastFinishedVersion = "";
|
||||
let timeoutTimer = null;
|
||||
let successCallback = null;
|
||||
let socketListening = false;
|
||||
|
||||
const phaseText = computed(
|
||||
() => phaseTextMap[phase.value] || t('updating_firmware')
|
||||
);
|
||||
const resultTitle = computed(() =>
|
||||
resultStatus.value === "success" ? t('firmware_update_complete_250') : t('firmware_update_failed')
|
||||
);
|
||||
const resultContent = computed(() => {
|
||||
if (resultStatus.value === "success") return t('firmware_update_complete');
|
||||
return resultReason.value || t('update_failed_check_your_device_and_network_then_retry');
|
||||
});
|
||||
|
||||
const clearTimers = () => {
|
||||
clearTimeout(timeoutTimer);
|
||||
timeoutTimer = null;
|
||||
};
|
||||
|
||||
const resetActiveUpdate = () => {
|
||||
clearTimers();
|
||||
successCallback = null;
|
||||
};
|
||||
|
||||
const finishUpdate = (status, reason = "", runId = updateRunId) => {
|
||||
if (runId !== updateRunId || !updating.value) return;
|
||||
const onSuccess = successCallback;
|
||||
resetActiveUpdate();
|
||||
updating.value = false;
|
||||
resultStatus.value = status;
|
||||
resultReason.value = reason;
|
||||
lastFinishedVersion = targetVersion;
|
||||
if (status === "success") {
|
||||
progress.value = 100;
|
||||
phase.value = "installing";
|
||||
onSuccess?.();
|
||||
}
|
||||
resultVisible.value = true;
|
||||
};
|
||||
|
||||
const isCurrentDeviceMessage = (data) => {
|
||||
const messageDeviceId = String(data?.deviceId || "");
|
||||
const currentDeviceId = String(deviceRef?.value?.deviceId || "");
|
||||
return !messageDeviceId || !currentDeviceId || messageDeviceId === currentDeviceId;
|
||||
};
|
||||
|
||||
const isCurrentVersionMessage = (data) => {
|
||||
const messageVersion = String(data?.versionNumber || "");
|
||||
return !messageVersion || !targetVersion || messageVersion === targetVersion;
|
||||
};
|
||||
|
||||
// 页面切换导致弹窗关闭后,使用下一条进度消息恢复当前 OTA 会话。
|
||||
const recoverUpdateFromMessage = (data) => {
|
||||
updateRunId += 1;
|
||||
const runId = updateRunId;
|
||||
targetVersion = String(data?.versionNumber || "");
|
||||
lastFinishedVersion = "";
|
||||
successCallback = null;
|
||||
resultVisible.value = false;
|
||||
resultStatus.value = "";
|
||||
resultReason.value = "";
|
||||
progress.value = 0;
|
||||
phase.value = "started";
|
||||
updating.value = true;
|
||||
clearTimers();
|
||||
timeoutTimer = setTimeout(() => {
|
||||
finishUpdate("failed", t('firmware_update_timed_out_please_try_again'), runId);
|
||||
}, UPDATE_TIMEOUT);
|
||||
};
|
||||
|
||||
function handleSocketMessage(message) {
|
||||
if (Number(message?.code ?? 0) !== 0) return;
|
||||
if (![OTA_PROGRESS_EVENT, OTA_RESULT_EVENT].includes(message?.event)) return;
|
||||
if (!isCurrentDeviceMessage(message.data)) return;
|
||||
|
||||
const messageVersion = String(message.data?.versionNumber || "");
|
||||
|
||||
if (message.event === OTA_PROGRESS_EVENT) {
|
||||
if (!updating.value) {
|
||||
if (
|
||||
lastFinishedVersion &&
|
||||
(!messageVersion || messageVersion === lastFinishedVersion)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
recoverUpdateFromMessage(message.data);
|
||||
} else if (!isCurrentVersionMessage(message.data)) {
|
||||
return;
|
||||
}
|
||||
if (!targetVersion && messageVersion) targetVersion = messageVersion;
|
||||
|
||||
const nextProgress = Math.min(
|
||||
100,
|
||||
Math.max(0, Number(message.data?.progress) || 0)
|
||||
);
|
||||
progress.value = Math.max(progress.value, nextProgress);
|
||||
if (phaseTextMap[message.data?.phase]) {
|
||||
phase.value = message.data.phase;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!updating.value) {
|
||||
if (
|
||||
lastFinishedVersion &&
|
||||
(!messageVersion || messageVersion === lastFinishedVersion)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
recoverUpdateFromMessage(message.data);
|
||||
} else if (!isCurrentVersionMessage(message.data)) {
|
||||
return;
|
||||
}
|
||||
if (!targetVersion && messageVersion) targetVersion = messageVersion;
|
||||
|
||||
if (message.data?.status === "success") {
|
||||
finishUpdate("success");
|
||||
} else if (message.data?.status === "failed") {
|
||||
finishUpdate("failed", message.data?.reason || "");
|
||||
}
|
||||
}
|
||||
|
||||
const startSocketListening = () => {
|
||||
if (socketListening) return;
|
||||
uni.$on("socket-inbox", handleSocketMessage);
|
||||
socketListening = true;
|
||||
};
|
||||
|
||||
export const useOtaUpdate = () => {
|
||||
const store = useStore();
|
||||
const { device } = storeToRefs(store);
|
||||
deviceRef = device;
|
||||
startSocketListening();
|
||||
|
||||
const startUpdate = async ({
|
||||
versionNumber,
|
||||
wifiSsid = "",
|
||||
wifiPassword = "",
|
||||
onSuccess,
|
||||
}) => {
|
||||
if (updating.value) return false;
|
||||
const nextVersion = String(versionNumber || "").trim();
|
||||
if (!nextVersion) {
|
||||
uni.showToast({ title: t('target_firmware_version_is_missing'), icon: "none" });
|
||||
return false;
|
||||
}
|
||||
|
||||
updateRunId += 1;
|
||||
const runId = updateRunId;
|
||||
targetVersion = nextVersion;
|
||||
lastFinishedVersion = "";
|
||||
successCallback = onSuccess || null;
|
||||
resultVisible.value = false;
|
||||
resultStatus.value = "";
|
||||
resultReason.value = "";
|
||||
progress.value = 0;
|
||||
phase.value = "started";
|
||||
updating.value = true;
|
||||
clearTimers();
|
||||
timeoutTimer = setTimeout(() => {
|
||||
finishUpdate("failed", t('firmware_update_timed_out_please_try_again'), runId);
|
||||
}, UPDATE_TIMEOUT);
|
||||
|
||||
try {
|
||||
const updateResult = await sendHardwareBoxUpdateAPI({
|
||||
versionNumber: targetVersion,
|
||||
wifiSsid,
|
||||
wifiPassword,
|
||||
});
|
||||
if (runId !== updateRunId || !updating.value) return false;
|
||||
if (!updateResult?.taskId) {
|
||||
finishUpdate("failed", t('could_not_create_the_firmware_update_task_please_retry'), runId);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (runId === updateRunId) {
|
||||
finishUpdate(
|
||||
"failed",
|
||||
error?.message || t('firmware_update_request_failed_please_retry'),
|
||||
runId
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const closeResult = () => {
|
||||
resultVisible.value = false;
|
||||
};
|
||||
|
||||
return {
|
||||
updating,
|
||||
progress,
|
||||
phase,
|
||||
phaseText,
|
||||
resultVisible,
|
||||
resultStatus,
|
||||
resultTitle,
|
||||
resultContent,
|
||||
startUpdate,
|
||||
closeResult,
|
||||
};
|
||||
};
|
||||
+27
-26
@@ -1,3 +1,4 @@
|
||||
import { t } from "@/locales";
|
||||
export const MESSAGETYPES = {
|
||||
ShootSyncMeArrowID: parseInt("0x789b6b0d"), // 2023451405
|
||||
ShootSyncMePracticeID: parseInt("0xD88AE05E"), // 3632980062
|
||||
@@ -80,15 +81,15 @@ export const meleeAvatarColors = [
|
||||
];
|
||||
|
||||
export const orderStatusNames = {
|
||||
1: "待付款",
|
||||
2: "已付款",
|
||||
3: "已发货",
|
||||
4: "已完成",
|
||||
5: "已关闭",
|
||||
6: "申请退款",
|
||||
7: "退款中",
|
||||
8: "已退款",
|
||||
9: "拒绝退款",
|
||||
1: t('awaiting_payment'),
|
||||
2: t('paid'),
|
||||
3: t('shipped'),
|
||||
4: t('completed_260'),
|
||||
5: t('closed'),
|
||||
6: t('refund_requested'),
|
||||
7: t('refund_in_progress'),
|
||||
8: t('refunded'),
|
||||
9: t('refund_declined'),
|
||||
};
|
||||
|
||||
export const getStatusColor = (status) => {
|
||||
@@ -103,14 +104,14 @@ export const getStatusColor = (status) => {
|
||||
};
|
||||
|
||||
export const directionAdjusts = {
|
||||
AdjustLowerRight: "向右下方调瞄",
|
||||
AdjustDown: "向下方调瞄",
|
||||
AdjustLowerLeft: "向左下方调瞄",
|
||||
AdjustLeft: "向左侧调瞄",
|
||||
AdjustUpperLeft: "向左上侧调瞄",
|
||||
AdjustUp: "向上方调瞄",
|
||||
AdjustUpperRight: "向右上方调瞄",
|
||||
AdjustRight: "向右侧调瞄",
|
||||
AdjustLowerRight: t('down_and_right'),
|
||||
AdjustDown: t('down'),
|
||||
AdjustLowerLeft: t('down_and_left'),
|
||||
AdjustLeft: t('left'),
|
||||
AdjustUpperLeft: t('up_and_left'),
|
||||
AdjustUp: t('up'),
|
||||
AdjustUpperRight: t('up_and_right'),
|
||||
AdjustRight: t('right'),
|
||||
};
|
||||
|
||||
export const getBattleResultTips = (
|
||||
@@ -145,12 +146,12 @@ export const getBattleResultTips = (
|
||||
} else {
|
||||
if (rank <= 3) {
|
||||
const tests = [
|
||||
"好成绩!全国排位赛等着你!",
|
||||
"持续练习,就会迎来更多高光时刻!",
|
||||
t('great_score_the_national_ranked_matches_await'),
|
||||
t('keep_practicing_for_more_moments_to_shine'),
|
||||
];
|
||||
return tests[getRandomIndex(2)];
|
||||
} else {
|
||||
return "每日练习打卡,争取下次脱颖而出!";
|
||||
return t('practice_daily_and_stand_out_next_time');
|
||||
}
|
||||
}
|
||||
} else if (gameMode === 2) {
|
||||
@@ -179,16 +180,16 @@ export const getBattleResultTips = (
|
||||
} else {
|
||||
if (score > 0) {
|
||||
const tests = [
|
||||
"王者一定属于你!",
|
||||
"高光时刻!继续保持!",
|
||||
"射灵世界的佼佼者!",
|
||||
t('the_top_rank_is_within_your_reach'),
|
||||
t('a_moment_to_shine_keep_it_up'),
|
||||
t('one_of_the_best_in_the_sheling_world'),
|
||||
];
|
||||
return tests[getRandomIndex(3)];
|
||||
} else {
|
||||
const tests = [
|
||||
"再来一次,定能脱颖而出!",
|
||||
"加强练习,争取越战越勇",
|
||||
"人生走过的每一步都算数",
|
||||
t('try_again_and_make_your_mark'),
|
||||
t('practice_more_and_come_back_stronger'),
|
||||
t('every_step_you_take_counts'),
|
||||
];
|
||||
return tests[getRandomIndex(3)];
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createSSRApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
import App from './App.vue'
|
||||
import { i18n } from './locales'
|
||||
import audioManager from './audioManager'
|
||||
|
||||
export function createApp() {
|
||||
@@ -9,6 +10,7 @@ export function createApp() {
|
||||
const pinia = createPinia()
|
||||
pinia.use(piniaPluginPersistedstate)
|
||||
app.use(pinia)
|
||||
app.use(i18n)
|
||||
|
||||
/**
|
||||
* 全局点击音效工具函数,用于在任意按钮/元素点击时自动播放音效。
|
||||
|
||||
+9
-1
@@ -294,6 +294,14 @@
|
||||
{
|
||||
"type" : "folder",
|
||||
"value" : "static/training-difficulty-design"
|
||||
},
|
||||
{
|
||||
"type" : "folder",
|
||||
"value" : "static/coin"
|
||||
},
|
||||
{
|
||||
"type" : "folder",
|
||||
"value" : "static/home-device"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -313,7 +321,7 @@
|
||||
"themeLocation" : "theme.json",
|
||||
"permission" : {
|
||||
"scope.userLocation": {
|
||||
"desc": "用于扫描附近 WiFi,完成设备 OTA 升级网络连接"
|
||||
"desc": "Find Wi-Fi for device updates"
|
||||
}
|
||||
},
|
||||
"requiredPrivateInfos" : [ "getLocation", "chooseLocation" ]
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
getPrecisionShotAudioKeys,
|
||||
getRhythmShotAudioKeys,
|
||||
getStabilityShotAudioKeys,
|
||||
getTrainingEndAudioKey,
|
||||
getTrainingStartAudioKey,
|
||||
STABILITY_ENERGY_50_AUDIO_KEY,
|
||||
STABILITY_ENERGY_70_AUDIO_KEY,
|
||||
@@ -603,6 +604,10 @@ function getAckAudioKeys(message, businessMessage) {
|
||||
if (businessMessage?.type === MESSAGETYPESV2.HalfRest) return ["中场休息"];
|
||||
return ["比赛结束"];
|
||||
case ServerMessageType.SERVER_MSG_PRACTICE_END:
|
||||
// 个人训练的 ACK 等待实际播放的通关语音,其他练习保留原有配置。
|
||||
if (currentContext?.practiceEndAudioKey === "练习结束") {
|
||||
return [getTrainingEndAudioKey(businessMessage?.completed)];
|
||||
}
|
||||
return [currentContext?.practiceEndAudioKey || "比赛结束"];
|
||||
case ServerMessageType.SERVER_MSG_CHECK:
|
||||
return getTestDistanceAudioKeys(businessMessage?.shootData);
|
||||
|
||||
+157
-151
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/index"
|
||||
},
|
||||
@@ -9,118 +9,121 @@
|
||||
{
|
||||
"path": "pages/friend-battle"
|
||||
},
|
||||
{
|
||||
"path": "pages/point-book"
|
||||
},
|
||||
{
|
||||
"path": "pages/point-book-rank"
|
||||
},
|
||||
{
|
||||
"path": "pages/my-like-list"
|
||||
},
|
||||
{
|
||||
"path": "pages/audio-test"
|
||||
},
|
||||
{
|
||||
"path": "pages/about-us"
|
||||
},
|
||||
{
|
||||
"path": "pages/webview",
|
||||
"style": {
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/melee-battle"
|
||||
},
|
||||
{
|
||||
"path": "pages/battle-result"
|
||||
},
|
||||
{
|
||||
"path": "pages/friend-battle-result"
|
||||
},
|
||||
{
|
||||
"path": "pages/point-book-edit"
|
||||
},
|
||||
{
|
||||
"path": "pages/point-book-create"
|
||||
},
|
||||
{
|
||||
"path": "pages/point-book-list"
|
||||
},
|
||||
{
|
||||
"path": "pages/point-book-detail"
|
||||
},
|
||||
{
|
||||
"path": "pages/point-book-detail-share"
|
||||
},
|
||||
{
|
||||
"path": "pages/match-page"
|
||||
},
|
||||
{
|
||||
"path": "pages/user"
|
||||
},
|
||||
{
|
||||
"path": "pages/grade-intro"
|
||||
},
|
||||
{
|
||||
"path": "pages/rank-intro"
|
||||
},
|
||||
{
|
||||
"path": "pages/my-growth"
|
||||
},
|
||||
{
|
||||
"path": "pages/first-try"
|
||||
},
|
||||
{
|
||||
"path": "pages/practise"
|
||||
},
|
||||
{
|
||||
"path": "pages/practise-one"
|
||||
},
|
||||
{
|
||||
"path": "pages/practise-two"
|
||||
},
|
||||
{
|
||||
"path": "pages/battle-room"
|
||||
},
|
||||
{
|
||||
"path": "pages/ranking"
|
||||
},
|
||||
{
|
||||
"path": "pages/rank-list"
|
||||
},
|
||||
{
|
||||
"path": "pages/match-detail"
|
||||
},
|
||||
{
|
||||
"path": "pages/melee-bow-data"
|
||||
},
|
||||
{
|
||||
"path": "pages/point-book"
|
||||
},
|
||||
{
|
||||
"path": "pages/point-book-rank"
|
||||
},
|
||||
{
|
||||
"path": "pages/my-like-list"
|
||||
},
|
||||
{
|
||||
"path": "pages/audio-test"
|
||||
},
|
||||
{
|
||||
"path": "pages/about-us"
|
||||
},
|
||||
{
|
||||
"path": "pages/webview",
|
||||
"style": {
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/melee-battle"
|
||||
},
|
||||
{
|
||||
"path": "pages/battle-result"
|
||||
},
|
||||
{
|
||||
"path": "pages/friend-battle-result"
|
||||
},
|
||||
{
|
||||
"path": "pages/point-book-edit"
|
||||
},
|
||||
{
|
||||
"path": "pages/point-book-create"
|
||||
},
|
||||
{
|
||||
"path": "pages/point-book-list"
|
||||
},
|
||||
{
|
||||
"path": "pages/point-book-detail"
|
||||
},
|
||||
{
|
||||
"path": "pages/point-book-detail-share"
|
||||
},
|
||||
{
|
||||
"path": "pages/match-page"
|
||||
},
|
||||
{
|
||||
"path": "pages/user"
|
||||
},
|
||||
{
|
||||
"path": "pages/grade-intro"
|
||||
},
|
||||
{
|
||||
"path": "pages/rank-intro"
|
||||
},
|
||||
{
|
||||
"path": "pages/my-growth"
|
||||
},
|
||||
{
|
||||
"path": "pages/first-try"
|
||||
},
|
||||
{
|
||||
"path": "pages/practise"
|
||||
},
|
||||
{
|
||||
"path": "pages/practise-one"
|
||||
},
|
||||
{
|
||||
"path": "pages/practise-two"
|
||||
},
|
||||
{
|
||||
"path": "pages/battle-room"
|
||||
},
|
||||
{
|
||||
"path": "pages/ranking"
|
||||
},
|
||||
{
|
||||
"path": "pages/rank-list"
|
||||
},
|
||||
{
|
||||
"path": "pages/match-detail"
|
||||
},
|
||||
{
|
||||
"path": "pages/melee-bow-data"
|
||||
},
|
||||
{
|
||||
"path": "pages/mine-bow-data"
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"backgroundColor": "@bgColor",
|
||||
"backgroundColorBottom": "@bgColorBottom",
|
||||
"backgroundColorTop": "@bgColorTop",
|
||||
"backgroundTextStyle": "@bgTxtStyle",
|
||||
"navigationBarBackgroundColor": "#000000",
|
||||
"navigationBarTextStyle": "@navTxtStyle",
|
||||
"navigationBarTitleText": "Uni Creator",
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": false
|
||||
},
|
||||
"easycom": {
|
||||
"autoscan": true,
|
||||
"custom": {
|
||||
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
|
||||
}
|
||||
},
|
||||
"globalStyle": {
|
||||
"backgroundColor": "@bgColor",
|
||||
"backgroundColorBottom": "@bgColorBottom",
|
||||
"backgroundColorTop": "@bgColorTop",
|
||||
"backgroundTextStyle": "@bgTxtStyle",
|
||||
"navigationBarBackgroundColor": "#000000",
|
||||
"navigationBarTextStyle": "@navTxtStyle",
|
||||
"navigationBarTitleText": "Uni Creator",
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": false
|
||||
},
|
||||
"easycom": {
|
||||
"autoscan": true,
|
||||
"custom": {
|
||||
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
|
||||
}
|
||||
},
|
||||
"subPackages": [
|
||||
{
|
||||
"root": "pages/coin",
|
||||
"pages": [
|
||||
{
|
||||
"path": "vip-intro"
|
||||
},
|
||||
{
|
||||
"path": "index"
|
||||
},
|
||||
@@ -156,6 +159,9 @@
|
||||
{
|
||||
"path": "device-bind-failure"
|
||||
},
|
||||
{
|
||||
"path": "device-already-bound"
|
||||
},
|
||||
{
|
||||
"path": "device-intro"
|
||||
},
|
||||
@@ -175,48 +181,48 @@
|
||||
},
|
||||
{
|
||||
"root": "pages/member",
|
||||
"pages": [
|
||||
{
|
||||
"path": "orders"
|
||||
},
|
||||
{
|
||||
"path": "order-detail"
|
||||
},
|
||||
{
|
||||
"path": "be-vip"
|
||||
},
|
||||
{
|
||||
"path": "vip-intro"
|
||||
},
|
||||
{
|
||||
"path": "agreement"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "pages/team-battle",
|
||||
"pages": [
|
||||
{
|
||||
"path": "index"
|
||||
},
|
||||
{
|
||||
"path": "team-bow-data"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "pages/training",
|
||||
"pages": [
|
||||
{
|
||||
"path": "index"
|
||||
},
|
||||
{
|
||||
"path": "difficulty"
|
||||
},
|
||||
{
|
||||
"path": "practise-one"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
"pages": [
|
||||
{
|
||||
"path": "orders"
|
||||
},
|
||||
{
|
||||
"path": "order-detail"
|
||||
},
|
||||
{
|
||||
"path": "be-vip"
|
||||
},
|
||||
{
|
||||
"path": "vip-intro"
|
||||
},
|
||||
{
|
||||
"path": "agreement"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "pages/team-battle",
|
||||
"pages": [
|
||||
{
|
||||
"path": "index"
|
||||
},
|
||||
{
|
||||
"path": "team-bow-data"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "pages/training",
|
||||
"pages": [
|
||||
{
|
||||
"path": "index"
|
||||
},
|
||||
{
|
||||
"path": "difficulty"
|
||||
},
|
||||
{
|
||||
"path": "practise-one"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import Container from "@/components/Container.vue";
|
||||
|
||||
const isIOS = uni.getDeviceInfo().osName === "ios";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Container title="关于我们">
|
||||
<Container :title="t('about_us')">
|
||||
<view class="container">
|
||||
<view class="text">
|
||||
《射灵星球》是以智能和物联网技术驱动的全球射箭专业选手及射箭爱好者互动交流平台,由广州光点飞舞网络有限公司研发并提供线上服务。
|
||||
</view>
|
||||
<view class="text">{{ t('sheling_planet_is_a_platform_for_professional_archers_and_archer') }}</view>
|
||||
|
||||
<view class="text">
|
||||
我们专注于智能射箭技术的探索和应用,通过物联网技术、激光系统、人工智能、嵌入式AI及射箭在线互娱模式的创新与研发,提供专业的智能体育设备和有趣的在线物联游戏,以此推动射箭运动及更多专业体育运动走入大众家庭。
|
||||
</view>
|
||||
<view class="text">{{ t('we_explore_and_apply_smart_archery_technology_through_innovation') }}</view>
|
||||
|
||||
<view
|
||||
class="copyright"
|
||||
:style="{ paddingBottom: isIOS ? '40rpx' : '20rpx' }"
|
||||
>
|
||||
<text>粤ICP备2025421150号-2X</text>
|
||||
<text>{{ t('guangdong_icp_no_2025421150_2x') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</Container>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||
import Container from "@/components/Container.vue";
|
||||
import audioManager, { audioFils } from "@/audioManager";
|
||||
@@ -30,25 +32,21 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Container title="音频测试">
|
||||
<Container :title="t('audio_test')">
|
||||
<view class="container">
|
||||
<view>
|
||||
<text>连续播放1</text>
|
||||
<button hover-class="none" @click="playAudio(['第一轮', '请蓝方射箭'])">
|
||||
播放
|
||||
</button>
|
||||
<text>{{ t('continuous_playback_1') }}</text>
|
||||
<button hover-class="none" @click="playAudio(['第一轮', '请蓝方射箭'])">{{ t('play') }}</button>
|
||||
</view>
|
||||
<view>
|
||||
<text>连续播放2</text>
|
||||
<button hover-class="none" @click="playAudio(['第二轮', '请红方射箭'])">
|
||||
播放
|
||||
</button>
|
||||
<text>{{ t('continuous_playback_2') }}</text>
|
||||
<button hover-class="none" @click="playAudio(['第二轮', '请红方射箭'])">{{ t('play') }}</button>
|
||||
</view>
|
||||
<view v-for="key in Object.keys(audioFils)" :key="key">
|
||||
<text>{{ key }}</text>
|
||||
<text>{{ loaded[key] ? "已加载" : "未加载" }}</text>
|
||||
<text>{{ loaded[key] ? t('loaded') : t('not_loaded') }}</text>
|
||||
<button hover-class="none" @click="playAudio(key)">
|
||||
{{ loaded[key] ? "播放" : "加载并播放" }}
|
||||
{{ loaded[key] ? t('play_301') : t('load_and_play') }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
+20
-21
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import Avatar from "@/components/Avatar.vue";
|
||||
@@ -92,7 +94,7 @@ const checkBowData = () => {
|
||||
? 'linear-gradient(270deg, #3597ff 0%, rgba(0,0,0,0) 100%);'
|
||||
: 'linear-gradient(270deg, #fd4444 0%, rgba(0, 0, 0, 0) 100%)',
|
||||
}"
|
||||
>{{ data.winTeam === 1 ? "蓝队" : "红队" }}获胜</text
|
||||
>{{ data.winTeam === 1 ? t('blue_team') : t('red_team') }}{{ t('win') }}</text
|
||||
>
|
||||
<Avatar
|
||||
:size="32"
|
||||
@@ -121,7 +123,7 @@ const checkBowData = () => {
|
||||
<view v-if="data.mvp && data.mvp.player_match_result.total_ring">
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/title-mvp.png" mode="widthFix" />
|
||||
<text
|
||||
>斩获<text
|
||||
>{{ t('scored') }}<text
|
||||
:style="{
|
||||
color: '#fed847',
|
||||
fontSize: '18px',
|
||||
@@ -129,7 +131,7 @@ const checkBowData = () => {
|
||||
fontWeight: '600',
|
||||
}"
|
||||
>{{ data.mvp.player_match_result.total_ring }}</text
|
||||
>环</text
|
||||
>{{ t('pts') }}</text
|
||||
>
|
||||
</view>
|
||||
<view v-if="data.mvp && data.mvp.length">
|
||||
@@ -145,7 +147,7 @@ const checkBowData = () => {
|
||||
:style="{
|
||||
backgroundColor: myTeam === 1 ? '#5fadff' : '#ff6060',
|
||||
}"
|
||||
>自己</text
|
||||
>{{ t('me') }}</text
|
||||
>
|
||||
</view>
|
||||
<text class="truncate">{{ player.name }}</text>
|
||||
@@ -156,7 +158,7 @@ const checkBowData = () => {
|
||||
<view class="battle-winner">
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/shining-bg.png" mode="widthFix" />
|
||||
<image
|
||||
:src="ifWin ? '../static/you-win.png' : '../static/you-lost.png'"
|
||||
:src="ifWin ? 'https://static.shelingxingqiu.com/shootmini/static/you-win.png' : 'https://static.shelingxingqiu.com/shootmini/static/you-lost.png'"
|
||||
mode="widthFix"
|
||||
class="scale-in"
|
||||
/>
|
||||
@@ -193,37 +195,37 @@ const checkBowData = () => {
|
||||
<image
|
||||
v-if="player.rank === 1"
|
||||
class="player-bg"
|
||||
src="../static/melee-player-bg1.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/melee-player-bg1.png"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<image
|
||||
v-if="player.rank === 2"
|
||||
class="player-bg"
|
||||
src="../static/melee-player-bg2.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/melee-player-bg2.png"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<image
|
||||
v-if="player.rank === 3"
|
||||
class="player-bg"
|
||||
src="../static/melee-player-bg3.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/melee-player-bg3.png"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<image
|
||||
v-if="player.rank === 1"
|
||||
class="player-crown"
|
||||
src="../static/champ1.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/champ1.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<image
|
||||
v-if="player.rank === 2"
|
||||
class="player-crown"
|
||||
src="../static/champ2.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/champ2.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<image
|
||||
v-if="player.rank === 3"
|
||||
class="player-crown"
|
||||
src="../static/champ3.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/champ3.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<view v-if="player.rank > 3" class="view-crown">{{
|
||||
@@ -239,8 +241,7 @@ const checkBowData = () => {
|
||||
<text>{{ getLvlName(player.rank_lvl) }}</text>
|
||||
</view>
|
||||
<text
|
||||
><text :style="{ color: '#fff' }">{{ player.totalRing }}</text>
|
||||
环</text
|
||||
><text :style="{ color: '#fff' }">{{ player.totalRing }}</text>{{ t('pts_308') }}</text
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
@@ -259,19 +260,17 @@ const checkBowData = () => {
|
||||
<text
|
||||
:style="{ backgroundColor: '#5fadff' }"
|
||||
v-if="data.mode <= 3 && myTeam === 1"
|
||||
>蓝队</text
|
||||
>{{ t('blue_team') }}</text
|
||||
>
|
||||
<text
|
||||
:style="{ backgroundColor: '#ff6060' }"
|
||||
v-if="data.mode <= 3 && myTeam === 2"
|
||||
>红队</text
|
||||
>{{ t('red_team') }}</text
|
||||
>
|
||||
</view>
|
||||
<text v-if="data.way === 1">
|
||||
你的经验 {{ totalPoints > 0 ? "+" + totalPoints : totalPoints }}
|
||||
<text v-if="data.way === 1">{{ t('your_xp') }}{{ totalPoints > 0 ? "+" + totalPoints : totalPoints }}
|
||||
</text>
|
||||
<text v-if="data.way === 2">
|
||||
你的积分 {{ totalPoints > 0 ? "+" + totalPoints : totalPoints }}
|
||||
<text v-if="data.way === 2">{{ t('your_points') }}{{ totalPoints > 0 ? "+" + totalPoints : totalPoints }}
|
||||
</text>
|
||||
</view>
|
||||
<text v-if="data.mode > 3" class="description">
|
||||
@@ -284,8 +283,8 @@ const checkBowData = () => {
|
||||
}}
|
||||
</text>
|
||||
<view class="op-btn">
|
||||
<view @click="checkBowData">查看成绩</view>
|
||||
<view @click="exit">返回</view>
|
||||
<view @click="checkBowData">{{ t('view_results') }}</view>
|
||||
<view @click="exit">{{ t('back') }}</view>
|
||||
</view>
|
||||
<UserUpgrade />
|
||||
<DeviceChargingDialog />
|
||||
|
||||
+43
-45
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref, onMounted, computed, onBeforeUnmount } from "vue";
|
||||
import { onShow, onHide, onLoad, onShareAppMessage } from "@dcloudio/uni-app";
|
||||
import Container from "@/components/Container.vue";
|
||||
@@ -53,10 +55,10 @@ const normalizeRoomPlayer = (member = {}) => ({
|
||||
* battleType=2: 多人乱斗
|
||||
*/
|
||||
const battleTitle = computed(() => {
|
||||
if (!room.value.battleType) return "好友约战";
|
||||
if (room.value.battleType === 2) return "多人乱斗";
|
||||
if (!room.value.battleType) return t('friend_challenge');
|
||||
if (room.value.battleType === 2) return t('free_for_all');
|
||||
const half = room.value.count / 2;
|
||||
return `${half}v${half}对抗赛`;
|
||||
return t('valuevvalue_versus', { p0: half, p1: half });
|
||||
});
|
||||
|
||||
/** 靶纸尺寸(cm),由 URL 参数或 API 返回的 targetType 字段填充 */
|
||||
@@ -146,7 +148,7 @@ async function reconcileBattleRoom(generation) {
|
||||
if (!isCurrentForeground(generation)) return;
|
||||
console.log("resume room match info error", error);
|
||||
uni.showToast({
|
||||
title: "比赛状态查询失败,请重试",
|
||||
title: t('could_not_check_match_status_please_retry'),
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
@@ -159,7 +161,7 @@ async function reconcileBattleRoom(generation) {
|
||||
} catch (error) {
|
||||
console.log("return to room battle error", error);
|
||||
uni.showToast({
|
||||
title: "进入比赛失败,请重试",
|
||||
title: t('could_not_join_the_match_please_retry'),
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
@@ -187,7 +189,7 @@ async function reconcileBattleRoom(generation) {
|
||||
if (!isCurrentForeground(generation)) return;
|
||||
console.log("retry room match info error", error);
|
||||
uni.showToast({
|
||||
title: "比赛状态查询失败,请重试",
|
||||
title: t('could_not_check_match_status_please_retry'),
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
@@ -200,7 +202,7 @@ async function reconcileBattleRoom(generation) {
|
||||
} catch (error) {
|
||||
console.log("return to room battle error", error);
|
||||
uni.showToast({
|
||||
title: "进入比赛失败,请重试",
|
||||
title: t('could_not_join_the_match_please_retry'),
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
@@ -208,7 +210,7 @@ async function reconcileBattleRoom(generation) {
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: "比赛正在创建,请稍候",
|
||||
title: t('creating_match_please_wait'),
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
@@ -352,7 +354,7 @@ async function onReceiveMessage(message) {
|
||||
} else if (msg.constructor === MESSAGETYPES.UserExitRoom) {
|
||||
if (msg.userId === user.value.id) {
|
||||
uni.showToast({
|
||||
title: "你被踢出房间",
|
||||
title: t('you_have_been_removed_from_the_room'),
|
||||
icon: "none",
|
||||
});
|
||||
setTimeout(() => {
|
||||
@@ -367,7 +369,7 @@ async function onReceiveMessage(message) {
|
||||
refreshRoomData();
|
||||
} else if (msg.constructor === MESSAGETYPES.RoomDestroy) {
|
||||
uni.showToast({
|
||||
title: "房间已解散",
|
||||
title: t('this_room_has_been_closed'),
|
||||
icon: "none",
|
||||
});
|
||||
setTimeout(() => {
|
||||
@@ -450,10 +452,10 @@ const canClick = computed(() => {
|
||||
*/
|
||||
const shareTitle = computed(() => {
|
||||
const { battleType, count } = room.value;
|
||||
if (battleType === 2) return '热血乱斗赛,敢来争锋?';
|
||||
if (battleType === 1 && count === 4) return '2v2对抗赛,是兄弟来助我一把!';
|
||||
if (battleType === 1 && count === 6) return '3v3对抗赛,来了马上发车!';
|
||||
return '星球论箭,来一决高下敢否?';
|
||||
if (battleType === 2) return t('ready_for_a_thrilling_free_for_all');
|
||||
if (battleType === 1 && count === 4) return t('join_my_team_for_a_2v2_match');
|
||||
if (battleType === 1 && count === 6) return t('join_our_3v3_match_we_re_ready_to_go');
|
||||
return t('ready_to_see_who_is_the_better_archer');
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -564,8 +566,8 @@ onBeforeUnmount(() => {
|
||||
<GuideTwo>
|
||||
<view class="battle-guide">
|
||||
<view class="guide-tips">
|
||||
<text class="guide-tips__target">请使用{{ targetSizeLabel }}全环靶</text>
|
||||
<text class="guide-tips__warn">如果实际靶纸与选择靶纸不同,将导致射箭无效</text>
|
||||
<text class="guide-tips__target">{{ t('please_use_a') }}{{ targetSizeLabel }}{{ t('full_ring_target') }}</text>
|
||||
<text class="guide-tips__warn">{{ t('shots_will_not_count_if_the_target_face_differs_from_your_select') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</GuideTwo>
|
||||
@@ -574,7 +576,7 @@ onBeforeUnmount(() => {
|
||||
<view>
|
||||
<view v-if="owner.id" class="player" :style="{ transform: 'translateY(-60px)' }">
|
||||
<Avatar :src="owner.avatar" :size="60" />
|
||||
<text>管理员</text>
|
||||
<text>{{ t('host') }}</text>
|
||||
<view
|
||||
v-if="isMember(owner)"
|
||||
:class="['room-player-name', ...getMemberNicknameClass(owner)]"
|
||||
@@ -587,14 +589,12 @@ onBeforeUnmount(() => {
|
||||
<text v-else class="room-player-name">{{ owner.name }}</text>
|
||||
</view>
|
||||
<view v-else class="no-player" :style="{ transform: 'translateY(-60px)' }">
|
||||
<image src="../static/question-mark.png" mode="widthFix" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/question-mark.png" mode="widthFix" />
|
||||
</view>
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/versus.png" mode="widthFix" />
|
||||
<view v-if="opponent.id" class="player" :style="{ transform: 'translateY(60px)' }">
|
||||
<Avatar :src="opponent.avatar" :size="60" />
|
||||
<text class="ready" :style="{ opacity: opponent.ready ? 1 : 0 }">
|
||||
已准备
|
||||
</text>
|
||||
<text class="ready" :style="{ opacity: opponent.ready ? 1 : 0 }">{{ t('ready_329') }}</text>
|
||||
<view
|
||||
v-if="isMember(opponent)"
|
||||
:class="['room-player-name', ...getMemberNicknameClass(opponent)]"
|
||||
@@ -607,11 +607,11 @@ onBeforeUnmount(() => {
|
||||
<text v-else class="room-player-name">{{ opponent.name }}</text>
|
||||
<button v-if="owner.id === user.id" hover-class="none" class="remove-player"
|
||||
@click="() => removePlayer(opponent)">
|
||||
<image src="../static/close-white.png" mode="widthFix" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/close-white.png" mode="widthFix" />
|
||||
</button>
|
||||
</view>
|
||||
<view class="no-player" v-else>
|
||||
<image src="../static/question-mark.png" mode="widthFix" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/question-mark.png" mode="widthFix" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -623,11 +623,11 @@ onBeforeUnmount(() => {
|
||||
<view>
|
||||
<view v-for="(item, index) in players" :key="index">
|
||||
<Avatar v-if="item.id" :src="item.avatar" :size="36" />
|
||||
<text v-if="owner.id === item.id">管理员</text>
|
||||
<text v-if="owner.id === item.id">{{ t('host') }}</text>
|
||||
<!-- 仅房主可见踢人按钮,且不能踢自己 -->
|
||||
<button v-if="owner.id !== item.id && item.id && owner.id === user.id" hover-class="none" class="remove-player"
|
||||
@click="() => removePlayer(item)" :style="{ top: '-10rpx', right: '-10rpx' }">
|
||||
<image src="../static/close-white.png" mode="widthFix" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/close-white.png" mode="widthFix" />
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -636,7 +636,7 @@ onBeforeUnmount(() => {
|
||||
<view>
|
||||
<view v-for="(item, index) in redTeam" :key="index" class="choose-side-left-item">
|
||||
<button hover-class="none" v-if="item.id === user.id" @click="chooseTeam(0)">
|
||||
<image src="../static/close-grey.png" mode="widthFix" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/close-grey.png" mode="widthFix" />
|
||||
</button>
|
||||
<view
|
||||
v-if="item.id && isMember(item)"
|
||||
@@ -647,13 +647,13 @@ onBeforeUnmount(() => {
|
||||
{{ item.name }}
|
||||
</text>
|
||||
</view>
|
||||
<text v-else class="truncate">{{ item.name || "我要加入" }}</text>
|
||||
<text v-else class="truncate">{{ item.name || t('join_330') }}</text>
|
||||
<view v-if="item.id" class="team-player-avatar">
|
||||
<Avatar :src="item.avatar" :size="36" />
|
||||
<text :style="{ opacity: !!item.state ? 1 : 0 }">已准备</text>
|
||||
<text :style="{ opacity: !!item.state ? 1 : 0 }">{{ t('ready') }}</text>
|
||||
</view>
|
||||
<button v-else hover-class="none" @click="chooseTeam(2)">
|
||||
<image src="../static/add-grey.png" mode="widthFix" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/add-grey.png" mode="widthFix" />
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -661,10 +661,10 @@ onBeforeUnmount(() => {
|
||||
<view v-for="(item, index) in blueTeam" :key="index" class="choose-side-right-item">
|
||||
<view v-if="item.id" class="team-player-avatar">
|
||||
<Avatar :src="item.avatar" :size="36" />
|
||||
<text :style="{ opacity: !!item.state ? 1 : 0 }">已准备</text>
|
||||
<text :style="{ opacity: !!item.state ? 1 : 0 }">{{ t('ready') }}</text>
|
||||
</view>
|
||||
<button v-else hover-class="none" @click="chooseTeam(1)">
|
||||
<image src="../static/add-grey.png" mode="widthFix" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/add-grey.png" mode="widthFix" />
|
||||
</button>
|
||||
<view
|
||||
v-if="item.id && isMember(item)"
|
||||
@@ -675,9 +675,9 @@ onBeforeUnmount(() => {
|
||||
{{ item.name }}
|
||||
</text>
|
||||
</view>
|
||||
<text v-else class="truncate">{{ item.name || "我要加入" }}</text>
|
||||
<text v-else class="truncate">{{ item.name || t('join_330') }}</text>
|
||||
<button hover-class="none" v-if="item.id === user.id" @click="chooseTeam(0)">
|
||||
<image src="../static/close-grey.png" mode="widthFix" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/close-grey.png" mode="widthFix" />
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -693,21 +693,21 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
{{
|
||||
allReady.value
|
||||
? "即将进入对局..."
|
||||
? t('joining_match')
|
||||
: owner.id === user.id
|
||||
? "开始对局"
|
||||
: "我准备好了"
|
||||
? t('start_match')
|
||||
: t('i_m_ready')
|
||||
}}
|
||||
</SButton>
|
||||
<text class="tips">所有人准备好后由房主点击开始</text>
|
||||
<text class="tips">{{ t('once_everyone_is_ready_the_host_can_start_the_match') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</Container>
|
||||
<ModalDialog
|
||||
:show="showLimitModal"
|
||||
:content="'今日约战次数已经用完\n开通会员可增加次数'"
|
||||
cancelText="知道了"
|
||||
confirmText="去开通"
|
||||
:content="t('you_have_used_all_your_challenges_for_today_activate_membership_')"
|
||||
:cancelText="t('got_it_335')"
|
||||
:confirmText="t('activate')"
|
||||
:onCancel="closeLimitModal"
|
||||
:onConfirm="goVipPage"
|
||||
/>
|
||||
@@ -715,13 +715,11 @@ onBeforeUnmount(() => {
|
||||
<ScreenHint :show="showKickConfirm">
|
||||
<view class="kick-confirm">
|
||||
<!-- 弹窗标题,玩家名字高亮显示 -->
|
||||
<text class="kick-confirm__title">
|
||||
是否把「<text style="color: #F7D550">{{ playerToKick && playerToKick.name }}</text>」移出房间?
|
||||
</text>
|
||||
<text class="kick-confirm__title">{{ t('remove') }}<text style="color: #F7D550">{{ playerToKick && playerToKick.name }}</text>{{ t('from_the_room') }}</text>
|
||||
<!-- 按钮区:确认在左,取消在右 -->
|
||||
<view class="kick-confirm__btns">
|
||||
<button hover-class="none" class="kick-confirm__btn kick-confirm__btn--confirm" @click="confirmKick">确定</button>
|
||||
<button hover-class="none" class="kick-confirm__btn kick-confirm__btn--cancel" @click="cancelKick">取消</button>
|
||||
<button hover-class="none" class="kick-confirm__btn kick-confirm__btn--confirm" @click="confirmKick">{{ t('confirm_134') }}</button>
|
||||
<button hover-class="none" class="kick-confirm__btn kick-confirm__btn--cancel" @click="cancelKick">{{ t('cancel_133') }}</button>
|
||||
</view>
|
||||
</view>
|
||||
</ScreenHint>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
defineProps({
|
||||
cumulative: {
|
||||
type: [Number, String],
|
||||
@@ -15,12 +17,12 @@ defineProps({
|
||||
<view class="balance-panel">
|
||||
<view class="balance-list">
|
||||
<view class="balance-item">
|
||||
<text class="balance-item__label">累计金币:</text>
|
||||
<text class="balance-item__label">{{ t('total_coins') }}</text>
|
||||
<text class="balance-item__value">{{ cumulative }}</text>
|
||||
</view>
|
||||
<view class="balance-list__line" />
|
||||
<view class="balance-item">
|
||||
<text class="balance-item__label">可兑换金币:</text>
|
||||
<text class="balance-item__label">{{ t('redeemable_coins') }}</text>
|
||||
<text class="balance-item__value">{{ available }}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -38,13 +40,12 @@ defineProps({
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 706rpx;
|
||||
width: 100%;
|
||||
height: 60rpx;
|
||||
margin-top: 0;
|
||||
border: 2rpx solid rgba(255, 217, 71, 0.25);
|
||||
border-radius: 12rpx;
|
||||
box-sizing: border-box;
|
||||
background-color: rgba(255, 217, 71, 0.06);
|
||||
background: linear-gradient(90deg, transparent, rgba(4, 4, 4, 0.7), transparent);
|
||||
}
|
||||
|
||||
.balance-item {
|
||||
@@ -69,7 +70,7 @@ defineProps({
|
||||
|
||||
.balance-list__line {
|
||||
width: 2rpx;
|
||||
height: 28rpx;
|
||||
height: 16rpx;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
margin: 0 22rpx;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
defineProps({
|
||||
text: {
|
||||
type: String,
|
||||
default: "暂无金币获取记录。",
|
||||
default: t('no_coin_earnings_yet'),
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -10,11 +10,13 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
switchable: { type: Boolean, default: false },
|
||||
onBack: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(["select-store"]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -30,16 +32,20 @@ const props = defineProps({
|
||||
>
|
||||
{{ title }}
|
||||
</text>
|
||||
<text v-if="subtitle" class="coin-header__subtitle">
|
||||
{{ subtitle }}
|
||||
</text>
|
||||
<view v-if="subtitle" class="coin-header__store" @click="switchable && emit('select-store')">
|
||||
<text class="coin-header__subtitle">{{ subtitle }}</text>
|
||||
<image v-if="switchable" class="coin-header__switch" src="https://static.shelingxingqiu.com/shootmini/static/coin/store-switch.png" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</Header>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="scss">
|
||||
.coin-header__store { display: flex; align-items: center; max-width: 430rpx; }
|
||||
.coin-header__store .coin-header__subtitle { overflow: hidden; text-overflow: ellipsis; }
|
||||
.coin-header__switch { flex-shrink: 0; width: 28rpx; height: 28rpx; margin-left: 10rpx; }
|
||||
.coin-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
defineProps({
|
||||
records: {
|
||||
type: Array,
|
||||
@@ -18,15 +20,15 @@ defineProps({
|
||||
>
|
||||
<view class="record-table__header">
|
||||
<template v-if="mode === 'exchange'">
|
||||
<text>兑换内容</text>
|
||||
<text>兑换类型</text>
|
||||
<text>时间</text>
|
||||
<text>金币使用</text>
|
||||
<text>{{ t('redeemed_item') }}</text>
|
||||
<text>{{ t('redemption_type') }}</text>
|
||||
<text>{{ t('time') }}</text>
|
||||
<text>{{ t('coins_used') }}</text>
|
||||
</template>
|
||||
<template v-else>
|
||||
<text>时间</text>
|
||||
<text>类型</text>
|
||||
<text>金币获取</text>
|
||||
<text>{{ t('time') }}</text>
|
||||
<text>{{ t('type') }}</text>
|
||||
<text>{{ t('coins_earned') }}</text>
|
||||
</template>
|
||||
</view>
|
||||
<view
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
const emit = defineEmits(["authorize"]);
|
||||
</script>
|
||||
|
||||
@@ -8,8 +10,8 @@ const emit = defineEmits(["authorize"]);
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/coin/location-permission.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>授权获取你的定位,以便推荐附近门店。</text>
|
||||
<button hover-class="none" @click="emit('authorize')">立即授权</button>
|
||||
<text>{{ t('allow_location_access_to_find_nearby_stores') }}</text>
|
||||
<button hover-class="none" @click="emit('authorize')">{{ t('allow_access') }}</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
defineProps({
|
||||
variant: {
|
||||
type: String,
|
||||
@@ -13,7 +15,7 @@ defineProps({
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/coin/icon-notice.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>暂不支持线上兑换,请前往线下门店进行兑换。</text>
|
||||
<text>{{ t('online_redemption_is_not_available_please_redeem_at_a_store') }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
const DEFAULT_PRODUCT_IMAGE =
|
||||
"https://static.shelingxingqiu.com/shootmini/static/coin/product-hero-item.png";
|
||||
"https://static.shelingxingqiu.com/shootmini/static/coin/product-hero-bg.png";
|
||||
const props = defineProps({
|
||||
images: {
|
||||
type: Array,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { computed } from "vue";
|
||||
|
||||
const DEFAULT_PRODUCT_IMAGE =
|
||||
@@ -26,9 +28,9 @@ const productImage = computed(() => {
|
||||
</view>
|
||||
<text class="product-card__name">{{ product.name }}</text>
|
||||
<view class="product-card__footer">
|
||||
<text>{{ product.cost }}金币</text>
|
||||
<text>{{ product.cost }}{{ t('coins') }}</text>
|
||||
<text class="product-card__divider">|</text>
|
||||
<text>剩余{{ product.stock }}个</text>
|
||||
<text>{{ t('remaining') }}{{ product.stock }}{{ t('empty_352') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -51,6 +53,9 @@ const productImage = computed(() => {
|
||||
height: 312rpx;
|
||||
overflow: hidden;
|
||||
border-radius: 12rpx;
|
||||
background-image: url("https://static.shelingxingqiu.com/shootmini/static/coin/product-card-bg.png");
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.product-card__image {
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
defineProps({
|
||||
benefit: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 与会员卡共用剩余次数,接口缺失时显示占位。 -->
|
||||
<view class="benefit-card">
|
||||
<text class="benefit-card__badge">{{ benefit.remaining }}{{ t('times') }}</text>
|
||||
<image class="benefit-card__icon" :src="benefit.icon" mode="aspectFit" />
|
||||
<view class="benefit-card__content">
|
||||
<text class="benefit-card__title">{{ benefit.title }}</text>
|
||||
<text class="benefit-card__description">{{ benefit.description }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.benefit-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 176rpx;
|
||||
margin-bottom: 24rpx;
|
||||
padding: 28rpx 28rpx 28rpx 56rpx;
|
||||
box-sizing: border-box;
|
||||
border-radius: 20rpx;
|
||||
background: linear-gradient(180deg, #080808, rgba(152, 124, 84, 0.4));
|
||||
}
|
||||
|
||||
// .benefit-card:last-child {
|
||||
// margin-bottom: 0;
|
||||
// }
|
||||
|
||||
.benefit-card__badge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 64rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 16rpx 0 16rpx 0;
|
||||
background: linear-gradient(135deg, #ffd19a, #a17636);
|
||||
color: #ffffff;
|
||||
font-size: 22rpx;
|
||||
line-height: 30rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.benefit-card__icon {
|
||||
flex-shrink: 0;
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
margin-right: 38rpx;
|
||||
}
|
||||
|
||||
.benefit-card__content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.benefit-card__title {
|
||||
display: block;
|
||||
color: #fae6bc;
|
||||
font-size: 30rpx;
|
||||
line-height: 42rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.benefit-card__description {
|
||||
display: block;
|
||||
margin-top: 10rpx;
|
||||
color: #ffffff;
|
||||
font-size: 22rpx;
|
||||
line-height: 30rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
const props = defineProps({
|
||||
store: {
|
||||
type: Object,
|
||||
@@ -19,13 +21,13 @@ const selectStore = () => {
|
||||
<text class="store-card__name">{{ store.name }}</text>
|
||||
<view class="store-card__info">
|
||||
<view class="store-card__row">
|
||||
<text>地址:{{ store.address }}</text>
|
||||
<text>{{ t('address') }}{{ store.address }}</text>
|
||||
</view>
|
||||
<view class="store-card__row">
|
||||
<text>电话:{{ store.phone }}</text>
|
||||
<text>{{ t('phone_355') }}{{ store.phone }}</text>
|
||||
</view>
|
||||
<view class="store-card__row">
|
||||
<text>营业时间:{{ store.hours }}</text>
|
||||
<text>{{ t('hours') }}{{ store.hours }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
tabs: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
active: {
|
||||
type: String,
|
||||
default: "vip",
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["select"]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 业务 Tab 只切换内容,不接管系统导航。 -->
|
||||
<view class="section-tabs">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="section-tabs__item"
|
||||
:class="{ 'section-tabs__item--active': active === tab.key }"
|
||||
hover-class="section-tabs__item--pressed"
|
||||
@click="emit('select', tab.key)"
|
||||
>
|
||||
<text>{{ tab.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.section-tabs {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 40rpx 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.section-tabs__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 326rpx;
|
||||
height: 68rpx;
|
||||
border-radius: 34rpx;
|
||||
color: #999999;
|
||||
background: #000000;
|
||||
font-size: 28rpx;
|
||||
line-height: 40rpx;
|
||||
}
|
||||
|
||||
.section-tabs__item--active {
|
||||
color: #ffffff;
|
||||
background: linear-gradient(135deg, #ffd19a, #a17636);
|
||||
}
|
||||
|
||||
.section-tabs__item--pressed {
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,137 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
defineProps({
|
||||
membership: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
benefits: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["introduction"]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="vip-card" :class="{ 'vip-card--active': membership.active }">
|
||||
<!-- 切图仅含静态字效与徽章,次数、有效期和状态独立渲染。 -->
|
||||
<image class="vip-card__background" :src="membership.cardImage" mode="aspectFit" />
|
||||
<view class="vip-card__benefits">
|
||||
<view v-for="benefit in benefits" :key="benefit.key" class="vip-card__benefit">
|
||||
<text class="vip-card__label">{{ benefit.title }}</text>
|
||||
<view class="vip-card__count">
|
||||
<text class="vip-card__number">{{ membership.remaining[benefit.key] }}</text>
|
||||
<text class="vip-card__unit">{{ t('times') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<text class="vip-card__status">
|
||||
{{ membership.active ? t('valid_until_value', { p0: membership.expiresAt }) : t('not_activated') }}
|
||||
</text>
|
||||
<view class="vip-card__introduction" @click="emit('introduction')">
|
||||
<text>{{ t('learn_more_about_experience_center_vip') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.vip-card {
|
||||
position: relative;
|
||||
width: 750rpx;
|
||||
height: 288rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.vip-card__background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 750rpx;
|
||||
height: 288rpx;
|
||||
}
|
||||
|
||||
.vip-card__benefits {
|
||||
position: absolute;
|
||||
top: 124rpx;
|
||||
left: 70rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 400rpx;
|
||||
}
|
||||
|
||||
.vip-card__benefit {
|
||||
width: 120rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vip-card__label {
|
||||
display: block;
|
||||
font-size: 20rpx;
|
||||
line-height: 28rpx;
|
||||
white-space: nowrap;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.vip-card__count {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.vip-card__count::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 16rpx;
|
||||
bottom: 4rpx;
|
||||
width: 88rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 8rpx;
|
||||
background: #ffd947;
|
||||
}
|
||||
|
||||
.vip-card__number {
|
||||
position: relative;
|
||||
font-size: 40rpx;
|
||||
line-height: 48rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.vip-card__unit {
|
||||
position: relative;
|
||||
margin-left: 2rpx;
|
||||
font-size: 22rpx;
|
||||
line-height: 30rpx;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.vip-card__status {
|
||||
position: absolute;
|
||||
top: 212rpx;
|
||||
left: 496rpx;
|
||||
width: 232rpx;
|
||||
color: #888888;
|
||||
font-size: 22rpx;
|
||||
line-height: 30rpx;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.vip-card--active .vip-card__status {
|
||||
color: #b47d42;
|
||||
}
|
||||
|
||||
.vip-card__introduction {
|
||||
position: absolute;
|
||||
left: 74rpx;
|
||||
bottom: 16rpx;
|
||||
padding: 6rpx 0;
|
||||
font-size: 22rpx;
|
||||
line-height: 30rpx;
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,64 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
const props = defineProps({
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(["open"]);
|
||||
const handleOpen = () => {
|
||||
if (!props.active) emit("open");
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 当前门店已开通时仅展示状态,未开通时提供线下开通提醒。 -->
|
||||
<view class="vip-footer">
|
||||
<text class="vip-footer__price">{{ t('special_offer_cny_9_9_90_days') }}</text>
|
||||
<view class="vip-footer__status" @click="handleOpen">
|
||||
<image class="vip-footer__badge" src="https://static.shelingxingqiu.com/shootmini/static/coin/store-vip-badge.png" mode="aspectFit" />
|
||||
<text>{{ active ? t('your_vip_is_active') : t('to_activate_please_contact_staff_at_a_store') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.vip-footer {
|
||||
margin-top: 84rpx;
|
||||
padding: 0 24rpx 22rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.vip-footer__price {
|
||||
display: block;
|
||||
color: #fae6bc;
|
||||
font-size: 40rpx;
|
||||
line-height: 56rpx;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vip-footer__status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 90rpx;
|
||||
margin-top: 12rpx;
|
||||
padding: 8rpx 28rpx;
|
||||
box-sizing: border-box;
|
||||
border-radius: 46rpx;
|
||||
color: #333333;
|
||||
background: #ffd947;
|
||||
font-size: 26rpx;
|
||||
line-height: 36rpx;
|
||||
}
|
||||
|
||||
.vip-footer__badge {
|
||||
flex-shrink: 0;
|
||||
width: 80rpx;
|
||||
height: 74rpx;
|
||||
margin-right: 4rpx;
|
||||
}
|
||||
</style>
|
||||
+33
-4
@@ -1,22 +1,51 @@
|
||||
import { t } from "@/locales";
|
||||
// 金币快捷入口保持原有路由与图标配置。
|
||||
export const quickMenus = [
|
||||
{
|
||||
key: "rules",
|
||||
label: "金币规则",
|
||||
label: t('coin_rules'),
|
||||
icon: "https://static.shelingxingqiu.com/shootmini/static/coin/icon-rules.png",
|
||||
},
|
||||
{
|
||||
key: "earningRecords",
|
||||
label: "获取明细",
|
||||
label: t('earnings'),
|
||||
icon: "https://static.shelingxingqiu.com/shootmini/static/coin/icon-earning-records.png",
|
||||
},
|
||||
{
|
||||
key: "exchangeRecords",
|
||||
label: "兑换记录",
|
||||
label: t('redemptions'),
|
||||
icon: "https://static.shelingxingqiu.com/shootmini/static/coin/icon-exchange-records.png",
|
||||
},
|
||||
{
|
||||
key: "nearbyStores",
|
||||
label: "附近门店",
|
||||
label: t('nearby_stores'),
|
||||
icon: "https://static.shelingxingqiu.com/shootmini/static/coin/icon-nearby-store.png",
|
||||
},
|
||||
];
|
||||
|
||||
// 体验中心业务 Tab。
|
||||
export const storeSections = [
|
||||
{ key: "vip", label: t('experience_center_vip') },
|
||||
{ key: "coin", label: t('my_coins') },
|
||||
];
|
||||
|
||||
export const storeVipBenefits = [
|
||||
{
|
||||
key: "trial",
|
||||
title: t('free_trial'),
|
||||
description: t('enjoy_one_free_30_minute_smart_archery_session_join_the_official'),
|
||||
icon: "https://static.shelingxingqiu.com/shootmini/static/coin/store-vip-benefit-trial.png",
|
||||
},
|
||||
{
|
||||
key: "discount",
|
||||
title: t('special_offer'),
|
||||
description: t('buy_one_30_minute_experience_session_at_the_special_price_of_cny'),
|
||||
icon: "https://static.shelingxingqiu.com/shootmini/static/coin/store-vip-benefit-discount.png",
|
||||
},
|
||||
{
|
||||
key: "drink",
|
||||
title: t('complimentary_drink'),
|
||||
description: t('receive_one_complimentary_designated_drink_each_time_you_visit'),
|
||||
icon: "https://static.shelingxingqiu.com/shootmini/static/coin/store-vip-benefit-drink.png",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import Container from "@/components/Container.vue";
|
||||
@@ -99,7 +101,7 @@ onLoad((options = {}) => {
|
||||
<template>
|
||||
<Container :bgType="6" :isHome="true" @scrolltolower="loadRecords">
|
||||
<template #header>
|
||||
<CoinHeader title="金币获取明细" />
|
||||
<CoinHeader :title="t('coin_earnings')" />
|
||||
</template>
|
||||
<view class="records-page">
|
||||
<CoinEmptyState v-if="showEmpty" />
|
||||
@@ -108,7 +110,7 @@ onLoad((options = {}) => {
|
||||
v-if="loading || (noMore && earningRecords.length)"
|
||||
class="records-page__status"
|
||||
>
|
||||
<text>{{ loading ? "加载中..." : "没有更多了" }}</text>
|
||||
<text>{{ loading ? t('loading') : t('no_more_results') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</Container>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import Container from "@/components/Container.vue";
|
||||
@@ -99,16 +101,16 @@ onLoad((options = {}) => {
|
||||
<template>
|
||||
<Container :bgType="6" :isHome="true" @scrolltolower="loadRecords">
|
||||
<template #header>
|
||||
<CoinHeader title="金币兑换记录" />
|
||||
<CoinHeader :title="t('coin_redemptions')" />
|
||||
</template>
|
||||
<view class="records-page">
|
||||
<CoinEmptyState v-if="showEmpty" text="暂无金币兑换记录。" />
|
||||
<CoinEmptyState v-if="showEmpty" :text="t('no_redemptions_yet')" />
|
||||
<CoinRecordList v-else mode="exchange" :records="exchangeRecords" />
|
||||
<view
|
||||
v-if="loading || (noMore && exchangeRecords.length)"
|
||||
class="records-page__status"
|
||||
>
|
||||
<text>{{ loading ? "加载中..." : "没有更多了" }}</text>
|
||||
<text>{{ loading ? t('loading') : t('no_more_results') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</Container>
|
||||
|
||||
+149
-31
@@ -1,14 +1,55 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import Container from "@/components/Container.vue";
|
||||
import ModalDialog from "@/components/ModalDialog.vue";
|
||||
import CoinHeader from "./components/CoinHeader.vue";
|
||||
import CoinBalancePanel from "./components/CoinBalancePanel.vue";
|
||||
import CoinQuickMenu from "./components/CoinQuickMenu.vue";
|
||||
import OfflineExchangeNotice from "./components/OfflineExchangeNotice.vue";
|
||||
import RewardProductCard from "./components/RewardProductCard.vue";
|
||||
import { getGiftListAPI, getMyGoldAPI } from "@/apis";
|
||||
import { quickMenus } from "./data";
|
||||
import { quickMenus, storeSections, storeVipBenefits } from "./data";
|
||||
import StoreSectionTabs from "./components/StoreSectionTabs.vue";
|
||||
import StoreVipCard from "./components/StoreVipCard.vue";
|
||||
import StoreBenefitCard from "./components/StoreBenefitCard.vue";
|
||||
import StoreVipFooter from "./components/StoreVipFooter.vue";
|
||||
import { getSelectedExperienceStore, rememberExperienceStore, loadStoreMembership } from "@/utils/storeVip";
|
||||
|
||||
const activeTab = ref("vip");
|
||||
const membership = ref(null);
|
||||
const vipLoading = ref(false);
|
||||
const vipError = ref("");
|
||||
const displayedBenefits = computed(() => storeVipBenefits.map((benefit) => ({
|
||||
...benefit, remaining: membership.value?.remaining[benefit.key] ?? "--",
|
||||
})));
|
||||
const loadMembership = async () => {
|
||||
if (!selectedStoreId.value || vipLoading.value) return;
|
||||
vipLoading.value = true;
|
||||
vipError.value = "";
|
||||
membership.value = null;
|
||||
try {
|
||||
const result = await loadStoreMembership(selectedStoreId.value);
|
||||
membership.value = result;
|
||||
if (result.name) selectedStoreName.value = result.name;
|
||||
rememberExperienceStore(selectedStoreId.value, selectedStoreName.value);
|
||||
} catch (error) {
|
||||
vipError.value = t('could_not_load_membership_details_retry_or_select_another_store');
|
||||
console.error("加载门店会员失败", error);
|
||||
} finally {
|
||||
vipLoading.value = false;
|
||||
}
|
||||
};
|
||||
const selectStore = () => uni.redirectTo({ url: `/pages/coin/nearby-stores?tab=${activeTab.value}` });
|
||||
const selectTab = (tab) => {
|
||||
if (storeSections.some((item) => item.key === tab)) activeTab.value = tab;
|
||||
};
|
||||
const showVipIntroduction = () => uni.navigateTo({ url: "/pages/coin/vip-intro" });
|
||||
const vipOpeningVisible = ref(false);
|
||||
const showVipOpening = () => { vipOpeningVisible.value = true; };
|
||||
const closeVipOpening = () => { vipOpeningVisible.value = false; };
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
const coinSummary = ref({
|
||||
@@ -19,8 +60,8 @@ const selectedStoreId = ref("");
|
||||
const selectedStoreName = ref("");
|
||||
const rewardProducts = ref([]);
|
||||
const productPage = ref(0);
|
||||
const productTotal = ref(0);
|
||||
const productLoading = ref(false);
|
||||
const productError = ref(false);
|
||||
const productNoMore = ref(false);
|
||||
|
||||
const loadCoinSummary = async () => {
|
||||
@@ -40,6 +81,7 @@ const loadProducts = async ({ reset = false } = {}) => {
|
||||
|
||||
const nextPage = reset ? 1 : productPage.value + 1;
|
||||
productLoading.value = true;
|
||||
productError.value = false;
|
||||
if (reset) productNoMore.value = false;
|
||||
|
||||
try {
|
||||
@@ -65,7 +107,6 @@ const loadProducts = async ({ reset = false } = {}) => {
|
||||
const totalCount = Number(result?.totalCount ?? result?.total);
|
||||
const responsePageSize =
|
||||
Number(result?.pageSize ?? result?.page_size) || PAGE_SIZE;
|
||||
productTotal.value = Number.isFinite(totalCount) ? totalCount : 0;
|
||||
|
||||
if (Number.isFinite(pageCount) && pageCount >= 0) {
|
||||
productNoMore.value = productPage.value >= pageCount;
|
||||
@@ -79,6 +120,7 @@ const loadProducts = async ({ reset = false } = {}) => {
|
||||
rewardProducts.value = [];
|
||||
productPage.value = 0;
|
||||
}
|
||||
productError.value = true;
|
||||
console.error("加载礼品列表失败", error);
|
||||
} finally {
|
||||
productLoading.value = false;
|
||||
@@ -115,7 +157,7 @@ const decodeRouteText = (value = "") => {
|
||||
const onMenuSelect = (menu) => {
|
||||
if (menu.key === "nearbyStores") {
|
||||
uni.redirectTo({
|
||||
url: "/pages/coin/nearby-stores",
|
||||
url: `/pages/coin/nearby-stores?tab=${activeTab.value}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -123,7 +165,7 @@ const onMenuSelect = (menu) => {
|
||||
const url = menuRoutes[menu.key];
|
||||
if (!url) {
|
||||
uni.showToast({
|
||||
title: "功能开发中",
|
||||
title: t('coming_soon_385'),
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
@@ -143,50 +185,102 @@ const toProductDetail = (product) => {
|
||||
};
|
||||
|
||||
onLoad((options = {}) => {
|
||||
const storeId = String(options.storeId || "");
|
||||
activeTab.value = options.tab === "coin" ? "coin" : "vip";
|
||||
const previous = getSelectedExperienceStore();
|
||||
const storeId = String(options.storeId || previous?.id || "");
|
||||
if (!/^\d+$/.test(storeId) || Number(storeId) <= 0) {
|
||||
uni.redirectTo({
|
||||
url: "/pages/coin/nearby-stores",
|
||||
});
|
||||
selectStore();
|
||||
return;
|
||||
}
|
||||
|
||||
selectedStoreId.value = storeId;
|
||||
selectedStoreName.value = decodeRouteText(options.storeName);
|
||||
loadCoinSummary();
|
||||
selectedStoreName.value = options.storeId
|
||||
? decodeRouteText(options.storeName)
|
||||
: previous?.name || "";
|
||||
rememberExperienceStore(storeId, selectedStoreName.value);
|
||||
loadProducts({ reset: true });
|
||||
});
|
||||
|
||||
// 从介绍、商品等页面返回时刷新会员和余额。
|
||||
onShow(() => {
|
||||
if (!selectedStoreId.value) return;
|
||||
loadMembership();
|
||||
loadCoinSummary();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Container :bgType="6" :isHome="true" @scrolltolower="loadProducts">
|
||||
<!-- 标题固定,门店切换复用原有附近门店页面。 -->
|
||||
<Container :bgType="6" bgColor="#22222e" :isHome="true" @scrolltolower="activeTab === 'coin' && loadProducts()">
|
||||
<template #header>
|
||||
<CoinHeader title="我的金币" :subtitle="selectedStoreName" />
|
||||
<CoinHeader :title="t('experience_center')" :subtitle="selectedStoreName || t('select_store')" switchable @select-store="selectStore" />
|
||||
</template>
|
||||
<view class="coin-page">
|
||||
<CoinBalancePanel
|
||||
:cumulative="coinSummary.cumulative"
|
||||
:available="coinSummary.available"
|
||||
<StoreSectionTabs
|
||||
:tabs="storeSections"
|
||||
:active="activeTab"
|
||||
@select="selectTab"
|
||||
/>
|
||||
<CoinQuickMenu :menus="quickMenus" @select="onMenuSelect" />
|
||||
<OfflineExchangeNotice />
|
||||
<view class="reward-section">
|
||||
<view class="reward-grid">
|
||||
<view
|
||||
v-for="product in rewardProducts"
|
||||
:key="product.id"
|
||||
class="reward-grid__item"
|
||||
@click="toProductDetail(product)"
|
||||
>
|
||||
<RewardProductCard :product="product" />
|
||||
|
||||
<!-- 卡片与权益列表共用当前门店的会员数据。 -->
|
||||
<view v-if="activeTab === 'vip'" class="vip-section">
|
||||
<view v-if="vipLoading" class="page-status">{{ t('loading_membership') }}</view>
|
||||
<view v-else-if="vipError" class="page-status" @click="loadMembership">{{ vipError }}</view>
|
||||
<StoreVipCard
|
||||
v-else-if="membership"
|
||||
:membership="membership"
|
||||
:benefits="storeVipBenefits"
|
||||
@introduction="showVipIntroduction"
|
||||
/>
|
||||
<view class="vip-section__content">
|
||||
<text class="vip-section__description">{{ t('experience_center_vip_is_designed_for_archery_enthusiasts_activa') }}</text>
|
||||
<view class="vip-section__benefits">
|
||||
<StoreBenefitCard
|
||||
v-for="benefit in displayedBenefits"
|
||||
:key="benefit.key"
|
||||
:benefit="benefit"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<StoreVipFooter v-if="membership" :active="membership.active" @open="showVipOpening" />
|
||||
</view>
|
||||
|
||||
<!-- 保留原金币接口、分页与详情链路。 -->
|
||||
<view v-else class="coin-section">
|
||||
<CoinBalancePanel
|
||||
:cumulative="coinSummary.cumulative"
|
||||
:available="coinSummary.available"
|
||||
/>
|
||||
<CoinQuickMenu :menus="quickMenus" @select="onMenuSelect" />
|
||||
<OfflineExchangeNotice />
|
||||
<view class="reward-section">
|
||||
<view class="reward-grid">
|
||||
<view
|
||||
v-for="product in rewardProducts"
|
||||
:key="product.id"
|
||||
class="reward-grid__item"
|
||||
@click="toProductDetail(product)"
|
||||
>
|
||||
<RewardProductCard :product="product" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="productLoading" class="page-status">{{ t('loading') }}</view>
|
||||
<view v-else-if="productError" class="page-status" @click="loadProducts()">{{ t('could_not_load_tap_to_retry') }}</view>
|
||||
<view v-else-if="productNoMore" class="page-status">{{ rewardProducts.length ? t('no_more_products') : t('no_items_available_for_redemption') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</Container>
|
||||
<ModalDialog
|
||||
:show="vipOpeningVisible"
|
||||
:content="t('please_visit_an_experience_center_and_contact_staff_to_activate_')"
|
||||
:show-cancel="false"
|
||||
:confirm-text="t('got_it_394')"
|
||||
:on-confirm="closeVipOpening"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="scss">
|
||||
.page-status { padding: 30rpx; color: #cccccc; text-align: center; font-size: 26rpx; }
|
||||
.coin-page {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
@@ -194,6 +288,30 @@ onLoad((options = {}) => {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.vip-section {
|
||||
padding-top: 30rpx;
|
||||
}
|
||||
|
||||
.vip-section__content {
|
||||
padding: 0 28rpx;
|
||||
}
|
||||
|
||||
.vip-section__description {
|
||||
display: block;
|
||||
margin-top: 50rpx;
|
||||
color: #ffffff;
|
||||
font-size: 24rpx;
|
||||
line-height: 34rpx;
|
||||
}
|
||||
|
||||
.vip-section__benefits {
|
||||
margin-top: 30rpx;
|
||||
}
|
||||
|
||||
.coin-section {
|
||||
padding-top: 20rpx;
|
||||
}
|
||||
|
||||
.reward-section {
|
||||
padding: 20rpx 28rpx 40rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import Container from "@/components/Container.vue";
|
||||
@@ -6,6 +8,9 @@ import CoinHeader from "./components/CoinHeader.vue";
|
||||
import LocationPermissionState from "./components/LocationPermissionState.vue";
|
||||
import StoreCard from "./components/StoreCard.vue";
|
||||
import { getNearbyStoresAPI } from "@/apis";
|
||||
import { rememberExperienceStore } from "@/utils/storeVip";
|
||||
|
||||
const activeTab = ref("vip");
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
const DEFAULT_STORE_IMAGE =
|
||||
@@ -106,25 +111,29 @@ const selectStore = (store) => {
|
||||
const storeId = String(store?.id || "");
|
||||
if (!/^\d+$/.test(storeId) || Number(storeId) <= 0) {
|
||||
uni.showToast({
|
||||
title: "门店信息无效",
|
||||
title: t('invalid_store_information'),
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const storeName = encodeURIComponent(store?.name || "");
|
||||
rememberExperienceStore(storeId, store?.name || "");
|
||||
uni.redirectTo({
|
||||
url: `/pages/coin/index?storeId=${encodeURIComponent(storeId)}&storeName=${storeName}`,
|
||||
url: `/pages/coin/index?storeId=${encodeURIComponent(storeId)}&storeName=${storeName}&tab=${activeTab.value}`,
|
||||
});
|
||||
};
|
||||
|
||||
onLoad(locateAndLoadStores);
|
||||
onLoad((options = {}) => {
|
||||
activeTab.value = options.tab === "coin" ? "coin" : "vip";
|
||||
locateAndLoadStores();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Container :bgType="6" :isHome="true" @scrolltolower="loadStores">
|
||||
<template #header>
|
||||
<CoinHeader title="附近门店" />
|
||||
<CoinHeader :title="t('nearby_stores')" />
|
||||
</template>
|
||||
<view class="stores-page">
|
||||
<LocationPermissionState
|
||||
@@ -138,10 +147,8 @@ onLoad(locateAndLoadStores);
|
||||
:store="store"
|
||||
@select="selectStore"
|
||||
/>
|
||||
<text v-if="loaded && !stores.length" class="stores-page__more">
|
||||
附近暂无门店~
|
||||
</text>
|
||||
<text v-else-if="noMore" class="stores-page__more">没有更多门店了~</text>
|
||||
<text v-if="loaded && !stores.length" class="stores-page__more">{{ t('no_nearby_stores') }}</text>
|
||||
<text v-else-if="noMore" class="stores-page__more">{{ t('no_more_stores') }}</text>
|
||||
</template>
|
||||
</view>
|
||||
</Container>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import Container from "@/components/Container.vue";
|
||||
@@ -47,7 +49,7 @@ onLoad((options = {}) => {
|
||||
const id = Number(options.id);
|
||||
if (!Number.isInteger(id) || id <= 0) {
|
||||
uni.showToast({
|
||||
title: "商品参数无效",
|
||||
title: t('invalid_product_parameters'),
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
@@ -60,7 +62,7 @@ onLoad((options = {}) => {
|
||||
<template>
|
||||
<Container :bgType="6" :isHome="true">
|
||||
<template #header>
|
||||
<CoinHeader title="商品详情" />
|
||||
<CoinHeader :title="t('product_details')" />
|
||||
</template>
|
||||
<view class="product-detail">
|
||||
<ProductHeroSwiper
|
||||
@@ -69,9 +71,9 @@ onLoad((options = {}) => {
|
||||
<view class="product-detail__summary">
|
||||
<text class="product-detail__name">{{ productDetail.name }}</text>
|
||||
<view class="product-detail__balance">
|
||||
<text>金币:</text>
|
||||
<text>{{ t('coins_403') }}</text>
|
||||
<text class="product-detail__amount">{{ productDetail.coinPrice }}</text>
|
||||
<text>(剩余{{ productDetail.stock }}个)</text>
|
||||
<text>{{ t('empty_404') }}{{ productDetail.stock }}{{ t('remaining_405') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<OfflineExchangeNotice variant="red" />
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import Container from "@/components/Container.vue";
|
||||
@@ -84,10 +86,10 @@ onLoad((options = {}) => {
|
||||
<template>
|
||||
<Container :bgType="6" :isHome="true" @scrolltolower="loadRules">
|
||||
<template #header>
|
||||
<CoinHeader title="金币规则" />
|
||||
<CoinHeader :title="t('coin_rules')" />
|
||||
</template>
|
||||
<view class="rules-page">
|
||||
<CoinEmptyState v-if="showEmpty" text="暂无金币规则。" />
|
||||
<CoinEmptyState v-if="showEmpty" :text="t('no_coin_rules_yet')" />
|
||||
<template v-else>
|
||||
<view
|
||||
v-for="rule in coinRules"
|
||||
@@ -98,7 +100,7 @@ onLoad((options = {}) => {
|
||||
</view>
|
||||
</template>
|
||||
<view v-if="loading" class="rules-page__status">
|
||||
<text>加载中...</text>
|
||||
<text>{{ t('loading') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</Container>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import Container from "@/components/Container.vue";
|
||||
import CoinHeader from "./components/CoinHeader.vue";
|
||||
import { getStoreVipIntroAPI } from "@/apis";
|
||||
|
||||
const content = ref("");
|
||||
const loading = ref(false);
|
||||
const failed = ref(false);
|
||||
|
||||
const loadIntroduction = async () => {
|
||||
if (loading.value) return;
|
||||
loading.value = true;
|
||||
failed.value = false;
|
||||
try {
|
||||
const result = await getStoreVipIntroAPI();
|
||||
if (typeof result?.content !== "string") {
|
||||
throw new Error("体验中心 VIP 介绍响应格式异常");
|
||||
}
|
||||
// 保留服务端原文与换行,空白内容单独展示提示。
|
||||
content.value = result.content;
|
||||
} catch (error) {
|
||||
failed.value = true;
|
||||
console.error("加载体验中心 VIP 介绍失败", error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onLoad(loadIntroduction);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Container :bgType="6" bgColor="#22222e" :isHome="true">
|
||||
<template #header><CoinHeader :title="t('about_experience_center_vip')" /></template>
|
||||
<view class="vip-intro">
|
||||
<text v-if="loading">{{ t('loading') }}</text>
|
||||
<text v-else-if="failed" @click="loadIntroduction">{{ t('could_not_load_tap_to_retry') }}</text>
|
||||
<text v-else-if="content.trim()">{{ content }}</text>
|
||||
<text v-else>{{ t('no_information_available') }}</text>
|
||||
</view>
|
||||
</Container>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.vip-intro { padding: 32rpx 28rpx; color: #ffffff; font-size: 28rpx; line-height: 44rpx; white-space: pre-wrap; }
|
||||
</style>
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||
import { onShow, onHide } from "@dcloudio/uni-app";
|
||||
import Container from "@/components/Container.vue";
|
||||
@@ -10,15 +12,15 @@ import { MESSAGETYPES } from "@/constants";
|
||||
|
||||
const guides = [
|
||||
{
|
||||
title: "箭头面向靶子",
|
||||
title: t('point_the_arrow_at_the_target'),
|
||||
src: "https://static.shelingxingqiu.com/attachment/2025-10-30/ddv9p5fk5wscg7hrfo.png",
|
||||
},
|
||||
{
|
||||
title: "摆出拉弓姿势",
|
||||
title: t('take_your_shooting_stance'),
|
||||
src: "https://static.shelingxingqiu.com/attachment/2025-10-30/ddv9p5fk5b7ljrhx3o.png",
|
||||
},
|
||||
{
|
||||
title: "调整瞄准器",
|
||||
title: t('adjust_the_sight'),
|
||||
src: "https://static.shelingxingqiu.com/attachment/2025-10-29/dduexjgrcxf9wjaiv4.png",
|
||||
},
|
||||
];
|
||||
@@ -112,7 +114,7 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Container title="校准智能弓">
|
||||
<Container :title="t('calibrate_smart_bow')">
|
||||
<view class="container">
|
||||
<view v-for="(guide, index) in guides" :key="guide.title" class="guide">
|
||||
<view>
|
||||
@@ -121,15 +123,13 @@ onBeforeUnmount(() => {
|
||||
</view>
|
||||
<image :src="guide.src" mode="widthFix" />
|
||||
</view>
|
||||
<text>请完成以上步骤校准智能弓</text>
|
||||
<text>{{ t('complete_the_steps_above_to_calibrate_your_smart_bow') }}</text>
|
||||
<SButton
|
||||
:onClick="onComplete"
|
||||
width="60vw"
|
||||
:rounded="40"
|
||||
:disabled="!done"
|
||||
>
|
||||
我已校准
|
||||
</SButton>
|
||||
>{{ t('i_ve_calibrated_it') }}</SButton>
|
||||
</view>
|
||||
</Container>
|
||||
</template>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { t } from "@/locales";
|
||||
import { bindDeviceAPIV2 } from "@/apis";
|
||||
|
||||
export function useDeviceBinding({
|
||||
@@ -13,7 +14,17 @@ export function useDeviceBinding({
|
||||
url: "/pages/device/device-bind-failure",
|
||||
fail: (error) => {
|
||||
console.error("打开绑定失败页失败", error);
|
||||
uni.showToast({ title: "二维码不正确,请重新扫码", icon: "none" });
|
||||
uni.showToast({ title: t('invalid_qr_code_please_scan_again'), icon: "none" });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const showAlreadyBoundPage = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/device/device-already-bound",
|
||||
fail: (error) => {
|
||||
console.error("打开设备已绑定页失败", error);
|
||||
uni.showToast({ title: t('this_device_is_bound_to_another_account_unbind_it_first'), icon: "none" });
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -45,10 +56,8 @@ export function useDeviceBinding({
|
||||
const result = await bindDeviceAPIV2(token.value);
|
||||
confirmBindTip.value = false;
|
||||
if (result?.binded) {
|
||||
uni.showToast({
|
||||
title: "设备已绑定其他账号,请解绑后再绑定",
|
||||
icon: "none",
|
||||
});
|
||||
token.value = "";
|
||||
showAlreadyBoundPage();
|
||||
return;
|
||||
}
|
||||
const deviceId = String(result?.deviceId || "").trim();
|
||||
@@ -69,7 +78,7 @@ export function useDeviceBinding({
|
||||
fail: (navigationError) => {
|
||||
applyBoundDevice();
|
||||
console.error("打开绑定成功页失败", navigationError);
|
||||
uni.showToast({ title: "绑定成功,请返回查看设备", icon: "none" });
|
||||
uni.showToast({ title: t('bound_successfully_go_back_to_view_your_device'), icon: "none" });
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { t, translateStatic } from "@/locales";
|
||||
import { computed, ref } from "vue";
|
||||
import {
|
||||
getDeviceDetailAPI,
|
||||
@@ -11,8 +12,10 @@ export function useDeviceStatus({
|
||||
user,
|
||||
device,
|
||||
deviceStatus,
|
||||
deviceUsageDuration,
|
||||
online,
|
||||
updateDevice,
|
||||
updateDeviceUsageDuration,
|
||||
clearDevice,
|
||||
unbindDialogVisible,
|
||||
}) {
|
||||
@@ -26,8 +29,8 @@ export function useDeviceStatus({
|
||||
() => isDeviceOnline.value && deviceStatus.value.charging === true
|
||||
);
|
||||
const statusText = computed(() => {
|
||||
if (!isDeviceOnline.value) return "未连接";
|
||||
return isDeviceCharging.value ? "已连接(充电中)" : "已连接";
|
||||
if (!isDeviceOnline.value) return t('disconnected');
|
||||
return isDeviceCharging.value ? t('connected_charging') : t('connected');
|
||||
});
|
||||
const statusClass = computed(() =>
|
||||
isDeviceOnline.value ? "device-status--online" : "device-status--offline"
|
||||
@@ -41,18 +44,32 @@ export function useDeviceStatus({
|
||||
const batteryText = computed(() =>
|
||||
battery.value === null ? "暂无数据" : `${battery.value}%`
|
||||
);
|
||||
const normalizeDuration = (value) => {
|
||||
if (value == null || String(value).trim() === "") return null;
|
||||
const seconds = Number(value);
|
||||
return Number.isFinite(seconds) && seconds >= 0 ? seconds : null;
|
||||
};
|
||||
|
||||
const onlineDurationText = computed(() => {
|
||||
const rawDuration = deviceStatus.value.onlineDuration;
|
||||
if (rawDuration == null || String(rawDuration).trim() === "") return "--";
|
||||
const currentDeviceId = String(device.value.deviceId || "");
|
||||
const cachedDuration =
|
||||
String(deviceUsageDuration.value.deviceId || "") === currentDeviceId
|
||||
? normalizeDuration(deviceUsageDuration.value.seconds)
|
||||
: null;
|
||||
const durations = [
|
||||
normalizeDuration(deviceStatus.value.onlineDuration),
|
||||
normalizeDuration(deviceDetails.value.onlineDuration),
|
||||
cachedDuration,
|
||||
].filter((value) => value !== null);
|
||||
|
||||
const seconds = Number(rawDuration);
|
||||
if (!Number.isFinite(seconds) || seconds < 0) return "--";
|
||||
if (!durations.length) return "--";
|
||||
const seconds = Math.max(...durations);
|
||||
|
||||
// WS 推送秒数,页面按完整分钟展示累计使用时间。
|
||||
// 累计时长应单调递增,页面按完整分钟展示当前已知最大值。
|
||||
const totalMinutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
return hours > 0 ? `${hours}小时${minutes}分钟` : `${minutes}分钟`;
|
||||
return hours > 0 ? t('valueh_valuemin', { p0: hours, p1: minutes }) : t('valuemin', { p0: minutes });
|
||||
});
|
||||
const networkType = computed(() =>
|
||||
String(deviceStatus.value.netType ?? "").trim().toLowerCase()
|
||||
@@ -61,19 +78,18 @@ export function useDeviceStatus({
|
||||
const netType = networkType.value;
|
||||
if (netType === "wifi") return "WiFi";
|
||||
if (netType === "4g") return "4G";
|
||||
return isDeviceOnline.value ? "在线" : "未连接";
|
||||
return isDeviceOnline.value ? t('online') : t('disconnected');
|
||||
});
|
||||
const maskedDeviceId = computed(() => {
|
||||
const id = String(device.value.deviceId || "");
|
||||
if (!id) return "暂无设备编号";
|
||||
if (id.length <= 3) return id;
|
||||
return `${"*".repeat(Math.min(5, id.length - 3))}${id.slice(-3)}`;
|
||||
if (!id) return t('no_device_number');
|
||||
return "*****";
|
||||
});
|
||||
const deviceRows = computed(() => [
|
||||
{ label: "设备型号", value: deviceDetails.value.model || "射灵智能弓" },
|
||||
{ label: "当前电量", value: batteryText.value },
|
||||
{ label: "连接方式", value: networkText.value },
|
||||
{ label: "设备编号", value: maskedDeviceId.value },
|
||||
{ label: t('model'), value: deviceDetails.value.model || t('sheling_smart_bow') },
|
||||
{ label: t('battery_434'), value: translateStatic(batteryText.value) },
|
||||
{ label: t('connection'), value: networkText.value },
|
||||
{ label: t('device_number'), value: maskedDeviceId.value },
|
||||
]);
|
||||
|
||||
const getDeviceNameOverrides = () => {
|
||||
@@ -83,7 +99,7 @@ export function useDeviceStatus({
|
||||
|
||||
const refreshDeviceDetails = async () => {
|
||||
const deviceId = device.value.deviceId;
|
||||
if (!deviceId) return;
|
||||
if (!deviceId) return null;
|
||||
|
||||
const requestVersion = ++deviceDetailRequestVersion;
|
||||
try {
|
||||
@@ -95,13 +111,17 @@ export function useDeviceStatus({
|
||||
requestVersion !== deviceDetailRequestVersion ||
|
||||
device.value.deviceId !== deviceId
|
||||
) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
deviceDetails.value = {
|
||||
...deviceDetails.value,
|
||||
...detail,
|
||||
deviceModelName: detail.deviceModelName ?? "",
|
||||
latestVersion: detail.latestVersion ?? "",
|
||||
needUpdate: detail.needUpdate ?? false,
|
||||
forceUpdate: detail.forceUpdate ?? false,
|
||||
version: detail.version ?? "",
|
||||
bindTime: String(
|
||||
detail.bindTime ?? deviceDetails.value.bindTime ?? ""
|
||||
).trim(),
|
||||
@@ -109,9 +129,12 @@ export function useDeviceStatus({
|
||||
detail.qrCodeUrl ?? deviceDetails.value.qrCodeUrl ?? ""
|
||||
).trim(),
|
||||
};
|
||||
updateDeviceUsageDuration(detail.onlineDuration, deviceId);
|
||||
return detail;
|
||||
} catch (error) {
|
||||
// 实时刷新失败时保留当前页面数据,等待下次通知或页面重新显示。
|
||||
console.log("刷新设备详情失败", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -126,6 +149,10 @@ export function useDeviceStatus({
|
||||
let latestDevice = {
|
||||
...currentDevice,
|
||||
deviceModelName: "",
|
||||
latestVersion: "",
|
||||
needUpdate: false,
|
||||
forceUpdate: false,
|
||||
version: "",
|
||||
bindTime: "",
|
||||
qrCodeUrl: "",
|
||||
};
|
||||
@@ -158,6 +185,7 @@ export function useDeviceStatus({
|
||||
latestDevice.name ||
|
||||
"我的智能弓"
|
||||
);
|
||||
updateDeviceUsageDuration(latestDevice.onlineDuration, latestDevice.deviceId);
|
||||
return;
|
||||
}
|
||||
clearDevice();
|
||||
@@ -177,7 +205,7 @@ export function useDeviceStatus({
|
||||
deviceDetailRequestVersion += 1;
|
||||
deviceDetails.value = {};
|
||||
unbindDialogVisible.value = false;
|
||||
uni.showToast({ title: "解绑成功", icon: "success" });
|
||||
uni.showToast({ title: t('device_unbound'), icon: "success" });
|
||||
} catch (error) {
|
||||
console.error("解绑设备失败", error);
|
||||
if (error?.type === "DEVICE_BIND_INVALID") {
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
import { computed, onUnmounted, ref } from "vue";
|
||||
import { sendHardwareBoxUpdateAPI } from "@/apis";
|
||||
import useStore from "@/store";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
const OTA_PROGRESS_EVENT = "/addons/shoot/otaProgress";
|
||||
const OTA_RESULT_EVENT = "/addons/shoot/otaResult";
|
||||
const UPDATE_TIMEOUT = 10 * 60 * 1000;
|
||||
|
||||
const phaseTextMap = {
|
||||
started: "正在准备固件更新",
|
||||
downloading: "正在下载固件",
|
||||
installing: "正在安装固件",
|
||||
};
|
||||
|
||||
export const useOtaUpdate = () => {
|
||||
const store = useStore();
|
||||
const { device } = storeToRefs(store);
|
||||
const updating = ref(false);
|
||||
const progress = ref(0);
|
||||
const phase = ref("started");
|
||||
const resultVisible = ref(false);
|
||||
const resultStatus = ref("");
|
||||
const resultReason = ref("");
|
||||
|
||||
let updateRunId = 0;
|
||||
let targetVersion = "";
|
||||
let timeoutTimer = null;
|
||||
let successCallback = null;
|
||||
let socketListening = false;
|
||||
|
||||
const phaseText = computed(
|
||||
() => phaseTextMap[phase.value] || "正在进行固件更新"
|
||||
);
|
||||
const resultTitle = computed(() =>
|
||||
resultStatus.value === "success" ? "固件更新完成" : "固件更新失败"
|
||||
);
|
||||
const resultContent = computed(() => {
|
||||
if (resultStatus.value === "success") return "固件更新已完成";
|
||||
return resultReason.value || "更新失败,请检查设备及网络后重试";
|
||||
});
|
||||
|
||||
const clearTimers = () => {
|
||||
clearTimeout(timeoutTimer);
|
||||
timeoutTimer = null;
|
||||
};
|
||||
|
||||
const stopSocketListening = () => {
|
||||
if (!socketListening) return;
|
||||
uni.$off("socket-inbox", handleSocketMessage);
|
||||
socketListening = false;
|
||||
};
|
||||
|
||||
const resetActiveUpdate = () => {
|
||||
clearTimers();
|
||||
stopSocketListening();
|
||||
successCallback = null;
|
||||
};
|
||||
|
||||
const finishUpdate = (status, reason = "", runId = updateRunId) => {
|
||||
if (runId !== updateRunId || !updating.value) return;
|
||||
const onSuccess = successCallback;
|
||||
resetActiveUpdate();
|
||||
updating.value = false;
|
||||
resultStatus.value = status;
|
||||
resultReason.value = reason;
|
||||
if (status === "success") {
|
||||
progress.value = 100;
|
||||
phase.value = "installing";
|
||||
onSuccess?.();
|
||||
}
|
||||
resultVisible.value = true;
|
||||
};
|
||||
|
||||
const isCurrentUpdateMessage = (data) => {
|
||||
const messageDeviceId = String(data?.deviceId || "");
|
||||
const currentDeviceId = String(device.value?.deviceId || "");
|
||||
if (messageDeviceId && currentDeviceId && messageDeviceId !== currentDeviceId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const messageVersion = String(data?.versionNumber || "");
|
||||
return !messageVersion || !targetVersion || messageVersion === targetVersion;
|
||||
};
|
||||
|
||||
function handleSocketMessage(message) {
|
||||
if (!updating.value || Number(message?.code ?? 0) !== 0) return;
|
||||
if (![OTA_PROGRESS_EVENT, OTA_RESULT_EVENT].includes(message?.event)) return;
|
||||
if (!isCurrentUpdateMessage(message.data)) return;
|
||||
|
||||
if (message.event === OTA_PROGRESS_EVENT) {
|
||||
const nextProgress = Math.min(
|
||||
100,
|
||||
Math.max(0, Number(message.data?.progress) || 0)
|
||||
);
|
||||
progress.value = Math.max(progress.value, nextProgress);
|
||||
if (phaseTextMap[message.data?.phase]) {
|
||||
phase.value = message.data.phase;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.data?.status === "success") {
|
||||
finishUpdate("success");
|
||||
return;
|
||||
}
|
||||
if (message.data?.status === "failed") {
|
||||
finishUpdate("failed", message.data?.reason || "");
|
||||
}
|
||||
}
|
||||
|
||||
const startSocketListening = () => {
|
||||
if (socketListening) return;
|
||||
uni.$on("socket-inbox", handleSocketMessage);
|
||||
socketListening = true;
|
||||
};
|
||||
|
||||
const startUpdate = async ({
|
||||
versionNumber,
|
||||
resourceUrl,
|
||||
wifiSsid = "",
|
||||
wifiPassword = "",
|
||||
onSuccess,
|
||||
}) => {
|
||||
if (updating.value) return false;
|
||||
|
||||
updateRunId += 1;
|
||||
const runId = updateRunId;
|
||||
targetVersion = String(versionNumber || "");
|
||||
successCallback = onSuccess || null;
|
||||
resultVisible.value = false;
|
||||
resultStatus.value = "";
|
||||
resultReason.value = "";
|
||||
progress.value = 0;
|
||||
phase.value = "started";
|
||||
updating.value = true;
|
||||
startSocketListening();
|
||||
timeoutTimer = setTimeout(() => {
|
||||
finishUpdate("failed", "固件更新超时,请稍后重试", runId);
|
||||
}, UPDATE_TIMEOUT);
|
||||
|
||||
try {
|
||||
const updateResult = await sendHardwareBoxUpdateAPI({
|
||||
versionNumber: targetVersion,
|
||||
wifiSsid,
|
||||
wifiPassword,
|
||||
resourceUrl,
|
||||
});
|
||||
if (runId !== updateRunId || !updating.value) return false;
|
||||
if (!updateResult?.taskId) {
|
||||
finishUpdate("failed", "固件更新任务创建失败,请稍后重试", runId);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (runId === updateRunId) {
|
||||
finishUpdate(
|
||||
"failed",
|
||||
error?.message || "固件更新请求失败,请稍后重试",
|
||||
runId
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const closeResult = () => {
|
||||
resultVisible.value = false;
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
updateRunId += 1;
|
||||
resetActiveUpdate();
|
||||
});
|
||||
|
||||
return {
|
||||
updating,
|
||||
progress,
|
||||
phase,
|
||||
phaseText,
|
||||
resultVisible,
|
||||
resultStatus,
|
||||
resultTitle,
|
||||
resultContent,
|
||||
startUpdate,
|
||||
closeResult,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,156 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import Container from "@/components/Container.vue";
|
||||
|
||||
// 结果页从设备页进入,返回时优先保留原页面状态。
|
||||
const goBackToDevicePage = () => {
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 1) {
|
||||
uni.navigateBack({ delta: 1 });
|
||||
return;
|
||||
}
|
||||
uni.redirectTo({ url: "/pages/device/my-device" });
|
||||
};
|
||||
|
||||
// 回到设备页后复用其扫码绑定流程。
|
||||
const retryScan = () => {
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 1) {
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
success: () => uni.$emit("device-bind-retry-scan"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
uni.redirectTo({ url: "/pages/device/my-device?retryScan=1" });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="device-already-bound-page">
|
||||
<Container
|
||||
:bgType="12"
|
||||
bgColor="transparent"
|
||||
:onBack="goBackToDevicePage"
|
||||
headerClass="already-bound-header"
|
||||
:scroll="false"
|
||||
:usePageScroll="true"
|
||||
>
|
||||
<view class="already-bound-page">
|
||||
<view class="already-bound-scene">
|
||||
<view class="already-bound-hero-wrap">
|
||||
<view class="already-bound-hero-shadow"></view>
|
||||
<image
|
||||
class="already-bound-hero"
|
||||
src="/static/device-assets/device-already-bound-hero.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
<view class="already-bound-message">
|
||||
<text>{{ t('this_device_is_already_bound_to_another_account') }}</text>
|
||||
<text>{{ t('unbind_it_first_to_bind_it_to_this_account') }}</text>
|
||||
</view>
|
||||
<view class="already-bound-retry" @click="$clickSound(retryScan)">
|
||||
<text>{{ t('scan_again') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</Container>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.device-already-bound-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.already-bound-header {
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* 与现有绑定结果页保持相同的 375 宽画布尺寸。 */
|
||||
.already-bound-page {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.already-bound-scene {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.already-bound-hero-wrap,
|
||||
.already-bound-message,
|
||||
.already-bound-retry {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.already-bound-hero-wrap {
|
||||
top: 568rpx;
|
||||
left: 244rpx;
|
||||
width: 260rpx;
|
||||
height: 222rpx;
|
||||
}
|
||||
|
||||
.already-bound-hero-shadow {
|
||||
position: absolute;
|
||||
left: 22rpx;
|
||||
bottom: 0;
|
||||
width: 238rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.already-bound-hero {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 260rpx;
|
||||
height: 222rpx;
|
||||
}
|
||||
|
||||
.already-bound-message {
|
||||
top: 804rpx;
|
||||
left: 110rpx;
|
||||
display: flex;
|
||||
width: 530rpx;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
font-family: PingFang SC-Regular;
|
||||
font-size: 28rpx;
|
||||
line-height: 40rpx;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.already-bound-retry {
|
||||
top: 930rpx;
|
||||
left: 195rpx;
|
||||
display: flex;
|
||||
width: 360rpx;
|
||||
height: 70rpx;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2rpx solid #ffd947;
|
||||
border-radius: 78rpx;
|
||||
color: #ffd947;
|
||||
font-size: 26rpx;
|
||||
line-height: 26rpx;
|
||||
pointer-events: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import Container from "@/components/Container.vue";
|
||||
|
||||
// 失败页由设备页进入,返回时优先回到上一页;页面栈异常时兜底回到设备页。
|
||||
@@ -41,16 +43,16 @@ const retryScan = () => {
|
||||
<view class="bind-failure-hero-shadow"></view>
|
||||
<image
|
||||
class="bind-failure-hero"
|
||||
src="../../static/device-assets/device-bind-failure-hero.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/device-assets/device-bind-failure-hero.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
<view class="bind-failure-message">
|
||||
<text>二维码不正确,</text>
|
||||
<text>仅支持扫描射灵智能弓箭的设备二维码。</text>
|
||||
<text>{{ t('invalid_qr_code') }}</text>
|
||||
<text>{{ t('only_sheling_smart_bow_device_qr_codes_are_supported') }}</text>
|
||||
</view>
|
||||
<view class="bind-failure-retry" @click="$clickSound(retryScan)">
|
||||
<text>重新扫码</text>
|
||||
<text>{{ t('scan_again') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import Container from "@/components/Container.vue";
|
||||
@@ -11,8 +13,8 @@ const bindResult = ref({
|
||||
|
||||
const bindSuccessHero = computed(() =>
|
||||
bindResult.value.isFirstBind
|
||||
? "../../static/device-assets/device-bind-success-hero-first.png"
|
||||
: "../../static/device-assets/device-bind-success-hero-nonfirst.png"
|
||||
? "https://static.shelingxingqiu.com/shootmini/static/device-assets/device-bind-success-hero-first.png"
|
||||
: "https://static.shelingxingqiu.com/shootmini/static/device-assets/device-bind-success-hero-nonfirst.png"
|
||||
);
|
||||
const bindRewardExpireDate = computed(() => bindResult.value.expireDate);
|
||||
|
||||
@@ -93,29 +95,26 @@ onLoad((options = {}) => {
|
||||
>
|
||||
<image
|
||||
class="bind-success-confetti"
|
||||
src="../../static/device-assets/device-bind-success-confetti.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/device-assets/device-bind-success-confetti.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<image class="bind-success-hero" :src="bindSuccessHero" mode="aspectFit" />
|
||||
<image
|
||||
class="bind-success-title-image"
|
||||
src="../../static/device-assets/device-bind-success-title.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/device-assets/device-bind-success-title.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view v-if="bindResult.isFirstBind" class="bind-reward-card">
|
||||
<image
|
||||
class="bind-reward-gift"
|
||||
src="../../static/device-assets/device-bind-success-reward-gift.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/device-assets/device-bind-success-reward-gift.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="bind-reward-text">
|
||||
新设备首次绑定礼包:
|
||||
<text class="bind-reward-highlight">6个月射灵会员</text>
|
||||
已自动发放至本账号<text v-if="bindRewardExpireDate">,有效期{{ bindRewardExpireDate }}</text>。
|
||||
<text class="bind-reward-text">{{ t('first_time_device_binding_gift') }}<text class="bind-reward-highlight">{{ t('6_months_of_sheling_membership') }}</text>{{ t('added_to_this_account_automatically') }}<text v-if="bindRewardExpireDate">{{ t('valid_until') }}{{ bindRewardExpireDate }}</text>。
|
||||
</text>
|
||||
</view>
|
||||
<view class="bind-success-tutorial" @click="$clickSound(toFirstTryPage)">
|
||||
<text>立即查看新手教程</text>
|
||||
<text>{{ t('view_beginner_tutorial') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -38,7 +38,7 @@ const onScrollView = (e) => {
|
||||
mode="widthFix"
|
||||
/>
|
||||
<navigator open-type="navigateBack">
|
||||
<image class="header-back" src="../../static/back.png" mode="widthFix" />
|
||||
<image class="header-back" src="https://static.shelingxingqiu.com/shootmini/static/back.png" mode="widthFix" />
|
||||
</navigator>
|
||||
<text
|
||||
:style="{ opacity: addBg ? 1 : 0, color: '#fff', fontWeight: 'bold' }"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { t } from "@/locales";
|
||||
|
||||
import { ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import Container from "@/components/Container.vue";
|
||||
@@ -53,9 +55,9 @@ const saveQrCode = async () => {
|
||||
uni.saveImageToPhotosAlbum({ success: resolve, fail: reject, filePath });
|
||||
});
|
||||
qrSaved.value = true;
|
||||
uni.showToast({ title: "已保存至相册", icon: "success" });
|
||||
uni.showToast({ title: t('saved_to_photos'), icon: "success" });
|
||||
} catch (error) {
|
||||
uni.showToast({ title: "请长按二维码保存", icon: "none" });
|
||||
uni.showToast({ title: t('press_and_hold_the_qr_code_to_save_it'), icon: "none" });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -75,19 +77,17 @@ onLoad((options = {}) => {
|
||||
<view class="qr-corner qr-corner--top"></view>
|
||||
<view class="qr-corner qr-corner--bottom"></view>
|
||||
<view class="qr-canvas">
|
||||
<text v-if="loading" class="qr-empty">二维码加载中...</text>
|
||||
<text v-if="loading" class="qr-empty">{{ t('loading_qr_code') }}</text>
|
||||
<template v-else-if="qrImageUrl">
|
||||
<image class="qr-image" :src="qrImageUrl" mode="aspectFit" show-menu-by-longpress />
|
||||
<text v-if="qrSaved" class="qr-device-id">设备ID:{{ deviceId }}</text>
|
||||
<text v-if="qrSaved" class="qr-device-id">{{ t('device_id') }}{{ deviceId }}</text>
|
||||
<view v-else class="qr-save-button" @click="saveQrCode">
|
||||
<text>保存至相册</text>
|
||||
<text>{{ t('save_to_photos') }}</text>
|
||||
</view>
|
||||
<text class="qr-description">
|
||||
该二维码为当前绑定弓箭的二维码,你可以截图保存到相册,以便当二维码丢失或不在身边时,可以扫描二维码进行设备绑定。
|
||||
</text>
|
||||
<text class="qr-note">注:解除绑定后将无法查看该二维码。</text>
|
||||
<text class="qr-description">{{ t('this_is_the_qr_code_for_your_bound_bow_save_a_screenshot_to_phot') }}</text>
|
||||
<text class="qr-note">{{ t('note_this_qr_code_will_no_longer_be_available_after_unbinding') }}</text>
|
||||
</template>
|
||||
<text v-else class="qr-empty">暂无设备二维码</text>
|
||||
<text v-else class="qr-empty">{{ t('no_device_qr_code_available') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</Container>
|
||||
|
||||
+172
-189
@@ -1,13 +1,15 @@
|
||||
<script setup>
|
||||
import { t, translateStatic } from "@/locales";
|
||||
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import Container from "@/components/Container.vue";
|
||||
import DeviceOnlineEffects from "@/components/DeviceOnlineEffects.vue";
|
||||
import Header from "@/components/Header.vue";
|
||||
import ScreenHint from "@/components/ScreenHint.vue";
|
||||
import ModalDialog from "@/components/ModalDialog.vue";
|
||||
import OtaModal from "@/components/OtaModal.vue";
|
||||
import {
|
||||
getHardwareBoxVersionAPI,
|
||||
laserAimAPI,
|
||||
updateDeviceAliasAPI,
|
||||
} from "@/apis";
|
||||
@@ -18,11 +20,11 @@ import {
|
||||
DEVICE_NAME_STORAGE_KEY,
|
||||
useDeviceStatus,
|
||||
} from "./composables/useDeviceStatus";
|
||||
import { useOtaUpdate } from "./composables/useOtaUpdate";
|
||||
import { getOtaInfoFromDetail, useOtaUpdate } from "@/composables/useOtaUpdate";
|
||||
|
||||
const store = useStore();
|
||||
const { updateDevice, clearDevice } = store;
|
||||
const { user, device, deviceStatus, online } = storeToRefs(store);
|
||||
const { updateDevice, updateDeviceUsageDuration, clearDevice } = store;
|
||||
const { user, device, deviceStatus, deviceUsageDuration, online } = storeToRefs(store);
|
||||
const DEVICE_NAME_MAX_LENGTH = 10;
|
||||
|
||||
const formatBindingDate = (value) => {
|
||||
@@ -61,11 +63,8 @@ const showDeviceId = ref(false);
|
||||
const latestVersionDialogVisible = ref(false);
|
||||
const firmwareConfirmVisible = ref(false);
|
||||
const wifiRequiredVisible = ref(false);
|
||||
const otaNeedUpdate = ref(false);
|
||||
const firmwareActionPending = ref(false);
|
||||
const pendingOtaInfo = ref(null);
|
||||
let otaCheckPromise = null;
|
||||
let otaCheckRequestVersion = 0;
|
||||
|
||||
const {
|
||||
updating: otaUpdating,
|
||||
@@ -96,8 +95,10 @@ const {
|
||||
user,
|
||||
device,
|
||||
deviceStatus,
|
||||
deviceUsageDuration,
|
||||
online,
|
||||
updateDevice,
|
||||
updateDeviceUsageDuration,
|
||||
clearDevice,
|
||||
unbindDialogVisible,
|
||||
});
|
||||
@@ -110,6 +111,28 @@ const wifiStatusText = computed(() => {
|
||||
const bindingDateText = computed(() =>
|
||||
formatBindingDate(deviceDetails.value.bindTime)
|
||||
);
|
||||
const otaNeedUpdate = computed(() => {
|
||||
const info = getOtaInfoFromDetail(deviceDetails.value);
|
||||
return isDeviceOnline.value &&
|
||||
deviceDetails.value.deviceId === device.value.deviceId &&
|
||||
info.needUpdate && !!info.versionNumber;
|
||||
});
|
||||
|
||||
// 固件版本统一以大写 V 开头,避免接口返回格式不一致影响弹窗展示。
|
||||
const formatFirmwareVersion = (value) => {
|
||||
const version = String(value ?? "").trim();
|
||||
if (!version) return "";
|
||||
return /^v/i.test(version) ? `V${version.slice(1)}` : `V${version}`;
|
||||
};
|
||||
|
||||
const firmwareConfirmContent = computed(() => {
|
||||
const currentVersion = formatFirmwareVersion(pendingOtaInfo.value?.currentVersion);
|
||||
const latestVersion = formatFirmwareVersion(pendingOtaInfo.value?.versionNumber);
|
||||
if (currentVersion && latestVersion) {
|
||||
return t('current_firmware_value_new_firmware_value_update_now', { p0: currentVersion, p1: latestVersion });
|
||||
}
|
||||
return latestVersion ? t('new_firmware_value_is_available_update_now', { p0: latestVersion }) : t('update_firmware_now');
|
||||
});
|
||||
|
||||
const deviceIdText = computed(() =>
|
||||
showDeviceId.value && device.value.deviceName
|
||||
@@ -120,23 +143,23 @@ const deviceIdText = computed(() =>
|
||||
// 设备型号直接读取详情接口,缺失时显示占位符。
|
||||
const designDeviceStats = computed(() => [
|
||||
{
|
||||
label: "设备型号",
|
||||
label: t('model'),
|
||||
value: deviceDetails.value.deviceModelName?.trim() || "--",
|
||||
},
|
||||
{
|
||||
label: "剩余电量",
|
||||
label: t('battery_468'),
|
||||
value: !isDeviceOnline.value
|
||||
? "设备离线"
|
||||
? t('device_offline')
|
||||
: batteryText.value === "暂无数据"
|
||||
? "--"
|
||||
: batteryText.value,
|
||||
},
|
||||
{
|
||||
label: "累计使用",
|
||||
label: t('total_usage'),
|
||||
value: onlineDurationText.value,
|
||||
},
|
||||
{
|
||||
label: "绑定时间",
|
||||
label: t('bound_on'),
|
||||
value: bindingDateText.value,
|
||||
},
|
||||
]);
|
||||
@@ -187,16 +210,16 @@ const confirmName = async () => {
|
||||
|
||||
const name = String(editingName.value || "").trim();
|
||||
if (!name) {
|
||||
uni.showToast({ title: "请输入设备名", icon: "none" });
|
||||
uni.showToast({ title: t('please_enter_a_device_name'), icon: "none" });
|
||||
return;
|
||||
}
|
||||
if (Array.from(name).length > DEVICE_NAME_MAX_LENGTH) {
|
||||
uni.showToast({ title: "设备名最多支持10个字符", icon: "none" });
|
||||
uni.showToast({ title: t('device_names_can_contain_up_to_10_characters'), icon: "none" });
|
||||
return;
|
||||
}
|
||||
if (!/^[\u4e00-\u9fa5A-Za-z0-9_-]+$/.test(name)) {
|
||||
uni.showToast({
|
||||
title: "仅支持中文、英文、数字、下划线和减号",
|
||||
title: t('only_chinese_and_english_letters_numbers_underscores_and_hyphens'),
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
@@ -204,7 +227,7 @@ const confirmName = async () => {
|
||||
|
||||
const deviceId = device.value.deviceId;
|
||||
if (!deviceId) {
|
||||
uni.showToast({ title: "暂无绑定设备", icon: "none" });
|
||||
uni.showToast({ title: t('no_device_bound'), icon: "none" });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -226,7 +249,7 @@ const confirmName = async () => {
|
||||
uni.setStorageSync(DEVICE_NAME_STORAGE_KEY, nameOverrides);
|
||||
|
||||
nameEditorVisible.value = false;
|
||||
uni.showToast({ title: "设备名已更新", icon: "success" });
|
||||
uni.showToast({ title: t('device_name_updated'), icon: "success" });
|
||||
void refreshDeviceDetails();
|
||||
} catch (error) {
|
||||
console.error("修改设备名失败", error);
|
||||
@@ -241,97 +264,33 @@ const toDeviceIntroPage = () => {
|
||||
|
||||
const joinWifi = () => {
|
||||
if (!isDeviceOnline.value) {
|
||||
uni.showToast({ title: "请先开启智能弓", icon: "none" });
|
||||
uni.showToast({ title: t('please_turn_on_your_smart_bow_first'), icon: "none" });
|
||||
return;
|
||||
}
|
||||
uni.navigateTo({ url: "/pages/device/ota-wifi" });
|
||||
};
|
||||
|
||||
const clearOtaState = () => {
|
||||
otaCheckRequestVersion += 1;
|
||||
otaCheckPromise = null;
|
||||
otaNeedUpdate.value = false;
|
||||
};
|
||||
|
||||
const loadOtaVersionInfo = async () => {
|
||||
if (otaCheckPromise) return otaCheckPromise;
|
||||
|
||||
const deviceId = device.value.deviceId;
|
||||
if (!deviceId || !isDeviceOnline.value) return null;
|
||||
|
||||
const requestVersion = ++otaCheckRequestVersion;
|
||||
const request = getHardwareBoxVersionAPI()
|
||||
.then((versionInfo) => {
|
||||
if (
|
||||
requestVersion !== otaCheckRequestVersion ||
|
||||
device.value.deviceId !== deviceId ||
|
||||
!isDeviceOnline.value
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!versionInfo || typeof versionInfo !== "object") {
|
||||
throw new Error("固件版本信息为空");
|
||||
}
|
||||
|
||||
const needUpdate =
|
||||
versionInfo.needUpdate === true || Number(versionInfo.needUpdate) === 1;
|
||||
otaNeedUpdate.value = needUpdate;
|
||||
return {
|
||||
versionNumber: versionInfo.versionNumber || "",
|
||||
resourceUrl: versionInfo.resourceUrl || "",
|
||||
needUpdate,
|
||||
};
|
||||
})
|
||||
.finally(() => {
|
||||
if (otaCheckPromise === request) {
|
||||
otaCheckPromise = null;
|
||||
}
|
||||
});
|
||||
|
||||
otaCheckPromise = request;
|
||||
return request;
|
||||
};
|
||||
|
||||
const refreshOtaUpdateState = async () => {
|
||||
otaNeedUpdate.value = false;
|
||||
if (!device.value.deviceId || !isDeviceOnline.value) {
|
||||
clearOtaState();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await loadOtaVersionInfo();
|
||||
} catch (error) {
|
||||
clearOtaState();
|
||||
console.log("检查固件更新失败", error);
|
||||
}
|
||||
};
|
||||
|
||||
const closeLatestVersionDialog = () => {
|
||||
latestVersionDialogVisible.value = false;
|
||||
};
|
||||
|
||||
const closeFirmwareConfirm = () => {
|
||||
firmwareConfirmVisible.value = false;
|
||||
};
|
||||
|
||||
const closeLatestVersionDialog = () => {
|
||||
latestVersionDialogVisible.value = false;
|
||||
};
|
||||
|
||||
const runFirmwareUpdate = () => {
|
||||
const versionInfo = pendingOtaInfo.value;
|
||||
if (!versionInfo) return;
|
||||
const otaInfo = pendingOtaInfo.value;
|
||||
if (!otaInfo || otaInfo.deviceId !== device.value.deviceId) return;
|
||||
void startOtaUpdate({
|
||||
versionNumber: versionInfo.versionNumber,
|
||||
resourceUrl: versionInfo.resourceUrl,
|
||||
onSuccess: () => {
|
||||
otaNeedUpdate.value = false;
|
||||
},
|
||||
versionNumber: otaInfo.versionNumber,
|
||||
onSuccess: () => void refreshDeviceDetails(),
|
||||
});
|
||||
};
|
||||
|
||||
const confirmFirmwareUpdate = () => {
|
||||
firmwareConfirmVisible.value = false;
|
||||
if (!isDeviceOnline.value) {
|
||||
uni.showToast({ title: "请先开启智能弓", icon: "none" });
|
||||
uni.showToast({ title: t('please_turn_on_your_smart_bow_first'), icon: "none" });
|
||||
return;
|
||||
}
|
||||
if (String(networkType.value || "").toLowerCase() === "wifi") {
|
||||
@@ -342,42 +301,43 @@ const confirmFirmwareUpdate = () => {
|
||||
};
|
||||
|
||||
const goWifiForFirmwareUpdate = () => {
|
||||
const versionInfo = pendingOtaInfo.value;
|
||||
if (!versionInfo) return;
|
||||
const otaInfo = pendingOtaInfo.value;
|
||||
if (!otaInfo || otaInfo.deviceId !== device.value.deviceId) return;
|
||||
wifiRequiredVisible.value = false;
|
||||
const query = [
|
||||
"source=firmware-update",
|
||||
`versionNumber=${encodeURIComponent(versionInfo.versionNumber)}`,
|
||||
`resourceUrl=${encodeURIComponent(versionInfo.resourceUrl)}`,
|
||||
].join("&");
|
||||
uni.navigateTo({ url: `/pages/device/ota-wifi?${query}` });
|
||||
const versionNumber = encodeURIComponent(otaInfo.versionNumber);
|
||||
uni.navigateTo({ url: `/pages/device/ota-wifi?source=firmware-update&versionNumber=${versionNumber}` });
|
||||
};
|
||||
|
||||
const handleOtaResultClose = () => {
|
||||
closeOtaResult();
|
||||
void refreshOtaUpdateState();
|
||||
void refreshDeviceDetails();
|
||||
};
|
||||
|
||||
const goFirmwareUpdate = async () => {
|
||||
if (firmwareActionPending.value) return;
|
||||
if (firmwareActionPending.value || otaUpdating.value) return;
|
||||
if (!isDeviceOnline.value) {
|
||||
uni.showToast({ title: "请先开启智能弓", icon: "none" });
|
||||
uni.showToast({ title: t('please_turn_on_your_smart_bow_first'), icon: "none" });
|
||||
return;
|
||||
}
|
||||
|
||||
firmwareActionPending.value = true;
|
||||
const deviceId = device.value.deviceId;
|
||||
try {
|
||||
const versionInfo = await loadOtaVersionInfo();
|
||||
if (!versionInfo) return;
|
||||
if (!versionInfo.needUpdate) {
|
||||
const detail = await refreshDeviceDetails();
|
||||
if (!detail || device.value.deviceId !== deviceId) {
|
||||
uni.showToast({ title: t('could_not_load_device_details_please_retry'), icon: "none" });
|
||||
return;
|
||||
}
|
||||
const otaInfo = getOtaInfoFromDetail(detail);
|
||||
if (!otaInfo.needUpdate) {
|
||||
latestVersionDialogVisible.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
pendingOtaInfo.value = versionInfo;
|
||||
if (!otaInfo.versionNumber) {
|
||||
uni.showToast({ title: t('target_firmware_version_is_missing_please_retry'), icon: "none" });
|
||||
return;
|
||||
}
|
||||
pendingOtaInfo.value = { ...otaInfo, deviceId };
|
||||
firmwareConfirmVisible.value = true;
|
||||
} catch (error) {
|
||||
uni.showToast({ title: "获取更新版本失败,请重试", icon: "none" });
|
||||
} finally {
|
||||
firmwareActionPending.value = false;
|
||||
}
|
||||
@@ -385,27 +345,27 @@ const goFirmwareUpdate = async () => {
|
||||
|
||||
const goCalibration = async () => {
|
||||
if (!isDeviceOnline.value) {
|
||||
uni.showToast({ title: "请先开启智能弓", icon: "none" });
|
||||
uni.showToast({ title: t('please_turn_on_your_smart_bow_first'), icon: "none" });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await laserAimAPI();
|
||||
uni.navigateTo({ url: "/pages/device/calibration" });
|
||||
} catch (error) {
|
||||
uni.showToast({ title: "设备未连接,暂时无法调瞄", icon: "none" });
|
||||
uni.showToast({ title: t('device_disconnected_sight_adjustment_is_unavailable'), icon: "none" });
|
||||
}
|
||||
};
|
||||
|
||||
const copyEmail = () => {
|
||||
uni.setClipboardData({
|
||||
data: "shelingxingqiu@163.com",
|
||||
success: () => uni.showToast({ title: "邮箱已复制", icon: "success" }),
|
||||
success: () => uni.showToast({ title: t('email_address_copied'), icon: "success" }),
|
||||
});
|
||||
};
|
||||
|
||||
const openQr = () => {
|
||||
if (!device.value.deviceId) {
|
||||
uni.showToast({ title: "暂无绑定设备", icon: "none" });
|
||||
uni.showToast({ title: t('no_device_bound'), icon: "none" });
|
||||
return;
|
||||
}
|
||||
uni.navigateTo({
|
||||
@@ -426,15 +386,20 @@ watch(
|
||||
(isOnline, wasOnline) => {
|
||||
if (isOnline && !wasOnline) {
|
||||
void refreshDeviceDetails();
|
||||
void refreshOtaUpdateState();
|
||||
return;
|
||||
}
|
||||
if (!isOnline) {
|
||||
clearOtaState();
|
||||
} else if (!isOnline) {
|
||||
firmwareConfirmVisible.value = false;
|
||||
wifiRequiredVisible.value = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(() => device.value.deviceId, () => {
|
||||
pendingOtaInfo.value = null;
|
||||
latestVersionDialogVisible.value = false;
|
||||
firmwareConfirmVisible.value = false;
|
||||
wifiRequiredVisible.value = false;
|
||||
});
|
||||
|
||||
onLoad((options = {}) => {
|
||||
retryScanOnShow.value = options.retryScan === "1";
|
||||
});
|
||||
@@ -450,7 +415,6 @@ onUnmounted(() => {
|
||||
onShow(async () => {
|
||||
calibration.value = uni.getStorageSync("calibration");
|
||||
await syncDeviceBinding();
|
||||
void refreshOtaUpdateState();
|
||||
if (retryScanOnShow.value) {
|
||||
retryScanOnShow.value = false;
|
||||
handleScan();
|
||||
@@ -468,7 +432,7 @@ onShow(async () => {
|
||||
<template #header>
|
||||
<view class="device-nav">
|
||||
<Header title="" />
|
||||
<text class="device-nav-title">我的设备</text>
|
||||
<text class="device-nav-title">{{ t('my_device') }}</text>
|
||||
<view v-if="device.deviceId" class="device-status" :class="statusClass">
|
||||
<view class="status-dot"></view>
|
||||
<text>{{ statusText }}</text>
|
||||
@@ -478,24 +442,24 @@ onShow(async () => {
|
||||
<view v-if="!device.deviceId" class="scan-code">
|
||||
<view class="unbound-content">
|
||||
<button class="scan-entry" hover-class="none" @click="$clickSound(handleScan)">
|
||||
<image src="../../static/device-assets/my-device-unbound-scan.png" mode="aspectFit" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/device-assets/my-device-unbound-scan.png" mode="aspectFit" />
|
||||
</button>
|
||||
<view class="scan-title">
|
||||
<text>请扫描</text>
|
||||
<text class="highlight-text">射灵智能弓箭</text>
|
||||
<text>设备上的二维码</text>
|
||||
<text>{{ t('scan_the') }}</text>
|
||||
<text class="highlight-text">{{ t('sheling_smart_bow_484') }}</text>
|
||||
<text>{{ t('qr_code_on_your_device') }}</text>
|
||||
</view>
|
||||
<view class="benefit-copy">
|
||||
<!-- <view class="benefit-copy">
|
||||
<text>新设备首次绑定账号可获赠</text>
|
||||
<text class="highlight-text">6个月射灵会员礼包</text>
|
||||
<text>,</text>
|
||||
<text>该礼包仅可使用一次,请确保当前登录账号为您本人账号。</text>
|
||||
</view>
|
||||
</view> -->
|
||||
<button class="help-link" hover-class="none" @click="showTip = true">
|
||||
<text>没找到二维码或遇到问题></text>
|
||||
<text>{{ t('can_t_find_the_qr_code_or_need_help') }}</text>
|
||||
</button>
|
||||
<button class="product-link" hover-class="none" @click="toDeviceIntroPage">
|
||||
<text>还没有射灵智能弓箭?点我获取></text>
|
||||
<text>{{ t('don_t_have_a_sheling_smart_bow_yet_get_one') }}</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -507,13 +471,20 @@ onShow(async () => {
|
||||
>
|
||||
<image
|
||||
class="device-stage"
|
||||
src="../../static/home-device/device-platform.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/home-device/device-platform.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<DeviceOnlineEffects
|
||||
v-if="isDeviceOnline"
|
||||
variant="detail"
|
||||
/>
|
||||
<image
|
||||
class="device-bow"
|
||||
src="../../static/home-device/device-bow.png"
|
||||
mode="widthFix"
|
||||
:class="{ 'device-bow--online': isDeviceOnline }"
|
||||
:src="isDeviceOnline
|
||||
? 'https://static.shelingxingqiu.com/shootmini/static/home-device/device-bow.png'
|
||||
: 'https://static.shelingxingqiu.com/shootmini/static/home-device/device-bow.png'"
|
||||
:mode="isDeviceOnline ? 'aspectFit' : 'widthFix'"
|
||||
/>
|
||||
</view>
|
||||
|
||||
@@ -522,19 +493,19 @@ onShow(async () => {
|
||||
:class="{ 'device-float-group': isDeviceOnline }"
|
||||
>
|
||||
<view class="device-title-row">
|
||||
<text class="device-name">{{ device.deviceName || "打弓佬" }}</text>
|
||||
<text class="device-name">{{ device.deviceName || t('archery_fan') }}</text>
|
||||
<view class="edit-name" @click="openNameEditor">
|
||||
<image src="../../static/home-device/device-edit.png" mode="aspectFit" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/home-device/device-edit.png" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="device-id-row">
|
||||
<text>设备ID:{{ deviceIdText }}</text>
|
||||
<text>{{ t('device_id') }}{{ deviceIdText }}</text>
|
||||
<view class="device-id-toggle" @click="toggleDeviceId">
|
||||
<image
|
||||
class="device-id-eye"
|
||||
:src="showDeviceId
|
||||
? '../../static/home-device/device-id-visible.png'
|
||||
: '../../static/home-device/device-id-hidden.png'"
|
||||
? 'https://static.shelingxingqiu.com/shootmini/static/home-device/device-id-visible.png'
|
||||
: 'https://static.shelingxingqiu.com/shootmini/static/home-device/device-id-hidden.png'"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
@@ -559,34 +530,31 @@ onShow(async () => {
|
||||
<view class="device-actions">
|
||||
<view class="action-item" @click="toDeviceIntroPage">
|
||||
<view class="action-icon">
|
||||
<image src="../../static/home-device/device-action-intro.png" mode="aspectFit" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/home-device/device-action-intro.png" mode="aspectFit" />
|
||||
</view>
|
||||
<text>设备介绍</text>
|
||||
<text>{{ t('about_the_device') }}</text>
|
||||
</view>
|
||||
<view class="action-item" @click="goCalibration">
|
||||
<view class="action-icon">
|
||||
<image src="../../static/home-device/device-action-calibration.png" mode="aspectFit" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/home-device/device-action-calibration.png" mode="aspectFit" />
|
||||
</view>
|
||||
<text>弓箭调瞄</text>
|
||||
<text>{{ t('sight_adjustment') }}</text>
|
||||
</view>
|
||||
<view class="action-item" @click="goFirmwareUpdate">
|
||||
<view class="action-icon">
|
||||
<image src="../../static/home-device/device-action-firmware.png" mode="aspectFit" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/home-device/device-action-firmware.png" mode="aspectFit" />
|
||||
</view>
|
||||
<view class="action-label-wrap">
|
||||
<text>固件更新</text>
|
||||
<text
|
||||
v-if="otaNeedUpdate"
|
||||
class="action-badge action-badge--new"
|
||||
>New</text>
|
||||
<text>{{ t('firmware_update') }}</text>
|
||||
<text v-if="otaNeedUpdate" class="action-badge action-badge--new">New</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="action-item" @click="joinWifi">
|
||||
<view class="action-icon">
|
||||
<image src="../../static/home-device/device-action-wifi.png" mode="aspectFit" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/home-device/device-action-wifi.png" mode="aspectFit" />
|
||||
</view>
|
||||
<view class="action-label-wrap">
|
||||
<text>WIFI设置</text>
|
||||
<text>{{ t('wi_fi_settings') }}</text>
|
||||
<text
|
||||
:class="[
|
||||
'action-badge',
|
||||
@@ -597,30 +565,30 @@ onShow(async () => {
|
||||
: '',
|
||||
]"
|
||||
>
|
||||
{{ wifiStatusText }}
|
||||
{{ translateStatic(wifiStatusText) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="action-item" @click="openQr">
|
||||
<view class="action-icon">
|
||||
<image src="../../static/home-device/device-action-qrcode.png" mode="aspectFit" />
|
||||
<image src="https://static.shelingxingqiu.com/shootmini/static/home-device/device-action-qrcode.png" mode="aspectFit" />
|
||||
</view>
|
||||
<text>设备二维码</text>
|
||||
<text>{{ t('device_qr_code') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="unbind-entry" @click="openUnbindDialog">
|
||||
<image
|
||||
class="unbind-icon"
|
||||
src="../../static/home-device/device-unbind.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/home-device/device-unbind.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>解除绑定</text>
|
||||
<text>{{ t('unbind') }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="calibration" class="calibration-tip">
|
||||
<text>如有场地或距离变化,请重新校准以保证智能弓射箭精准度</text>
|
||||
<view @click="goCalibration">重新校准</view>
|
||||
<text>{{ t('recalibrate_when_your_location_or_distance_changes_to_keep_your_') }}</text>
|
||||
<view @click="goCalibration">{{ t('recalibrate') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -632,71 +600,76 @@ onShow(async () => {
|
||||
:onClose="closeTip"
|
||||
>
|
||||
<view class="scan-tips">
|
||||
<text class="scan-tips-title">扫码绑定射灵弓箭</text>
|
||||
<text class="scan-tips-subtitle">配套令牌样例</text>
|
||||
<text class="scan-tips-title">{{ t('scan_to_bind_your_sheling_bow') }}</text>
|
||||
<text class="scan-tips-subtitle">{{ t('sample_device_tag') }}</text>
|
||||
<image
|
||||
class="scan-tips-qr"
|
||||
src="../../static/device-assets/my-device-unbound-qr-sample.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/device-assets/my-device-unbound-qr-sample.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<text class="scan-tips-note">【注】已被绑定的弓箭无法再次绑定。</text>
|
||||
<text class="scan-tips-note">{{ t('note_a_bow_that_is_already_bound_cannot_be_bound_again') }}</text>
|
||||
<view class="scan-tips-contact">
|
||||
<text>如有任何疑问,请随时联系:</text>
|
||||
<text>{{ t('if_you_have_any_questions_please_contact') }}</text>
|
||||
<button hover-class="none" @click="copyEmail">shelingxingqiu@163.com</button>
|
||||
</view>
|
||||
</view>
|
||||
</ModalDialog>
|
||||
|
||||
<ScreenHint :show="confirmBindTip" :onClose="closeConfirmBindTip">
|
||||
<ScreenHint
|
||||
:show="confirmBindTip"
|
||||
:onClose="closeConfirmBindTip"
|
||||
contentHeight="360rpx"
|
||||
>
|
||||
<view class="confirm-bind">
|
||||
<text>智能弓箭和系统账号需一一对应,你确定要将当前登录用户账号绑定这把弓箭吗?绑定后不可随意更换。</text>
|
||||
<text>{{ t('each_smart_bow_must_be_paired_with_one_account_bind_this_bow_to_') }}</text>
|
||||
<view class="confirm-actions">
|
||||
<view
|
||||
class="primary-action"
|
||||
:class="{ 'primary-action--disabled': binding }"
|
||||
@click="confirmBind"
|
||||
>
|
||||
{{ binding ? "绑定中..." : "确认绑定" }}
|
||||
{{ binding ? t('binding') : t('confirm_binding') }}
|
||||
</view>
|
||||
<view class="secondary-action" @click="confirmBindTip = false">取消</view>
|
||||
<view class="secondary-action" @click="confirmBindTip = false">{{ t('cancel_133') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</ScreenHint>
|
||||
|
||||
<ModalDialog
|
||||
:show="unbindDialogVisible"
|
||||
title="解除设备绑定"
|
||||
content="解除绑定后,将无法使用智能弓箭进行对战,确定继续吗?"
|
||||
cancelText="取消"
|
||||
confirmText="解除绑定"
|
||||
:title="t('unbind_device')"
|
||||
:content="t('after_unbinding_you_cannot_use_this_smart_bow_in_matches_continu')"
|
||||
:cancelText="t('cancel_133')"
|
||||
:confirmText="t('unbind')"
|
||||
:onCancel="closeUnbindDialog"
|
||||
:onConfirm="unbindDevice"
|
||||
></ModalDialog>
|
||||
|
||||
<ModalDialog
|
||||
:show="latestVersionDialogVisible"
|
||||
title="固件更新"
|
||||
content="已经是最新版本"
|
||||
confirmText="确定"
|
||||
:title="t('firmware_update')"
|
||||
:content="t('already_up_to_date')"
|
||||
:confirmText="t('confirm_134')"
|
||||
:showCancel="false"
|
||||
:onConfirm="closeLatestVersionDialog"
|
||||
></ModalDialog>
|
||||
|
||||
<ModalDialog
|
||||
:show="firmwareConfirmVisible"
|
||||
title="固件更新"
|
||||
content="是否现在进行固件更新?"
|
||||
cancelText="取消"
|
||||
confirmText="确定"
|
||||
:title="t('firmware_update')"
|
||||
:content="firmwareConfirmContent"
|
||||
:cancelText="t('later')"
|
||||
:confirmText="t('update_now')"
|
||||
:showCancel="!pendingOtaInfo?.forceUpdate"
|
||||
:onCancel="closeFirmwareConfirm"
|
||||
:onConfirm="confirmFirmwareUpdate"
|
||||
></ModalDialog>
|
||||
|
||||
<ModalDialog
|
||||
:show="wifiRequiredVisible"
|
||||
title="固件更新"
|
||||
content="请在WiFi网络下更新"
|
||||
confirmText="连接WiFi"
|
||||
:title="t('firmware_update')"
|
||||
:content="t('please_update_over_wi_fi')"
|
||||
:confirmText="t('connect_to_wi_fi')"
|
||||
:showCancel="false"
|
||||
:onConfirm="goWifiForFirmwareUpdate"
|
||||
></ModalDialog>
|
||||
@@ -718,7 +691,7 @@ onShow(async () => {
|
||||
:show="otaResultVisible && otaResultStatus === 'failed'"
|
||||
:title="otaResultTitle"
|
||||
:content="otaResultContent"
|
||||
confirmText="关闭"
|
||||
:confirmText="t('close')"
|
||||
:showCancel="false"
|
||||
:onConfirm="handleOtaResultClose"
|
||||
></ModalDialog>
|
||||
@@ -727,7 +700,7 @@ onShow(async () => {
|
||||
<view class="name-panel" @click.stop>
|
||||
<image
|
||||
class="name-mascot"
|
||||
src="../../static/home-device/device-name-mascot.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/home-device/device-name-mascot.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view class="name-sheet">
|
||||
@@ -740,7 +713,7 @@ onShow(async () => {
|
||||
:disabled="renaming"
|
||||
:maxlength="-1"
|
||||
confirm-type="done"
|
||||
placeholder="请输入设备名"
|
||||
:placeholder="t('please_enter_a_device_name')"
|
||||
placeholder-class="name-placeholder"
|
||||
@confirm="confirmName"
|
||||
/>
|
||||
@@ -751,13 +724,13 @@ onShow(async () => {
|
||||
>
|
||||
<image
|
||||
class="name-confirm-image"
|
||||
src="../../static/home-device/device-name-confirm.png"
|
||||
src="https://static.shelingxingqiu.com/shootmini/static/home-device/device-name-confirm.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="name-meta">
|
||||
<text class="name-helper">仅支持中文、英文、数字、下划线、减号</text>
|
||||
<text class="name-helper">{{ t('only_chinese_and_english_letters_numbers_underscores_and_hyphens_509') }}</text>
|
||||
<text
|
||||
class="name-count"
|
||||
:class="{ 'name-count--exceeded': isEditingNameTooLong }"
|
||||
@@ -895,7 +868,7 @@ onShow(async () => {
|
||||
}
|
||||
|
||||
.help-link {
|
||||
margin-top: 72rpx;
|
||||
margin-top: 36rpx;
|
||||
color: #ffd947;
|
||||
font-size: 26rpx;
|
||||
line-height: 36rpx;
|
||||
@@ -903,8 +876,8 @@ onShow(async () => {
|
||||
|
||||
.product-link {
|
||||
position: fixed;
|
||||
bottom: 104rpx;
|
||||
bottom: calc(env(safe-area-inset-bottom) + 104rpx);
|
||||
bottom: 208rpx;
|
||||
/* bottom: calc(env(safe-area-inset-bottom) + 104rpx); */
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
@@ -948,7 +921,7 @@ onShow(async () => {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10rpx);
|
||||
transform: translateY(-20rpx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -956,18 +929,24 @@ onShow(async () => {
|
||||
position: absolute;
|
||||
top: 214rpx;
|
||||
left: 50%;
|
||||
z-index: 1;
|
||||
width: 750rpx;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.device-bow {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
top: -2rpx;
|
||||
left: 50%;
|
||||
z-index: 4;
|
||||
width: 562rpx;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.device-bow--online {
|
||||
height: 466rpx;
|
||||
}
|
||||
|
||||
.device-heading {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
@@ -1305,6 +1284,10 @@ onShow(async () => {
|
||||
}
|
||||
|
||||
.confirm-bind {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding-top: 100rpx;
|
||||
transform: translateY(24rpx);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 26rpx;
|
||||
line-height: 42rpx;
|
||||
|
||||
+459
-344
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user