fix: 一些基本功能
This commit is contained in:
77
apps/web-antd/src/components/description/description.vue
Normal file
77
apps/web-antd/src/components/description/description.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<script setup lang="ts">
|
||||
import type { DescItem } from './types';
|
||||
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import { Descriptions, DescriptionsItem } from 'ant-design-vue';
|
||||
|
||||
import { componentMap } from '#/components/view/component-map';
|
||||
|
||||
defineProps({
|
||||
title: { type: String, default: '' },
|
||||
bordered: { type: Boolean, default: true },
|
||||
size: {
|
||||
type: String as PropType<'default' | 'middle' | 'small'>,
|
||||
default: undefined,
|
||||
},
|
||||
column: {
|
||||
type: [Number, Object],
|
||||
default: () => {
|
||||
// return { xxl: 4, xl: 3, lg: 3, md: 3, sm: 2, xs: 1 };
|
||||
return 12;
|
||||
},
|
||||
},
|
||||
labelStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
width: '120px',
|
||||
};
|
||||
},
|
||||
},
|
||||
contentStyle: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
width: '0px',
|
||||
};
|
||||
},
|
||||
},
|
||||
schema: {
|
||||
type: Array as PropType<DescItem[]>,
|
||||
default: () => [],
|
||||
},
|
||||
data: { type: Object, default: undefined },
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Descriptions
|
||||
:bordered="bordered"
|
||||
:column="column"
|
||||
:content-style="contentStyle"
|
||||
:label-style="labelStyle"
|
||||
:size="size"
|
||||
:title="title ? title : undefined"
|
||||
>
|
||||
<template v-for="item in schema" :key="item.field">
|
||||
<DescriptionsItem :label="item.label" :span="item.span">
|
||||
<component
|
||||
:is="(componentMap as Map<String, any>).get(item.component)"
|
||||
v-if="(componentMap as Map<String, any>).has(item.component)"
|
||||
:value="data?.[item.field]"
|
||||
v-bind="{ ...item.componentProps }"
|
||||
/>
|
||||
<component
|
||||
:is="item.render(data?.[item.field], data)"
|
||||
v-else-if="
|
||||
!(componentMap as Map<String, any>).has(item.component) &&
|
||||
item.render
|
||||
"
|
||||
:value="data?.[item.field]"
|
||||
v-bind="{ ...item.componentProps }"
|
||||
/>
|
||||
<template v-else>{{ data?.[item.field] }}</template>
|
||||
</DescriptionsItem>
|
||||
</template>
|
||||
</Descriptions>
|
||||
</template>
|
||||
2
apps/web-antd/src/components/description/index.ts
Normal file
2
apps/web-antd/src/components/description/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as Description } from './description.vue';
|
||||
export type * from './types';
|
||||
54
apps/web-antd/src/components/description/types.d.ts
vendored
Normal file
54
apps/web-antd/src/components/description/types.d.ts
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { CollapseContainerOptions } from '@/components/Container';
|
||||
import type { DescriptionsProps } from 'ant-design-vue/es/descriptions';
|
||||
|
||||
import type { CSSProperties, VNode } from 'vue';
|
||||
|
||||
export interface DescItem {
|
||||
labelMinWidth?: number;
|
||||
contentMinWidth?: number;
|
||||
labelStyle?: CSSProperties;
|
||||
field: string;
|
||||
label: JSX.Element | string | VNode;
|
||||
// Merge column
|
||||
span?: number;
|
||||
show?: (...arg: any) => boolean;
|
||||
// render
|
||||
render?: (
|
||||
val: any,
|
||||
data: Recordable,
|
||||
) => Element | JSX.Element | number | string | undefined | VNode;
|
||||
component: string;
|
||||
componentProps?: any;
|
||||
children?: DescItem[];
|
||||
}
|
||||
|
||||
export interface DescriptionProps extends DescriptionsProps {
|
||||
// Whether to include the collapse component
|
||||
useCollapse?: boolean;
|
||||
/**
|
||||
* item configuration
|
||||
* @type DescItem
|
||||
*/
|
||||
schema: DescItem[];
|
||||
/**
|
||||
* 数据
|
||||
* @type object
|
||||
*/
|
||||
data: Recordable;
|
||||
/**
|
||||
* Built-in CollapseContainer component configuration
|
||||
* @type CollapseContainerOptions
|
||||
*/
|
||||
collapseOptions?: CollapseContainerOptions;
|
||||
}
|
||||
|
||||
export interface DescInstance {
|
||||
setDescProps(descProps: Partial<DescriptionProps>): void;
|
||||
}
|
||||
|
||||
export type Register = (descInstance: DescInstance) => void;
|
||||
|
||||
/**
|
||||
* @description:
|
||||
*/
|
||||
export type UseDescReturnType = [Register, DescInstance];
|
||||
@@ -0,0 +1,104 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, markRaw, onMounted, watch } from 'vue';
|
||||
|
||||
import { useVbenForm } from '@vben/common-ui';
|
||||
|
||||
import { Spin } from 'ant-design-vue';
|
||||
|
||||
import { Description } from '#/components/description';
|
||||
import { useSchemaStore } from '#/store/schema';
|
||||
import { schemaToDetailForm } from '#/util/tool';
|
||||
|
||||
const props = defineProps({
|
||||
tableName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
view: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
addFieldPrefix: {
|
||||
// 追加字段前辍
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
const schemaStore = useSchemaStore();
|
||||
const schema = computed(() => {
|
||||
return schemaStore.getSchema(props.tableName);
|
||||
});
|
||||
const schemaDescItems = computed(() => {
|
||||
return schemaToDetailForm(schema.value, props.data);
|
||||
});
|
||||
|
||||
const formObj = computed(() => {
|
||||
const [Form, api] = useVbenForm({
|
||||
showDefaultActions: false,
|
||||
...schema.value,
|
||||
});
|
||||
return {
|
||||
form: Form,
|
||||
api,
|
||||
};
|
||||
});
|
||||
const cData = computed(() => {
|
||||
const newData: any = {};
|
||||
Object.keys(props.data || {}).forEach((key: string) => {
|
||||
// 如果key不以addFieldPrefix为前辍,则自动追加
|
||||
if (key.startsWith(props.addFieldPrefix)) {
|
||||
newData[key] = props.data[key];
|
||||
} else {
|
||||
newData[props.addFieldPrefix + key] = props.data[key];
|
||||
}
|
||||
});
|
||||
return newData;
|
||||
});
|
||||
watch(
|
||||
() => props.tableName,
|
||||
() => {
|
||||
schemaStore.requestData(props.tableName);
|
||||
},
|
||||
);
|
||||
onMounted(() => {
|
||||
schemaStore.requestData(props.tableName);
|
||||
});
|
||||
defineExpose({
|
||||
getForm() {
|
||||
return markRaw(formObj.value?.form);
|
||||
},
|
||||
getFormApi() {
|
||||
return formObj.value?.api;
|
||||
},
|
||||
});
|
||||
const spinning = computed(() => {
|
||||
return Object.keys(schema.value).length === 0;
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Spin :spinning="spinning">
|
||||
<div v-if="view" class="desc-wrap">
|
||||
<div v-for="item in schemaDescItems" :key="item.field" class="desc-card">
|
||||
<Description
|
||||
:data="cData"
|
||||
:label-style="{
|
||||
width: '120px',
|
||||
}"
|
||||
:schema="item.children"
|
||||
:title="schemaDescItems.length === 1 ? undefined : item.label"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Spin v-else :spinning="spinning">
|
||||
<component :is="markRaw(formObj?.form)" />
|
||||
</Spin>
|
||||
</Spin>
|
||||
</template>
|
||||
<style scoped lang="less">
|
||||
@import '#/assets/styles/common-form.less';
|
||||
@import '#/assets/styles/common-detail.less';
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as DynamicSchemaForm } from './dynamic-schema-form.vue';
|
||||
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import type { VbenFormProps } from '@vben/common-ui';
|
||||
|
||||
import { onMounted, type PropType, ref, watch } from 'vue';
|
||||
|
||||
import { schemaToDetailForm } from '#/util/tool';
|
||||
|
||||
import { type DescItem, Description } from '../description';
|
||||
|
||||
const props = defineProps({
|
||||
formOptions: {
|
||||
type: Object as PropType<VbenFormProps>,
|
||||
default: () => ({}),
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
descriptionProps: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const schemaDescItems = ref<DescItem[]>([]);
|
||||
const initialSchemaDescItems = () => {
|
||||
if (
|
||||
Object.keys(props.formOptions).length > 0 &&
|
||||
Object.keys(props.data).length > 0
|
||||
) {
|
||||
schemaDescItems.value = schemaToDetailForm(props.formOptions, props.data);
|
||||
}
|
||||
};
|
||||
onMounted(() => {
|
||||
initialSchemaDescItems();
|
||||
});
|
||||
watch(
|
||||
() => props.data,
|
||||
() => {
|
||||
initialSchemaDescItems();
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
watch(
|
||||
() => props.formOptions,
|
||||
() => {
|
||||
initialSchemaDescItems();
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div class="desc-wrap">
|
||||
<div
|
||||
v-for="schema in schemaDescItems"
|
||||
:key="schema.field"
|
||||
class="desc-card"
|
||||
>
|
||||
<Description
|
||||
:data="data"
|
||||
:label-style="{
|
||||
width: '150px',
|
||||
}"
|
||||
:schema="schema.children"
|
||||
:title="schemaDescItems.length === 1 ? undefined : schema.label"
|
||||
v-bind="descriptionProps"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="less" scoped></style>
|
||||
1
apps/web-antd/src/components/form-description/index.ts
Normal file
1
apps/web-antd/src/components/form-description/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as FormDescription } from './form-description.vue';
|
||||
32
apps/web-antd/src/components/form/component-map.ts
Normal file
32
apps/web-antd/src/components/form/component-map.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { CustomComponentType } from './types';
|
||||
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import { getFileNameWithoutExtension, toPascalCase } from '#/util/tool';
|
||||
|
||||
const componentMap = new Map<CustomComponentType | string, Component>();
|
||||
// import.meta.glob() 直接引入所有的模块 Vite 独有的功能
|
||||
const modules = import.meta.glob(
|
||||
['./components/**/*.vue', '../../views/**/components/form/*.vue'],
|
||||
{ eager: true },
|
||||
);
|
||||
// 加入到路由集合中
|
||||
Object.keys(modules).forEach((key) => {
|
||||
if (!key.includes('-ignore')) {
|
||||
const mod = (modules as any)[key].default || {};
|
||||
// ./components/ApiDict.vue
|
||||
// 获取ApiDict
|
||||
const compName = getFileNameWithoutExtension(key);
|
||||
componentMap.set(toPascalCase(compName), mod);
|
||||
}
|
||||
});
|
||||
/**
|
||||
* 注册组件
|
||||
* @param components
|
||||
*/
|
||||
export const registerComponent = (components: any) => {
|
||||
componentMap.forEach((value, key) => {
|
||||
components[key] = value as Component;
|
||||
});
|
||||
};
|
||||
export { componentMap };
|
||||
62
apps/web-antd/src/components/form/components/avatar.vue
Normal file
62
apps/web-antd/src/components/form/components/avatar.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Upload } from 'ant-design-vue';
|
||||
|
||||
import { uploadFile } from '#/api/core/upload';
|
||||
import { Icon } from '#/components/icon';
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
const emits = defineEmits(['update:value']);
|
||||
const mValue = useVModel(props, 'value', emits, {
|
||||
defaultValue: props.value,
|
||||
passive: true,
|
||||
});
|
||||
const customRequest = (e: any) => {
|
||||
uploadFile({
|
||||
file: e.file,
|
||||
}).then((data: any) => {
|
||||
mValue.value = data.url;
|
||||
});
|
||||
};
|
||||
const handleRemove = (e: Event) => {
|
||||
e.stopPropagation();
|
||||
mValue.value = '';
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<Upload
|
||||
:custom-request="customRequest"
|
||||
:show-upload-list="false"
|
||||
list-type="picture-card"
|
||||
>
|
||||
<div v-if="mValue" class="m-avatar-wrap">
|
||||
<Icon
|
||||
class="m-avatar-icon-delete"
|
||||
icon="ant-design:delete-outlined"
|
||||
@click="handleRemove"
|
||||
/>
|
||||
<img :src="value" width="100%" />
|
||||
</div>
|
||||
<Icon v-else icon="ant-design:plus-outlined" />
|
||||
</Upload>
|
||||
</template>
|
||||
<style lang="less" scoped>
|
||||
.m-avatar-wrap {
|
||||
position: relative;
|
||||
height: 102px;
|
||||
.m-avatar-icon-delete {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
162
apps/web-antd/src/components/form/components/upload.vue
Normal file
162
apps/web-antd/src/components/form/components/upload.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, type PropType, ref, useAttrs, watch } from 'vue';
|
||||
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
import {
|
||||
message,
|
||||
Upload,
|
||||
type UploadChangeParam,
|
||||
type UploadFile,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { getFileInfoByIds } from '#/api/core/upload';
|
||||
import { Icon } from '#/components/icon';
|
||||
import { omit } from '#/util/tool';
|
||||
|
||||
defineOptions({
|
||||
name: 'Upload',
|
||||
inheritAttrs: false,
|
||||
});
|
||||
const props = defineProps({
|
||||
value: {
|
||||
// 文件id,多个使用英文逗号分割
|
||||
type: [String] as PropType<string>,
|
||||
default: undefined,
|
||||
},
|
||||
action: {
|
||||
// 上传的地址
|
||||
type: String,
|
||||
default: `${import.meta.env.VITE_GLOB_API_URL}/sys/fileInfo/upload`,
|
||||
},
|
||||
headers: {
|
||||
// 上传请求的 headers
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
disabled: {
|
||||
// 是否禁用
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
listType: {
|
||||
// 上传类型
|
||||
type: String as PropType<'picture' | 'picture-card' | 'text'>,
|
||||
default: 'text',
|
||||
},
|
||||
maxCount: {
|
||||
// 最大上传数量
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
uploadName: {
|
||||
// 文件上传字段名,name被字段名覆写了,要换成uploadName
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
separator: {
|
||||
// 文件id分隔符
|
||||
type: String,
|
||||
default: ',',
|
||||
},
|
||||
});
|
||||
const emits = defineEmits(['update:modelValue']);
|
||||
const accessStore = useAccessStore();
|
||||
const fileList = ref<UploadFile[]>([]);
|
||||
const attrs = useAttrs();
|
||||
const bindProps = computed(() => {
|
||||
return {
|
||||
...omit(attrs, ['onChange', 'onInput', 'onBlur', 'name']),
|
||||
...props,
|
||||
name: props.uploadName || 'file',
|
||||
};
|
||||
});
|
||||
const _getFileInfoByIds = (val: string | string[] | undefined) => {
|
||||
if (!val) return;
|
||||
getFileInfoByIds({
|
||||
fileInfoIds: val,
|
||||
}).then((res) => {
|
||||
fileList.value = res || [];
|
||||
});
|
||||
};
|
||||
const mValue = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(val) {
|
||||
emits('update:modelValue', val);
|
||||
},
|
||||
});
|
||||
const mHeaders = {
|
||||
Authorization: `Bearer ${accessStore.accessToken}`,
|
||||
...props.headers,
|
||||
};
|
||||
const handleChange = (info: UploadChangeParam) => {
|
||||
if (info.file.status === 'done') {
|
||||
// 更新file-list当前file的uid
|
||||
fileList.value = fileList.value.map((item) => {
|
||||
if (item.uid === info.file.uid) {
|
||||
return {
|
||||
...item,
|
||||
uid: info.file.response.data.fileInfoId,
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
mValue.value = fileList.value
|
||||
.map((item) => {
|
||||
return item.uid;
|
||||
})
|
||||
.join(props.separator);
|
||||
} else if (info.file.status === 'error') {
|
||||
message.error(`${info.file.name} file upload failed.`);
|
||||
}
|
||||
};
|
||||
const handleRemove = (file: any) => {
|
||||
mValue.value = fileList.value
|
||||
.filter((item: any) => item.uid !== file.uid)
|
||||
.map((item: any) => item.uid)
|
||||
.join(props.separator);
|
||||
return true;
|
||||
};
|
||||
watch(
|
||||
() => props.value,
|
||||
(val) => {
|
||||
_getFileInfoByIds(val);
|
||||
},
|
||||
);
|
||||
onMounted(() => {
|
||||
_getFileInfoByIds(props.value);
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Upload
|
||||
v-model:file-list="fileList"
|
||||
v-bind="bindProps"
|
||||
:action="action"
|
||||
:disabled="disabled"
|
||||
:headers="mHeaders"
|
||||
:list-type="listType"
|
||||
:max-count="maxCount"
|
||||
@change="handleChange"
|
||||
@remove="handleRemove"
|
||||
>
|
||||
<template #default>
|
||||
<Icon
|
||||
v-if="listType === 'picture-card'"
|
||||
:size="24"
|
||||
icon="ant-design:plus-outlined"
|
||||
/>
|
||||
<Button v-else>
|
||||
上传文件
|
||||
<template #icon>
|
||||
<Icon icon="ant-design:cloud-upload-outlined" />
|
||||
</template>
|
||||
</Button>
|
||||
</template>
|
||||
<template v-for="item in Object.keys($slots)" #[item]="data">
|
||||
<slot :name="item" v-bind="data || {}"></slot>
|
||||
</template>
|
||||
</Upload>
|
||||
</template>
|
||||
7
apps/web-antd/src/components/form/index.ts
Normal file
7
apps/web-antd/src/components/form/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// export { default as ApiCheckboxGroup } from './components/api-checkbox-group.vue';
|
||||
// export { default as ApiDict } from './components/api-dict.vue';
|
||||
// export { default as ApiRadioGroup } from './components/api-radio-group.vue';
|
||||
// export { default as ApiSelect } from './components/api-select.vue';
|
||||
// export { default as ApiTreeSelect } from './components/api-tree-select.vue';
|
||||
// export { default as IconPicker } from './components/icon-picker.vue';
|
||||
export { default as Upload } from './components/upload.vue';
|
||||
7
apps/web-antd/src/components/form/types/index.d.ts
vendored
Normal file
7
apps/web-antd/src/components/form/types/index.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
export type CustomComponentType =
|
||||
| 'ApiCheckboxGroup'
|
||||
| 'ApiDict'
|
||||
| 'ApiRadioGroup'
|
||||
| 'ApiSelect'
|
||||
| 'ApiTreeSelect'
|
||||
| 'IconPicker';
|
||||
51
apps/web-antd/src/components/icon/icon.vue
Normal file
51
apps/web-antd/src/components/icon/icon.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, h } from 'vue';
|
||||
|
||||
import { IconifyIcon as VbenIcon } from '@vben/icons';
|
||||
|
||||
const props = defineProps({
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: '16px',
|
||||
},
|
||||
});
|
||||
const iconComp = computed(() => {
|
||||
if (props.icon.startsWith('http')) {
|
||||
return () => h('img', { src: props.icon, class: 'm-icon__' });
|
||||
}
|
||||
return '';
|
||||
});
|
||||
const styles = computed(() => {
|
||||
return {
|
||||
fontSize: props.size.toString().endsWith('px')
|
||||
? props.size
|
||||
: `${props.size}px`,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="iconComp" v-if="iconComp" :style="styles" />
|
||||
<VbenIcon v-else :icon="props.icon" :style="styles" class="m-icon__" />
|
||||
</template>
|
||||
<style lang="less" scoped>
|
||||
.m-icon__ {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
font-style: normal;
|
||||
line-height: 0;
|
||||
color: inherit;
|
||||
text-align: center;
|
||||
text-transform: none;
|
||||
vertical-align: -0.125em;
|
||||
text-rendering: optimizelegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
</style>
|
||||
1
apps/web-antd/src/components/icon/index.ts
Normal file
1
apps/web-antd/src/components/icon/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Icon } from './icon.vue';
|
||||
2
apps/web-antd/src/components/table-action/index.ts
Normal file
2
apps/web-antd/src/components/table-action/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as TableAction } from './table-action.vue';
|
||||
export type * from './types.d.ts';
|
||||
225
apps/web-antd/src/components/table-action/table-action.vue
Normal file
225
apps/web-antd/src/components/table-action/table-action.vue
Normal file
@@ -0,0 +1,225 @@
|
||||
<script setup lang="ts">
|
||||
import type { ButtonType } from 'ant-design-vue/es/button';
|
||||
|
||||
import type { ActionItem, PopConfirm } from './types';
|
||||
|
||||
import { computed, type PropType, toRaw } from 'vue';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { isBoolean, isFunction } from '@vben/utils';
|
||||
|
||||
import { Button, Dropdown, Menu, Popconfirm, Space } from 'ant-design-vue';
|
||||
|
||||
import { Icon } from '#/components/icon';
|
||||
|
||||
const props = defineProps({
|
||||
actions: {
|
||||
type: Array as PropType<ActionItem[]>,
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
dropDownActions: {
|
||||
type: Array as PropType<ActionItem[]>,
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
divider: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const MenuItem = Menu.Item;
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
function isIfShow(action: ActionItem): boolean {
|
||||
const ifShow = action.ifShow;
|
||||
|
||||
let isIfShow = true;
|
||||
|
||||
if (isBoolean(ifShow)) {
|
||||
isIfShow = ifShow;
|
||||
}
|
||||
if (isFunction(ifShow)) {
|
||||
isIfShow = ifShow(action);
|
||||
}
|
||||
return isIfShow;
|
||||
}
|
||||
|
||||
const getActions = computed(() => {
|
||||
return (toRaw(props.actions) || [])
|
||||
.filter((action) => {
|
||||
return (
|
||||
(hasAccessByCodes(action.auth || []) ||
|
||||
(action.auth || []).length === 0) &&
|
||||
isIfShow(action)
|
||||
);
|
||||
})
|
||||
.map((action) => {
|
||||
const { popConfirm } = action;
|
||||
return {
|
||||
// getPopupContainer: document.body,
|
||||
type: 'link' as ButtonType,
|
||||
...action,
|
||||
...popConfirm,
|
||||
onConfirm: popConfirm?.confirm,
|
||||
onCancel: popConfirm?.cancel,
|
||||
enable: !!popConfirm,
|
||||
};
|
||||
});
|
||||
});
|
||||
const getDropdownList = computed((): any[] => {
|
||||
return (toRaw(props.dropDownActions) || [])
|
||||
.filter((action) => {
|
||||
return (
|
||||
(hasAccessByCodes(action.auth || []) ||
|
||||
(action.auth || []).length === 0) &&
|
||||
isIfShow(action)
|
||||
);
|
||||
})
|
||||
.map((action, index) => {
|
||||
const { label, popConfirm } = action;
|
||||
return {
|
||||
...action,
|
||||
...popConfirm,
|
||||
onConfirm: popConfirm?.confirm,
|
||||
onCancel: popConfirm?.cancel,
|
||||
text: label,
|
||||
divider:
|
||||
index < props.dropDownActions.length - 1 ? props.divider : false,
|
||||
};
|
||||
});
|
||||
});
|
||||
const getPopConfirmProps = (attrs: PopConfirm) => {
|
||||
const originAttrs: any = attrs;
|
||||
delete originAttrs.icon;
|
||||
if (attrs.confirm && isFunction(attrs.confirm)) {
|
||||
originAttrs.onConfirm = attrs.confirm;
|
||||
delete originAttrs.confirm;
|
||||
}
|
||||
if (attrs.cancel && isFunction(attrs.cancel)) {
|
||||
originAttrs.onCancel = attrs.cancel;
|
||||
delete originAttrs.cancel;
|
||||
}
|
||||
return originAttrs;
|
||||
};
|
||||
const getButtonProps = (action: ActionItem) => {
|
||||
const res = {
|
||||
type: action.type || 'primary',
|
||||
...action,
|
||||
};
|
||||
delete res.icon;
|
||||
return res;
|
||||
};
|
||||
const handleMenuClick = (e: any) => {
|
||||
const action = getDropdownList.value[e.key];
|
||||
if (action.onClick && isFunction(action.onClick)) {
|
||||
action.onClick();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="m-table-action">
|
||||
<Space
|
||||
:size="
|
||||
getActions?.some((item: ActionItem) => item.type === 'link') ? 0 : 8
|
||||
"
|
||||
>
|
||||
<template v-for="(action, index) in getActions" :key="index">
|
||||
<Popconfirm
|
||||
v-if="action.popConfirm"
|
||||
v-bind="getPopConfirmProps(action.popConfirm)"
|
||||
>
|
||||
<template v-if="action.popConfirm.icon" #icon>
|
||||
<Icon :icon="action.popConfirm.icon" />
|
||||
</template>
|
||||
<Button v-bind="getButtonProps(action)">
|
||||
<template v-if="action.icon" #icon>
|
||||
<Icon :icon="action.icon" />
|
||||
</template>
|
||||
{{ action.label }}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<Button v-else v-bind="getButtonProps(action)" @click="action.onClick">
|
||||
<template v-if="action.icon" #icon>
|
||||
<Icon :icon="action.icon" />
|
||||
</template>
|
||||
{{ action.label }}
|
||||
</Button>
|
||||
</template>
|
||||
</Space>
|
||||
|
||||
<Dropdown v-if="getDropdownList.length > 0" :trigger="['hover']">
|
||||
<slot name="more">
|
||||
<Button size="small" type="link">
|
||||
<template #icon>
|
||||
<Icon class="icon-more" icon="ant-design:more-outlined" />
|
||||
</template>
|
||||
</Button>
|
||||
</slot>
|
||||
<template #overlay>
|
||||
<Menu @click="handleMenuClick">
|
||||
<MenuItem v-for="(action, index) in getDropdownList" :key="index">
|
||||
<template v-if="action.popConfirm">
|
||||
<Popconfirm v-bind="getPopConfirmProps(action.popConfirm)">
|
||||
<template v-if="action.popConfirm.icon" #icon>
|
||||
<Icon :icon="action.popConfirm.icon" />
|
||||
</template>
|
||||
<div
|
||||
:class="
|
||||
action.disabled === true
|
||||
? 'cursor-not-allowed text-gray-300'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<Icon v-if="action.icon" :icon="action.icon" />
|
||||
<span class="ml-1">{{ action.text }}</span>
|
||||
</div>
|
||||
</Popconfirm>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
:class="
|
||||
action.disabled === true
|
||||
? 'cursor-not-allowed text-gray-300'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<Icon v-if="action.icon" :icon="action.icon" />
|
||||
{{ action.label }}
|
||||
</div>
|
||||
</template>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="less">
|
||||
/** 修复 iconify 位置问题 **/
|
||||
.m-table-action {
|
||||
.ant-btn > .iconify + span,
|
||||
.ant-btn > span + .iconify {
|
||||
margin-inline-start: 8px;
|
||||
}
|
||||
|
||||
.ant-btn > .iconify {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
font-style: normal;
|
||||
line-height: 0;
|
||||
color: inherit;
|
||||
text-align: center;
|
||||
text-transform: none;
|
||||
vertical-align: -0.125em;
|
||||
text-rendering: optimizelegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
26
apps/web-antd/src/components/table-action/types.d.ts
vendored
Normal file
26
apps/web-antd/src/components/table-action/types.d.ts
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ButtonProps } from 'ant-design-vue/es/button/buttonTypes';
|
||||
import { TooltipProps } from 'ant-design-vue/es/tooltip/Tooltip';
|
||||
|
||||
export interface PopConfirm {
|
||||
title: string;
|
||||
okText?: string;
|
||||
cancelText?: string;
|
||||
confirm: Fn;
|
||||
cancel?: Fn;
|
||||
icon?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
export interface ActionItem extends ButtonProps {
|
||||
onClick?: Fn;
|
||||
label?: string;
|
||||
color?: 'error' | 'success' | 'warning';
|
||||
icon?: string;
|
||||
popConfirm?: PopConfirm;
|
||||
disabled?: boolean;
|
||||
divider?: boolean;
|
||||
// 权限编码控制是否显示
|
||||
auth?: string[];
|
||||
// 业务控制是否显示
|
||||
ifShow?: ((action: ActionItem) => boolean) | boolean;
|
||||
tooltip?: string | TooltipProps;
|
||||
}
|
||||
22
apps/web-antd/src/components/view/component-map.ts
Normal file
22
apps/web-antd/src/components/view/component-map.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import { getFileNameWithoutExtension, toPascalCase } from '#/util/tool';
|
||||
|
||||
const componentMap = new Map<string, Component>();
|
||||
// import.meta.glob() 直接引入所有的模块 Vite 独有的功能
|
||||
const modules = import.meta.glob(
|
||||
['./components/**/*.vue', '../../views/**/components/view/*.vue'],
|
||||
{ eager: true },
|
||||
);
|
||||
// 加入到路由集合中
|
||||
Object.keys(modules).forEach((key) => {
|
||||
if (!key.includes('-ignore')) {
|
||||
const mod = (modules as any)[key].default || {};
|
||||
// ./components/ApiDict.vue
|
||||
// 获取ApiDict
|
||||
const compName = getFileNameWithoutExtension(key);
|
||||
componentMap.set(toPascalCase(compName), mod);
|
||||
}
|
||||
});
|
||||
|
||||
export { componentMap };
|
||||
@@ -0,0 +1,98 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, type PropType, ref } from 'vue';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
value: {
|
||||
// 值
|
||||
type: [String, Number, Array],
|
||||
default: undefined,
|
||||
},
|
||||
api: {
|
||||
// 接口请求对象
|
||||
type: [Function, String] as PropType<
|
||||
((...arg: any) => Promise<any>) | String
|
||||
>,
|
||||
default() {
|
||||
return () => {
|
||||
return new Promise((resolve) => {
|
||||
resolve([]);
|
||||
});
|
||||
};
|
||||
},
|
||||
},
|
||||
params: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
cacheKey: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
requestMethod: {
|
||||
type: String,
|
||||
default: 'post',
|
||||
},
|
||||
valueField: {
|
||||
type: String,
|
||||
default: 'id',
|
||||
},
|
||||
labelField: {
|
||||
type: String,
|
||||
default: 'name',
|
||||
},
|
||||
// 分割符
|
||||
split: {
|
||||
type: String,
|
||||
default: '/',
|
||||
},
|
||||
});
|
||||
const currentData = ref([]);
|
||||
const cValue = computed(() => {
|
||||
return currentData.value
|
||||
.map((item) => item[props.labelField])
|
||||
.join(props.split);
|
||||
});
|
||||
onMounted(() => {
|
||||
const api: (...arg: any) => Promise<any> =
|
||||
typeof props.api === 'string'
|
||||
? (params: any) => {
|
||||
return (requestClient as any)[props.requestMethod](
|
||||
props.api as any,
|
||||
params,
|
||||
);
|
||||
}
|
||||
: (props.api as (...arg: any) => Promise<any>);
|
||||
const searchType = 'IN';
|
||||
const params =
|
||||
props.requestMethod === 'get'
|
||||
? {
|
||||
params: {
|
||||
...props.params,
|
||||
[`m_${searchType}_${props.valueField}`]: props.value,
|
||||
},
|
||||
}
|
||||
: {
|
||||
...props.params,
|
||||
[`m_${searchType}_${props.valueField}`]: props.value,
|
||||
};
|
||||
api(params).then((res) => {
|
||||
if (res.length > 0) {
|
||||
currentData.value = res;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div>{{ cValue }}</div>
|
||||
</template>
|
||||
<style lang="less" scoped></style>
|
||||
@@ -0,0 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import ApiSelect from './api-select.vue';
|
||||
</script>
|
||||
<template>
|
||||
<ApiSelect />
|
||||
</template>
|
||||
67
apps/web-antd/src/components/view/components/api-dict.vue
Normal file
67
apps/web-antd/src/components/view/components/api-dict.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue';
|
||||
|
||||
import { useDictStore } from '#/store/dict';
|
||||
|
||||
const props = defineProps({
|
||||
code: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
value: {
|
||||
// 值
|
||||
type: [String, Number, Array],
|
||||
default: undefined,
|
||||
},
|
||||
split: {
|
||||
// 分割符
|
||||
type: String,
|
||||
default: ',',
|
||||
},
|
||||
join: {
|
||||
// 连接符
|
||||
type: String,
|
||||
default: ',',
|
||||
},
|
||||
});
|
||||
const dictStore = useDictStore();
|
||||
const cValue = computed(() => {
|
||||
if (!props.value && props.value !== 0) {
|
||||
return '';
|
||||
}
|
||||
const arr: Array<any> = [];
|
||||
if (Array.isArray(props.value)) {
|
||||
arr.push(...props.value);
|
||||
} else {
|
||||
arr.push(...props.value.toString().split(props.split));
|
||||
}
|
||||
const dictData = dictStore.getDictData(props.code as string);
|
||||
const res: Array<any> = [];
|
||||
arr.forEach((item) => {
|
||||
for (let i = 0; i < dictData.length; i++) {
|
||||
if (dictData[i].value?.toString() === item?.toString()) {
|
||||
res.push(dictData[i].label);
|
||||
break;
|
||||
}
|
||||
if (i === dictData.length - 1) {
|
||||
res.push(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return res.join(props.join);
|
||||
});
|
||||
onMounted(() => {
|
||||
dictStore.requestData(props.code as string);
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div>{{ cValue }}</div>
|
||||
</template>
|
||||
<style lang="less" scoped></style>
|
||||
@@ -0,0 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import ApiSelect from './api-select.vue';
|
||||
</script>
|
||||
<template>
|
||||
<ApiSelect />
|
||||
</template>
|
||||
153
apps/web-antd/src/components/view/components/api-select.vue
Normal file
153
apps/web-antd/src/components/view/components/api-select.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, type PropType, watch } from 'vue';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
import { useDictStore } from '#/store/dict';
|
||||
|
||||
const props = defineProps({
|
||||
code: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
value: {
|
||||
// 值
|
||||
type: [Boolean, String, Number, Array],
|
||||
default: undefined,
|
||||
},
|
||||
split: {
|
||||
// 分割符
|
||||
type: String,
|
||||
default: ',',
|
||||
},
|
||||
join: {
|
||||
// 连接符
|
||||
type: String,
|
||||
default: ',',
|
||||
},
|
||||
api: {
|
||||
// 接口请求对象
|
||||
type: [Function, String] as PropType<
|
||||
((...arg: any) => Promise<any>) | String
|
||||
>,
|
||||
default() {
|
||||
return () => {
|
||||
return new Promise((resolve) => {
|
||||
resolve([]);
|
||||
});
|
||||
};
|
||||
},
|
||||
},
|
||||
params: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
cacheKey: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
requestMethod: {
|
||||
type: String,
|
||||
default: 'post',
|
||||
},
|
||||
});
|
||||
const dictStore = useDictStore();
|
||||
/**
|
||||
* 获取包含的id
|
||||
*/
|
||||
const getIncludeIds = () => {
|
||||
if (!props.value && props.value !== 0) {
|
||||
return [];
|
||||
}
|
||||
const arr: Array<any> = [];
|
||||
if (Array.isArray(props.value)) {
|
||||
arr.push(...props.value);
|
||||
} else {
|
||||
arr.push(...props.value.toString().split(props.split));
|
||||
}
|
||||
return arr;
|
||||
};
|
||||
/**
|
||||
* 获取缓存key
|
||||
*/
|
||||
const getCacheKey = () => {
|
||||
let cacheKey = props.cacheKey;
|
||||
if (typeof props.api === 'string' && !cacheKey) {
|
||||
cacheKey = props.api as string;
|
||||
}
|
||||
return cacheKey;
|
||||
};
|
||||
const cValue = computed(() => {
|
||||
if (!props.value && props.value !== 0) {
|
||||
return '';
|
||||
}
|
||||
const arr: Array<any> = getIncludeIds();
|
||||
const cacheKey = getCacheKey();
|
||||
const dictData = dictStore.getSelectData(cacheKey, {
|
||||
...props.params,
|
||||
includeType: 2,
|
||||
includeIds: getIncludeIds(),
|
||||
});
|
||||
const res: Array<any> = [];
|
||||
arr.forEach((item) => {
|
||||
for (let i = 0; i < dictData.length; i++) {
|
||||
if (dictData[i].value?.toString() === item?.toString()) {
|
||||
res.push(dictData[i].label);
|
||||
break;
|
||||
}
|
||||
if (i === dictData.length - 1) {
|
||||
res.push(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return res.join(props.join);
|
||||
});
|
||||
const requestData = () => {
|
||||
const api: (...arg: any) => Promise<any> =
|
||||
typeof props.api === 'string'
|
||||
? (params: any) => {
|
||||
return (requestClient as any)[props.requestMethod](
|
||||
props.api as any,
|
||||
params,
|
||||
);
|
||||
}
|
||||
: (props.api as (...arg: any) => Promise<any>);
|
||||
const cacheKey = getCacheKey();
|
||||
const params =
|
||||
props.requestMethod === 'get'
|
||||
? {
|
||||
params: {
|
||||
...props.params,
|
||||
includeType: 2,
|
||||
includeIds: getIncludeIds(),
|
||||
},
|
||||
}
|
||||
: {
|
||||
...props.params,
|
||||
includeType: 2,
|
||||
includeIds: getIncludeIds(),
|
||||
};
|
||||
dictStore.select(api, params, cacheKey);
|
||||
};
|
||||
onMounted(() => {
|
||||
requestData();
|
||||
});
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
requestData();
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div>{{ cValue }}</div>
|
||||
</template>
|
||||
<style lang="less" scoped></style>
|
||||
102
apps/web-antd/src/components/view/components/api-tree-select.vue
Normal file
102
apps/web-antd/src/components/view/components/api-tree-select.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, type PropType, ref } from 'vue';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
value: {
|
||||
// 值
|
||||
type: [String, Number, Array],
|
||||
default: undefined,
|
||||
},
|
||||
api: {
|
||||
// 接口请求对象
|
||||
type: [Function, String] as PropType<
|
||||
((...arg: any) => Promise<any>) | String
|
||||
>,
|
||||
default() {
|
||||
return () => {
|
||||
return new Promise((resolve) => {
|
||||
resolve([]);
|
||||
});
|
||||
};
|
||||
},
|
||||
},
|
||||
params: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
cacheKey: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
requestMethod: {
|
||||
type: String,
|
||||
default: 'post',
|
||||
},
|
||||
valueField: {
|
||||
type: String,
|
||||
default: 'id',
|
||||
},
|
||||
labelField: {
|
||||
type: String,
|
||||
default: 'name',
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 分割符
|
||||
split: {
|
||||
type: String,
|
||||
default: '/',
|
||||
},
|
||||
});
|
||||
const currentData = ref([]);
|
||||
const cValue = computed(() => {
|
||||
return currentData.value
|
||||
.map((item) => item[props.labelField])
|
||||
.join(props.split);
|
||||
});
|
||||
onMounted(() => {
|
||||
const api: (...arg: any) => Promise<any> =
|
||||
typeof props.api === 'string'
|
||||
? (params: any) => {
|
||||
return (requestClient as any)[props.requestMethod](
|
||||
props.api as any,
|
||||
params,
|
||||
);
|
||||
}
|
||||
: (props.api as (...arg: any) => Promise<any>);
|
||||
const searchType = props.multiple ? 'IN' : 'EQ';
|
||||
const params =
|
||||
props.requestMethod === 'get'
|
||||
? {
|
||||
params: {
|
||||
...props.params,
|
||||
[`m_${searchType}_${props.valueField}`]: props.value,
|
||||
},
|
||||
}
|
||||
: {
|
||||
...props.params,
|
||||
[`m_${searchType}_${props.valueField}`]: props.value,
|
||||
};
|
||||
api(params).then((res) => {
|
||||
if (res.length > 0) {
|
||||
currentData.value = res;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div>{{ cValue }}</div>
|
||||
</template>
|
||||
<style lang="less" scoped></style>
|
||||
14
apps/web-antd/src/components/view/components/avatar.vue
Normal file
14
apps/web-antd/src/components/view/components/avatar.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { Image } from 'ant-design-vue';
|
||||
|
||||
defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Image v-if="value" :src="value" :width="102" />
|
||||
</template>
|
||||
<style lang="less" scoped></style>
|
||||
22
apps/web-antd/src/components/view/components/icon-picker.vue
Normal file
22
apps/web-antd/src/components/view/components/icon-picker.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '#/components/icon';
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: '16px',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Icon v-if="value" :icon="value" :size="size" />
|
||||
<span v-else></span>
|
||||
</template>
|
||||
<style lang="less" scoped></style>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import Select from './select.vue';
|
||||
</script>
|
||||
<template>
|
||||
<Select />
|
||||
</template>
|
||||
<style lang="less" scoped></style>
|
||||
35
apps/web-antd/src/components/view/components/select.vue
Normal file
35
apps/web-antd/src/components/view/components/select.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: [String, Boolean, Number, Array],
|
||||
default: undefined,
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
join: {
|
||||
// 连接符
|
||||
type: String,
|
||||
default: ',',
|
||||
},
|
||||
});
|
||||
const cValue = computed(() => {
|
||||
return Array.isArray(props.value)
|
||||
? props.options
|
||||
.filter((item: any) => (props.value as Array<any>).includes(item.value))
|
||||
.map((item: any) => item.label)
|
||||
.join(props.join)
|
||||
: (
|
||||
props.options.find(
|
||||
(item: any) => item.value?.toString() === props.value?.toString(),
|
||||
) as any
|
||||
)?.label;
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div>{{ cValue }}</div>
|
||||
</template>
|
||||
<style lang="less" scoped></style>
|
||||
25
apps/web-antd/src/components/view/components/switch.vue
Normal file
25
apps/web-antd/src/components/view/components/switch.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, type PropType } from 'vue';
|
||||
|
||||
import { Switch } from 'ant-design-vue';
|
||||
|
||||
const props = defineProps({
|
||||
// 值
|
||||
value: {
|
||||
type: [String, Number, Boolean] as PropType<boolean | number | string>,
|
||||
default: undefined,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['update:value']);
|
||||
const mValue = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(val) {
|
||||
emit('update:value', val);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Switch v-model:checked="mValue" :disabled="true" />
|
||||
</template>
|
||||
25
apps/web-antd/src/components/view/components/upload.vue
Normal file
25
apps/web-antd/src/components/view/components/upload.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import { Upload } from '#/components/form';
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
listType: {
|
||||
type: String as PropType<'picture' | 'picture-card' | 'text'>,
|
||||
default: 'text',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Upload :disabled="true" :list-type="listType" :value="value">
|
||||
<template #default></template>
|
||||
</Upload>
|
||||
</template>
|
||||
<style lang="less" scoped></style>
|
||||
Reference in New Issue
Block a user