feat(pl): driver public feedback by product and brand qty
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
0667e8d77b
commit
2a215b2e1f
4 files changed
+436
-72
No files matched your search
@@ -15,7 +15,16 @@ from app.core.config import settings
|
||||
from app.core.database import get_db
|
||||
from app.core.response import error_response, success_response
|
||||
from app.crud import oss as oss_svc
|
||||
from app.models.pl_models import PlPlan, PlPlanProduct, PlPlanTransport, PlTransport
|
||||
from app.models.crm_models import CrmProductBrand
|
||||
from app.models.pl_models import (
|
||||
PlPlan,
|
||||
PlPlanProduct,
|
||||
PlPlanTransport,
|
||||
PlTransport,
|
||||
PlTransportProductBrandQty,
|
||||
PlTransportProductFeedback,
|
||||
)
|
||||
from app.schemas.pl.io_order import BrandOption, BrandQtyItem
|
||||
from app.schemas.pl.resource_snapshot import parse_resource_snapshot
|
||||
from app.schemas.pl.transport import (
|
||||
PublicTransportFeedback,
|
||||
@@ -23,6 +32,7 @@ from app.schemas.pl.transport import (
|
||||
PublicTransportPersonnelRead,
|
||||
PublicTransportPlanProductRead,
|
||||
)
|
||||
from app.services.pl_feedback_brand_qty import persist_transport_product_feedback
|
||||
from app.utils.excel_export import load_dict_label_maps
|
||||
|
||||
router = APIRouter(prefix="/public/pl/transport", tags=["运输计划-司机公共"])
|
||||
@@ -128,19 +138,86 @@ def _build_public_page(db: Session, transport: PlTransport) -> PublicTransportPa
|
||||
.all()
|
||||
)
|
||||
|
||||
crm_product_ids = {(p.product_id or "").strip() for p in products if (p.product_id or "").strip()}
|
||||
brand_options_by_product: dict[str, list[BrandOption]] = {}
|
||||
if crm_product_ids:
|
||||
brand_rows = (
|
||||
db.query(CrmProductBrand)
|
||||
.filter(
|
||||
CrmProductBrand.product_id.in_(list(crm_product_ids)),
|
||||
CrmProductBrand.tenant_no == transport.tenant_no,
|
||||
CrmProductBrand.status == "on",
|
||||
CrmProductBrand.del_flag == 0,
|
||||
)
|
||||
.order_by(CrmProductBrand.order_num.asc(), CrmProductBrand.create_time.asc())
|
||||
.all()
|
||||
)
|
||||
for row in brand_rows:
|
||||
brand_options_by_product.setdefault(row.product_id, []).append(
|
||||
BrandOption(brand_id=row.id, brand_name=row.brand_name or "")
|
||||
)
|
||||
|
||||
feedback_by_plan_product: dict[str, PlTransportProductFeedback] = {}
|
||||
brand_qtys_by_plan_product: dict[str, list[BrandQtyItem]] = {}
|
||||
feedback_rows = (
|
||||
db.query(PlTransportProductFeedback)
|
||||
.filter(
|
||||
PlTransportProductFeedback.transport_id == transport.id,
|
||||
PlTransportProductFeedback.tenant_no == transport.tenant_no,
|
||||
PlTransportProductFeedback.del_flag == 0,
|
||||
)
|
||||
.order_by(PlTransportProductFeedback.order_num.asc())
|
||||
.all()
|
||||
)
|
||||
for fb in feedback_rows:
|
||||
feedback_by_plan_product[fb.plan_product_id] = fb
|
||||
|
||||
if feedback_rows:
|
||||
feedback_ids = [fb.id for fb in feedback_rows]
|
||||
brand_qty_rows = (
|
||||
db.query(PlTransportProductBrandQty)
|
||||
.filter(
|
||||
PlTransportProductBrandQty.transport_id == transport.id,
|
||||
PlTransportProductBrandQty.tenant_no == transport.tenant_no,
|
||||
PlTransportProductBrandQty.del_flag == 0,
|
||||
PlTransportProductBrandQty.feedback_id.in_(feedback_ids),
|
||||
)
|
||||
.order_by(PlTransportProductBrandQty.order_num.asc())
|
||||
.all()
|
||||
)
|
||||
for row in brand_qty_rows:
|
||||
brand_qtys_by_plan_product.setdefault(row.plan_product_id, []).append(
|
||||
BrandQtyItem(
|
||||
brand_id=row.brand_id or "",
|
||||
brand_name=row.brand_name,
|
||||
qty=row.qty or 0,
|
||||
)
|
||||
)
|
||||
|
||||
product_reads = []
|
||||
unit_map = load_dict_label_maps(
|
||||
db, [CRM_PRODUCT_UNIT], tenant_no=transport.tenant_no
|
||||
).get(CRM_PRODUCT_UNIT, {})
|
||||
for prod in products:
|
||||
product_id = (prod.product_id or "").strip()
|
||||
options = brand_options_by_product.get(product_id) or None
|
||||
fb = feedback_by_plan_product.get(prod.id)
|
||||
brand_qtys = brand_qtys_by_plan_product.get(prod.id) or None
|
||||
product_reads.append(
|
||||
PublicTransportPlanProductRead(
|
||||
plan_product_id=prod.id,
|
||||
product_name=prod.product_name or "",
|
||||
model=prod.model,
|
||||
package_spec=prod.package_spec,
|
||||
unit=_resolve_unit_label(unit_map, prod.unit),
|
||||
quantity=prod.quantity or prod.inbound_estimate_qty or prod.outbound_estimate_qty,
|
||||
remark=(prod.remark or "").strip() or None,
|
||||
brand_mode=prod.brand_mode,
|
||||
brand_id=prod.brand_id,
|
||||
brand_name=prod.brand_name,
|
||||
brand_options=options,
|
||||
actual_qty=fb.actual_qty if fb else None,
|
||||
brand_qtys=brand_qtys,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -203,7 +280,39 @@ def save_public_feedback(
|
||||
if payload.feedback_remark is not None:
|
||||
transport.feedback_remark = payload.feedback_remark
|
||||
|
||||
if payload.feedback_qty is not None:
|
||||
if payload.products:
|
||||
link = (
|
||||
db.query(PlPlanTransport)
|
||||
.filter(
|
||||
PlPlanTransport.transport_id == transport.id,
|
||||
PlPlanTransport.del_flag == 0,
|
||||
)
|
||||
.order_by(PlPlanTransport.create_time.desc())
|
||||
.first()
|
||||
)
|
||||
if not link or not link.plan_id:
|
||||
return error_response(400, "未找到关联计划,无法保存产品回传")
|
||||
products_payload = [
|
||||
{
|
||||
"planProductId": item.plan_product_id,
|
||||
"actualQty": item.actual_qty,
|
||||
"brandQtys": [b.model_dump(by_alias=True) for b in (item.brand_qtys or [])],
|
||||
}
|
||||
for item in payload.products
|
||||
]
|
||||
try:
|
||||
persist_transport_product_feedback(
|
||||
db,
|
||||
tenant_no=transport.tenant_no,
|
||||
transport=transport,
|
||||
plan_id=link.plan_id,
|
||||
products=products_payload,
|
||||
user_id=None,
|
||||
enforce_brand=True,
|
||||
)
|
||||
except ValueError as exc:
|
||||
return error_response(400, str(exc))
|
||||
elif payload.feedback_qty is not None:
|
||||
transport.feedback_qty = payload.feedback_qty
|
||||
|
||||
transport.feedback_time = datetime.now()
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.schemas.pl.datetime_parse import OptionalPlanDateTime
|
||||
from app.schemas.pl.io_order import BrandOption, BrandQtyItem
|
||||
from app.schemas.pl.resource_snapshot import PlanResourceSnapshotMixin
|
||||
|
||||
|
||||
@@ -188,20 +189,35 @@ class TransportFeedback(BaseModel):
|
||||
model_config = {"populate_by_name": True}
|
||||
|
||||
|
||||
class PublicTransportProductFeedbackItem(BaseModel):
|
||||
plan_product_id: str = Field(..., alias="planProductId")
|
||||
actual_qty: Decimal = Field(..., alias="actualQty", ge=0)
|
||||
brand_qtys: Optional[list[BrandQtyItem]] = Field(default=None, alias="brandQtys")
|
||||
model_config = {"populate_by_name": True}
|
||||
|
||||
|
||||
class PublicTransportFeedback(BaseModel):
|
||||
feedback_images: Optional[list[str]] = Field(default=None, alias="feedbackImages")
|
||||
feedback_remark: Optional[str] = Field(default=None, alias="feedbackRemark")
|
||||
feedback_qty: Optional[Decimal] = Field(default=None, alias="feedbackQty", ge=0, description="回传数量")
|
||||
products: Optional[list[PublicTransportProductFeedbackItem]] = None
|
||||
model_config = {"populate_by_name": True}
|
||||
|
||||
|
||||
class PublicTransportPlanProductRead(BaseModel):
|
||||
plan_product_id: str = Field(alias="planProductId")
|
||||
product_name: str = Field(alias="productName")
|
||||
model: Optional[str] = None
|
||||
package_spec: Optional[str] = Field(default=None, alias="packageSpec")
|
||||
unit: Optional[str] = None
|
||||
quantity: Optional[Decimal] = None
|
||||
remark: Optional[str] = Field(default=None, alias="remark", description="运输计划产品备注")
|
||||
brand_mode: Optional[str] = Field(default=None, alias="brandMode")
|
||||
brand_id: Optional[str] = Field(default=None, alias="brandId")
|
||||
brand_name: Optional[str] = Field(default=None, alias="brandName")
|
||||
brand_options: Optional[list[BrandOption]] = Field(default=None, alias="brandOptions")
|
||||
actual_qty: Optional[Decimal] = Field(default=None, alias="actualQty")
|
||||
brand_qtys: Optional[list[BrandQtyItem]] = Field(default=None, alias="brandQtys")
|
||||
model_config = {"populate_by_name": True}
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
import { publicPlGet, publicPlPost, publicPlUpload } from '@/utils/http/public-pl'
|
||||
|
||||
export interface BrandQtyItem {
|
||||
brandId: string
|
||||
brandName?: string
|
||||
qty: number
|
||||
}
|
||||
|
||||
export interface PublicTransportProduct {
|
||||
planProductId: string
|
||||
productName: string
|
||||
model?: string
|
||||
packageSpec?: string
|
||||
unit?: string
|
||||
quantity?: number
|
||||
remark?: string
|
||||
brandMode?: string | null
|
||||
brandId?: string | null
|
||||
brandName?: string | null
|
||||
brandOptions?: { brandId: string; brandName: string }[]
|
||||
actualQty?: number | null
|
||||
brandQtys?: BrandQtyItem[]
|
||||
}
|
||||
|
||||
export interface PublicTransportPersonnel {
|
||||
@@ -51,7 +64,11 @@ export const publicTransportApi = {
|
||||
getPage: (token: string) => publicPlGet<PublicTransportPage>(`/public/pl/transport/${token}`),
|
||||
saveFeedback: (
|
||||
token: string,
|
||||
data: { feedbackImages?: string[]; feedbackRemark?: string; feedbackQty?: number | null }
|
||||
data: {
|
||||
feedbackImages?: string[]
|
||||
feedbackRemark?: string
|
||||
products: { planProductId: string; actualQty: number; brandQtys?: BrandQtyItem[] }[]
|
||||
}
|
||||
) => publicPlPost<{ success: boolean }>(`/public/pl/transport/${token}/feedback`, data),
|
||||
uploadAttachment: (token: string, file: File) =>
|
||||
publicPlUpload<PublicUploadResult>(`/public/pl/transport/${token}/attachments`, file)
|
||||
|
||||
@@ -80,32 +80,77 @@
|
||||
/>
|
||||
</van-cell-group>
|
||||
|
||||
<van-cell-group v-if="page.products.length" inset class="section-card" title="产品明细">
|
||||
<van-cell
|
||||
v-for="(item, idx) in page.products"
|
||||
:key="idx"
|
||||
:title="formatPublicTransportProductLine(item)"
|
||||
class="product-cell"
|
||||
/>
|
||||
</van-cell-group>
|
||||
|
||||
<van-cell-group inset class="section-card" title="回传数量">
|
||||
<div class="actual-box">
|
||||
<div class="actual-box__label">
|
||||
<strong>实际数量</strong>
|
||||
<span>请按实装填写,参考现场回执</span>
|
||||
<van-cell-group
|
||||
v-if="editableProducts.length"
|
||||
inset
|
||||
class="section-card"
|
||||
title="产品回传数量"
|
||||
>
|
||||
<div
|
||||
v-for="(item, idx) in editableProducts"
|
||||
:key="item.planProductId"
|
||||
class="product-card"
|
||||
>
|
||||
<div class="product-card__head">
|
||||
<div class="product-card__name">{{ item.productName || '产品' }}</div>
|
||||
<span class="product-card__badge">
|
||||
产品 {{ idx + 1 }}/{{ editableProducts.length }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="actual-box__row">
|
||||
<van-field
|
||||
v-model="feedbackQtyText"
|
||||
type="number"
|
||||
class="actual-box__field"
|
||||
placeholder="请输入"
|
||||
:border="false"
|
||||
<div class="meta-grid">
|
||||
<div class="meta-item">
|
||||
<div class="meta-item__label">规格型号</div>
|
||||
<div class="meta-item__value">{{ item.model || '—' }}</div>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<div class="meta-item__label">包装规格</div>
|
||||
<div class="meta-item__value">{{ item.packageSpec || '—' }}</div>
|
||||
</div>
|
||||
<div v-if="item.remark" class="meta-item meta-item--span">
|
||||
<div class="meta-item__label">备注</div>
|
||||
<div class="meta-item__value">{{ item.remark }}</div>
|
||||
</div>
|
||||
<div class="meta-item meta-item--span">
|
||||
<div class="meta-item__label">计划数量</div>
|
||||
<div class="meta-item__value meta-item__value--plan">
|
||||
{{ formatPlanQty(item) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actual-box">
|
||||
<div class="actual-box__label">
|
||||
<strong>实际数量</strong>
|
||||
<span v-if="item.brandMode">由品牌明细合计</span>
|
||||
<span v-else>请按实装填写,参考现场回执</span>
|
||||
</div>
|
||||
<div class="actual-box__row">
|
||||
<van-field
|
||||
v-model="item.actualQtyText"
|
||||
type="number"
|
||||
class="actual-box__field"
|
||||
placeholder="请输入"
|
||||
:border="false"
|
||||
:readonly="Boolean(item.brandMode)"
|
||||
/>
|
||||
<span class="actual-box__unit">{{ item.unit || '吨' }}</span>
|
||||
</div>
|
||||
<BrandQtyChipsField
|
||||
v-if="item.brandMode"
|
||||
v-model="item.brandQtyRows"
|
||||
:brand-mode="brandModeForField(item)"
|
||||
:required-brand-id="item.brandId"
|
||||
:brand-options="item.brandOptions || []"
|
||||
:unit="item.unit || '吨'"
|
||||
variant="mobile"
|
||||
:actual-qty="Number(item.actualQtyText || 0)"
|
||||
@update:actual-qty="(val) => syncItemActualQty(item, val)"
|
||||
/>
|
||||
<span class="actual-box__unit">{{ primaryQtyUnit }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="editableProducts.length > 1" class="total-box">
|
||||
<span class="total-box__label">合计回传</span>
|
||||
<span class="total-box__value">{{ totalFeedbackQty }}</span>
|
||||
</div>
|
||||
</van-cell-group>
|
||||
|
||||
<van-cell-group inset class="section-card" title="回执上传">
|
||||
@@ -152,16 +197,24 @@
|
||||
import { showFailToast, showSuccessToast } from 'vant'
|
||||
import {
|
||||
publicTransportApi,
|
||||
type BrandQtyItem,
|
||||
type PublicTransportPage,
|
||||
type PublicTransportPersonnel
|
||||
type PublicTransportPersonnel,
|
||||
type PublicTransportProduct
|
||||
} from '@/api/pl/public-transport'
|
||||
import BrandQtyChipsField from '@/components/business/brand-qty-chips-field/index.vue'
|
||||
import PublicMobileShell from '@/layouts/PublicMobileShell.vue'
|
||||
import { buildAmapNavigationUrl } from '@/utils/amap/navigation'
|
||||
import { formatPublicTransportProductLine, formatPlPersonnelNameWithRole } from '@/utils/pl-display'
|
||||
import { formatPlPersonnelNameWithRole } from '@/utils/pl-display'
|
||||
|
||||
import 'vant/es/toast/style'
|
||||
import '@/styles/public-mobile-vant.scss'
|
||||
|
||||
type EditableTransportProduct = PublicTransportProduct & {
|
||||
actualQtyText: string
|
||||
brandQtyRows: BrandQtyItem[]
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const token = computed(() => String(route.params.token || ''))
|
||||
|
||||
@@ -170,13 +223,17 @@
|
||||
const errorMessage = ref('')
|
||||
const page = ref<PublicTransportPage | null>(null)
|
||||
const feedbackRemark = ref('')
|
||||
const feedbackQtyText = ref('')
|
||||
const feedbackImages = ref<string[]>([])
|
||||
const uploaderFiles = ref<UploaderFileListItem[]>([])
|
||||
const editableProducts = ref<EditableTransportProduct[]>([])
|
||||
|
||||
const primaryQtyUnit = computed(() => {
|
||||
const unit = page.value?.products?.find((p) => p.unit)?.unit
|
||||
return unit || '吨'
|
||||
const totalFeedbackQty = computed(() => {
|
||||
const sum = editableProducts.value.reduce(
|
||||
(acc, item) => acc + Number(item.actualQtyText || 0),
|
||||
0
|
||||
)
|
||||
const unit = editableProducts.value.find((p) => p.unit)?.unit || '吨'
|
||||
return `${Number(sum.toFixed(3))} ${unit}`
|
||||
})
|
||||
|
||||
function formatPersonnelLine(person: PublicTransportPersonnel) {
|
||||
@@ -226,6 +283,13 @@
|
||||
return normalized.length > 16 ? normalized.slice(0, 16) : normalized
|
||||
}
|
||||
|
||||
function formatPlanQty(item: PublicTransportProduct) {
|
||||
const qty = item.quantity
|
||||
const unit = item.unit || ''
|
||||
if (qty == null) return '—'
|
||||
return unit ? `${qty} ${unit}` : String(qty)
|
||||
}
|
||||
|
||||
function resolveFileUrl(url: string) {
|
||||
if (!url) return ''
|
||||
if (/^https?:\/\//i.test(url)) return url
|
||||
@@ -242,6 +306,56 @@
|
||||
})) as UploaderFileListItem[]
|
||||
}
|
||||
|
||||
function mapBrandQtyRows(prod: PublicTransportProduct): BrandQtyItem[] {
|
||||
const existing = (prod.brandQtys || []).map((row) => ({
|
||||
brandId: row.brandId,
|
||||
brandName: row.brandName || '',
|
||||
qty: Number(row.qty ?? 0)
|
||||
}))
|
||||
if (existing.length) return existing
|
||||
|
||||
if (prod.brandMode === 'required') {
|
||||
const brandId = prod.brandId || prod.brandOptions?.[0]?.brandId || ''
|
||||
const brandName =
|
||||
prod.brandName ||
|
||||
prod.brandOptions?.find((o) => o.brandId === brandId)?.brandName ||
|
||||
'指定品牌'
|
||||
const seedQty = prod.actualQty != null ? Number(prod.actualQty) : 0
|
||||
return brandId ? [{ brandId, brandName, qty: seedQty }] : []
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
function brandModeForField(item: EditableTransportProduct): 'any' | 'required' | null {
|
||||
if (item.brandMode === 'any' || item.brandMode === 'required') return item.brandMode
|
||||
return null
|
||||
}
|
||||
|
||||
function syncItemActualQty(item: EditableTransportProduct, val: number) {
|
||||
item.actualQtyText = String(val)
|
||||
}
|
||||
|
||||
function brandQtySum(rows: BrandQtyItem[]) {
|
||||
return rows.reduce((sum, row) => sum + Number(row.qty || 0), 0)
|
||||
}
|
||||
|
||||
function validateBrandedProducts(): string | null {
|
||||
for (const item of editableProducts.value) {
|
||||
if (!item.brandMode) continue
|
||||
const positiveRows = (item.brandQtyRows || []).filter((r) => Number(r.qty) > 0)
|
||||
if (!positiveRows.length) {
|
||||
return `「${item.productName || '产品'}」请至少填写一个品牌数量`
|
||||
}
|
||||
const total = Number(brandQtySum(item.brandQtyRows).toFixed(3))
|
||||
const actual = Number(item.actualQtyText || 0)
|
||||
if (Math.abs(total - actual) >= 0.0005) {
|
||||
return `「${item.productName || '产品'}」品牌数量合计须等于实际数量`
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async function loadPage() {
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
@@ -249,9 +363,20 @@
|
||||
const data = await publicTransportApi.getPage(token.value)
|
||||
page.value = data
|
||||
feedbackRemark.value = data.feedbackRemark || ''
|
||||
feedbackQtyText.value =
|
||||
data.feedbackQty != null && data.feedbackQty !== undefined ? String(data.feedbackQty) : ''
|
||||
feedbackImages.value = [...(data.feedbackImages || [])]
|
||||
editableProducts.value = (data.products || []).map((prod) => {
|
||||
const brandQtyRows = mapBrandQtyRows(prod)
|
||||
return {
|
||||
...prod,
|
||||
brandOptions: prod.brandOptions || [],
|
||||
brandQtyRows,
|
||||
actualQtyText: prod.brandMode
|
||||
? String(Number(brandQtySum(brandQtyRows).toFixed(3)))
|
||||
: prod.actualQty != null
|
||||
? String(prod.actualQty)
|
||||
: ''
|
||||
}
|
||||
})
|
||||
syncUploaderFromImages()
|
||||
} catch (e: any) {
|
||||
errorMessage.value = e?.message || '链接无效或已失效'
|
||||
@@ -290,22 +415,35 @@
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
const qtyRaw = feedbackQtyText.value.trim()
|
||||
let feedbackQty: number | null = null
|
||||
if (qtyRaw !== '') {
|
||||
const parsed = Number(qtyRaw)
|
||||
if (!Number.isFinite(parsed) || parsed < 0) {
|
||||
showFailToast('请输入有效的回传数量')
|
||||
return
|
||||
}
|
||||
feedbackQty = parsed
|
||||
const brandErr = validateBrandedProducts()
|
||||
if (brandErr) {
|
||||
showFailToast(brandErr)
|
||||
return
|
||||
}
|
||||
|
||||
const products = editableProducts.value.map((item) => {
|
||||
const actualQty = Number(item.actualQtyText || 0)
|
||||
return {
|
||||
planProductId: item.planProductId,
|
||||
actualQty,
|
||||
brandQtys: item.brandMode
|
||||
? (item.brandQtyRows || [])
|
||||
.filter((row) => Number(row.qty) > 0)
|
||||
.map((row) => ({
|
||||
brandId: row.brandId,
|
||||
brandName: row.brandName,
|
||||
qty: Number(row.qty)
|
||||
}))
|
||||
: undefined
|
||||
}
|
||||
})
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
await publicTransportApi.saveFeedback(token.value, {
|
||||
feedbackRemark: feedbackRemark.value,
|
||||
feedbackImages: feedbackImages.value,
|
||||
feedbackQty
|
||||
products
|
||||
})
|
||||
showSuccessToast('保存成功')
|
||||
await loadPage()
|
||||
@@ -388,68 +526,152 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.product-cell :deep(.van-cell__title) {
|
||||
color: #1e293b;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.product-cell :deep(.van-cell__value) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.actual-box {
|
||||
margin: 4px 12px 12px;
|
||||
padding: 12px 14px;
|
||||
.product-card {
|
||||
margin: 10px 12px;
|
||||
padding: 14px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(180deg, #f0fdf4 0%, #fff 48%);
|
||||
border: 1px solid #d1fae5;
|
||||
}
|
||||
|
||||
.actual-box__label {
|
||||
.product-card__head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.actual-box__label strong {
|
||||
font-size: 14px;
|
||||
.product-card__name {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.actual-box__label span {
|
||||
.product-card__badge {
|
||||
flex-shrink: 0;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: #d1fae5;
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
background: rgb(255 255 255 / 85%);
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.meta-item--span {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.meta-item__label {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.meta-item__value {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.meta-item__value--plan {
|
||||
color: #059669;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.actual-box {
|
||||
background: #fff;
|
||||
border: 1px solid #a7f3d0;
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.actual-box__label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
|
||||
strong {
|
||||
color: #1e293b;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.actual-box__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
border: 1px solid #a7f3d0;
|
||||
}
|
||||
|
||||
.actual-box__field {
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
}
|
||||
background: #ecfdf5;
|
||||
border: 1.5px solid #6ee7b7;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
.actual-box__field :deep(.van-field__control) {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #059669;
|
||||
:deep(.van-field__control) {
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #059669;
|
||||
line-height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.actual-box__unit {
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #059669;
|
||||
min-width: 28px;
|
||||
}
|
||||
|
||||
.total-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 0 12px 12px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.total-box__label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.total-box__value {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.page-footer {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user