up
This commit is contained in:
@@ -1,23 +1,29 @@
|
|||||||
# 2026-04-29 工作日志
|
# 2026-04-29 工作日志
|
||||||
|
|
||||||
## 完成的工作
|
## 仓库物品列表添加列
|
||||||
|
|
||||||
### 1. 客户详情页添加编辑按钮
|
在仓库容器详情页的物品列表中,在"数量"列后面添加了"工单数量"和"关联客户"两列。
|
||||||
- 文件: `frontend/ops_vue_js/src/views/customer/CustomerDetail.vue`
|
|
||||||
- 功能: 根据后端返回的 `canModify` 字段显示/隐藏编辑按钮
|
|
||||||
- 权限逻辑: 创建者或客户管理员可编辑
|
|
||||||
|
|
||||||
### 2. 操作日志功能
|
### 修改内容
|
||||||
- 后端 API: `backend/my_work/routers/apiSysAdmin.go`
|
|
||||||
- 新增 `/operation_logs` 接口,聚合所有模块的操作日志
|
|
||||||
- 支持按模块筛选: all/customer/purchase/schedule/warehouse/work_order
|
|
||||||
- 支持分页,最新日志在前
|
|
||||||
|
|
||||||
- 前端组件: `frontend/ops_vue_js/src/views/sysadmin/OperationLogsTab.vue`
|
**后端** (`backend/my_work/routers/apiWarehouse.go`):
|
||||||
- 左侧模块选择器,右侧日志表格
|
- 修改 `/list_item` API,批量查询物品的工单绑定数量和客户关联信息
|
||||||
- 分页显示,最新日志在前
|
- 新增返回字段:`WorkOrderCount` (int) 和 `Customers` (数组)
|
||||||
|
|
||||||
- 系统管理页面: `frontend/ops_vue_js/src/views/sysadmin/SysAdminView.vue`
|
**前端** (`frontend/ops_vue_js/src/views/warehouse/WarehouseContainerDetail.vue`):
|
||||||
- 新增"操作日志"标签页
|
- 导入 `IconTool`, `IconUser` 图标和 `RouterLink` 组件
|
||||||
|
- 表格表头添加两列:工单数量、关联客户
|
||||||
|
- 表格数据行显示:
|
||||||
|
- 工单数量:橙色徽章显示,带工具图标
|
||||||
|
- 关联客户:蓝色标签显示客户姓名,可点击跳转,最多显示3个
|
||||||
|
- 更新 colspan 从 8 改为 10
|
||||||
|
|
||||||
- 翻译文件: 添加了 `operation_logs` 相关翻译键
|
**i18n 翻译**:
|
||||||
|
- `en.json`: 添加 `work_order.work_order_count` 和 `customer.related_customers`
|
||||||
|
- `zh-CN.json`: 添加对应中文翻译
|
||||||
|
- **修复**: 修正了重复添加 `customer` 对象的问题,将 `related_customers` 合并到已有的 `customer` 对象中
|
||||||
|
|
||||||
|
### 技术细节
|
||||||
|
- 工单数量通过 `TabWarehouseItemWorkOrderBind` 表统计
|
||||||
|
- 客户信息通过 `TabWarehouseItemCustomerBind` 和 `TabCustomer` 表关联查询
|
||||||
|
- 采用批量查询优化性能,避免N+1查询问题
|
||||||
|
|||||||
@@ -902,14 +902,73 @@ func ApiWarehouse(r *gin.RouterGroup) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 为每个物品计算面包屑
|
// 收集所有物品ID
|
||||||
|
itemIDs := make([]uint, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
itemIDs = append(itemIDs, item.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量查询工单绑定数量
|
||||||
|
workOrderCounts := make(map[uint]int)
|
||||||
|
if len(itemIDs) > 0 {
|
||||||
|
var woBinds []TabWarehouseItemWorkOrderBind
|
||||||
|
models.DB.Where("item_id IN ?", itemIDs).Find(&woBinds)
|
||||||
|
for _, bind := range woBinds {
|
||||||
|
workOrderCounts[bind.ItemID]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量查询客户关联
|
||||||
|
type CustomerInfo struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
FirstName string `json:"first_name"`
|
||||||
|
LastName string `json:"last_name"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
}
|
||||||
|
itemCustomers := make(map[uint][]CustomerInfo)
|
||||||
|
if len(itemIDs) > 0 {
|
||||||
|
var customerBinds []TabWarehouseItemCustomerBind
|
||||||
|
models.DB.Where("item_id IN ?", itemIDs).Find(&customerBinds)
|
||||||
|
customerIDs := make([]uint, 0)
|
||||||
|
for _, bind := range customerBinds {
|
||||||
|
customerIDs = append(customerIDs, bind.CustomerID)
|
||||||
|
}
|
||||||
|
// 查询客户信息
|
||||||
|
customerMap := make(map[uint]TabCustomer)
|
||||||
|
if len(customerIDs) > 0 {
|
||||||
|
var customers []TabCustomer
|
||||||
|
models.DB.Where("id IN ?", customerIDs).Find(&customers)
|
||||||
|
for _, c := range customers {
|
||||||
|
customerMap[c.ID] = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 构建物品ID到客户列表的映射
|
||||||
|
for _, bind := range customerBinds {
|
||||||
|
if c, ok := customerMap[bind.CustomerID]; ok {
|
||||||
|
itemCustomers[bind.ItemID] = append(itemCustomers[bind.ItemID], CustomerInfo{
|
||||||
|
ID: c.ID,
|
||||||
|
FirstName: c.FirstName,
|
||||||
|
LastName: c.LastName,
|
||||||
|
Title: c.Title,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为每个物品计算面包屑并添加额外信息
|
||||||
type ItemWithBreadcrumb struct {
|
type ItemWithBreadcrumb struct {
|
||||||
TabWarehouseItem
|
TabWarehouseItem
|
||||||
ContainerBreadcrumb string `json:"ContainerBreadcrumb"`
|
ContainerBreadcrumb string `json:"ContainerBreadcrumb"`
|
||||||
|
WorkOrderCount int `json:"WorkOrderCount"`
|
||||||
|
Customers []CustomerInfo `json:"Customers"`
|
||||||
}
|
}
|
||||||
itemsWithBreadcrumb := make([]ItemWithBreadcrumb, len(items))
|
itemsWithBreadcrumb := make([]ItemWithBreadcrumb, len(items))
|
||||||
for i, item := range items {
|
for i, item := range items {
|
||||||
itemsWithBreadcrumb[i] = ItemWithBreadcrumb{TabWarehouseItem: item}
|
itemsWithBreadcrumb[i] = ItemWithBreadcrumb{
|
||||||
|
TabWarehouseItem: item,
|
||||||
|
WorkOrderCount: workOrderCounts[item.ID],
|
||||||
|
Customers: itemCustomers[item.ID],
|
||||||
|
}
|
||||||
if item.ContainerID != nil {
|
if item.ContainerID != nil {
|
||||||
itemsWithBreadcrumb[i].ContainerBreadcrumb = buildContainerBreadcrumb(*item.ContainerID, containerMap)
|
itemsWithBreadcrumb[i].ContainerBreadcrumb = buildContainerBreadcrumb(*item.ContainerID, containerMap)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -330,9 +330,58 @@ func ApiWorkOrder(r *gin.RouterGroup) {
|
|||||||
Limit(from.Entries).
|
Limit(from.Entries).
|
||||||
Find(&orders)
|
Find(&orders)
|
||||||
|
|
||||||
|
// 定义返回结构
|
||||||
|
type CustomerInfo struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
FirstName string `json:"first_name"`
|
||||||
|
LastName string `json:"last_name"`
|
||||||
|
PrimaryPhone string `json:"primary_phone"`
|
||||||
|
}
|
||||||
|
type OrderWithCustomers struct {
|
||||||
|
TabWorkOrder
|
||||||
|
Customers []CustomerInfo `json:"customers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var ordersWithCustomers []OrderWithCustomers
|
||||||
|
for _, order := range orders {
|
||||||
|
orderItem := OrderWithCustomers{
|
||||||
|
TabWorkOrder: order,
|
||||||
|
Customers: []CustomerInfo{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询关联客户
|
||||||
|
var customerBinds []TabWorkOrderCustomerBind
|
||||||
|
models.DB.Where("work_order_id = ?", order.ID).Find(&customerBinds)
|
||||||
|
if len(customerBinds) > 0 {
|
||||||
|
var customerIDs []uint
|
||||||
|
for _, b := range customerBinds {
|
||||||
|
customerIDs = append(customerIDs, b.CustomerID)
|
||||||
|
}
|
||||||
|
var customers []TabCustomer
|
||||||
|
models.DB.Where("id IN ?", customerIDs).Find(&customers)
|
||||||
|
for _, c := range customers {
|
||||||
|
customerInfo := CustomerInfo{
|
||||||
|
ID: c.ID,
|
||||||
|
FirstName: c.FirstName,
|
||||||
|
LastName: c.LastName,
|
||||||
|
PrimaryPhone: "",
|
||||||
|
}
|
||||||
|
// 获取主电话
|
||||||
|
var phone TabCustomerPhone
|
||||||
|
if err := models.DB.Where("customer_id = ? AND is_primary = ?", c.ID, true).First(&phone).Error; err == nil {
|
||||||
|
customerInfo.PrimaryPhone = phone.Phone
|
||||||
|
} else if err := models.DB.Where("customer_id = ?", c.ID).First(&phone).Error; err == nil {
|
||||||
|
customerInfo.PrimaryPhone = phone.Phone
|
||||||
|
}
|
||||||
|
orderItem.Customers = append(orderItem.Customers, customerInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ordersWithCustomers = append(ordersWithCustomers, orderItem)
|
||||||
|
}
|
||||||
|
|
||||||
ReturnJson(ctx, "apiOK", gin.H{
|
ReturnJson(ctx, "apiOK", gin.H{
|
||||||
"all_count": count,
|
"all_count": count,
|
||||||
"all_orders": orders,
|
"all_orders": ordersWithCustomers,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -174,7 +174,8 @@
|
|||||||
"confirm_delete_commit": "Are you sure you want to delete this progress?",
|
"confirm_delete_commit": "Are you sure you want to delete this progress?",
|
||||||
"linked_items": "Linked Items",
|
"linked_items": "Linked Items",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
"save_changes": "Save Changes"
|
"save_changes": "Save Changes",
|
||||||
|
"work_order_count": "Work Order Count"
|
||||||
},
|
},
|
||||||
"warehouse": {
|
"warehouse": {
|
||||||
"title": "Warehouse",
|
"title": "Warehouse",
|
||||||
@@ -598,6 +599,7 @@
|
|||||||
"detail_title": "Customer Detail",
|
"detail_title": "Customer Detail",
|
||||||
"basic_info": "Basic Information",
|
"basic_info": "Basic Information",
|
||||||
"created_by": "Created By",
|
"created_by": "Created By",
|
||||||
"not_found": "Customer not found"
|
"not_found": "Customer not found",
|
||||||
|
"related_customers": "Related Customers"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,7 +174,8 @@
|
|||||||
"confirm_delete_commit": "确定要删除此进度吗?",
|
"confirm_delete_commit": "确定要删除此进度吗?",
|
||||||
"linked_items": "关联物品",
|
"linked_items": "关联物品",
|
||||||
"submit": "提交",
|
"submit": "提交",
|
||||||
"save_changes": "保存修改"
|
"save_changes": "保存修改",
|
||||||
|
"work_order_count": "工单数量"
|
||||||
},
|
},
|
||||||
"warehouse": {
|
"warehouse": {
|
||||||
"title": "仓库",
|
"title": "仓库",
|
||||||
@@ -598,6 +599,7 @@
|
|||||||
"detail_title": "客户详情",
|
"detail_title": "客户详情",
|
||||||
"basic_info": "基本信息",
|
"basic_info": "基本信息",
|
||||||
"created_by": "创建者",
|
"created_by": "创建者",
|
||||||
"not_found": "客户不存在"
|
"not_found": "客户不存在",
|
||||||
|
"related_customers": "关联客户"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ async function submit() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 搜索框 -->
|
<!-- 搜索框 -->
|
||||||
<div class="customer-search-wrapper relative">
|
<div class="customer-search-wrapper relative" style="z-index: 9999;">
|
||||||
<input
|
<input
|
||||||
v-model="customerSearchQuery"
|
v-model="customerSearchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -267,7 +267,7 @@ async function submit() {
|
|||||||
<!-- 下拉结果 -->
|
<!-- 下拉结果 -->
|
||||||
<div
|
<div
|
||||||
v-if="showCustomerDropdown && customerSearchResults.length > 0"
|
v-if="showCustomerDropdown && customerSearchResults.length > 0"
|
||||||
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
class="absolute z-[9999] mt-1 max-h-60 w-full overflow-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="customer in customerSearchResults"
|
v-for="customer in customerSearchResults"
|
||||||
@@ -284,14 +284,14 @@ async function submit() {
|
|||||||
<!-- 加载中 -->
|
<!-- 加载中 -->
|
||||||
<div
|
<div
|
||||||
v-if="showCustomerDropdown && customerSearchLoading"
|
v-if="showCustomerDropdown && customerSearchLoading"
|
||||||
class="absolute z-10 mt-1 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-500 shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
class="absolute z-[9999] mt-1 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-500 shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
||||||
>
|
>
|
||||||
{{ t('message.loading') }}
|
{{ t('message.loading') }}
|
||||||
</div>
|
</div>
|
||||||
<!-- 无结果 -->
|
<!-- 无结果 -->
|
||||||
<div
|
<div
|
||||||
v-if="showCustomerDropdown && !customerSearchLoading && customerSearchResults.length === 0 && customerSearchQuery.trim().length > 0"
|
v-if="showCustomerDropdown && !customerSearchLoading && customerSearchResults.length === 0 && customerSearchQuery.trim().length > 0"
|
||||||
class="absolute z-10 mt-1 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-500 shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
class="absolute z-[9999] mt-1 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-500 shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
||||||
>
|
>
|
||||||
{{ t('warehouse.linked_customer_not_found') || '未找到客户' }}
|
{{ t('warehouse.linked_customer_not_found') || '未找到客户' }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter, RouterLink } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useToastStore } from '@/stores/toast'
|
import { useToastStore } from '@/stores/toast'
|
||||||
import { usePageTitle } from '@/composables/usePageTitle'
|
import { usePageTitle } from '@/composables/usePageTitle'
|
||||||
@@ -16,6 +16,8 @@ import {
|
|||||||
IconEdit,
|
IconEdit,
|
||||||
IconTrash,
|
IconTrash,
|
||||||
IconSearch,
|
IconSearch,
|
||||||
|
IconTool,
|
||||||
|
IconUser,
|
||||||
} from '@tabler/icons-vue'
|
} from '@tabler/icons-vue'
|
||||||
|
|
||||||
usePageTitle('warehouse.container_detail')
|
usePageTitle('warehouse.container_detail')
|
||||||
@@ -557,6 +559,8 @@ onMounted(async () => {
|
|||||||
<th class="px-5 py-3 font-medium">{{ t('warehouse.serial_number') }}</th>
|
<th class="px-5 py-3 font-medium">{{ t('warehouse.serial_number') }}</th>
|
||||||
<th class="px-5 py-3 font-medium">{{ t('warehouse.remark') }}</th>
|
<th class="px-5 py-3 font-medium">{{ t('warehouse.remark') }}</th>
|
||||||
<th class="px-5 py-3 font-medium w-20 text-center">{{ t('warehouse.quantity') }}</th>
|
<th class="px-5 py-3 font-medium w-20 text-center">{{ t('warehouse.quantity') }}</th>
|
||||||
|
<th class="px-5 py-3 font-medium w-24 text-center">{{ t('work_order.work_order_count') }}</th>
|
||||||
|
<th class="px-5 py-3 font-medium">{{ t('customer.related_customers') }}</th>
|
||||||
<th class="px-5 py-3 font-medium whitespace-nowrap">{{ t('warehouse.created_at') }}</th>
|
<th class="px-5 py-3 font-medium whitespace-nowrap">{{ t('warehouse.created_at') }}</th>
|
||||||
<th class="px-5 py-3 font-medium whitespace-nowrap">{{ t('warehouse.updated_at') }}</th>
|
<th class="px-5 py-3 font-medium whitespace-nowrap">{{ t('warehouse.updated_at') }}</th>
|
||||||
<th class="px-5 py-3 font-medium">{{ t('warehouse.created_by') }}</th>
|
<th class="px-5 py-3 font-medium">{{ t('warehouse.created_by') }}</th>
|
||||||
@@ -565,7 +569,7 @@ onMounted(async () => {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-if="loadingItems">
|
<tr v-if="loadingItems">
|
||||||
<td colspan="8" class="px-5 py-8 text-center">
|
<td colspan="10" class="px-5 py-8 text-center">
|
||||||
<svg class="mx-auto h-5 w-5 animate-spin text-gray-400" viewBox="0 0 24 24" fill="none">
|
<svg class="mx-auto h-5 w-5 animate-spin text-gray-400" viewBox="0 0 24 24" fill="none">
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
|
||||||
@@ -573,7 +577,7 @@ onMounted(async () => {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-else-if="items.length === 0">
|
<tr v-else-if="items.length === 0">
|
||||||
<td colspan="8" class="px-5 py-8 text-center text-gray-400 dark:text-gray-500">
|
<td colspan="10" class="px-5 py-8 text-center text-gray-400 dark:text-gray-500">
|
||||||
{{ t('warehouse.no_items') }}
|
{{ t('warehouse.no_items') }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -593,6 +597,29 @@ onMounted(async () => {
|
|||||||
<td class="px-5 py-3 text-xs text-gray-500 dark:text-gray-400 max-w-[140px] truncate">{{ item.SerialNumber || '—' }}</td>
|
<td class="px-5 py-3 text-xs text-gray-500 dark:text-gray-400 max-w-[140px] truncate">{{ item.SerialNumber || '—' }}</td>
|
||||||
<td class="px-5 py-3 text-xs text-gray-500 dark:text-gray-400 max-w-[200px] truncate">{{ item.Remark || '—' }}</td>
|
<td class="px-5 py-3 text-xs text-gray-500 dark:text-gray-400 max-w-[200px] truncate">{{ item.Remark || '—' }}</td>
|
||||||
<td class="px-5 py-3 text-center text-sm">{{ item.Quantity }}</td>
|
<td class="px-5 py-3 text-center text-sm">{{ item.Quantity }}</td>
|
||||||
|
<td class="px-5 py-3 text-center">
|
||||||
|
<span v-if="item.WorkOrderCount > 0" class="inline-flex items-center gap-1 rounded-full bg-orange-100 px-2 py-0.5 text-xs font-medium text-orange-700 dark:bg-orange-900/40 dark:text-orange-400">
|
||||||
|
<IconTool :size="12" />
|
||||||
|
{{ item.WorkOrderCount }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="text-gray-400">—</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-5 py-3">
|
||||||
|
<div v-if="item.Customers && item.Customers.length > 0" class="flex flex-wrap gap-1">
|
||||||
|
<RouterLink
|
||||||
|
v-for="customer in item.Customers.slice(0, 3)"
|
||||||
|
:key="customer.id"
|
||||||
|
:to="`/customer/${customer.id}`"
|
||||||
|
class="inline-flex items-center gap-1 rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700 hover:bg-blue-200 dark:bg-blue-900/40 dark:text-blue-400 dark:hover:bg-blue-900/60"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<IconUser :size="10" />
|
||||||
|
{{ customer.first_name }} {{ customer.last_name }}
|
||||||
|
</RouterLink>
|
||||||
|
<span v-if="item.Customers.length > 3" class="text-xs text-gray-400">+{{ item.Customers.length - 3 }}</span>
|
||||||
|
</div>
|
||||||
|
<span v-else class="text-gray-400">—</span>
|
||||||
|
</td>
|
||||||
<td class="px-5 py-3 text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap">{{ fmtTs(item.CreatedAt) }}</td>
|
<td class="px-5 py-3 text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap">{{ fmtTs(item.CreatedAt) }}</td>
|
||||||
<td class="px-5 py-3 text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap">{{ fmtTs(item.UpdatedAt) }}</td>
|
<td class="px-5 py-3 text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap">{{ fmtTs(item.UpdatedAt) }}</td>
|
||||||
<td class="px-5 py-3">
|
<td class="px-5 py-3">
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const submitting = ref(false)
|
|||||||
const loadingItem = ref(true)
|
const loadingItem = ref(true)
|
||||||
const itemNotFound = ref(false)
|
const itemNotFound = ref(false)
|
||||||
const containerName = ref('')
|
const containerName = ref('')
|
||||||
|
const containerId = ref(null)
|
||||||
const existingPhotos = ref([])
|
const existingPhotos = ref([])
|
||||||
|
|
||||||
const dropzoneRef = ref(null)
|
const dropzoneRef = ref(null)
|
||||||
@@ -117,10 +118,13 @@ onMounted(async () => {
|
|||||||
|
|
||||||
// 获取容器名称
|
// 获取容器名称
|
||||||
if (data.item.ContainerID) {
|
if (data.item.ContainerID) {
|
||||||
|
containerId.value = data.item.ContainerID
|
||||||
const { errCode: cErr, data: cData } = await warehouseApi.getContainer(data.item.ContainerID)
|
const { errCode: cErr, data: cData } = await warehouseApi.getContainer(data.item.ContainerID)
|
||||||
if (cErr === 0 && cData?.container) {
|
if (cErr === 0 && cData?.container) {
|
||||||
containerName.value = cData.container.Title
|
containerName.value = cData.container.Title
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
containerId.value = null
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
itemNotFound.value = true
|
itemNotFound.value = true
|
||||||
@@ -193,14 +197,15 @@ async function submit() {
|
|||||||
<RouterLink to="/warehouse/container" class="text-blue-500 hover:underline">
|
<RouterLink to="/warehouse/container" class="text-blue-500 hover:underline">
|
||||||
{{ t('warehouse.container_list') }}
|
{{ t('warehouse.container_list') }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<span>/</span>
|
<template v-if="containerName">
|
||||||
<RouterLink
|
<span>/</span>
|
||||||
v-if="containerName"
|
<RouterLink
|
||||||
:to="`/warehouse/container/${itemId}`"
|
:to="`/warehouse/container/${containerId}`"
|
||||||
class="text-blue-500 hover:underline"
|
class="text-blue-500 hover:underline"
|
||||||
>
|
>
|
||||||
{{ containerName }}
|
{{ containerName }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
</template>
|
||||||
<span v-else>/</span>
|
<span v-else>/</span>
|
||||||
<span>/</span>
|
<span>/</span>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
@@ -300,7 +305,7 @@ async function submit() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 搜索框 -->
|
<!-- 搜索框 -->
|
||||||
<div class="customer-search-wrapper relative">
|
<div class="customer-search-wrapper relative" style="z-index: 9999;">
|
||||||
<input
|
<input
|
||||||
v-model="customerSearchQuery"
|
v-model="customerSearchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -312,7 +317,7 @@ async function submit() {
|
|||||||
<!-- 下拉结果 -->
|
<!-- 下拉结果 -->
|
||||||
<div
|
<div
|
||||||
v-if="showCustomerDropdown && customerSearchResults.length > 0"
|
v-if="showCustomerDropdown && customerSearchResults.length > 0"
|
||||||
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
class="absolute z-[9999] mt-1 max-h-60 w-full overflow-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="customer in customerSearchResults"
|
v-for="customer in customerSearchResults"
|
||||||
@@ -329,14 +334,14 @@ async function submit() {
|
|||||||
<!-- 加载中 -->
|
<!-- 加载中 -->
|
||||||
<div
|
<div
|
||||||
v-if="showCustomerDropdown && customerSearchLoading"
|
v-if="showCustomerDropdown && customerSearchLoading"
|
||||||
class="absolute z-10 mt-1 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-500 shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
class="absolute z-[9999] mt-1 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-500 shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
||||||
>
|
>
|
||||||
{{ t('message.loading') }}
|
{{ t('message.loading') }}
|
||||||
</div>
|
</div>
|
||||||
<!-- 无结果 -->
|
<!-- 无结果 -->
|
||||||
<div
|
<div
|
||||||
v-if="showCustomerDropdown && !customerSearchLoading && customerSearchResults.length === 0 && customerSearchQuery.trim().length > 0"
|
v-if="showCustomerDropdown && !customerSearchLoading && customerSearchResults.length === 0 && customerSearchQuery.trim().length > 0"
|
||||||
class="absolute z-10 mt-1 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-500 shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
class="absolute z-[9999] mt-1 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-500 shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
||||||
>
|
>
|
||||||
{{ t('warehouse.linked_customer_not_found') }}
|
{{ t('warehouse.linked_customer_not_found') }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter, RouterLink } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useToastStore } from '@/stores/toast'
|
import { useToastStore } from '@/stores/toast'
|
||||||
import { usePageTitle } from '@/composables/usePageTitle'
|
import { usePageTitle } from '@/composables/usePageTitle'
|
||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
IconSearch,
|
IconSearch,
|
||||||
IconTrash,
|
IconTrash,
|
||||||
IconArrowRight,
|
IconArrowRight,
|
||||||
|
IconTool,
|
||||||
|
IconUser,
|
||||||
} from '@tabler/icons-vue'
|
} from '@tabler/icons-vue'
|
||||||
|
|
||||||
usePageTitle('warehouse.item_list')
|
usePageTitle('warehouse.item_list')
|
||||||
@@ -215,6 +217,8 @@ onMounted(fetchItems)
|
|||||||
<th class="px-5 py-3 font-medium">{{ t('warehouse.item_name') }}</th>
|
<th class="px-5 py-3 font-medium">{{ t('warehouse.item_name') }}</th>
|
||||||
<th class="px-5 py-3 font-medium">{{ t('warehouse.serial_number') }}</th>
|
<th class="px-5 py-3 font-medium">{{ t('warehouse.serial_number') }}</th>
|
||||||
<th class="px-5 py-3 font-medium w-20 text-center">{{ t('warehouse.quantity') }}</th>
|
<th class="px-5 py-3 font-medium w-20 text-center">{{ t('warehouse.quantity') }}</th>
|
||||||
|
<th class="px-5 py-3 font-medium w-24 text-center">{{ t('work_order.work_order_count') }}</th>
|
||||||
|
<th class="px-5 py-3 font-medium">{{ t('customer.related_customers') }}</th>
|
||||||
<th class="px-5 py-3 font-medium">{{ t('warehouse.location') }}</th>
|
<th class="px-5 py-3 font-medium">{{ t('warehouse.location') }}</th>
|
||||||
<th class="px-5 py-3 font-medium whitespace-nowrap">{{ t('warehouse.created_at') }}</th>
|
<th class="px-5 py-3 font-medium whitespace-nowrap">{{ t('warehouse.created_at') }}</th>
|
||||||
<th class="px-5 py-3 font-medium w-16 text-right">{{ t('warehouse.actions') }}</th>
|
<th class="px-5 py-3 font-medium w-16 text-right">{{ t('warehouse.actions') }}</th>
|
||||||
@@ -230,6 +234,29 @@ onMounted(fetchItems)
|
|||||||
<td class="px-5 py-3 font-medium max-w-[200px] truncate">{{ item.Name }}</td>
|
<td class="px-5 py-3 font-medium max-w-[200px] truncate">{{ item.Name }}</td>
|
||||||
<td class="px-5 py-3 text-xs text-gray-500 dark:text-gray-400 max-w-[160px] truncate">{{ item.SerialNumber || '—' }}</td>
|
<td class="px-5 py-3 text-xs text-gray-500 dark:text-gray-400 max-w-[160px] truncate">{{ item.SerialNumber || '—' }}</td>
|
||||||
<td class="px-5 py-3 text-center text-sm">{{ item.Quantity }}</td>
|
<td class="px-5 py-3 text-center text-sm">{{ item.Quantity }}</td>
|
||||||
|
<td class="px-5 py-3 text-center">
|
||||||
|
<span v-if="item.WorkOrderCount > 0" class="inline-flex items-center gap-1 rounded-full bg-orange-100 px-2 py-0.5 text-xs font-medium text-orange-700 dark:bg-orange-900/40 dark:text-orange-400">
|
||||||
|
<IconTool :size="12" />
|
||||||
|
{{ item.WorkOrderCount }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="text-gray-400">—</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-5 py-3">
|
||||||
|
<div v-if="item.Customers && item.Customers.length > 0" class="flex flex-wrap gap-1">
|
||||||
|
<RouterLink
|
||||||
|
v-for="customer in item.Customers.slice(0, 3)"
|
||||||
|
:key="customer.id"
|
||||||
|
:to="`/customer/${customer.id}`"
|
||||||
|
class="inline-flex items-center gap-1 rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700 hover:bg-blue-200 dark:bg-blue-900/40 dark:text-blue-400 dark:hover:bg-blue-900/60"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<IconUser :size="10" />
|
||||||
|
{{ customer.first_name }} {{ customer.last_name }}
|
||||||
|
</RouterLink>
|
||||||
|
<span v-if="item.Customers.length > 3" class="text-xs text-gray-400">+{{ item.Customers.length - 3 }}</span>
|
||||||
|
</div>
|
||||||
|
<span v-else class="text-gray-400">—</span>
|
||||||
|
</td>
|
||||||
<td class="px-5 py-3">
|
<td class="px-5 py-3">
|
||||||
<span v-if="item.ContainerBreadcrumb" class="inline-flex items-center gap-1 text-blue-600 text-sm">
|
<span v-if="item.ContainerBreadcrumb" class="inline-flex items-center gap-1 text-blue-600 text-sm">
|
||||||
<IconArrowRight :size="13" />
|
<IconArrowRight :size="13" />
|
||||||
|
|||||||
@@ -456,7 +456,7 @@ async function handleSubmit() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 搜索框 -->
|
<!-- 搜索框 -->
|
||||||
<div class="item-search-wrapper relative">
|
<div class="item-search-wrapper relative" style="z-index: 9999;">
|
||||||
<input
|
<input
|
||||||
v-model="itemSearchQuery"
|
v-model="itemSearchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -468,7 +468,7 @@ async function handleSubmit() {
|
|||||||
<!-- 下拉结果 -->
|
<!-- 下拉结果 -->
|
||||||
<div
|
<div
|
||||||
v-if="showItemDropdown && itemSearchResults.length > 0"
|
v-if="showItemDropdown && itemSearchResults.length > 0"
|
||||||
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
class="absolute z-[9999] mt-1 max-h-60 w-full overflow-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="item in itemSearchResults"
|
v-for="item in itemSearchResults"
|
||||||
@@ -483,14 +483,14 @@ async function handleSubmit() {
|
|||||||
<!-- 加载中 -->
|
<!-- 加载中 -->
|
||||||
<div
|
<div
|
||||||
v-if="showItemDropdown && itemSearchLoading"
|
v-if="showItemDropdown && itemSearchLoading"
|
||||||
class="absolute z-10 mt-1 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-500 shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
class="absolute z-[9999] mt-1 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-500 shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
||||||
>
|
>
|
||||||
{{ t('message.loading') }}
|
{{ t('message.loading') }}
|
||||||
</div>
|
</div>
|
||||||
<!-- 无结果 -->
|
<!-- 无结果 -->
|
||||||
<div
|
<div
|
||||||
v-if="showItemDropdown && !itemSearchLoading && itemSearchResults.length === 0 && itemSearchQuery.trim().length > 0"
|
v-if="showItemDropdown && !itemSearchLoading && itemSearchResults.length === 0 && itemSearchQuery.trim().length > 0"
|
||||||
class="absolute z-10 mt-1 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-500 shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
class="absolute z-[9999] mt-1 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-500 shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
||||||
>
|
>
|
||||||
{{ t('work_order.linked_item_not_found') }}
|
{{ t('work_order.linked_item_not_found') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -523,7 +523,7 @@ async function handleSubmit() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 搜索框 -->
|
<!-- 搜索框 -->
|
||||||
<div class="customer-search-wrapper relative">
|
<div class="customer-search-wrapper relative" style="z-index: 9999;">
|
||||||
<input
|
<input
|
||||||
v-model="customerSearchQuery"
|
v-model="customerSearchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -535,7 +535,7 @@ async function handleSubmit() {
|
|||||||
<!-- 下拉结果 -->
|
<!-- 下拉结果 -->
|
||||||
<div
|
<div
|
||||||
v-if="showCustomerDropdown && customerSearchResults.length > 0"
|
v-if="showCustomerDropdown && customerSearchResults.length > 0"
|
||||||
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
class="absolute z-[9999] mt-1 max-h-60 w-full overflow-auto rounded-lg border border-gray-200 bg-white shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="customer in customerSearchResults"
|
v-for="customer in customerSearchResults"
|
||||||
@@ -554,14 +554,14 @@ async function handleSubmit() {
|
|||||||
<!-- 加载中 -->
|
<!-- 加载中 -->
|
||||||
<div
|
<div
|
||||||
v-if="showCustomerDropdown && customerSearchLoading"
|
v-if="showCustomerDropdown && customerSearchLoading"
|
||||||
class="absolute z-10 mt-1 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-500 shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
class="absolute z-[9999] mt-1 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-500 shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
||||||
>
|
>
|
||||||
{{ t('message.loading') }}
|
{{ t('message.loading') }}
|
||||||
</div>
|
</div>
|
||||||
<!-- 无结果 -->
|
<!-- 无结果 -->
|
||||||
<div
|
<div
|
||||||
v-if="showCustomerDropdown && !customerSearchLoading && customerSearchResults.length === 0 && customerSearchQuery.trim().length > 0"
|
v-if="showCustomerDropdown && !customerSearchLoading && customerSearchResults.length === 0 && customerSearchQuery.trim().length > 0"
|
||||||
class="absolute z-10 mt-1 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-500 shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
class="absolute z-[9999] mt-1 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-500 shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
||||||
>
|
>
|
||||||
{{ t('work_order.linked_customer_not_found') }}
|
{{ t('work_order.linked_customer_not_found') }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -569,7 +569,7 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 搜索框 -->
|
<!-- 搜索框 -->
|
||||||
<div ref="purchaseDropdownRef" class="relative">
|
<div ref="purchaseDropdownRef" class="relative" style="z-index: 9999;">
|
||||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||||
<IconSearch :size="14" class="text-gray-400" />
|
<IconSearch :size="14" class="text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
@@ -584,7 +584,7 @@ onUnmounted(() => {
|
|||||||
<!-- 搜索结果下拉 -->
|
<!-- 搜索结果下拉 -->
|
||||||
<div
|
<div
|
||||||
v-if="purchaseDropdownVisible && purchaseSearchResults.length > 0"
|
v-if="purchaseDropdownVisible && purchaseSearchResults.length > 0"
|
||||||
class="absolute z-10 mt-1 max-h-48 w-full overflow-auto rounded-lg border border-gray-200 bg-white py-1 shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
class="absolute z-[9999] mt-1 max-h-48 w-full overflow-auto rounded-lg border border-gray-200 bg-white py-1 shadow-lg dark:border-dk-muted dark:bg-dk-card"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-for="po in purchaseSearchResults"
|
v-for="po in purchaseSearchResults"
|
||||||
@@ -606,7 +606,7 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="purchaseDropdownVisible && purchaseSearchQuery && purchaseSearchResults.length === 0 && !purchaseSearchLoading"
|
v-else-if="purchaseDropdownVisible && purchaseSearchQuery && purchaseSearchResults.length === 0 && !purchaseSearchLoading"
|
||||||
class="absolute z-10 mt-1 w-full rounded-lg border border-gray-200 bg-white py-3 text-center text-sm text-gray-400 dark:border-dk-muted dark:bg-dk-card"
|
class="absolute z-[9999] mt-1 w-full rounded-lg border border-gray-200 bg-white py-3 text-center text-sm text-gray-400 dark:border-dk-muted dark:bg-dk-card"
|
||||||
>
|
>
|
||||||
未找到匹配的订单
|
未找到匹配的订单
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter, RouterLink } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useToastStore } from '@/stores/toast'
|
import { useToastStore } from '@/stores/toast'
|
||||||
import { usePageTitle } from '@/composables/usePageTitle'
|
import { usePageTitle } from '@/composables/usePageTitle'
|
||||||
@@ -144,6 +144,7 @@ onMounted(fetchOrders)
|
|||||||
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 w-16">No.</th>
|
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 w-16">No.</th>
|
||||||
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400">{{ t('work_order.title') }}</th>
|
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400">{{ t('work_order.title') }}</th>
|
||||||
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400">描述</th>
|
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400">描述</th>
|
||||||
|
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400">关联客户</th>
|
||||||
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 whitespace-nowrap w-44">{{ t('work_order.created_at') }}</th>
|
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 whitespace-nowrap w-44">{{ t('work_order.created_at') }}</th>
|
||||||
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 whitespace-nowrap w-44">{{ t('work_order.updated_at') }}</th>
|
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 whitespace-nowrap w-44">{{ t('work_order.updated_at') }}</th>
|
||||||
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 w-36">{{ t('work_order.status') }}</th>
|
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 w-36">{{ t('work_order.status') }}</th>
|
||||||
@@ -151,7 +152,7 @@ onMounted(fetchOrders)
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-if="loading">
|
<tr v-if="loading">
|
||||||
<td colspan="6" class="px-6 py-8 text-center text-gray-400">
|
<td colspan="7" class="px-6 py-8 text-center text-gray-400">
|
||||||
<svg class="mx-auto mb-2 h-5 w-5 animate-spin text-gray-400" viewBox="0 0 24 24" fill="none">
|
<svg class="mx-auto mb-2 h-5 w-5 animate-spin text-gray-400" viewBox="0 0 24 24" fill="none">
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
|
||||||
@@ -160,7 +161,7 @@ onMounted(fetchOrders)
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-else-if="orders.length === 0">
|
<tr v-else-if="orders.length === 0">
|
||||||
<td colspan="6" class="px-6 py-8 text-center text-gray-400 dark:text-gray-500">
|
<td colspan="7" class="px-6 py-8 text-center text-gray-400 dark:text-gray-500">
|
||||||
暂无工单
|
暂无工单
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -174,6 +175,19 @@ onMounted(fetchOrders)
|
|||||||
<td class="px-6 py-3 text-gray-500 dark:text-gray-400">{{ order.ID }}</td>
|
<td class="px-6 py-3 text-gray-500 dark:text-gray-400">{{ order.ID }}</td>
|
||||||
<td class="px-6 py-3 font-medium text-gray-900 dark:text-white max-w-xs truncate">{{ order.Title }}</td>
|
<td class="px-6 py-3 font-medium text-gray-900 dark:text-white max-w-xs truncate">{{ order.Title }}</td>
|
||||||
<td class="px-6 py-3 text-gray-500 dark:text-gray-400 max-w-xs truncate">{{ order.Description || '—' }}</td>
|
<td class="px-6 py-3 text-gray-500 dark:text-gray-400 max-w-xs truncate">{{ order.Description || '—' }}</td>
|
||||||
|
<td class="px-6 py-3">
|
||||||
|
<div v-if="order.customers && order.customers.length > 0" class="flex flex-wrap gap-1">
|
||||||
|
<RouterLink
|
||||||
|
v-for="c in order.customers"
|
||||||
|
:key="c.id"
|
||||||
|
:to="`/customer/detail/${c.id}`"
|
||||||
|
class="inline-flex items-center gap-1 rounded-full border border-blue-200 bg-blue-50 px-2 py-0.5 text-xs text-blue-700 hover:bg-blue-100 dark:border-blue-800 dark:bg-blue-900/30 dark:text-blue-300"
|
||||||
|
>
|
||||||
|
{{ (c.last_name || '') + (c.first_name ? ' ' + c.first_name : '') }}
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
<span v-else class="text-gray-400">—</span>
|
||||||
|
</td>
|
||||||
<td class="px-6 py-3 whitespace-nowrap text-gray-500 dark:text-gray-400">{{ formatDate(order.CreatedAt) }}</td>
|
<td class="px-6 py-3 whitespace-nowrap text-gray-500 dark:text-gray-400">{{ formatDate(order.CreatedAt) }}</td>
|
||||||
<td class="px-6 py-3 whitespace-nowrap text-gray-500 dark:text-gray-400">{{ formatDate(order.UpdatedAt) }}</td>
|
<td class="px-6 py-3 whitespace-nowrap text-gray-500 dark:text-gray-400">{{ formatDate(order.UpdatedAt) }}</td>
|
||||||
<td class="px-6 py-3">
|
<td class="px-6 py-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user