From b1a6b608c61e624da28d3dabae4b4ed341e76bcd Mon Sep 17 00:00:00 2001 From: zyh Date: Thu, 10 Apr 2025 02:25:25 +0000 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=AE=A1=E7=90=86=E7=AB=AF?= =?UTF-8?q?=E5=92=8C=E7=A7=BB=E5=8A=A8=E7=AB=AF=E7=9A=84=E5=A4=9A=E4=B8=AA?= =?UTF-8?q?=E6=96=B0=E5=8A=9F=E8=83=BD=E6=A8=A1=E5=9D=97=EF=BC=8C=E5=8C=85?= =?UTF-8?q?=E6=8B=AC=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0=E3=80=81=E5=9C=A8?= =?UTF-8?q?=E7=BA=BF=E5=9C=B0=E5=9B=BE=E3=80=81=E7=94=A8=E6=88=B7=E8=AE=A4?= =?UTF-8?q?=E8=AF=81=E3=80=81=E7=B3=BB=E7=BB=9F=E8=AE=BE=E7=BD=AE=E7=AD=89?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84?= =?UTF-8?q?=EF=BC=8C=E6=8F=90=E5=8D=87=E5=8F=AF=E7=BB=B4=E6=8A=A4=E6=80=A7?= =?UTF-8?q?=E5=92=8C=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- {asset => client}/admin/api.ts | 0 {asset => client}/admin/components_amap.tsx | 0 .../admin/components_uploader.tsx | 0 {asset => client}/admin/deno.json | 0 {asset => client}/admin/deno.lock | 0 {asset => client}/admin/hooks_sys.tsx | 0 {asset => client}/admin/pages_chart.tsx | 0 {asset => client}/admin/pages_login_reg.tsx | 0 {asset => client}/admin/pages_map.tsx | 0 {asset => client}/admin/pages_settings.tsx | 0 {asset => client}/admin/pages_sys.tsx | 0 {asset => client}/admin/style_amap.css | 0 {asset => client}/admin/utils.ts | 0 {asset => client}/admin/web_app.tsx | 0 client/mobile/deno.json | 18 + client/mobile/hooks.tsx | 271 ++++++++++++++ client/mobile/mobile_app.tsx | 313 ++++++++++++++++ client/mobile/pages/index.tsx | 336 ++++++++++++++++++ client/mobile/pages/login.tsx | 144 ++++++++ client/mobile/utils.ts | 169 +++++++++ {asset => client}/share/types.ts | 101 +++++- README.md => server/README.md | 0 app.tsx => server/app.tsx | 18 +- deno.json => server/deno.json | 0 deno.lock => server/deno.lock | 0 migrations.ts => server/migrations.ts | 11 +- routes_auth.ts => server/routes_auth.ts | 0 routes_charts.ts => server/routes_charts.ts | 13 +- routes_maps.ts => server/routes_maps.ts | 14 - routes_sys.ts => server/routes_sys.ts | 4 +- routes_users.ts => server/routes_users.ts | 0 31 files changed, 1366 insertions(+), 46 deletions(-) rename {asset => client}/admin/api.ts (100%) rename {asset => client}/admin/components_amap.tsx (100%) rename {asset => client}/admin/components_uploader.tsx (100%) rename {asset => client}/admin/deno.json (100%) rename {asset => client}/admin/deno.lock (100%) rename {asset => client}/admin/hooks_sys.tsx (100%) rename {asset => client}/admin/pages_chart.tsx (100%) rename {asset => client}/admin/pages_login_reg.tsx (100%) rename {asset => client}/admin/pages_map.tsx (100%) rename {asset => client}/admin/pages_settings.tsx (100%) rename {asset => client}/admin/pages_sys.tsx (100%) rename {asset => client}/admin/style_amap.css (100%) rename {asset => client}/admin/utils.ts (100%) rename {asset => client}/admin/web_app.tsx (100%) create mode 100644 client/mobile/deno.json create mode 100644 client/mobile/hooks.tsx create mode 100644 client/mobile/mobile_app.tsx create mode 100644 client/mobile/pages/index.tsx create mode 100644 client/mobile/pages/login.tsx create mode 100644 client/mobile/utils.ts rename {asset => client}/share/types.ts (81%) rename README.md => server/README.md (100%) rename app.tsx => server/app.tsx (96%) rename deno.json => server/deno.json (100%) rename deno.lock => server/deno.lock (100%) rename migrations.ts => server/migrations.ts (96%) rename routes_auth.ts => server/routes_auth.ts (100%) rename routes_charts.ts => server/routes_charts.ts (96%) rename routes_maps.ts => server/routes_maps.ts (95%) rename routes_sys.ts => server/routes_sys.ts (99%) rename routes_users.ts => server/routes_users.ts (100%) diff --git a/asset/admin/api.ts b/client/admin/api.ts similarity index 100% rename from asset/admin/api.ts rename to client/admin/api.ts diff --git a/asset/admin/components_amap.tsx b/client/admin/components_amap.tsx similarity index 100% rename from asset/admin/components_amap.tsx rename to client/admin/components_amap.tsx diff --git a/asset/admin/components_uploader.tsx b/client/admin/components_uploader.tsx similarity index 100% rename from asset/admin/components_uploader.tsx rename to client/admin/components_uploader.tsx diff --git a/asset/admin/deno.json b/client/admin/deno.json similarity index 100% rename from asset/admin/deno.json rename to client/admin/deno.json diff --git a/asset/admin/deno.lock b/client/admin/deno.lock similarity index 100% rename from asset/admin/deno.lock rename to client/admin/deno.lock diff --git a/asset/admin/hooks_sys.tsx b/client/admin/hooks_sys.tsx similarity index 100% rename from asset/admin/hooks_sys.tsx rename to client/admin/hooks_sys.tsx diff --git a/asset/admin/pages_chart.tsx b/client/admin/pages_chart.tsx similarity index 100% rename from asset/admin/pages_chart.tsx rename to client/admin/pages_chart.tsx diff --git a/asset/admin/pages_login_reg.tsx b/client/admin/pages_login_reg.tsx similarity index 100% rename from asset/admin/pages_login_reg.tsx rename to client/admin/pages_login_reg.tsx diff --git a/asset/admin/pages_map.tsx b/client/admin/pages_map.tsx similarity index 100% rename from asset/admin/pages_map.tsx rename to client/admin/pages_map.tsx diff --git a/asset/admin/pages_settings.tsx b/client/admin/pages_settings.tsx similarity index 100% rename from asset/admin/pages_settings.tsx rename to client/admin/pages_settings.tsx diff --git a/asset/admin/pages_sys.tsx b/client/admin/pages_sys.tsx similarity index 100% rename from asset/admin/pages_sys.tsx rename to client/admin/pages_sys.tsx diff --git a/asset/admin/style_amap.css b/client/admin/style_amap.css similarity index 100% rename from asset/admin/style_amap.css rename to client/admin/style_amap.css diff --git a/asset/admin/utils.ts b/client/admin/utils.ts similarity index 100% rename from asset/admin/utils.ts rename to client/admin/utils.ts diff --git a/asset/admin/web_app.tsx b/client/admin/web_app.tsx similarity index 100% rename from asset/admin/web_app.tsx rename to client/admin/web_app.tsx diff --git a/client/mobile/deno.json b/client/mobile/deno.json new file mode 100644 index 0000000..b18b6ae --- /dev/null +++ b/client/mobile/deno.json @@ -0,0 +1,18 @@ +{ + "imports": { + "react": "https://esm.d8d.fun/react@19.0.0?deps=react@19.0.0,react-dom@19.0.0", + "react-dom": "https://esm.d8d.fun/react-dom@19.0.0?deps=react@19.0.0,react-dom@19.0.0", + "react-dom/client": "https://esm.d8d.fun/react-dom@19.0.0/client?deps=react@19.0.0,react-dom@19.0.0", + "react-router": "https://esm.d8d.fun/react-router@7.3.0?deps=react@19.0.0,react-dom@19.0.0", + "@heroicons/react/24/outline": "https://esm.d8d.fun/@heroicons/react@2.1.1/24/outline?deps=react@19.0.0", + "@heroicons/react/24/solid": "https://esm.d8d.fun/@heroicons/react@2.1.1/24/solid?deps=react@19.0.0", + "axios": "https://esm.d8d.fun/axios@1.6.7", + "dayjs": "https://esm.d8d.fun/dayjs@1.11.13", + "dayjs/plugin/weekday": "https://esm.d8d.fun/dayjs@1.11.13/plugin/weekday", + "dayjs/plugin/localeData": "https://esm.d8d.fun/dayjs@1.11.13/plugin/localeData", + "dayjs/locale/zh-cn": "https://esm.d8d.fun/dayjs@1.11.13/locale/zh-cn", + "@tanstack/react-query": "https://esm.d8d.fun/@tanstack/react-query@5.67.1?deps=react@19.0.0,react-dom@19.0.0", + "@d8d-appcontainer/api": "https://esm.d8d.fun/@d8d-appcontainer/api@3.0.47", + "@d8d-appcontainer/types": "https://esm.d8d.fun/@d8d-appcontainer/types@3.0.47" + } +} \ No newline at end of file diff --git a/client/mobile/hooks.tsx b/client/mobile/hooks.tsx new file mode 100644 index 0000000..9dd7902 --- /dev/null +++ b/client/mobile/hooks.tsx @@ -0,0 +1,271 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; +import axios from 'axios'; +import { getLocalStorageWithExpiry, setLocalStorageWithExpiry } from './utils.ts'; +import type { User, AuthContextType, ThemeContextType, ThemeSettings } from '../share/types.ts'; +import { ThemeMode, FontSize, CompactMode } from '../share/types.ts'; + +// 创建axios实例 +const api = axios.create({ + baseURL: window.CONFIG?.API_BASE_URL || '/api', + timeout: 10000, + headers: { + 'Content-Type': 'application/json', + } +}); + +// 请求拦截器添加token +api.interceptors.request.use( + (config) => { + const token = getLocalStorageWithExpiry('token'); + if (token) { + config.headers['Authorization'] = `Bearer ${token}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +// 响应拦截器处理错误 +api.interceptors.response.use( + (response) => { + return response; + }, + (error) => { + if (error.response && error.response.status === 401) { + // 清除本地存储并刷新页面 + localStorage.removeItem('token'); + localStorage.removeItem('user'); + window.location.href = '/mobile/login'; + } + return Promise.reject(error); + } +); + +// 默认主题设置 +const defaultThemeSettings: ThemeSettings = { + user_id: 0, + theme_mode: ThemeMode.LIGHT, + primary_color: '#3B82F6', // 蓝色 + background_color: '#F9FAFB', + text_color: '#111827', + border_radius: 8, + font_size: FontSize.MEDIUM, + is_compact: CompactMode.NORMAL +}; + +// 创建认证上下文 +const AuthContext = createContext({ + user: null, + token: null, + login: async () => {}, + logout: async () => {}, + isAuthenticated: false, + isLoading: true +}); + +// 创建主题上下文 +const ThemeContext = createContext({ + isDark: false, + currentTheme: defaultThemeSettings, + updateTheme: () => {}, + saveTheme: async () => defaultThemeSettings, + resetTheme: async () => defaultThemeSettings, + toggleTheme: () => {} +}); + +// 认证提供者组件 +export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [user, setUser] = useState(null); + const [token, setToken] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + // 初始化时从本地存储获取用户信息和令牌 + useEffect(() => { + const storedToken = getLocalStorageWithExpiry('token'); + const storedUser = getLocalStorageWithExpiry('user'); + + if (storedToken && storedUser) { + setToken(storedToken); + setUser(storedUser); + } + + setIsLoading(false); + }, []); + + // 登录函数 + const login = async (username: string, password: string) => { + try { + const response = await api.post('/auth/login', { username, password }); + const { token, user } = response.data; + + // 保存到状态和本地存储 + setToken(token); + setUser(user); + setLocalStorageWithExpiry('token', token, 24); // 24小时过期 + setLocalStorageWithExpiry('user', user, 24); + + return user; + } catch (error) { + console.error('登录失败:', error); + throw error; + } + }; + + // 登出函数 + const logout = async () => { + try { + // 调用登出API + await api.post('/auth/logout'); + } catch (error) { + console.error('登出API调用失败:', error); + } finally { + // 无论API调用成功与否,都清除本地状态 + setToken(null); + setUser(null); + localStorage.removeItem('token'); + localStorage.removeItem('user'); + } + }; + + return ( + + {children} + + ); +}; + +// 主题提供者组件 +export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [currentTheme, setCurrentTheme] = useState(() => { + const storedTheme = localStorage.getItem('theme'); + return storedTheme ? JSON.parse(storedTheme) : defaultThemeSettings; + }); + + const isDark = currentTheme.theme_mode === ThemeMode.DARK; + + // 更新主题(实时预览) + const updateTheme = (theme: Partial) => { + setCurrentTheme(prev => { + const updatedTheme = { ...prev, ...theme }; + localStorage.setItem('theme', JSON.stringify(updatedTheme)); + return updatedTheme; + }); + }; + + // 保存主题到后端 + const saveTheme = async (theme: Partial): Promise => { + try { + const updatedTheme = { ...currentTheme, ...theme }; + const { data } = await api.post('/theme/save', updatedTheme); + + setCurrentTheme(data); + localStorage.setItem('theme', JSON.stringify(data)); + + return data; + } catch (error) { + console.error('保存主题失败:', error); + throw error; + } + }; + + // 重置主题 + const resetTheme = async (): Promise => { + try { + const { data } = await api.post('/theme/reset'); + + setCurrentTheme(data); + localStorage.setItem('theme', JSON.stringify(data)); + + return data; + } catch (error) { + console.error('重置主题失败:', error); + + // 如果API失败,至少重置到默认主题 + setCurrentTheme(defaultThemeSettings); + localStorage.setItem('theme', JSON.stringify(defaultThemeSettings)); + + return defaultThemeSettings; + } + }; + + // 切换主题模式(亮色/暗色) + const toggleTheme = () => { + const newMode = isDark ? ThemeMode.LIGHT : ThemeMode.DARK; + const updatedTheme = { + ...currentTheme, + theme_mode: newMode, + // 暗色和亮色模式下自动调整背景色和文字颜色 + background_color: newMode === ThemeMode.DARK ? '#121212' : '#F9FAFB', + text_color: newMode === ThemeMode.DARK ? '#E5E7EB' : '#111827' + }; + + setCurrentTheme(updatedTheme); + localStorage.setItem('theme', JSON.stringify(updatedTheme)); + }; + + // 主题变化时应用CSS变量 + useEffect(() => { + document.documentElement.style.setProperty('--primary-color', currentTheme.primary_color); + document.documentElement.style.setProperty('--background-color', currentTheme.background_color || '#F9FAFB'); + document.documentElement.style.setProperty('--text-color', currentTheme.text_color || '#111827'); + document.documentElement.style.setProperty('--border-radius', `${currentTheme.border_radius || 8}px`); + + // 设置字体大小 + let rootFontSize = '16px'; // 默认中等字体 + if (currentTheme.font_size === FontSize.SMALL) { + rootFontSize = '14px'; + } else if (currentTheme.font_size === FontSize.LARGE) { + rootFontSize = '18px'; + } + document.documentElement.style.setProperty('--font-size', rootFontSize); + + // 设置暗色模式类 + if (isDark) { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + }, [currentTheme, isDark]); + + return ( + + {children} + + ); +}; + +// 主题hook +export const useTheme = () => useContext(ThemeContext); + +// 认证hook +export const useAuth = () => useContext(AuthContext); + +// API hook +export const useApi = () => { + const { token } = useAuth(); + + return { + api, + isAuthenticated: !!token + }; +}; \ No newline at end of file diff --git a/client/mobile/mobile_app.tsx b/client/mobile/mobile_app.tsx new file mode 100644 index 0000000..c63e445 --- /dev/null +++ b/client/mobile/mobile_app.tsx @@ -0,0 +1,313 @@ +import React, { useEffect } from 'react'; +import { createRoot } from 'react-dom/client'; +import { + createBrowserRouter, + RouterProvider, + Outlet, + Navigate, + useLocation +} from 'react-router'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { AuthProvider, ThemeProvider, useAuth } from './hooks.tsx'; +import HomePage from './pages/index.tsx'; +import LoginPage from './pages/login.tsx'; +import dayjs from 'dayjs'; +import 'dayjs/locale/zh-cn'; + +// 设置中文语言 +dayjs.locale('zh-cn'); + +// 创建QueryClient实例 +const queryClient = new QueryClient(); + +// 添加全局CSS(使用TailwindCSS的类) +const injectGlobalStyles = () => { + const style = document.createElement('style'); + style.innerHTML = ` + :root { + --primary-color: #3B82F6; + --background-color: #F9FAFB; + --text-color: #111827; + --border-radius: 8px; + --font-size: 16px; + } + + * { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, + Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + background-color: var(--background-color); + color: var(--text-color); + font-size: var(--font-size); + line-height: 1.5; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + + .line-clamp-1 { + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; + } + + .line-clamp-2 { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + } + + .line-clamp-3 { + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + } + + /* 暗色模式支持 */ + .dark { + color-scheme: dark; + } + + .dark body { + background-color: #121212; + color: #E5E7EB; + } + + /* 滚动条美化 */ + ::-webkit-scrollbar { + width: 6px; + height: 6px; + } + + ::-webkit-scrollbar-track { + background: transparent; + } + + ::-webkit-scrollbar-thumb { + background: #BFDBFE; + border-radius: 3px; + } + + ::-webkit-scrollbar-thumb:hover { + background: #93C5FD; + } + + /* 移动端点击高亮颜色 */ + * { + -webkit-tap-highlight-color: transparent; + } + `; + document.head.appendChild(style); +}; + +// 授权路由守卫 +const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { + const { isAuthenticated, isLoading } = useAuth(); + const location = useLocation(); + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (!isAuthenticated) { + return ; + } + + return <>{children}; +}; + +// 页面组件 +const PageNotFound = () => ( +
+
404
+

页面不存在

+

您访问的页面不存在或已被移除

+ + 返回首页 + +
+); + +// 添加个人页面组件 +const ProfilePage = () => ( +
+

我的

+
+
+
+ 用户 +
+
+

用户名

+

个人信息

+
+
+
+
+
+ 设置 +
+
+
+ 账号安全 + +
+
+ 通知设置 + +
+
+ 隐私 + +
+
+ 关于 + +
+
+
+
+); + +// 添加通知页面组件 +const NotificationsPage = () => ( +
+

通知

+
+
+

系统通知

+

欢迎使用移动应用!

+

今天 10:00

+
+
+

活动提醒

+

您有一个新的活动邀请

+

昨天 14:30

+
+
+
+); + +// 移动端布局组件 - 包含底部导航 +const MobileLayout = () => { + const location = useLocation(); + + return ( +
+
+ +
+ + {/* 底部导航栏 */} + +
+ ); +}; + +// 主应用组件 +const App = () => { + // 创建路由器配置 + const router = createBrowserRouter([ + { + path: '/', + element: + }, + { + path: '/mobile/login', + element: + }, + { + path: '/mobile', + element: ( + + + + ), + children: [ + { + index: true, + element: + }, + { + path: 'profile', + element: + }, + { + path: 'notifications', + element: + } + ] + }, + { + path: '*', + element: + } + ]); + + return ; +}; + +// 渲染应用到DOM +const initApp = () => { + // 注入全局样式 + injectGlobalStyles(); + + // 渲染应用 + const root = createRoot(document.getElementById('root') as HTMLElement); + root.render( + + + + + + + + ); +}; + +// 初始化应用 +initApp(); \ No newline at end of file diff --git a/client/mobile/pages/index.tsx b/client/mobile/pages/index.tsx new file mode 100644 index 0000000..690743f --- /dev/null +++ b/client/mobile/pages/index.tsx @@ -0,0 +1,336 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate, useLocation } from 'react-router'; +import { + HomeIcon, + UserIcon, + NewspaperIcon, + BellIcon +} from '@heroicons/react/24/outline'; +import { useAuth } from '../hooks.tsx'; +import { formatRelativeTime } from '../utils.ts'; + +interface BannerItem { + id: number; + title: string; + image: string; + link: string; +} + +interface NewsItem { + id: number; + title: string; + summary: string; + publish_date: string; + cover?: string; + category: string; +} + +interface NoticeItem { + id: number; + title: string; + content: string; + created_at: string; + is_read: boolean; +} + +// 首页组件 +const HomePage: React.FC = () => { + const { user } = useAuth(); + const navigate = useNavigate(); + const location = useLocation(); + const [loading, setLoading] = useState(true); + const [banners, setBanners] = useState([]); + const [news, setNews] = useState([]); + const [notices, setNotices] = useState([]); + const [activeTab, setActiveTab] = useState('news'); + + // 模拟加载数据 + useEffect(() => { + // 模拟API请求 + setTimeout(() => { + // 模拟轮播图数据 + setBanners([ + { + id: 1, + title: '欢迎使用移动端应用', + image: 'https://images.unsplash.com/photo-1518655048521-f130df041f66?ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8cG9ydGZvbGlvJTIwYmFja2dyb3VuZHxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&w=1000&q=80', + link: '/welcome' + }, + { + id: 2, + title: '新功能上线了', + image: 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8cG9ydGZvbGlvJTIwYmFja2dyb3VuZHxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&w=1000&q=80', + link: '/new-features' + } + ]); + + // 模拟新闻数据 + setNews([ + { + id: 1, + title: '用户体验升级,新版本发布', + summary: '我们很高兴地宣布,新版本已经发布,带来了更好的用户体验和更多新功能。', + publish_date: '2023-05-01T08:30:00', + cover: 'https://images.unsplash.com/photo-1496171367470-9ed9a91ea931?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTB8fHRlY2h8ZW58MHx8MHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60', + category: '产品更新' + }, + { + id: 2, + title: '新的数据分析功能上线', + summary: '新的数据分析功能让您更深入地了解您的业务数据,提供更好的决策支持。', + publish_date: '2023-04-25T14:15:00', + cover: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTJ8fGNoYXJ0fGVufDB8fDB8fA%3D%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60', + category: '功能介绍' + }, + { + id: 3, + title: '如何提高工作效率的5个小技巧', + summary: '这篇文章分享了5个可以立即实施的小技巧,帮助您提高日常工作效率。', + publish_date: '2023-04-20T09:45:00', + category: '使用技巧' + } + ]); + + // 模拟通知数据 + setNotices([ + { + id: 1, + title: '系统维护通知', + content: '我们将于本周六凌晨2点至4点进行系统维护,期间系统可能会出现短暂不可用。', + created_at: '2023-05-02T10:00:00', + is_read: false + }, + { + id: 2, + title: '您的账户信息已更新', + content: '您的账户信息已成功更新,如非本人操作,请及时联系客服。', + created_at: '2023-05-01T16:30:00', + is_read: true + } + ]); + + setLoading(false); + }, 800); + }, []); + + // 处理轮播图点击 + const handleBannerClick = (link: string) => { + navigate(link); + }; + + // 处理新闻点击 + const handleNewsClick = (id: number) => { + navigate(`/news/${id}`); + }; + + // 处理通知点击 + const handleNoticeClick = (id: number) => { + navigate(`/notices/${id}`); + }; + + return ( +
+ {/* 顶部用户信息 */} +
+
+
+
+ {user?.avatar ? ( + {user?.nickname + ) : ( + + )} +
+
+

+ {user ? `您好,${user.nickname || user.username}` : '您好,游客'} +

+

+ {user ? '欢迎回来' : '请登录体验更多功能'} +

+
+
+ +
+ + {notices.some(notice => !notice.is_read) && ( + + )} +
+
+
+ + {/* 轮播图 */} + {!loading && banners.length > 0 && ( +
+
+ {banners.map((banner) => ( +
handleBannerClick(banner.link)} + > + {banner.title} +
+

{banner.title}

+
+
+ ))} +
+ + {/* 指示器 */} +
+ {banners.map((_, index) => ( + + ))} +
+
+ )} + + {/* 快捷入口 */} +
+ {[ + { icon: , name: '首页', path: '/' }, + { icon: , name: '资讯', path: '/news' }, + { icon: , name: '通知', path: '/notices' }, + { icon: , name: '我的', path: '/profile' } + ].map((item, index) => ( +
navigate(item.path)} + > +
+ {item.icon} +
+ {item.name} +
+ ))} +
+ + {/* 内容标签页 */} +
+
+ + +
+ +
+ {activeTab === 'news' ? ( + loading ? ( +
+
+
+ ) : ( +
+ {news.map((item) => ( +
handleNewsClick(item.id)} + > + {item.cover && ( + {item.title} + )} +
+

{item.title}

+

{item.summary}

+
+ + {item.category} + + + {formatRelativeTime(item.publish_date)} + +
+
+
+ ))} +
+ ) + ) : ( + loading ? ( +
+
+
+ ) : ( +
+ {notices.map((item) => ( +
handleNoticeClick(item.id)} + > +
+

+ {!item.is_read && ( + + )} + {item.title} +

+ + {formatRelativeTime(item.created_at)} + +
+

{item.content}

+
+ ))} +
+ ) + )} +
+
+ + {/* 底部导航 */} +
+ {[ + { icon: , name: '首页', path: '/' }, + { icon: , name: '资讯', path: '/news' }, + { icon: , name: '通知', path: '/notices' }, + { icon: , name: '我的', path: '/profile' } + ].map((item, index) => ( +
navigate(item.path)} + > +
+ {item.icon} +
+ + {item.name} + +
+ ))} +
+
+ ); +}; + +export default HomePage; \ No newline at end of file diff --git a/client/mobile/pages/login.tsx b/client/mobile/pages/login.tsx new file mode 100644 index 0000000..75312b0 --- /dev/null +++ b/client/mobile/pages/login.tsx @@ -0,0 +1,144 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router'; +import { ArrowRightIcon, LockClosedIcon, UserIcon } from '@heroicons/react/24/outline'; +import { useAuth } from '../hooks.tsx'; +import { handleApiError } from '../utils.ts'; + +// 登录页面组件 +const LoginPage: React.FC = () => { + const { login } = useAuth(); + const navigate = useNavigate(); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!username.trim() || !password.trim()) { + setError('用户名和密码不能为空'); + return; + } + + setLoading(true); + setError(null); + + try { + await login(username, password); + navigate('/'); + } catch (err) { + setError(handleApiError(err)); + } finally { + setLoading(false); + } + }; + + return ( +
+ {/* 顶部Logo和标题 */} +
+
+ + + + + +
+

+ {window.CONFIG?.APP_NAME || '移动应用'} +

+

登录您的账户

+
+ + {/* 登录表单 */} +
+ {error && ( +
+ {error} +
+ )} + +
+
+ +
+
+ +
+ setUsername(e.target.value)} + className="w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + placeholder="请输入用户名" + /> +
+
+ +
+ +
+
+ +
+ setPassword(e.target.value)} + className="w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + placeholder="请输入密码" + /> +
+
+ + +
+ +
+ + +
+
+ + {/* 底部文本 */} +
+ © {new Date().getFullYear()} {window.CONFIG?.APP_NAME || '移动应用'} +

保留所有权利

+
+
+ ); +}; + +export default LoginPage; \ No newline at end of file diff --git a/client/mobile/utils.ts b/client/mobile/utils.ts new file mode 100644 index 0000000..a23af3a --- /dev/null +++ b/client/mobile/utils.ts @@ -0,0 +1,169 @@ +import dayjs from 'dayjs'; +import { EnableStatus, DeleteStatus, AuditStatus } from '../share/types.ts'; + +// 日期格式化 +export const formatDate = (date: string | Date, format = 'YYYY-MM-DD HH:mm:ss'): string => { + if (!date) return '-'; + return dayjs(date).format(format); +}; + +// 格式化时间为相对时间(如:3小时前) +export const formatRelativeTime = (date: string | Date): string => { + if (!date) return '-'; + const now = dayjs(); + const dateObj = dayjs(date); + const diffInSeconds = now.diff(dateObj, 'second'); + + if (diffInSeconds < 60) { + return `${diffInSeconds}秒前`; + } else if (diffInSeconds < 3600) { + return `${Math.floor(diffInSeconds / 60)}分钟前`; + } else if (diffInSeconds < 86400) { + return `${Math.floor(diffInSeconds / 3600)}小时前`; + } else if (diffInSeconds < 2592000) { + return `${Math.floor(diffInSeconds / 86400)}天前`; + } else { + return formatDate(date, 'YYYY-MM-DD'); + } +}; + +// 获取枚举的选项(用于下拉菜单等) +export const getEnumOptions = (enumObj: Record) => { + return Object.entries(enumObj) + .filter(([key]) => !isNaN(Number(key))) // 过滤掉映射对象中的字符串键 + .map(([value, label]) => ({ + value: Number(value), + label: String(label) + })); +}; + +// 获取启用状态选项 +export const getEnableStatusOptions = () => { + return [ + { value: EnableStatus.ENABLED, label: '启用' }, + { value: EnableStatus.DISABLED, label: '禁用' } + ]; +}; + +// 获取删除状态选项 +export const getDeleteStatusOptions = () => { + return [ + { value: DeleteStatus.NOT_DELETED, label: '未删除' }, + { value: DeleteStatus.DELETED, label: '已删除' } + ]; +}; + +// 获取审核状态选项 +export const getAuditStatusOptions = () => { + return [ + { value: AuditStatus.PENDING, label: '待审核' }, + { value: AuditStatus.APPROVED, label: '已通过' }, + { value: AuditStatus.REJECTED, label: '已拒绝' } + ]; +}; + +// 格式化文件大小 +export const formatFileSize = (bytes: number): string => { + if (bytes === 0) return '0 B'; + + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +}; + +// 处理API错误 +export const handleApiError = (error: any): string => { + if (error.response) { + // 服务器响应错误 + const status = error.response.status; + const data = error.response.data; + + if (status === 401) { + return '您的登录已过期,请重新登录'; + } else if (status === 403) { + return '您没有权限执行此操作'; + } else if (status === 404) { + return '请求的资源不存在'; + } else if (status === 422) { + // 表单验证错误 + return data.message || '输入数据无效'; + } else { + return data.message || `服务器错误 (${status})`; + } + } else if (error.request) { + // 请求发送成功但没有收到响应 + return '网络连接错误,请检查您的网络连接'; + } else { + // 请求设置错误 + return '应用程序错误,请稍后再试'; + } +}; + +// 复制文本到剪贴板 +export const copyToClipboard = async (text: string): Promise => { + try { + await navigator.clipboard.writeText(text); + return true; + } catch (error) { + console.error('复制到剪贴板失败:', error); + return false; + } +}; + +// 防抖函数 +export const debounce = any>( + func: T, + wait: number +): ((...args: Parameters) => void) => { + let timeout: number | null = null; + + return (...args: Parameters) => { + if (timeout) { + clearTimeout(timeout); + } + + timeout = setTimeout(() => { + func(...args); + }, wait) as unknown as number; + }; +}; + +// 生成随机颜色 +export const getRandomColor = (): string => { + const letters = '0123456789ABCDEF'; + let color = '#'; + for (let i = 0; i < 6; i++) { + color += letters[Math.floor(Math.random() * 16)]; + } + return color; +}; + +// 获取本地存储值,带过期检查 +export const getLocalStorageWithExpiry = (key: string) => { + const itemStr = localStorage.getItem(key); + + if (!itemStr) return null; + + const item = JSON.parse(itemStr); + const now = new Date(); + + if (item.expiry && now.getTime() > item.expiry) { + localStorage.removeItem(key); + return null; + } + + return item.value; +}; + +// 设置本地存储值,带过期时间 +export const setLocalStorageWithExpiry = (key: string, value: any, expiryHours = 24) => { + const now = new Date(); + const item = { + value: value, + expiry: now.getTime() + expiryHours * 60 * 60 * 1000 + }; + + localStorage.setItem(key, JSON.stringify(item)); +}; \ No newline at end of file diff --git a/asset/share/types.ts b/client/share/types.ts similarity index 81% rename from asset/share/types.ts rename to client/share/types.ts index 33f5ec1..a9349c5 100644 --- a/asset/share/types.ts +++ b/client/share/types.ts @@ -308,4 +308,103 @@ export enum AllowedFileType { } // 允许的文件类型列表(用于系统设置) -export const ALLOWED_FILE_TYPES = Object.values(AllowedFileType).join(','); \ No newline at end of file +export const ALLOWED_FILE_TYPES = Object.values(AllowedFileType).join(','); + +// 文件库接口 +export interface FileLibrary { + /** 主键ID */ + id: number; + + /** 文件名称 */ + file_name: string; + + /** 原始文件名 */ + original_filename?: string; + + /** 文件路径 */ + file_path: string; + + /** 文件类型 */ + file_type: string; + + /** 文件大小(字节) */ + file_size: number; + + /** 上传用户ID */ + uploader_id?: number; + + /** 上传者名称 */ + uploader_name?: string; + + /** 文件分类 */ + category_id?: number; + + /** 文件标签 */ + tags?: string; + + /** 文件描述 */ + description?: string; + + /** 下载次数 */ + download_count: number; + + /** 是否禁用 (0否 1是) */ + is_disabled?: EnableStatus; + + /** 是否被删除 (0否 1是) */ + is_deleted?: DeleteStatus; + + /** 创建时间 */ + created_at: string; + + /** 更新时间 */ + updated_at: string; +} + +// 文件分类接口 +export interface FileCategory { + id: number; + name: string; + code: string; + description?: string; + is_deleted?: DeleteStatus; + created_at: string; + updated_at: string; +} + + +// 知识库表 +export interface KnowInfo { + /** 主键ID */ + id: number; + + /** 文章的标题 */ + title?: string; + + /** 文章的标签 */ + tags?: string; + + /** 文章的内容 */ + content?: string; + + /** 文章的作者 */ + author?: string; + + /** 文章的分类 */ + category?: string; + + /** 文章的封面图片URL */ + cover_url?: string; + + /** 审核状态 */ + audit_status?: number; + + /** 是否被删除 (0否 1是) */ + is_deleted?: number; + + /** 创建时间 */ + created_at: Date; + + /** 更新时间 */ + updated_at: Date; +} diff --git a/README.md b/server/README.md similarity index 100% rename from README.md rename to server/README.md diff --git a/app.tsx b/server/app.tsx similarity index 96% rename from app.tsx rename to server/app.tsx index 867c94b..0c12155 100644 --- a/app.tsx +++ b/server/app.tsx @@ -11,8 +11,8 @@ import { APIClient } from '@d8d-appcontainer/api' import debug from "debug" import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; -import type { SystemSettingRecord, GlobalConfig } from './asset/share/types.ts'; -import { SystemSettingKey, OssType, MapMode } from './asset/share/types.ts'; +import type { SystemSettingRecord, GlobalConfig } from '../client/share/types.ts'; +import { SystemSettingKey, OssType, MapMode } from '../client/share/types.ts'; import { createKnowInfoRoutes, @@ -388,7 +388,7 @@ export default function({ apiClient, app, moduleDir }: ModuleParams) { {title} {isProd ? ( - + ) : ( )} @@ -425,16 +425,16 @@ export default function({ apiClient, app, moduleDir }: ModuleParams) { // 后台管理路由 honoApp.get('/admin', createHtmlWithConfig({ src: "https://esm.d8d.fun/xb", - href: "/asset/admin/web_app.tsx", - denoJson: "/asset/admin/deno.json", + href: "/client/admin/web_app.tsx", + denoJson: "/client/admin/deno.json", refresh: true, prodPath: "admin/web_app.js" }, GLOBAL_CONFIG.APP_NAME)) honoApp.get('/admin/*', createHtmlWithConfig({ src: "https://esm.d8d.fun/xb", - href: "/asset/admin/web_app.tsx", - denoJson: "/asset/admin/deno.json", + href: "/client/admin/web_app.tsx", + denoJson: "/client/admin/deno.json", refresh: true, prodPath: "admin/web_app.js" }, GLOBAL_CONFIG.APP_NAME)) @@ -462,10 +462,10 @@ export default function({ apiClient, app, moduleDir }: ModuleParams) { }) // 静态资源路由 - honoApp.get('/asset/*', staticRoutes) + honoApp.get('/client/*', staticRoutes) honoApp.get('/amap/*', staticRoutes) honoApp.get('/tailwindcss@3.4.16/*', staticRoutes) - honoApp.get('/asset_dist/*', staticRoutes) + honoApp.get('/client_dist/*', staticRoutes) return honoApp } diff --git a/deno.json b/server/deno.json similarity index 100% rename from deno.json rename to server/deno.json diff --git a/deno.lock b/server/deno.lock similarity index 100% rename from deno.lock rename to server/deno.lock diff --git a/migrations.ts b/server/migrations.ts similarity index 96% rename from migrations.ts rename to server/migrations.ts index ceed0fc..0e7b8a8 100644 --- a/migrations.ts +++ b/server/migrations.ts @@ -1,17 +1,12 @@ import type { MigrationLiveDefinition } from '@d8d-appcontainer/types' import { - DeviceCategory, DeviceStatus, AlertLevel, AlertStatus, EnableStatus, DeleteStatus, - HandleType, ProblemType, NotifyType, ServerType, AssetTransferType, AuditStatus, - DeviceProtocolType, MetricType, ThemeMode, FontSize, CompactMode, - - AssetStatus, NetworkStatus, PacketLossStatus, - ZichanArea, ZichanCategory, DeviceType, DeviceInstance, RackInfo, RackServerType, RackServer, ZichanTransLog, DeviceAlertRule, - ZichanInfo, AlertNotifyConfig, KnowInfo, + EnableStatus, DeleteStatus, + AuditStatus, ThemeMode, FontSize, CompactMode, SystemSettingKey, SystemSettingGroup, ALLOWED_FILE_TYPES, -} from './asset/share/types.ts'; +} from '../client/share/types.ts'; // 定义用户表迁移 const createUsersTable: MigrationLiveDefinition = { diff --git a/routes_auth.ts b/server/routes_auth.ts similarity index 100% rename from routes_auth.ts rename to server/routes_auth.ts diff --git a/routes_charts.ts b/server/routes_charts.ts similarity index 96% rename from routes_charts.ts rename to server/routes_charts.ts index 910191e..2691d3b 100644 --- a/routes_charts.ts +++ b/server/routes_charts.ts @@ -1,19 +1,8 @@ import { Hono } from "hono"; import debug from "debug"; -import type { - FileLibrary, - FileCategory, - KnowInfo, - ThemeSettings, -} from "./asset/share/types.ts"; - import { - EnableStatus, DeleteStatus, - ThemeMode, - FontSize, - CompactMode, -} from "./asset/share/types.ts"; +} from "../client/share/types.ts"; import type { Variables, WithAuth } from "./app.tsx"; diff --git a/routes_maps.ts b/server/routes_maps.ts similarity index 95% rename from routes_maps.ts rename to server/routes_maps.ts index 2690f28..ec5716b 100644 --- a/routes_maps.ts +++ b/server/routes_maps.ts @@ -1,19 +1,5 @@ import { Hono } from "hono"; import debug from "debug"; -import type { - FileLibrary, - FileCategory, - KnowInfo, - ThemeSettings, -} from "./asset/share/types.ts"; - -import { - EnableStatus, - DeleteStatus, - ThemeMode, - FontSize, - CompactMode, -} from "./asset/share/types.ts"; import type { Variables, WithAuth } from "./app.tsx"; diff --git a/routes_sys.ts b/server/routes_sys.ts similarity index 99% rename from routes_sys.ts rename to server/routes_sys.ts index 44842c1..e30b4e6 100644 --- a/routes_sys.ts +++ b/server/routes_sys.ts @@ -7,7 +7,7 @@ import type { ThemeSettings, SystemSetting, SystemSettingGroupData, -} from "./asset/share/types.ts"; +} from "../client/share/types.ts"; import { EnableStatus, @@ -15,7 +15,7 @@ import { ThemeMode, FontSize, CompactMode, -} from "./asset/share/types.ts"; +} from "../client/share/types.ts"; import type { Variables, WithAuth } from "./app.tsx"; diff --git a/routes_users.ts b/server/routes_users.ts similarity index 100% rename from routes_users.ts rename to server/routes_users.ts