Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion apps/demo/src/pages/DocxViewerPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,49 @@
{{ errorCode ? `${errorCode}: ` : "" }}{{ error }}
</p>

<fieldset class="props-panel" data-testid="docx-props-panel">
<legend>组件 Props</legend>
<label class="checkbox-label">
<input v-model="showToolbar" data-testid="docx-prop-show-toolbar" type="checkbox" />
showToolbar
</label>
<label class="checkbox-label">
<input v-model="showUpload" data-testid="docx-prop-show-upload" type="checkbox" />
showUpload
</label>
<label class="checkbox-label">
<input v-model="showDownload" data-testid="docx-prop-show-download" type="checkbox" />
showDownload
</label>
<label class="checkbox-label">
<input v-model="defaultThumbnailsOpen" data-testid="docx-prop-default-thumbnails-open" type="checkbox" />
defaultThumbnailsOpen
</label>
<label class="checkbox-label">
<input v-model="isDark" data-testid="docx-prop-is-dark" type="checkbox" />
isDark
</label>
<label class="select-label">
defaultZoom
<select v-model.number="defaultZoom" data-testid="docx-prop-default-zoom">
<option v-for="zoom in zoomOptions" :key="zoom" :value="zoom">{{ zoom }}%</option>
</select>
</label>
</fieldset>

<div class="viewer-container product-surface">
<DocxViewer
v-if="fileBuffer"
:key="viewerKey"
:file="fileBuffer"
:file-name="displayName"
:runtime="runtime"
:show-toolbar="showToolbar"
:show-upload="showUpload"
:show-download="showDownload"
:default-thumbnails-open="defaultThumbnailsOpen"
:default-zoom="defaultZoom"
v-model:is-dark="isDark"
v-model:show-tracked-changes="showTrackedChanges"
v-model:show-comments="showComments"
:layout-options="{ pageWidth: 816, pageHeight: 1056 }"
Expand Down Expand Up @@ -131,6 +167,13 @@ const allowMainThreadFallback = ref(false)
const mainThreadFallbackMaxBytes = ref(0)
const showTrackedChanges = ref(true)
const showComments = ref(true)
const showToolbar = ref(true)
const showUpload = ref(true)
const showDownload = ref(true)
const defaultThumbnailsOpen = ref(false)
const isDark = ref(false)
const defaultZoom = ref(100)
const zoomOptions = [50, 75, 100, 125, 150, 175, 200]
const diagnostics = ref<VisibleDiagnostic[]>([])
let pageLoadSequence = 0
let activePageLoad:
Expand Down Expand Up @@ -203,7 +246,7 @@ function createPageRuntime() {
}

const runtime = shallowRef(createPageRuntime())
const viewerKey = computed(() => `${displayName.value}-${loadCounter.value}`)
const viewerKey = computed(() => `${displayName.value}-${loadCounter.value}-${String(defaultThumbnailsOpen.value)}`)

function setError(message: string, code: string): void {
error.value = message
Expand Down Expand Up @@ -331,6 +374,12 @@ h2 { margin-bottom: 4px; }
.runtime-config input[type="url"] { min-width: 280px; }
.runtime-config .checkbox-label { flex-direction: row; align-items: center; }
.runtime-config .checkbox-label input { padding: 0; }
.props-panel { display: flex; gap: 18px; align-items: center; flex-wrap: wrap; padding: 12px 16px; margin-bottom: 16px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--muted); }
.props-panel legend { padding: 0 6px; font-size: 13px; font-weight: 600; color: var(--muted-foreground); }
.props-panel .checkbox-label { display: flex; flex-direction: row; align-items: center; gap: 6px; font-size: 13px; color: var(--foreground); white-space: nowrap; }
.props-panel .checkbox-label input { padding: 0; }
.props-panel .select-label { display: flex; flex-direction: row; align-items: center; gap: 8px; font-size: 13px; color: var(--foreground); }
.props-panel select { padding: 6px 10px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--background); }
.error { color: #ef4444; font-size: 13px; }
.status-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 8px; padding: 12px; margin-bottom: 16px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--muted); font-size: 13px; }
.viewer-container { border: 1px solid var(--border); border-radius: var(--radius); overflow: auto; margin-bottom: 24px; }
Expand Down
2 changes: 2 additions & 0 deletions docs/guide/docx.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import "@arcships/vue-docx/style.css"
| `model` | `DocModel` | 无 | 已解析文档模型 |
| `runtime` | `DocxRuntime` | 组件创建 | 调用方持有的 Runtime |
| `showToolbar` | `boolean` | `true` | 显示查看工具栏 |
| `showUpload` | `boolean` | `true` | 上传入口,含工具栏按钮与空态、错误态的选择文件按钮 |
| `showDownload` | `boolean` | `true` | 工具栏下载按钮 |
| `defaultThumbnailsOpen` | `boolean` | `false` | 默认展开缩略图 |
| `defaultZoom` | `number` | `100` | 初始缩放百分比 |
| `isDark` | `boolean` | `false` | 深色界面 |
Expand Down
10 changes: 8 additions & 2 deletions packages/vue-docx/src/components/DocxViewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
:file-name="displayFileName"
:is-dark="darkTheme"
:sidebar-open="sidebarOpen"
:show-upload="showUpload"
:show-download="showDownload"
:show-tracked-changes="effectiveShowTrackedChanges"
:show-comments="effectiveShowComments"
:tracked-change-count="viewerTrackedChanges.length"
Expand Down Expand Up @@ -87,7 +89,7 @@
<strong>Unable to open this document</strong>
<span v-if="errorCode(error)" class="docx-viewer-error-code">{{ errorCode(error) }}</span>
<span>{{ error.message }}</span>
<button type="button" @click="uploadInputRef?.click()">Choose another Word file</button>
<button v-if="showUpload" type="button" @click="uploadInputRef?.click()">Choose another Word file</button>
</div>
</div>

Expand All @@ -98,7 +100,7 @@
<div>
<strong>No document open</strong>
<span>{{ emptyState }}</span>
<button type="button" @click="uploadInputRef?.click()">Choose Word file</button>
<button v-if="showUpload" type="button" @click="uploadInputRef?.click()">Choose Word file</button>
</div>
</div>

Expand Down Expand Up @@ -141,6 +143,8 @@ const props = withDefaults(
runtime?: DocxRuntime
fileName?: string
showToolbar?: boolean
showUpload?: boolean
showDownload?: boolean
defaultThumbnailsOpen?: boolean
defaultZoom?: number
isDark?: boolean
Expand All @@ -153,6 +157,8 @@ const props = withDefaults(
emptyState: "Upload or provide a DOCX file to start.",
fileName: "document.docx",
showToolbar: true,
showUpload: true,
showDownload: true,
defaultThumbnailsOpen: false,
defaultZoom: 100,
isDark: false,
Expand Down
41 changes: 24 additions & 17 deletions packages/vue-docx/src/components/DocxViewerToolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
</svg>
</button>
<button
v-if="showUpload"
type="button"
class="docx-viewer-toolbar__icon-button"
data-testid="docx-upload"
Expand All @@ -157,6 +158,7 @@
</svg>
</button>
<button
v-if="showDownload"
type="button"
class="docx-viewer-toolbar__icon-button"
data-testid="docx-download"
Expand All @@ -176,23 +178,28 @@
<script setup lang="ts">
import { ref, watch } from "vue"

const props = defineProps<{
canDownload: boolean
disabled: boolean
fileName: string
isDark: boolean
sidebarOpen: boolean
showTrackedChanges: boolean
showComments: boolean
trackedChangeCount: number
commentCount: number
currentPage: number
totalPages: number
zoom: number
searchQuery: string
searchResultCount: number
searchResultIndex: number
}>()
const props = withDefaults(
defineProps<{
canDownload: boolean
disabled: boolean
fileName: string
isDark: boolean
sidebarOpen: boolean
showTrackedChanges: boolean
showComments: boolean
trackedChangeCount: number
commentCount: number
currentPage: number
totalPages: number
zoom: number
searchQuery: string
searchResultCount: number
searchResultIndex: number
showUpload?: boolean
showDownload?: boolean
}>(),
{ showUpload: true, showDownload: true },
)

const emit = defineEmits<{
download: []
Expand Down
39 changes: 39 additions & 0 deletions tests/component/vue-components.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,45 @@ test("DocxViewer exposes empty, loading, success and error states with public ev
failed.app.unmount();
});

test("DocxViewer hides upload and download entry points through showUpload and showDownload", async () => {
const shown = await mount(DocxViewer, { emptyState: "DOCX empty" });
assert.ok(findByTestId(shown.root, "docx-upload"), "upload button visible by default");
assert.ok(findByTestId(shown.root, "docx-download"), "download button visible by default");
assert.match(textContent(shown.root), /Choose Word file/);
assert.deepEqual(shown.warnings, []);
shown.app.unmount();

const hidden = await mount(DocxViewer, {
emptyState: "DOCX empty",
showUpload: false,
showDownload: false,
});
assert.ok(findByTestId(hidden.root, "docx-viewer-toolbar"), "toolbar still renders");
assert.equal(findByTestId(hidden.root, "docx-upload"), undefined, "upload button hidden");
assert.equal(findByTestId(hidden.root, "docx-download"), undefined, "download button hidden");
assert.doesNotMatch(textContent(hidden.root), /Choose Word file/);
assert.deepEqual(hidden.warnings, []);
hidden.app.unmount();

const failure = new Error("broken DOCX");
failure.code = "PARSE_FAILED";
const failed = await mount(DocxViewer, {
file: new Uint8Array([9]).buffer,
showUpload: false,
runtime: {
createLoader: () => ({ load: async () => { throw failure; }, cancel() {}, dispose() {} }),
},
});
await waitFor(
() => findByTestId(failed.root, "docx-viewer")?.props["data-state"] === "error",
"DOCX error state",
);
assert.doesNotMatch(textContent(failed.root), /Choose another Word file/);
assert.ok(findByTestId(failed.root, "docx-download"), "download button retained when only upload is hidden");
assert.deepEqual(failed.warnings, []);
failed.app.unmount();
});

test("DocxViewer displays model-derived tracked changes and comments without an editor controller", async () => {
const file = readFileSync(new URL("../../apps/demo/public/samples/review-comments.docx", import.meta.url));
const bytes = file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength);
Expand Down
Loading