完成和平精英 无畏契约游戏界面

This commit is contained in:
tangxinyue 2026-07-08 14:40:48 +08:00
parent 1b65430bd2
commit 80ae3c1e1e
12 changed files with 1364 additions and 14 deletions

View File

@ -455,6 +455,22 @@
"navigationStyle": "custom"
}
},
{
"path" : "game/hepingjingying",
"style" :
{
"navigationBarTitleText": "和平精英",
"navigationStyle": "custom"
}
},
{
"path" : "game/wuweiqiyue",
"style" :
{
"navigationBarTitleText": "无畏契约",
"navigationStyle": "custom"
}
},
{
"path": "game/index",
"style": {

View File

@ -0,0 +1,614 @@
<template>
<view class="honor-of-kings">
<nav-bar title="和平精英" bgColor="#F5F5F5" isRightButton :rightButtonText="rightButtonText"
@right-click="onRightClick">
</nav-bar>
<view style="padding: 8px;box-sizing: border-box;">
<view class="painter-container" @click="handlePreview"
:style="`width:calc(100vw - 16px) ; height: ${posterContainerHeight}px; overflow: hidden; position: relative; transform: translateZ(0);`">
<!-- 运用 js 算出的无误差纯数字 scale 来实现高清画板的等比缩小彻底兼容所有老旧内核不再被截断 -->
<view
:style="`width: 822px; transform: scale(${posterScaleRatio}); transform-origin: left top; position: absolute; left: 0; top: 0;`">
<l-painter v-if="bgImage" isCanvasToTempFilePath @success="onPainterSuccess" :css="`width:822px;`">
<l-painter-view :css="`width: 822px; position: relative;`">
<!-- 直接使用主背景图撑开父容器移除不必要的 opacity: 0 占位图避免在大屏渲染时产生黑块 -->
<l-painter-image :src="bgImage" css="width: 822px; display: block;"></l-painter-image>
<!-- 人物图片 -->
<l-painter-image :src="hePingData.imgUrl" :css="characterStyle"
mode="heightFix"></l-painter-image>
<template v-if="hePingData.type == 2">
<!-- 头像 -->
<l-painter-image :src="hePingData.avatar"
css="position:absolute;left:50px;bottom:13px;width:36px;height:36px"></l-painter-image>
<!-- 头像框 -->
<l-painter-image src="/static/image/other/game/heping/touxiang.png"
css="position:absolute;left:0;bottom:0;width:100px;height:60px"></l-painter-image>
<!-- 昵称 -->
<l-painter-text :text="hePingData.nickname"
css="position: absolute; left: 102px; bottom: 15px; font-size: 10px; color: #EFF9FF;"></l-painter-text>
</template>
<!-- 水印 -->
<l-painter-image v-if="$isVip()" src="/static/image/other/shuiying.png"
:css="`position: absolute; left: 18.8px; bottom: 17.1px; width: 145.38px;height:42.76px`"></l-painter-image>
</l-painter-view>
</l-painter>
</view>
</view>
</view>
<!-- 水印 -->
<view v-if="$isVip()">
<liu-drag-button :canDocking="false" @clickBtn="$goRechargePage('watermark', 'uni_alipay_other_wangzhe')">
<c-lottie ref="cLottieRef" :src='$watermark()' width="94px" height='74px' :loop="true"></c-lottie>
</liu-drag-button>
</view>
<view class="save-action">
<button class="save-btn" @click="handleSave">保存</button>
</view>
<!-- 主题选择区域 -->
<view class="theme-selector">
<scroll-view scroll-x="true" class="theme-scroll" :show-scrollbar="false">
<view class="theme-list">
<view class="theme-item" v-for="(item, index) in ['样式一', '样式二']" :key="index + 1"
:class="{ active: hePingData.type == index + 1 }" @click="handleChangeTheme(index + 1)">
<text class="theme-text">{{ item }}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 数据编辑弹窗 -->
<uni-popup ref="editPopup" type="center">
<view class="edit-popup-content">
<view class="popup-header">
<text class="title">编辑主页数据</text>
</view>
<scroll-view scroll-y class="popup-scroll">
<view class="form-item avatar-form-item">
<text class="label">人物图片</text>
<view class=" avatar-uploader img-box" @click="handleChooseImgUrl">
<view class="avatar-preview" v-if="tempData.imgUrl">
<image class="preview-img" :src="tempData.imgUrl" mode="widthFix"></image>
<view class="delete-icon" @click.stop="handleDeleteImgUrl">
<text>×</text>
</view>
</view>
<view class="upload-btn" v-else @click="handleChooseImgUrl">
<text class="plus">+</text>
</view>
</view>
</view>
<view class="form-item avatar-form-item" v-if="tempData.type == 2">
<text class="label">头像</text>
<view class="avatar-uploader" @click="handleChooseAvatar">
<view class="avatar-preview" v-if="tempData.avatar">
<image class="preview-img" :src="tempData.avatar" mode="aspectFill"></image>
<view class="delete-icon" @click.stop="handleDeleteAvatar">
<text>×</text>
</view>
</view>
<view class="upload-btn" v-else @click="handleChooseAvatar">
<text class="plus">+</text>
</view>
</view>
</view>
<view class="form-item" v-if="tempData.type == 2">
<text class="label">昵称</text>
<input class="input" type="text" v-model="tempData.nickname" />
</view>
</scroll-view>
<view class="popup-footer">
<button class="cancel-btn" @click="closeEditPopup">取消</button>
<button class="confirm-btn" @click="confirmEdit">确定</button>
</view>
</view>
</uni-popup>
<!-- 横向全屏放大预览层 -->
<view class="preview-overlay" v-if="showPreview" @click="showPreview = false">
<image class="preview-image" :src="finalPosterPath" mode="aspectFill"></image>
</view>
</view>
</template>
<script setup>
import { ref, reactive, computed, watch, onMounted, getCurrentInstance } from 'vue'
import { onLoad, onReady, onUnload } from '@dcloudio/uni-app'
import { imgLocal } from '@/utils/common.js';
const instance = getCurrentInstance();
const { proxy } = getCurrentInstance();
//
const hePingData = reactive({
type: 1, // 1.2
avatar: '/static/image/shopping/pdd/avatars/avatars1.jpg', //
imgUrl: '/static/image/other/game/heping/default.png', //
nickname: '甜喵小贝', //
})
const characterImgWidth = ref(300); //
const characterStyle = computed(() => {
if (hePingData.type == 1) {
return `position: absolute; left: 50%; bottom:43%; transform: translate(-50%,50%); height: 300px; width: ${characterImgWidth.value}px;`;
} else {
return `position: absolute; left: 300px; bottom: 50%;transform: translate(-50%,50%); height: 300px; width: ${characterImgWidth.value}px;`;
}
});
//
watch(() => hePingData.imgUrl, (newUrl) => {
if (!newUrl) return;
uni.getImageInfo({
src: newUrl,
success: (res) => {
// 300px
const calcWidth = (300 / res.height) * res.width;
characterImgWidth.value = Math.round(calcWidth);
},
fail: () => {
characterImgWidth.value = 300; //
}
});
}, { immediate: true });
const bgImage = ref('')
// css calc()
const sysInfo = uni.getSystemInfoSync();
const windowWidth = sysInfo.windowWidth || 375;
// 16px padding 822
const posterScaleRatio = (windowWidth - 16) / 822;
const posterContainerHeight = 374 * posterScaleRatio;
onLoad(async () => {
const cachedData = uni.getStorageSync('hePingData');
if (cachedData) {
Object.assign(hePingData, cachedData);
// .jpg .png 404
if (hePingData.imgUrl === '/static/image/other/game/heping/default.jpg') {
hePingData.imgUrl = '/static/image/other/game/heping/default.png';
uni.setStorageSync('hePingData', hePingData);
}
}
bgImage.value = await imgLocal.getLocalImage(`other/game/heping/bg-${hePingData.type}.jpg`)
uni.$on('editFormPhoto', (info) => {
if (currentEditImgField.value) {
tempData[currentEditImgField.value] = info;
}
// #ifndef H5
if (info && !info.startsWith('/static/')) {
newAvatars.value.push(info);
}
// #endif
});
//
proxy.$apiUserEvent('all', {
type: 'click',
key: 'heping',
prefix: '.uni.other.',
value: "和平精英"
})
})
onUnload(() => {
uni.$off('editFormPhoto');
})
const rightButtonText = ref("编辑");
const finalPosterPath = ref('');
const showPreview = ref(false); //
const editPopup = ref(null);
const tempData = reactive({});
const newAvatars = ref([]);
const currentEditImgField = ref('');
function onRightClick() {
Object.assign(tempData, JSON.parse(JSON.stringify(hePingData)));
newAvatars.value = [];
editPopup.value.open();
}
const handleChooseAvatar = () => {
currentEditImgField.value = 'avatar';
uni.chooseImage({
count: 1,
success: (res) => {
uni.navigateTo({
url: `/pages/other/qf-image/qf-image?src=${res.tempFilePaths[0]}&width=200&height=200`
});
}
});
};
const handleChooseImgUrl = () => {
currentEditImgField.value = 'imgUrl';
uni.chooseImage({
count: 1,
success: (res) => {
if (res.tempFilePaths[0]) {
// qf-image 300x300 (150) isMatting=1
uni.navigateTo({
url: `/pages/other/qf-image/qf-image?src=${res.tempFilePaths[0]}&width=150&height=150&isMatting=1`
});
}
}
});
};
const handleDeleteAvatar = () => {
tempData.avatar = '';
};
const handleDeleteImgUrl = () => {
tempData.imgUrl = '';
};
function closeEditPopup() {
// #ifndef H5
newAvatars.value.forEach(path => {
uni.removeSavedFile({ filePath: path });
});
// #endif
editPopup.value.close();
}
function confirmEdit() {
// #ifndef H5
//
newAvatars.value.forEach(path => {
if (path !== tempData.avatar && path !== tempData.imgUrl) {
uni.removeSavedFile({ filePath: path });
}
});
//
if (hePingData.avatar !== tempData.avatar && hePingData.avatar && !hePingData.avatar.startsWith('/static/')) {
uni.removeSavedFile({ filePath: hePingData.avatar });
}
if (hePingData.imgUrl !== tempData.imgUrl && hePingData.imgUrl && !hePingData.imgUrl.startsWith('/static/')) {
uni.removeSavedFile({ filePath: hePingData.imgUrl });
}
// #endif
Object.assign(hePingData, tempData);
uni.setStorageSync('hePingData', hePingData);
editPopup.value.close();
//
finalPosterPath.value = '';
}
async function handleChangeTheme(typeIndex) {
//
finalPosterPath.value = '';
bgImage.value = ''; //
hePingData.type = typeIndex;
uni.setStorageSync('hePingData', hePingData);
bgImage.value = await imgLocal.getLocalImage(`other/game/heping/bg-${typeIndex}.jpg`);
}
//
const handlePreview = () => {
if (!finalPosterPath.value) {
uni.showToast({ title: '海报仍在生成中,请稍候', icon: 'none' });
return;
}
showPreview.value = true;
}
//
const onPainterSuccess = (path) => {
finalPosterPath.value = path;
}
//
const handleSave = () => {
if (!finalPosterPath.value) {
uni.showToast({ title: '图片仍在生成中,请稍候', icon: 'none' })
return
}
uni.saveImageToPhotosAlbum({
filePath: finalPosterPath.value,
success: () => {
uni.showToast({ title: '保存成功', icon: 'success' })
},
fail: () => {
uni.showToast({ title: '保存失败', icon: 'none' })
}
})
}
/**
* 将本地图片路径通过 Canvas 转换为 File 对象
* @param {string} localPath - 本地图片路径如从 uni.chooseImage 获取的 tempFilePath
* @param {Object} options - 可选参数
* @param {string} options.format - 输出格式 'image/png' 'image/jpeg'默认 'image/png'
* @param {number} options.quality - 图片质量 jpeg 有效0~1默认 0.92
* @param {number} options.maxWidth - 最大宽度等比缩放不填则使用原图尺寸
* @param {number} options.maxHeight - 最大高度等比缩放不填则使用原图尺寸
* @returns {Promise<File>} 返回一个 Promiseresolve File 对象
*/
function convertLocalImageToFile(localPath) {
return new Promise((resolve, reject) => {
// 1. Base64 Image
plus.io.resolveLocalFileSystemURL(localPath, (entry) => {
entry.file((file) => {
const reader = new plus.io.FileReader();
reader.onload = (e) => {
const base64Data = e.target.result; // data:image/jpeg;base64,/9j/...
resolve(e.target.result)
};
reader.onerror = (err) => reject(err);
reader.readAsDataURL(file); // DataURL
}, reject);
}, reject);
});
}
</script>
<style lang="less" scoped>
.preview-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #000;
z-index: 99999;
/* 抛弃 flex 布局,防止 width: 100vh 被父级限制而发生坍缩 */
}
.preview-overlay .preview-image {
position: absolute;
top: 50%;
left: 50%;
width: 100vh;
height: 100vw;
/* 先拉回自身中心点,再围绕中心点旋转 90 度 */
transform: translate(-50%, -50%) rotate(90deg);
}
.honor-of-kings {
width: 100%;
overflow-x: hidden;
}
.painter-container {
/* 强制画板缩放到设备屏幕宽度,避免物理 px 超出屏幕 */
padding: 8px;
zoom: calc(100vw / 798);
width: 100%;
max-width: 1000px;
/* 限制PC/iPad端的最大宽度 */
margin: 0 auto;
}
.save-action {
margin-top: 60rpx;
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 220rpx;
/* 留出底部主题固定栏的安全空间 */
.save-btn {
margin-top: 60rpx;
width: 316rpx;
background: #1777FF;
color: #fff;
border-radius: 56rpx;
}
}
.theme-selector {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
padding-top: 20rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
z-index: 99;
.theme-scroll {
width: 100%;
white-space: nowrap;
::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
}
.theme-list {
display: inline-flex;
width: 100%;
justify-content: center;
padding: 0 20rpx;
.theme-item {
display: inline-flex;
vertical-align: top;
margin: 0 10rpx;
padding: 0 40rpx;
height: 84rpx;
border-radius: 12rpx;
background-color: #FFFFFF;
justify-content: center;
align-items: center;
border: 2rpx solid transparent;
box-sizing: border-box;
transition: all 0.3s;
&.active {
background-color: #fff;
border-color: #3B7BFF;
.theme-text {
color: #3B7BFF;
font-weight: bold;
}
}
.theme-text {
font-size: 28rpx;
color: #666;
}
}
}
}
.edit-popup-content {
background-color: #fff;
border-radius: 20rpx;
width: 85vw;
.popup-header {
display: flex;
justify-content: center;
align-items: center;
padding: 40rpx 0 20rpx 0;
.title {
font-size: 34rpx;
font-weight: bold;
color: #333;
}
}
.popup-scroll {
max-height: 55vh;
padding: 20rpx 40rpx;
box-sizing: border-box;
}
.form-item {
display: flex;
align-items: center;
margin-bottom: 16rpx;
padding-bottom: 16rpx;
.label {
width: 200rpx;
font-size: 28rpx;
color: #333;
}
.avatar-uploader {
display: flex;
align-items: center;
.upload-btn {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background-color: #999DA7;
display: flex;
justify-content: center;
align-items: center;
.plus {
font-size: 60rpx;
color: #fff;
font-weight: 300;
margin-top: -6rpx;
}
}
.avatar-preview {
position: relative;
width: 100rpx;
height: 100rpx;
.preview-img {
width: 100%;
height: 100%;
border-radius: 50%;
}
.delete-icon {
position: absolute;
top: -4rpx;
right: -4rpx;
width: 32rpx;
height: 32rpx;
background-color: red;
color: #fff;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
font-size: 30rpx;
line-height: 28rpx;
}
}
}
.img-box {
width: 200rpx !important;
.avatar-preview {
width: 200rpx !important;
height: auto !important;
}
.preview-img {
width: 100%;
border-radius: 0 !important;
}
}
.input {
flex: 1;
font-size: 28rpx;
background-color: #F7F7F7;
border-radius: 12rpx;
height: 70rpx;
line-height: 70rpx;
padding: 0 20rpx;
color: #333;
}
}
.popup-footer {
display: flex;
justify-content: space-between;
padding: 20rpx 40rpx 40rpx;
button {
width: 46%;
height: 76rpx;
line-height: 76rpx;
font-size: 30rpx;
border-radius: 12rpx;
margin: 0;
&::after {
border: none;
}
}
.cancel-btn {
background-color: #F4F4F4;
color: #666;
}
.confirm-btn {
background-color: #3B7BFF;
color: #fff;
}
}
}
</style>

View File

@ -51,12 +51,12 @@ const menuList = ref([{
name: "和平精英",
icon: "hepingjingying",
shadowColor: "#4992F2",
url: ""
url: "/pages/other/game/hepingjingying"
}, {
name: "无畏契约",
icon: "wuweiqiyue",
shadowColor: "#FF3333",
url: ""
url: "/pages/other/game/wuweiqiyue"
}]);
const goTo = (url) => {

View File

@ -0,0 +1,708 @@
<template>
<view class="honor-of-kings">
<nav-bar title="无畏契约" bgColor="#F5F5F5" isRightButton :rightButtonText="rightButtonText"
@right-click="onRightClick">
</nav-bar>
<view style="padding: 8px;box-sizing: border-box;">
<view class="painter-container" @click="handlePreview"
:style="`width:calc(100vw - 16px) ; height: ${posterContainerHeight}px; overflow: hidden; position: relative; transform: translateZ(0);`">
<!-- 运用 js 算出的无误差纯数字 scale 来实现高清画板的等比缩小彻底兼容所有老旧内核不再被截断 -->
<view
:style="`width: 822px; transform: scale(${posterScaleRatio}); transform-origin: left top; position: absolute; left: 0; top: 0;`">
<l-painter v-if="bgImage" isCanvasToTempFilePath @success="onPainterSuccess" :css="`width:822px;`">
<l-painter-view :css="`width: 822px; position: relative;`">
<!-- 直接使用主背景图撑开父容器移除不必要的 opacity: 0 占位图避免在大屏渲染时产生黑块 -->
<l-painter-image :src="bgImage" css="width: 822px; display: block;"></l-painter-image>
<!-- 人物图片 -->
<l-painter-image :src="wuweiData.imgUrl" :css="characterStyle"
mode="widthFix"></l-painter-image>
<!-- 击败 -->
<l-painter-text :text="wuweiData.killCount" :css="killCountStyle"></l-painter-text>
<template v-if="wuweiData.type == 1">
<!-- 精准射击 -->
<l-painter-text :text="wuweiData.shooting"
:css="`${style1TextCommonStyle}; left:151px;`"></l-painter-text>
<!-- 输出伤害 -->
<l-painter-text :text="wuweiData.output"
:css="`${style1TextCommonStyle}; left:221px;`"></l-painter-text>
<!-- 获得资金 -->
<l-painter-text :text="wuweiData.harvest"
:css="`${style1TextCommonStyle}; left:292px;`"></l-painter-text>
<!-- 三连击败 -->
<l-painter-text :text="wuweiData.threeKill"
:css="`${style1TextCommonStyle}; left:362px;`"></l-painter-text>
</template>
<template v-if="wuweiData.type == 2">
<!-- 头像 -->
<l-painter-image :src="wuweiData.avatar"
css="position:absolute;left:65.5px;bottom:68.5px;width:42px;height:42px"></l-painter-image>
<!-- 昵称 -->
<l-painter-text :text="truncatedNickname"
css="position: absolute; left: 119px; bottom: 82px; font-size: 13px; font-family: Arial, sans-serif; color: #FFFFFF; width: 117px;"></l-painter-text>
<!-- 阵败 -->
<l-painter-text :text="wuweiData.defeatsCount"
css="position: absolute; left: 152.5px; bottom: 122px; font-size: 18px;font-weight: 700;color: #FFFFFF;line-height: 18px;transform: translateX(-50%)"></l-painter-text>
<!-- 助攻 -->
<l-painter-text :text="wuweiData.assistCount"
css="position: absolute; left: 215px; bottom: 122px; font-size: 18px;font-weight: 700;color: #FFFFFF;line-height: 18px;transform: translateX(-50%)"></l-painter-text>
<!-- 战斗分 -->
<l-painter-text :text="`平均战斗评分:${wuweiData.battlePoints}`"
css="position: absolute; left: 150px; bottom: 153px; font-size:10px;color: #FFFFFF;line-height: 10px;transform: translateX(-50%)"></l-painter-text>
</template>
<!-- 水印 -->
<l-painter-image v-if="$isVip()" src="/static/image/other/shuiying.png"
:css="`position: absolute; left: 18.8px; bottom: 17.1px; width: 145.38px;height:42.76px`"></l-painter-image>
</l-painter-view>
</l-painter>
</view>
</view>
</view>
<!-- 水印 -->
<view v-if="$isVip()">
<liu-drag-button :canDocking="false" @clickBtn="$goRechargePage('watermark', 'uni_alipay_other_wangzhe')">
<c-lottie ref="cLottieRef" :src='$watermark()' width="94px" height='74px' :loop="true"></c-lottie>
</liu-drag-button>
</view>
<view class="save-action">
<button class="save-btn" @click="handleSave">保存</button>
</view>
<!-- 主题选择区域 -->
<view class="theme-selector">
<scroll-view scroll-x="true" class="theme-scroll" :show-scrollbar="false">
<view class="theme-list">
<view class="theme-item" v-for="(item, index) in ['样式一', '样式二']" :key="index + 1"
:class="{ active: wuweiData.type == index + 1 }" @click="handleChangeTheme(index + 1)">
<text class="theme-text">{{ item }}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 数据编辑弹窗 -->
<uni-popup ref="editPopup" type="center">
<view class="edit-popup-content">
<view class="popup-header">
<text class="title">编辑主页数据</text>
</view>
<scroll-view scroll-y class="popup-scroll">
<view class="form-item avatar-form-item">
<text class="label">人物图片</text>
<view class=" avatar-uploader img-box" @click="handleChooseImgUrl">
<view class="avatar-preview" v-if="tempData.imgUrl">
<image class="preview-img" :src="tempData.imgUrl" mode="widthFix"></image>
<view class="delete-icon" @click.stop="handleDeleteImgUrl">
<text>×</text>
</view>
</view>
<view class="upload-btn" v-else @click="handleChooseImgUrl">
<text class="plus">+</text>
</view>
</view>
</view>
<view class="form-item avatar-form-item" v-if="tempData.type == 2">
<text class="label">头像</text>
<view class="avatar-uploader" @click="handleChooseAvatar">
<view class="avatar-preview" v-if="tempData.avatar">
<image class="preview-img" :src="tempData.avatar" mode="aspectFill"></image>
<view class="delete-icon" @click.stop="handleDeleteAvatar">
<text>×</text>
</view>
</view>
<view class="upload-btn" v-else @click="handleChooseAvatar">
<text class="plus">+</text>
</view>
</view>
</view>
<view class="form-item" v-if="tempData.type == 2">
<text class="label">昵称</text>
<input class="input" type="text" v-model="tempData.nickname" />
</view>
<view class="form-item">
<text class="label">击败</text>
<input class="input" type="number" v-model="tempData.killCount" />
</view>
<view class="form-item" v-if="tempData.type == 1">
<text class="label">精准射击</text>
<input class="input" type="number" v-model="tempData.shooting" />
</view>
<view class="form-item" v-if="tempData.type == 1">
<text class="label">输出伤害</text>
<input class="input" type="number" v-model="tempData.output" />
</view>
<view class="form-item" v-if="tempData.type == 1">
<text class="label">获得资金</text>
<input class="input" type="number" v-model="tempData.harvest" />
</view>
<view class="form-item" v-if="tempData.type == 1">
<text class="label">三连击败</text>
<input class="input" type="number" v-model="tempData.threeKill" />
</view>
<view class="form-item" v-if="tempData.type == 2">
<text class="label">阵败</text>
<input class="input" type="number" v-model="tempData.defeatsCount" />
</view>
<view class="form-item" v-if="tempData.type == 2">
<text class="label">助攻</text>
<input class="input" type="number" v-model="tempData.assistCount" />
</view>
<view class="form-item" v-if="tempData.type == 2">
<text class="label">战斗评分</text>
<input class="input" type="number" v-model="tempData.battlePoints" />
</view>
</scroll-view>
<view class="popup-footer">
<button class="cancel-btn" @click="closeEditPopup">取消</button>
<button class="confirm-btn" @click="confirmEdit">确定</button>
</view>
</view>
</uni-popup>
<!-- 横向全屏放大预览层 -->
<view class="preview-overlay" v-if="showPreview" @click="showPreview = false">
<image class="preview-image" :src="finalPosterPath" mode="aspectFill"></image>
</view>
</view>
</template>
<script setup>
import { ref, reactive, computed, watch, onMounted, getCurrentInstance } from 'vue'
import { onLoad, onReady, onUnload } from '@dcloudio/uni-app'
import { imgLocal } from '@/utils/common.js';
const instance = getCurrentInstance();
const { proxy } = getCurrentInstance();
//
const wuweiData = reactive({
type: 1, // 1.2
avatar: '/static/image/shopping/pdd/avatars/avatars1.jpg', //
imgUrl: '/static/image/other/game/heping/default.png', //
nickname: '甜喵小贝', //
killCount: 24,
defeatsCount: 2,
assistCount: 12,
battlePoints: 3854,
shooting: 7,
output: 4109,
harvest: 41750,
threeKill: 5
})
const characterImgHeight = ref(350); //
const characterStyle = computed(() => {
if (wuweiData.type == 1) {
return `position: absolute; right: 77px; bottom: 0; width: 300px; height: ${characterImgHeight.value}px;`;
} else {
return `position: absolute; right: 210px; bottom: 0; width: 300px; height: ${characterImgHeight.value}px;`;
}
});
const killCountStyle = computed(() => {
if (wuweiData.type == 1) {
return `position: absolute; left: 81px; bottom:79px; font-size: 16px;color: #FFFFFF;line-height: 16px;transform: translateX(-50%)`;
} else {
return `position: absolute; left: 90px; bottom: 122px; font-size: 18px;font-weight: 700;color: #FFFFFF;line-height: 18px;transform: translateX(-50%)`;
}
});
const style1TextCommonStyle = `position: absolute; bottom:79px; font-size: 16px;color: #FFFFFF;line-height: 16px;transform: translateX(-50%)`;
// line-clamp
const truncatedNickname = computed(() => {
let name = wuweiData.nickname || '';
let width = 0;
let result = '';
let max = 117 - 15; // 15px "..."
for (let i = 0; i < name.length; i++) {
let charCode = name.charCodeAt(i);
if (charCode >= 0 && charCode <= 128) {
width += 7.5; //
} else {
width += 13; // font-size: 13px
}
if (width > max) {
return result + '...';
}
result += name[i];
}
return name;
});
//
watch(() => wuweiData.imgUrl, (newUrl) => {
if (!newUrl) return;
uni.getImageInfo({
src: newUrl,
success: (res) => {
// 300px
const calcHeight = (300 / res.width) * res.height;
characterImgHeight.value = Math.round(calcHeight);
},
fail: () => {
characterImgHeight.value = 350; //
}
});
}, { immediate: true });
const bgImage = ref('')
// css calc()
const sysInfo = uni.getSystemInfoSync();
const windowWidth = sysInfo.windowWidth || 375;
// 16px padding 822
const posterScaleRatio = (windowWidth - 16) / 822;
const posterContainerHeight = 374 * posterScaleRatio;
onLoad(async () => {
const cachedData = uni.getStorageSync('wuweiData');
if (cachedData) {
Object.assign(wuweiData, cachedData);
// .jpg .png 404
if (wuweiData.imgUrl === '/static/image/other/game/heping/default.jpg') {
wuweiData.imgUrl = '/static/image/other/game/heping/default.png';
uni.setStorageSync('wuweiData', wuweiData);
}
}
bgImage.value = await imgLocal.getLocalImage(`other/game/wuwei/bg-${wuweiData.type}.jpg`)
uni.$on('editFormPhoto', (info) => {
if (currentEditImgField.value) {
tempData[currentEditImgField.value] = info;
}
// #ifndef H5
if (info && !info.startsWith('/static/')) {
newAvatars.value.push(info);
}
// #endif
});
//
proxy.$apiUserEvent('all', {
type: 'click',
key: 'heping',
prefix: '.uni.other.',
value: "和平精英"
})
})
onUnload(() => {
uni.$off('editFormPhoto');
})
const rightButtonText = ref("编辑");
const finalPosterPath = ref('');
const showPreview = ref(false); //
const editPopup = ref(null);
const tempData = reactive({});
const newAvatars = ref([]);
const currentEditImgField = ref('');
function onRightClick() {
Object.assign(tempData, JSON.parse(JSON.stringify(wuweiData)));
newAvatars.value = [];
editPopup.value.open();
}
const handleChooseAvatar = () => {
currentEditImgField.value = 'avatar';
uni.chooseImage({
count: 1,
success: (res) => {
uni.navigateTo({
url: `/pages/other/qf-image/qf-image?src=${res.tempFilePaths[0]}&width=200&height=200`
});
}
});
};
const handleChooseImgUrl = () => {
currentEditImgField.value = 'imgUrl';
uni.chooseImage({
count: 1,
success: (res) => {
if (res.tempFilePaths[0]) {
// qf-image 300x350 (150x175) isMatting=1
uni.navigateTo({
url: `/pages/other/qf-image/qf-image?src=${res.tempFilePaths[0]}&width=150&height=175&isMatting=1`
});
}
}
});
};
const handleDeleteAvatar = () => {
tempData.avatar = '';
};
const handleDeleteImgUrl = () => {
tempData.imgUrl = '';
};
function closeEditPopup() {
// #ifndef H5
newAvatars.value.forEach(path => {
uni.removeSavedFile({ filePath: path });
});
// #endif
editPopup.value.close();
}
function confirmEdit() {
// #ifndef H5
//
newAvatars.value.forEach(path => {
if (path !== tempData.avatar && path !== tempData.imgUrl) {
uni.removeSavedFile({ filePath: path });
}
});
//
if (wuweiData.avatar !== tempData.avatar && wuweiData.avatar && !wuweiData.avatar.startsWith('/static/')) {
uni.removeSavedFile({ filePath: wuweiData.avatar });
}
if (wuweiData.imgUrl !== tempData.imgUrl && wuweiData.imgUrl && !wuweiData.imgUrl.startsWith('/static/')) {
uni.removeSavedFile({ filePath: wuweiData.imgUrl });
}
// #endif
Object.assign(wuweiData, tempData);
uni.setStorageSync('wuweiData', wuweiData);
editPopup.value.close();
//
finalPosterPath.value = '';
}
async function handleChangeTheme(typeIndex) {
//
finalPosterPath.value = '';
bgImage.value = ''; //
wuweiData.type = typeIndex;
uni.setStorageSync('wuweiData', wuweiData);
bgImage.value = await imgLocal.getLocalImage(`other/game/wuwei/bg-${typeIndex}.jpg`);
}
//
const handlePreview = () => {
if (!finalPosterPath.value) {
uni.showToast({ title: '海报仍在生成中,请稍候', icon: 'none' });
return;
}
showPreview.value = true;
}
//
const onPainterSuccess = (path) => {
finalPosterPath.value = path;
}
//
const handleSave = () => {
if (!finalPosterPath.value) {
uni.showToast({ title: '图片仍在生成中,请稍候', icon: 'none' })
return
}
uni.saveImageToPhotosAlbum({
filePath: finalPosterPath.value,
success: () => {
uni.showToast({ title: '保存成功', icon: 'success' })
},
fail: () => {
uni.showToast({ title: '保存失败', icon: 'none' })
}
})
}
/**
* 将本地图片路径通过 Canvas 转换为 File 对象
* @param {string} localPath - 本地图片路径如从 uni.chooseImage 获取的 tempFilePath
* @param {Object} options - 可选参数
* @param {string} options.format - 输出格式 'image/png' 'image/jpeg'默认 'image/png'
* @param {number} options.quality - 图片质量 jpeg 有效0~1默认 0.92
* @param {number} options.maxWidth - 最大宽度等比缩放不填则使用原图尺寸
* @param {number} options.maxHeight - 最大高度等比缩放不填则使用原图尺寸
* @returns {Promise<File>} 返回一个 Promiseresolve File 对象
*/
function convertLocalImageToFile(localPath) {
return new Promise((resolve, reject) => {
// 1. Base64 Image
plus.io.resolveLocalFileSystemURL(localPath, (entry) => {
entry.file((file) => {
const reader = new plus.io.FileReader();
reader.onload = (e) => {
const base64Data = e.target.result; // data:image/jpeg;base64,/9j/...
resolve(e.target.result)
};
reader.onerror = (err) => reject(err);
reader.readAsDataURL(file); // DataURL
}, reject);
}, reject);
});
}
</script>
<style lang="less" scoped>
.preview-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #000;
z-index: 99999;
/* 抛弃 flex 布局,防止 width: 100vh 被父级限制而发生坍缩 */
}
.preview-overlay .preview-image {
position: absolute;
top: 50%;
left: 50%;
width: 100vh;
height: 100vw;
/* 先拉回自身中心点,再围绕中心点旋转 90 度 */
transform: translate(-50%, -50%) rotate(90deg);
}
.honor-of-kings {
width: 100%;
overflow-x: hidden;
}
.painter-container {
/* 强制画板缩放到设备屏幕宽度,避免物理 px 超出屏幕 */
padding: 8px;
zoom: calc(100vw / 798);
width: 100%;
max-width: 1000px;
/* 限制PC/iPad端的最大宽度 */
margin: 0 auto;
}
.save-action {
margin-top: 60rpx;
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 220rpx;
/* 留出底部主题固定栏的安全空间 */
.save-btn {
margin-top: 60rpx;
width: 316rpx;
background: #1777FF;
color: #fff;
border-radius: 56rpx;
}
}
.theme-selector {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
padding-top: 20rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
z-index: 99;
.theme-scroll {
width: 100%;
white-space: nowrap;
::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
}
.theme-list {
display: inline-flex;
width: 100%;
justify-content: center;
padding: 0 20rpx;
.theme-item {
display: inline-flex;
vertical-align: top;
margin: 0 10rpx;
padding: 0 40rpx;
height: 84rpx;
border-radius: 12rpx;
background-color: #FFFFFF;
justify-content: center;
align-items: center;
border: 2rpx solid transparent;
box-sizing: border-box;
transition: all 0.3s;
&.active {
background-color: #fff;
border-color: #3B7BFF;
.theme-text {
color: #3B7BFF;
font-weight: bold;
}
}
.theme-text {
font-size: 28rpx;
color: #666;
}
}
}
}
.edit-popup-content {
background-color: #fff;
border-radius: 20rpx;
width: 85vw;
.popup-header {
display: flex;
justify-content: center;
align-items: center;
padding: 40rpx 0 20rpx 0;
.title {
font-size: 34rpx;
font-weight: bold;
color: #333;
}
}
.popup-scroll {
max-height: 55vh;
padding: 20rpx 40rpx;
box-sizing: border-box;
}
.form-item {
display: flex;
align-items: center;
margin-bottom: 16rpx;
padding-bottom: 16rpx;
.label {
width: 200rpx;
font-size: 28rpx;
color: #333;
}
.avatar-uploader {
display: flex;
align-items: center;
.upload-btn {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background-color: #999DA7;
display: flex;
justify-content: center;
align-items: center;
.plus {
font-size: 60rpx;
color: #fff;
font-weight: 300;
margin-top: -6rpx;
}
}
.avatar-preview {
position: relative;
width: 100rpx;
height: 100rpx;
.preview-img {
width: 100%;
height: 100%;
border-radius: 50%;
}
.delete-icon {
position: absolute;
top: -4rpx;
right: -4rpx;
width: 32rpx;
height: 32rpx;
background-color: red;
color: #fff;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
font-size: 30rpx;
line-height: 28rpx;
}
}
}
.img-box {
width: 200rpx !important;
.avatar-preview {
width: 200rpx !important;
height: auto !important;
}
.preview-img {
width: 100%;
border-radius: 0 !important;
}
}
.input {
flex: 1;
font-size: 28rpx;
background-color: #F7F7F7;
border-radius: 12rpx;
height: 70rpx;
line-height: 70rpx;
padding: 0 20rpx;
color: #333;
}
}
.popup-footer {
display: flex;
justify-content: space-between;
padding: 20rpx 40rpx 40rpx;
button {
width: 46%;
height: 76rpx;
line-height: 76rpx;
font-size: 30rpx;
border-radius: 12rpx;
margin: 0;
&::after {
border: none;
}
}
.cancel-btn {
background-color: #F4F4F4;
color: #666;
}
.confirm-btn {
background-color: #3B7BFF;
color: #fff;
}
}
}
</style>

View File

@ -41,6 +41,9 @@
data.height=option.height*2
data.isCard=false
}
if (option.isMatting === '1') {
data.isCard = true; //
}
})
function handleCrop(e) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -459,3 +459,4 @@
// #endif
}
</style>
// cache bust 2024-07

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -261,7 +261,7 @@ export const stringUtil = {
* @returns {string} 唯一ID
*/
uuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
@ -486,7 +486,7 @@ export const util = {
timingFunc: 'easeIn'
}
})
setTimeout(function() {
setTimeout(function () {
uni.setNavigationBarColor({
animation: { // 动画效果
duration: 100,
@ -631,7 +631,7 @@ export const imgLocal = {
getLocalImage(url) {
return new Promise((resolve, reject) => {
if (!url) return reject('url不能为空');
const CACHE_KEY = 'IMG_CACHE_MAP';
const cache = uni.getStorageSync(CACHE_KEY) || {};
@ -640,23 +640,28 @@ export const imgLocal = {
// 2. 命中缓存
if (cache[key]) {
return resolve(cache[key]);
let resultPath = cache[key];
// #ifdef APP-PLUS
// iOS/Android 编译刷新后沙盒绝对路径的 UUID 会变!必须用相对路径重新计算最新绝对路径
resultPath = plus.io.convertLocalFileSystemURL(`_doc/img_cache/${key}.jpg`);
// #endif
return resolve(resultPath);
}
const config = uni.getStorageSync('config')
let imgConfig = config.config['client.uniapp.img_path']?.alipay||'http://cdn.u8t.cn/flaunt/uni_mp/img/alipay/static/image/'
url=imgConfig+url
let imgConfig = config.config['client.uniapp.img_path']?.alipay || 'http://cdn.u8t.cn/flaunt/uni_mp/img/alipay/static/image/'
url = imgConfig + url
// #ifdef APP-PLUS
const filename = `_doc/img_cache/${key}.jpg`;
const task = plus.downloader.createDownload(
url, {
filename
},
filename
},
(download, status) => {
if (status === 200) {
const localPath = plus.io.convertLocalFileSystemURL(download.filename);
cache[key] = localPath;
cache[key] = localPath; // 虽然存了绝对路径,但上面命中缓存时会强制重新计算
uni.setStorageSync(CACHE_KEY, cache);
resolve(localPath);