up功能基本完成
This commit is contained in:
@@ -1,35 +1,31 @@
|
|||||||
# 2026-04-23 工作日志
|
# 2026-04-23 日志
|
||||||
|
|
||||||
## warehouse 模块前端开发
|
## 物品详情页编辑弹窗增加图片管理功能
|
||||||
|
|
||||||
### 今日完成
|
**涉及文件:**
|
||||||
- `apiWarehouse.go` 重写,参照 `apiWorkOrder.go` 模式
|
- `frontend/ops_vue_js/src/views/warehouse/WarehouseItemDetail.vue` — 编辑弹窗增加 `useDropzone` 组件,支持加载已有图片、上传新图片、删除图片
|
||||||
- 修复 4 处图片绑定查询 `hash` 未使用的问题
|
- `frontend/ops_vue_js/src/components/useDropzone.vue` — 导出 `loadInitialFiles` 方法供外部调用
|
||||||
- 清理 6 处重复的 `AuthenticationAuthority` 调用
|
|
||||||
- 容器列表页 (`WarehouseContainerList.vue`) — 根容器列表 + 统计卡片
|
|
||||||
- 容器详情页 (`WarehouseContainerDetail.vue`) — 子容器/物品 Tab + 新增子容器弹窗
|
|
||||||
- 新增物品独立页 (`WarehouseAddItem.vue`) + 路由注册
|
|
||||||
- 路由: `/warehouse/container/:id/add-item`
|
|
||||||
- 物品 Tab 按钮从弹窗改为跳转独立页面
|
|
||||||
- 使用 `useDropzone` 组件上传图片
|
|
||||||
- 物品详情页 (`WarehouseItemDetail.vue`) — 物品信息 + 移动历史/关联工单 Tab
|
|
||||||
- **合并页面 (`WarehouseOverview.vue`)** — 容器+物品合并到一个页面
|
|
||||||
- 路由 `/warehouse/container` 直接渲染此页面
|
|
||||||
- 顶部 3 格统计卡片(容器数/物品数/未入库)
|
|
||||||
- Tab 切换「容器」/「物品」
|
|
||||||
- 容器 Tab:搜索+表格+分页+新增/编辑弹窗+删除确认
|
|
||||||
- 物品 Tab:搜索(400ms防抖)+表格+分页+删除确认
|
|
||||||
- 删除了 `/warehouse/item` 独立路由和侧边栏「物品总览」入口
|
|
||||||
- 补充 i18n key(中/英双语)
|
|
||||||
|
|
||||||
### 踩坑
|
**实现方式:**
|
||||||
- 后端 `TabWarehouseItem.SerialNumber` JSON 字段名为 `serial_number`(小写),前端须对应
|
- 编辑弹窗中新增 `editDropzoneRef` ref,绑定 `useDropzone` 组件
|
||||||
- `useDropzone` 组件通过 `dropzoneRef.value.return_files()` 获取已上传文件的 hash 数组
|
- `openEdit()` 时调用 `loadInitialFiles()` 刷新初始文件
|
||||||
- `RouterLink` 在此项目为全局组件,无需 import
|
- `submitEdit()` 时从 dropzone 获取所有图片哈希(包含新上传和已存在的),一并传给 `updateItem` API
|
||||||
- `watch` 需要显式 import:`import { ref, reactive, computed, onMounted, watch } from 'vue'`
|
- 后端 `update_item` API 已支持 `photos` 字段,会重建图片绑定
|
||||||
- **项目没有 daisyUI**,所有样式均用纯 Tailwind CSS v4 实现
|
|
||||||
- 不用 `btn`、`tabs`、`tab`、`input-bordered`、`table`、`modal`、`join`、`form-control`、`badge`、`card` 等 daisyUI 类
|
**关键代码片段:**
|
||||||
- 用 Tailwind 自定义样式:`rounded-xl border border-gray-200 bg-white shadow dark:border-dk-muted dark:bg-dk-card`
|
```javascript
|
||||||
- 加载动画用自定义 SVG spinner,不用 `loading loading-spinner`
|
// 提交时获取所有图片哈希
|
||||||
- **弹窗用 `<Transition name="fade">` + `v-if` + `@click.self` 关闭,不用 `<dialog :open>`**,`<dialog>` 的 `:open` 属性在某些场景不会正确响应 false
|
const photos = getEditPhotoHashes()
|
||||||
- **批量修改 Vue 模板后务必检查缩进**:逐块替换时外层 div 的闭合标签容易被吞,造成 "Element is missing end tag" 错误
|
warehouseApi.updateItem({ id, name, serial_number, remark, quantity, photos })
|
||||||
|
```
|
||||||
|
|
||||||
|
## 物品编辑改为独立页面
|
||||||
|
|
||||||
|
**涉及文件:**
|
||||||
|
- `frontend/ops_vue_js/src/views/warehouse/WarehouseItemEdit.vue` — 新建,物品编辑独立页面
|
||||||
|
- `frontend/ops_vue_js/src/views/warehouse/WarehouseItemDetail.vue` — 编辑按钮改为 `router.push('/warehouse/item/edit/:id')`,移除弹窗代码
|
||||||
|
- `frontend/ops_vue_js/src/router/index.js` — 新增 `/warehouse/item/edit/:id` 路由
|
||||||
|
|
||||||
|
**实现方式:**
|
||||||
|
- 创建 `WarehouseItemEdit.vue`,`onMounted` 获取物品数据(包含已有图片),通过 `setTimeout` 调用 `loadInitialFiles()` 加载到 dropzone
|
||||||
|
- 详情页编辑按钮改为跳转,移除弹窗及相关 state/function
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package routers
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"ops/models"
|
"ops/models"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -82,6 +83,32 @@ type TabWarehouseItemWorkOrderBind struct {
|
|||||||
CreatedAt time.Time `gorm:"type:datetime;autoCreateTime"`
|
CreatedAt time.Time `gorm:"type:datetime;autoCreateTime"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
warehouseUserGroup models.TabUserGroups_
|
||||||
|
warehouseAdmins []uint
|
||||||
|
)
|
||||||
|
|
||||||
|
// updateWarehouseAdminsCash 刷新仓库管理员缓存
|
||||||
|
func updateWarehouseAdminsCash() {
|
||||||
|
warehouseAdmins = nil
|
||||||
|
warehouseAdmins = append(warehouseAdmins, 1) // id=1 超级管理员
|
||||||
|
var binds []models.TabUserGroupBinds_
|
||||||
|
models.DB.Where("group_id = ?", warehouseUserGroup.ID).Find(&binds)
|
||||||
|
for _, item := range binds {
|
||||||
|
if !slices.Contains(warehouseAdmins, item.UserID) {
|
||||||
|
warehouseAdmins = append(warehouseAdmins, item.UserID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// canModifyWarehouse 判断是否有权限修改/删除仓库资源(创建者或仓库管理员)
|
||||||
|
func canModifyWarehouse(userID, creatorUserID uint) bool {
|
||||||
|
if slices.Contains(warehouseAdmins, userID) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return userID == creatorUserID
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- 初始化 ----------
|
// ---------- 初始化 ----------
|
||||||
|
|
||||||
func ApiWarehouseInit() {
|
func ApiWarehouseInit() {
|
||||||
@@ -94,6 +121,14 @@ func ApiWarehouseInit() {
|
|||||||
&TabWarehouseLog{},
|
&TabWarehouseLog{},
|
||||||
&TabWarehouseItemWorkOrderBind{},
|
&TabWarehouseItemWorkOrderBind{},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
warehouseUserGroup.Name = "warehouse_admin"
|
||||||
|
if models.DB.Where(&warehouseUserGroup).First(&warehouseUserGroup).Error == nil {
|
||||||
|
updateWarehouseAdminsCash()
|
||||||
|
} else {
|
||||||
|
warehouseUserGroup.Type = "usergroup"
|
||||||
|
models.DB.Create(&warehouseUserGroup)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 路由注册 ----------
|
// ---------- 路由注册 ----------
|
||||||
@@ -228,6 +263,11 @@ func ApiWarehouse(r *gin.RouterGroup) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !canModifyWarehouse(user.ID, c.CreatorID) {
|
||||||
|
ReturnJson(ctx, "no_permission", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
oldContent, _ := json.Marshal(c)
|
oldContent, _ := json.Marshal(c)
|
||||||
models.DB.Model(&c).Updates(map[string]interface{}{
|
models.DB.Model(&c).Updates(map[string]interface{}{
|
||||||
"title": from.Title,
|
"title": from.Title,
|
||||||
@@ -284,6 +324,11 @@ func ApiWarehouse(r *gin.RouterGroup) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !canModifyWarehouse(user.ID, c.CreatorID) {
|
||||||
|
ReturnJson(ctx, "no_permission", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否有子容器或物品
|
// 检查是否有子容器或物品
|
||||||
if c.ChildCount > 0 || c.ItemCount > 0 {
|
if c.ChildCount > 0 || c.ItemCount > 0 {
|
||||||
ReturnJson(ctx, "container_not_empty", nil)
|
ReturnJson(ctx, "container_not_empty", nil)
|
||||||
@@ -314,7 +359,7 @@ func ApiWarehouse(r *gin.RouterGroup) {
|
|||||||
|
|
||||||
// 获取容器列表
|
// 获取容器列表
|
||||||
r.POST("/list_container", func(ctx *gin.Context) {
|
r.POST("/list_container", func(ctx *gin.Context) {
|
||||||
isAuth, _, data := AuthenticationAuthority(ctx)
|
isAuth, user, data := AuthenticationAuthority(ctx)
|
||||||
if !isAuth {
|
if !isAuth {
|
||||||
ReturnJson(ctx, "userCookieError", nil)
|
ReturnJson(ctx, "userCookieError", nil)
|
||||||
return
|
return
|
||||||
@@ -357,15 +402,21 @@ func ApiWarehouse(r *gin.RouterGroup) {
|
|||||||
Limit(from.Entries).
|
Limit(from.Entries).
|
||||||
Find(&containers)
|
Find(&containers)
|
||||||
|
|
||||||
|
canModifyContainers := make([]bool, len(containers))
|
||||||
|
for i, c := range containers {
|
||||||
|
canModifyContainers[i] = canModifyWarehouse(user.ID, c.CreatorID)
|
||||||
|
}
|
||||||
|
|
||||||
ReturnJson(ctx, "apiOK", gin.H{
|
ReturnJson(ctx, "apiOK", gin.H{
|
||||||
"all_count": count,
|
"all_count": count,
|
||||||
"containers": containers,
|
"containers": containers,
|
||||||
|
"canModifyContainers": canModifyContainers,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取容器详情
|
// 获取容器详情
|
||||||
r.POST("/get_container", func(ctx *gin.Context) {
|
r.POST("/get_container", func(ctx *gin.Context) {
|
||||||
isAuth, _, data := AuthenticationAuthority(ctx)
|
isAuth, user, data := AuthenticationAuthority(ctx)
|
||||||
if !isAuth {
|
if !isAuth {
|
||||||
ReturnJson(ctx, "userCookieError", nil)
|
ReturnJson(ctx, "userCookieError", nil)
|
||||||
return
|
return
|
||||||
@@ -428,10 +479,11 @@ func ApiWarehouse(r *gin.RouterGroup) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ReturnJson(ctx, "apiOK", gin.H{
|
ReturnJson(ctx, "apiOK", gin.H{
|
||||||
"container": c,
|
"container": c,
|
||||||
"photos": files,
|
"photos": files,
|
||||||
"parent_chain": parentChain,
|
"parent_chain": parentChain,
|
||||||
"depth": depth,
|
"depth": depth,
|
||||||
|
"canModifyContainer": canModifyWarehouse(user.ID, c.CreatorID),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -618,6 +670,11 @@ func ApiWarehouse(r *gin.RouterGroup) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !canModifyWarehouse(user.ID, item.CreatorID) {
|
||||||
|
ReturnJson(ctx, "no_permission", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
oldContent, _ := json.Marshal(item)
|
oldContent, _ := json.Marshal(item)
|
||||||
models.DB.Model(&item).Updates(map[string]interface{}{
|
models.DB.Model(&item).Updates(map[string]interface{}{
|
||||||
"name": from.Name,
|
"name": from.Name,
|
||||||
@@ -676,6 +733,11 @@ func ApiWarehouse(r *gin.RouterGroup) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !canModifyWarehouse(user.ID, item.CreatorID) {
|
||||||
|
ReturnJson(ctx, "no_permission", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 所属容器 ItemCount -1
|
// 所属容器 ItemCount -1
|
||||||
if item.ContainerID != nil {
|
if item.ContainerID != nil {
|
||||||
models.DB.Model(&TabWarehouseContainer{}).Where("id = ?", *item.ContainerID).Update("item_count", models.DB.Raw("item_count - 1"))
|
models.DB.Model(&TabWarehouseContainer{}).Where("id = ?", *item.ContainerID).Update("item_count", models.DB.Raw("item_count - 1"))
|
||||||
@@ -703,7 +765,7 @@ func ApiWarehouse(r *gin.RouterGroup) {
|
|||||||
|
|
||||||
// 获取物品列表
|
// 获取物品列表
|
||||||
r.POST("/list_item", func(ctx *gin.Context) {
|
r.POST("/list_item", func(ctx *gin.Context) {
|
||||||
isAuth, _, data := AuthenticationAuthority(ctx)
|
isAuth, user, data := AuthenticationAuthority(ctx)
|
||||||
if !isAuth {
|
if !isAuth {
|
||||||
ReturnJson(ctx, "userCookieError", nil)
|
ReturnJson(ctx, "userCookieError", nil)
|
||||||
return
|
return
|
||||||
@@ -744,15 +806,21 @@ func ApiWarehouse(r *gin.RouterGroup) {
|
|||||||
Limit(from.Entries).
|
Limit(from.Entries).
|
||||||
Find(&items)
|
Find(&items)
|
||||||
|
|
||||||
|
canModifyItems := make([]bool, len(items))
|
||||||
|
for i, item := range items {
|
||||||
|
canModifyItems[i] = canModifyWarehouse(user.ID, item.CreatorID)
|
||||||
|
}
|
||||||
|
|
||||||
ReturnJson(ctx, "apiOK", gin.H{
|
ReturnJson(ctx, "apiOK", gin.H{
|
||||||
"all_count": count,
|
"all_count": count,
|
||||||
"items": items,
|
"items": items,
|
||||||
|
"canModifyItems": canModifyItems,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取物品详情
|
// 获取物品详情
|
||||||
r.POST("/get_item", func(ctx *gin.Context) {
|
r.POST("/get_item", func(ctx *gin.Context) {
|
||||||
isAuth, _, data := AuthenticationAuthority(ctx)
|
isAuth, user, data := AuthenticationAuthority(ctx)
|
||||||
if !isAuth {
|
if !isAuth {
|
||||||
ReturnJson(ctx, "userCookieError", nil)
|
ReturnJson(ctx, "userCookieError", nil)
|
||||||
return
|
return
|
||||||
@@ -807,10 +875,11 @@ func ApiWarehouse(r *gin.RouterGroup) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ReturnJson(ctx, "apiOK", gin.H{
|
ReturnJson(ctx, "apiOK", gin.H{
|
||||||
"item": item,
|
"item": item,
|
||||||
"photos": files,
|
"photos": files,
|
||||||
"commits": commits,
|
"commits": commits,
|
||||||
"work_orders": workOrders,
|
"work_orders": workOrders,
|
||||||
|
"canModifyItem": canModifyWarehouse(user.ID, item.CreatorID),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -226,11 +226,11 @@ function return_files() {
|
|||||||
function loadInitialFiles() {
|
function loadInitialFiles() {
|
||||||
if (!dropzoneInstance || !prop.initialFiles?.length) return;
|
if (!dropzoneInstance || !prop.initialFiles?.length) return;
|
||||||
|
|
||||||
|
// 每次调用时先清空已有文件,避免重复追加
|
||||||
|
files.splice(0, files.length)
|
||||||
|
|
||||||
prop.initialFiles.forEach((f) => {
|
prop.initialFiles.forEach((f) => {
|
||||||
// 构造 Dropzone 期望的 mock file 对象
|
const url = `/api/files/get/${f.Sha256}`
|
||||||
//console.log(f);
|
|
||||||
// 填充上传结果字段
|
|
||||||
const url = `/api/files/get/${f.Sha256}`;
|
|
||||||
const mockFile = {
|
const mockFile = {
|
||||||
name: f.Name,
|
name: f.Name,
|
||||||
size: f.Size,
|
size: f.Size,
|
||||||
@@ -238,17 +238,17 @@ function loadInitialFiles() {
|
|||||||
status: Dropzone.SUCCESS,
|
status: Dropzone.SUCCESS,
|
||||||
accepted: true,
|
accepted: true,
|
||||||
upload: { uuid: f.Sha256 },
|
upload: { uuid: f.Sha256 },
|
||||||
isInput:true,
|
isInput: true,
|
||||||
previewElement: null,
|
previewElement: null,
|
||||||
_removeLink: null,
|
_removeLink: null,
|
||||||
};
|
}
|
||||||
// 通知 Dropzone "这是一个已存在的文件,不要上传"
|
|
||||||
// dropzoneInstance.emit("addedfile", mockFile);
|
|
||||||
// dropzoneInstance.emit("complete", mockFile);
|
|
||||||
// dropzoneInstance.files.push(mockFile);
|
|
||||||
// dropzoneInstance.emit("thumbnail", mockFile, url);
|
|
||||||
|
|
||||||
dropzoneInstance.displayExistingFile(mockFile, url);
|
// displayExistingFile 会触发 addedfile 事件,但我们需要手动 push 到 files 数组,
|
||||||
|
// 所以传 false 阻止事件触发,由我们自己 push,同时手动绑定 lightbox
|
||||||
|
dropzoneInstance.displayExistingFile(mockFile, url, false)
|
||||||
|
|
||||||
|
// 为已有文件的预览元素绑定 lightbox 点击事件
|
||||||
|
clik_file_event(mockFile)
|
||||||
|
|
||||||
files.push({
|
files.push({
|
||||||
uuid: f.Sha256,
|
uuid: f.Sha256,
|
||||||
@@ -258,8 +258,8 @@ function loadInitialFiles() {
|
|||||||
file_name: f.Name,
|
file_name: f.Name,
|
||||||
file_size: f.Size,
|
file_size: f.Size,
|
||||||
is_upload: true,
|
is_upload: true,
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 组件挂载时初始化
|
// 组件挂载时初始化
|
||||||
@@ -277,6 +277,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
return_files,
|
return_files,
|
||||||
|
loadInitialFiles,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,11 @@ const router = createRouter({
|
|||||||
name: 'warehouse-item-detail',
|
name: 'warehouse-item-detail',
|
||||||
component: () => import('@/views/warehouse/WarehouseItemDetail.vue'),
|
component: () => import('@/views/warehouse/WarehouseItemDetail.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'warehouse/item/edit/:id',
|
||||||
|
name: 'warehouse-item-edit',
|
||||||
|
component: () => import('@/views/warehouse/WarehouseItemEdit.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'admin',
|
path: 'admin',
|
||||||
name: 'admin',
|
name: 'admin',
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ const container = ref(null)
|
|||||||
const photos = ref([])
|
const photos = ref([])
|
||||||
const parentChain = ref([])
|
const parentChain = ref([])
|
||||||
const containerDepth = ref(0)
|
const containerDepth = ref(0)
|
||||||
|
const canModifyContainer = ref(false)
|
||||||
const loadingDetail = ref(true)
|
const loadingDetail = ref(true)
|
||||||
const notFound = ref(false)
|
const notFound = ref(false)
|
||||||
|
|
||||||
@@ -134,6 +135,7 @@ async function fetchContainer() {
|
|||||||
photos.value = data.photos ?? []
|
photos.value = data.photos ?? []
|
||||||
parentChain.value = data.parent_chain ?? []
|
parentChain.value = data.parent_chain ?? []
|
||||||
containerDepth.value = data.depth ?? 0
|
containerDepth.value = data.depth ?? 0
|
||||||
|
canModifyContainer.value = data.canModifyContainer === true
|
||||||
} else {
|
} else {
|
||||||
notFound.value = true
|
notFound.value = true
|
||||||
}
|
}
|
||||||
@@ -328,6 +330,7 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
|
v-if="canModifyContainer"
|
||||||
class="inline-flex items-center gap-1.5 rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 dark:border-dk-muted dark:bg-dk-base dark:text-white dark:hover:bg-dk-muted"
|
class="inline-flex items-center gap-1.5 rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 dark:border-dk-muted dark:bg-dk-base dark:text-white dark:hover:bg-dk-muted"
|
||||||
@click="openEdit"
|
@click="openEdit"
|
||||||
>
|
>
|
||||||
@@ -335,7 +338,7 @@ onMounted(async () => {
|
|||||||
{{ t('warehouse.edit') }}
|
{{ t('warehouse.edit') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="container.ChildCount === 0 && container.ItemCount === 0"
|
v-if="canModifyContainer && container.ChildCount === 0 && container.ItemCount === 0"
|
||||||
class="inline-flex items-center gap-1.5 rounded-lg border border-red-300 bg-white px-3 py-1.5 text-sm font-medium text-red-600 transition-colors hover:bg-red-50 dark:border-red-900 dark:bg-dk-base dark:text-red-400 dark:hover:bg-red-900/20"
|
class="inline-flex items-center gap-1.5 rounded-lg border border-red-300 bg-white px-3 py-1.5 text-sm font-medium text-red-600 transition-colors hover:bg-red-50 dark:border-red-900 dark:bg-dk-base dark:text-red-400 dark:hover:bg-red-900/20"
|
||||||
@click="confirmDelete"
|
@click="confirmDelete"
|
||||||
>
|
>
|
||||||
@@ -378,7 +381,14 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<!-- 元信息 -->
|
<!-- 元信息 -->
|
||||||
<div class="flex flex-wrap gap-x-6 gap-y-1 text-xs text-gray-400 dark:text-gray-500">
|
<div class="flex flex-wrap gap-x-6 gap-y-1 text-xs text-gray-400 dark:text-gray-500">
|
||||||
<span>{{ t('warehouse.created_by') }}: {{ usersStore.getUsernameFromUserID(container.CreatorID) }}</span>
|
<span class="flex items-center gap-1">
|
||||||
|
<span>{{ t('warehouse.created_by') }}:</span>
|
||||||
|
<img
|
||||||
|
:src="usersStore.getAvatarUrlFromUserID(container.CreatorID)"
|
||||||
|
class="w-4 h-4 rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
{{ usersStore.getUsernameFromUserID(container.CreatorID) }}
|
||||||
|
</span>
|
||||||
<span>{{ t('warehouse.created_at') }}: {{ fmtTs(container.CreatedAt) }}</span>
|
<span>{{ t('warehouse.created_at') }}: {{ fmtTs(container.CreatedAt) }}</span>
|
||||||
<span>{{ t('warehouse.child_containers') }}: {{ container.ChildCount }}</span>
|
<span>{{ t('warehouse.child_containers') }}: {{ container.ChildCount }}</span>
|
||||||
<span>{{ t('warehouse.items') }}: {{ container.ItemCount }}</span>
|
<span>{{ t('warehouse.items') }}: {{ container.ItemCount }}</span>
|
||||||
@@ -418,12 +428,13 @@ onMounted(async () => {
|
|||||||
<th class="px-5 py-3 font-medium w-24 text-center">{{ t('warehouse.child_containers') }}</th>
|
<th class="px-5 py-3 font-medium w-24 text-center">{{ t('warehouse.child_containers') }}</th>
|
||||||
<th class="px-5 py-3 font-medium w-24 text-center">{{ t('warehouse.items') }}</th>
|
<th class="px-5 py-3 font-medium w-24 text-center">{{ t('warehouse.items') }}</th>
|
||||||
<th class="px-5 py-3 font-medium whitespace-nowrap">{{ t('warehouse.created_at') }}</th>
|
<th class="px-5 py-3 font-medium whitespace-nowrap">{{ t('warehouse.created_at') }}</th>
|
||||||
|
<th class="px-5 py-3 font-medium">{{ t('warehouse.created_by') }}</th>
|
||||||
<th class="px-5 py-3 font-medium w-24 text-right">{{ t('warehouse.actions') }}</th>
|
<th class="px-5 py-3 font-medium w-24 text-right">{{ t('warehouse.actions') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-if="loadingSub">
|
<tr v-if="loadingSub">
|
||||||
<td colspan="5" class="px-5 py-8 text-center">
|
<td colspan="6" class="px-5 py-8 text-center">
|
||||||
<svg class="mx-auto h-5 w-5 animate-spin text-gray-400" viewBox="0 0 24 24" fill="none">
|
<svg class="mx-auto h-5 w-5 animate-spin text-gray-400" viewBox="0 0 24 24" fill="none">
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
|
||||||
@@ -431,7 +442,7 @@ onMounted(async () => {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-else-if="subContainers.length === 0">
|
<tr v-else-if="subContainers.length === 0">
|
||||||
<td colspan="5" class="px-5 py-8 text-center text-gray-400 dark:text-gray-500">
|
<td colspan="6" class="px-5 py-8 text-center text-gray-400 dark:text-gray-500">
|
||||||
{{ t('warehouse.no_containers') }}
|
{{ t('warehouse.no_containers') }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -459,6 +470,15 @@ onMounted(async () => {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-5 py-3 text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap">{{ fmtTs(c.CreatedAt) }}</td>
|
<td class="px-5 py-3 text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap">{{ fmtTs(c.CreatedAt) }}</td>
|
||||||
|
<td class="px-5 py-3">
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<img
|
||||||
|
:src="usersStore.getAvatarUrlFromUserID(c.CreatorID)"
|
||||||
|
class="w-5 h-5 rounded-full object-cover flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<span class="truncate text-gray-600 dark:text-gray-400">{{ usersStore.getUsernameFromUserID(c.CreatorID) }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td class="px-5 py-3 text-right">
|
<td class="px-5 py-3 text-right">
|
||||||
<button
|
<button
|
||||||
class="text-xs text-blue-500 hover:underline"
|
class="text-xs text-blue-500 hover:underline"
|
||||||
@@ -534,12 +554,13 @@ onMounted(async () => {
|
|||||||
<th class="px-5 py-3 font-medium">{{ t('warehouse.serial_number') }}</th>
|
<th class="px-5 py-3 font-medium">{{ t('warehouse.serial_number') }}</th>
|
||||||
<th class="px-5 py-3 font-medium w-20 text-center">{{ t('warehouse.quantity') }}</th>
|
<th class="px-5 py-3 font-medium w-20 text-center">{{ t('warehouse.quantity') }}</th>
|
||||||
<th class="px-5 py-3 font-medium whitespace-nowrap">{{ t('warehouse.created_at') }}</th>
|
<th class="px-5 py-3 font-medium whitespace-nowrap">{{ t('warehouse.created_at') }}</th>
|
||||||
|
<th class="px-5 py-3 font-medium">{{ t('warehouse.created_by') }}</th>
|
||||||
<th class="px-5 py-3 font-medium w-20 text-right">{{ t('warehouse.actions') }}</th>
|
<th class="px-5 py-3 font-medium w-20 text-right">{{ t('warehouse.actions') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-if="loadingItems">
|
<tr v-if="loadingItems">
|
||||||
<td colspan="5" class="px-5 py-8 text-center">
|
<td colspan="6" class="px-5 py-8 text-center">
|
||||||
<svg class="mx-auto h-5 w-5 animate-spin text-gray-400" viewBox="0 0 24 24" fill="none">
|
<svg class="mx-auto h-5 w-5 animate-spin text-gray-400" viewBox="0 0 24 24" fill="none">
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
|
||||||
@@ -547,7 +568,7 @@ onMounted(async () => {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-else-if="items.length === 0">
|
<tr v-else-if="items.length === 0">
|
||||||
<td colspan="5" class="px-5 py-8 text-center text-gray-400 dark:text-gray-500">
|
<td colspan="6" class="px-5 py-8 text-center text-gray-400 dark:text-gray-500">
|
||||||
{{ t('warehouse.no_items') }}
|
{{ t('warehouse.no_items') }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -567,6 +588,15 @@ onMounted(async () => {
|
|||||||
<td class="px-5 py-3 text-xs text-gray-500 dark:text-gray-400 max-w-[140px] truncate">{{ item.serial_number || '—' }}</td>
|
<td class="px-5 py-3 text-xs text-gray-500 dark:text-gray-400 max-w-[140px] truncate">{{ item.serial_number || '—' }}</td>
|
||||||
<td class="px-5 py-3 text-center text-sm">{{ item.Quantity }}</td>
|
<td class="px-5 py-3 text-center text-sm">{{ item.Quantity }}</td>
|
||||||
<td class="px-5 py-3 text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap">{{ fmtTs(item.CreatedAt) }}</td>
|
<td class="px-5 py-3 text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap">{{ fmtTs(item.CreatedAt) }}</td>
|
||||||
|
<td class="px-5 py-3">
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<img
|
||||||
|
:src="usersStore.getAvatarUrlFromUserID(item.CreatorID)"
|
||||||
|
class="w-5 h-5 rounded-full object-cover flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<span class="truncate text-gray-600 dark:text-gray-400">{{ usersStore.getUsernameFromUserID(item.CreatorID) }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td class="px-5 py-3 text-right">
|
<td class="px-5 py-3 text-right">
|
||||||
<button
|
<button
|
||||||
class="text-xs text-blue-500 hover:underline"
|
class="text-xs text-blue-500 hover:underline"
|
||||||
@@ -727,7 +757,6 @@ onMounted(async () => {
|
|||||||
:confirm-text="t('warehouse.delete')"
|
:confirm-text="t('warehouse.delete')"
|
||||||
:cancel-text="t('message.cancel')"
|
:cancel-text="t('message.cancel')"
|
||||||
danger
|
danger
|
||||||
:confirm-loading="deleting"
|
|
||||||
@confirm="doDelete"
|
@confirm="doDelete"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const item = ref(null)
|
|||||||
const photos = ref([])
|
const photos = ref([])
|
||||||
const commits = ref([])
|
const commits = ref([])
|
||||||
const workOrders = ref([])
|
const workOrders = ref([])
|
||||||
|
const canModifyItem = ref(false)
|
||||||
const loadingDetail = ref(true)
|
const loadingDetail = ref(true)
|
||||||
const notFound = ref(false)
|
const notFound = ref(false)
|
||||||
|
|
||||||
@@ -40,16 +41,6 @@ const containerNames = reactive({})
|
|||||||
// ── Tab ──
|
// ── Tab ──
|
||||||
const activeTab = ref('work_orders')
|
const activeTab = ref('work_orders')
|
||||||
|
|
||||||
// ── 编辑弹窗 ──
|
|
||||||
const showEdit = ref(false)
|
|
||||||
const editForm = reactive({
|
|
||||||
name: '',
|
|
||||||
serial_number: '',
|
|
||||||
remark: '',
|
|
||||||
quantity: 1,
|
|
||||||
})
|
|
||||||
const submittingEdit = ref(false)
|
|
||||||
|
|
||||||
// ── 移动弹窗 ──
|
// ── 移动弹窗 ──
|
||||||
const showMove = ref(false)
|
const showMove = ref(false)
|
||||||
const moveTarget = ref(null)
|
const moveTarget = ref(null)
|
||||||
@@ -118,6 +109,7 @@ async function fetchItem() {
|
|||||||
photos.value = data.photos ?? []
|
photos.value = data.photos ?? []
|
||||||
commits.value = data.commits ?? []
|
commits.value = data.commits ?? []
|
||||||
workOrders.value = data.work_orders ?? []
|
workOrders.value = data.work_orders ?? []
|
||||||
|
canModifyItem.value = data.canModifyItem === true
|
||||||
loadContainerNames()
|
loadContainerNames()
|
||||||
} else {
|
} else {
|
||||||
notFound.value = true
|
notFound.value = true
|
||||||
@@ -184,43 +176,6 @@ function openLinkWorkOrder() {
|
|||||||
router.push('/work_order/add')
|
router.push('/work_order/add')
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 编辑 ──
|
|
||||||
function openEdit() {
|
|
||||||
editForm.name = item.value?.Name ?? ''
|
|
||||||
editForm.serial_number = item.value?.SerialNumber ?? ''
|
|
||||||
editForm.remark = item.value?.Remark ?? ''
|
|
||||||
editForm.quantity = item.value?.Quantity ?? 1
|
|
||||||
showEdit.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitEdit() {
|
|
||||||
if (!editForm.name.trim()) {
|
|
||||||
toast.error(t('warehouse.item_name_required'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
submittingEdit.value = true
|
|
||||||
try {
|
|
||||||
const { errCode } = await warehouseApi.updateItem({
|
|
||||||
id: itemId.value,
|
|
||||||
name: editForm.name.trim(),
|
|
||||||
serial_number: editForm.serial_number.trim(),
|
|
||||||
remark: editForm.remark.trim(),
|
|
||||||
quantity: editForm.quantity > 0 ? editForm.quantity : 1,
|
|
||||||
})
|
|
||||||
if (errCode === 0) {
|
|
||||||
toast.success(t('message.save_success'))
|
|
||||||
showEdit.value = false
|
|
||||||
fetchItem()
|
|
||||||
} else {
|
|
||||||
toast.error(t('message.server_error'))
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
toast.error(t('message.server_error'))
|
|
||||||
} finally {
|
|
||||||
submittingEdit.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 移动 ──
|
// ── 移动 ──
|
||||||
async function openMove() {
|
async function openMove() {
|
||||||
moveTarget.value = item.value?.ContainerID ?? null
|
moveTarget.value = item.value?.ContainerID ?? null
|
||||||
@@ -356,13 +311,15 @@ onMounted(() => {
|
|||||||
{{ t('warehouse.move_item') }}
|
{{ t('warehouse.move_item') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
v-if="canModifyItem"
|
||||||
class="inline-flex items-center gap-1.5 rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 dark:border-dk-muted dark:bg-dk-base dark:text-white dark:hover:bg-dk-muted"
|
class="inline-flex items-center gap-1.5 rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 dark:border-dk-muted dark:bg-dk-base dark:text-white dark:hover:bg-dk-muted"
|
||||||
@click="openEdit"
|
@click="router.push(`/warehouse/item/edit/${itemId}`)"
|
||||||
>
|
>
|
||||||
<IconEdit :size="14" />
|
<IconEdit :size="14" />
|
||||||
{{ t('warehouse.edit') }}
|
{{ t('warehouse.edit') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
v-if="canModifyItem"
|
||||||
class="inline-flex items-center gap-1.5 rounded-lg border border-red-300 bg-white px-3 py-1.5 text-sm font-medium text-red-600 transition-colors hover:bg-red-50 dark:border-red-900 dark:bg-dk-base dark:text-red-400 dark:hover:bg-red-900/20"
|
class="inline-flex items-center gap-1.5 rounded-lg border border-red-300 bg-white px-3 py-1.5 text-sm font-medium text-red-600 transition-colors hover:bg-red-50 dark:border-red-900 dark:bg-dk-base dark:text-red-400 dark:hover:bg-red-900/20"
|
||||||
@click="showDeleteConfirm = true"
|
@click="showDeleteConfirm = true"
|
||||||
>
|
>
|
||||||
@@ -423,7 +380,14 @@ onMounted(() => {
|
|||||||
|
|
||||||
<!-- 元信息 -->
|
<!-- 元信息 -->
|
||||||
<div class="flex flex-wrap gap-x-6 gap-y-1 text-xs text-gray-400 dark:text-gray-500">
|
<div class="flex flex-wrap gap-x-6 gap-y-1 text-xs text-gray-400 dark:text-gray-500">
|
||||||
<span>{{ t('warehouse.created_by') }}: {{ usersStore.getUsernameFromUserID(item.CreatorID) }}</span>
|
<span class="flex items-center gap-1">
|
||||||
|
<span>{{ t('warehouse.created_by') }}:</span>
|
||||||
|
<img
|
||||||
|
:src="usersStore.getAvatarUrlFromUserID(item.CreatorID)"
|
||||||
|
class="w-4 h-4 rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
{{ usersStore.getUsernameFromUserID(item.CreatorID) }}
|
||||||
|
</span>
|
||||||
<span>{{ t('warehouse.created_at') }}: {{ fmtTs(item.CreatedAt) }}</span>
|
<span>{{ t('warehouse.created_at') }}: {{ fmtTs(item.CreatedAt) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -514,77 +478,6 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 编辑弹窗 -->
|
|
||||||
<Transition name="fade">
|
|
||||||
<div
|
|
||||||
v-if="showEdit"
|
|
||||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
|
|
||||||
@click.self="showEdit = false"
|
|
||||||
>
|
|
||||||
<div class="w-full max-w-md rounded-xl border border-gray-200 bg-white p-5 shadow-xl dark:border-dk-muted dark:bg-dk-card">
|
|
||||||
<h3 class="mb-4 text-base font-semibold text-gray-900 dark:text-white">{{ t('warehouse.edit_item') }}</h3>
|
|
||||||
<div class="space-y-3">
|
|
||||||
<div>
|
|
||||||
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('warehouse.item_name') }} *</label>
|
|
||||||
<input
|
|
||||||
v-model="editForm.name"
|
|
||||||
type="text"
|
|
||||||
:placeholder="t('warehouse.item_name_placeholder')"
|
|
||||||
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-dk-muted dark:bg-dk-base dark:text-white"
|
|
||||||
@keyup.enter="submitEdit"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('warehouse.serial_number') }}</label>
|
|
||||||
<input
|
|
||||||
v-model="editForm.serial_number"
|
|
||||||
type="text"
|
|
||||||
:placeholder="t('warehouse.serial_number_placeholder')"
|
|
||||||
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-dk-muted dark:bg-dk-base dark:text-white"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('warehouse.quantity') }}</label>
|
|
||||||
<input
|
|
||||||
v-model.number="editForm.quantity"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
class="w-28 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-dk-muted dark:bg-dk-base dark:text-white"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('warehouse.remark') }}</label>
|
|
||||||
<textarea
|
|
||||||
v-model="editForm.remark"
|
|
||||||
:placeholder="t('warehouse.remark_placeholder')"
|
|
||||||
rows="3"
|
|
||||||
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-dk-muted dark:bg-dk-base dark:text-white"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-4 flex justify-end gap-2">
|
|
||||||
<button
|
|
||||||
class="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm dark:border-dk-muted dark:bg-dk-base dark:text-white hover:bg-gray-50 dark:hover:bg-dk-muted"
|
|
||||||
@click="showEdit = false"
|
|
||||||
>
|
|
||||||
{{ t('message.cancel') }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="inline-flex items-center gap-1.5 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
|
|
||||||
:disabled="submittingEdit"
|
|
||||||
@click="submitEdit"
|
|
||||||
>
|
|
||||||
<svg v-if="submittingEdit" class="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none">
|
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
|
||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
|
|
||||||
</svg>
|
|
||||||
{{ t('message.save') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
|
|
||||||
<!-- 移动弹窗 -->
|
<!-- 移动弹窗 -->
|
||||||
<Transition name="fade">
|
<Transition name="fade">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useToastStore } from '@/stores/toast'
|
||||||
|
import { usePageTitle } from '@/composables/usePageTitle'
|
||||||
|
import { warehouseApi } from '@/api/warehouse'
|
||||||
|
import useDropzone from '@/components/useDropzone.vue'
|
||||||
|
|
||||||
|
usePageTitle('warehouse.edit_item')
|
||||||
|
const { t } = useI18n()
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const toast = useToastStore()
|
||||||
|
|
||||||
|
const itemId = ref(parseInt(route.params.id))
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
name: '',
|
||||||
|
serial_number: '',
|
||||||
|
remark: '',
|
||||||
|
quantity: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
const submitting = ref(false)
|
||||||
|
const loadingItem = ref(true)
|
||||||
|
const itemNotFound = ref(false)
|
||||||
|
const containerName = ref('')
|
||||||
|
const existingPhotos = ref([])
|
||||||
|
|
||||||
|
const dropzoneRef = ref(null)
|
||||||
|
|
||||||
|
function getPhotoHashes() {
|
||||||
|
return dropzoneRef.value?.return_files().map((f) => f.hash) ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const { errCode, data } = await warehouseApi.getItem(itemId.value)
|
||||||
|
if (errCode === 0 && data?.item) {
|
||||||
|
form.name = data.item.Name ?? ''
|
||||||
|
form.serial_number = data.item.SerialNumber ?? ''
|
||||||
|
form.remark = data.item.Remark ?? ''
|
||||||
|
form.quantity = data.item.Quantity ?? 1
|
||||||
|
existingPhotos.value = data.photos ?? []
|
||||||
|
|
||||||
|
// 获取容器名称
|
||||||
|
if (data.item.ContainerID) {
|
||||||
|
const { errCode: cErr, data: cData } = await warehouseApi.getContainer(data.item.ContainerID)
|
||||||
|
if (cErr === 0 && cData?.container) {
|
||||||
|
containerName.value = cData.container.Title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
itemNotFound.value = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
itemNotFound.value = true
|
||||||
|
} finally {
|
||||||
|
loadingItem.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
if (!form.name.trim()) {
|
||||||
|
toast.error(t('warehouse.item_name_required'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待图片上传完成
|
||||||
|
await new Promise((r) => setTimeout(r, 200))
|
||||||
|
const hashes = getPhotoHashes()
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const { errCode } = await warehouseApi.updateItem({
|
||||||
|
id: itemId.value,
|
||||||
|
name: form.name.trim(),
|
||||||
|
serial_number: form.serial_number.trim(),
|
||||||
|
remark: form.remark.trim(),
|
||||||
|
quantity: form.quantity > 0 ? form.quantity : 1,
|
||||||
|
photos: hashes,
|
||||||
|
})
|
||||||
|
if (errCode === 0) {
|
||||||
|
toast.success(t('message.save_success'))
|
||||||
|
router.push(`/warehouse/item/${itemId.value}`)
|
||||||
|
} else {
|
||||||
|
toast.error(t('message.server_error'))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error(t('message.server_error'))
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-4 max-w-2xl mx-auto space-y-4">
|
||||||
|
|
||||||
|
<!-- 加载 -->
|
||||||
|
<div v-if="loadingItem" class="flex justify-center py-16">
|
||||||
|
<svg class="h-6 w-6 animate-spin text-gray-400" viewBox="0 0 24 24" fill="none">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 未找到 -->
|
||||||
|
<div v-else-if="itemNotFound" class="flex flex-col items-center justify-center py-16 text-gray-400">
|
||||||
|
<p>{{ t('message.not_found') }}</p>
|
||||||
|
<button class="mt-2 text-sm text-blue-500 hover:underline" @click="router.push('/warehouse/container')">
|
||||||
|
{{ t('warehouse.back_to_list') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表单 -->
|
||||||
|
<template v-else>
|
||||||
|
<!-- 面包屑 -->
|
||||||
|
<div class="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<RouterLink to="/warehouse/container" class="text-blue-500 hover:underline">
|
||||||
|
{{ t('warehouse.container_list') }}
|
||||||
|
</RouterLink>
|
||||||
|
<span>/</span>
|
||||||
|
<RouterLink
|
||||||
|
v-if="containerName"
|
||||||
|
:to="`/warehouse/container/${itemId}`"
|
||||||
|
class="text-blue-500 hover:underline"
|
||||||
|
>
|
||||||
|
{{ containerName }}
|
||||||
|
</RouterLink>
|
||||||
|
<span v-else>/</span>
|
||||||
|
<span>/</span>
|
||||||
|
<RouterLink
|
||||||
|
:to="`/warehouse/item/${itemId}`"
|
||||||
|
class="text-blue-500 hover:underline"
|
||||||
|
>
|
||||||
|
#{{ itemId }}
|
||||||
|
</RouterLink>
|
||||||
|
<span>/</span>
|
||||||
|
<span class="text-gray-700 dark:text-gray-200">{{ t('warehouse.edit_item') }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表单卡片 -->
|
||||||
|
<div class="rounded-xl border border-gray-200 bg-white px-5 py-5 shadow dark:border-dk-muted dark:bg-dk-card">
|
||||||
|
<h2 class="mb-5 text-base font-semibold text-gray-900 dark:text-white">
|
||||||
|
{{ t('warehouse.edit_item') }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
|
||||||
|
<!-- 物品名称 -->
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('warehouse.item_name') }} *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="form.name"
|
||||||
|
type="text"
|
||||||
|
:placeholder="t('warehouse.item_name_placeholder')"
|
||||||
|
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-dk-muted dark:bg-dk-base dark:text-white"
|
||||||
|
@keyup.enter="submit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 序列号 -->
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('warehouse.serial_number') }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="form.serial_number"
|
||||||
|
type="text"
|
||||||
|
:placeholder="t('warehouse.serial_number_placeholder')"
|
||||||
|
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-dk-muted dark:bg-dk-base dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 数量 -->
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('warehouse.quantity') }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model.number="form.quantity"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
class="w-28 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-dk-muted dark:bg-dk-base dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 备注 -->
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('warehouse.remark') }}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
v-model="form.remark"
|
||||||
|
:placeholder="t('warehouse.remark_placeholder')"
|
||||||
|
rows="3"
|
||||||
|
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-dk-muted dark:bg-dk-base dark:text-white"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图片上传 -->
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('purchase_addorder.upload_photos') }}
|
||||||
|
</label>
|
||||||
|
<useDropzone
|
||||||
|
ref="dropzoneRef"
|
||||||
|
uploadURL="/api/files/upload/image"
|
||||||
|
:max-files="9"
|
||||||
|
:initial-files="existingPhotos"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="mt-6 flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
class="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm dark:border-dk-muted dark:bg-dk-base dark:text-white hover:bg-gray-50 dark:hover:bg-dk-muted"
|
||||||
|
@click="router.push(`/warehouse/item/${itemId}`)"
|
||||||
|
>
|
||||||
|
{{ t('message.cancel') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center gap-1.5 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
|
||||||
|
:disabled="submitting"
|
||||||
|
@click="submit"
|
||||||
|
>
|
||||||
|
<svg v-if="submitting" class="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
|
||||||
|
</svg>
|
||||||
|
{{ t('message.save') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -22,6 +22,7 @@ const toast = useToastStore()
|
|||||||
|
|
||||||
// ── 状态 ──
|
// ── 状态 ──
|
||||||
const items = ref([])
|
const items = ref([])
|
||||||
|
const canModifyItems = ref([]) // 并行数组:与 items 下标对应
|
||||||
const totalCount = ref(0)
|
const totalCount = ref(0)
|
||||||
const pageSize = ref(10)
|
const pageSize = ref(10)
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
@@ -49,6 +50,11 @@ function getContainerTitle(cid) {
|
|||||||
return containerMap.value[cid] || `#${cid}`
|
return containerMap.value[cid] || `#${cid}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 权限判断 ──
|
||||||
|
function canModifyItem(idx) {
|
||||||
|
return canModifyItems.value[idx] === true
|
||||||
|
}
|
||||||
|
|
||||||
// ── 获取容器名映射 ──
|
// ── 获取容器名映射 ──
|
||||||
async function fetchContainerMap() {
|
async function fetchContainerMap() {
|
||||||
try {
|
try {
|
||||||
@@ -95,6 +101,7 @@ async function fetchItems() {
|
|||||||
})
|
})
|
||||||
if (errCode === 0 && data) {
|
if (errCode === 0 && data) {
|
||||||
items.value = data.items || []
|
items.value = data.items || []
|
||||||
|
canModifyItems.value = data.canModifyItems || []
|
||||||
totalCount.value = data.all_count || 0
|
totalCount.value = data.all_count || 0
|
||||||
stats.total = data.all_count || 0
|
stats.total = data.all_count || 0
|
||||||
stats.inContainer = items.value.filter(i => i.container_id != null).length
|
stats.inContainer = items.value.filter(i => i.container_id != null).length
|
||||||
@@ -273,7 +280,7 @@ onMounted(() => {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr
|
<tr
|
||||||
v-for="item in items"
|
v-for="(item, idx) in items"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="border-b border-gray-100 cursor-pointer transition-colors hover:bg-gray-50 dark:border-dk-muted dark:hover:bg-dk-base"
|
class="border-b border-gray-100 cursor-pointer transition-colors hover:bg-gray-50 dark:border-dk-muted dark:hover:bg-dk-base"
|
||||||
@click="goToDetail(item)"
|
@click="goToDetail(item)"
|
||||||
@@ -293,6 +300,7 @@ onMounted(() => {
|
|||||||
<td class="px-5 py-3 text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap">{{ formatDate(item.created_at) }}</td>
|
<td class="px-5 py-3 text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap">{{ formatDate(item.created_at) }}</td>
|
||||||
<td class="px-5 py-3 text-right" @click.stop>
|
<td class="px-5 py-3 text-right" @click.stop>
|
||||||
<button
|
<button
|
||||||
|
v-if="canModifyItem(idx)"
|
||||||
class="inline-flex items-center justify-center w-7 h-7 rounded text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20"
|
class="inline-flex items-center justify-center w-7 h-7 rounded text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20"
|
||||||
@click="askDelete(item)"
|
@click="askDelete(item)"
|
||||||
>
|
>
|
||||||
@@ -347,7 +355,6 @@ onMounted(() => {
|
|||||||
v-model="confirmDelete"
|
v-model="confirmDelete"
|
||||||
:title="t('warehouse.delete_item_title')"
|
:title="t('warehouse.delete_item_title')"
|
||||||
:message="t('warehouse.delete_item_msg', { name: deleteTarget?.name })"
|
:message="t('warehouse.delete_item_msg', { name: deleteTarget?.name })"
|
||||||
:confirm-loading="deletingItem"
|
|
||||||
@confirm="doDeleteItem"
|
@confirm="doDeleteItem"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ref, reactive, computed, onMounted } from 'vue'
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useToastStore } from '@/stores/toast'
|
import { useToastStore } from '@/stores/toast'
|
||||||
|
import { useUsersStore } from '@/stores/users'
|
||||||
import { usePageTitle } from '@/composables/usePageTitle'
|
import { usePageTitle } from '@/composables/usePageTitle'
|
||||||
import { warehouseApi } from '@/api/warehouse'
|
import { warehouseApi } from '@/api/warehouse'
|
||||||
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
||||||
@@ -23,6 +24,7 @@ usePageTitle('warehouse.overview')
|
|||||||
const { t, locale } = useI18n()
|
const { t, locale } = useI18n()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const toast = useToastStore()
|
const toast = useToastStore()
|
||||||
|
const usersStore = useUsersStore()
|
||||||
|
|
||||||
const isEn = computed(() => locale.value === 'en')
|
const isEn = computed(() => locale.value === 'en')
|
||||||
|
|
||||||
@@ -37,6 +39,7 @@ const stats = reactive({
|
|||||||
// 容器相关
|
// 容器相关
|
||||||
// ═══════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════
|
||||||
const containers = ref([])
|
const containers = ref([])
|
||||||
|
const canModifyContainers = ref([]) // 并行数组:与 containers 下标对应
|
||||||
const containerTotal = ref(0)
|
const containerTotal = ref(0)
|
||||||
const containerPage = ref(1)
|
const containerPage = ref(1)
|
||||||
const containerPageSize = ref(10)
|
const containerPageSize = ref(10)
|
||||||
@@ -68,6 +71,14 @@ function containerPageRange() {
|
|||||||
return Array.from({ length: end - start + 1 }, (_, i) => start + i)
|
return Array.from({ length: end - start + 1 }, (_, i) => start + i)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 权限判断 ──
|
||||||
|
function canModifyContainer(index) {
|
||||||
|
return canModifyContainers.value[index] === true
|
||||||
|
}
|
||||||
|
function canModifyItem(index) {
|
||||||
|
return canModifyItems.value[index] === true
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchContainerStats() {
|
async function fetchContainerStats() {
|
||||||
try {
|
try {
|
||||||
const { errCode, data } = await warehouseApi.getCount()
|
const { errCode, data } = await warehouseApi.getCount()
|
||||||
@@ -89,6 +100,7 @@ async function fetchContainers() {
|
|||||||
})
|
})
|
||||||
if (errCode === 0) {
|
if (errCode === 0) {
|
||||||
containers.value = data.containers ?? []
|
containers.value = data.containers ?? []
|
||||||
|
canModifyContainers.value = data.canModifyContainers ?? []
|
||||||
containerTotal.value = data.all_count ?? 0
|
containerTotal.value = data.all_count ?? 0
|
||||||
} else {
|
} else {
|
||||||
toast.error(t('message.server_error'))
|
toast.error(t('message.server_error'))
|
||||||
@@ -201,6 +213,7 @@ function jumpToContainer(id) {
|
|||||||
// 物品相关
|
// 物品相关
|
||||||
// ═══════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════
|
||||||
const items = ref([])
|
const items = ref([])
|
||||||
|
const canModifyItems = ref([]) // 并行数组:与 items 下标对应
|
||||||
const itemTotal = ref(0)
|
const itemTotal = ref(0)
|
||||||
const itemPage = ref(1)
|
const itemPage = ref(1)
|
||||||
const itemPageSize = ref(10)
|
const itemPageSize = ref(10)
|
||||||
@@ -220,6 +233,7 @@ async function fetchItems() {
|
|||||||
})
|
})
|
||||||
if (errCode === 0 && data) {
|
if (errCode === 0 && data) {
|
||||||
items.value = data.items || []
|
items.value = data.items || []
|
||||||
|
canModifyItems.value = data.canModifyItems || []
|
||||||
itemTotal.value = data.all_count || 0
|
itemTotal.value = data.all_count || 0
|
||||||
itemStats.total = data.all_count || 0
|
itemStats.total = data.all_count || 0
|
||||||
itemStats.inContainer = items.value.filter(i => i.ContainerID != null).length
|
itemStats.inContainer = items.value.filter(i => i.ContainerID != null).length
|
||||||
@@ -412,7 +426,7 @@ onMounted(() => {
|
|||||||
<th class="px-6 py-3 font-medium w-24 text-center">{{ t('warehouse.child_containers') }}</th>
|
<th class="px-6 py-3 font-medium w-24 text-center">{{ t('warehouse.child_containers') }}</th>
|
||||||
<th class="px-6 py-3 font-medium w-24 text-center">{{ t('warehouse.items') }}</th>
|
<th class="px-6 py-3 font-medium w-24 text-center">{{ t('warehouse.items') }}</th>
|
||||||
<th class="px-6 py-3 font-medium whitespace-nowrap w-44">{{ t('warehouse.created_at') }}</th>
|
<th class="px-6 py-3 font-medium whitespace-nowrap w-44">{{ t('warehouse.created_at') }}</th>
|
||||||
<th class="px-6 py-3 font-medium w-28 text-right">{{ t('warehouse.actions') }}</th>
|
<th class="px-6 py-3 font-medium">{{ t('warehouse.created_by') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -432,7 +446,7 @@ onMounted(() => {
|
|||||||
</tr>
|
</tr>
|
||||||
<tr
|
<tr
|
||||||
v-else
|
v-else
|
||||||
v-for="c in containers"
|
v-for="(c, idx) in containers"
|
||||||
:key="c.ID"
|
:key="c.ID"
|
||||||
class="cursor-pointer border-b border-gray-100 transition-colors hover:bg-gray-50 dark:border-dk-muted dark:hover:bg-dk-base"
|
class="cursor-pointer border-b border-gray-100 transition-colors hover:bg-gray-50 dark:border-dk-muted dark:hover:bg-dk-base"
|
||||||
@click="jumpToContainer(c.ID)"
|
@click="jumpToContainer(c.ID)"
|
||||||
@@ -458,30 +472,13 @@ onMounted(() => {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-3 whitespace-nowrap text-gray-500 dark:text-gray-400">{{ fmtTs(c.CreatedAt) }}</td>
|
<td class="px-6 py-3 whitespace-nowrap text-gray-500 dark:text-gray-400">{{ fmtTs(c.CreatedAt) }}</td>
|
||||||
<td class="px-6 py-3 text-right" @click.stop>
|
<td class="px-6 py-3">
|
||||||
<div class="flex items-center justify-end gap-1">
|
<div class="flex items-center gap-1.5">
|
||||||
<button
|
<img
|
||||||
v-if="c.ChildCount === 0 && c.ItemCount === 0"
|
:src="usersStore.getAvatarUrlFromUserID(c.CreatorID)"
|
||||||
class="rounded p-1.5 text-gray-400 hover:bg-gray-100 hover:text-red-500 dark:hover:bg-dk-muted"
|
class="w-5 h-5 rounded-full object-cover flex-shrink-0"
|
||||||
:title="t('warehouse.delete')"
|
/>
|
||||||
@click="confirmDeleteContainer(c.ID, c.Title, $event)"
|
<span class="truncate text-gray-600 dark:text-gray-400">{{ usersStore.getUsernameFromUserID(c.CreatorID) }}</span>
|
||||||
>
|
|
||||||
<IconTrash :size="15" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="rounded p-1.5 text-gray-400 hover:bg-gray-100 hover:text-blue-500 dark:hover:bg-dk-muted"
|
|
||||||
:title="t('warehouse.edit')"
|
|
||||||
@click="openEditContainer(c.ID, $event)"
|
|
||||||
>
|
|
||||||
<IconEdit :size="15" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="rounded p-1.5 text-gray-400 hover:bg-gray-100 hover:text-blue-500 dark:hover:bg-dk-muted"
|
|
||||||
:title="t('warehouse.view_items')"
|
|
||||||
@click="jumpToContainer(c.ID)"
|
|
||||||
>
|
|
||||||
<IconChevronRight :size="15" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -554,7 +551,7 @@ onMounted(() => {
|
|||||||
<th class="px-6 py-3 font-medium w-20 text-center">{{ t('warehouse.quantity') }}</th>
|
<th class="px-6 py-3 font-medium w-20 text-center">{{ t('warehouse.quantity') }}</th>
|
||||||
<th class="px-6 py-3 font-medium">{{ t('warehouse.location') }}</th>
|
<th class="px-6 py-3 font-medium">{{ t('warehouse.location') }}</th>
|
||||||
<th class="px-6 py-3 font-medium whitespace-nowrap">{{ t('warehouse.created_at') }}</th>
|
<th class="px-6 py-3 font-medium whitespace-nowrap">{{ t('warehouse.created_at') }}</th>
|
||||||
<th class="px-6 py-3 font-medium w-16 text-right">{{ t('warehouse.actions') }}</th>
|
<th class="px-6 py-3 font-medium">{{ t('warehouse.created_by') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -574,7 +571,7 @@ onMounted(() => {
|
|||||||
</tr>
|
</tr>
|
||||||
<tr
|
<tr
|
||||||
v-else
|
v-else
|
||||||
v-for="item in items" :key="item.ID"
|
v-for="(item, idx) in items" :key="item.ID"
|
||||||
class="cursor-pointer border-b border-gray-100 transition-colors hover:bg-gray-50 dark:border-dk-muted dark:hover:bg-dk-base"
|
class="cursor-pointer border-b border-gray-100 transition-colors hover:bg-gray-50 dark:border-dk-muted dark:hover:bg-dk-base"
|
||||||
@click="goToItemDetail(item)"
|
@click="goToItemDetail(item)"
|
||||||
>
|
>
|
||||||
@@ -591,13 +588,14 @@ onMounted(() => {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-3 whitespace-nowrap text-xs text-gray-400 dark:text-gray-500">{{ formatDate(item.CreatedAt) }}</td>
|
<td class="px-6 py-3 whitespace-nowrap text-xs text-gray-400 dark:text-gray-500">{{ formatDate(item.CreatedAt) }}</td>
|
||||||
<td class="px-6 py-3 text-right" @click.stop>
|
<td class="px-6 py-3">
|
||||||
<button
|
<div class="flex items-center gap-1.5">
|
||||||
class="flex h-7 w-7 items-center justify-center rounded text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20"
|
<img
|
||||||
@click="askDeleteItem(item)"
|
:src="usersStore.getAvatarUrlFromUserID(item.CreatorID)"
|
||||||
>
|
class="w-5 h-5 rounded-full object-cover flex-shrink-0"
|
||||||
<IconTrash :size="14" />
|
/>
|
||||||
</button>
|
<span class="truncate text-gray-600 dark:text-gray-400">{{ usersStore.getUsernameFromUserID(item.CreatorID) }}</span>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -698,7 +696,6 @@ onMounted(() => {
|
|||||||
:message="t('warehouse.delete_confirm_msg', { name: deletingName })"
|
:message="t('warehouse.delete_confirm_msg', { name: deletingName })"
|
||||||
:confirm-text="t('warehouse.delete')"
|
:confirm-text="t('warehouse.delete')"
|
||||||
:cancel-text="t('message.cancel')"
|
:cancel-text="t('message.cancel')"
|
||||||
:confirm-loading="deleting"
|
|
||||||
danger
|
danger
|
||||||
@confirm="doDeleteContainer"
|
@confirm="doDeleteContainer"
|
||||||
/>
|
/>
|
||||||
@@ -708,7 +705,6 @@ onMounted(() => {
|
|||||||
v-model="showDeleteItemConfirm"
|
v-model="showDeleteItemConfirm"
|
||||||
:title="t('warehouse.delete_item_title')"
|
:title="t('warehouse.delete_item_title')"
|
||||||
:message="t('warehouse.delete_item_msg', { name: deleteItemTarget?.name })"
|
:message="t('warehouse.delete_item_msg', { name: deleteItemTarget?.name })"
|
||||||
:confirm-loading="deletingItem"
|
|
||||||
@confirm="doDeleteItem"
|
@confirm="doDeleteItem"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user