fix: 基础功能,图片上传、管理员管理、菜单管理、角色管理
This commit is contained in:
@@ -21,6 +21,8 @@ import { $t } from '@vben/locales';
|
|||||||
|
|
||||||
import { notification } from 'ant-design-vue';
|
import { notification } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { registerComponent } from '#/components/form/component-map';
|
||||||
|
|
||||||
const AutoComplete = defineAsyncComponent(
|
const AutoComplete = defineAsyncComponent(
|
||||||
() => import('ant-design-vue/es/auto-complete'),
|
() => import('ant-design-vue/es/auto-complete'),
|
||||||
);
|
);
|
||||||
@@ -107,6 +109,7 @@ export type ComponentType =
|
|||||||
| 'ApiSelect'
|
| 'ApiSelect'
|
||||||
| 'ApiTreeSelect'
|
| 'ApiTreeSelect'
|
||||||
| 'AutoComplete'
|
| 'AutoComplete'
|
||||||
|
| 'Avatar'
|
||||||
| 'Checkbox'
|
| 'Checkbox'
|
||||||
| 'CheckboxGroup'
|
| 'CheckboxGroup'
|
||||||
| 'DatePicker'
|
| 'DatePicker'
|
||||||
@@ -199,6 +202,8 @@ async function initComponentAdapter() {
|
|||||||
Upload,
|
Upload,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 自动注册自定义组件
|
||||||
|
registerComponent(components);
|
||||||
// 将组件注册到全局共享状态中
|
// 将组件注册到全局共享状态中
|
||||||
globalShareState.setComponents(components);
|
globalShareState.setComponents(components);
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ export namespace AuthApi {
|
|||||||
/** 登录接口参数 */
|
/** 登录接口参数 */
|
||||||
export interface LoginParams {
|
export interface LoginParams {
|
||||||
password?: string;
|
password?: string;
|
||||||
username?: string;
|
account?: string;
|
||||||
|
remember?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 登录接口返回值 */
|
/** 登录接口返回值 */
|
||||||
export interface LoginResult {
|
export interface LoginResult {
|
||||||
accessToken: string;
|
token: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RefreshTokenResult {
|
export interface RefreshTokenResult {
|
||||||
@@ -22,14 +23,14 @@ export namespace AuthApi {
|
|||||||
* 登录
|
* 登录
|
||||||
*/
|
*/
|
||||||
export async function loginApi(data: AuthApi.LoginParams) {
|
export async function loginApi(data: AuthApi.LoginParams) {
|
||||||
return requestClient.post<AuthApi.LoginResult>('/auth/login', data);
|
return requestClient.post<AuthApi.LoginResult>('login', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刷新accessToken
|
* 刷新accessToken
|
||||||
*/
|
*/
|
||||||
export async function refreshTokenApi() {
|
export async function refreshTokenApi() {
|
||||||
return baseRequestClient.post<AuthApi.RefreshTokenResult>('/auth/refresh', {
|
return baseRequestClient.post<AuthApi.RefreshTokenResult>('admin/refresh', {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -38,7 +39,7 @@ export async function refreshTokenApi() {
|
|||||||
* 退出登录
|
* 退出登录
|
||||||
*/
|
*/
|
||||||
export async function logoutApi() {
|
export async function logoutApi() {
|
||||||
return baseRequestClient.post('/auth/logout', {
|
return baseRequestClient.post('admin/logout', {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -47,5 +48,5 @@ export async function logoutApi() {
|
|||||||
* 获取用户权限码
|
* 获取用户权限码
|
||||||
*/
|
*/
|
||||||
export async function getAccessCodesApi() {
|
export async function getAccessCodesApi() {
|
||||||
return requestClient.get<string[]>('/auth/codes');
|
return requestClient.get<string[]>('admin/codes');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,5 @@ import { requestClient } from '#/api/request';
|
|||||||
* 获取用户所有菜单
|
* 获取用户所有菜单
|
||||||
*/
|
*/
|
||||||
export async function getAllMenusApi() {
|
export async function getAllMenusApi() {
|
||||||
return requestClient.get<RouteRecordStringComponent[]>('/menu/all');
|
return requestClient.get<RouteRecordStringComponent[]>('admin/menu');
|
||||||
}
|
}
|
||||||
|
|||||||
17
apps/web-antd/src/api/core/upload.ts
Normal file
17
apps/web-antd/src/api/core/upload.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件上传
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export async function uploadFile(data: any) {
|
||||||
|
return requestClient.upload('upload/image', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过文件id集合获取文件信息
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export async function getFileInfoByIds(data: any) {
|
||||||
|
return requestClient.post('/sys/fileInfo/getFileInfoByIds', data);
|
||||||
|
}
|
||||||
@@ -6,5 +6,5 @@ import { requestClient } from '#/api/request';
|
|||||||
* 获取用户信息
|
* 获取用户信息
|
||||||
*/
|
*/
|
||||||
export async function getUserInfoApi() {
|
export async function getUserInfoApi() {
|
||||||
return requestClient.get<UserInfo>('/user/info');
|
return requestClient.get<UserInfo>('admin/my-info');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
|
|||||||
client.addResponseInterceptor(
|
client.addResponseInterceptor(
|
||||||
defaultResponseInterceptor({
|
defaultResponseInterceptor({
|
||||||
codeField: 'code',
|
codeField: 'code',
|
||||||
dataField: 'data',
|
dataField: 'result',
|
||||||
successCode: 0,
|
successCode: 0,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
77
apps/web-antd/src/components/description/description.vue
Normal file
77
apps/web-antd/src/components/description/description.vue
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
|
import type { DescItem } from './types';
|
||||||
|
|
||||||
|
import { Descriptions, DescriptionsItem } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { componentMap } from '#/components/view/component-map';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
title: { type: String, default: '' },
|
||||||
|
bordered: { type: Boolean, default: true },
|
||||||
|
size: {
|
||||||
|
type: String as PropType<'default' | 'middle' | 'small'>,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
type: [Number, Object],
|
||||||
|
default: () => {
|
||||||
|
// return { xxl: 4, xl: 3, lg: 3, md: 3, sm: 2, xs: 1 };
|
||||||
|
return 12;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
labelStyle: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {
|
||||||
|
width: '120px',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contentStyle: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {
|
||||||
|
width: '0px',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
type: Array as PropType<DescItem[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
data: { type: Object, default: undefined },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Descriptions
|
||||||
|
:bordered="bordered"
|
||||||
|
:column="column"
|
||||||
|
:content-style="contentStyle"
|
||||||
|
:label-style="labelStyle"
|
||||||
|
:size="size"
|
||||||
|
:title="title ? title : undefined"
|
||||||
|
>
|
||||||
|
<template v-for="item in schema" :key="item.field">
|
||||||
|
<DescriptionsItem :label="item.label" :span="item.span">
|
||||||
|
<component
|
||||||
|
:is="(componentMap as Map<String, any>).get(item.component)"
|
||||||
|
v-if="(componentMap as Map<String, any>).has(item.component)"
|
||||||
|
:value="data?.[item.field]"
|
||||||
|
v-bind="{ ...item.componentProps }"
|
||||||
|
/>
|
||||||
|
<component
|
||||||
|
:is="item.render(data?.[item.field], data)"
|
||||||
|
v-else-if="
|
||||||
|
!(componentMap as Map<String, any>).has(item.component) &&
|
||||||
|
item.render
|
||||||
|
"
|
||||||
|
:value="data?.[item.field]"
|
||||||
|
v-bind="{ ...item.componentProps }"
|
||||||
|
/>
|
||||||
|
<template v-else>{{ data?.[item.field] }}</template>
|
||||||
|
</DescriptionsItem>
|
||||||
|
</template>
|
||||||
|
</Descriptions>
|
||||||
|
</template>
|
||||||
2
apps/web-antd/src/components/description/index.ts
Normal file
2
apps/web-antd/src/components/description/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as Description } from './description.vue';
|
||||||
|
export type * from './types';
|
||||||
54
apps/web-antd/src/components/description/types.d.ts
vendored
Normal file
54
apps/web-antd/src/components/description/types.d.ts
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import type { CollapseContainerOptions } from '@/components/Container';
|
||||||
|
import type { DescriptionsProps } from 'ant-design-vue/es/descriptions';
|
||||||
|
|
||||||
|
import type { CSSProperties, VNode } from 'vue';
|
||||||
|
|
||||||
|
export interface DescItem {
|
||||||
|
labelMinWidth?: number;
|
||||||
|
contentMinWidth?: number;
|
||||||
|
labelStyle?: CSSProperties;
|
||||||
|
field: string;
|
||||||
|
label: JSX.Element | string | VNode;
|
||||||
|
// Merge column
|
||||||
|
span?: number;
|
||||||
|
show?: (...arg: any) => boolean;
|
||||||
|
// render
|
||||||
|
render?: (
|
||||||
|
val: any,
|
||||||
|
data: Recordable,
|
||||||
|
) => Element | JSX.Element | number | string | undefined | VNode;
|
||||||
|
component: string;
|
||||||
|
componentProps?: any;
|
||||||
|
children?: DescItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DescriptionProps extends DescriptionsProps {
|
||||||
|
// Whether to include the collapse component
|
||||||
|
useCollapse?: boolean;
|
||||||
|
/**
|
||||||
|
* item configuration
|
||||||
|
* @type DescItem
|
||||||
|
*/
|
||||||
|
schema: DescItem[];
|
||||||
|
/**
|
||||||
|
* 数据
|
||||||
|
* @type object
|
||||||
|
*/
|
||||||
|
data: Recordable;
|
||||||
|
/**
|
||||||
|
* Built-in CollapseContainer component configuration
|
||||||
|
* @type CollapseContainerOptions
|
||||||
|
*/
|
||||||
|
collapseOptions?: CollapseContainerOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DescInstance {
|
||||||
|
setDescProps(descProps: Partial<DescriptionProps>): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Register = (descInstance: DescInstance) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description:
|
||||||
|
*/
|
||||||
|
export type UseDescReturnType = [Register, DescInstance];
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, markRaw, onMounted, watch } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Spin } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { Description } from '#/components/description';
|
||||||
|
import { useSchemaStore } from '#/store/schema';
|
||||||
|
import { schemaToDetailForm } from '#/util/tool';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
tableName: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
addFieldPrefix: {
|
||||||
|
// 追加字段前辍
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const schemaStore = useSchemaStore();
|
||||||
|
const schema = computed(() => {
|
||||||
|
return schemaStore.getSchema(props.tableName);
|
||||||
|
});
|
||||||
|
const schemaDescItems = computed(() => {
|
||||||
|
return schemaToDetailForm(schema.value, props.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
const formObj = computed(() => {
|
||||||
|
const [Form, api] = useVbenForm({
|
||||||
|
showDefaultActions: false,
|
||||||
|
...schema.value,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
form: Form,
|
||||||
|
api,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const cData = computed(() => {
|
||||||
|
const newData: any = {};
|
||||||
|
Object.keys(props.data || {}).forEach((key: string) => {
|
||||||
|
// 如果key不以addFieldPrefix为前辍,则自动追加
|
||||||
|
if (key.startsWith(props.addFieldPrefix)) {
|
||||||
|
newData[key] = props.data[key];
|
||||||
|
} else {
|
||||||
|
newData[props.addFieldPrefix + key] = props.data[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
|
watch(
|
||||||
|
() => props.tableName,
|
||||||
|
() => {
|
||||||
|
schemaStore.requestData(props.tableName);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
onMounted(() => {
|
||||||
|
schemaStore.requestData(props.tableName);
|
||||||
|
});
|
||||||
|
defineExpose({
|
||||||
|
getForm() {
|
||||||
|
return markRaw(formObj.value?.form);
|
||||||
|
},
|
||||||
|
getFormApi() {
|
||||||
|
return formObj.value?.api;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const spinning = computed(() => {
|
||||||
|
return Object.keys(schema.value).length === 0;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Spin :spinning="spinning">
|
||||||
|
<div v-if="view" class="desc-wrap">
|
||||||
|
<div v-for="item in schemaDescItems" :key="item.field" class="desc-card">
|
||||||
|
<Description
|
||||||
|
:data="cData"
|
||||||
|
:label-style="{
|
||||||
|
width: '120px',
|
||||||
|
}"
|
||||||
|
:schema="item.children"
|
||||||
|
:title="schemaDescItems.length === 1 ? undefined : item.label"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Spin v-else :spinning="spinning">
|
||||||
|
<component :is="markRaw(formObj?.form)" />
|
||||||
|
</Spin>
|
||||||
|
</Spin>
|
||||||
|
</template>
|
||||||
|
<style scoped lang="less">
|
||||||
|
@import '#/assets/styles/common-form.less';
|
||||||
|
@import '#/assets/styles/common-detail.less';
|
||||||
|
</style>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as DynamicSchemaForm } from './dynamic-schema-form.vue';
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
|
import type { VbenFormProps } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import type { DescItem } from '../description';
|
||||||
|
|
||||||
|
import { onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { schemaToDetailForm } from '#/util/tool';
|
||||||
|
|
||||||
|
import { Description } from '../description';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
formOptions: {
|
||||||
|
type: Object as PropType<VbenFormProps>,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
descriptionProps: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const schemaDescItems = ref<DescItem[]>([]);
|
||||||
|
const initialSchemaDescItems = () => {
|
||||||
|
if (
|
||||||
|
Object.keys(props.formOptions).length > 0 &&
|
||||||
|
Object.keys(props.data).length > 0
|
||||||
|
) {
|
||||||
|
schemaDescItems.value = schemaToDetailForm(props.formOptions, props.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
onMounted(() => {
|
||||||
|
initialSchemaDescItems();
|
||||||
|
});
|
||||||
|
watch(
|
||||||
|
() => props.data,
|
||||||
|
() => {
|
||||||
|
initialSchemaDescItems();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
watch(
|
||||||
|
() => props.formOptions,
|
||||||
|
() => {
|
||||||
|
initialSchemaDescItems();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="desc-wrap">
|
||||||
|
<div
|
||||||
|
v-for="schema in schemaDescItems"
|
||||||
|
:key="schema.field"
|
||||||
|
class="desc-card"
|
||||||
|
>
|
||||||
|
<Description
|
||||||
|
:data="data"
|
||||||
|
:label-style="{
|
||||||
|
width: '150px',
|
||||||
|
}"
|
||||||
|
:schema="schema.children"
|
||||||
|
:title="schemaDescItems.length === 1 ? undefined : schema.label"
|
||||||
|
v-bind="descriptionProps"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped></style>
|
||||||
1
apps/web-antd/src/components/form-description/index.ts
Normal file
1
apps/web-antd/src/components/form-description/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as FormDescription } from './form-description.vue';
|
||||||
32
apps/web-antd/src/components/form/component-map.ts
Normal file
32
apps/web-antd/src/components/form/component-map.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import type { Component } from 'vue';
|
||||||
|
|
||||||
|
import type { CustomComponentType } from './types';
|
||||||
|
|
||||||
|
import { getFileNameWithoutExtension, toPascalCase } from '#/util/tool';
|
||||||
|
|
||||||
|
const componentMap = new Map<CustomComponentType | string, Component>();
|
||||||
|
// import.meta.glob() 直接引入所有的模块 Vite 独有的功能
|
||||||
|
const modules = import.meta.glob(
|
||||||
|
['./components/**/*.vue', '../../views/**/components/form/*.vue'],
|
||||||
|
{ eager: true },
|
||||||
|
);
|
||||||
|
// 加入到路由集合中
|
||||||
|
Object.keys(modules).forEach((key) => {
|
||||||
|
if (!key.includes('-ignore')) {
|
||||||
|
const mod = (modules as any)[key].default || {};
|
||||||
|
// ./components/ApiDict.vue
|
||||||
|
// 获取ApiDict
|
||||||
|
const compName = getFileNameWithoutExtension(key);
|
||||||
|
componentMap.set(toPascalCase(compName), mod);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* 注册组件
|
||||||
|
* @param components
|
||||||
|
*/
|
||||||
|
export const registerComponent = (components: any) => {
|
||||||
|
componentMap.forEach((value, key) => {
|
||||||
|
components[key] = value as Component;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
export { componentMap };
|
||||||
62
apps/web-antd/src/components/form/components/avatar.vue
Normal file
62
apps/web-antd/src/components/form/components/avatar.vue
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { Upload } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { uploadFile } from '#/api/core/upload';
|
||||||
|
import { Icon } from '#/components/icon';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
});
|
||||||
|
const props = defineProps({
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const emits = defineEmits(['update:value']);
|
||||||
|
const mValue = useVModel(props, 'value', emits, {
|
||||||
|
defaultValue: props.value,
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
const customRequest = (e: any) => {
|
||||||
|
uploadFile({
|
||||||
|
file: e.file,
|
||||||
|
}).then((data: any) => {
|
||||||
|
mValue.value = data.url;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const handleRemove = (e: Event) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
mValue.value = '';
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Upload
|
||||||
|
:custom-request="customRequest"
|
||||||
|
:show-upload-list="false"
|
||||||
|
list-type="picture-card"
|
||||||
|
>
|
||||||
|
<div v-if="mValue" class="m-avatar-wrap">
|
||||||
|
<Icon
|
||||||
|
class="m-avatar-icon-delete"
|
||||||
|
icon="ant-design:delete-outlined"
|
||||||
|
@click="handleRemove"
|
||||||
|
/>
|
||||||
|
<img :src="value" width="100%" />
|
||||||
|
</div>
|
||||||
|
<Icon v-else icon="ant-design:plus-outlined" />
|
||||||
|
</Upload>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-avatar-wrap {
|
||||||
|
position: relative;
|
||||||
|
height: 102px;
|
||||||
|
.m-avatar-icon-delete {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
175
apps/web-antd/src/components/form/components/upload-image.vue
Normal file
175
apps/web-antd/src/components/form/components/upload-image.vue
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { Modal, Upload } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { uploadFile } from '#/api/core/upload';
|
||||||
|
import { Icon } from '#/components/icon';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'UploadImage',
|
||||||
|
inheritAttrs: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Array as () => string[],
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
multiple: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
maxCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 9,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits(['update:modelValue']);
|
||||||
|
|
||||||
|
const mValue = useVModel(props, 'modelValue', emits, {
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileList = ref(
|
||||||
|
props.modelValue.map((url) => ({
|
||||||
|
uid: url,
|
||||||
|
name: url.split('/').pop() || 'file',
|
||||||
|
status: 'done',
|
||||||
|
url,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听 modelValue 变化,同步到 fileList
|
||||||
|
watch(props.modelValue, (newVal) => {
|
||||||
|
fileList.value = newVal.map((url) => ({
|
||||||
|
uid: url,
|
||||||
|
name: url.split('/').pop() || 'file',
|
||||||
|
status: 'done',
|
||||||
|
url,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听 fileList 变化,同步到 modelValue
|
||||||
|
watch(fileList, (newVal) => {
|
||||||
|
mValue.value = newVal
|
||||||
|
.filter((file) => file.status === 'done' && file.url)
|
||||||
|
.map((file) => file.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
const customRequest = async (e: any) => {
|
||||||
|
try {
|
||||||
|
const res = await uploadFile({
|
||||||
|
file: e.file,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新 fileList
|
||||||
|
fileList.value = [
|
||||||
|
...fileList.value.filter((file) => file.status !== 'uploading'),
|
||||||
|
{
|
||||||
|
uid: res.url,
|
||||||
|
name: e.file.name,
|
||||||
|
status: 'done',
|
||||||
|
url: res.url,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 更新 modelValue
|
||||||
|
mValue.value = fileList.value.map((file) => file.url);
|
||||||
|
|
||||||
|
// 触发上传完成回调
|
||||||
|
e.onSuccess?.(res);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传失败', error);
|
||||||
|
e.onError?.(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = (file: any) => {
|
||||||
|
// 从 fileList 中移除
|
||||||
|
fileList.value = fileList.value.filter((item) => item.uid !== file.uid);
|
||||||
|
|
||||||
|
// 更新 modelValue
|
||||||
|
mValue.value = fileList.value.map((file) => file.url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const previewVisible = ref(false);
|
||||||
|
const previewImage = ref('');
|
||||||
|
const previewTitle = ref('');
|
||||||
|
|
||||||
|
const handlePreview = async (file) => {
|
||||||
|
previewImage.value = file.response.url || file.preview;
|
||||||
|
previewVisible.value = true;
|
||||||
|
previewTitle.value =
|
||||||
|
file.name || file.url.slice(Math.max(0, file.url.lastIndexOf('/') + 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 计算是否还能上传更多图片
|
||||||
|
const showUploadButton = computed(() =>
|
||||||
|
props.multiple
|
||||||
|
? fileList.value.length < props.maxCount
|
||||||
|
: fileList.value.length === 0,
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Upload
|
||||||
|
v-model:value="fileList"
|
||||||
|
:custom-request="customRequest"
|
||||||
|
:multiple="multiple"
|
||||||
|
:limit="maxCount"
|
||||||
|
:show-upload-list="{ showPreviewIcon: true, showRemoveIcon: true }"
|
||||||
|
list-type="picture-card"
|
||||||
|
@remove="handleRemove"
|
||||||
|
@preview="handlePreview"
|
||||||
|
>
|
||||||
|
<div v-if="showUploadButton" class="upload-button">
|
||||||
|
<Icon icon="ant-design:plus-outlined" />
|
||||||
|
<div class="ant-upload-text">上传图片</div>
|
||||||
|
</div>
|
||||||
|
</Upload>
|
||||||
|
<Modal
|
||||||
|
v-model:visible="previewVisible"
|
||||||
|
:title="previewTitle"
|
||||||
|
footer=""
|
||||||
|
width="60%"
|
||||||
|
>
|
||||||
|
<img alt="example" style="width: 100%" :src="previewImage" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-avatar-wrap {
|
||||||
|
position: relative;
|
||||||
|
height: 102px;
|
||||||
|
width: 102px;
|
||||||
|
|
||||||
|
.m-avatar-icon-delete {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
color: white;
|
||||||
|
border-radius: 0 0 0 4px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .m-avatar-icon-delete {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
160
apps/web-antd/src/components/form/components/upload.vue
Normal file
160
apps/web-antd/src/components/form/components/upload.vue
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { UploadChangeParam, UploadFile } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
|
import { computed, onMounted, ref, useAttrs, watch } from 'vue';
|
||||||
|
|
||||||
|
import { useAccessStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import { Button, message, Upload } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getFileInfoByIds } from '#/api/core/upload';
|
||||||
|
import { Icon } from '#/components/icon';
|
||||||
|
import { omit } from '#/util/tool';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'Upload',
|
||||||
|
inheritAttrs: false,
|
||||||
|
});
|
||||||
|
const props = defineProps({
|
||||||
|
value: {
|
||||||
|
// 文件id,多个使用英文逗号分割
|
||||||
|
type: [String] as PropType<string>,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
// 上传的地址
|
||||||
|
type: String,
|
||||||
|
default: `${import.meta.env.VITE_GLOB_API_URL}/sys/fileInfo/upload`,
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
// 上传请求的 headers
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
// 是否禁用
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
listType: {
|
||||||
|
// 上传类型
|
||||||
|
type: String as PropType<'picture' | 'picture-card' | 'text'>,
|
||||||
|
default: 'text',
|
||||||
|
},
|
||||||
|
maxCount: {
|
||||||
|
// 最大上传数量
|
||||||
|
type: Number,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
uploadName: {
|
||||||
|
// 文件上传字段名,name被字段名覆写了,要换成uploadName
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
separator: {
|
||||||
|
// 文件id分隔符
|
||||||
|
type: String,
|
||||||
|
default: ',',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const emits = defineEmits(['update:modelValue']);
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
const fileList = ref<UploadFile[]>([]);
|
||||||
|
const attrs = useAttrs();
|
||||||
|
const bindProps = computed(() => {
|
||||||
|
return {
|
||||||
|
...omit(attrs, ['onChange', 'onInput', 'onBlur', 'name']),
|
||||||
|
...props,
|
||||||
|
name: props.uploadName || 'file',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const _getFileInfoByIds = (val: string | string[] | undefined) => {
|
||||||
|
if (!val) return;
|
||||||
|
getFileInfoByIds({
|
||||||
|
fileInfoIds: val,
|
||||||
|
}).then((res) => {
|
||||||
|
fileList.value = res || [];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const mValue = computed({
|
||||||
|
get() {
|
||||||
|
return props.value;
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
emits('update:modelValue', val);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const mHeaders = {
|
||||||
|
Authorization: `Bearer ${accessStore.accessToken}`,
|
||||||
|
...props.headers,
|
||||||
|
};
|
||||||
|
const handleChange = (info: UploadChangeParam) => {
|
||||||
|
if (info.file.status === 'done') {
|
||||||
|
// 更新file-list当前file的uid
|
||||||
|
fileList.value = fileList.value.map((item) => {
|
||||||
|
if (item.uid === info.file.uid) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
uid: info.file.response.data.fileInfoId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
mValue.value = fileList.value
|
||||||
|
.map((item) => {
|
||||||
|
return item.uid;
|
||||||
|
})
|
||||||
|
.join(props.separator);
|
||||||
|
} else if (info.file.status === 'error') {
|
||||||
|
message.error(`${info.file.name} file upload failed.`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleRemove = (file: any) => {
|
||||||
|
mValue.value = fileList.value
|
||||||
|
.filter((item: any) => item.uid !== file.uid)
|
||||||
|
.map((item: any) => item.uid)
|
||||||
|
.join(props.separator);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
watch(
|
||||||
|
() => props.value,
|
||||||
|
(val) => {
|
||||||
|
_getFileInfoByIds(val);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
onMounted(() => {
|
||||||
|
_getFileInfoByIds(props.value);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Upload
|
||||||
|
v-model:file-list="fileList"
|
||||||
|
v-bind="bindProps"
|
||||||
|
:action="action"
|
||||||
|
:disabled="disabled"
|
||||||
|
:headers="mHeaders"
|
||||||
|
:list-type="listType"
|
||||||
|
:max-count="maxCount"
|
||||||
|
@change="handleChange"
|
||||||
|
@remove="handleRemove"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<Icon
|
||||||
|
v-if="listType === 'picture-card'"
|
||||||
|
:size="24"
|
||||||
|
icon="ant-design:plus-outlined"
|
||||||
|
/>
|
||||||
|
<Button v-else>
|
||||||
|
上传文件
|
||||||
|
<template #icon>
|
||||||
|
<Icon icon="ant-design:cloud-upload-outlined" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
<template v-for="item in Object.keys($slots)" #[item]="data">
|
||||||
|
<slot :name="item" v-bind="data || {}"></slot>
|
||||||
|
</template>
|
||||||
|
</Upload>
|
||||||
|
</template>
|
||||||
7
apps/web-antd/src/components/form/index.ts
Normal file
7
apps/web-antd/src/components/form/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// export { default as ApiCheckboxGroup } from './components/api-checkbox-group.vue';
|
||||||
|
// export { default as ApiDict } from './components/api-dict.vue';
|
||||||
|
// export { default as ApiRadioGroup } from './components/api-radio-group.vue';
|
||||||
|
// export { default as ApiSelect } from './components/api-select.vue';
|
||||||
|
// export { default as ApiTreeSelect } from './components/api-tree-select.vue';
|
||||||
|
// export { default as IconPicker } from './components/icon-picker.vue';
|
||||||
|
export { default as Upload } from './components/upload.vue';
|
||||||
7
apps/web-antd/src/components/form/types/index.d.ts
vendored
Normal file
7
apps/web-antd/src/components/form/types/index.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export type CustomComponentType =
|
||||||
|
| 'ApiCheckboxGroup'
|
||||||
|
| 'ApiDict'
|
||||||
|
| 'ApiRadioGroup'
|
||||||
|
| 'ApiSelect'
|
||||||
|
| 'ApiTreeSelect'
|
||||||
|
| 'IconPicker';
|
||||||
51
apps/web-antd/src/components/icon/icon.vue
Normal file
51
apps/web-antd/src/components/icon/icon.vue
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, h } from 'vue';
|
||||||
|
|
||||||
|
import { IconifyIcon as VbenIcon } from '@vben/icons';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: '16px',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const iconComp = computed(() => {
|
||||||
|
if (props.icon.startsWith('http')) {
|
||||||
|
return () => h('img', { src: props.icon, class: 'm-icon__' });
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
const styles = computed(() => {
|
||||||
|
return {
|
||||||
|
fontSize: props.size.toString().endsWith('px')
|
||||||
|
? props.size
|
||||||
|
: `${props.size}px`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component :is="iconComp" v-if="iconComp" :style="styles" />
|
||||||
|
<VbenIcon v-else :icon="props.icon" :style="styles" class="m-icon__" />
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.m-icon__ {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: 0;
|
||||||
|
color: inherit;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: none;
|
||||||
|
vertical-align: -0.125em;
|
||||||
|
text-rendering: optimizelegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1
apps/web-antd/src/components/icon/index.ts
Normal file
1
apps/web-antd/src/components/icon/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as Icon } from './icon.vue';
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
const list = ref('');
|
||||||
|
const previewTitle = ref('二维码');
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
fullscreenButton: false,
|
||||||
|
draggable: true,
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onConfirm: async () => {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onOpenChange(isOpen: boolean) {
|
||||||
|
if (isOpen) {
|
||||||
|
const { values, title } = modalApi.getData<Record<string, any>>();
|
||||||
|
if (values) {
|
||||||
|
list.value = values;
|
||||||
|
previewTitle.value = title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Modal title="提示框" class="w-[50%]">
|
||||||
|
您的处方中有以下药品存在{{ previewTitle }}
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
169
apps/web-antd/src/components/modal/DoctorQrCodePreview.vue
Normal file
169
apps/web-antd/src/components/modal/DoctorQrCodePreview.vue
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Avatar, Button, Image } from 'ant-design-vue';
|
||||||
|
import html2canvas from 'html2canvas';
|
||||||
|
|
||||||
|
import { getDoctorInfoByStoreApi } from '#/views/doctor/doctor/api';
|
||||||
|
|
||||||
|
const qrCodeUrl = ref('');
|
||||||
|
const htmlToImage = ref('');
|
||||||
|
const previewAddress = ref('');
|
||||||
|
const goodAt = ref('');
|
||||||
|
const doctorInfo = ref();
|
||||||
|
const doctorId = ref(0);
|
||||||
|
const storeId = ref(0);
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
fullscreenButton: false,
|
||||||
|
footer: false,
|
||||||
|
showCancelButton: true,
|
||||||
|
showConfirmButton: true,
|
||||||
|
confirmText: '保存医生卡片图片',
|
||||||
|
cancelText: '保存医生二维码图片',
|
||||||
|
draggable: true,
|
||||||
|
onCancel() {
|
||||||
|
doctorInfo.value = null;
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onConfirm: async () => {
|
||||||
|
doctorInfo.value = null;
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onOpenChange(isOpen: boolean) {
|
||||||
|
modalApi.setState({
|
||||||
|
loading: true,
|
||||||
|
});
|
||||||
|
if (isOpen) {
|
||||||
|
const { values } = modalApi.getData<Record<string, any>>();
|
||||||
|
if (values) {
|
||||||
|
if (values.doctor_id !== undefined) {
|
||||||
|
doctorId.value = values.doctor_id;
|
||||||
|
}
|
||||||
|
if (values.store_id !== undefined) {
|
||||||
|
storeId.value = values.store_id;
|
||||||
|
}
|
||||||
|
getDoctorInfoByStore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const getDoctorInfoByStore = () => {
|
||||||
|
getDoctorInfoByStoreApi({
|
||||||
|
doctor_id: doctorId.value,
|
||||||
|
store_id: storeId.value,
|
||||||
|
}).then((res) => {
|
||||||
|
doctorInfo.value = res;
|
||||||
|
goodAt.value = res.doctor.good_at;
|
||||||
|
previewAddress.value =
|
||||||
|
res.store.province.name + res.store.city.name + res.store.position;
|
||||||
|
|
||||||
|
modalApi.setState({
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 下载二维码
|
||||||
|
* @param name
|
||||||
|
* @param isQr
|
||||||
|
*/
|
||||||
|
const downloadQRCode = async (name: string, isQr = false) => {
|
||||||
|
const element = isQr ? qrCodeUrl.value : htmlToImage.value;
|
||||||
|
html2canvas(element, {
|
||||||
|
useCORS: true,
|
||||||
|
allowTaint: true,
|
||||||
|
logging: false,
|
||||||
|
}).then((canvas) => {
|
||||||
|
// 创建a标签下载
|
||||||
|
const link = document.createElement('a'); // 创建a标签
|
||||||
|
link.href = canvas.toDataURL(); // 是canvas对象的一种方法,用于将canvas对象转换为base64位编码
|
||||||
|
link.setAttribute('download', `${doctorInfo.value.doctor.name}${name}.png`); // 利用了a标签的download 来下载 canvas图片
|
||||||
|
link.style.display = 'none'; // 将图片隐藏起来
|
||||||
|
document.body.append(link); // 插入到其中
|
||||||
|
link.click();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Modal v-if="doctorInfo" title="医生二维码" class="w-[50%]">
|
||||||
|
<template #footer>
|
||||||
|
<Button type="primary" @click="downloadQRCode('二维码卡片下载')">
|
||||||
|
二维码卡片下载
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" @click="downloadQRCode('二维码下载', true)">
|
||||||
|
二维码下载
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
<div class="qrBox" style="text-align: center">
|
||||||
|
<div ref="htmlToImage" class="qrModal">
|
||||||
|
<div id="qrcode" ref="qrCodeUrl" class="qrcode">
|
||||||
|
<Avatar :src="doctorInfo.doctor.avatar" />
|
||||||
|
<div class="qrTitle">
|
||||||
|
{{ doctorInfo.doctor.name }} {{ doctorInfo.doctor.title.name }}
|
||||||
|
</div>
|
||||||
|
<div class="qrAddress">
|
||||||
|
<span class="addressTitle">诊所地址:</span>{{ previewAddress }}
|
||||||
|
</div>
|
||||||
|
<Image
|
||||||
|
:preview="false"
|
||||||
|
:src="doctorInfo.qr_code"
|
||||||
|
class="mt-3"
|
||||||
|
height="30"
|
||||||
|
width="30"
|
||||||
|
/>
|
||||||
|
<h3 class="border-l-4 border-blue-500 pl-3 text-lg font-semibold">
|
||||||
|
擅长领域
|
||||||
|
</h3>
|
||||||
|
<div
|
||||||
|
class="prose max-w-none whitespace-pre-wrap"
|
||||||
|
style="text-indent: 2em"
|
||||||
|
>
|
||||||
|
{{ doctorInfo.doctor.good_at }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.qrModal {
|
||||||
|
padding: 20px 30px;
|
||||||
|
// 宽度适应文本长度
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle at center,
|
||||||
|
rgb(173 216 230 / 80%),
|
||||||
|
rgb(135 206 235 / 80%),
|
||||||
|
rgb(100 149 237 / 80%)
|
||||||
|
);
|
||||||
|
|
||||||
|
.qrTitle {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qrAddress {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addressTitle {
|
||||||
|
color: #3a8ee6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qrcode {
|
||||||
|
width: fit-content;
|
||||||
|
padding: 20px 30px;
|
||||||
|
//width: 50%;
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
34
apps/web-antd/src/components/modal/ImagePreview.vue
Normal file
34
apps/web-antd/src/components/modal/ImagePreview.vue
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Image } from 'ant-design-vue';
|
||||||
|
|
||||||
|
const url = ref('');
|
||||||
|
const previewTitle = ref('二维码');
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
fullscreenButton: false,
|
||||||
|
draggable: true,
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onConfirm: async () => {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onOpenChange(isOpen: boolean) {
|
||||||
|
if (isOpen) {
|
||||||
|
const { values, title } = modalApi.getData<Record<string, any>>();
|
||||||
|
if (values) {
|
||||||
|
url.value = values;
|
||||||
|
previewTitle.value = title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Modal :title="previewTitle" class="w-[50%]">
|
||||||
|
<Image :src="url" height="30" width="30" />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
135
apps/web-antd/src/components/modal/QrCodePreview.vue
Normal file
135
apps/web-antd/src/components/modal/QrCodePreview.vue
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Button, Image } from 'ant-design-vue';
|
||||||
|
import html2canvas from 'html2canvas';
|
||||||
|
|
||||||
|
const qrCodeUrl = ref('');
|
||||||
|
const htmlToImage = ref('');
|
||||||
|
const url = ref('');
|
||||||
|
const previewTitle = ref('二维码');
|
||||||
|
const previewAddress = ref('');
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
fullscreenButton: false,
|
||||||
|
footer: false,
|
||||||
|
header: false,
|
||||||
|
showCancelButton: true,
|
||||||
|
showConfirmButton: true,
|
||||||
|
confirmText: '保存诊所卡片图片',
|
||||||
|
cancelText: '保存诊所二维码图片',
|
||||||
|
draggable: true,
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onConfirm: async () => {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onOpenChange(isOpen: boolean) {
|
||||||
|
if (isOpen) {
|
||||||
|
const { values, title, address } =
|
||||||
|
modalApi.getData<Record<string, any>>();
|
||||||
|
if (values) {
|
||||||
|
url.value = values;
|
||||||
|
qrCodeUrl.value = values;
|
||||||
|
previewTitle.value = title;
|
||||||
|
previewAddress.value = address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* 下载二维码
|
||||||
|
* @param name
|
||||||
|
* @param isQr
|
||||||
|
*/
|
||||||
|
const downloadQRCode = async (name: string, isQr = false) => {
|
||||||
|
const element = isQr ? qrCodeUrl.value : htmlToImage.value;
|
||||||
|
html2canvas(element, {
|
||||||
|
useCORS: true,
|
||||||
|
allowTaint: true,
|
||||||
|
logging: false,
|
||||||
|
}).then((canvas) => {
|
||||||
|
// 创建a标签下载
|
||||||
|
const link = document.createElement('a'); // 创建a标签
|
||||||
|
link.href = canvas.toDataURL(); // 是canvas对象的一种方法,用于将canvas对象转换为base64位编码
|
||||||
|
link.setAttribute('download', `${previewTitle.value}${name}.png`); // 利用了a标签的download 来下载 canvas图片
|
||||||
|
link.style.display = 'none'; // 将图片隐藏起来
|
||||||
|
document.body.append(link); // 插入到其中
|
||||||
|
link.click();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Modal :title="`${previewTitle}二维码`" class="w-[50%]">
|
||||||
|
<div class="qrBox" style="text-align: center">
|
||||||
|
<div ref="htmlToImage" class="qrModal">
|
||||||
|
<div id="qrcode" ref="qrCodeUrl" class="qrcode">
|
||||||
|
<div class="qrTitle">{{ previewTitle }}</div>
|
||||||
|
<Image :preview="false" :src="url" height="30" width="30" />
|
||||||
|
<div class="qrAddress">
|
||||||
|
<span class="addressTitle">诊所地址:</span>{{ previewAddress }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
style="margin: 20px auto"
|
||||||
|
type="primary"
|
||||||
|
@click="downloadQRCode('二维码卡片下载')"
|
||||||
|
>
|
||||||
|
二维码卡片下载
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
style="margin: 20px auto 20px 20px"
|
||||||
|
type="primary"
|
||||||
|
@click="downloadQRCode('二维码下载', true)"
|
||||||
|
>
|
||||||
|
二维码下载
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.qrModal {
|
||||||
|
padding: 20px 30px;
|
||||||
|
// 宽度适应文本长度
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle at center,
|
||||||
|
rgb(173 216 230 / 80%),
|
||||||
|
rgb(135 206 235 / 80%),
|
||||||
|
rgb(100 149 237 / 80%)
|
||||||
|
);
|
||||||
|
|
||||||
|
.qrTitle {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qrAddress {
|
||||||
|
margin-top: 30px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addressTitle {
|
||||||
|
color: #3a8ee6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qrcode {
|
||||||
|
width: fit-content;
|
||||||
|
padding: 20px 30px;
|
||||||
|
//width: 50%;
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
2
apps/web-antd/src/components/table-action/index.ts
Normal file
2
apps/web-antd/src/components/table-action/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as TableAction } from './table-action.vue';
|
||||||
|
export type * from './types.d.ts';
|
||||||
227
apps/web-antd/src/components/table-action/table-action.vue
Normal file
227
apps/web-antd/src/components/table-action/table-action.vue
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { ButtonType } from 'ant-design-vue/es/button';
|
||||||
|
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
|
import type { ActionItem, PopConfirm } from './types';
|
||||||
|
|
||||||
|
import { computed, toRaw } from 'vue';
|
||||||
|
|
||||||
|
import { useAccess } from '@vben/access';
|
||||||
|
import { isBoolean, isFunction } from '@vben/utils';
|
||||||
|
|
||||||
|
import { Button, Dropdown, Menu, Popconfirm, Space } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { Icon } from '#/components/icon';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
actions: {
|
||||||
|
type: Array as PropType<ActionItem[]>,
|
||||||
|
default() {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dropDownActions: {
|
||||||
|
type: Array as PropType<ActionItem[]>,
|
||||||
|
default() {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
divider: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const MenuItem = Menu.Item;
|
||||||
|
|
||||||
|
const { hasAccessByCodes } = useAccess();
|
||||||
|
function isIfShow(action: ActionItem): boolean {
|
||||||
|
const ifShow = action.ifShow;
|
||||||
|
|
||||||
|
let isIfShow = true;
|
||||||
|
|
||||||
|
if (isBoolean(ifShow)) {
|
||||||
|
isIfShow = ifShow;
|
||||||
|
}
|
||||||
|
if (isFunction(ifShow)) {
|
||||||
|
isIfShow = ifShow(action);
|
||||||
|
}
|
||||||
|
return isIfShow;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getActions = computed(() => {
|
||||||
|
return (toRaw(props.actions) || [])
|
||||||
|
.filter((action) => {
|
||||||
|
return (
|
||||||
|
(hasAccessByCodes(action.auth || []) ||
|
||||||
|
(action.auth || []).length === 0) &&
|
||||||
|
isIfShow(action)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map((action) => {
|
||||||
|
const { popConfirm } = action;
|
||||||
|
return {
|
||||||
|
// getPopupContainer: document.body,
|
||||||
|
type: 'link' as ButtonType,
|
||||||
|
...action,
|
||||||
|
...popConfirm,
|
||||||
|
onConfirm: popConfirm?.confirm,
|
||||||
|
onCancel: popConfirm?.cancel,
|
||||||
|
enable: !!popConfirm,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const getDropdownList = computed((): any[] => {
|
||||||
|
return (toRaw(props.dropDownActions) || [])
|
||||||
|
.filter((action) => {
|
||||||
|
return (
|
||||||
|
(hasAccessByCodes(action.auth || []) ||
|
||||||
|
(action.auth || []).length === 0) &&
|
||||||
|
isIfShow(action)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map((action, index) => {
|
||||||
|
const { label, popConfirm } = action;
|
||||||
|
return {
|
||||||
|
...action,
|
||||||
|
...popConfirm,
|
||||||
|
onConfirm: popConfirm?.confirm,
|
||||||
|
onCancel: popConfirm?.cancel,
|
||||||
|
text: label,
|
||||||
|
divider:
|
||||||
|
index < props.dropDownActions.length - 1 ? props.divider : false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const getPopConfirmProps = (attrs: PopConfirm) => {
|
||||||
|
const originAttrs: any = attrs;
|
||||||
|
delete originAttrs.icon;
|
||||||
|
if (attrs.confirm && isFunction(attrs.confirm)) {
|
||||||
|
originAttrs.onConfirm = attrs.confirm;
|
||||||
|
delete originAttrs.confirm;
|
||||||
|
}
|
||||||
|
if (attrs.cancel && isFunction(attrs.cancel)) {
|
||||||
|
originAttrs.onCancel = attrs.cancel;
|
||||||
|
delete originAttrs.cancel;
|
||||||
|
}
|
||||||
|
return originAttrs;
|
||||||
|
};
|
||||||
|
const getButtonProps = (action: ActionItem) => {
|
||||||
|
const res = {
|
||||||
|
type: action.type || 'primary',
|
||||||
|
...action,
|
||||||
|
};
|
||||||
|
delete res.icon;
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
const handleMenuClick = (e: any) => {
|
||||||
|
const action = getDropdownList.value[e.key];
|
||||||
|
if (action.onClick && isFunction(action.onClick)) {
|
||||||
|
action.onClick();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="m-table-action">
|
||||||
|
<Space
|
||||||
|
:size="
|
||||||
|
getActions?.some((item: ActionItem) => item.type === 'link') ? 0 : 8
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<template v-for="(action, index) in getActions" :key="index">
|
||||||
|
<Popconfirm
|
||||||
|
v-if="action.popConfirm"
|
||||||
|
v-bind="getPopConfirmProps(action.popConfirm)"
|
||||||
|
>
|
||||||
|
<template v-if="action.popConfirm.icon" #icon>
|
||||||
|
<Icon :icon="action.popConfirm.icon" />
|
||||||
|
</template>
|
||||||
|
<Button v-bind="getButtonProps(action)">
|
||||||
|
<template v-if="action.icon" #icon>
|
||||||
|
<Icon :icon="action.icon" />
|
||||||
|
</template>
|
||||||
|
{{ action.label }}
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
<Button v-else v-bind="getButtonProps(action)" @click="action.onClick">
|
||||||
|
<template v-if="action.icon" #icon>
|
||||||
|
<Icon :icon="action.icon" />
|
||||||
|
</template>
|
||||||
|
{{ action.label }}
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<Dropdown v-if="getDropdownList.length > 0" :trigger="['hover']">
|
||||||
|
<slot name="more">
|
||||||
|
<Button size="small" type="link">
|
||||||
|
<template #icon>
|
||||||
|
<Icon class="icon-more" icon="ant-design:more-outlined" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</slot>
|
||||||
|
<template #overlay>
|
||||||
|
<Menu @click="handleMenuClick">
|
||||||
|
<MenuItem v-for="(action, index) in getDropdownList" :key="index">
|
||||||
|
<template v-if="action.popConfirm">
|
||||||
|
<Popconfirm v-bind="getPopConfirmProps(action.popConfirm)">
|
||||||
|
<template v-if="action.popConfirm.icon" #icon>
|
||||||
|
<Icon :icon="action.popConfirm.icon" />
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
action.disabled === true
|
||||||
|
? 'cursor-not-allowed text-gray-300'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<Icon v-if="action.icon" :icon="action.icon" />
|
||||||
|
<span class="ml-1">{{ action.text }}</span>
|
||||||
|
</div>
|
||||||
|
</Popconfirm>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
action.disabled === true
|
||||||
|
? 'cursor-not-allowed text-gray-300'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<Icon v-if="action.icon" :icon="action.icon" />
|
||||||
|
{{ action.label }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</template>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less">
|
||||||
|
/** 修复 iconify 位置问题 **/
|
||||||
|
.m-table-action {
|
||||||
|
.ant-btn > .iconify + span,
|
||||||
|
.ant-btn > span + .iconify {
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-btn > .iconify {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: 0;
|
||||||
|
color: inherit;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: none;
|
||||||
|
vertical-align: -0.125em;
|
||||||
|
text-rendering: optimizelegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
26
apps/web-antd/src/components/table-action/types.d.ts
vendored
Normal file
26
apps/web-antd/src/components/table-action/types.d.ts
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { ButtonProps } from 'ant-design-vue/es/button/buttonTypes';
|
||||||
|
import { TooltipProps } from 'ant-design-vue/es/tooltip/Tooltip';
|
||||||
|
|
||||||
|
export interface PopConfirm {
|
||||||
|
title: string;
|
||||||
|
okText?: string;
|
||||||
|
cancelText?: string;
|
||||||
|
confirm: Fn;
|
||||||
|
cancel?: Fn;
|
||||||
|
icon?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
export interface ActionItem extends ButtonProps {
|
||||||
|
onClick?: Fn;
|
||||||
|
label?: string;
|
||||||
|
color?: 'error' | 'success' | 'warning';
|
||||||
|
icon?: string;
|
||||||
|
popConfirm?: PopConfirm;
|
||||||
|
disabled?: boolean;
|
||||||
|
divider?: boolean;
|
||||||
|
// 权限编码控制是否显示
|
||||||
|
auth?: string[];
|
||||||
|
// 业务控制是否显示
|
||||||
|
ifShow?: ((action: ActionItem) => boolean) | boolean;
|
||||||
|
tooltip?: string | TooltipProps;
|
||||||
|
}
|
||||||
22
apps/web-antd/src/components/view/component-map.ts
Normal file
22
apps/web-antd/src/components/view/component-map.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import type { Component } from 'vue';
|
||||||
|
|
||||||
|
import { getFileNameWithoutExtension, toPascalCase } from '#/util/tool';
|
||||||
|
|
||||||
|
const componentMap = new Map<string, Component>();
|
||||||
|
// import.meta.glob() 直接引入所有的模块 Vite 独有的功能
|
||||||
|
const modules = import.meta.glob(
|
||||||
|
['./components/**/*.vue', '../../views/**/components/view/*.vue'],
|
||||||
|
{ eager: true },
|
||||||
|
);
|
||||||
|
// 加入到路由集合中
|
||||||
|
Object.keys(modules).forEach((key) => {
|
||||||
|
if (!key.includes('-ignore')) {
|
||||||
|
const mod = (modules as any)[key].default || {};
|
||||||
|
// ./components/ApiDict.vue
|
||||||
|
// 获取ApiDict
|
||||||
|
const compName = getFileNameWithoutExtension(key);
|
||||||
|
componentMap.set(toPascalCase(compName), mod);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export { componentMap };
|
||||||
100
apps/web-antd/src/components/view/components/api-cascader.vue
Normal file
100
apps/web-antd/src/components/view/components/api-cascader.vue
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
// 值
|
||||||
|
type: [String, Number, Array],
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
api: {
|
||||||
|
// 接口请求对象
|
||||||
|
type: [Function, String] as PropType<
|
||||||
|
((...arg: any) => Promise<any>) | String
|
||||||
|
>,
|
||||||
|
default() {
|
||||||
|
return () => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolve([]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cacheKey: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
requestMethod: {
|
||||||
|
type: String,
|
||||||
|
default: 'post',
|
||||||
|
},
|
||||||
|
valueField: {
|
||||||
|
type: String,
|
||||||
|
default: 'id',
|
||||||
|
},
|
||||||
|
labelField: {
|
||||||
|
type: String,
|
||||||
|
default: 'name',
|
||||||
|
},
|
||||||
|
// 分割符
|
||||||
|
split: {
|
||||||
|
type: String,
|
||||||
|
default: '/',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const currentData = ref([]);
|
||||||
|
const cValue = computed(() => {
|
||||||
|
return currentData.value
|
||||||
|
.map((item) => item[props.labelField])
|
||||||
|
.join(props.split);
|
||||||
|
});
|
||||||
|
onMounted(() => {
|
||||||
|
const api: (...arg: any) => Promise<any> =
|
||||||
|
typeof props.api === 'string'
|
||||||
|
? (params: any) => {
|
||||||
|
return (requestClient as any)[props.requestMethod](
|
||||||
|
props.api as any,
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
: (props.api as (...arg: any) => Promise<any>);
|
||||||
|
const searchType = 'IN';
|
||||||
|
const params =
|
||||||
|
props.requestMethod === 'get'
|
||||||
|
? {
|
||||||
|
params: {
|
||||||
|
...props.params,
|
||||||
|
[`m_${searchType}_${props.valueField}`]: props.value,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
...props.params,
|
||||||
|
[`m_${searchType}_${props.valueField}`]: props.value,
|
||||||
|
};
|
||||||
|
api(params).then((res) => {
|
||||||
|
if (res.length > 0) {
|
||||||
|
currentData.value = res;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>{{ cValue }}</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped></style>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import ApiSelect from './api-select.vue';
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<ApiSelect />
|
||||||
|
</template>
|
||||||
67
apps/web-antd/src/components/view/components/api-dict.vue
Normal file
67
apps/web-antd/src/components/view/components/api-dict.vue
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted } from 'vue';
|
||||||
|
|
||||||
|
import { useDictStore } from '#/store/dict';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
code: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
// 值
|
||||||
|
type: [String, Number, Array],
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
split: {
|
||||||
|
// 分割符
|
||||||
|
type: String,
|
||||||
|
default: ',',
|
||||||
|
},
|
||||||
|
join: {
|
||||||
|
// 连接符
|
||||||
|
type: String,
|
||||||
|
default: ',',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const dictStore = useDictStore();
|
||||||
|
const cValue = computed(() => {
|
||||||
|
if (!props.value && props.value !== 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const arr: Array<any> = [];
|
||||||
|
if (Array.isArray(props.value)) {
|
||||||
|
arr.push(...props.value);
|
||||||
|
} else {
|
||||||
|
arr.push(...props.value.toString().split(props.split));
|
||||||
|
}
|
||||||
|
const dictData = dictStore.getDictData(props.code as string);
|
||||||
|
const res: Array<any> = [];
|
||||||
|
arr.forEach((item) => {
|
||||||
|
for (let i = 0; i < dictData.length; i++) {
|
||||||
|
if (dictData[i].value?.toString() === item?.toString()) {
|
||||||
|
res.push(dictData[i].label);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (i === dictData.length - 1) {
|
||||||
|
res.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.join(props.join);
|
||||||
|
});
|
||||||
|
onMounted(() => {
|
||||||
|
dictStore.requestData(props.code as string);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>{{ cValue }}</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped></style>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import ApiSelect from './api-select.vue';
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<ApiSelect />
|
||||||
|
</template>
|
||||||
155
apps/web-antd/src/components/view/components/api-select.vue
Normal file
155
apps/web-antd/src/components/view/components/api-select.vue
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
|
import { computed, onMounted, watch } from 'vue';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
import { useDictStore } from '#/store/dict';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
code: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
// 值
|
||||||
|
type: [Boolean, String, Number, Array],
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
split: {
|
||||||
|
// 分割符
|
||||||
|
type: String,
|
||||||
|
default: ',',
|
||||||
|
},
|
||||||
|
join: {
|
||||||
|
// 连接符
|
||||||
|
type: String,
|
||||||
|
default: ',',
|
||||||
|
},
|
||||||
|
api: {
|
||||||
|
// 接口请求对象
|
||||||
|
type: [Function, String] as PropType<
|
||||||
|
((...arg: any) => Promise<any>) | String
|
||||||
|
>,
|
||||||
|
default() {
|
||||||
|
return () => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolve([]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cacheKey: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
requestMethod: {
|
||||||
|
type: String,
|
||||||
|
default: 'post',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const dictStore = useDictStore();
|
||||||
|
/**
|
||||||
|
* 获取包含的id
|
||||||
|
*/
|
||||||
|
const getIncludeIds = () => {
|
||||||
|
if (!props.value && props.value !== 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const arr: Array<any> = [];
|
||||||
|
if (Array.isArray(props.value)) {
|
||||||
|
arr.push(...props.value);
|
||||||
|
} else {
|
||||||
|
arr.push(...props.value.toString().split(props.split));
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 获取缓存key
|
||||||
|
*/
|
||||||
|
const getCacheKey = () => {
|
||||||
|
let cacheKey = props.cacheKey;
|
||||||
|
if (typeof props.api === 'string' && !cacheKey) {
|
||||||
|
cacheKey = props.api as string;
|
||||||
|
}
|
||||||
|
return cacheKey;
|
||||||
|
};
|
||||||
|
const cValue = computed(() => {
|
||||||
|
if (!props.value && props.value !== 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const arr: Array<any> = getIncludeIds();
|
||||||
|
const cacheKey = getCacheKey();
|
||||||
|
const dictData = dictStore.getSelectData(cacheKey, {
|
||||||
|
...props.params,
|
||||||
|
includeType: 2,
|
||||||
|
includeIds: getIncludeIds(),
|
||||||
|
});
|
||||||
|
const res: Array<any> = [];
|
||||||
|
arr.forEach((item) => {
|
||||||
|
for (let i = 0; i < dictData.length; i++) {
|
||||||
|
if (dictData[i].value?.toString() === item?.toString()) {
|
||||||
|
res.push(dictData[i].label);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (i === dictData.length - 1) {
|
||||||
|
res.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.join(props.join);
|
||||||
|
});
|
||||||
|
const requestData = () => {
|
||||||
|
const api: (...arg: any) => Promise<any> =
|
||||||
|
typeof props.api === 'string'
|
||||||
|
? (params: any) => {
|
||||||
|
return (requestClient as any)[props.requestMethod](
|
||||||
|
props.api as any,
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
: (props.api as (...arg: any) => Promise<any>);
|
||||||
|
const cacheKey = getCacheKey();
|
||||||
|
const params =
|
||||||
|
props.requestMethod === 'get'
|
||||||
|
? {
|
||||||
|
params: {
|
||||||
|
...props.params,
|
||||||
|
includeType: 2,
|
||||||
|
includeIds: getIncludeIds(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
...props.params,
|
||||||
|
includeType: 2,
|
||||||
|
includeIds: getIncludeIds(),
|
||||||
|
};
|
||||||
|
dictStore.select(api, params, cacheKey);
|
||||||
|
};
|
||||||
|
onMounted(() => {
|
||||||
|
requestData();
|
||||||
|
});
|
||||||
|
watch(
|
||||||
|
() => props.value,
|
||||||
|
() => {
|
||||||
|
requestData();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>{{ cValue }}</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped></style>
|
||||||
104
apps/web-antd/src/components/view/components/api-tree-select.vue
Normal file
104
apps/web-antd/src/components/view/components/api-tree-select.vue
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
// 值
|
||||||
|
type: [String, Number, Array],
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
api: {
|
||||||
|
// 接口请求对象
|
||||||
|
type: [Function, String] as PropType<
|
||||||
|
((...arg: any) => Promise<any>) | String
|
||||||
|
>,
|
||||||
|
default() {
|
||||||
|
return () => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolve([]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cacheKey: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
requestMethod: {
|
||||||
|
type: String,
|
||||||
|
default: 'post',
|
||||||
|
},
|
||||||
|
valueField: {
|
||||||
|
type: String,
|
||||||
|
default: 'id',
|
||||||
|
},
|
||||||
|
labelField: {
|
||||||
|
type: String,
|
||||||
|
default: 'name',
|
||||||
|
},
|
||||||
|
multiple: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
// 分割符
|
||||||
|
split: {
|
||||||
|
type: String,
|
||||||
|
default: '/',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const currentData = ref([]);
|
||||||
|
const cValue = computed(() => {
|
||||||
|
return currentData.value
|
||||||
|
.map((item) => item[props.labelField])
|
||||||
|
.join(props.split);
|
||||||
|
});
|
||||||
|
onMounted(() => {
|
||||||
|
const api: (...arg: any) => Promise<any> =
|
||||||
|
typeof props.api === 'string'
|
||||||
|
? (params: any) => {
|
||||||
|
return (requestClient as any)[props.requestMethod](
|
||||||
|
props.api as any,
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
: (props.api as (...arg: any) => Promise<any>);
|
||||||
|
const searchType = props.multiple ? 'IN' : 'EQ';
|
||||||
|
const params =
|
||||||
|
props.requestMethod === 'get'
|
||||||
|
? {
|
||||||
|
params: {
|
||||||
|
...props.params,
|
||||||
|
[`m_${searchType}_${props.valueField}`]: props.value,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
...props.params,
|
||||||
|
[`m_${searchType}_${props.valueField}`]: props.value,
|
||||||
|
};
|
||||||
|
api(params).then((res) => {
|
||||||
|
if (res.length > 0) {
|
||||||
|
currentData.value = res;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>{{ cValue }}</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped></style>
|
||||||
14
apps/web-antd/src/components/view/components/avatar.vue
Normal file
14
apps/web-antd/src/components/view/components/avatar.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Image } from 'ant-design-vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Image v-if="value" :src="value" :width="102" />
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped></style>
|
||||||
22
apps/web-antd/src/components/view/components/icon-picker.vue
Normal file
22
apps/web-antd/src/components/view/components/icon-picker.vue
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '#/components/icon';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
});
|
||||||
|
defineProps({
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: '16px',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Icon v-if="value" :icon="value" :size="size" />
|
||||||
|
<span v-else></span>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped></style>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Select from './select.vue';
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Select />
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped></style>
|
||||||
35
apps/web-antd/src/components/view/components/select.vue
Normal file
35
apps/web-antd/src/components/view/components/select.vue
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
value: {
|
||||||
|
type: [String, Boolean, Number, Array],
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
join: {
|
||||||
|
// 连接符
|
||||||
|
type: String,
|
||||||
|
default: ',',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const cValue = computed(() => {
|
||||||
|
return Array.isArray(props.value)
|
||||||
|
? props.options
|
||||||
|
.filter((item: any) => (props.value as Array<any>).includes(item.value))
|
||||||
|
.map((item: any) => item.label)
|
||||||
|
.join(props.join)
|
||||||
|
: (
|
||||||
|
props.options.find(
|
||||||
|
(item: any) => item.value?.toString() === props.value?.toString(),
|
||||||
|
) as any
|
||||||
|
)?.label;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>{{ cValue }}</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped></style>
|
||||||
27
apps/web-antd/src/components/view/components/switch.vue
Normal file
27
apps/web-antd/src/components/view/components/switch.vue
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { Switch } from 'ant-design-vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
// 值
|
||||||
|
value: {
|
||||||
|
type: [String, Number, Boolean] as PropType<boolean | number | string>,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const emit = defineEmits(['update:value']);
|
||||||
|
const mValue = computed({
|
||||||
|
get() {
|
||||||
|
return props.value;
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
emit('update:value', val);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Switch v-model:checked="mValue" :disabled="true" />
|
||||||
|
</template>
|
||||||
25
apps/web-antd/src/components/view/components/upload.vue
Normal file
25
apps/web-antd/src/components/view/components/upload.vue
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
|
import { Upload } from '#/components/form';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
});
|
||||||
|
defineProps({
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
listType: {
|
||||||
|
type: String as PropType<'picture' | 'picture-card' | 'text'>,
|
||||||
|
default: 'text',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Upload :disabled="true" :list-type="listType" :value="value">
|
||||||
|
<template #default></template>
|
||||||
|
</Upload>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped></style>
|
||||||
@@ -128,9 +128,9 @@ watch(
|
|||||||
<UserDropdown
|
<UserDropdown
|
||||||
:avatar
|
:avatar
|
||||||
:menus
|
:menus
|
||||||
:text="userStore.userInfo?.realName"
|
:text="userStore.userInfo?.nick_name"
|
||||||
description="ann.vben@gmail.com"
|
:description="userStore.userInfo?.email"
|
||||||
tag-text="Pro"
|
:tag-text="userStore.userInfo?.role?.name"
|
||||||
@logout="handleLogout"
|
@logout="handleLogout"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,9 +8,22 @@ import { defineOverridesPreferences } from '@vben/preferences';
|
|||||||
export const overridesPreferences = defineOverridesPreferences({
|
export const overridesPreferences = defineOverridesPreferences({
|
||||||
// overrides
|
// overrides
|
||||||
app: {
|
app: {
|
||||||
|
// 权限模式 frontend 默认前端控制 backend 后端控制
|
||||||
|
accessMode: 'backend',
|
||||||
name: import.meta.env.VITE_APP_TITLE,
|
name: import.meta.env.VITE_APP_TITLE,
|
||||||
layout: 'sidebar-mixed-nav',
|
layout: 'sidebar-mixed-nav',
|
||||||
|
// 是否开启检查更新
|
||||||
|
enableCheckUpdates: true,
|
||||||
|
// 检查更新的时间间隔,单位为分钟
|
||||||
|
checkUpdatesInterval: 1,
|
||||||
|
enableRefreshToken: false,
|
||||||
},
|
},
|
||||||
|
// logo: {
|
||||||
|
// enable: true,
|
||||||
|
// source:
|
||||||
|
// // 'https://yanydy.oss-cn-hangzhou.aliyuncs.com/uploads/20241219/2b38e38414a2b9383b8643983e3f69d7.png',
|
||||||
|
// '/img/logo.png',
|
||||||
|
// },
|
||||||
sidebar: {
|
sidebar: {
|
||||||
width: 220,
|
width: 220,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,15 +29,15 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
params: Recordable<any>,
|
params: Recordable<any>,
|
||||||
onSuccess?: () => Promise<void> | void,
|
onSuccess?: () => Promise<void> | void,
|
||||||
) {
|
) {
|
||||||
// 异步处理用户登录操作并获取 accessToken
|
// 异步处理用户登录操作并获取 token
|
||||||
let userInfo: null | UserInfo = null;
|
let userInfo: null | UserInfo = null;
|
||||||
try {
|
try {
|
||||||
loginLoading.value = true;
|
loginLoading.value = true;
|
||||||
const { accessToken } = await loginApi(params);
|
const { token } = await loginApi(params);
|
||||||
|
|
||||||
// 如果成功获取到 accessToken
|
// 如果成功获取到 token
|
||||||
if (accessToken) {
|
if (token) {
|
||||||
accessStore.setAccessToken(accessToken);
|
accessStore.setAccessToken(token);
|
||||||
|
|
||||||
// 获取用户信息并存储到 accessStore 中
|
// 获取用户信息并存储到 accessStore 中
|
||||||
const [fetchUserInfoResult, accessCodes] = await Promise.all([
|
const [fetchUserInfoResult, accessCodes] = await Promise.all([
|
||||||
|
|||||||
12054
apps/web-antd/src/util/address.ts
Normal file
12054
apps/web-antd/src/util/address.ts
Normal file
File diff suppressed because it is too large
Load Diff
405
apps/web-antd/src/util/tool.ts
Normal file
405
apps/web-antd/src/util/tool.ts
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
import type { VbenFormProps } from '#/adapter/form';
|
||||||
|
import type { DescItem } from '#/components/description';
|
||||||
|
|
||||||
|
import { isFunction } from '@vben/utils';
|
||||||
|
|
||||||
|
export const omit = (obj: any, keysToOmit: string[]) => {
|
||||||
|
// 如果 obj 不是对象或者 keysToOmit 不是数组,则直接返回 obj
|
||||||
|
if (typeof obj !== 'object' || !Array.isArray(keysToOmit)) {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建一个新的对象,避免修改原始对象
|
||||||
|
const result = {} as any;
|
||||||
|
|
||||||
|
// 遍历 obj 的所有键值对
|
||||||
|
for (const key in obj) {
|
||||||
|
// 如果当前键不在要忽略的键列表中,则复制到新对象
|
||||||
|
if (!keysToOmit.includes(key)) {
|
||||||
|
result[key] = obj[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
export const get = (object: any, path: string) => {
|
||||||
|
// 如果 object 不是对象或者 path 不是字符串,则直接返回 defaultValue
|
||||||
|
if (
|
||||||
|
typeof object !== 'object' ||
|
||||||
|
object === null ||
|
||||||
|
typeof path !== 'string'
|
||||||
|
) {
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将路径字符串转换为数组
|
||||||
|
const pathArray = path.split('.').filter(Boolean); // 过滤掉空字符串
|
||||||
|
let current = object;
|
||||||
|
|
||||||
|
// 遍历路径数组
|
||||||
|
for (const element of pathArray) {
|
||||||
|
// 如果当前层级不是对象或没有对应的键,则返回 defaultValue
|
||||||
|
if (
|
||||||
|
typeof current !== 'object' ||
|
||||||
|
current === null ||
|
||||||
|
!(element in current)
|
||||||
|
) {
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
// 更新 current 到下一层级
|
||||||
|
current = current[element];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回最终找到的值
|
||||||
|
return current;
|
||||||
|
};
|
||||||
|
export const ifDetail = (renderCallbackParams: any): boolean => {
|
||||||
|
const schema = renderCallbackParams.schema;
|
||||||
|
if (isFunction(schema.ifDetail)) {
|
||||||
|
return schema.ifDetail(renderCallbackParams);
|
||||||
|
}
|
||||||
|
return schema.ifDetail !== false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将表单元数据转换为详情表单
|
||||||
|
* @param formOptions
|
||||||
|
* @param values
|
||||||
|
*/
|
||||||
|
export const schemaToDetailForm = (
|
||||||
|
formOptions: VbenFormProps,
|
||||||
|
values: any,
|
||||||
|
): DescItem[] => {
|
||||||
|
const group: Array<DescItem> = [];
|
||||||
|
const formSchemas = formOptions.schema;
|
||||||
|
if (!Array.isArray(formSchemas)) return group;
|
||||||
|
for (let i = 0; i < formSchemas.length; i++) {
|
||||||
|
const item = formSchemas[i];
|
||||||
|
// 找到分割线
|
||||||
|
if (item?.component === 'Divider') {
|
||||||
|
group.push({
|
||||||
|
field: item.fieldName,
|
||||||
|
label: item.label,
|
||||||
|
children: [],
|
||||||
|
} as unknown as DescItem);
|
||||||
|
// 将分割线后面的数据添加到group中
|
||||||
|
for (let j = i + 1; j < formSchemas.length; j++) {
|
||||||
|
const nextItem = formSchemas[j];
|
||||||
|
// 找到下面的分割线
|
||||||
|
if (nextItem?.component === 'Divider') {
|
||||||
|
// 中断
|
||||||
|
i = j - 1;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
ifDetail({
|
||||||
|
schema: nextItem,
|
||||||
|
values,
|
||||||
|
model: values,
|
||||||
|
field: item.fieldName,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
group[group.length - 1]?.children?.push({
|
||||||
|
field: nextItem?.fieldName,
|
||||||
|
label: nextItem?.label,
|
||||||
|
span: nextItem?.detailSpan || 12,
|
||||||
|
component: nextItem?.component,
|
||||||
|
componentProps: nextItem?.componentProps,
|
||||||
|
} as DescItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (group.length > 0) {
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'baseinfo',
|
||||||
|
label: '基本信息',
|
||||||
|
children: formSchemas
|
||||||
|
.filter((item) => {
|
||||||
|
return ifDetail({
|
||||||
|
schema: item,
|
||||||
|
values,
|
||||||
|
model: values,
|
||||||
|
field: item.fieldName,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.map((item) => {
|
||||||
|
return {
|
||||||
|
field: item.fieldName,
|
||||||
|
label: item.label,
|
||||||
|
span: item.detailSpan || 12,
|
||||||
|
component: item.component,
|
||||||
|
componentProps: item.componentProps,
|
||||||
|
} as DescItem;
|
||||||
|
}),
|
||||||
|
} as DescItem,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取子节点id
|
||||||
|
export const getChildIds = (record: any) => {
|
||||||
|
const ids = [record.id];
|
||||||
|
record.children?.forEach((item: any) => {
|
||||||
|
ids.push(...getChildIds(item));
|
||||||
|
});
|
||||||
|
return ids;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有叶子节点
|
||||||
|
* @param treeData
|
||||||
|
*/
|
||||||
|
export const getLeafNodeIds = (treeData: any) => {
|
||||||
|
const leafNodeIds: any = [];
|
||||||
|
|
||||||
|
function traverse(node: any) {
|
||||||
|
// 如果当前节点没有 children 或 children 是空数组,则它是叶子节点
|
||||||
|
if (!node.children || node.children.length === 0) {
|
||||||
|
leafNodeIds.push(node.id);
|
||||||
|
} else {
|
||||||
|
// 否则,递归遍历每个子节点
|
||||||
|
for (const child of node.children) {
|
||||||
|
traverse(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 遍历树的根节点
|
||||||
|
for (const root of treeData) {
|
||||||
|
traverse(root);
|
||||||
|
}
|
||||||
|
return leafNodeIds;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有节点id
|
||||||
|
* @param treeData
|
||||||
|
*/
|
||||||
|
export const getAllNodeIds = (treeData: any) => {
|
||||||
|
const allNodeIds: any = [];
|
||||||
|
|
||||||
|
function traverse(node: any) {
|
||||||
|
// 收集当前节点的 id
|
||||||
|
allNodeIds.push(node.id);
|
||||||
|
|
||||||
|
// 如果当前节点有 children,则递归遍历每个子节点
|
||||||
|
if (node.children && node.children.length > 0) {
|
||||||
|
for (const child of node.children) {
|
||||||
|
traverse(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 遍历树的根节点
|
||||||
|
for (const root of treeData) {
|
||||||
|
traverse(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
return allNodeIds;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* -转大驼峰
|
||||||
|
* @param str
|
||||||
|
*/
|
||||||
|
export const toPascalCase = (str: any) => {
|
||||||
|
// 将连字符或下划线替换为空格,以便后续处理
|
||||||
|
const words = str.replaceAll(/[-_]/g, ' ').split(' ');
|
||||||
|
|
||||||
|
// 将每个单词的首字母大写,并将其余部分保持原样
|
||||||
|
const pascalCaseWords = words.map((word: any) => {
|
||||||
|
if (word) {
|
||||||
|
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
||||||
|
}
|
||||||
|
return word;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 将处理后的单词拼接成一个新的字符串
|
||||||
|
return pascalCaseWords.join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转-kebab
|
||||||
|
*/
|
||||||
|
export const toKebabCase = (str: string) => {
|
||||||
|
// 将每个单词的首字母小写,并在单词之间插入连字符
|
||||||
|
return str
|
||||||
|
.replaceAll(/([A-Z])/g, (match, _p1, offset) => {
|
||||||
|
// 如果是第一个字符,直接转为小写
|
||||||
|
if (offset === 0) {
|
||||||
|
return match.toLowerCase();
|
||||||
|
}
|
||||||
|
// 否则,在大写字母前加上连字符,并将大写字母转为小写
|
||||||
|
return `-${match.toLowerCase()}`;
|
||||||
|
})
|
||||||
|
.toLowerCase(); // 确保整个字符串都是小写
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getFileNameWithoutExtension(path: string) {
|
||||||
|
// 使用正则表达式匹配最后一个 / 后面的所有字符,但不包括最后一个点(.)之后的内容
|
||||||
|
const match = path.match(/[^/]+(?=\.[^./]+$|$)/);
|
||||||
|
return match ? match[0].replace(/\.[^/.]+$/, '') : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据脱敏
|
||||||
|
* @param data
|
||||||
|
* @param type
|
||||||
|
*/
|
||||||
|
export function desensitize(data: string, type: 'bankCard' | 'idCard'): string {
|
||||||
|
if (!data) return '';
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'bankCard': {
|
||||||
|
// 银行卡号脱敏规则:显示前6位和后4位,中间用*代替
|
||||||
|
if (data.length < 12) {
|
||||||
|
throw new Error('Invalid bank card number length');
|
||||||
|
}
|
||||||
|
return data.slice(0, 6) + '*'.repeat(data.length - 10) + data.slice(-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'idCard': {
|
||||||
|
// 身份证号脱敏规则:显示前6位和后4位,中间用*代替
|
||||||
|
if (data.length !== 18) {
|
||||||
|
throw new Error('Invalid ID card number length');
|
||||||
|
}
|
||||||
|
return data.slice(0, 6) + '*'.repeat(10) + data.slice(-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
throw new Error('Unsupported desensitization type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取操作系统的Icon
|
||||||
|
* @param str
|
||||||
|
*/
|
||||||
|
export function getIcon(str: string) {
|
||||||
|
switch (str) {
|
||||||
|
case 'Api Post': {
|
||||||
|
return 'logos:async-api-icon';
|
||||||
|
}
|
||||||
|
case 'Chrome': {
|
||||||
|
return 'devicon:chrome';
|
||||||
|
}
|
||||||
|
case 'Edge':
|
||||||
|
case 'Microsoft Edge': {
|
||||||
|
return 'logos:microsoft-edge';
|
||||||
|
}
|
||||||
|
case 'Firefox': {
|
||||||
|
return 'devicon:firefox';
|
||||||
|
}
|
||||||
|
case 'Linux': {
|
||||||
|
return 'logos:linux-tux';
|
||||||
|
}
|
||||||
|
case 'Mac': {
|
||||||
|
return 'wpf:mac-os';
|
||||||
|
}
|
||||||
|
case 'Safari': {
|
||||||
|
return 'devicon:safari';
|
||||||
|
}
|
||||||
|
case 'Windows': {
|
||||||
|
return 'logos:microsoft-windows-icon';
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return 'logos:microsoft-windows-icon';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取接诊状态
|
||||||
|
* @param status
|
||||||
|
*/
|
||||||
|
export function getRegisterStatus(status: number) {
|
||||||
|
switch (status) {
|
||||||
|
case 0: {
|
||||||
|
return '待支付';
|
||||||
|
}
|
||||||
|
case 1: {
|
||||||
|
return '待接诊';
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
return '接诊中';
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
return '已结束';
|
||||||
|
}
|
||||||
|
case 4: {
|
||||||
|
return '已取消';
|
||||||
|
}
|
||||||
|
case 5: {
|
||||||
|
return '待评价';
|
||||||
|
}
|
||||||
|
case 6: {
|
||||||
|
return '已评价';
|
||||||
|
}
|
||||||
|
case 7: {
|
||||||
|
return '已拒诊';
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return '未知状态';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 把格式化时间转换成相对时间(刚刚,几分钟前,几小时前,几天前,几周前)
|
||||||
|
export function formatTimeToRelative(time: string) {
|
||||||
|
const now = new Date();
|
||||||
|
const date = new Date(time);
|
||||||
|
const diff = now.getTime() - date.getTime();
|
||||||
|
const seconds = Math.floor(diff / 1000);
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
const weeks = Math.floor(days / 7);
|
||||||
|
|
||||||
|
if (seconds < 60) {
|
||||||
|
return '刚刚';
|
||||||
|
} else if (minutes < 60) {
|
||||||
|
return `${minutes} 分钟前`;
|
||||||
|
} else if (hours < 24) {
|
||||||
|
return `${hours} 小时前`;
|
||||||
|
} else if (days < 7) {
|
||||||
|
return `${days} 天前`;
|
||||||
|
} else if (weeks < 4) {
|
||||||
|
return `${weeks} 周前`;
|
||||||
|
}
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download according to the background interface file stream
|
||||||
|
* @param {*} data
|
||||||
|
* @param {*} filename
|
||||||
|
* @param {*} mime
|
||||||
|
* @param {*} bom
|
||||||
|
*/
|
||||||
|
export function downloadByData(
|
||||||
|
data: BlobPart,
|
||||||
|
filename: string,
|
||||||
|
mime?: string,
|
||||||
|
bom?: BlobPart,
|
||||||
|
) {
|
||||||
|
const blobData = bom === undefined ? [data] : [bom, data];
|
||||||
|
const blob = new Blob(blobData, { type: mime || 'application/octet-stream' });
|
||||||
|
|
||||||
|
const blobURL = window.URL.createObjectURL(blob);
|
||||||
|
const tempLink = document.createElement('a');
|
||||||
|
tempLink.style.display = 'none';
|
||||||
|
tempLink.href = blobURL;
|
||||||
|
tempLink.setAttribute('download', filename);
|
||||||
|
if (tempLink.download === undefined) {
|
||||||
|
tempLink.setAttribute('target', '_blank');
|
||||||
|
}
|
||||||
|
document.body.append(tempLink);
|
||||||
|
tempLink.click();
|
||||||
|
tempLink.remove();
|
||||||
|
window.URL.revokeObjectURL(blobURL);
|
||||||
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { VbenFormSchema } from '@vben/common-ui';
|
import type { VbenFormSchema } from '@vben/common-ui';
|
||||||
import type { BasicOption } from '@vben/types';
|
|
||||||
|
|
||||||
import { computed, markRaw } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui';
|
import { AuthenticationLogin, z } from '@vben/common-ui';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
import { useAuthStore } from '#/store';
|
import { useAuthStore } from '#/store';
|
||||||
@@ -13,37 +12,22 @@ defineOptions({ name: 'Login' });
|
|||||||
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
const MOCK_USER_OPTIONS: BasicOption[] = [
|
|
||||||
{
|
|
||||||
label: 'Super',
|
|
||||||
value: 'vben',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Admin',
|
|
||||||
value: 'admin',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'User',
|
|
||||||
value: 'jack',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const formSchema = computed((): VbenFormSchema[] => {
|
const formSchema = computed((): VbenFormSchema[] => {
|
||||||
return [
|
return [
|
||||||
{
|
// {
|
||||||
component: 'VbenSelect',
|
// component: 'VbenSelect',
|
||||||
componentProps: {
|
// componentProps: {
|
||||||
options: MOCK_USER_OPTIONS,
|
// options: MOCK_USER_OPTIONS,
|
||||||
placeholder: $t('authentication.selectAccount'),
|
// placeholder: $t('authentication.selectAccount'),
|
||||||
},
|
// },
|
||||||
fieldName: 'selectAccount',
|
// fieldName: 'selectAccount',
|
||||||
label: $t('authentication.selectAccount'),
|
// label: $t('authentication.selectAccount'),
|
||||||
rules: z
|
// rules: z
|
||||||
.string()
|
// .string()
|
||||||
.min(1, { message: $t('authentication.selectAccount') })
|
// .min(1, { message: $t('authentication.selectAccount') })
|
||||||
.optional()
|
// .optional()
|
||||||
.default('vben'),
|
// .default('vben'),
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
component: 'VbenInput',
|
component: 'VbenInput',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
@@ -51,21 +35,14 @@ const formSchema = computed((): VbenFormSchema[] => {
|
|||||||
},
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
trigger(values, form) {
|
trigger(values, form) {
|
||||||
if (values.selectAccount) {
|
form.setValues({
|
||||||
const findUser = MOCK_USER_OPTIONS.find(
|
password: 'qiqi991012',
|
||||||
(item) => item.value === values.selectAccount,
|
account: '15100000000',
|
||||||
);
|
});
|
||||||
if (findUser) {
|
|
||||||
form.setValues({
|
|
||||||
password: '123456',
|
|
||||||
username: findUser.value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
triggerFields: ['selectAccount'],
|
triggerFields: ['selectAccount'],
|
||||||
},
|
},
|
||||||
fieldName: 'username',
|
fieldName: 'account',
|
||||||
label: $t('authentication.username'),
|
label: $t('authentication.username'),
|
||||||
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
|
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
|
||||||
},
|
},
|
||||||
@@ -78,13 +55,13 @@ const formSchema = computed((): VbenFormSchema[] => {
|
|||||||
label: $t('authentication.password'),
|
label: $t('authentication.password'),
|
||||||
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
|
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
component: markRaw(SliderCaptcha),
|
// component: markRaw(SliderCaptcha),
|
||||||
fieldName: 'captcha',
|
// fieldName: 'captcha',
|
||||||
rules: z.boolean().refine((value) => value, {
|
// rules: z.boolean().refine((value) => value, {
|
||||||
message: $t('authentication.verifyRequiredTip'),
|
// message: $t('authentication.verifyRequiredTip'),
|
||||||
}),
|
// }),
|
||||||
},
|
// },
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -93,6 +70,12 @@ const formSchema = computed((): VbenFormSchema[] => {
|
|||||||
<AuthenticationLogin
|
<AuthenticationLogin
|
||||||
:form-schema="formSchema"
|
:form-schema="formSchema"
|
||||||
:loading="authStore.loginLoading"
|
:loading="authStore.loginLoading"
|
||||||
|
:show-code-login="false"
|
||||||
|
:show-forget-password="true"
|
||||||
|
:show-qrcode-login="false"
|
||||||
|
:show-register="false"
|
||||||
|
:show-remember-me="false"
|
||||||
|
:show-third-party-login="true"
|
||||||
@submit="authStore.authLogin"
|
@submit="authStore.authLogin"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
89
apps/web-antd/src/views/system/admin/api/index.ts
Normal file
89
apps/web-antd/src/views/system/admin/api/index.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
const prefix = 'admin/';
|
||||||
|
/**
|
||||||
|
* 分页查询用户列表
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export async function getAdminList(data: any) {
|
||||||
|
return requestClient.get<any>(`${prefix}list`, { params: data });
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 分页查询用户列表
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export async function getAdminAccountBalance(data: any = {}) {
|
||||||
|
return requestClient.get<any>(`${prefix}my-balance`, { params: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取管理员详情
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
export async function getAdminInfo(id: number) {
|
||||||
|
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增管理员
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export async function createAdmin(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}create`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑管理员
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export async function updateAdmin(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}update`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除管理员
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export async function deleteAdmin(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}delete`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改密码
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export async function updatePassword(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}update-password`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除管理员
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
export async function resetPassword(id: number) {
|
||||||
|
return requestClient.post<any>(`${prefix}reset-password`, {
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取我绑定的银行卡
|
||||||
|
*/
|
||||||
|
export async function getMyCard() {
|
||||||
|
return requestClient.get<any>(`${prefix}my-card`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取我绑定的银行卡
|
||||||
|
*/
|
||||||
|
export async function getQuickMenuApi() {
|
||||||
|
return requestClient.get<any>(`${prefix}get-quick-menu`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑账户绑定银行卡
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export async function saveCard(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}save-card`, data);
|
||||||
|
}
|
||||||
65
apps/web-antd/src/views/system/admin/components/modal.vue
Normal file
65
apps/web-antd/src/views/system/admin/components/modal.vue
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { createAdmin, updateAdmin } from '#/views/system/admin/api';
|
||||||
|
import { modalFormProps } from '#/views/system/admin/config/form';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'FormModelDemo',
|
||||||
|
});
|
||||||
|
|
||||||
|
const isUpdate = ref(false);
|
||||||
|
const gridApi = ref();
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm(modalFormProps);
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
fullscreenButton: false,
|
||||||
|
draggable: true,
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onConfirm: async () => {
|
||||||
|
formApi.validate().then(async (e: any) => {
|
||||||
|
if (e.valid) {
|
||||||
|
const values = await formApi.getValues();
|
||||||
|
modalApi.setState({ loading: true, confirmLoading: true });
|
||||||
|
const submitApi = isUpdate.value ? updateAdmin : createAdmin;
|
||||||
|
submitApi(values)
|
||||||
|
.then(() => {
|
||||||
|
message.success('保存成功');
|
||||||
|
gridApi.value?.reload();
|
||||||
|
modalApi.close();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
modalApi.setState({ loading: false, confirmLoading: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await formApi.validateAndSubmitForm();
|
||||||
|
},
|
||||||
|
onOpenChange(isOpen: boolean) {
|
||||||
|
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||||
|
if (isOpen) {
|
||||||
|
const { values, update } = modalApi.getData<Record<string, any>>();
|
||||||
|
if (values) {
|
||||||
|
isUpdate.value = update;
|
||||||
|
formApi.setValues(values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
:title="`${isUpdate === true ? '编辑' : '新增'}管理员`"
|
||||||
|
class="w-[60%]"
|
||||||
|
>
|
||||||
|
<Form />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
150
apps/web-antd/src/views/system/admin/config/form.ts
Normal file
150
apps/web-antd/src/views/system/admin/config/form.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import type { VbenFormProps } from '#/adapter/form';
|
||||||
|
|
||||||
|
import { z } from '#/adapter/form';
|
||||||
|
import { getRoleOption } from '#/views/system/role/api';
|
||||||
|
|
||||||
|
const defaultPassword = 'Xk123456@';
|
||||||
|
|
||||||
|
export const modalFormProps: VbenFormProps = {
|
||||||
|
wrapperClass: 'grid-cols-12', // 24栅格,
|
||||||
|
commonConfig: {
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
// 所有表单项
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// handleSubmit: onSubmit,
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
fieldName: 'id',
|
||||||
|
label: 'ID',
|
||||||
|
formItemClass: 'col-span-6',
|
||||||
|
dependencies: {
|
||||||
|
show: false,
|
||||||
|
triggerFields: ['id'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入管理员昵称',
|
||||||
|
},
|
||||||
|
fieldName: 'nick_name',
|
||||||
|
label: '昵称',
|
||||||
|
rules: 'required',
|
||||||
|
formItemClass: 'col-span-6',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Avatar',
|
||||||
|
fieldName: 'avatar',
|
||||||
|
label: '头像',
|
||||||
|
rules: 'required',
|
||||||
|
formItemClass: 'col-span-6',
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// component: 'UploadImage',
|
||||||
|
// fieldName: 'images',
|
||||||
|
// label: '其他图片',
|
||||||
|
// rules: 'required',
|
||||||
|
// formItemClass: 'col-span-6',
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入管理员手机号码',
|
||||||
|
},
|
||||||
|
fieldName: 'phone',
|
||||||
|
label: '手机号',
|
||||||
|
rules: 'required',
|
||||||
|
formItemClass: 'col-span-6',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
filterOption: (input: string, option: any) => {
|
||||||
|
// 自定义过滤逻辑,确保可以根据 name 进行搜索
|
||||||
|
return option?.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||||
|
},
|
||||||
|
showSearch: true,
|
||||||
|
// 菜单接口转options格式
|
||||||
|
afterFetch: (data: { id: number; name: string }[]) => {
|
||||||
|
return data.map((item: any) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.id,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
api: getRoleOption,
|
||||||
|
placeholder: '请选择',
|
||||||
|
},
|
||||||
|
fieldName: 'role_id',
|
||||||
|
formItemClass: 'col-span-6',
|
||||||
|
label: '角色',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'password',
|
||||||
|
label: '密码',
|
||||||
|
component: 'InputPassword',
|
||||||
|
help: '5-18位数字、字母、特殊字符组成。',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入密码',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
defaultValue: defaultPassword,
|
||||||
|
rules: z
|
||||||
|
.string()
|
||||||
|
.regex(
|
||||||
|
/^(?=.*[A-Z0-9])(?=.*[!@#$%^&*])[A-Z0-9!@#$%^&*]{5,18}$/i,
|
||||||
|
'密码由5-18位数字、字母、特殊字符组成。',
|
||||||
|
),
|
||||||
|
dependencies: {
|
||||||
|
if({ id }) {
|
||||||
|
return !id;
|
||||||
|
},
|
||||||
|
triggerFields: ['id'],
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-6',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'confirmPassword',
|
||||||
|
label: '确认密码',
|
||||||
|
component: 'InputPassword',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入确认密码',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
defaultValue: defaultPassword,
|
||||||
|
rules: z
|
||||||
|
.string()
|
||||||
|
.regex(/[\w!@#$%^&*]{5,18}/, '密码由5-18位数字、字母、特殊字符组成。'),
|
||||||
|
dependencies: {
|
||||||
|
if({ id }) {
|
||||||
|
return !id;
|
||||||
|
},
|
||||||
|
triggerFields: ['id', 'confirmPassword'],
|
||||||
|
rules: (values) => {
|
||||||
|
return z
|
||||||
|
.string()
|
||||||
|
.regex(
|
||||||
|
/[\w!@#$%^&*]{5,18}/,
|
||||||
|
'密码由5-18位数字、字母、特殊字符组成。',
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(confirmPassword) => {
|
||||||
|
return confirmPassword === values.password;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: '确认密码必须与密码一致',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-6',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
showDefaultActions: false,
|
||||||
|
};
|
||||||
56
apps/web-antd/src/views/system/admin/config/search.ts
Normal file
56
apps/web-antd/src/views/system/admin/config/search.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import type { VbenFormProps } from '#/adapter/form';
|
||||||
|
|
||||||
|
import { getRoleOption } from '#/views/system/role/api';
|
||||||
|
|
||||||
|
export const formOptions: VbenFormProps = {
|
||||||
|
// 默认展开
|
||||||
|
collapsed: false,
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '输入名称',
|
||||||
|
},
|
||||||
|
defaultValue: '',
|
||||||
|
fieldName: 'nick_name',
|
||||||
|
label: '管理员名称',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '输入手机号码',
|
||||||
|
},
|
||||||
|
defaultValue: '',
|
||||||
|
fieldName: 'phone',
|
||||||
|
label: '手机号码',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
allowClear: true,
|
||||||
|
filterOption: true,
|
||||||
|
showSearch: true,
|
||||||
|
// 菜单接口转options格式
|
||||||
|
afterFetch: (data: { id: number; name: string }[]) => {
|
||||||
|
return data.map((item: any) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.id,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
api: getRoleOption,
|
||||||
|
placeholder: '请选择',
|
||||||
|
},
|
||||||
|
fieldName: 'role_id',
|
||||||
|
label: '角色',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// 控制表单是否显示折叠按钮
|
||||||
|
showCollapseButton: true,
|
||||||
|
submitButtonOptions: {
|
||||||
|
content: '查询',
|
||||||
|
},
|
||||||
|
// 是否在字段值改变时提交表单
|
||||||
|
submitOnChange: true,
|
||||||
|
// 按下回车时是否提交表单
|
||||||
|
submitOnEnter: false,
|
||||||
|
};
|
||||||
83
apps/web-antd/src/views/system/admin/config/table.ts
Normal file
83
apps/web-antd/src/views/system/admin/config/table.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { getAdminList } from '#/views/system/admin/api';
|
||||||
|
|
||||||
|
interface RowType {
|
||||||
|
id: string;
|
||||||
|
nick_name: string;
|
||||||
|
email: string;
|
||||||
|
avatar: string;
|
||||||
|
roles: { name: string }[];
|
||||||
|
open_id: string;
|
||||||
|
code: string;
|
||||||
|
platform_id: string;
|
||||||
|
phone: string;
|
||||||
|
desc: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const gridOptions: VxeGridProps<RowType> = {
|
||||||
|
checkboxConfig: {
|
||||||
|
highlight: true,
|
||||||
|
labelField: '',
|
||||||
|
},
|
||||||
|
columnConfig: {
|
||||||
|
useKey: true,
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
useKey: true,
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{ type: 'checkbox', width: 60 },
|
||||||
|
{ field: 'id', align: 'left', title: 'ID', width: 100 },
|
||||||
|
{ field: 'nick_name', align: 'left', title: '名称' },
|
||||||
|
{
|
||||||
|
field: 'avatar',
|
||||||
|
align: 'left',
|
||||||
|
title: '头像',
|
||||||
|
slots: { default: 'avatar' },
|
||||||
|
width: 130,
|
||||||
|
},
|
||||||
|
{ field: 'role.name', title: '角色' },
|
||||||
|
{ field: 'open_id', title: 'Open ID' },
|
||||||
|
{ field: 'phone', title: '手机号码' },
|
||||||
|
{ field: 'email', title: '邮箱' },
|
||||||
|
{ field: 'desc', title: '备注' },
|
||||||
|
{ field: 'created_at', title: '注册时间' },
|
||||||
|
{ type: 'html', title: '操作', width: 200, slots: { default: 'action' } },
|
||||||
|
],
|
||||||
|
keepSource: true,
|
||||||
|
pagerConfig: {},
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
// 请求后端接口方法
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getAdminList({
|
||||||
|
page: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
height: 'auto',
|
||||||
|
border: false,
|
||||||
|
toolbarConfig: {
|
||||||
|
// 是否显示搜索表单控制按钮
|
||||||
|
// @ts-ignore 正式环境时有完整的类型声明
|
||||||
|
search: true,
|
||||||
|
refresh: true, // 刷新
|
||||||
|
print: false, // 打印
|
||||||
|
export: false, // 导出
|
||||||
|
// custom: true, // 自定义列
|
||||||
|
zoom: true, // 最大化最小化
|
||||||
|
slots: {
|
||||||
|
buttons: 'toolbar-buttons',
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
// 自定义列-图标
|
||||||
|
icon: 'vxe-icon-menu',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
showOverflow: false,
|
||||||
|
};
|
||||||
151
apps/web-antd/src/views/system/admin/index.vue
Normal file
151
apps/web-antd/src/views/system/admin/index.vue
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeGridListeners } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Button, Image, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { TableAction } from '#/components/table-action';
|
||||||
|
|
||||||
|
import { deleteAdmin, resetPassword } from './api';
|
||||||
|
import FormModalDemo from './components/modal.vue';
|
||||||
|
import { formOptions } from './config/search';
|
||||||
|
import { gridOptions } from './config/table';
|
||||||
|
|
||||||
|
const hasTopTableDropDownActions = ref(false);
|
||||||
|
|
||||||
|
const gridEvents: VxeGridListeners<any> = {
|
||||||
|
checkboxChange() {
|
||||||
|
const records = gridApi.grid.getCheckboxRecords();
|
||||||
|
hasTopTableDropDownActions.value = records.length > 0;
|
||||||
|
},
|
||||||
|
checkboxAll() {
|
||||||
|
const records = gridApi.grid.getCheckboxRecords();
|
||||||
|
hasTopTableDropDownActions.value = records.length > 0;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions,
|
||||||
|
gridOptions,
|
||||||
|
gridEvents,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: FormModalDemo,
|
||||||
|
});
|
||||||
|
|
||||||
|
const showModal = (data = {}, isUpdate = false) => {
|
||||||
|
formModalApi.setData({
|
||||||
|
// 表单值
|
||||||
|
values: data,
|
||||||
|
update: isUpdate,
|
||||||
|
gridApi,
|
||||||
|
});
|
||||||
|
formModalApi.open();
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteApi = (row: any) => {
|
||||||
|
let ids = [];
|
||||||
|
if (row) {
|
||||||
|
ids.push(row);
|
||||||
|
} else {
|
||||||
|
ids = gridApi.grid.getCheckboxRecords().map((item) => item.id);
|
||||||
|
}
|
||||||
|
deleteAdmin({ ids }).then(() => {
|
||||||
|
message.success('删除成功!');
|
||||||
|
gridApi.reload();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetPasswordApi = (id: number) => {
|
||||||
|
resetPassword(id).then(() => {
|
||||||
|
message.success('删除成功!');
|
||||||
|
gridApi.reload();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height title="管理员管理">
|
||||||
|
<FormModal />
|
||||||
|
<Grid>
|
||||||
|
<template #toolbar-buttons>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '新增',
|
||||||
|
type: 'primary',
|
||||||
|
icon: 'ant-design:plus-outlined',
|
||||||
|
// auth: ['超级管理员', 'sys:user:save'],
|
||||||
|
onClick: showModal.bind(null),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:drop-down-actions="[
|
||||||
|
{
|
||||||
|
label: '删除',
|
||||||
|
icon: 'ant-design:delete-outlined',
|
||||||
|
ifShow: hasTopTableDropDownActions,
|
||||||
|
// auth: ['超级管理员', 'sys:user:save'],
|
||||||
|
popConfirm: {
|
||||||
|
title: '确定删除吗?',
|
||||||
|
confirm: deleteApi.bind(null, false),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<template #more>
|
||||||
|
<Button style="margin-left: 16px">
|
||||||
|
批量操作
|
||||||
|
<Icon icon="ant-design:down-outlined" />
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</TableAction>
|
||||||
|
</template>
|
||||||
|
<template #avatar="{ row }">
|
||||||
|
<Image :src="row.avatar" height="30" width="30" />
|
||||||
|
</template>
|
||||||
|
<template #toolbar-tools></template>
|
||||||
|
<template #action="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '编辑',
|
||||||
|
type: 'link',
|
||||||
|
icon: 'uil:edit',
|
||||||
|
size: 'small',
|
||||||
|
// auth: ['admin', 'sys:role:detail'],
|
||||||
|
onClick: showModal.bind(null, row, true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '删除',
|
||||||
|
type: 'link',
|
||||||
|
icon: 'ant-design:delete-outlined',
|
||||||
|
size: 'small',
|
||||||
|
// auth: ['admin', 'sys:role:detail'],
|
||||||
|
popConfirm: {
|
||||||
|
title: '确定删除吗?',
|
||||||
|
confirm: deleteApi.bind(null, row.id),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:drop-down-actions="[
|
||||||
|
{
|
||||||
|
label: '重置密码',
|
||||||
|
type: 'link',
|
||||||
|
icon: 'bitcoin-icons:refresh-filled',
|
||||||
|
size: 'small',
|
||||||
|
popConfirm: {
|
||||||
|
title: '确定重置密码吗?',
|
||||||
|
confirm: resetPasswordApi.bind(null, row.id),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
75
apps/web-antd/src/views/system/menu/api/index.ts
Normal file
75
apps/web-antd/src/views/system/menu/api/index.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
const prefix = 'menu/';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询用户列表
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export async function getMenuList(data: any) {
|
||||||
|
return requestClient.get<any>(`${prefix}list`, { params: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询菜单下拉框
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export async function getMenuOption(data: any) {
|
||||||
|
return requestClient.get<any>(`${prefix}option`, {
|
||||||
|
params: {
|
||||||
|
...data,
|
||||||
|
is_select: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询菜单树形下拉框
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export async function getMenuTreeOption(data: any) {
|
||||||
|
return requestClient.get<any>(`${prefix}get-tree-option`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询菜单树形下拉框
|
||||||
|
*/
|
||||||
|
export async function getMenuTreeOptionSelect() {
|
||||||
|
return requestClient.get<any>(`${prefix}get-tree-option`, {
|
||||||
|
params: {
|
||||||
|
is_select: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取详情
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
export async function getMenuInfo(id: number) {
|
||||||
|
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增角色
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export async function createMenu(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}create`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑角色
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export async function updateMenu(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}update`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除角色
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export async function deleteMenu(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}delete`, data);
|
||||||
|
}
|
||||||
63
apps/web-antd/src/views/system/menu/components/modal.vue
Normal file
63
apps/web-antd/src/views/system/menu/components/modal.vue
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
|
||||||
|
import { createMenu, updateMenu } from '../api';
|
||||||
|
import { modalFormProps } from '../config/form';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'FormModelDemo',
|
||||||
|
});
|
||||||
|
|
||||||
|
const isUpdate = ref(false);
|
||||||
|
const gridApi = ref();
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm(modalFormProps);
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
fullscreenButton: false,
|
||||||
|
draggable: true,
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onConfirm: async () => {
|
||||||
|
formApi.validate().then(async (e: any) => {
|
||||||
|
if (e.valid) {
|
||||||
|
const values = await formApi.getValues();
|
||||||
|
modalApi.setState({ loading: true, confirmLoading: true });
|
||||||
|
const submitApi = isUpdate.value ? updateMenu : createMenu;
|
||||||
|
submitApi(values)
|
||||||
|
.then(() => {
|
||||||
|
message.success('保存成功');
|
||||||
|
gridApi.value?.reload();
|
||||||
|
modalApi.close();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
modalApi.setState({ loading: false, confirmLoading: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await formApi.validateAndSubmitForm();
|
||||||
|
},
|
||||||
|
onOpenChange(isOpen: boolean) {
|
||||||
|
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||||
|
if (isOpen) {
|
||||||
|
const { values, update } = modalApi.getData<Record<string, any>>();
|
||||||
|
if (values) {
|
||||||
|
isUpdate.value = update;
|
||||||
|
formApi.setValues(values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Modal :title="`${isUpdate === true ? '编辑' : '新增'}菜单`" class="w-[60%]">
|
||||||
|
<Form />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
180
apps/web-antd/src/views/system/menu/config/form.ts
Normal file
180
apps/web-antd/src/views/system/menu/config/form.ts
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import type { VbenFormProps } from '#/adapter/form';
|
||||||
|
|
||||||
|
import { getMenuTreeOptionSelect } from '#/views/system/menu/api';
|
||||||
|
|
||||||
|
export const modalFormProps: VbenFormProps = {
|
||||||
|
wrapperClass: 'grid-cols-12', // 24栅格,
|
||||||
|
commonConfig: {
|
||||||
|
formItemClass: 'col-span-6',
|
||||||
|
// 所有表单项
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// handleSubmit: onSubmit,
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
fieldName: 'id',
|
||||||
|
label: 'ID',
|
||||||
|
dependencies: {
|
||||||
|
show: false,
|
||||||
|
triggerFields: ['id'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入菜单标题',
|
||||||
|
},
|
||||||
|
fieldName: 'title',
|
||||||
|
label: '菜单标题',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'ApiTreeSelect',
|
||||||
|
// 对应组件的参数
|
||||||
|
componentProps: {
|
||||||
|
childrenField: 'children',
|
||||||
|
labelField: 'title',
|
||||||
|
valueField: 'id',
|
||||||
|
// 菜单接口
|
||||||
|
api: getMenuTreeOptionSelect,
|
||||||
|
},
|
||||||
|
defaultValue: 0,
|
||||||
|
fieldName: 'pid',
|
||||||
|
label: '父级菜单',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入菜单图标',
|
||||||
|
},
|
||||||
|
fieldName: 'icon',
|
||||||
|
label: '菜单图标',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// component: 'IconPicker',
|
||||||
|
// componentProps: {
|
||||||
|
// placeholder: '请输入菜单图标',
|
||||||
|
// },
|
||||||
|
// fieldName: 'icon',
|
||||||
|
// label: '菜单图标',
|
||||||
|
// rules: 'required',
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入路由名称',
|
||||||
|
},
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '路由名称',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入访问路由',
|
||||||
|
},
|
||||||
|
fieldName: 'path',
|
||||||
|
label: '访问路由',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'InputNumber',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入排序',
|
||||||
|
},
|
||||||
|
defaultValue: 0,
|
||||||
|
fieldName: 'sort',
|
||||||
|
label: '排序',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: '开启',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '关闭',
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
defaultValue: 1,
|
||||||
|
fieldName: 'keep_alive',
|
||||||
|
label: '缓存',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: '展示',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '隐藏',
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
defaultValue: 0,
|
||||||
|
fieldName: 'hide_in_menu',
|
||||||
|
label: '是否展示',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: '是',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '否',
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
defaultValue: 1,
|
||||||
|
fieldName: 'affix_tab',
|
||||||
|
label: '是否置顶',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入组件地址',
|
||||||
|
},
|
||||||
|
defaultValue: 'BasicLayout',
|
||||||
|
fieldName: 'component',
|
||||||
|
label: '组件地址',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入重定向地址',
|
||||||
|
},
|
||||||
|
fieldName: 'redirect',
|
||||||
|
label: '重定向地址',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入携带参数',
|
||||||
|
},
|
||||||
|
fieldName: 'query',
|
||||||
|
label: '携带参数',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
showDefaultActions: false,
|
||||||
|
};
|
||||||
47
apps/web-antd/src/views/system/menu/config/search.ts
Normal file
47
apps/web-antd/src/views/system/menu/config/search.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import type { VbenFormProps } from '#/adapter/form';
|
||||||
|
|
||||||
|
export const formOptions: VbenFormProps = {
|
||||||
|
// 默认展开
|
||||||
|
collapsed: false,
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '输入标题',
|
||||||
|
},
|
||||||
|
defaultValue: '',
|
||||||
|
fieldName: 'title',
|
||||||
|
label: '菜单标题',
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// component: 'VbenSelect',
|
||||||
|
// componentProps: {
|
||||||
|
// allowClear: true,
|
||||||
|
// filterOption: true,
|
||||||
|
// showSearch: true,
|
||||||
|
// options: [
|
||||||
|
// {
|
||||||
|
// label: '超管',
|
||||||
|
// value: 1,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// label: '菜单',
|
||||||
|
// value: 2,
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// placeholder: '请选择',
|
||||||
|
// },
|
||||||
|
// fieldName: 'role_id',
|
||||||
|
// label: '角色',
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
// 控制表单是否显示折叠按钮
|
||||||
|
showCollapseButton: true,
|
||||||
|
submitButtonOptions: {
|
||||||
|
content: '查询',
|
||||||
|
},
|
||||||
|
// 是否在字段值改变时提交表单
|
||||||
|
submitOnChange: true,
|
||||||
|
// 按下回车时是否提交表单
|
||||||
|
submitOnEnter: false,
|
||||||
|
};
|
||||||
97
apps/web-antd/src/views/system/menu/config/table.ts
Normal file
97
apps/web-antd/src/views/system/menu/config/table.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { getMenuList } from '../api';
|
||||||
|
|
||||||
|
interface RowType {
|
||||||
|
id: string;
|
||||||
|
nick_name: string;
|
||||||
|
email: string;
|
||||||
|
avatar: string;
|
||||||
|
roles: { name: string }[];
|
||||||
|
open_id: string;
|
||||||
|
code: string;
|
||||||
|
platform_id: string;
|
||||||
|
phone: string;
|
||||||
|
desc: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const gridOptions: VxeGridProps<RowType> = {
|
||||||
|
checkboxConfig: {
|
||||||
|
highlight: true,
|
||||||
|
labelField: '',
|
||||||
|
},
|
||||||
|
columnConfig: {
|
||||||
|
useKey: true,
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
useKey: true,
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{ type: 'checkbox', width: 60 },
|
||||||
|
{ width: 60, treeNode: true },
|
||||||
|
{ field: 'id', align: 'left', title: 'ID', width: 100 },
|
||||||
|
{ field: 'title', align: 'left', title: '菜单名称' },
|
||||||
|
{ field: 'icon', title: '图标', slots: { default: 'icon' } },
|
||||||
|
{ field: 'path', title: '路由' },
|
||||||
|
{ field: 'name', title: '路由Name' },
|
||||||
|
{ field: 'component', title: '组件地址' },
|
||||||
|
{ field: 'redirect', title: '重定向' },
|
||||||
|
{ field: 'keep_alive', title: '页面缓存' },
|
||||||
|
{ field: 'hide_in_menu', title: '菜单展示' },
|
||||||
|
{ field: 'badge', title: '徽标' },
|
||||||
|
{ field: 'badge_type', title: '徽标类型' },
|
||||||
|
{ field: 'badge_variants', title: '徽标颜色' },
|
||||||
|
{ field: 'iframe_src', title: '引用的页面地址' },
|
||||||
|
{ field: 'sort', title: '排序' },
|
||||||
|
{ field: 'query', title: '默认参数' },
|
||||||
|
{ field: 'created_at', title: '创建时间' },
|
||||||
|
{
|
||||||
|
type: 'html',
|
||||||
|
align: 'right',
|
||||||
|
title: '操作',
|
||||||
|
slots: { default: 'action' },
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
treeConfig: {
|
||||||
|
parentField: 'pid',
|
||||||
|
rowField: 'id',
|
||||||
|
transform: true,
|
||||||
|
expandAll: true,
|
||||||
|
},
|
||||||
|
keepSource: true,
|
||||||
|
pagerConfig: {},
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
// 请求后端接口方法
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getMenuList({
|
||||||
|
page: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
height: 'auto',
|
||||||
|
border: false,
|
||||||
|
toolbarConfig: {
|
||||||
|
// 是否显示搜索表单控制按钮
|
||||||
|
// @ts-ignore 正式环境时有完整的类型声明
|
||||||
|
search: true,
|
||||||
|
refresh: true, // 刷新
|
||||||
|
print: false, // 打印
|
||||||
|
export: false, // 导出
|
||||||
|
// custom: true, // 自定义列
|
||||||
|
zoom: true, // 最大化最小化
|
||||||
|
slots: {
|
||||||
|
buttons: 'toolbar-buttons',
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
// 自定义列-图标
|
||||||
|
icon: 'vxe-icon-menu',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
showOverflow: false,
|
||||||
|
};
|
||||||
166
apps/web-antd/src/views/system/menu/index.vue
Normal file
166
apps/web-antd/src/views/system/menu/index.vue
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeGridListeners } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Button, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { Icon } from '#/components/icon';
|
||||||
|
import { TableAction } from '#/components/table-action';
|
||||||
|
|
||||||
|
import { deleteMenu } from './api';
|
||||||
|
import FormModalDemo from './components/modal.vue';
|
||||||
|
import { formOptions } from './config/search';
|
||||||
|
import { gridOptions } from './config/table';
|
||||||
|
|
||||||
|
const hasTopTableDropDownActions = ref(false);
|
||||||
|
|
||||||
|
const gridEvents: VxeGridListeners<any> = {
|
||||||
|
checkboxChange() {
|
||||||
|
const records = gridApi.grid.getCheckboxRecords();
|
||||||
|
hasTopTableDropDownActions.value = records.length > 0;
|
||||||
|
},
|
||||||
|
checkboxAll() {
|
||||||
|
const records = gridApi.grid.getCheckboxRecords();
|
||||||
|
hasTopTableDropDownActions.value = records.length > 0;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions,
|
||||||
|
gridOptions,
|
||||||
|
gridEvents,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: FormModalDemo,
|
||||||
|
});
|
||||||
|
|
||||||
|
const showModal = (data = {}, isUpdate = false) => {
|
||||||
|
formModalApi.setData({
|
||||||
|
// 表单值
|
||||||
|
values: data,
|
||||||
|
update: isUpdate,
|
||||||
|
gridApi,
|
||||||
|
});
|
||||||
|
formModalApi.open();
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteApi = (row: any) => {
|
||||||
|
let ids = [];
|
||||||
|
if (row) {
|
||||||
|
ids.push(row);
|
||||||
|
} else {
|
||||||
|
ids = gridApi.grid.getCheckboxRecords().map((item) => item.id);
|
||||||
|
}
|
||||||
|
deleteMenu({ ids }).then(() => {
|
||||||
|
message.success('删除成功!');
|
||||||
|
gridApi.reload();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const expandAll = () => {
|
||||||
|
gridApi.grid?.setAllTreeExpand(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const collapseAll = () => {
|
||||||
|
gridApi.grid?.setAllTreeExpand(false);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height title="菜单管理">
|
||||||
|
<FormModal />
|
||||||
|
<Grid>
|
||||||
|
<template #toolbar-buttons>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '新增',
|
||||||
|
type: 'primary',
|
||||||
|
icon: 'ant-design:plus-outlined',
|
||||||
|
// auth: ['超级菜单', 'sys:user:save'],
|
||||||
|
onClick: showModal.bind(null),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '展开全部',
|
||||||
|
type: 'primary',
|
||||||
|
// auth: ['超级菜单', 'sys:user:save'],
|
||||||
|
onClick: expandAll.bind(null),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '收起全部',
|
||||||
|
type: 'primary',
|
||||||
|
// auth: ['超级菜单', 'sys:user:save'],
|
||||||
|
onClick: collapseAll.bind(null),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:drop-down-actions="[
|
||||||
|
{
|
||||||
|
label: '删除',
|
||||||
|
icon: 'ant-design:delete-outlined',
|
||||||
|
ifShow: hasTopTableDropDownActions,
|
||||||
|
// auth: ['超级菜单', 'sys:user:save'],
|
||||||
|
popConfirm: {
|
||||||
|
title: '确定删除吗?',
|
||||||
|
confirm: deleteApi.bind(null, false),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<template #more>
|
||||||
|
<Button style="margin-left: 16px">
|
||||||
|
批量操作
|
||||||
|
<Icon icon="ant-design:down-outlined" />
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</TableAction>
|
||||||
|
</template>
|
||||||
|
<template #icon="{ row }">
|
||||||
|
<Icon :icon="row.icon" />
|
||||||
|
</template>
|
||||||
|
<template #toolbar-tools></template>
|
||||||
|
<template #action="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '编辑',
|
||||||
|
type: 'link',
|
||||||
|
icon: 'uil:edit',
|
||||||
|
size: 'small',
|
||||||
|
// auth: ['admin', 'sys:role:detail'],
|
||||||
|
onClick: showModal.bind(null, row, true),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:drop-down-actions="[
|
||||||
|
{
|
||||||
|
label: '删除',
|
||||||
|
type: 'link',
|
||||||
|
icon: 'ant-design:delete-outlined',
|
||||||
|
size: 'small',
|
||||||
|
// auth: ['admin', 'sys:role:detail'],
|
||||||
|
popConfirm: {
|
||||||
|
title: '确定删除吗?',
|
||||||
|
confirm: deleteApi.bind(null, row.id),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '删除',
|
||||||
|
type: 'link',
|
||||||
|
icon: 'ant-design:delete-outlined',
|
||||||
|
size: 'small',
|
||||||
|
// auth: ['admin', 'sys:role:detail'],
|
||||||
|
popConfirm: {
|
||||||
|
title: '确定删除吗?',
|
||||||
|
confirm: deleteApi.bind(null, row.id),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
69
apps/web-antd/src/views/system/role/api/index.ts
Normal file
69
apps/web-antd/src/views/system/role/api/index.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
const prefix = 'role/';
|
||||||
|
/**
|
||||||
|
* 分页查询用户列表
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export async function getRoleList(data: any) {
|
||||||
|
return requestClient.get<any>(`${prefix}list`, { params: data });
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 分页查询用户列表
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export async function getRoleOption(data: any) {
|
||||||
|
return requestClient.get<any>(`${prefix}option`, { params: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取详情
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
export async function getRoleInfo(id: number) {
|
||||||
|
return requestClient.get<any>(`${prefix}detail`, { params: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询菜单下拉框
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export async function getMenuIdsByRoleIds(data: any) {
|
||||||
|
return requestClient.get<any>(`${prefix}get-menu-ids-by-role-ids`, {
|
||||||
|
params: {
|
||||||
|
role_id: data.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑角色
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export async function saveRoleMenu(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}save-role-menu`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增角色
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export async function createRole(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}create`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑角色
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export async function updateRole(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}update`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除角色
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export async function deleteRole(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}delete`, data);
|
||||||
|
}
|
||||||
140
apps/web-antd/src/views/system/role/components/auth-menu.vue
Normal file
140
apps/web-antd/src/views/system/role/components/auth-menu.vue
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenDrawer } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Button, message, Tree } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { Icon } from '#/components/icon';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
import { getAllNodeIds, getLeafNodeIds } from '#/util/tool';
|
||||||
|
import { getMenuTreeOption } from '#/views/system/menu/api';
|
||||||
|
|
||||||
|
import { getMenuIdsByRoleIds, saveRoleMenu } from '../api';
|
||||||
|
|
||||||
|
const record = ref();
|
||||||
|
const treeRef = ref();
|
||||||
|
const treeData = ref([]);
|
||||||
|
const isExpand = ref(false);
|
||||||
|
|
||||||
|
// 勾选的key
|
||||||
|
const checkedKeys = ref([]);
|
||||||
|
// 提交的勾选的key,会进行特殊处理,包含半勾状态的父节点halfCheckedKeys
|
||||||
|
const submitCheckedKeys = ref<any>([]);
|
||||||
|
// 所有叶子节点key
|
||||||
|
const leafKeys = ref<any>([]);
|
||||||
|
// 所有节点key
|
||||||
|
const allNodeIds = ref([]);
|
||||||
|
// 当前展开的key
|
||||||
|
const currentExpandedKeys = ref([]);
|
||||||
|
/**
|
||||||
|
* api请求成功回调
|
||||||
|
*/
|
||||||
|
const handleFetchSuccess = () => {
|
||||||
|
getMenuIdsByRoleIds({
|
||||||
|
id: record.value.id,
|
||||||
|
// appCode: props.appCode,
|
||||||
|
}).then((res: any) => {
|
||||||
|
// 设置的勾选节点只能为叶子节点
|
||||||
|
checkedKeys.value = res.filter((item: any) => {
|
||||||
|
return leafKeys.value.includes(item);
|
||||||
|
});
|
||||||
|
submitCheckedKeys.value = res;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const [Drawer, DrawerApi] = useVbenDrawer({
|
||||||
|
onOpenChange(isOpen) {
|
||||||
|
record.value = isOpen ? DrawerApi.getData()?.record : {};
|
||||||
|
if (isOpen) {
|
||||||
|
DrawerApi.setState({
|
||||||
|
loading: true,
|
||||||
|
});
|
||||||
|
getMenuTreeOption({
|
||||||
|
filterByUser: 1,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
treeData.value = res;
|
||||||
|
leafKeys.value = getLeafNodeIds(res);
|
||||||
|
allNodeIds.value = getAllNodeIds(res);
|
||||||
|
handleFetchSuccess();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
DrawerApi.setState({
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onConfirm() {
|
||||||
|
const menus = submitCheckedKeys.value.map((item: any) => {
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
DrawerApi.setState({
|
||||||
|
loading: true,
|
||||||
|
confirmLoading: true,
|
||||||
|
});
|
||||||
|
saveRoleMenu({
|
||||||
|
role_id: record.value.id,
|
||||||
|
menu_id: menus,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
message.success('保存成功');
|
||||||
|
DrawerApi.close();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
DrawerApi.setState({
|
||||||
|
loading: false,
|
||||||
|
confirmLoading: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* 点击复选框触发处理
|
||||||
|
* @param mCheckedKeys
|
||||||
|
*/
|
||||||
|
const handleCheck = (mCheckedKeys: any, e: any) => {
|
||||||
|
checkedKeys.value = mCheckedKeys;
|
||||||
|
// 提交的时候需要将半选的父节点也提交上
|
||||||
|
submitCheckedKeys.value = [...mCheckedKeys, ...e.halfCheckedKeys];
|
||||||
|
};
|
||||||
|
// 展开折叠事件
|
||||||
|
const handleExpand = (expandedKeys: any) => {
|
||||||
|
currentExpandedKeys.value = expandedKeys;
|
||||||
|
};
|
||||||
|
// 展开折叠按钮事件
|
||||||
|
const handleExpandAndCollapse = () => {
|
||||||
|
isExpand.value = !isExpand.value;
|
||||||
|
currentExpandedKeys.value = isExpand.value ? allNodeIds.value : [];
|
||||||
|
};
|
||||||
|
defineExpose(DrawerApi);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Drawer class="w-[60%]" title="授权菜单">
|
||||||
|
<Button type="primary" @click="handleExpandAndCollapse">
|
||||||
|
{{ isExpand ? '折叠' : '展开' }}
|
||||||
|
</Button>
|
||||||
|
<Tree
|
||||||
|
ref="treeRef"
|
||||||
|
v-model:checked-keys="checkedKeys"
|
||||||
|
:expanded-keys="currentExpandedKeys"
|
||||||
|
:field-names="{
|
||||||
|
title: 'title',
|
||||||
|
key: 'id',
|
||||||
|
}"
|
||||||
|
:show-line="true"
|
||||||
|
:tree-data="treeData"
|
||||||
|
checkable
|
||||||
|
style="margin: 20px auto"
|
||||||
|
@check="handleCheck"
|
||||||
|
@expand="handleExpand"
|
||||||
|
>
|
||||||
|
<template #title="{ title, icon }">
|
||||||
|
<Icon :icon="icon" />
|
||||||
|
{{ $t(title) }}
|
||||||
|
</template>
|
||||||
|
</Tree>
|
||||||
|
</Drawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
62
apps/web-antd/src/views/system/role/components/modal.vue
Normal file
62
apps/web-antd/src/views/system/role/components/modal.vue
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import { createRole, updateRole } from '#/views/system/role/api';
|
||||||
|
import { modalFormProps } from '#/views/system/role/config/form';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'FormModelDemo',
|
||||||
|
});
|
||||||
|
|
||||||
|
const isUpdate = ref(false);
|
||||||
|
const gridApi = ref();
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm(modalFormProps);
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
fullscreenButton: false,
|
||||||
|
draggable: true,
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onConfirm: async () => {
|
||||||
|
formApi.validate().then(async (e: any) => {
|
||||||
|
if (e.valid) {
|
||||||
|
const values = await formApi.getValues();
|
||||||
|
modalApi.setState({ loading: true, confirmLoading: true });
|
||||||
|
const submitApi = isUpdate.value ? updateRole : createRole;
|
||||||
|
submitApi(values)
|
||||||
|
.then(() => {
|
||||||
|
message.success('保存成功');
|
||||||
|
gridApi.value?.reload();
|
||||||
|
modalApi.close();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
modalApi.setState({ loading: false, confirmLoading: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await formApi.validateAndSubmitForm();
|
||||||
|
},
|
||||||
|
onOpenChange(isOpen: boolean) {
|
||||||
|
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||||
|
if (isOpen) {
|
||||||
|
const { values, update } = modalApi.getData<Record<string, any>>();
|
||||||
|
if (values) {
|
||||||
|
isUpdate.value = update;
|
||||||
|
formApi.setValues(values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Modal :title="`${isUpdate === true ? '编辑' : '新增'}角色`" class="w-[30%]">
|
||||||
|
<Form />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
53
apps/web-antd/src/views/system/role/config/form.ts
Normal file
53
apps/web-antd/src/views/system/role/config/form.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import type { VbenFormProps } from '#/adapter/form';
|
||||||
|
|
||||||
|
export const modalFormProps: VbenFormProps = {
|
||||||
|
wrapperClass: 'grid-cols-12', // 24栅格,
|
||||||
|
commonConfig: {
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
// 所有表单项
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// handleSubmit: onSubmit,
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
fieldName: 'id',
|
||||||
|
label: 'ID',
|
||||||
|
dependencies: {
|
||||||
|
show: false,
|
||||||
|
triggerFields: ['id'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入角色昵称',
|
||||||
|
},
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '昵称',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入角色代码',
|
||||||
|
},
|
||||||
|
fieldName: 'value',
|
||||||
|
label: '角色代码',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入角色说明',
|
||||||
|
},
|
||||||
|
fieldName: 'desc',
|
||||||
|
label: '角色说明',
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
showDefaultActions: false,
|
||||||
|
};
|
||||||
35
apps/web-antd/src/views/system/role/config/search.ts
Normal file
35
apps/web-antd/src/views/system/role/config/search.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import type { VbenFormProps } from '#/adapter/form';
|
||||||
|
|
||||||
|
export const formOptions: VbenFormProps = {
|
||||||
|
// 默认展开
|
||||||
|
collapsed: false,
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '输入名称',
|
||||||
|
},
|
||||||
|
defaultValue: '',
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '角色名称',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'VbenInput',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '输入角色代码',
|
||||||
|
},
|
||||||
|
defaultValue: '',
|
||||||
|
fieldName: 'value',
|
||||||
|
label: '角色代码',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// 控制表单是否显示折叠按钮
|
||||||
|
showCollapseButton: true,
|
||||||
|
submitButtonOptions: {
|
||||||
|
content: '查询',
|
||||||
|
},
|
||||||
|
// 是否在字段值改变时提交表单
|
||||||
|
submitOnChange: true,
|
||||||
|
// 按下回车时是否提交表单
|
||||||
|
submitOnEnter: false,
|
||||||
|
};
|
||||||
73
apps/web-antd/src/views/system/role/config/table.ts
Normal file
73
apps/web-antd/src/views/system/role/config/table.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { getRoleList } from '#/views/system/role/api';
|
||||||
|
|
||||||
|
interface RowType {
|
||||||
|
id: string;
|
||||||
|
nick_name: string;
|
||||||
|
email: string;
|
||||||
|
avatar: string;
|
||||||
|
roles: { name: string }[];
|
||||||
|
open_id: string;
|
||||||
|
code: string;
|
||||||
|
platform_id: string;
|
||||||
|
phone: string;
|
||||||
|
desc: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const gridOptions: VxeGridProps<RowType> = {
|
||||||
|
checkboxConfig: {
|
||||||
|
highlight: true,
|
||||||
|
labelField: '',
|
||||||
|
},
|
||||||
|
columnConfig: {
|
||||||
|
useKey: true,
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
useKey: true,
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{ type: 'checkbox', width: 60 },
|
||||||
|
{ field: 'id', align: 'left', title: 'ID', width: 100 },
|
||||||
|
{ field: 'name', align: 'left', title: '名称' },
|
||||||
|
{ field: 'value', title: '角色代码' },
|
||||||
|
{ field: 'desc', title: '备注' },
|
||||||
|
{ field: 'created_at', title: '创建时间' },
|
||||||
|
{ type: 'html', title: '操作', slots: { default: 'action' } },
|
||||||
|
],
|
||||||
|
keepSource: true,
|
||||||
|
pagerConfig: {},
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
// 请求后端接口方法
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getRoleList({
|
||||||
|
page: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
height: 'auto',
|
||||||
|
border: false,
|
||||||
|
toolbarConfig: {
|
||||||
|
// 是否显示搜索表单控制按钮
|
||||||
|
// @ts-ignore 正式环境时有完整的类型声明
|
||||||
|
search: true,
|
||||||
|
refresh: true, // 刷新
|
||||||
|
print: false, // 打印
|
||||||
|
export: false, // 导出
|
||||||
|
// custom: true, // 自定义列
|
||||||
|
zoom: true, // 最大化最小化
|
||||||
|
slots: {
|
||||||
|
buttons: 'toolbar-buttons',
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
// 自定义列-图标
|
||||||
|
icon: 'vxe-icon-menu',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
showOverflow: false,
|
||||||
|
};
|
||||||
157
apps/web-antd/src/views/system/role/index.vue
Normal file
157
apps/web-antd/src/views/system/role/index.vue
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeGridListeners } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Button, Image, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { TableAction } from '#/components/table-action';
|
||||||
|
import AuthMenu from '#/views/system/role/components/auth-menu.vue';
|
||||||
|
|
||||||
|
import { deleteRole } from './api';
|
||||||
|
import FormModalDemo from './components/modal.vue';
|
||||||
|
import { formOptions } from './config/search';
|
||||||
|
import { gridOptions } from './config/table';
|
||||||
|
|
||||||
|
const hasTopTableDropDownActions = ref(false);
|
||||||
|
|
||||||
|
const gridEvents: VxeGridListeners<any> = {
|
||||||
|
checkboxChange() {
|
||||||
|
const records = gridApi.grid.getCheckboxRecords();
|
||||||
|
hasTopTableDropDownActions.value = records.length > 0;
|
||||||
|
},
|
||||||
|
checkboxAll() {
|
||||||
|
const records = gridApi.grid.getCheckboxRecords();
|
||||||
|
hasTopTableDropDownActions.value = records.length > 0;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions,
|
||||||
|
gridOptions,
|
||||||
|
gridEvents,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: FormModalDemo,
|
||||||
|
});
|
||||||
|
|
||||||
|
const showModal = (data = {}, isUpdate = false) => {
|
||||||
|
formModalApi.setData({
|
||||||
|
// 表单值
|
||||||
|
values: {
|
||||||
|
id: data?.id,
|
||||||
|
name: data?.name,
|
||||||
|
value: data?.value,
|
||||||
|
desc: data?.desc,
|
||||||
|
},
|
||||||
|
update: isUpdate,
|
||||||
|
gridApi,
|
||||||
|
});
|
||||||
|
formModalApi.open();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 授权菜单
|
||||||
|
const authMenuRef = ref();
|
||||||
|
const handleAuthMenu = (record: any) => {
|
||||||
|
authMenuRef.value.setData({
|
||||||
|
record,
|
||||||
|
});
|
||||||
|
authMenuRef.value.open();
|
||||||
|
};
|
||||||
|
const deleteApi = (row: any) => {
|
||||||
|
let ids = [];
|
||||||
|
if (row) {
|
||||||
|
ids.push(row);
|
||||||
|
} else {
|
||||||
|
ids = gridApi.grid.getCheckboxRecords().map((item) => item.id);
|
||||||
|
}
|
||||||
|
deleteRole({ ids }).then(() => {
|
||||||
|
message.success('删除成功!');
|
||||||
|
gridApi.reload();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height title="角色管理">
|
||||||
|
<FormModal />
|
||||||
|
<AuthMenu ref="authMenuRef" />
|
||||||
|
<Grid>
|
||||||
|
<template #toolbar-buttons>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '新增',
|
||||||
|
type: 'primary',
|
||||||
|
icon: 'ant-design:plus-outlined',
|
||||||
|
// auth: ['超级角色', 'sys:user:save'],
|
||||||
|
onClick: showModal.bind(null),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:drop-down-actions="[
|
||||||
|
{
|
||||||
|
label: '删除',
|
||||||
|
icon: 'ant-design:delete-outlined',
|
||||||
|
ifShow: hasTopTableDropDownActions,
|
||||||
|
// auth: ['超级角色', 'sys:user:save'],
|
||||||
|
popConfirm: {
|
||||||
|
title: '确定删除吗?',
|
||||||
|
confirm: deleteApi.bind(null, false),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<template #more>
|
||||||
|
<Button style="margin-left: 16px">
|
||||||
|
批量操作
|
||||||
|
<Icon icon="ant-design:down-outlined" />
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</TableAction>
|
||||||
|
</template>
|
||||||
|
<template #avatar="{ row }">
|
||||||
|
<Image :src="row.avatar" height="30" width="30" />
|
||||||
|
</template>
|
||||||
|
<template #toolbar-tools></template>
|
||||||
|
<template #action="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: '编辑',
|
||||||
|
type: 'link',
|
||||||
|
icon: 'uil:edit',
|
||||||
|
size: 'small',
|
||||||
|
// auth: ['admin', 'sys:role:detail'],
|
||||||
|
onClick: showModal.bind(null, row, true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '授权菜单',
|
||||||
|
type: 'link',
|
||||||
|
icon: 'arcticons:microsoft-authenticator',
|
||||||
|
size: 'small',
|
||||||
|
// auth: ['admin', 'sys:role:detail'],
|
||||||
|
onClick: handleAuthMenu.bind(null, row),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:drop-down-actions="[
|
||||||
|
{
|
||||||
|
label: '删除',
|
||||||
|
type: 'link',
|
||||||
|
icon: 'ant-design:delete-outlined',
|
||||||
|
size: 'small',
|
||||||
|
// auth: ['admin', 'sys:role:detail'],
|
||||||
|
popConfirm: {
|
||||||
|
title: '确定删除吗?',
|
||||||
|
confirm: deleteApi.bind(null, row.id),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
@@ -11,7 +11,8 @@ export default defineConfig(async () => {
|
|||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||||
// mock代理目标地址
|
// mock代理目标地址
|
||||||
target: 'http://localhost:5320/api',
|
// target: 'http://localhost:5320/api',
|
||||||
|
target: 'http://127.0.0.1:18009/api/',
|
||||||
ws: true,
|
ws: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user