明树Git Lab
Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
J
jt_front
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
jt_front
Commits
37c7b72f
Commit
37c7b72f
authored
Dec 01, 2025
by
chenron
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
用户管理页面开发
parent
9d98e887
Changes
15
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
2308 additions
and
89 deletions
+2308
-89
Frame 10872.png
src/assets/images/Frame 10872.png
+0
-0
ConfigForm.md
src/components/ConfigForm.md
+402
-0
commonForm.vue
src/components/common/commonForm.vue
+726
-0
commonTable.vue
src/components/common/commonTable.vue
+20
-70
MainLayout.vue
src/layouts/MainLayout.vue
+31
-12
index.js
src/router/index.js
+18
-2
style.css
src/style.css
+2
-1
FormDemo.vue
src/views/examples/FormDemo.vue
+527
-0
CircleProgress.vue
src/views/homePage/components/CircleProgress.vue
+0
-0
Construct.vue
src/views/homePage/components/Construct.vue
+1
-1
Operation.vue
src/views/homePage/components/Operation.vue
+0
-0
ProjectApproval.vue
src/views/homePage/components/ProjectApproval.vue
+0
-0
index.vue
src/views/homePage/index.vue
+3
-3
index.vue
src/views/systemManage/index.vue
+7
-0
userManage.vue
src/views/systemManage/userManage.vue
+571
-0
No files found.
src/assets/images/Frame 10872.png
0 → 100644
View file @
37c7b72f
266 Bytes
src/components/ConfigForm.md
0 → 100644
View file @
37c7b72f
# ConfigForm 可配置表单组件
一个基于 Vue 3 和 Element Plus 的功能强大的可配置表单组件,支持多种表单控件类型和灵活的配置选项。
## 功能特性
-
🎯
**支持多种表单控件**
:输入框、选择器、单选框、复选框、日期选择器、时间选择器、数字输入框、开关、滑块、评分、颜色选择器、上传等
-
🎨
**灵活的布局配置**
:支持响应式布局、自定义列宽、间距等
-
✅
**完整的表单验证**
:支持自定义验证规则、异步验证等
-
🔧
**高度可配置**
:每个表单项都支持丰富的配置选项
-
🎪
**支持自定义插槽**
:可以插入自定义内容
-
📱
**响应式设计**
:支持不同屏幕尺寸的自适应布局
## 基础用法
```
vue
<
template
>
<config-form
v-model=
"formData"
:config=
"formConfig"
:items=
"formItems"
:rules=
"formRules"
@
submit=
"handleSubmit"
/>
</
template
>
<
script
setup
>
import
{
ref
}
from
'vue'
import
ConfigForm
from
'@/components/ConfigForm.vue'
const
formData
=
ref
({
name
:
''
,
age
:
null
,
gender
:
''
})
const
formConfig
=
{
labelWidth
:
'120px'
,
showButtons
:
true
,
submitText
:
'提交'
,
resetText
:
'重置'
}
const
formItems
=
[
{
type
:
'input'
,
prop
:
'name'
,
label
:
'姓名'
,
placeholder
:
'请输入姓名'
,
required
:
true
},
{
type
:
'number'
,
prop
:
'age'
,
label
:
'年龄'
,
min
:
1
,
max
:
120
},
{
type
:
'radio'
,
prop
:
'gender'
,
label
:
'性别'
,
options
:
[
{
label
:
'男'
,
value
:
'male'
},
{
label
:
'女'
,
value
:
'female'
}
]
}
]
const
formRules
=
{
name
:
[
{
required
:
true
,
message
:
'请输入姓名'
,
trigger
:
'blur'
}
]
}
const
handleSubmit
=
(
data
)
=>
{
console
.
log
(
'表单提交:'
,
data
)
}
</
script
>
```
## Props
### config (表单配置)
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| labelWidth | string | '120px' | 表单标签宽度 |
| labelPosition | string | 'right' | 表单标签位置:left/right/top |
| inline | boolean | false | 是否行内表单 |
| disabled | boolean | false | 是否禁用整个表单 |
| size | string | 'default' | 表单尺寸:large/default/small |
| gutter | number | 20 | 栅格间隔 |
| showButtons | boolean | true | 是否显示操作按钮 |
| showSubmit | boolean | true | 是否显示提交按钮 |
| showReset | boolean | true | 是否显示重置按钮 |
| submitText | string | '提交' | 提交按钮文本 |
| resetText | string | '重置' | 重置按钮文本 |
| submitLoading | boolean | false | 提交按钮加载状态 |
| customButtons | array |
[]
| 自定义按钮配置 |
### items (表单项配置)
每个表单项都支持以下基础配置:
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| type | string | - | 表单项类型(必填) |
| prop | string | - | 表单字段名(必填) |
| label | string | - | 表单标签 |
| placeholder | string | - | 占位符文本 |
| required | boolean | false | 是否必填 |
| disabled | boolean | false | 是否禁用 |
| readonly | boolean | false | 是否只读 |
| span | number | 24 | 栅格占位格数 |
| offset | number | 0 | 栅格左侧间隔格数 |
| xs/sm/md/lg/xl | number | - | 响应式栅格配置 |
| rules | array | - | 验证规则 |
#### 不同类型的特殊配置
##### input (输入框)
| 属性 | 类型 | 说明 |
|------|------|------|
| clearable | boolean | 是否可清空 |
| maxlength | number | 最大输入长度 |
| showWordLimit | boolean | 是否显示字数统计 |
| inputType | string | 输入框类型:text/password等 |
| rows | number | 文本域行数 |
| autosize | boolean/object | 文本域自适应高度 |
| prepend | string | 前置内容 |
| append | string | 后置内容 |
| prefix | string | 前缀图标 |
| suffix | string | 后缀图标 |
##### select (选择器)
| 属性 | 类型 | 说明 |
|------|------|------|
| clearable | boolean | 是否可清空 |
| multiple | boolean | 是否多选 |
| filterable | boolean | 是否可搜索 |
| remote | boolean | 是否远程搜索 |
| remoteMethod | function | 远程搜索方法 |
| loading | boolean | 是否加载中 |
| options | array | 选项数据 |
##### radio/checkbox (单选/复选框)
| 属性 | 类型 | 说明 |
|------|------|------|
| border | boolean | 是否显示边框 |
| options | array | 选项数据 |
| min/max | number | 复选框最少/最多选择数量 |
##### date (日期选择器)
| 属性 | 类型 | 说明 |
|------|------|------|
| dateType | string | 日期类型:date/daterange等 |
| format | string | 显示格式 |
| valueFormat | string | 绑定值格式 |
| startPlaceholder | string | 范围选择时开始日期占位符 |
| endPlaceholder | string | 范围选择时结束日期占位符 |
| disabledDate | function | 禁用日期函数 |
##### upload (上传)
| 属性 | 类型 | 说明 |
|------|------|------|
| action | string | 上传地址 |
| headers | object | 请求头 |
| multiple | boolean | 是否多选 |
| accept | string | 接受文件类型 |
| drag | boolean | 是否拖拽上传 |
| limit | number | 上传文件数量限制 |
| tip | string | 提示文本 |
## Events
| 事件名 | 参数 | 说明 |
|--------|------|------|
| update:modelValue | formData | 表单数据更新 |
| submit | formData | 表单提交 |
| reset | - | 表单重置 |
| change | prop, value | 表单项值变化 |
| input | prop, value | 输入事件 |
| focus | prop, event | 获得焦点 |
| blur | prop, event | 失去焦点 |
| validate | isValid, error | 表单验证 |
## Methods
通过 ref 可以调用以下方法:
| 方法名 | 参数 | 说明 |
|--------|------|------|
| validate | - | 验证整个表单 |
| validateField | prop | 验证指定字段 |
| clearValidate | props | 清除验证信息 |
| resetFields | - | 重置表单 |
## 完整示例
```
javascript
const
formItems
=
[
// 基础输入框
{
type
:
'input'
,
prop
:
'username'
,
label
:
'用户名'
,
placeholder
:
'请输入用户名'
,
clearable
:
true
,
prefix
:
'User'
,
rules
:
[
{
required
:
true
,
message
:
'请输入用户名'
,
trigger
:
'blur'
},
{
min
:
3
,
max
:
20
,
message
:
'长度在 3 到 20 个字符'
,
trigger
:
'blur'
}
]
},
// 密码输入框
{
type
:
'input'
,
prop
:
'password'
,
label
:
'密码'
,
inputType
:
'password'
,
placeholder
:
'请输入密码'
,
showPassword
:
true
,
rules
:
[
{
required
:
true
,
message
:
'请输入密码'
,
trigger
:
'blur'
},
{
min
:
6
,
message
:
'密码长度不能少于6位'
,
trigger
:
'blur'
}
]
},
// 数字输入框
{
type
:
'number'
,
prop
:
'age'
,
label
:
'年龄'
,
min
:
18
,
max
:
100
,
step
:
1
},
// 选择器
{
type
:
'select'
,
prop
:
'city'
,
label
:
'城市'
,
placeholder
:
'请选择城市'
,
clearable
:
true
,
filterable
:
true
,
options
:
[
{
label
:
'北京'
,
value
:
'beijing'
},
{
label
:
'上海'
,
value
:
'shanghai'
},
{
label
:
'广州'
,
value
:
'guangzhou'
},
{
label
:
'深圳'
,
value
:
'shenzhen'
}
]
},
// 级联选择器
{
type
:
'cascader'
,
prop
:
'region'
,
label
:
'地区'
,
options
:
regionOptions
,
props
:
{
value
:
'code'
,
label
:
'name'
,
children
:
'children'
}
},
// 单选框
{
type
:
'radio'
,
prop
:
'gender'
,
label
:
'性别'
,
border
:
true
,
options
:
[
{
label
:
'男'
,
value
:
'male'
},
{
label
:
'女'
,
value
:
'female'
}
]
},
// 复选框
{
type
:
'checkbox'
,
prop
:
'hobbies'
,
label
:
'爱好'
,
options
:
[
{
label
:
'读书'
,
value
:
'reading'
},
{
label
:
'运动'
,
value
:
'sports'
},
{
label
:
'音乐'
,
value
:
'music'
},
{
label
:
'旅行'
,
value
:
'travel'
}
]
},
// 开关
{
type
:
'switch'
,
prop
:
'status'
,
label
:
'状态'
,
activeText
:
'启用'
,
inactiveText
:
'禁用'
,
activeValue
:
1
,
inactiveValue
:
0
},
// 滑块
{
type
:
'slider'
,
prop
:
'score'
,
label
:
'分数'
,
min
:
0
,
max
:
100
,
step
:
5
,
showStops
:
true
},
// 评分
{
type
:
'rate'
,
prop
:
'rating'
,
label
:
'评分'
,
max
:
5
,
showText
:
true
,
texts
:
[
'很差'
,
'较差'
,
'一般'
,
'推荐'
,
'非常推荐'
]
},
// 日期选择器
{
type
:
'date'
,
prop
:
'birthday'
,
label
:
'生日'
,
valueFormat
:
'YYYY-MM-DD'
},
// 日期范围选择器
{
type
:
'date'
,
prop
:
'dateRange'
,
label
:
'日期范围'
,
dateType
:
'daterange'
,
startPlaceholder
:
'开始日期'
,
endPlaceholder
:
'结束日期'
},
// 时间选择器
{
type
:
'time'
,
prop
:
'workTime'
,
label
:
'工作时间'
,
valueFormat
:
'HH:mm:ss'
},
// 颜色选择器
{
type
:
'color'
,
prop
:
'themeColor'
,
label
:
'主题色'
,
showAlpha
:
true
,
predefine
:
[
'#409EFF'
,
'#67C23A'
,
'#E6A23C'
,
'#F56C6C'
]
},
// 文本域
{
type
:
'textarea'
,
prop
:
'description'
,
label
:
'描述'
,
placeholder
:
'请输入描述'
,
rows
:
4
,
maxlength
:
500
,
showWordLimit
:
true
},
// 上传
{
type
:
'upload'
,
prop
:
'avatar'
,
label
:
'头像'
,
action
:
'/api/upload'
,
accept
:
'image/*'
,
limit
:
1
,
tip
:
'只能上传jpg/png文件,且不超过2MB'
},
// 自定义插槽
{
type
:
'slot'
,
prop
:
'customField'
,
label
:
'自定义字段'
,
slotName
:
'customSlot'
}
]
```
## 注意事项
1.
**表单数据绑定**
:使用
`v-model`
双向绑定表单数据
2.
**验证规则**
:支持在
`rules`
prop 中定义全局规则,也可以在每个表单项中单独定义
3.
**响应式布局**
:使用 Element Plus 的栅格系统,支持
`xs/sm/md/lg/xl`
响应式配置
4.
**自定义插槽**
:通过
`type: 'slot'`
可以插入自定义内容
5.
**文件上传**
:需要配合后端接口实现文件上传功能
这个组件提供了表单开发中所需的大部分功能,通过配置化的方式可以快速构建各种类型的表单。
\ No newline at end of file
src/components/common/commonForm.vue
0 → 100644
View file @
37c7b72f
<
template
>
<div
class=
"config-form"
>
<el-form
ref=
"formRef"
:model=
"formData"
:rules=
"formRules"
:label-width=
"config.labelWidth || '120px'"
:label-position=
"config.labelPosition || 'right'"
:inline=
"config.inline || false"
:disabled=
"config.disabled || false"
:size=
"config.size || 'default'"
>
<el-row
:gutter=
"config.gutter || 20"
>
<el-col
v-for=
"(item, index) in formItems"
:key=
"item.prop || index"
:span=
"item.span || 24"
:offset=
"item.offset || 0"
:xs=
"item.xs"
:sm=
"item.sm"
:md=
"item.md"
:lg=
"item.lg"
:xl=
"item.xl"
>
<el-form-item
:label=
"item.label"
:prop=
"item.prop"
:required=
"item.required"
:error=
"item.error"
:show-message=
"item.showMessage !== false"
>
<!-- 输入框 -->
<el-input
v-if=
"item.type === 'input'"
v-model=
"formData[item.prop]"
:placeholder=
"item.placeholder || `请输入$
{item.label}`"
:clearable="item.clearable !== false"
:disabled="item.disabled"
:readonly="item.readonly"
:maxlength="item.maxlength"
:show-word-limit="item.showWordLimit"
:type="item.inputType || 'text'"
:rows="item.rows"
:autosize="item.autosize"
@blur="handleBlur(item, $event)"
@focus="handleFocus(item, $event)"
@input="handleInput(item, $event)"
@change="handleChange(item, $event)"
>
<template
v-if=
"item.prepend"
#
prepend
>
{{
item
.
prepend
}}
</
template
>
<
template
v-if=
"item.append"
#
append
>
{{
item
.
append
}}
</
template
>
<
template
v-if=
"item.prefix"
#
prefix
>
<el-icon><component
:is=
"item.prefix"
/></el-icon>
</
template
>
<
template
v-if=
"item.suffix"
#
suffix
>
<el-icon><component
:is=
"item.suffix"
/></el-icon>
</
template
>
</el-input>
<!-- 文本域 -->
<el-input
v-else-if=
"item.type === 'textarea'"
v-model=
"formData[item.prop]"
type=
"textarea"
:placeholder=
"item.placeholder || `请输入${item.label}`"
:clearable=
"item.clearable !== false"
:disabled=
"item.disabled"
:readonly=
"item.readonly"
:maxlength=
"item.maxlength"
:show-word-limit=
"item.showWordLimit"
:rows=
"item.rows || 3"
:autosize=
"item.autosize"
@
blur=
"handleBlur(item, $event)"
@
focus=
"handleFocus(item, $event)"
@
input=
"handleInput(item, $event)"
@
change=
"handleChange(item, $event)"
/>
<!-- 选择器 -->
<el-select
v-else-if=
"item.type === 'select'"
v-model=
"formData[item.prop]"
:placeholder=
"item.placeholder || `请选择${item.label}`"
:clearable=
"item.clearable !== false"
:disabled=
"item.disabled"
:multiple=
"item.multiple"
:filterable=
"item.filterable"
:remote=
"item.remote"
:remote-method=
"item.remoteMethod"
:loading=
"item.loading"
:no-match-text=
"item.noMatchText"
:no-data-text=
"item.noDataText"
@
change=
"handleChange(item, $event)"
@
visible-change=
"handleVisibleChange(item, $event)"
@
remove-tag=
"handleRemoveTag(item, $event)"
@
clear=
"handleClear(item)"
>
<el-option
v-for=
"option in item.options"
:key=
"option.value"
:label=
"option.label"
:value=
"option.value"
:disabled=
"option.disabled"
>
<span
v-if=
"option.icon"
style=
"margin-right: 8px"
>
<el-icon><component
:is=
"option.icon"
/></el-icon>
</span>
{{ option.label }}
</el-option>
</el-select>
<!-- 单选框组 -->
<el-radio-group
v-else-if=
"item.type === 'radio'"
v-model=
"formData[item.prop]"
:disabled=
"item.disabled"
:size=
"item.size"
@
change=
"handleChange(item, $event)"
>
<el-radio
v-for=
"option in item.options"
:key=
"option.value"
:label=
"option.value"
:disabled=
"option.disabled"
:border=
"item.border"
>
{{ option.label }}
</el-radio>
</el-radio-group>
<!-- 复选框组 -->
<el-checkbox-group
v-else-if=
"item.type === 'checkbox'"
v-model=
"formData[item.prop]"
:disabled=
"item.disabled"
:size=
"item.size"
:min=
"item.min"
:max=
"item.max"
@
change=
"handleChange(item, $event)"
>
<el-checkbox
v-for=
"option in item.options"
:key=
"option.value"
:label=
"option.value"
:disabled=
"option.disabled"
:border=
"item.border"
>
{{ option.label }}
</el-checkbox>
</el-checkbox-group>
<!-- 日期选择器 -->
<el-date-picker
v-else-if=
"item.type === 'date'"
v-model=
"formData[item.prop]"
:type=
"item.dateType || 'date'"
:placeholder=
"item.placeholder || `请选择${item.label}`"
:format=
"item.format"
:value-format=
"item.valueFormat"
:clearable=
"item.clearable !== false"
:disabled=
"item.disabled"
:readonly=
"item.readonly"
:editable=
"item.editable"
:start-placeholder=
"item.startPlaceholder"
:end-placeholder=
"item.endPlaceholder"
:range-separator=
"item.rangeSeparator"
:disabled-date=
"item.disabledDate"
@
change=
"handleChange(item, $event)"
@
blur=
"handleBlur(item, $event)"
@
focus=
"handleFocus(item, $event)"
/>
<!-- 时间选择器 -->
<el-time-picker
v-else-if=
"item.type === 'time'"
v-model=
"formData[item.prop]"
:placeholder=
"item.placeholder || `请选择${item.label}`"
:format=
"item.format"
:value-format=
"item.valueFormat"
:clearable=
"item.clearable !== false"
:disabled=
"item.disabled"
:readonly=
"item.readonly"
:editable=
"item.editable"
@
change=
"handleChange(item, $event)"
@
blur=
"handleBlur(item, $event)"
@
focus=
"handleFocus(item, $event)"
/>
<!-- 数字输入框 -->
<el-input-number
v-else-if=
"item.type === 'number'"
v-model=
"formData[item.prop]"
:min=
"item.min"
:max=
"item.max"
:step=
"item.step || 1"
:step-strictly=
"item.stepStrictly"
:precision=
"item.precision"
:size=
"item.size"
:disabled=
"item.disabled"
:controls=
"item.controls !== false"
:controls-position=
"item.controlsPosition"
:name=
"item.name"
@
change=
"handleChange(item, $event)"
@
focus=
"handleFocus(item, $event)"
@
blur=
"handleBlur(item, $event)"
/>
<!-- 开关 -->
<el-switch
v-else-if=
"item.type === 'switch'"
v-model=
"formData[item.prop]"
:disabled=
"item.disabled"
:loading=
"item.loading"
:size=
"item.size"
:active-text=
"item.activeText"
:inactive-text=
"item.inactiveText"
:active-value=
"item.activeValue"
:inactive-value=
"item.inactiveValue"
@
change=
"handleChange(item, $event)"
/>
<!-- 滑块 -->
<el-slider
v-else-if=
"item.type === 'slider'"
v-model=
"formData[item.prop]"
:min=
"item.min || 0"
:max=
"item.max || 100"
:step=
"item.step || 1"
:show-stops=
"item.showStops"
:show-tooltip=
"item.showTooltip !== false"
:disabled=
"item.disabled"
:range=
"item.range"
:vertical=
"item.vertical"
:height=
"item.height"
@
change=
"handleChange(item, $event)"
/>
<!-- 评分 -->
<el-rate
v-else-if=
"item.type === 'rate'"
v-model=
"formData[item.prop]"
:max=
"item.max || 5"
:disabled=
"item.disabled"
:allow-half=
"item.allowHalf"
:low-threshold=
"item.lowThreshold"
:high-threshold=
"item.highThreshold"
:colors=
"item.colors"
:void-color=
"item.voidColor"
:disabled-void-color=
"item.disabledVoidColor"
:icon-classes=
"item.iconClasses"
:void-icon-class=
"item.voidIconClass"
:disabled-void-icon-class=
"item.disabledVoidIconClass"
:show-text=
"item.showText"
:show-score=
"item.showScore"
:texts=
"item.texts"
@
change=
"handleChange(item, $event)"
/>
<!-- 颜色选择器 -->
<el-color-picker
v-else-if=
"item.type === 'color'"
v-model=
"formData[item.prop]"
:disabled=
"item.disabled"
:size=
"item.size"
:show-alpha=
"item.showAlpha"
:color-format=
"item.colorFormat"
:predefine=
"item.predefine"
@
change=
"handleChange(item, $event)"
@
active-change=
"handleActiveChange(item, $event)"
/>
<!-- 树形选择器 -->
<el-tree-select
v-else-if=
"item.type === 'tree'"
v-model=
"formData[item.prop]"
:data=
"item.data"
:props=
"
item.props || {
label: 'label',
value: 'value',
children: 'children',
}
"
:placeholder=
"item.placeholder || `请选择${item.label}`"
:clearable=
"item.clearable !== false"
:disabled=
"item.disabled"
:filterable=
"item.filterable"
:check-strictly=
"item.checkStrictly"
:render-after-expand=
"item.renderAfterExpand !== false"
:show-checkbox=
"item.showCheckbox"
:multiple=
"item.multiple"
:collapse-tags=
"item.collapseTags"
:max-collapse-tags=
"item.maxCollapseTags"
:node-key=
"item.nodeKey"
:default-expanded-keys=
"item.defaultExpandedKeys"
:default-checked-keys=
"item.defaultCheckedKeys"
:current-node-key=
"item.currentNodeKey"
:expand-on-click-node=
"item.expandOnClickNode !== false"
:check-on-click-node=
"item.checkOnClickNode"
:auto-expand-parent=
"item.autoExpandParent !== false"
:default-expand-all=
"item.defaultExpandAll"
:indent=
"item.indent"
:icon=
"item.icon"
:lazy=
"item.lazy"
:load=
"item.load"
:highlight-current=
"item.highlightCurrent"
@
change=
"handleChange(item, $event)"
@
node-click=
"handleNodeClick(item, $event)"
@
check=
"handleCheck(item, $event)"
@
node-expand=
"handleNodeExpand(item, $event)"
@
node-collapse=
"handleNodeCollapse(item, $event)"
/>
<!-- 上传 -->
<el-upload
v-else-if=
"item.type === 'upload'"
:action=
"item.action"
:headers=
"item.headers"
:method=
"item.method"
:multiple=
"item.multiple"
:data=
"item.data"
:name=
"item.name"
:with-credentials=
"item.withCredentials"
:show-file-list=
"item.showFileList !== false"
:drag=
"item.drag"
:accept=
"item.accept"
:auto-upload=
"item.autoUpload !== false"
:limit=
"item.limit"
:on-exceed=
"item.onExceed"
:on-success=
"
(response, file, fileList) =>
handleUploadSuccess(item, response, file, fileList)
"
:on-error=
"
(error, file, fileList) =>
handleUploadError(item, error, file, fileList)
"
:on-progress=
"
(event, file, fileList) =>
handleUploadProgress(item, event, file, fileList)
"
:on-change=
"
(file, fileList) => handleUploadChange(item, file, fileList)
"
:on-remove=
"
(file, fileList) => handleUploadRemove(item, file, fileList)
"
:on-preview=
"item.onPreview"
:before-upload=
"item.beforeUpload"
:before-remove=
"item.beforeRemove"
:http-request=
"item.httpRequest"
>
<el-button
v-if=
"!item.drag"
type=
"primary"
>
{{
item.buttonText || "点击上传"
}}
</el-button>
<div
v-else
class=
"upload-dragger"
>
<el-icon
class=
"el-icon--upload"
><upload-filled
/></el-icon>
<div
class=
"el-upload__text"
>
将文件拖到此处,或
<em>
点击上传
</em>
</div>
</div>
<
template
v-if=
"item.tip"
#
tip
>
<div
class=
"el-upload__tip"
>
{{
item
.
tip
}}
</div>
</
template
>
</el-upload>
<!-- 自定义插槽 -->
<slot
v-else-if=
"item.type === 'slot'"
:name=
"item.slotName || item.prop"
:prop=
"item.prop"
:item=
"item"
:value=
"formData[item.prop]"
:onChange=
"(value) => handleSlotChange(item, value)"
/>
<!-- 纯文本显示 -->
<span
v-else-if=
"item.type === 'text'"
>
{{
formData[item.prop]
}}
</span>
<!-- 自定义组件 -->
<component
v-else-if=
"item.type === 'component' && item.component"
:is=
"item.component"
v-model=
"formData[item.prop]"
v-bind=
"item.componentProps"
@
change=
"handleChange(item, $event)"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 表单操作按钮 -->
<el-form-item
v-if=
"config.showButtons !== false"
>
<el-button
v-if=
"config.showSubmit !== false"
type=
"primary"
:loading=
"config.submitLoading"
@
click=
"handleSubmit"
>
{{ config.submitText || "提交" }}
</el-button>
<el-button
v-if=
"config.showReset !== false"
@
click=
"handleReset"
>
{{ config.resetText || "重置" }}
</el-button>
<el-button
v-for=
"btn in config.customButtons"
:key=
"btn.key"
:type=
"btn.type"
:loading=
"btn.loading"
@
click=
"btn.handler"
>
{{ btn.text }}
</el-button>
</el-form-item>
</el-form>
</div>
</template>
<
script
setup
>
import
{
ref
,
reactive
,
computed
,
watch
,
onMounted
}
from
"vue"
;
import
{
UploadFilled
}
from
"@element-plus/icons-vue"
;
const
props
=
defineProps
({
// 表单配置
config
:
{
type
:
Object
,
default
:
()
=>
({}),
},
// 表单项配置
items
:
{
type
:
Array
,
default
:
()
=>
[],
},
// 表单数据
modelValue
:
{
type
:
Object
,
default
:
()
=>
({}),
},
// 表单规则
rules
:
{
type
:
Object
,
default
:
()
=>
({}),
},
});
const
emit
=
defineEmits
([
"update:modelValue"
,
"submit"
,
"reset"
,
"change"
,
"blur"
,
"focus"
,
"input"
,
"validate"
,
]);
const
formRef
=
ref
();
const
formData
=
reactive
({});
// 表单项配置
const
formItems
=
computed
(()
=>
props
.
items
);
// 表单验证规则
const
formRules
=
computed
(()
=>
{
const
rules
=
{
...
props
.
rules
};
formItems
.
value
.
forEach
((
item
)
=>
{
if
(
item
.
rules
)
{
rules
[
item
.
prop
]
=
item
.
rules
;
}
});
return
rules
;
});
// 初始化表单数据
const
initFormData
=
()
=>
{
Object
.
keys
(
formData
).
forEach
((
key
)
=>
{
delete
formData
[
key
];
});
formItems
.
value
.
forEach
((
item
)
=>
{
if
(
item
.
prop
)
{
if
(
props
.
modelValue
[
item
.
prop
]
!==
undefined
)
{
formData
[
item
.
prop
]
=
props
.
modelValue
[
item
.
prop
];
}
else
{
formData
[
item
.
prop
]
=
item
.
defaultValue
!==
undefined
?
item
.
defaultValue
:
getDefaultValue
(
item
.
type
);
}
}
});
};
// 获取不同类型的默认值
const
getDefaultValue
=
(
type
)
=>
{
switch
(
type
)
{
case
"checkbox"
:
return
[];
case
"switch"
:
return
false
;
case
"number"
:
return
0
;
case
"rate"
:
return
0
;
default
:
return
""
;
}
};
// 监听表单数据变化
watch
(
formData
,
(
newVal
)
=>
{
emit
(
"update:modelValue"
,
{
...
newVal
});
emit
(
"change"
,
newVal
);
},
{
deep
:
true
}
);
// 监听外部数据变化
watch
(
()
=>
props
.
modelValue
,
(
newVal
)
=>
{
Object
.
keys
(
newVal
).
forEach
((
key
)
=>
{
if
(
formData
.
hasOwnProperty
(
key
))
{
formData
[
key
]
=
newVal
[
key
];
}
});
},
{
deep
:
true
}
);
// 事件处理
const
handleInput
=
(
item
,
value
)
=>
{
emit
(
"input"
,
item
.
prop
,
value
);
};
const
handleChange
=
(
item
,
value
)
=>
{
emit
(
"change"
,
item
.
prop
,
value
);
};
const
handleBlur
=
(
item
,
event
)
=>
{
emit
(
"blur"
,
item
.
prop
,
event
);
};
const
handleFocus
=
(
item
,
event
)
=>
{
emit
(
"focus"
,
item
.
prop
,
event
);
};
const
handleVisibleChange
=
(
item
,
visible
)
=>
{
emit
(
"visible-change"
,
item
.
prop
,
visible
);
};
const
handleRemoveTag
=
(
item
,
tag
)
=>
{
emit
(
"remove-tag"
,
item
.
prop
,
tag
);
};
const
handleClear
=
(
item
)
=>
{
emit
(
"clear"
,
item
.
prop
);
};
const
handleActiveChange
=
(
item
,
color
)
=>
{
emit
(
"active-change"
,
item
.
prop
,
color
);
};
const
handleSlotChange
=
(
item
,
value
)
=>
{
formData
[
item
.
prop
]
=
value
;
emit
(
"change"
,
item
.
prop
,
value
);
};
// 树形选择器事件处理
const
handleNodeClick
=
(
item
,
data
)
=>
{
emit
(
"node-click"
,
item
.
prop
,
data
);
};
const
handleCheck
=
(
item
,
data
)
=>
{
emit
(
"check"
,
item
.
prop
,
data
);
};
const
handleNodeExpand
=
(
item
,
data
)
=>
{
emit
(
"node-expand"
,
item
.
prop
,
data
);
};
const
handleNodeCollapse
=
(
item
,
data
)
=>
{
emit
(
"node-collapse"
,
item
.
prop
,
data
);
};
// 上传事件处理
const
handleUploadSuccess
=
(
item
,
response
,
file
,
fileList
)
=>
{
if
(
item
.
onSuccess
)
{
item
.
onSuccess
(
response
,
file
,
fileList
);
}
emit
(
"upload-success"
,
item
.
prop
,
response
,
file
,
fileList
);
};
const
handleUploadError
=
(
item
,
error
,
file
,
fileList
)
=>
{
if
(
item
.
onError
)
{
item
.
onError
(
error
,
file
,
fileList
);
}
emit
(
"upload-error"
,
item
.
prop
,
error
,
file
,
fileList
);
};
const
handleUploadProgress
=
(
item
,
event
,
file
,
fileList
)
=>
{
if
(
item
.
onProgress
)
{
item
.
onProgress
(
event
,
file
,
fileList
);
}
emit
(
"upload-progress"
,
item
.
prop
,
event
,
file
,
fileList
);
};
const
handleUploadChange
=
(
item
,
file
,
fileList
)
=>
{
if
(
item
.
onChange
)
{
item
.
onChange
(
file
,
fileList
);
}
emit
(
"upload-change"
,
item
.
prop
,
file
,
fileList
);
};
const
handleUploadRemove
=
(
item
,
file
,
fileList
)
=>
{
if
(
item
.
onRemove
)
{
item
.
onRemove
(
file
,
fileList
);
}
emit
(
"upload-remove"
,
item
.
prop
,
file
,
fileList
);
};
// 表单提交
const
handleSubmit
=
async
()
=>
{
try
{
const
valid
=
await
formRef
.
value
.
validate
();
if
(
valid
)
{
emit
(
"submit"
,
{
...
formData
});
}
}
catch
(
error
)
{
emit
(
"validate"
,
false
,
error
);
}
};
// 表单重置
const
handleReset
=
()
=>
{
formRef
.
value
.
resetFields
();
emit
(
"reset"
);
};
// 表单验证
const
validate
=
async
()
=>
{
try
{
const
valid
=
await
formRef
.
value
.
validate
();
emit
(
"validate"
,
true
);
return
valid
;
}
catch
(
error
)
{
emit
(
"validate"
,
false
,
error
);
return
false
;
}
};
// 验证指定字段
const
validateField
=
async
(
prop
)
=>
{
try
{
await
formRef
.
value
.
validateField
(
prop
);
return
true
;
}
catch
(
error
)
{
return
false
;
}
};
// 清除验证
const
clearValidate
=
(
props
)
=>
{
formRef
.
value
.
clearValidate
(
props
);
};
// 暴露方法
defineExpose
({
validate
,
validateField
,
clearValidate
,
resetFields
:
handleReset
,
formRef
,
});
onMounted
(()
=>
{
initFormData
();
});
</
script
>
<
style
lang=
"less"
scoped
>
.config-form {
.upload-dragger {
text-align: center;
}
:deep(.el-form-item__label) {
font-weight: 500;
}
:deep(.el-form-item) {
margin-bottom: 18px;
}
:deep(.el-input-number) {
width: 100%;
}
:deep(.el-select) {
width: 100%;
}
:deep(.el-date-editor) {
width: 100%;
}
:deep(.el-time-picker) {
width: 100%;
}
:deep(.el-form-item) {
&:last-child {
.el-form-item__content {
justify-content: flex-end;
}
}
}
:deep(.el-form--inline .el-form-item) {
display: flex;
}
}
</
style
>
src/components/common/commonTable.vue
View file @
37c7b72f
...
...
@@ -11,11 +11,12 @@
<div
class=
"table-container"
>
<el-table
style=
"width: 100%"
:data=
"tableData"
:height=
"
h
eight"
:height=
"
tableH
eight"
:max-height=
"maxHeight"
:stripe=
"stripe"
:border=
"border"
border
:size=
"size"
:fit=
"fit"
:show-header=
"showTableHeader"
...
...
@@ -165,7 +166,10 @@ const props = defineProps({
default
:
true
,
},
// 表格高度
height
:
[
String
,
Number
],
tableHeight
:
{
type
:
[
String
,
Number
],
default
:
"auto"
,
},
// 表格最大高度
maxHeight
:
[
String
,
Number
],
// 是否为斑马纹表格
...
...
@@ -516,32 +520,7 @@ const handleNextClick = (val) => {
}
}
// 响应式设计
@media (max-width: 768px) {
.common-table {
padding: 12px;
.table-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.table-footer {
justify-content: center;
.el-pagination {
justify-content: center;
}
}
}
}
:deep(.el-table) {
th.el-table__cell {
// background-color: var(--el-table-header-bg-color);
// background-color: #e8ebf0;
background-color: #f2f6fd;
}
thead {
color: var(--el-table-header-text-color);
}
...
...
@@ -556,14 +535,10 @@ const handleNextClick = (val) => {
.el-pagination.is-background {
.el-pager li {
border: 1px solid #e7e9ee;
// background-color: #fff;
font-weight: 400;
// color: #333;
}
.el-pager li:not(.disabled).is-active {
// background: rgba(91, 183, 59, 0.1);
border: none;
// color: var(--el-color-primary);
font-weight: 400;
}
.btn-prev,
...
...
@@ -571,48 +546,23 @@ const handleNextClick = (val) => {
background-color: var(--el-disabled-bg-color);
}
}
.el-table__fixed-right-patch {
top: 0;
border: 1px solid #fff;
}
}
:deep(.el-table) {
th.el-table__cell {
border-right: 1px solid #ebeef5;
}
.el-table__fixed-right {
top: -1px;
background: #f5f7fa;
height: 50px;
text-align: center;
}
.el-table--border,
.el-table--group {
border: 0px;
.el-table__body {
td.el-table__cell {
height: 45px;
line-height: 45px;
}
}
.el-table__cell {
border-right: 0;
}
&::before {
width: 0px;
}
&::after {
width: 0px;
}
.el-table__border-left-patch {
width: 0;
.cell {
text-align: center;
}
}
}
:deep(.el-table__header .cell) {
display: flex;
align-items: center;
}
:deep(.el-table) {
background-color: rgba(255, 255, 255, 0.5);
}
.common-table {
background: rgb(157 188 218 / 40%);
}
:deep(.el-table th.el-table__cell) {
background: rgba(4, 66, 126, 0.4);
color: #fff;
}
:deep(.el-table tr) {
background: rgb(157 188 218 / 40%);
}
</
style
>
src/layouts/MainLayout.vue
View file @
37c7b72f
...
...
@@ -33,23 +33,32 @@
router
>
<
template
v-for=
"route in menuRoutes"
:key=
"route.path"
>
<el-menu-item
v-if=
"!route.meta?.parent"
:index=
"route.path"
>
<!-- 无子菜单的项目 -->
<el-menu-item
v-if=
"!route.children || route.children.length === 0"
:index=
"route.path"
>
<el-icon><component
:is=
"route.meta?.icon || 'menu'"
/></el-icon>
<span>
{{
route
.
meta
?.
menuName
||
route
.
name
}}
</span>
</el-menu-item>
<el-sub-menu
v-else
:index=
"route.meta.parent"
>
<!-- 有子菜单的项目 -->
<el-sub-menu
v-else
:index=
"route.path"
>
<template
#
title
>
<el-icon
><component
:is=
"route.meta?.icon || 'menu'"
/></el-icon>
<span>
{{
route
.
meta
.
parent
}}
</span>
<span>
{{
route
.
meta
?.
menuName
||
route
.
name
}}
</span>
</
template
>
<
template
v-for=
"child in route.children"
:key=
"child.path"
>
<el-menu-item
:index=
"child.path"
style=
"padding-left: 20px !important"
>
<el-icon><component
:is=
"child.meta?.icon || ''"
/></el-icon>
<span>
{{
child
.
meta
?.
menuName
||
child
.
name
}}
</span>
</el-menu-item>
</
template
>
<el-menu-item
:index=
"route.path"
style=
"padding-left: 70px !important"
>
{{ route.meta.menuName }}
</el-menu-item>
</el-sub-menu>
</template>
</el-menu>
...
...
@@ -64,7 +73,7 @@
</template>
<
script
setup
>
import
{
computed
}
from
"vue"
;
import
{
computed
,
h
,
resolveComponent
}
from
"vue"
;
import
{
useRouter
}
from
"vue-router"
;
const
router
=
useRouter
();
...
...
@@ -72,7 +81,11 @@ const router = useRouter();
// 计算菜单路由
const
menuRoutes
=
computed
(()
=>
{
const
mainRoute
=
router
.
options
.
routes
.
find
((
route
)
=>
route
.
path
===
"/"
);
return
mainRoute
?.
children
&&
mainRoute
?.
children
.
filter
(
route
=>
!
route
.
meta
||
route
.
meta
.
showInMenu
!==
false
)
||
[];
if
(
!
mainRoute
?.
children
)
return
[];
return
mainRoute
.
children
.
filter
(
(
route
)
=>
!
route
.
meta
||
route
.
meta
.
showInMenu
!==
false
);
});
// 处理退出登录
...
...
@@ -92,7 +105,7 @@ const handleLogout = () => {
}
/* 选中菜单项样式 */
.el-menu-item{
.el-menu-item
{
color: #666;
background-color: #fff;
}
...
...
@@ -102,6 +115,12 @@ const handleLogout = () => {
border-right: 4px solid #3d84ee;
}
/* 完全去掉系统管理子菜单的背景色 */
:deep(.el-sub-menu__title) {
color: #666;
background-color: #fff !important;
}
.city-header {
width: 100%;
height: 64px;
...
...
src/router/index.js
View file @
37c7b72f
...
...
@@ -20,7 +20,7 @@ const routes = [
name
:
'数据大屏'
,
title
:
'dataSummary'
,
component
:
()
=>
import
(
'@/views/homePage/index.vue'
),
meta
:
{
menuName
:
'数据大屏'
,
icon
:
'
home
'
}
meta
:
{
menuName
:
'数据大屏'
,
icon
:
'
platform
'
}
},
{
path
:
'/projectManage'
,
...
...
@@ -38,7 +38,23 @@ const routes = [
menuName
:
'新增项目'
,
showInMenu
:
false
// 不在菜单中显示
}
}
},
{
path
:
'/systemManage'
,
name
:
'系统管理'
,
title
:
'systemManage'
,
// component: () => import('@/views/systemManage/index.vue'),
meta
:
{
menuName
:
'系统管理'
,
icon
:
'tools'
},
children
:
[
{
path
:
'/systemManage/userManage'
,
name
:
'用户管理'
,
title
:
'userManage'
,
component
:
()
=>
import
(
'@/views/systemManage/userManage.vue'
),
meta
:
{
menuName
:
'用户管理'
,}
}
]
},
]
}
]
...
...
src/style.css
View file @
37c7b72f
全局样式重置和滚动条控制
/* 全局样式重置和滚动条控制 */
*
{
margin
:
0
;
padding
:
0
;
...
...
@@ -18,6 +18,7 @@ html, body {
#app
{
height
:
100vh
;
width
:
100vw
;
background
:
#f5f5f5
;
overflow
:
hidden
;
/* 确保应用容器不产生滚动条 */
}
...
...
src/views/examples/FormDemo.vue
0 → 100644
View file @
37c7b72f
<
template
>
<div
class=
"form-demo"
>
<h2>
可配置表单组件示例
</h2>
<!-- 基础表单示例 -->
<el-card
class=
"demo-card"
header=
"基础表单"
>
<config-form
v-model=
"basicForm"
:config=
"basicConfig"
:items=
"basicItems"
:rules=
"basicRules"
@
submit=
"handleBasicSubmit"
@
reset=
"handleBasicReset"
/>
</el-card>
<!-- 复杂表单示例 -->
<el-card
class=
"demo-card"
header=
"复杂表单"
>
<config-form
v-model=
"complexForm"
:config=
"complexConfig"
:items=
"complexItems"
:rules=
"complexRules"
@
submit=
"handleComplexSubmit"
@
reset=
"handleComplexReset"
@
change=
"handleComplexChange"
/>
</el-card>
<!-- 树形选择器示例 -->
<el-card
class=
"demo-card"
header=
"树形选择器"
>
<config-form
v-model=
"treeForm"
:config=
"treeConfig"
:items=
"treeItems"
@
submit=
"handleTreeSubmit"
@
node-click=
"handleNodeClick"
@
check=
"handleTreeCheck"
/>
</el-card>
<!-- 自定义表单示例 -->
<el-card
class=
"demo-card"
header=
"自定义表单"
>
<config-form
v-model=
"customForm"
:config=
"customConfig"
:items=
"customItems"
@
submit=
"handleCustomSubmit"
>
<template
#
customSlot=
"
{ item, value, onChange }">
<el-input
:model-value=
"value"
placeholder=
"这是一个自定义插槽"
@
input=
"onChange"
>
<template
#
append
>
<el-button
@
click=
"handleCustomAction(item)"
>
自定义操作
</el-button>
</
template
>
</el-input>
</template>
</config-form>
</el-card>
</div>
</template>
<
script
setup
>
import
{
ref
,
reactive
}
from
'vue'
import
{
ElMessage
}
from
'element-plus'
import
ConfigForm
from
'../ConfigForm.vue'
// 基础表单数据
const
basicForm
=
ref
({
name
:
''
,
age
:
null
,
gender
:
''
,
email
:
''
,
description
:
''
})
// 基础表单配置
const
basicConfig
=
{
labelWidth
:
'100px'
,
showButtons
:
true
,
submitText
:
'保存'
,
resetText
:
'清空'
}
// 基础表单项配置
const
basicItems
=
[
{
type
:
'input'
,
prop
:
'name'
,
label
:
'姓名'
,
placeholder
:
'请输入姓名'
,
clearable
:
true
,
rules
:
[
{
required
:
true
,
message
:
'请输入姓名'
,
trigger
:
'blur'
},
{
min
:
2
,
max
:
10
,
message
:
'长度在 2 到 10 个字符'
,
trigger
:
'blur'
}
]
},
{
type
:
'number'
,
prop
:
'age'
,
label
:
'年龄'
,
min
:
1
,
max
:
120
,
rules
:
[
{
required
:
true
,
message
:
'请输入年龄'
,
trigger
:
'blur'
},
{
type
:
'number'
,
message
:
'年龄必须为数字'
,
trigger
:
'blur'
}
]
},
{
type
:
'radio'
,
prop
:
'gender'
,
label
:
'性别'
,
required
:
true
,
options
:
[
{
label
:
'男'
,
value
:
'male'
},
{
label
:
'女'
,
value
:
'female'
}
]
},
{
type
:
'input'
,
prop
:
'email'
,
label
:
'邮箱'
,
placeholder
:
'请输入邮箱地址'
,
rules
:
[
{
type
:
'email'
,
message
:
'请输入正确的邮箱地址'
,
trigger
:
'blur'
}
]
},
{
type
:
'textarea'
,
prop
:
'description'
,
label
:
'个人简介'
,
placeholder
:
'请输入个人简介'
,
rows
:
3
,
maxlength
:
200
,
showWordLimit
:
true
}
]
// 基础表单验证规则
const
basicRules
=
{}
// 复杂表单数据
const
complexForm
=
ref
({
userInfo
:
{
name
:
''
,
department
:
''
},
skills
:
[],
level
:
3
,
isActive
:
true
,
birthDate
:
''
,
workTime
:
''
,
avatar
:
''
,
color
:
'#409EFF'
,
range
:
[
20
,
80
]
})
// 复杂表单配置
const
complexConfig
=
{
labelWidth
:
'120px'
,
gutter
:
20
,
showButtons
:
true
,
submitText
:
'提交'
,
resetText
:
'重置'
,
customButtons
:
[
{
key
:
'preview'
,
text
:
'预览'
,
type
:
'info'
,
handler
:
handlePreview
}
]
}
// 复杂表单项配置
const
complexItems
=
[
{
type
:
'input'
,
prop
:
'userInfo.name'
,
label
:
'姓名'
,
placeholder
:
'请输入姓名'
,
span
:
12
,
prefix
:
'User'
},
{
type
:
'select'
,
prop
:
'userInfo.department'
,
label
:
'部门'
,
placeholder
:
'请选择部门'
,
span
:
12
,
clearable
:
true
,
options
:
[
{
label
:
'技术部'
,
value
:
'tech'
},
{
label
:
'产品部'
,
value
:
'product'
},
{
label
:
'设计部'
,
value
:
'design'
},
{
label
:
'运营部'
,
value
:
'operation'
}
]
},
{
type
:
'checkbox'
,
prop
:
'skills'
,
label
:
'技能'
,
span
:
12
,
options
:
[
{
label
:
'JavaScript'
,
value
:
'js'
},
{
label
:
'Vue.js'
,
value
:
'vue'
},
{
label
:
'React'
,
value
:
'react'
},
{
label
:
'Node.js'
,
value
:
'node'
},
{
label
:
'Python'
,
value
:
'python'
}
]
},
{
type
:
'rate'
,
prop
:
'level'
,
label
:
'熟练度'
,
span
:
12
,
showText
:
true
,
texts
:
[
'新手'
,
'初级'
,
'中级'
,
'高级'
,
'专家'
]
},
{
type
:
'switch'
,
prop
:
'isActive'
,
label
:
'状态'
,
span
:
8
,
activeText
:
'激活'
,
inactiveText
:
'禁用'
},
{
type
:
'date'
,
prop
:
'birthDate'
,
label
:
'出生日期'
,
span
:
8
,
valueFormat
:
'YYYY-MM-DD'
},
{
type
:
'time'
,
prop
:
'workTime'
,
label
:
'工作时间'
,
span
:
8
,
valueFormat
:
'HH:mm:ss'
},
{
type
:
'upload'
,
prop
:
'avatar'
,
label
:
'头像'
,
span
:
12
,
action
:
'/api/upload'
,
accept
:
'image/*'
,
tip
:
'只能上传jpg/png文件,且不超过500kb'
},
{
type
:
'color'
,
prop
:
'color'
,
label
:
'主题色'
,
span
:
12
,
showAlpha
:
true
},
{
type
:
'slider'
,
prop
:
'range'
,
label
:
'范围'
,
span
:
24
,
range
:
true
,
min
:
0
,
max
:
100
,
showStops
:
true
}
]
// 复杂表单验证规则
const
complexRules
=
{
'userInfo.name'
:
[
{
required
:
true
,
message
:
'请输入姓名'
,
trigger
:
'blur'
}
],
'userInfo.department'
:
[
{
required
:
true
,
message
:
'请选择部门'
,
trigger
:
'change'
}
],
skills
:
[
{
type
:
'array'
,
required
:
true
,
message
:
'请至少选择一项技能'
,
trigger
:
'change'
}
]
}
// 树形选择器数据
const
treeForm
=
ref
({
department
:
''
,
departments
:
[],
category
:
''
})
// 部门树形数据
const
departmentData
=
[
{
value
:
'1'
,
label
:
'总公司'
,
children
:
[
{
value
:
'1-1'
,
label
:
'技术中心'
,
children
:
[
{
value
:
'1-1-1'
,
label
:
'前端开发部'
},
{
value
:
'1-1-2'
,
label
:
'后端开发部'
},
{
value
:
'1-1-3'
,
label
:
'测试部'
}
]
},
{
value
:
'1-2'
,
label
:
'产品中心'
,
children
:
[
{
value
:
'1-2-1'
,
label
:
'产品设计部'
},
{
value
:
'1-2-2'
,
label
:
'用户研究部'
}
]
},
{
value
:
'1-3'
,
label
:
'运营中心'
,
children
:
[
{
value
:
'1-3-1'
,
label
:
'市场推广部'
},
{
value
:
'1-3-2'
,
label
:
'客户服务部'
}
]
}
]
},
{
value
:
'2'
,
label
:
'分公司A'
,
children
:
[
{
value
:
'2-1'
,
label
:
'技术部'
},
{
value
:
'2-2'
,
label
:
'销售部'
}
]
}
]
// 分类数据
const
categoryData
=
[
{
value
:
'electronics'
,
label
:
'电子产品'
,
children
:
[
{
value
:
'phones'
,
label
:
'手机'
,
children
:
[
{
value
:
'iphone'
,
label
:
'iPhone'
},
{
value
:
'android'
,
label
:
'Android手机'
},
{
value
:
'huawei'
,
label
:
'华为手机'
}
]
},
{
value
:
'computers'
,
label
:
'电脑'
,
children
:
[
{
value
:
'laptop'
,
label
:
'笔记本电脑'
},
{
value
:
'desktop'
,
label
:
'台式电脑'
},
{
value
:
'tablet'
,
label
:
'平板电脑'
}
]
}
]
},
{
value
:
'clothing'
,
label
:
'服装'
,
children
:
[
{
value
:
'mens'
,
label
:
'男装'
},
{
value
:
'womens'
,
label
:
'女装'
},
{
value
:
'kids'
,
label
:
'童装'
}
]
}
]
// 自定义表单数据
const
customForm
=
ref
({
customField
:
''
,
status
:
'pending'
})
// 树形选择器配置
const
treeConfig
=
{
labelWidth
:
'120px'
,
showButtons
:
true
,
submitText
:
'保存'
,
resetText
:
'重置'
}
// 树形选择器表单项配置
const
treeItems
=
[
{
type
:
'tree'
,
prop
:
'department'
,
label
:
'所属部门'
,
placeholder
:
'请选择部门'
,
data
:
departmentData
,
clearable
:
true
,
filterable
:
true
,
checkStrictly
:
true
,
renderAfterExpand
:
false
,
span
:
12
},
{
type
:
'tree'
,
prop
:
'departments'
,
label
:
'多选部门'
,
placeholder
:
'请选择多个部门'
,
data
:
departmentData
,
clearable
:
true
,
filterable
:
true
,
showCheckbox
:
true
,
multiple
:
true
,
collapseTags
:
true
,
maxCollapseTags
:
3
,
span
:
12
},
{
type
:
'tree'
,
prop
:
'category'
,
label
:
'商品分类'
,
placeholder
:
'请选择分类'
,
data
:
categoryData
,
clearable
:
true
,
filterable
:
true
,
checkStrictly
:
true
,
defaultExpandAll
:
true
,
span
:
24
}
]
// 自定义表单配置
const
customConfig
=
{
labelWidth
:
'100px'
,
inline
:
true
}
// 自定义表单项配置
const
customItems
=
[
{
type
:
'slot'
,
prop
:
'customField'
,
label
:
'自定义字段'
,
slotName
:
'customSlot'
},
{
type
:
'select'
,
prop
:
'status'
,
label
:
'状态'
,
options
:
[
{
label
:
'待处理'
,
value
:
'pending'
},
{
label
:
'处理中'
,
value
:
'processing'
},
{
label
:
'已完成'
,
value
:
'completed'
}
]
}
]
// 事件处理函数
const
handleBasicSubmit
=
(
formData
)
=>
{
console
.
log
(
'基础表单提交:'
,
formData
)
ElMessage
.
success
(
'基础表单提交成功'
)
}
const
handleBasicReset
=
()
=>
{
ElMessage
.
info
(
'基础表单已重置'
)
}
const
handleComplexSubmit
=
(
formData
)
=>
{
console
.
log
(
'复杂表单提交:'
,
formData
)
ElMessage
.
success
(
'复杂表单提交成功'
)
}
const
handleComplexReset
=
()
=>
{
ElMessage
.
info
(
'复杂表单已重置'
)
}
const
handleComplexChange
=
(
formData
)
=>
{
console
.
log
(
'复杂表单数据变化:'
,
formData
)
}
const
handleCustomSubmit
=
(
formData
)
=>
{
console
.
log
(
'自定义表单提交:'
,
formData
)
ElMessage
.
success
(
'自定义表单提交成功'
)
}
const
handleCustomAction
=
(
item
)
=>
{
console
.
log
(
'自定义操作:'
,
item
)
ElMessage
.
info
(
'执行自定义操作'
)
}
const
handleTreeSubmit
=
(
formData
)
=>
{
console
.
log
(
'树形表单提交:'
,
formData
)
ElMessage
.
success
(
'树形表单提交成功'
)
}
const
handleNodeClick
=
(
prop
,
data
)
=>
{
console
.
log
(
'节点点击:'
,
prop
,
data
)
ElMessage
.
info
(
`点击了节点:
${
data
.
label
}
`
)
}
const
handleTreeCheck
=
(
prop
,
data
)
=>
{
console
.
log
(
'节点选择:'
,
prop
,
data
)
ElMessage
.
info
(
`选择了节点:
${
data
.
checkedNodes
?.
map
(
n
=>
n
.
label
).
join
(
', '
)}
`)
}
const handlePreview = () => {
console.log('预览数据:', complexForm.value)
ElMessage.info('预览功能')
}
</
script
>
<
style
lang=
"less"
scoped
>
.form-demo {
padding: 20px;
h2 {
margin-bottom: 20px;
color: #333;
}
.demo-card {
margin-bottom: 20px;
:deep(.el-card__header) {
background-color: #f5f7fa;
font-weight: 600;
}
}
}
</
style
>
\ No newline at end of file
src/views/components/CircleProgress.vue
→
src/views/
homePage/
components/CircleProgress.vue
View file @
37c7b72f
File moved
src/views/components/Construct.vue
→
src/views/
homePage/
components/Construct.vue
View file @
37c7b72f
...
...
@@ -147,7 +147,7 @@ const recycleList = reactive([
.vw(left,65);
height: 30%;
max-width: 83%;
background-image: url(/src/assets/images/
baseRateRel
.png);
background-image: url(/src/assets/images/
progress
.png);
background-size: 100% 100%;
background-repeat: no-repeat;
}
...
...
src/views/components/Operation.vue
→
src/views/
homePage/
components/Operation.vue
View file @
37c7b72f
File moved
src/views/components/ProjectApproval.vue
→
src/views/
homePage/
components/ProjectApproval.vue
View file @
37c7b72f
File moved
src/views/homePage/index.vue
View file @
37c7b72f
...
...
@@ -71,9 +71,9 @@
</
template
>
<
script
setup
>
import
Construct
from
".
.
/components/Construct.vue"
;
import
ProjectApproval
from
".
.
/components/ProjectApproval.vue"
;
import
Operation
from
".
.
/components/Operation.vue"
;
import
Construct
from
"./components/Construct.vue"
;
import
ProjectApproval
from
"./components/ProjectApproval.vue"
;
import
Operation
from
"./components/Operation.vue"
;
import
{
reactive
,
ref
}
from
"vue"
;
const
selectedLeftBtn
=
ref
(
"equity"
);
...
...
src/views/systemManage/index.vue
0 → 100644
View file @
37c7b72f
<
template
>
<div>
系统管理 setting
</div>
</
template
>
<
script
setup
></
script
>
<
style
scoped
lang=
"less"
></
style
>
src/views/systemManage/userManage.vue
0 → 100644
View file @
37c7b72f
<
template
>
<div
class=
"user-manage"
v-loading=
"loading"
>
<!-- 查询表单 -->
<div
class=
"search-form"
>
<commonForm
v-model=
"searchForm"
:config=
"searchConfig"
:items=
"searchItems"
@
submit=
"handleSearch"
@
reset=
"handleReset"
/>
</div>
<!-- 用户列表表格 -->
<div
class=
"table-container"
>
<common-table
:tableHeight=
"tableHeight"
:data=
"tableData"
:columns=
"tableColumns"
:total=
"total"
:current-page=
"currentPage"
:page-size=
"pageSize"
title=
"用户列表"
:border=
"true"
@
size-change=
"handleSizeChange"
@
current-page-change=
"handleCurrentPageChange"
>
<template
#
header-actions
>
<el-button
type=
"primary"
@
click=
"handleAdd"
>
<!--
<el-icon><Plus
/></el-icon>
-->
新增
</el-button>
</
template
>
<
template
#
enable=
"{ row }"
>
<el-switch
:model-value=
"row.enable === 0 ? true : false"
@
change=
"handleStatusChange($event, row)"
active-color=
"#13ce66"
inactive-color=
"#ff4949"
></el-switch>
</
template
>
<
template
#
operations=
"{ row, index }"
>
<el-button
type=
"text"
size=
"small"
@
click=
"handleEdit(row, index)"
>
编辑
</el-button>
<el-button
type=
"text"
size=
"small"
@
click=
"handleDelete(row, index)"
>
删除
</el-button>
</
template
>
</common-table>
</div>
<!-- 用户表单对话框 -->
<el-dialog
v-model=
"dialogVisible"
:title=
"dialogTitle"
width=
"600px"
@
close=
"handleDialogClose"
>
<commonForm
v-model=
"userForm"
:config=
"formConfig"
:items=
"formItems"
:rules=
"formRules"
@
submit=
"handleFormSubmit"
@
reset=
"handleFormReset"
/>
</el-dialog>
</div>
</template>
<
script
setup
>
import
{
ref
,
reactive
,
onMounted
,
getCurrentInstance
,
computed
}
from
"vue"
;
import
{
ElMessage
,
ElMessageBox
}
from
"element-plus"
;
import
{
Plus
,
Edit
,
Delete
}
from
"@element-plus/icons-vue"
;
import
commonForm
from
"@/components/common/commonForm.vue"
;
import
CommonTable
from
"@/components/common/commonTable.vue"
;
import
{
da
}
from
"element-plus/es/locales.mjs"
;
const
{
proxy
}
=
getCurrentInstance
();
const
loading
=
ref
(
false
);
// 计算表格高度
const
tableHeight
=
computed
(()
=>
{
const
headerHeight
=
50
;
const
paginationHeight
=
50
;
const
rowHeight
=
40
;
const
baseHeight
=
headerHeight
+
paginationHeight
;
// 1.如果数据超过10条,固定显示10行的高度 + 滚动条;2.如果数据不超过10条,按实际行数计算高度
const
maxRows
=
Math
.
min
(
tableData
.
value
.
length
,
10
);
const
contentHeight
=
maxRows
*
rowHeight
;
return
`
${
baseHeight
+
contentHeight
}
px`
;
});
// 数据转换函数
const
convertToTreeData
=
(
apiData
)
=>
{
return
apiData
.
map
((
item
)
=>
({
value
:
item
.
id
.
toString
(),
label
:
item
.
name
,
children
:
item
.
children
?
convertToTreeData
(
item
.
children
)
:
[],
}));
};
// 查询表单数据
const
searchForm
=
ref
({
name
:
""
,
mobile
:
""
,
});
// 查询表单配置
const
searchConfig
=
{
inline
:
true
,
labelWidth
:
"80px"
,
showButtons
:
true
,
submitText
:
"查询"
,
resetText
:
"重置"
,
};
// 查询表单项配置
const
searchItems
=
[
{
type
:
"input"
,
prop
:
"name"
,
label
:
"用户姓名"
,
placeholder
:
"请输入用户姓名"
,
clearable
:
true
,
span
:
8
,
},
{
type
:
"input"
,
prop
:
"mobile"
,
label
:
"手机号码"
,
placeholder
:
"请输入手机号码"
,
clearable
:
true
,
span
:
8
,
},
];
// 表格数据
const
tableData
=
ref
([]);
const
total
=
ref
(
0
);
const
currentPage
=
ref
(
1
);
const
pageSize
=
ref
(
10
);
// 表格列配置
const
tableColumns
=
[
{
prop
:
"name"
,
label
:
"用户姓名"
,
minWidth
:
100
,
showOverflowTooltip
:
true
,
},
{
prop
:
"departs"
,
label
:
"所属部门"
,
minWidth
:
120
,
showOverflowTooltip
:
true
,
},
{
prop
:
"positions"
,
label
:
"岗位"
,
minWidth
:
100
,
showOverflowTooltip
:
true
,
},
{
prop
:
"roles"
,
label
:
"角色"
,
minWidth
:
100
,
showOverflowTooltip
:
true
,
},
{
prop
:
"mobile"
,
label
:
"手机号码"
,
minWidth
:
100
,
},
{
prop
:
"createdAt"
,
label
:
"创建时间"
,
minWidth
:
160
,
},
{
prop
:
"enable"
,
label
:
"状态"
,
width
:
100
,
slot
:
"enable"
,
align
:
"center"
,
},
{
prop
:
"operations"
,
label
:
"操作"
,
width
:
160
,
slot
:
"operations"
,
fixed
:
"right"
,
align
:
"center"
,
},
];
// 对话框相关
const
dialogVisible
=
ref
(
false
);
const
dialogTitle
=
ref
(
"新增用户"
);
const
isEdit
=
ref
(
false
);
const
editIndex
=
ref
(
-
1
);
// 用户表单数据
const
userForm
=
ref
({
name
:
""
,
departs
:
[],
positions
:
[],
roles
:
[],
enable
:
"0"
,
});
// 用户表单配置
const
formConfig
=
{
labelWidth
:
"100px"
,
showButtons
:
true
,
submitText
:
"保存"
,
resetText
:
"取消"
,
};
const
departmentData
=
ref
([]);
const
positionsData
=
ref
([]);
const
rolesData
=
ref
([]);
const
loadDepartmentData
=
()
=>
{
proxy
.
$post
({
url
:
"/api/user/depart/treeDepart"
,
data
:
{},
callback
:
(
data
)
=>
{
departmentData
.
value
=
convertToTreeData
(
data
);
},
error
:
(
err
)
=>
{},
});
};
// 岗位下拉数据
const
loadPositionsData
=
()
=>
{
proxy
.
$post
({
url
:
"/api/user/position/listPosition"
,
data
:
{},
callback
:
(
data
)
=>
{
positionsData
.
value
=
convertToTreeData
(
data
.
rows
);
},
error
:
(
err
)
=>
{},
});
};
// 角色下拉数据
const
loadRolesData
=
()
=>
{
proxy
.
$post
({
url
:
"/api/user/role/listRole"
,
data
:
{
page
:
1
,
pageSize
:
10
,
},
callback
:
(
data
)
=>
{
rolesData
.
value
=
convertToTreeData
(
data
.
rows
);
},
error
:
(
err
)
=>
{},
});
};
// 用户表单项配置
const
formItems
=
computed
(()
=>
[
{
type
:
"input"
,
prop
:
"name"
,
label
:
"用户姓名"
,
placeholder
:
"请输入用户姓名"
,
// required: true,
span
:
12
,
// rules: [{ required: true, message: "请输入用户姓名", trigger: "blur" }],
},
{
type
:
"tree"
,
prop
:
"departs"
,
label
:
"所属部门"
,
placeholder
:
"请选择部门"
,
data
:
departmentData
.
value
,
clearable
:
true
,
filterable
:
true
,
checkStrictly
:
true
,
renderAfterExpand
:
false
,
showCheckbox
:
true
,
multiple
:
true
,
collapseTags
:
true
,
maxCollapseTags
:
2
,
span
:
12
,
},
{
type
:
"tree"
,
prop
:
"positions"
,
label
:
"岗位"
,
placeholder
:
"请选择岗位"
,
data
:
positionsData
.
value
,
clearable
:
true
,
filterable
:
true
,
checkStrictly
:
true
,
renderAfterExpand
:
false
,
showCheckbox
:
true
,
multiple
:
true
,
collapseTags
:
true
,
maxCollapseTags
:
2
,
span
:
12
,
},
{
type
:
"tree"
,
prop
:
"roles"
,
label
:
"角色"
,
placeholder
:
"请选择角色"
,
data
:
rolesData
.
value
,
clearable
:
true
,
filterable
:
true
,
checkStrictly
:
true
,
renderAfterExpand
:
false
,
showCheckbox
:
true
,
multiple
:
true
,
collapseTags
:
true
,
maxCollapseTags
:
2
,
span
:
12
,
},
{
type
:
"input"
,
prop
:
"mobile"
,
label
:
"手机号码"
,
placeholder
:
"请输入手机号码"
,
span
:
12
,
},
{
type
:
"radio"
,
prop
:
"enable"
,
label
:
"状态"
,
span
:
12
,
options
:
[
{
label
:
"启用"
,
value
:
"0"
},
{
label
:
"停用"
,
value
:
"1"
},
],
},
]);
// 表单验证规则
const
formRules
=
{};
// 事件处理函数
const
handleSearch
=
(
formData
)
=>
{
currentPage
.
value
=
1
;
loadTableData
();
};
const
handleReset
=
()
=>
{
searchForm
.
value
=
{
name
:
""
,
mobile
:
""
,
};
currentPage
.
value
=
1
;
loadTableData
();
};
const
handleSizeChange
=
(
size
)
=>
{
pageSize
.
value
=
size
;
currentPage
.
value
=
1
;
loadTableData
();
};
const
handleCurrentPageChange
=
(
page
)
=>
{
currentPage
.
value
=
page
;
loadTableData
();
};
// 新增用户
const
handleAdd
=
()
=>
{
isEdit
.
value
=
false
;
dialogTitle
.
value
=
"新增用户"
;
userForm
.
value
=
{
name
:
""
,
departs
:
[],
positions
:
[],
roles
:
[],
enable
:
"0"
,
};
loadDepartmentData
();
loadPositionsData
();
loadRolesData
();
dialogVisible
.
value
=
true
;
};
let
currentID
=
ref
();
// 编辑
const
handleEdit
=
(
row
,
index
)
=>
{
isEdit
.
value
=
true
;
dialogTitle
.
value
=
"编辑用户"
;
editIndex
.
value
=
index
;
proxy
.
$post
({
url
:
"/api/user/manage/getUserInfo"
,
data
:
{
id
:
row
.
id
},
callback
:
(
data
)
=>
{
userForm
.
value
=
{
...
data
};
currentID
.
value
=
data
.
id
;
},
error
:
(
err
)
=>
{
ElMessage
.
error
(
"编辑失败:"
,
err
);
},
});
dialogVisible
.
value
=
true
;
};
// 删除
const
handleDelete
=
async
(
row
,
index
)
=>
{
try
{
await
ElMessageBox
.
confirm
(
`确定要删除用户"
${
row
.
name
}
"吗?`
,
"提示"
,
{
confirmButtonText
:
"确定"
,
cancelButtonText
:
"取消"
,
type
:
"warning"
,
});
proxy
.
$post
({
url
:
"/api/user/manage/deleteUser"
,
data
:
{
id
:
row
.
id
},
callback
:
(
data
)
=>
{
dialogVisible
.
value
=
false
;
loadTableData
();
ElMessage
.
success
(
"删除成功"
);
},
error
:
(
err
)
=>
{
ElMessage
.
error
(
"删除失败:"
,
err
);
},
});
loadTableData
();
}
catch
{}
};
const
handleFormSubmit
=
(
formData
)
=>
{
if
(
isEdit
.
value
)
{
// 编辑用户
const
updateUser
=
{
...
formData
,
departs
:
Array
.
isArray
(
formData
.
departs
)
?
formData
.
departs
:
[],
positions
:
Array
.
isArray
(
formData
.
positions
)
?
formData
.
positions
:
[],
roles
:
Array
.
isArray
(
formData
.
roles
)
?
formData
.
roles
:
[],
id
:
currentID
.
value
,
};
proxy
.
$post
({
url
:
"/api/user/manage/updateUser"
,
data
:
updateUser
,
callback
:
(
data
)
=>
{
dialogVisible
.
value
=
false
;
loadTableData
();
ElMessage
.
success
(
"用户信息更新成功"
);
},
error
:
(
err
)
=>
{
ElMessage
.
error
(
"用户信息更新失败:"
,
err
);
},
});
}
else
{
// 新增用户
const
newUser
=
{
...
formData
,
departs
:
Array
.
isArray
(
formData
.
departs
)
?
formData
.
departs
:
[],
positions
:
Array
.
isArray
(
formData
.
positions
)
?
formData
.
positions
:
[],
roles
:
Array
.
isArray
(
formData
.
roles
)
?
formData
.
roles
:
[],
};
proxy
.
$post
({
url
:
"/api/user/manage/createUser"
,
data
:
newUser
,
callback
:
(
data
)
=>
{
dialogVisible
.
value
=
false
;
loadTableData
();
ElMessage
.
success
(
"用户添加成功"
);
},
error
:
(
err
)
=>
{
ElMessage
.
error
(
"用户添加失败:"
,
err
);
},
});
}
};
const
handleFormReset
=
()
=>
{
dialogVisible
.
value
=
false
;
};
const
handleDialogClose
=
()
=>
{
dialogVisible
.
value
=
false
;
};
// 处理状态切换
const
handleStatusChange
=
(
newValue
,
row
)
=>
{
const
newEnableValue
=
newValue
?
"0"
:
"1"
;
proxy
.
$post
({
url
:
"/api/user/manage/updateUser"
,
data
:
{
id
:
row
.
id
,
enable
:
newEnableValue
,
},
callback
:
(
data
)
=>
{
row
.
enable
=
newEnableValue
;
loadTableData
();
ElMessage
.
success
(
`用户状态已
${
newEnableValue
===
"0"
?
"启用"
:
"停用"
}
`
);
},
error
:
(
err
)
=>
{
ElMessage
.
error
(
"状态更新失败"
);
},
});
};
// 表格数据
const
loadTableData
=
()
=>
{
loading
.
value
=
true
;
proxy
.
$post
({
url
:
"/api/user/manage/listUser"
,
data
:
{
...
searchForm
.
value
,
page
:
currentPage
.
value
,
pageSize
:
pageSize
.
value
,
},
callback
:
(
data
)
=>
{
tableData
.
value
=
data
.
rows
.
map
((
item
)
=>
{
item
.
departs
=
item
.
departs
.
map
((
item
)
=>
item
.
name
).
join
(
","
);
item
.
positions
=
item
.
positions
.
map
((
item
)
=>
item
.
name
).
join
(
","
);
item
.
roles
=
item
.
roles
.
map
((
item
)
=>
item
.
name
).
join
(
","
);
return
item
;
});
total
.
value
=
data
.
count
;
loading
.
value
=
false
;
},
error
:
(
err
)
=>
{
loading
.
value
=
false
;
ElMessage
.
error
(
"加载数据失败"
);
},
});
};
onMounted
(()
=>
{
loadTableData
();
loadDepartmentData
();
loadPositionsData
();
loadRolesData
();
});
</
script
>
<
style
scoped
lang=
"less"
>
.user-manage {
padding: 20px;
background: rgba(157, 188, 218, 0.1);
height: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
.search-form {
background: rgba(255, 255, 255, 0.9);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
flex-shrink: 0;
}
.table-container {
background: rgba(255, 255, 255, 0.9);
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
// overflow: hidden;
// display: flex;
// flex-direction: column;
}
}
</
style
>
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment