Initial commit: X-Server Mobile enterprise tool system

- Frontend: React 18 + TypeScript + Vite + Ant Design Mobile
- Backend: PHP 8.5 + Webman 2.x + MySQL + Redis
- i18n support: Chinese, English, Russian, Spanish, French
- Approval workflow engine
- Docker development environment
- Feishu integration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-02-24 17:57:25 +08:00
commit 8d78e18267
121 changed files with 18563 additions and 0 deletions
@@ -0,0 +1,33 @@
.container {
background: var(--adm-color-background);
}
.header {
padding: 20px;
font-size: 16px;
font-weight: 600;
text-align: center;
border-bottom: 1px solid var(--adm-color-border);
}
.list {
width: 100%;
padding: 10px 0;
}
.item {
width: 100%;
padding: 12px 20px;
display: flex;
align-items: center;
gap: 12px;
}
.flag {
font-size: 20px;
}
.name {
font-size: 15px;
flex: 1;
}
@@ -0,0 +1,49 @@
import { Popup, Radio, Space } from 'antd-mobile'
import { useTranslation } from 'react-i18next'
import styles from './LanguageSwitcher.module.css'
const languages = [
{ code: 'zh', name: '简体中文', flag: '🇨🇳' },
{ code: 'en', name: 'English', flag: '🇬🇧' },
{ code: 'ru', name: 'Русский', flag: '🇷🇺' },
{ code: 'es', name: 'Español', flag: '🇪🇸' },
{ code: 'fr', name: 'Français', flag: '🇫🇷' },
]
export default function LanguageSwitcher({ onClose }: { onClose?: () => void }) {
const { i18n, t } = useTranslation()
const currentLang = i18n.language
const handleLanguageChange = (langCode: string) => {
i18n.changeLanguage(langCode)
onClose?.()
}
return (
<div className={styles.container}>
<div className={styles.header}>{t('settings.language')}</div>
<Radio.Group value={currentLang} onChange={(val) => handleLanguageChange(val)}>
<Space direction="vertical" className={styles.list}>
{languages.map((lang) => (
<Radio
key={lang.code}
value={lang.code}
className={styles.item}
checked={currentLang === lang.code}
>
<span className={styles.flag}>{lang.flag}</span>
<span className={styles.name}>{lang.name}</span>
</Radio>
))}
</Space>
</Radio.Group>
</div>
)
}
export function showLanguageSwitcher() {
Popup.show({
content: <LanguageSwitcher onClose={() => Popup.close()} />,
position: 'bottom',
})
}
@@ -0,0 +1,36 @@
.header {
position: sticky;
top: 0;
z-index: 100;
display: flex;
align-items: center;
height: 44px;
padding: 0 12px;
background: linear-gradient(180deg, #1677ff 0%, #0958d9 100%);
color: #fff;
}
.backBtn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
border: none;
background: transparent;
color: #fff;
cursor: pointer;
font-size: 20px;
}
.title {
flex: 1;
font-size: 17px;
font-weight: 500;
text-align: center;
}
.extra {
width: 32px;
}
+26
View File
@@ -0,0 +1,26 @@
import type { ReactNode } from 'react'
import { LeftOutline } from 'antd-mobile-icons'
import { useTranslation } from 'react-i18next'
import styles from './Header.module.css'
interface HeaderProps {
title?: string
onBack?: () => void
extra?: ReactNode
}
export default function Header({ title, onBack, extra }: HeaderProps) {
const { t } = useTranslation()
return (
<header className={styles.header}>
{onBack && (
<button className={styles.backBtn} onClick={onBack}>
<LeftOutline />
</button>
)}
<h1 className={styles.title}>{title || t('app.name')}</h1>
{extra && <div className={styles.extra}>{extra}</div>}
</header>
)
}
@@ -0,0 +1,24 @@
.layout {
display: flex;
flex-direction: column;
min-height: 100vh;
height: 100vh;
background-color: #f5f5f5;
overflow: hidden;
}
.main {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
/* TabBar 高度 50px + 安全区域 */
padding-bottom: calc(50px + env(safe-area-inset-bottom));
/* 确保滚动位置在顶部 */
scroll-behavior: smooth;
}
/* 重置滚动位置 */
.main::-webkit-scrollbar {
width: 0;
display: none;
}
@@ -0,0 +1,31 @@
import { Outlet, useNavigate, useLocation } from 'react-router-dom'
import { useEffect, useRef } from 'react'
import TabBar from './TabBar'
import Header from './Header'
import styles from './MobileLayout.module.css'
export default function MobileLayout() {
const navigate = useNavigate()
const location = useLocation()
const mainRef = useRef<HTMLDivElement>(null)
const showHeader = location.pathname !== '/login'
const showTabBar = !location.pathname.includes('/business-trip/')
// 路由变化时滚动到顶部
useEffect(() => {
if (mainRef.current) {
mainRef.current.scrollTop = 0
}
}, [location.pathname])
return (
<div className={styles.layout}>
{showHeader && <Header onBack={() => navigate(-1)} />}
<main className={styles.main} ref={mainRef}>
<Outlet />
</main>
{showTabBar && <TabBar />}
</div>
)
}
@@ -0,0 +1,36 @@
.tabBar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
display: flex;
height: 50px;
padding-bottom: env(safe-area-inset-bottom);
background: #fff;
border-top: 1px solid #eee;
}
.tab {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
color: #999;
cursor: pointer;
transition: color 0.2s;
}
.tab.active {
color: #1677ff;
}
.icon {
font-size: 22px;
}
.label {
font-size: 11px;
}
+47
View File
@@ -0,0 +1,47 @@
import { useNavigate, useLocation } from 'react-router-dom'
import { AppOutline, AppstoreOutline, UserOutline } from 'antd-mobile-icons'
import { useTranslation } from 'react-i18next'
import styles from './TabBar.module.css'
interface TabItem {
key: string
path: string
icon: React.ReactNode
labelKey: string
}
const tabs: TabItem[] = [
{ key: 'home', path: '/dashboard', icon: <AppOutline />, labelKey: 'tab.home' },
{ key: 'apps', path: '/apps', icon: <AppstoreOutline />, labelKey: 'tab.apps' },
{ key: 'profile', path: '/profile', icon: <UserOutline />, labelKey: 'tab.profile' },
]
export default function TabBar() {
const navigate = useNavigate()
const location = useLocation()
const { t } = useTranslation()
const activeTab = tabs.find((tab) => location.pathname.startsWith(tab.path))?.key || 'home'
const handleTabChange = (key: string) => {
const tab = tabs.find((t) => t.key === key)
if (tab) {
navigate(tab.path)
}
}
return (
<div className={styles.tabBar}>
{tabs.map((tab) => (
<div
key={tab.key}
className={`${styles.tab} ${activeTab === tab.key ? styles.active : ''}`}
onClick={() => handleTabChange(tab.key)}
>
<span className={styles.icon}>{tab.icon}</span>
<span className={styles.label}>{t(tab.labelKey)}</span>
</div>
))}
</div>
)
}