提交 74360564 作者: 方治民
......@@ -29,4 +29,5 @@ pnpm-debug.log*
*.sln
*.sw?
.mocks/
.mocks
.history
......@@ -3,4 +3,6 @@
# shellcheck source=./_/husky.sh
. "$(dirname "$0")/_/husky.sh"
PATH="/usr/local/bin:$PATH"
npx --no-install commitlint --edit "$1"
......@@ -4,5 +4,7 @@
[ -n "$CI" ] && exit 0
PATH="/usr/local/bin:$PATH"
# Format and submit code according to lintstagedrc.js configuration
npm run lint:lint-staged
{
"recommendations": [
"Vue.volar",
"vue.volar",
"dbaeumer.vscode-eslint",
"stylelint.vscode-stylelint",
"esbenp.prettier-vscode",
......
......@@ -33,6 +33,7 @@
},
"files.exclude": {
"**/.cache": true,
"**/.editorconfig": true,
"**/.eslintcache": true,
"**/bower_components": true,
"**/.idea": true,
......@@ -135,11 +136,7 @@
"pnpm",
"antd"
],
"vue-i18n.i18nPaths": "src\\locales,src\\locales\\lang,public\\resource\\tinymce\\langs",
"[properties]": {
"editor.defaultFormatter": "foxundermoon.shell-format"
},
"[nginx]": {
"editor.defaultFormatter": "teclado.vscode-nginx-format"
}
"vetur.format.scriptInitialIndent": true,
"vetur.format.styleInitialIndent": true,
"vetur.validation.script": false
}
......@@ -21,13 +21,15 @@ function createConfig(params: CreateConfigParams) {
try {
const windowConf = `window.${configName}`
// Ensure that the variable will not be modified
const configStr = `${windowConf}=${JSON.stringify(config)};
Object.freeze(${windowConf});
Object.defineProperty(window, "${configName}", {
configurable: false,
writable: false,
});
`.replace(/\s/g, '')
let configStr = `${windowConf}=${JSON.stringify(config)};`
configStr += `
Object.freeze(${windowConf});
Object.defineProperty(window, "${configName}", {
configurable: false,
writable: false,
});
`.replace(/\s/g, '')
fs.mkdirp(getRootPath(OUTPUT_DIR))
writeFileSync(getRootPath(`${OUTPUT_DIR}/${configFileName}`), configStr)
......
......@@ -5,9 +5,9 @@
import { createStyleImportPlugin } from 'vite-plugin-style-import'
export function configStyleImportPlugin(_isBuild: boolean) {
// if (!isBuild) {
// return [];
// }
if (!_isBuild) {
return []
}
const styleImportPlugin = createStyleImportPlugin({
libs: [
{
......@@ -64,6 +64,8 @@ export function configStyleImportPlugin(_isBuild: boolean) {
'layout-footer': 'layout',
'layout-header': 'layout',
'month-picker': 'date-picker',
'range-picker': 'date-picker',
'image-preview-group': 'image',
}
return ignoreList.includes(name)
......
const fs = require('fs')
const path = require('path')
const { execSync } = require('child_process')
const scopes = fs
.readdirSync(path.resolve(__dirname, 'src'), { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name.replace(/s$/, ''))
// precomputed scope
const scopeComplete = execSync('git status --porcelain || true')
.toString()
.trim()
.split('\n')
.find((r) => ~r.indexOf('M src'))
?.replace(/(\/)/g, '%%')
?.match(/src%%((\w|-)*)/)?.[1]
?.replace(/s$/, '')
/** @type {import('cz-git').UserConfig} */
module.exports = {
ignores: [(commit) => commit.includes('init')],
extends: ['@commitlint/config-conventional'],
......@@ -30,4 +50,58 @@ module.exports = {
],
],
},
prompt: {
/** @use `yarn commit :f` */
alias: {
f: 'docs: fix typos',
r: 'docs: update README',
s: 'style: update code format',
b: 'build: bump dependencies',
c: 'chore: update config',
},
customScopesAlign: !scopeComplete ? 'top' : 'bottom',
defaultScope: scopeComplete,
scopes: [...scopes, 'mock'],
allowEmptyIssuePrefixs: false,
allowCustomIssuePrefixs: false,
// English
typesAppend: [
{ value: 'wip', name: 'wip: work in process' },
{ value: 'workflow', name: 'workflow: workflow improvements' },
{ value: 'types', name: 'types: type definition file changes' },
],
// 中英文对照版
// messages: {
// type: '选择你要提交的类型 :',
// scope: '选择一个提交范围 (可选):',
// customScope: '请输入自定义的提交范围 :',
// subject: '填写简短精炼的变更描述 :\n',
// body: '填写更加详细的变更描述 (可选)。使用 "|" 换行 :\n',
// breaking: '列举非兼容性重大的变更 (可选)。使用 "|" 换行 :\n',
// footerPrefixsSelect: '选择关联issue前缀 (可选):',
// customFooterPrefixs: '输入自定义issue前缀 :',
// footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
// confirmCommit: '是否提交或修改commit ?',
// },
// types: [
// { value: 'feat', name: 'feat: 新增功能' },
// { value: 'fix', name: 'fix: 修复缺陷' },
// { value: 'docs', name: 'docs: 文档变更' },
// { value: 'style', name: 'style: 代码格式' },
// { value: 'refactor', name: 'refactor: 代码重构' },
// { value: 'perf', name: 'perf: 性能优化' },
// { value: 'test', name: 'test: 添加疏漏测试或已有测试改动' },
// { value: 'build', name: 'build: 构建流程、外部依赖变更 (如升级 npm 包、修改打包配置等)' },
// { value: 'ci', name: 'ci: 修改 CI 配置、脚本' },
// { value: 'revert', name: 'revert: 回滚 commit' },
// { value: 'chore', name: 'chore: 对构建过程或辅助工具和库的更改 (不影响源文件、测试用例)' },
// { value: 'wip', name: 'wip: 正在开发中' },
// { value: 'workflow', name: 'workflow: 工作流程改进' },
// { value: 'types', name: 'types: 类型定义文件修改' },
// ],
// emptyScopesAlias: 'empty: 不填写',
// customScopesAlias: 'custom: 自定义',
},
}
......@@ -49,7 +49,7 @@
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
"path": "node_modules/cz-git"
}
},
"dependencies": {
......@@ -127,6 +127,8 @@
"conventional-changelog-cli": "^2.2.2",
"cross-env": "^7.0.3",
"cz-conventional-changelog": "^3.3.0",
"cz-git": "^1.3.12",
"czg": "^1.3.12",
"dotenv": "^16.0.3",
"eslint": "^8.26.0",
"eslint-config-prettier": "^8.5.0",
......
......@@ -48,6 +48,8 @@ specifiers:
cross-env: ^7.0.3
crypto-js: ^4.1.1
cz-conventional-changelog: ^3.3.0
cz-git: ^1.3.12
czg: ^1.3.12
dayjs: ^1.11.6
dotenv: ^16.0.3
echarts: ^5.4.0
......@@ -195,6 +197,8 @@ devDependencies:
conventional-changelog-cli: 2.2.2
cross-env: 7.0.3
cz-conventional-changelog: 3.3.0
cz-git: 1.3.12
czg: 1.3.12
dotenv: 16.0.3
eslint: 8.26.0
eslint-config-prettier: 8.5.0_eslint@8.26.0
......@@ -4011,6 +4015,15 @@ packages:
- '@swc/wasm'
dev: true
/cz-git/1.3.12:
resolution: {integrity: sha512-grTgbgjsRpvkYNjm8kmpvHHw/LkkS3SlQOOwgamhRZo7c5qQfts1QZdkJ6RP0K61BcxyH5siMz8qC/MDoW/tBA==}
dev: true
/czg/1.3.12:
resolution: {integrity: sha512-p+Jm5AVatbaMeo2lRyuj5xm9iuxe/A6s9WA/Jfu+g3FWYHkG+StsNXJw1u8i1qqfmbu2hj9rXgF0lTltwHHKSA==}
hasBin: true
dev: true
/d/1.0.1:
resolution: {integrity: sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==}
dependencies:
......
......@@ -4,14 +4,4 @@
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
.tinymce-mobile-unfocused-selections .tinymce-mobile-unfocused-selection{position: absolute;display: inline-block;background-color: green;opacity: .5;}
body{-webkit-text-size-adjust: none;}
body img{max-width: 96vw;}
body table img{max-width: 95%;}
body{font-family: sans-serif;}
table{border-collapse: collapse;}
.tinymce-mobile-unfocused-selections .tinymce-mobile-unfocused-selection{background-color:green;display:inline-block;opacity:.5;position:absolute}body{-webkit-text-size-adjust:none}body img{max-width:96vw}body table img{max-width:95%}body{font-family:sans-serif}table{border-collapse:collapse}
This source diff could not be displayed because it is too large. You can view the blob instead.
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
body.tox-dialog__disable-scroll{overflow:hidden}.tox-fullscreen{border:0;height:100%;margin:0;overflow:hidden;-ms-scroll-chaining:none;overscroll-behavior:none;padding:0;touch-action:pinch-zoom;width:100%}.tox.tox-tinymce.tox-fullscreen .tox-statusbar__resize-handle{display:none}.tox-shadowhost.tox-fullscreen,.tox.tox-tinymce.tox-fullscreen{left:0;position:fixed;top:0;z-index:1200}.tox.tox-tinymce.tox-fullscreen{background-color:transparent}.tox-fullscreen .tox.tox-tinymce-aux,.tox-fullscreen~.tox.tox-tinymce-aux{z-index:1201}
......@@ -4,14 +4,4 @@
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
.tinymce-mobile-unfocused-selections .tinymce-mobile-unfocused-selection{position: absolute;display: inline-block;background-color: green;opacity: .5;}
body{-webkit-text-size-adjust: none;}
body img{max-width: 96vw;}
body table img{max-width: 95%;}
body{font-family: sans-serif;}
table{border-collapse: collapse;}
.tinymce-mobile-unfocused-selections .tinymce-mobile-unfocused-selection{background-color:green;display:inline-block;opacity:.5;position:absolute}body{-webkit-text-size-adjust:none}body img{max-width:96vw}body table img{max-width:95%}body{font-family:sans-serif}table{border-collapse:collapse}
This source diff could not be displayed because it is too large. You can view the blob instead.
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
body.tox-dialog__disable-scroll{overflow:hidden}.tox-fullscreen{border:0;height:100%;margin:0;overflow:hidden;-ms-scroll-chaining:none;overscroll-behavior:none;padding:0;touch-action:pinch-zoom;width:100%}.tox.tox-tinymce.tox-fullscreen .tox-statusbar__resize-handle{display:none}.tox-shadowhost.tox-fullscreen,.tox.tox-tinymce.tox-fullscreen{left:0;position:fixed;top:0;z-index:1200}.tox.tox-tinymce.tox-fullscreen{background-color:transparent}.tox-fullscreen .tox.tox-tinymce-aux,.tox-fullscreen~.tox.tox-tinymce-aux{z-index:1201}
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -10,18 +10,18 @@
<script lang="ts">
import { defineComponent } from 'vue'
import { Button } from 'ant-design-vue'
export default defineComponent({
name: 'AButton',
extends: Button,
inheritAttrs: false,
})
</script>
<script lang="ts" setup>
import { computed, unref } from 'vue'
import { Button } from 'ant-design-vue'
import Icon from '/@/components/Icon/src/Icon.vue'
import { buttonProps } from './props'
import { useAttrs } from '/@/hooks/core/useAttrs'
const props = defineProps(buttonProps)
// get component class
const attrs = useAttrs({ excludeDefaultKeys: false })
......@@ -34,7 +34,6 @@
},
]
})
// get inherit binding value
const getBindValue = computed(() => ({ ...unref(attrs), ...props }))
</script>
const validColors = ['error', 'warning', 'success', ''] as const
type ButtonColorType = typeof validColors[number]
export const buttonProps = {
color: { type: String, validator: (v) => ['error', 'warning', 'success', ''].includes(v) },
color: {
type: String as PropType<ButtonColorType>,
validator: (v) => validColors.includes(v),
default: '',
},
loading: { type: Boolean },
disabled: { type: Boolean },
/**
......
......@@ -25,6 +25,7 @@
<script lang="ts" setup>
import type { PropType } from 'vue'
import { ref } from 'vue'
import { isNil } from 'lodash-es'
// component
import { Skeleton } from 'ant-design-vue'
import { CollapseTransition } from '/@/components/Transition'
......@@ -33,7 +34,6 @@
// hook
import { useTimeoutFn } from '/@/hooks/core/useTimeout'
import { useDesign } from '/@/hooks/web/useDesign'
const props = defineProps({
title: { type: String, default: '' },
loading: { type: Boolean },
......@@ -58,25 +58,24 @@
*/
lazyTime: { type: Number, default: 0 },
})
const show = ref(true)
const { prefixCls } = useDesign('collapse-container')
/**
* @description: Handling development events
*/
function handleExpand() {
show.value = !show.value
function handleExpand(val: boolean) {
show.value = isNil(val) ? !show.value : val
if (props.triggerWindowResize) {
// 200 milliseconds here is because the expansion has animation,
useTimeoutFn(triggerWindowResize, 200)
}
}
defineExpose({
handleExpand,
})
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-collapse-container';
.@{prefix-cls} {
background-color: @component-background;
border-radius: 2px;
......
<script lang="tsx">
import type { ContextMenuItem, ItemContentProps, Axis } from './typing'
import type { FunctionalComponent, CSSProperties } from 'vue'
import type { FunctionalComponent, CSSProperties, PropType } from 'vue'
import { defineComponent, nextTick, onMounted, computed, ref, unref, onUnmounted } from 'vue'
import Icon from '/@/components/Icon'
import { Menu, Divider } from 'ant-design-vue'
const prefixCls = 'context-menu'
const props = {
width: { type: Number, default: 156 },
customEvent: { type: Object as PropType<Event>, default: null },
......@@ -27,7 +25,6 @@
},
},
}
const ItemContent: FunctionalComponent<ItemContentProps> = (props) => {
const { item } = props
return (
......@@ -37,21 +34,18 @@
</span>
)
}
export default defineComponent({
name: 'ContextMenu',
props,
setup(props) {
const wrapRef = ref(null)
const showRef = ref(false)
const getStyle = computed((): CSSProperties => {
const { axis, items, styles, width } = props
const { x, y } = axis || { x: 0, y: 0 }
const menuHeight = (items || []).length * 40
const menuWidth = width
const body = document.body
const left = body.clientWidth < x + menuWidth ? x - menuWidth : x
const top = body.clientHeight < y + menuHeight ? y - menuHeight : y
return {
......@@ -63,16 +57,13 @@
zIndex: 9999,
}
})
onMounted(() => {
nextTick(() => (showRef.value = true))
})
onUnmounted(() => {
const el = unref(wrapRef)
el && document.body.removeChild(el)
})
function handleAction(item: ContextMenuItem, e: MouseEvent) {
const { handler, disabled } = item
if (disabled) {
......@@ -83,17 +74,15 @@
e?.preventDefault()
handler?.()
}
function renderMenuItem(items: ContextMenuItem[]) {
return items.map((item) => {
const visibleItems = items.filter((item) => !item.hidden)
return visibleItems.map((item) => {
const { disabled, label, children, divider = false } = item
const contentProps = {
item,
handler: handleAction,
showIcon: props.showIcon,
}
if (!children || children.length === 0) {
return (
<>
......@@ -105,7 +94,6 @@
)
}
if (!unref(showRef)) return null
return (
<Menu.SubMenu key={label} disabled={disabled} popupClassName={`${prefixCls}__popup`}>
{{
......@@ -134,11 +122,8 @@
</script>
<style lang="less">
@default-height: 42px !important;
@small-height: 36px !important;
@large-height: 36px !important;
.item-style() {
li {
display: inline-block;
......@@ -191,7 +176,6 @@
.ant-divider {
margin: 0;
}
.item-style();
}
......
......@@ -6,6 +6,7 @@ export interface Axis {
export interface ContextMenuItem {
label: string
icon?: string
hidden?: boolean
disabled?: boolean
handler?: Fn
divider?: boolean
......
......@@ -32,7 +32,6 @@
import { useI18n } from '/@/hooks/web/useI18n'
import type { ButtonProps } from '/@/components/Button'
import Icon from '/@/components/Icon'
const props = {
width: { type: [String, Number], default: '200px' },
value: { type: String },
......@@ -41,7 +40,6 @@
btnText: { type: String, default: '' },
uploadApi: { type: Function as PropType<({ file: Blob, name: string }) => Promise<void>> },
}
export default defineComponent({
name: 'CropperAvatar',
components: { CopperModal, Icon },
......@@ -53,38 +51,28 @@
const [register, { openModal, closeModal }] = useModal()
const { createMessage } = useMessage()
const { t } = useI18n()
const getClass = computed(() => [prefixCls])
const getWidth = computed(() => `${props.width}`.replace(/px/, '') + 'px')
const getIconWidth = computed(() => parseInt(`${props.width}`.replace(/px/, '')) / 2 + 'px')
const getStyle = computed((): CSSProperties => ({ width: unref(getWidth) }))
const getImageWrapperStyle = computed(
(): CSSProperties => ({ width: unref(getWidth), height: unref(getWidth) }),
)
watchEffect(() => {
sourceValue.value = props.value || ''
})
watch(
() => sourceValue.value,
(v: string) => {
emit('update:value', v)
},
)
function handleUploadSuccess({ source }) {
function handleUploadSuccess({ source, data }) {
sourceValue.value = source
emit('change', source)
emit('change', { source, data })
createMessage.success(t('component.cropper.uploadSuccess'))
}
expose({ openModal: openModal.bind(null, true), closeModal })
return {
t,
prefixCls,
......@@ -103,7 +91,6 @@
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-cropper-avatar';
.@{prefix-cls} {
display: inline-block;
text-align: center;
......
......@@ -3,7 +3,7 @@
import type { DescriptionsProps } from 'ant-design-vue/es/descriptions/index'
import type { CSSProperties } from 'vue'
import type { CollapseContainerOptions } from '/@/components/Container/index'
import { defineComponent, computed, ref, unref } from 'vue'
import { defineComponent, computed, ref, unref, toRefs } from 'vue'
import { get } from 'lodash-es'
import { Descriptions } from 'ant-design-vue'
import { CollapseContainer } from '/@/components/Container/index'
......@@ -11,7 +11,6 @@
import { isFunction } from '/@/utils/is'
import { getSlot } from '/@/utils/helper/tsxHelper'
import { useAttrs } from '/@/hooks/core/useAttrs'
const props = {
useCollapse: { type: Boolean, default: true },
title: { type: String, default: '' },
......@@ -37,17 +36,14 @@
},
data: { type: Object },
}
export default defineComponent({
name: 'Description',
props,
emits: ['register'],
setup(props, { slots, emit }) {
const propsRef = ref<Partial<DescriptionProps> | null>(null)
const { prefixCls } = useDesign('description')
const attrs = useAttrs()
// Custom title component: get title
const getMergeProps = computed(() => {
return {
......@@ -55,7 +51,6 @@
...(unref(propsRef) as Recordable),
} as DescriptionProps
})
const getProps = computed(() => {
const opt = {
...unref(getMergeProps),
......@@ -63,12 +58,10 @@
}
return opt as DescriptionProps
})
/**
* @description: Whether to setting title
*/
const useWrapper = computed(() => !!unref(getMergeProps).title)
/**
* @description: Get configuration Collapse
*/
......@@ -79,11 +72,9 @@
...unref(getProps).collapseOptions,
}
})
const getDescriptionsProps = computed(() => {
return { ...unref(attrs), ...unref(getProps) } as DescriptionsProps
})
/**
* @description:设置desc
*/
......@@ -91,39 +82,36 @@
// Keep the last setDrawerProps
propsRef.value = { ...(unref(propsRef) as Recordable), ...descProps } as Recordable
}
// Prevent line breaks
function renderLabel({ label, labelMinWidth, labelStyle }: DescItem) {
if (!labelStyle && !labelMinWidth) {
return label
}
const labelStyles: CSSProperties = {
...labelStyle,
minWidth: `${labelMinWidth}px `,
}
return <div style={labelStyles}>{label}</div>
}
function renderItem() {
const { schema, data } = unref(getProps)
return unref(schema)
.map((item) => {
const { render, field, span, show, contentMinWidth } = item
if (show && isFunction(show) && !show(data)) {
return null
}
const getContent = () => {
const _data = unref(getProps)?.data
if (!_data) {
return null
}
const getField = get(_data, field)
if (getField && !toRefs(_data).hasOwnProperty(field)) {
return isFunction(render) ? render('', _data) : ''
}
return isFunction(render) ? render(getField, _data) : getField ?? ''
}
const width = contentMinWidth
return (
<Descriptions.Item label={renderLabel(item)} key={field} span={span}>
......@@ -141,7 +129,6 @@
})
.filter((item) => !!item)
}
const renderDesc = () => {
return (
<Descriptions class={`${prefixCls}`} {...(unref(getDescriptionsProps) as any)}>
......@@ -149,17 +136,14 @@
</Descriptions>
)
}
const renderContainer = () => {
const content = props.useCollapse ? renderDesc() : <div>{renderDesc()}</div>
// Reduce the dom level
if (!props.useCollapse) {
return content
}
const { canExpand, helpMessage } = unref(getCollapseOptions)
const { title } = unref(getMergeProps)
return (
<CollapseContainer title={title} canExpan={canExpand} helpMessage={helpMessage}>
{{
......@@ -169,11 +153,9 @@
</CollapseContainer>
)
}
const methods: DescInstance = {
setDescProps,
}
emit('register', methods)
return () => (unref(useWrapper) ? renderContainer() : renderDesc())
},
......
......@@ -47,7 +47,7 @@
const heightStr = `${props.height}`
return {
height: heightStr,
lineHeight: heightStr,
lineHeight: `calc(${heightStr} - 1px)`,
}
})
......
......@@ -6,6 +6,28 @@ const { utils, writeFile } = xlsx
const DEF_FILE_NAME = 'excel-list.xlsx'
/**
* @param data source data
* @param worksheet worksheet object
* @param min min width
*/
function setColumnWidth(data, worksheet, min = 3) {
const obj = {}
worksheet['!cols'] = []
data.forEach((item) => {
Object.keys(item).forEach((key) => {
const cur = item[key]
const length = cur.length
obj[key] = Math.max(min, length)
})
})
Object.keys(obj).forEach((key) => {
worksheet['!cols'].push({
wch: obj[key],
})
})
}
export function jsonToSheetXlsx<T = any>({
data,
header,
......@@ -20,7 +42,7 @@ export function jsonToSheetXlsx<T = any>({
}
const worksheet = utils.json_to_sheet(arrData, json2sheetOpts)
setColumnWidth(arrData, worksheet)
/* add worksheet to workbook */
const workbook: WorkBook = {
SheetNames: [filename],
......
......@@ -10,7 +10,6 @@
import { defineComponent, ref, unref } from 'vue'
import * as XLSX from 'xlsx'
import { dateUtil } from '/@/utils/dateUtil'
import type { ExcelData } from './typing'
export default defineComponent({
name: 'ImportExcel',
......@@ -25,12 +24,16 @@
type: Number,
default: 8,
},
// 是否直接返回选中文件
isReturnFile: {
type: Boolean,
default: false,
},
},
emits: ['success', 'error'],
setup(props, { emit }) {
const inputRef = ref<HTMLInputElement | null>(null)
const loadingRef = ref<Boolean>(false)
/**
* @description: 第一行作为头部
*/
......@@ -39,7 +42,6 @@
const headers: string[] = []
// A3:B7=>{s:{c:0, r:2}, e:{c:1, r:6}}
const range = XLSX.utils.decode_range(sheet['!ref'])
const R = range.s.r
/* start in the first row */
for (let C = range.s.c; C <= range.e.c; ++C) {
......@@ -52,7 +54,6 @@
}
return headers
}
/**
* @description: 获得excel数据
*/
......@@ -79,7 +80,6 @@
}
return row
})
excelData.push({
header,
results,
......@@ -90,7 +90,6 @@
}
return excelData
}
/**
* @description: 读取excel数据
*/
......@@ -117,7 +116,6 @@
reader.readAsArrayBuffer(rawFile)
})
}
async function upload(rawFile: File) {
const inputRefDom = unref(inputRef)
if (inputRefDom) {
......@@ -126,7 +124,6 @@
}
await readerData(rawFile)
}
/**
* @description: 触发选择文件管理器
*/
......@@ -134,9 +131,12 @@
const files = e && (e.target as HTMLInputElement).files
const rawFile = files && files[0] // only setting files[0]
if (!rawFile) return
if (props.isReturnFile) {
emit('success', rawFile)
return
}
upload(rawFile)
}
/**
* @description: 点击上传按钮
*/
......@@ -144,7 +144,6 @@
const inputRefDom = unref(inputRef)
inputRefDom && inputRefDom.click()
}
return { handleUpload, handleInputClick, inputRef }
},
})
......
......@@ -12,5 +12,6 @@ export { default as ApiTreeSelect } from './src/components/ApiTreeSelect.vue'
export { default as ApiTree } from './src/components/ApiTree.vue'
export { default as ApiRadioGroup } from './src/components/ApiRadioGroup.vue'
export { default as ApiCascader } from './src/components/ApiCascader.vue'
export { default as ApiTransfer } from './src/components/ApiTransfer.vue'
export { BasicForm }
......@@ -10,6 +10,7 @@
<slot name="formHeader"></slot>
<template v-for="schema in getSchema" :key="schema.field">
<FormItem
:isAdvanced="fieldsIsAdvancedMap[schema.field]"
:tableAction="tableAction"
:formActionType="formActionType"
:schema="schema"
......@@ -40,18 +41,14 @@
import type { FormActionType, FormProps, FormSchema } from './types/form'
import type { AdvanceState } from './types/hooks'
import type { Ref } from 'vue'
import { defineComponent, reactive, ref, computed, unref, onMounted, watch, nextTick } from 'vue'
import { Form, Row } from 'ant-design-vue'
import FormItem from './components/FormItem.vue'
import FormAction from './components/FormAction.vue'
import { dateItemType } from './helper'
import { dateUtil } from '/@/utils/dateUtil'
// import { cloneDeep } from 'lodash-es';
import { deepMerge } from '/@/utils'
import { useFormValues } from './hooks/useFormValues'
import useAdvanced from './hooks/useAdvanced'
import { useFormEvents } from './hooks/useFormEvents'
......@@ -59,39 +56,33 @@
import { useAutoFocus } from './hooks/useAutoFocus'
import { useModalContext } from '/@/components/Modal'
import { useDebounceFn } from '@vueuse/core'
import { basicProps } from './props'
import { useDesign } from '/@/hooks/web/useDesign'
import { cloneDeep } from 'lodash-es'
export default defineComponent({
name: 'BasicForm',
components: { FormItem, Form, Row, FormAction },
props: basicProps,
emits: ['advanced-change', 'reset', 'submit', 'register'],
emits: ['advanced-change', 'reset', 'submit', 'register', 'field-value-change'],
setup(props, { emit, attrs }) {
const formModel = reactive<Recordable>({})
const modalFn = useModalContext()
const advanceState = reactive<AdvanceState>({
isAdvanced: true,
hideAdvanceBtn: false,
isLoad: false,
actionSpan: 6,
})
const defaultValueRef = ref<Recordable>({})
const isInitedDefaultRef = ref(false)
const propsRef = ref<Partial<FormProps>>({})
const schemaRef = ref<Nullable<FormSchema[]>>(null)
const formElRef = ref<Nullable<FormActionType>>(null)
const { prefixCls } = useDesign('basic-form')
// Get the basic configuration of the form
const getProps = computed((): FormProps => {
return { ...props, ...unref(propsRef) } as FormProps
})
const getFormClass = computed(() => {
return [
prefixCls,
......@@ -100,7 +91,6 @@
},
]
})
// Get uniform row style and Row configuration for the entire form
const getRow = computed((): Recordable => {
const { baseRowStyle = {}, rowProps } = unref(getProps)
......@@ -109,15 +99,13 @@
...rowProps,
}
})
const getBindValue = computed(() => ({ ...attrs, ...props, ...unref(getProps) } as Recordable))
const getSchema = computed((): FormSchema[] => {
const schemas: FormSchema[] = unref(schemaRef) || (unref(getProps).schemas as any)
for (const schema of schemas) {
const { defaultValue, component } = schema
const { defaultValue, component, isHandleDateDefaultValue = true } = schema
// handle date type
if (defaultValue && dateItemType.includes(component)) {
if (isHandleDateDefaultValue && defaultValue && dateItemType.includes(component)) {
if (!Array.isArray(defaultValue)) {
schema.defaultValue = dateUtil(defaultValue)
} else {
......@@ -130,13 +118,12 @@
}
}
if (unref(getProps).showAdvancedButton) {
return schemas.filter((schema) => schema.component !== 'Divider') as FormSchema[]
return cloneDeep(schemas.filter((schema) => schema.component !== 'Divider') as FormSchema[])
} else {
return schemas as FormSchema[]
return cloneDeep(schemas as FormSchema[])
}
})
const { handleToggleAdvanced } = useAdvanced({
const { handleToggleAdvanced, fieldsIsAdvancedMap } = useAdvanced({
advanceState,
emit,
getProps,
......@@ -144,21 +131,18 @@
formModel,
defaultValueRef,
})
const { handleFormValues, initDefault } = useFormValues({
getProps,
defaultValueRef,
getSchema,
formModel,
})
useAutoFocus({
getSchema,
getProps,
isInitedDefault: isInitedDefaultRef,
formElRef: formElRef as Ref<FormActionType>,
})
const {
handleSubmit,
setFieldsValue,
......@@ -169,7 +153,7 @@
updateSchema,
resetSchema,
appendSchemaByField,
removeSchemaByFiled,
removeSchemaByField,
resetFields,
scrollToField,
} = useFormEvents({
......@@ -182,12 +166,10 @@
schemaRef: schemaRef as Ref<FormSchema[]>,
handleFormValues,
})
createFormContext({
resetAction: resetFields,
submitAction: handleSubmit,
})
watch(
() => unref(getProps).model,
() => {
......@@ -199,14 +181,12 @@
immediate: true,
},
)
watch(
() => unref(getProps).schemas,
(schemas) => {
resetSchema(schemas ?? [])
},
)
watch(
() => getSchema.value,
(schema) => {
......@@ -223,7 +203,6 @@
}
},
)
watch(
() => formModel,
useDebounceFn(() => {
......@@ -231,19 +210,17 @@
}, 300),
{ deep: true },
)
async function setProps(formProps: Partial<FormProps>): Promise<void> {
propsRef.value = deepMerge(unref(propsRef) || {}, formProps)
}
function setFormModel(key: string, value: any) {
formModel[key] = value
const { validateTrigger } = unref(getBindValue)
if (!validateTrigger || validateTrigger === 'change') {
validateFields([key]).catch((_) => {})
}
emit('field-value-change', key, value)
}
function handleEnterPress(e: KeyboardEvent) {
const { autoSubmitOnEnter } = unref(getProps)
if (!autoSubmitOnEnter) return
......@@ -254,7 +231,6 @@
}
}
}
const formActionType: Partial<FormActionType> = {
getFieldsValue,
setFieldsValue,
......@@ -262,7 +238,7 @@
updateSchema,
resetSchema,
setProps,
removeSchemaByFiled,
removeSchemaByField,
appendSchemaByField,
clearValidate,
validateFields,
......@@ -270,12 +246,10 @@
submit: handleSubmit,
scrollToField: scrollToField,
}
onMounted(() => {
initDefault()
emit('register', formActionType)
})
return {
getBindValue,
handleToggleAdvanced,
......@@ -291,6 +265,7 @@
setFormModel,
getFormClass,
getFormActionBindProps: computed((): Recordable => ({ ...getProps.value, ...advanceState })),
fieldsIsAdvancedMap,
...formActionType,
}
},
......@@ -298,7 +273,6 @@
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-basic-form';
.@{prefix-cls} {
.ant-form-item {
&-label label::after {
......
......@@ -27,6 +27,7 @@ import ApiSelect from './components/ApiSelect.vue'
import ApiTree from './components/ApiTree.vue'
import ApiTreeSelect from './components/ApiTreeSelect.vue'
import ApiCascader from './components/ApiCascader.vue'
import ApiTransfer from './components/ApiTransfer.vue'
import { BasicUpload } from '/@/components/Upload'
import { StrengthMeter } from '/@/components/StrengthMeter'
import { IconPicker } from '/@/components/Icon'
......@@ -57,6 +58,7 @@ componentMap.set('ApiCascader', ApiCascader)
componentMap.set('Cascader', Cascader)
componentMap.set('Slider', Slider)
componentMap.set('Rate', Rate)
componentMap.set('ApiTransfer', ApiTransfer)
componentMap.set('DatePicker', DatePicker)
componentMap.set('MonthPicker', DatePicker.MonthPicker)
......
......@@ -30,9 +30,7 @@
import { LoadingOutlined } from '@ant-design/icons-vue'
import { useI18n } from '/@/hooks/web/useI18n'
import { propTypes } from '/@/utils/propTypes'
type OptionsItem = { label: string; value: string; disabled?: boolean }
export default defineComponent({
name: 'ApiSelect',
components: {
......@@ -59,7 +57,7 @@
immediate: propTypes.bool.def(true),
alwaysLoad: propTypes.bool.def(false),
},
emits: ['options-change', 'change'],
emits: ['options-change', 'change', 'update:value'],
setup(props, { emit }) {
const options = ref<OptionsItem[]>([])
const loading = ref(false)
......@@ -67,13 +65,10 @@
const emitData = ref<any[]>([])
const attrs = useAttrs()
const { t } = useI18n()
// Embedded in the form, just use the hook binding to perform form verification
const [state] = useRuleFormItem(props, 'value', 'change', emitData)
const getOptions = computed(() => {
const { labelField, valueField, numberToString } = props
return unref(options).reduce((prev, next: Recordable) => {
if (next) {
const value = next[valueField]
......@@ -86,11 +81,15 @@
return prev
}, [] as OptionsItem[])
})
watchEffect(() => {
props.immediate && !props.alwaysLoad && fetch()
})
watch(
() => state.value,
(v) => {
emit('update:value', v)
},
)
watch(
() => props.params,
() => {
......@@ -98,7 +97,6 @@
},
{ deep: true },
)
async function fetch() {
const api = props.api
if (!api || !isFunction(api)) return
......@@ -121,7 +119,6 @@
loading.value = false
}
}
async function handleFetch(visible) {
if (visible) {
if (props.alwaysLoad) {
......@@ -132,15 +129,12 @@
}
}
}
function emitChange() {
emit('options-change', unref(getOptions))
}
function handleChange(_, ...args) {
emitData.value = args
}
return { state, attrs, getOptions, loading, t, handleFetch, handleChange }
},
})
......
<template>
<Transfer
:data-source="getdataSource"
:filter-option="filterOption"
:render="(item) => item.title"
:showSelectAll="showSelectAll"
:selectedKeys="selectedKeys"
:targetKeys="getTargetKeys"
:showSearch="showSearch"
@change="handleChange"
/>
</template>
<script lang="ts">
import { computed, defineComponent, watch, ref, unref, watchEffect } from 'vue'
import { Transfer } from 'ant-design-vue'
import { isFunction } from '/@/utils/is'
import { get, omit } from 'lodash-es'
import { propTypes } from '/@/utils/propTypes'
import { useI18n } from '/@/hooks/web/useI18n'
import { TransferDirection, TransferItem } from 'ant-design-vue/lib/transfer'
export default defineComponent({
name: 'ApiTransfer',
components: { Transfer },
props: {
value: { type: Array as PropType<Array<string>> },
api: {
type: Function as PropType<(arg?: Recordable) => Promise<TransferItem[]>>,
default: null,
},
params: { type: Object },
dataSource: { type: Array as PropType<Array<TransferItem>> },
immediate: propTypes.bool.def(true),
alwaysLoad: propTypes.bool.def(false),
afterFetch: { type: Function as PropType<Fn> },
resultField: propTypes.string.def(''),
labelField: propTypes.string.def('title'),
valueField: propTypes.string.def('key'),
showSearch: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
filterOption: {
type: Function as PropType<(inputValue: string, item: TransferItem) => boolean>,
},
selectedKeys: { type: Array as PropType<Array<string>> },
showSelectAll: { type: Boolean, default: false },
targetKeys: { type: Array as PropType<Array<string>> },
},
emits: ['options-change', 'change'],
setup(props, { attrs, emit }) {
const _dataSource = ref<TransferItem[]>([])
const _targetKeys = ref<string[]>([])
const { t } = useI18n()
const getAttrs = computed(() => {
return {
...(!props.api ? { dataSource: unref(_dataSource) } : {}),
...attrs,
}
})
const getdataSource = computed(() => {
const { labelField, valueField } = props
return unref(_dataSource).reduce((prev, next: Recordable) => {
if (next) {
prev.push({
...omit(next, [labelField, valueField]),
title: next[labelField],
key: next[valueField],
})
}
return prev
}, [] as TransferItem[])
})
const getTargetKeys = computed<string[]>(() => {
if (unref(_targetKeys).length > 0) {
return unref(_targetKeys)
}
if (Array.isArray(props.value)) {
return props.value
}
return []
})
function handleChange(keys: string[], direction: TransferDirection, moveKeys: string[]) {
_targetKeys.value = keys
console.log(direction)
console.log(moveKeys)
emit('change', keys)
}
watchEffect(() => {
props.immediate && !props.alwaysLoad && fetch()
})
watch(
() => props.params,
() => {
fetch()
},
{ deep: true },
)
async function fetch() {
const api = props.api
if (!api || !isFunction(api)) {
if (Array.isArray(props.dataSource)) {
_dataSource.value = props.dataSource
}
return
}
_dataSource.value = []
try {
const res = await api(props.params)
if (Array.isArray(res)) {
_dataSource.value = res
emitChange()
return
}
if (props.resultField) {
_dataSource.value = get(res, props.resultField) || []
}
emitChange()
} catch (error) {
console.warn(error)
} finally {
}
}
function emitChange() {
emit('options-change', unref(getdataSource))
}
return { getTargetKeys, getdataSource, t, getAttrs, handleChange }
},
})
</script>
......@@ -24,6 +24,7 @@
params: { type: Object },
immediate: { type: Boolean, default: true },
resultField: propTypes.string.def(''),
afterFetch: { type: Function as PropType<Fn> },
},
emits: ['options-change', 'change'],
setup(props, { attrs, emit }) {
......@@ -36,11 +37,9 @@
...attrs,
}
})
function handleChange(...args) {
emit('change', ...args)
}
watch(
() => props.params,
() => {
......@@ -48,20 +47,17 @@
},
{ deep: true },
)
watch(
() => props.immediate,
(v) => {
v && !isFirstLoaded.value && fetch()
},
)
onMounted(() => {
props.immediate && fetch()
})
async function fetch() {
const { api } = props
const { api, afterFetch } = props
if (!api || !isFunction(api)) return
loading.value = true
treeData.value = []
......@@ -71,6 +67,9 @@
} catch (e) {
console.error(e)
}
if (afterFetch && isFunction(afterFetch)) {
result = afterFetch(result)
}
loading.value = false
if (!result) return
if (!isArray(result)) {
......
import type { ColEx } from '../types'
import type { AdvanceState } from '../types/hooks'
import type { ComputedRef, Ref } from 'vue'
import { ComputedRef, getCurrentInstance, Ref, shallowReactive } from 'vue'
import type { FormProps, FormSchema } from '../types/form'
import { computed, unref, watch } from 'vue'
import { isBoolean, isFunction, isNumber, isObject } from '/@/utils/is'
......@@ -19,6 +19,8 @@ interface UseAdvancedContext {
}
export default function ({ advanceState, emit, getProps, getSchema, formModel, defaultValueRef }: UseAdvancedContext) {
const vm = getCurrentInstance()
const { realWidthRef, screenEnum, screenRef } = useBreakpoint()
const getEmptySpan = computed((): number => {
......@@ -104,6 +106,8 @@ export default function ({ advanceState, emit, getProps, getSchema, formModel, d
}
}
const fieldsIsAdvancedMap = shallowReactive({})
function updateAdvanced() {
let itemColSum = 0
let realItemColSum = 0
......@@ -136,10 +140,13 @@ export default function ({ advanceState, emit, getProps, getSchema, formModel, d
if (isAdvanced) {
realItemColSum = itemColSum
}
schema.isAdvanced = isAdvanced
fieldsIsAdvancedMap[schema.field] = isAdvanced
}
}
// 确保页面发送更新
vm?.proxy?.$forceUpdate()
advanceState.actionSpan = (realItemColSum % BASIC_COL_LEN) + unref(getEmptySpan)
getAdvanced(unref(getProps).actionColOptions || { span: BASIC_COL_LEN }, itemColSum, true)
......@@ -151,5 +158,5 @@ export default function ({ advanceState, emit, getProps, getSchema, formModel, d
advanceState.isAdvanced = !advanceState.isAdvanced
}
return { handleToggleAdvanced }
return { handleToggleAdvanced, fieldsIsAdvancedMap }
}
......@@ -79,8 +79,8 @@ export function useForm(props?: Props): UseFormReturnType {
})
},
removeSchemaByFiled: async (field: string | string[]) => {
unref(formRef)?.removeSchemaByFiled(field)
removeSchemaByField: async (field: string | string[]) => {
unref(formRef)?.removeSchemaByField(field)
},
// TODO promisify
......
......@@ -2,7 +2,7 @@ import type { ComputedRef, Ref } from 'vue'
import type { FormProps, FormSchema, FormActionType } from '../types/form'
import type { NamePath } from 'ant-design-vue/lib/form/interface'
import { unref, toRaw, nextTick } from 'vue'
import { isArray, isFunction, isNullOrUnDef, isObject, isString } from '/@/utils/is'
import { isArray, isFunction, isObject, isString, isDef, isNullOrUnDef } from '/@/utils/is'
import { deepMerge } from '/@/utils'
import { dateItemType, handleInputNumberValue, defaultValueComponents } from '../helper'
import { dateUtil } from '/@/utils/dateUtil'
......@@ -39,7 +39,8 @@ export function useFormEvents({
Object.keys(formModel).forEach((key) => {
const schema = unref(getSchema).find((item) => item.field === key)
const isInput = schema?.component && defaultValueComponents.includes(schema.component)
formModel[key] = isInput ? defaultValueRef.value[key] || '' : defaultValueRef.value[key]
const defaultValue = cloneDeep(defaultValueRef.value[key])
formModel[key] = isInput ? defaultValue || '' : defaultValue
})
nextTick(() => clearValidate())
......@@ -55,6 +56,10 @@ export function useFormEvents({
.map((item) => item.field)
.filter(Boolean)
// key 支持 a.b.c 的嵌套写法
const delimiter = '.'
const nestKeyArray = fields.filter((item) => item.indexOf(delimiter) >= 0)
const validKeys: string[] = []
Object.keys(values).forEach((key) => {
const schema = unref(getSchema).find((item) => item.field === key)
......@@ -85,6 +90,21 @@ export function useFormEvents({
formModel[key] = value
}
validKeys.push(key)
} else {
nestKeyArray.forEach((nestKey: string) => {
try {
const value = eval('values' + delimiter + nestKey)
if (isDef(value)) {
formModel[nestKey] = value
validKeys.push(nestKey)
}
} catch (e) {
// key not exist
if (isDef(defaultValueRef.value[nestKey])) {
formModel[nestKey] = cloneDeep(defaultValueRef.value[nestKey])
}
}
})
}
})
validateFields(validKeys).catch((_) => {})
......@@ -92,7 +112,7 @@ export function useFormEvents({
/**
* @description: Delete based on field name
*/
async function removeSchemaByFiled(fields: string | string[]): Promise<void> {
async function removeSchemaByField(fields: string | string[]): Promise<void> {
const schemaList: FormSchema[] = cloneDeep(unref(getSchema))
if (!fields) {
return
......@@ -103,7 +123,7 @@ export function useFormEvents({
fieldList = [fields]
}
for (const field of fieldList) {
_removeSchemaByFiled(field, schemaList)
_removeSchemaByFeild(field, schemaList)
}
schemaRef.value = schemaList
}
......@@ -111,7 +131,7 @@ export function useFormEvents({
/**
* @description: Delete based on field name
*/
function _removeSchemaByFiled(field: string, schemaList: FormSchema[]): void {
function _removeSchemaByFeild(field: string, schemaList: FormSchema[]): void {
if (isString(field)) {
const index = schemaList.findIndex((schema) => schema.field === field)
if (index !== -1) {
......@@ -206,12 +226,14 @@ export function useFormEvents({
}
const obj: Recordable = {}
const currentFieldsValue = getFieldsValue()
schemas.forEach((item) => {
if (
item.component != 'Divider' &&
Reflect.has(item, 'field') &&
item.field &&
!isNullOrUnDef(item.defaultValue)
!isNullOrUnDef(item.defaultValue) &&
!(item.field in currentFieldsValue)
) {
obj[item.field] = item.defaultValue
}
......@@ -280,7 +302,7 @@ export function useFormEvents({
updateSchema,
resetSchema,
appendSchemaByField,
removeSchemaByFiled,
removeSchemaByField,
resetFields,
setFieldsValue,
scrollToField,
......
......@@ -3,7 +3,7 @@ import { dateUtil } from '/@/utils/dateUtil'
import { unref } from 'vue'
import type { Ref, ComputedRef } from 'vue'
import type { FormProps, FormSchema } from '../types/form'
import { set } from 'lodash-es'
import { cloneDeep, set } from 'lodash-es'
interface UseFormValuesContext {
defaultValueRef: Ref<any>
......@@ -92,14 +92,21 @@ export function useFormValues({ defaultValueRef, getSchema, formModel, getProps
}
for (const [field, [startTimeKey, endTimeKey], format = 'YYYY-MM-DD'] of fieldMapToTime) {
if (!field || !startTimeKey || !endTimeKey || !values[field]) {
if (!field || !startTimeKey || !endTimeKey) {
continue
}
// If the value to be converted is empty, remove the field
if (!values[field]) {
Reflect.deleteProperty(values, field)
continue
}
const [startTime, endTime]: string[] = values[field]
values[startTimeKey] = dateUtil(startTime).format(format)
values[endTimeKey] = dateUtil(endTime).format(format)
const [startTimeFormat, endTimeFormat] = Array.isArray(format) ? format : [format, format]
values[startTimeKey] = dateUtil(startTime).format(startTimeFormat)
values[endTimeKey] = dateUtil(endTime).format(endTimeFormat)
Reflect.deleteProperty(values, field)
}
......@@ -113,10 +120,13 @@ export function useFormValues({ defaultValueRef, getSchema, formModel, getProps
const { defaultValue } = item
if (!isNullOrUnDef(defaultValue)) {
obj[item.field] = defaultValue
formModel[item.field] = defaultValue
if (formModel[item.field] === undefined) {
formModel[item.field] = defaultValue
}
}
})
defaultValueRef.value = obj
defaultValueRef.value = cloneDeep(obj)
}
return { handleFormValues, initDefault }
......
......@@ -7,7 +7,7 @@ import type { TableActionType } from '/@/components/Table/src/types/table'
import type { CSSProperties } from 'vue'
import type { RowProps } from 'ant-design-vue/lib/grid/Row'
export type FieldMapToTime = [string, [string, string], string?][]
export type FieldMapToTime = [string, [string, string], (string | [string, string])?][]
export type Rule = RuleObject & {
trigger?: 'blur' | 'change' | ['change', 'blur']
......@@ -33,7 +33,7 @@ export interface FormActionType {
updateSchema: (data: Partial<FormSchema> | Partial<FormSchema>[]) => Promise<void>
resetSchema: (data: Partial<FormSchema> | Partial<FormSchema>[]) => Promise<void>
setProps: (formProps: Partial<FormProps>) => Promise<void>
removeSchemaByFiled: (field: string | string[]) => Promise<void>
removeSchemaByField: (field: string | string[]) => Promise<void>
appendSchemaByField: (
schema: FormSchema,
prefixField: string | undefined,
......@@ -49,6 +49,7 @@ export type RegisterFn = (formInstance: FormActionType) => void
export type UseFormReturnType = [RegisterFn, FormActionType]
export interface FormProps {
name?: string
layout?: 'vertical' | 'inline' | 'horizontal'
// Form value
model?: Recordable
......@@ -171,6 +172,10 @@ export interface FormSchema {
// 默认值
defaultValue?: any
// 是否自动处理与时间相关组件的默认值
isHandleDateDefaultValue?: boolean
isAdvanced?: boolean
// Matching details components
......
......@@ -114,3 +114,4 @@ export type ComponentType =
| 'Slider'
| 'Rate'
| 'Divider'
| 'ApiTransfer'
......@@ -10,9 +10,8 @@
import { useModalContext } from '../../Modal'
import { useRootSetting } from '/@/hooks/setting/useRootSetting'
import { onMountedOrActivated } from '/@/hooks/core/onMountedOrActivated'
import { getTheme } from './getTheme'
type Lang = 'zh_CN' | 'en_US' | 'ja_JP' | 'ko_KR' | undefined
export default defineComponent({
inheritAttrs: false,
props: {
......@@ -24,28 +23,25 @@
const wrapRef = ref<ElRef>(null)
const vditorRef = ref(null) as Ref<Nullable<Vditor>>
const initedRef = ref(false)
const modalFn = useModalContext()
const { getLocale } = useLocale()
const { getDarkMode } = useRootSetting()
const valueRef = ref(props.value || '')
watch(
[() => getDarkMode.value, () => initedRef.value],
([val, inited]) => {
if (!inited) {
return
}
const theme = val === 'dark' ? 'dark' : 'classic'
instance.getVditor()?.setTheme(theme)
instance
.getVditor()
?.setTheme(getTheme(val) as any, getTheme(val, 'content'), getTheme(val, 'code'))
},
{
immediate: true,
flush: 'post',
},
)
watch(
() => props.value,
(v) => {
......@@ -55,7 +51,6 @@
valueRef.value = v
},
)
const getCurrentLang = computed((): 'zh_CN' | 'en_US' | 'ja_JP' | 'ko_KR' => {
let lang: Lang
switch (unref(getLocale)) {
......@@ -78,13 +73,22 @@
if (!wrapEl) return
const bindValue = { ...attrs, ...props }
const insEditor = new Vditor(wrapEl, {
theme: getDarkMode.value === 'dark' ? 'dark' : 'classic',
// 设置外观主题
theme: getTheme(getDarkMode.value) as any,
lang: unref(getCurrentLang),
mode: 'sv',
fullscreen: {
index: 520,
},
preview: {
theme: {
// 设置内容主题
current: getTheme(getDarkMode.value, 'content'),
},
hljs: {
// 设置代码块主题
style: getTheme(getDarkMode.value, 'code'),
},
actions: [],
},
input: (v) => {
......@@ -110,11 +114,9 @@
},
})
}
const instance = {
getVditor: (): Vditor => vditorRef.value!,
}
function destroy() {
const vditorInstance = unref(vditorRef)
if (!vditorInstance) return
......@@ -124,9 +126,7 @@
vditorRef.value = null
initedRef.value = false
}
onMountedOrActivated(init)
onBeforeUnmount(destroy)
onDeactivated(destroy)
return {
......
<template>
<!-- eslint-disable vue/no-v-html -->
<div v-html="getHtmlData" :class="$props.class" class="markdown-viewer"></div>
<div ref="viewerRef" id="markdownViewer" :class="$props.class"></div>
</template>
<script lang="ts" setup>
import { computed, defineProps } from 'vue'
import showdown from 'showdown'
const converter = new showdown.Converter()
converter.setOption('tables', true)
import { defineProps, onBeforeUnmount, onDeactivated, Ref, ref, unref, watch } from 'vue'
import VditorPreview from 'vditor/dist/method.min'
import { onMountedOrActivated } from '/@/hooks/core/onMountedOrActivated'
import { useRootSetting } from '/@/hooks/setting/useRootSetting'
import { getTheme } from './getTheme'
const props = defineProps({
value: { type: String },
class: { type: String },
})
const getHtmlData = computed(() => converter.makeHtml(props.value || ''))
</script>
<style scoped>
.markdown-viewer {
width: 100%;
const viewerRef = ref<ElRef>(null)
const vditorPreviewRef = ref(null) as Ref<Nullable<VditorPreview>>
const { getDarkMode } = useRootSetting()
function init() {
const viewerEl = unref(viewerRef) as HTMLElement
vditorPreviewRef.value = VditorPreview.preview(viewerEl, props.value, {
mode: getTheme(getDarkMode.value, 'content'),
theme: {
// 设置内容主题
current: getTheme(getDarkMode.value, 'content'),
},
hljs: {
// 设置代码块主题
style: getTheme(getDarkMode.value, 'code'),
},
})
}
</style>
watch(
() => getDarkMode.value,
(val) => {
VditorPreview.setContentTheme(getTheme(val, 'content'))
VditorPreview.setCodeTheme(getTheme(val, 'code'))
init()
},
)
watch(
() => props.value,
(v, oldValue) => {
v !== oldValue && init()
},
)
function destroy() {
const vditorInstance = unref(vditorPreviewRef)
if (!vditorInstance) return
try {
vditorInstance?.destroy?.()
} catch (error) {}
vditorPreviewRef.value = null
}
onMountedOrActivated(init)
onBeforeUnmount(destroy)
onDeactivated(destroy)
</script>
/**
* 获取主题类型 深色浅色模式 对应的值
* @param darkModeVal 深色模式值
* @param themeMode 主题类型——外观(默认), 内容, 代码块
*/
export const getTheme = (
darkModeVal: 'light' | 'dark' | string,
themeMode: 'default' | 'content' | 'code' = 'default',
) => {
const isDark = darkModeVal === 'dark'
switch (themeMode) {
case 'default':
return isDark ? 'dark' : 'classic'
case 'content':
return isDark ? 'dark' : 'light'
case 'code':
return isDark ? 'dracula' : 'github'
}
}
......@@ -26,7 +26,6 @@
&-title {
font-size: 16px;
font-weight: bold;
line-height: 16px;
.base-title {
cursor: move !important;
......@@ -111,19 +110,16 @@
.ant-modal-confirm .ant-modal-body {
padding: 24px !important;
}
@media screen and (max-height: 600px) {
.ant-modal {
top: 60px;
}
}
@media screen and (max-height: 540px) {
.ant-modal {
top: 30px;
}
}
@media screen and (max-height: 480px) {
.ant-modal {
top: 10px;
......
......@@ -29,7 +29,7 @@
</span>
<SimpleMenuTag :item="item" :collapseParent="!!collapse && !!parent" />
</template>
<template v-for="childrenItem in item.children || []" :key="childrenItem.path">
<template v-for="childrenItem in item.children || []" :key="childrenItem.paramPath || childrenItem.path">
<SimpleSubMenu v-bind="$props" :item="childrenItem" :parent="false" />
</template>
</SubMenu>
......@@ -37,17 +37,14 @@
<script lang="ts">
import type { PropType } from 'vue'
import type { Menu } from '/@/router/types'
import { defineComponent, computed } from 'vue'
import { useDesign } from '/@/hooks/web/useDesign'
import Icon from '/@/components/Icon/index'
import MenuItem from './components/MenuItem.vue'
import SubMenu from './components/SubMenuItem.vue'
import { propTypes } from '/@/utils/propTypes'
import { useI18n } from '/@/hooks/web/useI18n'
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent'
export default defineComponent({
name: 'SimpleSubMenu',
components: {
......@@ -69,7 +66,6 @@
setup(props) {
const { t } = useI18n()
const { prefixCls } = useDesign('simple-menu')
const getShowMenu = computed(() => !props.item?.meta?.hideMenu)
const getIcon = computed(() => props.item?.icon)
const getI18nName = computed(() => t(props.item?.name))
......@@ -83,7 +79,6 @@
},
]
})
function menuHasChildren(menuTreeItem: Menu): boolean {
return (
!menuTreeItem.meta?.hideChildrenInMenu &&
......@@ -92,7 +87,6 @@
menuTreeItem.children.length > 0
)
}
return {
prefixCls,
menuHasChildren,
......
......@@ -28,6 +28,10 @@
<template #headerCell="{ column }">
<HeaderCell :column="column" />
</template>
<!-- 增加对antdv3.x兼容 -->
<template #bodyCell="data">
<slot name="bodyCell" v-bind="data || {}"></slot>
</template>
<!-- <template #[`header-${column.dataIndex}`] v-for="(column, index) in columns" :key="index">-->
<!-- <HeaderCell :column="column" />-->
<!-- </template>-->
......@@ -36,14 +40,12 @@
</template>
<script lang="ts">
import type { BasicTableProps, TableActionType, SizeType, ColumnChangeParam } from './types/table'
import { defineComponent, ref, computed, unref, toRaw, inject, watchEffect } from 'vue'
import { Table } from 'ant-design-vue'
import { BasicForm, useForm } from '/@/components/Form/index'
import { PageWrapperFixedHeightKey } from '/@/components/Page'
import HeaderCell from './components/HeaderCell.vue'
import { InnerHandlers } from './types/table'
import { usePagination } from './hooks/usePagination'
import { useColumns } from './hooks/useColumns'
import { useDataSource } from './hooks/useDataSource'
......@@ -59,12 +61,10 @@
import { useTableFooter } from './hooks/useTableFooter'
import { useTableForm } from './hooks/useTableForm'
import { useDesign } from '/@/hooks/web/useDesign'
import { omit } from 'lodash-es'
import { basicProps } from './props'
import { isFunction } from '/@/utils/is'
import { warn } from '/@/utils/log'
export default defineComponent({
components: {
Table,
......@@ -93,18 +93,14 @@
setup(props, { attrs, emit, slots, expose }) {
const tableElRef = ref(null)
const tableData = ref<Recordable[]>([])
const wrapRef = ref(null)
const formRef = ref(null)
const innerPropsRef = ref<Partial<BasicTableProps>>()
const { prefixCls } = useDesign('basic-table')
const [registerForm, formActions] = useForm()
const getProps = computed(() => {
return { ...props, ...unref(innerPropsRef) } as BasicTableProps
})
const isFixedHeightPage = inject(PageWrapperFixedHeightKey, false)
watchEffect(() => {
unref(isFixedHeightPage) &&
......@@ -113,21 +109,19 @@
"'canResize' of BasicTable may not work in PageWrapper with 'fixedHeight' (especially in hot updates)",
)
})
const { getLoading, setLoading } = useLoading(getProps)
const { getPaginationInfo, getPagination, setPagination, setShowPagination, getShowPagination } =
usePagination(getProps)
const {
getRowSelection,
getRowSelectionRef,
getSelectRows,
setSelectedRows,
clearSelectedRowKeys,
getSelectRowKeys,
deleteSelectRowByKey,
setSelectedRowKeys,
} = useRowSelection(getProps, tableData, emit)
const {
handleTableChange: onTableChange,
getDataSourceRef,
......@@ -155,7 +149,6 @@
},
emit,
)
function handleTableChange(...args) {
onTableChange.call(undefined, ...args)
emit('change', ...args)
......@@ -163,10 +156,8 @@
const { onChange } = unref(getProps)
onChange && isFunction(onChange) && onChange.call(undefined, ...args)
}
const { getViewColumns, getColumns, setCacheColumnsByField, setColumns, getColumnsRef, getCacheColumns } =
useColumns(getProps, getPaginationInfo)
const { getScrollRef, redoHeight } = useTableScroll(
getProps,
tableElRef,
......@@ -176,9 +167,7 @@
wrapRef,
formRef,
)
const { scrollTo } = useTableScrollTo(tableElRef, getDataSourceRef)
const { customRow } = useCustomRow(getProps, {
setSelectedRowKeys,
getSelectRowKeys,
......@@ -186,11 +175,8 @@
getAutoCreateKey,
emit,
})
const { getRowClassName } = useTableStyle(getProps, prefixCls)
const { getExpandOption, expandAll, expandRows, collapseAll } = useTableExpand(getProps, tableData, emit)
const handlers: InnerHandlers = {
onColumnsChange: (data: ColumnChangeParam[]) => {
emit('columns-change', data)
......@@ -198,18 +184,14 @@
unref(getProps).onColumnsChange?.(data)
},
}
const { getHeaderProps } = useTableHeader(getProps, slots, handlers)
const { getFooterProps } = useTableFooter(getProps, getScrollRef, tableElRef, getDataSourceRef)
const { getFormProps, replaceFormSlotKey, getFormSlotKeys, handleSearchInfoChange } = useTableForm(
getProps,
slots,
fetch,
getLoading,
)
const getBindValues = computed(() => {
const dataSource = unref(getDataSourceRef)
let propsData: Recordable = {
......@@ -228,14 +210,12 @@
footer: unref(getFooterProps),
...unref(getExpandOption),
}
if (slots.expandedRowRender) {
propsData = omit(propsData, 'scroll')
}
// if (slots.expandedRowRender) {
// propsData = omit(propsData, 'scroll');
// }
propsData = omit(propsData, ['class', 'onChange'])
return propsData
})
const getWrapperClass = computed(() => {
const values = unref(getBindValues)
return [
......@@ -247,7 +227,6 @@
},
]
})
const getEmptyDataIsShowTable = computed(() => {
const { emptyDataIsShowTable, useSearchForm } = unref(getProps)
if (emptyDataIsShowTable || !useSearchForm) {
......@@ -255,11 +234,9 @@
}
return !!unref(getDataSourceRef).length
})
function setProps(props: Partial<BasicTableProps>) {
innerPropsRef.value = { ...unref(innerPropsRef), ...props }
}
const tableAction: TableActionType = {
reload,
getSelectRows,
......@@ -287,6 +264,7 @@
updateTableData,
setShowPagination,
getShowPagination,
setSelectedRows,
setCacheColumnsByField,
expandAll,
expandRows,
......@@ -297,11 +275,8 @@
},
}
createTableContext({ ...tableAction, wrapRef, getBindValues })
expose(tableAction)
emit('register', tableAction, formActions)
return {
formRef,
tableElRef,
......@@ -326,7 +301,6 @@
</script>
<style lang="less">
@border-color: #cecece4d;
@prefix-cls: ~'@{namespace}-basic-table';
[data-theme='dark'] {
......@@ -335,7 +309,6 @@
background-color: #262626;
}
}
.@{prefix-cls} {
max-width: 100%;
height: 100%;
......@@ -350,6 +323,7 @@
padding: 16px;
.ant-form {
width: 100%;
padding: 12px 10px 6px;
margin-bottom: 16px;
background-color: @component-background;
......@@ -387,7 +361,6 @@
justify-content: space-between;
align-items: center;
}
//.ant-table-tbody > tr.ant-table-row-selected td {
//background-color: fade(@primary-color, 8%) !important;
//}
......
import type { Component } from 'vue'
import { Input, Select, Checkbox, InputNumber, Switch, DatePicker, TimePicker } from 'ant-design-vue'
import {
Input,
Select,
Checkbox,
InputNumber,
Switch,
DatePicker,
TimePicker,
AutoComplete,
Radio,
} from 'ant-design-vue'
import type { ComponentType } from './types/componentType'
import { ApiSelect, ApiTreeSelect } from '/@/components/Form'
import { ApiSelect, ApiTreeSelect, RadioButtonGroup, ApiRadioGroup } from '/@/components/Form'
const componentMap = new Map<ComponentType, Component>()
......@@ -9,11 +19,15 @@ componentMap.set('Input', Input)
componentMap.set('InputNumber', InputNumber)
componentMap.set('Select', Select)
componentMap.set('ApiSelect', ApiSelect)
componentMap.set('AutoComplete', AutoComplete)
componentMap.set('ApiTreeSelect', ApiTreeSelect)
componentMap.set('Switch', Switch)
componentMap.set('Checkbox', Checkbox)
componentMap.set('DatePicker', DatePicker)
componentMap.set('TimePicker', TimePicker)
componentMap.set('RadioGroup', Radio.Group)
componentMap.set('RadioButtonGroup', RadioButtonGroup)
componentMap.set('ApiRadioGroup', ApiRadioGroup)
export function add(compName: ComponentType, component: Component) {
componentMap.set(compName, component)
......
......@@ -7,7 +7,7 @@ const { t } = useI18n()
* @description: 生成placeholder
*/
export function createPlaceholderMessage(component: ComponentType) {
if (component.includes('Input')) {
if (component.includes('Input') || component.includes('AutoComplete')) {
return t('common.inputText')
}
if (component.includes('Picker')) {
......
......@@ -98,6 +98,7 @@
import type { BasicColumn, ColumnChangeParam } from '../../types/table'
import { defineComponent, ref, reactive, toRefs, watchEffect, nextTick, unref, computed } from 'vue'
import { Tooltip, Popover, Checkbox, Divider } from 'ant-design-vue'
import type { CheckboxChangeEvent } from 'ant-design-vue/lib/checkbox/interface'
import { SettingOutlined, DragOutlined } from '@ant-design/icons-vue'
import { Icon } from '/@/components/Icon'
import { ScrollContainer } from '/@/components/Container'
......@@ -110,20 +111,17 @@
import { cloneDeep, omit } from 'lodash-es'
import Sortablejs from 'sortablejs'
import type Sortable from 'sortablejs'
interface State {
checkAll: boolean
isInit?: boolean
checkedList: string[]
defaultCheckList: string[]
}
interface Options {
label: string
value: string
fixed?: boolean | 'left' | 'right'
}
export default defineComponent({
name: 'ColumnSetting',
components: {
......@@ -138,49 +136,39 @@
Icon,
},
emits: ['columns-change'],
setup(_, { emit, attrs }) {
const { t } = useI18n()
const table = useTableContext()
const defaultRowSelection = omit(table.getRowSelection(), 'selectedRowKeys')
let inited = false
const cachePlainOptions = ref<Options[]>([])
const plainOptions = ref<Options[] | any>([])
const plainSortOptions = ref<Options[]>([])
const columnListRef = ref<ComponentRef>(null)
const state = reactive<State>({
checkAll: true,
checkedList: [],
defaultCheckList: [],
})
const checkIndex = ref(false)
const checkSelect = ref(false)
const { prefixCls } = useDesign('basic-column-setting')
const getValues = computed(() => {
return unref(table?.getBindValues) || {}
})
watchEffect(() => {
const columns = table.getColumns()
if (columns.length && !state.isInit) {
init()
}
setTimeout(() => {
const columns = table.getColumns()
if (columns.length && !state.isInit) {
init()
}
}, 0)
})
watchEffect(() => {
const values = unref(getValues)
checkIndex.value = !!values.showIndexColumn
checkSelect.value = !!values.rowSelection
})
function getColumns() {
const ret: Options[] = []
table.getColumns({ ignoreIndex: true, ignoreAction: true }).forEach((item) => {
......@@ -192,12 +180,10 @@
})
return ret
}
function init() {
const columns = getColumns()
const checkList = table
.getColumns({ ignoreAction: true })
.getColumns({ ignoreAction: true, ignoreIndex: true })
.map((item) => {
if (item.defaultHidden) {
return ''
......@@ -205,7 +191,6 @@
return item.dataIndex || item.title
})
.filter(Boolean) as string[]
if (!plainOptions.value.length) {
plainOptions.value = columns
plainSortOptions.value = columns
......@@ -215,7 +200,6 @@
// const fixedColumns = columns.filter((item) =>
// Reflect.has(item, 'fixed')
// ) as BasicColumn[];
unref(plainOptions).forEach((item: BasicColumn) => {
const findItem = columns.find((col: BasicColumn) => col.dataIndex === item.dataIndex)
if (findItem) {
......@@ -226,9 +210,8 @@
state.isInit = true
state.checkedList = checkList
}
// checkAll change
function onCheckAllChange(e: ChangeEvent) {
function onCheckAllChange(e: CheckboxChangeEvent) {
const checkList = plainOptions.value.map((item) => item.value)
if (e.target.checked) {
state.checkedList = checkList
......@@ -238,14 +221,12 @@
setColumns([])
}
}
const indeterminate = computed(() => {
const len = plainOptions.value.length
let checkedLen = state.checkedList.length
unref(checkIndex) && checkedLen--
// unref(checkIndex) && checkedLen--;
return checkedLen > 0 && checkedLen < len
})
// Trigger when check/uncheck a column
function onChange(checkedList: string[]) {
const len = plainSortOptions.value.length
......@@ -256,7 +237,6 @@
})
setColumns(checkedList)
}
let sortable: Sortable
let sortableOrder: string[] = []
// reset columns
......@@ -268,7 +248,6 @@
setColumns(table.getCacheColumns())
sortable.sort(sortableOrder)
}
// Open the pop-up window for drag and drop initialization
function handleVisibleChange() {
if (inited) return
......@@ -290,7 +269,6 @@
}
// Sort column
const columns = cloneDeep(plainSortOptions.value)
if (oldIndex > newIndex) {
columns.splice(newIndex, 0, columns[oldIndex])
columns.splice(oldIndex + 1, 1)
......@@ -298,9 +276,12 @@
columns.splice(newIndex + 1, 0, columns[oldIndex])
columns.splice(oldIndex, 1)
}
plainSortOptions.value = columns
setColumns(columns)
setColumns(
columns
.map((col: Options) => col.value)
.filter((value: string) => state.checkedList.includes(value)),
)
},
})
// 记录原始order 序列
......@@ -308,24 +289,20 @@
inited = true
})
}
// Control whether the serial number column is displayed
function handleIndexCheckChange(e: ChangeEvent) {
function handleIndexCheckChange(e: CheckboxChangeEvent) {
table.setProps({
showIndexColumn: e.target.checked,
})
}
// Control whether the check box is displayed
function handleSelectCheckChange(e: ChangeEvent) {
function handleSelectCheckChange(e: CheckboxChangeEvent) {
table.setProps({
rowSelection: e.target.checked ? defaultRowSelection : undefined,
})
}
function handleColumnFixed(item: BasicColumn, fixed?: 'left' | 'right') {
if (!state.checkedList.includes(item.dataIndex as string)) return
const columns = getColumns() as BasicColumn[]
const isFixed = item.fixed === fixed ? false : fixed
const index = columns.findIndex((col) => col.dataIndex === item.dataIndex)
......@@ -333,14 +310,12 @@
columns[index].fixed = isFixed
}
item.fixed = isFixed
if (isFixed && !item.width) {
item.width = 100
}
table.setCacheColumnsByField?.(item.dataIndex as string, { fixed: isFixed })
setColumns(columns)
}
function setColumns(columns: BasicColumn[] | string[]) {
table.setColumns(columns)
const data: ColumnChangeParam[] = unref(plainSortOptions).map((col) => {
......@@ -351,14 +326,11 @@
) !== -1
return { dataIndex: col.value, fixed: col.fixed, visible }
})
emit('columns-change', data)
}
function getPopupContainer() {
return isFunction(attrs.getPopupContainer) ? attrs.getPopupContainer() : getParentContainer()
}
return {
t,
...toRefs(state),
......@@ -388,7 +360,6 @@
margin: 0 5px;
cursor: move;
}
.@{prefix-cls} {
&__popover-title {
position: relative;
......
import type { BasicColumn, BasicTableProps, CellFormat, GetColumnsParams } from '../types/table'
import type { PaginationProps } from '../types/pagination'
import type { ComputedRef } from 'vue'
import { computed, Ref, ref, toRaw, unref, watch } from 'vue'
import { computed, Ref, ref, reactive, toRaw, unref, watch } from 'vue'
import { renderEditCell } from '../components/editable'
import { usePermission } from '/@/hooks/web/usePermission'
import { useI18n } from '/@/hooks/web/useI18n'
......@@ -167,7 +167,7 @@ export function useColumns(
if ((edit || editRow) && !isDefaultAction) {
column.customRender = renderEditCell(column)
}
return column
return reactive(column)
})
})
......@@ -293,7 +293,7 @@ export function formatCell(text: string, format: CellFormat, record: Recordable,
try {
// date type
const DATE_FORMAT_PREFIX = 'date|'
if (isString(format) && format.startsWith(DATE_FORMAT_PREFIX)) {
if (isString(format) && format.startsWith(DATE_FORMAT_PREFIX) && text) {
const dateFormat = format.replace(DATE_FORMAT_PREFIX, '')
if (!dateFormat) {
......
......@@ -76,6 +76,9 @@ export function useTable(tableProps?: Props): [
redoHeight: () => {
getTableInstance().redoHeight()
},
setSelectedRows: (rows: Recordable[]) => {
return toRaw(getTableInstance().setSelectedRows(rows))
},
setLoading: (loading: boolean) => {
getTableInstance().setLoading(loading)
},
......
......@@ -36,14 +36,13 @@ export function useTableFooter(
nextTick(() => {
const tableEl = unref(tableElRef)
if (!tableEl) return
const bodyDomList = tableEl.$el.querySelectorAll('.ant-table-body')
const bodyDom = bodyDomList[0]
const bodyDom = tableEl.$el.querySelector('.ant-table-content')
useEventListener({
el: bodyDom,
name: 'scroll',
listener: () => {
const footerBodyDom = tableEl.$el.querySelector(
'.ant-table-footer .ant-table-body',
'.ant-table-footer .ant-table-content',
) as HTMLDivElement
if (!footerBodyDom || !bodyDom) return
footerBodyDom.scrollLeft = bodyDom.scrollLeft
......
......@@ -87,7 +87,7 @@ export function useTableScroll(
bodyEl!.style.height = 'unset'
if (!unref(getCanResize) || tableData.length === 0) return
if (!unref(getCanResize) || !unref(tableData) || tableData.length === 0) return
await nextTick()
// Add a delay to get the correct bottomIncludeBody paginationHeight footerHeight headerHeight
......@@ -186,7 +186,7 @@ export function useTableScroll(
const columns = unref(columnsRef).filter((item) => !item.defaultHidden)
columns.forEach((item) => {
width += Number.parseInt(item.width as string) || 0
width += Number.parseFloat(item.width as string) || 0
})
const unsetWidthColumns = columns.filter((item) => !Reflect.has(item, 'width'))
......
......@@ -3,8 +3,12 @@ export type ComponentType =
| 'InputNumber'
| 'Select'
| 'ApiSelect'
| 'AutoComplete'
| 'ApiTreeSelect'
| 'Checkbox'
| 'Switch'
| 'DatePicker'
| 'TimePicker'
| 'RadioGroup'
| 'RadioButtonGroup'
| 'ApiRadioGroup'
......@@ -7,9 +7,12 @@ interface PaginationRenderProps {
originalElement: any
}
type PaginationPositon = 'topLeft' | 'topCenter' | 'topRight' | 'bottomLeft' | 'bottomCenter' | 'bottomRight'
export declare class PaginationConfig extends Pagination {
position?: 'top' | 'bottom' | 'both'
position?: PaginationPositon[]
}
export interface PaginationProps {
/**
* total number of data items
......@@ -96,4 +99,11 @@ export interface PaginationProps {
* @type Function
*/
itemRender?: (props: PaginationRenderProps) => VNodeChild | JSX.Element
/**
* specify the position of Pagination
* @default ['bottomRight']
* @type string[]
*/
position?: PaginationPositon[]
}
......@@ -84,10 +84,11 @@ export type SizeType = 'default' | 'middle' | 'small' | 'large'
export interface TableActionType {
reload: (opt?: FetchParams) => Promise<void>
setSelectedRows: (rows: Recordable[]) => void
getSelectRows: <T = Recordable>() => T[]
clearSelectedRowKeys: () => void
expandAll: () => void
expandRows: (keys: string[]) => void
expandRows: (keys: string[] | number[]) => void
collapseAll: () => void
scrollTo: (pos: string) => void // pos: id | "top" | "bottom"
getSelectRowKeys: () => string[]
......@@ -456,6 +457,8 @@ export interface BasicColumn extends ColumnProps<Recordable> {
column: BasicColumn
index: number
}) => VNodeChild | JSX.Element
// 动态 Disabled
editDynamicDisabled?: boolean | ((record: Recordable) => boolean)
}
export type ColumnChangeParam = {
......
import BasicTree from './src/Tree.vue'
import BasicTree from './src/BasicTree.vue'
import './style'
export { BasicTree }
export type { ContextMenuItem } from '/@/hooks/web/useContextMenu'
export * from './src/tree'
export * from './src/types/tree'
......@@ -37,12 +37,9 @@
import { useI18n } from '/@/hooks/web/useI18n'
import { useDebounceFn } from '@vueuse/core'
import { createBEM } from '/@/utils/bem'
import { ToolbarEnum } from './tree'
import { ToolbarEnum } from '../types/tree'
const searchValue = ref('')
const [bem] = createBEM('tree-header')
const props = defineProps({
helpMessage: {
type: [String, Array] as PropType<string | string[]>,
......@@ -78,10 +75,8 @@
},
} as const)
const emit = defineEmits(['strictly-change', 'search'])
const slots = useSlots()
const { t } = useI18n()
const getInputSearchCls = computed(() => {
const titleExists = slots.headerTitle || props.title
return [
......@@ -92,7 +87,6 @@
},
]
})
const toolbarList = computed(() => {
const { checkable } = props
const defaultToolbarList = [
......@@ -103,7 +97,6 @@
divider: checkable,
},
]
return checkable
? [
{ label: t('component.tree.selectAll'), value: ToolbarEnum.SELECT_ALL },
......@@ -114,14 +107,10 @@
},
...defaultToolbarList,
{ label: t('component.tree.checkStrictly'), value: ToolbarEnum.CHECK_STRICTLY },
{
label: t('component.tree.checkUnStrictly'),
value: ToolbarEnum.CHECK_UN_STRICTLY,
},
{ label: t('component.tree.checkUnStrictly'), value: ToolbarEnum.CHECK_UN_STRICTLY },
]
: defaultToolbarList
})
function handleMenuClick(e: { key: ToolbarEnum }) {
const { key } = e
switch (key) {
......@@ -145,20 +134,16 @@
break
}
}
function emitChange(value?: string): void {
emit('search', value)
}
const debounceEmitChange = useDebounceFn(emitChange, 200)
watch(
() => searchValue.value,
(v) => {
debounceEmitChange(v)
},
)
watch(
() => props.searchText,
(v) => {
......
import type { InsertNodeParams, KeyType, FieldNames } from './tree'
import type { InsertNodeParams, KeyType, FieldNames, TreeItem } from '../types/tree'
import type { Ref, ComputedRef } from 'vue'
import type { TreeDataItem } from 'ant-design-vue/es/tree/Tree'
......@@ -176,6 +176,23 @@ export function useTree(treeDataRef: Ref<TreeDataItem[]>, getFieldNames: Compute
}
}
}
// Get selected node
function getSelectedNode(key: KeyType, list?: TreeItem[], selectedNode?: TreeItem | null) {
if (!key && key !== 0) return null
const treeData = list || unref(treeDataRef)
treeData.forEach((item) => {
if (selectedNode?.key || selectedNode?.key === 0) return selectedNode
if (item.key === key) {
selectedNode = item
return
}
if (item.children && item.children.length) {
selectedNode = getSelectedNode(key, item.children, selectedNode)
}
})
return selectedNode || null
}
return {
deleteNodeByKey,
insertNodeByKey,
......@@ -185,5 +202,6 @@ export function useTree(treeDataRef: Ref<TreeDataItem[]>, getFieldNames: Compute
getAllKeys,
getChildrenKeys,
getEnabledKeys,
getSelectedNode,
}
}
......@@ -126,6 +126,11 @@ export const treeProps = buildProps({
checkOnSearch: Boolean,
// 搜索完成自动select所有结果
selectedOnSearch: Boolean,
loading: {
type: Boolean,
default: false,
},
treeWrapperClassName: String,
})
export type TreeProps = ExtractPropTypes<typeof treeProps>
......@@ -133,6 +138,7 @@ export type TreeProps = ExtractPropTypes<typeof treeProps>
export interface ContextMenuItem {
label: string
icon?: string
hidden?: boolean
disabled?: boolean
handler?: Fn
divider?: boolean
......@@ -177,4 +183,5 @@ export interface TreeActionType {
updateNodeByKey: (key: string, node: Omit<TreeDataItem, 'key'>) => void
setSearchValue: (value: string) => void
getSearchValue: () => string
getSelectedNode: (key: KeyType, treeList?: TreeItem[], selectNode?: TreeItem | null) => TreeItem | null
}
@import './pagination.less';
@import './input.less';
@import './btn.less';
@import url('./pagination.less');
@import url('./input.less');
@import url('./btn.less');
.ant-image-preview-root {
img {
......@@ -57,3 +57,11 @@ span.anticon:not(.app-iconify) {
border-top: 0 !important;
border-left: 0 !important;
}
.ant-form-item-control-input-content {
> div {
> div {
max-width: 100%;
}
}
}
......@@ -2,8 +2,11 @@
// input
.ant-input {
&-number {
&-number,
&-number-group-wrapper {
min-width: 110px;
width: 100% !important;
max-width: 100%;
}
}
......
@import 'transition/index.less';
@import 'var/index.less';
@import 'public.less';
@import 'ant/index.less';
@import './theme.less';
@import url('transition/index.less');
@import url('var/index.less');
@import url('public.less');
@import url('ant/index.less');
@import url('./theme.less');
input:-webkit-autofill {
box-shadow: 0 0 0 1000px white inset !important;
......@@ -21,8 +21,8 @@ html,
body {
width: 100%;
height: 100%;
overflow: visible !important;
overflow-x: hidden !important;
overflow: visible;
overflow-x: hidden;
&.color-weak {
filter: invert(80%);
......@@ -40,10 +40,5 @@ button,
div,
svg,
span {
outline: none !important;
}
// ================ vben ================
.vben-default-layout-main {
margin-left: 0 !important;
outline: none;
}
.fade-transition {
&-enter-active,
&-leave-active {
transition: opacity 0.2s ease-in-out;
}
&-enter-from,
&-leave-to {
opacity: 0;
}
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease-in-out;
......
@import './base.less';
@import './fade.less';
@import './scale.less';
@import './slide.less';
@import './scroll.less';
@import './zoom.less';
@import url('./base.less');
@import url('./fade.less');
@import url('./scale.less');
@import url('./slide.less');
@import url('./scroll.less');
@import url('./zoom.less');
.collapse-transition {
transition: 0.2s height ease-in-out, 0.2s padding-top ease-in-out, 0.2s padding-bottom ease-in-out;
......
@import (reference) '../color.less';
@import 'easing';
@import 'breakpoint';
@import url('easing');
@import url('breakpoint');
@namespace: vben;
......
......@@ -30,14 +30,18 @@ export enum SessionTimeoutProcessingEnum {
*/
export enum PermissionModeEnum {
// role
// 角色权限
ROLE = 'ROLE',
// black
// 后端
BACK = 'BACK',
// route mapping
// 路由映射
ROUTE_MAPPING = 'ROUTE_MAPPING',
}
// Route switching animation
// Route switching animation
// 路由切换动画
export enum RouterTransitionEnum {
ZOOM_FADE = 'zoom-fade',
ZOOM_OUT = 'zoom-out',
......
/**
* @description: Request result set
*/
export enum ResultEnum {}
export enum ResultEnum {
SUCCESS = 0,
ERROR = -1,
TIMEOUT = 401,
TYPE = 'success',
}
/**
* @description: request method
......
......@@ -31,7 +31,6 @@ export function createContext<T>(context: any, key: InjectionKey<T> = Symbol(),
}
export function useContext<T>(key: InjectionKey<T>, native?: boolean): T
export function useContext<T>(key: InjectionKey<T>, defaultValue?: any, native?: boolean): T
export function useContext<T>(key: InjectionKey<T> = Symbol(), defaultValue?: any): ShallowUnwrap<T> {
return inject(key, defaultValue || {})
......
......@@ -93,8 +93,8 @@ export function useMenuSetting() {
})
const getMiniWidthNumber = computed(() => {
const { collapsedShowTitle } = appStore.getMenuSetting
return collapsedShowTitle ? SIDE_BAR_SHOW_TIT_MINI_WIDTH : SIDE_BAR_MINI_WIDTH
const { collapsedShowTitle, siderHidden } = appStore.getMenuSetting
return siderHidden ? 0 : collapsedShowTitle ? SIDE_BAR_SHOW_TIT_MINI_WIDTH : SIDE_BAR_MINI_WIDTH
})
const getCalcContentWidth = computed(() => {
......
......@@ -8,9 +8,11 @@ import { useEventListener } from '/@/hooks/event/useEventListener'
import { useBreakpoint } from '/@/hooks/event/useBreakpoint'
import echarts from '/@/utils/lib/echarts'
import { useRootSetting } from '/@/hooks/setting/useRootSetting'
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting'
export function useECharts(elRef: Ref<HTMLDivElement>, theme: 'light' | 'dark' | 'default' = 'default') {
const { getDarkMode: getSysDarkMode } = useRootSetting()
const { getCollapsed } = useMenuSetting()
const getDarkMode = computed(() => {
return theme === 'default' ? getSysDarkMode.value : theme
......@@ -76,7 +78,12 @@ export function useECharts(elRef: Ref<HTMLDivElement>, theme: 'light' | 'dark' |
}
function resize() {
chartInstance?.resize()
chartInstance?.resize({
animation: {
duration: 300,
easing: 'quadraticIn',
},
})
}
watch(
......@@ -90,6 +97,12 @@ export function useECharts(elRef: Ref<HTMLDivElement>, theme: 'light' | 'dark' |
},
)
watch(getCollapsed, (_) => {
useTimeoutFn(() => {
resizeFn()
}, 300)
})
tryOnUnmounted(() => {
if (!chartInstance) return
removeResizeFn()
......
import type { RouteLocationRaw, Router } from 'vue-router'
import { PageEnum } from '/@/enums/pageEnum'
import { isString } from '/@/utils/is'
import { unref } from 'vue'
import { useRouter } from 'vue-router'
import { REDIRECT_NAME } from '/@/router/constant'
export type RouteLocationRawEx = Omit<RouteLocationRaw, 'path'> & { path: PageEnum }
export type PathAsPageEnum<T> = T extends { path: string } ? T & { path: PageEnum } : T
export type RouteLocationRawEx = PathAsPageEnum<RouteLocationRaw>
function handleError(e: Error) {
console.error(e)
}
// page switch
/**
* page switch
*/
export function useGo(_router?: Router) {
let router
if (!_router) {
router = useRouter()
}
const { push, replace } = _router || router
function go(opt: PageEnum | RouteLocationRawEx | string = PageEnum.BASE_HOME, isReplace = false) {
const { push, replace } = _router || useRouter()
function go(opt: RouteLocationRawEx = PageEnum.BASE_HOME, isReplace = false) {
if (!opt) {
return
}
if (isString(opt)) {
isReplace ? replace(opt).catch(handleError) : push(opt).catch(handleError)
} else {
const o = opt as RouteLocationRaw
isReplace ? replace(o).catch(handleError) : push(o).catch(handleError)
}
isReplace ? replace(opt).catch(handleError) : push(opt).catch(handleError)
}
return go
}
......@@ -38,7 +31,7 @@ export function useGo(_router?: Router) {
* @description: redo current page
*/
export const useRedo = (_router?: Router) => {
const { push, currentRoute } = _router || useRouter()
const { replace, currentRoute } = _router || useRouter()
const { query, params = {}, name, fullPath } = unref(currentRoute.value)
function redo(): Promise<boolean> {
return new Promise((resolve) => {
......@@ -53,7 +46,7 @@ export const useRedo = (_router?: Router) => {
params['_redirect_type'] = 'path'
params['path'] = fullPath
}
push({ name: REDIRECT_NAME, params, query }).then(() => resolve(true))
replace({ name: REDIRECT_NAME, params, query }).then(() => resolve(true))
})
}
return redo
......
......@@ -39,6 +39,7 @@ export function usePermission() {
/**
* Reset and regain authority resource information
* 重置和重新获得权限资源信息
* @param id
*/
async function resume() {
......
......@@ -11,20 +11,26 @@
import { Tooltip } from 'ant-design-vue'
import { useI18n } from '/@/hooks/web/useI18n'
import { useFullscreen } from '@vueuse/core'
import { FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons-vue'
export default defineComponent({
name: 'FullScreen',
components: { FullscreenExitOutlined, FullscreenOutlined, Tooltip },
setup() {
const { t } = useI18n()
const { toggle, isFullscreen } = useFullscreen()
// 重新检查全屏状态
isFullscreen.value = !!(
document.fullscreenElement ||
// @ts-ignore
document.webkitFullscreenElement ||
// @ts-ignore
document.mozFullScreenElement ||
// @ts-ignore
document.msFullscreenElement
)
const getTitle = computed(() => {
return unref(isFullscreen) ? t('layout.header.tooltipExitFull') : t('layout.header.tooltipEntryFull')
})
return {
getTitle,
isFullscreen,
......
......@@ -33,25 +33,19 @@
<script lang="ts">
// components
import { Dropdown, Menu } from 'ant-design-vue'
import type { MenuInfo } from 'ant-design-vue/lib/menu/src/interface'
import { defineComponent, computed } from 'vue'
import { DOC_URL } from '/@/settings/siteSetting'
import { useUserStore } from '/@/store/modules/user'
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting'
import { useI18n } from '/@/hooks/web/useI18n'
import { useDesign } from '/@/hooks/web/useDesign'
import { useModal } from '/@/components/Modal'
import headerImg from '/@/assets/images/header.jpg'
import { propTypes } from '/@/utils/propTypes'
import { openWindow } from '/@/utils'
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent'
type MenuEvent = 'logout' | 'doc' | 'lock'
export default defineComponent({
name: 'UserDropdown',
components: {
......@@ -69,30 +63,24 @@
const { t } = useI18n()
const { getShowDoc, getUseLockPage } = useHeaderSetting()
const userStore = useUserStore()
const getUserInfo = computed(() => {
const { realName = '', avatar, desc } = userStore.getUserInfo || {}
return { realName, avatar: avatar || headerImg, desc }
})
const [register, { openModal }] = useModal()
function handleLock() {
openModal(true)
}
// login out
function handleLoginOut() {
userStore.confirmLoginOut()
}
// open doc
function openDoc() {
openWindow(DOC_URL)
}
function handleMenuClick(e: { key: MenuEvent }) {
switch (e.key) {
function handleMenuClick(e: MenuInfo) {
switch (e.key as MenuEvent) {
case 'logout':
handleLoginOut()
break
......@@ -104,7 +92,6 @@
break
}
}
return {
prefixCls,
t,
......@@ -119,7 +106,6 @@
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-header-user-dropdown';
.@{prefix-cls} {
height: @header-height;
padding: 0 0 0 10px;
......@@ -153,11 +139,9 @@
&:hover {
background-color: @header-light-bg-hover-color;
}
.@{prefix-cls}__name {
color: @text-color-base;
}
.@{prefix-cls}__desc {
color: @header-light-desc-color;
}
......
import type { Ref } from 'vue'
import { computed, unref, onMounted, nextTick, ref } from 'vue'
import { computed, unref, onMounted, nextTick } from 'vue'
import { TriggerEnum } from '/@/enums/menuEnum'
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting'
import { useDebounceFn } from '@vueuse/core'
import { useAppStore } from '/@/store/modules/app'
/**
* Handle related operations of menu events
*/
export function useSiderEvent() {
const brokenRef = ref(false)
const appStore = useAppStore()
const { getMiniWidthNumber } = useMenuSetting()
const getCollapsedWidth = computed(() => {
return unref(brokenRef) ? 0 : unref(getMiniWidthNumber)
return unref(getMiniWidthNumber)
})
function onBreakpointChange(broken: boolean) {
brokenRef.value = broken
appStore.setProjectConfig({
menuSetting: {
siderHidden: broken,
},
})
}
return { getCollapsedWidth, onBreakpointChange }
......
......@@ -181,6 +181,25 @@ html[data-theme='light'] {
}
}
.ant-tabs-dropdown-menu {
&-title-content {
display: flex;
align-items: center;
.@{prefix-cls} {
&-content__info {
width: auto;
margin-left: 0;
line-height: 28px;
}
}
}
&-item-remove {
margin-left: auto;
}
}
.multiple-tabs__dropdown {
.ant-dropdown-content {
width: 172px;
......
......@@ -17,7 +17,9 @@
<keep-alive v-if="openCache" :include="getCaches">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
<component v-else :is="Component" :key="route.fullPath" />
<div v-else :key="route.name">
<component :is="Component" :key="route.fullPath" />
</div>
</transition>
</template>
</RouterView>
......@@ -26,37 +28,27 @@
<script lang="ts">
import { computed, defineComponent, unref } from 'vue'
import FrameLayout from '/@/layouts/iframe/index.vue'
import { useRootSetting } from '/@/hooks/setting/useRootSetting'
import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting'
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting'
import { getTransitionName } from './transition'
import { useMultipleTabStore } from '/@/store/modules/multipleTab'
export default defineComponent({
name: 'PageLayout',
components: { FrameLayout },
setup() {
const { getShowMultipleTab } = useMultipleTabSetting()
const tabStore = useMultipleTabStore()
const { getOpenKeepAlive, getCanEmbedIFramePage } = useRootSetting()
const { getBasicTransition, getEnableTransition } = useTransitionSetting()
const openCache = computed(() => unref(getOpenKeepAlive) && unref(getShowMultipleTab))
const getCaches = computed((): string[] => {
if (!unref(getOpenKeepAlive)) {
return []
}
return tabStore.getCachedTabList
})
return {
getTransitionName,
openCache,
......
......@@ -3,7 +3,7 @@ export default {
header: {
// user dropdown
dropdownItemDoc: 'Document',
dropdownItemLoginOut: 'Login Out',
dropdownItemLoginOut: 'Log Out',
tooltipErrorLog: 'Error log',
tooltipLock: 'Lock screen',
......
......@@ -111,6 +111,7 @@ export default {
dynamicForm: 'Dynamic',
customerForm: 'Custom',
appendForm: 'Append',
tabsForm: 'TabsForm',
},
iframe: {
frame: 'External',
......
......@@ -2,6 +2,7 @@ export default {
api: {
operationFailed: 'Operation failed',
errorTip: 'Error Tip',
successTip: 'Success Tip',
errorMessage: 'The operation failed, the system is abnormal!',
timeoutMessage: 'Login timed out, please log in again!',
apiTimeoutMessage: 'The interface request timed out, please refresh the page and try again!',
......@@ -85,7 +86,7 @@ export default {
loginSuccessDesc: 'Welcome back',
// placeholder
accountPlaceholder: 'Please input account',
accountPlaceholder: 'Please input username',
passwordPlaceholder: 'Please input password',
smsPlaceholder: 'Please input sms code',
mobilePlaceholder: 'Please input mobile',
......
export default {
lang: {
shortWeekDays: ['一', '二', '三', '四', '五', '六', '日'],
shortMonths: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
},
}
......@@ -107,6 +107,7 @@ export default {
dynamicForm: '动态表单',
customerForm: '自定义组件',
appendForm: '表单增删示例',
tabsForm: '标签页+多级field',
},
iframe: {
frame: '外部页面',
......@@ -162,13 +163,9 @@ export default {
},
example: {
example: '示例页',
api: 'API 示例',
hideLayout: '测试 - 隐藏布局',
fullScreen: '测试 - 自动全屏',
mapMars3d: 'Mars3d',
},
system: {
......
......@@ -2,6 +2,7 @@ export default {
api: {
operationFailed: '操作失败',
errorTip: '错误提示',
successTip: '成功提示',
errorMessage: '操作失败,系统异常!',
timeoutMessage: '登录超时,请重新登录!',
apiTimeoutMessage: '接口请求超时,请刷新页面重试!',
......@@ -21,11 +22,7 @@ export default {
errMsg504: '网络超时!',
errMsg505: 'http版本不支持该请求!',
},
app: {
logoutTip: '温馨提醒',
logoutMessage: '是否确认退出系统?',
menuLoading: '菜单加载中...',
},
app: { logoutTip: '温馨提醒', logoutMessage: '是否确认退出系统?', menuLoading: '菜单加载中...' },
errorLog: {
tableTitle: '错误日志列表',
tableColumnType: '类型',
......
......@@ -16,6 +16,12 @@ import { setupI18n } from '/@/locales/setupI18n'
import { setupAppConfig } from '/@/config/app'
import { registerGlobComp } from '/@/components/registerGlobComp'
import { isDevMode } from './utils/env'
if (isDevMode()) {
import('ant-design-vue/es/style')
}
async function bootstrap() {
const app = createApp(App)
......@@ -23,28 +29,37 @@ async function bootstrap() {
setupAppConfig()
// Configure store
// 配置 store
setupStore(app)
// Initialize internal system configuration
// 初始化内部系统配置
initAppConfigStore()
// Register global components
// 注册全局组件
registerGlobComp(app)
// Multilingual configuration
// 多语言配置
// Asynchronous case: language files may be obtained from the server side
// 异步案例:语言文件可能从服务器端获取
await setupI18n(app)
// Configure routing
// 配置路由
setupRouter(app)
// router-guard
// 路由守卫
setupRouterGuard(router)
// Register global directive
// 注册全局指令
setupGlobDirectives(app)
// Configure global error handling
// 配置全局错误处理
setupErrorHandle(app)
// https://next.router.vuejs.org/api/#isready
......
......@@ -18,7 +18,7 @@ export const getParentLayout = (_name?: string) => {
return () =>
new Promise((resolve) => {
resolve({
name: PARENT_LAYOUT_NAME,
name: _name || PARENT_LAYOUT_NAME,
})
})
}
......@@ -11,14 +11,18 @@ export function getAllParentPath<T = Recordable>(treeData: T[], path: string) {
return (menuList || []).map((item) => item.path)
}
// 路径处理
function joinParentPath(menus: Menu[], parentPath = '') {
for (let index = 0; index < menus.length; index++) {
const menu = menus[index]
// https://next.router.vuejs.org/guide/essentials/nested-routes.html
// Note that nested paths that start with / will be treated as a root path.
// 请注意,以 / 开头的嵌套路径将被视为根路径。
// This allows you to leverage the component nesting without having to use a nested URL.
// 这允许你利用组件嵌套,而无需使用嵌套 URL。
if (!(menu.path.startsWith('/') || isUrl(menu.path))) {
// path doesn't start with /, nor is it a url, join parent path
// 路径不以 / 开头,也不是 url,加入父路径
menu.path = `${parentPath}/${menu.path}`
}
if (menu?.children?.length) {
......@@ -37,14 +41,18 @@ export function transformMenuModule(menuModule: MenuModule): Menu {
return menuList[0]
}
// 将路由转换成菜单
export function transformRouteToMenu(routeModList: AppRouteModule[], routerMapping = false) {
// 借助 lodash 深拷贝
const cloneRouteModList = cloneDeep(routeModList)
const routeList: AppRouteRecordRaw[] = []
// 对路由项进行修改
cloneRouteModList.forEach((item) => {
if (routerMapping && item.meta.hideChildrenInMenu && typeof item.redirect === 'string') {
item.path = item.redirect
}
if (item.meta?.single) {
const realItem = item?.children?.[0]
realItem && routeList.push(realItem)
......@@ -52,6 +60,7 @@ export function transformRouteToMenu(routeModList: AppRouteModule[], routerMappi
routeList.push(item)
}
})
// 提取树指定结构
const list = treeMap(routeList, {
conversion: (node: AppRouteRecordRaw) => {
const { meta: { title, hideMenu = false } = {} } = node
......@@ -66,6 +75,7 @@ export function transformRouteToMenu(routeModList: AppRouteModule[], routerMappi
}
},
})
// 路径处理
joinParentPath(list)
return cloneDeep(list)
}
......@@ -74,6 +84,7 @@ export function transformRouteToMenu(routeModList: AppRouteModule[], routerMappi
* config menu with given params
*/
const menuParamRegex = /(?::)([\s\S]+?)((?=\/)|$)/g
export function configureDynamicParamsMenu(menu: Menu, params: RouteParams) {
const { path, paramPath } = toRaw(menu)
let realPath = paramPath ? paramPath : path
......
......@@ -65,6 +65,7 @@ function dynamicImport(dynamicViewsModules: Record<string, () => Promise<Recorda
}
// Turn background objects into routing objects
// 将背景对象变成路由对象
export function transformObjToRoute<T = AppRouteModule>(routeList: AppRouteModule[]): T[] {
routeList.forEach((route) => {
const component = route.component as string
......@@ -91,35 +92,46 @@ export function transformObjToRoute<T = AppRouteModule>(routeList: AppRouteModul
/**
* Convert multi-level routing to level 2 routing
* 将多级路由转换为 2 级路由
*/
export function flatMultiLevelRoutes(routeModules: AppRouteModule[]) {
const modules: AppRouteModule[] = cloneDeep(routeModules)
for (let index = 0; index < modules.length; index++) {
const routeModule = modules[index]
// 判断级别是否 多级 路由
if (!isMultipleRoute(routeModule)) {
// 声明终止当前循环, 即跳过此次循环,进行下一轮
continue
}
// 路由等级提升
promoteRouteLevel(routeModule)
}
return modules
}
// Routing level upgrade
// 路由等级提升
function promoteRouteLevel(routeModule: AppRouteModule) {
// Use vue-router to splice menus
// 使用vue-router拼接菜单
// createRouter 创建一个可以被 Vue 应用程序使用的路由实例
let router: Router | null = createRouter({
routes: [routeModule as unknown as RouteRecordNormalized],
history: createWebHashHistory(),
})
// getRoutes: 获取所有 路由记录的完整列表。
const routes = router.getRoutes()
// 将所有子路由添加到二级路由
addToChildren(routes, routeModule.children || [], routeModule)
router = null
// omit lodash的函数 对传入的item对象的children进行删除
routeModule.children = routeModule.children?.map((item) => omit(item, 'children'))
}
// Add all sub-routes to the secondary route
// 将所有子路由添加到二级路由
function addToChildren(routes: RouteRecordNormalized[], children: AppRouteRecordRaw[], routeModule: AppRouteModule) {
for (let index = 0; index < children.length; index++) {
const child = children[index]
......@@ -138,7 +150,9 @@ function addToChildren(routes: RouteRecordNormalized[], children: AppRouteRecord
}
// Determine whether the level exceeds 2 levels
// 判断级别是否超过2级
function isMultipleRoute(routeModule: AppRouteModule) {
// Reflect.has 与 in 操作符 相同, 用于检查一个对象(包括它原型链上)是否拥有某个属性
if (!routeModule || !Reflect.has(routeModule, 'children') || !routeModule.children?.length) {
return false
}
......
......@@ -14,9 +14,13 @@ const getRouteNames = (array: any[]) =>
getRouteNames(basicRoutes)
// app router
// 创建一个可以被 Vue 应用程序使用的路由实例
export const router = createRouter({
// 创建一个 hash 历史记录。
history: createWebHashHistory(import.meta.env.VITE_PUBLIC_PATH),
// 应该添加到路由的初始路由列表。
routes: basicRoutes as unknown as RouteRecordRaw[],
// 是否应该禁止尾部斜杠。默认为假
strict: true,
scrollBehavior: () => ({ left: 0, top: 0 }),
})
......@@ -32,6 +36,7 @@ export function resetRouter() {
}
// config router
// 配置路由器
export function setupRouter(app: App<Element>) {
app.use(router)
}
......@@ -2,6 +2,7 @@ import type { AppRouteRecordRaw, AppRouteModule } from '/@/router/types'
import { PAGE_NOT_FOUND_ROUTE, REDIRECT_ROUTE } from '/@/router/routes/basic'
import { mainOutRoutes } from './mainOut'
import { PageEnum } from '/@/enums/pageEnum'
import { t } from '/@/hooks/web/useI18n'
......@@ -9,6 +10,7 @@ const modules = import.meta.glob('./modules/**/*.ts', { eager: true })
const routeModuleList: AppRouteModule[] = []
// 加入到路由集合中
Object.keys(modules).forEach((key) => {
const mod = (modules as Recordable)[key].default || {}
const modList = Array.isArray(mod) ? [...mod] : [mod]
......@@ -17,6 +19,7 @@ Object.keys(modules).forEach((key) => {
export const asyncRoutes = [PAGE_NOT_FOUND_ROUTE, ...routeModuleList]
// 根路由
export const RootRoute: AppRouteRecordRaw = {
path: '/',
name: 'Root',
......@@ -36,4 +39,5 @@ export const LoginRoute: AppRouteRecordRaw = {
}
// Basic routing without permission
export const basicRoutes = [LoginRoute, RootRoute, REDIRECT_ROUTE, PAGE_NOT_FOUND_ROUTE]
// 未经许可的基本路由
export const basicRoutes = [LoginRoute, RootRoute, ...mainOutRoutes, REDIRECT_ROUTE, PAGE_NOT_FOUND_ROUTE]
/**
The routing of this file will not show the layout.
It is an independent new page.
the contents of the file still need to log in to access
*/
import type { AppRouteModule } from '/@/router/types'
// test
// http:ip:port/main-out
export const mainOutRoutes: AppRouteModule[] = [
{
path: '/main-out',
name: 'MainOut',
component: () => import('/@/views/demo/main-out/index.vue'),
meta: {
title: 'MainOut',
ignoreAuth: true,
},
},
]
export const mainOutRouteNames = mainOutRoutes.map((item) => item.name)
......@@ -3,7 +3,7 @@ import type { AppRouteModule } from '/@/router/types'
import { LAYOUT } from '/@/router/constant'
import { t } from '/@/hooks/web/useI18n'
const dashboard: AppRouteModule = {
const about: AppRouteModule = {
path: '/about',
name: 'About',
component: LAYOUT,
......@@ -28,4 +28,4 @@ const dashboard: AppRouteModule = {
],
}
export default dashboard
export default about
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论