Merge branch 'feature/new-sys' into 'master'

悦码项目首次上线

See merge request workbench/y-code!12
This commit is contained in:
林梓阳 2024-07-26 18:09:27 +08:00
commit 7247ee9c13
34 changed files with 1908 additions and 268 deletions

View File

@ -4,3 +4,5 @@ VITE_NODE_ENV = pre
VITE_OA_BASEURL = https://oa-pre.shiyue.com
VITE_YCODE_BASEURL = https://custom-chart-pre-api.shiyue.com
VITE_YCODE_BASEURL_FRONT = https://custom-chart.shiyue.com

View File

@ -3,3 +3,5 @@ VITE_NODE_ENV = prod
VITE_OA_BASEURL = https://oa.shiyuegame.com
VITE_YCODE_BASEURL = https://custom-chart-api.shiyuegame.com
VITE_YCODE_BASEURL_FRONT = https://custom-chart.shiyuegame.com

View File

@ -3,3 +3,5 @@ VITE_NODE_ENV = pre
VITE_OA_BASEURL = https://oa-pre.shiyue.com
VITE_YCODE_BASEURL = https://custom-chart-pre-api.shiyue.com
VITE_YCODE_BASEURL_FRONT = https://custom-chart.shiyue.com

View File

@ -12,7 +12,7 @@ module.exports = {
ecmaVersion: 'latest',
},
rules: {
semi: 2,
semi: 0,
'vue/multi-word-component-names': 0,
indent: [
2, 2, {

1
.npmrc
View File

@ -1 +0,0 @@
registry=http://sy-registry.shiyue.com

10
components.d.ts vendored
View File

@ -12,9 +12,9 @@ declare module 'vue' {
AButton: typeof import('ant-design-vue/es')['Button']
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
ACol: typeof import('ant-design-vue/es')['Col']
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
AEmpty: typeof import('ant-design-vue/es')['Empty']
AFloatButton: typeof import('ant-design-vue/es')['FloatButton']
AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem']
@ -25,14 +25,18 @@ declare module 'vue' {
AModal: typeof import('ant-design-vue/es')['Modal']
APagination: typeof import('ant-design-vue/es')['Pagination']
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
ARow: typeof import('ant-design-vue/es')['Row']
ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASpace: typeof import('ant-design-vue/es')['Space']
ASpin: typeof import('ant-design-vue/es')['Spin']
ASwitch: typeof import('ant-design-vue/es')['Switch']
ATable: typeof import('ant-design-vue/es')['Table']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Table: typeof import('./src/components/common/table.vue')['default']
YChart: typeof import('./src/components/common/y-chart.vue')['default']
YTable: typeof import('./src/components/common/y-table.vue')['default']
}

View File

@ -7,7 +7,7 @@
<title>悦码后台</title>
</head>
<body>
<div id="app"></div>
<div id="y-code-app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

801
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,8 @@
"private": true,
"type": "module",
"scripts": {
"dev": "vite --mode staging",
"dev": "cross-env DEV_ENV=development vite --mode staging",
"pro": "cross-env DEV_ENV=development vite --mode production",
"build:pre": "vite build --mode staging",
"build:pro": "vite build --mode production",
"type-check": "vue-tsc --build --force",
@ -15,10 +16,14 @@
"@vueuse/core": "^10.11.0",
"ant-design-vue": "^4.1.2",
"axios": "^1.6.7",
"cross-env": "^7.0.3",
"lodash": "^4.17.21",
"p-limit": "^6.1.0",
"pinia": "^2.1.7",
"vue": "^3.4.15",
"vue-router": "^4.2.5"
"vue-grid-layout": "^3.0.0-beta1",
"vue-router": "^4.2.5",
"yargs-parser": "^21.1.1"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.3.3",
@ -35,6 +40,7 @@
"typescript": "~5.3.0",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.0.11",
"vite-plugin-qiankun": "^1.0.15",
"vue-tsc": "^1.8.27"
}
}
}

View File

@ -1,4 +1,4 @@
import { get } from '@/utils/request';
import { get } from "@/utils/request";
export interface UserInfoType {
alias: string;
@ -15,8 +15,20 @@ export interface UserInfoType {
username: string;
}
export const getUserInfo = () => get<UserInfoType>({
url: '/api/home/grade',
});
interface DropListItem {
label: string;
value: string | number;
mark: string;
}
export const logout = () => get({ url: '/api/common/logout' });
export const getUserInfo = () =>
get<UserInfoType>({
url: "/api/home/grade",
});
export const logout = () => get({ url: "/api/common/logout" });
export const getProjectDrop = () =>
get<DropListItem[]>({
url: "/api/v1/project/get-project-drop",
});

View File

@ -1,28 +0,0 @@
import { get, post } from "@/utils/request";
// 预览
export function preview({ modularId, fieldIds, page, perPage, filter }) {
return post({
url: "api/v1/preview/view",
data: {
modular_id: modularId,
field_ids: fieldIds,
page,
per_page: perPage,
filter,
},
});
}
// 查看视图
export function searchInfo({ previewId, page, perPage, filter }) {
return get({
url: `/api/v1/preview/info`,
params: {
preview_id: previewId,
page,
per_page: perPage,
filter,
},
});
}

21
src/api/preview/index.ts Normal file
View File

@ -0,0 +1,21 @@
import { post } from "@/utils/request";
interface PreviewItemParams {
previewId: string | number;
filter?: string | [];
page?: number;
perPage?: number;
}
// 查看视图
export function searchInfo(data: PreviewItemParams) {
return post({
url: `/api/v1/preview/info`,
data: {
preview_id: data.previewId,
filter: data.filter,
page: data.page,
per_page: data.perPage,
},
});
}

View File

@ -1,34 +1,120 @@
<template>
<div class="chart-show-box">
<div class="switch-type">
<div class="chart-name">
<div class="title">{{ title }}</div>
</div>
<div class="chart-header">
<div class="chart-filter">
<div v-for="(item, index) in filterConfig" :key="index" class="filter-item">
<div>
<a-radio-group v-model:value="dateType" button-style="solid">
<a-radio-button value="day"></a-radio-button>
<a-radio-button value="week"></a-radio-button>
<a-radio-button value="month"></a-radio-button>
</a-radio-group>
<a-range-picker v-if="item.type === 'time'" class="date-item" v-model:value="filterData[item.name]" :picker="rangePicker" @change="toFilt" />
</div>
</div>
</div>
<a-radio-group v-model:value="chartType">
<a-radio-button value="line">折线图</a-radio-button>
<a-radio-button value="bar">柱状图</a-radio-button>
</a-radio-group>
</div>
<div class="chart-wrap">
<Column v-if="chartType === 'bar'" />
<Column v-if="chartType === 'bar'" :config="currentChart" />
<Line v-if="chartType === 'line'" :config="currentChart" />
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import { computed, ref, watch } from "vue";
import Line from "@/plugins/antv-g2plot/line.vue";
import Column from "@/plugins/antv-g2plot/column.vue";
import _ from 'lodash';
const props = defineProps({
title: {
type: String,
default: "",
},
chartCfg: {
type: Object,
default: () => ({}),
},
filterConfig: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(["toFilt"]);
const chartType = ref("line");
const dateType = ref("day");
const filterData = ref({
});
const rangePicker = computed(() => {
switch(dateType.value) {
case 'week':
return 'week';
case 'month':
return 'month';
default:
return 'date';
}
});
const currentChart = computed(() => {
return props.chartCfg[chartType.value];
})
const toFilt = () => {
const cloneFilter = _.cloneDeep(props.filterConfig);
const filter = cloneFilter
.filter((item) => {
return filterData.value[item.name] !== undefined && filterData.value[item.name] !== null;
})
.map((item) => {
return item.type === 'time' ? {
name: item.name,
type: item.type,
start_time: filterData.value[item.name][0].format('YYYY-MM-DD'),
end_time: filterData.value[item.name][1].format('YYYY-MM-DD'),
date_type: dateType.value,
} : {
name: item.name,
type: item.type,
value: filterData.value[item.name],
}
})
emit('toFilt', {
filter,
});
};
</script>
<style lang="less" scoped>
.chart-wrap {
padding: 20px;
}
.chart-name {
margin-bottom: 8px;
.title {
font-size: 18px;
font-weight: bold;
}
}
.chart-header {
display: flex;
justify-content: space-between;
}
.date-item {
margin-left: 10px;
}
</style>

View File

@ -1,5 +1,8 @@
<template>
<div class="y-table-container">
<div class="y-table-name">
<div class="title">{{ title }}</div>
</div>
<div class="y-table-filter">
<div
v-for="(item, index) in filterConfig"
@ -16,13 +19,14 @@
@change="toFilt"
></a-select>
<a-input
v-if="item.type === 'text'"
v-else-if="item.type === 'text'"
class="input-item"
placeholder="请输入"
allow-clear
v-model:value="filterData[item.name]"
@change="toFilt"
/>
<a-range-picker v-else-if="item.type === 'time'" class="date-item" v-model:value="filterData[item.name]" @change="toFilt" />
</div>
</div>
<div class="y-table-content">
@ -30,6 +34,7 @@
:columns="columnConfig"
:data-source="dataList"
:pagination="false"
:scroll="{ x: 1000, y: `calc(100vh - 280px)` }"
size="small"
bordered
></a-table>
@ -68,8 +73,12 @@ const props = defineProps({
type: Number,
default: 0,
},
title: {
type: String,
default: "",
},
});
const emit = defineEmits(["handleFilt"]);
const emit = defineEmits(["toFilt"]);
const filterData = ref({});
@ -78,25 +87,19 @@ const pageState = reactive({
perPage: 20,
});
watch(
() => props.filterConfig,
(newVal) => {
console.log("newVal", newVal);
newVal.forEach((item) => {
filterData.value[item.name] = undefined;
});
}
);
const getData = () => {
const cloneFilter = _.cloneDeep(props.filterConfig);
const filter = cloneFilter
.filter((item) => {
return filterData.value[item.name] !== undefined;
return filterData.value[item.name] !== undefined && filterData.value[item.name] !== null;
})
.map((item) => {
return {
return item.type === 'time' && filterData.value[item.name] ? {
name: item.name,
type: item.type,
start_time: filterData.value[item.name][0].format('YYYY-MM-DD'),
end_time: filterData.value[item.name][1].format('YYYY-MM-DD'),
} : {
name: item.name,
type: item.type,
value: filterData.value[item.name],
@ -119,6 +122,13 @@ const pageChange = () => {
</script>
<style lang="less" scoped>
.y-table-name {
margin-bottom: 10px;
.title {
font-size: 18px;
font-weight: bold;
}
}
.y-table-filter {
display: flex;
flex-wrap: wrap;
@ -126,14 +136,19 @@ const pageChange = () => {
.filter-item {
margin-right: 10px;
margin-bottom: 6px;
font-size: 14px;
}
.input-item {
width: 180px;
}
.date-item {
width: 240px;
}
.y-table-content {
margin-top: 10px;
}
.pagination-box {
text-align: center;
margin-top: 10px;
}
</style>

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { useUserInfoStore } from "@/stores/useUserInfoStore";
import { onMounted, ref } from "vue";
import { computed, onMounted, ref } from "vue";
import Header from "./components/Header.vue";
import Sider from "./components/Sider.vue";
import {
@ -9,6 +9,11 @@ import {
FullscreenExitOutlined,
} from "@ant-design/icons-vue";
import { useEventListener } from "@vueuse/core";
import { qiankunWindow } from 'vite-plugin-qiankun/dist/helper'
const __POWERED_BY_QIANKUN__ = computed(() => {
return qiankunWindow.__POWERED_BY_QIANKUN__ || window.proxy?.__POWERED_BY_QIANKUN__
})
// const userInfoStore = useUserInfoStore();
const isCollapsed = ref(false);
@ -34,7 +39,7 @@ const handleExitFullscreen = () => {
</script>
<template>
<section class="root">
<section v-if="!__POWERED_BY_QIANKUN__" class="root">
<section
class="left-aside"
:class="{ 'left-aside-collapsed': isCollapsed }"
@ -68,6 +73,7 @@ const handleExitFullscreen = () => {
</a-float-button>
</section>
</section>
<router-view v-else />
</template>
<style lang="less" scoped>

View File

@ -1,12 +1,55 @@
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router from './router';
import './global.less';
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
import { createProjectRouter } from './router'
import "./global.less";
import VueGridLayout from "vue-grid-layout"; // 引入layout
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper'
const app = createApp(App);
let app
function render(props: Object = {}) {
app = createApp(App);
const router = createProjectRouter(props.base)
app.use(router)
app.use(VueGridLayout);
app.use(createPinia())
app.mount("#y-code-app")
// const getContainer = () => {
// props.container ? props.container.querySelector('#y-code-container') : document.getElementById('y-code-container')
// }
// const container = getContainer()
// if (container) {
// app.mount(container)
// } else {
// window.addEventListener("DOMContentLoaded", () => {
// app.mount(getContainer())
// })
// }
}
app.use(createPinia());
app.use(router);
const __POWERED_BY_QIANKUN__ = qiankunWindow.__POWERED_BY_QIANKUN__ || window.proxy?.__POWERED_BY_QIANKUN__
if (__POWERED_BY_QIANKUN__) {
renderWithQiankun({
bootstrap() {
console.log('bootstrap')
return Promise.resolve()
},
mount(props) {
console.log('mount')
render(props)
return Promise.resolve()
},
unmount() {
console.log('unmount')
if (app) {
app.unmount()
}
return Promise.resolve()
},
update() {},
})
} else {
render()
}
app.mount('#app');

View File

@ -22,5 +22,5 @@ const props = defineProps({
},
});
const { container } = useChart(Column, props.config);
const { container } = useChart(Column, props);
</script>

View File

@ -3,6 +3,7 @@
</template>
<script setup>
import { Line } from "@antv/g2plot";
// hooks
import useChart from "./useChart";
@ -22,5 +23,5 @@ const props = defineProps({
},
});
const { container } = useChart(Line, props.config);
const { container } = useChart(Line, props);
</script>

View File

@ -22,5 +22,5 @@ const props = defineProps({
},
});
const { container } = useChart(Pie, props.config);
const { container } = useChart(Pie, props);
</script>

View File

@ -1,20 +1,20 @@
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
import _ from "lodash";
export default function useChart(ChartClass, config) {
export default function useChart(ChartClass, props) {
const chart = ref(null); // 表格实例
const chartOptions = ref(null); // 图表配置
const container = ref(null); // 渲染图表元素
const { onReady, onEvent } = config;
const { onReady, onEvent } = props.config;
// 全局事件侦听器
let handler;
onMounted(() => {
chartOptions.value = _.cloneDeep(config);
chartOptions.value = _.cloneDeep(props.config);
// 实例化图表
const chartInstance = new ChartClass(container.value, { ...config });
const chartInstance = new ChartClass(container.value, { ...props.config });
chartInstance.toDataURL = (type, encoderOptions) => {
return toDataURL(type, encoderOptions);
};
@ -46,7 +46,7 @@ export default function useChart(ChartClass, config) {
// 配置更改时更新图表
watch(
() => config,
() => props.config,
(config) => {
const newConfig = _.cloneDeep(config);
chartOptions.value = newConfig;

View File

@ -1,13 +1,19 @@
import { createRouter, createWebHistory } from 'vue-router';
import { createRouter, createWebHistory, type Router } from 'vue-router';
import { titleGuard } from './guards';
import routeList from './routes';
import { qiankunWindow } from 'vite-plugin-qiankun/dist/helper'
const router = createRouter({
history: createWebHistory(''),
routes: routeList,
});
let router: Router | null = null
export const createProjectRouter = (base = '') => {
const __POWERED_BY_QIANKUN__ = qiankunWindow.__POWERED_BY_QIANKUN__ || window.proxy?.__POWERED_BY_QIANKUN__
router = createRouter({
history: createWebHistory(base || (__POWERED_BY_QIANKUN__ ? '/y-code-app/' : '')),
routes: routeList,
})
// 全局前置守卫
router.beforeEach(titleGuard);
// 全局前置守卫
router.beforeEach(titleGuard)
return router
}
export default router;

View File

@ -1,7 +1,11 @@
import Layout from '@/layout/index.vue';
import { HomeOutlined, BarChartOutlined } from '@ant-design/icons-vue';
import { h } from 'vue';
import type { VNode, RendererNode, RendererElement } from 'vue';
import Layout from "@/layout/index.vue";
import {
HomeOutlined,
BarChartOutlined,
AppstoreOutlined,
} from "@ant-design/icons-vue";
import { h } from "vue";
import type { VNode, RendererNode, RendererElement } from "vue";
export interface RouteType {
path: string;
@ -11,70 +15,96 @@ export interface RouteType {
isMenu?: boolean;
redirect?: string;
children: RouteType[];
icon?: () => VNode<RendererNode, RendererElement, {
[key: string]: any;
}>
icon?: () => VNode<
RendererNode,
RendererElement,
{
[key: string]: any;
}
>;
}
const routeList: RouteType[] = [
{
path: '/',
name: 'layout',
path: "/",
name: "layout",
component: Layout,
meta: { title: '首页' },
meta: { title: "首页" },
children: [
{
path: '',
name: '-',
path: "",
name: "-",
meta: {},
children: [],
redirect: '/config-manage/project-cfg',
redirect: "/config-manage/project-cfg",
},
{
path: '/config-manage',
name: 'config-manage',
path: "/config-manage",
name: "config-manage",
isMenu: true,
meta: { title: '配置管理' },
meta: { title: "配置管理" },
icon: () => h(HomeOutlined),
children: [
{
path: 'project-cfg',
name: 'project-cfg',
component: () => import('@/views/config-manage/project-cfg/index.vue'),
meta: { title: '项目配置' },
path: "project-cfg",
name: "project-cfg",
component: () =>
import("@/views/config-manage/project-cfg/index.vue"),
meta: { title: "项目配置" },
isMenu: true,
children: [],
},
{
path: 'module-cfg',
name: 'module-cfg',
component: () => import('@/views/config-manage/module-cfg/index.vue'),
meta: { title: '数据表配置' },
path: "module-cfg",
name: "module-cfg",
component: () =>
import("@/views/config-manage/module-cfg/index.vue"),
meta: { title: "数据来源配置" },
isMenu: true,
children: [],
},
],
},
{
path: '/view-all-manage',
name: 'view-all-manage',
path: "/view-all-manage",
name: "view-all-manage",
isMenu: true,
meta: { title: '视图管理' },
meta: { title: "视图管理" },
icon: () => h(BarChartOutlined),
children: [
{
path: 'view-list',
name: 'view-list',
component: () => import('@/views/view-all-manage/view-list/index.vue'),
meta: { title: '视图列表' },
path: "view-list",
name: "view-list",
component: () =>
import("@/views/view-all-manage/view-list/index.vue"),
meta: { title: "视图列表" },
isMenu: true,
children: [],
},
{
path: 'create-view',
name: 'create-view',
component: () => import('@/views/view-all-manage/create-view/index.vue'),
meta: { title: '创建视图' },
path: "create-view",
name: "create-view",
component: () =>
import("@/views/view-all-manage/create-view/index.vue"),
meta: { title: "创建视图" },
isMenu: true,
children: [],
},
],
},
{
path: "/page-show-info",
name: "page-show-info",
isMenu: true,
meta: { title: "视图预览" },
icon: () => h(AppstoreOutlined),
children: [
{
path: "page-info",
name: "page-info",
component: () =>
import("@/views/page-show-info/page-info/index,.vue"),
meta: { title: "项目报表" },
isMenu: true,
children: [],
},

View File

@ -12,11 +12,12 @@
placeholder="请选择所属项目"
v-model:value="formData.project_id"
:options="projectSelect"
@change="toGetDbTable"
/>
</a-form-item>
<a-form-item label="数据表名称" name="modular_name">
<a-form-item label="数据来源" name="modular_name">
<a-input
placeholder="请输入数据表名称"
placeholder="请输入数据来源"
v-model:value="formData.modular_name"
/>
</a-form-item>
@ -27,26 +28,90 @@
:unCheckedValue="0"
/>
</a-form-item>
<a-form-item label="展示类型" name="show_type_id">
<a-select
placeholder="请选择展示类型"
v-model:value="formData.show_type_id"
:options="showTypes"
/>
<a-form-item label="数据源类型" name="original_type">
<a-radio-group v-model:value="formData.original_type">
<a-radio :value="1">自定义</a-radio>
<a-radio :value="2">指定表</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="sql数据源" name="original_sql">
<a-form-item
v-if="formData.original_type === 1"
label="sql数据源"
name="original_sql"
>
<a-input
placeholder="请输入sql数据源"
v-model:value="formData.original_sql"
/>
</a-form-item>
<a-form-item
v-if="formData.original_type === 2"
label="数据表"
name="table"
>
<a-select
placeholder="请先选择项目再选择数据表"
v-model:value="formData.table"
:options="tableTypes"
/>
</a-form-item>
<a-form-item label="数据库特殊配置" name="is_other_database">
<a-switch
v-model:checked="formData.is_other_database"
:checkedValue="1"
:unCheckedValue="0"
@change="isOtherChange"
/>
</a-form-item>
<template v-if="formData.is_other_database">
<a-form-item label="数据库驱动" name="drive_type">
<a-radio-group v-model:value="formData.drive_type">
<a-radio :value="1">MySQL</a-radio>
<a-radio :value="2">Click House</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="数据库地址" name="database_address">
<a-input
placeholder="请输入数据库地址"
v-model:value="formData.database_address"
/>
</a-form-item>
<a-form-item label="数据库端口" name="database_port">
<a-input-number
placeholder="请输入数据库端口"
v-model:value="formData.database_port"
style="width: 200px"
/>
</a-form-item>
<a-form-item label="数据库名称" name="database_name">
<a-input
placeholder="请输入数据库名称"
v-model:value="formData.database_name"
/>
</a-form-item>
<a-form-item label="数据库用户" name="database_username">
<a-input
placeholder="请输入数据库用户"
v-model:value="formData.database_username"
/>
</a-form-item>
<a-form-item label="数据库密码" name="database_password">
<a-space>
<a-input
placeholder="请输入数据库密码"
v-model:value="formData.database_password"
/>
</a-space>
</a-form-item>
</template>
</a-form>
</a-modal>
</template>
<script setup>
import { onMounted, ref, watch } from "vue";
import { getShowTypeSelect } from "@/views/config-manage/module-cfg/service";
import { ref, watch } from "vue";
import { getDbTableSelect } from "@/views/config-manage/module-cfg/service";
const props = defineProps({
open: {
@ -70,20 +135,31 @@ const props = defineProps({
const emit = defineEmits(["ok"]);
const formRules = ref({
modular_name: [
{ required: true, message: "请输入数据表名称", trigger: "submit" },
{ required: true, message: "请输入数据来源", trigger: "submit" },
],
show_type_id: [{ required: true, message: "请选择", trigger: "submit" }],
// show_type_id: [{ required: true, message: "", trigger: "submit" }],
original_type: [{ required: true, message: "请选择", trigger: "submit" }],
original_sql: [{ required: true, message: "请输入", trigger: "submit" }],
table: [{ required: true, message: "请选择", trigger: "submit" }],
});
const showTypes = ref([]);
const tableTypes = ref([]);
const formRef = ref();
const formData = ref({
project_id: undefined,
modular_name: undefined,
is_show: 0,
show_type_id: undefined,
original_type: 1, // 1 - 2 -
// show_type_id: undefined,
original_sql: undefined,
table: undefined,
is_other_database: 0,
drive_type: 1,
database_address: undefined,
database_port: undefined,
database_name: undefined,
database_username: undefined,
database_password: undefined,
});
watch(
@ -97,30 +173,53 @@ watch(
project_id: newVal.project_id,
modular_name: newVal.modular_name,
is_show: newVal.is_show,
show_type_id: newVal.show_type_id,
original_type: newVal.original_type,
// show_type_id: newVal.show_type_id,
original_sql: newVal.original_sql,
table: newVal.table,
drive_type: newVal.drive_type,
is_other_database: newVal.is_other_database,
database_address: newVal.database_address,
database_port: newVal.database_port,
database_name: newVal.database_name,
database_username: newVal.database_username,
database_password: newVal.database_password,
};
}
}
);
onMounted(() => {
toGetShowType();
});
const toGetShowType = () => {
getShowTypeSelect().then((res) => {
showTypes.value = res.data;
const toGetDbTable = () => {
getDbTableSelect({ projectId: formData.value.project_id }).then((res) => {
tableTypes.value = res.data;
});
};
const isOtherChange = (val) => {
formData.value.drive_type = val ? 1 : undefined
formData.value.database_address = undefined
formData.value.database_port = undefined
formData.value.database_name = undefined
formData.value.database_username = undefined
formData.value.database_password = undefined
}
const resetFormData = () => {
formData.value = {
project_id: undefined,
modular_name: undefined,
is_show: 0,
show_type_id: undefined,
original_type: 1, // 1 - 2 -
// show_type_id: undefined,
original_sql: undefined,
table: undefined,
is_other_database: 0,
drive_type: undefined,
database_address: undefined,
database_port: undefined,
database_name: undefined,
database_username: undefined,
database_password: undefined,
};
};

View File

@ -34,18 +34,33 @@
<a-input
v-if="editableData[record.field_id]"
v-model:value="record[column.dataIndex]"
allow-clear
placeholder="请输入"
/>
<template v-else>
{{ record[column.dataIndex] }}
</template>
</template>
<template v-if="column.dataIndex === 'field_numerical_name'">
<a-select
v-if="editableData[record.field_id]"
v-model:value="record.field_numerical_type_id"
:options="fieldNumTypeSel"
placeholder="请选择"
allow-clear
style="width: 160px">
</a-select>
<template v-else>
{{ record.field_numerical_name }}
</template>
</template>
<template v-if="column.dataIndex === 'field_type_name'">
<a-select
v-if="editableData[record.field_id]"
v-model:value="record.field_type_id"
:options="fieldTypeSel"
placeholder="请选择"
allow-clear
style="width: 160px"
>
</a-select>
@ -64,17 +79,30 @@
{{ record.is_search ? "是" : "否" }}
</template>
</template>
<template v-if="column.dataIndex === 'original_type'">
<a-select
v-if="editableData[record.field_id]"
placeholder="请选择"
v-model:value="record.original_type"
:options="originalTypes"
allow-clear
>
</a-select>
<template v-else>
{{ record.original_type_name }}
</template>
</template>
<template v-if="column.dataIndex === 'action'">
<a-space v-if="editableData[record.field_id]">
<a-button type="primary" size="small" @click="handleSave(record)"
>保存</a-button
>保存</a-button
>
<a-button size="small" @click="handleCancel(record)"
>取消</a-button
>取消</a-button
>
</a-space>
<a-button v-else type="link" @click="handleEdit(record)"
>修改</a-button
>修改</a-button
>
</template>
</template>
@ -93,9 +121,10 @@
<script setup>
import { onMounted, reactive, ref, watch } from "vue";
import { viewCfgCols } from "@/views/config-manage/module-cfg/config";
import { viewCfgCols, originalTypes } from "@/views/config-manage/module-cfg/config";
import {
getFieldTypeSelect,
getFieldNumSelect,
getFieldList,
// deleteField,
saveField,
@ -117,6 +146,7 @@ const listLoading = ref(false);
const fieldName = ref("");
const dataList = ref([]);
const fieldTypeSel = ref([]);
const fieldNumTypeSel = ref([]);
const pageState = reactive({
page: 1,
perPage: 20,
@ -135,15 +165,23 @@ watch(
onMounted(() => {
toGetFieldTypes();
toGetFieldNumSelect();
});
//
//
const toGetFieldTypes = () => {
getFieldTypeSelect().then((res) => {
fieldTypeSel.value = res.data;
});
};
//
const toGetFieldNumSelect = () => {
getFieldNumSelect().then((res) => {
fieldNumTypeSel.value = res.data;
})
};
//
const toGetList = () => {
listLoading.value = true;
@ -172,9 +210,11 @@ const addField = () => {
field_id: new Date().getTime() + "",
field_title: undefined,
field_name: undefined,
field_numerical_type_id: undefined,
is_search: 0,
field_type_id: undefined,
belong_to_table: undefined,
original_type: undefined,
original_sql: undefined,
sort: 0,
};
@ -200,21 +240,38 @@ const handleCancel = (record) => {
const handleSave = (record) => {
const params = {
field_id: record.field_id,
field_title: record.field_title,
field_name: record.field_name,
is_search: record.is_search,
field_type_id: record.field_type_id,
belong_to_table: record.belong_to_table,
original_sql: record.original_sql,
modular_id: props.modularId,
};
if (typeof params.field_id === "string") {
//
delete params.field_id;
} else {
//
const validateFields = [
{ field: 'field_title', msg: "请填写字段标题" },
{ field: 'field_name', msg: "请填写字段名称" },
{ field: 'field_numerical_type_id', msg: "请选择字段类型" },
{ field: 'belong_to_table', msg: "请填写关联表" },
{ field: 'original_type', msg: '请选择数据源类型' },
{ field: 'original_sql', msg: "请填写sql数据源" },
]
for(let i = 0; i < validateFields.length; i++) {
const curr = validateFields[i];
if (!record[curr.field]) {
message.error(curr.msg);
return;
} else {
params[curr.field] = record[curr.field];
}
}
if (record.is_search && !record.field_type_id) {
message.error("请选择搜索类型");
return;
}
//
if (typeof record.field_id === "number") {
params.field_id = record.field_id;
}
saveField(params).then(() => {
delete editableData[record.field_id];
message.success("保存成功");

View File

@ -1,9 +1,9 @@
export const moduleCfgCols = [
{ dataIndex: 'modular_id', title: '编号', align: 'center'},
{ dataIndex: 'modular_name', title: '数据名称', align: 'center'},
{ dataIndex: 'modular_name', title: '数据来源名称', align: 'center'},
{ dataIndex: 'project_name', title: '项目名称', align: 'center'},
{ dataIndex: 'is_show', title: '展示状态', align: 'center'},
{ dataIndex: 'show_type_handle', title: '展示类型', align: 'center'},
{ dataIndex: 'original_type_handle', title: '数据源类型', align: 'center'},
// { dataIndex: 'sort', title: '排序', align: 'center'},
{ dataIndex: 'action', title: '操作', align: 'center'},
];
@ -11,10 +11,17 @@ export const moduleCfgCols = [
export const viewCfgCols = [
{ dataIndex: 'field_name', title: '字段名称', align: 'center'},
{ dataIndex: 'field_title', title: '字段标题', align: 'center'},
{ dataIndex: 'field_type_name', title: '字段类型', align: 'center'},
{ dataIndex: 'field_numerical_name', title: '字段类型', align: 'center'},
{ dataIndex: 'field_type_name', title: '搜索类型', align: 'center'},
{ dataIndex: 'is_search', title: '是否可搜索', align: 'center'},
{ dataIndex: 'sort', title: '排序', align: 'center'},
{ dataIndex: 'belong_to_table', title: '所属表名称', align: 'center'},
{ dataIndex: 'original_sql', title: 'sql数据源', align: 'center'},
{ dataIndex: 'original_type', title: '数据源类型', align: 'center'},
{ dataIndex: 'original_sql', title: '数据源', align: 'center', width: 400},
{ dataIndex: 'action', title: '操作', align: 'center'},
];
export const originalTypes = [
{ label: 'sql', value: 1 },
{ label: 'json', value: 2 },
]

View File

@ -4,7 +4,7 @@
<a-space>
<a-input
v-model:value="modularName"
placeholder="请输入数据名称"
placeholder="请输入数据来源名称"
allow-clear
style="width: 200px"
@change="search"

View File

@ -54,20 +54,30 @@ export function getProjectSelect() {
});
}
// 展示类型下拉
export function getShowTypeSelect() {
// 数据表源下拉
export function getDbTableSelect({ projectId }) {
return get({
url: `/api/v1/modular/get-show-type-drop`,
url: `/api/v1/modular/get-database-table-drop`,
params: {
project_id: projectId,
},
});
}
// 字段类型下拉
// 字段搜索类型下拉
export function getFieldTypeSelect() {
return get({
url: `/api/v1/field/get-field-type-drop`,
});
}
// 字段类型下拉
export function getFieldNumSelect() {
return get({
url: `/api/v1/field/get-field-numerical-type-drop`,
})
}
// 获取字段列表
export function getFieldList({ modularId, fieldName, page, perPage }) {
return get({

View File

@ -14,7 +14,7 @@
/>
</a-form-item>
<a-form-item label="项目标识" name="mark">
<a-input placeholder="请输入项目标识例如oa" />
<a-input placeholder="请输入项目标识例如oa" v-model:value="formData.mark" />
</a-form-item>
<a-form-item label="展示状态" name="is_show">
<a-switch
@ -59,6 +59,12 @@
>
</a-space>
</a-form-item>
<a-form-item label="数据库驱动" name="drive_type">
<a-radio-group v-model:value="formData.drive_type">
<a-radio :value="1">MySQL</a-radio>
<a-radio :value="2">Click House</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
</a-modal>
</template>
@ -98,12 +104,6 @@ const formRules = {
database_name: [
{ required: true, message: "请输入数据库名称", trigger: "submit" },
],
database_username: [
{ required: true, message: "请输入数据库用户", trigger: "submit" },
],
database_password: [
{ required: true, message: "请输入数据库密码", trigger: "submit" },
],
};
const formRef = ref();
@ -116,6 +116,7 @@ const formData = ref({
database_name: undefined,
database_username: undefined,
database_password: undefined,
drive_type: 1,
});
watch(
@ -138,6 +139,7 @@ const toCheckDbConnect = () => {
database_address: formData.value.database_address,
database_username: formData.value.database_username,
database_password: formData.value.database_password,
drive_type: formData.value.drive_type,
}).then((res) => {
message.success(res.message);
});
@ -149,8 +151,6 @@ const validateConnect = () => {
database_name: "请输入数据库名称",
database_port: "请输入数据库端口",
database_address: "请输入数据库地址",
database_username: "请输入数据库用户",
database_password: "请输入数据库密码",
};
for (const key in fields) {
if (!formData.value[key]) {
@ -171,6 +171,7 @@ const resetFormData = () => {
database_name: undefined,
database_username: undefined,
database_password: undefined,
drive_type: 1,
};
};

View File

@ -0,0 +1,337 @@
<template>
<div class="page-view-wrapp">
<div v-if="!isInQiankun" class="project">
<span>项目: </span>
<a-select
style="min-width: 160px"
placeholder="请选择项目"
v-model:value="projectVal"
:options="projectOptions"
@change="handleProjectChange"
></a-select>
</div>
<div>
<grid-layout
v-if="isDraggable"
:layout.sync="layoutList"
:col-num="2"
:is-draggable="true"
:is-resizable="false"
:is-mirrored="false"
:vertical-compact="true"
:use-css-transforms="true"
>
<grid-item
v-for="(item, index) in layoutList"
:x="item.x"
:y="item.y"
:w="item.w"
:h="item.h"
:i="item.i"
:key="item.i"
drag-allow-from=".vue-draggable-handle"
drag-ignore-from=".no-drag"
>
<div class="view-box view-draggable">
<div class="vue-draggable-handle"><BarsOutlined /></div>
<div class="content no-drag">
<a-spin :spinning="ids[index].loading">
<div class="card-content">
<y-table
v-if="item.data.type === VIEW_TYPE.TABLE"
:filter-config="item.data.filter"
:data-list="item.data.data"
:column-config="item.data.header"
:total="item.data.count"
:title="item.data.preview_name"
@toFilt="
(params?:object) => {
handleSingle(ids[index], params);
}
"
></y-table>
<y-chart
v-if="item.data.type === VIEW_TYPE.CHART"
:chartCfg="item.data.config"
:title="item.data.preview_name"
:filter-config="item.data.filter"
@toFilt="
(params?:object) => {
handleSingle(ids[index], params);
}
"
></y-chart>
</div>
</a-spin>
</div>
</div>
</grid-item>
</grid-layout>
<a-row v-else :gutter="[16, 16]">
<a-col v-for="(item, index) in layoutList" :span="24">
<a-spin :spinning="item.loading">
<div>
<div class="view-box">
<div class="content">
<div class="card-content">
<y-table
v-if="item.data.type === VIEW_TYPE.TABLE"
:filter-config="item.data.filter"
:data-list="item.data.data"
:column-config="item.data.header"
:total="item.data.count"
:title="item.data.preview_name"
@toFilt="
(params?:object) => {
handleSingle(ids[index], params,);
}
"
></y-table>
<y-chart
v-if="item.data.type === VIEW_TYPE.CHART"
:chartCfg="item.data.config"
:title="item.data.preview_name"
:filter-config="item.data.filter"
@toFilt="
(params?:object) => {
handleSingle(ids[index], params);
}
"
></y-chart>
</div>
</div>
</div>
</div>
</a-spin>
</a-col>
</a-row>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, shallowRef, computed, onMounted, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { BarsOutlined } from "@ant-design/icons-vue";
// utils
import PLimit from "p-limit";
// api
import { searchInfo } from "@/api/preview/index";
import { getProjectDrop } from "@/api/common";
import { getPageInfo } from "./service";
import type { SelectProps } from "ant-design-vue";
import { qiankunWindow } from 'vite-plugin-qiankun/dist/helper'
interface ItemDetail {
id: number | string;
data: any;
loading: boolean;
}
interface Item {
id: number | string;
data: any;
loading: boolean;
}
interface Option extends SelectProps {
mark: string;
}
const VIEW_TYPE = {
TABLE: "table",
CHART: "chart",
};
const SEARCH_TYPE = {
SEARCH: "search",
INIT: "init",
};
// hooks
const route = useRoute();
const router = useRouter();
const projectTag = shallowRef();
const projectVal = shallowRef();
const pageId = shallowRef(route.query.pageId);
const projectOptions = shallowRef<Option[]>();
const isDraggable = false;
const isInQiankun = computed(() => {
return qiankunWindow.__POWERED_BY_QIANKUN__ || window.proxy?.__POWERED_BY_QIANKUN__
})
const layoutList = computed(() => {
return ids.value.map((item, index) => {
//
const row = Math.floor(index / 2);
// ji
const col = index % 2;
return {
i: item?.id,
x: col,
y: row,
w: 1,
h: 3,
minH: 3,
...item,
};
});
});
const ids = ref<Item[]>([]);
const pLimit = PLimit(2);
watch(() => route.query.viewId, () => {
getPageInfoData()
})
onMounted(() => {
getProjectList();
});
const handleSingle = (info: ItemDetail, otherParams?: object) => {
getSinglePreview({ info, otherParams, type: SEARCH_TYPE.SEARCH });
};
const handleProjectChange = (value: string | number, option: Option) => {
projectTag.value = option.mark;
router.replace({
path: route.path,
query: {
...route.query,
projectTag: projectTag.value,
},
});
getPageInfoData();
};
//
//
const getProjectList = () => {
getProjectDrop()
.then((res) => {
if (res.code === 200) {
projectOptions.value = res.data;
projectTag.value = route.query.projectTag || res.data[0].mark;
projectVal.value =
projectOptions.value?.find((item) => {
return item.mark === route.query.projectTag;
})?.value || res.data[0].value;
getPageInfoData();
}
})
.catch(() => {
projectOptions.value = [];
})
.finally(() => {});
};
//
const getSinglePreview = (data: {
info: ItemDetail;
otherParams?: object;
type?: string;
}) => {
const { info, otherParams, type } = data;
info.loading = true;
const params = { previewId: info.id, page: 1, perPage: 20, ...otherParams };
searchInfo(params)
.then((res) => {
if (res.code === 200) {
info.data = res.data;
}
})
.finally(() => {
info.loading = false;
});
};
// id
const getPageInfoData = () => {
getPageInfo({ mark:projectTag.value, page_id: pageId.value ?? "-1" })
.then((res) => {
if (res.code === 200) {
if (route.query.viewId) {
ids.value = res.data?.filter((item: any) => {
return item.preview_id === Number(route.query.viewId);
}).map((item: any) => {
return {
id: item.preview_id,
data: item,
loading: false,
}
})
} else {
ids.value = res.data?.map((item: any) => {
return {
id: item.preview_id,
data: item,
loading: false,
};
});
}
getAllCardsData();
}
})
.finally(() => {});
};
const fetchFn = (delay: number, info: ItemDetail) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(info);
getSinglePreview({ info, type: SEARCH_TYPE.INIT });
}, delay);
});
};
const getAllCardsData = async () => {
let listDB = [];
for (let i in ids.value) {
listDB.push(pLimit(() => fetchFn(i === "0" ? 200 : 1000, ids.value[i])));
}
await Promise.all(listDB);
//listDB
};
</script>
<style lang="less" scoped>
.view-box {
height: 100%;
width: 100%;
min-height: 350px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 4px;
transition: all 0.3s;
&:hover {
box-shadow: 0 0 20px 0 #0a103205, 0 14px 40px 0 #0a103208,
0 20px 60px 0 #0a10320d;
}
.content {
padding: 10px;
}
}
.project {
margin-bottom: 10px;
}
.view-draggable {
height: auto;
min-height: 450px;
}
.vue-draggable-handle {
padding: 0 8px 8px 0;
border-radius: 10px;
cursor: pointer;
}
.vue-grid-item {
height: auto;
}
</style>

View File

@ -0,0 +1,10 @@
import { get, post } from "@/utils/request";
interface PageInfoParams {
mark: string;
page_id: number | string;
}
export const getPageInfo = (data: PageInfoParams) =>
get({
url: "/api/v1/preview/get-preview-info",
params: data,
});

View File

@ -2,16 +2,16 @@
<div class="normal-container">
<div class="view-create-box">
<div class="left-box">
<a-form :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
<a-form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="项目"
><a-select
placeholder="请选择项目"
:options="projectSel"
v-model:value="projectId"
@change="onProjectChange"
></a-select
><a-select
placeholder="请选择项目"
:options="projectSel"
v-model:value="projectId"
@change="onProjectChange"
></a-select
></a-form-item>
<a-form-item label="数据">
<a-form-item label="数据来源">
<a-select
placeholder="请选择"
:options="modularSel"
@ -19,33 +19,49 @@
@change="onModularChange"
></a-select>
</a-form-item>
<a-form-item label="字段">
<a-checkbox-group v-if="fieldList.length" v-model:value="fieldIds">
<a-form-item label="展示类型">
<a-select
placeholder="请选择展示类型"
:options="showTypeSel"
v-model:value="showTypeId"
@change="onShowTypeChange"
></a-select>
</a-form-item>
<a-form-item label="字段" v-if="fieldList.length">
<a-checkbox-group v-model:value="fieldIds">
<a-checkbox
v-for="(item, index) in fieldList"
:key="index"
:value="item.value"
>{{ item.label }}</a-checkbox
>{{ item.label }}</a-checkbox
>
</a-checkbox-group>
<a-empty v-else description="暂无字段数据" />
</a-form-item>
<a-form-item label="x轴" v-if="xDataList.length">
<a-radio-group :options="xDataList" v-model:value="xDataId"></a-radio-group>
</a-form-item>
<a-form-item label="y轴" v-if="yDataList.length">
<a-checkbox-group :options="yDataList" v-model:value="yDataId"></a-checkbox-group>
</a-form-item>
</a-form>
<div class="footer">
<a-button
class="preview-btn"
:loading="previewLoading"
@click="toPreview"
>预览</a-button
@click="() => {toPreview({})}"
>预览</a-button
>
<a-button type="primary" @click="addViewName">点击保存</a-button>
</div>
</div>
<div class="right-box">
<div class="y-table-container" v-if="previewData.type === 'table'">
<div class="y-table-name">
<div class="title">{{ previewData.preview_name }}</div>
</div>
<div class="y-table-filter">
<div
v-for="(item, index) in previewData.filterConfig"
v-for="item in previewData.filterConfig"
:key="item.name"
class="filter-item"
>
@ -67,6 +83,7 @@
v-model:value="previewData.filterData[item.name]"
@change="toFilt"
/>
<a-range-picker v-if="item.type === 'time'" class="date-item" v-model:value="previewData.filterData[item.name]" @change="toFilt" />
</div>
</div>
<div class="y-table-content">
@ -74,7 +91,7 @@
:columns="previewData.columnConfig"
:data-source="previewData.dataList"
:pagination="false"
:scroll="{ x: 1200 }"
:scroll="{ x: 1000, y: `calc(100vh - 260px)` }"
size="small"
bordered
></a-table>
@ -85,10 +102,11 @@
:hide-on-single-page="false"
size="small"
class="pagination-box"
@change="toPreview"
@change="() => { toPreview({}) }"
/>
</div>
</div>
<y-chart v-else-if="previewData.type === 'chart'" :chart-cfg="previewData.chartCfg" :filter-config="previewData.filter" @toFilt="() => {toPreview({})}"></y-chart>
<div class="preview-area" v-else>
<div><BarChartOutlined /></div>
<div>预览区</div>
@ -113,16 +131,30 @@
<script setup>
import { onMounted, reactive, ref } from "vue";
import { getProModularField, preview, saveView } from "./service";
import {
getProModularField,
preview,
saveView,
getShowTypeSelect,
getFieldOpts,
} from "./service";
import { message } from "ant-design-vue";
import { BarChartOutlined } from "@ant-design/icons-vue";
import yChart from "@/components/common/y-chart.vue";
import _ from 'lodash'
const projectSel = ref([]);
const modularSel = ref([]);
const fieldList = ref([]);
const projectSel = ref([]); //
const modularSel = ref([]) //
const showTypeSel = ref([]); //
const fieldList = ref([]); //
const xDataList = ref([]); // x
const yDataList = ref([]); // y
const projectId = ref();
const modularId = ref();
const showTypeId = ref();
const fieldIds = ref([]);
const xDataId = ref();
const yDataId = ref();
const previewLoading = ref(false);
const nameVisible = ref(false);
@ -134,6 +166,8 @@ const previewData = reactive({
columnConfig: [], //
dataList: [], //
filterData: {},
config: {},
filter: [],
page: 1,
perPage: 20,
total: 0,
@ -141,6 +175,7 @@ const previewData = reactive({
onMounted(() => {
toGetProModularField();
toGetShowTypes();
});
const toGetProModularField = () => {
@ -149,51 +184,117 @@ const toGetProModularField = () => {
});
};
//
const toGetShowTypes = () => {
getShowTypeSelect().then((res) => {
showTypeSel.value = res.data;
});
};
//
const toGetFieldOpts = () => {
getFieldOpts({
modularId: modularId.value,
showTypeId: showTypeId.value,
}).then((res) => {
fieldList.value = res.data.list ? tranformList(res.data.list) : [];
xDataList.value = res.data.x_data ? tranformList(res.data.x_data) : [];
yDataList.value = res.data.y_data ? tranformList(res.data.y_data) : [];
if (!fieldList.value.length) {
fieldIds.value = [];
} else {
xDataId.value = undefined;
yDataId.value = [];
}
});
};
const onProjectChange = (val) => {
//
const target = projectSel.value.find((item) => item.value === val);
modularSel.value = target.child;
modularId.value = undefined;
resetSelectData();
resetPreviewData();
};
const onModularChange = () => {
resetSelectData();
resetPreviewData();
};
const onShowTypeChange = () => {
toGetFieldOpts();
};
const tranformList = (list) => {
return list.map((item) => {
return {
label: item.field_name,
value: item.field_id,
};
});
}
//
const resetSelectData = () => {
showTypeId.value = undefined
fieldList.value = [];
fieldIds.value = [];
resetPreviewData();
};
const onModularChange = (val) => {
const target = modularSel.value.find((item) => item.value === val);
fieldList.value = target.child;
resetPreviewData();
};
xDataList.value = [];
yDataList.value = [];
xDataId.value = undefined;
yDataId.value = [];
}
//
const resetPreviewData = () => {
previewData.type = "";
previewData.filterConfig = [];
previewData.columnConfig = [];
previewData.dataList = [];
previewData.filterData = {};
previewData.config = {};
previewData.page = 1;
previewData.perPage = 20;
previewData.total = 0;
};
const toPreview = () => {
//
const toPreview = ({filter}) => {
previewLoading.value = true;
const filter = previewData.filterConfig
.filter((item) => {
return previewData.filterData[item.name] !== undefined;
})
.map((item) => {
return {
name: item.name,
type: item.type,
value: previewData.filterData[item.name],
};
});
let filterData
if (!filter) {
const cloneFilter = _.cloneDeep(previewData.filterConfig)
filterData = cloneFilter
.filter((item) => {
return previewData.filterData[item.name] !== undefined && previewData.filterData[item.name] !== null;
})
.map((item) => {
return item.type === 'time' && previewData.filterData[item.name] ? {
name: item.name,
type: item.type,
start_time: previewData.filterData[item.name][0].format('YYYY-MM-DD'),
end_time: previewData.filterData[item.name][1].format('YYYY-MM-DD'),
} : {
name: item.name,
type: item.type,
value: previewData.filterData[item.name],
}
});
} else {
filterData = filter;
}
preview({
modularId: modularId.value,
fieldIds: fieldIds.value.toString(),
page: previewData.page,
perPage: previewData.perPage,
filter,
filter: filterData,
showTypeId: showTypeId.value,
xDataId: xDataId.value?.toString(),
yDataId: yDataId.value?.toString(),
})
.then((res) => {
previewData.type = res.data.type;
@ -207,6 +308,9 @@ const toPreview = () => {
previewData.columnConfig = res.data.header;
previewData.dataList = res.data.data;
previewData.total = res.data.count;
} else {
previewData.chartCfg = res.data.config;
previewData.filter = res.data.filter;
}
})
.finally(() => {
@ -228,15 +332,18 @@ const toSaveView = () => {
modularId: modularId.value,
fieldIds: fieldIds.value.toString(),
previewName: previewName.value,
showTypeId: showTypeId.value,
xDataId: xDataId.value?.toString(),
yDataId: yDataId.value?.toString(),
}).then(() => {
message.success("保存成功");
message.success("保存成功,可前往视图列表查看");
nameVisible.value = false;
});
};
const toFilt = () => {
previewData.page = 1;
toPreview();
toPreview({});
};
</script>
@ -282,7 +389,12 @@ const toFilt = () => {
font-size: 100px;
}
}
.y-table-name {
.title {
font-size: 18px;
font-weight: bold;
}
}
.y-table-filter {
display: flex;
flex-wrap: wrap;
@ -290,10 +402,14 @@ const toFilt = () => {
.filter-item {
margin-right: 10px;
margin-bottom: 6px;
font-size: 14px;
}
.input-item {
width: 180px;
}
.date-item {
width: 240px;
}
.y-table-content {
margin-top: 10px;
}

View File

@ -7,8 +7,26 @@ export function getProModularField() {
});
}
// 展示类型下拉
export function getShowTypeSelect() {
return get({
url: `/api/v1/modular/get-show-type-drop`,
});
}
// 字段列表
export function getFieldOpts({ modularId, showTypeId }) {
return get({
url: "/api/v1/preview/get-preview-field",
params: {
modular_id: modularId,
show_type_id: showTypeId,
},
});
}
// 预览
export function preview({ modularId, fieldIds, page, perPage, filter }) {
export function preview({ modularId, fieldIds, page, perPage, filter, showTypeId, xDataId, yDataId }) {
return post({
url: "api/v1/preview/view",
data: {
@ -17,18 +35,24 @@ export function preview({ modularId, fieldIds, page, perPage, filter }) {
page,
per_page: perPage,
filter,
show_type_id: showTypeId,
x_data_id: xDataId,
y_data_id: yDataId,
},
});
}
// 点击保存
export function saveView({ modularId, fieldIds, previewName }) {
export function saveView({ modularId, fieldIds, previewName, showTypeId, xDataId, yDataId }) {
return post({
url: "api/v1/preview/save",
data: {
modular_id: modularId,
field_ids: fieldIds,
preview_name: previewName,
show_type_id: showTypeId,
x_data_id: xDataId,
y_data_id: yDataId,
},
});
}

View File

@ -2,7 +2,7 @@
<div class="normal-container">
<div class="view-list-box">
<div class="left-box">
<a-form :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
<a-form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="项目">
<a-select
:options="projectSel"
@ -11,11 +11,11 @@
@change="onProjectChange"
></a-select>
</a-form-item>
<a-form-item label="数据">
<a-form-item label="数据来源">
<a-select
:options="modularSel"
v-model:value="modularId"
placeholder="请选择数据表"
placeholder="请先选好项目再选择"
@change="onModularChange"
></a-select>
</a-form-item>
@ -73,16 +73,18 @@
<div class="right-box">
<y-table
v-if="selectViewInfo.type === 'table'"
:filterConfig="selectViewInfo.filter"
:dataList="selectViewInfo.data"
:columnConfig="selectViewInfo.header"
:filter-config="selectViewInfo.filter"
:data-list="selectViewInfo.data"
:column-config="selectViewInfo.header"
:total="selectViewInfo.count"
:title="selectViewInfo.preview_name"
@toFilt="
(params) => {
toGetViewInfo(params);
}
"
/>
<y-chart v-else-if="selectViewInfo.type === 'chart'" :chartCfg="selectViewInfo.config" :title="selectViewInfo.preview_name" :filter-config="selectViewInfo.filter" @toFilt="toGetViewInfo" />
<div class="preview-area" v-else>
<div><BarChartOutlined /></div>
<div>展示区</div>
@ -109,6 +111,11 @@ const selectedRowId = ref();
const selectViewInfo = ref({
type: "",
filter: [],
config: {
line: {
data: []
}
}
});
const pageState = reactive({
@ -182,6 +189,7 @@ const toDelete = (previewId) => {
padding: 10px;
flex-shrink: 0;
border-right: 1px solid #ddd;
overflow: auto;
}
:deep(.ant-table-row:hover) {

View File

@ -4,10 +4,41 @@ import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import Components from 'unplugin-vue-components/vite';
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
import { server } from 'typescript';
import qiankun from 'vite-plugin-qiankun'
import yargsParser from 'yargs-parser'
const argv = yargsParser(process.argv.slice(2))
console.log(process.env.DEV_ENV)
let base: string
switch (argv.mode) {
case 'staging':
case 'test':
base = 'https://custom-chart.shiyue.com/'
break
case 'production':
base = 'https://custom-chart.shiyuegame.com/'
break
default:
base = 'http://localhost:8080/'
}
if (process.env.DEV_ENV === 'development') {
base = 'http://localhost:8080/'
}
// https://vitejs.dev/config/
export default defineConfig({
build: {
sourcemap: false,
rollupOptions: {
output: {
assetFileNames: '[name]-ycode-[hash:8].[ext]',
chunkFileNames: '[name]-ycode-[hash:8].js',
entryFileNames: '[name]-ycode-[hash:8].js',
},
},
},
plugins: [
vue(),
vueJsx(),
@ -16,14 +47,20 @@ export default defineConfig({
importStyle: 'less',
})],
}),
qiankun('y-code-app', { useDevMode: process.env.DEV_ENV === 'development' }),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
base,
define: {
_BASE_HOST_: `'${base}'`,
},
server: {
hmr: true,
host: '0.0.0.0',
port: 8080,
},
});