update
Some checks failed
Deploy / deploy (build, debian, 22) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822) (push) Has been cancelled

This commit is contained in:
2026-03-18 14:50:20 +08:00
parent 98d8099b5d
commit d68ef60ab9
17 changed files with 1207 additions and 314 deletions

View File

@@ -0,0 +1,148 @@
<template>
<n-modal
:show="show"
preset="card"
title="提示词"
style="width: 90vw; max-width: 1400px"
@update:show="$emit('update:show', $event)"
>
<n-spin :show="loading">
<n-empty v-if="!loading && rounds.length === 0" description="暂无对话记录" />
<div
v-else
style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; height: 75vh"
>
<div
style="
overflow-y: auto;
padding-right: 8px;
border-right: 1px solid #e0e0e0;
display: flex;
flex-direction: column;
gap: 8px;
"
>
<div
v-for="(round, index) in rounds"
:key="index"
style="display: flex; gap: 10px; align-items: flex-start; cursor: pointer"
@click="selectedRound = index"
>
<div
:style="{
flexShrink: 0, width: '22px', height: '22px', borderRadius: '50%',
background: selectedRound === index ? '#2080f0' : '#c2d5fb',
color: '#fff', fontSize: '12px', fontWeight: 'bold',
display: 'flex', alignItems: 'center', justifyContent: 'center',
marginTop: '2px', transition: 'background 0.2s',
}"
>
{{ index + 1 }}
</div>
<div
:style="{
flex: 1, padding: '10px 14px', borderRadius: '8px',
background: selectedRound === index ? '#e8f0fe' : '#f5f5f5',
border: selectedRound === index ? '1px solid #2080f0' : '1px solid #e0e0e0',
fontSize: '13px', lineHeight: '1.6', transition: 'all 0.2s',
}"
>
{{ round.question }}
</div>
</div>
</div>
<div style="display: flex; flex-direction: column; gap: 8px">
<div style="font-weight: bold; font-size: 13px; color: #555">
{{ selectedRound + 1 }} 轮网页
</div>
<iframe
v-if="selectedPageHtml"
:srcdoc="selectedPageHtml"
:key="selectedRound"
sandbox="allow-scripts"
style="flex: 1; border: 1px solid #e0e0e0; border-radius: 6px; background: #fff"
/>
<n-empty v-else description="该轮无网页代码" style="margin: auto" />
</div>
</div>
</n-spin>
</n-modal>
</template>
<script setup lang="ts">
import { computed, ref, watch } from "vue"
import { Prompt } from "../../api"
import type { PromptMessage } from "../../utils/type"
const props = defineProps<{
show: boolean
conversationId?: string
}>()
defineEmits<{ "update:show": [value: boolean] }>()
const loading = ref(false)
const messages = ref<PromptMessage[]>([])
const selectedRound = ref(0)
const rounds = computed(() => {
const result: { question: string; html: string | null; css: string | null; js: string | null }[] = []
for (const [i, msg] of messages.value.entries()) {
if (msg.role !== "user") continue
let html: string | null = null, css: string | null = null, js: string | null = null
for (const reply of messages.value.slice(i + 1)) {
if (reply.role === "user") break
if (reply.role === "assistant" && reply.code_html) {
html = reply.code_html
css = reply.code_css
js = reply.code_js
break
}
}
result.push({ question: msg.content, html, css, js })
}
return result
})
const selectedPageHtml = computed(() => {
const round = rounds.value[selectedRound.value]
if (!round?.html) return null
const style = round.css ? `<style>${round.css}</style>` : ""
const script = round.js ? `<script>${round.js}<\/script>` : ""
return `<!DOCTYPE html><html><head><meta charset="utf-8">${style}</head><body>${round.html}${script}</body></html>`
})
watch(
() => props.conversationId,
async (id) => {
if (!id || !props.show) return
loading.value = true
messages.value = []
selectedRound.value = 0
try {
messages.value = await Prompt.getMessages(id)
const last = rounds.value.length - 1
if (last >= 0) selectedRound.value = last
} finally {
loading.value = false
}
},
)
watch(
() => props.show,
async (visible) => {
if (!visible || !props.conversationId) return
loading.value = true
messages.value = []
selectedRound.value = 0
try {
messages.value = await Prompt.getMessages(props.conversationId)
const last = rounds.value.length - 1
if (last >= 0) selectedRound.value = last
} finally {
loading.value = false
}
},
)
</script>

View File

@@ -0,0 +1,35 @@
<template>
<n-modal preset="card" :show="show" style="max-width: 60%" @update:show="$emit('update:show', $event)">
<template #header>
<n-flex align="center">
<span>前端代码</span>
<n-button tertiary @click="$emit('copy-to-editor')">复制到编辑框</n-button>
</n-flex>
</template>
<n-tabs animated type="segment">
<n-tab-pane name="html" tab="html">
<n-code :code="html" language="html" word-wrap />
</n-tab-pane>
<n-tab-pane name="css" tab="css">
<n-code :code="css" language="css" word-wrap />
</n-tab-pane>
<n-tab-pane v-if="!!js" name="js" tab="js">
<n-code :code="js" language="js" word-wrap />
</n-tab-pane>
</n-tabs>
</n-modal>
</template>
<script setup lang="ts">
defineProps<{
show: boolean
html: string
css: string
js: string
}>()
defineEmits<{
"update:show": [value: boolean]
"copy-to-editor": []
}>()
</script>

View File

@@ -0,0 +1,107 @@
<template>
<n-spin v-if="loading" size="small" style="padding: 12px" />
<n-data-table
v-else-if="items"
:columns="subColumns"
:data="items"
size="small"
striped
:row-key="(r: SubmissionOut) => r.id"
:row-props="rowProps"
:row-class-name="rowClassName"
/>
</template>
<script setup lang="ts">
import { computed, h } from "vue"
import { NButton, NDataTable, NPopconfirm, NSpin, type DataTableColumn } from "naive-ui"
import type { SubmissionOut } from "../../utils/type"
import { TASK_TYPE } from "../../utils/const"
import { parseTime } from "../../utils/helper"
import { user } from "../../store/user"
import { submission } from "../../store/submission"
const props = defineProps<{
row: SubmissionOut
items: SubmissionOut[] | undefined
loading: boolean
}>()
const emit = defineEmits<{
select: [id: string]
delete: [row: SubmissionOut, parentId: string]
"show-chain": [conversationId: string]
}>()
const isChallenge = computed(() => props.row.task_type === TASK_TYPE.Challenge)
function rowProps(r: SubmissionOut) {
return {
style: { cursor: "pointer" },
onClick: () => emit("select", r.id),
}
}
function rowClassName(r: SubmissionOut) {
return submission.value.id === r.id ? "row-active" : ""
}
const subColumns = computed((): DataTableColumn<SubmissionOut>[] => [
{
title: "时间",
key: "created",
width: 160,
render: (r) => parseTime(r.created, "YYYY-MM-DD HH:mm:ss"),
},
{
title: "得分",
key: "score",
width: 80,
render: (r) => {
const myScore = r.my_score > 0 ? String(r.my_score) : "-"
const avgScore = r.score > 0 ? r.score.toFixed(2) : "-"
return h("div", { style: { display: "flex", gap: "6px", alignItems: "baseline" } }, [
h("span", avgScore),
h("span", { style: { fontSize: "11px", color: "#999" } }, myScore),
])
},
},
...(isChallenge.value
? [{
title: "提示词",
key: "conversation_id",
width: 70,
render: (r: SubmissionOut) => {
if (!r.conversation_id) return "-"
return h(
NButton,
{ text: true, type: "primary", onClick: (e: Event) => { e.stopPropagation(); emit("show-chain", r.conversation_id!) } },
() => "查看",
)
},
} as DataTableColumn<SubmissionOut>]
: []),
...(!isChallenge.value
? [{
title: "操作",
key: "actions",
width: 60,
render: (r: SubmissionOut) => {
if (r.username !== user.username) return null
return h(
NPopconfirm,
{ onPositiveClick: () => emit("delete", r, props.row.id) },
{
trigger: () => h(
NButton,
{ text: true, type: "error", size: "small", onClick: (e: Event) => e.stopPropagation() },
() => "删除",
),
default: () => "确定删除这次提交",
},
)
},
} as DataTableColumn<SubmissionOut>]
: []),
])
</script>

View File

@@ -0,0 +1,61 @@
<template>
<n-popover v-if="isAdmin" trigger="click">
<template #trigger>
<span :style="dotStyle" />
</template>
<n-space vertical size="small">
<n-button
v-for="opt in FLAG_OPTIONS"
:key="opt.value"
text
@click="$emit('update:flag', opt.value)"
>
<span style="display: flex; align-items: center; gap: 6px">
<span
:style="{
display: 'inline-block', width: '10px', height: '10px',
borderRadius: '50%', backgroundColor: opt.color,
}"
/>
{{ opt.label }}
</span>
</n-button>
<n-button v-if="flag" text block type="error" @click="$emit('update:flag', null)">
清除
</n-button>
</n-space>
</n-popover>
<span v-else :style="dotStyle" />
</template>
<script setup lang="ts">
import { computed } from "vue"
import type { FlagType } from "../../utils/type"
const FLAG_OPTIONS: { value: NonNullable<FlagType>; color: string; label: string }[] = [
{ value: "red", color: "#e03030", label: "值得展示" },
{ value: "blue", color: "#2080f0", label: "需要讲解" },
{ value: "green", color: "#18a058", label: "优秀作品" },
{ value: "yellow", color: "#f0a020", label: "需要改进" },
]
const props = defineProps<{
flag: FlagType
isAdmin: boolean
}>()
defineEmits<{ "update:flag": [value: FlagType] }>()
const dotStyle = computed(() => {
const match = FLAG_OPTIONS.find((f) => f.value === props.flag)
return {
display: "inline-block",
width: "12px",
height: "12px",
borderRadius: "50%",
backgroundColor: match ? match.color : "transparent",
border: match ? "none" : "1px dashed #ccc",
cursor: props.isAdmin ? "pointer" : "default",
}
})
</script>