Compare commits
3
Commits
6f0dd33de3
...
bc9da688ba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc9da688ba | ||
|
|
915b48ebc0 | ||
|
|
54003fd404 |
No files matched your search
@@ -1,143 +1,82 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="history-panel">
|
<div class="history-panel">
|
||||||
<n-flex
|
<div class="toolbar">
|
||||||
class="history-toolbar"
|
<span class="muted">共 {{ items.length }} 轮 · 点一条就把那一版放回编辑器</span>
|
||||||
align="center"
|
|
||||||
justify="space-between"
|
|
||||||
:wrap="false"
|
|
||||||
>
|
|
||||||
<n-text depth="3">共 {{ items.length }} 条历史对话</n-text>
|
|
||||||
<n-tooltip>
|
<n-tooltip>
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-button
|
<n-button quaternary size="small" :loading="loading" @click="load(true)">
|
||||||
quaternary
|
<template #icon><Icon icon="lucide:refresh-cw" /></template>
|
||||||
circle
|
|
||||||
size="small"
|
|
||||||
:loading="loading"
|
|
||||||
aria-label="刷新历史对话"
|
|
||||||
@click="load(true)"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<Icon icon="lucide:refresh-cw" />
|
|
||||||
</template>
|
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
刷新历史对话
|
刷新
|
||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
</n-flex>
|
</div>
|
||||||
|
|
||||||
<n-spin v-if="loading" class="state" />
|
<n-spin v-if="loading && !items.length" class="state" />
|
||||||
<n-empty
|
<n-empty v-else-if="!items.length" class="state" description="还没有和 AI 对话过" />
|
||||||
v-else-if="!items.length"
|
<n-scrollbar v-else class="scroll">
|
||||||
class="state"
|
<ol class="list">
|
||||||
description="暂无历史对话"
|
<li
|
||||||
/>
|
v-for="item in newestFirst"
|
||||||
<n-scrollbar v-else class="history-scrollbar">
|
|
||||||
<n-flex vertical :size="12" class="history-list">
|
|
||||||
<n-card
|
|
||||||
v-for="(item, index) in items"
|
|
||||||
:key="item.assistant_message_id"
|
:key="item.assistant_message_id"
|
||||||
class="history-card"
|
:class="['item', { on: selectedAssistantMessageId === item.assistant_message_id }]"
|
||||||
:class="{
|
|
||||||
'is-selected':
|
|
||||||
selectedAssistantMessageId === item.assistant_message_id,
|
|
||||||
}"
|
|
||||||
size="small"
|
|
||||||
:bordered="true"
|
|
||||||
hoverable
|
|
||||||
:embedded="selectedAssistantMessageId === item.assistant_message_id"
|
|
||||||
:content-style="{ padding: 0 }"
|
|
||||||
@click="selectItem(item)"
|
@click="selectItem(item)"
|
||||||
>
|
>
|
||||||
<n-flex
|
<div class="head">
|
||||||
class="history-main"
|
<span class="num">{{ item.index }}</span>
|
||||||
align="center"
|
<span :class="['src', `src-${item.source || 'conversation'}`]">
|
||||||
justify="space-between"
|
{{ sourceLabel(item.source) }}
|
||||||
:wrap="false"
|
</span>
|
||||||
>
|
<span v-if="item.prompt_level" class="level">{{ levelLabel(item.prompt_level) }}</span>
|
||||||
<n-flex align="center" :wrap="false" :size="6">
|
<span v-if="item.submission_id" class="submitted">交过</span>
|
||||||
<n-tag
|
<span class="time">{{ parseTime(item.created, "MM-DD HH:mm") }}</span>
|
||||||
round
|
<n-popconfirm @positive-click="deleteItem(item)">
|
||||||
size="small"
|
<template #trigger>
|
||||||
:bordered="false"
|
<n-button
|
||||||
:type="
|
quaternary
|
||||||
selectedAssistantMessageId === item.assistant_message_id
|
size="tiny"
|
||||||
? 'success'
|
class="del"
|
||||||
: 'default'
|
:loading="deletingId === item.assistant_message_id"
|
||||||
"
|
:disabled="deletingId !== null"
|
||||||
>
|
@click.stop
|
||||||
#{{ index + 1 }}
|
>
|
||||||
</n-tag>
|
<template #icon><Icon icon="lucide:trash-2" :width="14" /></template>
|
||||||
<n-tag
|
</n-button>
|
||||||
size="small"
|
</template>
|
||||||
:type="item.source === 'edit' ? 'warning' : item.source === 'manual' ? 'info' : 'success'"
|
<template v-if="item.submission_id">
|
||||||
>
|
这一版交过,删掉这轮对话后交的那一份也会一起删掉(有人评过分的删不了)。
|
||||||
{{ sourceLabel(item.source) }}
|
</template>
|
||||||
</n-tag>
|
<template v-else>删掉这一轮对话和它生成的网页?</template>
|
||||||
</n-flex>
|
</n-popconfirm>
|
||||||
<n-flex align="center" :wrap="false" :size="4">
|
</div>
|
||||||
<n-tag
|
<div class="q markdown-body" v-html="renderMarkdown(item.prompt)"></div>
|
||||||
v-if="selectedAssistantMessageId === item.assistant_message_id"
|
<div class="thumb">
|
||||||
size="small"
|
|
||||||
type="success"
|
|
||||||
:bordered="false"
|
|
||||||
>
|
|
||||||
正在预览
|
|
||||||
</n-tag>
|
|
||||||
<n-text depth="3">
|
|
||||||
{{ parseTime(item.created, "YYYY-MM-DD HH:mm") }}
|
|
||||||
</n-text>
|
|
||||||
<n-tooltip placement="top">
|
|
||||||
<template #trigger>
|
|
||||||
<n-button
|
|
||||||
quaternary
|
|
||||||
circle
|
|
||||||
size="small"
|
|
||||||
:loading="deletingId === item.assistant_message_id"
|
|
||||||
:disabled="deletingId !== null"
|
|
||||||
@click="deleteItem(item, $event)"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<Icon icon="lucide:trash-2" />
|
|
||||||
</template>
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
删除这条历史对话
|
|
||||||
</n-tooltip>
|
|
||||||
</n-flex>
|
|
||||||
</n-flex>
|
|
||||||
<div
|
|
||||||
class="prompt-markdown markdown-body"
|
|
||||||
v-html="renderMarkdown(item.prompt)"
|
|
||||||
/>
|
|
||||||
<div class="thumbnail" aria-label="页面缩略图">
|
|
||||||
<iframe
|
<iframe
|
||||||
v-if="item.hasPage"
|
v-if="item.hasPage"
|
||||||
title="页面缩略图"
|
title="页面缩略图"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
sandbox="allow-scripts"
|
:sandbox="PREVIEW_SANDBOX"
|
||||||
referrerpolicy="no-referrer"
|
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
:srcdoc="item.previewDoc"
|
:srcdoc="item.previewDoc"
|
||||||
/>
|
></iframe>
|
||||||
<n-empty v-else size="small" description="未生成页面" />
|
<span v-else class="muted">这一轮没有生成网页</span>
|
||||||
</div>
|
</div>
|
||||||
</n-card>
|
</li>
|
||||||
</n-flex>
|
</ol>
|
||||||
</n-scrollbar>
|
</n-scrollbar>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref, watch } from "vue"
|
import { computed, onMounted, ref, watch } from "vue"
|
||||||
import { Icon } from "../../icons"
|
import { Icon } from "../../icons"
|
||||||
import { useMessage } from "naive-ui"
|
import { useMessage } from "naive-ui"
|
||||||
import { renderMarkdown } from "../../utils/markdown"
|
import { renderMarkdown } from "../../utils/markdown"
|
||||||
import { sourceLabel } from "../../utils/helper"
|
import { levelLabel, sourceLabel } from "../../utils/helper"
|
||||||
import { Prompt } from "../../api"
|
import { Prompt } from "../../api"
|
||||||
import type { PromptHistoryItem } from "../../utils/type"
|
import type { PromptHistoryItem } from "../../utils/type"
|
||||||
import { parseTime } from "../../utils/helper"
|
import { parseTime } from "../../utils/helper"
|
||||||
import { buildPreviewDocument } from "../../utils/previewDocument"
|
import { buildPreviewDocument, PREVIEW_SANDBOX } from "../../utils/previewDocument"
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
taskId: number
|
taskId: number
|
||||||
@@ -152,6 +91,8 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
type HistoryViewItem = PromptHistoryItem & {
|
type HistoryViewItem = PromptHistoryItem & {
|
||||||
|
// 按时间先后的序号(从 1 开始)
|
||||||
|
index: number
|
||||||
hasPage: boolean
|
hasPage: boolean
|
||||||
previewDoc: string
|
previewDoc: string
|
||||||
}
|
}
|
||||||
@@ -165,12 +106,16 @@ let pendingRefresh = false
|
|||||||
|
|
||||||
const naiveMessage = useMessage()
|
const naiveMessage = useMessage()
|
||||||
|
|
||||||
function toViewItem(item: PromptHistoryItem): HistoryViewItem {
|
// 最新的放最上面,序号还是按时间先后
|
||||||
|
const newestFirst = computed(() => [...items.value].reverse())
|
||||||
|
|
||||||
|
function toViewItem(item: PromptHistoryItem, i: number): HistoryViewItem {
|
||||||
const html = item.code_html ?? ""
|
const html = item.code_html ?? ""
|
||||||
const css = item.code_css ?? ""
|
const css = item.code_css ?? ""
|
||||||
const js = item.code_js ?? ""
|
const js = item.code_js ?? ""
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
|
index: i + 1,
|
||||||
hasPage: !!(html.trim() || css.trim() || js.trim()),
|
hasPage: !!(html.trim() || css.trim() || js.trim()),
|
||||||
previewDoc: buildPreviewDocument({
|
previewDoc: buildPreviewDocument({
|
||||||
html,
|
html,
|
||||||
@@ -181,15 +126,14 @@ function toViewItem(item: PromptHistoryItem): HistoryViewItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteItem(item: HistoryViewItem, e: Event) {
|
async function deleteItem(item: HistoryViewItem) {
|
||||||
e.stopPropagation()
|
|
||||||
if (deletingId.value !== null) return
|
if (deletingId.value !== null) return
|
||||||
deletingId.value = item.assistant_message_id
|
deletingId.value = item.assistant_message_id
|
||||||
try {
|
try {
|
||||||
await Prompt.deleteMessagePair(item.assistant_message_id)
|
await Prompt.deleteMessagePair(item.assistant_message_id)
|
||||||
items.value = items.value.filter(
|
items.value = items.value
|
||||||
(i) => i.assistant_message_id !== item.assistant_message_id,
|
.filter((i) => i.assistant_message_id !== item.assistant_message_id)
|
||||||
)
|
.map((i, idx) => ({ ...i, index: idx + 1 }))
|
||||||
if (selectedAssistantMessageId.value === item.assistant_message_id) {
|
if (selectedAssistantMessageId.value === item.assistant_message_id) {
|
||||||
selectedAssistantMessageId.value = null
|
selectedAssistantMessageId.value = null
|
||||||
}
|
}
|
||||||
@@ -225,7 +169,7 @@ async function load(force = true) {
|
|||||||
try {
|
try {
|
||||||
const taskChanged = loadedTaskId !== props.taskId
|
const taskChanged = loadedTaskId !== props.taskId
|
||||||
const data = await Prompt.listHistory(props.taskId)
|
const data = await Prompt.listHistory(props.taskId)
|
||||||
items.value = data.map(toViewItem)
|
items.value = data.map((item, i) => toViewItem(item, i))
|
||||||
if (
|
if (
|
||||||
taskChanged ||
|
taskChanged ||
|
||||||
!items.value.some(
|
!items.value.some(
|
||||||
@@ -280,100 +224,164 @@ onMounted(() => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-toolbar {
|
.toolbar {
|
||||||
height: 42px;
|
height: 42px;
|
||||||
padding: 0 12px;
|
flex: none;
|
||||||
border-bottom: 1px solid var(--n-border-color, var(--wp-line));
|
display: flex;
|
||||||
flex-shrink: 0;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 8px 0 14px;
|
||||||
|
border-bottom: 1px solid var(--wp-line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--wp-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.state {
|
.state {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-scrollbar {
|
.scroll {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-list {
|
.list {
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-card {
|
|
||||||
cursor: pointer;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.history-card.is-selected {
|
|
||||||
--n-color: var(--wp-brand-soft);
|
|
||||||
box-shadow: 0 10px 24px rgba(13, 122, 82, 0.14);
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-card.is-selected .history-main {
|
|
||||||
background: var(--wp-brand-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-main {
|
|
||||||
padding: 10px 12px;
|
|
||||||
transition: background-color 0.18s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-markdown {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 12px 12px;
|
padding: 12px;
|
||||||
color: var(--wp-ink);
|
list-style: none;
|
||||||
font-size: 14px;
|
display: flex;
|
||||||
line-height: 1.6;
|
flex-direction: column;
|
||||||
word-break: break-word;
|
gap: 10px;
|
||||||
background: transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt-markdown :deep(p),
|
.item {
|
||||||
.prompt-markdown :deep(ul),
|
border: 1px solid var(--wp-line);
|
||||||
.prompt-markdown :deep(ol),
|
border-radius: 8px;
|
||||||
.prompt-markdown :deep(blockquote),
|
overflow: hidden;
|
||||||
.prompt-markdown :deep(pre) {
|
background: var(--wp-surface);
|
||||||
margin-top: 0;
|
cursor: pointer;
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt-markdown :deep(:last-child) {
|
.item:hover {
|
||||||
margin-bottom: 0;
|
border-color: var(--wp-faint);
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt-markdown :deep(pre) {
|
.item.on {
|
||||||
padding: 8px;
|
border-color: var(--wp-brand);
|
||||||
overflow-x: auto;
|
}
|
||||||
|
|
||||||
|
.head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 6px 4px 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt-markdown :deep(code) {
|
.num {
|
||||||
white-space: pre-wrap;
|
min-width: 18px;
|
||||||
word-break: break-word;
|
font: 600 12px var(--wp-font-mono);
|
||||||
|
color: var(--wp-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbnail {
|
.src,
|
||||||
height: 160px;
|
.level,
|
||||||
border-top: 1px solid var(--n-border-color, var(--wp-line));
|
.submitted {
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
line-height: 18px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.src {
|
||||||
|
color: var(--wp-css);
|
||||||
|
background: var(--wp-css-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.src-edit {
|
||||||
|
color: var(--wp-js);
|
||||||
|
background: var(--wp-js-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.src-manual {
|
||||||
|
color: var(--wp-html);
|
||||||
|
background: var(--wp-html-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.level {
|
||||||
|
color: var(--wp-ink);
|
||||||
background: var(--wp-paper);
|
background: var(--wp-paper);
|
||||||
overflow: hidden;
|
border: 1px solid var(--wp-line);
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-card.is-selected .thumbnail {
|
.submitted {
|
||||||
border-top-color: var(--wp-brand-soft);
|
color: var(--wp-brand);
|
||||||
background: var(--wp-brand-soft);
|
background: var(--wp-brand-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbnail iframe {
|
.time {
|
||||||
width: 100%;
|
margin-left: auto;
|
||||||
height: 100%;
|
font: 12px var(--wp-font-mono);
|
||||||
|
color: var(--wp-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.del {
|
||||||
|
color: var(--wp-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.q {
|
||||||
|
max-height: 66px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0 10px 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--wp-ink);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
word-break: break-word;
|
||||||
|
-webkit-mask-image: linear-gradient(#000 60%, transparent);
|
||||||
|
mask-image: linear-gradient(#000 60%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.q :deep(p),
|
||||||
|
.q :deep(ul),
|
||||||
|
.q :deep(ol),
|
||||||
|
.q :deep(pre),
|
||||||
|
.q :deep(blockquote) {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.q :deep(pre) {
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.q :deep(code) {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb {
|
||||||
|
position: relative;
|
||||||
|
height: 140px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border-top: 1px solid var(--wp-line);
|
||||||
|
background: var(--wp-paper);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按 2 倍大小渲染再缩小 */
|
||||||
|
.thumb iframe {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: var(--wp-surface);
|
transform: scale(0.5);
|
||||||
|
transform-origin: 0 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</n-spin>
|
</n-spin>
|
||||||
|
|
||||||
<template #footer>
|
<template v-if="!inReview" #footer>
|
||||||
<n-flex justify="end">
|
<n-flex justify="end">
|
||||||
<n-button type="primary" icon-placement="right" @click="openReview">
|
<n-button type="primary" icon-placement="right" @click="openReview">
|
||||||
去批改{{ isAll ? "" : " " + classname + " 班" }}
|
去批改{{ isAll ? "" : " " + classname + " 班" }}
|
||||||
@@ -153,8 +153,19 @@ import { Submission } from "../../api"
|
|||||||
import type { TaskStatsOut, TopScoredItem, UserTag } from "../../utils/type"
|
import type { TaskStatsOut, TopScoredItem, UserTag } from "../../utils/type"
|
||||||
|
|
||||||
// 老师下课前看的这道题的总结:默认看批改页正在看的班
|
// 老师下课前看的这道题的总结:默认看批改页正在看的班
|
||||||
const props = defineProps<{ taskId: number; show: boolean; title?: string }>()
|
const props = defineProps<{
|
||||||
defineEmits<{ (e: "update:show", v: boolean): void }>()
|
taskId: number
|
||||||
|
show: boolean
|
||||||
|
title?: string
|
||||||
|
// 在批改页里打开:不显示“去批改”,点高分作品直接在批改页里打开
|
||||||
|
inReview?: boolean
|
||||||
|
// 批改页正在看的班级(“__all__” 是全部班级);不传就用记住的
|
||||||
|
initialClass?: string | null
|
||||||
|
}>()
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:show": [v: boolean]
|
||||||
|
open: [submissionId: string, classname: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
// 和批改页共用“当前班级”
|
// 和批改页共用“当前班级”
|
||||||
const ALL = "__all__"
|
const ALL = "__all__"
|
||||||
@@ -237,11 +248,11 @@ watch(
|
|||||||
() => props.show,
|
() => props.show,
|
||||||
async (open) => {
|
async (open) => {
|
||||||
if (!open || !props.taskId) return
|
if (!open || !props.taskId) return
|
||||||
|
let saved: string | null = props.initialClass ?? null
|
||||||
try {
|
try {
|
||||||
classname.value = localStorage.getItem(CLASS_KEY) || ALL
|
saved ||= localStorage.getItem(CLASS_KEY)
|
||||||
} catch {
|
} catch {}
|
||||||
classname.value = ALL
|
classname.value = saved || ALL
|
||||||
}
|
|
||||||
await load()
|
await load()
|
||||||
// 记住的班级已经没有学生了
|
// 记住的班级已经没有学生了
|
||||||
if (!isAll.value && !stats.value?.classes.includes(classname.value)) {
|
if (!isAll.value && !stats.value?.classes.includes(classname.value)) {
|
||||||
@@ -257,6 +268,10 @@ function openInNewTab(query: Record<string, string>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openSubmission(item: TopScoredItem) {
|
function openSubmission(item: TopScoredItem) {
|
||||||
|
if (props.inReview) {
|
||||||
|
emit("open", item.submission_id, item.classname)
|
||||||
|
return
|
||||||
|
}
|
||||||
openInNewTab({
|
openInNewTab({
|
||||||
class: isAll.value ? item.classname : classname.value,
|
class: isAll.value ? item.classname : classname.value,
|
||||||
task: String(props.taskId),
|
task: String(props.taskId),
|
||||||
|
|||||||
@@ -48,6 +48,10 @@
|
|||||||
>
|
>
|
||||||
开始讲评({{ presentList.length }})
|
开始讲评({{ presentList.length }})
|
||||||
</n-button>
|
</n-button>
|
||||||
|
<n-button size="small" @click="showStats = true">
|
||||||
|
<template #icon><Icon icon="lucide:bar-chart-2" /></template>
|
||||||
|
课堂总结
|
||||||
|
</n-button>
|
||||||
<n-tooltip>
|
<n-tooltip>
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-button size="small" quaternary @click="loadWall">
|
<n-button size="small" quaternary @click="loadWall">
|
||||||
@@ -119,6 +123,16 @@
|
|||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<TaskStatsModal
|
||||||
|
v-if="task"
|
||||||
|
v-model:show="showStats"
|
||||||
|
:task-id="task.id"
|
||||||
|
:title="`${task.display}. ${task.title}`"
|
||||||
|
:initial-class="classname"
|
||||||
|
in-review
|
||||||
|
@open="openFromStats"
|
||||||
|
/>
|
||||||
|
|
||||||
<div v-if="presenting" class="present-mode">
|
<div v-if="presenting" class="present-mode">
|
||||||
<div class="present-head">
|
<div class="present-head">
|
||||||
<b>{{ presentList[presentIndex]?.username }}</b>
|
<b>{{ presentList[presentIndex]?.username }}</b>
|
||||||
@@ -134,11 +148,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted, onUnmounted, ref, watch } from "vue"
|
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue"
|
||||||
import { useRoute, useRouter } from "vue-router"
|
import { useRoute, useRouter } from "vue-router"
|
||||||
import { useStorage } from "@vueuse/core"
|
import { useStorage } from "@vueuse/core"
|
||||||
import { Icon } from "../icons"
|
import { Icon } from "../icons"
|
||||||
import ReviewPanel from "../components/review/ReviewPanel.vue"
|
import ReviewPanel from "../components/review/ReviewPanel.vue"
|
||||||
|
import TaskStatsModal from "../components/task/TaskStatsModal.vue"
|
||||||
import { Review } from "../api"
|
import { Review } from "../api"
|
||||||
import { buildPreviewDocument, PREVIEW_SANDBOX } from "../utils/previewDocument"
|
import { buildPreviewDocument, PREVIEW_SANDBOX } from "../utils/previewDocument"
|
||||||
import type { FlagType, ReviewOptions, ReviewProgress, ReviewStudent } from "../utils/type"
|
import type { FlagType, ReviewOptions, ReviewProgress, ReviewStudent } from "../utils/type"
|
||||||
@@ -170,6 +185,20 @@ function openDetail(id: string) {
|
|||||||
router.push({ query: { ...route.query, sub: id } })
|
router.push({ query: { ...route.query, sub: id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 课堂总结里点了高分作品:在这里打开;那个学生不在当前班就先切过去
|
||||||
|
const showStats = ref(false)
|
||||||
|
async function openFromStats(id: string, cls: string) {
|
||||||
|
showStats.value = false
|
||||||
|
if (classname.value !== ALL && classname.value !== cls) {
|
||||||
|
classname.value = cls
|
||||||
|
// 先让地址栏变成新班级(切班的 watch 也会这么改),关掉作品时“后退”才回到这个班
|
||||||
|
await nextTick()
|
||||||
|
await router.replace({ query: { class: cls, task: taskId.value ?? undefined } })
|
||||||
|
}
|
||||||
|
pushedDetail = true
|
||||||
|
router.push({ query: { class: classname.value ?? undefined, task: taskId.value ?? undefined, sub: id } })
|
||||||
|
}
|
||||||
|
|
||||||
function closeDetail() {
|
function closeDetail() {
|
||||||
if (pushedDetail) {
|
if (pushedDetail) {
|
||||||
pushedDetail = false
|
pushedDetail = false
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ export function buildPreviewDocument({
|
|||||||
<title>预览</title>
|
<title>预览</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
${assetBaseUrl ? `<base href="${assetBaseUrl}">` : ""}
|
${assetBaseUrl ? `<base href="${assetBaseUrl}">` : ""}
|
||||||
<style>${css}</style>
|
<!-- normalize 要在学生的样式前面,否则同样写法的规则(body、h1 的边距等)会被它盖掉 -->
|
||||||
<link rel="stylesheet" href="/normalize.min.css" />
|
<link rel="stylesheet" href="/normalize.min.css" />
|
||||||
|
<style>${css}</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
${html}
|
${html}
|
||||||
|
|||||||
Reference in new issue
Block a user