feat: 添加通知功能,集成模拟数据并更新通知视图
This commit is contained in:
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
@@ -161,3 +161,5 @@ Capacitor 配置本地开发服务器地址为 http://localhost:5173
|
|||||||
国际化支持中文和英文,配置文件在 src/locales/ 目前多语言只需要支持中英文,其他语言先不要更改
|
国际化支持中文和英文,配置文件在 src/locales/ 目前多语言只需要支持中英文,其他语言先不要更改
|
||||||
样式使用 TailwindCSS 4.x + Ionic CSS Variables 混合模式
|
样式使用 TailwindCSS 4.x + Ionic CSS Variables 混合模式
|
||||||
函数风格使用function关键字定义,一般不要使用箭头函数
|
函数风格使用function关键字定义,一般不要使用箭头函数
|
||||||
|
如果有任何不明确的地方,随时提问以获取更多信息
|
||||||
|
如果代码有错误,请先执行 pnpm run lint:fix 来自动修复代码问题
|
||||||
|
|||||||
10
components.d.ts
vendored
10
components.d.ts
vendored
@@ -18,6 +18,7 @@ declare module 'vue' {
|
|||||||
IonApp: typeof import('@ionic/vue')['IonApp']
|
IonApp: typeof import('@ionic/vue')['IonApp']
|
||||||
IonAvatar: typeof import('@ionic/vue')['IonAvatar']
|
IonAvatar: typeof import('@ionic/vue')['IonAvatar']
|
||||||
IonBackButton: typeof import('@ionic/vue')['IonBackButton']
|
IonBackButton: typeof import('@ionic/vue')['IonBackButton']
|
||||||
|
IonBadge: typeof import('@ionic/vue')['IonBadge']
|
||||||
IonButton: typeof import('@ionic/vue')['IonButton']
|
IonButton: typeof import('@ionic/vue')['IonButton']
|
||||||
IonButtons: typeof import('@ionic/vue')['IonButtons']
|
IonButtons: typeof import('@ionic/vue')['IonButtons']
|
||||||
IonCol: typeof import('@ionic/vue')['IonCol']
|
IonCol: typeof import('@ionic/vue')['IonCol']
|
||||||
@@ -32,6 +33,9 @@ declare module 'vue' {
|
|||||||
IonInfiniteScrollContent: typeof import('@ionic/vue')['IonInfiniteScrollContent']
|
IonInfiniteScrollContent: typeof import('@ionic/vue')['IonInfiniteScrollContent']
|
||||||
IonInput: typeof import('@ionic/vue')['IonInput']
|
IonInput: typeof import('@ionic/vue')['IonInput']
|
||||||
IonItem: typeof import('@ionic/vue')['IonItem']
|
IonItem: typeof import('@ionic/vue')['IonItem']
|
||||||
|
IonItemOption: typeof import('@ionic/vue')['IonItemOption']
|
||||||
|
IonItemOptions: typeof import('@ionic/vue')['IonItemOptions']
|
||||||
|
IonItemSliding: typeof import('@ionic/vue')['IonItemSliding']
|
||||||
IonLabel: typeof import('@ionic/vue')['IonLabel']
|
IonLabel: typeof import('@ionic/vue')['IonLabel']
|
||||||
IonList: typeof import('@ionic/vue')['IonList']
|
IonList: typeof import('@ionic/vue')['IonList']
|
||||||
IonListHeader: typeof import('@ionic/vue')['IonListHeader']
|
IonListHeader: typeof import('@ionic/vue')['IonListHeader']
|
||||||
@@ -49,6 +53,7 @@ declare module 'vue' {
|
|||||||
IonSegmentButton: typeof import('@ionic/vue')['IonSegmentButton']
|
IonSegmentButton: typeof import('@ionic/vue')['IonSegmentButton']
|
||||||
IonSelect: typeof import('@ionic/vue')['IonSelect']
|
IonSelect: typeof import('@ionic/vue')['IonSelect']
|
||||||
IonSelectOption: typeof import('@ionic/vue')['IonSelectOption']
|
IonSelectOption: typeof import('@ionic/vue')['IonSelectOption']
|
||||||
|
IonSpinner: typeof import('@ionic/vue')['IonSpinner']
|
||||||
IonTabBar: typeof import('@ionic/vue')['IonTabBar']
|
IonTabBar: typeof import('@ionic/vue')['IonTabBar']
|
||||||
IonTabButton: typeof import('@ionic/vue')['IonTabButton']
|
IonTabButton: typeof import('@ionic/vue')['IonTabButton']
|
||||||
IonTabs: typeof import('@ionic/vue')['IonTabs']
|
IonTabs: typeof import('@ionic/vue')['IonTabs']
|
||||||
@@ -71,6 +76,7 @@ declare global {
|
|||||||
const IonApp: typeof import('@ionic/vue')['IonApp']
|
const IonApp: typeof import('@ionic/vue')['IonApp']
|
||||||
const IonAvatar: typeof import('@ionic/vue')['IonAvatar']
|
const IonAvatar: typeof import('@ionic/vue')['IonAvatar']
|
||||||
const IonBackButton: typeof import('@ionic/vue')['IonBackButton']
|
const IonBackButton: typeof import('@ionic/vue')['IonBackButton']
|
||||||
|
const IonBadge: typeof import('@ionic/vue')['IonBadge']
|
||||||
const IonButton: typeof import('@ionic/vue')['IonButton']
|
const IonButton: typeof import('@ionic/vue')['IonButton']
|
||||||
const IonButtons: typeof import('@ionic/vue')['IonButtons']
|
const IonButtons: typeof import('@ionic/vue')['IonButtons']
|
||||||
const IonCol: typeof import('@ionic/vue')['IonCol']
|
const IonCol: typeof import('@ionic/vue')['IonCol']
|
||||||
@@ -85,6 +91,9 @@ declare global {
|
|||||||
const IonInfiniteScrollContent: typeof import('@ionic/vue')['IonInfiniteScrollContent']
|
const IonInfiniteScrollContent: typeof import('@ionic/vue')['IonInfiniteScrollContent']
|
||||||
const IonInput: typeof import('@ionic/vue')['IonInput']
|
const IonInput: typeof import('@ionic/vue')['IonInput']
|
||||||
const IonItem: typeof import('@ionic/vue')['IonItem']
|
const IonItem: typeof import('@ionic/vue')['IonItem']
|
||||||
|
const IonItemOption: typeof import('@ionic/vue')['IonItemOption']
|
||||||
|
const IonItemOptions: typeof import('@ionic/vue')['IonItemOptions']
|
||||||
|
const IonItemSliding: typeof import('@ionic/vue')['IonItemSliding']
|
||||||
const IonLabel: typeof import('@ionic/vue')['IonLabel']
|
const IonLabel: typeof import('@ionic/vue')['IonLabel']
|
||||||
const IonList: typeof import('@ionic/vue')['IonList']
|
const IonList: typeof import('@ionic/vue')['IonList']
|
||||||
const IonListHeader: typeof import('@ionic/vue')['IonListHeader']
|
const IonListHeader: typeof import('@ionic/vue')['IonListHeader']
|
||||||
@@ -102,6 +111,7 @@ declare global {
|
|||||||
const IonSegmentButton: typeof import('@ionic/vue')['IonSegmentButton']
|
const IonSegmentButton: typeof import('@ionic/vue')['IonSegmentButton']
|
||||||
const IonSelect: typeof import('@ionic/vue')['IonSelect']
|
const IonSelect: typeof import('@ionic/vue')['IonSelect']
|
||||||
const IonSelectOption: typeof import('@ionic/vue')['IonSelectOption']
|
const IonSelectOption: typeof import('@ionic/vue')['IonSelectOption']
|
||||||
|
const IonSpinner: typeof import('@ionic/vue')['IonSpinner']
|
||||||
const IonTabBar: typeof import('@ionic/vue')['IonTabBar']
|
const IonTabBar: typeof import('@ionic/vue')['IonTabBar']
|
||||||
const IonTabButton: typeof import('@ionic/vue')['IonTabButton']
|
const IonTabButton: typeof import('@ionic/vue')['IonTabButton']
|
||||||
const IonTabs: typeof import('@ionic/vue')['IonTabs']
|
const IonTabs: typeof import('@ionic/vue')['IonTabs']
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { WatchSource } from "vue";
|
|||||||
import { treaty } from "@elysiajs/eden";
|
import { treaty } from "@elysiajs/eden";
|
||||||
import { toastController } from "@ionic/vue";
|
import { toastController } from "@ionic/vue";
|
||||||
import { i18n } from "@/locales";
|
import { i18n } from "@/locales";
|
||||||
|
import { useMocks } from "@/mocks";
|
||||||
|
|
||||||
const client = treaty<App>(window.location.origin, {
|
const client = treaty<App>(window.location.origin, {
|
||||||
fetch: {
|
fetch: {
|
||||||
@@ -121,4 +122,17 @@ export function safeClient<T, E>(
|
|||||||
return promise as SafeClientReturn<T, E> & Promise<SafeClientReturn<T, E>>;
|
return promise as SafeClientReturn<T, E> & Promise<SafeClientReturn<T, E>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mockClient<T = any, E = any>(key: string): SafeClientReturn<T, E> & Promise<SafeClientReturn<T, E>> {
|
||||||
|
const requestPromise = async () => {
|
||||||
|
const mocks = useMocks();
|
||||||
|
const mockFn = mocks.get(key);
|
||||||
|
if (!mockFn) {
|
||||||
|
throw new Error(`Mock with key "${key}" not found.`);
|
||||||
|
}
|
||||||
|
const data = await mockFn();
|
||||||
|
return { data: data as T, error: null as E };
|
||||||
|
};
|
||||||
|
return safeClient<T, E>(requestPromise, { immediate: true });
|
||||||
|
}
|
||||||
|
|
||||||
export { client };
|
export { client };
|
||||||
|
|||||||
10
src/main.ts
10
src/main.ts
@@ -5,7 +5,7 @@ import uiComponents from "@/ui";
|
|||||||
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 { setupMocks } from "./mocks";
|
||||||
import { router } from "./router";
|
import { router } from "./router";
|
||||||
|
|
||||||
/* Core CSS required for Ionic components to work properly */
|
/* Core CSS required for Ionic components to work properly */
|
||||||
@@ -41,7 +41,8 @@ import "./theme/ionic.css";
|
|||||||
|
|
||||||
useTheme();
|
useTheme();
|
||||||
|
|
||||||
authClient.getSession().then((session) => {
|
function initApp() {
|
||||||
|
authClient.getSession().then((session) => {
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
const userStore = useUserStore(pinia);
|
const userStore = useUserStore(pinia);
|
||||||
userStore.setToken(session.data?.session.token || "");
|
userStore.setToken(session.data?.session.token || "");
|
||||||
@@ -63,4 +64,9 @@ authClient.getSession().then((session) => {
|
|||||||
router.isReady().then(() => {
|
router.isReady().then(() => {
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupMocks().then(() => {
|
||||||
|
initApp();
|
||||||
});
|
});
|
||||||
|
|||||||
1
src/mocks/data/index.ts
Normal file
1
src/mocks/data/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import "./notify";
|
||||||
80
src/mocks/data/notify.ts
Normal file
80
src/mocks/data/notify.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { useMocks } from "..";
|
||||||
|
|
||||||
|
const mocks = useMocks();
|
||||||
|
|
||||||
|
mocks.register("notify.list", () => {
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "系统通知",
|
||||||
|
content: "您的账户安全等级已提升至 LV2,现在可以享受更高的交易限额。",
|
||||||
|
date: new Date(now.getTime() - 1000 * 60 * 30).toISOString(), // 30分钟前
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: "交易通知",
|
||||||
|
content: "您的买入订单已成功成交,交易对: BTC/USDT,数量: 0.005 BTC",
|
||||||
|
date: new Date(now.getTime() - 1000 * 60 * 60 * 2).toISOString(), // 2小时前
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: "活动通知",
|
||||||
|
content: "新春特惠活动火热进行中!邀请好友注册交易即可获得最高 100 USDT 奖励,活动截止时间:2025-02-28",
|
||||||
|
date: new Date(now.getTime() - 1000 * 60 * 60 * 5).toISOString(), // 5小时前
|
||||||
|
read: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: "安全提醒",
|
||||||
|
content: "检测到您的账户在新设备登录,登录地点:北京,如非本人操作,请立即修改密码并联系客服。",
|
||||||
|
date: new Date(now.getTime() - 1000 * 60 * 60 * 24).toISOString(), // 1天前
|
||||||
|
read: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
title: "交易通知",
|
||||||
|
content: "您的卖出订单已成功成交,交易对: ETH/USDT,数量: 0.1 ETH,成交价格: 2850 USDT",
|
||||||
|
date: new Date(now.getTime() - 1000 * 60 * 60 * 24 * 2).toISOString(), // 2天前
|
||||||
|
read: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
title: "系统通知",
|
||||||
|
content: "平台将于北京时间 2025-01-05 02:00 至 04:00 进行系统升级维护,期间部分功能可能暂时无法使用。",
|
||||||
|
date: new Date(now.getTime() - 1000 * 60 * 60 * 24 * 3).toISOString(), // 3天前
|
||||||
|
read: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
title: "活动通知",
|
||||||
|
content: "恭喜您获得新手礼包!包含 10 USDT 体验金和 7 天 VIP 会员权益,请及时领取。",
|
||||||
|
date: new Date(now.getTime() - 1000 * 60 * 60 * 24 * 5).toISOString(), // 5天前
|
||||||
|
read: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
title: "安全提醒",
|
||||||
|
content: "您的提现申请已通过审核,预计将在 24 小时内到账,请注意查收。",
|
||||||
|
date: new Date(now.getTime() - 1000 * 60 * 60 * 24 * 7).toISOString(), // 7天前
|
||||||
|
read: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
title: "交易通知",
|
||||||
|
content: "市场波动较大,您持仓的 BTC 当前盈利已达 15%,是否考虑止盈?",
|
||||||
|
date: new Date(now.getTime() - 1000 * 60 * 60 * 24 * 10).toISOString(), // 10天前
|
||||||
|
read: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
title: "系统通知",
|
||||||
|
content: "为了更好地保护您的资产安全,建议您开启双重验证(2FA)功能。",
|
||||||
|
date: new Date(now.getTime() - 1000 * 60 * 60 * 24 * 15).toISOString(), // 15天前
|
||||||
|
read: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
41
src/mocks/index.ts
Normal file
41
src/mocks/index.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import type { Awaitable } from "@vueuse/core";
|
||||||
|
|
||||||
|
export class Mock {
|
||||||
|
private mocks: Map<string, () => Awaitable<any>>;
|
||||||
|
constructor() {
|
||||||
|
this.mocks = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
get keys(): string[] {
|
||||||
|
return Array.from(this.mocks.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
register(key: string, data: () => Awaitable<any>) {
|
||||||
|
this.mocks.set(key, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
unregister(key: string) {
|
||||||
|
this.mocks.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key: string): (() => Awaitable<any>) | undefined {
|
||||||
|
return this.mocks.get(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMocks() {
|
||||||
|
const singletonKey = "__mocks_singleton__";
|
||||||
|
if (!(globalThis as any)[singletonKey]) {
|
||||||
|
(globalThis as any)[singletonKey] = new Mock();
|
||||||
|
}
|
||||||
|
return (globalThis as any)[singletonKey] as Mock;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupMocks() {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
import("./data");
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import IcBaselineNotificationsNone from "~icons/ic/baseline-notifications-none";
|
||||||
import IconParkOutlineClearFormat from "~icons/icon-park-outline/clear-format";
|
import IconParkOutlineClearFormat from "~icons/icon-park-outline/clear-format";
|
||||||
import MaterialSymbolsAndroidContacts from "~icons/material-symbols/android-contacts";
|
import MaterialSymbolsAndroidContacts from "~icons/material-symbols/android-contacts";
|
||||||
|
import { mockClient } from "@/api";
|
||||||
|
|
||||||
|
const { data } = mockClient("notify.list");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -18,6 +22,27 @@ import MaterialSymbolsAndroidContacts from "~icons/material-symbols/android-cont
|
|||||||
</IonHeader>
|
</IonHeader>
|
||||||
<IonContent :fullscreen="true">
|
<IonContent :fullscreen="true">
|
||||||
<ion-searchbar placeholder="Search" />
|
<ion-searchbar placeholder="Search" />
|
||||||
|
|
||||||
|
<ion-list lines="none">
|
||||||
|
<ion-item v-for="item in data" :key="item.id" class="py-3">
|
||||||
|
<div slot="start" class="bg-text-900 p-2.5 rounded-full">
|
||||||
|
<IcBaselineNotificationsNone class="text-2xl text-(--ion-color-success-tint)" />
|
||||||
|
</div>
|
||||||
|
<div class="pl-3 w-full">
|
||||||
|
<div class="flex items-center justify-between mb-1">
|
||||||
|
<div class="text-md font-semibold">
|
||||||
|
{{ item.title }}
|
||||||
|
</div>
|
||||||
|
<ion-note slot="end" class="text-xs">
|
||||||
|
{{ useDateFormat(item.date, 'MM/DD HH:mm') }}
|
||||||
|
</ion-note>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs overflow-hidden text-ellipsis">
|
||||||
|
{{ item.content }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
</IonContent>
|
</IonContent>
|
||||||
</IonPage>
|
</IonPage>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user