1. 升级依赖版本

This commit is contained in:
Gary Fu
2024-03-30 20:56:30 +08:00
parent 5fa3fdbc78
commit 3d803b4ef5
41 changed files with 3340 additions and 663 deletions

4
.env
View File

@@ -1,7 +1,7 @@
# 程序名称 # 程序名称
VITE_APP_NAME=Simple Element+ 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_API_TIMEOUT=10000
VITE_APP_SYSTEM_KEY=SIMPLE-ELEMENT

75
mock/MockCity.js Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -11,27 +11,29 @@
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
"@vicons/material": "^0.12.0", "@vicons/material": "^0.12.0",
"@vueuse/core": "^10.7.0", "@vueuse/core": "^10.9.0",
"axios": "^1.6.3", "axios": "^1.6.8",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"element-plus": "^2.4.4", "element-plus": "^2.6.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mockjs": "^1.1.0",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1", "pinia-plugin-persistedstate": "^3.2.1",
"vue": "^3.3.13", "vite-plugin-mock": "^3.0.1",
"vue-i18n": "^9.8.0", "vue": "^3.4.21",
"vue-router": "^4.2.5", "vue-i18n": "^9.10.2",
"vue-router": "^4.3.0",
"vue-virtual-scroller": "^2.0.0-beta.8" "vue-virtual-scroller": "^2.0.0-beta.8"
}, },
"devDependencies": { "devDependencies": {
"@rushstack/eslint-patch": "^1.6.1", "@rushstack/eslint-patch": "^1.10.1",
"@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^6.18.1", "@typescript-eslint/parser": "^7.4.0",
"@vitejs/plugin-vue": "^4.5.2", "@vitejs/plugin-vue": "^5.0.4",
"@vue/eslint-config-standard": "^8.0.1", "@vue/eslint-config-standard": "^8.0.1",
"eslint": "^8.56.0", "eslint": "^8.57.0",
"eslint-plugin-vue": "^9.19.2", "eslint-plugin-vue": "^9.24.0",
"typescript": "^5.3.3", "typescript": "^5.4.3",
"vite": "^5.0.10" "vite": "^5.2.7"
} }
} }

View File

@@ -19,23 +19,62 @@ html, body, #app, .index-container {
border-right: 0 none; 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) { .el-menu-left:not(.el-menu--collapse) {
width: 250px; width: 250px;
min-height: 400px; 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-left1 {
padding-left: 5px; padding-left: 5px;
} }
.padding-left2 { .padding-left2 {
padding-left: 10px; padding-left: 10px;
} }
.padding-left3 {
padding-left: 15px;
}
.padding-right1 { .padding-right1 {
padding-right: 5px; padding-right: 5px;
} }
.padding-right2 { .padding-right2 {
padding-right: 10px; padding-right: 10px;
} }
.padding-right3 {
padding-right: 15px;
}
.padding-top1 { .padding-top1 {
padding-top: 5px; padding-top: 5px;
@@ -43,12 +82,59 @@ html, body, #app, .index-container {
.padding-top2 { .padding-top2 {
padding-top: 10px; padding-top: 10px;
} }
.padding-top3 {
padding-top: 15px;
}
.padding-bottom1 { .padding-bottom1 {
padding-bottom: 5px; padding-bottom: 5px;
} }
.padding-bottom2 { .padding-bottom2 {
padding-bottom: 10px; 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-center {
text-align: center; text-align: center;
} }
@@ -94,6 +180,10 @@ html, body, #app, .index-container {
flex-direction: column; flex-direction: column;
} }
.flex-start {
align-items: flex-start;
}
.container-center { .container-center {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -101,22 +191,63 @@ html, body, #app, .index-container {
padding-top: 20px; padding-top: 20px;
} }
.common-form.el-form--inline .el-input{ .reason-code-container .el-radio__label{
--el-input-width: 220px; 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{ .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 { .form-edit-width-70 {
width:70% width:70%
} }
.form-edit-width-90 {
width:90%
}
.form-edit-width-100 { .form-edit-width-100 {
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{ .common-autocomplete .el-popover__title{
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
@@ -133,6 +264,8 @@ html, body, #app, .index-container {
.common-autocomplete .common-select-page .el-tabs__item { .common-autocomplete .common-select-page .el-tabs__item {
height: 30px; height: 30px;
padding-left: 10px !important;
padding-right: 10px !important;
} }
.common-autocomplete .common-select-page .common-select-page-btn { .common-autocomplete .common-select-page .common-select-page-btn {
@@ -146,6 +279,90 @@ html, body, #app, .index-container {
padding: 5px; 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动画 * slide-fade动画
*/ */

View File

@@ -1,8 +1,7 @@
<script setup> <script setup>
import { computed, nextTick, onMounted, ref, watch } from 'vue' 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 { onClickOutside, onKeyStroke, useVModel } from '@vueuse/core'
import chunk from 'lodash/chunk'
import { UPDATE_MODEL_EVENT, CHANGE_EVENT, useFormItem } from 'element-plus' import { UPDATE_MODEL_EVENT, CHANGE_EVENT, useFormItem } from 'element-plus'
/** /**
@@ -51,7 +50,7 @@ const props = defineProps({
}, },
inputWidth: { inputWidth: {
type: String, type: String,
default: '200px' default: ''
}, },
autocompleteConfig: { autocompleteConfig: {
type: Object, type: Object,
@@ -128,10 +127,13 @@ const selectPageData = ref({})
const selectPageTab = ref(null) const selectPageTab = ref(null)
const popoverVisible = ref(false) const popoverVisible = ref(false)
const autocompletePopover = ref() const autocompletePopover = ref()
const autoPage = ref({ const defaultAutoPage = {
pageSize: 8, pageSize: props.autocompleteConfig?.pageSize || 8,
pageNumber: 1 pageNumber: 1
}) }
const autoPage = ref(props.autocompleteConfig?.frontendPaging
? null
: cloneDeep(defaultAutoPage))
const loadingData = ref(false) const loadingData = ref(false)
const idProp = computed(() => props.autocompleteConfig?.idProp || props.idProp) const idProp = computed(() => props.autocompleteConfig?.idProp || props.idProp)
@@ -194,10 +196,21 @@ const onInputKeywords = debounce((input) => {
} }
}, props.debounceTime) }, 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(() => { onMounted(() => {
onClickOutside(autocompletePopover.value?.popperRef?.contentRef, () => { onClickOutside(autocompletePopover.value?.popperRef?.contentRef, () => {
popoverVisible.value = false popoverVisible.value = false
}) })
setAutocompleteLabel(calcDefaultLabel.value)
}) })
watch(() => popoverVisible.value, (val) => { watch(() => popoverVisible.value, (val) => {
@@ -211,7 +224,6 @@ watch(() => popoverVisible.value, (val) => {
}) })
watch(() => props.modelValue, (value) => { watch(() => props.modelValue, (value) => {
console.info('=====================value', value)
if (!props.useIdModel) { if (!props.useIdModel) {
setAutocompleteLabel(value && isObject(value) ? value[labelProp.value] : '') setAutocompleteLabel(value && isObject(value) ? value[labelProp.value] : '')
if (isEmpty(value)) { if (isEmpty(value)) {
@@ -220,13 +232,7 @@ watch(() => props.modelValue, (value) => {
} }
}) })
watch(() => { watch(calcDefaultLabelFunc, (label) => {
if (!props.useIdModel) {
const value = props.modelValue
return value && isObject(value) ? value[labelProp.value] : ''
}
return props.defaultLabel
}, (label) => {
setAutocompleteLabel(label) setAutocompleteLabel(label)
}) })
@@ -287,7 +293,6 @@ const moveSelection = function (down) {
currentOnIndex.value = -1 currentOnIndex.value = -1
currentOnRow.value = null currentOnRow.value = null
} }
console.info('=================', tableRef.value.table, currentOnIndex.value, currentOnRow.value)
tableRef.value.table?.setCurrentRow(currentOnRow.value) tableRef.value.table?.setCurrentRow(currentOnRow.value)
} }
@@ -349,6 +354,15 @@ watch(() => props.selectPageConfig, () => {
selectPageTab.value = null selectPageTab.value = null
}) })
watch(() => props.autocompleteConfig, (autocompleteConfig) => {
defaultAutoPage.pageSize = autocompleteConfig.pageSize || 8
if (autocompleteConfig.frontendPaging) {
autoPage.value = null
} else {
autoPage.value = cloneDeep(defaultAutoPage)
}
})
</script> </script>
<template> <template>
@@ -443,6 +457,8 @@ watch(() => props.selectPageConfig, () => {
:empty-text="autocompleteConfig.emptyMessage" :empty-text="autocompleteConfig.emptyMessage"
:data="dataList" :data="dataList"
:page-attrs="pageAttrs" :page-attrs="pageAttrs"
:frontend-paging="autocompleteConfig.frontendPaging"
:frontend-page-size="defaultAutoPage.pageSize"
@row-click="onSelectData($event)" @row-click="onSelectData($event)"
@current-page-change="onInputKeywords(false)" @current-page-change="onInputKeywords(false)"
> >

View File

@@ -19,6 +19,18 @@ export interface CommonSelectPageOption {
} }
export interface CommonAutocompleteOption { export interface CommonAutocompleteOption {
/** id属性名 */
labelProp?: string;
/** label属性名 */
idProp?: string;
/**
* 分页数
*/
pageSize: number;
/**
* 前端分页模式
*/
frontendPaging: boolean;
/** 自动完成表格列配置 */ /** 自动完成表格列配置 */
columns: Array<CommonTableColumn>; columns: Array<CommonTableColumn>;
/** 空数据提示信息 */ /** 空数据提示信息 */

View File

@@ -2,7 +2,7 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useTabsViewStore } from '@/stores/TabsViewStore' import { useTabsViewStore } from '@/stores/TabsViewStore'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { useMenuInfo, useMenuName } from '@/components/utils' import { parsePathParams, useMenuInfo, useMenuName } from '@/components/utils'
const tabsViewStore = useTabsViewStore() const tabsViewStore = useTabsViewStore()
@@ -20,7 +20,7 @@ const breadcrumbs = computed(() => {
icon = item.meta.icon icon = item.meta.icon
} }
return { return {
path: item.path, path: parsePathParams(item.path, route.params),
menuName: useMenuName(item), menuName: useMenuName(item),
icon icon
} }
@@ -32,11 +32,11 @@ const breadcrumbs = computed(() => {
return notExist && !item.menuName.endsWith('Base') return notExist && !item.menuName.endsWith('Base')
}) })
}) })
</script> </script>
<template> <template>
<el-breadcrumb <el-breadcrumb
v-bind="$attrs"
class="common-breadcrumb" class="common-breadcrumb"
> >
<el-breadcrumb-item <el-breadcrumb-item

View 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>

View 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>

View 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 }}&nbsp;
<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>

View File

@@ -1,7 +1,6 @@
<script setup> <script setup>
import { computed } from 'vue' import { computed } from 'vue'
import { $i18nBundle } from '@/messages' import { toLabelByKey, useInputType } from '@/components/utils'
import { useInputType } from '@/components/utils'
/** /**
* @type {{option:CommonFormOption}} * @type {{option:CommonFormOption}}
@@ -21,31 +20,17 @@ const inputType = computed(() => useInputType(props.option))
const label = computed(() => { const label = computed(() => {
const option = props.option const option = props.option
if (option.labelKey) { if (option.labelKey) {
return $i18nBundle(option.labelKey) return toLabelByKey(option.labelKey)
} }
return option.label 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> </script>
<template> <template>
<component <component
:is="inputType" :is="inputType"
:value="option.value" :value="option.value"
:label="labelOrValue" :label="label"
v-bind="option.attrs" v-bind="option.attrs"
> >
{{ label }} {{ label }}

View File

@@ -1,10 +1,11 @@
<script setup> <script setup>
import { computed, ref, watch } from 'vue' import { computed, isVNode, ref, watch } from 'vue'
import { $i18nBundle } from '@/messages' import { $i18nBundle } from '@/messages'
import ControlChild from '@/components/common-form-control/control-child.vue' 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 cloneDeep from 'lodash/cloneDeep'
import { get, set } from 'lodash' import { get, isFunction, set } from 'lodash'
import dayjs from 'dayjs'
/** /**
* @type {{option:CommonFormOption}} * @type {{option:CommonFormOption}}
@@ -24,13 +25,27 @@ const props = defineProps({
labelWidth: { labelWidth: {
type: String, type: String,
default: null 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 modelAttrs = computed(() => {
const option = props.option const option = calcOption.value
const attrs = { ...option.attrs } 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)) { 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 attrs.clearable = true
@@ -38,33 +53,67 @@ const modelAttrs = computed(() => {
if (inputType.value === 'common-autocomplete' && option.getAutocompleteLabel) { if (inputType.value === 'common-autocomplete' && option.getAutocompleteLabel) {
attrs.defaultLabel = option.getAutocompleteLabel(props.model, option) 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 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 label = computed(() => {
const option = props.option const option = calcOption.value
if (option.labelKey) { if (option.labelKey) {
return $i18nBundle(option.labelKey) return toLabelByKey(option.labelKey)
} }
return option.label return option.label
}) })
const showLabel = computed(() => { 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({ const modelValue = computed({
get () { get () {
if (formModel.value && props.option.prop) { if (formModel.value && calcOption.value.prop) {
return get(formModel.value, props.option.prop) return get(formModel.value, calcOption.value.prop)
} }
return null return null
}, },
set (val) { set (val) {
if (formModel.value && props.option.prop) { if (formModel.value && calcOption.value.prop) {
set(formModel.value, props.option.prop, val) set(formModel.value, calcOption.value.prop, val)
} }
} }
}) })
@@ -76,7 +125,7 @@ const childTypeMapping = { // 自动映射子元素类型,配置的时候可
} }
const children = computed(() => { const children = computed(() => {
const option = props.option const option = calcOption.value
const result = option.children || [] // 初始化一些默认值 const result = option.children || [] // 初始化一些默认值
result.forEach(childItem => { result.forEach(childItem => {
if (!childItem.type) { if (!childItem.type) {
@@ -89,11 +138,11 @@ const children = computed(() => {
const formItemRef = ref() const formItemRef = ref()
const rules = computed(() => { const rules = computed(() => {
const option = props.option const option = calcOption.value
let _rules = cloneDeep(option.rules || []) let _rules = cloneDeep(option.rules || [])
if (option.prop) { if (option.prop) {
if (option.required !== undefined) { if (option.required !== undefined) {
const label = option.label || $i18nBundle(option.labelKey) const label = option.label || toLabelByKey(option.labelKey)
_rules = [{ _rules = [{
trigger: option.trigger, trigger: option.trigger,
required: option.required, required: option.required,
@@ -101,7 +150,7 @@ const rules = computed(() => {
}, ..._rules] }, ..._rules]
} }
if (option.pattern !== undefined) { if (option.pattern !== undefined) {
const label = option.label || $i18nBundle(option.labelKey) const label = option.label || toLabelByKey(option.labelKey)
_rules = [{ _rules = [{
pattern: option.pattern, pattern: option.pattern,
message: option.patternMsg || $i18nBundle('common.msg.patternInvalid', [label]) message: option.patternMsg || $i18nBundle('common.msg.patternInvalid', [label])
@@ -114,7 +163,7 @@ const rules = computed(() => {
const initFormModel = () => { const initFormModel = () => {
if (formModel.value) { if (formModel.value) {
const option = props.option const option = calcOption.value
if (option.prop) { if (option.prop) {
const defaultVal = get(formModel.value, option.prop) const defaultVal = get(formModel.value, option.prop)
set(formModel.value, option.prop, defaultVal ?? option.value ?? undefined) set(formModel.value, option.prop, defaultVal ?? option.value ?? undefined)
@@ -124,23 +173,48 @@ const initFormModel = () => {
initFormModel() initFormModel()
watch(() => props.option, initFormModel, { deep: true }) watch(() => calcOption.value, initFormModel, { deep: true })
const hasModelText = computed(() => { const hasModelText = computed(() => {
return !!(modelAttrs.value.modelText || modelAttrs.value.modelTextFunc) return modelAttrs.value.modelText || calcOption.value.formatter
}) })
const emit = defineEmits(['change']) const emit = defineEmits(['change'])
const controlChange = (...args) => { const controlChange = (...args) => {
const option = props.option const option = calcOption.value
if (option.change) { if (option.change) {
option.change(...args) option.change(...args)
} }
emit('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> </script>
@@ -149,26 +223,30 @@ const formItemEnabled = computed(() => props.option.enabled !== false)
v-if="formItemEnabled" v-if="formItemEnabled"
ref="formItemRef" ref="formItemRef"
:rules="rules" :rules="rules"
:prop="option.prop" :prop="calcOption.prop"
:label-width="labelWidth" :style="calcOption.style"
:label-width="controlLabelWidth"
v-bind="$attrs"
> >
<template <template
v-if="showLabel" v-if="showLabel"
#label #label
> >
<span>{{ label }}</span> <slot name="beforeLabel" />
<span :class="calcOption.labelCls">{{ label }}</span>
<slot name="afterLabel" />
<el-tooltip <el-tooltip
v-if="option.tooltip||option.tooltipFunc" v-if="calcOption.tooltip||calcOption.tooltipFunc"
class="box-item" class="box-item"
effect="dark" effect="dark"
:disabled="!option.tooltip" :disabled="!calcOption.tooltip"
:content="option.tooltip" :content="calcOption.tooltip"
placement="top-start" placement="top-start"
> >
<span> <span>
<el-link <el-link
:underline="false" :underline="false"
@click="option.tooltipFunc" @click="calcOption.tooltipFunc"
>&nbsp; >&nbsp;
<common-icon <common-icon
icon="QuestionFilled" icon="QuestionFilled"
@@ -181,17 +259,27 @@ const formItemEnabled = computed(() => props.option.enabled !== false)
:is="inputType" :is="inputType"
v-model="modelValue" v-model="modelValue"
v-bind="modelAttrs" v-bind="modelAttrs"
:placeholder="option.placeholder" :placeholder="calcOption.placeholder"
:disabled="option.disabled" :disabled="calcOption.disabled"
:readonly="option.readonly" :readonly="calcOption.readonly"
@change="controlChange" @change="controlChange"
> >
<template <template
v-if="hasModelText" v-if="hasModelText&&formatResult"
#default #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> </template>
<slot name="childBefore" />
<template v-if="children&&children.length"> <template v-if="children&&children.length">
<control-child <control-child
v-for="(childItem, index) in children" v-for="(childItem, index) in children"
@@ -199,7 +287,9 @@ const formItemEnabled = computed(() => props.option.enabled !== false)
:option="childItem" :option="childItem"
/> />
</template> </template>
<slot name="childAfter" />
</component> </component>
<slot name="after" />
</el-form-item> </el-form-item>
</template> </template>

View File

@@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref } from 'vue' import { inject, ref, onMounted, isRef, watchEffect } from 'vue'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
/** /**
@@ -23,6 +23,17 @@ const props = defineProps({
type: Object, type: Object,
default: null default: null
}, },
inline: {
type: Boolean
},
className: {
type: String,
default: 'common-form'
},
buttonStyle: {
type: [String, Object],
default: ''
},
validateOnRuleChange: { validateOnRuleChange: {
type: Boolean, type: Boolean,
default: false default: false
@@ -35,6 +46,10 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: true default: true
}, },
disableSubmitIfNotValid: {
type: Boolean,
default: false
},
submitLabel: { submitLabel: {
type: String, type: String,
default: '' default: ''
@@ -66,22 +81,37 @@ const emit = defineEmits(['submitForm', 'update:model'])
const formModel = useVModel(props, 'model', emit) const formModel = useVModel(props, 'model', emit)
//= ============form暴露============// //= ============form暴露============//
const form = ref() const form = ref()
defineExpose({ defineExpose({
form 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> </script>
<template> <template>
<el-form <el-form
ref="form" ref="form"
class="common-form" :inline="inline"
:class="className"
:model="formModel" :model="formModel"
:label-width="labelWidth" :label-width="labelWidth"
v-bind="$attrs"
:validate-on-rule-change="validateOnRuleChange" :validate-on-rule-change="validateOnRuleChange"
> >
<template <template
@@ -90,13 +120,13 @@ defineExpose({
> >
<slot <slot
v-if="option.slot" v-if="option.slot"
name="option.slot" :name="option.slot"
:option="option" :option="option"
:form="form" :form="form"
:model="formModel" :model="formModel"
/> />
<common-form-control <common-form-control
v-if="option.enabled!==false" v-if="!option.slot&&option.enabled!==false"
:model="formModel" :model="formModel"
:option="option" :option="option"
/> />
@@ -106,9 +136,13 @@ defineExpose({
:model="formModel" :model="formModel"
name="default" name="default"
/> />
<el-form-item v-if="showButtons"> <el-form-item
v-if="showButtons"
:style="buttonStyle"
>
<el-button <el-button
v-if="showSubmit" v-if="showSubmit"
:disabled="disableSubmit"
type="primary" type="primary"
@click="$emit('submitForm', form)" @click="$emit('submitForm', form)"
> >

View File

@@ -1,19 +1,114 @@
import { RuleItem } from 'async-validator/dist-types/interface' 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 { export type TimePickerProps = ExtractPropTypes<typeof timePickerDefaultProps>
/** 表单类型 */ export type SelectProps = ExtractPropTypes<typeof SelectV1Props>
type: 'input' | 'input-number' | 'cascader' | 'radio' export interface OptionProps {
| 'radio-group' | 'radio-button' | 'checkbox' | 'checkbox-group' | 'checkbox-button' | 'date-picker' label: string;
| 'time-picker' | 'switch' | 'select' | 'option' | 'slider' | 'transfer' | 'upload' | 'common-icon-select' | 'common-autocomplete' | 'tree-select'; 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; value?: any;
/** 属性名 */ /** 属性名 */
prop: string | string[]; prop?: string | string[];
/** 表单标签 */ /** 表单标签 */
label?: string; label?: string;
/** 用于国际化的label */ /** 用于国际化的label */
labelKey?: string; labelKey?: string;
/**
* 样式自定义
*/
labelCls?: string;
/**
* item样式
*/
style: CSSProperties;
/** 是否必填,后面解析成为rules的一部分 */ /** 是否必填,后面解析成为rules的一部分 */
required?: boolean; required?: boolean;
/** 正则表达式验证解析成为rules的一部分 */ /** 正则表达式验证解析成为rules的一部分 */
@@ -22,27 +117,42 @@ export interface CommonFormOption {
patternMsg?: string; patternMsg?: string;
/** 是否禁用 */ /** 是否禁用 */
disabled?: boolean; disabled?: boolean;
/** 是否显示 */
enabled?: boolean;
/** 是否只读 */ /** 是否只读 */
readonly?: boolean; readonly?: boolean;
/** 占位提示符 */ /** 占位提示符 */
placeholder?: string; placeholder?: string;
/** 其他可用属性 */
attrs?: {
showPassword: boolean,
[key: string]: any
};
/** 有些控件柚子节点 */ /** 有些控件柚子节点 */
children?: Array<CommonFormOption>; children?: Array<CommonFormOption>;
/** async-validator验证器 */ /** async-validator验证器 */
rules: Array<RuleItem>; rules?: Array<RuleItem>;
/** change事件 */ /** 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名称 */
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 { export interface CommonFormProps extends FormProps {
@@ -64,10 +174,14 @@ export interface CommonFormProps extends FormProps {
showButtons: boolean; showButtons: boolean;
/** 是否显示提交按钮 */ /** 是否显示提交按钮 */
showSubmit: boolean; showSubmit: boolean;
/** 当校验不通过时提交按钮不可点击,默认为 false: 校验不通过也可直接提交 */
disableSubmitIfNotValid: boolean;
/** 是否显示重置按钮 */ /** 是否显示重置按钮 */
showReset: boolean; showReset: boolean;
/** 提交逻辑 */ /** 提交逻辑 */
submitForm: (form: FormInstance) => void; submitForm: (form: FormInstance) => void;
/** 返回地址 */ /** 返回地址 */
backUrl: string; backUrl: string;
/** 行级排列 */
inline: boolean;
} }

View File

@@ -1,14 +1,9 @@
<script setup> <script setup>
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { filterIconsByKeywords } from '@/services/icon/IconService' import { filterIconsByKeywords } from '@/services/icon/IconService'
import { useVModel } from '@vueuse/core'
import { UPDATE_MODEL_EVENT, CHANGE_EVENT, useFormItem } from 'element-plus' import { UPDATE_MODEL_EVENT, CHANGE_EVENT, useFormItem } from 'element-plus'
const props = defineProps({ const props = defineProps({
modelValue: {
type: String,
default: ''
},
dialogAttrs: { dialogAttrs: {
type: Object, type: Object,
default () { default () {
@@ -60,7 +55,10 @@ const filterIcons = computed(() => {
const emit = defineEmits([UPDATE_MODEL_EVENT, CHANGE_EVENT]) const emit = defineEmits([UPDATE_MODEL_EVENT, CHANGE_EVENT])
const vModel = useVModel(props, 'modelValue', emit) const vModel = defineModel({
type: String,
default: ''
})
const { formItem } = useFormItem() const { formItem } = useFormItem()
@@ -91,76 +89,76 @@ const selectIcon = icon => {
type="primary" type="primary"
:disabled="disabled||readonly" :disabled="disabled||readonly"
size="small" size="small"
@click="iconSelectVisible = true" @click.prevent="iconSelectVisible = true"
> >
{{ $t('common.label.select') }} {{ $t('common.label.select') }}
</el-button> </el-button>
</label> <el-button
<el-button v-if="clearable&&vModel"
v-if="clearable&&vModel" type="danger"
type="danger" :disabled="disabled||readonly"
:disabled="disabled||readonly" size="small"
size="small" @click.prevent="selectIcon()"
@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-header height="40px"> {{ $t('common.label.clear') }}
<el-form label-width="120px"> </el-button>
<el-form-item :label="$t('common.label.keywords')"> <el-dialog
<el-input v-model="iconSelectVisible"
v-model="keyWords" :width="dialogWidth"
:placeholder="$t('common.msg.inputKeywords')" v-bind="dialogAttrs"
/> draggable
</el-form-item> class="icon-dialog"
</el-form> :title="$t('common.msg.pleaseSelectIcon')"
</el-header> >
<el-main class="icon-area"> <el-container
<recycle-scroller style="overflow: auto;"
v-slot="{ item }" :style="{ height: dialogHeight }"
class="scroller icon-list" class="icon-container"
:items="filterIcons" >
:item-size="80" <el-header height="40px">
key-field="id" <el-form label-width="120px">
> <el-form-item :label="$t('common.label.keywords')">
<el-row> <el-input
<el-col v-model="keyWords"
v-for="icon in item.icons" :placeholder="$t('common.msg.inputKeywords')"
:key="icon" />
:span="24/colSize" </el-form-item>
class="text-center" </el-form>
> </el-header>
<a <el-main class="icon-area">
class="el-button el-button--large is-text icon-a" <recycle-scroller
@click="selectIcon(icon)" 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> <a
<common-icon class="el-button el-button--large is-text icon-a"
size="20" @click.prevent="selectIcon(icon)"
:icon="icon" >
/><br> <div>
<span class="icon-text">{{ icon }}</span> <common-icon
</div> size="20"
</a> :icon="icon"
</el-col> /><br>
</el-row> <span class="icon-text">{{ icon }}</span>
</recycle-scroller> </div>
</el-main> </a>
</el-container> </el-col>
</el-dialog> </el-row>
</recycle-scroller>
</el-main>
</el-container>
</el-dialog>
</label>
</template> </template>
<style scoped> <style scoped>

View File

@@ -6,7 +6,7 @@ import kebabCase from 'lodash/kebabCase'
const props = defineProps({ const props = defineProps({
icon: { icon: {
type: String, type: String,
required: false default: ''
} }
}) })
const calcIcon = computed(() => { const calcIcon = computed(() => {

View File

@@ -1,7 +1,10 @@
<script setup> <script setup>
import { computed } from 'vue' import { computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useGlobalConfigStore } from '@/stores/GlobalConfigStore'
const router = useRouter() const router = useRouter()
const globalConfigStore = useGlobalConfigStore()
/** /**
* @type {CommonMenuItemProps} * @type {CommonMenuItemProps}
*/ */
@@ -15,7 +18,7 @@ const props = defineProps({
}, },
index: { index: {
type: [Number, String], type: [Number, String],
required: false default: ''
} }
}) })
const isSubMenu = computed(() => { const isSubMenu = computed(() => {
@@ -35,7 +38,7 @@ const menuCls = computed(() => {
}) })
const dropdownClick = (menuItem, $event) => { const dropdownClick = (menuItem, $event) => {
if (menuItem.click) { if (menuItem.click) {
menuItem.click($event) menuItem.click($event, menuItem)
} else { } else {
const route = menuItem.route || menuItem.index const route = menuItem.route || menuItem.index
if (route) { 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> </script>
<template> <template>
<div <div
v-if="menuItem.isSplit" v-if="menuItem.isSplit"
:key="menuItem.index||index"
:class="menuCls" :class="menuCls"
> >
<slot name="split" /> <slot name="split" />
</div> </div>
<el-sub-menu <el-sub-menu
v-else-if="isSubMenu" v-else-if="isSubMenu"
:key="menuItem.index||index"
:index="`${menuItem.index||index}`" :index="`${menuItem.index||index}`"
:class="menuCls" :class="menuCls"
:disabled="menuItem.disabled"
v-bind="menuItem.attrs" v-bind="menuItem.attrs"
> >
<template #title> <template #title>
<common-icon <common-icon
v-if="showMenuIcon"
:size="menuItem.iconSize" :size="menuItem.iconSize"
:icon="menuItem.icon" :icon="menuItem.icon"
/> />
<span v-if="menuItem.labelKey||menuItem.label"> <span v-if="menuItem.labelKey||menuItem.label">
{{ menuItem.labelKey?$t(menuItem.labelKey):menuItem.label }} {{ menuItem.labelKey?$t(menuItem.labelKey):menuItem.label }}
</span> </span>
<div
v-if="menuItem.html"
v-html="menuItem.html"
/>
</template> </template>
<common-menu-item <common-menu-item
v-for="(childMenu, childIdx) in menuItem.children" v-for="(childMenu, childIdx) in menuItem.children"
@@ -83,13 +90,14 @@ const dropdownClick = (menuItem, $event) => {
</el-sub-menu> </el-sub-menu>
<el-menu-item <el-menu-item
v-else-if="isDropdown" v-else-if="isDropdown"
:key="menuItem.index||index"
:class="menuCls" :class="menuCls"
@click="menuItem.click&&menuItem.click($event)" :disabled="menuItem.disabled"
@click="menuItem.click&&menuItem.click($event, menuItem)"
> >
<el-dropdown class="common-dropdown"> <el-dropdown class="common-dropdown">
<span class="el-dropdown-link"> <span class="el-dropdown-link">
<common-icon <common-icon
v-if="showMenuIcon"
:size="menuItem.iconSize" :size="menuItem.iconSize"
:icon="menuItem.icon" :icon="menuItem.icon"
/> />
@@ -104,6 +112,7 @@ const dropdownClick = (menuItem, $event) => {
@click="dropdownClick(childMenu, $event)" @click="dropdownClick(childMenu, $event)"
> >
<common-icon <common-icon
v-if="checkShowMenuIcon(childMenu)"
:size="childMenu.iconSize" :size="childMenu.iconSize"
:icon="childMenu.icon" :icon="childMenu.icon"
/> />
@@ -117,12 +126,14 @@ const dropdownClick = (menuItem, $event) => {
<el-menu-item <el-menu-item
v-else v-else
:class="menuCls" :class="menuCls"
:disabled="menuItem.disabled"
:route="menuItem.route" :route="menuItem.route"
v-bind="menuItem.attrs" v-bind="menuItem.attrs"
:index="menuItem.index" :index="menuItem.index"
@click="menuItem.click&&menuItem.click(menuItem, $event)" @click="menuItem.click&&menuItem.click($event,menuItem)"
> >
<common-icon <common-icon
v-if="showMenuIcon"
:size="menuItem.iconSize" :size="menuItem.iconSize"
:icon="menuItem.icon" :icon="menuItem.icon"
/> />
@@ -130,10 +141,6 @@ const dropdownClick = (menuItem, $event) => {
<span v-if="menuItem.labelKey||menuItem.label"> <span v-if="menuItem.labelKey||menuItem.label">
{{ menuItem.labelKey?$t(menuItem.labelKey):menuItem.label }} {{ menuItem.labelKey?$t(menuItem.labelKey):menuItem.label }}
</span> </span>
<div
v-if="menuItem.html"
v-html="menuItem.html"
/>
</template> </template>
</el-menu-item> </el-menu-item>
</template> </template>
@@ -142,7 +149,4 @@ const dropdownClick = (menuItem, $event) => {
.common-dropdown { .common-dropdown {
height: 100%; height: 100%;
} }
.common-dropdown .el-icon {
margin-top: 20px;
}
</style> </style>

View File

@@ -1,21 +1,28 @@
<script setup> <script setup>
import { computed } from 'vue' import { computed, ref, watchEffect } from 'vue'
import { filterMenus, useParentRoute } from '@/components/utils' import { processMenus, useParentRoute } from '@/components/utils'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
const props = defineProps({ const props = defineProps({
menus: { menus: {
type: Array, type: Array,
required: true required: true
},
defaultActivePath: {
type: String,
default: ''
} }
}) })
const menuItems = computed(() => { const route = useRoute()
return filterMenus(props.menus) const menuItems = ref([])
watchEffect(() => {
menuItems.value = processMenus(props.menus)
}) })
const activeRoutePath = computed(() => { const activeRoutePath = computed(() => {
let route = useRoute() if (props.defaultActivePath) {
route = useParentRoute(route) return props.defaultActivePath
return route && route.path !== '/' ? route.path : '' }
const current = useParentRoute(route)
return current && current.path !== '/' ? current.path : ''
}) })
</script> </script>

View File

@@ -8,6 +8,14 @@ export interface CommonMenuItem {
isDropdown?: boolean; isDropdown?: boolean;
/** 是否是分割元素 */ /** 是否是分割元素 */
isSplit?: boolean; isSplit?: boolean;
/**
* 是否禁用,禁用状态仍然是显示的
*/
disabled?: boolean;
/**
* 是否启用默认true设置false不显示
*/
enabled?: boolean;
/** 自定义样式 */ /** 自定义样式 */
menuCls?: string; menuCls?: string;
/** 路由地址 */ /** 路由地址 */

View 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
View File

@@ -0,0 +1,17 @@
export interface SortOption {
prop: string; // 排序字段如price该值会作为
labelKey?: string;// 国际化资源key首选该属性不存在才使用label
label?: string;
showIcon?: boolean; // 控制某些排序不显示图标
fixedValue?: 'ASC' | 'DESC' // 固定排序模式
}
/**
* 排序结果
* key是排序字段来自SortOption的propvalue是排序方式
* 'ASC':升序 'DESC':降序 '':不生效
* 如 { "deptTime": "DESC", "price": "", "duration": "" }
*/
export interface SortProps {
[key: string]: 'ASC' | 'DESC' | ''
}

View 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>

View 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>

View File

@@ -1,6 +1,6 @@
<script setup> <script setup>
import { formatDate } from '@/components/utils' import { formatDate, toLabelByKey } from '@/components/utils'
import { computed } from 'vue' import { computed, useSlots } from 'vue'
import { get } from 'lodash' import { get } from 'lodash'
/** /**
@@ -36,22 +36,24 @@ const formatter = computed(() => {
}) })
const getPropertyData = (row) => { const getPropertyData = (row) => {
return get(row, props.column.property) return get(row, props.column.prop || props.column.property)
} }
const slots = useSlots()
</script> </script>
<template> <template>
<el-table-column <el-table-column
v-if="!column.isOperation" v-if="!column.isOperation"
:label="column.label || $t(column.labelKey)" :label="column.label || toLabelByKey(column.labelKey)"
:prop="column.prop||column.property" :prop="column.prop||column.property"
:width="column.width" :width="column.width"
v-bind="column.attrs" v-bind="column.attrs"
:formatter="formatter" :formatter="formatter"
> >
<template <template
v-if="column.slot||column.click" v-if="column.click"
#default="scope" #default="scope"
> >
<el-link <el-link
@@ -62,16 +64,22 @@ const getPropertyData = (row) => {
> >
{{ formatter?formatter(scope.row, scope):getPropertyData(scope.row) }} {{ formatter?formatter(scope.row, scope):getPropertyData(scope.row) }}
</el-link> </el-link>
</template>
<template
v-for="(slot, slotKey) in slots"
:key="slotKey"
#[slotKey]="scope"
>
<slot <slot
:name="slotKey"
v-bind="scope" v-bind="scope"
:column-conf="column" :column-conf="column"
name="default"
/> />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
v-if="column.isOperation" v-if="column.isOperation"
:label="column.label || $t(column.labelKey)" :label="column.label || toLabelByKey(column.labelKey)"
:width="column.width" :width="column.width"
v-bind="column.attrs" v-bind="column.attrs"
> >
@@ -80,7 +88,7 @@ const getPropertyData = (row) => {
> >
<template v-for="(button, index) in column.buttons"> <template v-for="(button, index) in column.buttons">
<el-button <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" :key="index"
:type="button.type" :type="button.type"
:icon="button.icon" :icon="button.icon"
@@ -90,7 +98,7 @@ const getPropertyData = (row) => {
:circle="button.circle" :circle="button.circle"
@click="button.click&&button.click(scope.row, scope)" @click="button.click&&button.click(scope.row, scope)"
> >
{{ button.label || $t(button.labelKey) }} {{ button.label || toLabelByKey(button.labelKey) }}
</el-button> </el-button>
</template> </template>
<slot <slot

View File

@@ -1,6 +1,12 @@
<script setup> <script setup>
import CommonTableColumn from '@/components/common-table/common-table-column.vue' 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 * @type CommonTableProps
@@ -11,7 +17,7 @@ const props = defineProps({
*/ */
columns: { columns: {
type: Array, type: Array,
required: true default: () => []
}, },
/** /**
* 显示数据 * 显示数据
@@ -36,7 +42,7 @@ const props = defineProps({
}, },
/** /**
* el-button * el-button
* @type [ButtonProps] * @type [TableButtonProps]
*/ */
buttons: { buttons: {
type: Array, type: Array,
@@ -84,6 +90,26 @@ const props = defineProps({
loadingText: { loadingText: {
type: String, type: String,
default: '' 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(() => { const calcColumns = computed(() => {
let _columns = props.columns 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) { if (props.buttons.length || props.buttonsSlot) {
const buttonColumn = { const buttonColumn = {
labelKey: 'common.label.operation', labelKey: 'common.label.operation',
@@ -116,6 +151,74 @@ const currentPageChange = (pageNumber) => {
emit('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() const table = ref()
defineExpose({ defineExpose({
@@ -135,7 +238,8 @@ defineExpose({
v-bind="$attrs" v-bind="$attrs"
:highlight-current-row="highlightCurrentRow" :highlight-current-row="highlightCurrentRow"
:stripe="stripe" :stripe="stripe"
:data="data" :data="calcData"
:class="{'common-hide-expand': hideExpandBtn}"
:border="border" :border="border"
> >
<common-table-column <common-table-column
@@ -144,23 +248,50 @@ defineExpose({
:column="column" :column="column"
:button-size="buttonSize" :button-size="buttonSize"
> >
<template
v-if="column.headerSlot"
#header="scope"
>
<slot
v-bind="scope"
:name="column.headerSlot"
/>
</template>
<!--用于自定义显示属性--> <!--用于自定义显示属性-->
<template <template
v-if="column.slot"
#default="scope" #default="scope"
> >
<slot <slot
v-if="column.slot" v-bind="scope"
:row="scope.row"
:column="scope.column"
:item="scope.row" :item="scope.row"
:column-conf="scope.columnConf"
:name="column.slot" :name="column.slot"
/> />
</template> </template>
</common-table-column> </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-table>
<el-pagination <el-pagination
v-if="page&&page.pageCount" v-if="!infinitePaging&&!frontendPaging&&page&&page.pageCount"
class="common-pagination" class="common-pagination"
v-bind="pageAttrs" v-bind="pageAttrs"
:total="page.totalCount" :total="page.totalCount"
@@ -169,6 +300,16 @@ defineExpose({
@size-change="pageSizeChange($event)" @size-change="pageSizeChange($event)"
@current-change="currentPageChange($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> </el-container>
</template> </template>

View File

@@ -1,34 +1,52 @@
import { ButtonProps, LinkProps, TableProps, PaginationProps } from 'element-plus' 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 { 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 { export interface CommonTableColumn {
// 表格头 /** 是否显示 */
label: string; enabled?: boolean;
// 表格头国际化key /** 表格头 */
labelKey: string; label?: string;
// 属性名 /** 表格头国际化key */
property: string; labelKey?: string;
// 属性名同property /** 属性名 */
prop: string; property?: string;
// 宽度 /** 属性名同property */
width: string; prop?: string;
// 是否是可操作列 /** 宽度 */
isOperation: boolean; width?: string;
// 自定义插槽名称,用于自定义显示数据 /** 是否是可操作列 */
slot: string; isOperation?: boolean;
// 自定义按钮 /** 自定义插槽名称,用于自定义显示数据 */
buttons: Array<ButtonProps> slot?: string;
// 可选属性 /** 自定义插槽名称用于自定义显示Label */
attrs: any; headerSlot?: string;
// 链接可选属性 /** 自定义按钮 */
linkAttrs: LinkProps; buttons?: Array<TableButtonProps>
// 点击事件 /** 可选属性 */
click: (data: any) => any; attrs?: TableColumnProps;
// 格式化函数 /** 链接可选属性 */
formatter: (data: any, scope: any) => string; 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; border: boolean;
/** 自定义按钮配置 */ /** 自定义按钮配置 */
buttons?: Array<ButtonProps>; buttons?: Array<TableButtonProps>;
/** buttons插槽 */ /** buttons插槽 */
buttonsSlot?: string; buttonsSlot?: string;
/** 默认的按钮大小 */ /** 默认的按钮大小 */
buttonSize?: string; buttonSize?: string;
/** 按钮列配置 */ /** 按钮列配置 */
buttonsColumnAttrs?: { buttonsColumnAttrs?: TableColumnProps;
[key: string]: any
};
/** 分页配置 */ /** 分页配置 */
page?: CommonPage; page?: CommonPage;
/** 分页对齐 */ /** 分页对齐 */
@@ -65,6 +81,16 @@ export interface CommonTableProps extends TableProps<any> {
loading?: boolean; loading?: boolean;
/** loading显示消息 */ /** loading显示消息 */
loadingText?: string; loadingText?: string;
/** 可以展开的table */
expandTable: boolean;
/** 是否隐藏展开按钮 */
hideExpandBtn: boolean;
/** 是否是前台分页**/
frontendPaging: boolean;
/** 前端模式分页数量 **/
frontendPageSize: number;
/** 是否是无限加载分页**/
infinitePaging: boolean;
} }
export interface CommonTableColumnProps { export interface CommonTableColumnProps {

View File

@@ -1,6 +1,6 @@
<script setup> <script setup>
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import { computed } from 'vue' import { computed, ref, provide } from 'vue'
import { UPDATE_MODEL_EVENT } from 'element-plus' import { UPDATE_MODEL_EVENT } from 'element-plus'
const props = defineProps({ const props = defineProps({
@@ -8,10 +8,6 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false default: false
}, },
draggable: {
type: Boolean,
default: true
},
title: { title: {
type: String, type: String,
default: '' default: ''
@@ -24,6 +20,10 @@ const props = defineProps({
type: String, type: String,
default: '800px' default: '800px'
}, },
defaultCls: {
type: [String, Object],
default: ''
},
buttons: { buttons: {
type: Array, type: Array,
default: () => [] default: () => []
@@ -63,19 +63,44 @@ const props = defineProps({
closeClick: { closeClick: {
type: Function, type: Function,
default: null 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 emit = defineEmits([UPDATE_MODEL_EVENT])
const showDialog = useVModel(props, 'modelValue', emit) // 自动响应v-model const showDialog = useVModel(props, 'modelValue', emit) // 自动响应v-model
const windowForm = ref(null) // 如果common-window下面有common-form注册到这里
provide('commonWindowForm', windowForm)
const okButtonClick = $event => { const okButtonClick = $event => {
if (!props.okClick || props.okClick($event) !== false) { if (!props.okClick || props.okClick({ $event, form: windowForm.value }) !== false) {
showDialog.value = false showDialog.value = false
} }
} }
const cancelButtonClick = $event => { const cancelButtonClick = $event => {
if (!props.cancelClick || props.cancelClick($event) !== false) { if (!props.cancelClick || props.cancelClick({ $event, form: windowForm.value }) !== false) {
showDialog.value = false showDialog.value = false
} }
} }
@@ -85,7 +110,7 @@ const calcBeforeClose = computed(() => {
return props.beforeClose return props.beforeClose
} else if (props.closeClick) { } else if (props.closeClick) {
return done => { return done => {
if (props.closeClick() !== false) { if (props.closeClick({ form: windowForm.value }) !== false) {
done() done()
} }
} }
@@ -102,9 +127,14 @@ const calcBeforeClose = computed(() => {
:before-close="calcBeforeClose" :before-close="calcBeforeClose"
:width="width" :width="width"
:draggable="draggable" :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 <el-container
:class="defaultCls"
:style="{ height:height }" :style="{ height:height }"
> >
<slot <slot
@@ -135,7 +165,7 @@ const calcBeforeClose = computed(() => {
:disabled="button.disabled" :disabled="button.disabled"
:round="button.round" :round="button.round"
:circle="button.circle" :circle="button.circle"
@click="button.click&&button.click($event)" @click="button.click&&button.click({$event, form:windowForm})"
> >
{{ button.label || $t(button.labelKey) }} {{ button.label || $t(button.labelKey) }}
</el-button> </el-button>

View 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>

View 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' })

View 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)
}
}

View File

@@ -2,6 +2,8 @@ import CommonIcon from '@/components/common-icon/index.vue'
import CommonIconSelect from '@/components/common-icon-select/index.vue' import CommonIconSelect from '@/components/common-icon-select/index.vue'
import CommonForm from '@/components/common-form/index.vue' import CommonForm from '@/components/common-form/index.vue'
import CommonFormControl from '@/components/common-form-control/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 CommonFormLabel from '@/components/common-form-control/common-form-label.vue'
import CommonMenu from '@/components/common-menu/index.vue' import CommonMenu from '@/components/common-menu/index.vue'
import CommonMenuItem from '@/components/common-menu-item/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 CommonBreadcrumb from '@/components/common-breadcrumb/index.vue'
import CommonWindow from '@/components/common-window/index.vue' import CommonWindow from '@/components/common-window/index.vue'
import CommonAutocomplete from '@/components/common-autocomplete/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('CommonIconSelect', CommonIconSelect)
Vue.component('CommonForm', CommonForm) Vue.component('CommonForm', CommonForm)
Vue.component('CommonFormControl', CommonFormControl) Vue.component('CommonFormControl', CommonFormControl)
Vue.component('CommonFilterControl', CommonFilterControl)
Vue.component('CommonTabFilter', CommonTabFilter)
Vue.component('CommonFormLabel', CommonFormLabel) Vue.component('CommonFormLabel', CommonFormLabel)
Vue.component('CommonMenu', CommonMenu) Vue.component('CommonMenu', CommonMenu)
Vue.component('CommonMenuItem', CommonMenuItem) Vue.component('CommonMenuItem', CommonMenuItem)
@@ -33,5 +39,7 @@ export default {
Vue.component('CommonBreadcrumb', CommonBreadcrumb) Vue.component('CommonBreadcrumb', CommonBreadcrumb)
Vue.component('CommonWindow', CommonWindow) Vue.component('CommonWindow', CommonWindow)
Vue.component('CommonAutocomplete', CommonAutocomplete) Vue.component('CommonAutocomplete', CommonAutocomplete)
Vue.component('CommonSort', CommonSort)
Vue.use(CommonDirectives)
} }
} }

View File

@@ -1,7 +1,21 @@
import { ref } from 'vue' import { ref } from 'vue'
import { $i18nBundle } from '@/messages' import { $i18nBundle, $i18nKey } from '@/messages'
import { isArray, isObject } from 'lodash'
import dayjs from 'dayjs' 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 => { const calcWithIf = menuItem => {
['icon', 'labelKey', 'label', 'html'].forEach(key => { ['icon', 'labelKey', 'label', 'html'].forEach(key => {
const keyIf = menuItem[`${key}If`] const keyIf = menuItem[`${key}If`]
@@ -11,6 +25,10 @@ const calcWithIf = menuItem => {
}) })
} }
/**
* @param {CommonFormOption} option
* @return {string}
*/
export const useInputType = (option) => { export const useInputType = (option) => {
const inType = option.type || 'input' const inType = option.type || 'input'
if (inType.startsWith('common-') || inType.startsWith('el-')) { 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 => { export const useMenuName = item => {
const menuInfo = useMenuInfo(item) const menuInfo = useMenuInfo(item)
if (menuInfo) { if (menuInfo) {
@@ -35,23 +62,50 @@ export const useMenuName = item => {
return menuInfo.label return menuInfo.label
} }
if (menuInfo.labelKey) { if (menuInfo.labelKey) {
return $i18nBundle(menuInfo.labelKey) return toLabelByKey(menuInfo.labelKey)
} }
} }
if (item.meta && item.meta.labelKey) { if (item.meta && item.meta.labelKey) {
return $i18nBundle(item.meta.labelKey) return toLabelByKey(item.meta.labelKey)
} }
return item.name || 'No Name' 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 => { .map(menu => {
calcWithIf(menu) calcWithIf(menu)
if (menu.index) { // 把菜单存储下来,后面需要使用名字 if (menu.index) { // 把菜单存储下来,后面需要使用名字
MENU_INFO_LIST.value[menu.index] = menu MENU_INFO_LIST.value[menu.index] = menu
} }
if (menu.children && menu.children.length) { 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 return menu
}) })
@@ -62,7 +116,7 @@ export const filterMenus = menus => menus.filter(menu => !menu.disabled)
* @param route {RouteRecordMultipleViewsWithChildren} 路由信息 * @param route {RouteRecordMultipleViewsWithChildren} 路由信息
*/ */
export const useParentRoute = function (route) { export const useParentRoute = function (route) {
const parentName = route.meta?.replaceTabHistory const parentName = route?.meta?.replaceTabHistory
if (parentName) { if (parentName) {
const routes = route.matched || [] const routes = route.matched || []
for (let i = routes.length - 1; i > 0; i--) { for (let i = routes.length - 1; i > 0; i--) {
@@ -75,6 +129,48 @@ export const useParentRoute = function (route) {
return 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) => { export const formatDate = (date, format) => {
if (date) { if (date) {
return dayjs(date).format(format || 'YYYY-MM-DD HH:mm:ss') 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') return dayjs(date).format(format || 'YYYY-MM-DD')
} }
} }

View File

@@ -35,11 +35,15 @@ export const changeMessages = locale => {
export const $changeLocale = locale => { export const $changeLocale = locale => {
useGlobalConfigStore().changeLocale(locale) useGlobalConfigStore().changeLocale(locale)
} }
export const $isLocale = locale => {
return useGlobalConfigStore().currentLocale === locale
}
/** /**
* @param cn * @param cn 中文字段
* @param en * @param en 英文字段
* @param {boolean} replaceEmpty 为空是否用不为空的数据代替 * @param {boolean} replaceEmpty 为空是否用不为空的数据代替
* @returns {*} * @returns {String}
*/ */
export const $i18nMsg = (cn, en, replaceEmpty = true) => { export const $i18nMsg = (cn, en, replaceEmpty = true) => {
const { currentLocale } = useGlobalConfigStore() const { currentLocale } = useGlobalConfigStore()
@@ -48,16 +52,50 @@ export const $i18nMsg = (cn, en, replaceEmpty = true) => {
} }
return replaceEmpty ? (en || cn) : en return replaceEmpty ? (en || cn) : en
} }
/**
* @param {String} key 国际化资源key
* @param {String[]=} params 可选参数
* @returns {string}
*/
export const $i18nBundle = i18n.global.t 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 { export default {
install (app) { install (app) {
app.use(i18n) app.use(i18n)
Object.assign(app.config.globalProperties, { Object.assign(app.config.globalProperties, {
$changeLocale, $changeLocale,
$i18nMsg, $i18nMsg,
$i18nBundle $i18nKey,
$i18nBundle,
$isLocale,
$i18nByLocale
}) })
} }
} }

View File

@@ -1,10 +1,28 @@
import { createPinia } from 'pinia' 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 { export default {
install (app) { install (app) {
const pinia = createPinia() 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) app.use(pinia)
return pinia return pinia
} }

View File

@@ -3,6 +3,7 @@ import { computed, ref } from 'vue'
import { useCityAutocompleteConfig, useCitySelectPageConfig } from '@/services/city/CityService' import { useCityAutocompleteConfig, useCitySelectPageConfig } from '@/services/city/CityService'
import { $i18nMsg } from '@/messages' import { $i18nMsg } from '@/messages'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { defineFormOptions } from '@/components/utils'
const defaultCity = ref({}) const defaultCity = ref({})
@@ -14,11 +15,53 @@ setTimeout(() => {
} }
}, 1000) }, 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]} * @type {[CommonFormOption]}
*/ */
const formOptions = computed(() => { const formOptions = computed(() => {
return [{ return defineFormOptions([{
label: '用户名', label: '用户名',
prop: 'userName', prop: 'userName',
value: '', value: '',
@@ -52,22 +95,14 @@ const formOptions = computed(() => {
label: '兴趣爱好', label: '兴趣爱好',
type: 'checkbox-group', type: 'checkbox-group',
prop: 'hobby', prop: 'hobby',
value: '',
required: true, required: true,
children: [ children: getHobbyOptions()
{ }, {
label: '编程', label: '兴趣爱好',
value: 'program' type: 'checkbox-group',
}, prop: 'hobby',
{ required: true,
label: '吃饭', children: getHobbyOptions('checkbox-button')
value: 'eat'
},
{
label: '睡觉',
value: 'sleep'
}
]
}, { }, {
label: '职业', label: '职业',
type: 'select', type: 'select',
@@ -94,20 +129,14 @@ const formOptions = computed(() => {
prop: 'gender', prop: 'gender',
value: '', value: '',
required: true, required: true,
children: [ children: getGenderOptions()
{ }, {
label: '', label: '性别',
value: 'male' type: 'radio-group',
}, prop: 'gender',
{ value: '',
label: '女', required: true,
value: 'female' children: getGenderOptions('radio-button')
},
{
label: '保密',
value: 'unknown'
}
]
}, { }, {
label: '图标', label: '图标',
prop: 'icon', prop: 'icon',
@@ -124,6 +153,9 @@ const formOptions = computed(() => {
change: (city) => { change: (city) => {
defaultCity.value = city defaultCity.value = city
}, },
getAutocompleteLabel: () => {
return $i18nMsg(defaultCity.value?.nameCN, defaultCity.value?.nameEN)
},
attrs: { attrs: {
defaultLabel: $i18nMsg(defaultCity.value?.nameCn, defaultCity.value?.nameEn), defaultLabel: $i18nMsg(defaultCity.value?.nameCn, defaultCity.value?.nameEn),
autocompleteConfig: useCityAutocompleteConfig(), autocompleteConfig: useCityAutocompleteConfig(),
@@ -146,9 +178,11 @@ const formOptions = computed(() => {
prop: 'address', prop: 'address',
value: '', value: '',
attrs: { attrs: {
type: 'textarea' type: 'textarea',
maxlength: 100,
showWordLimit: true
} }
}] }])
}) })
const userDto = ref({ const userDto = ref({
contacts: [{ contacts: [{
@@ -195,7 +229,7 @@ const submitForm = (form) => {
</script> </script>
<template> <template>
<div> <el-container class="flex-column container-center">
<common-form <common-form
:model="userDto" :model="userDto"
:options="formOptions" :options="formOptions"
@@ -246,7 +280,7 @@ const submitForm = (form) => {
<div> <div>
{{ userDto }} {{ userDto }}
</div> </div>
</div> </el-container>
</template> </template>
<style scoped> <style scoped>

View File

@@ -13,8 +13,7 @@ const tableData = ref([])
const loading = ref(true) const loading = ref(true)
const loadUsers = async () => { const loadUsers = async () => {
loading.value = true loading.value = true
const usersResult = await loadUsersResult({ page: page.value }) const usersResult = await loadUsersResult({ page: page.value }).finally(() => (loading.value = false))
loading.value = false
if (usersResult.success && usersResult.resultData) { if (usersResult.success && usersResult.resultData) {
const resultData = usersResult.resultData const resultData = usersResult.resultData
tableData.value = resultData.userList tableData.value = resultData.userList

View File

@@ -2,10 +2,16 @@ import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import { viteMockServe } from 'vite-plugin-mock'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [vue()], plugins: [
vue(),
viteMockServe({
mockPath: './mock'
})
],
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)) '@': fileURLToPath(new URL('./src', import.meta.url))