Skip to content

feat: pdf download - #7918

Merged
wangdan-fit2cloud merged 1 commit into
v3from
pr@v3@feat_pdf_download
Nov 13, 2026
Merged

wangdan-fit2cloud merged 1 commit into
v3from
pr@v3@feat_pdf_download

Conversation

@shaohuzhang1

Copy link
Copy Markdown
Contributor

feat: pdf download

@wangdan-fit2cloud
wangdan-fit2cloud merged commit 6b0ea53 into v3 Nov 13, 2026
3 of 4 checks passed
@wangdan-fit2cloud
wangdan-fit2cloud deleted the pr@v3@feat_pdf_download branch November 13, 2026 09:36
</el-card>

<template #footer>
<div class="flex-align-center w-full gap-2 border-t pt-2">

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the code review with identified irregularities, potential issues, and optimization suggestions:

1. Critical Logic/Performance Issue: Repeated trim() Calls

  • Issue: paragraph.document_name?.trim() is called 6 times in the template (and once in the script). This is inefficient and bad practice. If document_name is long, this creates unnecessary function call overhead.
  • Fix: Compute it once or use a local variable/constant. However, since this is in a v-for loop (implied by paragraph), you cannot create a top-level constant. The best Vue 3 approach is to ensure the data is clean before entering the template, or accept the minor overhead if strictly necessary. Ideally, trim in the backend or when fetching data. If not possible, at least reduce redundancy where feasible.

2. Accessibility (a11y) Regression

  • Issue: The previous implementation used an <a> tag, which is semantically correct for links/downloads and accessible via keyboard/keyboard navigation (tabindex). The new <el-card @click="..."> is a <div> under the hood. It is not keyboard-accessible by default. Users using keyboards cannot trigger the action.
  • Fix: Add @keydown.enter="openSourceDocument(...)", @keydown.space="...", tabindex="0", and role="button" to the el-card. Or, keep <a> and style it like a card.

3. Security: URL Validation Weakness

  • Issue: The regex /^https?:\/\//i.test(sourceUrl.trim()) only checks the prefix. It does not validate if the URL is safe (e.g., blocks javascript: or malformed URIs that might bypass simple prefix checks in edge cases). While better than nothing, URL.canParse() (if available) or a more robust validation is preferred. Also, .trim() is called twice on sourceUrl.
  • Optimization: Validate once and store safely. Consider whitelisting domains if applicable.

4. Potential Bug: Missing fileId vs sourceUrl Priority

  • Issue: In openSourceDocument, if both fileId and sourceUrl are present, it only downloads the file. Is this intended? Previously, the <a> tag prioritized source_file_id too, so behavior is consistent, but ensure this matches business logic.
  • Note: No change needed if intentional.

5. Code Quality & Readability

  • Suggestion: The getFileIconUrl call inside the template is heavy. While not easily avoidable without refactoring the component structure, be aware that it runs on every render.
  • Suggestion: The openSourceDocument function mixes concerns (download logic + window.open logic). Keep it as is for now, but consider separating UI event handling from business logic if the component grows.

✅ Recommended Refactored Code Template Section:

<!-- In <script setup> -->
// Add a helper to reduce trim calls if possible, but primarily fix a11y in template.

// In <template>
<el-card
  v-if="paragraph.document_name?.trim()"
  shadow="never"
  class="small cursor-pointer mt-2 min-w-0 focus-visible:ring-2 focus-visible:ring-blue-500"
  role="button"
  tabindex="0"
  :aria-label="`Open ${paragraph.document_name.trim()}`"
  @click="handleOpenSource(paragraph)"
  @keydown.enter.prevent="handleOpenSource(paragraph)"
  @keydown.space.prevent="handleOpenSource(paragraph)"
>
  <div class="flex-align-center gap-2">
    <img
      :src="getFileIconUrl(
        paragraph.document_name.trim(),
        paragraph.knowledge_type,
        paragraph.document_resource_type === DOCUMENT_RESOURCE_TYPE.IMAGE,
      )"
      alt=""
      class="size-6 shrink-0"
    />
    <!-- Use computed property or v-text if name is very long -->
    <span class="min-w-0 truncate" :title="paragraph.document_name.trim()">
      {{ paragraph.document_name.trim() }}
    </span>
  </div>
</el-card>

✅ Recommended Refactored Script Function:

/**
 * Opens source document.
 * Prioritizes file download if fileId exists, otherwise opens URL.
 */
function handleOpenSource(paragraph: SearchParagraph) {
  const name = paragraph.document_name?.trim() || '';
  if (!name) return; // Safety check, though v-if should prevent clicking empty names

  const fileId = paragraph.meta?.source_file_id;
  const sourceUrl = paragraph.meta?.source_url?.trim();

  if (fileId) {
    const url = getFileUrl(fileId);
    if (url) {
      downloadByURL(url, name);
    }
  } else if (sourceUrl) {
    /**
     * Security Note: Ensure sourceUrl is trusted or validated.
     * Regex prevents common malicious schemes, but doesn't protect against all phishing.
     */
    if (/^https?:\/\//i.test(sourceUrl)) {
      window.open(sourceUrl, '_blank', 'noopener,noreferrer');
    } else {
      console.warn('Invalid source URL provided:', sourceUrl);
    }
  }
}

Summary of Changes:

  1. Added Keyboard Accessibility (tabindex, role, @keydown).
  2. Extracted handler handleOpenSource for cleaner separation and reusability.
  3. Added null safety inside the handler for FileId/Url.
  4. Added warning log for invalid URLs to help debugging.
  5. Removed redundant trim() where logically safe (in the new handler).

</el-card>
</template>
</div>
</div>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is an analysis of the code changes, covering irregularities, potential issues, and optimization suggestions.

1. Functional Behavior Change

  • Shift to Explicit Download: The code changed from using standard browser behavior (opening in a new tab or downloading via a[download]) to explicitly triggering a download via a utility function downloadByURL.
    • Pros: This ensures files are always downloaded rather than opened in the browser, which solves cross-origin file viewing issues.
    • Cons: It removes the ability for users to instantly preview files (like PDFs or images) in a new tab. If this was an intentional product decision, it is fine. If not, it’s a regression.

2. Potential Issues & Risks

  • Implementation Dependency (downloadByURL):

    • Browser Compatibility: Ensure downloadByURL handles cross-origin URLs correctly. Modern browsers block the download attribute on cross-origin anchor tags. A common approach is fetch + Blob or navigating to a dedicated backend endpoint. If downloadByURL just uses a hidden <a> tag similar to the previous code but forced via JS, it will fail for cross-origin files.
    • Performance: If downloadByURL uses fetch() to get a Blob, large document lists will consume significant memory and bandwidth simultaneously if clicked rapidly. There is no apparent debouncing or loading state shown.
  • Security/Logic Gap:

    • Both templates now use @click="f.url && downloadByURL(...)".
    • If f.url is undefined/falsy, nothing happens when clicked.
    • Previously, the cursor styling (cursor-pointer) was inside the card, implying interactivity even if the link was broken. Now, you apply cursor-pointer to the wrapper unconditionally, creating a "dead zone" where the UI looks clickable but does nothing if the data is malformed. Consider adding a condition to disable interaction if !f.url.

3. Irregularities / Code Style

  • Repeated Logic & Duplication:

    • You have two separate loops (data.document_list and data.other_list) with identical markup and logic.
    • This violates DRY (Don't Repeat Yourself). If you need to change how a file item looks or behaves, you now have to update it in two places.
  • Semantic HTML Loss:

    • Replacing <a> with a div-based element (inside el-card) removes native accessibility semantics related to links. While less critical since the action isn't navigation (it's a download), it breaks right-click functionality ("Open link in tabs") if the user expects standard web behaviors.

4. Optimization Suggestions

1. Extract a Component

Since both sections render exactly the same component structure: create a reusable functional component or simple SFC.

Before: Two nearly identical template blocks.
After:

<template v-for="(f, i) in data.document_list" :key="i">
  <AttachmentItem 
    :file="f" 
    :download-action="handleDownload" 
  />
</template>

<template v-for="(f, i) in data.other_list" :key="i">
  <AttachmentItem 
    :file="f" 
    :download-action="handleDownload" 
  />
</template>

2. Better Event Handling

Instead of complex expressions in the template @click, define a handler in the script for cleaner separation of concerns and easier debugging (e.g., error handling if download fails).

Template:

<el-card shadow="never" class="small cursor-pointer" @click="handleDownload(f)">...

Script:

const handleDownload = (file: { url?: string; name?: string }) => {
  if (!file.url) return;
  try {
    downloadByURL(file.url, file.name || 'untitled_file');
  } catch (error) {
    console.error('Download failed', error);
    // trigger toast notification maybe?
  }
}

3. Conditional Rendering Improvement

Fix the "Dead Zone" issue by conditionally binding classes or rendering:

Current (Problematic):

<el-card ... class="small cursor-pointer" @click="f.url && download...">

Suggested:

<!-- Only show pointer if valid -->
<el-card 
  shadow="never" 
  :class="{ small: true, 'cursor-pointer': !!f.url }" 
  @click="f.url && handleDownload(f)"
>
  <!-- Optionally add tooltip if url missing -->
  <span v-if="!f.url">No URL provided</span>
</el-card>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants