1514 lines
49 KiB
Vue
1514 lines
49 KiB
Vue
<template>
|
||
<section class="page">
|
||
<div class="page-hero">
|
||
<div>
|
||
<span class="eyebrow">录单中心</span>
|
||
<h2>{{ isEditMode ? '编辑订单' : '新建订单' }}</h2>
|
||
<p class="tips">填写客户要求和关键信息,快速创建订单。</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="message" class="message-box" :class="{ error: messageType === 'error' }">
|
||
{{ message }}
|
||
</div>
|
||
|
||
<!-- 智能填单 -->
|
||
<section v-if="!parsedResult" class="form-section parse-card">
|
||
<div class="section-header">
|
||
<h3>智能填单</h3>
|
||
<span v-if="isMockMode" class="mode-tag fallback">演示数据</span>
|
||
</div>
|
||
<div class="parse-text-area">
|
||
<textarea v-model="parseInput" rows="5" placeholder="请粘贴订单相关文本(微信聊天记录、电话记录等)"></textarea>
|
||
<div class="parse-actions">
|
||
<button type="button" class="primary-btn" :disabled="parseLoading || !parseInput.trim()" @click="handleParseSubmit">
|
||
{{ parseLoading ? '解析中...' : '智能解析' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 解析结果预览 -->
|
||
<section v-if="parsedResult" class="form-section parse-result-card">
|
||
<div class="section-header">
|
||
<h3>智能填单 - 解析结果</h3>
|
||
<div class="parse-result-meta">
|
||
<span v-if="isMockMode" class="mode-tag fallback">演示数据</span>
|
||
<span class="confidence-tag">置信度 {{ Math.round(parsedResult.confidence * 100) }}%</span>
|
||
</div>
|
||
</div>
|
||
<div class="parse-result-fields">
|
||
<label>
|
||
<span>客户姓名</span>
|
||
<input v-model="parseEditable.customer_name" type="text" />
|
||
</label>
|
||
<label>
|
||
<span>客户手机号</span>
|
||
<input v-model="parseEditable.customer_mobile" type="text" />
|
||
</label>
|
||
<label>
|
||
<span>客户地址</span>
|
||
<input v-model="parseEditable.customer_address" type="text" />
|
||
</label>
|
||
<label>
|
||
<span>备注</span>
|
||
<input v-model="parseEditable.remark" type="text" placeholder="可选" />
|
||
</label>
|
||
</div>
|
||
<div v-if="parseEditable.items.length" class="parse-result-items">
|
||
<div class="items-header" @click="itemsExpanded = !itemsExpanded">
|
||
<h4>产品明细 ({{ parseEditable.items.length }}项)</h4>
|
||
<span class="expand-icon">{{ itemsExpanded ? '▼' : '▶' }}</span>
|
||
</div>
|
||
<div v-show="itemsExpanded">
|
||
<div v-for="(item, idx) in parseEditable.items" :key="idx" class="parse-item-card">
|
||
<div class="parse-item-main">
|
||
<strong>{{ item.product_name || '未识别产品' }}</strong>
|
||
<span>{{ item.specification || '-' }} / {{ item.unit || '-' }} / {{ item.quantity || 0 }} / 单价 {{ item.sale_price || 0 }}</span>
|
||
<span v-if="item.demand_specification" class="demand-spec-tag">需求规格: {{ item.demand_specification }}</span>
|
||
<p v-if="item._calcHint" class="calc-hint">{{ item._calcHint }}</p>
|
||
</div>
|
||
<div class="parse-item-select">
|
||
<span class="select-label">手动选择产品:</span>
|
||
<div class="product-search-box parse-product-search">
|
||
<input
|
||
v-model="item._parseSearchText"
|
||
type="text"
|
||
placeholder="搜索产品名称/规格..."
|
||
@input="handleParseProductSearch(item)"
|
||
@focus="item._parseShowDropdown = true"
|
||
@blur="hideParseProductDropdown(item)"
|
||
/>
|
||
<div v-if="item._parseShowDropdown && item._parseFilteredProducts && item._parseFilteredProducts.length" class="product-dropdown">
|
||
<div
|
||
v-for="opt in item._parseFilteredProducts"
|
||
:key="opt.value"
|
||
class="product-dropdown-item"
|
||
@mousedown.prevent="handleParseProductSelect(idx, String(opt.value))"
|
||
>
|
||
{{ opt.label }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-if="parsedResult.product_matches?.[idx]?.candidates?.length" class="parse-item-candidates">
|
||
<span class="candidate-label">匹配建议:</span>
|
||
<button
|
||
v-for="(c, ci) in parsedResult.product_matches[idx].candidates"
|
||
:key="ci"
|
||
type="button"
|
||
class="candidate-btn"
|
||
:class="{ selected: item._selectedCandidate?.product_id === c.product_id }"
|
||
@click="handleProductCandidateSelect(idx, c)"
|
||
>
|
||
{{ c.product_name }} / {{ c.specification }} ({{ Math.round(c.match_score * 100) }}%)
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-if="parsedResult.warnings?.length" class="parse-warnings">
|
||
<p v-for="(w, wi) in parsedResult.warnings" :key="wi">{{ w }}</p>
|
||
</div>
|
||
<div class="parse-actions">
|
||
<button type="button" class="ghost-btn" @click="handleResetParse">重新解析</button>
|
||
<button type="button" class="primary-btn" @click="handleConfirmParse">确认填入表单</button>
|
||
</div>
|
||
</section>
|
||
|
||
<form class="form-grid" @submit.prevent="handleSubmit">
|
||
<!-- 客户信息 -->
|
||
<section class="form-section">
|
||
<div class="section-header">
|
||
<h3>客户信息</h3>
|
||
<span>选择或输入客户信息</span>
|
||
</div>
|
||
<div class="section-grid">
|
||
<label>
|
||
<span>选择客户</span>
|
||
<select v-model="selectedCustomerId" @change="handleCustomerChange">
|
||
<option value="">请选择客户</option>
|
||
<option v-for="option in customerOptions" :key="option.value" :value="String(option.value)">
|
||
{{ option.label }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
<label>
|
||
<span>客户姓名</span>
|
||
<input v-model.trim="form.customer_name" type="text" placeholder="请输入客户姓名" @blur="handleCustomerInputBlur" />
|
||
</label>
|
||
<label>
|
||
<span>客户手机号</span>
|
||
<input v-model.trim="form.customer_mobile" type="text" placeholder="请输入客户手机号" @blur="handleCustomerInputBlur" />
|
||
</label>
|
||
<label>
|
||
<span>收货地址</span>
|
||
<input v-model.trim="form.customer_address" type="text" placeholder="请输入收货地址" />
|
||
</label>
|
||
</div>
|
||
<p v-if="customerHint" class="hint-line">{{ customerHint }}</p>
|
||
</section>
|
||
|
||
<!-- 客户要求与产品 -->
|
||
<section class="form-section full-width">
|
||
<div class="section-header">
|
||
<h3>客户要求</h3>
|
||
<div class="section-actions">
|
||
<span>输入客户需求,支持手动选择产品</span>
|
||
<button type="button" class="ghost-btn small-btn" @click="addItemRow">添加产品</button>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="full-width-label">
|
||
<span>客户要求描述</span>
|
||
<textarea v-model.trim="form.customer_demand" rows="3" placeholder="请输入客户要求,如产品名称、规格、数量等"></textarea>
|
||
</label>
|
||
</div>
|
||
<div class="items-toggle" @click="formItemsExpanded = !formItemsExpanded">
|
||
<span>产品明细 ({{ items.length }}项)</span>
|
||
<span class="expand-icon">{{ formItemsExpanded ? '▼' : '▶' }}</span>
|
||
</div>
|
||
<p class="hint-text-cost">此处属于成本核算区域,需要根据实际的货物选择对应的规格</p>
|
||
<div v-show="formItemsExpanded">
|
||
<div v-for="(row, index) in items" :key="row.rowKey" class="item-card">
|
||
<div class="item-card-header">
|
||
<h4>产品 {{ index + 1 }}</h4>
|
||
<button v-if="items.length > 1" type="button" class="link-btn danger-link" @click="removeItemRow(index)">删除</button>
|
||
</div>
|
||
<div class="section-grid">
|
||
<label class="product-search-label">
|
||
<span>选择产品</span>
|
||
<div class="product-search-box">
|
||
<input
|
||
v-model="row._searchText"
|
||
type="text"
|
||
placeholder="搜索产品名称/规格..."
|
||
@input="handleProductSearch(row)"
|
||
@focus="row._showDropdown = true"
|
||
@blur="hideProductDropdown(row)"
|
||
/>
|
||
<div v-if="row._showDropdown && row._filteredProducts && row._filteredProducts.length" class="product-dropdown">
|
||
<div
|
||
v-for="opt in row._filteredProducts"
|
||
:key="opt.value"
|
||
class="product-dropdown-item"
|
||
@mousedown.prevent="selectProduct(row, opt)"
|
||
>
|
||
{{ opt.label }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</label>
|
||
<label>
|
||
<span>产品名称</span>
|
||
<input v-model.trim="row.product_name" type="text" placeholder="请输入产品名称" />
|
||
</label>
|
||
<label>
|
||
<span>规格</span>
|
||
<input v-model.trim="row.specification" type="text" placeholder="请输入规格" />
|
||
<p v-if="row._calcHint" class="calc-hint">{{ row._calcHint }}</p>
|
||
</label>
|
||
<label>
|
||
<span>需求规格</span>
|
||
<input v-model.trim="row.demand_specification" type="text" placeholder="如: 25cm×25cm" @input="calcDemandResult(row)" />
|
||
</label>
|
||
<label v-if="row._demandResult">
|
||
<span>计算结果</span>
|
||
<div class="calc-result">{{ row._demandResult }}</div>
|
||
<p v-if="row._demandCalcHint" class="calc-hint">{{ row._demandCalcHint }}</p>
|
||
</label>
|
||
<label v-for="field in row._extraInputs" :key="field.key">
|
||
<span>{{ field.label || field.key }}</span>
|
||
<select v-model="row[field.key]">
|
||
<option value="">{{ field.label || field.key }}</option>
|
||
<option v-for="opt in field.options" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
|
||
</select>
|
||
</label>
|
||
<label>
|
||
<span>单位</span>
|
||
<input v-model.trim="row.unit" type="text" placeholder="例如:吨" />
|
||
</label>
|
||
<label>
|
||
<span>数量</span>
|
||
<input v-model.number="row.quantity" type="number" min="0.01" step="0.01" @input="calcDemandResult(row)" />
|
||
</label>
|
||
<!-- 特殊计价字段 -->
|
||
<label>
|
||
<span>长度(米)</span>
|
||
<input v-model.number="row.length_m" type="number" min="0" step="0.01" placeholder="选填" />
|
||
</label>
|
||
<label>
|
||
<span>宽度(米)</span>
|
||
<input v-model.number="row.width_m" type="number" min="0" step="0.01" placeholder="选填" />
|
||
</label>
|
||
<label>
|
||
<span>克重(g)</span>
|
||
<input v-model.number="row.weight_g" type="number" min="0" step="1" placeholder="选填" />
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 合同金额 -->
|
||
<section class="form-section">
|
||
<div class="section-header">
|
||
<h3>合同金额</h3>
|
||
<span>填写订单金额与收款信息</span>
|
||
</div>
|
||
<div class="section-grid">
|
||
<label>
|
||
<span>合同金额</span>
|
||
<input v-model.number="form.contract_amount" type="number" min="0" step="0.01" placeholder="请输入合同金额" />
|
||
</label>
|
||
<label>
|
||
<span>收款渠道</span>
|
||
<div class="payment-method-input">
|
||
<select v-model="form.payment_method" @change="handlePaymentMethodChange">
|
||
<option value="">请选择收款渠道</option>
|
||
<option v-for="ch in paymentChannels" :key="ch.name" :value="ch.name">{{ ch.name }}</option>
|
||
<option value="其他">其他</option>
|
||
</select>
|
||
<input
|
||
v-if="form.payment_method === '其他'"
|
||
v-model.trim="form.payment_method_custom"
|
||
type="text"
|
||
placeholder="请输入自定义收款渠道"
|
||
/>
|
||
</div>
|
||
</label>
|
||
|
||
<label>
|
||
<span>订单类型</span>
|
||
<select v-model="form.order_type">
|
||
<option value="">请选择订单类型</option>
|
||
<option value="industry">工业订单</option>
|
||
<option value="daily">日用品订单</option>
|
||
</select>
|
||
</label>
|
||
<label>
|
||
<span>提成金额(可选)</span>
|
||
<input v-model.number="form.commission_amount" type="number" min="0" step="0.01" placeholder="留空表示无提成" />
|
||
</label>
|
||
<label class="checkline">
|
||
<input v-model="form.need_invoice" type="checkbox" />
|
||
<span>是否开发票</span>
|
||
</label>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 订单附件 -->
|
||
<section class="form-section full-width">
|
||
<div class="section-header">
|
||
<h3>订单附件</h3>
|
||
<span>上传合同、产品照片等</span>
|
||
</div>
|
||
<div class="attachment-area">
|
||
<label class="ghost-btn attachment-upload-label">
|
||
选择图片
|
||
<input type="file" accept="image/*" multiple style="display:none" @change="handleAttachmentChange" />
|
||
</label>
|
||
<p class="hint-line">支持 jpg/png/webp 格式,不限数量</p>
|
||
<div v-if="attachmentFiles.length" class="attachment-list">
|
||
<div v-for="(file, idx) in attachmentFiles" :key="file.rowKey" class="attachment-card">
|
||
<img :src="file.preview" class="attachment-thumb" />
|
||
<span class="attachment-name">{{ file.file.name }}</span>
|
||
<button type="button" class="link-btn danger-link" @click="removeAttachment(idx)">删除</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 物流信息 -->
|
||
<section class="form-section">
|
||
<div class="section-header">
|
||
<h3>物流信息</h3>
|
||
<span>如已填写快递单号,订单将跳过工厂下发环节</span>
|
||
</div>
|
||
<div class="section-grid">
|
||
<label class="checkline">
|
||
<input v-model="form.is_self_pickup" type="checkbox" @change="onSelfPickupChange" />
|
||
<span>客户自提(跳过物流)</span>
|
||
</label>
|
||
<label v-if="!form.is_self_pickup">
|
||
<span>快递单号(可选)</span>
|
||
<input v-model.trim="form.tracking_number" type="text" placeholder="" />
|
||
</label>
|
||
<label v-if="!form.is_self_pickup">
|
||
<span>快递公司(必选)</span>
|
||
<ExpressCompanyField v-model="form.express_company" />
|
||
</label>
|
||
</div>
|
||
</section>
|
||
</form>
|
||
|
||
<section class="actions">
|
||
<button type="button" class="secondary-btn" @click="resetForm">重置表单</button>
|
||
<button type="button" class="primary-btn" :disabled="submitting || optionLoading" @click="handleSubmit">
|
||
{{ submitting ? (isEditMode ? "更新中..." : "创建中...") : (isEditMode ? "更新订单" : "创建订单") }}
|
||
</button>
|
||
</section>
|
||
</section>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, onMounted, reactive, ref } from "vue";
|
||
import { useRoute, useRouter } from "vue-router";
|
||
|
||
import ExpressCompanyField from "../components/ExpressCompanyField.vue";
|
||
import {
|
||
createOrder,
|
||
fetchConfigItem,
|
||
fetchCustomerOptions,
|
||
fetchOrderForEdit,
|
||
fetchProductOptions,
|
||
localUploadFile,
|
||
parseOrderText,
|
||
saveAttachment,
|
||
updateOrder,
|
||
} from "../mockApi";
|
||
|
||
const router = useRouter();
|
||
const route = useRoute();
|
||
const submitting = ref(false);
|
||
const optionLoading = ref(true);
|
||
|
||
const message = ref("");
|
||
const messageType = ref("success");
|
||
const editingOrderId = ref(null);
|
||
const isEditMode = computed(() => Boolean(editingOrderId.value));
|
||
const customerOptions = ref([]);
|
||
const productOptions = ref([]);
|
||
const selectedCustomerId = ref("");
|
||
|
||
// 智能填单状态
|
||
const parseInput = ref("");
|
||
const parsedResult = ref(null);
|
||
const parseLoading = ref(false);
|
||
const itemsExpanded = ref(false);
|
||
const parseEditable = ref({
|
||
customer_name: "",
|
||
customer_mobile: "",
|
||
customer_address: "",
|
||
remark: "",
|
||
items: [],
|
||
});
|
||
|
||
function buildDefaultItem() {
|
||
return {
|
||
rowKey: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||
selectedProductId: "",
|
||
product_id: null,
|
||
product_name: "",
|
||
specification: "",
|
||
demand_specification: "",
|
||
_demandResult: "",
|
||
_demandCalcHint: "",
|
||
unit: "",
|
||
quantity: 1,
|
||
sale_price: 0,
|
||
cost_price: 0,
|
||
remark: "",
|
||
// 特殊计价字段
|
||
length_m: null,
|
||
width_m: null,
|
||
weight_g: null,
|
||
pricing_type: "",
|
||
price_tier: "",
|
||
_extraInputs: [],
|
||
_searchText: "",
|
||
_showDropdown: false,
|
||
_filteredProducts: [],
|
||
};
|
||
}
|
||
|
||
function buildDefaultForm() {
|
||
return {
|
||
customer_name: "",
|
||
customer_mobile: "",
|
||
customer_address: "",
|
||
customer_demand: "",
|
||
contract_amount: 0,
|
||
freight_total: 0,
|
||
rebate_total: 0,
|
||
tax_total: 0,
|
||
other_fee_total: 0,
|
||
commission_amount: 0,
|
||
tax_amount: 0,
|
||
need_invoice: false,
|
||
order_type: "",
|
||
is_self_pickup: false,
|
||
tracking_number: "",
|
||
express_company: "",
|
||
payment_method: "",
|
||
payment_method_custom: "",
|
||
auto_sync_customer: true,
|
||
remark: "",
|
||
};
|
||
}
|
||
|
||
const form = reactive(buildDefaultForm());
|
||
const items = ref([buildDefaultItem()]);
|
||
const formItemsExpanded = ref(false);
|
||
const customerHint = ref("");
|
||
const attachmentFiles = ref([]);
|
||
|
||
const saleTotal = computed(() =>
|
||
items.value.reduce((total, row) => total + Number(row.quantity || 0) * Number(row.sale_price || 0), 0),
|
||
);
|
||
|
||
function onSelfPickupChange() {
|
||
if (form.is_self_pickup) {
|
||
form.tracking_number = "";
|
||
form.express_company = "";
|
||
}
|
||
}
|
||
|
||
function resetForm() {
|
||
Object.assign(form, buildDefaultForm());
|
||
items.value = [buildDefaultItem()];
|
||
selectedCustomerId.value = "";
|
||
customerHint.value = "";
|
||
message.value = "";
|
||
formItemsExpanded.value = false;
|
||
}
|
||
|
||
function addItemRow() {
|
||
items.value.push(buildDefaultItem());
|
||
formItemsExpanded.value = true;
|
||
}
|
||
|
||
function removeItemRow(index) {
|
||
if (items.value.length <= 1) {
|
||
return;
|
||
}
|
||
items.value.splice(index, 1);
|
||
}
|
||
|
||
function handleCustomerChange() {
|
||
const selected = customerOptions.value.find((option) => String(option.value) === selectedCustomerId.value);
|
||
if (!selected) {
|
||
return;
|
||
}
|
||
form.customer_name = selected.customer_name;
|
||
form.customer_mobile = selected.customer_mobile;
|
||
form.customer_address = selected.customer_address;
|
||
customerHint.value = `已选中客户 ${selected.customer_name},系统将优先使用客户库信息。`;
|
||
}
|
||
|
||
function handleCustomerInputBlur() {
|
||
if (selectedCustomerId.value || !form.customer_name.trim() || !form.customer_mobile.trim()) {
|
||
return;
|
||
}
|
||
const exists = customerOptions.value.find((option) => option.customer_name === form.customer_name.trim() && option.customer_mobile === form.customer_mobile.trim());
|
||
if (exists) {
|
||
selectedCustomerId.value = String(exists.value);
|
||
handleCustomerChange();
|
||
customerHint.value = '已匹配到客户库中的同名同号客户,系统已自动关联。';
|
||
return;
|
||
}
|
||
customerHint.value = form.auto_sync_customer ? '系统将把当前客户视为新客户,并在提交后自动同步到客户库。' : '当前客户未在客户库中匹配到记录,请确认是否需要手动新增。';
|
||
}
|
||
|
||
function handlePaymentMethodChange() {
|
||
if (form.payment_method !== '其他') {
|
||
form.payment_method_custom = '';
|
||
}
|
||
}
|
||
|
||
// 智能填单
|
||
const isMockMode = computed(() => parsedResult.value?.is_mock === true);
|
||
|
||
async function handleParseSubmit() {
|
||
if (parseLoading.value || !parseInput.value.trim()) return;
|
||
parseLoading.value = true;
|
||
message.value = "";
|
||
try {
|
||
const result = await parseOrderText(parseInput.value);
|
||
parsedResult.value = result;
|
||
itemsExpanded.value = false;
|
||
parseEditable.value = {
|
||
customer_name: result.parsed_order.customer_name || "",
|
||
customer_mobile: result.parsed_order.customer_mobile || "",
|
||
customer_address: result.parsed_order.customer_address || "",
|
||
remark: result.parsed_order.remark || "",
|
||
items: (result.parsed_order.items || []).map((item, idx) => ({
|
||
...item,
|
||
_selectedCandidate: result.product_matches?.[idx]?.candidates?.[0] || null,
|
||
_parseSearchText: "",
|
||
_parseShowDropdown: false,
|
||
_parseFilteredProducts: [],
|
||
})),
|
||
};
|
||
} catch (error) {
|
||
message.value = error.message || "解析失败";
|
||
messageType.value = "error";
|
||
} finally {
|
||
parseLoading.value = false;
|
||
}
|
||
}
|
||
|
||
function handleConfirmParse() {
|
||
const parsed = parseEditable.value;
|
||
form.customer_name = parsed.customer_name || "";
|
||
form.customer_mobile = parsed.customer_mobile || "";
|
||
form.customer_address = parsed.customer_address || "";
|
||
form.remark = parsed.remark || "";
|
||
|
||
// 将产品需求填入客户需求字段
|
||
if (parsed.items.length > 0) {
|
||
const demandLines = parsed.items.map((item, idx) => {
|
||
const name = item.product_name || '未识别产品';
|
||
const spec = item.specification ? ` ${item.specification}` : '';
|
||
const unit = item.unit ? ` ${item.unit}` : '';
|
||
const qty = item.quantity ? ` ${item.quantity}${unit}` : '';
|
||
return `${idx + 1}. ${name}${spec}${qty}`;
|
||
});
|
||
form.customer_demand = demandLines.join('\n');
|
||
}
|
||
|
||
if (parsedResult.value?.parsed_order?.customer_id) {
|
||
const cid = parsedResult.value.parsed_order.customer_id;
|
||
selectedCustomerId.value = String(cid);
|
||
handleCustomerChange();
|
||
} else {
|
||
selectedCustomerId.value = "";
|
||
}
|
||
|
||
const newItems = [];
|
||
for (const parsedItem of parsed.items) {
|
||
const row = buildDefaultItem();
|
||
if (parsedItem._selectedCandidate) {
|
||
const c = parsedItem._selectedCandidate;
|
||
row.selectedProductId = String(c.product_id);
|
||
row.product_id = c.product_id;
|
||
row.product_name = c.product_name;
|
||
row.specification = c.specification;
|
||
row.demand_specification = parsedItem.demand_specification || "";
|
||
row.unit = c.unit;
|
||
row.sale_price = c.sale_price;
|
||
row.cost_price = c.cost_price;
|
||
row.quantity = parsedItem.quantity || 1;
|
||
loadExtraInputs(row);
|
||
// Auto-fill extra inputs from AI recognition (e.g., joint_type)
|
||
if (parsedItem.joint_type && row._extraInputs && row._extraInputs.length) {
|
||
row.joint_type = parsedItem.joint_type;
|
||
}
|
||
// 复制计算逻辑提示
|
||
|
||
if (parsedItem._calcHint) {
|
||
row._calcHint = parsedItem._calcHint;
|
||
} else if (c.surcharge_detail) {
|
||
try {
|
||
const detail = typeof c.surcharge_detail === 'string' ? JSON.parse(c.surcharge_detail) : c.surcharge_detail;
|
||
const parts = [];
|
||
if (detail.method) parts.push(`计价方式: ${detail.method}`);
|
||
if (detail.formula) parts.push(`公式: ${detail.formula}`);
|
||
if (detail.length_m && detail.width_m) parts.push(`${detail.length_m}m × ${detail.width_m}m`);
|
||
if (detail.area_sqm) parts.push(`面积: ${detail.area_sqm}㎡`);
|
||
row._calcHint = parts.join(' | ') || '';
|
||
} catch (e) {
|
||
row._calcHint = '';
|
||
}
|
||
}
|
||
} else {
|
||
row.product_name = parsedItem.product_name || "";
|
||
row.specification = parsedItem.specification || "";
|
||
row.demand_specification = parsedItem.demand_specification || "";
|
||
row.unit = parsedItem.unit || "";
|
||
row.quantity = parsedItem.quantity || 1;
|
||
row.sale_price = parsedItem.sale_price || 0;
|
||
row.cost_price = parsedItem.cost_price || 0;
|
||
}
|
||
if (!row.quantity || row.quantity <= 0) row.quantity = parsedItem.quantity || 1;
|
||
if (row.sale_price <= 0 && parsedItem.sale_price > 0) row.sale_price = parsedItem.sale_price;
|
||
if (parsedItem.remark) row.remark = parsedItem.remark;
|
||
if (row.demand_specification) calcDemandResult(row);
|
||
newItems.push(row);
|
||
}
|
||
items.value = newItems.length > 0 ? newItems : [buildDefaultItem()];
|
||
formItemsExpanded.value = true;
|
||
parsedResult.value = null;
|
||
parseEditable.value = { customer_name: "", customer_mobile: "", customer_address: "", remark: "", items: [] };
|
||
}
|
||
|
||
function handleProductCandidateSelect(itemIndex, candidate) {
|
||
const item = parseEditable.value.items[itemIndex];
|
||
if (!item) return;
|
||
item._selectedCandidate = candidate;
|
||
// 生成计算逻辑提示
|
||
if (candidate.surcharge_detail) {
|
||
try {
|
||
const detail = typeof candidate.surcharge_detail === 'string' ? JSON.parse(candidate.surcharge_detail) : candidate.surcharge_detail;
|
||
const parts = [];
|
||
if (detail.method) parts.push(`计价方式: ${detail.method}`);
|
||
if (detail.formula) parts.push(`公式: ${detail.formula}`);
|
||
if (detail.length_m && detail.width_m) parts.push(`${detail.length_m}m × ${detail.width_m}m`);
|
||
if (detail.area_sqm) parts.push(`面积: ${detail.area_sqm}㎡`);
|
||
item._calcHint = parts.join(' | ') || '';
|
||
} catch (e) {
|
||
item._calcHint = '';
|
||
}
|
||
} else {
|
||
item._calcHint = '';
|
||
}
|
||
}
|
||
|
||
function handleParseProductSelect(itemIndex, productId) {
|
||
const item = parseEditable.value.items[itemIndex];
|
||
if (!item) return;
|
||
if (!productId) {
|
||
item._selectedCandidate = null;
|
||
return;
|
||
}
|
||
const selected = productOptions.value.find((opt) => String(opt.value) === productId);
|
||
if (selected) {
|
||
item._selectedCandidate = {
|
||
product_id: selected.value,
|
||
product_name: selected.product_name,
|
||
specification: selected.specification,
|
||
unit: selected.unit,
|
||
sale_price: selected.sale_price,
|
||
cost_price: selected.cost_price,
|
||
surcharge_detail: selected.surcharge_detail,
|
||
match_score: 1,
|
||
};
|
||
item._parseSearchText = selected.product_name;
|
||
item._parseShowDropdown = false;
|
||
// 生成计算逻辑提示
|
||
if (selected.surcharge_detail) {
|
||
try {
|
||
const detail = typeof selected.surcharge_detail === 'string' ? JSON.parse(selected.surcharge_detail) : selected.surcharge_detail;
|
||
const parts = [];
|
||
if (detail.method) parts.push(`计价方式: ${detail.method}`);
|
||
if (detail.formula) parts.push(`公式: ${detail.formula}`);
|
||
if (detail.length_m && detail.width_m) parts.push(`${detail.length_m}m × ${detail.width_m}m`);
|
||
if (detail.area_sqm) parts.push(`面积: ${detail.area_sqm}㎡`);
|
||
item._calcHint = parts.join(' | ') || '';
|
||
} catch (e) {
|
||
item._calcHint = '';
|
||
}
|
||
} else {
|
||
item._calcHint = '';
|
||
}
|
||
}
|
||
}
|
||
|
||
function handleParseProductSearch(item) {
|
||
const text = (item._parseSearchText || "").toLowerCase();
|
||
if (!text) {
|
||
item._parseFilteredProducts = productOptions.value.slice(0, 50);
|
||
} else {
|
||
item._parseFilteredProducts = productOptions.value.filter((opt) => {
|
||
const name = (opt.product_name || "").toLowerCase();
|
||
const spec = (opt.specification || "").toLowerCase();
|
||
return name.includes(text) || spec.includes(text);
|
||
}).slice(0, 50);
|
||
}
|
||
}
|
||
|
||
function hideParseProductDropdown(item) {
|
||
setTimeout(() => { item._parseShowDropdown = false; }, 200);
|
||
}
|
||
|
||
function handleResetParse() {
|
||
parsedResult.value = null;
|
||
parseEditable.value = { customer_name: "", customer_mobile: "", customer_address: "", remark: "", items: [] };
|
||
parseInput.value = "";
|
||
itemsExpanded.value = false;
|
||
}
|
||
|
||
// 订单附件
|
||
function handleAttachmentChange(event) {
|
||
const files = Array.from(event.target.files || []);
|
||
for (const file of files) {
|
||
attachmentFiles.value.push({
|
||
rowKey: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||
file,
|
||
preview: URL.createObjectURL(file),
|
||
});
|
||
}
|
||
event.target.value = "";
|
||
}
|
||
|
||
function removeAttachment(index) {
|
||
const removed = attachmentFiles.value[index];
|
||
if (removed?.preview) URL.revokeObjectURL(removed.preview);
|
||
attachmentFiles.value.splice(index, 1);
|
||
}
|
||
|
||
async function uploadAttachments(orderId) {
|
||
for (const item of attachmentFiles.value) {
|
||
if (item.existing) continue; // 跳过已有附件,不重复上传
|
||
const uploadResult = await localUploadFile(item.file);
|
||
await saveAttachment({
|
||
biz_type: "sales_order",
|
||
biz_id: orderId,
|
||
file_name: item.file.name,
|
||
file_url: uploadResult.url,
|
||
file_type: item.file.type || "image/jpeg",
|
||
file_size: item.file.size,
|
||
});
|
||
}
|
||
}
|
||
|
||
function handleProductChange(row) {
|
||
const selected = productOptions.value.find((option) => String(option.value) === row.selectedProductId);
|
||
if (!selected) return;
|
||
row.product_id = selected.value;
|
||
row.product_name = selected.product_name;
|
||
row.specification = selected.specification;
|
||
row.unit = selected.unit;
|
||
row.sale_price = Number(selected.sale_price || 0);
|
||
row.cost_price = Number(selected.cost_price || 0);
|
||
// Load extra pricing inputs (e.g., joint_type for conveyor belts)
|
||
loadExtraInputs(row);
|
||
// 计算逻辑提示
|
||
if (selected.surcharge_detail) {
|
||
try {
|
||
const detail = typeof selected.surcharge_detail === 'string' ? JSON.parse(selected.surcharge_detail) : selected.surcharge_detail;
|
||
const parts = [];
|
||
if (detail.method) parts.push(`计价方式: ${detail.method}`);
|
||
if (detail.formula) parts.push(`公式: ${detail.formula}`);
|
||
if (detail.length_m && detail.width_m) parts.push(`${detail.length_m}m × ${detail.width_m}m`);
|
||
if (detail.area_sqm) parts.push(`面积: ${detail.area_sqm}㎡`);
|
||
row._calcHint = parts.join(' | ') || '';
|
||
} catch (e) {
|
||
row._calcHint = '';
|
||
}
|
||
} else {
|
||
row._calcHint = '';
|
||
}
|
||
}
|
||
|
||
function getJointTypeLabel(value) {
|
||
const map = {
|
||
niubi: "牛鼻子接头",
|
||
polyester_spiral: "聚酯螺旋接头",
|
||
steel_buckle: "钢扣接头",
|
||
wall_kevlar: "墙式凯夫拉布接头",
|
||
niubi_kevlar: "牛鼻子凯夫拉接头",
|
||
diagonal: "斜接",
|
||
overlap: "搭接",
|
||
flat: "平接",
|
||
};
|
||
return map[value] || value || "";
|
||
}
|
||
|
||
function loadExtraInputs(row) {
|
||
const selected = productOptions.value.find((opt) => String(opt.value) === row.selectedProductId);
|
||
if (!selected || !selected.pricing_inputs) {
|
||
row._extraInputs = [];
|
||
return;
|
||
}
|
||
try {
|
||
const inputs = JSON.parse(selected.pricing_inputs || '[]');
|
||
row._extraInputs = inputs.filter(f => f.type === 'select');
|
||
if (row.joint_type) {
|
||
row.joint_type_label = getJointTypeLabel(row.joint_type);
|
||
}
|
||
} catch { row._extraInputs = []; }
|
||
}
|
||
function handleProductSearch(row) {
|
||
const text = (row._searchText || "").toLowerCase();
|
||
if (!text) {
|
||
row._filteredProducts = productOptions.value.slice(0, 50);
|
||
} else {
|
||
row._filteredProducts = productOptions.value.filter((opt) => {
|
||
const name = (opt.product_name || "").toLowerCase();
|
||
const spec = (opt.specification || "").toLowerCase();
|
||
return name.includes(text) || spec.includes(text);
|
||
}).slice(0, 50);
|
||
}
|
||
}
|
||
|
||
function selectProduct(row, opt) {
|
||
row.selectedProductId = String(opt.value);
|
||
row._searchText = opt.product_name;
|
||
row._showDropdown = false;
|
||
handleProductChange(row);
|
||
}
|
||
|
||
function hideProductDropdown(row) {
|
||
setTimeout(() => { row._showDropdown = false; }, 200);
|
||
}
|
||
|
||
/**
|
||
* 解析需求规格并计算结果
|
||
* 支持格式:
|
||
* - 长度尺寸: "25cm×25cm", "25×30", "25cm x 30cm", "0.25m×0.3m", "250mm×300mm", "25厘米×30厘米"
|
||
* - 重量: "500克", "2斤", "1.5kg", "2公斤"
|
||
* 计算面积(宽×高)或显示重量
|
||
*/
|
||
function calcDemandResult(row) {
|
||
const spec = (row.demand_specification || "").trim();
|
||
if (!spec) {
|
||
row._demandResult = "";
|
||
row._demandCalcHint = "";
|
||
return;
|
||
}
|
||
|
||
// 标准化长度单位
|
||
const normalizeLengthUnit = (unit) => {
|
||
if (!unit) return "cm";
|
||
const u = unit.toLowerCase();
|
||
if (u === "m" || u === "米") return "m";
|
||
if (u === "mm" || u === "毫米") return "mm";
|
||
return "cm"; // 厘米、cm 或无单位默认
|
||
};
|
||
|
||
// 匹配长度尺寸: 数字 + 可选单位 × 数字 + 可选单位
|
||
const lengthMatch = spec.match(/([\d.]+)\s*(cm|厘米|m|米|mm|毫米)?\s*[×xX*]\s*([\d.]+)\s*(cm|厘米|m|米|mm|毫米)?/i);
|
||
if (lengthMatch) {
|
||
const v1 = parseFloat(lengthMatch[1]);
|
||
const u1 = normalizeLengthUnit(lengthMatch[2]);
|
||
const v2 = parseFloat(lengthMatch[3]);
|
||
const u2 = normalizeLengthUnit(lengthMatch[4]);
|
||
// 统一转换为米
|
||
const toM = (v, u) => u === "m" ? v : u === "mm" ? v / 1000 : v / 100;
|
||
const m1 = toM(v1, u1);
|
||
const m2 = toM(v2, u2);
|
||
row.length_m = m2;
|
||
row.width_m = m1;
|
||
const qty = Number(row.quantity) || 0;
|
||
const total = m1 * m2 * qty;
|
||
row._demandResult = qty > 0 ? `${total.toFixed(4)} m²` : `${(m1 * m2).toFixed(4)} m²`;
|
||
row._demandCalcHint = qty > 0
|
||
? `${v1}${u1} × ${v2}${u2} × ${qty} = ${m1}m × ${m2}m × ${qty} = ${total.toFixed(4)}m²`
|
||
: `${v1}${u1} × ${v2}${u2} = ${m1}m × ${m2}m = ${(m1 * m2).toFixed(4)}m²`;
|
||
return;
|
||
}
|
||
|
||
// 匹配重量: 数字 + 单位
|
||
const weightMatch = spec.match(/([\d.]+)\s*(克|斤|kg|公斤)/i);
|
||
if (weightMatch) {
|
||
const value = parseFloat(weightMatch[1]);
|
||
const unit = weightMatch[2];
|
||
row._demandResult = `${value}${unit}`;
|
||
row._demandCalcHint = `重量: ${value}${unit}`;
|
||
return;
|
||
}
|
||
|
||
// 无法识别
|
||
row._demandResult = "";
|
||
row._demandCalcHint = "";
|
||
}
|
||
|
||
const paymentChannels = ref([]);
|
||
|
||
async function loadOptions() {
|
||
optionLoading.value = true;
|
||
try {
|
||
const [customers, products, channelsConfig] = await Promise.all([
|
||
fetchCustomerOptions(),
|
||
fetchProductOptions(),
|
||
fetchConfigItem("payment_channels").catch(() => null),
|
||
]);
|
||
customerOptions.value = customers;
|
||
productOptions.value = products;
|
||
// 解析收款渠道配置
|
||
if (channelsConfig && channelsConfig.config_value) {
|
||
try {
|
||
paymentChannels.value = JSON.parse(channelsConfig.config_value);
|
||
} catch (e) {
|
||
paymentChannels.value = [];
|
||
}
|
||
}
|
||
} catch (error) {
|
||
message.value = error.message || "加载客户、产品选项失败";
|
||
messageType.value = "error";
|
||
} finally {
|
||
optionLoading.value = false;
|
||
}
|
||
}
|
||
|
||
async function handleSubmit() {
|
||
if (submitting.value) return; // 防止重复提交
|
||
if (!form.customer_name.trim()) {
|
||
message.value = "请输入客户姓名";
|
||
messageType.value = "error";
|
||
return;
|
||
}
|
||
if (!form.customer_mobile.trim()) {
|
||
message.value = "请输入客户手机号";
|
||
messageType.value = "error";
|
||
return;
|
||
}
|
||
const matchedCustomer = customerOptions.value.find((option) => option.customer_name === form.customer_name.trim() && option.customer_mobile === form.customer_mobile.trim());
|
||
if (!matchedCustomer && form.auto_sync_customer) {
|
||
customerHint.value = '当前客户未匹配到客户库,提交后将自动同步为新客户。';
|
||
}
|
||
const validItems = items.value.filter((row) => row.product_name.trim());
|
||
if (!validItems.length) {
|
||
message.value = "请至少填写一个产品";
|
||
messageType.value = "error";
|
||
return;
|
||
}
|
||
|
||
submitting.value = true;
|
||
message.value = "";
|
||
try {
|
||
const paymentMethod = form.payment_method === '其他' ? form.payment_method_custom : form.payment_method;
|
||
// 根据收款渠道自动判断订单来源(电商/普通)
|
||
const selectedChannel = paymentChannels.value.find((ch) => ch.name === form.payment_method);
|
||
const orderSource = selectedChannel && selectedChannel.is_ecommerce ? "电商" : "";
|
||
const selfDelivery = form.tracking_number ? 1 : 0;
|
||
const payload = {
|
||
customer_name: form.customer_name,
|
||
customer_mobile: form.customer_mobile,
|
||
customer_address: form.customer_address,
|
||
customer_demand: form.customer_demand,
|
||
contract_amount: form.contract_amount,
|
||
freight_total: form.freight_total,
|
||
rebate_total: form.commission_amount,
|
||
tax_total: form.tax_total,
|
||
other_fee_total: form.other_fee_total,
|
||
commission_amount: form.commission_amount,
|
||
tax_amount: form.tax_amount,
|
||
need_invoice: form.need_invoice ? 1 : 0,
|
||
order_type: form.order_type,
|
||
self_delivery: selfDelivery,
|
||
is_self_pickup: form.is_self_pickup ? 1 : 0,
|
||
tracking_number: form.tracking_number || null,
|
||
express_company: form.express_company || null,
|
||
payment_method: paymentMethod,
|
||
order_source: orderSource,
|
||
remark: form.remark,
|
||
auto_sync_customer: form.auto_sync_customer,
|
||
customer_sync_mode: matchedCustomer ? 'linked' : form.auto_sync_customer ? 'auto_create' : 'manual_review',
|
||
items: validItems.map(({ rowKey, selectedProductId, _searchText, _showDropdown, _filteredProducts, _demandResult, _demandCalcHint, _calcHint, _extraInputs, ...rest }) => {
|
||
const extra = {};
|
||
if (_extraInputs) {
|
||
for (const f of _extraInputs) {
|
||
if (rest[f.key]) extra[f.key] = rest[f.key];
|
||
}
|
||
}
|
||
return { ...rest, pricing_extra_inputs: Object.keys(extra).length ? extra : undefined };
|
||
}),
|
||
};
|
||
let result;
|
||
if (isEditMode.value) {
|
||
result = await updateOrder(editingOrderId.value, payload);
|
||
if (attachmentFiles.value.length) await uploadAttachments(result.order_id);
|
||
message.value = `订单 ${result.order_no} 更新成功。`;
|
||
} else {
|
||
result = await createOrder(payload);
|
||
if (attachmentFiles.value.length) await uploadAttachments(result.order_id);
|
||
message.value = `订单 ${result.order_no} 创建成功,已提交审批。`;
|
||
}
|
||
messageType.value = "success";
|
||
await router.push(`/orders/${result.order_id}`);
|
||
} catch (error) {
|
||
message.value = error.message || (isEditMode.value ? "更新订单失败" : "创建订单失败");
|
||
messageType.value = "error";
|
||
} finally {
|
||
submitting.value = false;
|
||
}
|
||
}
|
||
|
||
async function loadOrderForEdit(orderId) {
|
||
try {
|
||
const data = await fetchOrderForEdit(orderId);
|
||
form.customer_name = data.customer_name || "";
|
||
form.customer_mobile = data.customer_mobile || "";
|
||
form.customer_address = data.customer_address || "";
|
||
form.customer_demand = data.customer_demand || "";
|
||
form.contract_amount = data.contract_amount || 0;
|
||
form.freight_total = data.freight_total || 0;
|
||
form.rebate_total = data.rebate_total || 0;
|
||
form.tax_total = data.tax_total || 0;
|
||
form.other_fee_total = data.other_fee_total || 0;
|
||
form.commission_amount = data.commission_amount || 0;
|
||
form.tax_amount = data.tax_amount || 0;
|
||
form.need_invoice = data.need_invoice ? true : false;
|
||
form.order_type = data.order_type || "";
|
||
form.is_self_pickup = Boolean(data.is_self_pickup);
|
||
form.tracking_number = data.tracking_number || "";
|
||
form.express_company = data.express_company || "";
|
||
form.payment_method = data.payment_method || "";
|
||
form.remark = data.remark || "";
|
||
if (data.customer_id) {
|
||
selectedCustomerId.value = String(data.customer_id);
|
||
handleCustomerChange();
|
||
}
|
||
if (data.items && data.items.length) {
|
||
items.value = data.items.map((item) => {
|
||
const row = buildDefaultItem();
|
||
row.product_id = item.product_id;
|
||
row.product_name = item.product_name;
|
||
row.specification = item.specification;
|
||
row.unit = item.unit;
|
||
row.quantity = item.quantity;
|
||
row.sale_price = item.sale_price;
|
||
row.cost_price = item.cost_price || 0;
|
||
row.remark = item.remark || "";
|
||
if (item.product_id) {
|
||
row.selectedProductId = String(item.product_id);
|
||
}
|
||
return row;
|
||
});
|
||
}
|
||
customerHint.value = `正在编辑订单,修改后将覆盖原有订单内容。`;
|
||
if (data.attachments && data.attachments.length) {
|
||
attachmentFiles.value = data.attachments
|
||
.filter(function (a) { return a.biz_type === "sales_order"; })
|
||
.map(function (a) {
|
||
return {
|
||
rowKey: "existing-" + a.attachment_id,
|
||
file: { name: a.file_name, type: a.file_type, size: a.file_size },
|
||
preview: a.file_url,
|
||
existing: true,
|
||
attachmentId: a.attachment_id,
|
||
};
|
||
});
|
||
}
|
||
} catch (error) {
|
||
message.value = error.message || "加载订单数据失败";
|
||
messageType.value = "error";
|
||
}
|
||
}
|
||
|
||
onMounted(async () => {
|
||
await loadOptions();
|
||
const editId = route.query.edit;
|
||
if (editId) {
|
||
editingOrderId.value = Number(editId);
|
||
await loadOrderForEdit(editId);
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.page {
|
||
display: grid;
|
||
gap: 18px;
|
||
}
|
||
|
||
.page-hero,
|
||
.form-section,
|
||
.summary {
|
||
background: rgba(255, 255, 255, 0.92);
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 20px;
|
||
box-shadow: 0 14px 40px rgba(15, 23, 42, 0.06);
|
||
}
|
||
|
||
.page-hero {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 20px;
|
||
align-items: flex-start;
|
||
padding: 24px;
|
||
}
|
||
|
||
.eyebrow {
|
||
display: inline-flex;
|
||
padding: 6px 12px;
|
||
border-radius: 999px;
|
||
background: #dbeafe;
|
||
color: #1d4ed8;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.04em;
|
||
}
|
||
|
||
h2 {
|
||
margin: 12px 0 8px;
|
||
font-size: 30px;
|
||
}
|
||
|
||
.tips,
|
||
.section-header span {
|
||
color: #64748b;
|
||
}
|
||
|
||
.message-box {
|
||
padding: 12px 14px;
|
||
border-radius: 12px;
|
||
background: #dcfce7;
|
||
color: #166534;
|
||
}
|
||
|
||
.message-box.error {
|
||
background: #fee2e2;
|
||
color: #b91c1c;
|
||
}
|
||
|
||
.form-grid {
|
||
display: grid;
|
||
gap: 16px;
|
||
}
|
||
|
||
.form-section {
|
||
padding: 20px;
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
margin-bottom: 14px;
|
||
align-items: center;
|
||
}
|
||
|
||
.section-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||
|
||
.section-header h3,
|
||
.summary strong {
|
||
margin: 0;
|
||
}
|
||
|
||
.section-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 12px;
|
||
}
|
||
|
||
.full-width-label { grid-column: 1 / -1; }
|
||
.full-width { grid-column: 1 / -1; }
|
||
|
||
label {
|
||
display: grid;
|
||
gap: 8px;
|
||
}
|
||
|
||
label span {
|
||
color: #374151;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.checkline {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.checkline input[type="checkbox"] {
|
||
width: 18px;
|
||
height: 18px;
|
||
}
|
||
|
||
input,
|
||
textarea,
|
||
select {
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 12px;
|
||
padding: 10px 12px;
|
||
background: #fff;
|
||
}
|
||
|
||
.payment-method-input {
|
||
display: grid;
|
||
gap: 8px;
|
||
}
|
||
|
||
.summary {
|
||
display: grid;
|
||
grid-template-columns: 1fr;
|
||
max-width: 320px;
|
||
gap: 12px;
|
||
padding: 18px 20px;
|
||
}
|
||
|
||
.summary article {
|
||
padding: 16px;
|
||
border-radius: 16px;
|
||
background: linear-gradient(180deg, #ffffff, #f8fbff);
|
||
border: 1px solid #dbe3ef;
|
||
display: grid;
|
||
gap: 8px;
|
||
}
|
||
|
||
.summary span {
|
||
color: #64748b;
|
||
}
|
||
|
||
.summary strong {
|
||
font-size: 24px;
|
||
color: #0f172a;
|
||
}
|
||
|
||
.items-toggle {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
cursor: pointer;
|
||
padding: 10px 12px;
|
||
margin: 10px 0;
|
||
background: #f1f5f9;
|
||
border-radius: 10px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #374151;
|
||
}
|
||
|
||
.items-toggle:hover {
|
||
background: #e2e8f0;
|
||
}
|
||
|
||
.item-card {
|
||
margin-bottom: 14px;
|
||
padding: 14px;
|
||
border: 1px solid #dbe3ef;
|
||
border-radius: 16px;
|
||
background: #f8fbff;
|
||
overflow: visible;
|
||
}
|
||
|
||
.item-card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.item-card-header h4 { margin: 0; }
|
||
|
||
.product-search-label {
|
||
position: relative;
|
||
}
|
||
.product-search-box {
|
||
position: relative;
|
||
}
|
||
.product-search-box input {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
.product-dropdown {
|
||
position: absolute;
|
||
top: 100%;
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 100;
|
||
background: #fff;
|
||
border: 1px solid #dbe3ef;
|
||
border-radius: 8px;
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||
}
|
||
.parse-product-search {
|
||
position: relative;
|
||
min-width: 0;
|
||
}
|
||
.parse-product-search .product-dropdown {
|
||
width: 100%;
|
||
right: auto;
|
||
}
|
||
.product-dropdown-item {
|
||
padding: 8px 12px;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
}
|
||
.product-dropdown-item:hover {
|
||
background: #f0f7ff;
|
||
}
|
||
|
||
.hint-text-cost {
|
||
font-size: 12px;
|
||
color: #94a3b8;
|
||
margin: 4px 0 8px;
|
||
padding: 0 4px;
|
||
}
|
||
|
||
.calc-hint {
|
||
font-size: 11px;
|
||
color: #6366f1;
|
||
margin: 2px 0 0;
|
||
padding: 0;
|
||
}
|
||
.calc-result {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #059669;
|
||
padding: 8px 10px;
|
||
background: #f0fdf4;
|
||
border: 1px solid #bbf7d0;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
button {
|
||
border: 1px solid #d1d5db;
|
||
background: #fff;
|
||
border-radius: 12px;
|
||
padding: 10px 14px;
|
||
}
|
||
|
||
.small-btn { padding: 8px 12px; }
|
||
.danger-link { color: #dc2626; }
|
||
.link-btn { background: none; border: none; cursor: pointer; }
|
||
|
||
.primary-btn {
|
||
background: linear-gradient(135deg, #2563eb, #1d4ed8);
|
||
color: #fff;
|
||
border-color: transparent;
|
||
}
|
||
|
||
button:disabled {
|
||
color: #9ca3af;
|
||
background: #f3f4f6;
|
||
}
|
||
|
||
.secondary-btn {
|
||
color: #374151;
|
||
}
|
||
|
||
.hint-line {
|
||
font-size: 12px;
|
||
color: #64748b;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
@media (max-width: 960px) {
|
||
.page-hero {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.summary,
|
||
.section-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.actions {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.section-header,
|
||
.item-card-header {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.page {
|
||
gap: 12px;
|
||
}
|
||
|
||
.page-hero,
|
||
.form-section,
|
||
.summary {
|
||
padding: 14px;
|
||
border-radius: 16px;
|
||
}
|
||
|
||
h2 {
|
||
font-size: 24px;
|
||
margin: 10px 0 6px;
|
||
}
|
||
|
||
.tips,
|
||
.section-header span,
|
||
label span,
|
||
.summary span,
|
||
.message-box,
|
||
button {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.form-grid {
|
||
gap: 12px;
|
||
}
|
||
|
||
.form-section {
|
||
padding: 12px;
|
||
}
|
||
|
||
.section-header {
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.section-grid,
|
||
.summary {
|
||
gap: 10px;
|
||
}
|
||
|
||
input,
|
||
textarea,
|
||
select,
|
||
button {
|
||
padding: 9px 10px;
|
||
border-radius: 10px;
|
||
}
|
||
|
||
.summary strong {
|
||
font-size: 18px;
|
||
}
|
||
|
||
.actions {
|
||
gap: 8px;
|
||
}
|
||
}
|
||
|
||
/* 智能填单样式 */
|
||
.ghost-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 12px;
|
||
padding: 10px 14px;
|
||
background: #f8fafc;
|
||
color: #334155;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.ghost-btn:disabled {
|
||
color: #9ca3af;
|
||
background: #f3f4f6;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.parse-card { border: 2px dashed #93c5fd; background: linear-gradient(180deg, #f0f9ff, #fff); }
|
||
.parse-text-area textarea { width: 100%; border: 1px solid #d1d5db; border-radius: 12px; padding: 10px 12px; resize: vertical; font-size: 14px; }
|
||
.parse-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }
|
||
|
||
.parse-result-card { border: 2px solid #2563eb; background: linear-gradient(180deg, #eff6ff, #fff); }
|
||
.parse-result-meta { display: flex; gap: 8px; align-items: center; }
|
||
.confidence-tag { display: inline-flex; padding: 4px 10px; border-radius: 999px; background: #dcfce7; color: #166534; font-size: 12px; font-weight: 600; }
|
||
.mode-tag.fallback { display: inline-flex; padding: 4px 10px; border-radius: 999px; background: #fef3c7; color: #92400e; font-size: 12px; font-weight: 600; }
|
||
.parse-result-fields { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-bottom: 14px; }
|
||
.parse-result-items { margin-bottom: 14px; }
|
||
.items-header { display: flex; justify-content: space-between; align-items: center; cursor: pointer; padding: 8px 0; }
|
||
.items-header h4 { margin: 0; font-size: 14px; }
|
||
.expand-icon { font-size: 12px; color: #6b7280; }
|
||
.parse-item-card { padding: 10px; border: 1px solid #e5e7eb; border-radius: 10px; margin-bottom: 8px; background: #f9fafb; overflow: visible; }
|
||
.parse-item-main { display: flex; flex-direction: column; gap: 4px; }
|
||
.parse-item-main strong { font-size: 14px; }
|
||
.parse-item-main span { font-size: 12px; color: #6b7280; }
|
||
.demand-spec-tag {
|
||
display: inline-flex; padding: 2px 8px; margin-top: 4px;
|
||
background: #eff6ff; color: #1d4ed8; border-radius: 6px;
|
||
font-size: 11px; font-weight: 600;
|
||
}
|
||
.parse-item-select { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
|
||
.parse-item-select .select-label { font-size: 12px; color: #6b7280; white-space: nowrap; }
|
||
.parse-product-search { flex: 1; }
|
||
.parse-item-candidates { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; align-items: center; }
|
||
.candidate-label { font-size: 12px; color: #6b7280; }
|
||
.candidate-btn { border: 1px solid #d1d5db; background: #fff; border-radius: 8px; padding: 4px 10px; font-size: 11px; cursor: pointer; }
|
||
.candidate-btn.selected { background: #2563eb; color: #fff; border-color: #2563eb; }
|
||
.parse-warnings { margin-bottom: 10px; }
|
||
.parse-warnings p { font-size: 12px; color: #b45309; margin: 2px 0; }
|
||
|
||
/* 订单附件 */
|
||
.attachment-area { display: flex; flex-direction: column; gap: 10px; align-items: flex-start; }
|
||
.attachment-upload-label { cursor: pointer; }
|
||
.attachment-list { display: flex; flex-wrap: wrap; gap: 10px; }
|
||
.attachment-card {
|
||
display: flex; align-items: center; gap: 8px;
|
||
padding: 8px 10px; border: 1px solid #e5e7eb; border-radius: 10px; background: #f9fafb;
|
||
}
|
||
.attachment-thumb { width: 48px; height: 48px; object-fit: cover; border-radius: 8px; }
|
||
.attachment-name { font-size: 12px; color: #374151; max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
</style>
|