Pre Merge pull request !89 from 子夜/dev
commit
ff4d098285
|
|
@ -1,6 +0,0 @@
|
||||||
echo Start running commit-msg hook...
|
|
||||||
|
|
||||||
# Check whether the git commit information is standardized
|
|
||||||
pnpm exec commitlint --edit "$1"
|
|
||||||
|
|
||||||
echo Run commit-msg hook done.
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
# 每次 git pull 之后, 安装依赖
|
|
||||||
|
|
||||||
pnpm install
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
# update `.vscode/vben-admin.code-workspace` file
|
|
||||||
pnpm vsh code-workspace --auto-commit
|
|
||||||
|
|
||||||
# Format and submit code according to lintstagedrc.js configuration
|
|
||||||
pnpm exec lint-staged
|
|
||||||
|
|
||||||
echo Run pre-commit hook done.
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 31 KiB |
|
|
@ -1,20 +0,0 @@
|
||||||
export default {
|
|
||||||
'*.md': ['prettier --cache --ignore-unknown --write'],
|
|
||||||
'*.vue': [
|
|
||||||
'prettier --write',
|
|
||||||
'eslint --cache --fix',
|
|
||||||
'stylelint --fix --allow-empty-input',
|
|
||||||
],
|
|
||||||
'*.{js,jsx,ts,tsx}': [
|
|
||||||
'prettier --cache --ignore-unknown --write',
|
|
||||||
'eslint --cache --fix',
|
|
||||||
],
|
|
||||||
'*.{scss,less,styl,html,vue,css}': [
|
|
||||||
'prettier --cache --ignore-unknown --write',
|
|
||||||
'stylelint --fix --allow-empty-input',
|
|
||||||
],
|
|
||||||
'package.json': ['prettier --cache --write'],
|
|
||||||
'{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': [
|
|
||||||
'prettier --cache --write --parser json',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
2
.npmrc
2
.npmrc
|
|
@ -1,5 +1,5 @@
|
||||||
registry = "https://registry.npmmirror.com"
|
registry = "https://registry.npmmirror.com"
|
||||||
public-hoist-pattern[]=husky
|
public-hoist-pattern[]=lefthook
|
||||||
public-hoist-pattern[]=eslint
|
public-hoist-pattern[]=eslint
|
||||||
public-hoist-pattern[]=prettier
|
public-hoist-pattern[]=prettier
|
||||||
public-hoist-pattern[]=prettier-plugin-tailwindcss
|
public-hoist-pattern[]=prettier-plugin-tailwindcss
|
||||||
|
|
|
||||||
|
|
@ -219,7 +219,7 @@
|
||||||
"*.env": "$(capture).env.*",
|
"*.env": "$(capture).env.*",
|
||||||
"README.md": "README*,CHANGELOG*,LICENSE,CNAME",
|
"README.md": "README*,CHANGELOG*,LICENSE,CNAME",
|
||||||
"package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,.gitattributes,.gitignore,.gitpod.yml,.npmrc,.browserslistrc,.node-version,.git*,.tazerc.json",
|
"package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,.gitattributes,.gitignore,.gitpod.yml,.npmrc,.browserslistrc,.node-version,.git*,.tazerc.json",
|
||||||
"eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json",
|
"eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml",
|
||||||
"tailwind.config.mjs": "postcss.*"
|
"tailwind.config.mjs": "postcss.*"
|
||||||
},
|
},
|
||||||
"commentTranslate.hover.enabled": false,
|
"commentTranslate.hover.enabled": false,
|
||||||
|
|
|
||||||
|
|
@ -259,6 +259,7 @@ setupVbenVxeTable({
|
||||||
|
|
||||||
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
|
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
|
||||||
// vxeUI.formats.add
|
// vxeUI.formats.add
|
||||||
|
// add by 星语:数量格式化,例如说:金额
|
||||||
vxeUI.formats.add('formatAmount', {
|
vxeUI.formats.add('formatAmount', {
|
||||||
cellFormatMethod({ cellValue }, digits = 2) {
|
cellFormatMethod({ cellValue }, digits = 2) {
|
||||||
if (cellValue === null || cellValue === undefined) {
|
if (cellValue === null || cellValue === undefined) {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ export namespace BpmCategoryApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 模型分类信息 */
|
/** 模型分类信息 */
|
||||||
|
// TODO @jason:这个应该非 api 的,可以考虑抽到页面里哈。
|
||||||
export interface ModelCategoryInfo {
|
export interface ModelCategoryInfo {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
export namespace BpmModelApi {
|
export namespace BpmModelApi {
|
||||||
/** 用户信息 TODO 这个是不是可以抽取出来定义在公共模块 */
|
/** 用户信息 TODO 这个是不是可以抽取出来定义在公共模块 */
|
||||||
|
// TODO @芋艿:一起看看。
|
||||||
export interface UserInfo {
|
export interface UserInfo {
|
||||||
id: number;
|
id: number;
|
||||||
nickname: string;
|
nickname: string;
|
||||||
|
|
@ -9,6 +10,7 @@ export namespace BpmModelApi {
|
||||||
deptId?: number;
|
deptId?: number;
|
||||||
deptName?: string;
|
deptName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 流程定义 VO */
|
/** 流程定义 VO */
|
||||||
export interface ProcessDefinitionVO {
|
export interface ProcessDefinitionVO {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import type { Dayjs } from 'dayjs';
|
||||||
|
|
||||||
import type { PageParam, PageResult } from '@vben/request';
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
import { requestClient } from '#/api/request';
|
import { requestClient } from '#/api/request';
|
||||||
|
|
@ -7,8 +9,8 @@ export namespace Demo01ContactApi {
|
||||||
export interface Demo01Contact {
|
export interface Demo01Contact {
|
||||||
id: number; // 编号
|
id: number; // 编号
|
||||||
name?: string; // 名字
|
name?: string; // 名字
|
||||||
sex?: number; // 性别
|
sex?: boolean; // 性别
|
||||||
birthday?: Date; // 出生年
|
birthday?: Dayjs | string; // 出生年
|
||||||
description?: string; // 简介
|
description?: string; // 简介
|
||||||
avatar: string; // 头像
|
avatar: string; // 头像
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import type { Dayjs } from 'dayjs';
|
||||||
|
|
||||||
import type { PageParam, PageResult } from '@vben/request';
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
import { requestClient } from '#/api/request';
|
import { requestClient } from '#/api/request';
|
||||||
|
|
@ -24,7 +26,7 @@ export namespace Demo03StudentApi {
|
||||||
id: number; // 编号
|
id: number; // 编号
|
||||||
name?: string; // 名字
|
name?: string; // 名字
|
||||||
sex?: number; // 性别
|
sex?: number; // 性别
|
||||||
birthday?: Date; // 出生日期
|
birthday?: Dayjs | string; // 出生日期
|
||||||
description?: string; // 简介
|
description?: string; // 简介
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import type { Dayjs } from 'dayjs';
|
||||||
|
|
||||||
import type { PageParam, PageResult } from '@vben/request';
|
import type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
import { requestClient } from '#/api/request';
|
import { requestClient } from '#/api/request';
|
||||||
|
|
@ -24,7 +26,7 @@ export namespace Demo03StudentApi {
|
||||||
id: number; // 编号
|
id: number; // 编号
|
||||||
name?: string; // 名字
|
name?: string; // 名字
|
||||||
sex?: number; // 性别
|
sex?: number; // 性别
|
||||||
birthday?: Date; // 出生日期
|
birthday?: Dayjs | string; // 出生日期
|
||||||
description?: string; // 简介
|
description?: string; // 简介
|
||||||
demo03courses?: Demo03Course[];
|
demo03courses?: Demo03Course[];
|
||||||
demo03grade?: Demo03Grade;
|
demo03grade?: Demo03Grade;
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,13 @@ export namespace InfraFileApi {
|
||||||
configId: number; // 文件配置编号
|
configId: number; // 文件配置编号
|
||||||
uploadUrl: string; // 文件上传 URL
|
uploadUrl: string; // 文件上传 URL
|
||||||
url: string; // 文件 URL
|
url: string; // 文件 URL
|
||||||
|
path: string; // 文件路径
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 上传文件 */
|
/** 上传文件 */
|
||||||
export interface FileUploadReqVO {
|
export interface FileUploadReqVO {
|
||||||
file: globalThis.File;
|
file: globalThis.File;
|
||||||
path?: string;
|
directory?: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,11 +46,11 @@ export function deleteFile(id: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取文件预签名地址 */
|
/** 获取文件预签名地址 */
|
||||||
export function getFilePresignedUrl(path: string) {
|
export function getFilePresignedUrl(name: string, directory?: string) {
|
||||||
return requestClient.get<InfraFileApi.FilePresignedUrlRespVO>(
|
return requestClient.get<InfraFileApi.FilePresignedUrlRespVO>(
|
||||||
'/infra/file/presigned-url',
|
'/infra/file/presigned-url',
|
||||||
{
|
{
|
||||||
params: { path },
|
params: { name, directory },
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -64,5 +65,9 @@ export function uploadFile(
|
||||||
data: InfraFileApi.FileUploadReqVO,
|
data: InfraFileApi.FileUploadReqVO,
|
||||||
onUploadProgress?: AxiosProgressEvent,
|
onUploadProgress?: AxiosProgressEvent,
|
||||||
) {
|
) {
|
||||||
|
// 特殊:由于 upload 内部封装,即使 directory 为 undefined,也会传递给后端
|
||||||
|
if (!data.directory) {
|
||||||
|
delete data.directory;
|
||||||
|
}
|
||||||
return requestClient.upload('/infra/file/upload', data, { onUploadProgress });
|
return requestClient.upload('/infra/file/upload', data, { onUploadProgress });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,10 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
|
||||||
config.headers['tenant-id'] = tenantEnable
|
config.headers['tenant-id'] = tenantEnable
|
||||||
? accessStore.tenantId
|
? accessStore.tenantId
|
||||||
: undefined;
|
: undefined;
|
||||||
|
// 只有登录时,才设置 visit-tenant-id 访问租户
|
||||||
|
config.headers['visit-tenant-id'] = tenantEnable
|
||||||
|
? accessStore.visitTenantId
|
||||||
|
: undefined;
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -136,6 +140,10 @@ baseRequestClient.addRequestInterceptor({
|
||||||
config.headers['tenant-id'] = tenantEnable
|
config.headers['tenant-id'] = tenantEnable
|
||||||
? accessStore.tenantId
|
? accessStore.tenantId
|
||||||
: undefined;
|
: undefined;
|
||||||
|
// 只有登录时,才设置 visit-tenant-id 访问租户
|
||||||
|
config.headers['visit-tenant-id'] = tenantEnable
|
||||||
|
? accessStore.visitTenantId
|
||||||
|
: undefined;
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,13 @@ export function getTenant(id: number) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取租户精简信息列表 */
|
||||||
|
export function getTenantList() {
|
||||||
|
return requestClient.get<SystemTenantApi.Tenant[]>(
|
||||||
|
'/system/tenant/simple-list',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** 新增租户 */
|
/** 新增租户 */
|
||||||
export function createTenant(data: SystemTenantApi.Tenant) {
|
export function createTenant(data: SystemTenantApi.Tenant) {
|
||||||
return requestClient.post('/system/tenant/create', data);
|
return requestClient.post('/system/tenant/create', data);
|
||||||
|
|
|
||||||
|
|
@ -5,25 +5,45 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { CSSProperties } from 'vue';
|
import type { CSSProperties } from 'vue';
|
||||||
|
|
||||||
import { Card } from 'ant-design-vue';
|
import { ShieldQuestion } from '@vben/icons';
|
||||||
|
|
||||||
|
import { Card, Tooltip } from 'ant-design-vue';
|
||||||
|
|
||||||
defineOptions({ name: 'ContentWrap' });
|
defineOptions({ name: 'ContentWrap' });
|
||||||
|
|
||||||
withDefaults(
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
bodyStyle?: CSSProperties;
|
bodyStyle?: CSSProperties;
|
||||||
|
message?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
bodyStyle: () => ({ padding: '10px' }),
|
bodyStyle: () => ({ padding: '10px' }),
|
||||||
title: '',
|
title: '',
|
||||||
|
message: '',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
// TODO @puhui999:这个功能,和 vue3 貌似没对全哇?
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Card :body-style="bodyStyle" :title="title" class="mb-4">
|
<Card :body-style="bodyStyle" :title="title" class="mb-4">
|
||||||
|
<template v-if="title" #title>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="text-4 font-[700]">{{ title }}</span>
|
||||||
|
<Tooltip placement="right">
|
||||||
|
<template #title>
|
||||||
|
<div class="max-w-[200px]">{{ message }}</div>
|
||||||
|
</template>
|
||||||
|
<ShieldQuestion :size="14" class="ml-5px" />
|
||||||
|
</Tooltip>
|
||||||
|
<div class="pl-20px flex flex-grow">
|
||||||
|
<slot name="header"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<slot name="extra"></slot>
|
||||||
|
</template>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</Card>
|
</Card>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as TableToolbar } from './table-toolbar.vue';
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { VxeToolbarInstance } from 'vxe-table';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useContentMaximize, useRefresh } from '@vben/hooks';
|
||||||
|
import { Fullscreen, RefreshCw, Search } from '@vben/icons';
|
||||||
|
|
||||||
|
import { Button } from 'ant-design-vue';
|
||||||
|
import { VxeToolbar } from 'vxe-table';
|
||||||
|
|
||||||
|
/** 列表工具栏封装 */
|
||||||
|
defineOptions({ name: 'TableToolbar' });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
hiddenSearch: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emits = defineEmits(['update:hiddenSearch']);
|
||||||
|
|
||||||
|
const toolbarRef = ref<VxeToolbarInstance>();
|
||||||
|
const { toggleMaximizeAndTabbarHidden } = useContentMaximize();
|
||||||
|
const { refresh } = useRefresh();
|
||||||
|
|
||||||
|
/** 隐藏搜索栏 */
|
||||||
|
function onHiddenSearchBar() {
|
||||||
|
emits('update:hiddenSearch', !props.hiddenSearch);
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
getToolbarRef: () => toolbarRef.value,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VxeToolbar ref="toolbarRef" custom>
|
||||||
|
<template #toolPrefix>
|
||||||
|
<slot></slot>
|
||||||
|
<Button class="ml-2 font-[8px]" shape="circle" @click="onHiddenSearchBar">
|
||||||
|
<Search :size="15" />
|
||||||
|
</Button>
|
||||||
|
<Button class="ml-2 font-[8px]" shape="circle" @click="refresh">
|
||||||
|
<RefreshCw :size="15" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
class="ml-2 font-[8px]"
|
||||||
|
shape="circle"
|
||||||
|
@click="toggleMaximizeAndTabbarHidden"
|
||||||
|
>
|
||||||
|
<Fullscreen :size="15" />
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</VxeToolbar>
|
||||||
|
</template>
|
||||||
|
|
@ -28,6 +28,8 @@ const props = withDefaults(
|
||||||
file: File,
|
file: File,
|
||||||
onUploadProgress?: AxiosProgressEvent,
|
onUploadProgress?: AxiosProgressEvent,
|
||||||
) => Promise<AxiosResponse<any>>;
|
) => Promise<AxiosResponse<any>>;
|
||||||
|
// 上传的目录
|
||||||
|
directory?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
helpText?: string;
|
helpText?: string;
|
||||||
// 最大数量的文件,Infinity不限制
|
// 最大数量的文件,Infinity不限制
|
||||||
|
|
@ -44,13 +46,14 @@ const props = withDefaults(
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
value: () => [],
|
value: () => [],
|
||||||
|
directory: undefined,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
helpText: '',
|
helpText: '',
|
||||||
maxSize: 2,
|
maxSize: 2,
|
||||||
maxNumber: 1,
|
maxNumber: 1,
|
||||||
accept: () => [],
|
accept: () => [],
|
||||||
multiple: false,
|
multiple: false,
|
||||||
api: useUpload().httpRequest,
|
api: undefined,
|
||||||
resultField: '',
|
resultField: '',
|
||||||
showDescription: false,
|
showDescription: false,
|
||||||
},
|
},
|
||||||
|
|
@ -141,10 +144,9 @@ const beforeUpload = async (file: File) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
async function customRequest(info: UploadRequestOption<any>) {
|
async function customRequest(info: UploadRequestOption<any>) {
|
||||||
const { api } = props;
|
let { api } = props;
|
||||||
if (!api || !isFunction(api)) {
|
if (!api || !isFunction(api)) {
|
||||||
console.warn('upload api must exist and be a function');
|
api = useUpload(props.directory).httpRequest;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// 上传文件
|
// 上传文件
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ const props = withDefaults(
|
||||||
file: File,
|
file: File,
|
||||||
onUploadProgress?: AxiosProgressEvent,
|
onUploadProgress?: AxiosProgressEvent,
|
||||||
) => Promise<AxiosResponse<any>>;
|
) => Promise<AxiosResponse<any>>;
|
||||||
|
// 上传的目录
|
||||||
|
directory?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
helpText?: string;
|
helpText?: string;
|
||||||
listType?: UploadListType;
|
listType?: UploadListType;
|
||||||
|
|
@ -47,6 +49,7 @@ const props = withDefaults(
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
value: () => [],
|
value: () => [],
|
||||||
|
directory: undefined,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
listType: 'picture-card',
|
listType: 'picture-card',
|
||||||
helpText: '',
|
helpText: '',
|
||||||
|
|
@ -54,7 +57,7 @@ const props = withDefaults(
|
||||||
maxNumber: 1,
|
maxNumber: 1,
|
||||||
accept: () => defaultImageAccepts,
|
accept: () => defaultImageAccepts,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
api: useUpload().httpRequest,
|
api: undefined,
|
||||||
resultField: '',
|
resultField: '',
|
||||||
showDescription: true,
|
showDescription: true,
|
||||||
},
|
},
|
||||||
|
|
@ -177,10 +180,9 @@ const beforeUpload = async (file: File) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
async function customRequest(info: UploadRequestOption<any>) {
|
async function customRequest(info: UploadRequestOption<any>) {
|
||||||
const { api } = props;
|
let { api } = props;
|
||||||
if (!api || !isFunction(api)) {
|
if (!api || !isFunction(api)) {
|
||||||
console.warn('upload api must exist and be a function');
|
api = useUpload(props.directory).httpRequest;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// 上传文件
|
// 上传文件
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,7 @@ import { computed, unref } from 'vue';
|
||||||
import { useAppConfig } from '@vben/hooks';
|
import { useAppConfig } from '@vben/hooks';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
import CryptoJS from 'crypto-js';
|
// import CryptoJS from 'crypto-js';
|
||||||
|
|
||||||
import { createFile, getFilePresignedUrl, uploadFile } from '#/api/infra/file';
|
import { createFile, getFilePresignedUrl, uploadFile } from '#/api/infra/file';
|
||||||
import { baseRequestClient } from '#/api/request';
|
import { baseRequestClient } from '#/api/request';
|
||||||
|
|
||||||
|
|
@ -81,7 +80,7 @@ export function useUploadType({
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO @芋艿:目前保持和 admin-vue3 一致,后续可能重构
|
// TODO @芋艿:目前保持和 admin-vue3 一致,后续可能重构
|
||||||
export const useUpload = () => {
|
export const useUpload = (directory?: string) => {
|
||||||
// 后端上传地址
|
// 后端上传地址
|
||||||
const uploadUrl = getUploadUrl();
|
const uploadUrl = getUploadUrl();
|
||||||
// 是否使用前端直连上传
|
// 是否使用前端直连上传
|
||||||
|
|
@ -97,7 +96,7 @@ export const useUpload = () => {
|
||||||
// 1.1 生成文件名称
|
// 1.1 生成文件名称
|
||||||
const fileName = await generateFileName(file);
|
const fileName = await generateFileName(file);
|
||||||
// 1.2 获取文件预签名地址
|
// 1.2 获取文件预签名地址
|
||||||
const presignedInfo = await getFilePresignedUrl(fileName);
|
const presignedInfo = await getFilePresignedUrl(fileName, directory);
|
||||||
// 1.3 上传文件
|
// 1.3 上传文件
|
||||||
return baseRequestClient
|
return baseRequestClient
|
||||||
.put(presignedInfo.uploadUrl, file, {
|
.put(presignedInfo.uploadUrl, file, {
|
||||||
|
|
@ -107,13 +106,13 @@ export const useUpload = () => {
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// 1.4. 记录文件信息到后端(异步)
|
// 1.4. 记录文件信息到后端(异步)
|
||||||
createFile0(presignedInfo, fileName, file);
|
createFile0(presignedInfo, file);
|
||||||
// 通知成功,数据格式保持与后端上传的返回结果一致
|
// 通知成功,数据格式保持与后端上传的返回结果一致
|
||||||
return { data: presignedInfo.url };
|
return { url: presignedInfo.url };
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 模式二:后端上传
|
// 模式二:后端上传
|
||||||
return uploadFile({ file }, onUploadProgress);
|
return uploadFile({ file, directory }, onUploadProgress);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -134,18 +133,13 @@ export const getUploadUrl = (): string => {
|
||||||
* 创建文件信息
|
* 创建文件信息
|
||||||
*
|
*
|
||||||
* @param vo 文件预签名信息
|
* @param vo 文件预签名信息
|
||||||
* @param name 文件名称
|
|
||||||
* @param file 文件
|
* @param file 文件
|
||||||
*/
|
*/
|
||||||
function createFile0(
|
function createFile0(vo: InfraFileApi.FilePresignedUrlRespVO, file: File) {
|
||||||
vo: InfraFileApi.FilePresignedUrlRespVO,
|
|
||||||
name: string,
|
|
||||||
file: File,
|
|
||||||
) {
|
|
||||||
const fileVO = {
|
const fileVO = {
|
||||||
configId: vo.configId,
|
configId: vo.configId,
|
||||||
url: vo.url,
|
url: vo.url,
|
||||||
path: name,
|
path: vo.path,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
type: file.type,
|
type: file.type,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
|
|
@ -160,12 +154,13 @@ function createFile0(
|
||||||
* @param file 要上传的文件
|
* @param file 要上传的文件
|
||||||
*/
|
*/
|
||||||
async function generateFileName(file: File) {
|
async function generateFileName(file: File) {
|
||||||
// 读取文件内容
|
// // 读取文件内容
|
||||||
const data = await file.arrayBuffer();
|
// const data = await file.arrayBuffer();
|
||||||
const wordArray = CryptoJS.lib.WordArray.create(data);
|
// const wordArray = CryptoJS.lib.WordArray.create(data);
|
||||||
// 计算SHA256
|
// // 计算SHA256
|
||||||
const sha256 = CryptoJS.SHA256(wordArray).toString();
|
// const sha256 = CryptoJS.SHA256(wordArray).toString();
|
||||||
// 拼接后缀
|
// // 拼接后缀
|
||||||
const ext = file.name.slice(Math.max(0, file.name.lastIndexOf('.')));
|
// const ext = file.name.slice(Math.max(0, file.name.lastIndexOf('.')));
|
||||||
return `${sha256}${ext}`;
|
// return `${sha256}${ext}`;
|
||||||
|
return file.name;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import { useAuthStore } from '#/store';
|
||||||
import LoginForm from '#/views/_core/authentication/login.vue';
|
import LoginForm from '#/views/_core/authentication/login.vue';
|
||||||
|
|
||||||
import Help from './components/help.vue';
|
import Help from './components/help.vue';
|
||||||
|
import TenantDropdown from './components/tenant-dropdown.vue';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
|
@ -202,6 +203,9 @@ watch(
|
||||||
@read="handleNotificationRead"
|
@read="handleNotificationRead"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
<template #header-right-1>
|
||||||
|
<TenantDropdown class="w-30 mr-2" />
|
||||||
|
</template>
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<AuthenticationLoginExpiredModal
|
<AuthenticationLoginExpiredModal
|
||||||
v-model:open="accessStore.loginExpired"
|
v-model:open="accessStore.loginExpired"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useAccess } from '@vben/access';
|
||||||
|
import { isTenantEnable, useRefresh } from '@vben/hooks';
|
||||||
|
import { useAccessStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import { Button, Dropdown, Menu } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getTenantList } from '#/api/system/tenant';
|
||||||
|
|
||||||
|
const { refresh } = useRefresh();
|
||||||
|
|
||||||
|
const { hasAccessByCodes } = useAccess();
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
|
||||||
|
const tenantEnable = isTenantEnable();
|
||||||
|
|
||||||
|
const visitTenantList = computed(() => {
|
||||||
|
if (tenantEnable) {
|
||||||
|
const list = accessStore.visitTenantId.map((item) => ({
|
||||||
|
label: item.name,
|
||||||
|
id: item.id,
|
||||||
|
}));
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const tenant = ref<string>(
|
||||||
|
visitTenantList.value.find((item) => item.id === accessStore.tenantId)
|
||||||
|
?.label || '切换租户',
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleClick(id: number | undefined) {
|
||||||
|
if (id) {
|
||||||
|
accessStore.setTenantId(id);
|
||||||
|
refresh();
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (tenantEnable) {
|
||||||
|
const resp = await getTenantList();
|
||||||
|
accessStore.setVisitTenantId(resp);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Dropdown v-if="tenantEnable && hasAccessByCodes(['system:tenant:visit'])">
|
||||||
|
<template #overlay>
|
||||||
|
<Menu>
|
||||||
|
<template v-for="item in visitTenantList" :key="item.key">
|
||||||
|
<Menu.Item @click="handleClick(item.id)">
|
||||||
|
{{ item.label }}
|
||||||
|
</Menu.Item>
|
||||||
|
</template>
|
||||||
|
</Menu>
|
||||||
|
</template>
|
||||||
|
<Button> {{ tenant }} </Button>
|
||||||
|
</Dropdown>
|
||||||
|
</template>
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { Router } from 'vue-router';
|
import type { Router } from 'vue-router';
|
||||||
|
|
||||||
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
|
import { LOGIN_PATH } from '@vben/constants';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
import { preferences } from '@vben/preferences';
|
import { preferences } from '@vben/preferences';
|
||||||
import { useAccessStore, useUserStore } from '@vben/stores';
|
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||||
|
|
@ -61,7 +61,7 @@ function setupAccessGuard(router: Router) {
|
||||||
return decodeURIComponent(
|
return decodeURIComponent(
|
||||||
(to.query?.redirect as string) ||
|
(to.query?.redirect as string) ||
|
||||||
userStore.userInfo?.homePath ||
|
userStore.userInfo?.homePath ||
|
||||||
DEFAULT_HOME_PATH,
|
preferences.app.defaultHomePath,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -80,7 +80,7 @@ function setupAccessGuard(router: Router) {
|
||||||
path: LOGIN_PATH,
|
path: LOGIN_PATH,
|
||||||
// 如不需要,直接删除 query
|
// 如不需要,直接删除 query
|
||||||
query:
|
query:
|
||||||
to.fullPath === DEFAULT_HOME_PATH
|
to.fullPath === preferences.app.defaultHomePath
|
||||||
? {}
|
? {}
|
||||||
: { redirect: encodeURIComponent(to.fullPath) },
|
: { redirect: encodeURIComponent(to.fullPath) },
|
||||||
// 携带当前跳转的页面,登录后重新跳转该页面
|
// 携带当前跳转的页面,登录后重新跳转该页面
|
||||||
|
|
@ -131,8 +131,8 @@ function setupAccessGuard(router: Router) {
|
||||||
accessStore.setIsAccessChecked(true);
|
accessStore.setIsAccessChecked(true);
|
||||||
userStore.setUserRoles(userRoles);
|
userStore.setUserRoles(userRoles);
|
||||||
const redirectPath = (from.query.redirect ??
|
const redirectPath = (from.query.redirect ??
|
||||||
(to.path === DEFAULT_HOME_PATH
|
(to.path === preferences.app.defaultHomePath
|
||||||
? userInfo?.homePath || DEFAULT_HOME_PATH
|
? userInfo?.homePath || preferences.app.defaultHomePath
|
||||||
: to.fullPath)) as string;
|
: to.fullPath)) as string;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type { RouteRecordRaw } from 'vue-router';
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
|
import { LOGIN_PATH } from '@vben/constants';
|
||||||
|
import { preferences } from '@vben/preferences';
|
||||||
|
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
|
@ -34,7 +35,7 @@ const coreRoutes: RouteRecordRaw[] = [
|
||||||
},
|
},
|
||||||
name: 'Root',
|
name: 'Root',
|
||||||
path: '/',
|
path: '/',
|
||||||
redirect: DEFAULT_HOME_PATH,
|
redirect: preferences.app.defaultHomePath,
|
||||||
children: [],
|
children: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ import type { AuthApi } from '#/api';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
|
import { LOGIN_PATH } from '@vben/constants';
|
||||||
|
import { preferences } from '@vben/preferences';
|
||||||
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
|
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
|
||||||
|
|
||||||
import { notification } from 'ant-design-vue';
|
import { notification } from 'ant-design-vue';
|
||||||
|
|
@ -74,7 +75,9 @@ export const useAuthStore = defineStore('auth', () => {
|
||||||
} else {
|
} else {
|
||||||
onSuccess
|
onSuccess
|
||||||
? await onSuccess?.()
|
? await onSuccess?.()
|
||||||
: await router.push(userInfo.homePath || DEFAULT_HOME_PATH);
|
: await router.push(
|
||||||
|
userInfo.homePath || preferences.app.defaultHomePath,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userInfo?.nickname) {
|
if (userInfo?.nickname) {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import dayjs from 'dayjs';
|
||||||
// TODO @芋艿:后续整理下 迁移至 packages/core/base/shared/src/utils/date.ts,后续删除 使用 @vben/utils 的 getRangePickerDefaultProps
|
// TODO @芋艿:后续整理下 迁移至 packages/core/base/shared/src/utils/date.ts,后续删除 使用 @vben/utils 的 getRangePickerDefaultProps
|
||||||
|
|
||||||
/** 时间段选择器拓展 */
|
/** 时间段选择器拓展 */
|
||||||
export function getRangePickerDefaultProps() {
|
export function getRangePickerDefaultProps(): any {
|
||||||
return {
|
return {
|
||||||
showTime: {
|
showTime: {
|
||||||
format: 'HH:mm:ss',
|
format: 'HH:mm:ss',
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableInstance } from 'vxe-table';
|
||||||
|
|
||||||
import type { Demo01ContactApi } from '#/api/infra/demo/demo01';
|
import type { Demo01ContactApi } from '#/api/infra/demo/demo01';
|
||||||
|
|
||||||
import { h, onMounted, reactive, ref } from 'vue';
|
import { h, nextTick, onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
import { Page, useVbenModal } from '@vben/common-ui';
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
import { Download, Plus, RefreshCw, Search } from '@vben/icons';
|
import { Download, Plus } from '@vben/icons';
|
||||||
import { cloneDeep, formatDateTime } from '@vben/utils';
|
import { cloneDeep, formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -25,6 +27,7 @@ import {
|
||||||
} from '#/api/infra/demo/demo01';
|
} from '#/api/infra/demo/demo01';
|
||||||
import { ContentWrap } from '#/components/content-wrap';
|
import { ContentWrap } from '#/components/content-wrap';
|
||||||
import { DictTag } from '#/components/dict-tag';
|
import { DictTag } from '#/components/dict-tag';
|
||||||
|
import { TableToolbar } from '#/components/table-toolbar';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import { getRangePickerDefaultProps } from '#/utils/date';
|
import { getRangePickerDefaultProps } from '#/utils/date';
|
||||||
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
|
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
|
||||||
|
|
@ -118,9 +121,20 @@ async function onExport() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 隐藏搜索栏 */
|
||||||
|
const hiddenSearchBar = ref(false);
|
||||||
|
const tableToolbarRef = ref<InstanceType<typeof TableToolbar>>();
|
||||||
|
const tableRef = ref<VxeTableInstance>();
|
||||||
/** 初始化 */
|
/** 初始化 */
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
getList();
|
await getList();
|
||||||
|
await nextTick();
|
||||||
|
// 挂载 toolbar 工具栏
|
||||||
|
const table = tableRef.value;
|
||||||
|
const tableToolbar = tableToolbarRef.value;
|
||||||
|
if (table && tableToolbar) {
|
||||||
|
await table.connect(tableToolbar.getToolbarRef()!);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -128,23 +142,16 @@ onMounted(() => {
|
||||||
<Page auto-content-height>
|
<Page auto-content-height>
|
||||||
<FormModal @success="getList" />
|
<FormModal @success="getList" />
|
||||||
|
|
||||||
<ContentWrap>
|
<ContentWrap v-if="!hiddenSearchBar">
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<!-- TODO @puhui999:貌似 -mb-15px 没效果?可能和 ContentWrap 有关系? -->
|
<Form :model="queryParams" ref="queryFormRef" layout="inline">
|
||||||
<Form
|
|
||||||
class="-mb-15px"
|
|
||||||
:model="queryParams"
|
|
||||||
ref="queryFormRef"
|
|
||||||
layout="inline"
|
|
||||||
>
|
|
||||||
<Form.Item label="名字" name="name">
|
<Form.Item label="名字" name="name">
|
||||||
<!-- TODO @puhui999:貌似不一定 240?看着和 schema 还是不太一样 -->
|
|
||||||
<Input
|
<Input
|
||||||
v-model:value="queryParams.name"
|
v-model:value="queryParams.name"
|
||||||
placeholder="请输入名字"
|
placeholder="请输入名字"
|
||||||
allow-clear
|
allow-clear
|
||||||
@press-enter="handleQuery"
|
@press-enter="handleQuery"
|
||||||
class="!w-240px"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="性别" name="sex">
|
<Form.Item label="性别" name="sex">
|
||||||
|
|
@ -152,7 +159,7 @@ onMounted(() => {
|
||||||
v-model:value="queryParams.sex"
|
v-model:value="queryParams.sex"
|
||||||
placeholder="请选择性别"
|
placeholder="请选择性别"
|
||||||
allow-clear
|
allow-clear
|
||||||
class="!w-240px"
|
class="w-full"
|
||||||
>
|
>
|
||||||
<Select.Option
|
<Select.Option
|
||||||
v-for="dict in getDictOptions(
|
v-for="dict in getDictOptions(
|
||||||
|
|
@ -160,29 +167,35 @@ onMounted(() => {
|
||||||
'number',
|
'number',
|
||||||
)"
|
)"
|
||||||
:key="dict.value"
|
:key="dict.value"
|
||||||
:label="dict.label"
|
|
||||||
:value="dict.value"
|
:value="dict.value"
|
||||||
/>
|
>
|
||||||
|
{{ dict.label }}
|
||||||
|
</Select.Option>
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="创建时间" name="createTime">
|
<Form.Item label="创建时间" name="createTime">
|
||||||
<!-- TODO @puhui999:这里有个红色的告警,看看有办法处理哇? -->
|
|
||||||
<RangePicker
|
<RangePicker
|
||||||
v-model:value="queryParams.createTime"
|
v-model:value="queryParams.createTime"
|
||||||
v-bind="getRangePickerDefaultProps()"
|
v-bind="getRangePickerDefaultProps()"
|
||||||
class="!w-220px"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<!-- TODO @puhui999:搜索和重置;貌似样子和位置不太一样,有木有办法一致 -->
|
<Button class="ml-2" @click="resetQuery"> 重置 </Button>
|
||||||
<!-- TODO @puhui999:收齐、展开,好弄哇? -->
|
<Button class="ml-2" @click="handleQuery" type="primary">
|
||||||
<Button class="ml-2" @click="handleQuery" :icon="h(Search)">
|
|
||||||
搜索
|
搜索
|
||||||
</Button>
|
</Button>
|
||||||
<Button class="ml-2" @click="resetQuery" :icon="h(RefreshCw)">
|
</Form.Item>
|
||||||
重置
|
</Form>
|
||||||
</Button>
|
</ContentWrap>
|
||||||
<!-- TODO @puhui999:有办法放到 VxeTable 哪里么? -->
|
|
||||||
|
<!-- 列表 -->
|
||||||
|
<ContentWrap title="示例联系人">
|
||||||
|
<template #extra>
|
||||||
|
<TableToolbar
|
||||||
|
ref="tableToolbarRef"
|
||||||
|
v-model:hidden-search="hiddenSearchBar"
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
class="ml-2"
|
class="ml-2"
|
||||||
:icon="h(Plus)"
|
:icon="h(Plus)"
|
||||||
|
|
@ -202,14 +215,9 @@ onMounted(() => {
|
||||||
>
|
>
|
||||||
{{ $t('ui.actionTitle.export') }}
|
{{ $t('ui.actionTitle.export') }}
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</TableToolbar>
|
||||||
</Form>
|
</template>
|
||||||
</ContentWrap>
|
<VxeTable ref="tableRef" :data="list" show-overflow :loading="loading">
|
||||||
|
|
||||||
<!-- 列表 -->
|
|
||||||
<!-- TODO @puhui999:title 要不还是假起来? -->
|
|
||||||
<ContentWrap>
|
|
||||||
<VxeTable :data="list" show-overflow :loading="loading">
|
|
||||||
<VxeColumn field="id" title="编号" align="center" />
|
<VxeColumn field="id" title="编号" align="center" />
|
||||||
<VxeColumn field="name" title="名字" align="center" />
|
<VxeColumn field="name" title="名字" align="center" />
|
||||||
<VxeColumn field="sex" title="性别" align="center">
|
<VxeColumn field="sex" title="性别" align="center">
|
||||||
|
|
@ -253,7 +261,6 @@ onMounted(() => {
|
||||||
</VxeTable>
|
</VxeTable>
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<div class="mt-2 flex justify-end">
|
<div class="mt-2 flex justify-end">
|
||||||
<!-- TODO @puhui999:这个分页,看着不太一致 -->
|
|
||||||
<Pagination
|
<Pagination
|
||||||
:total="total"
|
:total="total"
|
||||||
v-model:current="queryParams.pageNo"
|
v-model:current="queryParams.pageNo"
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,9 @@ import { ImageUpload } from '#/components/upload';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
|
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
|
||||||
|
|
||||||
const emit = defineEmits(['success']); // TODO @puhui999:emit 和下面空一行?
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
const formRef = ref();
|
const formRef = ref();
|
||||||
// TODO @puhui999:labelCol、wrapperCol 直接写?不用单独定义变量,
|
|
||||||
const labelCol = { span: 5 };
|
|
||||||
const wrapperCol = { span: 13 };
|
|
||||||
const formData = ref<Partial<Demo01ContactApi.Demo01Contact>>({
|
const formData = ref<Partial<Demo01ContactApi.Demo01Contact>>({
|
||||||
id: undefined,
|
id: undefined,
|
||||||
name: undefined,
|
name: undefined,
|
||||||
|
|
@ -90,8 +88,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||||
resetForm();
|
resetForm();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 加载数据
|
||||||
// 加载数据 TODO @puhui999:这里不用空行
|
|
||||||
let data = modalApi.getData<Demo01ContactApi.Demo01Contact>();
|
let data = modalApi.getData<Demo01ContactApi.Demo01Contact>();
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -115,8 +112,8 @@ const [Modal, modalApi] = useVbenModal({
|
||||||
ref="formRef"
|
ref="formRef"
|
||||||
:model="formData"
|
:model="formData"
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
:label-col="labelCol"
|
:label-col="{ span: 5 }"
|
||||||
:wrapper-col="wrapperCol"
|
:wrapper-col="{ span: 18 }"
|
||||||
>
|
>
|
||||||
<Form.Item label="名字" name="name">
|
<Form.Item label="名字" name="name">
|
||||||
<Input v-model:value="formData.name" placeholder="请输入名字" />
|
<Input v-model:value="formData.name" placeholder="请输入名字" />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,266 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableInstance } from 'vxe-table';
|
||||||
|
|
||||||
|
import type { Demo02CategoryApi } from '#/api/infra/demo/demo02';
|
||||||
|
|
||||||
|
import { h, nextTick, onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { Download, Plus } from '@vben/icons';
|
||||||
|
import { cloneDeep, formatDateTime, isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { Button, Form, Input, message, RangePicker } from 'ant-design-vue';
|
||||||
|
import { VxeColumn, VxeTable } from 'vxe-table';
|
||||||
|
|
||||||
|
import {
|
||||||
|
deleteDemo02Category,
|
||||||
|
exportDemo02Category,
|
||||||
|
getDemo02CategoryList,
|
||||||
|
} from '#/api/infra/demo/demo02';
|
||||||
|
import { ContentWrap } from '#/components/content-wrap';
|
||||||
|
import { TableToolbar } from '#/components/table-toolbar';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils/date';
|
||||||
|
import { downloadByData } from '#/utils/download';
|
||||||
|
|
||||||
|
import Demo02CategoryForm from './modules/form.vue';
|
||||||
|
|
||||||
|
const loading = ref(true); // 列表的加载中
|
||||||
|
const list = ref<any[]>([]); // 树列表的数据
|
||||||
|
|
||||||
|
const queryParams = reactive({
|
||||||
|
name: undefined,
|
||||||
|
parentId: undefined,
|
||||||
|
createTime: undefined,
|
||||||
|
});
|
||||||
|
const queryFormRef = ref(); // 搜索的表单
|
||||||
|
const exportLoading = ref(false); // 导出的加载中
|
||||||
|
|
||||||
|
/** 查询列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const params = cloneDeep(queryParams) as any;
|
||||||
|
if (params.createTime && Array.isArray(params.createTime)) {
|
||||||
|
params.createTime = (params.createTime as string[]).join(',');
|
||||||
|
}
|
||||||
|
list.value = await getDemo02CategoryList(params);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
getList();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryFormRef.value.resetFields();
|
||||||
|
handleQuery();
|
||||||
|
};
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Demo02CategoryForm,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 创建示例分类 */
|
||||||
|
function onCreate() {
|
||||||
|
formModalApi.setData({}).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑示例分类 */
|
||||||
|
function onEdit(row: Demo02CategoryApi.Demo02Category) {
|
||||||
|
formModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增下级示例分类 */
|
||||||
|
function onAppend(row: Demo02CategoryApi.Demo02Category) {
|
||||||
|
formModalApi.setData({ parentId: row.id }).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除示例分类 */
|
||||||
|
async function onDelete(row: Demo02CategoryApi.Demo02Category) {
|
||||||
|
const hideLoading = message.loading({
|
||||||
|
content: $t('ui.actionMessage.deleting', [row.id]),
|
||||||
|
duration: 0,
|
||||||
|
key: 'action_process_msg',
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteDemo02Category(row.id as number);
|
||||||
|
message.success({
|
||||||
|
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
|
||||||
|
key: 'action_process_msg',
|
||||||
|
});
|
||||||
|
await getList();
|
||||||
|
} catch {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出表格 */
|
||||||
|
async function onExport() {
|
||||||
|
try {
|
||||||
|
exportLoading.value = true;
|
||||||
|
const data = await exportDemo02Category(queryParams);
|
||||||
|
downloadByData(data, '示例分类.xls');
|
||||||
|
} finally {
|
||||||
|
exportLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 隐藏搜索栏 */
|
||||||
|
const hiddenSearchBar = ref(false);
|
||||||
|
const tableToolbarRef = ref<InstanceType<typeof TableToolbar>>();
|
||||||
|
const tableRef = ref<VxeTableInstance>();
|
||||||
|
|
||||||
|
/** 切换树形展开/收缩状态 */
|
||||||
|
const isExpanded = ref(true);
|
||||||
|
function toggleExpand() {
|
||||||
|
isExpanded.value = !isExpanded.value;
|
||||||
|
tableRef.value?.setAllTreeExpand(isExpanded.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(async () => {
|
||||||
|
await getList();
|
||||||
|
await nextTick();
|
||||||
|
// 挂载 toolbar 工具栏
|
||||||
|
const table = tableRef.value;
|
||||||
|
const tableToolbar = tableToolbarRef.value;
|
||||||
|
if (table && tableToolbar) {
|
||||||
|
await table.connect(tableToolbar.getToolbarRef()!);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<FormModal @success="getList" />
|
||||||
|
|
||||||
|
<ContentWrap v-if="!hiddenSearchBar">
|
||||||
|
<!-- 搜索工作栏 -->
|
||||||
|
<Form :model="queryParams" ref="queryFormRef" layout="inline">
|
||||||
|
<Form.Item label="名字" name="name">
|
||||||
|
<Input
|
||||||
|
v-model:value="queryParams.name"
|
||||||
|
placeholder="请输入名字"
|
||||||
|
allow-clear
|
||||||
|
@press-enter="handleQuery"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="父级编号" name="parentId">
|
||||||
|
<Input
|
||||||
|
v-model:value="queryParams.parentId"
|
||||||
|
placeholder="请输入父级编号"
|
||||||
|
allow-clear
|
||||||
|
@press-enter="handleQuery"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="创建时间" name="createTime">
|
||||||
|
<RangePicker
|
||||||
|
v-model:value="queryParams.createTime"
|
||||||
|
v-bind="getRangePickerDefaultProps()"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Button class="ml-2" @click="resetQuery"> 重置 </Button>
|
||||||
|
<Button class="ml-2" @click="handleQuery" type="primary">
|
||||||
|
搜索
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</ContentWrap>
|
||||||
|
|
||||||
|
<!-- 列表 -->
|
||||||
|
<ContentWrap title="示例分类">
|
||||||
|
<template #extra>
|
||||||
|
<TableToolbar
|
||||||
|
ref="tableToolbarRef"
|
||||||
|
v-model:hidden-search="hiddenSearchBar"
|
||||||
|
>
|
||||||
|
<Button @click="toggleExpand" class="mr-2">
|
||||||
|
{{ isExpanded ? '收缩' : '展开' }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
class="ml-2"
|
||||||
|
:icon="h(Plus)"
|
||||||
|
type="primary"
|
||||||
|
@click="onCreate"
|
||||||
|
v-access:code="['infra:demo02-category:create']"
|
||||||
|
>
|
||||||
|
{{ $t('ui.actionTitle.create', ['示例分类']) }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
:icon="h(Download)"
|
||||||
|
type="primary"
|
||||||
|
class="ml-2"
|
||||||
|
:loading="exportLoading"
|
||||||
|
@click="onExport"
|
||||||
|
v-access:code="['infra:demo02-category:export']"
|
||||||
|
>
|
||||||
|
{{ $t('ui.actionTitle.export') }}
|
||||||
|
</Button>
|
||||||
|
</TableToolbar>
|
||||||
|
</template>
|
||||||
|
<VxeTable
|
||||||
|
ref="tableRef"
|
||||||
|
:data="list"
|
||||||
|
:tree-config="{
|
||||||
|
parentField: 'parentId',
|
||||||
|
rowField: 'id',
|
||||||
|
transform: true,
|
||||||
|
expandAll: true,
|
||||||
|
reserve: true,
|
||||||
|
}"
|
||||||
|
show-overflow
|
||||||
|
:loading="loading"
|
||||||
|
>
|
||||||
|
<VxeColumn field="id" title="编号" align="center" />
|
||||||
|
<VxeColumn field="name" title="名字" align="center" tree-node />
|
||||||
|
<VxeColumn field="parentId" title="父级编号" align="center" />
|
||||||
|
<VxeColumn field="createTime" title="创建时间" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDateTime(row.createTime) }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="operation" title="操作" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
@click="onAppend(row as any)"
|
||||||
|
v-access:code="['infra:demo02-category:create']"
|
||||||
|
>
|
||||||
|
新增下级
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
@click="onEdit(row as any)"
|
||||||
|
v-access:code="['infra:demo02-category:update']"
|
||||||
|
>
|
||||||
|
{{ $t('ui.actionTitle.edit') }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
danger
|
||||||
|
class="ml-2"
|
||||||
|
:disabled="!isEmpty(row?.children)"
|
||||||
|
@click="onDelete(row as any)"
|
||||||
|
v-access:code="['infra:demo02-category:delete']"
|
||||||
|
>
|
||||||
|
{{ $t('ui.actionTitle.delete') }}
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
</VxeTable>
|
||||||
|
</ContentWrap>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Rule } from 'ant-design-vue/es/form';
|
||||||
|
|
||||||
|
import type { Demo02CategoryApi } from '#/api/infra/demo/demo02';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
import { handleTree } from '@vben/utils';
|
||||||
|
|
||||||
|
import { Form, Input, message, TreeSelect } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createDemo02Category,
|
||||||
|
getDemo02Category,
|
||||||
|
getDemo02CategoryList,
|
||||||
|
updateDemo02Category,
|
||||||
|
} from '#/api/infra/demo/demo02';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
|
const formRef = ref();
|
||||||
|
const formData = ref<Partial<Demo02CategoryApi.Demo02Category>>({
|
||||||
|
id: undefined,
|
||||||
|
name: undefined,
|
||||||
|
parentId: undefined,
|
||||||
|
});
|
||||||
|
const rules: Record<string, Rule[]> = {
|
||||||
|
name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
|
||||||
|
parentId: [{ required: true, message: '父级编号不能为空', trigger: 'blur' }],
|
||||||
|
};
|
||||||
|
const demo02CategoryTree = ref<any[]>([]); // 树形结构
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', ['示例分类'])
|
||||||
|
: $t('ui.actionTitle.create', ['示例分类']);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 重置表单 */
|
||||||
|
const resetForm = () => {
|
||||||
|
formData.value = {
|
||||||
|
id: undefined,
|
||||||
|
name: undefined,
|
||||||
|
parentId: undefined,
|
||||||
|
};
|
||||||
|
formRef.value?.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获得示例分类树 */
|
||||||
|
const getDemo02CategoryTree = async () => {
|
||||||
|
demo02CategoryTree.value = [];
|
||||||
|
const data = await getDemo02CategoryList({});
|
||||||
|
data.unshift({
|
||||||
|
id: 0,
|
||||||
|
name: '顶级示例分类',
|
||||||
|
});
|
||||||
|
demo02CategoryTree.value = handleTree(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
await formRef.value?.validate();
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data = formData.value as Demo02CategoryApi.Demo02Category;
|
||||||
|
try {
|
||||||
|
await (formData.value?.id
|
||||||
|
? updateDemo02Category(data)
|
||||||
|
: createDemo02Category(data));
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success({
|
||||||
|
content: $t('ui.actionMessage.operationSuccess'),
|
||||||
|
key: 'action_process_msg',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
resetForm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
let data = modalApi.getData<Demo02CategoryApi.Demo02Category>();
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.id) {
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
data = await getDemo02Category(data.id);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
formData.value = data;
|
||||||
|
// 加载树数据
|
||||||
|
await getDemo02CategoryTree();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle">
|
||||||
|
<Form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="rules"
|
||||||
|
:label-col="{ span: 5 }"
|
||||||
|
:wrapper-col="{ span: 18 }"
|
||||||
|
>
|
||||||
|
<Form.Item label="名字" name="name">
|
||||||
|
<Input v-model:value="formData.name" placeholder="请输入名字" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="父级编号" name="parentId">
|
||||||
|
<TreeSelect
|
||||||
|
v-model:value="formData.parentId"
|
||||||
|
:tree-data="demo02CategoryTree"
|
||||||
|
:field-names="{
|
||||||
|
label: 'name',
|
||||||
|
value: 'id',
|
||||||
|
children: 'children',
|
||||||
|
}"
|
||||||
|
checkable
|
||||||
|
tree-default-expand-all
|
||||||
|
placeholder="请选择父级编号"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,324 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableInstance } from 'vxe-table';
|
||||||
|
|
||||||
|
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/erp';
|
||||||
|
|
||||||
|
import { h, nextTick, onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { Download, Plus } from '@vben/icons';
|
||||||
|
import { cloneDeep, formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DatePicker,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
message,
|
||||||
|
Pagination,
|
||||||
|
RangePicker,
|
||||||
|
Select,
|
||||||
|
Tabs,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
import { VxeColumn, VxeTable } from 'vxe-table';
|
||||||
|
|
||||||
|
import {
|
||||||
|
deleteDemo03Student,
|
||||||
|
exportDemo03Student,
|
||||||
|
getDemo03StudentPage,
|
||||||
|
} from '#/api/infra/demo/demo03/erp';
|
||||||
|
import { ContentWrap } from '#/components/content-wrap';
|
||||||
|
import { DictTag } from '#/components/dict-tag';
|
||||||
|
import { TableToolbar } from '#/components/table-toolbar';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils/date';
|
||||||
|
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
|
||||||
|
import { downloadByData } from '#/utils/download';
|
||||||
|
|
||||||
|
import Demo03CourseList from './modules/demo03-course-list.vue';
|
||||||
|
import Demo03GradeList from './modules/demo03-grade-list.vue';
|
||||||
|
import Demo03StudentForm from './modules/form.vue';
|
||||||
|
|
||||||
|
/** 子表的列表 */
|
||||||
|
const subTabsName = ref('demo03Course');
|
||||||
|
const selectDemo03Student = ref<Demo03StudentApi.Demo03Student>();
|
||||||
|
async function onCellClick({ row }: { row: Demo03StudentApi.Demo03Student }) {
|
||||||
|
selectDemo03Student.value = row;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loading = ref(true); // 列表的加载中
|
||||||
|
const list = ref<Demo03StudentApi.Demo03Student[]>([]); // 列表的数据
|
||||||
|
|
||||||
|
const total = ref(0); // 列表的总页数
|
||||||
|
const queryParams = reactive({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
name: undefined,
|
||||||
|
sex: undefined,
|
||||||
|
birthday: undefined,
|
||||||
|
description: undefined,
|
||||||
|
createTime: undefined,
|
||||||
|
});
|
||||||
|
const queryFormRef = ref(); // 搜索的表单
|
||||||
|
const exportLoading = ref(false); // 导出的加载中
|
||||||
|
|
||||||
|
/** 查询列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const params = cloneDeep(queryParams) as any;
|
||||||
|
if (params.birthday && Array.isArray(params.birthday)) {
|
||||||
|
params.birthday = (params.birthday as string[]).join(',');
|
||||||
|
}
|
||||||
|
if (params.createTime && Array.isArray(params.createTime)) {
|
||||||
|
params.createTime = (params.createTime as string[]).join(',');
|
||||||
|
}
|
||||||
|
const data = await getDemo03StudentPage(params);
|
||||||
|
list.value = data.list;
|
||||||
|
total.value = data.total;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.pageNo = 1;
|
||||||
|
getList();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryFormRef.value.resetFields();
|
||||||
|
handleQuery();
|
||||||
|
};
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Demo03StudentForm,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 创建学生 */
|
||||||
|
function onCreate() {
|
||||||
|
formModalApi.setData({}).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑学生 */
|
||||||
|
function onEdit(row: Demo03StudentApi.Demo03Student) {
|
||||||
|
formModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除学生 */
|
||||||
|
async function onDelete(row: Demo03StudentApi.Demo03Student) {
|
||||||
|
const hideLoading = message.loading({
|
||||||
|
content: $t('ui.actionMessage.deleting', [row.id]),
|
||||||
|
duration: 0,
|
||||||
|
key: 'action_process_msg',
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteDemo03Student(row.id as number);
|
||||||
|
message.success({
|
||||||
|
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
|
||||||
|
key: 'action_process_msg',
|
||||||
|
});
|
||||||
|
await getList();
|
||||||
|
} catch {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出表格 */
|
||||||
|
async function onExport() {
|
||||||
|
try {
|
||||||
|
exportLoading.value = true;
|
||||||
|
const data = await exportDemo03Student(queryParams);
|
||||||
|
downloadByData(data, '学生.xls');
|
||||||
|
} finally {
|
||||||
|
exportLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 隐藏搜索栏 */
|
||||||
|
const hiddenSearchBar = ref(false);
|
||||||
|
const tableToolbarRef = ref<InstanceType<typeof TableToolbar>>();
|
||||||
|
const tableRef = ref<VxeTableInstance>();
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(async () => {
|
||||||
|
await getList();
|
||||||
|
await nextTick();
|
||||||
|
// 挂载 toolbar 工具栏
|
||||||
|
const table = tableRef.value;
|
||||||
|
const tableToolbar = tableToolbarRef.value;
|
||||||
|
if (table && tableToolbar) {
|
||||||
|
await table.connect(tableToolbar.getToolbarRef()!);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<FormModal @success="getList" />
|
||||||
|
|
||||||
|
<ContentWrap v-if="!hiddenSearchBar">
|
||||||
|
<!-- 搜索工作栏 -->
|
||||||
|
<Form :model="queryParams" ref="queryFormRef" layout="inline">
|
||||||
|
<Form.Item label="名字" name="name">
|
||||||
|
<Input
|
||||||
|
v-model:value="queryParams.name"
|
||||||
|
placeholder="请输入名字"
|
||||||
|
allow-clear
|
||||||
|
@press-enter="handleQuery"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="性别" name="sex">
|
||||||
|
<Select
|
||||||
|
v-model:value="queryParams.sex"
|
||||||
|
placeholder="请选择性别"
|
||||||
|
allow-clear
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
<Select.Option
|
||||||
|
v-for="dict in getDictOptions(
|
||||||
|
DICT_TYPE.SYSTEM_USER_SEX,
|
||||||
|
'number',
|
||||||
|
)"
|
||||||
|
:key="dict.value"
|
||||||
|
:value="dict.value"
|
||||||
|
>
|
||||||
|
{{ dict.label }}
|
||||||
|
</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="出生日期" name="birthday">
|
||||||
|
<DatePicker
|
||||||
|
v-model:value="queryParams.birthday"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
placeholder="选择出生日期"
|
||||||
|
allow-clear
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="创建时间" name="createTime">
|
||||||
|
<RangePicker
|
||||||
|
v-model:value="queryParams.createTime"
|
||||||
|
v-bind="getRangePickerDefaultProps()"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Button class="ml-2" @click="resetQuery"> 重置 </Button>
|
||||||
|
<Button class="ml-2" @click="handleQuery" type="primary">
|
||||||
|
搜索
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</ContentWrap>
|
||||||
|
|
||||||
|
<!-- 列表 -->
|
||||||
|
<ContentWrap title="学生">
|
||||||
|
<template #extra>
|
||||||
|
<TableToolbar
|
||||||
|
ref="tableToolbarRef"
|
||||||
|
v-model:hidden-search="hiddenSearchBar"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
class="ml-2"
|
||||||
|
:icon="h(Plus)"
|
||||||
|
type="primary"
|
||||||
|
@click="onCreate"
|
||||||
|
v-access:code="['infra:demo03-student:create']"
|
||||||
|
>
|
||||||
|
{{ $t('ui.actionTitle.create', ['学生']) }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
:icon="h(Download)"
|
||||||
|
type="primary"
|
||||||
|
class="ml-2"
|
||||||
|
:loading="exportLoading"
|
||||||
|
@click="onExport"
|
||||||
|
v-access:code="['infra:demo03-student:export']"
|
||||||
|
>
|
||||||
|
{{ $t('ui.actionTitle.export') }}
|
||||||
|
</Button>
|
||||||
|
</TableToolbar>
|
||||||
|
</template>
|
||||||
|
<VxeTable
|
||||||
|
ref="tableRef"
|
||||||
|
:data="list"
|
||||||
|
@cell-click="onCellClick"
|
||||||
|
:row-config="{
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
isCurrent: true,
|
||||||
|
}"
|
||||||
|
show-overflow
|
||||||
|
:loading="loading"
|
||||||
|
>
|
||||||
|
<VxeColumn field="id" title="编号" align="center" />
|
||||||
|
<VxeColumn field="name" title="名字" align="center" />
|
||||||
|
<VxeColumn field="sex" title="性别" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<DictTag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" />
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="birthday" title="出生日期" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDateTime(row.birthday) }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="description" title="简介" align="center" />
|
||||||
|
<VxeColumn field="createTime" title="创建时间" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDateTime(row.createTime) }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="operation" title="操作" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
@click="onEdit(row as any)"
|
||||||
|
v-access:code="['infra:demo03-student:update']"
|
||||||
|
>
|
||||||
|
{{ $t('ui.actionTitle.edit') }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
danger
|
||||||
|
class="ml-2"
|
||||||
|
@click="onDelete(row as any)"
|
||||||
|
v-access:code="['infra:demo03-student:delete']"
|
||||||
|
>
|
||||||
|
{{ $t('ui.actionTitle.delete') }}
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
</VxeTable>
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="mt-2 flex justify-end">
|
||||||
|
<Pagination
|
||||||
|
:total="total"
|
||||||
|
v-model:current="queryParams.pageNo"
|
||||||
|
v-model:page-size="queryParams.pageSize"
|
||||||
|
show-size-changer
|
||||||
|
@change="getList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ContentWrap>
|
||||||
|
|
||||||
|
<ContentWrap>
|
||||||
|
<!-- 子表的表单 -->
|
||||||
|
<Tabs v-model:active-key="subTabsName">
|
||||||
|
<Tabs.TabPane key="demo03Course" tab="学生课程" force-render>
|
||||||
|
<Demo03CourseList :student-id="selectDemo03Student?.id" />
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane key="demo03Grade" tab="学生班级" force-render>
|
||||||
|
<Demo03GradeList :student-id="selectDemo03Student?.id" />
|
||||||
|
</Tabs.TabPane>
|
||||||
|
</Tabs>
|
||||||
|
</ContentWrap>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Rule } from 'ant-design-vue/es/form';
|
||||||
|
|
||||||
|
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/erp';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Form, Input, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createDemo03Course,
|
||||||
|
getDemo03Course,
|
||||||
|
updateDemo03Course,
|
||||||
|
} from '#/api/infra/demo/demo03/erp';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', ['学生课程'])
|
||||||
|
: $t('ui.actionTitle.create', ['学生课程']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const formRef = ref();
|
||||||
|
const formData = ref<Partial<Demo03StudentApi.Demo03Course>>({
|
||||||
|
id: undefined,
|
||||||
|
studentId: undefined,
|
||||||
|
name: undefined,
|
||||||
|
score: undefined,
|
||||||
|
});
|
||||||
|
const rules: Record<string, Rule[]> = {
|
||||||
|
studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
|
||||||
|
name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
|
||||||
|
score: [{ required: true, message: '分数不能为空', trigger: 'blur' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
await formRef.value?.validate();
|
||||||
|
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data = formData.value as Demo03StudentApi.Demo03Course;
|
||||||
|
try {
|
||||||
|
await (formData.value?.id
|
||||||
|
? updateDemo03Course(data)
|
||||||
|
: createDemo03Course(data));
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success({
|
||||||
|
content: $t('ui.actionMessage.operationSuccess'),
|
||||||
|
key: 'action_process_msg',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
resetForm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
let data = modalApi.getData<Demo03StudentApi.Demo03Course>();
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.id) {
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
data = await getDemo03Course(data.id);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 设置到 values
|
||||||
|
formData.value = data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 重置表单 */
|
||||||
|
const resetForm = () => {
|
||||||
|
formData.value = {
|
||||||
|
id: undefined,
|
||||||
|
studentId: undefined,
|
||||||
|
name: undefined,
|
||||||
|
score: undefined,
|
||||||
|
};
|
||||||
|
formRef.value?.resetFields();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle">
|
||||||
|
<Form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="rules"
|
||||||
|
:label-col="{ span: 5 }"
|
||||||
|
:wrapper-col="{ span: 18 }"
|
||||||
|
>
|
||||||
|
<Form.Item label="学生编号" name="studentId">
|
||||||
|
<Input
|
||||||
|
v-model:value="formData.studentId"
|
||||||
|
placeholder="请输入学生编号"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="名字" name="name">
|
||||||
|
<Input v-model:value="formData.name" placeholder="请输入名字" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="分数" name="score">
|
||||||
|
<Input v-model:value="formData.score" placeholder="请输入分数" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,265 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableInstance } from 'vxe-table';
|
||||||
|
|
||||||
|
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/erp';
|
||||||
|
|
||||||
|
import { h, nextTick, onMounted, reactive, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
import { Plus } from '@vben/icons';
|
||||||
|
import { cloneDeep, formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
message,
|
||||||
|
Pagination,
|
||||||
|
RangePicker,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
import { VxeColumn, VxeTable } from 'vxe-table';
|
||||||
|
|
||||||
|
import {
|
||||||
|
deleteDemo03Course,
|
||||||
|
getDemo03CoursePage,
|
||||||
|
} from '#/api/infra/demo/demo03/erp';
|
||||||
|
import { ContentWrap } from '#/components/content-wrap';
|
||||||
|
import { TableToolbar } from '#/components/table-toolbar';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils/date';
|
||||||
|
|
||||||
|
import Demo03CourseForm from './demo03-course-form.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
studentId?: number; // 学生编号(主表的关联字段)
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Demo03CourseForm,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 创建学生课程 */
|
||||||
|
function onCreate() {
|
||||||
|
if (!props.studentId) {
|
||||||
|
message.warning('请先选择一个学生!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
formModalApi.setData({ studentId: props.studentId }).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑学生课程 */
|
||||||
|
function onEdit(row: Demo03StudentApi.Demo03Course) {
|
||||||
|
formModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除学生课程 */
|
||||||
|
async function onDelete(row: Demo03StudentApi.Demo03Course) {
|
||||||
|
const hideLoading = message.loading({
|
||||||
|
content: $t('ui.actionMessage.deleting', [row.id]),
|
||||||
|
duration: 0,
|
||||||
|
key: 'action_process_msg',
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteDemo03Course(row.id as number);
|
||||||
|
message.success({
|
||||||
|
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
|
||||||
|
key: 'action_process_msg',
|
||||||
|
});
|
||||||
|
getList();
|
||||||
|
} catch {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loading = ref(true); // 列表的加载中
|
||||||
|
const list = ref<Demo03StudentApi.Demo03Course[]>([]); // 列表的数据
|
||||||
|
const total = ref(0); // 列表的总页数
|
||||||
|
const queryFormRef = ref(); // 搜索的表单
|
||||||
|
const queryParams = reactive({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
studentId: undefined,
|
||||||
|
name: undefined,
|
||||||
|
score: undefined,
|
||||||
|
createTime: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.pageNo = 1;
|
||||||
|
getList();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryFormRef.value.resetFields();
|
||||||
|
handleQuery();
|
||||||
|
};
|
||||||
|
/** 查询列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
if (!props.studentId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const params = cloneDeep(queryParams) as any;
|
||||||
|
if (params.birthday && Array.isArray(params.birthday)) {
|
||||||
|
params.birthday = (params.birthday as string[]).join(',');
|
||||||
|
}
|
||||||
|
if (params.createTime && Array.isArray(params.createTime)) {
|
||||||
|
params.createTime = (params.createTime as string[]).join(',');
|
||||||
|
}
|
||||||
|
params.studentId = props.studentId;
|
||||||
|
const data = await getDemo03CoursePage(params);
|
||||||
|
list.value = data.list;
|
||||||
|
total.value = data.total;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||||
|
watch(
|
||||||
|
() => props.studentId,
|
||||||
|
async (val) => {
|
||||||
|
if (!val) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await nextTick();
|
||||||
|
await getList();
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 隐藏搜索栏 */
|
||||||
|
const hiddenSearchBar = ref(false);
|
||||||
|
const tableToolbarRef = ref<InstanceType<typeof TableToolbar>>();
|
||||||
|
const tableRef = ref<VxeTableInstance>();
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(async () => {
|
||||||
|
await getList();
|
||||||
|
await nextTick();
|
||||||
|
// 挂载 toolbar 工具栏
|
||||||
|
const table = tableRef.value;
|
||||||
|
const tableToolbar = tableToolbarRef.value;
|
||||||
|
if (table && tableToolbar) {
|
||||||
|
await table.connect(tableToolbar.getToolbarRef()!);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FormModal @success="getList" />
|
||||||
|
<div class="h-[600px]">
|
||||||
|
<ContentWrap v-if="!hiddenSearchBar">
|
||||||
|
<!-- 搜索工作栏 -->
|
||||||
|
<Form :model="queryParams" ref="queryFormRef" layout="inline">
|
||||||
|
<Form.Item label="学生编号" name="studentId">
|
||||||
|
<Input
|
||||||
|
v-model:value="queryParams.studentId"
|
||||||
|
placeholder="请输入学生编号"
|
||||||
|
allow-clear
|
||||||
|
@press-enter="handleQuery"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="名字" name="name">
|
||||||
|
<Input
|
||||||
|
v-model:value="queryParams.name"
|
||||||
|
placeholder="请输入名字"
|
||||||
|
allow-clear
|
||||||
|
@press-enter="handleQuery"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="分数" name="score">
|
||||||
|
<Input
|
||||||
|
v-model:value="queryParams.score"
|
||||||
|
placeholder="请输入分数"
|
||||||
|
allow-clear
|
||||||
|
@press-enter="handleQuery"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="创建时间" name="createTime">
|
||||||
|
<RangePicker
|
||||||
|
v-model:value="queryParams.createTime"
|
||||||
|
v-bind="getRangePickerDefaultProps()"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Button class="ml-2" @click="resetQuery"> 重置 </Button>
|
||||||
|
<Button class="ml-2" @click="handleQuery" type="primary">
|
||||||
|
搜索
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</ContentWrap>
|
||||||
|
|
||||||
|
<!-- 列表 -->
|
||||||
|
<ContentWrap title="学生">
|
||||||
|
<template #extra>
|
||||||
|
<TableToolbar
|
||||||
|
ref="tableToolbarRef"
|
||||||
|
v-model:hidden-search="hiddenSearchBar"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
class="ml-2"
|
||||||
|
:icon="h(Plus)"
|
||||||
|
type="primary"
|
||||||
|
@click="onCreate"
|
||||||
|
v-access:code="['infra:demo03-student:create']"
|
||||||
|
>
|
||||||
|
{{ $t('ui.actionTitle.create', ['学生']) }}
|
||||||
|
</Button>
|
||||||
|
</TableToolbar>
|
||||||
|
</template>
|
||||||
|
<VxeTable ref="tableRef" :data="list" show-overflow :loading="loading">
|
||||||
|
<VxeColumn field="id" title="编号" align="center" />
|
||||||
|
<VxeColumn field="studentId" title="学生编号" align="center" />
|
||||||
|
<VxeColumn field="name" title="名字" align="center" />
|
||||||
|
<VxeColumn field="score" title="分数" align="center" />
|
||||||
|
<VxeColumn field="createTime" title="创建时间" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDateTime(row.createTime) }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="operation" title="操作" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
@click="onEdit(row as any)"
|
||||||
|
v-access:code="['infra:demo03-student:update']"
|
||||||
|
>
|
||||||
|
{{ $t('ui.actionTitle.edit') }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
danger
|
||||||
|
class="ml-2"
|
||||||
|
@click="onDelete(row as any)"
|
||||||
|
v-access:code="['infra:demo03-student:delete']"
|
||||||
|
>
|
||||||
|
{{ $t('ui.actionTitle.delete') }}
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
</VxeTable>
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="mt-2 flex justify-end">
|
||||||
|
<Pagination
|
||||||
|
:total="total"
|
||||||
|
v-model:current="queryParams.pageNo"
|
||||||
|
v-model:page-size="queryParams.pageSize"
|
||||||
|
show-size-changer
|
||||||
|
@change="getList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ContentWrap>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Rule } from 'ant-design-vue/es/form';
|
||||||
|
|
||||||
|
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/erp';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Form, Input, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createDemo03Grade,
|
||||||
|
getDemo03Grade,
|
||||||
|
updateDemo03Grade,
|
||||||
|
} from '#/api/infra/demo/demo03/erp';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', ['学生班级'])
|
||||||
|
: $t('ui.actionTitle.create', ['学生班级']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const formRef = ref();
|
||||||
|
const formData = ref<Partial<Demo03StudentApi.Demo03Grade>>({
|
||||||
|
id: undefined,
|
||||||
|
studentId: undefined,
|
||||||
|
name: undefined,
|
||||||
|
teacher: undefined,
|
||||||
|
});
|
||||||
|
const rules: Record<string, Rule[]> = {
|
||||||
|
studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
|
||||||
|
name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
|
||||||
|
teacher: [{ required: true, message: '班主任不能为空', trigger: 'blur' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
await formRef.value?.validate();
|
||||||
|
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data = formData.value as Demo03StudentApi.Demo03Grade;
|
||||||
|
try {
|
||||||
|
await (formData.value?.id
|
||||||
|
? updateDemo03Grade(data)
|
||||||
|
: createDemo03Grade(data));
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success({
|
||||||
|
content: $t('ui.actionMessage.operationSuccess'),
|
||||||
|
key: 'action_process_msg',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
resetForm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
let data = modalApi.getData<Demo03StudentApi.Demo03Grade>();
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.id) {
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
data = await getDemo03Grade(data.id);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 设置到 values
|
||||||
|
formData.value = data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 重置表单 */
|
||||||
|
const resetForm = () => {
|
||||||
|
formData.value = {
|
||||||
|
id: undefined,
|
||||||
|
studentId: undefined,
|
||||||
|
name: undefined,
|
||||||
|
teacher: undefined,
|
||||||
|
};
|
||||||
|
formRef.value?.resetFields();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle">
|
||||||
|
<Form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="rules"
|
||||||
|
:label-col="{ span: 5 }"
|
||||||
|
:wrapper-col="{ span: 18 }"
|
||||||
|
>
|
||||||
|
<Form.Item label="学生编号" name="studentId">
|
||||||
|
<Input
|
||||||
|
v-model:value="formData.studentId"
|
||||||
|
placeholder="请输入学生编号"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="名字" name="name">
|
||||||
|
<Input v-model:value="formData.name" placeholder="请输入名字" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="班主任" name="teacher">
|
||||||
|
<Input v-model:value="formData.teacher" placeholder="请输入班主任" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,265 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableInstance } from 'vxe-table';
|
||||||
|
|
||||||
|
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/erp';
|
||||||
|
|
||||||
|
import { h, nextTick, onMounted, reactive, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
import { Plus } from '@vben/icons';
|
||||||
|
import { cloneDeep, formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
message,
|
||||||
|
Pagination,
|
||||||
|
RangePicker,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
import { VxeColumn, VxeTable } from 'vxe-table';
|
||||||
|
|
||||||
|
import {
|
||||||
|
deleteDemo03Grade,
|
||||||
|
getDemo03GradePage,
|
||||||
|
} from '#/api/infra/demo/demo03/erp';
|
||||||
|
import { ContentWrap } from '#/components/content-wrap';
|
||||||
|
import { TableToolbar } from '#/components/table-toolbar';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils/date';
|
||||||
|
|
||||||
|
import Demo03GradeForm from './demo03-grade-form.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
studentId?: number; // 学生编号(主表的关联字段)
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Demo03GradeForm,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 创建学生班级 */
|
||||||
|
function onCreate() {
|
||||||
|
if (!props.studentId) {
|
||||||
|
message.warning('请先选择一个学生!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
formModalApi.setData({ studentId: props.studentId }).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑学生班级 */
|
||||||
|
function onEdit(row: Demo03StudentApi.Demo03Grade) {
|
||||||
|
formModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除学生班级 */
|
||||||
|
async function onDelete(row: Demo03StudentApi.Demo03Grade) {
|
||||||
|
const hideLoading = message.loading({
|
||||||
|
content: $t('ui.actionMessage.deleting', [row.id]),
|
||||||
|
duration: 0,
|
||||||
|
key: 'action_process_msg',
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteDemo03Grade(row.id as number);
|
||||||
|
message.success({
|
||||||
|
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
|
||||||
|
key: 'action_process_msg',
|
||||||
|
});
|
||||||
|
getList();
|
||||||
|
} catch {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loading = ref(true); // 列表的加载中
|
||||||
|
const list = ref<Demo03StudentApi.Demo03Grade[]>([]); // 列表的数据
|
||||||
|
const total = ref(0); // 列表的总页数
|
||||||
|
const queryFormRef = ref(); // 搜索的表单
|
||||||
|
const queryParams = reactive({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
studentId: undefined,
|
||||||
|
name: undefined,
|
||||||
|
teacher: undefined,
|
||||||
|
createTime: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.pageNo = 1;
|
||||||
|
getList();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryFormRef.value.resetFields();
|
||||||
|
handleQuery();
|
||||||
|
};
|
||||||
|
/** 查询列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
if (!props.studentId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const params = cloneDeep(queryParams) as any;
|
||||||
|
if (params.birthday && Array.isArray(params.birthday)) {
|
||||||
|
params.birthday = (params.birthday as string[]).join(',');
|
||||||
|
}
|
||||||
|
if (params.createTime && Array.isArray(params.createTime)) {
|
||||||
|
params.createTime = (params.createTime as string[]).join(',');
|
||||||
|
}
|
||||||
|
params.studentId = props.studentId;
|
||||||
|
const data = await getDemo03GradePage(params);
|
||||||
|
list.value = data.list;
|
||||||
|
total.value = data.total;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||||
|
watch(
|
||||||
|
() => props.studentId,
|
||||||
|
async (val) => {
|
||||||
|
if (!val) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await nextTick();
|
||||||
|
await getList();
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 隐藏搜索栏 */
|
||||||
|
const hiddenSearchBar = ref(false);
|
||||||
|
const tableToolbarRef = ref<InstanceType<typeof TableToolbar>>();
|
||||||
|
const tableRef = ref<VxeTableInstance>();
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(async () => {
|
||||||
|
await getList();
|
||||||
|
await nextTick();
|
||||||
|
// 挂载 toolbar 工具栏
|
||||||
|
const table = tableRef.value;
|
||||||
|
const tableToolbar = tableToolbarRef.value;
|
||||||
|
if (table && tableToolbar) {
|
||||||
|
await table.connect(tableToolbar.getToolbarRef()!);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FormModal @success="getList" />
|
||||||
|
<div class="h-[600px]">
|
||||||
|
<ContentWrap v-if="!hiddenSearchBar">
|
||||||
|
<!-- 搜索工作栏 -->
|
||||||
|
<Form :model="queryParams" ref="queryFormRef" layout="inline">
|
||||||
|
<Form.Item label="学生编号" name="studentId">
|
||||||
|
<Input
|
||||||
|
v-model:value="queryParams.studentId"
|
||||||
|
placeholder="请输入学生编号"
|
||||||
|
allow-clear
|
||||||
|
@press-enter="handleQuery"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="名字" name="name">
|
||||||
|
<Input
|
||||||
|
v-model:value="queryParams.name"
|
||||||
|
placeholder="请输入名字"
|
||||||
|
allow-clear
|
||||||
|
@press-enter="handleQuery"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="班主任" name="teacher">
|
||||||
|
<Input
|
||||||
|
v-model:value="queryParams.teacher"
|
||||||
|
placeholder="请输入班主任"
|
||||||
|
allow-clear
|
||||||
|
@press-enter="handleQuery"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="创建时间" name="createTime">
|
||||||
|
<RangePicker
|
||||||
|
v-model:value="queryParams.createTime"
|
||||||
|
v-bind="getRangePickerDefaultProps()"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Button class="ml-2" @click="resetQuery"> 重置 </Button>
|
||||||
|
<Button class="ml-2" @click="handleQuery" type="primary">
|
||||||
|
搜索
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</ContentWrap>
|
||||||
|
|
||||||
|
<!-- 列表 -->
|
||||||
|
<ContentWrap title="学生">
|
||||||
|
<template #extra>
|
||||||
|
<TableToolbar
|
||||||
|
ref="tableToolbarRef"
|
||||||
|
v-model:hidden-search="hiddenSearchBar"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
class="ml-2"
|
||||||
|
:icon="h(Plus)"
|
||||||
|
type="primary"
|
||||||
|
@click="onCreate"
|
||||||
|
v-access:code="['infra:demo03-student:create']"
|
||||||
|
>
|
||||||
|
{{ $t('ui.actionTitle.create', ['学生']) }}
|
||||||
|
</Button>
|
||||||
|
</TableToolbar>
|
||||||
|
</template>
|
||||||
|
<VxeTable ref="tableRef" :data="list" show-overflow :loading="loading">
|
||||||
|
<VxeColumn field="id" title="编号" align="center" />
|
||||||
|
<VxeColumn field="studentId" title="学生编号" align="center" />
|
||||||
|
<VxeColumn field="name" title="名字" align="center" />
|
||||||
|
<VxeColumn field="teacher" title="班主任" align="center" />
|
||||||
|
<VxeColumn field="createTime" title="创建时间" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDateTime(row.createTime) }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="operation" title="操作" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
@click="onEdit(row as any)"
|
||||||
|
v-access:code="['infra:demo03-student:update']"
|
||||||
|
>
|
||||||
|
{{ $t('ui.actionTitle.edit') }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
danger
|
||||||
|
class="ml-2"
|
||||||
|
@click="onDelete(row as any)"
|
||||||
|
v-access:code="['infra:demo03-student:delete']"
|
||||||
|
>
|
||||||
|
{{ $t('ui.actionTitle.delete') }}
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
</VxeTable>
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="mt-2 flex justify-end">
|
||||||
|
<Pagination
|
||||||
|
:total="total"
|
||||||
|
v-model:current="queryParams.pageNo"
|
||||||
|
v-model:page-size="queryParams.pageSize"
|
||||||
|
show-size-changer
|
||||||
|
@change="getList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ContentWrap>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Rule } from 'ant-design-vue/es/form';
|
||||||
|
|
||||||
|
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/erp';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DatePicker,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
message,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createDemo03Student,
|
||||||
|
getDemo03Student,
|
||||||
|
updateDemo03Student,
|
||||||
|
} from '#/api/infra/demo/demo03/erp';
|
||||||
|
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
|
const formRef = ref();
|
||||||
|
const formData = ref<Partial<Demo03StudentApi.Demo03Student>>({
|
||||||
|
id: undefined,
|
||||||
|
name: undefined,
|
||||||
|
sex: undefined,
|
||||||
|
birthday: undefined,
|
||||||
|
description: undefined,
|
||||||
|
});
|
||||||
|
const rules: Record<string, Rule[]> = {
|
||||||
|
name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
|
||||||
|
sex: [{ required: true, message: '性别不能为空', trigger: 'blur' }],
|
||||||
|
birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
|
||||||
|
description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
|
||||||
|
};
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', ['学生'])
|
||||||
|
: $t('ui.actionTitle.create', ['学生']);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 重置表单 */
|
||||||
|
const resetForm = () => {
|
||||||
|
formData.value = {
|
||||||
|
id: undefined,
|
||||||
|
name: undefined,
|
||||||
|
sex: undefined,
|
||||||
|
birthday: undefined,
|
||||||
|
description: undefined,
|
||||||
|
};
|
||||||
|
formRef.value?.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
await formRef.value?.validate();
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data = formData.value as Demo03StudentApi.Demo03Student;
|
||||||
|
try {
|
||||||
|
await (formData.value?.id
|
||||||
|
? updateDemo03Student(data)
|
||||||
|
: createDemo03Student(data));
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success({
|
||||||
|
content: $t('ui.actionMessage.operationSuccess'),
|
||||||
|
key: 'action_process_msg',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
resetForm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
let data = modalApi.getData<Demo03StudentApi.Demo03Student>();
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.id) {
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
data = await getDemo03Student(data.id);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
formData.value = data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle">
|
||||||
|
<Form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="rules"
|
||||||
|
:label-col="{ span: 5 }"
|
||||||
|
:wrapper-col="{ span: 18 }"
|
||||||
|
>
|
||||||
|
<Form.Item label="名字" name="name">
|
||||||
|
<Input v-model:value="formData.name" placeholder="请输入名字" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="性别" name="sex">
|
||||||
|
<RadioGroup v-model:value="formData.sex">
|
||||||
|
<Radio
|
||||||
|
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
|
||||||
|
:key="dict.value"
|
||||||
|
:value="dict.value"
|
||||||
|
>
|
||||||
|
{{ dict.label }}
|
||||||
|
</Radio>
|
||||||
|
</RadioGroup>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="出生日期" name="birthday">
|
||||||
|
<DatePicker
|
||||||
|
v-model:value="formData.birthday"
|
||||||
|
value-format="x"
|
||||||
|
placeholder="选择出生日期"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="简介" name="description">
|
||||||
|
<RichTextarea v-model="formData.description" height="500px" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,311 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableInstance } from 'vxe-table';
|
||||||
|
|
||||||
|
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/normal';
|
||||||
|
|
||||||
|
import { h, nextTick, onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { Download, Plus } from '@vben/icons';
|
||||||
|
import { cloneDeep, formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DatePicker,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
message,
|
||||||
|
Pagination,
|
||||||
|
RangePicker,
|
||||||
|
Select,
|
||||||
|
Tabs,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
import { VxeColumn, VxeTable } from 'vxe-table';
|
||||||
|
|
||||||
|
import {
|
||||||
|
deleteDemo03Student,
|
||||||
|
exportDemo03Student,
|
||||||
|
getDemo03StudentPage,
|
||||||
|
} from '#/api/infra/demo/demo03/normal';
|
||||||
|
import { ContentWrap } from '#/components/content-wrap';
|
||||||
|
import { DictTag } from '#/components/dict-tag';
|
||||||
|
import { TableToolbar } from '#/components/table-toolbar';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils/date';
|
||||||
|
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
|
||||||
|
import { downloadByData } from '#/utils/download';
|
||||||
|
|
||||||
|
import Demo03CourseList from './modules/demo03-course-list.vue';
|
||||||
|
import Demo03GradeList from './modules/demo03-grade-list.vue';
|
||||||
|
import Demo03StudentForm from './modules/form.vue';
|
||||||
|
|
||||||
|
/** 子表的列表 */
|
||||||
|
const subTabsName = ref('demo03Course');
|
||||||
|
|
||||||
|
const loading = ref(true); // 列表的加载中
|
||||||
|
const list = ref<Demo03StudentApi.Demo03Student[]>([]); // 列表的数据
|
||||||
|
|
||||||
|
const total = ref(0); // 列表的总页数
|
||||||
|
const queryParams = reactive({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
name: undefined,
|
||||||
|
sex: undefined,
|
||||||
|
birthday: undefined,
|
||||||
|
description: undefined,
|
||||||
|
createTime: undefined,
|
||||||
|
});
|
||||||
|
const queryFormRef = ref(); // 搜索的表单
|
||||||
|
const exportLoading = ref(false); // 导出的加载中
|
||||||
|
|
||||||
|
/** 查询列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const params = cloneDeep(queryParams) as any;
|
||||||
|
if (params.birthday && Array.isArray(params.birthday)) {
|
||||||
|
params.birthday = (params.birthday as string[]).join(',');
|
||||||
|
}
|
||||||
|
if (params.createTime && Array.isArray(params.createTime)) {
|
||||||
|
params.createTime = (params.createTime as string[]).join(',');
|
||||||
|
}
|
||||||
|
const data = await getDemo03StudentPage(params);
|
||||||
|
list.value = data.list;
|
||||||
|
total.value = data.total;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.pageNo = 1;
|
||||||
|
getList();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryFormRef.value.resetFields();
|
||||||
|
handleQuery();
|
||||||
|
};
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Demo03StudentForm,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 创建学生 */
|
||||||
|
function onCreate() {
|
||||||
|
formModalApi.setData({}).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑学生 */
|
||||||
|
function onEdit(row: Demo03StudentApi.Demo03Student) {
|
||||||
|
formModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除学生 */
|
||||||
|
async function onDelete(row: Demo03StudentApi.Demo03Student) {
|
||||||
|
const hideLoading = message.loading({
|
||||||
|
content: $t('ui.actionMessage.deleting', [row.id]),
|
||||||
|
duration: 0,
|
||||||
|
key: 'action_process_msg',
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteDemo03Student(row.id as number);
|
||||||
|
message.success({
|
||||||
|
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
|
||||||
|
key: 'action_process_msg',
|
||||||
|
});
|
||||||
|
await getList();
|
||||||
|
} catch {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出表格 */
|
||||||
|
async function onExport() {
|
||||||
|
try {
|
||||||
|
exportLoading.value = true;
|
||||||
|
const data = await exportDemo03Student(queryParams);
|
||||||
|
downloadByData(data, '学生.xls');
|
||||||
|
} finally {
|
||||||
|
exportLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 隐藏搜索栏 */
|
||||||
|
const hiddenSearchBar = ref(false);
|
||||||
|
const tableToolbarRef = ref<InstanceType<typeof TableToolbar>>();
|
||||||
|
const tableRef = ref<VxeTableInstance>();
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(async () => {
|
||||||
|
await getList();
|
||||||
|
await nextTick();
|
||||||
|
// 挂载 toolbar 工具栏
|
||||||
|
const table = tableRef.value;
|
||||||
|
const tableToolbar = tableToolbarRef.value;
|
||||||
|
if (table && tableToolbar) {
|
||||||
|
await table.connect(tableToolbar.getToolbarRef()!);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<FormModal @success="getList" />
|
||||||
|
|
||||||
|
<ContentWrap v-if="!hiddenSearchBar">
|
||||||
|
<!-- 搜索工作栏 -->
|
||||||
|
<Form :model="queryParams" ref="queryFormRef" layout="inline">
|
||||||
|
<Form.Item label="名字" name="name">
|
||||||
|
<Input
|
||||||
|
v-model:value="queryParams.name"
|
||||||
|
placeholder="请输入名字"
|
||||||
|
allow-clear
|
||||||
|
@press-enter="handleQuery"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="性别" name="sex">
|
||||||
|
<Select
|
||||||
|
v-model:value="queryParams.sex"
|
||||||
|
placeholder="请选择性别"
|
||||||
|
allow-clear
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
<Select.Option
|
||||||
|
v-for="dict in getDictOptions(
|
||||||
|
DICT_TYPE.SYSTEM_USER_SEX,
|
||||||
|
'number',
|
||||||
|
)"
|
||||||
|
:key="dict.value"
|
||||||
|
:value="dict.value"
|
||||||
|
>
|
||||||
|
{{ dict.label }}
|
||||||
|
</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="出生日期" name="birthday">
|
||||||
|
<DatePicker
|
||||||
|
v-model:value="queryParams.birthday"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
placeholder="选择出生日期"
|
||||||
|
allow-clear
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="创建时间" name="createTime">
|
||||||
|
<RangePicker
|
||||||
|
v-model:value="queryParams.createTime"
|
||||||
|
v-bind="getRangePickerDefaultProps()"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Button class="ml-2" @click="resetQuery"> 重置 </Button>
|
||||||
|
<Button class="ml-2" @click="handleQuery" type="primary">
|
||||||
|
搜索
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</ContentWrap>
|
||||||
|
|
||||||
|
<!-- 列表 -->
|
||||||
|
<ContentWrap title="学生">
|
||||||
|
<template #extra>
|
||||||
|
<TableToolbar
|
||||||
|
ref="tableToolbarRef"
|
||||||
|
v-model:hidden-search="hiddenSearchBar"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
class="ml-2"
|
||||||
|
:icon="h(Plus)"
|
||||||
|
type="primary"
|
||||||
|
@click="onCreate"
|
||||||
|
v-access:code="['infra:demo03-student:create']"
|
||||||
|
>
|
||||||
|
{{ $t('ui.actionTitle.create', ['学生']) }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
:icon="h(Download)"
|
||||||
|
type="primary"
|
||||||
|
class="ml-2"
|
||||||
|
:loading="exportLoading"
|
||||||
|
@click="onExport"
|
||||||
|
v-access:code="['infra:demo03-student:export']"
|
||||||
|
>
|
||||||
|
{{ $t('ui.actionTitle.export') }}
|
||||||
|
</Button>
|
||||||
|
</TableToolbar>
|
||||||
|
</template>
|
||||||
|
<VxeTable ref="tableRef" :data="list" show-overflow :loading="loading">
|
||||||
|
<!-- 子表的列表 -->
|
||||||
|
<VxeColumn type="expand" width="60">
|
||||||
|
<template #content="{ row }">
|
||||||
|
<!-- 子表的表单 -->
|
||||||
|
<Tabs v-model:active-key="subTabsName" class="mx-8">
|
||||||
|
<Tabs.TabPane key="demo03Course" tab="学生课程" force-render>
|
||||||
|
<Demo03CourseList :student-id="row?.id" />
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane key="demo03Grade" tab="学生班级" force-render>
|
||||||
|
<Demo03GradeList :student-id="row?.id" />
|
||||||
|
</Tabs.TabPane>
|
||||||
|
</Tabs>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="id" title="编号" align="center" />
|
||||||
|
<VxeColumn field="name" title="名字" align="center" />
|
||||||
|
<VxeColumn field="sex" title="性别" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<DictTag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" />
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="birthday" title="出生日期" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDateTime(row.birthday) }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="description" title="简介" align="center" />
|
||||||
|
<VxeColumn field="createTime" title="创建时间" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDateTime(row.createTime) }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="operation" title="操作" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
@click="onEdit(row as any)"
|
||||||
|
v-access:code="['infra:demo03-student:update']"
|
||||||
|
>
|
||||||
|
{{ $t('ui.actionTitle.edit') }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
danger
|
||||||
|
class="ml-2"
|
||||||
|
@click="onDelete(row as any)"
|
||||||
|
v-access:code="['infra:demo03-student:delete']"
|
||||||
|
>
|
||||||
|
{{ $t('ui.actionTitle.delete') }}
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
</VxeTable>
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="mt-2 flex justify-end">
|
||||||
|
<Pagination
|
||||||
|
:total="total"
|
||||||
|
v-model:current="queryParams.pageNo"
|
||||||
|
v-model:page-size="queryParams.pageSize"
|
||||||
|
show-size-changer
|
||||||
|
@change="getList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ContentWrap>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableInstance } from 'vxe-table';
|
||||||
|
|
||||||
|
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/normal';
|
||||||
|
|
||||||
|
import { h, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { Plus } from '@vben/icons';
|
||||||
|
|
||||||
|
import { Button, Input } from 'ant-design-vue';
|
||||||
|
import { VxeColumn, VxeTable } from 'vxe-table';
|
||||||
|
|
||||||
|
import { getDemo03CourseListByStudentId } from '#/api/infra/demo/demo03/normal';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
studentId?: number; // 学生编号(主表的关联字段)
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const list = ref<Demo03StudentApi.Demo03Course[]>([]); // 列表的数据
|
||||||
|
const tableRef = ref<VxeTableInstance>();
|
||||||
|
/** 添加学生课程 */
|
||||||
|
const onAdd = async () => {
|
||||||
|
await tableRef.value?.insertAt({} as Demo03StudentApi.Demo03Course, -1);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 删除学生课程 */
|
||||||
|
const onDelete = async (row: Demo03StudentApi.Demo03Course) => {
|
||||||
|
await tableRef.value?.remove(row);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 提供获取表格数据的方法供父组件调用 */
|
||||||
|
defineExpose({
|
||||||
|
getData: (): Demo03StudentApi.Demo03Course[] => {
|
||||||
|
const data = list.value as Demo03StudentApi.Demo03Course[];
|
||||||
|
const removeRecords =
|
||||||
|
tableRef.value?.getRemoveRecords() as Demo03StudentApi.Demo03Course[];
|
||||||
|
const insertRecords =
|
||||||
|
tableRef.value?.getInsertRecords() as Demo03StudentApi.Demo03Course[];
|
||||||
|
return data
|
||||||
|
.filter((row) => !removeRecords.some((removed) => removed.id === row.id))
|
||||||
|
?.concat(insertRecords.map((row: any) => ({ ...row, id: undefined })));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||||
|
watch(
|
||||||
|
() => props.studentId,
|
||||||
|
async (val) => {
|
||||||
|
if (!val) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
list.value = await getDemo03CourseListByStudentId(props.studentId!);
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VxeTable ref="tableRef" :data="list" show-overflow class="mx-4">
|
||||||
|
<VxeColumn field="name" title="名字" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<Input v-model:value="row.name" />
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="score" title="分数" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<Input v-model:value="row.score" />
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="operation" title="操作" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
danger
|
||||||
|
@click="onDelete(row as any)"
|
||||||
|
v-access:code="['infra:demo03-student:delete']"
|
||||||
|
>
|
||||||
|
{{ $t('ui.actionTitle.delete') }}
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
</VxeTable>
|
||||||
|
<div class="mt-4 flex justify-center">
|
||||||
|
<Button
|
||||||
|
:icon="h(Plus)"
|
||||||
|
type="primary"
|
||||||
|
ghost
|
||||||
|
@click="onAdd"
|
||||||
|
v-access:code="['infra:demo03-student:create']"
|
||||||
|
>
|
||||||
|
{{ $t('ui.actionTitle.create', ['学生课程']) }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/normal';
|
||||||
|
|
||||||
|
import { nextTick, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
import { VxeColumn, VxeTable } from 'vxe-table';
|
||||||
|
|
||||||
|
import { getDemo03CourseListByStudentId } from '#/api/infra/demo/demo03/normal';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
studentId?: number; // 学生编号(主表的关联字段)
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const loading = ref(true); // 列表的加载中
|
||||||
|
const list = ref<Demo03StudentApi.Demo03Course[]>([]); // 列表的数据
|
||||||
|
/** 查询列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
if (!props.studentId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
list.value = await getDemo03CourseListByStudentId(props.studentId!);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||||
|
watch(
|
||||||
|
() => props.studentId,
|
||||||
|
async (val) => {
|
||||||
|
if (!val) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await nextTick();
|
||||||
|
await getList();
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ContentWrap title="学生课程列表">
|
||||||
|
<VxeTable :data="list" show-overflow :loading="loading">
|
||||||
|
<VxeColumn field="id" title="编号" align="center" />
|
||||||
|
<VxeColumn field="studentId" title="学生编号" align="center" />
|
||||||
|
<VxeColumn field="name" title="名字" align="center" />
|
||||||
|
<VxeColumn field="score" title="分数" align="center" />
|
||||||
|
<VxeColumn field="createTime" title="创建时间" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDateTime(row.createTime) }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
</VxeTable>
|
||||||
|
</ContentWrap>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Rule } from 'ant-design-vue/es/form';
|
||||||
|
|
||||||
|
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/normal';
|
||||||
|
|
||||||
|
import { nextTick, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { Form, Input } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getDemo03GradeByStudentId } from '#/api/infra/demo/demo03/normal';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
studentId?: number; // 学生编号(主表的关联字段)
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const formRef = ref();
|
||||||
|
const formData = ref<Partial<Demo03StudentApi.Demo03Grade>>({
|
||||||
|
id: undefined,
|
||||||
|
studentId: undefined,
|
||||||
|
name: undefined,
|
||||||
|
teacher: undefined,
|
||||||
|
});
|
||||||
|
const rules: Record<string, Rule[]> = {
|
||||||
|
studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
|
||||||
|
name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
|
||||||
|
teacher: [{ required: true, message: '班主任不能为空', trigger: 'blur' }],
|
||||||
|
};
|
||||||
|
/** 暴露出表单校验方法和表单值获取方法 */
|
||||||
|
defineExpose({
|
||||||
|
validate: async () => await formRef.value?.validate(),
|
||||||
|
getValues: () => formData.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||||
|
watch(
|
||||||
|
() => props.studentId,
|
||||||
|
async (val) => {
|
||||||
|
if (!val) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await nextTick();
|
||||||
|
formData.value = await getDemo03GradeByStudentId(props.studentId!);
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Form
|
||||||
|
ref="formRef"
|
||||||
|
class="mx-4"
|
||||||
|
:model="formData"
|
||||||
|
:rules="rules"
|
||||||
|
:label-col="{ span: 5 }"
|
||||||
|
:wrapper-col="{ span: 18 }"
|
||||||
|
>
|
||||||
|
<Form.Item label="学生编号" name="studentId">
|
||||||
|
<Input v-model:value="formData.studentId" placeholder="请输入学生编号" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="名字" name="name">
|
||||||
|
<Input v-model:value="formData.name" placeholder="请输入名字" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="班主任" name="teacher">
|
||||||
|
<Input v-model:value="formData.teacher" placeholder="请输入班主任" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/normal';
|
||||||
|
|
||||||
|
import { nextTick, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
import { VxeColumn, VxeTable } from 'vxe-table';
|
||||||
|
|
||||||
|
import { getDemo03GradeByStudentId } from '#/api/infra/demo/demo03/normal';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
studentId?: number; // 学生编号(主表的关联字段)
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const loading = ref(true); // 列表的加载中
|
||||||
|
const list = ref<Demo03StudentApi.Demo03Grade[]>([]); // 列表的数据
|
||||||
|
/** 查询列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
if (!props.studentId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
list.value = [await getDemo03GradeByStudentId(props.studentId!)];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||||
|
watch(
|
||||||
|
() => props.studentId,
|
||||||
|
async (val) => {
|
||||||
|
if (!val) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await nextTick();
|
||||||
|
await getList();
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ContentWrap title="学生班级列表">
|
||||||
|
<VxeTable :data="list" show-overflow :loading="loading">
|
||||||
|
<VxeColumn field="id" title="编号" align="center" />
|
||||||
|
<VxeColumn field="studentId" title="学生编号" align="center" />
|
||||||
|
<VxeColumn field="name" title="名字" align="center" />
|
||||||
|
<VxeColumn field="teacher" title="班主任" align="center" />
|
||||||
|
<VxeColumn field="createTime" title="创建时间" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDateTime(row.createTime) }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
</VxeTable>
|
||||||
|
</ContentWrap>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Rule } from 'ant-design-vue/es/form';
|
||||||
|
|
||||||
|
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/normal';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DatePicker,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
message,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
Tabs,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createDemo03Student,
|
||||||
|
getDemo03Student,
|
||||||
|
updateDemo03Student,
|
||||||
|
} from '#/api/infra/demo/demo03/normal';
|
||||||
|
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
|
||||||
|
|
||||||
|
import Demo03CourseForm from './demo03-course-form.vue';
|
||||||
|
import Demo03GradeForm from './demo03-grade-form.vue';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
|
const formRef = ref();
|
||||||
|
const formData = ref<Partial<Demo03StudentApi.Demo03Student>>({
|
||||||
|
id: undefined,
|
||||||
|
name: undefined,
|
||||||
|
sex: undefined,
|
||||||
|
birthday: undefined,
|
||||||
|
description: undefined,
|
||||||
|
});
|
||||||
|
const rules: Record<string, Rule[]> = {
|
||||||
|
name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
|
||||||
|
sex: [{ required: true, message: '性别不能为空', trigger: 'blur' }],
|
||||||
|
birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
|
||||||
|
description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
|
||||||
|
};
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', ['学生'])
|
||||||
|
: $t('ui.actionTitle.create', ['学生']);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 子表的表单 */
|
||||||
|
const subTabsName = ref('demo03Course');
|
||||||
|
const demo03CourseFormRef = ref<InstanceType<typeof Demo03CourseForm>>();
|
||||||
|
const demo03GradeFormRef = ref<InstanceType<typeof Demo03GradeForm>>();
|
||||||
|
|
||||||
|
/** 重置表单 */
|
||||||
|
const resetForm = () => {
|
||||||
|
formData.value = {
|
||||||
|
id: undefined,
|
||||||
|
name: undefined,
|
||||||
|
sex: undefined,
|
||||||
|
birthday: undefined,
|
||||||
|
description: undefined,
|
||||||
|
};
|
||||||
|
formRef.value?.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
await formRef.value?.validate();
|
||||||
|
// 校验子表单
|
||||||
|
try {
|
||||||
|
await demo03GradeFormRef.value?.validate();
|
||||||
|
} catch {
|
||||||
|
subTabsName.value = 'demo03Grade';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data = formData.value as Demo03StudentApi.Demo03Student;
|
||||||
|
// 拼接子表的数据
|
||||||
|
data.demo03Courses = demo03CourseFormRef.value?.getData();
|
||||||
|
data.demo03Grade = demo03GradeFormRef.value?.getValues();
|
||||||
|
try {
|
||||||
|
await (formData.value?.id
|
||||||
|
? updateDemo03Student(data)
|
||||||
|
: createDemo03Student(data));
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success({
|
||||||
|
content: $t('ui.actionMessage.operationSuccess'),
|
||||||
|
key: 'action_process_msg',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
resetForm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
let data = modalApi.getData<Demo03StudentApi.Demo03Student>();
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.id) {
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
data = await getDemo03Student(data.id);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
formData.value = data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle">
|
||||||
|
<Form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="rules"
|
||||||
|
:label-col="{ span: 5 }"
|
||||||
|
:wrapper-col="{ span: 18 }"
|
||||||
|
>
|
||||||
|
<Form.Item label="名字" name="name">
|
||||||
|
<Input v-model:value="formData.name" placeholder="请输入名字" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="性别" name="sex">
|
||||||
|
<RadioGroup v-model:value="formData.sex">
|
||||||
|
<Radio
|
||||||
|
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
|
||||||
|
:key="dict.value"
|
||||||
|
:value="dict.value"
|
||||||
|
>
|
||||||
|
{{ dict.label }}
|
||||||
|
</Radio>
|
||||||
|
</RadioGroup>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="出生日期" name="birthday">
|
||||||
|
<DatePicker
|
||||||
|
v-model:value="formData.birthday"
|
||||||
|
value-format="x"
|
||||||
|
placeholder="选择出生日期"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="简介" name="description">
|
||||||
|
<RichTextarea v-model="formData.description" height="500px" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
<!-- 子表的表单 -->
|
||||||
|
<Tabs v-model:active-key="subTabsName">
|
||||||
|
<Tabs.TabPane key="demo03Course" tab="学生课程" force-render>
|
||||||
|
<Demo03CourseForm
|
||||||
|
ref="demo03CourseFormRef"
|
||||||
|
:student-id="formData?.id"
|
||||||
|
/>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane key="demo03Grade" tab="学生班级" force-render>
|
||||||
|
<Demo03GradeForm ref="demo03GradeFormRef" :student-id="formData?.id" />
|
||||||
|
</Tabs.TabPane>
|
||||||
|
</Tabs>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,291 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableInstance } from 'vxe-table';
|
||||||
|
|
||||||
|
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/normal';
|
||||||
|
|
||||||
|
import { h, nextTick, onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { Download, Plus } from '@vben/icons';
|
||||||
|
import { cloneDeep, formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DatePicker,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
message,
|
||||||
|
Pagination,
|
||||||
|
RangePicker,
|
||||||
|
Select,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
import { VxeColumn, VxeTable } from 'vxe-table';
|
||||||
|
|
||||||
|
import {
|
||||||
|
deleteDemo03Student,
|
||||||
|
exportDemo03Student,
|
||||||
|
getDemo03StudentPage,
|
||||||
|
} from '#/api/infra/demo/demo03/normal';
|
||||||
|
import { ContentWrap } from '#/components/content-wrap';
|
||||||
|
import { DictTag } from '#/components/dict-tag';
|
||||||
|
import { TableToolbar } from '#/components/table-toolbar';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils/date';
|
||||||
|
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
|
||||||
|
import { downloadByData } from '#/utils/download';
|
||||||
|
|
||||||
|
import Demo03StudentForm from './modules/form.vue';
|
||||||
|
|
||||||
|
const loading = ref(true); // 列表的加载中
|
||||||
|
const list = ref<Demo03StudentApi.Demo03Student[]>([]); // 列表的数据
|
||||||
|
|
||||||
|
const total = ref(0); // 列表的总页数
|
||||||
|
const queryParams = reactive({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
name: undefined,
|
||||||
|
sex: undefined,
|
||||||
|
birthday: undefined,
|
||||||
|
description: undefined,
|
||||||
|
createTime: undefined,
|
||||||
|
});
|
||||||
|
const queryFormRef = ref(); // 搜索的表单
|
||||||
|
const exportLoading = ref(false); // 导出的加载中
|
||||||
|
|
||||||
|
/** 查询列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const params = cloneDeep(queryParams) as any;
|
||||||
|
if (params.birthday && Array.isArray(params.birthday)) {
|
||||||
|
params.birthday = (params.birthday as string[]).join(',');
|
||||||
|
}
|
||||||
|
if (params.createTime && Array.isArray(params.createTime)) {
|
||||||
|
params.createTime = (params.createTime as string[]).join(',');
|
||||||
|
}
|
||||||
|
const data = await getDemo03StudentPage(params);
|
||||||
|
list.value = data.list;
|
||||||
|
total.value = data.total;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.pageNo = 1;
|
||||||
|
getList();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryFormRef.value.resetFields();
|
||||||
|
handleQuery();
|
||||||
|
};
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Demo03StudentForm,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 创建学生 */
|
||||||
|
function onCreate() {
|
||||||
|
formModalApi.setData({}).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑学生 */
|
||||||
|
function onEdit(row: Demo03StudentApi.Demo03Student) {
|
||||||
|
formModalApi.setData(row).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除学生 */
|
||||||
|
async function onDelete(row: Demo03StudentApi.Demo03Student) {
|
||||||
|
const hideLoading = message.loading({
|
||||||
|
content: $t('ui.actionMessage.deleting', [row.id]),
|
||||||
|
duration: 0,
|
||||||
|
key: 'action_process_msg',
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteDemo03Student(row.id as number);
|
||||||
|
message.success({
|
||||||
|
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
|
||||||
|
key: 'action_process_msg',
|
||||||
|
});
|
||||||
|
await getList();
|
||||||
|
} catch {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出表格 */
|
||||||
|
async function onExport() {
|
||||||
|
try {
|
||||||
|
exportLoading.value = true;
|
||||||
|
const data = await exportDemo03Student(queryParams);
|
||||||
|
downloadByData(data, '学生.xls');
|
||||||
|
} finally {
|
||||||
|
exportLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 隐藏搜索栏 */
|
||||||
|
const hiddenSearchBar = ref(false);
|
||||||
|
const tableToolbarRef = ref<InstanceType<typeof TableToolbar>>();
|
||||||
|
const tableRef = ref<VxeTableInstance>();
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(async () => {
|
||||||
|
await getList();
|
||||||
|
await nextTick();
|
||||||
|
// 挂载 toolbar 工具栏
|
||||||
|
const table = tableRef.value;
|
||||||
|
const tableToolbar = tableToolbarRef.value;
|
||||||
|
if (table && tableToolbar) {
|
||||||
|
await table.connect(tableToolbar.getToolbarRef()!);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<FormModal @success="getList" />
|
||||||
|
|
||||||
|
<ContentWrap v-if="!hiddenSearchBar">
|
||||||
|
<!-- 搜索工作栏 -->
|
||||||
|
<Form :model="queryParams" ref="queryFormRef" layout="inline">
|
||||||
|
<Form.Item label="名字" name="name">
|
||||||
|
<Input
|
||||||
|
v-model:value="queryParams.name"
|
||||||
|
placeholder="请输入名字"
|
||||||
|
allow-clear
|
||||||
|
@press-enter="handleQuery"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="性别" name="sex">
|
||||||
|
<Select
|
||||||
|
v-model:value="queryParams.sex"
|
||||||
|
placeholder="请选择性别"
|
||||||
|
allow-clear
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
<Select.Option
|
||||||
|
v-for="dict in getDictOptions(
|
||||||
|
DICT_TYPE.SYSTEM_USER_SEX,
|
||||||
|
'number',
|
||||||
|
)"
|
||||||
|
:key="dict.value"
|
||||||
|
:value="dict.value"
|
||||||
|
>
|
||||||
|
{{ dict.label }}
|
||||||
|
</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="出生日期" name="birthday">
|
||||||
|
<DatePicker
|
||||||
|
v-model:value="queryParams.birthday"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
placeholder="选择出生日期"
|
||||||
|
allow-clear
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="创建时间" name="createTime">
|
||||||
|
<RangePicker
|
||||||
|
v-model:value="queryParams.createTime"
|
||||||
|
v-bind="getRangePickerDefaultProps()"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Button class="ml-2" @click="resetQuery"> 重置 </Button>
|
||||||
|
<Button class="ml-2" @click="handleQuery" type="primary">
|
||||||
|
搜索
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</ContentWrap>
|
||||||
|
|
||||||
|
<!-- 列表 -->
|
||||||
|
<ContentWrap title="学生">
|
||||||
|
<template #extra>
|
||||||
|
<TableToolbar
|
||||||
|
ref="tableToolbarRef"
|
||||||
|
v-model:hidden-search="hiddenSearchBar"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
class="ml-2"
|
||||||
|
:icon="h(Plus)"
|
||||||
|
type="primary"
|
||||||
|
@click="onCreate"
|
||||||
|
v-access:code="['infra:demo03-student:create']"
|
||||||
|
>
|
||||||
|
{{ $t('ui.actionTitle.create', ['学生']) }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
:icon="h(Download)"
|
||||||
|
type="primary"
|
||||||
|
class="ml-2"
|
||||||
|
:loading="exportLoading"
|
||||||
|
@click="onExport"
|
||||||
|
v-access:code="['infra:demo03-student:export']"
|
||||||
|
>
|
||||||
|
{{ $t('ui.actionTitle.export') }}
|
||||||
|
</Button>
|
||||||
|
</TableToolbar>
|
||||||
|
</template>
|
||||||
|
<VxeTable ref="tableRef" :data="list" show-overflow :loading="loading">
|
||||||
|
<VxeColumn field="id" title="编号" align="center" />
|
||||||
|
<VxeColumn field="name" title="名字" align="center" />
|
||||||
|
<VxeColumn field="sex" title="性别" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<DictTag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" />
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="birthday" title="出生日期" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDateTime(row.birthday) }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="description" title="简介" align="center" />
|
||||||
|
<VxeColumn field="createTime" title="创建时间" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDateTime(row.createTime) }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="operation" title="操作" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
@click="onEdit(row as any)"
|
||||||
|
v-access:code="['infra:demo03-student:update']"
|
||||||
|
>
|
||||||
|
{{ $t('ui.actionTitle.edit') }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
danger
|
||||||
|
class="ml-2"
|
||||||
|
@click="onDelete(row as any)"
|
||||||
|
v-access:code="['infra:demo03-student:delete']"
|
||||||
|
>
|
||||||
|
{{ $t('ui.actionTitle.delete') }}
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
</VxeTable>
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="mt-2 flex justify-end">
|
||||||
|
<Pagination
|
||||||
|
:total="total"
|
||||||
|
v-model:current="queryParams.pageNo"
|
||||||
|
v-model:page-size="queryParams.pageSize"
|
||||||
|
show-size-changer
|
||||||
|
@change="getList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ContentWrap>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableInstance } from 'vxe-table';
|
||||||
|
|
||||||
|
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/normal';
|
||||||
|
|
||||||
|
import { h, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { Plus } from '@vben/icons';
|
||||||
|
|
||||||
|
import { Button, Input } from 'ant-design-vue';
|
||||||
|
import { VxeColumn, VxeTable } from 'vxe-table';
|
||||||
|
|
||||||
|
import { getDemo03CourseListByStudentId } from '#/api/infra/demo/demo03/normal';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
studentId?: number; // 学生编号(主表的关联字段)
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const list = ref<Demo03StudentApi.Demo03Course[]>([]); // 列表的数据
|
||||||
|
const tableRef = ref<VxeTableInstance>();
|
||||||
|
/** 添加学生课程 */
|
||||||
|
const onAdd = async () => {
|
||||||
|
await tableRef.value?.insertAt({} as Demo03StudentApi.Demo03Course, -1);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 删除学生课程 */
|
||||||
|
const onDelete = async (row: Demo03StudentApi.Demo03Course) => {
|
||||||
|
await tableRef.value?.remove(row);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 提供获取表格数据的方法供父组件调用 */
|
||||||
|
defineExpose({
|
||||||
|
getData: (): Demo03StudentApi.Demo03Course[] => {
|
||||||
|
const data = list.value as Demo03StudentApi.Demo03Course[];
|
||||||
|
const removeRecords =
|
||||||
|
tableRef.value?.getRemoveRecords() as Demo03StudentApi.Demo03Course[];
|
||||||
|
const insertRecords =
|
||||||
|
tableRef.value?.getInsertRecords() as Demo03StudentApi.Demo03Course[];
|
||||||
|
return data
|
||||||
|
.filter((row) => !removeRecords.some((removed) => removed.id === row.id))
|
||||||
|
?.concat(insertRecords.map((row: any) => ({ ...row, id: undefined })));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||||
|
watch(
|
||||||
|
() => props.studentId,
|
||||||
|
async (val) => {
|
||||||
|
if (!val) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
list.value = await getDemo03CourseListByStudentId(props.studentId!);
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VxeTable ref="tableRef" :data="list" show-overflow class="mx-4">
|
||||||
|
<VxeColumn field="name" title="名字" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<Input v-model:value="row.name" />
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="score" title="分数" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<Input v-model:value="row.score" />
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn field="operation" title="操作" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
danger
|
||||||
|
@click="onDelete(row as any)"
|
||||||
|
v-access:code="['infra:demo03-student:delete']"
|
||||||
|
>
|
||||||
|
{{ $t('ui.actionTitle.delete') }}
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
</VxeTable>
|
||||||
|
<div class="mt-4 flex justify-center">
|
||||||
|
<Button
|
||||||
|
:icon="h(Plus)"
|
||||||
|
type="primary"
|
||||||
|
ghost
|
||||||
|
@click="onAdd"
|
||||||
|
v-access:code="['infra:demo03-student:create']"
|
||||||
|
>
|
||||||
|
{{ $t('ui.actionTitle.create', ['学生课程']) }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Rule } from 'ant-design-vue/es/form';
|
||||||
|
|
||||||
|
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/normal';
|
||||||
|
|
||||||
|
import { nextTick, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { Form, Input } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getDemo03GradeByStudentId } from '#/api/infra/demo/demo03/normal';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
studentId?: number; // 学生编号(主表的关联字段)
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const formRef = ref();
|
||||||
|
const formData = ref<Partial<Demo03StudentApi.Demo03Grade>>({
|
||||||
|
id: undefined,
|
||||||
|
studentId: undefined,
|
||||||
|
name: undefined,
|
||||||
|
teacher: undefined,
|
||||||
|
});
|
||||||
|
const rules: Record<string, Rule[]> = {
|
||||||
|
studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
|
||||||
|
name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
|
||||||
|
teacher: [{ required: true, message: '班主任不能为空', trigger: 'blur' }],
|
||||||
|
};
|
||||||
|
/** 暴露出表单校验方法和表单值获取方法 */
|
||||||
|
defineExpose({
|
||||||
|
validate: async () => await formRef.value?.validate(),
|
||||||
|
getValues: () => formData.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||||
|
watch(
|
||||||
|
() => props.studentId,
|
||||||
|
async (val) => {
|
||||||
|
if (!val) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await nextTick();
|
||||||
|
formData.value = await getDemo03GradeByStudentId(props.studentId!);
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Form
|
||||||
|
ref="formRef"
|
||||||
|
class="mx-4"
|
||||||
|
:model="formData"
|
||||||
|
:rules="rules"
|
||||||
|
:label-col="{ span: 5 }"
|
||||||
|
:wrapper-col="{ span: 18 }"
|
||||||
|
>
|
||||||
|
<Form.Item label="学生编号" name="studentId">
|
||||||
|
<Input v-model:value="formData.studentId" placeholder="请输入学生编号" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="名字" name="name">
|
||||||
|
<Input v-model:value="formData.name" placeholder="请输入名字" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="班主任" name="teacher">
|
||||||
|
<Input v-model:value="formData.teacher" placeholder="请输入班主任" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Rule } from 'ant-design-vue/es/form';
|
||||||
|
|
||||||
|
import type { Demo03StudentApi } from '#/api/infra/demo/demo03/normal';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DatePicker,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
message,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
Tabs,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createDemo03Student,
|
||||||
|
getDemo03Student,
|
||||||
|
updateDemo03Student,
|
||||||
|
} from '#/api/infra/demo/demo03/normal';
|
||||||
|
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
|
||||||
|
|
||||||
|
import Demo03CourseForm from './demo03-course-form.vue';
|
||||||
|
import Demo03GradeForm from './demo03-grade-form.vue';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
|
const formRef = ref();
|
||||||
|
const formData = ref<Partial<Demo03StudentApi.Demo03Student>>({
|
||||||
|
id: undefined,
|
||||||
|
name: undefined,
|
||||||
|
sex: undefined,
|
||||||
|
birthday: undefined,
|
||||||
|
description: undefined,
|
||||||
|
});
|
||||||
|
const rules: Record<string, Rule[]> = {
|
||||||
|
name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
|
||||||
|
sex: [{ required: true, message: '性别不能为空', trigger: 'blur' }],
|
||||||
|
birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
|
||||||
|
description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
|
||||||
|
};
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', ['学生'])
|
||||||
|
: $t('ui.actionTitle.create', ['学生']);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 子表的表单 */
|
||||||
|
const subTabsName = ref('demo03Course');
|
||||||
|
const demo03CourseFormRef = ref<InstanceType<typeof Demo03CourseForm>>();
|
||||||
|
const demo03GradeFormRef = ref<InstanceType<typeof Demo03GradeForm>>();
|
||||||
|
|
||||||
|
/** 重置表单 */
|
||||||
|
const resetForm = () => {
|
||||||
|
formData.value = {
|
||||||
|
id: undefined,
|
||||||
|
name: undefined,
|
||||||
|
sex: undefined,
|
||||||
|
birthday: undefined,
|
||||||
|
description: undefined,
|
||||||
|
};
|
||||||
|
formRef.value?.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
await formRef.value?.validate();
|
||||||
|
// 校验子表单
|
||||||
|
try {
|
||||||
|
await demo03GradeFormRef.value?.validate();
|
||||||
|
} catch {
|
||||||
|
subTabsName.value = 'demo03Grade';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data = formData.value as Demo03StudentApi.Demo03Student;
|
||||||
|
// 拼接子表的数据
|
||||||
|
data.demo03Courses = demo03CourseFormRef.value?.getData();
|
||||||
|
data.demo03Grade = demo03GradeFormRef.value?.getValues();
|
||||||
|
try {
|
||||||
|
await (formData.value?.id
|
||||||
|
? updateDemo03Student(data)
|
||||||
|
: createDemo03Student(data));
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
message.success({
|
||||||
|
content: $t('ui.actionMessage.operationSuccess'),
|
||||||
|
key: 'action_process_msg',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
resetForm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
let data = modalApi.getData<Demo03StudentApi.Demo03Student>();
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.id) {
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
data = await getDemo03Student(data.id);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
formData.value = data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="getTitle">
|
||||||
|
<Form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="rules"
|
||||||
|
:label-col="{ span: 5 }"
|
||||||
|
:wrapper-col="{ span: 18 }"
|
||||||
|
>
|
||||||
|
<Form.Item label="名字" name="name">
|
||||||
|
<Input v-model:value="formData.name" placeholder="请输入名字" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="性别" name="sex">
|
||||||
|
<RadioGroup v-model:value="formData.sex">
|
||||||
|
<Radio
|
||||||
|
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
|
||||||
|
:key="dict.value"
|
||||||
|
:value="dict.value"
|
||||||
|
>
|
||||||
|
{{ dict.label }}
|
||||||
|
</Radio>
|
||||||
|
</RadioGroup>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="出生日期" name="birthday">
|
||||||
|
<DatePicker
|
||||||
|
v-model:value="formData.birthday"
|
||||||
|
value-format="x"
|
||||||
|
placeholder="选择出生日期"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="简介" name="description">
|
||||||
|
<RichTextarea v-model="formData.description" height="500px" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
<!-- 子表的表单 -->
|
||||||
|
<Tabs v-model:active-key="subTabsName">
|
||||||
|
<Tabs.TabPane key="demo03Course" tab="学生课程" force-render>
|
||||||
|
<Demo03CourseForm
|
||||||
|
ref="demo03CourseFormRef"
|
||||||
|
:student-id="formData?.id"
|
||||||
|
/>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane key="demo03Grade" tab="学生班级" force-render>
|
||||||
|
<Demo03GradeForm ref="demo03GradeFormRef" :student-id="formData?.id" />
|
||||||
|
</Tabs.TabPane>
|
||||||
|
</Tabs>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
@ -2,13 +2,18 @@
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
import { useAccessStore } from '@vben/stores';
|
||||||
|
|
||||||
import { DocAlert } from '#/components/doc-alert';
|
import { DocAlert } from '#/components/doc-alert';
|
||||||
import { IFrame } from '#/components/iframe';
|
import { IFrame } from '#/components/iframe';
|
||||||
|
|
||||||
defineOptions({ name: 'GoView' });
|
defineOptions({ name: 'GoView' });
|
||||||
|
|
||||||
const src = ref(import.meta.env.VITE_GOVIEW_URL);
|
const accessStore = useAccessStore();
|
||||||
|
|
||||||
|
const src = ref(
|
||||||
|
`${import.meta.env.VITE_GOVIEW_URL}?accessToken=${accessStore.accessToken}&refreshToken=${accessStore.refreshToken}`,
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { Page } from '@vben/common-ui';
|
||||||
|
import { useAccessStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import { DocAlert } from '#/components/doc-alert';
|
||||||
|
import { IFrame } from '#/components/iframe';
|
||||||
|
|
||||||
|
defineOptions({ name: 'JimuBI' });
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
|
||||||
|
const src = ref(
|
||||||
|
`${import.meta.env.VITE_BASE_URL}/drag/list?token=${
|
||||||
|
accessStore.refreshToken
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<template #doc>
|
||||||
|
<DocAlert title="大屏设计器" url="https://doc.iocoder.cn/screen/" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<IFrame :src="src" />
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
|
@ -38,13 +38,15 @@ export function useTypeFormSchema(): VbenFormSchema[] {
|
||||||
fieldName: 'type',
|
fieldName: 'type',
|
||||||
label: '字典类型',
|
label: '字典类型',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
componentProps: {
|
componentProps: (values) => {
|
||||||
|
return {
|
||||||
placeholder: '请输入字典类型',
|
placeholder: '请输入字典类型',
|
||||||
|
disabled: !!values.id,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
dependencies: {
|
dependencies: {
|
||||||
triggerFields: [''],
|
triggerFields: [''],
|
||||||
disabled: ({ values }) => values.id,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -107,9 +109,8 @@ export function useTypeGridColumns<T = SystemDictTypeApi.DictType>(
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
title: '字典名称',
|
title: '字典名称',
|
||||||
minWidth: 180,
|
minWidth: 200,
|
||||||
},
|
},
|
||||||
// TODO @芋艿:disable的;
|
|
||||||
{
|
{
|
||||||
field: 'type',
|
field: 'type',
|
||||||
title: '字典类型',
|
title: '字典类型',
|
||||||
|
|
@ -118,7 +119,7 @@ export function useTypeGridColumns<T = SystemDictTypeApi.DictType>(
|
||||||
{
|
{
|
||||||
field: 'status',
|
field: 'status',
|
||||||
title: '状态',
|
title: '状态',
|
||||||
minWidth: 180,
|
minWidth: 120,
|
||||||
cellRender: {
|
cellRender: {
|
||||||
name: 'CellDict',
|
name: 'CellDict',
|
||||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||||
|
|
|
||||||
|
|
@ -48,11 +48,10 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||||
},
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
// TODO @芋艿:图片上传
|
|
||||||
{
|
{
|
||||||
fieldName: 'logo',
|
fieldName: 'logo',
|
||||||
label: '应用图标',
|
label: '应用图标',
|
||||||
component: 'UploadImage',
|
component: 'ImageUpload',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
limit: 1,
|
limit: 1,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { Router } from 'vue-router';
|
import type { Router } from 'vue-router';
|
||||||
|
|
||||||
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
|
import { LOGIN_PATH } from '@vben/constants';
|
||||||
import { preferences } from '@vben/preferences';
|
import { preferences } from '@vben/preferences';
|
||||||
import { useAccessStore, useUserStore } from '@vben/stores';
|
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||||
import { startProgress, stopProgress } from '@vben/utils';
|
import { startProgress, stopProgress } from '@vben/utils';
|
||||||
|
|
@ -56,7 +56,7 @@ function setupAccessGuard(router: Router) {
|
||||||
return decodeURIComponent(
|
return decodeURIComponent(
|
||||||
(to.query?.redirect as string) ||
|
(to.query?.redirect as string) ||
|
||||||
userStore.userInfo?.homePath ||
|
userStore.userInfo?.homePath ||
|
||||||
DEFAULT_HOME_PATH,
|
preferences.app.defaultHomePath,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -75,7 +75,7 @@ function setupAccessGuard(router: Router) {
|
||||||
path: LOGIN_PATH,
|
path: LOGIN_PATH,
|
||||||
// 如不需要,直接删除 query
|
// 如不需要,直接删除 query
|
||||||
query:
|
query:
|
||||||
to.fullPath === DEFAULT_HOME_PATH
|
to.fullPath === preferences.app.defaultHomePath
|
||||||
? {}
|
? {}
|
||||||
: { redirect: encodeURIComponent(to.fullPath) },
|
: { redirect: encodeURIComponent(to.fullPath) },
|
||||||
// 携带当前跳转的页面,登录后重新跳转该页面
|
// 携带当前跳转的页面,登录后重新跳转该页面
|
||||||
|
|
@ -108,8 +108,8 @@ function setupAccessGuard(router: Router) {
|
||||||
accessStore.setAccessRoutes(accessibleRoutes);
|
accessStore.setAccessRoutes(accessibleRoutes);
|
||||||
accessStore.setIsAccessChecked(true);
|
accessStore.setIsAccessChecked(true);
|
||||||
const redirectPath = (from.query.redirect ??
|
const redirectPath = (from.query.redirect ??
|
||||||
(to.path === DEFAULT_HOME_PATH
|
(to.path === preferences.app.defaultHomePath
|
||||||
? userInfo.homePath || DEFAULT_HOME_PATH
|
? userInfo.homePath || preferences.app.defaultHomePath
|
||||||
: to.fullPath)) as string;
|
: to.fullPath)) as string;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type { RouteRecordRaw } from 'vue-router';
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
|
import { LOGIN_PATH } from '@vben/constants';
|
||||||
|
import { preferences } from '@vben/preferences';
|
||||||
|
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
|
@ -34,7 +35,7 @@ const coreRoutes: RouteRecordRaw[] = [
|
||||||
},
|
},
|
||||||
name: 'Root',
|
name: 'Root',
|
||||||
path: '/',
|
path: '/',
|
||||||
redirect: DEFAULT_HOME_PATH,
|
redirect: preferences.app.defaultHomePath,
|
||||||
children: [],
|
children: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ import type { Recordable, UserInfo } from '@vben/types';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
|
import { LOGIN_PATH } from '@vben/constants';
|
||||||
|
import { preferences } from '@vben/preferences';
|
||||||
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
|
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
|
||||||
|
|
||||||
import { ElNotification } from 'element-plus';
|
import { ElNotification } from 'element-plus';
|
||||||
|
|
@ -55,7 +56,9 @@ export const useAuthStore = defineStore('auth', () => {
|
||||||
} else {
|
} else {
|
||||||
onSuccess
|
onSuccess
|
||||||
? await onSuccess?.()
|
? await onSuccess?.()
|
||||||
: await router.push(userInfo.homePath || DEFAULT_HOME_PATH);
|
: await router.push(
|
||||||
|
userInfo.homePath || preferences.app.defaultHomePath,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userInfo?.realName) {
|
if (userInfo?.realName) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { Router } from 'vue-router';
|
import type { Router } from 'vue-router';
|
||||||
|
|
||||||
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
|
import { LOGIN_PATH } from '@vben/constants';
|
||||||
import { preferences } from '@vben/preferences';
|
import { preferences } from '@vben/preferences';
|
||||||
import { useAccessStore, useUserStore } from '@vben/stores';
|
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||||
import { startProgress, stopProgress } from '@vben/utils';
|
import { startProgress, stopProgress } from '@vben/utils';
|
||||||
|
|
@ -56,7 +56,7 @@ function setupAccessGuard(router: Router) {
|
||||||
return decodeURIComponent(
|
return decodeURIComponent(
|
||||||
(to.query?.redirect as string) ||
|
(to.query?.redirect as string) ||
|
||||||
userStore.userInfo?.homePath ||
|
userStore.userInfo?.homePath ||
|
||||||
DEFAULT_HOME_PATH,
|
preferences.app.defaultHomePath,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -75,7 +75,7 @@ function setupAccessGuard(router: Router) {
|
||||||
path: LOGIN_PATH,
|
path: LOGIN_PATH,
|
||||||
// 如不需要,直接删除 query
|
// 如不需要,直接删除 query
|
||||||
query:
|
query:
|
||||||
to.fullPath === DEFAULT_HOME_PATH
|
to.fullPath === preferences.app.defaultHomePath
|
||||||
? {}
|
? {}
|
||||||
: { redirect: encodeURIComponent(to.fullPath) },
|
: { redirect: encodeURIComponent(to.fullPath) },
|
||||||
// 携带当前跳转的页面,登录后重新跳转该页面
|
// 携带当前跳转的页面,登录后重新跳转该页面
|
||||||
|
|
@ -107,8 +107,8 @@ function setupAccessGuard(router: Router) {
|
||||||
accessStore.setAccessRoutes(accessibleRoutes);
|
accessStore.setAccessRoutes(accessibleRoutes);
|
||||||
accessStore.setIsAccessChecked(true);
|
accessStore.setIsAccessChecked(true);
|
||||||
const redirectPath = (from.query.redirect ??
|
const redirectPath = (from.query.redirect ??
|
||||||
(to.path === DEFAULT_HOME_PATH
|
(to.path === preferences.app.defaultHomePath
|
||||||
? userInfo.homePath || DEFAULT_HOME_PATH
|
? userInfo.homePath || preferences.app.defaultHomePath
|
||||||
: to.fullPath)) as string;
|
: to.fullPath)) as string;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type { RouteRecordRaw } from 'vue-router';
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
|
import { LOGIN_PATH } from '@vben/constants';
|
||||||
|
import { preferences } from '@vben/preferences';
|
||||||
|
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
|
@ -34,7 +35,7 @@ const coreRoutes: RouteRecordRaw[] = [
|
||||||
},
|
},
|
||||||
name: 'Root',
|
name: 'Root',
|
||||||
path: '/',
|
path: '/',
|
||||||
redirect: DEFAULT_HOME_PATH,
|
redirect: preferences.app.defaultHomePath,
|
||||||
children: [],
|
children: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ import type { Recordable, UserInfo } from '@vben/types';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
|
import { LOGIN_PATH } from '@vben/constants';
|
||||||
|
import { preferences } from '@vben/preferences';
|
||||||
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
|
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
|
||||||
|
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
|
|
@ -55,7 +56,9 @@ export const useAuthStore = defineStore('auth', () => {
|
||||||
} else {
|
} else {
|
||||||
onSuccess
|
onSuccess
|
||||||
? await onSuccess?.()
|
? await onSuccess?.()
|
||||||
: await router.push(userInfo.homePath || DEFAULT_HOME_PATH);
|
: await router.push(
|
||||||
|
userInfo.homePath || preferences.app.defaultHomePath,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userInfo?.realName) {
|
if (userInfo?.realName) {
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
|
| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
|
||||||
| connectedComponent | 连接另一个Modal组件 | `Component` | - |
|
| connectedComponent | 连接另一个Modal组件 | `Component` | - |
|
||||||
| destroyOnClose | 关闭时销毁`connectedComponent` | `boolean` | `false` |
|
| destroyOnClose | 关闭时销毁 | `boolean` | `false` |
|
||||||
| title | 标题 | `string\|slot` | - |
|
| title | 标题 | `string\|slot` | - |
|
||||||
| titleTooltip | 标题提示信息 | `string\|slot` | - |
|
| titleTooltip | 标题提示信息 | `string\|slot` | - |
|
||||||
| description | 描述信息 | `string\|slot` | - |
|
| description | 描述信息 | `string\|slot` | - |
|
||||||
|
|
|
||||||
|
|
@ -98,8 +98,8 @@ The execution command is: `pnpm run [script]` or `npm run [script]`.
|
||||||
"postinstall": "pnpm -r run stub --if-present",
|
"postinstall": "pnpm -r run stub --if-present",
|
||||||
// Only allow using pnpm
|
// Only allow using pnpm
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
// Install husky
|
// Install lefthook
|
||||||
"prepare": "is-ci || husky",
|
"prepare": "is-ci || lefthook install",
|
||||||
// Preview the application
|
// Preview the application
|
||||||
"preview": "turbo-run preview",
|
"preview": "turbo-run preview",
|
||||||
// Package specification check
|
// Package specification check
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,7 @@ const defaultPreferences: Preferences = {
|
||||||
contentCompact: 'wide',
|
contentCompact: 'wide',
|
||||||
defaultAvatar:
|
defaultAvatar:
|
||||||
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
|
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
|
||||||
|
defaultHomePath: '/analytics',
|
||||||
dynamicTitle: true,
|
dynamicTitle: true,
|
||||||
enableCheckUpdates: true,
|
enableCheckUpdates: true,
|
||||||
enablePreferences: true,
|
enablePreferences: true,
|
||||||
|
|
@ -289,6 +290,8 @@ interface AppPreferences {
|
||||||
contentCompact: ContentCompactType;
|
contentCompact: ContentCompactType;
|
||||||
// /** Default application avatar */
|
// /** Default application avatar */
|
||||||
defaultAvatar: string;
|
defaultAvatar: string;
|
||||||
|
/** Default homepage path */
|
||||||
|
defaultHomePath: string;
|
||||||
// /** Enable dynamic title */
|
// /** Enable dynamic title */
|
||||||
dynamicTitle: boolean;
|
dynamicTitle: boolean;
|
||||||
/** Whether to enable update checks */
|
/** Whether to enable update checks */
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ If you encounter a problem, you can start looking from the following aspects:
|
||||||
|
|
||||||
## Dependency Issues
|
## Dependency Issues
|
||||||
|
|
||||||
In a `Monorepo` project, it is necessary to develop the habit of executing `pnpm install` every time you `git pull` the code, as new dependency packages are often added. The project has already configured automatic execution of `pnpm install` in `.husky/git-merge`, but sometimes there might be issues. If it does not execute automatically, it is recommended to execute it manually once.
|
In a `Monorepo` project, it's important to get into the habit of running `pnpm install` after every `git pull` because new dependencies are often added. The project has configured automatic execution of `pnpm install` in `lefthook.yml`, but sometimes there might be issues. If it does not execute automatically, it is recommended to execute it manually once.
|
||||||
|
|
||||||
## About Cache Update Issues
|
## About Cache Update Issues
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,8 @@ The project integrates the following code verification tools:
|
||||||
- [Prettier](https://prettier.io/) for code formatting
|
- [Prettier](https://prettier.io/) for code formatting
|
||||||
- [Commitlint](https://commitlint.js.org/) for checking the standard of git commit messages
|
- [Commitlint](https://commitlint.js.org/) for checking the standard of git commit messages
|
||||||
- [Publint](https://publint.dev/) for checking the standard of npm packages
|
- [Publint](https://publint.dev/) for checking the standard of npm packages
|
||||||
- [Lint Staged](https://github.com/lint-staged/lint-staged) for running code verification before git commits
|
|
||||||
- [Cspell](https://cspell.org/) for checking spelling errors
|
- [Cspell](https://cspell.org/) for checking spelling errors
|
||||||
|
- [lefthook](https://github.com/evilmartians/lefthook) for managing Git hooks, automatically running code checks and formatting before commits
|
||||||
|
|
||||||
## ESLint
|
## ESLint
|
||||||
|
|
||||||
|
|
@ -148,18 +148,66 @@ The cspell configuration file is `cspell.json`, which can be modified according
|
||||||
|
|
||||||
Git hooks are generally combined with various lints to check code style during git commits. If the check fails, the commit will not proceed. Developers need to modify and resubmit.
|
Git hooks are generally combined with various lints to check code style during git commits. If the check fails, the commit will not proceed. Developers need to modify and resubmit.
|
||||||
|
|
||||||
### husky
|
### lefthook
|
||||||
|
|
||||||
One issue is that the check will verify all code, but we only want to check the code we are committing. This is where husky comes in.
|
One issue is that the check will verify all code, but we only want to check the code we are committing. This is where lefthook comes in.
|
||||||
|
|
||||||
The most effective solution is to perform Lint checks locally before committing. A common practice is to use husky or pre-commit to perform a Lint check before local submission.
|
The most effective solution is to perform Lint checks locally before committing. A common practice is to use lefthook to perform a Lint check before local submission.
|
||||||
|
|
||||||
The project defines corresponding hooks inside `.husky`.
|
The project defines corresponding hooks inside `lefthook.yml`:
|
||||||
|
|
||||||
#### How to Disable Husky
|
- `pre-commit`: Runs before commit, used for code formatting and checking
|
||||||
|
|
||||||
If you want to disable Husky, simply delete the .husky directory.
|
- `code-workspace`: Updates VSCode workspace configuration
|
||||||
|
- `lint-md`: Formats Markdown files
|
||||||
|
- `lint-vue`: Formats and checks Vue files
|
||||||
|
- `lint-js`: Formats and checks JavaScript/TypeScript files
|
||||||
|
- `lint-style`: Formats and checks style files
|
||||||
|
- `lint-package`: Formats package.json
|
||||||
|
- `lint-json`: Formats other JSON files
|
||||||
|
|
||||||
### lint-staged
|
- `post-merge`: Runs after merge, used for automatic dependency installation
|
||||||
|
|
||||||
Used for automatically fixing style issues of committed files. Its configuration file is `.lintstagedrc.mjs`, which can be modified according to project needs.
|
- `install`: Runs `pnpm install` to install new dependencies
|
||||||
|
|
||||||
|
- `commit-msg`: Runs during commit, used for checking commit message format
|
||||||
|
- `commitlint`: Uses commitlint to check commit messages
|
||||||
|
|
||||||
|
#### How to Disable lefthook
|
||||||
|
|
||||||
|
If you want to disable lefthook, there are two ways:
|
||||||
|
|
||||||
|
::: code-group
|
||||||
|
|
||||||
|
```bash [Temporary disable]
|
||||||
|
git commit -m 'feat: add home page' --no-verify
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash [Permanent disable]
|
||||||
|
# Simply delete the lefthook.yml file
|
||||||
|
rm lefthook.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
#### How to Modify lefthook Configuration
|
||||||
|
|
||||||
|
If you want to modify lefthook's configuration, you can edit the `lefthook.yml` file. For example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
pre-commit:
|
||||||
|
parallel: true # Execute tasks in parallel
|
||||||
|
jobs:
|
||||||
|
- name: lint-js
|
||||||
|
run: pnpm prettier --cache --ignore-unknown --write {staged_files}
|
||||||
|
glob: '*.{js,jsx,ts,tsx}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Where:
|
||||||
|
|
||||||
|
- `parallel`: Whether to execute tasks in parallel
|
||||||
|
- `jobs`: Defines the list of tasks to execute
|
||||||
|
- `name`: Task name
|
||||||
|
- `run`: Command to execute
|
||||||
|
- `glob`: File pattern to match
|
||||||
|
- `{staged_files}`: Represents the list of staged files
|
||||||
|
|
|
||||||
|
|
@ -98,8 +98,8 @@ npm 脚本是项目常见的配置,用于执行一些常见的任务,比如
|
||||||
"postinstall": "pnpm -r run stub --if-present",
|
"postinstall": "pnpm -r run stub --if-present",
|
||||||
// 只允许使用pnpm
|
// 只允许使用pnpm
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
// husky的安装
|
// lefthook的安装
|
||||||
"prepare": "is-ci || husky",
|
"prepare": "is-ci || lefthook install",
|
||||||
// 预览应用
|
// 预览应用
|
||||||
"preview": "turbo-run preview",
|
"preview": "turbo-run preview",
|
||||||
// 包规范检查
|
// 包规范检查
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,7 @@ const defaultPreferences: Preferences = {
|
||||||
contentCompact: 'wide',
|
contentCompact: 'wide',
|
||||||
defaultAvatar:
|
defaultAvatar:
|
||||||
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
|
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
|
||||||
|
defaultHomePath: '/analytics',
|
||||||
dynamicTitle: true,
|
dynamicTitle: true,
|
||||||
enableCheckUpdates: true,
|
enableCheckUpdates: true,
|
||||||
enablePreferences: true,
|
enablePreferences: true,
|
||||||
|
|
@ -312,6 +313,8 @@ interface AppPreferences {
|
||||||
contentCompact: ContentCompactType;
|
contentCompact: ContentCompactType;
|
||||||
// /** 应用默认头像 */
|
// /** 应用默认头像 */
|
||||||
defaultAvatar: string;
|
defaultAvatar: string;
|
||||||
|
/** 默认首页地址 */
|
||||||
|
defaultHomePath: string;
|
||||||
// /** 开启动态标题 */
|
// /** 开启动态标题 */
|
||||||
dynamicTitle: boolean;
|
dynamicTitle: boolean;
|
||||||
/** 是否开启检查更新 */
|
/** 是否开启检查更新 */
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
## 依赖问题
|
## 依赖问题
|
||||||
|
|
||||||
在 `Monorepo` 项目下,需要养成每次 `git pull`代码都要执行`pnpm install`的习惯,因为经常会有新的依赖包加入,项目在`.husky/git-merge`已经配置了自动执行`pnpm install`,但是有时候会出现问题,如果没有自动执行,建议手动执行一次。
|
在 `Monorepo` 项目下,需要养成每次 `git pull`代码都要执行`pnpm install`的习惯,因为经常会有新的依赖包加入,项目在`lefthook.yml`已经配置了自动执行`pnpm install`,但是有时候会出现问题,如果没有自动执行,建议手动执行一次。
|
||||||
|
|
||||||
## 关于缓存更新问题
|
## 关于缓存更新问题
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,8 @@
|
||||||
- [Prettier](https://prettier.io/) 用于代码格式化
|
- [Prettier](https://prettier.io/) 用于代码格式化
|
||||||
- [Commitlint](https://commitlint.js.org/) 用于检查 git 提交信息的规范
|
- [Commitlint](https://commitlint.js.org/) 用于检查 git 提交信息的规范
|
||||||
- [Publint](https://publint.dev/) 用于检查 npm 包的规范
|
- [Publint](https://publint.dev/) 用于检查 npm 包的规范
|
||||||
- [Lint Staged](https://github.com/lint-staged/lint-staged) 用于在 git 提交前运行代码校验
|
|
||||||
- [Cspell](https://cspell.org/) 用于检查拼写错误
|
- [Cspell](https://cspell.org/) 用于检查拼写错误
|
||||||
|
- [lefthook](https://github.com/evilmartians/lefthook) 用于管理 Git hooks,在提交前自动运行代码校验和格式化
|
||||||
|
|
||||||
## ESLint
|
## ESLint
|
||||||
|
|
||||||
|
|
@ -148,18 +148,66 @@ cspell 配置文件为 `cspell.json`,可以根据项目需求进行修改。
|
||||||
|
|
||||||
git hook 一般结合各种 lint,在 git 提交代码的时候进行代码风格校验,如果校验没通过,则不会进行提交。需要开发者自行修改后再次进行提交
|
git hook 一般结合各种 lint,在 git 提交代码的时候进行代码风格校验,如果校验没通过,则不会进行提交。需要开发者自行修改后再次进行提交
|
||||||
|
|
||||||
### husky
|
### lefthook
|
||||||
|
|
||||||
有一个问题就是校验会校验全部代码,但是我们只想校验我们自己提交的代码,这个时候就可以使用 husky。
|
有一个问题就是校验会校验全部代码,但是我们只想校验我们自己提交的代码,这个时候就可以使用 lefthook。
|
||||||
|
|
||||||
最有效的解决方案就是将 Lint 校验放到本地,常见做法是使用 husky 或者 pre-commit 在本地提交之前先做一次 Lint 校验。
|
最有效的解决方案就是将 Lint 校验放到本地,常见做法是使用 lefthook 在本地提交之前先做一次 Lint 校验。
|
||||||
|
|
||||||
项目在 `.husky` 内部定义了相应的 hooks
|
项目在 `lefthook.yml` 内部定义了相应的 hooks:
|
||||||
|
|
||||||
#### 如何关闭 Husky
|
- `pre-commit`: 在提交前运行,用于代码格式化和检查
|
||||||
|
|
||||||
如果你想关闭 Husky,直接删除 `.husky` 目录即可。
|
- `code-workspace`: 更新 VSCode 工作区配置
|
||||||
|
- `lint-md`: 格式化 Markdown 文件
|
||||||
|
- `lint-vue`: 格式化并检查 Vue 文件
|
||||||
|
- `lint-js`: 格式化并检查 JavaScript/TypeScript 文件
|
||||||
|
- `lint-style`: 格式化并检查样式文件
|
||||||
|
- `lint-package`: 格式化 package.json
|
||||||
|
- `lint-json`: 格式化其他 JSON 文件
|
||||||
|
|
||||||
### lint-staged
|
- `post-merge`: 在合并后运行,用于自动安装依赖
|
||||||
|
|
||||||
用于自动修复提交文件风格问题,其配置文件为 `.lintstagedrc.mjs`,可以根据项目需求进行修改。
|
- `install`: 运行 `pnpm install` 安装新依赖
|
||||||
|
|
||||||
|
- `commit-msg`: 在提交时运行,用于检查提交信息格式
|
||||||
|
- `commitlint`: 使用 commitlint 检查提交信息
|
||||||
|
|
||||||
|
#### 如何关闭 lefthook
|
||||||
|
|
||||||
|
如果你想关闭 lefthook,有两种方式:
|
||||||
|
|
||||||
|
::: code-group
|
||||||
|
|
||||||
|
```bash [临时关闭]
|
||||||
|
git commit -m 'feat: add home page' --no-verify
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash [永久关闭]
|
||||||
|
# 删除 lefthook.yml 文件即可
|
||||||
|
rm lefthook.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
#### 如何修改 lefthook 配置
|
||||||
|
|
||||||
|
如果你想修改 lefthook 的配置,可以编辑 `lefthook.yml` 文件。例如:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
pre-commit:
|
||||||
|
parallel: true # 并行执行任务
|
||||||
|
jobs:
|
||||||
|
- name: lint-js
|
||||||
|
run: pnpm prettier --cache --ignore-unknown --write {staged_files}
|
||||||
|
glob: '*.{js,jsx,ts,tsx}'
|
||||||
|
```
|
||||||
|
|
||||||
|
其中:
|
||||||
|
|
||||||
|
- `parallel`: 是否并行执行任务
|
||||||
|
- `jobs`: 定义要执行的任务列表
|
||||||
|
- `name`: 任务名称
|
||||||
|
- `run`: 要执行的命令
|
||||||
|
- `glob`: 匹配的文件模式
|
||||||
|
- `{staged_files}`: 表示暂存的文件列表
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ export async function perfectionist(): Promise<Linter.Config[]> {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'perfectionist/sort-objects': [
|
'perfectionist/sort-objects': [
|
||||||
'error',
|
'off',
|
||||||
{
|
{
|
||||||
customGroups: {
|
customGroups: {
|
||||||
items: 'items',
|
items: 'items',
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,13 @@ const customConfig: Linter.Config[] = [
|
||||||
'perfectionist/sort-objects': 'off',
|
'perfectionist/sort-objects': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
files: ['**/**.vue'],
|
||||||
|
ignores: restrictedImportIgnores,
|
||||||
|
rules: {
|
||||||
|
'perfectionist/sort-objects': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// apps内部的一些基础规则
|
// apps内部的一些基础规则
|
||||||
files: ['apps/**/**'],
|
files: ['apps/**/**'],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
# EXAMPLE USAGE:
|
||||||
|
#
|
||||||
|
# Refer for explanation to following link:
|
||||||
|
# https://lefthook.dev/configuration/
|
||||||
|
#
|
||||||
|
# pre-push:
|
||||||
|
# jobs:
|
||||||
|
# - name: packages audit
|
||||||
|
# tags:
|
||||||
|
# - frontend
|
||||||
|
# - security
|
||||||
|
# run: yarn audit
|
||||||
|
#
|
||||||
|
# - name: gems audit
|
||||||
|
# tags:
|
||||||
|
# - backend
|
||||||
|
# - security
|
||||||
|
# run: bundle audit
|
||||||
|
#
|
||||||
|
# pre-commit:
|
||||||
|
# parallel: true
|
||||||
|
# jobs:
|
||||||
|
# - run: yarn eslint {staged_files}
|
||||||
|
# glob: "*.{js,ts,jsx,tsx}"
|
||||||
|
#
|
||||||
|
# - name: rubocop
|
||||||
|
# glob: "*.rb"
|
||||||
|
# exclude:
|
||||||
|
# - config/application.rb
|
||||||
|
# - config/routes.rb
|
||||||
|
# run: bundle exec rubocop --force-exclusion {all_files}
|
||||||
|
#
|
||||||
|
# - name: govet
|
||||||
|
# files: git ls-files -m
|
||||||
|
# glob: "*.go"
|
||||||
|
# run: go vet {files}
|
||||||
|
#
|
||||||
|
# - script: "hello.js"
|
||||||
|
# runner: node
|
||||||
|
#
|
||||||
|
# - script: "hello.go"
|
||||||
|
# runner: go run
|
||||||
|
|
||||||
|
pre-commit:
|
||||||
|
parallel: true
|
||||||
|
commands:
|
||||||
|
code-workspace:
|
||||||
|
run: pnpm vsh code-workspace --auto-commit
|
||||||
|
lint-md:
|
||||||
|
run: pnpm prettier --cache --ignore-unknown --write {staged_files}
|
||||||
|
glob: '*.md'
|
||||||
|
lint-vue:
|
||||||
|
run: pnpm prettier --write {staged_files} && pnpm eslint --cache --fix {staged_files} && pnpm stylelint --fix --allow-empty-input {staged_files}
|
||||||
|
glob: '*.vue'
|
||||||
|
lint-js:
|
||||||
|
run: pnpm prettier --cache --ignore-unknown --write {staged_files} && pnpm eslint --cache --fix {staged_files}
|
||||||
|
glob: '*.{js,jsx,ts,tsx}'
|
||||||
|
lint-style:
|
||||||
|
run: pnpm prettier --cache --ignore-unknown --write {staged_files} && pnpm stylelint --fix --allow-empty-input {staged_files}
|
||||||
|
glob: '*.{scss,less,styl,html,vue,css}'
|
||||||
|
lint-package:
|
||||||
|
run: pnpm prettier --cache --write {staged_files}
|
||||||
|
glob: 'package.json'
|
||||||
|
lint-json:
|
||||||
|
run: pnpm prettier --cache --write --parser json {staged_files}
|
||||||
|
glob: '{!(package)*.json,*.code-snippets,.!(browserslist)*rc}'
|
||||||
|
|
||||||
|
post-merge:
|
||||||
|
commands:
|
||||||
|
install:
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
commit-msg:
|
||||||
|
commands:
|
||||||
|
commitlint:
|
||||||
|
run: pnpm exec commitlint --edit $1
|
||||||
|
|
@ -51,14 +51,14 @@
|
||||||
"lint": "vsh lint",
|
"lint": "vsh lint",
|
||||||
"postinstall": "pnpm -r run stub --if-present",
|
"postinstall": "pnpm -r run stub --if-present",
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
"prepare": "is-ci || husky",
|
|
||||||
"preview": "turbo-run preview",
|
"preview": "turbo-run preview",
|
||||||
"publint": "vsh publint",
|
"publint": "vsh publint",
|
||||||
"reinstall": "pnpm clean --del-lock && pnpm install",
|
"reinstall": "pnpm clean --del-lock && pnpm install",
|
||||||
"test:unit": "vitest run --dom",
|
"test:unit": "vitest run --dom",
|
||||||
"test:e2e": "turbo run test:e2e",
|
"test:e2e": "turbo run test:e2e",
|
||||||
"update:deps": "npx taze -r -w",
|
"update:deps": "npx taze -r -w",
|
||||||
"version": "pnpm exec changeset version && pnpm install --no-frozen-lockfile"
|
"version": "pnpm exec changeset version && pnpm install --no-frozen-lockfile",
|
||||||
|
"catalog": "pnpx codemod pnpm/catalog"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@changesets/changelog-github": "catalog:",
|
"@changesets/changelog-github": "catalog:",
|
||||||
|
|
@ -81,9 +81,8 @@
|
||||||
"cross-env": "catalog:",
|
"cross-env": "catalog:",
|
||||||
"cspell": "catalog:",
|
"cspell": "catalog:",
|
||||||
"happy-dom": "catalog:",
|
"happy-dom": "catalog:",
|
||||||
"husky": "catalog:",
|
|
||||||
"is-ci": "catalog:",
|
"is-ci": "catalog:",
|
||||||
"lint-staged": "catalog:",
|
"lefthook": "catalog:",
|
||||||
"playwright": "catalog:",
|
"playwright": "catalog:",
|
||||||
"rimraf": "catalog:",
|
"rimraf": "catalog:",
|
||||||
"tailwindcss": "catalog:",
|
"tailwindcss": "catalog:",
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ export {
|
||||||
Search,
|
Search,
|
||||||
SearchX,
|
SearchX,
|
||||||
Settings,
|
Settings,
|
||||||
|
ShieldQuestion,
|
||||||
Shrink,
|
Shrink,
|
||||||
Square,
|
Square,
|
||||||
SquareCheckBig,
|
SquareCheckBig,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
|
||||||
"compact": false,
|
"compact": false,
|
||||||
"contentCompact": "wide",
|
"contentCompact": "wide",
|
||||||
"defaultAvatar": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp",
|
"defaultAvatar": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp",
|
||||||
|
"defaultHomePath": "/analytics",
|
||||||
"dynamicTitle": true,
|
"dynamicTitle": true,
|
||||||
"enableCheckUpdates": true,
|
"enableCheckUpdates": true,
|
||||||
"enablePreferences": true,
|
"enablePreferences": true,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ const defaultPreferences: Preferences = {
|
||||||
contentCompact: 'wide',
|
contentCompact: 'wide',
|
||||||
defaultAvatar:
|
defaultAvatar:
|
||||||
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
|
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
|
||||||
|
defaultHomePath: '/analytics',
|
||||||
dynamicTitle: true,
|
dynamicTitle: true,
|
||||||
enableCheckUpdates: true,
|
enableCheckUpdates: true,
|
||||||
enablePreferences: true,
|
enablePreferences: true,
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@ interface AppPreferences {
|
||||||
contentCompact: ContentCompactType;
|
contentCompact: ContentCompactType;
|
||||||
// /** 应用默认头像 */
|
// /** 应用默认头像 */
|
||||||
defaultAvatar: string;
|
defaultAvatar: string;
|
||||||
|
/** 默认首页地址 */
|
||||||
|
defaultHomePath: string;
|
||||||
// /** 开启动态标题 */
|
// /** 开启动态标题 */
|
||||||
dynamicTitle: boolean;
|
dynamicTitle: boolean;
|
||||||
/** 是否开启检查更新 */
|
/** 是否开启检查更新 */
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vben-core/composables": "workspace:*",
|
"@vben-core/composables": "workspace:*",
|
||||||
|
"@vben-core/icons": "workspace:*",
|
||||||
"@vben-core/shadcn-ui": "workspace:*",
|
"@vben-core/shadcn-ui": "workspace:*",
|
||||||
"@vben-core/shared": "workspace:*",
|
"@vben-core/shared": "workspace:*",
|
||||||
"@vben-core/typings": "workspace:*",
|
"@vben-core/typings": "workspace:*",
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import type { FormSchema, MaybeComponentProps } from '../types';
|
||||||
|
|
||||||
import { computed, nextTick, onUnmounted, useTemplateRef, watch } from 'vue';
|
import { computed, nextTick, onUnmounted, useTemplateRef, watch } from 'vue';
|
||||||
|
|
||||||
|
import { CircleAlert } from '@vben-core/icons';
|
||||||
import {
|
import {
|
||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
FormDescription,
|
||||||
|
|
@ -12,6 +13,7 @@ import {
|
||||||
FormItem,
|
FormItem,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
VbenRenderContent,
|
VbenRenderContent,
|
||||||
|
VbenTooltip,
|
||||||
} from '@vben-core/shadcn-ui';
|
} from '@vben-core/shadcn-ui';
|
||||||
import { cn, isFunction, isObject, isString } from '@vben-core/shared/utils';
|
import { cn, isFunction, isObject, isString } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
|
|
@ -356,6 +358,24 @@ onUnmounted(() => {
|
||||||
</template>
|
</template>
|
||||||
<!-- <slot></slot> -->
|
<!-- <slot></slot> -->
|
||||||
</component>
|
</component>
|
||||||
|
<VbenTooltip
|
||||||
|
v-if="compact && isInValid"
|
||||||
|
:delay-duration="300"
|
||||||
|
side="left"
|
||||||
|
>
|
||||||
|
<template #trigger>
|
||||||
|
<slot name="trigger">
|
||||||
|
<CircleAlert
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'text-foreground/80 hover:text-foreground inline-flex size-5 cursor-pointer',
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
</template>
|
||||||
|
<FormMessage />
|
||||||
|
</VbenTooltip>
|
||||||
</slot>
|
</slot>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<!-- 自定义后缀 -->
|
<!-- 自定义后缀 -->
|
||||||
|
|
@ -367,7 +387,7 @@ onUnmounted(() => {
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Transition name="slide-up">
|
<Transition name="slide-up" v-if="!compact">
|
||||||
<FormMessage class="absolute bottom-1" />
|
<FormMessage class="absolute bottom-1" />
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,8 @@ export function useVbenForm<
|
||||||
h(VbenUseForm, { ...props, ...attrs, formApi: extendedApi }, slots);
|
h(VbenUseForm, { ...props, ...attrs, formApi: extendedApi }, slots);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
inheritAttrs: false,
|
|
||||||
name: 'VbenUseForm',
|
name: 'VbenUseForm',
|
||||||
|
inheritAttrs: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
// Add reactivity support
|
// Add reactivity support
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import {
|
||||||
createSubMenuContext,
|
createSubMenuContext,
|
||||||
useMenuStyle,
|
useMenuStyle,
|
||||||
} from '../hooks';
|
} from '../hooks';
|
||||||
|
import { useMenuScroll } from '../hooks/use-menu-scroll';
|
||||||
import { flattedChildren } from '../utils';
|
import { flattedChildren } from '../utils';
|
||||||
import SubMenu from './sub-menu.vue';
|
import SubMenu from './sub-menu.vue';
|
||||||
|
|
||||||
|
|
@ -44,6 +45,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
mode: 'vertical',
|
mode: 'vertical',
|
||||||
rounded: true,
|
rounded: true,
|
||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
|
scrollToActive: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|
@ -206,15 +208,19 @@ function handleResize() {
|
||||||
isFirstTimeRender = false;
|
isFirstTimeRender = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getActivePaths() {
|
const enableScroll = computed(
|
||||||
const activeItem = activePath.value && items.value[activePath.value];
|
() => props.scrollToActive && props.mode === 'vertical' && !props.collapse,
|
||||||
|
);
|
||||||
|
|
||||||
if (!activeItem || props.mode === 'horizontal' || props.collapse) {
|
const { scrollToActiveItem } = useMenuScroll(activePath, {
|
||||||
return [];
|
enable: enableScroll,
|
||||||
}
|
delay: 320,
|
||||||
|
});
|
||||||
|
|
||||||
return activeItem.parentPaths;
|
// 监听 activePath 变化,自动滚动到激活项
|
||||||
}
|
watch(activePath, () => {
|
||||||
|
scrollToActiveItem();
|
||||||
|
});
|
||||||
|
|
||||||
// 默认展开菜单
|
// 默认展开菜单
|
||||||
function initMenu() {
|
function initMenu() {
|
||||||
|
|
@ -318,6 +324,16 @@ function removeSubMenu(subMenu: MenuItemRegistered) {
|
||||||
function removeMenuItem(item: MenuItemRegistered) {
|
function removeMenuItem(item: MenuItemRegistered) {
|
||||||
Reflect.deleteProperty(items.value, item.path);
|
Reflect.deleteProperty(items.value, item.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getActivePaths() {
|
||||||
|
const activeItem = activePath.value && items.value[activePath.value];
|
||||||
|
|
||||||
|
if (!activeItem || props.mode === 'horizontal' || props.collapse) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return activeItem.parentPaths;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<ul
|
<ul
|
||||||
|
|
@ -374,10 +390,10 @@ $namespace: vben;
|
||||||
var(--menu-item-margin-x);
|
var(--menu-item-margin-x);
|
||||||
font-size: var(--menu-font-size);
|
font-size: var(--menu-font-size);
|
||||||
color: var(--menu-item-color);
|
color: var(--menu-item-color);
|
||||||
text-decoration: none;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
list-style: none;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
background: var(--menu-item-background-color);
|
background: var(--menu-item-background-color);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--menu-item-radius);
|
border-radius: var(--menu-item-radius);
|
||||||
|
|
@ -701,8 +717,8 @@ $namespace: vben;
|
||||||
width: var(--menu-item-icon-size);
|
width: var(--menu-item-icon-size);
|
||||||
height: var(--menu-item-icon-size);
|
height: var(--menu-item-icon-size);
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
import { watch } from 'vue';
|
||||||
|
|
||||||
|
import { useDebounceFn } from '@vueuse/core';
|
||||||
|
|
||||||
|
interface UseMenuScrollOptions {
|
||||||
|
delay?: number;
|
||||||
|
enable?: boolean | Ref<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMenuScroll(
|
||||||
|
activePath: Ref<string | undefined>,
|
||||||
|
options: UseMenuScrollOptions = {},
|
||||||
|
) {
|
||||||
|
const { enable = true, delay = 320 } = options;
|
||||||
|
|
||||||
|
function scrollToActiveItem() {
|
||||||
|
const isEnabled = typeof enable === 'boolean' ? enable : enable.value;
|
||||||
|
if (!isEnabled) return;
|
||||||
|
|
||||||
|
const activeElement = document.querySelector(
|
||||||
|
`aside li[role=menuitem].is-active`,
|
||||||
|
);
|
||||||
|
if (activeElement) {
|
||||||
|
activeElement.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'center',
|
||||||
|
inline: 'center',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const debouncedScroll = useDebounceFn(scrollToActiveItem, delay);
|
||||||
|
|
||||||
|
watch(activePath, () => {
|
||||||
|
const isEnabled = typeof enable === 'boolean' ? enable : enable.value;
|
||||||
|
if (!isEnabled) return;
|
||||||
|
|
||||||
|
debouncedScroll();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
scrollToActiveItem,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -18,15 +18,9 @@ defineOptions({
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
collapse: false,
|
collapse: false,
|
||||||
// theme: 'dark',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const forward = useForwardProps(props);
|
const forward = useForwardProps(props);
|
||||||
|
|
||||||
// const emit = defineEmits<{
|
|
||||||
// 'update:openKeys': [key: Key[]];
|
|
||||||
// 'update:selectedKeys': [key: Key[]];
|
|
||||||
// }>();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,12 @@ interface MenuProps {
|
||||||
*/
|
*/
|
||||||
rounded?: boolean;
|
rounded?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh_CN 是否自动滚动到激活的菜单项
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
scrollToActive?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh_CN 菜单主题
|
* @zh_CN 菜单主题
|
||||||
* @default dark
|
* @default dark
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ interface Props extends DrawerProps {
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
appendToMain: false,
|
appendToMain: false,
|
||||||
closeIconPlacement: 'right',
|
closeIconPlacement: 'right',
|
||||||
destroyOnClose: true,
|
destroyOnClose: false,
|
||||||
drawerApi: undefined,
|
drawerApi: undefined,
|
||||||
submitting: false,
|
submitting: false,
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,7 @@ import VbenDrawer from './drawer.vue';
|
||||||
|
|
||||||
const USER_DRAWER_INJECT_KEY = Symbol('VBEN_DRAWER_INJECT');
|
const USER_DRAWER_INJECT_KEY = Symbol('VBEN_DRAWER_INJECT');
|
||||||
|
|
||||||
const DEFAULT_DRAWER_PROPS: Partial<DrawerProps> = {
|
const DEFAULT_DRAWER_PROPS: Partial<DrawerProps> = {};
|
||||||
destroyOnClose: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function setDefaultDrawerProps(props: Partial<DrawerProps>) {
|
export function setDefaultDrawerProps(props: Partial<DrawerProps>) {
|
||||||
Object.assign(DEFAULT_DRAWER_PROPS, props);
|
Object.assign(DEFAULT_DRAWER_PROPS, props);
|
||||||
|
|
@ -66,9 +64,10 @@ export function useVbenDrawer<
|
||||||
slots,
|
slots,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line vue/one-component-per-file
|
||||||
{
|
{
|
||||||
inheritAttrs: false,
|
|
||||||
name: 'VbenParentDrawer',
|
name: 'VbenParentDrawer',
|
||||||
|
inheritAttrs: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return [Drawer, extendedApi as ExtendedDrawerApi] as const;
|
return [Drawer, extendedApi as ExtendedDrawerApi] as const;
|
||||||
|
|
@ -107,9 +106,10 @@ export function useVbenDrawer<
|
||||||
return () =>
|
return () =>
|
||||||
h(VbenDrawer, { ...props, ...attrs, drawerApi: extendedApi }, slots);
|
h(VbenDrawer, { ...props, ...attrs, drawerApi: extendedApi }, slots);
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line vue/one-component-per-file
|
||||||
{
|
{
|
||||||
inheritAttrs: false,
|
|
||||||
name: 'VbenDrawer',
|
name: 'VbenDrawer',
|
||||||
|
inheritAttrs: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
injectData.extendApi?.(extendedApi);
|
injectData.extendApi?.(extendedApi);
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ interface Props extends ModalProps {
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
appendToMain: false,
|
appendToMain: false,
|
||||||
destroyOnClose: true,
|
destroyOnClose: false,
|
||||||
modalApi: undefined,
|
modalApi: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,7 @@ import VbenModal from './modal.vue';
|
||||||
|
|
||||||
const USER_MODAL_INJECT_KEY = Symbol('VBEN_MODAL_INJECT');
|
const USER_MODAL_INJECT_KEY = Symbol('VBEN_MODAL_INJECT');
|
||||||
|
|
||||||
const DEFAULT_MODAL_PROPS: Partial<ModalProps> = {
|
const DEFAULT_MODAL_PROPS: Partial<ModalProps> = {};
|
||||||
destroyOnClose: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function setDefaultModalProps(props: Partial<ModalProps>) {
|
export function setDefaultModalProps(props: Partial<ModalProps>) {
|
||||||
Object.assign(DEFAULT_MODAL_PROPS, props);
|
Object.assign(DEFAULT_MODAL_PROPS, props);
|
||||||
|
|
@ -51,9 +49,10 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
||||||
slots,
|
slots,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line vue/one-component-per-file
|
||||||
{
|
{
|
||||||
inheritAttrs: false,
|
|
||||||
name: 'VbenParentModal',
|
name: 'VbenParentModal',
|
||||||
|
inheritAttrs: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return [Modal, extendedApi as ExtendedModalApi] as const;
|
return [Modal, extendedApi as ExtendedModalApi] as const;
|
||||||
|
|
@ -100,9 +99,10 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
||||||
slots,
|
slots,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line vue/one-component-per-file
|
||||||
{
|
{
|
||||||
inheritAttrs: false,
|
|
||||||
name: 'VbenModal',
|
name: 'VbenModal',
|
||||||
|
inheritAttrs: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
injectData.extendApi?.(extendedApi);
|
injectData.extendApi?.(extendedApi);
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ interface Props extends PopoverRootProps {
|
||||||
class?: ClassType;
|
class?: ClassType;
|
||||||
contentClass?: ClassType;
|
contentClass?: ClassType;
|
||||||
contentProps?: PopoverContentProps;
|
contentProps?: PopoverContentProps;
|
||||||
|
triggerClass?: ClassType;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {});
|
const props = withDefaults(defineProps<Props>(), {});
|
||||||
|
|
@ -32,6 +33,7 @@ const delegatedProps = computed(() => {
|
||||||
class: _cls,
|
class: _cls,
|
||||||
contentClass: _,
|
contentClass: _,
|
||||||
contentProps: _cProps,
|
contentProps: _cProps,
|
||||||
|
triggerClass: _tClass,
|
||||||
...delegated
|
...delegated
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
|
@ -43,7 +45,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PopoverRoot v-bind="forwarded">
|
<PopoverRoot v-bind="forwarded">
|
||||||
<PopoverTrigger>
|
<PopoverTrigger :class="triggerClass">
|
||||||
<slot name="trigger"></slot>
|
<slot name="trigger"></slot>
|
||||||
|
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ interface Props extends TabsProps {}
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'VbenTabsChrome',
|
name: 'VbenTabsChrome',
|
||||||
// eslint-disable-next-line perfectionist/sort-objects
|
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ interface Props extends TabsProps {}
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'VbenTabs',
|
name: 'VbenTabs',
|
||||||
// eslint-disable-next-line perfectionist/sort-objects
|
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
});
|
});
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
|
||||||
|
|
@ -15,5 +15,5 @@ pnpm add @vben/constants
|
||||||
### 使用
|
### 使用
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { DEFAULT_HOME_PATH } from '@vben/constants';
|
import { LOGIN_PATH } from '@vben/constants';
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,6 @@
|
||||||
*/
|
*/
|
||||||
export const LOGIN_PATH = '/auth/login';
|
export const LOGIN_PATH = '/auth/login';
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh_CN 默认首页地址
|
|
||||||
*/
|
|
||||||
export const DEFAULT_HOME_PATH = '/workspace';
|
|
||||||
|
|
||||||
export interface LanguageOption {
|
export interface LanguageOption {
|
||||||
label: string;
|
label: string;
|
||||||
value: 'en-US' | 'zh-CN';
|
value: 'en-US' | 'zh-CN';
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ async function generateAccessible(
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成菜单
|
// 生成菜单
|
||||||
const accessibleMenus = await generateMenus(accessibleRoutes, options.router);
|
const accessibleMenus = generateMenus(accessibleRoutes, options.router);
|
||||||
|
|
||||||
return { accessibleMenus, accessibleRoutes };
|
return { accessibleMenus, accessibleRoutes };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { VNode } from 'vue';
|
import type { VNode } from 'vue';
|
||||||
|
|
||||||
import { computed, ref, watch, watchEffect } from 'vue';
|
import { computed, ref, useAttrs, watch, watchEffect } from 'vue';
|
||||||
|
|
||||||
import { usePagination } from '@vben/hooks';
|
import { usePagination } from '@vben/hooks';
|
||||||
import { EmptyIcon, Grip, listIcons } from '@vben/icons';
|
import { EmptyIcon, Grip, listIcons } from '@vben/icons';
|
||||||
|
|
@ -22,8 +22,9 @@ import {
|
||||||
VbenIconButton,
|
VbenIconButton,
|
||||||
VbenPopover,
|
VbenPopover,
|
||||||
} from '@vben-core/shadcn-ui';
|
} from '@vben-core/shadcn-ui';
|
||||||
|
import { isFunction } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
import { refDebounced, watchDebounced } from '@vueuse/core';
|
import { objectOmit, refDebounced, watchDebounced } from '@vueuse/core';
|
||||||
|
|
||||||
import { fetchIconsData } from './icons';
|
import { fetchIconsData } from './icons';
|
||||||
|
|
||||||
|
|
@ -64,6 +65,8 @@ const emit = defineEmits<{
|
||||||
change: [string];
|
change: [string];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const attrs = useAttrs();
|
||||||
|
|
||||||
const modelValue = defineModel({ default: '', type: String });
|
const modelValue = defineModel({ default: '', type: String });
|
||||||
|
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
|
|
@ -165,13 +168,25 @@ const searchInputProps = computed(() => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function updateCurrentSelect(v: string) {
|
||||||
|
currentSelect.value = v;
|
||||||
|
const eventKey = `onUpdate:${props.modelValueProp}`;
|
||||||
|
if (attrs[eventKey] && isFunction(attrs[eventKey])) {
|
||||||
|
attrs[eventKey](v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const getBindAttrs = computed(() => {
|
||||||
|
return objectOmit(attrs, [`onUpdate:${props.modelValueProp}`]);
|
||||||
|
});
|
||||||
|
|
||||||
defineExpose({ toggleOpenState, open, close });
|
defineExpose({ toggleOpenState, open, close });
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VbenPopover
|
<VbenPopover
|
||||||
v-model:open="visible"
|
v-model:open="visible"
|
||||||
:content-props="{ align: 'end', alignOffset: -11, sideOffset: 8 }"
|
:content-props="{ align: 'end', alignOffset: -11, sideOffset: 8 }"
|
||||||
content-class="p-0 pt-3"
|
content-class="p-0 pt-3 w-full"
|
||||||
|
trigger-class="w-full"
|
||||||
>
|
>
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<template v-if="props.type === 'input'">
|
<template v-if="props.type === 'input'">
|
||||||
|
|
@ -183,7 +198,8 @@ defineExpose({ toggleOpenState, open, close });
|
||||||
role="combobox"
|
role="combobox"
|
||||||
:aria-label="$t('ui.iconPicker.placeholder')"
|
:aria-label="$t('ui.iconPicker.placeholder')"
|
||||||
aria-expanded="visible"
|
aria-expanded="visible"
|
||||||
v-bind="$attrs"
|
:[`onUpdate:${modelValueProp}`]="updateCurrentSelect"
|
||||||
|
v-bind="getBindAttrs"
|
||||||
>
|
>
|
||||||
<template #[iconSlot]>
|
<template #[iconSlot]>
|
||||||
<VbenIcon
|
<VbenIcon
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,16 @@ const getZIndex = computed(() => {
|
||||||
return props.zIndex || calcZIndex();
|
return props.zIndex || calcZIndex();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排除ant-message和loading:9999的z-index
|
||||||
|
*/
|
||||||
|
const zIndexExcludeClass = ['ant-message', 'loading'];
|
||||||
|
function isZIndexExcludeClass(element: Element) {
|
||||||
|
return zIndexExcludeClass.some((className) =>
|
||||||
|
element.classList.contains(className),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取最大的zIndex值
|
* 获取最大的zIndex值
|
||||||
*/
|
*/
|
||||||
|
|
@ -44,7 +54,11 @@ function calcZIndex() {
|
||||||
[...elements].forEach((element) => {
|
[...elements].forEach((element) => {
|
||||||
const style = window.getComputedStyle(element);
|
const style = window.getComputedStyle(element);
|
||||||
const zIndex = style.getPropertyValue('z-index');
|
const zIndex = style.getPropertyValue('z-index');
|
||||||
if (zIndex && !Number.isNaN(Number.parseInt(zIndex))) {
|
if (
|
||||||
|
zIndex &&
|
||||||
|
!Number.isNaN(Number.parseInt(zIndex)) &&
|
||||||
|
!isZIndexExcludeClass(element)
|
||||||
|
) {
|
||||||
maxZ = Math.max(maxZ, Number.parseInt(zIndex));
|
maxZ = Math.max(maxZ, Number.parseInt(zIndex));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,25 @@ export function useContentMaximize() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleMaximizeAndTabbarHidden() {
|
||||||
|
const isMaximize = contentIsMaximize.value;
|
||||||
|
updatePreferences({
|
||||||
|
header: {
|
||||||
|
hidden: !isMaximize,
|
||||||
|
},
|
||||||
|
sidebar: {
|
||||||
|
hidden: !isMaximize,
|
||||||
|
},
|
||||||
|
tabbar: {
|
||||||
|
enable: isMaximize,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
contentIsMaximize,
|
contentIsMaximize,
|
||||||
toggleMaximize,
|
toggleMaximize,
|
||||||
|
toggleMaximizeAndTabbarHidden,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ function handleMenuOpen(key: string, path: string[]) {
|
||||||
:menus="menus"
|
:menus="menus"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:rounded="rounded"
|
:rounded="rounded"
|
||||||
|
scroll-to-active
|
||||||
:theme="theme"
|
:theme="theme"
|
||||||
@open="handleMenuOpen"
|
@open="handleMenuOpen"
|
||||||
@select="handleMenuSelect"
|
@select="handleMenuSelect"
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,37 @@ import { isHttpUrl, openRouteInNewWindow, openWindow } from '@vben/utils';
|
||||||
|
|
||||||
function useNavigation() {
|
function useNavigation() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const routes = router.getRoutes();
|
|
||||||
|
|
||||||
const routeMetaMap = new Map<string, RouteRecordNormalized>();
|
const routeMetaMap = new Map<string, RouteRecordNormalized>();
|
||||||
|
|
||||||
|
// 初始化路由映射
|
||||||
|
const initRouteMetaMap = () => {
|
||||||
|
const routes = router.getRoutes();
|
||||||
routes.forEach((route) => {
|
routes.forEach((route) => {
|
||||||
routeMetaMap.set(route.path, route);
|
routeMetaMap.set(route.path, route);
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
initRouteMetaMap();
|
||||||
|
|
||||||
|
// 监听路由变化
|
||||||
|
router.afterEach(() => {
|
||||||
|
initRouteMetaMap();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 检查是否应该在新窗口打开
|
||||||
|
const shouldOpenInNewWindow = (path: string): boolean => {
|
||||||
|
if (isHttpUrl(path)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const route = routeMetaMap.get(path);
|
||||||
|
return route?.meta?.openInNewWindow ?? false;
|
||||||
|
};
|
||||||
|
|
||||||
const navigation = async (path: string) => {
|
const navigation = async (path: string) => {
|
||||||
|
try {
|
||||||
const route = routeMetaMap.get(path);
|
const route = routeMetaMap.get(path);
|
||||||
const { openInNewWindow = false, query = {} } = route?.meta ?? {};
|
const { openInNewWindow = false, query = {} } = route?.meta ?? {};
|
||||||
|
|
||||||
if (isHttpUrl(path)) {
|
if (isHttpUrl(path)) {
|
||||||
openWindow(path, { target: '_blank' });
|
openWindow(path, { target: '_blank' });
|
||||||
} else if (openInNewWindow) {
|
} else if (openInNewWindow) {
|
||||||
|
|
@ -27,18 +47,14 @@ function useNavigation() {
|
||||||
query,
|
query,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Navigation failed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const willOpenedByWindow = (path: string) => {
|
const willOpenedByWindow = (path: string) => {
|
||||||
const route = routeMetaMap.get(path);
|
return shouldOpenInNewWindow(path);
|
||||||
const { openInNewWindow = false } = route?.meta ?? {};
|
|
||||||
if (isHttpUrl(path)) {
|
|
||||||
return true;
|
|
||||||
} else if (openInNewWindow) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return { navigation, willOpenedByWindow };
|
return { navigation, willOpenedByWindow };
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ defineOptions({
|
||||||
name: 'LanguageToggle',
|
name: 'LanguageToggle',
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handleUpdate(value: string) {
|
async function handleUpdate(value: string | undefined) {
|
||||||
|
if (!value) return;
|
||||||
const locale = value as SupportedLanguagesType;
|
const locale = value as SupportedLanguagesType;
|
||||||
updatePreferences({
|
updatePreferences({
|
||||||
app: {
|
app: {
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,8 @@ const menus = computed((): VbenDropdownMenuItem[] => [
|
||||||
|
|
||||||
const { authPanelCenter, authPanelLeft, authPanelRight } = usePreferences();
|
const { authPanelCenter, authPanelLeft, authPanelRight } = usePreferences();
|
||||||
|
|
||||||
function handleUpdate(value: string) {
|
function handleUpdate(value: string | undefined) {
|
||||||
|
if (!value) return;
|
||||||
updatePreferences({
|
updatePreferences({
|
||||||
app: {
|
app: {
|
||||||
authPageLayout: value as AuthPageLayoutType,
|
authPageLayout: value as AuthPageLayoutType,
|
||||||
|
|
|
||||||
|
|
@ -79,14 +79,14 @@ const handleCheckboxChange = () => {
|
||||||
</SwitchItem>
|
</SwitchItem>
|
||||||
<CheckboxItem
|
<CheckboxItem
|
||||||
:items="[
|
:items="[
|
||||||
{ label: '收缩按钮', value: 'collapsed' },
|
{ label: $t('preferences.sidebar.buttonCollapsed'), value: 'collapsed' },
|
||||||
{ label: '固定按钮', value: 'fixed' },
|
{ label: $t('preferences.sidebar.buttonFixed'), value: 'fixed' },
|
||||||
]"
|
]"
|
||||||
multiple
|
multiple
|
||||||
v-model="sidebarButtons"
|
v-model="sidebarButtons"
|
||||||
:on-btn-click="handleCheckboxChange"
|
:on-btn-click="handleCheckboxChange"
|
||||||
>
|
>
|
||||||
按钮配置
|
{{ $t('preferences.sidebar.buttons') }}
|
||||||
</CheckboxItem>
|
</CheckboxItem>
|
||||||
<NumberFieldItem
|
<NumberFieldItem
|
||||||
v-model="sidebarWidth"
|
v-model="sidebarWidth"
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue