mirror of
https://github.com/m-xlsea/ruoyi-plus-soybean.git
synced 2025-09-24 07:49:47 +08:00
Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow
This commit is contained in:
35
src/components/common/data-table.vue
Normal file
35
src/components/common/data-table.vue
Normal file
@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { useAttrs } from 'vue';
|
||||
import type { DataTableProps } from 'naive-ui';
|
||||
import type { CreateRowKey } from 'naive-ui/es/data-table/src/interface';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
|
||||
defineOptions({
|
||||
name: 'DataTable',
|
||||
inheritAttrs: false
|
||||
});
|
||||
|
||||
interface Props {
|
||||
rowKey?: CreateRowKey<any>;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
|
||||
const { table } = useThemeStore();
|
||||
const attrs: DataTableProps = useAttrs();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDataTable
|
||||
:bordered="table.bordered"
|
||||
:bottom-bordered="table.bottomBordered"
|
||||
:single-column="table.singleColumn"
|
||||
:single-line="table.singleLine"
|
||||
:size="table.size"
|
||||
:striped="table.striped"
|
||||
:row-key="rowKey"
|
||||
v-bind="attrs"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
@ -2,7 +2,7 @@
|
||||
import { useAttrs } from 'vue';
|
||||
import type { TreeSelectProps } from 'naive-ui';
|
||||
import { useLoading } from '@sa/hooks';
|
||||
import { fetchGetDeptSelect } from '@/service/api/system';
|
||||
import { fetchGetDeptTree } from '@/service/api/system';
|
||||
|
||||
defineOptions({ name: 'DeptTreeSelect' });
|
||||
|
||||
@ -13,16 +13,21 @@ interface Props {
|
||||
defineProps<Props>();
|
||||
|
||||
const value = defineModel<CommonType.IdType | null>('value', { required: false });
|
||||
const options = defineModel<Api.System.Dept[]>('options', { required: false, default: [] });
|
||||
const options = defineModel<Api.Common.CommonTreeRecord>('options', { required: false, default: [] });
|
||||
const expandedKeys = defineModel<CommonType.IdType[]>('expandedKeys', { required: false, default: [] });
|
||||
|
||||
const attrs: TreeSelectProps = useAttrs();
|
||||
const { loading, startLoading, endLoading } = useLoading();
|
||||
|
||||
async function getDeptList() {
|
||||
startLoading();
|
||||
const { error, data } = await fetchGetDeptSelect();
|
||||
const { error, data } = await fetchGetDeptTree();
|
||||
if (error) return;
|
||||
options.value = data;
|
||||
// 设置默认展开的节点
|
||||
if (data?.length && !expandedKeys.value.length) {
|
||||
expandedKeys.value = [data[0].id];
|
||||
}
|
||||
endLoading();
|
||||
}
|
||||
|
||||
@ -32,13 +37,13 @@ getDeptList();
|
||||
<template>
|
||||
<NTreeSelect
|
||||
v-model:value="value"
|
||||
v-model:expanded-keys="expandedKeys"
|
||||
filterable
|
||||
class="h-full"
|
||||
:loading="loading"
|
||||
key-field="deptId"
|
||||
label-field="deptName"
|
||||
:options="options"
|
||||
:default-expanded-keys="[0]"
|
||||
key-field="id"
|
||||
label-field="label"
|
||||
:options="options as []"
|
||||
v-bind="attrs"
|
||||
/>
|
||||
</template>
|
||||
|
@ -3,6 +3,7 @@ import { computed, useAttrs } from 'vue';
|
||||
import type { TagProps } from 'naive-ui';
|
||||
import { useDict } from '@/hooks/business/dict';
|
||||
import { isNotNull } from '@/utils/common';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({ name: 'DictTag' });
|
||||
|
||||
@ -23,13 +24,18 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
|
||||
const attrs = useAttrs() as TagProps;
|
||||
|
||||
const { transformDictData } = useDict(props.dictCode, props.immediate);
|
||||
|
||||
const dictTagData = computed<Api.System.DictData[]>(() => {
|
||||
if (props.dictData) {
|
||||
return [props.dictData];
|
||||
const dictData = props.dictData;
|
||||
if (dictData.dictLabel?.startsWith(`dict.${dictData.dictType}.`)) {
|
||||
dictData.dictLabel = $t(dictData.dictLabel as App.I18n.I18nKey);
|
||||
}
|
||||
return [dictData];
|
||||
}
|
||||
// 避免 props.value 为 0 时,无法触发
|
||||
if (props.dictCode && isNotNull(props.value)) {
|
||||
const { transformDictData } = useDict(props.dictCode, props.immediate);
|
||||
return transformDictData(props.value) || [];
|
||||
}
|
||||
|
||||
@ -44,8 +50,8 @@ const dictTagData = computed<Api.System.DictData[]>(() => {
|
||||
:key="item.dictValue"
|
||||
class="m-1"
|
||||
:class="[item.cssClass]"
|
||||
:type="item.listClass"
|
||||
v-bind="attrs"
|
||||
:type="item.listClass || 'default'"
|
||||
>
|
||||
{{ item.dictLabel }}
|
||||
</NTag>
|
||||
|
@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, useAttrs, watch } from 'vue';
|
||||
import { computed, useAttrs } from 'vue';
|
||||
import type { UploadFileInfo, UploadProps } from 'naive-ui';
|
||||
import { fetchBatchDeleteOss } from '@/service/api/system/oss';
|
||||
import { getToken } from '@/store/modules/auth/shared';
|
||||
import { getServiceBaseURL } from '@/utils/service';
|
||||
import { AcceptType } from '@/enum/business';
|
||||
|
||||
defineOptions({
|
||||
name: 'FileUpload'
|
||||
@ -26,30 +27,24 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
defaultUpload: true,
|
||||
showTip: true,
|
||||
max: 5,
|
||||
accept: '.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.pdf',
|
||||
accept: undefined,
|
||||
fileSize: 5,
|
||||
uploadType: 'file'
|
||||
});
|
||||
|
||||
const accept = computed(() => {
|
||||
if (props.accept) {
|
||||
return props.accept;
|
||||
}
|
||||
return props.uploadType === 'file' ? AcceptType.File : AcceptType.Image;
|
||||
});
|
||||
|
||||
const attrs: UploadProps = useAttrs();
|
||||
|
||||
const value = defineModel<CommonType.IdType[]>('value', { required: false, default: [] });
|
||||
|
||||
let fileNum = 0;
|
||||
const fileList = ref<UploadFileInfo[]>([]);
|
||||
|
||||
const needRelaodData = ref<boolean>(false);
|
||||
defineExpose({
|
||||
refreshList: needRelaodData,
|
||||
fileList
|
||||
const fileList = defineModel<UploadFileInfo[]>('fileList', {
|
||||
default: () => []
|
||||
});
|
||||
watch(
|
||||
() => fileList.value,
|
||||
newValue => {
|
||||
needRelaodData.value = newValue.length > 0;
|
||||
value.value = newValue.map(item => item.id);
|
||||
}
|
||||
);
|
||||
|
||||
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
|
||||
const { baseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
|
||||
@ -64,12 +59,12 @@ function beforeUpload(options: { file: UploadFileInfo; fileList: UploadFileInfo[
|
||||
const { file } = options;
|
||||
|
||||
// 校检文件类型
|
||||
if (props.accept) {
|
||||
if (accept.value) {
|
||||
const fileName = file.name.split('.');
|
||||
const fileExt = `.${fileName[fileName.length - 1]}`;
|
||||
const isTypeOk = props.accept.split(',')?.includes(fileExt);
|
||||
const isTypeOk = accept.value.split(',')?.includes(fileExt);
|
||||
if (!isTypeOk) {
|
||||
window.$message?.error(`文件格式不正确, 请上传 ${props.accept} 格式文件!`);
|
||||
window.$message?.error(`文件格式不正确, 请上传 ${accept.value} 格式文件!`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -122,11 +117,12 @@ function handleError(options: { file: UploadFileInfo; event?: ProgressEvent }) {
|
||||
|
||||
async function handleRemove(file: UploadFileInfo) {
|
||||
if (file.status !== 'finished') {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
const { error } = await fetchBatchDeleteOss([file.id]);
|
||||
if (error) return;
|
||||
if (error) return false;
|
||||
window.$message?.success('删除成功');
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -9,11 +9,13 @@ defineOptions({ name: 'MenuTree' });
|
||||
|
||||
interface Props {
|
||||
immediate?: boolean;
|
||||
showHeader?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
immediate: true
|
||||
immediate: true,
|
||||
showHeader: true
|
||||
});
|
||||
|
||||
const { bool: expandAll } = useBoolean();
|
||||
@ -49,7 +51,6 @@ onMounted(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// 添加 watch 监听 expandAll 的变化,options有值后,计算expandedKeys
|
||||
watch([expandAll, options], ([newVal]) => {
|
||||
if (newVal) {
|
||||
// 展开所有节点
|
||||
@ -80,6 +81,21 @@ function getAllMenuIds(menu: Api.System.MenuList) {
|
||||
return menuIds;
|
||||
}
|
||||
|
||||
/** 获取所有叶子节点的 ID(没有子节点的节点) */
|
||||
function getLeafMenuIds(menu: Api.System.MenuList): CommonType.IdType[] {
|
||||
const leafIds: CommonType.IdType[] = [];
|
||||
menu.forEach(item => {
|
||||
if (!item.children || item.children.length === 0) {
|
||||
// 是叶子节点
|
||||
leafIds.push(item.id!);
|
||||
} else {
|
||||
// 有子节点,递归获取子节点中的叶子节点
|
||||
leafIds.push(...getLeafMenuIds(item.children));
|
||||
}
|
||||
});
|
||||
return leafIds;
|
||||
}
|
||||
|
||||
function handleCheckedTreeNodeAll(checked: boolean) {
|
||||
if (checked) {
|
||||
checkedKeys.value = getAllMenuIds(options.value);
|
||||
@ -88,16 +104,30 @@ function handleCheckedTreeNodeAll(checked: boolean) {
|
||||
checkedKeys.value = [];
|
||||
}
|
||||
|
||||
function getCheckedMenuIds() {
|
||||
function getCheckedMenuIds(isCascade: boolean = false) {
|
||||
const menuIds = menuTreeRef.value?.getCheckedData()?.keys as string[];
|
||||
const indeterminateData = menuTreeRef.value?.getIndeterminateData();
|
||||
if (cascade.value) {
|
||||
if (cascade.value || isCascade) {
|
||||
const parentIds: string[] = indeterminateData?.keys.filter(item => !menuIds?.includes(String(item))) as string[];
|
||||
menuIds?.push(...parentIds);
|
||||
}
|
||||
return menuIds;
|
||||
}
|
||||
|
||||
watch(cascade, () => {
|
||||
if (cascade.value) {
|
||||
// 获取当前菜单树中的所有叶子节点ID
|
||||
const allLeafIds = getLeafMenuIds(options.value);
|
||||
// 筛选出当前选中项中的叶子节点
|
||||
const selectedLeafIds = checkedKeys.value.filter(id => allLeafIds.includes(id));
|
||||
// 重新设置选中状态为只包含叶子节点,让组件基于父子联动规则重新计算父节点状态
|
||||
checkedKeys.value = selectedLeafIds;
|
||||
return;
|
||||
}
|
||||
// 禁用父子联动时,将半选中的父节点也加入到选中列表
|
||||
checkedKeys.value = getCheckedMenuIds(true);
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
getCheckedMenuIds,
|
||||
refresh: getMenuList
|
||||
@ -106,7 +136,7 @@ defineExpose({
|
||||
|
||||
<template>
|
||||
<div class="w-full flex-col gap-12px">
|
||||
<div class="w-full flex-center">
|
||||
<div v-if="showHeader" class="w-full flex-center">
|
||||
<NCheckbox v-model:checked="expandAll" :checked-value="true" :unchecked-value="false">展开/折叠</NCheckbox>
|
||||
<NCheckbox
|
||||
v-model:checked="checkAll"
|
||||
|
68
src/components/custom/oss-upload.vue
Normal file
68
src/components/custom/oss-upload.vue
Normal file
@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, useAttrs, watch } from 'vue';
|
||||
import type { UploadFileInfo } from 'naive-ui';
|
||||
import { useLoading } from '@sa/hooks';
|
||||
import { fetchGetOssListByIds } from '@/service/api/system/oss';
|
||||
import { isNotNull } from '@/utils/common';
|
||||
import FileUpload from '@/components/custom/file-upload.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'OssUpload'
|
||||
});
|
||||
|
||||
const attrs = useAttrs();
|
||||
|
||||
const value = defineModel<string>('value', { default: '' });
|
||||
|
||||
const { loading, startLoading, endLoading } = useLoading();
|
||||
|
||||
const fileList = ref<UploadFileInfo[]>([]);
|
||||
|
||||
async function handleFetchOssList(ossIds: string[]) {
|
||||
startLoading();
|
||||
const { error, data } = await fetchGetOssListByIds(ossIds);
|
||||
if (error) return;
|
||||
fileList.value = data.map(item => ({
|
||||
id: String(item.ossId),
|
||||
url: item.url,
|
||||
name: item.originalName,
|
||||
status: 'finished'
|
||||
}));
|
||||
endLoading();
|
||||
}
|
||||
|
||||
watch(
|
||||
value,
|
||||
async val => {
|
||||
const ossIds = val.split(',')?.filter(item => isNotNull(item));
|
||||
const fileIds = new Set(fileList.value.filter(item => item.status === 'finished').map(item => item.id));
|
||||
if (ossIds.every(item => fileIds.has(item))) {
|
||||
return;
|
||||
}
|
||||
if (ossIds.length === 0) {
|
||||
fileList.value = [];
|
||||
return;
|
||||
}
|
||||
await handleFetchOssList(ossIds);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
fileList,
|
||||
val => {
|
||||
value.value = val
|
||||
.filter(item => item.status === 'finished')
|
||||
.map(item => item.id)
|
||||
.join(',');
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpin v-if="loading" />
|
||||
<FileUpload v-else v-bind="attrs" v-model:file-list="fileList" />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
Reference in New Issue
Block a user