8c8327df92
- 重构 DesktopPreferences 为分栏式设置面板,整合连接、外观、通知、更新与诊断信息分区,并补充过渡动效与暗色主题样式 - DesktopLayout 侧边栏导航分组支持展开折叠,调整管理/项目区块顺序并统一图标与标题 - 新增 fileTaskFeedback 工具,统一 pickFiles/saveFile/openFile 的成功/取消提示,替换审计导出、权限日志、附件、文档、线程、项目配置等处的直接调用 - desktopUpdateManager 暴露更新状态快照与状态变更监听,区分检查中、安装中、已推迟、失败等状态 - DesktopServerSettings 增加连接诊断信息(检查时间、健康地址、耗时、HTTP 状态) - unified-page.css 与 ProjectMilestones 引入 CSS 变量以适配暗色主题 - WebLayout 将服务器设置入口改为打开系统偏好面板,管理菜单中邮件服务归入系统设置分组 - ProfileSettings 移除已迁入偏好面板的桌面端专属区块 - 补充 Layout.desktop 布局与偏好面板契约测试
139 lines
4.2 KiB
Vue
139 lines
4.2 KiB
Vue
<template>
|
|
<div class="thread-composer">
|
|
<div v-if="quoteItem" class="quote-box">
|
|
<div class="quote-meta">
|
|
{{ TEXT.common.actions.quote }} {{ displayUser(quoteItem.created_by, { users: userMap, members: memberMap }) }}
|
|
· {{ displayDateTime(quoteItem.created_at) }}
|
|
<el-button type="text" size="small" @click="$emit('clear-quote')">{{ TEXT.modules.knowledgeMedicalConsult.clearQuote }}</el-button>
|
|
</div>
|
|
<div class="quote-content">{{ quoteContent(quoteItem) }}</div>
|
|
</div>
|
|
<el-input v-model="contentProxy" type="textarea" rows="3" :placeholder="TEXT.common.placeholders.input" />
|
|
<div v-if="allowAttachments" class="upload-row">
|
|
<div class="upload-label">{{ TEXT.common.labels.attachments }}</div>
|
|
<el-upload v-if="!nativeFiles" v-model:file-list="fileListProxy" :auto-upload="false" multiple list-type="picture" class="thread-upload">
|
|
<el-button size="small" class="upload-button">{{ TEXT.common.actions.upload }}</el-button>
|
|
</el-upload>
|
|
<el-button v-else size="small" class="upload-button" @click="pickNativeAttachments">
|
|
{{ TEXT.common.actions.upload }}
|
|
</el-button>
|
|
</div>
|
|
<div class="actions">
|
|
<el-button v-if="showCancel" size="small" @click="$emit('cancel')">{{ TEXT.common.actions.cancel }}</el-button>
|
|
<el-button size="small" type="primary" :loading="submitting" @click="$emit('submit')">{{ TEXT.common.actions.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";
|
|
import { clientRuntime } from "../runtime";
|
|
import { pickFilesWithFeedback } from "../utils/fileTaskFeedback";
|
|
import { TEXT } from "../locales";
|
|
|
|
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 nativeFiles = clientRuntime.capabilities().nativeFiles;
|
|
|
|
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 ? TEXT.modules.knowledgeMedicalConsult.quoteDeleted : item?.content || TEXT.common.fallback);
|
|
|
|
const pickNativeAttachments = async () => {
|
|
const files = await pickFilesWithFeedback({ multiple: true, title: TEXT.common.labels.attachments });
|
|
const existing = new Set(props.fileList.map((item) => `${item.name}:${item.size}`));
|
|
const additions: UploadUserFile[] = files
|
|
.filter((file) => !existing.has(`${file.name}:${file.size}`))
|
|
.map((file) => ({
|
|
name: file.name,
|
|
size: file.size,
|
|
raw: file as any,
|
|
status: "ready",
|
|
uid: Date.now() + Math.floor(Math.random() * 100000),
|
|
}));
|
|
emit("update:fileList", [...props.fileList, ...additions]);
|
|
};
|
|
</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>
|