hookehuyr

feat(PosterBuilder): 添加文字描边和阴影效果支持并完善海报数据

为_drawSingleText函数添加描边和阴影效果支持,提升文字显示效果
重构PosterCheckin页面,将mock数据整合到海报列表中并调整海报配置
移除不必要的UI元素,优化海报布局和样式
......@@ -158,7 +158,13 @@ export function _drawSingleText(drawData, drawOptions) {
lineHeight = 0,
fontWeight = 'normal',
fontStyle = 'normal',
fontFamily = 'sans-serif'
fontFamily = 'sans-serif',
strokeStyle,
lineWidth = 0,
shadowColor,
shadowOffsetX = 0,
shadowOffsetY = 0,
shadowBlur = 0
} = drawData;
const { ctx } = drawOptions;
// 画笔初始化
......@@ -214,13 +220,36 @@ export function _drawSingleText(drawData, drawOptions) {
}
// 按行渲染文字
textArr.forEach((item, index) =>
ctx.fillText(
item,
getTextX(textAlign, x, width), // 根据文本对齐方式和宽度确定 x 坐标
y + (lineHeight || fontSize) * index // 根据行数、行高 || 字体大小确定 y 坐标
)
);
textArr.forEach((item, index) => {
const textX = getTextX(textAlign, x, width); // 根据文本对齐方式和宽度确定 x 坐标
const textY = y + (lineHeight || fontSize) * index; // 根据行数、行高 || 字体大小确定 y 坐标
// 设置阴影效果
if (shadowColor) {
ctx.shadowColor = shadowColor;
ctx.shadowOffsetX = shadowOffsetX;
ctx.shadowOffsetY = shadowOffsetY;
ctx.shadowBlur = shadowBlur;
}
// 如果有描边设置,先绘制描边
if (strokeStyle && lineWidth > 0) {
ctx.strokeStyle = strokeStyle;
ctx.lineWidth = lineWidth;
ctx.strokeText(item, textX, textY);
}
// 绘制填充文字
ctx.fillText(item, textX, textY);
// 清除阴影设置,避免影响后续绘制
if (shadowColor) {
ctx.shadowColor = 'transparent';
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.shadowBlur = 0;
}
});
ctx.restore();
// 文本修饰,下划线、删除线什么的
......
......@@ -37,9 +37,9 @@
class="w-full h-full"
/>
<!-- 打卡点标题 -->
<view class="absolute bottom-2 left-2 bg-blue-500 text-white text-xs px-2 py-1 rounded">
<!-- <view class="absolute bottom-2 left-2 bg-blue-500 text-white text-xs px-2 py-1 rounded">
{{ posterList[currentPosterIndex]?.title || '海报生成中' }}
</view>
</view> -->
<!-- 点击预览提示 -->
<view @tap="previewPoster" class="absolute bottom-2 right-2 bg-black bg-opacity-50 text-white text-xs px-2 py-1 rounded">
点击预览
......@@ -146,6 +146,9 @@ import { Left, Right } from '@nutui/icons-vue-taro'
import PosterBuilder from '@/components/PosterBuilder/index.vue'
import BASE_URL from '@/utils/config'
// 默认背景图
const defaultBackground = 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%B5%B7%E6%8A%A5%E9%BB%98%E8%AE%A4%E8%83%8C%E6%99%AF%E5%9B%BE1.png'
// 页面状态
const posterPath = ref('') // 生成的海报路径
const backgroundImage = ref('') // 用户上传的背景图
......@@ -179,28 +182,85 @@ const activityInfo = ref({
// activityInfo.value = {};
// 海报列表数据 - 模拟不同状态,实际使用时从API获取
// 海报数据列表 - 合并海报基础信息和内容数据,实际使用时从API获取
const posterList = ref([
{
id: 1,
title: '起点签到',
path: '',
checkPointId: 1,
backgroundImage: 'https://cdn.ipadbiz.cn/lls_prog/images/welcome_1.png'
backgroundImage: 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%B5%B7%E6%8A%A5%E9%BB%98%E8%AE%A4%E8%83%8C%E6%99%AF%E5%9B%BE1.png',
// 海报内容数据
user: {
avatar: 'https://cdn.ipadbiz.cn/icon/tou@2x.png',
nickname: '张大爷'
},
family: {
name: '幸福之家',
description: '一家人整整齐齐最重要'
},
activity: {
logo: 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%B5%B7%E6%8A%A5%E5%B7%A6%E4%B8%8A%E8%A7%92logo.png',
name: '南京路时尚citywalk'
},
level: {
logo: 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%B5%B7%E6%8A%A5%E5%8F%B3%E4%B8%8B%E8%A7%92icon.png',
name: '第一关卡'
},
qrcode: 'https://cdn.ipadbiz.cn/space/068a790496c87cb8d2ed6e551401c544.png',
qrcodeDesc: '长按识别,来,我们一起打卡!'
},
{
id: 2,
title: '商圈探索',
path: '',
checkPointId: 2,
backgroundImage: 'https://cdn.ipadbiz.cn/lls_prog/images/welcome_1.png'
backgroundImage: 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%B5%B7%E6%8A%A5%E9%BB%98%E8%AE%A4%E8%83%8C%E6%99%AF%E5%9B%BE1.png',
// 海报内容数据
user: {
avatar: 'https://cdn.ipadbiz.cn/icon/tou@2x.png',
nickname: '李奶奶'
},
family: {
name: '温馨小家',
description: '健康快乐每一天'
},
activity: {
logo: 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%B5%B7%E6%8A%A5%E5%B7%A6%E4%B8%8A%E8%A7%92logo.png',
name: '南京路时尚citywalk'
},
level: {
logo: 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%B5%B7%E6%8A%A5%E5%8F%B3%E4%B8%8B%E8%A7%92icon.png',
name: '第二关卡'
},
qrcode: 'https://cdn.ipadbiz.cn/space/068a790496c87cb8d2ed6e551401c544.png',
qrcodeDesc: '扫码加入我们的队伍!'
},
{
id: 3,
title: '文化体验',
path: '',
checkPointId: 3,
backgroundImage: 'https://cdn.ipadbiz.cn/lls_prog/images/welcome_1.png'
backgroundImage: 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%B5%B7%E6%8A%A5%E9%BB%98%E8%AE%A4%E8%83%8C%E6%99%AF%E5%9B%BE1.png',
// 海报内容数据
user: {
avatar: 'https://cdn.ipadbiz.cn/icon/tou@2x.png',
nickname: '王叔叔'
},
family: {
name: '快乐大家庭',
description: '运动让我们更年轻'
},
activity: {
logo: 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%B5%B7%E6%8A%A5%E5%B7%A6%E4%B8%8A%E8%A7%92logo.png',
name: '南京路时尚citywalk'
},
level: {
logo: 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%B5%B7%E6%8A%A5%E5%8F%B3%E4%B8%8B%E8%A7%92icon.png',
name: '第三关卡'
},
qrcode: 'https://cdn.ipadbiz.cn/space/068a790496c87cb8d2ed6e551401c544.png',
qrcodeDesc: '一起来挑战吧!'
}
])
......@@ -233,30 +293,20 @@ const currentPoster = computed(() => {
return posterList.value[currentPosterIndex.value] || { path: '', title: '' }
})
// Mock数据
const mockData = ref({
user: {
avatar: 'https://cdn.ipadbiz.cn/icon/tou@2x.png',
nickname: '张大爷'
},
family: {
name: '幸福之家',
description: '一家人整整齐齐最重要'
},
activity: {
logo: 'https://cdn.ipadbiz.cn/lls_prog/images/activity-logo.png',
name: '南京路时尚citywalk'
},
level: {
logo: 'https://cdn.ipadbiz.cn/lls_prog/images/level-badge.png',
name: '第一关卡'
},
qrcode: 'https://cdn.ipadbiz.cn/space/068a790496c87cb8d2ed6e551401c544.png',
qrcodeDesc: '长按识别小程序码\n送我一朵小红花为我助力吧~'
})
// 当前海报的内容数据
const currentMockData = computed(() => {
const currentPoster = posterList.value[currentPosterIndex.value]
if (!currentPoster) return null
// 默认背景图
const defaultBackground = 'https://cdn.ipadbiz.cn/lls_prog/images/welcome_1.png'
return {
user: currentPoster.user,
family: currentPoster.family,
activity: currentPoster.activity,
level: currentPoster.level,
qrcode: currentPoster.qrcode,
qrcodeDesc: currentPoster.qrcodeDesc
}
})
// 海报配置
const posterConfig = computed(() => {
......@@ -282,67 +332,88 @@ const posterConfig = computed(() => {
{
x: 40,
y: 40,
width: 80,
height: 80,
url: mockData.value.user.avatar,
borderRadius: 40,
width: 130,
height: 130,
url: currentMockData.value.user.avatar,
borderRadius: 65,
zIndex: 2
},
// 活动logo
{
x: 580,
x: 500,
y: 40,
width: 120,
width: 200,
height: 80,
url: mockData.value.activity.logo,
url: currentMockData.value.activity.logo,
zIndex: 2
},
// 关卡徽章
{
x: 80,
y: 750,
width: 240,
x: 50,
y: 800,
width: 300,
height: 240,
url: mockData.value.level.logo,
url: currentMockData.value.level.logo,
zIndex: 2
},
// 小程序码
{
x: 50,
y: 1100,
width: 200,
height: 200,
url: mockData.value.qrcode,
width: 180,
height: 180,
url: currentMockData.value.qrcode,
zIndex: 1
}
],
texts: [
// 家庭名称
{
x: 140,
y: 50,
text: mockData.value.family.name,
x: 40,
y: 190,
text: currentMockData.value.family.name,
fontSize: 36,
color: '#ffffff',
fontWeight: 'bold',
textAlign: 'left',
shadowColor: 'rgba(0, 0, 0, 0.6)',
shadowOffsetX: 2,
shadowOffsetY: 2,
shadowBlur: 4,
zIndex: 2
},
// 家庭描述
{
x: 140,
y: 100,
text: mockData.value.family.description,
fontSize: 26,
x: 40,
y: 250,
text: currentMockData.value.family.description,
fontSize: 28,
color: '#ffffff',
textAlign: 'left',
shadowColor: 'rgba(0, 0, 0, 0.6)',
shadowOffsetX: 2,
shadowOffsetY: 2,
shadowBlur: 4,
zIndex: 2
},
// 关卡描述
{
x: 280,
y: 1125,
text: currentMockData.value.level.name,
fontSize: 35,
color: '#333333',
lineHeight: 40,
lineNum: 2,
width: 400,
textAlign: 'left',
zIndex: 1
},
// 小程序码描述
{
x: 280,
y: 1150,
text: mockData.value.qrcodeDesc,
y: 1200,
text: currentMockData.value.qrcodeDesc,
fontSize: 28,
color: '#333333',
lineHeight: 40,
......@@ -363,15 +434,15 @@ const posterConfig = computed(() => {
zIndex: 0
},
// 用户信息背景遮罩
{
x: 30,
y: 30,
width: 520,
height: 120,
backgroundColor: 'rgba(0,0,0,0.3)',
borderRadius: 10,
zIndex: 1
}
// {
// x: 30,
// y: 180,
// width: 450,
// height: 140,
// backgroundColor: 'rgba(0,0,0,0.3)',
// borderRadius: 10,
// zIndex: 1
// }
]
}
})
......