完成热门管理

This commit is contained in:
expdsn 2025-01-23 11:31:05 +08:00
parent abfbfbb95e
commit 0aab21db19
12 changed files with 172 additions and 113 deletions

5
app/_lib/atom.ts Normal file
View File

@ -0,0 +1,5 @@
import { atom } from 'jotai';
// 定义一个简单的原子
export const linkTypeAtom = atom('');

View File

@ -4,7 +4,7 @@ import { Link as _Link } from "../api/link/route";
import { LinkType } from "../api/linkType/route"; import { LinkType } from "../api/linkType/route";
export default function LinkListBox({ linkTypeList, linkList }: { linkTypeList: LinkType[]; linkList: _Link[] }) { export default function LinkListBox({ linkTypeList, linkList }: { linkTypeList: LinkType[]; linkList: _Link[] }) {
return <div className="flex w-full flex-col gap-y-2"> return <div className="flex w-full flex-col gap-y-4">
{ {
linkTypeList.map(item => ( linkTypeList.map(item => (
<div className="flex flex-col gap-y-2" key={item._id}> <div className="flex flex-col gap-y-2" key={item._id}>
@ -17,11 +17,11 @@ export default function LinkListBox({ linkTypeList, linkList }: { linkTypeList:
{ {
linkList.filter(val => val.type === item._id).map(val => ( 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" <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 || ''}> href={val.link || ''} target="_blank">
<img src={val.logoLink} className="w-[40px] h-[40px]"></img> <img src={val.logoLink} className="w-[40px] h-[40px]"></img>
<div className="flex-1 w-0 flex flex-col justify-between"> <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=" 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> <span className=" text-ellipsis overflow-hidden whitespace-nowrap text-[#666] text-xs" title={val.description}>{val.description}</span>
</div> </div>
</Link> </Link>
)) ))

View File

@ -82,7 +82,7 @@ export default function Search() {
} }
}, [activeSearchKey]) }, [activeSearchKey])
return ( return (
<div className="w-full flex justify-center flex-col items-center py-10"> <div className="w-full flex justify-center flex-col items-center py-10 ">
<div className="w-[200px]"> <div className="w-[200px]">
<Logo></Logo> <Logo></Logo>

View File

@ -5,17 +5,19 @@ import Logo from "./Logo";
import clsx from "clsx"; import clsx from "clsx";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { LinkType } from "../api/linkType/route"; import { LinkType } from "../api/linkType/route";
import { useAtom } from "jotai";
import { linkTypeAtom } from "../_lib/atom";
export default function SiderNav({ linkList }: { linkList: LinkType[] }) { export default function SiderNav({ linkList }: { linkList: LinkType[] }) {
const pathname = usePathname() const pathname = usePathname()
console.log(pathname); console.log(pathname);
const [selectType, setSelectType] = useAtom(linkTypeAtom)
return ( return (
<div className="w-[220px] flex flex-col gap-y-2 fixed left-0 top-0 h-[100vh] bg-[#F9F9F9]"> <div className="w-[220px] flex flex-col gap-y-2 fixed left-0 top-0 h-[100vh] bg-[#F9F9F9]">
<div> <div>
<Logo /> <Logo />
</div> </div>
<nav className="flex flex-col px-1"> <nav className="flex flex-col py-1">
{ {
linkList.map((item) => { linkList.map((item) => {
return ( return (
@ -28,7 +30,11 @@ export default function SiderNav({ linkList }: { linkList: LinkType[] }) {
} }
<span>{item.label}</span> <span>{item.label}</span>
</Link> : </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}> <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}
onClick={() => {
setSelectType(item._id)
}}
>
<img src={item.icon as string} className="w-[20px] h-[20px] object-cover"></img> <img src={item.icon as string} className="w-[20px] h-[20px] object-cover"></img>
<span>{item.label}</span> <span>{item.label}</span>

View File

@ -1,79 +0,0 @@
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

@ -1,6 +1,8 @@
import { mRequest } from "@/app/_lib/request" import { mRequest } from "@/app/_lib/request"
import ImageUpload from "@/app/_ui/ImageUpload" import ImageUpload from "@/app/_ui/ImageUpload"
import { Link } from "@/app/api/link/route" import { Link } from "@/app/api/link/route"
import { LinkType } from "@/app/api/linkType/route"
import { useRequest } from "ahooks"
import { import {
Button, Button,
Card, Card,
@ -11,15 +13,22 @@ import {
message, message,
Modal, Modal,
Popconfirm, Popconfirm,
Radio,
Select,
Space, Space,
Table, Table,
} from "antd" } from "antd"
import { useCallback, useEffect, useState } from "react" import { useCallback, useEffect, useState } from "react"
import useSWR from "swr"
export default function LinkTable(props: { id: string }) { export default function LinkTable(props: { id: string }) {
const [list, setList] = useState<Link[]>([]) const [list, setList] = useState<Link[]>([])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const { data: LinkTypeList } = useRequest(async () => mRequest<{
list: LinkType[]
}>('GET', '/api/linkType'))
const refresh = useCallback(async () => { const refresh = useCallback(async () => {
setLoading(true) setLoading(true)
const res = await mRequest<{ list: Link[] }>( const res = await mRequest<{ list: Link[] }>(
@ -68,7 +77,13 @@ export default function LinkTable(props: { id: string }) {
<Image width={80} src={row.logoLink}></Image> <Image width={80} src={row.logoLink}></Image>
) )
}, },
{
title: "是否热门",
dataIndex: "isHot",
render: (_, row) => (
row.isHot ? '是' : '否'
)
},
{ {
title: "操作", title: "操作",
fixed: "right", fixed: "right",
@ -81,9 +96,7 @@ export default function LinkTable(props: { id: string }) {
<Popconfirm <Popconfirm
title="确认删除?" title="确认删除?"
onConfirm={async () => { onConfirm={async () => {
await mRequest("DELETE", "/app/link/" + row.id, { await mRequest("DELETE", "/api/link/" + row._id,)
returnType: "text",
})
refresh() refresh()
message.success("删除成功") message.success("删除成功")
}} }}
@ -110,12 +123,14 @@ export default function LinkTable(props: { id: string }) {
initialValues={ initialValues={
selected selected
? selected ? selected
: { title: "", url: "", logoLink: "", description: "", priority: 0 } : { title: "", url: "", logoLink: "", description: "", priority: 0, type: props.id }
} }
onFinish={async (res) => { onFinish={async (res) => {
if (selected) { if (selected) {
await mRequest("PUT", "/api/link", { await mRequest("PUT", "/api/link", {
id: selected._id, ...res _id: selected._id,
...res,
type: props.id
}) })
} else { } else {
await mRequest("POST", "/api/link", { await mRequest("POST", "/api/link", {
@ -134,6 +149,9 @@ export default function LinkTable(props: { id: string }) {
> >
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item name="link" label="链接" rules={[{ required: true, message: "链接必填" }]}>
<Input />
</Form.Item>
<Form.Item <Form.Item
name="logoLink" name="logoLink"
label="图标名称" label="图标名称"
@ -145,15 +163,38 @@ export default function LinkTable(props: { id: string }) {
height={60} height={60}
></ImageUpload> ></ImageUpload>
</Form.Item> </Form.Item>
<Form.Item name="link" label="链接" rules={[{ required: true, message: "链接必填" }]}>
<Input /> <Form.Item name="description" label="详情"
</Form.Item> rules={[{ required: true, message: "详情必填" }]}
<Form.Item name="description" label="详情" > >
<Input.TextArea /> <Input.TextArea />
</Form.Item> </Form.Item>
<Form.Item name="type" label="分类"
rules={[{ required: true, message: "分类必填" }]}
>
<Select options={LinkTypeList?.list.map(item => ({
label: item.label,
value: item._id
}))}></Select>
</Form.Item>
<Form.Item name="priority" label="优先级"> <Form.Item name="priority" label="优先级">
<InputNumber></InputNumber> <InputNumber></InputNumber>
</Form.Item> </Form.Item>
<Form.Item name="isHot" label="是否热门">
<Radio.Group
options={[
{
label: '是',
value: 1
},
{
label: '否',
value: 0
}
]}
/>
</Form.Item>
<Form.Item className="flex justify-end"> <Form.Item className="flex justify-end">
<Button type="primary" htmlType="submit"> <Button type="primary" htmlType="submit">
@ -161,6 +202,6 @@ export default function LinkTable(props: { id: string }) {
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>
</div> </div >
) )
} }

View File

@ -11,6 +11,7 @@ import ImageUpload from "@/app/_ui/ImageUpload";
import { useForm } from "antd/es/form/Form"; import { useForm } from "antd/es/form/Form";
export default function Page() { export default function Page() {
const [form] = useForm()
const { tableProps, refresh } = useAntdTable( const { tableProps, refresh } = useAntdTable(
async ({ current, pageSize }) => { async ({ current, pageSize }) => {
return mRequest<{ return mRequest<{
@ -120,6 +121,7 @@ export default function Page() {
}} }}
> >
<Form <Form
form={form}
labelCol={{ span: 4 }} labelCol={{ span: 4 }}
initialValues={selectedType ? initialValues={selectedType ?
selectedType selectedType

View File

@ -0,0 +1,44 @@
import { getCollection, getDb } from "@/app/_lib/mongodb";
import { ObjectId } from "mongodb";
import { NextRequest } from "next/server";
import { Link } from "../route";
export async function GET(req: NextRequest) {
try {
const collection = await getCollection('link');
// 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 typeId = req.nextUrl.searchParams.get('typeId')
// 计算起始索引和结束索引
const startIndex = (page - 1) * pageSize;
// 查询数据
const cursor = collection.find<Link>({ type: typeId }).skip(startIndex).limit(pageSize);
const data = await cursor.toArray();
// 计算总数量
const total = (await collection.find<Link>({ type: typeId }).toArray()).length
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')
collection.deleteOne({
_id: new ObjectId(slug)
})
return Response.json({ message: '删除成功' })
} catch (e) {
return Response.error()
}
}

View File

@ -13,13 +13,14 @@ export type Link = {
type: string; type: string;
priority: number; priority: number;
logoLink: string; logoLink: string;
isHot?: boolean;
} }
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
try { try {
const collection = await getCollection('link'); const collection = await getCollection('link');
// Check if the user is authenticated // Check if the user is authenticated
const page = parseInt(req.nextUrl.searchParams.get('page') || '1') || 1; const page = parseInt(req.nextUrl.searchParams.get('page') || '1') || 1;
const pageSize = parseInt(req.nextUrl.searchParams.get('pageSize') || '10') || 10; const pageSize = parseInt(req.nextUrl.searchParams.get('pageSize') || '10') || 10;
const typeId = req.nextUrl.searchParams.get('typeId') const typeId = req.nextUrl.searchParams.get('typeId')
@ -52,26 +53,13 @@ export async function POST(req: NextRequest) {
return Response.error() 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) { export async function PUT(req: NextRequest) {
try { try {
// 获取待更新的对象 // 获取待更新的对象
const link = await req.json() as Link const link = await req.json() as Link
const collection = await getCollection('link') const collection = await getCollection('link')
await collection.updateOne({ _id: new ObjectId(link.id) }, { $set: link }) await collection.replaceOne({ _id: new ObjectId(link._id) }, { ...link, _id: new ObjectId(link._id) })
return Response.json({ message: '成功' }) return Response.json({ message: '成功' })
} catch (e) { } catch (e) {
return Response.error() return Response.error()

View File

@ -9,6 +9,7 @@ export type LinkType = {
iconElement?: ReactNode; iconElement?: ReactNode;
_id: string; _id: string;
href?: string; href?: string;
priority: number;
location?: string; location?: string;
} }

View File

@ -20,11 +20,13 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"icons": "link:@awesome.me/kit-KIT_CODE/icons", "icons": "link:@awesome.me/kit-KIT_CODE/icons",
"jose": "^5.9.6", "jose": "^5.9.6",
"jotai": "^2.11.1",
"mongodb": "^6.12.0", "mongodb": "^6.12.0",
"next": "15.1.4", "next": "15.1.4",
"proxy-agent": "^6.5.0", "proxy-agent": "^6.5.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"swr": "^2.3.0",
"uuid": "^11.0.5", "uuid": "^11.0.5",
"zod": "^3.24.1" "zod": "^3.24.1"
}, },

View File

@ -41,6 +41,9 @@ importers:
jose: jose:
specifier: ^5.9.6 specifier: ^5.9.6
version: 5.9.6 version: 5.9.6
jotai:
specifier: ^2.11.1
version: 2.11.1(@types/react@19.0.7)(react@19.0.0)
mongodb: mongodb:
specifier: ^6.12.0 specifier: ^6.12.0
version: 6.12.0(socks@2.8.3) version: 6.12.0(socks@2.8.3)
@ -56,6 +59,9 @@ importers:
react-dom: react-dom:
specifier: ^19.0.0 specifier: ^19.0.0
version: 19.0.0(react@19.0.0) version: 19.0.0(react@19.0.0)
swr:
specifier: ^2.3.0
version: 2.3.0(react@19.0.0)
uuid: uuid:
specifier: ^11.0.5 specifier: ^11.0.5
version: 11.0.5 version: 11.0.5
@ -950,6 +956,10 @@ packages:
delegates@1.0.0: delegates@1.0.0:
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
destroy@1.2.0: destroy@1.2.0:
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
@ -1524,6 +1534,18 @@ packages:
jose@5.9.6: jose@5.9.6:
resolution: {integrity: sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==} resolution: {integrity: sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==}
jotai@2.11.1:
resolution: {integrity: sha512-41Su098mpHIX29hF/XOpDb0SqF6EES7+HXfrhuBqVSzRkxX48hD5i8nGsEewWZNAsBWJCTTmuz8M946Ih2PfcQ==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@types/react': '>=17.0.0'
react: '>=17.0.0'
peerDependenciesMeta:
'@types/react':
optional: true
react:
optional: true
js-base64@2.6.4: js-base64@2.6.4:
resolution: {integrity: sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==} resolution: {integrity: sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==}
@ -2500,6 +2522,11 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
swr@2.3.0:
resolution: {integrity: sha512-NyZ76wA4yElZWBHzSgEJc28a0u6QZvhb6w0azeL2k7+Q1gAzVK+IqQYXhVOC/mzi+HZIozrZvBVeSeOZNR2bqA==}
peerDependencies:
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
tailwindcss@3.4.17: tailwindcss@3.4.17:
resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
@ -2607,6 +2634,11 @@ packages:
proxy-agent: proxy-agent:
optional: true optional: true
use-sync-external-store@1.4.0:
resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
util-deprecate@1.0.2: util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@ -3632,6 +3664,8 @@ snapshots:
delegates@1.0.0: {} delegates@1.0.0: {}
dequal@2.0.3: {}
destroy@1.2.0: {} destroy@1.2.0: {}
detect-libc@2.0.3: {} detect-libc@2.0.3: {}
@ -4393,6 +4427,11 @@ snapshots:
jose@5.9.6: {} jose@5.9.6: {}
jotai@2.11.1(@types/react@19.0.7)(react@19.0.0):
optionalDependencies:
'@types/react': 19.0.7
react: 19.0.0
js-base64@2.6.4: {} js-base64@2.6.4: {}
js-cookie@3.0.5: {} js-cookie@3.0.5: {}
@ -5490,6 +5529,12 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {} supports-preserve-symlinks-flag@1.0.0: {}
swr@2.3.0(react@19.0.0):
dependencies:
dequal: 2.0.3
react: 19.0.0
use-sync-external-store: 1.4.0(react@19.0.0)
tailwindcss@3.4.17: tailwindcss@3.4.17:
dependencies: dependencies:
'@alloc/quick-lru': 5.2.0 '@alloc/quick-lru': 5.2.0
@ -5642,6 +5687,10 @@ snapshots:
optionalDependencies: optionalDependencies:
proxy-agent: 6.5.0 proxy-agent: 6.5.0
use-sync-external-store@1.4.0(react@19.0.0):
dependencies:
react: 19.0.0
util-deprecate@1.0.2: {} util-deprecate@1.0.2: {}
utility@1.18.0: utility@1.18.0: