This commit is contained in:
2026-06-10 11:21:17 +08:00
parent fdbde938f9
commit c39c0b547f
43 changed files with 1144 additions and 1243 deletions
@@ -67,7 +67,7 @@ onUnmounted(() => {
</div>
</div>
<el-alert
<t-alert
v-if="!state.backendDbReady"
:title="state.backendDbMessage"
type="warning"
@@ -76,7 +76,7 @@ onUnmounted(() => {
style="margin-bottom: 16px;"
/>
<div v-loading="state.loading">
<t-loading :loading="state.loading">
<DashboardTab v-if="state.activeTab === 'dashboard'" />
<ProductsTab v-else-if="state.activeTab === 'products'" />
<MaterialsTab v-else-if="state.activeTab === 'materials'" />
@@ -87,7 +87,7 @@ onUnmounted(() => {
<CustomersTab v-else-if="state.activeTab === 'customers'" />
<FinanceTab v-else-if="state.activeTab === 'finance'" />
<MovementsTab v-else-if="state.activeTab === 'movements'" />
</div>
</t-loading>
</section>
</div>
</div>
@@ -108,7 +108,7 @@ onUnmounted(() => {
.inventory-breadcrumb .sep { margin: 0 var(--space-2); }
.inventory-breadcrumb .current { color: var(--text-primary); font-weight: var(--font-medium); }
.inventory-sidebar-wrapper :deep(.el-menu) {
.inventory-sidebar-wrapper :deep(.t-menu) {
border-right: none;
background: transparent;
}
@@ -124,7 +124,7 @@ onUnmounted(() => {
cursor: pointer;
user-select: none;
}
.inventory-sidebar-wrapper :deep(.el-menu-item) {
.inventory-sidebar-wrapper :deep(.t-menu__item) {
height: 36px;
line-height: 36px;
font-size: var(--text-sm);
@@ -132,12 +132,12 @@ onUnmounted(() => {
border-radius: var(--radius-md);
padding: 0 var(--space-4);
}
.inventory-sidebar-wrapper :deep(.el-menu-item.is-active) {
.inventory-sidebar-wrapper :deep(.t-is-active) {
background: var(--primary-50);
color: var(--primary-600);
font-weight: var(--font-medium);
}
.inventory-sidebar-wrapper :deep(.el-menu-item:hover) {
.inventory-sidebar-wrapper :deep(.t-menu__item:hover) {
background: var(--bg-tertiary);
}
</style>
@@ -81,57 +81,57 @@ onMounted(() => { loadCustomers() })
<template>
<div class="customers-tab">
<div class="table-header">
<el-button type="primary" @click="openCreateCustomer">+ 新增客户</el-button>
<t-button type="primary" @click="openCreateCustomer">+ 新增客户</t-button>
</div>
<el-table :data="state.customers" style="width: 100%" v-loading="state.loading" empty-text="暂无客户数据">
<el-table-column prop="code" label="编码" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="contact_person" label="联系人">
<t-table :data="state.customers" style="width: 100%" :loading="state.loading" empty-text="暂无客户数据">
<t-table-column prop="code" label="编码" />
<t-table-column prop="name" label="名称" />
<t-table-column prop="contact_person" label="联系人">
<template #default="{ row }">
{{ row.contact_person || '-' }}
</template>
</el-table-column>
<el-table-column prop="phone" label="电话">
</t-table-column>
<t-table-column prop="phone" label="电话">
<template #default="{ row }">
{{ row.phone || '-' }}
</template>
</el-table-column>
<el-table-column prop="email" label="邮箱">
</t-table-column>
<t-table-column prop="email" label="邮箱">
<template #default="{ row }">
{{ row.email || '-' }}
</template>
</el-table-column>
<el-table-column label="操作" width="180">
</t-table-column>
<t-table-column label="操作" width="180">
<template #default="{ row }">
<el-button size="small" @click="editCustomer(row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteCustomer(row.id)">删除</el-button>
<t-button size="small" @click="editCustomer(row)">编辑</t-button>
<t-button size="small" type="danger" @click="deleteCustomer(row.id)">删除</t-button>
</template>
</el-table-column>
</el-table>
</t-table-column>
</t-table>
<el-dialog v-model="showModal" :title="editingItem ? '编辑客户' : '新增客户'" width="520px" :close-on-click-modal="false">
<el-form :model="form" label-width="80px">
<el-form-item label="名称" required>
<el-input v-model="form.name" placeholder="请输入客户名称" />
</el-form-item>
<el-form-item label="联系人">
<el-input v-model="form.contact_person" placeholder="请输入联系人" />
</el-form-item>
<el-form-item label="电话">
<el-input v-model="form.phone" placeholder="请输入电话" />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="form.email" type="email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="地址">
<el-input v-model="form.address" placeholder="请输入地址" />
</el-form-item>
</el-form>
<t-dialog v-model="showModal" :title="editingItem ? '编辑客户' : '新增客户'" width="520px" :close-on-click-modal="false">
<t-form :model="form" label-width="80px">
<t-form-item label="名称" required>
<t-input v-model="form.name" placeholder="请输入客户名称" />
</t-form-item>
<t-form-item label="联系人">
<t-input v-model="form.contact_person" placeholder="请输入联系人" />
</t-form-item>
<t-form-item label="电话">
<t-input v-model="form.phone" placeholder="请输入电话" />
</t-form-item>
<t-form-item label="邮箱">
<t-input v-model="form.email" type="email" placeholder="请输入邮箱" />
</t-form-item>
<t-form-item label="地址">
<t-input v-model="form.address" placeholder="请输入地址" />
</t-form-item>
</t-form>
<template #footer>
<el-button @click="showModal = false">取消</el-button>
<el-button type="primary" @click="save">保存</el-button>
<t-button @click="showModal = false">取消</t-button>
<t-button type="primary" @click="save">保存</t-button>
</template>
</el-dialog>
</t-dialog>
</div>
</template>
@@ -4,8 +4,8 @@ const { state, formatCurrency } = useInventory()
</script>
<template>
<el-row :gutter="16">
<el-col :span="8" v-for="card in [
<t-row :gutter="16">
<t-col :span="8" v-for="card in [
{ icon: '📦', value: state.dashboard?.product_count || 0, label: '成品数量' },
{ icon: '📊', value: state.dashboard?.total_stock || 0, label: '物料库存总量' },
{ icon: '💰', value: formatCurrency(state.dashboard?.total_value || 0), label: '库存价值' },
@@ -13,7 +13,7 @@ const { state, formatCurrency } = useInventory()
{ icon: '👥', value: state.dashboard?.customer_count || 0, label: '客户' },
{ icon: '🏪', value: state.dashboard?.warehouse_count || 0, label: '仓库' }
]" :key="card.label" style="margin-bottom: 16px">
<el-card shadow="hover">
<t-card shadow="hover">
<div style="display: flex; align-items: center; gap: 12px;">
<span style="font-size: 32px;">{{ card.icon }}</span>
<div>
@@ -21,9 +21,9 @@ const { state, formatCurrency } = useInventory()
<div style="color: var(--text-tertiary); font-size: 13px;">{{ card.label }}</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
</t-card>
</t-col>
</t-row>
</template>
<style scoped>
@@ -74,192 +74,192 @@ function getTransactionStatusLabel(status: string): string {
<div>
<div style="margin-bottom: 16px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap;">
<span style="font-weight: 600;">统计周期:</span>
<el-input-number
<t-input-number
v-model="periodYear"
:min="2020"
:max="2099"
style="width: 120px;"
/>
<span>年</span>
<el-select v-model="periodQuarter" placeholder="选择季度" style="width: 120px;">
<el-option
<t-select v-model="periodQuarter" placeholder="选择季度" style="width: 120px;">
<t-option
v-for="q in quarterOptions"
:key="q.value"
:label="q.label"
:value="q.value"
/>
</el-select>
<el-button type="primary" @click="refreshFinance">刷新统计</el-button>
<el-tag type="info" size="large">{{ periodLabel }}</el-tag>
</t-select>
<t-button type="primary" @click="refreshFinance">刷新统计</t-button>
<t-tag type="info" size="large">{{ periodLabel }}</t-tag>
</div>
<el-row :gutter="16" style="margin-bottom: 16px;">
<el-col :span="6">
<el-card shadow="hover">
<t-row :gutter="16" style="margin-bottom: 16px;">
<t-col :span="6">
<t-card shadow="hover">
<div style="text-align: center;">
<div style="color: #909399; font-size: 14px; margin-bottom: 8px;">应收总额</div>
<div style="font-size: 24px; font-weight: 700; color: #409eff;">
{{ formatCurrency(totalReceivable) }}
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
</t-card>
</t-col>
<t-col :span="6">
<t-card shadow="hover">
<div style="text-align: center;">
<div style="color: #909399; font-size: 14px; margin-bottom: 8px;">应付总额</div>
<div style="font-size: 24px; font-weight: 700; color: #e6a23c;">
{{ formatCurrency(totalPayable) }}
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
</t-card>
</t-col>
<t-col :span="6">
<t-card shadow="hover">
<div style="text-align: center;">
<div style="color: #909399; font-size: 14px; margin-bottom: 8px;">周期收款</div>
<div style="font-size: 24px; font-weight: 700; color: #67c23a;">
{{ formatCurrency(periodReceived) }}
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
</t-card>
</t-col>
<t-col :span="6">
<t-card shadow="hover">
<div style="text-align: center;">
<div style="color: #909399; font-size: 14px; margin-bottom: 8px;">周期付款</div>
<div style="font-size: 24px; font-weight: 700; color: #f56c6c;">
{{ formatCurrency(periodPaid) }}
</div>
</div>
</el-card>
</el-col>
</el-row>
</t-card>
</t-col>
</t-row>
<el-card style="margin-bottom: 16px;">
<t-card style="margin-bottom: 16px;">
<template #header>
<span style="font-weight: 600;">客户账款(周期)</span>
</template>
<el-table :data="state.customerFinanceStatement" v-loading="state.loading" stripe size="small">
<el-table-column prop="partner_name" label="客户" />
<el-table-column prop="order_count" label="订单数" />
<el-table-column prop="transaction_count" label="流水数" />
<el-table-column label="订单金额">
<t-table :data="state.customerFinanceStatement" :loading="state.loading" stripe size="small">
<t-table-column prop="partner_name" label="客户" />
<t-table-column prop="order_count" label="订单数" />
<t-table-column prop="transaction_count" label="流水数" />
<t-table-column label="订单金额">
<template #default="{ row }">{{ formatCurrency(row.order_amount) }}</template>
</el-table-column>
<el-table-column label="订单已收">
</t-table-column>
<t-table-column label="订单已收">
<template #default="{ row }">{{ formatCurrency(row.order_received) }}</template>
</el-table-column>
<el-table-column label="实收流水">
</t-table-column>
<t-table-column label="实收流水">
<template #default="{ row }">{{ formatCurrency(row.transaction_received) }}</template>
</el-table-column>
<el-table-column label="应收余额">
</t-table-column>
<t-table-column label="应收余额">
<template #default="{ row }">{{ formatCurrency(row.receivable_balance) }}</template>
</el-table-column>
</el-table>
<el-empty v-if="!state.loading && state.customerFinanceStatement.length === 0" description="暂无数据" />
</el-card>
</t-table-column>
</t-table>
<t-empty v-if="!state.loading && state.customerFinanceStatement.length === 0" description="暂无数据" />
</t-card>
<el-card style="margin-bottom: 16px;">
<t-card style="margin-bottom: 16px;">
<template #header>
<span style="font-weight: 600;">供应商账款(周期)</span>
</template>
<el-table :data="state.supplierFinanceStatement" v-loading="state.loading" stripe size="small">
<el-table-column prop="partner_name" label="供应商" />
<el-table-column prop="order_count" label="订单数" />
<el-table-column prop="transaction_count" label="流水数" />
<el-table-column label="订单金额">
<t-table :data="state.supplierFinanceStatement" :loading="state.loading" stripe size="small">
<t-table-column prop="partner_name" label="供应商" />
<t-table-column prop="order_count" label="订单数" />
<t-table-column prop="transaction_count" label="流水数" />
<t-table-column label="订单金额">
<template #default="{ row }">{{ formatCurrency(row.order_amount) }}</template>
</el-table-column>
<el-table-column label="订单已付">
</t-table-column>
<t-table-column label="订单已付">
<template #default="{ row }">{{ formatCurrency(row.order_paid) }}</template>
</el-table-column>
<el-table-column label="实付流水">
</t-table-column>
<t-table-column label="实付流水">
<template #default="{ row }">{{ formatCurrency(row.transaction_paid) }}</template>
</el-table-column>
<el-table-column label="应付余额">
</t-table-column>
<t-table-column label="应付余额">
<template #default="{ row }">{{ formatCurrency(row.payable_balance) }}</template>
</el-table-column>
</el-table>
<el-empty v-if="!state.loading && state.supplierFinanceStatement.length === 0" description="暂无数据" />
</el-card>
</t-table-column>
</t-table>
<t-empty v-if="!state.loading && state.supplierFinanceStatement.length === 0" description="暂无数据" />
</t-card>
<el-card style="margin-bottom: 16px;">
<t-card style="margin-bottom: 16px;">
<template #header>
<span style="font-weight: 600;">客户-商品追溯(周期)</span>
</template>
<el-table :data="state.customerProductStatement" v-loading="state.loading" stripe size="small">
<el-table-column prop="partner_name" label="客户" />
<el-table-column prop="product_sku" label="SKU" />
<el-table-column prop="product_name" label="商品" />
<el-table-column prop="order_count" label="订单数" />
<el-table-column prop="quantity" label="数量" />
<el-table-column label="订单金额">
<t-table :data="state.customerProductStatement" :loading="state.loading" stripe size="small">
<t-table-column prop="partner_name" label="客户" />
<t-table-column prop="product_sku" label="SKU" />
<t-table-column prop="product_name" label="商品" />
<t-table-column prop="order_count" label="订单数" />
<t-table-column prop="quantity" label="数量" />
<t-table-column label="订单金额">
<template #default="{ row }">{{ formatCurrency(row.order_amount) }}</template>
</el-table-column>
<el-table-column label="已结款">
</t-table-column>
<t-table-column label="已结款">
<template #default="{ row }">{{ formatCurrency(row.settled_amount) }}</template>
</el-table-column>
<el-table-column label="未结款">
</t-table-column>
<t-table-column label="未结款">
<template #default="{ row }">{{ formatCurrency(row.unsettled_amount) }}</template>
</el-table-column>
</el-table>
<el-empty v-if="!state.loading && state.customerProductStatement.length === 0" description="暂无数据" />
</el-card>
</t-table-column>
</t-table>
<t-empty v-if="!state.loading && state.customerProductStatement.length === 0" description="暂无数据" />
</t-card>
<el-card style="margin-bottom: 16px;">
<t-card style="margin-bottom: 16px;">
<template #header>
<span style="font-weight: 600;">供应商-商品追溯(周期)</span>
</template>
<el-table :data="state.supplierProductStatement" v-loading="state.loading" stripe size="small">
<el-table-column prop="partner_name" label="供应商" />
<el-table-column prop="product_sku" label="SKU" />
<el-table-column prop="product_name" label="商品" />
<el-table-column prop="order_count" label="订单数" />
<el-table-column prop="quantity" label="数量" />
<el-table-column label="订单金额">
<t-table :data="state.supplierProductStatement" :loading="state.loading" stripe size="small">
<t-table-column prop="partner_name" label="供应商" />
<t-table-column prop="product_sku" label="SKU" />
<t-table-column prop="product_name" label="商品" />
<t-table-column prop="order_count" label="订单数" />
<t-table-column prop="quantity" label="数量" />
<t-table-column label="订单金额">
<template #default="{ row }">{{ formatCurrency(row.order_amount) }}</template>
</el-table-column>
<el-table-column label="已结款">
</t-table-column>
<t-table-column label="已结款">
<template #default="{ row }">{{ formatCurrency(row.settled_amount) }}</template>
</el-table-column>
<el-table-column label="未结款">
</t-table-column>
<t-table-column label="未结款">
<template #default="{ row }">{{ formatCurrency(row.unsettled_amount) }}</template>
</el-table-column>
</el-table>
<el-empty v-if="!state.loading && state.supplierProductStatement.length === 0" description="暂无数据" />
</el-card>
</t-table-column>
</t-table>
<t-empty v-if="!state.loading && state.supplierProductStatement.length === 0" description="暂无数据" />
</t-card>
<el-card>
<t-card>
<template #header>
<span style="font-weight: 600;">最近财务流水</span>
</template>
<el-table :data="state.financeTransactions" v-loading="state.loading" stripe size="small">
<el-table-column prop="order_no" label="单号" />
<el-table-column label="类型">
<t-table :data="state.financeTransactions" :loading="state.loading" stripe size="small">
<t-table-column prop="order_no" label="单号" />
<t-table-column label="类型">
<template #default="{ row }">
<el-tag :type="getTransactionTypeTag(row.type)" size="small">
<t-tag :type="getTransactionTypeTag(row.type)" size="small">
{{ getTransactionTypeLabel(row.type) }}
</el-tag>
</t-tag>
</template>
</el-table-column>
<el-table-column prop="partner_name" label="往来方" />
<el-table-column label="金额">
</t-table-column>
<t-table-column prop="partner_name" label="往来方" />
<t-table-column label="金额">
<template #default="{ row }">{{ formatCurrency(row.amount) }}</template>
</el-table-column>
<el-table-column label="状态">
</t-table-column>
<t-table-column label="状态">
<template #default="{ row }">
<el-tag :type="row.status === 'confirmed' ? 'success' : 'warning'" size="small">
<t-tag :type="row.status === 'confirmed' ? 'success' : 'warning'" size="small">
{{ getTransactionStatusLabel(row.status) }}
</el-tag>
</t-tag>
</template>
</el-table-column>
<el-table-column label="日期">
</t-table-column>
<t-table-column label="日期">
<template #default="{ row }">{{ formatDate(row.transaction_date) }}</template>
</el-table-column>
</el-table>
<el-empty v-if="!state.loading && state.financeTransactions.length === 0" description="暂无数据" />
</el-card>
</t-table-column>
</t-table>
<t-empty v-if="!state.loading && state.financeTransactions.length === 0" description="暂无数据" />
</t-card>
</div>
</template>
@@ -5,7 +5,7 @@ const { state, menuGroups, openGroups, toggleGroup, handleMenuClick } = useInven
</script>
<template>
<el-menu
<t-menu
:default-active="state.activeTab"
class="inventory-sidebar"
@select="(key: string) => handleMenuClick(key)"
@@ -13,17 +13,17 @@ const { state, menuGroups, openGroups, toggleGroup, handleMenuClick } = useInven
<div v-for="group in menuGroups" :key="group.key" class="sidebar-group">
<div class="sidebar-group-title" @click="toggleGroup(group.key)">
<span>{{ group.title }}</span>
<el-icon><component :is="openGroups[group.key] ? 'ArrowDown' : 'ArrowRight'" /></el-icon>
<t-icon><component :is="openGroups[group.key] ? 'ArrowDown' : 'ArrowRight'" /></t-icon>
</div>
<template v-if="openGroups[group.key]">
<el-menu-item
<t-menu-item
v-for="item in group.items"
:key="group.key + '-' + item.key"
:index="item.key"
>
{{ item.label }}
</el-menu-item>
</t-menu-item>
</template>
</div>
</el-menu>
</t-menu>
</template>
@@ -87,62 +87,62 @@ onMounted(() => { loadInventory() })
<template>
<div class="inventory-tab">
<div class="table-header">
<el-button type="primary" @click="openCreate">+ 新增物料库存</el-button>
<t-button type="primary" @click="openCreate">+ 新增物料库存</t-button>
</div>
<el-table :data="state.inventory" style="width: 100%" v-loading="state.loading" empty-text="暂无库存数据">
<el-table-column prop="product_sku" label="SKU" />
<el-table-column prop="product_name" label="物料" />
<el-table-column prop="warehouse_name" label="仓库" />
<el-table-column prop="quantity" label="数量" />
<el-table-column prop="available_quantity" label="可用" />
<el-table-column label="操作" width="180">
<t-table :data="state.inventory" style="width: 100%" :loading="state.loading" empty-text="暂无库存数据">
<t-table-column prop="product_sku" label="SKU" />
<t-table-column prop="product_name" label="物料" />
<t-table-column prop="warehouse_name" label="仓库" />
<t-table-column prop="quantity" label="数量" />
<t-table-column prop="available_quantity" label="可用" />
<t-table-column label="操作" width="180">
<template #default="{ row }">
<el-button size="small" @click="openEdit(row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteItem(row.id)">删除</el-button>
<t-button size="small" @click="openEdit(row)">编辑</t-button>
<t-button size="small" type="danger" @click="deleteItem(row.id)">删除</t-button>
</template>
</el-table-column>
</el-table>
</t-table-column>
</t-table>
<el-dialog v-model="showModal" :title="editingItem ? '编辑物料库存' : '新增物料库存'" width="520px" :close-on-click-modal="false">
<el-form :model="form" label-width="80px">
<el-form-item label="物料" required>
<el-select v-model="form.product_id" placeholder="请选择物料" style="width: 100%">
<el-option
<t-dialog v-model="showModal" :title="editingItem ? '编辑物料库存' : '新增物料库存'" width="520px" :close-on-click-modal="false">
<t-form :model="form" label-width="80px">
<t-form-item label="物料" required>
<t-select v-model="form.product_id" placeholder="请选择物料" style="width: 100%">
<t-option
v-for="m in state.materials"
:key="m.id"
:label="m.name + (m.sku ? ' (' + m.sku + ')' : '')"
:value="m.id"
/>
</el-select>
</el-form-item>
<el-form-item label="仓库" required>
<el-select v-model="form.warehouse_id" placeholder="请选择仓库" style="width: 100%">
<el-option
</t-select>
</t-form-item>
<t-form-item label="仓库" required>
<t-select v-model="form.warehouse_id" placeholder="请选择仓库" style="width: 100%">
<t-option
v-for="w in state.warehouses"
:key="w.id"
:label="w.name"
:value="w.id"
/>
</el-select>
</el-form-item>
<el-form-item label="数量" required>
<el-input-number v-model="form.quantity" :min="0" style="width: 100%" />
</el-form-item>
<el-form-item label="锁定数量">
<el-input-number v-model="form.locked_quantity" :min="0" style="width: 100%" />
</el-form-item>
<el-form-item label="批次号">
<el-input v-model="form.batch_no" placeholder="请输入批次号" />
</el-form-item>
<el-form-item label="库位">
<el-input v-model="form.location" placeholder="请输入库位" />
</el-form-item>
</el-form>
</t-select>
</t-form-item>
<t-form-item label="数量" required>
<t-input-number v-model="form.quantity" :min="0" style="width: 100%" />
</t-form-item>
<t-form-item label="锁定数量">
<t-input-number v-model="form.locked_quantity" :min="0" style="width: 100%" />
</t-form-item>
<t-form-item label="批次号">
<t-input v-model="form.batch_no" placeholder="请输入批次号" />
</t-form-item>
<t-form-item label="库位">
<t-input v-model="form.location" placeholder="请输入库位" />
</t-form-item>
</t-form>
<template #footer>
<el-button @click="showModal = false">取消</el-button>
<el-button type="primary" @click="save">保存</el-button>
<t-button @click="showModal = false">取消</t-button>
<t-button type="primary" @click="save">保存</t-button>
</template>
</el-dialog>
</t-dialog>
</div>
</template>
@@ -175,139 +175,139 @@ onMounted(() => { loadMaterials() })
<template>
<div>
<div style="margin-bottom: 12px; display: flex; gap: 8px;">
<el-button type="primary" @click="openCreateMaterial">新增物料</el-button>
<el-button type="success" @click="openRestockModal">物料补货</el-button>
<t-button type="primary" @click="openCreateMaterial">新增物料</t-button>
<t-button type="success" @click="openRestockModal">物料补货</t-button>
</div>
<el-table :data="state.materials" v-loading="state.loading" stripe>
<el-table-column prop="sku" label="SKU" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="category" label="分类">
<t-table :data="state.materials" :loading="state.loading" stripe>
<t-table-column prop="sku" label="SKU" />
<t-table-column prop="name" label="名称" />
<t-table-column prop="category" label="分类">
<template #default="{ row }">{{ row.category || '-' }}</template>
</el-table-column>
<el-table-column prop="unit" label="单位" />
<el-table-column label="成本价">
</t-table-column>
<t-table-column prop="unit" label="单位" />
<t-table-column label="成本价">
<template #default="{ row }">{{ formatCurrency(row.cost_price) }}</template>
</el-table-column>
<el-table-column prop="min_stock" label="最低库存">
</t-table-column>
<t-table-column prop="min_stock" label="最低库存">
<template #default="{ row }">{{ row.min_stock ?? '-' }}</template>
</el-table-column>
<el-table-column label="操作" width="180">
</t-table-column>
<t-table-column label="操作" width="180">
<template #default="{ row }">
<el-button type="primary" size="small" @click="editMaterial(row)">编辑</el-button>
<el-button type="danger" size="small" @click="deleteMaterial(row.id)">删除</el-button>
<t-button type="primary" size="small" @click="editMaterial(row)">编辑</t-button>
<t-button type="danger" size="small" @click="deleteMaterial(row.id)">删除</t-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!state.loading && state.materials.length === 0" description="暂无物料数据" />
</t-table-column>
</t-table>
<t-empty v-if="!state.loading && state.materials.length === 0" description="暂无物料数据" />
<el-dialog
v-model="showModal"
<t-dialog
v-model:visible="showModal"
:title="(editingItem ? '编辑' : '新增') + '物料'"
width="560px"
@closed="closeModal"
>
<el-form label-width="100px">
<el-form-item label="类型">
<el-input model-value="物料(纳入库存)" disabled />
</el-form-item>
<el-form-item label="SKU" required>
<el-input v-model="form.sku" placeholder="产品编码" />
</el-form-item>
<el-form-item label="名称" required>
<el-input v-model="form.name" placeholder="产品名称" />
</el-form-item>
<el-form-item label="分类">
<el-input v-model="form.category" placeholder="产品分类" />
</el-form-item>
<el-form-item label="单位">
<el-input v-model="form.unit" placeholder="kg" />
</el-form-item>
<el-form-item label="成本价">
<el-input-number v-model="form.cost_price" :precision="2" :min="0" :step="0.01" style="width:100%" />
</el-form-item>
<el-form-item label="最低库存">
<el-input-number v-model="form.min_stock" :min="0" style="width:100%" />
</el-form-item>
<el-form-item label="最高库存">
<el-input-number v-model="form.max_stock" :min="0" style="width:100%" />
</el-form-item>
</el-form>
<t-form label-width="100px">
<t-form-item label="类型">
<t-input model-value="物料(纳入库存)" disabled />
</t-form-item>
<t-form-item label="SKU" required>
<t-input v-model="form.sku" placeholder="产品编码" />
</t-form-item>
<t-form-item label="名称" required>
<t-input v-model="form.name" placeholder="产品名称" />
</t-form-item>
<t-form-item label="分类">
<t-input v-model="form.category" placeholder="产品分类" />
</t-form-item>
<t-form-item label="单位">
<t-input v-model="form.unit" placeholder="kg" />
</t-form-item>
<t-form-item label="成本价">
<t-input-number v-model="form.cost_price" :precision="2" :min="0" :step="0.01" style="width:100%" />
</t-form-item>
<t-form-item label="最低库存">
<t-input-number v-model="form.min_stock" :min="0" style="width:100%" />
</t-form-item>
<t-form-item label="最高库存">
<t-input-number v-model="form.max_stock" :min="0" style="width:100%" />
</t-form-item>
</t-form>
<template #footer>
<el-button @click="showModal = false">取消</el-button>
<el-button type="primary" @click="saveProduct">保存</el-button>
<t-button @click="showModal = false">取消</t-button>
<t-button type="primary" @click="saveProduct">保存</t-button>
</template>
</el-dialog>
</t-dialog>
<el-dialog
v-model="showRestockModal"
<t-dialog
v-model:visible="showRestockModal"
title="物料补货"
width="900px"
@closed="closeRestockModal"
>
<el-form label-width="100px" style="margin-bottom: 16px;">
<el-form-item label="供应商">
<el-select v-model="restockSupplierId" placeholder="请选择供应商" style="width:100%">
<el-option
<t-form label-width="100px" style="margin-bottom: 16px;">
<t-form-item label="供应商">
<t-select v-model="restockSupplierId" placeholder="请选择供应商" style="width:100%">
<t-option
v-for="supplier in state.suppliers"
:key="supplier.id"
:label="supplier.name"
:value="supplier.id"
/>
</el-select>
</el-form-item>
</el-form>
</t-select>
</t-form-item>
</t-form>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<span style="font-weight: 600;">补货物料明细</span>
<el-button type="primary" size="small" @click="addRestockItem">+ 添加物料</el-button>
<t-button type="primary" size="small" @click="addRestockItem">+ 添加物料</t-button>
</div>
<el-table :data="restockItems" border size="small">
<el-table-column label="物料" min-width="200">
<t-table :data="restockItems" border size="small">
<t-table-column label="物料" min-width="200">
<template #default="{ row: line }">
<el-select v-model="line.material_id" placeholder="请选择物料" style="width:100%">
<el-option
<t-select v-model="line.material_id" placeholder="请选择物料" style="width:100%">
<t-option
v-for="material in state.materials"
:key="material.id"
:label="`${material.sku} - ${material.name}`"
:value="material.id"
/>
</el-select>
</t-select>
</template>
</el-table-column>
<el-table-column label="数量" width="120">
</t-table-column>
<t-table-column label="数量" width="120">
<template #default="{ row: line }">
<el-input-number v-model="line.quantity" :min="1" size="small" style="width:100%" />
<t-input-number v-model="line.quantity" :min="1" size="small" style="width:100%" />
</template>
</el-table-column>
<el-table-column label="单价" width="120">
</t-table-column>
<t-table-column label="单价" width="120">
<template #default="{ row: line }">
<el-input-number v-model="line.unit_price" :min="0" :precision="2" :step="0.01" size="small" style="width:100%" />
<t-input-number v-model="line.unit_price" :min="0" :precision="2" :step="0.01" size="small" style="width:100%" />
</template>
</el-table-column>
<el-table-column label="总价" width="120">
</t-table-column>
<t-table-column label="总价" width="120">
<template #default="{ row: line }">
{{ formatCurrency((line.unit_price || 0) * (line.quantity || 0)) }}
</template>
</el-table-column>
<el-table-column label="备注" width="150">
</t-table-column>
<t-table-column label="备注" width="150">
<template #default="{ row: line }">
<el-input v-model="line.remark" size="small" placeholder="备注" />
<t-input v-model="line.remark" size="small" placeholder="备注" />
</template>
</el-table-column>
<el-table-column label="操作" width="80">
</t-table-column>
<t-table-column label="操作" width="80">
<template #default="{ $index }">
<el-button type="danger" size="small" @click="removeRestockItem($index)">删除</el-button>
<t-button type="danger" size="small" @click="removeRestockItem($index)">删除</t-button>
</template>
</el-table-column>
</el-table>
</t-table-column>
</t-table>
<div style="text-align: right; margin-top: 8px; font-weight: 600;">
补货总金额:{{ formatCurrency(restockTotalAmount) }}
</div>
<template #footer>
<el-button @click="showRestockModal = false">取消</el-button>
<el-button type="primary" @click="saveRestock">创建采购订单</el-button>
<t-button @click="showRestockModal = false">取消</t-button>
<t-button type="primary" @click="saveRestock">创建采购订单</t-button>
</template>
</el-dialog>
</t-dialog>
</div>
</template>
@@ -16,28 +16,28 @@ onMounted(() => { loadMovements() })
<template>
<div class="movements-tab">
<el-table :data="state.movements" style="width: 100%" v-loading="state.loading" empty-text="暂无库存变动记录">
<el-table-column label="物料">
<t-table :data="state.movements" style="width: 100%" :loading="state.loading" empty-text="暂无库存变动记录">
<t-table-column label="物料">
<template #default="{ row }">
{{ row.product_name }}{{ row.product_sku ? ' (' + row.product_sku + ')' : '' }}
</template>
</el-table-column>
<el-table-column label="类型" width="140">
</t-table-column>
<t-table-column label="类型" width="140">
<template #default="{ row }">
<el-tag :type="getMovementTagType(row.movement_type)" size="small">
<t-tag :type="getMovementTagType(row.movement_type)" size="small">
{{ getMovementTypeLabel(row.movement_type) }}
</el-tag>
</t-tag>
</template>
</el-table-column>
<el-table-column prop="quantity" label="数量" width="80" />
<el-table-column prop="before_quantity" label="变动前" width="80" />
<el-table-column prop="after_quantity" label="变动后" width="80" />
<el-table-column label="时间" width="180">
</t-table-column>
<t-table-column prop="quantity" label="数量" width="80" />
<t-table-column prop="before_quantity" label="变动前" width="80" />
<t-table-column prop="after_quantity" label="变动后" width="80" />
<t-table-column label="时间" width="180">
<template #default="{ row }">
{{ formatDateTime(row.created_at) }}
</template>
</el-table-column>
</el-table>
</t-table-column>
</t-table>
</div>
</template>
@@ -124,102 +124,102 @@ onMounted(() => { loadFinishedProducts() })
<template>
<div>
<div style="margin-bottom: 12px;">
<el-button type="primary" @click="openCreateProduct">新增成品</el-button>
<t-button type="primary" @click="openCreateProduct">新增成品</t-button>
</div>
<el-table :data="state.finishedProducts" v-loading="state.loading" stripe>
<el-table-column prop="sku" label="SKU" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="category" label="分类">
<t-table :data="state.finishedProducts" :loading="state.loading" stripe>
<t-table-column prop="sku" label="SKU" />
<t-table-column prop="name" label="名称" />
<t-table-column prop="category" label="分类">
<template #default="{ row }">{{ row.category || '-' }}</template>
</el-table-column>
<el-table-column prop="unit" label="单位" />
<el-table-column label="成本价">
</t-table-column>
<t-table-column prop="unit" label="单位" />
<t-table-column label="成本价">
<template #default="{ row }">{{ formatCurrency(row.cost_price) }}</template>
</el-table-column>
<el-table-column label="销售价">
</t-table-column>
<t-table-column label="销售价">
<template #default="{ row }">{{ formatCurrency(row.sale_price) }}</template>
</el-table-column>
<el-table-column label="基础物料成本">
</t-table-column>
<t-table-column label="基础物料成本">
<template #default="{ row }">{{ formatCurrency(row.material_cost || 0) }}</template>
</el-table-column>
<el-table-column label="操作" width="180">
</t-table-column>
<t-table-column label="操作" width="180">
<template #default="{ row }">
<el-button type="primary" size="small" @click="editProduct(row)">编辑</el-button>
<el-button type="danger" size="small" @click="deleteProduct(row.id)">删除</el-button>
<t-button type="primary" size="small" @click="editProduct(row)">编辑</t-button>
<t-button type="danger" size="small" @click="deleteProduct(row.id)">删除</t-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!state.loading && state.finishedProducts.length === 0" description="暂无成品数据" />
</t-table-column>
</t-table>
<t-empty v-if="!state.loading && state.finishedProducts.length === 0" description="暂无成品数据" />
<el-dialog
v-model="showModal"
<t-dialog
v-model:visible="showModal"
:title="(editingItem ? '编辑' : '新增') + '成品'"
width="560px"
@closed="closeModal"
>
<el-form label-width="100px">
<el-form-item label="类型">
<el-input model-value="成品(按单生产,不做库存)" disabled />
</el-form-item>
<el-form-item label="SKU" required>
<el-input v-model="form.sku" placeholder="产品编码" />
</el-form-item>
<el-form-item label="名称" required>
<el-input v-model="form.name" placeholder="产品名称" />
</el-form-item>
<el-form-item label="分类">
<el-input v-model="form.category" placeholder="产品分类" />
</el-form-item>
<el-form-item label="单位">
<el-input v-model="form.unit" placeholder="件" />
</el-form-item>
<el-form-item label="成本价">
<el-input-number v-model="form.cost_price" :precision="2" :min="0" :step="0.01" style="width:100%" />
</el-form-item>
<el-form-item label="销售价">
<el-input-number v-model="form.sale_price" :precision="2" :min="0" :step="0.01" style="width:100%" />
</el-form-item>
<el-form-item label="说明">
<el-input model-value="成品不做库存,成本由下方BOM定义物料构成后自动计算" disabled />
</el-form-item>
<t-form label-width="100px">
<t-form-item label="类型">
<t-input model-value="成品(按单生产,不做库存)" disabled />
</t-form-item>
<t-form-item label="SKU" required>
<t-input v-model="form.sku" placeholder="产品编码" />
</t-form-item>
<t-form-item label="名称" required>
<t-input v-model="form.name" placeholder="产品名称" />
</t-form-item>
<t-form-item label="分类">
<t-input v-model="form.category" placeholder="产品分类" />
</t-form-item>
<t-form-item label="单位">
<t-input v-model="form.unit" placeholder="件" />
</t-form-item>
<t-form-item label="成本价">
<t-input-number v-model="form.cost_price" :precision="2" :min="0" :step="0.01" style="width:100%" />
</t-form-item>
<t-form-item label="销售价">
<t-input-number v-model="form.sale_price" :precision="2" :min="0" :step="0.01" style="width:100%" />
</t-form-item>
<t-form-item label="说明">
<t-input model-value="成品不做库存,成本由下方BOM定义物料构成后自动计算" disabled />
</t-form-item>
<template v-if="editingItem">
<el-divider content-position="left">BOM物料配置</el-divider>
<t-divider content-position="left">BOM物料配置</t-divider>
<div style="padding: 0 12px 12px 12px;">
<el-button type="primary" size="small" @click="addBomItem" style="margin-bottom:8px">+ 添加物料</el-button>
<el-table :data="bomItems" size="small" border>
<el-table-column label="物料">
<t-button type="primary" size="small" @click="addBomItem" style="margin-bottom:8px">+ 添加物料</t-button>
<t-table :data="bomItems" size="small" border>
<t-table-column label="物料">
<template #default="{ row: bomItem }">
<el-select v-model="bomItem.material_id" placeholder="选择物料" style="width:100%">
<el-option
<t-select v-model="bomItem.material_id" placeholder="选择物料" style="width:100%">
<t-option
v-for="material in state.materials"
:key="material.id"
:label="`${material.sku} - ${material.name}`"
:value="material.id"
/>
</el-select>
</t-select>
</template>
</el-table-column>
<el-table-column label="数量" width="120">
</t-table-column>
<t-table-column label="数量" width="120">
<template #default="{ row: bomItem }">
<el-input-number v-model="bomItem.quantity" :min="0.0001" :step="0.0001" size="small" style="width:100%" />
<t-input-number v-model="bomItem.quantity" :min="0.0001" :step="0.0001" size="small" style="width:100%" />
</template>
</el-table-column>
<el-table-column label="操作" width="80">
</t-table-column>
<t-table-column label="操作" width="80">
<template #default="{ $index }">
<el-button type="danger" size="small" @click="removeBomItem($index)">删除</el-button>
<t-button type="danger" size="small" @click="removeBomItem($index)">删除</t-button>
</template>
</el-table-column>
</el-table>
</t-table-column>
</t-table>
</div>
</template>
</el-form>
</t-form>
<template #footer>
<el-button @click="showModal = false">取消</el-button>
<el-button type="primary" @click="saveProduct">保存</el-button>
<t-button @click="showModal = false">取消</t-button>
<t-button type="primary" @click="saveProduct">保存</t-button>
</template>
</el-dialog>
</t-dialog>
</div>
</template>
@@ -1,10 +1,22 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ElMessageBox } from 'element-plus'
import { DialogPlugin } from 'tdesign-vue-next'
import { useInventory } from '../composables/useInventory'
import { apiRequest } from '@/shared/api'
import { addNotification, handleApiError } from '@/shared/notification'
function confirmDialog(header: string, body: string, theme: string, confirmText: string, cancelText: string): Promise<boolean> {
return new Promise((resolve) => {
const dlg = DialogPlugin.confirm({
header, body, theme: theme as any,
confirmBtn: confirmText, cancelBtn: cancelText,
onConfirm: () => { dlg.hide(); resolve(true) },
onCancel: () => { dlg.hide(); resolve(false) },
onClose: () => { resolve(false) }
})
})
}
const {
state,
formatCurrency,
@@ -140,11 +152,7 @@ async function savePurchaseOrder() {
}
async function deleteOrder(id: number) {
try {
await ElMessageBox.confirm('确定要删除这个采购订单吗?', '删除确认', {
confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning'
})
} catch { return }
if (!await confirmDialog('删除确认', '确定要删除这个采购订单吗?', 'warning', '删除', '取消')) return
try {
await apiRequest(`/api/purchase-orders/${id}`, { method: 'DELETE' })
addNotification('采购订单已删除', 'success')
@@ -212,29 +220,15 @@ async function receivePurchaseOrder() {
async function markAsPaid(orderId: number, receiptStatus: string) {
if (receiptStatus !== 'received') {
try {
await ElMessageBox.confirm(
'该订单尚未全部收货,确定要提前付款吗?建议先完成收货后再付款。',
'非标准流程提醒',
{ confirmButtonText: '仍要付款', cancelButtonText: '取消', type: 'warning' }
)
} catch { return }
if (!await confirmDialog('非标准流程提醒', '该订单尚未全部收货,确定要提前付款吗?建议先完成收货后再付款。', 'warning', '仍要付款', '取消')) return
} else {
try {
await ElMessageBox.confirm('确认已向供应商支付该订单款项?', '付款确认', {
confirmButtonText: '确认付款', cancelButtonText: '取消', type: 'success'
})
} catch { return }
if (!await confirmDialog('付款确认', '确认已向供应商支付该订单款项?', 'success', '确认付款', '取消')) return
}
await doUpdateStatus(orderId, 'paid', '已付款')
}
async function cancelOrder(orderId: number) {
try {
await ElMessageBox.confirm('确定要作废该订单吗?此操作不可撤销。', '作废确认', {
confirmButtonText: '确认作废', cancelButtonText: '取消', type: 'error'
})
} catch { return }
if (!await confirmDialog('作废确认', '确定要作废该订单吗?此操作不可撤销。', 'error', '确认作废', '取消')) return
await doUpdateStatus(orderId, 'cancelled', '已作废')
}
@@ -280,223 +274,223 @@ onMounted(() => {
<template>
<div>
<div style="margin-bottom: 12px; display: flex; gap: 12px; align-items: center;">
<el-select
<t-select
v-model="purchaseWarehouseId"
placeholder="选择仓库"
style="width: 200px;"
clearable
>
<el-option
<t-option
v-for="warehouse in state.warehouses"
:key="warehouse.id"
:label="warehouse.name"
:value="warehouse.id"
/>
</el-select>
<el-button @click="loadPurchaseOrders">刷新</el-button>
<el-button type="primary" @click="openCreateOrder">新增采购订单</el-button>
</t-select>
<t-button @click="loadPurchaseOrders">刷新</t-button>
<t-button type="primary" @click="openCreateOrder">新增采购订单</t-button>
</div>
<el-table :data="state.purchaseOrders" v-loading="state.loading" stripe>
<el-table-column prop="order_no" label="采购单" />
<el-table-column prop="supplier_name" label="供应商" />
<el-table-column label="状态">
<t-table :data="state.purchaseOrders" :loading="state.loading" stripe>
<t-table-column prop="order_no" label="采购单" />
<t-table-column prop="supplier_name" label="供应商" />
<t-table-column label="状态">
<template #default="{ row }">
<div style="display: flex; gap: 4px;">
<el-tag
<t-tag
:type="row.receipt_status === 'received' ? 'success' : row.receipt_status === 'partial_received' ? 'warning' : row.receipt_status === 'cancelled' ? 'danger' : 'info'"
size="small"
>{{ getReceiptStatusLabel(row.receipt_status) }}</el-tag>
<el-tag
>{{ getReceiptStatusLabel(row.receipt_status) }}</t-tag>
<t-tag
:type="row.payment_status === 'paid' ? 'success' : 'info'"
size="small"
>{{ getPaymentStatusLabel(row.payment_status) }}</el-tag>
>{{ getPaymentStatusLabel(row.payment_status) }}</t-tag>
</div>
</template>
</el-table-column>
<el-table-column label="订单创建">
</t-table-column>
<t-table-column label="订单创建">
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
</el-table-column>
<el-table-column label="预计到货">
</t-table-column>
<t-table-column label="预计到货">
<template #default="{ row }">{{ formatDate(row.expected_date) }}</template>
</el-table-column>
<el-table-column label="实际到货">
</t-table-column>
<t-table-column label="实际到货">
<template #default="{ row }">{{ row.received_at ? formatDateTime(row.received_at) : '-' }}</template>
</el-table-column>
<el-table-column label="实际付款">
</t-table-column>
<t-table-column label="实际付款">
<template #default="{ row }">{{ row.paid_at ? formatDateTime(row.paid_at) : '-' }}</template>
</el-table-column>
<el-table-column label="总金额">
</t-table-column>
<t-table-column label="总金额">
<template #default="{ row }">{{ formatCurrency(row.total_amount) }}</template>
</el-table-column>
<el-table-column label="已付款">
</t-table-column>
<t-table-column label="已付款">
<template #default="{ row }">{{ formatCurrency(row.paid_amount || 0) }}</template>
</el-table-column>
<el-table-column label="操作" width="280">
</t-table-column>
<t-table-column label="操作" width="280">
<template #default="{ row }">
<el-button
<t-button
type="primary"
size="small"
:disabled="isPurchaseOrderLocked(row.status)"
@click="editOrder(row)"
>
编辑
</el-button>
<el-button
</t-button>
<t-button
type="danger"
size="small"
:disabled="isPurchaseOrderLocked(row.status)"
@click="deleteOrder(row.id)"
>
删除
</el-button>
<el-button
</t-button>
<t-button
v-if="row.receipt_status === 'pending' || row.receipt_status === 'partial_received'"
type="success"
size="small"
@click="openReceiveDialog(row)"
>
到货入库
</el-button>
<el-button
</t-button>
<t-button
v-if="row.payment_status !== 'paid' && row.receipt_status !== 'cancelled'"
type="warning"
size="small"
@click="markAsPaid(row.id, row.receipt_status)"
>
标记已付款
</el-button>
<el-button
</t-button>
<t-button
v-if="row.payment_status !== 'paid' && row.receipt_status !== 'cancelled'"
type="info"
size="small"
@click="cancelOrder(row.id)"
>
作废
</el-button>
</t-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!state.loading && state.purchaseOrders.length === 0" description="暂无采购订单" />
</t-table-column>
</t-table>
<t-empty v-if="!state.loading && state.purchaseOrders.length === 0" description="暂无采购订单" />
<el-dialog
v-model="showModal"
<t-dialog
v-model:visible="showModal"
:title="(editingItem ? '编辑' : '新增') + '采购订单'"
width="800px"
@closed="closeModal"
>
<el-form label-width="100px">
<el-form-item label="供应商" required>
<el-select v-model="form.supplier_id" placeholder="请选择供应商" style="width:100%">
<el-option
<t-form label-width="100px">
<t-form-item label="供应商" required>
<t-select v-model="form.supplier_id" placeholder="请选择供应商" style="width:100%">
<t-option
v-for="supplier in state.suppliers"
:key="supplier.id"
:label="supplier.name"
:value="supplier.id"
/>
</el-select>
</el-form-item>
<el-form-item label="预计到货">
<el-date-picker
</t-select>
</t-form-item>
<t-form-item label="预计到货">
<t-date-picker
v-model="form.expected_date"
type="date"
placeholder="选择日期"
style="width:100%"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.remark" placeholder="备注信息" />
</el-form-item>
</el-form>
</t-form-item>
<t-form-item label="备注">
<t-input v-model="form.remark" placeholder="备注信息" />
</t-form-item>
</t-form>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<span style="font-weight: 600;">物料明细</span>
<el-button type="primary" size="small" @click="addOrderItem">+ 添加物料</el-button>
<t-button type="primary" size="small" @click="addOrderItem">+ 添加物料</t-button>
</div>
<el-table :data="orderItems" border size="small">
<el-table-column label="物料" min-width="200">
<t-table :data="orderItems" border size="small">
<t-table-column label="物料" min-width="200">
<template #default="{ row: line }">
<el-select v-model="line.product_id" placeholder="请选择物料" style="width:100%">
<el-option
<t-select v-model="line.product_id" placeholder="请选择物料" style="width:100%">
<t-option
v-for="material in state.materials"
:key="material.id"
:label="`${material.sku} - ${material.name}`"
:value="material.id"
/>
</el-select>
</t-select>
</template>
</el-table-column>
<el-table-column label="数量" width="120">
</t-table-column>
<t-table-column label="数量" width="120">
<template #default="{ row: line }">
<el-input-number v-model="line.quantity" :min="0.0001" :step="0.0001" size="small" style="width:100%" />
<t-input-number v-model="line.quantity" :min="0.0001" :step="0.0001" size="small" style="width:100%" />
</template>
</el-table-column>
<el-table-column label="单价" width="120">
</t-table-column>
<t-table-column label="单价" width="120">
<template #default="{ row: line }">
<el-input-number
<t-input-number
:model-value="(state.materials.find((m: any) => m.id === line.product_id)?.cost_price || 0)"
disabled
size="small"
style="width:100%"
/>
</template>
</el-table-column>
<el-table-column label="总价" width="120">
</t-table-column>
<t-table-column label="总价" width="120">
<template #default="{ row: line }">
{{ formatCurrency((state.materials.find((m: any) => m.id === line.product_id)?.cost_price || 0) * (line.quantity || 0)) }}
</template>
</el-table-column>
<el-table-column label="备注" width="150">
</t-table-column>
<t-table-column label="备注" width="150">
<template #default="{ row: line }">
<el-input v-model="line.remark" size="small" placeholder="备注" />
<t-input v-model="line.remark" size="small" placeholder="备注" />
</template>
</el-table-column>
<el-table-column label="操作" width="80">
</t-table-column>
<t-table-column label="操作" width="80">
<template #default="{ $index }">
<el-button type="danger" size="small" @click="removeOrderItem($index)">删除</el-button>
<t-button type="danger" size="small" @click="removeOrderItem($index)">删除</t-button>
</template>
</el-table-column>
</el-table>
</t-table-column>
</t-table>
<div style="text-align: right; margin-top: 8px; font-weight: 600;">
订单总金额:{{ formatCurrency(orderTotalAmount) }}
</div>
<template #footer>
<el-button @click="showModal = false">取消</el-button>
<el-button type="primary" @click="savePurchaseOrder">保存</el-button>
<t-button @click="showModal = false">取消</t-button>
<t-button type="primary" @click="savePurchaseOrder">保存</t-button>
</template>
</el-dialog>
</t-dialog>
<el-dialog
v-model="showReceiveModal"
<t-dialog
v-model:visible="showReceiveModal"
title="采购到货入库"
width="700px"
@closed="closeReceiveModal"
>
<el-form label-width="100px">
<el-form-item label="入库仓库" required>
<el-select v-model="receiveWarehouseId" placeholder="请选择仓库" style="width:100%">
<el-option
<t-form label-width="100px">
<t-form-item label="入库仓库" required>
<t-select v-model="receiveWarehouseId" placeholder="请选择仓库" style="width:100%">
<t-option
v-for="warehouse in state.warehouses"
:key="warehouse.id"
:label="warehouse.name"
:value="warehouse.id"
/>
</el-select>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="receiveRemark" placeholder="入库备注" />
</el-form-item>
</el-form>
<el-table :data="receiveItems" border size="small">
<el-table-column prop="material_label" label="物料" />
<el-table-column prop="id" label="明细ID" width="80" />
<el-table-column label="剩余待入库" width="100">
</t-select>
</t-form-item>
<t-form-item label="备注">
<t-input v-model="receiveRemark" placeholder="入库备注" />
</t-form-item>
</t-form>
<t-table :data="receiveItems" border size="small">
<t-table-column prop="material_label" label="物料" />
<t-table-column prop="id" label="明细ID" width="80" />
<t-table-column label="剩余待入库" width="100">
<template #default="{ row: item }">{{ item.remaining }}</template>
</el-table-column>
<el-table-column label="本次入库" width="140">
</t-table-column>
<t-table-column label="本次入库" width="140">
<template #default="{ row: item }">
<el-input-number
<t-input-number
v-model="item.receive_quantity"
:min="0"
:max="item.remaining"
@@ -504,13 +498,13 @@ onMounted(() => {
style="width:100%"
/>
</template>
</el-table-column>
</el-table>
</t-table-column>
</t-table>
<template #footer>
<el-button @click="showReceiveModal = false">取消</el-button>
<el-button type="primary" @click="receivePurchaseOrder">确认入库</el-button>
<t-button @click="showReceiveModal = false">取消</t-button>
<t-button type="primary" @click="receivePurchaseOrder">确认入库</t-button>
</template>
</el-dialog>
</t-dialog>
</div>
</template>
@@ -1,10 +1,22 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ElMessageBox } from 'element-plus'
import { DialogPlugin } from 'tdesign-vue-next'
import { useInventory } from '../composables/useInventory'
import { apiRequest } from '@/shared/api'
import { addNotification, handleApiError } from '@/shared/notification'
function confirmDialog(header: string, body: string, theme: string, confirmText: string, cancelText: string): Promise<boolean> {
return new Promise((resolve) => {
const dlg = DialogPlugin.confirm({
header, body, theme: theme as any,
confirmBtn: confirmText, cancelBtn: cancelText,
onConfirm: () => { dlg.hide(); resolve(true) },
onCancel: () => { dlg.hide(); resolve(false) },
onClose: () => { resolve(false) }
})
})
}
const {
state,
formatCurrency,
@@ -156,11 +168,7 @@ async function saveSalesOrder() {
}
async function deleteOrder(id: number) {
try {
await ElMessageBox.confirm('确定要删除这个销售订单吗?', '删除确认', {
confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning'
})
} catch { return }
if (!await confirmDialog('删除确认', '确定要删除这个销售订单吗?', 'warning', '删除', '取消')) return
try {
await apiRequest(`/api/sales-orders/${id}`, { method: 'DELETE' })
addNotification('销售订单已删除', 'success')
@@ -173,39 +181,21 @@ async function deleteOrder(id: number) {
}
async function markDelivered(orderId: number) {
try {
await ElMessageBox.confirm('确认该订单已交付?', '交付确认', {
confirmButtonText: '确认交付', cancelButtonText: '取消', type: 'success'
})
} catch { return }
if (!await confirmDialog('交付确认', '确认该订单已交付?', 'success', '确认交付', '取消')) return
await doUpdateStatus(orderId, 'delivered', '已交付')
}
async function markPaid(orderId: number, deliveryStatus: string) {
if (deliveryStatus !== 'delivered') {
try {
await ElMessageBox.confirm(
'当前订单尚未交付,确定要提前收款吗?建议先确认交付后再收款。',
'非标准流程提醒',
{ confirmButtonText: '仍要收款', cancelButtonText: '取消', type: 'warning' }
)
} catch { return }
if (!await confirmDialog('非标准流程提醒', '当前订单尚未交付,确定要提前收款吗?建议先确认交付后再收款。', 'warning', '仍要收款', '取消')) return
} else {
try {
await ElMessageBox.confirm('确认已收到该订单款项?', '收款确认', {
confirmButtonText: '确认收款', cancelButtonText: '取消', type: 'success'
})
} catch { return }
if (!await confirmDialog('收款确认', '确认已收到该订单款项?', 'success', '确认收款', '取消')) return
}
await doUpdateStatus(orderId, 'paid', '已收款')
}
async function cancelOrder(orderId: number) {
try {
await ElMessageBox.confirm('确定要作废该订单吗?此操作不可撤销。', '作废确认', {
confirmButtonText: '确认作废', cancelButtonText: '取消', type: 'error'
})
} catch { return }
if (!await confirmDialog('作废确认', '确定要作废该订单吗?此操作不可撤销。', 'error', '确认作废', '取消')) return
await doUpdateStatus(orderId, 'cancelled', '已作废')
}
@@ -316,146 +306,146 @@ onMounted(() => {
<template>
<div>
<div style="margin-bottom: 12px; display: flex; gap: 12px; align-items: center;">
<el-select
<t-select
v-model="productionWarehouseId"
placeholder="选择仓库"
style="width: 200px;"
clearable
>
<el-option
<t-option
v-for="warehouse in state.warehouses"
:key="warehouse.id"
:label="warehouse.name"
:value="warehouse.id"
/>
</el-select>
<el-button @click="loadProductionOrders">刷新</el-button>
<el-button type="primary" @click="openCreateOrder">新增销售订单</el-button>
</t-select>
<t-button @click="loadProductionOrders">刷新</t-button>
<t-button type="primary" @click="openCreateOrder">新增销售订单</t-button>
</div>
<el-table :data="state.productionOrders" v-loading="state.loading" stripe>
<el-table-column prop="order_no" label="销售单" />
<el-table-column prop="customer_name" label="客户" />
<el-table-column label="订单金额">
<t-table :data="state.productionOrders" :loading="state.loading" stripe>
<t-table-column prop="order_no" label="销售单" />
<t-table-column prop="customer_name" label="客户" />
<t-table-column label="订单金额">
<template #default="{ row }">{{ formatCurrency(row.total_amount) }}</template>
</el-table-column>
<el-table-column label="交付日期">
</t-table-column>
<t-table-column label="交付日期">
<template #default="{ row }">{{ formatDate(row.delivery_date) }}</template>
</el-table-column>
<el-table-column label="订单创建">
</t-table-column>
<t-table-column label="订单创建">
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
</el-table-column>
<el-table-column label="实际交付">
</t-table-column>
<t-table-column label="实际交付">
<template #default="{ row }">{{ row.delivered_at ? formatDateTime(row.delivered_at) : '-' }}</template>
</el-table-column>
<el-table-column label="实际收款">
</t-table-column>
<t-table-column label="实际收款">
<template #default="{ row }">{{ row.paid_at ? formatDateTime(row.paid_at) : '-' }}</template>
</el-table-column>
<el-table-column label="订单状态" width="160">
</t-table-column>
<t-table-column label="订单状态" width="160">
<template #default="{ row }">
<div style="display: flex; gap: 4px;">
<el-tag
<t-tag
:type="row.delivery_status === 'delivered' ? 'warning' : row.delivery_status === 'cancelled' ? 'danger' : 'info'"
size="small"
>{{ getDeliveryStatusLabel(row.delivery_status) }}</el-tag>
<el-tag
>{{ getDeliveryStatusLabel(row.delivery_status) }}</t-tag>
<t-tag
:type="row.payment_status === 'paid' ? 'success' : 'info'"
size="small"
>{{ getPaymentStatusLabel(row.payment_status) }}</el-tag>
>{{ getPaymentStatusLabel(row.payment_status) }}</t-tag>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="280">
</t-table-column>
<t-table-column label="操作" width="280">
<template #default="{ row }">
<el-button
<t-button
v-if="row.delivery_status === 'manufacturing'"
type="success" size="small"
@click="markDelivered(row.id)"
>已交付</el-button>
<el-button
>已交付</t-button>
<t-button
v-if="row.payment_status !== 'paid' && row.delivery_status !== 'cancelled'"
type="warning" size="small"
@click="markPaid(row.id, row.delivery_status)"
>已收款</el-button>
<el-button
>已收款</t-button>
<t-button
type="primary" size="small"
:disabled="row.payment_status === 'paid' || row.delivery_status === 'cancelled'"
@click="editOrder(row)"
>编辑</el-button>
<el-button type="danger" size="small" @click="deleteOrder(row.id)">删除</el-button>
<el-button
>编辑</t-button>
<t-button type="danger" size="small" @click="deleteOrder(row.id)">删除</t-button>
<t-button
v-if="row.payment_status !== 'paid' && row.delivery_status !== 'cancelled'"
type="info" size="small"
@click="cancelOrder(row.id)"
>作废</el-button>
>作废</t-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!state.loading && state.productionOrders.length === 0" description="暂无销售订单" />
</t-table-column>
</t-table>
<t-empty v-if="!state.loading && state.productionOrders.length === 0" description="暂无销售订单" />
<el-dialog
v-model="showModal"
<t-dialog
v-model:visible="showModal"
:title="(editingItem ? '编辑' : '新增') + '销售订单'"
width="900px"
@closed="closeModal"
>
<el-form label-width="100px">
<el-form-item label="客户" required>
<el-select v-model="form.customer_id" placeholder="请选择客户" style="width:100%">
<el-option
<t-form label-width="100px">
<t-form-item label="客户" required>
<t-select v-model="form.customer_id" placeholder="请选择客户" style="width:100%">
<t-option
v-for="customer in state.customers"
:key="customer.id"
:label="customer.name"
:value="customer.id"
/>
</el-select>
</el-form-item>
<el-form-item label="交付日期">
<el-date-picker
</t-select>
</t-form-item>
<t-form-item label="交付日期">
<t-date-picker
v-model="form.delivery_date"
type="date"
placeholder="选择日期"
style="width:100%"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.remark" placeholder="备注信息" />
</el-form-item>
</el-form>
</t-form-item>
<t-form-item label="备注">
<t-input v-model="form.remark" placeholder="备注信息" />
</t-form-item>
</t-form>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<span style="font-weight: 600;">模具明细</span>
<div style="display: flex; gap: 8px;">
<el-button v-if="editingItem" type="warning" size="small" @click="openConsumptionModal">+ 消耗物料</el-button>
<el-button type="primary" size="small" @click="addMoldItem">+ 添加模具</el-button>
<t-button v-if="editingItem" type="warning" size="small" @click="openConsumptionModal">+ 消耗物料</t-button>
<t-button type="primary" size="small" @click="addMoldItem">+ 添加模具</t-button>
</div>
</div>
<el-table :data="moldItems" border size="small">
<el-table-column label="模式" width="80">
<t-table :data="moldItems" border size="small">
<t-table-column label="模式" width="80">
<template #default="{ row: line }">
<el-select v-model="line.mold_mode" size="small" style="width:100%">
<el-option label="新模" value="new" />
<el-option label="改模" value="existing" />
</el-select>
<t-select v-model="line.mold_mode" size="small" style="width:100%">
<t-option label="新模" value="new" />
<t-option label="改模" value="existing" />
</t-select>
</template>
</el-table-column>
<el-table-column label="模具SKU" min-width="120">
</t-table-column>
<t-table-column label="模具SKU" min-width="120">
<template #default="{ row: line }">
<el-input v-if="line.mold_mode === 'new'" v-model="line.mold_sku" size="small" placeholder="模具SKU" />
<el-select v-else v-model="line.mold_id" placeholder="选择模具" size="small" style="width:100%">
<el-option
<t-input v-if="line.mold_mode === 'new'" v-model="line.mold_sku" size="small" placeholder="模具SKU" />
<t-select v-else v-model="line.mold_id" placeholder="选择模具" size="small" style="width:100%">
<t-option
v-for="product in state.finishedProducts"
:key="product.id"
:label="`${product.sku} - ${product.name}`"
:value="product.id"
/>
</el-select>
</t-select>
</template>
</el-table-column>
<el-table-column label="模具名称" min-width="120">
</t-table-column>
<t-table-column label="模具名称" min-width="120">
<template #default="{ row: line }">
<el-input
<t-input
v-if="line.mold_mode === 'new'"
v-model="line.mold_name"
size="small"
@@ -465,118 +455,118 @@ onMounted(() => {
{{ state.finishedProducts.find((p: any) => p.id === line.mold_id)?.name || '-' }}
</span>
</template>
</el-table-column>
<el-table-column label="数量" width="100">
</t-table-column>
<t-table-column label="数量" width="100">
<template #default="{ row: line }">
<el-input-number v-model="line.quantity" :min="1" size="small" style="width:100%" />
<t-input-number v-model="line.quantity" :min="1" size="small" style="width:100%" />
</template>
</el-table-column>
<el-table-column label="单价" width="120">
</t-table-column>
<t-table-column label="单价" width="120">
<template #default="{ row: line }">
<el-input-number v-model="line.unit_price" :min="0" :precision="2" :step="0.01" size="small" style="width:100%" />
<t-input-number v-model="line.unit_price" :min="0" :precision="2" :step="0.01" size="small" style="width:100%" />
</template>
</el-table-column>
<el-table-column label="备注" width="120">
</t-table-column>
<t-table-column label="备注" width="120">
<template #default="{ row: line }">
<el-input v-model="line.remark" size="small" placeholder="备注" />
<t-input v-model="line.remark" size="small" placeholder="备注" />
</template>
</el-table-column>
<el-table-column label="操作" width="80">
</t-table-column>
<t-table-column label="操作" width="80">
<template #default="{ $index }">
<el-button type="danger" size="small" @click="removeMoldItem($index)">删除</el-button>
<t-button type="danger" size="small" @click="removeMoldItem($index)">删除</t-button>
</template>
</el-table-column>
</el-table>
</t-table-column>
</t-table>
<div style="text-align: right; margin-top: 8px; font-weight: 600;">
订单总金额:{{ formatCurrency(orderTotalAmount) }}
</div>
<template v-if="editingItem && consumedMaterials.length > 0">
<el-divider content-position="left">已消耗物料</el-divider>
<el-table :data="consumedMaterials" border size="small">
<el-table-column label="物料">
<t-divider content-position="left">已消耗物料</t-divider>
<t-table :data="consumedMaterials" border size="small">
<t-table-column label="物料">
<template #default="{ row: item }">{{ item.material_name }}</template>
</el-table-column>
<el-table-column label="数量">
</t-table-column>
<t-table-column label="数量">
<template #default="{ row: item }">{{ item.quantity }}</template>
</el-table-column>
<el-table-column label="单价">
</t-table-column>
<t-table-column label="单价">
<template #default="{ row: item }">{{ formatCurrency(item.unit_price) }}</template>
</el-table-column>
<el-table-column label="备注">
</t-table-column>
<t-table-column label="备注">
<template #default="{ row: item }">{{ item.remark || '-' }}</template>
</el-table-column>
</el-table>
</t-table-column>
</t-table>
</template>
<template #footer>
<el-button @click="showModal = false">取消</el-button>
<el-button type="primary" @click="saveSalesOrder">保存</el-button>
<t-button @click="showModal = false">取消</t-button>
<t-button type="primary" @click="saveSalesOrder">保存</t-button>
</template>
</el-dialog>
</t-dialog>
<el-dialog
v-model="showConsumptionModal"
<t-dialog
v-model:visible="showConsumptionModal"
title="物料消耗"
width="800px"
@closed="closeConsumptionModal"
>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<span style="font-weight: 600;">消耗物料明细</span>
<el-button type="primary" size="small" @click="addConsumptionItem">+ 添加物料</el-button>
<t-button type="primary" size="small" @click="addConsumptionItem">+ 添加物料</t-button>
</div>
<el-table :data="consumptionItems" border size="small">
<el-table-column label="物料" min-width="200">
<t-table :data="consumptionItems" border size="small">
<t-table-column label="物料" min-width="200">
<template #default="{ row: line }">
<el-select v-model="line.material_id" placeholder="请选择物料" style="width:100%">
<el-option
<t-select v-model="line.material_id" placeholder="请选择物料" style="width:100%">
<t-option
v-for="material in state.materials"
:key="material.id"
:label="`${material.sku} - ${material.name}`"
:value="material.id"
/>
</el-select>
</t-select>
</template>
</el-table-column>
<el-table-column label="数量" width="120">
</t-table-column>
<t-table-column label="数量" width="120">
<template #default="{ row: line }">
<el-input-number v-model="line.quantity" :min="0.0001" :step="0.0001" size="small" style="width:100%" />
<t-input-number v-model="line.quantity" :min="0.0001" :step="0.0001" size="small" style="width:100%" />
</template>
</el-table-column>
<el-table-column label="单价" width="120">
</t-table-column>
<t-table-column label="单价" width="120">
<template #default="{ row: line }">
<el-input-number
<t-input-number
:model-value="state.materials.find((m: any) => m.id === line.material_id)?.cost_price || 0"
disabled
size="small"
style="width:100%"
/>
</template>
</el-table-column>
<el-table-column label="总价" width="120">
</t-table-column>
<t-table-column label="总价" width="120">
<template #default="{ row: line }">
{{ formatCurrency((state.materials.find((m: any) => m.id === line.material_id)?.cost_price || 0) * (line.quantity || 0)) }}
</template>
</el-table-column>
<el-table-column label="备注" width="150">
</t-table-column>
<t-table-column label="备注" width="150">
<template #default="{ row: line }">
<el-input v-model="line.remark" size="small" placeholder="备注" />
<t-input v-model="line.remark" size="small" placeholder="备注" />
</template>
</el-table-column>
<el-table-column label="操作" width="80">
</t-table-column>
<t-table-column label="操作" width="80">
<template #default="{ $index }">
<el-button type="danger" size="small" @click="removeConsumptionItem($index)">删除</el-button>
<t-button type="danger" size="small" @click="removeConsumptionItem($index)">删除</t-button>
</template>
</el-table-column>
</el-table>
</t-table-column>
</t-table>
<div style="text-align: right; margin-top: 8px; font-weight: 600;">
消耗总金额:{{ formatCurrency(consumptionTotalAmount) }}
</div>
<template #footer>
<el-button @click="showConsumptionModal = false">取消</el-button>
<el-button type="primary" @click="saveConsumption">保存消耗</el-button>
<t-button @click="showConsumptionModal = false">取消</t-button>
<t-button type="primary" @click="saveConsumption">保存消耗</t-button>
</template>
</el-dialog>
</t-dialog>
</div>
</template>
@@ -81,57 +81,57 @@ onMounted(() => { loadSuppliers() })
<template>
<div class="suppliers-tab">
<div class="table-header">
<el-button type="primary" @click="openCreateSupplier">+ 新增供应商</el-button>
<t-button type="primary" @click="openCreateSupplier">+ 新增供应商</t-button>
</div>
<el-table :data="state.suppliers" style="width: 100%" v-loading="state.loading" empty-text="暂无供应商数据">
<el-table-column prop="code" label="编码" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="contact_person" label="联系人">
<t-table :data="state.suppliers" style="width: 100%" :loading="state.loading" empty-text="暂无供应商数据">
<t-table-column prop="code" label="编码" />
<t-table-column prop="name" label="名称" />
<t-table-column prop="contact_person" label="联系人">
<template #default="{ row }">
{{ row.contact_person || '-' }}
</template>
</el-table-column>
<el-table-column prop="phone" label="电话">
</t-table-column>
<t-table-column prop="phone" label="电话">
<template #default="{ row }">
{{ row.phone || '-' }}
</template>
</el-table-column>
<el-table-column prop="email" label="邮箱">
</t-table-column>
<t-table-column prop="email" label="邮箱">
<template #default="{ row }">
{{ row.email || '-' }}
</template>
</el-table-column>
<el-table-column label="操作" width="180">
</t-table-column>
<t-table-column label="操作" width="180">
<template #default="{ row }">
<el-button size="small" @click="editSupplier(row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteSupplier(row.id)">删除</el-button>
<t-button size="small" @click="editSupplier(row)">编辑</t-button>
<t-button size="small" type="danger" @click="deleteSupplier(row.id)">删除</t-button>
</template>
</el-table-column>
</el-table>
</t-table-column>
</t-table>
<el-dialog v-model="showModal" :title="editingItem ? '编辑供应商' : '新增供应商'" width="520px" :close-on-click-modal="false">
<el-form :model="form" label-width="80px">
<el-form-item label="名称" required>
<el-input v-model="form.name" placeholder="请输入供应商名称" />
</el-form-item>
<el-form-item label="联系人">
<el-input v-model="form.contact_person" placeholder="请输入联系人" />
</el-form-item>
<el-form-item label="电话">
<el-input v-model="form.phone" placeholder="请输入电话" />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="form.email" type="email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="地址">
<el-input v-model="form.address" placeholder="请输入地址" />
</el-form-item>
</el-form>
<t-dialog v-model="showModal" :title="editingItem ? '编辑供应商' : '新增供应商'" width="520px" :close-on-click-modal="false">
<t-form :model="form" label-width="80px">
<t-form-item label="名称" required>
<t-input v-model="form.name" placeholder="请输入供应商名称" />
</t-form-item>
<t-form-item label="联系人">
<t-input v-model="form.contact_person" placeholder="请输入联系人" />
</t-form-item>
<t-form-item label="电话">
<t-input v-model="form.phone" placeholder="请输入电话" />
</t-form-item>
<t-form-item label="邮箱">
<t-input v-model="form.email" type="email" placeholder="请输入邮箱" />
</t-form-item>
<t-form-item label="地址">
<t-input v-model="form.address" placeholder="请输入地址" />
</t-form-item>
</t-form>
<template #footer>
<el-button @click="showModal = false">取消</el-button>
<el-button type="primary" @click="save">保存</el-button>
<t-button @click="showModal = false">取消</t-button>
<t-button type="primary" @click="save">保存</t-button>
</template>
</el-dialog>
</t-dialog>
</div>
</template>
+11 -13
View File
@@ -6,20 +6,18 @@
<h1 class="login-title">Gemold</h1>
<p class="login-subtitle">模具制造管理系统</p>
</div>
<form @submit.prevent="handleLogin">
<div class="form-group">
<label class="form-label">用户名</label>
<input v-model="state.username" type="text" class="form-input" placeholder="请输入用户名" required />
</div>
<div class="form-group">
<label class="form-label">密码</label>
<input v-model="state.password" type="password" class="form-input" placeholder="请输入密码" required />
</div>
<div class="form-error" v-if="state.error">{{ state.error }}</div>
<button type="submit" class="btn btn-primary w-full mt-4" :disabled="state.loading">
<t-form @submit="handleLogin">
<t-form-item label="用户名">
<t-input v-model="state.username" placeholder="请输入用户名" clearable />
</t-form-item>
<t-form-item label="密码">
<t-input v-model="state.password" type="password" placeholder="请输入密码" clearable />
</t-form-item>
<t-alert v-if="state.error" theme="error" :message="state.error" style="margin-bottom:16px" />
<t-button theme="primary" block type="submit" :loading="state.loading">
{{ state.loading ? '登录中...' : '登录' }}
</button>
</form>
</t-button>
</t-form>
</div>
</div>
</template>
@@ -60,19 +60,18 @@
<div class="upload-side-card">
<h2 class="section-title">2. 注塑模参数</h2>
<div class="material-panel compact-panel">
<div class="form-group">
<label class="form-label">产品材料</label>
<select v-model="state.selectedMaterial" class="form-select">
<option value="ABS">ABS (1.05 g/cm³)</option>
<option value="PP">PP (0.90 g/cm³)</option>
<option value="PE">PE (0.95 g/cm³)</option>
<option value="PC">PC (1.20 g/cm³)</option>
<option value="PA">PA (1.14 g/cm³)</option>
<option value="POM">POM (1.41 g/cm³)</option>
<option value="PMMA">PMMA (1.18 g/cm³)</option>
<option value="PBT">PBT (1.31 g/cm³)</option>
</select>
</div>
<t-form-item label="产品材料">
<t-select v-model="state.selectedMaterial">
<t-option value="ABS" label="ABS (1.05 g/cm³)" />
<t-option value="PP" label="PP (0.90 g/cm³)" />
<t-option value="PE" label="PE (0.95 g/cm³)" />
<t-option value="PC" label="PC (1.20 g/cm³)" />
<t-option value="PA" label="PA (1.14 g/cm³)" />
<t-option value="POM" label="POM (1.41 g/cm³)" />
<t-option value="PMMA" label="PMMA (1.18 g/cm³)" />
<t-option value="PBT" label="PBT (1.31 g/cm³)" />
</t-select>
</t-form-item>
<details class="advanced-params">
<summary>高级工艺参数</summary>
@@ -80,37 +79,35 @@
<div class="inline-note">默认值适用于多数注塑件;仅在已知工艺约束时再调整。</div>
<div class="advanced-param-grid">
<div class="param-input-card" v-for="field in moldParamFields" :key="field.key">
<label class="form-label">{{ field.label }}</label>
<div class="param-input-row">
<input
type="number"
class="form-input param-number-input"
<t-form-item :label="field.label">
<t-input-number
v-model.number="state.moldParams[field.key as keyof typeof state.moldParams]"
:min="field.min"
:max="field.max"
:step="field.step"
:suffix="field.unit"
/>
<span class="param-unit">{{ field.unit }}</span>
</div>
<div class="param-meta-row">
<span class="range-value">默认 {{ field.defaultValue }}{{ field.unit }}</span>
<span class="range-value">范围 {{ field.min }} - {{ field.max }}{{ field.unit }}</span>
</div>
<div class="param-hint">{{ field.hint }}</div>
<div class="param-meta-row">
<span class="range-value">默认 {{ field.defaultValue }}{{ field.unit }}</span>
<span class="range-value">范围 {{ field.min }} - {{ field.max }}{{ field.unit }}</span>
</div>
<div class="param-hint">{{ field.hint }}</div>
</t-form-item>
</div>
</div>
</div>
</details>
</div>
<button
<t-button
v-if="state.selectedFile"
class="btn-primary upload-submit-btn"
theme="primary"
class="upload-submit-btn"
@click="uploadFile"
:disabled="state.uploading || state.polling"
>
{{ state.uploading ? '上传中...' : state.polling ? '分析中...' : '3. 开始注塑模分析' }}
</button>
</t-button>
<div v-else class="inline-note">先选择 STP 文件,再填写材料并开始分析。</div>
</div>
</div>
@@ -118,90 +115,89 @@
<details v-if="state.history?.files?.length" class="history-collapsible section">
<summary class="history-summary">分析历史({{ state.history.files.length }} 个文件)</summary>
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th>文件名</th>
<th>上传次数</th>
<th>文件大小</th>
<th>最新状态</th>
<th>最新分析时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<template v-for="file in state.history.files" :key="file.filename">
<tr>
<td>{{ file.filename }}</td>
<td>
<span class="badge badge-info">{{ file.upload_count }} 次</span>
</td>
<td>{{ formatFileSize(file.file_size) }}</td>
<td>
<span :class="['badge', file.latest_status === 'completed' ? 'badge-success' : file.latest_status === 'failed' ? 'badge-error' : 'badge-warning']">
{{ file.latest_status }}
</span>
</td>
<td>{{ formatDateTime(file.latest_upload_time) }}</td>
<td>
<div class="action-buttons">
<button v-if="file.latest_status === 'completed'" class="btn-sm btn-primary" @click="viewResult({ task_id: file.latest_task_id })">
查看最新
</button>
<button class="btn-sm btn-icon" @click="toggleFileHistory(file.filename)" :title="state.expandedFiles[file.filename] ? '收起' : '展开历史记录'">
<span class="dropdown-icon" :class="{ expanded: state.expandedFiles[file.filename] }">▼</span>
</button>
</div>
</td>
</tr>
<tr v-if="state.expandedFiles[file.filename]" class="history-detail-row">
<td colspan="6">
<div class="history-dropdown">
<table class="data-table inner-table">
<thead>
<tr>
<th>上传时间</th>
<th>文件大小</th>
<th>状态</th>
<th>体积 (mm³)</th>
<th>表面积 (mm²)</th>
<th>重量 (g)</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="record in state.expandedFiles[file.filename]" :key="record.id">
<td>{{ formatDateTime(record.upload_time) }}</td>
<td>{{ formatFileSize(record.file_size) }}</td>
<td>
<span :class="['badge', record.status === 'completed' ? 'badge-success' : record.status === 'failed' ? 'badge-error' : 'badge-warning']">
{{ record.status }}
</span>
</td>
<td>{{ record.volume ? formatNumber(record.volume) : '-' }}</td>
<td>{{ record.surface_area ? formatNumber(record.surface_area) : '-' }}</td>
<td>{{ record.product_weight ? record.product_weight.toFixed(2) : '-' }}</td>
<td>
<button v-if="record.has_analysis" class="btn-sm btn-primary" @click="viewResult(record)">
查看详情
</button>
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
<t-table
row-key="filename"
:data="state.history.files"
:expanded-row-keys="historyExpandedKeys"
@expand-change="onHistoryExpand"
>
<t-table-column key="filename" title="文件名">
<template #cell="{ row }">{{ row.filename }}</template>
</t-table-column>
<t-table-column key="upload_count" title="上传次数">
<template #cell="{ row }">
<t-tag theme="primary" variant="light">{{ row.upload_count }} 次</t-tag>
</template>
</tbody>
</table>
</t-table-column>
<t-table-column key="file_size" title="文件大小">
<template #cell="{ row }">{{ formatFileSize(row.file_size) }}</template>
</t-table-column>
<t-table-column key="latest_status" title="最新状态">
<template #cell="{ row }">
<t-tag :theme="statusTagTheme(row.latest_status)" variant="light">{{ row.latest_status }}</t-tag>
</template>
</t-table-column>
<t-table-column key="latest_upload_time" title="最新分析时间">
<template #cell="{ row }">{{ formatDateTime(row.latest_upload_time) }}</template>
</t-table-column>
<t-table-column key="actions" title="操作">
<template #cell="{ row }">
<div class="action-buttons">
<t-button v-if="row.latest_status === 'completed'" theme="primary" size="small" @click="viewResult({ task_id: row.latest_task_id })">
查看最新
</t-button>
<t-button variant="text" size="small" @click="toggleFileHistory(row.filename)" :title="state.expandedFiles[row.filename] ? '收起' : '展开历史记录'">
<template #icon><span class="dropdown-icon" :class="{ expanded: state.expandedFiles[row.filename] }">▼</span></template>
</t-button>
</div>
</template>
</t-table-column>
<template #expandedRow="{ row }">
<div class="history-dropdown">
<t-table
v-if="state.expandedFiles[row.filename]"
row-key="id"
:data="state.expandedFiles[row.filename]"
>
<t-table-column key="upload_time" title="上传时间">
<template #cell="{ row: r }">{{ formatDateTime(r.upload_time) }}</template>
</t-table-column>
<t-table-column key="file_size" title="文件大小">
<template #cell="{ row: r }">{{ formatFileSize(r.file_size) }}</template>
</t-table-column>
<t-table-column key="status" title="状态">
<template #cell="{ row: r }">
<t-tag :theme="statusTagTheme(r.status)" variant="light">{{ r.status }}</t-tag>
</template>
</t-table-column>
<t-table-column key="volume" title="体积 (mm³)">
<template #cell="{ row: r }">{{ r.volume ? formatNumber(r.volume) : '-' }}</template>
</t-table-column>
<t-table-column key="surface_area" title="表面积 (mm²)">
<template #cell="{ row: r }">{{ r.surface_area ? formatNumber(r.surface_area) : '-' }}</template>
</t-table-column>
<t-table-column key="product_weight" title="重量 (g)">
<template #cell="{ row: r }">{{ r.product_weight ? r.product_weight.toFixed(2) : '-' }}</template>
</t-table-column>
<t-table-column key="detail_actions" title="操作">
<template #cell="{ row: r }">
<t-button v-if="r.has_analysis" theme="primary" size="small" @click="viewResult(r)">
查看详情
</t-button>
</template>
</t-table-column>
</t-table>
<div v-else class="inline-note">加载中...</div>
</div>
</template>
</t-table>
</div>
</details>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted } from 'vue'
import { reactive, ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAppStore } from '@/stores/app'
import { apiRequest } from '@/shared/api'
@@ -273,6 +269,20 @@ const state = reactive({
expandedFiles: {} as Record<string, HistoryRecord[] | null>,
})
const historyExpandedKeys = computed(() => {
return Object.keys(state.expandedFiles).filter(k => state.expandedFiles[k] !== null)
})
const statusTagTheme = (status: string): 'success' | 'warning' | 'danger' => {
if (status === 'completed') return 'success'
if (status === 'failed') return 'danger'
return 'warning'
}
const onHistoryExpand = async (expandedKeys: (string | number)[]) => {
// TDesign expand-change fires when keys change; we rely on toggleFileHistory instead
}
const loadHistory = async () => {
try {
state.history = await apiRequest<HistoryData>('/api/history')
+116 -136
View File
@@ -1,27 +1,21 @@
<template>
<div class="page-container">
<div class="page-header">
<button class="btn-back" @click="router.back()">
<t-button variant="text" @click="router.back()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
返回
</button>
</t-button>
<h1>分析结果</h1>
</div>
<div v-if="state.loading" class="loading-state">
<div class="spinner"></div>
<span>加载中...</span>
</div>
<t-loading :loading="state.loading" text="加载中...">
<t-alert v-if="state.error" theme="error" :message="state.error" />
<div v-else-if="state.error" class="error-state">
<p>{{ state.error }}</p>
</div>
<div v-else-if="state.task" class="result-container">
<div v-else-if="state.task" class="result-container">
<div class="result-header">
<h2>{{ state.task.filename }}</h2>
<span :class="['badge', state.task.status === 'completed' ? 'badge-success' : 'badge-error']">
<t-tag :type="state.task.status === 'completed' ? 'success' : 'danger'">
{{ state.task.status }}
</span>
</t-tag>
</div>
<div class="result-anchor-nav">
@@ -39,7 +33,7 @@
<div class="result-card result-card-highlight" style="cursor: pointer;" @click="selectScheme(selectedScheme?.scheme_id)">
<div class="summary-header">
<h3>推荐方案</h3>
<span class="badge badge-info">{{ selectedScheme?.title || selectedScheme?.scheme_id || '方案待定' }}</span>
<t-tag theme="primary">{{ selectedScheme?.title || selectedScheme?.scheme_id || '方案待定' }}</t-tag>
</div>
<div class="info-list">
<div class="info-item">
@@ -72,7 +66,7 @@
<div class="result-card result-card-highlight" v-if="candidateSchemes.length > 1" style="cursor: pointer;" @click="selectScheme(candidateSchemes[1]?.scheme_id)">
<div class="summary-header">
<h3>备选方案</h3>
<span class="badge badge-warning">1 个备选</span>
<t-tag theme="warning">1 个备选</t-tag>
</div>
<div class="info-list">
<template v-for="scheme in candidateSchemes.slice(1, 2)" :key="'alt-' + scheme.scheme_id">
@@ -102,7 +96,7 @@
<div class="result-card result-card-highlight" v-else>
<div class="summary-header">
<h3>方案概览</h3>
<span class="badge badge-success">单一方案</span>
<t-tag theme="success">单一方案</t-tag>
</div>
<div class="info-list">
<div class="info-item">
@@ -114,29 +108,26 @@
</div>
<div v-if="state.task.status === 'completed'" class="export-buttons-bar" style="display: flex; gap: var(--space-2); flex-wrap: wrap; justify-content: center; padding: var(--space-3) 0;">
<button class="btn-sm btn-primary" @click="exportCAD('step')" title="导出装配体STEP格式(UG/FreeCAD/SolidWorks通用)">
<t-button type="primary" size="small" @click="exportCAD('step')" title="导出装配体STEP格式(UG/FreeCAD/SolidWorks通用)">
📦 导出 STEP
</button>
<button class="btn-sm btn-secondary" @click="exportCAD('iges')" title="导出IGES格式(兼容旧系统)">
</t-button>
<t-button type="default" size="small" @click="exportCAD('iges')" title="导出IGES格式(兼容旧系统)">
导出 IGES
</button>
<button class="btn-sm btn-secondary" @click="exportCAD('stl')" title="导出STL网格格式(3D打印预览)">
</t-button>
<t-button type="default" size="small" @click="exportCAD('stl')" title="导出STL网格格式(3D打印预览)">
导出 STL
</button>
<button class="btn-sm btn-secondary" @click="exportCAD('brep')" title="导出BRep格式(FreeCAD原生)">
</t-button>
<t-button type="default" size="small" @click="exportCAD('brep')" title="导出BRep格式(FreeCAD原生)">
导出 BRep
</button>
</t-button>
</div>
<div id="preview-3d" v-if="selectedHtmlFile" class="viewer-section viewer-section-hero">
<div class="summary-header">
<h3>3D 预览</h3>
<span class="badge badge-info">重点区域</span>
</div>
<div v-if="state.previewStatus === 'error'" class="inline-alert inline-alert-warning" style="margin-bottom: var(--space-3);">
<div class="inline-alert-title">预览加载失败</div>
<div class="inline-alert-message">HTML 已生成但加载异常,请检查该链接是否可访问:{{ selectedHtmlFile }}</div>
<t-tag theme="primary">重点区域</t-tag>
</div>
<t-alert v-if="state.previewStatus === 'error'" theme="warning" title="预览加载失败" :message="'HTML 已生成但加载异常,请检查该链接是否可访问:' + selectedHtmlFile" style="margin-bottom: var(--space-3);" />
<iframe
:key="selectedHtmlFile"
:src="selectedHtmlFile"
@@ -149,66 +140,66 @@
<div id="preview-3d-placeholder" v-else class="viewer-section viewer-section-hero">
<div class="summary-header">
<h3>3D 预览</h3>
<span class="badge badge-warning">未生成</span>
</div>
<div class="inline-alert inline-alert-warning">
<div class="inline-alert-title">预览未生成</div>
<div class="inline-alert-message">当前任务没有返回 HTML 预览链接,属于未生成状态。</div>
<t-tag theme="warning">未生成</t-tag>
</div>
<t-alert theme="warning" title="预览未生成" message="当前任务没有返回 HTML 预览链接,属于未生成状态。" />
</div>
<div id="candidate-schemes" v-if="candidateSchemes.length > 1" class="viewer-section diagnostic-panel" style="margin-top: var(--space-3);">
<summary>📊 方案对比</summary>
<div class="result-card full-width diagnostic-panel-body">
<table class="data-table">
<thead>
<tr>
<th>方案</th>
<th>方向</th>
<th>位置</th>
<th>总分</th>
<th>可制造性</th>
<th>分型质量</th>
<th>倒扣数</th>
<th>锁模力</th>
</tr>
</thead>
<tbody>
<tr v-for="(scheme, idx) in candidateSchemes.slice(0, 2)" :key="'cmp-' + scheme.scheme_id" :style="state.selectedSchemeId === scheme.scheme_id ? 'background: rgba(var(--primary-rgb), 0.06); font-weight: 600;cursor:pointer' : 'cursor:pointer'" @click="selectScheme(scheme.scheme_id)">
<td>
<span v-if="idx === 0" style="color: var(--primary-color); font-weight: 600;">★ 推荐方案</span>
<span v-else style="color: var(--warning-color); font-weight: 600;">备选方案</span>
</td>
<td>{{ formatSchemeDirection(scheme) }}</td>
<td>{{ scheme.offset_label || '中面' }}</td>
<td>{{ formatNumber(scheme.score) }}</td>
<td>{{ formatNumber(scheme.score_breakdown?.manufacturability || 0) }}</td>
<td>{{ formatNumber(scheme.score_breakdown?.parting_quality || 0) }}</td>
<td>{{ scheme.key_info?.quality_considerations?.undercut_count || 0 }}</td>
<td>{{ scheme.cavity_data?.manufacturing_info?.estimated_clamping_force || 'N/A' }}</td>
</tr>
</tbody>
</table>
<t-table
:data="candidateSchemes.slice(0, 2)"
row-key="scheme_id"
stripe
hover
@row-click="(e: any) => selectScheme(e.row.scheme_id)"
>
<t-table-column title="方案">
<template #default="{ rowIndex }">
<span v-if="rowIndex === 0" style="color: var(--primary-color); font-weight: 600;">★ 推荐方案</span>
<span v-else style="color: var(--warning-color); font-weight: 600;">备选方案</span>
</template>
</t-table-column>
<t-table-column title="方向">
<template #default="{ row }">{{ formatSchemeDirection(row) }}</template>
</t-table-column>
<t-table-column title="位置">
<template #default="{ row }">{{ row.offset_label || '中面' }}</template>
</t-table-column>
<t-table-column title="总分">
<template #default="{ row }">{{ formatNumber(row.score) }}</template>
</t-table-column>
<t-table-column title="可制造性">
<template #default="{ row }">{{ formatNumber(row.score_breakdown?.manufacturability || 0) }}</template>
</t-table-column>
<t-table-column title="分型质量">
<template #default="{ row }">{{ formatNumber(row.score_breakdown?.parting_quality || 0) }}</template>
</t-table-column>
<t-table-column title="倒扣数">
<template #default="{ row }">{{ row.key_info?.quality_considerations?.undercut_count || 0 }}</template>
</t-table-column>
<t-table-column title="锁模力">
<template #default="{ row }">{{ row.cavity_data?.manufacturing_info?.estimated_clamping_force || 'N/A' }}</template>
</t-table-column>
</t-table>
</div>
</div>
<div id="ai-side-action" class="viewer-section">
<div class="summary-header">
<h3>AI 倒扣与抽芯分析</h3>
<span
<t-tag
v-if="sideActionAiAdvice"
:class="[
'badge',
sideActionAiAdvice.status === 'required'
? 'badge-error'
: sideActionAiAdvice.status === 'not_required'
? 'badge-success'
: 'badge-warning'
]"
:type="sideActionAiAdvice.status === 'required'
? 'danger'
: sideActionAiAdvice.status === 'not_required'
? 'success'
: 'warning'"
>
{{ sideActionAiAdvice.statusLabel }}
</span>
<span v-else class="badge badge-warning">未生成</span>
</t-tag>
<t-tag v-else theme="warning">未生成</t-tag>
</div>
<div v-if="sideActionAiAdvice" class="result-grid">
@@ -283,38 +274,35 @@
</div>
</div>
<div v-else class="inline-alert inline-alert-warning">
<div class="inline-alert-title">AI 结论未生成</div>
<div class="inline-alert-message">当前任务未生成倒扣与抽芯 AI 分析,请检查模型配置或重新运行分析。</div>
</div>
<t-alert v-else theme="warning" title="AI 结论未生成" message="当前任务未生成倒扣与抽芯 AI 分析,请检查模型配置或重新运行分析。" />
</div>
<div id="export-cam" class="viewer-section">
<div class="summary-header">
<h3>CAM 与 CNC 加工</h3>
<span class="badge badge-info">工艺规划</span>
<t-tag theme="primary">工艺规划</t-tag>
</div>
<div class="result-card" style="margin-bottom: var(--space-4);">
<h4>CAM 参数</h4>
<div class="info-list">
<div class="info-item">
<span class="info-label">模具钢</span>
<select v-model="state.camForm.mold_steel" class="form-select">
<option v-for="opt in camSteelOptions" :key="'steel-' + opt.value" :value="opt.value">{{ opt.label }}</option>
</select>
<t-select v-model="state.camForm.mold_steel" style="width:200px">
<t-option v-for="opt in camSteelOptions" :key="'steel-' + opt.value" :value="opt.value" :label="opt.label" />
</t-select>
</div>
<div class="info-item">
<span class="info-label">表面质量</span>
<select v-model="state.camForm.surface_quality" class="form-select">
<option v-for="opt in camSurfaceOptions" :key="'surf-' + opt.value" :value="opt.value">{{ opt.label }}</option>
</select>
<t-select v-model="state.camForm.surface_quality" style="width:200px">
<t-option v-for="opt in camSurfaceOptions" :key="'surf-' + opt.value" :value="opt.value" :label="opt.label" />
</t-select>
</div>
<div class="info-item">
<span class="info-label">控制器</span>
<select v-model="state.camForm.controller" class="form-select">
<option v-for="opt in camControllerOptions" :key="'ctrl-' + opt.value" :value="opt.value">{{ opt.label }}</option>
</select>
<t-select v-model="state.camForm.controller" style="width:200px">
<t-option v-for="opt in camControllerOptions" :key="'ctrl-' + opt.value" :value="opt.value" :label="opt.label" />
</t-select>
</div>
<div class="info-item">
<span class="info-label">包含G代码</span>
@@ -324,18 +312,12 @@
</label>
</div>
</div>
<button class="btn-sm btn-primary" :disabled="state.camLoading" @click="generateCamPlan()" style="margin-top: var(--space-2);" title="基于当前选中方案生成CAM工艺计划">
<t-button type="primary" size="small" :disabled="state.camLoading" @click="generateCamPlan()" style="margin-top: var(--space-2);" title="基于当前选中方案生成CAM工艺计划">
{{ state.camLoading ? '⏳ 生成中...' : '🔧 生成 CAM 计划' }}
</button>
</div>
<div v-if="state.camError" class="inline-alert inline-alert-warning">
<div class="inline-alert-title">CAM 计划生成失败</div>
<div class="inline-alert-message">{{ state.camError }}</div>
</div>
<div v-else-if="!state.camPlan" class="inline-alert inline-alert-warning">
<div class="inline-alert-title">尚未生成 CAM 计划</div>
<div class="inline-alert-message">点击上方"生成 CAM 计划"后,将返回工序计划、刀具建议与制造风险提示。</div>
</t-button>
</div>
<t-alert v-if="state.camError" theme="warning" title="CAM 计划生成失败" :message="state.camError" />
<t-alert v-else-if="!state.camPlan" theme="warning" title="尚未生成 CAM 计划" message="点击上方&quot;生成 CAM 计划&quot;后,将返回工序计划、刀具建议与制造风险提示。" />
<template v-else>
<div class="result-grid">
<div class="result-card">
@@ -375,28 +357,22 @@
</div>
<div class="result-card full-width">
<h4>工序计划</h4>
<table class="data-table">
<thead>
<tr>
<th>序号</th>
<th>工序</th>
<th>刀具</th>
<th>主轴转速</th>
<th>进给率</th>
<th>预计时长(min)</th>
</tr>
</thead>
<tbody>
<tr v-for="op in (state.camPlan.process_plan || [])" :key="'cam-op-' + op.seq">
<td>{{ op.seq }}</td>
<td>{{ op.operation }}</td>
<td>{{ op.tool_id || 'N/A' }}</td>
<td>{{ formatNumber(op.spindle_speed_rpm || 0) }}</td>
<td>{{ formatNumber(op.feed_rate_mm_min || 0) }}</td>
<td>{{ formatNumber(op.estimated_time_min || 0) }}</td>
</tr>
</tbody>
</table>
<t-table :data="state.camPlan.process_plan || []" row-key="seq" stripe>
<t-table-column colKey="seq" title="序号" />
<t-table-column colKey="operation" title="工序" />
<t-table-column title="刀具">
<template #default="{ row }">{{ row.tool_id || 'N/A' }}</template>
</t-table-column>
<t-table-column title="主轴转速">
<template #default="{ row }">{{ formatNumber(row.spindle_speed_rpm || 0) }}</template>
</t-table-column>
<t-table-column title="进给率">
<template #default="{ row }">{{ formatNumber(row.feed_rate_mm_min || 0) }}</template>
</t-table-column>
<t-table-column title="预计时长(min)">
<template #default="{ row }">{{ formatNumber(row.estimated_time_min || 0) }}</template>
</t-table-column>
</t-table>
</div>
<div class="result-card full-width">
<h4>制造风险提示</h4>
@@ -412,9 +388,9 @@
<div id="llm-report" class="viewer-section" v-if="hasVisibleDesignReport">
<div class="summary-header">
<h3>LLM 设计报告</h3>
<span class="badge" :class="(designReport?.overall_score || 0) >= 7 ? 'badge-success' : (designReport?.overall_score || 0) >= 5 ? 'badge-warning' : 'badge-error'">
<t-tag :type="(designReport?.overall_score || 0) >= 7 ? 'success' : (designReport?.overall_score || 0) >= 5 ? 'warning' : 'danger'">
评分 {{ designReport?.overall_score || 'N/A' }} / 10
</span>
</t-tag>
</div>
<div v-if="designReport?.overview" class="inline-note" style="margin-bottom: var(--space-4); font-size: 0.95rem; color: var(--text-primary);">
{{ designReport?.overview }}
@@ -438,18 +414,13 @@
</template>
<template v-else-if="sec.type === 'params_table'">
<table class="data-table">
<thead>
<tr>
<th v-for="(h, hi) in sec.headers" :key="'dr-h-' + hi">{{ h }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, ri) in sec.rows" :key="'dr-r-' + ri">
<td v-for="(cell, ci) in row" :key="'dr-c-' + ci">{{ cell }}</td>
</tr>
</tbody>
</table>
<t-table
:columns="sec.headers!.map((h: string, i: number) => ({ colKey: 'col_' + i, title: h }))"
:data="buildDynamicTableData(sec.rows!)"
row-key="_rowId"
stripe
bordered
/>
</template>
<template v-else-if="sec.type === 'recommendations'">
@@ -465,7 +436,8 @@
</template>
</div>
</div>
</div>
</div>
</t-loading>
</div>
</template>
@@ -837,7 +809,7 @@ const sideActionAiAdvice = computed(() => {
})
const ruleActionSummary = computed(() => {
const summary = selectedScheme.value?.side_actions?.summary || {}
const summary: any = selectedScheme.value?.side_actions?.summary || {}
return {
total_slider_count: summary.total_slider_count ?? 0,
total_lifter_count: summary.total_lifter_count ?? 0,
@@ -903,6 +875,14 @@ const resultAnchorLinks = [
{ id: 'llm-report', label: '设计报告' }
]
const buildDynamicTableData = (rows: unknown[][]) => {
return rows.map((row, ri) => {
const obj: Record<string, any> = { _rowId: ri }
row.forEach((cell, ci) => { obj['col_' + ci] = cell })
return obj
})
}
const selectScheme = (schemeId: string | undefined) => {
if (schemeId) {
state.selectedSchemeId = schemeId
+86 -85
View File
@@ -5,100 +5,86 @@
<h1>用户管理</h1>
<p>管理系统用户和权限</p>
</div>
<button class="btn btn-primary" @click="openUserModal()">+ 添加用户</button>
<t-button theme="primary" @click="openUserModal()">+ 添加用户</t-button>
</div>
<div v-if="state.loading" class="loading-state">
<div class="loading-spinner"></div>
<span>加载中...</span>
</div>
<t-loading :loading="state.loading" size="large">
<t-table :data="state.users" row-key="id" stripe>
<t-table-column colKey="username" title="用户名" />
<t-table-column colKey="email" title="邮箱" />
<t-table-column colKey="full_name" title="姓名">
<template #cell="{ row }">{{ row.full_name || '-' }}</template>
</t-table-column>
<t-table-column colKey="is_active" title="状态">
<template #cell="{ row }">
<t-tag :theme="row.is_active ? 'success' : 'danger'">
{{ row.is_active ? '正常' : '禁用' }}
</t-tag>
</template>
</t-table-column>
<t-table-column colKey="roles" title="角色">
<template #cell="{ row }">
<t-tag v-for="role in row.roles" :key="role" theme="primary" variant="light" style="margin-right: 4px;">
{{ role }}
</t-tag>
</template>
</t-table-column>
<t-table-column colKey="created_at" title="注册时间">
<template #cell="{ row }">{{ formatDateTime(row.created_at) }}</template>
</t-table-column>
<t-table-column colKey="actions" title="操作">
<template #cell="{ row }">
<t-space :size="4">
<t-button theme="primary" size="small" @click="openUserModal(row)">编辑</t-button>
<t-button theme="warning" size="small" @click="resetPassword(row)">重置密码</t-button>
<t-button v-if="row.id !== storeUser?.id" theme="danger" size="small" @click="deleteUser(row)">删除</t-button>
</t-space>
</template>
</t-table-column>
</t-table>
<t-empty v-if="!state.loading && state.users.length === 0" description="暂无用户数据" />
</t-loading>
<div v-else class="table-container">
<table class="data-table">
<thead>
<tr>
<th>用户名</th>
<th>邮箱</th>
<th>姓名</th>
<th>状态</th>
<th>角色</th>
<th>注册时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="user in state.users" :key="user.id">
<td>{{ user.username }}</td>
<td>{{ user.email }}</td>
<td>{{ user.full_name || '-' }}</td>
<td>
<span :class="['badge', user.is_active ? 'badge-success' : 'badge-error']">
{{ user.is_active ? '正常' : '禁用' }}
</span>
</td>
<td>
<span v-for="role in (user as any).roles" :key="role" class="badge badge-info" style="margin-right: 4px;">
{{ role }}
</span>
</td>
<td>{{ formatDateTime(user.created_at) }}</td>
<td>
<div class="action-buttons" style="display:flex; gap:4px;">
<button class="btn btn-sm" style="background:var(--primary-500);color:white;" @click="openUserModal(user)">编辑</button>
<button class="btn btn-sm" style="background:#eab308;color:white;" @click="resetPassword(user)">重置密码</button>
<button v-if="(user as any).id !== storeUser?.id" class="btn btn-sm" style="background:var(--error);color:white;" @click="deleteUser(user)">删除</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="state.showUserModal" class="modal-overlay" @click.self="state.showUserModal = false">
<div class="modal-content">
<div class="modal-header">
<h2>{{ state.editingUser ? '编辑用户' : '添加用户' }}</h2>
<button class="modal-close" @click="state.showUserModal = false">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">用户名</label>
<input v-model="state.userForm.username" type="text" class="form-input" :disabled="!!state.editingUser" />
<t-dialog
v-model:visible="state.showUserModal"
:header="state.editingUser ? '编辑用户' : '添加用户'"
:close-on-overlay-click="true"
width="520px"
>
<t-form label-width="80px">
<t-form-item label="用户名">
<t-input v-model="state.userForm.username" :disabled="!!state.editingUser" />
</t-form-item>
<t-form-item label="邮箱">
<t-input v-model="state.userForm.email" type="email" />
</t-form-item>
<t-form-item v-if="!state.editingUser" label="密码">
<t-input v-model="state.userForm.password" type="password" />
</t-form-item>
<t-form-item label="姓名">
<t-input v-model="state.userForm.full_name" />
</t-form-item>
<t-form-item label="角色">
<div style="display: flex; flex-direction: column; gap: 8px;">
<label v-for="role in state.roles" :key="role.id" style="display: flex; align-items: center; gap: 6px; cursor: pointer;">
<input type="checkbox" :value="role.id" v-model="state.userForm.role_ids" />
{{ role.name }}
</label>
</div>
<div class="form-group">
<label class="form-label">邮箱</label>
<input v-model="state.userForm.email" type="email" class="form-input" />
</div>
<div class="form-group" v-if="!state.editingUser">
<label class="form-label">密码</label>
<input v-model="state.userForm.password" type="password" class="form-input" />
</div>
<div class="form-group">
<label class="form-label">姓名</label>
<input v-model="state.userForm.full_name" type="text" class="form-input" />
</div>
<div class="form-group">
<label class="form-label">角色</label>
<div class="checkbox-group" style="flex-direction: column; gap: 8px;">
<label v-for="role in state.roles" :key="(role as any).id" class="checkbox-label">
<input type="checkbox" :value="(role as any).id" v-model="state.userForm.role_ids" class="checkbox-input" />
{{ (role as any).name }}
</label>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @click="state.showUserModal = false">取消</button>
<button class="btn btn-primary" @click="saveUser">保存</button>
</div>
</div>
</div>
</t-form-item>
</t-form>
<template #footer>
<t-button theme="default" @click="state.showUserModal = false">取消</t-button>
<t-button theme="primary" @click="saveUser">保存</t-button>
</template>
</t-dialog>
</div>
</template>
<script setup lang="ts">
import { reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { DialogPlugin } from 'tdesign-vue-next'
import { apiRequest } from '@/shared/api'
import { useAppStore } from '@/stores/app'
import { addNotification } from '@/shared/notification'
@@ -139,6 +125,21 @@ const state = reactive({
},
})
function confirmDialog(header: string, body: string, theme: string, confirmText: string, cancelText: string): Promise<boolean> {
return new Promise((resolve) => {
const dlg = DialogPlugin.confirm({
header,
body,
theme: theme as any,
confirmBtn: confirmText,
cancelBtn: cancelText,
onConfirm: () => { dlg.hide(); resolve(true) },
onCancel: () => { dlg.hide(); resolve(false) },
onClose: () => { resolve(false) },
})
})
}
const loadUsers = async () => {
try {
state.users = await apiRequest<UserItem[]>('/api/auth/users')
@@ -217,7 +218,7 @@ const saveUser = async () => {
}
const deleteUser = async (user: UserItem) => {
if (!confirm(`确定要删除用户 ${user.username} 吗?`)) return
if (!await confirmDialog('删除确认', `确定要删除用户 ${user.username} 吗?`, 'warning', '删除', '取消')) return
try {
await apiRequest(`/api/auth/users/${user.id}`, { method: 'DELETE' })