Compare commits

...

12 Commits

Author SHA1 Message Date
zzc
adde98c05c fix: index use 2026-01-23 00:01:31 +08:00
zzc
1519c4f486 fix: tab icon 2026-01-22 23:54:56 +08:00
zzc
3ecc525d2c fix: tab icon 2026-01-22 23:43:49 +08:00
zzc
64b2abd29e fix: index time set 2026-01-22 22:19:41 +08:00
zzc
2b2af1bab4 fix: card content font size 2026-01-22 22:11:02 +08:00
zzc
303654b1e1 fix: card content font size 2026-01-22 15:08:26 +08:00
zzc
9f5c5d2d1f fix: card content font 2026-01-22 14:56:32 +08:00
zzc
0181066b34 fix: card content font 2026-01-22 11:04:47 +08:00
zzc
bdaf3a3be1 fix: card content font 2026-01-22 09:34:08 +08:00
zzc
1c094ed14b fix: card content position 2026-01-22 08:55:00 +08:00
zzc
f7f62cebb5 feat: make gpage tpl 2026-01-22 08:40:37 +08:00
zzc
da194c920c feat: make gpage tpl 2026-01-22 08:28:34 +08:00
17 changed files with 745 additions and 254 deletions

View File

@@ -22,3 +22,10 @@ export const getCardTemplateList = async (page = 1) => {
method: "GET", method: "GET",
}); });
}; };
export const getCardTemplateContentList = async () => {
return request({
url: "/api/blessing/card/template-content/list",
method: "GET",
});
};

View File

@@ -74,24 +74,24 @@
}, },
"tabBar": { "tabBar": {
"color": "#999999", "color": "#999999",
"selectedColor": "#7DBB9D", "selectedColor": "#ff3b30",
"backgroundColor": "#f6f1ec", "backgroundColor": "#ffffff",
"iconWidth": "8px", "iconWidth": "24px",
"list": [ "list": [
{ {
"text": "祝福", "text": "首页",
"pagePath": "pages/index/index", "pagePath": "pages/index/index",
"iconPath": "static/images/tabBar/home.png", "iconPath": "static/images/tabBar/home.png",
"selectedIconPath": "static/images/tabBar/home_s.png" "selectedIconPath": "static/images/tabBar/home_s.png"
}, },
{ {
"text": "制作", "text": "定制贺卡",
"pagePath": "pages/make/index", "pagePath": "pages/make/index",
"iconPath": "static/images/tabBar/message.png", "iconPath": "static/images/tabBar/creation.png",
"selectedIconPath": "static/images/tabBar/message_s.png" "selectedIconPath": "static/images/tabBar/creation_s.png"
}, },
{ {
"text": "个人中心", "text": "我的",
"pagePath": "pages/mine/mine", "pagePath": "pages/mine/mine",
"iconPath": "static/images/tabBar/me.png", "iconPath": "static/images/tabBar/me.png",
"selectedIconPath": "static/images/tabBar/me_s.png" "selectedIconPath": "static/images/tabBar/me_s.png"

View File

@@ -112,7 +112,7 @@
import { ref, onMounted } from "vue"; import { ref, onMounted } from "vue";
import { getBavBarHeight } from "@/utils/system"; import { getBavBarHeight } from "@/utils/system";
import { onLoad } from "@dcloudio/uni-app"; import { onLoad } from "@dcloudio/uni-app";
import { getCardDetail } from "@/api/card.js"; import { getPageDetail } from "@/api/system.js";
const navBarHeight = ref(44); const navBarHeight = ref(44);
const statusBarHeight = ref(20); const statusBarHeight = ref(20);
@@ -122,7 +122,7 @@ const cardDetail = ref({});
onLoad(async (options) => { onLoad(async (options) => {
if (options.shareToken) { if (options.shareToken) {
const card = await getCardDetail(options.shareToken); const card = await getPageDetail(options.shareToken);
cardId.value = card.id; cardId.value = card.id;
cardDetail.value = card; cardDetail.value = card;
} }

View File

@@ -4,7 +4,7 @@
<view class="hero"> <view class="hero">
<view class="hero-badge"> <view class="hero-badge">
<text class="badge-dot"></text> <text class="badge-dot"></text>
<text class="badge-text">距春节还有 30 </text> <text class="badge-text">{{ countdownText }}</text>
</view> </view>
<view class="hero-title"> <view class="hero-title">
<text class="year">2026</text> <text class="year">2026</text>
@@ -37,38 +37,61 @@
</view> </view>
</view> </view>
<!-- 大家都在用竖向列表左图右文 --> <!-- 今日灵感 -->
<view class="daily-section">
<view class="daily-header">
<text class="daily-title">今日灵感</text>
<text class="daily-date">{{ todayDate }}</text>
</view>
<view class="daily-card">
<view class="daily-content">
<text class="quote-mark"></text>
<text class="daily-text">{{ dailyGreeting }}</text>
<text class="quote-mark right"></text>
</view>
<view class="daily-actions">
<view class="action-btn copy" @tap="copyGreeting">
<text class="icon"></text> 复制
</view>
<view class="action-btn use" @tap="useGreeting">
<text class="icon"></text> 去制作
</view>
</view>
</view>
</view>
<!-- 大家都在用网格布局 -->
<view class="section"> <view class="section">
<view class="section-header"> <view class="section-header">
<view class="section-bar"></view> <view class="section-bar"></view>
<text class="section-title">大家都在用</text> <text class="section-title">大家都在用</text>
<text class="section-more" @tap="onMore('use')">查看更多 ></text> <text class="section-more" @tap="onMore('use')">查看更多 ></text>
</view> </view>
<view class="use-list"> <view class="use-grid">
<view <view
v-for="(card, i) in popularCards" v-for="(card, i) in popularCards"
:key="i" :key="i"
class="use-row" class="use-card"
@tap="previewCard(card)" @tap="previewCard(card)"
> >
<view class="thumb-wrap"> <view class="card-cover-wrap">
<image :src="card.cover" class="thumb" mode="aspectFill" /> <image :src="card.cover" class="card-cover" mode="aspectFill" />
<view v-if="card.type === 'video'" class="thumb-play"></view> <view
</view>
<view class="use-right">
<view class="title-line">
<text class="use-title">{{ card.title }}</text>
<text
v-if="card.tag" v-if="card.tag"
class="tag" class="card-tag"
:class="`tag--${card.tagType || 'default'}`" :class="`tag--${card.tagType || 'default'}`"
>{{ card.tag }}</text
> >
{{ card.tag }}
</view>
</view>
<view class="card-info">
<view class="card-title">{{ card.title }}</view>
<view class="card-desc">{{ card.desc }}</view>
<view class="card-footer">
<view class="cta-btn" @tap.stop="onCta(card)">
{{ card.cta }}
</view>
</view> </view>
<text class="use-desc">{{ card.desc }}</text>
<text class="use-cta" @tap.stop="onCta(card)"
>{{ card.cta }} ></text
>
</view> </view>
</view> </view>
</view> </view>
@@ -77,10 +100,71 @@
</template> </template>
<script setup> <script setup>
import { ref } from "vue"; import { ref, onMounted } from "vue";
import { onPullDownRefresh, onShareAppMessage } from "@dcloudio/uni-app"; import { onPullDownRefresh, onShareAppMessage } from "@dcloudio/uni-app";
import { getBavBarHeight } from "@/utils/system"; import { getBavBarHeight } from "@/utils/system";
const countdownText = ref("");
const updateCountdown = () => {
const now = new Date();
const springFestival = new Date("2026-02-17T00:00:00"); // 2026春节
// 只比较日期,忽略时分秒
now.setHours(0, 0, 0, 0);
springFestival.setHours(0, 0, 0, 0);
const diffTime = now.getTime() - springFestival.getTime();
const days = Math.floor(diffTime / (1000 * 60 * 60 * 24));
if (days < 0) {
countdownText.value = `距春节还有 ${Math.abs(days)}`;
} else if (days === 0) {
countdownText.value = "大年初一";
} else {
const cnNums = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十"];
if (days < 10) {
countdownText.value = `大年初${cnNums[days]}`;
} else if (days < 15) {
const sub = days + 1 - 10;
const subCn = ["一", "二", "三", "四", "五"][sub - 1];
countdownText.value = `正月十${subCn}`;
} else if (days === 14) {
// days=14是第15天
countdownText.value = "元宵节";
} else {
countdownText.value = "蛇年大吉";
}
}
};
const todayDate = ref("");
const dailyGreeting = ref(
"岁岁常欢愉,年年皆胜意。愿你新的一年,多喜乐,长安宁。",
);
const copyGreeting = () => {
uni.setClipboardData({
data: dailyGreeting.value,
success: () => {
uni.showToast({ title: "复制成功", icon: "none" });
},
});
};
const useGreeting = () => {
uni.setStorageSync("TEMP_BLESSING_TEXT", dailyGreeting.value);
uni.switchTab({
url: "/pages/make/index",
});
};
onMounted(() => {
updateCountdown();
const date = new Date();
todayDate.value = `${date.getMonth() + 1}${date.getDate()}`;
});
const features = ref([ const features = ref([
{ {
title: "新春祝福卡片", title: "新春祝福卡片",
@@ -89,10 +173,10 @@ const features = ref([
type: "card", type: "card",
}, },
{ {
title: "红包封面", title: "新年运势",
subtitle: "取新年红包封面", subtitle: "取新年关键词",
icon: "/static/icon/hongbao.png", icon: "/static/icon/yunshi.png",
type: "video", type: "fortune",
}, },
{ {
title: "新春头像", title: "新春头像",
@@ -101,10 +185,10 @@ const features = ref([
type: "avatar_decor", type: "avatar_decor",
}, },
{ {
title: "新年运势", title: "精美壁纸",
subtitle: "抽取新年关键词", subtitle: "获取精美壁纸",
icon: "/static/icon/yunshi.png", icon: "/static/icon/bizhi.png",
type: "fortune", type: "video",
}, },
]); ]);
@@ -137,19 +221,28 @@ const popularCards = ref([
"https://file.lihailezzc.com/91cd1611-bb87-442b-a338-24e9d79e4ee9.png", "https://file.lihailezzc.com/91cd1611-bb87-442b-a338-24e9d79e4ee9.png",
type: "video", type: "video",
}, },
{
title: "福气满满",
tag: "新款",
tagType: "new",
desc: "福字当头,好运连连。送给最爱的人。",
cta: "立即制作",
cover:
"https://file.lihailezzc.com/resource/b48c41054c2633c478463ac1b1f1ca23.png",
},
]); ]);
const onFeatureTap = (item) => { const onFeatureTap = (item) => {
if (item.type === "avatar_decor" || item.type === "avatar_frame") {
uni.navigateTo({ url: "/pages/avatar/index" });
return;
}
if (item.type === "fortune") { if (item.type === "fortune") {
uni.navigateTo({ url: "/pages/fortune/index" }); uni.navigateTo({ url: "/pages/fortune/index" });
return; return;
} }
if (item.type === "card") { if (item.type === "card") {
uni.navigateTo({ url: "/pages/make/index" }); uni.switchTab({ url: "/pages/make/index" });
return;
}
if (item.type === "avatar_decor" || item.type === "avatar_frame") {
uni.navigateTo({ url: "/pages/avatar/index" });
return; return;
} }
uni.showToast({ title: `进入:${item.title}`, icon: "none" }); uni.showToast({ title: `进入:${item.title}`, icon: "none" });
@@ -164,7 +257,8 @@ const onMore = () => {
}; };
const onCta = (card) => { const onCta = (card) => {
uni.showToast({ title: `${card.cta} · ${card.title}`, icon: "none" }); // uni.showToast({ title: `${card.cta} · ${card.title}`, icon: "none" });
uni.switchTab({ url: "/pages/make/index" });
}; };
onPullDownRefresh(() => { onPullDownRefresh(() => {
@@ -309,6 +403,99 @@ onShareAppMessage(() => {
} }
} }
/* 今日灵感 */
.daily-section {
margin: 24rpx 24rpx 0;
.daily-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
.daily-title {
font-size: 32rpx;
font-weight: 700;
color: #333;
}
.daily-date {
font-size: 24rpx;
color: #999;
font-family: monospace;
}
}
.daily-card {
background: #fff;
border-radius: 20rpx;
padding: 30rpx;
box-shadow: 0 8rpx 24rpx rgba(255, 59, 48, 0.08);
position: relative;
overflow: hidden;
}
.daily-card::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 8rpx;
height: 100%;
background: #ff3b30;
}
.daily-content {
position: relative;
padding: 10rpx 20rpx;
.quote-mark {
font-size: 60rpx;
color: #ff3b30;
opacity: 0.2;
position: absolute;
line-height: 1;
font-family: serif;
}
.quote-mark:first-child {
top: -10rpx;
left: -10rpx;
}
.quote-mark.right {
bottom: -20rpx;
right: 0;
}
.daily-text {
font-size: 30rpx;
color: #444;
line-height: 1.8;
font-style: italic;
display: block;
text-align: justify;
}
}
.daily-actions {
display: flex;
justify-content: flex-end;
margin-top: 30rpx;
gap: 20rpx;
.action-btn {
display: flex;
align-items: center;
padding: 12rpx 24rpx;
border-radius: 999rpx;
font-size: 24rpx;
transition: all 0.2s;
.icon {
margin-right: 6rpx;
font-size: 26rpx;
}
}
.action-btn.copy {
background: #f5f5f5;
color: #666;
}
.action-btn.use {
background: #ff3b30;
color: #fff;
box-shadow: 0 4rpx 12rpx rgba(255, 59, 48, 0.3);
}
}
}
/* 通用区块标题 */ /* 通用区块标题 */
.section { .section {
margin-top: 28rpx; margin-top: 28rpx;
@@ -336,88 +523,99 @@ onShareAppMessage(() => {
} }
} }
/* 大家都在用 - 竖向列表 */ /* 大家都在用 - 网格列表 */
.use-list { .use-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
padding: 0 24rpx; padding: 0 24rpx;
margin-top: 16rpx; margin-top: 16rpx;
padding-bottom: 40rpx;
} }
.use-row { .use-card {
display: flex;
align-items: center;
background: #fff; background: #fff;
border-radius: 18rpx; border-radius: 20rpx;
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.06);
padding: 16rpx;
margin-bottom: 18rpx;
}
.thumb-wrap {
position: relative;
width: 120rpx;
height: 120rpx;
border-radius: 16rpx;
overflow: hidden; overflow: hidden;
margin-right: 16rpx; box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
display: flex;
flex-direction: column;
} }
.thumb { .card-cover-wrap {
position: relative;
width: 100%;
padding-bottom: 120%; /* 竖向卡片 */
background: #f5f5f5;
}
.card-cover {
position: absolute;
top: 0;
left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.thumb-play { .card-tag {
position: absolute; position: absolute;
right: 8rpx; top: 12rpx;
bottom: 8rpx; left: 12rpx;
width: 40rpx; padding: 4rpx 12rpx;
height: 40rpx; border-radius: 8rpx;
border-radius: 50%; font-size: 20rpx;
background: rgba(0, 0, 0, 0.55);
color: #fff; color: #fff;
font-size: 22rpx; font-weight: 500;
display: flex; &.tag--hot {
align-items: center; background: linear-gradient(135deg, #ff3b30, #ff9500);
justify-content: center; }
&.tag--featured {
background: linear-gradient(135deg, #007aff, #5ac8fa);
}
&.tag--hot2 {
background: linear-gradient(135deg, #ff2d55, #ff375f);
}
&.tag--new {
background: linear-gradient(135deg, #5856d6, #af52de);
}
&.tag--default {
background: rgba(0, 0, 0, 0.5);
}
} }
.use-right { .card-info {
padding: 16rpx;
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.title-line { .card-title {
display: flex;
align-items: center;
}
.use-title {
font-size: 28rpx; font-size: 28rpx;
color: #222;
font-weight: 600; font-weight: 600;
margin-right: 12rpx; color: #333;
margin-bottom: 8rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.tag { .card-desc {
font-size: 22rpx; font-size: 22rpx;
border-radius: 999rpx; color: #999;
padding: 4rpx 10rpx; line-height: 1.4;
margin-left: 4rpx; margin-bottom: 16rpx;
&.tag--hot { /* 限制2行 */
color: #ff6a00; overflow: hidden;
background: #fff4eb; text-overflow: ellipsis;
} display: -webkit-box;
&.tag--featured { -webkit-line-clamp: 2;
color: #ff4d6d; -webkit-box-orient: vertical;
background: #fff0f3;
}
&.tag--hot2 {
color: #7c4dff;
background: #f3efff;
}
} }
.use-desc { .card-footer {
margin-top: 6rpx; margin-top: auto;
font-size: 24rpx; display: flex;
color: #777; justify-content: flex-end;
line-height: 1.5;
} }
.use-cta { .cta-btn {
margin-top: 10rpx; font-size: 22rpx;
font-size: 24rpx;
color: #ff3b30; color: #ff3b30;
background: rgba(255, 59, 48, 0.08);
padding: 8rpx 20rpx;
border-radius: 999rpx;
font-weight: 500;
} }
</style> </style>

View File

@@ -12,16 +12,31 @@
<text class="main">新春快乐</text> <text class="main">新春快乐</text>
<text class="sub">2026 YEAR OF THE HORSE</text> <text class="sub">2026 YEAR OF THE HORSE</text>
</view> </view>
<view class="bubble" @tap="activeTool = 'text'"> <view
<text class="bubble-text" :style="{ color: selectedColor }">{{ class="bubble"
targetName + "\n " + blessingText @tap="activeTool = 'text'"
}}</text> :style="{ marginTop: 80 + bubbleOffsetY + 'rpx' }"
>
<text
class="bubble-text"
:style="{
color: selectedColor,
fontFamily: selectedFont.family,
fontSize: fontSize + 'rpx',
lineHeight: fontSize * 1.5 + 'rpx',
}"
>{{ targetName + "\n " + blessingText.content }}</text
>
</view> </view>
<view class="user"> <view class="user" :style="{ left: 160 + userOffsetX + 'rpx' }">
<image class="avatar" :src="userAvatar" mode="aspectFill" /> <image class="avatar" :src="userAvatar" mode="aspectFill" />
<view class="user-info"> <view class="user-info">
<text class="user-name">{{ signatureName }}</text> <text class="user-name" :style="{ color: signatureColor }">{{
<text class="user-desc">送上祝福</text> signatureName
}}</text>
<text class="user-desc" :style="{ color: signatureColor }"
>送上祝福</text
>
</view> </view>
</view> </view>
</view> </view>
@@ -105,6 +120,7 @@
v-model="targetName" v-model="targetName"
placeholder="请输入称呼" placeholder="请输入称呼"
placeholder-style="color:#ccc" placeholder-style="color:#ccc"
maxlength="5"
/> />
</view> </view>
@@ -122,11 +138,15 @@
v-for="(text, index) in displayedGreetings" v-for="(text, index) in displayedGreetings"
:key="index" :key="index"
class="greeting-card" class="greeting-card"
:class="{ active: blessingText === text }" :class="{ active: blessingText === text.content }"
@tap="selectGreeting(text)" @tap="selectGreeting(text)"
> >
<text class="greeting-text">{{ text }}</text> <text class="greeting-text">{{ text.content }}</text>
<view v-if="blessingText === text" class="check-mark"></view> <view
v-if="blessingText.content === text.content"
class="check-mark"
></view
>
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
@@ -141,14 +161,48 @@
v-model="signatureName" v-model="signatureName"
placeholder="请输入署名" placeholder="请输入署名"
placeholder-style="color:#ccc" placeholder-style="color:#ccc"
maxlength="5"
/> />
<text class="edit-icon"></text> <text class="edit-icon"></text>
</view> </view>
</view> </view>
<!-- 字体选择 -->
<view class="form-item">
<text class="label">字体样式</text>
<scroll-view scroll-x class="font-scroll" show-scrollbar="false">
<view class="font-list">
<view
v-for="(font, index) in fontList"
:key="index"
class="font-item"
:class="{ active: selectedFont.family === font.family }"
@tap="changeFont(font)"
>
<text :style="{ fontFamily: font.family }">{{
font.name
}}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 字体大小 -->
<view class="form-item">
<text class="label">字体大小</text>
<slider
:value="fontSize"
min="24"
max="64"
show-value
@change="(e) => (fontSize = e.detail.value)"
activeColor="#ff3b30"
/>
</view>
<!-- 文字颜色 --> <!-- 文字颜色 -->
<view class="form-item"> <view class="form-item">
<text class="label">文字颜色</text> <text class="label">祝福语颜色</text>
<view class="color-list"> <view class="color-list">
<view <view
v-for="(color, index) in textColors" v-for="(color, index) in textColors"
@@ -161,14 +215,47 @@
</view> </view>
</view> </view>
</view> </view>
<!-- 署名颜色 -->
<view class="form-item">
<text class="label">署名颜色</text>
<view class="color-list">
<view
v-for="(color, index) in textColors"
:key="index"
class="color-item"
:style="{ background: color }"
@tap="signatureColor = color"
>
<view v-if="signatureColor === color" class="color-check"></view>
</view>
</view>
</view>
</view> </view>
<!-- 图片/背景 --> <!-- 位置调整 -->
<view v-if="activeTool === 'image'" class="section"> <view v-if="activeTool === 'position'" class="section">
<view class="section-title"><text>替换背景</text></view> <view class="form-item">
<view class="row"> <text class="label">祝福语位置 (上下)</text>
<button class="btn" @tap="pickImage">从相册选择</button> <slider
<button class="btn" @tap="resetBackground">重置为模板背景</button> :value="bubbleOffsetY"
min="-200"
max="200"
show-value
@change="(e) => (bubbleOffsetY = e.detail.value)"
activeColor="#ff3b30"
/>
</view>
<view class="form-item">
<text class="label">署名位置 (左右)</text>
<slider
:value="userOffsetX"
min="-200"
max="200"
show-value
@change="(e) => (userOffsetX = e.detail.value)"
activeColor="#ff3b30"
/>
</view> </view>
</view> </view>
@@ -182,25 +269,44 @@
</view> </view>
<canvas <canvas
canvas-id="cardCanvas" type="2d"
id="cardCanvas"
class="hidden-canvas" class="hidden-canvas"
style="width: 540px; height: 960px" style="width: 540px; height: 960px"
/> />
<LoginPopup ref="loginPopupRef" @logind="handleLogind" />
</view> </view>
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from "vue"; import { ref, computed } from "vue";
import { getBavBarHeight, getDeviceInfo } from "@/utils/system"; import { getBavBarHeight, getDeviceInfo } from "@/utils/system";
import { createCardTmp, getCardTemplateList } from "@/api/make"; import { generateObjectId } from "@/utils/common";
import { createCardShareToken } from "@/api/card";
import { onShareAppMessage, onLoad, onReachBottom } from "@dcloudio/uni-app"; import {
createCardTmp,
updateCard,
getCardTemplateList,
getCardTemplateContentList,
} from "@/api/make";
import { createShareToken } from "@/api/system";
import {
onShareAppMessage,
onLoad,
onReachBottom,
onShow,
} from "@dcloudio/uni-app";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import LoginPopup from "@/components/LoginPopup/LoginPopup.vue";
const userStore = useUserStore();
const loginPopupRef = ref(null);
const isLoggedIn = computed(() => !!userStore.userInfo.nickName);
const templatePage = ref(1); const templatePage = ref(1);
const loadingTemplates = ref(false); const loadingTemplates = ref(false);
const hasMoreTemplates = ref(true); const hasMoreTemplates = ref(true);
const userStore = useUserStore();
const cardId = ref(""); const cardId = ref("");
const targetName = ref("祝您"); const targetName = ref("祝您");
@@ -210,26 +316,97 @@ const userAvatar = ref(
"https://file.lihailezzc.com/resource/b48c41054c2633c478463ac1b1f1ca23.png", "https://file.lihailezzc.com/resource/b48c41054c2633c478463ac1b1f1ca23.png",
); );
const blessingText = ref( const blessingText = ref({});
"岁末将至敬颂冬绥。平安喜乐万事胜意。祝您2026年大吉大利一马当先前程似锦龙马精神阖家安康", const fontSize = ref(32);
);
const textColors = ["#ffffff", "#ff3b30", "#F5A623", "#8B572A", "#000000"]; const textColors = [
const selectedColor = ref("#ffffff"); "#ffffff",
"#000000",
const greetingLib = [ "#ff3b30",
"在新的一年里身体健康,万事如意!马到成功,财源广进!", "#F5A623",
"岁末将至敬颂冬绥。平安喜乐万事胜意。祝您2026年大吉大利一马当先前程似锦龙马精神阖家安康", "#8B572A",
"一马当先,前程似锦!龙马精神,阖家安康!", "#D0021B",
"骏马奔腾,福运常在!策马扬鞭,步步高升!", "#F8E71C",
"新春快乐,阖家幸福!愿您在新的一年里,所有的希望都能如愿,所有的梦想都能实现。", "#7ED321",
"马年大吉!愿您事业如骏马奔腾,生活如春风得意!", "#4A90E2",
"#9013FE",
"#FFC0CB",
]; ];
const selectedColor = ref("#ffffff");
const signatureColor = ref("#ffffff");
const fontList = [
{ name: "默认", family: "PingFang SC", url: "" },
{
name: "毛笔",
family: "MaoBi",
url: "https://file.lihailezzc.com/MaShanZheng-Regular.ttf", // 示例地址
},
{
name: "手写",
family: "ShouXie",
url: "https://file.lihailezzc.com/ZhiMangXing-Regular.ttf", // 示例地址
},
{
name: "可爱",
family: "KeAi",
url: "https://file.lihailezzc.com/ZCOOLKuaiLe-Regular.ttf", // 示例地址
},
{
name: "草书",
family: "LiuJianMaoCao",
url: "https://file.lihailezzc.com/LiuJianMaoCao-Regular.ttf", // 示例地址
},
];
const selectedFont = ref(fontList[0]);
const loadedFonts = ref(new Set()); // 记录已加载的字体
const changeFont = (font) => {
// 1. 如果是默认字体或已加载过的字体,直接应用
if (!font.url || loadedFonts.value.has(font.family)) {
selectedFont.value = font;
return;
}
// 2. 否则加载字体
uni.showLoading({ title: "加载字体中", mask: true });
uni.loadFontFace({
global: true,
family: font.family,
source: `url("${font.url}")`,
scopes: ["webview", "native"],
success: () => {
selectedFont.value = font;
loadedFonts.value.add(font.family); // 标记为已加载
uni.hideLoading();
},
fail: (err) => {
console.error(err);
uni.hideLoading();
// 如果加载失败,可以尝试直接设置(有些情况可能已经缓存或本地支持)
// 或者提示用户
uni.showToast({ title: "字体加载失败", icon: "none" });
},
});
};
const greetingLib = ref([]);
const greetingIndex = ref(0);
const bubbleOffsetY = ref(0);
const userOffsetX = ref(0);
onLoad((options) => { onLoad((options) => {
cardId.value = "69674f307307beac4519025f";
// createCard();
getTemplateList(); getTemplateList();
getTemplateContentList();
});
onShow(() => {
const tempBlessing = uni.getStorageSync("TEMP_BLESSING_TEXT");
if (tempBlessing) {
blessingText.value = { content: tempBlessing, id: "" };
uni.removeStorageSync("TEMP_BLESSING_TEXT");
}
}); });
onReachBottom(() => { onReachBottom(() => {
@@ -238,15 +415,11 @@ onReachBottom(() => {
} }
}); });
const createCard = async () => { const createCard = () => {
const res = await createCardTmp({ const id = generateObjectId();
targetName: targetName.value, createCardTmp({ id });
signatureName: signatureName.value, cardId.value = id;
blessingText: blessingText.value, return id;
});
if (res.id) {
cardId.value = res.id;
}
}; };
const getTemplateList = async (isLoadMore = false) => { const getTemplateList = async (isLoadMore = false) => {
@@ -292,31 +465,61 @@ const getTemplateList = async (isLoadMore = false) => {
} }
}; };
const getTemplateContentList = async () => {
const res = await getCardTemplateContentList();
if (res.length) {
greetingLib.value = res;
displayedGreetings.value = greetingLib.value.slice(0, 2);
if (!blessingText.value.content) {
blessingText.value = greetingLib.value[0] || {};
}
}
};
const loadMoreTemplates = () => { const loadMoreTemplates = () => {
getTemplateList(true); getTemplateList(true);
}; };
onShareAppMessage(async () => { onShareAppMessage(async () => {
if (!isLoggedIn.value) {
return {
title: "新春祝福",
path: "/pages/index/index",
};
}
// 1. 确保有 cardId (如果内容有变动,最好是新建)
const id = createCard();
if (!id) {
return {
title: "新春祝福",
path: "/pages/index/index",
};
}
const deviceInfo = getDeviceInfo(); const deviceInfo = getDeviceInfo();
const shareTokenRes = await createCardShareToken({ const shareTokenRes = await createShareToken({
cardId: cardId.value, scene: "card_generate",
targetId: id,
...deviceInfo, ...deviceInfo,
}); });
return { return {
title: "新春祝福", title: "新春祝福",
path: "/pages/detail/index?shareToken=" + shareTokenRes.shareToken, path: "/pages/detail/index?shareToken=" + shareTokenRes.shareToken,
imageUrl: "/static/images/bg.jpg", imageUrl: currentTemplate.value?.imageUrl || "/static/images/bg.jpg",
}; };
}); });
const displayedGreetings = ref(greetingLib.slice(0, 2)); const displayedGreetings = ref([]);
const refreshGreetings = () => { const refreshGreetings = () => {
const start = Math.floor(Math.random() * (greetingLib.length - 1)); if (!greetingLib.value.length) return;
// 简单随机逻辑,实际可优化
let next = greetingLib.slice(start, start + 2); const nextIndex = (greetingIndex.value + 2) % greetingLib.value.length;
greetingIndex.value = nextIndex;
let next = greetingLib.value.slice(nextIndex, nextIndex + 2);
if (next.length < 2) { if (next.length < 2) {
next = [...next, ...greetingLib.slice(0, 2 - next.length)]; next = [...next, ...greetingLib.value.slice(0, 2 - next.length)];
} }
displayedGreetings.value = next; displayedGreetings.value = next;
}; };
@@ -328,7 +531,7 @@ const selectGreeting = (text) => {
const tools = [ const tools = [
{ type: "template", text: "模板", icon: "▦" }, { type: "template", text: "模板", icon: "▦" },
{ type: "text", text: "文字", icon: "文" }, { type: "text", text: "文字", icon: "文" },
// { type: "image", text: "图片/背景", icon: "图" }, { type: "position", text: "位置", icon: "图" },
// { type: "avatar", text: "头像挂饰", icon: "饰" }, // { type: "avatar", text: "头像挂饰", icon: "饰" },
]; ];
const activeTool = ref("template"); const activeTool = ref("template");
@@ -366,27 +569,39 @@ const preview = () => {
}; };
const shareOrSave = async () => { const shareOrSave = async () => {
// const tempPath = await saveByCanvas(false); if (!isLoggedIn.value) {
// const fileKeyRes = await uni.uploadFile({ loginPopupRef.value.open();
// url: "https://api.ai-meng.com/api/common/upload", return;
// filePath: tempPath, }
// name: "file", // 和后端接收文件字段名一致
// header: { const tempPath = await saveByCanvas(false);
// "x-app-id": "69665538a49b8ae3be50fe5d", const fileKeyRes = await uni.uploadFile({
// }, url: "https://api.ai-meng.com/api/common/upload",
// }); filePath: tempPath,
// if (fileKeyRes.statusCode < 400) { name: "file", // 和后端接收文件字段名一致
// const keyJson = JSON.parse(fileKeyRes.data); header: {
// const url = `https://file.lihailezzc.com/${keyJson?.data.key}`; "x-app-id": "69665538a49b8ae3be50fe5d",
// // const url = },
// // "https://file.lihailezzc.com/resource/99c9f7e0086ed66d20bd1675b4ab22e9.png"; });
// updateCard({ if (fileKeyRes.statusCode < 400) {
// id: cardId.value, const keyJson = JSON.parse(fileKeyRes.data);
// imageUrl: url, const url = `https://file.lihailezzc.com/${keyJson?.data.key}`;
// status: 1, // const url =
// }); // "https://file.lihailezzc.com/resource/99c9f7e0086ed66d20bd1675b4ab22e9.png";
// } // 1. 确保有 cardId
// createCard(); if (!cardId.value) {
createCard();
}
updateCard({
id: cardId.value,
imageUrl: url,
status: 1,
blessingId: blessingText.value?.id || "",
blessingTo: targetName.value,
blessingFrom: signatureName.value,
templateId: currentTemplate.value?.id || "",
});
}
// uni.showToast({ title: '已保存到相册并可分享', icon: 'none' }) // uni.showToast({ title: '已保存到相册并可分享', icon: 'none' })
}; };
@@ -395,74 +610,114 @@ const showMore = () => {
}; };
const saveByCanvas = async (save = true) => { const saveByCanvas = async (save = true) => {
const ctx = uni.createCanvasContext("cardCanvas"); return new Promise((resolve, reject) => {
const query = uni.createSelectorQuery();
query
.select("#cardCanvas")
.fields({ node: true, size: true })
.exec(async (res) => {
if (!res[0] || !res[0].node) {
reject("Canvas not found");
return;
}
const canvas = res[0].node;
const ctx = canvas.getContext("2d");
// 初始化画布尺寸
const dpr = uni.getSystemInfoSync().pixelRatio;
// 保持 540x960 的逻辑尺寸,为了清晰度可以考虑 * dpr
// 但为了保持和原来一致的输出尺寸,这里先固定物理尺寸
// 如果要高清,可以 set width = 540 * dpr然后 scale(dpr, dpr)
// 这里为了简单兼容原逻辑,我们让物理尺寸等于逻辑尺寸
canvas.width = 540;
canvas.height = 960;
// 画布尺寸rpx 转 px // 画布尺寸rpx 转 px
const W = 540; const W = 540;
const H = 960; const H = 960;
// 辅助函数:加载图片为 Image 对象
const loadCanvasImage = (url) => {
return new Promise((resolve, reject) => {
const img = canvas.createImage();
img.onload = () => resolve(img);
img.onerror = (e) => reject(e);
img.src = url;
});
};
try {
// 1⃣ 画背景 // 1⃣ 画背景
// ⭐ 先加载背景图 // ⭐ 先加载背景图
const [bgPath, avatarPath] = await Promise.all([ const [bgImg, avatarImg] = await Promise.all([
loadImage(currentTemplate?.value?.imageUrl), loadCanvasImage(currentTemplate?.value?.imageUrl),
loadImage(userAvatar.value), loadCanvasImage(userAvatar.value),
]); ]);
ctx.drawImage(bgPath, 0, 0, W, H); ctx.drawImage(bgImg, 0, 0, W, H);
// 2⃣ 半透明遮罩(和你 UI 一致) // 2⃣ 半透明遮罩(和你 UI 一致)
ctx.setFillStyle("rgba(0,0,0,0.08)"); ctx.fillStyle = "rgba(0,0,0,0.08)";
ctx.fillRect(0, 0, W, H); ctx.fillRect(0, 0, W, H);
// 3⃣ 标题 // 3⃣ 标题
ctx.setFillStyle("#ffffff"); ctx.fillStyle = "#ffffff";
ctx.setFontSize(42); ctx.font = "42px sans-serif"; // 默认字体
ctx.setTextAlign("center"); ctx.textAlign = "center";
ctx.textBaseline = "alphabetic"; // Canvas 2D 默认是 alphabetic
ctx.fillText("新春快乐", W / 2, 120); ctx.fillText("新春快乐", W / 2, 120);
ctx.setFontSize(22); ctx.font = "22px sans-serif";
ctx.setGlobalAlpha(0.9); ctx.globalAlpha = 0.9;
ctx.fillText("2026 YEAR OF THE HORSE", W / 2, 165); ctx.fillText("2026 YEAR OF THE HORSE", W / 2, 165);
ctx.setGlobalAlpha(1); ctx.globalAlpha = 1;
// 4⃣ 祝福语气泡 // 4⃣ 祝福语气泡
drawBubbleText(ctx, { drawBubbleText(ctx, {
text: targetName.value + "\n " + blessingText.value, text: targetName.value + "\n " + blessingText.value.content,
x: 70, x: 70,
y: 260, y: 260 + bubbleOffsetY.value,
maxWidth: 400, maxWidth: 400,
fontSize: 32, fontSize: fontSize.value,
lineHeight: 46, lineHeight: fontSize.value * 1.5,
backgroundColor: "rgba(255,255,255,0.85)", backgroundColor: "rgba(255,255,255,0.85)",
textColor: selectedColor.value, textColor: selectedColor.value,
fontFamily: selectedFont.value.family,
}); });
drawUserBubble(ctx, { drawUserBubble(ctx, {
x: 40, x: 160 + userOffsetX.value,
y: H - 120, y: H - 120,
avatarPath: avatarPath, avatarImg: avatarImg, // 传入 Image 对象
username: signatureName.value, username: signatureName.value,
desc: "送上祝福", desc: "送上祝福",
textColor: signatureColor.value,
}); });
// 6⃣ 输出 // 6⃣ 输出
const tempPath = await new Promise((resolve, reject) => {
ctx.draw(false, () => {
uni.canvasToTempFilePath({ uni.canvasToTempFilePath({
canvasId: "cardCanvas", canvas: canvas, // Canvas 2D 必须传 canvas 实例
width: W,
height: H,
destWidth: W,
destHeight: H,
success: (res) => { success: (res) => {
if (save) saveImage(res.tempFilePath); if (save) saveImage(res.tempFilePath);
resolve(res.tempFilePath); resolve(res.tempFilePath);
}, },
fail: (err) => reject(err), fail: (err) => reject(err),
}); });
} catch (error) {
console.error("Canvas draw error:", error);
reject(error);
}
}); });
}); });
return tempPath;
}; };
const loadImage = (url) => { const loadImage = (url) => {
// 此函数保留给其他可能用到的地方,但 saveByCanvas 内部使用 loadCanvasImage
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
uni.getImageInfo({ uni.getImageInfo({
src: url, src: url,
@@ -505,12 +760,10 @@ function drawBubbleText(ctx, options) {
fontSize = 32, fontSize = 32,
fontFamily = "PingFang SC", fontFamily = "PingFang SC",
} = options; } = options;
if (!text) return; if (!text) return;
ctx.setFontSize(fontSize); ctx.fillStyle = textColor;
ctx.setFillStyle(textColor); ctx.font = `${fontSize}px '${fontFamily}'`;
ctx.font = `${fontSize}px ${fontFamily}`;
ctx.textAlign = "left"; ctx.textAlign = "left";
ctx.textBaseline = "top"; ctx.textBaseline = "top";
@@ -552,7 +805,7 @@ function drawBubbleText(ctx, options) {
// ) // )
// 4⃣ 绘制文字 // 4⃣ 绘制文字
ctx.setFillStyle(textColor); ctx.fillStyle = textColor;
lines.forEach((line, index) => { lines.forEach((line, index) => {
ctx.fillText(line, x + padding, y + padding + index * lineHeight); ctx.fillText(line, x + padding, y + padding + index * lineHeight);
@@ -563,7 +816,7 @@ function drawUserBubble(ctx, options) {
const { const {
x = 40, // 气泡起点 x x = 40, // 气泡起点 x
y = 860, // 气泡起点 y y = 860, // 气泡起点 y
avatarPath, // 头像本地路径 avatarImg, // CanvasImage 对象
username = "zzc", username = "zzc",
desc = "送上祝福", desc = "送上祝福",
avatarSize = 64, // 头像直径 avatarSize = 64, // 头像直径
@@ -576,11 +829,11 @@ function drawUserBubble(ctx, options) {
// 设置字体 // 设置字体
ctx.textBaseline = "top"; ctx.textBaseline = "top";
ctx.font = `${fontSizeName}px PingFang SC`; ctx.font = `${fontSizeName}px 'PingFang SC'`;
// 测量文字宽度 // 测量文字宽度
const nameWidth = ctx.measureText(username).width; const nameWidth = ctx.measureText(username).width;
ctx.font = `${fontSizeDesc}px PingFang SC`; ctx.font = `${fontSizeDesc}px 'PingFang SC'`;
const descWidth = ctx.measureText(desc).width; const descWidth = ctx.measureText(desc).width;
// 计算气泡宽度和高度 // 计算气泡宽度和高度
@@ -614,19 +867,21 @@ function drawUserBubble(ctx, options) {
Math.PI * 2, Math.PI * 2,
); );
ctx.clip(); ctx.clip();
ctx.drawImage(avatarPath, avatarX, avatarY, avatarSize, avatarSize); if (avatarImg) {
ctx.drawImage(avatarImg, avatarX, avatarY, avatarSize, avatarSize);
}
ctx.restore(); ctx.restore();
// 3⃣ 绘制文字 // 3⃣ 绘制文字
const textX = avatarX + avatarSize + padding; const textX = avatarX + avatarSize + padding;
const textY = y + padding; const textY = y + padding;
ctx.setFillStyle(textColor); ctx.fillStyle = textColor;
ctx.font = `${fontSizeName}px PingFang SC`; ctx.font = `${fontSizeName}px 'PingFang SC'`;
ctx.fillText(username, textX, textY); ctx.fillText(username, textX, textY);
ctx.font = `${fontSizeDesc}px PingFang SC`; ctx.font = `${fontSizeDesc}px 'PingFang SC'`;
ctx.setGlobalAlpha(0.6); ctx.globalAlpha = 0.6;
ctx.fillText(desc, textX, textY + fontSizeName + 4); ctx.fillText(desc, textX, textY + fontSizeName + 4);
ctx.setGlobalAlpha(1); ctx.globalAlpha = 1;
} }
function drawRoundRect(ctx, x, y, w, h, r, color) { function drawRoundRect(ctx, x, y, w, h, r, color) {
@@ -922,8 +1177,8 @@ function drawRoundRect(ctx, x, y, w, h, r, color) {
box-sizing: border-box; box-sizing: border-box;
} }
.greeting-card.active { .greeting-card.active {
background: #fff5f5;
border-color: #ff3b30; border-color: #ff3b30;
background: #fff6f5;
} }
.greeting-text { .greeting-text {
font-size: 24rpx; font-size: 24rpx;
@@ -961,9 +1216,35 @@ function drawRoundRect(ctx, x, y, w, h, r, color) {
transform: translateY(-50%); transform: translateY(-50%);
color: #999; color: #999;
} }
.font-scroll {
white-space: nowrap;
width: 100%;
}
.font-list {
display: flex;
padding: 4rpx;
}
.font-item {
padding: 12rpx 24rpx;
background: #f5f5f5;
border-radius: 8rpx;
margin-right: 16rpx;
border: 2rpx solid transparent;
transition: all 0.3s;
}
.font-item.active {
background: #fff5f5;
border-color: #ff3b30;
color: #ff3b30;
}
.color-list { .color-list {
display: flex; display: flex;
gap: 24rpx; gap: 20rpx;
flex-wrap: wrap;
} }
.color-item { .color-item {
width: 64rpx; width: 64rpx;

BIN
static/icon/bizhi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1 +1,6 @@
export const generateObjectId = (
m = Math,
d = Date,
h = 16,
s = (s) => m.floor(s).toString(h),
) => s(d.now() / 1000) + " ".repeat(h).replace(/./g, () => s(m.random() * h));