This commit is contained in:
孟帅
2025-10-25 00:35:30 +08:00
parent 5ebc33f28b
commit 7313d22cdb
168 changed files with 2349 additions and 1455 deletions

View File

@@ -2,7 +2,7 @@
VITE_PORT=8001
# spa-title
VITE_GLOB_APP_TITLE=HotGo管理系统
VITE_GLOB_APP_TITLE=加载中
# spa shortname
VITE_GLOB_APP_SHORT_NAME=HG

79
web/auto-imports.d.ts vendored Normal file
View File

@@ -0,0 +1,79 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue').EffectScope
const computed: typeof import('vue').computed
const createApp: typeof import('vue').createApp
const customRef: typeof import('vue').customRef
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
const defineComponent: typeof import('vue').defineComponent
const effectScope: typeof import('vue').effectScope
const getCurrentInstance: typeof import('vue').getCurrentInstance
const getCurrentScope: typeof import('vue').getCurrentScope
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
const h: typeof import('vue').h
const inject: typeof import('vue').inject
const isProxy: typeof import('vue').isProxy
const isReactive: typeof import('vue').isReactive
const isReadonly: typeof import('vue').isReadonly
const isRef: typeof import('vue').isRef
const isShallow: typeof import('vue').isShallow
const markRaw: typeof import('vue').markRaw
const nextTick: typeof import('vue').nextTick
const onActivated: typeof import('vue').onActivated
const onBeforeMount: typeof import('vue').onBeforeMount
const onBeforeRouteLeave: typeof import('vue-router').onBeforeRouteLeave
const onBeforeRouteUpdate: typeof import('vue-router').onBeforeRouteUpdate
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
const onDeactivated: typeof import('vue').onDeactivated
const onErrorCaptured: typeof import('vue').onErrorCaptured
const onMounted: typeof import('vue').onMounted
const onRenderTracked: typeof import('vue').onRenderTracked
const onRenderTriggered: typeof import('vue').onRenderTriggered
const onScopeDispose: typeof import('vue').onScopeDispose
const onServerPrefetch: typeof import('vue').onServerPrefetch
const onUnmounted: typeof import('vue').onUnmounted
const onUpdated: typeof import('vue').onUpdated
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
const provide: typeof import('vue').provide
const reactive: typeof import('vue').reactive
const readonly: typeof import('vue').readonly
const ref: typeof import('vue').ref
const resolveComponent: typeof import('vue').resolveComponent
const shallowReactive: typeof import('vue').shallowReactive
const shallowReadonly: typeof import('vue').shallowReadonly
const shallowRef: typeof import('vue').shallowRef
const t: typeof import('@/locale/index').t
const toRaw: typeof import('vue').toRaw
const toRef: typeof import('vue').toRef
const toRefs: typeof import('vue').toRefs
const toValue: typeof import('vue').toValue
const triggerRef: typeof import('vue').triggerRef
const unref: typeof import('vue').unref
const useAttrs: typeof import('vue').useAttrs
const useCssModule: typeof import('vue').useCssModule
const useCssVars: typeof import('vue').useCssVars
const useId: typeof import('vue').useId
const useLink: typeof import('vue-router').useLink
const useModel: typeof import('vue').useModel
const useRoute: typeof import('vue-router').useRoute
const useRouter: typeof import('vue-router').useRouter
const useSlots: typeof import('vue').useSlots
const useTemplateRef: typeof import('vue').useTemplateRef
const watch: typeof import('vue').watch
const watchEffect: typeof import('vue').watchEffect
const watchPostEffect: typeof import('vue').watchPostEffect
const watchSyncEffect: typeof import('vue').watchSyncEffect
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

View File

@@ -1,6 +1,7 @@
import type { Plugin } from 'vite';
import Components from 'unplugin-vue-components/vite';
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
import AutoImport from 'unplugin-auto-import/vite';
import topLevelAwait from 'vite-plugin-top-level-await';
import setupExtend from 'vite-plugin-vue-setup-extend';
import vue from '@vitejs/plugin-vue';
@@ -23,6 +24,18 @@ export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) {
resolvers: [NaiveUiResolver()],
}),
// 自动引入API
AutoImport({
imports: [
'vue',
'vue-router',
{
'@/locale/index': ['t'],
},
],
dts: 'auto-imports.d.ts',
}),
// 支持顶级wait
topLevelAwait({
// The export name of top-level await promise for each chunk module

View File

@@ -1,7 +1,7 @@
{
"name": "hotgo",
"type": "module",
"version": "2.17.8",
"version": "2.18.6",
"author": {
"name": "MengShuai",
"email": "133814250@qq.com",
@@ -51,7 +51,7 @@
"lodash-es": "^4.17.21",
"mint-filter": "^4.0.3",
"mitt": "^3.0.1",
"naive-ui": "^2.42.0",
"naive-ui": "^2.43.1",
"pinia": "^2.2.2",
"pinyin-pro": "^3.24.2",
"print-js": "^1.6.0",
@@ -63,6 +63,7 @@
"throttle-debounce": "^5.0.2",
"vfonts": "^0.0.3",
"vue": "^3.4.38",
"vue-i18n": "^11.1.11",
"vue-router": "^4.4.3",
"vue-types": "^5.1.3",
"vue-waterfall-plugin-next": "^2.6.0",
@@ -118,6 +119,7 @@
"stylelint-scss": "^6.5.1",
"tailwindcss": "^3.4.10",
"typescript": "^5.5.4",
"unplugin-auto-import": "^20.2.0",
"unplugin-vue-components": "^0.27.4",
"vite": "^5.4.2",
"vite-plugin-compression": "^0.5.1",

View File

@@ -47,3 +47,12 @@ export function ClearKind(params) {
params,
});
}
// 链接图片转存
export function ImageTransferStorage(params) {
return http.request({
url: '/upload/imageTransferStorage',
method: 'POST',
params,
});
}

View File

@@ -15,7 +15,7 @@ import {
BellOutlined,
} from '@vicons/antd';
import { Refresh } from '@vicons/ionicons5';
import { Refresh, LanguageOutline } from '@vicons/ionicons5';
export default {
SettingOutlined,
@@ -33,4 +33,5 @@ export default {
CheckOutlined,
BellOutlined,
Refresh,
LanguageOutline,
};

View File

@@ -134,6 +134,30 @@
<span>全屏</span>
</n-tooltip>
</div>
<!-- 国际化 -->
<div
class="layout-header-trigger layout-header-trigger-min"
v-if="userStore.loginConfig?.i18nSwitch"
>
<n-dropdown
:value="i18nStore.getLocale()"
trigger="click"
@select="localeSelect"
:options="availableLocales"
show-arrow
>
<n-tooltip placement="bottom">
<template #trigger>
<n-icon size="18">
<LanguageOutline />
</n-icon>
</template>
<span>切换语言</span>
</n-tooltip>
</n-dropdown>
</div>
<!-- 个人中心 -->
<div class="layout-header-trigger layout-header-trigger-min">
<n-dropdown trigger="click" @select="avatarSelect" :options="avatarOptions" show-arrow>
@@ -200,7 +224,7 @@
import SystemMessage from './SystemMessage.vue';
import { notificationStoreWidthOut } from '@/store/modules/notification';
import { getIcon } from '@/enums/systemMessageEnum';
import { availableLocales, useI18nStore } from '@/store/modules/i18n';
import Search from './Search.vue';
export default defineComponent({
@@ -222,6 +246,7 @@
},
},
setup(props, { emit }) {
const i18nStore = useI18nStore();
const userStore = useUserStore();
const notificationStore = notificationStoreWidthOut();
const useLockscreen = useLockscreenStore();
@@ -238,7 +263,7 @@
// const { username, avatar } = userStore?.info || {};
const drawerSetting = ref();
const projectName = import.meta.env.VITE_GLOB_APP_TITLE;
const projectName = userStore.loginConfig?.projectName;
const state = reactive({
// username: username || '',
@@ -431,6 +456,15 @@
}
};
// 多久下拉菜单
const localeSelect = (key) => {
i18nStore.setLocale(key);
message.success('切换成功');
setTimeout(function () {
location.reload();
}, 800);
};
function openSetting() {
const { openDrawer } = drawerSetting.value;
openDrawer();
@@ -543,6 +577,9 @@
userStore,
updateMenu,
projectName,
localeSelect,
i18nStore,
availableLocales,
};
},
});

View File

@@ -6,6 +6,8 @@
</template>
<script>
import { useUserStore } from '@/store/modules/user';
export default {
name: 'Index',
props: {
@@ -14,7 +16,8 @@
},
},
setup() {
const projectName = import.meta.env.VITE_GLOB_APP_TITLE;
const userStore = useUserStore();
const projectName = userStore.loginConfig?.projectName;
return {
projectName,
};

View File

@@ -76,6 +76,7 @@
import { useLoadingBar } from 'naive-ui';
import { useRoute } from 'vue-router';
import { useProjectSettingStore } from '@/store/modules/projectSetting';
import { useI18nStore } from '@/store/modules/i18n';
const { getDarkTheme } = useDesignSetting();
const {
@@ -89,6 +90,7 @@
const route = useRoute();
const settingStore = useProjectSettingStore();
const i18nStore = useI18nStore();
const navMode = getNavMode;
@@ -183,6 +185,7 @@
onMounted(() => {
checkMobileMode();
window.addEventListener('resize', watchWidth);
i18nStore.initLocale();
//挂载在 window 方便与在js中使用
window['$loading'] = useLoadingBar();
window['$loading'].finish();

28
web/src/locale/en.json Normal file
View File

@@ -0,0 +1,28 @@
{
"你好,美丽世界": "Hello, Beautiful World",
"剩余{num}余额": "Remaining {num} Balance",
"卡板量": "Total Card",
"日": "Day",
"日同比": "Than Yesterday",
"周同比": "Than LastWeek",
"总卡板量:": "ALL Card:",
"激活卡板": "Activate Card",
"周": "Week",
"总激活卡板:": "Total Activate Card:",
"代理商": "Agent",
"总代理商量:": "Total Agent:",
"提现佣金": "Withdrawal Commission",
"月": "Month",
"月同比": "Than LastMonth",
"总提现额:": "Total Withdrawal Amount:",
"用户": "Users",
"分析": "Analysis",
"商品": "Goods",
"订单": "Order",
"票据": "Receipt",
"消息": "Message",
"标签": "Label",
"配置": "Configure",
"流量消耗趋势": "Trend Of Traffic Consumption",
"客户端访问量": "Client Traffic"
}

37
web/src/locale/index.ts Normal file
View File

@@ -0,0 +1,37 @@
import { createI18n } from 'vue-i18n';
import type { I18nOptions } from 'vue-i18n';
import en from './en.json';
import zhHant from './zh-Hant.json';
import zhHans from './zh-Hans.json';
const messages = {
en,
'zh-TW': zhHant,
'zh-CN': zhHans,
};
// 创建 i18n 实例配置
const i18nConfig: I18nOptions = {
legacy: false, // 使用 Composition API 模式
locale: 'zh-CN', // 默认语言
fallbackLocale: 'zh-CN', // 回退语言
messages, // 语言包
missingWarn: false, // 关闭找不到 key 的警告
fallbackWarn: false, // 关闭回退警告
// 当找不到翻译时,返回 key 本身
missing: (_locale, key) => {
return key;
},
};
const i18n = createI18n(i18nConfig);
export default i18n;
// 导出 t 函数,支持多种参数形式
export const t = (key: string, named?: Record<string, unknown>, options?: any): string => {
if (named !== undefined) {
return i18n.global.t(key, named, options) as string;
}
return i18n.global.t(key) as string;
};

View File

@@ -0,0 +1,3 @@
{
"剩余{num}余额": "剩余{num}余额"
}

View File

@@ -0,0 +1,28 @@
{
"你好,美丽世界": "你好,美麗世界",
"剩余{num}余额": "剩餘{num}餘額",
"卡板量": "卡板量",
"日": "日",
"日同比": "日同比",
"周同比": "週同比",
"总卡板量:": "總卡板量:",
"激活卡板": "激活卡板",
"周": "週",
"总激活卡板:": "總激活卡板:",
"代理商": "代理商",
"总代理商量:": "總代理商數:",
"提现佣金": "提現傭金",
"月": "月",
"月同比": "月同比",
"总提现额:": "總提現額:",
"用户": "使用者",
"分析": "分析",
"商品": "商品",
"订单": "訂單",
"票据": "票據",
"消息": "訊息",
"标签": "標籤",
"配置": "設定",
"流量消耗趋势": "流量消耗趨勢",
"客户端访问量": "用戶端訪問量"
}

View File

@@ -6,12 +6,17 @@ import { setupStore } from '@/store';
import { setupNaive, setupDirectives } from '@/plugins';
import { AppProvider } from '@/components/Application';
import setupWebsocket from '@/utils/websocket/index';
import i18n from '@/locale/index';
async function bootstrap() {
const appProvider = createApp(AppProvider);
const app = createApp(App);
// 国际化
app.use(i18n);
app.config.globalProperties.t = i18n.global.t;
// 注册全局常用的 naive-ui 组件
setupNaive(app);

View File

@@ -0,0 +1,68 @@
import { defineStore } from 'pinia';
import { createStorage } from '@/utils/Storage';
import { CurrentLocale } from '@/store/mutation-types';
import i18n from '@/locale/index';
import { useUserStore } from '@/store/modules/user';
export const availableLocales = [
{
label: '简体中文',
key: 'zh-CN',
},
{
label: '繁體中文',
key: 'zh-TW',
},
{
label: 'English',
key: 'en',
},
];
export interface II18nStore {
currentLocale: string;
}
const Storage = createStorage({ storage: localStorage });
export const useI18nStore = defineStore({
id: 'I18nStore',
state: (): II18nStore => ({
currentLocale: Storage.get(CurrentLocale, 'zh-CN'),
}),
getters: {},
actions: {
getLocale(): string {
return this.currentLocale;
},
setLocale(locale: string) {
if (availableLocales.some((l) => l.key === locale)) {
(i18n.global.locale as any).value = locale;
this.currentLocale = locale;
Storage.set(CurrentLocale, locale);
}
},
initLocale(): void {
const userStore = useUserStore();
const defaultLanguage = userStore.loginConfig?.defaultLanguage || 'zh-CN';
// 未开启国际化功能,仅设置语言不持久化
if (!userStore.loginConfig?.i18nSwitch) {
this.setLocale(defaultLanguage);
return;
}
// 优先使用本地存储的语言设置
const savedLocale = Storage.get(CurrentLocale, defaultLanguage);
if (savedLocale && availableLocales.some((l) => l.key === savedLocale)) {
this.setLocale(savedLocale);
return;
}
// 使用系统配置的默认语言
this.setLocale(defaultLanguage);
},
},
});

View File

@@ -17,6 +17,8 @@ import tabsView from './tabs-view';
import lockscreen from './lockscreen';
// @ts-ignore
import dict from './dict';
// @ts-ignore
import i18n from './i18n';
export default {
asyncRoute,
@@ -24,4 +26,5 @@ export default {
tabsView,
lockscreen,
dict,
i18n,
};

View File

@@ -67,6 +67,9 @@ export interface LoginConfigState {
loginAutoOpenId: number;
loginProtocol: string;
loginPolicy: string;
i18nSwitch: boolean;
defaultLanguage: string;
projectName: string;
}
export interface IUserState {

View File

@@ -5,3 +5,4 @@ export const CURRENT_LOGIN_CONFIG = 'CURRENT-LOGIN-CONFIG'; // 当前登录配
export const IS_LOCKSCREEN = 'IS-LOCKSCREEN'; // 是否锁屏
export const TABS_ROUTES = 'TABS-ROUTES'; // 标签页
export const CURRENT_DICT = 'CURRENT-DICT'; // 当前用户字典配置
export const CurrentLocale = 'Current-Locale'; // 当前语言

View File

@@ -6,20 +6,17 @@ import { checkStatus } from './checkStatus';
import { formatRequestDate, joinTimestamp } from './helper';
import { ContentTypeEnum, RequestEnum, ResultEnum } from '@/enums/httpEnum';
import { PageEnum } from '@/enums/pageEnum';
import { useGlobSetting } from '@/hooks/setting';
import { isString, isUrl } from '@/utils/is/';
import { deepMerge } from '@/utils';
import { setObjToUrlParams } from '@/utils/urlUtils';
import { CreateAxiosOptions, RequestOptions, Result } from './types';
import { useUserStoreWidthOut } from '@/store/modules/user';
import router from '@/router';
import { storage } from '@/utils/Storage';
import { encodeParams } from '@/utils/urlUtils';
import { delNullProperty } from '@/utils/array';
import { useI18nStore } from '@/store/modules/i18n';
const globSetting = useGlobSetting();
const urlPrefix = globSetting.urlPrefix || '';
@@ -182,6 +179,7 @@ const transform: AxiosTransform = {
*/
requestInterceptors: (config, options) => {
// 请求之前处理config
const i18nStore = useI18nStore();
const userStore = useUserStoreWidthOut();
const token = userStore.getToken;
if (token && (config as Recordable)?.requestOptions?.withToken !== false) {
@@ -190,6 +188,7 @@ const transform: AxiosTransform = {
? `${options.authenticationScheme} ${token}`
: token;
}
config.headers.Locale = i18nStore.getLocale();
return config;
},

View File

@@ -57,6 +57,14 @@
</template>
上传文档
</n-button>
<n-button type="success" @click="showUrlModal" class="ml-2">
<template #icon>
<n-icon>
<FileImageOutlined />
</n-icon>
</template>
链接转图片
</n-button>
<n-button type="error" @click="batchDelete" :disabled="batchDeleteDisabled" class="ml-2">
<template #icon>
<n-icon>
@@ -73,6 +81,7 @@
<FileUpload ref="imageUploadRef" :finish-call="handleFinishCall" upload-type="image" />
<FileUpload ref="docUploadRef" :finish-call="handleFinishCall" upload-type="doc" />
<MultipartUpload ref="multipartUploadRef" @on-finish="handleFinishCall" />
<UrlModal ref="urlModalRef" @reloadTable="reloadTable" />
</div>
</template>
@@ -93,6 +102,7 @@
import FileUpload from '@/components/FileChooser/src/Upload.vue';
import MultipartUpload from '@/components/Upload/multipartUpload.vue';
import { Attachment } from '@/components/FileChooser/src/model';
import UrlModal from './urlModal.vue';
import { adaTableScrollX } from '@/utils/hotgo';
const message = useMessage();
@@ -105,6 +115,7 @@
const imageUploadRef = ref();
const docUploadRef = ref();
const multipartUploadRef = ref();
const urlModalRef =ref();
const actionColumn = reactive({
width: 132,
@@ -216,6 +227,10 @@
}
}
function showUrlModal() {
urlModalRef.value?.showModal();
}
onMounted(async () => {
loadOptions();
});

View File

@@ -0,0 +1,90 @@
<template>
<div>
<n-modal
v-model:show="isShowModal"
:style="{
width: dialogWidth,
}"
:show-icon="false"
preset="dialog"
title="链接转图片"
>
<n-alert type="info"> 将外部图片链接下载并转存至平台的存储驱动中 </n-alert>
<n-form
:model="formParams"
ref="formPacketRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-spin :show="loading" description="请稍候...">
<n-form-item label="图片链接" path="url">
<n-input v-model:value="formParams.url" />
</n-form-item>
<n-form-item label="预览图片" v-if="formParams.url != ''">
<n-image width="150" :src="formParams.url" />
</n-form-item>
</n-spin>
</n-form>
<template #action>
<n-space>
<n-button @click="closeForm">关闭</n-button>
<n-button type="primary" :loading="formBtnLoading" @click="confirmForm">上传 </n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { adaModalWidth } from '@/utils/hotgo';
import { useMessage } from 'naive-ui';
import { ImageTransferStorage } from '@/api/apply/attachment';
const emit = defineEmits(['reloadTable']);
const loading = ref(false);
const isShowModal = ref(false);
const dialogWidth = ref(adaModalWidth(640));
const formBtnLoading = ref(false);
const formPacketRef = ref();
const message = useMessage();
const formParams = ref({
url: '',
});
function reloadTable() {
emit('reloadTable');
}
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
ImageTransferStorage(formParams.value)
.then((_res) => {
message.success('操作成功');
setTimeout(() => {
reloadTable();
closeForm();
});
})
.finally(() => {
formBtnLoading.value = false;
});
}
function closeForm() {
isShowModal.value = false;
}
function showModal() {
isShowModal.value = true;
formParams.value.url = '';
}
defineExpose({ showModal });
</script>
<style lang="less"></style>

View File

@@ -3,11 +3,17 @@
<NRow :gutter="24">
<NCol :span="24">
<n-card content-style="padding: 0;" :bordered="false">
<n-tabs type="line" size="large" :tabs-padding="20" pane-style="padding: 20px;">
<n-tab-pane name="流量消耗趋势">
<n-tabs
type="line"
size="large"
:tabs-padding="20"
pane-style="padding: 20px;"
default-value="FluxTrend"
>
<n-tab-pane name="FluxTrend" :tab="t('流量消耗趋势')">
<FluxTrend />
</n-tab-pane>
<n-tab-pane name="客户端访问量">
<n-tab-pane name="VisitAmount" :tab="t('客户端访问量')">
<VisitAmount />
</n-tab-pane>
</n-tabs>

View File

@@ -4,13 +4,13 @@
<n-grid cols="1 s:2 m:3 l:4 xl:4 2xl:4" responsive="screen" :x-gap="12" :y-gap="8">
<n-grid-item>
<NCard
title="卡板量"
:title="t('卡板量')"
:segmented="{ content: true, footer: true }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="success"></n-tag>
<n-tag type="success">{{ t('日') }}</n-tag>
</template>
<div class="py-1 px-1 flex justify-between">
<n-skeleton v-if="loading" :width="100" size="medium" />
@@ -20,7 +20,7 @@
<div class="text-sn">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
日同比
{{ t('日同比') }}
<CountTo :startVal="1" suffix="%" :endVal="visits.rise" />
<n-icon size="12" color="#00ff6f">
<CaretUpOutlined />
@@ -30,7 +30,7 @@
<div class="text-sn">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
周同比
{{ t('周同比') }}
<CountTo :startVal="1" suffix="%" :endVal="visits.decline" />
<n-icon size="12" color="#ffde66">
<CaretDownOutlined />
@@ -42,7 +42,7 @@
<div class="flex justify-between">
<n-skeleton v-if="loading" text :repeat="2" />
<template v-else>
<div class="text-sn"> 总卡板量 </div>
<div class="text-sn"> {{ t('总卡板量:') }} </div>
<div class="text-sn">
<CountTo :startVal="1" :endVal="visits.amount" />
</div>
@@ -53,13 +53,13 @@
</n-grid-item>
<n-grid-item>
<NCard
title="激活卡板"
:title="t('激活卡板')"
:segmented="{ content: true, footer: true }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="info"></n-tag>
<n-tag type="info">{{ t('周') }}</n-tag>
</template>
<div class="py-1 px-1 flex justify-between">
<n-skeleton v-if="loading" :width="100" size="medium" />
@@ -85,10 +85,9 @@
<div class="flex justify-between">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
<div class="text-sn"> 总激活卡板 </div>
<div class="text-sn"> {{ t('总激活卡板:') }} </div>
<div class="text-sn">
<CountTo :startVal="1" :endVal="saleroom.amount" />
<!-- prefix="¥"-->
</div>
</template>
</div>
@@ -97,13 +96,13 @@
</n-grid-item>
<n-grid-item>
<NCard
title="代理商"
:title="t('代理商')"
:segmented="{ content: true, footer: true }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="warning"></n-tag>
<n-tag type="warning">{{ t('周') }}</n-tag>
</template>
<div class="py-1 px-1 flex justify-between">
<n-skeleton v-if="loading" :width="100" size="medium" />
@@ -113,7 +112,7 @@
<div class="text-sn">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
日同比
{{ t('日同比') }}
<CountTo :startVal="1" suffix="%" :endVal="orderLarge.rise" />
<n-icon size="12" color="#00ff6f">
<CaretUpOutlined />
@@ -123,7 +122,7 @@
<div class="text-sn">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
周同比
{{ t('周同比') }}
<CountTo :startVal="1" suffix="%" :endVal="orderLarge.rise" />
<n-icon size="12" color="#ffde66">
<CaretDownOutlined />
@@ -135,7 +134,7 @@
<div class="flex justify-between">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
<div class="text-sn"> 总代理商量 </div>
<div class="text-sn"> {{ t('总代理商量:') }} </div>
<div class="text-sn">
<CountTo :startVal="1" suffix="%" :endVal="orderLarge.amount" />
</div>
@@ -146,13 +145,13 @@
</n-grid-item>
<n-grid-item>
<NCard
title="提现佣金"
:title="t('提现佣金')"
:segmented="{ content: true, footer: true }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="error"></n-tag>
<n-tag type="error">{{ t('月') }}</n-tag>
</template>
<div class="py-1 px-1 flex justify-between">
<n-skeleton v-if="loading" :width="100" size="medium" />
@@ -162,7 +161,7 @@
<div class="text-sn">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
月同比
{{ t('月同比') }}
<CountTo :startVal="1" suffix="%" :endVal="volume.rise" />
<n-icon size="12" color="#00ff6f">
<CaretUpOutlined />
@@ -172,7 +171,7 @@
<div class="text-sn">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
月同比
{{ t('月同比') }}
<CountTo :startVal="1" suffix="%" :endVal="volume.decline" />
<n-icon size="12" color="#ffde66">
<CaretDownOutlined />
@@ -184,7 +183,7 @@
<div class="flex justify-between">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
<div class="text-sn"> 总提现额 </div>
<div class="text-sn"> {{ t('总提现额:') }} </div>
<div class="text-sn">
<CountTo prefix="¥" :startVal="1" :endVal="volume.amount" />
</div>
@@ -230,8 +229,6 @@
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { getConsoleInfo } from '@/api/dashboard/console';
import VisiTab from './components/VisiTab.vue';
import { CountTo } from '@/components/CountTo';
@@ -256,80 +253,82 @@
const router = useRouter();
// 图标列表
const iconList = [
{
icon: UsergroupAddOutlined,
size: '32',
title: '用户',
color: '#69c0ff',
eventObject: {
click: () => router.push({ name: 'user' }),
const iconList = computed(() => {
return [
{
icon: UsergroupAddOutlined,
size: '32',
title: t('用户'),
color: '#69c0ff',
eventObject: {
click: () => router.push({ name: 'user' }),
},
},
},
{
icon: BarChartOutlined,
size: '32',
title: '分析',
color: '#69c0ff',
eventObject: {
click: () => {},
{
icon: BarChartOutlined,
size: '32',
title: t('分析'),
color: '#69c0ff',
eventObject: {
click: () => {},
},
},
},
{
icon: ShoppingCartOutlined,
size: '32',
title: '商品',
color: '#ff9c6e',
eventObject: {
click: () => {},
{
icon: ShoppingCartOutlined,
size: '32',
title: t('商品'),
color: '#ff9c6e',
eventObject: {
click: () => {},
},
},
},
{
icon: AccountBookOutlined,
size: '32',
title: '订单',
color: '#b37feb',
eventObject: {
click: () => {},
{
icon: AccountBookOutlined,
size: '32',
title: t('订单'),
color: '#b37feb',
eventObject: {
click: () => {},
},
},
},
{
icon: CreditCardOutlined,
size: '32',
title: '票据',
color: '#ffd666',
eventObject: {
click: () => {},
{
icon: CreditCardOutlined,
size: '32',
title: t('票据'),
color: '#ffd666',
eventObject: {
click: () => {},
},
},
},
{
icon: MailOutlined,
size: '32',
title: '消息',
color: '#5cdbd3',
eventObject: {
click: () => {},
{
icon: MailOutlined,
size: '32',
title: t('消息'),
color: '#5cdbd3',
eventObject: {
click: () => {},
},
},
},
{
icon: TagsOutlined,
size: '32',
title: '标签',
color: '#ff85c0',
eventObject: {
click: () => {},
{
icon: TagsOutlined,
size: '32',
title: t('标签'),
color: '#ff85c0',
eventObject: {
click: () => {},
},
},
},
{
icon: SettingOutlined,
size: '32',
title: '配置',
color: '#ffc069',
eventObject: {
click: () => {},
{
icon: SettingOutlined,
size: '32',
title: t('配置'),
color: '#ffc069',
eventObject: {
click: () => {},
},
},
},
];
];
});
onMounted(async () => {
const data = await getConsoleInfo();

View File

@@ -27,15 +27,13 @@
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import type { Component } from 'vue';
import LoginFrom from './login/index.vue';
import RegisterFrom from './register/index.vue';
import { useRouter } from 'vue-router';
import { useUserStore } from '@/store/modules/user';
const userStore = useUserStore();
const projectName = import.meta.env.VITE_GLOB_APP_TITLE;
const projectName = computed(() => userStore.loginConfig?.projectName);
interface LoginModule {
key: string;

View File

@@ -70,7 +70,7 @@
<n-button @click="closeForm">
取消
</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">
<n-button type="info" :loading="formBtnLoading" :disabled="!isFormValid" @click="confirmForm">
确定
</n-button>
</n-space>
@@ -100,6 +100,7 @@
const dialogWidth = computed(() => {
return adaModalWidth(840);
});
const isFormValid = ref(true);
// 提交表单
function confirmForm(e) {

View File

@@ -9,22 +9,22 @@ import { useDictStore } from '@/store/modules/dict';
const dict = useDictStore();
export class State {
public title = ''; // 标题
public id = 0; // ID
public pid = 0; // 上级
public level = 1; // 关系树级别
public tree = null; // 关系树
public categoryId = null; // 测试分类
public description = ''; // 描述
public sort = 0; // 排序
public status = 1; // 状态
public createdBy = 0; // 创建者
public createdBySumma?: null | MemberSumma = null; // 创建者摘要信息
public updatedBy = 0; // 更新者
public createdAt = ''; // 创建时间
public updatedAt = ''; // 修改时间
public deletedAt = ''; // 删除时间
public title = '';//标题
public id = 0;//ID
public pid = 0;//上级
public level = 1;//关系树级别
public tree = null;//关系树
public categoryId = null;//测试分类
public description = '';//描述
public sort = 0;//排序
public status = 1;//状态
public createdBy = 0;//创建者
public createdBySumma?: null | MemberSumma = null;//创建者摘要信息
public updatedBy = 0;//更新者
public createdAt = '';//创建时间
public updatedAt = '';//修改时间
public deletedAt = '';//删除时间
constructor(state?: Partial<State>) {
if (state) {
Object.assign(this, state);

View File

@@ -100,7 +100,7 @@
const allTreeKeys = ref([]);
const actionColumn = reactive({
width: 160,
width: 220,
title: '操作',
key: 'action',
fixed: 'right',
@@ -114,7 +114,7 @@
auth: ['/dept/edit'],
},
{
label: '添加',
label: '添加子部门',
onClick: handleAdd.bind(null, record),
auth: ['/dept/edit'],
},

View File

@@ -52,7 +52,7 @@
</n-space>
</template>
<div class="w-full menu">
<n-input type="text" v-model:value="pattern" placeholder="输入菜单名称搜索">
<n-input type="text" v-model:value="pattern" placeholder="输入菜单名称或权限路径搜索">
<template #suffix>
<n-icon size="18" class="cursor-pointer">
<SearchOutlined />
@@ -72,6 +72,7 @@
checkable
:virtual-scroll="true"
:pattern="pattern"
:filter="filterTreeNode"
:data="treeOption"
:expandedKeys="expandedKeys"
style="max-height: 650px; overflow: hidden"
@@ -165,6 +166,26 @@
expandedKeys.value = keys;
}
// 按名称和权限搜索
function filterTreeNode(pattern: string, node: any) {
if (!pattern) return true;
const searchText = pattern.toLowerCase();
const label = (node.label || node.title || '').toLowerCase();
if (label.includes(searchText)) {
return true;
}
const permissions = node.permissions || '';
if (permissions) {
const permissionsLower = permissions.toLowerCase();
if (permissionsLower.includes(searchText)) {
return true;
}
}
return false;
}
// 加载菜单选项树
function loadTreeOption() {
const needLoading = treeOption.value.length == 0;

View File

@@ -1,82 +1,78 @@
<template>
<div>
<n-modal
v-model:show="showModal"
:show-icon="false"
:mask-closable="false"
preset="dialog"
:title="'分配 ' + formValue.name + ' 的菜单权限'"
>
<n-spin :show="loading" description="请稍候...">
<div class="py-3 menu-list" :style="{ maxHeight: '90vh', height: '70vh' }">
<n-input size="small" v-model:value="pattern" placeholder="输入菜单名称搜索" class="mb-2">
<template #suffix>
<n-icon size="18" class="cursor-pointer">
<SearchOutlined />
</n-icon>
</template>
</n-input>
<n-tree
block-line
checkable
check-on-click
default-expand-all
virtual-scroll
:data="treeData"
:pattern="pattern"
:expandedKeys="expandedKeys"
:checked-keys="checkedKeys"
style="max-height: 950px; overflow: hidden"
@update:checked-keys="checkedTree"
@update:expanded-keys="onExpandedKeys"
/>
</div>
</n-spin>
<template #action>
<n-space class="mt-6" v-if="showImportSelect">
<n-input-group>
<n-tree-select
size="small"
placeholder="请选择一个要导入的角色"
:consistent-menu-width="false"
clearable
filterable
<n-drawer v-model:show="showModal" :width="dialogWidth" :show-icon="false" preset="dialog">
<n-drawer-content closable :title="`分配 ${formValue.name} 的菜单权限`">
<n-spin :show="loading" description="请稍候...">
<div :style="{ maxHeight: '78vh', height: '78vh' }">
<n-input v-model:value="pattern" placeholder="输入菜单名称或权限路径搜索" class="mb-2">
<template #suffix>
<n-icon size="18" class="cursor-pointer">
<SearchOutlined />
</n-icon>
</template>
</n-input>
<n-tree
block-line
checkable
check-on-click
default-expand-all
:options="editRoleOption"
key-field="id"
label-field="name"
:on-update:value="handleImportSelect"
virtual-scroll
:data="treeData"
:pattern="pattern"
:filter="filterTreeNode"
:expandedKeys="expandedKeys"
:checked-keys="checkedKeys"
style="max-height: 950px; overflow: hidden"
@update:checked-keys="checkedTree"
@update:expanded-keys="onExpandedKeys"
/>
<div class="mr-2"></div>
<n-button ghost @click="showImportSelect = false" size="small"> 取消 </n-button>
</n-input-group>
</n-space>
</div>
</n-spin>
<template #footer>
<n-space v-if="showImportSelect">
<n-input-group>
<n-tree-select
placeholder="请选择一个要导入的角色"
:consistent-menu-width="false"
clearable
filterable
default-expand-all
:options="editRoleOption"
key-field="id"
label-field="name"
:on-update:value="handleImportSelect"
style="width: 300px"
/>
<div class="mr-2"></div>
<n-button ghost @click="showImportSelect = false"> 取消 </n-button>
</n-input-group>
</n-space>
<n-space class="mt-6 space-group" v-if="!showImportSelect" size="small">
<n-button ghost @click="showImportSelect = true" size="small"> 导入权限 </n-button>
<n-button type="info" ghost icon-placement="left" @click="packHandle" size="small">
全部{{ expandedKeys.length ? '收起' : '展开' }}
</n-button>
<n-button type="info" ghost icon-placement="left" @click="checkedAllHandle" size="small">
全部{{ checkedAll ? '取消' : '选择' }}
</n-button>
<n-space v-if="!showImportSelect">
<n-button ghost @click="showImportSelect = true"> 导入权限 </n-button>
<n-button type="info" ghost icon-placement="left" @click="packHandle">
全部{{ expandedKeys.length ? '收起' : '展开' }}
</n-button>
<n-button type="info" ghost icon-placement="left" @click="checkedAllHandle">
全部{{ checkedAll ? '取消' : '选择' }}
</n-button>
<n-popconfirm @positive-click="confirmForm">
<template #trigger>
<n-button type="primary" :loading="formBtnLoading" size="small">提交</n-button>
</template>
你正在修改 {{ formValue.name }} 的菜单权限确定要提交吗
</n-popconfirm>
</n-space>
</template>
</n-modal>
<n-popconfirm @positive-click="confirmForm">
<template #trigger>
<n-button type="primary" :loading="formBtnLoading">提交</n-button>
</template>
你正在修改 {{ formValue.name }} 的菜单权限确定要提交吗
</n-popconfirm>
</n-space>
</template>
</n-drawer-content>
</n-drawer>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { GetPermissions, getRoleList, UpdatePermissions } from '@/api/system/role';
import { useProjectSettingStore } from '@/store/modules/projectSetting';
import { NButton, useMessage } from 'naive-ui';
import { adaModalWidth, getTreeKeys } from '@/utils/hotgo';
import { findTreeNode, getAllExpandKeys } from '@/utils';
@@ -86,7 +82,6 @@
const emit = defineEmits(['reloadTable']);
const message = useMessage();
const settingStore = useProjectSettingStore();
const loading = ref(false);
const showModal = ref(false);
const formValue = ref<State>(newState(null));
@@ -111,7 +106,7 @@
});
const dialogWidth = computed(() => {
return adaModalWidth(840);
return adaModalWidth(630);
});
function confirmForm(e) {
@@ -134,11 +129,6 @@
});
}
function closeForm() {
showModal.value = false;
loading.value = false;
}
function checkedTree(keys) {
checkedKeys.value = keys;
}
@@ -165,6 +155,26 @@
}
}
// 按名称和权限搜索
function filterTreeNode(pattern: string, node: any) {
if (!pattern) return true;
const searchText = pattern.toLowerCase();
const label = (node.label || node.title || '').toLowerCase();
if (label.includes(searchText)) {
return true;
}
const permissions = node.permissions || '';
if (permissions) {
const permissionsLower = permissions.toLowerCase();
if (permissionsLower.includes(searchText)) {
return true;
}
}
return false;
}
function handleImportSelect(key: number) {
showImportSelect.value = false;
showModal.value = true;
@@ -208,9 +218,4 @@
});
</script>
<style lang="less">
.space-group {
margin-left: -8px;
margin-right: -8px;
}
</style>
<style lang="less" scoped></style>

View File

@@ -77,7 +77,7 @@
type: 'default',
},
{
label: '添加',
label: '添加子角色',
onClick: handleAddSub.bind(null, record),
},
{

View File

@@ -49,6 +49,7 @@
"build/**/*.d.ts",
"mock/**/*.ts",
"components.d.ts",
"auto-imports.d.ts",
"vite.config.ts"
],
"exclude": [