Canvas.vue 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. <template>
  2. <div :style="boxStyle" class="w-full h-full box-border overflow-hidden relative">
  3. <svg
  4. class="absolute left-0 top-0 w-full h-full"
  5. :viewBox="`0 0 ${width} ${height}`"
  6. xmlns="http://www.w3.org/2000/svg"
  7. >
  8. <!-- 元素渲染 -->
  9. <template v-for="(el, index) in elements || []" :key="index">
  10. <!-- 矩形 -->
  11. <rect
  12. v-if="el.type === 'rect'"
  13. :x="el.props.x"
  14. :y="el.props.y"
  15. :width="el.props.width"
  16. :height="el.props.height"
  17. :fill="el.props.background_color"
  18. :stroke="el.props.border?.color"
  19. :stroke-width="el.props.border?.width"
  20. :rx="el.props.border?.radius"
  21. :ry="el.props.border?.radius"
  22. />
  23. <!-- 文本 -->
  24. <text
  25. v-else-if="el.type === 'text'"
  26. :x="el.props.x"
  27. :y="el.props.y"
  28. :fill="el.props.font_color"
  29. :font-size="el.props.font_size"
  30. :text-decoration="textDecorMap[el.props.text_decor] || 'none'"
  31. :style="getCanvasTextStyle(el.props)"
  32. dominant-baseline="hanging"
  33. >
  34. {{ getCanvasText(el.props.text) }}
  35. </text>
  36. <!-- 圆弧 -->
  37. <path
  38. v-else-if="el.type === 'arc'"
  39. :d="getArcPath(el.props)"
  40. :stroke="el.props.color"
  41. :stroke-width="el.props.width"
  42. fill="none"
  43. stroke-linecap="round"
  44. />
  45. <!-- 直线(折线) -->
  46. <polyline
  47. v-else-if="el.type === 'line'"
  48. v-show="(el.props.points || []).length > 1"
  49. :points="getLinePoints(el.props.points)"
  50. :stroke="el.props.color"
  51. :stroke-width="el.props.width"
  52. :stroke-linecap="el.props.round ? 'round' : 'butt'"
  53. fill="none"
  54. />
  55. <!-- 三角形 -->
  56. <polygon
  57. v-else-if="el.type === 'triangle'"
  58. v-show="(el.props.points || []).length >= 3"
  59. :points="getLinePoints((el.props.points || []).slice(0, 3))"
  60. :fill="el.props.background_color"
  61. />
  62. </template>
  63. </svg>
  64. <!-- 图片使用 img 绝对定位到画布上,方便加载本地资源 -->
  65. <template v-for="(el, index) in imageElements" :key="`img-${index}`">
  66. <img
  67. v-if="el.src"
  68. class="absolute"
  69. :style="{
  70. left: `${el.props.x}px`,
  71. top: `${el.props.y}px`,
  72. width: `${el.props.width}px`,
  73. height: `${el.props.height}px`,
  74. opacity: el.props.alpha / 255
  75. }"
  76. :src="el.src"
  77. />
  78. </template>
  79. </div>
  80. </template>
  81. <script setup lang="ts">
  82. import { computed } from 'vue'
  83. import { useProjectStore } from '@/store/modules/project'
  84. import { useLanguage } from '../hooks/useLanguage'
  85. type Point = { x: number; y: number }
  86. type RectProps = {
  87. x: number
  88. y: number
  89. width: number
  90. height: number
  91. background_color: string
  92. border: {
  93. color: string
  94. width: number
  95. radius: number
  96. }
  97. }
  98. type TextProps = {
  99. x: number
  100. y: number
  101. width: number
  102. font_color: string
  103. font_size: number
  104. font_family: string
  105. text_decor: string
  106. text: string
  107. }
  108. type ImageProps = {
  109. x: number
  110. y: number
  111. width: number
  112. height: number
  113. image: string
  114. alpha: number
  115. recolor: string
  116. }
  117. type ArcProps = {
  118. x: number
  119. y: number
  120. start_angle: number
  121. end_angle: number
  122. color: string
  123. width: number
  124. radius: number
  125. }
  126. type LineProps = {
  127. color: string
  128. width: number
  129. round: boolean
  130. points: Point[]
  131. }
  132. type TriangleProps = {
  133. background_color: string
  134. points: Point[]
  135. }
  136. type CanvasElement =
  137. | { type: 'rect'; props: RectProps }
  138. | { type: 'text'; props: TextProps }
  139. | { type: 'image'; props: ImageProps }
  140. | { type: 'arc'; props: ArcProps }
  141. | { type: 'line'; props: LineProps }
  142. | { type: 'triangle'; props: TriangleProps }
  143. const props = defineProps<{
  144. width: number
  145. height: number
  146. styles: any
  147. state?: string
  148. part?: string
  149. background_color: string
  150. elements: CanvasElement[]
  151. }>()
  152. const projectStore = useProjectStore()
  153. const { resolveText, getResolvedFontStyle } = useLanguage()
  154. const boxStyle = computed(() => {
  155. return {
  156. width: `${props.width}px`,
  157. height: `${props.height}px`,
  158. backgroundColor: props.background_color
  159. }
  160. })
  161. // 文本装饰映射
  162. const textDecorMap: Record<string, string> = {
  163. LV_TEXT_DECOR_NONE: 'none',
  164. LV_TEXT_DECOR_UNDERLINE: 'underline',
  165. LV_TEXT_DECOR_STRIKETHROUGH: 'line-through',
  166. 'LV_TEXT_DECOR_UNDERLINE | LV_TEXT_DECOR_STRIKETHROUGH': 'underline line-through'
  167. }
  168. // 计算图片元素及其本地路径
  169. const imageElements = computed(() => {
  170. return (props.elements || [])
  171. .filter((el): el is { type: 'image'; props: ImageProps } => el.type === 'image')
  172. .map((el) => {
  173. let src = ''
  174. const id = el.props.image
  175. const project = projectStore.project
  176. if (id && project) {
  177. const imgRes = project.resources.images.find((img) => img.id === id)
  178. if (imgRes) {
  179. src = `local:///${(projectStore.projectPath + imgRes.path).replaceAll('\\', '/')}`
  180. }
  181. }
  182. return { ...el, src }
  183. })
  184. })
  185. const getCanvasText = (text: string) => resolveText(text).text
  186. const getCanvasTextStyle = (textProps: TextProps) => {
  187. const resolvedStyle = getResolvedFontStyle(textProps.text)
  188. if (!resolvedStyle.fontSize && textProps.font_size) {
  189. resolvedStyle.fontSize = `${textProps.font_size}px`
  190. }
  191. if (!resolvedStyle.fontFamily && textProps.font_family && textProps.font_family !== 'xx') {
  192. const font = projectStore.project?.resources.fonts.find(
  193. (item) => item.id === textProps.font_family
  194. )
  195. if (font?.fileName) {
  196. resolvedStyle.fontFamily = `'${font.fileName}'`
  197. }
  198. }
  199. return resolvedStyle
  200. }
  201. // 直线/多边形 points 字符串
  202. const getLinePoints = (points: Point[] = []) => {
  203. return points.map((p) => `${p.x},${p.y}`).join(' ')
  204. }
  205. // 极坐标转直角坐标
  206. const polarToCartesian = (
  207. centerX: number,
  208. centerY: number,
  209. radius: number,
  210. angleInDegrees: number
  211. ) => {
  212. const radians = (angleInDegrees * Math.PI) / 180.0
  213. return {
  214. x: centerX + radius * Math.cos(radians),
  215. y: centerY + radius * Math.sin(radians)
  216. }
  217. }
  218. // 生成圆弧路径(始终顺时针)
  219. const describeArc = (
  220. x: number,
  221. y: number,
  222. radius: number,
  223. startAngle: number,
  224. endAngle: number
  225. ) => {
  226. // 归一化角度到 [0, 360)
  227. const s = ((startAngle % 360) + 360) % 360
  228. const e = ((endAngle % 360) + 360) % 360
  229. let clockwiseSpan = (e - s + 360) % 360
  230. // 0 或 360 视作整圆,这里用 359.999° 避免 SVG 报错
  231. if (clockwiseSpan === 0) clockwiseSpan = 359.999
  232. const start = polarToCartesian(x, y, radius, s)
  233. const end = polarToCartesian(x, y, radius, s + clockwiseSpan)
  234. const largeArcFlag = clockwiseSpan > 180 ? '1' : '0'
  235. const sweepFlag = '1'
  236. return [
  237. 'M',
  238. start.x,
  239. start.y,
  240. 'A',
  241. radius,
  242. radius,
  243. 0,
  244. largeArcFlag,
  245. sweepFlag,
  246. end.x,
  247. end.y
  248. ].join(' ')
  249. }
  250. const getArcPath = (p: ArcProps) => {
  251. const cx = Number.isFinite(p.x) ? p.x : 0
  252. const cy = Number.isFinite(p.y) ? p.y : 0
  253. const radius = Number(p.radius)
  254. const start = Number(p.start_angle)
  255. const end = Number(p.end_angle)
  256. if (!Number.isFinite(radius) || radius <= 0) return ''
  257. if (!Number.isFinite(start) || !Number.isFinite(end)) return ''
  258. return describeArc(cx, cy, radius, start, end)
  259. }
  260. </script>
  261. <style scoped></style>