Refactor frontend to Vue 3 + Vite + Pinia + Cypress E2E
- Replace single-file 8441-line HTML with Vue 3 SPA - Pinia stores: auth, oils, recipes, diary, ui - Composables: useApi, useDialog, useSmartPaste, useOilTranslation - 6 shared components: RecipeCard, RecipeDetailOverlay, TagPicker, etc. - 9 page views: RecipeSearch, RecipeManager, Inventory, OilReference, etc. - 14 Cypress E2E test specs (113 tests), all passing - Multi-stage Dockerfile (Node build + Python runtime) - Demo video generation scripts (TTS + subtitles + screen recording) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
759
frontend/src/views/Projects.vue
Normal file
759
frontend/src/views/Projects.vue
Normal file
@@ -0,0 +1,759 @@
|
||||
<template>
|
||||
<div class="projects-page">
|
||||
<!-- Project List -->
|
||||
<div class="toolbar">
|
||||
<h3 class="page-title">💼 商业核算</h3>
|
||||
<button class="btn-primary" @click="createProject">+ 新建项目</button>
|
||||
</div>
|
||||
|
||||
<div v-if="!selectedProject" class="project-list">
|
||||
<div
|
||||
v-for="p in projects"
|
||||
:key="p._id || p.id"
|
||||
class="project-card"
|
||||
@click="selectProject(p)"
|
||||
>
|
||||
<div class="proj-header">
|
||||
<span class="proj-name">{{ p.name }}</span>
|
||||
<span class="proj-date">{{ formatDate(p.updated_at || p.created_at) }}</span>
|
||||
</div>
|
||||
<div class="proj-summary">
|
||||
<span>成分: {{ (p.ingredients || []).length }} 种</span>
|
||||
<span class="proj-cost" v-if="p.ingredients && p.ingredients.length">
|
||||
成本 {{ oils.fmtPrice(oils.calcCost(p.ingredients)) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="proj-actions" @click.stop>
|
||||
<button class="btn-icon-sm" @click="deleteProject(p)" title="删除">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="projects.length === 0" class="empty-hint">暂无项目,点击上方创建</div>
|
||||
</div>
|
||||
|
||||
<!-- Project Detail -->
|
||||
<div v-if="selectedProject" class="project-detail">
|
||||
<div class="detail-toolbar">
|
||||
<button class="btn-back" @click="selectedProject = null">← 返回列表</button>
|
||||
<input
|
||||
v-model="selectedProject.name"
|
||||
class="proj-name-input"
|
||||
@blur="saveProject"
|
||||
/>
|
||||
<button class="btn-outline btn-sm" @click="importFromRecipe">📋 从配方导入</button>
|
||||
</div>
|
||||
|
||||
<!-- Ingredients Editor -->
|
||||
<div class="ingredients-section">
|
||||
<h4>🧴 配方成分</h4>
|
||||
<div v-for="(ing, i) in selectedProject.ingredients" :key="i" class="ing-row">
|
||||
<select v-model="ing.oil" class="form-select" @change="saveProject">
|
||||
<option value="">选择精油</option>
|
||||
<option v-for="name in oils.oilNames" :key="name" :value="name">{{ name }}</option>
|
||||
</select>
|
||||
<input
|
||||
v-model.number="ing.drops"
|
||||
type="number"
|
||||
min="0"
|
||||
class="form-input-sm"
|
||||
placeholder="滴数"
|
||||
@change="saveProject"
|
||||
/>
|
||||
<span class="ing-cost">{{ ing.oil ? oils.fmtPrice(oils.pricePerDrop(ing.oil) * (ing.drops || 0)) : '--' }}</span>
|
||||
<button class="btn-icon-sm" @click="removeIngredient(i)">✕</button>
|
||||
</div>
|
||||
<button class="btn-outline btn-sm" @click="addIngredient">+ 添加成分</button>
|
||||
</div>
|
||||
|
||||
<!-- Pricing Section -->
|
||||
<div class="pricing-section">
|
||||
<h4>💰 价格计算</h4>
|
||||
<div class="price-row">
|
||||
<span class="price-label">原料成本</span>
|
||||
<span class="price-value cost">{{ oils.fmtPrice(materialCost) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="price-row">
|
||||
<span class="price-label">包装费用</span>
|
||||
<div class="price-input-wrap">
|
||||
<span>¥</span>
|
||||
<input v-model.number="selectedProject.packaging_cost" type="number" class="form-input-inline" @change="saveProject" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="price-row">
|
||||
<span class="price-label">人工费用</span>
|
||||
<div class="price-input-wrap">
|
||||
<span>¥</span>
|
||||
<input v-model.number="selectedProject.labor_cost" type="number" class="form-input-inline" @change="saveProject" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="price-row">
|
||||
<span class="price-label">其他成本</span>
|
||||
<div class="price-input-wrap">
|
||||
<span>¥</span>
|
||||
<input v-model.number="selectedProject.other_cost" type="number" class="form-input-inline" @change="saveProject" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="price-row total">
|
||||
<span class="price-label">总成本</span>
|
||||
<span class="price-value cost">{{ oils.fmtPrice(totalCost) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="price-row">
|
||||
<span class="price-label">售价</span>
|
||||
<div class="price-input-wrap">
|
||||
<span>¥</span>
|
||||
<input v-model.number="selectedProject.selling_price" type="number" class="form-input-inline" @change="saveProject" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="price-row">
|
||||
<span class="price-label">批量数量</span>
|
||||
<input v-model.number="selectedProject.quantity" type="number" min="1" class="form-input-inline" @change="saveProject" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profit Analysis -->
|
||||
<div class="profit-section">
|
||||
<h4>📊 利润分析</h4>
|
||||
<div class="profit-grid">
|
||||
<div class="profit-card">
|
||||
<div class="profit-label">单件利润</div>
|
||||
<div class="profit-value" :class="{ negative: unitProfit < 0 }">{{ oils.fmtPrice(unitProfit) }}</div>
|
||||
</div>
|
||||
<div class="profit-card">
|
||||
<div class="profit-label">利润率</div>
|
||||
<div class="profit-value" :class="{ negative: profitMargin < 0 }">{{ profitMargin.toFixed(1) }}%</div>
|
||||
</div>
|
||||
<div class="profit-card">
|
||||
<div class="profit-label">批量总利润</div>
|
||||
<div class="profit-value" :class="{ negative: batchProfit < 0 }">{{ oils.fmtPrice(batchProfit) }}</div>
|
||||
</div>
|
||||
<div class="profit-card">
|
||||
<div class="profit-label">批量总收入</div>
|
||||
<div class="profit-value">{{ oils.fmtPrice(batchRevenue) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="notes-section">
|
||||
<h4>📝 备注</h4>
|
||||
<textarea
|
||||
v-model="selectedProject.notes"
|
||||
class="notes-textarea"
|
||||
rows="3"
|
||||
placeholder="项目备注..."
|
||||
@blur="saveProject"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import From Recipe Modal -->
|
||||
<div v-if="showImportModal" class="overlay" @click.self="showImportModal = false">
|
||||
<div class="overlay-panel">
|
||||
<div class="overlay-header">
|
||||
<h3>从配方导入</h3>
|
||||
<button class="btn-close" @click="showImportModal = false">✕</button>
|
||||
</div>
|
||||
<div class="recipe-import-list">
|
||||
<div
|
||||
v-for="r in recipeStore.recipes"
|
||||
:key="r._id"
|
||||
class="import-item"
|
||||
@click="doImport(r)"
|
||||
>
|
||||
<span class="import-name">{{ r.name }}</span>
|
||||
<span class="import-count">{{ r.ingredients.length }} 种精油</span>
|
||||
<span class="import-cost">{{ oils.fmtPrice(oils.calcCost(r.ingredients)) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { useOilsStore } from '../stores/oils'
|
||||
import { useRecipesStore } from '../stores/recipes'
|
||||
import { useUiStore } from '../stores/ui'
|
||||
import { api } from '../composables/useApi'
|
||||
import { showConfirm, showPrompt } from '../composables/useDialog'
|
||||
|
||||
const auth = useAuthStore()
|
||||
const oils = useOilsStore()
|
||||
const recipeStore = useRecipesStore()
|
||||
const ui = useUiStore()
|
||||
|
||||
const projects = ref([])
|
||||
const selectedProject = ref(null)
|
||||
const showImportModal = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
await loadProjects()
|
||||
})
|
||||
|
||||
async function loadProjects() {
|
||||
try {
|
||||
const res = await api('/api/projects')
|
||||
if (res.ok) {
|
||||
projects.value = await res.json()
|
||||
}
|
||||
} catch {
|
||||
projects.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function createProject() {
|
||||
const name = await showPrompt('项目名称:', '新项目')
|
||||
if (!name) return
|
||||
try {
|
||||
const res = await api('/api/projects', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
ingredients: [],
|
||||
packaging_cost: 0,
|
||||
labor_cost: 0,
|
||||
other_cost: 0,
|
||||
selling_price: 0,
|
||||
quantity: 1,
|
||||
notes: '',
|
||||
}),
|
||||
})
|
||||
if (res.ok) {
|
||||
await loadProjects()
|
||||
const data = await res.json()
|
||||
selectedProject.value = projects.value.find(p => (p._id || p.id) === (data._id || data.id)) || null
|
||||
ui.showToast('项目已创建')
|
||||
}
|
||||
} catch {
|
||||
ui.showToast('创建失败')
|
||||
}
|
||||
}
|
||||
|
||||
function selectProject(p) {
|
||||
selectedProject.value = {
|
||||
...p,
|
||||
ingredients: (p.ingredients || []).map(i => ({ ...i })),
|
||||
packaging_cost: p.packaging_cost || 0,
|
||||
labor_cost: p.labor_cost || 0,
|
||||
other_cost: p.other_cost || 0,
|
||||
selling_price: p.selling_price || 0,
|
||||
quantity: p.quantity || 1,
|
||||
notes: p.notes || '',
|
||||
}
|
||||
}
|
||||
|
||||
async function saveProject() {
|
||||
if (!selectedProject.value) return
|
||||
const id = selectedProject.value._id || selectedProject.value.id
|
||||
try {
|
||||
await api(`/api/projects/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(selectedProject.value),
|
||||
})
|
||||
await loadProjects()
|
||||
} catch {
|
||||
// silent save
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteProject(p) {
|
||||
const ok = await showConfirm(`确定删除项目 "${p.name}"?`)
|
||||
if (!ok) return
|
||||
const id = p._id || p.id
|
||||
try {
|
||||
await api(`/api/projects/${id}`, { method: 'DELETE' })
|
||||
projects.value = projects.value.filter(proj => (proj._id || proj.id) !== id)
|
||||
if (selectedProject.value && (selectedProject.value._id || selectedProject.value.id) === id) {
|
||||
selectedProject.value = null
|
||||
}
|
||||
ui.showToast('已删除')
|
||||
} catch {
|
||||
ui.showToast('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
function addIngredient() {
|
||||
if (!selectedProject.value) return
|
||||
selectedProject.value.ingredients.push({ oil: '', drops: 1 })
|
||||
}
|
||||
|
||||
function removeIngredient(index) {
|
||||
selectedProject.value.ingredients.splice(index, 1)
|
||||
saveProject()
|
||||
}
|
||||
|
||||
function importFromRecipe() {
|
||||
showImportModal.value = true
|
||||
}
|
||||
|
||||
function doImport(recipe) {
|
||||
if (!selectedProject.value) return
|
||||
selectedProject.value.ingredients = recipe.ingredients.map(i => ({ ...i }))
|
||||
showImportModal.value = false
|
||||
saveProject()
|
||||
ui.showToast(`已导入 "${recipe.name}" 的配方`)
|
||||
}
|
||||
|
||||
const materialCost = computed(() => {
|
||||
if (!selectedProject.value) return 0
|
||||
return oils.calcCost(selectedProject.value.ingredients.filter(i => i.oil))
|
||||
})
|
||||
|
||||
const totalCost = computed(() => {
|
||||
if (!selectedProject.value) return 0
|
||||
return materialCost.value +
|
||||
(selectedProject.value.packaging_cost || 0) +
|
||||
(selectedProject.value.labor_cost || 0) +
|
||||
(selectedProject.value.other_cost || 0)
|
||||
})
|
||||
|
||||
const unitProfit = computed(() => {
|
||||
if (!selectedProject.value) return 0
|
||||
return (selectedProject.value.selling_price || 0) - totalCost.value
|
||||
})
|
||||
|
||||
const profitMargin = computed(() => {
|
||||
if (!selectedProject.value || !selectedProject.value.selling_price) return 0
|
||||
return (unitProfit.value / selectedProject.value.selling_price) * 100
|
||||
})
|
||||
|
||||
const batchProfit = computed(() => {
|
||||
return unitProfit.value * (selectedProject.value?.quantity || 1)
|
||||
})
|
||||
|
||||
const batchRevenue = computed(() => {
|
||||
return (selectedProject.value?.selling_price || 0) * (selectedProject.value?.quantity || 1)
|
||||
})
|
||||
|
||||
function formatDate(d) {
|
||||
if (!d) return ''
|
||||
return new Date(d).toLocaleDateString('zh-CN')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.projects-page {
|
||||
padding: 0 12px 24px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: #3e3a44;
|
||||
}
|
||||
|
||||
.project-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
padding: 14px 16px;
|
||||
background: #fff;
|
||||
border: 1.5px solid #e5e4e7;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.project-card:hover {
|
||||
border-color: #7ec6a4;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.proj-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.proj-name {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
color: #3e3a44;
|
||||
}
|
||||
|
||||
.proj-date {
|
||||
font-size: 12px;
|
||||
color: #b0aab5;
|
||||
}
|
||||
|
||||
.proj-summary {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
color: #6b6375;
|
||||
}
|
||||
|
||||
.proj-cost {
|
||||
color: #4a9d7e;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.proj-actions {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.project-card:hover .proj-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Detail */
|
||||
.detail-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 18px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
border: none;
|
||||
background: #f0eeeb;
|
||||
padding: 8px 14px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
color: #6b6375;
|
||||
}
|
||||
|
||||
.btn-back:hover {
|
||||
background: #e5e4e7;
|
||||
}
|
||||
|
||||
.proj-name-input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1.5px solid #d4cfc7;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.proj-name-input:focus {
|
||||
border-color: #7ec6a4;
|
||||
}
|
||||
|
||||
.ingredients-section,
|
||||
.pricing-section,
|
||||
.profit-section,
|
||||
.notes-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 14px;
|
||||
background: #f8f7f5;
|
||||
border-radius: 12px;
|
||||
border: 1.5px solid #e5e4e7;
|
||||
}
|
||||
|
||||
.ingredients-section h4,
|
||||
.pricing-section h4,
|
||||
.profit-section h4,
|
||||
.notes-section h4 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 14px;
|
||||
color: #3e3a44;
|
||||
}
|
||||
|
||||
.ing-row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.form-select {
|
||||
flex: 1;
|
||||
padding: 8px 10px;
|
||||
border: 1.5px solid #d4cfc7;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.form-input-sm {
|
||||
width: 70px;
|
||||
padding: 8px 10px;
|
||||
border: 1.5px solid #d4cfc7;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ing-cost {
|
||||
font-size: 13px;
|
||||
color: #4a9d7e;
|
||||
font-weight: 500;
|
||||
min-width: 60px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.price-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #eae8e5;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.price-row.total {
|
||||
border-top: 2px solid #d4cfc7;
|
||||
border-bottom: 2px solid #d4cfc7;
|
||||
font-weight: 600;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.price-label {
|
||||
color: #6b6375;
|
||||
}
|
||||
|
||||
.price-value {
|
||||
font-weight: 600;
|
||||
color: #3e3a44;
|
||||
}
|
||||
|
||||
.price-value.cost {
|
||||
color: #4a9d7e;
|
||||
}
|
||||
|
||||
.price-input-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 14px;
|
||||
color: #3e3a44;
|
||||
}
|
||||
|
||||
.form-input-inline {
|
||||
width: 80px;
|
||||
padding: 6px 8px;
|
||||
border: 1.5px solid #d4cfc7;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.form-input-inline:focus {
|
||||
border-color: #7ec6a4;
|
||||
}
|
||||
|
||||
.profit-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.profit-card {
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
border: 1.5px solid #e5e4e7;
|
||||
}
|
||||
|
||||
.profit-label {
|
||||
font-size: 12px;
|
||||
color: #6b6375;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.profit-value {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #4a9d7e;
|
||||
}
|
||||
|
||||
.profit-value.negative {
|
||||
color: #ef5350;
|
||||
}
|
||||
|
||||
.notes-textarea {
|
||||
width: 100%;
|
||||
border: 1.5px solid #d4cfc7;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.notes-textarea:focus {
|
||||
border-color: #7ec6a4;
|
||||
}
|
||||
|
||||
/* Overlay */
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.overlay-panel {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.overlay-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.overlay-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: #3e3a44;
|
||||
}
|
||||
|
||||
.recipe-import-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.import-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.import-item:hover {
|
||||
background: #f0faf5;
|
||||
}
|
||||
|
||||
.import-name {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: #3e3a44;
|
||||
}
|
||||
|
||||
.import-count {
|
||||
font-size: 12px;
|
||||
color: #b0aab5;
|
||||
}
|
||||
|
||||
.import-cost {
|
||||
font-size: 13px;
|
||||
color: #4a9d7e;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #7ec6a4 0%, #4a9d7e 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 9px 20px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: #fff;
|
||||
color: #6b6375;
|
||||
border: 1.5px solid #d4cfc7;
|
||||
border-radius: 10px;
|
||||
padding: 9px 20px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: #f8f7f5;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 6px 14px;
|
||||
font-size: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.btn-icon-sm {
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
padding: 4px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
border: none;
|
||||
background: #f0eeeb;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #6b6375;
|
||||
}
|
||||
|
||||
.empty-hint {
|
||||
text-align: center;
|
||||
color: #b0aab5;
|
||||
font-size: 13px;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.profit-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user