feat: 合并代码

This commit is contained in:
duanshuwen
2025-12-21 20:25:38 +08:00
parent 11702dc9d2
commit 7d6b07e092
56 changed files with 2533 additions and 379 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,35 @@
import { createApp } from "vue";
import { createApp, type Plugin } from "vue";
import { createPinia } from "pinia";
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)
// 挂载应用到 DOM
app.mount("#app");

View File

@@ -1,77 +1,18 @@
import { createRouter, createWebHistory } from "vue-router";
import Layout from '@renderer/layout/index.vue'
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: "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
}
@@ -80,24 +21,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

@@ -0,0 +1,18 @@
import type { Plugin } from 'vue'
import logger from './logger'
export const errorHandler: Plugin = (app) => {
app.config.errorHandler = (err, instance, info) => {
logger.error('Vue error:', err, instance, 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,12 @@
export const logger = 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,86 +1,90 @@
<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 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 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 { RiUser3Fill, RiKey2Fill } from '@remixicon/vue'
import { generateUUID } from "@utils/generateUUID";
@@ -145,4 +149,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')