Browse Source
- List page with type filter (document/memory), keyword search, pagination - Detail page with AgentMarkdown rendering, edit modal, delete confirm - Add '文档' nav link in TopNav - Follow existing articles page patterns (layout, styles, BoDialog) Co-authored-by: CodeFree <codefree@chinatelcom.cn>feat/agent-independence
3 changed files with 989 additions and 0 deletions
@ -0,0 +1,557 @@ |
|||
<script setup lang="ts"> |
|||
import { request } from '~/utils/http/factory' |
|||
|
|||
definePageMeta({ layout: 'home' }) |
|||
|
|||
const { $toast } = useNuxtApp() |
|||
const { loggedIn } = useAuthSession() |
|||
const route = useRoute() |
|||
const router = useRouter() |
|||
|
|||
interface AgentDocumentDetail { |
|||
id: number |
|||
title: string |
|||
content: string |
|||
type: 'memory' | 'document' |
|||
summary: string | null |
|||
tags: string[] |
|||
sessionId: string | null |
|||
createdAt: string |
|||
updatedAt: string |
|||
} |
|||
|
|||
const doc = ref<AgentDocumentDetail | null>(null) |
|||
const loading = ref(true) |
|||
const notFound = ref(false) |
|||
|
|||
const editVisible = ref(false) |
|||
const editData = reactive({ |
|||
title: '', |
|||
content: '', |
|||
tags: [] as string[], |
|||
}) |
|||
const editSaving = ref(false) |
|||
const tagInput = ref('') |
|||
|
|||
const confirmShow = ref(false) |
|||
|
|||
async function fetchDocument() { |
|||
loading.value = true |
|||
try { |
|||
const id = route.params.id |
|||
const raw = await request<{ code: number; data: AgentDocumentDetail }>( |
|||
`/api/agent-documents/${id}`, |
|||
) |
|||
doc.value = raw.data |
|||
} catch (e: any) { |
|||
if (e?.statusCode === 404 || e?.data?.code === 404) { |
|||
notFound.value = true |
|||
} else { |
|||
$toast.error('加载失败') |
|||
} |
|||
} finally { |
|||
loading.value = false |
|||
} |
|||
} |
|||
|
|||
function openEdit() { |
|||
if (!doc.value) return |
|||
editData.title = doc.value.title |
|||
editData.content = doc.value.content |
|||
editData.tags = [...doc.value.tags] |
|||
tagInput.value = '' |
|||
editVisible.value = true |
|||
} |
|||
|
|||
function addTag() { |
|||
const t = tagInput.value.trim() |
|||
if (t && !editData.tags.includes(t)) { |
|||
editData.tags.push(t) |
|||
} |
|||
tagInput.value = '' |
|||
} |
|||
|
|||
function removeTag(idx: number) { |
|||
editData.tags.splice(idx, 1) |
|||
} |
|||
|
|||
async function handleSave() { |
|||
if (!editData.title.trim()) { |
|||
$toast.warning('请输入标题') |
|||
return |
|||
} |
|||
if (!editData.content.trim()) { |
|||
$toast.warning('请输入内容') |
|||
return |
|||
} |
|||
|
|||
editSaving.value = true |
|||
try { |
|||
await request(`/api/agent-documents/${route.params.id}`, { |
|||
method: 'PATCH', |
|||
body: { |
|||
title: editData.title.trim(), |
|||
content: editData.content, |
|||
tags: editData.tags, |
|||
}, |
|||
}) |
|||
$toast.success('已更新') |
|||
editVisible.value = false |
|||
await fetchDocument() |
|||
} catch (e: any) { |
|||
$toast.error(e?.data?.message || e?.message || '保存失败') |
|||
} finally { |
|||
editSaving.value = false |
|||
} |
|||
} |
|||
|
|||
async function handleDelete() { |
|||
try { |
|||
await request(`/api/agent-documents/${route.params.id}`, { method: 'DELETE' }) |
|||
$toast.success('已删除') |
|||
router.push('/agent-documents') |
|||
} catch (e: any) { |
|||
$toast.error(e?.data?.message || e?.message || '删除失败') |
|||
} |
|||
} |
|||
|
|||
onMounted(() => { |
|||
fetchDocument() |
|||
}) |
|||
</script> |
|||
|
|||
<template> |
|||
<div class="doc-detail-page"> |
|||
<div v-if="loading" class="loading-state"> |
|||
<p class="loading-text">加载中…</p> |
|||
</div> |
|||
|
|||
<div v-else-if="notFound" class="empty-state"> |
|||
<p class="empty-text">文档不存在或无权访问</p> |
|||
<NuxtLink to="/agent-documents" class="back-link">返回列表</NuxtLink> |
|||
</div> |
|||
|
|||
<template v-else-if="doc"> |
|||
<div class="detail-header"> |
|||
<NuxtLink to="/agent-documents" class="back-link">← 返回列表</NuxtLink> |
|||
<div v-if="loggedIn" class="header-actions"> |
|||
<button class="action-btn" @click="openEdit">编辑</button> |
|||
<button class="action-btn danger" @click="confirmShow = true">删除</button> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="detail-meta"> |
|||
<span class="type-badge" :class="doc.type"> |
|||
{{ doc.type === 'document' ? '文档' : '记忆' }} |
|||
</span> |
|||
<span class="meta-date"> |
|||
创建于 {{ new Date(doc.createdAt).toLocaleString('zh-CN') }} |
|||
</span> |
|||
<span class="meta-date"> |
|||
更新于 {{ new Date(doc.updatedAt).toLocaleString('zh-CN') }} |
|||
</span> |
|||
<span v-if="doc.tags.length > 0" class="meta-tags"> |
|||
<span v-for="tag in doc.tags" :key="tag" class="tag-chip">{{ tag }}</span> |
|||
</span> |
|||
</div> |
|||
|
|||
<h1 class="detail-title">{{ doc.title }}</h1> |
|||
|
|||
<p v-if="doc.summary" class="detail-summary">{{ doc.summary }}</p> |
|||
|
|||
<div class="detail-content"> |
|||
<AgentMarkdown :text="doc.content" /> |
|||
</div> |
|||
</template> |
|||
|
|||
<BoDialog v-model:show="editVisible"> |
|||
<div class="form-dialog"> |
|||
<h3 class="form-title">编辑文档</h3> |
|||
|
|||
<div class="form-group"> |
|||
<label class="form-label">标题</label> |
|||
<input |
|||
v-model="editData.title" |
|||
type="text" |
|||
class="form-input" |
|||
placeholder="输入标题" |
|||
maxlength="255" |
|||
/> |
|||
</div> |
|||
|
|||
<div class="form-group"> |
|||
<label class="form-label">内容 (Markdown)</label> |
|||
<textarea |
|||
v-model="editData.content" |
|||
class="form-textarea" |
|||
placeholder="输入内容…" |
|||
rows="16" |
|||
/> |
|||
</div> |
|||
|
|||
<div class="form-group"> |
|||
<label class="form-label">标签</label> |
|||
<div class="tag-editor"> |
|||
<span v-for="(t, i) in editData.tags" :key="t" class="tag-chip removable"> |
|||
{{ t }} |
|||
<button class="tag-remove" @click="removeTag(i)">×</button> |
|||
</span> |
|||
<input |
|||
v-model="tagInput" |
|||
type="text" |
|||
class="tag-input" |
|||
placeholder="输入标签后回车" |
|||
@keydown.enter.prevent="addTag" |
|||
/> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="form-actions"> |
|||
<button class="btn-cancel" @click="editVisible = false">取消</button> |
|||
<button |
|||
class="btn-save" |
|||
:disabled="editSaving" |
|||
@click="handleSave" |
|||
> |
|||
{{ editSaving ? '保存中…' : '保存' }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</BoDialog> |
|||
|
|||
<BoDialog v-model:show="confirmShow"> |
|||
<div class="confirm-dialog"> |
|||
<p class="confirm-msg">确定删除「{{ doc?.title }}」吗?此操作不可撤销。</p> |
|||
<div class="confirm-actions"> |
|||
<button class="btn-cancel" @click="confirmShow = false">取消</button> |
|||
<button class="btn-save danger" @click="handleDelete">确认删除</button> |
|||
</div> |
|||
</div> |
|||
</BoDialog> |
|||
</div> |
|||
</template> |
|||
|
|||
<style scoped> |
|||
.doc-detail-page { |
|||
padding: 32px 0 64px; |
|||
flex: 1; |
|||
max-width: 800px; |
|||
margin: 0 auto; |
|||
} |
|||
|
|||
.loading-state, |
|||
.empty-state { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
padding: 80px 0; |
|||
gap: 12px; |
|||
} |
|||
|
|||
.loading-text, |
|||
.empty-text { |
|||
font-size: 15px; |
|||
color: #6c6a64; |
|||
} |
|||
|
|||
.back-link { |
|||
font-size: 14px; |
|||
color: #cc785c; |
|||
text-decoration: none; |
|||
} |
|||
|
|||
.back-link:hover { |
|||
text-decoration: underline; |
|||
} |
|||
|
|||
.detail-header { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
.header-actions { |
|||
display: flex; |
|||
gap: 8px; |
|||
} |
|||
|
|||
.action-btn { |
|||
padding: 4px 12px; |
|||
border: 1px solid #e6dfd8; |
|||
border-radius: 6px; |
|||
background: #fff; |
|||
font-size: 12px; |
|||
color: #6c6a64; |
|||
cursor: pointer; |
|||
transition: all 0.15s; |
|||
} |
|||
|
|||
.action-btn:hover { |
|||
border-color: #cc785c; |
|||
color: #cc785c; |
|||
} |
|||
|
|||
.action-btn.danger:hover { |
|||
border-color: #c64545; |
|||
color: #c64545; |
|||
} |
|||
|
|||
.detail-meta { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 12px; |
|||
flex-wrap: wrap; |
|||
margin-bottom: 16px; |
|||
} |
|||
|
|||
.type-badge { |
|||
font-size: 11px; |
|||
padding: 2px 8px; |
|||
border-radius: 4px; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.type-badge.document { |
|||
background: #d4edda; |
|||
color: #2d6a4f; |
|||
} |
|||
|
|||
.type-badge.memory { |
|||
background: #d1e4f0; |
|||
color: #1a5276; |
|||
} |
|||
|
|||
.meta-date { |
|||
font-size: 12px; |
|||
color: #b8b2a6; |
|||
} |
|||
|
|||
.meta-tags { |
|||
display: flex; |
|||
gap: 4px; |
|||
flex-wrap: wrap; |
|||
} |
|||
|
|||
.tag-chip { |
|||
font-size: 11px; |
|||
padding: 1px 6px; |
|||
border-radius: 3px; |
|||
background: #efe9de; |
|||
color: #8b7e6a; |
|||
} |
|||
|
|||
.detail-title { |
|||
font-family: var(--font-serif, 'Noto Serif SC', serif); |
|||
font-size: 28px; |
|||
font-weight: 500; |
|||
color: #141413; |
|||
margin: 0 0 12px; |
|||
line-height: 1.4; |
|||
} |
|||
|
|||
.detail-summary { |
|||
font-size: 15px; |
|||
color: #6c6a64; |
|||
line-height: 1.6; |
|||
margin: 0 0 24px; |
|||
padding-bottom: 20px; |
|||
border-bottom: 1px solid #efe9de; |
|||
} |
|||
|
|||
.detail-content { |
|||
font-size: 15px; |
|||
line-height: 1.8; |
|||
color: #3d3d3a; |
|||
} |
|||
|
|||
.form-dialog { |
|||
background: #fff; |
|||
border-radius: 12px; |
|||
padding: 28px; |
|||
min-width: 640px; |
|||
max-width: 800px; |
|||
max-height: 85vh; |
|||
overflow-y: auto; |
|||
} |
|||
|
|||
.form-title { |
|||
font-size: 20px; |
|||
font-weight: 500; |
|||
color: #141413; |
|||
margin: 0 0 24px; |
|||
} |
|||
|
|||
.form-group { |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
.form-label { |
|||
display: block; |
|||
font-size: 13px; |
|||
font-weight: 500; |
|||
color: #6c6a64; |
|||
margin-bottom: 6px; |
|||
} |
|||
|
|||
.form-input { |
|||
width: 100%; |
|||
padding: 9px 14px; |
|||
border: 1px solid #e6dfd8; |
|||
border-radius: 8px; |
|||
font-size: 14px; |
|||
color: #3d3d3a; |
|||
outline: none; |
|||
background: #faf9f5; |
|||
transition: border-color 0.2s; |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
.form-input:focus { |
|||
border-color: #cc785c; |
|||
} |
|||
|
|||
.form-textarea { |
|||
width: 100%; |
|||
padding: 9px 14px; |
|||
border: 1px solid #e6dfd8; |
|||
border-radius: 8px; |
|||
font-size: 14px; |
|||
color: #3d3d3a; |
|||
outline: none; |
|||
background: #faf9f5; |
|||
resize: vertical; |
|||
font-family: monospace; |
|||
transition: border-color 0.2s; |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
.form-textarea:focus { |
|||
border-color: #cc785c; |
|||
} |
|||
|
|||
.tag-editor { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
gap: 6px; |
|||
align-items: center; |
|||
padding: 6px 8px; |
|||
border: 1px solid #e6dfd8; |
|||
border-radius: 8px; |
|||
background: #faf9f5; |
|||
min-height: 38px; |
|||
} |
|||
|
|||
.tag-chip.removable { |
|||
display: inline-flex; |
|||
align-items: center; |
|||
gap: 4px; |
|||
padding: 2px 4px 2px 8px; |
|||
} |
|||
|
|||
.tag-remove { |
|||
border: none; |
|||
background: none; |
|||
color: #8b7e6a; |
|||
cursor: pointer; |
|||
font-size: 14px; |
|||
line-height: 1; |
|||
padding: 0; |
|||
} |
|||
|
|||
.tag-remove:hover { |
|||
color: #c64545; |
|||
} |
|||
|
|||
.tag-input { |
|||
border: none; |
|||
background: none; |
|||
outline: none; |
|||
font-size: 13px; |
|||
color: #3d3d3a; |
|||
flex: 1; |
|||
min-width: 120px; |
|||
} |
|||
|
|||
.form-actions { |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
gap: 12px; |
|||
margin-top: 28px; |
|||
padding-top: 20px; |
|||
border-top: 1px solid #efe9de; |
|||
} |
|||
|
|||
.btn-cancel { |
|||
padding: 8px 20px; |
|||
border: 1px solid #e6dfd8; |
|||
border-radius: 8px; |
|||
background: #fff; |
|||
font-size: 14px; |
|||
color: #6c6a64; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.btn-cancel:hover { |
|||
background: #f5f0e8; |
|||
} |
|||
|
|||
.btn-save { |
|||
padding: 8px 20px; |
|||
border: none; |
|||
border-radius: 8px; |
|||
background: #cc785c; |
|||
font-size: 14px; |
|||
color: #fff; |
|||
cursor: pointer; |
|||
transition: background 0.15s; |
|||
} |
|||
|
|||
.btn-save:hover { |
|||
background: #a9583e; |
|||
} |
|||
|
|||
.btn-save:disabled { |
|||
opacity: 0.5; |
|||
cursor: not-allowed; |
|||
} |
|||
|
|||
.btn-save.danger { |
|||
background: #c64545; |
|||
} |
|||
|
|||
.btn-save.danger:hover { |
|||
background: #a33; |
|||
} |
|||
|
|||
.confirm-dialog { |
|||
background: #fff; |
|||
border-radius: 12px; |
|||
padding: 24px; |
|||
min-width: 320px; |
|||
} |
|||
|
|||
.confirm-msg { |
|||
font-size: 15px; |
|||
color: #3d3d3a; |
|||
margin: 0 0 20px; |
|||
} |
|||
|
|||
.confirm-actions { |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
gap: 12px; |
|||
} |
|||
|
|||
@media (max-width: 768px) { |
|||
.doc-detail-page { |
|||
padding: 20px 16px 48px; |
|||
} |
|||
|
|||
.form-dialog { |
|||
min-width: auto; |
|||
max-width: 100%; |
|||
padding: 20px; |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,431 @@ |
|||
<script setup lang="ts"> |
|||
import { request } from '~/utils/http/factory' |
|||
|
|||
definePageMeta({ layout: 'home' }) |
|||
|
|||
const { $toast } = useNuxtApp() |
|||
const { loggedIn } = useAuthSession() |
|||
const router = useRouter() |
|||
|
|||
interface AgentDocumentItem { |
|||
id: number |
|||
title: string |
|||
type: 'memory' | 'document' |
|||
summary: string | null |
|||
tags: string[] |
|||
createdAt: string |
|||
updatedAt: string |
|||
} |
|||
|
|||
interface ListResult { |
|||
items: AgentDocumentItem[] |
|||
total: number |
|||
page: number |
|||
pageSize: number |
|||
hasMore: boolean |
|||
} |
|||
|
|||
const documents = ref<AgentDocumentItem[]>([]) |
|||
const loading = ref(true) |
|||
const total = ref(0) |
|||
const page = ref(1) |
|||
const pageSize = 12 |
|||
const hasMore = ref(false) |
|||
const typeFilter = ref<'memory' | 'document' | ''>('') |
|||
const searchQuery = ref('') |
|||
|
|||
async function fetchDocuments() { |
|||
loading.value = true |
|||
try { |
|||
const params = new URLSearchParams() |
|||
params.set('page', String(page.value)) |
|||
params.set('pageSize', String(pageSize)) |
|||
if (typeFilter.value) params.set('type', typeFilter.value) |
|||
if (searchQuery.value) params.set('keyword', searchQuery.value) |
|||
|
|||
const raw = await request<{ code: number; data: ListResult }>( |
|||
`/api/agent-documents?${params.toString()}`, |
|||
) |
|||
documents.value = raw.data.items |
|||
total.value = raw.data.total |
|||
hasMore.value = raw.data.hasMore |
|||
} catch { |
|||
$toast.error('文档加载失败') |
|||
} finally { |
|||
loading.value = false |
|||
} |
|||
} |
|||
|
|||
function goDetail(id: number) { |
|||
router.push(`/agent-documents/${id}`) |
|||
} |
|||
|
|||
function onPageChange(dir: -1 | 1) { |
|||
const np = page.value + dir |
|||
if (np < 1) return |
|||
page.value = np |
|||
fetchDocuments() |
|||
} |
|||
|
|||
watch(typeFilter, () => { |
|||
page.value = 1 |
|||
fetchDocuments() |
|||
}) |
|||
|
|||
let searchTimer: ReturnType<typeof setTimeout> | null = null |
|||
function onSearchInput() { |
|||
if (searchTimer) clearTimeout(searchTimer) |
|||
searchTimer = setTimeout(() => { |
|||
page.value = 1 |
|||
fetchDocuments() |
|||
}, 400) |
|||
} |
|||
|
|||
onMounted(() => { |
|||
fetchDocuments() |
|||
}) |
|||
</script> |
|||
|
|||
<template> |
|||
<div class="agent-documents-page"> |
|||
<div class="page-header"> |
|||
<div class="header-left"> |
|||
<h1 class="page-title">Agent 文档</h1> |
|||
<span class="page-count">{{ total }} 篇</span> |
|||
</div> |
|||
<div class="header-right"> |
|||
<div class="filter-tabs"> |
|||
<button |
|||
class="filter-tab" |
|||
:class="{ active: typeFilter === '' }" |
|||
@click="typeFilter = ''" |
|||
> |
|||
全部 |
|||
</button> |
|||
<button |
|||
class="filter-tab" |
|||
:class="{ active: typeFilter === 'document' }" |
|||
@click="typeFilter = 'document'" |
|||
> |
|||
文档 |
|||
</button> |
|||
<button |
|||
class="filter-tab" |
|||
:class="{ active: typeFilter === 'memory' }" |
|||
@click="typeFilter = 'memory'" |
|||
> |
|||
记忆 |
|||
</button> |
|||
</div> |
|||
<div class="search-box"> |
|||
<input |
|||
v-model="searchQuery" |
|||
type="text" |
|||
placeholder="搜索文档…" |
|||
class="search-input" |
|||
@input="onSearchInput" |
|||
/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div v-if="loading" class="loading-state"> |
|||
<p class="loading-text">加载中…</p> |
|||
</div> |
|||
|
|||
<div v-else-if="documents.length === 0" class="empty-state"> |
|||
<p class="empty-text">暂无文档</p> |
|||
<p class="empty-hint">Agent 创建的文档会显示在这里</p> |
|||
</div> |
|||
|
|||
<div v-else class="document-list"> |
|||
<article |
|||
v-for="item in documents" |
|||
:key="item.id" |
|||
class="document-card" |
|||
@click="goDetail(item.id)" |
|||
> |
|||
<div class="card-body"> |
|||
<div class="card-meta"> |
|||
<span class="type-badge" :class="item.type"> |
|||
{{ item.type === 'document' ? '文档' : '记忆' }} |
|||
</span> |
|||
<span class="card-date">{{ new Date(item.updatedAt).toLocaleDateString('zh-CN') }}</span> |
|||
<span v-if="item.tags.length > 0" class="card-tags"> |
|||
<span v-for="tag in item.tags" :key="tag" class="tag-chip">{{ tag }}</span> |
|||
</span> |
|||
</div> |
|||
<h2 class="card-title">{{ item.title }}</h2> |
|||
<p v-if="item.summary" class="card-summary">{{ item.summary }}</p> |
|||
</div> |
|||
</article> |
|||
</div> |
|||
|
|||
<div v-if="total > pageSize" class="pagination"> |
|||
<button |
|||
class="page-btn" |
|||
:disabled="page <= 1" |
|||
@click="onPageChange(-1)" |
|||
> |
|||
上一页 |
|||
</button> |
|||
<span class="page-info">{{ page }} / {{ Math.ceil(total / pageSize) }}</span> |
|||
<button |
|||
class="page-btn" |
|||
:disabled="!hasMore" |
|||
@click="onPageChange(1)" |
|||
> |
|||
下一页 |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<style scoped> |
|||
.agent-documents-page { |
|||
padding: 32px 0 64px; |
|||
flex: 1; |
|||
} |
|||
|
|||
.page-header { |
|||
display: flex; |
|||
align-items: flex-start; |
|||
justify-content: space-between; |
|||
gap: 16px; |
|||
flex-wrap: wrap; |
|||
margin-bottom: 32px; |
|||
} |
|||
|
|||
.header-left { |
|||
display: flex; |
|||
align-items: baseline; |
|||
gap: 12px; |
|||
} |
|||
|
|||
.page-title { |
|||
font-family: var(--font-serif, 'Noto Serif SC', serif); |
|||
font-size: 28px; |
|||
font-weight: 500; |
|||
color: var(--color-ink, #141413); |
|||
margin: 0; |
|||
} |
|||
|
|||
.page-count { |
|||
font-size: 14px; |
|||
color: var(--color-muted, #6c6a64); |
|||
} |
|||
|
|||
.header-right { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 12px; |
|||
flex-wrap: wrap; |
|||
} |
|||
|
|||
.filter-tabs { |
|||
display: flex; |
|||
gap: 4px; |
|||
background: #efe9de; |
|||
border-radius: 8px; |
|||
padding: 3px; |
|||
} |
|||
|
|||
.filter-tab { |
|||
padding: 5px 14px; |
|||
border: none; |
|||
background: none; |
|||
font-size: 13px; |
|||
color: #6c6a64; |
|||
border-radius: 6px; |
|||
cursor: pointer; |
|||
transition: all 0.2s; |
|||
} |
|||
|
|||
.filter-tab.active { |
|||
background: #fff; |
|||
color: #141413; |
|||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06); |
|||
} |
|||
|
|||
.search-box { |
|||
position: relative; |
|||
} |
|||
|
|||
.search-input { |
|||
padding: 7px 14px; |
|||
border: 1px solid #e6dfd8; |
|||
border-radius: 8px; |
|||
font-size: 13px; |
|||
color: #3d3d3a; |
|||
background: #faf9f5; |
|||
outline: none; |
|||
width: 180px; |
|||
transition: border-color 0.2s; |
|||
} |
|||
|
|||
.search-input:focus { |
|||
border-color: #cc785c; |
|||
} |
|||
|
|||
.search-input::placeholder { |
|||
color: #b8b2a6; |
|||
} |
|||
|
|||
.loading-state, |
|||
.empty-state { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
padding: 80px 0; |
|||
gap: 8px; |
|||
} |
|||
|
|||
.loading-text, |
|||
.empty-text { |
|||
font-size: 15px; |
|||
color: #6c6a64; |
|||
} |
|||
|
|||
.empty-hint { |
|||
font-size: 13px; |
|||
color: #b8b2a6; |
|||
} |
|||
|
|||
.document-list { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 1px; |
|||
background: #e6dfd8; |
|||
border-radius: 12px; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.document-card { |
|||
background: #faf9f5; |
|||
padding: 20px 24px; |
|||
cursor: pointer; |
|||
transition: background 0.15s; |
|||
} |
|||
|
|||
.document-card:hover { |
|||
background: #f5f0e8; |
|||
} |
|||
|
|||
.card-body { |
|||
min-width: 0; |
|||
} |
|||
|
|||
.card-meta { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 10px; |
|||
margin-bottom: 8px; |
|||
flex-wrap: wrap; |
|||
} |
|||
|
|||
.type-badge { |
|||
font-size: 11px; |
|||
padding: 2px 8px; |
|||
border-radius: 4px; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.type-badge.document { |
|||
background: #d4edda; |
|||
color: #2d6a4f; |
|||
} |
|||
|
|||
.type-badge.memory { |
|||
background: #d1e4f0; |
|||
color: #1a5276; |
|||
} |
|||
|
|||
.card-date { |
|||
font-size: 12px; |
|||
color: #b8b2a6; |
|||
} |
|||
|
|||
.card-tags { |
|||
display: flex; |
|||
gap: 4px; |
|||
flex-wrap: wrap; |
|||
} |
|||
|
|||
.tag-chip { |
|||
font-size: 11px; |
|||
padding: 1px 6px; |
|||
border-radius: 3px; |
|||
background: #efe9de; |
|||
color: #8b7e6a; |
|||
} |
|||
|
|||
.card-title { |
|||
font-size: 18px; |
|||
font-weight: 500; |
|||
color: #141413; |
|||
margin: 0 0 6px; |
|||
line-height: 1.4; |
|||
} |
|||
|
|||
.card-summary { |
|||
font-size: 14px; |
|||
color: #6c6a64; |
|||
margin: 0; |
|||
line-height: 1.5; |
|||
display: -webkit-box; |
|||
-webkit-line-clamp: 2; |
|||
-webkit-box-orient: vertical; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.pagination { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
gap: 16px; |
|||
margin-top: 32px; |
|||
} |
|||
|
|||
.page-btn { |
|||
padding: 6px 16px; |
|||
border: 1px solid #e6dfd8; |
|||
border-radius: 6px; |
|||
background: #faf9f5; |
|||
font-size: 13px; |
|||
color: #3d3d3a; |
|||
cursor: pointer; |
|||
transition: all 0.15s; |
|||
} |
|||
|
|||
.page-btn:disabled { |
|||
opacity: 0.4; |
|||
cursor: not-allowed; |
|||
} |
|||
|
|||
.page-btn:hover:not(:disabled) { |
|||
background: #efe9de; |
|||
} |
|||
|
|||
.page-info { |
|||
font-size: 13px; |
|||
color: #6c6a64; |
|||
} |
|||
|
|||
@media (max-width: 768px) { |
|||
.agent-documents-page { |
|||
padding: 20px 16px 48px; |
|||
} |
|||
|
|||
.page-header { |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.header-right { |
|||
width: 100%; |
|||
} |
|||
|
|||
.search-input { |
|||
width: 120px; |
|||
} |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue