423 lines
11 KiB
Vue
423 lines
11 KiB
Vue
<script setup>
|
|
import { parse } from "@vue/compiler-sfc";
|
|
import { watchDebounced } from "@vueuse/core";
|
|
import {
|
|
RsAlert,
|
|
RsBadge,
|
|
RsButton,
|
|
RsCard,
|
|
RsCodeMirror,
|
|
RsCollapse,
|
|
RsCollapseItem,
|
|
RsDropdown,
|
|
RsDropdownItem,
|
|
RsFieldset,
|
|
RsModal,
|
|
RsProgressBar,
|
|
RsTab,
|
|
RsTabItem,
|
|
RsTable,
|
|
RsWizard,
|
|
} from "./index.js";
|
|
|
|
// Import pinia store
|
|
import { useThemeStore } from "~/stores/theme";
|
|
|
|
definePageMeta({
|
|
title: "AI SFC Playground",
|
|
description: "AI SFC Playground page",
|
|
layout: "empty",
|
|
middleware: ["auth"],
|
|
requiresAuth: true,
|
|
});
|
|
|
|
const CODE_STORAGE_KEY = "playground-code";
|
|
|
|
const code = ref(
|
|
localStorage.getItem(CODE_STORAGE_KEY) ||
|
|
`<template>
|
|
<rs-card>
|
|
<template #header>SFC Playground Demo</template>
|
|
<template #body>
|
|
<div class="space-y-4">
|
|
<rs-alert variant="info">{{ msg }}</rs-alert>
|
|
<rs-button @click="count++">Clicked {{ count }} times</rs-button>
|
|
<rs-badge>{{ count > 5 ? 'High' : 'Low' }}</rs-badge>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
</template>
|
|
|
|
<script setup>
|
|
const msg = 'Hello from SFC Playground';
|
|
const count = ref(0);
|
|
<\/script>`
|
|
);
|
|
|
|
const compiledCode = ref(null);
|
|
const componentKey = ref(0);
|
|
const compilationError = ref(null);
|
|
|
|
const previewSizes = [
|
|
{ name: "Mobile", width: "320px", icon: "ph:device-mobile-camera" },
|
|
{ name: "Tablet", width: "768px", icon: "ph:device-tablet-camera" },
|
|
{ name: "Desktop", width: "1024px", icon: "ph:desktop" },
|
|
{ name: "Full", width: "100%", icon: "material-symbols:fullscreen" },
|
|
];
|
|
|
|
const currentPreviewSize = ref(previewSizes[3]); // Default to Full
|
|
|
|
// Theme-related code
|
|
const themeStore = useThemeStore();
|
|
const editorTheme = ref({
|
|
label: themeStore.codeTheme,
|
|
value: themeStore.codeTheme,
|
|
});
|
|
const dropdownThemes = ref([]);
|
|
|
|
// Get all themes
|
|
const themes = codemirrorThemes();
|
|
|
|
// map the themes to the dropdown
|
|
dropdownThemes.value = themes.map((theme) => {
|
|
return {
|
|
label: theme.name,
|
|
value: theme.name,
|
|
};
|
|
});
|
|
|
|
// watch for changes in the theme
|
|
watch(editorTheme, (theme) => {
|
|
themeStore.setCodeTheme(theme.value);
|
|
});
|
|
|
|
const compileCode = async (newCode) => {
|
|
try {
|
|
const { descriptor, errors } = parse(newCode);
|
|
if (errors && errors.length > 0) {
|
|
compilationError.value = {
|
|
message: errors[0].message,
|
|
location: errors[0].loc,
|
|
};
|
|
return;
|
|
}
|
|
|
|
if (descriptor.template && descriptor.scriptSetup) {
|
|
const template = descriptor.template.content;
|
|
const scriptSetup = descriptor.scriptSetup.content;
|
|
|
|
// Dynamically import FormKit components
|
|
const {
|
|
FormKit,
|
|
FormKitSchema,
|
|
FormKitSchemaNode,
|
|
FormKitSchemaCondition,
|
|
FormKitSchemaValidation,
|
|
} = await import("@formkit/vue");
|
|
|
|
const component = defineComponent({
|
|
components: {
|
|
RsAlert,
|
|
RsBadge,
|
|
RsButton,
|
|
RsCard,
|
|
RsCodeMirror,
|
|
RsCollapse,
|
|
RsCollapseItem,
|
|
RsDropdown,
|
|
RsDropdownItem,
|
|
RsFieldset,
|
|
RsModal,
|
|
RsProgressBar,
|
|
RsTab,
|
|
RsTabItem,
|
|
RsTable,
|
|
RsWizard,
|
|
FormKit,
|
|
FormKitSchema,
|
|
FormKitSchemaNode,
|
|
FormKitSchemaCondition,
|
|
FormKitSchemaValidation,
|
|
},
|
|
template,
|
|
setup() {
|
|
const setupContext = reactive({});
|
|
|
|
try {
|
|
// Extract top-level declarations
|
|
const declarations =
|
|
scriptSetup.match(/const\s+(\w+)\s*=\s*([^;]+)/g) || [];
|
|
declarations.forEach((decl) => {
|
|
const [, varName, varValue] = decl.match(
|
|
/const\s+(\w+)\s*=\s*(.+)/
|
|
);
|
|
if (
|
|
varValue.trim().startsWith("'") ||
|
|
varValue.trim().startsWith('"')
|
|
) {
|
|
// It's a string literal, use it directly
|
|
setupContext[varName] = varValue.trim().slice(1, -1);
|
|
} else if (varValue.trim().startsWith("ref(")) {
|
|
// It's already a ref, use ref
|
|
setupContext[varName] = ref(null);
|
|
} else {
|
|
// For other cases, wrap in ref
|
|
setupContext[varName] = ref(null);
|
|
}
|
|
});
|
|
|
|
const setupFunction = new Function(
|
|
"ctx",
|
|
"ref",
|
|
"reactive",
|
|
"computed",
|
|
"watch",
|
|
"onMounted",
|
|
"onUnmounted",
|
|
"useFetch",
|
|
"fetch",
|
|
"useAsyncData",
|
|
"useNuxtApp",
|
|
"useRuntimeConfig",
|
|
"useRoute",
|
|
"useRouter",
|
|
"useState",
|
|
"FormKit",
|
|
"FormKitSchema",
|
|
"FormKitSchemaNode",
|
|
"FormKitSchemaCondition",
|
|
"FormKitSchemaValidation",
|
|
`
|
|
with (ctx) {
|
|
${scriptSetup}
|
|
}
|
|
return ctx;
|
|
`
|
|
);
|
|
|
|
const result = setupFunction(
|
|
setupContext,
|
|
ref,
|
|
reactive,
|
|
computed,
|
|
watch,
|
|
onMounted,
|
|
onUnmounted,
|
|
useFetch,
|
|
fetch,
|
|
useAsyncData,
|
|
useNuxtApp,
|
|
useRuntimeConfig,
|
|
useRoute,
|
|
useRouter,
|
|
useState,
|
|
FormKit,
|
|
FormKitSchema,
|
|
FormKitSchemaNode,
|
|
FormKitSchemaCondition,
|
|
FormKitSchemaValidation
|
|
);
|
|
|
|
// Merge the result back into setupContext
|
|
Object.assign(setupContext, result);
|
|
|
|
return setupContext;
|
|
} catch (error) {
|
|
console.error("Error in setup function:", error);
|
|
compilationError.value = {
|
|
message: `Error in setup function: ${error.message}`,
|
|
location: { start: 0, end: 0 },
|
|
};
|
|
// Return an empty object to prevent breaking the component
|
|
return {};
|
|
}
|
|
},
|
|
});
|
|
|
|
compiledCode.value = markRaw(component);
|
|
componentKey.value++;
|
|
compilationError.value = null;
|
|
} else {
|
|
compiledCode.value = null;
|
|
compilationError.value = {
|
|
message: "Invalid SFC format.",
|
|
location: { start: 0, end: 0 },
|
|
};
|
|
}
|
|
} catch (error) {
|
|
console.error("Compilation error:", error);
|
|
compiledCode.value = null;
|
|
compilationError.value = {
|
|
message: `Compilation error: ${error.message}`,
|
|
location: { start: 0, end: 0 },
|
|
};
|
|
}
|
|
};
|
|
|
|
watchDebounced(
|
|
code,
|
|
async (newCode) => {
|
|
await compileCode(newCode);
|
|
},
|
|
{ debounce: 300, immediate: true }
|
|
);
|
|
|
|
const handleFormatCode = () => {
|
|
// Recompile the code after formatting
|
|
setTimeout(() => compileCode(code.value), 100);
|
|
};
|
|
|
|
onMounted(async () => {
|
|
await compileCode(code.value);
|
|
});
|
|
|
|
const defaultCode = `<template>
|
|
<rs-card>
|
|
<template #header>SFC Playground Demo</template>
|
|
<template #body>
|
|
<div class="space-y-4">
|
|
<rs-alert variant="info">{{ msg }}</rs-alert>
|
|
<rs-button @click="count++">Clicked {{ count }} times</rs-button>
|
|
<rs-badge>{{ count > 5 ? 'High' : 'Low' }}</rs-badge>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
</template>
|
|
|
|
<script setup>
|
|
const msg = 'Hello from SFC Playground';
|
|
const count = ref(0);
|
|
<\/script>`;
|
|
|
|
const resetCode = () => {
|
|
code.value = defaultCode;
|
|
localStorage.setItem(CODE_STORAGE_KEY, defaultCode);
|
|
compileCode(code.value);
|
|
};
|
|
|
|
// Add a watch effect to save code changes to localStorage
|
|
watch(
|
|
code,
|
|
(newCode) => {
|
|
localStorage.setItem(CODE_STORAGE_KEY, newCode);
|
|
},
|
|
{ deep: true }
|
|
);
|
|
</script>
|
|
<template>
|
|
<div class="flex flex-col h-screen bg-gray-900">
|
|
<!-- Header -->
|
|
<header
|
|
class="bg-gray-800 p-2 flex flex-wrap items-center justify-between text-white"
|
|
>
|
|
<div class="flex items-center mb-2 sm:mb-0 gap-4">
|
|
<Icon
|
|
@click="navigateTo('/')"
|
|
name="ph:arrow-circle-left-duotone"
|
|
class="cursor-pointer"
|
|
/>
|
|
<img
|
|
src="@/assets/img/logo/logo-word-white.svg"
|
|
alt="Vue Logo"
|
|
class="h-8 block mr-2"
|
|
/>
|
|
</div>
|
|
<div class="flex flex-wrap items-center space-x-2">
|
|
<rs-button @click="resetCode" class="mr-2">
|
|
<Icon name="material-symbols:refresh" class="mr-2" />
|
|
Reset Code
|
|
</rs-button>
|
|
<h1 class="text-lg font-semibold">Code Playground</h1>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Main content -->
|
|
<div class="flex flex-col sm:flex-row flex-1 overflow-hidden">
|
|
<!-- Editor section -->
|
|
<div
|
|
class="w-full sm:w-1/2 flex flex-col border-b sm:border-b-0 sm:border-r border-gray-900"
|
|
>
|
|
<div class="flex-grow overflow-hidden">
|
|
<rs-code-mirror
|
|
v-model="code"
|
|
mode="javascript"
|
|
class="h-full"
|
|
@format-code="handleFormatCode"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Preview section -->
|
|
<div class="w-full sm:w-1/2 bg-white overflow-auto flex flex-col">
|
|
<div
|
|
class="bg-gray-800 p-2 flex justify-between items-center text-white"
|
|
>
|
|
<h2 class="text-sm font-semibold">Preview</h2>
|
|
<div class="flex space-x-2">
|
|
<rs-button
|
|
v-for="size in previewSizes"
|
|
:key="size.name"
|
|
@click="currentPreviewSize = size"
|
|
:class="{
|
|
'bg-blue-600': currentPreviewSize === size,
|
|
'bg-gray-600': currentPreviewSize !== size,
|
|
}"
|
|
class="px-2 py-1 text-xs rounded"
|
|
>
|
|
<Icon v-if="size.icon" :name="size.icon" class="!w-5 !h-5 mr-2" />
|
|
{{ size.name }}
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
<div class="flex-grow overflow-auto p-4 flex justify-center">
|
|
<div
|
|
:style="{
|
|
width: currentPreviewSize.width,
|
|
height: '100%',
|
|
overflow: 'auto',
|
|
}"
|
|
class="border border-gray-300 transition-all duration-300 ease-in-out"
|
|
>
|
|
<component
|
|
:key="componentKey"
|
|
v-if="compiledCode && !compilationError"
|
|
:is="compiledCode"
|
|
/>
|
|
<div v-else-if="compilationError?.message">
|
|
<div class="flex justify-center items-center p-5">
|
|
<div class="text-center">
|
|
<Icon name="ph:warning" class="text-6xl" />
|
|
<p class="text-lg font-semibold mt-4">
|
|
Something went wrong. Please refer the error in the editor.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else class="text-gray-500">Waiting for code changes...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.device-frame {
|
|
background-color: #f0f0f0;
|
|
border-radius: 16px;
|
|
padding: 16px;
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.device-frame {
|
|
padding: 8px;
|
|
border-radius: 8px;
|
|
}
|
|
}
|
|
|
|
:deep(.cm-editor) {
|
|
height: 100%;
|
|
}
|
|
</style>
|