细节优化——1
This commit is contained in:
@@ -8,12 +8,17 @@
|
||||
<el-input v-model="form.term" />
|
||||
</el-form-item>
|
||||
<el-form-item label="发生日期" prop="onset_date">
|
||||
<el-date-picker v-model="form.onset_date" type="date" value-format="YYYY-MM-DD" />
|
||||
<el-date-picker
|
||||
v-model="form.onset_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
:disabled-date="disableFutureDate"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="严重性" prop="seriousness">
|
||||
<el-form-item label="SAE" prop="seriousness">
|
||||
<el-select v-model="form.seriousness" placeholder="请选择">
|
||||
<el-option label="SERIOUS(重)" value="SERIOUS" />
|
||||
<el-option label="NON_SERIOUS(非重)" value="NON_SERIOUS" />
|
||||
<el-option label="是" value="SERIOUS" />
|
||||
<el-option label="否" value="NON_SERIOUS" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="严重程度" prop="severity">
|
||||
@@ -24,6 +29,11 @@
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input v-model="form.description" type="textarea" />
|
||||
</el-form-item>
|
||||
<el-form-item label="转归结局" prop="outcome">
|
||||
<el-select v-model="form.outcome" placeholder="可留空" clearable>
|
||||
<el-option v-for="o in outcomes" :key="o" :label="o" :value="o" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="footer-right">
|
||||
@@ -59,13 +69,14 @@ const submitting = ref(false);
|
||||
const study = useStudyStore();
|
||||
|
||||
const severities = ["G1", "G2", "G3", "G4", "G5"];
|
||||
const outcomes = ["痊愈", "好转", "持续", "恶化", "死亡", "其他"];
|
||||
const severityLabel = (v: string) =>
|
||||
({
|
||||
G1: "G1(轻)",
|
||||
G2: "G2(中)",
|
||||
G3: "G3(重)",
|
||||
G4: "G4(危重)",
|
||||
G5: "G5(致死)",
|
||||
G1: "I级",
|
||||
G2: "II级",
|
||||
G3: "III级",
|
||||
G4: "IV级",
|
||||
G5: "V级",
|
||||
}[v] || v);
|
||||
|
||||
const form = reactive({
|
||||
@@ -75,6 +86,7 @@ const form = reactive({
|
||||
seriousness: "NON_SERIOUS",
|
||||
severity: "G1",
|
||||
description: "",
|
||||
outcome: "",
|
||||
});
|
||||
|
||||
const rules: FormRules = {
|
||||
@@ -97,6 +109,7 @@ watch(
|
||||
seriousness: val.seriousness || "NON_SERIOUS",
|
||||
severity: val.severity || "G1",
|
||||
description: val.description || "",
|
||||
outcome: val.outcome || "",
|
||||
});
|
||||
} else {
|
||||
Object.assign(form, {
|
||||
@@ -106,6 +119,7 @@ watch(
|
||||
seriousness: "NON_SERIOUS",
|
||||
severity: "G1",
|
||||
description: "",
|
||||
outcome: "",
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -116,10 +130,16 @@ const onClose = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const disableFutureDate = (time: Date) => time.getTime() > Date.now();
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
if (form.onset_date && new Date(form.onset_date) > new Date()) {
|
||||
ElMessage.warning("发生日期不得晚于当前日期");
|
||||
return;
|
||||
}
|
||||
if (!study.currentStudy) {
|
||||
ElMessage.error("未选择项目");
|
||||
return;
|
||||
@@ -132,6 +152,9 @@ const onSubmit = async () => {
|
||||
payload[k] = v;
|
||||
}
|
||||
});
|
||||
if (form.outcome === "" || form.outcome === null || form.outcome === undefined) {
|
||||
payload.outcome = null;
|
||||
}
|
||||
if (isEdit.value && props.ae) {
|
||||
await updateAe(study.currentStudy.id, props.ae.id, payload);
|
||||
} else {
|
||||
|
||||
@@ -6,31 +6,45 @@
|
||||
<el-button v-if="canComment" type="primary" size="small" @click="showInput = !showInput">新增</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="showInput" class="input-area">
|
||||
<el-input v-model="newComment" type="textarea" rows="3" placeholder="输入评论" />
|
||||
<div class="actions">
|
||||
<el-button size="small" @click="showInput = false">取消</el-button>
|
||||
<el-button size="small" type="primary" :loading="loading" @click="submit">提交</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-timeline>
|
||||
<el-timeline-item v-for="c in comments" :key="c.id" :timestamp="displayDateTime(c.created_at)">
|
||||
<div class="comment-item">
|
||||
<strong>{{ displayUser(c.created_by, { users: userMap, members: memberMap }) }}</strong>
|
||||
<p>{{ c.content }}</p>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<ThreadComposer
|
||||
v-if="showInput"
|
||||
v-model="newComment"
|
||||
v-model:file-list="fileList"
|
||||
:submitting="loading || uploading"
|
||||
:quote-item="quoteComment"
|
||||
:member-map="memberMap"
|
||||
:user-map="userMap"
|
||||
:allow-attachments="true"
|
||||
show-cancel
|
||||
@submit="submit"
|
||||
@cancel="showInput = false"
|
||||
@clear-quote="clearQuote"
|
||||
/>
|
||||
<ThreadList
|
||||
:items="comments"
|
||||
:attachments-map="attachmentsMap"
|
||||
:member-map="memberMap"
|
||||
:user-map="userMap"
|
||||
:can-quote="canComment"
|
||||
:can-delete="canDelete"
|
||||
@quote="setQuote"
|
||||
@delete="remove"
|
||||
/>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { fetchComments, createComment } from "../api/comments";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import type { UploadUserFile } from "element-plus";
|
||||
import { fetchComments, createComment, deleteComment } from "../api/comments";
|
||||
import { fetchAttachments, uploadAttachment } from "../api/attachments";
|
||||
import { useStudyStore } from "../store/study";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
import { listMembers } from "../api/members";
|
||||
import { displayDateTime, displayUser, getMemberDisplayName } from "../utils/display";
|
||||
import { getMemberDisplayName } from "../utils/display";
|
||||
import ThreadComposer from "./ThreadComposer.vue";
|
||||
import ThreadList from "./ThreadList.vue";
|
||||
|
||||
interface Props {
|
||||
studyId: string;
|
||||
@@ -45,7 +59,12 @@ const loading = ref(false);
|
||||
const newComment = ref("");
|
||||
const showInput = ref(false);
|
||||
const study = useStudyStore();
|
||||
const auth = useAuthStore();
|
||||
const members = ref<any[]>([]);
|
||||
const quoteComment = ref<any | null>(null);
|
||||
const fileList = ref<UploadUserFile[]>([]);
|
||||
const attachmentsMap = ref<Record<string, any[]>>({});
|
||||
const uploading = ref(false);
|
||||
|
||||
const load = async () => {
|
||||
if (!props.studyId) return;
|
||||
@@ -53,6 +72,7 @@ const load = async () => {
|
||||
try {
|
||||
const { data } = await fetchComments(props.studyId, props.entityType, props.entityId);
|
||||
comments.value = data.items || data || [];
|
||||
await loadAttachments();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "评论加载失败");
|
||||
} finally {
|
||||
@@ -77,6 +97,13 @@ const memberMap = computed(() =>
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
const userMap = computed(() => {
|
||||
const map: Record<string, string> = {};
|
||||
if (auth.user?.id) {
|
||||
map[auth.user.id] = auth.user.display_name || auth.user.username || auth.user.email || auth.user.id;
|
||||
}
|
||||
return map;
|
||||
});
|
||||
|
||||
const submit = async () => {
|
||||
if (!newComment.value.trim()) {
|
||||
@@ -86,9 +113,26 @@ const submit = async () => {
|
||||
if (!study.currentStudy) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
await createComment(study.currentStudy.id, props.entityType, props.entityId, { content: newComment.value });
|
||||
const { data } = await createComment(study.currentStudy.id, props.entityType, props.entityId, {
|
||||
content: newComment.value,
|
||||
quote_comment_id: quoteComment.value?.id || null,
|
||||
});
|
||||
const created = data as any;
|
||||
if (fileList.value.length && created?.id) {
|
||||
uploading.value = true;
|
||||
try {
|
||||
for (const file of fileList.value) {
|
||||
if (!file.raw) continue;
|
||||
await uploadAttachment(study.currentStudy.id, "comments", created.id, file.raw as File);
|
||||
}
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
}
|
||||
}
|
||||
ElMessage.success("提交成功");
|
||||
newComment.value = "";
|
||||
quoteComment.value = null;
|
||||
fileList.value = [];
|
||||
showInput.value = false;
|
||||
load();
|
||||
} catch (e: any) {
|
||||
@@ -98,6 +142,53 @@ const submit = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const setQuote = (comment: any) => {
|
||||
quoteComment.value = comment;
|
||||
showInput.value = true;
|
||||
};
|
||||
|
||||
const clearQuote = () => {
|
||||
quoteComment.value = null;
|
||||
};
|
||||
|
||||
const canDelete = (comment: any) => auth.user?.role === "ADMIN" || comment?.created_by === auth.user?.id;
|
||||
|
||||
const remove = async (comment: any) => {
|
||||
if (!study.currentStudy || !comment?.id) return;
|
||||
const ok = await ElMessageBox.confirm("确认删除该评论?", "提示").catch(() => null);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await deleteComment(study.currentStudy.id, props.entityType, props.entityId, comment.id);
|
||||
ElMessage.success("评论已删除");
|
||||
load();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || "删除失败");
|
||||
}
|
||||
};
|
||||
|
||||
const loadAttachments = async () => {
|
||||
if (!props.studyId || comments.value.length === 0) {
|
||||
attachmentsMap.value = {};
|
||||
return;
|
||||
}
|
||||
const entries = await Promise.all(
|
||||
comments.value.map(async (c) => {
|
||||
try {
|
||||
const { data } = await fetchAttachments(props.studyId, "comments", c.id);
|
||||
const items = (data as any).items || data || [];
|
||||
return [c.id, items] as const;
|
||||
} catch {
|
||||
return [c.id, []] as const;
|
||||
}
|
||||
})
|
||||
);
|
||||
const next: Record<string, any[]> = {};
|
||||
entries.forEach(([id, items]) => {
|
||||
next[id] = items;
|
||||
});
|
||||
attachmentsMap.value = next;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadMembers();
|
||||
load();
|
||||
@@ -110,16 +201,4 @@ onMounted(() => {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.input-area {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.actions {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
.comment-item p {
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</div>
|
||||
<el-menu
|
||||
router
|
||||
:default-active="$route.path"
|
||||
:default-active="activeMenu"
|
||||
:collapse="isCollapsed"
|
||||
:collapse-transition="true"
|
||||
class="aside-menu"
|
||||
@@ -75,7 +75,7 @@
|
||||
</el-sub-menu>
|
||||
<el-menu-item index="/study/finance">
|
||||
<el-icon><Coin /></el-icon>
|
||||
<span>费用治理</span>
|
||||
<span>费用管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/study/faq">
|
||||
<el-icon><Notebook /></el-icon>
|
||||
@@ -143,7 +143,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
import { useStudyStore } from "../store/study";
|
||||
import StudySelector from "./StudySelector.vue";
|
||||
@@ -155,9 +155,23 @@ import {
|
||||
const auth = useAuthStore();
|
||||
const study = useStudyStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const isAdmin = computed(() => auth.user?.role === "ADMIN");
|
||||
const isPm = computed(() => auth.user?.role === "PM" || study.currentStudyRole === "PM");
|
||||
const isCollapsed = ref(localStorage.getItem("ctms_sidebar_collapsed") === "1");
|
||||
const activeMenu = computed(() => {
|
||||
const path = route.path;
|
||||
if (path.startsWith("/study/milestones/")) return "/study/milestones";
|
||||
if (path.startsWith("/study/subjects/")) return "/study/subjects";
|
||||
if (path.startsWith("/study/aes/")) return "/study/aes";
|
||||
if (path.startsWith("/study/issues/")) return "/study/issues";
|
||||
if (path.startsWith("/study/data-queries/")) return "/study/data-queries";
|
||||
if (path.startsWith("/study/finance/")) return "/study/finance";
|
||||
if (path.startsWith("/study/faq/")) return "/study/faq";
|
||||
if (path.startsWith("/projects/")) return "/admin/projects";
|
||||
if (path.startsWith("/admin/projects/")) return "/admin/projects";
|
||||
return path;
|
||||
});
|
||||
|
||||
const toggleCollapse = () => {
|
||||
isCollapsed.value = !isCollapsed.value;
|
||||
|
||||
@@ -36,7 +36,7 @@ const actions = [
|
||||
{ label: "不良事件", path: "/study/aes", icon: Warning },
|
||||
{ label: "数据问题", path: "/study/data-queries", icon: QuestionFilled },
|
||||
{ label: "药品库存", path: "/study/imp/inventory", icon: Management },
|
||||
{ label: "费用治理", path: "/study/finance", icon: Money },
|
||||
{ label: "费用管理", path: "/study/finance", icon: Money },
|
||||
{ label: "项目知识库", path: "/study/faq", icon: Collection },
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div class="thread-composer">
|
||||
<div v-if="quoteItem" class="quote-box">
|
||||
<div class="quote-meta">
|
||||
引用 {{ displayUser(quoteItem.created_by, { users: userMap, members: memberMap }) }}
|
||||
· {{ displayDateTime(quoteItem.created_at) }}
|
||||
<el-button type="text" size="small" @click="$emit('clear-quote')">取消引用</el-button>
|
||||
</div>
|
||||
<div class="quote-content">{{ quoteContent(quoteItem) }}</div>
|
||||
</div>
|
||||
<el-input v-model="contentProxy" type="textarea" rows="3" placeholder="输入内容" />
|
||||
<div v-if="allowAttachments" class="upload-row">
|
||||
<div class="upload-label">附件</div>
|
||||
<el-upload v-model:file-list="fileListProxy" :auto-upload="false" multiple list-type="picture" class="thread-upload">
|
||||
<el-button size="small" class="upload-button">上传附件</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button v-if="showCancel" size="small" @click="$emit('cancel')">取消</el-button>
|
||||
<el-button size="small" type="primary" :loading="submitting" @click="$emit('submit')">提交</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import type { UploadUserFile } from "element-plus";
|
||||
import { displayDateTime, displayUser } from "../utils/display";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string;
|
||||
fileList: UploadUserFile[];
|
||||
submitting?: boolean;
|
||||
showCancel?: boolean;
|
||||
allowAttachments?: boolean;
|
||||
quoteItem?: any | null;
|
||||
memberMap?: Record<string, string>;
|
||||
userMap?: Record<string, string>;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: string): void;
|
||||
(e: "update:fileList", value: UploadUserFile[]): void;
|
||||
(e: "submit"): void;
|
||||
(e: "cancel"): void;
|
||||
(e: "clear-quote"): void;
|
||||
}>();
|
||||
|
||||
const contentProxy = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val: string) => emit("update:modelValue", val),
|
||||
});
|
||||
|
||||
const fileListProxy = computed({
|
||||
get: () => props.fileList,
|
||||
set: (val: UploadUserFile[]) => emit("update:fileList", val),
|
||||
});
|
||||
|
||||
const quoteContent = (item: any) => (item?.is_deleted ? "内容已删除" : item?.content || "—");
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.thread-composer {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.upload-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.upload-label {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
padding: 4px 8px;
|
||||
border-radius: 999px;
|
||||
background: #f2f4f7;
|
||||
}
|
||||
.thread-upload :deep(.el-upload-list) {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.upload-button {
|
||||
border: 1px dashed #3b82f6;
|
||||
color: #1d4ed8;
|
||||
background: #eff6ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.upload-button:hover {
|
||||
border-color: #2563eb;
|
||||
color: #1e40af;
|
||||
background: #e0ecff;
|
||||
}
|
||||
.actions {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
.quote-box {
|
||||
background: #f5f7fa;
|
||||
border-left: 3px solid #dcdfe6;
|
||||
padding: 8px 10px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.quote-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.quote-content {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user