feat: add Empty component and language management

- Introduced a new Empty component for displaying no data states.
- Added language management functionality with support for loading saved languages.
- Updated App.vue to load saved language on mount.
- Modified components.d.ts to include new components and global variables.
- Updated pnpm-lock.yaml and pnpm-workspace.yaml to use the latest version of @capp/eden.
- Refactored home and service views to utilize the new data fetching logic with infinite scroll.
- Removed unused images and added new service banner.
- Enhanced signup functionality to include toast notifications on successful sign-in.
This commit is contained in:
2026-01-18 01:00:03 +07:00
parent 51719cd229
commit 4dd2a49c70
18 changed files with 542 additions and 208 deletions

View File

@@ -3,6 +3,11 @@ import { App as CapacitorApp } from "@capacitor/app";
const userStore = useUserStore();
const { isAuthenticated } = storeToRefs(userStore);
const { loadSavedLanguage } = useLanguage();
onBeforeMount(() => {
loadSavedLanguage();
});
onMounted(() => {
if (!isAuthenticated.value)

BIN
src/assets/images/empty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 481 KiB

View File

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 173 KiB

25
src/components/empty.vue Normal file
View File

@@ -0,0 +1,25 @@
<script lang='ts' setup>
import empty from "@/assets/images/empty.png?url";
withDefaults(defineProps<{
image?: string;
title?: string;
}>(), {
image: empty,
title: "暂无数据",
});
</script>
<template>
<div class="flex flex-col-center space-y-2 my-4">
<slot name="icon">
<img :src="empty" class="w-22 h-22 object-contain">
</slot>
<div class="text-sm text-text-400">
{{ title }}
</div>
<slot name="extra" />
</div>
</template>
<style lang='css' scoped></style>

View File

@@ -0,0 +1,77 @@
import type { Locale } from "vue-i18n";
import { client, safeClient } from "@/api";
export interface Language {
code: Locale;
name: string;
nativeName: string;
}
/**
* 语言管理组合式函数
*/
export function useLanguage() {
const { locale, availableLocales, mergeLocaleMessage } = useI18n();
const language = useStorage<Locale>("app-language", locale.value);
// 可用的语言列表
const languages: Language[] = [
{
code: "zh-CN",
name: "Chinese (Simplified)",
nativeName: "简体中文",
},
];
// 当前语言
const currentLanguage = computed(() => {
return languages.find(lang => lang.code === locale.value) || languages[0];
});
/**
* 切换语言
*/
function setLanguage(langCode: Locale) {
locale.value = langCode;
language.value = langCode;
loadRemoteLanguage();
}
function loadRemoteLanguage() {
const storageKey = `remote-lang-${language.value}`;
const remoteLangJson = useStorageCache<string>(storageKey, "", { ttl: 24 * 60 * 60 }); // 缓存 1 天
if (remoteLangJson.value) {
try {
const messages = JSON.parse(remoteLangJson.value);
mergeLocaleMessage(locale.value, messages);
return;
}
catch (e) {
console.error("Failed to parse remote language JSON:", e);
}
}
safeClient(client.api.error_messages({ lang: language.value }).get()).then((res) => {
clearExpiredCaches([storageKey]);
remoteLangJson.value = JSON.stringify(res.data.value);
mergeLocaleMessage(locale.value, res.data.value);
});
}
/**
* 从 localStorage 加载保存的语言
*/
function loadSavedLanguage() {
if (language.value && availableLocales.includes(language.value)) {
locale.value = language.value;
loadRemoteLanguage();
}
}
return {
languages,
currentLanguage,
locale,
setLanguage,
loadSavedLanguage,
};
}

View File

@@ -0,0 +1,14 @@
import type { MaybeRef } from "vue";
import cloneDeepWith from "lodash-es/cloneDeepWith";
import { isRef, ref } from "vue";
export function useResetRef<T>(value: MaybeRef<T>) {
const _valueDefine = cloneDeepWith(value as any);
const _value = isRef(value) ? value : ref(value);
function reset(value?: T) {
_value.value = value ? cloneDeepWith(value) : cloneDeepWith(_valueDefine);
}
return [_value, reset] as const;
}

View File

@@ -0,0 +1,248 @@
import type { RemovableRef, StorageLike } from "@vueuse/core";
interface CacheData<T> {
value: T;
expireTime?: number; // 过期时间戳(毫秒)
}
interface UseStorageCacheOptions {
/**
* 缓存过期时间(秒)
* @default undefined 永不过期
*/
ttl?: number;
/**
* 存储介质
* @default localStorage
*/
storage?: StorageLike;
/**
* 是否在过期后自动删除
* @default true
*/
autoRemoveOnExpire?: boolean;
}
/**
* 使用支持过期时间的 Storage
* @param key 存储键
* @param initialValue 初始值
* @param options 配置项
* @returns 响应式存储引用
*
* @example
* ```ts
* // 创建一个 5 分钟后过期的缓存
* const token = useStorageCache('user-token', '', { ttl: 300 })
*
* // 设置值
* token.value = 'new-token'
*
* // 检查是否过期
* if (isExpired(token)) {
* console.log('Token 已过期')
* }
*
* // 刷新过期时间
* refreshExpire(token, 600) // 延长到 10 分钟
* ```
*/
export function useStorageCache<T>(
key: string,
initialValue: T,
options: UseStorageCacheOptions = {},
): RemovableRef<T> {
const {
ttl,
storage = localStorage,
autoRemoveOnExpire = true,
} = options;
// 尝试从 storage 读取缓存数据
const rawData = storage.getItem(key);
let cachedData: CacheData<T> | null = null;
if (rawData) {
try {
cachedData = JSON.parse(rawData) as CacheData<T>;
// 检查是否过期
if (cachedData.expireTime && Date.now() > cachedData.expireTime) {
// 已过期
if (autoRemoveOnExpire) {
storage.removeItem(key);
}
cachedData = null;
}
}
catch (error) {
console.error(`解析缓存数据失败 [${key}]:`, error);
cachedData = null;
}
}
// 计算初始值
const defaultValue = cachedData ? cachedData.value : initialValue;
// 使用 useStorage 创建响应式引用
const storageRef = useStorage<T>(
key,
defaultValue,
storage,
{
serializer: {
read: (raw: string) => {
try {
const data = JSON.parse(raw) as CacheData<T>;
// 再次检查过期时间(防止并发问题)
if (data.expireTime && Date.now() > data.expireTime) {
if (autoRemoveOnExpire) {
storage.removeItem(key);
}
return initialValue;
}
return data.value;
}
catch {
return initialValue;
}
},
write: (value: T) => {
const cacheData: CacheData<T> = {
value,
};
// 如果设置了 ttl计算过期时间
if (ttl && ttl > 0) {
cacheData.expireTime = Date.now() + ttl * 1000;
}
return JSON.stringify(cacheData);
},
},
},
);
return storageRef;
}
/**
* 检查缓存是否过期
* @param key 存储键或存储引用
* @param storage 存储介质
* @returns 是否过期
*/
export function isCacheExpired(
key: string | RemovableRef<any>,
storage: StorageLike = localStorage,
): boolean {
const cacheKey = typeof key === "string" ? key : key.value;
try {
const rawData = storage.getItem(cacheKey);
if (!rawData) {
return true;
}
const data = JSON.parse(rawData) as CacheData<any>;
if (!data.expireTime) {
return false; // 没有设置过期时间,永不过期
}
return Date.now() > data.expireTime;
}
catch {
return true;
}
}
/**
* 刷新缓存过期时间
* @param key 存储键
* @param ttl 新的过期时间(秒)
* @param storage 存储介质
*/
export function refreshCacheExpire(
key: string,
ttl: number,
storage: StorageLike = localStorage,
): void {
try {
const rawData = storage.getItem(key);
if (!rawData) {
return;
}
const data = JSON.parse(rawData) as CacheData<any>;
data.expireTime = Date.now() + ttl * 1000;
storage.setItem(key, JSON.stringify(data));
}
catch (error) {
console.error(`刷新缓存过期时间失败 [${key}]:`, error);
}
}
/**
* 获取缓存剩余有效时间(秒)
* @param key 存储键
* @param storage 存储介质
* @returns 剩余时间(秒),如果已过期返回 0如果永不过期返回 Infinity
*/
export function getCacheRemainingTime(
key: string,
storage: StorageLike = localStorage,
): number {
try {
const rawData = storage.getItem(key);
if (!rawData) {
return 0;
}
const data = JSON.parse(rawData) as CacheData<any>;
if (!data.expireTime) {
return Infinity; // 永不过期
}
const remaining = Math.max(0, data.expireTime - Date.now());
return Math.floor(remaining / 1000);
}
catch {
return 0;
}
}
/**
* 清除指定键的过期缓存
* @param keys 要检查的键列表
* @param storage 存储介质
* @returns 被删除的键数量
*/
export function clearExpiredCaches(
keys: string[],
storage: StorageLike = localStorage,
): number {
let removedCount = 0;
keys.forEach((key) => {
try {
const rawData = storage.getItem(key);
if (!rawData) {
return;
}
const data = JSON.parse(rawData) as CacheData<any>;
// 检查是否过期
if (data.expireTime && Date.now() > data.expireTime) {
storage.removeItem(key);
removedCount++;
}
}
catch {
// 忽略解析错误
}
});
return removedCount;
}

View File

@@ -1,4 +1,4 @@
/* :root {
:root {
--ion-color-primary: #c31d39;
--ion-color-primary-rgb: 195,29,57;
--ion-color-primary-contrast: #ffffff;
@@ -62,8 +62,8 @@
--ion-color-dark-shade: #292929;
--ion-color-dark-tint: #444444;
} */
}
/*
:root {
--ion-color-primary: #2065c3;
--ion-color-primary-rgb: 32,101,195;
@@ -128,7 +128,7 @@
--ion-color-dark-shade: #292929;
--ion-color-dark-tint: #444444;
}
} */
.ion-toolbar {
--background: var(--ion-color-primary-contrast);

10
src/views/home/index.ts Normal file
View File

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

View File

@@ -1,149 +1,71 @@
<script lang='ts' setup>
import {
calendarOutline,
chatbubblesOutline,
chevronForwardOutline,
eyeOutline,
megaphoneOutline,
newspaperOutline,
peopleOutline,
rocketOutline,
timeOutline,
} from "ionicons/icons";
import { onMounted, onUnmounted, ref } from "vue";
import type { Treaty } from "@elysiajs/eden";
import type { InfiniteScrollCustomEvent } from "@ionic/vue";
import type { Action } from "./";
import type { TreatyQuery } from "@/api/types";
import { chevronForwardOutline, eyeOutline, timeOutline } from "ionicons/icons";
import { client, safeClient } from "@/api";
import { actions } from "./";
// 公告数据
const announcements = ref([
{ id: 1, title: "关于深化改革的重要通知", time: "2026-01-16" },
{ id: 2, title: "平台升级维护公告", time: "2026-01-15" },
{ id: 3, title: "新年贺词:砥砺前行,共创辉煌", time: "2026-01-01" },
]);
// 新闻数据(模拟数据)
const newsList = ref([
{
id: 1,
title: "深化改革进入新阶段",
subtitle: "全面推进现代化建设,开创新局面",
time: "2026-01-16 10:30",
views: 1520,
image: "https://picsum.photos/seed/news1/400/250",
},
{
id: 2,
title: "团队协作再创佳绩",
subtitle: "凝心聚力,共筑梦想,携手共进新时代",
time: "2026-01-15 16:20",
views: 2340,
image: "https://picsum.photos/seed/news2/400/250",
},
{
id: 3,
title: "战略布局取得重大突破",
subtitle: "科学谋划,精准施策,推动高质量发展",
time: "2026-01-14 09:15",
views: 1890,
image: "https://picsum.photos/seed/news3/400/250",
},
{
id: 4,
title: "深化改革进入新阶段",
subtitle: "全面推进现代化建设,开创新局面",
time: "2026-01-16 10:30",
views: 1520,
image: "https://picsum.photos/seed/news1/400/250",
},
{
id: 5,
title: "团队协作再创佳绩",
subtitle: "凝心聚力,共筑梦想,携手共进新时代",
time: "2026-01-15 16:20",
views: 2340,
image: "https://picsum.photos/seed/news2/400/250",
},
]);
type NewsItem = Treaty.Data<typeof client.api.news.get>["data"][number];
type NewsQuery = TreatyQuery<typeof client.api.news.get>;
const router = useRouter();
// 快捷入口
const quickActions = ref([
{ id: 1, name: "签到", icon: calendarOutline, color: "#2373c3" },
{ id: 2, name: "团队中心", icon: peopleOutline, color: "#2373c3" },
{ id: 3, name: "邀请好友", icon: rocketOutline, color: "#2373c3" },
{ id: 4, name: "在线客服", icon: chatbubblesOutline, color: "#2373c3" },
]);
const [query] = useResetRef<NewsQuery>({
offset: 0,
limit: 10,
});
const data = ref<NewsItem[]>([]);
const isFinished = ref(false);
function handleQuickAction(action: any) {
async function fetchNews() {
const { data: responseData } = await safeClient(client.api.news.get({ query: { ...query.value } }));
data.value.push(...(responseData.value?.data || []));
isFinished.value = responseData.value?.pagination.hasNextPage === false;
}
async function handleInfinite(event: InfiniteScrollCustomEvent) {
if (isFinished.value) {
event.target.complete();
event.target.disabled = true;
return;
}
query.value.offset! += query.value.limit!;
await fetchNews();
setTimeout(() => {
event.target.complete();
}, 500);
}
function handleQuickAction(action: Action) {
switch (action.id) {
case 1:
case "signup":
router.push("/signup");
break;
case 2:
case "team":
console.log("团队中心");
break;
case 3:
case "invite":
router.push("/invite");
break;
case "support":
window.open("https://chat.riwsan.com", "_blank");
break;
}
}
function handleAnnouncementClick(announcement: any) {
console.log("查看公告:", announcement.title);
// TODO: 跳转到公告详情
}
function handleNewsClick(news: any) {
function handleNewsClick(news: NewsItem) {
console.log("查看新闻:", news.title);
// TODO: 跳转到新闻详情
}
// 走马灯相关
const currentAnnouncementIndex = ref(0);
let announcementTimer: number | null = null;
function startAnnouncementCarousel() {
announcementTimer = setInterval(() => {
currentAnnouncementIndex.value = (currentAnnouncementIndex.value + 1) % announcements.value.length;
}, 3000); // 每3秒切换
}
function stopAnnouncementCarousel() {
if (announcementTimer) {
clearInterval(announcementTimer);
announcementTimer = null;
}
}
onMounted(() => {
startAnnouncementCarousel();
});
onUnmounted(() => {
stopAnnouncementCarousel();
});
</script>
<template>
<ion-page>
<ion-content :fullscreen="true" class="home-page">
<!-- <ion-header class="ion-no-border header">
<ion-toolbar class="ion-toolbar">
<div slot="start" class="flex items-center px-3 py-3">
<img src="@/assets/images/guohui.png" alt="国徽" class="inline-block h-10 mr-2">
<div class="font-semibold text-lg">
国务院深化改革战略推进委员会
</div>
</div>
</ion-toolbar>
</ion-header> -->
<img src="@/assets/images/home-banner.jpg" class="h-60 w-full object-cover" alt="首页横幅">
<div class="ion-padding-horizontal">
<!-- 快捷入口区域 -->
<section class="my-5 grid grid-cols-4 gap-4">
<!-- <div class="grid grid-cols-4 gap-4 bg-white/95 p-5 rounded-2xl shadow-lg"> -->
<div
v-for="action in quickActions"
v-for="action in actions"
:key="action.id"
class="flex flex-col items-center gap-2 cursor-pointer transition-transform active:scale-95"
@click="handleQuickAction(action)"
@@ -156,7 +78,6 @@ onUnmounted(() => {
</div>
<span class="text-xs text-[#333] font-medium text-center">{{ action.name }}</span>
</div>
<!-- </div> -->
</section>
<!-- 新闻列表区域 -->
@@ -168,41 +89,42 @@ onUnmounted(() => {
新闻动态
</div>
</div>
<ion-button fill="clear" size="small" class="text-sm text-white h-8">
<ion-button fill="clear" size="small" class="text-sm h-8" @click="router.push('/layout/service')">
更多
<ion-icon slot="end" :icon="chevronForwardOutline" />
</ion-button>
</div>
<div class="flex flex-col gap-4">
<empty v-if="data.length === 0" class="my-10" />
<div v-else class="flex flex-col gap-4">
<div
v-for="news in newsList"
:key="news.id"
v-for="item in data"
:key="item.id"
class="bg-white rounded-2xl overflow-hidden shadow-sm cursor-pointer transition-all active:translate-y-0.5 active:shadow-sm flex"
@click="handleNewsClick(news)"
@click="handleNewsClick(item)"
>
<div class="relative w-28 h-28 flex-shrink-0 overflow-hidden">
<img :src="news.image" :alt="news.title" class="w-full h-full object-cover">
<div class="news-badge absolute top-2 left-2 bg-linear-to-br from-[#78d0ff] to-[#1879aa] text-white px-2 py-0.5 rounded-lg text-xs font-semibold shadow-lg">
<div class="relative w-28 h-28 shrink-0 overflow-hidden">
<img v-if="item.thumbnailId" :src="item.thumbnail" :alt="item.title" class="w-full h-full object-cover">
<div class="news-badge absolute top-2 left-2 bg-linear-to-br from-[#c41e3a] to-[#8b1a2e] text-white px-2 py-0.5 rounded-lg text-xs font-semibold shadow-lg">
热点
</div>
</div>
<div class="flex-1 p-4 flex flex-col justify-between">
<div>
<div class="text-base font-bold text-[#1a1a1a] mb-1 leading-snug line-clamp-2">
{{ news.title }}
{{ item.title }}
</div>
<p class="text-sm text-[#666] leading-relaxed line-clamp-2">
{{ news.subtitle }}
{{ item.summary }}
</p>
</div>
<div class="flex items-center gap-4 text-xs text-[#999] mt-2">
<span class="flex items-center gap-1">
<ion-icon :icon="timeOutline" class="text-sm" />
{{ news.time }}
{{ item.createdAt }}
</span>
<span class="flex items-center gap-1">
<ion-icon :icon="eyeOutline" class="text-sm" />
{{ news.views }}
{{ item.viewCount }}
</span>
</div>
</div>
@@ -210,23 +132,15 @@ onUnmounted(() => {
</div>
</section>
</div>
<ion-infinite-scroll threshold="100px" disabled @ion-infinite="handleInfinite">
<ion-infinite-scroll-content loading-spinner="bubbles" loading-text="加载更多..." />
</ion-infinite-scroll>
</ion-content>
</ion-page>
</template>
<style lang='css' scoped>
.ion-toolbar {
/* --background: #c32120; */
/* --color: #fff; */
}
.home-pg {
background: linear-gradient(180deg, #c32120 0%, #c32120 55%, #fef5f1 100%);
}
/* .home-page {
--background: linear-gradient(180deg, #c41e3a 0%, #e8756d 15%, #f5d5c8 35%, #fef5f1 50%, #fef5f1 65%, #f5d5c8 85%);
} */
/* 中国风图案 */
.chinese-pattern {
position: absolute;

View File

@@ -1,48 +1,52 @@
<script lang='ts' setup>
import {
chevronForwardOutline,
eyeOutline,
newspaperOutline,
timeOutline,
} from "ionicons/icons";
import type { Treaty } from "@elysiajs/eden";
import type { InfiniteScrollCustomEvent } from "@ionic/vue";
import type { TreatyQuery } from "@/api/types";
import { eyeOutline, timeOutline } from "ionicons/icons";
import { client, safeClient } from "@/api";
const newsList = ref([
{
id: 1,
title: "深化改革进入新阶段",
subtitle: "全面推进现代化建设,开创新局面",
time: "2026-01-16 10:30",
views: 1520,
image: "https://picsum.photos/seed/news1/400/250",
},
{
id: 2,
title: "团队协作再创佳绩",
subtitle: "凝心聚力,共筑梦想,携手共进新时代",
time: "2026-01-15 16:20",
views: 2340,
image: "https://picsum.photos/seed/news2/400/250",
},
{
id: 3,
title: "战略布局取得重大突破",
subtitle: "科学谋划,精准施策,推动高质量发展",
time: "2026-01-14 09:15",
views: 1890,
image: "https://picsum.photos/seed/news3/400/250",
},
]);
type NewsItem = Treaty.Data<typeof client.api.news.get>["data"][number];
type NewsQuery = TreatyQuery<typeof client.api.news.get>;
const [query] = useResetRef<NewsQuery>({
offset: 0,
limit: 10,
});
const data = ref<NewsItem[]>([]);
const isFinished = ref(false);
async function fetchNews() {
const { data: responseData } = await safeClient(client.api.news.get({ query: { ...query.value } }));
data.value.push(...(responseData.value?.data || []));
isFinished.value = responseData.value?.pagination.hasNextPage === false;
}
async function handleInfinite(event: InfiniteScrollCustomEvent) {
if (isFinished.value) {
event.target.complete();
event.target.disabled = true;
return;
}
query.value.offset! += query.value.limit!;
await fetchNews();
setTimeout(() => {
event.target.complete();
}, 500);
}
function handleNewsClick(news: any) {
console.log("查看新闻:", news.title);
// TODO: 跳转到新闻详情
}
onMounted(() => {
fetchNews();
});
</script>
<template>
<ion-page>
<ion-content>
<img src="@/assets/images/service-banner1.jpg" class="h-50 w-full object-cover" alt="服务页横幅">
<img src="@/assets/images/service-banner.jpg" class="h-50 w-full object-cover" alt="服务页横幅">
<!-- 新闻列表区域 -->
<section class="mb-5 -mt-5 ion-padding-horizontal">
@@ -54,40 +58,45 @@ function handleNewsClick(news: any) {
</div>
</div>
</div>
<div class="flex flex-col gap-4">
<empty v-if="data.length === 0" class="my-10" />
<div v-else class="flex flex-col gap-4">
<div
v-for="news in newsList"
:key="news.id"
v-for="item in data"
:key="item.id"
class="bg-white rounded-2xl overflow-hidden shadow-sm cursor-pointer transition-all active:translate-y-0.5 active:shadow-sm"
@click="handleNewsClick(news)"
@click="handleNewsClick(item)"
>
<div class="relative w-full h-45 overflow-hidden">
<img :src="news.image" :alt="news.title" class="w-full h-full object-cover">
<img v-if="item.thumbnailId" :src="item.thumbnail" :alt="item.title" class="w-full h-full object-cover">
<div class="news-badge absolute top-3 left-3 bg-linear-to-br from-[#78d0ff] to-[#1879aa] text-white px-3 py-1 rounded-xl text-xs font-semibold shadow-lg">
热点
</div>
</div>
<div class="p-4">
<h4 class="text-base font-bold text-[#1a1a1a] mb-2 leading-snug">
{{ news.title }}
{{ item.title }}
</h4>
<p class="text-sm text-[#666] mb-3 leading-relaxed line-clamp-2">
{{ news.subtitle }}
{{ item.summary }}
</p>
<div class="flex items-center gap-4 text-xs text-[#999]">
<span class="flex items-center gap-1">
<ion-icon :icon="timeOutline" class="text-sm" />
{{ news.time }}
{{ item.createdAt }}
</span>
<span class="flex items-center gap-1">
<ion-icon :icon="eyeOutline" class="text-sm" />
{{ news.views }}
{{ item.viewCount }}
</span>
</div>
</div>
</div>
</div>
</section>
<ion-infinite-scroll threshold="100px" disabled @ion-infinite="handleInfinite">
<ion-infinite-scroll-content loading-spinner="bubbles" loading-text="加载更多..." />
</ion-infinite-scroll>
</ion-content>
</ion-page>
</template>

View File

@@ -1,11 +1,16 @@
<script lang='ts' setup>
import {
calendarOutline,
checkmarkCircleOutline,
flameOutline,
ribbonOutline,
trophyOutline,
} from "ionicons/icons";
import { toastController } from "@ionic/vue";
import dayjs from "dayjs";
import { checkmarkCircleOutline } from "ionicons/icons";
import { client, safeClient } from "@/api";
const [start, end] = [dayjs().startOf("week"), dayjs().endOf("week")];
const { data } = await safeClient(client.api.checkIns.get({
query: {
startDate: start.toISOString(),
endDate: end.toISOString(),
},
}));
// 签到信息
const signupInfo = ref({
@@ -25,9 +30,13 @@ const recentSignup = ref([
{ day: "周日", date: "01-19", signed: false },
]);
function handleSignup() {
console.log("立即签到");
// TODO: 实现签到功能
async function handleSignup() {
await safeClient(client.api.checkIns.post());
toastController.create({
message: "签到成功!",
duration: 2000,
color: "success",
}).then(toast => toast.present());
}
</script>