D2业务员H5+移动模拟全绿 / P2-2部署清单+ALLOWED_HOSTS通配符门 / P3-1跳过
This commit is contained in:
@@ -0,0 +1,630 @@
|
||||
<template>
|
||||
<div class="sm-wrap">
|
||||
<!-- 登录(业务员 = 后台用户 JWT,与 PC 端同一套账号) -->
|
||||
<div v-if="!token" class="sm-login">
|
||||
<h2>业务员开单</h2>
|
||||
<p class="sm-sub">外勤开单 / 查欠款 / 收款登记,用公司账号登录</p>
|
||||
<el-form :model="login" label-position="top" @keyup.enter="doLogin">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="login.username" placeholder="如 alice" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码">
|
||||
<el-input v-model="login.password" type="password" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="租户编码">
|
||||
<el-input v-model="login.tenant" placeholder="如 default" />
|
||||
</el-form-item>
|
||||
<el-button type="primary" style="width: 100%" :loading="loading" @click="doLogin">
|
||||
登 录
|
||||
</el-button>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 主界面 -->
|
||||
<template v-else>
|
||||
<div class="sm-head">
|
||||
<div>
|
||||
<div class="sm-shop">业务员开单</div>
|
||||
<div class="sm-sub">已登录 {{ username }} · {{ tenant }}</div>
|
||||
</div>
|
||||
<el-button text size="small" @click="logout">退出</el-button>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="tab">
|
||||
<!-- 外勤开单 -->
|
||||
<el-tab-pane label="开单" name="bill">
|
||||
<el-form label-position="top" size="small">
|
||||
<el-form-item label="客户" required>
|
||||
<el-select
|
||||
v-model="form.customer" filterable placeholder="选择客户"
|
||||
style="width: 100%" @change="onCustomerChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="c in customers" :key="c.id"
|
||||
:label="`${c.code} ${c.name}`" :value="c.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="仓库" required>
|
||||
<el-select v-model="form.warehouse" placeholder="选择仓库" style="width: 100%">
|
||||
<el-option
|
||||
v-for="w in warehouses" :key="w.id"
|
||||
:label="`${w.code} ${w.name}`" :value="w.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 信用额度条(复用 SalesBills 取价页同口径) -->
|
||||
<div v-if="Number(credit.limit) > 0" class="credit-bar">
|
||||
<span class="credit-label">
|
||||
信用:已用 ¥{{ credit.outstanding }} / 共 ¥{{ credit.limit }}
|
||||
</span>
|
||||
<el-progress
|
||||
:percentage="creditPercent" :status="creditStatus" :stroke-width="10"
|
||||
style="flex: 1; min-width: 120px"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="sm-search">
|
||||
<el-input
|
||||
v-model="search" placeholder="搜索商品名/编码" clearable
|
||||
@keyup.enter="filterProducts" @clear="filterProducts"
|
||||
>
|
||||
<template #append><el-button @click="filterProducts">搜</el-button></template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<div class="sm-list" v-loading="catalogLoading">
|
||||
<div v-for="p in visibleProducts" :key="p.id" class="sm-item">
|
||||
<div class="sm-item-main">
|
||||
<div class="sm-name">{{ p.name }}</div>
|
||||
<div class="sm-meta">{{ p.code }} | {{ p.spec || '—' }}</div>
|
||||
<div class="sm-price">
|
||||
¥ {{ lineOf(p).price.toFixed(2) }}
|
||||
<span class="sm-price-unit">/ {{ lineOf(p).unitName }}</span>
|
||||
<el-tag v-if="lineOf(p).source" size="small" type="info" effect="plain">
|
||||
{{ sourceText(lineOf(p).source) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div v-if="lineOf(p).taxRate > 0" class="sm-tax">
|
||||
税率 {{ (lineOf(p).taxRate * 100).toFixed(0) }}% · 税 ¥{{ lineTax(p) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="sm-item-ops">
|
||||
<el-input-number
|
||||
v-model="lineOf(p).qty" :min="0" size="small"
|
||||
:controls="false" style="width: 64px"
|
||||
/>
|
||||
<el-select
|
||||
v-model="lineOf(p).unitId" size="small" style="width: 84px"
|
||||
@change="() => onUnitChange(p)"
|
||||
>
|
||||
<el-option
|
||||
v-for="u in (lineOf(p).units || [])" :key="u.unit_id"
|
||||
:label="u.unit_name" :value="u.unit_id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button size="small" type="primary" @click="addLine(p)">加</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-if="!catalogLoading && !visibleProducts.length" description="暂无商品" />
|
||||
</div>
|
||||
|
||||
<!-- 当前单据 -->
|
||||
<div v-if="order.length" class="sm-order">
|
||||
<div class="sm-order-title">当前单据({{ order.length }} 行)</div>
|
||||
<div v-for="(r, i) in order" :key="i" class="sm-order-row">
|
||||
<span class="sm-order-name">{{ r.name }} × {{ r.qty }}{{ r.unitName }}</span>
|
||||
<span>¥ {{ (r.qty * r.price).toFixed(2) }}</span>
|
||||
<el-button link type="danger" size="small" @click="order.splice(i, 1)">删</el-button>
|
||||
</div>
|
||||
<div class="sm-totals">
|
||||
<span>行合计 ¥ {{ linesTotal.toFixed(2) }}</span>
|
||||
<span v-if="taxTotal > 0" class="sm-tax-total">其中税 ¥{{ taxTotal.toFixed(2) }}</span>
|
||||
</div>
|
||||
<div class="sm-totals">
|
||||
<span>抹零</span>
|
||||
<el-select v-model="form.round_to" size="small" style="width: 120px">
|
||||
<el-option label="不抹零" value="" />
|
||||
<el-option label="抹分(0.01)" value="0.01" />
|
||||
<el-option label="抹角(0.1)" value="0.1" />
|
||||
<el-option label="抹元(1)" value="1" />
|
||||
</el-select>
|
||||
<span v-if="roundOffAmount > 0" class="sm-round">- ¥{{ roundOffAmount.toFixed(2) }}</span>
|
||||
<b class="sm-net">应收 ¥{{ netTotal.toFixed(2) }}</b>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary" style="width: 100%; margin-top: 8px"
|
||||
:loading="submitting" @click="submitBill"
|
||||
>
|
||||
保存并过账
|
||||
</el-button>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 查欠款 -->
|
||||
<el-tab-pane label="欠款" name="debt">
|
||||
<el-form label-position="top" size="small">
|
||||
<el-form-item label="客户">
|
||||
<el-select
|
||||
v-model="debtCustomer" filterable clearable placeholder="全部客户"
|
||||
style="width: 100%" @change="loadDebts"
|
||||
>
|
||||
<el-option
|
||||
v-for="c in customers" :key="c.id"
|
||||
:label="`${c.code} ${c.name}`" :value="c.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="sm-debt-sum" v-if="debts.length">
|
||||
未结 <b>¥ {{ debtTotal.toFixed(2) }}</b>({{ debts.length }} 单)
|
||||
</div>
|
||||
<div class="sm-list" v-loading="debtsLoading">
|
||||
<div v-for="d in debts" :key="d.id" class="sm-debt">
|
||||
<div class="sm-item-main">
|
||||
<div class="sm-name">{{ d.bill_no }}</div>
|
||||
<div class="sm-meta">{{ d.customer_name }} | {{ d.bill_date }}</div>
|
||||
<div class="sm-price">
|
||||
未结 ¥ {{ Number(d.balance).toFixed(2) }}
|
||||
<span class="sm-price-unit">(共 ¥{{ Number(d.total_amount).toFixed(2) }},已收 ¥{{ Number(d.paid_amount).toFixed(2) }})</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-tag :type="d.status === 'paid' ? 'success' : d.status === 'partial' ? 'warning' : 'danger'" effect="plain" size="small">
|
||||
{{ debtStatusText(d.status) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<el-empty v-if="!debtsLoading && !debts.length" description="该客户暂无欠款" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 收款登记 -->
|
||||
<el-tab-pane label="收款" name="receipt">
|
||||
<el-form :model="receipt" label-position="top" size="small" :rules="receiptRules" ref="receiptRef">
|
||||
<el-form-item label="客户" prop="customer" required>
|
||||
<el-select v-model="receipt.customer" filterable placeholder="选择客户" style="width: 100%">
|
||||
<el-option
|
||||
v-for="c in customers" :key="c.id"
|
||||
:label="`${c.code} ${c.name}`" :value="c.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="收款单号" prop="bill_no" required>
|
||||
<el-input v-model="receipt.bill_no" placeholder="如 SK202609120001(租户内唯一)" maxlength="64" />
|
||||
</el-form-item>
|
||||
<el-form-item label="收款日期" prop="bill_date" required>
|
||||
<el-date-picker v-model="receipt.bill_date" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="收款金额" prop="amount" required>
|
||||
<el-input-number v-model="receipt.amount" :precision="2" :min="0" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="收款方式">
|
||||
<el-select v-model="receipt.method" placeholder="现金/银行/微信/支付宝" clearable style="width: 100%">
|
||||
<el-option label="现金" value="现金" />
|
||||
<el-option label="银行" value="银行" />
|
||||
<el-option label="微信" value="微信" />
|
||||
<el-option label="支付宝" value="支付宝" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="receipt.remark" type="textarea" :rows="2" maxlength="500" />
|
||||
</el-form-item>
|
||||
<el-button type="primary" style="width: 100%" :loading="receiptSaving" @click="saveReceipt">
|
||||
登记收款
|
||||
</el-button>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import api from '../api/client'
|
||||
import {
|
||||
unitRate,
|
||||
isValidQuantity as validateQuantity,
|
||||
isValidPrice as validatePrice,
|
||||
lineAmount,
|
||||
} from '../utils/transaction'
|
||||
|
||||
const token = ref(localStorage.getItem('token') || '')
|
||||
const username = ref(localStorage.getItem('username') || '')
|
||||
const tenant = ref(localStorage.getItem('tenant') || 'default')
|
||||
const loading = ref(false)
|
||||
const tab = ref('bill')
|
||||
|
||||
const login = reactive({
|
||||
username: localStorage.getItem('username') || '',
|
||||
password: '',
|
||||
tenant: localStorage.getItem('tenant') || 'default',
|
||||
})
|
||||
|
||||
const customers = ref([])
|
||||
const warehouses = ref([])
|
||||
const products = ref([])
|
||||
const visibleProducts = ref([])
|
||||
const search = ref('')
|
||||
const catalogLoading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const credit = ref({})
|
||||
|
||||
const form = reactive({
|
||||
customer: null, warehouse: null, round_to: '',
|
||||
})
|
||||
/** productId → {qty, unitId, unitName, units, price, source, taxRate}(模板纯读,无副作用) */
|
||||
const lineState = reactive({})
|
||||
const order = ref([])
|
||||
|
||||
const sourceMap = {
|
||||
customer: '客户专属价', level: '等级价', last: '上次成交价', default: '默认售价',
|
||||
}
|
||||
const sourceText = (s) => sourceMap[s] || s
|
||||
|
||||
async function doLogin() {
|
||||
if (!login.username || !login.password) { ElMessage.warning('请输入用户名与密码'); return }
|
||||
if (!login.tenant?.trim()) { ElMessage.warning('请输入租户编码'); return }
|
||||
loading.value = true
|
||||
try {
|
||||
// api client 的 baseURL 已是 /api/v1,此处用相对路径(与 Login.vue 的全路径等价)
|
||||
const data = await api.post('/auth/token/', {
|
||||
username: login.username, password: login.password,
|
||||
})
|
||||
localStorage.setItem('token', data.access)
|
||||
localStorage.setItem('username', login.username)
|
||||
localStorage.setItem('tenant', login.tenant.trim() || 'default')
|
||||
localStorage.removeItem('readonly')
|
||||
token.value = data.access
|
||||
username.value = login.username
|
||||
tenant.value = login.tenant.trim() || 'default'
|
||||
await loadRefs()
|
||||
await loadDebts()
|
||||
} catch (e) {
|
||||
ElMessage.error('登录失败:用户名/密码/租户不正确')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem('token')
|
||||
token.value = ''
|
||||
order.value = []
|
||||
}
|
||||
|
||||
function lineOf(p) {
|
||||
if (!lineState[p.id]) {
|
||||
lineState[p.id] = reactive({
|
||||
qty: 0, unitId: null, unitName: '', units: [], price: 0, source: '', taxRate: 0,
|
||||
})
|
||||
void initLine(p)
|
||||
}
|
||||
return lineState[p.id]
|
||||
}
|
||||
|
||||
async function initLine(p) {
|
||||
const row = lineState[p.id]
|
||||
try {
|
||||
const info = await api.get(`/catalog/products/${p.id}/units/`)
|
||||
row.units = info.units || []
|
||||
const base = row.units.find((u) => u.is_base) || row.units[0]
|
||||
row.unitId = base?.unit_id ?? null
|
||||
row.unitName = base?.unit_name || ''
|
||||
row.price = Number(base?.price ?? info.sale_price ?? 0)
|
||||
row.taxRate = Number(info.tax_rate || 0)
|
||||
} catch (e) {
|
||||
row.price = Number(p.sale_price || 0)
|
||||
}
|
||||
}
|
||||
|
||||
function onUnitChange(p) {
|
||||
const row = lineOf(p)
|
||||
const u = (row.units || []).find((x) => x.unit_id === row.unitId) || row.units[0]
|
||||
row.unitName = u?.unit_name || ''
|
||||
// 切单位后按同口径重算单价:已有取价用取价换算,否则用基本售价换算
|
||||
if (row.baseQuotePrice != null) {
|
||||
row.price = Number(row.baseQuotePrice) * unitRate({ source_unit: row.unitId, units: row.units })
|
||||
} else {
|
||||
row.price = Number(u?.price ?? row.price ?? 0)
|
||||
}
|
||||
if (form.customer) void fetchQuote(p)
|
||||
}
|
||||
|
||||
async function fetchQuote(p) {
|
||||
if (!form.customer) return
|
||||
const row = lineOf(p)
|
||||
try {
|
||||
const data = await api.get('/partner/price-quote/', {
|
||||
params: { customer_id: form.customer, product_ids: p.id },
|
||||
})
|
||||
const q = (data.results || [])[0]
|
||||
if (!q) return
|
||||
row.source = q.source || ''
|
||||
row.baseQuotePrice = Number(q.price)
|
||||
row.price = Number(q.price) * unitRate({ source_unit: row.unitId, units: row.units })
|
||||
} catch (e) {
|
||||
/* 取价失败不阻断开单 */
|
||||
}
|
||||
}
|
||||
|
||||
async function onCustomerChange() {
|
||||
credit.value = {}
|
||||
if (!form.customer) return
|
||||
try {
|
||||
credit.value = await api.get(`/partner/credit-usage/${form.customer}/`)
|
||||
} catch (e) {
|
||||
/* 无额度信息不阻断 */
|
||||
}
|
||||
for (const p of visibleProducts.value) {
|
||||
if (lineState[p.id]) await fetchQuote(p)
|
||||
}
|
||||
}
|
||||
|
||||
function filterProducts() {
|
||||
const kw = search.value.trim().toLowerCase()
|
||||
if (!kw) { visibleProducts.value = products.value; return }
|
||||
visibleProducts.value = products.value.filter(
|
||||
(p) => (p.name || '').toLowerCase().includes(kw) || (p.code || '').toLowerCase().includes(kw),
|
||||
)
|
||||
}
|
||||
|
||||
function addLine(p) {
|
||||
const row = lineOf(p)
|
||||
if (!row.qty || row.qty <= 0) { ElMessage.warning('请填写数量'); return }
|
||||
order.value.push({
|
||||
product: p.id, name: p.name, qty: row.qty,
|
||||
unitId: row.unitId, unitName: row.unitName, units: row.units,
|
||||
price: row.price, taxRate: row.taxRate,
|
||||
})
|
||||
row.qty = 0
|
||||
ElMessage.success(`已加入:${p.name}`)
|
||||
}
|
||||
|
||||
function lineTax(p) {
|
||||
const row = lineOf(p)
|
||||
const amt = Number(row.qty || 0) * Number(row.price || 0)
|
||||
const rate = Number(row.taxRate || 0)
|
||||
if (!rate || amt <= 0) return '0.00'
|
||||
return (amt - amt / (1 + rate)).toFixed(2)
|
||||
}
|
||||
|
||||
function rowAmount(r) {
|
||||
return lineAmount({ quantity: r.qty, unit_price: r.price })
|
||||
}
|
||||
const linesTotal = computed(() => order.value.reduce((s, r) => s + rowAmount(r), 0))
|
||||
const taxTotal = computed(() =>
|
||||
order.value.reduce((s, r) => {
|
||||
const amt = rowAmount(r)
|
||||
const rate = Number(r.taxRate || 0)
|
||||
return s + (rate > 0 && amt > 0 ? amt - amt / (1 + rate) : 0)
|
||||
}, 0),
|
||||
)
|
||||
const roundOffAmount = computed(() => {
|
||||
const rt = Number(form.round_to || 0)
|
||||
if (!rt) return 0
|
||||
const total = linesTotal.value
|
||||
return Number((total - Math.floor(total / rt) * rt).toFixed(4))
|
||||
})
|
||||
const netTotal = computed(() => linesTotal.value - roundOffAmount.value)
|
||||
|
||||
const creditPercent = computed(() => {
|
||||
const limit = Number(credit.value.limit || 0)
|
||||
if (!limit) return 0
|
||||
return Math.min(100, Math.round(Number(credit.value.outstanding || 0) / limit * 100))
|
||||
})
|
||||
const creditStatus = computed(() => {
|
||||
if (creditPercent.value >= 100) return 'exception'
|
||||
if (creditPercent.value >= 80) return 'warning'
|
||||
return 'success'
|
||||
})
|
||||
|
||||
async function submitBill() {
|
||||
if (!form.customer) { ElMessage.warning('请选择客户'); return }
|
||||
if (!form.warehouse) { ElMessage.warning('请选择仓库'); return }
|
||||
const badQty = order.value.filter((r) => !validateQuantity({ quantity: r.qty }))
|
||||
if (badQty.length) { ElMessage.error('有明细数量不合法(需 ≥0.001 且 ≤10 亿)'); return }
|
||||
const badPrice = order.value.filter((r) => !validatePrice({ unit_price: r.price }))
|
||||
if (badPrice.length) { ElMessage.error('有明细单价不合法(必须大于 0 且不超过 10 亿)'); return }
|
||||
const payload = {
|
||||
customer: form.customer,
|
||||
warehouse: form.warehouse,
|
||||
round_to: form.round_to || undefined,
|
||||
lines: order.value.map((r) => ({
|
||||
product: r.product, quantity: r.qty, unit_price: r.price,
|
||||
source_unit: r.unitId || undefined,
|
||||
})),
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
const bill = await api.post('/sales/bills/create-bill/', payload)
|
||||
try {
|
||||
await api.post(`/sales/bills/${bill.id}/confirm/`, { force: false })
|
||||
ElMessage.success(`${bill.bill_no || '销售单'} 已过账`)
|
||||
} catch (err) {
|
||||
const d = err.response?.data || {}
|
||||
if (err.response?.status === 402 || d.code === 'credit_limit_exceeded') {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`${d.detail}。是否强制过账(将记录超限预警)?`,
|
||||
'信用额度超限', {
|
||||
type: 'error', confirmButtonText: '强制过账', cancelButtonText: '取消',
|
||||
},
|
||||
)
|
||||
} catch (e) { await loadDebts(); return }
|
||||
await api.post(`/sales/bills/${bill.id}/confirm/`, { force: true })
|
||||
ElMessage.success(`${bill.bill_no || '销售单'} 已强制过账`)
|
||||
} else {
|
||||
ElMessage.warning(`草稿已保存(${bill.bill_no || ''}),过账失败:${d.detail || '库存不足/其他'}`)
|
||||
}
|
||||
}
|
||||
order.value = []
|
||||
form.round_to = ''
|
||||
await loadDebts()
|
||||
} catch (e) {
|
||||
/* 拦截器已提示 */
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 查欠款 ----
|
||||
const debts = ref([])
|
||||
const debtsLoading = ref(false)
|
||||
const debtCustomer = ref(null)
|
||||
const debtStatusMap = { open: '未收', partial: '部分已收', paid: '已结清', cancelled: '已取消' }
|
||||
const debtStatusText = (s) => debtStatusMap[s] || s
|
||||
const debtTotal = computed(() =>
|
||||
debts.value.filter((d) => d.status !== 'paid')
|
||||
.reduce((s, d) => s + Number(d.balance || 0), 0),
|
||||
)
|
||||
|
||||
async function loadDebts() {
|
||||
debtsLoading.value = true
|
||||
try {
|
||||
const params = { page_size: 50 }
|
||||
if (debtCustomer.value) params.customer = debtCustomer.value
|
||||
// 后端 Receivables 按 search/status 过滤,无 customer 精确过滤时前端按客户筛
|
||||
const data = await api.get('/finance/receivables/', { params })
|
||||
let rows = data.results || []
|
||||
// 只要未结清
|
||||
rows = rows.filter((r) => r.status === 'open' || r.status === 'partial')
|
||||
if (debtCustomer.value) rows = rows.filter((r) => r.customer === debtCustomer.value)
|
||||
debts.value = rows
|
||||
} catch (e) {
|
||||
debts.value = []
|
||||
} finally {
|
||||
debtsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 收款登记 ----
|
||||
const receiptRef = ref(null)
|
||||
const receiptSaving = ref(false)
|
||||
const receipt = reactive({
|
||||
customer: null, bill_no: '', bill_date: '', amount: null, method: '', remark: '',
|
||||
})
|
||||
const receiptRules = {
|
||||
customer: [{ required: true, message: '请选择客户', trigger: 'change' }],
|
||||
bill_no: [{ required: true, message: '请填写收款单号', trigger: 'blur' }],
|
||||
bill_date: [{ required: true, message: '请选择收款日期', trigger: 'change' }],
|
||||
amount: [
|
||||
{ required: true, message: '请输入收款金额', trigger: 'blur' },
|
||||
{ validator: (r, v, cb) => (Number(v) > 0 ? cb() : cb(new Error('收款金额必须大于 0'))), trigger: 'blur' },
|
||||
],
|
||||
}
|
||||
|
||||
function today() {
|
||||
return new Date().toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
async function saveReceipt() {
|
||||
const ok = await receiptRef.value?.validate().catch(() => false)
|
||||
if (!ok) return
|
||||
receiptSaving.value = true
|
||||
try {
|
||||
const payload = {
|
||||
customer: receipt.customer,
|
||||
bill_no: receipt.bill_no.trim(),
|
||||
bill_date: receipt.bill_date,
|
||||
amount: String(receipt.amount),
|
||||
method: receipt.method || '',
|
||||
remark: receipt.remark || '',
|
||||
}
|
||||
await api.post('/finance/receipts/', payload)
|
||||
ElMessage.success('收款单已登记')
|
||||
receipt.bill_no = ''
|
||||
receipt.amount = null
|
||||
receipt.remark = ''
|
||||
await loadDebts()
|
||||
} catch (e) {
|
||||
const d = e.response?.data || {}
|
||||
const first = d.customer || d.bill_no || d.bill_date || d.amount || d.detail
|
||||
if (first) ElMessage.error(`登记失败:${Array.isArray(first) ? first[0] : first}`)
|
||||
} finally {
|
||||
receiptSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRefs() {
|
||||
const [c, w, p] = await Promise.all([
|
||||
api.get('/partner/customers/', { params: { page_size: 200 } }),
|
||||
api.get('/inventory/warehouses/', { params: { page_size: 200 } }),
|
||||
api.get('/catalog/products/', { params: { page_size: 200 } }),
|
||||
])
|
||||
customers.value = c.results || []
|
||||
warehouses.value = w.results || []
|
||||
products.value = p.results || []
|
||||
visibleProducts.value = products.value
|
||||
if (!form.warehouse && warehouses.value.length) {
|
||||
form.warehouse = warehouses.value[0].id
|
||||
}
|
||||
if (!receipt.bill_date) receipt.bill_date = today()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (token.value) {
|
||||
try {
|
||||
await loadRefs()
|
||||
await loadDebts()
|
||||
} catch (e) {
|
||||
// token 失效由拦截器踢回登录
|
||||
}
|
||||
} else if (!receipt.bill_date) {
|
||||
receipt.bill_date = today()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sm-wrap { max-width: 560px; margin: 0 auto; padding: 12px; }
|
||||
.sm-login { padding-top: 40px; }
|
||||
.sm-login h2 { margin: 0 0 6px; font-size: 22px; }
|
||||
.sm-sub { color: #909399; font-size: 13px; margin: 0 0 18px; }
|
||||
.sm-head {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding-bottom: 10px; border-bottom: 1px solid #ebeef5; margin-bottom: 10px;
|
||||
}
|
||||
.sm-shop { font-size: 17px; font-weight: 600; }
|
||||
.sm-search { margin: 10px 0; }
|
||||
.sm-list { margin-top: 10px; }
|
||||
.sm-item, .sm-debt {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 10px; padding: 12px 10px; border: 1px solid #ebeef5;
|
||||
border-radius: 8px; margin-bottom: 10px; background: #fff;
|
||||
}
|
||||
.sm-item-main { flex: 1; min-width: 0; }
|
||||
.sm-name { font-size: 15px; font-weight: 600; margin-bottom: 4px; }
|
||||
.sm-meta { color: #909399; font-size: 12px; margin-bottom: 4px; }
|
||||
.sm-price { color: #d4380d; font-weight: 700; font-size: 15px; }
|
||||
.sm-price-unit { color: #909399; font-size: 12px; font-weight: 400; }
|
||||
.sm-tax, .sm-tax-total { color: #909399; font-size: 12px; }
|
||||
.sm-item-ops { display: flex; align-items: center; gap: 6px; }
|
||||
.sm-order { margin-top: 12px; border-top: 1px solid #ebeef5; padding-top: 10px; }
|
||||
.sm-order-title { font-weight: 600; margin-bottom: 6px; }
|
||||
.sm-order-row {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 8px; padding: 6px 0; font-size: 13px;
|
||||
}
|
||||
.sm-order-name { flex: 1; min-width: 0; }
|
||||
.sm-totals {
|
||||
display: flex; gap: 10px; justify-content: flex-end; align-items: center;
|
||||
padding: 8px 0; font-size: 13px; flex-wrap: wrap;
|
||||
}
|
||||
.sm-net { font-size: 16px; color: #d4380d; }
|
||||
.sm-round { color: #e6a23c; }
|
||||
.sm-debt-sum { padding: 8px 2px; font-size: 14px; }
|
||||
.sm-debt-sum b { color: #d4380d; font-size: 17px; }
|
||||
.credit-bar {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 8px 10px; background: #fafbfc;
|
||||
border: 1px solid #e4e7ed; border-radius: 6px; margin-bottom: 6px;
|
||||
}
|
||||
.credit-label { font-size: 12px; color: #606266; white-space: nowrap; }
|
||||
@media (max-width: 480px) {
|
||||
.sm-item { flex-direction: column; align-items: stretch; }
|
||||
.sm-item-ops { justify-content: flex-end; }
|
||||
}
|
||||
</style>
|
||||
@@ -5,6 +5,7 @@ const routes = [
|
||||
{ path: '/login', name: 'login', component: () => import('../pages/Login.vue') },
|
||||
{ path: '/register', name: 'register', component: () => import('../pages/Register.vue') },
|
||||
{ path: '/storefront', name: 'storefront', component: () => import('../pages/Storefront.vue'), meta: { title: '订货商城' } },
|
||||
{ path: '/salesman', name: 'salesman', component: () => import('../pages/Salesman.vue'), meta: { title: '业务员开单' } },
|
||||
{ path: '/storefront-admin', name: 'storefrontAdmin', component: () => import('../pages/StorefrontAdmin.vue'), meta: { title: '商城订单' } },
|
||||
{
|
||||
path: '/',
|
||||
@@ -40,7 +41,7 @@ const routes = [
|
||||
|
||||
const router = createRouter({ history: createWebHashHistory(), routes })
|
||||
|
||||
const PUBLIC_ROUTES = new Set(['login', 'register', 'storefront'])
|
||||
const PUBLIC_ROUTES = new Set(['login', 'register', 'storefront', 'salesman'])
|
||||
|
||||
router.beforeEach((to) => {
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
Reference in New Issue
Block a user