feat: 添加签到功能,更新相关路由和组件,集成日期选择器

This commit is contained in:
2026-01-18 01:45:22 +07:00
parent 4dd2a49c70
commit 1bdaad1434
9 changed files with 83 additions and 87 deletions

2
components.d.ts vendored
View File

@@ -19,6 +19,7 @@ declare module 'vue' {
IonButton: typeof import('@ionic/vue')['IonButton'] IonButton: typeof import('@ionic/vue')['IonButton']
IonCheckbox: typeof import('@ionic/vue')['IonCheckbox'] IonCheckbox: typeof import('@ionic/vue')['IonCheckbox']
IonContent: typeof import('@ionic/vue')['IonContent'] IonContent: typeof import('@ionic/vue')['IonContent']
IonDatetime: typeof import('@ionic/vue')['IonDatetime']
IonHeader: typeof import('@ionic/vue')['IonHeader'] IonHeader: typeof import('@ionic/vue')['IonHeader']
IonIcon: typeof import('@ionic/vue')['IonIcon'] IonIcon: typeof import('@ionic/vue')['IonIcon']
IonInfiniteScroll: typeof import('@ionic/vue')['IonInfiniteScroll'] IonInfiniteScroll: typeof import('@ionic/vue')['IonInfiniteScroll']
@@ -49,6 +50,7 @@ declare global {
const IonButton: typeof import('@ionic/vue')['IonButton'] const IonButton: typeof import('@ionic/vue')['IonButton']
const IonCheckbox: typeof import('@ionic/vue')['IonCheckbox'] const IonCheckbox: typeof import('@ionic/vue')['IonCheckbox']
const IonContent: typeof import('@ionic/vue')['IonContent'] const IonContent: typeof import('@ionic/vue')['IonContent']
const IonDatetime: typeof import('@ionic/vue')['IonDatetime']
const IonHeader: typeof import('@ionic/vue')['IonHeader'] const IonHeader: typeof import('@ionic/vue')['IonHeader']
const IonIcon: typeof import('@ionic/vue')['IonIcon'] const IonIcon: typeof import('@ionic/vue')['IonIcon']
const IonInfiniteScroll: typeof import('@ionic/vue')['IonInfiniteScroll'] const IonInfiniteScroll: typeof import('@ionic/vue')['IonInfiniteScroll']

14
pnpm-lock.yaml generated
View File

@@ -52,8 +52,8 @@ catalogs:
specifier: 8.0.0 specifier: 8.0.0
version: 8.0.0 version: 8.0.0
'@capp/eden': '@capp/eden':
specifier: http://192.168.1.2:9538/api/capp-eden-0.0.4.tgz specifier: http://192.168.1.2:9538/api/capp-eden-0.0.5.tgz
version: 0.0.4 version: 0.0.5
'@cloudflare/workers-types': '@cloudflare/workers-types':
specifier: ^4.20260113.0 specifier: ^4.20260113.0
version: 4.20260116.0 version: 4.20260116.0
@@ -298,7 +298,7 @@ importers:
version: 8.0.0(@capacitor/core@8.0.0) version: 8.0.0(@capacitor/core@8.0.0)
'@capp/eden': '@capp/eden':
specifier: 'catalog:' specifier: 'catalog:'
version: http://192.168.1.2:9538/api/capp-eden-0.0.4.tgz(@elysiajs/eden@1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))) version: http://192.168.1.2:9538/api/capp-eden-0.0.5.tgz(@elysiajs/eden@1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)))
'@elysiajs/eden': '@elysiajs/eden':
specifier: 'catalog:' specifier: 'catalog:'
version: 1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)) version: 1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))
@@ -1182,9 +1182,9 @@ packages:
'@capacitor/synapse@1.0.4': '@capacitor/synapse@1.0.4':
resolution: {integrity: sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==} resolution: {integrity: sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==}
'@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.4.tgz': '@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.5.tgz':
resolution: {tarball: http://192.168.1.2:9538/api/capp-eden-0.0.4.tgz} resolution: {tarball: http://192.168.1.2:9538/api/capp-eden-0.0.5.tgz}
version: 0.0.4 version: 0.0.5
peerDependencies: peerDependencies:
'@elysiajs/eden': ^1.4.6 '@elysiajs/eden': ^1.4.6
@@ -6903,7 +6903,7 @@ snapshots:
'@capacitor/synapse@1.0.4': {} '@capacitor/synapse@1.0.4': {}
'@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.4.tgz(@elysiajs/eden@1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)))': '@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.5.tgz(@elysiajs/eden@1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)))':
dependencies: dependencies:
'@elysiajs/eden': 1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)) '@elysiajs/eden': 1.4.6(elysia@1.4.22(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))

View File

@@ -18,7 +18,7 @@ catalog:
'@capacitor/keyboard': 8.0.0 '@capacitor/keyboard': 8.0.0
'@capacitor/share': ^8.0.0 '@capacitor/share': ^8.0.0
'@capacitor/status-bar': 8.0.0 '@capacitor/status-bar': 8.0.0
'@capp/eden': http://192.168.1.2:9538/api/capp-eden-0.0.4.tgz '@capp/eden': http://192.168.1.2:9538/api/capp-eden-0.0.5.tgz
'@cloudflare/workers-types': ^4.20260113.0 '@cloudflare/workers-types': ^4.20260113.0
'@elysiajs/eden': ^1.4.6 '@elysiajs/eden': ^1.4.6
'@faker-js/faker': ^10.2.0 '@faker-js/faker': ^10.2.0

4
src/dayjs/index.ts Normal file
View File

@@ -0,0 +1,4 @@
import dayjs from "dayjs";
import "dayjs/locale/zh-cn";
dayjs.locale("zh-cn");

View File

@@ -6,8 +6,9 @@ import { createApp } from "vue";
import App from "./App.vue"; import App from "./App.vue";
import { authClient } from "./auth"; import { authClient } from "./auth";
import { i18n } from "./locales"; import { i18n } from "./locales";
import { router } from "./router"; import { router } from "./router";
import "./dayjs";
/* Core CSS required for Ionic components to work properly */ /* Core CSS required for Ionic components to work properly */
import "@ionic/vue/css/core.css"; import "@ionic/vue/css/core.css";
/* Basic CSS for apps built with Ionic */ /* Basic CSS for apps built with Ionic */

View File

@@ -40,8 +40,8 @@ const routes: Array<RouteRecordRaw> = [
], ],
}, },
{ {
path: "/signup", path: "/check_in",
component: () => import("@/views/signup/index.vue"), component: () => import("@/views/check_in/index.vue"),
}, },
{ {
path: "/invite", path: "/invite",

View File

@@ -4,7 +4,7 @@ import dayjs from "dayjs";
import { checkmarkCircleOutline } from "ionicons/icons"; import { checkmarkCircleOutline } from "ionicons/icons";
import { client, safeClient } from "@/api"; import { client, safeClient } from "@/api";
const [start, end] = [dayjs().startOf("week"), dayjs().endOf("week")]; const [start, end] = [dayjs().startOf("month"), dayjs().endOf("month")];
const { data } = await safeClient(client.api.checkIns.get({ const { data } = await safeClient(client.api.checkIns.get({
query: { query: {
startDate: start.toISOString(), startDate: start.toISOString(),
@@ -12,23 +12,20 @@ const { data } = await safeClient(client.api.checkIns.get({
}, },
})); }));
// const { data: current_streak } = await safeClient(client.api.checkIns.current_streak.get());
const signupInfo = ref({ const { data: total_count } = await safeClient(client.api.checkIns.total_days.get());
consecutiveDays: 53, const { data: today_checkin } = await safeClient(client.api.checkIns.today.get({ query: { timezone: "IANA" } }));
totalDays: 127,
isSignedToday: false, const checkedInDates = computed(() => {
if (!data.value?.data)
return new Set<string>();
return new Set(data.value.data.map(item => dayjs(item.checkInAt).format("YYYY-MM-DD")));
}); });
// 7 function isCheckedIn(day: number) {
const recentSignup = ref([ const dateStr = dayjs().year(start.year()).month(start.month()).date(day).format("YYYY-MM-DD");
{ day: "周一", date: "01-13", signed: true }, return checkedInDates.value.has(dateStr);
{ day: "周二", date: "01-14", signed: true }, }
{ day: "周三", date: "01-15", signed: true },
{ day: "周四", date: "01-16", signed: true },
{ day: "周五", date: "01-17", signed: false },
{ day: "周六", date: "01-18", signed: false },
{ day: "周日", date: "01-19", signed: false },
]);
async function handleSignup() { async function handleSignup() {
await safeClient(client.api.checkIns.post()); await safeClient(client.api.checkIns.post());
@@ -75,7 +72,7 @@ async function handleSignup() {
连续签到 连续签到
</div> </div>
<div class="text-4xl font-bold mb-1"> <div class="text-4xl font-bold mb-1">
{{ signupInfo.consecutiveDays }} {{ current_streak?.currentStreakDays }}
</div> </div>
<div class="text-xs opacity-80"> <div class="text-xs opacity-80">
@@ -86,7 +83,7 @@ async function handleSignup() {
累计签到 累计签到
</div> </div>
<div class="text-4xl font-bold mb-1"> <div class="text-4xl font-bold mb-1">
{{ signupInfo.totalDays }} {{ total_count?.totalDays }}
</div> </div>
<div class="text-xs opacity-80"> <div class="text-xs opacity-80">
@@ -97,77 +94,76 @@ async function handleSignup() {
<!-- 签到按钮 --> <!-- 签到按钮 -->
<ion-button <ion-button
expand="block" expand="block"
:disabled="signupInfo.isSignedToday" :disabled="today_checkin?.checkedInToday"
@click="handleSignup" @click="handleSignup"
> >
<ion-icon slot="start" :icon="checkmarkCircleOutline" class="text-xl" /> <ion-icon slot="start" :icon="checkmarkCircleOutline" class="text-xl" />
{{ signupInfo.isSignedToday ? '今日已签到' : '立即签到' }} {{ today_checkin?.checkedInToday ? '今日已签到' : '立即签到' }}
</ion-button> </ion-button>
</div> </div>
</section> </section>
<!-- 本周签到记录 -->
<section class="mb-8"> <section class="mb-8">
<div class="card rounded-2xl shadow-lg p-5"> <div class="card rounded-2xl shadow-lg p-5">
<div class="flex items-center gap-2 mb-5"> <div class="flex items-center gap-2 mb-5">
<img src="@/assets/images/icon.png" class="size-7"> <img src="@/assets/images/icon.png" class="size-7">
<div class="text-xl font-bold text-[#1a1a1a]"> <div class="text-xl font-bold text-[#1a1a1a]">
本周签到 {{ dayjs().format('YYYY年MM月') }} 签到日历
</div> </div>
</div> </div>
<!-- 日历表头 - 星期 -->
<div class="grid grid-cols-7 gap-2 mb-3">
<div
v-for="weekday in ['日', '一', '二', '三', '四', '五', '六']"
:key="weekday"
class="text-center text-xs font-bold text-[#999]"
>
{{ weekday }}
</div>
</div>
<!-- 日历日期 -->
<div class="grid grid-cols-7 gap-2"> <div class="grid grid-cols-7 gap-2">
<!-- 填充空白日期月初之前 -->
<div <div
v-for="(day, index) in recentSignup" v-for="blank in Array.from({ length: start.day() })"
:key="index" :key="`blank-${blank}`"
class="flex flex-col items-center gap-2" class="aspect-square"
> />
<div
class="w-10 h-10 rounded-full flex-center text-xs font-bold transition-all"
:class="day.signed
? 'bg-primary text-white shadow-md'
: 'bg-gray-100 text-gray-400'"
>
<ion-icon v-if="day.signed" :icon="checkmarkCircleOutline" class="text-xl" />
<span v-else>{{ day.date.split('-')[1] }}</span>
</div>
<span class="text-xs text-[#666] font-medium">{{ day.day }}</span>
</div>
</div>
</div>
</section>
<!-- 签到奖励说明 --> <!-- 实际日期 -->
<section class="mb-8"> <div
<div class="card rounded-2xl shadow-lg p-5"> v-for="day in start.daysInMonth()"
<div class="flex items-center gap-2 mb-5"> :key="day"
<img src="@/assets/images/icon.png" class="size-7"> class="aspect-square flex-center"
<div class="text-xl font-bold text-[#1a1a1a]"> >
签到奖励 <div
class="w-full h-full rounded-lg flex flex-col items-center justify-center text-sm font-medium transition-all relative"
:class="isCheckedIn(day)
? 'bg-linear-to-br from-[#c41e3a] to-[#8b1a2e] text-white shadow-md'
: dayjs().date() === day
? 'bg-[#fff7f0] text-[#c41e3a] border-2 border-[#c41e3a]'
: 'bg-gray-50 text-[#666]'"
>
<span class="text-base">{{ day }}</span>
</div>
</div> </div>
</div> </div>
<div class="space-y-3"> <!-- 图例说明 -->
<div class="flex items-center justify-between p-3 bg-gradient-to-r from-[#fff7e6] to-transparent rounded-xl"> <div class="flex items-center justify-center gap-4 mt-4 pt-4 border-t border-gray-100">
<div class="flex items-center gap-3"> <div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full bg-[#faad14]" /> <div class="w-4 h-4 rounded bg-linear-to-br from-[#c41e3a] to-[#8b1a2e]" />
<span class="text-sm text-[#333] font-medium">连续签到7天</span> <span class="text-xs text-[#666]">已签到</span>
</div> </div>
<span class="text-sm text-[#c41e3a] font-bold">+10积分</span> <div class="flex items-center gap-2">
<div class="w-4 h-4 rounded bg-[#fff7f0] border-2 border-[#c41e3a]" />
<span class="text-xs text-[#666]">今天</span>
</div> </div>
<div class="flex items-center justify-between p-3 bg-gradient-to-r from-[#f6ffed] to-transparent rounded-xl"> <div class="flex items-center gap-2">
<div class="flex items-center gap-3"> <div class="w-4 h-4 rounded bg-gray-50" />
<div class="w-2 h-2 rounded-full bg-[#52c41a]" /> <span class="text-xs text-[#666]">未签到</span>
<span class="text-sm text-[#333] font-medium">连续签到30天</span>
</div>
<span class="text-sm text-[#c41e3a] font-bold">+50积分</span>
</div>
<div class="flex items-center justify-between p-3 bg-gradient-to-r from-[#fff1f0] to-transparent rounded-xl">
<div class="flex items-center gap-3">
<div class="w-2 h-2 rounded-full bg-[#c41e3a]" />
<span class="text-sm text-[#333] font-medium">连续签到60天</span>
</div>
<span class="text-sm text-[#c41e3a] font-bold">+100积分</span>
</div> </div>
</div> </div>
</div> </div>
@@ -192,14 +188,7 @@ async function handleSignup() {
.card { .card {
background: linear-gradient(180deg, #ffeef1, #ffffff 15%); background: linear-gradient(180deg, #ffeef1, #ffffff 15%);
} }
ion-datetime {
.signup-btn::part(native) { --background: transparent;
font-weight: 700;
}
.signup-btn:disabled {
--background: rgba(255, 255, 255, 0.5);
--color: rgba(255, 255, 255, 0.8);
opacity: 0.6;
} }
</style> </style>

View File

@@ -1,7 +1,7 @@
import { calendarOutline, chatbubblesOutline, peopleOutline, rocketOutline } from "ionicons/icons"; import { calendarOutline, chatbubblesOutline, peopleOutline, rocketOutline } from "ionicons/icons";
export const actions = [ export const actions = [
{ id: "signup", name: "签到", icon: calendarOutline, color: "#c32120" }, { id: "check_in", name: "签到", icon: calendarOutline, color: "#c32120" },
{ id: "team", name: "团队中心", icon: peopleOutline, color: "#c32120" }, { id: "team", name: "团队中心", icon: peopleOutline, color: "#c32120" },
{ id: "invite", name: "邀请好友", icon: rocketOutline, color: "#c32120" }, { id: "invite", name: "邀请好友", icon: rocketOutline, color: "#c32120" },
{ id: "support", name: "在线客服", icon: chatbubblesOutline, color: "#c32120" }, { id: "support", name: "在线客服", icon: chatbubblesOutline, color: "#c32120" },

View File

@@ -37,8 +37,8 @@ async function handleInfinite(event: InfiniteScrollCustomEvent) {
} }
function handleQuickAction(action: Action) { function handleQuickAction(action: Action) {
switch (action.id) { switch (action.id) {
case "signup": case "check_in":
router.push("/signup"); router.push("/check_in");
break; break;
case "team": case "team":
console.log("团队中心"); console.log("团队中心");