Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
# dependencies
node_modules/

# 项目使用 bun 作为包管理器;package-lock.json 由 npm 生成,不入库
package-lock.json

# Expo
.expo/
dist/
Expand Down Expand Up @@ -43,5 +46,8 @@ app-example
/android
**/android/build/

# Gradle 本地构建缓存
.gradle/

# OTA
!assets/certificate.pem
146 changes: 39 additions & 107 deletions app/(tabs)/function.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Ionicons } from "@expo/vector-icons";
import { type Href, router } from "expo-router";
import { useState } from "react";
import { useEffect, useState } from "react";
import {
KeyboardAvoidingView,
Modal,
Expand All @@ -20,103 +20,19 @@ import { IS_DEV } from "@/constants/is-dev";
import { useColorScheme } from "@/hooks/use-color-scheme";
import { useHaptics } from "@/hooks/use-haptics";
import { useMarkRouteInteractive } from "@/hooks/use-mark-route-interactive";
import { type TKey, useT } from "@/lib/i18n";
import { getResolvedLang, useT } from "@/lib/i18n";
import type {
FunctionWebApp,
LocalizedString,
} from "@/services/function-apps";
import { useFunctionAppsStore } from "@/store/function-apps";
import { useScheduleStore } from "@/store/schedule";

type WebApp = {
icon: React.ComponentProps<typeof Ionicons>["name"];
labelKey: TKey;
color: string;
// 应用内页面:走 expo-router 类型安全路由(即时跳转,也是 iwut:// 深链接的目标)
route?: Href;
// 外部网页:在内置浏览器打开
uri?: string;
lan?: boolean;
};

type Section = {
titleKey: TKey;
items: WebApp[];
};

const SECTIONS: Section[] = [
{
titleKey: "fn.section.study",
items: [
{
icon: "bar-chart-outline",
labelKey: "fn.app.grade",
color: "#8b5cf6",
route: "/grade",
},
{
icon: "calendar-clear-outline",
labelKey: "fn.app.exam",
color: "#3b82f6",
route: "/exam",
},
{
icon: "book-outline",
labelKey: "fn.app.classroom",
color: "#0d9488",
uri: "https://classroom-iwut.tokenteam.net",
lan: false,
},
{
icon: "easel-outline",
labelKey: "fn.app.icSpace",
color: "#06b6d4",
uri: "https://zhlgd.whut.edu.cn/tpass/login?service=https%3A%2F%2Fzw.whut.edu.cn%2Frem%2Fstatic%2Fsso%2FwebOAuthRed",
lan: true,
},
{
icon: "library-outline",
labelKey: "fn.app.library",
color: "#3b82f6",
uri: "https://library-info-iwut.tokenteam.net",
lan: true,
},
],
},
{
titleKey: "fn.section.life",
items: [
{
icon: "card-outline",
labelKey: "fn.app.card",
color: "#10b981",
uri: "https://cardcare-iwut.tokenteam.net",
lan: false,
},
{
icon: "flash-outline",
labelKey: "fn.app.elec",
color: "#eab308",
uri: "https://zhlgd.whut.edu.cn/tpass/login?service=http://nyyzf.whut.edu.cn/MobileWebOnlineHall/#/",
lan: false,
},
{
icon: "wifi-outline",
labelKey: "fn.app.netPay",
color: "#2563eb",
uri: "https://zhlgd.whut.edu.cn/tpass/login?service=http://cwsf.whut.edu.cn/netdetails515N023",
lan: false,
},
],
},
{
titleKey: "fn.section.info",
items: [
{
icon: "newspaper-outline",
labelKey: "fn.app.campusNews",
color: "#f97316",
uri: "http://i.whut.edu.cn",
lan: true,
},
],
},
];
function resolveLocalizedString(value: LocalizedString): string {
if (typeof value === "string") return value;
const lang = getResolvedLang();
return value[lang] ?? value.zh ?? "";
}

function openWeb(uri: string) {
router.push({ pathname: "/browser", params: { uri } });
Expand All @@ -133,12 +49,22 @@ export default function FunctionScreen() {
const { height } = useWindowDimensions();
const [showBrowser, setShowBrowser] = useState(false);
const [uri, setUri] = useState("");
const [pendingLanApp, setPendingLanApp] = useState<WebApp | null>(null);
const [pendingLanApp, setPendingLanApp] = useState<FunctionWebApp | null>(
null,
);

const handleOpenApp = (app: WebApp) => {
const remoteSections = useFunctionAppsStore((s) => s.sections);
const fetchFunctionApps = useFunctionAppsStore((s) => s.fetch);
const sections = remoteSections;

useEffect(() => {
fetchFunctionApps();
}, [fetchFunctionApps]);

const handleOpenApp = (app: FunctionWebApp) => {
haptic();
if (app.route) {
router.push(app.route);
router.push(app.route as Href);
return;
}
if (!app.uri) return;
Expand Down Expand Up @@ -211,17 +137,17 @@ export default function FunctionScreen() {
}`}
/>

{SECTIONS.map((section) => (
<View key={section.titleKey} className="mb-5">
{sections.map((section) => (
<View key={section.id} className="mb-5">
<Text className="mb-3 px-6 text-base font-semibold text-neutral-800 dark:text-neutral-100">
{t(section.titleKey)}
{resolveLocalizedString(section.title)}
</Text>
<View className="flex-row flex-wrap px-2">
{section.items.map((app) => (
<AppItem
key={app.labelKey}
key={app.id}
app={app}
label={t(app.labelKey)}
label={resolveLocalizedString(app.label)}
isDark={isDark}
hasBg={hasBgImage}
onPress={() => handleOpenApp(app)}
Expand Down Expand Up @@ -299,7 +225,9 @@ export default function FunctionScreen() {
onClose={() => setPendingLanApp(null)}
title={t("fn.lanTitle")}
description={t("fn.lanDesc", {
app: pendingLanApp ? t(pendingLanApp.labelKey) : "",
app: pendingLanApp
? resolveLocalizedString(pendingLanApp.label)
: "",
})}
confirmText={t("fn.lanConfirm")}
onConfirm={handleConfirmLan}
Expand All @@ -316,7 +244,7 @@ function AppItem({
hasBg,
onPress,
}: {
app: WebApp;
app: FunctionWebApp;
label: string;
isDark: boolean;
hasBg: boolean;
Expand Down Expand Up @@ -359,7 +287,11 @@ function AppItem({
backgroundColor: isDark ? `${app.color}18` : `${app.color}12`,
}}
/>
<Ionicons name={app.icon} size={20} color={app.color} />
<Ionicons
name={app.icon as React.ComponentProps<typeof Ionicons>["name"]}
size={20}
color={app.color}
/>
</View>
<Text
className={`mt-2 text-xs ${
Expand Down
2 changes: 2 additions & 0 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import { syncWidgetData } from "@/services/widget-sync";
import { useAnnouncementStore } from "@/store/announcements";
import { useCourseStore } from "@/store/course";
import { useExamStore } from "@/store/exam";
import { useFunctionAppsStore } from "@/store/function-apps";
import { useOnboardingStore } from "@/store/onboarding";
import { useSettingsStore } from "@/store/settings";
import { useThemeStore } from "@/store/theme";
Expand Down Expand Up @@ -112,6 +113,7 @@ function RootLayout() {
useEffect(() => {
useUpdateStore.getState().check();
useAnnouncementStore.getState().fetch();
useFunctionAppsStore.getState().fetch();
}, []);

useEffect(() => {
Expand Down
16 changes: 0 additions & 16 deletions lib/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -225,22 +225,6 @@
"fn": {
"title": "Apps",
"subtitle": "Campus services at your fingertips",
"section": {
"study": "Study",
"life": "Life",
"info": "Campus Info"
},
"app": {
"exam": "Exams",
"grade": "Grades",
"classroom": "Study Rooms",
"icSpace": "IC Space Booking",
"library": "Library",
"card": "Campus Card",
"elec": "Electricity",
"netPay": "Network Payment",
"campusNews": "Campus News"
},
"lanTitle": "Confirm campus network",
"lanDesc": "{app} is only available on the campus network. Please confirm you are connected before continuing.",
"lanConfirm": "Connected, open"
Expand Down
16 changes: 0 additions & 16 deletions lib/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -225,22 +225,6 @@
"fn": {
"title": "功能",
"subtitle": "校园服务,触手可及",
"section": {
"study": "学习工具",
"life": "生活服务",
"info": "校园资讯"
},
"app": {
"exam": "考试安排",
"grade": "成绩查询",
"classroom": "自习室查询",
"icSpace": "IC 空间预约",
"library": "图书馆管家",
"card": "智寻卡片",
"elec": "电费查询",
"netPay": "网络缴费",
"campusNews": "校园公告"
},
"lanTitle": "请确认校园网环境",
"lanDesc": "「{app}」需要在校园网环境下访问。请确认当前已连接校园网后继续打开。",
"lanConfirm": "已连接,继续打开"
Expand Down
Loading