diff --git a/Services/Media/PictureService.cs b/Services/Media/PictureService.cs index 7119297..dbf541a 100644 --- a/Services/Media/PictureService.cs +++ b/Services/Media/PictureService.cs @@ -766,11 +766,31 @@ public class PictureService( picture.Description = description.Trim(); } + // 只有当名称或描述发生变化时才更新嵌入向量 if (!string.IsNullOrWhiteSpace(name) || !string.IsNullOrWhiteSpace(description)) { - var combinedText = $"{picture.Name}. {picture.Description}"; - var embedding = await embeddingService.GetEmbeddingAsync(combinedText); - picture.Embedding = new Vector(embedding); + try + { + var combinedText = $"{picture.Name}. {picture.Description}"; + var embedding = await embeddingService.GetEmbeddingAsync(combinedText); + + // 只有在成功获取到非空嵌入向量时才更新 + if (embedding != null && embedding.Length > 0) + { + picture.Embedding = new Vector(embedding); + } + else + { + // 记录获取到空向量的警告 + Console.WriteLine($"警告: 图片 {pictureId} 的嵌入向量为空,跳过向量更新"); + } + } + catch (Exception ex) + { + // 记录错误但不抛出异常,允许其他字段的更新继续进行 + Console.WriteLine($"更新图片 {pictureId} 的嵌入向量时出错: {ex.Message}"); + // 不设置 picture.Embedding,保持原值不变 + } } if (tags != null) diff --git a/Web/src/api/index.ts b/Web/src/api/index.ts index 3ce6a2c..ce67681 100644 --- a/Web/src/api/index.ts +++ b/Web/src/api/index.ts @@ -24,7 +24,8 @@ export { unfavoritePicture, getUserFavorites, uploadPicture, - deleteMultiplePictures, // 添加导出删除图片函数 + deleteMultiplePictures, + updatePicture, } from './pictureApi'; // 导出Album API diff --git a/Web/src/api/pictureApi.ts b/Web/src/api/pictureApi.ts index 8ae62ed..eb67d83 100644 --- a/Web/src/api/pictureApi.ts +++ b/Web/src/api/pictureApi.ts @@ -1,4 +1,4 @@ -import type { PaginatedResult, PictureResponse, FilteredPicturesRequest, BaseResult } from './types'; +import type { PaginatedResult, PictureResponse, FilteredPicturesRequest, BaseResult, UpdatePictureRequest } from './types'; import { fetchApi, BASE_URL } from './fetchClient'; // 获取图片列表 @@ -196,3 +196,11 @@ export async function deleteMultiplePictures(pictureIds: number[]): Promise> { + return fetchApi('/picture/update_picture', { + method: 'POST', + body: JSON.stringify(request), + }); +} + diff --git a/Web/src/api/types.ts b/Web/src/api/types.ts index c30a8c1..7d5374a 100644 --- a/Web/src/api/types.ts +++ b/Web/src/api/types.ts @@ -220,3 +220,10 @@ export interface UpdateUserRequest { currentPassword?: string; newPassword?: string; } + +export interface UpdatePictureRequest { + id: number; + name?: string; + description?: string; + tags?: string[]; +} diff --git a/Web/src/components/image/EditImageDialog.css b/Web/src/components/image/EditImageDialog.css new file mode 100644 index 0000000..b65748e --- /dev/null +++ b/Web/src/components/image/EditImageDialog.css @@ -0,0 +1,55 @@ +.edit-image-dialog .ant-modal-body { + padding: 20px; +} + +.edit-image-container { + display: flex; + flex-direction: column; + gap: 20px; +} + +.edit-image-preview { + text-align: center; + margin-bottom: 10px; +} + +.edit-preview-img { + max-width: 100%; + max-height: 200px; + object-fit: contain; + border-radius: 4px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.edit-image-form { + width: 100%; +} + +.edit-form-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-bottom: 0; + margin-top: 10px; +} + +.custom-tag { + display: inline-flex; + align-items: center; + padding: 2px 8px; + margin-right: 6px; + background-color: #f0f0f0; + border-radius: 4px; + font-size: 12px; + color: #333; +} + +.custom-tag-close { + margin-left: 4px; + cursor: pointer; + opacity: 0.6; +} + +.custom-tag-close:hover { + opacity: 1; +} diff --git a/Web/src/components/image/EditImageDialog.tsx b/Web/src/components/image/EditImageDialog.tsx new file mode 100644 index 0000000..14623ca --- /dev/null +++ b/Web/src/components/image/EditImageDialog.tsx @@ -0,0 +1,185 @@ +import React, { useState, useEffect } from 'react'; +import { Modal, Form, Input, Button, message, Select, Spin } from 'antd'; +import { type PictureResponse, updatePicture } from '../../api'; +import './EditImageDialog.css'; + +const { TextArea } = Input; +const { Option } = Select; + +interface EditImageDialogProps { + visible: boolean; + onClose: () => void; + image: PictureResponse | null; + onSuccess?: (updatedImage: PictureResponse) => void; +} + +const EditImageDialog: React.FC = ({ + visible, + onClose, + image, + onSuccess +}) => { + const [form] = Form.useForm(); + const [submitting, setSubmitting] = useState(false); + + // 当图片信息变化时重置表单 + useEffect(() => { + if (visible && image) { + form.setFieldsValue({ + name: image.name, + description: image.description, + tags: image.tags || [] + }); + } + }, [visible, image, form]); + + // 提交表单 + const handleSubmit = async () => { + if (!image) return; + + try { + const values = await form.validateFields(); + setSubmitting(true); + + const result = await updatePicture({ + id: image.id, + name: values.name, + description: values.description, + tags: values.tags + }); + + if (result.success) { + message.success('图片信息已成功更新'); + if (onSuccess) { + onSuccess(result.data!); + } + onClose(); + } else { + // 根据错误信息判断提示内容 + if (result.message && result.message.includes('向量')) { + message.warning('图片信息已更新,但AI索引生成失败。您的图片可能在高级搜索中不可见,但基本功能不受影响。'); + // 尽管AI索引失败,但其他信息已更新,所以仍然调用成功回调 + if (onSuccess && result.data) { + onSuccess(result.data); + } + onClose(); + } else { + message.error(result.message || '更新图片失败'); + } + } + } catch (error) { + if (error instanceof Error) { + // 解析常见错误消息并提供友好提示 + const errorMsg = error.message; + if (errorMsg.includes('401') || errorMsg.includes('Unauthorized')) { + message.error('AI服务授权失败,请检查服务配置'); + } else if (errorMsg.includes('vector') || errorMsg.includes('dimension')) { + message.error('AI索引生成失败,请稍后重试'); + } else { + message.error(`提交失败: ${errorMsg}`); + } + } else { + message.error('提交失败,请检查表单'); + } + } finally { + setSubmitting(false); + } + }; + + // 自定义标签选择 + const tagRender = (props: any) => { + const { label, closable, onClose } = props; + return ( +
+ #{label} + {closable && ( + × + )} +
+ ); + }; + + if (!image) return null; + + return ( + +
+
+ {image.name} +
+ + +
+ + + + + +