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:
duanshuwen
2026-04-06 14:39:06 +08:00
parent e76b034d50
commit 6615d11dd6
311 changed files with 823682 additions and 4460 deletions

View File

@@ -0,0 +1,5 @@
<template>
<div class="mt-2 text-xs text-gray-400 ">
本回答由 AI 生成
</div>
</template>

View File

@@ -0,0 +1,34 @@
<template>
<div class="tag-flex flex-wrap pt-3">
<div class="inline-flex items-center justify-center box-border border border-[#E5E8EE] rounded-lg py-0.5 px-2.5 mr-2 mb-2"
v-for="(item, index) in questionList" :key="index" @click="handleClick(item)">
<span class="tag-text-[#2d91ff] text-[10px]">{{ item }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { onMounted } from "vue";
const props = defineProps({
question: {
type: String,
default: "",
},
});
const questionList = ref<string[]>([]);
// 定义 emit 事件,向父组件发送选中的 tag
const emit = defineEmits<{ (e: 'select', tag: string): void }>();
const handleClick = (item: string) => {
emit('select', item);
};
onMounted(() => {
questionList.value = props.question.split(/[&|;]/).filter((tag) => tag.trim() !== "");
});
</script>

View File

@@ -0,0 +1,13 @@
<template>
<img class="w-9 h-9 rounded-full shrink-0" :src="src" />
</template>
<script setup lang="ts">
interface Props {
src?: string
}
withDefaults(defineProps<Props>(), {
src: '@assets/images/login/blue_logo.png'
})
</script>

View File

@@ -0,0 +1,48 @@
<template>
<div class="h-[174px] bg-white rounded-lg border border-[#eef2f6] shadow-[0_1px_0_rgba(0,0,0,0.03)] p-4 mt-2 flex flex-col justify-between">
<textarea
rows="2"
placeholder="给我发布或者布置任务"
class="flex-1 resize-none outline-none text-sm"
:value="modelValue"
@input="onInput"
@keydown.enter="onKeydownEnter"
/>
<div class="flex justify-between items-end">
<button @click="onAttach">
<RiLink />
</button>
<button class="w-12 h-12 bg-[#F5F7FA] px-2.5 py-1.5 rounded-md flex items-center justify-center" @click="onSend">
<RiStopFill v-if="isSendingMessage" />
<RiSendPlaneFill v-else />
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'
import { RiLink, RiSendPlaneFill, RiStopFill } from '@remixicon/vue'
const props = defineProps({
modelValue: { type: String, default: '' },
isSendingMessage: { type: Boolean, default: false },
})
const emit = defineEmits(['update:modelValue', 'send', 'attach'])
const onInput = (e: Event) => {
const v = (e.target as HTMLTextAreaElement).value
emit('update:modelValue', v)
}
const onKeydownEnter = (e: KeyboardEvent) => {
if ((e as KeyboardEvent).shiftKey) return
e.preventDefault()
emit('send')
}
const onAttach = () => emit('attach')
const onSend = () => emit('send')
</script>

View File

@@ -0,0 +1,50 @@
<template>
<div class="wave">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
</template>
<script></script>
<style scoped>
.wave {
position: relative;
text-align: center;
width: 30px;
}
.dot {
display: inline-block;
width: 3px;
height: 3px;
border-radius: 50%;
margin-right: 3px;
background: #333333;
animation: wave 1.3s linear infinite;
}
.dot:nth-child(2) {
animation-delay: -1.1s;
}
.dot:nth-child(3) {
animation-delay: -0.9s;
}
@keyframes wave {
0%,
60%,
100% {
transform: initial;
}
30% {
transform: translateY(-5px);
}
}
</style>

View File

@@ -0,0 +1,65 @@
<template>
<div class="flex items-start gap-2 pt-0.5 mb-2" :class="props.showReverse ? 'flex-row-reverse' : 'flex-row'">
<span class="text-xs text-[#4E5969]">{{ props.msg?.messageRole === MessageRole.AI ? 'NIANXX' : '我' }}</span>
<span class="text-xs text-[#86909C]">{{ formattedTime }}</span>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { ChatMessage, MessageRole } from '../model/ChatModel'
interface Props {
msg?: ChatMessage
showReverse?: boolean
}
const props = withDefaults(defineProps<Props>(), {
showReverse: false
})
const formattedTime = computed(() => {
const tsRaw = props.msg?.timestamp
if (tsRaw == null) return ''
let ts = Number(tsRaw)
if (isNaN(ts)) return ''
const pad = (n: number) => String(n).padStart(2, '0')
// Heuristic:
// - If ts < 1e9, treat as a duration in seconds and convert to dd-hh-mm (legacy)
// - If ts looks like an epoch (seconds or ms) format to YYYY年MM月DD日 HH:mm:ss
if (ts < 1e9) {
const totalSeconds = Math.floor(ts)
const days = Math.floor(totalSeconds / 86400)
const hours = Math.floor((totalSeconds % 86400) / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
return `${String(days).padStart(2, '0')}-${pad(hours)}-${pad(minutes)}`
}
// epoch handling: convert seconds -> ms when appropriate
if (ts < 1e12) ts = ts * 1000
const d = new Date(ts)
if (isNaN(d.getTime())) return ''
const Y = d.getFullYear()
const M = pad(d.getMonth() + 1)
const D = pad(d.getDate())
const h = pad(d.getHours())
const m = pad(d.getMinutes())
const s = pad(d.getSeconds())
// If the timestamp is the same calendar day as today, show only time HH:mm:ss
const now = new Date()
const sameDay = now.getFullYear() === d.getFullYear()
&& now.getMonth() === d.getMonth()
&& now.getDate() === d.getDate()
if (sameDay) {
return `${h}:${m}:${s}`
}
// otherwise show YYYY-MM-DD HH:mm:ss
return `${Y}-${M}-${D} ${h}:${m}:${s}`
})
</script>

View File

@@ -0,0 +1,43 @@
<template>
<div class="mt-4 text-gray-500 flex items-center justify-between gap-4 ">
<RiFileCopyLine size="16px" @click="copyFileClick()" />
<div class="flex items-center gap-4">
<RiShareForwardLine size="16px" @click="shareForwardClick()" />
<RiDownload2Line size="16px" @click="downloadClick()" />
<RiThumbUpLine size="16px" @click="thumbUpClick()" />
<RiThumbDownLine size="16px" @click="thumbDownClick()" />
</div>
</div>
</template>
<script setup lang="ts">
import { RiFileCopyLine, RiShareForwardLine, RiDownload2Line, RiThumbUpLine, RiThumbDownLine } from '@remixicon/vue'
import { ChatMessage } from '../model/ChatModel';
interface Props {
msg: ChatMessage
}
const { msg } = defineProps<Props>()
/// actions 实现复制、分享、下载、点赞等功能
const copyFileClick = () => {
console.log('copy file', msg)
}
const shareForwardClick = () => {
console.log('share forward', msg)
}
const downloadClick = () => {
console.log('download', msg)
}
const thumbUpClick = () => {
console.log('thumb up', msg)
}
const thumbDownClick = () => {
console.log('thumb down', msg)
}
</script>

View File

@@ -0,0 +1,60 @@
<template>
<div class="max-w-[75%] flex flex-col">
<slot name="header"></slot>
<div v-if="!msg.messageContentList || msg.messageContentList.length === 0"
class="flex flex-row text-sm text-gray-700">
<div v-html="compiledMarkdown"></div>
<ChatLoading v-if="msg.isLoading" />
</div>
<div v-else class="flex flex-col p-2 mb-2 text-sm text-gray-700 bg-[#f7f9fc] rounded-md"
v-for="(_, index) in msg.messageContentList" :key="index">
<div v-html="compiledAt(index)"></div>
</div>
<slot name="footer"></slot>
</div>
</template>
<script lang="ts" setup>
import { ChatMessage } from '../model/ChatModel';
import { computed } from 'vue'
import MarkdownIt from 'markdown-it'
import hljs from 'highlight.js'
import 'highlight.js/styles/github.css'
import ChatLoading from './ChatLoading.vue';
interface Props {
msg: ChatMessage
}
const { msg } = defineProps<Props>()
const md = new MarkdownIt({
html: true,
linkify: true,
typographer: true,
highlight: function (str: string, lang: string) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(str, { language: lang, ignoreIllegals: true }).value;
} catch (__) { }
}
// 自动检测
return hljs.highlightAuto(str).value;
}
});
const compiledMarkdown = computed(() => md.render(msg.messageContent))
const compiledList = computed(() => {
return (msg.messageContentList || []).map((m: string) => md.render(m || ''))
})
const compiledAt = (index: number): string => {
const list: string[] = (compiledList as any).value || []
if (list[index]) return list[index]
const raw = msg?.messageContentList?.[index] || ''
return md.render(raw || '')
}
</script>

View File

@@ -0,0 +1,20 @@
<template>
<div class="max-w-[75%]">
<slot name="header"></slot>
<div class="text-sm text-gray-700 bg-[#f7f9fc] rounded-md px-2 py-2">
{{ msg.messageContent }}
</div>
<slot name="footer"></slot>
</div>
</template>
<script lang="ts" setup>
import { ChatMessage } from '../model/ChatModel';
interface Props {
msg: ChatMessage
}
const { msg } = defineProps<Props>()
</script>

View File

@@ -0,0 +1,119 @@
<template>
<el-dialog v-model="isVisible" :title="title" width="480" align-center>
<el-form :model="form" :rules="rules" ref="formRef" label-position="top" class="pl-4 pr-4 pt-4">
<el-form-item label="选择房型" prop="roomType">
<el-select v-model="form.roomType" placeholder="请选择房型">
<el-option v-for="item in roomList" :label="item.pmsName" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="选择日期" prop="range">
<el-date-picker v-model="form.range" type="daterange" value-format="YYYY-MM-DD" placeholder="请选择日期"
style="width: 100%">
</el-date-picker>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel">取消</el-button>
<el-button type="primary" @click="confirm">确认</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { taskCenterItem } from '@constant/taskCenterList'
import { hotelStaffTypeMappingListUsingPost } from '@api/index'
const isVisible = ref(false)
const roomList: any = ref([])
const title = ref('')
const formRef = ref()
const form = ref({
roomType: '',
operation: '',
range: [],
})
const rules = ref({
roomType: [
{ required: true, message: '请选择房型', trigger: 'blur' },
],
range: [
{
required: true,
message: '请选择日期范围',
trigger: 'change',
type: 'array'
},
],
})
// 打开弹窗
const open = ({ type }: taskCenterItem) => {
title.value = type === 'open' ? '开启渠道房型' : '关闭渠道房型'
isVisible.value = true
form.value.operation = type
getRoomTypeList()
}
// 关闭弹窗
const close = () => {
isVisible.value = false
}
// 重置form
const reset = () => {
form.value.roomType = ''
form.value.range = []
formRef.value.resetFields()
}
// 取消操作
const cancel = () => {
close()
reset()
}
// 确认操作
const confirm = () => {
formRef.value.validate((valid: boolean) => {
if (!valid) {
return
}
close()
// 将roomList.value数组处理成标准数组
const newList = roomList.value.map((item: any) => ({ ...item }))
const options = {
roomType: form.value.roomType,
startTime: form.value.range[0],
endTime: form.value.range[1],
operation: form.value.operation,
roomList: newList
}
console.log(options)
/**
* 坑传给进程的参数不能是ref包裹的reactive对象
*/
window.api.executeScript(options)
reset()
})
}
// 获取房型列表
const getRoomTypeList = async () => {
const res = await hotelStaffTypeMappingListUsingPost({ body: {} })
roomList.value = res.data
}
defineExpose({
open,
close,
})
</script>