chore: 容器框架升级,修复项目命令行异常问题

This commit is contained in:
wangxuefeng
2025-03-11 10:05:28 +08:00
parent de679d4289
commit 3e1a1b4a66
1187 changed files with 95352 additions and 12509 deletions

View File

@@ -0,0 +1,10 @@
# .env.development
VITE_NODE_ENV = development
VITE_PORT = 10013
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

@@ -0,0 +1,8 @@
VITE_NODE_ENV = prod
VITE_OA_BASEURL = https://oa.shiyuegame.com
VITE_YCODE_BASEURL = https://custom-chart-api.shiyuegame.com
# VITE_YCODE_BASEURL = https://custom-chart-api.shiyuegame.com:19998
VITE_YCODE_BASEURL_FRONT = https://custom-chart.shiyuegame.com

View File

@@ -0,0 +1,9 @@
VITE_NODE_ENV = staging
VITE_PORT = 10012
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

@@ -0,0 +1,25 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution');
module.exports = {
root: true,
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
],
parserOptions: {
ecmaVersion: 'latest',
},
rules: {
semi: 0,
'vue/multi-word-component-names': 0,
indent: [
2, 2, {
SwitchCase: 1,
},
],
'vue/html-indent': 2,
'@typescript-eslint/comma-dangle': [2, 'always-multiline'],
},
};

6
apps/y-code-v1/.npmrc Normal file
View File

@@ -0,0 +1,6 @@
# 默认使用淘宝镜像源
registry=https://registry.npmmirror.com/
# 公司私有源配置
@sy:registry=http://sy-registry.shiyue.com

View File

@@ -0,0 +1,17 @@
module.exports = {
branches: ["main", "master"],
plugins: [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
// "@semantic-release/npm",
[
"@semantic-release/git",
{
assets: ["CHANGELOG.md", "package.json"],
message: "chore(release): ${nextRelease.version} [skip ci]",
},
],
// "@semantic-release/github",
],
};

0
apps/y-code-v1/README.md Normal file
View File

45
apps/y-code-v1/components.d.ts vendored Normal file
View File

@@ -0,0 +1,45 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
declare module 'vue' {
export interface GlobalComponents {
ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb']
ABreadcrumbItem: typeof import('ant-design-vue/es')['BreadcrumbItem']
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']
AFloatButton: typeof import('ant-design-vue/es')['FloatButton']
AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem']
AImage: typeof import('ant-design-vue/es')['Image']
AInput: typeof import('ant-design-vue/es')['Input']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
AMenu: typeof import('ant-design-vue/es')['Menu']
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
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']
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']
YChart: typeof import('./src/components/common/y-chart.vue')['default']
YTable: typeof import('./src/components/common/y-table.vue')['default']
}
}

1
apps/y-code-v1/env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,50 @@
import path from 'node:path';
import { defineConfig, loadEnv } from '@farmfe/core';
import less from '@farmfe/js-plugin-less';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
import Components from 'unplugin-vue-components/vite';
import mkcert from 'vite-plugin-mkcert';
import qiankun from 'vite-plugin-qiankun';
// @ts-ignore
export default defineConfig(({ mode }) => {
console.log('mode', mode);
const env = loadEnv(mode, process.cwd(), ['VITE_']);
return {
plugins: [less()],
vitePlugins: [
vue(),
vueJsx(),
mkcert(),
qiankun('y-code-app', {
useDevMode: env.VITE_NODE_ENV === 'development',
}) as any,
Components({
resolvers: [
AntDesignVueResolver({
importStyle: 'less',
}),
],
}),
],
server: {
cors: true,
port: Number(env.VITE_PORT),
},
compilation: {
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
// output: {
// path: path.resolve(process.cwd(), "../../dist/y-code-v1"),
// clean: true,
// },
},
};
});

13
apps/y-code-v1/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>悦码后台</title>
</head>
<body>
<div id="y-code-app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,55 @@
{
"name": "@sy/low-code-y-code-v1",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "cross-env farm start --mode development",
"build": "cross-env farm build --mode production",
"build:staging": "cross-env farm build --mode staging",
"preview": "cross-env farm preview",
"clean": "rimraf node_modules"
},
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"@antv/g2plot": "^2.4.31",
"@farmfe/js-plugin-less": "^1.12.1",
"@sy/y-code-chart": "^1.2.1",
"@vueuse/core": "^10.11.0",
"ant-design-vue": "^4.2.6",
"axios": "catalog:",
"core-js": "^3.40.0",
"cross-env": "^7.0.3",
"dayjs": "catalog:",
"lodash-es": "^4.17.21",
"p-limit": "^6.1.0",
"pinia": "catalog:",
"vue": "catalog:",
"vue-grid-layout": "^3.0.0-beta1",
"vue-router": "catalog:",
"wujie-vue3": "^1.0.25"
},
"devDependencies": {
"@farmfe/cli": "^1.0.4",
"@farmfe/core": "^1.6.6",
"@rushstack/eslint-patch": "^1.10.5",
"@tsconfig/node20": "^20.1.4",
"@types/node": "^20.17.17",
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"@vue/eslint-config-typescript": "^13.0.0",
"@vue/tsconfig": "^0.7.0",
"eslint": "^9.20.1",
"eslint-plugin-vue": "^9.32.0",
"less": "^4.2.2",
"semantic-release": "^24.2.2",
"typescript": "catalog:",
"unplugin-vue-components": "^0.26.0",
"vite": "catalog:",
"vite-plugin-mkcert": "catalog:",
"vite-plugin-qiankun": "^1.0.15",
"vue-tsc": "catalog:",
"yargs-parser": "^21.1.1"
},
"packageManager": "pnpm@10.6.2"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,30 @@
<script setup>
import zhCN from 'ant-design-vue/es/locale/zh_CN';
import { legacyLogicalPropertiesTransformer, StyleProvider, ConfigProvider, theme } from 'ant-design-vue';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
const { compactAlgorithm } = theme;
dayjs.locale('zh-cn');
ConfigProvider.config({
prefixCls: 'ycode-ant',
})
</script>
<template>
<a-config-provider
:theme="{
algorithm: [ compactAlgorithm],
}"
:locale="zhCN"
:transformers="[legacyLogicalPropertiesTransformer]"
prefix-cls="ycode-ant"
>
<StyleProvider hash-priority="high">
<router-view />
</StyleProvider>
</a-config-provider>
</template>

View File

@@ -0,0 +1,34 @@
import { get } from "@/utils/request";
export interface UserInfoType {
alias: string;
all_dept_name: string;
avatar: string;
dept: string;
dept_id: number;
email: string;
job: string;
job_name: string;
job_type: string;
mobile: string;
user_id: number;
username: string;
}
interface DropListItem {
label: string;
value: string | number;
mark: string;
}
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

@@ -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,
},
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,5 @@
@primary-color: var(--primary-color); // 主题色
@primary-light-color: var(--primary-light-color); // 主题色 - 浅色
@table-head-bg-color: var(--table-head-bg-color); // 表头背景色
@table-head-font-color: var(--table-head-font-color); // 表头字体颜色
@primary-bg-color: #f8f8f8;

View File

@@ -0,0 +1,128 @@
<template>
<div class="chart-show-box">
<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'" :config="currentChart" />
<Line v-if="chartType === 'line'" :config="currentChart" />
</div>
</div>
</template>
<script setup>
import { computed, ref } from "vue";
import Line from "@/plugins/antv-g2plot/line.vue";
import Column from "@/plugins/antv-g2plot/column.vue";
import { cloneDeep } from "lodash-es";
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

@@ -0,0 +1,286 @@
<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"
:key="index"
class="filter-item"
>
<span>{{ item.label }}</span>
<!-- 选择框 -->
<a-select
v-if="item.type === 'select'"
class="input-item"
placeholder="请选择"
:options="item.options"
allow-clear
show-search
:filter-option="filterOption"
v-model:value="filterData[item.name]"
@change="toFilt"
></a-select>
<!-- 输入框 -->
<a-input
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"
/>
<!-- 带时分的日期范围 -->
<a-range-picker
v-else-if="item.type === 'date_time'"
class="date-time-item"
v-model:value="filterData[item.name]"
show-time
format="YYYY-MM-DD HH:mm"
@change="toFilt"
/>
<!-- 数值区间 -->
<div
v-else-if="item.type === 'number_range' && filterData[item.name]"
class="number_range_box"
>
<a-input-number
placeholder="最小值"
style="width: 140px"
v-model:value="filterData[item.name].min"
@change="toFilt"
/>
<span class="divider"> - </span>
<a-input-number
placeholder="最大值"
style="width: 140px"
v-model:value="filterData[item.name].max"
@change="toFilt"
/>
</div>
</div>
<div v-if="isExport" class="filter-item">
<a
:href="`${YCODE_BASEURL}/api/v1/preview/export?preview_id=${previewId}&filter=${JSON.stringify(getFilter())}`"
target="_blank"
><a-button type="primary"><CloudDownloadOutlined />导出</a-button></a
>
</div>
</div>
<div class="y-table-content">
<a-table
:columns="columnConfig"
:data-source="dataList"
:pagination="false"
:scroll="{ x: 1000, y: `calc(100vh - 280px)` }"
size="small"
bordered
>
<template #bodyCell="{ column, record }">
<a-image
v-if="column.show_type === 'img'"
:src="record[column.dataIndex]"
:width="160"
/>
<a
v-else-if="column.show_type === 'link'"
target="_blank"
:href="record[column.dataIndex]"
>{{ record[column.dataIndex] }}</a
>
<div
v-else-if="column.show_type === 'richText'"
v-html="record[column.dataIndex]"
></div>
<template v-else>{{ record[column.dataIndex] }}</template>
</template>
</a-table>
<a-pagination
v-model:current="pageState.page"
:total="total"
:page-size="pageState.perPage"
:hide-on-single-page="false"
:show-size-changer="false"
size="small"
class="pagination-box"
:show-total="total => `${total}`"
@change="pageChange"
/>
</div>
</div>
</template>
<script setup>
import { reactive, ref, watch } from "vue";
import { useDebounceFn } from "@vueuse/core";
import { cloneDeep } from "lodash-es";
import { CloudDownloadOutlined } from "@ant-design/icons-vue";
const YCODE_BASEURL = import.meta.env.VITE_YCODE_BASEURL
const props = defineProps({
previewId: {
type: Number,
default: null,
},
filterConfig: {
type: Array,
default: () => [],
},
columnConfig: {
type: Array,
default: () => [],
},
dataList: {
type: Array,
default: () => [],
},
total: {
type: Number,
default: 0,
},
title: {
type: String,
default: "",
},
isExport: {
type: Number,
default: 0,
},
});
const emit = defineEmits(["toFilt"]);
const filterData = ref({});
const pageState = reactive({
page: 1,
perPage: 20,
});
watch(() => props.filterConfig, (newVal) => {
newVal.forEach((item) => {
// 给数值区间类型赋初始值,防止报错
if (item.type === 'number_range' && !filterData.value[item.name]) {
filterData.value[item.name] = {
min: undefined,
max: undefined,
};
}
});
}, { immediate: true });
const filterOption = (input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
const getFilter = () => {
const cloneFilter = cloneDeep(props.filterConfig);
const filter = cloneFilter
.filter((item) => {
return filterData.value[item.name] !== undefined && filterData.value[item.name] !== null;
})
.map((item) => {
if (item.type === 'time' && filterData.value[item.name]) {
// 日期类型的参数
return {
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'),
};
} else if (item.type === 'date_time' && filterData.value[item.name]) {
// 带时分的日期类型参数
return {
name: item.name,
type: item.type,
start_time: filterData.value[item.name][0].format('YYYY-MM-DD HH:mm') + ':00',
end_time: filterData.value[item.name][1].format('YYYY-MM-DD HH:mm') + ':59',
};
} else if (item.type === 'number_range') {
// 数值区间
return {
name: item.name,
type: item.type,
min: filterData.value[item.name].min ? String(filterData.value[item.name].min) : '',
max: filterData.value[item.name].max ? String(filterData.value[item.name].max) : '',
};
} else {
return {
name: item.name,
type: item.type,
value: filterData.value[item.name],
};
}
});
return filter
};
const getData = () => {
emit("toFilt", {
filter: getFilter(),
page: pageState.page,
perPage: pageState.perPage,
});
};
const toFilt = useDebounceFn(() => {
pageState.page = 1
getData();
}, 500);
const pageChange = () => {
getData();
};
</script>
<style lang="less" scoped>
.y-table-name {
margin-bottom: 10px;
display: none;
.title {
font-size: 18px;
font-weight: bold;
}
}
.y-table-filter {
display: flex;
flex-wrap: wrap;
}
.filter-item {
margin-right: 10px;
margin-bottom: 6px;
font-size: 14px;
}
.input-item {
width: 180px;
}
.date-item {
width: 240px;
}
.date-time-item {
width: 300px;
}
.number_range_box {
display: inline-flex;
align-items: center;
.divider {
margin: 0 4px;
}
}
.y-table-content {
margin-top: 10px;
}
.pagination-box {
text-align: center;
margin-top: 10px;
}
</style>

View File

@@ -0,0 +1,46 @@
// @primary-bg-color: #f8f8f8;
@import "../src/assets/styles/variable.less";
html,
body {
background-color: @primary-bg-color;
padding: 0;
margin: 0;
height: 100%;
}
#app {
height: 100%;
}
/* 选择滚动条 */
::-webkit-scrollbar {
width: 8px; /* 滚动条宽度 */
}
/* 滚动条轨道 */
::-webkit-scrollbar-track {
background-color: #fff;
}
/* 滚动条滑块 */
::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.15);
border-radius: 15px;
}
/* 滚动条滑块悬停 */
::-webkit-scrollbar-thumb:hover {
background-color: rgba(0, 0, 0, 0.25);
}
.normal-container {
padding: 16px 24px;
border-radius: 6px;
background-color: #fff;
}
.mt-8 {
margin-top: 8px;
}
.mt-16 {
margin-top: 16px;
}

View File

@@ -0,0 +1,93 @@
<script setup lang="ts">
import { useRoute } from 'vue-router';
import { logout } from '@/api/common';
import avatar from '@/assets/avatar.png';
import { useUserInfoStore } from '@/stores/useUserInfoStore';
import { YCODE_BASEURL } from '@/utils/request';
import { FullscreenOutlined, HomeOutlined } from '@ant-design/icons-vue';
const emits = defineEmits(['requestFullscreen']);
const route = useRoute();
const userInfoStore = useUserInfoStore();
const handleLogout = () => {
logout().then(() => {
window.location.href = `${YCODE_BASEURL}/login?redirect=${encodeURIComponent(
window.location.href,
)}`;
});
};
</script>
<template>
<div class="root">
<a-breadcrumb>
<a-breadcrumb-item v-for="item in route.matched" :key="item.path">
<HomeOutlined v-if="item.path === '/' && item.name === 'layout'" />
<span v-else>{{ item.meta.title || item.path }}</span>
</a-breadcrumb-item>
</a-breadcrumb>
<div class="user-area">
<div class="fullscreen-icon-area" @click="emits('requestFullscreen')">
<FullscreenOutlined class="fullscreen-icon" />
</div>
<a-dropdown placement="bottom">
<div style="display: flex; align-items: center; cursor: pointer">
<img :src="userInfoStore.userInfo?.avatar || avatar" class="avatar" />
<div>{{ userInfoStore.userInfo?.alias || '-' }}</div>
</div>
<template #overlay>
<a-menu>
<a-menu-item @click="handleLogout">
<span>退出登录</span>
</a-menu-item>
<a-menu-item>
<a class="back-oa" :href="`${OA_BASEURL}/front/`"> 返回OA </a>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</div>
</template>
<style lang="less" scoped>
.root {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 30px;
background-color: #fff;
box-shadow: 0 0 12px 0 rgba(0, 0, 0, 0.08);
.user-area {
display: flex;
align-items: center;
color: rgb(51, 51, 51);
.fullscreen-icon-area {
width: 35px;
height: 35px;
display: flex;
justify-content: center;
align-items: center;
background-color: #ecf4fe;
border-radius: 50%;
cursor: pointer;
.fullscreen-icon {
font-size: 20px;
color: #1890ff;
}
}
.avatar {
width: 35px;
height: 35px;
border-radius: 50%;
margin-left: 16px;
margin-right: 8px;
}
}
}
</style>

View File

@@ -0,0 +1,70 @@
<script lang="ts"></script>
<script setup lang="ts">
import type { RouteType } from '@/router/routes';
import type { ItemType } from 'ant-design-vue';
import { computed, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import routeList from '@/router/routes';
function formatMemu(list: RouteType[], path: string = ''): ItemType[] {
return list
.filter((i) => i.isMenu)
.map((item) => {
const key = item.path.startsWith('/')
? item.path
: `${path}/${item.path}`;
return {
key,
icon: item.icon,
children:
item.children.length > 0 ? formatMemu(item.children, key) : void 0,
label: item.meta.title || '-',
};
});
}
const selectedKeys = ref<string[]>(['1']);
const openKeys = ref<string[]>(['sub1']);
const menuList = computed(() => formatMemu(routeList[0].children));
const route = useRoute();
const router = useRouter();
watch(
() => route.path,
(val) => {
if (!selectedKeys.value.includes(val)) {
selectedKeys.value = [val];
openKeys.value = route.matched.slice(1).map((i) => i.path);
}
},
{ immediate: true },
);
const handleClick = (config: { key: string }) => {
router.push(config.key);
};
</script>
<template>
<a-menu
v-model:open-keys="openKeys"
v-model:selected-keys="selectedKeys"
mode="inline"
:items="menuList"
class="sider-root"
@click="handleClick"
v-bind="$attrs"
/>
</template>
<style lang="less" scoped>
.sider-root {
flex-grow: 1;
overflow-y: auto;
border-inline-end: none !important;
}
</style>

View File

@@ -0,0 +1,136 @@
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import Header from "./components/Header.vue";
import Sider from "./components/Sider.vue";
import {
MenuFoldOutlined,
MenuUnfoldOutlined,
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);
const isFullscreen = ref(false);
const container = ref<HTMLDivElement>();
onMounted(() => {
// userInfoStore.fetchUserInfo();
});
useEventListener(window, "fullscreenchange", () => {
isFullscreen.value = !!document.fullscreenElement;
});
const handleFullscreen = () => {
if (container.value) {
container.value.requestFullscreen();
}
};
const handleExitFullscreen = () => {
document.exitFullscreen?.();
};
</script>
<template>
<section v-if="!__POWERED_BY_QIANKUN__" class="root">
<section
class="left-aside"
:class="{ 'left-aside-collapsed': isCollapsed }"
>
<Sider :inlineCollapsed="isCollapsed" />
<div class="collapsed-icon">
<component
:is="isCollapsed ? MenuUnfoldOutlined : MenuFoldOutlined"
@click="isCollapsed = !isCollapsed"
/>
</div>
</section>
<section
class="container"
:class="{
'container-fullscreen': isFullscreen,
'container-collapsed': isCollapsed,
}"
ref="container"
>
<header class="header">
<Header @requestFullscreen="handleFullscreen" />
</header>
<div class="i-container">
<router-view />
</div>
<a-float-button @click="handleExitFullscreen" v-if="isFullscreen">
<template #icon>
<FullscreenExitOutlined />
</template>
</a-float-button>
</section>
</section>
<router-view v-else />
</template>
<style lang="less" scoped>
@header-height: 60px;
@aside-width: 220px;
@aside-width-collapsed: 60px;
@header-margin: 12px;
.root {
height: 100%;
.header {
height: @header-height;
position: fixed;
width: calc(100% - 220px);
z-index: 1;
top: 0;
}
.left-aside {
width: @aside-width;
position: fixed;
left: 0;
z-index: 1;
height: 100vh;
overflow-y: hidden;
box-shadow: 0 0 12px 0 rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
background-color: #fff;
transition: width 0.3s cubic-bezier(0.2, 0, 0, 1) 0s;
.collapsed-icon {
flex-shrink: 0;
display: flex;
justify-content: center;
margin-bottom: 8px;
font-size: 18px;
}
}
.container {
padding-left: @aside-width;
padding-top: @header-height + @header-margin;
height: calc(100% - @header-height - @header-margin);
position: relative;
background-color: #f8f8f8;
transition: padding 0.3s cubic-bezier(0.2, 0, 0, 1) 0s;
margin-right: 8px;
}
.container-collapsed {
padding-left: @aside-width-collapsed + 8px;
}
.container-fullscreen {
padding: 0;
height: 100%;
}
:deep(
:where(.css-dev-only-do-not-override-1hsjdkk).ant-menu-inline-collapsed
),
.left-aside-collapsed {
width: @aside-width-collapsed;
}
}
</style>

View File

@@ -0,0 +1,63 @@
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";
let app;
function render(props: Object = {}) {
app = createApp(App);
setStyleSheet(props.styles);
const router = createProjectRouter(props.base);
app.use(router);
app.use(VueGridLayout);
app.use(createPinia());
app.mount("#y-code-app");
}
function setStyleSheet(styles: Object = {}) {
const styleEle = document.createElement("style");
styleEle.type = "text/css";
styleEle.innerHTML = `
:root {
--primary-color: ${styles.primaryColor || "#1677ff"};
--primary-light-color: ${styles.primaryLightColor || "#4096ff"};
--table-head-bg-color: ${styles.tableHeadBgColor || "#fafafa"};
--table-head-font-color: ${styles.tableHeadFontColor || "#191919"};
}
`;
document.head.appendChild(styleEle);
}
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();
}

View File

@@ -0,0 +1,26 @@
<template>
<div :class="className" :style="style" ref="container"></div>
</template>
<script setup>
import { Column } from "@antv/g2plot";
// hooks
import useChart from "./useChart";
const props = defineProps({
className: {
type: String,
default: "",
},
style: {
type: Object,
default: () => ({}),
},
config: {
type: Object,
default: () => ({}),
},
});
const { container } = useChart(Column, props);
</script>

View File

@@ -0,0 +1,27 @@
<template>
<div :class="className" :style="style" ref="container"></div>
</template>
<script setup>
import { Line } from "@antv/g2plot";
// hooks
import useChart from "./useChart";
const props = defineProps({
className: {
type: String,
default: "",
},
style: {
type: Object,
default: () => ({}),
},
config: {
type: Object,
default: () => ({}),
},
});
const { container } = useChart(Line, props);
</script>

View File

@@ -0,0 +1,26 @@
<template>
<div :class="className" :style="style" ref="container"></div>
</template>
<script setup>
import { Pie } from "@antv/g2plot";
// hooks
import useChart from "./useChart";
const props = defineProps({
className: {
type: String,
default: "",
},
style: {
type: Object,
default: () => ({}),
},
config: {
type: Object,
default: () => ({}),
},
});
const { container } = useChart(Pie, props);
</script>

View File

@@ -0,0 +1,91 @@
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
import { cloneDeep } from "lodash-es";
export default function useChart(ChartClass, props) {
const chart = ref(null); // 表格实例
const chartOptions = ref(null); // 图表配置
const container = ref(null); // 渲染图表元素
const { onReady, onEvent } = props.config;
// 全局事件侦听器
let handler;
onMounted(() => {
chartOptions.value = cloneDeep(props.config);
// 实例化图表
const chartInstance = new ChartClass(container.value, { ...props.config });
chartInstance.toDataURL = (type, encoderOptions) => {
return toDataURL(type, encoderOptions);
};
chartInstance.downloadImage = (name, type, encoderOptions) => {
return downloadImage(name, type, encoderOptions);
};
chartInstance.render(); // 渲染图表
chart.value = chartInstance;
// 图表渲染完成回调
if (onReady) {
onReady(chartInstance);
}
// 侦听全局事件
handler = (event) => {
if (onEvent) {
onEvent(chartInstance, event);
}
};
chartInstance.on("*", handler);
});
onBeforeUnmount(() => {
chart.value.destroy();
chart.value.off("*", handler);
chart.value = undefined;
});
// 配置更改时更新图表
watch(
() => props.config,
(config) => {
const newConfig = cloneDeep(config);
chartOptions.value = newConfig;
chart.value.update(newConfig);
},
{
deep: true,
}
);
const toDataURL = (type = "image/png", encoderOptions) => {
return chart.value?.chart.canvas.cfg.el.toDataURL(type, encoderOptions);
};
const downloadImage = (
name = "download",
type = "image/png",
encoderOptions
) => {
let imageName = name;
if (name.indexOf(".") === -1) {
imageName = `${name}.${type.split("/")[1]}`;
}
const base64 = chart.value?.chart.canvas.cfg.el.toDataURL(
type,
encoderOptions
);
let a = document.createElement("a");
a.href = base64;
a.download = imageName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
a = null;
return imageName;
};
return {
chart,
container,
};
}

View File

@@ -0,0 +1,8 @@
import type { NavigationGuardWithThis } from 'vue-router';
const titleGuard: NavigationGuardWithThis<undefined> = (to, from, next) => {
next();
document.title = to.meta.title ? `${to.meta.title} | 悦码` : '悦码';
};
export { titleGuard };

View File

@@ -0,0 +1,23 @@
import { createRouter, createWebHistory, type Router } from "vue-router";
import { titleGuard } from "./guards";
import routeList from "./routes";
import { qiankunWindow } from "vite-plugin-qiankun/dist/helper";
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);
return router;
};
export default router;

View File

@@ -0,0 +1,117 @@
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;
name: string;
component?: any;
meta: { title?: string };
isMenu?: boolean;
redirect?: string;
children: RouteType[];
icon?: () => VNode<
RendererNode,
RendererElement,
{
[key: string]: any;
}
>;
}
const routeList: RouteType[] = [
{
path: "/",
name: "layout",
component: Layout,
meta: { title: "首页" },
children: [
{
path: "",
name: "-",
meta: {},
children: [],
redirect: "/config-manage/project-cfg",
},
{
path: "/config-manage",
name: "config-manage",
isMenu: true,
meta: { title: "配置管理" },
icon: () => h(HomeOutlined),
children: [
{
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: "数据来源配置" },
isMenu: true,
children: [],
},
],
},
{
path: "/view-all-manage",
name: "view-all-manage",
isMenu: true,
meta: { title: "视图管理" },
icon: () => h(BarChartOutlined),
children: [
{
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: "创建视图" },
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: [],
},
],
},
],
},
];
export default routeList;

View File

@@ -0,0 +1,16 @@
import { readonly, ref } from 'vue';
import { defineStore } from 'pinia';
import { getUserInfo } from '@/api/common';
import type { UserInfoType } from '@/api/common';
export const useUserInfoStore = defineStore('userInfoStore', () => {
const userInfo = ref<UserInfoType>();
const fetchUserInfo = () => {
getUserInfo().then((res) => {
userInfo.value = res.data;
});
};
return { userInfo: readonly(userInfo), fetchUserInfo };
});

View File

@@ -0,0 +1,106 @@
import axios, { AxiosError } from 'axios';
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { message } from 'ant-design-vue';
export interface ResopnseType<T> {
reason: string
message: string
data: T
ts: string
}
// export const YCODE_BASEURL: string = import.meta.env.VITE_YCODE_BASEURL;
// https://custom-chart-pre-api.shiyue.com
export const YCODE_BASEURL: string = 'https://custom-chart-pre-api.shiyue.com'
const requestType = {
base: YCODE_BASEURL,
};
const baseAxios: AxiosInstance = axios.create({
baseURL: '',
timeout: 100000,
withCredentials: true,
});
const errorHandle = (error: AxiosError) => {
if (error.response) {
const status = error.response?.status;
switch (status) {
case 401:
message.warning('请先登录');
window.location.href = `${YCODE_BASEURL}/login?redirect=${encodeURIComponent(window.location.href)}`;
break;
case 403:
message.warning('权限不足');
break;
case 500:
message.warning('服务器出错了…… (>_<)');
break;
default:
message.warning('服务器出错了…… (>_<)');
break;
}
return Promise.reject(error);
}
message.error(error.message);
return Promise.reject(error);
};
//响应拦截器
baseAxios.interceptors.response.use((response: AxiosResponse) => {
const { data, status } = response;
if (status !== 200) {
return Promise.reject(data);
}
if (data.code) {
if (data.code === 200) {
return data;
} else {
message.warning(data.message);
return Promise.reject(data);
}
}
}, errorHandle);
type RequestConfig = Omit<AxiosRequestConfig, 'baseURL'> & { baseURL?: keyof typeof requestType }
const request = <T = any>(config: RequestConfig) => {
const host = requestType[config.baseURL || 'base'];
return new Promise<T>((resolve, reject) => {
baseAxios
.request<any, T>({ ...config, baseURL: host })
.then((res: T) => {
resolve(res);
})
.catch((err: unknown) => {
reject(err);
});
});
};
const get = <T = any>(config?: RequestConfig) => {
return request<ResopnseType<T>>({
...config,
method: 'GET',
});
};
const post = <T = any>(config?: RequestConfig) =>
request<ResopnseType<T>>({
...config,
method: 'POST',
});
const put = <T = any>(config?: RequestConfig) =>
request<ResopnseType<T>>({
...config,
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
});
const del = <T = any>(config?: { url: string }) => request<ResopnseType<T>>({ ...config, method: 'DELETE' });
export { get, post, del, put, request };

View File

@@ -0,0 +1,227 @@
<template>
<a-modal :open="open" @ok="handleOk">
<a-form
:model="formData"
ref="formRef"
:rules="formRules"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
>
<a-form-item label="所属项目" name="project_id">
<a-select
placeholder="请选择所属项目"
v-model:value="formData.project_id"
:options="projectSelect"
@change="toGetDbTable"
/>
</a-form-item>
<a-form-item label="数据来源" name="modular_name">
<a-input
placeholder="请输入数据来源"
v-model:value="formData.modular_name"
/>
</a-form-item>
<a-form-item label="展示状态" name="is_show">
<a-switch
v-model:checked="formData.is_show"
:checkedValue="1"
:unCheckedValue="0"
/>
</a-form-item>
<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
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-password
placeholder="请输入数据库密码"
v-model:value="formData.database_password"
/>
</a-space>
</a-form-item>
</template>
</a-form>
</a-modal>
</template>
<script setup>
import { ref, watch } from "vue";
import { getDbTableSelect } from "@/views/config-manage/module-cfg/service";
const props = defineProps({
open: {
type: Boolean,
default: false,
},
type: {
type: String,
default: "add",
},
data: {
type: Object,
default: () => ({}),
},
projectSelect: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(["ok"]);
const formRules = ref({
modular_name: [
{ 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 tableTypes = ref([]);
const formRef = ref();
const formData = ref({
project_id: undefined,
modular_name: undefined,
is_show: 0,
original_type: 1, // 1 - 自定义2 - 指定表
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(
() => props.data,
(newVal) => {
if (props.type === "add") {
resetFormData();
} else {
formData.value = {
modular_id: newVal.modular_id,
project_id: newVal.project_id,
modular_name: newVal.modular_name,
is_show: newVal.is_show,
original_type: newVal.original_type,
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,
};
}
}
);
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,
original_type: 1, // 1 - 自定义2 - 指定表
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,
};
};
const handleOk = () => {
formRef.value.validate().then(() => {
emit("ok", formData.value);
});
};
</script>

View File

@@ -0,0 +1,441 @@
<template>
<a-modal :open="open" title="字段管理" style="top: 30px" :footer="null">
<div class="field-manager">
<div class="header-box">
<a-space>
<a-input
v-model:value="fieldName"
placeholder="请输入字段名称"
allow-clear
style="width: 200px"
@change="search"
/>
<a-button type="primary" @click="addField">新建</a-button>
</a-space>
</div>
<a-table
:columns="viewCfgCols"
:data-source="dataList"
:pagination="false"
size="small"
bordered
>
<template #bodyCell="{ column, record }">
<template
v-if="
[
'field_name',
'field_title',
'belong_to_table',
'original_sql',
].includes(column.dataIndex)
"
>
<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: 120px"
>
</a-select>
<template v-else>
{{ record.field_numerical_name }}
</template>
</template>
<template v-if="column.dataIndex === 'show_type'">
<a-select
v-if="editableData[record.field_id]"
v-model:value="record.show_type"
:options="showTypeOpts"
placeholder="请选择"
allow-clear
style="width: 120px"
>
</a-select>
<template v-else>
{{ record.show_type }}
</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: 120px"
>
</a-select>
<template v-else>
{{ record.field_type_name }}
</template>
</template>
<template v-if="column.dataIndex === 'is_search'">
<a-switch
v-if="editableData[record.field_id]"
v-model:checked="record.is_search"
:checkedValue="1"
:unCheckedValue="0"
/>
<template v-else>
{{ record.is_search ? "是" : "否" }}
</template>
</template>
<template v-if="column.dataIndex === 'is_other_database'">
<template v-if="editableData[record.field_id]">
<a-switch
v-model:checked="record.is_other_database"
:checkedValue="1"
:unCheckedValue="0"
/>
<a-button
v-if="record.is_other_database"
type="link"
@click="openSpecialModal(record)"
>请填写</a-button
>
</template>
<template v-else>
{{ record.is_other_database ? "是" : "否" }}
</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 size="small" @click="handleCancel(record)"
>取消</a-button
>
</a-space>
<div v-else>
<a-button type="link" @click="handleEdit(record)">修改</a-button>
<a-popconfirm
title="确定删除字段?"
@confirm="handleDelete(record)"
>
<a-button type="link" danger>删除</a-button>
</a-popconfirm>
</div>
</template>
</template>
</a-table>
<a-pagination
:current="pageState.page"
:page-size="pageState.perPage"
:total="pageState.total"
class="pagination-box"
size="small"
@change="pageChange"
/>
</div>
<a-modal
:open="specialVisible"
:width="640"
title="数据库特配"
:bodyStyle="{ marginTop: '30px' }"
@ok="specialVisible = false"
@cancel="specialVisible = false"
>
<a-form :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
<a-form-item label="数据库驱动">
<a-radio-group
v-model:value="specialModalData[specialModalId].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="数据库地址">
<a-input
placeholder="请输入数据库地址"
v-model:value="specialModalData[specialModalId].database_address"
allow-clear
/>
</a-form-item>
<a-form-item label="数据库端口">
<a-input-number
placeholder="请输入数据库端口"
style="width: 200px"
v-model:value="specialModalData[specialModalId].database_port"
allow-clear
/>
</a-form-item>
<a-form-item label="数据库名称">
<a-input
placeholder="请输入数据库名称"
v-model:value="specialModalData[specialModalId].database_name"
allow-clear
/>
</a-form-item>
<a-form-item label="数据库用户">
<a-input
placeholder="请输入数据库用户"
v-model:value="specialModalData[specialModalId].database_username"
allow-clear
/>
</a-form-item>
<a-form-item label="数据库密码">
<a-input-password
placeholder="请输入数据库密码"
v-model:value="specialModalData[specialModalId].database_password"
allow-clear
/>
</a-form-item>
</a-form>
</a-modal>
</a-modal>
</template>
<script setup>
import { onMounted, reactive, ref, watch } from "vue";
import { viewCfgCols, originalTypes, showTypeOpts } from "@/views/config-manage/module-cfg/config";
import {
getFieldTypeSelect,
getFieldNumSelect,
getFieldList,
deleteField,
saveField,
// getFieldDetail,
} from "@/views/config-manage/module-cfg/service";
import { message } from "ant-design-vue";
const props = defineProps({
open: {
type: Boolean,
default: false,
},
modularId: {
type: Number,
default: 0,
},
});
const listLoading = ref(false);
const fieldName = ref("");
const dataList = ref([]);
const fieldTypeSel = ref([]);
const fieldNumTypeSel = ref([]);
const specialVisible = ref(false);
const specialModalId = ref();
const pageState = reactive({
page: 1,
perPage: 20,
total: 0,
});
const editableData = reactive({});
const specialModalData = reactive({
drive_type: undefined,
database_address: undefined,
database_port: undefined,
database_name: undefined,
database_username: undefined,
database_password: undefined,
});
watch(
() => props.open,
(newVal) => {
if (newVal) {
toGetList();
}
},
);
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;
getFieldList({
fieldName: fieldName.value,
modularId: props.modularId,
page: pageState.page,
perPage: pageState.perPage,
}).then((res) => {
dataList.value = res.data.list;
pageState.total = res.data.total;
});
};
const pageChange = () => {
toGetList();
};
const search = () => {
pageState.page = 1;
toGetList();
};
const addField = () => {
const item = {
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,
is_other_database: 0,
original_type: undefined,
original_sql: undefined,
sort: 0,
drive_type: undefined,
database_address: undefined,
database_port: undefined,
database_name: undefined,
database_username: undefined,
database_password: undefined,
};
dataList.value.unshift(item);
editableData[item.field_id] = {
...item,
};
};
const handleEdit = (record) => {
editableData[record.field_id] = {
...record,
};
};
const handleDelete = (record) => {
deleteField({
field_id: record.field_id,
}).then(() => {
message.success('删除成功')
toGetList();
})
}
const handleCancel = (record) => {
if (typeof record.field_id === "string") {
dataList.value.shift();
} else {
delete editableData[record.field_id];
}
};
const openSpecialModal = (record) => {
specialVisible.value = true
specialModalId.value = record.field_id
if (!specialModalData[record.field_id]) {
specialModalData[record.field_id] = {
drive_type: undefined,
database_address: undefined,
database_port: undefined,
database_name: undefined,
database_username: undefined,
database_password: undefined,
}
}
}
const handleSave = (record) => {
const params = {
is_search: record.is_search,
field_type_id: record.field_type_id,
modular_id: props.modularId,
original_type: record.original_type,
original_sql: record.original_sql,
is_other_database: record.is_other_database,
};
// 如果数据库特配弹框有数据
if (specialModalData[record.field_id]) {
params.drive_type = specialModalData[record.field_id].drive_type
params.database_address = specialModalData[record.field_id].database_address
params.database_port = specialModalData[record.field_id].database_port
params.database_name = specialModalData[record.field_id].database_name
params.database_username = specialModalData[record.field_id].database_username
params.database_password = specialModalData[record.field_id].database_password
}
// 检验必填参数
const validateFields = [
{ field: 'field_title', msg: "请填写字段标题" },
{ field: 'field_name', msg: "请填写字段名称" },
{ field: 'field_numerical_type_id', msg: "请选择字段类型" },
{ field: 'belong_to_table', msg: "请填写关联表" },
{ field: 'show_type', msg: "请选择展示类型" },
]
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("保存成功");
toGetList();
});
};
</script>
<style lang="less" scoped>
.header-box {
margin-bottom: 10px;
}
.pagination-box {
text-align: center;
}
:deep(.ycode-ant-btn) {
padding: 4px 8px;
}
</style>

View File

@@ -0,0 +1,41 @@
export const moduleCfgCols = [
{ dataIndex: "modular_id", title: "编号", align: "center" },
{ dataIndex: "modular_name", title: "数据来源名称", align: "center" },
{ dataIndex: "project_name", title: "项目名称", align: "center" },
{ dataIndex: "is_show", title: "展示状态", align: "center" },
{ dataIndex: "original_type_handle", title: "数据源类型", align: "center" },
// { dataIndex: 'sort', title: '排序', align: 'center'},
{ dataIndex: "action", title: "操作", align: "center" },
];
export const viewCfgCols = [
{ dataIndex: "field_name", title: "字段名称", align: "center" },
{ dataIndex: "field_title", title: "字段标题", align: "center" },
{
dataIndex: "field_numerical_name",
title: "字段类型",
align: "center",
width: 120,
},
{ dataIndex: "show_type", title: "展示类型", align: "center", width: 120 },
{ 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: "is_other_database", title: "数据库特配", align: "center" },
{ dataIndex: "original_type", title: "数据源类型", align: "center" },
{ dataIndex: "original_sql", title: "数据源", align: "center", width: 400 },
{ dataIndex: "action", title: "操作", align: "center", width: 120 },
];
export const originalTypes = [
{ label: "sql", value: 1 },
{ label: "json", value: 2 },
];
export const showTypeOpts = [
{ label: "文本", value: "text" },
{ label: "图片", value: "img" },
{ label: "链接", value: "link" },
{ label: "富文本", value: "richText" },
];

View File

@@ -0,0 +1,233 @@
<template>
<div class="normal-container module-cfg-box">
<div class="header-box">
<a-space>
<a-input
v-model:value="modularName"
placeholder="请输入数据来源名称"
allow-clear
style="width: 200px"
@change="search"
/>
<a-select
placeholder="请选项目"
v-model:value="projectId"
:options="projectSel"
allow-clear
style="width: 200px"
@change="search"
></a-select>
<a-button type="primary" @click="openCreateModal">新建</a-button>
</a-space>
</div>
<div class="content-box">
<a-table
:columns="moduleCfgCols"
:data-source="dataList"
:pagination="false"
size="small"
bordered
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'is_show'">
<a-switch
:checked="record.is_show"
:checkedValue="1"
:unCheckedValue="0"
@change="toChangeStatus(record.modular_id, $event)"
/>
</template>
<template v-if="column.dataIndex === 'action'">
<a-space>
<a-button type="link" size="small" @click="openFieldModal(record)"
>字段管理</a-button
>
<a-button
type="link"
size="small"
@click="toGetModularDetail(record.modular_id)"
>编辑</a-button
>
<a-popconfirm
title="确定删除吗?"
@confirm="toDelete(record.modular_id)"
>
<a-button type="link" size="small">删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
<a-pagination
v-model:current="pageState.page"
:total="pageState.total"
:page-size="pageState.perPage"
:hide-on-single-page="false"
size="small"
class="pagination-box"
@change="toGetModularList"
/>
</div>
<CreateModal
:width="700"
:open="modalState.visible"
:title="modalState.title"
:type="modalState.type"
:data="modalState.data"
:projectSelect="projectSel"
@cancel="modalState.visible = false"
@ok="toSave"
/>
<FieldModal
title="字段管理"
width="90%"
:open="fieldModalState.visible"
:modularId="fieldModalState.modularId"
@cancel="fieldModalState.visible = false"
/>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import { message } from "ant-design-vue";
import { moduleCfgCols } from "./config";
import {
getModularList,
deleteModular,
getModularDetail,
getProjectSelect,
saveModular,
updateStatus,
} from "./service";
import CreateModal from "./components/create-modal.vue";
import FieldModal from "./components/field-modal.vue";
const dataList = ref([]);
const listLoading = ref(false);
const saveLoading = ref(false);
const detailLoading = ref(false);
const modularName = ref("");
const projectId = ref();
const projectSel = ref([]);
const modalState = reactive({
title: "",
visible: false,
type: "", // add - 新建edit - 编辑
data: {},
});
const fieldModalState = reactive({
visible: false,
title: "",
modularId: undefined,
});
const pageState = reactive({
page: 1,
pageSize: 20,
total: 0,
});
onMounted(() => {
toGetModularList();
toGetProjectSel();
});
const toGetProjectSel = () => {
getProjectSelect().then((res) => {
projectSel.value = res.data;
});
};
const toGetModularList = () => {
listLoading.value = true;
getModularList({
page: pageState.page,
perPage: pageState.pageSize,
modularName: modularName.value,
projectId: projectId.value,
}).then((res) => {
dataList.value = res.data.list;
pageState.total = res.data.total;
listLoading.value = false;
});
};
// 详情
const toGetModularDetail = (modularId) => {
detailLoading.value = true;
modalState.visible = true;
modalState.title = "编辑";
modalState.type = "edit";
getModularDetail({ modularId })
.then((res) => {
modalState.data = res.data;
})
.finally(() => {
detailLoading.value = false;
});
};
// 保存
const toSave = (data) => {
saveLoading.value = true;
saveModular(data)
.then(() => {
message.success("保存成功");
modalState.visible = false;
toGetModularList();
})
.finally(() => {
saveLoading.value = false;
});
};
// 删除
const toDelete = (id) => {
deleteModular({ modular_id: id }).then(() => {
message.success("删除成功");
search();
});
};
const toChangeStatus = (id, status) => {
updateStatus({
modular_id: id,
status,
}).then(() => {
message.success("修改成功");
search();
});
};
// 点击新建
const openCreateModal = () => {
modalState.visible = true;
modalState.title = "新建";
modalState.type = "add";
modalState.data = {};
};
const search = () => {
pageState.page = 1;
toGetModularList();
};
const openFieldModal = (record) => {
fieldModalState.visible = true;
fieldModalState.modularId = record.modular_id;
};
</script>
<style lang="less">
.header-box {
margin-bottom: 10px;
}
.ycode-ant-table-wrapper {
margin-bottom: 10px;
}
.pagination-box {
text-align: center;
}
</style>

View File

@@ -0,0 +1,118 @@
import { get, post } from "@/utils/request";
// 获取数据表配置列表
export function getModularList({ page, perPage, modularName, projectId }) {
return get({
url: "/api/v1/modular/list",
params: {
page,
per_page: perPage,
modular_name: modularName,
project_id: projectId,
},
});
}
// 获取数据表配置详情
export function getModularDetail({ modularId }) {
return get({
url: "/api/v1/modular/info",
params: {
modular_id: modularId,
},
});
}
// 保存数据库配置
export function saveModular(data) {
return post({
url: "/api/v1/modular/save",
data,
});
}
// 删除数据库配置
export function deleteModular(data) {
return post({
url: "/api/v1/modular/del",
data,
});
}
// 修改数据表状态
export function updateStatus(data) {
return post({
url: "/api/v1/modular/change-status",
data,
});
}
// 项目下拉
export function getProjectSelect() {
return get({
url: `/api/v1/project/get-project-drop`,
});
}
// 数据表源下拉
export function getDbTableSelect({ projectId }) {
return get({
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({
url: "/api/v1/field/list",
params: {
modular_id: modularId,
field_name: fieldName,
page,
per_page: perPage,
},
});
}
// 获取字段详情
export function getFieldDetail({ fieldId }) {
return get({
url: "/api/v1/field/info",
params: {
field_id: fieldId,
},
});
}
// 保存字段
export function saveField(data) {
return post({
url: "/api/v1/field/save",
data,
});
}
// 删除字段
export function deleteField(data) {
return post({
url: "/api/v1/field/del",
data,
});
}

View File

@@ -0,0 +1,183 @@
<template>
<a-modal :open="open" @ok="handleOk">
<a-form
:model="formData"
ref="formRef"
:rules="formRules"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
>
<a-form-item label="项目名称" name="project_name">
<a-input
placeholder="请输入项目名称例如OA"
v-model:value="formData.project_name"
/>
</a-form-item>
<a-form-item label="项目标识" name="mark">
<a-input placeholder="请输入项目标识例如oa" v-model:value="formData.mark" />
</a-form-item>
<a-form-item label="展示状态" name="is_show">
<a-switch
v-model:checked="formData.is_show"
:checkedValue="1"
:unCheckedValue="0"
/>
</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-password
placeholder="请输入数据库密码"
v-model:value="formData.database_password"
/>
<a-button type="primary" @click="toCheckDbConnect"
>检测数据库连接</a-button
>
</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>
<script setup>
import { ref, watch } from "vue";
import { checkDbConnect } from "@/views/config-manage/project-cfg/service";
import { message } from "ant-design-vue";
const props = defineProps({
open: {
type: Boolean,
default: false,
},
type: {
type: String,
default: "add",
},
data: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits(["ok"]);
const formRules = {
project_name: [
{ required: true, message: "请输入项目名称", trigger: "submit" },
],
mark: [{ required: true, message: "请输入项目标识", trigger: "submit" }],
database_address: [
{ required: true, message: "请输入数据库地址", trigger: "submit" },
],
database_port: [
{ required: true, message: "请输入数据库端口", trigger: "submit" },
],
database_name: [
{ required: true, message: "请输入数据库名称", trigger: "submit" },
],
};
const formRef = ref();
const formData = ref({
project_name: undefined,
mark: undefined,
is_show: 0,
database_address: undefined,
database_port: undefined,
database_name: undefined,
database_username: undefined,
database_password: undefined,
drive_type: 1,
});
watch(
() => props.data,
(newVal) => {
if (props.type === "add") {
resetFormData();
} else {
formData.value = newVal;
}
}
);
// 检查数据库连接
const toCheckDbConnect = () => {
if (validateConnect()) {
checkDbConnect({
database_name: formData.value.database_name,
database_port: formData.value.database_port,
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);
});
}
};
const validateConnect = () => {
const fields = {
database_name: "请输入数据库名称",
database_port: "请输入数据库端口",
database_address: "请输入数据库地址",
};
for (const key in fields) {
if (!formData.value[key]) {
message.error(fields[key]);
return false;
}
}
return true;
};
const resetFormData = () => {
formData.value = {
project_name: undefined,
mark: undefined,
is_show: 0,
database_address: undefined,
database_port: undefined,
database_name: undefined,
database_username: undefined,
database_password: undefined,
drive_type: 1,
};
};
const handleOk = () => {
formRef.value.validate().then(() => {
emit("ok", formData.value);
});
};
</script>

View File

@@ -0,0 +1,9 @@
export const projectCfgCols = [
{ dataIndex: 'project_id', title: '编号', align: 'center'},
{ dataIndex: 'project_name', title: '项目名称', align: 'center'},
{ dataIndex: 'mark', title: '项目标识', align: 'center'},
{ dataIndex: 'database_name', title: '数据库名', align: 'center'},
{ dataIndex: 'is_show', title: '展示状态', align: 'center'},
// { dataIndex: 'sort', title: '排序', align: 'center'},
{ dataIndex: 'action', title: '操作', align: 'center'},
];

View File

@@ -0,0 +1,201 @@
<template>
<div class="normal-container project-cfg-box">
<div class="header-box">
<a-space>
<a-input
v-model:value="projectName"
placeholder="请输入项目名称"
allow-clear
style="width: 200px"
@change="search"
/>
<a-button type="primary" @click="openCreateModal">新建</a-button>
</a-space>
</div>
<div class="content-box">
<a-table
:columns="projectCfgCols"
:data-source="dataList"
:pagination="false"
size="small"
bordered
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'is_show'">
<a-switch
v-model:checked="record.is_show"
:checkedValue="1"
:unCheckedValue="0"
@change="toChangeStatus(record.project_id, $event)"
/>
</template>
<template v-if="column.dataIndex === 'action'">
<a-space>
<a-button
type="link"
size="small"
@click="toGetDetail(record.project_id)"
>
编辑
</a-button>
<a-popconfirm
title="确定删除?"
@confirm="toDelete(record.project_id)"
>
<a-button type="link" size="small">删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
<a-pagination
v-model:current="pageState.page"
:total="pageState.total"
:page-size="pageState.perPage"
:hide-on-single-page="false"
size="small"
class="pagination-box"
@change="toGetProjectList"
/>
</div>
<CreateModal
:width="700"
:open="modalState.visible"
:title="modalState.title"
:type="modalState.type"
:data="modalState.data"
:confirmLoading="saveLoading"
@cancel="modalState.visible = false"
@ok="toSave"
/>
</div>
</template>
<script setup>
import { onMounted, ref, reactive } from "vue";
import { message } from "ant-design-vue";
import { projectCfgCols } from "./config";
import {
getProjectList,
saveProject,
deleteProject,
getProjectDetail,
updateStatus,
} from "./service";
import CreateModal from "./components/create-modal.vue";
const dataList = ref([]);
const listLoading = ref(false);
const saveLoading = ref(false);
const detailLoading = ref(false);
const projectName = ref("");
const modalState = reactive({
title: undefined,
visible: false,
type: "", // add - 新建edit - 编辑
data: {
project_name: undefined,
is_show: 0,
database_address: undefined,
database_port: undefined,
database_name: undefined,
database_username: undefined,
database_password: undefined,
},
});
const pageState = reactive({
page: 1,
perPage: 20,
total: 0,
});
onMounted(() => {
toGetProjectList();
});
// 获取列表
const toGetProjectList = () => {
listLoading.value = true;
getProjectList({
page: pageState.page,
perPage: pageState.perPage,
projectName: projectName.value,
})
.then((res) => {
dataList.value = res.data.list;
pageState.total = res.data.total;
})
.finally(() => {
listLoading.value = false;
});
};
// 保存
const toSave = (data) => {
saveLoading.value = true;
saveProject(data)
.then(() => {
message.success("保存成功");
modalState.visible = false;
toGetProjectList();
})
.finally(() => {
saveLoading.value = false;
});
};
// 获取详情
const toGetDetail = (projectId) => {
detailLoading.value = true;
modalState.visible = true;
modalState.title = "编辑";
modalState.type = "edit";
getProjectDetail({ projectId }).then((res) => {
modalState.data = res.data;
});
};
// 删除
const toDelete = (id) => {
deleteProject({ projectId: id }).then(() => {
message.success("删除成功");
search();
});
};
const toChangeStatus = (id, status) => {
updateStatus({ project_id: id, status }).then(() => {
message.success("修改成功");
search();
});
};
const search = () => {
pageState.page = 1;
toGetProjectList();
};
// 点击新建
const openCreateModal = () => {
modalState.visible = true;
modalState.title = "新建";
modalState.type = "add";
modalState.data = {};
};
</script>
<style lang="less" scope>
.header-box {
margin-bottom: 10px;
}
.ycode-ant-table-wrapper {
margin-bottom: 10px;
}
.pagination-box {
text-align: center;
}
</style>

View File

@@ -0,0 +1,57 @@
import { get, post } from "@/utils/request";
// 获取项目列表
export function getProjectList({ page, perPage, projectName }) {
return get({
url: `api/v1/project/list`,
params: {
page,
per_page: perPage,
project_name: projectName,
},
});
}
// 获取项目详情
export function getProjectDetail({ projectId }) {
return get({
url: `api/v1/project/info`,
params: {
project_id: projectId,
},
});
}
// 保存
export function saveProject(data) {
return post({
url: `/api/v1/project/save`,
data,
});
}
// 删除
export function deleteProject({ projectId }) {
return post({
url: `/api/v1/project/del`,
data: {
project_id: projectId,
},
});
}
// 检测数据库链接
export function checkDbConnect(data) {
return post({
url: `/api/v1/project/check-database-connect`,
data,
});
}
// 修改展示状态
export function updateStatus(data) {
return post({
url: `/api/v1/project/change-status`,
data,
});
}

View File

@@ -0,0 +1,110 @@
<template>
<a-modal @ok="handleOk">
<a-form
:model="formData"
ref="formRef"
:rules="formRules"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
>
<a-form-item label="字段标题" name="field_title">
<a-input
placeholder="请输入字段标题"
v-model:value="formData.field_title"
/>
</a-form-item>
<a-form-item label="字段名称" name="field_name">
<a-input placeholder="请输入字段名称" v-model="formData.field_name" />
</a-form-item>
<a-form-item label="搜索状态" name="is_search">
<a-switch
v-model:checked="formData.is_search"
:checkedValue="1"
:unCheckedValue="0"
/>
</a-form-item>
<a-form-item label="字段类型" name="field_type_id">
<a-select
placeholder="请选择字段类型"
v-model:value="formData.field_type_id"
/>
</a-form-item>
<a-form-item label="所属表名称" name="belong_to_table">
<a-input
placeholder="请输入所属表名称"
v-model="formData.belong_to_table"
/>
</a-form-item>
<a-form-item label="sql数据源" name="original_sql">
<a-input
placeholder="请输入sql数据源"
v-model="formData.original_sql"
/>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup>
import { ref, watch } from "vue";
const props = defineProps({
type: {
type: String,
default: "add",
},
data: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits(["ok"]);
const formRules = {
field_title: [
{ required: true, message: "请输入字段标题", trigger: "submit" },
],
field_name: [
{ required: true, message: "请输入字段名称", trigger: "submit" },
],
field_type_id: [
{ required: true, message: "请选择字段类型", trigger: "submit" },
],
belong_to_table: [
{ required: true, message: "请输入所属表", trigger: "submit" },
],
original_sql: [
{ required: true, message: "请输入sql数据源", trigger: "submit" },
],
};
const formRef = ref();
const formData = ref({
field_title: undefined,
field_name: undefined,
is_search: 0,
field_type_id: undefined,
belong_to_table: undefined,
original_sql: undefined,
modular_id: undefined,
});
watch(
() => props.type,
(newVal) => {
if (newVal === "add") {
resetFormData();
} else {
formData.value = props.data;
}
}
);
const resetFormData = () => {};
const handleOk = () => {
formRef.value.validate().then(() => {
emit("ok", formData.value);
});
};
</script>

View File

@@ -0,0 +1,10 @@
export const viewCfgCols = [
{ dataIndex: 'field_name', title: '字段名称', align: 'center'},
{ dataIndex: 'field_title', 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: 'action', title: '操作', align: 'center'},
];

View File

@@ -0,0 +1,181 @@
<template>
<div class="normal-container view-cfg-box">
<div class="header-box">
<a-space>
<a-input
v-model:value="fieldName"
placeholder="请输入字段名称"
allow-clear
style="width: 200px"
@change="search"
/>
<a-select
placeholder="请选择数据表id"
v-model:value="modularId"
allow-clear
style="width: 160px"
@change="search"
></a-select>
<a-button type="primary" @click="openCreateModal">新建</a-button>
</a-space>
</div>
<div class="content-box">
<a-table
:columns="viewCfgCols"
:data-source="dataList"
:pagination="false"
size="small"
bordered
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a-space>
<a-button
type="link"
size="small"
@click="toGetDetail(record.field_id)"
>编辑</a-button
>
<a-popconfirm
title="确定删除?"
@confirm="toDelete(record.field_id)"
>
<a-button type="link" size="small">删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
<a-pagination
v-model:current="pageState.page"
:total="pageState.total"
:page-size="pageState.perPage"
:hide-on-single-page="false"
size="small"
class="pagination-box"
@change="toGetFieldList"
/>
</div>
<CreateModal
:width="700"
:open="modalState.visible"
:title="modalState.title"
:type="modalState.type"
:data="modalState.data"
@cancel="modalState.visible = false"
@ok="toSave"
/>
</div>
</template>
<script setup>
import { onMounted, ref, reactive } from "vue";
import { viewCfgCols } from "./config";
import {
getFieldList,
deleteField,
saveField,
getFieldDetail,
} from "./service";
import CreateModal from "./components/create-modal.vue";
import { message } from "ant-design-vue";
const listLoading = ref(false);
const saveLoading = ref(false);
const dataList = ref([]);
const fieldName = ref("");
const modularId = ref("");
const modalState = reactive({
title: undefined,
visible: false,
type: "", // add - 新建edit - 编辑
data: {},
});
const pageState = reactive({
page: 1,
perPage: 20,
total: 0,
});
onMounted(() => {
toGetFieldList();
});
const toGetFieldList = () => {
listLoading.value = true;
getFieldList({
fieldName: fieldName.value,
modularId: modularId.value,
page: pageState.page,
perPage: pageState.perPage,
})
.then((res) => {
dataList.value = res.data.list;
pageState.total = res.data.total;
})
.finally(() => {
listLoading.value = false;
});
};
// 保存
const toSave = (data) => {
saveLoading.value = true;
saveField(data)
.then(() => {
message.success("保存成功");
})
.finally(() => {
saveLoading.value = false;
});
};
// 获取详情
const toGetDetail = (fieldId) => {
modalState.visible = true;
modalState.title = "编辑";
modalState.type = "edit";
getFieldDetail({ fieldId }).then((res) => {
modalState.data = res.data;
});
};
// 删除
const toDelete = (fieldId) => {
deleteField({ field_id: fieldId }).then(() => {
message.success("删除成功");
search();
});
};
// 点击新建
const openCreateModal = () => {
modalState.visible = true;
modalState.title = "新建";
modalState.type = "add";
modalState.data = {};
};
const handleEdit = (record) => {
console.log(record);
};
const search = () => {
pageState.page = 1;
toGetFieldList();
};
</script>
<style lang="less" scoped>
.header-box {
margin-bottom: 10px;
}
.ycode-ant-table-wrapper {
margin-bottom: 10px;
}
.pagination-box {
text-align: center;
}
</style>

View File

@@ -0,0 +1,40 @@
import { get, post } from "@/utils/request";
// 获取列表
export function getFieldList({ modularId, fieldName, page, perPage }) {
return get({
url: "/api/v1/field/list",
params: {
modularId,
fieldName,
page,
perPage,
},
});
}
// 获取详情
export function getFieldDetail({ fieldId }) {
return get({
url: "/api/v1/field/info",
params: {
fieldId,
},
});
}
// 保存字段
export function saveField(data) {
return post({
url: "/api/v1/field/save",
data,
});
}
// 删除字段
export function deleteField(data) {
return post({
url: "/api/v1/field/del",
data,
});
}

View File

@@ -0,0 +1,342 @@
<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"
:preview-id="item.id"
:filter-config="item.data.filter"
:data-list="item.data.data"
:column-config="item.data.header"
:total="item.data.count"
:title="item.data.preview_name"
:is-export="item.data.is_export"
@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"
:preview-id="item.id"
:filter-config="item.data.filter"
:data-list="item.data.data"
:column-config="item.data.header"
:total="item.data.count"
:title="item.data.preview_name"
:is-export="item.data.is_export"
@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

@@ -0,0 +1,59 @@
@import "@/assets/styles/variable.less";
// 设置按钮
:deep(.ycode-ant-btn-primary) {
background-color: @primary-color !important;
&:hover {
background-color: @primary-light-color !important;
}
}
// 设置输入框
:deep(
.ycode-ant-input-affix-wrapper:not(
.ycode-ant-input-affix-wrapper-disabled
):hover
) {
border-color: @primary-light-color !important;
}
// 设置选择框
:deep(.ycode-ant-select-selector:hover) {
border-color: @primary-light-color !important;
}
:deep(.ycode-ant-select-focused) {
.ycode-ant-select-selector {
border-color: @primary-light-color !important;
}
}
// 设置日期框
:deep(.ycode-ant-picker) {
&:hover,
&-focused {
border-color: @primary-light-color !important;
}
&-active-bar {
background-color: @primary-light-color !important;
}
}
// 设置表格
:deep(.ycode-ant-table-thead > tr > th) {
background-color: @table-head-bg-color !important;
color: @table-head-font-color !important;
}
// 设置分页器
:deep(.ycode-ant-pagination-item) {
&-active {
border-color: @primary-color !important;
a {
color: @primary-color !important;
}
}
&-link-icon {
color: @primary-color !important;
}
}

View File

@@ -0,0 +1,538 @@
<template>
<div class="normal-container">
<div class="view-create-box">
<div class="left-box">
<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-form-item>
<a-form-item label="数据来源">
<a-select
placeholder="请选择"
:options="modularSel"
v-model:value="modularId"
@change="onModularChange"
></a-select>
</a-form-item>
<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="showTypeId === 1">
<a-switch
v-model:checked="isExport"
:checkedValue="1"
:unCheckedValue="0"
>
</a-switch>
</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
>
</a-checkbox-group>
</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
>
<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 in previewData.filterConfig"
:key="item.name"
class="filter-item"
>
<span>{{ item.label }}</span>
<a-select
v-if="item.type === 'select'"
class="input-item"
:options="item.options"
v-model:value="previewData.filterData[item.name]"
placeholder="请选择"
allow-clear
@change="toFilt"
></a-select>
<a-input
v-if="item.type === 'text'"
class="input-item"
placeholder="请输入"
allow-clear
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"
/>
<a-range-picker
v-else-if="item.type === 'date_time'"
class="date-time-item"
v-model:value="previewData.filterData[item.name]"
show-time
format="YYYY-MM-DD HH:mm"
@change="toFilt"
/>
<!-- 数值区间 -->
<div
v-else-if="item.type === 'number_range' && previewData.filterData[item.name]"
class="number_range_box"
>
<a-input-number
placeholder="最小值"
style="width: 140px"
v-model:value="previewData.filterData[item.name].min"
@change="toFilt"
/>
<span class="divider"> - </span>
<a-input-number
placeholder="最大值"
style="width: 140px"
v-model:value="previewData.filterData[item.name].max"
@change="toFilt"
/>
</div>
</div>
<div v-if="previewData.isExport" class="filter-item">
<a-button type="primary">导出</a-button>
</div>
</div>
<div class="y-table-content">
<a-table
:columns="previewData.columnConfig"
:data-source="previewData.dataList"
:pagination="false"
:scroll="{ x: 1000, y: `calc(100vh - 260px)` }"
size="small"
bordered
>
<template #bodyCell="{ column, record }">
<a-image
v-if="column.show_type === 'img'"
:src="record[column.dataIndex]"
:width="160"
/>
<a
v-else-if="column.show_type === 'link'"
target="_blank"
:href="record[column.dataIndex]"
>{{ record[column.dataIndex] }}</a
>
<div
v-else-if="column.show_type === 'richText'"
v-html="record[column.dataIndex]"
></div>
<template v-else>{{ record[column.dataIndex] }}</template>
</template>
</a-table>
<a-pagination
v-model:current="previewData.page"
:total="previewData.total"
:page-size="previewData.perPage"
:hide-on-single-page="false"
size="small"
class="pagination-box"
@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>
</div>
</div>
<a-modal
:open="nameVisible"
title="保存视图"
:bodyStyle="{ paddingTop: '20px' }"
@cancel="nameVisible = false"
@ok="toSaveView"
>
<a-form :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
<a-form-item label="视图名称">
<a-input placeholder="请输入视图名称" v-model:value="previewName" />
</a-form-item>
</a-form>
</a-modal>
</div>
</div>
</template>
<script setup>
import { onMounted, reactive, ref } from "vue";
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 { cloneDeep } from "lodash-es";
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 isExport = ref(0);
const previewLoading = ref(false);
const nameVisible = ref(false);
const previewName = ref();
const previewData = reactive({
type: "",
filterConfig: [], // 筛选条件
columnConfig: [], // 表格表头
dataList: [], // 表格数据
filterData: {},
config: {},
filter: [],
page: 1,
perPage: 20,
total: 0,
});
onMounted(() => {
toGetProModularField();
toGetShowTypes();
});
const toGetProModularField = () => {
getProModularField().then((res) => {
projectSel.value = res.data;
});
};
// 获取展示类型下拉
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
isExport.value = 0
fieldList.value = [];
fieldIds.value = [];
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 = ({filter}) => {
previewLoading.value = true;
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) => {
if (item.type === 'time' && previewData.filterData[item.name]) {
// 日期类型的参数
return {
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'),
};
} else if (item.type === 'date_time' && previewData.filterData[item.name]) {
// 带时分的日期类型参数
return {
name: item.name,
type: item.type,
start_time: previewData.filterData[item.name][0].format('YYYY-MM-DD HH:mm') + ':00',
end_time: previewData.filterData[item.name][1].format('YYYY-MM-DD HH:mm') + ':59',
};
} else if (item.type === 'number_range') {
// 数值区间
return {
name: item.name,
type: item.type,
min: previewData.filterData[item.name].min ? String(previewData.filterData[item.name].min) : '',
max: previewData.filterData[item.name].max ? String(previewData.filterData[item.name].max) : '',
};
} else {
return {
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: filterData,
showTypeId: showTypeId.value,
xDataId: xDataId.value?.toString(),
yDataId: yDataId.value?.toString(),
isExport: isExport.value,
})
.then((res) => {
previewData.type = res.data.type;
if (res.data.type === "table") {
previewData.filterConfig = res.data.filter;
previewData.filterConfig.forEach((item) => {
if (item.type === 'number_range' && !previewData.filterData[item.name]) {
previewData.filterData[item.name] = {
...item,
min: undefined,
max: undefined,
};
} else {
previewData.filterData[item.name] = previewData.filterData[item.name]
? previewData.filterData[item.name]
: undefined;
}
});
previewData.columnConfig = res.data.header;
previewData.dataList = res.data.data;
previewData.total = res.data.count;
previewData.isExport = res.data.is_export
} else {
previewData.chartCfg = res.data.config;
previewData.filter = res.data.filter;
}
})
.finally(() => {
previewLoading.value = false;
});
};
const addViewName = () => {
nameVisible.value = true;
previewName.value = null;
};
const toSaveView = () => {
if (!previewName.value) {
message.error("请输入名称");
return;
}
saveView({
modularId: modularId.value,
fieldIds: fieldIds.value.toString(),
previewName: previewName.value,
showTypeId: showTypeId.value,
xDataId: xDataId.value?.toString(),
yDataId: yDataId.value?.toString(),
isExport: isExport.value,
}).then(() => {
message.success("保存成功,可前往视图列表查看");
nameVisible.value = false;
});
};
const toFilt = () => {
previewData.page = 1;
toPreview({});
};
</script>
<style lang="less" scoped>
.normal-container {
height: calc(100vh - 120px);
padding: 16px;
}
.view-create-box {
display: flex;
height: 100%;
}
.left-box {
width: 320px;
flex-shrink: 0;
height: calc(100% - 20px);
padding: 10px;
border-right: 1px solid #ddd;
.footer {
text-align: right;
.preview-btn {
margin-right: 10px;
}
}
}
.right-box {
padding: 0 0 0 10px;
flex-grow: 1;
overflow: auto;
}
.preview-area {
background-color: #f6f6f6;
height: 100%;
width: 100%;
font-size: 20px;
color: #999;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.anticon-bar-chart {
font-size: 100px;
}
}
.y-table-name {
.title {
font-size: 18px;
font-weight: bold;
}
}
.y-table-filter {
display: flex;
flex-wrap: wrap;
}
.filter-item {
margin-right: 10px;
margin-bottom: 6px;
font-size: 14px;
}
.input-item {
width: 180px;
}
.date-item {
width: 240px;
}
.date-time-item {
width: 300px;
}
.number_range_box {
display: inline-flex;
align-items: center;
.divider {
margin: 0 4px;
}
}
.y-table-content {
margin-top: 10px;
}
.pagination-box {
text-align: center;
margin-top: 10px;
}
</style>

View File

@@ -0,0 +1,59 @@
import { get, post } from "@/utils/request";
// 项目-表-字段下拉
export function getProModularField() {
return get({
url: "/api/v1/field/get-project-modular-field-drop",
});
}
// 展示类型下拉
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, showTypeId, xDataId, yDataId, isExport }) {
return post({
url: "api/v1/preview/view",
data: {
modular_id: modularId,
field_ids: fieldIds,
page,
per_page: perPage,
filter,
show_type_id: showTypeId,
x_data_id: xDataId,
y_data_id: yDataId,
is_export: isExport,
},
});
}
// 点击保存
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

@@ -0,0 +1,6 @@
export const viewListCols = [
{ dataIndex: 'preview_id', title: 'id', align: 'center' },
{ dataIndex: 'preview_name', title: '视图名称', align: 'center' },
{ dataIndex: 'created_at', title: '创建时间', align: 'center' },
{ dataIndex: 'action', title: '操作', align: 'center' },
];

View File

@@ -0,0 +1,239 @@
<template>
<div class="normal-container">
<div class="view-list-box">
<div class="left-box">
<a-form :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="项目">
<a-select
:options="projectSel"
v-model:value="projectId"
placeholder="请选择项目"
@change="onProjectChange"
></a-select>
</a-form-item>
<a-form-item label="数据来源">
<a-select
:options="modularSel"
v-model:value="modularId"
placeholder="请先选好项目再选择"
@change="onModularChange"
></a-select>
</a-form-item>
</a-form>
<a-table
:data-source="dataList"
:columns="viewListCols"
:pagination="false"
:row-class-name="
(record) =>
record.preview_id === selectedRowId ? 'selected-row' : ''
"
:custom-row="
(record, index) => {
return {
onClick: () => {
selectedRowId = record.preview_id;
toGetViewInfo();
},
};
}
"
size="small"
bordered
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a-popconfirm
title="确定删除"
@confirm="toDelete(record.preview_id)"
>
<a-button
type="link"
@click="
(e) => {
e.stopPropagation();
}
"
>删除</a-button
>
</a-popconfirm>
</template>
</template>
</a-table>
<a-pagination
v-model:current="pageState.page"
:total="pageState.total"
:page-size="pageState.perPage"
:hide-on-single-page="false"
size="small"
class="pagination-box"
@change="toGetList"
/>
</div>
<div class="right-box">
<y-table
v-if="selectViewInfo.type === 'table'"
:previewId="selectedRowId"
:filter-config="selectViewInfo.filter"
:data-list="selectViewInfo.data"
:column-config="selectViewInfo.header"
:total="selectViewInfo.count"
:title="selectViewInfo.preview_name"
:is-export="selectViewInfo.is_export"
@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>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, ref, reactive } from "vue";
import { getProModular, getViewList, getViewInfo, deleteView } from "./service";
import { viewListCols } from "./config";
import yTable from "@/components/common/y-table.vue";
import { message } from "ant-design-vue";
import { BarChartOutlined } from "@ant-design/icons-vue";
const projectSel = ref([]);
const modularSel = ref([]);
const projectId = ref();
const modularId = ref();
const dataList = ref([]);
const selectedRowId = ref();
const selectViewInfo = ref({
type: "",
filter: [],
config: {
line: {
data: []
}
}
});
const pageState = reactive({
page: 1,
perPage: 20,
total: 0,
});
onMounted(() => {
toGetProModular();
});
const toGetProModular = () => {
getProModular().then((res) => {
projectSel.value = res.data;
if (res.data.length) {
projectId.value = res.data[0].value;
onProjectChange(projectId.value)
}
});
};
const toGetList = () => {
getViewList({
modularId: modularId.value,
page: pageState.page,
perPage: pageState.perPage,
}).then((res) => {
dataList.value = res.data.list;
pageState.total = res.data.total;
});
};
const toGetViewInfo = (params = {}) => {
getViewInfo({
previewId: selectedRowId.value,
...params,
}).then((res) => {
selectViewInfo.value = res.data;
});
};
const onProjectChange = (val) => {
modularSel.value = projectSel.value.find((item) => item.value === val).child;
modularId.value = null;
};
const onModularChange = () => {
pageState.page = 1;
toGetList();
};
const toDelete = (previewId) => {
deleteView({ previewId }).then(() => {
message.success("删除成功");
toGetList();
});
};
</script>
<style lang="less" scoped>
.normal-container {
height: calc(100vh - 120px);
padding: 16px;
}
.view-list-box {
display: flex;
height: 100%;
}
.left-box {
width: 320px;
height: calc(100% - 20px);
padding: 10px;
flex-shrink: 0;
border-right: 1px solid #ddd;
overflow: auto;
}
:deep(.ycode-ant-table-row:hover) {
cursor: pointer;
background-color: #e6f4ff;
}
:deep(.selected-row) {
background-color: #e6f4ff;
}
.pagination-box {
text-align: center;
margin-top: 10px;
}
.right-box {
padding: 0 0 0 10px;
flex-grow: 1;
overflow: auto;
}
.preview-area {
background-color: #f6f6f6;
height: 100%;
width: 100%;
font-size: 20px;
color: #999;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.anticon-bar-chart {
font-size: 100px;
}
}
</style>

View File

@@ -0,0 +1,48 @@
import { get, post } from "@/utils/request";
// 联动下拉
export function getProModular() {
return get({
url: "/api/v1/field/get-project-modular-drop",
});
}
// 视图列表
export function getViewList({ modularId, page = 1, perPage = 20 }) {
return get({
url: "/api/v1/preview/list",
params: {
modular_id: modularId,
page,
perPage: perPage,
},
});
}
// 查看视图
export function getViewInfo({
previewId,
page = 1,
perPage = 20,
filter = [],
}) {
return post({
url: "/api/v1/preview/info",
data: {
preview_id: previewId,
page,
per_page: perPage,
filter,
},
});
}
// 删除视图
export function deleteView({ previewId }) {
return post({
url: "/api/v1/preview/del",
data: {
preview_id: previewId,
},
});
}

View File

@@ -0,0 +1,15 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "src/**/*.tsx", "components.d.ts", "*.d.ts"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"target": "esnext"
}
}

View File

@@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

View File

@@ -0,0 +1,19 @@
{
"extends": "@tsconfig/node20/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
"compilerOptions": {
"composite": true,
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}