characters

This commit is contained in:
2025-11-15 20:21:29 +08:00
parent ec85ee3e82
commit 0a3e255dfe
7 changed files with 101 additions and 34 deletions

View File

@@ -1,12 +1,13 @@
import { useState } from "react";
import { CharactorParams } from "../network/models";
import { CharacterParams } from "../network/models";
import { network } from "../network/network";
import showToast from "./toast";
import { useTranslation } from "../utils/i18n";
import Button from "./button";
export default function CharactorEditor({charactor, setCharactor, onDelete}: {
charactor: CharactorParams;
setCharactor: (charactor: CharactorParams) => void;
charactor: CharacterParams;
setCharactor: (charactor: CharacterParams) => void;
onDelete: () => void;
}) {
const { t } = useTranslation();
@@ -39,7 +40,7 @@ export default function CharactorEditor({charactor, setCharactor, onDelete}: {
input.click();
}
return <div className="h-52 shadow rounded-2xl overflow-clip flex">
return <div className="h-52 shadow rounded-2xl overflow-clip flex bg-base-100">
<div className="w-36 h-full cursor-pointer relative" onClick={uploadImage}>
{
isUploading ?
@@ -95,3 +96,36 @@ export default function CharactorEditor({charactor, setCharactor, onDelete}: {
</div>
</div>;
}
export function FetchVndbCharactersButton({vnID, onFetch}: {
vnID: string;
onFetch: (characters: CharacterParams[]) => void;
}) {
const { t } = useTranslation();
const [isFetching, setFetching] = useState(false);
const fetchCharacters = async () => {
// validate vnID (v123456)
if (!/^v\d+$/.test(vnID)) {
showToast({
type: "error",
message: t("Invalid VNDB ID format"),
});
return;
}
setFetching(true);
const res = await network.getCharactersFromVNDB(vnID);
setFetching(false);
if (res.success && res.data) {
onFetch(res.data);
} else {
showToast({
type: "error",
message: t("Failed to fetch characters from VNDB"),
});
}
};
return <Button isLoading={isFetching} onClick={fetchCharacters}>
{t("Fetch from VNDB")}
</Button>;
}

View File

@@ -49,10 +49,10 @@ export interface CreateResourceParams {
images: number[];
gallery: number[];
gallery_nsfw: number[];
charactors: CharactorParams[];
characters: CharacterParams[];
}
export interface CharactorParams {
export interface CharacterParams {
name: string;
alias: string[];
cv: string;
@@ -96,7 +96,7 @@ export interface ResourceDetails {
related: Resource[];
gallery: number[];
galleryNsfw: number[];
charactors: CharactorParams[];
charactors: CharacterParams[];
}
export interface Storage {

View File

@@ -21,6 +21,7 @@ import {
CommentWithRef,
Collection,
Statistics,
CharacterParams,
} from "./models.ts";
class Network {
@@ -802,6 +803,14 @@ class Network {
axios.get(`${this.apiBaseUrl}/config/statistics`),
);
}
async getCharactersFromVNDB(vnID: string): Promise<Response<CharacterParams[]>> {
return this._callApi(() =>
axios.get(`${this.apiBaseUrl}/resource/vndb/characters`, {
params: { vnid: vnID },
}),
);
}
}
export const network = new Network();

View File

@@ -6,7 +6,7 @@ import {
MdDelete,
MdOutlineInfo,
} from "react-icons/md";
import { CharactorParams, Tag } from "../network/models.ts";
import { CharacterParams, Tag } from "../network/models.ts";
import { network } from "../network/network.ts";
import { useNavigate, useParams } from "react-router";
import showToast from "../components/toast.ts";
@@ -20,7 +20,7 @@ import {
SelectAndUploadImageButton,
UploadClipboardImageButton,
} from "../components/image_selector.tsx";
import CharactorEditor from "../components/charactor_edit.tsx";
import CharactorEditor, { FetchVndbCharactersButton } from "../components/charactor_edit.tsx";
export default function EditResourcePage() {
const [title, setTitle] = useState<string>("");
@@ -31,7 +31,7 @@ export default function EditResourcePage() {
const [links, setLinks] = useState<{ label: string; url: string }[]>([]);
const [galleryImages, setGalleryImages] = useState<number[]>([]);
const [galleryNsfw, setGalleryNsfw] = useState<number[]>([]);
const [charactors, setCharactors] = useState<CharactorParams[]>([]);
const [charactors, setCharactors] = useState<CharacterParams[]>([]);
const [error, setError] = useState<string | null>(null);
const [isSubmitting, setSubmitting] = useState(false);
const [isLoading, setLoading] = useState(true);
@@ -107,7 +107,7 @@ export default function EditResourcePage() {
links: links,
gallery: galleryImages,
gallery_nsfw: galleryNsfw,
charactors: charactors,
characters: charactors,
});
if (res.success) {
setSubmitting(false);
@@ -444,9 +444,9 @@ export default function EditResourcePage() {
})
}
</div>
<div className="flex">
<div className="flex my-2">
<button
className={"btn my-2"}
className={"btn h-9"}
type={"button"}
onClick={() => {
setCharactors([...charactors, { name: "", alias: [], cv: "", image: 0 }]);
@@ -455,6 +455,17 @@ export default function EditResourcePage() {
<MdAdd />
{t("Add Character")}
</button>
{
links.find(link => link.label.toLowerCase() === "vndb") &&
<div className="ml-4">
<FetchVndbCharactersButton
vnID={links.find(link => link.label.toLowerCase() === "vndb")?.url.split("/").pop() ?? ""}
onFetch={(fetchedCharacters) => {
setCharactors(fetchedCharacters);
}}
/>
</div>
}
</div>
</div>
{error && (

View File

@@ -6,7 +6,7 @@ import {
MdDelete,
MdOutlineInfo,
} from "react-icons/md";
import { CharactorParams, Tag } from "../network/models.ts";
import { CharacterParams, Tag } from "../network/models.ts";
import { network } from "../network/network.ts";
import { useNavigate } from "react-router";
import { useTranslation } from "../utils/i18n";
@@ -32,7 +32,7 @@ export default function PublishPage() {
const [galleryNsfw, setGalleryNsfw] = useState<number[]>([]);
const [error, setError] = useState<string | null>(null);
const [isSubmitting, setSubmitting] = useState(false);
const [charactors, setCharactors] = useState<CharactorParams[]>([]);
const [charactors, setCharactors] = useState<CharacterParams[]>([]);
const isFirstLoad = useRef(true);
useEffect(() => {
@@ -111,7 +111,7 @@ export default function PublishPage() {
links: links,
gallery: galleryImages,
gallery_nsfw: galleryNsfw,
charactors: charactors,
characters: charactors,
});
if (res.success) {
localStorage.removeItem("publish_data");

View File

@@ -18,7 +18,7 @@ import {
Tag,
Resource,
Collection,
CharactorParams,
CharacterParams,
} from "../network/models.ts";
import { network } from "../network/network.ts";
import showToast from "../components/toast.ts";
@@ -1931,6 +1931,9 @@ function Gallery({ images, nsfw }: { images: number[], nsfw: number[] }) {
nsfw = [];
}
// 如果图片数量超过8张显示数字而不是圆点
const showDots = images.length <= 8;
return (
<>
<dialog
@@ -2016,9 +2019,12 @@ function Gallery({ images, nsfw }: { images: number[], nsfw: number[] }) {
</>
)}
{/* 底部圆点 */}
{/* 底部指示器 */}
{images.length > 1 && (
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
<div className="absolute bottom-4 left-1/2 -translate-x-1/2">
{showDots ? (
/* 圆点指示器 */
<div className="flex gap-2">
{images.map((_, index) => (
<button
key={index}
@@ -2032,6 +2038,13 @@ function Gallery({ images, nsfw }: { images: number[], nsfw: number[] }) {
/>
))}
</div>
) : (
/* 数字指示器 */
<div className="bg-base-100/20 px-2 py-1 rounded-full text-xs">
{currentIndex + 1} / {images.length}
</div>
)}
</div>
)}
</div>
</>
@@ -2065,7 +2078,7 @@ function GalleryImage({src, nfsw}: {src: string, nfsw: boolean}) {
);
}
function Characters({ charactors }: { charactors: CharactorParams[] }) {
function Characters({ charactors }: { charactors: CharacterParams[] }) {
const { t } = useTranslation();
if (!charactors || charactors.length === 0) {
@@ -2084,7 +2097,7 @@ function Characters({ charactors }: { charactors: CharactorParams[] }) {
);
}
function CharacterCard({ charactor }: { charactor: CharactorParams }) {
function CharacterCard({ charactor }: { charactor: CharacterParams }) {
const navigate = useNavigate();
const handleCVClick = (e: React.MouseEvent) => {

View File

@@ -35,7 +35,7 @@ type ResourceParams struct {
Images []uint `json:"images"`
Gallery []uint `json:"gallery"`
GalleryNsfw []uint `json:"gallery_nsfw"`
Charactors []CharactorParams `json:"charactors"`
Charactors []CharactorParams `json:"characters"`
}
type CharactorParams struct {