chore: restructure project and add i18n support
- Reorganize project structure with new electron and shared directories - Add comprehensive i18n support with Chinese, English, and Japanese locales - Update build configurations and TypeScript paths for new structure - Add various UI components including chat interface and task management - Include Windows release binaries and localization files - Update dependencies and fix import paths throughout the codebase
This commit is contained in:
13
src/components/DragRegion/index.vue
Normal file
13
src/components/DragRegion/index.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div class="drag-region">
|
||||
<slot>
|
||||
<span class="_placeholder">_hidden</span>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
._placeholder {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
73
src/components/HeaderBar/index.vue
Normal file
73
src/components/HeaderBar/index.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<header class="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="color" :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="color" :width="btnSize"
|
||||
:height="btnSize" v-show="!isMaximized" />
|
||||
<iconify-icon icon="material-symbols:chrome-restore-outline-sharp" :color="color" :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="color" :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;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'HeaderBar', color: '#525866' })
|
||||
|
||||
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></style>
|
||||
24
src/components/Layout/index.vue
Normal file
24
src/components/Layout/index.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="bg-color h-screen flex flex-col">
|
||||
<header-bar>
|
||||
<drag-region class="w-full" />
|
||||
</header-bar>
|
||||
|
||||
<main class="box-border w-full h-[calc(100vh-40px)] flex pt-[8px] pb-[8px] pl-[8px] ">
|
||||
<div class="flex-1 flex">
|
||||
<slot />
|
||||
</div>
|
||||
<SideMenus />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="Layout">
|
||||
import SideMenus from '@src/components/SideMenus/index.vue'
|
||||
</script>
|
||||
<style scoped>
|
||||
.bg-color {
|
||||
background: linear-gradient(180deg, #EFF6FF 0%, #F5F7FA 40%);
|
||||
}
|
||||
</style>
|
||||
43
src/components/NativeTooltip/index.vue
Normal file
43
src/components/NativeTooltip/index.vue
Normal 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>
|
||||
49
src/components/Pagination/index.vue
Normal file
49
src/components/Pagination/index.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<el-pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" class="mt-[15px]"
|
||||
:pager-count="5" :page-sizes="pageSizes" :current-page="current" background :page-size="size" :layout="layout"
|
||||
:total="total">
|
||||
</el-pagination>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="pagination">
|
||||
const emit = defineEmits(['sizeChange', 'currentChange']);
|
||||
|
||||
const props = defineProps({
|
||||
current: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 10,
|
||||
},
|
||||
total: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
pageSizes: {
|
||||
type: Array as () => number[],
|
||||
default: () => {
|
||||
return [10, 20, 50, 100, 200];
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
type: String,
|
||||
default: 'total, sizes, prev, pager, next, jumper',
|
||||
},
|
||||
});
|
||||
// 分页改变
|
||||
const sizeChangeHandle = (val: number) => {
|
||||
emit('sizeChange', val);
|
||||
};
|
||||
// 分页改变
|
||||
const currentChangeHandle = (val: number) => {
|
||||
emit('currentChange', val);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.el-pagination__sizes) {
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
37
src/components/SideMenus/index.vue
Normal file
37
src/components/SideMenus/index.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="w-[80px] h-full box-border flex flex-col items-center pb-[8px]">
|
||||
<div :class="['flex flex-col gap-[16px]', { 'mt-auto mb-[8px] shrink-1': item.id === 4 }]"
|
||||
v-for="(item) in menus" :key="item.id">
|
||||
<div :class="['cursor-pointer flex flex-col items-center justify-center']" @click="handleClick(item)">
|
||||
<div :class="['box-border rounded-[16px] p-[8px]', { 'bg-white': item.id === currentId }]">
|
||||
<component :is="item.icon" :color="item.id === currentId ? item.activeColor : item.color"
|
||||
:class="['w-[32px] h-[32px]']" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
:class="['text-[14px] mt-[4px] mb-[8px]', item.id === currentId ? `text-[${item.activeColor}]` : item.color]">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-[48px] h-[48px] rounded-full overflow-hidden">
|
||||
<img class="w-full h-full object-cover" src="@assets/images/login/black_logo.png" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { menus, type MenuItem } from '@constant/menus'
|
||||
|
||||
const currentId = ref(1)
|
||||
const router = useRouter()
|
||||
|
||||
const handleClick = async (item: MenuItem) => {
|
||||
console.log("🚀 ~ handleClick ~ item:", item)
|
||||
currentId.value = item.id
|
||||
router.push(item.url)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
63
src/components/TaskList/Card.vue
Normal file
63
src/components/TaskList/Card.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<!--
|
||||
* @Author: kongbeiwu lishaohua-520@qq.com
|
||||
* @Date: 2025-12-21 23:02:06
|
||||
* @LastEditors: kongbeiwu lishaohua-520@qq.com
|
||||
* @LastEditTime: 2025-12-28 11:09:00
|
||||
* @FilePath: /project/zn-ai/src/renderer/components/TaskList/Card.vue
|
||||
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||
-->
|
||||
<template>
|
||||
<div v-for="item in task" :key="item.id"
|
||||
class="border border-solid border-[#E5E8EE] rounded-[12px] p-[12px] mb-[12px] task">
|
||||
<div class="flex items-center pb-[12px]" style="border-bottom: 1px dashed #E5E8EE;">
|
||||
<!-- <img class="w-[24px] h-[24px] rounded-[8px]] mr-[4px]" src="@assets/images/task/xc.png" /> -->
|
||||
<div
|
||||
class="w-[24px] h-[24px] rounded-[4px] bg-[#EFF6FF] text-[#2B7FFF] text-[14px] font-bold border border-solid border-[#BEDBFF] flex justify-center items-center">
|
||||
{{ item.name[0] }}</div>
|
||||
<div class="text-[16px] text-[#171717] font-bold mr-[8px] ml-[4px]">{{ item.name }}</div>
|
||||
<div class="pl-[8px] pr-[8px] text-[12px] rounded-[100px]" :class="item.statusColor">{{
|
||||
item.statusText }}</div>
|
||||
</div>
|
||||
<div class="flex items-center mt-[12px]">
|
||||
<component :is="item.desIcon" :color="item.color" class="w-[15px] mr-[4px]" />
|
||||
<div class="text-[14px]" :class="`text-[${item.color}]`" :style="{ color: item.color }">{{ item.des }}</div>
|
||||
</div>
|
||||
<div class="mt-[24px]">
|
||||
<button class="w-[100%] h-[40px] bg-[#2B7FFF] text-white text-[14px] rounded-[12px]">{{ item.statusColor !==
|
||||
'error' ? '查看' : '处理' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from "vue";
|
||||
import { task } from '@constant/task'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
transition: all .2s linear;
|
||||
}
|
||||
|
||||
.task .success {
|
||||
background-color: #E0FAEC;
|
||||
color: #1FC16B;
|
||||
}
|
||||
|
||||
.task .error {
|
||||
background-color: #FFEBEC;
|
||||
color: #FB3748;
|
||||
}
|
||||
|
||||
.task .warning {
|
||||
background-color: #FFF3EB;
|
||||
color: #FA7319;
|
||||
}
|
||||
|
||||
.task:hover {
|
||||
z-index: 2;
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, .1);
|
||||
transform: translate3d(0, -2px, 0);
|
||||
}</style>
|
||||
59
src/components/TaskList/List.vue
Normal file
59
src/components/TaskList/List.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="task p-[12px]">
|
||||
<div class="flex border border-[#BEDBFF] h-[48px] p-[4px] rounded-[10px] bg-[#EFF6FF] task-tab">
|
||||
<div v-for="item in tabs" :key="item.value" class="flex-1 flex text-center items-center h-full align-middle text" :class="active === item.value && 'active'" @click="changeTab(item.value)">
|
||||
<div class="flex-1">{{ item.name }}<span v-if="item.total">{{`(${item.total > 98 && item.total + '+' || item.total})`}}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between mt-[12px] mb-[12px] text-[14px]">
|
||||
<div class="text-[#171717]">今天</div>
|
||||
<div class="text-[#99A0AE]">02:32:05</div>
|
||||
</div>
|
||||
<div>
|
||||
<TaskCard />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TaskCard from './Card.vue';
|
||||
import { ref, reactive } from "vue";
|
||||
|
||||
const tabs = reactive([{
|
||||
name: '待处理',
|
||||
value: 1,
|
||||
total: 10,
|
||||
},{
|
||||
name: '已处理',
|
||||
value: 2,
|
||||
total: 99,
|
||||
}])
|
||||
const active = ref(1);
|
||||
const changeTab = (val:number) => {
|
||||
active.value = val;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-tab .text {
|
||||
color: #525866;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.task-tab .active {
|
||||
position: relative;
|
||||
color: #2B7FFF;
|
||||
background: #FFFFFF;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.task-tab .active::after {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: '';
|
||||
border-radius: 8px;
|
||||
border: 1px solid #2B7FFF;
|
||||
}
|
||||
</style>
|
||||
51
src/components/TaskList/index.vue
Normal file
51
src/components/TaskList/index.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div class="task-list w-[392px] h-full rounded-[16px] bg-white">
|
||||
<slot>
|
||||
<TaskList />
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TaskList from './List.vue';
|
||||
import { ref, reactive } from "vue";
|
||||
const tabs = reactive([{
|
||||
name: '待处理',
|
||||
value: 1,
|
||||
total: 10,
|
||||
}, {
|
||||
name: '已处理',
|
||||
value: 2,
|
||||
total: 99,
|
||||
}])
|
||||
const active = ref(1);
|
||||
const changeTab = (val: number) => {
|
||||
active.value = val;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-tab .text {
|
||||
color: #525866;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.task-tab .active {
|
||||
position: relative;
|
||||
color: #2B7FFF;
|
||||
background: #FFFFFF;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.task-tab .active::after {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: '';
|
||||
border-radius: 8px;
|
||||
border: 1px solid #2B7FFF;
|
||||
}
|
||||
</style>
|
||||
44
src/components/TitleSection/index.vue
Normal file
44
src/components/TitleSection/index.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<!--
|
||||
* @Author: kongbeiwu lishaohua-520@qq.com
|
||||
* @Date: 2025-12-21 23:02:06
|
||||
* @LastEditors: kongbeiwu lishaohua-520@qq.com
|
||||
* @LastEditTime: 2026-01-04 10:35:46
|
||||
* @FilePath: /project/zn-ai/src/renderer/components/TitleSection/index.vue
|
||||
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||
-->
|
||||
<template>
|
||||
<div class="flex justify-between items-center box-border border-b-[1px] border-b-[#E5E8EE] mb-[20px] pb-[20px]">
|
||||
<div class="flex">
|
||||
<div class="flex items-center">
|
||||
<el-icon v-if="attrs.onBackTo" @click="emits('back-to', true)" size="18px" color="#525866" class="mr-[15px] cursor-pointer">
|
||||
<ArrowLeftBold />
|
||||
</el-icon>
|
||||
<span class="text-[24px] font-500 text-[#171717] leading-[32px] mr-[8px]">
|
||||
{{ title }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-[12px] font-400 text-[#99A0AE] leading-[16px]" style="align-self: flex-end;">
|
||||
{{ desc }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<slot name="right"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAttrs, defineEmits } from 'vue';
|
||||
import { ArrowLeftBold } from '@element-plus/icons-vue';
|
||||
interface TitleSectionProps {
|
||||
title?: string
|
||||
desc?: string
|
||||
}
|
||||
|
||||
withDefaults(
|
||||
defineProps<TitleSectionProps>(),
|
||||
{ title: '', desc: '' }
|
||||
)
|
||||
const emits = defineEmits(["update:back-to"]);
|
||||
const attrs = useAttrs();
|
||||
</script>
|
||||
Reference in New Issue
Block a user