feat: 添加签到功能,更新相关路由和组件,集成日期选择器
This commit is contained in:
2
components.d.ts
vendored
2
components.d.ts
vendored
@@ -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
14
pnpm-lock.yaml
generated
@@ -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))
|
||||||
|
|
||||||
|
|||||||
@@ -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
4
src/dayjs/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
|
import "dayjs/locale/zh-cn";
|
||||||
|
|
||||||
|
dayjs.locale("zh-cn");
|
||||||
@@ -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 */
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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" },
|
||||||
|
|||||||
@@ -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("团队中心");
|
||||||
|
|||||||
Reference in New Issue
Block a user