up
This commit is contained in:
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 2,
|
|
||||||
"sessions": {
|
|
||||||
"ebf5e3bab92e4319aec2c39116541728": [
|
|
||||||
{
|
|
||||||
"expertId": "BackendArchitect",
|
|
||||||
"name": "磐石石",
|
|
||||||
"profession": "后端架构师",
|
|
||||||
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/BackendArchitect/BackendArchitect.png",
|
|
||||||
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/BackendArchitect/BackendArchitect_zh.md",
|
|
||||||
"usedAt": 1776327849643,
|
|
||||||
"industryId": "all"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"lastUpdated": 1776336333635
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
# 2026-04-16 工作日志
|
|
||||||
|
|
||||||
## OPS2 系统工作流程分析
|
|
||||||
|
|
||||||
完成了对 OPS2 流程管理系统三端架构的全面分析,生成了完整的工作流程分析报告(ops2_workflow_analysis.md)。
|
|
||||||
|
|
||||||
### 系统技术栈
|
|
||||||
- 后端:Go + Gin + GORM,支持 SQLite/MySQL/PostgreSQL 可配置切换
|
|
||||||
- PC前端:Vue3 + Vite + Pinia + Axios
|
|
||||||
- 移动端:uni-app(Vue3+Vite),目标 H5 + Android
|
|
||||||
|
|
||||||
### 核心功能模块
|
|
||||||
1. **用户认证**:Cookie-based 认证,MD5+salt 密码哈希,支持 Remember Me
|
|
||||||
2. **日程管理**:日历事件 CRUD,权限分层(创建者/模块管理员/全局管理员)
|
|
||||||
3. **采购订单**:6种状态流转(pending→ordered→arrived→received/lost/returned),含完整审计日志
|
|
||||||
4. **文件系统**:SHA256 内容去重,MIME 白名单校验,图片上传服务
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 移动端重构(对标 PC 前端)
|
|
||||||
|
|
||||||
完成了 ops_uniapp 移动端的全面重构,参考 PC 前端架构对齐。
|
|
||||||
|
|
||||||
### 新增/重构文件
|
|
||||||
- `api/request.js`:统一请求封装(自动注入 cookie、统一 err_code 解析、cookie 过期自动跳登录)
|
|
||||||
- `api/auth.js`、`api/schedule.js`、`api/purchase.js`:对标 PC 端 API 模块
|
|
||||||
- `store/user.js`:轻量单例 Store(对标 PC 端 Pinia useUserStore,含 restoreSession)
|
|
||||||
- `pages/signin.vue`:重构登录页(表单验证、密码显示/隐藏、remember me)
|
|
||||||
- `pages/index/index.vue`:重构首页(今日日程卡片 + 待处理采购统计)
|
|
||||||
- `pages/setting/my_info.vue`:重构设置页(头像上传、信息修改、退出登录)
|
|
||||||
- `pages.json`:修正 API 路径前缀(/api/v1 → /api),更新页面路由
|
|
||||||
- `App.vue`:接入 userStore.restoreSession()
|
|
||||||
|
|
||||||
### 关键设计
|
|
||||||
- API base 统一为 `/api`(修复旧版 /api/v1 错误)
|
|
||||||
- cookie 认证流程与 PC 端完全对齐
|
|
||||||
- store/user.js 用模块单例替代 Pinia(uni-app 兼容)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 补建预留路由对应的 Vue 文件
|
|
||||||
|
|
||||||
修复了 `vite:import-analysis` 报错(pages.json 声明了路由但文件不存在)。
|
|
||||||
|
|
||||||
### 新建文件
|
|
||||||
- `pages/schedule/schedule.vue`:移动端日程月份视图,支持按月浏览、添加/编辑/删除日程、颜色分类,30s 轮询刷新
|
|
||||||
- `pages/purchase/list.vue`:移动端采购订单列表,支持搜索/状态过滤/分页加载更多;底部抽屉查看订单详情(费用明细/图片/变更记录);已登录可变更订单状态
|
|
||||||
|
|
||||||
### 重要发现
|
|
||||||
- 移动端 `my_network_func.js` 中 `head_path = "/api/v1"` 与后端实际 `/api` 路径不匹配,需修复
|
|
||||||
- 审计日志系统完善(TabScheduleLog、TabPurchaseLog、TabPurchaseCommit)
|
|
||||||
- 后端同时托管静态前端产物,单体部署架构
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## @api 命名空间重构(修复 404 模块请求)
|
|
||||||
|
|
||||||
### 问题
|
|
||||||
`GET http://localhost:5173/api/auth.js net::ERR_ABORTED 404`
|
|
||||||
|
|
||||||
manifest.json 配置了 `/api` 代理到后端 8080,导致所有 `/api` 开头的请求都被代理。
|
|
||||||
即使加了 vite.config.js 的 resolve.alias,HBuilderX 的内置 dev server 行为也不完全符合预期。
|
|
||||||
|
|
||||||
### 解决
|
|
||||||
将 API 模块目录从 `api/` 重命名为 `@api/`,彻底消除与 `/api` 代理前缀的冲突。
|
|
||||||
|
|
||||||
- `api/` → `@api/`,所有 import 路径相应更新
|
|
||||||
- 真正的后端 API 请求走 `/api`(代理到 8080)
|
|
||||||
- `@api/*.js` 模块不走 `/api` 代理,不会冲突
|
|
||||||
- `manifest.json` 的 h5.devServer 只保留 disableHostCheck,proxy 移至 vite.config.js
|
|
||||||
|
|
||||||
### 更新文件
|
|
||||||
- `@api/request.js`、`@api/auth.js`、`@api/schedule.js`、`@api/purchase.js`(新建)
|
|
||||||
- `store/user.js`、`pages/signin.vue`、`pages/setting/my_info.vue`、`pages/index/index.vue`、`pages/schedule/schedule.vue`、`pages/purchase/list.vue`(更新 import 路径)
|
|
||||||
- `vite.config.js`(简化 alias 配置)
|
|
||||||
- 删除旧 `api/` 目录
|
|
||||||
@@ -1,31 +1,13 @@
|
|||||||
# 2026-04-23 日志
|
# 2026-04-23
|
||||||
|
|
||||||
## 物品详情页编辑弹窗增加图片管理功能
|
- 仓库容器详情页(WarehouseContainerDetail.vue):物品列表表格在序列号列后新增备注列,显示 `item.Remark` 字段,colspan 从 6 改为 7
|
||||||
|
- 仓库总览页(WarehouseOverview.vue):物品列表表格同样在序列号列后新增备注列,显示 `item.Remark` 字段,colspan 从 6 改为 7
|
||||||
|
- 工单详情页(ShowWorkOrder.vue):卡头时间显示从仅"创建时间"改为"创建时间 + 更新时间",使用 `order.UpdatedAt`,中英文 i18n 新增 `work_order.updated_at`
|
||||||
|
|
||||||
**涉及文件:**
|
# 2026-04-24
|
||||||
- `frontend/ops_vue_js/src/views/warehouse/WarehouseItemDetail.vue` — 编辑弹窗增加 `useDropzone` 组件,支持加载已有图片、上传新图片、删除图片
|
|
||||||
- `frontend/ops_vue_js/src/components/useDropzone.vue` — 导出 `loadInitialFiles` 方法供外部调用
|
|
||||||
|
|
||||||
**实现方式:**
|
- 后端 apiWarehouse.go:TabWarehouseContainer 和 TabWarehouseItem 的 CreatedAt/UpdatedAt 从 string 改为 *time.Time,加 gorm autoCreateTime/autoUpdateTime tag,清理了所有手动设置时间的代码(6 处),所有字段补齐 json tag
|
||||||
- 编辑弹窗中新增 `editDropzoneRef` ref,绑定 `useDropzone` 组件
|
- 工单列表页(WorkOrderList.vue):创建时间列后新增更新时间列,显示 `order.UpdatedAt`,colspan 从 4 改为 5
|
||||||
- `openEdit()` 时调用 `loadInitialFiles()` 刷新初始文件
|
- 仓库总览页(WarehouseOverview.vue):容器列表和物品列表均新增更新时间列(创建时间后),colspan 从 7 改为 8,中英文 i18n 新增 `warehouse.updated_at`
|
||||||
- `submitEdit()` 时从 dropzone 获取所有图片哈希(包含新上传和已存在的),一并传给 `updateItem` API
|
- 容器详情页(WarehouseContainerDetail.vue):卡头元信息、子容器列表、物品列表三处均新增更新日期列/显示
|
||||||
- 后端 `update_item` API 已支持 `photos` 字段,会重建图片绑定
|
- 物品详情页(WarehouseItemDetail.vue):卡头新增更新日期显示;修复移动物品弹窗中目标容器搜索下拉框闪退问题(@mousedown.prevent + blur 延迟关闭)
|
||||||
|
|
||||||
**关键代码片段:**
|
|
||||||
```javascript
|
|
||||||
// 提交时获取所有图片哈希
|
|
||||||
const photos = getEditPhotoHashes()
|
|
||||||
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
|
|
||||||
|
|||||||
@@ -13,25 +13,27 @@ import (
|
|||||||
// ---------- 数据表结构 ----------
|
// ---------- 数据表结构 ----------
|
||||||
|
|
||||||
type TabWarehouseContainer struct {
|
type TabWarehouseContainer struct {
|
||||||
ID uint `gorm:"primaryKey"`
|
ID uint `gorm:"primaryKey" json:"ID"`
|
||||||
Title string `gorm:"size:255;not null;comment:容器名"`
|
Title string `gorm:"size:255;not null;comment:容器名" json:"Title"`
|
||||||
Remark string `gorm:"type:text;comment:描述"`
|
Remark string `gorm:"type:text;comment:描述" json:"Remark"`
|
||||||
CreatedAt string `gorm:"size:20;comment:创建日期"`
|
CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime" json:"CreatedAt"`
|
||||||
CreatorID uint `gorm:"not null;index;comment:创建者id"`
|
UpdatedAt *time.Time `gorm:"type:datetime;autoUpdateTime" json:"UpdatedAt"`
|
||||||
ParentID *uint `gorm:"index;comment:父容器id,nil=顶级"`
|
CreatorID uint `gorm:"not null;index;comment:创建者id" json:"CreatorID"`
|
||||||
ItemCount int `gorm:"default:0;comment:直接子物品数量"`
|
ParentID *uint `gorm:"index;comment:父容器id,nil=顶级" json:"ParentID"`
|
||||||
ChildCount int `gorm:"default:0;comment:子容器数量"`
|
ItemCount int `gorm:"default:0;comment:直接子物品数量" json:"ItemCount"`
|
||||||
|
ChildCount int `gorm:"default:0;comment:子容器数量" json:"ChildCount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TabWarehouseItem struct {
|
type TabWarehouseItem struct {
|
||||||
ID uint `gorm:"primaryKey"`
|
ID uint `gorm:"primaryKey" json:"ID"`
|
||||||
Name string `gorm:"size:255;not null;comment:物品名"`
|
Name string `gorm:"size:255;not null;comment:物品名" json:"Name"`
|
||||||
SerialNumber string `gorm:"size:255;comment:序列号"`
|
SerialNumber string `gorm:"size:255;comment:序列号" json:"SerialNumber"`
|
||||||
Remark string `gorm:"type:text;comment:描述"`
|
Remark string `gorm:"type:text;comment:描述" json:"Remark"`
|
||||||
Quantity int `gorm:"default:1;comment:数量"`
|
Quantity int `gorm:"default:1;comment:数量" json:"Quantity"`
|
||||||
CreatedAt string `gorm:"size:20;comment:创建日期"`
|
CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime" json:"CreatedAt"`
|
||||||
CreatorID uint `gorm:"not null;index;comment:创建者id"`
|
UpdatedAt *time.Time `gorm:"type:datetime;autoUpdateTime" json:"UpdatedAt"`
|
||||||
ContainerID *uint `gorm:"index;comment:所属容器id,nil=未入库"`
|
CreatorID uint `gorm:"not null;index;comment:创建者id" json:"CreatorID"`
|
||||||
|
ContainerID *uint `gorm:"index;comment:所属容器id,nil=未入库" json:"ContainerID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TabWarehouseContainerFileBind struct {
|
type TabWarehouseContainerFileBind struct {
|
||||||
@@ -192,7 +194,6 @@ func ApiWarehouse(r *gin.RouterGroup) {
|
|||||||
c := TabWarehouseContainer{
|
c := TabWarehouseContainer{
|
||||||
Title: from.Title,
|
Title: from.Title,
|
||||||
Remark: from.Remark,
|
Remark: from.Remark,
|
||||||
CreatedAt: strconv.FormatInt(time.Now().Unix(), 10),
|
|
||||||
CreatorID: user.ID,
|
CreatorID: user.ID,
|
||||||
ParentID: from.ParentID,
|
ParentID: from.ParentID,
|
||||||
}
|
}
|
||||||
@@ -366,10 +367,11 @@ func ApiWarehouse(r *gin.RouterGroup) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FromList struct {
|
type FromList struct {
|
||||||
Search string `json:"search"`
|
Search string `json:"search"`
|
||||||
ParentID *uint `json:"parent_id"`
|
ParentID *uint `json:"parent_id"`
|
||||||
Entries int `json:"entries"`
|
AllLevels bool `json:"all_levels"`
|
||||||
Page int `json:"page"`
|
Entries int `json:"entries"`
|
||||||
|
Page int `json:"page"`
|
||||||
}
|
}
|
||||||
var from FromList
|
var from FromList
|
||||||
if err := decodeJSON(data, &from); err != nil {
|
if err := decodeJSON(data, &from); err != nil {
|
||||||
@@ -390,8 +392,8 @@ func ApiWarehouse(r *gin.RouterGroup) {
|
|||||||
}
|
}
|
||||||
if from.ParentID != nil {
|
if from.ParentID != nil {
|
||||||
query = query.Where("parent_id = ?", *from.ParentID)
|
query = query.Where("parent_id = ?", *from.ParentID)
|
||||||
} else if from.Search == "" {
|
} else if from.Search == "" && !from.AllLevels {
|
||||||
// 无搜索时默认只显示顶级容器
|
// 无搜索时默认只显示顶级容器(all_levels=true 时返回所有层级)
|
||||||
query = query.Where("parent_id IS NULL")
|
query = query.Where("parent_id IS NULL")
|
||||||
}
|
}
|
||||||
query.Count(&count)
|
query.Count(&count)
|
||||||
@@ -568,7 +570,6 @@ func ApiWarehouse(r *gin.RouterGroup) {
|
|||||||
SerialNumber: from.SerialNumber,
|
SerialNumber: from.SerialNumber,
|
||||||
Remark: from.Remark,
|
Remark: from.Remark,
|
||||||
Quantity: quantity,
|
Quantity: quantity,
|
||||||
CreatedAt: strconv.FormatInt(time.Now().Unix(), 10),
|
|
||||||
CreatorID: user.ID,
|
CreatorID: user.ID,
|
||||||
ContainerID: from.ContainerID,
|
ContainerID: from.ContainerID,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,6 +131,7 @@
|
|||||||
"no_photos": "No photos",
|
"no_photos": "No photos",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"created_at": "Created At",
|
"created_at": "Created At",
|
||||||
|
"updated_at": "Updated At",
|
||||||
"filter_all": "All",
|
"filter_all": "All",
|
||||||
"status_pending": "Pending",
|
"status_pending": "Pending",
|
||||||
"status_checked": "Checked",
|
"status_checked": "Checked",
|
||||||
@@ -171,6 +172,7 @@
|
|||||||
"remark": "Remark",
|
"remark": "Remark",
|
||||||
"remark_placeholder": "Enter remark (optional)",
|
"remark_placeholder": "Enter remark (optional)",
|
||||||
"created_at": "Created At",
|
"created_at": "Created At",
|
||||||
|
"updated_at": "Updated At",
|
||||||
"created_by": "Created By",
|
"created_by": "Created By",
|
||||||
"child_containers": "Sub-Containers",
|
"child_containers": "Sub-Containers",
|
||||||
"items": "Items",
|
"items": "Items",
|
||||||
|
|||||||
@@ -131,6 +131,7 @@
|
|||||||
"no_photos": "暂无图片",
|
"no_photos": "暂无图片",
|
||||||
"status": "状态",
|
"status": "状态",
|
||||||
"created_at": "创建时间",
|
"created_at": "创建时间",
|
||||||
|
"updated_at": "更新时间",
|
||||||
"filter_all": "全部",
|
"filter_all": "全部",
|
||||||
"status_pending": "待处理",
|
"status_pending": "待处理",
|
||||||
"status_checked": "已检查",
|
"status_checked": "已检查",
|
||||||
@@ -171,6 +172,7 @@
|
|||||||
"remark": "备注",
|
"remark": "备注",
|
||||||
"remark_placeholder": "输入备注(可选)",
|
"remark_placeholder": "输入备注(可选)",
|
||||||
"created_at": "创建日期",
|
"created_at": "创建日期",
|
||||||
|
"updated_at": "更新日期",
|
||||||
"created_by": "创建人",
|
"created_by": "创建人",
|
||||||
"child_containers": "子容器",
|
"child_containers": "子容器",
|
||||||
"items": "物品",
|
"items": "物品",
|
||||||
|
|||||||
@@ -390,6 +390,7 @@ onMounted(async () => {
|
|||||||
{{ usersStore.getUsernameFromUserID(container.CreatorID) }}
|
{{ usersStore.getUsernameFromUserID(container.CreatorID) }}
|
||||||
</span>
|
</span>
|
||||||
<span>{{ t('warehouse.created_at') }}: {{ fmtTs(container.CreatedAt) }}</span>
|
<span>{{ t('warehouse.created_at') }}: {{ fmtTs(container.CreatedAt) }}</span>
|
||||||
|
<span v-if="container.UpdatedAt">{{ t('warehouse.updated_at') }}: {{ fmtTs(container.UpdatedAt) }}</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>
|
||||||
</div>
|
</div>
|
||||||
@@ -428,13 +429,14 @@ 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 whitespace-nowrap">{{ t('warehouse.updated_at') }}</th>
|
||||||
<th class="px-5 py-3 font-medium">{{ t('warehouse.created_by') }}</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="6" class="px-5 py-8 text-center">
|
<td colspan="7" 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" />
|
||||||
@@ -442,7 +444,7 @@ onMounted(async () => {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-else-if="subContainers.length === 0">
|
<tr v-else-if="subContainers.length === 0">
|
||||||
<td colspan="6" class="px-5 py-8 text-center text-gray-400 dark:text-gray-500">
|
<td colspan="7" 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>
|
||||||
@@ -470,6 +472,7 @@ 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 text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap">{{ fmtTs(c.UpdatedAt) }}</td>
|
||||||
<td class="px-5 py-3">
|
<td class="px-5 py-3">
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
<img
|
<img
|
||||||
@@ -552,15 +555,17 @@ onMounted(async () => {
|
|||||||
<tr class="border-b border-gray-200 bg-gray-50 text-gray-500 dark:border-dk-muted dark:bg-dk-base dark:text-gray-400">
|
<tr class="border-b border-gray-200 bg-gray-50 text-gray-500 dark:border-dk-muted dark:bg-dk-base dark:text-gray-400">
|
||||||
<th class="px-5 py-3 font-medium">{{ t('warehouse.item_name') }}</th>
|
<th class="px-5 py-3 font-medium">{{ t('warehouse.item_name') }}</th>
|
||||||
<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">{{ t('warehouse.remark') }}</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 whitespace-nowrap">{{ t('warehouse.updated_at') }}</th>
|
||||||
<th class="px-5 py-3 font-medium">{{ t('warehouse.created_by') }}</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="6" class="px-5 py-8 text-center">
|
<td colspan="8" 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" />
|
||||||
@@ -568,7 +573,7 @@ onMounted(async () => {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-else-if="items.length === 0">
|
<tr v-else-if="items.length === 0">
|
||||||
<td colspan="6" class="px-5 py-8 text-center text-gray-400 dark:text-gray-500">
|
<td colspan="8" 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>
|
||||||
@@ -586,8 +591,10 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</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-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-[200px] truncate">{{ item.Remark || '—' }}</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 text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap">{{ fmtTs(item.UpdatedAt) }}</td>
|
||||||
<td class="px-5 py-3">
|
<td class="px-5 py-3">
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ const targetContainers = ref([])
|
|||||||
const targetSearch = ref('')
|
const targetSearch = ref('')
|
||||||
const targetLoading = ref(false)
|
const targetLoading = ref(false)
|
||||||
const showTargetDropdown = ref(false)
|
const showTargetDropdown = ref(false)
|
||||||
|
let targetDropdownTimer = null
|
||||||
|
|
||||||
// ── 删除确认 ──
|
// ── 删除确认 ──
|
||||||
const showDeleteConfirm = ref(false)
|
const showDeleteConfirm = ref(false)
|
||||||
@@ -186,18 +187,39 @@ async function openMove() {
|
|||||||
showMove.value = true
|
showMove.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onTargetFocus() {
|
||||||
|
if (targetDropdownTimer) clearTimeout(targetDropdownTimer)
|
||||||
|
showTargetDropdown.value = true
|
||||||
|
loadTargetContainers(targetSearch.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTargetInput() {
|
||||||
|
if (targetDropdownTimer) clearTimeout(targetDropdownTimer)
|
||||||
|
showTargetDropdown.value = true
|
||||||
|
loadTargetContainers(targetSearch.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeTargetDropdown() {
|
||||||
|
targetDropdownTimer = setTimeout(() => {
|
||||||
|
showTargetDropdown.value = false
|
||||||
|
}, 150)
|
||||||
|
}
|
||||||
|
|
||||||
async function loadTargetContainers(search = '') {
|
async function loadTargetContainers(search = '') {
|
||||||
targetLoading.value = true
|
targetLoading.value = true
|
||||||
try {
|
try {
|
||||||
|
const isSearch = search.trim().length > 0
|
||||||
const { errCode, data } = await warehouseApi.getContainers({
|
const { errCode, data } = await warehouseApi.getContainers({
|
||||||
search,
|
search,
|
||||||
entries: 50,
|
all_levels: true,
|
||||||
|
entries: isSearch ? 50 : 10,
|
||||||
page: 1,
|
page: 1,
|
||||||
})
|
})
|
||||||
if (errCode === 0) {
|
if (errCode === 0) {
|
||||||
targetContainers.value = (data.containers ?? []).filter(
|
const filtered = (data.containers ?? []).filter(
|
||||||
(c) => c.ID !== itemId.value && c.ID !== item.value?.ContainerID
|
(c) => c.ID !== item.value?.ContainerID
|
||||||
)
|
)
|
||||||
|
targetContainers.value = isSearch ? filtered : filtered.slice(0, 5)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
targetContainers.value = []
|
targetContainers.value = []
|
||||||
@@ -389,6 +411,7 @@ onMounted(() => {
|
|||||||
{{ usersStore.getUsernameFromUserID(item.CreatorID) }}
|
{{ usersStore.getUsernameFromUserID(item.CreatorID) }}
|
||||||
</span>
|
</span>
|
||||||
<span>{{ t('warehouse.created_at') }}: {{ fmtTs(item.CreatedAt) }}</span>
|
<span>{{ t('warehouse.created_at') }}: {{ fmtTs(item.CreatedAt) }}</span>
|
||||||
|
<span v-if="item.UpdatedAt">{{ t('warehouse.updated_at') }}: {{ fmtTs(item.UpdatedAt) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -503,19 +526,21 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('warehouse.target_container') }}</label>
|
<label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('warehouse.target_container') }}</label>
|
||||||
<div class="relative">
|
<div class="relative" @click.stop>
|
||||||
<IconSearch class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" :size="15" />
|
<IconSearch class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" :size="15" />
|
||||||
<input
|
<input
|
||||||
v-model="targetSearch"
|
v-model="targetSearch"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="t('warehouse.search_container')"
|
:placeholder="t('warehouse.search_container')"
|
||||||
class="w-full rounded-lg border border-gray-300 bg-white py-2 pl-9 pr-3 text-sm dark:border-dk-muted dark:bg-dk-base dark:text-white"
|
class="w-full rounded-lg border border-gray-300 bg-white py-2 pl-9 pr-3 text-sm dark:border-dk-muted dark:bg-dk-base dark:text-white"
|
||||||
@focus="loadTargetContainers(''); showTargetDropdown = true"
|
@focus="onTargetFocus"
|
||||||
@input="loadTargetContainers(targetSearch); showTargetDropdown = true"
|
@input="onTargetInput"
|
||||||
|
@blur="closeTargetDropdown"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="showTargetDropdown && (targetContainers.length > 0 || targetLoading)"
|
v-if="showTargetDropdown"
|
||||||
class="absolute z-10 mt-1 w-full rounded-lg border border-gray-200 bg-white shadow-lg dark:border-dk-muted dark:bg-dk-card max-h-60 overflow-y-auto"
|
class="absolute z-10 mt-1 w-full rounded-lg border border-gray-200 bg-white shadow-lg dark:border-dk-muted dark:bg-dk-card max-h-60 overflow-y-auto"
|
||||||
|
@mousedown.prevent
|
||||||
>
|
>
|
||||||
<div v-if="targetLoading" class="px-3 py-2 text-xs text-gray-400">
|
<div v-if="targetLoading" class="px-3 py-2 text-xs text-gray-400">
|
||||||
<svg class="inline h-3.5 w-3.5 animate-spin mr-1" viewBox="0 0 24 24" fill="none">
|
<svg class="inline h-3.5 w-3.5 animate-spin mr-1" viewBox="0 0 24 24" fill="none">
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ function formatDate(dateStr) {
|
|||||||
d = new Date(dateStr)
|
d = new Date(dateStr)
|
||||||
}
|
}
|
||||||
if (isNaN(d.getTime())) return '—'
|
if (isNaN(d.getTime())) return '—'
|
||||||
return d.toLocaleDateString(isEn.value ? 'en-US' : 'zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' })
|
return d.toLocaleString(isEn.value ? 'en-US' : 'zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false })
|
||||||
} catch { return dateStr }
|
} catch { return dateStr }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,12 +426,13 @@ 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 whitespace-nowrap w-44">{{ t('warehouse.updated_at') }}</th>
|
||||||
<th class="px-6 py-3 font-medium">{{ t('warehouse.created_by') }}</th>
|
<th class="px-6 py-3 font-medium">{{ t('warehouse.created_by') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-if="containerLoading">
|
<tr v-if="containerLoading">
|
||||||
<td colspan="7" class="px-6 py-8 text-center text-gray-400">
|
<td colspan="8" class="px-6 py-8 text-center text-gray-400">
|
||||||
<svg class="mx-auto mb-2 h-5 w-5 animate-spin text-gray-400" viewBox="0 0 24 24" fill="none">
|
<svg class="mx-auto mb-2 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" />
|
||||||
@@ -440,7 +441,7 @@ onMounted(() => {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-else-if="containers.length === 0">
|
<tr v-else-if="containers.length === 0">
|
||||||
<td colspan="7" class="px-6 py-8 text-center text-gray-400 dark:text-gray-500">
|
<td colspan="8" class="px-6 py-8 text-center text-gray-400 dark:text-gray-500">
|
||||||
{{ t('warehouse.no_containers') }}
|
{{ t('warehouse.no_containers') }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -472,6 +473,7 @@ 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 whitespace-nowrap text-gray-500 dark:text-gray-400">{{ fmtTs(c.UpdatedAt) }}</td>
|
||||||
<td class="px-6 py-3">
|
<td class="px-6 py-3">
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
<img
|
<img
|
||||||
@@ -548,15 +550,17 @@ onMounted(() => {
|
|||||||
<tr class="border-b border-gray-200 bg-gray-50 text-gray-500 dark:border-dk-muted dark:bg-dk-base dark:text-gray-400">
|
<tr class="border-b border-gray-200 bg-gray-50 text-gray-500 dark:border-dk-muted dark:bg-dk-base dark:text-gray-400">
|
||||||
<th class="px-6 py-3 font-medium">{{ t('warehouse.item_name') }}</th>
|
<th class="px-6 py-3 font-medium">{{ t('warehouse.item_name') }}</th>
|
||||||
<th class="px-6 py-3 font-medium">{{ t('warehouse.serial_number') }}</th>
|
<th class="px-6 py-3 font-medium">{{ t('warehouse.serial_number') }}</th>
|
||||||
|
<th class="px-6 py-3 font-medium">{{ t('warehouse.remark') }}</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 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 whitespace-nowrap">{{ t('warehouse.updated_at') }}</th>
|
||||||
<th class="px-6 py-3 font-medium">{{ t('warehouse.created_by') }}</th>
|
<th class="px-6 py-3 font-medium">{{ t('warehouse.created_by') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-if="itemLoading">
|
<tr v-if="itemLoading">
|
||||||
<td colspan="6" class="px-6 py-8 text-center text-gray-400">
|
<td colspan="8" class="px-6 py-8 text-center text-gray-400">
|
||||||
<svg class="mx-auto mb-2 h-5 w-5 animate-spin text-gray-400" viewBox="0 0 24 24" fill="none">
|
<svg class="mx-auto mb-2 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" />
|
||||||
@@ -565,7 +569,7 @@ onMounted(() => {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-else-if="items.length === 0">
|
<tr v-else-if="items.length === 0">
|
||||||
<td colspan="6" class="px-6 py-8 text-center text-gray-400 dark:text-gray-500">
|
<td colspan="8" class="px-6 py-8 text-center text-gray-400 dark:text-gray-500">
|
||||||
{{ t('warehouse.no_items') }}
|
{{ t('warehouse.no_items') }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -577,6 +581,7 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
<td class="px-6 py-3 font-medium max-w-[200px] truncate">{{ item.Name }}</td>
|
<td class="px-6 py-3 font-medium max-w-[200px] truncate">{{ item.Name }}</td>
|
||||||
<td class="px-6 py-3 max-w-[160px] truncate text-xs text-gray-500 dark:text-gray-400">{{ item.SerialNumber || '—' }}</td>
|
<td class="px-6 py-3 max-w-[160px] truncate text-xs text-gray-500 dark:text-gray-400">{{ item.SerialNumber || '—' }}</td>
|
||||||
|
<td class="px-6 py-3 max-w-[200px] truncate text-xs text-gray-500 dark:text-gray-400">{{ item.Remark || '—' }}</td>
|
||||||
<td class="px-6 py-3 text-center text-sm">{{ item.Quantity }}</td>
|
<td class="px-6 py-3 text-center text-sm">{{ item.Quantity }}</td>
|
||||||
<td class="px-6 py-3">
|
<td class="px-6 py-3">
|
||||||
<span v-if="item.ContainerID != null" class="inline-flex items-center gap-1 text-sm text-blue-600">
|
<span v-if="item.ContainerID != null" class="inline-flex items-center gap-1 text-sm text-blue-600">
|
||||||
@@ -588,6 +593,7 @@ 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 whitespace-nowrap text-xs text-gray-400 dark:text-gray-500">{{ formatDate(item.UpdatedAt) }}</td>
|
||||||
<td class="px-6 py-3">
|
<td class="px-6 py-3">
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -393,7 +393,10 @@ onUnmounted(() => {
|
|||||||
{{ usersStore.getUsernameFromUserID(order.UserID) }}
|
{{ usersStore.getUsernameFromUserID(order.UserID) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-sm text-gray-400">{{ formatDate(order?.CreatedAt) }}</span>
|
<div class="text-sm text-gray-400 text-right">
|
||||||
|
<div>{{ t('work_order.created_at') }}: {{ formatDate(order?.CreatedAt) }}</div>
|
||||||
|
<div v-if="order?.UpdatedAt">{{ t('work_order.updated_at') }}: {{ formatDate(order.UpdatedAt) }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 状态快捷切换(所有登录用户可见) -->
|
<!-- 状态快捷切换(所有登录用户可见) -->
|
||||||
|
|||||||
@@ -144,12 +144,13 @@ onMounted(fetchOrders)
|
|||||||
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 w-16">No.</th>
|
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 w-16">No.</th>
|
||||||
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400">{{ t('work_order.title') }}</th>
|
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400">{{ t('work_order.title') }}</th>
|
||||||
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 whitespace-nowrap w-44">{{ t('work_order.created_at') }}</th>
|
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 whitespace-nowrap w-44">{{ t('work_order.created_at') }}</th>
|
||||||
|
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 whitespace-nowrap w-44">{{ t('work_order.updated_at') }}</th>
|
||||||
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 w-36">{{ t('work_order.status') }}</th>
|
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 w-36">{{ t('work_order.status') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-if="loading">
|
<tr v-if="loading">
|
||||||
<td colspan="4" class="px-6 py-8 text-center text-gray-400">
|
<td colspan="5" class="px-6 py-8 text-center text-gray-400">
|
||||||
<svg class="mx-auto mb-2 h-5 w-5 animate-spin text-gray-400" viewBox="0 0 24 24" fill="none">
|
<svg class="mx-auto mb-2 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" />
|
||||||
@@ -158,7 +159,7 @@ onMounted(fetchOrders)
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-else-if="orders.length === 0">
|
<tr v-else-if="orders.length === 0">
|
||||||
<td colspan="4" class="px-6 py-8 text-center text-gray-400 dark:text-gray-500">
|
<td colspan="5" class="px-6 py-8 text-center text-gray-400 dark:text-gray-500">
|
||||||
暂无工单
|
暂无工单
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -172,6 +173,7 @@ onMounted(fetchOrders)
|
|||||||
<td class="px-6 py-3 text-gray-500 dark:text-gray-400">{{ order.ID }}</td>
|
<td class="px-6 py-3 text-gray-500 dark:text-gray-400">{{ order.ID }}</td>
|
||||||
<td class="px-6 py-3 font-medium text-gray-900 dark:text-white max-w-xs truncate">{{ order.Title }}</td>
|
<td class="px-6 py-3 font-medium text-gray-900 dark:text-white max-w-xs truncate">{{ order.Title }}</td>
|
||||||
<td class="px-6 py-3 whitespace-nowrap text-gray-500 dark:text-gray-400">{{ formatDate(order.CreatedAt) }}</td>
|
<td class="px-6 py-3 whitespace-nowrap text-gray-500 dark:text-gray-400">{{ formatDate(order.CreatedAt) }}</td>
|
||||||
|
<td class="px-6 py-3 whitespace-nowrap text-gray-500 dark:text-gray-400">{{ formatDate(order.UpdatedAt) }}</td>
|
||||||
<td class="px-6 py-3">
|
<td class="px-6 py-3">
|
||||||
<span
|
<span
|
||||||
class="inline-block rounded-full px-2.5 py-0.5 text-xs font-medium"
|
class="inline-block rounded-full px-2.5 py-0.5 text-xs font-medium"
|
||||||
|
|||||||
Reference in New Issue
Block a user