This commit is contained in:
2026-04-24 09:26:31 +08:00
parent dee84fda01
commit 11466cb56e
4 changed files with 232 additions and 11 deletions
+21
View File
@@ -0,0 +1,21 @@
# 2026-04-24 工作日志
## 采购订单详情页增加关联工单功能
**后端(apiPurchase.go):**
- `getorder` 接口新增返回字段 `workOrders`:通过 `TabWorkOrderPurchaseOrderBind` 表查询关联的工单列表(去重),包含 id/title/status
- 新增 `/purchase/search_work_orders` 接口:支持按 ID 精确匹配或标题/描述模糊搜索工单,返回最新的若干条结果
**前端 APIpurchase.js):**
- 新增 `searchWorkOrders(search, limit)` 方法
**前端 ShowOrder.vue(采购订单详情页):**
- 增加关联工单展示卡片(位于图片备注与状态记录之间)
- 显示所有关联工单列表(点击跳转工单详情)
- 右上角"新增工单"按钮,跳转到新增工单页面并预填采购订单信息(存 localStorage
- 工单状态颜色/文字与 ShowWorkOrder 保持一致
## 其他本次 session 改动(从 git 状态推断)
- 采购列表后端排序改为 `updated_at DESC, id DESC`apiPurchase.go
- 物品详情"关联工单"按钮文本改为 i18n `work_order.add`WarehouseItemDetail.vue
- 关联工单列表图标从 IconPackage 改为 IconFileWarehouseItemDetail.vue
+97 -9
View File
@@ -234,16 +234,49 @@ func ApiPurchase(r *gin.RouterGroup) {
commitResps = append(commitResps, resp) commitResps = append(commitResps, resp)
} }
// 判断当前用户是否可以修改 // 查询关联工单(通过 TabWorkOrderPurchaseOrderBind 表)
canModify := canModifyPurchase(user.ID, order.UserID) type WorkOrderInfo struct {
ID uint `json:"id"`
Title string `json:"title"`
CurrentStatus string `json:"status"`
}
var linkedWorkOrders []WorkOrderInfo
var woBinds []TabWorkOrderPurchaseOrderBind
models.DB.Where("purchase_order_id = ?", from.ID).Find(&woBinds)
if len(woBinds) > 0 {
woIDSet := make(map[uint]bool)
var woIDs []uint
for _, wb := range woBinds {
if !woIDSet[wb.WorkOrderID] {
woIDSet[wb.WorkOrderID] = true
woIDs = append(woIDs, wb.WorkOrderID)
}
}
var workOrders []TabWorkOrder
models.DB.Where("id IN ?", woIDs).Find(&workOrders)
for _, wo := range workOrders {
linkedWorkOrders = append(linkedWorkOrders, WorkOrderInfo{
ID: wo.ID,
Title: wo.Title,
CurrentStatus: wo.CurrentStatus,
})
}
}
if linkedWorkOrders == nil {
linkedWorkOrders = []WorkOrderInfo{}
}
ReturnJson(ctx, "apiOK", gin.H{ // 判断当前用户是否可以修改
"order": order, canModify := canModifyPurchase(user.ID, order.UserID)
"canModify": canModify,
"costs": costs, ReturnJson(ctx, "apiOK", gin.H{
"photos": files, "order": order,
"commits": commitResps, "canModify": canModify,
}) "costs": costs,
"photos": files,
"commits": commitResps,
"workOrders": linkedWorkOrders,
})
}) })
// 更新订单状态(可附带评论) // 更新订单状态(可附带评论)
@@ -800,4 +833,59 @@ func ApiPurchase(r *gin.RouterGroup) {
}) })
}) })
// 搜索工单(用于采购订单关联)
r.POST("/search_work_orders", func(ctx *gin.Context) {
isAuth, _, data := AuthenticationAuthority(ctx)
if !isAuth {
ReturnJson(ctx, "userCookieError", nil)
return
}
type FromSearch struct {
Search string `json:"search"`
Limit int `json:"limit"`
}
var from FromSearch
if err := decodeJSON(data, &from); err != nil {
ReturnJson(ctx, "jsonErr", nil)
return
}
if from.Limit <= 0 || from.Limit > 20 {
from.Limit = 5
}
query := models.DB.Model(&TabWorkOrder{})
if from.Search != "" {
var id uint
if _, err := parsefmt.Sscanf(from.Search, "%d", &id); err == nil && id > 0 {
query = query.Where("id = ?", id)
} else {
query = query.Where("title LIKE ? OR description LIKE ?",
"%"+from.Search+"%", "%"+from.Search+"%")
}
}
var orders []TabWorkOrder
query.Order("updated_at DESC, id DESC").Limit(from.Limit).Find(&orders)
type WorkOrderInfo struct {
ID uint `json:"id"`
Title string `json:"title"`
Status string `json:"status"`
}
var result []WorkOrderInfo
for _, o := range orders {
result = append(result, WorkOrderInfo{
ID: o.ID,
Title: o.Title,
Status: o.CurrentStatus,
})
}
if result == nil {
result = []WorkOrderInfo{}
}
ReturnJson(ctx, "apiOK", gin.H{"orders": result})
})
} }
+5
View File
@@ -40,4 +40,9 @@ export const purchaseApi = {
deleteCommit(orderId, commitId) { deleteCommit(orderId, commitId) {
return api.post('/purchase/delete_commit', { orderId, commitId }) return api.post('/purchase/delete_commit', { orderId, commitId })
}, },
/** 搜索工单(用于采购订单关联) */
searchWorkOrders(search = '', limit = 5) {
return api.post('/purchase/search_work_orders', { search, limit })
},
} }
@@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, reactive, computed, onMounted } from "vue"; import { ref, reactive, computed, onMounted, onUnmounted } from "vue";
import { useRoute, useRouter, RouterLink } 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";
@@ -17,6 +17,8 @@ import {
IconX, IconX,
IconUpload, IconUpload,
IconTrash, IconTrash,
IconSearch,
IconFile,
} from "@tabler/icons-vue"; } from "@tabler/icons-vue";
usePageTitle("purchase.order_detail"); usePageTitle("purchase.order_detail");
@@ -33,6 +35,7 @@ const order = ref(null);
const costs = ref([]); const costs = ref([]);
const photos = ref([]); const photos = ref([]);
const commits = ref([]); const commits = ref([]);
const workOrders = ref([]);
const canModify = ref(false); const canModify = ref(false);
const loading = ref(true); const loading = ref(true);
const notFound = ref(false); const notFound = ref(false);
@@ -41,6 +44,20 @@ const showStatusDialog = ref(false);
const pendingStatus = ref(""); const pendingStatus = ref("");
const pendingComment = ref(""); const pendingComment = ref("");
// 工单关联搜索
const workOrderSearchQuery = ref('')
const workOrderSearchResults = ref([])
const workOrderSearchLoading = ref(false)
const workOrderDropdownVisible = ref(false)
const workOrderDropdownRef = ref(null)
let workOrderSearchTimer = null
function onDocumentClick(e) {
if (workOrderDropdownRef.value && !workOrderDropdownRef.value.contains(e.target)) {
workOrderDropdownVisible.value = false
}
}
// 状态变更附带的图片 // 状态变更附带的图片
const pendingPhotos = ref([]); // { hash, url, uploading, error } const pendingPhotos = ref([]); // { hash, url, uploading, error }
const photoInputRef = ref(null); const photoInputRef = ref(null);
@@ -323,6 +340,7 @@ async function fetchOrder() {
costs.value = data.costs ?? []; costs.value = data.costs ?? [];
photos.value = data.photos ?? []; photos.value = data.photos ?? [];
commits.value = data.commits ?? []; commits.value = data.commits ?? [];
workOrders.value = data.workOrders ?? [];
} else { } else {
notFound.value = true; notFound.value = true;
} }
@@ -333,7 +351,61 @@ async function fetchOrder() {
} }
} }
onMounted(fetchOrder); // ── 工单搜索 ──
async function searchWorkOrders() {
workOrderSearchLoading.value = true
try {
const { errCode, data } = await purchaseApi.searchWorkOrders(workOrderSearchQuery.value, 10)
if (errCode === 0) {
workOrderSearchResults.value = data.orders || []
}
} catch {
workOrderSearchResults.value = []
} finally {
workOrderSearchLoading.value = false
}
}
function onWorkOrderSearchInput() {
workOrderDropdownVisible.value = true
clearTimeout(workOrderSearchTimer)
workOrderSearchTimer = setTimeout(() => searchWorkOrders(), 300)
}
function getWorkOrderStatusClass(status) {
const map = {
pending: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-400',
checked: 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-400',
parts_ordered: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-400',
repaired: 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-400',
returned: 'bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-300',
unrepairable: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-400',
}
return map[status] || 'bg-gray-100 text-gray-600'
}
function getWorkOrderStatusLabel(status) {
return t(`work_order.status_${status}`) || status
}
function openNewWorkOrder() {
if (!order.value) return
const prefillData = {
purchaseOrderId: order.value.ID,
purchaseOrderTitle: order.value.Title,
}
localStorage.setItem('prefill_work_order', JSON.stringify(prefillData))
router.push('/work_order/add')
}
onMounted(() => {
fetchOrder()
document.addEventListener('click', onDocumentClick)
})
onUnmounted(() => {
document.removeEventListener('click', onDocumentClick)
})
</script> </script>
<template> <template>
@@ -671,11 +743,46 @@ onMounted(fetchOrder);
</div> </div>
</div> </div>
<!-- 关联工单 -->
<div class="rounded-xl border border-gray-200 bg-white shadow-lg dark:border-dk-muted dark:bg-dk-card">
<div class="flex items-center justify-between border-b border-gray-100 px-6 py-4 dark:border-dk-muted">
<h4 class="text-sm font-semibold text-gray-500 dark:text-gray-400">
{{ t('work_order.list_title') }}
</h4>
</div>
<!-- 已关联工单列表 -->
<div v-if="workOrders.length > 0" class="space-y-2 px-6 py-4">
<RouterLink
v-for="wo in workOrders"
:key="wo.id"
:to="`/work_order/show/${wo.id}`"
class="rounded-xl border border-gray-200 bg-white px-4 py-3 flex items-center justify-between gap-3 hover:shadow transition-shadow dark:border-dk-muted dark:bg-dk-base dark:hover:shadow-none"
>
<div class="flex items-center gap-3 min-w-0">
<IconFile :size="16" class="text-blue-500 flex-shrink-0" />
<span class="font-medium text-sm text-gray-900 truncate dark:text-white">#{{ wo.id }} {{ wo.title }}</span>
</div>
<span
class="flex-shrink-0 rounded-full px-2.5 py-0.5 text-xs font-medium"
:class="getWorkOrderStatusClass(wo.status)"
>
{{ getWorkOrderStatusLabel(wo.status) }}
</span>
</RouterLink>
</div>
<div v-else class="px-6 py-8 text-center text-sm text-gray-400">
{{ t('warehouse.no_work_orders') }}
</div>
</div>
<!-- 状态记录Commit History --> <!-- 状态记录Commit History -->
<div <div
class="rounded-xl border border-gray-200 bg-white shadow-lg dark:border-dk-muted dark:bg-dk-card" class="rounded-xl border border-gray-200 bg-white shadow-lg dark:border-dk-muted dark:bg-dk-card"
> >
<div class="border-b border-gray-100 px-6 py-4 dark:border-dk-muted"> <div class="border-b border-gray-100 px-6 py-4 dark:border-dk-muted">
<h4 class="text-sm font-semibold text-gray-500 dark:text-gray-400"> <h4 class="text-sm font-semibold text-gray-500 dark:text-gray-400">
{{ t("purchase.commit_history") }} {{ t("purchase.commit_history") }}
</h4> </h4>