feat(project): init

This commit is contained in:
2026-03-10 17:06:22 +08:00
commit 1ae8509cce
70 changed files with 5141 additions and 0 deletions

View File

@@ -0,0 +1,174 @@
import {
handleDeleteCookie,
handleLoginData,
handleRefreshToken,
} from "@/components/login/libs/cookie"
import { genAccessToken } from "@/components/login/libs/session"
import { headers } from "next/headers"
import {
// NextRequest,
NextResponse,
} from "next/server"
const isParamsValid = (params: {} | null) =>
params !== null && // 排除 null
typeof params === "object" && // 确保是对象类型
!Array.isArray(params) && // 排除数组(可选)
Object.keys(params).length > 0 // 检查是否有自身可枚举属性
export async function POST(req: any) {
try {
const body = await req.json()
let {
url,
data,
options = {},
method = "GET",
isDownload = false,
isLogin = false,
isRefresh = false,
isDeleteCookie = false,
notJson = false,
} = body
if (isDeleteCookie) {
await handleDeleteCookie()
return new NextResponse(
JSON.stringify({
message: "Successfully delete cookie!",
code: 200,
status: true,
}),
{
headers: {
"Content-Type": "application/json",
},
}
)
}
const userData = await genAccessToken(req)
let fetchOptions: any = {}
if (isRefresh) {
if (!userData?.refresh_token) {
return NextResponse.json(
{ message: "Refresh token is empty", code: 406 },
{ status: 200 }
)
}
fetchOptions = {
method,
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
token: userData?.refresh_token,
}),
}
} else {
const authorization = `bearer ${userData?.access_token}`
fetchOptions = {
method,
headers: {
"content-type": "application/json",
authorization: authorization,
},
}
if (method === "POST" || method === "PUT") {
fetchOptions.body = data
} else {
if (isParamsValid(data)) {
let test = ""
for (const [key, value] of Object.entries(data)) {
test += `&${key}=${value}`
}
url += `?${test}`
}
}
}
const isPrefixUrl = url.startsWith("http://") || url.startsWith("https://")
const fetchUrl = isPrefixUrl ? url : `${process.env.NEXT_PUBLIC_URL}${url}`
const res = await fetch(fetchUrl, fetchOptions)
if (res?.status !== 200) {
let error = ""
if (res.headers.get("Content-Type")?.includes("application/json")) {
const data = await res.json()
error = data?.message || ""
} else {
error = await res.text()
}
return NextResponse.json({ message: error }, { status: res?.status })
}
if (
options?.responseType === "blob" ||
options?.responseType === "arraybuffer" ||
isDownload
) {
const blob = await res.blob()
const buffer = await blob.arrayBuffer()
return new NextResponse(buffer, {
headers: {
"Content-Type": blob.type,
"x-real-name": res?.headers?.get("content-disposition") || "",
},
})
} else if (!notJson) {
try {
const data = await (res.headers
.get("Content-Type")
?.includes("application/json")
? res.json()
: res.text())
if (isLogin) {
const header = await headers()
let clientIP =
header.get("x-forwarded-for")?.split(",")[0] ||
req?.socket?.remoteAddress
// ip v6地址
if (clientIP.startsWith("::")) {
clientIP = clientIP.slice(7)
}
const newData = await handleLoginData({
clientIP: clientIP,
fingerprint: body.fingerprint,
data,
})
return new NextResponse(JSON.stringify(newData), {
headers: {
"Content-Type": "application/json",
},
})
}
if (isRefresh) {
const newData = await handleRefreshToken({
userData,
data,
})
return new NextResponse(JSON.stringify(newData), {
headers: {
"Content-Type": "application/json",
},
})
}
return new NextResponse(JSON.stringify(data), {
headers: {
"Content-Type": "application/json",
},
})
} catch (err) {
return NextResponse.json({ error: err }, { status: 500 })
}
} else {
return res
}
} catch (err) {
return NextResponse.json({ error: err }, { status: 500 })
}
}

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

27
app/globals.css Normal file
View File

@@ -0,0 +1,27 @@
html,
body {
/* max-width: 100vw; */
overflow-x: hidden;
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}
/* 标准写法 */
:fullscreen::backdrop {
background: var(--mantine-color-body);
}
/* Safari / WebKit */
:-webkit-full-screen::backdrop {
background: var(--mantine-color-body);
}

68
app/layout.tsx Normal file
View File

@@ -0,0 +1,68 @@
"use client"
import "@mantine/core/styles.css"
import "@mantine/core/styles.layer.css"
import "@mantine/notifications/styles.css"
import "mantine-datatable/styles.css"
import Script from "next/script"
import { InfoCheck } from "@/app/store/InfoCheck"
import {
ColorSchemeScript,
MantineProvider,
// createTheme,
mantineHtmlProps,
} from "@mantine/core"
import { ModalsProvider } from "@mantine/modals"
import { Notifications } from "@mantine/notifications"
import { Suspense } from "react"
import "./globals.css"
import { theme } from "./theme"
const GLOBAL_NOTICE_Z_INDEX = 4000
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
// const theme = createTheme({
// colors: {
// blue: [
// "#e3f2fd", // 0
// "#bbdefb", // 1
// "#90caf9", // 2
// "#e8f0ff", // 3 ← dark默认主色
// "#42a5f5", // 4
// "#2196f3", // 5
// "#145bff", // 6 ← 默认主色
// "#1976d2", // 7
// "#1565c0", // 8
// "#0d47a1", // 9
// ],
// },
// })
return (
<html lang="en" {...mantineHtmlProps}>
<head>
<ColorSchemeScript defaultColorScheme="light" />
<Script
src="https://lf-scm-cn.feishucdn.com/lark/op/h5-js-sdk-1.5.44.js"
strategy="afterInteractive"
onLoad={() => {
console.log("飞书 SDK 加载完成", window.h5sdk)
}}
/>
</head>
<body>
<MantineProvider defaultColorScheme="light" theme={theme}>
<Notifications zIndex={GLOBAL_NOTICE_Z_INDEX} withinPortal />
<InfoCheck />
<ModalsProvider>
<Suspense fallback={<></>}>{children}</Suspense>
</ModalsProvider>
</MantineProvider>
</body>
</html>
)
}

7
app/login/page.tsx Normal file
View File

@@ -0,0 +1,7 @@
"use client"
import { useUserStore } from "../store/user"
import LoginComponent from "@/components/login"
export default function LoginPage() {
return <LoginComponent useUserStore={useUserStore} />
}

12
app/page.tsx Normal file
View File

@@ -0,0 +1,12 @@
"use client"
import { useRouter } from "next/navigation"
import { useEffect } from "react"
export default function Home() {
const router = useRouter()
useEffect(() => {
router.push("/project")
}, [router])
return <></>
}

21
app/project/layout.tsx Normal file
View File

@@ -0,0 +1,21 @@
import AppLayout from "@/components/layout/AppLayout"
import { PathnameRecorder } from "@/components/layout/PathnameRecorder"
import { componentList, showList } from "@/components/layout/common"
async function getMenu() {
return [...componentList, ...showList]
}
export default async function Layout({
children,
}: {
children: React.ReactNode
}) {
const menu = await getMenu()
return (
<AppLayout menu={menu}>
{children}
<PathnameRecorder menu={menu} />
</AppLayout>
)
}

5
app/project/own/page.tsx Normal file
View File

@@ -0,0 +1,5 @@
"use client"
export default function ProjectOwnPage() {
return <>Project Own</>
}

12
app/project/page.tsx Normal file
View File

@@ -0,0 +1,12 @@
"use client"
import { useRouter } from "next/navigation"
import { useEffect } from "react"
export default function ComponentPage() {
const router = useRouter()
useEffect(() => {
router.push("/project/own")
}, [router])
return <></>
}

View File

@@ -0,0 +1,5 @@
"use client"
export default function ProjectSettingPage() {
return <>Project Setting</>
}

22
app/store/InfoCheck.tsx Normal file
View File

@@ -0,0 +1,22 @@
"use client"
import { useUserStore } from "@/app/store/user"
import { usePathname } from "next/navigation"
import { useEffect } from "react"
export function InfoCheck() {
const pathname = usePathname()
const user_info = useUserStore.getState().user_info
useEffect(() => {
if (pathname.startsWith("/login")) {
return
}
if (!user_info.account) {
// tauri 打包后, 使用router.push跳转路由不执行会导致白屏
window.location.href = "/login"
}
}, [pathname, user_info.account])
return <></>
}

85
app/store/user.ts Normal file
View File

@@ -0,0 +1,85 @@
import { create } from "zustand"
import { persist } from "zustand/middleware"
interface RbacDataItem {
app: string
cat: string
platform: string
tenant: string
permission: {
[x: string]: {
[y: string]: [boolean, any, string]
}
}
}
interface UserState {
tenant: string
user_info: {
[x: string]: string
}
rbac: {
data: RbacDataItem[]
}
needResetPassword: boolean
access_token: string
refresh_token: string
setUserInfo: (
tenant: string,
user_info: {
[x: string]: string
},
rbac: {
data: RbacDataItem[]
}
) => void
removeUserInfo: () => void
refreshToken: (params: {
access_token: string
refresh_token: string
}) => void
setNeedResetPassword: (needResetPassword: boolean) => void
}
const initialUserState = {
tenant: "",
user_info: {},
rbac: {
data: [],
},
needResetPassword: false,
access_token: "",
refresh_token: "",
}
export const useUserStore = create(
persist<UserState>(
(set) => ({
tenant: initialUserState.tenant,
user_info: initialUserState.user_info,
rbac: initialUserState.rbac,
needResetPassword: initialUserState.needResetPassword,
access_token: initialUserState.access_token,
refresh_token: initialUserState.refresh_token,
setUserInfo: (tenant, user_info, rbac) =>
set((state) => {
return { ...state, tenant: tenant, user_info: user_info, rbac: rbac }
}),
removeUserInfo: () =>
set((state) => {
return { ...state, ...initialUserState }
}),
setNeedResetPassword: (needResetPassword) =>
set((state) => {
return { ...state, needResetPassword: needResetPassword }
}),
refreshToken: ({ access_token, refresh_token }) =>
set((state) => {
return { ...state, access_token, refresh_token }
}),
}),
{
name: "user-store",
}
)
)

155
app/theme.ts Normal file
View File

@@ -0,0 +1,155 @@
// theme.ts
import { Button, createTheme, MantineColorsTuple } from "@mantine/core"
/* -------- 1. 自定义颜色(可删/可改) -------- */
const brandColors: MantineColorsTuple = [
"#E8F4FC",
"#CEECFD",
"#A7DAFF",
"#69C0FF",
"#4ABAFF",
"#09ADFF",
"#00A1FF",
"#168DE8",
"#0D73D0",
"#0256A4",
]
const greyColors: MantineColorsTuple = [
"#FFFFFF",
"#F7F8F9",
"#F1F2F4",
"#DCDFE4",
"#B5BBC2",
"#8F979F",
"#5D6872",
"#47535F",
"#1B2A38",
"#0F151B",
]
const darkColors: MantineColorsTuple = [
"#FFFFFF",
"#EBEEF7",
"#D3D4D8",
"#7E858F",
"#3C4859",
"#2B3648",
"#212A39",
"#1A222D",
"#151C24",
"#0D0E12",
]
const successColors: MantineColorsTuple = [
"#E6FCF5",
"#C3FAE8",
"#96F2D7",
"#5BE7B6",
"#38D9A9",
"#20C997",
"#0EA879",
"#07815B",
"#076B3F",
"#043E22",
]
/* -------- 2. 创建主题 -------- */
export const theme = createTheme({
/* ---- 颜色系统 ---- */
primaryColor: "brand",
cursorType: "pointer",
// primaryShade: { light: 5, dark: 6 },
colors: {
brand: brandColors,
success: successColors,
grey: greyColors,
dark: darkColors,
},
/* ---- 圆角 ---- */
defaultRadius: "md",
radius: {
xs: "2px",
sm: "4px",
md: "8px",
lg: "12px",
xl: "16px",
xxl: "24px",
},
/* ---- 字体排版 ---- */
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
fontFamilyMonospace:
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
fontSizes: {
xs: "0.75rem",
sm: "0.875rem",
md: "1rem",
lg: "1.125rem",
xl: "1.25rem",
xxl: "1.5rem",
},
headings: {
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
fontWeight: "700",
sizes: {
h1: { fontSize: "2.25rem", lineHeight: "1.2" },
h2: { fontSize: "1.875rem", lineHeight: "1.3" },
h3: { fontSize: "1.5rem", lineHeight: "1.35" },
h4: { fontSize: "1.25rem", lineHeight: "1.4" },
h5: { fontSize: "1.125rem", lineHeight: "1.45" },
h6: { fontSize: "1rem", lineHeight: "1.5" },
},
},
/* ---- 间距 ---- */
spacing: {
xs: "8px",
sm: "12px",
md: "16px",
lg: "24px",
xl: "32px",
xxl: "48px",
},
/* ---- 阴影 ---- */
shadows: {
xs: "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
sm: "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)",
md: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
lg: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)",
xl: "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)",
},
/* ---- 响应式断点 ---- */
breakpoints: {
xs: "36em", // 576px
sm: "48em", // 768px
md: "62em", // 992px
lg: "75em", // 1200px
xl: "88em", // 1408px
},
/* ---- 默认渐变 ---- */
defaultGradient: {
from: "brand",
to: "success",
deg: 45,
},
/* ---- 组件默认行为(示例) ---- */
components: {
Button: Button.extend({
styles() {
return {
root: {
outline: "none",
},
}
},
}),
},
})