mirror of
https://github.com/m-xlsea/ruoyi-plus-soybean.git
synced 2025-09-24 07:49:47 +08:00
feat(projects): 1.0 beta
This commit is contained in:
@ -1,7 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<exception-base type="403" />
|
||||
<ExceptionBase type="403" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<exception-base type="404" />
|
||||
<ExceptionBase type="404" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<exception-base type="500" />
|
||||
<ExceptionBase type="500" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@ -1,107 +0,0 @@
|
||||
<template>
|
||||
<div class="wh-full flex-col-center">
|
||||
<n-gradient-text class="mb-24px" type="primary" :size="28">Custom Constant Page</n-gradient-text>
|
||||
<router-link :to="{ name: routeHomePath }">
|
||||
<n-button type="primary">回到首页</n-button>
|
||||
</router-link>
|
||||
<n-card :bordered="false" size="small" class="mt-24px rounded-8px shadow-sm">
|
||||
<div class="flex-center py-12px">
|
||||
<n-button type="primary" class="mr-24px" :disabled="isMoving" @click="startMove">开始</n-button>
|
||||
<n-button type="error" @click="endMove">暂停</n-button>
|
||||
</div>
|
||||
<div class="flex-center">
|
||||
<div class="relative bg-primary_active" :style="containerStyle">
|
||||
<svg class="w-full h-full">
|
||||
<ellipse
|
||||
:cx="ellipseConfig.cX"
|
||||
:cy="ellipseConfig.cY"
|
||||
:rx="ellipseConfig.rX"
|
||||
:ry="ellipseConfig.rY"
|
||||
:style="{ strokeWidth: ellipseConfig.strokeWidth + 'px' }"
|
||||
class="fill-none stroke-primary"
|
||||
></ellipse>
|
||||
</svg>
|
||||
<div class="absolute left-182px top-82px w-40px h-40px bg-red rounded-20px" :style="transformStyle"></div>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { routeName } from '@/router';
|
||||
|
||||
const routeHomePath = routeName('root');
|
||||
|
||||
interface EllipseConfig {
|
||||
/** 左上角x坐标 */
|
||||
cX: number;
|
||||
/** 左上角y坐标 */
|
||||
cY: number;
|
||||
/** 椭圆长半轴A */
|
||||
rX: number;
|
||||
/** 椭圆短半轴B */
|
||||
rY: number;
|
||||
/** 椭圆线宽 */
|
||||
strokeWidth: number;
|
||||
}
|
||||
|
||||
const ellipseConfig: EllipseConfig = {
|
||||
cX: 202,
|
||||
cY: 102,
|
||||
rX: 200,
|
||||
rY: 100,
|
||||
strokeWidth: 2
|
||||
};
|
||||
|
||||
const containerStyle = (() => {
|
||||
const { rX, rY, strokeWidth } = ellipseConfig;
|
||||
|
||||
const width = (rX + strokeWidth) * 2;
|
||||
const height = (rY + strokeWidth) * 2;
|
||||
|
||||
return `width:${width}px;height:${height}px;`;
|
||||
})();
|
||||
|
||||
/** 角度 */
|
||||
const angle = ref(0);
|
||||
|
||||
const transformStyle = computed(() => {
|
||||
const { rX, rY } = ellipseConfig;
|
||||
const x = rX * Math.sin(angle.value);
|
||||
const y = rY * Math.cos(angle.value);
|
||||
|
||||
return `transform: translate3d(${x}px,${y}px,0px)`;
|
||||
});
|
||||
|
||||
/** 运动速度(周/秒) */
|
||||
const speed = ref(2);
|
||||
|
||||
/** 一周的角度(弧度制) */
|
||||
const FULL_ANGLE = 2 * Math.PI;
|
||||
|
||||
/** requestAnimationFrame一秒执行的次数 */
|
||||
const TIMES_PER_SECONDS = 60;
|
||||
|
||||
const updateAnglePerTime = computed(() => FULL_ANGLE / speed.value / TIMES_PER_SECONDS);
|
||||
|
||||
const rafId = ref<number | null>(null);
|
||||
|
||||
const isMoving = computed(() => rafId.value !== null);
|
||||
|
||||
const startMove = () => {
|
||||
angle.value += updateAnglePerTime.value;
|
||||
|
||||
rafId.value = window.requestAnimationFrame(startMove);
|
||||
};
|
||||
|
||||
const endMove = () => {
|
||||
if (rafId.value !== null) {
|
||||
window.cancelAnimationFrame(rafId.value);
|
||||
rafId.value = null;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
11
src/views/_builtin/login/components/bind-wechat.vue
Normal file
11
src/views/_builtin/login/components/bind-wechat.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'BindWechat'
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
@ -1,60 +0,0 @@
|
||||
<template>
|
||||
<n-form ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
|
||||
<n-form-item path="phone">
|
||||
<n-input v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
|
||||
</n-form-item>
|
||||
<n-form-item path="code">
|
||||
<div class="flex-y-center w-full">
|
||||
<n-input v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
|
||||
<div class="w-18px"></div>
|
||||
<n-button size="large" :disabled="isCounting" :loading="smsLoading" @click="handleSmsCode">
|
||||
{{ label }}
|
||||
</n-button>
|
||||
</div>
|
||||
</n-form-item>
|
||||
<n-space :vertical="true" size="large">
|
||||
<n-button type="primary" size="large" :block="true" :round="true" @click="handleSubmit">
|
||||
{{ $t('page.login.common.confirm') }}
|
||||
</n-button>
|
||||
<n-button size="large" :block="true" :round="true" @click="toLoginModule('pwd-login')">
|
||||
{{ $t('page.login.common.back') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue';
|
||||
import type { FormInst } from 'naive-ui';
|
||||
import { useRouterPush } from '@/composables';
|
||||
import { useSmsCode } from '@/hooks';
|
||||
import { formRules } from '@/utils';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
const { toLoginModule } = useRouterPush();
|
||||
const { label, isCounting, loading: smsLoading, getSmsCode } = useSmsCode();
|
||||
|
||||
const formRef = ref<HTMLElement & FormInst>();
|
||||
|
||||
const model = reactive({
|
||||
phone: '',
|
||||
code: '',
|
||||
imgCode: ''
|
||||
});
|
||||
|
||||
const rules = {
|
||||
phone: formRules.phone,
|
||||
code: formRules.code
|
||||
};
|
||||
|
||||
function handleSmsCode() {
|
||||
getSmsCode(model.phone);
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await formRef.value?.validate();
|
||||
window.$message?.success($t('page.login.common.validateSuccess'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
31
src/views/_builtin/login/components/code-login.vue
Normal file
31
src/views/_builtin/login/components/code-login.vue
Normal file
@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { $t } from '@/locales';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
|
||||
defineOptions({
|
||||
name: 'CodeLogin'
|
||||
});
|
||||
|
||||
const { toggleLoginModule } = useRouterPush();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NForm size="large" :show-label="false">
|
||||
<NFormItem>
|
||||
<NInput :placeholder="$t('page.login.common.phonePlaceholder')" />
|
||||
</NFormItem>
|
||||
<NFormItem>
|
||||
<NInput :placeholder="$t('page.login.common.codePlaceholder')" />
|
||||
</NFormItem>
|
||||
<NSpace vertical :size="18" class="w-full">
|
||||
<NButton type="primary" size="large" block round>
|
||||
{{ $t('common.confirm') }}
|
||||
</NButton>
|
||||
<NButton size="large" block round @click="toggleLoginModule('pwd-login')">
|
||||
{{ $t('page.login.common.back') }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NForm>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
@ -1,78 +0,0 @@
|
||||
<template>
|
||||
<n-form ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
|
||||
<n-form-item path="phone">
|
||||
<n-input v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
|
||||
</n-form-item>
|
||||
<n-form-item path="code">
|
||||
<div class="flex-y-center w-full">
|
||||
<n-input v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
|
||||
<div class="w-18px"></div>
|
||||
<n-button size="large" :disabled="isCounting" :loading="smsLoading" @click="handleSmsCode">
|
||||
{{ label }}
|
||||
</n-button>
|
||||
</div>
|
||||
</n-form-item>
|
||||
<n-form-item path="imgCode">
|
||||
<n-input v-model:value="model.imgCode" :placeholder="$t('page.login.codeLogin.imageCodePlaceholder')" />
|
||||
<div class="pl-8px">
|
||||
<image-verify v-model:code="imgCode" />
|
||||
</div>
|
||||
</n-form-item>
|
||||
<n-space :vertical="true" :size="18">
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:block="true"
|
||||
:round="true"
|
||||
:loading="auth.loginLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ $t('page.login.common.confirm') }}
|
||||
</n-button>
|
||||
<n-button size="large" :block="true" :round="true" @click="toLoginModule('pwd-login')">
|
||||
{{ $t('page.login.common.back') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue';
|
||||
import type { FormInst } from 'naive-ui';
|
||||
import { useAuthStore } from '@/store';
|
||||
import { useRouterPush } from '@/composables';
|
||||
import { useSmsCode } from '@/hooks';
|
||||
import { formRules, getImgCodeRule } from '@/utils';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
const auth = useAuthStore();
|
||||
const { toLoginModule } = useRouterPush();
|
||||
const { label, isCounting, loading: smsLoading, getSmsCode } = useSmsCode();
|
||||
|
||||
const formRef = ref<HTMLElement & FormInst>();
|
||||
|
||||
const model = reactive({
|
||||
phone: '',
|
||||
code: '',
|
||||
imgCode: ''
|
||||
});
|
||||
|
||||
const imgCode = ref('');
|
||||
|
||||
const rules = {
|
||||
phone: formRules.phone,
|
||||
code: formRules.code,
|
||||
imgCode: getImgCodeRule(imgCode)
|
||||
};
|
||||
|
||||
function handleSmsCode() {
|
||||
getSmsCode(model.phone);
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await formRef.value?.validate();
|
||||
window.$message?.success($t('page.login.common.validateSuccess'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,8 +0,0 @@
|
||||
import LoginBg from './login-bg/index.vue';
|
||||
import PwdLogin from './pwd-login/index.vue';
|
||||
import CodeLogin from './code-login/index.vue';
|
||||
import Register from './register-user/index.vue';
|
||||
import ResetPwd from './reset-pwd/index.vue';
|
||||
import BindWechat from './bind-wechat/index.vue';
|
||||
|
||||
export { LoginBg, PwdLogin, CodeLogin, Register, ResetPwd, BindWechat };
|
@ -1,41 +0,0 @@
|
||||
<template>
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
height="896"
|
||||
width="967.8852157128662"
|
||||
>
|
||||
<defs>
|
||||
<path
|
||||
id="path-2"
|
||||
opacity="1"
|
||||
fill-rule="evenodd"
|
||||
d="M896,448 C1142.6325445712241,465.5747656464056 695.2579309733121,896 448,896 C200.74206902668806,896 5.684341886080802e-14,695.2579309733121 0,448.0000000000001 C0,200.74206902668806 200.74206902668791,5.684341886080802e-14 447.99999999999994,0 C695.2579309733121,0 475,418 896,448Z"
|
||||
/>
|
||||
<linearGradient id="linearGradient-3" x1="0.5" y1="0" x2="0.5" y2="1">
|
||||
<stop offset="0" :stop-color="startColor" stop-opacity="1" />
|
||||
<stop offset="1" :stop-color="endColor" stop-opacity="1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g opacity="1">
|
||||
<use xlink:href="#path-2" fill="url(#linearGradient-3)" fill-opacity="1" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
interface Props {
|
||||
/** 过渡的开始颜色 */
|
||||
startColor?: string;
|
||||
/** 过渡的结束颜色 */
|
||||
endColor?: string;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
startColor: '#28aff0',
|
||||
endColor: '#120fc4'
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,35 +0,0 @@
|
||||
<template>
|
||||
<svg height="1337" width="1337">
|
||||
<defs>
|
||||
<path
|
||||
id="path-1"
|
||||
opacity="1"
|
||||
fill-rule="evenodd"
|
||||
d="M1337,668.5 C1337,1037.455193874239 1037.455193874239,1337 668.5,1337 C523.6725684305388,1337 337,1236 370.50000000000006,1094 C434.03835568300906,824.6732385973953 6.906089672974592e-14,892.6277623047779 0,668.5000000000001 C0,299.5448061257611 299.5448061257609,1.1368683772161603e-13 668.4999999999999,0 C1037.455193874239,0 1337,299.544806125761 1337,668.5Z"
|
||||
/>
|
||||
<linearGradient id="linearGradient-2" x1="0.79" y1="0.62" x2="0.21" y2="0.86">
|
||||
<stop offset="0" :stop-color="startColor" stop-opacity="1" />
|
||||
<stop offset="1" :stop-color="endColor" stop-opacity="1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g opacity="1">
|
||||
<use xlink:href="#path-1" fill="url(#linearGradient-2)" fill-opacity="1" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
interface Props {
|
||||
/** 过渡的开始颜色 */
|
||||
startColor?: string;
|
||||
/** 过渡的结束颜色 */
|
||||
endColor?: string;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
startColor: '#28aff0',
|
||||
endColor: '#120fc4'
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,4 +0,0 @@
|
||||
import CornerTop from './corner-top.vue';
|
||||
import CornerBottom from './corner-bottom.vue';
|
||||
|
||||
export { CornerTop, CornerBottom };
|
@ -1,28 +0,0 @@
|
||||
<template>
|
||||
<div class="absolute-lt z-1 wh-full overflow-hidden">
|
||||
<div class="absolute -right-300px -top-900px <sm:(-right-100px -top-1170px)">
|
||||
<corner-top :start-color="lightColor" :end-color="darkColor" />
|
||||
</div>
|
||||
<div class="absolute -left-200px -bottom-400px <sm:(-left-100px -bottom-760px)">
|
||||
<corner-bottom :start-color="darkColor" :end-color="lightColor" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { getColorPalette } from '@/utils';
|
||||
import { CornerBottom, CornerTop } from './components';
|
||||
|
||||
interface Props {
|
||||
/** 主题颜色 */
|
||||
themeColor: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const lightColor = computed(() => getColorPalette(props.themeColor, 3));
|
||||
const darkColor = computed(() => getColorPalette(props.themeColor, 6));
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
71
src/views/_builtin/login/components/pwd-login.vue
Normal file
71
src/views/_builtin/login/components/pwd-login.vue
Normal file
@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue';
|
||||
import { $t } from '@/locales';
|
||||
import { loginModuleRecord } from '@/constants/app';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useNaiveForm, useFormRules } from '@/hooks/common/form';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
|
||||
defineOptions({
|
||||
name: 'PwdLogin'
|
||||
});
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const { toggleLoginModule } = useRouterPush();
|
||||
const { formRef, validate } = useNaiveForm();
|
||||
const { constantRules } = useFormRules();
|
||||
|
||||
interface FormModel {
|
||||
userName: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const model: FormModel = reactive({
|
||||
userName: 'Soybean',
|
||||
password: '123456'
|
||||
});
|
||||
|
||||
const rules: Record<keyof FormModel, App.Global.FormRule[]> = {
|
||||
userName: constantRules.userName,
|
||||
password: constantRules.pwd
|
||||
};
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
await authStore.login(model.userName, model.password);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
|
||||
<NFormItem path="userName">
|
||||
<NInput v-model:value="model.userName" :placeholder="$t('page.login.common.userNamePlaceholder')" />
|
||||
</NFormItem>
|
||||
<NFormItem path="password">
|
||||
<NInput
|
||||
v-model:value="model.password"
|
||||
type="password"
|
||||
:placeholder="$t('page.login.common.passwordPlaceholder')"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NSpace :vertical="true" :size="24">
|
||||
<div class="flex-y-center justify-between">
|
||||
<NCheckbox>{{ $t('page.login.pwdLogin.rememberMe') }}</NCheckbox>
|
||||
<NButton quaternary>{{ $t('page.login.pwdLogin.forgetPassword') }}</NButton>
|
||||
</div>
|
||||
<NButton type="primary" size="large" block round :loading="authStore.loginLoading" @click="handleSubmit">
|
||||
{{ $t('common.confirm') }}
|
||||
</NButton>
|
||||
<div class="flex-y-center justify-between gap-12px">
|
||||
<NButton class="flex-1" block @click="toggleLoginModule('code-login')">
|
||||
{{ $t(loginModuleRecord['code-login']) }}
|
||||
</NButton>
|
||||
<NButton class="flex-1" block @click="toggleLoginModule('register')">
|
||||
{{ $t(loginModuleRecord.register) }}
|
||||
</NButton>
|
||||
</div>
|
||||
</NSpace>
|
||||
</NForm>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
@ -1,4 +0,0 @@
|
||||
import OtherLogin from './other-login.vue';
|
||||
import OtherAccount from './other-account.vue';
|
||||
|
||||
export { OtherLogin, OtherAccount };
|
@ -1,60 +0,0 @@
|
||||
<template>
|
||||
<n-space :vertical="true">
|
||||
<n-divider class="!mb-0 text-14px text-#666">{{ $t('page.login.pwdLogin.otherAccountLogin') }}</n-divider>
|
||||
<n-space justify="center">
|
||||
<n-button
|
||||
v-for="item in accounts"
|
||||
:key="item.userName"
|
||||
type="primary"
|
||||
@click="login(item.userName, item.password)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { userRoleLabels } from '@/constants';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
interface Emits {
|
||||
(e: 'login', param: { userName: string; password: string }): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
interface Account {
|
||||
key: Auth.RoleType;
|
||||
label: string;
|
||||
userName: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const accounts: Account[] = [
|
||||
{
|
||||
key: 'super',
|
||||
label: userRoleLabels.super,
|
||||
userName: 'Super',
|
||||
password: 'super123'
|
||||
},
|
||||
{
|
||||
key: 'admin',
|
||||
label: userRoleLabels.admin,
|
||||
userName: 'Admin',
|
||||
password: 'admin123'
|
||||
},
|
||||
{
|
||||
key: 'user',
|
||||
label: userRoleLabels.user,
|
||||
userName: 'User01',
|
||||
password: 'user01123'
|
||||
}
|
||||
];
|
||||
|
||||
function login(userName: string, password: string) {
|
||||
emit('login', { userName, password });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,16 +0,0 @@
|
||||
<template>
|
||||
<n-space :vertical="true">
|
||||
<n-divider class="!mb-0 text-14px text-#666">{{ $t('page.login.pwdLogin.otherLoginMode') }}</n-divider>
|
||||
<div class="flex-center">
|
||||
<n-button :text="true">
|
||||
<icon-mdi-wechat class="text-22px text-#888 hover:text-#52BF5E" />
|
||||
</n-button>
|
||||
</div>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { $t } from '@/locales';
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,85 +0,0 @@
|
||||
<template>
|
||||
<n-form ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
|
||||
<n-form-item path="userName">
|
||||
<n-input v-model:value="model.userName" :placeholder="$t('page.login.common.userNamePlaceholder')" />
|
||||
</n-form-item>
|
||||
<n-form-item path="password">
|
||||
<n-input
|
||||
v-model:value="model.password"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
:placeholder="$t('page.login.common.passwordPlaceholder')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-space :vertical="true" :size="24">
|
||||
<div class="flex-y-center justify-between">
|
||||
<n-checkbox v-model:checked="rememberMe">{{ $t('page.login.pwdLogin.rememberMe') }}</n-checkbox>
|
||||
<n-button :text="true" @click="toLoginModule('reset-pwd')">
|
||||
{{ $t('page.login.pwdLogin.forgetPassword') }}
|
||||
</n-button>
|
||||
</div>
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:block="true"
|
||||
:round="true"
|
||||
:loading="auth.loginLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ $t('page.login.common.confirm') }}
|
||||
</n-button>
|
||||
<div class="flex-y-center justify-between">
|
||||
<n-button class="flex-1" :block="true" @click="toLoginModule('code-login')">
|
||||
{{ loginModuleLabels['code-login'] }}
|
||||
</n-button>
|
||||
<div class="w-12px"></div>
|
||||
<n-button class="flex-1" :block="true" @click="toLoginModule('register')">
|
||||
{{ loginModuleLabels.register }}
|
||||
</n-button>
|
||||
</div>
|
||||
</n-space>
|
||||
<other-account @login="handleLoginOtherAccount" />
|
||||
</n-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue';
|
||||
import type { FormInst, FormRules } from 'naive-ui';
|
||||
import { loginModuleLabels } from '@/constants';
|
||||
import { useAuthStore } from '@/store';
|
||||
import { useRouterPush } from '@/composables';
|
||||
import { formRules } from '@/utils';
|
||||
import { OtherAccount } from './components';
|
||||
|
||||
const auth = useAuthStore();
|
||||
const { login } = useAuthStore();
|
||||
const { toLoginModule } = useRouterPush();
|
||||
|
||||
const formRef = ref<HTMLElement & FormInst>();
|
||||
|
||||
const model = reactive({
|
||||
userName: 'Soybean',
|
||||
password: 'soybean123'
|
||||
});
|
||||
|
||||
const rules: FormRules = {
|
||||
password: formRules.pwd
|
||||
};
|
||||
|
||||
const rememberMe = ref(false);
|
||||
|
||||
async function handleSubmit() {
|
||||
await formRef.value?.validate();
|
||||
|
||||
const { userName, password } = model;
|
||||
|
||||
login(userName, password);
|
||||
}
|
||||
|
||||
function handleLoginOtherAccount(param: { userName: string; password: string }) {
|
||||
const { userName, password } = param;
|
||||
login(userName, password);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,82 +0,0 @@
|
||||
<template>
|
||||
<n-form ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
|
||||
<n-form-item path="phone">
|
||||
<n-input v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
|
||||
</n-form-item>
|
||||
<n-form-item path="code">
|
||||
<div class="flex-y-center w-full">
|
||||
<n-input v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
|
||||
<div class="w-18px"></div>
|
||||
<n-button size="large" :disabled="isCounting" :loading="smsLoading" @click="handleSmsCode">
|
||||
{{ label }}
|
||||
</n-button>
|
||||
</div>
|
||||
</n-form-item>
|
||||
<n-form-item path="pwd">
|
||||
<n-input
|
||||
v-model:value="model.pwd"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
:placeholder="$t('page.login.common.passwordPlaceholder')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item path="confirmPwd">
|
||||
<n-input
|
||||
v-model:value="model.confirmPwd"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
:placeholder="$t('page.login.common.confirmPasswordPlaceholder')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-space :vertical="true" :size="18">
|
||||
<login-agreement v-model:value="agreement" />
|
||||
<n-button type="primary" size="large" :block="true" :round="true" @click="handleSubmit">
|
||||
{{ $t('page.login.common.confirm') }}
|
||||
</n-button>
|
||||
<n-button size="large" :block="true" :round="true" @click="toLoginModule('pwd-login')">
|
||||
{{ $t('page.login.common.back') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, toRefs } from 'vue';
|
||||
import type { FormInst, FormRules } from 'naive-ui';
|
||||
import { useRouterPush } from '@/composables';
|
||||
import { useSmsCode } from '@/hooks';
|
||||
import { formRules, getConfirmPwdRule } from '@/utils';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
const { toLoginModule } = useRouterPush();
|
||||
const { label, isCounting, loading: smsLoading, start } = useSmsCode();
|
||||
|
||||
const formRef = ref<HTMLElement & FormInst>();
|
||||
|
||||
const model = reactive({
|
||||
phone: '',
|
||||
code: '',
|
||||
pwd: '',
|
||||
confirmPwd: ''
|
||||
});
|
||||
|
||||
const rules: FormRules = {
|
||||
phone: formRules.phone,
|
||||
code: formRules.code,
|
||||
pwd: formRules.pwd,
|
||||
confirmPwd: getConfirmPwdRule(toRefs(model).pwd)
|
||||
};
|
||||
|
||||
const agreement = ref(false);
|
||||
|
||||
function handleSmsCode() {
|
||||
start();
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await formRef.value?.validate();
|
||||
window.$message?.success($t('page.login.common.validateSuccess'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
31
src/views/_builtin/login/components/register.vue
Normal file
31
src/views/_builtin/login/components/register.vue
Normal file
@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { $t } from '@/locales';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
|
||||
defineOptions({
|
||||
name: 'CodeLogin'
|
||||
});
|
||||
|
||||
const { toggleLoginModule } = useRouterPush();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NForm size="large" :show-label="false">
|
||||
<NFormItem>
|
||||
<NInput :placeholder="$t('page.login.common.phonePlaceholder')" />
|
||||
</NFormItem>
|
||||
<NFormItem>
|
||||
<NInput :placeholder="$t('page.login.common.codePlaceholder')" />
|
||||
</NFormItem>
|
||||
<NSpace vertical :size="18" class="w-full">
|
||||
<NButton type="primary" size="large" block round>
|
||||
{{ $t('common.confirm') }}
|
||||
</NButton>
|
||||
<NButton size="large" block round @click="toggleLoginModule('pwd-login')">
|
||||
{{ $t('page.login.common.back') }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NForm>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
11
src/views/_builtin/login/components/reset-pwd.vue
Normal file
11
src/views/_builtin/login/components/reset-pwd.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'ResetPwd'
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
@ -1,79 +0,0 @@
|
||||
<template>
|
||||
<n-form ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
|
||||
<n-form-item path="phone">
|
||||
<n-input v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
|
||||
</n-form-item>
|
||||
<n-form-item path="code">
|
||||
<div class="flex-y-center w-full">
|
||||
<n-input v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
|
||||
<div class="w-18px"></div>
|
||||
<n-button size="large" :disabled="isCounting" :loading="smsLoading" @click="handleSmsCode">
|
||||
{{ label }}
|
||||
</n-button>
|
||||
</div>
|
||||
</n-form-item>
|
||||
<n-form-item path="pwd">
|
||||
<n-input
|
||||
v-model:value="model.pwd"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
:placeholder="$t('page.login.common.passwordPlaceholder')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item path="confirmPwd">
|
||||
<n-input
|
||||
v-model:value="model.confirmPwd"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
:placeholder="$t('page.login.common.confirmPasswordPlaceholder')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-space :vertical="true" size="large">
|
||||
<n-button type="primary" size="large" :block="true" :round="true" @click="handleSubmit">
|
||||
{{ $t('page.login.common.confirm') }}
|
||||
</n-button>
|
||||
<n-button size="large" :block="true" :round="true" @click="toLoginModule('pwd-login')">
|
||||
{{ $t('page.login.common.back') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, toRefs } from 'vue';
|
||||
import type { FormInst, FormRules } from 'naive-ui';
|
||||
import { useRouterPush } from '@/composables';
|
||||
import { useSmsCode } from '@/hooks';
|
||||
import { formRules, getConfirmPwdRule } from '@/utils';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
const { toLoginModule } = useRouterPush();
|
||||
const { label, isCounting, loading: smsLoading, start } = useSmsCode();
|
||||
|
||||
const formRef = ref<HTMLElement & FormInst>();
|
||||
|
||||
const model = reactive({
|
||||
phone: '',
|
||||
code: '',
|
||||
pwd: '',
|
||||
confirmPwd: ''
|
||||
});
|
||||
|
||||
const rules: FormRules = {
|
||||
phone: formRules.phone,
|
||||
code: formRules.code,
|
||||
pwd: formRules.pwd,
|
||||
confirmPwd: getConfirmPwdRule(toRefs(model).pwd)
|
||||
};
|
||||
|
||||
function handleSmsCode() {
|
||||
start();
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await formRef.value?.validate();
|
||||
window.$message?.success($t('page.login.common.validateSuccess'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,47 +1,30 @@
|
||||
<template>
|
||||
<div class="relative flex-center wh-full" :style="{ backgroundColor: bgColor }">
|
||||
<dark-mode-switch
|
||||
:dark="theme.darkMode"
|
||||
class="absolute left-48px top-24px z-3 text-20px"
|
||||
@update:dark="theme.setDarkMode"
|
||||
/>
|
||||
<n-card :bordered="false" size="large" class="z-4 !w-auto rounded-20px shadow-sm">
|
||||
<div class="w-300px sm:w-360px">
|
||||
<header class="flex-y-center justify-between">
|
||||
<system-logo class="text-64px text-primary" />
|
||||
<n-gradient-text type="primary" :size="28">{{ $t('system.title') }}</n-gradient-text>
|
||||
</header>
|
||||
<main class="pt-24px">
|
||||
<h3 class="text-18px text-primary font-medium">{{ activeModule.label }}</h3>
|
||||
<div class="pt-24px">
|
||||
<transition name="fade-slide" mode="out-in" appear>
|
||||
<component :is="activeModule.component" />
|
||||
</transition>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</n-card>
|
||||
<login-bg :theme-color="bgThemeColor" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { Component } from 'vue';
|
||||
import { loginModuleLabels } from '@/constants';
|
||||
import { useThemeStore } from '@/store';
|
||||
import { getColorPalette, mixColor } from '@/utils';
|
||||
import { getColorPalette, mixColor } from '@sa/utils';
|
||||
import { $t } from '@/locales';
|
||||
import { BindWechat, CodeLogin, LoginBg, PwdLogin, Register, ResetPwd } from './components';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { loginModuleRecord } from '@/constants/app';
|
||||
import PwdLogin from './components/pwd-login.vue';
|
||||
import CodeLogin from './components/code-login.vue';
|
||||
import Register from './components/register.vue';
|
||||
import ResetPwd from './components/reset-pwd.vue';
|
||||
import BindWechat from './components/bind-wechat.vue';
|
||||
|
||||
interface Props {
|
||||
/** 登录模块分类 */
|
||||
module: UnionKey.LoginModule;
|
||||
/**
|
||||
* the login module
|
||||
*/
|
||||
module?: UnionKey.LoginModule;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
module: 'pwd-login'
|
||||
});
|
||||
|
||||
const theme = useThemeStore();
|
||||
const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
interface LoginModule {
|
||||
key: UnionKey.LoginModule;
|
||||
@ -50,29 +33,65 @@ interface LoginModule {
|
||||
}
|
||||
|
||||
const modules: LoginModule[] = [
|
||||
{ key: 'pwd-login', label: loginModuleLabels['pwd-login'], component: PwdLogin },
|
||||
{ key: 'code-login', label: loginModuleLabels['code-login'], component: CodeLogin },
|
||||
{ key: 'register', label: loginModuleLabels.register, component: Register },
|
||||
{ key: 'reset-pwd', label: loginModuleLabels['reset-pwd'], component: ResetPwd },
|
||||
{ key: 'bind-wechat', label: loginModuleLabels['bind-wechat'], component: BindWechat }
|
||||
{ key: 'pwd-login', label: loginModuleRecord['pwd-login'], component: PwdLogin },
|
||||
{ key: 'code-login', label: loginModuleRecord['code-login'], component: CodeLogin },
|
||||
{ key: 'register', label: loginModuleRecord.register, component: Register },
|
||||
{ key: 'reset-pwd', label: loginModuleRecord['reset-pwd'], component: ResetPwd },
|
||||
{ key: 'bind-wechat', label: loginModuleRecord['bind-wechat'], component: BindWechat }
|
||||
];
|
||||
|
||||
const activeModule = computed(() => {
|
||||
const active: LoginModule = { ...modules[0] };
|
||||
const findItem = modules.find(item => item.key === props.module);
|
||||
if (findItem) {
|
||||
Object.assign(active, findItem);
|
||||
}
|
||||
return active;
|
||||
return findItem || modules[0];
|
||||
});
|
||||
|
||||
const bgThemeColor = computed(() => (theme.darkMode ? getColorPalette(theme.themeColor, 7) : theme.themeColor));
|
||||
const bgThemeColor = computed(() =>
|
||||
themeStore.darkMode ? getColorPalette(themeStore.themeColor, 7) : themeStore.themeColor
|
||||
);
|
||||
|
||||
const bgColor = computed(() => {
|
||||
const COLOR_WHITE = '#ffffff';
|
||||
const ratio = theme.darkMode ? 0.5 : 0.2;
|
||||
return mixColor(COLOR_WHITE, theme.themeColor, ratio);
|
||||
|
||||
const ratio = themeStore.darkMode ? 0.5 : 0.2;
|
||||
|
||||
return mixColor(COLOR_WHITE, themeStore.themeColor, ratio);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative flex-center wh-full overflow-hidden" :style="{ backgroundColor: bgColor }">
|
||||
<WaveBg :theme-color="bgThemeColor" />
|
||||
<NCard class="relative w-auto rd-12px z-4">
|
||||
<div class="w-400px <sm:w-300px">
|
||||
<header class="flex-y-center justify-between">
|
||||
<SystemLogo class="text-64px text-primary <sm:text-48px" />
|
||||
<h3 class="text-28px font-500 text-primary <sm:text-22px">{{ $t('system.title') }}</h3>
|
||||
<div class="i-flex-vertical">
|
||||
<ThemeSchemaSwitch
|
||||
:theme-schema="themeStore.themeScheme"
|
||||
:show-tooltip="false"
|
||||
class="text-20px <sm:text-18px"
|
||||
@switch="themeStore.toggleThemeScheme"
|
||||
/>
|
||||
<LangSwitch
|
||||
:lang="appStore.locale"
|
||||
:lang-options="appStore.localeOptions"
|
||||
:show-tooltip="false"
|
||||
@change-lang="appStore.changeLocale"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<main class="pt-24px">
|
||||
<h3 class="text-18px text-primary font-medium">{{ $t(activeModule.label) }}</h3>
|
||||
<div class="pt-24px">
|
||||
<Transition :name="themeStore.page.animateMode" mode="out-in" appear>
|
||||
<component :is="activeModule.component" />
|
||||
</Transition>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</NCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
@ -1,7 +0,0 @@
|
||||
<template>
|
||||
<exception-base type="404" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<style scoped></style>
|
@ -1,19 +0,0 @@
|
||||
<template>
|
||||
<n-card title="开发环境依赖" :bordered="false" size="small" class="rounded-8px shadow-sm">
|
||||
<n-descriptions label-placement="left" bordered size="small">
|
||||
<n-descriptions-item v-for="item in devDependencies" :key="item.name" :label="item.name">
|
||||
{{ item.version }}
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { pkgJson } from './model';
|
||||
|
||||
defineOptions({ name: 'DevDependency' });
|
||||
|
||||
const { devDependencies } = pkgJson;
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,6 +0,0 @@
|
||||
import ProjectIntroduction from './project-introduction.vue';
|
||||
import ProjectInfo from './project-info.vue';
|
||||
import ProDependency from './pro-dependency.vue';
|
||||
import DevDependency from './dev-dependency.vue';
|
||||
|
||||
export { ProjectIntroduction, ProjectInfo, ProDependency, DevDependency };
|
@ -1,39 +0,0 @@
|
||||
import pkg from '~/package.json';
|
||||
|
||||
/** npm依赖包版本信息 */
|
||||
export interface PkgVersionInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
interface Package {
|
||||
name: string;
|
||||
version: string;
|
||||
dependencies: Record<string, string>;
|
||||
devDependencies: Record<string, string>;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface PkgJson {
|
||||
name: string;
|
||||
version: string;
|
||||
dependencies: PkgVersionInfo[];
|
||||
devDependencies: PkgVersionInfo[];
|
||||
}
|
||||
|
||||
const pkgWithType = pkg as Package;
|
||||
|
||||
function transformVersionData(tuple: [string, string]): PkgVersionInfo {
|
||||
const [name, version] = tuple;
|
||||
return {
|
||||
name,
|
||||
version
|
||||
};
|
||||
}
|
||||
|
||||
export const pkgJson: PkgJson = {
|
||||
name: pkgWithType.name,
|
||||
version: pkgWithType.version,
|
||||
dependencies: Object.entries(pkgWithType.dependencies).map(item => transformVersionData(item)),
|
||||
devDependencies: Object.entries(pkgWithType.devDependencies).map(item => transformVersionData(item))
|
||||
};
|
@ -1,19 +0,0 @@
|
||||
<template>
|
||||
<n-card title="生产环境依赖" :bordered="false" size="small" class="rounded-8px shadow-sm">
|
||||
<n-descriptions label-placement="left" bordered size="small">
|
||||
<n-descriptions-item v-for="item in dependencies" :key="item.name" :label="item.name">
|
||||
{{ item.version }}
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { pkgJson } from './model';
|
||||
|
||||
defineOptions({ name: 'ProDependency' });
|
||||
|
||||
const { dependencies } = pkgJson;
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,29 +0,0 @@
|
||||
<template>
|
||||
<n-card title="项目信息" :bordered="false" size="small" class="rounded-8px shadow-sm">
|
||||
<n-descriptions label-placement="left" bordered size="small" :column="2">
|
||||
<n-descriptions-item label="版本">
|
||||
<n-tag type="primary">{{ version }}</n-tag>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="最后编译时间">
|
||||
<n-tag type="primary">{{ latestBuildTime }}</n-tag>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="Github地址">
|
||||
<a class="text-primary" href="https://github.com/honghuangdc/soybean-admin" target="_blank">Github地址</a>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="预览地址">
|
||||
<a class="text-primary" href="https://admin.soybeanjs.cn" target="_blank">预览地址</a>
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { pkgJson } from './model';
|
||||
|
||||
defineOptions({ name: 'ProjectInfo' });
|
||||
|
||||
const { version } = pkgJson;
|
||||
const latestBuildTime = PROJECT_BUILD_TIME;
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,14 +0,0 @@
|
||||
<template>
|
||||
<n-card title="关于" :bordered="false" size="large" class="rounded-8px shadow-sm">
|
||||
<p class="leading-24px">
|
||||
Soybean Admin 是一个基于 Vue3、Vite、Naive UI、TypeScript
|
||||
的中后台解决方案,它使用了最新的前端技术栈,并提炼了典型的业务模型,页面,包括二次封装组件、动态菜单、权限校验、粒子化权限控制等功能,它可以帮助你快速搭建企业级中后台项目,相信不管是从新技术使用还是其他方面,都能帮助到你。
|
||||
</p>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: 'ProjectIntroduction' });
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,23 +0,0 @@
|
||||
<template>
|
||||
<n-space :vertical="true" :size="16">
|
||||
<project-introduction />
|
||||
<project-info />
|
||||
<pro-dependency />
|
||||
<dev-dependency />
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onActivated, onMounted } from 'vue';
|
||||
import { DevDependency, ProDependency, ProjectInfo, ProjectIntroduction } from './components';
|
||||
|
||||
onActivated(() => {
|
||||
console.log('about page activated');
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
console.log('about page mounted');
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,55 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<n-card title="权限切换" :bordered="false" class="h-full rounded-8px shadow-sm">
|
||||
<div class="pb-12px">
|
||||
<n-gradient-text type="primary" :size="20">当前用户的权限:{{ auth.userInfo.userRole }}</n-gradient-text>
|
||||
</div>
|
||||
<n-select
|
||||
:value="auth.userInfo.userRole"
|
||||
class="w-120px"
|
||||
size="small"
|
||||
:options="options"
|
||||
@update:value="auth.updateUserRole"
|
||||
/>
|
||||
<div class="py-12px">
|
||||
<n-gradient-text type="primary" :size="20">权限指令 v-permission</n-gradient-text>
|
||||
</div>
|
||||
<div>
|
||||
<n-button v-permission="'super'" class="mr-12px">super可见</n-button>
|
||||
<n-button v-permission="'admin'" class="mr-12px">admin可见</n-button>
|
||||
<n-button v-permission="['admin', 'user']">admin和test可见</n-button>
|
||||
</div>
|
||||
<div class="py-12px">
|
||||
<n-gradient-text type="primary" :size="20">权限函数 hasPermission</n-gradient-text>
|
||||
</div>
|
||||
<n-space>
|
||||
<n-button v-if="hasPermission('super')">super可见</n-button>
|
||||
<n-button v-if="hasPermission('admin')">admin可见</n-button>
|
||||
<n-button v-if="hasPermission(['admin', 'user'])">admin和user可见</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch } from 'vue';
|
||||
import type { SelectOption } from 'naive-ui';
|
||||
import { userRoleOptions } from '@/constants';
|
||||
import { useAppStore, useAuthStore } from '@/store';
|
||||
import { usePermission } from '@/composables';
|
||||
|
||||
const app = useAppStore();
|
||||
const auth = useAuthStore();
|
||||
const { hasPermission } = usePermission();
|
||||
|
||||
const options: SelectOption[] = userRoleOptions;
|
||||
|
||||
watch(
|
||||
() => auth.userInfo.userRole,
|
||||
async () => {
|
||||
app.reloadPage();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,9 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<n-card title="当前页面只有super才能看到" :bordered="false" class="h-full rounded-8px shadow-sm"></n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped></style>
|
@ -1,575 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-card title="按钮" :bordered="false" class="h-full rounded-8px shadow-sm">
|
||||
<n-grid cols="s:1 m:2" responsive="screen" :x-gap="16" :y-gap="16">
|
||||
<n-grid-item v-for="item in buttonExample" :key="item.id">
|
||||
<n-card :title="item.label" class="min-h-180px">
|
||||
<p v-if="item.desc" class="pb-16px">{{ item.desc }}</p>
|
||||
<n-space>
|
||||
<n-button
|
||||
v-for="button in item.buttons"
|
||||
:key="button.id"
|
||||
v-bind="button.props"
|
||||
:style="`--icon-margin: ${button.props.circle ? 0 : 6}px`"
|
||||
>
|
||||
<template v-if="button.icon" #icon>
|
||||
<svg-icon :icon="button.icon" />
|
||||
</template>
|
||||
{{ button.label }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-grid-item>
|
||||
<n-grid-item class="h-180px">
|
||||
<n-card title="加载中" class="h-full">
|
||||
<p class="pb-16px">按钮有加载状态。</p>
|
||||
<n-space>
|
||||
<n-button :loading="loading" type="primary" @click="startLoading">开始加载</n-button>
|
||||
<n-button @click="endLoading">取消加载</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ButtonProps } from 'naive-ui';
|
||||
import { useLoading } from '@/hooks';
|
||||
|
||||
interface ButtonDetail {
|
||||
id: number;
|
||||
props: ButtonProps & { href?: string; target?: string };
|
||||
label?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
interface ButtonExample {
|
||||
id: number;
|
||||
label: string;
|
||||
buttons: ButtonDetail[];
|
||||
desc?: string;
|
||||
}
|
||||
|
||||
const { loading, startLoading, endLoading } = useLoading();
|
||||
|
||||
const buttonExample: ButtonExample[] = [
|
||||
{
|
||||
id: 0,
|
||||
label: '基础',
|
||||
buttons: [
|
||||
{
|
||||
id: 0,
|
||||
props: {},
|
||||
label: 'Default'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
props: { type: 'tertiary' },
|
||||
label: 'Tertiary'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
props: { type: 'primary' },
|
||||
label: 'Primary'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
props: { type: 'info' },
|
||||
label: 'Info'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
props: { type: 'success' },
|
||||
label: 'Success'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
props: { type: 'warning' },
|
||||
label: 'Warning'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
props: { type: 'error' },
|
||||
label: 'Error'
|
||||
}
|
||||
],
|
||||
desc: '按钮的 type 分别为 default、primary、info、success、warning 和 error。'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
label: '次要按钮',
|
||||
buttons: [
|
||||
{
|
||||
id: 0,
|
||||
props: { strong: true, secondary: true },
|
||||
label: 'Default'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
props: { strong: true, secondary: true, type: 'tertiary' },
|
||||
label: 'Tertiary'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
props: { strong: true, secondary: true, type: 'primary' },
|
||||
label: 'Primary'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
props: { strong: true, secondary: true, type: 'info' },
|
||||
label: 'Info'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
props: { strong: true, secondary: true, type: 'success' },
|
||||
label: 'Success'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
props: { strong: true, secondary: true, type: 'warning' },
|
||||
label: 'Warning'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
props: { strong: true, secondary: true, type: 'error' },
|
||||
label: 'Error'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
props: { strong: true, secondary: true, round: true },
|
||||
label: 'Default'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
props: { strong: true, secondary: true, round: true, type: 'tertiary' },
|
||||
label: 'Tertiary'
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
props: { strong: true, secondary: true, round: true, type: 'primary' },
|
||||
label: 'Primary'
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
props: { strong: true, secondary: true, round: true, type: 'info' },
|
||||
label: 'Info'
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
props: { strong: true, secondary: true, round: true, type: 'success' },
|
||||
label: 'Success'
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
props: { strong: true, secondary: true, round: true, type: 'warning' },
|
||||
label: 'Warning'
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
props: { strong: true, secondary: true, round: true, type: 'error' },
|
||||
label: 'Error'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: '次次要按钮',
|
||||
buttons: [
|
||||
{
|
||||
id: 0,
|
||||
props: { tertiary: true },
|
||||
label: 'Default'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
props: { tertiary: true, type: 'primary' },
|
||||
label: 'Primary'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
props: { tertiary: true, type: 'info' },
|
||||
label: 'Info'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
props: { tertiary: true, type: 'success' },
|
||||
label: 'Success'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
props: { tertiary: true, type: 'warning' },
|
||||
label: 'Warning'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
props: { tertiary: true, type: 'error' },
|
||||
label: 'Error'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
props: { tertiary: true, round: true },
|
||||
label: 'Default'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
props: { tertiary: true, round: true, type: 'primary' },
|
||||
label: 'Primary'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
props: { tertiary: true, round: true, type: 'info' },
|
||||
label: 'Info'
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
props: { tertiary: true, round: true, type: 'success' },
|
||||
label: 'Success'
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
props: { tertiary: true, round: true, type: 'warning' },
|
||||
label: 'Warning'
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
props: { tertiary: true, round: true, type: 'error' },
|
||||
label: 'Error'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: '次次次要按钮',
|
||||
buttons: [
|
||||
{
|
||||
id: 0,
|
||||
props: { quaternary: true },
|
||||
label: 'Default'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
props: { quaternary: true, type: 'primary' },
|
||||
label: 'Primary'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
props: { quaternary: true, type: 'info' },
|
||||
label: 'Info'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
props: { quaternary: true, type: 'success' },
|
||||
label: 'Success'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
props: { quaternary: true, type: 'warning' },
|
||||
label: 'Warning'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
props: { quaternary: true, type: 'error' },
|
||||
label: 'Error'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
props: { quaternary: true, round: true },
|
||||
label: 'Default'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
props: { quaternary: true, round: true, type: 'primary' },
|
||||
label: 'Primary'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
props: { quaternary: true, round: true, type: 'info' },
|
||||
label: 'Info'
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
props: { quaternary: true, round: true, type: 'success' },
|
||||
label: 'Success'
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
props: { quaternary: true, round: true, type: 'warning' },
|
||||
label: 'Warning'
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
props: { quaternary: true, round: true, type: 'error' },
|
||||
label: 'Error'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
label: '虚线按钮',
|
||||
buttons: [
|
||||
{
|
||||
id: 0,
|
||||
props: { dashed: true },
|
||||
label: 'Default'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
props: { dashed: true, type: 'tertiary' },
|
||||
label: 'Tertiary'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
props: { dashed: true, type: 'primary' },
|
||||
label: 'Primary'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
props: { dashed: true, type: 'info' },
|
||||
label: 'Info'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
props: { dashed: true, type: 'success' },
|
||||
label: 'Success'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
props: { dashed: true, type: 'warning' },
|
||||
label: 'Warning'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
props: { dashed: true, type: 'error' },
|
||||
label: 'Error'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
label: '尺寸',
|
||||
buttons: [
|
||||
{
|
||||
id: 0,
|
||||
props: { size: 'tiny', strong: true },
|
||||
label: '小小'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
props: { size: 'small', strong: true },
|
||||
label: '小'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
props: { size: 'medium', strong: true },
|
||||
label: '不小'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
props: { size: 'large', strong: true },
|
||||
label: '不不小'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
label: '文本按钮',
|
||||
buttons: [
|
||||
{
|
||||
id: 0,
|
||||
props: { text: true },
|
||||
label: '那车头依然吐着烟',
|
||||
icon: 'mdi:train'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
label: '自定义标签按钮',
|
||||
buttons: [
|
||||
{
|
||||
id: 0,
|
||||
props: {
|
||||
text: true,
|
||||
tag: 'a',
|
||||
href: 'https://github.com/honghuangdc/soybean-admin',
|
||||
target: '_blank',
|
||||
type: 'primary'
|
||||
},
|
||||
label: 'soybean-admin'
|
||||
}
|
||||
],
|
||||
desc: '你可以把按钮渲染成不同的标签,比如 a标签 。'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
label: '按钮禁用',
|
||||
buttons: [
|
||||
{
|
||||
id: 0,
|
||||
props: {
|
||||
disabled: true
|
||||
},
|
||||
label: '不许点'
|
||||
}
|
||||
],
|
||||
desc: '按钮可以被禁用'
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
label: '图标按钮',
|
||||
buttons: [
|
||||
{
|
||||
id: 0,
|
||||
props: {
|
||||
secondary: true,
|
||||
strong: true
|
||||
},
|
||||
label: '+100元',
|
||||
icon: 'mdi:cash-100'
|
||||
},
|
||||
{
|
||||
id: 0,
|
||||
props: {
|
||||
iconPlacement: 'right',
|
||||
secondary: true,
|
||||
strong: true
|
||||
},
|
||||
label: '+100元',
|
||||
icon: 'mdi:cash-100'
|
||||
}
|
||||
],
|
||||
desc: '在按钮上使用图标。'
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
label: '不同形状按钮',
|
||||
buttons: [
|
||||
{
|
||||
id: 0,
|
||||
props: {
|
||||
circle: true
|
||||
},
|
||||
icon: 'mdi:cash-100'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
props: {
|
||||
round: true
|
||||
},
|
||||
label: '圆角'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
props: {},
|
||||
label: '方'
|
||||
}
|
||||
],
|
||||
desc: '按钮拥有不同的形状。'
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
label: '透明背景按钮',
|
||||
buttons: [
|
||||
{
|
||||
id: 0,
|
||||
props: { ghost: true },
|
||||
label: 'Default'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
props: { ghost: true, type: 'tertiary' },
|
||||
label: 'Tertiary'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
props: { ghost: true, type: 'primary' },
|
||||
label: 'Primary'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
props: { ghost: true, type: 'info' },
|
||||
label: 'Info'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
props: { ghost: true, type: 'success' },
|
||||
label: 'Success'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
props: { ghost: true, type: 'warning' },
|
||||
label: 'Warning'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
props: { ghost: true, type: 'error' },
|
||||
label: 'Error'
|
||||
}
|
||||
],
|
||||
desc: 'Ghost 按钮有透明的背景。'
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
label: '自定义颜色',
|
||||
buttons: [
|
||||
{
|
||||
id: 0,
|
||||
props: {
|
||||
color: '#8a2be2'
|
||||
},
|
||||
label: '#8a2be2',
|
||||
icon: 'ic:baseline-color-lens'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
props: {
|
||||
color: '#ff69b4'
|
||||
},
|
||||
label: '#ff69b4',
|
||||
icon: 'ic:baseline-color-lens'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
props: {
|
||||
color: '#8a2be2',
|
||||
ghost: true
|
||||
},
|
||||
label: '#8a2be2',
|
||||
icon: 'ic:baseline-color-lens'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
props: {
|
||||
color: '#ff69b4',
|
||||
ghost: true
|
||||
},
|
||||
label: '#ff69b4',
|
||||
icon: 'ic:baseline-color-lens'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
props: {
|
||||
color: '#8a2be2',
|
||||
text: true
|
||||
},
|
||||
label: '#8a2be2',
|
||||
icon: 'ic:baseline-color-lens'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
props: {
|
||||
color: '#ff69b4',
|
||||
text: true
|
||||
},
|
||||
label: '#ff69b4',
|
||||
icon: 'ic:baseline-color-lens'
|
||||
}
|
||||
],
|
||||
desc: '这两个颜色看起来像毒蘑菇。'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,42 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-card title="卡片" :bordered="false" class="h-full rounded-8px shadow-sm">
|
||||
<n-space :vertical="true">
|
||||
<n-card title="基本用法">
|
||||
<p class="pb-16px">基础卡片</p>
|
||||
<n-card title="卡片">卡片内容</n-card>
|
||||
</n-card>
|
||||
<n-card title="尺寸">
|
||||
<p class="pb-16px">卡片有 small、medium、large、huge 尺寸。</p>
|
||||
<n-space vertical>
|
||||
<n-card title="小卡片" size="small">卡片内容</n-card>
|
||||
<n-card title="中卡片" size="medium">卡片内容</n-card>
|
||||
<n-card title="大卡片" size="large">卡片内容</n-card>
|
||||
<n-card title="超大卡片" size="huge">卡片内容</n-card>
|
||||
</n-space>
|
||||
</n-card>
|
||||
<n-card title="文本按钮">
|
||||
<p class="pb-16px">
|
||||
content 和 footer 可以被 hard 或 soft 分段,action 可以被分段。分段分割线会在区域的上方出现。
|
||||
</p>
|
||||
<n-card
|
||||
title="卡片分段示例"
|
||||
:segmented="{
|
||||
content: true,
|
||||
footer: 'soft'
|
||||
}"
|
||||
>
|
||||
<template #header-extra>#header-extra</template>
|
||||
卡片内容
|
||||
<template #footer>#footer</template>
|
||||
<template #action>#action</template>
|
||||
</n-card>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped></style>
|
@ -1,124 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full overflow-hidden">
|
||||
<n-card title="表格" :bordered="false" class="h-full rounded-8px shadow-sm">
|
||||
<n-space :vertical="true">
|
||||
<n-space>
|
||||
<n-button @click="getDataSource">有数据</n-button>
|
||||
<n-button @click="getEmptyDataSource">空数据</n-button>
|
||||
</n-space>
|
||||
<loading-empty-wrapper class="h-480px" :loading="loading" :empty="empty">
|
||||
<n-data-table :columns="columns" :data="dataSource" :flex-height="true" class="h-480px" />
|
||||
</loading-empty-wrapper>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { NSpace, NButton, NPopconfirm } from 'naive-ui';
|
||||
import type { DataTableColumn } from 'naive-ui';
|
||||
import { useLoadingEmpty } from '@/hooks';
|
||||
import { getRandomInteger } from '@/utils';
|
||||
|
||||
interface DataSource {
|
||||
name: string;
|
||||
age: number;
|
||||
address: string;
|
||||
}
|
||||
|
||||
const { loading, startLoading, endLoading, empty, setEmpty } = useLoadingEmpty();
|
||||
|
||||
const columns: DataTableColumn<DataSource>[] = [
|
||||
{
|
||||
title: 'Name',
|
||||
key: 'name',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: 'Age',
|
||||
key: 'age',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: 'Address',
|
||||
key: 'address',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
title: 'Action',
|
||||
align: 'center',
|
||||
render: row => {
|
||||
return (
|
||||
<NSpace justify={'center'}>
|
||||
<NButton
|
||||
size={'small'}
|
||||
onClick={() => {
|
||||
handleEdit(row.name);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</NButton>
|
||||
<NPopconfirm
|
||||
onPositiveClick={() => {
|
||||
handleDelete(row.name);
|
||||
}}
|
||||
>
|
||||
{{
|
||||
default: () => '确认删除',
|
||||
trigger: () => <NButton size={'small'}>删除</NButton>
|
||||
}}
|
||||
</NPopconfirm>
|
||||
</NSpace>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const dataSource = ref<DataSource[]>([]);
|
||||
|
||||
function createDataSource(): DataSource[] {
|
||||
return Array(100)
|
||||
.fill(1)
|
||||
.map((_item, index) => {
|
||||
return {
|
||||
name: `Name${index}`,
|
||||
age: getRandomInteger(30, 20),
|
||||
address: '中国'
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getDataSource() {
|
||||
startLoading();
|
||||
setTimeout(() => {
|
||||
dataSource.value = createDataSource();
|
||||
endLoading();
|
||||
setEmpty(!dataSource.value.length);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function getEmptyDataSource() {
|
||||
startLoading();
|
||||
setTimeout(() => {
|
||||
dataSource.value = [];
|
||||
endLoading();
|
||||
setEmpty(!dataSource.value.length);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function handleEdit(_name: string) {
|
||||
//
|
||||
}
|
||||
|
||||
function handleDelete(_name: string) {
|
||||
//
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getDataSource();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,136 +0,0 @@
|
||||
<template>
|
||||
<n-grid :x-gap="16" :y-gap="16" :item-responsive="true">
|
||||
<n-grid-item span="0:24 640:24 1024:8">
|
||||
<n-card title="时间线" :bordered="false" class="h-full rounded-8px shadow-sm">
|
||||
<n-timeline>
|
||||
<n-timeline-item v-for="item in timelines" :key="item.type" v-bind="item" />
|
||||
</n-timeline>
|
||||
</n-card>
|
||||
</n-grid-item>
|
||||
<n-grid-item span="0:24 640:24 1024:16">
|
||||
<n-card title="表格" :bordered="false" class="h-full rounded-8px shadow-sm">
|
||||
<n-data-table size="small" :columns="columns" :data="tableData" />
|
||||
</n-card>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue';
|
||||
import { NTag } from 'naive-ui';
|
||||
|
||||
defineOptions({ name: 'DashboardAnalysisBottomPart' });
|
||||
|
||||
interface TimelineData {
|
||||
type: 'default' | 'info' | 'success' | 'warning' | 'error';
|
||||
title: string;
|
||||
content: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
interface TableData {
|
||||
key: number;
|
||||
name: string;
|
||||
age: number;
|
||||
address: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
const timelines: TimelineData[] = [
|
||||
{ type: 'default', title: '啊', content: '', time: '2021-10-10 20:46' },
|
||||
{ type: 'success', title: '成功', content: '哪里成功', time: '2021-10-10 20:46' },
|
||||
{ type: 'error', title: '错误', content: '哪里错误', time: '2021-10-10 20:46' },
|
||||
{ type: 'warning', title: '警告', content: '哪里警告', time: '2021-10-10 20:46' },
|
||||
{ type: 'info', title: '信息', content: '是的', time: '2021-10-10 20:46' }
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'Name',
|
||||
key: 'name'
|
||||
},
|
||||
{
|
||||
title: 'Age',
|
||||
key: 'age'
|
||||
},
|
||||
{
|
||||
title: 'Address',
|
||||
key: 'address'
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
key: 'tags',
|
||||
render(row: TableData) {
|
||||
const tags = row.tags.map(tagKey => {
|
||||
return h(
|
||||
NTag,
|
||||
{
|
||||
style: {
|
||||
marginRight: '6px'
|
||||
},
|
||||
type: 'info'
|
||||
},
|
||||
{
|
||||
default: () => tagKey
|
||||
}
|
||||
);
|
||||
});
|
||||
return tags;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const tableData: TableData[] = [
|
||||
{
|
||||
key: 0,
|
||||
name: 'John Brown',
|
||||
age: 32,
|
||||
address: 'New York No. 1 Lake Park',
|
||||
tags: ['nice', 'developer']
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
name: 'Jim Green',
|
||||
age: 42,
|
||||
address: 'London No. 1 Lake Park',
|
||||
tags: ['wow']
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
name: 'Joe Black',
|
||||
age: 32,
|
||||
address: 'Sidney No. 1 Lake Park',
|
||||
tags: ['cool', 'teacher']
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
name: 'Soybean',
|
||||
age: 25,
|
||||
address: 'China Shenzhen',
|
||||
tags: ['handsome', 'programmer']
|
||||
},
|
||||
{
|
||||
key: 4,
|
||||
name: 'John Brown',
|
||||
age: 32,
|
||||
address: 'New York No. 1 Lake Park',
|
||||
tags: ['nice', 'developer']
|
||||
},
|
||||
{
|
||||
key: 5,
|
||||
name: 'Jim Green',
|
||||
age: 42,
|
||||
address: 'London No. 1 Lake Park',
|
||||
tags: ['wow']
|
||||
},
|
||||
{
|
||||
key: 6,
|
||||
name: 'Joe Black',
|
||||
age: 32,
|
||||
address: 'Sidney No. 1 Lake Park',
|
||||
tags: ['cool', 'teacher']
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,25 +0,0 @@
|
||||
<template>
|
||||
<div class="p-16px rounded-8px text-white" :style="{ backgroundImage: gradientStyle }">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
/** 渐变开始的颜色 */
|
||||
startColor?: string;
|
||||
/** 渐变结束的颜色 */
|
||||
endColor?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
startColor: '#56cdf3',
|
||||
endColor: '#719de3'
|
||||
});
|
||||
|
||||
const gradientStyle = computed(() => `linear-gradient(to bottom right, ${props.startColor}, ${props.endColor})`);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,3 +0,0 @@
|
||||
import GradientBg from './gradient-bg.vue';
|
||||
|
||||
export { GradientBg };
|
@ -1,72 +0,0 @@
|
||||
<template>
|
||||
<n-card :bordered="false" class="h-full rounded-8px shadow-sm">
|
||||
<n-grid cols="s:1 m:2 l:4" responsive="screen" :x-gap="16" :y-gap="16">
|
||||
<n-grid-item v-for="item in cardData" :key="item.id">
|
||||
<gradient-bg class="h-100px" :start-color="item.colors[0]" :end-color="item.colors[1]">
|
||||
<h3 class="text-16px">{{ item.title }}</h3>
|
||||
<div class="flex justify-between pt-12px">
|
||||
<svg-icon :icon="item.icon" class="text-32px" />
|
||||
<count-to
|
||||
:prefix="item.unit"
|
||||
:start-value="1"
|
||||
:end-value="item.value"
|
||||
class="text-30px text-white dark:text-dark"
|
||||
/>
|
||||
</div>
|
||||
</gradient-bg>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { GradientBg } from './components';
|
||||
|
||||
defineOptions({ name: 'DashboardAnalysisDataCard' });
|
||||
|
||||
interface CardData {
|
||||
id: string;
|
||||
title: string;
|
||||
value: number;
|
||||
unit: string;
|
||||
colors: [string, string];
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const cardData: CardData[] = [
|
||||
{
|
||||
id: 'visit',
|
||||
title: '访问量',
|
||||
value: 1000000,
|
||||
unit: '',
|
||||
colors: ['#ec4786', '#b955a4'],
|
||||
icon: 'ant-design:bar-chart-outlined'
|
||||
},
|
||||
{
|
||||
id: 'amount',
|
||||
title: '成交额',
|
||||
value: 234567.89,
|
||||
unit: '$',
|
||||
colors: ['#865ec0', '#5144b4'],
|
||||
icon: 'ant-design:money-collect-outlined'
|
||||
},
|
||||
{
|
||||
id: 'download',
|
||||
title: '下载数',
|
||||
value: 666666,
|
||||
unit: '',
|
||||
colors: ['#56cdf3', '#719de3'],
|
||||
icon: 'carbon:document-download'
|
||||
},
|
||||
{
|
||||
id: 'trade',
|
||||
title: '成交数',
|
||||
value: 999999,
|
||||
unit: '',
|
||||
colors: ['#fcbc25', '#f68057'],
|
||||
icon: 'ant-design:trademark-circle-outlined'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,5 +0,0 @@
|
||||
import TopChart from './top-chart/index.vue';
|
||||
import DataCard from './data-card/index.vue';
|
||||
import BottomPart from './bottom-part/index.vue';
|
||||
|
||||
export { TopChart, DataCard, BottomPart };
|
@ -1,184 +0,0 @@
|
||||
<template>
|
||||
<n-grid :x-gap="16" :y-gap="16" :item-responsive="true">
|
||||
<n-grid-item span="0:24 640:24 1024:6">
|
||||
<n-card :bordered="false" class="rounded-8px shadow-sm">
|
||||
<div class="w-full h-360px py-12px">
|
||||
<h3 class="text-16px font-bold">Dashboard</h3>
|
||||
<p class="text-#aaa">Overview Of Lasted Month</p>
|
||||
<h3 class="pt-32px text-24px font-bold">
|
||||
<count-to prefix="$" :start-value="0" :end-value="7754" />
|
||||
</h3>
|
||||
<p class="text-#aaa">Current Month Earnings</p>
|
||||
<h3 class="pt-32px text-24px font-bold">
|
||||
<count-to :start-value="0" :end-value="1234" />
|
||||
</h3>
|
||||
<p class="text-#aaa">Current Month Sales</p>
|
||||
<n-button class="mt-24px whitespace-pre-wrap" type="primary">Last Month Summary</n-button>
|
||||
</div>
|
||||
</n-card>
|
||||
</n-grid-item>
|
||||
<n-grid-item span="0:24 640:24 1024:10">
|
||||
<n-card :bordered="false" class="rounded-8px shadow-sm">
|
||||
<div ref="lineRef" class="w-full h-360px"></div>
|
||||
</n-card>
|
||||
</n-grid-item>
|
||||
<n-grid-item span="0:24 640:24 1024:8">
|
||||
<n-card :bordered="false" class="rounded-8px shadow-sm">
|
||||
<div ref="pieRef" class="w-full h-360px"></div>
|
||||
</n-card>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import { type ECOption, useEcharts } from '@/composables';
|
||||
|
||||
defineOptions({ name: 'DashboardAnalysisTopCard' });
|
||||
|
||||
const lineOptions = ref<ECOption>({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['下载量', '注册数']
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: ['06:00', '08:00', '10:00', '12:00', '14:00', '16:00', '18:00', '20:00', '22:00', '24:00']
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value'
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
color: '#8e9dff',
|
||||
name: '下载量',
|
||||
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: [4623, 6145, 6268, 6411, 1890, 4251, 2978, 3880, 3606, 4311]
|
||||
},
|
||||
{
|
||||
color: '#26deca',
|
||||
name: '注册数',
|
||||
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: [2208, 2016, 2916, 4512, 8281, 2008, 1963, 2367, 2956, 678]
|
||||
}
|
||||
]
|
||||
}) as Ref<ECOption>;
|
||||
const { domRef: lineRef } = useEcharts(lineOptions);
|
||||
|
||||
const pieOptions = ref<ECOption>({
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
bottom: '1%',
|
||||
left: 'center',
|
||||
itemStyle: {
|
||||
borderWidth: 0
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
color: ['#5da8ff', '#8e9dff', '#fedc69', '#26deca'],
|
||||
name: '时间安排',
|
||||
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: [
|
||||
{ value: 20, name: '学习' },
|
||||
{ value: 10, name: '娱乐' },
|
||||
{ value: 30, name: '工作' },
|
||||
{ value: 40, name: '休息' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}) as Ref<ECOption>;
|
||||
const { domRef: pieRef } = useEcharts(pieOptions);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,13 +0,0 @@
|
||||
<template>
|
||||
<n-space :vertical="true" :size="16">
|
||||
<top-chart />
|
||||
<data-card />
|
||||
<bottom-part />
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { BottomPart, DataCard, TopChart } from './components';
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,4 +0,0 @@
|
||||
import WorkbenchHeader from './workbench-header/index.vue';
|
||||
import WorkbenchMain from './workbench-main/index.vue';
|
||||
|
||||
export { WorkbenchHeader, WorkbenchMain };
|
@ -1,50 +0,0 @@
|
||||
<template>
|
||||
<n-card :bordered="false" class="rounded-8px shadow-sm">
|
||||
<div class="flex-y-center justify-between">
|
||||
<div class="flex-y-center">
|
||||
<icon-local-avatar class="text-70px" />
|
||||
<div class="pl-12px">
|
||||
<h3 class="text-18px font-semibold">早安,{{ auth.userInfo.userName }}, 今天又是充满活力的一天!</h3>
|
||||
<p class="leading-30px text-#999">今日多云转晴,20℃ - 25℃!</p>
|
||||
</div>
|
||||
</div>
|
||||
<n-space :size="24" :wrap="false">
|
||||
<n-statistic v-for="item in statisticData" :key="item.id" class="whitespace-nowrap" v-bind="item"></n-statistic>
|
||||
</n-space>
|
||||
</div>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore } from '@/store';
|
||||
|
||||
defineOptions({ name: 'DashboardWorkbenchHeader' });
|
||||
|
||||
const auth = useAuthStore();
|
||||
|
||||
interface StatisticData {
|
||||
id: number;
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const statisticData: StatisticData[] = [
|
||||
{
|
||||
id: 0,
|
||||
label: '项目数',
|
||||
value: '25'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
label: '待办',
|
||||
value: '4/16'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: '消息',
|
||||
value: '12'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,4 +0,0 @@
|
||||
import TechnologyCard from './technology-card.vue';
|
||||
import ShortcutsCard from './shortcuts-card.vue';
|
||||
|
||||
export { TechnologyCard, ShortcutsCard };
|
@ -1,25 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex-col-center h-120px p-12px border-1px border-#efeff5 dark:border-#ffffff17 rounded-4px hover:shadow-sm cursor-pointer"
|
||||
>
|
||||
<svg-icon :icon="icon" :style="{ color: iconColor }" class="text-30px" />
|
||||
<p class="py-8px text-16px">{{ label }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: 'DashboardWorkbenchMainShortcutsCard' });
|
||||
|
||||
interface Props {
|
||||
/** 快捷操作名称 */
|
||||
label: string;
|
||||
/** 图标 */
|
||||
icon: string;
|
||||
/** 图标颜色 */
|
||||
iconColor: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,42 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="h-120px p-4px border-1px border-#efeff5 dark:border-#ffffff17 rounded-4px hover:shadow-sm cursor-pointer"
|
||||
@click="handleOpenSite"
|
||||
>
|
||||
<header class="flex-y-center">
|
||||
<svg-icon :icon="icon" :style="{ color: iconColor }" class="text-30px" />
|
||||
<h3 class="pl-12px text-18px font-semibold">{{ name }}</h3>
|
||||
</header>
|
||||
<p class="py-8px h-56px text-#999">{{ description }}</p>
|
||||
<div class="flex justify-end">
|
||||
<span>{{ author }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: 'DashboardWorkbenchMainTechnologyCard' });
|
||||
|
||||
interface Props {
|
||||
/** 技术名称 */
|
||||
name: string;
|
||||
/** 技术描述 */
|
||||
description: string;
|
||||
/** 技术作者 */
|
||||
author: string;
|
||||
/** 技术官网 */
|
||||
site: string;
|
||||
/** 技术图标 */
|
||||
icon: string;
|
||||
/** 图标颜色 */
|
||||
iconColor?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
function handleOpenSite() {
|
||||
window.open(props.site, '_blank');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,146 +0,0 @@
|
||||
<template>
|
||||
<n-grid :item-responsive="true" :x-gap="16" :y-gap="16">
|
||||
<n-grid-item span="0:24 640:24 1024:16">
|
||||
<n-space :vertical="true" :size="16">
|
||||
<n-card title="项目主要技术栈" :bordered="false" size="small" class="rounded-8px shadow-sm">
|
||||
<template #header-extra>
|
||||
<a class="text-primary" href="javascript:;">更多技术栈</a>
|
||||
</template>
|
||||
<n-grid :item-responsive="true" responsive="screen" cols="m:2 l:3" :x-gap="8" :y-gap="8">
|
||||
<n-grid-item v-for="item in technology" :key="item.id">
|
||||
<technology-card v-bind="item" />
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
<n-card title="动态" :bordered="false" size="small" class="rounded-8px shadow-sm">
|
||||
<template #header-extra>
|
||||
<a class="text-primary" href="javascript:;">更多动态</a>
|
||||
</template>
|
||||
<n-list>
|
||||
<n-list-item v-for="item in activity" :key="item.id">
|
||||
<template #prefix>
|
||||
<icon-local-avatar class="text-48px" />
|
||||
</template>
|
||||
<n-thing :title="item.content" :description="item.time" />
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</n-grid-item>
|
||||
<n-grid-item span="0:24 640:24 1024:8">
|
||||
<n-space :vertical="true" :size="16">
|
||||
<n-card title="快捷操作" :bordered="false" size="small" class="rounded-8px shadow-sm">
|
||||
<n-grid :item-responsive="true" responsive="screen" cols="m:2 l:3" :x-gap="8" :y-gap="8">
|
||||
<n-grid-item v-for="item in shortcuts" :key="item.id">
|
||||
<shortcuts-card v-bind="item" />
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
<n-card title="创意" :bordered="false" size="small" class="rounded-8px shadow-sm">
|
||||
<div class="flex-center h-380px">
|
||||
<icon-local-banner class="text-400px sm:text-320px text-primary" />
|
||||
</div>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ShortcutsCard, TechnologyCard } from './components';
|
||||
|
||||
defineOptions({ name: 'DashboardWorkbenchMain' });
|
||||
|
||||
interface Technology {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
author: string;
|
||||
site: string;
|
||||
icon: string;
|
||||
iconColor?: string;
|
||||
}
|
||||
|
||||
const technology: Technology[] = [
|
||||
{
|
||||
id: 0,
|
||||
name: 'Vue',
|
||||
description: '一套用于构建用户界面的渐进式框架',
|
||||
author: '尤雨溪 - Evan You',
|
||||
site: 'https://v3.cn.vuejs.org/',
|
||||
icon: 'logos:vue'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'TypeScript',
|
||||
description: 'JavaScript类型的超集,它可以编译成纯JavaScript',
|
||||
author: '微软 - Microsoft',
|
||||
site: 'https://www.typescriptlang.org/',
|
||||
icon: 'logos:typescript-icon'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Vite',
|
||||
description: '下一代前端开发与构建工具',
|
||||
author: '尤雨溪 - Evan You',
|
||||
site: 'https://vitejs.cn/',
|
||||
icon: 'logos:vitejs'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'NaiveUI',
|
||||
description: '一个 Vue 3 组件库',
|
||||
author: '图森未来 - TuSimple',
|
||||
site: 'https://www.naiveui.com/zh-CN/os-theme',
|
||||
icon: 'logos:naiveui'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'UnoCSS',
|
||||
description: '下一代实用优先的CSS框架',
|
||||
author: 'Anthony Fu',
|
||||
site: 'https://uno.antfu.me/?s=',
|
||||
icon: 'logos:unocss'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Pinia',
|
||||
description: 'vue状态管理框架,支持vue2、vue3',
|
||||
author: 'Posva',
|
||||
site: 'https://pinia.esm.dev/',
|
||||
icon: 'noto:pineapple'
|
||||
}
|
||||
];
|
||||
|
||||
interface Activity {
|
||||
id: number;
|
||||
content: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
const activity: Activity[] = [
|
||||
{ id: 4, content: 'Soybean 刚才把工作台页面随便写了一些,凑合能看了!', time: '2021-11-07 22:45:32' },
|
||||
{ id: 3, content: 'Soybean 正在忙于为soybean-admin写项目说明文档!', time: '2021-11-03 20:33:31' },
|
||||
{ id: 2, content: 'Soybean 准备为soybean-admin 1.0的发布做充分的准备工作!', time: '2021-10-31 22:43:12' },
|
||||
{ id: 1, content: '@yanbowe 向soybean-admin提交了一个bug,多标签栏不会自适应。', time: '2021-10-27 10:24:54' },
|
||||
{ id: 0, content: 'Soybean 在2021年5月28日创建了开源项目soybean-admin!', time: '2021-05-28 22:22:22' }
|
||||
];
|
||||
|
||||
interface Shortcuts {
|
||||
id: number;
|
||||
label: string;
|
||||
icon: string;
|
||||
iconColor: string;
|
||||
}
|
||||
|
||||
const shortcuts: Shortcuts[] = [
|
||||
{ id: 0, label: '主控台', icon: 'mdi:desktop-mac-dashboard', iconColor: '#409eff' },
|
||||
{ id: 1, label: '系统管理', icon: 'ic:outline-settings', iconColor: '#7238d1' },
|
||||
{ id: 2, label: '权限管理', icon: 'mdi:family-tree', iconColor: '#f56c6c' },
|
||||
{ id: 3, label: '组件', icon: 'fluent:app-store-24-filled', iconColor: '#19a2f1' },
|
||||
{ id: 4, label: '表格', icon: 'mdi:table-large', iconColor: '#fab251' },
|
||||
{ id: 5, label: '图表', icon: 'mdi:chart-areaspline', iconColor: '#8aca6b' }
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,12 +0,0 @@
|
||||
<template>
|
||||
<n-space :vertical="true" :size="16">
|
||||
<workbench-header />
|
||||
<workbench-main />
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { WorkbenchHeader, WorkbenchMain } from './components';
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,13 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<iframe class="wh-full" :src="src"></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const src = ref('https://www.naiveui.com/zh-CN/os-theme/docs/introduction');
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,13 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<iframe class="wh-full" :src="src"></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const src = ref('https://admin-docs.soybeanjs.cn/');
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,13 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<iframe class="wh-full" :src="src"></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const src = ref('https://cn.vitejs.dev/');
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,13 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<iframe class="wh-full" :src="src"></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const src = ref('https://v3.cn.vuejs.org/');
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,7 +0,0 @@
|
||||
<template>
|
||||
<exception-base type="403" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<style scoped></style>
|
@ -1,7 +0,0 @@
|
||||
<template>
|
||||
<exception-base type="404" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<style scoped></style>
|
@ -1,7 +0,0 @@
|
||||
<template>
|
||||
<exception-base type="500" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<style scoped></style>
|
@ -1,27 +0,0 @@
|
||||
<template>
|
||||
<n-space :vertical="true" :size="16">
|
||||
<n-card title="Tab Detail" :bordered="false" size="small" class="rounded-8px shadow-sm">
|
||||
<n-space :vertical="true" :size="12">
|
||||
<div>当前路由的描述数据(meta):</div>
|
||||
<div>{{ route.meta }}</div>
|
||||
<n-button @click="handleToTab">返回Tab</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router';
|
||||
import { routeName } from '@/router';
|
||||
import { useRouterPush } from '@/composables';
|
||||
|
||||
const { routerPush } = useRouterPush();
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
function handleToTab() {
|
||||
routerPush({ name: routeName('function_tab') });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,28 +0,0 @@
|
||||
<template>
|
||||
<n-space :vertical="true" :size="16">
|
||||
<n-card title="Tab Detail" :bordered="false" size="small" class="rounded-8px shadow-sm">
|
||||
<n-space :vertical="true" :size="12">
|
||||
<div>当前路由的描述数据(meta):</div>
|
||||
<div>{{ route.meta }}</div>
|
||||
<div>当前路由的查询数据(query):</div>
|
||||
<div>{{ route.query }}</div>
|
||||
<n-button @click="handleToTab">返回Tab</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router';
|
||||
import { routeName } from '@/router';
|
||||
import { useRouterPush } from '@/composables';
|
||||
|
||||
const route = useRoute();
|
||||
const { routerPush } = useRouterPush();
|
||||
|
||||
function handleToTab() {
|
||||
routerPush({ name: routeName('function_tab') });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,44 +0,0 @@
|
||||
<template>
|
||||
<n-space :vertical="true" :size="16">
|
||||
<n-card title="Tab Home" :bordered="false" size="small" class="rounded-8px shadow-sm">
|
||||
<n-space :vertical="true" :size="12">
|
||||
<n-button @click="handleToTabDetail">跳转Tab Detail</n-button>
|
||||
<n-button @click="handleToTabMultiDetail(1)">跳转Tab Multi Detail 1</n-button>
|
||||
<n-button @click="handleToTabMultiDetail(2)">跳转Tab Multi Detail 2</n-button>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="title" />
|
||||
<n-button type="primary" @click="handleSetTitle">设置当前Tab页标题</n-button>
|
||||
</n-input-group>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { routeName } from '@/router';
|
||||
import { useTabStore } from '@/store';
|
||||
import { useRouterPush } from '@/composables';
|
||||
|
||||
const { routerPush } = useRouterPush();
|
||||
const tabStore = useTabStore();
|
||||
const title = ref('');
|
||||
|
||||
function handleToTabDetail() {
|
||||
routerPush({ name: routeName('function_tab-detail'), query: { name: 'abc' }, hash: '#DEMO_HASH' });
|
||||
}
|
||||
|
||||
function handleToTabMultiDetail(num: number) {
|
||||
routerPush({ name: routeName('function_tab-multi-detail'), query: { name: 'abc', num }, hash: '#DEMO_HASH' });
|
||||
}
|
||||
|
||||
function handleSetTitle() {
|
||||
if (!title.value) {
|
||||
window.$message?.warning('请输入要设置的标题名称');
|
||||
} else {
|
||||
tabStore.setActiveTabTitle(title.value);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
9
src/views/home/index.vue
Normal file
9
src/views/home/index.vue
Normal file
@ -0,0 +1,9 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<NCalendar />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
@ -1,49 +0,0 @@
|
||||
import type { RouteComponent } from 'vue-router';
|
||||
|
||||
export const views: Record<
|
||||
PageRoute.LastDegreeRouteKey,
|
||||
RouteComponent | (() => Promise<{ default: RouteComponent }>)
|
||||
> = {
|
||||
403: () => import('./_builtin/403/index.vue'),
|
||||
404: () => import('./_builtin/404/index.vue'),
|
||||
500: () => import('./_builtin/500/index.vue'),
|
||||
'constant-page': () => import('./_builtin/constant-page/index.vue'),
|
||||
login: () => import('./_builtin/login/index.vue'),
|
||||
'not-found': () => import('./_builtin/not-found/index.vue'),
|
||||
about: () => import('./about/index.vue'),
|
||||
'auth-demo_permission': () => import('./auth-demo/permission/index.vue'),
|
||||
'auth-demo_super': () => import('./auth-demo/super/index.vue'),
|
||||
component_button: () => import('./component/button/index.vue'),
|
||||
component_card: () => import('./component/card/index.vue'),
|
||||
component_table: () => import('./component/table/index.vue'),
|
||||
dashboard_analysis: () => import('./dashboard/analysis/index.vue'),
|
||||
dashboard_workbench: () => import('./dashboard/workbench/index.vue'),
|
||||
document_naive: () => import('./document/naive/index.vue'),
|
||||
'document_project-link': () => import('./document/project-link/index.vue'),
|
||||
document_project: () => import('./document/project/index.vue'),
|
||||
document_vite: () => import('./document/vite/index.vue'),
|
||||
document_vue: () => import('./document/vue/index.vue'),
|
||||
exception_403: () => import('./exception/403/index.vue'),
|
||||
exception_404: () => import('./exception/404/index.vue'),
|
||||
exception_500: () => import('./exception/500/index.vue'),
|
||||
'function_tab-detail': () => import('./function/tab-detail/index.vue'),
|
||||
'function_tab-multi-detail': () => import('./function/tab-multi-detail/index.vue'),
|
||||
function_tab: () => import('./function/tab/index.vue'),
|
||||
function_websocket: () => import('./function/websocket/index.vue'),
|
||||
management_auth: () => import('./management/auth/index.vue'),
|
||||
management_role: () => import('./management/role/index.vue'),
|
||||
management_route: () => import('./management/route/index.vue'),
|
||||
management_user: () => import('./management/user/index.vue'),
|
||||
'multi-menu_first_second-new_third': () => import('./multi-menu/first/second-new/third/index.vue'),
|
||||
'multi-menu_first_second': () => import('./multi-menu/first/second/index.vue'),
|
||||
plugin_charts_antv: () => import('./plugin/charts/antv/index.vue'),
|
||||
plugin_charts_echarts: () => import('./plugin/charts/echarts/index.vue'),
|
||||
plugin_copy: () => import('./plugin/copy/index.vue'),
|
||||
plugin_editor_markdown: () => import('./plugin/editor/markdown/index.vue'),
|
||||
plugin_editor_quill: () => import('./plugin/editor/quill/index.vue'),
|
||||
plugin_icon: () => import('./plugin/icon/index.vue'),
|
||||
plugin_map: () => import('./plugin/map/index.vue'),
|
||||
plugin_print: () => import('./plugin/print/index.vue'),
|
||||
plugin_swiper: () => import('./plugin/swiper/index.vue'),
|
||||
plugin_video: () => import('./plugin/video/index.vue')
|
||||
};
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<LookForward />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
15
src/views/manage/route/index.vue
Normal file
15
src/views/manage/route/index.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { onActivated } from 'vue';
|
||||
|
||||
console.log('setup');
|
||||
|
||||
onActivated(() => {
|
||||
console.log('onActivated');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LookForward />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
13
src/views/manage/user-detail/[id].vue
Normal file
13
src/views/manage/user-detail/[id].vue
Normal file
@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
id: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LookForward />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<LookForward />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
@ -1,7 +0,0 @@
|
||||
<template>
|
||||
<div>权限管理</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx"></script>
|
||||
|
||||
<style scoped></style>
|
@ -1,69 +0,0 @@
|
||||
<template>
|
||||
<n-popover placement="bottom" trigger="click">
|
||||
<template #trigger>
|
||||
<n-button size="small" type="primary">
|
||||
<icon-ant-design-setting-outlined class="mr-4px text-16px" />
|
||||
表格列设置
|
||||
</n-button>
|
||||
</template>
|
||||
<div class="w-180px">
|
||||
<vue-draggable v-model="list" item-key="key">
|
||||
<template #item="{ element }">
|
||||
<div v-if="element.key" class="flex-y-center h-36px px-12px hover:bg-primary_active">
|
||||
<icon-mdi-drag class="mr-8px text-20px cursor-move" />
|
||||
<n-checkbox v-model:checked="element.checked">
|
||||
{{ element.title }}
|
||||
</n-checkbox>
|
||||
</div>
|
||||
</template>
|
||||
</vue-draggable>
|
||||
</div>
|
||||
</n-popover>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import type { DataTableColumn } from 'naive-ui';
|
||||
import VueDraggable from 'vuedraggable';
|
||||
|
||||
type Column = DataTableColumn<UserManagement.User>;
|
||||
|
||||
interface Props {
|
||||
columns: Column[];
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:columns', columns: Column[]): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
type List = Column & { checked?: boolean };
|
||||
|
||||
const list = ref(initList());
|
||||
|
||||
function initList(): List[] {
|
||||
return props.columns.map(item => ({ ...item, checked: true }));
|
||||
}
|
||||
|
||||
watch(
|
||||
list,
|
||||
newValue => {
|
||||
const newColumns = newValue.filter(item => item.checked);
|
||||
|
||||
const columns: Column[] = newColumns.map(item => {
|
||||
const column = { ...item };
|
||||
delete column.checked;
|
||||
|
||||
return column;
|
||||
}) as Column[];
|
||||
|
||||
emit('update:columns', columns);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,150 +0,0 @@
|
||||
<template>
|
||||
<n-modal v-model:show="modalVisible" preset="card" :title="title" class="w-700px">
|
||||
<n-form ref="formRef" label-placement="left" :label-width="80" :model="formModel" :rules="rules">
|
||||
<n-grid :cols="24" :x-gap="18">
|
||||
<n-form-item-grid-item :span="12" label="用户名" path="userName">
|
||||
<n-input v-model:value="formModel.userName" />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="12" label="年龄" path="age">
|
||||
<n-input-number v-model:value="formModel.age" clearable />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="12" label="性别" path="gender">
|
||||
<n-radio-group v-model:value="formModel.gender">
|
||||
<n-radio v-for="item in genderOptions" :key="item.value" :value="item.value">{{ item.label }}</n-radio>
|
||||
</n-radio-group>
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="12" label="手机号" path="phone">
|
||||
<n-input v-model:value="formModel.phone" />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="12" label="邮箱" path="email">
|
||||
<n-input v-model:value="formModel.email" />
|
||||
</n-form-item-grid-item>
|
||||
<n-form-item-grid-item :span="12" label="状态" path="userStatus">
|
||||
<n-select v-model:value="formModel.userStatus" :options="userStatusOptions" />
|
||||
</n-form-item-grid-item>
|
||||
</n-grid>
|
||||
<n-space class="w-full pt-16px" :size="24" justify="end">
|
||||
<n-button class="w-72px" @click="closeModal">取消</n-button>
|
||||
<n-button class="w-72px" type="primary" @click="handleSubmit">确定</n-button>
|
||||
</n-space>
|
||||
</n-form>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, reactive, watch } from 'vue';
|
||||
import type { FormInst, FormItemRule } from 'naive-ui';
|
||||
import { genderOptions, userStatusOptions } from '@/constants';
|
||||
import { formRules, createRequiredFormRule } from '@/utils';
|
||||
|
||||
export interface Props {
|
||||
/** 弹窗可见性 */
|
||||
visible: boolean;
|
||||
/**
|
||||
* 弹窗类型
|
||||
* add: 新增
|
||||
* edit: 编辑
|
||||
*/
|
||||
type?: 'add' | 'edit';
|
||||
/** 编辑的表格行数据 */
|
||||
editData?: UserManagement.User | null;
|
||||
}
|
||||
|
||||
export type ModalType = NonNullable<Props['type']>;
|
||||
|
||||
defineOptions({ name: 'TableActionModal' });
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
type: 'add',
|
||||
editData: null
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const modalVisible = computed({
|
||||
get() {
|
||||
return props.visible;
|
||||
},
|
||||
set(visible) {
|
||||
emit('update:visible', visible);
|
||||
}
|
||||
});
|
||||
const closeModal = () => {
|
||||
modalVisible.value = false;
|
||||
};
|
||||
|
||||
const title = computed(() => {
|
||||
const titles: Record<ModalType, string> = {
|
||||
add: '添加用户',
|
||||
edit: '编辑用户'
|
||||
};
|
||||
return titles[props.type];
|
||||
});
|
||||
|
||||
const formRef = ref<HTMLElement & FormInst>();
|
||||
|
||||
type FormModel = Pick<UserManagement.User, 'userName' | 'age' | 'gender' | 'phone' | 'email' | 'userStatus'>;
|
||||
|
||||
const formModel = reactive<FormModel>(createDefaultFormModel());
|
||||
|
||||
const rules: Record<keyof FormModel, FormItemRule | FormItemRule[]> = {
|
||||
userName: createRequiredFormRule('请输入用户名'),
|
||||
age: createRequiredFormRule('请输入年龄'),
|
||||
gender: createRequiredFormRule('请选择性别'),
|
||||
phone: formRules.phone,
|
||||
email: formRules.email,
|
||||
userStatus: createRequiredFormRule('请选择用户状态')
|
||||
};
|
||||
|
||||
function createDefaultFormModel(): FormModel {
|
||||
return {
|
||||
userName: '',
|
||||
age: null,
|
||||
gender: null,
|
||||
phone: '',
|
||||
email: null,
|
||||
userStatus: null
|
||||
};
|
||||
}
|
||||
|
||||
function handleUpdateFormModel(model: Partial<FormModel>) {
|
||||
Object.assign(formModel, model);
|
||||
}
|
||||
|
||||
function handleUpdateFormModelByModalType() {
|
||||
const handlers: Record<ModalType, () => void> = {
|
||||
add: () => {
|
||||
const defaultFormModel = createDefaultFormModel();
|
||||
handleUpdateFormModel(defaultFormModel);
|
||||
},
|
||||
edit: () => {
|
||||
if (props.editData) {
|
||||
handleUpdateFormModel(props.editData);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handlers[props.type]();
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await formRef.value?.validate();
|
||||
window.$message?.success('新增成功!');
|
||||
closeModal();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
newValue => {
|
||||
if (newValue) {
|
||||
handleUpdateFormModelByModalType();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,212 +0,0 @@
|
||||
<template>
|
||||
<div class="overflow-hidden">
|
||||
<n-card title="用户管理" :bordered="false" class="h-full rounded-8px shadow-sm">
|
||||
<div class="flex-col h-full">
|
||||
<n-space class="pb-12px" justify="space-between">
|
||||
<n-space>
|
||||
<n-button type="primary" @click="handleAddTable">
|
||||
<icon-ic-round-plus class="mr-4px text-20px" />
|
||||
新增
|
||||
</n-button>
|
||||
<n-button type="error">
|
||||
<icon-ic-round-delete class="mr-4px text-20px" />
|
||||
删除
|
||||
</n-button>
|
||||
<n-button type="success">
|
||||
<icon-uil:export class="mr-4px text-20px" />
|
||||
导出Excel
|
||||
</n-button>
|
||||
</n-space>
|
||||
<n-space align="center" :size="18">
|
||||
<n-button size="small" type="primary" @click="getTableData">
|
||||
<icon-mdi-refresh class="mr-4px text-16px" :class="{ 'animate-spin': loading }" />
|
||||
刷新表格
|
||||
</n-button>
|
||||
<column-setting v-model:columns="columns" />
|
||||
</n-space>
|
||||
</n-space>
|
||||
<n-data-table
|
||||
:columns="columns"
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
flex-height
|
||||
class="flex-1-hidden"
|
||||
/>
|
||||
<table-action-modal v-model:visible="visible" :type="modalType" :edit-data="editData" />
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { reactive, ref } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import { NButton, NPopconfirm, NSpace, NTag } from 'naive-ui';
|
||||
import type { DataTableColumns, PaginationProps } from 'naive-ui';
|
||||
import { genderLabels, userStatusLabels } from '@/constants';
|
||||
import { fetchUserList } from '@/service';
|
||||
import { useBoolean, useLoading } from '@/hooks';
|
||||
import TableActionModal from './components/table-action-modal.vue';
|
||||
import type { ModalType } from './components/table-action-modal.vue';
|
||||
import ColumnSetting from './components/column-setting.vue';
|
||||
|
||||
const { loading, startLoading, endLoading } = useLoading(false);
|
||||
const { bool: visible, setTrue: openModal } = useBoolean();
|
||||
|
||||
const tableData = ref<UserManagement.User[]>([]);
|
||||
function setTableData(data: UserManagement.User[]) {
|
||||
tableData.value = data;
|
||||
}
|
||||
|
||||
async function getTableData() {
|
||||
startLoading();
|
||||
const { data } = await fetchUserList();
|
||||
if (data) {
|
||||
setTimeout(() => {
|
||||
setTableData(data);
|
||||
endLoading();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
const columns: Ref<DataTableColumns<UserManagement.User>> = ref([
|
||||
{
|
||||
type: 'selection',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'index',
|
||||
title: '序号',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'userName',
|
||||
title: '用户名',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'age',
|
||||
title: '用户年龄',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'gender',
|
||||
title: '性别',
|
||||
align: 'center',
|
||||
render: row => {
|
||||
if (row.gender) {
|
||||
const tagTypes: Record<UserManagement.GenderKey, NaiveUI.ThemeColor> = {
|
||||
'0': 'success',
|
||||
'1': 'warning'
|
||||
};
|
||||
|
||||
return <NTag type={tagTypes[row.gender]}>{genderLabels[row.gender]}</NTag>;
|
||||
}
|
||||
|
||||
return <span></span>;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'phone',
|
||||
title: '手机号码',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'email',
|
||||
title: '邮箱',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
key: 'userStatus',
|
||||
title: '状态',
|
||||
align: 'center',
|
||||
render: row => {
|
||||
if (row.userStatus) {
|
||||
const tagTypes: Record<UserManagement.UserStatusKey, NaiveUI.ThemeColor> = {
|
||||
'1': 'success',
|
||||
'2': 'error',
|
||||
'3': 'warning',
|
||||
'4': 'default'
|
||||
};
|
||||
|
||||
return <NTag type={tagTypes[row.userStatus]}>{userStatusLabels[row.userStatus]}</NTag>;
|
||||
}
|
||||
return <span></span>;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
title: '操作',
|
||||
align: 'center',
|
||||
render: row => {
|
||||
return (
|
||||
<NSpace justify={'center'}>
|
||||
<NButton size={'small'} onClick={() => handleEditTable(row.id)}>
|
||||
编辑
|
||||
</NButton>
|
||||
<NPopconfirm onPositiveClick={() => handleDeleteTable(row.id)}>
|
||||
{{
|
||||
default: () => '确认删除',
|
||||
trigger: () => <NButton size={'small'}>删除</NButton>
|
||||
}}
|
||||
</NPopconfirm>
|
||||
</NSpace>
|
||||
);
|
||||
}
|
||||
}
|
||||
]) as Ref<DataTableColumns<UserManagement.User>>;
|
||||
|
||||
const modalType = ref<ModalType>('add');
|
||||
|
||||
function setModalType(type: ModalType) {
|
||||
modalType.value = type;
|
||||
}
|
||||
|
||||
const editData = ref<UserManagement.User | null>(null);
|
||||
|
||||
function setEditData(data: UserManagement.User | null) {
|
||||
editData.value = data;
|
||||
}
|
||||
|
||||
function handleAddTable() {
|
||||
openModal();
|
||||
setModalType('add');
|
||||
}
|
||||
|
||||
function handleEditTable(rowId: string) {
|
||||
const findItem = tableData.value.find(item => item.id === rowId);
|
||||
if (findItem) {
|
||||
setEditData(findItem);
|
||||
}
|
||||
setModalType('edit');
|
||||
openModal();
|
||||
}
|
||||
|
||||
function handleDeleteTable(rowId: string) {
|
||||
window.$message?.info(`点击了删除,rowId为${rowId}`);
|
||||
}
|
||||
|
||||
const pagination: PaginationProps = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 15, 20, 25, 30],
|
||||
onChange: (page: number) => {
|
||||
pagination.page = page;
|
||||
},
|
||||
onUpdatePageSize: (pageSize: number) => {
|
||||
pagination.pageSize = pageSize;
|
||||
pagination.page = 1;
|
||||
}
|
||||
});
|
||||
|
||||
function init() {
|
||||
getTableData();
|
||||
}
|
||||
|
||||
// 初始化
|
||||
init();
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,9 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<n-card title="多级菜单 - 三级菜单" :bordered="false" class="h-full rounded-8px shadow-sm"></n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped></style>
|
@ -1,9 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<n-card title="多级菜单 - 二级菜单" :bordered="false" class="h-full rounded-8px shadow-sm"></n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped></style>
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>路由管理</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<LookForward />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>角色管理</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<LookForward />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
@ -1,482 +0,0 @@
|
||||
<template>
|
||||
<n-space :vertical="true" :size="16">
|
||||
<n-card :bordered="false" class="rounded-8px shadow-sm">
|
||||
<div ref="pieRef" class="h-400px"></div>
|
||||
</n-card>
|
||||
<n-card :bordered="false" class="rounded-8px shadow-sm">
|
||||
<div ref="lineRef" class="h-400px"></div>
|
||||
</n-card>
|
||||
<n-card :bordered="false" class="rounded-8px shadow-sm">
|
||||
<div ref="barRef" class="h-400px"></div>
|
||||
</n-card>
|
||||
<n-card :bordered="false" class="rounded-8px shadow-sm">
|
||||
<div ref="scatterRef" class="h-400px"></div>
|
||||
</n-card>
|
||||
<n-card :bordered="false" class="rounded-8px shadow-sm">
|
||||
<div ref="areaRef" class="h-400px"></div>
|
||||
</n-card>
|
||||
<n-card :bordered="false" class="rounded-8px shadow-sm">
|
||||
<div ref="radarRef" class="h-400px"></div>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import DataSet from '@antv/data-set';
|
||||
import { Chart } from '@antv/g2';
|
||||
|
||||
const pieRef = ref<HTMLElement>();
|
||||
const lineRef = ref<HTMLElement>();
|
||||
const barRef = ref<HTMLElement>();
|
||||
const scatterRef = ref<HTMLElement>();
|
||||
const areaRef = ref<HTMLElement>();
|
||||
const radarRef = ref<HTMLElement>();
|
||||
|
||||
function renderPieChart() {
|
||||
if (!pieRef.value) return;
|
||||
|
||||
const data = [
|
||||
{ item: 'rose 1', count: 40, percent: 0.4 },
|
||||
{ item: 'rose 2', count: 40, percent: 0.4 },
|
||||
{ item: 'rose 3', count: 40, percent: 0.4 },
|
||||
{ item: 'rose 4', count: 40, percent: 0.4 },
|
||||
{ item: 'rose 5', count: 21, percent: 0.21 },
|
||||
{ item: 'rose 6', count: 17, percent: 0.17 },
|
||||
{ item: 'rose 7', count: 13, percent: 0.13 },
|
||||
{ item: 'rose 8', count: 9, percent: 0.09 }
|
||||
];
|
||||
|
||||
const chart = new Chart({
|
||||
container: pieRef.value,
|
||||
autoFit: true
|
||||
});
|
||||
|
||||
chart.data(data);
|
||||
|
||||
chart.coordinate('theta', {
|
||||
radius: 0.85
|
||||
});
|
||||
|
||||
chart.scale('percent', {
|
||||
formatter: (val: number) => `${val * 100}%`
|
||||
});
|
||||
chart.tooltip({
|
||||
showTitle: false,
|
||||
showMarkers: false
|
||||
});
|
||||
chart.legend({ position: 'top' });
|
||||
chart.axis(false); // 关闭坐标轴
|
||||
chart
|
||||
.interval()
|
||||
.adjust('stack')
|
||||
.position('percent')
|
||||
.color('item')
|
||||
.label('percent', {
|
||||
offset: -40,
|
||||
style: {
|
||||
textAlign: 'center',
|
||||
shadowBlur: 2,
|
||||
shadowColor: 'rgba(0, 0, 0, .45)',
|
||||
fill: '#fff'
|
||||
}
|
||||
})
|
||||
.tooltip('item*percent', (item, percent) => {
|
||||
return {
|
||||
name: item,
|
||||
value: `${percent * 100}%`
|
||||
};
|
||||
})
|
||||
.style({
|
||||
lineWidth: 1,
|
||||
stroke: '#fff'
|
||||
});
|
||||
chart.interaction('element-single-selected');
|
||||
chart.render();
|
||||
}
|
||||
|
||||
function renderLineChart() {
|
||||
fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/terrorism.json')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const ds = new DataSet();
|
||||
if (!lineRef.value) return;
|
||||
|
||||
const chart = new Chart({
|
||||
container: lineRef.value,
|
||||
autoFit: true,
|
||||
syncViewPadding: true
|
||||
});
|
||||
|
||||
chart.scale({
|
||||
Deaths: {
|
||||
sync: true,
|
||||
nice: true
|
||||
},
|
||||
death: {
|
||||
sync: true,
|
||||
nice: true
|
||||
}
|
||||
});
|
||||
|
||||
const dv1 = ds.createView().source(data);
|
||||
dv1.transform({
|
||||
type: 'map',
|
||||
callback: (row: any) => {
|
||||
const currentRow = { ...row };
|
||||
if (typeof row.Deaths === 'string') {
|
||||
currentRow.Deaths = row.Deaths.replace(',', '');
|
||||
}
|
||||
currentRow.Deaths = parseInt(row.Deaths, 10);
|
||||
currentRow.death = row.Deaths;
|
||||
currentRow.year = row.Year;
|
||||
return currentRow;
|
||||
}
|
||||
});
|
||||
const view1 = chart.createView();
|
||||
view1.data(dv1.rows);
|
||||
view1.axis('Year', {
|
||||
subTickLine: {
|
||||
count: 3,
|
||||
length: 3
|
||||
},
|
||||
tickLine: {
|
||||
length: 6
|
||||
}
|
||||
});
|
||||
view1.axis('Deaths', {
|
||||
label: {
|
||||
formatter: text => {
|
||||
return text.replace(/(\d)(?=(?:\d{3})+$)/g, '$1,');
|
||||
}
|
||||
}
|
||||
});
|
||||
view1.line().position('Year*Deaths');
|
||||
|
||||
const dv2 = ds.createView().source(dv1.rows);
|
||||
dv2.transform({
|
||||
type: 'regression',
|
||||
method: 'polynomial',
|
||||
fields: ['year', 'death'],
|
||||
bandwidth: 0.1,
|
||||
as: ['year', 'death']
|
||||
});
|
||||
|
||||
const view2 = chart.createView();
|
||||
view2.axis(false);
|
||||
view2.data(dv2.rows);
|
||||
view2
|
||||
.line()
|
||||
.position('year*death')
|
||||
.style({
|
||||
stroke: '#969696',
|
||||
lineDash: [3, 3]
|
||||
})
|
||||
.tooltip(false);
|
||||
view1.annotation().text({
|
||||
content: '趋势线',
|
||||
position: ['1970', 2500],
|
||||
style: {
|
||||
fill: '#8c8c8c',
|
||||
fontSize: 14,
|
||||
fontWeight: 300
|
||||
},
|
||||
offsetY: -70
|
||||
});
|
||||
chart.render();
|
||||
});
|
||||
}
|
||||
|
||||
function renderBarChart() {
|
||||
if (!barRef.value) return;
|
||||
|
||||
const data = [
|
||||
{ type: '未知', value: 654, percent: 0.02 },
|
||||
{ type: '17 岁以下', value: 654, percent: 0.02 },
|
||||
{ type: '18-24 岁', value: 4400, percent: 0.2 },
|
||||
{ type: '25-29 岁', value: 5300, percent: 0.24 },
|
||||
{ type: '30-39 岁', value: 6200, percent: 0.28 },
|
||||
{ type: '40-49 岁', value: 3300, percent: 0.14 },
|
||||
{ type: '50 岁以上', value: 1500, percent: 0.06 }
|
||||
];
|
||||
|
||||
const chart = new Chart({
|
||||
container: barRef.value,
|
||||
autoFit: true,
|
||||
height: 500,
|
||||
padding: [50, 20, 50, 20]
|
||||
});
|
||||
chart.data(data);
|
||||
chart.scale('value', {
|
||||
alias: '销售额(万)'
|
||||
});
|
||||
|
||||
chart.axis('type', {
|
||||
tickLine: {
|
||||
alignTick: false
|
||||
}
|
||||
});
|
||||
chart.axis('value', false);
|
||||
|
||||
chart.tooltip({
|
||||
showMarkers: false
|
||||
});
|
||||
chart.interval().position('type*value');
|
||||
chart.interaction('element-active');
|
||||
|
||||
// 添加文本标注
|
||||
data.forEach(item => {
|
||||
chart
|
||||
.annotation()
|
||||
.text({
|
||||
position: [item.type, item.value],
|
||||
content: item.value,
|
||||
style: {
|
||||
textAlign: 'center'
|
||||
},
|
||||
offsetY: -30
|
||||
})
|
||||
.text({
|
||||
position: [item.type, item.value],
|
||||
content: `${(item.percent * 100).toFixed(0)}%`,
|
||||
style: {
|
||||
textAlign: 'center'
|
||||
},
|
||||
offsetY: -12
|
||||
});
|
||||
});
|
||||
chart.render();
|
||||
}
|
||||
|
||||
function renderScatterChart() {
|
||||
const colorMap = {
|
||||
Asia: '#1890FF',
|
||||
Americas: '#2FC25B',
|
||||
Europe: '#FACC14',
|
||||
Oceania: '#223273'
|
||||
};
|
||||
|
||||
fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/bubble.json')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (!scatterRef.value) return;
|
||||
const chart = new Chart({
|
||||
container: scatterRef.value,
|
||||
autoFit: true,
|
||||
height: 500
|
||||
});
|
||||
chart.data(data);
|
||||
// 为各个字段设置别名
|
||||
chart.scale({
|
||||
LifeExpectancy: {
|
||||
alias: '人均寿命(年)',
|
||||
nice: true
|
||||
},
|
||||
Population: {
|
||||
type: 'pow',
|
||||
alias: '人口总数'
|
||||
},
|
||||
GDP: {
|
||||
alias: '人均国内生产总值($)',
|
||||
nice: true
|
||||
},
|
||||
Country: {
|
||||
alias: '国家/地区'
|
||||
}
|
||||
});
|
||||
chart.axis('GDP', {
|
||||
label: {
|
||||
formatter(value) {
|
||||
return `${(Number(value) / 1000).toFixed(0)}k`;
|
||||
} // 格式化坐标轴的显示
|
||||
}
|
||||
});
|
||||
chart.tooltip({
|
||||
showTitle: false,
|
||||
showMarkers: false
|
||||
});
|
||||
chart.legend('Population', false); // 该图表默认会生成多个图例,设置不展示 Population 和 Country 两个维度的图例
|
||||
chart
|
||||
.point()
|
||||
.position('GDP*LifeExpectancy')
|
||||
.size('Population', [4, 65])
|
||||
.color('continent', val => {
|
||||
const key = val as keyof typeof colorMap;
|
||||
return colorMap[key];
|
||||
})
|
||||
.shape('circle')
|
||||
.tooltip('Country*Population*GDP*LifeExpectancy')
|
||||
.style('continent', val => {
|
||||
const key = val as keyof typeof colorMap;
|
||||
return {
|
||||
lineWidth: 1,
|
||||
strokeOpacity: 1,
|
||||
fillOpacity: 0.3,
|
||||
opacity: 0.65,
|
||||
stroke: colorMap[key]
|
||||
};
|
||||
});
|
||||
chart.interaction('element-active');
|
||||
chart.render();
|
||||
});
|
||||
}
|
||||
|
||||
function renderAreaChart() {
|
||||
if (!areaRef.value) return;
|
||||
|
||||
const data = [
|
||||
{ country: 'Asia', year: '1750', value: 502 },
|
||||
{ country: 'Asia', year: '1800', value: 635 },
|
||||
{ country: 'Asia', year: '1850', value: 809 },
|
||||
{ country: 'Asia', year: '1900', value: 5268 },
|
||||
{ country: 'Asia', year: '1950', value: 4400 },
|
||||
{ country: 'Asia', year: '1999', value: 3634 },
|
||||
{ country: 'Asia', year: '2050', value: 947 },
|
||||
{ country: 'Africa', year: '1750', value: 106 },
|
||||
{ country: 'Africa', year: '1800', value: 107 },
|
||||
{ country: 'Africa', year: '1850', value: 111 },
|
||||
{ country: 'Africa', year: '1900', value: 1766 },
|
||||
{ country: 'Africa', year: '1950', value: 221 },
|
||||
{ country: 'Africa', year: '1999', value: 767 },
|
||||
{ country: 'Africa', year: '2050', value: 133 },
|
||||
{ country: 'Europe', year: '1750', value: 163 },
|
||||
{ country: 'Europe', year: '1800', value: 203 },
|
||||
{ country: 'Europe', year: '1850', value: 276 },
|
||||
{ country: 'Europe', year: '1900', value: 628 },
|
||||
{ country: 'Europe', year: '1950', value: 547 },
|
||||
{ country: 'Europe', year: '1999', value: 729 },
|
||||
{ country: 'Europe', year: '2050', value: 408 },
|
||||
{ country: 'Oceania', year: '1750', value: 200 },
|
||||
{ country: 'Oceania', year: '1800', value: 200 },
|
||||
{ country: 'Oceania', year: '1850', value: 200 },
|
||||
{ country: 'Oceania', year: '1900', value: 460 },
|
||||
{ country: 'Oceania', year: '1950', value: 230 },
|
||||
{ country: 'Oceania', year: '1999', value: 300 },
|
||||
{ country: 'Oceania', year: '2050', value: 300 }
|
||||
];
|
||||
const chart = new Chart({
|
||||
container: areaRef.value,
|
||||
autoFit: true,
|
||||
height: 500
|
||||
});
|
||||
|
||||
chart.data(data);
|
||||
chart.scale('year', {
|
||||
type: 'linear',
|
||||
tickInterval: 50
|
||||
});
|
||||
chart.scale('value', {
|
||||
nice: true
|
||||
});
|
||||
|
||||
chart.tooltip({
|
||||
showCrosshairs: true,
|
||||
shared: true
|
||||
});
|
||||
|
||||
chart.area().adjust('stack').position('year*value').color('country');
|
||||
chart.line().adjust('stack').position('year*value').color('country');
|
||||
|
||||
chart.interaction('element-highlight');
|
||||
|
||||
chart.render();
|
||||
}
|
||||
|
||||
function renderRadarChart() {
|
||||
if (!radarRef.value) return;
|
||||
|
||||
const data = [
|
||||
{ item: 'Design', a: 70, b: 30 },
|
||||
{ item: 'Development', a: 60, b: 70 },
|
||||
{ item: 'Marketing', a: 50, b: 60 },
|
||||
{ item: 'Users', a: 40, b: 50 },
|
||||
{ item: 'Test', a: 60, b: 70 },
|
||||
{ item: 'Language', a: 70, b: 50 },
|
||||
{ item: 'Technology', a: 50, b: 40 },
|
||||
{ item: 'Support', a: 30, b: 40 },
|
||||
{ item: 'Sales', a: 60, b: 40 },
|
||||
{ item: 'UX', a: 50, b: 60 }
|
||||
];
|
||||
const { DataView } = DataSet;
|
||||
const dv = new DataView().source(data);
|
||||
dv.transform({
|
||||
type: 'fold',
|
||||
fields: ['a', 'b'], // 展开字段集
|
||||
key: 'user', // key字段
|
||||
value: 'score' // value字段
|
||||
});
|
||||
|
||||
const chart = new Chart({
|
||||
container: radarRef.value,
|
||||
autoFit: true,
|
||||
height: 500
|
||||
});
|
||||
chart.data(dv.rows);
|
||||
chart.scale('score', {
|
||||
min: 0,
|
||||
max: 80
|
||||
});
|
||||
chart.coordinate('polar', {
|
||||
radius: 0.8
|
||||
});
|
||||
chart.tooltip({
|
||||
shared: true,
|
||||
showCrosshairs: true,
|
||||
crosshairs: {
|
||||
line: {
|
||||
style: {
|
||||
lineDash: [4, 4],
|
||||
stroke: '#333'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
chart.axis('item', {
|
||||
line: null,
|
||||
tickLine: null,
|
||||
grid: {
|
||||
line: {
|
||||
style: {
|
||||
lineDash: null
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
chart.axis('score', {
|
||||
line: null,
|
||||
tickLine: null,
|
||||
grid: {
|
||||
line: {
|
||||
type: 'line',
|
||||
style: {
|
||||
lineDash: null
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
chart.line().position('item*score').color('user').size(2);
|
||||
chart.point().position('item*score').color('user').shape('circle').size(4).style({
|
||||
stroke: '#fff',
|
||||
lineWidth: 1,
|
||||
fillOpacity: 1
|
||||
});
|
||||
chart.area().position('item*score').color('user');
|
||||
chart.render();
|
||||
}
|
||||
|
||||
function init() {
|
||||
renderPieChart();
|
||||
renderLineChart();
|
||||
renderBarChart();
|
||||
renderScatterChart();
|
||||
renderAreaChart();
|
||||
renderRadarChart();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,783 +0,0 @@
|
||||
<template>
|
||||
<n-space :vertical="true" :size="16">
|
||||
<n-card :bordered="false" class="rounded-8px shadow-sm">
|
||||
<div ref="pieRef" class="h-400px"></div>
|
||||
</n-card>
|
||||
<n-card :bordered="false" class="rounded-8px shadow-sm">
|
||||
<div ref="lineRef" class="h-400px"></div>
|
||||
</n-card>
|
||||
<n-card :bordered="false" class="rounded-8px shadow-sm">
|
||||
<div ref="barRef" class="h-400px"></div>
|
||||
</n-card>
|
||||
<n-card :bordered="false" class="rounded-8px shadow-sm">
|
||||
<div ref="pictorialBarRef" class="h-600px"></div>
|
||||
</n-card>
|
||||
<n-card :bordered="false" class="rounded-8px shadow-sm">
|
||||
<div ref="scatterRef" class="h-600px"></div>
|
||||
</n-card>
|
||||
<n-card :bordered="false" class="rounded-8px shadow-sm">
|
||||
<div ref="radarRef" class="h-400px"></div>
|
||||
</n-card>
|
||||
<n-card :bordered="false" class="rounded-8px shadow-sm">
|
||||
<div ref="gaugeRef" class="h-640px"></div>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onUnmounted, ref } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import { graphic } from 'echarts';
|
||||
import { type ECOption, useEcharts } from '@/composables';
|
||||
|
||||
const pieOptions = ref<ECOption>({
|
||||
legend: {},
|
||||
toolbox: {
|
||||
show: true,
|
||||
feature: {
|
||||
mark: { show: true },
|
||||
dataView: { show: true, readOnly: false },
|
||||
restore: { show: true },
|
||||
saveAsImage: { show: true }
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'Nightingale Chart',
|
||||
type: 'pie',
|
||||
radius: [50, 150],
|
||||
center: ['50%', '50%'],
|
||||
roseType: 'area',
|
||||
itemStyle: {
|
||||
borderRadius: 8
|
||||
},
|
||||
data: [
|
||||
{ value: 40, name: 'rose 1' },
|
||||
{ value: 38, name: 'rose 2' },
|
||||
{ value: 32, name: 'rose 3' },
|
||||
{ value: 30, name: 'rose 4' },
|
||||
{ value: 28, name: 'rose 5' },
|
||||
{ value: 26, name: 'rose 6' },
|
||||
{ value: 22, name: 'rose 7' },
|
||||
{ value: 18, name: 'rose 8' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}) as Ref<ECOption>;
|
||||
const { domRef: pieRef } = useEcharts(pieOptions);
|
||||
|
||||
const lineOptions = ref<ECOption>({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
}
|
||||
},
|
||||
title: {
|
||||
text: 'Stacked Line'
|
||||
},
|
||||
legend: {
|
||||
data: ['Email', 'Union Ads', 'Video Ads', 'Direct', 'Search Engine']
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: {}
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
color: '#37a2da',
|
||||
name: 'Email',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
stack: 'Total',
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0.25,
|
||||
color: '#37a2da'
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: '#fff'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: [120, 132, 101, 134, 90, 230, 210]
|
||||
},
|
||||
{
|
||||
color: '#9fe6b8',
|
||||
name: 'Union Ads',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
stack: 'Total',
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0.25,
|
||||
color: '#9fe6b8'
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: '#fff'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: [220, 182, 191, 234, 290, 330, 310]
|
||||
},
|
||||
{
|
||||
color: '#fedb5c',
|
||||
name: 'Video Ads',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
stack: 'Total',
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0.25,
|
||||
color: '#fedb5c'
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: '#fff'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: [150, 232, 201, 154, 190, 330, 410]
|
||||
},
|
||||
{
|
||||
color: '#fb7293',
|
||||
name: 'Direct',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
stack: 'Total',
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0.25,
|
||||
color: '#fb7293'
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: '#fff'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: [320, 332, 301, 334, 390, 330, 320]
|
||||
},
|
||||
{
|
||||
color: '#e7bcf3',
|
||||
name: 'Search Engine',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
stack: 'Total',
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0.25,
|
||||
color: '#e7bcf3'
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: '#fff'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: [820, 932, 901, 934, 1290, 1330, 1320]
|
||||
}
|
||||
]
|
||||
}) as Ref<ECOption>;
|
||||
const { domRef: lineRef } = useEcharts(lineOptions);
|
||||
|
||||
const barOptions = ref<ECOption>({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: [120, 200, 150, 80, 70, 110, 130],
|
||||
type: 'bar',
|
||||
color: '#8378ea',
|
||||
showBackground: true,
|
||||
barGap: 100,
|
||||
itemStyle: {
|
||||
borderRadius: [40, 40, 0, 0]
|
||||
},
|
||||
backgroundStyle: {
|
||||
color: 'rgba(180, 180, 180, 0.2)'
|
||||
}
|
||||
}
|
||||
]
|
||||
}) as Ref<ECOption>;
|
||||
const { domRef: barRef } = useEcharts(barOptions);
|
||||
|
||||
const pictorialBarOption = ref<ECOption>(getPictorialBarOption()) as Ref<ECOption>;
|
||||
const { domRef: pictorialBarRef } = useEcharts(pictorialBarOption);
|
||||
function getPictorialBarOption(): ECOption {
|
||||
const category: string[] = [];
|
||||
let dottedBase = Number(new Date());
|
||||
const lineData: number[] = [];
|
||||
const barData: number[] = [];
|
||||
|
||||
for (let i = 0; i < 20; i += 1) {
|
||||
const date = new Date((dottedBase += 3600 * 24 * 1000));
|
||||
category.push([date.getFullYear(), date.getMonth() + 1, date.getDate()].join('-'));
|
||||
const b = Math.random() * 200;
|
||||
const d = Math.random() * 200;
|
||||
barData.push(b);
|
||||
lineData.push(d + b);
|
||||
}
|
||||
|
||||
const options: ECOption = {
|
||||
backgroundColor: '#0f375f',
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['line', 'bar'],
|
||||
textStyle: {
|
||||
color: '#ccc'
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
data: category,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#ccc'
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
splitLine: { show: false },
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#ccc'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'line',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
showAllSymbol: true,
|
||||
symbol: 'emptyCircle',
|
||||
symbolSize: 15,
|
||||
data: lineData
|
||||
},
|
||||
{
|
||||
name: 'bar',
|
||||
type: 'bar',
|
||||
barWidth: 10,
|
||||
itemStyle: {
|
||||
borderRadius: 5,
|
||||
color: new graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#14c8d4' },
|
||||
{ offset: 1, color: '#43eec6' }
|
||||
])
|
||||
},
|
||||
data: barData
|
||||
},
|
||||
{
|
||||
name: 'line',
|
||||
type: 'bar',
|
||||
barGap: '-100%',
|
||||
barWidth: 10,
|
||||
itemStyle: {
|
||||
color: new graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(20,200,212,0.5)' },
|
||||
{ offset: 0.2, color: 'rgba(20,200,212,0.2)' },
|
||||
{ offset: 1, color: 'rgba(20,200,212,0)' }
|
||||
])
|
||||
},
|
||||
z: -12,
|
||||
data: lineData
|
||||
},
|
||||
{
|
||||
name: 'dotted',
|
||||
type: 'pictorialBar',
|
||||
symbol: 'rect',
|
||||
itemStyle: {
|
||||
color: '#0f375f'
|
||||
},
|
||||
symbolRepeat: true,
|
||||
symbolSize: [12, 4],
|
||||
symbolMargin: 1,
|
||||
z: -10,
|
||||
data: lineData
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
const scatterOptions = ref<ECOption>(getScatterOption()) as Ref<ECOption>;
|
||||
const { domRef: scatterRef } = useEcharts(scatterOptions);
|
||||
|
||||
function getScatterOption() {
|
||||
// prettier-ignore
|
||||
const hours = ['12a', '1a', '2a', '3a', '4a', '5a', '6a', '7a', '8a', '9a','10a','11a', '12p', '1p', '2p', '3p', '4p', '5p', '6p', '7p', '8p', '9p', '10p', '11p'];
|
||||
|
||||
// prettier-ignore
|
||||
const days = ['Saturday', 'Friday', 'Thursday', 'Wednesday', 'Tuesday', 'Monday', 'Sunday'];
|
||||
|
||||
// prettier-ignore
|
||||
const data: [number, number, number][] = [[0,0,5],[0,1,1],[0,2,0],[0,3,0],[0,4,0],[0,5,0],[0,6,0],[0,7,0],[0,8,0],[0,9,0],[0,10,0],[0,11,2],[0,12,4],[0,13,1],[0,14,1],[0,15,3],[0,16,4],[0,17,6],[0,18,4],[0,19,4],[0,20,3],[0,21,3],[0,22,2],[0,23,5],[1,0,7],[1,1,0],[1,2,0],[1,3,0],[1,4,0],[1,5,0],[1,6,0],[1,7,0],[1,8,0],[1,9,0],[1,10,5],[1,11,2],[1,12,2],[1,13,6],[1,14,9],[1,15,11],[1,16,6],[1,17,7],[1,18,8],[1,19,12],[1,20,5],[1,21,5],[1,22,7],[1,23,2],[2,0,1],[2,1,1],[2,2,0],[2,3,0],[2,4,0],[2,5,0],[2,6,0],[2,7,0],[2,8,0],[2,9,0],[2,10,3],[2,11,2],[2,12,1],[2,13,9],[2,14,8],[2,15,10],[2,16,6],[2,17,5],[2,18,5],[2,19,5],[2,20,7],[2,21,4],[2,22,2],[2,23,4],[3,0,7],[3,1,3],[3,2,0],[3,3,0],[3,4,0],[3,5,0],[3,6,0],[3,7,0],[3,8,1],[3,9,0],[3,10,5],[3,11,4],[3,12,7],[3,13,14],[3,14,13],[3,15,12],[3,16,9],[3,17,5],[3,18,5],[3,19,10],[3,20,6],[3,21,4],[3,22,4],[3,23,1],[4,0,1],[4,1,3],[4,2,0],[4,3,0],[4,4,0],[4,5,1],[4,6,0],[4,7,0],[4,8,0],[4,9,2],[4,10,4],[4,11,4],[4,12,2],[4,13,4],[4,14,4],[4,15,14],[4,16,12],[4,17,1],[4,18,8],[4,19,5],[4,20,3],[4,21,7],[4,22,3],[4,23,0],[5,0,2],[5,1,1],[5,2,0],[5,3,3],[5,4,0],[5,5,0],[5,6,0],[5,7,0],[5,8,2],[5,9,0],[5,10,4],[5,11,1],[5,12,5],[5,13,10],[5,14,5],[5,15,7],[5,16,11],[5,17,6],[5,18,0],[5,19,5],[5,20,3],[5,21,4],[5,22,2],[5,23,0],[6,0,1],[6,1,0],[6,2,0],[6,3,0],[6,4,0],[6,5,0],[6,6,0],[6,7,0],[6,8,0],[6,9,0],[6,10,1],[6,11,0],[6,12,2],[6,13,1],[6,14,3],[6,15,4],[6,16,0],[6,17,0],[6,18,0],[6,19,0],[6,20,1],[6,21,2],[6,22,2],[6,23,6]];
|
||||
|
||||
const title: echarts.TitleComponentOption[] = [];
|
||||
const singleAxis: echarts.SingleAxisComponentOption[] = [];
|
||||
const series: echarts.ScatterSeriesOption[] = [];
|
||||
|
||||
days.forEach((day, idx) => {
|
||||
title.push({
|
||||
textBaseline: 'middle',
|
||||
top: `${((idx + 0.5) * 100) / 7}%`,
|
||||
text: day
|
||||
});
|
||||
singleAxis.push({
|
||||
left: 150,
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: hours,
|
||||
top: `${(idx * 100) / 7 + 5}%`,
|
||||
height: `${100 / 7 - 10}%`,
|
||||
axisLabel: {
|
||||
interval: 2
|
||||
}
|
||||
});
|
||||
series.push({
|
||||
singleAxisIndex: idx,
|
||||
coordinateSystem: 'singleAxis',
|
||||
type: 'scatter',
|
||||
data: [],
|
||||
symbolSize(dataItem) {
|
||||
return dataItem[1] * 4;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
data.forEach(dataItem => {
|
||||
(series as any)[dataItem[0]].data.push([dataItem[1], dataItem[2]]);
|
||||
});
|
||||
|
||||
const option: ECOption = {
|
||||
tooltip: {
|
||||
position: 'top'
|
||||
},
|
||||
title,
|
||||
singleAxis,
|
||||
series: series as any
|
||||
};
|
||||
|
||||
return option;
|
||||
}
|
||||
|
||||
const radarOptions = ref<ECOption>({
|
||||
title: {
|
||||
text: 'Multiple Radar'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
left: 'center',
|
||||
data: ['A Software', 'A Phone', 'Another Phone', 'Precipitation', 'Evaporation']
|
||||
},
|
||||
radar: [
|
||||
{
|
||||
indicator: [
|
||||
{ name: 'Brand', max: 100 },
|
||||
{ name: 'Content', max: 100 },
|
||||
{ name: 'Usability', max: 100 },
|
||||
{ name: 'Function', max: 100 }
|
||||
],
|
||||
center: ['25%', '40%'],
|
||||
radius: 80
|
||||
},
|
||||
{
|
||||
indicator: [
|
||||
{ name: 'Look', max: 100 },
|
||||
{ name: 'Photo', max: 100 },
|
||||
{ name: 'System', max: 100 },
|
||||
{ name: 'Performance', max: 100 },
|
||||
{ name: 'Screen', max: 100 }
|
||||
],
|
||||
radius: 80,
|
||||
center: ['50%', '60%']
|
||||
},
|
||||
{
|
||||
indicator: (() => {
|
||||
const res = [];
|
||||
for (let i = 1; i <= 12; i += 1) {
|
||||
res.push({ name: `${i}月`, max: 100 });
|
||||
}
|
||||
return res;
|
||||
})(),
|
||||
center: ['75%', '40%'],
|
||||
radius: 80
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
type: 'radar',
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
areaStyle: {},
|
||||
data: [
|
||||
{
|
||||
value: [60, 73, 85, 40],
|
||||
name: 'A Software'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'radar',
|
||||
radarIndex: 1,
|
||||
areaStyle: {},
|
||||
data: [
|
||||
{
|
||||
value: [85, 90, 90, 95, 95],
|
||||
name: 'A Phone'
|
||||
},
|
||||
{
|
||||
value: [95, 80, 95, 90, 93],
|
||||
name: 'Another Phone'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'radar',
|
||||
radarIndex: 2,
|
||||
areaStyle: {},
|
||||
data: [
|
||||
{
|
||||
name: 'Precipitation',
|
||||
value: [2.6, 5.9, 9.0, 26.4, 28.7, 70.7, 75.6, 82.2, 48.7, 18.8, 6.0, 2.3]
|
||||
},
|
||||
{
|
||||
name: 'Evaporation',
|
||||
value: [2.0, 4.9, 7.0, 23.2, 25.6, 76.7, 35.6, 62.2, 32.6, 20.0, 6.4, 3.3]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}) as Ref<ECOption>;
|
||||
const { domRef: radarRef } = useEcharts(radarOptions);
|
||||
|
||||
const gaugeOptions = ref<ECOption>({
|
||||
series: [
|
||||
{
|
||||
name: 'hour',
|
||||
type: 'gauge',
|
||||
startAngle: 90,
|
||||
endAngle: -270,
|
||||
min: 0,
|
||||
max: 12,
|
||||
splitNumber: 12,
|
||||
clockwise: true,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
width: 15,
|
||||
color: [[1, 'rgba(0,0,0,0.7)']],
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
shadowBlur: 15
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)',
|
||||
shadowBlur: 3,
|
||||
shadowOffsetX: 1,
|
||||
shadowOffsetY: 2
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
fontSize: 50,
|
||||
distance: 25,
|
||||
formatter(value) {
|
||||
if (value === 0) {
|
||||
return '';
|
||||
}
|
||||
return `${value}`;
|
||||
}
|
||||
},
|
||||
anchor: {
|
||||
show: true,
|
||||
icon: 'path://M532.8,70.8C532.8,70.8,532.8,70.8,532.8,70.8L532.8,70.8C532.7,70.8,532.8,70.8,532.8,70.8z M456.1,49.6c-2.2-6.2-8.1-10.6-15-10.6h-37.5v10.6h37.5l0,0c2.9,0,5.3,2.4,5.3,5.3c0,2.9-2.4,5.3-5.3,5.3v0h-22.5c-1.5,0.1-3,0.4-4.3,0.9c-4.5,1.6-8.1,5.2-9.7,9.8c-0.6,1.7-0.9,3.4-0.9,5.3v16h10.6v-16l0,0l0,0c0-2.7,2.1-5,4.7-5.3h10.3l10.4,21.2h11.8l-10.4-21.2h0c6.9,0,12.8-4.4,15-10.6c0.6-1.7,0.9-3.5,0.9-5.3C457,53,456.7,51.2,456.1,49.6z M388.9,92.1h11.3L381,39h-3.6h-11.3L346.8,92v0h11.3l3.9-10.7h7.3h7.7l3.9-10.6h-7.7h-7.3l7.7-21.2v0L388.9,92.1z M301,38.9h-10.6v53.1H301V70.8h28.4l3.7-10.6H301V38.9zM333.2,38.9v10.6v10.7v31.9h10.6V38.9H333.2z M249.5,81.4L249.5,81.4L249.5,81.4c-2.9,0-5.3-2.4-5.3-5.3h0V54.9h0l0,0c0-2.9,2.4-5.3,5.3-5.3l0,0l0,0h33.6l3.9-10.6h-37.5c-1.9,0-3.6,0.3-5.3,0.9c-4.5,1.6-8.1,5.2-9.7,9.7c-0.6,1.7-0.9,3.5-0.9,5.3l0,0v21.3c0,1.9,0.3,3.6,0.9,5.3c1.6,4.5,5.2,8.1,9.7,9.7c1.7,0.6,3.5,0.9,5.3,0.9h33.6l3.9-10.6H249.5z M176.8,38.9v10.6h49.6l3.9-10.6H176.8z M192.7,81.4L192.7,81.4L192.7,81.4c-2.9,0-5.3-2.4-5.3-5.3l0,0v-5.3h38.9l3.9-10.6h-53.4v10.6v5.3l0,0c0,1.9,0.3,3.6,0.9,5.3c1.6,4.5,5.2,8.1,9.7,9.7c1.7,0.6,3.4,0.9,5.3,0.9h23.4h10.2l3.9-10.6l0,0H192.7z M460.1,38.9v10.6h21.4v42.5h10.6V49.6h17.5l3.8-10.6H460.1z M541.6,68.2c-0.2,0.1-0.4,0.3-0.7,0.4C541.1,68.4,541.4,68.3,541.6,68.2L541.6,68.2z M554.3,60.2h-21.6v0l0,0c-2.9,0-5.3-2.4-5.3-5.3c0-2.9,2.4-5.3,5.3-5.3l0,0l0,0h33.6l3.8-10.6h-37.5l0,0c-6.9,0-12.8,4.4-15,10.6c-0.6,1.7-0.9,3.5-0.9,5.3c0,1.9,0.3,3.7,0.9,5.3c2.2,6.2,8.1,10.6,15,10.6h21.6l0,0c2.9,0,5.3,2.4,5.3,5.3c0,2.9-2.4,5.3-5.3,5.3l0,0h-37.5v10.6h37.5c6.9,0,12.8-4.4,15-10.6c0.6-1.7,0.9-3.5,0.9-5.3c0-1.9-0.3-3.7-0.9-5.3C567.2,64.6,561.3,60.2,554.3,60.2z',
|
||||
showAbove: false,
|
||||
offsetCenter: [0, '-35%'],
|
||||
size: 120,
|
||||
keepAspect: true,
|
||||
itemStyle: {
|
||||
color: '#707177'
|
||||
}
|
||||
},
|
||||
pointer: {
|
||||
icon: 'path://M2.9,0.7L2.9,0.7c1.4,0,2.6,1.2,2.6,2.6v115c0,1.4-1.2,2.6-2.6,2.6l0,0c-1.4,0-2.6-1.2-2.6-2.6V3.3C0.3,1.9,1.4,0.7,2.9,0.7z',
|
||||
width: 12,
|
||||
length: '55%',
|
||||
offsetCenter: [0, '8%'],
|
||||
itemStyle: {
|
||||
color: '#C0911F',
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)',
|
||||
shadowBlur: 8,
|
||||
shadowOffsetX: 2,
|
||||
shadowOffsetY: 4
|
||||
}
|
||||
},
|
||||
detail: {
|
||||
show: false
|
||||
},
|
||||
title: {
|
||||
offsetCenter: [0, '30%']
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'minute',
|
||||
type: 'gauge',
|
||||
startAngle: 90,
|
||||
endAngle: -270,
|
||||
min: 0,
|
||||
max: 60,
|
||||
clockwise: true,
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
splitLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
show: false
|
||||
},
|
||||
pointer: {
|
||||
icon: 'path://M2.9,0.7L2.9,0.7c1.4,0,2.6,1.2,2.6,2.6v115c0,1.4-1.2,2.6-2.6,2.6l0,0c-1.4,0-2.6-1.2-2.6-2.6V3.3C0.3,1.9,1.4,0.7,2.9,0.7z',
|
||||
width: 8,
|
||||
length: '70%',
|
||||
offsetCenter: [0, '8%'],
|
||||
itemStyle: {
|
||||
color: '#C0911F',
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)',
|
||||
shadowBlur: 8,
|
||||
shadowOffsetX: 2,
|
||||
shadowOffsetY: 4
|
||||
}
|
||||
},
|
||||
anchor: {
|
||||
show: true,
|
||||
size: 20,
|
||||
showAbove: false,
|
||||
itemStyle: {
|
||||
borderWidth: 15,
|
||||
borderColor: '#C0911F',
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)',
|
||||
shadowBlur: 8,
|
||||
shadowOffsetX: 2,
|
||||
shadowOffsetY: 4
|
||||
}
|
||||
},
|
||||
detail: {
|
||||
show: false
|
||||
},
|
||||
title: {
|
||||
offsetCenter: ['0%', '-40%']
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'second',
|
||||
type: 'gauge',
|
||||
startAngle: 90,
|
||||
endAngle: -270,
|
||||
min: 0,
|
||||
max: 60,
|
||||
animationEasingUpdate: 'bounceOut',
|
||||
clockwise: true,
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
splitLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
show: false
|
||||
},
|
||||
pointer: {
|
||||
icon: 'path://M2.9,0.7L2.9,0.7c1.4,0,2.6,1.2,2.6,2.6v115c0,1.4-1.2,2.6-2.6,2.6l0,0c-1.4,0-2.6-1.2-2.6-2.6V3.3C0.3,1.9,1.4,0.7,2.9,0.7z',
|
||||
width: 4,
|
||||
length: '85%',
|
||||
offsetCenter: [0, '8%'],
|
||||
itemStyle: {
|
||||
color: '#C0911F',
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)',
|
||||
shadowBlur: 8,
|
||||
shadowOffsetX: 2,
|
||||
shadowOffsetY: 4
|
||||
}
|
||||
},
|
||||
anchor: {
|
||||
show: true,
|
||||
size: 15,
|
||||
showAbove: true,
|
||||
itemStyle: {
|
||||
color: '#C0911F',
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)',
|
||||
shadowBlur: 8,
|
||||
shadowOffsetX: 2,
|
||||
shadowOffsetY: 4
|
||||
}
|
||||
},
|
||||
detail: {
|
||||
show: false
|
||||
},
|
||||
title: {
|
||||
offsetCenter: ['0%', '-40%']
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}) as Ref<ECOption>;
|
||||
|
||||
let intervalId: NodeJS.Timeout;
|
||||
const { domRef: gaugeRef } = useEcharts(gaugeOptions, chart => {
|
||||
intervalId = setInterval(() => {
|
||||
const date = new Date();
|
||||
const second = date.getSeconds();
|
||||
const minute = date.getMinutes() + second / 60;
|
||||
const hour = (date.getHours() % 12) + minute / 60;
|
||||
|
||||
chart.setOption({
|
||||
animationDurationUpdate: 300,
|
||||
series: [
|
||||
{
|
||||
name: 'hour',
|
||||
animation: hour !== 0,
|
||||
data: [{ value: hour }]
|
||||
},
|
||||
{
|
||||
name: 'minute',
|
||||
animation: minute !== 0,
|
||||
data: [{ value: minute }]
|
||||
},
|
||||
{
|
||||
animation: second !== 0,
|
||||
name: 'second',
|
||||
data: [{ value: second }]
|
||||
}
|
||||
]
|
||||
});
|
||||
}, 1000);
|
||||
});
|
||||
function clearClock() {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
clearClock();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,33 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<n-card title="文本复制" :bordered="false" class="h-full rounded-8px shadow-sm">
|
||||
<n-input-group>
|
||||
<n-input v-model:value="source" placeholder="请输入要复制的内容吧" />
|
||||
<n-button type="primary" @click="handleCopy">复制</n-button>
|
||||
</n-input-group>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
|
||||
const source = ref('');
|
||||
const { copy, isSupported } = useClipboard();
|
||||
|
||||
function handleCopy() {
|
||||
if (!isSupported) {
|
||||
window.$message?.error('您的浏览器不支持Clipboard API');
|
||||
return;
|
||||
}
|
||||
if (!source.value) {
|
||||
window.$message?.error('请输入要复制的内容');
|
||||
return;
|
||||
}
|
||||
copy(source.value);
|
||||
window.$message?.success(`复制成功:${source.value}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,50 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<n-card title="markdown插件" :bordered="false" class="rounded-8px shadow-sm">
|
||||
<div ref="domRef"></div>
|
||||
<template #footer>
|
||||
<github-link link="https://github.com/Vanessa219/vditor" />
|
||||
</template>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import Vditor from 'vditor';
|
||||
import 'vditor/dist/index.css';
|
||||
import { useThemeStore } from '@/store';
|
||||
|
||||
const theme = useThemeStore();
|
||||
|
||||
const vditor = ref<Vditor>();
|
||||
const domRef = ref<HTMLElement>();
|
||||
|
||||
function renderVditor() {
|
||||
if (!domRef.value) return;
|
||||
vditor.value = new Vditor(domRef.value, {
|
||||
minHeight: 400,
|
||||
theme: theme.darkMode ? 'dark' : 'classic',
|
||||
icon: 'material',
|
||||
cache: { enable: false }
|
||||
});
|
||||
}
|
||||
|
||||
const stopHandle = watch(
|
||||
() => theme.darkMode,
|
||||
newValue => {
|
||||
const themeMode = newValue ? 'dark' : 'classic';
|
||||
vditor.value?.setTheme(themeMode);
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
renderVditor();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopHandle();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,45 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<n-card title="富文本插件" :bordered="false" class="rounded-8px shadow-sm">
|
||||
<div ref="domRef" class="bg-white dark:bg-dark"></div>
|
||||
<template #footer>
|
||||
<github-link link="https://github.com/wangeditor-team/wangEditor" />
|
||||
</template>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import WangEditor from 'wangeditor';
|
||||
|
||||
const editor = ref<WangEditor>();
|
||||
const domRef = ref<HTMLElement>();
|
||||
|
||||
function renderWangEditor() {
|
||||
editor.value = new WangEditor(domRef.value);
|
||||
setEditorConfig();
|
||||
editor.value.create();
|
||||
}
|
||||
|
||||
function setEditorConfig() {
|
||||
if (editor.value?.config?.zIndex) {
|
||||
editor.value.config.zIndex = 10;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
renderWangEditor();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.w-e-toolbar) {
|
||||
background: inherit !important;
|
||||
border-color: #999 !important;
|
||||
}
|
||||
:deep(.w-e-text-container) {
|
||||
background: inherit;
|
||||
border-color: #999 !important;
|
||||
}
|
||||
</style>
|
@ -1,32 +0,0 @@
|
||||
export const icons = [
|
||||
'mdi:emoticon',
|
||||
'mdi:ab-testing',
|
||||
'ph:alarm',
|
||||
'ph:android-logo',
|
||||
'ph:align-bottom',
|
||||
'ph:archive-box-light',
|
||||
'uil:basketball',
|
||||
'uil:brightness-plus',
|
||||
'uil:capture',
|
||||
'mdi:apps-box',
|
||||
'mdi:alert',
|
||||
'mdi:airballoon',
|
||||
'mdi:airplane-edit',
|
||||
'mdi:alpha-f-box-outline',
|
||||
'mdi:arm-flex-outline',
|
||||
'ic:baseline-10mp',
|
||||
'ic:baseline-access-time',
|
||||
'ic:baseline-brightness-4',
|
||||
'ic:baseline-brightness-5',
|
||||
'ic:baseline-credit-card',
|
||||
'ic:baseline-filter-1',
|
||||
'ic:baseline-filter-2',
|
||||
'ic:baseline-filter-3',
|
||||
'ic:baseline-filter-4',
|
||||
'ic:baseline-filter-5',
|
||||
'ic:baseline-filter-6',
|
||||
'ic:baseline-filter-7',
|
||||
'ic:baseline-filter-8',
|
||||
'ic:baseline-filter-9',
|
||||
'ic:baseline-filter-9-plus'
|
||||
];
|
@ -1,51 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<n-card title="Icon组件示例" :bordered="false" class="rounded-8px shadow-sm">
|
||||
<div class="grid grid-cols-10">
|
||||
<template v-for="item in icons" :key="item">
|
||||
<div class="mt-5px flex-x-center">
|
||||
<svg-icon :icon="item" class="text-30px" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="mt-50px">
|
||||
<h1 class="mb-20px text-18px font-500">Icon图标选择器</h1>
|
||||
<icon-select v-model:value="selectValue" :icons="icons" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<web-site-link label="iconify地址:" link="https://icones.js.org/" class="mt-10px" />
|
||||
</template>
|
||||
</n-card>
|
||||
<n-card title="自定义图标示例" :bordered="false" class="mt-10px rounded-8px shadow-sm">
|
||||
<div class="pb-12px text-16px">
|
||||
在src/assets/svg-icon文件夹下的svg文件,通过在template里面以 icon-local-{文件名} 直接渲染,
|
||||
其中icon-local为.env文件里的 VITE_ICON_LOCAL_PREFIX
|
||||
</div>
|
||||
<div class="grid grid-cols-10">
|
||||
<div class="mt-5px flex-x-center">
|
||||
<icon-local-activity class="text-40px text-success" />
|
||||
</div>
|
||||
<div class="mt-5px flex-x-center">
|
||||
<icon-local-cast class="text-20px text-error" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-12px text-16px">通过SvgIcon组件动态渲染, 菜单通过meta的localIcon属性渲染自定义图标</div>
|
||||
<div class="grid grid-cols-10">
|
||||
<div v-for="(fileName, index) in localIcons" :key="index" class="mt-5px flex-x-center">
|
||||
<svg-icon :local-icon="fileName" class="text-30px text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { icons } from './icons';
|
||||
|
||||
const selectValue = ref('');
|
||||
|
||||
const localIcons = ['custom-icon', 'activity', 'at-sign', 'cast', 'chrome', 'copy', 'wind'];
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,30 +0,0 @@
|
||||
<template>
|
||||
<div ref="domRef" class="w-full h-full"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useScriptTag } from '@vueuse/core';
|
||||
import { BAIDU_MAP_SDK_URL } from '@/config';
|
||||
|
||||
defineOptions({ name: 'BaiduMap' });
|
||||
|
||||
const { load } = useScriptTag(BAIDU_MAP_SDK_URL);
|
||||
|
||||
const domRef = ref<HTMLDivElement>();
|
||||
|
||||
async function renderMap() {
|
||||
await load(true);
|
||||
if (!domRef.value) return;
|
||||
const map = new BMap.Map(domRef.value);
|
||||
const point = new BMap.Point(114.05834626586915, 22.546789983033168);
|
||||
map.centerAndZoom(point, 15);
|
||||
map.enableScrollWheelZoom();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
renderMap();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,32 +0,0 @@
|
||||
<template>
|
||||
<div ref="domRef" class="w-full h-full"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useScriptTag } from '@vueuse/core';
|
||||
import { AMAP_SDK_URL } from '@/config';
|
||||
|
||||
defineOptions({ name: 'GaodeMap' });
|
||||
|
||||
const { load } = useScriptTag(AMAP_SDK_URL);
|
||||
|
||||
const domRef = ref<HTMLDivElement>();
|
||||
|
||||
async function renderMap() {
|
||||
await load(true);
|
||||
if (!domRef.value) return;
|
||||
const map = new AMap.Map(domRef.value, {
|
||||
zoom: 11,
|
||||
center: [114.05834626586915, 22.546789983033168],
|
||||
viewMode: '3D'
|
||||
});
|
||||
map.getCenter();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
renderMap();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,5 +0,0 @@
|
||||
import BaiduMap from './baidu-map.vue';
|
||||
import GaodeMap from './gaode-map.vue';
|
||||
import TencentMap from './tencent-map.vue';
|
||||
|
||||
export { BaiduMap, GaodeMap, TencentMap };
|
@ -1,32 +0,0 @@
|
||||
<template>
|
||||
<div ref="domRef" class="w-full h-full"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useScriptTag } from '@vueuse/core';
|
||||
import { TENCENT_MAP_SDK_URL } from '@/config';
|
||||
|
||||
defineOptions({ name: 'TencentMap' });
|
||||
|
||||
const { load } = useScriptTag(TENCENT_MAP_SDK_URL);
|
||||
|
||||
const domRef = ref<HTMLDivElement | null>(null);
|
||||
|
||||
async function renderMap() {
|
||||
await load(true);
|
||||
if (!domRef.value) return;
|
||||
// eslint-disable-next-line no-new
|
||||
new TMap.Map(domRef.value, {
|
||||
center: new TMap.LatLng(39.98412, 116.307484),
|
||||
zoom: 11,
|
||||
viewMode: '3D'
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
renderMap();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,30 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<n-card title="地图插件" :bordered="false" class="h-full rounded-8px shadow-sm" content-style="overflow:hidden">
|
||||
<n-tabs type="line" class="flex-col-stretch h-full" pane-class="flex-1-hidden">
|
||||
<n-tab-pane v-for="item in maps" :key="item.id" :name="item.id" :tab="item.label">
|
||||
<component :is="item.component" />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Component } from 'vue';
|
||||
import { BaiduMap, GaodeMap, TencentMap } from './components';
|
||||
|
||||
interface Map {
|
||||
id: string;
|
||||
label: string;
|
||||
component: Component;
|
||||
}
|
||||
|
||||
const maps: Map[] = [
|
||||
{ id: 'gaode', label: '高德地图', component: GaodeMap },
|
||||
{ id: 'tencent', label: '腾讯地图', component: TencentMap },
|
||||
{ id: 'baidu', label: '百度地图', component: BaiduMap }
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,39 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<n-card title="打印" :bordered="false" class="rounded-8px shadow-sm">
|
||||
<n-button type="primary" class="mr-10px" @click="printTable">打印表格</n-button>
|
||||
<n-button type="primary" @click="printImage">打印图片</n-button>
|
||||
<template #footer>
|
||||
<github-link label="printJS:" link="https://github.com/crabbly/Print.js" class="mt-10px" />
|
||||
</template>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import printJS from 'print-js';
|
||||
|
||||
function printTable() {
|
||||
printJS({
|
||||
printable: [
|
||||
{ name: 'soybean', wechat: 'honghuangdc', remark: '欢迎来技术交流' },
|
||||
{ name: 'soybean', wechat: 'honghuangdc', remark: '欢迎来技术交流' }
|
||||
],
|
||||
properties: ['name', 'wechat', 'remark'],
|
||||
type: 'json'
|
||||
});
|
||||
}
|
||||
function printImage() {
|
||||
printJS({
|
||||
printable: [
|
||||
'https://i.loli.net/2021/11/24/1J6REWXiHomU2kM.jpg',
|
||||
'https://i.loli.net/2021/11/24/1J6REWXiHomU2kM.jpg'
|
||||
],
|
||||
type: 'image',
|
||||
header: 'Multiple Images',
|
||||
imageStyle: 'width:100%;'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,110 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-card title="Swiper插件" :bordered="false" class="rounded-8px shadow-sm">
|
||||
<n-space :vertical="true">
|
||||
<github-link link="https://github.com/nolimits4web/swiper" />
|
||||
<web-site-link label="vue3版文档地址:" link="https://swiperjs.com/vue" />
|
||||
<web-site-link label="插件demo地址:" link="https://swiperjs.com/demos" />
|
||||
</n-space>
|
||||
<n-space :vertical="true">
|
||||
<div v-for="item in swiperExample" :key="item.id">
|
||||
<h3 class="py-24px text-24px font-bold">{{ item.label }}</h3>
|
||||
<swiper v-bind="item.options">
|
||||
<swiper-slide v-for="i in 5" :key="i">
|
||||
<div class="flex-center h-240px border-1px border-#999 text-18px font-bold">Slide{{ i }}</div>
|
||||
</swiper-slide>
|
||||
</swiper>
|
||||
</div>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SwiperCore from 'swiper';
|
||||
import { Navigation, Pagination } from 'swiper/modules';
|
||||
import { Swiper, SwiperSlide } from 'swiper/vue';
|
||||
import type { SwiperOptions } from 'swiper/types';
|
||||
|
||||
type SwiperExampleOptions = Pick<
|
||||
SwiperOptions,
|
||||
'navigation' | 'pagination' | 'scrollbar' | 'slidesPerView' | 'slidesPerGroup' | 'spaceBetween' | 'direction' | 'loop'
|
||||
>;
|
||||
|
||||
interface SwiperExample {
|
||||
id: number;
|
||||
label: string;
|
||||
options: Partial<SwiperExampleOptions>;
|
||||
}
|
||||
|
||||
SwiperCore.use([Navigation, Pagination]);
|
||||
|
||||
const swiperExample: SwiperExample[] = [
|
||||
{ id: 0, label: 'Default', options: {} },
|
||||
{
|
||||
id: 1,
|
||||
label: 'Navigation',
|
||||
options: {
|
||||
navigation: true
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: 'Pagination',
|
||||
options: {
|
||||
pagination: true
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: 'Pagination dynamic',
|
||||
options: {
|
||||
pagination: { dynamicBullets: true }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
label: 'Pagination progress',
|
||||
options: {
|
||||
navigation: true,
|
||||
pagination: {
|
||||
type: 'progressbar'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
label: 'Pagination fraction',
|
||||
options: {
|
||||
navigation: true,
|
||||
pagination: {
|
||||
type: 'fraction'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
label: 'Slides per view',
|
||||
options: {
|
||||
pagination: {
|
||||
clickable: true
|
||||
},
|
||||
slidesPerView: 3,
|
||||
spaceBetween: 30
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
label: 'Infinite loop',
|
||||
options: {
|
||||
navigation: true,
|
||||
pagination: {
|
||||
clickable: true
|
||||
},
|
||||
loop: true
|
||||
}
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,40 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<n-card title="视频播放器插件" :bordered="false" class="h-full rounded-8px shadow-sm">
|
||||
<div ref="domRef" class=""></div>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import Player from 'xgplayer';
|
||||
import 'xgplayer/dist/index.min.css';
|
||||
|
||||
const domRef = ref<HTMLElement>();
|
||||
const player = ref<Player>();
|
||||
|
||||
function renderXgPlayer() {
|
||||
if (!domRef.value) return;
|
||||
const url = 'https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/byted-player-videos/1.0.0/xgplayer-demo.mp4';
|
||||
player.value = new Player({
|
||||
el: domRef.value,
|
||||
url,
|
||||
playbackRate: [0.5, 0.75, 1, 1.5, 2],
|
||||
fluid: true
|
||||
});
|
||||
}
|
||||
function destroyXgPlayer() {
|
||||
player.value?.destroy();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
renderXgPlayer();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
destroyXgPlayer();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
7
src/views/user-center/index.vue
Normal file
7
src/views/user-center/index.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<LookForward />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
Reference in New Issue
Block a user