feat: 新增配置管理和视图管理

This commit is contained in:
sy2084 2024-07-15 11:54:32 +08:00
parent aeba86bb91
commit 8f9cef2a90
36 changed files with 2322 additions and 81 deletions

6
.env.development Normal file
View File

@ -0,0 +1,6 @@
# .env.development
VITE_NODE_ENV = pre
VITE_OA_BASEURL = https://oa-pre.shiyue.com
VITE_YCODE_BASEURL = https://custom-chart-pre-api.shiyue.com

View File

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

View File

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

18
components.d.ts vendored
View File

@ -9,12 +9,30 @@ 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']
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
AEmpty: typeof import('ant-design-vue/es')['Empty']
AFloatButton: typeof import('ant-design-vue/es')['FloatButton']
AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem']
AInput: typeof import('ant-design-vue/es')['Input']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
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']
ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASpace: typeof import('ant-design-vue/es')['Space']
ASwitch: typeof import('ant-design-vue/es')['Switch']
ATable: typeof import('ant-design-vue/es')['Table']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Table: typeof import('./src/components/common/table.vue')['default']
YTable: typeof import('./src/components/common/y-table.vue')['default']
}
}

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
<title>悦码后台</title>
</head>
<body>
<div id="app"></div>

6
package-lock.json generated
View File

@ -8,7 +8,7 @@
"name": "y-code",
"version": "0.0.0",
"dependencies": {
"@vueuse/core": "^10.9.0",
"@vueuse/core": "^10.11.0",
"ant-design-vue": "^4.1.2",
"axios": "^1.6.7",
"pinia": "^2.1.7",
@ -1944,7 +1944,7 @@
},
"node_modules/@vueuse/core": {
"version": "10.11.0",
"resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-10.11.0.tgz",
"resolved": "http://sy-registry.shiyue.com/@vueuse/core/-/core-10.11.0.tgz",
"integrity": "sha512-x3sD4Mkm7PJ+pcq3HX8PLPBadXCAlSDR/waK87dz0gQE+qJnaaFhc/dZVfJz+IUYzTMVGum2QlR7ImiJQN4s6g==",
"dependencies": {
"@types/web-bluetooth": "^0.0.20",
@ -6143,7 +6143,7 @@
},
"@vueuse/core": {
"version": "10.11.0",
"resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-10.11.0.tgz",
"resolved": "http://sy-registry.shiyue.com/@vueuse/core/-/core-10.11.0.tgz",
"integrity": "sha512-x3sD4Mkm7PJ+pcq3HX8PLPBadXCAlSDR/waK87dz0gQE+qJnaaFhc/dZVfJz+IUYzTMVGum2QlR7ImiJQN4s6g==",
"requires": {
"@types/web-bluetooth": "^0.0.20",

View File

@ -11,7 +11,7 @@
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
},
"dependencies": {
"@vueuse/core": "^10.9.0",
"@vueuse/core": "^10.11.0",
"ant-design-vue": "^4.1.2",
"axios": "^1.6.7",
"pinia": "^2.1.7",

28
src/api/preview/index.js Normal file
View File

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

View File

@ -0,0 +1,136 @@
<template>
<div class="y-table-container">
<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"
:options="item.options"
v-model:value="filterData[item.name]"
@change="toFilt"
></a-select>
<a-input
v-if="item.type === 'text'"
class="input-item"
placeholder="请输入"
v-model:value="filterData[item.name]"
@change="toFilt"
/>
</div>
</div>
<div class="y-table-content">
<a-table
:columns="columnConfig"
:data-source="dataList"
:pagination="false"
size="small"
bordered
></a-table>
<a-pagination
v-model:current="pageState.page"
:total="total"
:page-size="pageState.perPage"
:hide-on-single-page="false"
size="small"
class="pagination-box"
@change="pageChange"
/>
</div>
</div>
</template>
<script setup>
import { reactive, ref, watch } from "vue";
import { useDebounceFn } from "@vueuse/core";
const props = defineProps({
filterConfig: {
type: Array,
default: () => [],
},
columnConfig: {
type: Array,
default: () => [],
},
dataList: {
type: Array,
default: () => [],
},
total: {
type: Number,
default: 0,
},
});
const emit = defineEmits(["handleFilt"]);
const filterData = ref({});
// const filterConfig = ref([]);
// const columnConfig = ref([]);
// const dataList = ref([]);
const pageState = reactive({
page: 1,
perPage: 20,
});
watch(
() => props.filterConfig,
(newVal) => {
console.log("newVal", newVal);
newVal.forEach((item) => {
filterData.value[item.name] = undefined;
});
for (let i = 0; i < newVal.length; i++) {
console.log(
"props.filterConfig",
newVal[i].label,
newVal[i].name,
newVal[i].type
);
}
}
);
const getData = () => {
emit("toFilt", {
filter: filterData.value,
page: pageState.page,
perPage: pageState.perPage,
});
};
const toFilt = useDebounceFn(() => {
getData();
}, 500);
const pageChange = () => {
getData();
};
</script>
<style lang="less" scoped>
.y-table-filter {
display: flex;
flex-wrap: wrap;
}
.filter-item {
margin-right: 10px;
margin-bottom: 6px;
}
.input-item {
width: 180px;
}
.y-table-content {
margin-top: 10px;
}
.pagination-box {
text-align: center;
}
</style>

View File

@ -1,19 +1,21 @@
<script setup lang="ts">
import { useRoute } from 'vue-router';
import { HomeOutlined, FullscreenOutlined } from '@ant-design/icons-vue';
import { useUserInfoStore } from '@/stores/useUserInfoStore';
import avatar from '@/assets/avatar.png';
import { OA_BASEURL } from '@/utils/request';
import { logout } from '@/api/common';
import { useRoute } from "vue-router";
import { HomeOutlined, FullscreenOutlined } from "@ant-design/icons-vue";
import { useUserInfoStore } from "@/stores/useUserInfoStore";
import avatar from "@/assets/avatar.png";
import { YCODE_BASEURL } from "@/utils/request";
import { logout } from "@/api/common";
const emits = defineEmits(['requestFullscreen']);
const emits = defineEmits(["requestFullscreen"]);
const route = useRoute();
const userInfoStore = useUserInfoStore();
const handleLogout = () => {
logout().then(() => {
window.location.href = `${OA_BASEURL}/login?redirect=${encodeURIComponent(window.location.href)}`;
window.location.href = `${YCODE_BASEURL}/login?redirect=${encodeURIComponent(
window.location.href
)}`;
});
};
</script>
@ -31,12 +33,10 @@ const handleLogout = () => {
<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;">
<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>{{ userInfoStore.userInfo?.alias || "-" }}</div>
</div>
<template #overlay>
@ -45,9 +45,7 @@ const handleLogout = () => {
<span>退出登录</span>
</a-menu-item>
<a-menu-item>
<a class="back-oa" :href="`${OA_BASEURL}/front/`">
返回OA
</a>
<a class="back-oa" :href="`${OA_BASEURL}/front/`"> 返回OA </a>
</a-menu-item>
</a-menu>
</template>
@ -63,7 +61,7 @@ const handleLogout = () => {
align-items: center;
padding: 0 30px;
background-color: #fff;
box-shadow: 0 0 12px 0 rgba(0,0,0,.08);
box-shadow: 0 0 12px 0 rgba(0, 0, 0, 0.08);
.user-area {
display: flex;
align-items: center;

View File

@ -1,21 +1,25 @@
<script setup lang="ts">
import { useUserInfoStore } from '@/stores/useUserInfoStore';
import { 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 { useUserInfoStore } from "@/stores/useUserInfoStore";
import { 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";
const userInfoStore = useUserInfoStore();
// const userInfoStore = useUserInfoStore();
const isCollapsed = ref(false);
const isFullscreen = ref(false);
const container = ref<HTMLDivElement>();
onMounted(() => {
userInfoStore.fetchUserInfo();
// userInfoStore.fetchUserInfo();
});
useEventListener(window, 'fullscreenchange', () => {
useEventListener(window, "fullscreenchange", () => {
isFullscreen.value = !!document.fullscreenElement;
});
@ -31,17 +35,32 @@ const handleExitFullscreen = () => {
<template>
<section class="root">
<header class="header">
<Header @requestFullscreen="handleFullscreen" />
</header>
<section class="left-aside" :class="{ 'left-aside-collapsed': isCollapsed }">
<section
class="left-aside"
:class="{ 'left-aside-collapsed': isCollapsed }"
>
<Sider :inlineCollapsed="isCollapsed" />
<div class="collapsed-icon">
<component :is="isCollapsed ? MenuUnfoldOutlined : MenuFoldOutlined" @click="isCollapsed = !isCollapsed" />
<component
:is="isCollapsed ? MenuUnfoldOutlined : MenuFoldOutlined"
@click="isCollapsed = !isCollapsed"
/>
</div>
</section>
<section class="container" :class="{ 'container-fullscreen': isFullscreen, 'container-collapsed': isCollapsed }" ref="container">
<router-view />
<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 />
@ -62,21 +81,18 @@ const handleExitFullscreen = () => {
.header {
height: @header-height;
position: fixed;
width: 100%;
width: calc(100% - 220px);
z-index: 1;
top: 0;
}
.left-aside {
width: @aside-width;
position: fixed;
top: @header-height;
left: 0;
z-index: 1;
height: calc(100% - @header-height - @header-margin);
height: 100vh;
overflow-y: hidden;
margin-top: @header-margin;
border-radius: 8px;
box-shadow: 0 0 12px 0 rgba(0,0,0,.08);
box-shadow: 0 0 12px 0 rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
background-color: #fff;
@ -90,7 +106,7 @@ const handleExitFullscreen = () => {
}
}
.container {
padding-left: @aside-width + 8px;
padding-left: @aside-width;
padding-top: @header-height + @header-margin;
height: calc(100% - @header-height - @header-margin);
position: relative;
@ -98,6 +114,9 @@ const handleExitFullscreen = () => {
transition: padding 0.3s cubic-bezier(0.2, 0, 0, 1) 0s;
margin-right: 8px;
}
.i-container {
padding: 0 8px 8px 8px;
}
.container-collapsed {
padding-left: @aside-width-collapsed + 8px;
}
@ -105,7 +124,10 @@ const handleExitFullscreen = () => {
padding: 0;
height: 100%;
}
:deep(:where(.css-dev-only-do-not-override-1hsjdkk).ant-menu-inline-collapsed), .left-aside-collapsed {
:deep(
:where(.css-dev-only-do-not-override-1hsjdkk).ant-menu-inline-collapsed
),
.left-aside-collapsed {
width: @aside-width-collapsed;
}
}

View File

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

View File

@ -28,41 +28,57 @@ const routeList: RouteType[] = [
name: '-',
meta: {},
children: [],
redirect: '/flow-manager/list',
redirect: '/config-manage/project-cfg',
},
{
path: '/flow-manager',
name: 'flow-manager',
path: '/config-manage',
name: 'config-manage',
isMenu: true,
meta: { title: '机制管理' },
meta: { title: '配置管理' },
icon: () => h(HomeOutlined),
children: [
{
path: 'list',
name: 'list',
component: () => import('@/views/flow-manager/list/index.vue'),
meta: { title: '机制列表' },
path: 'project-cfg',
name: 'project-cfg',
component: () => import('@/views/config-manage/project-cfg/index.vue'),
meta: { title: '项目配置' },
isMenu: true,
children: [],
},
{
path: 'create',
name: 'create',
component: () => import('@/views/flow-manager/create/index.vue'),
meta: { title: '机制创建' },
path: 'module-cfg',
name: 'module-cfg',
component: () => import('@/views/config-manage/module-cfg/index.vue'),
meta: { title: '数据表配置' },
isMenu: true,
children: [],
},
],
},
{
path: '/data-overview',
name: 'data-overview',
path: '/view-all-manage',
name: 'view-all-manage',
isMenu: true,
meta: { title: '数据总览' },
children: [],
meta: { title: '视图管理' },
icon: () => h(BarChartOutlined),
component: () => import('@/views/data-overview/index.vue'),
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: [],
},
],
},
],
},

View File

@ -8,10 +8,10 @@ export interface ResopnseType<T> {
data: T
ts: string
}
export const OA_BASEURL: string = import.meta.env.VITE_OA_BASEURL;
export const YCODE_BASEURL: string = import.meta.env.VITE_YCODE_BASEURL;
const requestType = {
base: OA_BASEURL,
base: YCODE_BASEURL,
};
const baseAxios: AxiosInstance = axios.create({
@ -26,7 +26,7 @@ const errorHandle = (error: AxiosError) => {
switch (status) {
case 401:
message.warning('请先登录');
window.location.href = `${OA_BASEURL}/login?redirect=${encodeURIComponent(window.location.href)}`;
window.location.href = `${YCODE_BASEURL}/login?redirect=${encodeURIComponent(window.location.href)}`;
break;
case 403:
message.warning('权限不足');

View File

@ -0,0 +1,132 @@
<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"
/>
</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="show_type_id">
<a-select
placeholder="请选择展示类型"
v-model:value="formData.show_type_id"
:options="showTypes"
/>
</a-form-item>
<a-form-item label="sql数据源" name="original_sql">
<a-input
placeholder="请输入sql数据源"
v-model:value="formData.original_sql"
/>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup>
import { onMounted, ref, watch } from "vue";
import { getShowTypeSelect } 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" },
],
show_type_id: [{ required: true, message: "请选择", trigger: "submit" }],
original_sql: [{ required: true, message: "请输入", trigger: "submit" }],
});
const showTypes = ref([]);
const formRef = ref();
const formData = ref({
project_id: undefined,
modular_name: undefined,
is_show: 0,
show_type_id: undefined,
original_sql: 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,
show_type_id: newVal.show_type_id,
original_sql: newVal.original_sql,
};
}
}
);
onMounted(() => {
toGetShowType();
});
const toGetShowType = () => {
getShowTypeSelect().then((res) => {
showTypes.value = res.data;
});
};
const resetFormData = () => {
formData.value = {
project_id: undefined,
modular_name: undefined,
is_show: 0,
show_type_id: undefined,
original_sql: undefined,
};
};
const handleOk = () => {
formRef.value.validate().then(() => {
emit("ok", formData.value);
});
};
</script>

View File

@ -0,0 +1,233 @@
<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]"
placeholder="请输入"
/>
<template v-else>
{{ record[column.dataIndex] }}
</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="请选择"
style="width: 160px"
>
</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 === '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>
<a-button v-else type="link" @click="handleEdit(record)"
>修改</a-button
>
</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>
</template>
<script setup>
import { onMounted, reactive, ref, watch } from "vue";
import { viewCfgCols } from "@/views/config-manage/module-cfg/config";
import {
getFieldTypeSelect,
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 pageState = reactive({
page: 1,
perPage: 20,
total: 0,
});
const editableData = reactive({});
watch(
() => props.open,
(newVal) => {
if (newVal) {
toGetList();
}
}
);
onMounted(() => {
toGetFieldTypes();
});
//
const toGetFieldTypes = () => {
getFieldTypeSelect().then((res) => {
fieldTypeSel.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,
is_search: 0,
field_type_id: undefined,
belong_to_table: undefined,
original_sql: undefined,
sort: 0,
};
dataList.value.unshift(item);
editableData[item.field_id] = {
...item,
};
};
const handleEdit = (record) => {
editableData[record.field_id] = {
...record,
};
};
const handleCancel = (record) => {
if (typeof record.field_id === "string") {
dataList.value.shift();
} else {
delete editableData[record.field_id];
}
};
const handleSave = (record) => {
const params = {
field_title: record.field_title,
field_name: record.field_name,
is_search: record.is_search,
field_type_id: record.field_type_id,
belong_to_table: record.belong_to_table,
original_sql: record.original_sql,
modular_id: props.modularId,
};
if (typeof params.field_id === "string") {
//
delete params.field_id;
} else {
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;
}
</style>

View File

@ -0,0 +1,20 @@
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: 'show_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_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,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="80%"
: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;
}
.ant-table-wrapper {
margin-bottom: 10px;
}
.pagination-box {
text-align: center;
}
</style>

View File

@ -0,0 +1,108 @@
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 getShowTypeSelect() {
return get({
url: `/api/v1/modular/get-show-type-drop`,
});
}
// 字段类型下拉
export function getFieldTypeSelect() {
return get({
url: `/api/v1/field/get-field-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,176 @@
<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="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
placeholder="请输入数据库密码"
v-model:value="formData.database_password"
/>
<a-button type="primary" @click="toCheckDbConnect"
>检测数据库连接</a-button
>
</a-space>
</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" },
],
database_address: [
{ required: true, message: "请输入数据库地址", trigger: "submit" },
],
database_port: [
{ required: true, message: "请输入数据库端口", trigger: "submit" },
],
database_name: [
{ required: true, message: "请输入数据库名称", trigger: "submit" },
],
database_username: [
{ required: true, message: "请输入数据库用户", trigger: "submit" },
],
database_password: [
{ required: true, message: "请输入数据库密码", trigger: "submit" },
],
};
const formRef = ref();
const formData = ref({
project_name: undefined,
is_show: 0,
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 = 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,
}).then((res) => {
message.success(res.message);
});
}
};
const validateConnect = () => {
const fields = {
database_name: "请输入数据库名称",
database_port: "请输入数据库端口",
database_address: "请输入数据库地址",
database_username: "请输入数据库用户",
database_password: "请输入数据库密码",
};
for (const key in fields) {
if (!formData.value[key]) {
message.error(fields[key]);
return false;
}
}
return true;
};
const resetFormData = () => {
formData.value = {
project_name: undefined,
is_show: 0,
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,8 @@
export const projectCfgCols = [
{ dataIndex: 'project_id', title: '编号', align: 'center'},
{ dataIndex: 'project_name', 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,200 @@
<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;
}
.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;
}
.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

@ -1,3 +0,0 @@
<template>
data
</template>

View File

@ -1,3 +0,0 @@
<template>
create
</template>

View File

@ -1,8 +0,0 @@
<template>
<div class="normal-container">
111
</div>
<div class="normal-container mt-16">
222
</div>
</template>

View File

@ -0,0 +1,268 @@
<template>
<div class="normal-container">
<div class="view-create-box">
<div class="left-box">
<a-form :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
<a-form-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-checkbox-group v-if="fieldList.length" 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-empty v-else description="暂无字段数据" />
</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-filter">
<div
v-for="(item, index) 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="请选择"
@change="toFilt"
></a-select>
<a-input
v-else
class="input-item"
placeholder="请输入"
v-model:value="previewData.filterData[item.name]"
@change="
(e) => {
toFilt(item.name, e.target.value);
}
"
/>
</div>
</div>
<div class="y-table-content">
<a-table
:columns="previewData.columnConfig"
:data-source="previewData.dataList"
:pagination="false"
size="small"
bordered
></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>
</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 } from "./service";
import { message } from "ant-design-vue";
const projectSel = ref([]);
const modularSel = ref([]);
const fieldList = ref([]);
const projectId = ref();
const modularId = ref();
const fieldIds = ref([]);
const previewLoading = ref(false);
const nameVisible = ref(false);
const previewName = ref();
const previewData = reactive({
type: "",
filterConfig: [], //
columnConfig: [], //
dataList: [], //
filterData: {},
page: 1,
perPage: 20,
total: 0,
});
onMounted(() => {
toGetProModularField();
});
const toGetProModularField = () => {
getProModularField().then((res) => {
projectSel.value = res.data;
});
};
const onProjectChange = (val) => {
const target = projectSel.value.find((item) => item.value === val);
modularSel.value = target.child;
modularId.value = undefined;
fieldList.value = [];
fieldIds.value = [];
};
const onModularChange = (val) => {
const target = modularSel.value.find((item) => item.value === val);
fieldList.value = target.child;
};
const toPreview = () => {
previewLoading.value = true;
const filter = previewData.filterConfig.map((item) => {
return {
name: item.name,
type: item.type,
value: previewData.filterData[item.name],
};
});
preview({
modularId: modularId.value,
fieldIds: fieldIds.value.toString(),
page: previewData.page,
perPage: previewData.perPage,
filter,
})
.then((res) => {
previewData.type = res.data.type;
if (res.data.type === "table") {
previewData.filterConfig = res.data.filter;
previewData.filterConfig.forEach((item) => {
previewData.filterData[item.name] = previewData.filterData[item.name]
? previewData.filterData[item.name]
: undefined;
});
console.log("filterData", previewData.filterData);
previewData.columnConfig = res.data.header;
previewData.dataList = res.data.data;
previewData.total = res.data.count;
}
})
.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,
}).then(() => {
message.success("保存成功");
nameVisible.value = false;
});
};
const toFilt = (key, value) => {
previewData.filterData[key] = value;
previewData.page = 1;
toPreview();
};
</script>
<style lang="less" scoped>
.normal-container {
height: calc(100vh - 120px);
}
.view-create-box {
display: flex;
height: 100%;
}
.left-box {
width: 400px;
height: calc(100% - 20px);
padding: 10px;
border-right: 1px solid #ddd;
.footer {
text-align: right;
.preview-btn {
margin-right: 10px;
}
}
}
.right-box {
padding: 10px;
flex-grow: 1;
overflow: auto;
}
.y-table-filter {
display: flex;
flex-wrap: wrap;
}
.filter-item {
margin-right: 10px;
margin-bottom: 6px;
}
.input-item {
width: 180px;
}
.y-table-content {
margin-top: 10px;
}
.pagination-box {
text-align: center;
}
</style>

View File

@ -0,0 +1,34 @@
import { get, post } from "@/utils/request";
// 项目-表-字段下拉
export function getProModularField() {
return get({
url: "/api/v1/field/get-project-modular-field-drop",
});
}
// 预览
export function preview({ modularId, fieldIds, page, perPage, filter }) {
return post({
url: "api/v1/preview/view",
data: {
modular_id: modularId,
field_ids: fieldIds,
page,
per_page: perPage,
filter,
},
});
}
// 点击保存
export function saveView({ modularId, fieldIds, previewName }) {
return post({
url: "api/v1/preview/save",
data: {
modular_id: modularId,
field_ids: fieldIds,
preview_name: previewName,
},
});
}

View File

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

View File

@ -0,0 +1,165 @@
<template>
<div class="normal-container">
<div class="view-list-box">
<div class="left-box">
<a-form :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
<a-form-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
></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'"
:filterConfig="selectViewInfo.filter"
:dataList="selectViewInfo.data"
:columnConfig="selectViewInfo.header"
:total="selectViewInfo.count"
@toFilt="
(params) => {
toGetViewInfo(params);
}
"
/>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, ref, reactive } from "vue";
import { getProModular, getViewList, getViewInfo } from "./service";
import { viewListCols } from "./config";
import yTable from "@/components/common/y-table.vue";
const projectSel = ref([]);
const modularSel = ref([]);
const projectId = ref();
const modularId = ref();
const dataList = ref([]);
const selectedRowId = ref();
const selectViewInfo = ref({
type: "",
});
const pageState = reactive({
page: 1,
perPage: 20,
total: 0,
});
onMounted(() => {
toGetProModular();
});
const toGetProModular = () => {
getProModular().then((res) => {
projectSel.value = res.data;
});
};
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();
};
</script>
<style lang="less" scoped>
.normal-container {
height: calc(100vh - 120px);
}
.view-list-box {
display: flex;
height: 100%;
}
.left-box {
width: 400px;
height: calc(100% - 20px);
padding: 10px;
border-right: 1px solid #ddd;
}
.selected-row {
background-color: beige;
}
.pagination-box {
text-align: center;
margin-top: 10px;
}
.right-box {
padding: 10px;
flex-grow: 1;
overflow: auto;
}
</style>

View File

@ -0,0 +1,38 @@
import { get } 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 get({
url: "/api/v1/preview/info",
params: {
preview_id: previewId,
page,
perPage: perPage,
filter,
},
});
}

View File

@ -4,6 +4,7 @@ import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import Components from 'unplugin-vue-components/vite';
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
import { server } from 'typescript';
// https://vitejs.dev/config/
export default defineConfig({
@ -21,4 +22,8 @@ export default defineConfig({
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
hmr: true,
host: '0.0.0.0',
},
});