init
This commit is contained in:
3
web/apps/web-antd/src/views/_core/README.md
Normal file
3
web/apps/web-antd/src/views/_core/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# \_core
|
||||
|
||||
此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。
|
||||
9
web/apps/web-antd/src/views/_core/about/index.vue
Normal file
9
web/apps/web-antd/src/views/_core/about/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { About } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'About' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<About />
|
||||
</template>
|
||||
@@ -0,0 +1,69 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { AuthenticationCodeLogin, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
defineOptions({ name: 'CodeLogin' });
|
||||
|
||||
const loading = ref(false);
|
||||
const CODE_LENGTH = 6;
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.mobile'),
|
||||
},
|
||||
fieldName: 'phoneNumber',
|
||||
label: $t('authentication.mobile'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.mobileTip') })
|
||||
.refine((v) => /^\d{11}$/.test(v), {
|
||||
message: $t('authentication.mobileErrortip'),
|
||||
}),
|
||||
},
|
||||
{
|
||||
component: 'VbenPinInput',
|
||||
componentProps: {
|
||||
codeLength: CODE_LENGTH,
|
||||
createText: (countdown: number) => {
|
||||
const text =
|
||||
countdown > 0
|
||||
? $t('authentication.sendText', [countdown])
|
||||
: $t('authentication.sendCode');
|
||||
return text;
|
||||
},
|
||||
placeholder: $t('authentication.code'),
|
||||
},
|
||||
fieldName: 'code',
|
||||
label: $t('authentication.code'),
|
||||
rules: z.string().length(CODE_LENGTH, {
|
||||
message: $t('authentication.codeTip', [CODE_LENGTH]),
|
||||
}),
|
||||
},
|
||||
];
|
||||
});
|
||||
/**
|
||||
* 异步处理登录操作
|
||||
* Asynchronously handle the login process
|
||||
* @param values 登录表单数据
|
||||
*/
|
||||
async function handleLogin(values: Recordable<any>) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(values);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationCodeLogin
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
@submit="handleLogin"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { AuthenticationForgetPassword, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
defineOptions({ name: 'ForgetPassword' });
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: 'example@example.com',
|
||||
},
|
||||
fieldName: 'email',
|
||||
label: $t('authentication.email'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.emailTip') })
|
||||
.email($t('authentication.emailValidErrorTip')),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
function handleSubmit(value: Recordable<any>) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('reset email:', value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationForgetPassword
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
98
web/apps/web-antd/src/views/_core/authentication/login.vue
Normal file
98
web/apps/web-antd/src/views/_core/authentication/login.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { BasicOption } from '@vben/types';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { AuthenticationLogin, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
defineOptions({ name: 'Login' });
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const MOCK_USER_OPTIONS: BasicOption[] = [
|
||||
{
|
||||
label: 'Super',
|
||||
value: 'vben',
|
||||
},
|
||||
{
|
||||
label: 'Admin',
|
||||
value: 'admin',
|
||||
},
|
||||
{
|
||||
label: 'User',
|
||||
value: 'jack',
|
||||
},
|
||||
];
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
componentProps: {
|
||||
options: MOCK_USER_OPTIONS,
|
||||
placeholder: $t('authentication.selectAccount'),
|
||||
},
|
||||
fieldName: 'selectAccount',
|
||||
label: $t('authentication.selectAccount'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.selectAccount') })
|
||||
.optional()
|
||||
.default('admin'),
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.usernameTip'),
|
||||
},
|
||||
dependencies: {
|
||||
trigger(values, form) {
|
||||
if (values.selectAccount) {
|
||||
const findUser = MOCK_USER_OPTIONS.find(
|
||||
(item) => item.value === values.selectAccount,
|
||||
);
|
||||
if (findUser) {
|
||||
form.setValues({
|
||||
password: 'admin123',
|
||||
username: findUser.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
triggerFields: ['selectAccount'],
|
||||
},
|
||||
fieldName: 'username',
|
||||
label: $t('authentication.username'),
|
||||
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
|
||||
},
|
||||
{
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.password'),
|
||||
},
|
||||
fieldName: 'password',
|
||||
label: $t('authentication.password'),
|
||||
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
|
||||
},
|
||||
// {
|
||||
// component: markRaw(SliderCaptcha),
|
||||
// fieldName: 'captcha',
|
||||
// rules: z.boolean().refine((value) => value, {
|
||||
// message: $t('authentication.verifyRequiredTip'),
|
||||
// }),
|
||||
// },
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationLogin
|
||||
:form-schema="formSchema"
|
||||
:loading="authStore.loginLoading"
|
||||
@submit="authStore.authLogin"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { AuthenticationQrCodeLogin } from '@vben/common-ui';
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
|
||||
defineOptions({ name: 'QrCodeLogin' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationQrCodeLogin :login-path="LOGIN_PATH" />
|
||||
</template>
|
||||
@@ -0,0 +1,96 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { computed, h, ref } from 'vue';
|
||||
|
||||
import { AuthenticationRegister, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
defineOptions({ name: 'Register' });
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.usernameTip'),
|
||||
},
|
||||
fieldName: 'username',
|
||||
label: $t('authentication.username'),
|
||||
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
|
||||
},
|
||||
{
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
passwordStrength: true,
|
||||
placeholder: $t('authentication.password'),
|
||||
},
|
||||
fieldName: 'password',
|
||||
label: $t('authentication.password'),
|
||||
renderComponentContent() {
|
||||
return {
|
||||
strengthText: () => $t('authentication.passwordStrength'),
|
||||
};
|
||||
},
|
||||
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
|
||||
},
|
||||
{
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.confirmPassword'),
|
||||
},
|
||||
dependencies: {
|
||||
rules(values) {
|
||||
const { password } = values;
|
||||
return z
|
||||
.string({ required_error: $t('authentication.passwordTip') })
|
||||
.min(1, { message: $t('authentication.passwordTip') })
|
||||
.refine((value) => value === password, {
|
||||
message: $t('authentication.confirmPasswordTip'),
|
||||
});
|
||||
},
|
||||
triggerFields: ['password'],
|
||||
},
|
||||
fieldName: 'confirmPassword',
|
||||
label: $t('authentication.confirmPassword'),
|
||||
},
|
||||
{
|
||||
component: 'VbenCheckbox',
|
||||
fieldName: 'agreePolicy',
|
||||
renderComponentContent: () => ({
|
||||
default: () =>
|
||||
h('span', [
|
||||
$t('authentication.agree'),
|
||||
h(
|
||||
'a',
|
||||
{
|
||||
class: 'vben-link ml-1 ',
|
||||
href: '',
|
||||
},
|
||||
`${$t('authentication.privacyPolicy')} & ${$t('authentication.terms')}`,
|
||||
),
|
||||
]),
|
||||
}),
|
||||
rules: z.boolean().refine((value) => !!value, {
|
||||
message: $t('authentication.agreeTip'),
|
||||
}),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
function handleSubmit(value: Recordable<any>) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('register submit:', value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationRegister
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="coming-soon" />
|
||||
</template>
|
||||
9
web/apps/web-antd/src/views/_core/fallback/forbidden.vue
Normal file
9
web/apps/web-antd/src/views/_core/fallback/forbidden.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'Fallback403Demo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="403" />
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'Fallback500Demo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="500" />
|
||||
</template>
|
||||
9
web/apps/web-antd/src/views/_core/fallback/not-found.vue
Normal file
9
web/apps/web-antd/src/views/_core/fallback/not-found.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'Fallback404Demo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="404" />
|
||||
</template>
|
||||
9
web/apps/web-antd/src/views/_core/fallback/offline.vue
Normal file
9
web/apps/web-antd/src/views/_core/fallback/offline.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'FallbackOfflineDemo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="offline" />
|
||||
</template>
|
||||
@@ -0,0 +1,98 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
grid: {
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '2 %',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
areaStyle: {},
|
||||
data: [
|
||||
111, 2000, 6000, 16_000, 33_333, 55_555, 64_000, 33_333, 18_000,
|
||||
36_000, 70_000, 42_444, 23_222, 13_000, 8000, 4000, 1200, 333, 222,
|
||||
111,
|
||||
],
|
||||
itemStyle: {
|
||||
color: '#5ab1ef',
|
||||
},
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
},
|
||||
{
|
||||
areaStyle: {},
|
||||
data: [
|
||||
33, 66, 88, 333, 3333, 6200, 20_000, 3000, 1200, 13_000, 22_000,
|
||||
11_000, 2221, 1201, 390, 198, 60, 30, 22, 11,
|
||||
],
|
||||
itemStyle: {
|
||||
color: '#019680',
|
||||
},
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
color: '#019680',
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
trigger: 'axis',
|
||||
},
|
||||
// xAxis: {
|
||||
// axisTick: {
|
||||
// show: false,
|
||||
// },
|
||||
// boundaryGap: false,
|
||||
// data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
|
||||
// type: 'category',
|
||||
// },
|
||||
xAxis: {
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
boundaryGap: false,
|
||||
data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'solid',
|
||||
width: 1,
|
||||
},
|
||||
show: true,
|
||||
},
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
max: 80_000,
|
||||
splitArea: {
|
||||
show: true,
|
||||
},
|
||||
splitNumber: 4,
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,82 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
legend: {
|
||||
bottom: 0,
|
||||
data: ['访问', '趋势'],
|
||||
},
|
||||
radar: {
|
||||
indicator: [
|
||||
{
|
||||
name: '网页',
|
||||
},
|
||||
{
|
||||
name: '移动端',
|
||||
},
|
||||
{
|
||||
name: 'Ipad',
|
||||
},
|
||||
{
|
||||
name: '客户端',
|
||||
},
|
||||
{
|
||||
name: '第三方',
|
||||
},
|
||||
{
|
||||
name: '其它',
|
||||
},
|
||||
],
|
||||
radius: '60%',
|
||||
splitNumber: 8,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
areaStyle: {
|
||||
opacity: 1,
|
||||
shadowBlur: 0,
|
||||
shadowColor: 'rgba(0,0,0,.2)',
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 10,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
itemStyle: {
|
||||
color: '#b6a2de',
|
||||
},
|
||||
name: '访问',
|
||||
value: [90, 50, 86, 40, 50, 20],
|
||||
},
|
||||
{
|
||||
itemStyle: {
|
||||
color: '#5ab1ef',
|
||||
},
|
||||
name: '趋势',
|
||||
value: [70, 75, 70, 76, 20, 85],
|
||||
},
|
||||
],
|
||||
itemStyle: {
|
||||
// borderColor: '#fff',
|
||||
borderRadius: 10,
|
||||
borderWidth: 2,
|
||||
},
|
||||
symbolSize: 0,
|
||||
type: 'radar',
|
||||
},
|
||||
],
|
||||
tooltip: {},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,46 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
series: [
|
||||
{
|
||||
animationDelay() {
|
||||
return Math.random() * 400;
|
||||
},
|
||||
animationEasing: 'exponentialInOut',
|
||||
animationType: 'scale',
|
||||
center: ['50%', '50%'],
|
||||
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
|
||||
data: [
|
||||
{ name: '外包', value: 500 },
|
||||
{ name: '定制', value: 310 },
|
||||
{ name: '技术支持', value: 274 },
|
||||
{ name: '远程', value: 400 },
|
||||
].sort((a, b) => {
|
||||
return a.value - b.value;
|
||||
}),
|
||||
name: '商业占比',
|
||||
radius: '80%',
|
||||
roseType: 'radius',
|
||||
type: 'pie',
|
||||
},
|
||||
],
|
||||
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
legend: {
|
||||
bottom: '2%',
|
||||
left: 'center',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
animationDelay() {
|
||||
return Math.random() * 100;
|
||||
},
|
||||
animationEasing: 'exponentialInOut',
|
||||
animationType: 'scale',
|
||||
avoidLabelOverlap: false,
|
||||
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
|
||||
data: [
|
||||
{ name: '搜索引擎', value: 1048 },
|
||||
{ name: '直接访问', value: 735 },
|
||||
{ name: '邮件营销', value: 580 },
|
||||
{ name: '联盟广告', value: 484 },
|
||||
],
|
||||
emphasis: {
|
||||
label: {
|
||||
fontSize: '12',
|
||||
fontWeight: 'bold',
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
itemStyle: {
|
||||
// borderColor: '#fff',
|
||||
borderRadius: 10,
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
position: 'center',
|
||||
show: false,
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
name: '访问来源',
|
||||
radius: ['40%', '65%'],
|
||||
type: 'pie',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,55 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
grid: {
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '2 %',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
barMaxWidth: 80,
|
||||
// color: '#4f69fd',
|
||||
data: [
|
||||
3000, 2000, 3333, 5000, 3200, 4200, 3200, 2100, 3000, 5100, 6000,
|
||||
3200, 4800,
|
||||
],
|
||||
type: 'bar',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
// color: '#4f69fd',
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
trigger: 'axis',
|
||||
},
|
||||
xAxis: {
|
||||
data: Array.from({ length: 12 }).map((_item, index) => `${index + 1}月`),
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: {
|
||||
max: 8000,
|
||||
splitNumber: 4,
|
||||
type: 'value',
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
90
web/apps/web-antd/src/views/dashboard/analytics/index.vue
Normal file
90
web/apps/web-antd/src/views/dashboard/analytics/index.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<script lang="ts" setup>
|
||||
import type { AnalysisOverviewItem } from '@vben/common-ui';
|
||||
import type { TabOption } from '@vben/types';
|
||||
|
||||
import {
|
||||
AnalysisChartCard,
|
||||
AnalysisChartsTabs,
|
||||
AnalysisOverview,
|
||||
} from '@vben/common-ui';
|
||||
import {
|
||||
SvgBellIcon,
|
||||
SvgCakeIcon,
|
||||
SvgCardIcon,
|
||||
SvgDownloadIcon,
|
||||
} from '@vben/icons';
|
||||
|
||||
import AnalyticsTrends from './analytics-trends.vue';
|
||||
import AnalyticsVisitsData from './analytics-visits-data.vue';
|
||||
import AnalyticsVisitsSales from './analytics-visits-sales.vue';
|
||||
import AnalyticsVisitsSource from './analytics-visits-source.vue';
|
||||
import AnalyticsVisits from './analytics-visits.vue';
|
||||
|
||||
const overviewItems: AnalysisOverviewItem[] = [
|
||||
{
|
||||
icon: SvgCardIcon,
|
||||
title: '用户量',
|
||||
totalTitle: '总用户量',
|
||||
totalValue: 120_000,
|
||||
value: 2000,
|
||||
},
|
||||
{
|
||||
icon: SvgCakeIcon,
|
||||
title: '访问量',
|
||||
totalTitle: '总访问量',
|
||||
totalValue: 500_000,
|
||||
value: 20_000,
|
||||
},
|
||||
{
|
||||
icon: SvgDownloadIcon,
|
||||
title: '下载量',
|
||||
totalTitle: '总下载量',
|
||||
totalValue: 120_000,
|
||||
value: 8000,
|
||||
},
|
||||
{
|
||||
icon: SvgBellIcon,
|
||||
title: '使用量',
|
||||
totalTitle: '总使用量',
|
||||
totalValue: 50_000,
|
||||
value: 5000,
|
||||
},
|
||||
];
|
||||
|
||||
const chartTabs: TabOption[] = [
|
||||
{
|
||||
label: '流量趋势',
|
||||
value: 'trends',
|
||||
},
|
||||
{
|
||||
label: '月访问量',
|
||||
value: 'visits',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-5">
|
||||
<AnalysisOverview :items="overviewItems" />
|
||||
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
|
||||
<template #trends>
|
||||
<AnalyticsTrends />
|
||||
</template>
|
||||
<template #visits>
|
||||
<AnalyticsVisits />
|
||||
</template>
|
||||
</AnalysisChartsTabs>
|
||||
|
||||
<div class="mt-5 w-full md:flex">
|
||||
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问数量">
|
||||
<AnalyticsVisitsData />
|
||||
</AnalysisChartCard>
|
||||
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问来源">
|
||||
<AnalyticsVisitsSource />
|
||||
</AnalysisChartCard>
|
||||
<AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源">
|
||||
<AnalyticsVisitsSales />
|
||||
</AnalysisChartCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
266
web/apps/web-antd/src/views/dashboard/workspace/index.vue
Normal file
266
web/apps/web-antd/src/views/dashboard/workspace/index.vue
Normal file
@@ -0,0 +1,266 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
WorkbenchProjectItem,
|
||||
WorkbenchQuickNavItem,
|
||||
WorkbenchTodoItem,
|
||||
WorkbenchTrendItem,
|
||||
} from '@vben/common-ui';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import {
|
||||
AnalysisChartCard,
|
||||
WorkbenchHeader,
|
||||
WorkbenchProject,
|
||||
WorkbenchQuickNav,
|
||||
WorkbenchTodo,
|
||||
WorkbenchTrends,
|
||||
} from '@vben/common-ui';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import { openWindow } from '@vben/utils';
|
||||
|
||||
import AnalyticsVisitsSource from '../analytics/analytics-visits-source.vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 这是一个示例数据,实际项目中需要根据实际情况进行调整
|
||||
// url 也可以是内部路由,在 navTo 方法中识别处理,进行内部跳转
|
||||
// 例如:url: /dashboard/workspace
|
||||
const projectItems: WorkbenchProjectItem[] = [
|
||||
{
|
||||
color: '',
|
||||
content: '不要等待机会,而要创造机会。',
|
||||
date: '2021-04-01',
|
||||
group: '开源组',
|
||||
icon: 'carbon:logo-github',
|
||||
title: 'Github',
|
||||
url: 'https://github.com',
|
||||
},
|
||||
{
|
||||
color: '#3fb27f',
|
||||
content: '现在的你决定将来的你。',
|
||||
date: '2021-04-01',
|
||||
group: '算法组',
|
||||
icon: 'ion:logo-vue',
|
||||
title: 'Vue',
|
||||
url: 'https://vuejs.org',
|
||||
},
|
||||
{
|
||||
color: '#e18525',
|
||||
content: '没有什么才能比努力更重要。',
|
||||
date: '2021-04-01',
|
||||
group: '上班摸鱼',
|
||||
icon: 'ion:logo-html5',
|
||||
title: 'Html5',
|
||||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/HTML',
|
||||
},
|
||||
{
|
||||
color: '#bf0c2c',
|
||||
content: '热情和欲望可以突破一切难关。',
|
||||
date: '2021-04-01',
|
||||
group: 'UI',
|
||||
icon: 'ion:logo-angular',
|
||||
title: 'Angular',
|
||||
url: 'https://angular.io',
|
||||
},
|
||||
{
|
||||
color: '#00d8ff',
|
||||
content: '健康的身体是实现目标的基石。',
|
||||
date: '2021-04-01',
|
||||
group: '技术牛',
|
||||
icon: 'bx:bxl-react',
|
||||
title: 'React',
|
||||
url: 'https://reactjs.org',
|
||||
},
|
||||
{
|
||||
color: '#EBD94E',
|
||||
content: '路是走出来的,而不是空想出来的。',
|
||||
date: '2021-04-01',
|
||||
group: '架构组',
|
||||
icon: 'ion:logo-javascript',
|
||||
title: 'Js',
|
||||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript',
|
||||
},
|
||||
];
|
||||
|
||||
// 同样,这里的 url 也可以使用以 http 开头的外部链接
|
||||
const quickNavItems: WorkbenchQuickNavItem[] = [
|
||||
{
|
||||
color: '#1fdaca',
|
||||
icon: 'ion:home-outline',
|
||||
title: '首页',
|
||||
url: '/',
|
||||
},
|
||||
{
|
||||
color: '#bf0c2c',
|
||||
icon: 'ion:grid-outline',
|
||||
title: '仪表盘',
|
||||
url: '/dashboard',
|
||||
},
|
||||
{
|
||||
color: '#e18525',
|
||||
icon: 'ion:layers-outline',
|
||||
title: '组件',
|
||||
url: '/demos/features/icons',
|
||||
},
|
||||
{
|
||||
color: '#3fb27f',
|
||||
icon: 'ion:settings-outline',
|
||||
title: '系统管理',
|
||||
url: '/demos/features/login-expired', // 这里的 URL 是示例,实际项目中需要根据实际情况进行调整
|
||||
},
|
||||
{
|
||||
color: '#4daf1bc9',
|
||||
icon: 'ion:key-outline',
|
||||
title: '权限管理',
|
||||
url: '/demos/access/page-control',
|
||||
},
|
||||
{
|
||||
color: '#00d8ff',
|
||||
icon: 'ion:bar-chart-outline',
|
||||
title: '图表',
|
||||
url: '/analytics',
|
||||
},
|
||||
];
|
||||
|
||||
const todoItems = ref<WorkbenchTodoItem[]>([
|
||||
{
|
||||
completed: false,
|
||||
content: `审查最近提交到Git仓库的前端代码,确保代码质量和规范。`,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '审查前端代码提交',
|
||||
},
|
||||
{
|
||||
completed: true,
|
||||
content: `检查并优化系统性能,降低CPU使用率。`,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '系统性能优化',
|
||||
},
|
||||
{
|
||||
completed: false,
|
||||
content: `进行系统安全检查,确保没有安全漏洞或未授权的访问。 `,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '安全检查',
|
||||
},
|
||||
{
|
||||
completed: false,
|
||||
content: `更新项目中的所有npm依赖包,确保使用最新版本。`,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '更新项目依赖',
|
||||
},
|
||||
{
|
||||
completed: false,
|
||||
content: `修复用户报告的页面UI显示问题,确保在不同浏览器中显示一致。 `,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '修复UI显示问题',
|
||||
},
|
||||
]);
|
||||
const trendItems: WorkbenchTrendItem[] = [
|
||||
{
|
||||
avatar: 'svg:avatar-1',
|
||||
content: `在 <a>开源组</a> 创建了项目 <a>Vue</a>`,
|
||||
date: '刚刚',
|
||||
title: '威廉',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-2',
|
||||
content: `关注了 <a>威廉</a> `,
|
||||
date: '1个小时前',
|
||||
title: '艾文',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-3',
|
||||
content: `发布了 <a>个人动态</a> `,
|
||||
date: '1天前',
|
||||
title: '克里斯',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: `发表文章 <a>如何编写一个Vite插件</a> `,
|
||||
date: '2天前',
|
||||
title: 'Vben',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-1',
|
||||
content: `回复了 <a>杰克</a> 的问题 <a>如何进行项目优化?</a>`,
|
||||
date: '3天前',
|
||||
title: '皮特',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-2',
|
||||
content: `关闭了问题 <a>如何运行项目</a> `,
|
||||
date: '1周前',
|
||||
title: '杰克',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-3',
|
||||
content: `发布了 <a>个人动态</a> `,
|
||||
date: '1周前',
|
||||
title: '威廉',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: `推送了代码到 <a>Github</a>`,
|
||||
date: '2021-04-01 20:00',
|
||||
title: '威廉',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: `发表文章 <a>如何编写使用 Admin Vben</a> `,
|
||||
date: '2021-03-01 20:00',
|
||||
title: 'Vben',
|
||||
},
|
||||
];
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 这是一个示例方法,实际项目中需要根据实际情况进行调整
|
||||
// This is a sample method, adjust according to the actual project requirements
|
||||
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
||||
if (nav.url?.startsWith('http')) {
|
||||
openWindow(nav.url);
|
||||
return;
|
||||
}
|
||||
if (nav.url?.startsWith('/')) {
|
||||
router.push(nav.url).catch((error) => {
|
||||
console.error('Navigation failed:', error);
|
||||
});
|
||||
} else {
|
||||
console.warn(`Unknown URL for navigation item: ${nav.title} -> ${nav.url}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-5">
|
||||
<WorkbenchHeader
|
||||
:avatar="userStore.userInfo?.avatar || preferences.app.defaultAvatar"
|
||||
>
|
||||
<template #title>
|
||||
早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧!
|
||||
</template>
|
||||
<template #description> 今日晴,20℃ - 32℃! </template>
|
||||
</WorkbenchHeader>
|
||||
|
||||
<div class="mt-5 flex flex-col lg:flex-row">
|
||||
<div class="mr-4 w-full lg:w-3/5">
|
||||
<WorkbenchProject :items="projectItems" title="项目" @click="navTo" />
|
||||
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" />
|
||||
</div>
|
||||
<div class="w-full lg:w-2/5">
|
||||
<WorkbenchQuickNav
|
||||
:items="quickNavItems"
|
||||
class="mt-5 lg:mt-0"
|
||||
title="快捷导航"
|
||||
@click="navTo"
|
||||
/>
|
||||
<WorkbenchTodo :items="todoItems" class="mt-5" title="待办事项" />
|
||||
<AnalysisChartCard class="mt-5" title="访问来源">
|
||||
<AnalyticsVisitsSource />
|
||||
</AnalysisChartCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
66
web/apps/web-antd/src/views/demos/antd/index.vue
Normal file
66
web/apps/web-antd/src/views/demos/antd/index.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script lang="ts" setup>
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Button, Card, message, notification, Space } from 'ant-design-vue';
|
||||
|
||||
type NotificationType = 'error' | 'info' | 'success' | 'warning';
|
||||
|
||||
function info() {
|
||||
message.info('How many roads must a man walk down');
|
||||
}
|
||||
|
||||
function error() {
|
||||
message.error({
|
||||
content: 'Once upon a time you dressed so fine',
|
||||
duration: 2500,
|
||||
});
|
||||
}
|
||||
|
||||
function warning() {
|
||||
message.warning('How many roads must a man walk down');
|
||||
}
|
||||
function success() {
|
||||
message.success('Cause you walked hand in hand With another man in my place');
|
||||
}
|
||||
|
||||
function notify(type: NotificationType) {
|
||||
notification[type]({
|
||||
duration: 2500,
|
||||
message: '说点啥呢',
|
||||
type,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page
|
||||
description="支持多语言,主题功能集成切换等"
|
||||
title="Ant Design Vue组件使用演示"
|
||||
>
|
||||
<Card class="mb-5" title="按钮">
|
||||
<Space>
|
||||
<Button>Default</Button>
|
||||
<Button type="primary"> Primary </Button>
|
||||
<Button> Info </Button>
|
||||
<Button danger> Error </Button>
|
||||
</Space>
|
||||
</Card>
|
||||
<Card class="mb-5" title="Message">
|
||||
<Space>
|
||||
<Button @click="info"> 信息 </Button>
|
||||
<Button danger @click="error"> 错误 </Button>
|
||||
<Button @click="warning"> 警告 </Button>
|
||||
<Button @click="success"> 成功 </Button>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card class="mb-5" title="Notification">
|
||||
<Space>
|
||||
<Button @click="notify('info')"> 信息 </Button>
|
||||
<Button danger @click="notify('error')"> 错误 </Button>
|
||||
<Button @click="notify('warning')"> 警告 </Button>
|
||||
<Button @click="notify('success')"> 成功 </Button>
|
||||
</Space>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
161
web/apps/web-antd/src/views/system/dept/data.ts
Normal file
161
web/apps/web-antd/src/views/system/dept/data.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
|
||||
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn } from '#/adapter/vxe-table';
|
||||
import type { SystemDeptApi } from '#/api/system/dept';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getDeptList } from '#/api/system/dept';
|
||||
import { $t } from '#/locales';
|
||||
import { format_datetime } from '#/utils/date';
|
||||
/**
|
||||
* 获取编辑表单的字段配置。如果没有使用多语言,可以直接export一个数组常量
|
||||
*/
|
||||
export function useSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
api: getDeptList,
|
||||
class: 'w-full',
|
||||
resultField: 'items',
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
},
|
||||
fieldName: 'pid',
|
||||
label: $t('system.dept.parentDept'),
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: $t('system.dept.deptName'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(2, $t('ui.formRules.minLength', [$t('system.dept.deptName'), 2]))
|
||||
.max(
|
||||
20,
|
||||
$t('ui.formRules.maxLength', [$t('system.dept.deptName'), 20]),
|
||||
),
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'sort',
|
||||
label: '显示排序',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'leader',
|
||||
label: '负责人',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'phone',
|
||||
label: '联系电话',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'email',
|
||||
label: '邮箱',
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
buttonStyle: 'solid',
|
||||
options: [
|
||||
{ label: $t('common.enabled'), value: 1 },
|
||||
{ label: $t('common.disabled'), value: 0 },
|
||||
],
|
||||
optionType: 'button',
|
||||
},
|
||||
defaultValue: 1,
|
||||
fieldName: 'status',
|
||||
label: $t('system.dept.status'),
|
||||
},
|
||||
{
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
maxLength: 50,
|
||||
rows: 3,
|
||||
showCount: true,
|
||||
},
|
||||
fieldName: 'remark',
|
||||
label: $t('system.dept.remark'),
|
||||
rules: z
|
||||
.string()
|
||||
.max(50, $t('ui.formRules.maxLength', [$t('system.dept.remark'), 50]))
|
||||
.optional(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取表格列配置
|
||||
* @description 使用函数的形式返回列数据而不是直接export一个Array常量,是为了响应语言切换时重新翻译表头
|
||||
* @param onActionClick 表格操作按钮点击事件
|
||||
*/
|
||||
export function useColumns(
|
||||
onActionClick?: OnActionClickFn<SystemDeptApi.SystemDept>,
|
||||
): VxeTableGridOptions<SystemDeptApi.SystemDept>['columns'] {
|
||||
return [
|
||||
{
|
||||
align: 'left',
|
||||
field: 'name',
|
||||
fixed: 'left',
|
||||
title: $t('system.dept.deptName'),
|
||||
treeNode: true,
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
field: 'sort',
|
||||
title: '排序',
|
||||
},
|
||||
{
|
||||
cellRender: { name: 'CellTag' },
|
||||
field: 'status',
|
||||
title: $t('system.dept.status'),
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
field: 'create_time',
|
||||
title: $t('system.dept.createTime'),
|
||||
width: 180,
|
||||
formatter: ({ cellValue }) => format_datetime(cellValue),
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: $t('system.dept.remark'),
|
||||
},
|
||||
{
|
||||
align: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: $t('system.dept.name'),
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'append',
|
||||
text: '新增下级',
|
||||
},
|
||||
'edit', // 默认的编辑按钮
|
||||
{
|
||||
code: 'delete', // 默认的删除按钮
|
||||
disabled: (row: SystemDeptApi.SystemDept) => {
|
||||
return !!(row.children && row.children.length > 0);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
field: 'operation',
|
||||
fixed: 'right',
|
||||
headerAlign: 'center',
|
||||
showOverflow: false,
|
||||
title: $t('system.dept.operation'),
|
||||
width: 200,
|
||||
},
|
||||
];
|
||||
}
|
||||
143
web/apps/web-antd/src/views/system/dept/list.vue
Normal file
143
web/apps/web-antd/src/views/system/dept/list.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { SystemDeptApi } from '#/api/system/dept';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteDept, getDeptList } from '#/api/system/dept';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useColumns } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* 编辑部门
|
||||
* @param row
|
||||
*/
|
||||
function onEdit(row: SystemDeptApi.SystemDept) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加下级部门
|
||||
* @param row
|
||||
*/
|
||||
function onAppend(row: SystemDeptApi.SystemDept) {
|
||||
formModalApi.setData({ pid: row.id }).open();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新部门
|
||||
*/
|
||||
function onCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除部门
|
||||
* @param row
|
||||
*/
|
||||
function onDelete(row: SystemDeptApi.SystemDept) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
deleteDept(row.id)
|
||||
.then(() => {
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
refreshGrid();
|
||||
})
|
||||
.catch(() => {
|
||||
hideLoading();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 表格操作按钮的回调函数
|
||||
*/
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<SystemDeptApi.SystemDept>) {
|
||||
switch (code) {
|
||||
case 'append': {
|
||||
onAppend(row);
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridEvents: {},
|
||||
gridOptions: {
|
||||
columns: useColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async (_params) => {
|
||||
return await getDeptList();
|
||||
},
|
||||
},
|
||||
},
|
||||
toolbarConfig: {
|
||||
custom: true,
|
||||
export: false,
|
||||
refresh: { code: 'query' },
|
||||
zoom: true,
|
||||
},
|
||||
treeConfig: {
|
||||
parentField: 'pid',
|
||||
rowField: 'id',
|
||||
transform: false,
|
||||
},
|
||||
} as VxeTableGridOptions,
|
||||
});
|
||||
|
||||
/**
|
||||
* 刷新表格
|
||||
*/
|
||||
function refreshGrid() {
|
||||
gridApi.query();
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="refreshGrid" />
|
||||
<Grid table-title="部门列表">
|
||||
<template #toolbar-tools>
|
||||
<Button type="primary" @click="onCreate">
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', [$t('system.dept.name')]) }}
|
||||
</Button>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
80
web/apps/web-antd/src/views/system/dept/modules/form.vue
Normal file
80
web/apps/web-antd/src/views/system/dept/modules/form.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<script lang="ts" setup>
|
||||
import type { SystemDeptApi } from '#/api/system/dept';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { createDept, updateDept } from '#/api/system/dept';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useSchema } from '../data';
|
||||
|
||||
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core';
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<SystemDeptApi.SystemDept>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', [$t('system.dept.name')])
|
||||
: $t('ui.actionTitle.create', [$t('system.dept.name')]);
|
||||
});
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind);
|
||||
const isHorizontal = computed(() => breakpoints.greaterOrEqual('md').value);
|
||||
const [Form, formApi] = useVbenForm({
|
||||
layout: 'vertical',
|
||||
schema: useSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
function resetForm() {
|
||||
formApi.resetForm();
|
||||
formApi.setValues(formData.value || {});
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (valid) {
|
||||
modalApi.lock();
|
||||
const data = await formApi.getValues();
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateDept(formData.value.id, data)
|
||||
: createDept(data));
|
||||
modalApi.close();
|
||||
emit('success');
|
||||
} finally {
|
||||
modalApi.lock(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
onOpenChange(isOpen) {
|
||||
if (isOpen) {
|
||||
const data = modalApi.getData<SystemDeptApi.SystemDept>();
|
||||
if (data) {
|
||||
if (data.pid === 0) {
|
||||
data.pid = undefined;
|
||||
}
|
||||
formData.value = data;
|
||||
formApi.setValues(formData.value);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" :layout="isHorizontal ? 'horizontal' : 'vertical'" />
|
||||
<template #prepend-footer>
|
||||
<div class="flex-auto">
|
||||
<Button type="primary" danger @click="resetForm">
|
||||
{{ $t('common.reset') }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
214
web/apps/web-antd/src/views/system/dict_data/data.ts
Normal file
214
web/apps/web-antd/src/views/system/dict_data/data.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
|
||||
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn } from '#/adapter/vxe-table';
|
||||
import type { SystemDictDataApi } from '#/api/system/dict_data';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getDictTypeList } from '#/api/system/dict_type';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
/**
|
||||
* 获取编辑表单的字段配置。如果没有使用多语言,可以直接export一个数组常量
|
||||
*/
|
||||
export function useSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
api: getDictTypeList,
|
||||
class: 'w-full',
|
||||
resultField: 'items',
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
},
|
||||
fieldName: 'dict_type',
|
||||
label: '字典类型',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'label',
|
||||
label: '字典标签',
|
||||
rules: z
|
||||
.string()
|
||||
.min(2, $t('ui.formRules.minLength', [$t('system.dict_data.type'), 2]))
|
||||
.max(
|
||||
20,
|
||||
$t('ui.formRules.maxLength', [$t('system.dict_data.type'), 20]),
|
||||
),
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'value',
|
||||
label: '字典键值',
|
||||
rules: z
|
||||
.string()
|
||||
.min(2, $t('ui.formRules.minLength', [$t('system.dict_data.type'), 2]))
|
||||
.max(
|
||||
50,
|
||||
$t('ui.formRules.maxLength', [$t('system.dict_data.type'), 50]),
|
||||
),
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'sort',
|
||||
label: '字典排序',
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
fieldName: 'color_type',
|
||||
label: '颜色类型',
|
||||
componentProps: {
|
||||
name: 'CellTag',
|
||||
options: [
|
||||
{
|
||||
value: 'default',
|
||||
label: '默认',
|
||||
},
|
||||
{
|
||||
value: 'primary',
|
||||
label: '主要',
|
||||
},
|
||||
{
|
||||
value: 'success',
|
||||
label: '成功',
|
||||
},
|
||||
{
|
||||
value: 'info',
|
||||
label: '信息',
|
||||
},
|
||||
{
|
||||
value: 'warning',
|
||||
label: '警告',
|
||||
},
|
||||
{
|
||||
value: 'danger',
|
||||
label: '危险',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'css_class',
|
||||
label: 'CSS Class',
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
buttonStyle: 'solid',
|
||||
options: [
|
||||
{ label: '开启', value: true },
|
||||
{ label: '关闭', value: false },
|
||||
],
|
||||
optionType: 'button',
|
||||
},
|
||||
defaultValue: 1,
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 50,
|
||||
rows: 3,
|
||||
showCount: true,
|
||||
},
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
rules: z
|
||||
.string()
|
||||
.max(50, $t('ui.formRules.maxLength', [$t('system.remark'), 50]))
|
||||
.optional(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取表格列配置
|
||||
* @description 使用函数的形式返回列数据而不是直接export一个Array常量,是为了响应语言切换时重新翻译表头
|
||||
* @param onActionClick 表格操作按钮点击事件
|
||||
*/
|
||||
export function useColumns(
|
||||
onActionClick?: OnActionClickFn<SystemDictDataApi.SystemDictData>,
|
||||
): VxeTableGridOptions<SystemDictDataApi.SystemDictData>['columns'] {
|
||||
return [
|
||||
{
|
||||
align: 'left',
|
||||
field: 'id',
|
||||
fixed: 'left',
|
||||
title: '字典编码',
|
||||
treeNode: true,
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
field: 'label',
|
||||
title: '字典标签',
|
||||
},
|
||||
{
|
||||
field: 'value',
|
||||
title: '字典键值',
|
||||
},
|
||||
{
|
||||
field: 'sort',
|
||||
title: '字典排序',
|
||||
},
|
||||
{
|
||||
field: 'color_type',
|
||||
title: '颜色类型',
|
||||
|
||||
},
|
||||
{
|
||||
field: 'css_class',
|
||||
title: 'CSS Class',
|
||||
},
|
||||
{
|
||||
cellRender: {
|
||||
name: 'CellTag',
|
||||
options: [
|
||||
{ label: $t('common.enabled'), value: true },
|
||||
{ label: $t('common.disabled'), value: false },
|
||||
],
|
||||
},
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
},
|
||||
{
|
||||
field: 'create_time',
|
||||
title: '创建时间',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
align: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: $t('system.dict_data.name'),
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
'edit', // 默认的编辑按钮
|
||||
{
|
||||
code: 'delete', // 默认的删除按钮
|
||||
disabled: (row: SystemDictDataApi.SystemDictData) => {
|
||||
return !!(row.children && row.children.length > 0);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
field: 'operation',
|
||||
fixed: 'right',
|
||||
headerAlign: 'center',
|
||||
showOverflow: false,
|
||||
title: '操作',
|
||||
width: 200,
|
||||
},
|
||||
];
|
||||
}
|
||||
143
web/apps/web-antd/src/views/system/dict_data/list.vue
Normal file
143
web/apps/web-antd/src/views/system/dict_data/list.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { SystemDictDataApi } from '#/api/system/dict_data';
|
||||
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteDictData, getDictDataList } from '#/api/system/dict_data';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useColumns } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* 编辑套餐
|
||||
* @param row
|
||||
*/
|
||||
function onEdit(row: SystemDictDataApi.SystemDictData) {
|
||||
if (row.menu_ids) {
|
||||
row.menu_ids = row.menu_ids.split(',').map(Number);
|
||||
}
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新套餐
|
||||
*/
|
||||
function onCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除套餐
|
||||
* @param row
|
||||
*/
|
||||
function onDelete(row: SystemDictDataApi.SystemDictData) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
deleteDictData(row.id)
|
||||
.then(() => {
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
refreshGrid();
|
||||
})
|
||||
.catch(() => {
|
||||
hideLoading();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 表格操作按钮的回调函数
|
||||
*/
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<SystemDictDataApi.SystemDictData>) {
|
||||
switch (code) {
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridEvents: {},
|
||||
gridOptions: {
|
||||
columns: useColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
const { dict_type } = route.query;
|
||||
return await getDictDataList({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
dict_type,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
toolbarConfig: {
|
||||
custom: true,
|
||||
export: false,
|
||||
refresh: { code: 'query' },
|
||||
zoom: true,
|
||||
},
|
||||
treeConfig: {
|
||||
parentField: 'pid',
|
||||
rowField: 'id',
|
||||
transform: false,
|
||||
},
|
||||
} as VxeTableGridOptions,
|
||||
});
|
||||
|
||||
/**
|
||||
* 刷新表格
|
||||
*/
|
||||
function refreshGrid() {
|
||||
gridApi.query();
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="refreshGrid" />
|
||||
<Grid table-title="字典数据">
|
||||
<template #toolbar-tools>
|
||||
<Button type="primary" @click="onCreate">
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', [$t('system.dict_data.name')]) }}
|
||||
</Button>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,82 @@
|
||||
<script lang="ts" setup>
|
||||
import type { SystemDictDataApi } from '#/api/system/dict_data';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { createDictData, updateDictData } from '#/api/system/dict_data';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<SystemDictDataApi.SystemDictData>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', [$t('system.dict_data.name')])
|
||||
: $t('ui.actionTitle.create', [$t('system.dict_data.name')]);
|
||||
});
|
||||
const route = useRoute();
|
||||
const [Form, formApi] = useVbenForm({
|
||||
layout: 'vertical',
|
||||
schema: useSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
function resetForm() {
|
||||
formApi.resetForm();
|
||||
formApi.setValues(formData.value || {});
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (valid) {
|
||||
modalApi.lock();
|
||||
const data = await formApi.getValues();
|
||||
const { dict_type } = route.query;
|
||||
data.dict_type = dict_type;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateDictData(formData.value.id, data)
|
||||
: createDictData(data));
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
} finally {
|
||||
modalApi.lock(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
onOpenChange(isOpen) {
|
||||
if (isOpen) {
|
||||
const data = modalApi.getData<SystemDictDataApi.SystemDictData>();
|
||||
if (data) {
|
||||
if (data.pid === 0) {
|
||||
data.pid = undefined;
|
||||
}
|
||||
formData.value = data;
|
||||
formApi.setValues(formData.value);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form />
|
||||
<template #prepend-footer>
|
||||
<div class="flex-auto">
|
||||
<Button type="primary" danger @click="resetForm">
|
||||
{{ $t('common.reset') }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
<style lang="css" scoped></style>
|
||||
148
web/apps/web-antd/src/views/system/dict_type/data.ts
Normal file
148
web/apps/web-antd/src/views/system/dict_type/data.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
|
||||
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn } from '#/adapter/vxe-table';
|
||||
import type { SystemDictTypeApi } from '#/api/system/dict_type';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
/**
|
||||
* 获取编辑表单的字段配置。如果没有使用多语言,可以直接export一个数组常量
|
||||
*/
|
||||
export function useSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: '字典名称',
|
||||
rules: z
|
||||
.string()
|
||||
.min(2, $t('ui.formRules.minLength', [$t('system.dict_type.name'), 2]))
|
||||
.max(
|
||||
20,
|
||||
$t('ui.formRules.maxLength', [$t('system.dict_type.name'), 20]),
|
||||
),
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'type',
|
||||
label: '字典类型',
|
||||
rules: z
|
||||
.string()
|
||||
.min(2, $t('ui.formRules.minLength', [$t('system.dict_type.type'), 2]))
|
||||
.max(
|
||||
20,
|
||||
$t('ui.formRules.maxLength', [$t('system.dict_type.type'), 20]),
|
||||
),
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
buttonStyle: 'solid',
|
||||
options: [
|
||||
{ label: '开启', value: true },
|
||||
{ label: '关闭', value: false },
|
||||
],
|
||||
optionType: 'button',
|
||||
},
|
||||
defaultValue: 1,
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 50,
|
||||
rows: 3,
|
||||
showCount: true,
|
||||
},
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
rules: z
|
||||
.string()
|
||||
.max(50, $t('ui.formRules.maxLength', [$t('system.remark'), 50]))
|
||||
.optional(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取表格列配置
|
||||
* @description 使用函数的形式返回列数据而不是直接export一个Array常量,是为了响应语言切换时重新翻译表头
|
||||
* @param onActionClick 表格操作按钮点击事件
|
||||
*/
|
||||
export function useColumns(
|
||||
onActionClick?: OnActionClickFn<SystemDictTypeApi.SystemDictType>,
|
||||
): VxeTableGridOptions<SystemDictTypeApi.SystemDictType>['columns'] {
|
||||
return [
|
||||
{
|
||||
align: 'left',
|
||||
field: 'id',
|
||||
fixed: 'left',
|
||||
title: '字典编号',
|
||||
treeNode: true,
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: '字典名称',
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
title: '字典类型',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
cellRender: {
|
||||
name: 'CellTag',
|
||||
options: [
|
||||
{ label: $t('common.enabled'), value: true },
|
||||
{ label: $t('common.disabled'), value: false },
|
||||
],
|
||||
},
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
},
|
||||
{
|
||||
field: 'create_time',
|
||||
title: '创建时间',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
align: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: $t('system.dict_type.name'),
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
'edit', // 默认的编辑按钮
|
||||
{
|
||||
code: 'view', // 新增查看详情按钮(可自定义code)
|
||||
text: '数据', // 按钮文本(国际化)
|
||||
},
|
||||
{
|
||||
code: 'delete', // 默认的删除按钮
|
||||
disabled: (row: SystemDictTypeApi.SystemDictType) => {
|
||||
return !!(row.children && row.children.length > 0);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
field: 'operation',
|
||||
fixed: 'right',
|
||||
headerAlign: 'center',
|
||||
showOverflow: false,
|
||||
title: '操作',
|
||||
width: 200,
|
||||
},
|
||||
];
|
||||
}
|
||||
150
web/apps/web-antd/src/views/system/dict_type/list.vue
Normal file
150
web/apps/web-antd/src/views/system/dict_type/list.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { SystemDictTypeApi } from '#/api/system/dict_type';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteDictType, getDictTypeList } from '#/api/system/dict_type';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useColumns } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* 编辑字典
|
||||
* @param row
|
||||
*/
|
||||
function onEdit(row: SystemDictTypeApi.SystemDictType) {
|
||||
if (row.menu_ids) {
|
||||
row.menu_ids = row.menu_ids.split(',').map(Number);
|
||||
}
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新字典
|
||||
*/
|
||||
function onCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除字典
|
||||
* @param row
|
||||
*/
|
||||
function onDelete(row: SystemDictTypeApi.SystemDictType) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
deleteDictType(row.id)
|
||||
.then(() => {
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
refreshGrid();
|
||||
})
|
||||
.catch(() => {
|
||||
hideLoading();
|
||||
});
|
||||
}
|
||||
const handleViewDetail = (row: SystemDictTypeApi.SystemDictType) => {
|
||||
router.push({
|
||||
path: '/system/dict_data/', // 目标页面路径
|
||||
query: { dict_type: row.id }, // 传递查询参数
|
||||
});
|
||||
};
|
||||
/**
|
||||
* 表格操作按钮的回调函数
|
||||
*/
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<SystemDictTypeApi.SystemDictType>) {
|
||||
switch (code) {
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
case 'view': {
|
||||
handleViewDetail(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridEvents: {},
|
||||
gridOptions: {
|
||||
columns: useColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getDictTypeList({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
toolbarConfig: {
|
||||
custom: true,
|
||||
export: false,
|
||||
refresh: { code: 'query' },
|
||||
zoom: true,
|
||||
},
|
||||
treeConfig: {
|
||||
parentField: 'pid',
|
||||
rowField: 'id',
|
||||
transform: false,
|
||||
},
|
||||
} as VxeTableGridOptions,
|
||||
});
|
||||
|
||||
/**
|
||||
* 刷新表格
|
||||
*/
|
||||
function refreshGrid() {
|
||||
gridApi.query();
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="refreshGrid" />
|
||||
<Grid table-title="字典列表">
|
||||
<template #toolbar-tools>
|
||||
<Button type="primary" @click="onCreate">
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', [$t('system.dict_type.name')]) }}
|
||||
</Button>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,79 @@
|
||||
<script lang="ts" setup>
|
||||
import type { SystemDictTypeApi } from '#/api/system/dict_type';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { createDictType, updateDictType } from '#/api/system/dict_type';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<SystemDictTypeApi.SystemDictType>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', [$t('system.dict_type.name')])
|
||||
: $t('ui.actionTitle.create', [$t('system.dict_type.name')]);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
layout: 'vertical',
|
||||
schema: useSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
function resetForm() {
|
||||
formApi.resetForm();
|
||||
formApi.setValues(formData.value || {});
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (valid) {
|
||||
modalApi.lock();
|
||||
const data = await formApi.getValues();
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateDictType(formData.value.id, data)
|
||||
: createDictType(data));
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
} finally {
|
||||
modalApi.lock(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
onOpenChange(isOpen) {
|
||||
if (isOpen) {
|
||||
const data = modalApi.getData<SystemDictTypeApi.SystemDictType>();
|
||||
if (data) {
|
||||
if (data.pid === 0) {
|
||||
data.pid = undefined;
|
||||
}
|
||||
formData.value = data;
|
||||
formApi.setValues(formData.value);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form />
|
||||
<template #prepend-footer>
|
||||
<div class="flex-auto">
|
||||
<Button type="primary" danger @click="resetForm">
|
||||
{{ $t('common.reset') }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
<style lang="css" scoped></style>
|
||||
109
web/apps/web-antd/src/views/system/menu/data.ts
Normal file
109
web/apps/web-antd/src/views/system/menu/data.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemMenuApi } from '#/api/system/menu';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
export function getMenuTypeOptions() {
|
||||
return [
|
||||
{
|
||||
color: 'processing',
|
||||
label: $t('system.menu.typeCatalog'),
|
||||
value: 'catalog',
|
||||
},
|
||||
{ color: 'default', label: $t('system.menu.typeMenu'), value: 'menu' },
|
||||
{ color: 'error', label: $t('system.menu.typeButton'), value: 'button' },
|
||||
{
|
||||
color: 'success',
|
||||
label: $t('system.menu.typeEmbedded'),
|
||||
value: 'embedded',
|
||||
},
|
||||
{ color: 'warning', label: $t('system.menu.typeLink'), value: 'link' },
|
||||
];
|
||||
}
|
||||
|
||||
export function useColumns(
|
||||
onActionClick: OnActionClickFn<SystemMenuApi.SystemMenu>,
|
||||
): VxeTableGridOptions<SystemMenuApi.SystemMenu>['columns'] {
|
||||
return [
|
||||
{
|
||||
align: 'left',
|
||||
field: 'meta.title',
|
||||
fixed: 'left',
|
||||
slots: { default: 'title' },
|
||||
title: $t('system.menu.menuTitle'),
|
||||
treeNode: true,
|
||||
width: 250,
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
cellRender: { name: 'CellTag', options: getMenuTypeOptions() },
|
||||
field: 'type',
|
||||
title: $t('system.menu.type'),
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
field: 'authCode',
|
||||
title: $t('system.menu.authCode'),
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
align: 'left',
|
||||
field: 'path',
|
||||
title: $t('system.menu.path'),
|
||||
width: 200,
|
||||
},
|
||||
|
||||
{
|
||||
align: 'left',
|
||||
field: 'component',
|
||||
formatter: ({ row }) => {
|
||||
switch (row.type) {
|
||||
case 'catalog':
|
||||
case 'menu': {
|
||||
return row.component ?? '';
|
||||
}
|
||||
case 'embedded': {
|
||||
return row.meta?.iframeSrc ?? '';
|
||||
}
|
||||
case 'link': {
|
||||
return row.meta?.link ?? '';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
},
|
||||
minWidth: 200,
|
||||
title: $t('system.menu.component'),
|
||||
},
|
||||
{
|
||||
cellRender: { name: 'CellTag' },
|
||||
field: 'status',
|
||||
title: $t('system.menu.status'),
|
||||
width: 100,
|
||||
},
|
||||
|
||||
{
|
||||
align: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
{
|
||||
code: 'append',
|
||||
text: '新增下级',
|
||||
},
|
||||
'edit', // 默认的编辑按钮
|
||||
'delete', // 默认的删除按钮
|
||||
],
|
||||
},
|
||||
field: 'operation',
|
||||
fixed: 'right',
|
||||
headerAlign: 'center',
|
||||
showOverflow: false,
|
||||
title: $t('system.menu.operation'),
|
||||
width: 200,
|
||||
},
|
||||
];
|
||||
}
|
||||
162
web/apps/web-antd/src/views/system/menu/list.vue
Normal file
162
web/apps/web-antd/src/views/system/menu/list.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
|
||||
import { Page, useVbenDrawer } from '@vben/common-ui';
|
||||
import { IconifyIcon, Plus } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { MenuBadge } from '@vben-core/menu-ui';
|
||||
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteMenu, getMenuList, SystemMenuApi } from '#/api/system/menu';
|
||||
|
||||
import { useColumns } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormDrawer, formDrawerApi] = useVbenDrawer({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {
|
||||
columns: useColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async (_params) => {
|
||||
return await getMenuList();
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
custom: true,
|
||||
export: false,
|
||||
refresh: { code: 'query' },
|
||||
zoom: true,
|
||||
},
|
||||
treeConfig: {
|
||||
parentField: 'pid',
|
||||
rowField: 'id',
|
||||
transform: false,
|
||||
},
|
||||
} as VxeTableGridOptions,
|
||||
});
|
||||
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<SystemMenuApi.SystemMenu>) {
|
||||
switch (code) {
|
||||
case 'append': {
|
||||
onAppend(row);
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
function onEdit(row: SystemMenuApi.SystemMenu) {
|
||||
formDrawerApi.setData(row).open();
|
||||
}
|
||||
function onCreate() {
|
||||
formDrawerApi.setData({}).open();
|
||||
}
|
||||
function onAppend(row: SystemMenuApi.SystemMenu) {
|
||||
formDrawerApi.setData({ pid: row.id }).open();
|
||||
}
|
||||
|
||||
function onDelete(row: SystemMenuApi.SystemMenu) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
deleteMenu(row.id)
|
||||
.then(() => {
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
onRefresh();
|
||||
})
|
||||
.catch(() => {
|
||||
hideLoading();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormDrawer @success="onRefresh" />
|
||||
<Grid>
|
||||
<template #toolbar-tools>
|
||||
<Button type="primary" @click="onCreate">
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', [$t('system.menu.name')]) }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #title="{ row }">
|
||||
<div class="flex w-full items-center gap-1">
|
||||
<div class="size-5 flex-shrink-0">
|
||||
<IconifyIcon
|
||||
v-if="row.type === 'button'"
|
||||
icon="carbon:security"
|
||||
class="size-full"
|
||||
/>
|
||||
<IconifyIcon
|
||||
v-else-if="row.meta?.icon"
|
||||
:icon="row.meta?.icon || 'carbon:circle-dash'"
|
||||
class="size-full"
|
||||
/>
|
||||
</div>
|
||||
<span class="flex-auto">{{ $t(row.meta?.title) }}</span>
|
||||
<div class="items-center justify-end"></div>
|
||||
</div>
|
||||
<MenuBadge
|
||||
v-if="row.meta?.badgeType"
|
||||
class="menu-badge"
|
||||
:badge="row.meta.badge"
|
||||
:badge-type="row.meta.badgeType"
|
||||
:badge-variants="row.meta.badgeVariants"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.menu-badge {
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translateY(-50%);
|
||||
|
||||
& > :deep(div) {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
506
web/apps/web-antd/src/views/system/menu/modules/form.vue
Normal file
506
web/apps/web-antd/src/views/system/menu/modules/form.vue
Normal file
@@ -0,0 +1,506 @@
|
||||
<script lang="ts" setup>
|
||||
import type { ChangeEvent } from 'ant-design-vue/es/_util/EventInterface';
|
||||
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
|
||||
import { computed, h, ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { $te } from '@vben/locales';
|
||||
import { getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core';
|
||||
|
||||
import { useVbenForm, z } from '#/adapter/form';
|
||||
import {
|
||||
createMenu,
|
||||
getMenuList,
|
||||
isMenuPathExists,
|
||||
isMenuSearchExists,
|
||||
SystemMenuApi,
|
||||
updateMenu,
|
||||
} from '#/api/system/menu';
|
||||
import { $t } from '#/locales';
|
||||
import { componentKeys } from '#/router/routes';
|
||||
|
||||
import { getMenuTypeOptions } from '../data';
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: [];
|
||||
}>();
|
||||
const formData = ref<SystemMenuApi.SystemMenu>();
|
||||
const titleSuffix = ref<string>();
|
||||
const schema: VbenFormSchema[] = [
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
buttonStyle: 'solid',
|
||||
options: getMenuTypeOptions(),
|
||||
optionType: 'button',
|
||||
},
|
||||
defaultValue: 'menu',
|
||||
fieldName: 'type',
|
||||
formItemClass: 'col-span-2 md:col-span-2',
|
||||
label: $t('system.menu.type'),
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: $t('system.menu.menuName'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(2, $t('ui.formRules.minLength', [$t('system.menu.menuName'), 2]))
|
||||
.max(30, $t('ui.formRules.maxLength', [$t('system.menu.menuName'), 30]))
|
||||
.refine(
|
||||
async (value: string) => {
|
||||
return !(await isMenuSearchExists(value, formData.value?.id));
|
||||
},
|
||||
(value) => ({
|
||||
message: $t('ui.formRules.alreadyExists', [
|
||||
$t('system.menu.menuName'),
|
||||
value,
|
||||
]),
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
api: getMenuList,
|
||||
class: 'w-full',
|
||||
filterTreeNode(input: string, node: Recordable<any>) {
|
||||
if (!input || input.length === 0) {
|
||||
return true;
|
||||
}
|
||||
const title: string = node.meta?.title ?? '';
|
||||
if (!title) return false;
|
||||
return title.includes(input) || $t(title).includes(input);
|
||||
},
|
||||
getPopupContainer,
|
||||
resultField: 'items',
|
||||
labelField: 'meta.title',
|
||||
showSearch: true,
|
||||
treeDefaultExpandAll: true,
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
},
|
||||
fieldName: 'pid',
|
||||
label: $t('system.menu.parent'),
|
||||
renderComponentContent() {
|
||||
return {
|
||||
title({ label, meta }: { label: string; meta: Recordable<any> }) {
|
||||
const coms = [];
|
||||
if (!label) return '';
|
||||
if (meta?.icon) {
|
||||
coms.push(h(IconifyIcon, { class: 'size-4', icon: meta.icon }));
|
||||
}
|
||||
coms.push(h('span', { class: '' }, $t(label || '')));
|
||||
return h('div', { class: 'flex items-center gap-1' }, coms);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps() {
|
||||
// 不需要处理多语言时就无需这么做
|
||||
return {
|
||||
addonAfter: titleSuffix.value,
|
||||
onChange({ target: { value } }: ChangeEvent) {
|
||||
titleSuffix.value = value && $te(value) ? $t(value) : undefined;
|
||||
},
|
||||
};
|
||||
},
|
||||
fieldName: 'meta.title',
|
||||
label: $t('system.menu.menuTitle'),
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
show: (values) => {
|
||||
return ['catalog', 'embedded', 'menu'].includes(values.type);
|
||||
},
|
||||
triggerFields: ['type'],
|
||||
},
|
||||
fieldName: 'path',
|
||||
label: $t('system.menu.path'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(2, $t('ui.formRules.minLength', [$t('system.menu.path'), 2]))
|
||||
.max(100, $t('ui.formRules.maxLength', [$t('system.menu.path'), 100]))
|
||||
.refine(
|
||||
(value: string) => {
|
||||
return value.startsWith('/');
|
||||
},
|
||||
$t('ui.formRules.startWith', [$t('system.menu.path'), '/']),
|
||||
)
|
||||
.refine(
|
||||
async (value: string) => {
|
||||
return !(await isMenuPathExists(value, formData.value?.id));
|
||||
},
|
||||
(value) => ({
|
||||
message: $t('ui.formRules.alreadyExists', [
|
||||
$t('system.menu.path'),
|
||||
value,
|
||||
]),
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
show: (values) => {
|
||||
return ['embedded', 'menu'].includes(values.type);
|
||||
},
|
||||
triggerFields: ['type'],
|
||||
},
|
||||
fieldName: 'activePath',
|
||||
help: $t('system.menu.activePathHelp'),
|
||||
label: $t('system.menu.activePath'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(2, $t('ui.formRules.minLength', [$t('system.menu.path'), 2]))
|
||||
.max(100, $t('ui.formRules.maxLength', [$t('system.menu.path'), 100]))
|
||||
.refine(
|
||||
(value: string) => {
|
||||
return value.startsWith('/');
|
||||
},
|
||||
$t('ui.formRules.startWith', [$t('system.menu.path'), '/']),
|
||||
)
|
||||
.refine(async (value: string) => {
|
||||
return await isMenuPathExists(value, formData.value?.id);
|
||||
}, $t('system.menu.activePathMustExist'))
|
||||
.optional(),
|
||||
},
|
||||
{
|
||||
component: 'IconPicker',
|
||||
componentProps: {
|
||||
prefix: 'carbon',
|
||||
},
|
||||
dependencies: {
|
||||
show: (values) => {
|
||||
return ['catalog', 'embedded', 'link', 'menu'].includes(values.type);
|
||||
},
|
||||
triggerFields: ['type'],
|
||||
},
|
||||
fieldName: 'meta.icon',
|
||||
label: $t('system.menu.icon'),
|
||||
},
|
||||
{
|
||||
component: 'IconPicker',
|
||||
componentProps: {
|
||||
prefix: 'carbon',
|
||||
},
|
||||
dependencies: {
|
||||
show: (values) => {
|
||||
return ['catalog', 'embedded', 'menu'].includes(values.type);
|
||||
},
|
||||
triggerFields: ['type'],
|
||||
},
|
||||
fieldName: 'meta.activeIcon',
|
||||
label: $t('system.menu.activeIcon'),
|
||||
},
|
||||
{
|
||||
component: 'AutoComplete',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
class: 'w-full',
|
||||
filterOption(input: string, option: { value: string }) {
|
||||
return option.value.toLowerCase().includes(input.toLowerCase());
|
||||
},
|
||||
options: componentKeys.map((v) => ({ value: v })),
|
||||
},
|
||||
dependencies: {
|
||||
rules: (values) => {
|
||||
return values.type === 'menu' ? 'required' : null;
|
||||
},
|
||||
show: (values) => {
|
||||
return values.type === 'menu';
|
||||
},
|
||||
triggerFields: ['type'],
|
||||
},
|
||||
fieldName: 'component',
|
||||
label: $t('system.menu.component'),
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
show: (values) => {
|
||||
return ['embedded', 'link'].includes(values.type);
|
||||
},
|
||||
triggerFields: ['type'],
|
||||
},
|
||||
fieldName: 'linkSrc',
|
||||
label: $t('system.menu.linkSrc'),
|
||||
rules: z.string().url($t('ui.formRules.invalidURL')),
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
rules: (values) => {
|
||||
return values.type === 'action' ? 'required' : null;
|
||||
},
|
||||
show: (values) => {
|
||||
return ['action', 'catalog', 'embedded', 'menu'].includes(values.type);
|
||||
},
|
||||
triggerFields: ['type'],
|
||||
},
|
||||
fieldName: 'authCode',
|
||||
label: $t('system.menu.authCode'),
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
buttonStyle: 'solid',
|
||||
options: [
|
||||
{ label: $t('common.enabled'), value: 1 },
|
||||
{ label: $t('common.disabled'), value: 0 },
|
||||
],
|
||||
optionType: 'button',
|
||||
},
|
||||
defaultValue: 1,
|
||||
fieldName: 'status',
|
||||
label: $t('system.menu.status'),
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
class: 'w-full',
|
||||
options: [
|
||||
{ label: $t('system.menu.badgeType.dot'), value: 'dot' },
|
||||
{ label: $t('system.menu.badgeType.normal'), value: 'normal' },
|
||||
],
|
||||
},
|
||||
dependencies: {
|
||||
show: (values) => {
|
||||
return values.type !== 'action';
|
||||
},
|
||||
triggerFields: ['type'],
|
||||
},
|
||||
fieldName: 'meta.badgeType',
|
||||
label: $t('system.menu.badgeType.title'),
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: (values) => {
|
||||
return {
|
||||
allowClear: true,
|
||||
class: 'w-full',
|
||||
disabled: values.meta?.badgeType !== 'normal',
|
||||
};
|
||||
},
|
||||
dependencies: {
|
||||
show: (values) => {
|
||||
return values.type !== 'action';
|
||||
},
|
||||
triggerFields: ['type'],
|
||||
},
|
||||
fieldName: 'meta.badge',
|
||||
label: $t('system.menu.badge'),
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
class: 'w-full',
|
||||
options: SystemMenuApi.BadgeVariants.map((v) => ({
|
||||
label: v,
|
||||
value: v,
|
||||
})),
|
||||
},
|
||||
dependencies: {
|
||||
show: (values) => {
|
||||
return values.type !== 'action';
|
||||
},
|
||||
triggerFields: ['type'],
|
||||
},
|
||||
fieldName: 'meta.badgeVariants',
|
||||
label: $t('system.menu.badgeVariants'),
|
||||
},
|
||||
{
|
||||
component: 'Divider',
|
||||
dependencies: {
|
||||
show: (values) => {
|
||||
return !['action', 'link'].includes(values.type);
|
||||
},
|
||||
triggerFields: ['type'],
|
||||
},
|
||||
fieldName: 'divider1',
|
||||
formItemClass: 'col-span-2 md:col-span-2 pb-0',
|
||||
hideLabel: true,
|
||||
renderComponentContent() {
|
||||
return {
|
||||
default: () => $t('system.menu.advancedSettings'),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Checkbox',
|
||||
dependencies: {
|
||||
show: (values) => {
|
||||
return ['menu'].includes(values.type);
|
||||
},
|
||||
triggerFields: ['type'],
|
||||
},
|
||||
fieldName: 'meta.keepAlive',
|
||||
renderComponentContent() {
|
||||
return {
|
||||
default: () => $t('system.menu.keepAlive'),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Checkbox',
|
||||
dependencies: {
|
||||
show: (values) => {
|
||||
return ['embedded', 'menu'].includes(values.type);
|
||||
},
|
||||
triggerFields: ['type'],
|
||||
},
|
||||
fieldName: 'meta.affixTab',
|
||||
renderComponentContent() {
|
||||
return {
|
||||
default: () => $t('system.menu.affixTab'),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Checkbox',
|
||||
dependencies: {
|
||||
show: (values) => {
|
||||
return !['action'].includes(values.type);
|
||||
},
|
||||
triggerFields: ['type'],
|
||||
},
|
||||
fieldName: 'meta.hideInMenu',
|
||||
renderComponentContent() {
|
||||
return {
|
||||
default: () => $t('system.menu.hideInMenu'),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Checkbox',
|
||||
dependencies: {
|
||||
show: (values) => {
|
||||
return ['catalog', 'menu'].includes(values.type);
|
||||
},
|
||||
triggerFields: ['type'],
|
||||
},
|
||||
fieldName: 'meta.hideChildrenInMenu',
|
||||
renderComponentContent() {
|
||||
return {
|
||||
default: () => $t('system.menu.hideChildrenInMenu'),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Checkbox',
|
||||
dependencies: {
|
||||
show: (values) => {
|
||||
return !['action', 'link'].includes(values.type);
|
||||
},
|
||||
triggerFields: ['type'],
|
||||
},
|
||||
fieldName: 'meta.hideInBreadcrumb',
|
||||
renderComponentContent() {
|
||||
return {
|
||||
default: () => $t('system.menu.hideInBreadcrumb'),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Checkbox',
|
||||
dependencies: {
|
||||
show: (values) => {
|
||||
return !['action', 'link'].includes(values.type);
|
||||
},
|
||||
triggerFields: ['type'],
|
||||
},
|
||||
fieldName: 'meta.hideInTab',
|
||||
renderComponentContent() {
|
||||
return {
|
||||
default: () => $t('system.menu.hideInTab'),
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind);
|
||||
const isHorizontal = computed(() => breakpoints.greaterOrEqual('md').value);
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
colon: true,
|
||||
formItemClass: 'col-span-2 md:col-span-1',
|
||||
},
|
||||
schema,
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-2 gap-x-4',
|
||||
});
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
onConfirm: onSubmit,
|
||||
onOpenChange(isOpen) {
|
||||
if (isOpen) {
|
||||
const data = drawerApi.getData<SystemMenuApi.SystemMenu>();
|
||||
if (data?.type === 'link') {
|
||||
data.linkSrc = data.meta?.link;
|
||||
} else if (data?.type === 'embedded') {
|
||||
data.linkSrc = data.meta?.iframeSrc;
|
||||
}
|
||||
if (data) {
|
||||
formData.value = data;
|
||||
formApi.setValues(formData.value);
|
||||
titleSuffix.value = formData.value.meta?.title
|
||||
? $t(formData.value.meta.title)
|
||||
: '';
|
||||
} else {
|
||||
formApi.resetForm();
|
||||
titleSuffix.value = '';
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
async function onSubmit() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (valid) {
|
||||
drawerApi.lock();
|
||||
const data =
|
||||
await formApi.getValues<
|
||||
Omit<SystemMenuApi.SystemMenu, 'children' | 'id'>
|
||||
>();
|
||||
if (data.type === 'link') {
|
||||
data.meta = { ...data.meta, link: data.linkSrc };
|
||||
} else if (data.type === 'embedded') {
|
||||
data.meta = { ...data.meta, iframeSrc: data.linkSrc };
|
||||
}
|
||||
delete data.linkSrc;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateMenu(formData.value.id, data)
|
||||
: createMenu(data));
|
||||
drawerApi.close();
|
||||
emit('success');
|
||||
} finally {
|
||||
drawerApi.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
const getDrawerTitle = computed(() =>
|
||||
formData.value?.id
|
||||
? $t('ui.actionTitle.edit', [$t('system.menu.name')])
|
||||
: $t('ui.actionTitle.create', [$t('system.menu.name')]),
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<Drawer class="w-full max-w-[800px]" :title="getDrawerTitle">
|
||||
<Form class="mx-4" :layout="isHorizontal ? 'horizontal' : 'vertical'" />
|
||||
</Drawer>
|
||||
</template>
|
||||
129
web/apps/web-antd/src/views/system/role/data.ts
Normal file
129
web/apps/web-antd/src/views/system/role/data.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SystemRoleApi } from '#/api/system/role';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
import { format_datetime } from '#/utils/date';
|
||||
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: $t('system.role.roleName'),
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
buttonStyle: 'solid',
|
||||
options: [
|
||||
{ label: $t('common.enabled'), value: 1 },
|
||||
{ label: $t('common.disabled'), value: 0 },
|
||||
],
|
||||
optionType: 'button',
|
||||
},
|
||||
defaultValue: 1,
|
||||
fieldName: 'status',
|
||||
label: $t('system.role.status'),
|
||||
},
|
||||
{
|
||||
component: 'Textarea',
|
||||
fieldName: 'remark',
|
||||
label: $t('system.role.remark'),
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'permissions',
|
||||
formItemClass: 'items-start',
|
||||
label: $t('system.role.setPermissions'),
|
||||
modelPropName: 'modelValue',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: $t('system.role.roleName'),
|
||||
},
|
||||
{ component: 'Input', fieldName: 'id', label: $t('system.role.id') },
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: [
|
||||
{ label: $t('common.enabled'), value: 1 },
|
||||
{ label: $t('common.disabled'), value: 0 },
|
||||
],
|
||||
},
|
||||
fieldName: 'status',
|
||||
label: $t('system.role.status'),
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'remark',
|
||||
label: $t('system.role.remark'),
|
||||
},
|
||||
{
|
||||
component: 'RangePicker',
|
||||
fieldName: 'createTime',
|
||||
label: $t('system.role.createTime'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function useColumns<T = SystemRoleApi.SystemRole>(
|
||||
onActionClick: OnActionClickFn<T>,
|
||||
onStatusChange?: (newStatus: any, row: T) => PromiseLike<boolean | undefined>,
|
||||
): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
title: $t('system.role.roleName'),
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
field: 'id',
|
||||
title: $t('system.role.id'),
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
cellRender: {
|
||||
attrs: { beforeChange: onStatusChange },
|
||||
name: onStatusChange ? 'CellSwitch' : 'CellTag',
|
||||
},
|
||||
field: 'status',
|
||||
title: $t('system.role.status'),
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
minWidth: 100,
|
||||
title: $t('system.role.remark'),
|
||||
},
|
||||
{
|
||||
field: 'create_time',
|
||||
title: $t('system.role.createTime'),
|
||||
width: 200,
|
||||
formatter: ({ cellValue }) => format_datetime(cellValue),
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: $t('system.role.name'),
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
},
|
||||
field: 'operation',
|
||||
fixed: 'right',
|
||||
title: $t('system.role.operation'),
|
||||
width: 130,
|
||||
},
|
||||
];
|
||||
}
|
||||
166
web/apps/web-antd/src/views/system/role/list.vue
Normal file
166
web/apps/web-antd/src/views/system/role/list.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { SystemRoleApi } from '#/api/system/role';
|
||||
|
||||
import { Page, useVbenDrawer } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import { Button, message, Modal } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteRole, getRoleList, patchRole } from '#/api/system/role';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormDrawer, formDrawerApi] = useVbenDrawer({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
fieldMappingTime: [['createTime', ['startTime', 'endTime']]],
|
||||
schema: useGridFormSchema(),
|
||||
submitOnChange: true,
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useColumns(onActionClick, onStatusChange),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getRoleList({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
toolbarConfig: {
|
||||
custom: true,
|
||||
export: false,
|
||||
refresh: { code: 'query' },
|
||||
search: true,
|
||||
zoom: true,
|
||||
},
|
||||
} as VxeTableGridOptions<SystemRoleApi.SystemRole>,
|
||||
});
|
||||
|
||||
function onActionClick(e: OnActionClickParams<SystemRoleApi.SystemRole>) {
|
||||
switch (e.code) {
|
||||
case 'delete': {
|
||||
onDelete(e.row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(e.row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将Antd的Modal.confirm封装为promise,方便在异步函数中调用。
|
||||
* @param content 提示内容
|
||||
* @param title 提示标题
|
||||
*/
|
||||
function confirm(content: string, title: string) {
|
||||
return new Promise((reslove, reject) => {
|
||||
Modal.confirm({
|
||||
content,
|
||||
onCancel() {
|
||||
reject(new Error('已取消'));
|
||||
},
|
||||
onOk() {
|
||||
reslove(true);
|
||||
},
|
||||
title,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态开关即将改变
|
||||
* @param newStatus 期望改变的状态值
|
||||
* @param row 行数据
|
||||
* @returns 返回false则中止改变,返回其他值(undefined、true)则允许改变
|
||||
*/
|
||||
async function onStatusChange(
|
||||
newStatus: number,
|
||||
row: SystemRoleApi.SystemRole,
|
||||
) {
|
||||
const status: Recordable<string> = {
|
||||
0: '禁用',
|
||||
1: '启用',
|
||||
};
|
||||
try {
|
||||
await confirm(
|
||||
`你要将${row.name}的状态切换为 【${status[newStatus.toString()]}】 吗?`,
|
||||
`切换状态`,
|
||||
);
|
||||
await patchRole(row.id, { status: newStatus });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function onEdit(row: SystemRoleApi.SystemRole) {
|
||||
formDrawerApi.setData(row).open();
|
||||
}
|
||||
|
||||
function onDelete(row: SystemRoleApi.SystemRole) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
deleteRole(row.id)
|
||||
.then(() => {
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
onRefresh();
|
||||
})
|
||||
.catch(() => {
|
||||
hideLoading();
|
||||
});
|
||||
}
|
||||
|
||||
function onRefresh() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
function onCreate() {
|
||||
formDrawerApi.setData({}).open();
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormDrawer @success="onRefresh"/>
|
||||
<Grid :table-title="$t('system.role.list')">
|
||||
<template #toolbar-tools>
|
||||
<Button type="primary" @click="onCreate">
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', [$t('system.role.name')]) }}
|
||||
</Button>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
139
web/apps/web-antd/src/views/system/role/modules/form.vue
Normal file
139
web/apps/web-antd/src/views/system/role/modules/form.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DataNode } from 'ant-design-vue/es/tree';
|
||||
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import type { SystemRoleApi } from '#/api/system/role';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer, VbenTree } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { Spin } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { getMenuList } from '#/api/system/menu';
|
||||
import { createRole, updateRole } from '#/api/system/role';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emits = defineEmits(['success']);
|
||||
|
||||
const formData = ref<SystemRoleApi.SystemRole>();
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const permissions = ref<DataNode[]>([]);
|
||||
const loadingPermissions = ref(false);
|
||||
|
||||
const id = ref();
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) return;
|
||||
const values = await formApi.getValues();
|
||||
drawerApi.lock();
|
||||
(id.value ? updateRole(id.value, values) : createRole(values))
|
||||
.then(() => {
|
||||
emits('success');
|
||||
drawerApi.close();
|
||||
})
|
||||
.catch(() => {
|
||||
drawerApi.unlock();
|
||||
});
|
||||
},
|
||||
onOpenChange(isOpen) {
|
||||
if (isOpen) {
|
||||
const data = drawerApi.getData<SystemRoleApi.SystemRole>();
|
||||
formApi.resetForm();
|
||||
if (data) {
|
||||
formData.value = data;
|
||||
id.value = data.id;
|
||||
formApi.setValues(data);
|
||||
} else {
|
||||
id.value = undefined;
|
||||
}
|
||||
|
||||
if (permissions.value.length === 0) {
|
||||
loadPermissions();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
async function loadPermissions() {
|
||||
loadingPermissions.value = true;
|
||||
try {
|
||||
const res = await getMenuList();
|
||||
permissions.value = res.items as unknown as DataNode[];
|
||||
} finally {
|
||||
loadingPermissions.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const getDrawerTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('common.edit', $t('system.role.name'))
|
||||
: $t('common.create', $t('system.role.name'));
|
||||
});
|
||||
|
||||
function getNodeClass(node: Recordable<any>) {
|
||||
const classes: string[] = [];
|
||||
if (node.value?.type === 'button') {
|
||||
classes.push('inline-flex');
|
||||
if (node.index % 3 >= 1) {
|
||||
classes.push('!pl-0');
|
||||
}
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Drawer :title="getDrawerTitle">
|
||||
<Form>
|
||||
<template #permissions="slotProps">
|
||||
<Spin :spinning="loadingPermissions" wrapper-class-name="w-full">
|
||||
<VbenTree
|
||||
:tree-data="permissions"
|
||||
multiple
|
||||
bordered
|
||||
:default-expanded-level="2"
|
||||
:get-node-class="getNodeClass"
|
||||
v-bind="slotProps"
|
||||
value-field="id"
|
||||
label-field="meta.title"
|
||||
icon-field="meta.icon"
|
||||
>
|
||||
<template #node="{ value }">
|
||||
<IconifyIcon v-if="value.meta.icon" :icon="value.meta.icon" />
|
||||
{{ $t(value.meta.title) }}
|
||||
</template>
|
||||
</VbenTree>
|
||||
</Spin>
|
||||
</template>
|
||||
</Form>
|
||||
</Drawer>
|
||||
</template>
|
||||
<style lang="css" scoped>
|
||||
:deep(.ant-tree-title) {
|
||||
.tree-actions {
|
||||
display: none;
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-tree-title:hover) {
|
||||
.tree-actions {
|
||||
display: flex;
|
||||
flex: auto;
|
||||
justify-content: flex-end;
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
133
web/apps/web-antd/src/views/system/tenant_package/data.ts
Normal file
133
web/apps/web-antd/src/views/system/tenant_package/data.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
|
||||
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn } from '#/adapter/vxe-table';
|
||||
import type { SystemTenantPackageApi } from '#/api/system/tenant_package';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getTenantPackageList } from '#/api/system/tenant_package';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
/**
|
||||
* 获取编辑表单的字段配置。如果没有使用多语言,可以直接export一个数组常量
|
||||
*/
|
||||
export function useSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: '套餐名',
|
||||
rules: z
|
||||
.string()
|
||||
.min(2, $t('ui.formRules.minLength', [$t('system.dept.deptName'), 2]))
|
||||
.max(
|
||||
20,
|
||||
$t('ui.formRules.maxLength', [$t('system.dept.deptName'), 20]),
|
||||
),
|
||||
},
|
||||
// 菜单权限
|
||||
{
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
api: getTenantPackageList,
|
||||
class: 'w-full',
|
||||
resultField: 'items',
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
},
|
||||
fieldName: 'pid',
|
||||
label: $t('system.dept.parentDept'),
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
buttonStyle: 'solid',
|
||||
options: [
|
||||
{ label: '开启', value: 1 },
|
||||
{ label: '关闭', value: 0 },
|
||||
],
|
||||
optionType: 'button',
|
||||
},
|
||||
defaultValue: 1,
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 50,
|
||||
rows: 3,
|
||||
showCount: true,
|
||||
},
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
rules: z
|
||||
.string()
|
||||
.max(50, $t('ui.formRules.maxLength', [$t('system.remark'), 50]))
|
||||
.optional(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取表格列配置
|
||||
* @description 使用函数的形式返回列数据而不是直接export一个Array常量,是为了响应语言切换时重新翻译表头
|
||||
* @param onActionClick 表格操作按钮点击事件
|
||||
*/
|
||||
export function useColumns(
|
||||
onActionClick?: OnActionClickFn<SystemTenantPackageApi.SystemTenantPackage>,
|
||||
): VxeTableGridOptions<SystemTenantPackageApi.SystemTenantPackage>['columns'] {
|
||||
return [
|
||||
{
|
||||
align: 'left',
|
||||
field: 'name',
|
||||
fixed: 'left',
|
||||
title: '套餐名',
|
||||
treeNode: true,
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
cellRender: { name: 'CellTag' },
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: '备注',
|
||||
},
|
||||
{
|
||||
field: 'create_time',
|
||||
title: '创建时间',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
align: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: $t('system.dept.name'),
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
'edit', // 默认的编辑按钮
|
||||
{
|
||||
code: 'delete', // 默认的删除按钮
|
||||
disabled: (row: SystemTenantPackageApi.SystemTenantPackage) => {
|
||||
return !!(row.children && row.children.length > 0);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
field: 'operation',
|
||||
fixed: 'right',
|
||||
headerAlign: 'center',
|
||||
showOverflow: false,
|
||||
title: '操作',
|
||||
width: 200,
|
||||
},
|
||||
];
|
||||
}
|
||||
135
web/apps/web-antd/src/views/system/tenant_package/list.vue
Normal file
135
web/apps/web-antd/src/views/system/tenant_package/list.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { SystemTenantPackageApi } from '#/api/system/tenant_package';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteTenantPackage,
|
||||
getTenantPackageList,
|
||||
} from '#/api/system/tenant_package';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useColumns } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* 编辑套餐
|
||||
* @param row
|
||||
*/
|
||||
function onEdit(row: SystemTenantPackageApi.SystemTenantPackage) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建新套餐
|
||||
*/
|
||||
function onCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除套餐
|
||||
* @param row
|
||||
*/
|
||||
function onDelete(row: SystemTenantPackageApi.SystemTenantPackage) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
deleteTenantPackage(row.id)
|
||||
.then(() => {
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
refreshGrid();
|
||||
})
|
||||
.catch(() => {
|
||||
hideLoading();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 表格操作按钮的回调函数
|
||||
*/
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<SystemTenantPackageApi.SystemTenantPackage>) {
|
||||
switch (code) {
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridEvents: {},
|
||||
gridOptions: {
|
||||
columns: useColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async (_params) => {
|
||||
return await getTenantPackageList();
|
||||
},
|
||||
},
|
||||
},
|
||||
toolbarConfig: {
|
||||
custom: true,
|
||||
export: false,
|
||||
refresh: { code: 'query' },
|
||||
zoom: true,
|
||||
},
|
||||
treeConfig: {
|
||||
parentField: 'pid',
|
||||
rowField: 'id',
|
||||
transform: false,
|
||||
},
|
||||
} as VxeTableGridOptions,
|
||||
});
|
||||
|
||||
/**
|
||||
* 刷新表格
|
||||
*/
|
||||
function refreshGrid() {
|
||||
gridApi.query();
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="refreshGrid" />
|
||||
<Grid table-title="租户套餐">
|
||||
<template #toolbar-tools>
|
||||
<Button type="primary" @click="onCreate">
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', [$t('system.tenants_package.name')]) }}
|
||||
</Button>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,78 @@
|
||||
<script lang="ts" setup>
|
||||
import type { SystemTenantPackageApi } from '#/api/system/tenant_package';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { createTenantPackage, updateTenantPackage } from '#/api/system/tenant_package';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<SystemTenantPackageApi.SystemTenantPackage>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', [$t('system.dept.name')])
|
||||
: $t('ui.actionTitle.create', [$t('system.dept.name')]);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
layout: 'vertical',
|
||||
schema: useSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
function resetForm() {
|
||||
formApi.resetForm();
|
||||
formApi.setValues(formData.value || {});
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (valid) {
|
||||
modalApi.lock();
|
||||
const data = await formApi.getValues();
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateTenantPackage(formData.value.id, data)
|
||||
: createTenantPackage(data));
|
||||
modalApi.close();
|
||||
emit('success');
|
||||
} finally {
|
||||
modalApi.lock(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
onOpenChange(isOpen) {
|
||||
if (isOpen) {
|
||||
const data = modalApi.getData<SystemTenantPackagestApi.SystemTenantPackage>();
|
||||
if (data) {
|
||||
if (data.pid === 0) {
|
||||
data.pid = undefined;
|
||||
}
|
||||
formData.value = data;
|
||||
formApi.setValues(formData.value);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
<template #prepend-footer>
|
||||
<div class="flex-auto">
|
||||
<Button type="primary" danger @click="resetForm">
|
||||
{{ $t('common.reset') }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
157
web/apps/web-antd/src/views/system/tenants/data.ts
Normal file
157
web/apps/web-antd/src/views/system/tenants/data.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
|
||||
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { OnActionClickFn } from '#/adapter/vxe-table';
|
||||
import type { SystemTenantsApi } from '#/api/system/tenants';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
/**
|
||||
* 获取编辑表单的字段配置。如果没有使用多语言,可以直接export一个数组常量
|
||||
*/
|
||||
export function useSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'name',
|
||||
label: $t('system.tenant.tenantName'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(
|
||||
2,
|
||||
$t('ui.formRules.minLength', [$t('system.tenant.tenantName'), 2]),
|
||||
)
|
||||
.max(
|
||||
20,
|
||||
$t('ui.formRules.maxLength', [$t('system.tenant.tenantName'), 20]),
|
||||
),
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
buttonStyle: 'solid',
|
||||
options: [
|
||||
{ label: $t('common.enabled'), value: 1 },
|
||||
{ label: $t('common.disabled'), value: 0 },
|
||||
],
|
||||
optionType: 'button',
|
||||
},
|
||||
defaultValue: 1,
|
||||
fieldName: 'status',
|
||||
label: $t('system.status'),
|
||||
},
|
||||
{
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
maxLength: 50,
|
||||
rows: 3,
|
||||
showCount: true,
|
||||
},
|
||||
fieldName: 'remark',
|
||||
label: $t('system.remark'),
|
||||
rules: z
|
||||
.string()
|
||||
.max(50, $t('ui.formRules.maxLength', [$t('system.remark'), 50]))
|
||||
.optional(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取表格列配置
|
||||
* @description 使用函数的形式返回列数据而不是直接export一个Array常量,是为了响应语言切换时重新翻译表头
|
||||
* @param onActionClick 表格操作按钮点击事件
|
||||
*/
|
||||
export function useColumns(
|
||||
onActionClick?: OnActionClickFn<SystemTenantsApi.SystemTenants>,
|
||||
): VxeTableGridOptions<SystemTenantsApi.SystemTenants>['columns'] {
|
||||
return [
|
||||
{
|
||||
align: 'left',
|
||||
field: 'name',
|
||||
fixed: 'left',
|
||||
title: $t('system.tenant.tenantName'),
|
||||
treeNode: true,
|
||||
width: 150,
|
||||
},
|
||||
// `contact_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '联系人',
|
||||
// `contact_mobile` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '联系手机',
|
||||
// `status` tinyint NOT NULL DEFAULT '0' COMMENT '租户状态(0正常 1停用)',
|
||||
// `website` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '绑定域名',
|
||||
// `package_id` bigint NOT NULL COMMENT '租户套餐编号',
|
||||
// `expire_time` datetime NOT NULL COMMENT '过期时间',
|
||||
// `account_count` int NOT NULL COMMENT '账号数量',
|
||||
{
|
||||
cellRender: { name: 'CellTag' },
|
||||
field: 'contact_name',
|
||||
title: $t('system.tenant.contact_mobile'),
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
cellRender: { name: 'CellTag' },
|
||||
field: 'website',
|
||||
title: $t('system.tenant.website'),
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
cellRender: { name: 'CellTag' },
|
||||
field: 'package_id',
|
||||
title: $t('system.tenant.package_id'),
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
cellRender: { name: 'CellTag' },
|
||||
field: 'expire_time',
|
||||
title: $t('system.tenant.expire_time'),
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
cellRender: { name: 'CellTag' },
|
||||
field: 'account_count',
|
||||
title: $t('system.tenant.account_count'),
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
cellRender: { name: 'CellTag' },
|
||||
field: 'status',
|
||||
title: $t('system.status'),
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
field: 'create_time',
|
||||
title: $t('system.createTime'),
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
title: $t('system.remark'),
|
||||
},
|
||||
{
|
||||
align: 'right',
|
||||
cellRender: {
|
||||
attrs: {
|
||||
nameField: 'name',
|
||||
nameTitle: $t('system.name'),
|
||||
onClick: onActionClick,
|
||||
},
|
||||
name: 'CellOperation',
|
||||
options: [
|
||||
'edit', // 默认的编辑按钮
|
||||
{
|
||||
code: 'delete', // 默认的删除按钮
|
||||
disabled: (row: SystemTenantsApi.SystemTenants) => {
|
||||
return !!(row.children && row.children.length > 0);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
field: 'operation',
|
||||
fixed: 'right',
|
||||
headerAlign: 'center',
|
||||
showOverflow: false,
|
||||
title: $t('system.operation'),
|
||||
width: 200,
|
||||
},
|
||||
];
|
||||
}
|
||||
131
web/apps/web-antd/src/views/system/tenants/list.vue
Normal file
131
web/apps/web-antd/src/views/system/tenants/list.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
OnActionClickParams,
|
||||
VxeTableGridOptions,
|
||||
} from '#/adapter/vxe-table';
|
||||
import type { SystemTenantsApi } from '#/api/system/tenants';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteTenants, getTenantsList } from '#/api/system/tenants';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useColumns } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* 编辑租户
|
||||
* @param row
|
||||
*/
|
||||
function onEdit(row: SystemTenantsApi.SystemTenants) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新租户
|
||||
*/
|
||||
function onCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除租户
|
||||
* @param row
|
||||
*/
|
||||
function onDelete(row: SystemTenantsApi.SystemTenants) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||
duration: 0,
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
deleteTenants(row.id)
|
||||
.then(() => {
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
refreshGrid();
|
||||
})
|
||||
.catch(() => {
|
||||
hideLoading();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 表格操作按钮的回调函数
|
||||
*/
|
||||
function onActionClick({
|
||||
code,
|
||||
row,
|
||||
}: OnActionClickParams<SystemTenantsApi.SystemTenants>) {
|
||||
switch (code) {
|
||||
case 'delete': {
|
||||
onDelete(row);
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
onEdit(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridEvents: {},
|
||||
gridOptions: {
|
||||
columns: useColumns(onActionClick),
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async (_params) => {
|
||||
return await getTenantsList();
|
||||
},
|
||||
},
|
||||
},
|
||||
toolbarConfig: {
|
||||
custom: true,
|
||||
export: false,
|
||||
refresh: { code: 'query' },
|
||||
zoom: true,
|
||||
},
|
||||
treeConfig: {
|
||||
parentField: 'pid',
|
||||
rowField: 'id',
|
||||
transform: false,
|
||||
},
|
||||
} as VxeTableGridOptions,
|
||||
});
|
||||
|
||||
/**
|
||||
* 刷新表格
|
||||
*/
|
||||
function refreshGrid() {
|
||||
gridApi.query();
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="refreshGrid" />
|
||||
<Grid table-title="租户列表">
|
||||
<template #toolbar-tools>
|
||||
<Button type="primary" @click="onCreate">
|
||||
<Plus class="size-5" />
|
||||
{{ $t('ui.actionTitle.create', [$t('system.tenants.name')]) }}
|
||||
</Button>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
78
web/apps/web-antd/src/views/system/tenants/modules/form.vue
Normal file
78
web/apps/web-antd/src/views/system/tenants/modules/form.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<script lang="ts" setup>
|
||||
import type { SystemTenantsApi } from '#/api/system/tenants';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { createTenants, updateTenants } from '#/api/system/tenants';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<SystemTenantsApi.SystemTenants>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', [$t('system.dept.name')])
|
||||
: $t('ui.actionTitle.create', [$t('system.dept.name')]);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
layout: 'vertical',
|
||||
schema: useSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
function resetForm() {
|
||||
formApi.resetForm();
|
||||
formApi.setValues(formData.value || {});
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (valid) {
|
||||
modalApi.lock();
|
||||
const data = await formApi.getValues();
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateTenants(formData.value.id, data)
|
||||
: createTenants(data));
|
||||
modalApi.close();
|
||||
emit('success');
|
||||
} finally {
|
||||
modalApi.lock(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
onOpenChange(isOpen) {
|
||||
if (isOpen) {
|
||||
const data = modalApi.getData<SystemTenantstApi.SystemTenants>();
|
||||
if (data) {
|
||||
if (data.pid === 0) {
|
||||
data.pid = undefined;
|
||||
}
|
||||
formData.value = data;
|
||||
formApi.setValues(formData.value);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
<template #prepend-footer>
|
||||
<div class="flex-auto">
|
||||
<Button type="primary" danger @click="resetForm">
|
||||
{{ $t('common.reset') }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
Reference in New Issue
Block a user