Compare commits

..

2 Commits

Author SHA1 Message Date
2afc81e9ab update
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled
2026-09-07 04:09:09 -06:00
fb071e71a1 fix: 修复分享链接的多个问题
- reset() 清空时一并剥离 share 参数,否则重载后分享代码会被写回
- share() 以去掉 query/share 的地址为基址,避免预设代码在 init 里覆盖分享内容
- 打开分享链接前比对本地已存代码,会覆盖用户改动时先弹窗确认
- SQL 分享带上选中的数据表 id,接收方运行结果与分享者一致
- 分享负载的 code/table 字段补类型校验
- 链接损坏或预设加载失败时给出提示,不再静默吞掉
- 剪贴板写入失败时不再提示"已复制"

组件外的 init 阶段拿不到 useMessage(),新增 composables/notice.ts
用 createDiscreteApi 提供提示与确认框,主题跟随 useDark()。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CWT2cy7mkZevZR4WVVfAVu
2026-09-07 04:08:54 -06:00
6 changed files with 114 additions and 31 deletions

View File

@@ -9,9 +9,9 @@ jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- uses: actions/setup-node@v4
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm

View File

@@ -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<LANGUAGE>("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<boolean>((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() {

15
src/composables/notice.ts Normal file
View File

@@ -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

View File

@@ -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")

View File

@@ -9,8 +9,11 @@ import { code, loading, run, share, size } from "../composables/code"
const message = useMessage()
function handleShare() {
share()
if (share()) {
message.success("分享链接已复制")
} else {
message.error("复制失败,请检查浏览器剪贴板权限")
}
}
</script>

View File

@@ -17,8 +17,11 @@ function copy() {
}
function handleShare() {
share()
if (share()) {
message.success("分享链接已复制")
} else {
message.error("复制失败,请检查浏览器剪贴板权限")
}
}
const menu: DropdownOption[] = [