完成了基本功能,网站热维护功能,完成测试基本的操作

This commit is contained in:
expdsn 2025-01-22 17:37:56 +08:00
parent 115dc19e3d
commit abfbfbb95e
20 changed files with 1537 additions and 227 deletions

20
app/_lib/test/oss.ts Normal file
View File

@ -0,0 +1,20 @@
const OSS = require('ali-oss');
// 创建OSS实例
const client = new OSS({
accessKeyId: 'LTAI5tNzopZHJFa2Q9vqr1u5',
accessKeySecret: 'qPu7fyft0KJ1l6SGqbS71IW0vDbRlr'
// 先不指定region和bucket以便后续查询
});
// 列出所有Bucket
client.listBuckets().then((result) => {
console.log('所有的Bucket:', result.buckets);
// 打印每个Bucket的信息
result.buckets.forEach((bucket) => {
console.log('Bucket名称:', bucket.name);
console.log('Bucket所在区域:', bucket.location);
});
}).catch((error) => {
console.error('列出Bucket失败:', error);
});

34
app/_lib/upload.ts Normal file
View File

@ -0,0 +1,34 @@
import OSS from "ali-oss";
import { v4 as uuid } from "uuid";
import { mRequest } from "./request";
const ossBase = "https://aihlp.com.cn";
const accessKeyId = process.env.ALIYUN_RAM_ACCESS_KEY_ID || ''
const accessKeySecret = process.env.ALIYUN_RAM_ACCESS_KEY_SECRET || ''
console.log('id');
console.log(accessKeyId);
export default async function uploadOss(file: File, root: string) {
const path = `/admin/${root}/${uuid()}_${file.name}`;
const res = await mRequest<{
accessKeyId: string;
accessKeySecret: string;
region: string;
bucket: string;
}>('GET', '/api/uploadKey')
const client = new OSS({
...res
})
return client
.put(path, file, {
mime: file.type,
headers: {
"Content-Type": file.type,
},
})
.then((res) => {
console.log(res);
return ossBase + "/" + res.name;
});
}

76
app/_ui/ImageUpload.tsx Normal file
View File

@ -0,0 +1,76 @@
import { UploadOutlined } from "@ant-design/icons"
import { Button, Input, Space } from "antd"
import { useRef, useState } from "react"
import uploadOss from "../_lib/upload"
export default function ImageUpload(props: {
accept: string
width?: number
height?: number
value?: string
background?: string
onChange?: (val: string) => void
}) {
const inputRef = useRef<HTMLInputElement>(null)
const [loading, setLoading] = useState(false)
const handleFile = async (file: File) => {
setLoading(true)
try {
const url = await uploadOss(file, "aibot")
setLoading(false)
if (url) {
props.onChange?.(url)
}
} catch (e) {
setLoading(false)
}
}
return (
<>
<div
className="bg-center bg-no-repeat bg-contain rounded-lg shadow-lg flex items-center"
style={{
width: props.width || 240 + "px",
height: props.height || 120 + "px",
backgroundImage: `url('${props.value}')`,
backgroundColor: props.background || "rgba(0,0,0,0.2)",
}}
/>
<Space className="mt-2">
<Input value={props.value} onChange={(e) => {
props.onChange?.(e.target.value)
}}></Input>
<Button
loading={loading}
disabled={loading}
icon={<UploadOutlined />}
onClick={() => {
inputRef.current?.click()
}}
>
</Button>
</Space>
<input
type="file"
accept={props.accept}
ref={inputRef}
style={{
display: "none",
}}
onChange={(e) => {
const file = e.target.files?.[0]
e.target.value = ""
if (!file) return
handleFile(file)
}}
/>
</>
)
}

34
app/_ui/LinkListBox.tsx Normal file
View File

@ -0,0 +1,34 @@
"use client";
import Link from "next/link";
import { Link as _Link } from "../api/link/route";
import { LinkType } from "../api/linkType/route";
export default function LinkListBox({ linkTypeList, linkList }: { linkTypeList: LinkType[]; linkList: _Link[] }) {
return <div className="flex w-full flex-col gap-y-2">
{
linkTypeList.map(item => (
<div className="flex flex-col gap-y-2" key={item._id}>
<div className="flex items-center text-[#555555] gap-x-1 text-xl">
<img src={item.icon as string} className="w-[30px] h-[30px] object-cover"></img>
<span>{item.label}</span>
</div>
<div className=" grid grid-cols-3 lg:grid-cols-6 gap-4 ">
{
linkList.filter(val => val.type === item._id).map(val => (
<Link key={val._id} className="flex gap-x-2 bg-white rounded-lg py-4 pl-2 cursor-pointer duration-150 hover:-translate-y-1 shadow-sm"
href={val.link || ''}>
<img src={val.logoLink} className="w-[40px] h-[40px]"></img>
<div className="flex-1 w-0 flex flex-col justify-between">
<span className=" font-bold text-ellipsis overflow-hidden whitespace-nowrap">{val.name}</span>
<span className=" text-ellipsis overflow-hidden whitespace-nowrap text-[#666] text-xs">{val.description}</span>
</div>
</Link>
))
}
</div>
</div>
))
}
</div>
}

View File

@ -1,8 +1,15 @@
"use client";
import Link from "next/link";
import { LinkTypeItem } from "../_lib/types";
import Logo from "./Logo";
import clsx from "clsx";
import { usePathname } from "next/navigation";
import { LinkType } from "../api/linkType/route";
export default function SiderNav({ linkList }: { linkList: LinkType[] }) {
const pathname = usePathname()
console.log(pathname);
export default function SiderNav({ linkList }: { linkList: LinkTypeItem[] }) {
return (
<div className="w-[220px] flex flex-col gap-y-2 fixed left-0 top-0 h-[100vh] bg-[#F9F9F9]">
<div>
@ -13,16 +20,17 @@ export default function SiderNav({ linkList }: { linkList: LinkTypeItem[] }) {
linkList.map((item) => {
return (
item?.href ?
<Link className="cursor-pointer py-3 flex gap-x-2 items-center hover:bg-[#E0E0E0] rounded pl-3 text-[#515C6B] hover:text-[#5961F9] text-[14px]" href={item.href} key={item.id}>
<Link className={clsx("cursor-pointer py-3 flex gap-x-2 items-center hover:bg-[#E0E0E0] rounded pl-3 hover:text-[#5961F9] text-[14px]",
pathname === item.href ? "text-[#5961F9]" : "text-[#515C6B]")
} href={item.href} key={item._id}>
{
item.icon
item.iconElement
}
<span>{item.label}</span>
</Link> :
<div className="cursor-pointer py-3 flex gap-x-2 items-center hover:bg-[#E0E0E0] rounded pl-3 text-[#515C6B] hover:text-[#5961F9] text-[14px]" key={item.id}>
{
item.icon
}
<div className="cursor-pointer py-3 flex gap-x-2 items-center hover:bg-[#E0E0E0] rounded pl-3 text-[#515C6B] hover:text-[#5961F9] text-[14px]" key={item._id}>
<img src={item.icon as string} className="w-[20px] h-[20px] object-cover"></img>
<span>{item.label}</span>
</div>

View File

@ -0,0 +1,79 @@
import { Button, Form, Input, message } from "antd";
import { useEffect } from "react";
import { mRequest } from "@/app/_lib/request";
export default function EditIconContent({
item,
onRefresh,
}: {
item: | null;
onRefresh: () => void;
}) {
const [form] = Form.useForm();
console.log(item);
useEffect(() => {
if (item) {
form.setFieldsValue({ ...item });
} else {
form.resetFields();
}
}, [item, form]);
return (
<Form
layout="vertical"
form={form}
initialValues={{
// ...item,
// name: "",
// icons: [],
// link: "",
}}
onFinish={(res) => {
console.log(res);
mRequest(item ? "PUT" : "POST", "/app/linkIcon", {
data: item ? { id: item.id, ...res } : res,
returnType: "text",
}).then(() => {
message.success((item ? "修改" : "新增") + "成功");
onRefresh();
});
}}
>
<Form.Item
label="名称"
name="name"
rules={[{ required: true, message: "名称必填" }]}
>
<Input />
</Form.Item>
{
!item ?
<>
<Form.Item
label="链接名称"
name="linkName"
rules={[{ required: true, message: "链接必填" }]}
>
<Input />
</Form.Item>
<Form.Item
label="链接标签"
name="linkTag"
rules={[{ required: true, message: "链接必填" }]}
>
<Input />
</Form.Item>
</> : ''
}
<Form.Item className="flex justify-end">
<Button type="primary" htmlType="submit">
{item ? "修改" : "添加"}
</Button>
</Form.Item>
</Form>
);
}

View File

@ -0,0 +1,166 @@
import { mRequest } from "@/app/_lib/request"
import ImageUpload from "@/app/_ui/ImageUpload"
import { Link } from "@/app/api/link/route"
import {
Button,
Card,
Form,
Image,
Input,
InputNumber,
message,
Modal,
Popconfirm,
Space,
Table,
} from "antd"
import { useCallback, useEffect, useState } from "react"
export default function LinkTable(props: { id: string }) {
const [list, setList] = useState<Link[]>([])
const [loading, setLoading] = useState(false)
const refresh = useCallback(async () => {
setLoading(true)
const res = await mRequest<{ list: Link[] }>(
"GET",
`/api/link?typeId=${props.id}&page=1&pageSize=9999`
)
setList(res.list)
setLoading(false)
}, [props.id])
useEffect(() => {
refresh()
}, [refresh])
const [selected, setSelected] = useState<undefined | null | Link>(
undefined
)
return (
<div className="p-4">
<Card
title="链接列表"
extra={
<Space>
<Button onClick={refresh}>
</Button>
<Button type="primary" onClick={() => setSelected(null)}>
</Button>
</Space>
}
>
<Table<Link>
loading={loading}
dataSource={list}
pagination={false}
rowKey="_id"
columns={[
{
title: "名称",
dataIndex: "name",
},
{
title: "图标",
dataIndex: "logoLink",
render: (_, row) => (
<Image width={80} src={row.logoLink}></Image>
)
},
{
title: "操作",
fixed: "right",
width: 200,
render: (_, row) => (
<Space>
<Button type="link" onClick={() => setSelected(row)}>
</Button>
<Popconfirm
title="确认删除?"
onConfirm={async () => {
await mRequest("DELETE", "/app/link/" + row.id, {
returnType: "text",
})
refresh()
message.success("删除成功")
}}
>
<Button type="link" danger>
</Button>
</Popconfirm>
</Space>
),
},
]}
/>
</Card>
<Modal
open={selected !== undefined}
onCancel={() => setSelected(undefined)}
destroyOnClose
title={(selected ? "编辑" : "新增") + "链接"}
footer={false}
>
<Form
labelCol={{ span: 4 }}
initialValues={
selected
? selected
: { title: "", url: "", logoLink: "", description: "", priority: 0 }
}
onFinish={async (res) => {
if (selected) {
await mRequest("PUT", "/api/link", {
id: selected._id, ...res
})
} else {
await mRequest("POST", "/api/link", {
...res, type: props.id
})
}
refresh()
setSelected(undefined)
message.success("操作成功")
}}
>
<Form.Item
name="name"
label="图标名称"
rules={[{ required: true, message: "名称必填" }]}
>
<Input />
</Form.Item>
<Form.Item
name="logoLink"
label="图标名称"
rules={[{ required: true, message: "名称必填" }]}
>
<ImageUpload
accept="image/*"
width={60}
height={60}
></ImageUpload>
</Form.Item>
<Form.Item name="link" label="链接" rules={[{ required: true, message: "链接必填" }]}>
<Input />
</Form.Item>
<Form.Item name="description" label="详情" >
<Input.TextArea />
</Form.Item>
<Form.Item name="priority" label="优先级">
<InputNumber></InputNumber>
</Form.Item>
<Form.Item className="flex justify-end">
<Button type="primary" htmlType="submit">
</Button>
</Form.Item>
</Form>
</Modal>
</div>
)
}

View File

@ -0,0 +1,5 @@
export default function Page() {
return (
<>ad</>
)
}

View File

@ -0,0 +1,5 @@
export default function Page() {
return (
<></>
)
}

View File

@ -1,30 +0,0 @@
import { Space } from 'antd';
import { ColumnsType } from 'antd/es/table';
export default [
{
title: '姓名',
dataIndex: 'name',
key: 'name',
},
{
title: '年龄',
dataIndex: 'age',
key: 'age',
},
{
title: '地址',
dataIndex: 'address',
key: 'address',
},
{
title: 'Action',
key: 'action',
// eslint-disable-next-line @typescript-eslint/no-unused-vars
render: (_, record) => (
<Space size="middle" >
< a > Delete </a>
</Space>
),
},
] as ColumnsType<unknown>;

View File

@ -0,0 +1,6 @@
export default function Loading() {
return <>
loading
</>
}

View File

@ -1,132 +1,252 @@
"use client";
import { Button, Card, Drawer, Form, Input, Space, Table } from "antd";
import { useState } from "react";
import { Button, Card, Drawer, Form, Image, Input, message, Modal, Popconfirm, Space, Table } from "antd";
import { useRef, useState } from "react";
import '@ant-design/v5-patch-for-react-19';
import tableConfig from "./config"
import { useAntdTable } from "ahooks";
import { User } from "@/app/_lib/data/user";
import { mRequest } from "@/app/_lib/request";
interface DataType {
key: string;
name: string;
age: number;
address: string;
tags: string[];
}
const data: DataType[] = [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
tags: ['nice', 'developer'],
},
{
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
tags: ['loser'],
},
{
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sydney No. 1 Lake Park',
tags: ['cool', 'teacher'],
},
];
import { LinkTypeItem } from "@/app/_lib/types";
import { LinkType } from "@/app/api/linkType/route";
import LinkTable from "./LinkTable";
import ImageUpload from "@/app/_ui/ImageUpload";
import { useForm } from "antd/es/form/Form";
export default function Page() {
const [open, setOpen] = useState(false)
const { tableProps, loading, error } = useAntdTable(({ current, pageSize }) => mRequest<{
const { tableProps, refresh } = useAntdTable(
async ({ current, pageSize }) => {
return mRequest<{
total: number;
list: User[];
}>('GET', `/api/links?page=${current}&pageSize=${pageSize}}`),
{
defaultPageSize: 10,
data: LinkTypeItem[]
}>(
"GET",
`/api/linkType?page=${current}&pageSize=${pageSize}`
).then((res: any) => {
return {
total: res?.total || 0,
list: res?.list || [],
}
);
})
},
)
const [selectedType, setSelectedType] = useState<undefined | null | LinkType>(
undefined
)
return (
<>
<Card
title="链接管理"
title="链接管理"
extra={
<Button type="primary" onClick={() => {
setOpen(true)
}}></Button>
}>
<Table
columns={tableConfig}
dataSource={data}
/>
</Card>
<Drawer
title={'添加图标'}
open={open}
width={500}
onClose={() => {
setOpen(false)
}}
footer={
<Space>
<Button></Button>
<Button type="primary"></Button>
<Button onClick={() => {
refresh()
}}></Button>
<Button
type="primary"
onClick={() => {
setSelectedType(null)
}}
>
</Button>
</Space>
}
>
<Form
labelCol={{ span: 4 }}
onFinish={(res) => {
mRequest('POST', "/api/link").then(res => {
setOpen(false)
<Table<LinkType>
{...tableProps}
rowKey="_id"
expandable={{
expandedRowRender: (row) => <LinkTable id={row._id} />,
}}
columns={[
{
title: "名称",
dataIndex: "label",
},
{
title: "图标",
dataIndex: "icon",
render: (_, item) => (
<>
<Image src={item.icon} width={70} ></Image>
</>
)
},
{
title: "操作",
render: (_, item) => (
<Space>
<Button
type="primary"
ghost
size="small"
onClick={() => {
setSelectedType(item)
}}
>
</Button>
<Popconfirm
title={`确认要删除${item.label}么?`}
onConfirm={() => {
mRequest("DELETE", "/api/linkType/" + item._id, {
returnType: "text",
}).then(() => {
refresh()
message.success("删除成功")
})
}}
>
<Button type="primary" danger size="small">
</Button>
</Popconfirm>
</Space>
),
},
]}
/>
</Card>
<Drawer
destroyOnClose
title={selectedType !== undefined ? selectedType === null ? "添加分类" : "修改分类" : ""}
open={selectedType !== undefined}
width={500}
onClose={() => {
setSelectedType(undefined)
}}
>
<Form
labelCol={{ span: 4 }}
initialValues={selectedType ?
selectedType
: {}}
onFinish={async (res) => {
if (selectedType) {
await mRequest("PUT", "/api/linkType", {
_id: selectedType._id, ...res
})
} else {
await mRequest("POST", "/api/linkType", {
...res,
})
}
refresh()
setSelectedType(undefined)
message.success("操作成功")
}}
>
<Form.Item
name="name"
name="label"
label="名称"
rules={[{ required: true, message: "请输入名称" }]}
>
<Input />
</Form.Item>
<Form.Item
name="desc"
label="关键词"
name="icon"
label="图标"
rules={[{ required: true, message: "请输入" }]}
>
<ImageUpload
accept="image/*"
width={80}
height={80}
></ImageUpload>
</Form.Item>
<Form.Item
name="location"
label="显示位置"
>
<Input />
</Form.Item>
<Form.Item className="flex justify-end">
<Space>
<Button>
</Button>
<Button type="primary" htmlType="submit">
</Button>
</Space>
</Form.Item>
</Form>
</Drawer>
{/* <Drawer
forceRender
title={'添加图标'}
open={selectedType !== undefined}
width={500}
onClose={() => {
setSelectedType(undefined)
}}
>
<Form
form={fo}
labelCol={{ span: 4 }}
onFinish={async (res) => {
if (selectedType) {
await mRequest("PUT", "/api/linkType", {
_id: selectedType._id, ...res
})
} else {
await mRequest("POST", "/api/linkType", {
...res,
})
}
refresh()
setSelectedType(undefined)
message.success("操作成功")
}}
>
<Form.Item
name="label"
label="名称"
rules={[{ required: true, message: "请输入名称" }]}
>
<Input />
</Form.Item>
<Form.Item
name="url"
label="链接"
name="icon"
label="图标"
rules={[{ required: true, message: "请输入" }]}
>
<ImageUpload
accept="image/*"
width={60}
height={60}
background={fo?.getFieldValue("icon ")}
></ImageUpload>
</Form.Item>
<Form.Item
name="location"
label="显示位置"
rules={[{ required: true, message: "请输入链接" }]}
>
<Input />
</Form.Item>
{/* <Form.Item
name="icon"
label="图标"
rules={[{ required: true, message: "请上传图标" }]}
>
<ResourceUpload></ResourceUpload>
</Form.Item> */}
<Form.Item className="flex justify-end">
<Space>
<Button>
</Button>
<Button type="primary" htmlType="submit">
</Button>
</Space>
</Form.Item>
</Form>
</Drawer>
</Drawer> */}
</>
)

View File

@ -1,6 +1,6 @@
import LoginState from "@/app/_ui/LoginState";
import SiderNav from "../../_ui/SiderNav";
import { faMagnet } from "@fortawesome/free-solid-svg-icons"
import { faAd, faArrowAltCircleLeft, faMagnet, faPenClip } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
export default function Layout({
@ -8,15 +8,33 @@ export default function Layout({
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<div className="flex flex-col">
<SiderNav linkList={[
<SiderNav
linkList={[
{
label: '链接管理',
icon: <FontAwesomeIcon icon={faMagnet} className=" fa-fw"></FontAwesomeIcon>,
id: 'add',
href: '/admin'
}
iconElement: <FontAwesomeIcon icon={faMagnet} className=" fa-fw"></FontAwesomeIcon>,
_id: 'addLink',
href: '/admin/dashboard'
},
{
label: '广告管理',
iconElement: <FontAwesomeIcon icon={faAd}></FontAwesomeIcon>,
_id: 'adMenagement',
href: '/admin/dashboard/ad'
},
{
label: '文章管理',
iconElement: <FontAwesomeIcon icon={faPenClip}></FontAwesomeIcon>,
_id: 'articleMenagement',
href: '/admin/dashboard/article'
},
]}></SiderNav>
<div>
<div className="h-[50px] bg-white/80 shadow flex items-center justify-between px-5 ">

View File

@ -3,12 +3,13 @@ import { User } from "@/app/_lib/data/user";
import { getCollection, getDb } from "@/app/_lib/mongodb";
import { message } from "antd";
import bcrypt from 'bcrypt';
import { ObjectId } from "mongodb";
import { NextRequest } from "next/server";
export type Link = {
title: string;
url?: string;
name: string;
link?: string;
description: string;
id: number;
_id: string;
type: string;
priority: number;
logoLink: string;
@ -18,31 +19,25 @@ export async function GET(req: NextRequest) {
try {
const collection = await getCollection('link');
// Check if the user is authenticated
const session = await verifySession()
if (!session) {
// User is not authenticated
return new Response(null, { status: 401 })
}
// 获取分页参数
const page = parseInt(req.nextUrl.searchParams.get('page') || '1') || 1;
const pageSize = parseInt(req.nextUrl.searchParams.get('page') || '10') || 10;
const page = parseInt(req.nextUrl.searchParams.get('page') || '1') || 1;
const pageSize = parseInt(req.nextUrl.searchParams.get('pageSize') || '10') || 10;
const typeId = req.nextUrl.searchParams.get('typeId')
// 计算起始索引和结束索引
const startIndex = (page - 1) * pageSize;
// 查询数据
const cursor = collection.find<Link>({}).skip(startIndex).limit(pageSize);
const cursor = collection.find<Link>({ type: typeId }).skip(startIndex).limit(pageSize);
const data = await cursor.toArray();
// 计算总数量
const total = await collection.countDocuments();
const total = (await collection.find<Link>({ type: typeId }).toArray()).length
return Response.json({
total,
list: data,
})
} catch (e) {
console.log(e);
return Response.error()
}
}
@ -54,7 +49,31 @@ export async function POST(req: NextRequest) {
await collection.insertOne(link)
return Response.json({ message: '成功' })
} catch (e) {
console.log(e);
return Response.error()
}
}
export async function DELETE(req: NextRequest) {
try {
// 获取路径参数
const segments = req.nextUrl.pathname.split('/')
const dynamicParam = segments[segments.length - 1]
const collection = await getCollection('link')
collection.deleteOne({
_id: new ObjectId(dynamicParam)
})
return Response.json({ message: '删除成功' })
} catch (e) {
return Response.error()
}
}
export async function PUT(req: NextRequest) {
try {
// 获取待更新的对象
const link = await req.json() as Link
const collection = await getCollection('link')
await collection.updateOne({ _id: new ObjectId(link.id) }, { $set: link })
return Response.json({ message: '成功' })
} catch (e) {
return Response.error()
}
}

View File

@ -0,0 +1,51 @@
import { verifySession } from "@/app/_lib/dal";
import { getCollection, getDb } from "@/app/_lib/mongodb";
import { ObjectId } from "mongodb";
import { NextRequest } from "next/server";
import { LinkType } from "../route";
export async function GET(req: NextRequest) {
try {
const collection = await getCollection('link-type');
// Check if the user is authenticated
const session = await verifySession()
if (!session) {
// User is not authenticated
return new Response(null, { status: 401 })
}
// 获取分页参数
const page = parseInt(req.nextUrl.searchParams.get('page') || '1') || 1;
const pageSize = parseInt(req.nextUrl.searchParams.get('pageSize') || '10') || 10;
// 计算起始索引和结束索引
const startIndex = (page - 1) * pageSize;
// 查询数据
const cursor = collection.find<LinkType>({}).skip(startIndex).limit(pageSize);
const data = await cursor.toArray();
// 计算总数量
const total = await collection.countDocuments();
return Response.json({
total,
list: data,
})
} catch (e) {
return Response.error()
}
}
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
// 获取路径参数
const slug = (await params).id
const collection = await getCollection('link-type')
collection.deleteOne({
_id: new ObjectId(slug)
})
return Response.json({ message: '删除成功' })
} catch (e) {
return Response.error()
}
}

66
app/api/linkType/route.ts Normal file
View File

@ -0,0 +1,66 @@
import { verifySession } from "@/app/_lib/dal";
import { getCollection, getDb } from "@/app/_lib/mongodb";
import { ObjectId } from "mongodb";
import { NextRequest } from "next/server";
import { ReactNode } from "react";
export type LinkType = {
label: string;
icon?: string;
iconElement?: ReactNode;
_id: string;
href?: string;
location?: string;
}
export async function GET(req: NextRequest) {
try {
const collection = await getCollection('link-type');
// Check if the user is authenticated
// 获取分页参数
const page = parseInt(req.nextUrl.searchParams.get('page') || '1') || 1;
const pageSize = parseInt(req.nextUrl.searchParams.get('pageSize') || '10') || 10;
// 计算起始索引和结束索引
const startIndex = (page - 1) * pageSize;
// 查询数据
const cursor = collection.find<LinkType>({}).skip(startIndex).limit(pageSize);
const data = await cursor.toArray();
// 计算总数量
const total = await collection.countDocuments();
return Response.json({
total,
list: data,
})
} catch (e) {
return Response.error()
}
}
export async function POST(req: NextRequest) {
try {
// 获取待插入的对象
const link = await req.json()
const collection = await getCollection('link-type')
await collection.insertOne(link)
return Response.json({ message: '成功' })
} catch (e) {
return Response.error()
}
}
export async function PUT(req: NextRequest) {
try {
// 获取待更新的对象
const link = await req.json() as LinkType
const collection = await getCollection('link-type')
await collection.replaceOne({ _id: new ObjectId(link._id) }, { ...link, _id: new ObjectId(link._id) })
return Response.json({ message: '成功' })
} catch (e) {
console.log(e);
return Response.error()
}
}

View File

@ -0,0 +1,10 @@
export async function GET() {
const accessKeyId = process.env.ALIYUN_RAM_ACCESS_KEY_ID || ''
const accessKeySecret = process.env.ALIYUN_RAM_ACCESS_KEY_SECRET || ''
return Response.json({
accessKeyId: accessKeyId,
accessKeySecret: accessKeySecret,
region: 'oss-cn-hangzhou',
bucket: 'newuitab'
})
}

View File

@ -5,70 +5,30 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowRotateBack, faDeafness, faImage, faMagnet, faMessage, faPenClip, faSearch, faThumbsUp, faVideo } from '@fortawesome/free-solid-svg-icons'
import { LinkTypeItem } from "./_lib/types";
import PosterBox from "./_ui/PosterBox";
import { mRequest } from "./_lib/request";
import { LinkType } from "./api/linkType/route";
import { getCollection } from "./_lib/mongodb";
import { Link as _Link } from "./api/link/route";
import Link from "next/link";
import LinkListBox from "./_ui/LinkListBox";
const defaultLinkList = [
{
label: 'AI应用集',
icon: <FontAwesomeIcon icon={faThumbsUp} className="fa-fw text-[20px]" />,
href: '/ai-apps',
id: 1,
},
{
label: 'AI写作工具',
icon: <FontAwesomeIcon icon={faPenClip} className="fa-fw text-[20px]" />,
href: '/?type=ai-writing',
id: 2,
},
{
label: 'AI图像工具',
icon: <FontAwesomeIcon icon={faImage} className="fa-fw text-[20px]" />,
href: '/?type=ai-image',
id: 3,
},
{
label: 'AI办公工具',
icon: <FontAwesomeIcon icon={faArrowRotateBack} className="fa-fw text-[20px]" />,
href: '/?type=ai-office',
id: 4,
},
{
label: 'AI对话聊天',
icon: <FontAwesomeIcon icon={faMessage} className="fa-fw text-[20px]" />,
href: '/?type=ai-chat',
id: 5,
},
{
label: 'AI编程工具',
icon: <FontAwesomeIcon icon={faMagnet} className="fa-fw text-[20px]" />,
href: '/?type=ai-programming',
id: 6,
},
{
label: 'AI搜索引擎',
icon: <FontAwesomeIcon icon={faSearch} className="fa-fw text-[20px]" />,
href: '/?type=ai-search',
id: 7,
},
{
label: 'AI音频工具',
icon: <FontAwesomeIcon icon={faVideo} className="fa-fw text-[20px]" />,
href: '/?type=ai-audio',
id: 8,
},
{
label: 'AI开发平台',
icon: <FontAwesomeIcon icon={faDeafness} className="fa-fw text-[20px]" />,
href: '/?type=ai-platform',
id: 9,
}
] as LinkTypeItem[];
export default async function Home() {
const collection = await getCollection('link-type')
const result = (await collection.find<LinkType>({}).toArray())
const linkTypeList = result.map(doc => {
doc._id = doc._id.toString(); // 将 _id 转换为字符串
return doc;
});
const linkCollect = await getCollection('link')
const linkList = (await linkCollect.find<_Link>({}).toArray()).map(doc => {
doc._id = doc._id.toString(); // 将 _id 转换为字符串
return doc;
})
return (
<div className="flex min-h-full w-full font-[family-name:var(--font-geist-sans)] relative">
<SiderNav linkList={defaultLinkList} />
<SiderNav linkList={linkTypeList} />
<div className="absolute -z-10 from-[#E6EEF4] h-[50vh] w-full bg-gradient-to-br via-[#F1ECF4] to-[#F5ECEA]">
<div className="absolute z-10 from-[#F9F9F9] left-0 to-transparent bg-gradient-to-t w-full h-[100px] bottom-[0px]">
@ -80,20 +40,7 @@ export default async function Home() {
<HeaderNav></HeaderNav>
<Search></Search>
<PosterBox posterList={[]} />
<div className="flex w-full flex-col">
{
defaultLinkList.map(item => (
<div className="flex flex-col" key={item.id}>
<div className="flex items-center text-[#555555] ">
{item.icon}
<span>{item.label}</span>
</div>
</div>
))
}
</div>
<LinkListBox linkList={linkList} linkTypeList={linkTypeList}></LinkListBox>
</main>
</div>
);

View File

@ -9,10 +9,12 @@
"lint": "next lint"
},
"dependencies": {
"@ant-design/icons": "^5.5.2",
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"ahooks": "^3.8.4",
"ali-oss": "^6.22.0",
"antd": "^5.23.2",
"bcrypt": "^5.1.1",
"clsx": "^2.1.1",
@ -20,13 +22,16 @@
"jose": "^5.9.6",
"mongodb": "^6.12.0",
"next": "15.1.4",
"proxy-agent": "^6.5.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"uuid": "^11.0.5",
"zod": "^3.24.1"
},
"devDependencies": {
"@ant-design/v5-patch-for-react-19": "^1.0.3",
"@eslint/eslintrc": "^3",
"@types/ali-oss": "^6.16.11",
"@types/bcrypt": "^5.0.2",
"@types/node": "^20",
"@types/react": "^19",

File diff suppressed because it is too large Load Diff