1
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
This commit is contained in:
3
apps/web-antd/src/views/_core/README.md
Normal file
3
apps/web-antd/src/views/_core/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# \_core
|
||||
|
||||
此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。
|
||||
69
apps/web-antd/src/views/_core/authentication/code-login.vue
Normal file
69
apps/web-antd/src/views/_core/authentication/code-login.vue
Normal file
@@ -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>
|
||||
53
apps/web-antd/src/views/_core/authentication/login.vue
Normal file
53
apps/web-antd/src/views/_core/authentication/login.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { BasicOption } from '@vben/types';
|
||||
|
||||
import { computed, markRaw } from 'vue';
|
||||
|
||||
import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
defineOptions({ name: 'Login' });
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
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: {
|
||||
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>
|
||||
96
apps/web-antd/src/views/_core/authentication/register.vue
Normal file
96
apps/web-antd/src/views/_core/authentication/register.vue
Normal file
@@ -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>
|
||||
7
apps/web-antd/src/views/_core/fallback/coming-soon.vue
Normal file
7
apps/web-antd/src/views/_core/fallback/coming-soon.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="coming-soon" />
|
||||
</template>
|
||||
9
apps/web-antd/src/views/_core/fallback/forbidden.vue
Normal file
9
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
apps/web-antd/src/views/_core/fallback/not-found.vue
Normal file
9
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
apps/web-antd/src/views/_core/fallback/offline.vue
Normal file
9
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>
|
||||
65
apps/web-antd/src/views/_core/profile/base-setting.vue
Normal file
65
apps/web-antd/src/views/_core/profile/base-setting.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import type { BasicOption } from '@vben/types';
|
||||
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { ProfileBaseSetting } from '@vben/common-ui';
|
||||
|
||||
import { getUserInfoApi } from '#/api';
|
||||
|
||||
const profileBaseSettingRef = ref();
|
||||
|
||||
const MOCK_ROLES_OPTIONS: BasicOption[] = [
|
||||
{
|
||||
label: '管理员',
|
||||
value: 'super',
|
||||
},
|
||||
{
|
||||
label: '用户',
|
||||
value: 'user',
|
||||
},
|
||||
{
|
||||
label: '测试',
|
||||
value: 'test',
|
||||
},
|
||||
];
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
fieldName: 'realName',
|
||||
component: 'Input',
|
||||
label: '姓名',
|
||||
},
|
||||
{
|
||||
fieldName: 'username',
|
||||
component: 'Input',
|
||||
label: '用户名',
|
||||
},
|
||||
{
|
||||
fieldName: 'roles',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
mode: 'tags',
|
||||
options: MOCK_ROLES_OPTIONS,
|
||||
},
|
||||
label: '角色',
|
||||
},
|
||||
{
|
||||
fieldName: 'introduction',
|
||||
component: 'Textarea',
|
||||
label: '个人简介',
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
const data = await getUserInfoApi();
|
||||
profileBaseSettingRef.value.getFormApi().setValues(data);
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<ProfileBaseSetting ref="profileBaseSettingRef" :form-schema="formSchema" />
|
||||
</template>
|
||||
49
apps/web-antd/src/views/_core/profile/index.vue
Normal file
49
apps/web-antd/src/views/_core/profile/index.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Profile } from '@vben/common-ui';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
|
||||
import ProfileBase from './base-setting.vue';
|
||||
import ProfileNotificationSetting from './notification-setting.vue';
|
||||
import ProfilePasswordSetting from './password-setting.vue';
|
||||
import ProfileSecuritySetting from './security-setting.vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const tabsValue = ref<string>('basic');
|
||||
|
||||
const tabs = ref([
|
||||
{
|
||||
label: '基本设置',
|
||||
value: 'basic',
|
||||
},
|
||||
{
|
||||
label: '安全设置',
|
||||
value: 'security',
|
||||
},
|
||||
{
|
||||
label: '修改密码',
|
||||
value: 'password',
|
||||
},
|
||||
{
|
||||
label: '新消息提醒',
|
||||
value: 'notice',
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
<template>
|
||||
<Profile
|
||||
v-model:model-value="tabsValue"
|
||||
title="个人中心"
|
||||
:user-info="userStore.userInfo"
|
||||
:tabs="tabs"
|
||||
>
|
||||
<template #content>
|
||||
<ProfileBase v-if="tabsValue === 'basic'" />
|
||||
<ProfileSecuritySetting v-if="tabsValue === 'security'" />
|
||||
<ProfilePasswordSetting v-if="tabsValue === 'password'" />
|
||||
<ProfileNotificationSetting v-if="tabsValue === 'notice'" />
|
||||
</template>
|
||||
</Profile>
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { ProfileNotificationSetting } from '@vben/common-ui';
|
||||
|
||||
const formSchema = computed(() => {
|
||||
return [
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'accountPassword',
|
||||
label: '账户密码',
|
||||
description: '其他用户的消息将以站内信的形式通知',
|
||||
},
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'systemMessage',
|
||||
label: '系统消息',
|
||||
description: '系统消息将以站内信的形式通知',
|
||||
},
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'todoTask',
|
||||
label: '待办任务',
|
||||
description: '待办任务将以站内信的形式通知',
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<ProfileNotificationSetting :form-schema="formSchema" />
|
||||
</template>
|
||||
66
apps/web-antd/src/views/_core/profile/password-setting.vue
Normal file
66
apps/web-antd/src/views/_core/profile/password-setting.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { ProfilePasswordSetting, z } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
const profilePasswordSettingRef = ref();
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
fieldName: 'oldPassword',
|
||||
label: '旧密码',
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
placeholder: '请输入旧密码',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'newPassword',
|
||||
label: '新密码',
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
passwordStrength: true,
|
||||
placeholder: '请输入新密码',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'confirmPassword',
|
||||
label: '确认密码',
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
passwordStrength: true,
|
||||
placeholder: '请再次输入新密码',
|
||||
},
|
||||
dependencies: {
|
||||
rules(values) {
|
||||
const { newPassword } = values;
|
||||
return z
|
||||
.string({ required_error: '请再次输入新密码' })
|
||||
.min(1, { message: '请再次输入新密码' })
|
||||
.refine((value) => value === newPassword, {
|
||||
message: '两次输入的密码不一致',
|
||||
});
|
||||
},
|
||||
triggerFields: ['newPassword'],
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
function handleSubmit() {
|
||||
message.success('密码修改成功');
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<ProfilePasswordSetting
|
||||
ref="profilePasswordSettingRef"
|
||||
class="w-1/3"
|
||||
:form-schema="formSchema"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
43
apps/web-antd/src/views/_core/profile/security-setting.vue
Normal file
43
apps/web-antd/src/views/_core/profile/security-setting.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { ProfileSecuritySetting } from '@vben/common-ui';
|
||||
|
||||
const formSchema = computed(() => {
|
||||
return [
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'accountPassword',
|
||||
label: '账户密码',
|
||||
description: '当前密码强度:强',
|
||||
},
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'securityPhone',
|
||||
label: '密保手机',
|
||||
description: '已绑定手机:138****8293',
|
||||
},
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'securityQuestion',
|
||||
label: '密保问题',
|
||||
description: '未设置密保问题,密保问题可有效保护账户安全',
|
||||
},
|
||||
{
|
||||
value: true,
|
||||
fieldName: 'securityEmail',
|
||||
label: '备用邮箱',
|
||||
description: '已绑定邮箱:ant***sign.com',
|
||||
},
|
||||
{
|
||||
value: false,
|
||||
fieldName: 'securityMfa',
|
||||
label: 'MFA 设备',
|
||||
description: '未绑定 MFA 设备,绑定后,可以进行二次确认',
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<ProfileSecuritySetting :form-schema="formSchema" />
|
||||
</template>
|
||||
291
apps/web-antd/src/views/booking/list.vue
Normal file
291
apps/web-antd/src/views/booking/list.vue
Normal file
@@ -0,0 +1,291 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Button, Card, Table, Space, message, Modal, Input, DatePicker, Select, Tag } from 'ant-design-vue';
|
||||
import {
|
||||
getBookingListApi,
|
||||
approveBookingApi,
|
||||
rejectBookingApi,
|
||||
type BookingApi
|
||||
} from '#/api';
|
||||
|
||||
defineOptions({ name: 'BookingList' });
|
||||
|
||||
const loading = ref(false);
|
||||
const tableData = ref<BookingApi.BookingInfo[]>([]);
|
||||
const total = ref(0);
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const searchForm = reactive({
|
||||
classroom_id: undefined as number | undefined,
|
||||
booking_date: '',
|
||||
status: undefined as number | undefined,
|
||||
student_id: undefined as number | undefined,
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '教室名称',
|
||||
dataIndex: 'classroom_name',
|
||||
key: 'classroom_name',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '座位号',
|
||||
dataIndex: 'seat_number',
|
||||
key: 'seat_number',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '学生ID',
|
||||
dataIndex: 'student_id',
|
||||
key: 'student_id',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '预订日期',
|
||||
dataIndex: 'booking_date',
|
||||
key: 'booking_date',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '时间范围',
|
||||
dataIndex: 'time_range',
|
||||
key: 'time_range',
|
||||
width: 180,
|
||||
customRender: ({ text }: { text: string }) => {
|
||||
if (!text || text === '00:00:00 - 00:00:00') {
|
||||
return '-';
|
||||
}
|
||||
return text;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '用途',
|
||||
dataIndex: 'purpose',
|
||||
key: 'purpose',
|
||||
width: 150,
|
||||
customRender: ({ text }: { text: string | null }) => {
|
||||
return text || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'create_time',
|
||||
key: 'create_time',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 150,
|
||||
fixed: 'right' as const,
|
||||
},
|
||||
];
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params: BookingApi.ListParams = {
|
||||
page: pagination.current,
|
||||
limit: pagination.pageSize,
|
||||
...searchForm,
|
||||
};
|
||||
const res = await getBookingListApi(params);
|
||||
// 根据实际返回格式,code 为 0 表示成功
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
tableData.value = Array.isArray(res.data) ? res.data : [];
|
||||
total.value = res.count || 0;
|
||||
} else {
|
||||
tableData.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取预订列表失败:', error);
|
||||
tableData.value = [];
|
||||
total.value = 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1;
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.classroom_id = undefined;
|
||||
searchForm.booking_date = '';
|
||||
searchForm.status = undefined;
|
||||
searchForm.student_id = undefined;
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
const getStatusText = (status?: number) => {
|
||||
const statusMap: Record<number, string> = {
|
||||
0: '待审核',
|
||||
1: '已通过',
|
||||
2: '已拒绝',
|
||||
};
|
||||
return statusMap[status ?? -1] || '未知';
|
||||
};
|
||||
|
||||
const getStatusColor = (status?: number) => {
|
||||
const colorMap: Record<number, string> = {
|
||||
0: 'orange',
|
||||
1: 'green',
|
||||
2: 'red',
|
||||
};
|
||||
return colorMap[status ?? -1] || 'default';
|
||||
};
|
||||
|
||||
const handleApprove = (record: BookingApi.BookingInfo) => {
|
||||
Modal.confirm({
|
||||
title: '确认同意',
|
||||
content: '确定要同意这个座位变更申请吗?',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await approveBookingApi({ id: record.id! });
|
||||
message.success('操作成功');
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleReject = (record: BookingApi.BookingInfo) => {
|
||||
Modal.confirm({
|
||||
title: '拒绝申请',
|
||||
content: '请输入拒绝原因',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await rejectBookingApi({ id: record.id!, reason: '管理员拒绝' });
|
||||
message.success('操作成功');
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleTableChange = (pag: any) => {
|
||||
pagination.current = pag.current;
|
||||
pagination.pageSize = pag.pageSize;
|
||||
fetchData();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="预订列表">
|
||||
<Card>
|
||||
<div class="mb-4">
|
||||
<Space wrap>
|
||||
<Input
|
||||
v-model:value="searchForm.classroom_id"
|
||||
placeholder="教室ID"
|
||||
style="width: 150px"
|
||||
type="number"
|
||||
allow-clear
|
||||
@press-enter="handleSearch"
|
||||
/>
|
||||
<Input
|
||||
v-model:value="searchForm.student_id"
|
||||
placeholder="学生ID"
|
||||
style="width: 150px"
|
||||
type="number"
|
||||
allow-clear
|
||||
@press-enter="handleSearch"
|
||||
/>
|
||||
<DatePicker
|
||||
v-model:value="searchForm.booking_date"
|
||||
placeholder="预订日期"
|
||||
style="width: 180px"
|
||||
format="YYYY-MM-DD"
|
||||
allow-clear
|
||||
/>
|
||||
<Select
|
||||
v-model:value="searchForm.status"
|
||||
placeholder="状态"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
>
|
||||
<Select.Option :value="0">待审核</Select.Option>
|
||||
<Select.Option :value="1">已通过</Select.Option>
|
||||
<Select.Option :value="2">已拒绝</Select.Option>
|
||||
</Select>
|
||||
<Button type="primary" @click="handleSearch">搜索</Button>
|
||||
<Button @click="handleReset">重置</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:loading="loading"
|
||||
:scroll="{ x: 1400 }"
|
||||
:pagination="{
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
total: total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
}"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<Tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Space>
|
||||
<Button
|
||||
v-if="record.status === 0"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleApprove(record)"
|
||||
>
|
||||
同意
|
||||
</Button>
|
||||
<Button
|
||||
v-if="record.status === 0"
|
||||
type="link"
|
||||
danger
|
||||
size="small"
|
||||
@click="handleReject(record)"
|
||||
>
|
||||
拒绝
|
||||
</Button>
|
||||
<span v-else class="text-gray-400">-</span>
|
||||
</Space>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
250
apps/web-antd/src/views/class/detail.vue
Normal file
250
apps/web-antd/src/views/class/detail.vue
Normal file
@@ -0,0 +1,250 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Card, Form, Input, Button, message, Select, Radio } from 'ant-design-vue';
|
||||
import {
|
||||
getClassDetailApi,
|
||||
saveClassApi,
|
||||
getTeachersApi,
|
||||
getClassroomsApi,
|
||||
type ClassApi
|
||||
} from '#/api';
|
||||
|
||||
defineOptions({ name: 'ClassDetail' });
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const formRef = ref();
|
||||
const loading = ref(false);
|
||||
const teacherList = ref<any[]>([]);
|
||||
const classroomList = ref<any[]>([]);
|
||||
const teacherListLoading = ref(false);
|
||||
const classroomListLoading = ref(false);
|
||||
|
||||
const formData = ref<Partial<ClassApi.SaveParams>>({
|
||||
name: '',
|
||||
grade: '',
|
||||
teacher_id: undefined,
|
||||
class_room_id: undefined,
|
||||
description: '',
|
||||
status: 1,
|
||||
});
|
||||
|
||||
const isEdit = computed(() => !!route.params.id);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
loading.value = true;
|
||||
const data = { ...formData.value };
|
||||
if (isEdit.value) {
|
||||
data.id = Number(route.params.id);
|
||||
}
|
||||
await saveClassApi(data);
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
router.back();
|
||||
} catch (error: any) {
|
||||
console.error('保存失败:', error);
|
||||
message.error(error?.response?.data?.message || error?.response?.data?.msg || '保存失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchDetail = async () => {
|
||||
if (!isEdit.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getClassDetailApi({ id: Number(route.params.id) });
|
||||
// 支持 code 为 0 或 200 的成功响应
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
// 根据实际返回格式,数据在 res.data 中
|
||||
const data = res.data;
|
||||
if (data) {
|
||||
formData.value = {
|
||||
name: data.name || '',
|
||||
grade: data.grade || '',
|
||||
teacher_id: data.teacher_id || undefined,
|
||||
class_room_id: data.class_room_id || undefined,
|
||||
description: data.description || '',
|
||||
status: data.status !== undefined ? data.status : 1,
|
||||
};
|
||||
} else {
|
||||
message.error('获取班级详情失败:数据为空');
|
||||
}
|
||||
} else {
|
||||
message.error(res?.message || res?.msg || '获取班级详情失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取详情失败:', error);
|
||||
message.error(error?.response?.data?.message || error?.response?.data?.msg || '获取班级详情失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取班主任列表
|
||||
const fetchTeacherList = async () => {
|
||||
teacherListLoading.value = true;
|
||||
try {
|
||||
const res: any = await getTeachersApi();
|
||||
if (Array.isArray(res)) {
|
||||
teacherList.value = res;
|
||||
} else if (res && (res.code === 0 || res.code === 200)) {
|
||||
teacherList.value = Array.isArray(res.data) ? res.data : [];
|
||||
} else {
|
||||
teacherList.value = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取班主任列表失败:', error);
|
||||
teacherList.value = [];
|
||||
} finally {
|
||||
teacherListLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取教室列表
|
||||
const fetchClassroomList = async () => {
|
||||
classroomListLoading.value = true;
|
||||
try {
|
||||
const res = await getClassroomsApi();
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
// 根据实际返回格式,教室列表在 res.data.all_classrooms 中
|
||||
if (res.data && res.data.all_classrooms) {
|
||||
classroomList.value = Array.isArray(res.data.all_classrooms) ? res.data.all_classrooms : [];
|
||||
} else if (Array.isArray(res.data)) {
|
||||
// 兼容直接返回数组的情况
|
||||
classroomList.value = res.data;
|
||||
} else {
|
||||
classroomList.value = [];
|
||||
}
|
||||
} else if (Array.isArray(res)) {
|
||||
// 兼容直接返回数组的情况
|
||||
classroomList.value = res;
|
||||
} else {
|
||||
classroomList.value = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取教室列表失败:', error);
|
||||
classroomList.value = [];
|
||||
} finally {
|
||||
classroomListLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
// 先加载下拉列表数据,确保在填充表单时选项已存在
|
||||
await Promise.all([fetchTeacherList(), fetchClassroomList()]);
|
||||
// 然后再加载详情数据
|
||||
if (isEdit.value) {
|
||||
fetchDetail();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :title="isEdit ? '编辑班级' : '新增班级'">
|
||||
<Card>
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:label-col="{ span: 4 }"
|
||||
:wrapper-col="{ span: 20 }"
|
||||
>
|
||||
<Form.Item
|
||||
label="班级名称"
|
||||
name="name"
|
||||
:rules="[{ required: true, message: '请输入班级名称' }]"
|
||||
>
|
||||
<Input v-model:value="formData.name" placeholder="请输入班级名称" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="年级"
|
||||
name="grade"
|
||||
:rules="[{ required: true, message: '请输入年级' }]"
|
||||
>
|
||||
<Input v-model:value="formData.grade" placeholder="请输入年级" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="班主任"
|
||||
name="teacher_id"
|
||||
:rules="[{ required: true, message: '请选择班主任' }]"
|
||||
>
|
||||
<Select
|
||||
v-model:value="formData.teacher_id"
|
||||
placeholder="请选择班主任"
|
||||
:loading="teacherListLoading"
|
||||
allow-clear
|
||||
show-search
|
||||
:filter-option="(input, option) => {
|
||||
const label = option?.label || '';
|
||||
return label.toLowerCase().includes(input.toLowerCase());
|
||||
}"
|
||||
>
|
||||
<template v-if="teacherList.length === 0 && !teacherListLoading">
|
||||
<Select.Option disabled value="">暂无班主任数据</Select.Option>
|
||||
</template>
|
||||
<Select.Option
|
||||
v-for="item in teacherList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
:label="item.name || ''"
|
||||
>
|
||||
{{ item.name || `班主任 ${item.id}` }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
<div v-if="teacherList.length > 0" style="margin-top: 4px; font-size: 12px; color: #999;">
|
||||
共 {{ teacherList.length }} 个班主任可选
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="教室"
|
||||
name="class_room_id"
|
||||
:rules="[{ required: true, message: '请选择教室' }]"
|
||||
>
|
||||
<Select
|
||||
v-model:value="formData.class_room_id"
|
||||
placeholder="请选择教室"
|
||||
:loading="classroomListLoading"
|
||||
allow-clear
|
||||
show-search
|
||||
:filter-option="(input, option) => {
|
||||
const label = option?.label || '';
|
||||
return label.toLowerCase().includes(input.toLowerCase());
|
||||
}"
|
||||
>
|
||||
<template v-if="classroomList.length === 0 && !classroomListLoading">
|
||||
<Select.Option disabled value="">暂无教室数据</Select.Option>
|
||||
</template>
|
||||
<Select.Option
|
||||
v-for="item in classroomList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
:label="item.name || ''"
|
||||
>
|
||||
{{ item.name || `教室 ${item.id}` }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
<div v-if="classroomList.length > 0" style="margin-top: 4px; font-size: 12px; color: #999;">
|
||||
共 {{ classroomList.length }} 个教室可选
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Form.Item label="描述" name="description">
|
||||
<Input.TextArea v-model:value="formData.description" placeholder="请输入描述" :rows="3" />
|
||||
</Form.Item>
|
||||
<Form.Item label="状态" name="status">
|
||||
<Radio.Group v-model:value="formData.status">
|
||||
<Radio :value="1">启用</Radio>
|
||||
<Radio :value="0">禁用</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item :wrapper-col="{ offset: 4, span: 20 }">
|
||||
<Button type="primary" :loading="loading" @click="handleSubmit">保存</Button>
|
||||
<Button class="ml-2" @click="router.back()">取消</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
203
apps/web-antd/src/views/class/list.vue
Normal file
203
apps/web-antd/src/views/class/list.vue
Normal file
@@ -0,0 +1,203 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Button, Card, Table, Space, message, Modal, Input } from 'ant-design-vue';
|
||||
import {
|
||||
getClassListApi,
|
||||
deleteClassApi,
|
||||
type ClassApi
|
||||
} from '#/api';
|
||||
|
||||
defineOptions({ name: 'ClassList' });
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const loading = ref(false);
|
||||
const tableData = ref<ClassApi.ClassInfo[]>([]);
|
||||
const total = ref(0);
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const searchForm = reactive({
|
||||
name: '',
|
||||
teacher_name: '',
|
||||
school_id: undefined as number | undefined,
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '班级名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '年级',
|
||||
dataIndex: 'grade',
|
||||
key: 'grade',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '班主任',
|
||||
key: 'teacher_name',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '教室',
|
||||
key: 'classroom_name',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '学生人数',
|
||||
dataIndex: 'student_count',
|
||||
key: 'student_count',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
},
|
||||
];
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params: ClassApi.ListParams = {
|
||||
page: pagination.current,
|
||||
limit: pagination.pageSize,
|
||||
...searchForm,
|
||||
};
|
||||
const res = await getClassListApi(params);
|
||||
// 根据实际返回格式,code 为 0 表示成功,返回格式为 {code, msg, count, data}
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
// 处理数据,将嵌套的 teacher 和 classroom 信息映射到表格需要的字段
|
||||
const data = Array.isArray(res.data) ? res.data : [];
|
||||
tableData.value = data.map((item: ClassApi.ClassInfo) => ({
|
||||
...item,
|
||||
// 映射 teacher_name 字段
|
||||
teacher_name: item.teacher?.name || '',
|
||||
// 映射 classroom_name 字段
|
||||
classroom_name: item.classroom?.name || '',
|
||||
// 确保 classroom_id 字段存在(使用 class_room_id)
|
||||
classroom_id: item.class_room_id || item.classroom?.id,
|
||||
}));
|
||||
total.value = res.count || 0;
|
||||
} else {
|
||||
tableData.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取班级列表失败:', error);
|
||||
tableData.value = [];
|
||||
total.value = 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1;
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
router.push({ name: 'ClassDetail' });
|
||||
};
|
||||
|
||||
const handleEdit = (record: ClassApi.ClassInfo) => {
|
||||
router.push({
|
||||
name: 'ClassDetail',
|
||||
params: { id: record.id }
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = (record: ClassApi.ClassInfo) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除班级"${record.name}"吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteClassApi({ id: record.id! });
|
||||
message.success('删除成功');
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleTableChange = (pag: any) => {
|
||||
pagination.current = pag.current;
|
||||
pagination.pageSize = pag.pageSize;
|
||||
fetchData();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="班级列表">
|
||||
<Card>
|
||||
<div class="mb-4">
|
||||
<Space>
|
||||
<Input
|
||||
v-model:value="searchForm.name"
|
||||
placeholder="班级名称"
|
||||
style="width: 200px"
|
||||
@press-enter="handleSearch"
|
||||
/>
|
||||
<Input
|
||||
v-model:value="searchForm.teacher_name"
|
||||
placeholder="班主任姓名"
|
||||
style="width: 200px"
|
||||
@press-enter="handleSearch"
|
||||
/>
|
||||
<Button type="primary" @click="handleSearch">搜索</Button>
|
||||
<Button type="primary" @click="handleAdd">添加班级</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:loading="loading"
|
||||
:pagination="{
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
total: total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
}"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'teacher_name'">
|
||||
{{ record.teacher?.name || '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'classroom_name'">
|
||||
{{ record.classroom?.name || '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Space>
|
||||
<Button type="link" size="small" @click="handleEdit(record)">编辑</Button>
|
||||
<Button type="link" danger size="small" @click="handleDelete(record)">删除</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
289
apps/web-antd/src/views/classroom/detail.vue
Normal file
289
apps/web-antd/src/views/classroom/detail.vue
Normal file
@@ -0,0 +1,289 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Card, Form, Input, Select, InputNumber, Button, message, Tabs, Radio } from 'ant-design-vue';
|
||||
import {
|
||||
getClassroomDetailApi,
|
||||
saveClassroomApi,
|
||||
getSchoolListApi,
|
||||
getTeacherListApi,
|
||||
type ClassroomApi,
|
||||
type SchoolApi,
|
||||
type TeacherApi
|
||||
} from '#/api';
|
||||
import SeatLayoutEditor from '#/components/classroom/SeatLayoutEditor.vue';
|
||||
|
||||
defineOptions({ name: 'ClassroomDetail' });
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const formRef = ref();
|
||||
const loading = ref(false);
|
||||
const activeTab = ref('basic');
|
||||
const schoolList = ref<SchoolApi.SchoolInfo[]>([]);
|
||||
const teacherList = ref<TeacherApi.TeacherInfo[]>([]);
|
||||
const schoolListLoading = ref(false);
|
||||
const teacherListLoading = ref(false);
|
||||
const formData = ref<ClassroomApi.SaveParams>({
|
||||
name: '',
|
||||
building: '',
|
||||
floor: '',
|
||||
room: '',
|
||||
type: 'normal',
|
||||
capacity: 0,
|
||||
school_id: undefined,
|
||||
teacher_id: undefined,
|
||||
description: '',
|
||||
status: 1,
|
||||
});
|
||||
const layout = ref<ClassroomApi.ClassroomLayout | null>(null);
|
||||
|
||||
const isEdit = computed(() => !!route.params.id);
|
||||
const classroomId = computed(() => (isEdit.value ? Number(route.params.id) : 0));
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
loading.value = true;
|
||||
const data: any = { ...formData.value };
|
||||
if (isEdit.value) {
|
||||
data.id = Number(route.params.id);
|
||||
}
|
||||
// 将 building, floor, room 组合成 location 对象
|
||||
if (data.building || data.floor || data.room) {
|
||||
data.location = {
|
||||
building: data.building || '',
|
||||
floor: data.floor || '',
|
||||
room: data.room || '',
|
||||
};
|
||||
// 删除单独的字段,避免重复
|
||||
delete data.building;
|
||||
delete data.floor;
|
||||
delete data.room;
|
||||
}
|
||||
await saveClassroomApi(data);
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
router.back();
|
||||
} catch (error: any) {
|
||||
console.error('保存失败:', error);
|
||||
message.error(error?.response?.data?.message || error?.response?.data?.msg || '保存失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchDetail = async () => {
|
||||
if (!isEdit.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getClassroomDetailApi({ id: Number(route.params.id) });
|
||||
// 支持 code 为 0 或 200 的成功响应
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
// 根据实际返回格式,数据在 res.data 中
|
||||
const data = res.data || res;
|
||||
// 解析 location 对象
|
||||
const location = data.location || {};
|
||||
formData.value = {
|
||||
name: data.name || '',
|
||||
building: location.building || data.building || '',
|
||||
floor: location.floor || data.floor || '',
|
||||
room: location.room || data.room || '',
|
||||
type: data.type || 'normal',
|
||||
capacity: data.capacity || 0,
|
||||
school_id: data.school_id,
|
||||
teacher_id: data.teacher_id,
|
||||
description: data.description || '',
|
||||
status: data.status !== undefined ? data.status : 1,
|
||||
};
|
||||
// 加载布局数据
|
||||
if (data.layout) {
|
||||
layout.value = data.layout;
|
||||
}
|
||||
} else {
|
||||
message.error(res?.message || res?.msg || '获取教室详情失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取详情失败:', error);
|
||||
message.error(error?.response?.data?.message || error?.response?.data?.msg || '获取教室详情失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取学校列表
|
||||
const fetchSchoolList = async () => {
|
||||
schoolListLoading.value = true;
|
||||
try {
|
||||
const res = await getSchoolListApi({ page: 1, limit: 1000 });
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
schoolList.value = Array.isArray(res.data) ? res.data : [];
|
||||
} else {
|
||||
schoolList.value = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取学校列表失败:', error);
|
||||
schoolList.value = [];
|
||||
} finally {
|
||||
schoolListLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取教师列表
|
||||
const fetchTeacherList = async () => {
|
||||
teacherListLoading.value = true;
|
||||
try {
|
||||
const res = await getTeacherListApi({ page: 1, limit: 1000 });
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
teacherList.value = Array.isArray(res.data) ? res.data : [];
|
||||
} else {
|
||||
teacherList.value = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取教师列表失败:', error);
|
||||
teacherList.value = [];
|
||||
} finally {
|
||||
teacherListLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleLayoutSaved = (newLayout: ClassroomApi.ClassroomLayout) => {
|
||||
layout.value = newLayout;
|
||||
// 更新容量(座位数量)
|
||||
const seatCount = newLayout.cells.filter((cell) => cell.type === 'seat').length;
|
||||
if (seatCount > 0) {
|
||||
formData.value.capacity = seatCount;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchSchoolList();
|
||||
fetchTeacherList();
|
||||
if (isEdit.value) {
|
||||
fetchDetail();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :title="isEdit ? '编辑教室' : '新增教室'">
|
||||
<Card>
|
||||
<Tabs v-model:activeKey="activeTab">
|
||||
<Tabs.TabPane key="basic" tab="基本信息">
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:label-col="{ span: 4 }"
|
||||
:wrapper-col="{ span: 20 }"
|
||||
>
|
||||
<Form.Item
|
||||
label="教室名称"
|
||||
name="name"
|
||||
:rules="[{ required: true, message: '请输入教室名称' }]"
|
||||
>
|
||||
<Input v-model:value="formData.name" placeholder="请输入教室名称" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="楼栋"
|
||||
name="building"
|
||||
:rules="[{ required: true, message: '请输入楼栋' }]"
|
||||
>
|
||||
<Input v-model:value="formData.building" placeholder="请输入楼栋" />
|
||||
</Form.Item>
|
||||
<Form.Item label="楼层" name="floor">
|
||||
<Input v-model:value="formData.floor" placeholder="请输入楼层" />
|
||||
</Form.Item>
|
||||
<Form.Item label="房间号" name="room">
|
||||
<Input v-model:value="formData.room" placeholder="请输入房间号" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="教室类型"
|
||||
name="type"
|
||||
:rules="[{ required: true, message: '请选择教室类型' }]"
|
||||
>
|
||||
<Select v-model:value="formData.type" placeholder="请选择教室类型">
|
||||
<Select.Option value="normal">普通教室</Select.Option>
|
||||
<Select.Option value="multimedia">多媒体教室</Select.Option>
|
||||
<Select.Option value="lab">实验室</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="容量" name="capacity">
|
||||
<InputNumber v-model:value="formData.capacity" :min="0" placeholder="请输入容量" />
|
||||
</Form.Item>
|
||||
<Form.Item label="学校" name="school_id">
|
||||
<Select
|
||||
v-model:value="formData.school_id"
|
||||
placeholder="请选择学校"
|
||||
:loading="schoolListLoading"
|
||||
allow-clear
|
||||
show-search
|
||||
:filter-option="(input, option) => {
|
||||
const label = option?.label || '';
|
||||
return label.toLowerCase().includes(input.toLowerCase());
|
||||
}"
|
||||
>
|
||||
<template v-if="schoolList.length === 0 && !schoolListLoading">
|
||||
<Select.Option disabled value="">暂无学校数据</Select.Option>
|
||||
</template>
|
||||
<Select.Option
|
||||
v-for="item in schoolList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
:label="item.name || ''"
|
||||
>
|
||||
{{ item.name || `学校 ${item.id}` }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="负责教师" name="teacher_id">
|
||||
<Select
|
||||
v-model:value="formData.teacher_id"
|
||||
placeholder="请选择负责教师"
|
||||
:loading="teacherListLoading"
|
||||
allow-clear
|
||||
show-search
|
||||
:filter-option="(input, option) => {
|
||||
const label = option?.label || '';
|
||||
return label.toLowerCase().includes(input.toLowerCase());
|
||||
}"
|
||||
>
|
||||
<template v-if="teacherList.length === 0 && !teacherListLoading">
|
||||
<Select.Option disabled value="">暂无教师数据</Select.Option>
|
||||
</template>
|
||||
<Select.Option
|
||||
v-for="item in teacherList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
:label="item.name || ''"
|
||||
>
|
||||
{{ item.name || `教师 ${item.id}` }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="描述" name="description">
|
||||
<Input.TextArea v-model:value="formData.description" placeholder="请输入描述" :rows="3" />
|
||||
</Form.Item>
|
||||
<Form.Item label="状态" name="status">
|
||||
<Radio.Group v-model:value="formData.status">
|
||||
<Radio :value="1">启用</Radio>
|
||||
<Radio :value="0">禁用</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item :wrapper-col="{ offset: 4, span: 20 }">
|
||||
<Button type="primary" :loading="loading" @click="handleSubmit">保存</Button>
|
||||
<Button class="ml-2" @click="router.back()">取消</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane v-if="isEdit" key="layout" tab="座位布局">
|
||||
<SeatLayoutEditor
|
||||
:classroom-id="classroomId"
|
||||
:layout="layout"
|
||||
@saved="handleLayoutSaved"
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
258
apps/web-antd/src/views/classroom/list.vue
Normal file
258
apps/web-antd/src/views/classroom/list.vue
Normal file
@@ -0,0 +1,258 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Button, Card, Table, Space, message, Modal, Input, Select } from 'ant-design-vue';
|
||||
import {
|
||||
getClassroomListApi,
|
||||
deleteClassroomApi,
|
||||
batchDeleteClassroomApi,
|
||||
type ClassroomApi
|
||||
} from '#/api';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
defineOptions({ name: 'ClassroomList' });
|
||||
|
||||
const router = useRouter();
|
||||
const loading = ref(false);
|
||||
const tableData = ref<ClassroomApi.ClassroomInfo[]>([]);
|
||||
const total = ref(0);
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const searchForm = reactive({
|
||||
name: '',
|
||||
building: '',
|
||||
type: undefined as 'normal' | 'multimedia' | 'lab' | undefined,
|
||||
school_id: undefined as number | undefined,
|
||||
});
|
||||
|
||||
const selectedRowKeys = ref<number[]>([]);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '教室名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '楼栋',
|
||||
dataIndex: 'building',
|
||||
key: 'building',
|
||||
customRender: ({ record }: { record: ClassroomApi.ClassroomInfo }) => {
|
||||
return record.location?.building || record.building || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '楼层',
|
||||
dataIndex: 'floor',
|
||||
key: 'floor',
|
||||
customRender: ({ record }: { record: ClassroomApi.ClassroomInfo }) => {
|
||||
return record.location?.floor || record.floor || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '房间号',
|
||||
dataIndex: 'room',
|
||||
key: 'room',
|
||||
customRender: ({ record }: { record: ClassroomApi.ClassroomInfo }) => {
|
||||
return record.location?.room || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
customRender: ({ text }: { text: string }) => {
|
||||
const typeMap: Record<string, string> = {
|
||||
normal: '普通教室',
|
||||
multimedia: '多媒体教室',
|
||||
lab: '实验室',
|
||||
};
|
||||
return typeMap[text] || text;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '容量',
|
||||
dataIndex: 'capacity',
|
||||
key: 'capacity',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
},
|
||||
];
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params: ClassroomApi.ListParams = {
|
||||
page: pagination.current,
|
||||
limit: pagination.pageSize,
|
||||
...searchForm,
|
||||
};
|
||||
const res = await getClassroomListApi(params);
|
||||
// 根据实际返回格式,code 为 0 表示成功,返回格式为 {code, msg, count, data}
|
||||
// 使用 responseReturn: 'body' 获取完整响应体
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
tableData.value = Array.isArray(res.data) ? res.data : [];
|
||||
total.value = res.count || 0;
|
||||
} else {
|
||||
tableData.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取教室列表失败:', error);
|
||||
tableData.value = [];
|
||||
total.value = 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1;
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.name = '';
|
||||
searchForm.building = '';
|
||||
searchForm.type = undefined;
|
||||
searchForm.school_id = undefined;
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
router.push({ name: 'ClassroomDetail' });
|
||||
};
|
||||
|
||||
const handleEdit = (record: ClassroomApi.ClassroomInfo) => {
|
||||
router.push({ name: 'ClassroomDetail', params: { id: record.id } });
|
||||
};
|
||||
|
||||
const handleDelete = (record: ClassroomApi.ClassroomInfo) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除教室"${record.name}"吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteClassroomApi({ id: record.id! });
|
||||
message.success('删除成功');
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleBatchDelete = () => {
|
||||
if (selectedRowKeys.value.length === 0) {
|
||||
message.warning('请选择要删除的教室');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '确认批量删除',
|
||||
content: `确定要删除选中的 ${selectedRowKeys.value.length} 个教室吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await batchDeleteClassroomApi({ ids: selectedRowKeys.value });
|
||||
message.success('批量删除成功');
|
||||
selectedRowKeys.value = [];
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
console.error('批量删除失败:', error);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleTableChange = (pag: any) => {
|
||||
pagination.current = pag.current;
|
||||
pagination.pageSize = pag.pageSize;
|
||||
fetchData();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="教室列表">
|
||||
<Card>
|
||||
<div class="mb-4">
|
||||
<Space>
|
||||
<Input
|
||||
v-model:value="searchForm.name"
|
||||
placeholder="教室名称"
|
||||
style="width: 200px"
|
||||
@press-enter="handleSearch"
|
||||
/>
|
||||
<Input
|
||||
v-model:value="searchForm.building"
|
||||
placeholder="楼栋/房间号"
|
||||
style="width: 200px"
|
||||
@press-enter="handleSearch"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="searchForm.type"
|
||||
placeholder="教室类型"
|
||||
style="width: 150px"
|
||||
allow-clear
|
||||
>
|
||||
<Select.Option value="normal">普通教室</Select.Option>
|
||||
<Select.Option value="multimedia">多媒体教室</Select.Option>
|
||||
<Select.Option value="lab">实验室</Select.Option>
|
||||
</Select>
|
||||
<Button type="primary" @click="handleSearch">搜索</Button>
|
||||
<Button @click="handleReset">重置</Button>
|
||||
<Button type="primary" @click="handleAdd">新增</Button>
|
||||
<Button danger :disabled="selectedRowKeys.length === 0" @click="handleBatchDelete">
|
||||
批量删除
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:loading="loading"
|
||||
:pagination="{
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
total: total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
}"
|
||||
:row-selection="{
|
||||
selectedRowKeys: selectedRowKeys,
|
||||
onChange: (keys: number[]) => {
|
||||
selectedRowKeys = keys;
|
||||
},
|
||||
}"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'action'">
|
||||
<Space>
|
||||
<Button type="link" size="small" @click="handleEdit(record)">编辑</Button>
|
||||
<Button type="link" danger size="small" @click="handleDelete(record)">删除</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
78
apps/web-antd/src/views/dashboard/index.vue
Normal file
78
apps/web-antd/src/views/dashboard/index.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Card, Descriptions, Spin } from 'ant-design-vue';
|
||||
import { getSystemInfoApi, type IndexApi } from '#/api';
|
||||
|
||||
defineOptions({ name: 'Dashboard' });
|
||||
|
||||
const loading = ref(false);
|
||||
const systemInfo = ref<IndexApi.SystemInfo>({});
|
||||
|
||||
const fetchSystemInfo = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getSystemInfoApi();
|
||||
// 支持 code 为 0 或 200 的成功响应
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
// 根据实际返回格式,数据在 res.data 中
|
||||
systemInfo.value = res.data || {};
|
||||
} else {
|
||||
console.error('获取系统信息失败:', res?.message || res?.msg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取系统信息失败:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchSystemInfo();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="系统概览">
|
||||
<Card title="系统信息">
|
||||
<Spin :spinning="loading">
|
||||
<Descriptions :column="2" bordered>
|
||||
<Descriptions.Item label="操作系统">
|
||||
{{ systemInfo.os || '-' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="PHP版本">
|
||||
{{ systemInfo.php || '-' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Web服务器">
|
||||
{{ systemInfo.server || '-' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="MySQL版本">
|
||||
{{ systemInfo.mysql || '-' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="框架版本">
|
||||
{{ systemInfo.framework_version || '-' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="上传限制">
|
||||
{{ systemInfo.upload_max || '-' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最大执行时间">
|
||||
{{ systemInfo.max_execution_time || '-' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="运行目录">
|
||||
{{ systemInfo.runtime_path || '-' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="磁盘总空间" :span="2">
|
||||
{{ systemInfo.disk_total_space || '-' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="磁盘可用空间">
|
||||
{{ systemInfo.disk_free_space || '-' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="磁盘使用率">
|
||||
{{ systemInfo.disk_usage || '-' }}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Spin>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
317
apps/web-antd/src/views/school/detail.vue
Normal file
317
apps/web-antd/src/views/school/detail.vue
Normal file
@@ -0,0 +1,317 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Card, Form, Input, Button, message, Tabs, Table, Space, Modal, Radio } from 'ant-design-vue';
|
||||
import {
|
||||
getSchoolDetailApi,
|
||||
saveSchoolApi,
|
||||
getSchoolAccountListApi,
|
||||
saveSchoolAccountApi,
|
||||
deleteSchoolAccountApi,
|
||||
type SchoolApi
|
||||
} from '#/api';
|
||||
|
||||
defineOptions({ name: 'SchoolDetail' });
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const formRef = ref();
|
||||
const accountFormRef = ref();
|
||||
const loading = ref(false);
|
||||
const accountLoading = ref(false);
|
||||
const activeTab = ref('basic');
|
||||
const formData = ref<Partial<SchoolApi.SaveParams>>({
|
||||
name: '',
|
||||
address: '',
|
||||
contact: '',
|
||||
phone: '',
|
||||
description: '',
|
||||
});
|
||||
|
||||
// 账号管理相关
|
||||
const accountList = ref<SchoolApi.AccountInfo[]>([]);
|
||||
const accountFormData = ref<Partial<SchoolApi.SaveAccountParams>>({
|
||||
username: '',
|
||||
password: '',
|
||||
status: 1,
|
||||
});
|
||||
const accountModalVisible = ref(false);
|
||||
const isAccountEdit = ref(false);
|
||||
|
||||
const isEdit = computed(() => !!route.params.id);
|
||||
const schoolId = computed(() => isEdit.value ? Number(route.params.id) : 0);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
loading.value = true;
|
||||
const data = { ...formData.value };
|
||||
if (isEdit.value) {
|
||||
data.id = Number(route.params.id);
|
||||
}
|
||||
await saveSchoolApi(data);
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
router.back();
|
||||
} catch (error: any) {
|
||||
console.error('保存失败:', error);
|
||||
message.error(error?.response?.data?.message || error?.response?.data?.msg || '保存失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchDetail = async () => {
|
||||
if (!isEdit.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getSchoolDetailApi({ id: Number(route.params.id) });
|
||||
// 支持 code 为 0 或 200 的成功响应
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
// 根据实际返回格式,数据在 res.data 中
|
||||
const data = res.data;
|
||||
if (data) {
|
||||
formData.value = {
|
||||
name: data.name || '',
|
||||
address: data.address || '',
|
||||
contact: data.contact || '',
|
||||
phone: data.phone || '',
|
||||
description: data.description || '',
|
||||
};
|
||||
} else {
|
||||
message.error('获取学校详情失败:数据为空');
|
||||
}
|
||||
} else {
|
||||
message.error(res?.message || res?.msg || '获取学校详情失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取详情失败:', error);
|
||||
message.error(error?.response?.data?.message || error?.response?.data?.msg || '获取学校详情失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 账号管理相关方法
|
||||
const accountColumns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
},
|
||||
];
|
||||
|
||||
const fetchAccountList = async () => {
|
||||
if (!isEdit.value) return;
|
||||
accountLoading.value = true;
|
||||
try {
|
||||
const res = await getSchoolAccountListApi({ school_id: schoolId.value });
|
||||
// 支持 code 为 0 或 200 的成功响应
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
// 根据实际返回格式,数据在 res.data 数组中
|
||||
const data = Array.isArray(res.data) ? res.data : [];
|
||||
accountList.value = data;
|
||||
} else {
|
||||
accountList.value = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取账号列表失败:', error);
|
||||
accountList.value = [];
|
||||
} finally {
|
||||
accountLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddAccount = () => {
|
||||
isAccountEdit.value = false;
|
||||
accountFormData.value = {
|
||||
username: '',
|
||||
password: '',
|
||||
status: 1,
|
||||
};
|
||||
accountModalVisible.value = true;
|
||||
};
|
||||
|
||||
const handleEditAccount = (record: SchoolApi.AccountInfo) => {
|
||||
isAccountEdit.value = true;
|
||||
accountFormData.value = {
|
||||
id: record.id,
|
||||
username: record.username || '',
|
||||
password: '', // 编辑时不显示密码
|
||||
status: record.status !== undefined ? record.status : 1,
|
||||
};
|
||||
accountModalVisible.value = true;
|
||||
};
|
||||
|
||||
const handleDeleteAccount = (record: SchoolApi.AccountInfo) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除账号"${record.username}"吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteSchoolAccountApi({ id: record.id! });
|
||||
message.success('删除成功');
|
||||
fetchAccountList();
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleAccountSubmit = async () => {
|
||||
try {
|
||||
await accountFormRef.value.validate();
|
||||
accountLoading.value = true;
|
||||
const data = {
|
||||
...accountFormData.value,
|
||||
school_id: schoolId.value,
|
||||
};
|
||||
await saveSchoolAccountApi(data);
|
||||
message.success(isAccountEdit.value ? '更新成功' : '创建成功');
|
||||
accountModalVisible.value = false;
|
||||
fetchAccountList();
|
||||
} catch (error: any) {
|
||||
console.error('保存账号失败:', error);
|
||||
message.error(error?.response?.data?.message || error?.response?.data?.msg || '保存账号失败');
|
||||
} finally {
|
||||
accountLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (isEdit.value) {
|
||||
fetchDetail();
|
||||
fetchAccountList();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :title="isEdit ? '编辑学校' : '新增学校'">
|
||||
<Card>
|
||||
<Tabs v-model:activeKey="activeTab">
|
||||
<Tabs.TabPane key="basic" tab="基本信息">
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:label-col="{ span: 4 }"
|
||||
:wrapper-col="{ span: 20 }"
|
||||
>
|
||||
<Form.Item
|
||||
label="学校名称"
|
||||
name="name"
|
||||
:rules="[{ required: true, message: '请输入学校名称' }]"
|
||||
>
|
||||
<Input v-model:value="formData.name" placeholder="请输入学校名称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="地址" name="address">
|
||||
<Input v-model:value="formData.address" placeholder="请输入地址" />
|
||||
</Form.Item>
|
||||
<Form.Item label="联系人" name="contact">
|
||||
<Input v-model:value="formData.contact" placeholder="请输入联系人" />
|
||||
</Form.Item>
|
||||
<Form.Item label="联系电话" name="phone">
|
||||
<Input v-model:value="formData.phone" placeholder="请输入联系电话" />
|
||||
</Form.Item>
|
||||
<Form.Item label="描述" name="description">
|
||||
<Input.TextArea v-model:value="formData.description" placeholder="请输入描述" :rows="3" />
|
||||
</Form.Item>
|
||||
<Form.Item :wrapper-col="{ offset: 4, span: 20 }">
|
||||
<Button type="primary" :loading="loading" @click="handleSubmit">保存</Button>
|
||||
<Button class="ml-2" @click="router.back()">取消</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="account" tab="账号管理" :disabled="!isEdit">
|
||||
<div class="mb-4">
|
||||
<Button type="primary" @click="handleAddAccount">添加账号</Button>
|
||||
</div>
|
||||
<Table
|
||||
:columns="accountColumns"
|
||||
:data-source="accountList"
|
||||
:loading="accountLoading"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
{{ record.status === 1 ? '启用' : '禁用' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Space>
|
||||
<Button type="link" size="small" @click="handleEditAccount(record)">编辑</Button>
|
||||
<Button type="link" danger size="small" @click="handleDeleteAccount(record)">删除</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
|
||||
<!-- 账号编辑弹窗 -->
|
||||
<Modal
|
||||
v-model:open="accountModalVisible"
|
||||
:title="isAccountEdit ? '编辑账号' : '新增账号'"
|
||||
:confirm-loading="accountLoading"
|
||||
@ok="handleAccountSubmit"
|
||||
@cancel="accountModalVisible = false"
|
||||
>
|
||||
<Form
|
||||
ref="accountFormRef"
|
||||
:model="accountFormData"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<Form.Item
|
||||
label="用户名"
|
||||
name="username"
|
||||
:rules="[{ required: true, message: '请输入用户名' }]"
|
||||
>
|
||||
<Input
|
||||
v-model:value="accountFormData.username"
|
||||
placeholder="请输入用户名"
|
||||
:disabled="isAccountEdit"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="密码"
|
||||
name="password"
|
||||
:rules="[{ required: !isAccountEdit, message: '请输入密码' }]"
|
||||
>
|
||||
<Input.Password
|
||||
v-model:value="accountFormData.password"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
<div v-if="isAccountEdit" style="margin-top: 4px; font-size: 12px; color: #999;">
|
||||
留空则不修改密码
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Form.Item label="状态" name="status">
|
||||
<Radio.Group v-model:value="accountFormData.status">
|
||||
<Radio :value="1">启用</Radio>
|
||||
<Radio :value="0">禁用</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
183
apps/web-antd/src/views/school/list.vue
Normal file
183
apps/web-antd/src/views/school/list.vue
Normal file
@@ -0,0 +1,183 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Button, Card, Table, Space, message, Modal, Input } from 'ant-design-vue';
|
||||
import {
|
||||
getSchoolListApi,
|
||||
deleteSchoolApi,
|
||||
type SchoolApi
|
||||
} from '#/api';
|
||||
|
||||
defineOptions({ name: 'SchoolList' });
|
||||
|
||||
const router = useRouter();
|
||||
const loading = ref(false);
|
||||
const tableData = ref<SchoolApi.SchoolInfo[]>([]);
|
||||
const total = ref(0);
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const searchForm = reactive({
|
||||
name: '',
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '学校名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '地址',
|
||||
dataIndex: 'address',
|
||||
key: 'address',
|
||||
width: 250,
|
||||
},
|
||||
{
|
||||
title: '联系人',
|
||||
dataIndex: 'contact',
|
||||
key: 'contact',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
dataIndex: 'phone',
|
||||
key: 'phone',
|
||||
width: 130,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
},
|
||||
];
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params: SchoolApi.ListParams = {
|
||||
page: pagination.current,
|
||||
limit: pagination.pageSize,
|
||||
...searchForm,
|
||||
};
|
||||
const res = await getSchoolListApi(params);
|
||||
// 根据实际返回格式,code 为 0 表示成功,返回格式为 {code, msg, count, data}
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
const data = Array.isArray(res.data) ? res.data : [];
|
||||
tableData.value = data;
|
||||
total.value = res.count || 0;
|
||||
} else {
|
||||
tableData.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取学校列表失败:', error);
|
||||
tableData.value = [];
|
||||
total.value = 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1;
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const handleEdit = (record: SchoolApi.SchoolInfo) => {
|
||||
router.push({
|
||||
name: 'SchoolDetail',
|
||||
params: { id: record.id }
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = (record: SchoolApi.SchoolInfo) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除学校"${record.name}"吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteSchoolApi({ id: record.id! });
|
||||
message.success('删除成功');
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleTableChange = (pag: any) => {
|
||||
pagination.current = pag.current;
|
||||
pagination.pageSize = pag.pageSize;
|
||||
fetchData();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="学校列表">
|
||||
<Card>
|
||||
<div class="mb-4">
|
||||
<Space>
|
||||
<Input
|
||||
v-model:value="searchForm.name"
|
||||
placeholder="学校名称"
|
||||
style="width: 200px"
|
||||
@press-enter="handleSearch"
|
||||
/>
|
||||
<Button type="primary" @click="handleSearch">搜索</Button>
|
||||
<Button type="primary" @click="router.push({ name: 'SchoolDetail' })">新增</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:loading="loading"
|
||||
:pagination="{
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
total: total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
}"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'address'">
|
||||
<span :title="record.address || '-'">
|
||||
{{ record.address || '-' }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'contact'">
|
||||
{{ record.contact || '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'phone'">
|
||||
{{ record.phone || '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Space>
|
||||
<Button type="link" size="small" @click="handleEdit(record)">编辑</Button>
|
||||
<Button type="link" danger size="small" @click="handleDelete(record)">删除</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
170
apps/web-antd/src/views/setting/index.vue
Normal file
170
apps/web-antd/src/views/setting/index.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Card, Form, Input, Button, message, Tabs, InputNumber } from 'ant-design-vue';
|
||||
import {
|
||||
getSettingApi,
|
||||
saveSettingApi,
|
||||
type SettingApi
|
||||
} from '#/api';
|
||||
|
||||
defineOptions({ name: 'SettingIndex' });
|
||||
|
||||
const formRef = ref();
|
||||
const loading = ref(false);
|
||||
const activeTab = ref('basic');
|
||||
const formData = ref<Partial<SettingApi.SettingInfo>>({
|
||||
site_name: '',
|
||||
site_desc: '',
|
||||
site_icp: '',
|
||||
default_avatar: '',
|
||||
upload_allowed_ext: '',
|
||||
upload_max_size: '',
|
||||
upload_path: '',
|
||||
upload_image_size: '',
|
||||
upload_image_ext: '',
|
||||
mail_host: '',
|
||||
mail_port: '',
|
||||
mail_username: '',
|
||||
mail_password: '',
|
||||
wxapp_appid: '',
|
||||
wxapp_secret: '',
|
||||
wxapp_name: '',
|
||||
wxapp_original: '',
|
||||
});
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getSettingApi();
|
||||
// 支持 code 为 0 或 200 的成功响应
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
// 根据实际返回格式,数据在 res.data 中
|
||||
const data = res.data || res;
|
||||
formData.value = {
|
||||
site_name: data.site_name || '',
|
||||
site_desc: data.site_desc || '',
|
||||
site_icp: data.site_icp || '',
|
||||
site_banners: data.site_banners || '',
|
||||
default_avatar: data.default_avatar || '',
|
||||
upload_allowed_ext: data.upload_allowed_ext || '',
|
||||
upload_max_size: data.upload_max_size || '',
|
||||
upload_path: data.upload_path || '',
|
||||
upload_image_size: data.upload_image_size || '',
|
||||
upload_image_ext: data.upload_image_ext || '',
|
||||
mail_host: data.mail_host || '',
|
||||
mail_port: data.mail_port || '',
|
||||
mail_username: data.mail_username || '',
|
||||
mail_password: data.mail_password || '',
|
||||
wxapp_appid: data.wxapp_appid || '',
|
||||
wxapp_secret: data.wxapp_secret || '',
|
||||
wxapp_name: data.wxapp_name || '',
|
||||
wxapp_original: data.wxapp_original || '',
|
||||
};
|
||||
} else {
|
||||
message.error(res?.message || res?.msg || '获取系统设置失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取系统设置失败:', error);
|
||||
message.error(error?.response?.data?.message || error?.response?.data?.msg || '获取系统设置失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
loading.value = true;
|
||||
await saveSettingApi(formData.value);
|
||||
message.success('保存成功');
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="系统设置">
|
||||
<Card>
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:label-col="{ span: 4 }"
|
||||
:wrapper-col="{ span: 20 }"
|
||||
>
|
||||
<Tabs v-model:activeKey="activeTab">
|
||||
<Tabs.TabPane key="basic" tab="基本信息">
|
||||
<Form.Item label="站点名称" name="site_name">
|
||||
<Input v-model:value="formData.site_name" placeholder="请输入站点名称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="站点描述" name="site_desc">
|
||||
<Input v-model:value="formData.site_desc" placeholder="请输入站点描述" />
|
||||
</Form.Item>
|
||||
<Form.Item label="ICP备案号" name="site_icp">
|
||||
<Input v-model:value="formData.site_icp" placeholder="请输入ICP备案号" />
|
||||
</Form.Item>
|
||||
<Form.Item label="默认头像" name="default_avatar">
|
||||
<Input v-model:value="formData.default_avatar" placeholder="默认头像路径" />
|
||||
</Form.Item>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="upload" tab="上传设置">
|
||||
<Form.Item label="上传路径" name="upload_path">
|
||||
<Input v-model:value="formData.upload_path" placeholder="请输入上传路径" />
|
||||
</Form.Item>
|
||||
<Form.Item label="允许上传扩展名" name="upload_allowed_ext">
|
||||
<Input v-model:value="formData.upload_allowed_ext" placeholder="如:jpg,png" />
|
||||
</Form.Item>
|
||||
<Form.Item label="上传最大大小(MB)" name="upload_max_size">
|
||||
<InputNumber v-model:value="formData.upload_max_size" :min="0" placeholder="请输入上传最大大小" style="width: 100%" />
|
||||
</Form.Item>
|
||||
<Form.Item label="图片扩展名" name="upload_image_ext">
|
||||
<Input v-model:value="formData.upload_image_ext" placeholder="如:jpg,jpeg,png,gif,webp" />
|
||||
</Form.Item>
|
||||
<Form.Item label="图片大小限制(MB)" name="upload_image_size">
|
||||
<InputNumber v-model:value="formData.upload_image_size" :min="0" placeholder="请输入图片大小限制" style="width: 100%" />
|
||||
</Form.Item>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="mail" tab="邮件设置">
|
||||
<Form.Item label="邮件主机" name="mail_host">
|
||||
<Input v-model:value="formData.mail_host" placeholder="请输入邮件主机" />
|
||||
</Form.Item>
|
||||
<Form.Item label="邮件端口" name="mail_port">
|
||||
<InputNumber v-model:value="formData.mail_port" :min="0" :max="65535" placeholder="请输入邮件端口" style="width: 100%" />
|
||||
</Form.Item>
|
||||
<Form.Item label="邮件用户名" name="mail_username">
|
||||
<Input v-model:value="formData.mail_username" placeholder="请输入邮件用户名" />
|
||||
</Form.Item>
|
||||
<Form.Item label="邮件密码" name="mail_password">
|
||||
<Input.Password v-model:value="formData.mail_password" placeholder="请输入邮件密码" />
|
||||
</Form.Item>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="wxapp" tab="微信小程序">
|
||||
<Form.Item label="小程序AppID" name="wxapp_appid">
|
||||
<Input v-model:value="formData.wxapp_appid" placeholder="请输入小程序AppID" />
|
||||
</Form.Item>
|
||||
<Form.Item label="小程序Secret" name="wxapp_secret">
|
||||
<Input.Password v-model:value="formData.wxapp_secret" placeholder="请输入小程序Secret" />
|
||||
</Form.Item>
|
||||
<Form.Item label="小程序名称" name="wxapp_name">
|
||||
<Input v-model:value="formData.wxapp_name" placeholder="请输入小程序名称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="原始ID" name="wxapp_original">
|
||||
<Input v-model:value="formData.wxapp_original" placeholder="请输入原始ID" />
|
||||
</Form.Item>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
<Form.Item :wrapper-col="{ offset: 4, span: 20 }">
|
||||
<Button type="primary" :loading="loading" @click="handleSubmit">保存</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
201
apps/web-antd/src/views/student/detail.vue
Normal file
201
apps/web-antd/src/views/student/detail.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Card, Form, Input, Button, message, Select, Radio } from 'ant-design-vue';
|
||||
import {
|
||||
getStudentDetailApi,
|
||||
saveStudentApi,
|
||||
type StudentApi
|
||||
} from '#/api';
|
||||
import { getClassListApi, type ClassApi } from '#/api';
|
||||
|
||||
defineOptions({ name: 'StudentDetail' });
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const formRef = ref();
|
||||
const loading = ref(false);
|
||||
const classList = ref<ClassApi.ClassInfo[]>([]);
|
||||
const classListLoading = ref(false);
|
||||
const formData = ref<Partial<StudentApi.SaveParams>>({
|
||||
student_id: '',
|
||||
name: '',
|
||||
gender: undefined,
|
||||
class_id: undefined,
|
||||
phone: '',
|
||||
email: '',
|
||||
avatar: null,
|
||||
remark: '',
|
||||
status: 1,
|
||||
});
|
||||
|
||||
const isEdit = computed(() => !!route.params.id);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
loading.value = true;
|
||||
const data = { ...formData.value };
|
||||
if (isEdit.value) {
|
||||
data.id = Number(route.params.id);
|
||||
}
|
||||
await saveStudentApi(data);
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
router.back();
|
||||
} catch (error: any) {
|
||||
console.error('保存失败:', error);
|
||||
message.error(error?.response?.data?.message || error?.response?.data?.msg || '保存失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchDetail = async () => {
|
||||
if (!isEdit.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getStudentDetailApi({ id: Number(route.params.id) });
|
||||
// 支持 code 为 0 或 200 的成功响应
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
// 根据实际返回格式,数据在 res.data 中
|
||||
const data = res.data || res;
|
||||
formData.value = {
|
||||
student_id: (data.student_id || data.id?.toString() || '') as string,
|
||||
name: data.name || '',
|
||||
class_id: data.class_id,
|
||||
status: data.status !== undefined ? data.status : 1,
|
||||
gender: data.gender || undefined,
|
||||
phone: data.phone || '',
|
||||
email: data.email || '',
|
||||
avatar: data.avatar || null,
|
||||
remark: data.remark || '',
|
||||
};
|
||||
} else {
|
||||
message.error(res?.message || res?.msg || '获取学生详情失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取详情失败:', error);
|
||||
message.error(error?.response?.data?.message || error?.response?.data?.msg || '获取学生详情失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取班级列表
|
||||
const fetchClassList = async () => {
|
||||
classListLoading.value = true;
|
||||
try {
|
||||
const res = await getClassListApi({ page: 1, limit: 1000 });
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
// 根据实际返回格式,data 是数组
|
||||
const data = res.data;
|
||||
if (Array.isArray(data)) {
|
||||
classList.value = data;
|
||||
} else {
|
||||
classList.value = [];
|
||||
}
|
||||
} else {
|
||||
classList.value = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取班级列表失败:', error);
|
||||
classList.value = [];
|
||||
} finally {
|
||||
classListLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchClassList();
|
||||
if (isEdit.value) {
|
||||
fetchDetail();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :title="isEdit ? '编辑学生' : '新增学生'">
|
||||
<Card>
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:label-col="{ span: 4 }"
|
||||
:wrapper-col="{ span: 20 }"
|
||||
>
|
||||
<Form.Item
|
||||
label="学员ID"
|
||||
name="student_id"
|
||||
:rules="[{ required: true, message: '请输入学员ID' }]"
|
||||
>
|
||||
<Input
|
||||
v-model:value="formData.student_id as string"
|
||||
:placeholder="isEdit ? '学员ID' : '请输入学员ID'"
|
||||
:disabled="isEdit"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="姓名"
|
||||
name="name"
|
||||
:rules="[{ required: true, message: '请输入姓名' }]"
|
||||
>
|
||||
<Input v-model:value="formData.name" placeholder="请输入姓名" />
|
||||
</Form.Item>
|
||||
<Form.Item label="性别" name="gender">
|
||||
<Radio.Group v-model:value="formData.gender">
|
||||
<Radio value="男">男</Radio>
|
||||
<Radio value="女">女</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item label="电话" name="phone">
|
||||
<Input v-model:value="formData.phone" placeholder="请输入电话" />
|
||||
</Form.Item>
|
||||
<Form.Item label="邮箱" name="email">
|
||||
<Input v-model:value="formData.email" placeholder="请输入邮箱" />
|
||||
</Form.Item>
|
||||
<Form.Item label="班级" name="class_id">
|
||||
<Select
|
||||
v-model:value="formData.class_id"
|
||||
placeholder="请选择班级"
|
||||
:loading="classListLoading"
|
||||
allow-clear
|
||||
show-search
|
||||
:filter-option="(input, option) => {
|
||||
const label = option?.label || '';
|
||||
return label.toLowerCase().includes(input.toLowerCase());
|
||||
}"
|
||||
>
|
||||
<template v-if="classList.length === 0 && !classListLoading">
|
||||
<Select.Option disabled value="">暂无班级数据</Select.Option>
|
||||
</template>
|
||||
<Select.Option
|
||||
v-for="item in classList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
:label="item.name || ''"
|
||||
>
|
||||
{{ item.name || `班级 ${item.id}` }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
<div v-if="classList.length > 0" style="margin-top: 4px; font-size: 12px; color: #999;">
|
||||
共 {{ classList.length }} 个班级可选
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Form.Item label="备注" name="remark">
|
||||
<Input.TextArea v-model:value="formData.remark" placeholder="请输入备注" :rows="3" />
|
||||
</Form.Item>
|
||||
<Form.Item label="状态" name="status">
|
||||
<Radio.Group v-model:value="formData.status">
|
||||
<Radio :value="1">启用</Radio>
|
||||
<Radio :value="0">禁用</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item :wrapper-col="{ offset: 4, span: 20 }">
|
||||
<Button type="primary" :loading="loading" @click="handleSubmit">保存</Button>
|
||||
<Button class="ml-2" @click="router.back()">取消</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
496
apps/web-antd/src/views/student/list.vue
Normal file
496
apps/web-antd/src/views/student/list.vue
Normal file
@@ -0,0 +1,496 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Button, Card, Table, Space, message, Modal, Input, Select, Tag, Upload } from 'ant-design-vue';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import {
|
||||
getStudentListApi,
|
||||
deleteStudentApi,
|
||||
uploadStudentFileApi,
|
||||
importStudentDataApi,
|
||||
type StudentApi
|
||||
} from '#/api';
|
||||
import { getClassListApi, type ClassApi } from '#/api';
|
||||
|
||||
defineOptions({ name: 'StudentList' });
|
||||
|
||||
const router = useRouter();
|
||||
const loading = ref(false);
|
||||
const tableData = ref<StudentApi.StudentInfo[]>([]);
|
||||
const total = ref(0);
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const searchForm = reactive({
|
||||
student_id: '',
|
||||
name: '',
|
||||
phone: '',
|
||||
class_id: undefined as number | undefined,
|
||||
school_id: undefined as number | undefined,
|
||||
bind_status: undefined as string | undefined,
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '性别',
|
||||
dataIndex: 'gender',
|
||||
key: 'gender',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '电话',
|
||||
dataIndex: 'phone',
|
||||
key: 'phone',
|
||||
width: 130,
|
||||
},
|
||||
{
|
||||
title: '学校',
|
||||
dataIndex: 'school_name',
|
||||
key: 'school_name',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '班级',
|
||||
dataIndex: 'class_name',
|
||||
key: 'class_name',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '绑定状态',
|
||||
dataIndex: 'bind_status',
|
||||
key: 'bind_status',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'create_time',
|
||||
key: 'create_time',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 150,
|
||||
fixed: 'right' as const,
|
||||
},
|
||||
];
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params: StudentApi.ListParams = {
|
||||
page: pagination.current,
|
||||
limit: pagination.pageSize,
|
||||
...searchForm,
|
||||
};
|
||||
const res = await getStudentListApi(params);
|
||||
console.log('学生列表API返回:', res); // 调试用
|
||||
// 根据实际返回格式,code 为 0 表示成功
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
const data = res.data;
|
||||
console.log('学生列表数据:', data); // 调试用
|
||||
console.log('学生列表总数:', res.count); // 调试用
|
||||
if (Array.isArray(data)) {
|
||||
tableData.value = data;
|
||||
total.value = res.count || data.length;
|
||||
console.log('设置学生列表,数量:', tableData.value.length, '总数:', total.value); // 调试用
|
||||
} else {
|
||||
console.warn('学生列表数据不是数组:', data);
|
||||
tableData.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
} else {
|
||||
console.warn('获取学生列表失败,响应码:', res?.code, res);
|
||||
tableData.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取学生列表失败:', error);
|
||||
tableData.value = [];
|
||||
total.value = 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1;
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.student_id = '';
|
||||
searchForm.name = '';
|
||||
searchForm.phone = '';
|
||||
searchForm.class_id = undefined;
|
||||
searchForm.school_id = undefined;
|
||||
searchForm.bind_status = undefined;
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
router.push({ name: 'StudentDetail' });
|
||||
};
|
||||
|
||||
const handleEdit = (record: StudentApi.StudentInfo) => {
|
||||
router.push({ name: 'StudentDetail', params: { id: record.id } });
|
||||
};
|
||||
|
||||
const handleDelete = (record: StudentApi.StudentInfo) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除学生"${record.name}"吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteStudentApi({ id: record.id! });
|
||||
message.success('删除成功');
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleTableChange = (pag: any) => {
|
||||
pagination.current = pag.current;
|
||||
pagination.pageSize = pag.pageSize;
|
||||
fetchData();
|
||||
};
|
||||
|
||||
// 导入相关
|
||||
const importModalVisible = ref(false);
|
||||
const importLoading = ref(false);
|
||||
const uploadedFile = ref<string>('');
|
||||
const selectedClassId = ref<number | undefined>(undefined);
|
||||
const classList = ref<ClassApi.ClassInfo[]>([]);
|
||||
const fileList = ref<any[]>([]);
|
||||
|
||||
// 获取班级列表
|
||||
const fetchClassList = async () => {
|
||||
try {
|
||||
const res = await getClassListApi({ page: 1, limit: 1000 });
|
||||
console.log('导入对话框-班级列表API返回:', res); // 调试用
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
// 根据实际返回格式,data 是数组
|
||||
const data = res.data;
|
||||
console.log('导入对话框-班级列表数据:', data); // 调试用
|
||||
if (Array.isArray(data)) {
|
||||
classList.value = data;
|
||||
console.log('导入对话框-设置班级列表,数量:', classList.value.length); // 调试用
|
||||
} else {
|
||||
console.warn('导入对话框-班级列表数据不是数组:', data);
|
||||
classList.value = [];
|
||||
}
|
||||
} else {
|
||||
console.warn('导入对话框-获取班级列表失败:', res);
|
||||
classList.value = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('导入对话框-获取班级列表失败:', error);
|
||||
classList.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
// 打开导入对话框
|
||||
const handleImport = () => {
|
||||
importModalVisible.value = true;
|
||||
uploadedFile.value = '';
|
||||
selectedClassId.value = undefined;
|
||||
fileList.value = [];
|
||||
fetchClassList();
|
||||
};
|
||||
|
||||
// 文件上传前验证
|
||||
const beforeUpload = (file: File) => {
|
||||
const isExcel = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
|| file.type === 'application/vnd.ms-excel'
|
||||
|| file.name.endsWith('.xlsx')
|
||||
|| file.name.endsWith('.xls');
|
||||
if (!isExcel) {
|
||||
message.error('只能上传 Excel 文件(.xlsx 或 .xls)');
|
||||
return false;
|
||||
}
|
||||
const isLt10M = file.size / 1024 / 1024 < 10;
|
||||
if (!isLt10M) {
|
||||
message.error('文件大小不能超过 10MB');
|
||||
return false;
|
||||
}
|
||||
return false; // 阻止自动上传
|
||||
};
|
||||
|
||||
// 文件变化处理
|
||||
const handleFileChange = async (info: any) => {
|
||||
const { file, fileList: newFileList } = info;
|
||||
fileList.value = newFileList;
|
||||
|
||||
// 文件被移除
|
||||
if (file.status === 'removed') {
|
||||
uploadedFile.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 文件选择后,手动上传
|
||||
if (file.status === 'ready' && file.originFileObj) {
|
||||
importLoading.value = true;
|
||||
try {
|
||||
const res = await uploadStudentFileApi(file.originFileObj);
|
||||
// 根据API,上传接口返回文件路径
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
uploadedFile.value = res.data || res.file || '';
|
||||
message.success('文件上传成功');
|
||||
// 更新文件状态为已完成
|
||||
fileList.value = [{
|
||||
uid: file.uid,
|
||||
name: file.name,
|
||||
status: 'done',
|
||||
}];
|
||||
} else {
|
||||
message.error(res?.msg || '文件上传失败');
|
||||
fileList.value = [];
|
||||
uploadedFile.value = '';
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('文件上传失败:', error);
|
||||
message.error(error?.response?.data?.msg || '文件上传失败');
|
||||
fileList.value = [];
|
||||
uploadedFile.value = '';
|
||||
} finally {
|
||||
importLoading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 确认导入
|
||||
const handleConfirmImport = async () => {
|
||||
if (!uploadedFile.value) {
|
||||
message.warning('请先上传 Excel 文件');
|
||||
return;
|
||||
}
|
||||
if (!selectedClassId.value) {
|
||||
message.warning('请选择班级');
|
||||
return;
|
||||
}
|
||||
|
||||
importLoading.value = true;
|
||||
try {
|
||||
await importStudentDataApi({
|
||||
class_id: selectedClassId.value,
|
||||
file: uploadedFile.value,
|
||||
});
|
||||
message.success('学生数据导入成功');
|
||||
importModalVisible.value = false;
|
||||
uploadedFile.value = '';
|
||||
selectedClassId.value = undefined;
|
||||
fileList.value = [];
|
||||
fetchData(); // 刷新列表
|
||||
} catch (error) {
|
||||
console.error('导入失败:', error);
|
||||
message.error('学生数据导入失败');
|
||||
} finally {
|
||||
importLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 取消导入
|
||||
const handleCancelImport = () => {
|
||||
importModalVisible.value = false;
|
||||
uploadedFile.value = '';
|
||||
selectedClassId.value = undefined;
|
||||
fileList.value = [];
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="学生列表">
|
||||
<Card>
|
||||
<div class="mb-4">
|
||||
<Space wrap>
|
||||
<Input
|
||||
v-model:value="searchForm.name"
|
||||
placeholder="姓名"
|
||||
style="width: 150px"
|
||||
allow-clear
|
||||
@press-enter="handleSearch"
|
||||
/>
|
||||
<Input
|
||||
v-model:value="searchForm.phone"
|
||||
placeholder="电话"
|
||||
style="width: 150px"
|
||||
allow-clear
|
||||
@press-enter="handleSearch"
|
||||
/>
|
||||
<Input
|
||||
v-model:value="searchForm.student_id"
|
||||
placeholder="学号"
|
||||
style="width: 150px"
|
||||
allow-clear
|
||||
@press-enter="handleSearch"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="searchForm.bind_status"
|
||||
placeholder="绑定状态"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
>
|
||||
<Select.Option value="已绑定">已绑定</Select.Option>
|
||||
<Select.Option value="未绑定">未绑定</Select.Option>
|
||||
</Select>
|
||||
<Button type="primary" @click="handleSearch">搜索</Button>
|
||||
<Button @click="handleReset">重置</Button>
|
||||
<Button type="primary" @click="handleAdd">新增</Button>
|
||||
<Button @click="handleImport">导入学生</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:loading="loading"
|
||||
:scroll="{ x: 1400 }"
|
||||
:pagination="{
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
total: total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
}"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'bind_status'">
|
||||
<Tag :color="record.bind_status === '已绑定' ? 'green' : 'default'">
|
||||
{{ record.bind_status || '-' }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<Tag :color="record.status === 1 ? 'green' : 'red'">
|
||||
{{ record.status === 1 ? '启用' : '禁用' }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Space>
|
||||
<Button type="link" size="small" @click="handleEdit(record)">编辑</Button>
|
||||
<Button type="link" danger size="small" @click="handleDelete(record)">删除</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</Card>
|
||||
|
||||
<!-- 导入学生对话框 -->
|
||||
<Modal
|
||||
v-model:open="importModalVisible"
|
||||
title="导入学生"
|
||||
:confirm-loading="importLoading"
|
||||
@ok="handleConfirmImport"
|
||||
@cancel="handleCancelImport"
|
||||
width="600px"
|
||||
>
|
||||
<div class="import-form">
|
||||
<div class="form-item">
|
||||
<div class="form-label">上传 Excel 文件:</div>
|
||||
<Upload
|
||||
v-model:file-list="fileList"
|
||||
:before-upload="beforeUpload"
|
||||
@change="handleFileChange"
|
||||
:max-count="1"
|
||||
accept=".xlsx,.xls"
|
||||
>
|
||||
<Button :loading="importLoading">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="lucide:upload" />
|
||||
</template>
|
||||
选择文件
|
||||
</Button>
|
||||
</Upload>
|
||||
<div class="form-hint">
|
||||
支持 .xlsx 和 .xls 格式,文件大小不超过 10MB
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-label">选择班级:</div>
|
||||
<Select
|
||||
v-model:value="selectedClassId"
|
||||
placeholder="请选择班级"
|
||||
style="width: 100%"
|
||||
:loading="importLoading"
|
||||
show-search
|
||||
:filter-option="(input, option) => {
|
||||
const label = option?.label || '';
|
||||
return label.toLowerCase().includes(input.toLowerCase());
|
||||
}"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="item in classList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
:label="item.name"
|
||||
>
|
||||
{{ item.name }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="form-hint">
|
||||
<p><strong>导入说明:</strong></p>
|
||||
<p>1. 请先上传 Excel 文件,文件上传成功后再选择班级</p>
|
||||
<p>2. Excel 文件格式请参考系统要求</p>
|
||||
<p>3. 导入过程中请勿关闭此对话框</p>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.import-form {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: var(--ant-color-text);
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--ant-color-text-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.form-hint p {
|
||||
margin: 4px 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
139
apps/web-antd/src/views/teacher/detail.vue
Normal file
139
apps/web-antd/src/views/teacher/detail.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Card, Form, Input, InputNumber, Button, message, Radio } from 'ant-design-vue';
|
||||
import {
|
||||
getTeacherDetailApi,
|
||||
saveTeacherApi,
|
||||
type TeacherApi
|
||||
} from '#/api';
|
||||
|
||||
defineOptions({ name: 'TeacherDetail' });
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const formRef = ref();
|
||||
const loading = ref(false);
|
||||
const formData = ref<Partial<TeacherApi.SaveParams>>({
|
||||
teacher_id: null,
|
||||
name: '',
|
||||
gender: undefined,
|
||||
nickname: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
remark: '',
|
||||
status: 1,
|
||||
});
|
||||
|
||||
const isEdit = computed(() => !!route.params.id);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
loading.value = true;
|
||||
const data = { ...formData.value };
|
||||
if (isEdit.value) {
|
||||
data.id = Number(route.params.id);
|
||||
}
|
||||
await saveTeacherApi(data);
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
router.back();
|
||||
} catch (error: any) {
|
||||
console.error('保存失败:', error);
|
||||
message.error(error?.response?.data?.message || error?.response?.data?.msg || '保存失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchDetail = async () => {
|
||||
if (!isEdit.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getTeacherDetailApi({ id: Number(route.params.id) });
|
||||
// 支持 code 为 0 或 200 的成功响应
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
// 根据实际返回格式,数据在 res.data 中
|
||||
const data = res.data;
|
||||
if (data) {
|
||||
formData.value = {
|
||||
teacher_id: data.teacher_id ?? null,
|
||||
name: data.name || '',
|
||||
gender: data.gender || undefined,
|
||||
nickname: data.nickname || '',
|
||||
phone: data.phone || '',
|
||||
email: data.email || '',
|
||||
remark: data.remark || '',
|
||||
status: data.status !== undefined ? data.status : 1,
|
||||
};
|
||||
} else {
|
||||
message.error('获取教师详情失败:数据为空');
|
||||
}
|
||||
} else {
|
||||
message.error(res?.message || res?.msg || '获取教师详情失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取详情失败:', error);
|
||||
message.error(error?.response?.data?.message || error?.response?.data?.msg || '获取教师详情失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (isEdit.value) {
|
||||
fetchDetail();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :title="isEdit ? '编辑教师' : '新增教师'">
|
||||
<Card>
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:label-col="{ span: 4 }"
|
||||
:wrapper-col="{ span: 20 }"
|
||||
>
|
||||
<Form.Item
|
||||
label="姓名"
|
||||
name="name"
|
||||
:rules="[{ required: true, message: '请输入姓名' }]"
|
||||
>
|
||||
<Input v-model:value="formData.name" placeholder="请输入姓名" />
|
||||
</Form.Item>
|
||||
<Form.Item label="性别" name="gender">
|
||||
<Radio.Group v-model:value="formData.gender">
|
||||
<Radio value="男">男</Radio>
|
||||
<Radio value="女">女</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item label="昵称" name="nickname">
|
||||
<Input v-model:value="formData.nickname" placeholder="请输入昵称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="电话" name="phone">
|
||||
<Input v-model:value="formData.phone" placeholder="请输入电话" />
|
||||
</Form.Item>
|
||||
<Form.Item label="邮箱" name="email">
|
||||
<Input v-model:value="formData.email" placeholder="请输入邮箱" />
|
||||
</Form.Item>
|
||||
<Form.Item label="备注" name="remark">
|
||||
<Input.TextArea v-model:value="formData.remark" placeholder="请输入备注" :rows="3" />
|
||||
</Form.Item>
|
||||
<Form.Item label="状态" name="status">
|
||||
<Radio.Group v-model:value="formData.status">
|
||||
<Radio :value="1">启用</Radio>
|
||||
<Radio :value="0">禁用</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item :wrapper-col="{ offset: 4, span: 20 }">
|
||||
<Button type="primary" :loading="loading" @click="handleSubmit">保存</Button>
|
||||
<Button class="ml-2" @click="router.back()">取消</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
191
apps/web-antd/src/views/teacher/list.vue
Normal file
191
apps/web-antd/src/views/teacher/list.vue
Normal file
@@ -0,0 +1,191 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Button, Card, Table, Space, message, Modal, Input } from 'ant-design-vue';
|
||||
import {
|
||||
getTeacherListApi,
|
||||
deleteTeacherApi,
|
||||
type TeacherApi
|
||||
} from '#/api';
|
||||
|
||||
defineOptions({ name: 'TeacherList' });
|
||||
|
||||
const router = useRouter();
|
||||
const loading = ref(false);
|
||||
const tableData = ref<TeacherApi.TeacherInfo[]>([]);
|
||||
const total = ref(0);
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const searchForm = reactive({
|
||||
teacher_id: '',
|
||||
name: '',
|
||||
nickname: '',
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '性别',
|
||||
dataIndex: 'gender',
|
||||
key: 'gender',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '昵称',
|
||||
dataIndex: 'nickname',
|
||||
key: 'nickname',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '电话',
|
||||
dataIndex: 'phone',
|
||||
key: 'phone',
|
||||
width: 130,
|
||||
},
|
||||
{
|
||||
title: '管理的教室',
|
||||
key: 'classroom_names',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
},
|
||||
];
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params: TeacherApi.ListParams = {
|
||||
page: pagination.current,
|
||||
limit: pagination.pageSize,
|
||||
...searchForm,
|
||||
};
|
||||
const res = await getTeacherListApi(params);
|
||||
// 根据实际返回格式,code 为 0 表示成功,返回格式为 {code, msg, count, data}
|
||||
if (res && (res.code === 0 || res.code === 200)) {
|
||||
const data = Array.isArray(res.data) ? res.data : [];
|
||||
tableData.value = data;
|
||||
total.value = res.count || 0;
|
||||
} else {
|
||||
tableData.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取教师列表失败:', error);
|
||||
tableData.value = [];
|
||||
total.value = 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1;
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const handleEdit = (record: TeacherApi.TeacherInfo) => {
|
||||
router.push({
|
||||
name: 'TeacherDetail',
|
||||
params: { id: record.id }
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = (record: TeacherApi.TeacherInfo) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除教师"${record.name}"吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteTeacherApi({ id: record.id! });
|
||||
message.success('删除成功');
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleTableChange = (pag: any) => {
|
||||
pagination.current = pag.current;
|
||||
pagination.pageSize = pag.pageSize;
|
||||
fetchData();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="教师列表">
|
||||
<Card>
|
||||
<div class="mb-4">
|
||||
<Space>
|
||||
<Input
|
||||
v-model:value="searchForm.teacher_id"
|
||||
placeholder="工号"
|
||||
style="width: 200px"
|
||||
@press-enter="handleSearch"
|
||||
/>
|
||||
<Input
|
||||
v-model:value="searchForm.name"
|
||||
placeholder="姓名"
|
||||
style="width: 200px"
|
||||
@press-enter="handleSearch"
|
||||
/>
|
||||
<Button type="primary" @click="handleSearch">搜索</Button>
|
||||
<Button type="primary" @click="router.push({ name: 'TeacherDetail' })">新增</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:loading="loading"
|
||||
:pagination="{
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
total: total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
}"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'classroom_names'">
|
||||
<span v-if="record.classroom_names && record.classroom_names.length > 0">
|
||||
{{ record.classroom_names.join('、') }}
|
||||
</span>
|
||||
<span v-else style="color: #999;">-</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Space>
|
||||
<Button type="link" size="small" @click="handleEdit(record)">编辑</Button>
|
||||
<Button type="link" danger size="small" @click="handleDelete(record)">删除</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user