From fb071e71a14fc91251ff5daa55e722d232df5379 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Mon, 7 Sep 2026 04:08:54 -0600 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=88=86=E4=BA=AB?= =?UTF-8?q?=E9=93=BE=E6=8E=A5=E7=9A=84=E5=A4=9A=E4=B8=AA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - reset() 清空时一并剥离 share 参数,否则重载后分享代码会被写回 - share() 以去掉 query/share 的地址为基址,避免预设代码在 init 里覆盖分享内容 - 打开分享链接前比对本地已存代码,会覆盖用户改动时先弹窗确认 - SQL 分享带上选中的数据表 id,接收方运行结果与分享者一致 - 分享负载的 code/table 字段补类型校验 - 链接损坏或预设加载失败时给出提示,不再静默吞掉 - 剪贴板写入失败时不再提示"已复制" 组件外的 init 阶段拿不到 useMessage(),新增 composables/notice.ts 用 createDiscreteApi 提供提示与确认框,主题跟随 useDark()。 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CWT2cy7mkZevZR4WVVfAVu --- src/composables/code.ts | 96 ++++++++++++++++++++++++++++++------- src/composables/notice.ts | 15 ++++++ src/composables/sqlTable.ts | 16 ++++--- src/desktop/Header.vue | 7 ++- src/mobile/Header.vue | 7 ++- 5 files changed, 112 insertions(+), 29 deletions(-) create mode 100644 src/composables/notice.ts diff --git a/src/composables/code.ts b/src/composables/code.ts index d0bab61..252f608 100644 --- a/src/composables/code.ts +++ b/src/composables/code.ts @@ -1,15 +1,22 @@ import { useStorage } from "@vueuse/core" import copyTextToClipboard from "copy-text-to-clipboard" import qs from "query-string" -import { reactive, ref, watch } from "vue" +import { nextTick, reactive, ref, watch } from "vue" import { formatCode, getCodeByQuery, submit } from "../api" import { sources } from "../templates" import { Cache, Code, LANGUAGE, Status } from "../types" import { atou, utoa } from "../utils" import { isMobile } from "./breakpoints" -import { buildSqlScript, resetSqlTableSelection } from "./sqlTable" +import { dialog, notice } from "./notice" +import { + buildSqlScript, + resetSqlTableSelection, + selectSqlTable, + selectedTableId, +} from "./sqlTable" const defaultLanguage = "python" +const languages: LANGUAGE[] = ["python", "c", "cpp", "turtle", "sql"] const cache: Cache = { language: useStorage("code_language", defaultLanguage), @@ -62,6 +69,58 @@ watch(input, (value: string) => { cache.input.value = value }) +interface Shared { + lang: LANGUAGE + code: string + input: string + table?: string +} + +// 链接内容完全来自 URL,字段都要校验后才能落到编辑器里 +function parseShared(base64: string): Shared { + const data = JSON.parse(atou(base64)) + const lang = languages.includes(data.lang) + ? (data.lang as LANGUAGE) + : defaultLanguage + return { + lang, + code: typeof data.code === "string" ? data.code : sources[lang], + input: typeof data.input === "string" ? data.input : "", + table: typeof data.table === "string" ? data.table : undefined, + } +} + +function confirmOverwrite() { + return new Promise((resolve) => { + dialog.warning({ + title: "打开分享的代码", + content: "这会覆盖你当前保存的代码,是否继续?", + positiveText: "打开分享", + negativeText: "保留我的代码", + onPositiveClick: () => resolve(true), + onNegativeClick: () => resolve(false), + onClose: () => resolve(false), + onMaskClick: () => resolve(false), + }) + }) +} + +async function applyShared(shared: Shared) { + const saved = cache.code[shared.lang].value + const safe = saved === sources[shared.lang] || saved === shared.code + if (!safe && !(await confirmOverwrite())) return + + cache.code[shared.lang].value = shared.code + code.language = shared.lang + code.value = shared.code + input.value = shared.input + if (shared.lang === "sql") { + // 切换语言的 watch 会重置选中的表,要等它跑完再应用分享里的表 + await nextTick() + selectSqlTable(shared.table) + } +} + export async function init() { code.language = cache.language.value code.value = cache.code[code.language].value @@ -72,24 +131,24 @@ export async function init() { const parsed = qs.parse(location.search) const base64 = parsed.share as string if (base64) { + let shared: Shared try { - const data = JSON.parse(atou(base64)) - const lang = ["python", "c", "cpp", "turtle", "sql"].includes(data.lang) - ? (data.lang as LANGUAGE) - : defaultLanguage - const sharedCode = data.code ?? sources[lang] - cache.code[lang].value = sharedCode - code.language = lang - code.value = sharedCode - input.value = typeof data.input === "string" ? data.input : "" - } catch (err) {} + shared = parseShared(base64) + } catch (err) { + notice.error("分享链接已损坏,可能在传输中被截断") + return + } + await applyShared(shared) + return } const preset = parsed.query as string if (preset) { try { const result = await getCodeByQuery(preset) code.value = result.data.code - } catch (err) {} + } catch (err) { + notice.error("预设代码加载失败") + } } } @@ -102,7 +161,7 @@ export function reset() { cache.code[code.language].value = sources[code.language] output.value = "" status.value = Status.NotStarted - const url = qs.exclude(location.href, ["query"]) + const url = qs.exclude(location.href, ["query", "share"]) window.location.href = url } @@ -131,15 +190,16 @@ export async function run() { } export function share() { - const data = { + const data: Shared = { lang: code.language, code: code.value, input: input.value, } + if (code.language === "sql") data.table = selectedTableId.value const base64 = utoa(JSON.stringify(data)) - copyTextToClipboard( - qs.stringifyUrl({ url: location.href, query: { share: base64 } }), - ) + // 基址要去掉 query(预设代码会在 init 里覆盖分享内容)和上一次的 share + const url = qs.exclude(location.href, ["query", "share"]) + return copyTextToClipboard(qs.stringifyUrl({ url, query: { share: base64 } })) } export async function format() { diff --git a/src/composables/notice.ts b/src/composables/notice.ts new file mode 100644 index 0000000..fd8afeb --- /dev/null +++ b/src/composables/notice.ts @@ -0,0 +1,15 @@ +import { useDark } from "@vueuse/core" +import { createDiscreteApi, darkTheme } from "naive-ui" +import { computed } from "vue" + +const isDark = useDark() + +// 组件外(如 init 阶段)需要提示时用这套脱离上下文的 API,主题跟随 App.vue +const api = createDiscreteApi(["message", "dialog"], { + configProviderProps: computed(() => ({ + theme: isDark.value ? darkTheme : null, + })), +}) + +export const notice = api.message +export const dialog = api.dialog diff --git a/src/composables/sqlTable.ts b/src/composables/sqlTable.ts index f860116..c81ed85 100644 --- a/src/composables/sqlTable.ts +++ b/src/composables/sqlTable.ts @@ -1,9 +1,5 @@ import { ref } from "vue" -import { - buildSetupSql, - defaultSqlTableId, - sqlTables, -} from "../data/sqlTables" +import { buildSetupSql, defaultSqlTableId, sqlTables } from "../data/sqlTables" export const selectedTableId = ref(defaultSqlTableId) @@ -11,6 +7,13 @@ export function resetSqlTableSelection() { selectedTableId.value = defaultSqlTableId } +// 分享链接里的表 id 不可信,认不出来就退回默认表 +export function selectSqlTable(id: unknown) { + selectedTableId.value = sqlTables.some((item) => item.id === id) + ? (id as string) + : defaultSqlTableId +} + // SELECT / WITH 属于查询,直接展示查询结果的列;其余(增删改)回显整张表 function isQuery(sql: string): boolean { return /^\s*(SELECT|WITH)\b/i.test(sql) @@ -18,8 +21,7 @@ function isQuery(sql: string): boolean { export function buildSqlScript(studentSql: string) { const table = - sqlTables.find((item) => item.id === selectedTableId.value) ?? - sqlTables[0] + sqlTables.find((item) => item.id === selectedTableId.value) ?? sqlTables[0] const normalizedSql = studentSql.trim().replace(/;?\s*$/, ";") if (isQuery(studentSql.trim())) { return [buildSetupSql(table), ".headers on", normalizedSql].join("\n\n") diff --git a/src/desktop/Header.vue b/src/desktop/Header.vue index f84ac97..7ecf0f8 100644 --- a/src/desktop/Header.vue +++ b/src/desktop/Header.vue @@ -9,8 +9,11 @@ import { code, loading, run, share, size } from "../composables/code" const message = useMessage() function handleShare() { - share() - message.success("分享链接已复制") + if (share()) { + message.success("分享链接已复制") + } else { + message.error("复制失败,请检查浏览器剪贴板权限") + } } diff --git a/src/mobile/Header.vue b/src/mobile/Header.vue index f671024..87f8b20 100644 --- a/src/mobile/Header.vue +++ b/src/mobile/Header.vue @@ -17,8 +17,11 @@ function copy() { } function handleShare() { - share() - message.success("分享链接已复制") + if (share()) { + message.success("分享链接已复制") + } else { + message.error("复制失败,请检查浏览器剪贴板权限") + } } const menu: DropdownOption[] = [