Files

792 lines
17 KiB
Vue
Raw Permalink Normal View History

2026-03-01 22:35:20 +08:00
<template>
<view class="daily-page">
2026-03-01 22:58:21 +08:00
<NavBar title="每日精选" color="#333" />
2026-03-01 22:35:20 +08:00
<!-- Header -->
2026-03-01 22:58:21 +08:00
<view class="page-header">
2026-03-01 22:35:20 +08:00
<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">
2026-03-02 15:23:00 +08:00
<view
class="quote-card"
:style="
currentQuote.backgroundUrl
? {
backgroundImage: `url(${currentQuote.backgroundUrl})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}
: {}
"
>
2026-03-01 22:35:20 +08:00
<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>
2026-03-01 22:58:21 +08:00
<view class="quote-author" v-if="authorName">
<text> {{ authorName }} 的专属问候</text>
2026-03-01 22:35:20 +08:00
</view>
<view class="card-actions">
<view class="refresh-btn" @tap="refreshQuote">
<uni-icons type="loop" size="16" color="#666" />
<text>换一句</text>
</view>
2026-03-01 22:58:21 +08:00
<view class="author-edit-box">
<input
class="input"
type="text"
v-model="authorName"
placeholder="输入名字生成专属问候"
/>
</view>
2026-03-01 22:35:20 +08:00
</view>
</view>
</view>
<!-- Categories -->
<view class="category-section">
<view class="category-grid">
<view
2026-03-02 15:23:00 +08:00
v-for="(cat, index) in scenes"
2026-03-01 22:35:20 +08:00
:key="index"
class="category-item"
@tap="selectCategory(cat.id)"
>
<view
class="cat-icon-box"
:class="{ active: currentCategory === cat.id }"
:style="{ background: cat.bg }"
>
2026-03-02 15:23:00 +08:00
<text class="cat-emoji">{{ cat.icon }}</text>
2026-03-01 22:35:20 +08:00
</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"
/>
2026-03-01 22:41:38 +08:00
<uni-icons v-else type="vip-filled" size="24" color="#cd7f32" />
2026-03-01 22:35:20 +08:00
</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">
2026-03-01 22:41:38 +08:00
<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>
2026-03-01 23:14:49 +08:00
<button class="send-btn" open-type="share">
2026-03-01 22:41:38 +08:00
<uni-icons type="paperplane-filled" size="20" color="#fff" />
<text>立即发送今日问候</text>
</button>
</view>
2026-03-01 22:35:20 +08:00
</view>
2026-03-02 21:14:37 +08:00
<!-- 登录弹窗 -->
<LoginPopup />
2026-03-01 22:35:20 +08:00
</view>
</template>
<script setup>
2026-03-02 21:14:37 +08:00
import { ref, computed, onMounted } from "vue";
2026-03-01 22:35:20 +08:00
import NavBar from "@/components/NavBar/NavBar.vue";
2026-03-02 21:14:37 +08:00
import { onShareAppMessage, onShareTimeline } from "@dcloudio/uni-app";
2026-03-01 23:14:49 +08:00
import { useUserStore } from "@/stores/user";
2026-03-02 21:14:37 +08:00
import {
getDailyInfo,
getDailyRandomGreeting,
saveDailyGreeting,
} from "@/api/daily";
import {
getShareToken,
saveViewRequest,
generateObjectId,
} from "@/utils/common.js";
import LoginPopup from "@/components/LoginPopup/LoginPopup.vue";
2026-03-02 15:23:00 +08:00
2026-03-01 23:14:49 +08:00
const userStore = useUserStore();
2026-03-02 15:23:00 +08:00
const streakDays = ref(0);
2026-03-01 22:35:20 +08:00
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 "晚安,好梦相伴";
});
2026-03-02 15:23:00 +08:00
const scenes = ref([]);
const currentCategory = ref("");
2026-03-01 22:35:20 +08:00
2026-03-02 15:23:00 +08:00
const currentQuote = ref({
text: "",
highlight: "",
author: "",
backgroundUrl: "",
id: "",
});
const authorName = ref("");
2026-03-01 22:35:20 +08:00
2026-03-02 15:23:00 +08:00
const loadDailyInfo = async () => {
try {
const res = await getDailyInfo();
if (res) {
streakDays.value = res.streakDays || 0;
if (res.lastSignature && !authorName.value) {
authorName.value = res.lastSignature;
}
2026-03-01 22:35:20 +08:00
2026-03-02 15:23:00 +08:00
if (res.scenes && res.scenes.length > 0) {
scenes.value = res.scenes;
if (!currentCategory.value) {
currentCategory.value = res.scenes[0].id;
}
}
2026-03-01 22:58:21 +08:00
2026-03-02 15:23:00 +08:00
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,
2026-03-02 21:14:37 +08:00
content: greetingContent,
2026-03-02 15:23:00 +08:00
};
if (greetingScene) {
currentCategory.value = greetingScene;
}
}
}
} catch (e) {
console.error("Failed to load daily info:", e);
}
};
onMounted(() => {
loadDailyInfo();
});
2026-03-01 22:35:20 +08:00
const hotList = ref([
{ text: "新的一年,愿灵马带走烦恼...", count: "2.4" },
{ text: "事事顺意,岁岁平安。", count: "1.8" },
]);
const selectCategory = (id) => {
currentCategory.value = id;
refreshQuote();
};
2026-03-02 15:32:37 +08:00
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,
2026-03-02 21:14:37 +08:00
content: greetingContent,
2026-03-02 15:32:37 +08:00
};
}
} catch (e) {
console.error("Failed to refresh quote:", e);
}
2026-03-01 22:35:20 +08:00
};
const useHotItem = (item) => {
currentQuote.value = {
2026-03-02 15:23:00 +08:00
...currentQuote.value,
2026-03-01 22:35:20 +08:00
text: item.text,
highlight: "",
author: "热榜推荐",
};
};
2026-03-01 22:41:38 +08:00
const saveCard = () => {
uni.showToast({ title: "保存功能开发中", icon: "none" });
2026-03-01 22:35:20 +08:00
};
2026-03-01 23:14:49 +08:00
// 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)}`,
// });
// };
2026-03-02 21:14:37 +08:00
onShareAppMessage(async (options) => {
const id = createGreeting();
const shareToken = await getShareToken("daily_greeting", id);
return {
title: `${authorName.value}给你发来了一份今日问候`,
path: "/pages/greeting/share?shareToken=" + shareToken,
};
2026-03-01 23:14:49 +08:00
const fromUser = userStore.userInfo?.nickName || "神秘好友";
const fromAvatar = userStore.userInfo?.avatarUrl || "";
2026-03-02 11:14:00 +08:00
// 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,
};
});
2026-03-02 21:14:37 +08:00
const createGreeting = () => {
const id = generateObjectId();
saveDailyGreeting({
id,
greetingId: currentQuote.value.id,
content: currentQuote.value.content,
signature: authorName.value,
imageUrl: currentQuote.value.backgroundUrl,
sceneId: currentCategory.value,
});
return id;
};
2026-03-02 11:14:00 +08:00
onShareTimeline(() => {
const fromUser = userStore.userInfo?.nickName || "神秘好友";
2026-03-01 23:14:49 +08:00
return {
title: `${fromUser}给你发来了一份今日问候`,
2026-03-02 11:14:00 +08:00
query: `content=${encodeURIComponent(
2026-03-01 22:58:21 +08:00
currentQuote.value.text,
2026-03-01 23:14:49 +08:00
)}&author=${encodeURIComponent(
authorName.value || currentQuote.value.author,
)}`,
2026-03-02 11:14:00 +08:00
imageUrl: "", // Optional: add a custom image if needed
2026-03-01 23:14:49 +08:00
};
});
2026-03-01 22:35:20 +08:00
</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;
2026-03-01 22:58:21 +08:00
margin-bottom: 15rpx;
2026-03-01 22:35:20 +08:00
}
.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;
}
}
2026-03-01 22:58:21 +08:00
.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;
}
}
2026-03-01 22:35:20 +08:00
/* 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));
}
2026-03-01 22:41:38 +08:00
.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;
2026-03-01 22:35:20 +08:00
height: 96rpx;
border-radius: 48rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
2026-03-01 22:41:38 +08:00
font-size: 32rpx;
2026-03-01 22:35:20 +08:00
font-weight: bold;
border: none;
2026-03-01 22:41:38 +08:00
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;
2026-03-01 22:35:20 +08:00
2026-03-01 22:41:38 +08:00
&::after {
border: none;
2026-03-01 22:35:20 +08:00
}
&:active {
2026-03-01 22:41:38 +08:00
transform: scale(0.98);
box-shadow: 0 4rpx 12rpx rgba(142, 0, 0, 0.2);
2026-03-01 22:35:20 +08:00
}
}
2026-03-01 22:41:38 +08:00
</style>