增加i18n国际化多语言支持
This commit is contained in:
parent
69eb39ff44
commit
2edff4a061
@ -11,11 +11,19 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { zhCN, dateZhCN, NMessageProvider, NDialogProvider, NNotificationProvider } from 'naive-ui'
|
||||
import type { GlobalTheme } from 'naive-ui'
|
||||
import { provideLocale, initLocale } from '@/shared/i18n'
|
||||
|
||||
const theme = ref<GlobalTheme | null>(null)
|
||||
|
||||
// 提供语言状态
|
||||
provideLocale()
|
||||
|
||||
onMounted(() => {
|
||||
initLocale()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
160
frontend/src/shared/i18n/index.ts
Normal file
160
frontend/src/shared/i18n/index.ts
Normal file
@ -0,0 +1,160 @@
|
||||
import { ref, provide, inject, InjectionKey } from 'vue'
|
||||
|
||||
export interface LocaleConfig {
|
||||
code: string
|
||||
name: string
|
||||
flag: string
|
||||
}
|
||||
|
||||
// 支持的语言列表
|
||||
export const supportedLocales: LocaleConfig[] = [
|
||||
{ code: 'zh-CN', name: '简体中文', flag: '🇨🇳' },
|
||||
{ code: 'en-US', name: 'English', flag: '🇺🇸' }
|
||||
]
|
||||
|
||||
// 翻译内容
|
||||
export const translations: Record<string, Record<string, string>> = {
|
||||
'zh-CN': {
|
||||
// 节点状态
|
||||
'status.active': '活跃',
|
||||
'status.inactive': '停用',
|
||||
'status.draft': '草稿',
|
||||
'status.pending': '待处理',
|
||||
'status.completed': '已完成',
|
||||
'status.failed': '失败',
|
||||
'status.running': '运行中',
|
||||
'status.stopped': '已停止',
|
||||
|
||||
// 节点分类
|
||||
'category.input': '输入',
|
||||
'category.output': '输出',
|
||||
'category.process': '处理',
|
||||
'category.condition': '条件',
|
||||
'category.loop': '循环',
|
||||
'category.api': 'API',
|
||||
'category.database': '数据库',
|
||||
'category.ai': 'AI',
|
||||
'category.utility': '工具',
|
||||
|
||||
// 节点管理页面
|
||||
'nodeManagement.title': '节点管理',
|
||||
'nodeManagement.description': '管理和配置您的AI节点组件',
|
||||
'nodeManagement.cardView': '卡片视图',
|
||||
'nodeManagement.listView': '列表视图',
|
||||
'nodeManagement.loading': '加载中...',
|
||||
'nodeManagement.noData': '暂无数据',
|
||||
'nodeManagement.viewDetails': '查看详情',
|
||||
|
||||
// 表格头部
|
||||
'table.icon': '图标',
|
||||
'table.name': '名称',
|
||||
'table.type': '类型',
|
||||
'table.category': '分类',
|
||||
'table.status': '状态',
|
||||
'table.actions': '操作'
|
||||
},
|
||||
'en-US': {
|
||||
// Node Status
|
||||
'status.active': 'Active',
|
||||
'status.inactive': 'Inactive',
|
||||
'status.draft': 'Draft',
|
||||
'status.pending': 'Pending',
|
||||
'status.completed': 'Completed',
|
||||
'status.failed': 'Failed',
|
||||
'status.running': 'Running',
|
||||
'status.stopped': 'Stopped',
|
||||
|
||||
// Node Categories
|
||||
'category.input': 'Input',
|
||||
'category.output': 'Output',
|
||||
'category.process': 'Process',
|
||||
'category.condition': 'Condition',
|
||||
'category.loop': 'Loop',
|
||||
'category.api': 'API',
|
||||
'category.database': 'Database',
|
||||
'category.ai': 'AI',
|
||||
'category.utility': 'Utility',
|
||||
|
||||
// Node Management Page
|
||||
'nodeManagement.title': 'Node Management',
|
||||
'nodeManagement.description': 'Manage and configure your AI node components',
|
||||
'nodeManagement.cardView': 'Card View',
|
||||
'nodeManagement.listView': 'List View',
|
||||
'nodeManagement.loading': 'Loading...',
|
||||
'nodeManagement.noData': 'No data available',
|
||||
'nodeManagement.viewDetails': 'View Details',
|
||||
|
||||
// Table headers
|
||||
'table.icon': 'Icon',
|
||||
'table.name': 'Name',
|
||||
'table.type': 'Type',
|
||||
'table.category': 'Category',
|
||||
'table.status': 'Status',
|
||||
'table.actions': 'Actions'
|
||||
}
|
||||
}
|
||||
|
||||
// 语言状态管理
|
||||
const localeKey: InjectionKey<typeof currentLocale> = Symbol('locale')
|
||||
|
||||
// 全局语言状态
|
||||
const currentLocale = ref<string>('zh-CN')
|
||||
|
||||
// 提供语言状态
|
||||
export function provideLocale() {
|
||||
provide(localeKey, currentLocale)
|
||||
return currentLocale
|
||||
}
|
||||
|
||||
// 注入语言状态
|
||||
export function useLocale() {
|
||||
const locale = inject(localeKey, currentLocale)
|
||||
return locale
|
||||
}
|
||||
|
||||
// 获取翻译文本
|
||||
export function t(key: string, locale?: string): string {
|
||||
const targetLocale = locale || currentLocale.value
|
||||
const translation = translations[targetLocale]?.[key]
|
||||
return translation || key
|
||||
}
|
||||
|
||||
// 设置当前语言
|
||||
export function setLocale(locale: string) {
|
||||
if (supportedLocales.some(l => l.code === locale)) {
|
||||
currentLocale.value = locale
|
||||
localStorage.setItem('locale', locale)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前语言
|
||||
export function getCurrentLocale(): string {
|
||||
return currentLocale.value
|
||||
}
|
||||
|
||||
// 初始化语言
|
||||
export function initLocale() {
|
||||
const savedLocale = localStorage.getItem('locale')
|
||||
if (savedLocale && supportedLocales.some(l => l.code === savedLocale)) {
|
||||
currentLocale.value = savedLocale
|
||||
} else {
|
||||
const browserLanguage = navigator.language
|
||||
const matchedLocale = supportedLocales.find(l => browserLanguage.startsWith(l.code))
|
||||
if (matchedLocale) {
|
||||
currentLocale.value = matchedLocale.code
|
||||
} else {
|
||||
currentLocale.value = 'zh-CN'
|
||||
}
|
||||
localStorage.setItem('locale', currentLocale.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态翻译
|
||||
export function getStatusTranslation(status: string, locale?: string): string {
|
||||
return t(`status.${status.toLowerCase()}`, locale)
|
||||
}
|
||||
|
||||
// 获取分类翻译
|
||||
export function getCategoryTranslation(category: string, locale?: string): string {
|
||||
return t(`category.${category.toLowerCase()}`, locale)
|
||||
}
|
||||
@ -51,6 +51,14 @@
|
||||
<n-form-item label="超时时间">
|
||||
<n-input-number v-model:value="systemSettings.timeout" :min="5" :max="300" />
|
||||
</n-form-item>
|
||||
<n-form-item label="界面语言">
|
||||
<n-select
|
||||
v-model:value="systemSettings.language"
|
||||
:options="languageOptions"
|
||||
style="width: 200px"
|
||||
@update:value="changeLanguage"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-button type="primary" @click="saveSystemSettings">
|
||||
保存设置
|
||||
@ -65,8 +73,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { NGrid, NGridItem, NCard, NForm, NFormItem, NInput, NButton, NUpload, NInputNumber, useMessage } from 'naive-ui'
|
||||
import { NGrid, NGridItem, NCard, NForm, NFormItem, NInput, NButton, NUpload, NInputNumber, NSelect, useMessage } from 'naive-ui'
|
||||
import { useAuthStore } from '@/shared/stores/auth'
|
||||
import { getCurrentLocale, setLocale, supportedLocales, initLocale } from '@/shared/i18n'
|
||||
|
||||
const message = useMessage()
|
||||
const authStore = useAuthStore()
|
||||
@ -81,15 +90,27 @@ const systemSettings = reactive({
|
||||
jingrowApiUrl: (import.meta as any).env.VITE_BACKEND_SERVER_URL || '',
|
||||
apiKey: (import.meta as any).env.VITE_BACKEND_API_KEY || '',
|
||||
apiSecret: (import.meta as any).env.VITE_BACKEND_API_SECRET || '',
|
||||
timeout: 30
|
||||
timeout: 30,
|
||||
language: getCurrentLocale()
|
||||
})
|
||||
|
||||
const fileList = ref([])
|
||||
|
||||
// 语言选项
|
||||
const languageOptions = supportedLocales.map(locale => ({
|
||||
label: `${locale.flag} ${locale.name}`,
|
||||
value: locale.code
|
||||
}))
|
||||
|
||||
const handleUploadChange = (options: any) => {
|
||||
fileList.value = options.fileList
|
||||
}
|
||||
|
||||
const changeLanguage = (locale: string) => {
|
||||
setLocale(locale)
|
||||
message.success('语言设置已更新')
|
||||
}
|
||||
|
||||
const saveUserSettings = () => {
|
||||
message.success('个人设置已保存')
|
||||
}
|
||||
@ -99,6 +120,8 @@ const saveSystemSettings = () => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initLocale()
|
||||
systemSettings.language = getCurrentLocale()
|
||||
if (authStore.user) {
|
||||
userSettings.username = authStore.user.username
|
||||
userSettings.email = authStore.user.email
|
||||
|
||||
@ -1,8 +1,29 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<h2>节点管理</h2>
|
||||
<div class="actions">
|
||||
<div class="header-left">
|
||||
<h2>{{ translate('nodeManagement.title') }}</h2>
|
||||
<p class="page-description">{{ translate('nodeManagement.description') }}</p>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="view-toggle">
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ active: viewMode === 'card' }"
|
||||
@click="viewMode = 'card'"
|
||||
:title="translate('nodeManagement.cardView')"
|
||||
>
|
||||
<i class="fa fa-th-large"></i>
|
||||
</button>
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ active: viewMode === 'list' }"
|
||||
@click="viewMode = 'list'"
|
||||
:title="translate('nodeManagement.listView')"
|
||||
>
|
||||
<i class="fa fa-list"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="refresh-btn" @click="reload" :disabled="loading">
|
||||
<i :class="loading ? 'fa fa-spinner fa-spin' : 'fa fa-refresh'"></i>
|
||||
</button>
|
||||
@ -11,10 +32,11 @@
|
||||
|
||||
<div class="page-content">
|
||||
<div v-if="loading" class="loading">
|
||||
<i class="fa fa-spinner fa-spin"></i> 加载中...
|
||||
<i class="fa fa-spinner fa-spin"></i> {{ translate('nodeManagement.loading') }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="node-grid">
|
||||
<!-- 卡片视图 -->
|
||||
<div v-if="viewMode === 'card'" class="node-grid">
|
||||
<div v-for="node in nodes" :key="node.name" class="node-card" @click="openDetail(node.name)">
|
||||
<div class="icon" :style="{ color: node.node_color || '#6b7280' }">
|
||||
<i :class="resolveIcon(node.node_icon)"></i>
|
||||
@ -24,6 +46,44 @@
|
||||
<div class="desc">{{ node.description || '—' }}</div>
|
||||
<div class="meta">
|
||||
<span v-if="node.category" class="badge">{{ node.category }}</span>
|
||||
<span v-if="node.status" class="status-badge" :class="node.status">{{ node.status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 列表视图 -->
|
||||
<div v-else class="node-list">
|
||||
<div class="list-header">
|
||||
<div class="col-icon">{{ translate('table.icon') }}</div>
|
||||
<div class="col-name">{{ translate('table.name') }}</div>
|
||||
<div class="col-type">{{ translate('table.type') }}</div>
|
||||
<div class="col-category">{{ translate('table.category') }}</div>
|
||||
<div class="col-status">{{ translate('table.status') }}</div>
|
||||
<div class="col-actions">{{ translate('table.actions') }}</div>
|
||||
</div>
|
||||
<div class="list-body">
|
||||
<div v-for="node in nodes" :key="node.name" class="node-list-item" @click="openDetail(node.name)">
|
||||
<div class="col-icon">
|
||||
<div class="icon" :style="{ color: node.node_color || '#6b7280' }">
|
||||
<i :class="resolveIcon(node.node_icon)"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-name">
|
||||
<div class="name">{{ node.title || node.type }}</div>
|
||||
<div class="description">{{ node.description || '—' }}</div>
|
||||
</div>
|
||||
<div class="col-type">{{ node.type }}</div>
|
||||
<div class="col-category">
|
||||
<span v-if="node.category" class="badge">{{ node.category }}</span>
|
||||
</div>
|
||||
<div class="col-status">
|
||||
<span v-if="node.status" class="status-badge" :class="node.status">{{ node.status }}</span>
|
||||
</div>
|
||||
<div class="col-actions" @click.stop>
|
||||
<button class="action-btn" @click="openDetail(node.name)" :title="translate('nodeManagement.viewDetails')">
|
||||
<i class="fa fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -34,12 +94,20 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { t, useLocale } from '@/shared/i18n'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(true)
|
||||
const nodes = ref<any[]>([])
|
||||
const viewMode = ref<'card' | 'list'>('card')
|
||||
|
||||
// 使用语言状态
|
||||
const locale = useLocale()
|
||||
|
||||
// 响应式翻译函数
|
||||
const translate = computed(() => (key: string) => t(key, locale.value))
|
||||
|
||||
async function fetchNodes() {
|
||||
loading.value = true
|
||||
@ -80,12 +148,81 @@ function openDetail(name: string) {
|
||||
router.push({ name: 'NodeDetail', params: { name } })
|
||||
}
|
||||
|
||||
onMounted(fetchNodes)
|
||||
onMounted(() => {
|
||||
fetchNodes()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { padding: 16px; }
|
||||
.page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
|
||||
.page {
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.header-left h2 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.page-description {
|
||||
font-size: 16px;
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
background: #f8fafc;
|
||||
border-radius: 8px;
|
||||
padding: 2px;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.toggle-btn:hover {
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.toggle-btn.active {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.toggle-btn.active:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
/* 现代化刷新按钮 */
|
||||
.refresh-btn {
|
||||
@ -157,14 +294,225 @@ onMounted(fetchNodes)
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.node-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 12px; }
|
||||
.node-card { display: flex; gap: 12px; padding: 12px; border: 1px solid #e5e7eb; border-radius: 8px; cursor: pointer; background: #fff; }
|
||||
.node-card:hover { border-color: #cbd5e1; background: #fafafa; }
|
||||
.icon { width: 42px; height: 42px; border-radius: 8px; display:flex; align-items:center; justify-content:center; background: #f3f4f6; font-size: 20px; }
|
||||
.info { flex: 1; }
|
||||
.title { font-weight: 600; color: #111827; margin-bottom: 2px; }
|
||||
.desc { color: #6b7280; font-size: 12px; line-height: 1.4; height: 32px; overflow: hidden; }
|
||||
.meta { margin-top: 6px; display:flex; gap: 6px; }
|
||||
.badge { background: #eef2ff; color:#334155; padding: 2px 6px; border-radius: 6px; font-size: 11px; }
|
||||
/* 卡片视图样式 */
|
||||
.node-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.node-card {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.node-card:hover {
|
||||
border-color: #3b82f6;
|
||||
background: #f8fafc;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f3f4f6;
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin-bottom: 4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: #6b7280;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
height: 36px;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.meta {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.badge {
|
||||
background: #eef2ff;
|
||||
color: #334155;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-badge.active {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.status-badge.inactive {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.status-badge.draft {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
/* 列表视图样式 */
|
||||
.node-list {
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: grid;
|
||||
grid-template-columns: 60px 2fr 1fr 1fr 1fr 80px;
|
||||
gap: 16px;
|
||||
padding: 16px 20px;
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.list-body {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.node-list-item {
|
||||
display: grid;
|
||||
grid-template-columns: 60px 2fr 1fr 1fr 1fr 80px;
|
||||
gap: 16px;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.node-list-item:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.node-list-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.col-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.col-icon .icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.col-name {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.col-name .name {
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
font-size: 14px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.col-name .description {
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.col-type {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #6b7280;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.col-category {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.col-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.col-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
background: #f3f4f6;
|
||||
color: #6b7280;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user