hookehuyr

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

为_drawSingleText函数添加描边和阴影效果支持,提升文字显示效果
重构PosterCheckin页面,将mock数据整合到海报列表中并调整海报配置
移除不必要的UI元素,优化海报布局和样式
...@@ -158,7 +158,13 @@ export function _drawSingleText(drawData, drawOptions) { ...@@ -158,7 +158,13 @@ export function _drawSingleText(drawData, drawOptions) {
158 lineHeight = 0, 158 lineHeight = 0,
159 fontWeight = 'normal', 159 fontWeight = 'normal',
160 fontStyle = 'normal', 160 fontStyle = 'normal',
161 - fontFamily = 'sans-serif' 161 + fontFamily = 'sans-serif',
162 + strokeStyle,
163 + lineWidth = 0,
164 + shadowColor,
165 + shadowOffsetX = 0,
166 + shadowOffsetY = 0,
167 + shadowBlur = 0
162 } = drawData; 168 } = drawData;
163 const { ctx } = drawOptions; 169 const { ctx } = drawOptions;
164 // 画笔初始化 170 // 画笔初始化
...@@ -214,13 +220,36 @@ export function _drawSingleText(drawData, drawOptions) { ...@@ -214,13 +220,36 @@ export function _drawSingleText(drawData, drawOptions) {
214 } 220 }
215 221
216 // 按行渲染文字 222 // 按行渲染文字
217 - textArr.forEach((item, index) => 223 + textArr.forEach((item, index) => {
218 - ctx.fillText( 224 + const textX = getTextX(textAlign, x, width); // 根据文本对齐方式和宽度确定 x 坐标
219 - item, 225 + const textY = y + (lineHeight || fontSize) * index; // 根据行数、行高 || 字体大小确定 y 坐标
220 - getTextX(textAlign, x, width), // 根据文本对齐方式和宽度确定 x 坐标 226 +
221 - y + (lineHeight || fontSize) * index // 根据行数、行高 || 字体大小确定 y 坐标 227 + // 设置阴影效果
222 - ) 228 + if (shadowColor) {
223 - ); 229 + ctx.shadowColor = shadowColor;
230 + ctx.shadowOffsetX = shadowOffsetX;
231 + ctx.shadowOffsetY = shadowOffsetY;
232 + ctx.shadowBlur = shadowBlur;
233 + }
234 +
235 + // 如果有描边设置,先绘制描边
236 + if (strokeStyle && lineWidth > 0) {
237 + ctx.strokeStyle = strokeStyle;
238 + ctx.lineWidth = lineWidth;
239 + ctx.strokeText(item, textX, textY);
240 + }
241 +
242 + // 绘制填充文字
243 + ctx.fillText(item, textX, textY);
244 +
245 + // 清除阴影设置,避免影响后续绘制
246 + if (shadowColor) {
247 + ctx.shadowColor = 'transparent';
248 + ctx.shadowOffsetX = 0;
249 + ctx.shadowOffsetY = 0;
250 + ctx.shadowBlur = 0;
251 + }
252 + });
224 ctx.restore(); 253 ctx.restore();
225 254
226 // 文本修饰,下划线、删除线什么的 255 // 文本修饰,下划线、删除线什么的
......
...@@ -37,9 +37,9 @@ ...@@ -37,9 +37,9 @@
37 class="w-full h-full" 37 class="w-full h-full"
38 /> 38 />
39 <!-- 打卡点标题 --> 39 <!-- 打卡点标题 -->
40 - <view class="absolute bottom-2 left-2 bg-blue-500 text-white text-xs px-2 py-1 rounded"> 40 + <!-- <view class="absolute bottom-2 left-2 bg-blue-500 text-white text-xs px-2 py-1 rounded">
41 {{ posterList[currentPosterIndex]?.title || '海报生成中' }} 41 {{ posterList[currentPosterIndex]?.title || '海报生成中' }}
42 - </view> 42 + </view> -->
43 <!-- 点击预览提示 --> 43 <!-- 点击预览提示 -->
44 <view @tap="previewPoster" class="absolute bottom-2 right-2 bg-black bg-opacity-50 text-white text-xs px-2 py-1 rounded"> 44 <view @tap="previewPoster" class="absolute bottom-2 right-2 bg-black bg-opacity-50 text-white text-xs px-2 py-1 rounded">
45 点击预览 45 点击预览
...@@ -146,6 +146,9 @@ import { Left, Right } from '@nutui/icons-vue-taro' ...@@ -146,6 +146,9 @@ import { Left, Right } from '@nutui/icons-vue-taro'
146 import PosterBuilder from '@/components/PosterBuilder/index.vue' 146 import PosterBuilder from '@/components/PosterBuilder/index.vue'
147 import BASE_URL from '@/utils/config' 147 import BASE_URL from '@/utils/config'
148 148
149 +// 默认背景图
150 +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'
151 +
149 // 页面状态 152 // 页面状态
150 const posterPath = ref('') // 生成的海报路径 153 const posterPath = ref('') // 生成的海报路径
151 const backgroundImage = ref('') // 用户上传的背景图 154 const backgroundImage = ref('') // 用户上传的背景图
...@@ -179,28 +182,85 @@ const activityInfo = ref({ ...@@ -179,28 +182,85 @@ const activityInfo = ref({
179 182
180 // activityInfo.value = {}; 183 // activityInfo.value = {};
181 184
182 -// 海报列表数据 - 模拟不同状态,实际使用时从API获取 185 +// 海报数据列表 - 合并海报基础信息和内容数据,实际使用时从API获取
183 const posterList = ref([ 186 const posterList = ref([
184 { 187 {
185 id: 1, 188 id: 1,
186 title: '起点签到', 189 title: '起点签到',
187 path: '', 190 path: '',
188 checkPointId: 1, 191 checkPointId: 1,
189 - backgroundImage: 'https://cdn.ipadbiz.cn/lls_prog/images/welcome_1.png' 192 + 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',
193 + // 海报内容数据
194 + user: {
195 + avatar: 'https://cdn.ipadbiz.cn/icon/tou@2x.png',
196 + nickname: '张大爷'
197 + },
198 + family: {
199 + name: '幸福之家',
200 + description: '一家人整整齐齐最重要'
201 + },
202 + activity: {
203 + logo: 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%B5%B7%E6%8A%A5%E5%B7%A6%E4%B8%8A%E8%A7%92logo.png',
204 + name: '南京路时尚citywalk'
205 + },
206 + level: {
207 + logo: 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%B5%B7%E6%8A%A5%E5%8F%B3%E4%B8%8B%E8%A7%92icon.png',
208 + name: '第一关卡'
209 + },
210 + qrcode: 'https://cdn.ipadbiz.cn/space/068a790496c87cb8d2ed6e551401c544.png',
211 + qrcodeDesc: '长按识别,来,我们一起打卡!'
190 }, 212 },
191 { 213 {
192 id: 2, 214 id: 2,
193 title: '商圈探索', 215 title: '商圈探索',
194 path: '', 216 path: '',
195 checkPointId: 2, 217 checkPointId: 2,
196 - backgroundImage: 'https://cdn.ipadbiz.cn/lls_prog/images/welcome_1.png' 218 + 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',
219 + // 海报内容数据
220 + user: {
221 + avatar: 'https://cdn.ipadbiz.cn/icon/tou@2x.png',
222 + nickname: '李奶奶'
223 + },
224 + family: {
225 + name: '温馨小家',
226 + description: '健康快乐每一天'
227 + },
228 + activity: {
229 + logo: 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%B5%B7%E6%8A%A5%E5%B7%A6%E4%B8%8A%E8%A7%92logo.png',
230 + name: '南京路时尚citywalk'
231 + },
232 + level: {
233 + logo: 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%B5%B7%E6%8A%A5%E5%8F%B3%E4%B8%8B%E8%A7%92icon.png',
234 + name: '第二关卡'
235 + },
236 + qrcode: 'https://cdn.ipadbiz.cn/space/068a790496c87cb8d2ed6e551401c544.png',
237 + qrcodeDesc: '扫码加入我们的队伍!'
197 }, 238 },
198 { 239 {
199 id: 3, 240 id: 3,
200 title: '文化体验', 241 title: '文化体验',
201 path: '', 242 path: '',
202 checkPointId: 3, 243 checkPointId: 3,
203 - backgroundImage: 'https://cdn.ipadbiz.cn/lls_prog/images/welcome_1.png' 244 + 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',
245 + // 海报内容数据
246 + user: {
247 + avatar: 'https://cdn.ipadbiz.cn/icon/tou@2x.png',
248 + nickname: '王叔叔'
249 + },
250 + family: {
251 + name: '快乐大家庭',
252 + description: '运动让我们更年轻'
253 + },
254 + activity: {
255 + logo: 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%B5%B7%E6%8A%A5%E5%B7%A6%E4%B8%8A%E8%A7%92logo.png',
256 + name: '南京路时尚citywalk'
257 + },
258 + level: {
259 + logo: 'https://cdn.ipadbiz.cn/lls_prog/images/%E6%B5%B7%E6%8A%A5%E5%8F%B3%E4%B8%8B%E8%A7%92icon.png',
260 + name: '第三关卡'
261 + },
262 + qrcode: 'https://cdn.ipadbiz.cn/space/068a790496c87cb8d2ed6e551401c544.png',
263 + qrcodeDesc: '一起来挑战吧!'
204 } 264 }
205 ]) 265 ])
206 266
...@@ -233,30 +293,20 @@ const currentPoster = computed(() => { ...@@ -233,30 +293,20 @@ const currentPoster = computed(() => {
233 return posterList.value[currentPosterIndex.value] || { path: '', title: '' } 293 return posterList.value[currentPosterIndex.value] || { path: '', title: '' }
234 }) 294 })
235 295
236 -// Mock数据 296 +// 当前海报的内容数据
237 -const mockData = ref({ 297 +const currentMockData = computed(() => {
238 - user: { 298 + const currentPoster = posterList.value[currentPosterIndex.value]
239 - avatar: 'https://cdn.ipadbiz.cn/icon/tou@2x.png', 299 + if (!currentPoster) return null
240 - nickname: '张大爷'
241 - },
242 - family: {
243 - name: '幸福之家',
244 - description: '一家人整整齐齐最重要'
245 - },
246 - activity: {
247 - logo: 'https://cdn.ipadbiz.cn/lls_prog/images/activity-logo.png',
248 - name: '南京路时尚citywalk'
249 - },
250 - level: {
251 - logo: 'https://cdn.ipadbiz.cn/lls_prog/images/level-badge.png',
252 - name: '第一关卡'
253 - },
254 - qrcode: 'https://cdn.ipadbiz.cn/space/068a790496c87cb8d2ed6e551401c544.png',
255 - qrcodeDesc: '长按识别小程序码\n送我一朵小红花为我助力吧~'
256 -})
257 300
258 -// 默认背景图 301 + return {
259 -const defaultBackground = 'https://cdn.ipadbiz.cn/lls_prog/images/welcome_1.png' 302 + user: currentPoster.user,
303 + family: currentPoster.family,
304 + activity: currentPoster.activity,
305 + level: currentPoster.level,
306 + qrcode: currentPoster.qrcode,
307 + qrcodeDesc: currentPoster.qrcodeDesc
308 + }
309 +})
260 310
261 // 海报配置 311 // 海报配置
262 const posterConfig = computed(() => { 312 const posterConfig = computed(() => {
...@@ -282,67 +332,88 @@ const posterConfig = computed(() => { ...@@ -282,67 +332,88 @@ const posterConfig = computed(() => {
282 { 332 {
283 x: 40, 333 x: 40,
284 y: 40, 334 y: 40,
285 - width: 80, 335 + width: 130,
286 - height: 80, 336 + height: 130,
287 - url: mockData.value.user.avatar, 337 + url: currentMockData.value.user.avatar,
288 - borderRadius: 40, 338 + borderRadius: 65,
289 zIndex: 2 339 zIndex: 2
290 }, 340 },
291 // 活动logo 341 // 活动logo
292 { 342 {
293 - x: 580, 343 + x: 500,
294 y: 40, 344 y: 40,
295 - width: 120, 345 + width: 200,
296 height: 80, 346 height: 80,
297 - url: mockData.value.activity.logo, 347 + url: currentMockData.value.activity.logo,
298 zIndex: 2 348 zIndex: 2
299 }, 349 },
300 // 关卡徽章 350 // 关卡徽章
301 { 351 {
302 - x: 80, 352 + x: 50,
303 - y: 750, 353 + y: 800,
304 - width: 240, 354 + width: 300,
305 height: 240, 355 height: 240,
306 - url: mockData.value.level.logo, 356 + url: currentMockData.value.level.logo,
307 zIndex: 2 357 zIndex: 2
308 }, 358 },
309 // 小程序码 359 // 小程序码
310 { 360 {
311 x: 50, 361 x: 50,
312 y: 1100, 362 y: 1100,
313 - width: 200, 363 + width: 180,
314 - height: 200, 364 + height: 180,
315 - url: mockData.value.qrcode, 365 + url: currentMockData.value.qrcode,
316 zIndex: 1 366 zIndex: 1
317 } 367 }
318 ], 368 ],
319 texts: [ 369 texts: [
320 // 家庭名称 370 // 家庭名称
321 { 371 {
322 - x: 140, 372 + x: 40,
323 - y: 50, 373 + y: 190,
324 - text: mockData.value.family.name, 374 + text: currentMockData.value.family.name,
325 fontSize: 36, 375 fontSize: 36,
326 color: '#ffffff', 376 color: '#ffffff',
327 fontWeight: 'bold', 377 fontWeight: 'bold',
328 textAlign: 'left', 378 textAlign: 'left',
379 + shadowColor: 'rgba(0, 0, 0, 0.6)',
380 + shadowOffsetX: 2,
381 + shadowOffsetY: 2,
382 + shadowBlur: 4,
329 zIndex: 2 383 zIndex: 2
330 }, 384 },
331 // 家庭描述 385 // 家庭描述
332 { 386 {
333 - x: 140, 387 + x: 40,
334 - y: 100, 388 + y: 250,
335 - text: mockData.value.family.description, 389 + text: currentMockData.value.family.description,
336 - fontSize: 26, 390 + fontSize: 28,
337 color: '#ffffff', 391 color: '#ffffff',
338 textAlign: 'left', 392 textAlign: 'left',
393 + shadowColor: 'rgba(0, 0, 0, 0.6)',
394 + shadowOffsetX: 2,
395 + shadowOffsetY: 2,
396 + shadowBlur: 4,
339 zIndex: 2 397 zIndex: 2
340 }, 398 },
399 + // 关卡描述
400 + {
401 + x: 280,
402 + y: 1125,
403 + text: currentMockData.value.level.name,
404 + fontSize: 35,
405 + color: '#333333',
406 + lineHeight: 40,
407 + lineNum: 2,
408 + width: 400,
409 + textAlign: 'left',
410 + zIndex: 1
411 + },
341 // 小程序码描述 412 // 小程序码描述
342 { 413 {
343 x: 280, 414 x: 280,
344 - y: 1150, 415 + y: 1200,
345 - text: mockData.value.qrcodeDesc, 416 + text: currentMockData.value.qrcodeDesc,
346 fontSize: 28, 417 fontSize: 28,
347 color: '#333333', 418 color: '#333333',
348 lineHeight: 40, 419 lineHeight: 40,
...@@ -363,15 +434,15 @@ const posterConfig = computed(() => { ...@@ -363,15 +434,15 @@ const posterConfig = computed(() => {
363 zIndex: 0 434 zIndex: 0
364 }, 435 },
365 // 用户信息背景遮罩 436 // 用户信息背景遮罩
366 - { 437 + // {
367 - x: 30, 438 + // x: 30,
368 - y: 30, 439 + // y: 180,
369 - width: 520, 440 + // width: 450,
370 - height: 120, 441 + // height: 140,
371 - backgroundColor: 'rgba(0,0,0,0.3)', 442 + // backgroundColor: 'rgba(0,0,0,0.3)',
372 - borderRadius: 10, 443 + // borderRadius: 10,
373 - zIndex: 1 444 + // zIndex: 1
374 - } 445 + // }
375 ] 446 ]
376 } 447 }
377 }) 448 })
......