Compare commits
12 Commits
083d52a5d7
...
adde98c05c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
adde98c05c | ||
|
|
1519c4f486 | ||
|
|
3ecc525d2c | ||
|
|
64b2abd29e | ||
|
|
2b2af1bab4 | ||
|
|
303654b1e1 | ||
|
|
9f5c5d2d1f | ||
|
|
0181066b34 | ||
|
|
bdaf3a3be1 | ||
|
|
1c094ed14b | ||
|
|
f7f62cebb5 | ||
|
|
da194c920c |
@@ -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",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
16
pages.json
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
v-if="card.tag"
|
||||||
<view class="use-right">
|
class="card-tag"
|
||||||
<view class="title-line">
|
:class="`tag--${card.tagType || 'default'}`"
|
||||||
<text class="use-title">{{ card.title }}</text>
|
|
||||||
<text
|
|
||||||
v-if="card.tag"
|
|
||||||
class="tag"
|
|
||||||
:class="`tag--${card.tagType || 'default'}`"
|
|
||||||
>{{ card.tag }}</text
|
|
||||||
>
|
|
||||||
</view>
|
|
||||||
<text class="use-desc">{{ card.desc }}</text>
|
|
||||||
<text class="use-cta" @tap.stop="onCta(card)"
|
|
||||||
>{{ card.cta }} ></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>
|
</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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
// 画布尺寸(rpx 转 px)
|
const canvas = res[0].node;
|
||||||
const W = 540;
|
const ctx = canvas.getContext("2d");
|
||||||
const H = 960;
|
|
||||||
|
|
||||||
// 1️⃣ 画背景
|
// 初始化画布尺寸
|
||||||
// ⭐ 先加载背景图
|
const dpr = uni.getSystemInfoSync().pixelRatio;
|
||||||
const [bgPath, avatarPath] = await Promise.all([
|
// 保持 540x960 的逻辑尺寸,为了清晰度可以考虑 * dpr,
|
||||||
loadImage(currentTemplate?.value?.imageUrl),
|
// 但为了保持和原来一致的输出尺寸,这里先固定物理尺寸
|
||||||
loadImage(userAvatar.value),
|
// 如果要高清,可以 set width = 540 * dpr,然后 scale(dpr, dpr)
|
||||||
]);
|
// 这里为了简单兼容原逻辑,我们让物理尺寸等于逻辑尺寸
|
||||||
|
canvas.width = 540;
|
||||||
|
canvas.height = 960;
|
||||||
|
|
||||||
ctx.drawImage(bgPath, 0, 0, W, H);
|
// 画布尺寸(rpx 转 px)
|
||||||
|
const W = 540;
|
||||||
|
const H = 960;
|
||||||
|
|
||||||
// 2️⃣ 半透明遮罩(和你 UI 一致)
|
// 辅助函数:加载图片为 Image 对象
|
||||||
ctx.setFillStyle("rgba(0,0,0,0.08)");
|
const loadCanvasImage = (url) => {
|
||||||
ctx.fillRect(0, 0, W, H);
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = canvas.createImage();
|
||||||
|
img.onload = () => resolve(img);
|
||||||
|
img.onerror = (e) => reject(e);
|
||||||
|
img.src = url;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// 3️⃣ 标题
|
try {
|
||||||
ctx.setFillStyle("#ffffff");
|
// 1️⃣ 画背景
|
||||||
ctx.setFontSize(42);
|
// ⭐ 先加载背景图
|
||||||
ctx.setTextAlign("center");
|
const [bgImg, avatarImg] = await Promise.all([
|
||||||
ctx.fillText("新春快乐", W / 2, 120);
|
loadCanvasImage(currentTemplate?.value?.imageUrl),
|
||||||
|
loadCanvasImage(userAvatar.value),
|
||||||
|
]);
|
||||||
|
|
||||||
ctx.setFontSize(22);
|
ctx.drawImage(bgImg, 0, 0, W, H);
|
||||||
ctx.setGlobalAlpha(0.9);
|
|
||||||
ctx.fillText("2026 YEAR OF THE HORSE", W / 2, 165);
|
|
||||||
ctx.setGlobalAlpha(1);
|
|
||||||
|
|
||||||
// 4️⃣ 祝福语气泡
|
// 2️⃣ 半透明遮罩(和你 UI 一致)
|
||||||
drawBubbleText(ctx, {
|
ctx.fillStyle = "rgba(0,0,0,0.08)";
|
||||||
text: targetName.value + "\n " + blessingText.value,
|
ctx.fillRect(0, 0, W, H);
|
||||||
x: 70,
|
|
||||||
y: 260,
|
|
||||||
maxWidth: 400,
|
|
||||||
fontSize: 32,
|
|
||||||
lineHeight: 46,
|
|
||||||
backgroundColor: "rgba(255,255,255,0.85)",
|
|
||||||
textColor: selectedColor.value,
|
|
||||||
});
|
|
||||||
|
|
||||||
drawUserBubble(ctx, {
|
// 3️⃣ 标题
|
||||||
x: 40,
|
ctx.fillStyle = "#ffffff";
|
||||||
y: H - 120,
|
ctx.font = "42px sans-serif"; // 默认字体
|
||||||
avatarPath: avatarPath,
|
ctx.textAlign = "center";
|
||||||
username: signatureName.value,
|
ctx.textBaseline = "alphabetic"; // Canvas 2D 默认是 alphabetic
|
||||||
desc: "送上祝福",
|
ctx.fillText("新春快乐", W / 2, 120);
|
||||||
});
|
|
||||||
|
|
||||||
// 6️⃣ 输出
|
ctx.font = "22px sans-serif";
|
||||||
const tempPath = await new Promise((resolve, reject) => {
|
ctx.globalAlpha = 0.9;
|
||||||
ctx.draw(false, () => {
|
ctx.fillText("2026 YEAR OF THE HORSE", W / 2, 165);
|
||||||
uni.canvasToTempFilePath({
|
ctx.globalAlpha = 1;
|
||||||
canvasId: "cardCanvas",
|
|
||||||
success: (res) => {
|
// 4️⃣ 祝福语气泡
|
||||||
if (save) saveImage(res.tempFilePath);
|
drawBubbleText(ctx, {
|
||||||
resolve(res.tempFilePath);
|
text: targetName.value + "\n " + blessingText.value.content,
|
||||||
},
|
x: 70,
|
||||||
fail: (err) => reject(err),
|
y: 260 + bubbleOffsetY.value,
|
||||||
|
maxWidth: 400,
|
||||||
|
fontSize: fontSize.value,
|
||||||
|
lineHeight: fontSize.value * 1.5,
|
||||||
|
backgroundColor: "rgba(255,255,255,0.85)",
|
||||||
|
textColor: selectedColor.value,
|
||||||
|
fontFamily: selectedFont.value.family,
|
||||||
|
});
|
||||||
|
|
||||||
|
drawUserBubble(ctx, {
|
||||||
|
x: 160 + userOffsetX.value,
|
||||||
|
y: H - 120,
|
||||||
|
avatarImg: avatarImg, // 传入 Image 对象
|
||||||
|
username: signatureName.value,
|
||||||
|
desc: "送上祝福",
|
||||||
|
textColor: signatureColor.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6️⃣ 输出
|
||||||
|
uni.canvasToTempFilePath({
|
||||||
|
canvas: canvas, // Canvas 2D 必须传 canvas 实例
|
||||||
|
width: W,
|
||||||
|
height: H,
|
||||||
|
destWidth: W,
|
||||||
|
destHeight: H,
|
||||||
|
success: (res) => {
|
||||||
|
if (save) saveImage(res.tempFilePath);
|
||||||
|
resolve(res.tempFilePath);
|
||||||
|
},
|
||||||
|
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
|
After Width: | Height: | Size: 6.9 KiB |
BIN
static/images/tabBar/creation.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
static/images/tabBar/creation_s.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 5.0 KiB |
BIN
static/images/tabBar/home_old.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 5.0 KiB |
BIN
static/images/tabBar/home_s_old.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 5.9 KiB |
BIN
static/images/tabBar/me_old.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 5.8 KiB |
BIN
static/images/tabBar/me_s_old.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
@@ -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));
|
||||||
|
|||||||