细节优化——1

This commit is contained in:
Cheng Zhou
2025-12-30 14:07:57 +08:00
parent 71db309e12
commit 0c1fc49f17
40 changed files with 1610 additions and 389 deletions
+197
View File
@@ -0,0 +1,197 @@
<template>
<el-timeline>
<el-timeline-item v-for="item in items" :key="item.id" :timestamp="displayDateTime(item.created_at)">
<div class="thread-item" :class="{ 'is-highlighted': isHighlighted(item) }">
<div class="thread-meta">
<strong>{{ displayUser(item.created_by, { users: userMap, members: memberMap }) }}</strong>
<div class="thread-actions">
<el-button v-if="canQuote" type="text" size="small" @click="$emit('quote', item)">引用</el-button>
<el-button v-if="canDelete?.(item)" type="text" size="small" class="danger" @click="$emit('delete', item)">
删除
</el-button>
<slot name="actions" :item="item" />
</div>
</div>
<div v-if="item.quote" class="quote-block">
<div class="quote-meta">
{{ displayUser(item.quote.created_by, { users: userMap, members: memberMap }) }}
· {{ displayDateTime(item.quote.created_at) }}
</div>
<div class="quote-content">{{ quoteContent(item.quote) }}</div>
</div>
<div v-if="attachmentsMap[item.id]?.length" class="thread-attachments">
<div v-for="file in attachmentsMap[item.id]" :key="file.id" class="attachment-item">
<el-image
v-if="isImage(file)"
:src="attachmentUrl(file.id)"
:preview-src-list="[attachmentUrl(file.id)]"
fit="cover"
preview-teleported
class="attachment-image"
/>
<div v-if="isImage(file)" class="attachment-name" :title="file.filename">
{{ file.filename }}
</div>
<el-link v-else :underline="false" class="file-link" @click="download(file.id)">
<span class="file-badge">文件</span>
<span class="file-name">{{ file.filename }}</span>
</el-link>
</div>
</div>
<p>{{ contentText(item) }}</p>
</div>
</el-timeline-item>
</el-timeline>
</template>
<script setup lang="ts">
import { displayDateTime, displayUser } from "../utils/display";
const props = defineProps<{
items: any[];
attachmentsMap: Record<string, any[]>;
memberMap?: Record<string, string>;
userMap?: Record<string, string>;
canQuote?: boolean;
canDelete?: (item: any) => boolean;
highlightIds?: Array<string | number>;
}>();
defineEmits<{
(e: "quote", item: any): void;
(e: "delete", item: any): void;
}>();
const quoteContent = (item: any) => (item?.is_deleted ? "内容已删除" : item?.content || "—");
const contentText = (item: any) => (item?.is_deleted ? "内容已删除" : item?.content || "—");
const attachmentUrl = (id: string) => {
const token = localStorage.getItem("ctms_token");
return token ? `/api/v1/attachments/${id}/download?token=${token}` : `/api/v1/attachments/${id}/download`;
};
const download = (id: string) => {
window.open(attachmentUrl(id), "_blank");
};
const isImage = (file: any) => {
const type = String(file?.content_type || "").toLowerCase();
if (type.startsWith("image/")) return true;
const name = String(file?.filename || "").toLowerCase();
return [".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp"].some((ext) => name.endsWith(ext));
};
const isHighlighted = (item: any) => {
if (!props.highlightIds?.length) return false;
return props.highlightIds.includes(item?.id);
};
</script>
<style scoped>
.thread-item.is-highlighted {
background: #fff7ed;
border: 1px solid #fed7aa;
border-radius: 10px;
padding: 10px 12px;
}
.thread-item {
padding: 10px 12px;
border-radius: 10px;
border: 1px solid #edf1f7;
background: #ffffff;
margin: 6px 0;
}
.thread-item.is-highlighted {
border-color: #fed7aa;
}
.thread-item p {
margin: 4px 0 0;
}
.thread-meta {
display: flex;
justify-content: space-between;
align-items: center;
}
.thread-actions {
display: flex;
gap: 8px;
align-items: center;
}
.quote-block {
background: #f5f7fa;
border-left: 3px solid #dcdfe6;
padding: 8px 10px;
border-radius: 4px;
margin-top: 6px;
}
.quote-meta {
display: flex;
justify-content: space-between;
color: #909399;
font-size: 12px;
margin-bottom: 6px;
}
.quote-content {
white-space: pre-wrap;
}
.thread-attachments {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 6px;
}
.attachment-item {
display: inline-flex;
flex-direction: column;
align-items: flex-start;
gap: 6px;
max-width: 140px;
}
.attachment-image {
width: 128px;
height: 96px;
border-radius: 10px;
border: 1px solid #ebeef5;
background: #fafafa;
box-shadow: 0 6px 14px rgba(15, 23, 42, 0.08);
}
.attachment-name {
font-size: 12px;
color: #606266;
max-width: 128px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-link {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 8px;
background: #f8fafc;
border: 1px solid #e2e8f0;
color: #1f2937;
font-weight: 600;
}
.file-link:hover {
border-color: #cbd5f5;
background: #eef2ff;
}
.file-badge {
font-size: 11px;
color: #334155;
background: #e2e8f0;
padding: 2px 6px;
border-radius: 999px;
}
.file-name {
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.danger {
color: #f56c6c;
}
</style>