├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── app.vue ├── assets ├── about │ ├── 001.jpg │ ├── 002.jpg │ ├── 003.jpg │ └── 004.jpg ├── css │ └── tailwind.css ├── images │ └── logo.png ├── styles │ └── global.less └── svg │ ├── 1111.svg │ ├── layout-1-active.svg │ ├── layout-1.svg │ ├── layout-2-active.svg │ ├── layout-2.svg │ ├── layout-3-active.svg │ ├── layout-3.svg │ ├── layout-4-active.svg │ ├── layout-4.svg │ ├── layout-5-active.svg │ ├── layout-5.svg │ └── layout.svg ├── components ├── G-codemirror.vue ├── G-iconButton.vue ├── G-loading.vue ├── G-logo.vue ├── G-message.vue ├── G-selectMenu.vue ├── G-svgIcon.vue └── helper.ts ├── composables ├── requestCore.ts ├── useEditorCodeParams.ts ├── useEditorLayoutStore.ts ├── userStore.ts └── userThemeStore.ts ├── constants └── editor.ts ├── middleware └── auth.global.ts ├── nuxt.config.ts ├── package.json ├── pages ├── components │ ├── EditorLayout.vue │ ├── EditorLayoutSetting.vue │ ├── EditorSettingDialog.vue │ ├── EditorSettingSidebar.vue │ ├── Links.vue │ ├── RunIframe.vue │ └── helper.ts ├── err.vue ├── error.vue └── index.vue ├── plugins ├── svgicon.ts └── vuetify.ts ├── pnpm-lock.yaml ├── public └── favicon.ico ├── server └── tsconfig.json ├── store ├── helper.ts └── useGlobal.ts ├── tailwind.config.ts ├── tsconfig.json └── utils ├── editor └── index.ts ├── export └── index.ts ├── index.ts ├── prettier └── index.ts └── runIframe ├── console.ts ├── index.ts ├── normalize.ts ├── preview.ts └── reset.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | ./node_modules 12 | 13 | # Logs 14 | logs 15 | *.log 16 | 17 | # Misc 18 | .DS_Store 19 | .fleet 20 | .idea 21 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | **/node_modules 3 | .nuxt 4 | assets/icons/* 5 | assets/fonts/* 6 | public/**/* 7 | 8 | # Ignore all HTML files: 9 | **/*.md 10 | **/*.html -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": false, 4 | "bracketSpacing": true, 5 | "singleQuote": false, 6 | "arrowParens": "avoid", 7 | "trailingComma": "none", 8 | "printWidth": 140, 9 | "plugins": [ 10 | "prettier-plugin-tailwindcss" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "camelcase", 4 | "composables", 5 | "estree", 6 | "materialdesignicons", 7 | "nuxt", 8 | "nuxtignore", 9 | "nuxtjs", 10 | "Parens", 11 | "persistedstate", 12 | "pinia", 13 | "tailwindcss", 14 | "uuidv", 15 | "vuetify", 16 | "vueuse" 17 | ], 18 | "editor.formatOnSave": true, 19 | "editor.codeActionsOnSave": { 20 | "source.fixAll": "always", 21 | "source.fixAll.eslint": "always" 22 | }, 23 | "editor.tabSize": 2, 24 | "editor.insertSpaces": true, 25 | "editor.indentSize": "tabSize" 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Online snippet editor 2 | 3 | 一个基于Nuxt3的在线代码片段编辑器,可以使用 html,css,less,scss,js实现前端在线编辑,支持远程引入css,js脚本。 4 | [Online snippet editor 代码仓库 在线体验](https://cool.mmmss.com/) 5 | 6 | ## 页面展示 7 | 8 |  9 | 10 |  11 | 12 |  13 | 14 |  15 | 16 | ## 项目环境 17 | 18 | - node > 18 19 | - pnpm 20 | - nuxt3 21 | 22 | ## 项目启动 23 | 24 | - pnpm install 25 | - pnpm dev 26 | 27 | ### ICON 28 | 29 | ICON组件的svg图标来自 30 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 36 | 37 | -------------------------------------------------------------------------------- /assets/about/001.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CooperJiang/online-snippet-editor/ff0f62fecdf7cde56dd5961113eedfd08ae7e572/assets/about/001.jpg -------------------------------------------------------------------------------- /assets/about/002.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CooperJiang/online-snippet-editor/ff0f62fecdf7cde56dd5961113eedfd08ae7e572/assets/about/002.jpg -------------------------------------------------------------------------------- /assets/about/003.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CooperJiang/online-snippet-editor/ff0f62fecdf7cde56dd5961113eedfd08ae7e572/assets/about/003.jpg -------------------------------------------------------------------------------- /assets/about/004.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CooperJiang/online-snippet-editor/ff0f62fecdf7cde56dd5961113eedfd08ae7e572/assets/about/004.jpg -------------------------------------------------------------------------------- /assets/css/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CooperJiang/online-snippet-editor/ff0f62fecdf7cde56dd5961113eedfd08ae7e572/assets/images/logo.png -------------------------------------------------------------------------------- /assets/styles/global.less: -------------------------------------------------------------------------------- 1 | ::-webkit-scrollbar { 2 | width: 8px; 3 | height: 8px; 4 | } 5 | 6 | ::-webkit-scrollbar-thumb { 7 | background: rgba(144, 147, 153, .6); 8 | border-radius: 20px; 9 | } -------------------------------------------------------------------------------- /assets/svg/1111.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/svg/layout-1-active.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/svg/layout-1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/svg/layout-2-active.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/svg/layout-2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/svg/layout-3-active.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/svg/layout-3.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/svg/layout-4-active.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/svg/layout-4.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/svg/layout-5-active.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/svg/layout-5.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/svg/layout.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/G-codemirror.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 68 | 69 | 70 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /components/G-iconButton.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /components/G-loading.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Loading... 11 | 12 | 13 | 14 | 15 | 16 | 163 | -------------------------------------------------------------------------------- /components/G-logo.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Online Editor 5 | 6 | 7 | 8 | 13 | 14 | 57 | -------------------------------------------------------------------------------- /components/G-message.vue: -------------------------------------------------------------------------------- 1 | 2 | 14 | {{ snackbar.text }} 15 | 16 | 17 | mdi-close 18 | 19 | 20 | 21 | 22 | 23 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /components/G-selectMenu.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 16 | {{ item.name }} 17 | 18 | 19 | 20 | 21 | 22 | 41 | 42 | 47 | -------------------------------------------------------------------------------- /components/G-svgIcon.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 29 | 30 | 38 | -------------------------------------------------------------------------------- /components/helper.ts: -------------------------------------------------------------------------------- 1 | export interface MenuItem { 2 | name: string 3 | key: any 4 | options?: any 5 | } 6 | 7 | export interface SelectMenuProps { 8 | icon?: string 9 | menuList: MenuItem[] 10 | width?: number 11 | offset?: any 12 | location?: string 13 | } 14 | -------------------------------------------------------------------------------- /composables/requestCore.ts: -------------------------------------------------------------------------------- 1 | import { useGlobalStore } from "~/store/useGlobal" 2 | 3 | export const requestCore = (url: string, method = "GET", data: any, opts: any) => { 4 | const config = useRuntimeConfig() 5 | const nuxtApp = useNuxtApp() 6 | 7 | return useFetch(url, { 8 | baseURL: config.public.baseURL, 9 | method, 10 | onRequest({ options }) { 11 | if (method === "GET" || method === "DELETE") { 12 | options.params = data; 13 | } 14 | if (method === "POST" || method === "PUT") { 15 | options.body = data; 16 | } 17 | const token = useUserStore().token 18 | options.headers = options?.headers || {} 19 | options.headers = { 20 | Authorization: token, 21 | ...options?.headers 22 | } 23 | }, 24 | onResponse({ response }) { 25 | if (response.status >= 200 && response.status < 300) { 26 | const code = response._data.code 27 | if (code !== 200) { 28 | if (import.meta.client) { 29 | useGlobalStore().openSnackbar(`${response._data.message} ${response._data.code}`) 30 | } 31 | 32 | // 10000 为基础错误 仅提示即可 例如参数错误 其他错误 无法处理则跳转到error page 33 | if (code !== 10000) { 34 | // TODO 非200 错误场景调整至错误页面 35 | nuxtApp.runWithContext(() => { 36 | navigateTo({ 37 | path: '/err', 38 | query: { 39 | code: response._data.code, 40 | message: response._data.message 41 | } 42 | }) 43 | }) 44 | } 45 | } 46 | } 47 | }, 48 | // statusCode 非2开头则走入error 49 | onResponseError({ response }) { 50 | if (response.status === 401) { 51 | // TODO unLogin 52 | } 53 | nuxtApp.runWithContext(() => { 54 | navigateTo({ 55 | path: '/err', 56 | query: { 57 | code: response._data.code, 58 | message: response._data.message 59 | } 60 | }) 61 | }) 62 | }, 63 | ...opts 64 | }) 65 | } 66 | 67 | 68 | export const GetApi = (url: string, params: any, options?: any) => { 69 | return new Promise((resolve, reject) => { 70 | requestCore(url, "GET", params, { 71 | ...options 72 | }).then((res: any) => { 73 | resolve(res.data.value) 74 | }).catch(err => { 75 | reject(err) 76 | }) 77 | }) 78 | } 79 | 80 | export const PostApi = (url: string, data?: any, options?: any) => { 81 | return new Promise((resolve, reject) => { 82 | requestCore(url, "POST", data, { 83 | ...options 84 | }).then((res: any) => { 85 | resolve(res.data.value) 86 | }).catch(err => { 87 | reject(err) 88 | }) 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /composables/useEditorCodeParams.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { getDefaultSettings, type PreSettings } from '~/pages/components/helper' 3 | 4 | export const useEditorCodeParams = defineStore('editorCodeParams', () => { 5 | 6 | const runIframeOptions = ref({}) 7 | const preSettings = ref(getDefaultSettings()) 8 | 9 | 10 | const setRunIframeOptions = (params: any) => { 11 | runIframeOptions.value = params; 12 | } 13 | 14 | const setPreSettings = (params: PreSettings) => { 15 | preSettings.value = params; 16 | } 17 | 18 | const resetEditorCodeParams = () => { 19 | runIframeOptions.value = {} 20 | preSettings.value = getDefaultSettings() 21 | } 22 | 23 | 24 | return { runIframeOptions, preSettings, setRunIframeOptions, setPreSettings, resetEditorCodeParams } 25 | }, { 26 | persist: { 27 | storage: persistedState.localStorage 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /composables/useEditorLayoutStore.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | export const useEditorLayoutStore = defineStore('editorLayout', () => { 4 | const containerRect = ref<{ width: number; height: number }>({ 5 | width: 0, 6 | height: 0 7 | }) 8 | const layoutMode = ref(2) 9 | const layoutParams = ref({ 10 | layoutMode: 3, 11 | htmlContent: { 12 | width: 0, 13 | height: 0 14 | }, 15 | cssContent: { 16 | width: 0, 17 | height: 0 18 | }, 19 | jsContent: { 20 | width: 0, 21 | height: 0 22 | }, 23 | runContent: { 24 | width: 0, 25 | height: 0 26 | } 27 | }) 28 | 29 | const setLayoutMode = (mode: number) => { 30 | layoutMode.value = mode; 31 | } 32 | 33 | const setContentRect = (rect: { width: number; height: number }) => { 34 | containerRect.value = rect; 35 | } 36 | 37 | const setLayoutParams = (params: any) => { 38 | layoutParams.value = params; 39 | } 40 | 41 | const updateLayoutParams = (updateKey: T, contentKey: K, val: typeof layoutParams.value[T][K]) => { 42 | layoutParams.value[updateKey][contentKey] = val; 43 | } 44 | 45 | return { layoutMode, containerRect, setContentRect, layoutParams, setLayoutMode, setLayoutParams, updateLayoutParams } 46 | }, { 47 | persist: { 48 | storage: persistedState.localStorage 49 | } 50 | }) 51 | -------------------------------------------------------------------------------- /composables/userStore.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | export const useUserStore = defineStore('user', () => { 4 | const token = ref("") 5 | 6 | const setToken = (t: string) => { 7 | token.value = t; 8 | } 9 | 10 | const getToken = () => { 11 | return token.value; 12 | } 13 | 14 | const removeToken = () => { 15 | token.value = ""; 16 | } 17 | 18 | return { token, setToken, getToken, removeToken } 19 | }, { 20 | persist: true 21 | }) 22 | -------------------------------------------------------------------------------- /composables/userThemeStore.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | export const useThemeStore = defineStore('theme', () => { 4 | const theme = ref("dark") 5 | 6 | const setTheme = (t?: string) => { 7 | theme.value = t || 'dark'; 8 | } 9 | 10 | return { theme, setTheme } 11 | }, { 12 | persist: true 13 | }) 14 | -------------------------------------------------------------------------------- /constants/editor.ts: -------------------------------------------------------------------------------- 1 | export const cssTypeMap = { 2 | css: "CSS", 3 | less: "LESS", 4 | sass: "SASS" 5 | } 6 | 7 | export const htmlTypeMap = { 8 | "html": "Html", 9 | "html-tailwindcss": "Html+Tailwindcss", 10 | } 11 | 12 | export const cssTypeIconMap = { 13 | css: "logos:css-3", 14 | less: "logos:less", 15 | sass: "logos:sass" 16 | } 17 | 18 | export const htmlMenuList = [ 19 | { 20 | name: "Html格式化", 21 | key: 1, 22 | options: { language: "html", event: "format" }, 23 | }, 24 | ]; 25 | 26 | export const cssMenuList = [ 27 | { name: "css格式化", key: 1, options: { language: "css", event: "format" } }, 28 | ]; 29 | export const jsMenuList = [ 30 | { name: "js格式化", key: 1, options: { language: "js", event: "format" } }, 31 | ]; 32 | export const cssTypeList = [ 33 | { name: "Css", key: 1, options: { event: "updateCssType", type: "css" } }, 34 | { name: "Less", key: 2, options: { event: "updateCssType", type: "less" } }, 35 | { name: "Sass", key: 3, options: { event: "updateCssType", type: "sass" } }, 36 | ]; 37 | 38 | export const htmlTypeList = [ 39 | { name: "Html", key: 1, options: { event: "updateHtmlType", type: "html" } }, 40 | { 41 | name: "Html+Tailwindcss", 42 | key: 2, 43 | options: { event: "updateHtmlType", type: "html-tailwindcss" }, 44 | }, 45 | ]; 46 | -------------------------------------------------------------------------------- /middleware/auth.global.ts: -------------------------------------------------------------------------------- 1 | import { useGlobalStore } from "~/store/useGlobal" 2 | 3 | const whiteList: string[] = ['/', '/login'] 4 | 5 | export default defineNuxtRouteMiddleware((to, from) => { 6 | // if (!whiteList.includes(to.path)) { 7 | // const token = useUserStore().getToken() 8 | // if (import.meta.client) { 9 | // if (!token) { 10 | // useGlobalStore().openSnackbar({ text: "请先登录!" }) 11 | // return navigateTo('/login') 12 | // } 13 | // } 14 | // } 15 | }) 16 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify' 2 | import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' 3 | import path from 'path' 4 | 5 | 6 | export default defineNuxtConfig({ 7 | devtools: { enabled: false }, 8 | build: { 9 | transpile: ['vuetify', 'pinia-plugin-persistedstate'], 10 | }, 11 | modules: [ 12 | "@vueuse/nuxt", 13 | "nuxt-codemirror", 14 | "@nuxtjs/style-resources", 15 | "@nuxt/icon", 16 | "nuxt-lodash", 17 | (_options, nuxt) => { 18 | nuxt.hooks.hook('vite:extendConfig', (config) => { 19 | if (!config.plugins) return; 20 | config.plugins.push(vuetify({ autoImport: true })) 21 | }) 22 | }, 23 | '@nuxtjs/tailwindcss', 24 | [ 25 | '@pinia/nuxt', 26 | { 27 | autoImports: [ 28 | ['defineStore', 'definePiniaStore'], 29 | ], 30 | }, 31 | ], 32 | '@pinia-plugin-persistedstate/nuxt', 33 | ], 34 | 35 | vite: { 36 | vue: { 37 | template: { 38 | transformAssetUrls, 39 | }, 40 | }, 41 | 42 | server: { 43 | proxy: { 44 | '/api/v1': { 45 | target: 'http://localhost:8888', 46 | changeOrigin: true 47 | } 48 | } 49 | }, 50 | 51 | plugins: [ 52 | createSvgIconsPlugin({ 53 | iconDirs: [path.resolve(process.cwd(), 'assets/svg')] 54 | }) 55 | ] 56 | }, 57 | 58 | runtimeConfig: { 59 | isServer: false, 60 | public: { 61 | baseURL: process.env.NUXT_BASE_URL 62 | }, 63 | }, 64 | 65 | tailwindcss: { 66 | configPath: '~/tailwind.config.ts' 67 | }, 68 | 69 | 70 | styleResources: { 71 | less: [ 72 | './assets/styles/global.less' 73 | ] 74 | }, 75 | 76 | css: [ 77 | './assets/styles/global.less', 78 | ] 79 | 80 | }) 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "online-snippet-editor", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build": "nuxt build --dotenv .env.production", 7 | "start": "pm2 start ecosystem.config.cjs", 8 | "start:node": "node ./.output/server/index.mjs", 9 | "dev": "nuxt dev --dotenv .env.development", 10 | "dev:pro": "nuxt dev --dotenv .env.production", 11 | "generate": "nuxt generate", 12 | "preview": "nuxt preview", 13 | "postinstall": "nuxt prepare" 14 | }, 15 | "dependencies": { 16 | "@codemirror/lang-css": "^6.2.1", 17 | "@codemirror/lang-html": "^6.4.9", 18 | "@codemirror/lang-javascript": "^6.2.2", 19 | "@codemirror/state": "^6.4.1", 20 | "@codemirror/theme-one-dark": "^6.1.2", 21 | "@codemirror/view": "^6.30.0", 22 | "@iconify-json/logos": "^1.1.44", 23 | "@mdi/font": "^7.4.47", 24 | "@nuxt/icon": "^1.4.5", 25 | "@nuxtjs/eslint-config-typescript": "^12.1.0", 26 | "@nuxtjs/style-resources": "2.0.0", 27 | "@pinia/nuxt": "^0.5.2", 28 | "@types/less": "^3.0.6", 29 | "@types/uuid": "^10.0.0", 30 | "@vueuse/core": "^10.11.0", 31 | "@vueuse/nuxt": "^10.11.0", 32 | "codemirror": "^6.0.1", 33 | "eslint-config-prettier": "^9.1.0", 34 | "eslint-plugin-prettier": "^5.2.1", 35 | "eslint-plugin-vue": "^9.27.0", 36 | "less": "^4.2.0", 37 | "less-loader": "^12.2.0", 38 | "lodash": "^4.17.21", 39 | "nuxt": "^3.12.4", 40 | "nuxt-codemirror": "^0.0.11", 41 | "nuxt-icon": "1.0.0-beta.7", 42 | "nuxt-lodash": "^2.5.3", 43 | "postcss": "^8.4.41", 44 | "postcss-discard-comments": "^7.0.1", 45 | "postcss-discard-duplicates": "^7.0.0", 46 | "postcss-discard-empty": "^7.0.0", 47 | "prettier": "^3.3.3", 48 | "prettier-plugin-tailwindcss": "^0.6.5", 49 | "uuid": "^10.0.0", 50 | "vite-plugin-svg-icons": "^2.0.1", 51 | "vue": "latest" 52 | }, 53 | "devDependencies": { 54 | "@nuxtjs/tailwindcss": "^6.12.1", 55 | "@pinia-plugin-persistedstate/nuxt": "^1.2.1", 56 | "eslint": "^9.8.0", 57 | "sass-embedded": "^1.77.8", 58 | "vite-plugin-vuetify": "^2.0.3", 59 | "vuetify": "^3.6.13" 60 | } 61 | } -------------------------------------------------------------------------------- /pages/components/EditorLayout.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 482 | 483 | 544 | -------------------------------------------------------------------------------- /pages/components/EditorLayoutSetting.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 16 | 17 | 18 | 19 | 20 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /pages/components/EditorSettingDialog.vue: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 编辑器设置 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 26 | 27 | head标签内容 28 | 33 | html class设置 34 | 39 | body class设置 40 | 45 | 46 | 47 | 48 | 基础样式 49 | 50 | 51 | 52 | 53 | 54 | Css文件类型 55 | 56 | 57 | 58 | 59 | 60 | 添加依赖资源 61 | 65 | 66 | 67 | 68 | 添加依赖资源 69 | 73 | 74 | 75 | 76 | 项目名称 77 | 82 | 项目描述 83 | 88 | 89 | 90 | 91 | 自动保存 92 | 96 | 97 | 自动运行 98 | 102 | 103 | 自动格式化 104 | 108 | 109 | 110 | 111 | 代码缩进 112 | 116 | 117 | 118 | 119 | 120 | 代码缩进尺寸 121 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 170 | 171 | 176 | -------------------------------------------------------------------------------- /pages/components/EditorSettingSidebar.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | {{ item.name }} 11 | 12 | 13 | 14 | 15 | 43 | -------------------------------------------------------------------------------- /pages/components/Links.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 添加新资源 8 | 9 | 10 | 11 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /pages/components/RunIframe.vue: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 91 | -------------------------------------------------------------------------------- /pages/components/helper.ts: -------------------------------------------------------------------------------- 1 | export type LayoutMode = "html" | "style" | "script" | "project" | "save" | "editor" 2 | 3 | export type StyleType = "css" | "less" | "sass" 4 | 5 | export type HtmlType = "html" | "html-tailwindcss" 6 | export interface PreSettings { 7 | htmlSettings: { 8 | headSettings: string 9 | htmlClass: string 10 | bodyClass: string 11 | htmlType: HtmlType 12 | } 13 | styleSettings: { 14 | defaultCss: number 15 | links: string[], 16 | styleType: StyleType 17 | } 18 | scriptSettings: { 19 | scripts: string[] 20 | } 21 | projectInfo: { 22 | name: string 23 | description: string 24 | } 25 | saveSettings: { 26 | autoSave: boolean 27 | autoRun: boolean 28 | autoFormat: boolean 29 | } 30 | editorSetting: { 31 | layout: number 32 | indentSize: number 33 | indentType: string 34 | } 35 | } 36 | 37 | export interface EditorSettingProps { 38 | modelValue: boolean 39 | preSettings: PreSettings 40 | } 41 | 42 | export interface EditorSettingEmits { 43 | (e: "update:modelValue", val: boolean): void 44 | (e: "update:settings", val: PreSettings): void 45 | } 46 | 47 | export interface EditorSettingSidebarProps { 48 | modelValue: string 49 | } 50 | 51 | export interface EditorSettingSidebarEmits { 52 | (e: "update:modelValue", val: string): void 53 | 54 | } 55 | 56 | export function getDefaultSettings(): PreSettings { 57 | return { 58 | htmlSettings: { 59 | headSettings: "", 60 | htmlClass: "", 61 | bodyClass: "", 62 | htmlType: "html" 63 | }, 64 | styleSettings: { 65 | defaultCss: 0, 66 | links: [''], 67 | styleType: "css" 68 | }, 69 | scriptSettings: { 70 | scripts: [''] 71 | }, 72 | projectInfo: { 73 | name: "", 74 | description: "" 75 | }, 76 | saveSettings: { 77 | autoSave: true, 78 | autoRun: true, 79 | autoFormat: true 80 | }, 81 | editorSetting: { 82 | layout: 1, 83 | indentSize: 2, 84 | indentType: "tab" 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /pages/err.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /pages/error.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | nuxt 内置错误 4 | {{ JSON.stringify(error) }} 5 | 6 | 7 | 8 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 运行 9 | 清空编辑器 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | + 20 | 21 | 22 | {{ htmlLabel }} 23 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {{ cssLabel }} 43 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | JavaScript 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 217 | 218 | 219 | -------------------------------------------------------------------------------- /plugins/svgicon.ts: -------------------------------------------------------------------------------- 1 | import 'virtual:svg-icons-register' 2 | import svgIcon from '~/components/G-svgIcon.vue' 3 | export default defineNuxtPlugin(nuxtApp => { 4 | nuxtApp.vueApp.component('svg-icon', svgIcon) 5 | }) 6 | -------------------------------------------------------------------------------- /plugins/vuetify.ts: -------------------------------------------------------------------------------- 1 | import '@mdi/font/css/materialdesignicons.css' 2 | import 'vuetify/styles' 3 | import { createVuetify } from 'vuetify' 4 | import * as components from "vuetify/components"; 5 | import * as directives from "vuetify/directives"; 6 | 7 | export default defineNuxtPlugin((app) => { 8 | const vuetify = createVuetify({ 9 | components, 10 | directives, 11 | ssr: true, 12 | }) 13 | app.vueApp.use(vuetify) 14 | }) 15 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CooperJiang/online-snippet-editor/ff0f62fecdf7cde56dd5961113eedfd08ae7e572/public/favicon.ico -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /store/helper.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | 3 | export type MessageTipsLocation = 'top' | "start top" | "end top" | "center" | "start center" | "end center" | "bottom" | "start bottom" | "end bottom" 4 | 5 | export type MessageTips = { 6 | key: string 7 | text: string 8 | color: string 9 | visible: boolean 10 | showCloseIcon: boolean 11 | timeout: number 12 | location: MessageTipsLocation 13 | } 14 | 15 | export const getDefaultMessageConfig = (): MessageTips => { 16 | return { 17 | key: uuidv4(), 18 | text: '', 19 | color: '', 20 | visible: true, 21 | showCloseIcon: true, 22 | timeout: 2000, 23 | location: 'top' 24 | } 25 | } 26 | 27 | export type GlobalStore = { 28 | snackbarQueue: MessageTips[] 29 | } 30 | -------------------------------------------------------------------------------- /store/useGlobal.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultMessageConfig, type GlobalStore } from "./helper" 2 | 3 | export const useGlobalStore = defineStore('global', { 4 | state: (): GlobalStore => ({ 5 | snackbarQueue: [] 6 | }), 7 | 8 | getters: { 9 | messageColor: (state: GlobalStore) => { 10 | return "" 11 | } 12 | }, 13 | 14 | actions: { 15 | openSnackbar(options: any) { 16 | const optionsType = typeof options 17 | if (optionsType === 'string') options = { text: options } 18 | const config = Object.assign(getDefaultMessageConfig(), options) 19 | this.snackbarQueue.push(config) 20 | }, 21 | 22 | closeSnackbar(key: string) { 23 | this.snackbarQueue = this.snackbarQueue.filter((item) => item.key !== key) 24 | } 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | content: [ 4 | 'components/**/*.vue', 5 | 'layouts/**/*.vue', 6 | 'pages/**/*.vue', 7 | 'plugins/**/*.js', 8 | 'nuxt.config.js' 9 | ], 10 | theme: { 11 | extend: {}, 12 | }, 13 | variants: { 14 | extend: {}, 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json", 4 | "compilerOptions": { 5 | "types": ["vite-plugin-svg-icons/client"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /utils/editor/index.ts: -------------------------------------------------------------------------------- 1 | import less from 'less'; 2 | import postcss from 'postcss'; 3 | import discardComments from 'postcss-discard-comments'; 4 | import discardEmpty from 'postcss-discard-empty'; 5 | import discardDuplicates from 'postcss-discard-duplicates'; 6 | 7 | 8 | export function convertLessToCss(lessCode: string): Promise { 9 | if (typeof lessCode !== 'string') { 10 | console.error('The LESS code is not a string:', lessCode); 11 | return Promise.reject(new TypeError('The LESS code must be a string')); 12 | } 13 | return new Promise((resolve, reject) => { 14 | less.render(lessCode, (err: any, output: any) => { 15 | if (err) { 16 | reject(err); 17 | } else { 18 | postcss([discardComments, discardEmpty, discardDuplicates]) 19 | .process(output.css, { from: undefined }) 20 | .then(result => { 21 | resolve(result.css); 22 | }) 23 | .catch(postcssErr => { 24 | console.log('postcssErr: ', postcssErr); 25 | reject(postcssErr); 26 | }); 27 | } 28 | }); 29 | }); 30 | } 31 | 32 | 33 | export async function convertSassToCss(scssCode: string): Promise { 34 | if (typeof scssCode !== 'string') { 35 | console.error('The SCSS code is not a string:', scssCode); 36 | return Promise.reject(new TypeError('The SCSS code must be a string')); 37 | } 38 | return new Promise(async (resolve, reject) => { 39 | if (!window?.Sass) return 40 | window.Sass.compile(scssCode, (result: any) => { 41 | if (result.status !== 0) { 42 | reject(new Error(result.formatted)); 43 | } else { 44 | postcss([discardComments, discardEmpty, discardDuplicates]) 45 | .process(result.text, { from: undefined }) 46 | .then(postcssResult => { 47 | resolve(postcssResult.css); 48 | }) 49 | .catch(postcssErr => { 50 | console.error('postcssErr: ', postcssErr); 51 | reject(postcssErr); 52 | }); 53 | } 54 | }); 55 | }); 56 | } 57 | 58 | 59 | declare const window: { 60 | Sass?: any; 61 | }; 62 | 63 | export const parseStrFromArr = (str: any) => { 64 | try { 65 | return str.split(','); 66 | } catch (error) { 67 | return [] 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /utils/export/index.ts: -------------------------------------------------------------------------------- 1 | export function exportHtml(filename: string, content: string) { 2 | const blob = new Blob([content], { type: 'text/html' }); 3 | const link = document.createElement('a'); 4 | link.href = URL.createObjectURL(blob); 5 | link.download = filename; 6 | document.body.appendChild(link); 7 | link.click(); 8 | document.body.removeChild(link); 9 | } 10 | -------------------------------------------------------------------------------- /utils/index.ts: -------------------------------------------------------------------------------- 1 | import { parseStrFromArr } from "./editor" 2 | 3 | export function transformEditorParamsFromDb(snippetCode: any) { 4 | const { 5 | htmlCode, 6 | cssCode, 7 | jsCode, 8 | headSettings, 9 | htmlClass, 10 | bodyClass, 11 | htmlType, 12 | defaultCss, 13 | links, 14 | styleType, 15 | scripts, 16 | projectName, 17 | projectDescription 18 | } = snippetCode 19 | const preSettings = { 20 | htmlSettings: { 21 | headSettings, 22 | htmlClass, 23 | bodyClass, 24 | htmlType 25 | }, 26 | styleSettings: { 27 | defaultCss, 28 | links: parseStrFromArr(links), 29 | styleType 30 | }, 31 | scriptSettings: { 32 | scripts: parseStrFromArr(scripts) 33 | }, 34 | projectInfo: { 35 | name: projectName, 36 | description: projectDescription 37 | }, 38 | saveSettings: { 39 | autoSave: true, 40 | autoRun: true, 41 | autoFormat: true 42 | }, 43 | editorSetting: { 44 | layout: 1, 45 | indentSize: 2, 46 | indentType: "tab" 47 | } 48 | } 49 | const runIframeOptions = { 50 | html: htmlCode, 51 | css: cssCode, 52 | js: jsCode, 53 | } 54 | 55 | return { 56 | preSettings, 57 | runIframeOptions 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /utils/prettier/index.ts: -------------------------------------------------------------------------------- 1 | import prettier from 'prettier/standalone'; 2 | import * as htmlParser from 'prettier/plugins/html.js'; 3 | import * as babelParser from 'prettier/plugins/babel.js'; // 用于解析 JavaScript 4 | import * as cssParser from 'prettier/plugins/postcss.js'; // 用于解析 CSS 5 | import * as esTreeParser from 'prettier/plugins/estree.js'; // 用于解析 TypeScript 6 | import * as postcssParser from 'prettier/plugins/postcss.js'; // 用于解析 CSS 7 | 8 | // https://prettier.io/docs/en/options 9 | 10 | export async function formatCode(code: string, language: string) { 11 | let parser = ''; 12 | switch (language) { 13 | case 'html': 14 | parser = 'html'; 15 | break; 16 | case 'js': 17 | case 'javascript': 18 | parser = 'babel'; 19 | break; 20 | case 'css': 21 | case 'vue': 22 | parser = 'css'; 23 | break; 24 | default: 25 | throw new Error('Unsupported language'); 26 | } 27 | 28 | const data = await prettier.format(code, { 29 | parser: parser, 30 | plugins: [htmlParser, babelParser, esTreeParser, cssParser, postcssParser], 31 | tabWidth: 2, 32 | htmlWhitespaceSensitivity: "ignore" 33 | }); 34 | return data; 35 | } 36 | -------------------------------------------------------------------------------- /utils/runIframe/console.ts: -------------------------------------------------------------------------------- 1 | export const proxyConsoleScript = ` 2 | 57 | `; 58 | 59 | -------------------------------------------------------------------------------- /utils/runIframe/index.ts: -------------------------------------------------------------------------------- 1 | import type { PreSettings } from "~/pages/components/helper"; 2 | import { convertLessToCss, convertSassToCss } from "~/utils/editor/index" 3 | import { resetCss } from "./reset" 4 | import { normalizeCss } from "./normalize" 5 | import { compilerPreviewCss } from './preview' 6 | import { proxyConsoleScript } from './console' 7 | 8 | export interface RunIframeParams { 9 | html?: string; 10 | css?: string; 11 | js?: string; 12 | } 13 | 14 | export interface PreviewSettings { 15 | preview: boolean 16 | previewScale: number | undefined 17 | } 18 | 19 | let cacheSuccessfulCss = ""; // 如果本次编译有问题就使用上次的css等待本次css编写完成 20 | 21 | export async function compilerIframeCode(options: RunIframeParams, preSettings: PreSettings, previewOpts: PreviewSettings) { 22 | try { 23 | const { html = '', css = '', js = "" } = options; 24 | const { 25 | htmlSettings: { headSettings = '', htmlClass = '', bodyClass = '', htmlType = 'html' }, 26 | styleSettings: { links: preLinks = [], styleType = "css", defaultCss = 0 }, 27 | scriptSettings: { scripts: preScripts = [] }, 28 | } = preSettings; 29 | 30 | let preCss = ``; 31 | let preViewCss = ``; 32 | 33 | if (defaultCss === 1) { 34 | preCss = normalizeCss 35 | } 36 | if (defaultCss === 2) { 37 | preCss = resetCss 38 | } 39 | 40 | if (previewOpts.preview) { 41 | preViewCss = compilerPreviewCss(previewOpts) 42 | } 43 | 44 | // 过滤掉空字符串的链接和脚本 45 | const filteredScripts = preScripts.filter(src => src.trim() !== ''); 46 | const filteredLinks = preLinks.filter(href => href.trim() !== ''); 47 | 48 | /* 对于tailwindcss 类型的 注入脚本 */ 49 | if (htmlType === 'html-tailwindcss') { 50 | filteredScripts.push('https://cdn.tailwindcss.com') 51 | } 52 | 53 | /* 对于特殊类型的css进行一次解析 普通css也可以通过less解析 */ 54 | let formatCss = css 55 | if (['less', 'css'].includes(styleType)) { 56 | try { 57 | formatCss = await convertLessToCss(css) 58 | cacheSuccessfulCss = formatCss 59 | } catch (error) { 60 | formatCss = cacheSuccessfulCss 61 | } 62 | } 63 | 64 | if (['sass'].includes(styleType)) { 65 | try { 66 | formatCss = await convertSassToCss(css) 67 | cacheSuccessfulCss = formatCss 68 | } catch (error) { 69 | formatCss = cacheSuccessfulCss 70 | } 71 | } 72 | 73 | // 将scripts数组转换成多个script标签,移除async属性 74 | const scriptsTags = filteredScripts 75 | .map(src => ``) 76 | .join('\n'); 77 | 78 | // 将links数组转换成多个link标签 79 | const linksTags = filteredLinks 80 | .map(href => ``) 81 | .join('\n'); 82 | 83 | /* 将import type=module语句单独提取放入顶层 */ 84 | const importStatements: any[] = []; 85 | const otherJsStatements = js.split('\n').filter(line => { 86 | if (line.startsWith('import ')) { 87 | importStatements.push(line); 88 | return false; 89 | } 90 | return true; 91 | }).join('\n'); 92 | 93 | 94 | const code = ` 95 | 96 | 97 | 98 | ${headSettings} 99 | ${`${defaultCss !== 0 ? preCss : ''}`} 100 | ${`${previewOpts.preview ? preViewCss : ''}`} 101 | 102 | ${linksTags} 103 | ${scriptsTags} 104 | ${proxyConsoleScript} 105 | 114 | 115 | 116 | ${html} 117 | 120 | 127 | 128 | 129 | `; 130 | return code; 131 | } catch (error) { 132 | console.log('compilerIframeCode error: ', error); 133 | return "" 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /utils/runIframe/normalize.ts: -------------------------------------------------------------------------------- 1 | export const normalizeCss = ` 2 | 150 | ` 151 | -------------------------------------------------------------------------------- /utils/runIframe/preview.ts: -------------------------------------------------------------------------------- 1 | import type { PreviewSettings } from "."; 2 | 3 | export function compilerPreviewCss(previewOpts: PreviewSettings) { 4 | let scale = Number(previewOpts?.previewScale) / 100 5 | scale < 0.1 && (scale = 0.1) 6 | return ` 7 | 23 | ` 24 | } 25 | -------------------------------------------------------------------------------- /utils/runIframe/reset.ts: -------------------------------------------------------------------------------- 1 | export const resetCss = ` 2 | 47 | ` 48 | --------------------------------------------------------------------------------