fix: 一些基本功能

This commit is contained in:
2025-03-07 08:18:45 +08:00
parent 638462295d
commit 4cdb8351d4
453 changed files with 13336 additions and 24197 deletions

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

View File

@@ -0,0 +1,2 @@
export { default as Description } from './description.vue';
export type * from './types';

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

View File

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

View File

@@ -0,0 +1 @@
export { default as DynamicSchemaForm } from './dynamic-schema-form.vue';

View File

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

View File

@@ -0,0 +1 @@
export { default as FormDescription } from './form-description.vue';

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

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

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

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

View File

@@ -0,0 +1,7 @@
export type CustomComponentType =
| 'ApiCheckboxGroup'
| 'ApiDict'
| 'ApiRadioGroup'
| 'ApiSelect'
| 'ApiTreeSelect'
| 'IconPicker';

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

View File

@@ -0,0 +1 @@
export { default as Icon } from './icon.vue';

View File

@@ -0,0 +1,2 @@
export { default as TableAction } from './table-action.vue';
export type * from './types.d.ts';

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

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

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

View File

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

View File

@@ -0,0 +1,6 @@
<script setup lang="ts">
import ApiSelect from './api-select.vue';
</script>
<template>
<ApiSelect />
</template>

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

View File

@@ -0,0 +1,6 @@
<script setup lang="ts">
import ApiSelect from './api-select.vue';
</script>
<template>
<ApiSelect />
</template>

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

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

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

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

View File

@@ -0,0 +1,7 @@
<script setup lang="ts">
import Select from './select.vue';
</script>
<template>
<Select />
</template>
<style lang="less" scoped></style>

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

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

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