This commit is contained in:
2026-04-23 16:42:10 +08:00
parent dac1102ae5
commit 6b68eb254e
10 changed files with 280 additions and 16 deletions
+26
View File
@@ -47,3 +47,29 @@
- 前端:状态选 `parts_ordered` 时显示采购订单搜索框(防抖300ms),输入框获取焦点自动搜索 - 前端:状态选 `parts_ordered` 时显示采购订单搜索框(防抖300ms),输入框获取焦点自动搜索
- 时间线每条 commit 下方展示关联的采购订单链接(点击跳转到采购详情页) - 时间线每条 commit 下方展示关联的采购订单链接(点击跳转到采购详情页)
## 今日功能迭代
**ConfirmDialog 组件 v-model 修复**
- 组件使用 `v-if="modelValue"` 控制弹窗显示,但外部只用了 `v-if` 控制组件存在,没有传入 `modelValue` prop
- 修复:在所有使用 ConfirmDialog 的地方改用 `v-model="xxx"` 绑定
- 涉及文件:ShowWorkOrder.vue、AddEditWorkOrder.vue、ShowOrder.vue
**ShowWorkOrder.vue 进度删除功能**
- 新增删除按钮(新样式:带边框和背景色的文字按钮)
- 每条 commit 加边框和背景色,便于区分
- 最新状态不显示删除按钮
- 权限判断:工单创建者 OR 进度创建者 OR 管理员
- 删除后前端直接移除该 commit,保持滚动位置
**useDropzone v-model 问题修复**
- useDropzone 组件没有实现 v-model,是通过 `return_files()` 方法暴露文件
- 修复:添加 `ref="commitDropzoneRef"`,通过 `commitDropzoneRef.value?.return_files()` 获取文件
- 只筛选 `is_upload === true` 的文件获取 hash
**采购订单状态记录删除功能**
- 后端:apiPurchase.go 新增 `/delete_commit` 接口,权限判断同工单
- 前端:ShowOrder.vue 新增删除按钮,样式和逻辑同工单页面
- 新增翻译:purchase.confirm_delete_commit
+48
View File
@@ -350,6 +350,54 @@ func ApiPurchase(r *gin.RouterGroup) {
ReturnJson(ctx, "apiOK", nil) ReturnJson(ctx, "apiOK", nil)
}) })
// 删除状态记录
r.POST("/delete_commit", func(ctx *gin.Context) {
isAuth, user, data := AuthenticationAuthority(ctx)
if !isAuth {
ReturnJson(ctx, "userCookieError", nil)
return
}
type FromDeleteCommit struct {
OrderID uint `json:"orderId"`
CommitID uint `json:"commitId"`
}
var from FromDeleteCommit
if err := decodeJSON(data, &from); err != nil || from.OrderID == 0 || from.CommitID == 0 {
ReturnJson(ctx, "jsonErr", nil)
return
}
// 获取订单信息
var order TabPurchaseOrder
if err := models.DB.Where("id = ?", from.OrderID).First(&order).Error; err != nil {
ReturnJson(ctx, "order_not_found", nil)
return
}
// 获取进度信息
var commit TabPurchaseCommit
if err := models.DB.Where("id = ? AND order_id = ?", from.CommitID, from.OrderID).First(&commit).Error; err != nil {
ReturnJson(ctx, "commit_not_found", nil)
return
}
// 权限判断:订单创建者 或 进度创建者 或 管理员
isOrderCreator := user.ID == order.UserID
isCommitCreator := user.ID == commit.UserID
isAdmin := slices.Contains(purchaseAdmins, user.ID)
if !isOrderCreator && !isCommitCreator && !isAdmin {
ReturnJson(ctx, "no_permission", nil)
return
}
// 删除进度
models.DB.Where("id = ?", from.CommitID).Delete(&TabPurchaseCommit{})
ReturnJson(ctx, "apiOK", nil)
})
r.POST("/getorders", func(ctx *gin.Context) { r.POST("/getorders", func(ctx *gin.Context) {
isAuth, _, data := AuthenticationAuthority(ctx) isAuth, _, data := AuthenticationAuthority(ctx)
if isAuth { if isAuth {
+53 -1
View File
@@ -354,7 +354,7 @@ func ApiWorkOrder(r *gin.RouterGroup) {
// commits // commits
var commits []TabWorkOrderCommit var commits []TabWorkOrderCommit
models.DB.Where("work_order_id = ?", from.ID).Order("created_at ASC").Find(&commits) models.DB.Where("work_order_id = ?", from.ID).Order("created_at DESC").Find(&commits)
// 为每条 commit 附加图片和采购订单 // 为每条 commit 附加图片和采购订单
type CommitWithPhotos struct { type CommitWithPhotos struct {
@@ -566,6 +566,58 @@ func ApiWorkOrder(r *gin.RouterGroup) {
ReturnJson(ctx, "apiOK", nil) ReturnJson(ctx, "apiOK", nil)
}) })
// 删除进度
r.POST("/delete_commit", func(ctx *gin.Context) {
isAuth, user, data := AuthenticationAuthority(ctx)
if !isAuth {
ReturnJson(ctx, "userCookieError", nil)
return
}
type FromDeleteCommit struct {
WorkOrderID uint `json:"workOrderId"`
CommitID uint `json:"commitId"`
}
var from FromDeleteCommit
if err := decodeJSON(data, &from); err != nil || from.WorkOrderID == 0 || from.CommitID == 0 {
ReturnJson(ctx, "jsonErr", nil)
return
}
// 获取工单信息
var order TabWorkOrder
if err := models.DB.Where("id = ?", from.WorkOrderID).First(&order).Error; err != nil {
ReturnJson(ctx, "order_not_found", nil)
return
}
// 获取进度信息
var commit TabWorkOrderCommit
if err := models.DB.Where("id = ? AND work_order_id = ?", from.CommitID, from.WorkOrderID).First(&commit).Error; err != nil {
ReturnJson(ctx, "commit_not_found", nil)
return
}
// 权限判断:工单创建者 或 进度创建者 或 管理员
isOrderCreator := user.ID == order.UserID
isCommitCreator := user.ID == commit.UserID
isAdmin := slices.Contains(workOrderAdmins, user.ID)
if !isOrderCreator && !isCommitCreator && !isAdmin {
ReturnJson(ctx, "no_permission", nil)
return
}
// 删除关联的采购订单绑定
models.DB.Where("commit_id = ?", from.CommitID).Delete(&TabWorkOrderPurchaseOrderBind{})
// 删除关联的图片
models.DB.Where("commit_id = ?", from.CommitID).Delete(&TabWorkOrderCommitFileBind{})
// 删除进度记录
models.DB.Where("id = ?", from.CommitID).Delete(&commit)
ReturnJson(ctx, "apiOK", nil)
})
// 获取工单数量统计 // 获取工单数量统计
r.POST("/count", func(ctx *gin.Context) { r.POST("/count", func(ctx *gin.Context) {
isAuth, _, _ := AuthenticationAuthority(ctx) isAuth, _, _ := AuthenticationAuthority(ctx)
+5
View File
@@ -35,4 +35,9 @@ export const purchaseApi = {
deleteOrder(id) { deleteOrder(id) {
return api.post('/purchase/deleteorder', { id }) return api.post('/purchase/deleteorder', { id })
}, },
/** 删除状态记录 */
deleteCommit(orderId, commitId) {
return api.post('/purchase/delete_commit', { orderId, commitId })
},
} }
@@ -40,4 +40,9 @@ export const workOrderApi = {
searchPurchaseOrders(search = '', limit = 5) { searchPurchaseOrders(search = '', limit = 5) {
return api.post('/work_order/search_purchase_orders', { search, limit }) return api.post('/work_order/search_purchase_orders', { search, limit })
}, },
/** 删除进度 */
deleteCommit(workOrderId, commitId) {
return api.post('/work_order/delete_commit', { workOrderId, commitId })
},
} }
+4 -1
View File
@@ -108,7 +108,8 @@
"upload_photos": "Upload Photos", "upload_photos": "Upload Photos",
"commit_create": "Order created", "commit_create": "Order created",
"edit_order": "Edit Order", "edit_order": "Edit Order",
"submit_changes":"Submit changes" "submit_changes":"Submit changes",
"confirm_delete_commit": "Are you sure you want to delete this progress?"
}, },
"work_order": { "work_order": {
"list_title": "Work Order List", "list_title": "Work Order List",
@@ -146,6 +147,7 @@
"back_to_list": "Back to List", "back_to_list": "Back to List",
"not_found": "Work order not found", "not_found": "Work order not found",
"confirm_delete": "Are you sure you want to delete this work order? This action cannot be undone.", "confirm_delete": "Are you sure you want to delete this work order? This action cannot be undone.",
"confirm_delete_commit": "Are you sure you want to delete this progress?",
"submit": "Submit", "submit": "Submit",
"save_changes": "Save Changes" "save_changes": "Save Changes"
}, },
@@ -292,6 +294,7 @@
"cancel": "Cancel", "cancel": "Cancel",
"save_success": "Saved successfully", "save_success": "Saved successfully",
"submit": "Submit", "submit": "Submit",
"submitting": "Submitting...",
"loading": "Loading..." "loading": "Loading..."
}, },
"settings": { "settings": {
+4 -1
View File
@@ -108,7 +108,8 @@
"upload_photos": "上传图片", "upload_photos": "上传图片",
"commit_create": "订单创建", "commit_create": "订单创建",
"edit_order": "编辑订单", "edit_order": "编辑订单",
"submit_changes":"提交修改" "submit_changes":"提交修改",
"confirm_delete_commit": "确定要删除此进度吗?"
}, },
"work_order": { "work_order": {
"list_title": "工单列表", "list_title": "工单列表",
@@ -146,6 +147,7 @@
"back_to_list": "返回列表", "back_to_list": "返回列表",
"not_found": "工单不存在", "not_found": "工单不存在",
"confirm_delete": "确定要删除此工单吗?此操作不可撤销。", "confirm_delete": "确定要删除此工单吗?此操作不可撤销。",
"confirm_delete_commit": "确定要删除此进度吗?",
"submit": "提交", "submit": "提交",
"save_changes": "保存修改" "save_changes": "保存修改"
}, },
@@ -292,6 +294,7 @@
"delete_ok": "删除成功", "delete_ok": "删除成功",
"save_success": "保存成功", "save_success": "保存成功",
"submit": "提交", "submit": "提交",
"submitting": "提交中...",
"loading": "加载中..." "loading": "加载中..."
}, },
"settings": { "settings": {
@@ -7,6 +7,7 @@ import { usePageTitle } from "@/composables/usePageTitle";
import { purchaseApi } from "@/api/purchase"; import { purchaseApi } from "@/api/purchase";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import { useUsersStore } from "@/stores/users"; import { useUsersStore } from "@/stores/users";
import ConfirmDialog from "@/components/ConfirmDialog.vue";
import { import {
IconChevronLeft, IconChevronLeft,
IconExternalLink, IconExternalLink,
@@ -44,6 +45,47 @@ const pendingComment = ref("");
const pendingPhotos = ref([]); // { hash, url, uploading, error } const pendingPhotos = ref([]); // { hash, url, uploading, error }
const photoInputRef = ref(null); const photoInputRef = ref(null);
// 删除进度相关
const showDeleteConfirm = ref(false);
const pendingDeleteCommitId = ref(null);
// 判断是否可以删除进度
function canDeleteCommit(commit, index) {
// 最新状态(第0条)不显示删除按钮
if (index === 0) return false;
// 订单创建者
if (order.value?.UserID === userStore.user?.ID) return true;
// 进度创建者
if (commit.userId === userStore.user?.ID) return true;
// 管理员
if (userStore.user?.Type === 'admin') return true;
return false;
}
function handleDeleteCommit(commitId) {
pendingDeleteCommitId.value = commitId;
showDeleteConfirm.value = true;
}
async function confirmDeleteCommit() {
if (!pendingDeleteCommitId.value) return;
try {
const { errCode } = await purchaseApi.deleteCommit(orderId.value, pendingDeleteCommitId.value);
if (errCode === 0) {
toast.success(t("message.delete_ok"));
// 前端直接移除该 commit,保持滚动位置
commits.value = commits.value.filter(c => c.id !== pendingDeleteCommitId.value);
} else {
toast.error(t("message.server_error"));
}
} catch {
toast.error(t("message.server_error"));
} finally {
pendingDeleteCommitId.value = null;
showDeleteConfirm.value = false;
}
}
// 状态选项 // 状态选项
const statusOptions = [ const statusOptions = [
{ value: "pending", labelKey: "status_pending", color: "yellow" }, { value: "pending", labelKey: "status_pending", color: "yellow" },
@@ -644,9 +686,9 @@ onMounted(fetchOrder);
class="divide-y divide-gray-50 px-6 py-2 dark:divide-dk-muted/50" class="divide-y divide-gray-50 px-6 py-2 dark:divide-dk-muted/50"
> >
<div <div
v-for="commit in commits" v-for="(commit, index) in commits"
:key="commit.id" :key="commit.id"
class="flex items-start gap-3 py-3" class="flex items-start gap-3 py-3 rounded-lg border border-gray-100 bg-gray-50/50 px-4 my-2 dark:border-dk-muted dark:bg-dk-base/30"
> >
<!-- 左侧头像 + 用户名 --> <!-- 左侧头像 + 用户名 -->
<div class="flex w-20 flex-shrink-0 flex-col items-center gap-1"> <div class="flex w-20 flex-shrink-0 flex-col items-center gap-1">
@@ -697,6 +739,15 @@ onMounted(fetchOrder);
<span class="ml-auto text-xs text-gray-400">{{ <span class="ml-auto text-xs text-gray-400">{{
formatDate(commit.createdAt) formatDate(commit.createdAt)
}}</span> }}</span>
<!-- 删除按钮 -->
<button
v-if="canDeleteCommit(commit, index)"
class="ml-2 rounded-lg border border-red-200 bg-red-50 px-3 py-1 text-xs font-medium text-red-600 transition-colors hover:bg-red-100 hover:border-red-300 dark:border-red-900/50 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50"
@click="handleDeleteCommit(commit.id)"
>
<IconTrash :size="14" class="mr-1 inline align-middle" />
删除
</button>
</div> </div>
<p <p
v-if="commit.comment" v-if="commit.comment"
@@ -899,6 +950,14 @@ onMounted(fetchOrder);
</div> </div>
</Transition> </Transition>
</Teleport> </Teleport>
<!-- 删除进度确认弹窗 -->
<ConfirmDialog
v-model="showDeleteConfirm"
:message="t('purchase.confirm_delete_commit')"
danger
@confirm="confirmDeleteCommit"
/>
</template> </template>
<style scoped> <style scoped>
@@ -259,9 +259,9 @@ async function handleSubmit() {
<!-- 删除确认弹窗 --> <!-- 删除确认弹窗 -->
<ConfirmDialog <ConfirmDialog
v-if="showDeleteConfirm" v-model="showDeleteConfirm"
:message="t('work_order.confirm_delete')" :message="t('work_order.confirm_delete')"
danger
@confirm="doDelete" @confirm="doDelete"
@cancel="showDeleteConfirm = false"
/> />
</template> </template>
@@ -8,6 +8,7 @@ import { useUserStore } from '@/stores/user'
import { useUsersStore } from '@/stores/users' import { useUsersStore } from '@/stores/users'
import { workOrderApi } from '@/api/work_order' import { workOrderApi } from '@/api/work_order'
import useDropzone from '@/components/useDropzone.vue' import useDropzone from '@/components/useDropzone.vue'
import ConfirmDialog from '@/components/ConfirmDialog.vue'
import { import {
IconChevronLeft, IconChevronLeft,
IconCheck, IconCheck,
@@ -39,7 +40,7 @@ const notFound = ref(false)
const submittingCommit = ref(false) const submittingCommit = ref(false)
const commitStatus = ref('pending') const commitStatus = ref('pending')
const commitComment = ref('') const commitComment = ref('')
const commitPhotos = ref([]) const commitDropzoneRef = ref(null)
// 采购订单关联相关 // 采购订单关联相关
const purchaseSearchQuery = ref('') const purchaseSearchQuery = ref('')
@@ -54,7 +55,7 @@ const purchaseDropdownRef = ref(null)
const canSubmit = computed(() => { const canSubmit = computed(() => {
const hasSelectedOrders = selectedPurchaseOrders.value.length > 0 const hasSelectedOrders = selectedPurchaseOrders.value.length > 0
const hasComment = !!commitComment.value const hasComment = !!commitComment.value
const hasPhotos = commitPhotos.value.length > 0 const hasPhotos = commitDropzoneRef.value?.return_files().filter(f => f.is_upload).length > 0
// 订单、备注、上传图片都为空时才禁止提交 // 订单、备注、上传图片都为空时才禁止提交
return hasSelectedOrders || hasComment || hasPhotos return hasSelectedOrders || hasComment || hasPhotos
}) })
@@ -138,18 +139,22 @@ async function handleCommit() {
submittingCommit.value = true submittingCommit.value = true
try { try {
const purchaseOrderIds = selectedPurchaseOrders.value.map(p => p.id) const purchaseOrderIds = selectedPurchaseOrders.value.map(p => p.id)
// 从 dropzone 获取已上传的文件 hash
const uploadedPhotos = commitDropzoneRef.value?.return_files()
.filter(f => f.is_upload)
.map(f => f.hash) ?? []
const { errCode } = await workOrderApi.commit( const { errCode } = await workOrderApi.commit(
orderId.value, orderId.value,
commitStatus.value, commitStatus.value,
commitComment.value, commitComment.value,
commitPhotos.value, uploadedPhotos,
purchaseOrderIds, purchaseOrderIds,
) )
if (errCode === 0) { if (errCode === 0) {
toast.success(t('message.save_ok')) toast.success(t('message.save_ok'))
commitComment.value = '' commitComment.value = ''
commitPhotos.value = []
selectedPurchaseOrders.value = [] selectedPurchaseOrders.value = []
// 清空 dropzone(刷新组件即可)
await fetchOrder() await fetchOrder()
} else { } else {
toast.error(t('message.server_error')) toast.error(t('message.server_error'))
@@ -161,6 +166,47 @@ async function handleCommit() {
} }
} }
// ==================== 删除进度 ====================
const showDeleteCommitConfirm = ref(false)
const pendingDeleteCommitId = ref(null)
function handleDeleteCommit(commitId) {
pendingDeleteCommitId.value = commitId
showDeleteCommitConfirm.value = true
}
async function confirmDeleteCommit() {
if (!pendingDeleteCommitId.value) return
try {
const { errCode } = await workOrderApi.deleteCommit(orderId.value, pendingDeleteCommitId.value)
if (errCode === 0) {
toast.success(t('message.delete_ok'))
// 前端直接移除该 commit,保持滚动位置
commits.value = commits.value.filter(c => c.ID !== pendingDeleteCommitId.value)
} else {
toast.error(t('message.server_error'))
}
} catch {
toast.error(t('message.server_error'))
} finally {
pendingDeleteCommitId.value = null
showDeleteCommitConfirm.value = false
}
}
// 判断是否可以删除进度
function canDeleteCommit(commit, index) {
// 最新状态(第0条)不显示删除按钮
if (index === 0) return false
// 订单创建者
if (order.value?.UserID === userStore.user?.ID) return true
// 进度创建者
if (commit.UserID === userStore.user?.ID) return true
// 管理员
if (userStore.user?.Type === 'admin') return true
return false
}
// ==================== 快捷切换状态 ==================== // ==================== 快捷切换状态 ====================
async function quickChangeStatus(newStatus) { async function quickChangeStatus(newStatus) {
if (newStatus === order.value?.CurrentStatus) return if (newStatus === order.value?.CurrentStatus) return
@@ -492,7 +538,7 @@ onUnmounted(() => {
<div class="mb-3"> <div class="mb-3">
<label class="mb-1 block text-xs font-medium text-gray-400">{{ t('work_order.commit_photos_label') }}</label> <label class="mb-1 block text-xs font-medium text-gray-400">{{ t('work_order.commit_photos_label') }}</label>
<useDropzone <useDropzone
v-model="commitPhotos" ref="commitDropzoneRef"
:maxFiles="10" :maxFiles="10"
:maxSize="10 * 1024 * 1024" :maxSize="10 * 1024 * 1024"
accept="image/*" accept="image/*"
@@ -502,11 +548,11 @@ onUnmounted(() => {
<!-- 第四行提交 --> <!-- 第四行提交 -->
<div class="flex justify-end"> <div class="flex justify-end">
<button <button
:disabled="isCommitting || !canSubmit" :disabled="submittingCommit || !canSubmit"
class="rounded-lg bg-blue-600 px-6 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50" class="rounded-lg bg-blue-600 px-6 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50"
@click="handleCommit" @click="handleCommit"
> >
{{ isCommitting ? '提交中...' : t('work_order.commit_submit') }} {{ submittingCommit ? t('message.submitting') : t('work_order.commit_submit') }}
</button> </button>
</div> </div>
</div> </div>
@@ -516,9 +562,9 @@ onUnmounted(() => {
<div v-if="commits.length === 0" class="py-4 text-sm text-gray-400">{{ t('work_order.no_commits') }}</div> <div v-if="commits.length === 0" class="py-4 text-sm text-gray-400">{{ t('work_order.no_commits') }}</div>
<ol v-else class="relative border-l border-gray-200 dark:border-dk-muted"> <ol v-else class="relative border-l border-gray-200 dark:border-dk-muted">
<li <li
v-for="commit in [...commits].reverse()" v-for="(commit, index) in commits"
:key="commit.ID" :key="commit.ID"
class="mb-6 ml-4" class="mb-6 ml-4 rounded-lg border border-gray-100 bg-gray-50/50 px-4 py-3 dark:border-dk-muted dark:bg-dk-base/30"
> >
<!-- 时间线圆点 --> <!-- 时间线圆点 -->
<div <div
@@ -545,6 +591,15 @@ onUnmounted(() => {
</span> </span>
<!-- 时间 --> <!-- 时间 -->
<time class="text-xs text-gray-400">{{ formatDate(commit.CreatedAt) }}</time> <time class="text-xs text-gray-400">{{ formatDate(commit.CreatedAt) }}</time>
<!-- 删除按钮 -->
<button
v-if="canDeleteCommit(commit, index)"
class="ml-auto rounded-lg border border-red-200 bg-red-50 px-3 py-1.5 text-xs font-medium text-red-600 transition-colors hover:bg-red-100 hover:border-red-300 dark:border-red-900/50 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50"
@click="handleDeleteCommit(commit.ID)"
>
<IconTrash :size="14" class="mr-1 inline align-middle" />
删除
</button>
</div> </div>
<!-- 备注文字 --> <!-- 备注文字 -->
@@ -606,4 +661,12 @@ onUnmounted(() => {
</div> </div>
</div> </div>
<!-- 删除进度确认弹窗 -->
<ConfirmDialog
v-model="showDeleteCommitConfirm"
:message="t('work_order.confirm_delete_commit')"
danger
@confirm="confirmDeleteCommit"
/>
</template> </template>