feat(ActivitiesCover): 添加活动分享功能及海报生成组件
- 新增分享按钮及分享弹窗组件 - 实现海报生成功能,支持保存至相册 - 添加分享选项包括微信分享和海报生成 - 优化图片下载工具函数,增加重试机制 - 更新babel配置支持TypeScript - 添加相关SVG图标资源
Showing
13 changed files
with
685 additions
and
14 deletions
| ... | @@ -37,6 +37,7 @@ | ... | @@ -37,6 +37,7 @@ |
| 37 | "license": "MIT", | 37 | "license": "MIT", |
| 38 | "dependencies": { | 38 | "dependencies": { |
| 39 | "@babel/runtime": "^7.7.7", | 39 | "@babel/runtime": "^7.7.7", |
| 40 | + "@nutui/icons-vue": "^0.1.1", | ||
| 40 | "@nutui/icons-vue-taro": "^0.0.9", | 41 | "@nutui/icons-vue-taro": "^0.0.9", |
| 41 | "@nutui/nutui-taro": "^4.3.13", | 42 | "@nutui/nutui-taro": "^4.3.13", |
| 42 | "@tarojs/components": "4.1.2", | 43 | "@tarojs/components": "4.1.2", |
| ... | @@ -64,6 +65,7 @@ | ... | @@ -64,6 +65,7 @@ |
| 64 | "@tarojs/cli": "4.1.2", | 65 | "@tarojs/cli": "4.1.2", |
| 65 | "@tarojs/taro-loader": "4.1.2", | 66 | "@tarojs/taro-loader": "4.1.2", |
| 66 | "@tarojs/webpack5-runner": "4.1.2", | 67 | "@tarojs/webpack5-runner": "4.1.2", |
| 68 | + "@types/node": "^24.3.0", | ||
| 67 | "@types/webpack-env": "^1.13.6", | 69 | "@types/webpack-env": "^1.13.6", |
| 68 | "@vue/babel-plugin-jsx": "^1.0.6", | 70 | "@vue/babel-plugin-jsx": "^1.0.6", |
| 69 | "@vue/compiler-sfc": "^3.0.0", | 71 | "@vue/compiler-sfc": "^3.0.0", |
| ... | @@ -75,6 +77,7 @@ | ... | @@ -75,6 +77,7 @@ |
| 75 | "postcss": "^8.5.6", | 77 | "postcss": "^8.5.6", |
| 76 | "style-loader": "1.3.0", | 78 | "style-loader": "1.3.0", |
| 77 | "tailwindcss": "^3.4.0", | 79 | "tailwindcss": "^3.4.0", |
| 80 | + "typescript": "^5.9.2", | ||
| 78 | "unplugin-vue-components": "^0.26.0", | 81 | "unplugin-vue-components": "^0.26.0", |
| 79 | "vue-loader": "^17.0.0", | 82 | "vue-loader": "^17.0.0", |
| 80 | "weapp-tailwindcss": "^4.1.10", | 83 | "weapp-tailwindcss": "^4.1.10", | ... | ... |
pnpm-lock.yaml
0 → 100644
This diff could not be displayed because it is too large.
src/assets/images/icon/line.svg
0 → 100644
| 1 | +<svg width="949" height="108" viewBox="0 0 949 108" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
| 2 | + <rect width="949" height="2" y="53" fill="#E5E7EB"/> | ||
| 3 | + <circle cx="100" cy="54" r="8" fill="#3B82F6"/> | ||
| 4 | + <circle cx="200" cy="54" r="6" fill="#10B981"/> | ||
| 5 | + <circle cx="300" cy="54" r="8" fill="#F59E0B"/> | ||
| 6 | + <circle cx="400" cy="54" r="6" fill="#EF4444"/> | ||
| 7 | + <circle cx="500" cy="54" r="8" fill="#8B5CF6"/> | ||
| 8 | + <circle cx="600" cy="54" r="6" fill="#06B6D4"/> | ||
| 9 | + <circle cx="700" cy="54" r="8" fill="#84CC16"/> | ||
| 10 | + <circle cx="800" cy="54" r="6" fill="#F97316"/> | ||
| 11 | +</svg> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/assets/images/icon/location.svg
0 → 100644
| 1 | +<svg width="35" height="40" viewBox="0 0 35 40" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
| 2 | + <path d="M17.5 0C7.835 0 0 7.835 0 17.5C0 30.625 17.5 40 17.5 40S35 30.625 35 17.5C35 7.835 27.165 0 17.5 0Z" fill="#4CAF50"/> | ||
| 3 | + <circle cx="17.5" cy="17.5" r="7" fill="#fff"/> | ||
| 4 | +</svg> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/assets/images/icon/share.svg
0 → 100644
| 1 | +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
| 2 | + <path d="M18 16.08C17.24 16.08 16.56 16.38 16.04 16.85L8.91 12.7C8.96 12.47 9 12.24 9 12C9 11.76 8.96 11.53 8.91 11.3L15.96 7.19C16.5 7.69 17.21 8 18 8C19.66 8 21 6.66 21 5C21 3.34 19.66 2 18 2C16.34 2 15 3.34 15 5C15 5.24 15.04 5.47 15.09 5.7L8.04 9.81C7.5 9.31 6.79 9 6 9C4.34 9 3 10.34 3 12C3 13.66 4.34 15 6 15C6.79 15 7.5 14.69 8.04 14.19L15.16 18.34C15.11 18.55 15.08 18.77 15.08 19C15.08 20.61 16.39 21.92 18 21.92C19.61 21.92 20.92 20.61 20.92 19C20.92 17.39 19.61 16.08 18 16.08Z" fill="currentColor"/> | ||
| 3 | +</svg> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/assets/images/icon/time.svg
0 → 100644
| 1 | +<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
| 2 | + <circle cx="20" cy="20" r="18" fill="#FF6B35" stroke="#fff" stroke-width="2"/> | ||
| 3 | + <path d="M20 8v12l8 4" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> | ||
| 4 | +</svg> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| 1 | /* eslint-disable prefer-destructuring */ | 1 | /* eslint-disable prefer-destructuring */ |
| 2 | -import Taro, { CanvasContext, CanvasGradient } from '@tarojs/taro'; | 2 | +import Taro from '@tarojs/taro' |
| 3 | 3 | ||
| 4 | -declare const wx: any; | 4 | +// 全局变量声明 |
| 5 | +// eslint-disable-next-line no-undef | ||
| 6 | +const wx = globalThis.wx || {}; | ||
| 5 | 7 | ||
| 6 | /** | 8 | /** |
| 7 | * @description 生成随机字符串 | 9 | * @description 生成随机字符串 |
| ... | @@ -128,28 +130,70 @@ export const toRpx = (px, factor = getFactor()) => | ... | @@ -128,28 +130,70 @@ export const toRpx = (px, factor = getFactor()) => |
| 128 | /** | 130 | /** |
| 129 | * 下载图片资源 | 131 | * 下载图片资源 |
| 130 | * @param { string } url | 132 | * @param { string } url |
| 133 | + * @param { number } retryCount - 重试次数 | ||
| 131 | * @returns { Promise } | 134 | * @returns { Promise } |
| 132 | */ | 135 | */ |
| 133 | -export function downImage(url) { | 136 | +export function downImage(url, retryCount = 2) { |
| 134 | - return new Promise<string>((resolve, reject) => { | 137 | + return new Promise((resolve, reject) => { |
| 135 | // eslint-disable-next-line no-undef | 138 | // eslint-disable-next-line no-undef |
| 136 | if (/^http/.test(url) && !new RegExp(wx.env.USER_DATA_PATH).test(url)) { | 139 | if (/^http/.test(url) && !new RegExp(wx.env.USER_DATA_PATH).test(url)) { |
| 137 | // wx.env.USER_DATA_PATH 文件系统中的用户目录路径 | 140 | // wx.env.USER_DATA_PATH 文件系统中的用户目录路径 |
| 141 | + const attemptDownload = (attempt = 0) => { | ||
| 138 | Taro.downloadFile({ | 142 | Taro.downloadFile({ |
| 139 | url: mapHttpToHttps(url), | 143 | url: mapHttpToHttps(url), |
| 144 | + timeout: 10000, // 设置10秒超时 | ||
| 140 | success: (res) => { | 145 | success: (res) => { |
| 141 | if (res.statusCode === 200) { | 146 | if (res.statusCode === 200) { |
| 142 | resolve(res.tempFilePath); | 147 | resolve(res.tempFilePath); |
| 143 | } else { | 148 | } else { |
| 144 | - console.log('下载失败', res); | 149 | + // console.log('下载失败', res); |
| 150 | + if (attempt < retryCount) { | ||
| 151 | + // console.log(`重试下载图片,第${attempt + 1}次重试`); | ||
| 152 | + setTimeout(() => attemptDownload(attempt + 1), 1000); | ||
| 153 | + } else { | ||
| 145 | reject(res); | 154 | reject(res); |
| 146 | } | 155 | } |
| 156 | + } | ||
| 147 | }, | 157 | }, |
| 148 | fail(err) { | 158 | fail(err) { |
| 149 | - console.log('下载失败了', err); | 159 | + // console.log('下载失败了', err); |
| 160 | + // 如果是代理连接问题,尝试使用原始URL | ||
| 161 | + if (err.errMsg && err.errMsg.includes('127.0.0.1:7890')) { | ||
| 162 | + // console.log('检测到代理问题,尝试使用原始URL'); | ||
| 163 | + if (attempt < retryCount) { | ||
| 164 | + setTimeout(() => { | ||
| 165 | + Taro.downloadFile({ | ||
| 166 | + url: url, // 使用原始URL,不转换https | ||
| 167 | + timeout: 10000, | ||
| 168 | + success: (res) => { | ||
| 169 | + if (res.statusCode === 200) { | ||
| 170 | + resolve(res.tempFilePath); | ||
| 171 | + } else { | ||
| 172 | + reject(res); | ||
| 173 | + } | ||
| 174 | + }, | ||
| 175 | + fail: () => { | ||
| 176 | + if (attempt + 1 < retryCount) { | ||
| 177 | + attemptDownload(attempt + 1); | ||
| 178 | + } else { | ||
| 150 | reject(err); | 179 | reject(err); |
| 151 | } | 180 | } |
| 181 | + } | ||
| 182 | + }); | ||
| 183 | + }, 1000); | ||
| 184 | + } else { | ||
| 185 | + reject(err); | ||
| 186 | + } | ||
| 187 | + } else if (attempt < retryCount) { | ||
| 188 | + // console.log(`重试下载图片,第${attempt + 1}次重试`); | ||
| 189 | + setTimeout(() => attemptDownload(attempt + 1), 1000); | ||
| 190 | + } else { | ||
| 191 | + reject(err); | ||
| 192 | + } | ||
| 193 | + } | ||
| 152 | }); | 194 | }); |
| 195 | + }; | ||
| 196 | + attemptDownload(); | ||
| 153 | } else { | 197 | } else { |
| 154 | resolve(url); // 支持本地地址 | 198 | resolve(url); // 支持本地地址 |
| 155 | } | 199 | } |
| ... | @@ -191,7 +235,7 @@ export const getImageInfo = (item, index) => | ... | @@ -191,7 +235,7 @@ export const getImageInfo = (item, index) => |
| 191 | borderColor: item.borderColor, | 235 | borderColor: item.borderColor, |
| 192 | borderRadiusGroup: item.borderRadiusGroup, | 236 | borderRadiusGroup: item.borderRadiusGroup, |
| 193 | zIndex: typeof zIndex !== 'undefined' ? zIndex : index, | 237 | zIndex: typeof zIndex !== 'undefined' ? zIndex : index, |
| 194 | - imgPath: url, | 238 | + imgPath: imgPath, // 使用下载后的临时文件路径 |
| 195 | sx, | 239 | sx, |
| 196 | sy, | 240 | sy, |
| 197 | sw: imgWidth - sx * 2, | 241 | sw: imgWidth - sx * 2, |
| ... | @@ -204,7 +248,7 @@ export const getImageInfo = (item, index) => | ... | @@ -204,7 +248,7 @@ export const getImageInfo = (item, index) => |
| 204 | resolve(result); | 248 | resolve(result); |
| 205 | }) | 249 | }) |
| 206 | .catch((err) => { | 250 | .catch((err) => { |
| 207 | - console.log('读取图片信息失败', err); | 251 | + // console.log('读取图片信息失败', err); |
| 208 | reject(err); | 252 | reject(err); |
| 209 | }) | 253 | }) |
| 210 | ); | 254 | ); |
| ... | @@ -222,7 +266,7 @@ export const getImageInfo = (item, index) => | ... | @@ -222,7 +266,7 @@ export const getImageInfo = (item, index) => |
| 222 | */ | 266 | */ |
| 223 | // TODO: 待优化, 支持所有角度,多个颜色的线性渐变 | 267 | // TODO: 待优化, 支持所有角度,多个颜色的线性渐变 |
| 224 | export function getLinearColor( | 268 | export function getLinearColor( |
| 225 | - ctx: CanvasContext, | 269 | + ctx, |
| 226 | color, | 270 | color, |
| 227 | startX, | 271 | startX, |
| 228 | startY, | 272 | startY, |
| ... | @@ -238,7 +282,7 @@ export function getLinearColor( | ... | @@ -238,7 +282,7 @@ export function getLinearColor( |
| 238 | console.warn('坐标或者宽高只支持数字'); | 282 | console.warn('坐标或者宽高只支持数字'); |
| 239 | return color; | 283 | return color; |
| 240 | } | 284 | } |
| 241 | - let grd: CanvasGradient | string = color; | 285 | + let grd = color; |
| 242 | if (color.includes('linear-gradient')) { | 286 | if (color.includes('linear-gradient')) { |
| 243 | // fillStyle 不支持线性渐变色 | 287 | // fillStyle 不支持线性渐变色 |
| 244 | const colorList = color.match(/\((\d+)deg,\s(.+)\s\d+%,\s(.+)\s\d+%/); | 288 | const colorList = color.match(/\((\d+)deg,\s(.+)\s\d+%,\s(.+)\s\d+%/); |
| ... | @@ -263,8 +307,8 @@ export function getLinearColor( | ... | @@ -263,8 +307,8 @@ export function getLinearColor( |
| 263 | } else { | 307 | } else { |
| 264 | throw new Error('只支持0 <= 颜色弧度 <= 180'); | 308 | throw new Error('只支持0 <= 颜色弧度 <= 180'); |
| 265 | } | 309 | } |
| 266 | - (grd as CanvasGradient).addColorStop(0, color1); | 310 | + grd.addColorStop(0, color1); |
| 267 | - (grd as CanvasGradient).addColorStop(1, color2); | 311 | + grd.addColorStop(1, color2); |
| 268 | } | 312 | } |
| 269 | return grd; | 313 | return grd; |
| 270 | } | 314 | } | ... | ... |
src/images/icon/share.svg
0 → 100644
| 1 | +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
| 2 | + <path d="M18 16.08C17.24 16.08 16.56 16.38 16.04 16.85L8.91 12.7C8.96 12.47 9 12.24 9 12C9 11.76 8.96 11.53 8.91 11.3L15.96 7.19C16.5 7.69 17.21 8 18 8C19.66 8 21 6.66 21 5C21 3.34 19.66 2 18 2C16.34 2 15 3.34 15 5C15 5.24 15.04 5.47 15.09 5.7L8.04 9.81C7.5 9.31 6.79 9 6 9C4.34 9 3 10.34 3 12C3 13.66 4.34 15 6 15C6.79 15 7.5 14.69 8.04 14.19L15.16 18.34C15.11 18.55 15.08 18.77 15.08 19C15.08 20.61 16.39 21.92 18 21.92C19.61 21.92 20.92 20.61 20.92 19C20.92 17.39 19.61 16.08 18 16.08Z" fill="#666666"/> | ||
| 3 | +</svg> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
src/images/line.svg
0 → 100644
| 1 | +<svg width="949" height="108" viewBox="0 0 949 108" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
| 2 | + <defs> | ||
| 3 | + <pattern id="dots" patternUnits="userSpaceOnUse" width="20" height="20"> | ||
| 4 | + <circle cx="10" cy="10" r="2" fill="#E5E5E5"/> | ||
| 5 | + </pattern> | ||
| 6 | + </defs> | ||
| 7 | + <rect width="949" height="108" fill="url(#dots)"/> | ||
| 8 | + <line x1="0" y1="54" x2="949" y2="54" stroke="#CCCCCC" stroke-width="2" stroke-dasharray="10,5"/> | ||
| 9 | +</svg> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| ... | @@ -55,6 +55,27 @@ | ... | @@ -55,6 +55,27 @@ |
| 55 | } | 55 | } |
| 56 | } | 56 | } |
| 57 | 57 | ||
| 58 | +// 分享按钮 | ||
| 59 | +.share-button { | ||
| 60 | + position: absolute; | ||
| 61 | + top: 40rpx; | ||
| 62 | + right: 40rpx; | ||
| 63 | + width: 80rpx; | ||
| 64 | + height: 80rpx; | ||
| 65 | + background-color: rgba(0, 0, 0, 0.5); | ||
| 66 | + border-radius: 50%; | ||
| 67 | + display: flex; | ||
| 68 | + align-items: center; | ||
| 69 | + justify-content: center; | ||
| 70 | + color: white; | ||
| 71 | + font-size: 32rpx; | ||
| 72 | + z-index: 10; | ||
| 73 | + | ||
| 74 | + &:active { | ||
| 75 | + background-color: rgba(0, 0, 0, 0.7); | ||
| 76 | + } | ||
| 77 | +} | ||
| 78 | + | ||
| 58 | // 详情区域 | 79 | // 详情区域 |
| 59 | .details-section { | 80 | .details-section { |
| 60 | flex: 1; | 81 | flex: 1; |
| ... | @@ -189,7 +210,112 @@ | ... | @@ -189,7 +210,112 @@ |
| 189 | box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3); | 210 | box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3); |
| 190 | } | 211 | } |
| 191 | } | 212 | } |
| 213 | +} | ||
| 214 | + | ||
| 215 | +// 弹窗样式 | ||
| 216 | +.share-popup { | ||
| 217 | + .nut-popup__content { | ||
| 218 | + border-radius: 24rpx 24rpx 0 0; | ||
| 219 | + padding: 40rpx; | ||
| 220 | + } | ||
| 221 | +} | ||
| 222 | + | ||
| 223 | +.share-title { | ||
| 224 | + font-size: 32rpx; | ||
| 225 | + font-weight: bold; | ||
| 226 | + text-align: center; | ||
| 227 | + margin-bottom: 40rpx; | ||
| 228 | + color: #333; | ||
| 229 | +} | ||
| 192 | 230 | ||
| 231 | +.share-options { | ||
| 232 | + display: flex; | ||
| 233 | + justify-content: space-around; | ||
| 234 | + margin-bottom: 40rpx; | ||
| 235 | +} | ||
| 236 | + | ||
| 237 | +.share-option { | ||
| 238 | + display: flex; | ||
| 239 | + flex-direction: column; | ||
| 240 | + align-items: center; | ||
| 241 | + padding: 20rpx; | ||
| 242 | + border-radius: 12rpx; | ||
| 243 | + | ||
| 244 | + &:active { | ||
| 245 | + background-color: #f5f5f5; | ||
| 246 | + } | ||
| 247 | +} | ||
| 248 | + | ||
| 249 | +.share-icon { | ||
| 250 | + width: 80rpx; | ||
| 251 | + height: 80rpx; | ||
| 252 | + margin-bottom: 16rpx; | ||
| 253 | + border-radius: 12rpx; | ||
| 254 | + background-color: #1890ff; | ||
| 255 | + display: flex; | ||
| 256 | + align-items: center; | ||
| 257 | + justify-content: center; | ||
| 258 | + color: white; | ||
| 259 | + font-size: 36rpx; | ||
| 260 | +} | ||
| 261 | + | ||
| 262 | +.share-text { | ||
| 263 | + font-size: 24rpx; | ||
| 264 | + color: #666; | ||
| 265 | +} | ||
| 266 | + | ||
| 267 | +.cancel-button { | ||
| 268 | + width: 100%; | ||
| 269 | + height: 88rpx; | ||
| 270 | + border-radius: 44rpx; | ||
| 271 | + font-size: 32rpx; | ||
| 272 | + background-color: #f5f5f5; | ||
| 273 | + color: #666; | ||
| 274 | + border: none; | ||
| 275 | +} | ||
| 276 | + | ||
| 277 | +// 海报预览弹窗 | ||
| 278 | +.poster-preview-popup { | ||
| 279 | + .nut-popup__content { | ||
| 280 | + width: 90%; | ||
| 281 | + max-width: 600rpx; | ||
| 282 | + border-radius: 24rpx; | ||
| 283 | + padding: 40rpx; | ||
| 284 | + background-color: white; | ||
| 285 | + } | ||
| 286 | +} | ||
| 287 | + | ||
| 288 | +.poster-preview { | ||
| 289 | + width: 100%; | ||
| 290 | + border-radius: 12rpx; | ||
| 291 | + margin-bottom: 40rpx; | ||
| 292 | +} | ||
| 293 | + | ||
| 294 | +.preview-actions { | ||
| 295 | + display: flex; | ||
| 296 | + gap: 20rpx; | ||
| 297 | +} | ||
| 298 | + | ||
| 299 | +.preview-button { | ||
| 300 | + flex: 1; | ||
| 301 | + height: 80rpx; | ||
| 302 | + border-radius: 40rpx; | ||
| 303 | + font-size: 28rpx; | ||
| 304 | + | ||
| 305 | + &.primary { | ||
| 306 | + background-color: #1890ff; | ||
| 307 | + color: white; | ||
| 308 | + border: none; | ||
| 309 | + } | ||
| 310 | + | ||
| 311 | + &.secondary { | ||
| 312 | + background-color: #f5f5f5; | ||
| 313 | + color: #666; | ||
| 314 | + border: none; | ||
| 315 | + } | ||
| 316 | + } | ||
| 317 | + | ||
| 318 | +.join-button { | ||
| 193 | &.nut-button--loading { | 319 | &.nut-button--loading { |
| 194 | opacity: 0.7; | 320 | opacity: 0.7; |
| 195 | } | 321 | } | ... | ... |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2022-09-19 14:11:06 | 2 | * @Date: 2022-09-19 14:11:06 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-08-28 15:16:04 | 4 | + * @LastEditTime: 2025-08-29 00:53:27 |
| 5 | * @FilePath: /lls_program/src/pages/ActivitiesCover/index.vue | 5 | * @FilePath: /lls_program/src/pages/ActivitiesCover/index.vue |
| 6 | * @Description: 活动海报页面 - 展示活动信息并处理定位授权 | 6 | * @Description: 活动海报页面 - 展示活动信息并处理定位授权 |
| 7 | --> | 7 | --> |
| ... | @@ -21,6 +21,12 @@ | ... | @@ -21,6 +21,12 @@ |
| 21 | <view class="activity-subtitle">{{ activityData.subtitle }}</view> | 21 | <view class="activity-subtitle">{{ activityData.subtitle }}</view> |
| 22 | <view class="activity-date">{{ activityData.dateRange }}</view> | 22 | <view class="activity-date">{{ activityData.dateRange }}</view> |
| 23 | </view> | 23 | </view> |
| 24 | + | ||
| 25 | + <!-- 分享按钮 --> | ||
| 26 | + <view @tap="shareActivity" class="share-button"> | ||
| 27 | + <!-- <nut-icon name="share" color="white" size="20" /> --> | ||
| 28 | + <text>分享</text> | ||
| 29 | + </view> | ||
| 24 | </view> | 30 | </view> |
| 25 | 31 | ||
| 26 | <!-- 活动详情区域 --> | 32 | <!-- 活动详情区域 --> |
| ... | @@ -73,6 +79,76 @@ | ... | @@ -73,6 +79,76 @@ |
| 73 | 79 | ||
| 74 | <!-- 底部导航 --> | 80 | <!-- 底部导航 --> |
| 75 | <BottomNav /> | 81 | <BottomNav /> |
| 82 | + | ||
| 83 | + <!-- 分享选项弹窗 --> | ||
| 84 | + <nut-popup | ||
| 85 | + v-model:visible="show_share" | ||
| 86 | + position="bottom" | ||
| 87 | + :style="{ height: '40%' }" | ||
| 88 | + class="share-popup" | ||
| 89 | + > | ||
| 90 | + <div class="share-title">立即分享给好友</div> | ||
| 91 | + <div class="share-options"> | ||
| 92 | + <div | ||
| 93 | + v-for="option in share_options" | ||
| 94 | + :key="option.name" | ||
| 95 | + class="share-option" | ||
| 96 | + @click="onSelectShare({ detail: option })" | ||
| 97 | + > | ||
| 98 | + <div class="share-icon">{{ option.icon }}</div> | ||
| 99 | + <div class="share-text">{{ option.name }}</div> | ||
| 100 | + </div> | ||
| 101 | + </div> | ||
| 102 | + <nut-button class="cancel-button" @click="onCancelShare"> | ||
| 103 | + 取消 | ||
| 104 | + </nut-button> | ||
| 105 | + </nut-popup> | ||
| 106 | + | ||
| 107 | + <!-- 海报预览弹窗 --> | ||
| 108 | + <nut-popup | ||
| 109 | + v-model:visible="show_post" | ||
| 110 | + position="center" | ||
| 111 | + class="poster-preview-popup" | ||
| 112 | + > | ||
| 113 | + <view class="wrapper"> | ||
| 114 | + <view class="preview-area" @click="onClickPost"> | ||
| 115 | + <image v-if="posterPath" :src="posterPath" mode="widthFix" /> | ||
| 116 | + </view> | ||
| 117 | + </view> | ||
| 118 | + </nut-popup> | ||
| 119 | + | ||
| 120 | + <!-- 海报生成组件 --> | ||
| 121 | + <PosterBuilder | ||
| 122 | + v-if="startDraw" | ||
| 123 | + custom-style="position: fixed; left: 200%;" | ||
| 124 | + :config="base" | ||
| 125 | + @success="drawSuccess" | ||
| 126 | + @fail="drawFail" | ||
| 127 | + /> | ||
| 128 | + | ||
| 129 | + <!-- 保存选项弹窗 --> | ||
| 130 | + <nut-popup | ||
| 131 | + v-model:visible="show_save" | ||
| 132 | + position="bottom" | ||
| 133 | + :style="{ height: '30%' }" | ||
| 134 | + class="share-popup" | ||
| 135 | + > | ||
| 136 | + <div class="share-title">保存选项</div> | ||
| 137 | + <div class="share-options"> | ||
| 138 | + <div | ||
| 139 | + v-for="option in actions_save" | ||
| 140 | + :key="option.name" | ||
| 141 | + class="share-option" | ||
| 142 | + @click="onSelectSave({ detail: option })" | ||
| 143 | + > | ||
| 144 | + <div class="share-icon">💾</div> | ||
| 145 | + <div class="share-text">{{ option.name }}</div> | ||
| 146 | + </div> | ||
| 147 | + </div> | ||
| 148 | + <nut-button class="cancel-button" @click="onCancelSave"> | ||
| 149 | + 取消 | ||
| 150 | + </nut-button> | ||
| 151 | + </nut-popup> | ||
| 76 | </view> | 152 | </view> |
| 77 | </template> | 153 | </template> |
| 78 | 154 | ||
| ... | @@ -82,6 +158,8 @@ import { ref, onMounted } from "vue" | ... | @@ -82,6 +158,8 @@ import { ref, onMounted } from "vue" |
| 82 | import Taro from '@tarojs/taro' | 158 | import Taro from '@tarojs/taro' |
| 83 | import "./index.less" | 159 | import "./index.less" |
| 84 | import BottomNav from '../../components/BottomNav.vue' | 160 | import BottomNav from '../../components/BottomNav.vue' |
| 161 | +import PosterBuilder from '../../components/PosterBuilder/index.vue' | ||
| 162 | +import icon_share from '@/images/icon/share.svg' | ||
| 85 | 163 | ||
| 86 | /** | 164 | /** |
| 87 | * 活动海报页面组件 | 165 | * 活动海报页面组件 |
| ... | @@ -93,6 +171,30 @@ const hasLocationAuth = ref(false) // 是否已授权定位 | ... | @@ -93,6 +171,30 @@ const hasLocationAuth = ref(false) // 是否已授权定位 |
| 93 | const isJoining = ref(false) // 是否正在加入活动 | 171 | const isJoining = ref(false) // 是否正在加入活动 |
| 94 | const userLocation = ref({ lng: null, lat: null }) // 用户位置信息 | 172 | const userLocation = ref({ lng: null, lat: null }) // 用户位置信息 |
| 95 | 173 | ||
| 174 | +// 海报生成相关状态 | ||
| 175 | +const show_share = ref(false) // 显示分享弹窗 | ||
| 176 | +const show_post = ref(false) // 显示海报预览 | ||
| 177 | +const show_save = ref(false) // 显示保存弹窗 | ||
| 178 | +const startDraw = ref(false) // 开始绘制海报 | ||
| 179 | +const posterPath = ref('') // 海报路径 | ||
| 180 | +const nickname = ref('老来赛用户') // 用户昵称 | ||
| 181 | +const avatar = ref('https://cdn.ipadbiz.cn/icon/tou@2x.png') // 用户头像 | ||
| 182 | + | ||
| 183 | +// 分享选项 | ||
| 184 | +const share_options = [ | ||
| 185 | + { name: '微信', icon: 'wechat', openType: 'share' }, | ||
| 186 | + { name: '生成海报', icon: icon_share }, | ||
| 187 | +] | ||
| 188 | + | ||
| 189 | +// 保存选项 | ||
| 190 | +const actions_save = ref([{ | ||
| 191 | + name: '保存至相册' | ||
| 192 | +}]) | ||
| 193 | + | ||
| 194 | +// 海报配置 | ||
| 195 | +let base = {} | ||
| 196 | +let qrcode_url = 'https://cdn.ipadbiz.cn/space/068a790496c87cb8d2ed6e551401c544.png' // Mock二维码 | ||
| 197 | + | ||
| 96 | // Mock活动数据 | 198 | // Mock活动数据 |
| 97 | const activityData = ref({ | 199 | const activityData = ref({ |
| 98 | title: '南京路商圈时尚Citywalk', | 200 | title: '南京路商圈时尚Citywalk', |
| ... | @@ -210,6 +312,329 @@ const handleJoinActivity = async () => { | ... | @@ -210,6 +312,329 @@ const handleJoinActivity = async () => { |
| 210 | } | 312 | } |
| 211 | } | 313 | } |
| 212 | 314 | ||
| 315 | +/** | ||
| 316 | + * 分享活动 | ||
| 317 | + */ | ||
| 318 | +const shareActivity = () => { | ||
| 319 | + show_share.value = true | ||
| 320 | +} | ||
| 321 | + | ||
| 322 | +/** | ||
| 323 | + * 取消分享 | ||
| 324 | + */ | ||
| 325 | +const onCancelShare = () => { | ||
| 326 | + show_share.value = false | ||
| 327 | +} | ||
| 328 | + | ||
| 329 | +/** | ||
| 330 | + * 选择分享方式 | ||
| 331 | + */ | ||
| 332 | +const onSelectShare = ({ detail }) => { | ||
| 333 | + show_share.value = false | ||
| 334 | + if (detail.name === '生成海报') { | ||
| 335 | + show_post.value = true | ||
| 336 | + startGeneratePoster() | ||
| 337 | + } | ||
| 338 | +} | ||
| 339 | + | ||
| 340 | +/** | ||
| 341 | + * 点击海报预览 | ||
| 342 | + */ | ||
| 343 | +const onClickPost = () => { | ||
| 344 | + show_save.value = true | ||
| 345 | +} | ||
| 346 | + | ||
| 347 | +/** | ||
| 348 | + * 取消保存 | ||
| 349 | + */ | ||
| 350 | +const onCancelSave = () => { | ||
| 351 | + show_save.value = false | ||
| 352 | + show_post.value = false | ||
| 353 | +} | ||
| 354 | + | ||
| 355 | +/** | ||
| 356 | + * 选择保存方式 | ||
| 357 | + */ | ||
| 358 | +const onSelectSave = ({ detail }) => { | ||
| 359 | + if (detail.name === '保存至相册') { | ||
| 360 | + show_save.value = false | ||
| 361 | + show_post.value = false | ||
| 362 | + savePoster() | ||
| 363 | + } | ||
| 364 | +} | ||
| 365 | + | ||
| 366 | +/** | ||
| 367 | + * 开始生成海报 | ||
| 368 | + */ | ||
| 369 | +const startGeneratePoster = async () => { | ||
| 370 | + // 配置海报参数 | ||
| 371 | + base = { | ||
| 372 | + width: 1024, | ||
| 373 | + height: 1334, | ||
| 374 | + backgroundColor: '', | ||
| 375 | + debug: false, | ||
| 376 | + blocks: [ | ||
| 377 | + { // 上部分canvas画布高度 | ||
| 378 | + x: 40, | ||
| 379 | + y: 20, | ||
| 380 | + width: 950, | ||
| 381 | + height: 950, | ||
| 382 | + paddingLeft: 0, | ||
| 383 | + paddingRight: 0, | ||
| 384 | + borderWidth: 1, | ||
| 385 | + borderColor: '#fff', | ||
| 386 | + backgroundColor: '#fff', | ||
| 387 | + borderRadiusGroup: [16, 16, 0, 0], | ||
| 388 | + }, | ||
| 389 | + { // 活动时间背景图 | ||
| 390 | + x: 40, | ||
| 391 | + y: 730, | ||
| 392 | + height: 75, | ||
| 393 | + paddingLeft: 80, | ||
| 394 | + paddingRight: 0, | ||
| 395 | + borderWidth: 0, | ||
| 396 | + text: { | ||
| 397 | + x: 0, | ||
| 398 | + y: 0, | ||
| 399 | + text: activityData.value.dateRange, | ||
| 400 | + fontSize: 40, | ||
| 401 | + color: '#222', | ||
| 402 | + opacity: 1, | ||
| 403 | + baseLine: 'top', | ||
| 404 | + lineHeight: 48, | ||
| 405 | + lineNum: 2, | ||
| 406 | + textAlign: 'left', | ||
| 407 | + zIndex: 0, | ||
| 408 | + }, | ||
| 409 | + backgroundColor: '#FFF9F3', | ||
| 410 | + borderRadiusGroup: [0, 25, 25, 0], | ||
| 411 | + }, | ||
| 412 | + { // 活动地点背景图 | ||
| 413 | + x: 40, | ||
| 414 | + y: 830, | ||
| 415 | + height: 75, | ||
| 416 | + paddingLeft: 80, | ||
| 417 | + paddingRight: 0, | ||
| 418 | + borderWidth: 0, | ||
| 419 | + text: { | ||
| 420 | + x: 0, | ||
| 421 | + y: 0, | ||
| 422 | + text: '上海市黄浦区南京东路', | ||
| 423 | + fontSize: 40, | ||
| 424 | + color: '#222', | ||
| 425 | + opacity: 1, | ||
| 426 | + baseLine: 'top', | ||
| 427 | + lineHeight: 48, | ||
| 428 | + lineNum: 2, | ||
| 429 | + textAlign: 'left', | ||
| 430 | + zIndex: 0, | ||
| 431 | + }, | ||
| 432 | + backgroundColor: '#FFF9F3', | ||
| 433 | + borderRadiusGroup: [0, 25, 25, 0], | ||
| 434 | + }, | ||
| 435 | + { // 下部分canvas画布高度 | ||
| 436 | + x: 40, | ||
| 437 | + y: 1060, | ||
| 438 | + width: 950, | ||
| 439 | + height: 250, | ||
| 440 | + paddingLeft: 0, | ||
| 441 | + paddingRight: 0, | ||
| 442 | + borderWidth: 1, | ||
| 443 | + borderColor: '#fff', | ||
| 444 | + backgroundColor: '#fff', | ||
| 445 | + borderRadiusGroup: [0, 0, 16, 16], | ||
| 446 | + } | ||
| 447 | + ], | ||
| 448 | + texts: [ | ||
| 449 | + { | ||
| 450 | + x: 80, | ||
| 451 | + y: 630, | ||
| 452 | + text: activityData.value.title, | ||
| 453 | + fontSize: 50, | ||
| 454 | + color: '#000', | ||
| 455 | + opacity: 1, | ||
| 456 | + baseLine: 'middle', | ||
| 457 | + lineHeight: 60, | ||
| 458 | + lineNum: 2, | ||
| 459 | + textAlign: 'left', | ||
| 460 | + width: 800, | ||
| 461 | + zIndex: 999, | ||
| 462 | + fontFamily: 'Monospace', | ||
| 463 | + }, | ||
| 464 | + { | ||
| 465 | + x: 135, | ||
| 466 | + y: 770, | ||
| 467 | + text: activityData.value.dateRange, | ||
| 468 | + fontSize: 40, | ||
| 469 | + color: '#222', | ||
| 470 | + opacity: 1, | ||
| 471 | + baseLine: 'middle', | ||
| 472 | + lineHeight: 48, | ||
| 473 | + lineNum: 2, | ||
| 474 | + textAlign: 'left', | ||
| 475 | + zIndex: 999, | ||
| 476 | + }, | ||
| 477 | + { | ||
| 478 | + x: 135, | ||
| 479 | + y: 870, | ||
| 480 | + text: '上海市黄浦区南京东路', | ||
| 481 | + fontSize: 40, | ||
| 482 | + color: '#222', | ||
| 483 | + opacity: 1, | ||
| 484 | + baseLine: 'middle', | ||
| 485 | + lineHeight: 48, | ||
| 486 | + lineNum: 2, | ||
| 487 | + textAlign: 'left', | ||
| 488 | + zIndex: 999, | ||
| 489 | + }, | ||
| 490 | + { | ||
| 491 | + x: 300, | ||
| 492 | + y: 1150, | ||
| 493 | + text: nickname.value, | ||
| 494 | + fontSize: 50, | ||
| 495 | + color: '#333', | ||
| 496 | + opacity: 1, | ||
| 497 | + baseLine: 'middle', | ||
| 498 | + textAlign: 'left', | ||
| 499 | + lineHeight: 50, | ||
| 500 | + lineNum: 1, | ||
| 501 | + zIndex: 999, | ||
| 502 | + }, | ||
| 503 | + { | ||
| 504 | + x: 300, | ||
| 505 | + y: 1220, | ||
| 506 | + text: '邀请你一起来活动!', | ||
| 507 | + fontSize: 42, | ||
| 508 | + color: '#8F9399', | ||
| 509 | + opacity: 1, | ||
| 510 | + baseLine: 'middle', | ||
| 511 | + textAlign: 'left', | ||
| 512 | + lineHeight: 42, | ||
| 513 | + lineNum: 1, | ||
| 514 | + zIndex: 999, | ||
| 515 | + } | ||
| 516 | + ], | ||
| 517 | + images: [ | ||
| 518 | + { | ||
| 519 | + url: qrcode_url, | ||
| 520 | + width: 949, | ||
| 521 | + height: 108, | ||
| 522 | + x: 40, | ||
| 523 | + y: 960, | ||
| 524 | + zIndex: 10, | ||
| 525 | + }, | ||
| 526 | + { | ||
| 527 | + url: qrcode_url, | ||
| 528 | + width: 950, | ||
| 529 | + height: 500, | ||
| 530 | + x: 40, | ||
| 531 | + y: 20, | ||
| 532 | + borderRadiusGroup: [18, 18, 0, 0], | ||
| 533 | + zIndex: 10, | ||
| 534 | + }, | ||
| 535 | + { | ||
| 536 | + url: qrcode_url, | ||
| 537 | + width: 40, | ||
| 538 | + height: 40, | ||
| 539 | + x: 80, | ||
| 540 | + y: 750, | ||
| 541 | + borderRadius: 100, | ||
| 542 | + borderWidth: 0, | ||
| 543 | + zIndex: 10, | ||
| 544 | + }, | ||
| 545 | + { | ||
| 546 | + url: qrcode_url, | ||
| 547 | + width: 35, | ||
| 548 | + height: 40, | ||
| 549 | + x: 80, | ||
| 550 | + y: 850, | ||
| 551 | + borderRadius: 100, | ||
| 552 | + borderWidth: 0, | ||
| 553 | + zIndex: 10, | ||
| 554 | + }, | ||
| 555 | + { | ||
| 556 | + url: qrcode_url, | ||
| 557 | + width: 170, | ||
| 558 | + height: 170, | ||
| 559 | + x: 80, | ||
| 560 | + y: 1090, | ||
| 561 | + borderRadius: 100, | ||
| 562 | + borderWidth: 0, | ||
| 563 | + zIndex: 10, | ||
| 564 | + }, | ||
| 565 | + { | ||
| 566 | + url: qrcode_url, | ||
| 567 | + width: 170, | ||
| 568 | + height: 170, | ||
| 569 | + x: 750, | ||
| 570 | + y: 1090, | ||
| 571 | + borderRadius: 100, | ||
| 572 | + borderWidth: 0, | ||
| 573 | + zIndex: 10, | ||
| 574 | + }, | ||
| 575 | + ], | ||
| 576 | + lines: [] | ||
| 577 | + } | ||
| 578 | + | ||
| 579 | + startDraw.value = true | ||
| 580 | + if (!posterPath.value) Taro.showLoading({ title: '生成海报中...' }) | ||
| 581 | +} | ||
| 582 | + | ||
| 583 | +/** | ||
| 584 | + * 海报绘制成功回调 | ||
| 585 | + */ | ||
| 586 | +const drawSuccess = (result) => { | ||
| 587 | + console.log('绘制成功', result) | ||
| 588 | + const { tempFilePath, errMsg } = result | ||
| 589 | + if (errMsg === 'canvasToTempFilePath:ok') { | ||
| 590 | + posterPath.value = tempFilePath | ||
| 591 | + Taro.hideLoading() | ||
| 592 | + } else { | ||
| 593 | + Taro.hideLoading() | ||
| 594 | + Taro.showToast({ | ||
| 595 | + title: '生成失败,请稍后重试', | ||
| 596 | + icon: 'none', | ||
| 597 | + duration: 2500 | ||
| 598 | + }) | ||
| 599 | + } | ||
| 600 | +} | ||
| 601 | + | ||
| 602 | +/** | ||
| 603 | + * 海报绘制失败回调 | ||
| 604 | + */ | ||
| 605 | +const drawFail = (result) => { | ||
| 606 | + console.log('绘制失败', result) | ||
| 607 | + Taro.hideLoading() | ||
| 608 | + Taro.showToast({ | ||
| 609 | + title: '生成失败,请稍后重试', | ||
| 610 | + icon: 'none', | ||
| 611 | + duration: 2500 | ||
| 612 | + }) | ||
| 613 | +} | ||
| 614 | + | ||
| 615 | +/** | ||
| 616 | + * 保存海报到相册 | ||
| 617 | + */ | ||
| 618 | +const savePoster = () => { | ||
| 619 | + Taro.saveImageToPhotosAlbum({ | ||
| 620 | + filePath: posterPath.value, | ||
| 621 | + success() { | ||
| 622 | + Taro.showToast({ | ||
| 623 | + title: '已保存到相册', | ||
| 624 | + icon: 'success', | ||
| 625 | + duration: 2000 | ||
| 626 | + }) | ||
| 627 | + }, | ||
| 628 | + fail() { | ||
| 629 | + Taro.showToast({ | ||
| 630 | + title: '保存失败', | ||
| 631 | + icon: 'none', | ||
| 632 | + duration: 2000 | ||
| 633 | + }) | ||
| 634 | + } | ||
| 635 | + }) | ||
| 636 | +} | ||
| 637 | + | ||
| 213 | // 页面挂载时检查定位授权状态 | 638 | // 页面挂载时检查定位授权状态 |
| 214 | onMounted(() => { | 639 | onMounted(() => { |
| 215 | checkLocationAuth() | 640 | checkLocationAuth() | ... | ... |
tsconfig.json
0 → 100644
| 1 | +{ | ||
| 2 | + "compilerOptions": { | ||
| 3 | + "target": "ES2018", | ||
| 4 | + "lib": [ | ||
| 5 | + "ES6", | ||
| 6 | + "ES2017", | ||
| 7 | + "ES2018", | ||
| 8 | + "ES2019" | ||
| 9 | + ], | ||
| 10 | + "allowJs": true, | ||
| 11 | + "skipLibCheck": true, | ||
| 12 | + "esModuleInterop": true, | ||
| 13 | + "allowSyntheticDefaultImports": true, | ||
| 14 | + "strict": false, | ||
| 15 | + "forceConsistentCasingInFileNames": true, | ||
| 16 | + "module": "ESNext", | ||
| 17 | + "moduleResolution": "node", | ||
| 18 | + "resolveJsonModule": true, | ||
| 19 | + "isolatedModules": true, | ||
| 20 | + "noEmit": true, | ||
| 21 | + "jsx": "preserve", | ||
| 22 | + "baseUrl": ".", | ||
| 23 | + "paths": { | ||
| 24 | + "@/*": ["./src/*"] | ||
| 25 | + }, | ||
| 26 | + "types": [ | ||
| 27 | + "node" | ||
| 28 | + ] | ||
| 29 | + }, | ||
| 30 | + "include": [ | ||
| 31 | + "./src/**/*", | ||
| 32 | + "./types/**/*" | ||
| 33 | + ], | ||
| 34 | + "exclude": [ | ||
| 35 | + "node_modules", | ||
| 36 | + "dist", | ||
| 37 | + "**/*.js" | ||
| 38 | + ] | ||
| 39 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
-
Please register or login to post a comment