up
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
# 2026-04-24 工作日志
|
||||
|
||||
## 采购订单详情页增加关联工单功能
|
||||
|
||||
**后端(apiPurchase.go):**
|
||||
- `getorder` 接口新增返回字段 `workOrders`:通过 `TabWorkOrderPurchaseOrderBind` 表查询关联的工单列表(去重),包含 id/title/status
|
||||
- 新增 `/purchase/search_work_orders` 接口:支持按 ID 精确匹配或标题/描述模糊搜索工单,返回最新的若干条结果
|
||||
|
||||
**前端 API(purchase.js):**
|
||||
- 新增 `searchWorkOrders(search, limit)` 方法
|
||||
|
||||
**前端 ShowOrder.vue(采购订单详情页):**
|
||||
- 增加关联工单展示卡片(位于图片备注与状态记录之间)
|
||||
- 显示所有关联工单列表(点击跳转工单详情)
|
||||
- 右上角"新增工单"按钮,跳转到新增工单页面并预填采购订单信息(存 localStorage)
|
||||
- 工单状态颜色/文字与 ShowWorkOrder 保持一致
|
||||
|
||||
## 其他本次 session 改动(从 git 状态推断)
|
||||
- 采购列表后端排序改为 `updated_at DESC, id DESC`(apiPurchase.go)
|
||||
- 物品详情"关联工单"按钮文本改为 i18n `work_order.add`(WarehouseItemDetail.vue)
|
||||
- 关联工单列表图标从 IconPackage 改为 IconFile(WarehouseItemDetail.vue)
|
||||
@@ -234,16 +234,49 @@ func ApiPurchase(r *gin.RouterGroup) {
|
||||
commitResps = append(commitResps, resp)
|
||||
}
|
||||
|
||||
// 判断当前用户是否可以修改
|
||||
canModify := canModifyPurchase(user.ID, order.UserID)
|
||||
// 查询关联工单(通过 TabWorkOrderPurchaseOrderBind 表)
|
||||
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": canModify,
|
||||
"costs": costs,
|
||||
"photos": files,
|
||||
"commits": commitResps,
|
||||
})
|
||||
// 判断当前用户是否可以修改
|
||||
canModify := canModifyPurchase(user.ID, order.UserID)
|
||||
|
||||
ReturnJson(ctx, "apiOK", gin.H{
|
||||
"order": order,
|
||||
"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})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -40,4 +40,9 @@ export const purchaseApi = {
|
||||
deleteCommit(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>
|
||||
import { ref, reactive, computed, onMounted } from "vue";
|
||||
import { ref, reactive, computed, onMounted, onUnmounted } from "vue";
|
||||
import { useRoute, useRouter, RouterLink } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useToastStore } from "@/stores/toast";
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
IconX,
|
||||
IconUpload,
|
||||
IconTrash,
|
||||
IconSearch,
|
||||
IconFile,
|
||||
} from "@tabler/icons-vue";
|
||||
|
||||
usePageTitle("purchase.order_detail");
|
||||
@@ -33,6 +35,7 @@ const order = ref(null);
|
||||
const costs = ref([]);
|
||||
const photos = ref([]);
|
||||
const commits = ref([]);
|
||||
const workOrders = ref([]);
|
||||
const canModify = ref(false);
|
||||
const loading = ref(true);
|
||||
const notFound = ref(false);
|
||||
@@ -41,6 +44,20 @@ const showStatusDialog = ref(false);
|
||||
const pendingStatus = 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 photoInputRef = ref(null);
|
||||
@@ -323,6 +340,7 @@ async function fetchOrder() {
|
||||
costs.value = data.costs ?? [];
|
||||
photos.value = data.photos ?? [];
|
||||
commits.value = data.commits ?? [];
|
||||
workOrders.value = data.workOrders ?? [];
|
||||
} else {
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -671,11 +743,46 @@ onMounted(fetchOrder);
|
||||
</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) -->
|
||||
<div
|
||||
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">
|
||||
|
||||
<h4 class="text-sm font-semibold text-gray-500 dark:text-gray-400">
|
||||
{{ t("purchase.commit_history") }}
|
||||
</h4>
|
||||
|
||||
Reference in New Issue
Block a user