feat(project): init
This commit is contained in:
174
app/api/transfer/[path]/route.ts
Normal file
174
app/api/transfer/[path]/route.ts
Normal 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
BIN
app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
27
app/globals.css
Normal file
27
app/globals.css
Normal 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
68
app/layout.tsx
Normal 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
7
app/login/page.tsx
Normal 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
12
app/page.tsx
Normal 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
21
app/project/layout.tsx
Normal 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
5
app/project/own/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
"use client"
|
||||
|
||||
export default function ProjectOwnPage() {
|
||||
return <>Project Own</>
|
||||
}
|
||||
12
app/project/page.tsx
Normal file
12
app/project/page.tsx
Normal 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 <></>
|
||||
}
|
||||
5
app/project/setting/page.tsx
Normal file
5
app/project/setting/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
"use client"
|
||||
|
||||
export default function ProjectSettingPage() {
|
||||
return <>Project Setting</>
|
||||
}
|
||||
22
app/store/InfoCheck.tsx
Normal file
22
app/store/InfoCheck.tsx
Normal 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
85
app/store/user.ts
Normal 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
155
app/theme.ts
Normal 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",
|
||||
},
|
||||
}
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user