Merge branch 'main' of https://git.nianxx.cn/duanshuwen/zn-ai into feature/lishaohua

# Conflicts:
#	package-lock.json
#	src/renderer/router/index.ts
This commit is contained in:
kongbeiwu
2025-12-22 01:41:31 +08:00
59 changed files with 2680 additions and 431 deletions

View File

@@ -192,6 +192,7 @@ declare global {
const useFullscreen: typeof import('@vueuse/core').useFullscreen
const useGamepad: typeof import('@vueuse/core').useGamepad
const useGeolocation: typeof import('@vueuse/core').useGeolocation
const useI18n: typeof import('vue-i18n').useI18n
const useId: typeof import('vue').useId
const useIdle: typeof import('@vueuse/core').useIdle
const useImage: typeof import('@vueuse/core').useImage

View File

@@ -0,0 +1,76 @@
<template>
<header class="title-bar flex items-start justify-between h-[40px]">
<div class="title-bar-main flex-auto">
<slot>{{ title ?? '' }}</slot>
</div>
<div class="title-bar-controls w-[168px] flex items-center justify-end text-tx-secondary">
<native-tooltip :content="t('window.minimize')">
<button v-show="isMinimizable" class="flex items-center justify-center cursor-pointer w-[40px] h-[40px]"
@click="minimizeWindow">
<iconify-icon icon="material-symbols:check-indeterminate-small" color="#ffffff" :width="btnSize"
:height="btnSize" />
</button>
</native-tooltip>
<native-tooltip :content="isMaximized ? t('window.restore') : t('window.maximize')">
<button v-show="isMaximizable" class="flex items-center justify-center cursor-pointer w-[40px] h-[40px]"
@click="maximizeWindow">
<iconify-icon icon="material-symbols:chrome-maximize-outline-sharp" color="#ffffff" :width="btnSize"
:height="btnSize" v-show="!isMaximized" />
<iconify-icon icon="material-symbols:chrome-restore-outline-sharp" color="#ffffff" :width="btnSize"
:height="btnSize" v-show="isMaximized" />
</button>
</native-tooltip>
<native-tooltip :content="t('window.close')">
<button v-show="isClosable" class="flex items-center justify-center cursor-pointer w-[40px] h-[40px]"
@click="handleClose">
<iconify-icon icon="material-symbols:close" color="#ffffff" :width="btnSize" :height="btnSize" />
</button>
</native-tooltip>
</div>
</header>
</template>
<script setup lang="ts">
import { Icon as IconifyIcon } from '@iconify/vue'
import { useWinManager } from '@hooks/useWinManager'
import NativeTooltip from '@components/NativeTooltip/index.vue'
interface HeaderBarProps {
title?: string;
isMaximizable?: boolean;
isMinimizable?: boolean;
isClosable?: boolean;
}
defineOptions({ name: 'HeaderBar' })
withDefaults(defineProps<HeaderBarProps>(), {
isMaximizable: true,
isMinimizable: true,
isClosable: true,
})
const emit = defineEmits(['close']);
const { t } = useI18n();
const btnSize = 16;
const {
isMaximized,
closeWindow,
minimizeWindow,
maximizeWindow
} = useWinManager();
function handleClose() {
emit('close');
closeWindow();
}
</script>
<style scoped>
.title-bar {
background-color: rgba(255, 255, 255, 0.2);
}
</style>

View File

@@ -0,0 +1,43 @@
<template>
<template v-if="slots.default()[0].el">
<slot></slot>
</template>
<template v-else>
<span :title="content">
<slot></slot>
</span>
</template>
</template>
<script setup lang="ts">
import { logger } from '@utils/logger'
interface Props {
content: string;
}
defineOptions({ name: 'NativeTooltip' });
const props = defineProps<Props>();
const slots = defineSlots()
if (slots?.default?.().length > 1) {
logger.warn('NativeTooltip only support one slot.')
}
const updateTooltipContent = (content: string) => {
const defaultSlot = slots?.default?.();
if (defaultSlot) {
const slotElement = defaultSlot[0]?.el
if (slotElement && slotElement instanceof HTMLElement) {
slotElement.title = content;
}
}
}
onMounted(() => updateTooltipContent(props.content))
watch(() => props.content, (val: string) => updateTooltipContent(val));
</script>

View File

@@ -0,0 +1,31 @@
export function useWinManager() {
const isMaximized = ref(false)
function closeWindow() {
window.api.closeWindow();
}
function minimizeWindow() {
window.api.minimizeWindow();
}
function maximizeWindow() {
window.api.maximizeWindow();
}
onMounted(async () => {
await nextTick();
window.api.viewIsReady();
isMaximized.value = await window.api.isWindowMaximized();
window.api.onWindowMaximized((_isMaximized: boolean) => isMaximized.value = _isMaximized);
})
return {
isMaximized,
closeWindow,
minimizeWindow,
maximizeWindow
}
};
export default useWinManager;

44
src/renderer/i18n.ts Normal file
View File

@@ -0,0 +1,44 @@
import { createI18n, I18n, type I18nOptions } from 'vue-i18n';
const languages = ['zh', 'en'] as const;
type LanguageType = (typeof languages)[number];
async function createI18nInstance() {
const options: I18nOptions = {
legacy: false,
locale: 'zh',
fallbackLocale: 'zh',
messages: {
zh: await import('@locales/zh.json').then(m => m.default),
en: await import('@locales/en.json').then(m => m.default),
}
}
const i18n = createI18n(options);
return i18n
}
export const i18n = await createI18nInstance();
export async function setLanguage(lang:LanguageType,_i18n?:I18n){
const __i18n = _i18n ?? i18n;
if(__i18n.mode === 'legacy'){
__i18n.global.locale = lang;
return;
}
(__i18n.global.locale as unknown as Ref<LanguageType>).value = lang;
}
export function getLanguage(){
if(i18n.mode === 'legacy'){
return i18n.global.locale;
}
return (i18n.global.locale as unknown as Ref<LanguageType>).value;
}
export default i18n;

View File

@@ -1,25 +1,37 @@
import { createApp } from "vue";
import { createPinia } from "pinia";
import router from "./router";
import App from "./App.vue";
import { createApp, type Plugin } from "vue"
import { createPinia } from "pinia"
import errorHandler from "@utils/errorHandler"
import router from "./router"
import App from "./App.vue"
import ElementPlus from 'element-plus'
import locale from 'element-plus/es/locale/lang/zh-cn'
import i18n from './i18n'
// import './permission'
// 样式文件隔离
import "./styles/index.css";
import 'element-plus/dist/index.css'
// 引入全局组件
import HeaderBar from '@components/HeaderBar/index.vue'
import DragRegion from '@components/DragRegion/index.vue'
const components: Plugin = (app) => {
app.component('HeaderBar', HeaderBar);
app.component('DragRegion', DragRegion);
}
// 创建 Vue 应用实例
const app = createApp(App);
const pinia = createPinia();
// 使用 Pinia 状态管理
app.use(createPinia());
// 使用 Vue Router
app.use(pinia);
app.use(router);
app.use(ElementPlus, { locale })
app.use(components)
app.use(i18n)
app.use(errorHandler)
// 挂载应用到 DOM
app.mount("#app");

View File

@@ -15,7 +15,7 @@ router.beforeEach((to: any, _from: any, next: any) => {
} else if (isWhiteList(to.path)) {
next()
} else {
next()
}
} else {
// no token

View File

@@ -1,83 +1,26 @@
import { createRouter, createWebHistory } from "vue-router";
import Layout from '@renderer/layout/index.vue'
/*
* @Author: kongbeiwu lishaohua-520@qq.com
* @Date: 2025-12-22 01:28:13
* @LastEditors: kongbeiwu lishaohua-520@qq.com
* @LastEditTime: 2025-12-22 01:31:48
* @FilePath: /project/zn-ai/src/renderer/router/index.ts
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import { createRouter, createMemoryHistory } from "vue-router";
const routes = [
{
path: "/login",
name: "Login",
component: () => import("@renderer/views/login/index.vue"),
},
{
path: "/browser",
name: "Browser",
component: () => import("@renderer/browser/BrowserLayout.vue"),
meta: { requiresAuth: true },
},
{
path: "/",
component: Layout,
children: [
{
path: "home",
component: () => import("@renderer/views/home/index.vue"),
name: "Home",
meta: { requiresAuth: true },
},
{
path: "stock",
name: "Stock",
component: () => import("@renderer/views/stock/index.vue"),
meta: { requiresAuth: true },
},
{
path: "rate",
name: "Rate",
component: () => import("@renderer/views/rate/index.vue"),
meta: { requiresAuth: true },
},
{
path: "knowledge",
name: "Knowledge",
component: () => import("@renderer/views/knowledge/index.vue"),
meta: { requiresAuth: true },
},
{
path: "order",
name: "Order",
component: () => import("@renderer/views/order/index.vue"),
meta: { requiresAuth: true },
},
{
path: "more",
name: "More",
component: () => import("@renderer/views/more/index.vue"),
meta: { requiresAuth: true },
},
{
path: "setting",
name: "Setting",
component: () => import("@renderer/views/setting/index.vue"),
meta: { requiresAuth: true },
},
{
path: "/dashboard",
name: "Dashboard",
component: () => import("@renderer/views/dashboard/index.vue"),
meta: { requiresAuth: true },
},
]
},
{
path: "/about",
name: "About",
component: () => import("@renderer/views/about/index.vue"),
},
component: () => import("@renderer/views/login/index.vue"),
name: "Login",
meta: { requiresAuth: false },
}
];
const router = createRouter({
history: createWebHistory(),
history: createMemoryHistory(),
routes,
scrollBehavior(to: any, from: any, savedPosition: any) {
scrollBehavior(_to: any, _from: any, savedPosition: any) {
if (savedPosition) {
return savedPosition
}
@@ -86,24 +29,24 @@ const router = createRouter({
},
});
router.beforeEach((to: any, from: any, next: any) => {
const token = localStorage.getItem("token");
if (to.meta && (to.meta as any).requiresAuth && !token) {
next({ path: "/login" });
return;
}
// router.beforeEach((to: any, from: any, next: any) => {
// const token = localStorage.getItem("token");
// if (to.meta && (to.meta as any).requiresAuth && !token) {
// next({ path: "/login" });
// return;
// }
if (token && to.path === "/login") {
next({ path: "/home" });
return;
}
// if (token && to.path === "/login") {
// next({ path: "/home" });
// return;
// }
if (token && to.path === "/") {
next({ path: "/home" });
return;
}
// if (token && to.path === "/") {
// next({ path: "/home" });
// return;
// }
next();
});
// next();
// });
export default router;

View File

@@ -1,22 +0,0 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
function decrement() {
count.value--
}
function reset() {
count.value = 0
}
return { count, doubleCount, increment, decrement, reset }
})

View File

@@ -0,0 +1,50 @@
import { defineStore } from 'pinia'
import { authOauth2TokenUsingPost } from "@renderer/api"
import { getToken, setToken, removeToken } from '@utils/auth'
export const useUserStore = defineStore('userInfo', {
state: () => ({
token: getToken(),
}),
actions: {
/**
* 登录方法
* @function login
* @async
* @param {Object} data - 登录数据
* @returns {Promise<Object>}
*/
async login(data: LoginForm) {
data.grant_type = 'password';
data.scope = 'server';
return new Promise((resolve, reject) => {
authOauth2TokenUsingPost({body: {...data, clientId: ''}})
.then((res: any) => {
// 存储token 信息
setToken(res.access_token)
resolve(res)
})
.catch((err) => {
reject(err);
});
});
},
// 退出系统
logOut() {
return new Promise((resolve, reject) => {
// logout(this.token).then(() => {
// this.token = ''
// this.roles = []
// this.permissions = []
// removeToken()
// resolve()
// }).catch(error => {
// reject(error)
// })
})
}
}
})

View File

@@ -0,0 +1,19 @@
import type { Plugin } from 'vue'
import logger from './logger'
export const errorHandler: Plugin = (app) => {
app.config.errorHandler = (err, instance, info) => {
// 过滤掉无法序列化的 Vue 实例对象
logger.error('Vue error:', err, info);
};
window.onerror = (message, source, lineno, colno, error) => {
logger.error('Window error:', message, source, lineno, colno, error);
};
window.onunhandledrejection = (event) => {
logger.error('Unhandled Promise Rejection:', event);
};
};
export default errorHandler;

View File

@@ -0,0 +1,74 @@
const safeStringify = (arg: any) => {
try {
// 处理 Error 对象
if (arg instanceof Error) {
return {
message: arg.message,
stack: arg.stack,
name: arg.name
}
}
// 简单值直接返回
if (typeof arg !== 'object' || arg === null) {
return arg
}
// 处理 Vue 响应式对象Proxy
if (arg?.__v_isRef || arg?.__v_isReactive || arg?.__v_isReadonly) {
// 尝试解包 Proxy/Ref
try {
const raw = JSON.parse(JSON.stringify(arg))
return raw
} catch (e) {
return '[Vue Reactive Object]'
}
}
// 尝试深拷贝,如果失败则说明包含不可序列化对象
const raw = JSON.parse(JSON.stringify(arg))
return raw
} catch (e) {
// 序列化失败,返回字符串描述
return String(arg)
}
}
// 缓存原始 console 方法,防止递归调用和保持控制台输出
const originalConsole = {
debug: console.debug.bind(console),
log: console.log.bind(console),
info: console.info.bind(console),
warn: console.warn.bind(console),
error: console.error.bind(console),
}
const createSafeLogger = (originalLogger: any) => {
return {
debug: (message: string, ...meta: any[]) => {
originalConsole.debug(message, ...meta)
originalLogger.debug(String(message), ...meta.map(safeStringify))
},
info: (message: string, ...meta: any[]) => {
originalConsole.info(message, ...meta)
originalLogger.info(String(message), ...meta.map(safeStringify))
},
warn: (message: string, ...meta: any[]) => {
originalConsole.warn(message, ...meta)
originalLogger.warn(String(message), ...meta.map(safeStringify))
},
error: (message: string, ...meta: any[]) => {
originalConsole.error(message, ...meta)
originalLogger.error(String(message), ...meta.map(safeStringify))
},
}
}
export const logger = window.api.logger ? createSafeLogger(window.api.logger) : console
if (window.api.logger) {
console.debug = logger.debug;
console.log = logger.info;
console.info = logger.info;
console.warn = logger.warn;
console.error = logger.error;
}
export default logger;

View File

@@ -0,0 +1,15 @@
import '@renderer/styles/index.css'
import errorHandler from '@utils/errorHandler'
import i18n from '@renderer/i18n'
import HeaderBar from '@renderer/components/HeaderBar/index.vue'
import DragRegion from '@renderer/components/DragRegion/index.vue'
import Dialog from './index.vue'
createApp(Dialog)
.use(i18n)
.use(errorHandler)
.component('HeaderBar', HeaderBar)
.component('DragRegion', DragRegion)
.mount('#app')

View File

@@ -0,0 +1,5 @@
<template></template>
<script setup lang="ts">
</script>

View File

@@ -0,0 +1,16 @@
import '@renderer/styles/index.css'
import errorHandler from '@utils/errorHandler'
import i18n from '@renderer/i18n'
import HeaderBar from '@renderer/components/HeaderBar/index.vue'
import DragRegion from '@renderer/components/DragRegion/index.vue'
import Login from './index.vue'
createApp(Login)
.use(i18n)
.use(createPinia())
.use(errorHandler)
.component('HeaderBar', HeaderBar)
.component('DragRegion', DragRegion)
.mount('#app')

View File

@@ -1,102 +1,98 @@
<template>
<div class="h-screen box-border p-[8px] login-bg flex items-center justify-center">
<div class="w-[836px] h-full bg-white rounded-2xl p-[32px] flex flex-col">
<div class="flex items-center">
<img class="w-[48px] h-[48px]" src="@assets/images/login/blue_logo.png" />
<div class="h-screen login-bg flex flex-col">
<header-bar>
<drag-region class="w-full" />
</header-bar>
<span class="ml-auto text-[14px] text-gray-600">没有账号</span>
<button
class="bg-sky-50 rounded-[8px] text-[14px] text-sky-600 px-[12px] py-[6px] focus-visible:outline-none">注册</button>
</div>
<main class="box-border p-[8px] flex-auto flex ">
<div class="w-[836px] box-border bg-white rounded-2xl p-[32px] flex flex-col">
<div class="flex items-center">
<img class="w-[48px] h-[48px]" src="@assets/images/login/blue_logo.png" />
<div class="flex flex-col items-center justify-center mb-[24px] box-border pt-[108px]">
<img class="w-[80px] h-[80px] mb-[12px]" src="@assets/images/login/user_icon.png" />
<div class="text-[24px] font-500 text-gray-800 line-height-[32px] mb-[4px]">登录</div>
<div class="text-[16px] text-gray-500 line-height-[24px]">24小时在岗从不打烊的数字员工</div>
</div>
<!-- <span class="ml-auto text-[14px] text-gray-600">没有账号</span>
<button
class="bg-sky-50 rounded-[8px] text-[14px] text-sky-600 px-[12px] py-[6px] focus-visible:outline-none cursor-pointer">注册</button> -->
</div>
<el-form class="w-[392px] ml-auto mr-auto" ref="formRef" :rules="rules" :model="form" label-position="top"
@keyup.enter="onSubmit">
<el-form-item prop="username">
<div class="text-[14px] text-gray-600">账号</div>
<el-input v-model.trim="form.username" placeholder="请输入账号" clearable autocomplete="off">
<template #prefix>
<RiUser3Fill size="20px" color="#99A0AE" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<div class="text-[14px] text-gray-600">密码</div>
<el-input v-model.trim="form.password" placeholder="请输入密码" clearable autocomplete="off">
<template #prefix>
<RiKey2Fill size="20px" color="#99A0AE" />
</template>
</el-input>
</el-form-item>
<div class="flex flex-col items-center justify-center mb-[24px] box-border pt-[108px]">
<img class="w-[80px] h-[80px] mb-[12px]" src="@assets/images/login/user_icon.png" />
<div class="text-[24px] font-500 text-gray-800 line-height-[32px] mb-[4px]">登录</div>
<div class="text-[16px] text-gray-500 line-height-[24px]">24小时在岗从不打烊的数字员工</div>
</div>
<el-form-item prop="code">
<span class="text-[14px] text-gray-600">验证码</span>
<el-input v-model.trim="form.code" placeholder="请输入验证码" clearable autocomplete="off">
<template #suffix>
<img class="w-[80px] h-[38px] cursor-pointer" :src="imgSrc" @click="getVerifyCode" />
</template>
</el-input>
</el-form-item>
<el-form class="w-[392px] ml-auto mr-auto" ref="formRef" :rules="rules" :model="form" label-position="top"
@keyup.enter="onSubmit">
<el-form-item prop="username">
<div class="text-[14px] text-gray-600">账号</div>
<el-input class="h-[40px]" v-model.trim="form.username" placeholder="请输入账号" clearable autocomplete="off">
<template #prefix>
<RiUser3Fill size="20px" color="#99A0AE" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<div class="text-[14px] text-gray-600">密码</div>
<el-input class="h-[40px]" v-model.trim="form.password" placeholder="请输入密码" clearable autocomplete="off">
<template #prefix>
<RiKey2Fill size="20px" color="#99A0AE" />
</template>
</el-input>
</el-form-item>
<!-- 记住密码|忘记密码 -->
<div class="flex items-center justify-between mb-[24px] mt-[24px]">
<div class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="showPwd" class="w-[14px] h-[14px] rounded-[4px]" />
<span class="text-[14px] text-gray-600">记住密码</span>
<el-form-item prop="code">
<span class="text-[14px] text-gray-600">验证码</span>
<el-input v-model.trim="form.code" placeholder="请输入验证码" autocomplete="off">
<template #suffix>
<img class="w-[80px] h-[38px] cursor-pointer" :src="imgSrc" @click="getVerifyCode" />
</template>
</el-input>
</el-form-item>
<!-- 记住密码|忘记密码 -->
<div class="flex items-center justify-between mb-[24px] mt-[24px]">
<div class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="showPwd" class="w-[14px] h-[14px] rounded-[4px]" />
<span class="text-[14px] text-gray-600">记住密码</span>
</div>
<!-- <span class="text-[14px] text-sky-600 cursor-pointer">忘记密码</span> -->
</div>
<span class="text-[14px] text-sky-600">忘记密码</span>
<!-- 登录按钮 -->
<button type="button"
class="w-full py-2 bg-blue-600 cursor-pointer text-white rounded-[8px] hover:bg-blue-700 disabled:bg-blue-300"
:loading="loading" @click="onSubmit">
</button>
<!-- 同意协议 -->
<!-- <div class="flex items-center justify-center gap-2 mt-[24px]">
<input type="checkbox" v-model="isAgree" class="w-[14px] h-[14px] rounded-[4px]" />
<span class="text-[14px] text-gray-600">我已同意</span>
<span class="text-[14px] text-sky-600 cursor-pointer">使用协议</span>
<span class="text-[14px] text-gray-600"></span>
<span class="text-[14px] text-sky-600 cursor-pointer">隐私协议</span>
</div> -->
</el-form>
<!-- Copy Right -->
<div class="text-[14px] text-gray-500 text-center mt-auto">
© 2025 贵州智念科技服务有限公司 版权所有
</div>
<!-- 登录按钮 -->
<button class="w-full py-2 bg-blue-600 text-white rounded-[8px] hover:bg-blue-700 disabled:bg-blue-300"
@click="onSubmit">
{{ loading ? '登录中' : '登录' }}
</button>
<!-- 同意协议 -->
<div class="flex items-center justify-center gap-2 mt-[24px]">
<input type="checkbox" v-model="isAgree" class="w-[14px] h-[14px] rounded-[4px]" />
<span class="text-[14px] text-gray-600">我已同意</span>
<span class="text-[14px] text-sky-600">使用协议</span>
<span class="text-[14px] text-gray-600"></span>
<span class="text-[14px] text-sky-600">隐私协议</span>
</div>
</el-form>
<!-- Copy Right -->
<div class="text-[14px] text-gray-500 text-center mt-auto">
© 2025 贵州智念科技服务有限公司 版权所有
</div>
</div>
<img class="w-[570px]" src="@assets/images/login/logo.png" />
<img class="w-[540px]" src="@assets/images/login/logo.png" />
</main>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from "vue";
import { useRouter } from "vue-router";
import { authOauth2TokenUsingPost } from "@renderer/api";
import { useUserStore } from "@renderer/store/userinfo"
import { RiUser3Fill, RiKey2Fill } from '@remixicon/vue'
import { generateUUID } from "@utils/generateUUID";
import { generateUUID } from "@utils/generateUUID"
import { rule } from '@utils/validate'
// form 表单数据类型声明
interface LoginForm {
username: string;
password: string;
randomStr: string;
code: string;
}
const router = useRouter();
const form = reactive<LoginForm>({ username: "", password: "", randomStr: '', code: "" });
const userStore = useUserStore()
const form = reactive<LoginForm>({ username: "", password: "", randomStr: '', code: "", grant_type: '', scope: '' });
const loading = ref(false);
const showPwd = ref(false);
const isAgree = ref(false);
@@ -121,16 +117,14 @@ const formRef = ref()
const onSubmit = async () => {
const valid = await formRef.value.validate().catch(() => { }); // 表单校验
if (!valid) return false;
loading.value = true;
try {
// localStorage.setItem("token", "dev-token");
// const res: any = await authOauth2TokenUsingPost({ body: form });
// const token = res && (res.token || res.data?.token || res.access_token);
// if (!token) throw new Error("登录失败");
// localStorage.setItem("token", token);
// await (window as any).api.app.setFrameless('/home')
router.push('/home');
userStore.login(form).then(() => {
// window.location.href = 'index.html'
console.log(form)
})
} finally {
getVerifyCode()
loading.value = false; // 登录结束
@@ -145,4 +139,12 @@ const onSubmit = async () => {
background-position: 0 0;
background-repeat: no-repeat;
}
:deep(.el-input__wrapper) {
border-radius: 10px;
}
:deep(.el-form-item__error) {
padding-top: 8px;
}
</style>

View File

@@ -0,0 +1,15 @@
import '@renderer/styles/index.css'
import errorHandler from '@renderer/utils/errorHandler'
import i18n from '@renderer/i18n'
import HeaderBar from '@renderer/components/HeaderBar/index.vue'
import DragRegion from '@renderer/components/DragRegion/index.vue'
import Setting from './index.vue'
createApp(Setting)
.use(i18n)
.use(errorHandler)
.component('HeaderBar', HeaderBar)
.component('DragRegion', DragRegion)
.mount('#app')