770 lines
16 KiB
Vue
770 lines
16 KiB
Vue
<template>
|
|
<view class="daily-page">
|
|
<NavBar title="每日精选" color="#333" />
|
|
|
|
<!-- Header -->
|
|
<view class="page-header">
|
|
<view class="header-left">
|
|
<uni-icons type="sun-filled" size="24" color="#ff9800" />
|
|
<text class="header-title">{{ greetingTitle }}</text>
|
|
</view>
|
|
<view class="streak-badge">
|
|
<text>已连续问候</text>
|
|
<text class="streak-count">{{ streakDays }}</text>
|
|
<text>天</text>
|
|
<text class="fire-icon">🔥</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Main Card -->
|
|
<view class="main-card-container">
|
|
<view
|
|
class="quote-card"
|
|
:style="
|
|
currentQuote.backgroundUrl
|
|
? {
|
|
backgroundImage: `url(${currentQuote.backgroundUrl})`,
|
|
backgroundSize: 'cover',
|
|
backgroundPosition: 'center',
|
|
}
|
|
: {}
|
|
"
|
|
>
|
|
<view class="quote-icon">❝</view>
|
|
<view class="quote-content">
|
|
<text class="quote-text">{{ currentQuote.text }}</text>
|
|
<text class="quote-highlight" v-if="currentQuote.highlight">{{
|
|
currentQuote.highlight
|
|
}}</text>
|
|
</view>
|
|
<view class="quote-divider"></view>
|
|
<view class="quote-author" v-if="authorName">
|
|
<text>—— {{ authorName }} 的专属问候</text>
|
|
</view>
|
|
|
|
<view class="card-actions">
|
|
<view class="refresh-btn" @tap="refreshQuote">
|
|
<uni-icons type="loop" size="16" color="#666" />
|
|
<text>换一句</text>
|
|
</view>
|
|
|
|
<view class="author-edit-box">
|
|
<input
|
|
class="input"
|
|
type="text"
|
|
v-model="authorName"
|
|
placeholder="输入名字生成专属问候"
|
|
/>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Categories -->
|
|
<view class="category-section">
|
|
<view class="category-grid">
|
|
<view
|
|
v-for="(cat, index) in scenes"
|
|
:key="index"
|
|
class="category-item"
|
|
@tap="selectCategory(cat.id)"
|
|
>
|
|
<view
|
|
class="cat-icon-box"
|
|
:class="{ active: currentCategory === cat.id }"
|
|
:style="{ background: cat.bg }"
|
|
>
|
|
<text class="cat-emoji">{{ cat.icon }}</text>
|
|
</view>
|
|
<text
|
|
class="cat-name"
|
|
:class="{ active: currentCategory === cat.id }"
|
|
>{{ cat.name }}</text
|
|
>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Hot List -->
|
|
<view class="hot-list-section">
|
|
<view class="section-title-row">
|
|
<view class="title-bar"></view>
|
|
<text class="section-title">今日最热榜单</text>
|
|
<text class="section-subtitle">REAL-TIME DATA</text>
|
|
</view>
|
|
|
|
<view class="hot-list">
|
|
<view
|
|
v-for="(item, index) in hotList"
|
|
:key="index"
|
|
class="hot-item"
|
|
@tap="useHotItem(item)"
|
|
>
|
|
<view class="rank-icon">
|
|
<uni-icons
|
|
v-if="index === 0"
|
|
type="vip-filled"
|
|
size="24"
|
|
color="#ffbc00"
|
|
/>
|
|
<uni-icons
|
|
v-else-if="index === 1"
|
|
type="vip-filled"
|
|
size="24"
|
|
color="#b0bec5"
|
|
/>
|
|
<uni-icons v-else type="vip-filled" size="24" color="#cd7f32" />
|
|
</view>
|
|
<view class="hot-content">
|
|
<text class="hot-text">{{ item.text }}</text>
|
|
<view class="hot-meta">
|
|
<text class="fire">🔥</text>
|
|
<text class="hot-count">{{ item.count }}w 人正在使用</text>
|
|
</view>
|
|
</view>
|
|
<uni-icons type="right" size="16" color="#ccc" />
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Bottom Actions -->
|
|
<view class="bottom-actions safe-area-bottom">
|
|
<view class="action-btn-group">
|
|
<button class="save-btn" @tap="saveCard">
|
|
<view class="icon-circle">
|
|
<uni-icons type="download" size="20" color="#333" />
|
|
</view>
|
|
<text class="btn-text">保存</text>
|
|
</button>
|
|
<button class="send-btn" open-type="share">
|
|
<uni-icons type="paperplane-filled" size="20" color="#fff" />
|
|
<text>立即发送今日问候</text>
|
|
</button>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted, watch } from "vue";
|
|
import { getBavBarHeight } from "@/utils/system";
|
|
import NavBar from "@/components/NavBar/NavBar.vue";
|
|
import {
|
|
onShareAppMessage,
|
|
onShareTimeline,
|
|
onShow,
|
|
onLoad,
|
|
} from "@dcloudio/uni-app";
|
|
import { useUserStore } from "@/stores/user";
|
|
|
|
import { getDailyInfo, getDailyRandomGreeting } from "@/api/daily";
|
|
|
|
const userStore = useUserStore();
|
|
|
|
const navBarHeight = getBavBarHeight();
|
|
const streakDays = ref(0);
|
|
|
|
const dateStr = computed(() => {
|
|
const date = new Date();
|
|
return `${date.getFullYear()} 灵马送福 · 每日精选`;
|
|
});
|
|
|
|
const greetingTitle = computed(() => {
|
|
const hour = new Date().getHours();
|
|
if (hour < 9) return "早安,今天也要好运";
|
|
if (hour < 12) return "上午好,元气满满";
|
|
if (hour < 14) return "午安,记得休息";
|
|
if (hour < 18) return "下午好,继续加油";
|
|
return "晚安,好梦相伴";
|
|
});
|
|
|
|
const scenes = ref([]);
|
|
const currentCategory = ref("");
|
|
|
|
const currentQuote = ref({
|
|
text: "",
|
|
highlight: "",
|
|
author: "",
|
|
backgroundUrl: "",
|
|
id: "",
|
|
});
|
|
const authorName = ref("");
|
|
|
|
const loadDailyInfo = async () => {
|
|
try {
|
|
const res = await getDailyInfo();
|
|
if (res) {
|
|
streakDays.value = res.streakDays || 0;
|
|
if (res.lastSignature && !authorName.value) {
|
|
authorName.value = res.lastSignature;
|
|
}
|
|
|
|
if (res.scenes && res.scenes.length > 0) {
|
|
scenes.value = res.scenes;
|
|
if (!currentCategory.value) {
|
|
currentCategory.value = res.scenes[0].id;
|
|
}
|
|
}
|
|
|
|
if (res.mainHero) {
|
|
const { greetingContent, backgroundUrl, greetingId, greetingScene } =
|
|
res.mainHero;
|
|
let text = "";
|
|
let highlight = "";
|
|
if (greetingContent) {
|
|
const parts = greetingContent.split(" ");
|
|
if (parts.length > 1) {
|
|
highlight = parts.pop();
|
|
text = parts.join("\n");
|
|
} else {
|
|
text = greetingContent;
|
|
}
|
|
}
|
|
|
|
currentQuote.value = {
|
|
text,
|
|
highlight,
|
|
author: authorName.value, // Will be reactive in template via authorName ref
|
|
backgroundUrl: backgroundUrl || "",
|
|
id: greetingId,
|
|
};
|
|
|
|
if (greetingScene) {
|
|
currentCategory.value = greetingScene;
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to load daily info:", e);
|
|
}
|
|
};
|
|
|
|
onMounted(() => {
|
|
loadDailyInfo();
|
|
});
|
|
|
|
const hotList = ref([
|
|
{ text: "新的一年,愿灵马带走烦恼...", count: "2.4" },
|
|
{ text: "事事顺意,岁岁平安。", count: "1.8" },
|
|
]);
|
|
|
|
const selectCategory = (id) => {
|
|
currentCategory.value = id;
|
|
refreshQuote();
|
|
};
|
|
|
|
const refreshQuote = async () => {
|
|
try {
|
|
const res = await getDailyRandomGreeting(currentCategory.value);
|
|
if (res) {
|
|
const { greetingContent, backgroundUrl, greetingId } = res;
|
|
let text = "";
|
|
let highlight = "";
|
|
if (greetingContent) {
|
|
const parts = greetingContent.split(" ");
|
|
if (parts.length > 1) {
|
|
highlight = parts.pop();
|
|
text = parts.join("\n");
|
|
} else {
|
|
text = greetingContent;
|
|
}
|
|
}
|
|
|
|
currentQuote.value = {
|
|
...currentQuote.value,
|
|
text,
|
|
highlight,
|
|
backgroundUrl: backgroundUrl || "",
|
|
id: greetingId,
|
|
};
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to refresh quote:", e);
|
|
}
|
|
};
|
|
|
|
const useHotItem = (item) => {
|
|
currentQuote.value = {
|
|
...currentQuote.value,
|
|
text: item.text,
|
|
highlight: "",
|
|
author: "热榜推荐",
|
|
};
|
|
};
|
|
|
|
const saveCard = () => {
|
|
uni.showToast({ title: "保存功能开发中", icon: "none" });
|
|
};
|
|
|
|
// const sendGreeting = () => {
|
|
// // Use uni.navigateTo for normal flow if not open-type="share"
|
|
// // But here we might want to trigger share directly or just navigate to make page as before
|
|
// // Based on user request "share after friend opens page", it implies we need to handle share
|
|
// // Let's keep the navigation to make page for "Sending" (which usually means making a card first)
|
|
// // BUT also add onShareAppMessage so the top right menu share works
|
|
// uni.navigateTo({
|
|
// url: `/pages/make/index?scene=daily&content=${encodeURIComponent(
|
|
// currentQuote.value.text,
|
|
// )}&author=${encodeURIComponent(authorName.value)}`,
|
|
// });
|
|
// };
|
|
|
|
onShareAppMessage((res) => {
|
|
const fromUser = userStore.userInfo?.nickName || "神秘好友";
|
|
const fromAvatar = userStore.userInfo?.avatarUrl || "";
|
|
|
|
// Log the share attempt and path
|
|
console.log("Sharing daily greeting", {
|
|
res,
|
|
fromUser,
|
|
content: currentQuote.value.text,
|
|
});
|
|
|
|
const path = `/pages/greeting/share?content=${encodeURIComponent(
|
|
currentQuote.value.text,
|
|
)}&author=${encodeURIComponent(
|
|
authorName.value || currentQuote.value.author,
|
|
)}&fromUser=${encodeURIComponent(fromUser)}&fromAvatar=${encodeURIComponent(
|
|
fromAvatar,
|
|
)}`;
|
|
|
|
console.log("Share path:", path);
|
|
|
|
return {
|
|
title: `${fromUser}给你发来了一份今日问候`,
|
|
path: path,
|
|
};
|
|
});
|
|
|
|
onShareTimeline(() => {
|
|
const fromUser = userStore.userInfo?.nickName || "神秘好友";
|
|
return {
|
|
title: `${fromUser}给你发来了一份今日问候`,
|
|
query: `content=${encodeURIComponent(
|
|
currentQuote.value.text,
|
|
)}&author=${encodeURIComponent(
|
|
authorName.value || currentQuote.value.author,
|
|
)}`,
|
|
imageUrl: "", // Optional: add a custom image if needed
|
|
};
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.daily-page {
|
|
min-height: 100vh;
|
|
background: #fbfbf9;
|
|
padding-bottom: 200rpx;
|
|
}
|
|
|
|
.page-header {
|
|
padding: 0 32rpx;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 15rpx;
|
|
}
|
|
|
|
.header-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12rpx;
|
|
}
|
|
|
|
.header-title {
|
|
font-size: 36rpx;
|
|
font-weight: 800;
|
|
color: #333;
|
|
}
|
|
|
|
.streak-badge {
|
|
display: flex;
|
|
align-items: center;
|
|
background: #fff;
|
|
padding: 6rpx 20rpx;
|
|
border-radius: 30rpx;
|
|
font-size: 22rpx;
|
|
color: #666;
|
|
border: 1rpx solid #eee;
|
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
|
|
|
.streak-count {
|
|
font-size: 28rpx;
|
|
font-weight: bold;
|
|
color: #ff3b30;
|
|
margin: 0 4rpx;
|
|
}
|
|
|
|
.fire-icon {
|
|
margin-left: 4rpx;
|
|
font-size: 24rpx;
|
|
}
|
|
}
|
|
|
|
.sub-header {
|
|
padding: 0 32rpx;
|
|
font-size: 24rpx;
|
|
color: #999;
|
|
margin-bottom: 40rpx;
|
|
}
|
|
|
|
/* Main Card */
|
|
.main-card-container {
|
|
padding: 0 40rpx;
|
|
margin-bottom: 60rpx;
|
|
}
|
|
|
|
.quote-card {
|
|
background: #fff;
|
|
border-radius: 40rpx;
|
|
padding: 60rpx 40rpx 40rpx;
|
|
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.08);
|
|
border: 4rpx solid #f5e6d3; /* Gold-ish border */
|
|
position: relative;
|
|
min-height: 600rpx;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
|
|
/* Inner Border Effect */
|
|
&::after {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 12rpx;
|
|
border: 2rpx solid #f9f3e8;
|
|
border-radius: 32rpx;
|
|
pointer-events: none;
|
|
}
|
|
}
|
|
|
|
.quote-icon {
|
|
font-size: 80rpx;
|
|
color: #f5e6d3;
|
|
margin-bottom: 40rpx;
|
|
line-height: 1;
|
|
}
|
|
|
|
.quote-content {
|
|
text-align: center;
|
|
margin-bottom: 60rpx;
|
|
}
|
|
|
|
.quote-text {
|
|
font-size: 40rpx;
|
|
color: #333;
|
|
font-weight: bold;
|
|
line-height: 1.6;
|
|
font-family: "Songti SC", serif;
|
|
display: block;
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
.quote-highlight {
|
|
display: block;
|
|
font-size: 44rpx;
|
|
color: #d81e06;
|
|
font-weight: 800;
|
|
margin-top: 20rpx;
|
|
font-family: "Songti SC", serif;
|
|
}
|
|
|
|
.quote-divider {
|
|
width: 80rpx;
|
|
height: 2rpx;
|
|
background: #eee;
|
|
margin-bottom: 30rpx;
|
|
}
|
|
|
|
.quote-author {
|
|
font-size: 24rpx;
|
|
color: #999;
|
|
margin-bottom: 60rpx;
|
|
font-style: italic;
|
|
}
|
|
|
|
.card-actions {
|
|
width: 100%;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
z-index: 2;
|
|
}
|
|
|
|
.refresh-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12rpx;
|
|
padding: 16rpx 32rpx;
|
|
background: #fff;
|
|
border: 1rpx solid #eee;
|
|
border-radius: 40rpx;
|
|
font-size: 26rpx;
|
|
color: #666;
|
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
|
|
|
|
&:active {
|
|
background: #f5f5f5;
|
|
}
|
|
}
|
|
|
|
.author-edit-box {
|
|
flex: 1;
|
|
margin-left: 24rpx;
|
|
background: #f9f9f9;
|
|
border: 2rpx solid #eee;
|
|
border-radius: 40rpx;
|
|
height: 80rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0 24rpx;
|
|
transition: all 0.3s ease;
|
|
|
|
&:active,
|
|
&:focus-within {
|
|
background: #fff;
|
|
border-color: #ff9800;
|
|
box-shadow: 0 4rpx 16rpx rgba(255, 152, 0, 0.15);
|
|
}
|
|
|
|
.label {
|
|
font-size: 24rpx;
|
|
color: #999;
|
|
margin-right: 12rpx;
|
|
font-weight: 500;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.input {
|
|
flex: 1;
|
|
height: 100%;
|
|
font-size: 28rpx;
|
|
color: #333;
|
|
}
|
|
}
|
|
|
|
/* Categories */
|
|
.category-section {
|
|
padding: 0 32rpx;
|
|
margin-bottom: 60rpx;
|
|
}
|
|
|
|
.category-grid {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.category-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 16rpx;
|
|
}
|
|
|
|
.cat-icon-box {
|
|
width: 100rpx;
|
|
height: 100rpx;
|
|
border-radius: 30rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: all 0.2s;
|
|
border: 2rpx solid transparent;
|
|
|
|
&.active {
|
|
transform: translateY(-4rpx);
|
|
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.08);
|
|
border-color: rgba(0, 0, 0, 0.05);
|
|
}
|
|
}
|
|
|
|
.cat-emoji {
|
|
font-size: 48rpx;
|
|
}
|
|
|
|
.cat-name {
|
|
font-size: 24rpx;
|
|
color: #666;
|
|
font-weight: 500;
|
|
|
|
&.active {
|
|
color: #333;
|
|
font-weight: bold;
|
|
}
|
|
}
|
|
|
|
/* Hot List */
|
|
.hot-list-section {
|
|
padding: 0 32rpx;
|
|
margin-bottom: 40rpx;
|
|
}
|
|
|
|
.section-title-row {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 30rpx;
|
|
}
|
|
|
|
.title-bar {
|
|
width: 8rpx;
|
|
height: 32rpx;
|
|
background: #d4a017;
|
|
border-radius: 4rpx;
|
|
margin-right: 16rpx;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 32rpx;
|
|
font-weight: bold;
|
|
color: #333;
|
|
margin-right: 16rpx;
|
|
}
|
|
|
|
.section-subtitle {
|
|
font-size: 20rpx;
|
|
color: #ccc;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.hot-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 24rpx;
|
|
}
|
|
|
|
.hot-item {
|
|
background: #fff;
|
|
border-radius: 24rpx;
|
|
padding: 24rpx 32rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.02);
|
|
|
|
&:active {
|
|
background: #f9f9f9;
|
|
}
|
|
}
|
|
|
|
.rank-icon {
|
|
margin-right: 24rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.hot-content {
|
|
flex: 1;
|
|
}
|
|
|
|
.hot-text {
|
|
font-size: 28rpx;
|
|
color: #333;
|
|
margin-bottom: 8rpx;
|
|
display: block;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
max-width: 450rpx;
|
|
}
|
|
|
|
.hot-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
font-size: 22rpx;
|
|
color: #999;
|
|
}
|
|
|
|
.fire {
|
|
color: #ff3b30;
|
|
margin-right: 4rpx;
|
|
font-size: 20rpx;
|
|
}
|
|
|
|
/* Bottom Actions */
|
|
.bottom-actions {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
background: #fff; /* Glass effect if supported */
|
|
background: rgba(255, 255, 255, 0.95);
|
|
backdrop-filter: blur(20rpx);
|
|
padding: 20rpx 32rpx;
|
|
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
|
|
z-index: 100;
|
|
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
|
}
|
|
|
|
.action-btn-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 24rpx;
|
|
}
|
|
|
|
.save-btn {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: transparent;
|
|
padding: 0;
|
|
margin: 0;
|
|
line-height: 1;
|
|
border: none;
|
|
width: 100rpx;
|
|
|
|
&::after {
|
|
border: none;
|
|
}
|
|
|
|
.icon-circle {
|
|
width: 80rpx;
|
|
height: 80rpx;
|
|
background: #f5f5f5;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-bottom: 8rpx;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.btn-text {
|
|
font-size: 20rpx;
|
|
color: #666;
|
|
font-weight: 500;
|
|
}
|
|
|
|
&:active .icon-circle {
|
|
background: #eee;
|
|
transform: scale(0.95);
|
|
}
|
|
}
|
|
|
|
.send-btn {
|
|
flex: 1;
|
|
height: 96rpx;
|
|
border-radius: 48rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 16rpx;
|
|
font-size: 32rpx;
|
|
font-weight: bold;
|
|
border: none;
|
|
background: #8e0000; /* Deep Red/Brown */
|
|
color: #fff;
|
|
background: linear-gradient(135deg, #8e0000 0%, #600000 100%);
|
|
box-shadow: 0 8rpx 24rpx rgba(142, 0, 0, 0.3);
|
|
transition: all 0.2s;
|
|
|
|
&::after {
|
|
border: none;
|
|
}
|
|
|
|
&:active {
|
|
transform: scale(0.98);
|
|
box-shadow: 0 4rpx 12rpx rgba(142, 0, 0, 0.2);
|
|
}
|
|
}
|
|
</style>
|