| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295 |
- <template>
- <div :style="boxStyle" class="w-full h-full box-border overflow-hidden relative">
- <svg
- class="absolute left-0 top-0 w-full h-full"
- :viewBox="`0 0 ${width} ${height}`"
- xmlns="http://www.w3.org/2000/svg"
- >
- <!-- 元素渲染 -->
- <template v-for="(el, index) in elements || []" :key="index">
- <!-- 矩形 -->
- <rect
- v-if="el.type === 'rect'"
- :x="el.props.x"
- :y="el.props.y"
- :width="el.props.width"
- :height="el.props.height"
- :fill="el.props.background_color"
- :stroke="el.props.border?.color"
- :stroke-width="el.props.border?.width"
- :rx="el.props.border?.radius"
- :ry="el.props.border?.radius"
- />
- <!-- 文本 -->
- <text
- v-else-if="el.type === 'text'"
- :x="el.props.x"
- :y="el.props.y"
- :fill="el.props.font_color"
- :font-size="el.props.font_size"
- :text-decoration="textDecorMap[el.props.text_decor] || 'none'"
- :style="getCanvasTextStyle(el.props)"
- dominant-baseline="hanging"
- >
- {{ getCanvasText(el.props.text) }}
- </text>
- <!-- 圆弧 -->
- <path
- v-else-if="el.type === 'arc'"
- :d="getArcPath(el.props)"
- :stroke="el.props.color"
- :stroke-width="el.props.width"
- fill="none"
- stroke-linecap="round"
- />
- <!-- 直线(折线) -->
- <polyline
- v-else-if="el.type === 'line'"
- v-show="(el.props.points || []).length > 1"
- :points="getLinePoints(el.props.points)"
- :stroke="el.props.color"
- :stroke-width="el.props.width"
- :stroke-linecap="el.props.round ? 'round' : 'butt'"
- fill="none"
- />
- <!-- 三角形 -->
- <polygon
- v-else-if="el.type === 'triangle'"
- v-show="(el.props.points || []).length >= 3"
- :points="getLinePoints((el.props.points || []).slice(0, 3))"
- :fill="el.props.background_color"
- />
- </template>
- </svg>
- <!-- 图片使用 img 绝对定位到画布上,方便加载本地资源 -->
- <template v-for="(el, index) in imageElements" :key="`img-${index}`">
- <img
- v-if="el.src"
- class="absolute"
- :style="{
- left: `${el.props.x}px`,
- top: `${el.props.y}px`,
- width: `${el.props.width}px`,
- height: `${el.props.height}px`,
- opacity: el.props.alpha / 255
- }"
- :src="el.src"
- />
- </template>
- </div>
- </template>
- <script setup lang="ts">
- import { computed } from 'vue'
- import { useProjectStore } from '@/store/modules/project'
- import { useLanguage } from '../hooks/useLanguage'
- type Point = { x: number; y: number }
- type RectProps = {
- x: number
- y: number
- width: number
- height: number
- background_color: string
- border: {
- color: string
- width: number
- radius: number
- }
- }
- type TextProps = {
- x: number
- y: number
- width: number
- font_color: string
- font_size: number
- font_family: string
- text_decor: string
- text: string
- }
- type ImageProps = {
- x: number
- y: number
- width: number
- height: number
- image: string
- alpha: number
- recolor: string
- }
- type ArcProps = {
- x: number
- y: number
- start_angle: number
- end_angle: number
- color: string
- width: number
- radius: number
- }
- type LineProps = {
- color: string
- width: number
- round: boolean
- points: Point[]
- }
- type TriangleProps = {
- background_color: string
- points: Point[]
- }
- type CanvasElement =
- | { type: 'rect'; props: RectProps }
- | { type: 'text'; props: TextProps }
- | { type: 'image'; props: ImageProps }
- | { type: 'arc'; props: ArcProps }
- | { type: 'line'; props: LineProps }
- | { type: 'triangle'; props: TriangleProps }
- const props = defineProps<{
- width: number
- height: number
- styles: any
- state?: string
- part?: string
- background_color: string
- elements: CanvasElement[]
- }>()
- const projectStore = useProjectStore()
- const { resolveText, getResolvedFontStyle } = useLanguage()
- const boxStyle = computed(() => {
- return {
- width: `${props.width}px`,
- height: `${props.height}px`,
- backgroundColor: props.background_color
- }
- })
- // 文本装饰映射
- const textDecorMap: Record<string, string> = {
- LV_TEXT_DECOR_NONE: 'none',
- LV_TEXT_DECOR_UNDERLINE: 'underline',
- LV_TEXT_DECOR_STRIKETHROUGH: 'line-through',
- 'LV_TEXT_DECOR_UNDERLINE | LV_TEXT_DECOR_STRIKETHROUGH': 'underline line-through'
- }
- // 计算图片元素及其本地路径
- const imageElements = computed(() => {
- return (props.elements || [])
- .filter((el): el is { type: 'image'; props: ImageProps } => el.type === 'image')
- .map((el) => {
- let src = ''
- const id = el.props.image
- const project = projectStore.project
- if (id && project) {
- const imgRes = project.resources.images.find((img) => img.id === id)
- if (imgRes) {
- src = `local:///${(projectStore.projectPath + imgRes.path).replaceAll('\\', '/')}`
- }
- }
- return { ...el, src }
- })
- })
- const getCanvasText = (text: string) => resolveText(text).text
- const getCanvasTextStyle = (textProps: TextProps) => {
- const resolvedStyle = getResolvedFontStyle(textProps.text)
- if (!resolvedStyle.fontSize && textProps.font_size) {
- resolvedStyle.fontSize = `${textProps.font_size}px`
- }
- if (!resolvedStyle.fontFamily && textProps.font_family && textProps.font_family !== 'xx') {
- const font = projectStore.project?.resources.fonts.find(
- (item) => item.id === textProps.font_family
- )
- if (font?.fileName) {
- resolvedStyle.fontFamily = `'${font.fileName}'`
- }
- }
- return resolvedStyle
- }
- // 直线/多边形 points 字符串
- const getLinePoints = (points: Point[] = []) => {
- return points.map((p) => `${p.x},${p.y}`).join(' ')
- }
- // 极坐标转直角坐标
- const polarToCartesian = (
- centerX: number,
- centerY: number,
- radius: number,
- angleInDegrees: number
- ) => {
- const radians = (angleInDegrees * Math.PI) / 180.0
- return {
- x: centerX + radius * Math.cos(radians),
- y: centerY + radius * Math.sin(radians)
- }
- }
- // 生成圆弧路径(始终顺时针)
- const describeArc = (
- x: number,
- y: number,
- radius: number,
- startAngle: number,
- endAngle: number
- ) => {
- // 归一化角度到 [0, 360)
- const s = ((startAngle % 360) + 360) % 360
- const e = ((endAngle % 360) + 360) % 360
- let clockwiseSpan = (e - s + 360) % 360
- // 0 或 360 视作整圆,这里用 359.999° 避免 SVG 报错
- if (clockwiseSpan === 0) clockwiseSpan = 359.999
- const start = polarToCartesian(x, y, radius, s)
- const end = polarToCartesian(x, y, radius, s + clockwiseSpan)
- const largeArcFlag = clockwiseSpan > 180 ? '1' : '0'
- const sweepFlag = '1'
- return [
- 'M',
- start.x,
- start.y,
- 'A',
- radius,
- radius,
- 0,
- largeArcFlag,
- sweepFlag,
- end.x,
- end.y
- ].join(' ')
- }
- const getArcPath = (p: ArcProps) => {
- const cx = Number.isFinite(p.x) ? p.x : 0
- const cy = Number.isFinite(p.y) ? p.y : 0
- const radius = Number(p.radius)
- const start = Number(p.start_angle)
- const end = Number(p.end_angle)
- if (!Number.isFinite(radius) || radius <= 0) return ''
- if (!Number.isFinite(start) || !Number.isFinite(end)) return ''
- return describeArc(cx, cy, radius, start, end)
- }
- </script>
- <style scoped></style>
|