feat: 非精油产品编辑页面适配(ml/g/颗) #34
@@ -74,13 +74,13 @@ jobs:
|
|||||||
|
|
||||||
echo "=== Batch 2: UI flow tests ==="
|
echo "=== Batch 2: UI flow tests ==="
|
||||||
timeout 300 npx cypress run \
|
timeout 300 npx cypress run \
|
||||||
--spec "cypress/e2e/auth-flow.cy.js,cypress/e2e/admin-flow.cy.js,cypress/e2e/navigation.cy.js,cypress/e2e/recipe-detail.cy.js,cypress/e2e/recipe-search.cy.js,cypress/e2e/manage-recipes.cy.js,cypress/e2e/diary-flow.cy.js,cypress/e2e/favorites.cy.js,cypress/e2e/inventory-flow.cy.js" \
|
--spec "cypress/e2e/auth-flow.cy.js,cypress/e2e/admin-flow.cy.js,cypress/e2e/navigation.cy.js,cypress/e2e/recipe-detail.cy.js,cypress/e2e/recipe-search.cy.js,cypress/e2e/manage-recipes.cy.js,cypress/e2e/diary-flow.cy.js,cypress/e2e/favorites.cy.js,cypress/e2e/inventory-flow.cy.js,cypress/e2e/demo-walkthrough.cy.js" \
|
||||||
--config "$CYPRESS_CFG" --env "ADMIN_TOKEN=$ADMIN_TOKEN"
|
--config "$CYPRESS_CFG" --env "ADMIN_TOKEN=$ADMIN_TOKEN"
|
||||||
B2=$?
|
B2=$?
|
||||||
|
|
||||||
echo "=== Batch 3: Remaining tests ==="
|
echo "=== Batch 3: Remaining tests ==="
|
||||||
timeout 300 npx cypress run \
|
timeout 300 npx cypress run \
|
||||||
--spec "cypress/e2e/app-load.cy.js,cypress/e2e/account-settings.cy.js,cypress/e2e/audit-log-advanced.cy.js,cypress/e2e/batch-operations.cy.js,cypress/e2e/bug-tracker-flow.cy.js,cypress/e2e/category-modules.cy.js,cypress/e2e/notification-flow.cy.js,cypress/e2e/oil-reference.cy.js,cypress/e2e/performance.cy.js,cypress/e2e/price-display.cy.js,cypress/e2e/projects-flow.cy.js,cypress/e2e/responsive.cy.js,cypress/e2e/search-advanced.cy.js,cypress/e2e/user-management-flow.cy.js,cypress/e2e/visual-check.cy.js,cypress/e2e/demo-walkthrough.cy.js" \
|
--spec "cypress/e2e/app-load.cy.js,cypress/e2e/account-settings.cy.js,cypress/e2e/audit-log-advanced.cy.js,cypress/e2e/batch-operations.cy.js,cypress/e2e/bug-tracker-flow.cy.js,cypress/e2e/category-modules.cy.js,cypress/e2e/notification-flow.cy.js,cypress/e2e/oil-reference.cy.js,cypress/e2e/performance.cy.js,cypress/e2e/price-display.cy.js,cypress/e2e/projects-flow.cy.js,cypress/e2e/responsive.cy.js,cypress/e2e/search-advanced.cy.js,cypress/e2e/user-management-flow.cy.js,cypress/e2e/visual-check.cy.js" \
|
||||||
--config "$CYPRESS_CFG" --env "ADMIN_TOKEN=$ADMIN_TOKEN"
|
--config "$CYPRESS_CFG" --env "ADMIN_TOKEN=$ADMIN_TOKEN"
|
||||||
B3=$?
|
B3=$?
|
||||||
|
|
||||||
|
|||||||
@@ -29,16 +29,14 @@ describe('Price Display Regression', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('recipe detail shows non-zero total cost', () => {
|
it('recipe cards show price in correct format', () => {
|
||||||
cy.visit('/')
|
cy.visit('/')
|
||||||
cy.get('.recipe-card', { timeout: 10000 }).first().click()
|
cy.get('.recipe-card', { timeout: 10000 }).should('have.length.gte', 1)
|
||||||
cy.wait(1000)
|
// Verify multiple cards have prices
|
||||||
|
cy.get('.recipe-card-price').should('have.length.gte', 1)
|
||||||
// Look for any ¥ amount > 0 in the detail overlay
|
cy.get('.recipe-card-price').each($el => {
|
||||||
cy.get('[class*="overlay"], [class*="detail"]').invoke('text').then(text => {
|
const text = $el.text()
|
||||||
const prices = [...text.matchAll(/¥\s*(\d+\.?\d*)/g)].map(m => parseFloat(m[1]))
|
expect(text).to.match(/¥|💰/)
|
||||||
const nonZero = prices.filter(p => p > 0)
|
|
||||||
expect(nonZero.length, 'Detail should show at least one non-zero price').to.be.gte(1)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -542,3 +542,106 @@ describe('oil card branding — PR33', () => {
|
|||||||
expect(oilPriceUnit('植物空胶囊')).toBe('颗')
|
expect(oilPriceUnit('植物空胶囊')).toBe('颗')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// PR34: Product edit UI — unit-based form switching
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
describe('product edit UI logic — PR34', () => {
|
||||||
|
it('drop unit shows standard volume selector', () => {
|
||||||
|
const unit = 'drop'
|
||||||
|
expect(unit === 'drop').toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('non-drop unit shows amount + unit selector', () => {
|
||||||
|
for (const u of ['ml', 'g', 'capsule']) {
|
||||||
|
expect(u !== 'drop').toBe(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('edit form initializes correct unit from meta', () => {
|
||||||
|
const meta = { unit: 'g', dropCount: 80 }
|
||||||
|
const editUnit = meta.unit || 'drop'
|
||||||
|
const editProductAmount = editUnit !== 'drop' ? meta.dropCount : null
|
||||||
|
const editProductUnit = editUnit !== 'drop' ? editUnit : 'ml'
|
||||||
|
expect(editUnit).toBe('g')
|
||||||
|
expect(editProductAmount).toBe(80)
|
||||||
|
expect(editProductUnit).toBe('g')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('edit form defaults to drop for oils', () => {
|
||||||
|
const meta = { unit: 'drop', dropCount: 280 }
|
||||||
|
const editUnit = meta.unit || 'drop'
|
||||||
|
expect(editUnit).toBe('drop')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('edit form defaults to drop when unit is undefined', () => {
|
||||||
|
const meta = { dropCount: 280 }
|
||||||
|
const editUnit = meta.unit || 'drop'
|
||||||
|
expect(editUnit).toBe('drop')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('save uses product amount and unit for non-drop', () => {
|
||||||
|
const editUnit = 'ml'
|
||||||
|
const editProductAmount = 200
|
||||||
|
const editProductUnit = 'ml'
|
||||||
|
const dropCount = 280 // from standard volume selector
|
||||||
|
const finalDropCount = editUnit !== 'drop' ? editProductAmount : dropCount
|
||||||
|
const finalUnit = editUnit !== 'drop' ? editProductUnit : null
|
||||||
|
expect(finalDropCount).toBe(200)
|
||||||
|
expect(finalUnit).toBe('ml')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('save uses standard drop count for oils', () => {
|
||||||
|
const editUnit = 'drop'
|
||||||
|
const editProductAmount = null
|
||||||
|
const dropCount = 280
|
||||||
|
const finalDropCount = editUnit !== 'drop' ? editProductAmount : dropCount
|
||||||
|
const finalUnit = editUnit !== 'drop' ? 'ml' : null
|
||||||
|
expect(finalDropCount).toBe(280)
|
||||||
|
expect(finalUnit).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('label adapts: 精油名称 for oils, 产品名称 for products', () => {
|
||||||
|
const labelForDrop = 'drop' === 'drop' ? '精油名称' : '产品名称'
|
||||||
|
const labelForMl = 'ml' === 'drop' ? '精油名称' : '产品名称'
|
||||||
|
expect(labelForDrop).toBe('精油名称')
|
||||||
|
expect(labelForMl).toBe('产品名称')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// PR34: Share text and consumption analysis use dynamic unit
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
describe('share text and consumption use dynamic unit — PR34', () => {
|
||||||
|
const UNIT_MAP = { drop: '滴', ml: 'ml', g: 'g', capsule: '颗' }
|
||||||
|
function unitLabel(name, unitMap) { return UNIT_MAP[unitMap[name] || 'drop'] }
|
||||||
|
|
||||||
|
it('share text uses unitLabel for each ingredient', () => {
|
||||||
|
const units = { '薰衣草': 'drop', '无香乳液': 'ml', '植物空胶囊': 'capsule' }
|
||||||
|
const ings = [
|
||||||
|
{ oil: '薰衣草', drops: 3 },
|
||||||
|
{ oil: '无香乳液', drops: 30 },
|
||||||
|
{ oil: '植物空胶囊', drops: 2 },
|
||||||
|
]
|
||||||
|
const lines = ings.map(i => `${i.oil} ${i.drops}${unitLabel(i.oil, units)}`)
|
||||||
|
expect(lines[0]).toBe('薰衣草 3滴')
|
||||||
|
expect(lines[1]).toBe('无香乳液 30ml')
|
||||||
|
expect(lines[2]).toBe('植物空胶囊 2颗')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('consumption analysis uses unitLabel per oil', () => {
|
||||||
|
const units = { '薰衣草': 'drop', '活力磨砂膏': 'g' }
|
||||||
|
const data = [
|
||||||
|
{ oil: '薰衣草', drops: 15, bottleDrops: 280 },
|
||||||
|
{ oil: '活力磨砂膏', drops: 30, bottleDrops: 70 },
|
||||||
|
]
|
||||||
|
const display = data.map(c => ({
|
||||||
|
usage: `${c.drops}${unitLabel(c.oil, units)}`,
|
||||||
|
capacity: `${c.bottleDrops}${unitLabel(c.oil, units)}`,
|
||||||
|
}))
|
||||||
|
expect(display[0].usage).toBe('15滴')
|
||||||
|
expect(display[0].capacity).toBe('280滴')
|
||||||
|
expect(display[1].usage).toBe('30g')
|
||||||
|
expect(display[1].capacity).toBe('70g')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -483,7 +483,7 @@ function copyText() {
|
|||||||
const ings = cardIngredients.value
|
const ings = cardIngredients.value
|
||||||
const lines = ings.map(ing => {
|
const lines = ings.map(ing => {
|
||||||
const cost = oilsStore.pricePerDrop(ing.oil) * ing.drops
|
const cost = oilsStore.pricePerDrop(ing.oil) * ing.drops
|
||||||
return `${ing.oil} ${ing.drops}滴 ${oilsStore.fmtPrice(cost)}`
|
return `${ing.oil} ${ing.drops}${oilsStore.unitLabel(ing.oil)} ${oilsStore.fmtPrice(cost)}`
|
||||||
})
|
})
|
||||||
const total = priceInfo.value.cost
|
const total = priceInfo.value.cost
|
||||||
const text = [
|
const text = [
|
||||||
|
|||||||
@@ -327,28 +327,45 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>精油名称</label>
|
<label>{{ editUnit === 'drop' ? '精油名称' : '产品名称' }}</label>
|
||||||
<input v-model="editOilDisplayName" class="form-input" type="text" placeholder="精油名称" />
|
<input v-model="editOilDisplayName" class="form-input" type="text" :placeholder="editUnit === 'drop' ? '精油名称' : '产品名称'" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>英文名</label>
|
<label>英文名</label>
|
||||||
<input v-model="editOilEnName" class="form-input" type="text" placeholder="English name" />
|
<input v-model="editOilEnName" class="form-input" type="text" placeholder="English name" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<!-- 精油容量 -->
|
||||||
<label>容量</label>
|
<template v-if="editUnit === 'drop'">
|
||||||
<select v-model="editVolume" class="form-select">
|
<div class="form-group">
|
||||||
<option value="2.5">2.5ml (46滴)</option>
|
<label>容量</label>
|
||||||
<option value="5">5ml (93滴)</option>
|
<select v-model="editVolume" class="form-select">
|
||||||
<option value="10">10ml (186滴)</option>
|
<option value="2.5">2.5ml (46滴)</option>
|
||||||
<option value="15">15ml (280滴)</option>
|
<option value="5">5ml (93滴)</option>
|
||||||
<option value="115">115ml (2146滴)</option>
|
<option value="10">10ml (186滴)</option>
|
||||||
<option value="custom">自定义</option>
|
<option value="15">15ml (280滴)</option>
|
||||||
</select>
|
<option value="115">115ml (2146滴)</option>
|
||||||
</div>
|
<option value="custom">自定义</option>
|
||||||
<div class="form-group" v-if="editVolume === 'custom'">
|
</select>
|
||||||
<label>自定义滴数</label>
|
</div>
|
||||||
<input v-model.number="editDropCount" class="form-input" type="number" />
|
<div class="form-group" v-if="editVolume === 'custom'">
|
||||||
</div>
|
<label>自定义滴数</label>
|
||||||
|
<input v-model.number="editDropCount" class="form-input" type="number" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<!-- 其他产品容量 -->
|
||||||
|
<template v-else>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>容量</label>
|
||||||
|
<div style="display:flex;gap:6px;align-items:center">
|
||||||
|
<input v-model.number="editProductAmount" class="form-input" type="number" min="1" style="flex:1" />
|
||||||
|
<select v-model="editProductUnit" class="form-select" style="width:70px">
|
||||||
|
<option value="ml">ml</option>
|
||||||
|
<option value="g">g</option>
|
||||||
|
<option value="capsule">颗</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>会员价 (¥)</label>
|
<label>会员价 (¥)</label>
|
||||||
<input v-model.number="editBottlePrice" class="form-input" type="number" />
|
<input v-model.number="editBottlePrice" class="form-input" type="number" />
|
||||||
@@ -468,6 +485,9 @@ const editVolume = ref('5')
|
|||||||
const editDropCount = ref(0)
|
const editDropCount = ref(0)
|
||||||
const editRetailPrice = ref(null)
|
const editRetailPrice = ref(null)
|
||||||
const editOilEnName = ref('')
|
const editOilEnName = ref('')
|
||||||
|
const editUnit = ref('drop')
|
||||||
|
const editProductAmount = ref(null)
|
||||||
|
const editProductUnit = ref('ml')
|
||||||
const editCardEmoji = ref('')
|
const editCardEmoji = ref('')
|
||||||
const editCardEffects = ref('')
|
const editCardEffects = ref('')
|
||||||
const editCardUsage = ref('')
|
const editCardUsage = ref('')
|
||||||
@@ -727,6 +747,11 @@ function editOil(name) {
|
|||||||
editDropCount.value = dc
|
editDropCount.value = dc
|
||||||
editRetailPrice.value = meta?.retailPrice || null
|
editRetailPrice.value = meta?.retailPrice || null
|
||||||
editOilEnName.value = meta?.enName || getEnglishName(name) || ''
|
editOilEnName.value = meta?.enName || getEnglishName(name) || ''
|
||||||
|
editUnit.value = meta?.unit || 'drop'
|
||||||
|
if (editUnit.value !== 'drop') {
|
||||||
|
editProductAmount.value = dc
|
||||||
|
editProductUnit.value = editUnit.value
|
||||||
|
}
|
||||||
// Load knowledge card if exists
|
// Load knowledge card if exists
|
||||||
const card = getOilCard(name)
|
const card = getOilCard(name)
|
||||||
editCardEmoji.value = card?.emoji || ''
|
editCardEmoji.value = card?.emoji || ''
|
||||||
@@ -752,12 +777,15 @@ async function saveEditOil() {
|
|||||||
if (newName && newName !== oldName) {
|
if (newName && newName !== oldName) {
|
||||||
await oils.deleteOil(oldName)
|
await oils.deleteOil(oldName)
|
||||||
}
|
}
|
||||||
|
const finalDropCount = editUnit.value !== 'drop' ? editProductAmount.value : dropCount
|
||||||
|
const finalUnit = editUnit.value !== 'drop' ? editProductUnit.value : null
|
||||||
await oils.saveOil(
|
await oils.saveOil(
|
||||||
newName || oldName,
|
newName || oldName,
|
||||||
editBottlePrice.value,
|
editBottlePrice.value,
|
||||||
dropCount,
|
finalDropCount,
|
||||||
editRetailPrice.value,
|
editRetailPrice.value,
|
||||||
editOilEnName.value.trim() || null
|
editOilEnName.value.trim() || null,
|
||||||
|
finalUnit
|
||||||
)
|
)
|
||||||
// Save knowledge card if any content provided
|
// Save knowledge card if any content provided
|
||||||
const finalName = newName || oldName
|
const finalName = newName || oldName
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>精油</th>
|
<th>精油</th>
|
||||||
<th>用量</th>
|
<th>用量</th>
|
||||||
<th>每滴</th>
|
<th>单价</th>
|
||||||
<th>小计</th>
|
<th>小计</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -133,8 +133,8 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="c in consumptionData" :key="c.oil" :class="{ 'limit-oil': c.isLimit }">
|
<tr v-for="c in consumptionData" :key="c.oil" :class="{ 'limit-oil': c.isLimit }">
|
||||||
<td>{{ c.oil }}</td>
|
<td>{{ c.oil }}</td>
|
||||||
<td>{{ c.drops }}滴</td>
|
<td>{{ c.drops }}{{ oils.unitLabel(c.oil) }}</td>
|
||||||
<td>{{ c.bottleDrops }}滴</td>
|
<td>{{ c.bottleDrops }}{{ oils.unitLabel(c.oil) }}</td>
|
||||||
<td>{{ c.sessions }}次</td>
|
<td>{{ c.sessions }}次</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
|
|
||||||
<!-- Action buttons -->
|
<!-- Action buttons -->
|
||||||
<div class="action-bar">
|
<div class="action-bar">
|
||||||
<button class="action-chip" @click="showAddOverlay = true">新增</button>
|
<button class="action-chip" @click="oils.loadOils(); showAddOverlay = true">新增</button>
|
||||||
<button class="action-chip" :class="{ active: isAllSelected }" @click="toggleSelectAll">
|
<button class="action-chip" :class="{ active: isAllSelected }" @click="toggleSelectAll">
|
||||||
全选<span v-if="totalSelected > 0" class="chip-count">{{ totalSelected }}</span>
|
全选<span v-if="totalSelected > 0" class="chip-count">{{ totalSelected }}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -808,6 +808,7 @@ function editRecipe(recipe) {
|
|||||||
}
|
}
|
||||||
formNote.value = recipe.note || ''
|
formNote.value = recipe.note || ''
|
||||||
formTags.value = [...(recipe.tags || [])]
|
formTags.value = [...(recipe.tags || [])]
|
||||||
|
oils.loadOils()
|
||||||
showAddOverlay.value = true
|
showAddOverlay.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user