rework the memory view and rename profiles to Agent
Replace the three separate memory editors with a card layout that shows the source file, a plain-text summary, the last modified time, and the character count for each section, so the stored files are visible without opening them. Rename the sidebar "用户" entry to "Agent" in both locales and add the memory.editTitle, memory.writeTo, and memory.charCount strings.
This commit is contained in:
@@ -204,7 +204,7 @@ export default {
|
|||||||
kanban: 'Kanban',
|
kanban: 'Kanban',
|
||||||
workflow: 'Workflow',
|
workflow: 'Workflow',
|
||||||
models: 'Models',
|
models: 'Models',
|
||||||
profiles: 'Profiles',
|
profiles: 'Agent',
|
||||||
skills: 'Skills',
|
skills: 'Skills',
|
||||||
plugins: 'Plugins',
|
plugins: 'Plugins',
|
||||||
mcp: 'MCP',
|
mcp: 'MCP',
|
||||||
@@ -1195,6 +1195,9 @@ export default {
|
|||||||
title: 'Memory',
|
title: 'Memory',
|
||||||
refresh: 'Refresh',
|
refresh: 'Refresh',
|
||||||
loadFailed: 'Failed to load memory',
|
loadFailed: 'Failed to load memory',
|
||||||
|
editTitle: 'Edit · {name}',
|
||||||
|
writeTo: 'Write to',
|
||||||
|
charCount: '{count} chars',
|
||||||
myNotes: 'My Notes',
|
myNotes: 'My Notes',
|
||||||
noNotes: 'No notes yet.',
|
noNotes: 'No notes yet.',
|
||||||
notesPlaceholder: 'Write your notes...',
|
notesPlaceholder: 'Write your notes...',
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ export default {
|
|||||||
kanban: '看板',
|
kanban: '看板',
|
||||||
workflow: '工作流',
|
workflow: '工作流',
|
||||||
models: '模型',
|
models: '模型',
|
||||||
profiles: '用户',
|
profiles: 'Agent',
|
||||||
plugins: '插件',
|
plugins: '插件',
|
||||||
mcp: 'MCP',
|
mcp: 'MCP',
|
||||||
skills: '技能',
|
skills: '技能',
|
||||||
@@ -1195,6 +1195,9 @@ export default {
|
|||||||
title: '记忆',
|
title: '记忆',
|
||||||
refresh: '刷新',
|
refresh: '刷新',
|
||||||
loadFailed: '加载记忆失败',
|
loadFailed: '加载记忆失败',
|
||||||
|
editTitle: '修改 · {name}',
|
||||||
|
writeTo: '写入文件',
|
||||||
|
charCount: '{count} 字符',
|
||||||
myNotes: '我的笔记',
|
myNotes: '我的笔记',
|
||||||
noNotes: '暂无笔记。',
|
noNotes: '暂无笔记。',
|
||||||
notesPlaceholder: '输入笔记内容...',
|
notesPlaceholder: '输入笔记内容...',
|
||||||
|
|||||||
@@ -2,19 +2,40 @@
|
|||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { useMessage } from '@/composables/useMessage'
|
import { useMessage } from '@/composables/useMessage'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import MarkdownRenderer from '@/components/hermes/chat/MarkdownRenderer.vue'
|
|
||||||
import { fetchMemory, saveMemory, type MemoryData } from '@/api/hermes/skills'
|
import { fetchMemory, saveMemory, type MemoryData } from '@/api/hermes/skills'
|
||||||
import { useProfilesStore } from '@/stores/hermes/profiles'
|
import { useProfilesStore } from '@/stores/hermes/profiles'
|
||||||
|
|
||||||
|
type MemorySection = 'memory' | 'user' | 'soul'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const profilesStore = useProfilesStore()
|
const profilesStore = useProfilesStore()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const data = ref<MemoryData | null>(null)
|
const data = ref<MemoryData | null>(null)
|
||||||
const editingSection = ref<'memory' | 'user' | 'soul' | null>(null)
|
const editingSection = ref<MemorySection | null>(null)
|
||||||
const editContent = ref('')
|
const editContent = ref('')
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
|
|
||||||
|
// 三段记忆固定对应三个文件,落盘路径在服务端决定,前端只做展示。
|
||||||
|
const SECTION_FILES: Record<MemorySection, string> = {
|
||||||
|
memory: 'memories/MEMORY.md',
|
||||||
|
user: 'memories/USER.md',
|
||||||
|
soul: 'SOUL.md',
|
||||||
|
}
|
||||||
|
|
||||||
|
const SECTIONS: { key: MemorySection; labelKey: string }[] = [
|
||||||
|
{ key: 'memory', labelKey: 'memory.myNotes' },
|
||||||
|
{ key: 'user', labelKey: 'memory.userProfile' },
|
||||||
|
{ key: 'soul', labelKey: 'memory.soul' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 各段的修改时间在接口里是平铺字段,key 不可由 section 直接拼出(soul_mtime 等)。
|
||||||
|
const MTIME_KEY: Record<MemorySection, 'memory_mtime' | 'user_mtime' | 'soul_mtime'> = {
|
||||||
|
memory: 'memory_mtime',
|
||||||
|
user: 'user_mtime',
|
||||||
|
soul: 'soul_mtime',
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(loadMemory)
|
onMounted(loadMemory)
|
||||||
|
|
||||||
async function loadMemory() {
|
async function loadMemory() {
|
||||||
@@ -32,7 +53,7 @@ async function loadMemory() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startEdit(section: 'memory' | 'user' | 'soul') {
|
function startEdit(section: MemorySection) {
|
||||||
editingSection.value = section
|
editingSection.value = section
|
||||||
editContent.value = data.value?.[section] || ''
|
editContent.value = data.value?.[section] || ''
|
||||||
}
|
}
|
||||||
@@ -59,7 +80,7 @@ async function handleSave() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatTime(ts: number | null): string {
|
function formatTime(ts: number | null): string {
|
||||||
if (!ts) return ''
|
if (!ts) return '—'
|
||||||
return new Date(ts).toLocaleString([], {
|
return new Date(ts).toLocaleString([], {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
@@ -68,20 +89,78 @@ function formatTime(ts: number | null): string {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const memoryEmpty = computed(() => !data.value?.memory?.trim())
|
// 存储态用 § 分隔段落;展示与摘要都按纯文本处理。
|
||||||
const userEmpty = computed(() => !data.value?.user?.trim())
|
function plainText(raw: string | undefined): string {
|
||||||
const soulEmpty = computed(() => !data.value?.soul?.trim())
|
return (raw || '').replace(/§/g, ' ').replace(/\s+/g, ' ').trim()
|
||||||
|
}
|
||||||
|
|
||||||
const displayMemory = computed(() => (data.value?.memory || '').replace(/§/g, '\n\n'))
|
interface MemoryCard {
|
||||||
const displayUser = computed(() => (data.value?.user || '').replace(/§/g, '\n\n'))
|
key: MemorySection
|
||||||
const displaySoul = computed(() => (data.value?.soul || '').replace(/§/g, '\n\n'))
|
title: string
|
||||||
|
file: string
|
||||||
|
summary: string
|
||||||
|
mtime: number | null
|
||||||
|
chars: number
|
||||||
|
empty: boolean
|
||||||
|
// 进度条按 28 字符折算 1%,与原型一致。
|
||||||
|
percent: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const cards = computed<MemoryCard[]>(() =>
|
||||||
|
SECTIONS.map(({ key, labelKey }) => {
|
||||||
|
const raw = data.value?.[key] || ''
|
||||||
|
const plain = plainText(raw)
|
||||||
|
const chars = raw.length
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
title: t(labelKey),
|
||||||
|
file: SECTION_FILES[key],
|
||||||
|
summary: plain.length > 120 ? `${plain.slice(0, 120)}…` : plain,
|
||||||
|
mtime: data.value?.[MTIME_KEY[key]] ?? null,
|
||||||
|
chars,
|
||||||
|
empty: !raw.trim(),
|
||||||
|
percent: Math.min(100, Math.max(3, Math.round(chars / 28))),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const SECTION_LABEL_KEY: Record<MemorySection, string> = {
|
||||||
|
memory: 'memory.myNotes',
|
||||||
|
user: 'memory.userProfile',
|
||||||
|
soul: 'memory.soul',
|
||||||
|
}
|
||||||
|
|
||||||
|
const SECTION_PLACEHOLDER_KEY: Record<MemorySection, string> = {
|
||||||
|
memory: 'memory.notesPlaceholder',
|
||||||
|
user: 'memory.profilePlaceholder',
|
||||||
|
soul: 'memory.soulPlaceholder',
|
||||||
|
}
|
||||||
|
|
||||||
|
const SECTION_EMPTY_KEY: Record<MemorySection, string> = {
|
||||||
|
memory: 'memory.noNotes',
|
||||||
|
user: 'memory.noProfile',
|
||||||
|
soul: 'memory.noSoul',
|
||||||
|
}
|
||||||
|
|
||||||
|
const editingTitle = computed(() =>
|
||||||
|
editingSection.value
|
||||||
|
? t('memory.editTitle', { name: t(SECTION_LABEL_KEY[editingSection.value]) })
|
||||||
|
: '',
|
||||||
|
)
|
||||||
|
|
||||||
|
const editingFile = computed(() => (editingSection.value ? SECTION_FILES[editingSection.value] : ''))
|
||||||
|
|
||||||
|
const editingPlaceholder = computed(() =>
|
||||||
|
editingSection.value ? t(SECTION_PLACEHOLDER_KEY[editingSection.value]) : '',
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="memory-view">
|
<div class="memory-view">
|
||||||
<header class="page-header">
|
<header class="page-header">
|
||||||
<h2 class="header-title">{{ t('memory.title') }}</h2>
|
<h2 class="header-title">{{ t('memory.title') }}</h2>
|
||||||
<a-button size="small" type="text" @click="loadMemory">
|
<div class="header-actions">
|
||||||
|
<a-button size="small" type="text" :loading="loading" @click="loadMemory">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<polyline points="23 4 23 10 17 10" />
|
<polyline points="23 4 23 10 17 10" />
|
||||||
@@ -90,118 +169,38 @@ const displaySoul = computed(() => (data.value?.soul || '').replace(/§/g, '\n\n
|
|||||||
</template>
|
</template>
|
||||||
{{ t('memory.refresh') }}
|
{{ t('memory.refresh') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="memory-content">
|
<div class="memory-content cy-view">
|
||||||
<div v-if="loading && !data" class="memory-loading">{{ t('common.loading') }}</div>
|
<div v-if="loading && !data" class="memory-loading">{{ t('common.loading') }}</div>
|
||||||
<div v-else class="memory-sections">
|
|
||||||
<!-- My Notes -->
|
<div v-else class="memcards">
|
||||||
<div class="memory-section cy-panel">
|
<article v-for="card in cards" :key="card.key" class="memcard cy-panel">
|
||||||
<div class="section-header cy-panel-h">
|
<header class="memcard-h cy-panel-h">
|
||||||
<span class="section-icon">
|
<span class="memcard-t">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
<span class="mi">
|
||||||
|
<svg v-if="card.key === 'memory'" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||||
<polyline points="14 2 14 8 20 8" />
|
<polyline points="14 2 14 8 20 8" />
|
||||||
<line x1="16" y1="13" x2="8" y2="13" />
|
<line x1="16" y1="13" x2="8" y2="13" />
|
||||||
<line x1="16" y1="17" x2="8" y2="17" />
|
<line x1="16" y1="17" x2="8" y2="17" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
<svg v-else-if="card.key === 'user'" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
<h3 class="section-title">{{ t('memory.myNotes') }}</h3>
|
|
||||||
<span v-if="data?.memory_mtime" class="cy-meta">{{ formatTime(data.memory_mtime) }}</span>
|
|
||||||
<div class="cy-panel-actions">
|
|
||||||
<a-button v-if="editingSection !== 'memory'" size="mini" type="text" @click="startEdit('memory')">
|
|
||||||
<template #icon>
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
||||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
|
||||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
{{ t('common.edit') }}
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- View mode -->
|
|
||||||
<div v-if="editingSection !== 'memory'" class="section-body cy-panel-b">
|
|
||||||
<MarkdownRenderer v-if="!memoryEmpty" :content="displayMemory" />
|
|
||||||
<p v-else class="empty-text">{{ t('memory.noNotes') }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Edit mode -->
|
|
||||||
<div v-else class="section-edit cy-panel-b">
|
|
||||||
<textarea
|
|
||||||
v-model="editContent"
|
|
||||||
class="edit-textarea"
|
|
||||||
:placeholder="t('memory.notesPlaceholder')"
|
|
||||||
spellcheck="false"
|
|
||||||
></textarea>
|
|
||||||
<div class="edit-actions">
|
|
||||||
<a-button size="small" @click="cancelEdit">{{ t('common.cancel') }}</a-button>
|
|
||||||
<a-button size="small" type="primary" :loading="saving" @click="handleSave">{{ t('common.save') }}</a-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- User Profile -->
|
|
||||||
<div class="memory-section cy-panel">
|
|
||||||
<div class="section-header cy-panel-h">
|
|
||||||
<span class="section-icon">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
||||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
||||||
<circle cx="12" cy="7" r="4" />
|
<circle cx="12" cy="7" r="4" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
<svg v-else width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
<h3 class="section-title">{{ t('memory.userProfile') }}</h3>
|
|
||||||
<span v-if="data?.user_mtime" class="cy-meta">{{ formatTime(data.user_mtime) }}</span>
|
|
||||||
<div class="cy-panel-actions">
|
|
||||||
<a-button v-if="editingSection !== 'user'" size="mini" type="text" @click="startEdit('user')">
|
|
||||||
<template #icon>
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
||||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
|
||||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
{{ t('common.edit') }}
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- View mode -->
|
|
||||||
<div v-if="editingSection !== 'user'" class="section-body cy-panel-b">
|
|
||||||
<MarkdownRenderer v-if="!userEmpty" :content="displayUser" />
|
|
||||||
<p v-else class="empty-text">{{ t('memory.noProfile') }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Edit mode -->
|
|
||||||
<div v-else class="section-edit cy-panel-b">
|
|
||||||
<textarea
|
|
||||||
v-model="editContent"
|
|
||||||
class="edit-textarea"
|
|
||||||
:placeholder="t('memory.profilePlaceholder')"
|
|
||||||
spellcheck="false"
|
|
||||||
></textarea>
|
|
||||||
<div class="edit-actions">
|
|
||||||
<a-button size="small" @click="cancelEdit">{{ t('common.cancel') }}</a-button>
|
|
||||||
<a-button size="small" type="primary" :loading="saving" @click="handleSave">{{ t('common.save') }}</a-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Soul -->
|
|
||||||
<div class="memory-section cy-panel">
|
|
||||||
<div class="section-header cy-panel-h">
|
|
||||||
<span class="section-icon">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
||||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z" />
|
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z" />
|
||||||
<path d="M8 14s1.5 2 4 2 4-2 4-2" />
|
<path d="M8 14s1.5 2 4 2 4-2 4-2" />
|
||||||
<line x1="9" y1="9" x2="9.01" y2="9" />
|
<line x1="9" y1="9" x2="9.01" y2="9" />
|
||||||
<line x1="15" y1="9" x2="15.01" y2="9" />
|
<line x1="15" y1="9" x2="15.01" y2="9" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<h3 class="section-title">{{ t('memory.soul') }}</h3>
|
<h3>{{ card.title }}</h3>
|
||||||
<span v-if="data?.soul_mtime" class="cy-meta">{{ formatTime(data.soul_mtime) }}</span>
|
</span>
|
||||||
<div class="cy-panel-actions">
|
<div class="cy-panel-actions">
|
||||||
<a-button v-if="editingSection !== 'soul'" size="mini" type="text" @click="startEdit('soul')">
|
<a-button size="mini" type="text" @click="startEdit(card.key)">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
||||||
@@ -211,50 +210,60 @@ const displaySoul = computed(() => (data.value?.soul || '').replace(/§/g, '\n\n
|
|||||||
{{ t('common.edit') }}
|
{{ t('common.edit') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="memcard-b">
|
||||||
|
<p v-if="!card.empty" class="memcard-sum">{{ card.summary }}</p>
|
||||||
|
<p v-else class="memcard-sum empty-text">{{ t(SECTION_EMPTY_KEY[card.key]) }}</p>
|
||||||
|
<div class="memcard-meta">
|
||||||
|
<span class="memcard-file">{{ card.file }}</span>
|
||||||
|
<span class="grow"></span>
|
||||||
|
<span class="memcard-time">{{ formatTime(card.mtime) }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- View mode -->
|
<div class="memcard-bar">
|
||||||
<div v-if="editingSection !== 'soul'" class="section-body cy-panel-b">
|
<span class="cy-meter"><i :style="{ width: `${card.percent}%` }"></i></span>
|
||||||
<MarkdownRenderer v-if="!soulEmpty" :content="displaySoul" />
|
<span class="memcard-chars">{{ t('memory.charCount', { count: card.chars }) }}</span>
|
||||||
<p v-else class="empty-text">{{ t('memory.noSoul') }}</p>
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Edit mode -->
|
<a-modal
|
||||||
<div v-else class="section-edit cy-panel-b">
|
:visible="editingSection !== null"
|
||||||
|
:title="editingTitle"
|
||||||
|
width="min(1080px, calc(100vw - 32px))"
|
||||||
|
:mask-closable="!saving"
|
||||||
|
:closable="!saving"
|
||||||
|
:esc-to-close="!saving"
|
||||||
|
@cancel="cancelEdit"
|
||||||
|
>
|
||||||
|
<div class="edit-body">
|
||||||
|
<div class="cy-kv">
|
||||||
|
<div class="cy-kr">
|
||||||
|
<span class="cy-kk">{{ t('memory.writeTo') }}</span>
|
||||||
|
<span class="cy-kv-v edit-file">{{ editingFile }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
v-model="editContent"
|
v-model="editContent"
|
||||||
class="edit-textarea"
|
class="edit-textarea"
|
||||||
:placeholder="t('memory.soulPlaceholder')"
|
:placeholder="editingPlaceholder"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
></textarea>
|
></textarea>
|
||||||
<div class="edit-actions">
|
</div>
|
||||||
<a-button size="small" @click="cancelEdit">{{ t('common.cancel') }}</a-button>
|
<template #footer>
|
||||||
|
<a-button size="small" :disabled="saving" @click="cancelEdit">{{ t('common.cancel') }}</a-button>
|
||||||
<a-button size="small" type="primary" :loading="saving" @click="handleSave">{{ t('common.save') }}</a-button>
|
<a-button size="small" type="primary" :loading="saving" @click="handleSave">{{ t('common.save') }}</a-button>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
</a-modal>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@use '@/styles/variables' as *;
|
@use '@/styles/variables' as *;
|
||||||
|
|
||||||
.memory-view {
|
|
||||||
height: calc(100 * var(--vh));
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.memory-content {
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 20px 22px 24px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.memory-loading {
|
.memory-loading {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -264,82 +273,166 @@ const displaySoul = computed(() => (data.value?.soul || '').replace(/§/g, '\n\n
|
|||||||
color: $text-muted;
|
color: $text-muted;
|
||||||
}
|
}
|
||||||
|
|
||||||
.memory-sections {
|
// 三张卡片横排,宽度不足时自动折行 —— 对应原型 .memcards。
|
||||||
display: flex;
|
// 最小列宽比原型的 280px 更紧:侧栏占了 240px,
|
||||||
gap: 16px;
|
// 280px 的门槛会让中等宽度下折成 2+1,原型是三连排。
|
||||||
flex: 1;
|
.memcards {
|
||||||
min-height: 0;
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
margin-top: 4px;
|
||||||
|
|
||||||
@media (max-width: $breakpoint-mobile) {
|
@media (max-width: $breakpoint-mobile) {
|
||||||
flex-direction: column;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.memory-section {
|
.memcard {
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: border-color $transition-fast, transform $transition-fast;
|
||||||
|
|
||||||
// 三栏并排,不吃 .cy-panel 的纵向堆叠间距。
|
// 三卡并排,不吃 .cy-panel 的纵向堆叠间距。
|
||||||
& + .memory-section {
|
& + .memcard {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: rgba(var(--accent-primary-rgb), 0.38);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.memcard-h {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
background: $bg-secondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-icon {
|
.memcard-t {
|
||||||
color: $text-faint;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-family: $font-code;
|
||||||
|
font-size: 13.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: $text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mi {
|
||||||
|
display: flex;
|
||||||
|
color: $accent-primary;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-body {
|
.memcard-b {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 11px;
|
||||||
|
padding: 13px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memcard-sum {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12.5px;
|
||||||
|
line-height: 1.72;
|
||||||
|
color: $text-secondary;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 5;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-text {
|
.empty-text {
|
||||||
color: $text-muted;
|
color: $text-muted;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-size: 13px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-edit {
|
.memcard-meta {
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
min-height: 0;
|
gap: 8px;
|
||||||
}
|
margin-top: auto;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px dashed $border-light;
|
||||||
|
|
||||||
.edit-textarea {
|
.grow {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
padding: 12px;
|
|
||||||
border: 1px solid $border-color;
|
|
||||||
border-radius: $radius-sm;
|
|
||||||
background: $bg-input;
|
|
||||||
color: $text-primary;
|
|
||||||
font-family: $font-code;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.6;
|
|
||||||
resize: none;
|
|
||||||
outline: none;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
border-color: $accent-primary;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-actions {
|
.memcard-file {
|
||||||
|
font-family: $font-code;
|
||||||
|
font-size: 11px;
|
||||||
|
color: $text-faint;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memcard-time {
|
||||||
|
font-size: 11px;
|
||||||
|
color: $text-faint;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memcard-bar {
|
||||||
|
flex: 0 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
margin-top: 10px;
|
padding: 9px 14px;
|
||||||
|
border-top: 1px solid $border-light;
|
||||||
|
background: $bg-secondary;
|
||||||
|
|
||||||
|
.cy-meter {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.memcard-chars {
|
||||||
|
font-size: 11px;
|
||||||
|
color: $text-muted;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-file {
|
||||||
|
font-family: $font-code;
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: $text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-textarea {
|
||||||
|
width: 100%;
|
||||||
|
// 弹窗比原型更宽更高,编辑区跟着放大。
|
||||||
|
min-height: min(60vh, 560px);
|
||||||
|
resize: vertical;
|
||||||
|
outline: none;
|
||||||
|
padding: 12px 13px;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
border-radius: $radius-md;
|
||||||
|
background: $bg-input;
|
||||||
|
color: $text-primary;
|
||||||
|
font-family: $font-code;
|
||||||
|
font-size: 12.5px;
|
||||||
|
line-height: 1.72;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: $accent-primary;
|
||||||
|
box-shadow: 0 0 0 3px var(--accent-soft);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user