feat:新增svg组件,新增地图工具组件
This commit is contained in:
parent
ae0fc5a3cd
commit
2d2fe4329c
1
src/assets/icons/svg/enlarge.svg
Normal file
1
src/assets/icons/svg/enlarge.svg
Normal 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 |
1
src/assets/icons/svg/rule.svg
Normal file
1
src/assets/icons/svg/rule.svg
Normal 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 |
1
src/assets/icons/svg/shrink.svg
Normal file
1
src/assets/icons/svg/shrink.svg
Normal 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 |
@ -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
190
src/components/svg-icon.vue
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user