feat(PosterBuilder): 添加文字描边和阴影效果支持并完善海报数据
为_drawSingleText函数添加描边和阴影效果支持,提升文字显示效果 重构PosterCheckin页面,将mock数据整合到海报列表中并调整海报配置 移除不必要的UI元素,优化海报布局和样式
Showing
2 changed files
with
169 additions
and
69 deletions
| ... | @@ -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 | }) | ... | ... |
-
Please register or login to post a comment