2026-01-09 11:24:40 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<view class="make-page" :style="{ paddingTop: getBavBarHeight() + 'px' }">
|
|
|
|
|
|
<!-- 预览卡片 -->
|
|
|
|
|
|
<view class="card-preview">
|
2026-01-21 23:58:21 +08:00
|
|
|
|
<image
|
|
|
|
|
|
class="card-bg"
|
|
|
|
|
|
:src="currentTemplate?.imageUrl"
|
|
|
|
|
|
mode="aspectFill"
|
|
|
|
|
|
/>
|
2026-01-09 11:24:40 +08:00
|
|
|
|
<view class="card-overlay">
|
2026-01-31 11:33:22 +08:00
|
|
|
|
<view class="watermark">年禧集.马年春节祝福</view>
|
2026-01-31 11:56:39 +08:00
|
|
|
|
<!-- <view class="title">
|
2026-01-09 11:24:40 +08:00
|
|
|
|
<text class="main">新春快乐</text>
|
|
|
|
|
|
<text class="sub">2026 YEAR OF THE HORSE</text>
|
2026-01-31 11:56:39 +08:00
|
|
|
|
</view> -->
|
2026-01-22 08:55:00 +08:00
|
|
|
|
<view
|
|
|
|
|
|
class="bubble"
|
|
|
|
|
|
@tap="activeTool = 'text'"
|
2026-01-31 11:56:39 +08:00
|
|
|
|
:style="{
|
|
|
|
|
|
marginTop: 230 + bubbleOffsetY + 'rpx',
|
|
|
|
|
|
maxWidth: bubbleMaxWidth + 80 + 'rpx',
|
|
|
|
|
|
}"
|
2026-01-22 08:55:00 +08:00
|
|
|
|
>
|
2026-01-22 09:34:08 +08:00
|
|
|
|
<text
|
|
|
|
|
|
class="bubble-text"
|
|
|
|
|
|
:style="{
|
|
|
|
|
|
color: selectedColor,
|
|
|
|
|
|
fontFamily: selectedFont.family,
|
2026-01-22 15:08:26 +08:00
|
|
|
|
fontSize: fontSize + 'rpx',
|
|
|
|
|
|
lineHeight: fontSize * 1.5 + 'rpx',
|
2026-01-22 09:34:08 +08:00
|
|
|
|
}"
|
2026-01-22 22:11:02 +08:00
|
|
|
|
>{{ targetName + "\n " + blessingText.content }}</text
|
2026-01-22 09:34:08 +08:00
|
|
|
|
>
|
2026-01-09 11:24:40 +08:00
|
|
|
|
</view>
|
2026-01-31 11:25:53 +08:00
|
|
|
|
<view class="user" :style="{ left: 160 + userOffsetX + 'rpx', bottom: 40 - userOffsetY + 'rpx' }">
|
2026-01-15 08:43:10 +08:00
|
|
|
|
<image class="avatar" :src="userAvatar" mode="aspectFill" />
|
2026-01-09 11:24:40 +08:00
|
|
|
|
<view class="user-info">
|
2026-01-22 15:08:26 +08:00
|
|
|
|
<text class="user-name" :style="{ color: signatureColor }">{{
|
|
|
|
|
|
signatureName
|
|
|
|
|
|
}}</text>
|
|
|
|
|
|
<text class="user-desc" :style="{ color: signatureColor }"
|
|
|
|
|
|
>送上祝福</text
|
|
|
|
|
|
>
|
2026-01-09 11:24:40 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view class="tip-line">
|
2026-01-31 11:25:53 +08:00
|
|
|
|
<text>分享或保存即可去除水印</text>
|
2026-01-09 11:24:40 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 编辑工具区 -->
|
|
|
|
|
|
<view class="editor-panel">
|
|
|
|
|
|
<view class="drag-handle"></view>
|
|
|
|
|
|
|
2026-01-21 23:58:21 +08:00
|
|
|
|
<!-- 底部操作 -->
|
|
|
|
|
|
<view class="bottom-actions">
|
|
|
|
|
|
<button class="btn secondary" @tap="preview">
|
|
|
|
|
|
<uni-icons type="cloud-download" size="20" color="#888"></uni-icons>
|
|
|
|
|
|
<view>保存</view>
|
|
|
|
|
|
</button>
|
2026-01-27 19:35:47 +08:00
|
|
|
|
<button open-type="share" class="btn primary">
|
2026-01-21 23:58:21 +08:00
|
|
|
|
<uni-icons
|
|
|
|
|
|
type="paperplane-filled"
|
|
|
|
|
|
size="20"
|
|
|
|
|
|
color="#fff"
|
|
|
|
|
|
></uni-icons>
|
|
|
|
|
|
<view>分享给好友</view>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
2026-01-09 11:24:40 +08:00
|
|
|
|
<!-- 功能入口 -->
|
|
|
|
|
|
<view class="tools">
|
|
|
|
|
|
<view
|
|
|
|
|
|
v-for="(tool, idx) in tools"
|
|
|
|
|
|
:key="idx"
|
|
|
|
|
|
class="tool-item"
|
|
|
|
|
|
:class="{ active: activeTool === tool.type }"
|
|
|
|
|
|
@tap="activeTool = tool.type"
|
|
|
|
|
|
>
|
|
|
|
|
|
<view class="tool-icon">{{ tool.icon }}</view>
|
|
|
|
|
|
<text class="tool-text">{{ tool.text }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 模板区 -->
|
|
|
|
|
|
<view v-if="activeTool === 'template'" class="section">
|
|
|
|
|
|
<view class="section-title">
|
|
|
|
|
|
<text>热门模板</text>
|
|
|
|
|
|
</view>
|
2026-01-22 00:01:30 +08:00
|
|
|
|
<view class="tpl-scroll">
|
2026-01-21 23:58:21 +08:00
|
|
|
|
<view class="tpl-grid">
|
2026-01-09 11:24:40 +08:00
|
|
|
|
<view
|
|
|
|
|
|
v-for="(tpl, i) in templates"
|
|
|
|
|
|
:key="i"
|
|
|
|
|
|
class="tpl-card"
|
2026-01-21 23:58:21 +08:00
|
|
|
|
:class="{ selected: tpl?.id === currentTemplate?.id }"
|
2026-01-09 11:24:40 +08:00
|
|
|
|
@tap="applyTemplate(tpl)"
|
|
|
|
|
|
>
|
2026-01-21 23:58:21 +08:00
|
|
|
|
<image :src="tpl.imageUrl" class="tpl-cover" mode="aspectFill" />
|
2026-01-09 11:24:40 +08:00
|
|
|
|
<view class="tpl-name">{{ tpl.name }}</view>
|
2026-01-21 23:58:21 +08:00
|
|
|
|
<view v-if="tpl?.id === currentTemplate?.id" class="tpl-check"
|
2026-01-15 08:43:10 +08:00
|
|
|
|
>✔</view
|
|
|
|
|
|
>
|
2026-01-09 11:24:40 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2026-01-21 23:58:21 +08:00
|
|
|
|
<view v-if="loadingTemplates" class="loading-more">加载中...</view>
|
|
|
|
|
|
<view
|
|
|
|
|
|
v-else-if="!hasMoreTemplates && templates.length > 0"
|
|
|
|
|
|
class="no-more"
|
|
|
|
|
|
>没有更多了</view
|
|
|
|
|
|
>
|
2026-01-22 00:01:30 +08:00
|
|
|
|
</view>
|
2026-01-09 11:24:40 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 文字编辑 -->
|
2026-01-15 08:43:10 +08:00
|
|
|
|
<view v-if="activeTool === 'text'" class="section text-edit-section">
|
|
|
|
|
|
<!-- 祝贺对象 -->
|
|
|
|
|
|
<view class="form-item">
|
|
|
|
|
|
<text class="label">祝贺对象</text>
|
|
|
|
|
|
<input
|
|
|
|
|
|
class="input-box"
|
|
|
|
|
|
v-model="targetName"
|
|
|
|
|
|
placeholder="请输入称呼"
|
|
|
|
|
|
placeholder-style="color:#ccc"
|
2026-01-22 15:08:26 +08:00
|
|
|
|
maxlength="5"
|
2026-01-15 08:43:10 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 祝福语库 -->
|
|
|
|
|
|
<view class="form-item">
|
|
|
|
|
|
<view class="label-row">
|
|
|
|
|
|
<text class="label">祝福语库</text>
|
|
|
|
|
|
<view class="refresh-btn" @tap="refreshGreetings">
|
|
|
|
|
|
<text class="refresh-icon">↻</text> 换一批
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<scroll-view scroll-x class="greeting-scroll" show-scrollbar="false">
|
|
|
|
|
|
<view class="greeting-list">
|
|
|
|
|
|
<view
|
|
|
|
|
|
v-for="(text, index) in displayedGreetings"
|
|
|
|
|
|
:key="index"
|
|
|
|
|
|
class="greeting-card"
|
2026-01-22 22:11:02 +08:00
|
|
|
|
:class="{ active: blessingText === text.content }"
|
2026-01-15 08:43:10 +08:00
|
|
|
|
@tap="selectGreeting(text)"
|
|
|
|
|
|
>
|
2026-01-22 22:11:02 +08:00
|
|
|
|
<text class="greeting-text">{{ text.content }}</text>
|
|
|
|
|
|
<view
|
|
|
|
|
|
v-if="blessingText.content === text.content"
|
|
|
|
|
|
class="check-mark"
|
|
|
|
|
|
>✔</view
|
|
|
|
|
|
>
|
2026-01-15 08:43:10 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</scroll-view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 署名 -->
|
|
|
|
|
|
<view class="form-item">
|
|
|
|
|
|
<text class="label">署名</text>
|
|
|
|
|
|
<view class="input-wrapper">
|
|
|
|
|
|
<input
|
|
|
|
|
|
class="input-box"
|
|
|
|
|
|
v-model="signatureName"
|
|
|
|
|
|
placeholder="请输入署名"
|
|
|
|
|
|
placeholder-style="color:#ccc"
|
2026-01-22 15:08:26 +08:00
|
|
|
|
maxlength="5"
|
2026-01-15 08:43:10 +08:00
|
|
|
|
/>
|
|
|
|
|
|
<text class="edit-icon">✎</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
2026-01-22 09:34:08 +08:00
|
|
|
|
<!-- 字体选择 -->
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-01-22 15:08:26 +08:00
|
|
|
|
<!-- 字体大小 -->
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-01-15 08:43:10 +08:00
|
|
|
|
<!-- 文字颜色 -->
|
|
|
|
|
|
<view class="form-item">
|
2026-01-22 15:08:26 +08:00
|
|
|
|
<text class="label">祝福语颜色</text>
|
2026-01-15 08:43:10 +08:00
|
|
|
|
<view class="color-list">
|
|
|
|
|
|
<view
|
|
|
|
|
|
v-for="(color, index) in textColors"
|
|
|
|
|
|
:key="index"
|
|
|
|
|
|
class="color-item"
|
|
|
|
|
|
:style="{ background: color }"
|
|
|
|
|
|
@tap="selectedColor = color"
|
|
|
|
|
|
>
|
|
|
|
|
|
<view v-if="selectedColor === color" class="color-check">✔</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2026-01-22 15:08:26 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 署名颜色 -->
|
|
|
|
|
|
<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>
|
2026-01-09 11:24:40 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
|
2026-01-22 08:55:00 +08:00
|
|
|
|
<!-- 位置调整 -->
|
2026-01-31 20:59:51 +08:00
|
|
|
|
<view v-if="activeTool === 'position'" class="section position-section">
|
2026-01-22 08:55:00 +08:00
|
|
|
|
<view class="form-item">
|
|
|
|
|
|
<text class="label">祝福语位置 (上下)</text>
|
|
|
|
|
|
<slider
|
|
|
|
|
|
:value="bubbleOffsetY"
|
|
|
|
|
|
min="-200"
|
|
|
|
|
|
max="200"
|
|
|
|
|
|
show-value
|
|
|
|
|
|
@change="(e) => (bubbleOffsetY = e.detail.value)"
|
|
|
|
|
|
activeColor="#ff3b30"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</view>
|
2026-01-31 11:56:39 +08:00
|
|
|
|
<view class="form-item">
|
|
|
|
|
|
<text class="label">祝福语宽度</text>
|
|
|
|
|
|
<slider
|
|
|
|
|
|
:value="bubbleMaxWidth"
|
|
|
|
|
|
min="200"
|
|
|
|
|
|
max="460"
|
|
|
|
|
|
show-value
|
|
|
|
|
|
@change="(e) => (bubbleMaxWidth = e.detail.value)"
|
|
|
|
|
|
activeColor="#ff3b30"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</view>
|
2026-01-22 08:55:00 +08:00
|
|
|
|
<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"
|
|
|
|
|
|
/>
|
2026-01-09 11:24:40 +08:00
|
|
|
|
</view>
|
2026-01-31 11:25:53 +08:00
|
|
|
|
<view class="form-item">
|
|
|
|
|
|
<text class="label">署名位置 (上下)</text>
|
|
|
|
|
|
<slider
|
|
|
|
|
|
:value="userOffsetY"
|
|
|
|
|
|
min="-200"
|
|
|
|
|
|
max="200"
|
|
|
|
|
|
show-value
|
|
|
|
|
|
@change="(e) => (userOffsetY = e.detail.value)"
|
|
|
|
|
|
activeColor="#ff3b30"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</view>
|
2026-01-09 11:24:40 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 头像挂饰 -->
|
|
|
|
|
|
<view v-if="activeTool === 'avatar'" class="section">
|
|
|
|
|
|
<view class="section-title"><text>头像挂饰</text></view>
|
|
|
|
|
|
<view class="row">
|
|
|
|
|
|
<button class="btn" @tap="toggleAvatarDecor">切换挂饰</button>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<canvas
|
2026-01-22 11:04:47 +08:00
|
|
|
|
type="2d"
|
|
|
|
|
|
id="cardCanvas"
|
2026-01-15 08:43:10 +08:00
|
|
|
|
class="hidden-canvas"
|
|
|
|
|
|
style="width: 540px; height: 960px"
|
|
|
|
|
|
/>
|
2026-01-22 22:11:02 +08:00
|
|
|
|
|
|
|
|
|
|
<LoginPopup ref="loginPopupRef" @logind="handleLogind" />
|
2026-01-09 11:24:40 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2026-01-22 22:11:02 +08:00
|
|
|
|
import { ref, computed } from "vue";
|
2026-01-15 08:43:10 +08:00
|
|
|
|
import { getBavBarHeight, getDeviceInfo } from "@/utils/system";
|
2026-01-22 22:11:02 +08:00
|
|
|
|
import { generateObjectId } from "@/utils/common";
|
|
|
|
|
|
|
2026-01-22 08:28:34 +08:00
|
|
|
|
import {
|
|
|
|
|
|
createCardTmp,
|
2026-01-22 22:11:02 +08:00
|
|
|
|
updateCard,
|
2026-01-22 08:28:34 +08:00
|
|
|
|
getCardTemplateList,
|
|
|
|
|
|
getCardTemplateContentList,
|
|
|
|
|
|
} from "@/api/make";
|
2026-01-27 18:18:04 +08:00
|
|
|
|
import { createShareToken, abilityCheck, getShareReward } from "@/api/system";
|
2026-01-22 23:54:56 +08:00
|
|
|
|
import {
|
|
|
|
|
|
onShareAppMessage,
|
|
|
|
|
|
onLoad,
|
|
|
|
|
|
onReachBottom,
|
|
|
|
|
|
onShow,
|
|
|
|
|
|
} from "@dcloudio/uni-app";
|
2026-01-15 08:43:10 +08:00
|
|
|
|
import { useUserStore } from "@/stores/user";
|
2026-01-22 22:11:02 +08:00
|
|
|
|
import LoginPopup from "@/components/LoginPopup/LoginPopup.vue";
|
2026-01-27 18:18:04 +08:00
|
|
|
|
import { saveRecordRequest, uploadImage } from "@/utils/common.js";
|
2026-01-22 22:11:02 +08:00
|
|
|
|
|
|
|
|
|
|
const userStore = useUserStore();
|
|
|
|
|
|
const loginPopupRef = ref(null);
|
|
|
|
|
|
const isLoggedIn = computed(() => !!userStore.userInfo.nickName);
|
2026-01-15 08:43:10 +08:00
|
|
|
|
|
2026-01-21 23:58:21 +08:00
|
|
|
|
const templatePage = ref(1);
|
|
|
|
|
|
const loadingTemplates = ref(false);
|
|
|
|
|
|
const hasMoreTemplates = ref(true);
|
2026-01-15 08:43:10 +08:00
|
|
|
|
const cardId = ref("");
|
|
|
|
|
|
|
|
|
|
|
|
const targetName = ref("祝您");
|
|
|
|
|
|
const signatureName = ref(userStore?.userInfo?.nickName || "xxx");
|
|
|
|
|
|
const userAvatar = ref(
|
|
|
|
|
|
userStore?.userInfo?.avatarUrl ||
|
2026-01-21 23:58:21 +08:00
|
|
|
|
"https://file.lihailezzc.com/resource/b48c41054c2633c478463ac1b1f1ca23.png",
|
2026-01-15 08:43:10 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
2026-01-22 22:11:02 +08:00
|
|
|
|
const blessingText = ref({});
|
2026-01-22 15:08:26 +08:00
|
|
|
|
const fontSize = ref(32);
|
|
|
|
|
|
|
|
|
|
|
|
const textColors = [
|
|
|
|
|
|
"#ffffff",
|
|
|
|
|
|
"#000000",
|
|
|
|
|
|
"#ff3b30",
|
|
|
|
|
|
"#F5A623",
|
|
|
|
|
|
"#8B572A",
|
|
|
|
|
|
"#D0021B",
|
|
|
|
|
|
"#F8E71C",
|
|
|
|
|
|
"#7ED321",
|
|
|
|
|
|
"#4A90E2",
|
|
|
|
|
|
"#9013FE",
|
|
|
|
|
|
"#FFC0CB",
|
|
|
|
|
|
];
|
2026-01-15 08:43:10 +08:00
|
|
|
|
const selectedColor = ref("#ffffff");
|
2026-01-22 15:08:26 +08:00
|
|
|
|
const signatureColor = ref("#ffffff");
|
2026-01-15 08:43:10 +08:00
|
|
|
|
|
2026-01-22 09:34:08 +08:00
|
|
|
|
const fontList = [
|
|
|
|
|
|
{ name: "默认", family: "PingFang SC", url: "" },
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "毛笔",
|
|
|
|
|
|
family: "MaoBi",
|
|
|
|
|
|
url: "https://file.lihailezzc.com/MaShanZheng-Regular.ttf", // 示例地址
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "手写",
|
|
|
|
|
|
family: "ShouXie",
|
2026-01-22 11:04:47 +08:00
|
|
|
|
url: "https://file.lihailezzc.com/ZhiMangXing-Regular.ttf", // 示例地址
|
2026-01-22 09:34:08 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "可爱",
|
|
|
|
|
|
family: "KeAi",
|
2026-01-22 11:04:47 +08:00
|
|
|
|
url: "https://file.lihailezzc.com/ZCOOLKuaiLe-Regular.ttf", // 示例地址
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: "草书",
|
|
|
|
|
|
family: "LiuJianMaoCao",
|
|
|
|
|
|
url: "https://file.lihailezzc.com/LiuJianMaoCao-Regular.ttf", // 示例地址
|
2026-01-22 09:34:08 +08:00
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
const selectedFont = ref(fontList[0]);
|
2026-01-22 14:56:32 +08:00
|
|
|
|
const loadedFonts = ref(new Set()); // 记录已加载的字体
|
2026-01-22 09:34:08 +08:00
|
|
|
|
|
|
|
|
|
|
const changeFont = (font) => {
|
2026-01-22 14:56:32 +08:00
|
|
|
|
// 1. 如果是默认字体或已加载过的字体,直接应用
|
|
|
|
|
|
if (!font.url || loadedFonts.value.has(font.family)) {
|
2026-01-22 09:34:08 +08:00
|
|
|
|
selectedFont.value = font;
|
2026-01-22 14:56:32 +08:00
|
|
|
|
return;
|
2026-01-22 09:34:08 +08:00
|
|
|
|
}
|
2026-01-22 14:56:32 +08:00
|
|
|
|
|
|
|
|
|
|
// 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" });
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2026-01-22 09:34:08 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-22 08:40:37 +08:00
|
|
|
|
const greetingLib = ref([]);
|
|
|
|
|
|
const greetingIndex = ref(0);
|
2026-01-15 08:43:10 +08:00
|
|
|
|
|
2026-01-22 08:55:00 +08:00
|
|
|
|
const bubbleOffsetY = ref(0);
|
2026-01-31 11:56:39 +08:00
|
|
|
|
const bubbleMaxWidth = ref(400); // 默认宽度
|
2026-01-22 08:55:00 +08:00
|
|
|
|
const userOffsetX = ref(0);
|
2026-01-31 11:25:53 +08:00
|
|
|
|
const userOffsetY = ref(0);
|
2026-01-22 08:55:00 +08:00
|
|
|
|
|
2026-01-15 08:43:10 +08:00
|
|
|
|
onLoad((options) => {
|
2026-01-21 23:58:21 +08:00
|
|
|
|
getTemplateList();
|
2026-01-22 08:28:34 +08:00
|
|
|
|
getTemplateContentList();
|
2026-01-15 08:43:10 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-22 23:54:56 +08:00
|
|
|
|
onShow(() => {
|
2026-01-28 15:55:26 +08:00
|
|
|
|
const recommendData = uni.getStorageSync("RECOMMEND_CARD_DATA");
|
|
|
|
|
|
if (recommendData) {
|
|
|
|
|
|
uni.removeStorageSync("RECOMMEND_CARD_DATA");
|
|
|
|
|
|
if (recommendData.imageUrl) {
|
|
|
|
|
|
const tpl = {
|
|
|
|
|
|
id: recommendData.recommendId,
|
|
|
|
|
|
imageUrl: recommendData.imageUrl,
|
|
|
|
|
|
name: "推荐模板", // 暂时使用通用名称,如果需要可以从接口获取更多信息
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 切换到模板 Tab
|
|
|
|
|
|
activeTool.value = "template";
|
|
|
|
|
|
|
|
|
|
|
|
// 应用模板
|
|
|
|
|
|
currentTemplate.value = tpl;
|
|
|
|
|
|
|
|
|
|
|
|
// 如果模板列表中存在,更新引用
|
|
|
|
|
|
const found = templates.value.find((t) => t.id === tpl.id);
|
|
|
|
|
|
if (found) currentTemplate.value = found;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 23:54:56 +08:00
|
|
|
|
const tempBlessing = uni.getStorageSync("TEMP_BLESSING_TEXT");
|
|
|
|
|
|
if (tempBlessing) {
|
|
|
|
|
|
blessingText.value = { content: tempBlessing, id: "" };
|
|
|
|
|
|
uni.removeStorageSync("TEMP_BLESSING_TEXT");
|
2026-01-28 15:55:26 +08:00
|
|
|
|
activeTool.value = "text";
|
2026-01-22 23:54:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-22 00:01:30 +08:00
|
|
|
|
onReachBottom(() => {
|
|
|
|
|
|
if (activeTool.value === "template") {
|
|
|
|
|
|
loadMoreTemplates();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-29 17:07:39 +08:00
|
|
|
|
const handleLogind = async () => {};
|
|
|
|
|
|
|
2026-01-22 22:11:02 +08:00
|
|
|
|
const createCard = () => {
|
|
|
|
|
|
const id = generateObjectId();
|
|
|
|
|
|
createCardTmp({ id });
|
|
|
|
|
|
cardId.value = id;
|
|
|
|
|
|
return id;
|
2026-01-15 08:43:10 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-21 23:58:21 +08:00
|
|
|
|
const getTemplateList = async (isLoadMore = false) => {
|
|
|
|
|
|
if (loadingTemplates.value || (!hasMoreTemplates.value && isLoadMore)) return;
|
|
|
|
|
|
|
|
|
|
|
|
loadingTemplates.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await getCardTemplateList(templatePage.value);
|
|
|
|
|
|
|
|
|
|
|
|
// 兼容数组或对象列表格式
|
|
|
|
|
|
const list = Array.isArray(res) ? res : res.list || [];
|
|
|
|
|
|
|
|
|
|
|
|
if (list.length > 0) {
|
|
|
|
|
|
if (isLoadMore) {
|
|
|
|
|
|
templates.value = [...templates.value, ...list];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
templates.value = list;
|
|
|
|
|
|
// 初始加载时设置第一个为当前选中
|
|
|
|
|
|
if (list.length > 0 && !currentTemplate.value) {
|
|
|
|
|
|
currentTemplate.value = list[0];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 判断是否还有更多
|
|
|
|
|
|
if (typeof res.hasNext !== "undefined") {
|
|
|
|
|
|
hasMoreTemplates.value = res.hasNext;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 如果没有 hasNext 字段,根据返回数量简单判断
|
|
|
|
|
|
hasMoreTemplates.value = list.length >= 8; // 假设每页 10 条
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (hasMoreTemplates.value) {
|
|
|
|
|
|
templatePage.value++;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (!isLoadMore) templates.value = [];
|
|
|
|
|
|
hasMoreTemplates.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("加载模板失败:", error);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loadingTemplates.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-22 08:28:34 +08:00
|
|
|
|
const getTemplateContentList = async () => {
|
|
|
|
|
|
const res = await getCardTemplateContentList();
|
|
|
|
|
|
if (res.length) {
|
2026-01-22 22:11:02 +08:00
|
|
|
|
greetingLib.value = res;
|
2026-01-22 08:28:34 +08:00
|
|
|
|
displayedGreetings.value = greetingLib.value.slice(0, 2);
|
2026-01-22 23:54:56 +08:00
|
|
|
|
if (!blessingText.value.content) {
|
|
|
|
|
|
blessingText.value = greetingLib.value[0] || {};
|
|
|
|
|
|
}
|
2026-01-22 08:28:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-21 23:58:21 +08:00
|
|
|
|
const loadMoreTemplates = () => {
|
|
|
|
|
|
getTemplateList(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-15 08:43:10 +08:00
|
|
|
|
onShareAppMessage(async () => {
|
2026-01-27 18:18:04 +08:00
|
|
|
|
getShareReward({ scene: "card_generate" });
|
2026-01-22 22:11:02 +08:00
|
|
|
|
if (!isLoggedIn.value) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
title: "新春祝福",
|
|
|
|
|
|
path: "/pages/index/index",
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
// 1. 确保有 cardId (如果内容有变动,最好是新建)
|
|
|
|
|
|
const id = createCard();
|
|
|
|
|
|
if (!id) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
title: "新春祝福",
|
|
|
|
|
|
path: "/pages/index/index",
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 08:43:10 +08:00
|
|
|
|
const deviceInfo = getDeviceInfo();
|
2026-01-22 22:11:02 +08:00
|
|
|
|
const shareTokenRes = await createShareToken({
|
|
|
|
|
|
scene: "card_generate",
|
|
|
|
|
|
targetId: id,
|
2026-01-15 08:43:10 +08:00
|
|
|
|
...deviceInfo,
|
|
|
|
|
|
});
|
2026-01-27 18:18:04 +08:00
|
|
|
|
shareOrSave(id);
|
2026-01-15 08:43:10 +08:00
|
|
|
|
return {
|
|
|
|
|
|
title: "新春祝福",
|
|
|
|
|
|
path: "/pages/detail/index?shareToken=" + shareTokenRes.shareToken,
|
2026-01-22 22:11:02 +08:00
|
|
|
|
imageUrl: currentTemplate.value?.imageUrl || "/static/images/bg.jpg",
|
2026-01-15 08:43:10 +08:00
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-22 08:28:34 +08:00
|
|
|
|
const displayedGreetings = ref([]);
|
2026-01-15 08:43:10 +08:00
|
|
|
|
|
|
|
|
|
|
const refreshGreetings = () => {
|
2026-01-22 08:40:37 +08:00
|
|
|
|
if (!greetingLib.value.length) return;
|
|
|
|
|
|
|
|
|
|
|
|
const nextIndex = (greetingIndex.value + 2) % greetingLib.value.length;
|
|
|
|
|
|
greetingIndex.value = nextIndex;
|
|
|
|
|
|
|
|
|
|
|
|
let next = greetingLib.value.slice(nextIndex, nextIndex + 2);
|
2026-01-15 08:43:10 +08:00
|
|
|
|
if (next.length < 2) {
|
2026-01-22 08:40:37 +08:00
|
|
|
|
next = [...next, ...greetingLib.value.slice(0, 2 - next.length)];
|
2026-01-15 08:43:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
displayedGreetings.value = next;
|
|
|
|
|
|
};
|
2026-01-09 11:24:40 +08:00
|
|
|
|
|
2026-01-15 08:43:10 +08:00
|
|
|
|
const selectGreeting = (text) => {
|
|
|
|
|
|
blessingText.value = text;
|
|
|
|
|
|
};
|
2026-01-09 11:24:40 +08:00
|
|
|
|
|
|
|
|
|
|
const tools = [
|
2026-01-15 08:43:10 +08:00
|
|
|
|
{ type: "template", text: "模板", icon: "▦" },
|
|
|
|
|
|
{ type: "text", text: "文字", icon: "文" },
|
2026-01-22 08:55:00 +08:00
|
|
|
|
{ type: "position", text: "位置", icon: "图" },
|
2026-01-21 23:58:21 +08:00
|
|
|
|
// { type: "avatar", text: "头像挂饰", icon: "饰" },
|
2026-01-15 08:43:10 +08:00
|
|
|
|
];
|
|
|
|
|
|
const activeTool = ref("template");
|
2026-01-09 11:24:40 +08:00
|
|
|
|
|
2026-01-21 23:58:21 +08:00
|
|
|
|
const templates = ref([]);
|
2026-01-15 08:43:10 +08:00
|
|
|
|
|
|
|
|
|
|
const currentTemplate = ref(templates.value[0]);
|
2026-01-09 11:24:40 +08:00
|
|
|
|
|
|
|
|
|
|
const applyTemplate = (tpl) => {
|
2026-01-15 08:43:10 +08:00
|
|
|
|
currentTemplate.value = tpl;
|
|
|
|
|
|
};
|
2026-01-09 11:24:40 +08:00
|
|
|
|
|
|
|
|
|
|
const pickImage = () => {
|
|
|
|
|
|
uni.chooseImage({
|
|
|
|
|
|
count: 1,
|
|
|
|
|
|
success: (res) => {
|
2026-01-15 08:43:10 +08:00
|
|
|
|
const path = res.tempFilePaths?.[0];
|
|
|
|
|
|
if (path)
|
|
|
|
|
|
currentTemplate.value = { ...currentTemplate.value, cover: path };
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
2026-01-09 11:24:40 +08:00
|
|
|
|
|
|
|
|
|
|
const resetBackground = () => {
|
2026-01-15 08:43:10 +08:00
|
|
|
|
currentTemplate.value = templates.value[0];
|
|
|
|
|
|
};
|
2026-01-09 11:24:40 +08:00
|
|
|
|
|
|
|
|
|
|
const toggleAvatarDecor = () => {
|
2026-01-15 08:43:10 +08:00
|
|
|
|
uni.showToast({ title: "挂饰功能即将上线~", icon: "none" });
|
|
|
|
|
|
};
|
2026-01-09 11:24:40 +08:00
|
|
|
|
|
2026-01-27 16:46:39 +08:00
|
|
|
|
const preview = async () => {
|
|
|
|
|
|
if (!isLoggedIn.value) {
|
|
|
|
|
|
loginPopupRef.value.open();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-01-27 18:18:04 +08:00
|
|
|
|
const abilityRes = await abilityCheck("card_generate");
|
|
|
|
|
|
if (!abilityRes.canUse) {
|
|
|
|
|
|
if (
|
|
|
|
|
|
abilityRes?.blockType === "need_share" &&
|
|
|
|
|
|
abilityRes?.message === "分享可继续"
|
|
|
|
|
|
) {
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: "分享给好友可继续使用",
|
|
|
|
|
|
icon: "none",
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: "您今日祝福卡下载次数已用完,直接分享给好友或者明日再试",
|
|
|
|
|
|
icon: "none",
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-01-27 16:46:39 +08:00
|
|
|
|
const tempPath = await saveByCanvas(true);
|
2026-01-27 19:35:47 +08:00
|
|
|
|
id = createCard();
|
|
|
|
|
|
shareOrSave(id);
|
|
|
|
|
|
saveRecordRequest(tempPath, id, "card_generate");
|
2026-01-27 16:46:39 +08:00
|
|
|
|
|
2026-01-15 08:43:10 +08:00
|
|
|
|
// uni.showToast({ title: '已保存到相册', icon: 'checkmarkempty' })
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-27 18:18:04 +08:00
|
|
|
|
const shareOrSave = async (id) => {
|
2026-01-27 19:35:47 +08:00
|
|
|
|
if (!id) id = createCard();
|
|
|
|
|
|
|
2026-01-22 22:11:02 +08:00
|
|
|
|
const tempPath = await saveByCanvas(false);
|
2026-01-27 18:18:04 +08:00
|
|
|
|
const imageUrl = await uploadImage(tempPath);
|
|
|
|
|
|
|
|
|
|
|
|
updateCard({
|
|
|
|
|
|
id,
|
|
|
|
|
|
imageUrl,
|
|
|
|
|
|
status: 1,
|
|
|
|
|
|
blessingId: blessingText.value?.id || "",
|
|
|
|
|
|
blessingTo: targetName.value,
|
|
|
|
|
|
blessingFrom: signatureName.value,
|
|
|
|
|
|
templateId: currentTemplate.value?.id || "",
|
2026-01-22 22:11:02 +08:00
|
|
|
|
});
|
2026-01-15 08:43:10 +08:00
|
|
|
|
};
|
2026-01-09 11:24:40 +08:00
|
|
|
|
|
|
|
|
|
|
const showMore = () => {
|
2026-01-15 08:43:10 +08:00
|
|
|
|
uni.showToast({ title: "更多模板即将上线~", icon: "none" });
|
|
|
|
|
|
};
|
2026-01-09 11:24:40 +08:00
|
|
|
|
|
2026-01-15 08:43:10 +08:00
|
|
|
|
const saveByCanvas = async (save = true) => {
|
2026-01-22 11:04:47 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-01-09 11:24:40 +08:00
|
|
|
|
|
2026-01-22 11:04:47 +08:00
|
|
|
|
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)
|
|
|
|
|
|
const W = 540;
|
|
|
|
|
|
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️⃣ 画背景
|
|
|
|
|
|
// ⭐ 先加载背景图
|
|
|
|
|
|
const [bgImg, avatarImg] = await Promise.all([
|
|
|
|
|
|
loadCanvasImage(currentTemplate?.value?.imageUrl),
|
|
|
|
|
|
loadCanvasImage(userAvatar.value),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
ctx.drawImage(bgImg, 0, 0, W, H);
|
|
|
|
|
|
|
|
|
|
|
|
// 2️⃣ 半透明遮罩(和你 UI 一致)
|
|
|
|
|
|
ctx.fillStyle = "rgba(0,0,0,0.08)";
|
|
|
|
|
|
ctx.fillRect(0, 0, W, H);
|
|
|
|
|
|
|
|
|
|
|
|
// 3️⃣ 标题
|
2026-01-31 11:56:39 +08:00
|
|
|
|
// ctx.fillStyle = "#ffffff";
|
|
|
|
|
|
// ctx.font = "42px sans-serif"; // 默认字体
|
|
|
|
|
|
// ctx.textAlign = "center";
|
|
|
|
|
|
// ctx.textBaseline = "alphabetic"; // Canvas 2D 默认是 alphabetic
|
|
|
|
|
|
// ctx.fillText("新春快乐", W / 2, 120);
|
2026-01-22 11:04:47 +08:00
|
|
|
|
|
2026-01-31 11:56:39 +08:00
|
|
|
|
// ctx.font = "22px sans-serif";
|
|
|
|
|
|
// ctx.globalAlpha = 0.9;
|
|
|
|
|
|
// ctx.fillText("2026 YEAR OF THE HORSE", W / 2, 165);
|
|
|
|
|
|
// ctx.globalAlpha = 1;
|
2026-01-22 11:04:47 +08:00
|
|
|
|
|
|
|
|
|
|
// 4️⃣ 祝福语气泡
|
|
|
|
|
|
drawBubbleText(ctx, {
|
2026-01-22 22:11:02 +08:00
|
|
|
|
text: targetName.value + "\n " + blessingText.value.content,
|
2026-01-31 11:56:39 +08:00
|
|
|
|
x: 70,
|
2026-01-22 11:04:47 +08:00
|
|
|
|
y: 260 + bubbleOffsetY.value,
|
2026-01-31 11:56:39 +08:00
|
|
|
|
maxWidth: bubbleMaxWidth.value, // 使用动态宽度
|
|
|
|
|
|
canvasWidth: W, // 传入画布宽度以实现自动居中
|
2026-01-22 15:08:26 +08:00
|
|
|
|
fontSize: fontSize.value,
|
|
|
|
|
|
lineHeight: fontSize.value * 1.5,
|
2026-01-22 11:04:47 +08:00
|
|
|
|
backgroundColor: "rgba(255,255,255,0.85)",
|
|
|
|
|
|
textColor: selectedColor.value,
|
|
|
|
|
|
fontFamily: selectedFont.value.family,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
drawUserBubble(ctx, {
|
|
|
|
|
|
x: 160 + userOffsetX.value,
|
2026-01-31 11:25:53 +08:00
|
|
|
|
y: H - 136 + userOffsetY.value,
|
2026-01-22 11:04:47 +08:00
|
|
|
|
avatarImg: avatarImg, // 传入 Image 对象
|
|
|
|
|
|
username: signatureName.value,
|
|
|
|
|
|
desc: "送上祝福",
|
2026-01-22 15:08:26 +08:00
|
|
|
|
textColor: signatureColor.value,
|
2026-01-22 11:04:47 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
2026-01-15 08:43:10 +08:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
2026-01-09 11:24:40 +08:00
|
|
|
|
|
|
|
|
|
|
const loadImage = (url) => {
|
2026-01-22 11:04:47 +08:00
|
|
|
|
// 此函数保留给其他可能用到的地方,但 saveByCanvas 内部使用 loadCanvasImage
|
2026-01-09 11:24:40 +08:00
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
|
uni.getImageInfo({
|
|
|
|
|
|
src: url,
|
2026-01-15 08:43:10 +08:00
|
|
|
|
success: (res) => {
|
|
|
|
|
|
resolve(res.path); // 本地路径
|
2026-01-09 11:24:40 +08:00
|
|
|
|
},
|
2026-01-15 08:43:10 +08:00
|
|
|
|
fail: (err) => {
|
|
|
|
|
|
reject(err);
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
2026-01-09 11:24:40 +08:00
|
|
|
|
|
|
|
|
|
|
const saveImage = (path) => {
|
|
|
|
|
|
uni.saveImageToPhotosAlbum({
|
|
|
|
|
|
filePath: path,
|
|
|
|
|
|
success() {
|
2026-01-15 08:43:10 +08:00
|
|
|
|
uni.showToast({ title: "已保存到相册" });
|
2026-01-09 11:24:40 +08:00
|
|
|
|
},
|
|
|
|
|
|
fail() {
|
|
|
|
|
|
uni.showModal({
|
2026-01-15 08:43:10 +08:00
|
|
|
|
title: "提示",
|
|
|
|
|
|
content: "请授权保存到相册",
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
2026-01-09 11:24:40 +08:00
|
|
|
|
|
|
|
|
|
|
function drawBubbleText(ctx, options) {
|
|
|
|
|
|
const {
|
|
|
|
|
|
text,
|
|
|
|
|
|
x,
|
|
|
|
|
|
y,
|
|
|
|
|
|
maxWidth = 400,
|
2026-01-15 08:43:10 +08:00
|
|
|
|
padding = 24,
|
2026-01-09 11:24:40 +08:00
|
|
|
|
lineHeight = 42,
|
|
|
|
|
|
radius = 24,
|
2026-01-15 08:43:10 +08:00
|
|
|
|
backgroundColor = "rgba(255,255,255,0.9)",
|
|
|
|
|
|
textColor = "#fff",
|
2026-01-09 11:24:40 +08:00
|
|
|
|
fontSize = 32,
|
2026-01-15 08:43:10 +08:00
|
|
|
|
fontFamily = "PingFang SC",
|
2026-01-31 11:56:39 +08:00
|
|
|
|
canvasWidth, // 新增:画布宽度,用于居中计算
|
2026-01-15 08:43:10 +08:00
|
|
|
|
} = options;
|
|
|
|
|
|
if (!text) return;
|
2026-01-09 11:24:40 +08:00
|
|
|
|
|
2026-01-22 11:04:47 +08:00
|
|
|
|
ctx.fillStyle = textColor;
|
|
|
|
|
|
ctx.font = `${fontSize}px '${fontFamily}'`;
|
2026-01-15 08:43:10 +08:00
|
|
|
|
ctx.textAlign = "left";
|
|
|
|
|
|
ctx.textBaseline = "top";
|
2026-01-09 11:24:40 +08:00
|
|
|
|
|
|
|
|
|
|
// 1️⃣ 文本自动换行
|
2026-01-15 08:43:10 +08:00
|
|
|
|
const paragraphs = text.split("\n");
|
|
|
|
|
|
|
|
|
|
|
|
const lines = [];
|
|
|
|
|
|
|
|
|
|
|
|
/** ② 每一段再自动换行 */
|
|
|
|
|
|
paragraphs.forEach((p) => {
|
|
|
|
|
|
let line = "";
|
|
|
|
|
|
for (let char of p) {
|
|
|
|
|
|
const testLine = line + char;
|
|
|
|
|
|
const { width } = ctx.measureText(testLine);
|
|
|
|
|
|
if (width > maxWidth) {
|
|
|
|
|
|
lines.push(line);
|
|
|
|
|
|
line = char;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
line = testLine;
|
|
|
|
|
|
}
|
2026-01-09 11:24:40 +08:00
|
|
|
|
}
|
2026-01-15 08:43:10 +08:00
|
|
|
|
if (line) lines.push(line);
|
|
|
|
|
|
});
|
2026-01-09 11:24:40 +08:00
|
|
|
|
|
2026-01-31 11:56:39 +08:00
|
|
|
|
// 计算实际最大宽度以支持居中
|
|
|
|
|
|
let maxLineWidth = 0;
|
|
|
|
|
|
lines.forEach((line) => {
|
|
|
|
|
|
const { width } = ctx.measureText(line);
|
|
|
|
|
|
if (width > maxLineWidth) maxLineWidth = width;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 如果提供了 canvasWidth,则自动计算居中的 x 坐标
|
|
|
|
|
|
let drawX = x;
|
|
|
|
|
|
if (canvasWidth) {
|
|
|
|
|
|
const totalWidth = maxLineWidth + padding * 2;
|
|
|
|
|
|
drawX = (canvasWidth - totalWidth) / 2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 11:24:40 +08:00
|
|
|
|
// 2️⃣ 计算气泡尺寸
|
2026-01-15 08:43:10 +08:00
|
|
|
|
// const bubbleWidth = maxWidth
|
2026-01-09 11:24:40 +08:00
|
|
|
|
|
2026-01-15 08:43:10 +08:00
|
|
|
|
// const bubbleHeight = lines.length * lineHeight + padding * 2
|
2026-01-09 11:24:40 +08:00
|
|
|
|
|
|
|
|
|
|
// 3️⃣ 绘制气泡(圆角矩形)
|
2026-01-15 08:43:10 +08:00
|
|
|
|
// drawRoundRect(
|
|
|
|
|
|
// ctx,
|
2026-01-31 11:56:39 +08:00
|
|
|
|
// drawX,
|
2026-01-15 08:43:10 +08:00
|
|
|
|
// y,
|
|
|
|
|
|
// bubbleWidth,
|
|
|
|
|
|
// bubbleHeight,
|
|
|
|
|
|
// radius,
|
|
|
|
|
|
// backgroundColor
|
|
|
|
|
|
// )
|
|
|
|
|
|
|
|
|
|
|
|
// 4️⃣ 绘制文字
|
2026-01-22 11:04:47 +08:00
|
|
|
|
ctx.fillStyle = textColor;
|
2026-01-15 08:43:10 +08:00
|
|
|
|
|
|
|
|
|
|
lines.forEach((line, index) => {
|
2026-01-31 11:56:39 +08:00
|
|
|
|
ctx.fillText(line, drawX + padding, y + padding + index * lineHeight);
|
2026-01-15 08:43:10 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function drawUserBubble(ctx, options) {
|
|
|
|
|
|
const {
|
|
|
|
|
|
x = 40, // 气泡起点 x
|
|
|
|
|
|
y = 860, // 气泡起点 y
|
2026-01-22 11:04:47 +08:00
|
|
|
|
avatarImg, // CanvasImage 对象
|
2026-01-15 08:43:10 +08:00
|
|
|
|
username = "zzc",
|
|
|
|
|
|
desc = "送上祝福",
|
|
|
|
|
|
avatarSize = 64, // 头像直径
|
|
|
|
|
|
padding = 16, // 气泡内边距
|
|
|
|
|
|
fontSizeName = 24,
|
|
|
|
|
|
fontSizeDesc = 20,
|
|
|
|
|
|
bubbleColor = "rgba(255,255,255,0.18)",
|
|
|
|
|
|
textColor = "#ffffff",
|
|
|
|
|
|
} = options;
|
|
|
|
|
|
|
|
|
|
|
|
// 设置字体
|
|
|
|
|
|
ctx.textBaseline = "top";
|
2026-01-22 11:04:47 +08:00
|
|
|
|
ctx.font = `${fontSizeName}px 'PingFang SC'`;
|
2026-01-15 08:43:10 +08:00
|
|
|
|
|
|
|
|
|
|
// 测量文字宽度
|
|
|
|
|
|
const nameWidth = ctx.measureText(username).width;
|
2026-01-22 11:04:47 +08:00
|
|
|
|
ctx.font = `${fontSizeDesc}px 'PingFang SC'`;
|
2026-01-15 08:43:10 +08:00
|
|
|
|
const descWidth = ctx.measureText(desc).width;
|
|
|
|
|
|
|
|
|
|
|
|
// 计算气泡宽度和高度
|
|
|
|
|
|
const textWidth = Math.max(nameWidth, descWidth);
|
|
|
|
|
|
const bubbleHeight =
|
|
|
|
|
|
Math.max(avatarSize, fontSizeName + fontSizeDesc + 4) + padding * 2;
|
|
|
|
|
|
const bubbleWidth = avatarSize + padding + textWidth + padding * 2;
|
|
|
|
|
|
|
|
|
|
|
|
// 1️⃣ 绘制气泡(左右半圆)
|
2026-01-09 11:24:40 +08:00
|
|
|
|
drawRoundRect(
|
|
|
|
|
|
ctx,
|
|
|
|
|
|
x,
|
|
|
|
|
|
y,
|
|
|
|
|
|
bubbleWidth,
|
|
|
|
|
|
bubbleHeight,
|
2026-01-15 08:43:10 +08:00
|
|
|
|
bubbleHeight / 2,
|
2026-01-21 23:58:21 +08:00
|
|
|
|
bubbleColor,
|
2026-01-15 08:43:10 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 2️⃣ 绘制头像
|
|
|
|
|
|
const avatarX = x + padding;
|
|
|
|
|
|
const avatarY = y + (bubbleHeight - avatarSize) / 2;
|
|
|
|
|
|
|
|
|
|
|
|
ctx.save();
|
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
|
ctx.arc(
|
|
|
|
|
|
avatarX + avatarSize / 2,
|
|
|
|
|
|
avatarY + avatarSize / 2,
|
|
|
|
|
|
avatarSize / 2,
|
|
|
|
|
|
0,
|
2026-01-21 23:58:21 +08:00
|
|
|
|
Math.PI * 2,
|
2026-01-15 08:43:10 +08:00
|
|
|
|
);
|
|
|
|
|
|
ctx.clip();
|
2026-01-22 11:04:47 +08:00
|
|
|
|
if (avatarImg) {
|
|
|
|
|
|
ctx.drawImage(avatarImg, avatarX, avatarY, avatarSize, avatarSize);
|
|
|
|
|
|
}
|
2026-01-15 08:43:10 +08:00
|
|
|
|
ctx.restore();
|
|
|
|
|
|
|
|
|
|
|
|
// 3️⃣ 绘制文字
|
|
|
|
|
|
const textX = avatarX + avatarSize + padding;
|
|
|
|
|
|
const textY = y + padding;
|
2026-01-22 11:04:47 +08:00
|
|
|
|
ctx.fillStyle = textColor;
|
|
|
|
|
|
ctx.font = `${fontSizeName}px 'PingFang SC'`;
|
2026-01-15 08:43:10 +08:00
|
|
|
|
ctx.fillText(username, textX, textY);
|
2026-01-22 11:04:47 +08:00
|
|
|
|
ctx.font = `${fontSizeDesc}px 'PingFang SC'`;
|
|
|
|
|
|
ctx.globalAlpha = 0.6;
|
2026-01-15 08:43:10 +08:00
|
|
|
|
ctx.fillText(desc, textX, textY + fontSizeName + 4);
|
2026-01-22 11:04:47 +08:00
|
|
|
|
ctx.globalAlpha = 1;
|
2026-01-09 11:24:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function drawRoundRect(ctx, x, y, w, h, r, color) {
|
2026-01-15 08:43:10 +08:00
|
|
|
|
ctx.beginPath();
|
|
|
|
|
|
// ctx.setFillStyle(color)
|
|
|
|
|
|
ctx.fillStyle = "rgba(255,255,255,0.18)";
|
|
|
|
|
|
ctx.fill();
|
|
|
|
|
|
|
|
|
|
|
|
// 描边(非常关键)
|
|
|
|
|
|
ctx.strokeStyle = "rgba(255,255,255,0.35)";
|
|
|
|
|
|
ctx.lineWidth = 1;
|
|
|
|
|
|
ctx.stroke();
|
|
|
|
|
|
|
|
|
|
|
|
ctx.moveTo(x + r, y);
|
|
|
|
|
|
ctx.lineTo(x + w - r, y);
|
|
|
|
|
|
ctx.arcTo(x + w, y, x + w, y + r, r);
|
|
|
|
|
|
ctx.lineTo(x + w, y + h - r);
|
|
|
|
|
|
ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
|
|
|
|
|
|
ctx.lineTo(x + r, y + h);
|
|
|
|
|
|
ctx.arcTo(x, y + h, x, y + h - r, r);
|
|
|
|
|
|
ctx.lineTo(x, y + r);
|
|
|
|
|
|
ctx.arcTo(x, y, x + r, y, r);
|
|
|
|
|
|
ctx.closePath();
|
|
|
|
|
|
ctx.fill();
|
2026-01-09 11:24:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
|
.make-page {
|
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 卡片预览 */
|
|
|
|
|
|
.card-preview {
|
|
|
|
|
|
margin: 24rpx auto;
|
|
|
|
|
|
height: 960rpx;
|
|
|
|
|
|
width: 540rpx;
|
|
|
|
|
|
border-radius: 30rpx;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
position: relative;
|
2026-01-15 08:43:10 +08:00
|
|
|
|
box-shadow: 0 16rpx 40rpx rgba(0, 0, 0, 0.12);
|
2026-01-09 11:24:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
.card-bg {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
.card-overlay {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
padding: 30rpx;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
2026-01-31 11:33:22 +08:00
|
|
|
|
.watermark {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translate(-50%, -50%) rotate(-45deg);
|
|
|
|
|
|
font-size: 36rpx;
|
|
|
|
|
|
color: rgba(255, 255, 255, 0.2);
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
z-index: 1;
|
|
|
|
|
|
}
|
2026-01-09 11:24:40 +08:00
|
|
|
|
.card-overlay .title {
|
2026-01-15 08:43:10 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-top: 60rpx;
|
2026-01-09 11:24:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
.title .main {
|
|
|
|
|
|
font-size: 42rpx;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
}
|
|
|
|
|
|
.title .sub {
|
|
|
|
|
|
margin-top: 8rpx;
|
|
|
|
|
|
font-size: 22rpx;
|
|
|
|
|
|
opacity: 0.9;
|
|
|
|
|
|
}
|
|
|
|
|
|
.bubble {
|
|
|
|
|
|
margin-top: 80rpx;
|
|
|
|
|
|
padding: 40rpx;
|
2026-01-15 08:43:10 +08:00
|
|
|
|
max-width: 500rpx;
|
2026-01-09 11:24:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
.bubble-text {
|
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
|
line-height: 1.6;
|
2026-01-15 08:43:10 +08:00
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
text-align: left;
|
2026-01-09 11:24:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
.user {
|
2026-01-15 08:43:10 +08:00
|
|
|
|
position: absolute;
|
|
|
|
|
|
bottom: 40rpx;
|
2026-01-09 11:24:40 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.18);
|
|
|
|
|
|
border: 1rpx solid rgba(255, 255, 255, 0.35);
|
2026-01-15 08:43:10 +08:00
|
|
|
|
border-radius: 9999rpx;
|
2026-01-09 11:24:40 +08:00
|
|
|
|
padding: 15rpx;
|
|
|
|
|
|
padding-left: 20rpx;
|
|
|
|
|
|
padding-right: 20rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.avatar {
|
|
|
|
|
|
width: 64rpx;
|
|
|
|
|
|
height: 64rpx;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
margin-right: 14rpx;
|
2026-01-15 08:43:10 +08:00
|
|
|
|
border: 2rpx solid rgba(255, 255, 255, 0.6);
|
|
|
|
|
|
}
|
|
|
|
|
|
.user .user-info {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2026-01-09 11:24:40 +08:00
|
|
|
|
}
|
2026-01-15 08:43:10 +08:00
|
|
|
|
.user-name {
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
.user-desc {
|
|
|
|
|
|
font-size: 20rpx;
|
|
|
|
|
|
opacity: 0.6;
|
2026-01-09 11:24:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 顶部提示 */
|
|
|
|
|
|
.tip-line {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
font-size: 22rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 编辑工具区 */
|
|
|
|
|
|
.editor-panel {
|
|
|
|
|
|
margin: 20rpx 24rpx 40rpx;
|
|
|
|
|
|
border-radius: 30rpx 30rpx 0 0;
|
|
|
|
|
|
background: #fff;
|
2026-01-15 08:43:10 +08:00
|
|
|
|
box-shadow: 0 -10rpx 30rpx rgba(0, 0, 0, 0.06);
|
2026-01-09 11:24:40 +08:00
|
|
|
|
padding-bottom: env(safe-area-inset-bottom);
|
|
|
|
|
|
}
|
|
|
|
|
|
.drag-handle {
|
2026-01-15 08:43:10 +08:00
|
|
|
|
width: 120rpx;
|
|
|
|
|
|
height: 8rpx;
|
|
|
|
|
|
border-radius: 999rpx;
|
|
|
|
|
|
background: #eee;
|
|
|
|
|
|
margin: 12rpx auto;
|
2026-01-09 11:24:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 工具入口 */
|
|
|
|
|
|
.tools {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(4, 1fr);
|
|
|
|
|
|
padding: 16rpx 24rpx 8rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.tool-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 14rpx 0;
|
|
|
|
|
|
border-radius: 18rpx;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
}
|
|
|
|
|
|
.tool-item.active {
|
|
|
|
|
|
background: #fff6f5;
|
|
|
|
|
|
color: #ff3b30;
|
|
|
|
|
|
}
|
|
|
|
|
|
.tool-icon {
|
2026-01-15 08:43:10 +08:00
|
|
|
|
width: 64rpx;
|
|
|
|
|
|
height: 64rpx;
|
2026-01-09 11:24:40 +08:00
|
|
|
|
border-radius: 16rpx;
|
|
|
|
|
|
background: #fafafa;
|
2026-01-15 08:43:10 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2026-01-09 11:24:40 +08:00
|
|
|
|
margin-bottom: 8rpx;
|
|
|
|
|
|
}
|
2026-01-15 08:43:10 +08:00
|
|
|
|
.tool-text {
|
|
|
|
|
|
font-size: 22rpx;
|
|
|
|
|
|
}
|
2026-01-09 11:24:40 +08:00
|
|
|
|
|
|
|
|
|
|
/* 模板区 */
|
2026-01-15 08:43:10 +08:00
|
|
|
|
.section {
|
|
|
|
|
|
padding: 12rpx 24rpx 0;
|
|
|
|
|
|
}
|
2026-01-09 11:24:40 +08:00
|
|
|
|
.section-title {
|
2026-01-15 08:43:10 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-01-09 11:24:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
.section-title .more {
|
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
|
color: #ff3b30;
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
}
|
2026-01-15 08:43:10 +08:00
|
|
|
|
.tpl-scroll {
|
|
|
|
|
|
margin-top: 12rpx;
|
|
|
|
|
|
}
|
2026-01-21 23:58:21 +08:00
|
|
|
|
.tpl-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(3, 1fr);
|
|
|
|
|
|
gap: 16rpx;
|
|
|
|
|
|
padding-bottom: 20rpx;
|
2026-01-15 08:43:10 +08:00
|
|
|
|
}
|
2026-01-09 11:24:40 +08:00
|
|
|
|
.tpl-card {
|
2026-01-21 23:58:21 +08:00
|
|
|
|
width: 100%; /* 自适应 grid 宽度 */
|
|
|
|
|
|
border-radius: 12rpx;
|
2026-01-15 08:43:10 +08:00
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
background: #fff;
|
2026-01-09 11:24:40 +08:00
|
|
|
|
position: relative;
|
2026-01-21 23:58:21 +08:00
|
|
|
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
|
2026-01-15 08:43:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
.tpl-card.selected {
|
|
|
|
|
|
outline: 4rpx solid #ff3b30;
|
|
|
|
|
|
}
|
|
|
|
|
|
.tpl-cover {
|
|
|
|
|
|
width: 100%;
|
2026-01-21 23:58:21 +08:00
|
|
|
|
height: 368rpx;
|
2026-01-09 11:24:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
.tpl-name {
|
2026-01-21 23:58:21 +08:00
|
|
|
|
font-size: 20rpx;
|
2026-01-15 08:43:10 +08:00
|
|
|
|
color: #333;
|
2026-01-21 23:58:21 +08:00
|
|
|
|
padding: 6rpx 8rpx;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
2026-01-09 11:24:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
.tpl-check {
|
2026-01-15 08:43:10 +08:00
|
|
|
|
position: absolute;
|
2026-01-21 23:58:21 +08:00
|
|
|
|
right: 6rpx;
|
|
|
|
|
|
top: 6rpx;
|
|
|
|
|
|
width: 32rpx;
|
|
|
|
|
|
height: 32rpx;
|
2026-01-15 08:43:10 +08:00
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background: #ff3b30;
|
|
|
|
|
|
color: #fff;
|
2026-01-21 23:58:21 +08:00
|
|
|
|
font-size: 20rpx;
|
2026-01-15 08:43:10 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
2026-01-21 23:58:21 +08:00
|
|
|
|
.loading-more,
|
|
|
|
|
|
.no-more {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
font-size: 22rpx;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
padding: 10rpx 0;
|
|
|
|
|
|
}
|
2026-01-15 08:43:10 +08:00
|
|
|
|
|
|
|
|
|
|
/* 文字编辑区 */
|
|
|
|
|
|
.text-edit-section {
|
|
|
|
|
|
padding: 10rpx 24rpx 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.form-item {
|
|
|
|
|
|
margin-bottom: 32rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.label {
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
margin-bottom: 16rpx;
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
|
|
|
|
|
.input-box {
|
|
|
|
|
|
background: #f9f9f9;
|
|
|
|
|
|
border-radius: 12rpx;
|
|
|
|
|
|
padding: 20rpx 24rpx;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
.label-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 16rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.refresh-btn {
|
|
|
|
|
|
font-size: 22rpx;
|
|
|
|
|
|
color: #ff3b30;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.refresh-icon {
|
|
|
|
|
|
margin-right: 6rpx;
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.greeting-scroll {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
.greeting-list {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
padding-bottom: 10rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.greeting-card {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
width: 320rpx;
|
|
|
|
|
|
height: 160rpx;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 2rpx solid #eee;
|
|
|
|
|
|
border-radius: 16rpx;
|
|
|
|
|
|
padding: 20rpx;
|
|
|
|
|
|
margin-right: 20rpx;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
}
|
|
|
|
|
|
.greeting-card.active {
|
2026-01-22 09:34:08 +08:00
|
|
|
|
background: #fff5f5;
|
2026-01-15 08:43:10 +08:00
|
|
|
|
border-color: #ff3b30;
|
|
|
|
|
|
}
|
|
|
|
|
|
.greeting-text {
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
display: -webkit-box;
|
|
|
|
|
|
-webkit-box-orient: vertical;
|
|
|
|
|
|
-webkit-line-clamp: 4;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
2026-01-31 20:59:51 +08:00
|
|
|
|
|
|
|
|
|
|
.position-section{
|
|
|
|
|
|
margin-bottom: 40rpx;
|
|
|
|
|
|
}
|
2026-01-15 08:43:10 +08:00
|
|
|
|
.greeting-card.active .greeting-text {
|
|
|
|
|
|
color: #ff3b30;
|
|
|
|
|
|
}
|
|
|
|
|
|
.check-mark {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 10rpx;
|
|
|
|
|
|
right: 10rpx;
|
|
|
|
|
|
width: 32rpx;
|
|
|
|
|
|
height: 32rpx;
|
|
|
|
|
|
background: #ff3b30;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
font-size: 20rpx;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.input-wrapper {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
.edit-icon {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
right: 24rpx;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
}
|
2026-01-22 09:34:08 +08:00
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 08:43:10 +08:00
|
|
|
|
.color-list {
|
|
|
|
|
|
display: flex;
|
2026-01-22 09:34:08 +08:00
|
|
|
|
gap: 20rpx;
|
2026-01-22 15:08:26 +08:00
|
|
|
|
flex-wrap: wrap;
|
2026-01-15 08:43:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
.color-item {
|
|
|
|
|
|
width: 64rpx;
|
|
|
|
|
|
height: 64rpx;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
border: 2rpx solid rgba(0, 0, 0, 0.05);
|
|
|
|
|
|
}
|
|
|
|
|
|
.color-check {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
font-size: 32rpx;
|
|
|
|
|
|
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
|
2026-01-09 11:24:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 按钮区 */
|
|
|
|
|
|
.bottom-actions {
|
2026-01-15 08:43:10 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2026-01-09 11:24:40 +08:00
|
|
|
|
padding: 20rpx 24rpx 30rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
.btn {
|
2026-01-15 08:43:10 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
height: 88rpx;
|
|
|
|
|
|
border-radius: 999rpx;
|
|
|
|
|
|
padding: 0 40rpx;
|
2026-01-09 11:24:40 +08:00
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
}
|
2026-01-15 08:43:10 +08:00
|
|
|
|
.btn view {
|
|
|
|
|
|
margin-left: 10rpx;
|
|
|
|
|
|
}
|
2026-01-09 11:24:40 +08:00
|
|
|
|
.btn.secondary {
|
2026-01-15 08:43:10 +08:00
|
|
|
|
background: #fff;
|
|
|
|
|
|
color: #000;
|
|
|
|
|
|
border: 1px solid #ccc;
|
2026-01-09 11:24:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
.btn.primary {
|
2026-01-15 08:43:10 +08:00
|
|
|
|
background: #ff3b30;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
box-shadow: 0 12rpx 24rpx rgba(255, 59, 48, 0.35);
|
2026-01-09 11:24:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
.hidden-canvas {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
left: -9999px;
|
|
|
|
|
|
top: -9999px;
|
|
|
|
|
|
}
|
2026-01-15 08:43:10 +08:00
|
|
|
|
</style>
|