This commit is contained in:
2026-04-14 15:54:15 +08:00
parent 0657117ec2
commit 3a38c34ea0
8 changed files with 288 additions and 22 deletions
+1 -1
View File
@@ -13,5 +13,5 @@
} }
] ]
}, },
"lastUpdated": 1776148343594 "lastUpdated": 1776153171221
} }
+45 -3
View File
@@ -317,6 +317,7 @@ func ApiPurchase(r *gin.RouterGroup) {
type From_purchase_getorders struct { type From_purchase_getorders struct {
Search string Search string
Status string
Entries int Entries int
Page int Page int
} }
@@ -338,12 +339,18 @@ func ApiPurchase(r *gin.RouterGroup) {
//读取有多少条目 //读取有多少条目
var count int64 var count int64
models.DB.Model(TabPurchaseOrder{}).Count(&count) query := models.DB.Model(TabPurchaseOrder{})
//fmt.Println(count) if jsondata.Search != "" {
query = query.Where("title LIKE ?", "%"+jsondata.Search+"%")
}
if jsondata.Status != "" {
query = query.Where("order_status = ?", jsondata.Status)
}
query.Count(&count)
//读取条目 //读取条目
var getorders []TabPurchaseOrder var getorders []TabPurchaseOrder
models.DB.Order("created_at DESC").Offset(jsondata.Entries * (jsondata.Page - 1)).Limit(jsondata.Entries).Find(&getorders) query.Order("created_at DESC").Offset(jsondata.Entries * (jsondata.Page - 1)).Limit(jsondata.Entries).Find(&getorders)
ReturnJson(ctx, "apiOK", map[string]interface{}{ ReturnJson(ctx, "apiOK", map[string]interface{}{
"all_count": count, "all_count": count,
@@ -611,4 +618,39 @@ func ApiPurchase(r *gin.RouterGroup) {
ReturnJson(ctx, "apiOK", nil) ReturnJson(ctx, "apiOK", nil)
}) })
// 删除订单
r.POST("/deleteorder", func(ctx *gin.Context) {
isAuth, _, data := AuthenticationAuthority(ctx)
if !isAuth {
ReturnJson(ctx, "userCookieError", nil)
return
}
type FromDeleteOrder struct {
ID uint `json:"id"`
}
var from FromDeleteOrder
if err := decodeJSON(data, &from); err != nil || from.ID == 0 {
ReturnJson(ctx, "jsonErr", nil)
return
}
var order TabPurchaseOrder
if err := models.DB.Where("id = ?", from.ID).First(&order).Error; err != nil {
ReturnJson(ctx, "order_not_found", nil)
return
}
// 关联删除(硬删,不保留)
models.DB.Where("order_id = ?", from.ID).Delete(&TabPurchaseCosts{})
models.DB.Where("order_id = ?", from.ID).Delete(&TabPurchaseFileBind{})
models.DB.Where("order_id = ?", from.ID).Delete(&TabPurchaseCommit{})
models.DB.Where("order_id = ?", from.ID).Delete(&TabPurchaseLog{})
// 软删除订单本身
models.DB.Delete(&order)
ReturnJson(ctx, "apiOK", nil)
})
} }
+5
View File
@@ -25,4 +25,9 @@ export const purchaseApi = {
updateOrder(id, data) { updateOrder(id, data) {
return api.post('/purchase/updateorder', { id, ...data }) return api.post('/purchase/updateorder', { id, ...data })
}, },
/** 删除订单 */
deleteOrder(id) {
return api.post('/purchase/deleteorder', { id })
},
} }
@@ -0,0 +1,142 @@
<script setup>
/**
* ConfirmDialog —— 通用确认弹窗
*
* 使用方式:
* const ok = await confirmDialog({
* title: "提示标题",
* message: "确认要删除吗?",
* confirmText: "删除",
* cancelText: "取消",
* danger: true, // 红色确认按钮
* })
* if (ok) { ... }
*
* 或者作为组件使用 v-model:
* <ConfirmDialog v-model="show" @confirm="..." @cancel="..." />
*/
import { ref, watch } from "vue";
import { useI18n } from "vue-i18n";
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
title: {
type: String,
default: "",
},
message: {
type: String,
default: "",
},
confirmText: {
type: String,
default: "",
},
cancelText: {
type: String,
default: "",
},
danger: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(["update:modelValue", "confirm", "cancel"]);
const { t } = useI18n();
function close() {
emit("update:modelValue", false);
emit("cancel");
}
function confirm() {
emit("update:modelValue", false);
emit("confirm");
}
watch(
() => props.modelValue,
(val) => {
if (val) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
},
);
</script>
<template>
<Teleport to="body">
<Transition name="fade">
<div
v-if="modelValue"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
@click.self="close"
>
<div
class="min-w-[320px] max-w-sm rounded-xl border border-gray-200 bg-white shadow-2xl dark:border-dk-muted dark:bg-dk-card"
>
<!-- 标题 -->
<div class="flex items-center justify-between border-b border-gray-100 px-5 py-4 dark:border-dk-muted">
<h3 class="text-base font-semibold text-gray-900 dark:text-white">
{{ title || t("message.confirm") }}
</h3>
<button
class="ml-4 flex-shrink-0 rounded p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-dk-muted dark:hover:text-gray-200"
@click="close"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- 内容 -->
<div class="px-5 py-4">
<p class="text-sm leading-relaxed text-gray-600 dark:text-gray-300">
{{ message }}
</p>
</div>
<!-- 操作栏 -->
<div class="flex justify-end gap-3 border-t border-gray-100 px-5 py-4 dark:border-dk-muted">
<button
class="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 dark:border-dk-muted dark:bg-dk-base dark:text-gray-200 dark:hover:bg-dk-muted"
@click="close"
>
{{ cancelText || t("message.cancel") }}
</button>
<button
class="rounded-lg px-4 py-2 text-sm font-semibold text-white transition-colors"
:class="
danger
? 'bg-red-500 hover:bg-red-600'
: 'bg-blue-600 hover:bg-blue-700'
"
@click="confirm"
>
{{ confirmText || t("message.confirm") }}
</button>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.15s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
+7
View File
@@ -66,6 +66,7 @@
"created_at": "Created At", "created_at": "Created At",
"updated_at": "Updated At", "updated_at": "Updated At",
"status": "Status", "status": "Status",
"filter_all": "All",
"status_pending": "Pending", "status_pending": "Pending",
"status_ordered": "Ordered", "status_ordered": "Ordered",
"status_arrived": "Arrived", "status_arrived": "Arrived",
@@ -82,8 +83,11 @@
"There_are_a_total_of": ",There are a total of", "There_are_a_total_of": ",There are a total of",
"items": "orders.", "items": "orders.",
"order_detail": "Order Detail", "order_detail": "Order Detail",
"back": "Back",
"back_to_list": "Back to List", "back_to_list": "Back to List",
"order_not_found": "Order Not Found", "order_not_found": "Order Not Found",
"delete_order": "Delete Order",
"confirm_delete": "Are you sure you want to delete this order? This action cannot be undone.",
"order_info": "Order Information", "order_info": "Order Information",
"cost_detail": "Cost Details", "cost_detail": "Cost Details",
"photo_remarks": "Photos", "photo_remarks": "Photos",
@@ -227,12 +231,15 @@
"administrator": "Administrator", "administrator": "Administrator",
"select_date": "Select a date", "select_date": "Select a date",
"save_ok": "Saved successfully", "save_ok": "Saved successfully",
"delete_ok": "Deleted successfully",
"change_ok": "Changed successfully", "change_ok": "Changed successfully",
"type_old_pass": "Enter old password", "type_old_pass": "Enter old password",
"type_new_pass": "Enter new password", "type_new_pass": "Enter new password",
"type_cof_pass": "Confirm new password", "type_cof_pass": "Confirm new password",
"old_pass_incorrect": "Old password is incorrect", "old_pass_incorrect": "Old password is incorrect",
"confirm_password_incorrect": "Confirm password is incorrect", "confirm_password_incorrect": "Confirm password is incorrect",
"confirm": "Confirm",
"cancel": "Cancel",
"save_success": "Saved successfully", "save_success": "Saved successfully",
"submit": "Submit", "submit": "Submit",
"loading": "Loading..." "loading": "Loading..."
+7
View File
@@ -66,6 +66,7 @@
"created_at": "创建日期", "created_at": "创建日期",
"updated_at": "更新日期", "updated_at": "更新日期",
"status": "状态", "status": "状态",
"filter_all": "全部",
"status_pending": "待处理", "status_pending": "待处理",
"status_ordered": "已下单", "status_ordered": "已下单",
"status_arrived": "已到达", "status_arrived": "已到达",
@@ -82,8 +83,11 @@
"There_are_a_total_of": ",一共", "There_are_a_total_of": ",一共",
"items": "个订单", "items": "个订单",
"order_detail": "订单详情", "order_detail": "订单详情",
"back": "返回",
"back_to_list": "返回列表", "back_to_list": "返回列表",
"order_not_found": "订单不存在", "order_not_found": "订单不存在",
"delete_order": "删除订单",
"confirm_delete": "确定要删除此订单吗?此操作不可撤销。",
"order_info": "订单信息", "order_info": "订单信息",
"cost_detail": "费用明细", "cost_detail": "费用明细",
"photo_remarks": "图片备注", "photo_remarks": "图片备注",
@@ -233,6 +237,9 @@
"type_cof_pass": "确认新密码", "type_cof_pass": "确认新密码",
"old_pass_incorrect": "旧密码不正确", "old_pass_incorrect": "旧密码不正确",
"confirm_password_incorrect": "确认密码不正确", "confirm_password_incorrect": "确认密码不正确",
"confirm": "确认",
"cancel": "取消",
"delete_ok": "删除成功",
"save_success": "保存成功", "save_success": "保存成功",
"submit": "提交", "submit": "提交",
"loading": "加载中..." "loading": "加载中..."
@@ -5,7 +5,7 @@ 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'
import { purchaseApi } from '@/api/purchase' import { purchaseApi } from '@/api/purchase'
import { IconPlus, IconChevronLeftPipe, IconChevronRightPipe, IconChevronsLeft, IconChevronsRight, IconSearch } from '@tabler/icons-vue' import { IconPlus, IconChevronLeftPipe, IconChevronRightPipe, IconChevronsLeft, IconChevronsRight } from '@tabler/icons-vue'
usePageTitle('appname.purchase') usePageTitle('appname.purchase')
const { t, locale } = useI18n() const { t, locale } = useI18n()
@@ -16,9 +16,19 @@ const orders = ref([])
const totalCount = ref(0) const totalCount = ref(0)
const pageSize = ref(10) const pageSize = ref(10)
const currentPage = ref(1) const currentPage = ref(1)
const searchQuery = ref('') const statusFilter = ref('')
const loading = ref(false) const loading = ref(false)
const statusOptions = [
{ value: '', labelKey: 'purchase.filter_all' },
{ value: 'pending', labelKey: 'purchase.status_pending' },
{ value: 'ordered', labelKey: 'purchase.status_ordered' },
{ value: 'arrived', labelKey: 'purchase.status_arrived' },
{ value: 'received', labelKey: 'purchase.status_received' },
{ value: 'lost', labelKey: 'purchase.status_lost' },
{ value: 'returned', labelKey: 'purchase.status_returned' },
]
const totalPages = computed(() => Math.ceil(totalCount.value / pageSize.value) || 1) const totalPages = computed(() => Math.ceil(totalCount.value / pageSize.value) || 1)
const pageRange = computed(() => { const pageRange = computed(() => {
@@ -34,7 +44,7 @@ async function fetchOrders() {
loading.value = true loading.value = true
try { try {
const { errCode, data } = await purchaseApi.getOrders({ const { errCode, data } = await purchaseApi.getOrders({
search: searchQuery.value, status: statusFilter.value,
entries: pageSize.value, entries: pageSize.value,
page: currentPage.value, page: currentPage.value,
}) })
@@ -107,11 +117,16 @@ onMounted(fetchOrders)
<IconPlus :size="16" /> <IconPlus :size="16" />
{{ t('purchase.add_part') }} {{ t('purchase.add_part') }}
</RouterLink> </RouterLink>
<button class="rounded-lg border border-gray-300 px-3 py-1.5 text-sm text-gray-600 transition-colors hover:bg-gray-50 dark:border-dk-muted dark:text-gray-400">{{ t('purchase.exp_report') }}</button> <!-- 状态下拉筛选 -->
</div> <select
<div class="flex items-center gap-2"> v-model="statusFilter"
<label class="text-sm text-gray-500">{{ t('purchase.search') }}</label> class="rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm dark:border-dk-muted dark:bg-dk-base dark:text-white"
<input v-model="searchQuery" type="text" class="w-48 rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm outline-none transition-colors focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 dark:border-dk-muted dark:bg-dk-base dark:text-white" @keydown.enter="currentPage=1;fetchOrders()" /> @change="currentPage = 1; fetchOrders()"
>
<option v-for="opt in statusOptions" :key="opt.value" :value="opt.value">
{{ t(opt.labelKey) }}
</option>
</select>
</div> </div>
</div> </div>
@@ -17,6 +17,7 @@ import { useValidation } from "@/composables";
import { purchaseApi } from "@/api/purchase"; import { purchaseApi } from "@/api/purchase";
import tagadder from "@/components/tagadder.vue"; import tagadder from "@/components/tagadder.vue";
import useDropzone from "@/components/useDropzone.vue"; import useDropzone from "@/components/useDropzone.vue";
import ConfirmDialog from "@/components/ConfirmDialog.vue";
usePageTitle("purchase_addorder.edit_order"); usePageTitle("purchase_addorder.edit_order");
@@ -96,6 +97,7 @@ watch(
// ==================== 图片上传 ==================== // ==================== 图片上传 ====================
const dropzoneRef = ref(null); const dropzoneRef = ref(null);
const showDeleteConfirm = ref(false);
function getPhotoHashes() { function getPhotoHashes() {
return dropzoneRef.value?.return_files().map((f) => f.hash) ?? []; return dropzoneRef.value?.return_files().map((f) => f.hash) ?? [];
@@ -152,6 +154,30 @@ onMounted(async () => {
} }
}); });
// ==================== 提交 ====================
// ==================== 删除订单 ====================
async function handleDelete() {
showDeleteConfirm.value = true;
}
async function doDelete() {
loading.value = true;
try {
const res = await purchaseApi.deleteOrder(orderId);
if (res.errCode === 0) {
toast.success(t("message.delete_ok"));
router.replace("/purchase");
} else {
toast.error(t("message.server_error"));
}
} catch {
toast.error(t("message.server_error"));
} finally {
loading.value = false;
}
}
// ==================== 提交 ==================== // ==================== 提交 ====================
async function handleSubmit() { async function handleSubmit() {
clearErrors(); clearErrors();
@@ -241,6 +267,19 @@ async function handleSubmit() {
<h4 class="text-sm font-semibold text-gray-900 dark:text-white"> <h4 class="text-sm font-semibold text-gray-900 dark:text-white">
{{ t("purchase_addorder.edit_order") }} {{ t("purchase_addorder.edit_order") }}
</h4> </h4>
<!-- 操作按钮组 -->
<div class="flex items-center gap-2">
<!-- 删除按钮 -->
<button
class="flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20"
:disabled="loading"
@click="handleDelete"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
{{ t("purchase.delete_order") }}
</button>
<!-- 返回按钮 --> <!-- 返回按钮 -->
<button <button
class="flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-dk-base" class="flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-dk-base"
@@ -259,9 +298,10 @@ async function handleSubmit() {
d="M15 19l-7-7 7-7" d="M15 19l-7-7 7-7"
/> />
</svg> </svg>
{{ t("purchase.back_to_list") }} {{ t("purchase.back") }}
</button> </button>
</div> </div>
</div>
<!-- 错误提示字段验证 --> <!-- 错误提示字段验证 -->
<div <div
@@ -532,4 +572,12 @@ async function handleSubmit() {
</div> </div>
</div> </div>
</div> </div>
<!-- 通用确认弹窗 -->
<ConfirmDialog
v-model="showDeleteConfirm"
:title="t('purchase.confirm_delete')"
danger
@confirm="doDelete"
/>
</template> </template>