mirror of
https://github.com/fugary/simple-element-plus-template.git
synced 2025-11-12 14:27:49 +00:00
1. 升级依赖版本
This commit is contained in:
4
.env
4
.env
@@ -1,7 +1,7 @@
|
||||
# 程序名称
|
||||
VITE_APP_NAME=Simple Element+
|
||||
# 接口地址
|
||||
VITE_APP_API_BASE_URL=https://www.fastmock.site/mock/80793bea9d60828fda74202f7017e953/simple
|
||||
VITE_APP_API_BASE_URL=/simple
|
||||
# 超时配置
|
||||
VITE_APP_API_TIMEOUT=10000
|
||||
|
||||
VITE_APP_SYSTEM_KEY=SIMPLE-ELEMENT
|
||||
|
||||
75
mock/MockCity.js
Normal file
75
mock/MockCity.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import Mock from 'mockjs'
|
||||
export default [
|
||||
{
|
||||
url: '/simple/city/autoCities',
|
||||
method: 'post',
|
||||
response: request => {
|
||||
return {
|
||||
success: true,
|
||||
message: 'Success',
|
||||
resultData: function () {
|
||||
let pageSize = 10
|
||||
if (request.body.page) {
|
||||
pageSize = request.body.page.pageSize || 10
|
||||
}
|
||||
const total = 99
|
||||
const pageCount = parseInt((total + pageSize - 1) / pageSize)
|
||||
const result = {
|
||||
page: {
|
||||
pageSize: function () {
|
||||
return pageSize
|
||||
},
|
||||
pageNumber: function () {
|
||||
if (request.body.page) {
|
||||
return request.body.page.pageNumber || 1
|
||||
}
|
||||
return 1
|
||||
},
|
||||
pageCount,
|
||||
totalCount: total
|
||||
}
|
||||
}
|
||||
let size = 10
|
||||
if (request.body.page) {
|
||||
size = request.body.page.pageSize
|
||||
}
|
||||
result['cityList|' + size] = [{
|
||||
code: function () {
|
||||
return Mock.Random.string('ABCDEFGHIJKLMNOPQRSTUVWXYZ', 3)
|
||||
},
|
||||
nameCn: function () {
|
||||
return Mock.Random.city()
|
||||
},
|
||||
nameEn: function () {
|
||||
return 'En' + this.nameCn
|
||||
}
|
||||
}]
|
||||
return Mock.mock(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/simple/city/selectCities',
|
||||
method: 'post',
|
||||
response: request => {
|
||||
return {
|
||||
success: true,
|
||||
message: 'Success',
|
||||
resultData: {
|
||||
'cityList|20-70': [{
|
||||
code: function () {
|
||||
return Mock.Random.string('ABCDEFGHIJKLMNOPQRSTUVWXYZ', 3)
|
||||
},
|
||||
nameCn: function () {
|
||||
return Mock.Random.city()
|
||||
},
|
||||
nameEn: function () {
|
||||
return 'En' + this.nameCn
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
31
mock/MockLogin.js
Normal file
31
mock/MockLogin.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
*/
|
||||
export default [{
|
||||
url: '/simple/login',
|
||||
method: 'post',
|
||||
response: (request) => {
|
||||
return {
|
||||
success: function () {
|
||||
return request.body.userName === 'admin' && request.body.userPassword === '123456'
|
||||
},
|
||||
message: function () {
|
||||
return this.success ? '登录成功' : '用户名或密码错误'
|
||||
},
|
||||
resultData: function () {
|
||||
if (this.success) {
|
||||
return {
|
||||
account: {
|
||||
userNameEN: 'Tom',
|
||||
userNameCN: '汤姆',
|
||||
gender: 'male',
|
||||
email: 'tomcat@fugary.com'
|
||||
},
|
||||
accessToken: 'abcdefghijklmn'
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
159
mock/MockMenus.js
Normal file
159
mock/MockMenus.js
Normal file
@@ -0,0 +1,159 @@
|
||||
const allMenus = [
|
||||
{
|
||||
id: 1,
|
||||
iconCls: 'setting',
|
||||
nameCn: '系统管理',
|
||||
nameEn: 'System'
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
parentId: 1,
|
||||
iconCls: 'user',
|
||||
nameCn: '用户管理',
|
||||
nameEn: 'Users',
|
||||
menuUrl: '/admin/users'
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
parentId: 1,
|
||||
iconCls: 'GroupOutlined',
|
||||
nameCn: '角色管理',
|
||||
nameEn: 'Roles',
|
||||
menuUrl: '/admin/roles'
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
parentId: 1,
|
||||
iconCls: 'lock',
|
||||
nameCn: '权限管理',
|
||||
nameEn: 'Authority',
|
||||
menuUrl: '/admin/authority'
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
parentId: 1,
|
||||
iconCls: 'GroupsOutlined',
|
||||
nameCn: '用户组管理',
|
||||
nameEn: 'Groups',
|
||||
menuUrl: '/admin/groups'
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
parentId: 1,
|
||||
iconCls: 'SupervisedUserCircleOutlined',
|
||||
nameCn: '租户管理',
|
||||
nameEn: 'Tenants',
|
||||
menuUrl: '/admin/tenants'
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
parentId: 1,
|
||||
iconCls: 'menu',
|
||||
nameCn: '菜单管理',
|
||||
nameEn: 'Menus',
|
||||
menuUrl: '/admin/menus'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
iconCls: 'BuildFilled',
|
||||
nameCn: '常用工具',
|
||||
nameEn: 'Tools'
|
||||
},
|
||||
{
|
||||
id: 21,
|
||||
parentId: 2,
|
||||
iconCls: 'InsertEmoticonOutlined',
|
||||
nameCn: '图标管理',
|
||||
nameEn: 'Icons',
|
||||
menuUrl: '/icons'
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
parentId: 2,
|
||||
iconCls: 'TableRowsFilled',
|
||||
nameCn: '表单示例',
|
||||
nameEn: 'Forms',
|
||||
menuUrl: '/forms'
|
||||
},
|
||||
{
|
||||
id: 23,
|
||||
parentId: 2,
|
||||
iconCls: 'Grid',
|
||||
nameCn: '表格示例',
|
||||
nameEn: 'Tables',
|
||||
menuUrl: '/tables'
|
||||
},
|
||||
{
|
||||
id: 24,
|
||||
parentId: 2,
|
||||
iconCls: 'TipsAndUpdatesOutlined',
|
||||
nameCn: '其他示例',
|
||||
nameEn: 'Others',
|
||||
menuUrl: '/tests'
|
||||
}
|
||||
]
|
||||
|
||||
export default [{
|
||||
url: '/simple/api/menus',
|
||||
method: 'post',
|
||||
response: () => {
|
||||
return {
|
||||
success: true,
|
||||
message: 'Success',
|
||||
resultData: {
|
||||
menuList: allMenus
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
url: '/simple/api/searchMenus',
|
||||
method: 'post',
|
||||
response: request => {
|
||||
return {
|
||||
success: true,
|
||||
message: 'Success',
|
||||
resultData: function () {
|
||||
const menuList = allMenus
|
||||
let pageSize = 10
|
||||
let pageNumber = 1
|
||||
if (request.body.page) {
|
||||
pageSize = +request.body.page.pageSize || 10
|
||||
pageNumber = +request.body.page.pageNumber || 1
|
||||
}
|
||||
const total = menuList.length
|
||||
const pageCount = (total + pageSize - 1) / pageSize
|
||||
const result = {
|
||||
page: {
|
||||
pageSize: function () {
|
||||
return pageSize
|
||||
},
|
||||
pageNumber: function ({ request }) {
|
||||
if (request.body.page) {
|
||||
return request.body.page.pageNumber || 1
|
||||
}
|
||||
return 1
|
||||
},
|
||||
pageCount,
|
||||
totalCount: total
|
||||
}
|
||||
}
|
||||
result.menuList = menuList.slice((pageNumber - 1) * pageSize, pageNumber * pageSize)
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
url: '/simple/api/menus/:id',
|
||||
method: 'get',
|
||||
response: request => {
|
||||
return {
|
||||
success: true,
|
||||
message: 'Success',
|
||||
resultData: function () {
|
||||
return {
|
||||
menu: allMenus.filter(menu => menu.id === +request.query.id)[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
74
mock/MockUsers.js
Normal file
74
mock/MockUsers.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import Mock from 'mockjs'
|
||||
export default [
|
||||
{
|
||||
url: '/simple/api/users',
|
||||
method: 'post',
|
||||
response: request => {
|
||||
return {
|
||||
success: true,
|
||||
message: 'Success',
|
||||
resultData: function () {
|
||||
let pageSize = 10
|
||||
if (request.body.page) {
|
||||
pageSize = +request.body.page.pageSize || 10
|
||||
}
|
||||
const total = 999
|
||||
const pageCount = (total + pageSize - 1) / pageSize
|
||||
const result = {
|
||||
page: {
|
||||
pageSize: function () {
|
||||
return pageSize
|
||||
},
|
||||
pageNumber: function () {
|
||||
if (request.body.page) {
|
||||
return request.body.page.pageNumber || 1
|
||||
}
|
||||
return 1
|
||||
},
|
||||
pageCount,
|
||||
totalCount: total
|
||||
}
|
||||
}
|
||||
let size = 10
|
||||
if (request.body.page) {
|
||||
size = request.body.page.pageSize
|
||||
}
|
||||
result['userList|' + size] = [{
|
||||
id: '@id',
|
||||
'gender|1': ['male', 'female'],
|
||||
nameCn: '@cname',
|
||||
nameEn: '@name',
|
||||
address: function () {
|
||||
return Mock.Random.city(true)
|
||||
},
|
||||
birthday: '@date'
|
||||
}]
|
||||
return Mock.mock(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
url: '/simple/api/users/:id',
|
||||
method: 'get',
|
||||
response: request => {
|
||||
return {
|
||||
success: true,
|
||||
message: 'Success',
|
||||
resultData: {
|
||||
user: {
|
||||
id: function () {
|
||||
return request.query.id
|
||||
},
|
||||
'gender|1': ['male', 'female'],
|
||||
nameCn: '@cname',
|
||||
nameEn: '@name',
|
||||
address: function () {
|
||||
return Mock.Random.city(true)
|
||||
},
|
||||
birthday: '@date'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
1445
package-lock.json
generated
1445
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@@ -11,27 +11,29 @@
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@vicons/material": "^0.12.0",
|
||||
"@vueuse/core": "^10.7.0",
|
||||
"axios": "^1.6.3",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"axios": "^1.6.8",
|
||||
"dayjs": "^1.11.10",
|
||||
"element-plus": "^2.4.4",
|
||||
"element-plus": "^2.6.3",
|
||||
"lodash": "^4.17.21",
|
||||
"mockjs": "^1.1.0",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia-plugin-persistedstate": "^3.2.1",
|
||||
"vue": "^3.3.13",
|
||||
"vue-i18n": "^9.8.0",
|
||||
"vue-router": "^4.2.5",
|
||||
"vite-plugin-mock": "^3.0.1",
|
||||
"vue": "^3.4.21",
|
||||
"vue-i18n": "^9.10.2",
|
||||
"vue-router": "^4.3.0",
|
||||
"vue-virtual-scroller": "^2.0.0-beta.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.6.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
||||
"@typescript-eslint/parser": "^6.18.1",
|
||||
"@vitejs/plugin-vue": "^4.5.2",
|
||||
"@rushstack/eslint-patch": "^1.10.1",
|
||||
"@typescript-eslint/eslint-plugin": "^7.4.0",
|
||||
"@typescript-eslint/parser": "^7.4.0",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue/eslint-config-standard": "^8.0.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-vue": "^9.19.2",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.10"
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-vue": "^9.24.0",
|
||||
"typescript": "^5.4.3",
|
||||
"vite": "^5.2.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,23 +19,62 @@ html, body, #app, .index-container {
|
||||
border-right: 0 none;
|
||||
}
|
||||
|
||||
.common-el-tooltip {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.common-dropdown .el-dropdown-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.el-dropdown-link:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.el-menu-left:not(.el-menu--collapse) {
|
||||
width: 250px;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.no-padding {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.padding-5 {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.padding-10 {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.padding-15 {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.padding-main {
|
||||
padding: var(--el-main-padding);
|
||||
}
|
||||
|
||||
.padding-left1 {
|
||||
padding-left: 5px;
|
||||
}
|
||||
.padding-left2 {
|
||||
padding-left: 10px;
|
||||
}
|
||||
.padding-left3 {
|
||||
padding-left: 15px;
|
||||
}
|
||||
.padding-right1 {
|
||||
padding-right: 5px;
|
||||
}
|
||||
.padding-right2 {
|
||||
padding-right: 10px;
|
||||
}
|
||||
.padding-right3 {
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.padding-top1 {
|
||||
padding-top: 5px;
|
||||
@@ -43,12 +82,59 @@ html, body, #app, .index-container {
|
||||
.padding-top2 {
|
||||
padding-top: 10px;
|
||||
}
|
||||
.padding-top3 {
|
||||
padding-top: 15px;
|
||||
}
|
||||
.padding-bottom1 {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.padding-bottom2 {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.padding-bottom3 {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.margin-bottom1 {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.margin-bottom2 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.margin-bottom3 {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.margin-left1 {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.margin-left2 {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.margin-left3 {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.margin-right1 {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.margin-right2 {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.margin-right3 {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.margin-top1 {
|
||||
margin-top: 5px;
|
||||
}
|
||||
.margin-top2 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.margin-top3 {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -94,6 +180,10 @@ html, body, #app, .index-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.container-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -101,22 +191,63 @@ html, body, #app, .index-container {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.common-form.el-form--inline .el-input{
|
||||
--el-input-width: 220px;
|
||||
.reason-code-container .el-radio__label{
|
||||
white-space: break-spaces;
|
||||
}
|
||||
|
||||
.common-tabs .el-tabs__new-tab {
|
||||
width: 50%;
|
||||
border: none;
|
||||
margin: 10px 10px 0 10px;
|
||||
justify-content: right;
|
||||
}
|
||||
|
||||
.el-tabs__item.is-active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.common-form.el-form--inline .el-select,
|
||||
.common-subform.el-form--inline .el-select{
|
||||
--el-select-width: 200px;
|
||||
}
|
||||
|
||||
.common-form.el-form--inline .el-input,
|
||||
.common-subform.el-form--inline .el-input{
|
||||
--el-input-width: 220px;
|
||||
--el-input-width: 200px;
|
||||
--el-date-editor-width: 200px;
|
||||
}
|
||||
|
||||
.common-form-small.common-form.el-form--inline .el-select{
|
||||
--el-select-width: 160px;
|
||||
}
|
||||
|
||||
.common-form-small.common-form.el-form--inline .el-input{
|
||||
--el-input-width: 160px;
|
||||
--el-date-editor-width: 160px;
|
||||
}
|
||||
|
||||
.form-edit-width-70 {
|
||||
width:70%
|
||||
}
|
||||
|
||||
.form-edit-width-90 {
|
||||
width:90%
|
||||
}
|
||||
|
||||
.form-edit-width-100 {
|
||||
width:100%
|
||||
}
|
||||
|
||||
.form-edit-width-70 .el-select:not(:is(.el-form--inline .el-select,.el-pagination .el-select)),
|
||||
.form-edit-width-90 .el-select:not(:is(.el-form--inline .el-select,.el-pagination .el-select)),
|
||||
.form-edit-width-100 .el-select:not(:is(.el-form--inline .el-select,.el-pagination .el-select)){
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.common-autocomplete .el-popover__title{
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
@@ -133,6 +264,8 @@ html, body, #app, .index-container {
|
||||
|
||||
.common-autocomplete .common-select-page .el-tabs__item {
|
||||
height: 30px;
|
||||
padding-left: 10px !important;
|
||||
padding-right: 10px !important;
|
||||
}
|
||||
|
||||
.common-autocomplete .common-select-page .common-select-page-btn {
|
||||
@@ -146,6 +279,90 @@ html, body, #app, .index-container {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.common-hide-expand.el-table .el-table__expand-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.exchange-button {
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
}
|
||||
|
||||
.small-card .el-card__header{
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.small-card.el-card {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.table-card .el-card__body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.product-book-card .el-card__body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.operation-card .card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.common-data-row label {
|
||||
font-weight: 600 ;
|
||||
}
|
||||
|
||||
.common-data-row .el-col {
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
.el-dropdown+.el-button {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.segment-label {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.common-form-auto {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.common-form-auto .el-form-item {
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
}
|
||||
|
||||
.common-form-auto .el-form-item__content {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.common-form-auto .el-form--inline .el-input,
|
||||
.common-form-auto.el-form--inline .el-input,
|
||||
.common-form-auto .el-form--inline .el-select,
|
||||
.common-form-auto.el-form--inline .el-select {
|
||||
--el-input-width: 100%;
|
||||
--el-date-editor-width: 100%;
|
||||
}
|
||||
|
||||
.common-form-auto.el-form--inline .el-form-item,
|
||||
.common-form-auto .el-form--inline .el-form-item {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.el-form-item.is-required .common-form-label .common-form-label-text:before {
|
||||
content: "*";
|
||||
color: var(--el-color-danger);
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.flex-grow2 {
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* slide-fade动画
|
||||
*/
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { debounce, isEmpty, isObject } from 'lodash'
|
||||
import { debounce, isEmpty, isObject, cloneDeep, chunk } from 'lodash'
|
||||
import { onClickOutside, onKeyStroke, useVModel } from '@vueuse/core'
|
||||
import chunk from 'lodash/chunk'
|
||||
import { UPDATE_MODEL_EVENT, CHANGE_EVENT, useFormItem } from 'element-plus'
|
||||
|
||||
/**
|
||||
@@ -51,7 +50,7 @@ const props = defineProps({
|
||||
},
|
||||
inputWidth: {
|
||||
type: String,
|
||||
default: '200px'
|
||||
default: ''
|
||||
},
|
||||
autocompleteConfig: {
|
||||
type: Object,
|
||||
@@ -128,10 +127,13 @@ const selectPageData = ref({})
|
||||
const selectPageTab = ref(null)
|
||||
const popoverVisible = ref(false)
|
||||
const autocompletePopover = ref()
|
||||
const autoPage = ref({
|
||||
pageSize: 8,
|
||||
const defaultAutoPage = {
|
||||
pageSize: props.autocompleteConfig?.pageSize || 8,
|
||||
pageNumber: 1
|
||||
})
|
||||
}
|
||||
const autoPage = ref(props.autocompleteConfig?.frontendPaging
|
||||
? null
|
||||
: cloneDeep(defaultAutoPage))
|
||||
const loadingData = ref(false)
|
||||
|
||||
const idProp = computed(() => props.autocompleteConfig?.idProp || props.idProp)
|
||||
@@ -194,10 +196,21 @@ const onInputKeywords = debounce((input) => {
|
||||
}
|
||||
}, props.debounceTime)
|
||||
|
||||
const calcDefaultLabelFunc = () => {
|
||||
if (!props.useIdModel) {
|
||||
const value = props.modelValue
|
||||
return value && isObject(value) ? value[labelProp.value] : ''
|
||||
}
|
||||
return props.defaultLabel
|
||||
}
|
||||
|
||||
const calcDefaultLabel = computed(calcDefaultLabelFunc)
|
||||
|
||||
onMounted(() => {
|
||||
onClickOutside(autocompletePopover.value?.popperRef?.contentRef, () => {
|
||||
popoverVisible.value = false
|
||||
})
|
||||
setAutocompleteLabel(calcDefaultLabel.value)
|
||||
})
|
||||
|
||||
watch(() => popoverVisible.value, (val) => {
|
||||
@@ -211,7 +224,6 @@ watch(() => popoverVisible.value, (val) => {
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (value) => {
|
||||
console.info('=====================value', value)
|
||||
if (!props.useIdModel) {
|
||||
setAutocompleteLabel(value && isObject(value) ? value[labelProp.value] : '')
|
||||
if (isEmpty(value)) {
|
||||
@@ -220,13 +232,7 @@ watch(() => props.modelValue, (value) => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => {
|
||||
if (!props.useIdModel) {
|
||||
const value = props.modelValue
|
||||
return value && isObject(value) ? value[labelProp.value] : ''
|
||||
}
|
||||
return props.defaultLabel
|
||||
}, (label) => {
|
||||
watch(calcDefaultLabelFunc, (label) => {
|
||||
setAutocompleteLabel(label)
|
||||
})
|
||||
|
||||
@@ -287,7 +293,6 @@ const moveSelection = function (down) {
|
||||
currentOnIndex.value = -1
|
||||
currentOnRow.value = null
|
||||
}
|
||||
console.info('=================', tableRef.value.table, currentOnIndex.value, currentOnRow.value)
|
||||
tableRef.value.table?.setCurrentRow(currentOnRow.value)
|
||||
}
|
||||
|
||||
@@ -349,6 +354,15 @@ watch(() => props.selectPageConfig, () => {
|
||||
selectPageTab.value = null
|
||||
})
|
||||
|
||||
watch(() => props.autocompleteConfig, (autocompleteConfig) => {
|
||||
defaultAutoPage.pageSize = autocompleteConfig.pageSize || 8
|
||||
if (autocompleteConfig.frontendPaging) {
|
||||
autoPage.value = null
|
||||
} else {
|
||||
autoPage.value = cloneDeep(defaultAutoPage)
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -443,6 +457,8 @@ watch(() => props.selectPageConfig, () => {
|
||||
:empty-text="autocompleteConfig.emptyMessage"
|
||||
:data="dataList"
|
||||
:page-attrs="pageAttrs"
|
||||
:frontend-paging="autocompleteConfig.frontendPaging"
|
||||
:frontend-page-size="defaultAutoPage.pageSize"
|
||||
@row-click="onSelectData($event)"
|
||||
@current-page-change="onInputKeywords(false)"
|
||||
>
|
||||
|
||||
12
src/components/common-autocomplete/public.d.ts
vendored
12
src/components/common-autocomplete/public.d.ts
vendored
@@ -19,6 +19,18 @@ export interface CommonSelectPageOption {
|
||||
}
|
||||
|
||||
export interface CommonAutocompleteOption {
|
||||
/** id属性名 */
|
||||
labelProp?: string;
|
||||
/** label属性名 */
|
||||
idProp?: string;
|
||||
/**
|
||||
* 分页数
|
||||
*/
|
||||
pageSize: number;
|
||||
/**
|
||||
* 前端分页模式
|
||||
*/
|
||||
frontendPaging: boolean;
|
||||
/** 自动完成表格列配置 */
|
||||
columns: Array<CommonTableColumn>;
|
||||
/** 空数据提示信息 */
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed } from 'vue'
|
||||
import { useTabsViewStore } from '@/stores/TabsViewStore'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useMenuInfo, useMenuName } from '@/components/utils'
|
||||
import { parsePathParams, useMenuInfo, useMenuName } from '@/components/utils'
|
||||
|
||||
const tabsViewStore = useTabsViewStore()
|
||||
|
||||
@@ -20,7 +20,7 @@ const breadcrumbs = computed(() => {
|
||||
icon = item.meta.icon
|
||||
}
|
||||
return {
|
||||
path: item.path,
|
||||
path: parsePathParams(item.path, route.params),
|
||||
menuName: useMenuName(item),
|
||||
icon
|
||||
}
|
||||
@@ -32,11 +32,11 @@ const breadcrumbs = computed(() => {
|
||||
return notExist && !item.menuName.endsWith('Base')
|
||||
})
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-breadcrumb
|
||||
v-bind="$attrs"
|
||||
class="common-breadcrumb"
|
||||
>
|
||||
<el-breadcrumb-item
|
||||
|
||||
136
src/components/common-form-control/common-filter-control.vue
Normal file
136
src/components/common-form-control/common-filter-control.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<script setup>
|
||||
import { get, isArray, isObject, set } from 'lodash'
|
||||
import { computed, onMounted, useSlots } from 'vue'
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* @type {CommonFormOption}
|
||||
*/
|
||||
option: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
model: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
labelWidth: {
|
||||
type: String,
|
||||
default: '150px'
|
||||
},
|
||||
prop: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
unlimitedEnable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
const calcFilterType = computed(() => props.option.type || 'checkbox-group')
|
||||
|
||||
const isUnlimited = computed(() => {
|
||||
const option = props.option
|
||||
const value = get(props.model, option.prop)
|
||||
const filterType = calcFilterType.value
|
||||
if (filterType === 'common-tab-filter') {
|
||||
if (isObject(value) && option.attrs?.tabs && option.attrs?.tabs.length) {
|
||||
let unlimited = true
|
||||
option.attrs.tabs.forEach(tab => {
|
||||
const tabVal = get(value, tab.prop)
|
||||
if (unlimited) {
|
||||
if (isArray(tabVal)) {
|
||||
if (tabVal.length) {
|
||||
unlimited = false
|
||||
}
|
||||
} else {
|
||||
unlimited = !tabVal
|
||||
}
|
||||
}
|
||||
})
|
||||
return unlimited
|
||||
}
|
||||
}
|
||||
return !value || !value.length
|
||||
})
|
||||
|
||||
const setUnlimited = () => {
|
||||
const option = props.option
|
||||
const filterType = calcFilterType.value
|
||||
let value
|
||||
if (filterType.includes('checkbox')) {
|
||||
value = []
|
||||
} else if (filterType === 'common-tab-filter') {
|
||||
value = {}
|
||||
}
|
||||
set(props.model, option.prop, value)
|
||||
}
|
||||
|
||||
const initFilterModel = () => {
|
||||
const filterType = calcFilterType.value
|
||||
if (filterType === 'common-tab-filter') {
|
||||
const value = get(props.model, props.option.prop)
|
||||
if (!value) {
|
||||
set(props.model, props.option.prop, {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initFilterModel()
|
||||
})
|
||||
|
||||
const newOption = computed(() => {
|
||||
const option = cloneDeep(props.option)
|
||||
option.type = calcFilterType.value
|
||||
return option
|
||||
})
|
||||
|
||||
const slots = computed(() => {
|
||||
const tmpSlots = cloneDeep(useSlots())
|
||||
delete tmpSlots.afterLabel
|
||||
return tmpSlots
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<common-form-control
|
||||
:label-width="labelWidth"
|
||||
:model="model"
|
||||
:option="newOption"
|
||||
>
|
||||
<template
|
||||
v-if="unlimitedEnable"
|
||||
#afterLabel
|
||||
>
|
||||
<slot name="afterLabel" />
|
||||
<el-link
|
||||
class="margin-left1 margin-top1"
|
||||
type="primary"
|
||||
:underline="false"
|
||||
@click="setUnlimited()"
|
||||
>
|
||||
<el-tag
|
||||
size="small"
|
||||
:effect="isUnlimited?'dark':'light'"
|
||||
>
|
||||
{{ $t('common.label.unlimited') }}
|
||||
</el-tag>
|
||||
</el-link>
|
||||
</template>
|
||||
<template
|
||||
v-for="(slot, slotKey) in slots"
|
||||
:key="slotKey"
|
||||
#[slotKey]
|
||||
>
|
||||
<slot :name="slotKey" />
|
||||
</template>
|
||||
</common-form-control>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
23
src/components/common-form-control/common-form-label.vue
Normal file
23
src/components/common-form-control/common-form-label.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="common-form-label">
|
||||
<slot />
|
||||
<component
|
||||
:is="component"
|
||||
v-if="component"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
89
src/components/common-form-control/common-tab-filter.vue
Normal file
89
src/components/common-form-control/common-tab-filter.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<script setup>
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import ControlChild from '@/components/common-form-control/control-child.vue'
|
||||
import { computed } from 'vue'
|
||||
import { useInputType } from '@/components/utils'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
tabs: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'checkbox-group'
|
||||
},
|
||||
defaultIcon: {
|
||||
type: String,
|
||||
default: 'CaretBottom'
|
||||
},
|
||||
iconSize: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const vModel = useVModel(props, 'modelValue', emit)
|
||||
|
||||
const childTypeMapping = { // 自动映射子元素类型,配置的时候可以不写type
|
||||
'checkbox-group': 'checkbox',
|
||||
'radio-group': 'radio',
|
||||
select: 'option'
|
||||
}
|
||||
|
||||
const calcTabs = computed(() => {
|
||||
return props.tabs.map(tab => {
|
||||
tab.children = (tab.children || []).map(child => {
|
||||
child.type = child.type || childTypeMapping[props.type]
|
||||
return child
|
||||
})
|
||||
if (!tab.icon) {
|
||||
tab.icon = props.defaultIcon
|
||||
}
|
||||
return tab
|
||||
})
|
||||
})
|
||||
|
||||
const inputType = computed(() => useInputType({ type: props.type }))
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-tabs
|
||||
type="border-card"
|
||||
class="form-edit-width-100"
|
||||
>
|
||||
<el-tab-pane
|
||||
v-for="(tab, index) in calcTabs"
|
||||
:key="tab.label + index"
|
||||
>
|
||||
<template #label>
|
||||
{{ tab.label }}
|
||||
<common-icon
|
||||
:size="tab.iconSize||iconSize"
|
||||
:icon="tab.icon"
|
||||
/>
|
||||
</template>
|
||||
<component
|
||||
:is="inputType"
|
||||
v-if="vModel"
|
||||
v-model="vModel[tab.prop]"
|
||||
@change="vModel._tabFilter=true"
|
||||
>
|
||||
<control-child
|
||||
v-for="(childItem, childIdx) in tab.children"
|
||||
:key="childIdx"
|
||||
:option="childItem"
|
||||
/>
|
||||
</component>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { $i18nBundle } from '@/messages'
|
||||
import { useInputType } from '@/components/utils'
|
||||
import { toLabelByKey, useInputType } from '@/components/utils'
|
||||
|
||||
/**
|
||||
* @type {{option:CommonFormOption}}
|
||||
@@ -21,31 +20,17 @@ const inputType = computed(() => useInputType(props.option))
|
||||
const label = computed(() => {
|
||||
const option = props.option
|
||||
if (option.labelKey) {
|
||||
return $i18nBundle(option.labelKey)
|
||||
return toLabelByKey(option.labelKey)
|
||||
}
|
||||
return option.label
|
||||
})
|
||||
/**
|
||||
* element-plus的复选框和单选框没有value值,只有label用于存储值,因此特殊处理
|
||||
* @type {string[]}
|
||||
*/
|
||||
const labelAsValueKeys = ['el-checkbox', 'el-radio', 'el-checkbox-button', 'el-radio-button']
|
||||
|
||||
const labelOrValue = computed(() => {
|
||||
const option = props.option
|
||||
if (labelAsValueKeys.includes(inputType.value)) {
|
||||
return option.value
|
||||
}
|
||||
return label.value
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="inputType"
|
||||
:value="option.value"
|
||||
:label="labelOrValue"
|
||||
:label="label"
|
||||
v-bind="option.attrs"
|
||||
>
|
||||
{{ label }}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, isVNode, ref, watch } from 'vue'
|
||||
import { $i18nBundle } from '@/messages'
|
||||
import ControlChild from '@/components/common-form-control/control-child.vue'
|
||||
import { useInputType } from '@/components/utils'
|
||||
import { toLabelByKey, useInputType } from '@/components/utils'
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import { get, set } from 'lodash'
|
||||
import { get, isFunction, set } from 'lodash'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
/**
|
||||
* @type {{option:CommonFormOption}}
|
||||
@@ -24,13 +25,27 @@ const props = defineProps({
|
||||
labelWidth: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
addInfo: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const inputType = computed(() => useInputType(props.option))
|
||||
const calcOption = computed(() => {
|
||||
let option = props.option
|
||||
if (isFunction(option.dynamicOption)) {
|
||||
option = { ...option, ...option.dynamicOption(props.model, option, props.addInfo) }
|
||||
} else if (isFunction(option.dynamicAttrs)) {
|
||||
option = { ...option, attrs: { ...option.attrs, ...option.dynamicAttrs(props.model, option, props.addInfo) } }
|
||||
}
|
||||
return option
|
||||
})
|
||||
|
||||
const inputType = computed(() => useInputType(calcOption.value))
|
||||
|
||||
const modelAttrs = computed(() => {
|
||||
const option = props.option
|
||||
const option = calcOption.value
|
||||
const attrs = { ...option.attrs }
|
||||
if (attrs.clearable === undefined && ['el-input', 'el-select', 'el-select-v2', 'common-autocomplete', 'el-autocomplete', 'el-cascader', 'el-tree-select'].includes(inputType.value)) {
|
||||
attrs.clearable = true
|
||||
@@ -38,33 +53,67 @@ const modelAttrs = computed(() => {
|
||||
if (inputType.value === 'common-autocomplete' && option.getAutocompleteLabel) {
|
||||
attrs.defaultLabel = option.getAutocompleteLabel(props.model, option)
|
||||
}
|
||||
if (inputType.value === 'el-date-picker') {
|
||||
attrs.disabledDate = (date) => {
|
||||
const option = calcOption.value
|
||||
let result = false
|
||||
if (option.minDate) {
|
||||
result = date.getTime() < dayjs(option.minDate).startOf('d').toDate().getTime()
|
||||
}
|
||||
if (!result && option.maxDate) {
|
||||
result = date.getTime() > dayjs(option.maxDate).startOf('d').toDate().getTime()
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
const defaultValue = modelValue.value || option.minDate
|
||||
if (defaultValue) {
|
||||
attrs.defaultValue = dayjs(defaultValue).toDate()
|
||||
}
|
||||
return attrs
|
||||
})
|
||||
|
||||
watch(() => [inputType.value, calcOption.value.minDate, calcOption.value.maxDate], ([type, minDate, maxDate]) => {
|
||||
const option = calcOption.value
|
||||
const date = modelValue.value
|
||||
if (type === 'el-date-picker' && date && !option.disabled && option.clearInvalidDate !== false) {
|
||||
let invalid = false
|
||||
if (minDate) {
|
||||
invalid = dayjs(date).isBefore(dayjs(option.minDate).startOf('d'))
|
||||
}
|
||||
if (invalid && maxDate) {
|
||||
invalid = dayjs(date).isAfter(dayjs(option.maxDate).startOf('d'))
|
||||
}
|
||||
if (invalid) {
|
||||
modelValue.value = undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const label = computed(() => {
|
||||
const option = props.option
|
||||
const option = calcOption.value
|
||||
if (option.labelKey) {
|
||||
return $i18nBundle(option.labelKey)
|
||||
return toLabelByKey(option.labelKey)
|
||||
}
|
||||
return option.label
|
||||
})
|
||||
|
||||
const showLabel = computed(() => {
|
||||
return props.option.showLabel !== false && props.labelWidth !== '0'
|
||||
return calcOption.value.showLabel !== false && props.labelWidth !== '0'
|
||||
})
|
||||
|
||||
const formModel = computed(() => props.option.model || props.model)
|
||||
const formModel = computed(() => calcOption.value.model || props.model)
|
||||
|
||||
const modelValue = computed({
|
||||
get () {
|
||||
if (formModel.value && props.option.prop) {
|
||||
return get(formModel.value, props.option.prop)
|
||||
if (formModel.value && calcOption.value.prop) {
|
||||
return get(formModel.value, calcOption.value.prop)
|
||||
}
|
||||
return null
|
||||
},
|
||||
set (val) {
|
||||
if (formModel.value && props.option.prop) {
|
||||
set(formModel.value, props.option.prop, val)
|
||||
if (formModel.value && calcOption.value.prop) {
|
||||
set(formModel.value, calcOption.value.prop, val)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -76,7 +125,7 @@ const childTypeMapping = { // 自动映射子元素类型,配置的时候可
|
||||
}
|
||||
|
||||
const children = computed(() => {
|
||||
const option = props.option
|
||||
const option = calcOption.value
|
||||
const result = option.children || [] // 初始化一些默认值
|
||||
result.forEach(childItem => {
|
||||
if (!childItem.type) {
|
||||
@@ -89,11 +138,11 @@ const children = computed(() => {
|
||||
const formItemRef = ref()
|
||||
|
||||
const rules = computed(() => {
|
||||
const option = props.option
|
||||
const option = calcOption.value
|
||||
let _rules = cloneDeep(option.rules || [])
|
||||
if (option.prop) {
|
||||
if (option.required !== undefined) {
|
||||
const label = option.label || $i18nBundle(option.labelKey)
|
||||
const label = option.label || toLabelByKey(option.labelKey)
|
||||
_rules = [{
|
||||
trigger: option.trigger,
|
||||
required: option.required,
|
||||
@@ -101,7 +150,7 @@ const rules = computed(() => {
|
||||
}, ..._rules]
|
||||
}
|
||||
if (option.pattern !== undefined) {
|
||||
const label = option.label || $i18nBundle(option.labelKey)
|
||||
const label = option.label || toLabelByKey(option.labelKey)
|
||||
_rules = [{
|
||||
pattern: option.pattern,
|
||||
message: option.patternMsg || $i18nBundle('common.msg.patternInvalid', [label])
|
||||
@@ -114,7 +163,7 @@ const rules = computed(() => {
|
||||
|
||||
const initFormModel = () => {
|
||||
if (formModel.value) {
|
||||
const option = props.option
|
||||
const option = calcOption.value
|
||||
if (option.prop) {
|
||||
const defaultVal = get(formModel.value, option.prop)
|
||||
set(formModel.value, option.prop, defaultVal ?? option.value ?? undefined)
|
||||
@@ -124,23 +173,48 @@ const initFormModel = () => {
|
||||
|
||||
initFormModel()
|
||||
|
||||
watch(() => props.option, initFormModel, { deep: true })
|
||||
watch(() => calcOption.value, initFormModel, { deep: true })
|
||||
|
||||
const hasModelText = computed(() => {
|
||||
return !!(modelAttrs.value.modelText || modelAttrs.value.modelTextFunc)
|
||||
return modelAttrs.value.modelText || calcOption.value.formatter
|
||||
})
|
||||
|
||||
const emit = defineEmits(['change'])
|
||||
|
||||
const controlChange = (...args) => {
|
||||
const option = props.option
|
||||
const option = calcOption.value
|
||||
if (option.change) {
|
||||
option.change(...args)
|
||||
}
|
||||
emit('change', ...args)
|
||||
}
|
||||
|
||||
const formItemEnabled = computed(() => props.option.enabled !== false)
|
||||
const formItemEnabled = computed(() => calcOption.value.enabled !== false)
|
||||
|
||||
const controlLabelWidth = computed(() => {
|
||||
const option = calcOption.value
|
||||
const labelWidth = props.labelWidth
|
||||
return option.labelWidth || modelAttrs.value.labelWidth || labelWidth
|
||||
})
|
||||
|
||||
const formatResult = computed(() => {
|
||||
if (hasModelText.value) {
|
||||
if (modelAttrs.value.modelText) {
|
||||
return {
|
||||
modelText: modelAttrs.value.modelText
|
||||
}
|
||||
}
|
||||
const option = calcOption.value
|
||||
if (option.formatter) {
|
||||
const result = option.formatter(modelValue.value, calcOption.value)
|
||||
return {
|
||||
modelText: result,
|
||||
vnode: isVNode(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
@@ -149,26 +223,30 @@ const formItemEnabled = computed(() => props.option.enabled !== false)
|
||||
v-if="formItemEnabled"
|
||||
ref="formItemRef"
|
||||
:rules="rules"
|
||||
:prop="option.prop"
|
||||
:label-width="labelWidth"
|
||||
:prop="calcOption.prop"
|
||||
:style="calcOption.style"
|
||||
:label-width="controlLabelWidth"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<template
|
||||
v-if="showLabel"
|
||||
#label
|
||||
>
|
||||
<span>{{ label }}</span>
|
||||
<slot name="beforeLabel" />
|
||||
<span :class="calcOption.labelCls">{{ label }}</span>
|
||||
<slot name="afterLabel" />
|
||||
<el-tooltip
|
||||
v-if="option.tooltip||option.tooltipFunc"
|
||||
v-if="calcOption.tooltip||calcOption.tooltipFunc"
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
:disabled="!option.tooltip"
|
||||
:content="option.tooltip"
|
||||
:disabled="!calcOption.tooltip"
|
||||
:content="calcOption.tooltip"
|
||||
placement="top-start"
|
||||
>
|
||||
<span>
|
||||
<el-link
|
||||
:underline="false"
|
||||
@click="option.tooltipFunc"
|
||||
@click="calcOption.tooltipFunc"
|
||||
>
|
||||
<common-icon
|
||||
icon="QuestionFilled"
|
||||
@@ -181,17 +259,27 @@ const formItemEnabled = computed(() => props.option.enabled !== false)
|
||||
:is="inputType"
|
||||
v-model="modelValue"
|
||||
v-bind="modelAttrs"
|
||||
:placeholder="option.placeholder"
|
||||
:disabled="option.disabled"
|
||||
:readonly="option.readonly"
|
||||
:placeholder="calcOption.placeholder"
|
||||
:disabled="calcOption.disabled"
|
||||
:readonly="calcOption.readonly"
|
||||
@change="controlChange"
|
||||
>
|
||||
<template
|
||||
v-if="hasModelText"
|
||||
v-if="hasModelText&&formatResult"
|
||||
#default
|
||||
>
|
||||
{{ modelAttrs.modelText || modelAttrs.modelTextFunc(modelValue) }}
|
||||
<span
|
||||
v-if="formatResult.modelText&&!formatResult.vnode"
|
||||
class="common-form-label-text"
|
||||
v-html="formatResult.modelText"
|
||||
/>
|
||||
<component
|
||||
:is="formatResult.modelText"
|
||||
v-if="formatResult.vnode"
|
||||
class="common-form-label-text"
|
||||
/>
|
||||
</template>
|
||||
<slot name="childBefore" />
|
||||
<template v-if="children&&children.length">
|
||||
<control-child
|
||||
v-for="(childItem, index) in children"
|
||||
@@ -199,7 +287,9 @@ const formItemEnabled = computed(() => props.option.enabled !== false)
|
||||
:option="childItem"
|
||||
/>
|
||||
</template>
|
||||
<slot name="childAfter" />
|
||||
</component>
|
||||
<slot name="after" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { inject, ref, onMounted, isRef, watchEffect } from 'vue'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
|
||||
/**
|
||||
@@ -23,6 +23,17 @@ const props = defineProps({
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
inline: {
|
||||
type: Boolean
|
||||
},
|
||||
className: {
|
||||
type: String,
|
||||
default: 'common-form'
|
||||
},
|
||||
buttonStyle: {
|
||||
type: [String, Object],
|
||||
default: ''
|
||||
},
|
||||
validateOnRuleChange: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
@@ -35,6 +46,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
disableSubmitIfNotValid: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
submitLabel: {
|
||||
type: String,
|
||||
default: ''
|
||||
@@ -66,22 +81,37 @@ const emit = defineEmits(['submitForm', 'update:model'])
|
||||
const formModel = useVModel(props, 'model', emit)
|
||||
|
||||
//= ============form暴露============//
|
||||
|
||||
const form = ref()
|
||||
|
||||
defineExpose({
|
||||
form
|
||||
})
|
||||
onMounted(() => {
|
||||
const windowFormRef = inject('commonWindowForm', null)
|
||||
if (isRef(windowFormRef)) {
|
||||
windowFormRef.value = form.value
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单校验不通过时禁止点击提交按钮
|
||||
*/
|
||||
const disableSubmit = ref(false)
|
||||
watchEffect(async () => {
|
||||
if (!props.disableSubmitIfNotValid) { return false }
|
||||
if (!form.value) { return true }
|
||||
await form.value.validate((ok) => { disableSubmit.value = !ok })
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-form
|
||||
ref="form"
|
||||
class="common-form"
|
||||
:inline="inline"
|
||||
:class="className"
|
||||
:model="formModel"
|
||||
:label-width="labelWidth"
|
||||
v-bind="$attrs"
|
||||
:validate-on-rule-change="validateOnRuleChange"
|
||||
>
|
||||
<template
|
||||
@@ -90,13 +120,13 @@ defineExpose({
|
||||
>
|
||||
<slot
|
||||
v-if="option.slot"
|
||||
name="option.slot"
|
||||
:name="option.slot"
|
||||
:option="option"
|
||||
:form="form"
|
||||
:model="formModel"
|
||||
/>
|
||||
<common-form-control
|
||||
v-if="option.enabled!==false"
|
||||
v-if="!option.slot&&option.enabled!==false"
|
||||
:model="formModel"
|
||||
:option="option"
|
||||
/>
|
||||
@@ -106,9 +136,13 @@ defineExpose({
|
||||
:model="formModel"
|
||||
name="default"
|
||||
/>
|
||||
<el-form-item v-if="showButtons">
|
||||
<el-form-item
|
||||
v-if="showButtons"
|
||||
:style="buttonStyle"
|
||||
>
|
||||
<el-button
|
||||
v-if="showSubmit"
|
||||
:disabled="disableSubmit"
|
||||
type="primary"
|
||||
@click="$emit('submitForm', form)"
|
||||
>
|
||||
|
||||
148
src/components/common-form/public.d.ts
vendored
148
src/components/common-form/public.d.ts
vendored
@@ -1,19 +1,114 @@
|
||||
import { RuleItem } from 'async-validator/dist-types/interface'
|
||||
import { FormInstance, FormProps } from 'element-plus'
|
||||
import {
|
||||
FormInstance, FormProps, DialogProps,
|
||||
InputProps, InputNumberProps, CascaderProps,
|
||||
RadioGroupProps, RadioProps, RadioButtonProps,
|
||||
CheckboxProps, CheckboxGroupProps, CheckboxButtonProps,
|
||||
DatePickerProps, timePickerDefaultProps, SwitchProps, SliderProps, TransferProps
|
||||
} from 'element-plus'
|
||||
import { SelectProps as SelectV1Props } from 'element-plus/es/components/select/src/select'
|
||||
import SelectV2Props from 'element-plus/es/components/select-v2/src/select-v2/defaults'
|
||||
import { CommonAutocompleteProps } from '../common-autocomplete/public'
|
||||
import { TreeComponentProps } from 'element-plus/es/components/tree/src/tree.type'
|
||||
import { ExtractPropTypes, CSSProperties, VNode } from 'vue'
|
||||
|
||||
export interface CommonFormOption {
|
||||
/** 表单类型 */
|
||||
type: 'input' | 'input-number' | 'cascader' | 'radio'
|
||||
| 'radio-group' | 'radio-button' | 'checkbox' | 'checkbox-group' | 'checkbox-button' | 'date-picker'
|
||||
| 'time-picker' | 'switch' | 'select' | 'option' | 'slider' | 'transfer' | 'upload' | 'common-icon-select' | 'common-autocomplete' | 'tree-select';
|
||||
export type TimePickerProps = ExtractPropTypes<typeof timePickerDefaultProps>
|
||||
export type SelectProps = ExtractPropTypes<typeof SelectV1Props>
|
||||
export interface OptionProps {
|
||||
label: string;
|
||||
value: any;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export interface IconSelectProps {
|
||||
dialogAttrs: DialogProps,
|
||||
colSize: number,
|
||||
dialogHeight: string,
|
||||
dialogWidth: string,
|
||||
disabled: boolean,
|
||||
readonly: boolean,
|
||||
placeholder: string,
|
||||
clearable: boolean,
|
||||
validateEvent: boolean
|
||||
}
|
||||
|
||||
export interface CommonFormLabelProps {
|
||||
/**
|
||||
* 显示文本
|
||||
*/
|
||||
modelText: string;
|
||||
}
|
||||
|
||||
export interface CommonTabFilterProps {
|
||||
tabs: Array<{
|
||||
icon: string,
|
||||
label: string,
|
||||
prop: string,
|
||||
children: OptionProps[]
|
||||
}>,
|
||||
type: 'checkbox-group' | 'radio-group',
|
||||
defaultIcon: string,
|
||||
iconSize: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 'input' | 'input-number' | 'cascader' | 'radio'
|
||||
* | 'radio-group' | 'radio-button' | 'checkbox' | 'checkbox-group' | 'checkbox-button' | 'date-picker'
|
||||
* | 'time-picker' | 'switch' | 'select' | 'select-v2' | 'option' | 'slider' | 'transfer' | 'upload'
|
||||
* | 'common-tab-filter' | 'common-icon-select' | 'common-autocomplete' | 'common-form-label'
|
||||
* | 'tree-select'
|
||||
*/
|
||||
export type PropsMap = {
|
||||
'input': InputProps,
|
||||
'input-number': InputNumberProps,
|
||||
'cascader': CascaderProps,
|
||||
'radio': RadioProps,
|
||||
'radio-group': RadioGroupProps,
|
||||
'radio-button': RadioButtonProps,
|
||||
'checkbox': CheckboxProps,
|
||||
'checkbox-group': CheckboxGroupProps,
|
||||
'checkbox-button': CheckboxButtonProps,
|
||||
'date-picker': DatePickerProps,
|
||||
'time-picker': TimePickerProps,
|
||||
'switch': SwitchProps,
|
||||
'select': SelectProps,
|
||||
'select-v2': SelectV2Props,
|
||||
'option': OptionProps,
|
||||
'slider': SliderProps,
|
||||
'transfer': TransferProps,
|
||||
'upload': TransferProps,
|
||||
'tree-select': SelectProps & TreeComponentProps,
|
||||
'common-tab-filter': CommonTabFilterProps,
|
||||
'common-form-label': CommonFormLabelProps,
|
||||
'common-icon-select': IconSelectProps,
|
||||
'common-autocomplete': CommonAutocompleteProps,
|
||||
[key:string]: InputProps
|
||||
}
|
||||
|
||||
type FormControlTypeOption = {
|
||||
[Type in keyof PropsMap]: {
|
||||
type?: Type,
|
||||
attrs?: PropsMap[Type]
|
||||
}
|
||||
}[keyof PropsMap]
|
||||
|
||||
export interface CommonFormOption extends FormControlTypeOption {
|
||||
/** 数据值 */
|
||||
value?: any;
|
||||
/** 属性名 */
|
||||
prop: string | string[];
|
||||
prop?: string | string[];
|
||||
/** 表单标签 */
|
||||
label?: string;
|
||||
/** 用于国际化的label */
|
||||
labelKey?: string;
|
||||
/**
|
||||
* 样式自定义
|
||||
*/
|
||||
labelCls?: string;
|
||||
/**
|
||||
* item样式
|
||||
*/
|
||||
style: CSSProperties;
|
||||
/** 是否必填,后面解析成为rules的一部分 */
|
||||
required?: boolean;
|
||||
/** 正则表达式验证,解析成为rules的一部分 */
|
||||
@@ -22,27 +117,42 @@ export interface CommonFormOption {
|
||||
patternMsg?: string;
|
||||
/** 是否禁用 */
|
||||
disabled?: boolean;
|
||||
/** 是否显示 */
|
||||
enabled?: boolean;
|
||||
/** 是否只读 */
|
||||
readonly?: boolean;
|
||||
/** 占位提示符 */
|
||||
placeholder?: string;
|
||||
/** 其他可用属性 */
|
||||
attrs?: {
|
||||
showPassword: boolean,
|
||||
[key: string]: any
|
||||
};
|
||||
/** 有些控件柚子节点 */
|
||||
children?: Array<CommonFormOption>;
|
||||
/** async-validator验证器 */
|
||||
rules: Array<RuleItem>;
|
||||
rules?: Array<RuleItem>;
|
||||
/** change事件 */
|
||||
change: (val: any) => void;
|
||||
change?: (val: any) => void;
|
||||
/** 提示信息 */
|
||||
tooltip: string;
|
||||
tooltip?: string;
|
||||
/** 提示函数 */
|
||||
tooltipFunc: () => void;
|
||||
tooltipFunc?: () => void;
|
||||
/**
|
||||
* common-form-label格式化
|
||||
* @param modelValue 数据
|
||||
* @param option 选项
|
||||
*/
|
||||
formatter?: (modelValue:any, option: CommonFormOption) => string|VNode;
|
||||
/** 自定义slot名称 */
|
||||
slot: string;
|
||||
slot?: string;
|
||||
/**
|
||||
* 根据model数据动态计算Option值
|
||||
* @param model 表单model
|
||||
* @param option 原始选项
|
||||
*/
|
||||
dynamicOption?: (model: any, option: CommonFormOption, addInfo?: any) => CommonFormOption;
|
||||
/**
|
||||
* 根据model数据动态计算attrs的值
|
||||
* @param model 表单model
|
||||
* @param option 原始选项
|
||||
*/
|
||||
dynamicAttrs?: (model: any, option: CommonFormOption, addInfo?: any) => any;
|
||||
}
|
||||
|
||||
export interface CommonFormProps extends FormProps {
|
||||
@@ -64,10 +174,14 @@ export interface CommonFormProps extends FormProps {
|
||||
showButtons: boolean;
|
||||
/** 是否显示提交按钮 */
|
||||
showSubmit: boolean;
|
||||
/** 当校验不通过时提交按钮不可点击,默认为 false: 校验不通过也可直接提交 */
|
||||
disableSubmitIfNotValid: boolean;
|
||||
/** 是否显示重置按钮 */
|
||||
showReset: boolean;
|
||||
/** 提交逻辑 */
|
||||
submitForm: (form: FormInstance) => void;
|
||||
/** 返回地址 */
|
||||
backUrl: string;
|
||||
/** 行级排列 */
|
||||
inline: boolean;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { filterIconsByKeywords } from '@/services/icon/IconService'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import { UPDATE_MODEL_EVENT, CHANGE_EVENT, useFormItem } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
dialogAttrs: {
|
||||
type: Object,
|
||||
default () {
|
||||
@@ -60,7 +55,10 @@ const filterIcons = computed(() => {
|
||||
|
||||
const emit = defineEmits([UPDATE_MODEL_EVENT, CHANGE_EVENT])
|
||||
|
||||
const vModel = useVModel(props, 'modelValue', emit)
|
||||
const vModel = defineModel({
|
||||
type: String,
|
||||
default: ''
|
||||
})
|
||||
|
||||
const { formItem } = useFormItem()
|
||||
|
||||
@@ -91,76 +89,76 @@ const selectIcon = icon => {
|
||||
type="primary"
|
||||
:disabled="disabled||readonly"
|
||||
size="small"
|
||||
@click="iconSelectVisible = true"
|
||||
@click.prevent="iconSelectVisible = true"
|
||||
>
|
||||
{{ $t('common.label.select') }}
|
||||
</el-button>
|
||||
</label>
|
||||
<el-button
|
||||
v-if="clearable&&vModel"
|
||||
type="danger"
|
||||
:disabled="disabled||readonly"
|
||||
size="small"
|
||||
@click="selectIcon()"
|
||||
>
|
||||
{{ $t('common.label.clear') }}
|
||||
</el-button>
|
||||
<el-dialog
|
||||
v-model="iconSelectVisible"
|
||||
:width="dialogWidth"
|
||||
v-bind="dialogAttrs"
|
||||
draggable
|
||||
class="icon-dialog"
|
||||
:title="$t('common.msg.pleaseSelectIcon')"
|
||||
>
|
||||
<el-container
|
||||
style="overflow: auto;"
|
||||
:style="{ height: dialogHeight }"
|
||||
class="icon-container"
|
||||
<el-button
|
||||
v-if="clearable&&vModel"
|
||||
type="danger"
|
||||
:disabled="disabled||readonly"
|
||||
size="small"
|
||||
@click.prevent="selectIcon()"
|
||||
>
|
||||
<el-header height="40px">
|
||||
<el-form label-width="120px">
|
||||
<el-form-item :label="$t('common.label.keywords')">
|
||||
<el-input
|
||||
v-model="keyWords"
|
||||
:placeholder="$t('common.msg.inputKeywords')"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-header>
|
||||
<el-main class="icon-area">
|
||||
<recycle-scroller
|
||||
v-slot="{ item }"
|
||||
class="scroller icon-list"
|
||||
:items="filterIcons"
|
||||
:item-size="80"
|
||||
key-field="id"
|
||||
>
|
||||
<el-row>
|
||||
<el-col
|
||||
v-for="icon in item.icons"
|
||||
:key="icon"
|
||||
:span="24/colSize"
|
||||
class="text-center"
|
||||
>
|
||||
<a
|
||||
class="el-button el-button--large is-text icon-a"
|
||||
@click="selectIcon(icon)"
|
||||
{{ $t('common.label.clear') }}
|
||||
</el-button>
|
||||
<el-dialog
|
||||
v-model="iconSelectVisible"
|
||||
:width="dialogWidth"
|
||||
v-bind="dialogAttrs"
|
||||
draggable
|
||||
class="icon-dialog"
|
||||
:title="$t('common.msg.pleaseSelectIcon')"
|
||||
>
|
||||
<el-container
|
||||
style="overflow: auto;"
|
||||
:style="{ height: dialogHeight }"
|
||||
class="icon-container"
|
||||
>
|
||||
<el-header height="40px">
|
||||
<el-form label-width="120px">
|
||||
<el-form-item :label="$t('common.label.keywords')">
|
||||
<el-input
|
||||
v-model="keyWords"
|
||||
:placeholder="$t('common.msg.inputKeywords')"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-header>
|
||||
<el-main class="icon-area">
|
||||
<recycle-scroller
|
||||
v-slot="{ item }"
|
||||
class="scroller icon-list"
|
||||
:items="filterIcons"
|
||||
:item-size="80"
|
||||
key-field="id"
|
||||
>
|
||||
<el-row>
|
||||
<el-col
|
||||
v-for="icon in item.icons"
|
||||
:key="icon"
|
||||
:span="24/colSize"
|
||||
class="text-center"
|
||||
>
|
||||
<div>
|
||||
<common-icon
|
||||
size="20"
|
||||
:icon="icon"
|
||||
/><br>
|
||||
<span class="icon-text">{{ icon }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</recycle-scroller>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-dialog>
|
||||
<a
|
||||
class="el-button el-button--large is-text icon-a"
|
||||
@click.prevent="selectIcon(icon)"
|
||||
>
|
||||
<div>
|
||||
<common-icon
|
||||
size="20"
|
||||
:icon="icon"
|
||||
/><br>
|
||||
<span class="icon-text">{{ icon }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</recycle-scroller>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-dialog>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -6,7 +6,7 @@ import kebabCase from 'lodash/kebabCase'
|
||||
const props = defineProps({
|
||||
icon: {
|
||||
type: String,
|
||||
required: false
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
const calcIcon = computed(() => {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useGlobalConfigStore } from '@/stores/GlobalConfigStore'
|
||||
|
||||
const router = useRouter()
|
||||
const globalConfigStore = useGlobalConfigStore()
|
||||
/**
|
||||
* @type {CommonMenuItemProps}
|
||||
*/
|
||||
@@ -15,7 +18,7 @@ const props = defineProps({
|
||||
},
|
||||
index: {
|
||||
type: [Number, String],
|
||||
required: false
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
const isSubMenu = computed(() => {
|
||||
@@ -35,7 +38,7 @@ const menuCls = computed(() => {
|
||||
})
|
||||
const dropdownClick = (menuItem, $event) => {
|
||||
if (menuItem.click) {
|
||||
menuItem.click($event)
|
||||
menuItem.click($event, menuItem)
|
||||
} else {
|
||||
const route = menuItem.route || menuItem.index
|
||||
if (route) {
|
||||
@@ -44,35 +47,39 @@ const dropdownClick = (menuItem, $event) => {
|
||||
}
|
||||
}
|
||||
|
||||
const checkShowMenuIcon = menuItem => {
|
||||
return menuItem.icon && (globalConfigStore.showMenuIcon || (!menuItem.labelKey && !menuItem.label))
|
||||
}
|
||||
|
||||
const showMenuIcon = computed(() => {
|
||||
return checkShowMenuIcon(props.menuItem)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="menuItem.isSplit"
|
||||
:key="menuItem.index||index"
|
||||
:class="menuCls"
|
||||
>
|
||||
<slot name="split" />
|
||||
</div>
|
||||
<el-sub-menu
|
||||
v-else-if="isSubMenu"
|
||||
:key="menuItem.index||index"
|
||||
:index="`${menuItem.index||index}`"
|
||||
:class="menuCls"
|
||||
:disabled="menuItem.disabled"
|
||||
v-bind="menuItem.attrs"
|
||||
>
|
||||
<template #title>
|
||||
<common-icon
|
||||
v-if="showMenuIcon"
|
||||
:size="menuItem.iconSize"
|
||||
:icon="menuItem.icon"
|
||||
/>
|
||||
<span v-if="menuItem.labelKey||menuItem.label">
|
||||
{{ menuItem.labelKey?$t(menuItem.labelKey):menuItem.label }}
|
||||
</span>
|
||||
<div
|
||||
v-if="menuItem.html"
|
||||
v-html="menuItem.html"
|
||||
/>
|
||||
</template>
|
||||
<common-menu-item
|
||||
v-for="(childMenu, childIdx) in menuItem.children"
|
||||
@@ -83,13 +90,14 @@ const dropdownClick = (menuItem, $event) => {
|
||||
</el-sub-menu>
|
||||
<el-menu-item
|
||||
v-else-if="isDropdown"
|
||||
:key="menuItem.index||index"
|
||||
:class="menuCls"
|
||||
@click="menuItem.click&&menuItem.click($event)"
|
||||
:disabled="menuItem.disabled"
|
||||
@click="menuItem.click&&menuItem.click($event, menuItem)"
|
||||
>
|
||||
<el-dropdown class="common-dropdown">
|
||||
<span class="el-dropdown-link">
|
||||
<common-icon
|
||||
v-if="showMenuIcon"
|
||||
:size="menuItem.iconSize"
|
||||
:icon="menuItem.icon"
|
||||
/>
|
||||
@@ -104,6 +112,7 @@ const dropdownClick = (menuItem, $event) => {
|
||||
@click="dropdownClick(childMenu, $event)"
|
||||
>
|
||||
<common-icon
|
||||
v-if="checkShowMenuIcon(childMenu)"
|
||||
:size="childMenu.iconSize"
|
||||
:icon="childMenu.icon"
|
||||
/>
|
||||
@@ -117,12 +126,14 @@ const dropdownClick = (menuItem, $event) => {
|
||||
<el-menu-item
|
||||
v-else
|
||||
:class="menuCls"
|
||||
:disabled="menuItem.disabled"
|
||||
:route="menuItem.route"
|
||||
v-bind="menuItem.attrs"
|
||||
:index="menuItem.index"
|
||||
@click="menuItem.click&&menuItem.click(menuItem, $event)"
|
||||
@click="menuItem.click&&menuItem.click($event,menuItem)"
|
||||
>
|
||||
<common-icon
|
||||
v-if="showMenuIcon"
|
||||
:size="menuItem.iconSize"
|
||||
:icon="menuItem.icon"
|
||||
/>
|
||||
@@ -130,10 +141,6 @@ const dropdownClick = (menuItem, $event) => {
|
||||
<span v-if="menuItem.labelKey||menuItem.label">
|
||||
{{ menuItem.labelKey?$t(menuItem.labelKey):menuItem.label }}
|
||||
</span>
|
||||
<div
|
||||
v-if="menuItem.html"
|
||||
v-html="menuItem.html"
|
||||
/>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
@@ -142,7 +149,4 @@ const dropdownClick = (menuItem, $event) => {
|
||||
.common-dropdown {
|
||||
height: 100%;
|
||||
}
|
||||
.common-dropdown .el-icon {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { filterMenus, useParentRoute } from '@/components/utils'
|
||||
import { computed, ref, watchEffect } from 'vue'
|
||||
import { processMenus, useParentRoute } from '@/components/utils'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const props = defineProps({
|
||||
menus: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
defaultActivePath: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
const menuItems = computed(() => {
|
||||
return filterMenus(props.menus)
|
||||
const route = useRoute()
|
||||
const menuItems = ref([])
|
||||
watchEffect(() => {
|
||||
menuItems.value = processMenus(props.menus)
|
||||
})
|
||||
const activeRoutePath = computed(() => {
|
||||
let route = useRoute()
|
||||
route = useParentRoute(route)
|
||||
return route && route.path !== '/' ? route.path : ''
|
||||
if (props.defaultActivePath) {
|
||||
return props.defaultActivePath
|
||||
}
|
||||
const current = useParentRoute(route)
|
||||
return current && current.path !== '/' ? current.path : ''
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
8
src/components/common-menu/public.d.ts
vendored
8
src/components/common-menu/public.d.ts
vendored
@@ -8,6 +8,14 @@ export interface CommonMenuItem {
|
||||
isDropdown?: boolean;
|
||||
/** 是否是分割元素 */
|
||||
isSplit?: boolean;
|
||||
/**
|
||||
* 是否禁用,禁用状态仍然是显示的
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* 是否启用,默认true,设置false不显示
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/** 自定义样式 */
|
||||
menuCls?: string;
|
||||
/** 路由地址 */
|
||||
|
||||
101
src/components/common-sort/index.vue
Normal file
101
src/components/common-sort/index.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<script setup>
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import { onMounted } from 'vue'
|
||||
import { toLabelByKey } from '@/components/utils'
|
||||
|
||||
/**
|
||||
* 同时只能有一个字段被排序
|
||||
* 排序的数据会被放到vModel中
|
||||
* 如 { "deptTime": "DESC", "price": "", "duration": "" }
|
||||
* @type {{modelValue:SortProps}} 可以传入默认排序值,但是最多只能有一个字段有值 如:{ "deptTime": "DESC", "price": "", "duration": "" }
|
||||
* @type {{options:SortOption[]}} 所有要排序的字段和要显示的值
|
||||
*/
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => { }
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default () {
|
||||
return []
|
||||
}
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const vModel = useVModel(props, 'modelValue', emit)
|
||||
|
||||
onMounted(() => {
|
||||
// 根据传入的option和初始vmodel设置vModel
|
||||
const model = {}
|
||||
for (const option of props.options) {
|
||||
const prop = option.prop
|
||||
if (vModel.value[prop] !== undefined) {
|
||||
model[prop] = vModel.value[prop]
|
||||
} else {
|
||||
model[option.prop] = ''
|
||||
}
|
||||
}
|
||||
vModel.value = model
|
||||
})
|
||||
|
||||
/**
|
||||
* 计算要显示的label
|
||||
* @param {SortOption} option
|
||||
*/
|
||||
const calLabel = (option) => {
|
||||
if (option.labelKey) {
|
||||
return toLabelByKey(option.labelKey)
|
||||
}
|
||||
if (option.label) {
|
||||
return option.label
|
||||
}
|
||||
return 'undefined'
|
||||
}
|
||||
const handleClick = (key, fixedValue) => {
|
||||
const newValue = { ...vModel.value }
|
||||
for (const k in newValue) {
|
||||
if (k === key) { continue }
|
||||
newValue[k] = ''
|
||||
}
|
||||
if (fixedValue) {
|
||||
newValue[key] = fixedValue
|
||||
} else {
|
||||
newValue[key] = newValue[key] === 'ASC' ? 'DESC' : 'ASC'
|
||||
}
|
||||
vModel.value = newValue
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-button-group
|
||||
class="ml-4"
|
||||
>
|
||||
<template
|
||||
v-for="option in props.options"
|
||||
:key="option.prop"
|
||||
>
|
||||
<el-button
|
||||
:type="vModel[option.prop]===''?'':'primary'"
|
||||
@click="handleClick(option.prop, option.fixedValue)"
|
||||
>
|
||||
{{ calLabel(option) }}
|
||||
<template v-if="option.showIcon!==false">
|
||||
<common-icon
|
||||
v-if="vModel[option.prop]==='ASC'"
|
||||
icon="SortUp"
|
||||
/>
|
||||
<common-icon
|
||||
v-if="vModel[option.prop]==='DESC'"
|
||||
icon="SortDown"
|
||||
/>
|
||||
</template>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-button-group>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
17
src/components/common-sort/public.d.ts
vendored
Normal file
17
src/components/common-sort/public.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
export interface SortOption {
|
||||
prop: string; // 排序字段,如price,该值会作为
|
||||
labelKey?: string;// 国际化资源key,首选该属性,不存在才使用label
|
||||
label?: string;
|
||||
showIcon?: boolean; // 控制某些排序不显示图标
|
||||
fixedValue?: 'ASC' | 'DESC' // 固定排序模式
|
||||
}
|
||||
|
||||
/**
|
||||
* 排序结果
|
||||
* key是排序字段,来自SortOption的prop,value是排序方式
|
||||
* 'ASC':升序 'DESC':降序 '':不生效
|
||||
* 如 { "deptTime": "DESC", "price": "", "duration": "" }
|
||||
*/
|
||||
export interface SortProps {
|
||||
[key: string]: 'ASC' | 'DESC' | ''
|
||||
}
|
||||
136
src/components/common-table-form/index.vue
Normal file
136
src/components/common-table-form/index.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import TableFormControl from '@/components/common-table-form/table-form-control.vue'
|
||||
import { toLabelByKey } from '@/components/utils'
|
||||
|
||||
const props = defineProps({
|
||||
formOptions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
model: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
dataListKey: {
|
||||
type: String,
|
||||
default: 'items'
|
||||
},
|
||||
showOperation: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
const dataList = computed(() => {
|
||||
return props.model[props.dataListKey] || []
|
||||
})
|
||||
|
||||
const emit = defineEmits(['delete', 'change'])
|
||||
|
||||
const deleteItem = (item, index) => {
|
||||
emit('delete', {
|
||||
item, index
|
||||
})
|
||||
}
|
||||
|
||||
const formChange = ($event, row, $index, option) => {
|
||||
const args = [$event, {
|
||||
model: row,
|
||||
index: $index,
|
||||
option
|
||||
}]
|
||||
if (option.formChange) { // 动态表单change事件
|
||||
option.formChange(...args)
|
||||
}
|
||||
emit('change', ...args)
|
||||
}
|
||||
|
||||
const options = computed(() => {
|
||||
return props.formOptions.filter(option => option.enabled !== false)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-table
|
||||
:data="dataList"
|
||||
border
|
||||
class="common-table-form"
|
||||
>
|
||||
<el-table-column
|
||||
v-for="(option, index) in options"
|
||||
:key="index"
|
||||
:label="option.label||toLabelByKey(option.labelKey)"
|
||||
:width="option.width"
|
||||
>
|
||||
<template
|
||||
v-if="option.headerSlot"
|
||||
#header="scope"
|
||||
>
|
||||
<slot
|
||||
v-bind="scope"
|
||||
:name="option.headerSlot"
|
||||
/>
|
||||
</template>
|
||||
<!--用于自定义显示属性-->
|
||||
<template
|
||||
v-if="option.slot"
|
||||
#default="scope"
|
||||
>
|
||||
<slot
|
||||
v-bind="scope"
|
||||
:item="scope.row"
|
||||
:name="option.slot"
|
||||
/>
|
||||
</template>
|
||||
<template
|
||||
v-else
|
||||
#default="{row, $index}"
|
||||
>
|
||||
<table-form-control
|
||||
:model="row"
|
||||
label-width="0"
|
||||
:option="option"
|
||||
:prop="`${dataListKey}.${$index}.${option.prop}`"
|
||||
@change="formChange($event, row, $index, option)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="showOperation"
|
||||
:label="$t('common.label.operation')"
|
||||
>
|
||||
<template #default="{row, $index}">
|
||||
<div class="el-form-item">
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
:underline="false"
|
||||
@click="deleteItem(row, $index)"
|
||||
>
|
||||
<common-icon
|
||||
icon="Delete"
|
||||
/>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty="scope">
|
||||
<slot
|
||||
name="empty"
|
||||
v-bind="scope"
|
||||
/>
|
||||
</template>
|
||||
<template #append="scope">
|
||||
<slot
|
||||
name="append"
|
||||
v-bind="scope"
|
||||
/>
|
||||
</template>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
38
src/components/common-table-form/table-form-control.vue
Normal file
38
src/components/common-table-form/table-form-control.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
/**
|
||||
* @type {CommonFormOption}
|
||||
*/
|
||||
option: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
model: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
labelWidth: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
prop: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<common-form-control
|
||||
:model="model"
|
||||
:label-width="labelWidth"
|
||||
:option="option"
|
||||
:prop="prop"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { formatDate } from '@/components/utils'
|
||||
import { computed } from 'vue'
|
||||
import { formatDate, toLabelByKey } from '@/components/utils'
|
||||
import { computed, useSlots } from 'vue'
|
||||
import { get } from 'lodash'
|
||||
|
||||
/**
|
||||
@@ -36,22 +36,24 @@ const formatter = computed(() => {
|
||||
})
|
||||
|
||||
const getPropertyData = (row) => {
|
||||
return get(row, props.column.property)
|
||||
return get(row, props.column.prop || props.column.property)
|
||||
}
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-table-column
|
||||
v-if="!column.isOperation"
|
||||
:label="column.label || $t(column.labelKey)"
|
||||
:label="column.label || toLabelByKey(column.labelKey)"
|
||||
:prop="column.prop||column.property"
|
||||
:width="column.width"
|
||||
v-bind="column.attrs"
|
||||
:formatter="formatter"
|
||||
>
|
||||
<template
|
||||
v-if="column.slot||column.click"
|
||||
v-if="column.click"
|
||||
#default="scope"
|
||||
>
|
||||
<el-link
|
||||
@@ -62,16 +64,22 @@ const getPropertyData = (row) => {
|
||||
>
|
||||
{{ formatter?formatter(scope.row, scope):getPropertyData(scope.row) }}
|
||||
</el-link>
|
||||
</template>
|
||||
<template
|
||||
v-for="(slot, slotKey) in slots"
|
||||
:key="slotKey"
|
||||
#[slotKey]="scope"
|
||||
>
|
||||
<slot
|
||||
:name="slotKey"
|
||||
v-bind="scope"
|
||||
:column-conf="column"
|
||||
name="default"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="column.isOperation"
|
||||
:label="column.label || $t(column.labelKey)"
|
||||
:label="column.label || toLabelByKey(column.labelKey)"
|
||||
:width="column.width"
|
||||
v-bind="column.attrs"
|
||||
>
|
||||
@@ -80,7 +88,7 @@ const getPropertyData = (row) => {
|
||||
>
|
||||
<template v-for="(button, index) in column.buttons">
|
||||
<el-button
|
||||
v-if="!button.buttonIf||button.buttonIf(scope.row, scope)"
|
||||
v-if="(!button.buttonIf||button.buttonIf(scope.row, scope))&&button.enabled!==false"
|
||||
:key="index"
|
||||
:type="button.type"
|
||||
:icon="button.icon"
|
||||
@@ -90,7 +98,7 @@ const getPropertyData = (row) => {
|
||||
:circle="button.circle"
|
||||
@click="button.click&&button.click(scope.row, scope)"
|
||||
>
|
||||
{{ button.label || $t(button.labelKey) }}
|
||||
{{ button.label || toLabelByKey(button.labelKey) }}
|
||||
</el-button>
|
||||
</template>
|
||||
<slot
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<script setup>
|
||||
import CommonTableColumn from '@/components/common-table/common-table-column.vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, ref, onMounted, watch } from 'vue'
|
||||
import { useIntersectionObserver } from '@vueuse/core'
|
||||
import { getFrontendPage } from '@/components/utils'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
/**
|
||||
* @type CommonTableProps
|
||||
@@ -11,7 +17,7 @@ const props = defineProps({
|
||||
*/
|
||||
columns: {
|
||||
type: Array,
|
||||
required: true
|
||||
default: () => []
|
||||
},
|
||||
/**
|
||||
* 显示数据
|
||||
@@ -36,7 +42,7 @@ const props = defineProps({
|
||||
},
|
||||
/**
|
||||
* el-button
|
||||
* @type [ButtonProps]
|
||||
* @type [TableButtonProps]
|
||||
*/
|
||||
buttons: {
|
||||
type: Array,
|
||||
@@ -84,6 +90,26 @@ const props = defineProps({
|
||||
loadingText: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
expandTable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
hideExpandBtn: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
frontendPaging: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
frontendPageSize: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
infinitePaging: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
/**
|
||||
@@ -92,6 +118,15 @@ const props = defineProps({
|
||||
*/
|
||||
const calcColumns = computed(() => {
|
||||
let _columns = props.columns
|
||||
if (props.expandTable) {
|
||||
_columns = [{
|
||||
slot: 'expand',
|
||||
attrs: {
|
||||
type: 'expand',
|
||||
width: props.hideExpandBtn ? 1 : 0
|
||||
}
|
||||
}, ..._columns]
|
||||
}
|
||||
if (props.buttons.length || props.buttonsSlot) {
|
||||
const buttonColumn = {
|
||||
labelKey: 'common.label.operation',
|
||||
@@ -116,6 +151,74 @@ const currentPageChange = (pageNumber) => {
|
||||
emit('currentPageChange', pageNumber)
|
||||
}
|
||||
|
||||
const calcData = ref([])
|
||||
const frontendPage = ref(getFrontendPage(0, props.frontendPageSize))
|
||||
const infiniteRef = ref(null)
|
||||
const isInfiniteEnd = ref(false)
|
||||
|
||||
function checkInfiniteEnd (pageVal) {
|
||||
if (props.infinitePaging) {
|
||||
isInfiniteEnd.value = pageVal ? pageVal.pageNumber >= pageVal.pageCount : true
|
||||
}
|
||||
}
|
||||
|
||||
function calcFrontEndPageData () {
|
||||
if (props.frontendPaging) {
|
||||
calcData.value = props.data?.slice((frontendPage.value.pageNumber - 1) * frontendPage.value.pageSize,
|
||||
frontendPage.value.pageNumber * frontendPage.value.pageSize) // 展示数据
|
||||
checkInfiniteEnd(frontendPage.value)
|
||||
}
|
||||
}
|
||||
|
||||
function calcTableDataAndPage () {
|
||||
if (props.frontendPaging) { // 前端分页模式
|
||||
frontendPage.value = getFrontendPage(props.data?.length, frontendPage.value.pageSize, frontendPage.value.pageNumber) // 前端分页信息
|
||||
calcFrontEndPageData()
|
||||
} else { // 后端分页模式
|
||||
if (props.infinitePaging) { // 无限加载模式
|
||||
if (!calcData.value.length && props.page && props.page.pageNumber > 1) { // 如果进来就是后面的页码,重新查询
|
||||
emit('update:page', { ...props.page, pageNumber: 1 }) // 仅更新pageNumber
|
||||
} else {
|
||||
calcData.value = [...calcData.value, ...props.data]
|
||||
}
|
||||
checkInfiniteEnd(props.page)
|
||||
} else {
|
||||
calcData.value = props.data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.data, () => {
|
||||
calcTableDataAndPage()
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
watch(() => frontendPage, () => {
|
||||
calcFrontEndPageData()
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
if (props.infinitePaging) {
|
||||
console.info('================================mounted', infiniteRef.value)
|
||||
useIntersectionObserver(infiniteRef, onInfiniteLoad, {
|
||||
threshold: 1
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const onInfiniteLoad = (args) => {
|
||||
const isIntersecting = args[0].isIntersecting
|
||||
console.info('===========================infinite', isIntersecting, ...args)
|
||||
if (isIntersecting && calcData.value?.length) {
|
||||
if (props.frontendPaging) {
|
||||
frontendPage.value = getFrontendPage(props.data?.length,
|
||||
frontendPage.value.pageSize + props.frontendPageSize,
|
||||
frontendPage.value.pageNumber)
|
||||
} else if (props.page) {
|
||||
currentPageChange(props.page.pageNumber + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const table = ref()
|
||||
|
||||
defineExpose({
|
||||
@@ -135,7 +238,8 @@ defineExpose({
|
||||
v-bind="$attrs"
|
||||
:highlight-current-row="highlightCurrentRow"
|
||||
:stripe="stripe"
|
||||
:data="data"
|
||||
:data="calcData"
|
||||
:class="{'common-hide-expand': hideExpandBtn}"
|
||||
:border="border"
|
||||
>
|
||||
<common-table-column
|
||||
@@ -144,23 +248,50 @@ defineExpose({
|
||||
:column="column"
|
||||
:button-size="buttonSize"
|
||||
>
|
||||
<template
|
||||
v-if="column.headerSlot"
|
||||
#header="scope"
|
||||
>
|
||||
<slot
|
||||
v-bind="scope"
|
||||
:name="column.headerSlot"
|
||||
/>
|
||||
</template>
|
||||
<!--用于自定义显示属性-->
|
||||
<template
|
||||
v-if="column.slot"
|
||||
#default="scope"
|
||||
>
|
||||
<slot
|
||||
v-if="column.slot"
|
||||
:row="scope.row"
|
||||
:column="scope.column"
|
||||
v-bind="scope"
|
||||
:item="scope.row"
|
||||
:column-conf="scope.columnConf"
|
||||
:name="column.slot"
|
||||
/>
|
||||
</template>
|
||||
</common-table-column>
|
||||
<template #append="scope">
|
||||
<slot
|
||||
name="append"
|
||||
v-bind="scope"
|
||||
/>
|
||||
<el-container
|
||||
v-show="infinitePaging&&!isInfiniteEnd"
|
||||
ref="infiniteRef"
|
||||
v-loading="true"
|
||||
class="container-center"
|
||||
>
|
||||
Loading
|
||||
</el-container>
|
||||
</template>
|
||||
<template #empty="scope">
|
||||
<slot
|
||||
name="empty"
|
||||
v-bind="scope"
|
||||
/>
|
||||
</template>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-if="page&&page.pageCount"
|
||||
v-if="!infinitePaging&&!frontendPaging&&page&&page.pageCount"
|
||||
class="common-pagination"
|
||||
v-bind="pageAttrs"
|
||||
:total="page.totalCount"
|
||||
@@ -169,6 +300,16 @@ defineExpose({
|
||||
@size-change="pageSizeChange($event)"
|
||||
@current-change="currentPageChange($event)"
|
||||
/>
|
||||
<el-pagination
|
||||
v-if="!infinitePaging&&frontendPaging&&frontendPage&&frontendPage.pageCount"
|
||||
class="common-pagination"
|
||||
v-bind="pageAttrs"
|
||||
:total="frontendPage.totalCount"
|
||||
:page-size="frontendPage.pageSize"
|
||||
:current-page="frontendPage.pageNumber"
|
||||
@size-change="frontendPage.pageSize=$event"
|
||||
@current-change="frontendPage.pageNumber=$event"
|
||||
/>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
|
||||
82
src/components/common-table/public.d.ts
vendored
82
src/components/common-table/public.d.ts
vendored
@@ -1,34 +1,52 @@
|
||||
import { ButtonProps, LinkProps, TableProps, PaginationProps } from 'element-plus'
|
||||
import tableColumnProps from 'element-plus/es/components/table/src/table-column/defaults'
|
||||
import { CommonPage } from '../public'
|
||||
import { ExtractPropTypes } from 'vue'
|
||||
|
||||
export type TableColumnProps = ExtractPropTypes<typeof tableColumnProps>
|
||||
|
||||
export type TableButtonProps = {
|
||||
/**
|
||||
* 计算是否显示按钮
|
||||
* @param data 表格数据
|
||||
*/
|
||||
buttonIf: (data: any) => boolean
|
||||
} & ButtonProps
|
||||
|
||||
/**
|
||||
* 表格列定义
|
||||
*/
|
||||
export interface CommonTableColumn {
|
||||
// 表格头
|
||||
label: string;
|
||||
// 表格头国际化key
|
||||
labelKey: string;
|
||||
// 属性名
|
||||
property: string;
|
||||
// 属性名,同property
|
||||
prop: string;
|
||||
// 宽度
|
||||
width: string;
|
||||
// 是否是可操作列
|
||||
isOperation: boolean;
|
||||
// 自定义插槽名称,用于自定义显示数据
|
||||
slot: string;
|
||||
// 自定义按钮
|
||||
buttons: Array<ButtonProps>
|
||||
// 可选属性
|
||||
attrs: any;
|
||||
// 链接可选属性
|
||||
linkAttrs: LinkProps;
|
||||
// 点击事件
|
||||
click: (data: any) => any;
|
||||
// 格式化函数
|
||||
formatter: (data: any, scope: any) => string;
|
||||
/** 是否显示 */
|
||||
enabled?: boolean;
|
||||
/** 表格头 */
|
||||
label?: string;
|
||||
/** 表格头国际化key */
|
||||
labelKey?: string;
|
||||
/** 属性名 */
|
||||
property?: string;
|
||||
/** 属性名,同property */
|
||||
prop?: string;
|
||||
/** 宽度 */
|
||||
width?: string;
|
||||
/** 是否是可操作列 */
|
||||
isOperation?: boolean;
|
||||
/** 自定义插槽名称,用于自定义显示数据 */
|
||||
slot?: string;
|
||||
/** 自定义插槽名称,用于自定义显示Label */
|
||||
headerSlot?: string;
|
||||
/** 自定义按钮 */
|
||||
buttons?: Array<TableButtonProps>
|
||||
/** 可选属性 */
|
||||
attrs?: TableColumnProps;
|
||||
/** 链接可选属性 */
|
||||
linkAttrs?: LinkProps;
|
||||
/** 点击事件 */
|
||||
click?: (data: any) => any;
|
||||
/** 格式化函数 */
|
||||
formatter?: (data: any, scope: any) => string;
|
||||
/** 日期格式化 */
|
||||
dateFormat?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,15 +64,13 @@ export interface CommonTableProps extends TableProps<any> {
|
||||
/** 边框配置 */
|
||||
border: boolean;
|
||||
/** 自定义按钮配置 */
|
||||
buttons?: Array<ButtonProps>;
|
||||
buttons?: Array<TableButtonProps>;
|
||||
/** buttons插槽 */
|
||||
buttonsSlot?: string;
|
||||
/** 默认的按钮大小 */
|
||||
buttonSize?: string;
|
||||
/** 按钮列配置 */
|
||||
buttonsColumnAttrs?: {
|
||||
[key: string]: any
|
||||
};
|
||||
buttonsColumnAttrs?: TableColumnProps;
|
||||
/** 分页配置 */
|
||||
page?: CommonPage;
|
||||
/** 分页对齐 */
|
||||
@@ -65,6 +81,16 @@ export interface CommonTableProps extends TableProps<any> {
|
||||
loading?: boolean;
|
||||
/** loading显示消息 */
|
||||
loadingText?: string;
|
||||
/** 可以展开的table */
|
||||
expandTable: boolean;
|
||||
/** 是否隐藏展开按钮 */
|
||||
hideExpandBtn: boolean;
|
||||
/** 是否是前台分页**/
|
||||
frontendPaging: boolean;
|
||||
/** 前端模式分页数量 **/
|
||||
frontendPageSize: number;
|
||||
/** 是否是无限加载分页**/
|
||||
infinitePaging: boolean;
|
||||
}
|
||||
|
||||
export interface CommonTableColumnProps {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref, provide } from 'vue'
|
||||
import { UPDATE_MODEL_EVENT } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -8,10 +8,6 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
draggable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
@@ -24,6 +20,10 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: '800px'
|
||||
},
|
||||
defaultCls: {
|
||||
type: [String, Object],
|
||||
default: ''
|
||||
},
|
||||
buttons: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
@@ -63,19 +63,44 @@ const props = defineProps({
|
||||
closeClick: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
destroyOnClose: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
draggable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
overflow: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
closeOnClickModal: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
closeOnPressEscape: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
appendToBody: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits([UPDATE_MODEL_EVENT])
|
||||
const showDialog = useVModel(props, 'modelValue', emit) // 自动响应v-model
|
||||
const windowForm = ref(null) // 如果common-window下面有common-form,注册到这里
|
||||
provide('commonWindowForm', windowForm)
|
||||
|
||||
const okButtonClick = $event => {
|
||||
if (!props.okClick || props.okClick($event) !== false) {
|
||||
if (!props.okClick || props.okClick({ $event, form: windowForm.value }) !== false) {
|
||||
showDialog.value = false
|
||||
}
|
||||
}
|
||||
const cancelButtonClick = $event => {
|
||||
if (!props.cancelClick || props.cancelClick($event) !== false) {
|
||||
if (!props.cancelClick || props.cancelClick({ $event, form: windowForm.value }) !== false) {
|
||||
showDialog.value = false
|
||||
}
|
||||
}
|
||||
@@ -85,7 +110,7 @@ const calcBeforeClose = computed(() => {
|
||||
return props.beforeClose
|
||||
} else if (props.closeClick) {
|
||||
return done => {
|
||||
if (props.closeClick() !== false) {
|
||||
if (props.closeClick({ form: windowForm.value }) !== false) {
|
||||
done()
|
||||
}
|
||||
}
|
||||
@@ -102,9 +127,14 @@ const calcBeforeClose = computed(() => {
|
||||
:before-close="calcBeforeClose"
|
||||
:width="width"
|
||||
:draggable="draggable"
|
||||
v-bind="$attrs"
|
||||
:overflow="true"
|
||||
:destroy-on-close="destroyOnClose"
|
||||
:close-on-click-modal="closeOnClickModal"
|
||||
:close-on-press-escape="closeOnPressEscape"
|
||||
:append-to-body="appendToBody"
|
||||
>
|
||||
<el-container
|
||||
:class="defaultCls"
|
||||
:style="{ height:height }"
|
||||
>
|
||||
<slot
|
||||
@@ -135,7 +165,7 @@ const calcBeforeClose = computed(() => {
|
||||
:disabled="button.disabled"
|
||||
:round="button.round"
|
||||
:circle="button.circle"
|
||||
@click="button.click&&button.click($event)"
|
||||
@click="button.click&&button.click({$event, form:windowForm})"
|
||||
>
|
||||
{{ button.label || $t(button.labelKey) }}
|
||||
</el-button>
|
||||
|
||||
52
src/components/directives/CommonTooltip.vue
Normal file
52
src/components/directives/CommonTooltip.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
const triggerRef = ref()
|
||||
const visible = ref(false)
|
||||
|
||||
defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'el-tooltip'
|
||||
}
|
||||
})
|
||||
|
||||
const propConfig = ref({
|
||||
placement: 'top-start',
|
||||
rawContent: true
|
||||
})
|
||||
|
||||
const setConfig = (config) => {
|
||||
Object.assign(propConfig.value, config)
|
||||
}
|
||||
|
||||
const showOrHideTooltip = (show) => {
|
||||
visible.value = show
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
triggerRef,
|
||||
setConfig,
|
||||
showOrHideTooltip
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="type"
|
||||
v-if="triggerRef"
|
||||
:disabled="!propConfig.content"
|
||||
:popper-class="`common-${type}`"
|
||||
:visible="visible"
|
||||
:virtual-ref="triggerRef"
|
||||
virtual-triggering
|
||||
v-bind="propConfig"
|
||||
>
|
||||
<template #content>
|
||||
<div v-html="propConfig.content" />
|
||||
</template>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
56
src/components/directives/CommonTooltipDirective.js
Normal file
56
src/components/directives/CommonTooltipDirective.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import { isObject, isString } from 'lodash'
|
||||
import CommonTooltip from '@/components/directives/CommonTooltip.vue'
|
||||
import { DynamicHelper } from '@/components/directives/index'
|
||||
|
||||
const calcTooltipConfig = (binding) => {
|
||||
let config = {}
|
||||
if (isObject(binding.value)) {
|
||||
config = { ...config, ...binding.value }
|
||||
} else if (isString(binding.value)) {
|
||||
config.content = binding.value
|
||||
}
|
||||
if (binding.arg) {
|
||||
config.placement = binding.arg
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
const initTooltipVnode = (el, binding, props) => {
|
||||
const dynamicHelper = new DynamicHelper()
|
||||
const tooltipVnode = dynamicHelper.createAndRender(CommonTooltip, props)
|
||||
el.tooltipVnode = tooltipVnode
|
||||
el.tooltipDynamicHelper = dynamicHelper
|
||||
if (!el.tooltipConfig) {
|
||||
el.tooltipConfig = calcTooltipConfig(binding)
|
||||
}
|
||||
if (tooltipVnode.component?.exposed?.triggerRef) {
|
||||
tooltipVnode.component.exposed.triggerRef.value = el
|
||||
}
|
||||
}
|
||||
|
||||
const getTooltipDirective = (props) => {
|
||||
return {
|
||||
mounted (el, binding) {
|
||||
el.addEventListener('mouseenter', () => {
|
||||
if (!el.tooltipVnode) {
|
||||
initTooltipVnode(el, binding, props)
|
||||
}
|
||||
el?.tooltipVnode?.component?.exposed?.setConfig(el.tooltipConfig)
|
||||
el.tooltipVnode?.component?.exposed?.showOrHideTooltip(true)
|
||||
})
|
||||
el.addEventListener('mouseleave', () => {
|
||||
el.tooltipVnode?.component?.exposed?.showOrHideTooltip(false)
|
||||
})
|
||||
},
|
||||
updated (el, binding) {
|
||||
el.tooltipConfig = calcTooltipConfig(binding)
|
||||
},
|
||||
unmounted (el) {
|
||||
el.tooltipDynamicHelper?.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const CommonTooltipDirective = getTooltipDirective({ type: 'el-tooltip' })
|
||||
|
||||
export const CommonPopoverDirective = getTooltipDirective({ type: 'el-popover' })
|
||||
43
src/components/directives/index.js
Normal file
43
src/components/directives/index.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { CommonPopoverDirective, CommonTooltipDirective } from '@/components/directives/CommonTooltipDirective'
|
||||
import { h, render } from 'vue'
|
||||
|
||||
export class DynamicHelper {
|
||||
constructor () {
|
||||
this.appDivId = 'app'
|
||||
this.context = DynamicHelper.app._context
|
||||
this.container = DynamicHelper.createContainer()
|
||||
this.destroy = DynamicHelper.getDestroyFunc(this.container)
|
||||
}
|
||||
|
||||
static createContainer () {
|
||||
return document.createElement('div')
|
||||
}
|
||||
|
||||
static getDestroyFunc (container) {
|
||||
return () => {
|
||||
if (container) {
|
||||
render(null, container)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createAndRender (...args) {
|
||||
const container = this.container
|
||||
const vnode = h(...args)
|
||||
vnode.appContext = this.context
|
||||
render(vnode, container)
|
||||
const appDiv = document.getElementById(this.appDivId)
|
||||
if (appDiv && container.firstElementChild) {
|
||||
appDiv.appendChild(container.firstElementChild)
|
||||
}
|
||||
return vnode
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
install (Vue) {
|
||||
DynamicHelper.app = Vue
|
||||
Vue.directive('common-tooltip', CommonTooltipDirective)
|
||||
Vue.directive('common-popover', CommonPopoverDirective)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import CommonIcon from '@/components/common-icon/index.vue'
|
||||
import CommonIconSelect from '@/components/common-icon-select/index.vue'
|
||||
import CommonForm from '@/components/common-form/index.vue'
|
||||
import CommonFormControl from '@/components/common-form-control/index.vue'
|
||||
import CommonFilterControl from '@/components/common-form-control/common-filter-control.vue'
|
||||
import CommonTabFilter from '@/components/common-form-control/common-tab-filter.vue'
|
||||
import CommonFormLabel from '@/components/common-form-control/common-form-label.vue'
|
||||
import CommonMenu from '@/components/common-menu/index.vue'
|
||||
import CommonMenuItem from '@/components/common-menu-item/index.vue'
|
||||
@@ -11,6 +13,8 @@ import CommonTableForm from '@/components/common-table-form/index.vue'
|
||||
import CommonBreadcrumb from '@/components/common-breadcrumb/index.vue'
|
||||
import CommonWindow from '@/components/common-window/index.vue'
|
||||
import CommonAutocomplete from '@/components/common-autocomplete/index.vue'
|
||||
import CommonSort from '@/components/common-sort/index.vue'
|
||||
import CommonDirectives from '@/components/directives'
|
||||
|
||||
/**
|
||||
* 自定义通用组件自动注册
|
||||
@@ -24,6 +28,8 @@ export default {
|
||||
Vue.component('CommonIconSelect', CommonIconSelect)
|
||||
Vue.component('CommonForm', CommonForm)
|
||||
Vue.component('CommonFormControl', CommonFormControl)
|
||||
Vue.component('CommonFilterControl', CommonFilterControl)
|
||||
Vue.component('CommonTabFilter', CommonTabFilter)
|
||||
Vue.component('CommonFormLabel', CommonFormLabel)
|
||||
Vue.component('CommonMenu', CommonMenu)
|
||||
Vue.component('CommonMenuItem', CommonMenuItem)
|
||||
@@ -33,5 +39,7 @@ export default {
|
||||
Vue.component('CommonBreadcrumb', CommonBreadcrumb)
|
||||
Vue.component('CommonWindow', CommonWindow)
|
||||
Vue.component('CommonAutocomplete', CommonAutocomplete)
|
||||
Vue.component('CommonSort', CommonSort)
|
||||
Vue.use(CommonDirectives)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
import { ref } from 'vue'
|
||||
import { $i18nBundle } from '@/messages'
|
||||
import { $i18nBundle, $i18nKey } from '@/messages'
|
||||
import { isArray, isObject } from 'lodash'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
export const getFrontendPage = (totalCount, pageSize, pageNumber = 1) => {
|
||||
const pageCount = Math.floor((totalCount + pageSize - 1) / pageSize)
|
||||
if (pageNumber > pageCount && pageCount > 0) {
|
||||
pageNumber = pageCount
|
||||
}
|
||||
return {
|
||||
pageNumber,
|
||||
pageSize,
|
||||
totalCount,
|
||||
pageCount
|
||||
}
|
||||
}
|
||||
|
||||
const calcWithIf = menuItem => {
|
||||
['icon', 'labelKey', 'label', 'html'].forEach(key => {
|
||||
const keyIf = menuItem[`${key}If`]
|
||||
@@ -11,6 +25,10 @@ const calcWithIf = menuItem => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CommonFormOption} option
|
||||
* @return {string}
|
||||
*/
|
||||
export const useInputType = (option) => {
|
||||
const inType = option.type || 'input'
|
||||
if (inType.startsWith('common-') || inType.startsWith('el-')) {
|
||||
@@ -28,6 +46,15 @@ export const useMenuInfo = item => {
|
||||
}
|
||||
}
|
||||
|
||||
export const toLabelByKey = labelKey => {
|
||||
if (isArray(labelKey)) {
|
||||
return $i18nKey(...labelKey)
|
||||
}
|
||||
if (labelKey) {
|
||||
return $i18nBundle(labelKey)
|
||||
}
|
||||
}
|
||||
|
||||
export const useMenuName = item => {
|
||||
const menuInfo = useMenuInfo(item)
|
||||
if (menuInfo) {
|
||||
@@ -35,23 +62,50 @@ export const useMenuName = item => {
|
||||
return menuInfo.label
|
||||
}
|
||||
if (menuInfo.labelKey) {
|
||||
return $i18nBundle(menuInfo.labelKey)
|
||||
return toLabelByKey(menuInfo.labelKey)
|
||||
}
|
||||
}
|
||||
if (item.meta && item.meta.labelKey) {
|
||||
return $i18nBundle(item.meta.labelKey)
|
||||
return toLabelByKey(item.meta.labelKey)
|
||||
}
|
||||
return item.name || 'No Name'
|
||||
}
|
||||
/**
|
||||
* 外部链接判断
|
||||
* @param path
|
||||
* @return {boolean}
|
||||
*/
|
||||
export const isExternalLink = (path) => {
|
||||
return /^(https?:|mailto:|tel:)/.test(path)
|
||||
}
|
||||
/**
|
||||
* 外部菜单类型判断
|
||||
* @param menuItem
|
||||
* @return {boolean}
|
||||
*/
|
||||
export const isExternalMenu = menuItem => {
|
||||
return menuItem.external || isExternalLink(menuItem.index)
|
||||
}
|
||||
|
||||
export const filterMenus = menus => menus.filter(menu => !menu.disabled)
|
||||
/**
|
||||
* @param menus {[CommonMenuItem] }菜单列表
|
||||
* @return {[CommonMenuItem]}
|
||||
*/
|
||||
export const processMenus = menus => menus.filter(menu => menu.enabled !== false)
|
||||
.map(menu => {
|
||||
calcWithIf(menu)
|
||||
if (menu.index) { // 把菜单存储下来,后面需要使用名字
|
||||
MENU_INFO_LIST.value[menu.index] = menu
|
||||
}
|
||||
if (menu.children && menu.children.length) {
|
||||
menu.children = filterMenus(menu.children)
|
||||
menu.children = processMenus(menu.children)
|
||||
}
|
||||
if (isExternalMenu(menu) && !menu.click) { // 跳转外部链接
|
||||
const url = menu.index
|
||||
menu.index = ''
|
||||
menu.click = () => {
|
||||
window.open(url, menu.target || '_blank')
|
||||
}
|
||||
}
|
||||
return menu
|
||||
})
|
||||
@@ -62,7 +116,7 @@ export const filterMenus = menus => menus.filter(menu => !menu.disabled)
|
||||
* @param route {RouteRecordMultipleViewsWithChildren} 路由信息
|
||||
*/
|
||||
export const useParentRoute = function (route) {
|
||||
const parentName = route.meta?.replaceTabHistory
|
||||
const parentName = route?.meta?.replaceTabHistory
|
||||
if (parentName) {
|
||||
const routes = route.matched || []
|
||||
for (let i = routes.length - 1; i > 0; i--) {
|
||||
@@ -75,6 +129,48 @@ export const useParentRoute = function (route) {
|
||||
return route
|
||||
}
|
||||
|
||||
export const parsePathParams = (path, params) => {
|
||||
if (path && path.includes(':') && isObject(params)) {
|
||||
Object.keys(params).forEach(key => {
|
||||
path = path.replace(new RegExp(`:${key}`, 'g'), params[key])
|
||||
})
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义表单选项,带有jsdoc注解,方便代码提示
|
||||
* @param {CommonFormOption[]} formOptions 表单选项
|
||||
* @return {CommonFormOption[]} 表单选项配置
|
||||
*/
|
||||
export const defineFormOptions = (formOptions) => {
|
||||
return formOptions
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义表格选项,带有jsdoc注解,方便代码提示
|
||||
* @param {CommonTableColumn[]} tableColumns 表单的列
|
||||
* @return {CommonTableColumn[]} 表单的列配置
|
||||
*/
|
||||
export const defineTableColumns = (tableColumns) => {
|
||||
return tableColumns
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义表格的按钮,带有jsdoc注解,方便代码提示
|
||||
* @param {TableButtonProps[]} tableButtons 表格的按钮
|
||||
* @return {TableButtonProps[]} 表格的按钮配置
|
||||
*/
|
||||
export const defineTableButtons = (tableButtons) => {
|
||||
return tableButtons
|
||||
}
|
||||
/**
|
||||
* @param menuItems {CommonMenuItem[]} 菜单配置项
|
||||
* @return {CommonMenuItem[]} 菜单配置项
|
||||
*/
|
||||
export const defineMenuItems = (menuItems) => {
|
||||
return menuItems
|
||||
}
|
||||
export const formatDate = (date, format) => {
|
||||
if (date) {
|
||||
return dayjs(date).format(format || 'YYYY-MM-DD HH:mm:ss')
|
||||
@@ -86,3 +182,4 @@ export const formatDay = (date, format) => {
|
||||
return dayjs(date).format(format || 'YYYY-MM-DD')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,11 +35,15 @@ export const changeMessages = locale => {
|
||||
export const $changeLocale = locale => {
|
||||
useGlobalConfigStore().changeLocale(locale)
|
||||
}
|
||||
|
||||
export const $isLocale = locale => {
|
||||
return useGlobalConfigStore().currentLocale === locale
|
||||
}
|
||||
/**
|
||||
* @param cn
|
||||
* @param en
|
||||
* @param cn 中文字段
|
||||
* @param en 英文字段
|
||||
* @param {boolean} replaceEmpty 为空是否用不为空的数据代替
|
||||
* @returns {*}
|
||||
* @returns {String}
|
||||
*/
|
||||
export const $i18nMsg = (cn, en, replaceEmpty = true) => {
|
||||
const { currentLocale } = useGlobalConfigStore()
|
||||
@@ -48,16 +52,50 @@ export const $i18nMsg = (cn, en, replaceEmpty = true) => {
|
||||
}
|
||||
return replaceEmpty ? (en || cn) : en
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} key 国际化资源key
|
||||
* @param {String[]=} params 可选参数
|
||||
* @returns {string}
|
||||
*/
|
||||
export const $i18nBundle = i18n.global.t
|
||||
|
||||
/**
|
||||
* 根据key和locale返回数据<br>
|
||||
* vue-i18n似乎有bug,按照官方文档传locale得不到正确的消息:<br>
|
||||
* <code>$t('ab.c', 'zh-CN')</code><br>
|
||||
* https://vue-i18n.intlify.dev/api/injection.html#t-key-locale
|
||||
* @param key
|
||||
* @param locale
|
||||
* @param [args]
|
||||
* @return {String}
|
||||
*/
|
||||
export const $i18nByLocale = (key, locale, args) => {
|
||||
return i18n.global.t(key, locale, {
|
||||
locale,
|
||||
list: args || []
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 方便多个资源key解析
|
||||
* @param {String} key 国际化资源key
|
||||
* @param {String} args 可选参数,也是资源key,方便多个资源key解析
|
||||
*/
|
||||
export const $i18nKey = (key, ...args) => {
|
||||
args = args.map(argKey => $i18nBundle(argKey))
|
||||
return $i18nBundle(key, args)
|
||||
}
|
||||
|
||||
export default {
|
||||
install (app) {
|
||||
app.use(i18n)
|
||||
Object.assign(app.config.globalProperties, {
|
||||
$changeLocale,
|
||||
$i18nMsg,
|
||||
$i18nBundle
|
||||
$i18nKey,
|
||||
$i18nBundle,
|
||||
$isLocale,
|
||||
$i18nByLocale
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,28 @@
|
||||
import { createPinia } from 'pinia'
|
||||
import piniaPluginPersistedState from 'pinia-plugin-persistedstate'
|
||||
import { createPersistedState } from 'pinia-plugin-persistedstate'
|
||||
|
||||
/**
|
||||
* 组合式api的$reset需要自己实现
|
||||
*
|
||||
* @param store
|
||||
*/
|
||||
const piniaPluginResetStore = ({ store }) => {
|
||||
const initialState = JSON.parse(JSON.stringify(store.$state)) // deep clone(store.$state)
|
||||
store.$reset = () => {
|
||||
store.$state = JSON.parse(JSON.stringify(initialState))
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
install (app) {
|
||||
const pinia = createPinia()
|
||||
pinia.use(piniaPluginPersistedState)
|
||||
pinia.use(piniaPluginResetStore)
|
||||
pinia.use(createPersistedState({
|
||||
key: key => {
|
||||
const systemKey = import.meta.env.VITE_APP_SYSTEM_KEY
|
||||
return `__${systemKey}__${key}`
|
||||
}
|
||||
}))
|
||||
app.use(pinia)
|
||||
return pinia
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { computed, ref } from 'vue'
|
||||
import { useCityAutocompleteConfig, useCitySelectPageConfig } from '@/services/city/CityService'
|
||||
import { $i18nMsg } from '@/messages'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { defineFormOptions } from '@/components/utils'
|
||||
|
||||
const defaultCity = ref({})
|
||||
|
||||
@@ -14,11 +15,53 @@ setTimeout(() => {
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
const getGenderOptions = (type) => {
|
||||
type = type || 'radio'
|
||||
return [
|
||||
{
|
||||
type,
|
||||
label: '男',
|
||||
value: 'male'
|
||||
},
|
||||
{
|
||||
type,
|
||||
label: '女',
|
||||
value: 'female'
|
||||
},
|
||||
{
|
||||
type,
|
||||
label: '保密',
|
||||
value: 'unknown'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const getHobbyOptions = (type) => {
|
||||
type = type || 'checkbox'
|
||||
return [
|
||||
{
|
||||
type,
|
||||
label: '编程',
|
||||
value: 'program'
|
||||
},
|
||||
{
|
||||
type,
|
||||
label: '吃饭',
|
||||
value: 'eat'
|
||||
},
|
||||
{
|
||||
type,
|
||||
label: '睡觉',
|
||||
value: 'sleep'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {[CommonFormOption]}
|
||||
*/
|
||||
const formOptions = computed(() => {
|
||||
return [{
|
||||
return defineFormOptions([{
|
||||
label: '用户名',
|
||||
prop: 'userName',
|
||||
value: '',
|
||||
@@ -52,22 +95,14 @@ const formOptions = computed(() => {
|
||||
label: '兴趣爱好',
|
||||
type: 'checkbox-group',
|
||||
prop: 'hobby',
|
||||
value: '',
|
||||
required: true,
|
||||
children: [
|
||||
{
|
||||
label: '编程',
|
||||
value: 'program'
|
||||
},
|
||||
{
|
||||
label: '吃饭',
|
||||
value: 'eat'
|
||||
},
|
||||
{
|
||||
label: '睡觉',
|
||||
value: 'sleep'
|
||||
}
|
||||
]
|
||||
children: getHobbyOptions()
|
||||
}, {
|
||||
label: '兴趣爱好',
|
||||
type: 'checkbox-group',
|
||||
prop: 'hobby',
|
||||
required: true,
|
||||
children: getHobbyOptions('checkbox-button')
|
||||
}, {
|
||||
label: '职业',
|
||||
type: 'select',
|
||||
@@ -94,20 +129,14 @@ const formOptions = computed(() => {
|
||||
prop: 'gender',
|
||||
value: '',
|
||||
required: true,
|
||||
children: [
|
||||
{
|
||||
label: '男',
|
||||
value: 'male'
|
||||
},
|
||||
{
|
||||
label: '女',
|
||||
value: 'female'
|
||||
},
|
||||
{
|
||||
label: '保密',
|
||||
value: 'unknown'
|
||||
}
|
||||
]
|
||||
children: getGenderOptions()
|
||||
}, {
|
||||
label: '性别',
|
||||
type: 'radio-group',
|
||||
prop: 'gender',
|
||||
value: '',
|
||||
required: true,
|
||||
children: getGenderOptions('radio-button')
|
||||
}, {
|
||||
label: '图标',
|
||||
prop: 'icon',
|
||||
@@ -124,6 +153,9 @@ const formOptions = computed(() => {
|
||||
change: (city) => {
|
||||
defaultCity.value = city
|
||||
},
|
||||
getAutocompleteLabel: () => {
|
||||
return $i18nMsg(defaultCity.value?.nameCN, defaultCity.value?.nameEN)
|
||||
},
|
||||
attrs: {
|
||||
defaultLabel: $i18nMsg(defaultCity.value?.nameCn, defaultCity.value?.nameEn),
|
||||
autocompleteConfig: useCityAutocompleteConfig(),
|
||||
@@ -146,9 +178,11 @@ const formOptions = computed(() => {
|
||||
prop: 'address',
|
||||
value: '',
|
||||
attrs: {
|
||||
type: 'textarea'
|
||||
type: 'textarea',
|
||||
maxlength: 100,
|
||||
showWordLimit: true
|
||||
}
|
||||
}]
|
||||
}])
|
||||
})
|
||||
const userDto = ref({
|
||||
contacts: [{
|
||||
@@ -195,7 +229,7 @@ const submitForm = (form) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<el-container class="flex-column container-center">
|
||||
<common-form
|
||||
:model="userDto"
|
||||
:options="formOptions"
|
||||
@@ -246,7 +280,7 @@ const submitForm = (form) => {
|
||||
<div>
|
||||
{{ userDto }}
|
||||
</div>
|
||||
</div>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -13,8 +13,7 @@ const tableData = ref([])
|
||||
const loading = ref(true)
|
||||
const loadUsers = async () => {
|
||||
loading.value = true
|
||||
const usersResult = await loadUsersResult({ page: page.value })
|
||||
loading.value = false
|
||||
const usersResult = await loadUsersResult({ page: page.value }).finally(() => (loading.value = false))
|
||||
if (usersResult.success && usersResult.resultData) {
|
||||
const resultData = usersResult.resultData
|
||||
tableData.value = resultData.userList
|
||||
|
||||
@@ -2,10 +2,16 @@ import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { viteMockServe } from 'vite-plugin-mock'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
plugins: [
|
||||
vue(),
|
||||
viteMockServe({
|
||||
mockPath: './mock'
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
|
||||
Reference in New Issue
Block a user