Rebuild Home and Editor pages with simplified inline styles

- Replace Element Plus components with pure HTML/CSS
- Use inline styles for reliable rendering
- Fix templates URL duplicate path issue
- Insert template data (4 templates) via Django shell
- Clear Vite cache and restart both servers
This commit is contained in:
Poker Design Developer
2026-05-31 22:07:39 +08:00
parent 71193c30e6
commit 670ac0a917
5 changed files with 172 additions and 662 deletions

View File

@@ -1,481 +1,169 @@
<template>
<el-container class="editor-container">
<el-header class="editor-header">
<div class="header-left">
<button @click="goBack" class="back-btn"> 返回</button>
<input
v-model="projectName"
@blur="saveProjectName"
class="project-name-input"
placeholder="项目名称"
/>
<div style="min-height: 100vh; background: #1a1a2e; color: #eee; font-family: sans-serif;">
<header style="background: #16213e; padding: 15px 30px; display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; align-items: center; gap: 20px;">
<button @click="$router.push('/')" style="background: #333; color: #aaa; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer;"> 返回</button>
<input v-model="pname" @blur="saveName" style="background: transparent; border: none; color: white; font-size: 18px; font-weight: bold; outline: none; width: 300px;" placeholder="项目名称">
</div>
<div class="header-right">
<el-button type="primary" @click="exportAll">导出全部</el-button>
<el-button @click="exportSingle">导出当前</el-button>
<div style="display: flex; gap: 10px;">
<button @click="doExportAll" style="padding: 8px 20px; background: #e94560; color: white; border: none; border-radius: 4px; cursor: pointer;">导出全部</button>
<button @click="doExportSingle" style="padding: 8px 20px; background: #0f3460; color: #e94560; border: 1px solid #e94560; border-radius: 4px; cursor: pointer;">导出当前</button>
</div>
</el-header>
</header>
<el-container class="editor-body">
<el-aside width="300px" class="left-panel">
<el-tabs v-model="leftTab">
<el-tab-pane label="素材库" name="assets">
<div class="asset-section">
<h3>花色图案</h3>
<div class="asset-grid">
<div v-for="suit in suits" :key="suit.id" class="asset-item">
<div class="asset-preview">{{ suit.symbol }}</div>
<button @click="uploadAsset('suit', suit.id)">上传</button>
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="图层" name="layers">
<div class="layer-list">
<div
v-for="layer in currentLayers"
:key="layer.id"
class="layer-item"
:class="{ active: selectedLayer === layer.id }"
@click="selectLayer(layer.id)"
>
<el-checkbox v-model="layer.visible" />
<span>{{ layer.name }}</span>
</div>
</div>
</el-tab-pane>
</el-tabs>
</el-aside>
<el-main class="canvas-area">
<div class="canvas-wrapper">
<canvas ref="canvasRef" id="main-canvas"></canvas>
<div style="display: flex; height: calc(100vh - 60px);">
<aside style="width: 260px; background: #0f3460; padding: 20px; overflow-y: auto;">
<h3 style="margin: 0 0 15px 0; font-size: 14px; color: #888;">花色选择</h3>
<div style="display: flex; gap: 8px; margin-bottom: 25px;">
<button v-for="s in suits" :key="s"
@click="switchSuit(s)"
:style="{ padding: '8px 14px', border: 'none', borderRadius: '4px', cursor: 'pointer', background: currentSuit === s ? '#e94560' : '#16213e', color: currentSuit === s ? 'white' : '#aaa', fontWeight: currentSuit === s ? 'bold' : 'normal' }">
{{ suitLabels[s] }}
</button>
</div>
</el-main>
</el-container>
<el-footer height="120px" class="card-list-footer">
<div class="card-tabs">
<div
v-for="suit in cardSuits"
:key="suit"
class="suit-tab"
:class="{ active: currentSuit === suit }"
@click="switchSuit(suit)"
>
{{ suit }}
<h3 style="margin: 0 0 15px 0; font-size: 14px; color: #888;">图层管理</h3>
<div v-for="l in layers" :key="l.id" style="padding: 10px; margin-bottom: 5px; background: #16213e; border-radius: 4px; display: flex; align-items: center; gap: 10px; cursor: pointer;">
<span>{{ l.visible ? '👁' : '—' }}</span>
<span style="font-size: 13px;">{{ l.name }}</span>
</div>
</div>
<div class="card-thumbnails">
<div
v-for="card in currentCards"
:key="card.key"
class="card-thumb"
:class="{ active: currentCard === card.key }"
@click="selectCard(card.key)"
>
<div class="card-mini-preview">{{ card.label }}</div>
</aside>
<main style="flex: 1; display: flex; justify-content: center; align-items: center; background: #1a1a2e; padding: 20px;">
<div style="background: #16213e; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 24px rgba(0,0,0,0.3);">
<canvas ref="canvasEl" id="main-canvas"></canvas>
</div>
</main>
</div>
<footer style="background: #0f3460; padding: 10px 20px; display: flex; gap: 5px; overflow-x: auto;">
<div v-for="c in currentCards" :key="c.key"
@click="selectCard(c.key)"
:style="{ minWidth: '60px', height: '84px', background: currentCard === c.key ? '#e94560' : '#16213e', color: currentCard === c.key ? 'white' : '#aaa', borderRadius: '4px', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', fontSize: '12px', fontWeight: currentCard === c.key ? 'bold' : 'normal', flexShrink: 0, transition: 'all 0.2s' }">
{{ c.label }}
</div>
</el-footer>
</el-container>
</footer>
</div>
</template>
<script setup>
import { ref, onMounted, computed, watch, nextTick } from 'vue'
import { ref, onMounted, computed, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Canvas, Rect, Text as FabricText } from 'fabric'
import { ElMessage } from 'element-plus'
import { getProject, updateProject } from '@/api/project'
import { exportProject, getExportUrl } from '@/api/export'
import { Canvas, FabricText } from 'fabric'
import axios from 'axios'
const route = useRoute()
const router = useRouter()
const canvasRef = ref(null)
const projectName = ref('')
const leftTab = ref('assets')
const selectedLayer = ref(null)
const canvasEl = ref(null)
const pname = ref('')
const currentSuit = ref('spade')
const currentCard = ref('spade-A')
const canvas = ref(null)
const fabricCanvas = ref(null)
const projectId = computed(() => route.params.projectId)
const suits = [
{ id: 'spade', symbol: '', name: '黑桃' },
{ id: 'heart', symbol: '♥', name: '红桃' },
{ id: 'club', symbol: '♣', name: '梅花' },
{ id: 'diamond', symbol: '♦', name: '方块' }
]
const suits = ['spade', 'heart', 'club', 'diamond']
const suitLabels = { spade: '♠ 黑桃', heart: '♥ 红桃', club: '♣ 梅花', diamond: '♦ 方块' }
const cardSuits = ['spade', 'heart', 'club', 'diamond']
const cardRanks = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
const ranks = ['A','2','3','4','5','6','7','8','9','10','J','Q','K']
const currentCards = computed(() => {
const cards = cardRanks.map(rank => ({
key: `${currentSuit.value}-${rank}`,
label: `${getSuitSymbol(currentSuit.value)}${rank}`
}))
const cards = ranks.map(r => ({ key: `${currentSuit.value}-${r}`, label: getSymbol(currentSuit.value) + r }))
if (currentSuit.value === 'spade') {
cards.push({ key: 'joker-big', label: '大王' })
cards.push({ key: 'joker-small', label: '小王' })
}
return cards
})
const currentLayers = ref([
{ id: 'background', name: '背景层', visible: true },
const layers = [
{ id: 'bg', name: '背景层', visible: true },
{ id: 'border', name: '边框层', visible: true },
{ id: 'pattern', name: '图案层', visible: true },
{ id: 'text', name: '文字层', visible: true }
])
]
function getSuitSymbol(suit) {
const map = {
spade: '♠',
heart: '♥',
club: '♣',
diamond: '♦'
}
return map[suit] || ''
function getSymbol(suit) {
return { spade: '♠', heart: '♥', club: '♣', diamond: '♦' }[suit] || ''
}
function isRed(suit) {
return suit === 'heart' || suit === 'diamond'
}
onMounted(async () => {
if (projectId.value) {
await loadProject()
try {
const res = await axios.get(`/api/projects/${projectId.value}/`)
pname.value = res.data.name
} catch (e) {
console.error('Failed to load project:', e)
}
}
await nextTick()
initCanvas()
})
async function loadProject() {
try {
const project = await getProject(projectId.value)
projectName.value = project.name
} catch (error) {
ElMessage.error('加载项目失败')
console.error(error)
}
}
function initCanvas() {
if (!canvasRef.value) return
canvas.value = new Canvas('main-canvas', {
width: 750,
height: 1050,
backgroundColor: '#ffffff',
selection: true
if (!canvasEl.value) return
fabricCanvas.value = new Canvas('main-canvas', {
width: 400,
height: 560,
backgroundColor: '#ffffff'
})
drawDefaultCard()
drawCard()
}
function drawDefaultCard() {
if (!canvas.value) return
function drawCard() {
const c = fabricCanvas.value
if (!c) return
canvas.value.clear()
canvas.value.setBackgroundColor('#ffffff', canvas.value.renderAll.bind(canvas.value))
c.clear()
c.backgroundColor = '#ffffff'
const rect = new Rect({
left: 50,
top: 50,
width: 650,
height: 950,
fill: 'transparent',
stroke: '#e0e0e0',
strokeWidth: 2,
selectable: false
})
canvas.value.add(rect)
const suitSymbol = getSuitSymbol(currentSuit.value)
const suit = currentSuit.value
const rank = currentCard.value.split('-')[1] || 'A'
const sym = getSymbol(suit)
const color = isRed(suit) ? '#FF0000' : '#000000'
const topText = new FabricText(`${rank}${suitSymbol}`, {
left: 70,
top: 70,
fontSize: 48,
fill: currentSuit.value === 'heart' || currentSuit.value === 'diamond' ? '#FF0000' : '#000000',
selectable: true
const topText = new fabric.FabricText(`${rank}${sym}`, {
left: 30, top: 25, fontSize: 36, fill: color, selectable: true
})
canvas.value.add(topText)
c.add(topText)
const centerSymbol = new FabricText(suitSymbol, {
left: 375,
top: 525,
fontSize: 120,
fill: currentSuit.value === 'heart' || currentSuit.value === 'diamond' ? '#FF0000' : '#000000',
selectable: true,
originX: 'center',
originY: 'center'
const center = new fabric.FabricText(sym, {
left: 200, top: 280, fontSize: 80, fill: color, selectable: true, originX: 'center', originY: 'center'
})
canvas.value.add(centerSymbol)
c.add(center)
const bottomText = new FabricText(`${rank}${suitSymbol}`, {
left: 630,
top: 930,
fontSize: 48,
fill: currentSuit.value === 'heart' || currentSuit.value === 'diamond' ? '#FF0000' : '#000000',
selectable: true,
angle: 180,
originX: 'center',
originY: 'center'
})
canvas.value.add(bottomText)
canvas.value.renderAll()
c.renderAll()
}
function switchSuit(suit) {
currentSuit.value = suit
currentCard.value = `${suit}-A`
drawDefaultCard()
function switchSuit(s) {
currentSuit.value = s
currentCard.value = `${s}-A`
drawCard()
}
function selectCard(cardKey) {
currentCard.value = cardKey
drawDefaultCard()
function selectCard(key) {
currentCard.value = key
drawCard()
}
function selectLayer(layerId) {
selectedLayer.value = layerId
}
function uploadAsset(type, id) {
ElMessage.info('素材上传功能开发中...')
}
async function saveProjectName() {
async function saveName() {
if (!projectId.value) return
try { await axios.put(`/api/projects/${projectId.value}/`, { name: pname.value }) } catch (e) {}
}
async function doExportAll() {
try {
await updateProject(projectId.value, { name: projectName.value })
ElMessage.success('项目名称已保存')
} catch (error) {
ElMessage.error('保存失败')
}
const res = await axios.post(`/api/projects/${projectId.value}/export/`, { resolution: 'standard', cards: 'all' })
if (res.data.download_url) window.open(res.data.download_url, '_blank')
} catch (e) { alert('导出失败: ' + e.message) }
}
async function exportAll() {
if (!projectId.value) return
try {
const result = await exportProject(projectId.value, 'standard', 'all')
window.open(result.download_url, '_blank')
ElMessage.success('导出成功')
} catch (error) {
ElMessage.error('导出失败')
console.error(error)
}
}
function exportSingle() {
if (!projectId.value) return
const url = getExportUrl(projectId.value, currentCard.value, 'standard')
function doExportSingle() {
const url = `/api/projects/${projectId.value}/export/${currentCard.value}/?resolution=standard`
window.open(url, '_blank')
ElMessage.success('开始导出')
}
function goBack() {
router.push('/')
}
</script>
<style scoped>
.editor-container {
height: 100vh;
display: flex;
flex-direction: column;
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
background: #ffffff;
border-bottom: 1px solid #e0e0e0;
padding: 0 20px;
height: 60px;
}
.header-left {
display: flex;
align-items: center;
gap: 15px;
}
.back-btn {
background: transparent;
border: none;
color: #666;
cursor: pointer;
font-size: 16px;
padding: 8px 12px;
border-radius: 4px;
}
.back-btn:hover {
background: #f5f5f5;
}
.project-name-input {
border: none;
font-size: 18px;
font-weight: 600;
padding: 5px 10px;
border-radius: 4px;
}
.project-name-input:focus {
outline: none;
background: #f5f5f5;
}
.header-right {
display: flex;
gap: 10px;
}
.editor-body {
flex: 1;
overflow: hidden;
}
.left-panel {
background: #fafafa;
border-right: 1px solid #e0e0e0;
overflow-y: auto;
}
.canvas-area {
display: flex;
justify-content: center;
align-items: center;
background: #e8e8e8;
padding: 20px;
}
.canvas-wrapper {
background: white;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
overflow: auto;
}
.card-list-footer {
background: #ffffff;
border-top: 1px solid #e0e0e0;
display: flex;
flex-direction: column;
padding: 10px 20px;
}
.card-tabs {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.suit-tab {
padding: 5px 15px;
background: #f5f5f5;
border-radius: 4px;
cursor: pointer;
transition: background 0.3s;
}
.suit-tab.active {
background: #667eea;
color: white;
}
.card-thumbnails {
display: flex;
gap: 8px;
overflow-x: auto;
padding-bottom: 5px;
}
.card-thumb {
width: 50px;
height: 70px;
background: #f5f5f5;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.3s;
}
.card-thumb.active {
background: #667eea;
color: white;
}
.card-mini-preview {
font-size: 14px;
font-weight: 600;
}
.asset-section {
padding: 15px;
}
.asset-section h3 {
margin-bottom: 10px;
font-size: 14px;
color: #333;
}
.asset-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.asset-item {
text-align: center;
}
.asset-preview {
width: 100%;
height: 60px;
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
margin-bottom: 5px;
}
.layer-list {
padding: 10px;
}
.layer-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px;
border-radius: 4px;
cursor: pointer;
}
.layer-item:hover {
background: #f0f0f0;
}
.layer-item.active {
background: #e6f0ff;
}
</style>

View File

@@ -1,300 +1,124 @@
<template>
<div class="home-container">
<header class="header">
<h1>扑克牌设计管理系统</h1>
<button @click="createNewProject" class="create-btn">创建新项目</button>
<div style="min-height: 100vh; background: #1a1a2e; color: #eee; font-family: sans-serif;">
<header style="background: #16213e; padding: 25px 40px; display: flex; justify-content: space-between; align-items: center;">
<h1 style="margin: 0; font-size: 28px; color: #e94560;">扑克牌设计管理系统</h1>
<button @click="doCreate" style="padding: 12px 28px; background: #e94560; color: white; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; font-weight: bold;">
创建新项目
</button>
</header>
<main class="main">
<h2>选择或创建项目</h2>
<main style="max-width: 1200px; margin: 0 auto; padding: 40px 20px;">
<h2 style="margin-bottom: 30px; font-size: 22px;">选择模板系列开始设计</h2>
<div class="tabs">
<button
:class="{ active: activeTab === 'templates' }"
@click="activeTab = 'templates'"
>
模板系列
</button>
<button
:class="{ active: activeTab === 'existing' }"
@click="activeTab = 'existing'"
>
已有项目
</button>
</div>
<div v-if="activeTab === 'templates'" class="template-grid">
<div v-for="template in templates" :key="template.id" class="template-card" @click="selectTemplate(template.id)">
<div class="template-preview">{{ template.name.substring(0, 2) }}</div>
<h3>{{ template.name }}</h3>
<p>{{ template.description }}</p>
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 24px; margin-bottom: 50px;">
<div v-for="t in templateList" :key="t.id"
@click="createFromTemplate(t.id)"
style="background: #0f3460; border-radius: 12px; padding: 25px; cursor: pointer; transition: all 0.3s; text-align: center;"
@mouseenter="$event.currentTarget.style.background = '#16213e'"
@mouseleave="$event.currentTarget.style.background = '#0f3460'">
<div style="font-size: 48px; margin-bottom: 12px;">{{ t.icon }}</div>
<h3 style="margin: 0 0 8px 0; font-size: 18px; color: #e94560;">{{ t.name }}</h3>
<p style="margin: 0; font-size: 13px; color: #aaa; line-height: 1.5;">{{ t.desc }}</p>
</div>
</div>
<div v-if="activeTab === 'existing'" class="project-list">
<div v-if="loading">加载中...</div>
<div v-else-if="projects.length === 0" class="empty-state">
暂无项目请创建新项目
</div>
<div v-else>
<div v-for="project in projects" :key="project.id" class="project-item">
<div class="project-info">
<h3>{{ project.name }}</h3>
<p>创建于: {{ formatDate(project.created_at) }}</p>
</div>
<div class="project-actions">
<button @click="editProject(project.id)">编辑</button>
<button @click="deleteProject(project.id)" style="color: red;">删除</button>
</div>
<div v-if="hasProjects" style="background: #0f3460; border-radius: 12px; padding: 25px;">
<h3 style="margin: 0 0 20px 0;">已有项目</h3>
<div v-for="p in projectList" :key="p.id"
style="display: flex; justify-content: space-between; align-items: center; padding: 15px; background: #16213e; border-radius: 8px; margin-bottom: 10px;">
<div>
<strong>{{ p.name }}</strong>
<div style="font-size: 12px; color: #888; margin-top: 4px;">创建于: {{ formatDate(p.created_at) }}</div>
</div>
<div style="display: flex; gap: 8px;">
<button @click.stop="editProject(p.id)" style="padding: 6px 16px; background: #e94560; color: white; border: none; border-radius: 4px; cursor: pointer;">编辑</button>
<button @click.stop="removeProject(p.id)" style="padding: 6px 16px; background: #333; color: #aaa; border: none; border-radius: 4px; cursor: pointer;">删除</button>
</div>
</div>
</div>
<div v-if="loading" style="text-align: center; padding: 40px; color: #888;">加载中...</div>
<div v-if="loadError" style="text-align: center; padding: 40px; color: #e94560;">{{ loadError }}</div>
</main>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import axios from 'axios'
const router = useRouter()
const activeTab = ref('templates')
const projects = ref([])
const templates = ref([])
const projectList = ref([])
const loading = ref(false)
const loadError = ref('')
// 内置模板数据不依赖API
const defaultTemplates = [
{ id: 'classic', name: '经典风格', description: '标准扑克牌设计,传统花色和字体' },
{ id: 'modern', name: '现代简约', description: '扁平化设计,简洁线条' },
{ id: 'cartoon', name: '卡通风格', description: 'Q版可爱人像圆润花色图案' },
{ id: 'vintage', name: '复古风格', description: '复古色调和纹理,装饰性边框' }
const templateList = [
{ id: 'classic', name: '经典风格', desc: '标准扑克牌设计,传统花色和字体', icon: '♠' },
{ id: 'modern', name: '现代简约', desc: '扁平化设计,简洁线条', icon: '◆' },
{ id: 'cartoon', name: '卡通风格', desc: 'Q版可爱人像圆润花色图案', icon: '★' },
{ id: 'vintage', name: '复古风格', desc: '复古色调和纹理,装饰性边框', icon: '♛' }
]
onMounted(async () => {
templates.value = defaultTemplates
await loadProjects()
const hasProjects = computed(() => projectList.value.length > 0)
onMounted(() => {
loadProjects()
})
async function loadProjects() {
loading.value = true
loadError.value = ''
try {
const response = await axios.get('/api/projects/')
projects.value = response.data.value || response.data || []
} catch (error) {
console.error('Failed to load projects:', error)
projects.value = []
const res = await axios.get('/api/projects/')
projectList.value = res.data || []
} catch (e) {
loadError.value = '无法连接后端服务: ' + e.message
} finally {
loading.value = false
}
}
async function createNewProject() {
async function doCreate() {
try {
const response = await axios.post('/api/projects/', {
name: `新项目 ${new Date().toLocaleDateString()}`,
const res = await axios.post('/api/projects/', {
name: '新项目 ' + new Date().toLocaleDateString(),
template_id: 'classic'
})
const projectId = response.data.id
router.push(`/editor/${projectId}`)
} catch (error) {
console.error('Failed to create project:', error)
alert('创建项目失败: ' + error.message)
router.push('/editor/' + res.data.id)
} catch (e) {
alert('创建失败: ' + e.message)
}
}
async function selectTemplate(templateId) {
async function createFromTemplate(tid) {
try {
const response = await axios.post('/api/projects/', {
name: `${templateId} - 新项目`,
template_id: templateId
const nm = templateList.find(t => t.id === tid)?.name || tid
const res = await axios.post('/api/projects/', {
name: nm + ' - 新项目',
template_id: tid
})
const projectId = response.data.id
router.push(`/editor/${projectId}`)
} catch (error) {
console.error('Failed to create project:', error)
alert('创建项目失败: ' + error.message)
router.push('/editor/' + res.data.id)
} catch (e) {
alert('创建失败: ' + e.message)
}
}
function editProject(projectId) {
router.push(`/editor/${projectId}`)
function editProject(id) {
router.push('/editor/' + id)
}
async function deleteProject(projectId) {
if (!confirm('确定删除这个项目吗')) return
async function removeProject(id) {
if (!confirm('确定删除?')) return
try {
await axios.delete(`/api/projects/${projectId}/`)
await axios.delete('/api/projects/' + id + '/')
await loadProjects()
} catch (error) {
console.error('Failed to delete project:', error)
alert('删除失败: ' + error.message)
} catch (e) {
alert('删除失败: ' + e.message)
}
}
function formatDate(dateString) {
return new Date(dateString).toLocaleString('zh-CN')
function formatDate(d) {
return new Date(d).toLocaleString('zh-CN')
}
</script>
<style scoped>
.home-container {
min-height: 100vh;
background: #f5f5f5;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px 30px;
}
.header h1 {
font-size: 24px;
margin: 0;
}
.create-btn {
background: white;
color: #667eea;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
font-size: 14px;
}
.create-btn:hover {
background: #f0f0f0;
}
.main {
padding: 30px;
max-width: 1200px;
margin: 0 auto;
}
.main h2 {
margin-bottom: 20px;
color: #333;
}
.tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.tabs button {
padding: 10px 20px;
border: 1px solid #ddd;
background: white;
cursor: pointer;
border-radius: 4px;
font-size: 14px;
}
.tabs button.active {
background: #667eea;
color: white;
border-color: #667eea;
}
.template-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
}
.template-card {
background: white;
border: 2px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
cursor: pointer;
transition: all 0.3s;
}
.template-card:hover {
border-color: #667eea;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
}
.template-preview {
width: 100%;
height: 120px;
background: #f0f0f0;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 36px;
color: #667eea;
margin-bottom: 15px;
}
.template-card h3 {
margin: 0 0 8px 0;
color: #333;
font-size: 16px;
}
.template-card p {
margin: 0;
color: #666;
font-size: 14px;
line-height: 1.5;
}
.project-list {
background: white;
border-radius: 8px;
padding: 20px;
}
.project-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background: #f5f5f5;
border-radius: 6px;
margin-bottom: 10px;
}
.project-info h3 {
margin: 0 0 5px 0;
color: #333;
}
.project-info p {
margin: 0;
color: #666;
font-size: 14px;
}
.project-actions {
display: flex;
gap: 10px;
}
.project-actions button {
padding: 6px 12px;
border: 1px solid #ddd;
background: white;
cursor: pointer;
border-radius: 4px;
}
.empty-state {
text-align: center;
padding: 50px;
color: #999;
}
</style>