From f7cb16b70374a43eea16667440d46994db0a5e4e Mon Sep 17 00:00:00 2001 From: tianma-if Date: Mon, 17 Aug 2026 02:54:08 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=A7=BB=E5=8A=A8=E7=AB=AF?= =?UTF-8?q?=E6=99=BA=E8=83=BD=E6=A0=87=E7=AD=BE=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EdgeEver/DesignSystem/ChromeParity.swift | 1 + .../Features/Workspace/MemoEditView.swift | 295 +++++++----------- .../EdgeEverTests/ChromeParityTests.swift | 3 + apps/mobile/src/screens/WorkspaceScreen.tsx | 252 +++++++-------- apps/mobile/src/screens/workspace-styles.ts | 116 +------ 5 files changed, 240 insertions(+), 427 deletions(-) diff --git a/apps/ios/EdgeEver/DesignSystem/ChromeParity.swift b/apps/ios/EdgeEver/DesignSystem/ChromeParity.swift index 71c44664..91c26a8a 100644 --- a/apps/ios/EdgeEver/DesignSystem/ChromeParity.swift +++ b/apps/ios/EdgeEver/DesignSystem/ChromeParity.swift @@ -14,6 +14,7 @@ enum CreateMemoChrome { static let metaRow = "createMemoMetaRow" static let notebook = "createMemoNotebookButton" static let tags = "createMemoTagsInput" + static let smartTags = "createMemoSmartTagsButton" static let editorFrame = "createMemoEditorFrame" static let imageTool = "createMemoImageTool" } diff --git a/apps/ios/EdgeEver/Features/Workspace/MemoEditView.swift b/apps/ios/EdgeEver/Features/Workspace/MemoEditView.swift index d7c07bf9..8ad23995 100644 --- a/apps/ios/EdgeEver/Features/Workspace/MemoEditView.swift +++ b/apps/ios/EdgeEver/Features/Workspace/MemoEditView.swift @@ -51,6 +51,12 @@ struct MemoEditView: View { @State private var showEmptyAiSelectionAlert = false @State private var aiUndoToken: UUID? @State private var didImportSharedImages = false + @State private var isSuggestingTags = false + @State private var smartTagsAdded = false + @State private var showSmartTagAlert = false + @State private var smartTagAlertTitle = "" + @State private var smartTagAlertMessage = "" + @State private var smartTagTask: Task? private var title: String { get { viewModel.title } nonmutating set { viewModel.title = newValue } } private var tagsText: String { get { viewModel.tagsText } nonmutating set { viewModel.tagsText = newValue } } @@ -148,9 +154,7 @@ struct MemoEditView: View { } .sheet(isPresented: $showTagPicker) { MemoTagPickerSheet( - selectedTags: viewModel.tags, - title: title, - contentMarkdown: contentMarkdown + selectedTags: viewModel.tags ) { tags in tagsText = tags.joined(separator: ", ") markDirtyAndScheduleSave() @@ -198,6 +202,11 @@ struct MemoEditView: View { en: "Select some text in the note body, then tap AI again." )) } + .alert(smartTagAlertTitle, isPresented: $showSmartTagAlert) { + Button(env.preferences.t("好的", en: "OK"), role: .cancel) {} + } message: { + Text(smartTagAlertMessage) + } .alert( env.preferences.t("应用模板?", en: "Apply template?"), isPresented: $showApplyTemplateConfirm @@ -291,6 +300,8 @@ struct MemoEditView: View { await importInitialSharedImagesIfNeeded() } .onDisappear { + smartTagTask?.cancel() + smartTagTask = nil viewModel.cancelScheduledSave() // Create commit is owned by Back / Done (Android `requestClose` = createMutation). // Only flush edit sessions, or create-after-image-materialize if still dirty and @@ -476,6 +487,35 @@ struct MemoEditView: View { .accessibilityLabel(env.preferences.t("笔记标签", en: "Tags")) .accessibilityIdentifier(CreateMemoChrome.tags) + Button { + generateAndApplySmartTags() + } label: { + Group { + if isSuggestingTags { + ProgressView() + .controlSize(.small) + .tint(AppTheme.accentStrong) + } else { + Image(systemName: smartTagsAdded ? "checkmark" : "sparkles") + .font(.system(size: 14, weight: .semibold)) + .foregroundStyle(AppTheme.accentStrong) + } + } + .frame(width: 34, height: 34) + .background(smartTagsAdded ? AppTheme.accentSoft : Color.clear) + .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) + } + .buttonStyle(.plain) + .disabled(isSuggestingTags || busyChrome || viewModel.tags.count >= 24 + || (title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + && contentMarkdown.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)) + .opacity((busyChrome || viewModel.tags.count >= 24) ? 0.45 : 1) + .accessibilityLabel(env.preferences.t( + isSuggestingTags ? "正在生成智能标签" : smartTagsAdded ? "智能标签已添加" : "智能标签", + en: isSuggestingTags ? "Generating smart tags" : smartTagsAdded ? "Smart tags added" : "Smart tags" + )) + .accessibilityIdentifier(CreateMemoChrome.smartTags) + } .frame(minHeight: 40) .accessibilityIdentifier(CreateMemoChrome.metaRow) @@ -702,6 +742,70 @@ struct MemoEditView: View { scheduleSave() } + private func generateAndApplySmartTags() { + guard !isSuggestingTags, !busyChrome, viewModel.tags.count < 24 else { return } + smartTagTask?.cancel() + isSuggestingTags = true + smartTagsAdded = false + smartTagTask = Task { @MainActor in + await pullEditorSnapshotIfPossible() + let currentTags = viewModel.tags + let input = AiTagSuggestionsInput( + title: title, + contentMarkdown: contentMarkdown, + currentTags: currentTags, + locale: env.preferences.isEnglish ? "en-US" : "zh-CN" + ) + do { + let response = try await env.session.client.suggestAiTags(input) + try Task.checkCancellation() + let availableSlots = max(0, 24 - currentTags.count) + let additions = Array(response.suggestions + .map(\.name) + .filter { name in + !currentTags.contains { $0.caseInsensitiveCompare(name) == .orderedSame } + } + .prefix(availableSlots)) + guard !additions.isEmpty else { + isSuggestingTags = false + smartTagAlertTitle = env.preferences.t("智能标签", en: "Smart tags") + smartTagAlertMessage = env.preferences.t( + "没有找到适合这篇笔记的新标签。", + en: "No useful new tags were found for this note." + ) + showSmartTagAlert = true + smartTagTask = nil + return + } + tagsText = (currentTags + additions).joined(separator: ", ") + markDirtyAndScheduleSave() + isSuggestingTags = false + smartTagsAdded = true + try? await Task.sleep(nanoseconds: 4_000_000_000) + guard !Task.isCancelled else { return } + smartTagsAdded = false + smartTagTask = nil + } catch is CancellationError { + isSuggestingTags = false + } catch let apiError as APIError where apiError.code == "ai_not_configured" { + isSuggestingTags = false + smartTagAlertTitle = env.preferences.t("智能标签生成失败", en: "Couldn't generate smart tags") + smartTagAlertMessage = env.preferences.t( + "请先在“AI 集成”中配置默认模型。", + en: "Configure a model in AI Integrations first." + ) + showSmartTagAlert = true + smartTagTask = nil + } catch { + isSuggestingTags = false + smartTagAlertTitle = env.preferences.t("智能标签生成失败", en: "Couldn't generate smart tags") + smartTagAlertMessage = error.localizedDescription + showSmartTagAlert = true + smartTagTask = nil + } + } + } + private func requestApplyTemplate(_ seed: CreateMemoSeed) { let current = CreateMemoSeed(title: title, contentMarkdown: contentMarkdown, tagsText: tagsText) if current.hasContent { @@ -1213,24 +1317,13 @@ private struct MemoTagPickerSheet: View { @State private var query = "" @State private var availableTags: [TagSummary] = [] @State private var error: String? - @State private var aiSuggestions: [AiTagSuggestion]? - @State private var selectedAiSuggestions = Set() - @State private var aiError: String? - @State private var isSuggesting = false - @State private var suggestionTask: Task? - let title: String - let contentMarkdown: String let onChange: ([String]) -> Void init( selectedTags: [String], - title: String, - contentMarkdown: String, onChange: @escaping ([String]) -> Void ) { _selection = State(initialValue: selectedTags) - self.title = title - self.contentMarkdown = contentMarkdown self.onChange = onChange } @@ -1295,8 +1388,6 @@ private struct MemoTagPickerSheet: View { .overlay(RoundedRectangle(cornerRadius: 9).stroke(AppTheme.border, lineWidth: 1)) .padding(.horizontal, 16) - aiSuggestionCard - if let error { Text(error).font(.system(size: 13)).foregroundStyle(AppTheme.danger) } @@ -1337,111 +1428,9 @@ private struct MemoTagPickerSheet: View { } } .task { loadTags() } - .onDisappear { - suggestionTask?.cancel() - suggestionTask = nil - } } } - private var aiSuggestionCard: some View { - VStack(alignment: .leading, spacing: 10) { - HStack(alignment: .top, spacing: 10) { - VStack(alignment: .leading, spacing: 3) { - Label(env.preferences.t("AI 推荐标签", en: "AI tag suggestions"), systemImage: "sparkles") - .font(.system(size: 14, weight: .bold)) - .foregroundStyle(AppTheme.accentText) - Text(env.preferences.t( - "确认后再添加到当前笔记;合适时会优先复用已有标签。", - en: "Review suggestions before adding them. Existing tags are preferred when they fit." - )) - .font(.system(size: 12)) - .foregroundStyle(AppTheme.accentStrong) - } - Spacer(minLength: 4) - Button { - requestAiSuggestions() - } label: { - HStack(spacing: 5) { - if isSuggesting { - ProgressView().tint(.white).controlSize(.small) - } else { - Image(systemName: "sparkles") - } - Text(env.preferences.t( - aiSuggestions == nil ? "生成建议" : "重新生成", - en: aiSuggestions == nil ? "Suggest" : "Suggest again" - )) - } - .font(.system(size: 12, weight: .bold)) - .foregroundStyle(.white) - .padding(.horizontal, 10) - .frame(minHeight: 34) - .background(AppTheme.accentAction) - .clipShape(RoundedRectangle(cornerRadius: 8)) - } - .buttonStyle(.plain) - .disabled(isSuggesting || (title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty - && contentMarkdown.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)) - .opacity(isSuggesting ? 0.6 : 1) - } - - if let aiError { - Text(aiError).font(.system(size: 12, weight: .semibold)).foregroundStyle(AppTheme.danger) - } - - if let aiSuggestions { - if aiSuggestions.isEmpty { - Text(env.preferences.t("没有找到适合这篇笔记的标签。", en: "No useful tags were found for this note.")) - .font(.system(size: 12)) - .foregroundStyle(AppTheme.accentStrong) - } else { - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 7) { - ForEach(aiSuggestions) { suggestion in - let alreadySelected = containsTag(suggestion.name, in: selection) - let chosen = alreadySelected || selectedAiSuggestions.contains(suggestion.name) - Button { - toggleAiSuggestion(suggestion.name) - } label: { - HStack(spacing: 4) { - if chosen { Image(systemName: "checkmark") } - Text("#\(suggestion.name) · \(suggestionLabel(suggestion, alreadySelected: alreadySelected))") - } - .font(.system(size: 12, weight: .semibold)) - .foregroundStyle(alreadySelected ? AppTheme.accentStrong : chosen ? Color.white : AppTheme.accentText) - .padding(.horizontal, 10) - .frame(minHeight: 30) - .background(alreadySelected ? AppTheme.accentSoft : chosen ? AppTheme.accentAction : AppTheme.card) - .clipShape(Capsule()) - .overlay(Capsule().stroke(AppTheme.accentBorder, lineWidth: 1)) - } - .buttonStyle(.plain) - .disabled(alreadySelected) - } - } - } - - Button(env.preferences.t( - "添加选中标签(\(selectedAiSuggestions.count))", - en: "Add selected (\(selectedAiSuggestions.count))" - )) { - applyAiSuggestions() - } - .font(.system(size: 12, weight: .bold)) - .buttonStyle(.borderedProminent) - .tint(AppTheme.title) - .disabled(selectedAiSuggestions.isEmpty) - } - } - } - .padding(12) - .background(AppTheme.accentSoft) - .clipShape(RoundedRectangle(cornerRadius: 10)) - .overlay(RoundedRectangle(cornerRadius: 10).stroke(AppTheme.accentBorder, lineWidth: 1)) - .padding(.horizontal, 16) - } - private func loadTags() { guard let scope = env.session.dataScope else { return } do { @@ -1472,76 +1461,6 @@ private struct MemoTagPickerSheet: View { onChange(selection) } - private func requestAiSuggestions() { - suggestionTask?.cancel() - isSuggesting = true - aiError = nil - let input = AiTagSuggestionsInput( - title: title, - contentMarkdown: contentMarkdown, - currentTags: selection, - locale: env.preferences.isEnglish ? "en-US" : "zh-CN" - ) - suggestionTask = Task { @MainActor in - do { - let response = try await env.session.client.suggestAiTags(input) - try Task.checkCancellation() - aiSuggestions = response.suggestions - let availableSlots = max(0, 24 - selection.count) - selectedAiSuggestions = Set( - response.suggestions - .filter { !containsTag($0.name, in: selection) } - .prefix(availableSlots) - .map(\.name) - ) - } catch is CancellationError { - return - } catch let apiError as APIError where apiError.code == "ai_not_configured" { - aiSuggestions = nil - selectedAiSuggestions = [] - aiError = env.preferences.t( - "请先在“AI 集成”中配置默认模型。", - en: "Configure a model in AI Integrations first." - ) - } catch { - aiSuggestions = nil - selectedAiSuggestions = [] - aiError = error.localizedDescription - } - isSuggesting = false - suggestionTask = nil - } - } - - private func toggleAiSuggestion(_ name: String) { - if selectedAiSuggestions.contains(name) { - selectedAiSuggestions.remove(name) - } else if selection.count + selectedAiSuggestions.count < 24 { - selectedAiSuggestions.insert(name) - } - } - - private func applyAiSuggestions() { - for suggestion in aiSuggestions ?? [] - where selectedAiSuggestions.contains(suggestion.name) - && selection.count < 24 - && !containsTag(suggestion.name, in: selection) { - selection.append(suggestion.name) - } - selectedAiSuggestions = [] - onChange(selection) - } - - private func containsTag(_ name: String, in tags: [String]) -> Bool { - tags.contains { $0.caseInsensitiveCompare(name) == .orderedSame } - } - - private func suggestionLabel(_ suggestion: AiTagSuggestion, alreadySelected: Bool) -> String { - if alreadySelected { return env.preferences.t("已添加", en: "Added") } - return suggestion.existing - ? env.preferences.t("已有", en: "Existing") - : env.preferences.t("新建", en: "New") - } } diff --git a/apps/ios/Tests/EdgeEverTests/ChromeParityTests.swift b/apps/ios/Tests/EdgeEverTests/ChromeParityTests.swift index ac5facd4..f688e7b0 100644 --- a/apps/ios/Tests/EdgeEverTests/ChromeParityTests.swift +++ b/apps/ios/Tests/EdgeEverTests/ChromeParityTests.swift @@ -100,6 +100,9 @@ final class ChromeParityTests: XCTestCase { XCTAssertTrue(src.contains("CreateMemoChrome.metaRow"), "missing meta row") XCTAssertTrue(src.contains("CreateMemoChrome.notebook"), "missing notebook control") XCTAssertTrue(src.contains("CreateMemoChrome.tags"), "missing tags field") + XCTAssertTrue(src.contains("CreateMemoChrome.smartTags"), "missing one-tap smart tags control") + XCTAssertTrue(src.contains("generateAndApplySmartTags"), "smart tags must generate and apply in one action") + XCTAssertFalse(src.contains("AI 推荐标签"), "tag picker must not retain the multi-step AI suggestion flow") XCTAssertTrue(src.contains("CreateMemoChrome.editorFrame"), "missing bordered editor") XCTAssertTrue(src.contains("TipTapWebView"), "editor wiring must remain") XCTAssertTrue(src.contains("commitCreate") || src.contains("persistDraftOrQueue"), "save path must remain") diff --git a/apps/mobile/src/screens/WorkspaceScreen.tsx b/apps/mobile/src/screens/WorkspaceScreen.tsx index b61cf2d7..6df04845 100644 --- a/apps/mobile/src/screens/WorkspaceScreen.tsx +++ b/apps/mobile/src/screens/WorkspaceScreen.tsx @@ -1789,33 +1789,118 @@ const NotebookPickerModal = ({ ); }; -const TagPickerModal = ({ +const SmartTagButton = ({ client, contentMarkdown, + disabled = false, + onChange, + selectedTags, + title, +}: { + client: ReturnType["client"]; + contentMarkdown: string; + disabled?: boolean; + onChange: (tags: string[]) => void; + selectedTags: string[]; + title: string; +}) => { + const { resolvedLocale, translate } = useMobileLocale(); + const [status, setStatus] = useState<"idle" | "loading" | "success">("idle"); + const controllerRef = useRef(null); + const feedbackTimerRef = useRef | null>(null); + const unavailable = disabled || !client || selectedTags.length >= 24 || (!title.trim() && !contentMarkdown.trim()); + + useEffect(() => () => { + controllerRef.current?.abort(); + if (feedbackTimerRef.current) clearTimeout(feedbackTimerRef.current); + }, []); + + const generateAndApplyTags = async () => { + if (unavailable || !client) return; + controllerRef.current?.abort(); + if (feedbackTimerRef.current) clearTimeout(feedbackTimerRef.current); + const controller = new AbortController(); + controllerRef.current = controller; + setStatus("loading"); + try { + const selectedTagKeys = new Set(selectedTags.map((tag) => tag.toLocaleLowerCase())); + const result = await client.suggestAiTags({ + title, + contentMarkdown, + currentTags: selectedTags, + locale: resolvedLocale, + }, controller.signal); + const additions = result.suggestions + .filter((suggestion) => !selectedTagKeys.has(suggestion.name.toLocaleLowerCase())) + .slice(0, Math.max(0, 24 - selectedTags.length)) + .map((suggestion) => suggestion.name); + if (additions.length === 0) { + setStatus("idle"); + Alert.alert(translate("智能标签"), translate("没有找到适合这篇笔记的新标签。")); + return; + } + onChange(Array.from(new Set([...selectedTags, ...additions])).slice(0, 24)); + setStatus("success"); + feedbackTimerRef.current = setTimeout(() => { + setStatus("idle"); + feedbackTimerRef.current = null; + }, 4000); + } catch (error) { + if (controller.signal.aborted) return; + setStatus("idle"); + Alert.alert( + translate("智能标签生成失败"), + error instanceof ApiRequestError && error.code === "ai_not_configured" + ? translate("请先在“AI 集成”中配置默认模型。") + : error instanceof Error + ? error.message + : translate("AI 标签建议生成失败。") + ); + } finally { + if (controllerRef.current === controller) controllerRef.current = null; + } + }; + + const accessibilityLabel = status === "loading" + ? translate("正在生成智能标签") + : status === "success" + ? translate("智能标签已添加") + : translate("智能标签"); + + return ( + void generateAndApplyTags()} + style={[styles.smartTagButton, status === "success" && styles.smartTagButtonSuccess, unavailable && styles.buttonDisabled]} + > + {status === "loading" + ? + : status === "success" + ? + : } + + ); +}; + +const TagPickerModal = ({ dataScope, onChange, onClose, selectedTags, - title, visible, }: { - client: ReturnType["client"]; - contentMarkdown: string; dataScope: string; onChange: (tags: string[]) => void; onClose: () => void; selectedTags: string[]; - title: string; visible: boolean; }) => { - const { resolvedLocale, translate } = useMobileLocale(); + const { translate } = useMobileLocale(); const safeAreaInsets = useSafeAreaInsets(); const [searchText, setSearchText] = useState(""); - const [aiSuggestions, setAiSuggestions] = useState["suggestAiTags"]>>["suggestions"] | null>(null); - const [selectedAiSuggestions, setSelectedAiSuggestions] = useState([]); - const [aiError, setAiError] = useState(null); - const [aiLoading, setAiLoading] = useState(false); - const aiControllerRef = useRef(null); const tagsQuery = useQuery({ queryKey: ["mobile-tags", dataScope], queryFn: () => listLocalTags(dataScope), @@ -1829,18 +1914,9 @@ const TagPickerModal = ({ useEffect(() => { if (visible) { setSearchText(""); - setAiSuggestions(null); - setSelectedAiSuggestions([]); - setAiError(null); - return; } - aiControllerRef.current?.abort(); - aiControllerRef.current = null; - setAiLoading(false); }, [visible]); - useEffect(() => () => aiControllerRef.current?.abort(), []); - const commit = (nextTags: string[]) => onChange(Array.from(new Set(nextTags)).slice(0, 24)); const toggleTag = (name: string) => commit( selectedTags.includes(name) ? selectedTags.filter((tag) => tag !== name) : [...selectedTags, name] @@ -1851,53 +1927,6 @@ const TagPickerModal = ({ commit([...selectedTags, ...additions]); setSearchText(""); }; - const selectedTagKeys = new Set(selectedTags.map((tag) => tag.toLocaleLowerCase())); - const requestAiSuggestions = async () => { - if (!client || (!title.trim() && !contentMarkdown.trim())) return; - aiControllerRef.current?.abort(); - const controller = new AbortController(); - aiControllerRef.current = controller; - setAiLoading(true); - setAiError(null); - try { - const result = await client.suggestAiTags({ - title, - contentMarkdown, - currentTags: selectedTags, - locale: resolvedLocale, - }, controller.signal); - setAiSuggestions(result.suggestions); - setSelectedAiSuggestions( - result.suggestions - .filter((suggestion) => !selectedTagKeys.has(suggestion.name.toLocaleLowerCase())) - .slice(0, Math.max(0, 24 - selectedTags.length)) - .map((suggestion) => suggestion.name) - ); - } catch (error) { - if (controller.signal.aborted) return; - setAiSuggestions(null); - setSelectedAiSuggestions([]); - setAiError( - error instanceof ApiRequestError && error.code === "ai_not_configured" - ? translate("请先在“AI 集成”中配置默认模型。") - : error instanceof Error - ? error.message - : translate("AI 标签建议生成失败。") - ); - } finally { - if (aiControllerRef.current === controller) { - aiControllerRef.current = null; - setAiLoading(false); - } - } - }; - const toggleAiSuggestion = (name: string) => setSelectedAiSuggestions((current) => - current.includes(name) ? current.filter((tag) => tag !== name) : [...current, name] - ); - const applyAiSuggestions = () => { - commit([...selectedTags, ...selectedAiSuggestions]); - setSelectedAiSuggestions([]); - }; return ( @@ -1947,67 +1976,6 @@ const TagPickerModal = ({ ) : null} - - - - - - {translate("AI 推荐标签")} - - {translate("确认后再添加到当前笔记;合适时会优先复用已有标签。")} - - void requestAiSuggestions()} - style={[styles.tagPickerAiGenerateButton, (aiLoading || !client || (!title.trim() && !contentMarkdown.trim())) && styles.buttonDisabled]} - > - {aiLoading ? : } - {translate(aiSuggestions ? "重新生成" : "生成建议")} - - - - {aiError ? {aiError} : null} - {aiSuggestions ? ( - aiSuggestions.length > 0 ? ( - <> - - {aiSuggestions.map((suggestion) => { - const alreadySelected = selectedTagKeys.has(suggestion.name.toLocaleLowerCase()); - const chosen = alreadySelected || selectedAiSuggestions.includes(suggestion.name); - return ( - toggleAiSuggestion(suggestion.name)} - style={[styles.tagPickerAiSuggestion, chosen && styles.tagPickerAiSuggestionSelected, alreadySelected && styles.tagPickerAiSuggestionAdded]} - > - {chosen ? : null} - - #{suggestion.name} · {translate(alreadySelected ? "已添加" : suggestion.existing ? "已有" : "新建")} - - - ); - })} - - - {translate(`添加选中标签(${selectedAiSuggestions.length})`)} - - - ) : {translate("没有找到适合这篇笔记的标签。")} - ) : null} - - {tagsQuery.isLoading ? ( ) : visibleTags.length === 0 ? ( @@ -2610,6 +2578,16 @@ const CreateMemoModal = ({ + { + setTagsText(nextTags.join(", ")); + markDirty(); + }} + selectedTags={parseTags(tagsText)} + title={title} + /> @@ -2632,8 +2610,6 @@ const CreateMemoModal = ({ visible={notebookPickerOpen} /> { setTagsText(nextTags.join(", ")); @@ -2641,7 +2617,6 @@ const CreateMemoModal = ({ }} onClose={() => setTagPickerOpen(false)} selectedTags={parseTags(tagsText)} - title={title} visible={tagPickerOpen} /> + { + setTagsText(nextTags.join(", ")); + dirtyRef.current = true; + setDirty(true); + }} + selectedTags={parseTags(tagsText)} + title={title} + /> {draftRestored ? 已恢复上次未完成的本地草稿 : null} @@ -3212,8 +3199,6 @@ const RichEditorModal = ({ visible={notebookPickerOpen} /> { setTagsText(nextTags.join(", ")); @@ -3222,7 +3207,6 @@ const RichEditorModal = ({ }} onClose={() => setTagPickerOpen(false)} selectedTags={parseTags(tagsText)} - title={title} visible={tagPickerOpen} />