诚实性修复: API限流Retry-After(wait=60s)+断言 / purge定时接线(04:00 cron+actor+测试) / Channel表单校验(ref+rules+validate) / README PG数字513
This commit is contained in:
@@ -44,8 +44,8 @@
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialogVisible" title="接入新店铺" width="480px">
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-form-item label="平台">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="平台" prop="platform">
|
||||
<el-select v-model="form.platform" style="width: 100%">
|
||||
<el-option label="抖音电商 / 抖店" value="douyin" />
|
||||
<el-option label="拼多多" value="pinduoduo" />
|
||||
@@ -54,9 +54,9 @@
|
||||
<el-option label="自定义开放接口" value="custom" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="店铺ID"><el-input v-model="form.shop_id" /></el-form-item>
|
||||
<el-form-item label="店铺名称"><el-input v-model="form.shop_name" /></el-form-item>
|
||||
<el-form-item label="App Key"><el-input v-model="form.app_key" placeholder="留空则使用模拟数据" /></el-form-item>
|
||||
<el-form-item label="店铺ID" prop="shop_id"><el-input v-model="form.shop_id" /></el-form-item>
|
||||
<el-form-item label="店铺名称" prop="shop_name"><el-input v-model="form.shop_name" /></el-form-item>
|
||||
<el-form-item label="App Key" prop="app_key"><el-input v-model="form.app_key" placeholder="留空则使用模拟数据" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
@@ -78,7 +78,17 @@ const ordersLoading = ref(false)
|
||||
const syncingId = ref(null)
|
||||
const saving = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const formRef = ref(null)
|
||||
const form = reactive({ platform: 'douyin', shop_id: '', shop_name: '', app_key: '' })
|
||||
// P1-2:与 validate.js 同口径(平台/店铺ID 必填;App Key 留空合法=走 Mock,不拦)
|
||||
const rules = {
|
||||
platform: [{ required: true, message: '请选择平台', trigger: 'change' }],
|
||||
shop_id: [
|
||||
{ required: true, message: '请输入店铺ID', trigger: 'blur' },
|
||||
{ max: 64, message: '店铺ID最多 64 字符', trigger: 'blur' },
|
||||
],
|
||||
shop_name: [{ required: true, message: '请输入店铺名称', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
@@ -103,6 +113,8 @@ function openCreate() {
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const ok = await formRef.value.validate().catch(() => false)
|
||||
if (!ok) return
|
||||
saving.value = true
|
||||
try {
|
||||
await api.post('/channel/accounts/', form)
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
🎬 免注册体验演示账套
|
||||
</el-button>
|
||||
<div class="demo-hint">演示账套为只读模式,可试用全部查询、报表与 AI 功能</div>
|
||||
<div class="back-link"><router-link to="/register">没有账号?免费注册开通账套</router-link></div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
@@ -107,4 +108,5 @@ async function enterDemo() {
|
||||
margin-top: 8px; text-align: center;
|
||||
color: #c0c4cc; font-size: 12px;
|
||||
}
|
||||
.back-link { margin-top: 10px; text-align: center; }
|
||||
</style>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<el-option label="已结清" value="paid" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<el-button type="success" @click="openPayment">登记付款</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="rows" v-loading="loading" size="small" stripe>
|
||||
@@ -33,6 +34,11 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" :disabled="row.status === 'paid'" @click="openAllocate(row)">核销</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
@@ -43,11 +49,66 @@
|
||||
v-model:current-page="page"
|
||||
@current-change="load"
|
||||
/>
|
||||
|
||||
<el-dialog v-model="paymentVisible" title="登记付款" width="480px">
|
||||
<el-form ref="paymentFormRef" :model="paymentForm" :rules="paymentRules" label-width="90px">
|
||||
<el-form-item label="供应商" prop="supplier">
|
||||
<el-select v-model="paymentForm.supplier" filterable placeholder="选择供应商" style="width: 100%">
|
||||
<el-option v-for="s in suppliers" :key="s.id" :label="`${s.code} · ${s.name}`" :value="s.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="单据编号" prop="bill_no">
|
||||
<el-input v-model="paymentForm.bill_no" placeholder="留空自动生成" maxlength="64" />
|
||||
</el-form-item>
|
||||
<el-form-item label="付款日期" prop="bill_date">
|
||||
<el-date-picker v-model="paymentForm.bill_date" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="付款金额" prop="amount">
|
||||
<el-input-number v-model="paymentForm.amount" :precision="2" :min="0" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="方式" prop="method">
|
||||
<el-select v-model="paymentForm.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="备注" prop="remark"><el-input v-model="paymentForm.remark" maxlength="500" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="paymentVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="paymentSaving" @click="savePayment">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="allocVisible" :title="`核销应付单 ${allocTarget?.bill_no || ''}`" width="480px">
|
||||
<el-form ref="allocFormRef" :model="allocForm" :rules="allocRules" label-width="90px">
|
||||
<el-form-item label="付款单" prop="payment">
|
||||
<el-select v-model="allocForm.payment" filterable placeholder="选择草稿付款单" style="width: 100%">
|
||||
<el-option
|
||||
v-for="p in payments" :key="p.id"
|
||||
:label="`${p.bill_no} · ¥${p.amount}(${p.bill_date})`"
|
||||
:value="p.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="核销金额" prop="amount">
|
||||
<el-input-number v-model="allocForm.amount" :precision="2" :min="0" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<div v-if="allocTarget" class="hint">应付未结余额 ¥{{ allocTarget.balance }};超额核销会被后端拒绝(400)。</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="allocVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="allocSaving" @click="saveAllocate">确认核销</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import api from '../api/client'
|
||||
|
||||
const rows = ref([])
|
||||
@@ -74,10 +135,115 @@ async function load() {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 登记付款 ----
|
||||
const paymentVisible = ref(false)
|
||||
const paymentSaving = ref(false)
|
||||
const paymentFormRef = ref(null)
|
||||
const paymentForm = ref({ supplier: null, bill_no: '', bill_date: '', amount: null, method: '', remark: '' })
|
||||
const paymentRules = {
|
||||
supplier: [{ required: true, message: '请选择供应商', trigger: 'change' }],
|
||||
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' },
|
||||
],
|
||||
}
|
||||
const suppliers = ref([])
|
||||
|
||||
function today() {
|
||||
return new Date().toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
async function openPayment() {
|
||||
paymentForm.value = { supplier: null, bill_no: '', bill_date: today(), amount: null, method: '', remark: '' }
|
||||
if (!suppliers.value.length) {
|
||||
try {
|
||||
const s = await api.get('/partner/suppliers/', { params: { page_size: 200 } })
|
||||
suppliers.value = s.results || []
|
||||
} catch (e) { /* 拦截器已提示 */ }
|
||||
}
|
||||
paymentVisible.value = true
|
||||
}
|
||||
|
||||
async function savePayment() {
|
||||
const ok = await paymentFormRef.value.validate().catch(() => false)
|
||||
if (!ok) return
|
||||
paymentSaving.value = true
|
||||
try {
|
||||
const payload = {
|
||||
supplier: paymentForm.value.supplier,
|
||||
bill_date: paymentForm.value.bill_date,
|
||||
amount: String(paymentForm.value.amount),
|
||||
method: paymentForm.value.method || '',
|
||||
remark: paymentForm.value.remark || '',
|
||||
}
|
||||
if (paymentForm.value.bill_no.trim()) payload.bill_no = paymentForm.value.bill_no.trim()
|
||||
await api.post('/finance/payments/', payload)
|
||||
ElMessage.success('付款单已登记')
|
||||
paymentVisible.value = false
|
||||
await load()
|
||||
} catch (e) {
|
||||
const d = e.response?.data || {}
|
||||
const first = d.supplier || d.bill_no || d.bill_date || d.amount || d.detail
|
||||
if (first) ElMessage.error(`登记失败:${Array.isArray(first) ? first[0] : first}`)
|
||||
} finally {
|
||||
paymentSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 核销:把一张付款单的金额核到当前应付单 ----
|
||||
const allocVisible = ref(false)
|
||||
const allocSaving = ref(false)
|
||||
const allocFormRef = ref(null)
|
||||
const allocTarget = ref(null)
|
||||
const allocForm = ref({ payment: null, amount: null })
|
||||
const allocRules = {
|
||||
payment: [{ 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' },
|
||||
],
|
||||
}
|
||||
const payments = ref([])
|
||||
|
||||
async function openAllocate(row) {
|
||||
allocTarget.value = row
|
||||
allocForm.value = { payment: null, amount: Number(row.balance) > 0 ? row.balance : null }
|
||||
try {
|
||||
const r = await api.get('/finance/payments/', {
|
||||
params: { page_size: 200, supplier: row.supplier, status: 'draft' },
|
||||
})
|
||||
payments.value = r.results || []
|
||||
} catch (e) { payments.value = []; /* 拦截器已提示 */ }
|
||||
allocVisible.value = true
|
||||
}
|
||||
|
||||
async function saveAllocate() {
|
||||
const ok = await allocFormRef.value.validate().catch(() => false)
|
||||
if (!ok) return
|
||||
allocSaving.value = true
|
||||
try {
|
||||
const data = await api.post(`/finance/payments/${allocForm.value.payment}/allocate/`, {
|
||||
allocations: [{ payable: allocTarget.value.id, amount: String(allocForm.value.amount) }],
|
||||
})
|
||||
const left = data.payment ? `,付款单剩余 ${data.payment.amount}` : ''
|
||||
ElMessage.success(`核销成功${left}`)
|
||||
allocVisible.value = false
|
||||
await load()
|
||||
} catch (e) {
|
||||
const d = e.response?.data || {}
|
||||
const first = d.allocations || d.detail
|
||||
if (first) ElMessage.error(`核销失败:${Array.isArray(first) ? first[0] : first}`)
|
||||
} finally {
|
||||
allocSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar { display: flex; gap: 10px; margin-bottom: 14px; }
|
||||
.danger { color: #f56c6c; font-weight: 600; }
|
||||
.hint { margin-top: 4px; color: #909399; font-size: 12px; }
|
||||
</style>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<el-option label="已结清" value="paid" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<el-button type="success" @click="openReceipt">登记收款</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="rows" v-loading="loading" size="small" stripe>
|
||||
@@ -33,6 +34,11 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" :disabled="row.status === 'paid'" @click="openAllocate(row)">核销</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
@@ -43,11 +49,66 @@
|
||||
v-model:current-page="page"
|
||||
@current-change="load"
|
||||
/>
|
||||
|
||||
<el-dialog v-model="receiptVisible" title="登记收款" width="480px">
|
||||
<el-form ref="receiptFormRef" :model="receiptForm" :rules="receiptRules" label-width="90px">
|
||||
<el-form-item label="客户" prop="customer">
|
||||
<el-select v-model="receiptForm.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">
|
||||
<el-input v-model="receiptForm.bill_no" placeholder="留空自动生成" maxlength="64" />
|
||||
</el-form-item>
|
||||
<el-form-item label="收款日期" prop="bill_date">
|
||||
<el-date-picker v-model="receiptForm.bill_date" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="收款金额" prop="amount">
|
||||
<el-input-number v-model="receiptForm.amount" :precision="2" :min="0" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="方式" prop="method">
|
||||
<el-select v-model="receiptForm.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="备注" prop="remark"><el-input v-model="receiptForm.remark" maxlength="500" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="receiptVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="receiptSaving" @click="saveReceipt">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="allocVisible" :title="`核销应收单 ${allocTarget?.bill_no || ''}`" width="480px">
|
||||
<el-form ref="allocFormRef" :model="allocForm" :rules="allocRules" label-width="90px">
|
||||
<el-form-item label="收款单" prop="receipt">
|
||||
<el-select v-model="allocForm.receipt" filterable placeholder="选择草稿收款单" style="width: 100%">
|
||||
<el-option
|
||||
v-for="r in receipts" :key="r.id"
|
||||
:label="`${r.bill_no} · ¥${r.amount}(${r.bill_date})`"
|
||||
:value="r.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="核销金额" prop="amount">
|
||||
<el-input-number v-model="allocForm.amount" :precision="2" :min="0" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<div v-if="allocTarget" class="hint">应收未结余额 ¥{{ allocTarget.balance }};超额核销会被后端拒绝(400)。</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="allocVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="allocSaving" @click="saveAllocate">确认核销</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import api from '../api/client'
|
||||
|
||||
const rows = ref([])
|
||||
@@ -74,10 +135,115 @@ async function load() {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 登记收款:建收款单(后端自动处理 bill_no 唯一/租户隔离,错误由拦截器+本地透出) ----
|
||||
const receiptVisible = ref(false)
|
||||
const receiptSaving = ref(false)
|
||||
const receiptFormRef = ref(null)
|
||||
const receiptForm = ref({ customer: null, bill_no: '', bill_date: '', amount: null, method: '', remark: '' })
|
||||
const receiptRules = {
|
||||
customer: [{ required: true, message: '请选择客户', trigger: 'change' }],
|
||||
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' },
|
||||
],
|
||||
}
|
||||
const customers = ref([])
|
||||
|
||||
function today() {
|
||||
return new Date().toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
async function openReceipt() {
|
||||
receiptForm.value = { customer: null, bill_no: '', bill_date: today(), amount: null, method: '', remark: '' }
|
||||
if (!customers.value.length) {
|
||||
try {
|
||||
const c = await api.get('/partner/customers/', { params: { page_size: 200 } })
|
||||
customers.value = c.results || []
|
||||
} catch (e) { /* 拦截器已提示 */ }
|
||||
}
|
||||
receiptVisible.value = true
|
||||
}
|
||||
|
||||
async function saveReceipt() {
|
||||
const ok = await receiptFormRef.value.validate().catch(() => false)
|
||||
if (!ok) return
|
||||
receiptSaving.value = true
|
||||
try {
|
||||
const payload = {
|
||||
customer: receiptForm.value.customer,
|
||||
bill_date: receiptForm.value.bill_date,
|
||||
amount: String(receiptForm.value.amount),
|
||||
method: receiptForm.value.method || '',
|
||||
remark: receiptForm.value.remark || '',
|
||||
}
|
||||
if (receiptForm.value.bill_no.trim()) payload.bill_no = receiptForm.value.bill_no.trim()
|
||||
await api.post('/finance/receipts/', payload)
|
||||
ElMessage.success('收款单已登记')
|
||||
receiptVisible.value = false
|
||||
await load()
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 核销:把一张收款单的金额核到当前应收单(可部分核销) ----
|
||||
const allocVisible = ref(false)
|
||||
const allocSaving = ref(false)
|
||||
const allocFormRef = ref(null)
|
||||
const allocTarget = ref(null)
|
||||
const allocForm = ref({ receipt: null, amount: null })
|
||||
const allocRules = {
|
||||
receipt: [{ 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' },
|
||||
],
|
||||
}
|
||||
const receipts = ref([])
|
||||
|
||||
async function openAllocate(row) {
|
||||
allocTarget.value = row
|
||||
allocForm.value = { receipt: null, amount: Number(row.balance) > 0 ? row.balance : null }
|
||||
try {
|
||||
const r = await api.get('/finance/receipts/', {
|
||||
params: { page_size: 200, customer: row.customer, status: 'draft' },
|
||||
})
|
||||
receipts.value = r.results || []
|
||||
} catch (e) { receipts.value = []; /* 拦截器已提示 */ }
|
||||
allocVisible.value = true
|
||||
}
|
||||
|
||||
async function saveAllocate() {
|
||||
const ok = await allocFormRef.value.validate().catch(() => false)
|
||||
if (!ok) return
|
||||
allocSaving.value = true
|
||||
try {
|
||||
const data = await api.post(`/finance/receipts/${allocForm.value.receipt}/allocate/`, {
|
||||
allocations: [{ receivable: allocTarget.value.id, amount: String(allocForm.value.amount) }],
|
||||
})
|
||||
const left = data.receipt ? `,收款单剩余 ${data.receipt.amount}` : ''
|
||||
ElMessage.success(`核销成功${left}`)
|
||||
allocVisible.value = false
|
||||
await load()
|
||||
} catch (e) {
|
||||
const d = e.response?.data || {}
|
||||
const first = d.allocations || d.detail
|
||||
if (first) ElMessage.error(`核销失败:${Array.isArray(first) ? first[0] : first}`)
|
||||
} finally {
|
||||
allocSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar { display: flex; gap: 10px; margin-bottom: 14px; }
|
||||
.danger { color: #f56c6c; font-weight: 600; }
|
||||
.hint { margin-top: 4px; color: #909399; font-size: 12px; }
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<div class="login-wrap">
|
||||
<el-card class="login-card">
|
||||
<h2 class="title">免费注册 · 开通你的账套</h2>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" @keyup.enter="submit">
|
||||
<el-form-item label="公司名称" prop="company_name">
|
||||
<el-input v-model="form.company_name" placeholder="如:海鸥贸易" />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="form.username" placeholder="登录用,注册后不可改" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号(可选)" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="13800000000" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input v-model="form.password" type="password" show-password placeholder="至少 8 位" />
|
||||
</el-form-item>
|
||||
<el-form-item label="确认密码" prop="confirm">
|
||||
<el-input v-model="form.confirm" type="password" show-password placeholder="再输一次" />
|
||||
</el-form-item>
|
||||
<el-button type="primary" style="width: 100%" :loading="loading" @click="submit">
|
||||
注 册 并 进 入
|
||||
</el-button>
|
||||
<div class="back-link"><router-link to="/login">已有账号?去登录</router-link></div>
|
||||
<div class="demo-hint">注册即开通免费版账套,成为账套所有者</div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import axios from 'axios'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const formRef = ref(null)
|
||||
const form = reactive({
|
||||
company_name: '',
|
||||
username: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
confirm: '',
|
||||
})
|
||||
|
||||
const rules = {
|
||||
company_name: [
|
||||
{ required: true, message: '请输入公司名称', trigger: 'blur' },
|
||||
{ min: 2, message: '公司名称至少 2 个字符', trigger: 'blur' },
|
||||
],
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 2, message: '用户名至少 2 个字符', trigger: 'blur' },
|
||||
],
|
||||
phone: [
|
||||
{
|
||||
validator: (rule, value, cb) => {
|
||||
if (!value) return cb()
|
||||
return /^1\d{10}$/.test(value.trim())
|
||||
? cb()
|
||||
: cb(new Error('手机号格式不正确'))
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 8, message: '密码至少 8 位', trigger: 'blur' },
|
||||
],
|
||||
confirm: [
|
||||
{ required: true, message: '请再输一次密码', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value, cb) =>
|
||||
value === form.password ? cb() : cb(new Error('两次输入的密码不一致')),
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
const ok = await formRef.value.validate().catch(() => false)
|
||||
if (!ok) return
|
||||
loading.value = true
|
||||
try {
|
||||
const { data } = await axios.post('/api/v1/auth/register/', {
|
||||
username: form.username.trim(),
|
||||
password: form.password,
|
||||
company_name: form.company_name.trim(),
|
||||
phone: form.phone.trim(),
|
||||
})
|
||||
localStorage.setItem('token', data.access)
|
||||
localStorage.setItem('refresh', data.refresh)
|
||||
localStorage.setItem('username', data.username)
|
||||
localStorage.setItem('tenant', data.tenant)
|
||||
localStorage.removeItem('readonly')
|
||||
localStorage.removeItem('sf_token')
|
||||
localStorage.removeItem('sf_customer')
|
||||
localStorage.removeItem('sf_shop')
|
||||
ElMessage.success(`注册成功,已为「${data.tenant}」开通账套`)
|
||||
router.push('/dashboard')
|
||||
} catch (e) {
|
||||
const d = e.response?.data || {}
|
||||
const first = d.username || d.password || d.company_name || d.phone || d.detail
|
||||
ElMessage.error(`注册失败:${Array.isArray(first) ? first[0] : first || '请检查输入'}`)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-wrap {
|
||||
height: 100vh; display: flex; align-items: center; justify-content: center;
|
||||
background: linear-gradient(135deg, #001529 0%, #16325c 100%);
|
||||
}
|
||||
.login-card { width: 400px; padding: 8px 12px; }
|
||||
.title { text-align: center; margin: 4px 0 20px; color: #303133; font-size: 20px; }
|
||||
.back-link { margin-top: 12px; text-align: center; }
|
||||
.demo-hint {
|
||||
margin-top: 8px; text-align: center;
|
||||
color: #c0c4cc; font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -60,11 +60,11 @@
|
||||
|
||||
<!-- 手工制单 -->
|
||||
<el-dialog v-model="dialogVisible" title="手工制单(借贷必须平衡)" width="680px">
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="凭证日期">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
|
||||
<el-form-item label="凭证日期" prop="voucher_date">
|
||||
<el-date-picker v-model="form.voucher_date" type="date" value-format="YYYY-MM-DD" />
|
||||
</el-form-item>
|
||||
<el-form-item label="摘要"><el-input v-model="form.summary" /></el-form-item>
|
||||
<el-form-item label="摘要" prop="summary"><el-input v-model="form.summary" /></el-form-item>
|
||||
<el-form-item label="过账方式">
|
||||
<el-switch v-model="form.auto_post" active-text="创建后立即过账" inactive-text="保存为草稿" />
|
||||
</el-form-item>
|
||||
@@ -107,6 +107,13 @@
|
||||
import { onMounted, ref, reactive, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import api from '../api/client'
|
||||
import { voucherEntryErrors } from '../utils/validate'
|
||||
|
||||
const formRef = ref(null)
|
||||
const rules = {
|
||||
voucher_date: [{ required: true, message: '请选择凭证日期', trigger: 'change' }],
|
||||
summary: [{ required: true, message: '请输入摘要', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
const rows = ref([])
|
||||
const count = ref(0)
|
||||
@@ -154,8 +161,11 @@ function addEntry(side) {
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!balanced.value) {
|
||||
ElMessage.error('借贷不平衡,无法保存')
|
||||
const ok = await formRef.value.validate().catch(() => false)
|
||||
if (!ok) return
|
||||
const entryErrors = voucherEntryErrors(form.entries)
|
||||
if (entryErrors.length) {
|
||||
ElMessage.error(entryErrors[0])
|
||||
return
|
||||
}
|
||||
const payload = {
|
||||
|
||||
@@ -3,6 +3,7 @@ import MainLayout from '../layouts/MainLayout.vue'
|
||||
|
||||
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: '/storefront-admin', name: 'storefrontAdmin', component: () => import('../pages/StorefrontAdmin.vue'), meta: { title: '商城订单' } },
|
||||
{
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// P1-2 · 前端纯函数校验(与 transaction.js 同风格:无框架依赖 + node --test 单测)。
|
||||
//
|
||||
// 注意(迭代 2 教训):Element Plus `el-input-number` 的 `:min` 是自动纠正
|
||||
// 不是校验 —— 非法值会被静默改掉。所以 `:min` 要允许非法值如实呈现,
|
||||
// 由这里的显式校验函数拦截。
|
||||
|
||||
export function required(value) {
|
||||
if (value === null || value === undefined) return false
|
||||
return String(value).trim().length > 0
|
||||
}
|
||||
|
||||
export function positiveAmount(value) {
|
||||
const n = Number(value)
|
||||
return Number.isFinite(n) && n > 0
|
||||
}
|
||||
|
||||
export function nonEmptyArray(value) {
|
||||
return Array.isArray(value) && value.length > 0
|
||||
}
|
||||
|
||||
export function shopId(value) {
|
||||
return required(value) && String(value).trim().length <= 64
|
||||
}
|
||||
|
||||
export function voucherBalanced(entries) {
|
||||
if (!nonEmptyArray(entries)) return false
|
||||
let debit = 0
|
||||
let credit = 0
|
||||
for (const e of entries) {
|
||||
const d = Number(e?.debit || 0)
|
||||
const c = Number(e?.credit || 0)
|
||||
if (!Number.isFinite(d) || !Number.isFinite(c) || d < 0 || c < 0) return false
|
||||
if (d > 0 && c > 0) return false // 同一行借贷不能同时有数
|
||||
const code = String(e?.account_code || '').trim()
|
||||
if (!code) return false
|
||||
debit += d
|
||||
credit += c
|
||||
}
|
||||
return debit > 0 && debit === credit
|
||||
}
|
||||
|
||||
export function voucherEntryErrors(entries) {
|
||||
const errors = []
|
||||
if (!nonEmptyArray(entries)) {
|
||||
errors.push('至少需要一条分录')
|
||||
return errors
|
||||
}
|
||||
entries.forEach((e, i) => {
|
||||
const n = i + 1
|
||||
if (!String(e?.account_code || '').trim()) errors.push(`第 ${n} 行:科目编码必填`)
|
||||
const d = Number(e?.debit || 0)
|
||||
const c = Number(e?.credit || 0)
|
||||
if (d > 0 && c > 0) errors.push(`第 ${n} 行:借方贷方不能同时填写`)
|
||||
if (d < 0 || c < 0) errors.push(`第 ${n} 行:金额不能为负`)
|
||||
if (d === 0 && c === 0) errors.push(`第 ${n} 行:借方贷方至少填一边`)
|
||||
})
|
||||
if (errors.length === 0) {
|
||||
const debit = entries.reduce((s, e) => s + Number(e?.debit || 0), 0)
|
||||
const credit = entries.reduce((s, e) => s + Number(e?.credit || 0), 0)
|
||||
if (debit <= 0) errors.push('借方合计必须大于 0')
|
||||
else if (debit !== credit) errors.push(`借贷不平衡:借 ${debit} ≠ 贷 ${credit}`)
|
||||
}
|
||||
return errors
|
||||
}
|
||||
Reference in New Issue
Block a user