alipay-emulator/pages/other/game/wuweiqiyue.vue

728 lines
21 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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"></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_game_wuweiqiyue')">
<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>
<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';
import NavBar from '@/components/nav-bar/nav-bar.vue';
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: 350px;`;
} else {
return `position: absolute; right: 210px; bottom: 0; width: 300px; height: 350px;`;
}
});
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);
}
}
try {
bgImage.value = await imgLocal.getLocalImage(`other/game/wuwei/bg-${wuweiData.type}.jpg`);
} catch (e) {
console.error('获取背景图失败:', e);
uni.showToast({ title: '背景图加载失败', icon: 'none' });
}
uni.$on('editFormPhoto', (info) => {
if (currentEditImgField.value) {
tempData[currentEditImgField.value] = info;
}
// #ifndef H5
if (info && !info.startsWith('/static/')) {
newAvatars.value.push(info);
}
// #endif
});
// 进入页面埋点
if (proxy && proxy.$apiUserEvent) {
proxy.$apiUserEvent('all', {
type: 'click',
key: 'wuwei',
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 = '';
let tempBg = bgImage.value;
bgImage.value = ''; // 强制卸载旧画板,确保能重新渲染和生成新海报
setTimeout(() => {
bgImage.value = tempBg;
}, 50);
}
async function handleChangeTheme(typeIndex) {
// 切换主题时,先清空旧海报,触发界面“加载中”提示
finalPosterPath.value = '';
bgImage.value = ''; // 强制卸载旧画板,确保能重新渲染和生成新海报
wuweiData.type = typeIndex;
uni.setStorageSync('wuweiData', wuweiData);
try {
bgImage.value = await imgLocal.getLocalImage(`other/game/wuwei/bg-${typeIndex}.jpg`);
} catch (e) {
console.error('切换主题加载背景图失败:', e);
uni.showToast({ title: '背景加载失败', icon: 'none' });
}
}
// 点击画布触发横向预览
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>