Compare commits
2 Commits
4031bbe82e
...
2afc81e9ab
| Author | SHA1 | Date | |
|---|---|---|---|
| 2afc81e9ab | |||
| fb071e71a1 |
4
.github/workflows/deploy.yaml
vendored
4
.github/workflows/deploy.yaml
vendored
@@ -9,9 +9,9 @@ jobs:
|
|||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: npm
|
cache: npm
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
import { useStorage } from "@vueuse/core"
|
import { useStorage } from "@vueuse/core"
|
||||||
import copyTextToClipboard from "copy-text-to-clipboard"
|
import copyTextToClipboard from "copy-text-to-clipboard"
|
||||||
import qs from "query-string"
|
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 { formatCode, getCodeByQuery, submit } from "../api"
|
||||||
import { sources } from "../templates"
|
import { sources } from "../templates"
|
||||||
import { Cache, Code, LANGUAGE, Status } from "../types"
|
import { Cache, Code, LANGUAGE, Status } from "../types"
|
||||||
import { atou, utoa } from "../utils"
|
import { atou, utoa } from "../utils"
|
||||||
import { isMobile } from "./breakpoints"
|
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 defaultLanguage = "python"
|
||||||
|
const languages: LANGUAGE[] = ["python", "c", "cpp", "turtle", "sql"]
|
||||||
|
|
||||||
const cache: Cache = {
|
const cache: Cache = {
|
||||||
language: useStorage<LANGUAGE>("code_language", defaultLanguage),
|
language: useStorage<LANGUAGE>("code_language", defaultLanguage),
|
||||||
@@ -62,6 +69,58 @@ watch(input, (value: string) => {
|
|||||||
cache.input.value = value
|
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() {
|
export async function init() {
|
||||||
code.language = cache.language.value
|
code.language = cache.language.value
|
||||||
code.value = cache.code[code.language].value
|
code.value = cache.code[code.language].value
|
||||||
@@ -72,24 +131,24 @@ export async function init() {
|
|||||||
const parsed = qs.parse(location.search)
|
const parsed = qs.parse(location.search)
|
||||||
const base64 = parsed.share as string
|
const base64 = parsed.share as string
|
||||||
if (base64) {
|
if (base64) {
|
||||||
|
let shared: Shared
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(atou(base64))
|
shared = parseShared(base64)
|
||||||
const lang = ["python", "c", "cpp", "turtle", "sql"].includes(data.lang)
|
} catch (err) {
|
||||||
? (data.lang as LANGUAGE)
|
notice.error("分享链接已损坏,可能在传输中被截断")
|
||||||
: defaultLanguage
|
return
|
||||||
const sharedCode = data.code ?? sources[lang]
|
}
|
||||||
cache.code[lang].value = sharedCode
|
await applyShared(shared)
|
||||||
code.language = lang
|
return
|
||||||
code.value = sharedCode
|
|
||||||
input.value = typeof data.input === "string" ? data.input : ""
|
|
||||||
} catch (err) {}
|
|
||||||
}
|
}
|
||||||
const preset = parsed.query as string
|
const preset = parsed.query as string
|
||||||
if (preset) {
|
if (preset) {
|
||||||
try {
|
try {
|
||||||
const result = await getCodeByQuery(preset)
|
const result = await getCodeByQuery(preset)
|
||||||
code.value = result.data.code
|
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]
|
cache.code[code.language].value = sources[code.language]
|
||||||
output.value = ""
|
output.value = ""
|
||||||
status.value = Status.NotStarted
|
status.value = Status.NotStarted
|
||||||
const url = qs.exclude(location.href, ["query"])
|
const url = qs.exclude(location.href, ["query", "share"])
|
||||||
window.location.href = url
|
window.location.href = url
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,15 +190,16 @@ export async function run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function share() {
|
export function share() {
|
||||||
const data = {
|
const data: Shared = {
|
||||||
lang: code.language,
|
lang: code.language,
|
||||||
code: code.value,
|
code: code.value,
|
||||||
input: input.value,
|
input: input.value,
|
||||||
}
|
}
|
||||||
|
if (code.language === "sql") data.table = selectedTableId.value
|
||||||
const base64 = utoa(JSON.stringify(data))
|
const base64 = utoa(JSON.stringify(data))
|
||||||
copyTextToClipboard(
|
// 基址要去掉 query(预设代码会在 init 里覆盖分享内容)和上一次的 share
|
||||||
qs.stringifyUrl({ url: location.href, query: { share: base64 } }),
|
const url = qs.exclude(location.href, ["query", "share"])
|
||||||
)
|
return copyTextToClipboard(qs.stringifyUrl({ url, query: { share: base64 } }))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function format() {
|
export async function format() {
|
||||||
|
|||||||
15
src/composables/notice.ts
Normal file
15
src/composables/notice.ts
Normal 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
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
import { ref } from "vue"
|
import { ref } from "vue"
|
||||||
import {
|
import { buildSetupSql, defaultSqlTableId, sqlTables } from "../data/sqlTables"
|
||||||
buildSetupSql,
|
|
||||||
defaultSqlTableId,
|
|
||||||
sqlTables,
|
|
||||||
} from "../data/sqlTables"
|
|
||||||
|
|
||||||
export const selectedTableId = ref(defaultSqlTableId)
|
export const selectedTableId = ref(defaultSqlTableId)
|
||||||
|
|
||||||
@@ -11,6 +7,13 @@ export function resetSqlTableSelection() {
|
|||||||
selectedTableId.value = defaultSqlTableId
|
selectedTableId.value = defaultSqlTableId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 分享链接里的表 id 不可信,认不出来就退回默认表
|
||||||
|
export function selectSqlTable(id: unknown) {
|
||||||
|
selectedTableId.value = sqlTables.some((item) => item.id === id)
|
||||||
|
? (id as string)
|
||||||
|
: defaultSqlTableId
|
||||||
|
}
|
||||||
|
|
||||||
// SELECT / WITH 属于查询,直接展示查询结果的列;其余(增删改)回显整张表
|
// SELECT / WITH 属于查询,直接展示查询结果的列;其余(增删改)回显整张表
|
||||||
function isQuery(sql: string): boolean {
|
function isQuery(sql: string): boolean {
|
||||||
return /^\s*(SELECT|WITH)\b/i.test(sql)
|
return /^\s*(SELECT|WITH)\b/i.test(sql)
|
||||||
@@ -18,8 +21,7 @@ function isQuery(sql: string): boolean {
|
|||||||
|
|
||||||
export function buildSqlScript(studentSql: string) {
|
export function buildSqlScript(studentSql: string) {
|
||||||
const table =
|
const table =
|
||||||
sqlTables.find((item) => item.id === selectedTableId.value) ??
|
sqlTables.find((item) => item.id === selectedTableId.value) ?? sqlTables[0]
|
||||||
sqlTables[0]
|
|
||||||
const normalizedSql = studentSql.trim().replace(/;?\s*$/, ";")
|
const normalizedSql = studentSql.trim().replace(/;?\s*$/, ";")
|
||||||
if (isQuery(studentSql.trim())) {
|
if (isQuery(studentSql.trim())) {
|
||||||
return [buildSetupSql(table), ".headers on", normalizedSql].join("\n\n")
|
return [buildSetupSql(table), ".headers on", normalizedSql].join("\n\n")
|
||||||
|
|||||||
@@ -9,8 +9,11 @@ import { code, loading, run, share, size } from "../composables/code"
|
|||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
function handleShare() {
|
function handleShare() {
|
||||||
share()
|
if (share()) {
|
||||||
message.success("分享链接已复制")
|
message.success("分享链接已复制")
|
||||||
|
} else {
|
||||||
|
message.error("复制失败,请检查浏览器剪贴板权限")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,11 @@ function copy() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleShare() {
|
function handleShare() {
|
||||||
share()
|
if (share()) {
|
||||||
message.success("分享链接已复制")
|
message.success("分享链接已复制")
|
||||||
|
} else {
|
||||||
|
message.error("复制失败,请检查浏览器剪贴板权限")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const menu: DropdownOption[] = [
|
const menu: DropdownOption[] = [
|
||||||
|
|||||||
Reference in New Issue
Block a user