refactor: 精简前端
This commit is contained in:
parent
9b5766ba65
commit
71960aed6d
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div ref="processRef" class="process-block" :class="{ 'is-streaming': streaming }">
|
||||
<div class="process-block" :class="{ 'is-streaming': streaming }">
|
||||
<!-- Single loop: render all steps in index order for proper alternation -->
|
||||
<template v-for="item in orderedItems">
|
||||
<!-- Thinking Step -->
|
||||
|
|
@ -77,7 +77,6 @@ const props = defineProps({
|
|||
streaming: { type: Boolean, default: false },
|
||||
})
|
||||
|
||||
const processRef = ref(null)
|
||||
const expandedKeys = ref(new Set())
|
||||
|
||||
// 构建 processItems 从 processSteps
|
||||
|
|
@ -191,24 +190,6 @@ const orderedItems = computed(() =>
|
|||
[...allItems.value].sort((a, b) => (a.index || 0) - (b.index || 0))
|
||||
)
|
||||
|
||||
const orderedThinkingItems = computed(() =>
|
||||
allItems.value
|
||||
.filter(i => i.type === 'thinking')
|
||||
.sort((a, b) => (a.index || 0) - (b.index || 0))
|
||||
)
|
||||
|
||||
const orderedToolCallItems = computed(() =>
|
||||
allItems.value
|
||||
.filter(i => i.type === 'tool_call')
|
||||
.sort((a, b) => (a.index || 0) - (b.index || 0))
|
||||
)
|
||||
|
||||
const orderedTextItems = computed(() =>
|
||||
allItems.value
|
||||
.filter(i => i.type === 'text')
|
||||
.sort((a, b) => (a.index || 0) - (b.index || 0))
|
||||
)
|
||||
|
||||
const hasContent = computed(() => allItems.value.length > 0)
|
||||
const lastThinkingKey = computed(() => {
|
||||
const items = allItems.value.filter(i => i.type === 'thinking')
|
||||
|
|
@ -377,8 +358,6 @@ const alertIcon = `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" s
|
|||
animation: pulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.tool_call.loading .step-header { background: var(--bg-hover); }
|
||||
|
||||
.step-content { padding: 12px; margin-top: 4px; background: var(--bg-code); border: 1px solid var(--border-light); border-radius: 8px; overflow: hidden; }
|
||||
.thinking-text { font-size: 13px; color: var(--text-secondary); line-height: 1.6; white-space: pre-wrap; }
|
||||
.tool-detail { font-size: 13px; }
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
// 导出组件
|
||||
export { default as AppHeader } from './components/AppHeader.vue'
|
||||
export { default as ProcessBlock } from './components/ProcessBlock.vue'
|
||||
export { default as MessageBubble } from './components/MessageBubble.vue'
|
||||
export { default as MessageNav } from './components/MessageNav.vue'
|
||||
|
|
@ -386,20 +386,6 @@ body {
|
|||
transition: background 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
/* ============ App Layout ============ */
|
||||
.app {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-panel {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
/* ============ Transitions ============ */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
|
|
|
|||
|
|
@ -36,121 +36,6 @@ api.interceptors.response.use(
|
|||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* SSE 流式请求处理器
|
||||
* @param {string} url - API URL (不含 baseURL 前缀)
|
||||
* @param {object} body - 请求体
|
||||
* @param {object} callbacks - 事件回调: { onProcessStep, onDone, onError }
|
||||
* @returns {{ abort: () => void }}
|
||||
*/
|
||||
export function createSSEStream(url, body, { onProcessStep, onDone, onError }) {
|
||||
const token = localStorage.getItem('access_token')
|
||||
const controller = new AbortController()
|
||||
|
||||
const promise = (async () => {
|
||||
try {
|
||||
const res = await fetch(`/api${url}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: controller.signal
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}))
|
||||
throw new Error(err.message || `HTTP ${res.status}`)
|
||||
}
|
||||
|
||||
const reader = res.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
let completed = false
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
|
||||
// 处理数据
|
||||
if (value) {
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
}
|
||||
|
||||
// 流结束时,先处理 buffer 中的剩余数据,再 break
|
||||
if (done) {
|
||||
// 处理 buffer 中剩余的数据
|
||||
const lines = buffer.split('\n')
|
||||
buffer = ''
|
||||
|
||||
let currentEvent = ''
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('event: ')) {
|
||||
currentEvent = line.slice(7).trim()
|
||||
} else if (line.startsWith('data: ')) {
|
||||
try {
|
||||
const data = JSON.parse(line.slice(6))
|
||||
if (currentEvent === 'process_step' && onProcessStep) {
|
||||
onProcessStep(data.step)
|
||||
} else if (currentEvent === 'done' && onDone) {
|
||||
completed = true
|
||||
onDone(data)
|
||||
} else if (currentEvent === 'error' && onError) {
|
||||
onError(data.content)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('SSE parse error:', e, 'line:', line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有收到 done 事件,触发错误
|
||||
if (!completed && onError) {
|
||||
onError('stream ended without done event')
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
const lines = buffer.split('\n')
|
||||
buffer = lines.pop() || ''
|
||||
|
||||
let currentEvent = ''
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('event: ')) {
|
||||
currentEvent = line.slice(7).trim()
|
||||
} else if (line.startsWith('data: ')) {
|
||||
try {
|
||||
const data = JSON.parse(line.slice(6))
|
||||
if (currentEvent === 'process_step' && onProcessStep) {
|
||||
onProcessStep(data.step)
|
||||
} else if (currentEvent === 'done' && onDone) {
|
||||
completed = true
|
||||
onDone(data)
|
||||
} else if (currentEvent === 'error' && onError) {
|
||||
onError(data.content)
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略解析错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 流结束但没有收到 done 事件,才报错
|
||||
if (!completed && onError) {
|
||||
onError('stream ended unexpectedly')
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.name !== 'AbortError' && onError) {
|
||||
onError(e.message)
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
promise.abort = () => controller.abort()
|
||||
return promise
|
||||
}
|
||||
|
||||
// ============ 认证接口 ============
|
||||
|
||||
export const authAPI = {
|
||||
|
|
@ -177,17 +62,6 @@ export const conversationsAPI = {
|
|||
export const messagesAPI = {
|
||||
list: (conversationId, params) => api.get('/messages/', { params: { conversation_id: conversationId, ...params } }),
|
||||
send: (data) => api.post('/messages/', data),
|
||||
|
||||
// 发送消息(流式)
|
||||
sendStream: (data, callbacks) => {
|
||||
return createSSEStream('/messages/stream', {
|
||||
conversation_id: data.conversation_id,
|
||||
content: data.content,
|
||||
thinking_enabled: data.thinking_enabled || false,
|
||||
enabled_tools: data.enabled_tools || []
|
||||
}, callbacks)
|
||||
},
|
||||
|
||||
delete: (id) => api.delete(`/messages/${id}`)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1 @@
|
|||
/**
|
||||
* Luxx 前端工具库
|
||||
* 合并了 composables、services 和 stores 的统一导出
|
||||
*/
|
||||
|
||||
// ============ API 服务 ============
|
||||
export { default as api, authAPI, conversationsAPI, messagesAPI, toolsAPI, providersAPI, createSSEStream } from './api.js'
|
||||
|
||||
// ============ Pinia 状态管理 ============
|
||||
export { default as pinia } from './store.js'
|
||||
|
||||
// ============ 认证相关 ============
|
||||
export { useAuth } from './useAuth.js'
|
||||
|
||||
// ============ API 请求组合式函数 ============
|
||||
export { useApi, usePagination, useForm } from './useApi.js'
|
||||
|
||||
// ============ 格式化工具 ============
|
||||
export { formatDate, formatNumber, truncate, formatFileSize, capitalize, formatTokens } from './useFormatters.js'
|
||||
|
||||
// ============ 通用工具函数 ============
|
||||
export { debounce, throttle, deepClone, generateId, storage, getDeviceType, copyToClipboard } from './useUtils.js'
|
||||
|
||||
// ============ Markdown 渲染 ============
|
||||
export { renderMarkdown } from './markdown.js'
|
||||
|
|
|
|||
|
|
@ -1,171 +0,0 @@
|
|||
/**
|
||||
* API 请求组合式函数
|
||||
* 提供统一的错误处理和加载状态管理
|
||||
*/
|
||||
import { ref } from 'vue'
|
||||
|
||||
export function useApi() {
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
/**
|
||||
* 执行 API 请求
|
||||
* @param {Function} apiFn - API 函数
|
||||
* @param {Object} options - 配置选项
|
||||
* @returns {Promise<Object>} 响应数据
|
||||
*/
|
||||
const request = async (apiFn, options = {}) => {
|
||||
const { showLoading = true, errorMessage = '请求失败' } = options
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await apiFn()
|
||||
|
||||
if (response.success) {
|
||||
return response.data
|
||||
} else {
|
||||
throw new Error(response.message || errorMessage)
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err.message || errorMessage
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置状态
|
||||
*/
|
||||
const reset = () => {
|
||||
loading.value = false
|
||||
error.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
request,
|
||||
reset
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页组合式函数
|
||||
*/
|
||||
export function usePagination(initialPage = 1, initialPageSize = 20) {
|
||||
const page = ref(initialPage)
|
||||
const pageSize = ref(initialPageSize)
|
||||
const total = ref(0)
|
||||
|
||||
const totalPages = () => Math.ceil(total.value / pageSize.value)
|
||||
|
||||
const nextPage = () => {
|
||||
if (page.value < totalPages()) {
|
||||
page.value++
|
||||
}
|
||||
}
|
||||
|
||||
const prevPage = () => {
|
||||
if (page.value > 1) {
|
||||
page.value--
|
||||
}
|
||||
}
|
||||
|
||||
const goToPage = (p) => {
|
||||
if (p >= 1 && p <= totalPages()) {
|
||||
page.value = p
|
||||
}
|
||||
}
|
||||
|
||||
const resetPagination = () => {
|
||||
page.value = initialPage
|
||||
total.value = 0
|
||||
}
|
||||
|
||||
return {
|
||||
page,
|
||||
pageSize,
|
||||
total,
|
||||
totalPages,
|
||||
nextPage,
|
||||
prevPage,
|
||||
goToPage,
|
||||
resetPagination
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单组合式函数
|
||||
*/
|
||||
export function useForm(initialData = {}) {
|
||||
const formData = ref({ ...initialData })
|
||||
const errors = ref({})
|
||||
const submitting = ref(false)
|
||||
|
||||
const updateField = (field, value) => {
|
||||
formData.value[field] = value
|
||||
// 清除字段错误
|
||||
if (errors.value[field]) {
|
||||
delete errors.value[field]
|
||||
}
|
||||
}
|
||||
|
||||
const setFieldError = (field, message) => {
|
||||
errors.value[field] = message
|
||||
}
|
||||
|
||||
const setErrors = (errorObj) => {
|
||||
errors.value = errorObj
|
||||
}
|
||||
|
||||
const clearErrors = () => {
|
||||
errors.value = {}
|
||||
}
|
||||
|
||||
const resetForm = (newData = {}) => {
|
||||
formData.value = { ...initialData, ...newData }
|
||||
errors.value = {}
|
||||
}
|
||||
|
||||
const validate = (rules) => {
|
||||
const newErrors = {}
|
||||
|
||||
for (const [field, rule] of Object.entries(rules)) {
|
||||
const value = formData.value[field]
|
||||
|
||||
if (rule.required && !value) {
|
||||
newErrors[field] = rule.message || `${field}不能为空`
|
||||
}
|
||||
|
||||
if (rule.minLength && value && value.length < rule.minLength) {
|
||||
newErrors[field] = rule.message || `${field}长度不能少于${rule.minLength}位`
|
||||
}
|
||||
|
||||
if (rule.maxLength && value && value.length > rule.maxLength) {
|
||||
newErrors[field] = rule.message || `${field}长度不能超过${rule.maxLength}位`
|
||||
}
|
||||
|
||||
if (rule.pattern && value && !rule.pattern.test(value)) {
|
||||
newErrors[field] = rule.message || `${field}格式不正确`
|
||||
}
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
|
||||
return {
|
||||
formData,
|
||||
errors,
|
||||
submitting,
|
||||
updateField,
|
||||
setFieldError,
|
||||
setErrors,
|
||||
clearErrors,
|
||||
resetForm,
|
||||
validate
|
||||
}
|
||||
}
|
||||
|
|
@ -40,27 +40,6 @@ export const formatNumber = (num, options = {}) => {
|
|||
}).format(num) + suffix
|
||||
}
|
||||
|
||||
/**
|
||||
* 截断文本
|
||||
*/
|
||||
export const truncate = (text, maxLength = 50, suffix = '...') =>
|
||||
!text || text.length <= maxLength ? text : text.slice(0, maxLength - suffix.length) + suffix
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
*/
|
||||
export const formatFileSize = (bytes) => {
|
||||
if (!bytes) return '0 B'
|
||||
const k = 1024, sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 首字母大写
|
||||
*/
|
||||
export const capitalize = (str) => str ? str.replace(/^./, c => c.toUpperCase()) : ''
|
||||
|
||||
/**
|
||||
* 格式化令牌数
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
/**
|
||||
* 防抖函数
|
||||
* @param {Function} func - 要执行的函数
|
||||
* @param {number} wait - 等待时间(毫秒)
|
||||
*/
|
||||
export const debounce = (fn, wait = 300) => {
|
||||
let t
|
||||
return (...args) => (clearTimeout(t), t = setTimeout(() => fn(...args), wait))
|
||||
}
|
||||
|
||||
/**
|
||||
* 节流函数
|
||||
* @param {Function} func - 要执行的函数
|
||||
* @param {number} limit - 时间限制(毫秒)
|
||||
*/
|
||||
export const throttle = (fn, limit = 300) => {
|
||||
let t
|
||||
return (...args) => !t && (fn(...args), t = setTimeout(() => t = null, limit))
|
||||
}
|
||||
|
||||
/**
|
||||
* 深拷贝(使用原生 API)
|
||||
* @param {any} obj - 要拷贝的对象
|
||||
*/
|
||||
export const deepClone = (obj) => structuredClone(obj)
|
||||
|
||||
/**
|
||||
* 生成随机 Id
|
||||
* @param {number} length - 长度
|
||||
*/
|
||||
export const generateId = (length = 8) =>
|
||||
Math.random().toString(36).slice(2, 2 + length)
|
||||
|
||||
/**
|
||||
* 本地存储工具
|
||||
*/
|
||||
export const storage = {
|
||||
get: (key, defaultValue = null) => {
|
||||
try {
|
||||
const item = localStorage.getItem(key)
|
||||
return item ? JSON.parse(item) : defaultValue
|
||||
} catch { return defaultValue }
|
||||
},
|
||||
set: (key, value) => { try { localStorage.setItem(key, JSON.stringify(value)); return true } catch { return false } },
|
||||
remove: (key) => { try { localStorage.removeItem(key); return true } catch { return false } },
|
||||
clear: () => { try { localStorage.clear(); return true } catch { return false } }
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测设备类型
|
||||
*/
|
||||
export const getDeviceType = () => {
|
||||
const ua = navigator.userAgent
|
||||
if (/(tablet|ipad|playbook|silk)|(android(?!.*mobi))/i.test(ua)) return 'tablet'
|
||||
if (/Mobile|Android|iP(hone|od)|IEMobile|BlackBerry|Kindle|Silk-Accelerated|(hpw|web)OS|Opera M(obi|ini)/.test(ua)) return 'mobile'
|
||||
return 'desktop'
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制到剪贴板
|
||||
* @param {string} text - 要复制的文本
|
||||
*/
|
||||
export const copyToClipboard = (text) => navigator.clipboard.writeText(text).then(() => true).catch(() => false)
|
||||
|
|
@ -202,8 +202,6 @@ const form = ref({ title: '', provider_id: null, model: '' })
|
|||
const newMessage = ref('')
|
||||
const messagesContainer = ref(null)
|
||||
const activeMessageId = ref(null)
|
||||
let scrollObserver = null
|
||||
const observedElements = new Set()
|
||||
|
||||
const editConv = ref(null)
|
||||
|
||||
|
|
@ -314,7 +312,6 @@ onMounted(() => {
|
|||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
scrollObserver?.disconnect()
|
||||
cleanup()
|
||||
})
|
||||
</script>
|
||||
|
|
@ -385,9 +382,6 @@ onUnmounted(() => {
|
|||
|
||||
/* 聊天视图容器 */
|
||||
.chat-view-container { flex: 1; display: flex; flex-direction: column; height: 100%; overflow: hidden; }
|
||||
.chat-header { display: flex; justify-content: flex-end; padding: 8px; border-bottom: 1px solid var(--border-light); }
|
||||
.btn-nav-toggle { background: none; border: none; font-size: 18px; cursor: pointer; padding: 4px 8px; border-radius: 4px; transition: background 0.15s; }
|
||||
.btn-nav-toggle:hover { background: var(--bg-hover); }
|
||||
.chat-messages { flex: 1; overflow-y: auto; padding: 0.75rem; }
|
||||
.chat-message { display: flex; gap: 0.75rem; margin-bottom: 0.75rem; }
|
||||
.chat-message.user { flex-direction: row-reverse; }
|
||||
|
|
@ -396,21 +390,10 @@ onUnmounted(() => {
|
|||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }
|
||||
.message-content { max-width: 80%; width: 80%; }
|
||||
.chat-message.user .message-content { max-width: 80%; width: auto; }
|
||||
.message-text { padding: 0.65rem 0.9rem; border-radius: 12px; font-size: 0.9rem; line-height: 1.5; background: var(--bg-secondary); color: var(--text-primary); word-break: break-word; }
|
||||
.chat-message.user .message-text { background: var(--accent-primary); color: white; }
|
||||
.chat-empty { flex: 1; display: flex; align-items: center; justify-content: center; color: var(--text-tertiary); font-size: 0.85rem; }
|
||||
.loading-messages { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 2rem; color: var(--text-secondary); font-size: 0.85rem; }
|
||||
.loading-messages .spinner-small { margin-bottom: 0.5rem; }
|
||||
|
||||
/* Markdown 内容样式 */
|
||||
.message-text { line-height: 1.6; }
|
||||
.message-text :deep(pre) { background: var(--bg-code); border-radius: 8px; padding: 0.75rem; overflow-x: auto; margin: 0.5rem 0; }
|
||||
.message-text :deep(code) { background: var(--bg-code); padding: 0.15rem 0.35rem; border-radius: 4px; font-size: 0.85em; }
|
||||
.message-text :deep(pre code) { background: none; padding: 0; }
|
||||
.message-text :deep(p) { margin: 0.5rem 0; }
|
||||
.message-text :deep(p:first-child) { margin-top: 0; }
|
||||
.message-text :deep(p:last-child) { margin-bottom: 0; }
|
||||
|
||||
/* 聊天输入区 */
|
||||
.chat-input-area { padding: 1rem; border-top: 1px solid var(--border-light); display: flex; gap: 0.75rem; }
|
||||
.chat-input { flex: 1; padding: 0.65rem 0.9rem; border: 1px solid var(--border-input); border-radius: 8px; background: var(--bg-input); color: var(--text-primary); font-size: 0.9rem; }
|
||||
|
|
|
|||
|
|
@ -9,17 +9,14 @@
|
|||
|
||||
<div class="stats-section">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon"></div>
|
||||
<div class="stat-number">{{ stats.conversations }}</div>
|
||||
<div class="stat-label">会话</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon"></div>
|
||||
<div class="stat-number">{{ stats.tools }}</div>
|
||||
<div class="stat-label">工具</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon"></div>
|
||||
<div class="stat-number">{{ formatTokens(stats.totalTokens) }}</div>
|
||||
<div class="stat-label">Tokens</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -613,8 +613,6 @@ onMounted(() => {
|
|||
.section-title { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem; padding: 0.5rem 0; }
|
||||
.section-icon { font-size: 1rem; }
|
||||
.section-text { font-size: 1rem; font-weight: 700; color: var(--text-primary); }
|
||||
.btn-add { margin-left: auto; padding: 0.4rem 0.8rem; background: var(--accent-primary); color: white; border: none; border-radius: 6px; font-size: 0.8rem; cursor: pointer; transition: all 0.2s; }
|
||||
.btn-add:hover { background: var(--accent-primary-hover); }
|
||||
|
||||
/* 设置卡片 */
|
||||
.settings-card { background: var(--bg-primary); border: 1px solid var(--border-light); border-radius: 12px; overflow: hidden; }
|
||||
|
|
@ -648,7 +646,6 @@ textarea { width: 100%; padding: 0.75rem; border: 1px solid var(--border-input);
|
|||
.name-col { width: 15%; min-width: 120px; }
|
||||
.info-col { width: 60%; min-width: 200px; }
|
||||
.switch-col { text-align: center; width: 80px; }
|
||||
.action-col { text-align: center; width: 80px; }
|
||||
.ops-col { width: 15%; min-width: 180px; text-align: center; }
|
||||
|
||||
/* Provider 单元格 */
|
||||
|
|
@ -703,12 +700,6 @@ textarea { width: 100%; padding: 0.75rem; border: 1px solid var(--border-input);
|
|||
.form-group label { display: block; margin-bottom: 0.5rem; font-weight: 500; color: var(--text-primary); font-size: 0.9rem; }
|
||||
.form-group input { width: 100%; padding: 0.65rem; border: 1px solid var(--border-input); border-radius: 8px; background: var(--bg-input); box-sizing: border-box; color: var(--text-primary); font-size: 0.9rem; }
|
||||
.form-group .hint { font-size: 0.75rem; color: var(--text-tertiary); margin-top: 4px; display: block; }
|
||||
.switch-card { display: flex; align-items: center; justify-content: space-between; padding: 1rem 1.25rem; background: var(--bg-secondary); border: 1px solid var(--border-light); border-radius: 10px; cursor: pointer; transition: all 0.2s; }
|
||||
.switch-card:hover { border-color: var(--accent-primary); }
|
||||
.switch-card.active { border-color: var(--accent-primary); background: var(--accent-primary-light); }
|
||||
.switch-content { display: flex; flex-direction: column; gap: 0.25rem; }
|
||||
.switch-title { font-weight: 600; color: var(--text-primary); }
|
||||
.switch-desc { font-size: 0.8rem; color: var(--text-secondary); }
|
||||
.optional { color: var(--text-tertiary); font-weight: normal; font-size: 0.8rem; }
|
||||
.error { color: var(--danger-color); background: var(--danger-bg); padding: 0.75rem; border-radius: 8px; margin-top: 0.75rem; font-size: 0.85rem; }
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue