更新README
@@ -0,0 +1,12 @@
|
|||||||
|
# 截图素材
|
||||||
|
|
||||||
|
将以下截图文件放入此目录,替换 README 中的配图:
|
||||||
|
|
||||||
|
| 文件 | 说明 | 建议尺寸 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `designer-overview.png` | 设计器界面全貌,展示控件库 + 画布 + 属性面板 | 1920×1080 |
|
||||||
|
| `ai-assistant.png` | AI 助手聊天窗口 + 生成的面单模板 | 1200×800 |
|
||||||
|
| `cloud-print.png` | 云打印架构拓扑图 | 1200×800 |
|
||||||
|
| `report-design.png` | 报表设计效果(表格 + 图表) | 1200×800 |
|
||||||
|
| `desktop-client.png` | C++ Qt 桌面客户端界面 | 1200×800 |
|
||||||
|
| `erp-integration.png` | ERP 对接流程示意图 | 1200×800 |
|
||||||
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 83 KiB |
@@ -4,7 +4,7 @@
|
|||||||
* 输出一串 <rect>(按序列分组并排)+ 坐标轴 + 可选网格 + 图例 + 数据标签。
|
* 输出一串 <rect>(按序列分组并排)+ 坐标轴 + 可选网格 + 图例 + 数据标签。
|
||||||
* 全部用逻辑坐标(viewBox),实际尺寸由外层容器缩放,矢量清晰。
|
* 全部用逻辑坐标(viewBox),实际尺寸由外层容器缩放,矢量清晰。
|
||||||
*/
|
*/
|
||||||
import { escapeXml, fmt, niceMax, seriesColor, truncate, legendStartX, xAxisLabelPos } from './core'
|
import { escapeXml, fmt, niceMax, seriesColor, truncate, legendStartX } from './core'
|
||||||
import type { ChartModel } from './types'
|
import type { ChartModel } from './types'
|
||||||
|
|
||||||
export function renderBar(model: ChartModel): string {
|
export function renderBar(model: ChartModel): string {
|
||||||
@@ -21,7 +21,7 @@ export function renderBar(model: ChartModel): string {
|
|||||||
const titleH = title ? 22 : 0
|
const titleH = title ? 22 : 0
|
||||||
const mTop = 10 + titleH
|
const mTop = 10 + titleH
|
||||||
const mRight = 16
|
const mRight = 16
|
||||||
const mBottom = (showAxis ? 34 : 18) + legendH
|
const mBottom = (showAxis ? 38 : 22) + legendH
|
||||||
const mLeft = showAxis ? 44 : 16
|
const mLeft = showAxis ? 44 : 16
|
||||||
|
|
||||||
const px0 = mLeft
|
const px0 = mLeft
|
||||||
@@ -86,16 +86,15 @@ export function renderBar(model: ChartModel): string {
|
|||||||
parts.push(`<text x="${(x + barW * 0.45).toFixed(1)}" y="${(y - 4).toFixed(1)}" text-anchor="middle" font-size="9" fill="#5a6068">${fmt(val)}</text>`)
|
parts.push(`<text x="${(x + barW * 0.45).toFixed(1)}" y="${(y - 4).toFixed(1)}" text-anchor="middle" font-size="9" fill="#5a6068">${fmt(val)}</text>`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// x 轴标签
|
// x 轴标签:始终以类目为基准居中于对应条带正下方,并与图表留一点间距
|
||||||
if (showAxis) {
|
if (showAxis) {
|
||||||
const pos = xAxisLabelPos(opt.labelAlign, px0 + band * c, px0 + band * (c + 1), cx)
|
parts.push(`<text x="${cx.toFixed(1)}" y="${(py1 + 18).toFixed(1)}" text-anchor="middle" font-size="10" fill="#5a6068">${escapeXml(truncate(String(cats[c] ?? '')))}</text>`)
|
||||||
parts.push(`<text x="${pos.x.toFixed(1)}" y="${(py1 + 14).toFixed(1)}" text-anchor="${pos.anchor}" font-size="10" fill="#5a6068">${escapeXml(truncate(String(cats[c] ?? '')))}</text>`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 图例
|
// 图例
|
||||||
if (showLegend) {
|
if (showLegend) {
|
||||||
const align = opt.labelAlign ?? 'left'
|
const align = opt.labelAlign ?? 'center'
|
||||||
let totalW = 0
|
let totalW = 0
|
||||||
for (let s = 0; s < sCount; s++) {
|
for (let s = 0; s < sCount; s++) {
|
||||||
const name = model.series[s]!.name || `系列${s + 1}`
|
const name = model.series[s]!.name || `系列${s + 1}`
|
||||||
@@ -103,7 +102,7 @@ export function renderBar(model: ChartModel): string {
|
|||||||
}
|
}
|
||||||
const startX = legendStartX(align, W, totalW, px0)
|
const startX = legendStartX(align, W, totalW, px0)
|
||||||
let lx = startX
|
let lx = startX
|
||||||
let ly = py1 + (showAxis ? 24 : 10)
|
let ly = py1 + (showAxis ? 30 : 16)
|
||||||
for (let s = 0; s < sCount; s++) {
|
for (let s = 0; s < sCount; s++) {
|
||||||
const name = model.series[s]!.name || `系列${s + 1}`
|
const name = model.series[s]!.name || `系列${s + 1}`
|
||||||
const color = seriesColor(model.series[s]!, s, opt)
|
const color = seriesColor(model.series[s]!, s, opt)
|
||||||
|
|||||||
@@ -117,15 +117,3 @@ export function legendStartX(
|
|||||||
if (align === 'right') return Math.max(leftPad, W - totalW - 8)
|
if (align === 'right') return Math.max(leftPad, W - totalW - 8)
|
||||||
return leftPad
|
return leftPad
|
||||||
}
|
}
|
||||||
|
|
||||||
/** x 轴类目标签的水平对齐 → SVG text-anchor 与基准 x */
|
|
||||||
export function xAxisLabelPos(
|
|
||||||
align: 'left' | 'center' | 'right' | undefined,
|
|
||||||
bandLeft: number,
|
|
||||||
bandRight: number,
|
|
||||||
bandCenter: number,
|
|
||||||
): { x: number; anchor: 'start' | 'middle' | 'end' } {
|
|
||||||
if (align === 'left') return { x: bandLeft + 4, anchor: 'start' }
|
|
||||||
if (align === 'right') return { x: bandRight - 4, anchor: 'end' }
|
|
||||||
return { x: bandCenter, anchor: 'middle' }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* 多序列折线 + 坐标轴 + 可选网格 + 图例 + 数据标签。
|
* 多序列折线 + 坐标轴 + 可选网格 + 图例 + 数据标签。
|
||||||
* 支持 smooth(Catmull-Rom 平滑曲线)与 area(折线下方面积填充)。
|
* 支持 smooth(Catmull-Rom 平滑曲线)与 area(折线下方面积填充)。
|
||||||
*/
|
*/
|
||||||
import { escapeXml, fmt, niceMax, seriesColor, truncate, legendStartX, xAxisLabelPos } from './core'
|
import { escapeXml, fmt, niceMax, seriesColor, truncate, legendStartX } from './core'
|
||||||
import type { ChartModel } from './types'
|
import type { ChartModel } from './types'
|
||||||
|
|
||||||
interface Pt { x: number; y: number }
|
interface Pt { x: number; y: number }
|
||||||
@@ -43,7 +43,7 @@ export function renderLine(model: ChartModel): string {
|
|||||||
const titleH = title ? 22 : 0
|
const titleH = title ? 22 : 0
|
||||||
const mTop = 10 + titleH
|
const mTop = 10 + titleH
|
||||||
const mRight = 16
|
const mRight = 16
|
||||||
const mBottom = (showAxis ? 34 : 18) + legendH
|
const mBottom = (showAxis ? 38 : 22) + legendH
|
||||||
const mLeft = showAxis ? 44 : 16
|
const mLeft = showAxis ? 44 : 16
|
||||||
|
|
||||||
const px0 = mLeft
|
const px0 = mLeft
|
||||||
@@ -101,8 +101,8 @@ export function renderLine(model: ChartModel): string {
|
|||||||
parts.push(`<line x1="${px0}" y1="${py1}" x2="${px1}" y2="${py1}" stroke="#cccccc" stroke-width="1"/>`)
|
parts.push(`<line x1="${px0}" y1="${py1}" x2="${px1}" y2="${py1}" stroke="#cccccc" stroke-width="1"/>`)
|
||||||
for (let c = 0; c < n; c++) {
|
for (let c = 0; c < n; c++) {
|
||||||
const x = px0 + band * (c + 0.5)
|
const x = px0 + band * (c + 0.5)
|
||||||
const pos = xAxisLabelPos(opt.labelAlign, px0 + band * c, px0 + band * (c + 1), x)
|
// x 轴类目标签:始终以类目为基准居中于对应点正下方,并与图表留一点间距
|
||||||
parts.push(`<text x="${pos.x.toFixed(1)}" y="${(py1 + 14).toFixed(1)}" text-anchor="${pos.anchor}" font-size="10" fill="#5a6068">${escapeXml(truncate(String(cats[c] ?? '')))}</text>`)
|
parts.push(`<text x="${x.toFixed(1)}" y="${(py1 + 18).toFixed(1)}" text-anchor="middle" font-size="10" fill="#5a6068">${escapeXml(truncate(String(cats[c] ?? '')))}</text>`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ export function renderLine(model: ChartModel): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showLegend) {
|
if (showLegend) {
|
||||||
const align = opt.labelAlign ?? 'left'
|
const align = opt.labelAlign ?? 'center'
|
||||||
let totalW = 0
|
let totalW = 0
|
||||||
for (let s = 0; s < seriesPts.length; s++) {
|
for (let s = 0; s < seriesPts.length; s++) {
|
||||||
const name = seriesPts[s]!.name || `系列${s + 1}`
|
const name = seriesPts[s]!.name || `系列${s + 1}`
|
||||||
@@ -140,7 +140,7 @@ export function renderLine(model: ChartModel): string {
|
|||||||
}
|
}
|
||||||
const startX = legendStartX(align, W, totalW, px0)
|
const startX = legendStartX(align, W, totalW, px0)
|
||||||
let lx = startX
|
let lx = startX
|
||||||
let ly = py1 + (showAxis ? 24 : 10)
|
let ly = py1 + (showAxis ? 30 : 16)
|
||||||
for (let s = 0; s < seriesPts.length; s++) {
|
for (let s = 0; s < seriesPts.length; s++) {
|
||||||
const name = seriesPts[s]!.name || `系列${s + 1}`
|
const name = seriesPts[s]!.name || `系列${s + 1}`
|
||||||
const color = seriesPts[s]!.color
|
const color = seriesPts[s]!.color
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export function renderPie(model: ChartModel): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showLegend) {
|
if (showLegend) {
|
||||||
const align = opt.labelAlign ?? 'left'
|
const align = opt.labelAlign ?? 'center'
|
||||||
let totalW = 0
|
let totalW = 0
|
||||||
for (let i = 0; i < data.length; i++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
const name = cats[i] ?? `类目${i + 1}`
|
const name = cats[i] ?? `类目${i + 1}`
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ const valueLabel = computed({
|
|||||||
set: (v: boolean) => patchOption('valueLabel', v),
|
set: (v: boolean) => patchOption('valueLabel', v),
|
||||||
})
|
})
|
||||||
const labelAlign = computed({
|
const labelAlign = computed({
|
||||||
get: () => control.value?.options?.labelAlign ?? 'left',
|
get: () => control.value?.options?.labelAlign ?? 'center',
|
||||||
set: (v: 'left' | 'center' | 'right') => patchOption('labelAlign', v),
|
set: (v: 'left' | 'center' | 'right') => patchOption('labelAlign', v),
|
||||||
})
|
})
|
||||||
const smooth = computed({
|
const smooth = computed({
|
||||||
@@ -220,7 +220,7 @@ const donut = computed({
|
|||||||
<NSwitch size="small" :value="valueLabel" @update:value="valueLabel = $event" />
|
<NSwitch size="small" :value="valueLabel" @update:value="valueLabel = $event" />
|
||||||
</div>
|
</div>
|
||||||
<div class="props-row">
|
<div class="props-row">
|
||||||
<span class="props-label">标签对齐</span>
|
<span class="props-label">图例对齐</span>
|
||||||
<NSelect
|
<NSelect
|
||||||
size="small"
|
size="small"
|
||||||
:value="labelAlign"
|
:value="labelAlign"
|
||||||
|
|||||||
|
After Width: | Height: | Size: 705 KiB |
|
After Width: | Height: | Size: 243 KiB |
|
After Width: | Height: | Size: 377 KiB |
|
After Width: | Height: | Size: 240 KiB |