初始化项目
This commit is contained in:
7
src/views/_builtin/403/index.vue
Normal file
7
src/views/_builtin/403/index.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<ExceptionBase type="403" />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
7
src/views/_builtin/404/index.vue
Normal file
7
src/views/_builtin/404/index.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<ExceptionBase type="404" />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
7
src/views/_builtin/500/index.vue
Normal file
7
src/views/_builtin/500/index.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<ExceptionBase type="500" />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
73
src/views/_builtin/login/index.vue
Normal file
73
src/views/_builtin/login/index.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { Component } from 'vue';
|
||||
import { getColorPalette, mixColor } from '@sa/utils';
|
||||
import { $t } from '@/locales';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { loginModuleRecord } from '@/constants/app';
|
||||
import PwdLogin from './modules/pwd-login.vue';
|
||||
import CodeLogin from './modules/code-login.vue';
|
||||
import Register from './modules/register.vue';
|
||||
import ResetPwd from './modules/reset-pwd.vue';
|
||||
import BindWechat from './modules/bind-wechat.vue';
|
||||
|
||||
interface Props {
|
||||
/** The login module */
|
||||
module?: UnionKey.LoginModule;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
interface LoginModule {
|
||||
label: string;
|
||||
component: Component;
|
||||
}
|
||||
|
||||
const moduleMap: Record<UnionKey.LoginModule, LoginModule> = {
|
||||
'pwd-login': { label: loginModuleRecord['pwd-login'], component: PwdLogin },
|
||||
'code-login': { label: loginModuleRecord['code-login'], component: CodeLogin },
|
||||
register: { label: loginModuleRecord.register, component: Register },
|
||||
'reset-pwd': { label: loginModuleRecord['reset-pwd'], component: ResetPwd },
|
||||
'bind-wechat': { label: loginModuleRecord['bind-wechat'], component: BindWechat }
|
||||
};
|
||||
|
||||
const activeModule = computed(() => moduleMap[props.module || 'pwd-login']);
|
||||
|
||||
const bgThemeColor = computed(() =>
|
||||
themeStore.darkMode ? getColorPalette(themeStore.themeColor, 7) : themeStore.themeColor
|
||||
);
|
||||
|
||||
const bgColor = computed(() => {
|
||||
const COLOR_WHITE = '#ffffff';
|
||||
|
||||
const ratio = themeStore.darkMode ? 0.5 : 0.2;
|
||||
|
||||
return mixColor(COLOR_WHITE, themeStore.themeColor, ratio);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative size-full flex-center" :style="{ backgroundColor: bgColor }">
|
||||
<WaveBg :theme-color="bgThemeColor" />
|
||||
<ACard class="relative z-4">
|
||||
<div class="w-400px lt-sm:w-300px">
|
||||
<header class="flex-y-center justify-between">
|
||||
<SystemLogo class="text-64px text-primary lt-sm:text-48px" />
|
||||
<h3 class="text-28px text-primary font-500 lt-sm:text-22px">{{ $t('system.title') }}</h3>
|
||||
</header>
|
||||
<main class="pt-24px">
|
||||
<h3 class="text-18px text-primary font-medium">{{ $t(activeModule.label) }}</h3>
|
||||
<div class="animation-slide-in-left pt-24px">
|
||||
<Transition :name="themeStore.page.animateMode" mode="out-in" appear>
|
||||
<component :is="activeModule.component" />
|
||||
</Transition>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</ACard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
11
src/views/_builtin/login/modules/bind-wechat.vue
Normal file
11
src/views/_builtin/login/modules/bind-wechat.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'BindWechat'
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
66
src/views/_builtin/login/modules/code-login.vue
Normal file
66
src/views/_builtin/login/modules/code-login.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive } from 'vue';
|
||||
import { $t } from '@/locales';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useAntdForm, useFormRules } from '@/hooks/common/form';
|
||||
import { useCaptcha } from '@/hooks/business/captcha';
|
||||
|
||||
defineOptions({
|
||||
name: 'CodeLogin'
|
||||
});
|
||||
|
||||
const { toggleLoginModule } = useRouterPush();
|
||||
const { formRef, validate } = useAntdForm();
|
||||
const { label, isCounting, loading, getCaptcha } = useCaptcha();
|
||||
|
||||
interface FormModel {
|
||||
phone: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
const model: FormModel = reactive({
|
||||
phone: '',
|
||||
code: ''
|
||||
});
|
||||
|
||||
const rules = computed<Record<keyof FormModel, App.Global.FormRule[]>>(() => {
|
||||
const { formRules } = useFormRules();
|
||||
|
||||
return {
|
||||
phone: formRules.phone,
|
||||
code: formRules.code
|
||||
};
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
// request
|
||||
$message?.success($t('page.login.common.validateSuccess'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AForm ref="formRef" :model="model" :rules="rules">
|
||||
<AFormItem name="phone">
|
||||
<AInput v-model:value="model.phone" size="large" :placeholder="$t('page.login.common.phonePlaceholder')" />
|
||||
</AFormItem>
|
||||
<AFormItem name="code">
|
||||
<div class="w-full flex-y-center gap-16px">
|
||||
<AInput v-model:value="model.code" size="large" :placeholder="$t('page.login.common.codePlaceholder')" />
|
||||
<AButton size="large" :disabled="isCounting" :loading="loading" @click="getCaptcha(model.phone)">
|
||||
{{ label }}
|
||||
</AButton>
|
||||
</div>
|
||||
</AFormItem>
|
||||
<ASpace direction="vertical" size="large" class="w-full">
|
||||
<AButton type="primary" block size="large" shape="round" @click="handleSubmit">
|
||||
{{ $t('common.confirm') }}
|
||||
</AButton>
|
||||
<AButton block size="large" shape="round" @click="toggleLoginModule('pwd-login')">
|
||||
{{ $t('page.login.common.back') }}
|
||||
</AButton>
|
||||
</ASpace>
|
||||
</AForm>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
81
src/views/_builtin/login/modules/pwd-login.vue
Normal file
81
src/views/_builtin/login/modules/pwd-login.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
import { $t } from '@/locales';
|
||||
import { useAntdForm, useFormRules } from '@/hooks/common/form';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
|
||||
defineOptions({
|
||||
name: 'PwdLogin'
|
||||
});
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const { formRef, validate } = useAntdForm();
|
||||
const { patternRules } = useFormRules();
|
||||
const codeImg = ref('');
|
||||
|
||||
getCheckCode();
|
||||
|
||||
const model = reactive({
|
||||
username: 'ryadmin',
|
||||
password: 'admin123',
|
||||
code: '',
|
||||
uuid: ''
|
||||
});
|
||||
|
||||
const rules = {
|
||||
username: patternRules.username,
|
||||
password: patternRules.pwd
|
||||
};
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
await authStore.login({
|
||||
loginForm: model,
|
||||
onError() {
|
||||
getCheckCode();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function getCheckCode() {
|
||||
const { data, error } = await doGetCheckCode();
|
||||
if (!error) {
|
||||
codeImg.value = `data:image/png;base64,${data.img}`;
|
||||
model.uuid = data.uuid;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AForm ref="formRef" :model="model" :rules="rules">
|
||||
<AFormItem name="username">
|
||||
<AInput v-model:value="model.username" size="large" :placeholder="$t('page.login.common.userNamePlaceholder')" />
|
||||
</AFormItem>
|
||||
<AFormItem name="password">
|
||||
<AInputPassword
|
||||
v-model:value="model.password"
|
||||
size="large"
|
||||
:placeholder="$t('page.login.common.passwordPlaceholder')"
|
||||
/>
|
||||
</AFormItem>
|
||||
<AFormItem>
|
||||
<div class="flex gap-4">
|
||||
<AInput v-model:value="model.code" size="large" :placeholder="$t('page.login.common.checkCode')">
|
||||
<template #suffix></template>
|
||||
</AInput>
|
||||
<AImage :preview="false" :src="codeImg" @click="getCheckCode" />
|
||||
</div>
|
||||
</AFormItem>
|
||||
|
||||
<ASpace direction="vertical" size="large" class="w-full">
|
||||
<div class="flex-y-center justify-between">
|
||||
<ACheckbox>{{ $t('page.login.pwdLogin.rememberMe') }}</ACheckbox>
|
||||
<AButton type="text">{{ $t('page.login.pwdLogin.forgetPassword') }}</AButton>
|
||||
</div>
|
||||
<AButton type="primary" block size="large" shape="round" :loading="authStore.loginLoading" @click="handleSubmit">
|
||||
{{ $t('common.confirm') }}
|
||||
</AButton>
|
||||
</ASpace>
|
||||
</AForm>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
86
src/views/_builtin/login/modules/register.vue
Normal file
86
src/views/_builtin/login/modules/register.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive } from 'vue';
|
||||
import { $t } from '@/locales';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useAntdForm, useFormRules } from '@/hooks/common/form';
|
||||
import { useCaptcha } from '@/hooks/business/captcha';
|
||||
|
||||
defineOptions({
|
||||
name: 'CodeLogin'
|
||||
});
|
||||
|
||||
const { toggleLoginModule } = useRouterPush();
|
||||
const { formRef, validate } = useAntdForm();
|
||||
const { label, isCounting, loading, getCaptcha } = useCaptcha();
|
||||
|
||||
interface FormModel {
|
||||
phone: string;
|
||||
code: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
|
||||
const model: FormModel = reactive({
|
||||
phone: '',
|
||||
code: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
});
|
||||
|
||||
const rules = computed<Record<keyof FormModel, App.Global.FormRule[]>>(() => {
|
||||
const { formRules, createConfirmPwdRule } = useFormRules();
|
||||
|
||||
return {
|
||||
phone: formRules.phone,
|
||||
code: formRules.code,
|
||||
password: formRules.pwd,
|
||||
confirmPassword: createConfirmPwdRule(model.password)
|
||||
};
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
// request to register
|
||||
$message?.success($t('page.login.common.validateSuccess'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AForm ref="formRef" :model="model" :rules="rules">
|
||||
<AFormItem name="phone">
|
||||
<AInput v-model:value="model.phone" size="large" :placeholder="$t('page.login.common.phonePlaceholder')" />
|
||||
</AFormItem>
|
||||
<AFormItem name="code">
|
||||
<div class="w-full flex-y-center gap-16px">
|
||||
<AInput v-model:value="model.code" size="large" :placeholder="$t('page.login.common.codePlaceholder')" />
|
||||
<AButton size="large" :disabled="isCounting" :loading="loading" @click="getCaptcha(model.phone)">
|
||||
{{ label }}
|
||||
</AButton>
|
||||
</div>
|
||||
</AFormItem>
|
||||
<AFormItem name="password">
|
||||
<AInputPassword
|
||||
v-model:value="model.password"
|
||||
size="large"
|
||||
:placeholder="$t('page.login.common.passwordPlaceholder')"
|
||||
/>
|
||||
</AFormItem>
|
||||
<AFormItem name="confirmPassword">
|
||||
<AInputPassword
|
||||
v-model:value="model.confirmPassword"
|
||||
size="large"
|
||||
:placeholder="$t('page.login.common.confirmPasswordPlaceholder')"
|
||||
/>
|
||||
</AFormItem>
|
||||
<ASpace direction="vertical" size="large" class="w-full">
|
||||
<AButton type="primary" block size="large" shape="round" @click="handleSubmit">
|
||||
{{ $t('common.confirm') }}
|
||||
</AButton>
|
||||
<AButton block size="large" shape="round" @click="toggleLoginModule('pwd-login')">
|
||||
{{ $t('page.login.common.back') }}
|
||||
</AButton>
|
||||
</ASpace>
|
||||
</AForm>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
80
src/views/_builtin/login/modules/reset-pwd.vue
Normal file
80
src/views/_builtin/login/modules/reset-pwd.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive } from 'vue';
|
||||
import { $t } from '@/locales';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useAntdForm, useFormRules } from '@/hooks/common/form';
|
||||
|
||||
defineOptions({
|
||||
name: 'ResetPwd'
|
||||
});
|
||||
|
||||
const { toggleLoginModule } = useRouterPush();
|
||||
const { formRef, validate } = useAntdForm();
|
||||
|
||||
interface FormModel {
|
||||
phone: string;
|
||||
code: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
|
||||
const model: FormModel = reactive({
|
||||
phone: '',
|
||||
code: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
});
|
||||
|
||||
type RuleRecord = Partial<Record<keyof FormModel, App.Global.FormRule[]>>;
|
||||
|
||||
const rules = computed<RuleRecord>(() => {
|
||||
const { formRules, createConfirmPwdRule } = useFormRules();
|
||||
|
||||
return {
|
||||
phone: formRules.phone,
|
||||
password: formRules.pwd,
|
||||
confirmPassword: createConfirmPwdRule(model.password)
|
||||
};
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
// request to reset password
|
||||
$message?.success($t('page.login.common.validateSuccess'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AForm ref="formRef" :model="model" :rules="rules">
|
||||
<AFormItem name="phone">
|
||||
<AInput v-model:value="model.phone" size="large" :placeholder="$t('page.login.common.phonePlaceholder')" />
|
||||
</AFormItem>
|
||||
<AFormItem name="code">
|
||||
<AInput v-model:value="model.code" size="large" :placeholder="$t('page.login.common.codePlaceholder')" />
|
||||
</AFormItem>
|
||||
<AFormItem name="password">
|
||||
<AInputPassword
|
||||
v-model:value="model.password"
|
||||
size="large"
|
||||
:placeholder="$t('page.login.common.passwordPlaceholder')"
|
||||
/>
|
||||
</AFormItem>
|
||||
<AFormItem name="confirmPassword">
|
||||
<AInputPassword
|
||||
v-model:value="model.confirmPassword"
|
||||
size="large"
|
||||
:placeholder="$t('page.login.common.confirmPasswordPlaceholder')"
|
||||
/>
|
||||
</AFormItem>
|
||||
<ASpace direction="vertical" size="large" class="w-full">
|
||||
<AButton type="primary" block size="large" shape="round" @click="handleSubmit">
|
||||
{{ $t('common.confirm') }}
|
||||
</AButton>
|
||||
<AButton block size="large" shape="round" @click="toggleLoginModule('pwd-login')">
|
||||
{{ $t('page.login.common.back') }}
|
||||
</AButton>
|
||||
</ASpace>
|
||||
</AForm>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
79
src/views/about/index.vue
Normal file
79
src/views/about/index.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<script setup lang="ts">
|
||||
import { $t } from '@/locales';
|
||||
import pkg from '~/package.json';
|
||||
|
||||
interface PkgJson {
|
||||
name: string;
|
||||
version: string;
|
||||
dependencies: PkgVersionInfo[];
|
||||
devDependencies: PkgVersionInfo[];
|
||||
}
|
||||
|
||||
interface PkgVersionInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
const { name, version, dependencies, devDependencies } = pkg;
|
||||
|
||||
function transformVersionData(tuple: [string, string]): PkgVersionInfo {
|
||||
const [$name, $version] = tuple;
|
||||
return {
|
||||
name: $name,
|
||||
version: $version
|
||||
};
|
||||
}
|
||||
|
||||
const pkgJson: PkgJson = {
|
||||
name,
|
||||
version,
|
||||
dependencies: Object.entries(dependencies).map(item => transformVersionData(item)),
|
||||
devDependencies: Object.entries(devDependencies).map(item => transformVersionData(item))
|
||||
};
|
||||
|
||||
const latestBuildTime = BUILD_TIME;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ASpace direction="vertical" :size="16">
|
||||
<ACard :title="$t('page.about.title')" :bordered="false" size="small" class="card-wrapper">
|
||||
<p>{{ $t('page.about.introduction') }}</p>
|
||||
</ACard>
|
||||
<ACard :title="$t('page.about.projectInfo.title')" :bordered="false" size="small" class="card-wrapper">
|
||||
<ADescriptions label-placement="left" bordered size="small" :column="{ xs: 1, sm: 2 }">
|
||||
<ADescriptionsItem :label="$t('page.about.projectInfo.version')">
|
||||
<ATag color="blue">{{ pkgJson.version }}</ATag>
|
||||
</ADescriptionsItem>
|
||||
<ADescriptionsItem :label="$t('page.about.projectInfo.latestBuildTime')">
|
||||
<ATag color="blue">{{ latestBuildTime }}</ATag>
|
||||
</ADescriptionsItem>
|
||||
<ADescriptionsItem :label="$t('page.about.projectInfo.githubLink')">
|
||||
<a class="text-primary" :href="pkg.homepage" target="_blank" rel="noopener noreferrer">
|
||||
{{ $t('page.about.projectInfo.githubLink') }}
|
||||
</a>
|
||||
</ADescriptionsItem>
|
||||
<ADescriptionsItem :label="$t('page.about.projectInfo.previewLink')">
|
||||
<a class="text-primary" :href="pkg.website" target="_blank" rel="noopener noreferrer">
|
||||
{{ $t('page.about.projectInfo.previewLink') }}
|
||||
</a>
|
||||
</ADescriptionsItem>
|
||||
</ADescriptions>
|
||||
</ACard>
|
||||
<ACard :title="$t('page.about.prdDep')" :bordered="false" size="small" class="card-wrapper">
|
||||
<ADescriptions label-placement="left" bordered size="small" :column="{ xs: 1, sm: 2 }">
|
||||
<ADescriptionsItem v-for="item in pkgJson.dependencies" :key="item.name" :label="item.name">
|
||||
{{ item.version }}
|
||||
</ADescriptionsItem>
|
||||
</ADescriptions>
|
||||
</ACard>
|
||||
<ACard :title="$t('page.about.devDep')" :bordered="false" size="small" class="card-wrapper">
|
||||
<ADescriptions label-placement="left" bordered size="small" :column="{ xs: 1, sm: 2 }">
|
||||
<ADescriptionsItem v-for="item in pkgJson.devDependencies" :key="item.name" :label="item.name">
|
||||
{{ item.version }}
|
||||
</ADescriptionsItem>
|
||||
</ADescriptions>
|
||||
</ACard>
|
||||
</ASpace>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
7
src/views/function/hide-child/one/index.vue
Normal file
7
src/views/function/hide-child/one/index.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<LookForward />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
7
src/views/function/hide-child/three/index.vue
Normal file
7
src/views/function/hide-child/three/index.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div>three</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
7
src/views/function/hide-child/two/index.vue
Normal file
7
src/views/function/hide-child/two/index.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div>two</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
24
src/views/function/multi-tab/index.vue
Normal file
24
src/views/function/multi-tab/index.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router';
|
||||
import { computed } from 'vue';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
const route = useRoute();
|
||||
const { routerPushByKey } = useRouterPush();
|
||||
|
||||
const routeQuery = computed(() => JSON.stringify(route.query));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<LookForward>
|
||||
<div>
|
||||
<AButton @click="routerPushByKey('function_tab')">{{ $t('page.function.multiTab.backTab') }}</AButton>
|
||||
<div class="py-24px">{{ $t('page.function.multiTab.routeParam') }}: {{ routeQuery }}</div>
|
||||
</div>
|
||||
</LookForward>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
30
src/views/function/request/index.vue
Normal file
30
src/views/function/request/index.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { $t } from '@/locales';
|
||||
async function logout() {
|
||||
await fetchCustomBackendError('8888', $t('request.logoutMsg'));
|
||||
}
|
||||
|
||||
async function logoutWithModal() {
|
||||
await fetchCustomBackendError('7777', $t('request.logoutWithModalMsg'));
|
||||
}
|
||||
|
||||
async function refreshToken() {
|
||||
await fetchCustomBackendError('9999', $t('request.tokenExpired'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ASpace direction="vertical" :size="16">
|
||||
<ACard :title="$t('request.logout')" :bordered="false" size="small" class="card-wrapper">
|
||||
<AButton @click="logout">{{ $t('common.trigger') }}</AButton>
|
||||
</ACard>
|
||||
<ACard :title="$t('request.logoutWithModal')" :bordered="false" size="small" class="card-wrapper">
|
||||
<AButton @click="logoutWithModal">{{ $t('common.trigger') }}</AButton>
|
||||
</ACard>
|
||||
<ACard :title="$t('request.refreshToken')" :bordered="false" size="small" class="card-wrapper">
|
||||
<AButton @click="refreshToken">{{ $t('common.trigger') }}</AButton>
|
||||
</ACard>
|
||||
</ASpace>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
7
src/views/function/super-page/index.vue
Normal file
7
src/views/function/super-page/index.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<LookForward />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
62
src/views/function/tab/index.vue
Normal file
62
src/views/function/tab/index.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { $t } from '@/locales';
|
||||
import { useTabStore } from '@/store/modules/tab';
|
||||
|
||||
const tabStore = useTabStore();
|
||||
const { routerPushByKey } = useRouterPush();
|
||||
|
||||
const tabLabel = ref('');
|
||||
|
||||
function changeTabLabel() {
|
||||
tabStore.setTabLabel(tabLabel.value);
|
||||
}
|
||||
|
||||
function resetTabLabel() {
|
||||
tabStore.resetTabLabel();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ASpace direction="vertical" :size="16">
|
||||
<ACard :title="$t('page.function.tab.tabOperate.title')" :bordered="false" size="small" class="card-wrapper">
|
||||
<ADivider orientation="left">{{ $t('page.function.tab.tabOperate.addTab') }}</ADivider>
|
||||
<AButton @click="routerPushByKey('about')">{{ $t('page.function.tab.tabOperate.addTabDesc') }}</AButton>
|
||||
|
||||
<ADivider orientation="left">{{ $t('page.function.tab.tabOperate.closeTab') }}</ADivider>
|
||||
<ASpace :size="16">
|
||||
<AButton @click="tabStore.removeActiveTab">
|
||||
{{ $t('page.function.tab.tabOperate.closeCurrentTab') }}
|
||||
</AButton>
|
||||
<AButton @click="tabStore.removeTabByRouteName('about')">
|
||||
{{ $t('page.function.tab.tabOperate.closeAboutTab') }}
|
||||
</AButton>
|
||||
</ASpace>
|
||||
|
||||
<ADivider orientation="left">{{ $t('page.function.tab.tabOperate.addMultiTab') }}</ADivider>
|
||||
<ASpace :size="16" wrap class="m-0!">
|
||||
<AButton @click="routerPushByKey('function_multi-tab')">
|
||||
{{ $t('page.function.tab.tabOperate.addMultiTabDesc1') }}
|
||||
</AButton>
|
||||
<AButton @click="routerPushByKey('function_multi-tab', { query: { a: '1' } })">
|
||||
{{ $t('page.function.tab.tabOperate.addMultiTabDesc2') }}
|
||||
</AButton>
|
||||
</ASpace>
|
||||
</ACard>
|
||||
<ACard :title="$t('page.function.tab.tabTitle.title')" :bordered="false" size="small" class="card-wrapper">
|
||||
<ADivider orientation="left">{{ $t('page.function.tab.tabTitle.changeTitle') }}</ADivider>
|
||||
<AInputSearch
|
||||
v-model:value="tabLabel"
|
||||
:enter-button="$t('page.function.tab.tabTitle.change')"
|
||||
class="max-w-240px"
|
||||
@search="changeTabLabel"
|
||||
/>
|
||||
|
||||
<ADivider orientation="left">{{ $t('page.function.tab.tabTitle.resetTitle') }}</ADivider>
|
||||
<AButton @click="resetTabLabel">{{ $t('page.function.tab.tabTitle.reset') }}</AButton>
|
||||
</ACard>
|
||||
</ASpace>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
92
src/views/function/toggle-auth/index.vue
Normal file
92
src/views/function/toggle-auth/index.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { useLoading } from '@sa/hooks';
|
||||
import { $t } from '@/locales';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
import { useAuth } from '@/hooks/business/auth';
|
||||
|
||||
const appStore = useAppStore();
|
||||
const authStore = useAuthStore();
|
||||
const { hasAuth } = useAuth();
|
||||
const { loading, startLoading, endLoading } = useLoading();
|
||||
|
||||
type AccountKey = 'super' | 'admin' | 'user';
|
||||
|
||||
interface Account {
|
||||
key: AccountKey;
|
||||
label: string;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const accounts = computed<Account[]>(() => [
|
||||
{
|
||||
key: 'super',
|
||||
label: $t('page.login.pwdLogin.superAdmin'),
|
||||
username: 'Super',
|
||||
password: '123456'
|
||||
},
|
||||
{
|
||||
key: 'admin',
|
||||
label: $t('page.login.pwdLogin.admin'),
|
||||
username: 'Admin',
|
||||
password: '123456'
|
||||
},
|
||||
{
|
||||
key: 'user',
|
||||
label: $t('page.login.pwdLogin.user'),
|
||||
username: 'User',
|
||||
password: '123456'
|
||||
}
|
||||
]);
|
||||
|
||||
const loginAccount = ref<AccountKey>('super');
|
||||
|
||||
async function handleToggleAccount(account: Account) {
|
||||
loginAccount.value = account.key;
|
||||
|
||||
startLoading();
|
||||
await authStore.login(account.username, account.password, false);
|
||||
endLoading();
|
||||
appStore.reloadPage();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ASpace direction="vertical" :size="16">
|
||||
<ACard :title="$t('route.function_toggle-auth')" :bordered="false" size="small" class="card-wrapper">
|
||||
<ADescriptions layout="vertical" bordered size="small" :column="1">
|
||||
<ADescriptionsItem :label="$t('page.manage.user.userRole')">
|
||||
<ASpace>
|
||||
<ATag v-for="role in authStore.userInfo.roles" :key="role">{{ role }}</ATag>
|
||||
</ASpace>
|
||||
</ADescriptionsItem>
|
||||
<ADescriptionsItem ions-item :label="$t('page.function.toggleAuth.toggleAccount')">
|
||||
<ASpace>
|
||||
<AButton
|
||||
v-for="account in accounts"
|
||||
:key="account.key"
|
||||
:loading="loading && loginAccount === account.key"
|
||||
:disabled="loading && loginAccount !== account.key"
|
||||
@click="handleToggleAccount(account)"
|
||||
>
|
||||
{{ account.label }}
|
||||
</AButton>
|
||||
</ASpace>
|
||||
</ADescriptionsItem>
|
||||
</ADescriptions>
|
||||
</ACard>
|
||||
<ACard :title="$t('page.function.toggleAuth.authHook')" :bordered="false" size="small" class="card-wrapper">
|
||||
<ASpace>
|
||||
<AButton v-if="hasAuth('B_CODE1')">{{ $t('page.function.toggleAuth.superAdminVisible') }}</AButton>
|
||||
<AButton v-if="hasAuth('B_CODE2')">{{ $t('page.function.toggleAuth.adminVisible') }}</AButton>
|
||||
<AButton v-if="hasAuth('B_CODE3')">
|
||||
{{ $t('page.function.toggleAuth.adminOrUserVisible') }}
|
||||
</AButton>
|
||||
</ASpace>
|
||||
</ACard>
|
||||
</ASpace>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
33
src/views/home/index.vue
Normal file
33
src/views/home/index.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import HeaderBanner from './modules/header-banner.vue';
|
||||
import CardData from './modules/card-data.vue';
|
||||
import LineChart from './modules/line-chart.vue';
|
||||
import PieChart from './modules/pie-chart.vue';
|
||||
import ProjectNews from './modules/project-news.vue';
|
||||
import CreativityBanner from './modules/creativity-banner.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ASpace direction="vertical" :size="16">
|
||||
<HeaderBanner />
|
||||
<CardData />
|
||||
<ARow :gutter="[16, 16]">
|
||||
<ACol :span="24" :lg="14">
|
||||
<LineChart />
|
||||
</ACol>
|
||||
<ACol :span="24" :lg="10">
|
||||
<PieChart />
|
||||
</ACol>
|
||||
</ARow>
|
||||
<ARow :gutter="[16, 16]">
|
||||
<ACol :span="24" :lg="14">
|
||||
<ProjectNews />
|
||||
</ACol>
|
||||
<ACol :span="24" :lg="10">
|
||||
<CreativityBanner />
|
||||
</ACol>
|
||||
</ARow>
|
||||
</ASpace>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
109
src/views/home/modules/card-data.vue
Normal file
109
src/views/home/modules/card-data.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { createReusableTemplate } from '@vueuse/core';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'CardData'
|
||||
});
|
||||
|
||||
interface CardData {
|
||||
key: string;
|
||||
title: string;
|
||||
value: number;
|
||||
unit: string;
|
||||
color: {
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const cardData = computed<CardData[]>(() => [
|
||||
{
|
||||
key: 'visitCount',
|
||||
title: $t('page.home.visitCount'),
|
||||
value: 9725,
|
||||
unit: '',
|
||||
color: {
|
||||
start: '#ec4786',
|
||||
end: '#b955a4'
|
||||
},
|
||||
icon: 'ant-design:bar-chart-outlined'
|
||||
},
|
||||
{
|
||||
key: 'turnover',
|
||||
title: $t('page.home.turnover'),
|
||||
value: 1026,
|
||||
unit: '$',
|
||||
color: {
|
||||
start: '#865ec0',
|
||||
end: '#5144b4'
|
||||
},
|
||||
icon: 'ant-design:money-collect-outlined'
|
||||
},
|
||||
{
|
||||
key: 'downloadCount',
|
||||
title: $t('page.home.downloadCount'),
|
||||
value: 970925,
|
||||
unit: '',
|
||||
color: {
|
||||
start: '#56cdf3',
|
||||
end: '#719de3'
|
||||
},
|
||||
icon: 'carbon:document-download'
|
||||
},
|
||||
{
|
||||
key: 'dealCount',
|
||||
title: $t('page.home.dealCount'),
|
||||
value: 9527,
|
||||
unit: '',
|
||||
color: {
|
||||
start: '#fcbc25',
|
||||
end: '#f68057'
|
||||
},
|
||||
icon: 'ant-design:trademark-circle-outlined'
|
||||
}
|
||||
]);
|
||||
|
||||
interface GradientBgProps {
|
||||
gradientColor: string;
|
||||
}
|
||||
|
||||
const [DefineGradientBg, GradientBg] = createReusableTemplate<GradientBgProps>();
|
||||
|
||||
function getGradientColor(color: CardData['color']) {
|
||||
return `linear-gradient(to bottom right, ${color.start}, ${color.end})`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ACard :bordered="false" size="small" class="card-wrapper">
|
||||
<!-- define component start: GradientBg -->
|
||||
<DefineGradientBg v-slot="{ $slots, gradientColor }">
|
||||
<div class="rd-8px px-16px pb-4px pt-8px text-white" :style="{ backgroundImage: gradientColor }">
|
||||
<component :is="$slots.default" />
|
||||
</div>
|
||||
</DefineGradientBg>
|
||||
<!-- define component end: GradientBg -->
|
||||
|
||||
<ARow :gutter="[16, 16]">
|
||||
<ACol v-for="item in cardData" :key="item.key" :span="24" :md="12" :lg="6">
|
||||
<GradientBg :gradient-color="getGradientColor(item.color)" class="flex-1">
|
||||
<h3 class="text-16px">{{ item.title }}</h3>
|
||||
<div class="flex justify-between pt-12px">
|
||||
<SvgIcon :icon="item.icon" class="text-32px" />
|
||||
<CountTo
|
||||
:prefix="item.unit"
|
||||
:start-value="1"
|
||||
:end-value="item.value"
|
||||
class="text-30px text-white dark:text-dark"
|
||||
/>
|
||||
</div>
|
||||
</GradientBg>
|
||||
</ACol>
|
||||
</ARow>
|
||||
</ACard>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
23
src/views/home/modules/creativity-banner.vue
Normal file
23
src/views/home/modules/creativity-banner.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'CreativityBanner'
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ACard
|
||||
:title="$t('page.home.creativity')"
|
||||
:bordered="false"
|
||||
size="small"
|
||||
class="h-full flex-col-stretch card-wrapper"
|
||||
:body-style="{ flex: 1, overflow: 'hidden' }"
|
||||
>
|
||||
<div class="h-full flex-center">
|
||||
<icon-local-banner class="text-400px text-primary sm:text-320px" />
|
||||
</div>
|
||||
</ACard>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
62
src/views/home/modules/header-banner.vue
Normal file
62
src/views/home/modules/header-banner.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { $t } from '@/locales';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
|
||||
defineOptions({
|
||||
name: 'HeaderBanner'
|
||||
});
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
interface StatisticData {
|
||||
id: number;
|
||||
title: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const statisticData = computed<StatisticData[]>(() => [
|
||||
{
|
||||
id: 0,
|
||||
title: $t('page.home.projectCount'),
|
||||
value: '25'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: $t('page.home.todo'),
|
||||
value: '4/16'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: $t('page.home.message'),
|
||||
value: '12'
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ACard :bordered="false" class="card-wrapper">
|
||||
<ARow :gutter="[16, 16]">
|
||||
<ACol :span="24" :md="18">
|
||||
<div class="flex-y-center">
|
||||
<div class="size-72px shrink-0 overflow-hidden rd-1/2">
|
||||
<img src="@/assets/imgs/soybean.jpg" class="size-full" />
|
||||
</div>
|
||||
<div class="pl-12px">
|
||||
<h3 class="text-18px font-semibold">
|
||||
{{ $t('page.home.greeting', { username: authStore.userInfo.username }) }}
|
||||
</h3>
|
||||
<p class="text-#999 leading-30px">{{ $t('page.home.weatherDesc') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</ACol>
|
||||
<ACol :span="24" :md="6">
|
||||
<ASpace class="w-full justify-end" :size="24">
|
||||
<AStatistic v-for="item in statisticData" :key="item.id" class="whitespace-nowrap" v-bind="item" />
|
||||
</ASpace>
|
||||
</ACol>
|
||||
</ARow>
|
||||
</ACard>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
151
src/views/home/modules/line-chart.vue
Normal file
151
src/views/home/modules/line-chart.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<script setup lang="ts">
|
||||
import { watch } from 'vue';
|
||||
import { $t } from '@/locales';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useEcharts } from '@/hooks/common/echarts';
|
||||
|
||||
defineOptions({
|
||||
name: 'LineChart'
|
||||
});
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const { domRef, updateOptions } = useEcharts(() => ({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: [$t('page.home.downloadCount'), $t('page.home.registerCount')]
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: [] as string[]
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
color: '#8e9dff',
|
||||
name: $t('page.home.downloadCount'),
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
stack: 'Total',
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0.25,
|
||||
color: '#8e9dff'
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: '#fff'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: [] as number[]
|
||||
},
|
||||
{
|
||||
color: '#26deca',
|
||||
name: $t('page.home.registerCount'),
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
stack: 'Total',
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0.25,
|
||||
color: '#26deca'
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: '#fff'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: []
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
async function mockData() {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 1000);
|
||||
});
|
||||
|
||||
updateOptions(opts => {
|
||||
opts.xAxis.data = ['06:00', '08:00', '10:00', '12:00', '14:00', '16:00', '18:00', '20:00', '22:00', '24:00'];
|
||||
opts.series[0].data = [4623, 6145, 6268, 6411, 1890, 4251, 2978, 3880, 3606, 4311];
|
||||
opts.series[1].data = [2208, 2016, 2916, 4512, 8281, 2008, 1963, 2367, 2956, 678];
|
||||
|
||||
return opts;
|
||||
});
|
||||
}
|
||||
|
||||
function updateLocale() {
|
||||
updateOptions((opts, factory) => {
|
||||
const originOpts = factory();
|
||||
|
||||
opts.legend.data = originOpts.legend.data;
|
||||
opts.series[0].name = originOpts.series[0].name;
|
||||
opts.series[1].name = originOpts.series[1].name;
|
||||
|
||||
return opts;
|
||||
});
|
||||
}
|
||||
|
||||
async function init() {
|
||||
mockData();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => appStore.locale,
|
||||
() => {
|
||||
updateLocale();
|
||||
}
|
||||
);
|
||||
|
||||
// init
|
||||
init();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ACard :bordered="false" class="card-wrapper">
|
||||
<div ref="domRef" class="h-360px overflow-hidden"></div>
|
||||
</ACard>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
109
src/views/home/modules/pie-chart.vue
Normal file
109
src/views/home/modules/pie-chart.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
import { watch } from 'vue';
|
||||
import { $t } from '@/locales';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useEcharts } from '@/hooks/common/echarts';
|
||||
|
||||
defineOptions({
|
||||
name: 'PieChart'
|
||||
});
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const { domRef, updateOptions } = useEcharts(() => ({
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
bottom: '1%',
|
||||
left: 'center',
|
||||
itemStyle: {
|
||||
borderWidth: 0
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
color: ['#5da8ff', '#8e9dff', '#fedc69', '#26deca'],
|
||||
name: $t('page.home.schedule'),
|
||||
type: 'pie',
|
||||
radius: ['45%', '75%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 1
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '12'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: [] as { name: string; value: number }[]
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
async function mockData() {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 1000);
|
||||
});
|
||||
|
||||
updateOptions(opts => {
|
||||
opts.series[0].data = [
|
||||
{ name: $t('page.home.study'), value: 20 },
|
||||
{ name: $t('page.home.entertainment'), value: 10 },
|
||||
{ name: $t('page.home.work'), value: 40 },
|
||||
{ name: $t('page.home.rest'), value: 30 }
|
||||
];
|
||||
|
||||
return opts;
|
||||
});
|
||||
}
|
||||
|
||||
function updateLocale() {
|
||||
updateOptions((opts, factory) => {
|
||||
const originOpts = factory();
|
||||
|
||||
opts.series[0].name = originOpts.series[0].name;
|
||||
|
||||
opts.series[0].data = [
|
||||
{ name: $t('page.home.study'), value: 20 },
|
||||
{ name: $t('page.home.entertainment'), value: 10 },
|
||||
{ name: $t('page.home.work'), value: 40 },
|
||||
{ name: $t('page.home.rest'), value: 30 }
|
||||
];
|
||||
|
||||
return opts;
|
||||
});
|
||||
}
|
||||
|
||||
async function init() {
|
||||
mockData();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => appStore.locale,
|
||||
() => {
|
||||
updateLocale();
|
||||
}
|
||||
);
|
||||
|
||||
// init
|
||||
init();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ACard :bordered="false" class="card-wrapper">
|
||||
<div ref="domRef" class="h-360px overflow-hidden"></div>
|
||||
</ACard>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
43
src/views/home/modules/project-news.vue
Normal file
43
src/views/home/modules/project-news.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'ProjectNews'
|
||||
});
|
||||
|
||||
interface NewsItem {
|
||||
id: number;
|
||||
content: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
const newses = computed<NewsItem[]>(() => [
|
||||
{ id: 1, content: $t('page.home.projectNews.desc1'), time: '2021-05-28 22:22:22' },
|
||||
{ id: 2, content: $t('page.home.projectNews.desc2'), time: '2021-10-27 10:24:54' },
|
||||
{ id: 3, content: $t('page.home.projectNews.desc3'), time: '2021-10-31 22:43:12' },
|
||||
{ id: 4, content: $t('page.home.projectNews.desc4'), time: '2021-11-03 20:33:31' },
|
||||
{ id: 5, content: $t('page.home.projectNews.desc5'), time: '2021-11-07 22:45:32' }
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ACard :title="$t('page.home.projectNews.title')" :bordered="false" size="small" class="card-wrapper">
|
||||
<template #extra>
|
||||
<a class="text-primary" href="javascript:;">{{ $t('page.home.projectNews.moreNews') }}</a>
|
||||
</template>
|
||||
<AList :data-source="newses">
|
||||
<template #renderItem="{ item }">
|
||||
<AListItem>
|
||||
<AListItemMeta :title="item.content" :description="item.time">
|
||||
<template #avatar>
|
||||
<SoybeanAvatar class="size-48px!" />
|
||||
</template>
|
||||
</AListItemMeta>
|
||||
</AListItem>
|
||||
</template>
|
||||
</AList>
|
||||
</ACard>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
162
src/views/manage/dept/index.vue
Normal file
162
src/views/manage/dept/index.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<script setup lang="tsx">
|
||||
import { Button, Popconfirm, Tag } from 'ant-design-vue';
|
||||
import { $t } from '@/locales';
|
||||
import { enableStatusRecord } from '@/constants/business';
|
||||
import DeptOperateModal from './modules/dept-operate-modal.vue';
|
||||
import DeptSearch from './modules/dept-search.vue';
|
||||
|
||||
const wrapperEl = shallowRef<HTMLElement | null>(null);
|
||||
const { height: wrapperElHeight } = useElementSize(wrapperEl);
|
||||
|
||||
const scrollConfig = computed(() => {
|
||||
return {
|
||||
y: wrapperElHeight.value - 72,
|
||||
x: 702
|
||||
};
|
||||
});
|
||||
|
||||
const { columns, columnChecks, data, loading, getData, searchParams, resetSearchParams } = useTable({
|
||||
apiFn: doGetDeptList,
|
||||
apiParams: {
|
||||
status: '0'
|
||||
},
|
||||
rowKey: 'deptId',
|
||||
columns: () => [
|
||||
{
|
||||
key: 'deptName',
|
||||
dataIndex: 'deptName',
|
||||
title: '部门名称',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'leader',
|
||||
dataIndex: 'leader',
|
||||
title: '负责人',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
dataIndex: 'status',
|
||||
title: '状态',
|
||||
align: 'center',
|
||||
customRender: ({ record }) => {
|
||||
if (record.status === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tagMap: Record<Api.Common.EnableStatus, string> = {
|
||||
'0': 'success',
|
||||
'1': 'warning'
|
||||
};
|
||||
|
||||
const label = $t(enableStatusRecord[record.status]);
|
||||
|
||||
return <Tag color={tagMap[record.status]}>{label}</Tag>;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'remark',
|
||||
dataIndex: 'remark',
|
||||
align: 'center',
|
||||
title: '备注'
|
||||
},
|
||||
{
|
||||
key: 'createTime',
|
||||
dataIndex: 'createTime',
|
||||
align: 'center',
|
||||
title: '创建时间'
|
||||
},
|
||||
{
|
||||
key: 'operate',
|
||||
title: $t('common.operate'),
|
||||
align: 'center',
|
||||
width: 200,
|
||||
customRender: ({ record }) => (
|
||||
<div class="flex justify-around gap-8px">
|
||||
{isShowBtn('system:dept:edit') && (
|
||||
<Button size="small" onClick={() => edit(record.deptId)}>
|
||||
{$t('common.edit')}
|
||||
</Button>
|
||||
)}
|
||||
{isShowBtn('system:dept:remove') && (
|
||||
<Popconfirm onConfirm={() => handleDelete(record.deptId)} title={$t('common.confirmDelete')}>
|
||||
<Button danger size="small">
|
||||
{$t('common.delete')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const { drawerVisible, operateType, editingData, handleAdd, handleEdit, checkedRowKeys, onBatchDeleted, onDeleted } =
|
||||
useTableOperate(data, { getData, idKey: 'deptId' });
|
||||
|
||||
const treeData = computed(() => {
|
||||
const rootDept = data.value.find(item => item.deptId === 100);
|
||||
return rootDept ? transformListToTree(data.value, 'deptId') : removeEmptyChildren(data.value);
|
||||
});
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const { error } = await doDeleteDept(checkedRowKeys.value.join(','));
|
||||
if (!error) {
|
||||
onBatchDeleted();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
const { error } = await doDeleteDept(id);
|
||||
if (!error) {
|
||||
onDeleted();
|
||||
}
|
||||
}
|
||||
|
||||
function edit(id: number) {
|
||||
handleEdit(id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
|
||||
<DeptSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getData" />
|
||||
<ACard
|
||||
title="部门列表"
|
||||
:bordered="false"
|
||||
:body-style="{ flex: 1, overflow: 'hidden' }"
|
||||
class="flex-col-stretch sm:flex-1-hidden card-wrapper"
|
||||
>
|
||||
<template #extra>
|
||||
<TableHeaderOperation
|
||||
v-model:columns="columnChecks"
|
||||
table-type="dept"
|
||||
:disabled-delete="checkedRowKeys.length === 0"
|
||||
:loading="loading"
|
||||
@add="handleAdd"
|
||||
@delete="handleBatchDelete"
|
||||
@refresh="getData"
|
||||
/>
|
||||
</template>
|
||||
<ATable
|
||||
ref="wrapperEl"
|
||||
:columns="columns"
|
||||
:data-source="treeData"
|
||||
:loading="loading"
|
||||
row-key="deptId"
|
||||
size="small"
|
||||
:scroll="scrollConfig"
|
||||
class="h-full"
|
||||
/>
|
||||
<DeptOperateModal
|
||||
v-model:visible="drawerVisible"
|
||||
:operate-type="operateType"
|
||||
:tree-data="treeData"
|
||||
:editing-data="editingData"
|
||||
@submitted="getData"
|
||||
/>
|
||||
</ACard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
135
src/views/manage/dept/modules/dept-operate-modal.vue
Normal file
135
src/views/manage/dept/modules/dept-operate-modal.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<script lang="ts" setup>
|
||||
import { $t } from '@/locales';
|
||||
import type { DeptFormType } from '@/service/api/dept';
|
||||
import { formRules, resetForm, statusOptions } from './form';
|
||||
|
||||
const props = defineProps<{
|
||||
operateType: AntDesign.TableOperateType;
|
||||
editingData?: Api.SystemManage.Dept | null;
|
||||
treeData?: Api.SystemManage.Dept[];
|
||||
}>();
|
||||
const emits = defineEmits<{
|
||||
'submit-success': [];
|
||||
}>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
const { formRef, validate, resetFields } = useAntdForm();
|
||||
|
||||
const model = ref<DeptFormType>(resetForm());
|
||||
|
||||
const title = computed(() => {
|
||||
const titles: Record<AntDesign.TableOperateType, string> = {
|
||||
add: '新增部门',
|
||||
edit: '编辑部门'
|
||||
};
|
||||
return titles[props.operateType];
|
||||
});
|
||||
|
||||
watch(visible, val => {
|
||||
if (val) {
|
||||
if (props.operateType === 'edit' && props.editingData) {
|
||||
model.value = { ...props.editingData };
|
||||
}
|
||||
} else {
|
||||
resetFields();
|
||||
model.value = resetForm();
|
||||
}
|
||||
});
|
||||
|
||||
const submitForm = async () => {
|
||||
await validate();
|
||||
if (!formRef.value?.validate) return;
|
||||
if (props.operateType === 'edit' && !props.editingData?.deptId) return;
|
||||
|
||||
const { error } = await (props.operateType === 'add'
|
||||
? doAddDept(model.value)
|
||||
: doEditDept({ ...model.value, deptId: props.editingData!.deptId }));
|
||||
|
||||
if (!error) {
|
||||
$message?.success($t(props.operateType === 'add' ? 'common.addSuccess' : 'common.updateSuccess'));
|
||||
emits('submit-success');
|
||||
}
|
||||
closeModal();
|
||||
};
|
||||
|
||||
function closeModal() {
|
||||
visible.value = false;
|
||||
resetFields();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AModal v-model:open="visible" :title="title" :width="700">
|
||||
<AForm
|
||||
ref="formRef"
|
||||
mt-7
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 17, offset: 1 }"
|
||||
:model="model"
|
||||
:rules="formRules"
|
||||
>
|
||||
<ARow :gutter="[16, 16]">
|
||||
<ACol :xs="24" :sm="12">
|
||||
<AFormItem label="上级部门" name="parentId">
|
||||
<ATreeSelect
|
||||
v-model:value="model.parentId"
|
||||
:default-value="100"
|
||||
:field-names="{ value: 'deptId', label: 'deptName' }"
|
||||
:tree-data="treeData"
|
||||
show-search
|
||||
allow-clear
|
||||
tree-node-filter-prop="label"
|
||||
/>
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
<ACol :xs="24" :sm="12">
|
||||
<AFormItem label="部门名称" name="deptName">
|
||||
<AInput v-model:value="model.deptName" />
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
<ACol :xs="24" :sm="12">
|
||||
<AFormItem label="负责人" name="leader">
|
||||
<AInput v-model:value="model.leader" />
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
<ACol :xs="24" :sm="12">
|
||||
<AFormItem label="联系电话" name="phone">
|
||||
<AInput v-model:value="model.phone" />
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
<ACol :xs="24" :sm="12">
|
||||
<AFormItem label="邮箱" name="email">
|
||||
<AInput v-model:value="model.email" />
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
<ACol :xs="24" :sm="12">
|
||||
<AFormItem label="状态" name="status">
|
||||
<ASelect v-model:value="model.status" :options="statusOptions" />
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
<ACol :xs="24" :sm="12">
|
||||
<AFormItem label="排序" name="orderNum">
|
||||
<AInputNumber v-model:value="model.orderNum" w-full />
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
<ACol :span="24">
|
||||
<AFormItem :label-col="{ span: 3 }" :wrapper-col="{ span: 20, offset: 0 }" label="备注" name="remark">
|
||||
<ATextarea v-model:value="model.remark" />
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
</ARow>
|
||||
</AForm>
|
||||
<template #footer>
|
||||
<AButton @click="submitForm">确定</AButton>
|
||||
<AButton @click="closeModal">取消</AButton>
|
||||
</template>
|
||||
</AModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.ant-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
70
src/views/manage/dept/modules/dept-search.vue
Normal file
70
src/views/manage/dept/modules/dept-search.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { $t } from '@/locales';
|
||||
import { enableStatusOptions } from '@/constants/business';
|
||||
|
||||
defineOptions({
|
||||
name: 'DeptSearch'
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
(e: 'reset'): void;
|
||||
(e: 'search'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const model = defineModel<Api.SystemManage.DeptSearchParams>('model', {
|
||||
required: true
|
||||
});
|
||||
|
||||
function reset() {
|
||||
emit('reset');
|
||||
}
|
||||
|
||||
function search() {
|
||||
emit('search');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ACard :title="$t('common.search')" :bordered="false" class="card-wrapper">
|
||||
<AForm :model="model" :label-width="80">
|
||||
<ARow :gutter="[16, 16]" wrap>
|
||||
<ACol :span="24" :md="12" :lg="8">
|
||||
<AFormItem :label="$t('page.manage.dept.deptName')" name="deptName" class="m-0">
|
||||
<AInput v-model:value="model.deptName" :placeholder="$t('page.manage.dept.form.deptName')" />
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
<ACol :span="24" :md="12" :lg="8">
|
||||
<AFormItem :label="$t('page.manage.dept.status')" name="status" class="m-0">
|
||||
<ASelect v-model:value="model.status" :placeholder="$t('page.manage.dept.form.status')" allow-clear>
|
||||
<ASelectOption v-for="option in enableStatusOptions" :key="option.value" :value="option.value">
|
||||
{{ $t(option.label) }}
|
||||
</ASelectOption>
|
||||
</ASelect>
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
<ACol :span="24" :md="12" :lg="8">
|
||||
<AFormItem class="m-0">
|
||||
<div class="w-full flex-y-center justify-end gap-12px">
|
||||
<AButton @click="reset">
|
||||
<div class="flex-y-center gap-8px">
|
||||
<icon-ic-round-refresh class="text-icon" />
|
||||
<span>{{ $t('common.reset') }}</span>
|
||||
</div>
|
||||
</AButton>
|
||||
<AButton type="primary" ghost @click="search">
|
||||
<div class="flex-y-center gap-8px">
|
||||
<icon-ic-round-search class="text-icon" />
|
||||
<span>{{ $t('common.search') }}</span>
|
||||
</div>
|
||||
</AButton>
|
||||
</div>
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
</ARow>
|
||||
</AForm>
|
||||
</ACard>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
37
src/views/manage/dept/modules/form.ts
Normal file
37
src/views/manage/dept/modules/form.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { DeptFormType } from '@/service/api/dept';
|
||||
|
||||
const { defaultRequiredRule } = useFormRules();
|
||||
|
||||
export function resetForm(): DeptFormType {
|
||||
return {
|
||||
deptName: '',
|
||||
parentId: 100,
|
||||
orderNum: 0,
|
||||
leader: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
status: '0',
|
||||
remark: ''
|
||||
};
|
||||
}
|
||||
|
||||
export const statusOptions = [
|
||||
{ label: '启用', value: '0' },
|
||||
{ label: '禁用', value: '1' }
|
||||
];
|
||||
|
||||
export const formRules = {
|
||||
deptName: defaultRequiredRule,
|
||||
parentId: defaultRequiredRule,
|
||||
orderNum: [
|
||||
defaultRequiredRule,
|
||||
{
|
||||
validator: (rule, value) => {
|
||||
if (value < 0) {
|
||||
return Promise.reject(rule.message || '排序必须大于等于0');
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
]
|
||||
} as Record<string, App.Global.FormRule | App.Global.FormRule[]>;
|
||||
166
src/views/manage/dict/index.vue
Normal file
166
src/views/manage/dict/index.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<script setup lang="tsx">
|
||||
import type { Key } from 'ant-design-vue/es/_util/type';
|
||||
import { Button, Popconfirm, Tag } from 'ant-design-vue';
|
||||
import { useTable, useTableOperate } from '@/hooks/common/table';
|
||||
import { $t } from '@/locales';
|
||||
import { enableStatusRecord } from '@/constants/business';
|
||||
import DictOperateDrawer from './modules/dict-operate-drawer.vue';
|
||||
import DictSearch from './modules/dict-search.vue';
|
||||
|
||||
const wrapperEl = shallowRef<HTMLElement | null>(null);
|
||||
const { height: wrapperElHeight } = useElementSize(wrapperEl);
|
||||
|
||||
const scrollConfig = computed(() => {
|
||||
return {
|
||||
y: wrapperElHeight.value - 72,
|
||||
x: 1000
|
||||
};
|
||||
});
|
||||
|
||||
const { columns, columnChecks, data, loading, getData, mobilePagination, searchParams, resetSearchParams } = useTable({
|
||||
apiFn: doGetDictList,
|
||||
apiParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
status: undefined,
|
||||
dictName: undefined,
|
||||
dictType: undefined
|
||||
},
|
||||
rowKey: 'dictId',
|
||||
columns: () => [
|
||||
{
|
||||
key: 'dictName',
|
||||
dataIndex: 'dictName',
|
||||
title: '字典名称',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'dictType',
|
||||
dataIndex: 'dictType',
|
||||
title: '字典类型',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
dataIndex: 'status',
|
||||
title: '状态',
|
||||
align: 'center',
|
||||
customRender: ({ record }) => {
|
||||
if (record.status === null) {
|
||||
return null;
|
||||
}
|
||||
const tagMap: Record<Api.Common.EnableStatus, string> = {
|
||||
'0': 'success',
|
||||
'1': 'warning'
|
||||
};
|
||||
const label = $t(enableStatusRecord[record.status]);
|
||||
return <Tag color={tagMap[record.status]}>{label}</Tag>;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'remark',
|
||||
dataIndex: 'remark',
|
||||
align: 'center',
|
||||
title: '备注'
|
||||
},
|
||||
{
|
||||
key: 'createTime',
|
||||
dataIndex: 'createTime',
|
||||
align: 'center',
|
||||
title: '创建时间'
|
||||
},
|
||||
{
|
||||
key: 'operate',
|
||||
title: '操作',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
customRender: ({ record }) => (
|
||||
<div class="flex justify-around gap-8px">
|
||||
{isShowBtn('system:dict:edit') && (
|
||||
<Button size="small" onClick={() => edit(record.dictId)}>
|
||||
编辑
|
||||
</Button>
|
||||
)}
|
||||
{isShowBtn('system:dict:remove') && (
|
||||
<Popconfirm onConfirm={() => handleDelete(record.dictId)} title="确认删除吗?">
|
||||
<Button danger size="small">
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const { drawerVisible, operateType, editingData, handleAdd, handleEdit, checkedRowKeys, onBatchDeleted, onDeleted } =
|
||||
useTableOperate(data, { getData, idKey: 'dictId' });
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const { error } = await doDeleteDict(checkedRowKeys.value.join(','));
|
||||
if (!error) {
|
||||
onBatchDeleted();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
const { error } = await doDeleteDict(id);
|
||||
if (!error) {
|
||||
onDeleted();
|
||||
}
|
||||
}
|
||||
|
||||
function edit(id: number) {
|
||||
handleEdit(id);
|
||||
}
|
||||
|
||||
function handleDictSelectChange(selectedRowKeys: Key[]) {
|
||||
checkedRowKeys.value = selectedRowKeys as number[];
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
|
||||
<DictSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getData" />
|
||||
<ACard
|
||||
title="字典列表"
|
||||
:bordered="false"
|
||||
:body-style="{ flex: 1, overflow: 'hidden' }"
|
||||
class="flex-col-stretch sm:flex-1-hidden card-wrapper"
|
||||
>
|
||||
<template #extra>
|
||||
<TableHeaderOperation
|
||||
v-model:columns="columnChecks"
|
||||
:disabled-delete="checkedRowKeys.length === 0"
|
||||
:loading="loading"
|
||||
:show-delete="true"
|
||||
table-type="dict"
|
||||
@add="handleAdd"
|
||||
@delete="handleBatchDelete"
|
||||
@refresh="getData"
|
||||
/>
|
||||
</template>
|
||||
<ATable
|
||||
ref="wrapperEl"
|
||||
:columns="columns"
|
||||
:data-source="data"
|
||||
:loading="loading"
|
||||
:row-selection="{ selectedRowKeys: checkedRowKeys, onChange: handleDictSelectChange }"
|
||||
row-key="dictId"
|
||||
size="small"
|
||||
:pagination="mobilePagination"
|
||||
:scroll="scrollConfig"
|
||||
class="h-full"
|
||||
/>
|
||||
<DictOperateDrawer
|
||||
v-model:visible="drawerVisible"
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
@submitted="getData"
|
||||
/>
|
||||
</ACard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
140
src/views/manage/dict/modules/dict-operate-drawer.vue
Normal file
140
src/views/manage/dict/modules/dict-operate-drawer.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<script setup lang="ts">
|
||||
import { SimpleScrollbar } from '@sa/materials';
|
||||
import { useAntdForm, useFormRules } from '@/hooks/common/form';
|
||||
import { $t } from '@/locales';
|
||||
import { enableStatusOptions } from '@/constants/business';
|
||||
import type { DictSubmitModel } from '@/service/api/dict';
|
||||
|
||||
defineOptions({
|
||||
name: 'DictOperateDrawer'
|
||||
});
|
||||
|
||||
interface Props {
|
||||
/** the type of operation */
|
||||
operateType: AntDesign.TableOperateType;
|
||||
/** the edit row data */
|
||||
rowData?: Api.SystemManage.Dict | null;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
interface Emits {
|
||||
(e: 'submitted'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const { formRef, validate, resetFields } = useAntdForm();
|
||||
const { defaultRequiredRule } = useFormRules();
|
||||
|
||||
const title = computed(() => {
|
||||
const titles: Record<AntDesign.TableOperateType, string> = {
|
||||
add: $t('page.manage.dict.addDict'),
|
||||
edit: $t('page.manage.dict.editDict')
|
||||
};
|
||||
return titles[props.operateType];
|
||||
});
|
||||
|
||||
const model = ref<DictSubmitModel>(createDefaultModel());
|
||||
|
||||
function createDefaultModel(): DictSubmitModel {
|
||||
return {
|
||||
dictName: '',
|
||||
dictType: '',
|
||||
status: '0',
|
||||
remark: ''
|
||||
};
|
||||
}
|
||||
|
||||
type RuleKey = Exclude<keyof DictSubmitModel, 'remark'>;
|
||||
|
||||
const rules: Record<RuleKey, App.Global.FormRule> = {
|
||||
dictId: defaultRequiredRule,
|
||||
dictName: defaultRequiredRule,
|
||||
dictType: defaultRequiredRule,
|
||||
status: defaultRequiredRule
|
||||
};
|
||||
|
||||
function handleUpdateModelWhenEdit() {
|
||||
if (props.operateType === 'add') {
|
||||
model.value = createDefaultModel();
|
||||
}
|
||||
|
||||
if (props.operateType === 'edit' && props.rowData) {
|
||||
model.value = { ...props.rowData };
|
||||
}
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
|
||||
const { error } = await (props.operateType === 'edit' ? doEditDict : doAddDict)(model.value);
|
||||
|
||||
if (!error) {
|
||||
$message?.success($t(props.operateType === 'add' ? 'common.addSuccess' : 'common.updateSuccess'));
|
||||
closeDrawer();
|
||||
emit('submitted');
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
val => {
|
||||
if (val) {
|
||||
handleUpdateModelWhenEdit();
|
||||
resetFields();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ADrawer
|
||||
v-model:open="visible"
|
||||
:body-style="{ paddingRight: '0px', paddingTop: '0', paddingBottom: '0' }"
|
||||
:title="title"
|
||||
:width="460"
|
||||
>
|
||||
<SimpleScrollbar>
|
||||
<AForm ref="formRef" py-20px pr-20px layout="vertical" :model="model" :rules="rules">
|
||||
<AFormItem :label="$t('page.manage.dict.dictName')" name="dictName">
|
||||
<AInput v-model:value="model.dictName" :placeholder="$t('page.manage.dict.form.dictName')" />
|
||||
</AFormItem>
|
||||
<AFormItem :label="$t('page.manage.dict.dictType')" name="dictType">
|
||||
<AInput v-model:value="model.dictType" :placeholder="$t('page.manage.dict.form.dictType')" />
|
||||
</AFormItem>
|
||||
<AFormItem :label="$t('page.manage.dict.status')" name="status">
|
||||
<ARadioGroup v-model:value="model.status">
|
||||
<ARadio v-for="item in enableStatusOptions" :key="item.value" :value="item.value">
|
||||
{{ $t(item.label) }}
|
||||
</ARadio>
|
||||
</ARadioGroup>
|
||||
</AFormItem>
|
||||
<AFormItem :label="$t('page.manage.dict.remark')" name="remark">
|
||||
<ATextarea v-model:value="model.remark" :placeholder="$t('page.manage.dict.form.remark')" />
|
||||
</AFormItem>
|
||||
</AForm>
|
||||
</SimpleScrollbar>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex-y-center justify-end gap-12px">
|
||||
<AButton @click="closeDrawer">{{ $t('common.cancel') }}</AButton>
|
||||
<AButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</AButton>
|
||||
</div>
|
||||
</template>
|
||||
</ADrawer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.ant-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
70
src/views/manage/dict/modules/dict-search.vue
Normal file
70
src/views/manage/dict/modules/dict-search.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { $t } from '@/locales';
|
||||
import { enableStatusOptions } from '@/constants/business';
|
||||
|
||||
defineOptions({
|
||||
name: 'DictSearch'
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
(e: 'reset'): void;
|
||||
(e: 'search'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const model = defineModel<Api.SystemManage.DictSearchParams>('model', {
|
||||
required: true
|
||||
});
|
||||
|
||||
function reset() {
|
||||
emit('reset');
|
||||
}
|
||||
|
||||
function search() {
|
||||
emit('search');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ACard :title="$t('common.search')" :bordered="false" class="card-wrapper">
|
||||
<AForm :model="model" :label-width="80">
|
||||
<ARow :gutter="[16, 16]" wrap>
|
||||
<ACol :span="24" :md="12" :lg="8">
|
||||
<AFormItem :label="$t('page.manage.dict.dictName')" name="dictName" class="m-0">
|
||||
<AInput v-model:value="model.dictName" :placeholder="$t('page.manage.dict.form.dictName')" />
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
<ACol :span="24" :md="12" :lg="8">
|
||||
<AFormItem :label="$t('page.manage.dict.status')" name="status" class="m-0">
|
||||
<ASelect v-model:value="model.status" :placeholder="$t('page.manage.dict.form.status')" allow-clear>
|
||||
<ASelectOption v-for="option in enableStatusOptions" :key="option.value" :value="option.value">
|
||||
{{ $t(option.label) }}
|
||||
</ASelectOption>
|
||||
</ASelect>
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
<ACol :span="24" :md="12" :lg="8">
|
||||
<AFormItem class="m-0">
|
||||
<div class="w-full flex-y-center justify-end gap-12px">
|
||||
<AButton @click="reset">
|
||||
<div class="flex-y-center gap-8px">
|
||||
<icon-ic-round-refresh class="text-icon" />
|
||||
<span>{{ $t('common.reset') }}</span>
|
||||
</div>
|
||||
</AButton>
|
||||
<AButton type="primary" ghost @click="search">
|
||||
<div class="flex-y-center gap-8px">
|
||||
<icon-ic-round-search class="text-icon" />
|
||||
<span>{{ $t('common.search') }}</span>
|
||||
</div>
|
||||
</AButton>
|
||||
</div>
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
</ARow>
|
||||
</AForm>
|
||||
</ACard>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
171
src/views/manage/menu/index.vue
Normal file
171
src/views/manage/menu/index.vue
Normal file
@@ -0,0 +1,171 @@
|
||||
<script setup lang="tsx">
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { Button, Tag } from 'ant-design-vue';
|
||||
import { SimpleScrollbar } from '~/packages/materials/src';
|
||||
import MenuOperateModal from './modules/menu-operate-modal.vue';
|
||||
|
||||
const { data, columns, loading, getData } = useTable({
|
||||
apiFn: doGetMenuList,
|
||||
columns: () => [
|
||||
{
|
||||
key: 'menuName',
|
||||
dataIndex: 'menuName',
|
||||
title: '菜单名称',
|
||||
align: 'left',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
key: 'path',
|
||||
dataIndex: 'path',
|
||||
title: '路由地址',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'component',
|
||||
dataIndex: 'component',
|
||||
title: '组件路径',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
dataIndex: 'status',
|
||||
title: '状态',
|
||||
align: 'center',
|
||||
customRender: ({ record }) => {
|
||||
const colorTagMap: Record<string, string> = {
|
||||
0: 'success',
|
||||
1: 'warning'
|
||||
};
|
||||
|
||||
const labelMap: Record<string, string> = {
|
||||
0: '正常',
|
||||
1: '停用'
|
||||
};
|
||||
return <Tag color={colorTagMap[record.status]}>{labelMap[record.status]}</Tag>;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'menuType',
|
||||
dataIndex: 'menuType',
|
||||
title: '类型',
|
||||
align: 'center',
|
||||
customRender: ({ record }) => {
|
||||
const colorTagMap: Record<string, string> = {
|
||||
M: 'processing',
|
||||
C: 'success',
|
||||
F: 'default'
|
||||
};
|
||||
|
||||
const labelMap: Record<string, string> = {
|
||||
M: '目录',
|
||||
C: '菜单',
|
||||
F: '按钮'
|
||||
};
|
||||
return <Tag color={colorTagMap[record.menuType]}>{labelMap[record.menuType]}</Tag>;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'orderNum',
|
||||
dataIndex: 'orderNum',
|
||||
title: '排序',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'icon',
|
||||
dataIndex: 'icon',
|
||||
title: '图标',
|
||||
align: 'center',
|
||||
customRender: ({ record }) => {
|
||||
return (
|
||||
<div class="flex-center text-5">
|
||||
<Icon icon={`${record.icon}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'createTime',
|
||||
dataIndex: 'createTime',
|
||||
align: 'center',
|
||||
title: '创建时间'
|
||||
},
|
||||
{
|
||||
key: 'operate',
|
||||
title: '操作',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
customRender: ({ record }) => (
|
||||
<div class="flex justify-around gap-8px">
|
||||
<Button size="small" onClick={() => edit(record.menuId)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Button size="small" danger onClick={() => handleDelete(record.menuId)}>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
],
|
||||
rowKey: 'menuId'
|
||||
});
|
||||
const { handleEdit, handleAdd, checkedRowKeys, operateType, drawerVisible, onDeleted, editingData } = useTableOperate(
|
||||
data,
|
||||
{
|
||||
getData,
|
||||
idKey: 'menuId'
|
||||
}
|
||||
);
|
||||
|
||||
function edit(id: number) {
|
||||
handleEdit(id);
|
||||
}
|
||||
|
||||
function handleDelete(id: number) {
|
||||
$modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定删除该菜单吗?',
|
||||
onOk: async () => {
|
||||
const { error } = await doDeleteMenu(id);
|
||||
if (!error) {
|
||||
$message.success('删除成功');
|
||||
onDeleted();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const treeData = computed(() => {
|
||||
return transformListToTree(data.value, 'menuId');
|
||||
});
|
||||
|
||||
function handleSubmitSuccess() {
|
||||
getData();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SimpleScrollbar>
|
||||
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
|
||||
<ACard title="菜单列表">
|
||||
<AButton mb-4 type="primary" @click="handleAdd">新增菜单</AButton>
|
||||
|
||||
<ATable
|
||||
:checked-row-keys="checkedRowKeys"
|
||||
:data-source="treeData"
|
||||
row-key="menuId"
|
||||
:columns="columns"
|
||||
:pagination="false"
|
||||
:loading="loading"
|
||||
/>
|
||||
<MenuOperateModal
|
||||
v-model:visible="drawerVisible"
|
||||
:editing-data="editingData"
|
||||
:operate-type="operateType"
|
||||
@submit-success="handleSubmitSuccess"
|
||||
/>
|
||||
</ACard>
|
||||
</div>
|
||||
</SimpleScrollbar>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
70
src/views/manage/menu/modules/form.ts
Normal file
70
src/views/manage/menu/modules/form.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
const { defaultRequiredRule } = useFormRules();
|
||||
|
||||
export type MenuModelType = Pick<
|
||||
Api.SystemManage.Menu,
|
||||
| 'menuName'
|
||||
| 'menuType'
|
||||
| 'icon'
|
||||
| 'path'
|
||||
| 'component'
|
||||
| 'orderNum'
|
||||
| 'status'
|
||||
| 'parentId'
|
||||
| 'hideInMenu'
|
||||
| 'fixedIndexInTab'
|
||||
| 'iconType'
|
||||
| 'isFrame'
|
||||
| 'perms'
|
||||
| 'isCache'
|
||||
| 'name'
|
||||
>;
|
||||
|
||||
export function resetAddForm(): MenuModelType {
|
||||
return {
|
||||
menuName: '',
|
||||
menuType: 'M',
|
||||
icon: '',
|
||||
path: '',
|
||||
component: '',
|
||||
orderNum: 0,
|
||||
status: '0',
|
||||
parentId: 0,
|
||||
iconType: '1',
|
||||
hideInMenu: '0',
|
||||
isFrame: '1',
|
||||
perms: '',
|
||||
name: '',
|
||||
isCache: '0'
|
||||
};
|
||||
}
|
||||
|
||||
export const formRules = {
|
||||
menuName: defaultRequiredRule,
|
||||
menuType: defaultRequiredRule,
|
||||
icon: defaultRequiredRule,
|
||||
path: defaultRequiredRule,
|
||||
component: defaultRequiredRule,
|
||||
status: defaultRequiredRule,
|
||||
orderNum: [
|
||||
defaultRequiredRule,
|
||||
{
|
||||
validator: (rule, value) => {
|
||||
if (value < 0) {
|
||||
return Promise.reject(rule.message || '排序必须大于等于0');
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
]
|
||||
} as Record<string, App.Global.FormRule | App.Global.FormRule[]>;
|
||||
|
||||
export const menuTypeOptions = [
|
||||
{ label: '目录', value: 'M' },
|
||||
{ label: '菜单', value: 'C' },
|
||||
{ label: '按钮', value: 'F' }
|
||||
];
|
||||
|
||||
export const menuStatusOptions = [
|
||||
{ label: '正常', value: '0' },
|
||||
{ label: '停用', value: '1' }
|
||||
];
|
||||
67
src/views/manage/menu/modules/icon-select.vue
Normal file
67
src/views/manage/menu/modules/icon-select.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<script lang="ts" setup>
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { SimpleScrollbar } from '~/packages/materials/src';
|
||||
import { icons } from './icon';
|
||||
const visible = ref(false);
|
||||
defineOptions({
|
||||
name: 'IconSelect'
|
||||
});
|
||||
|
||||
const value = defineModel<string>({ default: '' });
|
||||
const iconSelectRef = ref<HTMLElement | null>(null);
|
||||
const { isOutside } = useMouseInElement(iconSelectRef);
|
||||
|
||||
const blur = () => {
|
||||
if (isOutside.value) {
|
||||
visible.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
function selectIcon(icon: string) {
|
||||
value.value = icon;
|
||||
setTimeout(() => {
|
||||
visible.value = false;
|
||||
}, 100);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div relative>
|
||||
<div flex items-center gap-2 @click="visible = true">
|
||||
<AInput v-model:value="value" @blur="blur">
|
||||
<template #suffix>
|
||||
<Icon v-if="value" text-4 :icon="`${value}`" @click.prevent="() => {}"></Icon>
|
||||
<Icon v-if="!value" text-4 icon="carbon:apps" @click.prevent="() => {}"></Icon>
|
||||
</template>
|
||||
</AInput>
|
||||
</div>
|
||||
|
||||
<Transition>
|
||||
<template v-if="visible">
|
||||
<div
|
||||
ref="iconSelectRef"
|
||||
class="absolute left-0 top-36px z-3000 h-80 w-92 rounded-md bg-light-2 shadow-md dark:bg-dark-4"
|
||||
>
|
||||
<SimpleScrollbar>
|
||||
<div flex="~ wrap" gap-1 p-4>
|
||||
<div
|
||||
v-for="icon in icons"
|
||||
:key="icon"
|
||||
:class="
|
||||
icon === value ? 'text-dark-800 bg-gray-200 dark:text-gray-4 dark:bg-dark6' : 'text-gray-500 bg-none'
|
||||
"
|
||||
hover="bg-light-500 dark:bg-dark-500 "
|
||||
class="flex-center cursor-pointer rounded-md p-2 transition-all"
|
||||
@click="selectIcon(icon)"
|
||||
>
|
||||
<Icon class="text-5.5 font-bold" :icon="`${icon}`" />
|
||||
</div>
|
||||
</div>
|
||||
</SimpleScrollbar>
|
||||
</div>
|
||||
</template>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
39
src/views/manage/menu/modules/icon.ts
Normal file
39
src/views/manage/menu/modules/icon.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
export const icons = [
|
||||
'carbon:3d-cursor',
|
||||
'carbon:account',
|
||||
'carbon:ai-results-low',
|
||||
'carbon:assembly-cluster',
|
||||
'carbon:battery-charging',
|
||||
'carbon:battery-full',
|
||||
'carbon:bicycle',
|
||||
'carbon:building-insights-3',
|
||||
'carbon:calendar',
|
||||
'carbon:carbon',
|
||||
'carbon:carbon-for-ibm-product',
|
||||
'carbon:category',
|
||||
'carbon:center-to-fit',
|
||||
'carbon:chart-area-smooth',
|
||||
'carbon:chart-median',
|
||||
'carbon:chat-bot',
|
||||
'carbon:cics-system-group',
|
||||
'carbon:database-messaging',
|
||||
'carbon:document',
|
||||
'carbon:document-multiple-01',
|
||||
'carbon:earth-americas-filled',
|
||||
'carbon:folder',
|
||||
'carbon:forum',
|
||||
'carbon:group',
|
||||
'carbon:ibm-cloud-bare-metal-server',
|
||||
'carbon:ibm-telehealth',
|
||||
'carbon:image-search-alt',
|
||||
'carbon:laptop',
|
||||
'carbon:machine-learning',
|
||||
'carbon:report',
|
||||
'carbon:rocket',
|
||||
'carbon:settings',
|
||||
'carbon:settings-services',
|
||||
'carbon:shopping-cart',
|
||||
'carbon:user-avatar',
|
||||
'carbon:user-multiple',
|
||||
'carbon:volume-block-storage'
|
||||
];
|
||||
185
src/views/manage/menu/modules/menu-operate-modal.vue
Normal file
185
src/views/manage/menu/modules/menu-operate-modal.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<script lang="ts" setup>
|
||||
import type {} from 'ant-design-vue';
|
||||
import { generatedRoutes } from '@/router/elegant/routes';
|
||||
import { $t } from '@/locales';
|
||||
import type { MenuModelType } from './form';
|
||||
import { formRules, menuStatusOptions, menuTypeOptions, resetAddForm } from './form';
|
||||
import IconSelect from './icon-select.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
operateType: AntDesign.TableOperateType;
|
||||
editingData?: Api.SystemManage.Menu | null;
|
||||
}>();
|
||||
const emits = defineEmits<{
|
||||
'submit-success': [];
|
||||
}>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
const { formRef, validate, resetFields } = useAntdForm();
|
||||
|
||||
const model = ref<MenuModelType>(resetAddForm());
|
||||
const treeData = ref<Api.SystemManage.MenuTree[]>([]);
|
||||
|
||||
const title = computed(() => {
|
||||
const titles: Record<AntDesign.TableOperateType, string> = {
|
||||
add: '新增菜单',
|
||||
edit: '编辑菜单'
|
||||
};
|
||||
return titles[props.operateType];
|
||||
});
|
||||
|
||||
// TODO: 根据菜单类型动态加载组件路径、目录类型只允许选择带有子元素的,菜单类型只允许选择没有子元素的
|
||||
const componentOptions = computed(() => {
|
||||
const excludePaths = ['/404', '/403', '/500'];
|
||||
function transformRoutes(routes: any[]): any[] {
|
||||
return routes.filter(route => {
|
||||
if (route.children) {
|
||||
route.children = transformRoutes(route.children);
|
||||
return true;
|
||||
}
|
||||
if (!route.hideInMenu && !excludePaths.includes(route.path) && !route.path.startsWith('/login')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
return transformRoutes(generatedRoutes);
|
||||
});
|
||||
|
||||
watch(visible, val => {
|
||||
if (val) {
|
||||
if (props.operateType === 'edit' && props.editingData) {
|
||||
model.value = {
|
||||
...props.editingData,
|
||||
parentId: props.editingData.parentId || 0,
|
||||
hideInMenu: props.editingData.visible === '0' ? '1' : '0'
|
||||
};
|
||||
}
|
||||
} else {
|
||||
resetFields();
|
||||
model.value = resetAddForm();
|
||||
}
|
||||
});
|
||||
|
||||
const submitForm = async () => {
|
||||
await validate();
|
||||
const { error } = await (props.operateType === 'add' ? doAddMenu(model.value) : doEditMenu(model.value));
|
||||
if (!error) {
|
||||
$message?.success($t(props.operateType === 'add' ? 'common.addSuccess' : 'common.updateSuccess'));
|
||||
emits('submit-success');
|
||||
}
|
||||
closeModal();
|
||||
};
|
||||
|
||||
function closeModal() {
|
||||
visible.value = false;
|
||||
resetFields();
|
||||
model.value = resetAddForm();
|
||||
}
|
||||
|
||||
async function getTreeData() {
|
||||
const { data, error } = await fetchGetMenuTree();
|
||||
if (!error && data) {
|
||||
// 添加根节点
|
||||
treeData.value = [
|
||||
{
|
||||
id: 0,
|
||||
pId: -1,
|
||||
label: '根节点',
|
||||
children: data
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
function handleTreeSelect(node: any) {
|
||||
model.value.component = node.component;
|
||||
model.value.name = node.name;
|
||||
}
|
||||
|
||||
getTreeData();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AModal v-model:open="visible" :title="title" :width="700">
|
||||
<AForm
|
||||
ref="formRef"
|
||||
:model="model"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16, offset: 1 }"
|
||||
class="grid grid-cols-2 gap-3 px-4 py-4 pt-6"
|
||||
>
|
||||
<AFormItem label="上级菜单" name="parentId">
|
||||
<ATreeSelect
|
||||
v-model:value="model.parentId"
|
||||
show-search
|
||||
:field-names="{ value: 'id' }"
|
||||
allow-clear
|
||||
:tree-data="treeData"
|
||||
tree-node-filter-prop="label"
|
||||
/>
|
||||
</AFormItem>
|
||||
<AFormItem label="菜单名称" name="menuName">
|
||||
<AInput v-model:value="model.menuName" />
|
||||
</AFormItem>
|
||||
<AFormItem label="菜单类型" name="menuType">
|
||||
<ASelect v-model:value="model.menuType" :options="menuTypeOptions" />
|
||||
</AFormItem>
|
||||
<AFormItem v-if="model.menuType !== 'F'" label="菜单图标" name="icon">
|
||||
<IconSelect v-model="model.icon" />
|
||||
</AFormItem>
|
||||
<AFormItem v-if="model.menuType === 'C'" label="是否外链">
|
||||
<ARadioGroup v-model:value="model.isFrame" name="radioGroup" @change="() => (model.path = '')">
|
||||
<ARadio value="0">是</ARadio>
|
||||
<ARadio value="1">否</ARadio>
|
||||
</ARadioGroup>
|
||||
</AFormItem>
|
||||
<AFormItem v-if="model.menuType !== 'F'" label="菜单路径" name="path">
|
||||
<div>
|
||||
<!-- @vue-ignore -->
|
||||
<ATreeSelect
|
||||
v-if="model.isFrame === '1'"
|
||||
v-model:value="model.path"
|
||||
show-search
|
||||
:field-names="{ value: 'path', label: 'path' }"
|
||||
allow-clear
|
||||
:tree-data="componentOptions"
|
||||
tree-node-filter-prop="label"
|
||||
@select="(_val, node) => handleTreeSelect(node)"
|
||||
/>
|
||||
<AInput v-else v-model:value="model.path" />
|
||||
</div>
|
||||
</AFormItem>
|
||||
<AFormItem v-if="model.menuType === 'C'" label="隐藏菜单" name="hideInMenu">
|
||||
<ASwitch v-model:checked="model.hideInMenu" checked-value="0" un-checked-value="1" />
|
||||
</AFormItem>
|
||||
<AFormItem label="排序" name="orderNum">
|
||||
<AInputNumber v-model:value="model.orderNum" w-full />
|
||||
</AFormItem>
|
||||
<AFormItem v-if="model.menuType !== 'F'" label="状态" name="status">
|
||||
<ASelect v-model:value="model.status" :options="menuStatusOptions" />
|
||||
</AFormItem>
|
||||
<AFormItem v-if="model.menuType === 'C'" label="缓存" name="isCache">
|
||||
<ASwitch v-model:checked="model.isCache" checked-value="0" un-checked-value="1" />
|
||||
</AFormItem>
|
||||
<AFormItem v-if="model.menuType === 'F'" label="权限标识" name="perms">
|
||||
<AInput v-model:value="model.perms" />
|
||||
</AFormItem>
|
||||
</AForm>
|
||||
|
||||
<template #footer>
|
||||
<AButton @click="submitForm">确定</AButton>
|
||||
<AButton @click="closeModal">取消</AButton>
|
||||
</template>
|
||||
</AModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.ant-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
173
src/views/manage/post/index.vue
Normal file
173
src/views/manage/post/index.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<script setup lang="tsx">
|
||||
import type { Key } from 'ant-design-vue/es/_util/type';
|
||||
import { Button, Popconfirm, Tag } from 'ant-design-vue';
|
||||
import { useTable, useTableOperate } from '@/hooks/common/table';
|
||||
import { $t } from '@/locales';
|
||||
import { enableStatusRecord } from '@/constants/business';
|
||||
import PostOperateDrawer from './modules/post-operate-drawer.vue';
|
||||
import PostSearch from './modules/post-search.vue';
|
||||
|
||||
const wrapperEl = shallowRef<HTMLElement | null>(null);
|
||||
const { height: wrapperElHeight } = useElementSize(wrapperEl);
|
||||
|
||||
const scrollConfig = computed(() => {
|
||||
return {
|
||||
y: wrapperElHeight.value - 72,
|
||||
x: 1000
|
||||
};
|
||||
});
|
||||
|
||||
const { columns, columnChecks, data, loading, getData, mobilePagination, searchParams, resetSearchParams } = useTable({
|
||||
apiFn: doGetPostList,
|
||||
apiParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
status: undefined,
|
||||
postName: undefined,
|
||||
postCode: undefined
|
||||
},
|
||||
rowKey: 'postId',
|
||||
columns: () => [
|
||||
{
|
||||
key: 'postCode',
|
||||
dataIndex: 'postCode',
|
||||
title: $t('page.manage.post.postCode'),
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'postName',
|
||||
dataIndex: 'postName',
|
||||
title: $t('page.manage.post.postName'),
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'postSort',
|
||||
dataIndex: 'postSort',
|
||||
title: $t('page.manage.post.postSort'),
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
dataIndex: 'status',
|
||||
title: $t('page.manage.post.status'),
|
||||
align: 'center',
|
||||
customRender: ({ record }) => {
|
||||
if (record.status === null) {
|
||||
return null;
|
||||
}
|
||||
const tagMap: Record<Api.Common.EnableStatus, string> = {
|
||||
'0': 'success',
|
||||
'1': 'warning'
|
||||
};
|
||||
const label = $t(enableStatusRecord[record.status]);
|
||||
return <Tag color={tagMap[record.status]}>{label}</Tag>;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'remark',
|
||||
dataIndex: 'remark',
|
||||
align: 'center',
|
||||
title: $t('page.manage.post.remark')
|
||||
},
|
||||
{
|
||||
key: 'createTime',
|
||||
dataIndex: 'createTime',
|
||||
align: 'center',
|
||||
title: '创建时间'
|
||||
},
|
||||
{
|
||||
key: 'operate',
|
||||
title: $t('common.operate'),
|
||||
align: 'center',
|
||||
width: 200,
|
||||
customRender: ({ record }) =>
|
||||
!record.flag && (
|
||||
<div class="flex justify-around gap-8px">
|
||||
{isShowBtn('system:post:edit') && (
|
||||
<Button size="small" onClick={() => edit(record.postId)}>
|
||||
{$t('common.edit')}
|
||||
</Button>
|
||||
)}
|
||||
{isShowBtn('system:post:remove') && (
|
||||
<Popconfirm onConfirm={() => handleDelete(record.postId)} title={$t('common.confirmDelete')}>
|
||||
<Button danger size="small">
|
||||
{$t('common.delete')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const { drawerVisible, operateType, editingData, handleAdd, handleEdit, checkedRowKeys, onBatchDeleted, onDeleted } =
|
||||
useTableOperate(data, { getData, idKey: 'postId' });
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const { error } = await doDeletePost(checkedRowKeys.value.join(','));
|
||||
if (!error) {
|
||||
onBatchDeleted();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
const { error } = await doDeletePost(id);
|
||||
if (!error) {
|
||||
onDeleted();
|
||||
}
|
||||
}
|
||||
|
||||
function edit(id: number) {
|
||||
handleEdit(id);
|
||||
}
|
||||
|
||||
function handlePostSelectChange(selectedRowKeys: Key[]) {
|
||||
checkedRowKeys.value = selectedRowKeys as number[];
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
|
||||
<PostSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getData" />
|
||||
<ACard
|
||||
:title="$t('page.manage.post.title')"
|
||||
:bordered="false"
|
||||
:body-style="{ flex: 1, overflow: 'hidden' }"
|
||||
class="flex-col-stretch sm:flex-1-hidden card-wrapper"
|
||||
>
|
||||
<template #extra>
|
||||
<TableHeaderOperation
|
||||
v-model:columns="columnChecks"
|
||||
:disabled-delete="checkedRowKeys.length === 0"
|
||||
:loading="loading"
|
||||
:show-delete="true"
|
||||
table-type="post"
|
||||
@add="handleAdd"
|
||||
@delete="handleBatchDelete"
|
||||
@refresh="getData"
|
||||
/>
|
||||
</template>
|
||||
<ATable
|
||||
ref="wrapperEl"
|
||||
:columns="columns"
|
||||
:data-source="data"
|
||||
:loading="loading"
|
||||
:row-selection="{ selectedRowKeys: checkedRowKeys, onChange: handlePostSelectChange }"
|
||||
row-key="postId"
|
||||
size="small"
|
||||
:pagination="mobilePagination"
|
||||
:scroll="scrollConfig"
|
||||
class="h-full"
|
||||
/>
|
||||
<PostOperateDrawer
|
||||
v-model:visible="drawerVisible"
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
@submitted="getData"
|
||||
/>
|
||||
</ACard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
144
src/views/manage/post/modules/post-operate-drawer.vue
Normal file
144
src/views/manage/post/modules/post-operate-drawer.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<script setup lang="ts">
|
||||
import { SimpleScrollbar } from '@sa/materials';
|
||||
import { useAntdForm, useFormRules } from '@/hooks/common/form';
|
||||
import { $t } from '@/locales';
|
||||
import { enableStatusOptions } from '@/constants/business';
|
||||
import type { PostSubmitModel } from '@/service/api/post';
|
||||
|
||||
defineOptions({
|
||||
name: 'PostOperateDrawer'
|
||||
});
|
||||
|
||||
interface Props {
|
||||
/** the type of operation */
|
||||
operateType: AntDesign.TableOperateType;
|
||||
/** the edit row data */
|
||||
rowData?: Api.SystemManage.Post | null;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
interface Emits {
|
||||
(e: 'submitted'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const { formRef, validate, resetFields } = useAntdForm();
|
||||
const { defaultRequiredRule } = useFormRules();
|
||||
|
||||
const title = computed(() => {
|
||||
const titles: Record<AntDesign.TableOperateType, string> = {
|
||||
add: $t('page.manage.post.addPost'),
|
||||
edit: $t('page.manage.post.editPost')
|
||||
};
|
||||
return titles[props.operateType];
|
||||
});
|
||||
|
||||
const model = ref<PostSubmitModel>(createDefaultModel());
|
||||
|
||||
function createDefaultModel(): PostSubmitModel {
|
||||
return {
|
||||
postCode: '',
|
||||
postName: '',
|
||||
postSort: 0,
|
||||
status: '0',
|
||||
remark: ''
|
||||
};
|
||||
}
|
||||
|
||||
type RuleKey = Exclude<keyof PostSubmitModel, 'remark'>;
|
||||
|
||||
const rules: Record<RuleKey, App.Global.FormRule> = {
|
||||
postCode: defaultRequiredRule,
|
||||
postName: defaultRequiredRule,
|
||||
postSort: defaultRequiredRule,
|
||||
status: defaultRequiredRule
|
||||
};
|
||||
|
||||
function handleUpdateModelWhenEdit() {
|
||||
if (props.operateType === 'add') {
|
||||
model.value = createDefaultModel();
|
||||
}
|
||||
|
||||
if (props.operateType === 'edit' && props.rowData) {
|
||||
model.value = { ...props.rowData };
|
||||
}
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
|
||||
const { error } = await (props.operateType === 'edit' ? doEditPost : doAddPost)(model.value);
|
||||
|
||||
if (!error) {
|
||||
$message?.success($t(props.operateType === 'add' ? 'common.addSuccess' : 'common.updateSuccess'));
|
||||
closeDrawer();
|
||||
emit('submitted');
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
val => {
|
||||
if (val) {
|
||||
handleUpdateModelWhenEdit();
|
||||
resetFields();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ADrawer
|
||||
v-model:open="visible"
|
||||
:body-style="{ paddingRight: '0px', paddingTop: '0', paddingBottom: '0' }"
|
||||
:title="title"
|
||||
:width="460"
|
||||
>
|
||||
<SimpleScrollbar>
|
||||
<AForm ref="formRef" py-20px pr-20px layout="vertical" :model="model" :rules="rules">
|
||||
<AFormItem :label="$t('page.manage.post.postCode')" name="postCode">
|
||||
<AInput v-model:value="model.postCode" :placeholder="$t('page.manage.post.form.postCode')" />
|
||||
</AFormItem>
|
||||
<AFormItem :label="$t('page.manage.post.postName')" name="postName">
|
||||
<AInput v-model:value="model.postName" :placeholder="$t('page.manage.post.form.postName')" />
|
||||
</AFormItem>
|
||||
<AFormItem :label="$t('page.manage.post.postSort')" name="postSort">
|
||||
<AInputNumber v-model:value="model.postSort" :placeholder="$t('page.manage.post.form.postSort')" />
|
||||
</AFormItem>
|
||||
<AFormItem :label="$t('page.manage.post.status')" name="status">
|
||||
<ARadioGroup v-model:value="model.status">
|
||||
<ARadio v-for="item in enableStatusOptions" :key="item.value" :value="item.value">
|
||||
{{ $t(item.label) }}
|
||||
</ARadio>
|
||||
</ARadioGroup>
|
||||
</AFormItem>
|
||||
<AFormItem :label="$t('page.manage.post.remark')" name="remark">
|
||||
<ATextarea v-model:value="model.remark" :placeholder="$t('page.manage.post.form.remark')" />
|
||||
</AFormItem>
|
||||
</AForm>
|
||||
</SimpleScrollbar>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex-y-center justify-end gap-12px">
|
||||
<AButton @click="closeDrawer">{{ $t('common.cancel') }}</AButton>
|
||||
<AButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</AButton>
|
||||
</div>
|
||||
</template>
|
||||
</ADrawer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.ant-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
75
src/views/manage/post/modules/post-search.vue
Normal file
75
src/views/manage/post/modules/post-search.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import { $t } from '@/locales';
|
||||
import { enableStatusOptions } from '@/constants/business';
|
||||
|
||||
defineOptions({
|
||||
name: 'PostSearch'
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
(e: 'reset'): void;
|
||||
(e: 'search'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const model = defineModel<Api.SystemManage.PostSearchParams>('model', {
|
||||
required: true
|
||||
});
|
||||
|
||||
function reset() {
|
||||
emit('reset');
|
||||
}
|
||||
|
||||
function search() {
|
||||
emit('search');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ACard :title="$t('common.search')" :bordered="false" class="card-wrapper">
|
||||
<AForm :model="model" :label-width="80">
|
||||
<ARow :gutter="[16, 16]" wrap>
|
||||
<ACol :span="24" :md="12" :lg="6">
|
||||
<AFormItem :label="$t('page.manage.post.postCode')" name="postCode" class="m-0">
|
||||
<AInput v-model:value="model.postCode" :placeholder="$t('page.manage.post.form.postCode')" />
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
<ACol :span="24" :md="12" :lg="6">
|
||||
<AFormItem :label="$t('page.manage.post.postName')" name="postName" class="m-0">
|
||||
<AInput v-model:value="model.postName" :placeholder="$t('page.manage.post.form.postName')" />
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
<ACol :span="24" :md="12" :lg="6">
|
||||
<AFormItem :label="$t('page.manage.post.status')" name="status" class="m-0">
|
||||
<ASelect v-model:value="model.status" :placeholder="$t('page.manage.post.form.status')" allow-clear>
|
||||
<ASelectOption v-for="option in enableStatusOptions" :key="option.value" :value="option.value">
|
||||
{{ $t(option.label) }}
|
||||
</ASelectOption>
|
||||
</ASelect>
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
<ACol :span="24" :md="12" :lg="6">
|
||||
<AFormItem class="m-0">
|
||||
<div class="w-full flex-y-center justify-end gap-12px">
|
||||
<AButton @click="reset">
|
||||
<div class="flex-y-center gap-8px">
|
||||
<icon-ic-round-refresh class="text-icon" />
|
||||
<span>{{ $t('common.reset') }}</span>
|
||||
</div>
|
||||
</AButton>
|
||||
<AButton type="primary" ghost @click="search">
|
||||
<div class="flex-y-center gap-8px">
|
||||
<icon-ic-round-search class="text-icon" />
|
||||
<span>{{ $t('common.search') }}</span>
|
||||
</div>
|
||||
</AButton>
|
||||
</div>
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
</ARow>
|
||||
</AForm>
|
||||
</ACard>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
181
src/views/manage/role/index.vue
Normal file
181
src/views/manage/role/index.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<script setup lang="tsx">
|
||||
import { Button, Popconfirm, Tag } from 'ant-design-vue';
|
||||
import type { Key } from 'ant-design-vue/es/_util/type';
|
||||
import { useTable, useTableOperate } from '@/hooks/common/table';
|
||||
import { $t } from '@/locales';
|
||||
import { enableStatusRecord } from '@/constants/business';
|
||||
import RoleOperateDrawer from './modules/role-operate-drawer.vue';
|
||||
import RoleSearch from './modules/role-search.vue';
|
||||
|
||||
const wrapperEl = shallowRef<HTMLElement | null>(null);
|
||||
const { height: wrapperElHeight } = useElementSize(wrapperEl);
|
||||
|
||||
const scrollConfig = computed(() => {
|
||||
return {
|
||||
y: wrapperElHeight.value - 72,
|
||||
x: 702
|
||||
};
|
||||
});
|
||||
|
||||
const { columns, columnChecks, data, loading, getData, mobilePagination, searchParams, resetSearchParams } = useTable({
|
||||
apiFn: doGetRoleList,
|
||||
apiParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
status: undefined,
|
||||
roleName: undefined,
|
||||
roleKey: undefined
|
||||
},
|
||||
rowKey: 'roleId',
|
||||
columns: () => [
|
||||
{
|
||||
key: 'roleKey',
|
||||
dataIndex: 'roleKey',
|
||||
title: $t('page.manage.role.roleCode'),
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'roleName',
|
||||
dataIndex: 'roleName',
|
||||
title: $t('page.manage.role.roleName'),
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
dataIndex: 'status',
|
||||
title: $t('page.manage.role.roleStatus'),
|
||||
align: 'center',
|
||||
customRender: ({ record }) => {
|
||||
if (record.status === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tagMap: Record<Api.Common.EnableStatus, string> = {
|
||||
'0': 'success',
|
||||
'1': 'warning'
|
||||
};
|
||||
|
||||
const label = $t(enableStatusRecord[record.status]);
|
||||
|
||||
return <Tag color={tagMap[record.status]}>{label}</Tag>;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'remark',
|
||||
dataIndex: 'remark',
|
||||
title: $t('page.manage.role.roleDesc')
|
||||
},
|
||||
{
|
||||
key: 'createTime',
|
||||
dataIndex: 'createTime',
|
||||
align: 'center',
|
||||
title: '创建时间'
|
||||
},
|
||||
{
|
||||
key: 'operate',
|
||||
title: $t('common.operate'),
|
||||
align: 'center',
|
||||
width: 200,
|
||||
customRender: ({ record }) =>
|
||||
!record.admin && (
|
||||
<div class="flex justify-around gap-8px">
|
||||
{isShowBtn('system:role:edit') && (
|
||||
<Button size="small" onClick={() => edit(record.roleId)}>
|
||||
{$t('common.edit')}
|
||||
</Button>
|
||||
)}
|
||||
{isShowBtn('system:role:remove') && (
|
||||
<Popconfirm onConfirm={() => handleDelete(record.roleId)} title={$t('common.confirmDelete')}>
|
||||
<Button danger size="small">
|
||||
{$t('common.delete')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const {
|
||||
drawerVisible,
|
||||
operateType,
|
||||
editingData,
|
||||
handleAdd,
|
||||
handleEdit,
|
||||
checkedRowKeys,
|
||||
onBatchDeleted,
|
||||
onDeleted
|
||||
// closeDrawer
|
||||
} = useTableOperate(data, { getData, idKey: 'roleId' });
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const { error } = await doDeleteRole(checkedRowKeys.value.join(','));
|
||||
if (!error) {
|
||||
onBatchDeleted();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
const { error } = await doDeleteRole(id);
|
||||
if (!error) {
|
||||
onDeleted();
|
||||
}
|
||||
}
|
||||
|
||||
function edit(id: number) {
|
||||
handleEdit(id);
|
||||
}
|
||||
|
||||
function handleRoleSelectChange(selectedRowKeys: Key[]) {
|
||||
checkedRowKeys.value = selectedRowKeys as number[];
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
|
||||
<RoleSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getData" />
|
||||
<ACard
|
||||
:title="$t('page.manage.role.title')"
|
||||
:bordered="false"
|
||||
:body-style="{ flex: 1, overflow: 'hidden' }"
|
||||
class="flex-col-stretch sm:flex-1-hidden card-wrapper"
|
||||
>
|
||||
<template #extra>
|
||||
<TableHeaderOperation
|
||||
v-model:columns="columnChecks"
|
||||
:show-delete="true"
|
||||
:disabled-delete="checkedRowKeys.length === 0"
|
||||
:loading="loading"
|
||||
@add="handleAdd"
|
||||
@delete="handleBatchDelete"
|
||||
@refresh="getData"
|
||||
/>
|
||||
</template>
|
||||
<ATable
|
||||
ref="wrapperEl"
|
||||
:columns="columns"
|
||||
:data-source="data"
|
||||
:loading="loading"
|
||||
:row-selection="{
|
||||
selectedRowKeys: checkedRowKeys,
|
||||
onChange: handleRoleSelectChange,
|
||||
getCheckboxProps: record => ({ disabled: record.admin })
|
||||
}"
|
||||
row-key="id"
|
||||
size="small"
|
||||
:pagination="mobilePagination"
|
||||
:scroll="scrollConfig"
|
||||
class="h-full"
|
||||
/>
|
||||
<RoleOperateDrawer
|
||||
v-model:visible="drawerVisible"
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
@submitted="getData"
|
||||
/>
|
||||
</ACard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
123
src/views/manage/role/modules/menu-auth.vue
Normal file
123
src/views/manage/role/modules/menu-auth.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
import type { DataNode } from 'ant-design-vue/es/tree';
|
||||
import { SimpleScrollbar } from '@sa/materials';
|
||||
|
||||
defineOptions({
|
||||
name: 'MenuAuthModal'
|
||||
});
|
||||
|
||||
interface Props {
|
||||
/** the roleId */
|
||||
roleId: number;
|
||||
type: 'add' | 'edit';
|
||||
drawerVisible: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const menuIds = defineModel<number[]>('menuIds', { default: [] });
|
||||
|
||||
const tree = shallowRef<DataNode[]>([]);
|
||||
|
||||
watch(
|
||||
() => props.drawerVisible,
|
||||
val => {
|
||||
if (val) {
|
||||
init();
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
|
||||
async function getTree() {
|
||||
const { error, data } = await fetchGetMenuTree();
|
||||
|
||||
if (!error) {
|
||||
tree.value = recursiveTransform(data);
|
||||
}
|
||||
}
|
||||
|
||||
function recursiveTransform(data: Api.SystemManage.MenuTree[]): DataNode[] {
|
||||
return data.map(item => {
|
||||
const { id: key, label } = item;
|
||||
|
||||
if (item.children) {
|
||||
return {
|
||||
key,
|
||||
title: label,
|
||||
children: recursiveTransform(item.children)
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
key,
|
||||
title: label
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function getChecks() {
|
||||
const { data, error } = await doGetRoleMenuList(props.roleId);
|
||||
if (!error) {
|
||||
if (props.type === 'edit') {
|
||||
tree.value = recursiveTransform(data.menus);
|
||||
nextTick(() => {
|
||||
menuIds.value = data.checkedKeys;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function init() {
|
||||
if (props.type === 'edit') {
|
||||
await getChecks();
|
||||
} else {
|
||||
await getTree();
|
||||
}
|
||||
}
|
||||
|
||||
function clearChecks() {
|
||||
menuIds.value = [];
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
clearChecks,
|
||||
checkedKeys: menuIds,
|
||||
tree
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="border-0.5 border-gray-300 rounded-md p-2 transition-all dark:border-dark-300" hover="border-gray-500">
|
||||
<SimpleScrollbar>
|
||||
<ATree
|
||||
v-model:checked-keys="menuIds"
|
||||
:selectable="false"
|
||||
:virtual="false"
|
||||
:tree-data="tree"
|
||||
checkable
|
||||
block-node
|
||||
/>
|
||||
</SimpleScrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.ant-tree .ant-tree-switcher) {
|
||||
line-height: normal;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.ant-tree .ant-tree-checkbox) {
|
||||
margin-block-start: unset;
|
||||
}
|
||||
|
||||
:deep(.ant-tree .ant-tree-title) {
|
||||
font-size: 14px;
|
||||
padding: 2px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
167
src/views/manage/role/modules/role-operate-drawer.vue
Normal file
167
src/views/manage/role/modules/role-operate-drawer.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<script setup lang="ts">
|
||||
import { SimpleScrollbar } from '@sa/materials';
|
||||
import { useAntdForm, useFormRules } from '@/hooks/common/form';
|
||||
import { $t } from '@/locales';
|
||||
import { enableStatusOptions } from '@/constants/business';
|
||||
import { doPostRole, doPutRole } from '@/service/api/role';
|
||||
import MenuAuth from './menu-auth.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'RoleOperateDrawer'
|
||||
});
|
||||
|
||||
interface Props {
|
||||
/** the type of operation */
|
||||
operateType: AntDesign.TableOperateType;
|
||||
/** the edit row data */
|
||||
rowData?: Api.SystemManage.Role | null;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
interface Emits {
|
||||
(e: 'submitted'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
const menuAuthRef = ref<InstanceType<typeof MenuAuth> | null>(null);
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const { formRef, validate, resetFields } = useAntdForm();
|
||||
const { defaultRequiredRule } = useFormRules();
|
||||
|
||||
const title = computed(() => {
|
||||
const titles: Record<AntDesign.TableOperateType, string> = {
|
||||
add: $t('page.manage.role.addRole'),
|
||||
edit: $t('page.manage.role.editRole')
|
||||
};
|
||||
return titles[props.operateType];
|
||||
});
|
||||
|
||||
type Model = Pick<Api.SystemManage.Role, 'roleName' | 'roleKey' | 'remark' | 'status'> & { menuIds: number[] };
|
||||
|
||||
const model = ref<Model>(createDefaultModel());
|
||||
|
||||
function createDefaultModel(): Model {
|
||||
return {
|
||||
roleName: '',
|
||||
roleKey: '',
|
||||
remark: '',
|
||||
status: '0',
|
||||
menuIds: []
|
||||
};
|
||||
}
|
||||
|
||||
type RuleKey = Exclude<keyof Model, 'remark' | 'menuIds'>;
|
||||
|
||||
const rules: Record<RuleKey, App.Global.FormRule> = {
|
||||
roleName: defaultRequiredRule,
|
||||
roleKey: defaultRequiredRule,
|
||||
status: defaultRequiredRule
|
||||
};
|
||||
|
||||
const roleId = computed(() => props.rowData?.roleId || -1);
|
||||
|
||||
function handleUpdateModelWhenEdit() {
|
||||
if (props.operateType === 'add') {
|
||||
model.value = createDefaultModel();
|
||||
}
|
||||
|
||||
if (props.operateType === 'edit' && props.rowData) {
|
||||
model.value = { ...props.rowData, menuIds: [] };
|
||||
}
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
const menuIds = transformMenuChildWithRootIds(menuAuthRef.value?.tree || [], model.value.menuIds);
|
||||
|
||||
const { error } = await (props.operateType === 'edit' ? doPutRole : doPostRole)({
|
||||
...model.value,
|
||||
menuIds,
|
||||
menuCheckStrictly: true
|
||||
} as Api.SystemManage.Role);
|
||||
|
||||
if (!error) {
|
||||
authStore.refreshUserInfo();
|
||||
$message?.success($t(props.operateType === 'add' ? 'common.addSuccess' : 'common.updateSuccess'));
|
||||
closeDrawer();
|
||||
emit('submitted');
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
val => {
|
||||
if (val) {
|
||||
handleUpdateModelWhenEdit();
|
||||
resetFields();
|
||||
} else {
|
||||
menuAuthRef.value?.clearChecks();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ADrawer
|
||||
v-model:open="visible"
|
||||
:body-style="{ paddingRight: '0px', paddingTop: '0', paddingBottom: '0' }"
|
||||
:title="title"
|
||||
:width="460"
|
||||
>
|
||||
<SimpleScrollbar>
|
||||
<AForm ref="formRef" py-20px pr-20px layout="vertical" :model="model" :rules="rules">
|
||||
<AFormItem :label="$t('page.manage.role.roleName')" name="roleName">
|
||||
<AInput v-model:value="model.roleName" :placeholder="$t('page.manage.role.form.roleName')" />
|
||||
</AFormItem>
|
||||
<AFormItem :label="$t('page.manage.role.roleCode')" name="roleKey">
|
||||
<AInput v-model:value="model.roleKey" :placeholder="$t('page.manage.role.form.roleCode')" />
|
||||
</AFormItem>
|
||||
<AFormItem :label="$t('page.manage.role.roleStatus')" name="status">
|
||||
<ARadioGroup v-model:value="model.status">
|
||||
<ARadio v-for="item in enableStatusOptions" :key="item.value" :value="item.value">
|
||||
{{ $t(item.label) }}
|
||||
</ARadio>
|
||||
</ARadioGroup>
|
||||
</AFormItem>
|
||||
<AFormItem :label="$t('page.manage.role.roleDesc')" name="roleDesc">
|
||||
<AInput v-model:value="model.remark" :placeholder="$t('page.manage.role.form.roleDesc')" />
|
||||
</AFormItem>
|
||||
|
||||
<AFormItem>
|
||||
<MenuAuth
|
||||
ref="menuAuthRef"
|
||||
v-model:menu-ids="model.menuIds"
|
||||
:drawer-visible="visible"
|
||||
:type="operateType"
|
||||
:role-id="roleId"
|
||||
/>
|
||||
<template #label>
|
||||
<div class="w-full flex-between">
|
||||
<span>{{ $t('page.manage.role.menuAuth') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</AFormItem>
|
||||
</AForm>
|
||||
</SimpleScrollbar>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex-y-center justify-end gap-12px">
|
||||
<AButton @click="closeDrawer">{{ $t('common.cancel') }}</AButton>
|
||||
<AButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</AButton>
|
||||
</div>
|
||||
</template>
|
||||
</ADrawer>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
73
src/views/manage/role/modules/role-search.vue
Normal file
73
src/views/manage/role/modules/role-search.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import { $t } from '@/locales';
|
||||
import { enableStatusOptions } from '@/constants/business';
|
||||
|
||||
defineOptions({
|
||||
name: 'RoleSearch'
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
(e: 'reset'): void;
|
||||
(e: 'search'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const model = defineModel<Api.SystemManage.RoleSearchParams>('model', { required: true });
|
||||
|
||||
function reset() {
|
||||
emit('reset');
|
||||
}
|
||||
|
||||
function search() {
|
||||
emit('search');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ACard :title="$t('common.search')" :bordered="false" class="card-wrapper">
|
||||
<AForm :model="model" :label-width="80">
|
||||
<ARow :gutter="[16, 16]" wrap>
|
||||
<ACol :span="24" :md="12" :lg="6">
|
||||
<AFormItem :label="$t('page.manage.role.roleName')" name="roleName" class="m-0">
|
||||
<AInput v-model:value="model.roleName" :placeholder="$t('page.manage.role.form.roleName')" />
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
<ACol :span="24" :md="12" :lg="6">
|
||||
<AFormItem :label="$t('page.manage.role.roleCode')" name="roleCode" class="m-0">
|
||||
<AInput v-model:value="model.roleKey" :placeholder="$t('page.manage.role.form.roleCode')" />
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
<ACol :span="24" :md="12" :lg="6">
|
||||
<AFormItem :label="$t('page.manage.role.roleStatus')" name="status" class="m-0">
|
||||
<ASelect v-model:value="model.status" :placeholder="$t('page.manage.role.form.roleStatus')" allow-clear>
|
||||
<ASelectOption v-for="option in enableStatusOptions" :key="option.value" :value="option.value">
|
||||
{{ $t(option.label) }}
|
||||
</ASelectOption>
|
||||
</ASelect>
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
<ACol :span="24" :md="12" :lg="6">
|
||||
<AFormItem class="m-0">
|
||||
<div class="w-full flex-y-center justify-end gap-12px">
|
||||
<AButton @click="reset">
|
||||
<div class="flex-y-center gap-8px">
|
||||
<icon-ic-round-refresh class="text-icon" />
|
||||
<span>{{ $t('common.reset') }}</span>
|
||||
</div>
|
||||
</AButton>
|
||||
<AButton type="primary" ghost @click="search">
|
||||
<div class="flex-y-center gap-8px">
|
||||
<icon-ic-round-search class="text-icon" />
|
||||
<span>{{ $t('common.search') }}</span>
|
||||
</div>
|
||||
</AButton>
|
||||
</div>
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
</ARow>
|
||||
</AForm>
|
||||
</ACard>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
7
src/views/manage/route/index.vue
Normal file
7
src/views/manage/route/index.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div>路由</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
15
src/views/manage/user-detail/[id].vue
Normal file
15
src/views/manage/user-detail/[id].vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
id: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LookForward>
|
||||
<p>{{ id }}</p>
|
||||
</LookForward>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
216
src/views/manage/user/index.vue
Normal file
216
src/views/manage/user/index.vue
Normal file
@@ -0,0 +1,216 @@
|
||||
<script setup lang="tsx">
|
||||
import { Button, Popconfirm, Tag } from 'ant-design-vue';
|
||||
import type { Key } from 'ant-design-vue/es/_util/type';
|
||||
import { useTable, useTableOperate } from '@/hooks/common/table';
|
||||
import { $t } from '@/locales';
|
||||
import { enableStatusRecord } from '@/constants/business';
|
||||
import { SimpleScrollbar } from '~/packages/materials/src';
|
||||
import UserOperateDrawer from './modules/user-operate-drawer.vue';
|
||||
import UserSearch from './modules/user-search.vue';
|
||||
|
||||
const wrapperEl = shallowRef<HTMLElement | null>(null);
|
||||
const { height: wrapperElHeight } = useElementSize(wrapperEl);
|
||||
|
||||
const scrollConfig = computed(() => {
|
||||
return {
|
||||
y: wrapperElHeight.value - 72,
|
||||
x: 1000
|
||||
};
|
||||
});
|
||||
|
||||
const { columns, columnChecks, data, loading, getData, mobilePagination, searchParams, resetSearchParams } = useTable({
|
||||
apiFn: doGetUserList,
|
||||
apiParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
status: undefined,
|
||||
userName: undefined,
|
||||
email: undefined
|
||||
},
|
||||
rowKey: 'userId',
|
||||
columns: () => [
|
||||
{
|
||||
key: 'userName',
|
||||
dataIndex: 'userName',
|
||||
title: $t('page.manage.user.userName'),
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'nickName',
|
||||
dataIndex: 'nickName',
|
||||
title: $t('page.manage.user.nickName'),
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'email',
|
||||
dataIndex: 'email',
|
||||
title: $t('page.manage.user.email'),
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'phonenumber',
|
||||
dataIndex: 'phonenumber',
|
||||
title: $t('page.manage.user.phonenumber'),
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
dataIndex: 'status',
|
||||
title: $t('page.manage.user.status'),
|
||||
align: 'center',
|
||||
customRender: ({ record }) => {
|
||||
if (record.status === null) {
|
||||
return null;
|
||||
}
|
||||
const tagMap: Record<Api.Common.EnableStatus, string> = {
|
||||
'0': 'success',
|
||||
'1': 'warning'
|
||||
};
|
||||
const label = $t(enableStatusRecord[record.status]);
|
||||
return <Tag color={tagMap[record.status]}>{label}</Tag>;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'dept',
|
||||
dataIndex: 'dept',
|
||||
title: $t('page.manage.user.dept'),
|
||||
align: 'center',
|
||||
customRender: ({ record }) => record.dept?.deptName || ''
|
||||
},
|
||||
{
|
||||
key: 'createTime',
|
||||
dataIndex: 'createTime',
|
||||
align: 'center',
|
||||
title: '创建时间'
|
||||
},
|
||||
{
|
||||
key: 'operate',
|
||||
title: $t('common.operate'),
|
||||
align: 'center',
|
||||
width: 200,
|
||||
customRender: ({ record }) =>
|
||||
!record.admin && (
|
||||
<div class="flex justify-around gap-8px">
|
||||
{isShowBtn('system:user:edit') && (
|
||||
<Button size="small" onClick={() => edit(record.userId)}>
|
||||
{$t('common.edit')}
|
||||
</Button>
|
||||
)}
|
||||
{isShowBtn('system:user:remove') && (
|
||||
<Popconfirm onConfirm={() => handleDelete(record.userId)} title={$t('common.confirmDelete')}>
|
||||
<Button danger size="small">
|
||||
{$t('common.delete')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const {
|
||||
drawerVisible,
|
||||
operateType,
|
||||
editingData,
|
||||
handleAdd,
|
||||
handleEdit,
|
||||
checkedRowKeys,
|
||||
onBatchDeleted,
|
||||
onDeleted
|
||||
// closeDrawer
|
||||
} = useTableOperate(data, { getData, idKey: 'userId' });
|
||||
|
||||
const deptTreeData = ref<Api.Common.CommonTree>([]);
|
||||
|
||||
onMounted(() => {
|
||||
getUserDeptTree();
|
||||
});
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const { error } = await doDeleteUser(checkedRowKeys.value.join(','));
|
||||
if (!error) {
|
||||
onBatchDeleted();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
const { error } = await doDeleteUser(id);
|
||||
if (!error) {
|
||||
onDeleted();
|
||||
}
|
||||
}
|
||||
|
||||
function edit(id: number) {
|
||||
handleEdit(id);
|
||||
}
|
||||
|
||||
function handleUserSelectChange(selectedRowKeys: Key[]) {
|
||||
checkedRowKeys.value = selectedRowKeys as number[];
|
||||
}
|
||||
|
||||
async function getUserDeptTree() {
|
||||
const { error, data: tree } = await doGetUserDeptTree();
|
||||
if (!error) {
|
||||
deptTreeData.value = tree;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SimpleScrollbar>
|
||||
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
|
||||
<UserSearch
|
||||
v-model:model="searchParams"
|
||||
:dept-tree-data="deptTreeData"
|
||||
@reset="resetSearchParams"
|
||||
@search="getData"
|
||||
/>
|
||||
<ACard
|
||||
:title="$t('page.manage.user.title')"
|
||||
:bordered="false"
|
||||
:body-style="{ flex: 1, overflow: 'hidden' }"
|
||||
class="flex-col-stretch sm:flex-1-hidden card-wrapper"
|
||||
>
|
||||
<template #extra>
|
||||
<TableHeaderOperation
|
||||
v-model:columns="columnChecks"
|
||||
:disabled-delete="checkedRowKeys.length === 0"
|
||||
:loading="loading"
|
||||
:show-delete="true"
|
||||
@add="handleAdd"
|
||||
@delete="handleBatchDelete"
|
||||
@refresh="getData"
|
||||
/>
|
||||
</template>
|
||||
<ATable
|
||||
ref="wrapperEl"
|
||||
:columns="columns"
|
||||
:data-source="data"
|
||||
:loading="loading"
|
||||
:row-selection="{
|
||||
selectedRowKeys: checkedRowKeys,
|
||||
onChange: handleUserSelectChange,
|
||||
getCheckboxProps: record => ({
|
||||
disabled: record.admin
|
||||
})
|
||||
}"
|
||||
row-key="userId"
|
||||
size="small"
|
||||
:pagination="mobilePagination"
|
||||
:scroll="scrollConfig"
|
||||
class="h-full"
|
||||
/>
|
||||
<UserOperateDrawer
|
||||
v-model:visible="drawerVisible"
|
||||
:dept-tree-data="deptTreeData"
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
@submitted="getData"
|
||||
/>
|
||||
</ACard>
|
||||
</div>
|
||||
</SimpleScrollbar>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
216
src/views/manage/user/modules/user-operate-drawer.vue
Normal file
216
src/views/manage/user/modules/user-operate-drawer.vue
Normal file
@@ -0,0 +1,216 @@
|
||||
<script setup lang="ts">
|
||||
import { SimpleScrollbar } from '@sa/materials';
|
||||
import { useAntdForm, useFormRules } from '@/hooks/common/form';
|
||||
import { $t } from '@/locales';
|
||||
import { enableStatusOptions } from '@/constants/business';
|
||||
|
||||
defineOptions({
|
||||
name: 'UserOperateDrawer'
|
||||
});
|
||||
|
||||
interface Props {
|
||||
/** the type of operation */
|
||||
operateType: AntDesign.TableOperateType;
|
||||
/** the edit row data */
|
||||
rowData?: Api.Auth.User | null;
|
||||
deptTreeData: Api.Common.CommonTree;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
interface Emits {
|
||||
(e: 'submitted'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const { formRef, validate, resetFields } = useAntdForm();
|
||||
const { defaultRequiredRule, formRules } = useFormRules();
|
||||
const [spinning, spin] = useToggle();
|
||||
|
||||
const title = computed(() => {
|
||||
const titles: Record<AntDesign.TableOperateType, string> = {
|
||||
add: $t('page.manage.user.addUser'),
|
||||
edit: $t('page.manage.user.editUser')
|
||||
};
|
||||
return titles[props.operateType];
|
||||
});
|
||||
|
||||
type Model = Partial<
|
||||
Pick<Api.Auth.User, 'userName' | 'nickName' | 'email' | 'phonenumber' | 'status' | 'deptId' | 'remark'> & {
|
||||
postIds: number[];
|
||||
roleIds: number[];
|
||||
password?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
const model = ref<Model>(createDefaultModel());
|
||||
|
||||
const userPosts = ref<Api.SystemManage.Post[]>([]);
|
||||
const userRoles = ref<Api.SystemManage.Role[]>([]);
|
||||
|
||||
function createDefaultModel(): Model {
|
||||
return {
|
||||
userName: '',
|
||||
nickName: '',
|
||||
email: '',
|
||||
phonenumber: '',
|
||||
status: '0',
|
||||
deptId: undefined,
|
||||
remark: '',
|
||||
postIds: [],
|
||||
roleIds: [],
|
||||
password: ''
|
||||
};
|
||||
}
|
||||
|
||||
const rules = {
|
||||
userName: defaultRequiredRule,
|
||||
nickName: defaultRequiredRule,
|
||||
status: defaultRequiredRule,
|
||||
deptId: defaultRequiredRule,
|
||||
email: formRules.email,
|
||||
phonenumber: formRules.phone,
|
||||
password: formRules.pwd,
|
||||
postIds: defaultRequiredRule,
|
||||
roleIds: defaultRequiredRule
|
||||
};
|
||||
|
||||
async function init(userId: number | undefined = undefined) {
|
||||
spin(true);
|
||||
try {
|
||||
await Promise.all([getUserPostAndRole(userId)]);
|
||||
spin(false);
|
||||
} catch (error) {
|
||||
spin(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserPostAndRole(userId: number | undefined) {
|
||||
const { error, data } = await doGetUserPostsAndRoles(userId);
|
||||
if (!error) {
|
||||
const { postIds, posts, roleIds, roles } = data;
|
||||
userPosts.value = posts;
|
||||
userRoles.value = roles;
|
||||
model.value.postIds = postIds;
|
||||
model.value.roleIds = roleIds;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdateModelWhenEdit() {
|
||||
if (props.operateType === 'add') {
|
||||
model.value = createDefaultModel();
|
||||
}
|
||||
|
||||
if (props.operateType === 'edit' && props.rowData) {
|
||||
await init(props.rowData.userId);
|
||||
model.value = Object.assign(model.value, omit(props.rowData, ['postIds', 'roleIds']));
|
||||
} else {
|
||||
await init();
|
||||
model.value = createDefaultModel();
|
||||
}
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
|
||||
const { error } = await (props.operateType === 'edit' ? doPutUser : doPostUser)(model.value as Api.Auth.User);
|
||||
|
||||
if (!error) {
|
||||
$message?.success($t(props.operateType === 'add' ? 'common.addSuccess' : 'common.updateSuccess'));
|
||||
closeDrawer();
|
||||
emit('submitted');
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
val => {
|
||||
if (val) {
|
||||
handleUpdateModelWhenEdit();
|
||||
resetFields();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ADrawer
|
||||
v-model:open="visible"
|
||||
:body-style="{ paddingRight: '0px', paddingTop: '0', paddingBottom: '0' }"
|
||||
:title="title"
|
||||
:width="460"
|
||||
>
|
||||
<SimpleScrollbar>
|
||||
<ASpin :spinning="spinning" size="small">
|
||||
<AForm ref="formRef" py-20px pr-20px layout="vertical" :model="model" :rules="rules">
|
||||
<AFormItem :label="$t('page.manage.user.userName')" name="userName">
|
||||
<AInput v-model:value="model.userName" :placeholder="$t('page.manage.user.form.userName')" />
|
||||
</AFormItem>
|
||||
<AFormItem :label="$t('page.manage.user.nickName')" name="nickName">
|
||||
<AInput v-model:value="model.nickName" :placeholder="$t('page.manage.user.form.nickName')" />
|
||||
</AFormItem>
|
||||
<AFormItem :label="$t('page.manage.user.email')" name="email">
|
||||
<AInput v-model:value="model.email" :placeholder="$t('page.manage.user.form.email')" />
|
||||
</AFormItem>
|
||||
<AFormItem :label="$t('page.manage.user.phonenumber')" name="phonenumber">
|
||||
<AInput v-model:value="model.phonenumber" :placeholder="$t('page.manage.user.form.phonenumber')" />
|
||||
</AFormItem>
|
||||
<AFormItem v-if="props.operateType === 'add'" :label="$t('page.manage.user.password')" name="password">
|
||||
<AInput
|
||||
v-model:value="model.password"
|
||||
type="password"
|
||||
:placeholder="$t('page.manage.user.form.password')"
|
||||
/>
|
||||
</AFormItem>
|
||||
<AFormItem :label="$t('page.manage.user.post')" name="postIds">
|
||||
<ASelect
|
||||
v-model:value="model.postIds"
|
||||
:field-names="{ label: 'postName', value: 'postId' }"
|
||||
mode="multiple"
|
||||
:options="userPosts"
|
||||
/>
|
||||
</AFormItem>
|
||||
<AFormItem :label="$t('page.manage.user.role')" name="roleIds">
|
||||
<ASelect
|
||||
v-model:value="model.roleIds"
|
||||
:field-names="{ label: 'roleName', value: 'roleId' }"
|
||||
mode="multiple"
|
||||
:options="userRoles"
|
||||
/>
|
||||
</AFormItem>
|
||||
<AFormItem :label="$t('page.manage.user.status')" name="status">
|
||||
<ARadioGroup v-model:value="model.status">
|
||||
<ARadio v-for="item in enableStatusOptions" :key="item.value" :value="item.value">
|
||||
{{ $t(item.label) }}
|
||||
</ARadio>
|
||||
</ARadioGroup>
|
||||
</AFormItem>
|
||||
<AFormItem :label="$t('page.manage.user.dept')" name="deptId">
|
||||
<ATreeSelect v-model:value="model.deptId" :field-names="{ value: 'id' }" :tree-data="deptTreeData" />
|
||||
</AFormItem>
|
||||
<AFormItem :label="$t('page.manage.user.remark')" name="remark">
|
||||
<ATextarea v-model:value="model.remark" :placeholder="$t('page.manage.user.form.remark')" />
|
||||
</AFormItem>
|
||||
</AForm>
|
||||
</ASpin>
|
||||
</SimpleScrollbar>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex-y-center justify-end gap-12px">
|
||||
<AButton @click="closeDrawer">{{ $t('common.cancel') }}</AButton>
|
||||
<AButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</AButton>
|
||||
</div>
|
||||
</template>
|
||||
</ADrawer>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
90
src/views/manage/user/modules/user-search.vue
Normal file
90
src/views/manage/user/modules/user-search.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<script setup lang="ts">
|
||||
import { $t } from '@/locales';
|
||||
import { enableStatusOptions } from '@/constants/business';
|
||||
|
||||
defineOptions({
|
||||
name: 'UserSearch'
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
(e: 'reset'): void;
|
||||
(e: 'search'): void;
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
deptTreeData: Api.Common.CommonTree;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const model = defineModel<Api.SystemManage.UserSearchParams>('model', { required: true });
|
||||
|
||||
function reset() {
|
||||
emit('reset');
|
||||
}
|
||||
|
||||
function search() {
|
||||
emit('search');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ACard :title="$t('common.search')" :bordered="false" class="card-wrapper">
|
||||
<AForm :model="model" :label-width="80">
|
||||
<ARow :gutter="[16, 16]" wrap>
|
||||
<ACol :span="24" :md="12" :lg="4">
|
||||
<AFormItem :label="$t('page.manage.user.userName')" name="userName" class="m-0">
|
||||
<AInput v-model:value="model.userName" :placeholder="$t('page.manage.user.form.userName')" />
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
<ACol :span="24" :md="12" :lg="4">
|
||||
<AFormItem :label="$t('page.manage.user.phonenumber')" name="phonenumber" class="m-0">
|
||||
<AInput v-model:value="model.phonenumber" :placeholder="$t('page.manage.user.form.phonenumber')" />
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
<ACol :span="24" :md="12" :lg="4">
|
||||
<AFormItem :label="$t('page.manage.user.status')" name="status" class="m-0">
|
||||
<ASelect v-model:value="model.status" :placeholder="$t('page.manage.user.form.status')" allow-clear>
|
||||
<ASelectOption v-for="option in enableStatusOptions" :key="option.value" :value="option.value">
|
||||
{{ $t(option.label) }}
|
||||
</ASelectOption>
|
||||
</ASelect>
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
<ACol :span="24" :md="12" :lg="4">
|
||||
<!-- 部门 -->
|
||||
<AFormItem :label="$t('page.manage.user.dept')" name="deptId" class="m-0">
|
||||
<ATreeSelect
|
||||
v-model:value="model.deptId"
|
||||
:field-names="{ value: 'id' }"
|
||||
:placeholder="$t('page.manage.user.form.dept')"
|
||||
:tree-data="deptTreeData"
|
||||
:tree-default-expand-all="true"
|
||||
:allow-clear="true"
|
||||
/>
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
<ACol :span="24" :md="12" :lg="8">
|
||||
<AFormItem class="m-0">
|
||||
<div class="w-full flex-y-center justify-end gap-12px">
|
||||
<AButton @click="reset">
|
||||
<div class="flex-y-center gap-8px">
|
||||
<icon-ic-round-refresh class="text-icon" />
|
||||
<span>{{ $t('common.reset') }}</span>
|
||||
</div>
|
||||
</AButton>
|
||||
<AButton type="primary" ghost @click="search">
|
||||
<div class="flex-y-center gap-8px">
|
||||
<icon-ic-round-search class="text-icon" />
|
||||
<span>{{ $t('common.search') }}</span>
|
||||
</div>
|
||||
</AButton>
|
||||
</div>
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
</ARow>
|
||||
</AForm>
|
||||
</ACard>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
11
src/views/user-center/index.vue
Normal file
11
src/views/user-center/index.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>{{ authStore.userInfo.user }}</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
Reference in New Issue
Block a user