mirror of
https://github.com/wgh136/nysoure.git
synced 2025-09-27 12:17:24 +00:00
Compare commits
4 Commits
2288926e31
...
0c841f2723
Author | SHA1 | Date | |
---|---|---|---|
0c841f2723 | |||
63b3a075c8 | |||
d35473c905 | |||
25720f5e49 |
@@ -236,7 +236,8 @@ export const i18nData = {
|
|||||||
"Collection created successfully": "合集创建成功",
|
"Collection created successfully": "合集创建成功",
|
||||||
"Collection deleted successfully": "合集删除成功",
|
"Collection deleted successfully": "合集删除成功",
|
||||||
"Remove Resource": "移除资源",
|
"Remove Resource": "移除资源",
|
||||||
"Are you sure you want to remove this resource?": "您确定要移除此资源吗?",
|
"Are you sure you want to remove this resource?":
|
||||||
|
"您确定要移除此资源吗?",
|
||||||
"Resource deleted successfully": "资源移除成功",
|
"Resource deleted successfully": "资源移除成功",
|
||||||
"Edit Collection": "编辑合集",
|
"Edit Collection": "编辑合集",
|
||||||
"Edit successful": "编辑成功",
|
"Edit successful": "编辑成功",
|
||||||
@@ -250,7 +251,9 @@ export const i18nData = {
|
|||||||
"Update File Info": "更新文件信息",
|
"Update File Info": "更新文件信息",
|
||||||
"File info updated successfully": "文件信息更新成功",
|
"File info updated successfully": "文件信息更新成功",
|
||||||
"File URL": "文件URL",
|
"File URL": "文件URL",
|
||||||
"You do not have permission to upload files, please contact the administrator.": "您没有上传文件的权限,请联系管理员。",
|
"You do not have permission to upload files, please contact the administrator.":
|
||||||
|
"您没有上传文件的权限,请联系管理员。",
|
||||||
|
"Private": "私有",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"zh-TW": {
|
"zh-TW": {
|
||||||
@@ -490,7 +493,8 @@ export const i18nData = {
|
|||||||
"Collection created successfully": "合集創建成功",
|
"Collection created successfully": "合集創建成功",
|
||||||
"Collection deleted successfully": "合集刪除成功",
|
"Collection deleted successfully": "合集刪除成功",
|
||||||
"Remove Resource": "移除資源",
|
"Remove Resource": "移除資源",
|
||||||
"Are you sure you want to remove this resource?": "您確定要移除此資源嗎?",
|
"Are you sure you want to remove this resource?":
|
||||||
|
"您確定要移除此資源嗎?",
|
||||||
"Resource deleted successfully": "資源移除成功",
|
"Resource deleted successfully": "資源移除成功",
|
||||||
"Edit Collection": "編輯合集",
|
"Edit Collection": "編輯合集",
|
||||||
"Edit successful": "編輯成功",
|
"Edit successful": "編輯成功",
|
||||||
@@ -504,7 +508,9 @@ export const i18nData = {
|
|||||||
"Update File Info": "更新檔案信息",
|
"Update File Info": "更新檔案信息",
|
||||||
"File info updated successfully": "檔案信息更新成功",
|
"File info updated successfully": "檔案信息更新成功",
|
||||||
"File URL": "檔案URL",
|
"File URL": "檔案URL",
|
||||||
"You do not have permission to upload files, please contact the administrator.": "您沒有上傳檔案的權限,請聯繫管理員。",
|
"You do not have permission to upload files, please contact the administrator.":
|
||||||
|
"您沒有上傳檔案的權限,請聯繫管理員。",
|
||||||
|
"Private": "私有",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@@ -199,4 +199,5 @@ export interface Collection {
|
|||||||
user: User;
|
user: User;
|
||||||
resources_count: number;
|
resources_count: number;
|
||||||
images: Image[];
|
images: Image[];
|
||||||
|
isPublic: boolean;
|
||||||
}
|
}
|
||||||
|
@@ -693,11 +693,13 @@ class Network {
|
|||||||
async createCollection(
|
async createCollection(
|
||||||
title: string,
|
title: string,
|
||||||
article: string,
|
article: string,
|
||||||
|
isPublic: boolean,
|
||||||
): Promise<Response<Collection>> {
|
): Promise<Response<Collection>> {
|
||||||
return this._callApi(() =>
|
return this._callApi(() =>
|
||||||
axios.postForm(`${this.apiBaseUrl}/collection/create`, {
|
axios.postForm(`${this.apiBaseUrl}/collection/create`, {
|
||||||
title,
|
title,
|
||||||
article,
|
article,
|
||||||
|
public: isPublic,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -706,12 +708,14 @@ class Network {
|
|||||||
id: number,
|
id: number,
|
||||||
title: string,
|
title: string,
|
||||||
article: string,
|
article: string,
|
||||||
|
isPublic: boolean,
|
||||||
): Promise<Response<any>> {
|
): Promise<Response<any>> {
|
||||||
return this._callApi(() =>
|
return this._callApi(() =>
|
||||||
axios.postForm(`${this.apiBaseUrl}/collection/update`, {
|
axios.postForm(`${this.apiBaseUrl}/collection/update`, {
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
article,
|
article,
|
||||||
|
public: isPublic,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -6,10 +6,11 @@ import { Collection } from "../network/models";
|
|||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
import ResourcesView from "../components/resources_view";
|
import ResourcesView from "../components/resources_view";
|
||||||
import Loading from "../components/loading";
|
import Loading from "../components/loading";
|
||||||
import { MdOutlineDelete, MdOutlineEdit } from "react-icons/md";
|
import { MdOutlineDelete, MdOutlineEdit, MdOutlineLock } from "react-icons/md";
|
||||||
import { app } from "../app";
|
import { app } from "../app";
|
||||||
import { useTranslation } from "../utils/i18n";
|
import { useTranslation } from "../utils/i18n";
|
||||||
import Button from "../components/button";
|
import Button from "../components/button";
|
||||||
|
import Badge from "../components/badge";
|
||||||
|
|
||||||
export default function CollectionPage() {
|
export default function CollectionPage() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
@@ -54,7 +55,7 @@ export default function CollectionPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!collection) return;
|
if (!collection) return;
|
||||||
document.title = collection.title;
|
document.title = collection.title;
|
||||||
}, [collection])
|
}, [collection]);
|
||||||
|
|
||||||
const toBeDeletedRID = useRef<number | null>(null);
|
const toBeDeletedRID = useRef<number | null>(null);
|
||||||
|
|
||||||
@@ -160,6 +161,12 @@ export default function CollectionPage() {
|
|||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<span className="flex-1" />
|
||||||
|
{!collection.isPublic && (
|
||||||
|
<Badge className="badge-soft badge-error text-xs mr-2 shadow-xs">
|
||||||
|
<MdOutlineLock size={16} className="inline-block" /> {t("Private")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ResourcesView
|
<ResourcesView
|
||||||
@@ -275,6 +282,7 @@ function EditCollectionDialog({
|
|||||||
}) {
|
}) {
|
||||||
const [editTitle, setEditTitle] = useState(collection.title);
|
const [editTitle, setEditTitle] = useState(collection.title);
|
||||||
const [editArticle, setEditArticle] = useState(collection.article);
|
const [editArticle, setEditArticle] = useState(collection.article);
|
||||||
|
const [editIsPublic, setEditIsPublic] = useState(collection.isPublic);
|
||||||
const [editLoading, setEditLoading] = useState(false);
|
const [editLoading, setEditLoading] = useState(false);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -292,6 +300,7 @@ function EditCollectionDialog({
|
|||||||
collection.id,
|
collection.id,
|
||||||
editTitle,
|
editTitle,
|
||||||
editArticle,
|
editArticle,
|
||||||
|
editIsPublic,
|
||||||
);
|
);
|
||||||
setEditLoading(false);
|
setEditLoading(false);
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
@@ -300,7 +309,12 @@ function EditCollectionDialog({
|
|||||||
if (getRes.success) {
|
if (getRes.success) {
|
||||||
onSaved(getRes.data!);
|
onSaved(getRes.data!);
|
||||||
} else {
|
} else {
|
||||||
onSaved({ ...collection, title: editTitle, article: editArticle });
|
onSaved({
|
||||||
|
...collection,
|
||||||
|
title: editTitle,
|
||||||
|
article: editArticle,
|
||||||
|
isPublic: editIsPublic,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showToast({
|
showToast({
|
||||||
@@ -325,11 +339,21 @@ function EditCollectionDialog({
|
|||||||
/>
|
/>
|
||||||
<label className="block mb-1">{t("Description")}</label>
|
<label className="block mb-1">{t("Description")}</label>
|
||||||
<textarea
|
<textarea
|
||||||
className="textarea w-full min-h-32"
|
className="textarea w-full min-h-32 mb-2"
|
||||||
value={editArticle}
|
value={editArticle}
|
||||||
onChange={(e) => setEditArticle(e.target.value)}
|
onChange={(e) => setEditArticle(e.target.value)}
|
||||||
disabled={editLoading}
|
disabled={editLoading}
|
||||||
/>
|
/>
|
||||||
|
<label className="flex items-center mb-4 mt-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={!editIsPublic}
|
||||||
|
onChange={(e) => setEditIsPublic(!e.target.checked)}
|
||||||
|
className="checkbox mr-2"
|
||||||
|
disabled={editLoading}
|
||||||
|
/>
|
||||||
|
{t("Private")}
|
||||||
|
</label>
|
||||||
<div className="modal-action">
|
<div className="modal-action">
|
||||||
<button className="btn" onClick={onClose} disabled={editLoading}>
|
<button className="btn" onClick={onClose} disabled={editLoading}>
|
||||||
{t("Cancel")}
|
{t("Cancel")}
|
||||||
|
@@ -8,6 +8,7 @@ import { useNavigate } from "react-router";
|
|||||||
export default function CreateCollectionPage() {
|
export default function CreateCollectionPage() {
|
||||||
const [title, setTitle] = useState<string>("");
|
const [title, setTitle] = useState<string>("");
|
||||||
const [article, setArticle] = useState<string>("");
|
const [article, setArticle] = useState<string>("");
|
||||||
|
const [isPublic, setIsPublic] = useState<boolean>(true);
|
||||||
const [isLoading, setLoading] = useState(false);
|
const [isLoading, setLoading] = useState(false);
|
||||||
const [isUploadingimage, setUploadingImage] = useState(false);
|
const [isUploadingimage, setUploadingImage] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -73,7 +74,7 @@ export default function CreateCollectionPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await network.createCollection(title, article);
|
const res = await network.createCollection(title, article, isPublic);
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
showToast({
|
showToast({
|
||||||
message: t("Collection created successfully"),
|
message: t("Collection created successfully"),
|
||||||
@@ -110,6 +111,17 @@ export default function CreateCollectionPage() {
|
|||||||
onChange={(e) => setArticle(e.target.value)}
|
onChange={(e) => setArticle(e.target.value)}
|
||||||
className="textarea mt-1 w-full min-h-80"
|
className="textarea mt-1 w-full min-h-80"
|
||||||
/>
|
/>
|
||||||
|
<div className="mt-4 mx-1">
|
||||||
|
<label className="flex items-center py-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={!isPublic}
|
||||||
|
onChange={(e) => setIsPublic(!e.target.checked)}
|
||||||
|
className="checkbox mr-2 checkbox-primary"
|
||||||
|
/>
|
||||||
|
{t("Private")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={"flex items-center mt-4"}>
|
<div className={"flex items-center mt-4"}>
|
||||||
<button
|
<button
|
||||||
|
@@ -218,7 +218,8 @@ export default function ResourcePage() {
|
|||||||
</button>
|
</button>
|
||||||
<Tags tags={resource.tags} />
|
<Tags tags={resource.tags} />
|
||||||
<p className={"px-3 mt-2"}>
|
<p className={"px-3 mt-2"}>
|
||||||
{resource.links && resource.links.map((l) => {
|
{resource.links &&
|
||||||
|
resource.links.map((l) => {
|
||||||
return (
|
return (
|
||||||
<a href={l.url} target={"_blank"}>
|
<a href={l.url} target={"_blank"}>
|
||||||
<span
|
<span
|
||||||
@@ -1637,7 +1638,7 @@ function CollectionDialog({ rid }: { rid: number }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!app.isLoggedIn()) {
|
if (!app.isLoggedIn()) {
|
||||||
return <></>
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1683,13 +1684,16 @@ function CollectionDialog({ rid }: { rid: number }) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="modal-action">
|
<div className="modal-action">
|
||||||
<Button className="btn-ghost" onClick={() => {
|
<Button
|
||||||
|
className="btn-ghost"
|
||||||
|
onClick={() => {
|
||||||
const dialog = document.getElementById(
|
const dialog = document.getElementById(
|
||||||
"collection_dialog",
|
"collection_dialog",
|
||||||
) as HTMLDialogElement;
|
) as HTMLDialogElement;
|
||||||
dialog.close();
|
dialog.close();
|
||||||
navigate("/create-collection");
|
navigate("/create-collection");
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<MdOutlineAdd size={20} className={"inline-block mr-1"} />
|
<MdOutlineAdd size={20} className={"inline-block mr-1"} />
|
||||||
{t("Create")}
|
{t("Create")}
|
||||||
|
@@ -18,6 +18,7 @@ import {
|
|||||||
MdOutlineAdd,
|
MdOutlineAdd,
|
||||||
MdOutlineArchive,
|
MdOutlineArchive,
|
||||||
MdOutlineComment,
|
MdOutlineComment,
|
||||||
|
MdOutlineLock,
|
||||||
MdOutlinePhotoAlbum,
|
MdOutlinePhotoAlbum,
|
||||||
} from "react-icons/md";
|
} from "react-icons/md";
|
||||||
import { useTranslation } from "../utils/i18n";
|
import { useTranslation } from "../utils/i18n";
|
||||||
@@ -413,7 +414,8 @@ function Collections({ username }: { username?: string }) {
|
|||||||
onChange={(e) => delayedSetSearchKeyword(e.target.value)}
|
onChange={(e) => delayedSetSearchKeyword(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<span className="flex-1" />
|
<span className="flex-1" />
|
||||||
{username == app.user?.username && <button
|
{username == app.user?.username && (
|
||||||
|
<button
|
||||||
className="btn btn-primary btn-soft"
|
className="btn btn-primary btn-soft"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate("/create-collection");
|
navigate("/create-collection");
|
||||||
@@ -421,7 +423,8 @@ function Collections({ username }: { username?: string }) {
|
|||||||
>
|
>
|
||||||
<MdOutlineAdd size={20} className="inline-block mr-1" />
|
<MdOutlineAdd size={20} className="inline-block mr-1" />
|
||||||
{t("Create")}
|
{t("Create")}
|
||||||
</button>}
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<CollectionsList
|
<CollectionsList
|
||||||
username={username}
|
username={username}
|
||||||
@@ -522,6 +525,12 @@ function CollectionCard({ collection }: { collection: Collection }) {
|
|||||||
<MdOutlinePhotoAlbum size={16} className="inline-block" />
|
<MdOutlinePhotoAlbum size={16} className="inline-block" />
|
||||||
{collection.resources_count} {t("Resources")}
|
{collection.resources_count} {t("Resources")}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
<span className="flex-1" />
|
||||||
|
{!collection.isPublic && (
|
||||||
|
<Badge className="badge-soft badge-error text-xs mr-2 shadow-xs">
|
||||||
|
<MdOutlineLock size={16} className="inline-block" /> {t("Private")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@@ -15,11 +15,16 @@ func handleCreateCollection(c fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
title := c.FormValue("title")
|
title := c.FormValue("title")
|
||||||
article := c.FormValue("article")
|
article := c.FormValue("article")
|
||||||
|
publicStr := c.FormValue("public")
|
||||||
|
public := false
|
||||||
|
if publicStr == "true" || publicStr == "1" {
|
||||||
|
public = true
|
||||||
|
}
|
||||||
if title == "" || article == "" {
|
if title == "" || article == "" {
|
||||||
return model.NewRequestError("Title and article are required")
|
return model.NewRequestError("Title and article are required")
|
||||||
}
|
}
|
||||||
host := c.Hostname()
|
host := c.Hostname()
|
||||||
col, err := service.CreateCollection(uid, title, article, host)
|
col, err := service.CreateCollection(uid, title, article, host, public)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -38,6 +43,11 @@ func handleUpdateCollection(c fiber.Ctx) error {
|
|||||||
idStr := c.FormValue("id")
|
idStr := c.FormValue("id")
|
||||||
title := c.FormValue("title")
|
title := c.FormValue("title")
|
||||||
article := c.FormValue("article")
|
article := c.FormValue("article")
|
||||||
|
publicStr := c.FormValue("public")
|
||||||
|
public := false
|
||||||
|
if publicStr == "true" || publicStr == "1" {
|
||||||
|
public = true
|
||||||
|
}
|
||||||
if idStr == "" || title == "" || article == "" {
|
if idStr == "" || title == "" || article == "" {
|
||||||
return model.NewRequestError("ID, title and article are required")
|
return model.NewRequestError("ID, title and article are required")
|
||||||
}
|
}
|
||||||
@@ -46,7 +56,7 @@ func handleUpdateCollection(c fiber.Ctx) error {
|
|||||||
return model.NewRequestError("Invalid collection ID")
|
return model.NewRequestError("Invalid collection ID")
|
||||||
}
|
}
|
||||||
host := c.Hostname()
|
host := c.Hostname()
|
||||||
if err := service.UpdateCollection(uid, uint(id), title, article, host); err != nil {
|
if err := service.UpdateCollection(uid, uint(id), title, article, host, public); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return c.Status(fiber.StatusOK).JSON(model.Response[any]{
|
return c.Status(fiber.StatusOK).JSON(model.Response[any]{
|
||||||
@@ -83,7 +93,11 @@ func handleGetCollection(c fiber.Ctx) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return model.NewRequestError("Invalid collection ID")
|
return model.NewRequestError("Invalid collection ID")
|
||||||
}
|
}
|
||||||
col, err := service.GetCollectionByID(uint(id))
|
|
||||||
|
// Get viewer UID (0 if not authenticated)
|
||||||
|
viewerUID, _ := c.Locals("uid").(uint)
|
||||||
|
|
||||||
|
col, err := service.GetCollectionByID(uint(id), viewerUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -104,7 +118,11 @@ func handleListUserCollections(c fiber.Ctx) error {
|
|||||||
if username == "" {
|
if username == "" {
|
||||||
return model.NewRequestError("Username is required")
|
return model.NewRequestError("Username is required")
|
||||||
}
|
}
|
||||||
cols, total, err := service.ListUserCollections(username, page)
|
|
||||||
|
// Get viewer UID (0 if not authenticated)
|
||||||
|
viewerUID, _ := c.Locals("uid").(uint)
|
||||||
|
|
||||||
|
cols, total, err := service.ListUserCollections(username, page, viewerUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -127,7 +145,11 @@ func handleListCollectionResources(c fiber.Ctx) error {
|
|||||||
if err != nil || page < 1 {
|
if err != nil || page < 1 {
|
||||||
page = 1
|
page = 1
|
||||||
}
|
}
|
||||||
res, total, err := service.ListCollectionResources(uint(id), page)
|
|
||||||
|
// Get viewer UID (0 if not authenticated)
|
||||||
|
viewerUID, _ := c.Locals("uid").(uint)
|
||||||
|
|
||||||
|
res, total, err := service.ListCollectionResources(uint(id), page, viewerUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -209,7 +231,11 @@ func handleSearchUserCollections(c fiber.Ctx) error {
|
|||||||
excludedRID = uint(rid)
|
excludedRID = uint(rid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cols, err := service.SearchUserCollections(username, keyword, excludedRID)
|
|
||||||
|
// Get viewer UID (0 if not authenticated)
|
||||||
|
viewerUID, _ := c.Locals("uid").(uint)
|
||||||
|
|
||||||
|
cols, err := service.SearchUserCollections(username, keyword, excludedRID, viewerUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -6,13 +6,14 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CreateCollection(uid uint, title string, article string, images []uint) (model.Collection, error) {
|
func CreateCollection(uid uint, title string, article string, images []uint, public bool) (model.Collection, error) {
|
||||||
var collection model.Collection
|
var collection model.Collection
|
||||||
err := db.Transaction(func(tx *gorm.DB) error {
|
err := db.Transaction(func(tx *gorm.DB) error {
|
||||||
collection = model.Collection{
|
collection = model.Collection{
|
||||||
UserID: uid,
|
UserID: uid,
|
||||||
Title: title,
|
Title: title,
|
||||||
Article: article,
|
Article: article,
|
||||||
|
Public: public, // 新增
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Create(&collection).Error; err != nil {
|
if err := tx.Create(&collection).Error; err != nil {
|
||||||
@@ -32,17 +33,23 @@ func CreateCollection(uid uint, title string, article string, images []uint) (mo
|
|||||||
return collection, nil
|
return collection, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateCollection(id uint, title string, article string, images []uint) error {
|
func UpdateCollection(id uint, title string, article string, images []uint, public bool) error {
|
||||||
return db.Transaction(func(tx *gorm.DB) error {
|
return db.Transaction(func(tx *gorm.DB) error {
|
||||||
collection := &model.Collection{
|
collection := &model.Collection{}
|
||||||
Model: gorm.Model{
|
|
||||||
ID: id,
|
// First find the existing collection
|
||||||
},
|
if err := tx.Where("id = ?", id).First(collection).Error; err != nil {
|
||||||
Title: title,
|
return err
|
||||||
Article: article,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Model(collection).Updates(collection).Error; err != nil {
|
// Update the fields
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
"title": title,
|
||||||
|
"article": article,
|
||||||
|
"public": public,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Model(collection).Updates(updates).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,19 +149,22 @@ func GetCollectionByID(id uint) (*model.Collection, error) {
|
|||||||
return collection, nil
|
return collection, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListUserCollections(uid uint, page int, pageSize int) ([]*model.Collection, int64, error) {
|
func ListUserCollections(uid uint, page int, pageSize int, showPrivate bool) ([]*model.Collection, int64, error) {
|
||||||
var collections []*model.Collection
|
var collections []*model.Collection
|
||||||
var total int64
|
var total int64
|
||||||
|
|
||||||
if err := db.Model(&model.Collection{}).Where("user_id = ?", uid).Count(&total).Error; err != nil {
|
query := db.Model(&model.Collection{}).Where("user_id = ?", uid)
|
||||||
|
if !showPrivate {
|
||||||
|
query = query.Where("public = ?", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := query.Count(&total).Error; err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := db.
|
if err := query.
|
||||||
Model(&model.Collection{}).
|
|
||||||
Preload("Images").
|
Preload("Images").
|
||||||
Preload("Resources").
|
Preload("Resources").
|
||||||
Where("user_id = ?", uid).
|
|
||||||
Offset((page - 1) * pageSize).
|
Offset((page - 1) * pageSize).
|
||||||
Limit(pageSize).
|
Limit(pageSize).
|
||||||
Find(&collections).Error; err != nil {
|
Find(&collections).Error; err != nil {
|
||||||
@@ -196,12 +206,16 @@ func ListCollectionResources(collectionID uint, page int, pageSize int) ([]*mode
|
|||||||
|
|
||||||
// SearchUserCollections searches for collections by user ID and keyword limited to 10 results.
|
// SearchUserCollections searches for collections by user ID and keyword limited to 10 results.
|
||||||
// excludedRID: if >0, only return collections not containing this resource.
|
// excludedRID: if >0, only return collections not containing this resource.
|
||||||
func SearchUserCollections(uid uint, keyword string, excludedRID uint) ([]*model.Collection, error) {
|
func SearchUserCollections(uid uint, keyword string, excludedRID uint, showPrivate bool) ([]*model.Collection, error) {
|
||||||
var collections []*model.Collection
|
var collections []*model.Collection
|
||||||
|
|
||||||
query := db.Model(&model.Collection{}).
|
query := db.Model(&model.Collection{}).
|
||||||
Where("user_id = ?", uid)
|
Where("user_id = ?", uid)
|
||||||
|
|
||||||
|
if !showPrivate {
|
||||||
|
query = query.Where("public = ?", true)
|
||||||
|
}
|
||||||
|
|
||||||
if keyword != "" {
|
if keyword != "" {
|
||||||
query = query.Where("title LIKE ?", "%"+keyword+"%")
|
query = query.Where("title LIKE ?", "%"+keyword+"%")
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,7 @@ import (
|
|||||||
"nysoure/server/model"
|
"nysoure/server/model"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3/log"
|
"github.com/gofiber/fiber/v3/log"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
)
|
)
|
||||||
@@ -55,6 +56,12 @@ func ErrorHandler(c fiber.Ctx) error {
|
|||||||
Data: nil,
|
Data: nil,
|
||||||
Message: fiberErr.Message,
|
Message: fiberErr.Message,
|
||||||
})
|
})
|
||||||
|
} else if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(model.Response[any]{
|
||||||
|
Success: false,
|
||||||
|
Data: nil,
|
||||||
|
Message: "Not found",
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
var fiberErr *fiber.Error
|
var fiberErr *fiber.Error
|
||||||
if errors.As(err, &fiberErr) {
|
if errors.As(err, &fiberErr) {
|
||||||
|
@@ -137,7 +137,7 @@ func serveIndexHtml(c fiber.Ctx) error {
|
|||||||
collectionIDStr := strings.TrimPrefix(path, "/collection/")
|
collectionIDStr := strings.TrimPrefix(path, "/collection/")
|
||||||
collectionID, err := strconv.Atoi(collectionIDStr)
|
collectionID, err := strconv.Atoi(collectionIDStr)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
coll, err := service.GetCollectionByID(uint(collectionID))
|
coll, err := service.GetCollectionByID(uint(collectionID), 0)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
title = coll.Title
|
title = coll.Title
|
||||||
description = utils.ArticleToDescription(coll.Article, 256)
|
description = utils.ArticleToDescription(coll.Article, 256)
|
||||||
|
@@ -11,6 +11,7 @@ type Collection struct {
|
|||||||
ResourcesCount int `gorm:"default:0"`
|
ResourcesCount int `gorm:"default:0"`
|
||||||
Images []Image `gorm:"many2many:collection_images;"`
|
Images []Image `gorm:"many2many:collection_images;"`
|
||||||
Resources []Resource `gorm:"many2many:collection_resources;"`
|
Resources []Resource `gorm:"many2many:collection_resources;"`
|
||||||
|
Public bool `gorm:"default:false"` // 新增公开/私有字段
|
||||||
}
|
}
|
||||||
|
|
||||||
type CollectionView struct {
|
type CollectionView struct {
|
||||||
@@ -20,6 +21,7 @@ type CollectionView struct {
|
|||||||
User UserView `json:"user"`
|
User UserView `json:"user"`
|
||||||
ResourcesCount int `json:"resources_count"`
|
ResourcesCount int `json:"resources_count"`
|
||||||
Images []Image `json:"images"`
|
Images []Image `json:"images"`
|
||||||
|
IsPublic bool `json:"isPublic"` // 新增公开/私有字段
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Collection) ToView() *CollectionView {
|
func (c Collection) ToView() *CollectionView {
|
||||||
@@ -30,5 +32,6 @@ func (c Collection) ToView() *CollectionView {
|
|||||||
User: c.User.ToView(),
|
User: c.User.ToView(),
|
||||||
ResourcesCount: c.ResourcesCount,
|
ResourcesCount: c.ResourcesCount,
|
||||||
Images: c.Images,
|
Images: c.Images,
|
||||||
|
IsPublic: c.Public, // 新增
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,11 +6,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Create a new collection.
|
// Create a new collection.
|
||||||
func CreateCollection(uid uint, title, article string, host string) (*model.CollectionView, error) {
|
func CreateCollection(uid uint, title, article string, host string, public bool) (*model.CollectionView, error) {
|
||||||
if uid == 0 || title == "" || article == "" {
|
if uid == 0 || title == "" || article == "" {
|
||||||
return nil, model.NewRequestError("invalid parameters")
|
return nil, model.NewRequestError("invalid parameters")
|
||||||
}
|
}
|
||||||
c, err := dao.CreateCollection(uid, title, article, findImagesInContent(article, host))
|
c, err := dao.CreateCollection(uid, title, article, findImagesInContent(article, host), public)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ func CreateCollection(uid uint, title, article string, host string) (*model.Coll
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update an existing collection with user validation.
|
// Update an existing collection with user validation.
|
||||||
func UpdateCollection(uid, id uint, title, article string, host string) error {
|
func UpdateCollection(uid, id uint, title, article string, host string, public bool) error {
|
||||||
if uid == 0 || id == 0 || title == "" || article == "" {
|
if uid == 0 || id == 0 || title == "" || article == "" {
|
||||||
return model.NewRequestError("invalid parameters")
|
return model.NewRequestError("invalid parameters")
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@ func UpdateCollection(uid, id uint, title, article string, host string) error {
|
|||||||
if collection.UserID != uid {
|
if collection.UserID != uid {
|
||||||
return model.NewUnAuthorizedError("user does not have permission to update this collection")
|
return model.NewUnAuthorizedError("user does not have permission to update this collection")
|
||||||
}
|
}
|
||||||
return dao.UpdateCollection(id, title, article, findImagesInContent(article, host))
|
return dao.UpdateCollection(id, title, article, findImagesInContent(article, host), public)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete a collection by ID.
|
// Delete a collection by ID.
|
||||||
@@ -83,7 +83,7 @@ func RemoveResourceFromCollection(uid, collectionID, resourceID uint) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get a collection by ID.
|
// Get a collection by ID.
|
||||||
func GetCollectionByID(id uint) (*model.CollectionView, error) {
|
func GetCollectionByID(id uint, viewerUID uint) (*model.CollectionView, error) {
|
||||||
if id == 0 {
|
if id == 0 {
|
||||||
return nil, model.NewRequestError("invalid collection id")
|
return nil, model.NewRequestError("invalid collection id")
|
||||||
}
|
}
|
||||||
@@ -91,11 +91,17 @@ func GetCollectionByID(id uint) (*model.CollectionView, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if collection is private and viewer is not the owner
|
||||||
|
if !c.Public && c.UserID != viewerUID {
|
||||||
|
return nil, model.NewUnAuthorizedError("you do not have permission to view this private collection")
|
||||||
|
}
|
||||||
|
|
||||||
return c.ToView(), nil
|
return c.ToView(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// List collections of a user with pagination.
|
// List collections of a user with pagination.
|
||||||
func ListUserCollections(username string, page int) ([]*model.CollectionView, int64, error) {
|
func ListUserCollections(username string, page int, viewerUID uint) ([]*model.CollectionView, int64, error) {
|
||||||
if username == "" || page < 1 {
|
if username == "" || page < 1 {
|
||||||
return nil, 0, model.NewRequestError("invalid parameters")
|
return nil, 0, model.NewRequestError("invalid parameters")
|
||||||
}
|
}
|
||||||
@@ -104,7 +110,11 @@ func ListUserCollections(username string, page int) ([]*model.CollectionView, in
|
|||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
uid := user.ID
|
uid := user.ID
|
||||||
collections, total, err := dao.ListUserCollections(uid, page, pageSize)
|
|
||||||
|
// Check if viewer can see private collections (only owner can see their private collections)
|
||||||
|
showPrivate := uid == viewerUID
|
||||||
|
|
||||||
|
collections, total, err := dao.ListUserCollections(uid, page, pageSize, showPrivate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
@@ -116,10 +126,21 @@ func ListUserCollections(username string, page int) ([]*model.CollectionView, in
|
|||||||
}
|
}
|
||||||
|
|
||||||
// List resources in a collection with pagination.
|
// List resources in a collection with pagination.
|
||||||
func ListCollectionResources(collectionID uint, page int) ([]*model.ResourceView, int64, error) {
|
func ListCollectionResources(collectionID uint, page int, viewerUID uint) ([]*model.ResourceView, int64, error) {
|
||||||
if collectionID == 0 || page < 1 {
|
if collectionID == 0 || page < 1 {
|
||||||
return nil, 0, model.NewRequestError("invalid parameters")
|
return nil, 0, model.NewRequestError("invalid parameters")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check collection privacy first
|
||||||
|
collection, err := dao.GetCollectionByID(collectionID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !collection.Public && collection.UserID != viewerUID {
|
||||||
|
return nil, 0, model.NewUnAuthorizedError("you do not have permission to view this private collection")
|
||||||
|
}
|
||||||
|
|
||||||
resources, total, err := dao.ListCollectionResources(collectionID, page, pageSize)
|
resources, total, err := dao.ListCollectionResources(collectionID, page, pageSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
@@ -134,7 +155,7 @@ func ListCollectionResources(collectionID uint, page int) ([]*model.ResourceView
|
|||||||
|
|
||||||
// Search user collections by keyword, limited to 10 results.
|
// Search user collections by keyword, limited to 10 results.
|
||||||
// excludedRID: if >0, only return collections not containing this resource.
|
// excludedRID: if >0, only return collections not containing this resource.
|
||||||
func SearchUserCollections(username string, keyword string, excludedRID uint) ([]*model.CollectionView, error) {
|
func SearchUserCollections(username string, keyword string, excludedRID uint, viewerUID uint) ([]*model.CollectionView, error) {
|
||||||
if username == "" {
|
if username == "" {
|
||||||
return nil, model.NewRequestError("invalid parameters")
|
return nil, model.NewRequestError("invalid parameters")
|
||||||
}
|
}
|
||||||
@@ -143,7 +164,11 @@ func SearchUserCollections(username string, keyword string, excludedRID uint) ([
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
uid := user.ID
|
uid := user.ID
|
||||||
collections, err := dao.SearchUserCollections(uid, keyword, excludedRID)
|
|
||||||
|
// Check if viewer can see private collections
|
||||||
|
showPrivate := uid == viewerUID
|
||||||
|
|
||||||
|
collections, err := dao.SearchUserCollections(uid, keyword, excludedRID, showPrivate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user