Browse Source

init: 初始化看板组件库

liaojiaxing 1 year ago
parent
commit
166273de65
54 changed files with 531502 additions and 8582 deletions
  1. 11 0
      components/CodeEditor/index.ts
  2. 58 0
      components/CodeEditor/src/Editor.vue
  3. 48 0
      components/CodeEditor/src/index.vue
  4. 13 0
      components/charts/Bar/BasicBar/index.ts
  5. 17 0
      components/charts/Bar/BasicBar/src/BasicBar.vue
  6. 51 0
      components/charts/Bar/BasicBar/src/Config.vue
  7. 119 0
      components/charts/Bar/BasicBar/src/props.ts
  8. 798 0
      components/charts/Bar/BasicBar/src/styleFormData.ts
  9. 53 0
      components/charts/Charts.vue
  10. 161 0
      components/charts/DataConfig.vue
  11. 12 0
      components/charts/Line/BasicLine/index.ts
  12. 17 0
      components/charts/Line/BasicLine/src/BasicLine.vue
  13. 51 0
      components/charts/Line/BasicLine/src/Config.vue
  14. 115 0
      components/charts/Line/BasicLine/src/props.ts
  15. 798 0
      components/charts/Line/BasicLine/src/styleFormData.ts
  16. 7 0
      components/charts/chartEnum.ts
  17. 132 0
      components/charts/config/index.ts
  18. 102 0
      components/charts/hooks/useChartOptions.ts
  19. 100 0
      components/charts/hooks/useEcharts.ts
  20. 25 0
      components/charts/types.ts
  21. 70 0
      components/charts/utils/index.ts
  22. 39 0
      components/components.ts
  23. 7 0
      components/cusForm/index.ts
  24. 122 0
      components/cusForm/src/BackgroundSelect.vue
  25. 74 0
      components/cusForm/src/ColorScheme.vue
  26. 66 0
      components/cusForm/src/ColorSelect.vue
  27. 94 0
      components/cusForm/src/CusFormItem.vue
  28. 27 0
      components/cusForm/src/CusSlider.vue
  29. 86 0
      components/cusForm/src/FontStyle.vue
  30. 37 0
      components/cusForm/src/Position.vue
  31. 92 0
      components/cusForm/src/index.vue
  32. 52 0
      components/cusForm/src/type.ts
  33. 20 0
      components/index.ts
  34. 11 0
      components/text/Title/index.ts
  35. 53 0
      components/text/Title/src/Config.vue
  36. 32 0
      components/text/Title/src/index.vue
  37. 54 0
      components/text/Title/src/props.ts
  38. 1 0
      index.js
  39. 1 0
      lib/demo.html
  40. 258429 0
      lib/shalu-dashboard-ui.common.js
  41. 1 0
      lib/shalu-dashboard-ui.common.js.map
  42. 1 0
      lib/shalu-dashboard-ui.css
  43. 258440 0
      lib/shalu-dashboard-ui.umd.js
  44. 1 0
      lib/shalu-dashboard-ui.umd.js.map
  45. 19 0
      lib/shalu-dashboard-ui.umd.min.js
  46. 1 0
      lib/shalu-dashboard-ui.umd.min.js.map
  47. 27 7
      package.json
  48. 10844 0
      pnpm-lock.yaml
  49. 41 5
      src/App.vue
  50. 7 1
      src/main.ts
  51. 3 1
      tsconfig.json
  52. 1 0
      types/index.d.ts
  53. 61 0
      utils/transStyle.ts
  54. 0 8568
      yarn.lock

+ 11 - 0
components/CodeEditor/index.ts

@@ -0,0 +1,11 @@
+import CodeEditorModal from './src/index.vue';
+
+export type CodeEditorModalInstance = {
+  open: (code: string) => void;
+  close: () => void;
+  getCode: () => string;
+};
+
+export {
+  CodeEditorModal,
+}

+ 58 - 0
components/CodeEditor/src/Editor.vue

@@ -0,0 +1,58 @@
+<template>
+  <Codemirror
+    ref="editorRef"
+    placeholder="请输入"
+    style="height: 500px;"
+    :model-value="modelValue"
+    :tab-size="2"
+    :auto-focus="true"
+    :indent-with-tabs="true"
+    :extensions="[
+      oneDark,
+      javascript(),
+      json()
+    ]"
+    @change="handleCodeChange"
+  />
+</template>
+
+<script setup lang="ts">
+import { ref, defineProps, watch, defineEmits } from 'vue';
+import { Codemirror } from 'vue-codemirror';
+import { oneDark } from '@codemirror/theme-one-dark';
+import { javascript } from '@codemirror/lang-javascript';
+import { json } from '@codemirror/lang-json';
+import jsBeautify from 'js-beautify';
+
+const props = defineProps({
+  code: {
+    type: String,
+    default: ''
+  }
+});
+const emit = defineEmits(['update:code', 'change']);
+const editorRef = ref(null);
+const modelValue = ref(props.code);
+
+watch(
+  () => props.code,
+  (val) => {
+    modelValue.value = jsBeautify.js(val, { indent_size: 2 });
+  },
+  { immediate: true }
+)
+
+const handleCodeChange = (val: string) => {
+  try {
+    emit('update:code', val);
+    emit('change', val);
+  } catch (error) {
+    console.error(error);
+    return;
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 48 - 0
components/CodeEditor/src/index.vue

@@ -0,0 +1,48 @@
+<template>
+  <Modal
+    v-model:open="open"
+    :title="title"
+    :width="width"
+    @ok="handleOk"
+  >
+    <Editor v-model:code="code"/>
+  </Modal>
+</template>
+
+<script setup lang="ts">
+import { Modal  } from 'ant-design-vue';
+import { ref, defineProps, withDefaults, defineExpose, defineEmits } from 'vue';
+import Editor from './Editor.vue';
+
+interface IProp {
+  title?: string;
+  width?: number;
+}
+withDefaults(defineProps<IProp>(), {
+  title: '编辑',
+  width: 800,
+});
+const emit = defineEmits(['ok']);
+const open = ref(false);
+const code = ref('');
+
+const handleOk = () => {
+  // TODO: 检验code
+  emit('ok', code.value);
+  open.value = false;
+};
+
+defineExpose({
+  open: (codeStr: string) => {
+    open.value = true;
+    code.value = codeStr;
+  },
+  close: () => {
+    open.value = false;
+  },
+});
+</script>
+
+<style scoped>
+
+</style>

+ 13 - 0
components/charts/Bar/BasicBar/index.ts

@@ -0,0 +1,13 @@
+import BasicBar from './src/BasicBar.vue';
+import Config from './src/Config.vue';
+
+BasicBar.Config = Config;
+BasicBar.install = (app: any) => {
+  app.component('FmDashboardBasicBar', BasicBar);
+  return app;
+};
+
+export default BasicBar;
+export { Config };
+export { formItems as basicBarConfigFormItems } from './src/styleFormData';
+export { defaultPropsValue, basicBarProps } from './src/props';

+ 17 - 0
components/charts/Bar/BasicBar/src/BasicBar.vue

@@ -0,0 +1,17 @@
+<template>
+  <Charts :width="width" :height="height" :echarts-options="options" :loading="loading"></Charts>
+</template>
+
+<script setup lang="ts" name="fmDashboardBasicBar">
+import { defineProps } from 'vue';
+import Charts from '../../../Charts.vue';
+import { basicBarProps } from "./props";
+import { useChartOptions } from "../../../hooks/useChartOptions";
+
+const props = defineProps(basicBarProps);
+
+const { options, loading } = useChartOptions(props);
+
+</script>
+
+<style scoped></style>

+ 51 - 0
components/charts/Bar/BasicBar/src/Config.vue

@@ -0,0 +1,51 @@
+<template>
+  <div class="chart-config">
+    <div class="config-tab">
+      <Tabs v-model:activeKey="activeTab" size="small" centered>
+        <TabPane key="1">
+          <template #tab>
+            <DatabaseOutlined />
+            <span>数据设置</span>
+          </template>
+        </TabPane>
+        <TabPane key="2">
+          <template #tab>
+            <SkinOutlined />
+            <span>样式设置</span>
+          </template>
+        </TabPane>
+      </Tabs>
+    </div>
+
+    <DataConfig v-if="activeTab === '1'" :dataSource="dataSource" @change="handleChange"/>
+    <CusForm v-if="activeTab === '2'" :columns="formItems"/>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, defineProps, defineEmits } from 'vue';
+import { Tabs, TabPane } from 'ant-design-vue';
+import { DatabaseOutlined, SkinOutlined } from '@ant-design/icons-vue';
+import DataConfig from '../../../DataConfig.vue';
+import { CusForm } from '../../../../cusForm';
+import { basicBarProps } from './props';
+import { formItems } from './styleFormData';
+
+const props = defineProps(basicBarProps);
+const activeTab = ref('1');
+const emit = defineEmits(['change']);
+
+const handleChange = (data: any) => {
+  emit('change', {
+    ...props,
+    dataSource: data,
+  });
+}
+</script>
+
+<style lang="less" scoped>
+.config-tab {
+  text-align: center;
+  margin-bottom: 12px;
+}
+</style>

+ 119 - 0
components/charts/Bar/BasicBar/src/props.ts

@@ -0,0 +1,119 @@
+import { type PropType } from "vue";
+import { EChartsOption } from "echarts";
+import { getNormalizedChart, dataSource } from "../../../utils";
+import { DataSourceType } from "../../../chartEnum";
+
+export const basicBarProps = {
+  width: {
+    type: Number as PropType<number>,
+    default: 400,
+  },
+  height: {
+    type: Number as PropType<number>,
+    default: 260,
+  },
+  dataSource,
+  // 标题
+  title: {
+    type: Object as PropType<EChartsOption["title"]>,
+  },
+  // 图例
+  legend: {
+    type: Object as PropType<EChartsOption["legend"]>,
+  },
+  // 背景
+  backgroundColor: {
+    type: String as PropType<string>,
+  },
+  // 边框
+  grid: {
+    type: Object as PropType<EChartsOption["grid"]>,
+  },
+  // 提示框
+  tooltip: {
+    type: Object as PropType<EChartsOption["tooltip"]>,
+  },
+  // x轴数据
+  xAxis: {
+    type: Object as PropType<EChartsOption["xAxis"]>,
+  },
+  // y轴数据
+  yAxis: {
+    type: Object as PropType<EChartsOption["yAxis"]>,
+  },
+  // 折线
+  series: {
+    type: Array as PropType<EChartsOption["series"]>,
+  },
+  // 数据集
+  dataset: {
+    type: Object as PropType<EChartsOption["dataset"]>,
+  },
+  // color
+  color: {
+    type: Object as PropType<EChartsOption["color"]>
+  }
+};
+
+const chartOptions = getNormalizedChart({
+  title: {
+    text: "柱状图标题",
+  },
+  xAxis: {
+    data: ['轴标签A', '轴标签B', '轴标签C', '轴标签D']
+  },
+})
+
+export const defaultPropsValue: EChartsOption = {
+  // 组件容器默认属性
+  container: {
+    props: {
+      width: 400,
+      height: 260,
+    },
+  },
+  // 图表默认属性
+  props: {
+    // 数据源
+    dataSource: {
+      sourceType: DataSourceType.STATIC,
+      data: {
+        xData: ['轴标签A', '轴标签B', '轴标签C', '轴标签D'],
+        series: [
+          {
+            type: 'bar',
+            name: '系列1',
+            data: [89.3, 92.1, 94.4, 85.4]
+          },
+          {
+            type: 'bar',
+            name: '系列2',
+            data: [95.8, 89.4, 91.2, 76.9]
+          },
+        ]
+      },
+      url: location.origin + "/mock/api/get/example/bar",
+      method: "POST",
+      params: {},
+      headers: {},
+      refreshTime: 0,
+      dataProcess: `
+        (res) => {
+          // 取出列表
+          const data = res.data;
+          // x轴数据
+          const xData = data.map((item) => item.name); 
+          // 系列数据
+          const series = [
+            { type: 'bar', name: '价格', data: data.map(item => item.price) },
+            { type: 'bar', name: '总量', data: data.map(item => item.count) },
+          ];
+
+          // 返回图表数据
+          return { xData, series };
+        }
+      `
+    },
+    ...chartOptions
+  },
+};

+ 798 - 0
components/charts/Bar/BasicBar/src/styleFormData.ts

@@ -0,0 +1,798 @@
+import { IFormItem } from "../../../../cusForm";
+
+export const formItems: IFormItem[] = [
+  {
+    label: "标题",
+    prop: "title",
+    type: "group",
+    children: [
+      {
+        label: " ",
+        prop: "showTitle",
+        type: "checkboxGroup",
+        fieldProps: {
+          options: [{ label: "标题可见", value: true }],
+        },
+        defaultValue: []
+      },
+      {
+        label: "文本",
+        prop: "titleText",
+        type: "input",
+        defaultValue: "图表标题"
+      },
+      {
+        label: "位置",
+        prop: "titlePosition",
+        type: "position",
+        defaultValue: "center"
+      },
+      {
+        label: "样式",
+        prop: "titleStyle",
+        type: "fontStyle",
+        defaultValue: {
+          size: 18,
+          bold: true,
+          italic: false,
+          underline: false,
+        }
+      },
+      {
+        label: "背景",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "填充",
+        prop: "titleBackground",
+        type: "backgroundSelect",
+        fieldProps: {
+          filterOptions: ["image"],
+        },
+        defaultValue: {
+          type: "color",
+          color: "#fff",
+        }
+      },
+      {
+        label: "透明度",
+        prop: "titleOpacity",
+        type: "slider",
+        defaultValue: 100
+      },
+      {
+        label: "圆角",
+        prop: "titleBorderRadius",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 0
+      },
+    ],
+  },
+  {
+    label: "图例",
+    prop: "legend",
+    type: "group",
+    children: [
+      {
+        label: " ",
+        prop: "showLegend",
+        type: "checkboxGroup",
+        fieldProps: {
+          options: [{ label: "图例可见", value: true }],
+        },
+        defaultValue: [true]
+      },
+      {
+        label: "位置",
+        prop: "legendPosition",
+        type: "position",
+        fieldProps: {
+          type: "round",
+        },
+        defaultValue: "top"
+      },
+      {
+        label: "样式",
+        prop: "legendStyle",
+        type: "fontStyle",
+        defaultValue: {
+          size: 12,
+          bold: false,
+          italic: false,
+          underline: false,
+        }
+      },
+      {
+        label: "边框",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "线宽",
+        prop: "legendBorderWidth",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 0
+      },
+      {
+        label: "颜色",
+        prop: "legendBorderColor",
+        type: "colorSelect",
+        defaultValue: "#ccc"
+      },
+      {
+        label: "圆角",
+        prop: "legendBorderRadius",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 0
+      },
+      {
+        label: "背景",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "背景",
+        prop: "legendBackground",
+        type: "backgroundSelect",
+        fieldProps: {
+          filterOptions: ["image"],
+        },
+        defaultValue: {
+          type: "color",
+          color: "#fff",
+        }
+      },
+      {
+        label: "背景透明度",
+        prop: "legendBackgroudOpacity",
+        type: "slider",
+        defaultValue: 100
+      },
+      {
+        label: "背景圆角",
+        prop: "legendBackgroundRadius",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 0
+      },
+      {
+        label: "阴影",
+        prop: "legendShadow",
+        type: "radioGroup",
+        fieldProps: {
+          options: [
+            { label: "开启", value: true },
+            { label: "关闭", value: false },
+          ],
+        },
+        defaultValue: false
+      },
+    ],
+  },
+  {
+    label: "标签",
+    prop: "label",
+    type: "group",
+    children: [
+      {
+        label: " ",
+        prop: "showLabel",
+        type: "checkboxGroup",
+        fieldProps: {
+          options: [{ label: "标签可见", value: true }],
+        },
+        defaultValue: false
+      },
+      {
+        label: "文本",
+        prop: "labelValueType",
+        type: "checkboxGroup",
+        fieldProps: {
+          options: [
+            { label: "分类名", value: 'category' },
+            { label: "系列名", value: 'serie' },
+            { label: "数值", value: 'value' },
+            { label: "百分比", value: 'percent' },
+          ]
+        },
+        defaultValue: ['value']
+      },
+      {
+        label: "格式化",
+        prop: "labelFormatter",
+        type: "input",
+        tip: "支持字符串模板和回调函数",
+        defaultValue: "{value}"
+      },
+      {
+        label: "颜色",
+        prop: "labelColor",
+        type: "colorSelect",
+        defaultValue: "#000"
+      },
+      {
+        label: "样式",
+        prop: "labelStyle",
+        type: "fontStyle",
+        defaultValue: {
+          size: 12,
+          bold: false,
+          italic: false,
+          underline: false,
+        }
+      },
+      {
+        label: "布局",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "位置",
+        prop: "labelPosition",
+        type: "radioGroup",
+        fieldProps: {
+          options: [
+            { label: "内部", value: "inside" },
+            { label: "外部", value: "outside" },
+            { label: "中间", value: "center" },
+          ],
+        },
+        defaultValue: "outside"
+      },
+      {
+        label: "文本方向",
+        prop: "labelDirection",
+        type: "radioGroup",
+        fieldProps: {
+          options: [
+            { label: "水平", value: "horizontal" },
+            { label: "垂直", value: "vertical" },
+          ],
+        },
+        defaultValue: "horizontal"
+      },
+      {
+        label: "边框",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "线宽",
+        prop: "labelBorderWidth",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 0
+      },
+      {
+        label: "颜色",
+        prop: "labelBorderColor",
+        type: "colorSelect",
+        defaultValue: "#ccc"
+      },
+      {
+        label: "圆角",
+        prop: "labelBorderRadius",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 0
+      },
+    ],
+  },
+  {
+    label: "系列",
+    prop: "serie",
+    type: "group",
+    children: [
+      {
+        label: "配色",
+        prop: "colorScheme",
+        type: "colorScheme",
+        defaultValue: "custom"
+      },
+      {
+        label: "样式",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "固定柱宽",
+        prop: "serieBarFixedWidth",
+        type: "radioGroup",
+        fieldProps: {
+          options: [
+            { label: "是", value: true },
+            { label: "否", value: false },
+          ],
+        },
+        defaultValue: false
+      },
+      {
+        label: "系列间隔",
+        prop: "serieGap",
+        type: "slider",
+        defaultValue: 20
+      },
+      {
+        label: "分类间隔",
+        prop: "categoryGap",
+        type: "slider",
+        defaultValue: 20
+      },
+      {
+        label: "边框",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "线宽",
+        prop: "serieBorderWidth",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 0
+      },
+      {
+        label: "颜色",
+        prop: "serieBorderColor",
+        type: "colorSelect",
+        defaultValue: "#ccc"
+      },
+      {
+        label: "圆角",
+        prop: "serieBorderRadius",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 0
+      },
+    ]
+  },
+  {
+    label: "X 轴",
+    prop: "xAxis",
+    type: "group",
+    children: [
+      {
+        label: "类型",
+        prop: "xAxisType",
+        type: "select",
+        fieldProps: {
+          options: [
+            { label: "类目坐标轴", value: "category" },
+            { label: "数值坐标轴", value: "value" },
+            { label: "时间坐标轴", value: "time" },
+          ],
+        },
+        defaultValue: "category"
+      },
+      {
+        label: "轴标题",
+        prop: "xAliasShowTitle",
+        type: "radioGroup",
+        fieldProps: {
+          options: [
+            { label: "显示", value: true },
+            { label: "隐藏", value: false },
+          ],
+        },
+        defaultValue: false
+      },
+      {
+        label: "标题内容",
+        prop: "xAliasTitle",
+        type: "input",
+        defaultValue: "X 轴标题"
+      },
+      {
+        label: "标题位置",
+        prop: "xAliasPosition",
+        type: "position",
+        defaultValue: "center"
+      },
+      {
+        label: "标题样式",
+        prop: "xAliasStyle",
+        type: "fontStyle",
+        defaultValue: {
+          size: 12,
+          bold: false,
+          italic: false,
+          underline: false,
+        }
+      },
+      {
+        label: "轴线",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "线宽",
+        prop: "xAliasLineWidth",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 1
+      },
+      {
+        label: "颜色",
+        prop: "xAliasLineColor",
+        type: "colorSelect",
+        defaultValue: "#ccc"
+      },
+      {
+        label: "刻度",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "显示刻度",
+        prop: "xAliasTickShow",
+        type: "radioGroup",
+        fieldProps: {
+          options: [
+            { label: "显示", value: true },
+            { label: "隐藏", value: false },
+          ],
+        },
+        defaultValue: true
+      },
+      {
+        label: "刻度长度",
+        prop: "xAliasTickLength",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 5
+      },
+      {
+        label: "刻度颜色",
+        prop: "xAliasTickColor",
+        type: "colorSelect",
+        defaultValue: "#ccc"
+      },
+      {
+        label: "标签",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "颜色",
+        prop: "xAliasLabelColor",
+        type: "colorSelect",
+        defaultValue: "#000"
+      },
+      {
+        label: "样式",
+        prop: "xAliasLabelStyle",
+        type: "fontStyle",
+        defaultValue: {
+          size: 12,
+          bold: false,
+          italic: false,
+          underline: false,
+        }
+      },
+    ]
+  },
+  {
+    label: "Y 轴",
+    prop: "yAxis",
+    type: "group",
+    children: [
+      {
+        label: "轴标题",
+        prop: "yAliasShowTitle",
+        type: "radioGroup",
+        fieldProps: {
+          options: [
+            { label: "显示", value: true },
+            { label: "隐藏", value: false },
+          ],
+        },
+        defaultValue: false
+      },
+      {
+        label: "标题内容",
+        prop: "yAliasTitle",
+        type: "input",
+        defaultValue: "Y 轴标题"
+      },
+      {
+        label: "标题位置",
+        prop: "yAliasPosition",
+        type: "position",
+        defaultValue: "center"
+      },
+      {
+        label: "标题样式",
+        prop: "yAliasStyle",
+        type: "fontStyle",
+        defaultValue: {
+          size: 12,
+          bold: false,
+          italic: false,
+          underline: false,
+        }
+      },
+      {
+        label: "轴线",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "线宽",
+        prop: "yAliasLineWidth",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 1
+      },
+      {
+        label: "颜色",
+        prop: "yAliasLineColor",
+        type: "colorSelect",
+        defaultValue: "#ccc"
+      },
+      {
+        label: "刻度",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "显示刻度",
+        prop: "yAliasTickShow",
+        type: "radioGroup",
+        fieldProps: {
+          options: [
+            { label: "显示", value: true },
+            { label: "隐藏", value: false },
+          ],
+        },
+        defaultValue: true
+      },
+      {
+        label: "刻度长度",
+        prop: "yAliasTickLength",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 5
+      },
+      {
+        label: "刻度颜色",
+        prop: "yAliasTickColor",
+        type: "colorSelect",
+        defaultValue: "#ccc"
+      },
+      {
+        label: "标签",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "颜色",
+        prop: "yAliasLabelColor",
+        type: "colorSelect",
+        defaultValue: "#000"
+      },
+      {
+        label: "样式",
+        prop: "yAliasLabelStyle",
+        type: "fontStyle",
+        defaultValue: {
+          size: 12,
+          bold: false,
+          italic: false,
+          underline: false,
+        }
+      },
+    ]
+  },
+  {
+    label: "提示",
+    prop: "tooltip",
+    type: "group",
+    children: [
+      {
+        label: " ",
+        prop: "showTooltip",
+        type: "checkboxGroup",
+        fieldProps: {
+          options: [
+            { label: "提示可见", value: true },
+          ],
+        },
+        defaultValue: [true]
+      },
+      {
+        label: "文本",
+        prop: "tooltipValueType",
+        type: "checkboxGroup",
+        fieldProps: {
+          options: [
+            { label: "分类名", value: 'category' },
+            { label: "系列名", value: 'serie' },
+            { label: "数值", value: 'value' },
+            { label: "百分比", value: 'percent' },
+          ]
+        },
+        defaultValue: ['value']
+      },
+      {
+        label: "格式化",
+        prop: "tooltipFormatter",
+        type: "input",
+        tip: "支持字符串模板和回调函数",
+        defaultValue: "{value}"
+      },
+      {
+        label: "样式",
+        prop: "tooltipStyle",
+        type: "fontStyle",
+        defaultValue: {
+          size: 12,
+          bold: false,
+          italic: false,
+          underline: false,
+        }
+      },
+      {
+        label: "边框",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "线宽",
+        prop: "tooltipBorderWidth",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 1
+      },
+      {
+        label: "颜色",
+        prop: "tooltipBorderColor",
+        type: "colorSelect",
+        defaultValue: "#ccc"
+      },
+      {
+        label: "圆角",
+        prop: "tooltipBorderRadius",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 0
+      },
+      {
+        label: "背景",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "填充",
+        prop: "tooltipBackground",
+        type: "backgroundSelect",
+        fieldProps: {
+          filterOptions: ["image"],
+        },
+        defaultValue: {
+          type: "color",
+          color: "#fff",
+        }
+      },
+      {
+        label: "背景透明度",
+        prop: "tooltipBackgroudOpacity",
+        type: "slider",
+        defaultValue: 100
+      },
+      {
+        label: "阴影",
+        prop: "tooltipShadow",
+        type: "radioGroup",
+        fieldProps: {
+          options: [
+            { label: "开启", value: true },
+            { label: "关闭", value: false },
+          ],
+        },
+        defaultValue: false
+      },
+    ]
+  },
+  {
+    label: "背景",
+    prop: "background",
+    type: "group",
+    children: [
+      {
+        label: "边框",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "线宽",
+        prop: "backgroundBorderWidth",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 0
+      },
+      {
+        label: "颜色",
+        prop: "backgroundBorderColor",
+        type: "colorSelect",
+        defaultValue: "#ccc"
+      },
+      {
+        label: "圆角",
+        prop: "backgroundBorderRadius",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 0
+      },
+      {
+        label: "背景",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "填充",
+        prop: "backgroundBackground",
+        type: "backgroundSelect",
+        fieldProps: {
+          filterOptions: ["image"],
+        },
+        defaultValue: {
+          type: "color",
+          color: "#fff",
+        }
+      },
+      {
+        label: "背景透明度",
+        prop: "backgroundBackgroudOpacity",
+        type: "slider",
+        defaultValue: 100
+      },
+      {
+        label: "阴影",
+        prop: "backgroundShadow",
+        type: "radioGroup",
+        fieldProps: {
+          options: [
+            { label: "开启", value: true },
+            { label: "关闭", value: false },
+          ],
+        },
+        defaultValue: false
+      },
+    ]
+  }
+];

+ 53 - 0
components/charts/Charts.vue

@@ -0,0 +1,53 @@
+<!-- charts基础组件 -->
+<template>
+  <Spin :spinning="loading" :indicator="indicator">
+    <div ref="chartRef" :style="{width: width + 'px', height: height + 'px'}"></div>
+  </Spin>
+</template>
+
+<script setup lang="ts">
+import { ref, Ref, defineProps, watch, nextTick, h } from "vue";
+import { Spin } from "ant-design-vue";
+import { LoadingOutlined } from "@ant-design/icons-vue";
+import { useEcharts } from "./hooks/useEcharts";
+import type { EChartsOption } from "echarts";
+import { throttle } from "lodash";
+
+const props = defineProps<{
+  echartsOptions: EChartsOption;
+  width: number;
+  height: number;
+  loading?: boolean;
+}>();
+const chartRef: Ref<null | HTMLDivElement> = ref(null);
+const { setOptions, resize } = useEcharts(chartRef as Ref<HTMLDivElement>);
+
+const indicator = h(LoadingOutlined, {
+  style: {
+    fontSize: "24px",
+  },
+});
+
+watch(
+  () => [props.width, props.height],
+  throttle(async () => {
+    resize();
+  }, 200)
+);
+
+watch(
+  () => props,
+  async () => {
+    await nextTick();
+    const { echartsOptions } = props;
+
+    setOptions(echartsOptions);
+  },
+  {
+    immediate: true,
+    deep: true,
+  }
+);
+</script>
+
+<style scoped></style>

+ 161 - 0
components/charts/DataConfig.vue

@@ -0,0 +1,161 @@
+<template>
+  <Form
+    size="small"
+    layout="horizontal"
+    :model="formModel"
+    :label-col="{ span: 8 }"
+    :wrapper-col="{ span: 16 }"
+  >
+    <Form.Item label="类型" name="sourceType">
+      <Select v-model:value="formModel.sourceType">
+        <SelectOption :value="DataSourceType.STATIC">静态数据</SelectOption>
+        <SelectOption :value="DataSourceType.API">动态数据</SelectOption>
+      </Select>
+    </Form.Item>
+    <!-- 静态数据 -->
+    <template v-if="formModel.sourceType === DataSourceType.STATIC">
+      <Form.Item label="数据" name="data">
+        <Button type="default" size="small" @click="handleEditData('data')"
+          >编辑</Button
+        >
+      </Form.Item>
+    </template>
+    <!-- 接口 -->
+    <template v-else-if="formModel.sourceType === DataSourceType.API">
+      <Form.Item label="接口地址" name="url">
+        <Input.TextArea
+          :auto-size="{ minRows: 5 }"
+          placeholder="请输入接口地址"
+          v-model:value="formModel.url"
+        ></Input.TextArea>
+      </Form.Item>
+      <Form.Item label="请求方式" name="method">
+        <RadioGroup v-model:value="formModel.method">
+          <Radio value="GET">GET</Radio>
+          <Radio value="POST">POST</Radio>
+        </RadioGroup>
+      </Form.Item>
+      <Form.Item label="刷新时间" name="refreshTime">
+        <InputNumber
+          v-model:value="formModel.refreshTime"
+          :step="1"
+          style="width: 100%"
+          @change="handleRefreshTimeChange"
+        >
+          <template #addonAfter>
+            <span class="text-gray-500">秒</span>
+          </template>
+        </InputNumber>
+      </Form.Item>
+      <Form.Item label="请求参数" name="params">
+        <Button type="default" size="small" @click="handleEditData('params')"
+          >编辑</Button
+        >
+      </Form.Item>
+      <Form.Item label="请求头" name="headers">
+        <Button type="default" size="small" @click="handleEditData('headers')"
+          >编辑</Button
+        >
+      </Form.Item>
+      <Form.Item label="数据处理" name="dataProcess">
+        <Button
+          type="default"
+          size="small"
+          @click="handleEditData('dataProcess')"
+          >编辑</Button
+        >
+      </Form.Item>
+    </template>
+  </Form>
+  <CodeEditorModal ref="codeEditorRef" title="编辑数据" @ok="handleCodeSave" />
+</template>
+
+<script setup lang="ts">
+import type { DataSource } from "./types";
+import type { Ref } from "vue";
+import { ref, defineProps, watch, defineEmits } from "vue";
+import {
+  Form,
+  Input,
+  Button,
+  InputNumber,
+  Select,
+  SelectOption,
+  RadioGroup,
+  Radio
+} from "ant-design-vue";
+import { DataSourceType } from "./chartEnum";
+import {
+  CodeEditorModal,
+  type CodeEditorModalInstance,
+} from "../CodeEditor";
+
+/**
+ * 通用数据data约定内容结构
+ * {
+ *  xData: ['x轴标签A', 'x轴标签B', 'x轴标签C', 'x轴标签D'],
+ *  yData: ['y轴数据A', 'y轴数据B', 'y轴数据C', 'y轴数据D'],
+ *  // 根据不同类型的图表配置不同的series
+ *  series: [
+ *    {
+ *      name: '系列A',
+ *      data: [10, 20, 30, 40]
+ *    },
+ *    {
+ *      name: '系列B',
+ *      data: [10, 20, 30, 40]
+ *    }
+ *  ]
+ * }
+ */
+const emit = defineEmits(["change"]);
+const props = defineProps<{
+  dataSource: DataSource;
+}>();
+const formModel = ref({
+  sourceType: DataSourceType.STATIC,
+  // 静态数据相关
+  data: "",
+  // 接口相关
+  url: '',
+  method: "",
+  params: {},
+  headers: {},
+  refreshTime: 0,
+  // 数据处理
+  dataProcess: "",
+});
+
+const handleRefreshTimeChange = (val: unknown) => {
+  formModel.value.refreshTime = val === 0 || val as number >= 60 ? val as number : 60;
+};
+
+/* =====================编辑代码======================= */
+let pathKey: "data" | "params" | "headers" | "dataProcess";
+const codeEditorRef = ref<Ref<CodeEditorModalInstance> | null>(null);
+const handleEditData = (key: "data" | "params" | "headers" | "dataProcess") => {
+  pathKey = key;
+  codeEditorRef.value?.open(JSON.stringify(formModel.value[key]));
+};
+const handleCodeSave = (code: string) => {
+  formModel.value[pathKey] = JSON.parse(code);
+};
+
+watch(
+  () => props.dataSource,
+  (val) => {
+    Object.assign(formModel.value, val || {});
+  },
+  { immediate: true }
+);
+
+watch(
+  () => formModel.value,
+  (val) => {
+    emit("change", val);
+  },
+  { deep: true }
+);
+</script>
+
+<style scoped></style>

+ 12 - 0
components/charts/Line/BasicLine/index.ts

@@ -0,0 +1,12 @@
+import BasicLine from './src/BasicLine.vue';
+import Config from './src/Config.vue';
+
+BasicLine.Config = Config;
+BasicLine.install = (app: any) => {
+  app.component('FmDashboardBasicLine', BasicLine);
+  return app;
+};
+export default BasicLine;
+export { Config };
+export { formItems as basicLineConfigFormItems } from './src/styleFormData';
+export { defaultPropsValue, basicLineProps } from './src/props';

+ 17 - 0
components/charts/Line/BasicLine/src/BasicLine.vue

@@ -0,0 +1,17 @@
+<template>
+  <Charts :width="width" :height="height" :echarts-options="options" :loading="loading"></Charts>
+</template>
+
+<script setup lang="ts">
+import { defineProps } from "vue";
+import Charts from '../../../Charts.vue';
+import { basicLineProps } from "./props";
+import { useChartOptions } from "../../../hooks/useChartOptions";
+
+const props = defineProps(basicLineProps);
+
+const { options, loading } = useChartOptions(props);
+
+</script>
+
+<style scoped></style>

+ 51 - 0
components/charts/Line/BasicLine/src/Config.vue

@@ -0,0 +1,51 @@
+<template>
+  <div class="chart-config">
+    <div class="config-tab">
+      <Tabs v-model:activeKey="activeTab" size="small" centered>
+        <TabPane key="1">
+          <template #tab>
+            <DatabaseOutlined />
+            <span>数据设置</span>
+          </template>
+        </TabPane>
+        <TabPane key="2">
+          <template #tab>
+            <SkinOutlined />
+            <span>样式设置</span>
+          </template>
+        </TabPane>
+      </Tabs>
+    </div>
+
+    <DataConfig v-if="activeTab === '1'" :dataSource="dataSource" @change="handleChange"/>
+    <CusForm v-if="activeTab === '2'" :columns="formItems"/>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, defineProps, defineEmits } from 'vue';
+import { Tabs, TabPane } from 'ant-design-vue';
+import { DatabaseOutlined, SkinOutlined } from '@ant-design/icons-vue';
+import DataConfig from '../../../DataConfig.vue';
+import { CusForm } from '../../../../cusForm';
+import { basicLineProps } from './props';
+import { formItems } from './styleFormData';
+
+const props = defineProps(basicLineProps);
+const activeTab = ref('1');
+const emit = defineEmits(['change']);
+
+const handleChange = (data: any) => {
+  emit('change', {
+    ...props,
+    dataSource: data,
+  });
+}
+</script>
+
+<style lang="less" scoped>
+.config-tab {
+  text-align: center;
+  margin-bottom: 12px;
+}
+</style>

+ 115 - 0
components/charts/Line/BasicLine/src/props.ts

@@ -0,0 +1,115 @@
+import { type PropType } from "vue";
+import { EChartsOption } from "echarts";
+import { getNormalizedChart } from "../../../utils";
+import { dataSource } from "../../../utils";
+import { DataSourceType } from "../../../chartEnum";
+
+export const basicLineProps = {
+  width: {
+    type: Number as PropType<number>,
+    default: 400,
+  },
+  height: {
+    type: Number as PropType<number>,
+    default: 260,
+  },
+  dataSource,
+  // 标题
+  title: {
+    type: Object as PropType<EChartsOption["title"]>,
+  },
+  // 图例
+  legend: {
+    type: Object as PropType<EChartsOption["legend"]>,
+  },
+  // 背景
+  backgroundColor: {
+    type: String as PropType<string>,
+  },
+  // 边框
+  grid: {
+    type: Object as PropType<EChartsOption["grid"]>,
+  },
+  // 提示框
+  tooltip: {
+    type: Object as PropType<EChartsOption["tooltip"]>,
+  },
+  // x轴数据
+  xAxis: {
+    type: Object as PropType<EChartsOption["xAxis"]>,
+  },
+  // y轴数据
+  yAxis: {
+    type: Object as PropType<EChartsOption["yAxis"]>,
+  },
+  // 折线
+  series: {
+    type: Array as PropType<EChartsOption["series"]>,
+  },
+  // 数据集
+  dataset: {
+    type: Object as PropType<EChartsOption["dataset"]>,
+  },
+};
+
+const chartOptions = getNormalizedChart({
+  title: {
+    text: "折线图标题",
+  },
+  xAxis: {
+    data: ['轴标签A', '轴标签B', '轴标签C', '轴标签D']
+  },
+});
+
+export const defaultPropsValue: EChartsOption = {
+  container: {
+    props: {
+      width: 400,
+      height: 260,
+    },
+  },
+  props: {
+    // 数据源
+    dataSource: {
+      sourceType: DataSourceType.STATIC,
+      data: {
+        xData: ['轴标签A', '轴标签B', '轴标签C', '轴标签D'],
+        series: [
+          {
+            type: 'line',
+            name: '系列1',
+            data: [89.3, 92.1, 94.4, 85.4]
+          },
+          {
+            type: 'line',
+            name: '系列2',
+            data: [95.8, 89.4, 91.2, 76.9]
+          },
+        ]
+      },
+      url: location.origin + "/mock/api/get/example/line",
+      method: "POST",
+      params: {},
+      headers: {},
+      refreshTime: 0,
+      dataProcess: `
+        (res) => {
+          // 取出列表
+          const data = res.data;
+          // x轴数据
+          const xData = data.map((item) => item.name); 
+          // 系列数据
+          const series = [
+            { type: 'line', name: '苹果', data: data.map(item => item.apple) },
+            { type: 'line', name: 'VIVO', data: data.map(item => item.vivo) },
+            { type: 'line', name: '小米', data: data.map(item => item.mi) },
+          ];
+
+          // 返回图表数据
+          return { xData, series };
+        }
+      `
+    },
+    ...chartOptions
+  },
+};

+ 798 - 0
components/charts/Line/BasicLine/src/styleFormData.ts

@@ -0,0 +1,798 @@
+import { IFormItem } from "../../../../cusForm";
+
+export const formItems: IFormItem[] = [
+  {
+    label: "标题",
+    prop: "title",
+    type: "group",
+    children: [
+      {
+        label: " ",
+        prop: "showTitle",
+        type: "checkboxGroup",
+        fieldProps: {
+          options: [{ label: "标题可见", value: true }],
+        },
+        defaultValue: []
+      },
+      {
+        label: "文本",
+        prop: "titleText",
+        type: "input",
+        defaultValue: "图表标题"
+      },
+      {
+        label: "位置",
+        prop: "titlePosition",
+        type: "position",
+        defaultValue: "center"
+      },
+      {
+        label: "样式",
+        prop: "titleStyle",
+        type: "fontStyle",
+        defaultValue: {
+          size: 18,
+          bold: true,
+          italic: false,
+          underline: false,
+        }
+      },
+      {
+        label: "背景",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "填充",
+        prop: "titleBackground",
+        type: "backgroundSelect",
+        fieldProps: {
+          filterOptions: ["image"],
+        },
+        defaultValue: {
+          type: "color",
+          color: "#fff",
+        }
+      },
+      {
+        label: "透明度",
+        prop: "titleOpacity",
+        type: "slider",
+        defaultValue: 100
+      },
+      {
+        label: "圆角",
+        prop: "titleBorderRadius",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 0
+      },
+    ],
+  },
+  {
+    label: "图例",
+    prop: "legend",
+    type: "group",
+    children: [
+      {
+        label: " ",
+        prop: "showLegend",
+        type: "checkboxGroup",
+        fieldProps: {
+          options: [{ label: "图例可见", value: true }],
+        },
+        defaultValue: [true]
+      },
+      {
+        label: "位置",
+        prop: "legendPosition",
+        type: "position",
+        fieldProps: {
+          type: "round",
+        },
+        defaultValue: "top"
+      },
+      {
+        label: "样式",
+        prop: "legendStyle",
+        type: "fontStyle",
+        defaultValue: {
+          size: 12,
+          bold: false,
+          italic: false,
+          underline: false,
+        }
+      },
+      {
+        label: "边框",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "线宽",
+        prop: "legendBorderWidth",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 0
+      },
+      {
+        label: "颜色",
+        prop: "legendBorderColor",
+        type: "colorSelect",
+        defaultValue: "#ccc"
+      },
+      {
+        label: "圆角",
+        prop: "legendBorderRadius",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 0
+      },
+      {
+        label: "背景",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "背景",
+        prop: "legendBackground",
+        type: "backgroundSelect",
+        fieldProps: {
+          filterOptions: ["image"],
+        },
+        defaultValue: {
+          type: "color",
+          color: "#fff",
+        }
+      },
+      {
+        label: "背景透明度",
+        prop: "legendBackgroudOpacity",
+        type: "slider",
+        defaultValue: 100
+      },
+      {
+        label: "背景圆角",
+        prop: "legendBackgroundRadius",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 0
+      },
+      {
+        label: "阴影",
+        prop: "legendShadow",
+        type: "radioGroup",
+        fieldProps: {
+          options: [
+            { label: "开启", value: true },
+            { label: "关闭", value: false },
+          ],
+        },
+        defaultValue: false
+      },
+    ],
+  },
+  {
+    label: "标签",
+    prop: "label",
+    type: "group",
+    children: [
+      {
+        label: " ",
+        prop: "showLabel",
+        type: "checkboxGroup",
+        fieldProps: {
+          options: [{ label: "标签可见", value: true }],
+        },
+        defaultValue: false
+      },
+      {
+        label: "文本",
+        prop: "labelValueType",
+        type: "checkboxGroup",
+        fieldProps: {
+          options: [
+            { label: "分类名", value: 'category' },
+            { label: "系列名", value: 'serie' },
+            { label: "数值", value: 'value' },
+            { label: "百分比", value: 'percent' },
+          ]
+        },
+        defaultValue: ['value']
+      },
+      {
+        label: "格式化",
+        prop: "labelFormatter",
+        type: "input",
+        tip: "支持字符串模板和回调函数",
+        defaultValue: "{value}"
+      },
+      {
+        label: "颜色",
+        prop: "labelColor",
+        type: "colorSelect",
+        defaultValue: "#000"
+      },
+      {
+        label: "样式",
+        prop: "labelStyle",
+        type: "fontStyle",
+        defaultValue: {
+          size: 12,
+          bold: false,
+          italic: false,
+          underline: false,
+        }
+      },
+      {
+        label: "布局",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "位置",
+        prop: "labelPosition",
+        type: "radioGroup",
+        fieldProps: {
+          options: [
+            { label: "内部", value: "inside" },
+            { label: "外部", value: "outside" },
+            { label: "中间", value: "center" },
+          ],
+        },
+        defaultValue: "outside"
+      },
+      {
+        label: "文本方向",
+        prop: "labelDirection",
+        type: "radioGroup",
+        fieldProps: {
+          options: [
+            { label: "水平", value: "horizontal" },
+            { label: "垂直", value: "vertical" },
+          ],
+        },
+        defaultValue: "horizontal"
+      },
+      {
+        label: "边框",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "线宽",
+        prop: "labelBorderWidth",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 0
+      },
+      {
+        label: "颜色",
+        prop: "labelBorderColor",
+        type: "colorSelect",
+        defaultValue: "#ccc"
+      },
+      {
+        label: "圆角",
+        prop: "labelBorderRadius",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 0
+      },
+    ],
+  },
+  {
+    label: "系列",
+    prop: "serie",
+    type: "group",
+    children: [
+      {
+        label: "配色",
+        prop: "colorScheme",
+        type: "colorScheme",
+        defaultValue: "custom"
+      },
+      {
+        label: "样式",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "固定柱宽",
+        prop: "serieBarFixedWidth",
+        type: "radioGroup",
+        fieldProps: {
+          options: [
+            { label: "是", value: true },
+            { label: "否", value: false },
+          ],
+        },
+        defaultValue: false
+      },
+      {
+        label: "系列间隔",
+        prop: "serieGap",
+        type: "slider",
+        defaultValue: 20
+      },
+      {
+        label: "分类间隔",
+        prop: "categoryGap",
+        type: "slider",
+        defaultValue: 20
+      },
+      {
+        label: "边框",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "线宽",
+        prop: "serieBorderWidth",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 0
+      },
+      {
+        label: "颜色",
+        prop: "serieBorderColor",
+        type: "colorSelect",
+        defaultValue: "#ccc"
+      },
+      {
+        label: "圆角",
+        prop: "serieBorderRadius",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 0
+      },
+    ]
+  },
+  {
+    label: "X 轴",
+    prop: "xAxis",
+    type: "group",
+    children: [
+      {
+        label: "类型",
+        prop: "xAxisType",
+        type: "select",
+        fieldProps: {
+          options: [
+            { label: "类目坐标轴", value: "category" },
+            { label: "数值坐标轴", value: "value" },
+            { label: "时间坐标轴", value: "time" },
+          ],
+        },
+        defaultValue: "category"
+      },
+      {
+        label: "轴标题",
+        prop: "xAliasShowTitle",
+        type: "radioGroup",
+        fieldProps: {
+          options: [
+            { label: "显示", value: true },
+            { label: "隐藏", value: false },
+          ],
+        },
+        defaultValue: false
+      },
+      {
+        label: "标题内容",
+        prop: "xAliasTitle",
+        type: "input",
+        defaultValue: "X 轴标题"
+      },
+      {
+        label: "标题位置",
+        prop: "xAliasPosition",
+        type: "position",
+        defaultValue: "center"
+      },
+      {
+        label: "标题样式",
+        prop: "xAliasStyle",
+        type: "fontStyle",
+        defaultValue: {
+          size: 12,
+          bold: false,
+          italic: false,
+          underline: false,
+        }
+      },
+      {
+        label: "轴线",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "线宽",
+        prop: "xAliasLineWidth",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 1
+      },
+      {
+        label: "颜色",
+        prop: "xAliasLineColor",
+        type: "colorSelect",
+        defaultValue: "#ccc"
+      },
+      {
+        label: "刻度",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "显示刻度",
+        prop: "xAliasTickShow",
+        type: "radioGroup",
+        fieldProps: {
+          options: [
+            { label: "显示", value: true },
+            { label: "隐藏", value: false },
+          ],
+        },
+        defaultValue: true
+      },
+      {
+        label: "刻度长度",
+        prop: "xAliasTickLength",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 5
+      },
+      {
+        label: "刻度颜色",
+        prop: "xAliasTickColor",
+        type: "colorSelect",
+        defaultValue: "#ccc"
+      },
+      {
+        label: "标签",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "颜色",
+        prop: "xAliasLabelColor",
+        type: "colorSelect",
+        defaultValue: "#000"
+      },
+      {
+        label: "样式",
+        prop: "xAliasLabelStyle",
+        type: "fontStyle",
+        defaultValue: {
+          size: 12,
+          bold: false,
+          italic: false,
+          underline: false,
+        }
+      },
+    ]
+  },
+  {
+    label: "Y 轴",
+    prop: "yAxis",
+    type: "group",
+    children: [
+      {
+        label: "轴标题",
+        prop: "yAliasShowTitle",
+        type: "radioGroup",
+        fieldProps: {
+          options: [
+            { label: "显示", value: true },
+            { label: "隐藏", value: false },
+          ],
+        },
+        defaultValue: false
+      },
+      {
+        label: "标题内容",
+        prop: "yAliasTitle",
+        type: "input",
+        defaultValue: "Y 轴标题"
+      },
+      {
+        label: "标题位置",
+        prop: "yAliasPosition",
+        type: "position",
+        defaultValue: "center"
+      },
+      {
+        label: "标题样式",
+        prop: "yAliasStyle",
+        type: "fontStyle",
+        defaultValue: {
+          size: 12,
+          bold: false,
+          italic: false,
+          underline: false,
+        }
+      },
+      {
+        label: "轴线",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "线宽",
+        prop: "yAliasLineWidth",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 1
+      },
+      {
+        label: "颜色",
+        prop: "yAliasLineColor",
+        type: "colorSelect",
+        defaultValue: "#ccc"
+      },
+      {
+        label: "刻度",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "显示刻度",
+        prop: "yAliasTickShow",
+        type: "radioGroup",
+        fieldProps: {
+          options: [
+            { label: "显示", value: true },
+            { label: "隐藏", value: false },
+          ],
+        },
+        defaultValue: true
+      },
+      {
+        label: "刻度长度",
+        prop: "yAliasTickLength",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 5
+      },
+      {
+        label: "刻度颜色",
+        prop: "yAliasTickColor",
+        type: "colorSelect",
+        defaultValue: "#ccc"
+      },
+      {
+        label: "标签",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "颜色",
+        prop: "yAliasLabelColor",
+        type: "colorSelect",
+        defaultValue: "#000"
+      },
+      {
+        label: "样式",
+        prop: "yAliasLabelStyle",
+        type: "fontStyle",
+        defaultValue: {
+          size: 12,
+          bold: false,
+          italic: false,
+          underline: false,
+        }
+      },
+    ]
+  },
+  {
+    label: "提示",
+    prop: "tooltip",
+    type: "group",
+    children: [
+      {
+        label: " ",
+        prop: "showTooltip",
+        type: "checkboxGroup",
+        fieldProps: {
+          options: [
+            { label: "提示可见", value: true },
+          ],
+        },
+        defaultValue: [true]
+      },
+      {
+        label: "文本",
+        prop: "tooltipValueType",
+        type: "checkboxGroup",
+        fieldProps: {
+          options: [
+            { label: "分类名", value: 'category' },
+            { label: "系列名", value: 'serie' },
+            { label: "数值", value: 'value' },
+            { label: "百分比", value: 'percent' },
+          ]
+        },
+        defaultValue: ['value']
+      },
+      {
+        label: "格式化",
+        prop: "tooltipFormatter",
+        type: "input",
+        tip: "支持字符串模板和回调函数",
+        defaultValue: "{value}"
+      },
+      {
+        label: "样式",
+        prop: "tooltipStyle",
+        type: "fontStyle",
+        defaultValue: {
+          size: 12,
+          bold: false,
+          italic: false,
+          underline: false,
+        }
+      },
+      {
+        label: "边框",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "线宽",
+        prop: "tooltipBorderWidth",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 1
+      },
+      {
+        label: "颜色",
+        prop: "tooltipBorderColor",
+        type: "colorSelect",
+        defaultValue: "#ccc"
+      },
+      {
+        label: "圆角",
+        prop: "tooltipBorderRadius",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 0
+      },
+      {
+        label: "背景",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "填充",
+        prop: "tooltipBackground",
+        type: "backgroundSelect",
+        fieldProps: {
+          filterOptions: ["image"],
+        },
+        defaultValue: {
+          type: "color",
+          color: "#fff",
+        }
+      },
+      {
+        label: "背景透明度",
+        prop: "tooltipBackgroudOpacity",
+        type: "slider",
+        defaultValue: 100
+      },
+      {
+        label: "阴影",
+        prop: "tooltipShadow",
+        type: "radioGroup",
+        fieldProps: {
+          options: [
+            { label: "开启", value: true },
+            { label: "关闭", value: false },
+          ],
+        },
+        defaultValue: false
+      },
+    ]
+  },
+  {
+    label: "背景",
+    prop: "background",
+    type: "group",
+    children: [
+      {
+        label: "边框",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "线宽",
+        prop: "backgroundBorderWidth",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 0
+      },
+      {
+        label: "颜色",
+        prop: "backgroundBorderColor",
+        type: "colorSelect",
+        defaultValue: "#ccc"
+      },
+      {
+        label: "圆角",
+        prop: "backgroundBorderRadius",
+        type: "inputNumber",
+        fieldProps: {
+          addonAfter: "px",
+        },
+        defaultValue: 0
+      },
+      {
+        label: "背景",
+        prop: "",
+        type: "divider",
+      },
+      {
+        label: "填充",
+        prop: "backgroundBackground",
+        type: "backgroundSelect",
+        fieldProps: {
+          filterOptions: ["image"],
+        },
+        defaultValue: {
+          type: "color",
+          color: "#fff",
+        }
+      },
+      {
+        label: "背景透明度",
+        prop: "backgroundBackgroudOpacity",
+        type: "slider",
+        defaultValue: 100
+      },
+      {
+        label: "阴影",
+        prop: "backgroundShadow",
+        type: "radioGroup",
+        fieldProps: {
+          options: [
+            { label: "开启", value: true },
+            { label: "关闭", value: false },
+          ],
+        },
+        defaultValue: false
+      },
+    ]
+  }
+];

+ 7 - 0
components/charts/chartEnum.ts

@@ -0,0 +1,7 @@
+/* 数据来源 */
+export enum DataSourceType {
+  /* 静态数据 */
+  STATIC,
+  /* 接口数据 */
+  API,
+}

+ 132 - 0
components/charts/config/index.ts

@@ -0,0 +1,132 @@
+import { EChartsOption } from "echarts"
+
+
+// 颜色预设集合
+export const colorPreset = [
+  {
+    name: '清新',
+    color: ['#00a8e1', '#99cc00', '#e30039', '#fcd300', '#800080']
+  },
+  {
+    name: '复古',
+    color: ['#FFA69E', '#FAE3D9', '#B8F2E6', '#56E39F', '#3A837D']
+  },
+  {
+    name: '商务',
+    color: ['#194f97', '#555555', '#bd6b08', '#00686b', '#c82d31']
+  },
+  {
+    name: '经典',
+    color: ['#002c53', '#ffa510', '#0c84c6', '#ffffff', '#f74d4d']
+  },
+  {
+    name: '怀旧',
+    color: ['#3b6291', '#943c39', '#779043', '#624c7c', '#388498']
+  }
+]
+
+// 图表默认配置项
+export const chartDefaultConfig: EChartsOption = {
+  // 调色盘-预设颜色
+  color: colorPreset[0].color,
+  // 标题
+  title: {
+    left: "center",
+    top: 8,
+    textStyle: {
+      color: "#fff",
+      fontSize: 16,
+    },
+  },
+  // 图例
+  legend: {
+    textStyle: {
+      color: "#fff",
+    },
+    top: 32,
+  },
+  // 布局
+  grid: {
+    bottom: 34,
+    right: 20,
+    top: 60,
+  },
+  // 提示框
+  tooltip: {},
+  // x轴
+  xAxis: {
+    type: "category",
+    axisLabel: {
+      color: "#9fadbf",
+    },
+  },
+  // y轴
+  yAxis: {
+    axisLabel: {
+      color: "#9fadbf",
+    },
+    splitLine: {
+      lineStyle: {
+        type: "dashed",
+        color: "#36485f",
+      },
+    },
+  },
+}
+
+
+// 组件容器基本设置
+export const containerDefaultConfig = {
+  /* ===================================== 通用容器样式 ============================================ */
+  style: {
+    background: {
+      type: "color", // none, color, image
+      color: "rgba(0, 0, 0, 0.1)",
+      image: "",
+      fillType: "",
+    },
+    border: {
+      type: "none",
+      color: "#000",
+      width: 0,
+    },
+    borderRadius: {
+      type: "all", // all, custom
+      value: 2, // 整体圆角值
+      unit: "px", // 单位
+      units: ["px", "%"], // 单位列表
+      topLeft: 0,
+      topRight: 0,
+      bottomLeft: 0,
+      bottomRight: 0,
+    },
+    boxShadow: {
+      enabled: false,
+      color: "#000",
+      offsetX: 0,
+      offsetY: 0,
+      blurRadius: 0,
+      spreadRadius: 0,
+      inset: false,
+    },
+    backdropFilter: {
+      enabled: false,
+      blur: 0,
+    }
+  },
+  /* ===================================== 通用容器属性 ============================================ */
+  props: {
+    width: 0,
+    height: 0,
+    x: 0,
+    y: 0,
+    paddingLeft: 0,
+    paddingRight: 0,
+    paddingTop: 0,
+    paddingBottom: 0,
+    rotateX: 0,
+    rotateY: 0,
+    rotateZ: 0,
+    opacity: 1,
+  },
+};

+ 102 - 0
components/charts/hooks/useChartOptions.ts

@@ -0,0 +1,102 @@
+import type { EChartsOption } from "echarts";
+import { computed, watch, ref } from "vue";
+import { omit, defaultsDeep } from "lodash";
+import { useRequest } from "vue-hooks-plus";
+import { DataSourceType } from "../chartEnum";
+import { message } from "ant-design-vue";
+import { cllJsCode } from "../utils";
+
+export const useChartOptions = (chartProps: Record<string, any>) => {
+  const dataSource = chartProps.dataSource || {};
+  const xAxis = ref<EChartsOption["xAxis"]>({ data: dataSource?.data?.xData });
+  const yAxis = ref<EChartsOption["yAxis"]>({ data: dataSource?.data?.yData });
+  const series = ref<EChartsOption["series"]>(dataSource?.data?.series);
+
+  const server = computed(() => {
+    return async () =>
+      await fetch(chartProps.dataSource.url, {
+        method: chartProps.dataSource.method,
+      })
+      .then((res) => res.json());
+  });
+
+  // 请求数据
+  const { run, refresh, cancel, data, loading } = useRequest(server.value, {
+    defaultParams: chartProps.dataSource.params,
+    manual: true,
+    pollingInterval: (chartProps.dataSource?.refreshTime || 0) * 1000, // 刷新时间
+    onError: (error) => {
+      console.error(error);
+      message.error(chartProps.dataSource.url + "请求失败");
+    }
+  });
+
+  /* 初始请求 */
+  if (chartProps.dataSource.sourceType === DataSourceType.API) {
+    run();
+  }
+
+  watch(
+    () => data.value,
+    async (val) => {
+      if (val && chartProps.dataSource.sourceType === DataSourceType.API) {
+        let res = val;
+        if(chartProps.dataSource.dataProcess) {
+          res = await cllJsCode(chartProps.dataSource.dataProcess, JSON.stringify(val));
+        }
+    
+        xAxis.value = res.xAxis || { data: res.xData };
+        yAxis.value = res.yAxis || { data: res.yData };
+        series.value = res.series;
+      }
+    },
+    {
+      deep: true,
+    }
+  );
+
+  watch(
+    () => [
+      chartProps.dataSource.sourceType,
+      chartProps.dataSource.method
+    ],
+    () => {
+      if (chartProps.dataSource.sourceType === DataSourceType.API) {
+        refresh();
+      } else {
+        cancel();
+        const dataSource = chartProps.dataSource || {};
+        xAxis.value = { data: dataSource?.data?.xData };
+        yAxis.value = { data: dataSource?.data?.yData };
+        series.value = dataSource?.data?.series;
+      }
+    },
+    {
+      deep: true,
+    }
+  );
+
+  const options = computed((): EChartsOption => {
+    const opt = omit(chartProps, [
+      "width",
+      "height",
+      "dataSource",
+    ]) as EChartsOption;
+
+    const result = defaultsDeep(
+      {
+        xAxis: xAxis.value,
+        yAxis: yAxis.value,
+        series: series.value,
+      },
+      opt
+    );
+
+    return result;
+  });
+
+  return {
+    options,
+    loading,
+  };
+};

+ 100 - 0
components/charts/hooks/useEcharts.ts

@@ -0,0 +1,100 @@
+import type { Ref } from "vue";
+import type { EChartsOption } from "echarts";
+import * as echarts from "echarts";
+import { unref, nextTick, watch, computed, ref } from "vue";
+import { tryOnUnmounted, useDebounceFn, useEventListener } from "@vueuse/core";
+
+export function useEcharts(
+  elRef: Ref<HTMLElement>,
+  theme: "default" | "dark" | "light" = "default"
+) {
+  let chartInstance: echarts.ECharts | null = null;
+  let resizeFn: () => void = resize;
+  const cacheOptions = ref({});
+
+  let removeResizeFn: () => void;
+
+  const getOptions = computed(() => {
+    return cacheOptions.value;
+  });
+
+  resizeFn = useDebounceFn(resize, 200);
+
+  function resize() {
+    chartInstance?.resize({
+      animation: {
+        duration: 300,
+        easing: "quadraticIn",
+      },
+    });
+  }
+
+  function initCharts(t = theme) {
+    const el = unref(elRef);
+    if (!el || !unref(el)) {
+      return;
+    }
+
+    chartInstance = echarts.init(el, t);
+    const removeEvent = useEventListener(window, "resize", resizeFn);
+
+    removeResizeFn = removeEvent;
+  }
+
+  function setOptions(options: EChartsOption, clear = true) {
+    cacheOptions.value = options;
+    return new Promise((resolve) => {
+      if (unref(elRef)?.offsetHeight === 0) {
+        setTimeout(() => {
+          setOptions(unref(getOptions));
+          resolve(null);
+        }, 30);
+      }
+      nextTick(() => {
+        setTimeout(() => {
+          if (!chartInstance) {
+            initCharts("default");
+
+            if (!chartInstance) return;
+          }
+          clear && chartInstance?.clear();
+
+          chartInstance?.setOption(unref(getOptions));
+          resolve(null);
+        }, 30);
+      });
+    });
+  }
+
+  watch(
+    () => theme,
+    (theme) => {
+      if (chartInstance) {
+        chartInstance.dispose();
+        initCharts(theme as "default");
+        setOptions(cacheOptions.value);
+      }
+    }
+  );
+
+  tryOnUnmounted(() => {
+    if (!chartInstance) return;
+    removeResizeFn();
+    chartInstance.dispose();
+    chartInstance = null;
+  });
+
+  function getInstance(): echarts.ECharts | null {
+    if (!chartInstance) {
+      initCharts("default");
+    }
+    return chartInstance;
+  }
+
+  return {
+    setOptions,
+    resize,
+    echarts,
+    getInstance,
+  };
+}

+ 25 - 0
components/charts/types.ts

@@ -0,0 +1,25 @@
+import { DataSourceType } from "./chartEnum";
+
+export interface ChartData {
+  xData: string[];
+  yData: number[] | string[];
+  series: any[];
+}
+// chart数据源
+export interface DataSource {
+  // 类型
+  sourceType: DataSourceType.STATIC,
+  // 数据
+  data?: ChartData,
+  // 接口相关
+  url?: string,
+  // 请求方式
+  method?: 'GET' | 'POST',
+  // 请求参数
+  params?: any,
+  // 请求头
+  headers?: Record<string, any>,
+  refreshTime?: number,
+  // 数据处理
+  dataProcess: () => any[],
+}

+ 70 - 0
components/charts/utils/index.ts

@@ -0,0 +1,70 @@
+import type { DataSource } from "../types";
+import { defaultsDeep } from "lodash";
+import { containerDefaultConfig, chartDefaultConfig } from "../config";
+import { EChartsOption } from "echarts";
+import { PropType } from "vue";
+import { DataSourceType } from "../chartEnum";
+
+/**
+ * 获取容器组件属性
+ *  @param {Record<string, any>} config - 传入属性
+ *  @returns {Record<string, any>} - 返回与默认配置合并后的属性
+ */
+export function getNormalizedContainer(config: Record<string, any>) {
+  return defaultsDeep(config, containerDefaultConfig);
+}
+
+/**
+ * 获取图表组件属性
+ *  @param {Record<string, any>} config - 传入属性
+ *  @returns {Record<string, any>} - 返回与默认配置合并后的属性
+ */
+export function getNormalizedChart(config: EChartsOption) {
+  return defaultsDeep(config, chartDefaultConfig);
+}
+
+// 图表组件数据来源prop
+export const dataSource = {
+  type: Object as PropType<DataSource>,
+  default: () => ({
+    sourceType: DataSourceType.STATIC,
+    data: [],
+    url: "",
+    method: "GET",
+    params: {},
+    headers: {},
+    refreshTime: 0,
+    dataProcess: () => [],
+  }),
+}
+
+
+/**
+ * 执行动态js
+ * @param code 代码
+ * @param param 参数
+ * @returns promise
+ */
+export const cllJsCode = (code: string, param: string): Promise<any> => {
+  
+  return new Promise((resove, reject) => {
+    // 生成一份new webwork
+    const blob = new Blob([`
+      self.onmessage = function(e) {
+        self.postMessage((${code}).call(null, e.data));
+      }
+    `], { type: 'application/javascript' });
+
+    const worker = new Worker(URL.createObjectURL(blob));
+    // 向webwork发送消息
+    worker.postMessage(JSON.parse(param));
+    worker.onmessage = (e) => {
+      worker.terminate();
+      resove(e.data);
+    }
+    worker.onerror = (e) => {
+      worker.terminate();
+      reject(e);
+    }
+  });
+}

+ 39 - 0
components/components.ts

@@ -0,0 +1,39 @@
+export const asyncComponentAll = {
+  Title: () => import('./text/Title'),
+  BasicLine: () => import('./charts/Line/BasicLine'),
+  BasicBar: () => import('./charts/Bar/BasicBar/src/BasicBar.vue'),
+}
+
+export { DataSourceType } from './charts/chartEnum';
+export type { DataSource, ChartData } from './charts/types';
+
+/* 标题 */
+import { default as Title } from './text/Title'; 
+
+/* 基础折线图 */
+import { default as BasicLine } from './charts/Line/BasicLine';
+export { 
+  Config as BasicLineConfig,
+  defaultPropsValue as BasicLineDefaultProps,
+  basicLineProps as BasicLineProps
+} from './charts/Line/BasicLine';
+
+/* 基础柱形图 */
+import { default as BasicBar } from './charts/Bar/BasicBar/src/BasicBar.vue';
+export { 
+  Config as BasicBarConfig,
+  defaultPropsValue as BasicBarDefaultProps,
+  basicBarProps as BasicBarProps
+} from './charts/Bar/BasicBar';
+
+export const components = {
+  BasicLine,
+  BasicBar,
+  Title
+}
+
+export {
+  BasicLine,
+  BasicBar,
+  Title
+}

+ 7 - 0
components/cusForm/index.ts

@@ -0,0 +1,7 @@
+import CusForm from './src/index.vue';
+import type { IFormItem } from './src/type';
+
+export {
+  CusForm,
+  IFormItem
+}

+ 122 - 0
components/cusForm/src/BackgroundSelect.vue

@@ -0,0 +1,122 @@
+<template>
+  <Select v-model:value="backgroundObj.type" style="width: 100%" :options="getOptions" @change="handleChangeColor"/>
+  <template v-if="backgroundObj.type === 'color'">
+    <div class="color-box">
+      <ElColorPicker v-model="backgroundObj.color" color-format="hex" show-alpha size="small"/>
+      <ElInput v-model="backgroundObj.color" size="small"></ElInput>
+    </div>
+  </template>
+  <template v-else-if="backgroundObj.type === 'image'">
+    <div class="img-preview">
+      <div class="img-empty">未选择</div>
+      <Image :src="backgroundObj.image" />
+      <div class="img-tip">选择素材</div>
+    </div>
+    <ElRadioGroup v-model="backgroundObj.fillType">
+      <ElRadioButton value="cover">填充</ElRadioButton>
+      <ElRadioButton value="contain">适应</ElRadioButton>
+      <ElRadioButton value="stretch">拉伸</ElRadioButton>
+    </ElRadioGroup>
+  </template>
+</template>
+
+<script setup lang="ts">
+import { defineEmits, defineProps, withDefaults, ref, watch, computed } from "vue";
+import { Select, Image } from "ant-design-vue";
+import { ElColorPicker, ElRadioGroup, ElRadioButton, ElInput } from "element-plus";
+
+interface Prop {
+  background: {
+    type: "none" | "color" | "image";
+    color?: string;
+    image?: string;
+    fillType?: "cover" | "contain" | "stretch" | "";
+  };
+  filterOptions?: string[];
+}
+const props = withDefaults(defineProps<Prop>(), {
+  background: () => ({
+    type: "none",
+    color: "",
+    image: "",
+    fillType: ""
+  })
+});
+
+const emit = defineEmits(["update:background"]);
+
+const backgroundObj = ref(props.background);
+
+const options = [
+  { label: "无", value: "none" },
+  { label: "颜色", value: "color" },
+  { label: "图片", value: "image" },
+];
+
+const getOptions = computed(() => {
+  if(!props.filterOptions) {
+    return options;
+  }
+  return options.filter((item) => !props.filterOptions?.includes(item.value));
+});
+
+watch(
+  () => backgroundObj.value,
+  () => {
+    emit("update:background", backgroundObj.value);
+  }, {
+    deep: true,
+    immediate: true
+  }
+)
+
+const handleChangeColor = (type: any) => {
+  if(type === 'color' && !backgroundObj.value.color) {
+    backgroundObj.value.color = '#0B074BFF';
+  }
+};
+</script>
+
+<style lang="less" scoped>
+.color-box {
+  display: flex;
+  align-items: center;
+  margin: 12px 0;
+  margin-bottom: 0;
+  :deep(.el-color-picker) {
+    margin-right: 8px;
+  }
+}
+
+.img-preview {
+  margin: 12px 0;
+  border: solid 1px #eee;
+  height: 120px;
+  position: relative;
+  .img-empty {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 100%;
+    color: #999;
+  }
+  .img-tip {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    left: 0;
+    top: 0;
+    background: rgba(0, 0, 0, .6);
+    color: #fff;
+    font-size: 12px;
+    opacity: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    &:hover {
+      opacity: 1;
+    }
+  }
+}
+</style>

+ 74 - 0
components/cusForm/src/ColorScheme.vue

@@ -0,0 +1,74 @@
+<template>
+  <div>
+    <Select v-model:value="colorType" size="small" style="width: 100%" @change="changeColorType">
+      <SelectOption
+        v-for="item in colorPreset"
+        :key="item.name"
+        :value="item.color.join(',')"
+      >
+        <span
+          class="color-block"
+          v-for="color in item.color.slice(0, 5)"
+          :key="color"
+          :style="{background: color}"
+        ></span>
+        {{ item.name }}</SelectOption
+      >
+
+      <SelectOption value="custom">自定义组合</SelectOption>
+      <!-- <SelectOption value="gradient">自定义渐变色</SelectOption> -->
+    </Select>
+    <div class="color-list">
+      <span
+        class="color-block"
+        v-for="(color, index) in value"
+        :key="index"
+        :style="{background: color}"
+      ></span>
+      <span class="color-block cus-btn"><PlusOutlined /></span>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from "vue";
+import { Select, SelectOption } from "ant-design-vue";
+import { PlusOutlined } from "@ant-design/icons-vue";
+import { colorPreset } from "@/config/colorDefaultConfig";
+
+const value = ref<string[]>(colorPreset[0].color);
+const colorType = ref<string>(colorPreset[0].color.join(","));
+
+const changeColorType = (val: string) => {
+  if (val === "custom") {
+    value.value = [];
+  } else {
+    value.value = val.split(",");
+  }
+};
+</script>
+
+<style lang="less" scoped>
+.color-block {
+  display: inline-block;
+  width: 10px;
+  height: 10px;
+  margin-right: 4px;
+}
+.color-list {
+  margin-top: 12px;
+  display: flex;
+  flex-wrap: wrap;
+  .color-block {
+    margin-right: 0;
+    width: 16px;
+    height: 16px;
+  }
+  .cus-btn {
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+}
+</style>

+ 66 - 0
components/cusForm/src/ColorSelect.vue

@@ -0,0 +1,66 @@
+<template>
+  <ElRadioGroup v-model="colorType" size="small" style="width: 100%">
+    <ElRadioButton value="pure">单色</ElRadioButton>
+    <ElRadioButton value="gradient">渐变色</ElRadioButton>
+  </ElRadioGroup>
+  <template v-if="colorType === 'pure'">
+    <div class="color-box">
+      <ElColorPicker v-model="color" color-format="hex" show-alpha size="small"/>
+      <ElInput v-model="color" size="small"></ElInput>
+    </div>
+  </template>
+  <template v-else-if="colorType === 'gradient'">
+    <div class="gradient-box" :style="{ background: color }">
+      <ElColorPicker v-model="gradientColor[0]" color-format="hex" show-alpha size="small"/>
+      <ElColorPicker v-model="gradientColor[1]" color-format="hex" show-alpha size="small"/>
+    </div>
+  </template>
+</template>
+
+<script setup lang="ts">
+import { ElRadioGroup, ElRadioButton, ElColorPicker, ElInput, } from 'element-plus';
+import { defineEmits, defineProps, ref, watch } from 'vue';
+
+const emit = defineEmits(["update:value"]);
+const props = defineProps<{
+  value: string;
+}>();
+const colorType = ref(props.value?.length <= 9 || !props.value ? 'pure' : 'gradient');
+const color = ref(props.value);
+// 'linear-gradient(90deg,#9CEC5BFF,#0764F0FF)'
+const gradientColor = ref(props.value?.length >= 9 ? props.value.slice(22).replace(')', '').split(',') : ['#4ba9ff', '#fff']);
+
+watch(
+  () => [colorType.value, color.value, gradientColor.value],
+  () => {
+    if (colorType.value === 'pure') {
+      color.value = color.value.length > 9 ? '#FFFFFFFF' : color.value;
+    } else {
+      color.value = `linear-gradient(90deg, ${gradientColor.value.join(',')})`
+    }
+    emit('update:value', color.value);
+  },
+  {
+    deep: true
+  }
+);
+</script>
+
+<style lang="less" scoped>
+.color-box {
+  display: flex;
+  align-items: center;
+  margin-top: 12px;
+  :deep(.el-color-picker) {
+    margin-right: 8px;
+  }
+}
+.gradient-box {
+  padding: 2px;
+  margin-top: 12px;
+  border: solid 1px #eee;
+  width: 100%;
+  display: flex;
+  justify-content: space-between;
+}
+</style>

+ 94 - 0
components/cusForm/src/CusFormItem.vue

@@ -0,0 +1,94 @@
+<template>
+  <FormItem :label="item.type !== 'divider' ? item.label : ''" :name="item.prop" :rules="item.rules">
+    <template v-if="item.type === 'divider'">
+      <Divider style="margin: 0">{{ item.label }}</Divider>
+    </template>
+    <template v-else-if="item.type === 'input'">
+      <Input v-model:value="model" v-bind="item?.fieldProps"/>
+    </template>
+    <template v-else-if="item.type === 'select'">
+      <Select v-model:value="model" v-bind="item?.fieldProps"></Select>
+    </template>
+    <template v-else-if="item.type === 'inputNumber'">
+      <InputNumber v-model:value="model"  v-bind="item?.fieldProps" style="width: 100%"/>
+    </template>
+    <template v-else-if="item.type === 'image'">
+      <Image v-model:value="model"  v-bind="item?.fieldProps"/>
+    </template>
+    <template v-else-if="item.type === 'checkboxGroup'">
+      <CheckboxGroup v-model:value="model" v-bind="item?.fieldProps">
+      </CheckboxGroup>
+    </template>
+    <template v-else-if="item.type === 'backgroundSelect'">
+      <BackgroundSelect v-model:background="model"  v-bind="item?.fieldProps"/>
+    </template>
+    <template v-else-if="item.type === 'colorSelect'">
+      <ColorSelect v-model:value="model"  v-bind="item?.fieldProps"/>
+    </template>
+    <template v-else-if="item.type === 'radioGroup'">
+      <RadioGroup v-model:value="model" size="small" v-bind="item?.fieldProps">
+      </RadioGroup>
+    </template>
+    <template v-else-if="item.type === 'position'">
+      <Position v-model:value="model"  v-bind="item?.fieldProps"/>
+    </template>
+    <template v-else-if="item.type === 'fontStyle'">
+      <FontStyle v-model:value="model"  v-bind="item?.fieldProps"/>
+    </template>
+    <template v-else-if="item.type === 'slider'">
+      <FormItemRest>
+        <CusSlider v-model:value="model"  v-bind="item?.fieldProps"/>
+      </FormItemRest>
+    </template>
+    <!-- 提示 -->
+    <template v-if="item.tip">
+      <Tooltip :title="item.tip">
+        <InfoCircleOutlined style="color: #666"/>
+      </Tooltip>
+    </template>
+  </FormItem>
+</template>
+
+<script setup lang="ts">
+import { IFormItem } from "./type";
+import { defineProps, defineEmits, ref, watch } from "vue";
+import {
+  FormItem,
+  FormItemRest,
+  Input,
+  Select,
+  InputNumber,
+  CheckboxGroup,
+  Image,
+  Divider,
+  RadioGroup,
+  Tooltip
+} from "ant-design-vue";
+import { InfoCircleOutlined } from "@ant-design/icons-vue";
+
+import BackgroundSelect from "./BackgroundSelect.vue";
+import ColorSelect from "./ColorSelect.vue";
+import Position from "./Position.vue";
+import FontStyle from "./FontStyle.vue";
+import CusSlider from "./CusSlider.vue";
+
+const props = defineProps<{item: IFormItem, modelValue: any}>();
+const emit = defineEmits(["update:modelValue"]);
+
+const model = ref(props.modelValue);
+
+watch(
+  () => model.value,
+  () => {
+    emit("update:modelValue", model.value);
+  }
+)
+
+</script>
+
+<style lang="less" scoped>
+:deep(.ant-divider-inner-text ){
+  font-size: 12px;
+  color: #666;
+}
+</style>

+ 27 - 0
components/cusForm/src/CusSlider.vue

@@ -0,0 +1,27 @@
+<template>
+  <div class="cus-slider">
+    <Slider :value="value" @change="(val) => $emit('update:value', val)" :tip-formatter="(val) => `${val}%`"/>
+    <InputNumber :value="value" addon-after="%"/>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { defineProps, defineEmits } from "vue";
+import { Slider, InputNumber } from "ant-design-vue";
+
+defineProps<{ value: number }>();
+defineEmits(["update:value"]);
+</script>
+
+<style lang="less" scoped>
+.cus-slider {
+  display: flex;
+  align-items: center;
+  .ant-slider {
+    flex: 1;
+  }
+  .ant-input-number-group-wrapper {
+    flex: 15%;
+  }
+}
+</style>

+ 86 - 0
components/cusForm/src/FontStyle.vue

@@ -0,0 +1,86 @@
+<template>
+  <div class="font-style">
+    <span class="cus-btn" :class="{ 'active-btn': bold }"
+      ><BoldOutlined @click="handleBold"
+    /></span>
+    <span class="cus-btn" :class="{ 'active-btn': italic }"
+      ><ItalicOutlined @click="handleItalic"
+    /></span>
+    <span class="cus-btn" :class="{ 'active-btn': underline }"
+      ><UnderlineOutlined @click="handleUnderline"
+    /></span>
+    <InputNumber
+      size="small"
+      :value="size"
+      :min="12"
+      :step="1"
+      :precision="0"
+      style="width: 80px"
+      @change="handleChange"
+    >
+      <template #addonAfter>px</template>
+    </InputNumber>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { defineProps, defineEmits, ref } from "vue";
+import {
+  BoldOutlined,
+  ItalicOutlined,
+  UnderlineOutlined,
+} from "@ant-design/icons-vue";
+import { InputNumber } from "ant-design-vue";
+
+const props = defineProps<{
+  value: {
+    size: number;
+    bold: boolean;
+    italic: boolean;
+    underline: boolean;
+  };
+}>();
+
+const emit = defineEmits(["update:value"]);
+
+const bold = ref(props.value?.bold);
+const italic = ref(props.value?.italic);
+const underline = ref(props.value?.underline);
+const size = ref(props.value?.size);
+
+const handleUpdate = () => {
+  emit("update:value", {
+    size: size.value,
+    bold: bold.value,
+    italic: italic.value,
+    underline: underline.value,
+  });
+};
+
+const handleBold = () => {
+  bold.value = !bold.value;
+  handleUpdate();
+};
+const handleItalic = () => {
+  italic.value = !italic.value;
+  handleUpdate();
+};
+const handleUnderline = () => {
+  underline.value = !underline.value;
+  handleUpdate();
+};
+const handleChange = (val: number) => {
+  size.value = val;
+  handleUpdate();
+};
+</script>
+
+<style lang="less" scoped>
+.font-style {
+  display: flex;
+  justify-content: space-between;
+}
+.active-btn {
+  color: #1890ff;
+}
+</style>

+ 37 - 0
components/cusForm/src/Position.vue

@@ -0,0 +1,37 @@
+<template>
+  <div class="position">
+    <RadioGroup v-if="type === 'line'" :value="value" @change="(e) => $emit('update:value', e.target.value)">
+      <RadioButton value="left"><AlignLeftOutlined/></RadioButton>
+      <RadioButton value="center"><AlignCenterOutlined/></RadioButton>
+      <RadioButton value="right"><AlignRightOutlined/></RadioButton>
+    </RadioGroup>
+
+    <RadioGroup v-else-if="type === 'round'" :value="value" @change="(e) => $emit('update:value', e.target.value)">
+      <RadioButton value="top"><BorderTopOutlined/></RadioButton>
+      <RadioButton value="bottom"><BorderBottomOutlined/></RadioButton>
+      <RadioButton value="left"><BorderLeftOutlined/></RadioButton>
+      <RadioButton value="right"><BorderRightOutlined/></RadioButton>
+      <!-- <RadioButton value="right-top"><AlignRightOutlined/></RadioButton> -->
+    </RadioGroup>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { defineProps, defineEmits, withDefaults } from 'vue';
+import { RadioGroup, RadioButton } from 'ant-design-vue';
+import { AlignCenterOutlined, AlignRightOutlined, AlignLeftOutlined, BorderBottomOutlined, BorderTopOutlined, BorderLeftOutlined, BorderRightOutlined } from '@ant-design/icons-vue';
+
+withDefaults(defineProps<{value: string, type?: 'round' | 'line'}>(), { type: 'line'});
+defineEmits(['update:value']);
+</script>
+
+<style lang="less" scoped>
+:deep(.ant-radio-group) {
+  width: 100%;
+  display: flex;
+}
+:deep(.ant-radio-button-wrapper) {
+  flex: 1;
+  text-align: center;
+}
+</style>

+ 92 - 0
components/cusForm/src/index.vue

@@ -0,0 +1,92 @@
+<template>
+  <Form
+    :model="formModel"
+    :colon="false"
+    :label-col="{ span: 8 }"
+    ref="formRef"
+    layout="horizontal"
+    size="small"
+  >
+    <template v-for="item in formItems" :key="item.prop">
+      <!-- 分组 -->
+      <Collapse v-if="item.type === 'group'">
+        <CollapsePanel :key="item.prop" :header="item.label">
+          <CusFormItem v-for="child in item.children" :key="child.prop" :item="child" v-model="formModel[child.prop]" />
+        </CollapsePanel>
+      </Collapse>
+      <!-- 单个表单项 -->
+      <CusFormItem v-else :item="item" v-model="formModel[item.prop]" />
+    </template>
+  </Form>
+</template>
+
+<script setup lang="ts">
+import type { IFormItem } from "./type";
+import type { FormInstance } from "ant-design-vue";
+import {
+  ref,
+  defineProps,
+  defineExpose,
+  computed,
+  watch,
+  defineEmits,
+} from "vue";
+import {
+  Form,
+  Collapse,
+  CollapsePanel
+} from "ant-design-vue";
+import CusFormItem from "./CusFormItem.vue";
+
+const props = defineProps<{
+  columns: IFormItem[];
+  formModel?: Record<string, any>;
+}>();
+
+const emit = defineEmits(["change"]);
+
+const formModel = ref<Record<string, any>>({});
+const formRef = ref<FormInstance>();
+
+const formItems = computed(() => {
+  return props.columns.map((item) => {
+    return {
+      ...item,
+      rules: item.rules || [],
+    };
+  });
+});
+
+watch(
+  () => formItems.value,
+  (val) => {
+    val &&
+      props.columns?.forEach((item) => {
+        // 设置表单初始值
+        if (item.type === "group") {
+          const children = item.children || [];
+          children.forEach((child) => {
+            if(item.type === 'divider') return;
+            formModel.value[child.prop] = child?.defaultValue;
+          });
+        } else {
+          if(item.type === 'divider') return;
+          formModel.value[item.prop] = item?.defaultValue;
+        }
+      });
+  },
+  { immediate: true }
+);
+
+watch(
+  () => formModel.value,
+  (val) => {
+    emit("change", val);
+  },
+  { deep: true }
+);
+
+defineExpose(formRef.value);
+</script>
+
+<style lang="less" scoped></style>

+ 52 - 0
components/cusForm/src/type.ts

@@ -0,0 +1,52 @@
+import { VNode } from "vue";
+
+export interface IFormItem {
+  label: string;
+  icon?: string;
+  prop: string;
+  type:
+    // 内置类型--分组
+    | "group"
+    | "input"
+    | "select"
+    | "inputNumber"
+    | "image"
+    | "checkboxGroup"
+    | "radioGroup"
+    // 背景选择
+    | "backgroundSelect"
+    // 边框选择
+    | "boderSelect"
+    // 边框圆角选择
+    | "boderRadiusSelect"
+    // 阴影选择
+    | "shodowSelect"
+    // 内边距选择
+    | "paddingSelect"
+    // 旋转选择
+    | "rotateSelect"
+    // 透明度选择
+    | "opacitySelect"
+    // 颜色选择
+    | "colorSelect"
+    // 配色方案
+    | "colorScheme"
+    // 字体样式
+    | "fontStyle"
+    // 位置
+    | "position"
+    // 滑块
+    | "slider"
+    // 分割线
+    | "divider"
+  rules?: any[];
+  defaultValue?: any;
+  // 分组表单项
+  children?: IFormItem[];
+  // 组件属性
+  fieldProps?: any;
+  // 自定义渲染
+  render?: (form: any) => VNode;
+  // 提示内容
+  tip?: string;
+}

+ 20 - 0
components/index.ts

@@ -0,0 +1,20 @@
+import type { App } from 'vue';
+import { components } from './components';
+export * from './components';
+
+export const install = function (app: App) {
+  Object.keys(components).forEach(key => {
+    const component = components[key as keyof typeof components] as { install?: (app: App<any>) => any };
+    if (component?.install) {
+      console.log('install', key);
+      app.use(component as any);
+    }
+  });
+  
+  return app;
+};
+
+export default {
+  version: '1.0.0',
+  install,
+};

+ 11 - 0
components/text/Title/index.ts

@@ -0,0 +1,11 @@
+import Title from './src/index.vue';
+import Config from './src/Config.vue';
+
+Title.Config = Config;
+Title.install = (app: any) => {
+  app.component('FmDashboardTitle', Title);
+  return app;
+};
+export default Title;
+export { Config };
+export { defaultPropsValue, titleProps } from './src/props';

+ 53 - 0
components/text/Title/src/Config.vue

@@ -0,0 +1,53 @@
+<!-- 组件自定义配置部分 -->
+<template>
+  <CusForm :columns="columns" v-bind="$attrs" @change="handleChange"> </CusForm>
+</template>
+
+<script setup lang="ts">
+import { defineProps, defineEmits, computed } from "vue";
+import { CusForm, type IFormItem } from "../../../cusForm";
+import { titleProps } from "./props";
+
+const props = defineProps(titleProps);
+const emit = defineEmits(["change"]);
+
+const columns = computed((): IFormItem[] => [
+  {
+    label: "内容",
+    prop: "text",
+    type: "input",
+    defaultValue: props.text,
+  },
+  {
+    label: "字体",
+    prop: "fontSize",
+    type: "inputNumber",
+    defaultValue: props.fontSize,
+  },
+  {
+    label: "颜色",
+    prop: "color",
+    type: "colorSelect",
+    defaultValue: props.color,
+  },
+  {
+    label: "对齐",
+    prop: "textAlign",
+    type: "radioGroup",
+    defaultValue: props.textAlign,
+    fieldProps: {
+      options: [
+        { label: "左对齐", value: "left" },
+        { label: "居中", value: "center" },
+        { label: "右对齐", value: "right" },
+      ],
+    },
+  },
+]);
+
+const handleChange = (val: Record<string, any>) => {
+  emit("change", val);
+};
+</script>
+
+<style lang="scss" scoped></style>

+ 32 - 0
components/text/Title/src/index.vue

@@ -0,0 +1,32 @@
+<template>
+  <div class="cus-title" :style="style">
+    {{ text }}
+  </div>
+</template>
+
+<script setup lang="ts" name="fmDashboardBasicTitle">
+import { defineProps, computed } from 'vue';
+import { titleProps } from './props';
+import { transformStyle } from '../../../../utils/transStyle';
+
+const props = defineProps(titleProps);
+const style = computed(() => {
+  const style = transformStyle(props);
+  const obj: Record<string, string> = {};
+  if(style.color.length > 9) {
+    obj.backgroundImage = style.color;
+    obj.webkitBackgroundClip = 'text';
+    obj.webkitTextFillColor = 'transparent';
+  }
+  return {
+    ...obj,
+    ...style,
+    width: '100%',
+    height: '100%',
+    lineHeight: props.height + 'px'
+  }
+});
+</script>
+
+<style lang="less" scoped>
+</style>

+ 54 - 0
components/text/Title/src/props.ts

@@ -0,0 +1,54 @@
+import { PropType } from 'vue';
+
+export const titleProps = {
+  text: {
+    type: String,
+    required: true
+  },
+  width: {
+    type: Number,
+  },
+  height: {
+    type: Number,
+  },
+  fontSize: {
+    type: Number,
+  },
+  fontWeight: {
+    type: [Number, String],
+  },
+  color: {
+    type: String,
+  },
+  backgroundColor: {
+    type: String,
+  },
+  textAlign: {
+    type: String,
+  },
+  direction: {
+    type: String as PropType<'horizontal' | 'vertical'>,
+  }
+}
+
+export const defaultPropsValue = {
+  container: {
+    style: {
+      background: {
+        type: 'none'
+      }
+    },
+    props: {
+      width: 300,
+      height: 80,
+    }
+  },
+  props: {
+    text: '标题内容',
+    fontSize: 24,
+    color: '#fff',
+    fontWeight: 'bold',
+    textAlign: 'left',
+    direction: 'horizontal'
+  }
+}

+ 1 - 0
index.js

@@ -0,0 +1 @@
+export * from "./components";

+ 1 - 0
lib/demo.html

@@ -0,0 +1 @@
+<!doctype html><meta charset="utf-8"><title>shalu-dashboard-ui demo</title><script src="./shalu-dashboard-ui.umd.js"></script><link rel="stylesheet" href="./shalu-dashboard-ui.css"><script>console.log(window['shalu-dashboard-ui'])</script>

File diff suppressed because it is too large
+ 258429 - 0
lib/shalu-dashboard-ui.common.js


File diff suppressed because it is too large
+ 1 - 0
lib/shalu-dashboard-ui.common.js.map


File diff suppressed because it is too large
+ 1 - 0
lib/shalu-dashboard-ui.css


File diff suppressed because it is too large
+ 258440 - 0
lib/shalu-dashboard-ui.umd.js


File diff suppressed because it is too large
+ 1 - 0
lib/shalu-dashboard-ui.umd.js.map


File diff suppressed because it is too large
+ 19 - 0
lib/shalu-dashboard-ui.umd.min.js


File diff suppressed because it is too large
+ 1 - 0
lib/shalu-dashboard-ui.umd.min.js.map


+ 27 - 7
package.json

@@ -4,13 +4,15 @@
   "private": true,
   "scripts": {
     "serve": "vue-cli-service serve",
-    "build": "vue-cli-service build",
+    "build": "vue-cli-service build --target lib --dest lib index.js",
     "test:unit": "vue-cli-service test:unit",
     "lint": "vue-cli-service lint"
   },
   "dependencies": {
     "core-js": "^3.8.3",
-    "vue": "^3.2.13"
+    "@vueuse/core": "^10.11.0",
+    "echarts": "^5.5.1",
+    "lodash": "^4.17.21"
   },
   "devDependencies": {
     "@types/jest": "^27.0.1",
@@ -33,7 +35,25 @@
     "lint-staged": "^11.1.2",
     "prettier": "^2.4.1",
     "ts-jest": "^27.0.4",
-    "typescript": "~4.5.5"
+    "typescript": "~4.5.5",
+    "@ant-design/icons-vue": "^7.0.1",
+    "@codemirror/lang-javascript": "^6.2.2",
+    "@codemirror/lang-json": "^6.0.1",
+    "@codemirror/theme-one-dark": "^6.1.2",
+    "ant-design-vue": "^4.2.3",
+    "css-loader": "^7.1.2",
+    "element-plus": "^2.7.6",
+    "js-beautify": "^1.15.1",
+    "less-loader": "^12.2.0",
+    "progress-bar-webpack-plugin": "^2.1.0",
+    "style-loader": "^4.0.0",
+    "vue": "^3.2.13",
+    "vue-codemirror": "^6.1.1",
+    "vue-hooks-plus": "^2.2.0",
+    "vue-loader": "^17.4.2",
+    "webpack": "^5.92.1",
+    "webpack-cli": "^5.1.4",
+    "webpack-dev-server": "^5.0.4"
   },
   "eslintConfig": {
     "root": true,
@@ -43,13 +63,14 @@
     "extends": [
       "plugin:vue/vue3-essential",
       "eslint:recommended",
-      "@vue/typescript/recommended",
-      "plugin:prettier/recommended"
+      "@vue/typescript/recommended"
     ],
     "parserOptions": {
       "ecmaVersion": 2020
     },
-    "rules": {},
+    "rules": {
+      "vue/multi-word-component-names":"off"
+    },
     "overrides": [
       {
         "files": [
@@ -72,7 +93,6 @@
     "preset": "@vue/cli-plugin-unit-jest/presets/typescript-and-babel"
   },
   "gitHooks": {
-    "pre-commit": "lint-staged"
   },
   "lint-staged": {
     "*.{js,jsx,vue,ts,tsx}": "vue-cli-service lint"

File diff suppressed because it is too large
+ 10844 - 0
pnpm-lock.yaml


+ 41 - 5
src/App.vue

@@ -1,16 +1,52 @@
 <template>
-  <img alt="Vue logo" src="./assets/logo.png" />
-  <HelloWorld msg="Welcome to Your Vue.js + TypeScript App" />
+  <FmDashboardBasicBar :width="400" :height="300" :dataSource="dataSource" />
+  <FmDashboardBasicLine :width="400" :height="300" :dataSource="lineDataSource" />
 </template>
 
 <script lang="ts">
 import { defineComponent } from "vue";
-import HelloWorld from "./components/HelloWorld.vue";
 
 export default defineComponent({
   name: "App",
-  components: {
-    HelloWorld,
+  setup: () => {
+    return {
+      dataSource: {
+        sourceType: 0,
+        data: {
+          xData: ["轴标签A", "轴标签B", "轴标签C", "轴标签D"],
+          series: [
+            {
+              type: "bar",
+              name: "系列1",
+              data: [89.3, 92.1, 94.4, 85.4],
+            },
+            {
+              type: "bar",
+              name: "系列2",
+              data: [95.8, 89.4, 91.2, 76.9],
+            },
+          ],
+        },
+      },
+      lineDataSource: {
+        sourceType: 0,
+        data: {
+          xData: ["轴标签A", "轴标签B", "轴标签C", "轴标签D"],
+          series: [
+            {
+              type: "line",
+              name: "系列1",
+              data: [89.3, 92.1, 94.4, 85.4],
+            },
+            {
+              type: "line",
+              name: "系列2",
+              data: [95.8, 89.4, 91.2, 76.9],
+            },
+          ],
+        },
+      },
+    };
   },
 });
 </script>

+ 7 - 1
src/main.ts

@@ -1,4 +1,10 @@
 import { createApp } from "vue";
 import App from "./App.vue";
+import shaluDashboard from "../components/index"
 
-createApp(App).mount("#app");
+
+const app = createApp(App)
+
+app.use(shaluDashboard)
+
+app.mount("#app");

+ 3 - 1
tsconfig.json

@@ -33,7 +33,9 @@
     "src/**/*.tsx",
     "src/**/*.vue",
     "tests/**/*.ts",
-    "tests/**/*.tsx"
+    "tests/**/*.tsx",
+    "types/**/*.d.ts",
+    "types/**/*.ts"
   ],
   "exclude": [
     "node_modules"

+ 1 - 0
types/index.d.ts

@@ -0,0 +1 @@
+declare module 'lodash';

+ 61 - 0
utils/transStyle.ts

@@ -0,0 +1,61 @@
+/**
+ * 转换样式对象
+ * @param style
+ */
+export function transformStyle(style: Record<string, any>) {
+  const styleObj: Record<string, any> = {};
+  for (const key in style) {
+    if (typeof style[key] === "object") {
+      switch (key) {
+        // 背景色对象
+        case "background":
+          if (style[key].type === "none") styleObj[key] = "none";
+          if (style[key].type === "color") styleObj[key] = style[key].color;
+          if (style[key].type === "image") {
+            styleObj[
+              key + "-image"
+            ] = `url(${style[key].image}) no-repeat center center`;
+            styleObj[key + "-size"] = style[key].fillType;
+          }
+          break;
+        // 颜色对象
+        case "border":
+          if (style[key].type === "none") styleObj[key] = "none";
+          else
+            styleObj[
+              key
+            ] = `${style[key].width}px ${style[key].type} ${style[key].color}`;
+          break;
+        // 边框对象
+        case "borderRadius":
+          if (style[key].type === "all")
+            styleObj[key] = `${style[key].value}${style[key].unit}`;
+          else
+            styleObj[
+              key
+            ] = `${style[key].topLeft}${style[key].unit} ${style[key].topRight}${style[key].unit} ${style[key].bottomLeft}${style[key].unit} ${style[key].bottomRight}${style[key].unit}`;
+          break;
+        // 阴影对象
+        case "boxShadow":
+          if (style[key].enabled) {
+            styleObj[key] = `${style[key].offsetX}px ${style[key].offsetY}px ${
+              style[key].blurRadius
+            }px ${style[key].spreadRadius}px ${style[key].color} ${
+              style[key].inset ? "inset" : ""
+            }`;
+          }
+          break;
+        // 毛玻璃对象
+        case "backdropFilter":
+          if (style[key].enabled) styleObj[key] = `blur(${style[key].blur}px)`;
+          break;
+      }
+    } else if (typeof style[key] === "number") {
+      styleObj[key] = style[key] + "px";
+    } else if (typeof style[key] === "string") {
+      styleObj[key] = style[key];
+    }
+  }
+
+  return styleObj;
+}

File diff suppressed because it is too large
+ 0 - 8568
yarn.lock