完成了基本功能,网站热维护功能,完成测试基本的操作
This commit is contained in:
parent
115dc19e3d
commit
abfbfbb95e
|
@ -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);
|
||||
});
|
|
@ -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;
|
||||
});
|
||||
}
|
|
@ -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)
|
||||
}}
|
||||
/>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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>
|
||||
|
||||
|
@ -30,7 +38,7 @@ export default function SiderNav({ linkList }: { linkList: LinkTypeItem[] }) {
|
|||
})
|
||||
}
|
||||
</nav>
|
||||
|
||||
|
||||
</div >
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export default function Page() {
|
||||
return (
|
||||
<>ad</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export default function Page() {
|
||||
return (
|
||||
<>文章管理</>
|
||||
)
|
||||
}
|
|
@ -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>;
|
|
@ -0,0 +1,6 @@
|
|||
export default function Loading() {
|
||||
return <>
|
||||
|
||||
loading
|
||||
</>
|
||||
}
|
|
@ -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<{
|
||||
total: number;
|
||||
list: User[];
|
||||
}>('GET', `/api/links?page=${current}&pageSize=${pageSize}}`),
|
||||
{
|
||||
defaultPageSize: 10,
|
||||
}
|
||||
);
|
||||
const { tableProps, refresh } = useAntdTable(
|
||||
async ({ current, pageSize }) => {
|
||||
return mRequest<{
|
||||
total: number;
|
||||
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>
|
||||
}
|
||||
>
|
||||
<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 }}
|
||||
onFinish={(res) => {
|
||||
mRequest('POST', "/api/link").then(res => {
|
||||
setOpen(false)
|
||||
})
|
||||
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">
|
||||
<Button type="primary" htmlType="submit">
|
||||
确认
|
||||
</Button>
|
||||
<Space>
|
||||
<Button>
|
||||
重置
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit">
|
||||
确认
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
</Drawer>
|
||||
|
||||
</Drawer> */}
|
||||
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -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,16 +8,34 @@ export default function Layout({
|
|||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<SiderNav linkList={[
|
||||
{
|
||||
label: '链接管理',
|
||||
icon: <FontAwesomeIcon icon={faMagnet} className=" fa-fw"></FontAwesomeIcon>,
|
||||
id: 'add',
|
||||
href: '/admin'
|
||||
}
|
||||
]}></SiderNav>
|
||||
<SiderNav
|
||||
|
||||
linkList={[
|
||||
{
|
||||
label: '链接管理',
|
||||
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 ">
|
||||
<span className="font-bold text-xl">后台管理面板</span>
|
||||
|
|
|
@ -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 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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
})
|
||||
}
|
91
app/page.tsx
91
app/page.tsx
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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",
|
||||
|
|
675
pnpm-lock.yaml
675
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue