Commit 2cdd280b authored by yaoke.yk's avatar yaoke.yk

页面整体调整

parent 3cd16618
This diff is collapsed.
This diff is collapsed.
......@@ -9,10 +9,20 @@ import { useAuth } from "../../hooks/useAuth";
export function LoginPage() {
const { login, loginPending, loginError } = useAuth();
const [form, setForm] = useState({ email: "", password: "" });
const [formError, setFormError] = useState<string | null>(null);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
login(form);
const email = form.email.trim();
const password = form.password.trim();
if (!email || !password) {
setFormError("请输入邮箱和密码");
return;
}
setFormError(null);
login({ email, password });
};
return (
......@@ -23,7 +33,7 @@ export function LoginPage() {
<CardDescription>登录您的账号</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<form onSubmit={handleSubmit} className="space-y-4" noValidate>
<div className="space-y-2">
<Label htmlFor="email">邮箱</Label>
<Input
......@@ -32,7 +42,10 @@ export function LoginPage() {
placeholder="you@example.com"
required
value={form.email}
onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))}
onChange={(e) => {
setFormError(null);
setForm((f) => ({ ...f, email: e.target.value }));
}}
/>
</div>
<div className="space-y-2">
......@@ -43,13 +56,16 @@ export function LoginPage() {
placeholder="••••••••"
required
value={form.password}
onChange={(e) => setForm((f) => ({ ...f, password: e.target.value }))}
onChange={(e) => {
setFormError(null);
setForm((f) => ({ ...f, password: e.target.value }));
}}
/>
</div>
{loginError && (
{(formError || loginError) && (
<p className="text-sm text-destructive">
{loginError instanceof Error ? loginError.message : "登录失败"}
{formError ?? (loginError instanceof Error ? loginError.message : "登录失败")}
</p>
)}
......
This diff is collapsed.
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { aiApi } from "../lib/api/ai";
import type { Character, Episode, Scene, Storyboard, StructuredVideoRequest } from "../lib/api/ai";
import type { Character, Episode, Outline, Scene, Storyboard, StructuredVideoRequest } from "../lib/api/ai";
// ---- Characters ----
const charactersKey = (pid: string) => ["characters", pid];
......@@ -84,6 +84,14 @@ export function useGenerateOutline(projectId: string) {
});
}
export function useUpdateOutline(projectId: string) {
const qc = useQueryClient();
return useMutation({
mutationFn: (patch: Partial<Outline>) => aiApi.updateOutline(projectId, patch),
onSuccess: () => qc.invalidateQueries({ queryKey: outlineKey(projectId) }),
});
}
// ---- Episodes ----
export function useEpisodes(projectId: string) {
return useQuery({
......
......@@ -152,6 +152,10 @@ export const aiApi = {
const r = await apiClient.get(`/projects/${projectId}/outline`);
return r.data.data;
},
updateOutline: async (projectId: string, patch: Partial<Outline>): Promise<Outline> => {
const r = await apiClient.put(`/projects/${projectId}/outline`, patch);
return r.data.data;
},
// ---- Episodes ----
generateEpisodes: async (projectId: string): Promise<Episode[]> => {
......
......@@ -137,6 +137,15 @@
font-size: var(--font-size);
}
html,
body,
#root {
width: 100%;
min-width: 0;
height: 100%;
margin: 0;
}
h1 {
font-size: var(--text-2xl);
font-weight: var(--font-weight-medium);
......
......@@ -52,6 +52,15 @@ public class OutlineController {
return ApiResponse.success(OutlineDTO.from(outline));
}
@Operation(summary = "编辑项目大纲")
@PutMapping("/outline")
public ApiResponse<OutlineDTO> updateOutline(@PathVariable Long projectId,
@RequestBody Outline req) {
Long tenantId = TenantContext.get();
Outline outline = outlinePipelineService.updateOutline(projectId, tenantId, req);
return ApiResponse.success(OutlineDTO.from(outline));
}
@Operation(summary = "生成分集内容(使用最新大纲)")
@PostMapping("/episodes/generate")
public ApiResponse<List<EpisodeDTO>> generateEpisodes(@PathVariable Long projectId) {
......
......@@ -19,6 +19,11 @@ public interface OutlinePipelineService {
Outline getOutline(Long projectId, Long tenantId);
/**
* 手动编辑大纲基础信息(title/genre/synopsis/episodeCount,非 null 字段才更新)。
*/
Outline updateOutline(Long projectId, Long tenantId, Outline req);
List<Episode> getEpisodes(Long projectId, Long tenantId);
/**
......
......@@ -205,6 +205,24 @@ public class OutlinePipelineServiceImpl implements OutlinePipelineService {
}
@Override
public Outline updateOutline(Long projectId, Long tenantId, Outline req) {
Outline existing = outlineMapper.findLatestByProject(projectId, tenantId);
if (existing == null) {
throw new BizException(ErrorCode.NOT_FOUND, "大纲不存在,请先生成或保存大纲");
}
if (req.getTitle() != null) existing.setTitle(req.getTitle());
if (req.getGenre() != null) existing.setGenre(req.getGenre());
if (req.getSynopsis() != null) existing.setSynopsis(req.getSynopsis());
if (req.getEpisodeCount() != null && req.getEpisodeCount() > 0) {
existing.setEpisodeCount(req.getEpisodeCount());
}
existing.setStatus("ready");
outlineMapper.updateById(existing);
log.info("Outline updated: id={}, projectId={}", existing.getId(), projectId);
return existing;
}
@Override
public List<Episode> getEpisodes(Long projectId, Long tenantId) {
return episodeMapper.findByProject(projectId, tenantId);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment