This commit is contained in:
expdsn 2025-02-10 19:20:12 +08:00
parent fbf3c4102d
commit a5637efccb
21 changed files with 1314 additions and 6513 deletions

View File

@ -41,5 +41,8 @@ export async function getArticleList({ page = 1, pageSize = 9999 }: {
}
export async function getArticle(id: string) {
const collection = await getCollection('article');
return collection.findOne<ArticleType>({ _id: new ObjectId(id) })
return collection.findOne({ _id: new ObjectId(id) }).then(res => res ? {
...res,
_id: res._id.toString()
} as ArticleType : null)
}

View File

@ -18,7 +18,7 @@ export async function getLinkTypeList({ page = 1, pageSize = 9999 }: {
}) {
const collection = await getCollection('link-type');
const startIndex = (page - 1) * pageSize;
const pipeline = [
const list = await collection.aggregate<LinkType>( [
{ $sort: { priority: 1 } },
{ $skip: startIndex },
{ $limit: pageSize },
@ -27,12 +27,10 @@ export async function getLinkTypeList({ page = 1, pageSize = 9999 }: {
_id: { $toString: "$_id" }
}
}
];
const cursor = collection.aggregate<LinkType>(pipeline);
const list = await cursor.toArray();
]).toArray();
// 计算总数量
const total = (await collection.find<LinkType>({}).toArray()).length
const total = await collection.countDocuments()
return {
total,
list

View File

@ -10,25 +10,34 @@ if (!uri) {
throw new Error('Please add your Mongo URI to.env.local');
}
if (process.env.NODE_ENV === 'development') {
// In development mode, use a global variable so that the value
// is preserved across module reloads caused by HMR (Hot Module Replacement).
if (!global._mongoClientPromise) {
client = new MongoClient(uri);
global._mongoClientPromise = client.connect();
}
clientPromise = global._mongoClientPromise;
} else {
// In production mode, it's best to not use a global variable.
client = new MongoClient(uri);
clientPromise = client.connect();
}
export const getDb = async (): Promise<Db> => {
const client = await clientPromise;
return client.db('ai-bot');
// client = new MongoClient(uri,{
// maxPoolSize: 5,
// maxConnecting:5,
// maxIdleTimeMS: 3000,
// waitQueueTimeoutMS: 1000
// });
// clientPromise = client.connect();
// export const getDb = async () => {
// const client = await clientPromise;
// return client.db('ai-bot')
// };
// export const getCollection = async (collection: string) => {
// const client = await clientPromise;
// return client.db('ai-bot').collection(collection);
// };
const ins = new MongoClient(uri,{
maxPoolSize: 5,
maxConnecting:5,
maxIdleTimeMS: 3000,
waitQueueTimeoutMS: 1000
}).db('ai-bot')
export const getDb = async () => {
return ins
};
export const getCollection = async (collection: string) => {
const client = await clientPromise;
return client.db('ai-bot').collection(collection);
return ins.collection(collection);
};

View File

@ -9,6 +9,7 @@ export default function ImageUpload(props: {
height?: number
value?: string
background?: string
hiddenCover?: boolean
onChange?: (val: string) => void
}) {
const inputRef = useRef<HTMLInputElement>(null)
@ -31,6 +32,9 @@ export default function ImageUpload(props: {
}
return (
<>
{
!props?.hiddenCover
&&
<div
className="bg-center bg-no-repeat bg-contain rounded-lg shadow-lg flex items-center"
style={{
@ -40,7 +44,9 @@ export default function ImageUpload(props: {
backgroundColor: props.background || "rgba(0,0,0,0.2)",
}}
/>
<Space className="mt-2">
}
<Space className={!props?.hiddenCover ? "mt-2" : ""}>
<Input value={props.value} onChange={(e) => {
props.onChange?.(e.target.value)

View File

@ -7,6 +7,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faClock, faFire, faTimeline, faTimesCircle, faTimesRectangle, faVolumeTimes } from "@fortawesome/free-solid-svg-icons";
import { useAtom } from "jotai";
import { linkTypeAtom } from "../_lib/atom";
import Image from "next/image";
export default function LinkListBox({ linkTypeList, linkList, showHot, showRecent }: { linkTypeList: LinkType[]; linkList: _Link[]; showHot: boolean; showRecent: boolean }) {
const hotList = useMemo(() => linkList.filter((val, index) => val.isHot && index < 12), [])

View File

@ -1,7 +1,7 @@
import { Link } from "@/app/_lib/data/link"
import { LinkType } from "@/app/_lib/data/linkType"
import { mRequest } from "@/app/_lib/request"
import ImageUpload from "@/app/_ui/ImageUpload"
import { Link } from "@/app/api/link/route"
import { LinkType } from "@/app/api/linkType/route"
import { useAntdTable, useRequest } from "ahooks"
import {
Button,

View File

@ -1,72 +0,0 @@
"use client"
import { ArticleType } from '@/app/_lib/data/article';
import ImageUpload from '@/app/_ui/ImageUpload';
import { faArrowLeft, faRibbon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { BackTop, Button, Card, Col, Form, Grid, Input, Row, Upload } from 'antd';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/navigation'
import { useState } from 'react';
const MDEditor = dynamic(() => import('@uiw/react-md-editor'), { ssr: false });
export default function Page() {
const router = useRouter()
return <>
<div className='w-full bg-white p-2 flex gap-x-1 items-center rounded shadow'>
<div className='flex gap-x-1 items-center text-[#666] hover:text-[#333] cursor-pointer '
onClick={() => {
router?.back()
}}
>
<FontAwesomeIcon icon={faArrowLeft}></FontAwesomeIcon>
</div>
</div>
<Card title="新增文章" className='mt-2'>
<Form<ArticleType>>
<Row gutter={24}>
<Col span={12}>
<Form.Item label="标题" name={"title"} >
<Input />
</Form.Item>
<Form.Item label="副标题" name={"description"}>
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="链接" name={"link"}>
<Input />
</Form.Item>
<Form.Item label="封面" name={"cover"}>
<ImageUpload
accept="image/*"
width={80}
height={80}
></ImageUpload>
</Form.Item>
</Col>
</Row>
<Form.Item label="内容" name={"content"}>
<MDEditor extraCommands={[
{
name: 'image',
icon: <span>1</span>, // 可以使用图标库的图标
execute: () => {
}, // 点击时调用插入图片的函数
},
]} />
</Form.Item>
</Form>
</Card>
</>
}

View File

@ -0,0 +1,150 @@
"use client"
import { ArticleType } from '@/app/_lib/data/article';
import ImageUpload from '@/app/_ui/ImageUpload';
import { faArrowLeft, faRibbon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { BackTop, Button, Card, Col, DatePicker, Form, Grid, Input, InputNumber, message, Row, Space, Upload } from 'antd';
import dynamic from 'next/dynamic';
import { useParams, useRouter } from 'next/navigation'
import { useState } from 'react';
// const MDEditor = dynamic(() => import('@uiw/react-md-editor'), { ssr: false });
import { MdEditor } from 'md-editor-rt';
import 'md-editor-rt/lib/style.css';
import '@ant-design/v5-patch-for-react-19';
import { mRequest } from '@/app/_lib/request';
import dayjs from 'dayjs';
export default function AddOrEdit({ editData }: { editData?: ArticleType | null }) {
const router = useRouter()
console.log(editData);
return <>
<div className='w-full bg-white p-2 flex gap-x-1 items-center rounded shadow'>
<div className='flex gap-x-1 items-center text-[#666] hover:text-[#333] cursor-pointer '
onClick={() => {
router?.back()
}}
>
<FontAwesomeIcon icon={faArrowLeft}></FontAwesomeIcon>
</div>
</div>
<Card title="新增文章" className='mt-2'>
<Form<ArticleType>
onFinish={async (res) => {
if (editData) {
await mRequest("PUT", "/api/article", {
...res,
_id: editData._id
})
} else {
await mRequest("POST", "/api/article", {
...res,
})
}
message.success("操作成功")
}}
initialValues={editData ? {
...editData,
addTime: dayjs(editData.addTime)
} : {
priority: 0,
addTime: dayjs()
}}
>
<Row gutter={24}>
<Col span={12}>
<Form.Item label="标题" name={"title"} rules={[{ required: true, message: '请输入标题' }]}>
<Input />
</Form.Item>
<Form.Item label="副标题" name={"description"}
rules={[
{
required: true,
message: '请输入副标题'
}
]}
>
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="链接" name={"link"}
rules={[
{
required: true,
message: '请输入链接'
}
]}
>
<Input />
</Form.Item>
<Form.Item label="封面" name={"cover"}
rules={[
{
required: true,
message: '请输入封面'
}
]}
>
<ImageUpload
accept="image/*"
width={80}
height={80}
hiddenCover
></ImageUpload>
</Form.Item>
</Col>
</Row>
<Row>
<Col span={12}>
<Form.Item label="修改时间" name={"addTime"}>
<DatePicker showTime />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="优先级" name={"priority"}>
<InputNumber></InputNumber>
</Form.Item>
</Col>
</Row>
<Form.Item label="内容" name={"content"} rules={[
{
required: true,
message: '请输入内容'
}
]}>
{/* <MDEditor className='my-markdown ' /> */}
<MdEditor />
</Form.Item>
<Form.Item className="flex justify-end">
<Space>
<Button>
</Button>
<Button type="primary" htmlType="submit">
</Button>
</Space>
</Form.Item>
</Form>
</Card>
</>
}

View File

@ -0,0 +1,13 @@
import { getArticle } from '@/app/_lib/data/article';
import AddOrEdit from '../AddOrEdit';
export default async function Page({
params,
}: { params: Promise<{ id: string }> }) {
const id = (await params).id
const data = await getArticle(id)
return <>
<AddOrEdit editData={data}></AddOrEdit>
</>
}

View File

@ -0,0 +1,12 @@
"use client"
import AddOrEdit from './AddOrEdit';
export default function Page() {
return <>
<AddOrEdit></AddOrEdit>
</>
}

View File

@ -1,36 +1,58 @@
"use client"
import { Button, Card, Space, Table } from "antd";
import { Button, Card, Image, Space, Table } from "antd";
import '@ant-design/v5-patch-for-react-19';
import { useRouter } from "next/navigation";
import { useAntdTable } from "ahooks";
import { mRequest } from "@/app/_lib/request";
import { ArticleType } from "@/app/_lib/data/article";
export default function Page() {
const router = useRouter()
const { tableProps, refresh } = useAntdTable(async ({ current, pageSize }) => mRequest<{
total: number;
list: ArticleType[]
}>('GET', `/api/article?page=${current}&pageSize=${pageSize}`))
return (
<Card title="文章管理" extra={<Button type="primary" onClick={() => {
router.push("/admin/dashboard/article/add")
<Card title="文章管理" extra={
<Space>
<Button onClick={() => refresh()}></Button>
<Button type="primary" onClick={() => {
router.push("/admin/dashboard/article/detail")
}} >
</Button>}>
</Button>
</Space>
}>
<Table
{...tableProps}
rowKey="_id"
columns={[
{
title: '标题',
dataIndex: 'title',
},
{
title: '作者',
dataIndex: 'author',
title: '副标题',
dataIndex: 'description',
},
{
title: '内容',
dataIndex: 'content',
title: "封面",
dataIndex: "content",
render: (_, item) => (
<>
<Image src={item.content} width={70} ></Image>
</>
)
},
{
title: '操作',
render: (_, row) => (
<Space>
<Button type="primary"></Button>
<Button type="primary"></Button>
<Button type="primary" onClick={() => {
router.push(`/admin/dashboard/article/detail/${row._id}`)
}} ></Button>
<Button type="primary" danger></Button>
</Space>
)
}

View File

@ -1,17 +0,0 @@
import SiderNav from "../_ui/SiderNav";
import { AIAppLinkList } from "./page";
export default function Layout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<div >
<SiderNav linkList={AIAppLinkList}></SiderNav>
{children}
</div>
);
}

View File

@ -1,14 +0,0 @@
import { faBackward } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
export const AIAppLinkList = [
{
label: '返回',
icon: <FontAwesomeIcon icon={faBackward} className="fa-fw " />,
href: '/',
id: 1,
},
]
export default function Page() {
return <div className="bg-red-300">AI Apps</div>
}

View File

@ -7,7 +7,7 @@ export async function GET(request: NextRequest) {
const page = parseInt(request.nextUrl.searchParams.get('page') || '1') || 1;
const pageSize = parseInt(request.nextUrl.searchParams.get('pageSize') || '10') || 10;
// 计算起始索引和结束索引
const res = getArticleList({ page, pageSize })
const res = await getArticleList({ page, pageSize })
return Response.json(res)
}
export async function POST(request: NextRequest) {

View File

@ -1,6 +1,4 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";
:root {
--background: #ffffff;
@ -13,7 +11,15 @@
--foreground: #ededed;
}
}
@layer base {
ol {
list-style-type: decimal;
margin: 1em 0;
padding-left: 2em;
}
}
body {
color: var(--foreground);
background: var(--background);
@ -24,7 +30,7 @@ body {
background-color: transparent !important;
color: rgba(0, 0, 0, 0.85) !important;
}
.ant-checkbox-disabled+span{
.ant-checkbox-disabled + span {
background-color: transparent !important;
color: rgba(0, 0, 0, 0.85) !important;
}

5895
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
@ -13,6 +13,7 @@
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@tailwindcss/postcss": "^4.0.4",
"@uiw/react-md-editor": "^4.0.5",
"ahooks": "^3.8.4",
"ali-oss": "^6.22.0",
@ -23,6 +24,7 @@
"icons": "link:@awesome.me/kit-KIT_CODE/icons",
"jose": "^5.9.6",
"jotai": "^2.11.1",
"md-editor-rt": "^5.2.2",
"mongodb": "^6.12.0",
"next": "15.1.4",
"proxy-agent": "^6.5.0",
@ -42,8 +44,8 @@
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.1.4",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"postcss": "^8.5.1",
"tailwindcss": "^4.0.4",
"typescript": "^5"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
"@tailwindcss/postcss": {},
},
};