Skip to content

Commit 5cf7765

Browse files
authored
Merge pull request #8 from qianmoQ/dev-25.0.0
feat (core): 拆分 Button 为独立组件
2 parents 533d56f + 891f2fe commit 5cf7765

File tree

8 files changed

+243
-56
lines changed

8 files changed

+243
-56
lines changed

README.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,6 @@ pnpm tauri dev
4343
pnpm tauri build
4444
```
4545

46-
## 使用
47-
48-
1. 在左侧编辑器输入代码
49-
2. 点击 "执行代码" 按钮执行
50-
3. 在右侧面板查看输出结果
51-
5246
## 技术栈
5347

5448
- **前端:** Vue 3 + TypeScript + Tailwind CSS

src-tauri/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ async fn execute_code(
6464
for cmd in plugin.get_commands() {
6565
let args = plugin.get_execute_args(file_path.to_str().unwrap());
6666
debug!(
67-
"执行插件代码 -> 执行命令: {} 携带参数: {:?} 语言: {}",
68-
cmd, args, request.language
67+
"执行 {:?} 代码 -> 执行命令: {:?} 携带参数: {:?}",
68+
request.language, cmd, args
6969
);
7070

7171
let output = Command::new(cmd)

src-tauri/tauri.conf.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
"windows": [
1414
{
1515
"title": "CodeForge",
16+
"minWidth": 1000,
17+
"minHeight": 600,
1618
"width": 1800,
1719
"height": 1200,
1820
"additionalBrowserArgs": "--disable-context-menu"

src/components/About.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<!-- 关闭按钮 -->
1212
<div class="flex justify-end mb-4">
1313
<button
14-
class="text-gray-400 hover:cursor-pointer hover:text-gray-600 dark:hover:text-gray-200 transition-all duration-200 hover:scale-110 rounded-full p-1 hover:bg-gray-100 dark:hover:bg-gray-700"
14+
class="text-gray-400 hover:cursor-pointer dark:hover:text-gray-200 transition-all duration-200 hover:scale-110 rounded-full p-1 hover:bg-gray-300 dark:hover:bg-gray-700"
1515
@click="closeAbout">
1616
<X class="w-5 h-5"></X>
1717
</button>
@@ -61,7 +61,7 @@
6161
<!-- 技术栈 -->
6262
<div class="mb-6 transition-all duration-600 delay-500">
6363
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">技术栈</h3>
64-
<hr class="border-gray-200/50 dark:border-gray-600/50 my-3" />
64+
<hr class="border-gray-200/50 dark:border-gray-600/50 my-3"/>
6565
<div class="flex flex-wrap gap-2">
6666
<span v-for="(tech, index) in techStack"
6767
class="px-3 py-1 text-xs rounded-full font-medium transform transition-all duration-200 hover:scale-110 hover:-translate-y-1 cursor-pointer"

src/components/AppHeader.vue

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<!-- 自定义下拉选择器 -->
66
<div class="relative">
77
<Select v-model="selectedLanguage"
8+
class="w-48"
89
:options="supportedLanguages as any"
910
:disabled="isRunning"
1011
placeholder="选择语言"
@@ -17,23 +18,18 @@
1718
</div>
1819

1920
<div class="flex items-center space-x-3">
20-
<button @click="$emit('run-code')"
21+
<Button @click="$emit('run-code')"
2122
:disabled="isRunning || !envInstalled"
22-
:class="['flex items-center space-x-2 px-3 py-1.5 rounded-md font-medium transition-all duration-200',
23-
isRunning || !envInstalled
24-
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
25-
: 'btn-success shadow-sm hover:shadow-md cursor-pointer'
26-
]">
27-
<component :is="isRunning ? Square : Play" class="w-3 h-3"/>
23+
:icon="isRunning ? Square : Play">
2824
<span>{{ isRunning ? '运行中...' : '运行代码' }}</span>
29-
</button>
25+
</Button>
3026

31-
<button class="btn-danger px-3 py-2.5 rounded-md font-medium transition-all duration-200"
27+
<Button @click="$emit('clear-output')"
3228
:disabled="isRunning || !envInstalled"
33-
:class="[isRunning || !envInstalled ? 'cursor-not-allowed' : 'cursor-pointer']"
34-
@click="$emit('clear-output')">
35-
<Trash2 class="w-4 h-4"/>
36-
</button>
29+
type="danger"
30+
:icon-only="true"
31+
:icon="Trash2">
32+
</Button>
3733
</div>
3834
</div>
3935
</template>
@@ -42,6 +38,7 @@
4238
import { ref, watch } from 'vue'
4339
import { Play, Square, Trash2 } from 'lucide-vue-next'
4440
import Select from '../ui/Select.vue'
41+
import Button from '../ui/Button.vue'
4542
4643
interface Language
4744
{

src/components/Settings.vue

Lines changed: 20 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<div class="fixed inset-0 backdrop-blur-sm bg-white/20 dark:bg-gray-900/20 flex items-center justify-center z-50 transition-all duration-300 ease-out"
33
:class="{ 'opacity-0': !isVisible, 'opacity-100': isVisible }">
44
<div
5-
class="bg-white/95 dark:bg-gray-800/95 backdrop-blur-md rounded-xl shadow-2xl w-4xl p-6 transform transition-all duration-300 ease-out border border-white/20 dark:border-gray-700/30 max-h-[80vh] overflow-y-auto"
5+
class="bg-white/95 dark:bg-gray-800/95 backdrop-blur-md rounded-xl shadow-2xl w-6xl h-screen p-6 transform transition-all duration-300 ease-out border border-white/20 dark:border-gray-700/30 max-h-[80vh] overflow-y-auto"
66
:class="{
77
'scale-95 opacity-0 translate-y-4': !isVisible,
88
'scale-100 opacity-100 translate-y-0': isVisible
@@ -47,32 +47,29 @@
4747
placeholder="选择或输入日志目录路径"
4848
class="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
4949
readonly/>
50-
<button
51-
class="px-4 py-2 cursor-pointer bg-blue-500 hover:bg-blue-600 text-white text-sm font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
52-
@click="selectLogDirectory">
53-
<Folder class="w-4 h-4"/>
54-
</button>
50+
51+
<Button type="primary"
52+
:icon-only="true"
53+
:icon="Folder"
54+
@click="selectLogDirectory">
55+
</Button>
5556
</div>
5657
</div>
5758

5859
<!-- 操作按钮 -->
59-
<div class="flex gap-3 pt-2">
60-
<button
61-
@click="applyLogDirChange"
62-
:disabled="!newLogDir || newLogDir === currentLogDir"
63-
class="cursor-pointer px-4 py-2 bg-green-500 hover:bg-green-600 disabled:bg-gray-300 disabled:cursor-not-allowed text-white text-sm font-medium rounded-md transition-colors">
60+
<div class="flex gap-3 pt-0.5">
61+
<Button @click="applyLogDirChange"
62+
:disabled="!newLogDir || newLogDir === currentLogDir"
63+
type="secondary">
6464
应用更改
65-
</button>
66-
<button
67-
@click="openLogDirectory"
68-
class="cursor-pointer px-4 py-2 bg-gray-500 hover:bg-gray-600 text-white text-sm font-medium rounded-md transition-colors">
65+
</Button>
66+
<Button @click="openLogDirectory" type="secondary">
6967
打开日志目录
70-
</button>
71-
<button
72-
@click="resetLogDirectory"
73-
class="cursor-pointer px-4 py-2 bg-orange-500 hover:bg-orange-600 text-white text-sm font-medium rounded-md transition-colors">
68+
</Button>
69+
<Button @click="resetLogDirectory"
70+
class="cursor-pointer px-4 py-2 bg-orange-500 hover:bg-orange-600 text-white text-sm font-medium rounded-md transition-colors">
7471
重置为默认
75-
</button>
72+
</Button>
7673
</div>
7774

7875
<!-- 日志文件列表 -->
@@ -110,23 +107,13 @@
110107
:options="keepDaysOptions"
111108
placeholder="选择保留天数">
112109
</Select>
113-
<button class="cursor-pointer px-4 py-2 bg-red-500 hover:bg-red-600 text-white text-sm font-medium rounded-md transition-colors"
114-
@click="clearLogs">
110+
<Button type="danger" @click="clearLogs">
115111
立即清理
116-
</button>
112+
</Button>
117113
</div>
118114
</div>
119115
</div>
120116
</div>
121-
122-
<!-- 底部按钮 -->
123-
<div class="flex justify-end gap-3 pt-4 border-t border-gray-200/50 dark:border-gray-600/50">
124-
<button
125-
@click="closeSettings"
126-
class="cursor-pointer px-4 py-2 bg-gray-500 hover:bg-gray-600 text-white text-sm font-medium rounded-md transition-colors">
127-
关闭
128-
</button>
129-
</div>
130117
</div>
131118
</div>
132119
</template>
@@ -139,6 +126,7 @@ import { openPath } from '@tauri-apps/plugin-opener'
139126
import { FileText, Folder, Settings2, X } from 'lucide-vue-next'
140127
import Select from '../ui/Select.vue'
141128
import { useToast } from '../plugins/toast'
129+
import Button from '../ui/Button.vue'
142130
143131
const toast = useToast()
144132
const isVisible = ref(false)

src/ui/Button.vue

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
<template>
2+
<button :type="htmlType"
3+
:disabled="disabled || loading"
4+
:class="buttonClasses"
5+
class="flex items-center space-x-2 justify-center rounded-md transition-all duration-200 cursor-pointer"
6+
@click="handleClick"
7+
v-bind="$attrs">
8+
<!-- 加载状态图标 -->
9+
<div v-if="loading" class="animate-spin mr-2">
10+
<Loader2 class="w-4 h-4"/>
11+
</div>
12+
13+
<!-- 左侧图标 -->
14+
<component v-else-if="icon && iconPosition === 'left'"
15+
:is="icon"
16+
:class="iconClasses"/>
17+
18+
<!-- 按钮文本 -->
19+
<span v-if="$slots.default || text" :class="textClasses">
20+
<slot>{{ text }}</slot>
21+
</span>
22+
23+
<!-- 右侧图标 -->
24+
<component v-if="icon && iconPosition === 'right' && !loading"
25+
:is="icon"
26+
:class="iconClasses"/>
27+
</button>
28+
</template>
29+
30+
<script setup lang="ts">
31+
import type { Component } from 'vue'
32+
import { computed } from 'vue'
33+
import { Loader2 } from 'lucide-vue-next'
34+
35+
interface Props
36+
{
37+
// 按钮类型
38+
type?: 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'ghost' | 'link'
39+
// 按钮尺寸
40+
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
41+
// 按钮变体
42+
variant?: 'solid' | 'outline' | 'ghost' | 'soft'
43+
// HTML 按钮类型
44+
htmlType?: 'button' | 'submit' | 'reset'
45+
// 禁用状态
46+
disabled?: boolean
47+
// 加载状态
48+
loading?: boolean
49+
// 圆形按钮
50+
circle?: boolean
51+
// 圆角按钮
52+
round?: boolean
53+
// 块级按钮
54+
block?: boolean
55+
// 图标
56+
icon?: Component | string
57+
// 图标位置
58+
iconPosition?: 'left' | 'right'
59+
// 仅图标按钮
60+
iconOnly?: boolean
61+
// 按钮文本
62+
text?: string
63+
// 自定义类名
64+
customClass?: string | string[]
65+
}
66+
67+
const props = withDefaults(defineProps<Props>(), {
68+
type: 'primary',
69+
size: 'md',
70+
variant: 'solid',
71+
htmlType: 'button',
72+
disabled: false,
73+
loading: false,
74+
circle: false,
75+
round: false,
76+
block: false,
77+
iconPosition: 'left',
78+
iconOnly: false
79+
})
80+
81+
const emit = defineEmits<{
82+
click: [event: MouseEvent]
83+
}>()
84+
85+
// 基础样式
86+
const baseClasses = computed(() => [
87+
'inline-flex items-center justify-center font-medium transition-all duration-200',
88+
'focus:outline-none focus:ring-2 focus:ring-offset-2',
89+
'disabled:cursor-not-allowed disabled:opacity-50',
90+
// 圆形按钮
91+
props.circle ? 'rounded-full' : props.round ? 'rounded-full' : 'rounded-lg',
92+
// 块级按钮
93+
props.block ? 'w-full' : '',
94+
// 仅图标按钮
95+
props.iconOnly ? 'p-0' : ''
96+
])
97+
98+
// 尺寸样式
99+
const sizeClasses = computed(() => {
100+
const sizes = {
101+
xs: props.iconOnly ? 'w-6 h-6' : 'px-2 py-1 text-xs',
102+
sm: props.iconOnly ? 'w-8 h-8' : 'px-3 py-1.5 text-sm',
103+
md: props.iconOnly ? 'w-10 h-10' : 'px-4 py-2 text-sm',
104+
lg: props.iconOnly ? 'w-12 h-12' : 'px-6 py-3 text-base',
105+
xl: props.iconOnly ? 'w-14 h-14' : 'px-8 py-4 text-lg'
106+
}
107+
return sizes[props.size]
108+
})
109+
110+
// 类型和变体样式
111+
const typeClasses = computed(() => {
112+
const { type, variant } = props
113+
114+
const styles = {
115+
primary: {
116+
solid: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500 border border-transparent',
117+
outline: 'border border-blue-600 text-blue-600 hover:bg-blue-50 focus:ring-blue-500',
118+
ghost: 'text-blue-600 hover:bg-blue-50 focus:ring-blue-500 border border-transparent',
119+
soft: 'bg-blue-50 text-blue-600 hover:bg-blue-100 focus:ring-blue-500 border border-transparent'
120+
},
121+
secondary: {
122+
solid: 'bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500 border border-transparent',
123+
outline: 'border border-gray-300 text-gray-700 hover:bg-gray-50 focus:ring-gray-500',
124+
ghost: 'text-gray-700 hover:bg-gray-50 focus:ring-gray-500 border border-transparent',
125+
soft: 'bg-gray-50 text-gray-700 hover:bg-gray-100 focus:ring-gray-500 border border-transparent'
126+
},
127+
success: {
128+
solid: 'bg-green-600 text-white hover:bg-green-700 focus:ring-green-500 border border-transparent',
129+
outline: 'border border-green-600 text-green-600 hover:bg-green-50 focus:ring-green-500',
130+
ghost: 'text-green-600 hover:bg-green-50 focus:ring-green-500 border border-transparent',
131+
soft: 'bg-green-50 text-green-600 hover:bg-green-100 focus:ring-green-500 border border-transparent'
132+
},
133+
warning: {
134+
solid: 'bg-yellow-500 text-white hover:bg-yellow-600 focus:ring-yellow-500 border border-transparent',
135+
outline: 'border border-yellow-500 text-yellow-600 hover:bg-yellow-50 focus:ring-yellow-500',
136+
ghost: 'text-yellow-600 hover:bg-yellow-50 focus:ring-yellow-500 border border-transparent',
137+
soft: 'bg-yellow-50 text-yellow-600 hover:bg-yellow-100 focus:ring-yellow-500 border border-transparent'
138+
},
139+
danger: {
140+
solid: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500 border border-transparent',
141+
outline: 'border border-red-600 text-red-600 hover:bg-red-50 focus:ring-red-500',
142+
ghost: 'text-red-600 hover:bg-red-50 focus:ring-red-500 border border-transparent',
143+
soft: 'bg-red-50 text-red-600 hover:bg-red-100 focus:ring-red-500 border border-transparent'
144+
},
145+
info: {
146+
solid: 'bg-cyan-600 text-white hover:bg-cyan-700 focus:ring-cyan-500 border border-transparent',
147+
outline: 'border border-cyan-600 text-cyan-600 hover:bg-cyan-50 focus:ring-cyan-500',
148+
ghost: 'text-cyan-600 hover:bg-cyan-50 focus:ring-cyan-500 border border-transparent',
149+
soft: 'bg-cyan-50 text-cyan-600 hover:bg-cyan-100 focus:ring-cyan-500 border border-transparent'
150+
},
151+
link: {
152+
solid: 'text-blue-600 hover:text-blue-700 underline focus:ring-blue-500 border border-transparent bg-transparent p-0',
153+
outline: 'text-blue-600 hover:text-blue-700 underline focus:ring-blue-500 border border-transparent bg-transparent p-0',
154+
ghost: 'text-blue-600 hover:text-blue-700 underline focus:ring-blue-500 border border-transparent bg-transparent p-0',
155+
soft: 'text-blue-600 hover:text-blue-700 underline focus:ring-blue-500 border border-transparent bg-transparent p-0'
156+
}
157+
}
158+
159+
return styles[type as keyof typeof styles]?.[variant] || styles.primary.solid
160+
})
161+
162+
// 图标样式
163+
const iconClasses = computed(() => {
164+
const hasText = props.text || !!(props as any).$slots?.default
165+
const sizeMap = {
166+
xs: 'w-3 h-3',
167+
sm: 'w-4 h-4',
168+
md: 'w-4 h-4',
169+
lg: 'w-5 h-5',
170+
xl: 'w-6 h-6'
171+
}
172+
173+
const marginMap = {
174+
left: hasText && !props.iconOnly ? 'mr-2' : '',
175+
right: hasText && !props.iconOnly ? 'ml-2' : ''
176+
}
177+
178+
return [
179+
sizeMap[props.size],
180+
marginMap[props.iconPosition]
181+
].filter(Boolean)
182+
})
183+
184+
// 文本样式
185+
const textClasses = computed(() => {
186+
if (props.iconOnly) {
187+
return 'sr-only'
188+
}
189+
return ''
190+
})
191+
192+
// 最终样式组合
193+
const buttonClasses = computed(() => [
194+
...baseClasses.value,
195+
sizeClasses.value,
196+
typeClasses.value,
197+
...(Array.isArray(props.customClass) ? props.customClass : [props.customClass]).filter(Boolean)
198+
])
199+
200+
// 点击处理
201+
const handleClick = (event: MouseEvent) => {
202+
if (!props.disabled && !props.loading) {
203+
emit('click', event)
204+
}
205+
}
206+
</script>

0 commit comments

Comments
 (0)