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)
|
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})
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user