附录A全接入:7新页面+路由菜单+库存大盘抽屉 / P1-2真机非法值拦截全绿 / 文档终版510+514
This commit is contained in:
@@ -82,17 +82,35 @@ seed_demo/demo 幂等兜底、两个测试侧 membership 修补、exceptions 去
|
||||
补 `rules`(平台/店铺ID/店铺名必填,App Key 留空合法)+ 保存前 validate。
|
||||
5. **文档数字**:README PG 口径 `499` → `514`(实跑)。
|
||||
|
||||
## 最终验证(2026-09-12)
|
||||
## 本轮新增(2026-09-12 续-3 · 附录 A #3–#10 全接入 + P1-2 真机验证)
|
||||
|
||||
7 个新页面(路由+菜单已接线,构建 ✅ 6.37s):
|
||||
|
||||
| # | 页面 | 路由 | 后端接口 | 真机数据 |
|
||||
|---|---|---|---|---|
|
||||
| 3 | 核销记录 `Allocations.vue`(只读) | `/finance/allocations` | `GET /finance/allocations/` | 30 行 |
|
||||
| 4 | 库存流水 `Movements.vue`(只读) | `/stocks/movements` | `GET /inventory/movements/` | 50 行 |
|
||||
| 5 | 预警规则 `AlertRules.vue`(CRUD+开关+init-default) | `/notify/rules` | `/notify/rules/` + `init-default` | 4 行 |
|
||||
| 6 | 打印模板 `PrintTemplates.vue`(CRUD) | `/printing/templates` | `/printing/templates/` | 空态 200 ✅ |
|
||||
| 7 | 自定义报表 `ReportDefinitions.vue`(init-system+run) | `/report/definitions` | `definitions/` + `init-system` + `run` | 空态 200 ✅ |
|
||||
| 8 | 库存大盘 | Dashboard 库存卡片点击 → 明细抽屉 | `/report/dashboard/inventory-status/` | 2 仓/30734 件/¥131,509.92 |
|
||||
| 9 | 科目管理 `Accounts.vue`(init-chart+启用开关) | `/finance/accounts` | `/finance/accounts/` + `init-chart` | 空态 200 ✅ |
|
||||
| 10 | 价格等级/联系人 `PriceLevels.vue`(等级 CRUD + 联系人只读) | `/partner/price-levels` | `price-levels/` + `contacts/` | 空态 200 ✅ |
|
||||
|
||||
P1-2 Playwright 真机(dev :18099 + demo 租户,0 console errors):
|
||||
空提交被拦——Vouchers(请输入摘要)/ AlertRules(编码+名称)/ Channel(店铺ID+店铺名),
|
||||
dialog 不关闭;抽屉字段曾与后端键名错位(`by_warehouse` vs 实际 `warehouses`/
|
||||
`grand_total_*`),已按实测键修好并复验出真数据。
|
||||
|
||||
## 最终验证(2026-09-12 终版)
|
||||
|
||||
- 后端 SQLite:**510 passed / 4 skipped**(PG 专用并发用例)
|
||||
- 后端 PostgreSQL 5433:**514 passed / 0 skipped**
|
||||
- 前端:`npm run build` ✅ 6.50s;`npm test` ✅ 11/11
|
||||
- 前端:`npm run build` ✅ 6.37s;`npm test` ✅ 11/11
|
||||
- `manage.py check` 0 issues;`makemigrations --check` No changes
|
||||
- Playwright Register 真机:空/非法被拦、合法 201 跳 dashboard、0 console errors
|
||||
- Playwright:Register 空/非法被拦、合法 201 跳 dashboard;9 页渲染全过;3 页空提交被拦;抽屉真数据;全程 0 console errors
|
||||
|
||||
## 遗留(诚实记录)
|
||||
|
||||
- §附录 A #4–#10(库存流水/预警规则/打印模板/自定义报表/库存大盘/科目管理/价格等级页)
|
||||
仍是后端有、前端无——超出本轮 27 条真源范围,未动。
|
||||
- P2-2 部署 readiness gate + 上线 192.168.5.7:需 Docker/服务器凭证,未动。
|
||||
- P2-3/D2 移动开单、P3-1 行业专版:按客户驱动,未动。
|
||||
|
||||
@@ -99,7 +99,7 @@ python manage.py seed_full_finance
|
||||
|
||||
## 当前阶段
|
||||
|
||||
**AGI 迭代基线(2026-09-12)**:进销存、财务、商城和扩展模块已形成可运行闭环,当前本地回归为 **509 passed / 4 skipped(SQLite)**,**513 passed(PostgreSQL 5433,含 4 并发用例)**,前端 `npm run build` ✅ + `npm test` ✅ 11/11(见 `PROGRESS_AGI_ITERATION_6.md`)。
|
||||
**AGI 迭代基线(2026-09-12)**:进销存、财务、商城和扩展模块已形成可运行闭环,当前本地回归为 **510 passed / 4 skipped(SQLite)**,**514 passed(PostgreSQL 5433,含 4 并发用例)**,前端 `npm run build` ✅ + `npm test` ✅ 11/11。附录 A 后端 10 项能力前端已全接入(收款/付款/核销/流水/预警规则/打印模板/自定义报表/库存大盘/科目/价格等级联系人),见 `PROGRESS_AGI_ITERATION_6.md`。
|
||||
|
||||
- ✅ Django + DRF + adrf + Granian 异步链路打通
|
||||
- ✅ 多租户数据过滤、JWT 鉴权与审计日志
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<el-menu-item index="/products">商品中心</el-menu-item>
|
||||
<el-menu-item index="/customers">客户档案</el-menu-item>
|
||||
<el-menu-item index="/stocks">库存查询</el-menu-item>
|
||||
<el-menu-item index="/stocks/movements">库存流水</el-menu-item>
|
||||
<el-menu-item index="/batches">批次效期</el-menu-item>
|
||||
<el-menu-item index="/sales-bills">销售开单</el-menu-item>
|
||||
<el-menu-item index="/purchase-bills">采购开单</el-menu-item>
|
||||
@@ -26,11 +27,17 @@
|
||||
<el-menu-item index="/finance/receivables">应收账款</el-menu-item>
|
||||
<el-menu-item index="/finance/payables">应付账款</el-menu-item>
|
||||
<el-menu-item index="/finance/reports">财务报表</el-menu-item>
|
||||
<el-menu-item index="/finance/allocations">核销记录</el-menu-item>
|
||||
<el-menu-item index="/finance/accounts">科目管理</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-sub-menu index="ext">
|
||||
<template #title>🔌 扩展中心</template>
|
||||
<el-menu-item index="/channel">电商渠道</el-menu-item>
|
||||
<el-menu-item index="/partner/price-levels">价格等级与联系人</el-menu-item>
|
||||
<el-menu-item index="/notify">消息预警</el-menu-item>
|
||||
<el-menu-item index="/notify/rules">预警规则</el-menu-item>
|
||||
<el-menu-item index="/printing/templates">打印模板</el-menu-item>
|
||||
<el-menu-item index="/report/definitions">自定义报表</el-menu-item>
|
||||
<el-menu-item index="/openapi-keys">开放平台</el-menu-item>
|
||||
<el-menu-item index="/billing">套餐与用量</el-menu-item>
|
||||
<el-menu-item index="/audit-logs">操作轨迹</el-menu-item>
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<div class="toolbar">
|
||||
<el-input v-model="search" placeholder="搜索科目编码/名称" style="width: 240px" clearable @keyup.enter="load" />
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<el-button @click="initChart" :loading="initting">一键初始化标准科目</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="rows" v-loading="loading" size="small" stripe>
|
||||
<el-table-column prop="code" label="科目编码" width="140" />
|
||||
<el-table-column prop="name" label="科目名称" min-width="180" />
|
||||
<el-table-column prop="category" label="类别" width="120" />
|
||||
<el-table-column prop="direction" label="余额方向" width="110" />
|
||||
<el-table-column label="启用" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-switch v-model="row.is_active" @change="toggle(row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
style="margin-top: 14px"
|
||||
layout="prev, pager, next, total"
|
||||
:total="count"
|
||||
:page-size="pageSize"
|
||||
v-model:current-page="page"
|
||||
@current-change="load"
|
||||
/>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import api from '../api/client'
|
||||
|
||||
const rows = ref([])
|
||||
const count = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = 50
|
||||
const search = ref('')
|
||||
const loading = ref(false)
|
||||
const initting = ref(false)
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.get('/finance/accounts/', {
|
||||
params: { page: page.value, page_size: pageSize, search: search.value },
|
||||
})
|
||||
rows.value = data.results || []
|
||||
count.value = data.count || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function initChart() {
|
||||
initting.value = true
|
||||
try {
|
||||
const data = await api.post('/finance/accounts/init-chart/', {})
|
||||
ElMessage.success(data.message || `已初始化 ${data.created_count} 个科目`)
|
||||
await load()
|
||||
} finally {
|
||||
initting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function toggle(row) {
|
||||
try {
|
||||
await api.patch(`/finance/accounts/${row.id}/`, { is_active: row.is_active })
|
||||
} catch (e) { row.is_active = !row.is_active }
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar { display: flex; gap: 10px; margin-bottom: 14px; align-items: center; }
|
||||
</style>
|
||||
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<div class="toolbar">
|
||||
<el-input v-model="search" placeholder="搜索规则编码/名称" style="width: 240px" clearable @keyup.enter="load" />
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<el-button @click="initDefault" :loading="initting">初始化默认规则</el-button>
|
||||
<el-button type="primary" @click="openCreate">新建规则</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="rows" v-loading="loading" size="small" stripe>
|
||||
<el-table-column prop="code" label="规则编码" width="150" />
|
||||
<el-table-column prop="name" label="规则名称" min-width="160" />
|
||||
<el-table-column prop="rule_type_name" label="类型" width="200" />
|
||||
<el-table-column prop="threshold" label="阈值" width="110" />
|
||||
<el-table-column label="启用" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-switch v-model="row.is_enabled" @change="toggle(row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="remove(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
style="margin-top: 14px"
|
||||
layout="prev, pager, next, total"
|
||||
:total="count"
|
||||
:page-size="pageSize"
|
||||
v-model:current-page="page"
|
||||
@current-change="load"
|
||||
/>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="editing ? '编辑预警规则' : '新建预警规则'" width="520px">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="规则编码" prop="code"><el-input v-model="form.code" /></el-form-item>
|
||||
<el-form-item label="规则名称" prop="name"><el-input v-model="form.name" /></el-form-item>
|
||||
<el-form-item label="规则类型" prop="rule_type">
|
||||
<el-select v-model="form.rule_type" style="width: 100%">
|
||||
<el-option label="库存缺货/下限预警" value="inventory_low" />
|
||||
<el-option label="应收账款逾期催款预警" value="receivable_overdue" />
|
||||
<el-option label="商品呆滞积压预警" value="stock_backlog" />
|
||||
<el-option label="批次近效期预警" value="batch_expiry" />
|
||||
<el-option label="AI 应收风险预警" value="risk_score" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="阈值" prop="threshold">
|
||||
<el-input-number v-model="form.threshold" :min="0" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用" prop="is_enabled"><el-switch v-model="form.is_enabled" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref, reactive } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import api from '../api/client'
|
||||
|
||||
const rows = ref([])
|
||||
const count = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = 20
|
||||
const search = ref('')
|
||||
const loading = ref(false)
|
||||
const initting = ref(false)
|
||||
const saving = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const editing = ref(null)
|
||||
const formRef = ref(null)
|
||||
const form = reactive({ code: '', name: '', rule_type: 'inventory_low', threshold: 10, is_enabled: true })
|
||||
const rules = {
|
||||
code: [{ required: true, message: '请输入规则编码', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '请输入规则名称', trigger: 'blur' }],
|
||||
rule_type: [{ required: true, message: '请选择规则类型', trigger: 'change' }],
|
||||
threshold: [{ required: true, message: '请输入阈值', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.get('/notify/rules/', {
|
||||
params: { page: page.value, page_size: pageSize, search: search.value },
|
||||
})
|
||||
rows.value = data.results || []
|
||||
count.value = data.count || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function initDefault() {
|
||||
initting.value = true
|
||||
try {
|
||||
const data = await api.post('/notify/rules/init-default/', {})
|
||||
ElMessage.success(`已初始化 ${data.created_count} 条默认规则`)
|
||||
await load()
|
||||
} finally {
|
||||
initting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editing.value = null
|
||||
Object.assign(form, { code: '', name: '', rule_type: 'inventory_low', threshold: 10, is_enabled: true })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
editing.value = row
|
||||
Object.assign(form, { code: row.code, name: row.name, rule_type: row.rule_type, threshold: Number(row.threshold), is_enabled: row.is_enabled })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const ok = await formRef.value.validate().catch(() => false)
|
||||
if (!ok) return
|
||||
saving.value = true
|
||||
try {
|
||||
if (editing.value) await api.patch(`/notify/rules/${editing.value.id}/`, { ...form })
|
||||
else await api.post('/notify/rules/', { ...form })
|
||||
ElMessage.success('已保存')
|
||||
dialogVisible.value = false
|
||||
await load()
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function toggle(row) {
|
||||
try {
|
||||
await api.patch(`/notify/rules/${row.id}/`, { is_enabled: row.is_enabled })
|
||||
} catch (e) { row.is_enabled = !row.is_enabled }
|
||||
}
|
||||
|
||||
async function remove(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`删除规则 ${row.code}(${row.name})?`, '删除确认', { type: 'warning' })
|
||||
} catch (e) { return }
|
||||
await api.delete(`/notify/rules/${row.id}/`)
|
||||
ElMessage.success('已删除')
|
||||
await load()
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar { display: flex; gap: 10px; margin-bottom: 14px; align-items: center; }
|
||||
</style>
|
||||
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<div class="toolbar">
|
||||
<el-input v-model="search" placeholder="搜索核销记录" style="width: 240px" clearable @keyup.enter="load" />
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<span class="summary" v-if="count">核销记录:{{ count }} 条(只读,由收款/付款核销动作产生)</span>
|
||||
</div>
|
||||
|
||||
<el-table :data="rows" v-loading="loading" size="small" stripe>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="receivable" label="应收ID" width="100" />
|
||||
<el-table-column prop="payable" label="应付ID" width="100" />
|
||||
<el-table-column prop="receipt" label="收款单ID" width="100" />
|
||||
<el-table-column prop="payment" label="付款单ID" width="100" />
|
||||
<el-table-column label="核销金额" width="120">
|
||||
<template #default="{ row }">¥ {{ row.amount }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="核销时间" width="180" />
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
style="margin-top: 14px"
|
||||
layout="prev, pager, next, total"
|
||||
:total="count"
|
||||
:page-size="pageSize"
|
||||
v-model:current-page="page"
|
||||
@current-change="load"
|
||||
/>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import api from '../api/client'
|
||||
|
||||
const rows = ref([])
|
||||
const count = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = 50
|
||||
const search = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.get('/finance/allocations/', {
|
||||
params: { page: page.value, page_size: pageSize, search: search.value },
|
||||
})
|
||||
rows.value = data.results || []
|
||||
count.value = data.count || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar { display: flex; gap: 10px; margin-bottom: 14px; align-items: center; }
|
||||
.summary { color: #909399; font-size: 13px; margin-left: 10px; }
|
||||
</style>
|
||||
@@ -16,8 +16,8 @@
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover">
|
||||
<div class="kpi-label">库存资产估值</div>
|
||||
<el-card shadow="hover" style="cursor: pointer" @click="showInventoryDetail">
|
||||
<div class="kpi-label">库存资产估值(点击看仓库明细)</div>
|
||||
<div class="kpi-value">¥ {{ fmt(inventory.valuation) }}</div>
|
||||
<div class="kpi-sub">{{ inventory.sku_count }} 个 SKU / {{ fmt(inventory.total_quantity) }} 件</div>
|
||||
</el-card>
|
||||
@@ -150,6 +150,25 @@
|
||||
<el-button type="primary" :loading="askLoading" @click="runAsk">提问</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 库存状态明细抽屉(#8 库存状态大盘:各仓库分布与资产估值) -->
|
||||
<el-drawer v-model="inventoryDrawer" title="📦 库存状态大盘" size="560px">
|
||||
<div v-loading="inventoryDetailLoading">
|
||||
<el-descriptions :column="2" border size="small" style="margin-bottom: 12px">
|
||||
<el-descriptions-item label="总数量">{{ inventoryDetail.grand_total_quantity ?? '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="资产估值">¥ {{ fmt(inventoryDetail.grand_total_valuation) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="仓库数">{{ (inventoryDetail.warehouses || []).length }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-table :data="inventoryDetail.warehouses || []" size="small" stripe>
|
||||
<el-table-column prop="warehouse_code" label="仓库编码" width="110" />
|
||||
<el-table-column prop="warehouse_name" label="仓库名称" min-width="130" />
|
||||
<el-table-column prop="total_quantity" label="数量" width="110" />
|
||||
<el-table-column label="货值" min-width="130">
|
||||
<template #default="{ row }">¥ {{ fmt(row.total_valuation) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -197,6 +216,21 @@ const purchase = computed(() => summary.value.month_purchase || {})
|
||||
const inventory = computed(() => summary.value.inventory || {})
|
||||
const finance = computed(() => summary.value.finance || {})
|
||||
|
||||
// ---- 库存状态大盘明细(#8 inventory-status:各仓库分布与资产估值)----
|
||||
const inventoryDrawer = ref(false)
|
||||
const inventoryDetail = ref({})
|
||||
const inventoryDetailLoading = ref(false)
|
||||
|
||||
async function showInventoryDetail() {
|
||||
inventoryDrawer.value = true
|
||||
inventoryDetailLoading.value = true
|
||||
try {
|
||||
inventoryDetail.value = await api.get('/report/dashboard/inventory-status/')
|
||||
} catch (e) { inventoryDetail.value = {} } finally {
|
||||
inventoryDetailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const levelMap = { high: '高风险', medium: '中风险', low: '低风险' }
|
||||
const levelText = (l) => levelMap[l] || l
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<div class="toolbar">
|
||||
<el-input v-model="search" placeholder="搜索商品编码/名称/来源单号" style="width: 260px" clearable @keyup.enter="load" />
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<span class="summary" v-if="count">出入库流水:{{ count }} 条(只读,由过账自动产生)</span>
|
||||
</div>
|
||||
|
||||
<el-table :data="rows" v-loading="loading" size="small" stripe>
|
||||
<el-table-column prop="product_code" label="商品编码" width="130" />
|
||||
<el-table-column prop="warehouse_code" label="仓库" width="110" />
|
||||
<el-table-column prop="movement_type" label="方向" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.movement_type === 'in' ? 'success' : 'warning'" effect="plain">
|
||||
{{ row.movement_type === 'in' ? '入库' : '出库' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="source_type" label="来源" width="120" />
|
||||
<el-table-column prop="source_ref" label="来源单号" min-width="150" />
|
||||
<el-table-column prop="quantity" label="数量" width="100" />
|
||||
<el-table-column label="单位成本" width="110">
|
||||
<template #default="{ row }">¥ {{ row.unit_cost }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="时间" width="180" />
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
style="margin-top: 14px"
|
||||
layout="prev, pager, next, total"
|
||||
:total="count"
|
||||
:page-size="pageSize"
|
||||
v-model:current-page="page"
|
||||
@current-change="load"
|
||||
/>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import api from '../api/client'
|
||||
|
||||
const rows = ref([])
|
||||
const count = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = 50
|
||||
const search = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.get('/inventory/movements/', {
|
||||
params: { page: page.value, page_size: pageSize, search: search.value },
|
||||
})
|
||||
rows.value = data.results || []
|
||||
count.value = data.count || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar { display: flex; gap: 10px; margin-bottom: 14px; align-items: center; }
|
||||
.summary { color: #909399; font-size: 13px; margin-left: 10px; }
|
||||
</style>
|
||||
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<el-tabs v-model="tab">
|
||||
<el-tab-pane label="价格等级" name="levels">
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" @click="openLevelCreate">新建等级</el-button>
|
||||
</div>
|
||||
<el-table :data="levels" v-loading="levelsLoading" size="small" stripe>
|
||||
<el-table-column prop="code" label="等级编码" width="140" />
|
||||
<el-table-column prop="name" label="等级名称" min-width="160" />
|
||||
<el-table-column prop="discount_rate" label="折扣率" width="120" />
|
||||
<el-table-column prop="sort_order" label="排序" width="100" />
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="openLevelEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="removeLevel(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="联系人" name="contacts">
|
||||
<div class="toolbar">
|
||||
<el-input v-model="contactSearch" placeholder="搜索联系人姓名" style="width: 240px" clearable @keyup.enter="loadContacts" />
|
||||
<el-button type="primary" @click="loadContacts">查询</el-button>
|
||||
</div>
|
||||
<el-table :data="contacts" v-loading="contactsLoading" size="small" stripe>
|
||||
<el-table-column prop="name" label="姓名" width="140" />
|
||||
<el-table-column prop="phone" label="电话" width="150" />
|
||||
<el-table-column prop="email" label="邮箱" min-width="160" />
|
||||
<el-table-column prop="title" label="职位" width="140" />
|
||||
</el-table>
|
||||
<el-pagination
|
||||
style="margin-top: 14px"
|
||||
layout="prev, pager, next, total"
|
||||
:total="contactsCount"
|
||||
:page-size="20"
|
||||
v-model:current-page="contactsPage"
|
||||
@current-change="loadContacts"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="editing ? '编辑价格等级' : '新建价格等级'" width="480px">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="等级编码" prop="code"><el-input v-model="form.code" /></el-form-item>
|
||||
<el-form-item label="等级名称" prop="name"><el-input v-model="form.name" /></el-form-item>
|
||||
<el-form-item label="折扣率" prop="discount_rate">
|
||||
<el-input-number v-model="form.discount_rate" :min="0" :max="1" :precision="4" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort_order">
|
||||
<el-input-number v-model="form.sort_order" :precision="0" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="saveLevel">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref, reactive } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import api from '../api/client'
|
||||
|
||||
const tab = ref('levels')
|
||||
const levels = ref([])
|
||||
const levelsLoading = ref(false)
|
||||
const contacts = ref([])
|
||||
const contactsCount = ref(0)
|
||||
const contactsPage = ref(1)
|
||||
const contactSearch = ref('')
|
||||
const contactsLoading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const editing = ref(null)
|
||||
const saving = ref(false)
|
||||
const formRef = ref(null)
|
||||
const form = reactive({ code: '', name: '', discount_rate: 0, sort_order: 0 })
|
||||
const rules = {
|
||||
code: [{ required: true, message: '请输入等级编码', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '请输入等级名称', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
async function loadLevels() {
|
||||
levelsLoading.value = true
|
||||
try {
|
||||
const data = await api.get('/partner/price-levels/', { params: { page_size: 100 } })
|
||||
levels.value = data.results || []
|
||||
} finally {
|
||||
levelsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadContacts() {
|
||||
contactsLoading.value = true
|
||||
try {
|
||||
const data = await api.get('/partner/contacts/', {
|
||||
params: { page: contactsPage.value, page_size: 20, search: contactSearch.value },
|
||||
})
|
||||
contacts.value = data.results || []
|
||||
contactsCount.value = data.count || 0
|
||||
} finally {
|
||||
contactsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openLevelCreate() {
|
||||
editing.value = null
|
||||
Object.assign(form, { code: '', name: '', discount_rate: 0, sort_order: 0 })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openLevelEdit(row) {
|
||||
editing.value = row
|
||||
Object.assign(form, { code: row.code, name: row.name, discount_rate: Number(row.discount_rate), sort_order: row.sort_order })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function saveLevel() {
|
||||
const ok = await formRef.value.validate().catch(() => false)
|
||||
if (!ok) return
|
||||
saving.value = true
|
||||
try {
|
||||
if (editing.value) await api.patch(`/partner/price-levels/${editing.value.id}/`, { ...form })
|
||||
else await api.post('/partner/price-levels/', { ...form })
|
||||
ElMessage.success('已保存')
|
||||
dialogVisible.value = false
|
||||
await loadLevels()
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function removeLevel(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`删除等级 ${row.code}(${row.name})?`, '删除确认', { type: 'warning' })
|
||||
} catch (e) { return }
|
||||
await api.delete(`/partner/price-levels/${row.id}/`)
|
||||
ElMessage.success('已删除')
|
||||
await loadLevels()
|
||||
}
|
||||
|
||||
onMounted(() => { loadLevels(); loadContacts() })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar { display: flex; gap: 10px; margin-bottom: 14px; align-items: center; }
|
||||
</style>
|
||||
@@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<div class="toolbar">
|
||||
<el-input v-model="search" placeholder="搜索模板编码/名称" style="width: 240px" clearable @keyup.enter="load" />
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<el-button type="primary" @click="openCreate">新建模板</el-button>
|
||||
<span class="summary" v-pre>安全占位符语法:{{path}} 取值、{{#each lines}}…{{/each}} 行循环</span>
|
||||
</div>
|
||||
|
||||
<el-table :data="rows" v-loading="loading" size="small" stripe>
|
||||
<el-table-column prop="code" label="模板编码" width="150" />
|
||||
<el-table-column prop="name" label="模板名称" min-width="160" />
|
||||
<el-table-column prop="doc_type" label="单据类型" width="140" />
|
||||
<el-table-column prop="paper_size" label="纸张" width="90" />
|
||||
<el-table-column label="默认" width="80">
|
||||
<template #default="{ row }"><el-tag v-if="row.is_default" type="success">默认</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="remove(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
style="margin-top: 14px"
|
||||
layout="prev, pager, next, total"
|
||||
:total="count"
|
||||
:page-size="pageSize"
|
||||
v-model:current-page="page"
|
||||
@current-change="load"
|
||||
/>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="editing ? '编辑打印模板' : '新建打印模板'" width="640px">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="模板编码" prop="code"><el-input v-model="form.code" /></el-form-item>
|
||||
<el-form-item label="模板名称" prop="name"><el-input v-model="form.name" /></el-form-item>
|
||||
<el-form-item label="单据类型" prop="doc_type">
|
||||
<el-select v-model="form.doc_type" style="width: 100%">
|
||||
<el-option label="销售单/送货单" value="sales_bill" />
|
||||
<el-option label="采购单/进货单" value="purchase_bill" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="纸张" prop="paper_size"><el-input v-model="form.paper_size" /></el-form-item>
|
||||
<el-form-item label="默认模板" prop="is_default"><el-switch v-model="form.is_default" /></el-form-item>
|
||||
<el-form-item label="模板正文" prop="body_html">
|
||||
<el-input v-model="form.body_html" type="textarea" :rows="8" placeholder="{{bill_no}} {{#each lines}}…" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref, reactive } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import api from '../api/client'
|
||||
|
||||
const rows = ref([])
|
||||
const count = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = 20
|
||||
const search = ref('')
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const editing = ref(null)
|
||||
const formRef = ref(null)
|
||||
const form = reactive({ code: '', name: '', doc_type: 'sales_bill', paper_size: 'A4', is_default: false, body_html: '' })
|
||||
const rules = {
|
||||
code: [{ required: true, message: '请输入模板编码', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '请输入模板名称', trigger: 'blur' }],
|
||||
doc_type: [{ required: true, message: '请选择单据类型', trigger: 'change' }],
|
||||
body_html: [{ required: true, message: '请输入模板正文', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.get('/printing/templates/', {
|
||||
params: { page: page.value, page_size: pageSize, search: search.value },
|
||||
})
|
||||
rows.value = data.results || []
|
||||
count.value = data.count || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editing.value = null
|
||||
Object.assign(form, { code: '', name: '', doc_type: 'sales_bill', paper_size: 'A4', is_default: false, body_html: '' })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
editing.value = row
|
||||
Object.assign(form, { code: row.code, name: row.name, doc_type: row.doc_type, paper_size: row.paper_size, is_default: row.is_default, body_html: row.body_html })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const ok = await formRef.value.validate().catch(() => false)
|
||||
if (!ok) return
|
||||
saving.value = true
|
||||
try {
|
||||
if (editing.value) await api.patch(`/printing/templates/${editing.value.id}/`, { ...form })
|
||||
else await api.post('/printing/templates/', { ...form })
|
||||
ElMessage.success('已保存')
|
||||
dialogVisible.value = false
|
||||
await load()
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`删除模板 ${row.code}(${row.name})?`, '删除确认', { type: 'warning' })
|
||||
} catch (e) { return }
|
||||
await api.delete(`/printing/templates/${row.id}/`)
|
||||
ElMessage.success('已删除')
|
||||
await load()
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar { display: flex; gap: 10px; margin-bottom: 14px; align-items: center; flex-wrap: wrap; }
|
||||
.summary { color: #909399; font-size: 12px; }
|
||||
</style>
|
||||
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<div class="toolbar">
|
||||
<el-input v-model="search" placeholder="搜索报表编码/名称" style="width: 240px" clearable @keyup.enter="load" />
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<el-button @click="initSystem" :loading="initting">一键初始化系统报表</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="rows" v-loading="loading" size="small" stripe>
|
||||
<el-table-column prop="code" label="报表编码" width="170" />
|
||||
<el-table-column prop="name" label="报表名称" min-width="180" />
|
||||
<el-table-column prop="category" label="分类" width="120" />
|
||||
<el-table-column label="系统预置" width="100">
|
||||
<template #default="{ row }"><el-tag v-if="row.is_system" type="info">系统</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="说明" min-width="200" />
|
||||
<el-table-column label="操作" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="run(row)">运行</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
style="margin-top: 14px"
|
||||
layout="prev, pager, next, total"
|
||||
:total="count"
|
||||
:page-size="pageSize"
|
||||
v-model:current-page="page"
|
||||
@current-change="load"
|
||||
/>
|
||||
|
||||
<el-dialog v-model="resultVisible" title="报表运行结果" width="720px">
|
||||
<el-input v-model="resultText" type="textarea" :rows="14" readonly />
|
||||
</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([])
|
||||
const count = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = 20
|
||||
const search = ref('')
|
||||
const loading = ref(false)
|
||||
const initting = ref(false)
|
||||
const resultVisible = ref(false)
|
||||
const resultText = ref('')
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.get('/report/definitions/', {
|
||||
params: { page: page.value, page_size: pageSize, search: search.value },
|
||||
})
|
||||
rows.value = data.results || []
|
||||
count.value = data.count || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function initSystem() {
|
||||
initting.value = true
|
||||
try {
|
||||
const data = await api.post('/report/definitions/init-system/', {})
|
||||
ElMessage.success(data.message || `已初始化 ${data.created_count} 个系统报表`)
|
||||
await load()
|
||||
} finally {
|
||||
initting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function run(row) {
|
||||
try {
|
||||
const data = await api.post(`/report/definitions/${row.id}/run/`, {})
|
||||
resultText.value = JSON.stringify(data, null, 2)
|
||||
resultVisible.value = true
|
||||
} catch (e) { /* 拦截器已提示 */ }
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar { display: flex; gap: 10px; margin-bottom: 14px; align-items: center; }
|
||||
</style>
|
||||
@@ -15,6 +15,7 @@ const routes = [
|
||||
{ path: 'products', name: 'products', component: () => import('../pages/Products.vue'), meta: { title: '商品中心' } },
|
||||
{ path: 'customers', name: 'customers', component: () => import('../pages/Customers.vue'), meta: { title: '客户档案' } },
|
||||
{ path: 'stocks', name: 'stocks', component: () => import('../pages/Stocks.vue'), meta: { title: '库存查询' } },
|
||||
{ path: 'stocks/movements', name: 'movements', component: () => import('../pages/Movements.vue'), meta: { title: '库存流水' } },
|
||||
{ path: 'batches', name: 'batches', component: () => import('../pages/Batches.vue'), meta: { title: '批次效期' } },
|
||||
{ path: 'sales-bills', name: 'salesBills', component: () => import('../pages/SalesBills.vue'), meta: { title: '销售开单' } },
|
||||
{ path: 'purchase-bills', name: 'purchaseBills', component: () => import('../pages/PurchaseBills.vue'), meta: { title: '采购开单' } },
|
||||
@@ -22,8 +23,14 @@ const routes = [
|
||||
{ path: 'finance/receivables', name: 'receivables', component: () => import('../pages/Receivables.vue'), meta: { title: '应收账款' } },
|
||||
{ path: 'finance/payables', name: 'payables', component: () => import('../pages/Payables.vue'), meta: { title: '应付账款' } },
|
||||
{ path: 'finance/reports', name: 'financeReports', component: () => import('../pages/FinanceReports.vue'), meta: { title: '财务报表' } },
|
||||
{ path: 'finance/allocations', name: 'allocations', component: () => import('../pages/Allocations.vue'), meta: { title: '核销记录' } },
|
||||
{ path: 'finance/accounts', name: 'accounts', component: () => import('../pages/Accounts.vue'), meta: { title: '科目管理' } },
|
||||
{ path: 'channel', name: 'channel', component: () => import('../pages/Channel.vue'), meta: { title: '电商渠道' } },
|
||||
{ path: 'notify', name: 'notify', component: () => import('../pages/Notify.vue'), meta: { title: '消息预警' } },
|
||||
{ path: 'notify/rules', name: 'alertRules', component: () => import('../pages/AlertRules.vue'), meta: { title: '预警规则' } },
|
||||
{ path: 'printing/templates', name: 'printTemplates', component: () => import('../pages/PrintTemplates.vue'), meta: { title: '打印模板' } },
|
||||
{ path: 'report/definitions', name: 'reportDefinitions', component: () => import('../pages/ReportDefinitions.vue'), meta: { title: '自定义报表' } },
|
||||
{ path: 'partner/price-levels', name: 'priceLevels', component: () => import('../pages/PriceLevels.vue'), meta: { title: '价格等级与联系人' } },
|
||||
{ path: 'openapi-keys', name: 'openapiKeys', component: () => import('../pages/OpenApiKeys.vue'), meta: { title: '开放平台' } },
|
||||
{ path: 'billing', name: 'billing', component: () => import('../pages/Billing.vue'), meta: { title: '套餐与用量' } },
|
||||
{ path: 'audit-logs', name: 'auditLogs', component: () => import('../pages/AuditLogs.vue'), meta: { title: '操作轨迹' } },
|
||||
|
||||
Reference in New Issue
Block a user