hookehuyr

feat(ActivitiesCover): 添加活动分享功能及海报生成组件

- 新增分享按钮及分享弹窗组件
- 实现海报生成功能,支持保存至相册
- 添加分享选项包括微信分享和海报生成
- 优化图片下载工具函数,增加重试机制
- 更新babel配置支持TypeScript
- 添加相关SVG图标资源
......@@ -4,7 +4,7 @@ module.exports = {
presets: [
['taro', {
framework: 'vue3',
ts: false,
ts: true,
compiler: 'webpack5',
}]
]
......
......@@ -37,6 +37,7 @@
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.7.7",
"@nutui/icons-vue": "^0.1.1",
"@nutui/icons-vue-taro": "^0.0.9",
"@nutui/nutui-taro": "^4.3.13",
"@tarojs/components": "4.1.2",
......@@ -64,6 +65,7 @@
"@tarojs/cli": "4.1.2",
"@tarojs/taro-loader": "4.1.2",
"@tarojs/webpack5-runner": "4.1.2",
"@types/node": "^24.3.0",
"@types/webpack-env": "^1.13.6",
"@vue/babel-plugin-jsx": "^1.0.6",
"@vue/compiler-sfc": "^3.0.0",
......@@ -75,6 +77,7 @@
"postcss": "^8.5.6",
"style-loader": "1.3.0",
"tailwindcss": "^3.4.0",
"typescript": "^5.9.2",
"unplugin-vue-components": "^0.26.0",
"vue-loader": "^17.0.0",
"weapp-tailwindcss": "^4.1.10",
......
This diff could not be displayed because it is too large.
<svg width="949" height="108" viewBox="0 0 949 108" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="949" height="2" y="53" fill="#E5E7EB"/>
<circle cx="100" cy="54" r="8" fill="#3B82F6"/>
<circle cx="200" cy="54" r="6" fill="#10B981"/>
<circle cx="300" cy="54" r="8" fill="#F59E0B"/>
<circle cx="400" cy="54" r="6" fill="#EF4444"/>
<circle cx="500" cy="54" r="8" fill="#8B5CF6"/>
<circle cx="600" cy="54" r="6" fill="#06B6D4"/>
<circle cx="700" cy="54" r="8" fill="#84CC16"/>
<circle cx="800" cy="54" r="6" fill="#F97316"/>
</svg>
\ No newline at end of file
<svg width="35" height="40" viewBox="0 0 35 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<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"/>
<circle cx="17.5" cy="17.5" r="7" fill="#fff"/>
</svg>
\ No newline at end of file
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<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"/>
</svg>
\ No newline at end of file
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="20" cy="20" r="18" fill="#FF6B35" stroke="#fff" stroke-width="2"/>
<path d="M20 8v12l8 4" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
\ No newline at end of file
/* eslint-disable prefer-destructuring */
import Taro, { CanvasContext, CanvasGradient } from '@tarojs/taro';
import Taro from '@tarojs/taro'
declare const wx: any;
// 全局变量声明
// eslint-disable-next-line no-undef
const wx = globalThis.wx || {};
/**
* @description 生成随机字符串
......@@ -128,28 +130,70 @@ export const toRpx = (px, factor = getFactor()) =>
/**
* 下载图片资源
* @param { string } url
* @param { number } retryCount - 重试次数
* @returns { Promise }
*/
export function downImage(url) {
return new Promise<string>((resolve, reject) => {
export function downImage(url, retryCount = 2) {
return new Promise((resolve, reject) => {
// eslint-disable-next-line no-undef
if (/^http/.test(url) && !new RegExp(wx.env.USER_DATA_PATH).test(url)) {
// wx.env.USER_DATA_PATH 文件系统中的用户目录路径
Taro.downloadFile({
url: mapHttpToHttps(url),
success: (res) => {
if (res.statusCode === 200) {
resolve(res.tempFilePath);
} else {
console.log('下载失败', res);
reject(res);
const attemptDownload = (attempt = 0) => {
Taro.downloadFile({
url: mapHttpToHttps(url),
timeout: 10000, // 设置10秒超时
success: (res) => {
if (res.statusCode === 200) {
resolve(res.tempFilePath);
} else {
// console.log('下载失败', res);
if (attempt < retryCount) {
// console.log(`重试下载图片,第${attempt + 1}次重试`);
setTimeout(() => attemptDownload(attempt + 1), 1000);
} else {
reject(res);
}
}
},
fail(err) {
// console.log('下载失败了', err);
// 如果是代理连接问题,尝试使用原始URL
if (err.errMsg && err.errMsg.includes('127.0.0.1:7890')) {
// console.log('检测到代理问题,尝试使用原始URL');
if (attempt < retryCount) {
setTimeout(() => {
Taro.downloadFile({
url: url, // 使用原始URL,不转换https
timeout: 10000,
success: (res) => {
if (res.statusCode === 200) {
resolve(res.tempFilePath);
} else {
reject(res);
}
},
fail: () => {
if (attempt + 1 < retryCount) {
attemptDownload(attempt + 1);
} else {
reject(err);
}
}
});
}, 1000);
} else {
reject(err);
}
} else if (attempt < retryCount) {
// console.log(`重试下载图片,第${attempt + 1}次重试`);
setTimeout(() => attemptDownload(attempt + 1), 1000);
} else {
reject(err);
}
}
},
fail(err) {
console.log('下载失败了', err);
reject(err);
}
});
});
};
attemptDownload();
} else {
resolve(url); // 支持本地地址
}
......@@ -191,7 +235,7 @@ export const getImageInfo = (item, index) =>
borderColor: item.borderColor,
borderRadiusGroup: item.borderRadiusGroup,
zIndex: typeof zIndex !== 'undefined' ? zIndex : index,
imgPath: url,
imgPath: imgPath, // 使用下载后的临时文件路径
sx,
sy,
sw: imgWidth - sx * 2,
......@@ -204,7 +248,7 @@ export const getImageInfo = (item, index) =>
resolve(result);
})
.catch((err) => {
console.log('读取图片信息失败', err);
// console.log('读取图片信息失败', err);
reject(err);
})
);
......@@ -222,7 +266,7 @@ export const getImageInfo = (item, index) =>
*/
// TODO: 待优化, 支持所有角度,多个颜色的线性渐变
export function getLinearColor(
ctx: CanvasContext,
ctx,
color,
startX,
startY,
......@@ -238,7 +282,7 @@ export function getLinearColor(
console.warn('坐标或者宽高只支持数字');
return color;
}
let grd: CanvasGradient | string = color;
let grd = color;
if (color.includes('linear-gradient')) {
// fillStyle 不支持线性渐变色
const colorList = color.match(/\((\d+)deg,\s(.+)\s\d+%,\s(.+)\s\d+%/);
......@@ -263,8 +307,8 @@ export function getLinearColor(
} else {
throw new Error('只支持0 <= 颜色弧度 <= 180');
}
(grd as CanvasGradient).addColorStop(0, color1);
(grd as CanvasGradient).addColorStop(1, color2);
grd.addColorStop(0, color1);
grd.addColorStop(1, color2);
}
return grd;
}
......
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<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"/>
</svg>
\ No newline at end of file
<svg width="949" height="108" viewBox="0 0 949 108" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" patternUnits="userSpaceOnUse" width="20" height="20">
<circle cx="10" cy="10" r="2" fill="#E5E5E5"/>
</pattern>
</defs>
<rect width="949" height="108" fill="url(#dots)"/>
<line x1="0" y1="54" x2="949" y2="54" stroke="#CCCCCC" stroke-width="2" stroke-dasharray="10,5"/>
</svg>
\ No newline at end of file
......@@ -55,6 +55,27 @@
}
}
// 分享按钮
.share-button {
position: absolute;
top: 40rpx;
right: 40rpx;
width: 80rpx;
height: 80rpx;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 32rpx;
z-index: 10;
&:active {
background-color: rgba(0, 0, 0, 0.7);
}
}
// 详情区域
.details-section {
flex: 1;
......@@ -189,7 +210,112 @@
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3);
}
}
}
// 弹窗样式
.share-popup {
.nut-popup__content {
border-radius: 24rpx 24rpx 0 0;
padding: 40rpx;
}
}
.share-title {
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 40rpx;
color: #333;
}
.share-options {
display: flex;
justify-content: space-around;
margin-bottom: 40rpx;
}
.share-option {
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx;
border-radius: 12rpx;
&:active {
background-color: #f5f5f5;
}
}
.share-icon {
width: 80rpx;
height: 80rpx;
margin-bottom: 16rpx;
border-radius: 12rpx;
background-color: #1890ff;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 36rpx;
}
.share-text {
font-size: 24rpx;
color: #666;
}
.cancel-button {
width: 100%;
height: 88rpx;
border-radius: 44rpx;
font-size: 32rpx;
background-color: #f5f5f5;
color: #666;
border: none;
}
// 海报预览弹窗
.poster-preview-popup {
.nut-popup__content {
width: 90%;
max-width: 600rpx;
border-radius: 24rpx;
padding: 40rpx;
background-color: white;
}
}
.poster-preview {
width: 100%;
border-radius: 12rpx;
margin-bottom: 40rpx;
}
.preview-actions {
display: flex;
gap: 20rpx;
}
.preview-button {
flex: 1;
height: 80rpx;
border-radius: 40rpx;
font-size: 28rpx;
&.primary {
background-color: #1890ff;
color: white;
border: none;
}
&.secondary {
background-color: #f5f5f5;
color: #666;
border: none;
}
}
.join-button {
&.nut-button--loading {
opacity: 0.7;
}
......
This diff is collapsed. Click to expand it.
{
"compilerOptions": {
"target": "ES2018",
"lib": [
"ES6",
"ES2017",
"ES2018",
"ES2019"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"types": [
"node"
]
},
"include": [
"./src/**/*",
"./types/**/*"
],
"exclude": [
"node_modules",
"dist",
"**/*.js"
]
}
\ No newline at end of file