feat:新增svg组件,新增地图工具组件

This commit is contained in:
xudan 2025-09-03 11:42:01 +08:00
parent ae0fc5a3cd
commit 2d2fe4329c
5 changed files with 271 additions and 11 deletions

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1756867535732" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4381" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M926.72 829.44q28.672 32.768 31.232 57.344t-18.944 48.128q-24.576 27.648-54.272 26.112t-57.344-24.064l-164.864-158.72q-46.08 30.72-99.84 47.616t-113.152 16.896q-80.896 0-151.552-30.72t-123.392-83.456-82.944-123.392-30.208-151.552q0-79.872 30.208-150.528t82.944-123.392 123.392-83.456 151.552-30.72 151.552 30.72 123.392 83.456 83.456 123.392 30.72 150.528q0 61.44-17.92 116.736t-49.664 101.376q13.312 14.336 37.376 38.4t48.128 48.64 44.544 44.032zM449.536 705.536q53.248 0 100.352-19.968t81.92-54.784 54.784-81.92 19.968-100.352-19.968-100.352-54.784-81.92-81.92-54.784-100.352-19.968-99.84 19.968-81.408 54.784-55.296 81.92-20.48 100.352 20.48 100.352 55.296 81.92 81.408 54.784 99.84 19.968zM512 384l128 0 0 128-128 0 0 128-129.024 0 0-128-126.976 0 0-128 126.976 0 0-128 129.024 0 0 128z" p-id="4382"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1756866391252" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1471" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M995.939462 314.569016L703.259415 21.888968c-28.631744-28.631744-44.538268-28.631744-69.988707-3.181305C426.485892 222.311174 222.88238 425.914686 19.278869 632.699502c-22.269134 22.269134-25.450439 44.538268 0 69.988707 101.801756 98.620451 200.422206 200.422206 302.223962 302.223962 9.543915 9.543915 19.087829 15.906524 31.813049 15.906524 19.087829 0 28.631744-12.725219 41.356963-22.269134l604.447924-604.447924c25.450439-34.994353 25.450439-47.719573-3.181305-79.532621zM890.956402 387.739027L385.128928 893.566501c-25.450439 25.450439-44.538268 25.450439-69.988707 0l-190.878291-190.878292c-19.087829-19.087829-28.631744-38.175658-9.543915-60.444793 15.906524-22.269134 28.631744-31.813049 50.900878-6.362609 28.631744 31.813049 60.444792 60.444792 89.076536 92.257841 12.725219 12.725219 19.087829 15.906524 34.994354 0 34.994353-34.994353 34.994353-34.994353 0-69.988707L210.157161 578.617319c-12.725219-12.725219-15.906524-19.087829 0-34.994353 38.175658-38.175658 34.994353-38.175658 73.170012 0 12.725219 12.725219 28.631744 25.450439 38.175658 38.175658 22.269134 28.631744 38.175658 28.631744 60.444793 0 15.906524-19.087829 22.269134-28.631744 0-44.538268-15.906524-12.725219-31.813049-31.813049-47.719573-44.538268-38.175658-38.175658-38.175658-38.175658 3.181304-76.351317 12.725219-9.543915 15.906524-12.725219 28.631744 0 28.631744 31.813049 60.444792 60.444792 89.076536 92.257841 19.087829 19.087829 31.813049 28.631744 54.082183 3.181305 19.087829-22.269134 31.813049-31.813049 3.181305-57.263488-31.813049-25.450439-57.263488-57.263488-89.076536-85.895231-15.906524-15.906524-12.725219-22.269134 0-34.994353 34.994353-31.813049 31.813049-34.994353 66.807402 0l57.263487 57.263487c9.543915 12.725219 19.087829 9.543915 28.631744 0 41.356963-34.994353 41.356963-34.994353 0-73.170012-15.906524-15.906524-31.813049-34.994353-50.900878-50.900877-25.450439-22.269134-3.181305-31.813049 9.543915-41.356964 12.725219-9.543915 19.087829-38.175658 44.538268-12.725219l95.439146 95.439146c25.450439 31.813049 34.994353 0 50.900878-9.543915 19.087829-12.725219 25.450439-25.450439 3.181305-44.538268-31.813049-25.450439-57.263488-54.082183-85.895232-85.895231-9.543915-9.543915-28.631744-15.906524-19.087829-28.631744 9.543915-15.906524 22.269134-31.813049 44.538268-31.813049 15.906524 0 25.450439 6.36261 34.994354 15.906525l190.878292 190.878292c25.450439 28.631744 25.450439 47.719573-3.181305 73.170011z" fill="#666666" p-id="1472"></path></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1756867549852" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5380" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M927.744 829.44q28.672 32.768 31.232 57.344t-18.944 48.128q-24.576 27.648-54.272 26.112t-57.344-24.064l-164.864-157.696q-46.08 29.696-99.84 46.592t-113.152 16.896q-80.896 0-151.552-30.72t-123.392-83.456-82.944-123.392-30.208-151.552q0-79.872 30.208-150.528t82.944-123.392 123.392-83.456 151.552-30.72 151.552 30.72 123.392 83.456 83.456 123.392 30.72 150.528q0 61.44-17.92 116.736t-49.664 102.4l36.864 37.888q24.576 23.552 48.64 48.128t43.52 44.032zM450.56 705.536q53.248 0 100.352-19.968t81.92-54.784 54.784-81.92 19.968-100.352-19.968-100.352-54.784-81.92-81.92-54.784-100.352-19.968-99.84 19.968-81.408 54.784-55.296 81.92-20.48 100.352 20.48 100.352 55.296 81.92 81.408 54.784 99.84 19.968zM256 384l385.024 0 0 128-385.024 0 0-128z" p-id="5381"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,4 +1,5 @@
<script setup lang="ts">
import SvgIcon from '@common/svg-icon.vue';
import type { EditorService } from '@core/editor.service';
import { useToolbar } from '@core/useToolbar';
import { computed, inject, type InjectionKey, onBeforeUnmount, onMounted, ref, type ShallowRef } from 'vue';
@ -146,16 +147,35 @@ onBeforeUnmount(() => {
<template>
<div class="map-toolbar">
<a-space size="small">
<a-button size="small" @click="zoomIn">放大</a-button>
<a-button size="small" @click="zoomOut">缩小</a-button>
<a-button size="small" @click="fitView">适配视图</a-button>
<a-button size="small" type="primary" ghost @click="toggleMeasure">
{{ measuring ? '退出测量' : '测量尺' }}
<a-button class="icon-btn tool-btn" size="small" :title="'放大'" @click="zoomIn">
<SvgIcon name="enlarge" :size="18" :forceColor="true" />
</a-button>
<a-button size="small" @click="toggleRule">标尺</a-button>
<a-button size="small" @click="toggleGrid">网格</a-button>
<a-button size="small" @click="exportImage">截图</a-button>
<a-button size="small" @click="toggleFullscreen">{{ isFullscreen ? '退出全屏' : '全屏' }}</a-button>
<a-button class="icon-btn tool-btn" size="small" :title="'缩小'" @click="zoomOut">
<SvgIcon name="shrink" :size="18" :forceColor="true" />
</a-button>
<!-- <a-button class="icon-btn tool-btn" size="small" :title="'适配视图'" @click="fitView"> 适配视图 </a-button> -->
<a-button
class="icon-btn tool-btn"
size="small"
type="primary"
:ghost="!measuring"
:class="{ measuring }"
@click="toggleMeasure"
:title="measuring ? '退出测量' : '测量尺'"
>
<SvgIcon name="rule" :size="16" :forceColor="true" :active="measuring" />
</a-button>
<a-button class="icon-btn tool-btn" size="small" :title="'标尺'" @click="toggleRule">标尺</a-button>
<a-button class="icon-btn tool-btn" size="small" :title="'网格'" @click="toggleGrid">网格</a-button>
<!-- <a-button class="icon-btn tool-btn" size="small" :title="'截图'" @click="exportImage">截图</a-button>
<a-button
class="icon-btn tool-btn"
size="small"
:title="isFullscreen ? '退出全屏' : '全屏'"
@click="toggleFullscreen"
>
{{ isFullscreen ? '退出全屏' : '全屏' }}
</a-button> -->
</a-space>
</div>
@ -201,8 +221,8 @@ onBeforeUnmount(() => {
<style scoped lang="scss">
.map-toolbar {
position: fixed;
right: 16px;
bottom: 16px;
right: 24px;
bottom: 24px;
z-index: 101;
background: rgba(0, 0, 0, 0.45);
padding: 8px 10px;
@ -212,6 +232,53 @@ onBeforeUnmount(() => {
:deep(.ant-btn) {
padding: 0 10px;
}
.icon-btn {
padding: 8px;
margin: 0 4px;
// width: 32px;
// height: 32px;
display: flex;
align-items: center;
justify-content: center;
background-color: get-color(primary);
}
/* 提升特异性,覆盖全局 .ant-btn.icon-btn.tool-btn:hover 的 !important 背景色 */
:deep(.ant-btn.icon-btn.tool-btn:hover) {
background-color: transparent !important;
color: #0dbb8a !important;
box-shadow: none !important;
}
.ant-btn.icon-btn.tool-btn:not(:disabled):hover {
background-color: transparent !important;
color: #0dbb8a !important;
box-shadow: none !important;
}
}
/* 测量按钮激活状态(背景红色) */
.map-toolbar {
.ant-btn.icon-btn.tool-btn {
width: 32px;
height: 32px;
&:focus-visible {
border: none !important;
outline: none !important;
}
}
/* 常规与悬停覆盖(需高特异性与 !important 以压过上面的 hover 规则) */
.ant-btn.icon-btn.tool-btn.measuring {
background-color: #0dbb8a;
width: 32px;
height: 32px;
border-color: #0dbb8a;
}
// :deep(.ant-btn.icon-btn.tool-btn.measuring:hover) {
// background-color: #ff4d4f !important;
// border-color: #ff4d4f !important;
// color: #fff !important;
// box-shadow: none !important;
// }
}
/* 网格背景(占位实现),通过 toggleGrid 开关 */

190
src/components/svg-icon.vue Normal file
View File

@ -0,0 +1,190 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue';
/**
* 通用 SVG 图标组件
* 功能
* - 通过 name 自动在 src/assets/icons/** 中匹配import.meta.glob 原始内容未命中则回退到 public/icons/{name}.svg
* - 支持颜色active/disabled 状态size/width/height自定义 class可选旋转
* - 默认通过 CSS currentColor 上色 forceColor=true 强制替换 svg 内的 fill/stroke
*/
interface Props {
name: string; // "lock"
size?: number | string; // width/height
width?: number | string;
height?: number | string;
color?: string; //
active?: boolean;
disabled?: boolean;
activeColor?: string;
disabledColor?: string;
title?: string;
spin?: boolean; //
forceColor?: boolean; // fill/stroke currentColor
ariaLabel?: string;
}
const props = defineProps<Props>();
const emit = defineEmits<{ (e: 'load', ok: boolean): void }>();
// 1) src svg
// as: 'raw' Vite Vite 2.7+
const rawSvgs = import.meta.glob('/src/assets/icons/**/*.svg', { as: 'raw', eager: true }) as Record<string, string>;
const svgHtml = ref<string>('');
const loading = ref<boolean>(false);
const error = ref<string | null>(null);
const px = (v?: number | string) => (v == null ? undefined : typeof v === 'number' ? `${v}px` : v);
const resolvedColor = computed(() => {
if (props.disabled) return props.disabledColor ?? 'var(--icon-disabled-color, rgba(0,0,0,0.25))';
if (props.active) return props.activeColor ?? props.color ?? 'currentColor';
return props.color ?? 'currentColor';
});
const styleVars = computed(() => {
const w = px(props.width ?? props.size);
const h = px(props.height ?? props.size);
return {
'--svg-icon-width': w ?? '1em',
'--svg-icon-height': h ?? '1em',
'--svg-icon-color': resolvedColor.value,
} as Record<string, string>;
});
const classes = computed(() => [
'svg-icon',
props.active && 'is-active',
props.disabled && 'is-disabled',
props.spin && 'is-spin',
]);
function pickFromSrcByName(name: string): string | null {
// name xxx/name.svg name.svg
const entries = Object.entries(rawSvgs);
const found = entries.find(([path]) => path.endsWith(`/${name}.svg`));
return found ? found[1] : null;
}
function applyColor(html: string): string {
if (!props.forceColor) return html;
try {
// fill/stroke currentColor none
return html
.replace(/fill\s*=\s*"(?!none)[^"]*"/gi, 'fill="currentColor"')
.replace(/stroke\s*=\s*"(?!none)[^"]*"/gi, 'stroke="currentColor"');
} catch (e: unknown) {
console.error(e);
return html;
}
}
async function loadSvg(name: string) {
loading.value = true;
error.value = null;
try {
let html = pickFromSrcByName(name);
if (!html) {
// 退 public/icons
const url = `/icons/${name}.svg`;
const res = await fetch(url, { cache: 'no-cache' });
if (!res.ok) throw new Error(`fetch ${url} ${res.status}`);
html = await res.text();
}
svgHtml.value = applyColor(html);
emit('load', true);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
error.value = msg;
svgHtml.value = '';
emit('load', false);
} finally {
loading.value = false;
}
}
watch(
() => props.name,
(n) => {
if (n) loadSvg(n);
},
{ immediate: true },
);
onMounted(() => {
if (props.name) loadSvg(props.name);
});
</script>
<template>
<span
:class="classes"
:style="styleVars as any"
role="img"
:aria-label="ariaLabel || title || name"
:title="title || undefined"
>
<!-- 内联 svg方便用 currentColor 控制颜色 -->
<span v-if="svgHtml && !loading && !error" class="svg-body" v-html="svgHtml" />
<span v-else-if="loading" class="svg-loading" />
<span v-else-if="error" class="svg-error" />
</span>
</template>
<style scoped lang="scss">
.svg-icon {
display: inline-flex;
width: var(--svg-icon-width, 1em);
height: var(--svg-icon-height, 1em);
color: var(--svg-icon-color, currentColor);
line-height: 0;
align-items: center;
justify-content: center;
vertical-align: -0.125em;
}
/* 让内部 svg 默认使用 currentColor若原文件未写死 fill这样可生效 */
.svg-icon :deep(svg) {
width: 100%;
height: 100%;
fill: currentColor;
stroke: currentColor;
}
.svg-icon.is-disabled {
opacity: 0.5;
filter: grayscale(0.3);
pointer-events: none;
}
.svg-icon.is-active {
/* 可按需覆盖 */
color: get-color(primary);
}
.svg-icon.is-spin {
animation: svg-spin 1s linear infinite;
}
@keyframes svg-spin {
100% {
transform: rotate(360deg);
}
}
.svg-loading,
.svg-error {
width: 100%;
height: 100%;
background: repeating-linear-gradient(
45deg,
rgba(0, 0, 0, 0.06),
rgba(0, 0, 0, 0.06) 6px,
rgba(0, 0, 0, 0.12) 6px,
rgba(0, 0, 0, 0.12) 12px
);
}
</style>