add creation
自由创作:新增 /creation 列表与详情页、Creation API 与类型定义, TaskPanel 增加入口,PromptPanel 支持 allowSubmit 控制提交按钮。 顺带升级依赖并按 prettier 重新格式化。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1
.github/workflows/deploy.yaml
vendored
1
.github/workflows/deploy.yaml
vendored
@@ -12,6 +12,7 @@ jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: debian
|
||||
|
||||
1340
package-lock.json
generated
1340
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@@ -11,32 +11,32 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
"@codemirror/lang-html": "^6.4.12",
|
||||
"@codemirror/lang-javascript": "^6.2.5",
|
||||
"@vueuse/core": "^14.3.0",
|
||||
"axios": "^1.16.0",
|
||||
"@vueuse/core": "^14.4.0",
|
||||
"axios": "^1.19.0",
|
||||
"codemirror": "^6.0.2",
|
||||
"copy-text-to-clipboard": "^3.2.2",
|
||||
"github-markdown-css": "^5.9.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"marked": "^18.0.0",
|
||||
"highlight.js": "^11.12.0",
|
||||
"marked": "^18.0.9",
|
||||
"marked-alert": "^2.1.2",
|
||||
"marked-code-preview": "^1.3.7",
|
||||
"marked-highlight": "^2.2.4",
|
||||
"md-editor-v3": "^6.5.0",
|
||||
"md-editor-v3": "^6.5.6",
|
||||
"naive-ui": "^2.44.1",
|
||||
"vue": "^3.5.33",
|
||||
"vue": "^3.5.41",
|
||||
"vue-codemirror": "^6.1.1",
|
||||
"vue-router": "^5.0.6"
|
||||
"vue-router": "^5.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/vue": "^5.0.0",
|
||||
"@rsbuild/core": "^2.0.3",
|
||||
"@rsbuild/plugin-vue": "^1.2.7",
|
||||
"@iconify/vue": "^5.0.1",
|
||||
"@rsbuild/core": "^2.1.13",
|
||||
"@rsbuild/plugin-vue": "^2.0.1",
|
||||
"@vue/tsconfig": "^0.9.1",
|
||||
"core-js": "^3.49.0",
|
||||
"prettier": "^3.8.3",
|
||||
"typescript": "^6.0.3",
|
||||
"unplugin-vue-components": "^32.0.0"
|
||||
"core-js": "^3.50.0",
|
||||
"prettier": "^3.9.6",
|
||||
"typescript": "^7.0.2",
|
||||
"unplugin-vue-components": "^32.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
38
src/api.ts
38
src/api.ts
@@ -21,6 +21,9 @@ import type {
|
||||
ShowcaseDetail,
|
||||
PromptRound,
|
||||
RandomRatingItem,
|
||||
CreationSlim,
|
||||
CreationDetail,
|
||||
CreationIn,
|
||||
} from "./utils/type"
|
||||
import { BASE_URL, STORAGE_KEY } from "./utils/const"
|
||||
|
||||
@@ -164,6 +167,41 @@ export const Challenge = {
|
||||
},
|
||||
}
|
||||
|
||||
export const Creation = {
|
||||
async listMine(): Promise<CreationSlim[]> {
|
||||
const res = await http.get("/creation/mine")
|
||||
return res.data
|
||||
},
|
||||
|
||||
async create(payload: CreationIn): Promise<CreationDetail> {
|
||||
const res = await http.post("/creation/", payload)
|
||||
return res.data
|
||||
},
|
||||
|
||||
async get(display: number): Promise<CreationDetail> {
|
||||
const res = await http.get(`/creation/${display}`)
|
||||
return res.data
|
||||
},
|
||||
|
||||
async update(display: number, payload: CreationIn): Promise<CreationDetail> {
|
||||
const res = await http.put(`/creation/${display}`, payload)
|
||||
return res.data
|
||||
},
|
||||
|
||||
async saveCode(
|
||||
display: number,
|
||||
code: { html: string; css: string; js: string },
|
||||
): Promise<CreationDetail> {
|
||||
const res = await http.put(`/creation/${display}/code`, code)
|
||||
return res.data
|
||||
},
|
||||
|
||||
async remove(display: number) {
|
||||
const res = await http.delete(`/creation/${display}`)
|
||||
return res.data
|
||||
},
|
||||
}
|
||||
|
||||
export const Submission = {
|
||||
async create(
|
||||
taskId: number,
|
||||
|
||||
@@ -20,14 +20,23 @@
|
||||
<!-- User message -->
|
||||
<div class="message user">
|
||||
<div class="message-role">我</div>
|
||||
<div class="message-content" v-html="renderContent(pair.userMsg)"></div>
|
||||
<div
|
||||
class="message-content"
|
||||
v-html="renderContent(pair.userMsg)"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- Assistant message -->
|
||||
<div v-if="pair.assistantMsg" class="message assistant">
|
||||
<div class="message-role">AI</div>
|
||||
<div class="message-content" v-html="renderContent(pair.assistantMsg)"></div>
|
||||
<div v-if="hasCode(pair.assistantMsg)" class="message-actions">
|
||||
<div
|
||||
class="message-content"
|
||||
v-html="renderContent(pair.assistantMsg)"
|
||||
></div>
|
||||
<div
|
||||
v-if="allowSubmit && hasCode(pair.assistantMsg)"
|
||||
class="message-actions"
|
||||
>
|
||||
<n-button
|
||||
size="tiny"
|
||||
:disabled="pair.assistantMsg.submitted"
|
||||
@@ -128,6 +137,8 @@ import {
|
||||
import { Prompt, Submission } from "../../api"
|
||||
import { renderMarkdown } from "../../utils/markdown"
|
||||
|
||||
const { allowSubmit = true } = defineProps<{ allowSubmit?: boolean }>()
|
||||
|
||||
const emit = defineEmits<{ deleted: []; submitted: [] }>()
|
||||
|
||||
const input = ref("")
|
||||
@@ -160,7 +171,8 @@ const pairs = computed(() => {
|
||||
let i = 0
|
||||
while (i < msgs.length) {
|
||||
if (msgs[i].role === "user") {
|
||||
const assistantMsg = msgs[i + 1]?.role === "assistant" ? msgs[i + 1] : null
|
||||
const assistantMsg =
|
||||
msgs[i + 1]?.role === "assistant" ? msgs[i + 1] : null
|
||||
result.push({ userMsg: msgs[i], assistantMsg, index: i })
|
||||
i += assistantMsg ? 2 : 1
|
||||
} else {
|
||||
@@ -294,7 +306,6 @@ watch([() => messages.value.length, streamingContent], () => {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
@@ -374,7 +385,9 @@ watch([() => messages.value.length, streamingContent], () => {
|
||||
cursor: pointer;
|
||||
color: #bbb;
|
||||
line-height: 1;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
transition:
|
||||
color 0.15s,
|
||||
border-color 0.15s;
|
||||
}
|
||||
|
||||
.pair-delete-btn:hover {
|
||||
|
||||
@@ -57,6 +57,14 @@
|
||||
</template>
|
||||
提交记录
|
||||
</n-tooltip>
|
||||
<n-tooltip v-if="authed" trigger="hover">
|
||||
<template #trigger>
|
||||
<n-button text @click="$router.push({ name: 'creation-list' })">
|
||||
<Icon :width="16" icon="lucide:sparkles"></Icon>
|
||||
</n-button>
|
||||
</template>
|
||||
自由创作
|
||||
</n-tooltip>
|
||||
<n-tooltip v-if="authed" trigger="hover">
|
||||
<template #trigger>
|
||||
<n-button text @click="$router.push({ name: 'showcase' })">
|
||||
|
||||
274
src/pages/CreationDetail.vue
Normal file
274
src/pages/CreationDetail.vue
Normal file
@@ -0,0 +1,274 @@
|
||||
<template>
|
||||
<div class="creation-layout">
|
||||
<div class="creation-sider">
|
||||
<n-tabs v-model:value="activeTab" type="line" class="left-tabs">
|
||||
<template #prefix>
|
||||
<n-button text @click="back" style="margin: 0 8px">
|
||||
<Icon :width="20" icon="pepicons-pencil:arrow-left" />
|
||||
</n-button>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<n-flex style="margin: 0 8px">
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<n-button text :loading="savingCode" @click="saveCode(true)">
|
||||
<Icon :width="16" icon="lucide:save" />
|
||||
</n-button>
|
||||
</template>
|
||||
保存当前作品
|
||||
</n-tooltip>
|
||||
</n-flex>
|
||||
</template>
|
||||
<n-tab-pane name="topic" tab="我的主题" display-directive="show">
|
||||
<div class="topic-pane">
|
||||
<n-form-item label="主题名称" :show-feedback="false">
|
||||
<n-input
|
||||
v-model:value="title"
|
||||
placeholder="给你的主题起个名字"
|
||||
maxlength="100"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item
|
||||
label="主题描述"
|
||||
:show-feedback="false"
|
||||
style="margin-top: 12px"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="content"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 8 }"
|
||||
placeholder="想做一个什么样的页面?有哪些颜色、布局、交互?"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-flex style="margin-top: 16px">
|
||||
<n-button
|
||||
:loading="savingTopic"
|
||||
:disabled="!title.trim()"
|
||||
@click="saveTopic"
|
||||
>
|
||||
保存主题
|
||||
</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
:disabled="!content.trim() || streaming"
|
||||
@click="generate"
|
||||
>
|
||||
用这个主题生成
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-text depth="3" class="topic-hint">
|
||||
自由创作只属于你自己,不会进入提交记录和评分。
|
||||
</n-text>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="chat" tab="AI 对话" display-directive="show">
|
||||
<PromptPanel :allow-submit="false" @deleted="historyRefreshKey++" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="history" tab="历史对话" display-directive="show">
|
||||
<PromptHistoryPanel
|
||||
:task-id="taskId"
|
||||
:active="activeTab === 'history'"
|
||||
:refresh-key="historyRefreshKey"
|
||||
@select="previewHistoryItem"
|
||||
@deleted="removeMessagePair"
|
||||
/>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</div>
|
||||
<div class="creation-content">
|
||||
<Preview
|
||||
:html="html"
|
||||
:css="css"
|
||||
:js="js"
|
||||
show-code-button
|
||||
clearable
|
||||
@showCode="showCode = true"
|
||||
@clear="clearAll"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<n-modal
|
||||
v-model:show="showCode"
|
||||
preset="card"
|
||||
title="代码"
|
||||
style="width: 700px"
|
||||
>
|
||||
<n-tabs type="line">
|
||||
<n-tab-pane name="html" tab="HTML">
|
||||
<n-code :code="html" language="html" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="css" tab="CSS">
|
||||
<n-code :code="css" language="css" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="js" tab="JS">
|
||||
<n-code :code="js" language="javascript" />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref, watch } from "vue"
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
import { useMessage } from "naive-ui"
|
||||
import { Icon } from "@iconify/vue"
|
||||
import PromptPanel from "../components/ai/PromptPanel.vue"
|
||||
import PromptHistoryPanel from "../components/ai/PromptHistoryPanel.vue"
|
||||
import Preview from "../components/editor/Preview.vue"
|
||||
import { Creation } from "../api"
|
||||
import { html, css, js } from "../store/editors"
|
||||
import {
|
||||
connectPrompt,
|
||||
disconnectPrompt,
|
||||
removeMessagePair,
|
||||
sendPrompt,
|
||||
setOnCodeComplete,
|
||||
streaming,
|
||||
} from "../store/prompt"
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
|
||||
const display = Number(route.params.display)
|
||||
const activeTab = ref("topic")
|
||||
const taskId = ref(0)
|
||||
const title = ref("")
|
||||
const content = ref("")
|
||||
const showCode = ref(false)
|
||||
const savingTopic = ref(false)
|
||||
const savingCode = ref(false)
|
||||
const historyRefreshKey = ref(0)
|
||||
|
||||
watch(streaming, (val) => {
|
||||
if (val) activeTab.value = "chat"
|
||||
})
|
||||
|
||||
async function load() {
|
||||
const data = await Creation.get(display)
|
||||
taskId.value = data.task_id
|
||||
title.value = data.title
|
||||
content.value = data.content
|
||||
html.value = data.html
|
||||
css.value = data.css
|
||||
js.value = data.js
|
||||
connectPrompt(data.task_id)
|
||||
setOnCodeComplete(() => saveCode())
|
||||
}
|
||||
|
||||
async function saveTopic() {
|
||||
if (!title.value.trim()) return
|
||||
savingTopic.value = true
|
||||
try {
|
||||
await Creation.update(display, {
|
||||
title: title.value.trim(),
|
||||
content: content.value,
|
||||
})
|
||||
message.success("主题已保存")
|
||||
} catch {
|
||||
message.error("保存失败,请重试")
|
||||
} finally {
|
||||
savingTopic.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCode(notify = false) {
|
||||
savingCode.value = true
|
||||
try {
|
||||
await Creation.saveCode(display, {
|
||||
html: html.value,
|
||||
css: css.value,
|
||||
js: js.value,
|
||||
})
|
||||
if (notify) message.success("作品已保存")
|
||||
} catch {
|
||||
if (notify) message.error("保存失败,请重试")
|
||||
} finally {
|
||||
savingCode.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function generate() {
|
||||
const text = content.value.trim()
|
||||
if (!text || streaming.value) return
|
||||
activeTab.value = "chat"
|
||||
sendPrompt(text)
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
html.value = ""
|
||||
css.value = ""
|
||||
js.value = ""
|
||||
}
|
||||
|
||||
function previewHistoryItem(code: { html: string; css: string; js: string }) {
|
||||
html.value = code.html
|
||||
css.value = code.css
|
||||
js.value = code.js
|
||||
}
|
||||
|
||||
function back() {
|
||||
disconnectPrompt()
|
||||
router.push({ name: "creation-list" })
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
onUnmounted(disconnectPrompt)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.creation-layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.creation-sider {
|
||||
width: 40%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border-right: 1px solid var(--n-border-color, #efeff5);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.creation-content {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.left-tabs {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.left-tabs :deep(.n-tabs-pane-wrapper) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.left-tabs :deep(.n-tab-pane) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.topic-pane {
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.topic-hint {
|
||||
display: block;
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
207
src/pages/CreationHome.vue
Normal file
207
src/pages/CreationHome.vue
Normal file
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<main class="creation-home">
|
||||
<n-flex
|
||||
justify="space-between"
|
||||
align="flex-end"
|
||||
style="margin-bottom: 32px"
|
||||
>
|
||||
<div>
|
||||
<n-flex align="center" :size="8">
|
||||
<n-button text @click="$router.push({ name: 'home' })">
|
||||
<Icon :width="20" icon="pepicons-pencil:arrow-left" />
|
||||
</n-button>
|
||||
<n-h2 style="margin: 0">自由创作</n-h2>
|
||||
</n-flex>
|
||||
<n-text depth="3">自己定一个主题,和 AI 一起把它做出来</n-text>
|
||||
</div>
|
||||
<n-button type="primary" @click="openCreate">
|
||||
<template #icon>
|
||||
<Icon icon="lucide:plus" :width="16" />
|
||||
</template>
|
||||
新建主题
|
||||
</n-button>
|
||||
</n-flex>
|
||||
|
||||
<n-spin :show="loading">
|
||||
<n-empty
|
||||
v-if="!loading && !creations.length"
|
||||
description="还没有创作,点右上角新建一个主题吧"
|
||||
style="margin-top: 72px"
|
||||
/>
|
||||
<div v-else class="card-grid">
|
||||
<n-card
|
||||
v-for="item in creations"
|
||||
:key="item.display"
|
||||
class="creation-card"
|
||||
hoverable
|
||||
@click="open(item)"
|
||||
>
|
||||
<template #header>
|
||||
<n-ellipsis style="max-width: 100%">{{ item.title }}</n-ellipsis>
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<n-flex :size="4" align="center">
|
||||
<n-tag v-if="item.has_code" type="success" size="small">
|
||||
已有作品
|
||||
</n-tag>
|
||||
<n-button text @click.stop="remove(item)">
|
||||
<Icon icon="lucide:trash-2" :width="15" />
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
<n-ellipsis :line-clamp="2" class="creation-desc">
|
||||
{{ item.content || "还没写主题描述" }}
|
||||
</n-ellipsis>
|
||||
<template #footer>
|
||||
<n-text depth="3" style="font-size: 12px">
|
||||
更新于 {{ formatTime(item.modified) }}
|
||||
</n-text>
|
||||
</template>
|
||||
</n-card>
|
||||
</div>
|
||||
</n-spin>
|
||||
|
||||
<n-modal
|
||||
v-model:show="showCreate"
|
||||
preset="card"
|
||||
title="新建主题"
|
||||
style="width: 520px"
|
||||
>
|
||||
<n-form>
|
||||
<n-form-item label="主题名称">
|
||||
<n-input
|
||||
v-model:value="form.title"
|
||||
placeholder="例如:我的番茄钟"
|
||||
maxlength="100"
|
||||
show-count
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="主题描述(可以之后再补充)">
|
||||
<n-input
|
||||
v-model:value="form.content"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 4, maxRows: 8 }"
|
||||
placeholder="想做一个什么样的页面?有哪些颜色、布局、交互?"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #footer>
|
||||
<n-flex justify="flex-end">
|
||||
<n-button @click="showCreate = false">取消</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
:loading="creating"
|
||||
:disabled="!form.title.trim()"
|
||||
@click="submit"
|
||||
>
|
||||
创建并开始
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-modal>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from "vue"
|
||||
import { useRouter } from "vue-router"
|
||||
import { useDialog, useMessage } from "naive-ui"
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { Creation } from "../api"
|
||||
import type { CreationSlim } from "../utils/type"
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
|
||||
const loading = ref(true)
|
||||
const creating = ref(false)
|
||||
const showCreate = ref(false)
|
||||
const creations = ref<CreationSlim[]>([])
|
||||
const form = reactive({ title: "", content: "" })
|
||||
|
||||
function formatTime(value: string) {
|
||||
return new Date(value).toLocaleString("zh-CN", {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
}
|
||||
|
||||
function open(item: CreationSlim) {
|
||||
router.push({ name: "creation", params: { display: item.display } })
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
form.title = ""
|
||||
form.content = ""
|
||||
showCreate.value = true
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
const title = form.title.trim()
|
||||
if (!title) return
|
||||
creating.value = true
|
||||
try {
|
||||
const created = await Creation.create({ title, content: form.content })
|
||||
showCreate.value = false
|
||||
router.push({ name: "creation", params: { display: created.display } })
|
||||
} catch {
|
||||
message.error("创建失败,请重试")
|
||||
} finally {
|
||||
creating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function remove(item: CreationSlim) {
|
||||
dialog.warning({
|
||||
title: "删除主题",
|
||||
content: `确定删除「${item.title}」吗?和它相关的对话也会一起删除。`,
|
||||
positiveText: "删除",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await Creation.remove(item.display)
|
||||
creations.value = creations.value.filter(
|
||||
(c) => c.display !== item.display,
|
||||
)
|
||||
message.success("已删除")
|
||||
} catch {
|
||||
message.error("删除失败,请重试")
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
creations.value = await Creation.listMine()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.creation-home {
|
||||
max-width: 1180px;
|
||||
margin: 0 auto;
|
||||
padding: 32px 20px 48px;
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.creation-card {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.creation-desc {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
@@ -7,9 +7,24 @@ import { authed } from "./store/user"
|
||||
|
||||
const routes = [
|
||||
{ path: "/", name: "home", component: Workspace, meta: { auth: true } },
|
||||
{ path: "/tutorial", name: "home-tutorial-list", component: Workspace, meta: { auth: true } },
|
||||
{ path: "/tutorial/:display", name: "home-tutorial", component: Workspace, meta: { auth: true } },
|
||||
{ path: "/challenge", name: "home-challenge-list", component: Workspace, meta: { auth: true } },
|
||||
{
|
||||
path: "/tutorial",
|
||||
name: "home-tutorial-list",
|
||||
component: Workspace,
|
||||
meta: { auth: true },
|
||||
},
|
||||
{
|
||||
path: "/tutorial/:display",
|
||||
name: "home-tutorial",
|
||||
component: Workspace,
|
||||
meta: { auth: true },
|
||||
},
|
||||
{
|
||||
path: "/challenge",
|
||||
name: "home-challenge-list",
|
||||
component: Workspace,
|
||||
meta: { auth: true },
|
||||
},
|
||||
{
|
||||
path: "/challenge/:display",
|
||||
name: "home-challenge",
|
||||
@@ -29,6 +44,18 @@ const routes = [
|
||||
props: true,
|
||||
meta: { auth: true },
|
||||
},
|
||||
{
|
||||
path: "/creation",
|
||||
name: "creation-list",
|
||||
component: () => import("./pages/CreationHome.vue"),
|
||||
meta: { auth: true },
|
||||
},
|
||||
{
|
||||
path: "/creation/:display",
|
||||
name: "creation",
|
||||
component: () => import("./pages/CreationDetail.vue"),
|
||||
meta: { auth: true },
|
||||
},
|
||||
{
|
||||
path: "/showcase",
|
||||
name: "showcase",
|
||||
|
||||
@@ -96,6 +96,28 @@ export interface ChallengeIn {
|
||||
example_js?: string | null
|
||||
}
|
||||
|
||||
export interface CreationSlim {
|
||||
display: number
|
||||
title: string
|
||||
content: string
|
||||
created: string
|
||||
modified: string
|
||||
has_code: boolean
|
||||
}
|
||||
|
||||
export interface CreationDetail extends CreationSlim {
|
||||
task_id: number
|
||||
html: string
|
||||
css: string
|
||||
js: string
|
||||
owner_name: string
|
||||
}
|
||||
|
||||
export interface CreationIn {
|
||||
title: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: number
|
||||
username: string
|
||||
|
||||
Reference in New Issue
Block a user