255 lines
6.7 KiB
Vue
255 lines
6.7 KiB
Vue
<template>
|
||
<view class="calculator-page">
|
||
<!-- 自定义导航栏 -->
|
||
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||
<view class="custom-navbar__content" :style="{ height: navBarHeight + 'px' }">
|
||
<image class="custom-navbar__back" src="/static/ic_back.png" mode="aspectFit" @click="goBack" />
|
||
<text class="custom-navbar__title">钻戒计算器</text>
|
||
<view class="custom-navbar__placeholder" />
|
||
</view>
|
||
</view>
|
||
<view :style="{ height: (statusBarHeight + navBarHeight) + 'px' }" />
|
||
<view class="section">
|
||
<view class="section__title">基础参数</view>
|
||
<view class="form-item" v-for="field in basicFields" :key="field.key">
|
||
<text class="form-item__label">{{ field.label }}({{ field.unit }})</text>
|
||
<input
|
||
class="form-item__input"
|
||
type="digit"
|
||
:placeholder="'请输入' + field.label"
|
||
:value="String(form[field.key])"
|
||
@input="onInput(field.key, $event)"
|
||
/>
|
||
<text v-if="errors[field.key]" class="form-item__error">{{ errors[field.key] }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section">
|
||
<view class="section__title">费用参数</view>
|
||
<view class="form-item" v-for="field in feeFields" :key="field.key">
|
||
<text class="form-item__label">{{ field.label }}({{ field.unit }})</text>
|
||
<input
|
||
class="form-item__input"
|
||
type="digit"
|
||
:placeholder="'请输入' + field.label"
|
||
:value="String(form[field.key])"
|
||
@input="onInput(field.key, $event)"
|
||
/>
|
||
<text v-if="errors[field.key]" class="form-item__error">{{ errors[field.key] }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section result-section">
|
||
<view class="section__title">计算结果</view>
|
||
<view class="result-row" v-for="r in resultRows" :key="r.label">
|
||
<text class="result-row__label">{{ r.label }}</text>
|
||
<text class="result-row__value">{{ r.value }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, computed } from 'vue'
|
||
import type { RingCalculatorInput, RingCalculatorResult } from '../../types/calculator'
|
||
import { calculateRing } from '../../utils/calculator'
|
||
|
||
const statusBarHeight = ref(20)
|
||
const navBarHeight = ref(44)
|
||
try {
|
||
const sysInfo = uni.getSystemInfoSync()
|
||
statusBarHeight.value = sysInfo.statusBarHeight || 20
|
||
// #ifdef MP-WEIXIN
|
||
const menuBtn = uni.getMenuButtonBoundingClientRect()
|
||
navBarHeight.value = (menuBtn.top - (sysInfo.statusBarHeight || 20)) * 2 + menuBtn.height
|
||
// #endif
|
||
} catch { /* fallback */ }
|
||
|
||
function goBack() {
|
||
uni.navigateBack({ delta: 1 })
|
||
}
|
||
|
||
interface FieldDef {
|
||
key: keyof RingCalculatorInput
|
||
label: string
|
||
unit: string
|
||
}
|
||
|
||
const basicFields: FieldDef[] = [
|
||
{ key: 'goldWeight', label: '金重', unit: 'g' },
|
||
{ key: 'mainStoneWeight', label: '主石重', unit: 'ct' },
|
||
{ key: 'sideStoneWeight', label: '副石重', unit: 'ct' },
|
||
{ key: 'lossRate', label: '损耗', unit: '倍率' },
|
||
{ key: 'moldGoldPrice', label: '倒模金价', unit: '元' },
|
||
]
|
||
|
||
const feeFields: FieldDef[] = [
|
||
{ key: 'mainStoneUnitPrice', label: '主石单价', unit: '元' },
|
||
{ key: 'sideStoneUnitPrice', label: '副石单价', unit: '元' },
|
||
{ key: 'sideStoneCount', label: '副石粒数', unit: 'p' },
|
||
{ key: 'microSettingFee', label: '微镶费', unit: '元/粒' },
|
||
{ key: 'mainStoneSettingFee', label: '主石镶费', unit: '元' },
|
||
{ key: 'threeDFee', label: '3D起板费', unit: '元' },
|
||
{ key: 'basicLaborCost', label: '基本工费', unit: '元' },
|
||
{ key: 'otherCost', label: '其他费用', unit: '元' },
|
||
]
|
||
|
||
const form = reactive<RingCalculatorInput>({
|
||
goldWeight: 0,
|
||
mainStoneWeight: 0,
|
||
sideStoneWeight: 0,
|
||
lossRate: 1,
|
||
moldGoldPrice: 0,
|
||
mainStoneUnitPrice: 0,
|
||
sideStoneUnitPrice: 0,
|
||
sideStoneCount: 0,
|
||
microSettingFee: 0,
|
||
mainStoneSettingFee: 0,
|
||
threeDFee: 0,
|
||
basicLaborCost: 0,
|
||
otherCost: 0,
|
||
})
|
||
|
||
const errors = reactive<Partial<Record<keyof RingCalculatorInput, string>>>({})
|
||
|
||
function onInput(key: keyof RingCalculatorInput, event: { detail: { value: string } }) {
|
||
const raw = event.detail.value
|
||
const num = Number(raw)
|
||
|
||
if (raw === '' || raw === '-') {
|
||
form[key] = 0
|
||
delete errors[key]
|
||
return
|
||
}
|
||
|
||
if (isNaN(num)) {
|
||
errors[key] = '请输入有效数字'
|
||
return
|
||
}
|
||
|
||
if (num < 0) {
|
||
errors[key] = '不能为负数'
|
||
return
|
||
}
|
||
|
||
delete errors[key]
|
||
form[key] = num
|
||
}
|
||
|
||
const result = computed<RingCalculatorResult | null>(() => {
|
||
const hasErrors = Object.keys(errors).length > 0
|
||
if (hasErrors) return null
|
||
try {
|
||
return calculateRing({ ...form })
|
||
} catch {
|
||
return null
|
||
}
|
||
})
|
||
|
||
const resultRows = computed(() => {
|
||
const r = result.value
|
||
if (!r) return []
|
||
return [
|
||
{ label: '净金重', value: r.netGoldWeight.toFixed(4) + ' g' },
|
||
{ label: '含耗重', value: r.weightWithLoss.toFixed(4) + ' g' },
|
||
{ label: '金值', value: '¥' + r.goldValue.toFixed(2) },
|
||
{ label: '主石总价', value: '¥' + r.mainStoneTotal.toFixed(2) },
|
||
{ label: '副石总价', value: '¥' + r.sideStoneTotal.toFixed(2) },
|
||
{ label: '微镶总价', value: '¥' + r.microSettingTotal.toFixed(2) },
|
||
{ label: '总价', value: '¥' + r.totalPrice.toFixed(2) },
|
||
]
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.custom-navbar {
|
||
background: linear-gradient(to right, #FFCFDE, #FFA6C4);
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 100;
|
||
}
|
||
.custom-navbar__content {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 24rpx;
|
||
}
|
||
.custom-navbar__back {
|
||
width: 44rpx;
|
||
height: 44rpx;
|
||
}
|
||
.custom-navbar__title {
|
||
flex: 1;
|
||
text-align: center;
|
||
font-size: 34rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
.custom-navbar__placeholder {
|
||
width: 44rpx;
|
||
}
|
||
.calculator-page {
|
||
min-height: 100vh;
|
||
background: #f5f5f5;
|
||
padding: 16rpx;
|
||
}
|
||
.section {
|
||
background: #fff;
|
||
border-radius: 16rpx;
|
||
padding: 24rpx;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
.section__title {
|
||
font-size: 30rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
.form-item {
|
||
margin-bottom: 20rpx;
|
||
}
|
||
.form-item__label {
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
display: block;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
.form-item__input {
|
||
border: 1rpx solid #ddd;
|
||
border-radius: 8rpx;
|
||
padding: 16rpx;
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
}
|
||
.form-item__error {
|
||
font-size: 22rpx;
|
||
color: #e4393c;
|
||
margin-top: 4rpx;
|
||
display: block;
|
||
}
|
||
.result-section {
|
||
background: #fff8f0;
|
||
}
|
||
.result-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 12rpx 0;
|
||
border-bottom: 1rpx solid #f0e8e0;
|
||
}
|
||
.result-row:last-child {
|
||
border-bottom: none;
|
||
font-weight: bold;
|
||
font-size: 32rpx;
|
||
color: #e4393c;
|
||
}
|
||
.result-row__label {
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
}
|
||
.result-row__value {
|
||
font-size: 26rpx;
|
||
color: #333;
|
||
}
|
||
</style>
|