This commit is contained in:
AN
2025-06-11 20:51:46 +08:00
70 changed files with 2485 additions and 1303 deletions

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

View File

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

View File

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

View File

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

View File

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

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