init
This commit is contained in:
7
src/views/_builtin/403/index.vue
Normal file
7
src/views/_builtin/403/index.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<ExceptionBase type="403" />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
7
src/views/_builtin/404/index.vue
Normal file
7
src/views/_builtin/404/index.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<ExceptionBase type="404" />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
7
src/views/_builtin/500/index.vue
Normal file
7
src/views/_builtin/500/index.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<ExceptionBase type="500" />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
15
src/views/_builtin/iframe-page/[url].vue
Normal file
15
src/views/_builtin/iframe-page/[url].vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
url: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<iframe id="iframePage" class="size-full" :src="url"></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
90
src/views/_builtin/login/index.vue
Normal file
90
src/views/_builtin/login/index.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { Component } from 'vue';
|
||||
import { getPaletteColorByNumber, mixColor } from '@sa/color';
|
||||
import { loginModuleRecord } from '@/constants/app';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { $t } from '@/locales';
|
||||
import PwdLogin from './modules/pwd-login.vue';
|
||||
import CodeLogin from './modules/code-login.vue';
|
||||
import Register from './modules/register.vue';
|
||||
import ResetPwd from './modules/reset-pwd.vue';
|
||||
import BindWechat from './modules/bind-wechat.vue';
|
||||
|
||||
interface Props {
|
||||
/** The login module */
|
||||
module?: UnionKey.LoginModule;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const appStore = useAppStore();
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
interface LoginModule {
|
||||
label: App.I18n.I18nKey;
|
||||
component: Component;
|
||||
}
|
||||
|
||||
const moduleMap: Record<UnionKey.LoginModule, LoginModule> = {
|
||||
'pwd-login': { label: loginModuleRecord['pwd-login'], component: PwdLogin },
|
||||
'code-login': { label: loginModuleRecord['code-login'], component: CodeLogin },
|
||||
register: { label: loginModuleRecord.register, component: Register },
|
||||
'reset-pwd': { label: loginModuleRecord['reset-pwd'], component: ResetPwd },
|
||||
'bind-wechat': { label: loginModuleRecord['bind-wechat'], component: BindWechat }
|
||||
};
|
||||
|
||||
const activeModule = computed(() => moduleMap[props.module || 'pwd-login']);
|
||||
|
||||
const bgThemeColor = computed(() =>
|
||||
themeStore.darkMode ? getPaletteColorByNumber(themeStore.themeColor, 600) : themeStore.themeColor
|
||||
);
|
||||
|
||||
const bgColor = computed(() => {
|
||||
const COLOR_WHITE = '#ffffff';
|
||||
|
||||
const ratio = themeStore.darkMode ? 0.5 : 0.2;
|
||||
|
||||
return mixColor(COLOR_WHITE, themeStore.themeColor, ratio);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative size-full flex-center overflow-hidden" :style="{ backgroundColor: bgColor }">
|
||||
<WaveBg :theme-color="bgThemeColor" />
|
||||
<NCard :bordered="false" class="relative z-4 w-auto rd-12px">
|
||||
<div class="w-400px lt-sm:w-300px">
|
||||
<header class="flex-y-center justify-between">
|
||||
<SystemLogo class="text-64px text-primary lt-sm:text-48px" />
|
||||
<h3 class="text-28px text-primary font-500 lt-sm:text-22px">{{ $t('system.title') }}</h3>
|
||||
<div class="i-flex-col">
|
||||
<ThemeSchemaSwitch
|
||||
:theme-schema="themeStore.themeScheme"
|
||||
:show-tooltip="false"
|
||||
class="text-20px lt-sm:text-18px"
|
||||
@switch="themeStore.toggleThemeScheme"
|
||||
/>
|
||||
<LangSwitch
|
||||
v-if="themeStore.header.multilingual.visible"
|
||||
: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>
|
||||
11
src/views/_builtin/login/modules/bind-wechat.vue
Normal file
11
src/views/_builtin/login/modules/bind-wechat.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'BindWechat'
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
66
src/views/_builtin/login/modules/code-login.vue
Normal file
66
src/views/_builtin/login/modules/code-login.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive } from 'vue';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||
import { useCaptcha } from '@/hooks/business/captcha';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'CodeLogin'
|
||||
});
|
||||
|
||||
const { toggleLoginModule } = useRouterPush();
|
||||
const { formRef, validate } = useNaiveForm();
|
||||
const { label, isCounting, loading, getCaptcha } = useCaptcha();
|
||||
|
||||
interface FormModel {
|
||||
phone: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
const model: FormModel = reactive({
|
||||
phone: '',
|
||||
code: ''
|
||||
});
|
||||
|
||||
const rules = computed<Record<keyof FormModel, App.Global.FormRule[]>>(() => {
|
||||
const { formRules } = useFormRules();
|
||||
|
||||
return {
|
||||
phone: formRules.phone,
|
||||
code: formRules.code
|
||||
};
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
// request
|
||||
window.$message?.success($t('page.login.common.validateSuccess'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit">
|
||||
<NFormItem path="phone">
|
||||
<NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
|
||||
</NFormItem>
|
||||
<NFormItem path="code">
|
||||
<div class="w-full flex-y-center gap-16px">
|
||||
<NInput v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
|
||||
<NButton size="large" :disabled="isCounting" :loading="loading" @click="getCaptcha(model.phone)">
|
||||
{{ label }}
|
||||
</NButton>
|
||||
</div>
|
||||
</NFormItem>
|
||||
<NSpace vertical :size="18" class="w-full">
|
||||
<NButton type="primary" size="large" round block @click="handleSubmit">
|
||||
{{ $t('common.confirm') }}
|
||||
</NButton>
|
||||
<NButton size="large" round block @click="toggleLoginModule('pwd-login')">
|
||||
{{ $t('page.login.common.back') }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NForm>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
118
src/views/_builtin/login/modules/pwd-login.vue
Normal file
118
src/views/_builtin/login/modules/pwd-login.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive } from 'vue';
|
||||
import { loginModuleRecord } from '@/constants/app';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'PwdLogin'
|
||||
});
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const { toggleLoginModule } = useRouterPush();
|
||||
const { formRef, validate } = useNaiveForm();
|
||||
|
||||
interface FormModel {
|
||||
userName: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const model: FormModel = reactive({
|
||||
userName: 'superadmin',
|
||||
password: 'admin123'
|
||||
});
|
||||
|
||||
const rules = computed<Record<keyof FormModel, App.Global.FormRule[]>>(() => {
|
||||
// inside computed to make locale reactive, if not apply i18n, you can define it without computed
|
||||
const { formRules } = useFormRules();
|
||||
|
||||
return {
|
||||
userName: formRules.userName,
|
||||
password: formRules.pwd
|
||||
};
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
await authStore.login(model.userName, model.password);
|
||||
}
|
||||
|
||||
type AccountKey = 'super' | 'admin' | 'user';
|
||||
|
||||
interface Account {
|
||||
key: AccountKey;
|
||||
label: string;
|
||||
userName: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const accounts = computed<Account[]>(() => [
|
||||
{
|
||||
key: 'super',
|
||||
label: $t('page.login.pwdLogin.superAdmin'),
|
||||
userName: 'Super',
|
||||
password: '123456'
|
||||
},
|
||||
{
|
||||
key: 'admin',
|
||||
label: $t('page.login.pwdLogin.admin'),
|
||||
userName: 'Admin',
|
||||
password: '123456'
|
||||
},
|
||||
{
|
||||
key: 'user',
|
||||
label: $t('page.login.pwdLogin.user'),
|
||||
userName: 'User',
|
||||
password: '123456'
|
||||
}
|
||||
]);
|
||||
|
||||
async function handleAccountLogin(account: Account) {
|
||||
await authStore.login(account.userName, account.password);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit">
|
||||
<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"
|
||||
show-password-on="click"
|
||||
:placeholder="$t('page.login.common.passwordPlaceholder')"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NSpace vertical :size="24">
|
||||
<div class="flex-y-center justify-between">
|
||||
<NCheckbox>{{ $t('page.login.pwdLogin.rememberMe') }}</NCheckbox>
|
||||
<NButton quaternary @click="toggleLoginModule('reset-pwd')">
|
||||
{{ $t('page.login.pwdLogin.forgetPassword') }}
|
||||
</NButton>
|
||||
</div>
|
||||
<NButton type="primary" size="large" round block :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>
|
||||
<NDivider class="text-14px text-#666 !m-0">{{ $t('page.login.pwdLogin.otherAccountLogin') }}</NDivider>
|
||||
<div class="flex-center gap-12px">
|
||||
<NButton v-for="item in accounts" :key="item.key" type="primary" @click="handleAccountLogin(item)">
|
||||
{{ item.label }}
|
||||
</NButton>
|
||||
</div>
|
||||
</NSpace>
|
||||
</NForm>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
88
src/views/_builtin/login/modules/register.vue
Normal file
88
src/views/_builtin/login/modules/register.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive } from 'vue';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||
import { useCaptcha } from '@/hooks/business/captcha';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'Register'
|
||||
});
|
||||
|
||||
const { toggleLoginModule } = useRouterPush();
|
||||
const { formRef, validate } = useNaiveForm();
|
||||
const { label, isCounting, loading, getCaptcha } = useCaptcha();
|
||||
|
||||
interface FormModel {
|
||||
phone: string;
|
||||
code: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
|
||||
const model: FormModel = reactive({
|
||||
phone: '',
|
||||
code: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
});
|
||||
|
||||
const rules = computed<Record<keyof FormModel, App.Global.FormRule[]>>(() => {
|
||||
const { formRules, createConfirmPwdRule } = useFormRules();
|
||||
|
||||
return {
|
||||
phone: formRules.phone,
|
||||
code: formRules.code,
|
||||
password: formRules.pwd,
|
||||
confirmPassword: createConfirmPwdRule(model.password)
|
||||
};
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
// request to register
|
||||
window.$message?.success($t('page.login.common.validateSuccess'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit">
|
||||
<NFormItem path="phone">
|
||||
<NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
|
||||
</NFormItem>
|
||||
<NFormItem path="code">
|
||||
<div class="w-full flex-y-center gap-16px">
|
||||
<NInput v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
|
||||
<NButton size="large" :disabled="isCounting" :loading="loading" @click="getCaptcha(model.phone)">
|
||||
{{ label }}
|
||||
</NButton>
|
||||
</div>
|
||||
</NFormItem>
|
||||
<NFormItem path="password">
|
||||
<NInput
|
||||
v-model:value="model.password"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
:placeholder="$t('page.login.common.passwordPlaceholder')"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem path="confirmPassword">
|
||||
<NInput
|
||||
v-model:value="model.confirmPassword"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
:placeholder="$t('page.login.common.confirmPasswordPlaceholder')"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NSpace vertical :size="18" class="w-full">
|
||||
<NButton type="primary" size="large" round block @click="handleSubmit">
|
||||
{{ $t('common.confirm') }}
|
||||
</NButton>
|
||||
<NButton size="large" round block @click="toggleLoginModule('pwd-login')">
|
||||
{{ $t('page.login.common.back') }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NForm>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
82
src/views/_builtin/login/modules/reset-pwd.vue
Normal file
82
src/views/_builtin/login/modules/reset-pwd.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive } from 'vue';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'ResetPwd'
|
||||
});
|
||||
|
||||
const { toggleLoginModule } = useRouterPush();
|
||||
const { formRef, validate } = useNaiveForm();
|
||||
|
||||
interface FormModel {
|
||||
phone: string;
|
||||
code: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
|
||||
const model: FormModel = reactive({
|
||||
phone: '',
|
||||
code: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
});
|
||||
|
||||
type RuleRecord = Partial<Record<keyof FormModel, App.Global.FormRule[]>>;
|
||||
|
||||
const rules = computed<RuleRecord>(() => {
|
||||
const { formRules, createConfirmPwdRule } = useFormRules();
|
||||
|
||||
return {
|
||||
phone: formRules.phone,
|
||||
password: formRules.pwd,
|
||||
confirmPassword: createConfirmPwdRule(model.password)
|
||||
};
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
// request to reset password
|
||||
window.$message?.success($t('page.login.common.validateSuccess'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit">
|
||||
<NFormItem path="phone">
|
||||
<NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
|
||||
</NFormItem>
|
||||
<NFormItem path="code">
|
||||
<NInput v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
|
||||
</NFormItem>
|
||||
<NFormItem path="password">
|
||||
<NInput
|
||||
v-model:value="model.password"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
:placeholder="$t('page.login.common.passwordPlaceholder')"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem path="confirmPassword">
|
||||
<NInput
|
||||
v-model:value="model.confirmPassword"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
:placeholder="$t('page.login.common.confirmPasswordPlaceholder')"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NSpace vertical :size="18" class="w-full">
|
||||
<NButton type="primary" size="large" round block @click="handleSubmit">
|
||||
{{ $t('common.confirm') }}
|
||||
</NButton>
|
||||
<NButton size="large" round block @click="toggleLoginModule('pwd-login')">
|
||||
{{ $t('page.login.common.back') }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NForm>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
117
src/views/deposit/fiat/index.vue
Normal file
117
src/views/deposit/fiat/index.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<script lang="ts" setup>
|
||||
import { h, ref, useTemplateRef } from 'vue';
|
||||
import { NInputNumber, useDialog, useMessage } from 'naive-ui';
|
||||
import { client, safeClient } from '@/service/api';
|
||||
import { DepositTypeEnum } from '@/enum';
|
||||
import type { TableBaseColumns, TableFetchData, TableInst } from '@/components/table';
|
||||
|
||||
const dialog = useDialog();
|
||||
const message = useMessage();
|
||||
const tableInst = useTemplateRef<TableInst>('tableInst');
|
||||
|
||||
const fetchData: TableFetchData = () => {
|
||||
return safeClient(() =>
|
||||
client.api.admin.deposit.pending.get({
|
||||
query: {}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const columns: TableBaseColumns = [
|
||||
{
|
||||
title: 'ID',
|
||||
key: 'id'
|
||||
},
|
||||
{
|
||||
title: '用户ID',
|
||||
key: 'userId'
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
key: 'user.email'
|
||||
},
|
||||
{
|
||||
title: '资产代码',
|
||||
key: 'assetCode'
|
||||
},
|
||||
{
|
||||
title: '充值类型',
|
||||
key: 'depositType',
|
||||
render(row) {
|
||||
return DepositTypeEnum[row.depositType as keyof typeof DepositTypeEnum];
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '金额',
|
||||
key: 'amount'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
fixed: 'right',
|
||||
key: 'operation',
|
||||
operations: (row: any) => [
|
||||
{
|
||||
contentText: '通过',
|
||||
type: 'primary',
|
||||
strong: true,
|
||||
secondary: true,
|
||||
size: 'small',
|
||||
onClick: async () => {
|
||||
const amount = ref<string>(row.amount);
|
||||
dialog.create({
|
||||
title: '通过充值',
|
||||
positiveText: '是',
|
||||
negativeText: '否',
|
||||
content: () =>
|
||||
h(NInputNumber, {
|
||||
value: Number(amount.value),
|
||||
'onUpdate:value': value => {
|
||||
amount.value = String(value);
|
||||
},
|
||||
placeholder: '请输入实际充值金额'
|
||||
}),
|
||||
onPositiveClick: async () => {
|
||||
await safeClient(() =>
|
||||
client.api.admin.deposit.approve({ orderId: row.id as string }).post({
|
||||
actualAmount: amount.value
|
||||
})
|
||||
);
|
||||
tableInst.value?.reload();
|
||||
message.success('充值通过成功');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
contentText: '拒绝',
|
||||
type: 'error',
|
||||
ghost: true,
|
||||
size: 'small',
|
||||
onClick: async () => {
|
||||
dialog.create({
|
||||
title: '提示',
|
||||
positiveText: '是',
|
||||
negativeText: '否',
|
||||
content: '确认拒绝该充值请求吗?',
|
||||
onPositiveClick: async () => {
|
||||
safeClient(() =>
|
||||
client.api.admin.deposit.reject({ orderId: row.id as string }).post({
|
||||
reviewNote: '管理员拒绝充值'
|
||||
})
|
||||
);
|
||||
tableInst.value?.reload();
|
||||
message.success('充值拒绝成功');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TableBase ref="tableInst" :columns="columns" :fetch-data="fetchData" />
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped></style>
|
||||
46
src/views/home/index.vue
Normal file
46
src/views/home/index.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import HeaderBanner from './modules/header-banner.vue';
|
||||
import CardData from './modules/card-data.vue';
|
||||
import LineChart from './modules/line-chart.vue';
|
||||
import PieChart from './modules/pie-chart.vue';
|
||||
import ProjectNews from './modules/project-news.vue';
|
||||
import CreativityBanner from './modules/creativity-banner.vue';
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const gap = computed(() => (appStore.isMobile ? 0 : 16));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<NAlert :title="$t('common.tip')" type="warning">
|
||||
{{ $t('page.home.branchDesc') }}
|
||||
</NAlert>
|
||||
<HeaderBanner />
|
||||
<CardData />
|
||||
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive>
|
||||
<NGi span="24 s:24 m:14">
|
||||
<NCard :bordered="false" class="card-wrapper">
|
||||
<LineChart />
|
||||
</NCard>
|
||||
</NGi>
|
||||
<NGi span="24 s:24 m:10">
|
||||
<NCard :bordered="false" class="card-wrapper">
|
||||
<PieChart />
|
||||
</NCard>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive>
|
||||
<NGi span="24 s:24 m:14">
|
||||
<ProjectNews />
|
||||
</NGi>
|
||||
<NGi span="24 s:24 m:10">
|
||||
<CreativityBanner />
|
||||
</NGi>
|
||||
</NGrid>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
115
src/views/home/modules/card-data.vue
Normal file
115
src/views/home/modules/card-data.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { createReusableTemplate } from '@vueuse/core';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'CardData'
|
||||
});
|
||||
|
||||
interface CardData {
|
||||
key: string;
|
||||
title: string;
|
||||
value: number;
|
||||
unit: string;
|
||||
color: {
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const cardData = computed<CardData[]>(() => [
|
||||
{
|
||||
key: 'visitCount',
|
||||
title: $t('page.home.visitCount'),
|
||||
value: 9725,
|
||||
unit: '',
|
||||
color: {
|
||||
start: '#ec4786',
|
||||
end: '#b955a4'
|
||||
},
|
||||
icon: 'ant-design:bar-chart-outlined'
|
||||
},
|
||||
{
|
||||
key: 'turnover',
|
||||
title: $t('page.home.turnover'),
|
||||
value: 1026,
|
||||
unit: '$',
|
||||
color: {
|
||||
start: '#865ec0',
|
||||
end: '#5144b4'
|
||||
},
|
||||
icon: 'ant-design:money-collect-outlined'
|
||||
},
|
||||
{
|
||||
key: 'downloadCount',
|
||||
title: $t('page.home.downloadCount'),
|
||||
value: 970925,
|
||||
unit: '',
|
||||
color: {
|
||||
start: '#56cdf3',
|
||||
end: '#719de3'
|
||||
},
|
||||
icon: 'carbon:document-download'
|
||||
},
|
||||
{
|
||||
key: 'dealCount',
|
||||
title: $t('page.home.dealCount'),
|
||||
value: 9527,
|
||||
unit: '',
|
||||
color: {
|
||||
start: '#fcbc25',
|
||||
end: '#f68057'
|
||||
},
|
||||
icon: 'ant-design:trademark-circle-outlined'
|
||||
}
|
||||
]);
|
||||
|
||||
interface GradientBgProps {
|
||||
gradientColor: string;
|
||||
}
|
||||
|
||||
const [DefineGradientBg, GradientBg] = createReusableTemplate<GradientBgProps>();
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
function getGradientColor(color: CardData['color']) {
|
||||
return `linear-gradient(to bottom right, ${color.start}, ${color.end})`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NCard :bordered="false" size="small" class="card-wrapper">
|
||||
<!-- define component start: GradientBg -->
|
||||
<DefineGradientBg v-slot="{ $slots, gradientColor }">
|
||||
<div
|
||||
class="px-16px pb-4px pt-8px text-white"
|
||||
:style="{ backgroundImage: gradientColor, borderRadius: themeStore.themeRadius + 'px' }"
|
||||
>
|
||||
<component :is="$slots.default" />
|
||||
</div>
|
||||
</DefineGradientBg>
|
||||
<!-- define component end: GradientBg -->
|
||||
|
||||
<NGrid cols="s:1 m:2 l:4" responsive="screen" :x-gap="16" :y-gap="16">
|
||||
<NGi v-for="item in cardData" :key="item.key">
|
||||
<GradientBg :gradient-color="getGradientColor(item.color)" class="flex-1">
|
||||
<h3 class="text-16px">{{ item.title }}</h3>
|
||||
<div class="flex justify-between pt-12px">
|
||||
<SvgIcon :icon="item.icon" class="text-32px" />
|
||||
<CountTo
|
||||
:prefix="item.unit"
|
||||
:start-value="1"
|
||||
:end-value="item.value"
|
||||
class="text-30px text-white dark:text-dark"
|
||||
/>
|
||||
</div>
|
||||
</GradientBg>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
</NCard>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
17
src/views/home/modules/creativity-banner.vue
Normal file
17
src/views/home/modules/creativity-banner.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'CreativityBanner'
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NCard :title="$t('page.home.creativity')" :bordered="false" size="small" class="h-full card-wrapper">
|
||||
<div class="h-full flex-center">
|
||||
<icon-local-banner class="text-400px text-primary sm:text-320px" />
|
||||
</div>
|
||||
</NCard>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
66
src/views/home/modules/header-banner.vue
Normal file
66
src/views/home/modules/header-banner.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'HeaderBanner'
|
||||
});
|
||||
|
||||
const appStore = useAppStore();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const gap = computed(() => (appStore.isMobile ? 0 : 16));
|
||||
|
||||
interface StatisticData {
|
||||
id: number;
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const statisticData = computed<StatisticData[]>(() => [
|
||||
{
|
||||
id: 0,
|
||||
label: $t('page.home.projectCount'),
|
||||
value: '25'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
label: $t('page.home.todo'),
|
||||
value: '4/16'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: $t('page.home.message'),
|
||||
value: '12'
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NCard :bordered="false" class="card-wrapper">
|
||||
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive>
|
||||
<NGi span="24 s:24 m:18">
|
||||
<div class="flex-y-center">
|
||||
<div class="size-72px shrink-0 overflow-hidden rd-1/2">
|
||||
<img src="@/assets/imgs/soybean.jpg" class="size-full" />
|
||||
</div>
|
||||
<div class="pl-12px">
|
||||
<h3 class="text-18px font-semibold">
|
||||
{{ $t('page.home.greeting', { userName: authStore.userInfo.username }) }}
|
||||
</h3>
|
||||
<p class="text-#999 leading-30px">{{ $t('page.home.weatherDesc') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</NGi>
|
||||
<NGi span="24 s:24 m:6">
|
||||
<NSpace :size="24" justify="end">
|
||||
<NStatistic v-for="item in statisticData" :key="item.id" class="whitespace-nowrap" v-bind="item" />
|
||||
</NSpace>
|
||||
</NGi>
|
||||
</NGrid>
|
||||
</NCard>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
152
src/views/home/modules/line-chart.vue
Normal file
152
src/views/home/modules/line-chart.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<script setup lang="ts">
|
||||
import { watch } from 'vue';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useEcharts } from '@/hooks/common/echarts';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'LineChart'
|
||||
});
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const { domRef, updateOptions } = useEcharts(() => ({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
label: {
|
||||
backgroundColor: '#6a7985'
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: [$t('page.home.downloadCount'), $t('page.home.registerCount')],
|
||||
top: '0'
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
top: '15%'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: [] as string[]
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
color: '#8e9dff',
|
||||
name: $t('page.home.downloadCount'),
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
stack: 'Total',
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0.25,
|
||||
color: '#8e9dff'
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: '#fff'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: [] as number[]
|
||||
},
|
||||
{
|
||||
color: '#26deca',
|
||||
name: $t('page.home.registerCount'),
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
stack: 'Total',
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0.25,
|
||||
color: '#26deca'
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: '#fff'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
},
|
||||
data: []
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
async function mockData() {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 1000);
|
||||
});
|
||||
|
||||
updateOptions(opts => {
|
||||
opts.xAxis.data = ['06:00', '08:00', '10:00', '12:00', '14:00', '16:00', '18:00', '20:00', '22:00', '24:00'];
|
||||
opts.series[0].data = [4623, 6145, 6268, 6411, 1890, 4251, 2978, 3880, 3606, 4311];
|
||||
opts.series[1].data = [2208, 2016, 2916, 4512, 8281, 2008, 1963, 2367, 2956, 678];
|
||||
|
||||
return opts;
|
||||
});
|
||||
}
|
||||
|
||||
function updateLocale() {
|
||||
updateOptions((opts, factory) => {
|
||||
const originOpts = factory();
|
||||
|
||||
opts.legend.data = originOpts.legend.data;
|
||||
opts.series[0].name = originOpts.series[0].name;
|
||||
opts.series[1].name = originOpts.series[1].name;
|
||||
|
||||
return opts;
|
||||
});
|
||||
}
|
||||
|
||||
async function init() {
|
||||
mockData();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => appStore.locale,
|
||||
() => {
|
||||
updateLocale();
|
||||
}
|
||||
);
|
||||
|
||||
// init
|
||||
init();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NCard :bordered="false" class="card-wrapper">
|
||||
<div ref="domRef" class="h-360px overflow-hidden"></div>
|
||||
</NCard>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
109
src/views/home/modules/pie-chart.vue
Normal file
109
src/views/home/modules/pie-chart.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
import { watch } from 'vue';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useEcharts } from '@/hooks/common/echarts';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'PieChart'
|
||||
});
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const { domRef, updateOptions } = useEcharts(() => ({
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
bottom: '1%',
|
||||
left: 'center',
|
||||
itemStyle: {
|
||||
borderWidth: 0
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
color: ['#5da8ff', '#8e9dff', '#fedc69', '#26deca'],
|
||||
name: $t('page.home.schedule'),
|
||||
type: 'pie',
|
||||
radius: ['45%', '75%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 1
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '12'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: [] as { name: string; value: number }[]
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
async function mockData() {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 1000);
|
||||
});
|
||||
|
||||
updateOptions(opts => {
|
||||
opts.series[0].data = [
|
||||
{ name: $t('page.home.study'), value: 20 },
|
||||
{ name: $t('page.home.entertainment'), value: 10 },
|
||||
{ name: $t('page.home.work'), value: 40 },
|
||||
{ name: $t('page.home.rest'), value: 30 }
|
||||
];
|
||||
|
||||
return opts;
|
||||
});
|
||||
}
|
||||
|
||||
function updateLocale() {
|
||||
updateOptions((opts, factory) => {
|
||||
const originOpts = factory();
|
||||
|
||||
opts.series[0].name = originOpts.series[0].name;
|
||||
|
||||
opts.series[0].data = [
|
||||
{ name: $t('page.home.study'), value: 20 },
|
||||
{ name: $t('page.home.entertainment'), value: 10 },
|
||||
{ name: $t('page.home.work'), value: 40 },
|
||||
{ name: $t('page.home.rest'), value: 30 }
|
||||
];
|
||||
|
||||
return opts;
|
||||
});
|
||||
}
|
||||
|
||||
async function init() {
|
||||
mockData();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => appStore.locale,
|
||||
() => {
|
||||
updateLocale();
|
||||
}
|
||||
);
|
||||
|
||||
// init
|
||||
init();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NCard :bordered="false" class="card-wrapper">
|
||||
<div ref="domRef" class="h-360px overflow-hidden"></div>
|
||||
</NCard>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
40
src/views/home/modules/project-news.vue
Normal file
40
src/views/home/modules/project-news.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'ProjectNews'
|
||||
});
|
||||
|
||||
interface NewsItem {
|
||||
id: number;
|
||||
content: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
const newses = computed<NewsItem[]>(() => [
|
||||
{ id: 1, content: $t('page.home.projectNews.desc1'), time: '2021-05-28 22:22:22' },
|
||||
{ id: 2, content: $t('page.home.projectNews.desc2'), time: '2021-10-27 10:24:54' },
|
||||
{ id: 3, content: $t('page.home.projectNews.desc3'), time: '2021-10-31 22:43:12' },
|
||||
{ id: 4, content: $t('page.home.projectNews.desc4'), time: '2021-11-03 20:33:31' },
|
||||
{ id: 5, content: $t('page.home.projectNews.desc5'), time: '2021-11-07 22:45:32' }
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NCard :title="$t('page.home.projectNews.title')" :bordered="false" size="small" segmented class="card-wrapper">
|
||||
<template #header-extra>
|
||||
<a class="text-primary" href="javascript:;">{{ $t('page.home.projectNews.moreNews') }}</a>
|
||||
</template>
|
||||
<NList>
|
||||
<NListItem v-for="item in newses" :key="item.id">
|
||||
<template #prefix>
|
||||
<SoybeanAvatar class="size-48px!" />
|
||||
</template>
|
||||
<NThing :title="item.content" :description="item.time" />
|
||||
</NListItem>
|
||||
</NList>
|
||||
</NCard>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
50
src/views/withdraw/fiat/index.vue
Normal file
50
src/views/withdraw/fiat/index.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<script lang="ts" setup>
|
||||
import { useTemplateRef } from 'vue';
|
||||
import { client, safeClient } from '@/service/api';
|
||||
import type { TableBaseColumns, TableFetchData, TableInst } from '@/components/table';
|
||||
import TableBase from '@/components/table/table-base.vue';
|
||||
|
||||
const tableInst = useTemplateRef<TableInst>('tableInst');
|
||||
|
||||
const fetchData: TableFetchData = () => {
|
||||
return safeClient(() => client.api.admin.withdraw.pending.get());
|
||||
};
|
||||
|
||||
const columns: TableBaseColumns = [
|
||||
{
|
||||
title: 'ID',
|
||||
key: 'id'
|
||||
},
|
||||
{
|
||||
title: '提现金额',
|
||||
key: 'amount'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
fixed: 'right',
|
||||
key: 'operation',
|
||||
operations: row => [
|
||||
{
|
||||
contentText: '处理',
|
||||
type: 'primary',
|
||||
onClick: () => {
|
||||
tableInst.value?.reload();
|
||||
}
|
||||
},
|
||||
{
|
||||
contentText: '处理',
|
||||
type: 'primary',
|
||||
onClick: () => {
|
||||
tableInst.value?.reload();
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TableBase :columns="columns" :fetch-data="fetchData" />
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped></style>
|
||||
Reference in New Issue
Block a user