u
This commit is contained in:
1 parent
4aaa93df7d
commit
c330e52fd3
66 files changed
+7386
-4
No files matched your search
Generated
-1
@@ -6,7 +6,6 @@
|
||||
<sourceOutputDir name="target/generated-sources/annotations" />
|
||||
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
||||
<outputRelativeToContentRoot value="true" />
|
||||
<module name="ai-demo" />
|
||||
</profile>
|
||||
</annotationProcessing>
|
||||
</component>
|
||||
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+4
-3
@@ -21,9 +21,9 @@
|
||||
</MavenGeneralSettings>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectColorInfo"><![CDATA[{
|
||||
"associatedIndex": 0
|
||||
}]]></component>
|
||||
<component name="ProjectColorInfo">{
|
||||
"associatedIndex": 0
|
||||
}</component>
|
||||
<component name="ProjectId" id="3B4WeoSJ6hbOtn69mZjcFoJWHtB" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
@@ -34,6 +34,7 @@
|
||||
"Application.Main.executor": "Run",
|
||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"git-widget-placeholder": "master",
|
||||
"kotlin-language-version-configured": "true",
|
||||
"onboarding.tips.debug.path": "D:/workspace/code/ai-demo/src/main/java/org/example/Main.java",
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
Chrome >= 49
|
||||
Firefox >= 45
|
||||
Safari >= 10
|
||||
Edge >= 15
|
||||
iOS >= 9
|
||||
Android >= 4.4
|
||||
@@ -0,0 +1,13 @@
|
||||
# Application
|
||||
# 应用名称(用于标题、日志等)
|
||||
PUBLIC_APP_NAME=g3hy2026
|
||||
|
||||
# Server
|
||||
# 应用部署基础路径(路由和静态资源 base)
|
||||
PUBLIC_SERVER_PATH=/g3hy2026/
|
||||
# 本地开发服务器端口
|
||||
PUBLIC_SERVER_PORT=3000
|
||||
|
||||
# API
|
||||
# 后端 API 基础地址
|
||||
PUBLIC_API_BASE_URL=http://127.0.0.1:8084/api
|
||||
@@ -0,0 +1,22 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"editor.cursorSmoothCaretAnimation": "on",
|
||||
"oxc.fmt.configPath": ".oxfmtrc.json",
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode",
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"Print Vue3 SFC": {
|
||||
"scope": "vue",
|
||||
"prefix": "v3",
|
||||
"body": [
|
||||
"<script lang=\"js\" setup>",
|
||||
"",
|
||||
"</script>",
|
||||
"",
|
||||
"<template>",
|
||||
" <div class=\"\"></div>",
|
||||
"</template>",
|
||||
"",
|
||||
"<style lang=\"css\" scoped>",
|
||||
"",
|
||||
"</style>",
|
||||
],
|
||||
"description": "生成 Vue3 SFC 文件(无注释)",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
# Rsbuild project
|
||||
|
||||
## Setup
|
||||
|
||||
Install the dependencies:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## Get started
|
||||
|
||||
Start the dev server, and the app will be available at [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
Build the app for production:
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
Preview the production build locally:
|
||||
|
||||
```bash
|
||||
pnpm run preview
|
||||
```
|
||||
|
||||
## Learn more
|
||||
|
||||
To learn more about Rsbuild, check out the following resources:
|
||||
|
||||
- [Rsbuild documentation](https://rsbuild.rs) - explore Rsbuild features and APIs.
|
||||
- [Rsbuild GitHub repository](https://github.com/web-infra-dev/rsbuild) - your feedback and contributions are welcome!
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// biome-ignore lint: disable
|
||||
// oxlint-disable
|
||||
// ------
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSplitter: typeof import('element-plus/es')['ElSplitter']
|
||||
ElSplitterPanel: typeof import('element-plus/es')['ElSplitterPanel']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElText: typeof import('element-plus/es')['ElText']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
ElTree: typeof import('element-plus/es')['ElTree']
|
||||
G3Column: typeof import('./src/components/g3-table/g3-column.vue')['default']
|
||||
G3EditTable: typeof import('./src/components/g3-table/G3EditTable.vue')['default']
|
||||
G3Grid: typeof import('./src/components/g3-grid/g3-grid.vue')['default']
|
||||
G3GridGroup: typeof import('./src/components/g3-grid/g3-grid-group.vue')['default']
|
||||
G3GridItem: typeof import('./src/components/g3-grid/g3-grid-item.vue')['default']
|
||||
G3Icon: typeof import('./src/components/g3-icon/index.vue')['default']
|
||||
G3RecursiveColumn: typeof import('./src/components/g3-table/G3RecursiveColumn.vue')['default']
|
||||
G3Select: typeof import('./src/components/g3-select/index.vue')['default']
|
||||
G3TableView: typeof import('./src/components/g3-table/g3-table-view.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
<script>
|
||||
// IE11 检测
|
||||
if (!!window.MSInputMethodContext && !!document.documentMode) {
|
||||
document.write(
|
||||
'<div style="display:flex;justify-content:center;align-items:center;height:100vh;font-size:20px;">' +
|
||||
'您的浏览器版本过低,请使用 Chrome / Edge / Firefox 浏览器访问' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "rsbuild-project",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "rsbuild build",
|
||||
"dev": "rsbuild dev --open",
|
||||
"preview": "rsbuild preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify/vue": "^5.0.0",
|
||||
"axios": "^1.13.6",
|
||||
"element-plus": "^2.13.3",
|
||||
"lodash": "^4.17.23",
|
||||
"mitt": "^3.0.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^3.0.4",
|
||||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
"unplugin-auto-import": "^21.0.0",
|
||||
"unplugin-vue-components": "^31.0.0",
|
||||
"uuid": "^13.0.0",
|
||||
"vue": "^3.5.26",
|
||||
"vue-router": "^5.0.3",
|
||||
"vue3-puzzle-vcode": "^1.1.7",
|
||||
"vxe-pc-ui": "4.13.0",
|
||||
"vxe-table": "4.17.49",
|
||||
"xe-utils": "3.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/json": "^2.2.446",
|
||||
"@rsbuild/core": "^1.7.1",
|
||||
"@rsbuild/plugin-babel": "^1.1.2",
|
||||
"@rsbuild/plugin-check-syntax": "^1.6.1",
|
||||
"@rsbuild/plugin-sass": "^1.5.1",
|
||||
"@rsbuild/plugin-vue": "^1.2.2",
|
||||
"@rsbuild/plugin-vue-jsx": "^1.1.2",
|
||||
"oxfmt": "^0.40.0",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"unplugin-icons": "^23.0.1"
|
||||
}
|
||||
}
|
||||
+2979
File diff suppressed because it is too large.
Load diff
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,73 @@
|
||||
// @ts-check
|
||||
import { defineConfig } from "@rsbuild/core";
|
||||
import { pluginVue } from "@rsbuild/plugin-vue";
|
||||
import { pluginSass } from "@rsbuild/plugin-sass";
|
||||
|
||||
import AutoImport from "unplugin-auto-import/rspack";
|
||||
import Components from "unplugin-vue-components/rspack";
|
||||
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
|
||||
import { pluginCheckSyntax } from "@rsbuild/plugin-check-syntax";
|
||||
import { pluginBabel } from "@rsbuild/plugin-babel";
|
||||
import { pluginVueJsx } from "@rsbuild/plugin-vue-jsx";
|
||||
|
||||
import IconsResolver from "unplugin-icons/resolver";
|
||||
import Icons from "unplugin-icons/rspack";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
pluginBabel({
|
||||
include: /\.[jt]sx?$/,
|
||||
}),
|
||||
pluginVue(),
|
||||
pluginVueJsx(),
|
||||
pluginSass(),
|
||||
pluginCheckSyntax({
|
||||
ecmaVersion: 2015,
|
||||
}),
|
||||
],
|
||||
tools: {
|
||||
rspack: {
|
||||
plugins: [
|
||||
Icons({
|
||||
compiler: "vue3",
|
||||
autoInstall: true,
|
||||
}),
|
||||
|
||||
AutoImport({
|
||||
imports: ["vue", "vue-router"],
|
||||
resolvers: [ElementPlusResolver()],
|
||||
}),
|
||||
|
||||
Components({
|
||||
dirs: ['src/components'],
|
||||
dts: true,
|
||||
deep: true,
|
||||
resolvers: [ElementPlusResolver(), IconsResolver()],
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
source: {
|
||||
include: [{ not: /[\\/]core-js[\\/]/ }],
|
||||
entry: {
|
||||
index: "./src/main.js",
|
||||
},
|
||||
tsconfigPath: "./jsconfig.json",
|
||||
},
|
||||
output: {
|
||||
polyfill: "usage",
|
||||
},
|
||||
html: {
|
||||
template: "./index.html",
|
||||
mountId: "app",
|
||||
meta: {
|
||||
viewport: false,
|
||||
},
|
||||
favicon: "./public/logo.png",
|
||||
title: process.env.PUBLIC_APP_NAME,
|
||||
},
|
||||
server: {
|
||||
base: process.env.PUBLIC_SERVER_PATH || "/",
|
||||
port: Number(process.env.PUBLIC_SERVER_PORT) || 3000,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script lang="js" setup></script>
|
||||
|
||||
<style scoped></style>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 323 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,47 @@
|
||||
<script setup lang="js">
|
||||
const props = defineProps({
|
||||
column: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
width: {
|
||||
type: [String, Number],
|
||||
},
|
||||
border: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
labelWidth: {
|
||||
type: [String, Number],
|
||||
},
|
||||
});
|
||||
|
||||
const gridStyle = computed(() => ({
|
||||
width: props.width
|
||||
? typeof props.width === "number"
|
||||
? props.width + "px"
|
||||
: props.width
|
||||
: "100%",
|
||||
gridTemplateColumns: `repeat(${props.column}, 1fr)`,
|
||||
}));
|
||||
|
||||
// 传递给子组件
|
||||
provide("G3GridGroupConfig", toRefs(props));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="g3-grid-group" :class="{ border: props.border }" :style="gridStyle">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.g3-grid-group {
|
||||
display: grid;
|
||||
|
||||
&.border {
|
||||
border-right: 1px solid #ccc;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,223 @@
|
||||
<script setup lang="js">
|
||||
// 子组件
|
||||
const props = defineProps({
|
||||
border: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
labelWidth: {
|
||||
type: [String, Number],
|
||||
default: undefined,
|
||||
},
|
||||
height: {
|
||||
type: [String, Number],
|
||||
},
|
||||
span: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
},
|
||||
labelAlign: {
|
||||
type: String,
|
||||
default: "right",
|
||||
validator: (v) => ["left", "center", "right"].includes(v),
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const parentConfig = inject("G3GridGroupConfig", {});
|
||||
|
||||
const mergedBorder = computed(() => {
|
||||
const val = props.border !== undefined ? props.border : parentConfig.border?.value;
|
||||
|
||||
return val;
|
||||
});
|
||||
|
||||
const gridItemStyle = computed(() => {
|
||||
const style = {
|
||||
gridColumn: `span ${props.span}`,
|
||||
};
|
||||
|
||||
if (props.height !== undefined && props.height !== null) {
|
||||
style.height = typeof props.height === "number" ? props.height + "px" : props.height;
|
||||
}
|
||||
|
||||
return style;
|
||||
});
|
||||
|
||||
const gridLabelStyle = computed(() => {
|
||||
const val = props.labelWidth ?? parentConfig.labelWidth?.value;
|
||||
const alignVal = props.labelAlign ?? parentConfig.labelAlign?.value ?? "right";
|
||||
|
||||
const justifyMap = {
|
||||
left: "flex-start",
|
||||
center: "center",
|
||||
right: "flex-end",
|
||||
};
|
||||
|
||||
return {
|
||||
flex: `0 0 ${typeof val === "number" ? val + "px" : val}`,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: justifyMap[alignVal],
|
||||
};
|
||||
});
|
||||
|
||||
const requiredStyle = computed(() => {
|
||||
const alignVal = props.labelAlign ?? parentConfig.labelAlign?.value ?? "right";
|
||||
|
||||
return alignVal === "right" ? { flexDirection: "row-reverse" } : {};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="g3-grid-item" :class="{ border: mergedBorder }" :style="gridItemStyle">
|
||||
<!-- label -->
|
||||
<div
|
||||
class="label"
|
||||
:class="{ required, 'is-slot': $slots.label }"
|
||||
:style="[gridLabelStyle, requiredStyle]"
|
||||
>
|
||||
<span class="label-inner">
|
||||
<slot name="label">
|
||||
{{ label }}
|
||||
</slot>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- content -->
|
||||
<div class="content">
|
||||
<slot>
|
||||
{{ content }}
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.g3-grid-item {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
&.border {
|
||||
border-top: 1px solid #ccc;
|
||||
border-left: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.label {
|
||||
background-color: #f9f9f9;
|
||||
border-right: 1px solid #ccc;
|
||||
font-size: 14px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
|
||||
&.required::before {
|
||||
content: "*";
|
||||
color: #f56c6c;
|
||||
margin-right: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.is-slot .label-inner {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.label-inner {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
padding: 0px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
background-color: #fff;
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.el-input) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.el-input__wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
transition: box-shadow 0.15s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: inset 0 0 0 1px var(--el-color-primary-light-7);
|
||||
}
|
||||
|
||||
&.is-focus {
|
||||
box-shadow: inset 0 0 0 1px var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-select) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
.el-select__wrapper {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
border-radius: 0;
|
||||
min-height: 0px;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
transition: box-shadow 0.15s;
|
||||
&.is-hovering {
|
||||
box-shadow: inset 0 0 0 1px var(--el-color-primary-light-7);
|
||||
}
|
||||
|
||||
&.is-focused {
|
||||
box-shadow: inset 0 0 0 1px var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-textarea) {
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
.el-textarea__inner {
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
border-radius: 0px;
|
||||
box-shadow: unset;
|
||||
min-height: 100% !important;
|
||||
z-index: 10;
|
||||
padding: 0px 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
box-shadow: inset 0 0 0 1px var(--el-color-primary-light-7);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: inset 0 0 0 1px var(--el-color-primary);
|
||||
outline: none;
|
||||
z-index: 12;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script setup lang="js">
|
||||
const props = defineProps({
|
||||
direction: {
|
||||
type: String,
|
||||
default: "row",
|
||||
validator: (v) => ["row", "column"].includes(v),
|
||||
},
|
||||
gap: {
|
||||
type: [String, Number],
|
||||
default: 28,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="g3-grid"
|
||||
:class="props.direction === 'row' ? 'row-gap' : 'column-gap'"
|
||||
:style="{
|
||||
'--gap': typeof props.gap === 'number' ? props.gap + 'px' : props.gap,
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "@/styles/mixins.scss" as *;
|
||||
|
||||
.g3-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&.row-gap {
|
||||
flex-direction: row;
|
||||
@include flexGapRow(var(--gap));
|
||||
}
|
||||
|
||||
&.column-gap {
|
||||
flex-direction: column;
|
||||
@include flexGapColumn(var(--gap));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,72 @@
|
||||
<script setup>
|
||||
import { Icon } from "@iconify/vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
size: {
|
||||
type: [Number, String],
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "currentColor",
|
||||
},
|
||||
spin: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["click"]);
|
||||
|
||||
const fontStyle = computed(() => ({
|
||||
...(props.size ? { fontSize: `${props.size}px` } : {}),
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
lineHeight: 1,
|
||||
}));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<i
|
||||
class="g3-icon"
|
||||
:class="[props.spin && 'g3-icon-spin']"
|
||||
:style="fontStyle"
|
||||
@click="(e) => emit('click', e)"
|
||||
>
|
||||
<Icon v-if="props.icon" :icon="props.icon" :color="props.color" class="g3-icon-svg" />
|
||||
<slot />
|
||||
</i>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.g3-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.g3-icon-svg {
|
||||
display: block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.g3-icon-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
--rotate-angle: 360deg;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(var(--rotate-angle));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,368 @@
|
||||
<script lang="js" setup>
|
||||
import { loadPageDataApi } from "@/request/api";
|
||||
import { ClickOutside as vClickOutside } from "element-plus";
|
||||
import { useThrottle } from "@/utils/throttle";
|
||||
|
||||
// ===============================
|
||||
// Props
|
||||
// ===============================
|
||||
const props = defineProps({
|
||||
modelValue: { type: null, default: "" },
|
||||
label: { type: String, default: "" },
|
||||
labelKey: { type: String, default: "b_name" },
|
||||
valueKey: { type: String, default: "b_id" },
|
||||
fitInputWidth: { type: Boolean, default: true },
|
||||
viewName: { type: String, default: "" },
|
||||
searchCondition: { type: String, default: "" },
|
||||
searchColumns: { type: Array, default: () => [] },
|
||||
orderField: { type: String, default: "" },
|
||||
sql: { type: String, default: "" },
|
||||
});
|
||||
|
||||
// ===============================
|
||||
// Emits
|
||||
// ===============================
|
||||
const emits = defineEmits(["update:modelValue", "update:label", "change"]);
|
||||
|
||||
// ===============================
|
||||
// Refs & Reactive
|
||||
// ===============================
|
||||
const innerInput = ref(props.label); // 输入框内部值
|
||||
const inputRef = ref(); // el-input ref
|
||||
const popoverRef = ref(); // el-popover ref
|
||||
const scrollbarRef = ref(); // 滚动条 ref
|
||||
const popoverVisible = ref(false); // 弹窗显示状态
|
||||
const popoverWidth = ref(""); // 弹窗宽度
|
||||
const options = ref([]); // 下拉选项
|
||||
const loading = ref(false); // 加载状态
|
||||
const inputFocus = ref(false); // 输入框焦点状态
|
||||
const highlightIndex = ref(0); // 高亮索引
|
||||
const inputChange = ref(false); // 输入变化标记
|
||||
|
||||
const pagination = reactive({ page: 1, size: 10, total: 0 }); // 分页
|
||||
|
||||
const popperPaneRef = computed(() => popoverRef.value?.popperRef?.contentRef); // Popover 内容 DOM
|
||||
const searchColumns = computed(() => [
|
||||
props.labelKey,
|
||||
props.valueKey,
|
||||
...props.searchColumns,
|
||||
]); // 搜索列
|
||||
|
||||
// ===============================
|
||||
// Watchers
|
||||
// ===============================
|
||||
watch(
|
||||
() => props.label,
|
||||
(newVal) => {
|
||||
innerInput.value = newVal;
|
||||
},
|
||||
);
|
||||
|
||||
// ===============================
|
||||
// Popover 生命周期
|
||||
// ===============================
|
||||
function onPopoverBeforeEnter() {
|
||||
if (props.fitInputWidth)
|
||||
popoverWidth.value = `${inputRef.value.$el.offsetWidth}px`;
|
||||
}
|
||||
|
||||
function onPopoverAfterLeave() {
|
||||
pagination.page = 1;
|
||||
pagination.total = 0;
|
||||
options.value = [];
|
||||
highlightIndex.value = 0;
|
||||
inputChange.value = false;
|
||||
}
|
||||
|
||||
function onPopoverBeforeLeave() {
|
||||
// 弹窗关闭前恢复或清空状态
|
||||
if (props.label) {
|
||||
if (innerInput.value === "") {
|
||||
emits("update:label", "");
|
||||
emits("update:modelValue", "");
|
||||
emits("change", null);
|
||||
} else {
|
||||
innerInput.value = props.label;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// 输入框事件
|
||||
// ===============================
|
||||
function onFocus() {
|
||||
inputFocus.value = true;
|
||||
}
|
||||
function onBlur() {
|
||||
inputFocus.value = false;
|
||||
}
|
||||
|
||||
function onInput(value) {
|
||||
pagination.page = 1;
|
||||
pagination.total = 0;
|
||||
highlightIndex.value = 0;
|
||||
inputChange.value = true;
|
||||
|
||||
fetchOptions(value);
|
||||
}
|
||||
|
||||
function onClick(event) {
|
||||
event.stopPropagation();
|
||||
if (!popoverVisible.value) {
|
||||
inputChange.value = false;
|
||||
fetchOptions("");
|
||||
}
|
||||
}
|
||||
|
||||
function closePopover() {
|
||||
popoverVisible.value = false;
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// 键盘操作
|
||||
// ===============================
|
||||
function onKeydown(e) {
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
moveHighlight(1);
|
||||
} else if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
moveHighlight(-1);
|
||||
} else if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!popoverVisible.value) {
|
||||
inputChange.value = false;
|
||||
fetchOptions("");
|
||||
} else {
|
||||
let index = highlightIndex.value;
|
||||
if (index < 0) index = 0;
|
||||
handleSelectOption(options.value[index]);
|
||||
}
|
||||
} else if (e.key === "Escape") {
|
||||
popoverVisible.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// 高亮与滚动
|
||||
// ===============================
|
||||
function moveHighlight(direction) {
|
||||
const total = options.value.length;
|
||||
if (!total) return;
|
||||
|
||||
let newIndex = highlightIndex.value + direction;
|
||||
if (newIndex < 0) newIndex = total - 1;
|
||||
if (newIndex >= total) newIndex = 0;
|
||||
|
||||
highlightIndex.value = newIndex;
|
||||
scrollToOption(newIndex);
|
||||
}
|
||||
|
||||
function scrollToOption(index) {
|
||||
nextTick(() => {
|
||||
const wrap = scrollbarRef.value?.wrapRef;
|
||||
if (!wrap) return;
|
||||
const opts = wrap.querySelectorAll(".option");
|
||||
const el = opts[index];
|
||||
if (el) el.scrollIntoView({ block: "nearest" });
|
||||
});
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// 数据请求
|
||||
// ===============================
|
||||
function _fetchOptions(searchTitle) {
|
||||
loading.value = true;
|
||||
|
||||
let searchCondition = props.searchCondition || "";
|
||||
if (searchTitle) {
|
||||
const condition = `${props.labelKey} like '%${searchTitle}%'`;
|
||||
searchCondition = searchCondition
|
||||
? `${searchCondition} AND ${condition}`
|
||||
: condition;
|
||||
}
|
||||
|
||||
if (!inputChange.value) searchCondition = "";
|
||||
|
||||
loadPageDataApi({
|
||||
page: pagination.page,
|
||||
size: pagination.size,
|
||||
viewName: props.viewName,
|
||||
searchColumns: searchColumns.value,
|
||||
searchCondition,
|
||||
orderField: props.orderField,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
options.value =
|
||||
pagination.page === 1
|
||||
? res.data.rows
|
||||
: [...options.value, ...res.data.rows];
|
||||
pagination.total = res.data.total;
|
||||
|
||||
popoverVisible.value = true
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
const fetchOptions = useThrottle(_fetchOptions, 300);
|
||||
|
||||
// ===============================
|
||||
// 滚动加载
|
||||
// ===============================
|
||||
function onScrollbarReachEnd() {
|
||||
if (loading.value) return;
|
||||
if (pagination.page * pagination.size >= pagination.total) return;
|
||||
pagination.page += 1;
|
||||
fetchOptions(innerInput.value);
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// 选中操作
|
||||
// ===============================
|
||||
function handleSelectOption(option) {
|
||||
if (!option) return;
|
||||
emits("update:label", option[props.labelKey]);
|
||||
emits("update:modelValue", option[props.valueKey]);
|
||||
emits("change", option);
|
||||
popoverVisible.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-popover
|
||||
ref="popoverRef"
|
||||
trigger="manual"
|
||||
:visible="popoverVisible"
|
||||
placement="bottom"
|
||||
popper-style="padding:0"
|
||||
transition="el-zoom-in-top"
|
||||
:width="popoverWidth"
|
||||
@before-enter="onPopoverBeforeEnter"
|
||||
@before-leave="onPopoverBeforeLeave"
|
||||
@after-leave="onPopoverAfterLeave"
|
||||
>
|
||||
<template #reference>
|
||||
<el-input
|
||||
v-model="innerInput"
|
||||
ref="inputRef"
|
||||
@click.stop="onClick"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@input="onInput"
|
||||
v-click-outside:[popperPaneRef]="closePopover"
|
||||
@keydown="onKeydown"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon
|
||||
class="el-input__icon arrow-icon"
|
||||
:class="{
|
||||
rotate: inputFocus && !loading,
|
||||
spinning: loading,
|
||||
}"
|
||||
>
|
||||
<G3Icon :icon="loading ? 'lucide:loader' : 'ep:arrow-down'" />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</template>
|
||||
|
||||
<div class="option-wrapper" @mousedown.prevent>
|
||||
<div v-if="!loading && options.length === 0" class="empty">
|
||||
<span>暂无数据</span>
|
||||
</div>
|
||||
<template v-else>
|
||||
<el-scrollbar
|
||||
ref="scrollbarRef"
|
||||
max-height="260px"
|
||||
:distance="20"
|
||||
@end-reached="onScrollbarReachEnd"
|
||||
>
|
||||
<div
|
||||
v-for="(item, index) in options"
|
||||
:key="item[props.valueKey]"
|
||||
class="option"
|
||||
:class="{
|
||||
selected: props.modelValue === item[valueKey],
|
||||
highlighted: highlightIndex === index,
|
||||
}"
|
||||
@click="handleSelectOption(item)"
|
||||
>
|
||||
{{ item[labelKey] }}
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
</div>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.arrow-icon {
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
.arrow-icon.rotate {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.arrow-icon.spinning {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.option-wrapper {
|
||||
.loading {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--el-text-color-placeholder);
|
||||
font-size: var(--el-font-size-small);
|
||||
}
|
||||
|
||||
.option {
|
||||
font-size: var(--el-font-size-base);
|
||||
padding: 0 32px 0 20px;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--el-text-color-regular);
|
||||
height: 34px;
|
||||
line-height: 34px;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&.highlighted {
|
||||
background-color: var(--el-fill-color-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="js" setup></script>
|
||||
|
||||
<template>
|
||||
<div class=""></div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="js" setup>
|
||||
defineOptions({
|
||||
name: "G3RecursiveColumn",
|
||||
});
|
||||
|
||||
const isGroup = computed(() => {
|
||||
return Array.isArray(props.column.children) && props.column.children.length > 0;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<vxe-colgroup v-if="isGroup" :title="column.label" :field="column.field">
|
||||
|
||||
</vxe-colgroup>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="js" setup>
|
||||
const props = defineProps({
|
||||
field: Object,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="field && field.col_edit_type">
|
||||
<el-input v-if="field.col_edit_type === 'textarea'" type="textarea" v-bind="$attrs" />
|
||||
<el-date-picker v-else-if="field.col_edit_type === 'date'" type="date" v-bind="$attrs" />
|
||||
<el-date-picker
|
||||
v-else-if="field.col_edit_type === 'datetime'"
|
||||
type="datetime"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
<el-input v-else v-bind="$attrs" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped></style>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="js" setup>
|
||||
const props = defineProps({
|
||||
viewName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const loading = ref(true);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<vxe-table
|
||||
border
|
||||
stripe
|
||||
header-align="center"
|
||||
:column-drag-config="{
|
||||
showIcon: false,
|
||||
trigger: 'cell',
|
||||
}"
|
||||
>
|
||||
</vxe-table>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,109 @@
|
||||
<script setup>
|
||||
import { useTabStore } from "@/store/tab";
|
||||
import emitter from "@/utils/emitter";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
const tabStore = useTabStore();
|
||||
|
||||
const refreshKey = ref(0);
|
||||
|
||||
// keep-alive include keys
|
||||
const includeKeys = computed(() => tabStore.cachedKeys);
|
||||
|
||||
// 缓存 wrapper 组件实例
|
||||
const wrapperMap = new Map();
|
||||
|
||||
// 刷新函数
|
||||
function refresh() {
|
||||
refreshKey.value += 1;
|
||||
}
|
||||
|
||||
// 生成 wrapper 组件,用于 keep-alive
|
||||
function formatComponentInstance(component, fullPath) {
|
||||
if (!component) return null;
|
||||
|
||||
let wrapper = wrapperMap.get(fullPath);
|
||||
|
||||
if (!wrapper) {
|
||||
wrapper = defineComponent({
|
||||
name: fullPath,
|
||||
inheritAttrs: false,
|
||||
setup(_, { attrs }) {
|
||||
return () => h(component, attrs); // 保证 props 响应式
|
||||
},
|
||||
});
|
||||
|
||||
wrapperMap.set(fullPath, wrapper);
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
// watch tabs 变化,自动清理 wrapper
|
||||
watch(
|
||||
() => tabStore.tabs,
|
||||
(tabs) => {
|
||||
const alive = new Set(tabs.map((t) => t.fullPath));
|
||||
for (const key of wrapperMap.keys()) {
|
||||
if (!alive.has(key)) {
|
||||
wrapperMap.delete(key);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// 监听刷新事件
|
||||
onMounted(() => {
|
||||
emitter.on("view.refresh", refresh);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
emitter.off("view.refresh", refresh);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="g3-content">
|
||||
<RouterView v-slot="{ Component, route: childRoute }">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<template v-if="childRoute.meta.keepAlive">
|
||||
<keep-alive :include="includeKeys">
|
||||
<component
|
||||
:is="formatComponentInstance(Component, childRoute.fullPath)"
|
||||
:key="childRoute.fullPath + '?' + refreshKey"
|
||||
/>
|
||||
</keep-alive>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<component :is="Component" :key="childRoute.fullPath + '?' + refreshKey" />
|
||||
</template>
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.g3-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
margin: 6px;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
.fade-enter-to,
|
||||
.fade-leave-from {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<el-sub-menu v-if="hasChildren" :index="menuIndex">
|
||||
<template #title>
|
||||
<el-icon v-if="item.icon">
|
||||
<G3Icon :icon="item.icon" />
|
||||
</el-icon>
|
||||
<span class="menu-label">{{ item.label }}</span>
|
||||
</template>
|
||||
|
||||
<MenuItem
|
||||
v-for="(child, idx) in item.children"
|
||||
:key="child.path || idx"
|
||||
:item="child"
|
||||
:parent-index="menuIndex + '-' + idx"
|
||||
/>
|
||||
</el-sub-menu>
|
||||
|
||||
<el-menu-item
|
||||
v-else
|
||||
:index="item.path"
|
||||
:route="{
|
||||
path: item.path,
|
||||
}"
|
||||
>
|
||||
<el-icon v-if="item.icon">
|
||||
<G3Icon :icon="item.icon" />
|
||||
</el-icon>
|
||||
<span class="menu-label">{{ item.label }}</span>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
item: { type: Object, required: true },
|
||||
parentIndex: { type: String, default: "0" },
|
||||
});
|
||||
|
||||
const hasChildren = props.item.children && props.item.children.length > 0;
|
||||
|
||||
// 父菜单没有 path,用索引生成唯一 index,子菜单 path 就用真实路由
|
||||
const menuIndex = props.item.path || props.parentIndex;
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.menu-label {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,162 @@
|
||||
<script lang="js" setup>
|
||||
import { loadDataApi } from "@/request/api";
|
||||
import MenuItem from "./menu-item.vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
const title = ref("G3后台管理系统");
|
||||
const route = useRoute();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
onMounted(() => {
|
||||
loadDataApi("s_keyvalue", `b_key='bodycaption'`, ``, ["b_value"]).then((res) => {
|
||||
if (res.success) {
|
||||
title.value = res.data[0].b_value;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const menuList = ref([
|
||||
{
|
||||
label: "首页",
|
||||
path: "/",
|
||||
icon: "lucide:house",
|
||||
},
|
||||
{
|
||||
label: "业务管理",
|
||||
icon: "lucide:briefcase-business",
|
||||
children: [
|
||||
{
|
||||
label: "海运管理",
|
||||
path: "/business",
|
||||
},
|
||||
{
|
||||
label: "装箱统计",
|
||||
path: "/business/field",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "系统管理",
|
||||
icon: "lucide:settings",
|
||||
children: [
|
||||
{
|
||||
label: "字段配置",
|
||||
path: "/system/field",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const selectedKey = computed(() => route.fullPath);
|
||||
|
||||
function handleMenuChange(val) {
|
||||
router.push(val);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="g3-menu">
|
||||
<div class="g3-logo">
|
||||
<div class="logo-inner">
|
||||
<img src="@/assets/logo.png" alt="logo" />
|
||||
<div class="title">{{ title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-menu
|
||||
class="el-menu-vertical"
|
||||
:collapse-transition="false"
|
||||
:default-active="selectedKey"
|
||||
@select="handleMenuChange"
|
||||
>
|
||||
<MenuItem
|
||||
v-for="(item, idx) in menuList"
|
||||
:key="item.path || idx"
|
||||
:item="item"
|
||||
:parent-index="idx.toString()"
|
||||
/>
|
||||
</el-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.g3-menu {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.g3-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 42px;
|
||||
padding: 16px 4px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
|
||||
.logo-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 24px;
|
||||
width: 80px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-left: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #70cfb1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: calc(100% - 90px);
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-vertical {
|
||||
overflow-y: auto;
|
||||
border: none;
|
||||
margin: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
:deep(.el-sub-menu__title) {
|
||||
height: 36px !important;
|
||||
line-height: 36px !important;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
:deep(.el-menu-item) {
|
||||
height: 36px !important;
|
||||
line-height: 36px !important;
|
||||
border-radius: 6px;
|
||||
|
||||
&.is-active {
|
||||
background-color: var(--el-color-primary-light-8);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu) {
|
||||
.el-sub-menu__title {
|
||||
border-radius: 6px;
|
||||
}
|
||||
.el-menu-item {
|
||||
padding-left: 48px !important;
|
||||
}
|
||||
&.is-active {
|
||||
.el-sub-menu__title {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,352 @@
|
||||
<script lang="js" setup>
|
||||
import { useTabStore } from "@/store/tab";
|
||||
import { useRoute } from "vue-router";
|
||||
import emitter from "@/utils/emitter";
|
||||
|
||||
const { refs, setRefs } = useRefs();
|
||||
const showScroll = ref(false);
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const tabStore = useTabStore();
|
||||
|
||||
const fullScreen = ref(false);
|
||||
|
||||
const emits = defineEmits(["fullScreen-change"]);
|
||||
|
||||
watch(
|
||||
() => tabStore.activeTab,
|
||||
(newVal) => {
|
||||
nextTick(() => {
|
||||
updateScroll();
|
||||
|
||||
const tabIndex = tabStore.findTabIndex(newVal);
|
||||
moveScroll(tabIndex);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
function useRefs() {
|
||||
const refs = reactive({});
|
||||
|
||||
function setRefs(name) {
|
||||
return (el) => {
|
||||
refs[name] = el;
|
||||
return () => refs[name];
|
||||
};
|
||||
}
|
||||
|
||||
return { refs, setRefs };
|
||||
}
|
||||
|
||||
function updateScroll() {
|
||||
const wrap = refs.scroller;
|
||||
if (!wrap) return;
|
||||
showScroll.value = wrap.scrollWidth > wrap.clientWidth;
|
||||
}
|
||||
|
||||
function moveScroll(index) {
|
||||
nextTick(() => {
|
||||
const el = refs[`item-${index}`];
|
||||
const wrap = refs.scroller;
|
||||
if (!el || !wrap) return;
|
||||
|
||||
let attempts = 0;
|
||||
const maxAttempts = 5;
|
||||
|
||||
const scrollToActiveTab = () => {
|
||||
const scrollLeft =
|
||||
el.getBoundingClientRect().left -
|
||||
wrap.getBoundingClientRect().left +
|
||||
wrap.scrollLeft -
|
||||
wrap.clientWidth / 2 +
|
||||
el.clientWidth / 2;
|
||||
|
||||
wrap.scrollLeft = scrollLeft;
|
||||
|
||||
if (
|
||||
Math.abs(wrap.scrollLeft - scrollLeft) > 1 &&
|
||||
attempts < maxAttempts
|
||||
) {
|
||||
attempts++;
|
||||
requestAnimationFrame(scrollToActiveTab);
|
||||
}
|
||||
};
|
||||
|
||||
scrollToActiveTab();
|
||||
});
|
||||
}
|
||||
|
||||
// 切换标签
|
||||
function switchTab(tab, index) {
|
||||
router.push(tab.fullPath);
|
||||
moveScroll(index);
|
||||
}
|
||||
|
||||
// 关闭标签
|
||||
async function closeTab(tab) {
|
||||
const activePath = await tabStore.closeTab(tab.fullPath);
|
||||
if (activePath && route.fullPath !== activePath) {
|
||||
router.push(activePath);
|
||||
} else if (!activePath) {
|
||||
router.push("/");
|
||||
}
|
||||
}
|
||||
|
||||
function closeLeft(data) {
|
||||
const currentIndex = tabStore.tabs.findIndex(
|
||||
(item) => item.fullPath === data.fullPath,
|
||||
);
|
||||
if (currentIndex === -1) return;
|
||||
|
||||
// 关闭左侧所有标签
|
||||
tabStore.tabs.slice(0, currentIndex).forEach((item) => {
|
||||
const index = tabStore.tabs.findIndex((t) => t.fullPath === item.fullPath);
|
||||
if (index !== -1) tabStore.tabs.splice(index, 1);
|
||||
if (item.fullPath === route.fullPath) {
|
||||
router.push(data.fullPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeRight(data) {
|
||||
const currentIndex = tabStore.tabs.findIndex(
|
||||
(item) => item.fullPath === data.fullPath,
|
||||
);
|
||||
if (currentIndex === -1) return;
|
||||
|
||||
tabStore.tabs.slice(currentIndex + 1).forEach((item) => {
|
||||
const index = tabStore.tabs.findIndex((t) => t.fullPath === item.fullPath);
|
||||
if (index !== -1) tabStore.tabs.splice(index, 1);
|
||||
if (item.fullPath === route.fullPath) {
|
||||
router.push(data.fullPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeOther(tab) {
|
||||
tabStore.tabs = tabStore.tabs.filter((t) => t.fullPath === tab.fullPath);
|
||||
|
||||
router.push(tab.fullPath);
|
||||
}
|
||||
|
||||
function closeAll() {
|
||||
tabStore.tabs = [];
|
||||
router.push("/");
|
||||
}
|
||||
|
||||
function toHome() {
|
||||
router.push("/");
|
||||
}
|
||||
|
||||
function refreshPage() {
|
||||
emitter.emit("view.refresh");
|
||||
}
|
||||
|
||||
function fullScreenPage() {
|
||||
fullScreen.value = !fullScreen.value;
|
||||
|
||||
emits("fullScreen-change", fullScreen.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="g3-tabs">
|
||||
<div class="g3-tab-op">
|
||||
<el-tooltip content="刷新">
|
||||
<div class="g3-action-icon" @click="refreshPage">
|
||||
<G3Icon icon="lucide:refresh-ccw" />
|
||||
</div>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip content="首页">
|
||||
<div class="g3-action-icon" @click="toHome">
|
||||
<G3Icon icon="lucide:house" />
|
||||
</div>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip :content="fullScreen ? '恢复' : '全屏'">
|
||||
<div class="g3-action-icon" @click="fullScreenPage">
|
||||
<G3Icon :icon="fullScreen ? 'lucide:minimize' : 'lucide:maximize'" />
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="g3-divider"></div>
|
||||
|
||||
<div v-if="showScroll" class="g3-action-icon g3-scroll-more">
|
||||
<G3Icon icon="lucide:chevrons-left" />
|
||||
</div>
|
||||
|
||||
<div class="g3-tab-container">
|
||||
<div :ref="setRefs('scroller')" class="app-tab-scroller">
|
||||
<el-dropdown
|
||||
v-for="(item, index) in tabStore.tabs"
|
||||
:key="item.fullPath"
|
||||
trigger="contextmenu"
|
||||
:persistent="false"
|
||||
>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu v-if="item.fullPath !== '/'">
|
||||
<el-dropdown-item @click="closeTab(item)">
|
||||
关闭
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="closeOther(item)">
|
||||
关闭其他
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="closeLeft(item)">
|
||||
关闭左侧
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="closeRight(item)">
|
||||
关闭右侧
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="closeAll"> 关闭所有 </el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
<div
|
||||
:ref="setRefs(`item-${index}`)"
|
||||
class="app-tab-item"
|
||||
:class="{ active: item.fullPath === tabStore.activeTab }"
|
||||
@click="switchTab(item)"
|
||||
>
|
||||
<span class="label">{{ item.title }}</span>
|
||||
<G3Icon
|
||||
v-if="item.fullPath !== '/'"
|
||||
icon="lucide:x"
|
||||
size="14"
|
||||
class="icon"
|
||||
@click.stop="closeTab(item)"
|
||||
/>
|
||||
</div>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="showScroll"
|
||||
class="g3-action-icon g3-scroll-more"
|
||||
style="margin: 0px 10px"
|
||||
>
|
||||
<G3Icon icon="lucide:chevrons-right" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.g3-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 38px;
|
||||
padding-left: 10px;
|
||||
user-select: none;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #e4e4e7;
|
||||
}
|
||||
|
||||
.g3-tab-op {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.g3-action-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease-in-out;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
margin-right: 8px;
|
||||
&:hover {
|
||||
border-color: var(--border-color-hover);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.g3-divider {
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
background-color: #ddd;
|
||||
margin: 0px 12px;
|
||||
}
|
||||
|
||||
.g3-tab-container {
|
||||
height: 30px;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
:deep(.el-dropdown) {
|
||||
margin-right: 12px;
|
||||
&:last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-tab-scroller {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
scrollbar-width: none;
|
||||
overflow: hidden;
|
||||
scroll-behavior: smooth;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.app-tab-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 28px;
|
||||
padding: 0px 8px;
|
||||
cursor: pointer;
|
||||
color: #18181b;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: #fff;
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-color-primary-light-8);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--el-color-primary-light-8);
|
||||
.label {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
.icon {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--el-color-primary-light-8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.g3-scroll-more {
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,107 @@
|
||||
<script lang="js" setup>
|
||||
const props = defineProps({
|
||||
collapse: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(["collapse"]);
|
||||
|
||||
function onCollapse() {
|
||||
emits("collapse");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="g3-topbar">
|
||||
<div class="g3-topbar-left">
|
||||
<div class="g3-action-icon" @click="onCollapse()">
|
||||
<G3Icon
|
||||
:icon="
|
||||
props.collapse
|
||||
? 'lucide:list-indent-increase'
|
||||
: 'lucide:list-indent-decrease'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="g3-topbar-right">
|
||||
<div class="user-card compact">
|
||||
<G3Icon icon="lucide:circle-user-round" size="18" />
|
||||
<div class="user-info">
|
||||
<span class="user-name">001</span>
|
||||
<!-- <span class="divider">·</span>
|
||||
<span class="user-org">g3cd</span> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.g3-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
max-width: 100%;
|
||||
height: 42px;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid #e4e4e7;
|
||||
padding: 0 10px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.g3-action-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease-in-out;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
&:hover {
|
||||
border-color: var(--border-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.user-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 14px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--border-color-hover);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
margin-left: 10px;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.user-org {
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 0 4px;
|
||||
color: #bbb;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,98 @@
|
||||
<script lang="js" setup>
|
||||
import Menu from "./components/menu.vue";
|
||||
import Topbar from "./components/topbar.vue";
|
||||
import Tab from "./components/tab.vue";
|
||||
import Content from "./components/content.vue";
|
||||
const isCollapse = ref(false);
|
||||
const leftSize = ref(220);
|
||||
const fullScreen = ref(false);
|
||||
|
||||
function handleCollapse() {
|
||||
if (leftSize.value < 220) {
|
||||
leftSize.value = 220;
|
||||
isCollapse.value = false;
|
||||
} else {
|
||||
leftSize.value = 0;
|
||||
isCollapse.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function handleResizeEnd(index, sizes) {
|
||||
if (sizes[0] < 220) {
|
||||
isCollapse.value = true;
|
||||
} else {
|
||||
isCollapse.value = false;
|
||||
}
|
||||
leftSize.value = sizes[0];
|
||||
}
|
||||
|
||||
function onFullScreenChange(val) {
|
||||
fullScreen.value = val;
|
||||
|
||||
if (fullScreen.value) {
|
||||
leftSize.value = 0;
|
||||
} else {
|
||||
handleCollapse();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="g3-layout">
|
||||
<el-splitter @resize-end="handleResizeEnd">
|
||||
<el-splitter-panel :min="0" :size="leftSize" :max="400">
|
||||
<div class="g3-layout-left">
|
||||
<Menu />
|
||||
</div>
|
||||
</el-splitter-panel>
|
||||
<el-splitter-panel>
|
||||
<div class="g3-layout-right">
|
||||
<Topbar
|
||||
:collapse="isCollapse"
|
||||
@collapse="handleCollapse"
|
||||
:style="fullScreen ? { height: '0', border: 'none' } : {}"
|
||||
/>
|
||||
<Tab @full-screen-change="onFullScreenChange" />
|
||||
<Content />
|
||||
</div>
|
||||
</el-splitter-panel>
|
||||
</el-splitter>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.g3-layout {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #fafafa;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(.el-splitter-bar__dragger-horizontal::after),
|
||||
:deep(.el-splitter-bar__dragger-horizontal::before) {
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
:deep(.el-splitter-bar__dragger::after),
|
||||
:deep(.el-splitter-bar__dragger::before) {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.g3-layout-left {
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border-right: 1px solid #bfbfc6;
|
||||
box-shadow: 3px 0 8px rgba(0, 0, 0, 0.05);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.g3-layout-right {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,53 @@
|
||||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import { createPinia } from "pinia";
|
||||
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
||||
import router from "@/router/index";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
|
||||
import { SetupPlugins } from "./plugins";
|
||||
|
||||
import "element-plus/dist/index.css";
|
||||
|
||||
import VxeUIBase from "vxe-pc-ui";
|
||||
import "vxe-pc-ui/es/style.css";
|
||||
|
||||
import VxeUITable from "vxe-table";
|
||||
import "vxe-table/es/style.css";
|
||||
|
||||
import "./style.css";
|
||||
|
||||
// ElementPlus 兼容低版本浏览器
|
||||
window.ResizeObserver = ResizeObserver;
|
||||
|
||||
VxeUITable.setConfig({
|
||||
table: {
|
||||
stripe: true,
|
||||
border: "full",
|
||||
cellConfig: {
|
||||
height: 28,
|
||||
},
|
||||
headerCellConfig: {
|
||||
height: 32,
|
||||
},
|
||||
footerCellConfig: {
|
||||
height: 32,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// pinia
|
||||
const pinia = createPinia();
|
||||
pinia.use(piniaPluginPersistedstate);
|
||||
|
||||
SetupPlugins();
|
||||
|
||||
// 创建 Vue 应用
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(pinia);
|
||||
app.use(router);
|
||||
app.use(VxeUIBase);
|
||||
app.use(VxeUITable);
|
||||
|
||||
app.mount("#app");
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* 用到的图标集在这里添加,可以离线使用
|
||||
*/
|
||||
|
||||
import { addCollection } from "@iconify/vue";
|
||||
import Lucide from "@iconify/json/json/lucide.json";
|
||||
import ElementPlus from "@iconify/json/json/ep.json";
|
||||
|
||||
export function SetupIcons() {
|
||||
addCollection(Lucide);
|
||||
addCollection(ElementPlus);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { SetupIcons } from "./icon";
|
||||
|
||||
export function SetupPlugins() {
|
||||
SetupIcons();
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { httpPost } from ".";
|
||||
|
||||
// 登录
|
||||
export function loginApi(orgid, userid, password) {
|
||||
return httpPost(`/auth/login`, {
|
||||
orgid,
|
||||
userid,
|
||||
password,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================= data =============================
|
||||
export function loadDataApi(viewName, searchCondition = "", orderField = "", searchColumns = []) {
|
||||
return httpPost("/data/loadData", {
|
||||
viewName,
|
||||
searchCondition,
|
||||
orderField,
|
||||
searchColumns,
|
||||
});
|
||||
}
|
||||
|
||||
export function loadDataBySqlApi(sql) {
|
||||
return httpPost("/data/loadDataBySql", {
|
||||
sql,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页加载表格数据
|
||||
* @param {Object} params 参数对象
|
||||
* @param {number} params.page 当前页
|
||||
* @param {number} params.size 每页条数
|
||||
* @param {string} params.viewName 视图名称
|
||||
* @param {string} [params.searchCondition] 搜索条件
|
||||
* @param {string} [params.orderField] 排序字段
|
||||
* @param {string[]} [params.searchColumns] 搜索列
|
||||
* @returns {Promise<{
|
||||
* success: boolean,
|
||||
* data: {
|
||||
* rows: Array<Record<string, any>>,
|
||||
* total: number,
|
||||
* page: number,
|
||||
* size: number
|
||||
* },
|
||||
* code: number,
|
||||
* message: string
|
||||
* }>} 返回 Promise 对象
|
||||
*/
|
||||
export function loadPageDataApi(
|
||||
{ page, size, viewName, searchCondition = "", orderField = "", searchColumns = [] },
|
||||
config = {},
|
||||
) {
|
||||
return httpPost(
|
||||
"/data/page",
|
||||
{
|
||||
page,
|
||||
size,
|
||||
viewName,
|
||||
searchCondition,
|
||||
orderField,
|
||||
searchColumns,
|
||||
},
|
||||
config,
|
||||
);
|
||||
}
|
||||
|
||||
export function saveObjectApi(data) {
|
||||
return httpPost("/data/saveObject", data);
|
||||
}
|
||||
|
||||
export function saveObjectKeysApi(data) {
|
||||
return httpPost("/data/saveObjectKeys", data);
|
||||
}
|
||||
// ============================= data =============================
|
||||
|
||||
// ============================= column =============================
|
||||
export function loadViewColumnsApi(viewName) {
|
||||
return httpPost("/column/view", {
|
||||
viewName,
|
||||
});
|
||||
}
|
||||
|
||||
export function loadEditColumnsApi(viewName) {
|
||||
return httpPost("/column/edit", {
|
||||
viewName,
|
||||
});
|
||||
}
|
||||
|
||||
export function loadAllColumnsApi(viewName) {
|
||||
return httpPost("/column/all", {
|
||||
viewName,
|
||||
});
|
||||
}
|
||||
|
||||
export function checkColumnsApi(viewName) {
|
||||
return httpPost("/column/check", {
|
||||
viewName,
|
||||
});
|
||||
}
|
||||
// ============================= column =============================
|
||||
@@ -0,0 +1,116 @@
|
||||
import axios from "axios";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { useUserStore } from "@/store/user";
|
||||
import router from "@/router";
|
||||
|
||||
let hasShown401 = false;
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
// baseURL: "http://127.0.0.1:8084/api",
|
||||
baseURL: process.env.PUBLIC_API_BASE_URL,
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
axiosInstance.interceptors.request.use(
|
||||
(config) => {
|
||||
const userStore = useUserStore();
|
||||
const token = userStore.userInfo.token;
|
||||
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error),
|
||||
);
|
||||
|
||||
axiosInstance.interceptors.response.use(
|
||||
(response) => {
|
||||
const resData = response.data;
|
||||
|
||||
resData.success = resData.code === 200;
|
||||
|
||||
if (resData.code === 401) {
|
||||
if (!hasShown401) {
|
||||
hasShown401 = true;
|
||||
const userStore = useUserStore();
|
||||
ElMessage.warning("登录已过期,请重新登录");
|
||||
userStore.resetUserInfo();
|
||||
router.replace("/login");
|
||||
}
|
||||
} else if (resData.code === 500) {
|
||||
// 系统异常
|
||||
ElMessage.error(resData.message || "系统异常,请稍后再试");
|
||||
} else if (resData.code === 1000) {
|
||||
// 业务异常
|
||||
ElMessage.error(resData.message || "业务异常,请稍后再试");
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
let message = "请求失败";
|
||||
|
||||
if (error.code === "ECONNABORTED") {
|
||||
message = "请求超时,请稍后重试";
|
||||
} else if (error.message === "Network Error") {
|
||||
message = "网络异常,请检查网络连接";
|
||||
} else if (error.response) {
|
||||
const status = error.response.status;
|
||||
|
||||
switch (status) {
|
||||
case 400:
|
||||
message = "请求参数错误";
|
||||
break;
|
||||
case 404:
|
||||
message = "请求接口不存在";
|
||||
break;
|
||||
case 500:
|
||||
message = "服务器异常";
|
||||
break;
|
||||
default:
|
||||
message = error.response.data?.msg || `请求失败 (${status})`;
|
||||
}
|
||||
}
|
||||
|
||||
// ElMessage.error(message);
|
||||
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* 通用 API 响应类型
|
||||
* @template T
|
||||
* @typedef {Object} ApiResponse
|
||||
* @property {boolean} success - 请求是否成功
|
||||
* @property {number} code - 状态码
|
||||
* @property {string} message - 消息
|
||||
* @property {T} data - 数据
|
||||
*/
|
||||
|
||||
/**
|
||||
* 通用 HTTP POST 请求
|
||||
* @template T
|
||||
* @param {string} url
|
||||
* @param {any} data
|
||||
* @param {any} config
|
||||
* @returns {Promise<ApiResponse<T>>}
|
||||
*/
|
||||
export function httpPost(url, data, config) {
|
||||
return axiosInstance
|
||||
.post(url, data, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
...config,
|
||||
})
|
||||
.then((res) => res.data);
|
||||
}
|
||||
|
||||
export function reset401Flag() {
|
||||
hasShown401 = false;
|
||||
}
|
||||
|
||||
export default axiosInstance;
|
||||
@@ -0,0 +1,64 @@
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import { routes } from "./routes";
|
||||
import { useUserStore } from "@/store/user";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { useTabStore } from "@/store/tab";
|
||||
import NProgress from "nprogress";
|
||||
|
||||
NProgress.configure({ easing: "ease", speed: 500 });
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.PUBLIC_SERVER_PATH || "/"),
|
||||
routes,
|
||||
});
|
||||
|
||||
router.beforeEach((to) => {
|
||||
NProgress?.start?.();
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
// if (!userStore.isLogined() && to.path !== "/login") {
|
||||
// ElMessage.warning("登录已过期,请先登录");
|
||||
// return "/login";
|
||||
// }
|
||||
|
||||
handleTabPath(to);
|
||||
|
||||
return;
|
||||
});
|
||||
|
||||
router.afterEach(() => {
|
||||
NProgress?.done?.();
|
||||
});
|
||||
|
||||
function handleTabPath(to) {
|
||||
const path = to.path;
|
||||
|
||||
if (path === "/login") {
|
||||
return;
|
||||
}
|
||||
|
||||
const meta = to.meta;
|
||||
const query = to.query;
|
||||
|
||||
// 处理标签页
|
||||
if (meta.title) {
|
||||
let displayTitle = meta.title;
|
||||
if (query.id) {
|
||||
displayTitle = `${meta.title} ${query.id}`;
|
||||
}
|
||||
|
||||
const tabStore = useTabStore();
|
||||
|
||||
tabStore.addTab({
|
||||
path: path,
|
||||
fullPath: to.fullPath,
|
||||
title: displayTitle,
|
||||
params: to.params,
|
||||
meta: to.meta,
|
||||
query: to.query,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,40 @@
|
||||
import BaseLayout from "@/layout/index.vue";
|
||||
|
||||
export const routes = [
|
||||
{
|
||||
path: "/",
|
||||
component: BaseLayout,
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
component: () => import("@/views/index.vue"),
|
||||
meta: {
|
||||
title: "首页",
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
// ==================== 业务管理 ====================
|
||||
{
|
||||
path: "/business",
|
||||
component: () => import("@/views/business/index.vue"),
|
||||
meta: {
|
||||
title: "海运业务",
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
// ==================== 系统管理 ====================
|
||||
{
|
||||
path: "/system/field",
|
||||
component: () => import("@/views/system/field/index.vue"),
|
||||
meta: {
|
||||
title: "字段配置",
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
component: () => import("@/views/auth/login.vue"),
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,215 @@
|
||||
CREATE TABLE s_columnlib2 (
|
||||
-- 主键,自增
|
||||
b_id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
|
||||
-- 基本标识
|
||||
col_table_name VARCHAR(50) NOT NULL, -- 表名
|
||||
col_field_name VARCHAR(50) NOT NULL, -- 字段名
|
||||
col_field_name_v VARCHAR(50), -- 字段别名/显示名
|
||||
col_caption VARCHAR(50), -- 字段描述
|
||||
|
||||
-- 展示相关
|
||||
col_visible CHAR(1) DEFAULT '1', -- 是否可见
|
||||
col_width INT, -- 宽度
|
||||
col_position INT, -- 显示位置
|
||||
col_group_index INT, -- 分组编号
|
||||
col_color VARCHAR(50), -- 颜色
|
||||
col_display_format VARCHAR(50), -- 显示格式
|
||||
col_show_summary CHAR(1) DEFAULT '0', -- 是否统计汇总
|
||||
|
||||
-- 校验 / 默认
|
||||
col_allow_null CHAR(1) DEFAULT '1', -- 是否允许空值
|
||||
col_allow_repeat CHAR(1) DEFAULT '1', -- 是否允许重复
|
||||
col_allow_readonly CHAR(1) DEFAULT '0', -- 是否只读
|
||||
col_allow_reserve CHAR(1) DEFAULT '0', -- 是否保留字段
|
||||
col_default_value TEXT, -- 默认值
|
||||
|
||||
-- 查询相关
|
||||
col_search CHAR(1) DEFAULT '0', -- 是否可搜索
|
||||
col_search_order INT, -- 搜索顺序
|
||||
col_search_group_index INT, -- 搜索分组编号
|
||||
|
||||
-- 类型定义
|
||||
col_type VARCHAR(50), -- 字段类型
|
||||
col_show_property CHAR(1) DEFAULT '0', -- 显示属性
|
||||
|
||||
-- 编辑相关
|
||||
col_edit CHAR(1) DEFAULT '0', -- 是否可编辑
|
||||
col_edit_position INT, -- 编辑位置
|
||||
col_edit_width INT, -- 编辑宽度
|
||||
col_edit_height INT, -- 编辑高度
|
||||
col_edit_group_index INT, -- 编辑分组编号
|
||||
col_edit_batch CHAR(1) DEFAULT '0', -- 是否批量编辑
|
||||
col_edit_source_data VARCHAR(50), -- 编辑数据源表
|
||||
col_edit_source_field VARCHAR(50), -- 编辑数据源字段
|
||||
|
||||
-- 唯一约束
|
||||
CONSTRAINT uk_tablename_fieldname UNIQUE (col_table_name, col_field_name)
|
||||
);
|
||||
|
||||
INSERT INTO s_columnlib (
|
||||
col_table_name,
|
||||
col_field_name,
|
||||
col_field_name_v,
|
||||
col_caption,
|
||||
col_width,
|
||||
col_visible,
|
||||
col_position,
|
||||
col_show_summary,
|
||||
col_group_index,
|
||||
col_allow_null,
|
||||
col_allow_repeat,
|
||||
col_default_value,
|
||||
col_allow_readonly,
|
||||
col_color,
|
||||
col_search,
|
||||
col_search_order,
|
||||
col_allow_reserve,
|
||||
col_display_format,
|
||||
col_search_group_index,
|
||||
col_type,
|
||||
col_show_property,
|
||||
col_edit,
|
||||
col_edit_position,
|
||||
col_edit_width,
|
||||
col_edit_height,
|
||||
col_edit_group_index,
|
||||
col_edit_batch,
|
||||
col_edit_source_data,
|
||||
col_edit_source_field
|
||||
)
|
||||
SELECT
|
||||
col_TableName,
|
||||
col_FieldName,
|
||||
col_FieldName_v,
|
||||
col_Caption,
|
||||
TRY_CAST(col_Width AS INT) AS col_width,
|
||||
col_Visible,
|
||||
TRY_CAST(col_Position AS INT) AS col_position,
|
||||
col_ShowSummary,
|
||||
TRY_CAST(col_BandIndex AS INT) AS col_group_index, -- 分组编号
|
||||
col_AllowNull,
|
||||
col_AllowRepeat,
|
||||
col_DefaultValue,
|
||||
col_ReadOnly,
|
||||
col_Color,
|
||||
col_CanSearch, -- 是否可搜索
|
||||
TRY_CAST(col_SearchOrder AS INT) AS col_search_order, -- 搜索顺序
|
||||
col_NeedReserve, -- 保留字段
|
||||
col_DisplayFormat,
|
||||
TRY_CAST(col_SearchGroupIndex AS INT) AS col_search_group_index,
|
||||
col_type,
|
||||
col_edit_type,
|
||||
col_ShowDetail, -- 显示属性
|
||||
col_edit,
|
||||
TRY_CAST(col_edit_position AS INT) AS col_edit_position,
|
||||
TRY_CAST(col_edit_width AS INT) AS col_edit_width,
|
||||
TRY_CAST(col_edit_height AS INT) AS col_edit_height,
|
||||
TRY_CAST(col_edit_grouping AS INT) AS col_edit_group_index, -- 编辑分组编号
|
||||
col_edit_batch,
|
||||
col_edit_source_table, -- 编辑数据源表
|
||||
col_edit_source_field
|
||||
FROM s_columnLib3
|
||||
WHERE
|
||||
-- 只保留关键字段不为空的行,例如表名和字段名必须有值
|
||||
col_TableName IS NOT NULL AND col_FieldName IS NOT NULL;
|
||||
|
||||
|
||||
CREATE PROCEDURE [dbo].[G3_CheckColumnSettings_New]
|
||||
@viewname SYSNAME
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
/* ========== 1. 取视图 / 表字段 ========== */
|
||||
DECLARE @Tmp TABLE (name SYSNAME);
|
||||
|
||||
INSERT INTO @Tmp (name)
|
||||
SELECT c.name
|
||||
FROM sys.columns c
|
||||
WHERE c.object_id = OBJECT_ID(@viewname);
|
||||
|
||||
/* ========== 2. 删除不存在的字段配置 ========== */
|
||||
DELETE s
|
||||
FROM s_columnLib s
|
||||
WHERE s.col_table_name = @viewname
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM @Tmp t
|
||||
WHERE t.name = s.col_field_name
|
||||
);
|
||||
|
||||
/* ========== 3. 计算起始 position ========== */
|
||||
DECLARE @startPos INT;
|
||||
SELECT @startPos = ISNULL(MAX(col_position), 0) + 10
|
||||
FROM s_columnLib
|
||||
WHERE col_table_name = @viewname;
|
||||
|
||||
/* ========== 4. 插入新增字段配置 ========== */
|
||||
INSERT INTO s_columnLib (
|
||||
col_table_name,
|
||||
col_field_name,
|
||||
col_field_name_v,
|
||||
col_caption,
|
||||
col_width,
|
||||
col_visible,
|
||||
col_position,
|
||||
col_show_summary,
|
||||
col_group_index,
|
||||
col_allow_null,
|
||||
col_allow_repeat,
|
||||
col_allow_readonly,
|
||||
col_allow_reserve,
|
||||
col_default_value,
|
||||
col_search,
|
||||
col_search_order,
|
||||
col_search_group_index,
|
||||
col_type,
|
||||
col_show_property,
|
||||
col_edit,
|
||||
col_edit_position,
|
||||
col_edit_width,
|
||||
col_edit_height,
|
||||
col_edit_group_index,
|
||||
col_edit_batch,
|
||||
col_edit_source_data,
|
||||
col_edit_source_field,
|
||||
col_display_format
|
||||
)
|
||||
SELECT
|
||||
@viewname,
|
||||
t.name,
|
||||
'', -- col_field_name_v 默认空
|
||||
t.name, -- col_caption 默认用字段名
|
||||
60, -- col_width 默认 60
|
||||
'1', -- col_visible 默认显示
|
||||
@startPos + ROW_NUMBER() OVER (ORDER BY t.name) * 10,
|
||||
'0', -- col_show_summary 默认 0
|
||||
0, -- col_group_index 默认 0
|
||||
'1', -- col_allow_null 默认允许
|
||||
'1', -- col_allow_repeat 默认允许
|
||||
'0', -- col_allow_readonly 默认不只读
|
||||
'0', -- col_allow_reserve 默认 0
|
||||
'', -- col_default_value 默认空
|
||||
'0', -- col_search 默认 0
|
||||
0, -- col_search_index 默认 0
|
||||
0,
|
||||
'', -- col_type 默认空
|
||||
'0', -- col_show_property 默认 0
|
||||
'0', -- col_edit 默认 0
|
||||
0, -- col_edit_position 默认 0
|
||||
0, -- col_edit_width 默认 0
|
||||
0, -- col_edit_height 默认 0
|
||||
0, -- col_edit_group_index 默认 0
|
||||
'0', -- col_edit_batch 默认 0
|
||||
'', -- col_edit_source_data 默认空
|
||||
'', -- col_edit_source_field 默认空
|
||||
'' -- col_display_format 默认空
|
||||
FROM @Tmp t
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM s_columnLib s
|
||||
WHERE s.col_table_name = @viewname
|
||||
AND s.col_field_name = t.name
|
||||
);
|
||||
END
|
||||
Whitespace-only changes.
@@ -0,0 +1,74 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
export const useTabStore = defineStore("tabs", () => {
|
||||
const tabs = ref([{ title: "首页", path: "/", fullPath: "/" }]);
|
||||
const activeTab = ref("/");
|
||||
const cachedKeys = ref(["/"]);
|
||||
|
||||
const tabMap = computed(() => {
|
||||
const map = new Map();
|
||||
tabs.value.forEach((t) => map.set(t.fullPath, t));
|
||||
return map;
|
||||
});
|
||||
|
||||
const addTab = (tab) => {
|
||||
const existing = tabMap.value.get(tab.fullPath);
|
||||
if (existing) {
|
||||
existing.title = tab.title;
|
||||
} else {
|
||||
tabs.value.push(tab);
|
||||
if (tab.meta?.keepAlive && !cachedKeys.value.includes(tab.fullPath)) {
|
||||
cachedKeys.value.push(tab.fullPath);
|
||||
}
|
||||
}
|
||||
activeTab.value = tab.fullPath;
|
||||
};
|
||||
|
||||
const setActiveTab = (fullPath) => {
|
||||
if (tabMap.value.has(fullPath)) activeTab.value = fullPath;
|
||||
};
|
||||
|
||||
const closeTab = (fullPath) => {
|
||||
if (fullPath === "/") return activeTab.value;
|
||||
|
||||
const index = tabs.value.findIndex((t) => t.fullPath === fullPath);
|
||||
if (index === -1) return activeTab.value;
|
||||
|
||||
tabs.value.splice(index, 1);
|
||||
const cacheIndex = cachedKeys.value.indexOf(fullPath);
|
||||
if (cacheIndex !== -1) cachedKeys.value.splice(cacheIndex, 1);
|
||||
|
||||
if (activeTab.value === fullPath) {
|
||||
const newIndex = Math.max(index - 1, 0);
|
||||
activeTab.value = tabs.value[newIndex].fullPath;
|
||||
}
|
||||
|
||||
return activeTab.value;
|
||||
};
|
||||
|
||||
const resetTabs = () => {
|
||||
const homeTab = { title: "首页", path: "/", fullPath: "/" };
|
||||
tabs.value = [homeTab];
|
||||
activeTab.value = "/";
|
||||
cachedKeys.value = ["/"];
|
||||
};
|
||||
|
||||
const currentActiveTab = computed(() => tabMap.value.get(activeTab.value));
|
||||
|
||||
const findTabIndex = (fullPath) => {
|
||||
return tabs.value.findIndex((t) => t.fullPath === fullPath);
|
||||
};
|
||||
|
||||
return {
|
||||
tabs,
|
||||
activeTab,
|
||||
cachedKeys,
|
||||
addTab,
|
||||
setActiveTab,
|
||||
closeTab,
|
||||
resetTabs,
|
||||
currentActiveTab,
|
||||
findTabIndex,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
export const useUserStore = defineStore(
|
||||
"userInfo",
|
||||
() => {
|
||||
const userInfo = ref({
|
||||
orgid: "",
|
||||
userid: "",
|
||||
username: "",
|
||||
token: "",
|
||||
});
|
||||
|
||||
function setUserInfo(orgid, userid, username, token) {
|
||||
userInfo.value.orgid = orgid;
|
||||
userInfo.value.userid = userid;
|
||||
userInfo.value.token = token;
|
||||
userInfo.value.username = username;
|
||||
}
|
||||
|
||||
function resetUserInfo() {
|
||||
userInfo.value.token = "";
|
||||
}
|
||||
|
||||
function isLogined() {
|
||||
return !!userInfo.value.orgid && !!userInfo.value.userid && !!userInfo.value.token;
|
||||
}
|
||||
|
||||
return {
|
||||
userInfo,
|
||||
setUserInfo,
|
||||
resetUserInfo,
|
||||
isLogined,
|
||||
};
|
||||
},
|
||||
{
|
||||
persist: {
|
||||
paths: ["userInfo"],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,22 @@
|
||||
@import "./styles/nprogress.css";
|
||||
@import "./styles/element-plus.scss";
|
||||
@import "./styles/vxe-table.scss";
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
:root {
|
||||
--border-color: #dcdfe6;
|
||||
--border-color-hover: var(--el-color-primary);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
:root {
|
||||
--el-color-primary: #165dff;
|
||||
|
||||
--el-component-size-small: 24px;
|
||||
--el-component-size: 28px;
|
||||
--el-component-size-large: 32px;
|
||||
|
||||
--el-loading-spinner-size: 36px !important;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
height: 28px;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.el-button--small {
|
||||
height: 24px;
|
||||
}
|
||||
.el-button--large {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.el-button + .el-button {
|
||||
margin-left: 0px ;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
@mixin flexGapRow($size) {
|
||||
gap: $size;
|
||||
& > * {
|
||||
margin-right: $size;
|
||||
}
|
||||
& > *:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin flexGapColumn($size) {
|
||||
gap: $size;
|
||||
& > * {
|
||||
margin-bottom: $size;
|
||||
}
|
||||
& > *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容低版本浏览器的 Flex Wrap 布局 Mixin
|
||||
* 支持浏览器:自动检测 gap 支持,不支持时回退到负边距方案
|
||||
* @param {number} $row-gap - 行间距
|
||||
* @param {number} $column-gap - 列间距
|
||||
* @param {string} $direction: row - 布局方向 (row|column)
|
||||
*/
|
||||
@mixin flexGapWrap($row-gap, $column-gap, $direction: row) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: $direction;
|
||||
|
||||
// 现代浏览器:直接使用 gap
|
||||
@supports (gap: 1px) {
|
||||
gap: $row-gap $column-gap;
|
||||
}
|
||||
|
||||
// 低版本浏览器回退方案
|
||||
@supports not (gap: 1px) {
|
||||
@if $direction == row {
|
||||
// 横向布局:用负边距补偿
|
||||
margin: -$row-gap / 2 (-$column-gap / 2);
|
||||
|
||||
& > * {
|
||||
margin: $row-gap / 2 $column-gap / 2;
|
||||
}
|
||||
} @else {
|
||||
// 纵向布局
|
||||
margin: (-$row-gap / 2) (-$column-gap / 2);
|
||||
|
||||
& > * {
|
||||
margin: $row-gap / 2 $column-gap / 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/* Make clicks pass-through */
|
||||
#nprogress {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#nprogress .bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1031;
|
||||
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--el-color-primary);
|
||||
}
|
||||
/* Fancy blur effect */
|
||||
#nprogress .peg {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
display: block;
|
||||
width: 100px;
|
||||
height: 100%;
|
||||
box-shadow:
|
||||
0 0 10px var(--el-color-primary),
|
||||
0 0 5px var(--el-color-primary);
|
||||
opacity: 1;
|
||||
|
||||
-webkit-transform: rotate(3deg) translate(0px, -4px);
|
||||
-ms-transform: rotate(3deg) translate(0px, -4px);
|
||||
transform: rotate(3deg) translate(0px, -4px);
|
||||
}
|
||||
/* Remove these to get rid of the spinner */
|
||||
#nprogress .spinner {
|
||||
position: fixed;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
z-index: 1031;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#nprogress .spinner-icon {
|
||||
box-sizing: border-box;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
||||
border: solid 2px transparent;
|
||||
border-top-color: var(--el-color-primary);
|
||||
border-left-color: var(--el-color-primary);
|
||||
border-radius: 50%;
|
||||
|
||||
-webkit-animation: nprogress-spinner 300ms linear infinite;
|
||||
animation: nprogress-spinner 300ms linear infinite;
|
||||
}
|
||||
|
||||
.nprogress-custom-parent {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nprogress-custom-parent #nprogress .spinner,
|
||||
.nprogress-custom-parent #nprogress .bar {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@-webkit-keyframes nprogress-spinner {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes nprogress-spinner {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
.vxe-table {
|
||||
.el-checkbox {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.col--edit {
|
||||
.vxe-cell {
|
||||
padding: 1px !important;
|
||||
.vxe-cell--wrapper {
|
||||
height: 100%;
|
||||
|
||||
.el-input,
|
||||
.el-select {
|
||||
height: 100%;
|
||||
line-height: 100%;
|
||||
|
||||
.el-input__wrapper,
|
||||
.el-select__wrapper {
|
||||
height: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 对比新旧对象,只返回变化字段,同时保留主键
|
||||
* @param {Object} newData 新对象
|
||||
* @param {Object} oldData 旧对象
|
||||
* @param {Array<string>} primaryKeys 主键字段数组
|
||||
* @param {Array<string>} excludeKeys 排除字段(可选)
|
||||
* @returns {{ isChanged: boolean, result: Object }} 明确返回值结构
|
||||
*/
|
||||
export function diffObject(newData, oldData, primaryKeys = [], excludeKeys = []) {
|
||||
const result = {};
|
||||
let isChanged = false;
|
||||
|
||||
// 先把主键字段放入结果
|
||||
for (const pk of primaryKeys) {
|
||||
if (pk in newData) {
|
||||
result[pk] = newData[pk];
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of Object.keys(newData)) {
|
||||
if (excludeKeys.includes(key)) continue;
|
||||
if (key.startsWith("_") || key.startsWith("v_")) continue; // 可选:忽略 v_ 前缀字段
|
||||
|
||||
const newVal = newData[key];
|
||||
const oldVal = oldData[key];
|
||||
|
||||
// 只比较原始类型
|
||||
if (newVal !== oldVal) {
|
||||
result[key] = newVal;
|
||||
isChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
return { isChanged, result };
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import mitt from "mitt";
|
||||
|
||||
const emitter = mitt();
|
||||
|
||||
export default emitter;
|
||||
@@ -0,0 +1,5 @@
|
||||
import throttle from "lodash/throttle";
|
||||
|
||||
export function useThrottle(fn, delay = 150) {
|
||||
return throttle(fn, delay);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// utils/uuid.js
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
/**
|
||||
* 获取唯一 ID
|
||||
*/
|
||||
export function getUuid() {
|
||||
const id = uuidv4();
|
||||
return id;
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
<script lang="js" setup>
|
||||
import { loginApi } from "@/request/api";
|
||||
import { useUserStore } from "@/store/user";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { useRouter } from "vue-router";
|
||||
import Vcode from "vue3-puzzle-vcode";
|
||||
import { reset401Flag } from "@/request";
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const form = ref({
|
||||
orgid: userStore.userInfo.orgid,
|
||||
userid: userStore.userInfo.userid,
|
||||
password: "",
|
||||
});
|
||||
|
||||
const showVcode = ref(false);
|
||||
const loginLoading = ref(false);
|
||||
|
||||
const linkList = ref([
|
||||
{
|
||||
title: "新手上路",
|
||||
icon: "solar:key-bold",
|
||||
href: "http://bbs.g3soft.cn/forum.php?mod=viewthread&tid=115&extra=page%3D1",
|
||||
},
|
||||
{
|
||||
title: "会员活动",
|
||||
icon: "mingcute:vip-1-fill",
|
||||
href: "http://www.g3soft.cn/help/guide/promotion.html",
|
||||
},
|
||||
{
|
||||
title: "视频教程",
|
||||
icon: "material-symbols:auto-videocam-rounded",
|
||||
href: "http://bbs.g3soft.cn/forum.php?mod=viewthread&tid=86&extra=page%3D1",
|
||||
},
|
||||
{
|
||||
title: "常见问题",
|
||||
icon: "ic:baseline-help",
|
||||
href: "http://bbs.g3soft.cn/forum.php?mod=forumdisplay&fid=37",
|
||||
},
|
||||
]);
|
||||
|
||||
const endLinkList = ref([
|
||||
{
|
||||
title: "圆通官网",
|
||||
href: "http://www.g3soft.cn",
|
||||
},
|
||||
{
|
||||
title: "申请试用",
|
||||
href: "http://dns.g3soft.cn/demo/jsys/login/login.jsp",
|
||||
},
|
||||
{
|
||||
title: "联系我们",
|
||||
href: "http://www.g3soft.cn/aboutus.html",
|
||||
},
|
||||
{
|
||||
title: "客服热线: 0574-87812201",
|
||||
href: "",
|
||||
},
|
||||
]);
|
||||
|
||||
function toHref(href) {
|
||||
if (href) {
|
||||
window.open(href, "_blank");
|
||||
}
|
||||
}
|
||||
|
||||
function onVcodeShow() {
|
||||
if (form.value.orgid === "") {
|
||||
ElMessage.error("机构码不能为空!");
|
||||
return;
|
||||
}
|
||||
if (form.value.userid === "") {
|
||||
ElMessage.error("用户名不能为空!");
|
||||
return;
|
||||
}
|
||||
showVcode.value = true;
|
||||
}
|
||||
|
||||
function onVcodeClose() {
|
||||
showVcode.value = false;
|
||||
}
|
||||
|
||||
function onVcodeSuccess() {
|
||||
onVcodeClose();
|
||||
|
||||
loginLoading.value = true;
|
||||
|
||||
try {
|
||||
loginApi(form.value.orgid, form.value.userid, form.value.password).then(
|
||||
(res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("登录成功");
|
||||
|
||||
userStore.setUserInfo(
|
||||
form.value.orgid,
|
||||
form.value.userid,
|
||||
res.data.username,
|
||||
res.data.token,
|
||||
);
|
||||
router.push("/");
|
||||
|
||||
reset401Flag();
|
||||
}
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
loginLoading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<div class="login-header">
|
||||
<div class="logo">
|
||||
<img src="@/assets/logo.png" />
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="name">网上服务平台</div>
|
||||
</div>
|
||||
|
||||
<div class="login-body">
|
||||
<div class="login-main">
|
||||
<div class="tip">登录</div>
|
||||
<div class="split"></div>
|
||||
<div class="form">
|
||||
<div class="form-item">
|
||||
<div class="label required">机构码</div>
|
||||
|
||||
<el-input v-model="form.orgid">
|
||||
<template #prefix>
|
||||
<G3Icon icon="lucide:building" size="18" />
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="label required">账号</div>
|
||||
<el-input v-model="form.userid">
|
||||
<template #prefix>
|
||||
<G3Icon icon="lucide:user-round" size="18" />
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="label">密码</div>
|
||||
<el-input v-model="form.password" show-password>
|
||||
<template #prefix>
|
||||
<G3Icon icon="lucide:lock-keyhole" size="18" />
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
<el-button
|
||||
size="large"
|
||||
long
|
||||
type="primary"
|
||||
style="
|
||||
background-color: #33dcd0;
|
||||
border-color: #00d3c4;
|
||||
font-size: 18px;
|
||||
"
|
||||
@click="onVcodeShow"
|
||||
:loading="loginLoading"
|
||||
>
|
||||
登录
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="service-container">
|
||||
<div class="service-title">
|
||||
<div class="service-left"></div>
|
||||
<div class="service-core">服务中心</div>
|
||||
<div class="service-right"></div>
|
||||
</div>
|
||||
|
||||
<div class="service-link">
|
||||
<template v-for="(item, index) in linkList" :key="index">
|
||||
<div class="link-item" @click="toHref(item.href)">
|
||||
<div class="link-icon">
|
||||
<G3Icon :icon="item.icon" size="38" color="#6cd6b3" />
|
||||
</div>
|
||||
<div class="link-title">{{ item.title }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="end-container">
|
||||
<template v-for="(item, index) in endLinkList" :key="index">
|
||||
<div class="end-li" @click="toHref(item.href)">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
|
||||
<div v-if="index !== endLinkList.length - 1" class="end-line"></div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<Vcode :show="showVcode" @success="onVcodeSuccess" @close="onVcodeClose" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "@/styles/mixins.scss" as *;
|
||||
|
||||
.login-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
width: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 81px;
|
||||
margin-left: 25%;
|
||||
|
||||
.logo {
|
||||
img {
|
||||
height: 40px;
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 28px;
|
||||
background-color: #ddd;
|
||||
margin: 0px 24px;
|
||||
}
|
||||
|
||||
.name {
|
||||
color: #595959;
|
||||
}
|
||||
}
|
||||
|
||||
.login-body {
|
||||
background-image: url("@/assets/login-bg.jpg");
|
||||
width: 100%;
|
||||
height: 420px;
|
||||
position: relative;
|
||||
|
||||
.login-main {
|
||||
position: absolute;
|
||||
right: 25%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
|
||||
background-color: #fdfdfd;
|
||||
padding: 24px 28px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.tip {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
margin: 0 30px;
|
||||
vertical-align: super;
|
||||
}
|
||||
.split {
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
border-bottom: 3px solid #00d3c4;
|
||||
margin: 0 auto;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.form {
|
||||
margin-top: 16px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 12px;
|
||||
.label {
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.required {
|
||||
&::before {
|
||||
content: "*";
|
||||
color: red;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
height: 38px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.service-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
padding-bottom: 56px;
|
||||
flex-shrink: 0;
|
||||
.service-title {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 40px 0px;
|
||||
padding: 0 40px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.service-left {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 38%;
|
||||
height: 2px;
|
||||
background-color: #cccccc;
|
||||
vertical-align: middle;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: -6px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #ccc;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.service-core {
|
||||
display: inline-block;
|
||||
width: 24%;
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
height: 34px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.service-right {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 38%;
|
||||
height: 2px;
|
||||
background-color: #cccccc;
|
||||
vertical-align: middle;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -6px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #ccc;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.service-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@include flexGapWrap(40px, 160px);
|
||||
|
||||
.link-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
|
||||
.link-icon {
|
||||
padding: 12px;
|
||||
border-radius: 999px;
|
||||
border: 2px solid #6cd6b3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.link-title {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.end-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #595959;
|
||||
margin: 24px 0px;
|
||||
|
||||
.end-li {
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.end-line {
|
||||
margin: 0 22px;
|
||||
width: 1px;
|
||||
height: 14px;
|
||||
border-left: 1px solid #595959;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script lang="js" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class=""></div>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script lang="js" setup></script>
|
||||
|
||||
<template>
|
||||
<div class="">
|
||||
<div></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped></style>
|
||||
@@ -0,0 +1,49 @@
|
||||
<script setup lang="js">
|
||||
const value = ref("");
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: "Option1",
|
||||
label: "Option1",
|
||||
},
|
||||
{
|
||||
value: "Option2",
|
||||
label: "Option2",
|
||||
},
|
||||
{
|
||||
value: "Option3",
|
||||
label: "Option3",
|
||||
},
|
||||
{
|
||||
value: "Option4",
|
||||
label: "Option4",
|
||||
},
|
||||
{
|
||||
value: "Option5",
|
||||
label: "Option5",
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<G3Grid direction="row">
|
||||
<G3GridGroup :label-width="80" :width="440" :column="2">
|
||||
<G3GridItem label="1222222222222222222222222" content="2" :span="1" required>
|
||||
<el-input type="textarea" v-model="value" />
|
||||
</G3GridItem>
|
||||
|
||||
<G3GridItem label="3" :span="1">
|
||||
<el-input />
|
||||
</G3GridItem>
|
||||
</G3GridGroup>
|
||||
<G3GridGroup :label-width="80" :width="440" :column="2">
|
||||
<G3GridItem label="1222222222222222222222222" content="2" :span="1" required>
|
||||
<el-input type="textarea" v-model="value" />
|
||||
</G3GridItem>
|
||||
|
||||
<G3GridItem label="3" :span="1">
|
||||
<el-input />
|
||||
</G3GridItem>
|
||||
</G3GridGroup>
|
||||
</G3Grid>
|
||||
</template>
|
||||
@@ -0,0 +1,714 @@
|
||||
<script lang="js" setup>
|
||||
import {
|
||||
checkColumnsApi,
|
||||
loadDataApi,
|
||||
loadAllColumnsApi,
|
||||
loadDataBySqlApi,
|
||||
saveObjectApi,
|
||||
} from "@/request/api";
|
||||
import { ElMessage } from "element-plus";
|
||||
import PreviewSql from "./preview-sql.vue";
|
||||
import { diffObject } from "@/utils/diff";
|
||||
|
||||
const tableRef = ref();
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const leftSize = ref(200);
|
||||
const isCollapse = ref(false);
|
||||
|
||||
const showSql = ref(false);
|
||||
|
||||
const treeData = [{ b_id: "root", mc_b: "全部分类", isLeaf: false, children: [] }];
|
||||
|
||||
const treeProps = {
|
||||
children: "children",
|
||||
label: "mc_b",
|
||||
};
|
||||
const currentNode = ref(null);
|
||||
const currentModule = ref(null);
|
||||
const isSelectedNode = computed(() => !!currentNode.value);
|
||||
|
||||
const tableData = ref([]);
|
||||
|
||||
const currentRow = ref({});
|
||||
const isSelectedRow = computed(() => !!currentRow.value?.col_FieldName);
|
||||
let oldRowData = {};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
prop: "col_field_name",
|
||||
label: "字段",
|
||||
width: 140,
|
||||
fixed: "left",
|
||||
},
|
||||
{
|
||||
prop: "col_caption",
|
||||
label: "标题",
|
||||
width: 100,
|
||||
fixed: "left",
|
||||
type: "input",
|
||||
},
|
||||
{
|
||||
prop: "col_field_name_v",
|
||||
label: "视图字段",
|
||||
width: 100,
|
||||
type: "input",
|
||||
},
|
||||
{
|
||||
prop: "col_group_index",
|
||||
label: "分组",
|
||||
width: 80,
|
||||
type: "input",
|
||||
},
|
||||
{
|
||||
prop: "col_visible",
|
||||
label: "显示",
|
||||
width: 80,
|
||||
type: "checkbox",
|
||||
},
|
||||
{
|
||||
prop: "col_allow_readonly",
|
||||
label: "只读",
|
||||
width: 80,
|
||||
type: "checkbox",
|
||||
},
|
||||
{
|
||||
prop: "col_allow_null",
|
||||
label: "可不输",
|
||||
width: 80,
|
||||
type: "checkbox",
|
||||
},
|
||||
{
|
||||
prop: "col_allow_repeat",
|
||||
label: "可重复",
|
||||
width: 80,
|
||||
type: "checkbox",
|
||||
},
|
||||
{
|
||||
prop: "col_allow_reserve",
|
||||
label: "可保留",
|
||||
width: 80,
|
||||
type: "checkbox",
|
||||
},
|
||||
{
|
||||
prop: "col_search",
|
||||
label: "可查找",
|
||||
width: 80,
|
||||
type: "checkbox",
|
||||
},
|
||||
{
|
||||
prop: "col_search_order",
|
||||
label: "查找顺序",
|
||||
width: 100,
|
||||
type: "input",
|
||||
},
|
||||
{
|
||||
prop: "col_search_group_index",
|
||||
label: "查找分组",
|
||||
width: 100,
|
||||
type: "input",
|
||||
},
|
||||
{
|
||||
prop: "col_type",
|
||||
label: "字段类型",
|
||||
width: 100,
|
||||
type: "option",
|
||||
},
|
||||
{
|
||||
prop: "col_default_value",
|
||||
label: "默认值",
|
||||
width: 80,
|
||||
type: "input",
|
||||
},
|
||||
{
|
||||
prop: "col_width",
|
||||
label: "宽度",
|
||||
width: 80,
|
||||
type: "input",
|
||||
},
|
||||
{
|
||||
prop: "col_position",
|
||||
label: "位置",
|
||||
width: 80,
|
||||
type: "input",
|
||||
},
|
||||
{
|
||||
prop: "col_show_summary",
|
||||
label: "合计",
|
||||
width: 80,
|
||||
type: "checkbox",
|
||||
},
|
||||
{
|
||||
prop: "col_display_format",
|
||||
label: "显示格式",
|
||||
width: 100,
|
||||
type: "input",
|
||||
},
|
||||
{
|
||||
prop: "col_edit",
|
||||
label: "编辑",
|
||||
width: 80,
|
||||
type: "checkbox",
|
||||
},
|
||||
{
|
||||
prop: "col_edit_position",
|
||||
label: "编辑位置",
|
||||
width: 100,
|
||||
type: "input",
|
||||
},
|
||||
{
|
||||
prop: "col_edit_width",
|
||||
label: "编辑宽度",
|
||||
width: 100,
|
||||
type: "input",
|
||||
},
|
||||
{
|
||||
prop: "col_edit_height",
|
||||
label: "编辑高度",
|
||||
width: 100,
|
||||
type: "input",
|
||||
},
|
||||
{
|
||||
prop: "col_edit_source_data",
|
||||
label: "编辑数据",
|
||||
width: 160,
|
||||
type: "input",
|
||||
},
|
||||
{
|
||||
prop: "col_edit_source_field",
|
||||
label: "下拉表显示字段",
|
||||
width: 100,
|
||||
type: "input",
|
||||
},
|
||||
{
|
||||
prop: "col_edit_group_index",
|
||||
label: "编辑分组",
|
||||
width: 100,
|
||||
type: "input",
|
||||
},
|
||||
{
|
||||
prop: "col_edit_batch",
|
||||
label: "批量编辑",
|
||||
width: 100,
|
||||
type: "checkbox",
|
||||
},
|
||||
{
|
||||
prop: "col_show_property",
|
||||
label: "属性",
|
||||
width: 80,
|
||||
type: "checkbox",
|
||||
},
|
||||
];
|
||||
|
||||
const editTypeOptions = [
|
||||
{
|
||||
value: "input",
|
||||
label: "input",
|
||||
},
|
||||
{
|
||||
value: "textarea",
|
||||
label: "textarea",
|
||||
},
|
||||
{
|
||||
value: "date",
|
||||
label: "date",
|
||||
},
|
||||
{
|
||||
value: "datetime",
|
||||
label: "datetime",
|
||||
},
|
||||
{
|
||||
value: "singleId",
|
||||
label: "singleId",
|
||||
},
|
||||
{
|
||||
value: "singleName",
|
||||
label: "singleName",
|
||||
},
|
||||
{
|
||||
value: "option",
|
||||
label: "option",
|
||||
},
|
||||
{
|
||||
value: "checkbox",
|
||||
label: "checkbox",
|
||||
},
|
||||
{
|
||||
value: "radio",
|
||||
label: "radio",
|
||||
},
|
||||
];
|
||||
|
||||
function getOptions(field) {
|
||||
if (field === "col_type") {
|
||||
return editTypeOptions;
|
||||
}
|
||||
}
|
||||
|
||||
function updateCollapse() {
|
||||
if (isCollapse.value) {
|
||||
isCollapse.value = false;
|
||||
leftSize.value = 200;
|
||||
} else {
|
||||
isCollapse.value = true;
|
||||
leftSize.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTreeLoad(node, resolve) {
|
||||
try {
|
||||
const { level: nodeLevel, data: nodeData } = node;
|
||||
if (nodeLevel === 0) {
|
||||
resolve([
|
||||
{
|
||||
b_id: "root",
|
||||
mc_b: "全部分类",
|
||||
children: [],
|
||||
isLeaf: false,
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentLevel = nodeLevel - 1;
|
||||
let sql = "";
|
||||
|
||||
if (currentLevel === 0) {
|
||||
sql = `SELECT * FROM s_module_bmfl WHERE js='1' AND b_canuse='1' ORDER BY b_xh`;
|
||||
} else {
|
||||
if (!nodeData) {
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const parentField = `bm_${currentLevel}_b`;
|
||||
const parentCondition = ` AND ${parentField}='${nodeData.b_id}'`;
|
||||
const childLevel = currentLevel + 1;
|
||||
|
||||
sql = `
|
||||
SELECT s_module_bmfl.*, s_module.b_id AS b_moduleid
|
||||
FROM s_module_bmfl
|
||||
LEFT JOIN s_module ON s_module.b_cate_id = s_module_bmfl.b_id
|
||||
WHERE s_module_bmfl.b_canuse='1'
|
||||
AND js='${childLevel}'
|
||||
${parentCondition}
|
||||
AND s_module_bmfl.b_canuse='1'
|
||||
ORDER BY b_xh
|
||||
`;
|
||||
}
|
||||
|
||||
const res = await loadDataBySqlApi(sql);
|
||||
if (res.success) {
|
||||
resolve(res.data);
|
||||
} else {
|
||||
resolve([]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
resolve([]);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTreeNodeClick(nodeData) {
|
||||
if (!nodeData || nodeData.b_id === "root" || !nodeData.b_moduleid) return;
|
||||
|
||||
currentNode.value = nodeData;
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const res = await loadDataApi("s_module", `b_id = '${nodeData.b_moduleid}'`);
|
||||
|
||||
if (res.success) {
|
||||
currentModule.value = res.data[0] || null;
|
||||
|
||||
if (currentModule.value?.b_ViewTable) {
|
||||
const viewTable = currentModule.value?.b_ViewTable;
|
||||
await loadDatas(viewTable);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("加载模块或列失败", err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
async function loadDatas(viewName) {
|
||||
if (!viewName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await loadAllColumnsApi(viewName);
|
||||
|
||||
if (res.success) {
|
||||
tableData.value = res.data;
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑关闭事件
|
||||
async function handleEditClosed({ row, rowIndex, column, columnIndex }) {
|
||||
const $table = tableRef.value;
|
||||
if ($table) {
|
||||
await handleSaveRow(row);
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑激活事件
|
||||
function handleEditActivated({ row }) {
|
||||
const $table = tableRef.value;
|
||||
if ($table) {
|
||||
oldRowData = JSON.parse(JSON.stringify(row));
|
||||
}
|
||||
}
|
||||
|
||||
// 选中行事件
|
||||
function handleCurrentRowChange({ newValue }) {
|
||||
currentRow.value = newValue;
|
||||
}
|
||||
|
||||
// 保存
|
||||
function handleSaveRow(row) {
|
||||
if (!row?.b_id) return;
|
||||
|
||||
const diffRowResult = diffObject(row, oldRowData, ["b_id"], []);
|
||||
|
||||
if (!diffRowResult.isChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
const saveData = [
|
||||
{
|
||||
tableName: "s_columnLib",
|
||||
keyField: "b_id",
|
||||
inserts: [],
|
||||
updates: [diffRowResult.result],
|
||||
deletes: [],
|
||||
log: "0",
|
||||
user: "0",
|
||||
},
|
||||
];
|
||||
|
||||
saveObjectApi(saveData).then((res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("保存成功");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 检查
|
||||
function handleCheckColumns() {
|
||||
if (!currentModule.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewName = currentModule.value.b_ViewTable || "";
|
||||
|
||||
if (!viewName) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
checkColumnsApi(viewName)
|
||||
.then(async (res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("检查成功");
|
||||
await loadDatas(viewName);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="g3-wrapper">
|
||||
<el-splitter>
|
||||
<el-splitter-panel :size="leftSize" :min="0" :max="260">
|
||||
<div class="g3-wrapper-left">
|
||||
<div class="tree-header">
|
||||
<el-text>模块名</el-text>
|
||||
<div class="tree-options"></div>
|
||||
</div>
|
||||
|
||||
<div class="tree-container">
|
||||
<el-scrollbar style="flex: 1">
|
||||
<el-tree
|
||||
lazy
|
||||
highlight-current
|
||||
node-key="b_id"
|
||||
:data="treeData"
|
||||
:props="treeProps"
|
||||
:load="handleTreeLoad"
|
||||
:default-expanded-keys="['root']"
|
||||
@node-click="handleTreeNodeClick"
|
||||
/>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</el-splitter-panel>
|
||||
<el-splitter-panel style="margin-left: 8px">
|
||||
<div class="g3-wrapper-right">
|
||||
<div class="content">
|
||||
<div class="content-head">
|
||||
<div class="content-head-left">
|
||||
<el-button type="primary" :disabled="!isSelectedNode" @click="handleCheckColumns"
|
||||
>检查</el-button
|
||||
>
|
||||
<el-button type="primary" :disabled="!isSelectedNode">浏览栏位预览</el-button>
|
||||
<el-button type="primary" :disabled="!isSelectedNode">编辑栏位预览</el-button>
|
||||
<el-button type="primary" :disabled="!isSelectedNode">属性预览</el-button>
|
||||
<el-button type="primary">修改标签</el-button>
|
||||
<el-button type="primary" :disabled="!isSelectedNode" @click="showSql = !showSql">
|
||||
SQL语句
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="content-head-right">
|
||||
<el-input placeholder="请输入关键词搜索" />
|
||||
<el-button type="primary">搜索</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-table">
|
||||
<vxe-table
|
||||
ref="tableRef"
|
||||
height="100%"
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
:row-config="{
|
||||
useKey: true,
|
||||
keyField: 'col_FieldName',
|
||||
isCurrent: true,
|
||||
}"
|
||||
:column-config="{
|
||||
resizable: true,
|
||||
useKey: true,
|
||||
}"
|
||||
:edit-config="{
|
||||
trigger: 'click',
|
||||
mode: 'row',
|
||||
autoClear: false,
|
||||
showStatus: false,
|
||||
showIcon: false,
|
||||
}"
|
||||
:scroll-x="{
|
||||
gt: 0,
|
||||
enabled: true,
|
||||
}"
|
||||
:scroll-y="{
|
||||
gt: 0,
|
||||
enabled: true,
|
||||
}"
|
||||
show-overflow
|
||||
show-header-overflow
|
||||
@current-row-change="handleCurrentRowChange"
|
||||
@edit-closed="handleEditClosed"
|
||||
@edit-activated="handleEditActivated"
|
||||
>
|
||||
<vxe-column type="seq" width="50" fixed="left" />
|
||||
|
||||
<vxe-column
|
||||
v-for="column in columns"
|
||||
:key="column.prop"
|
||||
:field="column.prop"
|
||||
:title="column.label"
|
||||
:width="column.width"
|
||||
align="center"
|
||||
sortable
|
||||
:edit-render="{ autoFocus: true }"
|
||||
:fixed="column.fixed"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<ElCheckbox
|
||||
v-if="column.type === 'checkbox'"
|
||||
true-value="1"
|
||||
false-value="0"
|
||||
:checked="row[column.prop] === '1'"
|
||||
/>
|
||||
<template v-else>
|
||||
{{ row[column.prop] }}
|
||||
</template>
|
||||
</template>
|
||||
<template #edit="{ row }">
|
||||
<ElCheckbox
|
||||
v-if="column.type === 'checkbox'"
|
||||
v-model="row[column.prop]"
|
||||
true-value="1"
|
||||
false-value="0"
|
||||
/>
|
||||
<el-select
|
||||
v-else-if="column.type === 'option'"
|
||||
v-model="row[column.prop]"
|
||||
size="small"
|
||||
placeholder=" "
|
||||
>
|
||||
<el-option
|
||||
v-for="item in getOptions(column.prop)"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<ElInput
|
||||
v-else-if="column.type === 'input'"
|
||||
v-model="row[column.prop]"
|
||||
size="small"
|
||||
/>
|
||||
<template v-else>
|
||||
{{ row[column.prop] }}
|
||||
</template>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-splitter-panel>
|
||||
</el-splitter>
|
||||
|
||||
<PreviewSql v-model="showSql" :data="tableData" :viewtable="currentModule?.b_ViewTable" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "@/styles/mixins.scss" as *;
|
||||
|
||||
.g3-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
:deep(.el-splitter-bar__dragger-horizontal::after),
|
||||
:deep(.el-splitter-bar__dragger-horizontal::before) {
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
:deep(.el-splitter-bar__dragger::after),
|
||||
:deep(.el-splitter-bar__dragger::before) {
|
||||
background-color: #f2f6fc;
|
||||
}
|
||||
}
|
||||
|
||||
.g3-wrapper-left {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
|
||||
.tree-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 40px;
|
||||
padding: 0 10px;
|
||||
border-bottom: 1px solid #f2f6fc;
|
||||
}
|
||||
.tree-options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
list-style: none;
|
||||
margin-left: 5px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
color: #333;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tree-container {
|
||||
height: calc(100% - 40px);
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.g3-wrapper-right {
|
||||
flex: 1;
|
||||
transition: width 0.3s;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #fff;
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 40px;
|
||||
padding: 0 10px;
|
||||
border-bottom: 1px solid #f2f6fc;
|
||||
flex-shrink: 0;
|
||||
.options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
list-style: none;
|
||||
margin-left: 5px;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
color: #333;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.item-bg {
|
||||
cursor: pointer;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
|
||||
.content-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.content-head-left {
|
||||
display: flex;
|
||||
@include flexGapWrap(4px, 8px);
|
||||
}
|
||||
|
||||
.content-head-right {
|
||||
display: flex;
|
||||
@include flexGapWrap(4px, 8px);
|
||||
flex-wrap: nowrap;
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.content-table {
|
||||
margin-top: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Whitespace-only changes.
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<el-dialog v-bind="$attrs" title="Sql预览" width="1000px" class="dialog" @open="initData">
|
||||
<div class="sql-preview">
|
||||
{{ sqlStr }}
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: null,
|
||||
},
|
||||
viewtable: {
|
||||
type: null,
|
||||
},
|
||||
});
|
||||
|
||||
const sqlStr = ref("");
|
||||
|
||||
function initData() {
|
||||
if (!props.viewtable) return;
|
||||
|
||||
const vrows = props.data || [];
|
||||
|
||||
const sqlArr = [];
|
||||
|
||||
// 删除语句
|
||||
const deleteSql = `DELETE FROM s_columnlib WHERE col_TableName='${props.viewtable}';`;
|
||||
sqlArr.push(deleteSql);
|
||||
|
||||
// 插入语句
|
||||
vrows.forEach((row) => {
|
||||
const insertSql =
|
||||
"INSERT INTO s_columnlib(col_TableName,col_FieldName,col_Caption,col_Width,col_Visible,col_Position," +
|
||||
"col_ShowSummary,col_BandIndex,col_AllowNull,col_AllowRepeat,col_DefaultValue,col_ReadOnly,col_Color," +
|
||||
"col_CanSearch,col_SearchOrder,col_NeedReserve,col_sorted,col_DisplayFormat,col_type,col_datatype,col_foot)";
|
||||
|
||||
const valueSql =
|
||||
`VALUES('${props.viewtable}', '${row.col_FieldName}', '${row.col_Caption}', '${row.col_Width}', '${row.col_Visible}', '${row.col_Position}', ` +
|
||||
`'${row.col_ShowSummary}', '${row.col_BandIndex}', '${row.col_AllowNull}', '${row.col_AllowRepeat}', '${row.col_DefaultValue}', '${row.col_ReadOnly}', '${row.col_Color}', ` +
|
||||
`'${row.col_CanSearch}', '${row.col_SearchOrder}', '${row.col_NeedReserve}', '${row.col_sorted}', '${row.col_DisplayFormat}', '${row.col_type}', '${row.col_datatype}', '${row.col_foot}');`;
|
||||
|
||||
sqlArr.push(insertSql);
|
||||
sqlArr.push(valueSql);
|
||||
});
|
||||
|
||||
// 合并为字符串
|
||||
sqlStr.value = sqlArr.join("\n");
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-content {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
.sql-preview {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
font-family: monospace;
|
||||
padding: 8px;
|
||||
border: 1px solid #e0e0e0;
|
||||
background-color: #fafafa;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="js" setup></script>
|
||||
|
||||
<template>
|
||||
<div class=""></div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script lang="js" setup>
|
||||
const router = useRouter();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="">
|
||||
test-detail
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="js" setup></script>
|
||||
|
||||
<template>
|
||||
<div class=""></div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,102 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>自适应 Descriptions 每行自动填满</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 每行一个 grid 容器 */
|
||||
.row {
|
||||
display: grid;
|
||||
border-top: 1px solid #ccc;
|
||||
border-left: 1px solid #ccc;
|
||||
margin-bottom: 10px;
|
||||
gap: 0;
|
||||
/* 紧凑像表格 */
|
||||
}
|
||||
|
||||
/* item 样式 */
|
||||
.item {
|
||||
display: flex;
|
||||
border-right: 1px solid #ccc;
|
||||
border-bottom: 1px solid #ccc;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: 0 0 100px;
|
||||
/* label 固定宽度 */
|
||||
background-color: #e0f7fa;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.value {
|
||||
flex: 1;
|
||||
background-color: #fff9c4;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 响应式:小屏幕每个 item 占满整行 */
|
||||
@media (max-width: 500px) {
|
||||
.row {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<h2>自适应 Descriptions 示例</h2>
|
||||
|
||||
<!-- 第一行 2 列 -->
|
||||
<div class="row" style="grid-template-columns: repeat(2, 1fr);">
|
||||
<div class="item">
|
||||
<span class="label">姓名:</span>
|
||||
<span class="value">张三</span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<span class="label">年龄:</span>
|
||||
<span class="value">18</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第二行 1 列 -->
|
||||
<div class="row" style="grid-template-columns: repeat(1, 1fr);">
|
||||
<div class="item">
|
||||
<span class="label">邮箱:</span>
|
||||
<span class="value">zhangsan@example.com</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第三行 3 列 -->
|
||||
<div class="row" style="grid-template-columns: repeat(3, 1fr);">
|
||||
<div class="item">
|
||||
<span class="label">城市:</span>
|
||||
<span class="value">北京</span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<span class="label">职业:</span>
|
||||
<span class="value">学生</span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<span class="label">爱好:</span>
|
||||
<span class="value">篮球</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in new issue
Block a user