[Core] Add some feature (#741)

This commit is contained in:
qianmoQ 2024-04-15 12:06:21 +08:00 committed by GitHub
commit 5e9456c8ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 379 additions and 722 deletions

View File

@ -178,3 +178,6 @@ ALTER TABLE `datacap_source_query`
ALTER TABLE `datacap_report` ALTER TABLE `datacap_report`
ADD COLUMN `description` VARCHAR(2000); ADD COLUMN `description` VARCHAR(2000);
ALTER TABLE `datacap_dashboard`
CHANGE `version` `description` VARCHAR(1000) DEFAULT NULL COMMENT 'Description';

View File

@ -37,8 +37,8 @@ public class DashboardEntity
@Column(name = "configure") @Column(name = "configure")
private String configure; private String configure;
@Column(name = "version") @Column(name = "description")
private String version; private String description;
@ManyToOne @ManyToOne
@JoinTable(name = "datacap_dashboard_user_relation", @JoinTable(name = "datacap_dashboard_user_relation",

View File

@ -30,7 +30,6 @@
"axios": "^1.6.7", "axios": "^1.6.7",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"echarts": "^5.5.0",
"embla-carousel": "^8.0.2", "embla-carousel": "^8.0.2",
"embla-carousel-autoplay": "^8.0.2", "embla-carousel-autoplay": "^8.0.2",
"embla-carousel-vue": "^8.0.2", "embla-carousel-vue": "^8.0.2",
@ -38,7 +37,6 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lucide-vue-next": "^0.356.0", "lucide-vue-next": "^0.356.0",
"md-editor-v3": "^4.12.1", "md-editor-v3": "^4.12.1",
"naive-ui": "^2.38.1",
"radix-vue": "^1.5.2", "radix-vue": "^1.5.2",
"tailwind-merge": "^2.2.1", "tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import { AspectRatio, type AspectRatioProps } from 'radix-vue'
const props = defineProps<AspectRatioProps>()
</script>
<template>
<AspectRatio v-bind="props">
<slot />
</AspectRatio>
</template>

View File

@ -0,0 +1 @@
export { default as AspectRatio } from './AspectRatio.vue'

View File

@ -12,5 +12,6 @@ export default {
deleteTip2: 'Warning: This cannot be undone. ', deleteTip2: 'Warning: This cannot be undone. ',
deleteTip3: 'To confirm, type [ $NAME ] in the box below', deleteTip3: 'To confirm, type [ $NAME ] in the box below',
publishSuccess: 'Dashboard [ $VALUE ] published successfully', publishSuccess: 'Dashboard [ $VALUE ] published successfully',
notFound: 'Dashboard [ $VALUE ] not found',
} }
} }

View File

@ -83,6 +83,20 @@ export default {
visualConfigureDataBreakpointContinuous: 'Continuous', visualConfigureDataBreakpointContinuous: 'Continuous',
visualConfigureDataBreakpointZero: 'Zero', visualConfigureDataBreakpointZero: 'Zero',
visualConfigureDataBreakpointIgnore: 'Ignore', visualConfigureDataBreakpointIgnore: 'Ignore',
visualConfigureGeneralGroup: 'General Configure',
visualConfigureTitleGroup: 'Title Configure',
visualConfigureTitleGroupVisible: 'Visible',
visualConfigureTitleGroupText: 'Title',
visualConfigureTitleGroupSubText: 'Sub Title',
visualConfigureTitleGroupPosition: 'Position',
visualConfigureTitleGroupPositionLeft: 'Left',
visualConfigureTitleGroupPositionRight: 'Right',
visualConfigureTitleGroupPositionTop: 'Top',
visualConfigureTitleGroupPositionBottom: 'Bottom',
visualConfigureTitleGroupAlign: 'Align',
visualConfigureTitleGroupAlignLeft: 'Left',
visualConfigureTitleGroupAlignCenter: 'Center',
visualConfigureTitleGroupAlignRight: 'Right',
columnExpressionMax: 'Maximum', columnExpressionMax: 'Maximum',
columnExpressionMin: 'Minimum', columnExpressionMin: 'Minimum',
columnExpressionSum: 'Sum', columnExpressionSum: 'Sum',

View File

@ -12,5 +12,6 @@ export default {
deleteTip2: '警告:此操作无法撤消。 ', deleteTip2: '警告:此操作无法撤消。 ',
deleteTip3: '要确认,请在下面的框中键入 [ $NAME ]', deleteTip3: '要确认,请在下面的框中键入 [ $NAME ]',
publishSuccess: '仪表板 [ $VALUE ] 发布成功', publishSuccess: '仪表板 [ $VALUE ] 发布成功',
notFound: '仪表板 [ $VALUE ] 不存在',
} }
} }

View File

@ -83,6 +83,20 @@ export default {
visualConfigureDataBreakpointContinuous: '连续', visualConfigureDataBreakpointContinuous: '连续',
visualConfigureDataBreakpointZero: '补 0', visualConfigureDataBreakpointZero: '补 0',
visualConfigureDataBreakpointIgnore: '忽略', visualConfigureDataBreakpointIgnore: '忽略',
visualConfigureGeneralGroup: '通用配置',
visualConfigureTitleGroup: '标题配置',
visualConfigureTitleGroupVisible: '是否展示',
visualConfigureTitleGroupText: '标题',
visualConfigureTitleGroupSubText: '子标题',
visualConfigureTitleGroupPosition: '位置',
visualConfigureTitleGroupPositionLeft: '左',
visualConfigureTitleGroupPositionRight: '右',
visualConfigureTitleGroupPositionTop: '顶部',
visualConfigureTitleGroupPositionBottom: '底部',
visualConfigureTitleGroupAlign: '对齐方式',
visualConfigureTitleGroupAlignLeft: '左对齐',
visualConfigureTitleGroupAlignCenter: '居中对齐',
visualConfigureTitleGroupAlignRight: '右对齐',
columnExpressionMax: '最大值', columnExpressionMax: '最大值',
columnExpressionMin: '最小值', columnExpressionMin: '最小值',
columnExpressionSum: '总和', columnExpressionSum: '总和',
@ -114,12 +128,12 @@ export default {
info: '查看详情', info: '查看详情',
lifeCycleColumn: '生命周期列', lifeCycleColumn: '生命周期列',
lifeCycleNumber: '生命周期数', lifeCycleNumber: '生命周期数',
continuousBuild: '连续构建', continuousBuild: '连续构建'
}, },
validator: { validator: {
duplicateColumn: '列名 [ $VALUE ] 已存在', duplicateColumn: '列名 [ $VALUE ] 已存在',
specifiedColumn: '排序键或主键必须指定', specifiedColumn: '排序键或主键必须指定',
specifiedName: '数据集名必须指定', specifiedName: '数据集名必须指定'
}, },
tip: { tip: {
selectExpression: '请选择表达式', selectExpression: '请选择表达式',
@ -131,6 +145,6 @@ export default {
rebuildProgress: '重建只会进行未完成进度', rebuildProgress: '重建只会进行未完成进度',
lifeCycleMustDateColumn: '生命周期必须包含一个日期列', lifeCycleMustDateColumn: '生命周期必须包含一个日期列',
modifyNotSupportDataPreview: '修改暂不支持数据预览', modifyNotSupportDataPreview: '修改暂不支持数据预览',
publishSuccess: '数据集 [ $VALUE ] 发布成功', publishSuccess: '数据集 [ $VALUE ] 发布成功'
} }
} }

View File

@ -12,6 +12,7 @@ export interface DashboardModel
updateTime?: string updateTime?: string
user?: UserModel user?: UserModel
reports?: ReportModel[] reports?: ReportModel[]
code?: string
} }
export class DashboardRequest export class DashboardRequest

View File

@ -129,7 +129,7 @@ const createAdminRouter = (router: any) => {
component: () => import('@/views/pages/admin/dashboard/DashboardHome.vue') component: () => import('@/views/pages/admin/dashboard/DashboardHome.vue')
}, },
{ {
path: 'dashboard/info/:id/preview', path: 'dashboard/preview/:code',
meta: { meta: {
title: 'common.dashboard', title: 'common.dashboard',
isRoot: false isRoot: false
@ -137,7 +137,7 @@ const createAdminRouter = (router: any) => {
component: () => import('@/views/pages/admin/dashboard/DashboardPreview.vue') component: () => import('@/views/pages/admin/dashboard/DashboardPreview.vue')
}, },
{ {
path: 'dashboard/info/:id?', path: 'dashboard/info/:code?',
meta: { meta: {
title: 'common.dashboard', title: 'common.dashboard',
isRoot: false isRoot: false

View File

@ -1,17 +0,0 @@
/**
* Returns an array of unique values extracted from the specified key in each object within the provided array.
*
* @param {string} key - The key to extract values from each object in the array.
* @param {any[]} columns - The array of objects to extract values from.
* @return {any[]} An array containing unique values extracted from the specified key in each object.
*/
export function getValueByKey(key: string, columns: never[]): any[]
{
const container: any[] = []
columns.forEach(column => {
if (container.indexOf(column[key]) === -1) {
container.push(column[key])
}
});
return container
}

View File

@ -1,9 +0,0 @@
import { EchartsType } from '@/views/components/echarts/EchartsType'
export class EchartsConfigure
{
headers: [] | undefined
types: [] | undefined
columns: [] | undefined
type: EchartsType = EchartsType.LINE
}

View File

@ -1,264 +0,0 @@
<template>
<div>
<Modal v-model="visible"
placement="right"
:title="$t('common.visualization')"
:mask-closable="false"
:width="'90%'"
:transfer="false">
<Form :inline="true">
<FormItem :label="$t('common.name')"
:label-width="80">
<Input v-model="formState.name"/>
</FormItem>
<FormItem :label="$t('common.realtime')"
:label-width="80">
<Switch v-model="formState.realtime"/>
</FormItem>
</Form>
<Layout v-if="configure">
<Layout>
<Content>
<EchartsPreview :key="referKey" :height="'500px'" :configure="chartOptions"/>
</Content>
<Sider style="background-color: #FFFFFF;"
hide-trigger>
<Form label-position="left"
:label-width="50">
<Card padding="0"
dis-hover
:bordered="false">
<template #title>
<RadioGroup v-model="chartType"
type="button"
@on-change="handlerChangeValue('Type')">
<Space>
<Radio label="line">
<FontAwesomeIcon icon="chart-line">
</FontAwesomeIcon>
</Radio>
<Radio label="bar">
<FontAwesomeIcon icon="chart-bar">
</FontAwesomeIcon>
</Radio>
</Space>
</RadioGroup>
</template>
<Collapse v-if="chartType"
v-model="collapseValue"
accordion>
<div v-if="chartOptions">
<Panel name="xAxis">
{{ $t('common.xAxis') }}
<template #content>
<FormItem :label="$t('common.column')">
<Select v-model="defaultConfigure.xAxis" @on-change="handlerChangeValue('xAxis')">
<Option v-for="value of configure.headers" :value="value" v-bind:key="value">{{ value }}</Option>
</Select>
</FormItem>
<FormItem v-if="chartOptions.xAxis" :label="$t('common.type')">
<RadioGroup v-model="chartOptions.xAxis.type" type="button" size="small" @on-change="handlerChangeValue">
<Radio label="value">{{ $t('common.column') }}</Radio>
<Radio label="category">{{ $t('common.tag') }}</Radio>
</RadioGroup>
</FormItem>
</template>
</Panel>
<Panel v-if="chartOptions.xAxis && chartOptions.yAxis && !chartOptions.yAxis.disabled" disabled name="yAxis">
{{ $t('common.yAxis') }}
<template #content>
<FormItem label="Value">
<Select v-model="defaultConfigure.yAxis">
<Option v-for="value of configure.headers" :value="value" v-bind:key="value">{{ value }}</Option>
</Select>
</FormItem>
<FormItem label="Type">
<RadioGroup v-model="chartOptions.yAxis.type" @on-change="handlerChangeValue">
<Radio label="value"></Radio>
<Radio label="category"></Radio>
</RadioGroup>
</FormItem>
</template>
</Panel>
</div>
<div v-if="chartOptions">
<Panel v-if="chartOptions.xAxis" name="Series">
{{ $t('common.data') }}
<template #content>
<FormItem :label="$t('common.column')">
<Select v-model="defaultConfigure.series" @on-change="handlerChangeValue('Series')">
<Option v-for="value of configure.headers" :value="value" v-bind:key="value">{{ value }}</Option>
</Select>
</FormItem>
</template>
</Panel>
</div>
</Collapse>
</Card>
</Form>
</Sider>
</Layout>
</Layout>
<Result v-else
type="warning">
<template #desc>
</template>
<template #actions>
</template>
</Result>
<template #footer>
<Button type="primary"
:loading="loading"
@click="handlerPublish">
{{ $t('common.publish') }}
</Button>
</template>
</Modal>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { ChartConfigure } from '@/views/components/echarts/configure/ChartConfigure'
import { getValueByKey } from './DataUtils'
import { SeriesConfigure } from '@/views/components/echarts/configure/SeriesConfigure'
import { isEmpty } from 'lodash'
import { EchartsConfigure } from '@/views/components/echarts/EchartsConfigure'
import EchartsPreview from '@/views/components/echarts/EchartsPreview.vue'
import { AxisConfigure } from '@/views/components/echarts/configure/AxisConfigure'
import ReportService from '@/services/report'
import { ObjectUtils } from '@/utils/object'
import { ToastUtils } from '@/utils/toast'
export default defineComponent({
name: 'EchartsEditor',
components: {EchartsPreview},
props: {
isVisible: {
type: Boolean,
default: () => false
},
configure: {
type: Object as () => EchartsConfigure,
default: () => null
},
sourceId: {
type: Number
},
query: {
type: String
}
},
data()
{
return {
collapseValue: 'xAxis',
referKey: 0,
defaultConfigure: {
xAxis: '',
yAxis: '',
series: ''
},
chartOptions: null as ChartConfigure | null,
chartType: null,
formState: {
name: null as string | null,
realtime: null,
type: 'QUERY',
configure: null as string | null,
source: {id: null as number | null},
query: null as string | null
},
loading: false
}
},
created()
{
this.handlerInitialize()
},
methods: {
handlerInitialize()
{
this.chartOptions = new ChartConfigure()
},
handlerChangeValue(type: string)
{
this.referKey = ObjectUtils.getTimestamp()
switch (type) {
case 'xAxis':
if (this.chartOptions && this.configure) {
this.chartOptions.xAxis = new AxisConfigure()
this.chartOptions.xAxis.data = getValueByKey(this.defaultConfigure.xAxis, this.configure.columns as never[]) as never[]
this.chartOptions.xAxis.meta.column = this.defaultConfigure.xAxis
this.chartOptions.yAxis = new AxisConfigure()
this.chartOptions.yAxis.type = 'value'
this.chartOptions.yAxis.data = getValueByKey(this.defaultConfigure.yAxis, this.configure.columns as never[]) as never[]
this.chartOptions.yAxis.disabled = true
this.chartOptions.yAxis.meta.column = this.defaultConfigure.yAxis
}
break
case 'Series': {
if (this.chartOptions && this.configure) {
const series: SeriesConfigure = new SeriesConfigure()
series.data = getValueByKey(this.defaultConfigure.series, this.configure.columns as never[])
series.type = this.chartType as any
series.meta.column = this.defaultConfigure.series
this.chartOptions.series = []
this.chartOptions.series.push(series)
}
break;
}
case 'Type': {
if (this.chartOptions) {
const array = this.chartOptions.series as never[]
if (array.length > 0) {
(array as any[])[0].type = this.chartType
}
}
}
}
this.handlerSetDefaultValue();
},
handlerSetDefaultValue()
{
if (this.chartOptions) {
if (isEmpty(this.defaultConfigure.xAxis)) {
this.chartOptions.xAxis = new AxisConfigure()
}
if (isEmpty(this.defaultConfigure.yAxis)) {
this.chartOptions.yAxis = new AxisConfigure()
}
}
},
handlerPublish()
{
if (this.formState) {
this.loading = true
this.formState.configure = JSON.stringify(this.chartOptions)
this.formState.source.id = this.sourceId as number
this.formState.query = this.query as string
ReportService.saveOrUpdate(this.formState)
.then(response => {
if (response.status) {
ToastUtils.success(this.$t('report.publishSuccess').replace('REPLACE_NAME', this.formState.name as string))
this.visible = false
}
})
.finally(() => this.loading = false)
}
}
},
computed: {
visible: {
get(): boolean
{
return this.isVisible;
},
set(value: boolean)
{
this.$emit('close', value);
}
}
}
});
</script>

View File

@ -1,107 +0,0 @@
<template>
<div>
<CircularLoading v-if="loading" :show="loading"/>
<div v-else :style="{width: width, height: height, padding: '0'}" :id="key"/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import * as echarts from 'echarts'
import { v4 as uuidv4 } from 'uuid'
import CircularLoading from '@/views/components/loading/CircularLoading.vue'
import { ChartConfigure } from './configure/ChartConfigure'
import ReportService from '@/services/report'
import { getValueByKey } from './DataUtils'
import { SeriesConfigure } from './configure/SeriesConfigure'
import { ExecuteModel } from '@/model/execute'
import ExecuteService from '@/services/execute'
export default defineComponent({
name: 'EchartsPreview',
components: {CircularLoading},
props: {
width: {
type: String,
default: () => '100%'
},
height: {
type: String,
default: () => '300px'
},
configure: {
type: Object as () => ChartConfigure | null
},
id: {
type: Number
}
},
watch: {
width: 'handlerInitialize',
height: 'handlerInitialize'
},
created()
{
this.handlerInitialize()
},
data()
{
return {
loading: false,
key: ''
}
},
methods: {
handlerInitialize()
{
this.key = uuidv4()
setTimeout(() => {
try {
if (this.key) {
const echartsContainer = document.getElementById(this.key)
const echartsChart = echarts.init(echartsContainer)
echartsChart.resize()
if (this.id) {
this.loading = true
ReportService.getById(this.id)
.then(response => {
if (response.status && response.data.realtime) {
if (response.data.source) {
const queryConfigure: ExecuteModel = {
name: response.data.source.id,
content: response.data.query,
format: 'JSON',
mode: 'REPORT'
}
ExecuteService.execute(queryConfigure, null)
.then(response => {
const configure: any = this.configure as ChartConfigure;
configure.xAxis.data = getValueByKey(configure.xAxis.meta.column, response.data.columns)
configure.yAxis.data = getValueByKey(configure.yAxis.meta.column, response.data.columns)
configure.series.forEach((item: SeriesConfigure) => {
const series: SeriesConfigure = item as SeriesConfigure
series.data = getValueByKey(series.meta.column as string, response.data.columns)
})
echartsChart.setOption(configure)
})
.finally(() => this.loading = false)
}
}
else {
echartsChart.setOption(this.configure as any)
}
})
.finally(() => this.loading = false)
}
else {
echartsChart.setOption(this.configure as any)
}
}
}
catch (e) {
console.error(e)
}
}, 0)
}
}
});
</script>

View File

@ -1,5 +0,0 @@
export enum EchartsType
{
LINE = ('line'),
BAR = ('bar')
}

View File

@ -1,9 +0,0 @@
export class AxisConfigure
{
type = 'category'
data: never[] = []
disabled = false
meta = {
column: null as string | null
}
}

View File

@ -1,15 +0,0 @@
import { SeriesConfigure } from '@/views/components/echarts/configure/SeriesConfigure'
import { AxisConfigure } from '@/views/components/echarts/configure/AxisConfigure'
import { TooltipConfigure } from '@/views/components/echarts/configure/TooltipConfigure'
export class ChartConfigure
{
xAxis: AxisConfigure | undefined
yAxis: AxisConfigure | undefined
series: Array<SeriesConfigure> | undefined
tooltip: TooltipConfigure = new TooltipConfigure()
}
export default function toOptions(options: ChartConfigure): any {
return JSON.stringify(options)
}

View File

@ -1,11 +0,0 @@
import { EchartsType } from '@/views/components/echarts/EchartsType'
export class SeriesConfigure
{
data: any[] = []
type: EchartsType = EchartsType.LINE
smooth = true
meta = {
column: null as string | null
}
}

View File

@ -1,3 +0,0 @@
export class TooltipConfigure {
trigger = 'axis'
}

View File

@ -1,14 +0,0 @@
class CommonUtils
{
getXAxis(type = 'value', items: any[])
{
return {
type: type,
data: items
}
}
userEditorConfigure = 'DataCapUserEditorConfigure'
}
export default new CommonUtils()

View File

@ -4,8 +4,7 @@
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle class="border-b -mt-4 pb-2">SQL</AlertDialogTitle> <AlertDialogTitle class="border-b -mt-4 pb-2">SQL</AlertDialogTitle>
</AlertDialogHeader> </AlertDialogHeader>
<VAceEditor v-if="configure" lang="mysql" :theme="configure.theme" :style="{height: '200px', fontSize: configure.fontSize + 'px'}" :value="localContent" <AceEditor :value="content as string" read-only/>
:options="editorOptions"/>
<AlertDialogFooter class="-mb-4 border-t pt-2"> <AlertDialogFooter class="-mb-4 border-t pt-2">
<Button @click="handlerCancel">{{ $t('common.cancel') }}</Button> <Button @click="handlerCancel">{{ $t('common.cancel') }}</Button>
</AlertDialogFooter> </AlertDialogFooter>
@ -26,18 +25,15 @@ import {
AlertDialogTitle, AlertDialogTitle,
AlertDialogTrigger AlertDialogTrigger
} from '@/components/ui/alert-dialog' } from '@/components/ui/alert-dialog'
import CommonUtils from '@/views/components/echarts/utils/CommonUtils'
import { UserEditor } from '@/model/user'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { VAceEditor } from 'vue3-ace-editor' import AceEditor from '@/views/components/editor/AceEditor.vue'
import '@/ace-editor-theme'
export default defineComponent({ export default defineComponent({
name: 'SqlInfo', name: 'SqlInfo',
components: { components: {
AceEditor,
Button, Button,
AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger
VAceEditor
}, },
props: { props: {
isVisible: { isVisible: {
@ -48,25 +44,7 @@ export default defineComponent({
type: String as PropType<string | null> type: String as PropType<string | null>
} }
}, },
data()
{
return {
localContent: this.content as string,
configure: null as UserEditor | null,
editorOptions: {readOnly: true}
}
},
created()
{
this.handlerInitialize()
},
methods: { methods: {
handlerInitialize()
{
const localEditorConfigure = localStorage.getItem(CommonUtils.userEditorConfigure)
const defaultEditorConfigure: UserEditor = {fontSize: 12, theme: 'chrome'}
this.configure = localEditorConfigure ? JSON.parse(localEditorConfigure) : defaultEditorConfigure
},
handlerCancel() handlerCancel()
{ {
this.visible = false this.visible = false

View File

@ -33,7 +33,18 @@ export interface IChart
invalidType?: string invalidType?: string
showLegend?: boolean showLegend?: boolean
startAngle?: number[] startAngle?: number[]
endAngle?: number[] endAngle?: number[],
titleVisible?: boolean
titleText?: string
titleSubText?: string
titlePosition?: string
titleAlign?: string
}
export interface ChartFieldGroup
{
label?: string
fields?: ChartField[]
} }
export interface ChartField export interface ChartField
@ -46,4 +57,12 @@ export interface ChartField
min?: number min?: number
max?: number max?: number
step?: number step?: number
disabled?: ChartFieldItem
} }
export interface ChartFieldItem
{
field?: string
value?: any
}

View File

@ -1,3 +1,6 @@
import { Type } from '@/views/components/visual/Type.ts'
import { ChartField, ChartFieldGroup } from '@/views/components/visual/Configuration.ts'
/** /**
* Generates a table header based on the given data array. * Generates a table header based on the given data array.
* *
@ -14,6 +17,108 @@ const createdTableHeader = (data: never[]): object[] => {
}) })
} }
export { const createdConfigure = (type: Type, i18n: any): Array<ChartFieldGroup> => {
createdTableHeader const fieldGroups: Array<ChartFieldGroup> = new Array<ChartFieldGroup>()
const categoryField: ChartField = { label: i18n.t('dataset.common.visualConfigureCategoryField'), field: 'xAxis' }
const valueField: ChartField = { label: i18n.t('dataset.common.visualConfigureValueField'), field: 'yAxis' }
const seriesField: ChartField = { label: i18n.t('dataset.common.visualConfigureSeriesField'), field: 'series' }
const showLegend: ChartField = { label: i18n.t('dataset.common.visualConfigureShowLegend'), field: 'showLegend', type: 'SWITCH' }
const outerRadius: ChartField = { label: i18n.t('dataset.common.visualConfigureOuterRadius'), field: 'outerRadius', type: 'SLIDER', value: 0.8, min: 0.1, max: 1, step: 0.1 }
const innerRadius: ChartField = { label: i18n.t('dataset.common.visualConfigureInnerRadius'), field: 'innerRadius', type: 'SLIDER', value: 0.5, min: 0.1, max: 1, step: 0.1 }
const startAngle: ChartField = { label: i18n.t('dataset.common.visualConfigureStartAngle'), field: 'startAngle', type: 'SLIDER', value: -180, min: -360, max: 360, step: 1 }
const endAngle: ChartField = { label: i18n.t('dataset.common.visualConfigureEndAngle'), field: 'endAngle', type: 'SLIDER', value: 0, min: -360, max: 360, step: 1 }
const dataBreakpoint: ChartField = {
label: i18n.t('dataset.common.visualConfigureDataBreakpoint'),
field: 'dataBreakpoint',
values: [
{ label: i18n.t('dataset.common.visualConfigureDataBreakpointBreak'), value: 'break' },
{ label: i18n.t('dataset.common.visualConfigureDataBreakpointContinuous'), value: 'link' },
{ label: i18n.t('dataset.common.visualConfigureDataBreakpointZero'), value: 'zero' },
{ label: i18n.t('dataset.common.visualConfigureDataBreakpointIgnore'), value: 'ignore' }
]
}
const leftField: ChartField = { label: i18n.t('dataset.common.visualConfigureCategoryLeftField'), field: 'leftField' }
const rightField: ChartField = { label: i18n.t('dataset.common.visualConfigureCategoryRightField'), field: 'rightField' }
const titleVisible: ChartField = { label: i18n.t('dataset.common.visualConfigureTitleGroupVisible'), field: 'titleVisible', type: 'SWITCH', value: true }
const titleText: ChartField = {
label: i18n.t('dataset.common.visualConfigureTitleGroupText'),
field: 'titleText',
type: 'TEXT',
disabled: { field: 'titleVisible', value: false }
}
const titleSubText: ChartField = {
label: i18n.t('dataset.common.visualConfigureTitleGroupSubText'),
field: 'titleSubText',
type: 'TEXT',
disabled: { field: 'titleVisible', value: false }
}
const titlePosition: ChartField = {
label: i18n.t('dataset.common.visualConfigureTitleGroupPosition'), field: 'titlePosition',
values: [
{ label: i18n.t('dataset.common.visualConfigureTitleGroupPositionLeft'), value: 'left' },
{ label: i18n.t('dataset.common.visualConfigureTitleGroupPositionTop'), value: 'top' },
{ label: i18n.t('dataset.common.visualConfigureTitleGroupPositionRight'), value: 'right' },
{ label: i18n.t('dataset.common.visualConfigureTitleGroupPositionBottom'), value: 'bottom' }
],
value: 'top',
disabled: { field: 'titleVisible', value: false }
}
const titleAlign: ChartField = {
label: i18n.t('dataset.common.visualConfigureTitleGroupAlign'), field: 'titleAlign',
values: [
{ label: i18n.t('dataset.common.visualConfigureTitleGroupAlignLeft'), value: 'left' },
{ label: i18n.t('dataset.common.visualConfigureTitleGroupAlignCenter'), value: 'center' },
{ label: i18n.t('dataset.common.visualConfigureTitleGroupAlignRight'), value: 'right' }
],
value: 'left',
disabled: { field: 'titleVisible', value: false }
}
if (type === Type.LINE) {
const fields: Array<ChartField> = [categoryField, valueField, seriesField, dataBreakpoint]
const generalField: ChartFieldGroup = { label: i18n.t('dataset.common.visualConfigureGeneralGroup'), fields: fields }
const titleGroup: ChartFieldGroup = {
label: i18n.t('dataset.common.visualConfigureTitleGroup'),
fields: [titleVisible, titleText, titleSubText, titlePosition, titleAlign]
}
fieldGroups.push(generalField, titleGroup)
}
else if (type === Type.AREA || type === Type.BAR || type === Type.RADAR || type === Type.SCATTER) {
const fields: Array<ChartField> = [categoryField, valueField]
const generalField: ChartFieldGroup = { label: i18n.t('dataset.common.visualConfigureGeneralGroup'), fields: fields }
fieldGroups.push(generalField)
}
else if (type === Type.FUNNEL) {
const fields: Array<ChartField> = [categoryField, valueField, showLegend]
const generalField: ChartFieldGroup = { label: i18n.t('dataset.common.visualConfigureGeneralGroup'), fields: fields }
fieldGroups.push(generalField)
}
else if (type === Type.GAUGE) {
const fields: Array<ChartField> = [categoryField, valueField, outerRadius, innerRadius, startAngle, endAngle]
const generalGroup: ChartFieldGroup = { label: i18n.t('dataset.common.visualConfigureGeneralGroup'), fields: fields }
fieldGroups.push(generalGroup)
}
else if (type === Type.PIE) {
const fields: Array<ChartField> = [categoryField, valueField, outerRadius]
const generalField: ChartFieldGroup = { label: i18n.t('dataset.common.visualConfigureGeneralGroup'), fields: fields }
fieldGroups.push(generalField)
}
else if (type === Type.HISTOGRAM) {
const fields: Array<ChartField> = [leftField, rightField, valueField]
const generalField: ChartFieldGroup = { label: i18n.t('dataset.common.visualConfigureGeneralGroup'), fields: fields }
fieldGroups.push(generalField)
}
else if (type === Type.WORDCLOUD) {
const fields: Array<ChartField> = [categoryField, valueField, seriesField]
const generalField: ChartFieldGroup = { label: i18n.t('dataset.common.visualConfigureGeneralGroup'), fields: fields }
fieldGroups.push(generalField)
}
return fieldGroups
}
export {
createdTableHeader,
createdConfigure
} }

View File

@ -27,12 +27,12 @@
<div v-else-if="configuration"> <div v-else-if="configuration">
<ToggleGroup v-model="configuration.type" type="single"> <ToggleGroup v-model="configuration.type" type="single">
<div class="grid grid-cols-4 items-center space-x-1 space-y-1"> <div class="grid grid-cols-4 items-center space-x-1 space-y-1">
<ToggleGroupItem class="mt-1" :value="Type.TABLE"> <ToggleGroupItem class="mt-1" :disabled="configuration.headers.length === 0" :value="Type.TABLE">
<Tooltip :content="$t('dataset.common.visualTypeTable')"> <Tooltip :content="$t('dataset.common.visualTypeTable')">
<Table :size="20"/> <Table :size="20"/>
</Tooltip> </Tooltip>
</ToggleGroupItem> </ToggleGroupItem>
<ToggleGroupItem :value="Type.LINE"> <ToggleGroupItem :disabled="configuration.headers.length === 0" :value="Type.LINE">
<Tooltip :content="$t('dataset.common.visualTypeLine')"> <Tooltip :content="$t('dataset.common.visualTypeLine')">
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="24" viewBox="0 0 24 24" fill="none" class="vchart-dropdown-content-item-icon" id="lineChart" <svg xmlns="http://www.w3.org/2000/svg" width="30" height="24" viewBox="0 0 24 24" fill="none" class="vchart-dropdown-content-item-icon" id="lineChart"
style="width: 24px;"> style="width: 24px;">
@ -42,7 +42,7 @@
</svg> </svg>
</Tooltip> </Tooltip>
</ToggleGroupItem> </ToggleGroupItem>
<ToggleGroupItem :value="Type.BAR"> <ToggleGroupItem :disabled="configuration.headers.length === 0" :value="Type.BAR">
<Tooltip :content="$t('dataset.common.visualTypeBar')"> <Tooltip :content="$t('dataset.common.visualTypeBar')">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" class="vchart-dropdown-content-item-icon" id="barChart" <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" class="vchart-dropdown-content-item-icon" id="barChart"
style="width: 24px;"> style="width: 24px;">
@ -53,7 +53,7 @@
</svg> </svg>
</Tooltip> </Tooltip>
</ToggleGroupItem> </ToggleGroupItem>
<ToggleGroupItem :value="Type.AREA"> <ToggleGroupItem :disabled="configuration.headers.length === 0" :value="Type.AREA">
<Tooltip :content="$t('dataset.common.visualTypeArea')"> <Tooltip :content="$t('dataset.common.visualTypeArea')">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" class="vchart-dropdown-content-item-icon" id="areaChart" <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" class="vchart-dropdown-content-item-icon" id="areaChart"
style="width: 24px;"> style="width: 24px;">
@ -64,7 +64,7 @@
</svg> </svg>
</Tooltip> </Tooltip>
</ToggleGroupItem> </ToggleGroupItem>
<ToggleGroupItem :value="Type.PIE"> <ToggleGroupItem :disabled="configuration.headers.length === 0" :value="Type.PIE">
<Tooltip :content="$t('dataset.common.visualTypePie')"> <Tooltip :content="$t('dataset.common.visualTypePie')">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none" class="vchart-dropdown-content-item-icon" id="pieChart" <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none" class="vchart-dropdown-content-item-icon" id="pieChart"
style="width: 24px;"> style="width: 24px;">
@ -76,7 +76,7 @@
</svg> </svg>
</Tooltip> </Tooltip>
</ToggleGroupItem> </ToggleGroupItem>
<ToggleGroupItem :value="Type.HISTOGRAM"> <ToggleGroupItem :disabled="configuration.headers.length === 0" :value="Type.HISTOGRAM">
<Tooltip :content="$t('dataset.common.visualTypeHistogram')"> <Tooltip :content="$t('dataset.common.visualTypeHistogram')">
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="20" viewBox="0 0 21 20" fill="none"> <svg xmlns="http://www.w3.org/2000/svg" width="21" height="20" viewBox="0 0 21 20" fill="none">
<g clip-path="url(#clip0_1700_69225)"> <g clip-path="url(#clip0_1700_69225)">
@ -98,7 +98,7 @@
</svg> </svg>
</Tooltip> </Tooltip>
</ToggleGroupItem> </ToggleGroupItem>
<ToggleGroupItem :value="Type.WORDCLOUD"> <ToggleGroupItem :disabled="configuration.headers.length === 0" :value="Type.WORDCLOUD">
<Tooltip :content="$t('dataset.common.visualTypeWordCloud')"> <Tooltip :content="$t('dataset.common.visualTypeWordCloud')">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" class="vchart-dropdown-content-item-icon" id="wordCloud" <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" class="vchart-dropdown-content-item-icon" id="wordCloud"
style="width: 24px;"> style="width: 24px;">
@ -116,7 +116,7 @@
</svg> </svg>
</Tooltip> </Tooltip>
</ToggleGroupItem> </ToggleGroupItem>
<ToggleGroupItem :value="Type.SCATTER"> <ToggleGroupItem :disabled="configuration.headers.length === 0" :value="Type.SCATTER">
<Tooltip :content="$t('dataset.common.visualTypeScatter')"> <Tooltip :content="$t('dataset.common.visualTypeScatter')">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" class="vchart-dropdown-content-item-icon" id="scatterChart" <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" class="vchart-dropdown-content-item-icon" id="scatterChart"
style="width: 24px;"> style="width: 24px;">
@ -128,7 +128,7 @@
</svg> </svg>
</Tooltip> </Tooltip>
</ToggleGroupItem> </ToggleGroupItem>
<ToggleGroupItem :value="Type.RADAR"> <ToggleGroupItem :disabled="configuration.headers.length === 0" :value="Type.RADAR">
<Tooltip :content="$t('dataset.common.visualTypeRadar')"> <Tooltip :content="$t('dataset.common.visualTypeRadar')">
<svg width="22" height="21" viewBox="0 0 22 21" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="22" height="21" viewBox="0 0 22 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path
@ -140,7 +140,7 @@
</svg> </svg>
</Tooltip> </Tooltip>
</ToggleGroupItem> </ToggleGroupItem>
<ToggleGroupItem :value="Type.FUNNEL"> <ToggleGroupItem :disabled="configuration.headers.length === 0" :value="Type.FUNNEL">
<Tooltip :content="$t('dataset.common.visualTypeFunnel')"> <Tooltip :content="$t('dataset.common.visualTypeFunnel')">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" class="vchart-dropdown-content-item-icon" id="funnelChart" <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" class="vchart-dropdown-content-item-icon" id="funnelChart"
style="width: 24px;"> style="width: 24px;">
@ -152,7 +152,7 @@
</svg> </svg>
</Tooltip> </Tooltip>
</ToggleGroupItem> </ToggleGroupItem>
<ToggleGroupItem :value="Type.GAUGE"> <ToggleGroupItem :disabled="configuration.headers.length === 0" :value="Type.GAUGE">
<Tooltip :content="$t('dataset.common.visualTypeGauge')"> <Tooltip :content="$t('dataset.common.visualTypeGauge')">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" class="vchart-dropdown-content-item-icon" id="gauge" <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" class="vchart-dropdown-content-item-icon" id="gauge"
style="width: 24px;"> style="width: 24px;">
@ -169,13 +169,15 @@
<Card body-class="p-2"> <Card body-class="p-2">
<template #title>{{ $t('dataset.common.visualConfigure') }}</template> <template #title>{{ $t('dataset.common.visualConfigure') }}</template>
<CircularLoading v-if="loading" :show="loading"/> <CircularLoading v-if="loading" :show="loading"/>
<div v-else-if="configuration"> <div v-else-if="configuration" class="flex items-center justify-center">
<Alert v-if="configuration.type === Type.TABLE" :title="$t('dataset.common.visualConfigureNotSpecified')"/> <Alert v-if="configuration.type === Type.TABLE" :title="$t('dataset.common.visualConfigureNotSpecified')"/>
<VisualConfigure v-else :configuration="configuration" :fields="forwardFiled(configuration.type)" @change="configuration.chartConfigure = $event"/> <Button v-else size="sm" class="w-[80%]" @click="configureVisible = true">{{ $t('common.configure') }}</Button>
</div> </div>
</Card> </Card>
</div> </div>
</div> </div>
<VisualConfigure v-if="configureVisible && configuration" :is-visible="configureVisible" :configuration="configuration" :field-group="forwardFiled(configuration.type)"
@close="configureVisible = $event" @change="configuration.chartConfigure = $event"/>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Type } from '@/views/components/visual/Type' import { Type } from '@/views/components/visual/Type'
@ -183,7 +185,7 @@ import VisualWordCloud from '@/views/components/visual/components/VisualWordClou
import VisualHistogram from '@/views/components/visual/components/VisualHistogram.vue' import VisualHistogram from '@/views/components/visual/components/VisualHistogram.vue'
import VisualPie from '@/views/components/visual/components/VisualPie.vue' import VisualPie from '@/views/components/visual/components/VisualPie.vue'
import VisualArea from '@/views/components/visual/components/VisualArea.vue' import VisualArea from '@/views/components/visual/components/VisualArea.vue'
import { ChartField, Configuration } from './Configuration' import { ChartFieldGroup, Configuration } from './Configuration'
import VisualBar from '@/views/components/visual/components/VisualBar.vue' import VisualBar from '@/views/components/visual/components/VisualBar.vue'
import VisualLine from '@/views/components/visual/components/VisualLine.vue' import VisualLine from '@/views/components/visual/components/VisualLine.vue'
import VisualTable from '@/views/components/visual/components/VisualTable.vue' import VisualTable from '@/views/components/visual/components/VisualTable.vue'
@ -200,21 +202,14 @@ import VisualConfigure from '@/views/components/visual/components/VisualConfigur
import VisualRadar from '@/views/components/visual/components/VisualRadar.vue' import VisualRadar from '@/views/components/visual/components/VisualRadar.vue'
import VisualFunnel from '@/views/components/visual/components/VisualFunnel.vue' import VisualFunnel from '@/views/components/visual/components/VisualFunnel.vue'
import VisualGauge from '@/views/components/visual/components/VisualGauge.vue' import VisualGauge from '@/views/components/visual/components/VisualGauge.vue'
import Button from '@/views/ui/button'
import { createdConfigure } from '@/views/components/visual/Utils.ts'
import { useI18n } from 'vue-i18n'
export default defineComponent({ export default defineComponent({
name: 'VisualEditor', name: 'VisualEditor',
computed: {
Type()
{
return Type
}
},
components: { components: {
VisualGauge, VisualGauge, VisualFunnel, VisualRadar, VisualConfigure, VisualScatter,
VisualFunnel,
VisualRadar,
VisualConfigure,
VisualScatter,
Card, Card,
Tooltip, Tooltip,
RadioGroup, RadioGroupItem, RadioGroup, RadioGroupItem,
@ -222,8 +217,15 @@ export default defineComponent({
Table, Table,
CircularLoading, CircularLoading,
Alert, Alert,
Button,
VisualWordCloud, VisualHistogram, VisualPie, VisualArea, VisualBar, VisualLine, VisualTable VisualWordCloud, VisualHistogram, VisualPie, VisualArea, VisualBar, VisualLine, VisualTable
}, },
computed: {
Type()
{
return Type
}
},
props: { props: {
loading: { loading: {
type: Boolean, type: Boolean,
@ -233,61 +235,28 @@ export default defineComponent({
type: Object as PropType<Configuration | null> type: Object as PropType<Configuration | null>
} }
}, },
setup()
{
const i18n = useI18n()
return {
i18n
}
},
data()
{
return {
configureVisible: false
}
},
methods: { methods: {
handlerCommit(value: any) handlerCommit(value: any)
{ {
this.$emit('commitOptions', value) this.$emit('commitOptions', value)
}, },
forwardFiled(type: Type): ChartField[] forwardFiled(type: Type): ChartFieldGroup[]
{ {
const fields: Array<ChartField> = new Array<ChartField>() return createdConfigure(type, this.i18n)
const categoryField: ChartField = { label: this.$t('dataset.common.visualConfigureCategoryField'), field: 'xAxis' }
const valueField: ChartField = { label: this.$t('dataset.common.visualConfigureValueField'), field: 'yAxis' }
const seriesField: ChartField = { label: this.$t('dataset.common.visualConfigureSeriesField'), field: 'series' }
const showLegend: ChartField = { label: this.$t('dataset.common.visualConfigureShowLegend'), field: 'showLegend', type: 'SWITCH' }
const outerRadius: ChartField = { label: this.$t('dataset.common.visualConfigureOuterRadius'), field: 'outerRadius', type: 'SLIDER', value: 0.8, min: 0.1, max: 1, step: 0.1 }
const innerRadius: ChartField = { label: this.$t('dataset.common.visualConfigureInnerRadius'), field: 'innerRadius', type: 'SLIDER', value: 0.5, min: 0.1, max: 1, step: 0.1 }
const startAngle: ChartField = { label: this.$t('dataset.common.visualConfigureStartAngle'), field: 'startAngle', type: 'SLIDER', value: -180, min: -360, max: 360, step: 1 }
const endAngle: ChartField = { label: this.$t('dataset.common.visualConfigureEndAngle'), field: 'endAngle', type: 'SLIDER', value: 0, min: -360, max: 360, step: 1 }
const dataBreakpoint: ChartField = {
label: this.$t('dataset.common.visualConfigureDataBreakpoint'),
field: 'dataBreakpoint',
values: [
{ label: this.$t('dataset.common.visualConfigureDataBreakpointBreak'), value: 'break' },
{ label: this.$t('dataset.common.visualConfigureDataBreakpointContinuous'), value: 'link' },
{ label: this.$t('dataset.common.visualConfigureDataBreakpointZero'), value: 'zero' },
{ label: this.$t('dataset.common.visualConfigureDataBreakpointIgnore'), value: 'ignore' }
]
}
const leftField: ChartField = { label: this.$t('dataset.common.visualConfigureCategoryLeftField'), field: 'leftField' }
const rightField: ChartField = { label: this.$t('dataset.common.visualConfigureCategoryRightField'), field: 'rightField' }
switch (type) {
case Type.RADAR:
case Type.BAR:
case Type.AREA:
case Type.SCATTER:
fields.push(categoryField, valueField)
break
case Type.FUNNEL:
fields.push(categoryField, valueField, showLegend)
break
case Type.GAUGE:
fields.push(categoryField, valueField, outerRadius, innerRadius, startAngle, endAngle)
break
case Type.PIE:
fields.push(categoryField, valueField, outerRadius)
break
case Type.LINE:
fields.push(categoryField, valueField, seriesField, dataBreakpoint)
break
case Type.HISTOGRAM:
fields.push(leftField, rightField, valueField)
break
case Type.WORDCLOUD:
fields.push(categoryField, valueField, seriesField)
break
}
return fields
} }
} }
}) })

View File

@ -1,67 +1,110 @@
<template> <template>
<div v-if="configuration && formState" class="space-y-2"> <Dialog :is-visible="visible" :title="$t('common.configure')" width="40%" @close="handlerCancel">
<FormField v-for="item in fields" :name="item.field as string"> <div v-if="configuration && formState" class="space-y-2 pl-3 pr-3">
<FormItem> <Tabs v-model="activeGroup">
<FormLabel>{{ item.label }}</FormLabel> <TabsList class="grid w-full grid-cols-2">
<FormControl> <TabsTrigger v-for="group in fieldGroup" :key="group.label" :value="group.label as string">{{ group.label }}</TabsTrigger>
<Switch v-if="item.type === 'SWITCH'" class="ml-2" :default-checked="formState[item.field as keyof IChart] as any" </TabsList>
@update:checked="formState[item.field as keyof IChart] = $event as any"/> <TabsContent :value="activeGroup as any" class="grid grid-cols-4 gap-4">
<Tooltip v-else-if="item.type === 'SLIDER'" :content="formState[item.field as keyof IChart] ? formState[item.field as keyof IChart] : [item.value] as any"> <FormField v-for="item in fieldGroup.find(value => value.label === activeGroup)?.fields" :name="item.field as string">
<Slider v-model="formState[item.field as keyof IChart] as any" class="ml-2 w-[95%]" :default-value="[item.value]" :min="item.min" :max="item.max" :step="item.step" <FormItem>
@update:modelValue="formState[item.field as keyof IChart] = $event as any"/> <FormLabel>{{ item.label }}</FormLabel>
</Tooltip> <FormControl>
<Select v-else v-model="formState[item.field as keyof IChart] as string" :disabled="configuration.headers.length === 0"> <div v-if="item.type === 'SWITCH'">
<SelectTrigger class="w-full"> <Switch class="mt-2" :value="item.value" :default-checked="formState[item.field as keyof IChart] ? formState[item.field as keyof IChart] as boolean : item.value"
<SelectValue :placeholder="`Select ${item.label}`"/> @update:checked="formState[item.field as keyof IChart] = $event as any"/>
</SelectTrigger> </div>
<SelectContent> <Tooltip v-else-if="item.type === 'SLIDER'" :content="formState[item.field as keyof IChart] ? formState[item.field as keyof IChart] : [item.value] as any">
<SelectItem v-if="item.values" class="cursor-pointer" v-for="data in item.values" :value="data.value as string">{{ data.label }}</SelectItem> <Slider v-model="formState[item.field as keyof IChart] as any" class="pt-3" :default-value="[item.value]" :min="item.min" :max="item.max"
<SelectItem v-else v-for="item in configuration.headers" class="cursor-pointer" :value="item as string">{{ item }}</SelectItem> :step="item.step"
</SelectContent> @update:modelValue="formState[item.field as keyof IChart] = $event as any"/>
</Select> </Tooltip>
</FormControl> <Input v-else-if="item.type === 'TEXT'" v-model="formState[item.field as keyof IChart] as string" :placeholder="item.label"
</FormItem> :disabled="item.disabled?.field ? formState[item.disabled?.field as keyof IChart] === item.disabled?.value : false"/>
</FormField> <Select v-else v-model="formState[item.field as keyof IChart] as string" :default-value="item.value"
</div> :disabled="item.disabled?.field ? formState[item.disabled?.field as keyof IChart] === item.disabled?.value : false">
<SelectTrigger class="w-full">
<SelectValue :placeholder="`Select ${item.label}`"/>
</SelectTrigger>
<SelectContent>
<SelectItem v-if="item.values" class="cursor-pointer" v-for="data in item.values" :value="data.value as string">{{ data.label }}</SelectItem>
<SelectItem v-else v-for="item in configuration.headers" class="cursor-pointer" :value="item as string">{{ item }}</SelectItem>
</SelectContent>
</Select>
</FormControl>
</FormItem>
</FormField>
</TabsContent>
</Tabs>
</div>
<template #footer>
<div class="space-x-5">
<Button variant="outline" size="sm" @click="handlerCancel">
{{ $t('common.cancel') }}
</Button>
<Button size="sm" @click="handlerSubmit">
{{ $t('common.apply') }}
</Button>
</div>
</template>
</Dialog>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue' import { defineComponent } from 'vue'
import { FormControl, FormDescription, FormField, FormItem, FormLabel } from '@/components/ui/form' import { FormControl, FormDescription, FormField, FormItem, FormLabel } from '@/components/ui/form'
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from '@/components/ui/select' import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from '@/components/ui/select'
import { ChartField, Configuration, IChart } from '@/views/components/visual/Configuration.ts' import { ChartFieldGroup, Configuration, IChart } from '@/views/components/visual/Configuration.ts'
import { cloneDeep, keys } from 'lodash' import { cloneDeep, keys } from 'lodash'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { Slider } from '@/components/ui/slider' import { Slider } from '@/components/ui/slider'
import Tooltip from '@/views/ui/tooltip' import Tooltip from '@/views/ui/tooltip'
import Dialog from '@/views/ui/dialog'
import Button from '@/views/ui/button'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Input } from '@/components/ui/input'
export default defineComponent({ export default defineComponent({
name: 'VisualConfigure', name: 'VisualConfigure',
components: { components: {
Input,
Tabs, TabsTrigger, TabsList, TabsContent,
Button,
Dialog,
Slider, Slider,
Switch, Switch,
SelectGroup, SelectTrigger, SelectContent, SelectItem, Select, SelectLabel, SelectValue, SelectGroup, SelectTrigger, SelectContent, SelectItem, Select, SelectLabel, SelectValue,
FormDescription, FormControl, FormLabel, FormField, FormItem, FormDescription, FormControl, FormLabel, FormField, FormItem,
Tooltip Tooltip
}, },
computed: {
visible: {
get(): boolean
{
return this.isVisible
},
set(value: boolean)
{
this.$emit('close', value)
}
}
},
props: { props: {
configuration: { configuration: {
type: Object as () => Configuration type: Object as () => Configuration
}, },
fields: { fieldGroup: {
type: Array as () => ChartField[], type: Array as () => ChartFieldGroup[],
default: [] default: []
} },
}, isVisible: {
watch: { type: Boolean
formState: {
handler: 'handlerCommit',
deep: true
} }
}, },
data() data()
{ {
return { return {
activeGroup: this.fieldGroup[0]?.label,
formState: null as IChart | null formState: null as IChart | null
} }
}, },
@ -72,18 +115,25 @@ export default defineComponent({
} }
else { else {
const obj = {} as any const obj = {} as any
this.fields.forEach(field => { this.fieldGroup.forEach(group => {
if (field.field) { group?.fields?.forEach(field => {
obj[field.field] = undefined if (field.field) {
} obj[field.field] = undefined
}
})
}) })
this.formState = obj this.formState = obj
} }
}, },
methods: { methods: {
handlerCommit() handlerCancel()
{
this.visible = false
},
handlerSubmit()
{ {
this.$emit('change', this.formState) this.$emit('change', this.formState)
this.handlerCancel()
} }
} }
}) })

View File

@ -53,7 +53,18 @@ export default defineComponent({
yField: this.configuration.chartConfigure?.yAxis, yField: this.configuration.chartConfigure?.yAxis,
seriesField: this.configuration.chartConfigure?.series, seriesField: this.configuration.chartConfigure?.series,
invalidType: this.configuration.chartConfigure?.invalidType invalidType: this.configuration.chartConfigure?.invalidType
} as any
if (this.configuration.chartConfigure) {
options.title = {
visible: this.configuration.chartConfigure?.titleVisible,
text: this.configuration.chartConfigure?.titleText,
subtext: this.configuration.chartConfigure?.titleSubText,
orient: this.configuration.chartConfigure?.titlePosition,
align: this.configuration.chartConfigure?.titleAlign
}
} }
if (!reset) { if (!reset) {
instance = new VChart(options, { dom: this.$refs.content as HTMLElement }) instance = new VChart(options, { dom: this.$refs.content as HTMLElement })
instance.renderAsync() instance.renderAsync()

View File

@ -11,9 +11,18 @@
<div v-else class="hidden flex-col md:flex"> <div v-else class="hidden flex-col md:flex">
<div class="flex-1 space-y-4 pt-6"> <div class="flex-1 space-y-4 pt-6">
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-6"> <div class="grid gap-4 md:grid-cols-2 lg:grid-cols-6">
<Card title-class="p-2 pl-4 pr-4" v-for="item in data"> <Card title-class="p-2 pl-4 pr-4" footer-class="p-2 pl-2 pr-4" body-class="p-4" v-for="item in data">
<template #title> <template #title>
<RouterLink :to="`/admin/dashboard/info/${item.id}/preview`" target="_blank">{{ item.name }}</RouterLink> <div class="flex space-x-1">
<div>
<RouterLink :to="`/admin/dashboard/preview/${item.code}`" target="_blank">{{ item.name }}</RouterLink>
</div>
<div>
<Tooltip :content="item.description">
<Info :size="18" class="cursor-pointer"/>
</Tooltip>
</div>
</div>
</template> </template>
<template #extra> <template #extra>
<DropdownMenu class="justify-items-end"> <DropdownMenu class="justify-items-end">
@ -22,7 +31,7 @@
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuItem class="cursor-pointer"> <DropdownMenuItem class="cursor-pointer">
<RouterLink :to="`/admin/dashboard/info/${item.id}`" target="_blank" class="flex items-center"> <RouterLink :to="`/admin/dashboard/info/${item.code}`" target="_blank" class="flex items-center">
<Pencil :size="15" class="mr-1"/> <Pencil :size="15" class="mr-1"/>
{{ $t('dashboard.common.modify') }} {{ $t('dashboard.common.modify') }}
</RouterLink> </RouterLink>
@ -34,8 +43,14 @@
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</template> </template>
<div class="text-2xl font-bold"></div> <div class="shadow-blackA7 w-full overflow-hidden rounded-md">
<p class="text-xs text-muted-foreground mt-2">{{ item.createTime }}</p> <AspectRatio :ratio="16 / 11">
<img class="h-full w-full object-cover" src="/static/images/dashboard.png" :alt="item.name"/>
</AspectRatio>
</div>
<template #footer>
<p class="text-xs text-muted-foreground text-right">{{ item.createTime }}</p>
</template>
</Card> </Card>
</div> </div>
<div v-if="data.length === 0" class="text-center"> <div v-if="data.length === 0" class="text-center">
@ -53,7 +68,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue' import { defineComponent } from 'vue'
import { Cog, Loader2, Pencil, Trash } from 'lucide-vue-next' import { Cog, Info, Loader2, Pencil, Trash } from 'lucide-vue-next'
import DashboardService from '@/services/dashboard' import DashboardService from '@/services/dashboard'
import { FilterModel } from '@/model/filter' import { FilterModel } from '@/model/filter'
import { PaginationModel, PaginationRequest } from '@/model/pagination' import { PaginationModel, PaginationRequest } from '@/model/pagination'
@ -64,15 +79,19 @@ import Card from '@/views/ui/card'
import Pagination from '@/views/ui/pagination' import Pagination from '@/views/ui/pagination'
import Button from '@/views/ui/button' import Button from '@/views/ui/button'
import { TableCaption } from '@/components/ui/table' import { TableCaption } from '@/components/ui/table'
import Tooltip from '@/views/ui/tooltip'
import { AspectRatio } from 'radix-vue'
export default defineComponent({ export default defineComponent({
name: 'DashboardHome', name: 'DashboardHome',
components: { components: {
AspectRatio,
Tooltip,
TableCaption, TableCaption,
Pagination, Pagination,
DashboardDelete, DashboardDelete,
DropdownMenuItem, DropdownMenuSeparator, DropdownMenuLabel, DropdownMenuContent, DropdownMenuTrigger, DropdownMenu, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuLabel, DropdownMenuContent, DropdownMenuTrigger, DropdownMenu,
Loader2, Cog, Trash, Pencil, Loader2, Cog, Trash, Pencil, Info,
Card, Card,
Button Button
}, },

View File

@ -15,7 +15,6 @@ import { ToastUtils } from '@/utils/toast'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { DashboardModel } from '@/model/dashboard' import { DashboardModel } from '@/model/dashboard'
import DashboardEditor from '@/views/pages/admin/dashboard/components/DashboardEditor.vue' import DashboardEditor from '@/views/pages/admin/dashboard/components/DashboardEditor.vue'
import { toNumber } from 'lodash'
export default defineComponent({ export default defineComponent({
name: 'DashboardInfo', name: 'DashboardInfo',
@ -37,10 +36,10 @@ export default defineComponent({
{ {
const router = useRouter() const router = useRouter()
const params = router.currentRoute.value.params const params = router.currentRoute.value.params
const id = params['id'] const code = params['code'] as string
if (id) { if (code) {
this.loading = true this.loading = true
DashboardService.getById(toNumber(id)) DashboardService.getByCode(code)
.then(response => { .then(response => {
if (response.status) { if (response.status) {
this.dataInfo = response.data this.dataInfo = response.data

View File

@ -2,6 +2,7 @@
<div class="w-full"> <div class="w-full">
<Loader2 v-if="loading" class="w-full justify-center animate-spin mt-10"/> <Loader2 v-if="loading" class="w-full justify-center animate-spin mt-10"/>
<DashboardView v-else-if="data" :layouts="JSON.parse(data.configure as string)"/> <DashboardView v-else-if="data" :layouts="JSON.parse(data.configure as string)"/>
<Alert v-else type="error" :title="$t('dashboard.tip.notFound').replace('$VALUE', $router.currentRoute?.value?.params['code'] as string)"/>
</div> </div>
</template> </template>
@ -12,10 +13,12 @@ import DashboardService from '@/services/dashboard'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import DashboardView from '@/views/pages/admin/dashboard/components/DashboardView.vue' import DashboardView from '@/views/pages/admin/dashboard/components/DashboardView.vue'
import { DashboardModel } from '@/model/dashboard' import { DashboardModel } from '@/model/dashboard'
import Alert from '@/views/ui/alert'
export default defineComponent({ export default defineComponent({
name: 'DashboardPreview', name: 'DashboardPreview',
components: { components: {
Alert,
DashboardView, DashboardView,
Loader2 Loader2
}, },
@ -36,7 +39,7 @@ export default defineComponent({
this.loading = true this.loading = true
const router = useRouter() const router = useRouter()
const params = router.currentRoute.value.params const params = router.currentRoute.value.params
DashboardService.getById(params['id'] as unknown as number) DashboardService.getByCode(params['code'] as string)
.then(response => { .then(response => {
if (response.status) { if (response.status) {
this.data = response.data this.data = response.data

View File

@ -1,71 +0,0 @@
<template>
<div>
<CircularLoading v-if="loading" :show="loading"/>
<Card v-else class="mt-3" v-for="node in data">
<CardHeader>
<CardTitle>{{ node.name }}</CardTitle>
</CardHeader>
<CardContent>
<div :draggable="true" @dragstart="onDragStart($event, node)">
<EchartsPreview :key="node.id" :height="'200px'" :id="node.id" :configure="JSON.parse(node.configure)"/>
</div>
</CardContent>
</Card>
</div>
</template>
<script lang="ts">
import '../style.css'
import { defineComponent } from 'vue'
import ReportService from '@/services/report'
import CircularLoading from '@/views/components/loading/CircularLoading.vue'
import { FilterModel } from '@/model/filter'
import EchartsPreview from '@/views/components/echarts/EchartsPreview.vue'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
interface Node {
id: number
name: string
configure: string
}
export default defineComponent({
name: 'DashboardChart',
components: {
CardContent, CardHeader, CardTitle, Card,
EchartsPreview,
CircularLoading
},
setup()
{
const onDragStart = (event: any, node: any) => {
if (event.dataTransfer) {
event.dataTransfer.setData('application/vueflow', JSON.stringify(node))
event.dataTransfer.effectAllowed = 'move'
}
}
return {
onDragStart
}
},
data()
{
return {
loading: false,
data: [] as Node[]
}
},
created()
{
this.loading = true
const filter: FilterModel = new FilterModel()
ReportService.getAll(filter)
.then((response) => {
if (response.status) {
this.data = response.data.content
}
})
.finally(() => this.loading = false)
}
});
</script>

View File

@ -32,21 +32,28 @@
</div> </div>
</Card> </Card>
<Dialog :is-visible="configureVisible" :title="$t('common.configure')"> <Dialog :is-visible="configureVisible" :title="$t('common.configure')">
<div v-if="formState" class="p-6"> <div v-if="formState" class="pl-3 pr-4">
<FormField name="name"> <FormField name="name">
<FormItem class="space-y-1"> <FormItem class="space-y-2">
<FormLabel>{{ $t('common.name') }}</FormLabel> <FormLabel>{{ $t('common.name') }}</FormLabel>
<FormMessage/> <FormMessage/>
<Input v-model="formState.name"/> <Input v-model="formState.name"/>
</FormItem> </FormItem>
</FormField> </FormField>
<FormField name="description">
<FormItem class="space-y-2">
<FormLabel>{{ $t('common.description') }}</FormLabel>
<FormMessage/>
<Textarea v-model="formState.description"/>
</FormItem>
</FormField>
</div> </div>
<template #footer> <template #footer>
<div class="space-x-5"> <div class="space-x-5">
<Button variant="outline" size="sm" @click="configureVisible = false"> <Button variant="outline" size="sm" @click="configureVisible = false">
{{ $t('common.cancel') }} {{ $t('common.cancel') }}
</Button> </Button>
<Button :loading="loading" size="sm" @click="handlerSave"> <Button :loading="loading" :disabled="loading" size="sm" @click="handlerSave">
{{ $t('common.save') }} {{ $t('common.save') }}
</Button> </Button>
</div> </div>
@ -72,10 +79,12 @@ import Button from '@/views/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form' import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
import { cloneDeep } from 'lodash' import { cloneDeep } from 'lodash'
import { Textarea } from '@/components/ui/textarea'
export default defineComponent({ export default defineComponent({
name: 'DashboardEditor', name: 'DashboardEditor',
components: { components: {
Textarea,
Input, Input,
ChartContainer, ChartContainer,
VisualView, VisualView,
@ -149,8 +158,8 @@ export default defineComponent({
.then(response => { .then(response => {
if (response.status) { if (response.status) {
ToastUtils.success(this.$t('dashboard.tip.publishSuccess').replace('$VALUE', <string>this.formState?.name)) ToastUtils.success(this.$t('dashboard.tip.publishSuccess').replace('$VALUE', <string>this.formState?.name))
if (response.data?.id) { if (response.data) {
this.$router.push(`/admin/dashboard/info/${ response.data.id }/preview`) this.$router.push(`/admin/dashboard/preview/${ response.data?.code }`)
} }
else { else {
this.$router.push('/console/dashboard') this.$router.push('/console/dashboard')

View File

@ -1,25 +0,0 @@
<template>
<div>
<NodeResizer :min-width="100" :min-height="30"/>
<EchartsPreview :width="'250px'" :height="'200px'" :id="id" :configure="configure"/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { NodeResizer } from '@vue-flow/node-resizer'
import EchartsPreview from '@/views/components/echarts/EchartsPreview.vue'
import { ChartConfigure } from '@/views/components/echarts/configure/ChartConfigure'
export default defineComponent({
name: 'DashboardNode',
components: {NodeResizer, EchartsPreview},
props: {
configure: {
type: Object as () => ChartConfigure | null
},
id: {
type: Number
}
}
});
</script>

View File

@ -293,6 +293,7 @@ export default defineComponent({
.then(response => { .then(response => {
if (response.status) { if (response.status) {
this.formState.name = response.data.name this.formState.name = response.data.name
this.formState.description = response.data.description
const query = JSON.parse(response.data.query) const query = JSON.parse(response.data.query)
this.mergeColumns(query.columns, this.metrics, ColumnType.METRIC) this.mergeColumns(query.columns, this.metrics, ColumnType.METRIC)
this.mergeColumns(query.columns, this.dimensions, ColumnType.DIMENSION) this.mergeColumns(query.columns, this.dimensions, ColumnType.DIMENSION)

View File

@ -3,8 +3,7 @@
<template #title>{{ title }}</template> <template #title>{{ title }}</template>
<CircularLoading v-if="loading" :show="loading"/> <CircularLoading v-if="loading" :show="loading"/>
<div v-else-if="info"> <div v-else-if="info">
<EchartsPreview v-if="info.type === 'QUERY'" :width="'100%'" :height="'300px'" :id="info.id" :configure="JSON.parse(info.configure as string)"/> <VisualView :code="info.dataset?.code" :configuration="JSON.parse(info.configure as string)" :query="JSON.parse(info.query as string)"/>
<VisualView v-else-if="info.type === 'DATASET'" :code="info.dataset?.code" :configuration="JSON.parse(info.configure as string)" :query="JSON.parse(info.query as string)"/>
</div> </div>
<template #footer> <template #footer>
<Button variant="outline" size="sm" @click="handlerCancel"> <Button variant="outline" size="sm" @click="handlerCancel">
@ -20,14 +19,12 @@ import { ReportModel } from '@/model/report'
import Dialog from '@/views/ui/dialog' import Dialog from '@/views/ui/dialog'
import Button from '@/views/ui/button' import Button from '@/views/ui/button'
import CircularLoading from '@/views/components/loading/CircularLoading.vue' import CircularLoading from '@/views/components/loading/CircularLoading.vue'
import EchartsPreview from '@/views/components/echarts/EchartsPreview.vue'
import VisualView from '@/views/components/visual/VisualView.vue' import VisualView from '@/views/components/visual/VisualView.vue'
export default defineComponent({ export default defineComponent({
name: 'ReportView', name: 'ReportView',
components: { components: {
VisualView, VisualView,
EchartsPreview,
CircularLoading, CircularLoading,
Dialog, Dialog,
Button Button

View File

@ -14,7 +14,7 @@
<CardContent :class="`${bodyClass}`"> <CardContent :class="`${bodyClass}`">
<slot/> <slot/>
</CardContent> </CardContent>
<CardFooter v-if="$slots.footer"> <CardFooter v-if="$slots.footer" :class="`border-t ${footerClass}`">
<slot name="footer"/> <slot name="footer"/>
</CardFooter> </CardFooter>
</Card> </Card>
@ -39,6 +39,9 @@ export default defineComponent({
bodyClass: { bodyClass: {
type: String type: String
}, },
footerClass: {
type: String
},
hiddenTitle: { hiddenTitle: {
type: Boolean, type: Boolean,
default: false default: false