- 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>
760 lines
17 KiB
Vue
760 lines
17 KiB
Vue
<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>
|