This commit is contained in:
BianLzhaoMin 2026-02-02 15:41:05 +08:00
parent 8076c143fb
commit 75ed7e7e7f
51 changed files with 13612 additions and 116 deletions

8
.prettierrc.json Normal file
View File

@ -0,0 +1,8 @@
{
"tabWidth": 4,
"singleQuote": true,
"semi": false,
"printWidth": 100,
"trailingComma": "all",
"endOfLine": "auto"
}

View File

@ -0,0 +1,75 @@
# 博诺思管理系统
基于若依框架和 Element Plus 组件库构建的企业级组件管理系统。
## 环境要求
- Node.js 18.0 或更高版本
- npm 或 yarn
## 快速开始
### 1. 安装依赖
```bash
npm install
```
### 2. 启动开发服务器
```bash
npm run dev
```
项目将在 `http://localhost:80` 启动
## 项目说明
### Mock 数据
项目已集成 Mock 数据模拟,无需后端即可进行开发和测试:
- **登录接口**:任意用户名密码即可登录(验证码输入任意 4 位数字)
- **菜单数据**:自动加载模拟菜单数据
- Mock 数据位于 `mock/` 目录
### 组件库
基于 Element Plus 进行二次封装,当前已封装:
- **ComButton**:按钮组件(示例)
- 更多组件持续封装中...
## 常用命令
```bash
# 开发环境
npm run dev
# 生产构建
npm run build:prod
# 预览构建结果
npm run preview
```
## 技术栈
- Vue 3 + Vite
- Element Plus
- Pinia
- Vue Router
- Axios
## 项目结构
```
├── mock/ # Mock 数据
├── src/
│ ├── api/ # API 接口
│ ├── components/ # 公共组件
│ ├── views/ # 页面视图
│ ├── router/ # 路由配置
│ └── store/ # 状态管理
└── vite.config.js # Vite 配置
```

View File

@ -23,7 +23,37 @@
"name": "showText",
"meta": {
"title": "文本组件",
"icon": "text"
"icon": "table"
}
},
{
"name": "ShowTable",
"path": "showTable",
"hidden": false,
"component": "showComponents/showTable/index",
"meta": {
"title": "表格组件",
"icon": "table",
"noCache": false,
"link": null
}
},
{
"path": "showMap",
"component": "showComponents/showMap/index",
"name": "showMap",
"meta": {
"title": "地图组件",
"icon": "table"
}
},
{
"path": "showDialog",
"component": "showComponents/showDialog/index",
"name": "showDialog",
"meta": {
"title": "弹窗组件",
"icon": "table"
}
}
]

View File

@ -17,6 +17,7 @@
},
"dependencies": {
"@element-plus/icons-vue": "2.3.1",
"@turf/turf": "^7.3.1",
"@vueup/vue-quill": "1.2.0",
"@vueuse/core": "13.3.0",
"axios": "1.9.0",
@ -29,22 +30,27 @@
"js-cookie": "3.0.5",
"jsencrypt": "3.3.2",
"lodash-es": "^4.17.21",
"mars3d": "^3.10.11",
"mars3d-cesium": "^1.136.0",
"nprogress": "0.2.0",
"pinia": "3.0.2",
"splitpanes": "4.0.4",
"vue": "3.5.16",
"vue-cropper": "1.1.1",
"vue-draggable-plus": "^0.6.0",
"vue-router": "4.5.1",
"vuedraggable": "4.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "5.2.4",
"less": "^4.5.1",
"mockjs": "^1.1.0",
"sass-embedded": "1.89.1",
"unplugin-auto-import": "0.18.6",
"unplugin-vue-setup-extend-plus": "1.0.1",
"vite": "6.3.5",
"vite-plugin-compression": "0.5.1",
"vite-plugin-mars3d": "^4.2.2",
"vite-plugin-mock": "^3.0.2",
"vite-plugin-svg-icons": "2.0.1"
},

BIN
src/assets/3d/tower_1.glb Normal file

Binary file not shown.

BIN
src/assets/3d/tower_2.glb Normal file

Binary file not shown.

View File

@ -0,0 +1,12 @@
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="800.000000" height="800.000000" viewBox="0 0 800.000000 800.000000" preserveAspectRatio="xMidYMid meet">
<metadata>
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#" xmlns:dc="http://purl.org/dc/elements/1.1/">
<rdf:Description dc:format="image/svg+xml" dc:Label="1" dc:ContentProducer="001191330110MACRLGPT8B00000" dc:ProduceID="315971907" dc:ReservedCode1="jA7ILZwlq0Mdcs79qlecSqexLoT20IvdYh3YS5kXY7g=" dc:ContentPropagator="001191330110MACRLGPT8B00000" dc:PropagateID="315971907" dc:ReservedCode2="jA7ILZwlq0Mdcs79qlecSqexLoT20IvdYh3YS5kXY7g="/>
</rdf:RDF>
</metadata>
<g transform="translate(-160.689798,962.759061) scale(0.139993,-0.139993)" fill="#000000" stroke="none">
<path d="M2669 5489 c-31 -33 -39 -82 -19 -120 28 -51 -30 -49 1355 -49 1385 0 1327 -2 1355 49 20 39 12 91 -19 122 l-29 29 -1307 0 -1307 0 -29 -31z"/>
<path d="M3772 4868 c-107 -106 -200 -207 -207 -223 -18 -41 -8 -80 27 -110 56 -47 78 -38 205 87 l113 112 0 -719 0 -720 -115 115 c-104 104 -117 114 -150 113 -41 -1 -60 -14 -82 -56 -28 -56 -15 -77 184 -284 103 -107 201 -202 216 -210 60 -31 83 -16 266 170 93 95 183 189 200 211 39 49 41 93 5 135 -21 25 -33 31 -67 31 -40 0 -46 -4 -149 -112 l-108 -113 0 715 0 715 108 -108 c123 -122 145 -131 204 -78 26 23 33 36 33 69 0 38 -9 48 -152 198 -228 240 -244 254 -294 254 -42 0 -46 -3 -237 -192z"/>
<path d="M2667 2692 c-52 -54 -29 -142 42 -162 47 -13 2559 -14 2594 -1 71 27 91 114 39 164 l-28 27 -1310 0 -1310 0 -27 -28z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -13,88 +13,90 @@
:disabled="disabled"
:autofocus="autofocus"
@click="onButtonClick"
v-bind="omit(attrs, ['onClick'])">
v-bind="omit(attrs, ['onClick'])"
>
<slot></slot>
</el-button>
</template>
<script setup name="ComButton">
import { ref, useAttrs } from "vue";
import { omit } from "lodash-es"; //
const loading = ref(false); //
const attrs = useAttrs(); //
import { ref, useAttrs } from 'vue'
import { omit } from 'lodash-es' //
const loading = ref(false) //
const attrs = useAttrs() //
//
defineOptions({
inheritAttrs: false,
});
//
defineOptions({
inheritAttrs: false,
})
//
defineProps({
//
type: {
type: String,
default: "primary",
},
//
icon: {
type: String,
default: "",
},
//
size: {
type: String,
default: "default",
},
//
link: {
type: Boolean,
default: false,
},
//
style: {
type: Object,
default: {},
},
//
round: {
type: Boolean,
default: false,
},
//
plain: {
type: Boolean,
default: false,
},
//
circle: {
type: Boolean,
default: false,
},
//
disabled: {
type: Boolean,
default: false,
},
//
autofocus: {
type: Boolean,
default: false,
},
});
//
defineProps({
//
type: {
type: String,
default: 'primary',
},
//
//
const onButtonClick = async () => {
// loading
loading.value = true;
try {
//
await attrs.onClick?.();
} catch (error) {
console.error(error);
} finally {
// loading
loading.value = false;
}
};
icon: {
type: String,
default: '',
},
//
size: {
type: String,
default: 'default',
},
//
link: {
type: Boolean,
default: false,
},
//
style: {
type: Object,
default: {},
},
//
round: {
type: Boolean,
default: false,
},
//
plain: {
type: Boolean,
default: false,
},
//
circle: {
type: Boolean,
default: false,
},
//
disabled: {
type: Boolean,
default: false,
},
//
autofocus: {
type: Boolean,
default: false,
},
})
//
const onButtonClick = async () => {
// loading
loading.value = true
try {
//
await attrs.onClick?.()
} catch (error) {
console.error(error)
} finally {
// loading
loading.value = false
}
}
</script>

View File

@ -0,0 +1,239 @@
<template>
<el-dialog
v-model="visible"
title="列设置"
width="600px"
:append-to-body="true"
@close="handleClose"
>
<div class="column-setting">
<div class="column-setting-header">
<el-checkbox
:indeterminate="isIndeterminate"
:model-value="isAllChecked"
@change="handleToggleAll"
>
列展示
</el-checkbox>
<el-button type="primary" link @click="handleReset">重置</el-button>
</div>
<div class="column-list">
<draggable
v-model="localColumns"
:animation="200"
handle=".drag-handle"
item-key="prop"
@end="handleDragEnd"
>
<template #item="{ element: column }">
<div
class="column-item"
:class="{
'is-fixed-left': column.fixed === 'left',
'is-fixed-right': column.fixed === 'right',
}"
>
<div class="column-item-left">
<el-icon class="drag-handle"><Sort /></el-icon>
<el-checkbox v-model="column.visible" @change="handleColumnChange">
{{ column.label }}
</el-checkbox>
</div>
<div class="column-item-right">
<el-button
:type="column.fixed === 'left' ? 'primary' : 'info'"
:plain="column.fixed !== 'left'"
link
size="small"
@click="handleFixColumn(column, 'left')"
>
<el-icon><ArrowLeft /></el-icon>
</el-button>
<el-button
:type="column.fixed === 'right' ? 'primary' : 'info'"
:plain="column.fixed !== 'right'"
link
size="small"
@click="handleFixColumn(column, 'right')"
>
<el-icon><ArrowRight /></el-icon>
</el-button>
</div>
</div>
</template>
</draggable>
</div>
</div>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleConfirm">确定</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import draggable from 'vuedraggable'
import { ArrowLeft, ArrowRight, Sort } from '@element-plus/icons-vue'
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
columns: {
type: Array,
default: () => [],
},
})
const emit = defineEmits(['update:modelValue', 'confirm'])
const visible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
})
const localColumns = ref([])
const originalColumns = ref([])
//
const initColumns = () => {
localColumns.value = props.columns.map((col) => ({
...col,
visible: col.visible !== false,
fixed: col.fixed || false,
}))
originalColumns.value = JSON.parse(JSON.stringify(localColumns.value))
}
//
const isAllChecked = computed(() => {
return localColumns.value.length > 0 && localColumns.value.every((col) => col.visible)
})
//
const isIndeterminate = computed(() => {
const checkedCount = localColumns.value.filter((col) => col.visible).length
return checkedCount > 0 && checkedCount < localColumns.value.length
})
//
const handleToggleAll = (val) => {
localColumns.value.forEach((col) => {
col.visible = val
})
}
//
const handleColumnChange = () => {
//
}
//
const handleFixColumn = (column, position) => {
if (column.fixed === position) {
column.fixed = false
} else {
column.fixed = position
}
}
//
const handleDragEnd = () => {
// localColumns
}
//
const handleReset = () => {
localColumns.value = JSON.parse(JSON.stringify(originalColumns.value))
}
//
const handleConfirm = () => {
emit('confirm', localColumns.value)
visible.value = false
}
//
const handleClose = () => {
visible.value = false
}
// visible
watch(
() => props.modelValue,
(val) => {
if (val) {
initColumns()
}
},
{ immediate: true },
)
</script>
<style scoped>
.column-setting {
padding: 10px 0;
}
.column-setting-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 12px;
margin-bottom: 12px;
border-bottom: 1px solid #ebeef5;
}
.column-list {
max-height: 400px;
overflow-y: auto;
}
.column-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
margin-bottom: 8px;
background: #f5f7fa;
border-radius: 4px;
transition: all 0.3s;
}
.column-item:hover {
background: #ecf5ff;
}
.column-item.is-fixed-left {
border-left: 3px solid #409eff;
}
.column-item.is-fixed-right {
border-right: 3px solid #409eff;
}
.column-item-left {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
.drag-handle {
cursor: move;
color: #909399;
font-size: 16px;
margin-right: 4px;
}
.drag-handle:hover {
color: #409eff;
}
.column-item-right {
display: flex;
gap: 8px;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,157 @@
<template>
<!-- 通用弹框组件支持外层 + 内层 -->
<div class="com-dialog">
<!-- 外层弹框 -->
<el-dialog
v-if="dialogConfig.outerVisible"
v-model="dialogConfig.outerVisible"
:title="dialogConfig.outerTitle"
:width="dialogConfig.outerWidth || '720px'"
:before-close="handleCloseOuter"
:append-to-body="true"
:close-on-click-modal="false"
:style="{
'--com-dialog-min-height': dialogConfig.minHeight || '320px',
'--com-dialog-max-height': dialogConfig.maxHeight || '80vh',
}"
class="com-dialog__outer"
>
<!-- 外层弹框内容插槽 -->
<slot name="outerContent" />
<!-- 内层弹框 -->
<el-dialog
v-if="dialogConfig.innerVisible"
v-model="dialogConfig.innerVisible"
:title="dialogConfig.innerTitle"
:width="dialogConfig.innerWidth || '640px'"
:before-close="handleCloseInner"
:append-to-body="true"
:close-on-click-modal="false"
:style="{
'--com-dialog-min-height': dialogConfig.innerMinHeight || '260px',
'--com-dialog-max-height': dialogConfig.innerMaxHeight || '70vh',
}"
class="com-dialog__inner"
>
<!-- 内层弹框内容插槽 -->
<slot name="innerContent" />
</el-dialog>
</el-dialog>
</div>
</template>
<script setup>
const props = defineProps({
//
dialogConfig: {
type: Object,
default: () => ({}),
},
})
const emit = defineEmits(['closeDialogOuter', 'closeDialogInner'])
//
const handleCloseOuter = () => {
emit('closeDialogOuter', false)
}
//
const handleCloseInner = () => {
emit('closeDialogInner', false)
}
</script>
<style lang="scss">
/* 全局样式:仅作用于带 com-dialog__outer / com-dialog__inner 类名的弹框 */
/* 外层 + 内层弹框通用外观 */
.el-dialog:not(.is-fullscreen) {
margin-top: 0 !important;
}
.com-dialog__outer,
.com-dialog__inner {
display: flex;
flex-direction: column;
margin: 0 !important;
position: absolute;
top: 50% !important;
left: 50%;
transform: translate(-50%, -50%) !important;
min-height: var(--com-dialog-min-height);
max-height: var(--com-dialog-max-height);
border-radius: 16px;
overflow: hidden;
box-shadow: 0 22px 60px rgba(15, 23, 42, 0.35);
background: #ffffff;
}
/* 头部区域:浅色背景 + 左侧色条 */
.com-dialog__outer .el-dialog__header,
.com-dialog__inner .el-dialog__header {
position: relative;
padding: 12px 18px;
margin: 0;
border-bottom: 1px solid #edf0f5;
background: #f8f9fd;
color: #1f2937;
display: flex;
align-items: center;
justify-content: space-between;
}
.com-dialog__outer .el-dialog__header::before,
.com-dialog__inner .el-dialog__header::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
border-radius: 0 4px 4px 0;
background: linear-gradient(180deg, #1677ff 0%, #69b1ff 100%);
}
.com-dialog__outer .el-dialog__title,
.com-dialog__inner .el-dialog__title {
font-size: 15px;
font-weight: 600;
letter-spacing: 0.02em;
color: #111827;
}
.com-dialog__outer .el-dialog__headerbtn,
.com-dialog__inner .el-dialog__headerbtn {
top: 4px;
right: 16px;
.el-dialog__close {
color: #4b5563; /* 默认深灰,保证在浅色标题背景下清晰可见 */
transition: transform 0.2s ease, color 0.2s ease;
}
&:hover .el-dialog__close {
color: #111827;
transform: scale(1.05);
}
}
/* 内容区域 */
.com-dialog__outer .el-dialog__body,
.com-dialog__inner .el-dialog__body {
flex: 1;
overflow-y: auto;
padding: 18px 20px 20px;
box-sizing: border-box;
background-color: #fafbff;
}
/* 底部区域 */
.com-dialog__outer .el-dialog__footer,
.com-dialog__inner .el-dialog__footer {
padding: 12px 20px 16px;
border-top: 1px solid #eef1f6;
background-color: #ffffff;
}
</style>

View File

@ -0,0 +1,45 @@
import * as mars3d from "mars3d"
// 通过Canvas绘制复杂或动态对象的图标点Graphic
export class CanvasBillboard extends mars3d.graphic.BillboardPrimitive {
/**
* 文字
* @type {string}
*/
get text() {
return this.style.text
}
set text(val) {
this.style.text = val
this.label.text = val
}
/**
* 对象添加到图层前创建一些对象的钩子方法
* 只会调用一次
* @return {Promise<object>}
* @private
*/
_addedHook(style) {
style.image = "//data.mars3d.cn/img/marker/bg/textPnl.png"
style.label = {
...style,
text: this.style.text,
font_size: 55,
color: style.textColor ?? "#ffffff",
hasPixelOffset: true,
pixelOffsetX: 0,
pixelOffsetY: -36 * (style.scale ?? 1)
}
if (style.scaleByDistance) {
style.label.pixelOffsetScaleByDistance = style.scaleByDistance
}
super._addedHook(style)
}
}
// 注册下
mars3d.GraphicUtil.register("canvasBillboard", CanvasBillboard)

View File

@ -0,0 +1,18 @@
// 注册mars3d插件 (插件清单访问http://mars3d.cn/dev/guide/start/architecture.html)
// import "mars3d-space"
// import "mars3d-heatmap"
// import "mars3d-echarts"
// import "mars3d-mapv"
// import "mars3d-tdt"
// 注册mars3d继承的相关类
import './task/CameraList.js'
import './task/CameraView.js'
import './task/MapRotate.js'
import './task/PointRotate.js'
import './task/RouteLine.js'
import './task/ZoomIn.js'
import './task/ZoomOut.js'
import './task/FlickerEntity.js'
import './graphic/CanvasBillboard.js'

View File

@ -0,0 +1,24 @@
import * as mars3d from "mars3d"
/**
* 视角列表播放分步执行
*
* @param {object} [options] 参数对象包括以下
* @param {string} [options.name] 标题名称
* @param {string} [options.type] 类型标识自动赋值的无需手动传入
* @param {number} [options.start] 开始时间相当于map.clock.startTime的秒数
* @param {number} [options.duration] 时长
* @param {object[]} [options.list] 视角数组
*/
export class CameraList extends mars3d.TaskItem {
// 进入,激活开始处理事务
_activateWork() {
this._map.setCameraViewList(this.options.list)
}
// 暂停(非必须)
_pauseWork() {
this._disableWork()
}
}
mars3d.thing.Task.register("cameraList", CameraList)

View File

@ -0,0 +1,25 @@
import * as mars3d from "mars3d"
/**
* 单个视角定位
*
* @param {object} [options] 参数对象包括以下
* @param {string} [options.name] 标题名称
* @param {string} [options.type] 类型标识自动赋值的无需手动传入
* @param {number} [options.start] 开始时间相当于map.clock.startTime的秒数
* @param {number} [options.duration] 时长
*
* @param {object} [options.center] 视角参数
*/
export class CameraView extends mars3d.TaskItem {
// 进入,激活开始处理事务
_activateWork() {
this._map.setCameraView(this.options.center, { duration: this._duration })
}
// 离开,释放相关对象
_disableWork() {
this._map.cancelFlyTo()
}
}
mars3d.thing.Task.register("camera", CameraView)

View File

@ -0,0 +1,47 @@
import * as mars3d from "mars3d"
/**
* 矢量对象高亮闪烁(仅Entity)
*
* @param {object} [options] 参数对象包括以下
* @param {string} [options.name] 标题名称
* @param {string} [options.type] 类型标识自动赋值的无需手动传入
* @param {number} [options.start] 开始时间相当于map.clock.startTime的秒数
* @param {number} [options.duration] 时长
*
* @param {string|number} [options.layerId] 图层ID
* @param {string|number} [options.graphicId] 矢量对象ID
*
* @param {number} [options.step=10] 闪烁增量, 控制速度
* @param {string} [options.color] 高亮的颜色
* @param {number} [options.maxAlpha=0.3] 闪烁的最大透明度 0 maxAlpha 渐变
*/
export class FlickerEntity extends mars3d.TaskItem {
// 进入,激活开始处理事务
_activateWork() {
const layer = this._map.getLayerById(this.options.layerId)
if (layer) {
layer.show = true
layer.readyPromise.then(() => {
this._graphic = layer.getGraphicById(this.options.graphicId)
if (this._graphic) {
this._graphic.show = true
this._graphic.startFlicker({
time: this._duration,
step: this.options.step,
maxAlpha: this.options.maxAlpha,
color: this.options.color
})
}
})
}
}
// 离开,释放相关对象
_disableWork() {
if (this._graphic) {
this._graphic.stopFlicker()
}
}
}
mars3d.thing.Task.register("flickerEntity", FlickerEntity)

View File

@ -0,0 +1,46 @@
import * as mars3d from "mars3d"
/**
* 地球自旋转
*
* @param {object} [options] 参数对象包括以下
* @param {string} [options.name] 标题名称
* @param {string} [options.type] 类型标识自动赋值的无需手动传入
* @param {number} [options.start] 开始时间相当于map.clock.startTime的秒数
* @param {number} [options.duration] 时长
*
* @param {object} [options.center] 初始视角
* @param {number} [options.speed] 旋转速度
*/
export class MapRotate extends mars3d.TaskItem {
constructor(options = {}) {
super(options)
this._speed = this.options.speed || 0.01
this._center = this.options.center || { lat: 29.093038, lng: 108.804459, alt: 23321232.7, heading: 0, pitch: -90 }
}
// 进入,激活开始处理事务
_activateWork() {
this._map.setCameraView(this._center, {
duration: 1,
complete: () => {
this._map.on(mars3d.EventType.clockTick, this._map_onClockTick, this)
}
})
}
// 离开,释放相关对象
_disableWork() {
this._map.off(mars3d.EventType.clockTick, this._map_onClockTick, this)
}
_map_onClockTick() {
if (this.isPause) {
return // 暂停时不执行
}
this._map.scene.camera.rotate(mars3d.Cesium.Cartesian3.UNIT_Z, this._speed)
}
}
mars3d.thing.Task.register("mapRotate", MapRotate)

View File

@ -0,0 +1,58 @@
import * as mars3d from "mars3d"
/**
* 内或外旋转
* @param {object} [options] 参数对象包括以下
* @param {string} [options.name] 标题名称
* @param {string} [options.type] 类型标识自动赋值的无需手动传入
* @param {number} [options.start] 开始时间相当于map.clock.startTime的秒数
* @param {number} [options.duration] 时长
*
* @param {boolean} [options.isRotateOut] true:绕外旋转 ,false:绕内旋转
* @param {boolean} [options.direction=false] 旋转方向, true逆时针false顺时针
* @param {number} [options.time=60] 飞行一周所需时间(单位 )控制速度
* @param {boolean} [options.autoStop] 是否自动停止
* @param {number} [options.autoStopAngle] 自动停止的角度值0-360未设置时不自动停止
* @param {object} [options.point] 绕点旋转对应的中心点位置
*/
export class PointRotate extends mars3d.TaskItem {
// 进入,激活开始处理事务
_activateWork() {
if (this.options.center) {
this._map.setCameraView(this.options.center, { duration: 0 })
}
if (this.options.autoStop) {
delete this.options.autoStopAngle // 是否自动停止
}
if (this.options.isRotateOut) {
this._rotateOut = new mars3d.thing.RotateOut(this.options)
this._map.addThing(this._rotateOut)
} else {
this._rotatePoint = new mars3d.thing.RotatePoint(this.options)
this._map.addThing(this._rotatePoint)
}
if (this.options.isRotateOut) {
this._rotateOut.start()
} else {
this._rotatePoint.start(this.options.point)
}
}
// 离开,释放相关对象
_disableWork() {
if (this._rotatePoint) {
this._rotatePoint.stop()
this._rotatePoint.destroy()
delete this._rotatePoint
}
if (this._rotateOut) {
this._rotateOut.stop()
this._rotateOut.destroy()
delete this._rotateOut
}
}
}
mars3d.thing.Task.register("pointRotate", PointRotate)

View File

@ -0,0 +1,58 @@
import * as mars3d from "mars3d"
/**
* 按路线漫游
*
* @param {object} [options] 参数对象包括以下
* @param {string} [options.name] 标题名称
* @param {string} [options.type] 类型标识自动赋值的无需手动传入
* @param {number} [options.start] 开始时间相当于map.clock.startTime的秒数
* @param {number} [options.duration] 时长
*
* @param {object} [options.route] FixedRoute对应的构造参数
*/
export class RouteLine extends mars3d.TaskItem {
// constructor(options) {
// super(options)
// }
// 进入,激活开始处理事务
_activateWork() {
this._graphicLayer = new mars3d.layer.GraphicLayer()
this._map.addLayer(this._graphicLayer)
const fixedRoute = new mars3d.graphic.FixedRoute(this.options.route)
this._graphicLayer.addGraphic(fixedRoute)
this._fixedRoute = fixedRoute
fixedRoute.start()
}
// 暂停(非必须)
_pauseWork(options) {
if (this._fixedRoute) {
this._fixedRoute.pause()
}
}
// 继续(非必须)
_proceedWork() {
if (this._fixedRoute) {
this._fixedRoute.proceed()
}
}
// 离开,释放相关对象
_disableWork() {
if (this._fixedRoute) {
this._fixedRoute.stop()
delete this._fixedRoute
}
if (this._graphicLayer) {
this._graphicLayer.destroy()
delete this._graphicLayer
}
}
}
mars3d.thing.Task.register("routeLine", RouteLine)

View File

@ -0,0 +1,20 @@
import * as mars3d from "mars3d"
/**
* 放大地图
*
* @param {object} [options] 参数对象包括以下
* @param {string} [options.name] 标题名称
* @param {string} [options.type] 类型标识自动赋值的无需手动传入
* @param {number} [options.start] 开始时间相当于map.clock.startTime的秒数
* @param {number} [options.duration] 时长
*
* @param {number} [options.relativeAmount=2] 相对量
*/
export class ZoomIn extends mars3d.TaskItem {
// 进入,激活开始处理事务
_activateWork() {
this._map.zoomIn(this.options.relativeAmount)
}
}
mars3d.thing.Task.register("zoomIn", ZoomIn)

View File

@ -0,0 +1,20 @@
import * as mars3d from "mars3d"
/**
* 缩小地图
*
* @param {object} [options] 参数对象包括以下
* @param {string} [options.name] 标题名称
* @param {string} [options.type] 类型标识自动赋值的无需手动传入
* @param {number} [options.start] 开始时间相当于map.clock.startTime的秒数
* @param {number} [options.duration] 时长
*
* @param {number} [options.relativeAmount=2] 相对量
*/
export class ZoomOut extends mars3d.TaskItem {
// 进入,激活开始处理事务
_activateWork() {
this._map.zoomOut(this.options.relativeAmount)
}
}
mars3d.thing.Task.register("zoomOut", ZoomOut)

View File

@ -0,0 +1,434 @@
<template>
<div :id="withKeyId" class="mars3d-container"></div>
</template>
<script setup>
import * as mars3d from 'mars3d'
import './expand/index' // js
import { computed, onUnmounted, onMounted, toRaw } from 'vue'
// import { $alert, $message } from "@mars/components/mars-ui/index"
const props = defineProps({
mapKey: {
type: String,
default: 'default',
}, // ,key
url: {
type: String,
default: undefined,
}, // urloptions
options: {
type: Object,
default: undefined,
}, // optionsurl
})
//
let map //
// 使 mapKey withKeyId id
const withKeyId = computed(() => `mars3d-container-${props.mapKey}`)
// onload
const emit = defineEmits(['onload'])
const initMars3d = async () => {
//
let mapOptions
if (props.url) {
// url
mapOptions = await mars3d.Util.fetchJson({ url: props.url })
if (props.options) {
mapOptions = mars3d.Util.merge(mapOptions, toRaw(props.options)) //
}
} else if (props.options) {
mapOptions = toRaw(props.options)
}
console.log('Map地图构造参数', mapOptions)
map = new mars3d.Map(withKeyId.value, mapOptions)
map.flyToPoint([106.898417, 33.685833], {
radius: 5000, //
duration: 3, //
heading: 0, // 0-360
pitch: -45, // -90090
})
//
if (mars3d.Util.isPCBroswer()) {
map.zoomFactor = 2.0 //
// IE
if (window.navigator.userAgent.toLowerCase().indexOf('msie') >= 0) {
map.viewer.targetFrameRate = 20 //
map.scene.requestRenderMode = false //
}
} else {
map.zoomFactor = 5.0 //
//
map.scene.requestRenderMode = false //
map.scene.fog.enabled = false
map.scene.skyAtmosphere.show = false
map.scene.globe.showGroundAtmosphere = false
}
//
if (map.viewer.sceneModePicker) {
map.viewer.sceneModePicker.viewModel.duration = 0.0
}
//
// map.bindContextMenu(getContextMenu())
// webgl
// map.on(mars3d.EventType.renderError, async () => {
// await $alert("")
// window.location.reload()
// })
onMapLoad() // map
emit('onload', map)
}
// map
function onMapLoad() {
// Mars3D使
// window.globalAlert = $alert;
// window.globalMsg = $message;
}
onMounted(() => {
initMars3d()
})
// mars3d
onUnmounted(() => {
if (map) {
map.destroy()
map = null
}
console.log('map销毁完成', map)
})
</script>
<style lang="less">
/**cesium 工具按钮栏*/
.cesium-viewer-toolbar {
top: auto !important;
bottom: 35px !important;
left: 12px !important;
right: auto !important;
}
.cesium-toolbar-button img {
width: 22px;
height: 100%;
}
.cesium-toolbar-button:hover img {
width: 28px;
}
.cesium-svgPath-svg {
scale: 0.8;
}
.cesium-svgPath-svg:hover {
scale: 1;
}
.cesium-button .cesium-baseLayerPicker-selected {
width: 100%;
}
.cesium-button:hover .cesium-baseLayerPicker-selected {
width: 100%;
}
.cesium-viewer-toolbar > .cesium-toolbar-button,
.cesium-navigationHelpButton-wrapper,
.cesium-viewer-geocoderContainer {
margin-bottom: 5px;
float: left;
clear: both;
text-align: center;
}
.cesium-viewer-geocoderContainer form .cesium-geocoder-input {
border-width: 1px;
border-image: url('//data.mars3d.cn/img/control/border.svg') 1 round stretch;
}
.cesium-button {
background-color: rgba(39, 44, 54, 0.8);
border-radius: 2px;
border-width: 1px;
border-image: url('//data.mars3d.cn/img/control/border.svg') 1 round stretch;
color: #ffffff;
fill: #e6e6e6;
line-height: 38px;
}
.cesium-button:hover {
background-color: rgba(51, 133, 255, 1);
box-shadow: none;
border: none;
}
/**cesium 底图切换面板*/
.cesium-baseLayerPicker-dropDown {
bottom: 0;
left: 40px;
max-height: 700px;
margin-bottom: 5px;
background-color: rgba(23, 49, 71, 0.7);
}
/**cesium 帮助面板*/
.cesium-navigation-help {
top: auto;
bottom: 0;
left: 40px;
transform-origin: left bottom;
background: none;
background-color: rgba(23, 49, 71, 0.8);
.cesium-navigation-help-instructions,
.cesium-navigation-button {
background: none;
}
.cesium-navigation-button-selected,
.cesium-navigation-button-unselected:hover {
background-color: rgba(1, 35, 22, 1);
}
}
/**cesium 二维三维切换*/
.cesium-sceneModePicker-wrapper {
width: auto;
}
.cesium-sceneModePicker-wrapper .cesium-sceneModePicker-dropDown-icon {
float: right;
margin: 0 3px;
}
/**cesium POI查询输入框*/
.cesium-viewer-geocoderContainer .search-results {
left: 0;
right: 40px;
width: auto;
z-index: 9999;
}
.cesium-geocoder-searchButton {
width: 38px;
height: 38px;
background-color: rgba(39, 44, 54, 0.8);
border-radius: 2px;
border-width: 1px;
border-image: url('//data.mars3d.cn/img/control/border.svg') 1 round stretch;
fill: #e6e6e6;
}
.cesium-viewer-geocoderContainer .cesium-geocoder-input {
height: 40px;
width: 40px;
background-color: rgba(63, 72, 84, 0.7);
}
.cesium-viewer-geocoderContainer .cesium-geocoder-input:focus {
background-color: var(--mars-base-bg, rgba(63, 72, 84, 0.9));
}
.cesium-viewer-geocoderContainer .search-results {
background-color: rgba(23, 49, 71, 0.8);
}
/**cesium info信息框*/
.cesium-infoBox {
top: 50px;
background: var(--mars-base-bg, rgba(63, 72, 84, 0.9));
}
.cesium-infoBox-title {
background-color: rgba(23, 49, 71, 0.8);
}
/**cesium 任务栏的FPS信息*/
.cesium-performanceDisplay-defaultContainer {
top: auto;
bottom: 35px;
right: 50px;
}
.cesium-performanceDisplay-ms,
.cesium-performanceDisplay-fps {
color: #fff;
}
/**cesium tileset调试信息面板*/
.cesium-viewer-cesiumInspectorContainer {
top: 10px;
left: 10px;
right: auto;
}
.cesium-cesiumInspector {
background-color: var(--mars-base-bg, rgba(63, 72, 84, 0.9));
}
/**覆盖mars3d内部控件的颜色等样式*/
.mars3d-compass .mars3d-compass-outer {
fill: rgba(39, 44, 54, 0.8);
}
.mars3d-compass .mars3d-compass-inner {
background: rgba(39, 44, 54, 0.8);
fill: #fff;
}
.mars3d-contextmenu-ul,
.mars3d-sub-menu {
background-color: var(--mars-base-bg, rgba(63, 72, 84, 0.9));
}
.mars3d-contextmenu-ul {
border-radius: 2px;
border-width: 1px;
border-image: url('//data.mars3d.cn/img/control/border.svg') 1 round stretch;
}
.mars3d-contextmenu-ul > li > a:hover,
.mars3d-sub-menu > li > a:hover,
.mars3d-contextmenu-ul > li > a:focus,
.mars3d-sub-menu > li > a:focus,
.mars3d-contextmenu-ul > li > .active,
.mars3d-sub-menu > li > .active {
background-color: var(--mars-hover-color, #3ea6ff);
}
.mars3d-contextmenu-ul > .active > a,
.mars3d-sub-menu > .active > a,
.mars3d-contextmenu-ul > .active > a:hover,
.mars3d-sub-menu > .active > a:hover,
.mars3d-contextmenu-ul > .active > a:focus,
.mars3d-sub-menu > .active > a:focus {
background-color: var(--mars-hover-color, #3ea6ff);
}
/* Popup样式*/
.mars3d-popup-color {
color: var(--mars-text-color, #ffffff);
}
.mars3d-popup-background {
// background: nonepopup
background: var(--mars-base-bg, rgba(63, 72, 84, 0.9));
}
// .mars3d-popup-content-wrapper {
// box-shadow: none !important;
// padding: 0 !important;
// background: var(--mars-base-border) !important;
// border-radius: 4px;
// }
.mars3d-popup-content {
margin: 15px;
}
.mars3d-popup-btn-custom {
padding: 3px 10px;
border: 1px solid #209ffd;
background: #209ffd1c;
color: var(--mars-text-color);
}
.mars3d-tooltip {
color: var(--mars-text-color, #ffffff);
background: var(--mars-base-bg, rgba(63, 72, 84, 0.9));
border: 1px solid var(--mars-base-bg, rgba(63, 72, 84, 0.9));
}
.mars3d-tooltip-top:before {
border-top-color: var(--mars-bg-base, rgba(23, 49, 71, 0.8));
}
.mars3d-tooltip-bottom:before {
border-bottom-color: var(--mars-bg-base, rgba(23, 49, 71, 0.8));
}
.mars3d-tooltip-left:before {
border-left-color: var(--mars-bg-base, rgba(23, 49, 71, 0.8));
}
.mars3d-tooltip-right:before {
border-right-color: var(--mars-bg-base, rgba(23, 49, 71, 0.8));
}
.mars3d-template-content label {
padding-right: 6px;
}
/* all 中的html样式 */
.mars3d-template-titile {
height: 33px;
line-height: 33px;
padding-left: 10px;
border-radius: 4px 4px 0px 0px;
box-shadow: 0px 6px 12px -2px rgba(50, 50, 93, 0.15), 0px 3px 7px -3px rgba(0, 0, 0, 0.2);
color: var(--mars-control-text) !important;
background: var(--mars-msg-title-bg);
font-family: var(--mars-font-family);
a {
font-size: 16px;
color: var(--mars-msg-title-color, #479be0);
text-decoration: none;
}
}
.mars3d-template-content {
margin-top: 0 !important;
background-color: var(--mars-dropdown-bg);
padding: 10px;
color: #eaf2ff;
label {
padding-right: 6px;
}
input {
color: var(--mars-text-color);
background-color: transparent !important;
padding: 4px 5px;
}
input::placeholder {
color: #cdcdcd !important;
}
textarea {
color: var(--mars-base-color);
background-color: transparent !important;
padding: 4px 5px;
}
textarea::placeholder {
color: #cdcdcd !important;
}
}
.mars3d-popup-btn-custom {
padding: 3px 10px;
border: 1px solid #209ffd;
background: #209ffd1c;
color: var(--mars-text-color, #ffffff);
}
.mars3d-popup-content {
margin: 15px;
}
.mars3d-divGraphic:hover {
z-index: 999 !important;
}
</style>

View File

@ -0,0 +1,71 @@
<template>
<el-cascader
v-model="cascaderValue"
:options="options"
:props="cascaderProps"
:placeholder="placeholder"
:clearable="clearable"
:disabled="disabled"
:size="size"
:style="style"
filterable
v-bind="$attrs"
/>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
modelValue: {
// emitPath 使
type: [String, Number, Array],
default: '',
},
options: {
type: Array,
default: () => [],
},
props: {
type: Object,
default: () => ({
value: 'value',
label: 'label',
children: 'children',
// +
multiple: false,
checkStrictly: true,
emitPath: false,
}),
},
placeholder: {
type: String,
default: '请选择',
},
clearable: {
type: Boolean,
default: true,
},
disabled: {
type: Boolean,
default: false,
},
size: {
type: String,
default: 'default',
},
style: {
type: Object,
default: () => ({ width: '200px' }),
},
})
const emit = defineEmits(['update:modelValue'])
const cascaderValue = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
})
const cascaderProps = computed(() => props.props)
</script>

View File

@ -0,0 +1,74 @@
<template>
<el-date-picker
v-model="dateValue"
:type="dateType"
:placeholder="placeholder"
:range-separator="rangeSeparator"
:start-placeholder="startPlaceholder"
:end-placeholder="endPlaceholder"
:value-format="valueFormat"
:clearable="clearable"
:disabled="disabled"
:size="size"
:style="style"
v-bind="$attrs"
/>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
modelValue: {
type: [String, Array, Date],
default: null,
},
dateType: {
type: String,
default: 'date', // date | daterange | datetime | datetimerange
},
placeholder: {
type: String,
default: '选择日期',
},
rangeSeparator: {
type: String,
default: '-',
},
startPlaceholder: {
type: String,
default: '开始日期',
},
endPlaceholder: {
type: String,
default: '结束日期',
},
valueFormat: {
type: String,
default: 'YYYY-MM-DD',
},
clearable: {
type: Boolean,
default: true,
},
disabled: {
type: Boolean,
default: false,
},
size: {
type: String,
default: 'default',
},
style: {
type: Object,
default: () => ({ width: '200px' }),
},
})
const emit = defineEmits(['update:modelValue'])
const dateValue = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
})
</script>

View File

@ -0,0 +1,54 @@
<template>
<el-input
v-model="inputValue"
:placeholder="placeholder"
:clearable="clearable"
:disabled="disabled"
:size="size"
:style="style"
v-bind="$attrs"
@keyup.enter="handleEnter"
/>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
modelValue: {
type: [String, Number],
default: '',
},
placeholder: {
type: String,
default: '请输入',
},
clearable: {
type: Boolean,
default: true,
},
disabled: {
type: Boolean,
default: false,
},
size: {
type: String,
default: 'default',
},
style: {
type: Object,
default: () => ({ width: '200px' }),
},
})
const emit = defineEmits(['update:modelValue', 'enter'])
const inputValue = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
})
const handleEnter = () => {
emit('enter')
}
</script>

View File

@ -0,0 +1 @@
<!-- 已废弃请使用 ItemInput.vue -->

View File

@ -0,0 +1,61 @@
<template>
<el-select
:style="style"
:size="size"
v-bind="$attrs"
:disabled="disabled"
v-model="selectValue"
:clearable="clearable"
:placeholder="placeholder"
>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
:disabled="item.disabled"
/>
</el-select>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
modelValue: {
type: [String, Number, Array],
default: '',
},
options: {
type: Array,
default: () => [],
},
placeholder: {
type: String,
default: '请选择',
},
clearable: {
type: Boolean,
default: true,
},
disabled: {
type: Boolean,
default: false,
},
size: {
type: String,
default: 'default',
},
style: {
type: Object,
default: () => ({ width: '200px' }),
},
})
const emit = defineEmits(['update:modelValue'])
const selectValue = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
})
</script>

View File

@ -0,0 +1,327 @@
<template>
<!-- 添加过渡动画让搜索表单显示/隐藏更平滑 -->
<transition name="search-form-fade">
<el-card class="search-form-card" v-show="showSearch">
<el-form ref="formRef" :model="formData" :label-width="labelWidth" :size="size">
<el-row :gutter="10" style="display: flex; align-items: center">
<!-- 左侧表单区域使用Grid布局每行固定5个表单项 -->
<el-col :span="22" class="form-col">
<div class="form-grid">
<el-form-item
v-for="item in formColumns"
:key="item.prop"
:label="item.label"
:prop="item.prop"
>
<component
@enter="handleSearch"
:is="getFormComponent(item)"
v-model="formData[item.prop]"
v-bind="getComponentProps(item)"
/>
</el-form-item>
</div>
</el-col>
<!-- 右侧按钮区域垂直居中显示 -->
<el-col :span="2" class="button-col">
<div class="button-wrapper">
<ComButton type="primary" icon="Search" @click="handleSearch">
搜索
</ComButton>
<ComButton
style="margin-left: 0"
type="warning"
plain
icon="Refresh"
@click="handleReset"
>
重置
</ComButton>
</div>
</el-col>
</el-row>
</el-form>
</el-card>
</transition>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
import ComButton from '@/components/ComButton/index.vue'
import ItemInput from './FormItem/ItemInput.vue'
import ItemSelect from './FormItem/ItemSelect.vue'
import ItemDate from './FormItem/ItemDate.vue'
import ItemCascader from './FormItem/ItemCascader.vue'
const props = defineProps({
//
formColumns: {
type: Array,
default: () => [],
},
//
showSearch: {
type: Boolean,
default: true,
},
//
size: {
type: String,
default: 'default',
},
//
labelWidth: {
type: String,
default: 'auto',
},
//
defaultFormData: {
type: Object,
default: () => ({}),
},
})
const emit = defineEmits(['search', 'reset'])
const formRef = ref(null)
const formData = reactive({})
//
const componentMap = {
input: ItemInput,
select: ItemSelect,
date: ItemDate,
month: ItemDate,
cascader: ItemCascader,
}
//
const initFormData = () => {
props.formColumns.forEach((item) => {
if (props.defaultFormData[item.prop] !== undefined) {
formData[item.prop] = props.defaultFormData[item.prop]
} else {
formData[item.prop] =
item.defaultValue ??
(item.type === 'date' && item.dateType?.includes('range') ? [] : '')
}
})
}
//
const getFormComponent = (item) => {
return componentMap[item.type] || ItemInput
}
//
const getComponentProps = (item) => {
// style使 style style
const baseProps = {
placeholder: item.placeholder,
clearable: item.clearable !== false,
disabled: item.disabled || false,
...(item.style && { style: item.style }),
}
switch (item.type) {
case 'select':
return {
...baseProps,
options: item.options || [],
}
case 'date':
return {
...baseProps,
dateType: item.dateType || 'date',
valueFormat: item.valueFormat || 'YYYY-MM-DD',
rangeSeparator: item.rangeSeparator || '-',
startPlaceholder: item.startPlaceholder,
endPlaceholder: item.endPlaceholder,
...(item.style && { style: item.style }),
}
case 'month':
return {
...baseProps,
dateType: 'month',
valueFormat: item.valueFormat || 'YYYY-MM',
}
case 'cascader':
return {
...baseProps,
options: item.options || [],
}
default:
return baseProps
}
}
// paramsList
const processFormData = (data) => {
const processedData = { ...data }
props.formColumns.forEach((item) => {
// paramsList
if (item.paramsList && Array.isArray(item.paramsList) && item.paramsList.length > 0) {
const value = processedData[item.prop]
//
if (Array.isArray(value) && value.length > 0) {
item.paramsList.forEach((paramName, index) => {
if (
value[index] !== undefined &&
value[index] !== null &&
value[index] !== ''
) {
processedData[paramName] = value[index]
}
})
}
// month[0] month[1]
delete processedData[item.prop]
}
})
return processedData
}
//
const handleSearch = () => {
const processedData = processFormData(formData)
emit('search', processedData)
}
//
const handleReset = () => {
formRef.value?.resetFields()
initFormData()
const processedData = processFormData(formData)
emit('reset', processedData)
}
//
defineExpose({
getFormData: () => processFormData(formData),
resetForm: handleReset,
formRef,
})
//
initFormData()
//
watch(
() => props.formColumns,
() => {
initFormData()
},
{ deep: true },
)
</script>
<style scoped>
.search-form-card {
margin-bottom: 10px;
}
.search-form-card :deep(.el-card__body) {
padding: 15px 10px 5px 20px !important;
}
/* 搜索表单显示/隐藏动画 */
.search-form-fade-enter-active,
.search-form-fade-leave-active {
transition: opacity 0.2s ease, transform 0.2s ease;
}
.search-form-fade-enter-from,
.search-form-fade-leave-to {
opacity: 0;
transform: translateY(-6px);
}
/* 左侧表单区域 */
.form-col {
border-right: 1px solid #e6e6e6;
padding-right: 10px;
}
/* Grid布局每行固定5个表单项 */
.form-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 10px;
}
/* 表单项样式 */
.form-grid :deep(.el-form-item) {
margin-bottom: 8px;
}
.form-grid :deep(.el-form-item__label) {
padding-right: 8px;
}
/* 确保所有表单组件宽度为100%,继承父容器宽度 */
.form-grid :deep(.el-form-item__content) {
width: 100%;
}
/* 覆盖所有表单组件的宽度,使其继承父容器宽度 */
.form-grid :deep(.el-input),
.form-grid :deep(.el-input__wrapper),
.form-grid :deep(.el-select),
.form-grid :deep(.el-date-editor),
.form-grid :deep(.el-cascader) {
width: 100% !important;
}
/* 确保输入框内部元素也自适应 */
.form-grid :deep(.el-input__inner) {
width: 100%;
}
/* 右侧按钮区域 */
.button-col {
display: flex;
align-items: flex-start;
justify-content: center;
padding-left: 10px;
padding-top: 0;
min-height: fit-content;
/* 与表单第一行输入框对齐 */
/* 由于表单项 margin-bottom: 8px第一行从顶部开始 */
/* 如果表单项有label需要加上label的高度来对齐输入框 */
}
.button-wrapper {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
justify-content: center;
align-items: center;
}
/* 确保按钮宽度一致高度与表单输入框一致32px */
.button-wrapper :deep(.el-button) {
width: 100%;
min-width: 0;
height: 32px;
padding: 0 15px;
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
white-space: nowrap;
flex-shrink: 0;
}
/* 确保按钮内部图标和文字对齐 */
.button-wrapper :deep(.el-button) > i,
.button-wrapper :deep(.el-button) > span {
display: inline-flex;
align-items: center;
line-height: 1;
}
</style>

View File

@ -0,0 +1,250 @@
<template>
<div class="com-table-container">
<!-- 查询表单 -->
<ComSearchForm
v-if="showSearch"
ref="searchFormRef"
:form-columns="formColumns"
:show-search="isShowSearch"
:default-form-data="defaultFormData"
:size="size"
@search="handleSearch"
@reset="handleReset"
/>
<!-- 数据表格 -->
<ComDataTable
ref="dataTableRef"
:table-data="tableData"
:table-columns="tableColumns"
:loading="loading"
:pagination="pagination"
:show-selection="showSelection"
:show-index="showIndex"
:show-toolbar="showToolbar"
:show-action="showAction"
:action-columns="actionColumns"
:show-more-action="showMoreAction"
:more-action-count="moreActionCount"
@selection-change="handleSelectionChange"
@refresh="handleRefresh"
@pagination-change="handlePaginationChange"
@action="handleAction"
@hide-search="handleHideSearch"
>
<!-- 工具栏插槽 -->
<template #toolbar>
<slot
name="toolbar"
:formData="
Object.assign({}, searchFormRef?.getFormData() || {}, {
pageNum: pagination.page,
pageSize: pagination.limit,
}) || {}
"
/>
</template>
<!-- 自定义列插槽 -->
<template
v-for="column in tableColumns"
:key="column.prop"
#[column.slot]="{ row, index, prop }"
>
<slot :name="column.slot" :row="row" :index="index" :prop="prop" />
</template>
<!-- 操作列插槽 -->
<template #action="{ row, index }">
<slot name="action" :row="row" :index="index" />
</template>
</ComDataTable>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import ComSearchForm from '@/components/ComSearchForm/index.vue'
import ComDataTable from '@/components/ComDataTable/index.vue'
const props = defineProps({
//
formColumns: {
type: Array,
default: () => [],
},
//
tableColumns: {
type: Array,
default: () => [],
},
//
showSearch: {
type: Boolean,
default: true,
},
//
size: {
type: String,
default: 'default',
},
//
defaultFormData: {
type: Object,
default: () => ({}),
},
//
showSelection: {
type: Boolean,
default: false,
},
showIndex: {
type: Boolean,
default: true,
},
showToolbar: {
type: Boolean,
default: false,
},
showAction: {
type: Boolean,
default: false,
},
//
actionColumns: {
type: Array,
default: () => [],
},
//
loadData: {
type: Function,
default: null,
},
//
defaultQueryParams: {
type: Object,
default: () => ({}),
},
//
showMoreAction: {
type: Boolean,
default: false,
},
//
moreActionCount: {
type: Number,
default: 2,
},
})
const emit = defineEmits(['search', 'reset', 'selection-change', 'action'])
const searchFormRef = ref(null)
const dataTableRef = ref(null)
const isShowSearch = ref(true)
const loading = ref(false)
const tableData = ref([])
const pagination = reactive({
page: 1,
limit: 10,
total: 0,
})
//
const handleSearch = (formData) => {
pagination.page = 1
fetchData(formData)
emit('search', formData)
}
//
const handleReset = (formData) => {
pagination.page = 1
fetchData(formData)
emit('reset', formData)
}
//
const handleSelectionChange = (selection) => {
emit('selection-change', selection)
}
//
const handleRefresh = () => {
const formData = searchFormRef.value?.getFormData() || {}
fetchData(formData)
}
//
const handleHideSearch = () => {
isShowSearch.value = !isShowSearch.value
}
//
const handlePaginationChange = (newPagination) => {
Object.assign(pagination, newPagination)
const formData = searchFormRef.value?.getFormData() || {}
fetchData(formData)
}
//
const handleAction = (action, row, index) => {
emit('action', action, row, index)
}
//
const fetchData = async (formData = {}) => {
if (!props.loadData) {
console.warn('ComTable: loadData function is not provided')
return
}
loading.value = true
try {
const params = {
...formData,
...props.defaultQueryParams,
pageNum: pagination.page, // pageNum
pageSize: pagination.limit, // pageSize
}
const response = await props.loadData(params)
// { rows: [], total: 0 } { data: { rows: [], total: 0 } }
if (response) {
const data = response.rows || response.data?.rows || response.data || []
const total = response.total || response.data?.total || 0
tableData.value = data
pagination.total = total
}
} catch (error) {
console.error('ComTable: fetchData error', error)
tableData.value = []
pagination.total = 0
} finally {
loading.value = false
}
}
onMounted(() => {
fetchData()
})
//
defineExpose({
searchFormRef,
dataTableRef,
refresh: handleRefresh,
getFormData: () => searchFormRef.value?.getFormData() || {},
resetForm: () => searchFormRef.value?.resetForm(),
clearSelection: () => dataTableRef.value?.clearSelection(),
})
</script>
<style scoped>
.com-table-container {
width: 100%;
}
</style>

View File

@ -5,6 +5,8 @@ import Cookies from 'js-cookie'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import 'mars3d-cesium/Build/Cesium/Widgets/widgets.css'
import 'mars3d/mars3d.css'
import locale from 'element-plus/es/locale/lang/zh-cn'
import '@/assets/styles/index.scss' // global css
@ -26,21 +28,28 @@ import elementIcons from '@/components/SvgIcon/svgicon'
import './permission' // permission control
import { useDict } from '@/utils/dict'
import { getConfigKey } from "@/api/system/config"
import { parseTime, resetForm, addDateRange, handleTree, selectDictLabel, selectDictLabels } from '@/utils/ruoyi'
import { getConfigKey } from '@/api/system/config'
import {
parseTime,
resetForm,
addDateRange,
handleTree,
selectDictLabel,
selectDictLabels,
} from '@/utils/ruoyi'
// 分页组件
import Pagination from '@/components/Pagination'
// 自定义表格工具组件
import RightToolbar from '@/components/RightToolbar'
// 富文本组件
import Editor from "@/components/Editor"
import Editor from '@/components/Editor'
// 文件上传组件
import FileUpload from "@/components/FileUpload"
import FileUpload from '@/components/FileUpload'
// 图片上传组件
import ImageUpload from "@/components/ImageUpload"
import ImageUpload from '@/components/ImageUpload'
// 图片预览组件
import ImagePreview from "@/components/ImagePreview"
import ImagePreview from '@/components/ImagePreview'
// 字典标签组件
import DictTag from '@/components/DictTag'
@ -76,9 +85,9 @@ directive(app)
// 使用element-plus 并且设置全局的大小
app.use(ElementPlus, {
locale: locale,
// 支持 large、default、small
size: Cookies.get('size') || 'default'
locale: locale,
// 支持 large、default、small
size: Cookies.get('size') || 'default',
})
app.mount('#app')

View File

@ -0,0 +1,440 @@
<template>
<div class="dialog-showcase">
<!-- 介绍区 -->
<section class="intro">
<p class="eyebrow">组件展示 · Dialog</p>
<h1>弹框组件设计指南</h1>
<p class="subtitle">
基于 Element Plus 弹框能力的二次封装支持外层 +
内层嵌套弹框统一的视觉样式和交互行为
适合在复杂业务中复用避免每个页面重复堆样式与逻辑
</p>
</section>
<!-- 基础示例外层弹框 -->
<section class="card-full">
<el-card shadow="hover" class="demo-card">
<template #header>
<div class="card-header">
<h3>基础弹框</h3>
<span>最常见的确认 / 信息展示场景</span>
</div>
</template>
<p class="card-description">
点击下方按钮打开基础弹框外层弹框默认居中吸附带有统一的头部样式阴影和内容区域滚动处理
</p>
<div class="demo-actions">
<ComButton type="primary" size="large" @click="openBaseDialog">
打开基础弹框
</ComButton>
</div>
<ComDialog
:dialog-config="baseDialog"
@closeDialogOuter="handleCloseBaseOuter"
@closeDialogInner="handleCloseBaseInner"
>
<template #outerContent>
<div class="dialog-content">
<h4 class="dialog-title">创建任务</h4>
<p class="dialog-subtitle">
在这里展示业务表单或提示文案内容区域已处理最大高度和滚动保证在小屏幕下也能完整展示
</p>
<el-form label-width="80px" class="dialog-form">
<el-form-item label="任务名称">
<el-input
v-model="baseForm.name"
placeholder="请输入任务名称"
/>
</el-form-item>
<el-form-item label="优先级">
<el-select v-model="baseForm.level" placeholder="请选择">
<el-option label="普通" value="normal" />
<el-option label="重要" value="important" />
<el-option label="紧急" value="urgent" />
</el-select>
</el-form-item>
<el-form-item label="备注说明">
<el-input
v-model="baseForm.remark"
type="textarea"
:rows="3"
placeholder="可选填写,补充说明信息"
/>
</el-form-item>
</el-form>
<div class="dialog-footer">
<ComButton plain @click="closeBaseDialog">取消</ComButton>
<ComButton type="primary" @click="submitBaseDialog">
确认提交
</ComButton>
</div>
</div>
</template>
</ComDialog>
</el-card>
</section>
<!-- 内外层嵌套弹框示例 -->
<section class="card-full">
<el-card shadow="hover" class="demo-card">
<template #header>
<div class="card-header">
<h3>内外层嵌套弹框</h3>
<span>适用于详情 + 深层操作的复杂流程</span>
</div>
</template>
<p class="card-description">
外层弹框承载主流程如详情编辑表单当需要处理更深入的步骤如高级设置二次确认
可以在外层内部再打开内层弹框保持上下文不丢失
</p>
<div class="demo-actions">
<ComButton type="primary" @click="openNestedOuter"> 打开嵌套弹框 </ComButton>
</div>
<ComDialog
:dialog-config="nestedDialog"
@closeDialogOuter="handleCloseNestedOuter"
@closeDialogInner="handleCloseNestedInner"
>
<!-- 外层弹框内容 -->
<template #outerContent>
<div class="dialog-content">
<h4 class="dialog-title">用户详情</h4>
<p class="dialog-subtitle">
外层用于展示主要信息和基础操作点击高级设置将打开内层弹框避免页面跳转打断当前上下文
</p>
<el-descriptions :column="2" border class="dialog-descriptions">
<el-descriptions-item label="用户名">
bns_admin
</el-descriptions-item>
<el-descriptions-item label="角色">
超级管理员
</el-descriptions-item>
<el-descriptions-item label="部门"> 技术中心 </el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag type="success">启用</el-tag>
</el-descriptions-item>
</el-descriptions>
<div class="dialog-footer">
<ComButton plain @click="closeNestedOuter">关闭</ComButton>
<ComButton type="primary" @click="openNestedInner">
打开高级设置
</ComButton>
</div>
</div>
</template>
<!-- 内层弹框内容 -->
<template #innerContent>
<div class="dialog-content">
<h4 class="dialog-title">高级设置</h4>
<p class="dialog-subtitle">
内层弹框通常体量较小用于配置少量但影响较大的选项例如权限范围敏感操作确认等
</p>
<el-form label-width="90px" class="dialog-form">
<el-form-item label="数据权限">
<el-select
v-model="nestedForm.scope"
placeholder="请选择数据范围"
>
<el-option label="仅本人" value="self" />
<el-option label="本部门" value="dept" />
<el-option label="本部门及下级" value="dept_child" />
<el-option label="全部数据" value="all" />
</el-select>
</el-form-item>
<el-form-item label="登录保护">
<el-switch v-model="nestedForm.mfa" />
</el-form-item>
</el-form>
<div class="dialog-footer">
<ComButton plain @click="closeNestedInner">取消</ComButton>
<ComButton type="primary" @click="submitNestedInner">
保存设置
</ComButton>
</div>
</div>
</template>
</ComDialog>
</el-card>
</section>
<!-- 使用建议 -->
<section class="card-full guide-card">
<el-card shadow="never">
<template #header>
<div class="card-header">
<h3>使用建议</h3>
<span>保持一致的交互体验与视觉规范</span>
</div>
</template>
<ul class="guide-list">
<li>尽量保证系统中所有弹框都通过 `ComDialog` 使用统一头部样式与位置</li>
<li>外层弹框承担主要流程内层弹框仅在确有必要时使用避免过深的弹框嵌套</li>
<li>
推荐使用 `dialogConfig`
对象集中管理弹框状态与标题避免在多个变量间来回切换
</li>
<li>
关闭弹框时结合 `closeDialogOuter / closeDialogInner`
事件重置表单清理临时状态
</li>
</ul>
</el-card>
</section>
</div>
</template>
<script setup name="ShowDialog">
import { reactive } from 'vue'
import ComDialog from '@/components/ComDialog/index.vue'
import ComButton from '@/components/ComButton/index.vue'
//
const baseDialog = reactive({
outerVisible: false,
outerTitle: '基础弹框示例',
outerWidth: '720px',
minHeight: '320px',
maxHeight: '70vh',
// 使
innerVisible: false,
})
const baseForm = reactive({
name: '',
level: 'normal',
remark: '',
})
const openBaseDialog = () => {
baseDialog.outerVisible = true
}
const closeBaseDialog = () => {
baseDialog.outerVisible = false
}
const submitBaseDialog = () => {
//
console.log('提交基础弹框表单:', { ...baseForm })
baseDialog.outerVisible = false
}
const handleCloseBaseOuter = () => {
baseDialog.outerVisible = false
}
const handleCloseBaseInner = () => {
baseDialog.innerVisible = false
}
//
const nestedDialog = reactive({
outerVisible: false,
outerTitle: '外层弹框 · 用户详情',
outerWidth: '760px',
minHeight: '360px',
maxHeight: '75vh',
innerVisible: false,
innerTitle: '内层弹框 · 高级设置',
innerWidth: '520px',
innerMinHeight: '260px',
innerMaxHeight: '60vh',
})
const nestedForm = reactive({
scope: 'dept_child',
mfa: true,
})
const openNestedOuter = () => {
nestedDialog.outerVisible = true
}
const closeNestedOuter = () => {
nestedDialog.outerVisible = false
nestedDialog.innerVisible = false
}
const openNestedInner = () => {
nestedDialog.innerVisible = true
}
const closeNestedInner = () => {
nestedDialog.innerVisible = false
}
const submitNestedInner = () => {
console.log('保存高级设置:', { ...nestedForm })
nestedDialog.innerVisible = false
}
const handleCloseNestedOuter = () => {
closeNestedOuter()
}
const handleCloseNestedInner = () => {
closeNestedInner()
}
</script>
<style scoped>
.dialog-showcase {
display: flex;
flex-direction: column;
gap: 32px;
padding: 32px 40px 48px;
background: linear-gradient(160deg, #f6f8ff 0%, #ffffff 55%, #f9fbff 100%);
min-height: 100%;
}
.intro {
max-width: 720px;
text-align: left;
}
.eyebrow {
font-size: 14px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: #5c6aff;
margin-bottom: 12px;
font-weight: 600;
}
.intro h1 {
font-size: 32px;
font-weight: 700;
margin: 0 0 12px;
color: #1f2a56;
}
.subtitle {
font-size: 16px;
line-height: 1.7;
color: #4c5a7a;
margin: 0;
}
.card-full {
width: 100%;
}
.demo-card {
border-radius: 18px;
overflow: hidden;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
}
.card-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #1f2a56;
}
.card-header span {
font-size: 14px;
color: #8792b0;
}
.card-description {
font-size: 14px;
line-height: 1.6;
color: #4c5a7a;
margin-bottom: 20px;
}
.demo-actions {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 8px;
}
.dialog-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.dialog-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #111827;
}
.dialog-subtitle {
margin: 0;
font-size: 13px;
line-height: 1.6;
color: #6b7280;
}
.dialog-form {
margin-top: 4px;
}
.dialog-descriptions {
margin-top: 8px;
}
.dialog-footer {
margin-top: 12px;
display: flex;
justify-content: flex-end;
gap: 12px;
}
.guide-card :deep(.el-card__body) {
padding: 20px 28px 28px;
}
.guide-list {
margin: 0;
padding-left: 18px;
display: grid;
gap: 10px;
color: #4c5a7a;
font-size: 14px;
line-height: 1.6;
}
.guide-list li {
list-style: disc;
}
@media (max-width: 1024px) {
.dialog-showcase {
padding: 24px 24px 36px;
}
}
@media (max-width: 600px) {
.intro h1 {
font-size: 26px;
}
.subtitle {
font-size: 15px;
}
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,183 @@
// 表单配置
export const formColumns = [
{
type: 'input',
prop: 'queryTableName',
label: '', // 表格名称
placeholder: '请输入表格名称',
defaultValue: '', // 默认值
isShowLabel: false,
},
{
type: 'select',
prop: 'queryStatus',
label: '', // 状态
placeholder: '请选择状态',
options: [
{ label: '启用', value: '1' },
{ label: '禁用', value: '0' },
],
},
{
type: 'cascader',
prop: 'queryClass',
label: '', // 班级
placeholder: '请选择班级',
options: [
{
label: '班级1',
value: '1',
children: [
{ label: '班级1-1', value: '1-1' },
{ label: '班级1-2', value: '1-2' },
{ label: '班级1-3', value: '1-3' },
],
},
{
label: '班级2',
value: '2',
children: [
{ label: '班级2-1', value: '2-1' },
{ label: '班级2-2', value: '2-2' },
{ label: '班级2-3', value: '2-3' },
],
},
{
label: '班级3',
value: '3',
children: [
{ label: '班级3-1', value: '3-1' },
{ label: '班级3-2', value: '3-2' },
{ label: '班级3-3', value: '3-3' },
],
},
],
},
{
type: 'date',
prop: 'queryDateRange',
label: '', // 日期区间
dateType: 'daterange', // 日期区间
paramsList: ['queryStartDate', 'queryEndDate'],
valueFormat: 'YYYY-MM-DD',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
},
// 月份区间筛选Element Plus 支持 monthrange
{
type: 'date',
prop: 'queryMonthRange',
label: '', // 月份区间
dateType: 'monthrange', // 月份区间
paramsList: ['queryStartMonth', 'queryEndMonth'],
valueFormat: 'YYYY-MM',
startPlaceholder: '开始月份',
endPlaceholder: '结束月份',
},
{
type: 'date',
prop: 'queryYear',
label: '', // 年份
dateType: 'year', // 单选年份
valueFormat: 'YYYY',
placeholder: '请选择年份',
},
{
type: 'date',
prop: 'queryMonth',
label: '', // 月份
dateType: 'month', // 单选月份
valueFormat: 'YYYY-MM',
placeholder: '请选择月份',
},
{
type: 'date',
prop: 'queryDate',
label: '', // 日期
dateType: 'date', // 单选日期
valueFormat: 'YYYY-MM-DD',
placeholder: '请选择日期',
},
]
// 表格列配置
export const tableColumns = [
{
prop: 'tableName', // 字段名
label: '表格名称', // 标签
width: 180, // 列宽
minWidth: 120, // 最小列宽
align: 'left', // 对齐方式
showOverflowTooltip: true, // 超出显示提示
id: 'tableName',
},
{
prop: 'status',
label: '状态',
width: 100,
// 自定义渲染(使用插槽)
slot: 'status',
id: 'status',
// 或者使用格式化函数
// formatter: (row, column, cellValue) => {
// return cellValue === '1' ? '启用' : '禁用'
// }
},
{
prop: 'createTime',
label: '创建时间',
width: 180,
id: 'createTime',
},
{
prop: 'remark',
label: '备注',
minWidth: 200,
showOverflowTooltip: true,
id: 'remark',
},
]
// 操作列配置(可选)
export const actionColumns = [
{
label: '编辑',
type: 'primary',
link: true,
handler: (row) => {
console.log('编辑', row)
},
},
{
label: '删除',
type: 'danger',
link: true,
handler: (row) => {
console.log('删除', row)
},
},
{
label: '详情',
type: 'danger',
link: true,
handler: (row) => {
console.log('删除', row)
},
},
{
label: '执行',
type: 'danger',
link: true,
handler: (row) => {
console.log('删除', row)
},
},
{
label: '测试',
type: 'warning',
link: true,
handler: (row) => {
console.log('删除', row)
},
},
]

View File

@ -0,0 +1,444 @@
<template>
<div class="table-showcase">
<section class="intro">
<p class="eyebrow">组件展示 · Table</p>
<h1>表格组件设计指南</h1>
<p class="subtitle">
基于配置驱动的列表查询组件通过简单的配置即可快速构建包含查询表单和数据表格的完整页面支持丰富的表单类型和表格功能
</p>
</section>
<section class="card-full">
<el-card shadow="hover" class="demo-card">
<template #header>
<div class="card-header">
<h3>列表查询示例</h3>
<span>基于配置驱动的表格组件</span>
</div>
</template>
<p class="card-description">
以下示例展示了如何使用配置化的方式快速构建一个包含查询表单和数据表格的完整列表页面
通过简单的配置即可实现表单渲染数据查询表格展示等完整功能
</p>
<div class="demo-content">
<ComTable
:size="`large`"
:show-action="true"
:show-toolbar="true"
:load-data="loadData"
@action="handleAction"
:form-columns="formColumns"
:table-columns="tableColumns"
:action-columns="actionColumns"
:show-more-action="true"
:more-action-count="2"
@selection-change="handleSelectionChange"
>
<!-- 工具栏插槽 -->
<template #toolbar>
<ComButton type="primary" icon="Plus" @click="handleAdd"
>新增</ComButton
>
<ComButton
type="danger"
icon="Delete"
:disabled="!multipleSelection.length"
@click="handleBatchDelete"
>
批量删除
</ComButton>
</template>
<!-- 状态列自定义渲染 -->
<template #status="{ row }">
<el-tag :type="row.status === '1' ? 'success' : 'danger'">
{{ row.status === '1' ? '启用' : '禁用' }}
</el-tag>
</template>
</ComTable>
</div>
</el-card>
</section>
<section class="card-full guide-card">
<el-card shadow="never">
<template #header>
<div class="card-header">
<h3>配置说明</h3>
<span>了解组件的配置项查询字段与使用方法</span>
</div>
</template>
<div class="config-demo">
<div class="config-section">
<h4>表单配置formColumns</h4>
<pre class="code-block">{{ formConfigExample }}</pre>
</div>
<div class="config-section">
<h4>表格配置tableColumns</h4>
<pre class="code-block">{{ tableConfigExample }}</pre>
</div>
<div class="config-section">
<h4>搜索字段示例本次查询参数</h4>
<p class="config-tip">
在上方查询表单中输入条件并点击搜索这里会实时展示本次请求携带的查询字段名及其对应的值
</p>
<pre class="code-block">{{ searchParamsDisplay }}</pre>
</div>
</div>
</el-card>
</section>
<section class="card-full guide-card">
<el-card shadow="never">
<template #header>
<div class="card-header">
<h3>使用建议</h3>
<span>保持一致性提升开发效率</span>
</div>
</template>
<ul class="guide-list">
<li>表单和表格组件采用职责分离设计可独立使用也可组合使用</li>
<li>通过配置驱动的方式定义表单字段和表格列减少重复代码</li>
<li>表单组件已二次封装提供统一的接口和样式便于维护和扩展</li>
<li>支持插槽自定义渲染满足复杂场景的定制化需求</li>
<li>表格支持分页选择操作列等常用功能开箱即用</li>
<li>数据加载通过 loadData 函数统一处理便于对接后端接口</li>
</ul>
</el-card>
</section>
</div>
</template>
<script setup name="ShowTable">
import { ref, computed } from 'vue'
import ComTable from '@/components/ComTable/index.vue'
import ComButton from '@/components/ComButton/index.vue'
import { formColumns, tableColumns, actionColumns } from './config'
const multipleSelection = ref([])
// config
const formConfigExample = `[
{
type: 'input', //
prop: 'queryTableName', //
placeholder: '请输入表格名称',
defaultValue: '',
},
{
type: 'select', //
prop: 'queryStatus',
placeholder: '请选择状态',
options: [
{ label: '启用', value: '1' },
{ label: '禁用', value: '0' },
],
},
{
type: 'cascader', //
prop: 'queryClass',
placeholder: '请选择班级',
options: [/* 班级层级数据 */],
},
{
type: 'date', //
prop: 'queryDateRange',
dateType: 'daterange',
paramsList: ['queryStartDate', 'queryEndDate'],
valueFormat: 'YYYY-MM-DD',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
},
{
type: 'date', //
prop: 'queryMonthRange',
dateType: 'monthrange',
paramsList: ['queryStartMonth', 'queryEndMonth'],
valueFormat: 'YYYY-MM',
},
{
type: 'date', //
prop: 'queryYear',
dateType: 'year',
valueFormat: 'YYYY',
},
]`
const tableConfigExample = `[
{
prop: 'tableName', //
label: '表格名称', //
width: 180,
align: 'left',
showOverflowTooltip: true,
},
{
prop: 'status',
label: '状态',
width: 100,
slot: 'status', // 使
},
]`
//
const lastQueryParams = ref({})
const searchParamsDisplay = computed(() => {
const data = lastQueryParams.value || {}
const keys = Object.keys(data || {})
if (!keys.length) {
return '// 暂未触发查询,先在上方表单中输入条件并点击“搜索”\n'
}
return JSON.stringify(data, null, 2)
})
//
const loadData = async (params) => {
console.log('加载数据,参数:', params)
//
lastQueryParams.value = params || {}
// API
return new Promise((resolve) => {
setTimeout(() => {
const mockData = {
rows: [
{
id: 1,
tableName: '用户表',
status: '1',
createTime: '2024-01-01 10:00:00',
remark: '用户信息表,存储系统用户的基本信息',
},
{
id: 2,
tableName: '角色表',
status: '1',
createTime: '2024-01-02 10:00:00',
remark: '角色信息表,存储系统角色和权限信息',
},
{
id: 3,
tableName: '菜单表',
status: '0',
createTime: '2024-01-03 10:00:00',
remark: '菜单信息表,存储系统菜单和路由信息',
},
{
id: 4,
tableName: '部门表',
status: '1',
createTime: '2024-01-04 10:00:00',
remark: '部门信息表,存储组织架构信息',
},
{
id: 5,
tableName: '字典表',
status: '1',
createTime: '2024-01-05 10:00:00',
remark: '字典信息表,存储系统字典数据',
},
],
total: 5,
}
resolve(mockData)
}, 500)
})
}
//
const handleSelectionChange = (selection) => {
multipleSelection.value = selection
console.log('选择变化:', selection)
}
//
const handleAction = (action, row, index) => {
console.log('操作:', action, row, index)
}
//
const handleAdd = () => {
console.log('新增')
}
//
const handleEdit = (row) => {
console.log('编辑', row)
}
//
const handleDelete = (row) => {
console.log('删除', row)
}
//
const handleBatchDelete = () => {
console.log('批量删除', multipleSelection.value)
}
</script>
<style scoped>
.table-showcase {
display: flex;
flex-direction: column;
gap: 32px;
padding: 18px;
background: linear-gradient(160deg, #f6f8ff 0%, #ffffff 55%, #f9fbff 100%);
min-height: 100%;
}
.intro {
max-width: 720px;
text-align: left;
}
.eyebrow {
font-size: 14px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: #5c6aff;
margin-bottom: 12px;
font-weight: 600;
}
.intro h1 {
font-size: 32px;
font-weight: 700;
margin: 0 0 12px;
color: #1f2a56;
}
.subtitle {
font-size: 16px;
line-height: 1.7;
color: #4c5a7a;
margin: 0;
}
.card-full {
width: 100%;
}
.demo-card {
border-radius: 18px;
overflow: hidden;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
}
.card-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #1f2a56;
}
.card-header span {
font-size: 14px;
color: #8792b0;
}
.card-description {
font-size: 14px;
line-height: 1.6;
color: #4c5a7a;
margin-bottom: 24px;
}
.demo-content {
width: 100%;
}
.guide-card :deep(.el-card__body) {
padding: 20px 28px 28px;
}
.config-demo {
display: flex;
flex-direction: column;
gap: 24px;
}
.config-section h4 {
margin: 0 0 12px;
font-size: 16px;
font-weight: 600;
color: #1f2a56;
}
.code-block {
background: #f8f9fa;
border-radius: 8px;
padding: 16px;
margin: 0;
font-size: 13px;
line-height: 1.6;
color: #2c3e50;
overflow-x: auto;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
border: 1px solid #e9ecef;
}
.config-tip {
margin: 0 0 8px;
font-size: 13px;
color: #6b7280;
}
.guide-list {
margin: 0;
padding-left: 18px;
display: grid;
gap: 10px;
color: #4c5a7a;
font-size: 14px;
line-height: 1.6;
}
.guide-list li {
list-style: disc;
}
/* 覆盖组件内部样式 */
.demo-content :deep(.search-form-card) {
margin-bottom: 20px;
}
.demo-content :deep(.search-form-card .el-card__body) {
padding: 18px 20px 0;
}
.demo-content :deep(.data-table-card) {
margin-bottom: 0;
}
@media (max-width: 1024px) {
.table-showcase {
padding: 24px 24px 36px;
}
}
@media (max-width: 600px) {
.intro h1 {
font-size: 26px;
}
.subtitle {
font-size: 15px;
}
.config-demo {
gap: 16px;
}
.code-block {
font-size: 12px;
padding: 12px;
}
}
</style>

View File

@ -26,9 +26,9 @@
v-for="item in group.texts"
:key="item.label"
class="text-item">
<ComText v-bind="item.props">{{
item.content
}}</ComText>
<ComText v-bind="item.props">
{{ item.content }}
</ComText>
<span class="text-note">{{ item.note }}</span>
</div>
</div>

View File

@ -1,44 +1,47 @@
import { defineConfig, loadEnv } from "vite";
import path from "path";
import createVitePlugins from "./vite/plugins";
import { defineConfig, loadEnv } from 'vite'
import path from 'path'
import createVitePlugins from './vite/plugins'
import { mars3dPlugin } from 'vite-plugin-mars3d'
const baseUrl = "http://localhost:8080"; // 后端接口
const baseUrl = 'http://localhost:8080' // 后端接口
// https://vitejs.dev/config/
export default defineConfig(({ mode, command }) => {
const env = loadEnv(mode, process.cwd());
env.VITE_APP_BASE_API = env.VITE_APP_BASE_API || "/dev-api";
env.VITE_APP_TITLE = env.VITE_APP_TITLE || "博诺思管理系统";
const { VITE_APP_ENV } = env;
const env = loadEnv(mode, process.cwd())
env.VITE_APP_BASE_API = env.VITE_APP_BASE_API || '/dev-api'
env.VITE_APP_TITLE = env.VITE_APP_TITLE || '博诺思管理系统'
const { VITE_APP_ENV } = env
return {
// 部署生产环境和开发环境下的URL。
// 默认情况下vite 会假设你的应用是被部署在一个域名的根路径上
// 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。
base: VITE_APP_ENV === "production" ? "/" : "/",
plugins: createVitePlugins(env, command === "build"),
base: VITE_APP_ENV === 'production' ? '/' : '/',
plugins: [...createVitePlugins(env, command === 'build'), mars3dPlugin()],
resolve: {
// https://cn.vitejs.dev/config/#resolve-alias
alias: {
// 设置路径
"~": path.resolve(__dirname, "./"),
'~': path.resolve(__dirname, './'),
// 设置别名
"@": path.resolve(__dirname, "./src"),
'@': path.resolve(__dirname, './src'),
},
// https://cn.vitejs.dev/config/#resolve-extensions
extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json", ".vue"],
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
},
// 让 Vite 将 .glb 视为静态资源,直接复制到产物中
assetsInclude: ['**/*.glb'],
// 打包配置
build: {
// https://vite.dev/config/build-options.html
sourcemap: command === "build" ? false : "inline",
outDir: "dist",
assetsDir: "assets",
sourcemap: command === 'build' ? false : 'inline',
outDir: 'dist',
assetsDir: 'assets',
chunkSizeWarningLimit: 2000,
rollupOptions: {
output: {
chunkFileNames: "static/js/[name]-[hash].js",
entryFileNames: "static/js/[name]-[hash].js",
assetFileNames: "static/[ext]/[name]-[hash].[ext]",
chunkFileNames: 'static/js/[name]-[hash].js',
entryFileNames: 'static/js/[name]-[hash].js',
assetFileNames: 'static/[ext]/[name]-[hash].[ext]',
},
},
},
@ -49,13 +52,13 @@ export default defineConfig(({ mode, command }) => {
open: true,
proxy: {
// https://cn.vitejs.dev/config/#server-proxy
"/dev-api": {
'/dev-api': {
target: baseUrl,
changeOrigin: true,
rewrite: (p) => p.replace(/^\/dev-api/, ""),
rewrite: (p) => p.replace(/^\/dev-api/, ''),
},
// springdoc proxy
"^/v3/api-docs/(.*)": {
'^/v3/api-docs/(.*)': {
target: baseUrl,
changeOrigin: true,
},
@ -65,11 +68,11 @@ export default defineConfig(({ mode, command }) => {
postcss: {
plugins: [
{
postcssPlugin: "internal:charset-removal",
postcssPlugin: 'internal:charset-removal',
AtRule: {
charset: (atRule) => {
if (atRule.name === "charset") {
atRule.remove();
if (atRule.name === 'charset') {
atRule.remove()
}
},
},
@ -77,5 +80,5 @@ export default defineConfig(({ mode, command }) => {
],
},
},
};
});
}
})