yanghao 1 ماه پیش
والد
کامیت
44e675447f
1فایلهای تغییر یافته به همراه415 افزوده شده و 4 حذف شده
  1. 415 4
      src/views/findings/copyright/index.vue

+ 415 - 4
src/views/findings/copyright/index.vue

@@ -97,8 +97,11 @@
                 align="center"
                 show-overflow-tooltip
               />
-              <zm-table-column label="操作" align="center" fixed="right" width="150" action>
+              <zm-table-column label="操作" align="center" fixed="right" width="205" action>
                 <template #default="scope">
+                  <el-button link type="primary" @click="openDetail(scope.row.id)">
+                    <Icon icon="ep:view" class="mr-1" />详情
+                  </el-button>
                   <el-button link type="primary" @click="openForm('update', scope.row.id)"
                     >编辑</el-button
                   >
@@ -244,6 +247,116 @@
       <el-button @click="dialogVisible = false">取消</el-button>
     </template>
   </Dialog>
+
+  <Dialog
+    v-model="detailVisible"
+    title="研究成果详情"
+    width="820px"
+    :close-on-click-modal="false"
+    destroy-on-close
+  >
+    <div v-loading="detailLoading" class="achievement-detail">
+      <div class="detail-hero">
+        <div class="detail-hero-icon"><Icon icon="ep:trophy" /></div>
+        <div class="detail-hero-content">
+          <div class="detail-hero-label">RESEARCH ACHIEVEMENT</div>
+          <h2>{{ detailData.achievementName || '未命名成果' }}</h2>
+          <div class="detail-hero-meta">
+            <dict-tag :type="DICT_TYPE.ACHIEVEMENT_TYPE" :value="detailData.achievementType" />
+            <dict-tag
+              :type="getSubTypeDict(detailData.achievementType)"
+              :value="detailData.subType"
+            />
+          </div>
+        </div>
+      </div>
+
+      <div class="detail-section">
+        <div class="detail-section-title"><Icon icon="ep:info-filled" />基本信息</div>
+        <div class="detail-info-grid">
+          <div class="detail-info-item">
+            <span>所属部门</span>
+            <strong>{{ getDeptName(detailData.deptId) }}</strong>
+          </div>
+          <div class="detail-info-item">
+            <span>成果类型</span>
+            <dict-tag :type="DICT_TYPE.ACHIEVEMENT_TYPE" :value="detailData.achievementType" />
+          </div>
+          <div class="detail-info-item">
+            <span>子类型</span>
+            <dict-tag
+              :type="getSubTypeDict(detailData.achievementType)"
+              :value="detailData.subType"
+            />
+          </div>
+        </div>
+      </div>
+
+      <div class="detail-section detail-text-section">
+        <div class="detail-section-title"><Icon icon="ep:document" />成果概述</div>
+        <div class="detail-text-block">
+          <p>{{ detailData.summary || '暂无内容' }}</p>
+        </div>
+      </div>
+
+      <div v-if="detailExtendAttributes.length" class="detail-section">
+        <div class="detail-section-title"><Icon icon="ep:collection-tag" />详细信息</div>
+        <div class="detail-attribute-grid">
+          <div
+            v-for="attribute in detailExtendAttributes"
+            :key="attribute.identifier"
+            class="detail-attribute-item"
+          >
+            <span class="pr-4">{{ attribute.name }}</span>
+            <strong>{{ formatAttributeValue(attribute) }}</strong>
+          </div>
+        </div>
+      </div>
+
+      <div v-if="detailAttachmentGroups.length" class="detail-section">
+        <div class="detail-section-title"><Icon icon="ep:paperclip" />相关附件</div>
+        <div class="detail-attachments">
+          <div v-for="group in detailAttachmentGroups" :key="group.type" class="attachment-group">
+            <span class="attachment-label">{{ group.label }}</span>
+            <div v-if="group.files.length" class="attachment-list">
+              <div
+                v-for="(file, index) in group.files"
+                :key="file.filePath || index"
+                class="attachment-item"
+              >
+                <span
+                  class="attachment-name"
+                  :title="file.filename || file.fileName || file.filePath"
+                >
+                  <Icon icon="ep:document" class="attachment-file-icon" />
+                  {{ file.filename || file.fileName || file.filePath }}
+                </span>
+                <span class="attachment-actions">
+                  <el-button
+                    link
+                    type="primary"
+                    title="预览文件"
+                    @click="viewFileInfo(file.filePath)"
+                  >
+                    <Icon icon="ep:view" />
+                  </el-button>
+                  <el-button
+                    link
+                    type="primary"
+                    title="下载文件"
+                    @click="downloadFile(file.filePath, file.filename || file.fileName)"
+                  >
+                    <Icon icon="ep:download" />
+                  </el-button>
+                </span>
+              </div>
+            </div>
+            <span v-else class="empty-value">暂无文件</span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </Dialog>
 </template>
 
 <script setup lang="ts">
@@ -261,6 +374,15 @@ import { useUserStore } from '@/store/modules/user'
 import { defaultProps } from '@/utils/tree'
 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 
+const Base64 = {
+  encode: (value: string) =>
+    window.btoa(
+      encodeURIComponent(value).replace(/%([0-9A-F]{2})/g, (_, code) =>
+        String.fromCharCode(Number.parseInt(code, 16))
+      )
+    )
+}
+
 defineOptions({ name: 'Copyright' })
 
 const rootDeptId = 156
@@ -278,6 +400,9 @@ const deptList = ref<Tree[]>([])
 const formRef = ref()
 const queryFormRef = ref()
 const attachmentFiles = reactive<Record<string, any[]>>({})
+const detailVisible = ref(false)
+const detailLoading = ref(false)
+const detailData = ref<any>({ extAttributes: [], attachments: [] })
 
 const queryParams = reactive({
   pageNo: 1,
@@ -338,8 +463,8 @@ const subTypeOptions = computed(() =>
     : []
 )
 
-const attachmentGroups = computed<AttachmentGroup[]>(() => {
-  const type = String(formData.value.achievementType)
+const getAttachmentGroups = (achievementType: string | number): AttachmentGroup[] => {
+  const type = String(achievementType)
   if (type === '1') return [{ type: 'PAPER', label: '论文附件' }]
   if (type === '2') {
     return [
@@ -352,7 +477,9 @@ const attachmentGroups = computed<AttachmentGroup[]>(() => {
   if (type === '3') return [{ type: 'COPYRIGHT', label: '软著附件' }]
   if (type === '4') return [{ type: 'HONOR_CERTIFICATE', label: '荣誉证书附件' }]
   return []
-})
+}
+
+const attachmentGroups = computed(() => getAttachmentGroups(formData.value.achievementType))
 
 const getDeptName = (id?: number) => {
   if (!id) return '-'
@@ -450,6 +577,23 @@ const getAttachments = () =>
     })
   )
 
+const detailExtendAttributes = computed(() =>
+  normalizeExtendAttributes(detailData.value.extAttributes)
+)
+
+const detailAttachmentGroups = computed(() => {
+  const groups = getAttachmentGroups(detailData.value.achievementType).map((group) => ({
+    ...group,
+    files: [] as any[]
+  }))
+  const attachments = normalizeAttachments(detailData.value.attachments)
+  attachments.forEach((attachment) => {
+    const group = groups.find((item) => item.type === attachment.type)
+    if (group) group.files.push(attachment)
+  })
+  return groups
+})
+
 const normalizeExtendAttributes = (data: unknown): ExtendAttribute[] => {
   if (Array.isArray(data)) return data
   if (data && typeof data === 'object' && Array.isArray((data as any).data)) {
@@ -495,6 +639,22 @@ const getAttributeRules = (attribute: ExtendAttribute) => {
     : []
 }
 
+const formatAttributeValue = (attribute: ExtendAttribute) => {
+  const value = attribute.actualValue
+  if (value === undefined || value === null || value === '') return '-'
+  if (attribute.dataType === 'date') {
+    const date = toDateValue(value)
+    return date ? date.toLocaleDateString('zh-CN') : String(value)
+  }
+  if (attribute.dataType === 'dropdown') {
+    return (
+      attribute.dropdownList?.find((option) => String(option.value) === String(value))?.name ||
+      String(value)
+    )
+  }
+  return String(value)
+}
+
 const openForm = async (type: 'create' | 'update', id?: number) => {
   formType.value = type
   dialogTitle.value = type === 'create' ? '新增研究成果' : '编辑研究成果'
@@ -517,6 +677,34 @@ const openForm = async (type: 'create' | 'update', id?: number) => {
   dialogVisible.value = true
 }
 
+const openDetail = async (id: number) => {
+  detailVisible.value = true
+  detailLoading.value = true
+  try {
+    detailData.value = {
+      extAttributes: [],
+      attachments: [],
+      ...(await getResultDetail(id))
+    }
+  } finally {
+    detailLoading.value = false
+  }
+}
+
+const viewFileInfo = (file: string) => {
+  window.open(
+    'http://doc.deepoil.cc:8012/onlinePreview?url=' + encodeURIComponent(Base64.encode(file))
+  )
+}
+
+const downloadFile = (file: string, fileName?: string) => {
+  const link = document.createElement('a')
+  link.href = file
+  link.download = fileName || file.split('/').pop() || '下载文件'
+  link.target = '_blank'
+  link.click()
+}
+
 const submitForm = async () => {
   const valid = await formRef.value?.validate()
   if (!valid) return
@@ -563,3 +751,226 @@ onMounted(async () => {
   deptList.value = await DeptApi.getSimpleDeptList()
 })
 </script>
+
+<style scoped lang="scss">
+.achievement-detail {
+  max-height: 68vh;
+  overflow-y: auto;
+  padding: 0 4px 8px;
+}
+
+.detail-hero {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  padding: 20px 22px;
+  border-radius: 10px;
+  background: linear-gradient(135deg, var(--el-color-primary-light-9), var(--el-fill-color-light));
+}
+
+.detail-hero-icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 48px;
+  height: 48px;
+  flex: 0 0 48px;
+  border-radius: 12px;
+  color: var(--el-color-primary);
+  background: var(--el-color-primary-light-8);
+  font-size: 24px;
+}
+
+.detail-hero-label {
+  color: var(--el-color-primary);
+  font-size: 11px;
+  letter-spacing: 1.5px;
+}
+
+.detail-hero h2 {
+  margin: 4px 0 8px;
+  color: var(--el-text-color-primary);
+  font-size: 20px;
+}
+
+.detail-hero-meta,
+.detail-info-item,
+.attachment-group {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.detail-hero-meta {
+  color: var(--el-text-color-secondary);
+  font-size: 13px;
+}
+
+.detail-section {
+  margin-top: 22px;
+}
+
+.detail-section-title {
+  display: flex;
+  align-items: center;
+  gap: 7px;
+  margin-bottom: 13px;
+  color: var(--el-text-color-primary);
+  font-size: 15px;
+  font-weight: 600;
+}
+
+.detail-section-title :deep(.iconify) {
+  color: var(--el-color-primary);
+}
+
+.detail-info-grid,
+.detail-attribute-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 1px;
+  overflow: hidden;
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+  background: var(--el-border-color-lighter);
+}
+
+.detail-info-item,
+.detail-attribute-item {
+  min-height: 48px;
+  padding: 0 16px;
+  background: var(--el-bg-color);
+}
+
+.detail-info-item > span,
+.detail-attribute-item > span,
+.detail-text-block > span,
+.attachment-label {
+  color: var(--el-text-color-secondary);
+  font-size: 13px;
+}
+
+.detail-info-item > span,
+.detail-attribute-item > span {
+  width: 80px;
+  flex: 0 0 80px;
+}
+
+.detail-info-item strong,
+.detail-attribute-item strong {
+  overflow: hidden;
+  color: var(--el-text-color-primary);
+  font-size: 13px;
+  font-weight: 500;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.detail-attribute-item {
+  display: flex;
+  align-items: center;
+}
+
+.detail-text-block {
+  padding: 12px 14px;
+  border-left: 3px solid var(--el-color-primary-light-5);
+  background: var(--el-fill-color-lighter);
+}
+
+.detail-text-block p {
+  margin: 0;
+  color: var(--el-text-color-primary);
+  font-size: 13px;
+  line-height: 1.7;
+  white-space: pre-wrap;
+}
+
+.detail-attachments {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+  padding: 13px 16px;
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+}
+
+.attachment-label {
+  width: 100px;
+  flex: 0 0 100px;
+}
+
+.attachment-list {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.attachment-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  min-width: 0;
+  width: 100%;
+}
+
+.attachment-name {
+  display: flex;
+  align-items: center;
+  min-width: 0;
+  flex: 1;
+  overflow: hidden;
+  color: var(--el-color-primary);
+  font-size: 13px;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.attachment-file-icon {
+  flex: 0 0 auto;
+  margin-right: 4px;
+}
+
+.attachment-actions {
+  display: flex;
+  flex: 0 0 auto;
+  align-items: center;
+  gap: 2px;
+}
+
+.attachment-actions :deep(.el-button) {
+  width: 26px;
+  height: 26px;
+  margin-left: 0;
+  padding: 0;
+}
+
+.empty-value {
+  color: var(--el-text-color-placeholder);
+  font-size: 13px;
+}
+
+:deep(.el-empty) {
+  padding: 12px 0 4px;
+}
+
+@media (max-width: 640px) {
+  .detail-info-grid,
+  .detail-attribute-grid {
+    grid-template-columns: 1fr;
+  }
+
+  .detail-hero {
+    padding: 16px;
+  }
+
+  .detail-hero h2 {
+    font-size: 18px;
+  }
+
+  .attachment-group {
+    align-items: flex-start;
+  }
+}
+</style>