|
@@ -1,452 +0,0 @@
|
|
|
-<template>
|
|
|
|
|
- <div class="web-preview-panel">
|
|
|
|
|
- <!-- 地址栏:面板里已经在浏览的真实地址(同源,可读 location),随时能复制或跳回新窗口 -->
|
|
|
|
|
- <div class="web-toolbar">
|
|
|
|
|
- <div class="web-address-row">
|
|
|
|
|
- <div class="web-url" :title="url">
|
|
|
|
|
- <span class="web-url-icon"></span>
|
|
|
|
|
- <span class="web-url-text">{{ displayUrl }}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="web-controls-row">
|
|
|
|
|
- <a-select
|
|
|
|
|
- v-model:value="sizeKey"
|
|
|
|
|
- class="web-size-select"
|
|
|
|
|
- size="small"
|
|
|
|
|
- :options="sizeOptions"
|
|
|
|
|
- :dropdown-match-select-width="false"
|
|
|
|
|
- dropdown-class-name="web-size-dropdown"
|
|
|
|
|
- />
|
|
|
|
|
- <div class="web-actions">
|
|
|
|
|
- <button type="button" class="web-action-btn" title="刷新" @click="reload">
|
|
|
|
|
- <span class="web-action-icon web-action-icon-refresh"></span>
|
|
|
|
|
- <span>刷新</span>
|
|
|
|
|
- </button>
|
|
|
|
|
- <button type="button" class="web-action-btn" title="在新窗口打开" @click="openInBrowser">
|
|
|
|
|
- <span class="web-action-icon web-action-icon-export"></span>
|
|
|
|
|
- <span>新窗口</span>
|
|
|
|
|
- </button>
|
|
|
|
|
- <button type="button" class="web-action-btn" title="复制链接" @click="copyUrl">
|
|
|
|
|
- <span class="web-action-icon web-action-icon-copy"></span>
|
|
|
|
|
- <span>复制链接</span>
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <!--
|
|
|
|
|
- 舞台:iframe 按选定分辨率(默认 1280×720)作为「真实视口」渲染,再整体等比缩放到面板里显示。
|
|
|
|
|
- 内网系统基本是按固定设计稿尺寸做的、不响应式,直接铺满窄面板会被挤变形,所以要这样缩。
|
|
|
|
|
- 只缩不放(放大会糊),四周留白居中;选「自适应面板」时退回铺满、不缩放。
|
|
|
|
|
- -->
|
|
|
|
|
- <div ref="stageRef" class="web-stage">
|
|
|
|
|
- <div v-if="loading" class="web-loading">
|
|
|
|
|
- <span class="web-loading-spinner"></span>
|
|
|
|
|
- <span>正在加载页面…</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="web-viewport" :class="{ 'is-loading': loading }" :style="viewportStyle">
|
|
|
|
|
- <iframe ref="frameRef" class="web-frame" :src="url" frameborder="0" @load="loading = false"></iframe>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-</template>
|
|
|
|
|
-
|
|
|
|
|
-<script setup lang="ts">
|
|
|
|
|
- import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
|
|
|
- import { message } from 'ant-design-vue';
|
|
|
|
|
- import { copyToClipboard } from '/@/views/ventAI/manageAssistent/lib/index';
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 右侧面板的网页预览:http(s) 地址直接 iframe 嵌入(判定见 lib/webLink.ts,当前不区分站内站外)。
|
|
|
|
|
- *
|
|
|
|
|
- * 不加 sandbox:地址多为内网系统,加了(尤其不带 allow-same-origin 时)会让它变成不透明来源,
|
|
|
|
|
- * 登录态、下载、弹窗等能力都受影响。要收紧的话给 iframe 加
|
|
|
|
|
- * sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-downloads" 即可。
|
|
|
|
|
- */
|
|
|
|
|
- const props = defineProps<{ url: string }>();
|
|
|
|
|
-
|
|
|
|
|
- const frameRef = ref<HTMLIFrameElement>();
|
|
|
|
|
- const stageRef = ref<HTMLElement>();
|
|
|
|
|
- const loading = ref(true);
|
|
|
|
|
-
|
|
|
|
|
- // 预设视口尺寸:内网系统按设计稿分辨率渲染才不会变形
|
|
|
|
|
- const SIZE_PRESETS = [
|
|
|
|
|
- { value: '1280x720', label: '1280 × 720', width: 1280, height: 720 },
|
|
|
|
|
- { value: '1440x900', label: '1440 × 900', width: 1440, height: 900 },
|
|
|
|
|
- { value: '1920x1080', label: '1920 × 1080', width: 1920, height: 1080 },
|
|
|
|
|
- { value: 'fit', label: '自适应面板', width: 0, height: 0 },
|
|
|
|
|
- ];
|
|
|
|
|
- const sizeOptions = SIZE_PRESETS.map((p) => ({ value: p.value, label: p.label }));
|
|
|
|
|
- const sizeKey = ref('1280x720');
|
|
|
|
|
- const preset = computed(() => SIZE_PRESETS.find((p) => p.value === sizeKey.value) || SIZE_PRESETS[0]);
|
|
|
|
|
-
|
|
|
|
|
- // 舞台可用尺寸:缩放比要跟着面板大小变(全屏/收起面板时都要重算)
|
|
|
|
|
- const stageSize = ref({ width: 0, height: 0 });
|
|
|
|
|
- const scale = computed(() => {
|
|
|
|
|
- const p = preset.value;
|
|
|
|
|
- const { width, height } = stageSize.value;
|
|
|
|
|
- if (!p.width || !p.height || !width || !height) return 1;
|
|
|
|
|
- return Math.min(1, width / p.width, height / p.height);
|
|
|
|
|
- });
|
|
|
|
|
- const viewportStyle = computed(() => {
|
|
|
|
|
- const p = preset.value;
|
|
|
|
|
- if (!p.width || !p.height) return { top: '0', left: '0', width: '100%', height: '100%', transform: 'none' };
|
|
|
|
|
- return {
|
|
|
|
|
- top: '50%',
|
|
|
|
|
- left: '50%',
|
|
|
|
|
- width: `${p.width}px`,
|
|
|
|
|
- height: `${p.height}px`,
|
|
|
|
|
- transform: `translate(-50%, -50%) scale(${scale.value})`,
|
|
|
|
|
- };
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- let resizeObserver: ResizeObserver | null = null;
|
|
|
|
|
- onMounted(() => {
|
|
|
|
|
- const el = stageRef.value;
|
|
|
|
|
- if (!el) return;
|
|
|
|
|
- const measure = () => {
|
|
|
|
|
- stageSize.value = { width: el.clientWidth, height: el.clientHeight };
|
|
|
|
|
- };
|
|
|
|
|
- resizeObserver = new ResizeObserver(measure);
|
|
|
|
|
- resizeObserver.observe(el);
|
|
|
|
|
- measure();
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- onBeforeUnmount(() => {
|
|
|
|
|
- resizeObserver?.disconnect();
|
|
|
|
|
- resizeObserver = null;
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // 地址栏文案:同源页面显示路径,外部页面显示域名 + 路径;完整地址放在 title 上。
|
|
|
|
|
- // 之前只取 pathname,外部站点根路径会显示成孤零零的 "/"。
|
|
|
|
|
- const displayUrl = computed(() => {
|
|
|
|
|
- try {
|
|
|
|
|
- const u = new URL(props.url, window.location.href);
|
|
|
|
|
- const path = `${u.pathname}${u.search}${u.hash}`;
|
|
|
|
|
- if (u.origin === window.location.origin) return path || '/';
|
|
|
|
|
- return `${u.host}${path === '/' ? '' : path}`;
|
|
|
|
|
- } catch {
|
|
|
|
|
- return props.url;
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // 换地址(同一标签页复用实例时)重新进加载态
|
|
|
|
|
- watch(
|
|
|
|
|
- () => props.url,
|
|
|
|
|
- () => {
|
|
|
|
|
- loading.value = true;
|
|
|
|
|
- }
|
|
|
|
|
- );
|
|
|
|
|
-
|
|
|
|
|
- /** iframe 里当前真实地址:可读时用它;跳到读不到的页面(跨域)时退回标签原始地址 */
|
|
|
|
|
- const currentHref = (): string => {
|
|
|
|
|
- try {
|
|
|
|
|
- return frameRef.value?.contentWindow?.location.href || props.url;
|
|
|
|
|
- } catch {
|
|
|
|
|
- return props.url;
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const reload = () => {
|
|
|
|
|
- const win = frameRef.value?.contentWindow;
|
|
|
|
|
- try {
|
|
|
|
|
- // 刷新 iframe 内的当前页,而不是回到打开时的初始地址
|
|
|
|
|
- loading.value = true;
|
|
|
|
|
- win?.location.reload();
|
|
|
|
|
- } catch {
|
|
|
|
|
- // 跨域读不到时退回重新加载 src
|
|
|
|
|
- if (frameRef.value) frameRef.value.src = props.url;
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const openInBrowser = () => {
|
|
|
|
|
- window.open(currentHref(), '_blank', 'noopener,noreferrer');
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const copyUrl = () => {
|
|
|
|
|
- copyToClipboard(currentHref()).then(() => message.success('链接已复制'));
|
|
|
|
|
- };
|
|
|
|
|
-</script>
|
|
|
|
|
-
|
|
|
|
|
-<style scoped lang="less">
|
|
|
|
|
- .web-preview-panel {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- height: 100%;
|
|
|
|
|
- min-height: 0;
|
|
|
|
|
- background: linear-gradient(180deg, rgb(8 37 58 / 98%), rgb(4 22 36 / 98%));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .web-toolbar {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- gap: 8px;
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
- padding: 10px 12px;
|
|
|
|
|
- border-bottom: 1px solid rgb(63 80 106 / 35%);
|
|
|
|
|
- background: rgb(2 18 30 / 35%);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .web-address-row {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 8px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .web-url {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 8px;
|
|
|
|
|
- min-width: 0;
|
|
|
|
|
- height: 30px;
|
|
|
|
|
- padding: 0 10px;
|
|
|
|
|
- border: 1px solid rgb(63 80 106 / 50%);
|
|
|
|
|
- border-radius: 6px;
|
|
|
|
|
- background: rgb(1 16 27 / 72%);
|
|
|
|
|
- color: #c9d1d9;
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- transition:
|
|
|
|
|
- border-color 0.18s,
|
|
|
|
|
- background 0.18s;
|
|
|
|
|
-
|
|
|
|
|
- &:hover {
|
|
|
|
|
- border-color: rgb(10 132 255 / 40%);
|
|
|
|
|
- background: rgb(1 20 34 / 90%);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .web-url-icon {
|
|
|
|
|
- width: 14px;
|
|
|
|
|
- height: 14px;
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
- background-image: var(--img-chat-link-icon);
|
|
|
|
|
- background-repeat: no-repeat;
|
|
|
|
|
- background-size: 100% 100%;
|
|
|
|
|
- opacity: 0.8;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .web-url-text {
|
|
|
|
|
- min-width: 0;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
- text-overflow: ellipsis;
|
|
|
|
|
- white-space: nowrap;
|
|
|
|
|
- letter-spacing: 0.2px;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .web-controls-row {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 8px;
|
|
|
|
|
- flex-wrap: wrap;
|
|
|
|
|
- min-width: 0;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .web-size-select {
|
|
|
|
|
- width: 118px;
|
|
|
|
|
- height: 28px;
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
-
|
|
|
|
|
- :deep(.zxm-select-selector) {
|
|
|
|
|
- height: 28px !important;
|
|
|
|
|
- padding: 0 9px !important;
|
|
|
|
|
- border-color: rgb(63 80 106 / 50%) !important;
|
|
|
|
|
- border-radius: 6px !important;
|
|
|
|
|
- background: rgb(1 16 27 / 72%) !important;
|
|
|
|
|
- box-shadow: none !important;
|
|
|
|
|
- transition:
|
|
|
|
|
- border-color 0.18s,
|
|
|
|
|
- background 0.18s;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- :deep(.zxm-select-selection-item) {
|
|
|
|
|
- color: #c9d1d9 !important;
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- line-height: 26px !important;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- :deep(.zxm-select-arrow) {
|
|
|
|
|
- color: #8b949e !important;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &:hover :deep(.zxm-select-selector) {
|
|
|
|
|
- border-color: rgb(10 132 255 / 40%) !important;
|
|
|
|
|
- background: rgb(1 20 34 / 90%) !important;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &.zxm-select-focused :deep(.zxm-select-selector) {
|
|
|
|
|
- border-color: #5cb8ff !important;
|
|
|
|
|
- box-shadow: 0 0 0 2px rgb(10 132 255 / 15%) !important;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .web-actions {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 4px;
|
|
|
|
|
- margin-left: auto;
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .web-action-btn {
|
|
|
|
|
- display: inline-flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 4px;
|
|
|
|
|
- height: 28px;
|
|
|
|
|
- padding: 0 9px;
|
|
|
|
|
- border: 1px solid transparent;
|
|
|
|
|
- border-radius: 6px;
|
|
|
|
|
- background: transparent;
|
|
|
|
|
- color: #8b949e;
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- line-height: 1;
|
|
|
|
|
- white-space: nowrap;
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- transition:
|
|
|
|
|
- color 0.18s,
|
|
|
|
|
- background 0.18s,
|
|
|
|
|
- border-color 0.18s;
|
|
|
|
|
-
|
|
|
|
|
- .web-action-icon {
|
|
|
|
|
- width: 14px;
|
|
|
|
|
- height: 14px;
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
- background-repeat: no-repeat;
|
|
|
|
|
- background-size: 100% 100%;
|
|
|
|
|
- opacity: 0.7;
|
|
|
|
|
- transition: opacity 0.18s;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .web-action-icon-refresh {
|
|
|
|
|
- background-image: var(--img-chat-regenerate-icon);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .web-action-icon-export {
|
|
|
|
|
- background-image: var(--img-chat-export-icon);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .web-action-icon-copy {
|
|
|
|
|
- background-image: var(--img-chat-copy-icon);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &:hover {
|
|
|
|
|
- border-color: rgb(10 132 255 / 22%);
|
|
|
|
|
- background: rgb(10 132 255 / 12%);
|
|
|
|
|
- color: #e0e6ed;
|
|
|
|
|
-
|
|
|
|
|
- .web-action-icon {
|
|
|
|
|
- opacity: 1;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &:active {
|
|
|
|
|
- background: rgb(10 132 255 / 20%);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &:focus-visible {
|
|
|
|
|
- outline: 1px solid #5cb8ff;
|
|
|
|
|
- outline-offset: 1px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /* 鼠标点击后不保留持续高亮,只有键盘聚焦才显示描边 */
|
|
|
|
|
- &:focus:not(:focus-visible) {
|
|
|
|
|
- outline: none;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .web-stage {
|
|
|
|
|
- position: relative;
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- min-height: 0;
|
|
|
|
|
- margin: 10px 12px 12px;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
- border: 1px solid rgb(63 80 106 / 35%);
|
|
|
|
|
- border-radius: 8px;
|
|
|
|
|
- // 缩放后四周的留白底色,跟面板区分开,看起来像「一块屏幕」
|
|
|
|
|
- background: radial-gradient(ellipse at 50% 0%, rgb(10 132 255 / 8%), transparent 60%), #03111d;
|
|
|
|
|
- box-shadow:
|
|
|
|
|
- inset 0 0 0 1px rgb(255 255 255 / 2%),
|
|
|
|
|
- 0 8px 24px rgb(0 0 0 / 25%);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .web-viewport {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- transform-origin: center center;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
- border-radius: 4px;
|
|
|
|
|
- background: #fff;
|
|
|
|
|
- box-shadow: 0 8px 28px rgb(0 0 0 / 42%);
|
|
|
|
|
-
|
|
|
|
|
- &.is-loading {
|
|
|
|
|
- visibility: hidden;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .web-frame {
|
|
|
|
|
- display: block;
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- height: 100%;
|
|
|
|
|
- border: none;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .web-loading {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- inset: 0;
|
|
|
|
|
- z-index: 1;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- gap: 8px;
|
|
|
|
|
- background: rgb(3 17 29 / 40%);
|
|
|
|
|
- color: #8b949e;
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
-
|
|
|
|
|
- .web-loading-spinner {
|
|
|
|
|
- width: 14px;
|
|
|
|
|
- height: 14px;
|
|
|
|
|
- border: 2px solid rgb(92 184 255 / 20%);
|
|
|
|
|
- border-top-color: #5cb8ff;
|
|
|
|
|
- border-radius: 50%;
|
|
|
|
|
- animation: web-loading-spin 0.8s linear infinite;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- @keyframes web-loading-spin {
|
|
|
|
|
- to {
|
|
|
|
|
- transform: rotate(360deg);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-</style>
|
|
|
|
|
-
|
|
|
|
|
-<style lang="less">
|
|
|
|
|
- /*
|
|
|
|
|
- * 下拉弹层挂在 body 下,scoped 样式够不到,只能写在这里;与面板深色风格保持一致。
|
|
|
|
|
- */
|
|
|
|
|
- .web-size-dropdown.zxm-select-dropdown {
|
|
|
|
|
- padding: 4px !important;
|
|
|
|
|
- border: 1px solid #1a4a6f !important;
|
|
|
|
|
- border-radius: 6px !important;
|
|
|
|
|
- background: #0a2a3f !important;
|
|
|
|
|
- box-shadow: 0 6px 20px rgb(0 0 0 / 42%) !important;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .web-size-dropdown {
|
|
|
|
|
- .zxm-select-item {
|
|
|
|
|
- min-height: 28px;
|
|
|
|
|
- padding: 4px 9px;
|
|
|
|
|
- border-radius: 4px;
|
|
|
|
|
- color: #c9d1d9 !important;
|
|
|
|
|
- background: transparent !important;
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .zxm-select-item-option-active:not(.zxm-select-item-option-disabled) {
|
|
|
|
|
- background: rgb(10 132 255 / 12%) !important;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .zxm-select-item-option-selected:not(.zxm-select-item-option-disabled) {
|
|
|
|
|
- background: rgb(10 132 255 / 18%) !important;
|
|
|
|
|
- color: #e0e6ed !important;
|
|
|
|
|
- font-weight: 400;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-</style>
|
|
|