Compare commits
12 Commits
b547844dca
...
da8db95141
| Author | SHA1 | Date | |
|---|---|---|---|
| da8db95141 | |||
| e183ea5583 | |||
| 34347dafb0 | |||
| a0bdce0359 | |||
| c0feab6be4 | |||
| eb52e2fd48 | |||
| f3fb000bda | |||
| b00ebd892f | |||
| ae720656b1 | |||
| 3b29bb0bd1 | |||
| c836944f92 | |||
| f665cf7e24 |
@ -154,16 +154,17 @@ async def receive_task_endpoint(
|
|||||||
task_id: str,
|
task_id: str,
|
||||||
operator_id: str | None = Query(None, description="操作人ID"),
|
operator_id: str | None = Query(None, description="操作人ID"),
|
||||||
remark: str | None = Body(None, description="接收备注", embed=True),
|
remark: str | None = Body(None, description="接收备注", embed=True),
|
||||||
|
task_name: str | None = Body(None, description="接收人选定的工序名称", embed=True),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
**确认接收任务。**
|
**确认接收任务。工人选定工序名称后接收。**
|
||||||
|
|
||||||
校验:只有状态为 PENDING 的任务可接收。
|
校验:只有状态为 PENDING 的任务可接收。
|
||||||
动作:将状态改为 WIP,记录 received_at 为当前时间,写入 remark。
|
动作:状态改为 WIP,记录 received_at,更新 task_name,同步产品宏观状态。
|
||||||
"""
|
"""
|
||||||
return await task_service.receive_task(
|
return await task_service.receive_task(
|
||||||
db, uuid.UUID(task_id), operator_id, remark
|
db, uuid.UUID(task_id), operator_id, remark, task_name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -180,9 +180,20 @@ async def get_top_level_tasks(db: AsyncSession, product_id: uuid.UUID) -> list[T
|
|||||||
|
|
||||||
|
|
||||||
async def create_task(db: AsyncSession, data: TaskCreate) -> TaskResponse:
|
async def create_task(db: AsyncSession, data: TaskCreate) -> TaskResponse:
|
||||||
"""创建任务"""
|
"""创建任务,并同步产品宏观状态"""
|
||||||
task = Task(**data.model_dump())
|
task = Task(**data.model_dump())
|
||||||
db.add(task)
|
db.add(task)
|
||||||
|
|
||||||
|
# 同步产品宏观状态 + 当前位置
|
||||||
|
product_result = await db.execute(select(Product).where(Product.id == data.product_id))
|
||||||
|
product = product_result.scalar_one_or_none()
|
||||||
|
if product:
|
||||||
|
if data.task_name:
|
||||||
|
product.overall_status = "在库" if "virtual_warehouse" in data.task_name else data.task_name
|
||||||
|
# 派发给人 → 产品离开仓库
|
||||||
|
if data.assignee_id and data.assignee_id != VIRTUAL_WAREHOUSE:
|
||||||
|
product.current_location_id = data.assignee_id
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(task)
|
await db.refresh(task)
|
||||||
return _to_response(task)
|
return _to_response(task)
|
||||||
@ -284,6 +295,12 @@ async def spawn_subtask(
|
|||||||
db.add(child)
|
db.add(child)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
|
|
||||||
|
# 同步产品宏观状态
|
||||||
|
product_result = await db.execute(select(Product).where(Product.id == task.product_id))
|
||||||
|
product = product_result.scalar_one_or_none()
|
||||||
|
if product and data.task_name:
|
||||||
|
product.overall_status = "在库" if "virtual_warehouse" in data.task_name else data.task_name
|
||||||
|
|
||||||
await _create_task_log(db, child.id, action_type="create", operator_id=operator_id,
|
await _create_task_log(db, child.id, action_type="create", operator_id=operator_id,
|
||||||
remark=f"协助分支(由「{task.task_name}」派发,分配给 {data.assignee_id})")
|
remark=f"协助分支(由「{task.task_name}」派发,分配给 {data.assignee_id})")
|
||||||
await db.commit()
|
await db.commit()
|
||||||
@ -318,13 +335,13 @@ ARCHIVED_STATUS = "ARCHIVED"
|
|||||||
|
|
||||||
async def receive_task(
|
async def receive_task(
|
||||||
db: AsyncSession, task_id: uuid.UUID, operator_id: str | None = None,
|
db: AsyncSession, task_id: uuid.UUID, operator_id: str | None = None,
|
||||||
remark: str | None = None,
|
remark: str | None = None, task_name: str | None = None,
|
||||||
) -> TaskResponse:
|
) -> TaskResponse:
|
||||||
"""
|
"""
|
||||||
操作员确认接收任务。
|
操作员确认接收任务。工人选定工序名称后接收。
|
||||||
|
|
||||||
校验:只有状态为 PENDING 的任务可接收。
|
校验:只有状态为 PENDING 的任务可接收。
|
||||||
动作:状态改为 WIP,记录 received_at,写入 remark。
|
动作:状态改为 WIP,记录 received_at,更新 task_name,同步产品宏观状态。
|
||||||
"""
|
"""
|
||||||
task = await _get_task_or_404(db, task_id)
|
task = await _get_task_or_404(db, task_id)
|
||||||
|
|
||||||
@ -347,6 +364,8 @@ async def receive_task(
|
|||||||
task.received_at = now
|
task.received_at = now
|
||||||
if remark:
|
if remark:
|
||||||
task.remark = remark
|
task.remark = remark
|
||||||
|
if task_name:
|
||||||
|
task.task_name = task_name
|
||||||
|
|
||||||
await _create_task_log(
|
await _create_task_log(
|
||||||
db, task_id,
|
db, task_id,
|
||||||
@ -355,6 +374,15 @@ async def receive_task(
|
|||||||
remark=remark or f"操作员确认接收任务「{task.task_name}」",
|
remark=remark or f"操作员确认接收任务「{task.task_name}」",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 接收时同步产品位置到接收人 + 宏观状态同步
|
||||||
|
product_result = await db.execute(select(Product).where(Product.id == task.product_id))
|
||||||
|
product = product_result.scalar_one_or_none()
|
||||||
|
if product:
|
||||||
|
if task.assignee_id:
|
||||||
|
product.current_location_id = task.assignee_id
|
||||||
|
if task_name:
|
||||||
|
product.overall_status = task_name
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(task)
|
await db.refresh(task)
|
||||||
|
|
||||||
@ -587,8 +615,10 @@ async def transfer_task(
|
|||||||
if product:
|
if product:
|
||||||
if has_warehouse and not real_branches:
|
if has_warehouse and not real_branches:
|
||||||
product.current_location_id = VIRTUAL_WAREHOUSE
|
product.current_location_id = VIRTUAL_WAREHOUSE
|
||||||
|
product.overall_status = "在库"
|
||||||
elif real_branches:
|
elif real_branches:
|
||||||
product.current_location_id = real_branches[0][1]
|
product.current_location_id = real_branches[0][1]
|
||||||
|
product.overall_status = real_branches[0][0]
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
@ -599,7 +629,7 @@ async def transfer_task(
|
|||||||
await db.refresh(nt)
|
await db.refresh(nt)
|
||||||
created_task_responses.append(_to_response(nt))
|
created_task_responses.append(_to_response(nt))
|
||||||
|
|
||||||
assignee_list = ", ".join(real_assignees)
|
assignee_list = ", ".join(a for _, a in real_branches) if real_branches else "仓库"
|
||||||
location_info = ""
|
location_info = ""
|
||||||
if has_warehouse:
|
if has_warehouse:
|
||||||
location_info = ",产品已入库(virtual_warehouse)"
|
location_info = ",产品已入库(virtual_warehouse)"
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
class="tree-movable-view"
|
class="tree-movable-view"
|
||||||
direction="all"
|
direction="all"
|
||||||
:x="0" :y="0"
|
:x="0" :y="0"
|
||||||
:scale="true" :scale-min="0.5" :scale-max="2"
|
:scale="true" :scale-min="0.3" :scale-max="2"
|
||||||
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
|
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
|
||||||
>
|
>
|
||||||
<view class="canvas-inner">
|
<view class="canvas-inner">
|
||||||
@ -93,7 +93,7 @@ export default {
|
|||||||
if (currentLevel.length > 0) levels.push(currentLevel);
|
if (currentLevel.length > 0) levels.push(currentLevel);
|
||||||
|
|
||||||
// 4. 计算 X, Y 坐标
|
// 4. 计算 X, Y 坐标
|
||||||
const CARD_W = 240, CARD_H = 140, GAP_X = 60, GAP_Y = 80;
|
const CARD_W = 320, CARD_H = 460, GAP_X = 80, GAP_Y = 120;
|
||||||
const nodes = [];
|
const nodes = [];
|
||||||
nodes._lines = [];
|
nodes._lines = [];
|
||||||
|
|
||||||
@ -165,26 +165,26 @@ export default {
|
|||||||
.zero-icon { font-size: 40px; margin-bottom: 10px; }
|
.zero-icon { font-size: 40px; margin-bottom: 10px; }
|
||||||
.zero-text { font-size: 14px; color: #9ca3af; }
|
.zero-text { font-size: 14px; color: #9ca3af; }
|
||||||
.canvas-hint { font-size: 11px; color: #9ca3af; text-align: center; padding: 8px 0; background: #fff; border-bottom: 1px solid #e5e7eb; flex-shrink: 0; }
|
.canvas-hint { font-size: 11px; color: #9ca3af; text-align: center; padding: 8px 0; background: #fff; border-bottom: 1px solid #e5e7eb; flex-shrink: 0; }
|
||||||
.tree-movable-area { flex: 1; width: 100%; }
|
.tree-movable-area { flex: 1; width: 100%; background-color: #f8fafc; background-image: linear-gradient(#e5e7eb 1px, transparent 1px), linear-gradient(90deg, #e5e7eb 1px, transparent 1px); background-size: 20px 20px; }
|
||||||
.canvas-inner { position: absolute; left: 0; top: 0; }
|
.canvas-inner { position: absolute; left: 0; top: 0; }
|
||||||
.canvas-node { position: absolute; width: 240px; min-height: 120px; background: #ffffff; border-radius: 12px; padding: 16px; box-shadow: 0 6px 16px rgba(0,0,0,0.06); border-left: 8px solid #3b82f6; border-top: none; z-index: 10; display: flex; flex-direction: column; gap: 8px; }
|
.canvas-node { position: absolute; width: 320px; min-height: 460px; background: #ffffff; border-radius: 20px; padding: 24px; box-shadow: 0 10px 30px rgba(0,0,0,0.10), 0 4px 8px rgba(0,0,0,0.06); border-left: 12px solid #3b82f6; z-index: 10; display: flex; flex-direction: column; gap: 10px; }
|
||||||
.canvas-node.s-yellow { border-left-color: #f59e0b; }
|
.canvas-node.s-yellow { border-left-color: #f59e0b; }
|
||||||
.canvas-node.s-blue { border-left-color: #3b82f6; }
|
.canvas-node.s-blue { border-left-color: #3b82f6; }
|
||||||
.canvas-node.s-green { border-left-color: #22c55e; }
|
.canvas-node.s-green { border-left-color: #22c55e; }
|
||||||
.canvas-node.s-red { border-left-color: #ef4444; }
|
.canvas-node.s-red { border-left-color: #ef4444; }
|
||||||
.cn-header { display: flex; align-items: center; justify-content: space-between; }
|
.cn-header { display: flex; align-items: center; justify-content: space-between; }
|
||||||
.cn-name { font-size: 16px; font-weight: 800; color: #1f2937; }
|
.cn-name { font-size: 24px; font-weight: 800; color: #1f2937; }
|
||||||
.cn-badge { font-size: 11px; padding: 2px 8px; border-radius: 8px; font-weight: 700; }
|
.cn-badge { font-size: 14px; padding: 4px 12px; border-radius: 8px; font-weight: 700; }
|
||||||
.cn-badge.s-yellow { background: #fef3c7; color: #b45309; }
|
.cn-badge.s-yellow { background: #fef3c7; color: #b45309; }
|
||||||
.cn-badge.s-blue { background: #dbeafe; color: #1d4ed8; }
|
.cn-badge.s-blue { background: #dbeafe; color: #1d4ed8; }
|
||||||
.cn-badge.s-green { background: #dcfce7; color: #15803d; }
|
.cn-badge.s-green { background: #dcfce7; color: #15803d; }
|
||||||
.cn-badge.s-red { background: #fce4ec; color: #be123c; }
|
.cn-badge.s-red { background: #fce4ec; color: #be123c; }
|
||||||
.cn-assignee { font-size: 13px; color: #6b7280; font-weight: 500; }
|
.cn-assignee { font-size: 16px; color: #6b7280; font-weight: 500; margin-top: 10px; }
|
||||||
.tag-rework { font-size: 11px; color: #fff; background: #ef4444; padding: 2px 6px; border-radius: 6px; display: inline-block; width: max-content; }
|
.tag-rework { font-size: 13px; color: #fff; background: #ef4444; padding: 4px 10px; border-radius: 6px; display: inline-block; width: max-content; }
|
||||||
.cn-remark { font-size: 12px; color: #a16207; background: #fefce8; padding: 6px 10px; border-radius: 6px; border: 1px solid #fef08a; line-height: 1.4; word-break: break-all; }
|
.cn-remark { font-size: 16px; color: #a16207; background: #fefce8; padding: 16px; border-radius: 8px; border: 1px solid #fef08a; line-height: 1.5; word-break: break-all; min-height: 100px; white-space: normal; margin-top: 16px; }
|
||||||
.cn-footer { display: flex; align-items: center; justify-content: space-between; margin-top: auto; padding-top: 10px; border-top: 1px dashed #e5e7eb; }
|
.cn-footer { display: flex; align-items: center; justify-content: space-between; margin-top: auto; padding-top: 14px; border-top: 1px dashed #e5e7eb; }
|
||||||
.cn-end { font-size: 11px; font-weight: 700; color: #16a34a; }
|
.cn-end { font-size: 13px; font-weight: 700; color: #16a34a; }
|
||||||
.cn-end-wh { color: #7c3aed; }
|
.cn-end-wh { color: #7c3aed; }
|
||||||
.cn-records { font-size: 12px; color: #2563eb; font-weight: 700; background: #eff6ff; padding: 2px 8px; border-radius: 12px; }
|
.cn-records { font-size: 14px; color: #2563eb; font-weight: 700; background: #eff6ff; padding: 4px 12px; border-radius: 12px; }
|
||||||
.canvas-line { position: absolute; height: 3px; background: #cbd5e1; z-index: 1; transform-origin: 0 0; }
|
.canvas-line { position: absolute; height: 3px; background: #94a3b8; z-index: 1; transform-origin: 0 0; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -46,6 +46,8 @@
|
|||||||
<view class="fc-header">
|
<view class="fc-header">
|
||||||
<text class="fc-status">{{ statusLabel(lockedTask.status) }}</text>
|
<text class="fc-status">{{ statusLabel(lockedTask.status) }}</text>
|
||||||
<text v-if="lockedTask.is_rework" class="tag tag-rework-sm">⚠返工</text>
|
<text v-if="lockedTask.is_rework" class="tag tag-rework-sm">⚠返工</text>
|
||||||
|
<text v-if="lockedTask.parent_task_id && lockedTask.status === 'WIP'" class="sub-branch-end"
|
||||||
|
@tap="$emit('action', { task: lockedTask, type: 'end' })">🛑 结束协助</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="fc-name">{{ lockedTask.task_name }}</text>
|
<text class="fc-name">{{ lockedTask.task_name }}</text>
|
||||||
|
|
||||||
@ -75,17 +77,15 @@
|
|||||||
|
|
||||||
<view class="footer-actions">
|
<view class="footer-actions">
|
||||||
<button v-if="lockedTask.status === 'WIP'" class="footer-btn footer-transfer"
|
<button v-if="lockedTask.status === 'WIP'" class="footer-btn footer-transfer"
|
||||||
@tap="$emit('action', { task: lockedTask, type: 'transfer' })">🔄 完工转交</button>
|
@tap="$emit('action', { task: lockedTask, type: 'transfer' })"><text class="btn-icon">🔄</text><text class="btn-txt">完工转交</text></button>
|
||||||
<button v-if="lockedTask.status === 'WIP'" class="footer-btn footer-record"
|
<button v-if="lockedTask.status === 'WIP'" class="footer-btn footer-record"
|
||||||
@tap="$emit('action', { task: lockedTask, type: 'record' })">📝 记录/拍照</button>
|
@tap="$emit('action', { task: lockedTask, type: 'record' })"><text class="btn-icon">📝</text><text class="btn-txt">记录拍照</text></button>
|
||||||
<button v-if="lockedTask.status === 'WIP'" class="footer-btn footer-spawn"
|
<button v-if="lockedTask.status === 'WIP'" class="footer-btn footer-spawn"
|
||||||
@tap="$emit('action', { task: lockedTask, type: 'spawn' })">➕ 派发协助分支</button>
|
@tap="$emit('action', { task: lockedTask, type: 'spawn' })"><text class="btn-icon">➕</text><text class="btn-txt">派发协助</text></button>
|
||||||
<button v-if="lockedTask.status === 'WIP'" class="footer-btn footer-end"
|
|
||||||
@tap="$emit('action', { task: lockedTask, type: 'end' })">🏁 结束分支</button>
|
|
||||||
<button v-if="lockedTask.status === 'PENDING'" class="footer-btn footer-receive"
|
<button v-if="lockedTask.status === 'PENDING'" class="footer-btn footer-receive"
|
||||||
@tap="$emit('action', { task: lockedTask, type: 'receive' })">✅ 接收任务</button>
|
@tap="$emit('action', { task: lockedTask, type: 'receive' })"><text class="btn-icon">✅</text><text class="btn-txt">接收任务</text></button>
|
||||||
<button v-if="lockedTask.status === 'PENDING'" class="footer-btn footer-reject"
|
<button v-if="lockedTask.status === 'PENDING'" class="footer-btn footer-reject"
|
||||||
@tap="$emit('action', { task: lockedTask, type: 'reject' })">❌ 驳回任务</button>
|
@tap="$emit('action', { task: lockedTask, type: 'reject' })"><text class="btn-icon">❌</text><text class="btn-txt">驳回任务</text></button>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
</view>
|
</view>
|
||||||
@ -172,6 +172,7 @@ export default {
|
|||||||
.s-yellow .fc-status { background: #fef3c7; color: #b45309; }
|
.s-yellow .fc-status { background: #fef3c7; color: #b45309; }
|
||||||
.s-blue .fc-status { background: #dbeafe; color: #1d4ed8; }
|
.s-blue .fc-status { background: #dbeafe; color: #1d4ed8; }
|
||||||
.tag-rework-sm { font-size: 10px; padding: 2px 6px; border-radius: 8px; background: #ef4444; color: #fff; }
|
.tag-rework-sm { font-size: 10px; padding: 2px 6px; border-radius: 8px; background: #ef4444; color: #fff; }
|
||||||
|
.sub-branch-end { font-size: 11px; color: #ef4444; font-weight: 600; margin-left: auto; padding: 2px 6px; }
|
||||||
.fc-name { font-size: 22px; font-weight: 800; color: #1f2937; display: block; margin-bottom: 10px; }
|
.fc-name { font-size: 22px; font-weight: 800; color: #1f2937; display: block; margin-bottom: 10px; }
|
||||||
.fc-link { padding: 6px 10px; border-radius: 8px; margin-bottom: 4px; font-size: 12px; margin-top: 10px; }
|
.fc-link { padding: 6px 10px; border-radius: 8px; margin-bottom: 4px; font-size: 12px; margin-top: 10px; }
|
||||||
.fc-up { background: #f0fdf4; color: #16a34a; }
|
.fc-up { background: #f0fdf4; color: #16a34a; }
|
||||||
@ -183,9 +184,11 @@ export default {
|
|||||||
.zero-task { display: flex; flex-direction: column; align-items: center; padding: 24px 0; }
|
.zero-task { display: flex; flex-direction: column; align-items: center; padding: 24px 0; }
|
||||||
.zero-icon { font-size: 40px; margin-bottom: 8px; }
|
.zero-icon { font-size: 40px; margin-bottom: 8px; }
|
||||||
.zero-text { font-size: 14px; color: #9ca3af; }
|
.zero-text { font-size: 14px; color: #9ca3af; }
|
||||||
.footer-actions { display: flex; gap: 20rpx; padding: 20rpx 30rpx; padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); background: #ffffff; box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.05); flex-shrink: 0; }
|
.footer-actions { display: flex; gap: 12rpx; padding: 16rpx 20rpx; padding-bottom: calc(16rpx + env(safe-area-inset-bottom)); background: #ffffff; box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.05); flex-shrink: 0; }
|
||||||
.footer-btn { flex: 1; height: 88rpx; border: none; border-radius: 12rpx; font-size: 30rpx; font-weight: 700; line-height: 88rpx; }
|
.footer-btn { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100rpx; border: none; border-radius: 16rpx; line-height: 1.2; padding: 0; }
|
||||||
.footer-btn::after { border: none; }
|
.footer-btn::after { border: none; }
|
||||||
|
.btn-icon { font-size: 36rpx; margin-bottom: 6rpx; }
|
||||||
|
.btn-txt { font-size: 24rpx; font-weight: 700; }
|
||||||
.footer-transfer { background: #dcfce7; color: #16a34a; }
|
.footer-transfer { background: #dcfce7; color: #16a34a; }
|
||||||
.footer-record { background: #eff6ff; color: #2563eb; }
|
.footer-record { background: #eff6ff; color: #2563eb; }
|
||||||
.footer-receive { background: #dbeafe; color: #1d4ed8; }
|
.footer-receive { background: #dbeafe; color: #1d4ed8; }
|
||||||
|
|||||||
@ -4,10 +4,10 @@
|
|||||||
<view v-if="error" class="error-box">{{ error }}</view>
|
<view v-if="error" class="error-box">{{ error }}</view>
|
||||||
|
|
||||||
<template v-if="product && !loading">
|
<template v-if="product && !loading">
|
||||||
<view class="overall-bar" @tap="showStatusPicker = true">
|
<view class="overall-bar" @tap="handleOverallBarClick">
|
||||||
<text class="overall-label">宏观状态</text>
|
<text class="overall-label">宏观状态</text>
|
||||||
<text :class="['overall-val', product.overall_status ? '' : 'overall-empty']">{{ product.overall_status || '点击设定' }}</text>
|
<text :class="['overall-val', product.overall_status ? '' : 'overall-empty']">{{ product.overall_status || '未激活 — 点击发起首道工序' }}</text>
|
||||||
<text class="overall-arrow">▾</text>
|
<text v-if="product.task_tree && product.task_tree.length" class="overall-arrow">▾</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="card">
|
<view class="card">
|
||||||
@ -20,7 +20,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="info-grid">
|
<view class="info-grid">
|
||||||
<view class="info-item"><text class="label">序列号</text><text class="value sn">{{ product.serial_number }}</text></view>
|
<view class="info-item"><text class="label">身份证</text><text class="value sn">{{ product.serial_number }}</text></view>
|
||||||
<view class="info-item"><text class="label">物料名称</text><text class="value">{{ product.material_name || product.material_id || '—' }}</text></view>
|
<view class="info-item"><text class="label">物料名称</text><text class="value">{{ product.material_name || product.material_id || '—' }}</text></view>
|
||||||
<view class="info-item"><text class="label">规格型号</text><text class="value">{{ product.spec_model || '—' }}</text></view>
|
<view class="info-item"><text class="label">规格型号</text><text class="value">{{ product.spec_model || '—' }}</text></view>
|
||||||
<view class="info-item"><text class="label">订单编号</text><text class="value">{{ product.order_no || '—' }}</text></view>
|
<view class="info-item"><text class="label">订单编号</text><text class="value">{{ product.order_no || '—' }}</text></view>
|
||||||
@ -31,6 +31,13 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 📤 仓库转出横幅(当前用户有活跃任务时隐藏) -->
|
||||||
|
<view v-if="product && product.current_location_id === 'virtual_warehouse' && !hasMyActiveTask"
|
||||||
|
class="warehouse-transfer-banner" @tap="openWarehouseTransfer">
|
||||||
|
<text class="wt-icon">📤</text>
|
||||||
|
<text class="wt-text">该产品在仓库中 — 点击此处转出并派发给指定人员</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
<WorkspaceArea v-if="currentMode === 'workspace'" :product="product"
|
<WorkspaceArea v-if="currentMode === 'workspace'" :product="product"
|
||||||
:currentUserId="currentUserId" :currentUsername="currentUsername"
|
:currentUserId="currentUserId" :currentUsername="currentUsername"
|
||||||
@action="handleTaskAction" @viewRecords="handleViewRecords" />
|
@action="handleTaskAction" @viewRecords="handleViewRecords" />
|
||||||
@ -53,23 +60,26 @@
|
|||||||
<view v-if="editProductVisible" class="overlay" @tap="editProductVisible = false">
|
<view v-if="editProductVisible" class="overlay" @tap="editProductVisible = false">
|
||||||
<view class="popup" @tap.stop>
|
<view class="popup" @tap.stop>
|
||||||
<text class="popup-title">编辑产品</text>
|
<text class="popup-title">编辑产品</text>
|
||||||
<input v-model="editForm.order_no" class="popup-input" placeholder="订单编号" />
|
<view class="field-label">订单编号</view>
|
||||||
<input v-model="editForm.external_serial" class="popup-input" placeholder="外部序列号" />
|
<input v-model="editForm.order_no" class="popup-input" placeholder="请输入订单编号" />
|
||||||
|
<view class="field-label" style="margin-top:10px;">产品序列号</view>
|
||||||
|
<input v-model="editForm.external_serial" class="popup-input" placeholder="请输入产品序列号" />
|
||||||
<view class="popup-btns"><button class="btn-cancel" @tap="editProductVisible = false">取消</button><button class="btn-primary" :disabled="editSaving" @tap="doEditProduct">{{ editSaving ? '保存中...' : '保存' }}</button></view>
|
<view class="popup-btns"><button class="btn-cancel" @tap="editProductVisible = false">取消</button><button class="btn-primary" :disabled="editSaving" @tap="doEditProduct">{{ editSaving ? '保存中...' : '保存' }}</button></view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<!-- 发起首道工序 -->
|
<!-- 发起首道工序 (只选人+填备注) -->
|
||||||
<view v-if="createFirstVisible" class="overlay" @tap="createFirstVisible = false">
|
<view v-if="createFirstVisible" class="overlay" @tap="createFirstVisible = false">
|
||||||
<view class="popup" @tap.stop>
|
<view class="popup" @tap.stop>
|
||||||
<text class="popup-title">🚀 发起首道工序</text>
|
<text class="popup-title">{{ isWarehouseTransfer ? '📤 仓库转出派发' : '🚀 发起首道工序' }}</text>
|
||||||
<view class="field-label">工序名称 <text class="required">*</text></view>
|
|
||||||
<picker :range="TASK_NAME_OPTIONS" :value="firstForm.taskNameIdx" @change="onTaskNameChange"><view class="picker-box">{{ firstForm.task_name || '请选择工序名称' }}</view></picker>
|
|
||||||
<view class="field-label">接收人 <text class="required">*</text></view>
|
<view class="field-label">接收人 <text class="required">*</text></view>
|
||||||
<picker :range="userLabels" :value="firstForm.assigneeIdx" @change="onAssigneeChange"><view class="picker-box">{{ firstForm.assigneeLabel || '请选择接收人' }}</view></picker>
|
<view class="user-grid">
|
||||||
<view class="field-label">备注 <text class="required">*</text></view>
|
<view v-for="u in userGridOptions" :key="u.id"
|
||||||
|
:class="['user-grid-item', firstForm.assignee_id === u.id ? 'user-grid-active' : '']"
|
||||||
|
@tap="firstForm.assignee_id = u.id; firstForm.assigneeLabel = u.name">{{ formatName(u.name) }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="field-label" style="margin-top:12px;">备注 <text class="required">*</text></view>
|
||||||
<textarea v-model="firstForm.note" class="popup-textarea" placeholder="请填写备注说明(必填)" :maxlength="500" />
|
<textarea v-model="firstForm.note" class="popup-textarea" placeholder="请填写备注说明(必填)" :maxlength="500" />
|
||||||
<label class="switch-row" @tap="firstForm.autoReceive = !firstForm.autoReceive"><text class="switch-label">⚡ 立即接收并开始计时</text><switch :checked="firstForm.autoReceive" color="#2563EB" style="transform:scale(0.8)" /></label>
|
<view class="popup-btns"><button class="btn-cancel" @tap="createFirstVisible = false">取消</button><button class="btn-primary" :disabled="firstSaving || !firstForm.assignee_id || !firstForm.note.trim()" @tap="doCreateFirstTask">{{ firstSaving ? '创建中...' : '确认创建' }}</button></view>
|
||||||
<view class="popup-btns"><button class="btn-cancel" @tap="createFirstVisible = false">取消</button><button class="btn-primary" :disabled="firstSaving || !firstForm.task_name || !firstForm.assignee_id || !firstForm.note.trim()" @tap="doCreateFirstTask">{{ firstSaving ? '创建中...' : (firstForm.autoReceive ? '创建并接收' : '确认创建') }}</button></view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<!-- 记录/拍照 -->
|
<!-- 记录/拍照 -->
|
||||||
@ -93,8 +103,14 @@
|
|||||||
<text class="popup-title">确认接收任务</text>
|
<text class="popup-title">确认接收任务</text>
|
||||||
<view class="popup-task">{{ actionPopup.task && actionPopup.task.task_name }}</view>
|
<view class="popup-task">{{ actionPopup.task && actionPopup.task.task_name }}</view>
|
||||||
<text class="popup-hint">状态: {{ statusLabel(actionPopup.task && actionPopup.task.status) }} → 进行中</text>
|
<text class="popup-hint">状态: {{ statusLabel(actionPopup.task && actionPopup.task.status) }} → 进行中</text>
|
||||||
<textarea v-model="receiveRemark" class="popup-textarea" placeholder="接收备注(选填)" :maxlength="500" />
|
<view class="field-label">选择工序 <text class="required">*</text></view>
|
||||||
<view class="popup-btns"><button class="btn-cancel" @tap="closeActionPopup">取消</button><button class="btn-primary" :disabled="actionLoading" @tap="doReceive">确认接收</button></view>
|
<view class="user-grid">
|
||||||
|
<view v-for="opt in TASK_NAME_OPTIONS" :key="opt"
|
||||||
|
:class="['user-grid-item', receiveTaskName === opt ? 'user-grid-active' : '']"
|
||||||
|
@tap="receiveTaskName = opt">{{ opt }}</view>
|
||||||
|
</view>
|
||||||
|
<textarea v-model="receiveRemark" class="popup-textarea" placeholder="接收备注(选填)" :maxlength="500" style="margin-top:12px;" />
|
||||||
|
<view class="popup-btns"><button class="btn-cancel" @tap="closeActionPopup">取消</button><button class="btn-primary" :disabled="actionLoading || !receiveTaskName" @tap="doReceive">确认接收</button></view>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="actionPopup.type === 'reject'">
|
<template v-if="actionPopup.type === 'reject'">
|
||||||
<text class="popup-title">品质驳回</text>
|
<text class="popup-title">品质驳回</text>
|
||||||
@ -104,20 +120,29 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-if="actionPopup.type === 'transfer'">
|
<template v-if="actionPopup.type === 'transfer'">
|
||||||
<text class="popup-title">完工转交</text>
|
<text class="popup-title">完工转交</text>
|
||||||
<view class="form-item"><text class="form-label">下一道工序 <text class="required">*</text></text><picker mode="selector" :range="processOptions" @change="onTransferProcessChange"><view class="picker-value"><text :class="transferForm.next_task_name ? '' : 'picker-placeholder'">{{ transferForm.next_task_name || '请选择工序' }}</text><text class="picker-arrow">▾</text></view></picker></view>
|
<view class="field-label">接收人 <text class="required">*</text></view>
|
||||||
<view v-if="!transferForm.isWarehouse" class="form-item"><text class="form-label">接收人 <text class="required">*</text></text><picker mode="selector" :range="userOptions" range-key="name" @change="onTransferUserChange"><view class="picker-value"><text :class="transferUserName ? '' : 'picker-placeholder'">{{ transferUserName || '请选择接收人' }}</text><text class="picker-arrow">▾</text></view></picker></view>
|
<view class="user-grid">
|
||||||
<view v-if="transferForm.isWarehouse" class="warehouse-hint">📦 入库 — 直接归档,无需指定接收人</view>
|
<view v-for="u in userGridOptions" :key="u.id"
|
||||||
|
:class="['user-grid-item', transferForm.selectedUserId === u.id ? 'user-grid-active' : '']"
|
||||||
|
@tap="transferForm.selectedUserId = u.id">{{ formatName(u.name) }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="field-label" style="margin-top:12px;">或</view>
|
||||||
|
<view :class="['user-grid-item', transferForm.isWarehouse ? 'user-grid-active' : '']" style="width:100%;" @tap="transferForm.isWarehouse = !transferForm.isWarehouse; transferForm.selectedUserId = '';">📦 入库 (virtual_warehouse)</view>
|
||||||
<input v-model="transferForm.note" class="popup-input" placeholder="交接备注(选填)" style="margin-top:12px;" />
|
<input v-model="transferForm.note" class="popup-input" placeholder="交接备注(选填)" style="margin-top:12px;" />
|
||||||
<view v-if="transferForm.next_task_name" class="preview-hint">{{ transferForm.isWarehouse ? '产品将入库并从个人待办中移除' : '将创建 1 个「' + transferForm.next_task_name + '」任务指派给 ' + (transferUserName || '—') }}</view>
|
<view v-if="transferForm.isWarehouse || transferForm.selectedUserId" class="preview-hint">{{ transferForm.isWarehouse ? '产品将入库并从个人待办中移除' : '将创建新任务指派给 ' + (transferUserName || '—') }}</view>
|
||||||
<view class="popup-btns"><button class="btn-cancel" @tap="closeActionPopup">取消</button><button class="btn-primary" :disabled="actionLoading || !transferForm.next_task_name || (!transferForm.isWarehouse && !transferForm.selectedUserId)" @tap="doTransfer">{{ actionLoading ? '提交中...' : (transferForm.isWarehouse ? '📦 确认入库' : '确认转交') }}</button></view>
|
<view class="popup-btns"><button class="btn-cancel" @tap="closeActionPopup">取消</button><button class="btn-primary" :disabled="actionLoading || (!transferForm.isWarehouse && !transferForm.selectedUserId)" @tap="doTransfer">{{ actionLoading ? '提交中...' : (transferForm.isWarehouse ? '📦 确认入库' : '确认转交') }}</button></view>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="actionPopup.type === 'spawn'">
|
<template v-if="actionPopup.type === 'spawn'">
|
||||||
<text class="popup-title">➕ 派发协助分支</text>
|
<text class="popup-title">➕ 派发协助分支</text>
|
||||||
<text class="popup-hint">为当前任务创建一个并行协助任务,当前任务保持进行中</text>
|
<text class="popup-hint">为当前任务创建并行协助,当前任务保持进行中</text>
|
||||||
<view class="form-item"><text class="form-label">工序名称 <text class="required">*</text></text><picker mode="selector" :range="spawnProcessOptions" @change="onSpawnProcessChange"><view class="picker-value"><text :class="spawnForm.task_name ? '' : 'picker-placeholder'">{{ spawnForm.task_name || '请选择工序' }}</text><text class="picker-arrow">▾</text></view></picker></view>
|
<view class="field-label">接收人 <text class="required">*</text></view>
|
||||||
<view class="form-item"><text class="form-label">接收人 <text class="required">*</text></text><picker mode="selector" :range="userOptions" range-key="name" @change="onSpawnUserChange"><view class="picker-value"><text :class="spawnUserName ? '' : 'picker-placeholder'">{{ spawnUserName || '请选择接收人' }}</text><text class="picker-arrow">▾</text></view></picker></view>
|
<view class="user-grid">
|
||||||
<input v-model="spawnForm.remark" class="popup-input" placeholder="派发备注(选填)" />
|
<view v-for="u in userGridOptions" :key="u.id"
|
||||||
<view class="popup-btns"><button class="btn-cancel" @tap="closeActionPopup">取消</button><button class="btn-primary btn-spawn" :disabled="actionLoading || !spawnForm.task_name || !spawnForm.assignee_id" @tap="doSpawn">{{ actionLoading ? '提交中...' : '确认派发' }}</button></view>
|
:class="['user-grid-item', spawnForm.assignee_id === u.id ? 'user-grid-active' : '']"
|
||||||
|
@tap="spawnForm.assignee_id = u.id">{{ formatName(u.name) }}</view>
|
||||||
|
</view>
|
||||||
|
<input v-model="spawnForm.remark" class="popup-input" placeholder="派发备注(选填)" style="margin-top:12px;" />
|
||||||
|
<view class="popup-btns"><button class="btn-cancel" @tap="closeActionPopup">取消</button><button class="btn-primary btn-spawn" :disabled="actionLoading || !spawnForm.assignee_id" @tap="doSpawn">{{ actionLoading ? '提交中...' : '确认派发' }}</button></view>
|
||||||
</template>
|
</template>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@ -140,13 +165,13 @@ export default {
|
|||||||
OVERALL_OPTIONS, loading: true, error: "", product: null,
|
OVERALL_OPTIONS, loading: true, error: "", product: null,
|
||||||
showStatusPicker: false, editProductVisible: false, editForm: { order_no: "", external_serial: "" }, editSaving: false,
|
showStatusPicker: false, editProductVisible: false, editForm: { order_no: "", external_serial: "" }, editSaving: false,
|
||||||
users: [], TASK_NAME_OPTIONS,
|
users: [], TASK_NAME_OPTIONS,
|
||||||
createFirstVisible: false, firstForm: { task_name: "", taskNameIdx: 0, assignee_id: "", assigneeLabel: "", assigneeIdx: 0, note: "", autoReceive: true }, firstSaving: false,
|
createFirstVisible: false, isWarehouseTransfer: false, firstForm: { task_name: "", taskNameIdx: 0, assignee_id: "", assigneeLabel: "", assigneeIdx: 0, note: "" }, firstSaving: false,
|
||||||
recordPopup: { visible: false, task: null }, recordForm: { recordId: null, remark: "", images: [], pendingCount: 0 }, recordSaving: false, isUploading: false,
|
recordPopup: { visible: false, task: null }, recordForm: { recordId: null, remark: "", images: [], pendingCount: 0 }, recordSaving: false, isUploading: false,
|
||||||
currentUser: null, currentUserId: "", currentUsername: "", currentMode: "tree",
|
currentUser: null, currentUserId: "", currentUsername: "", currentMode: "tree",
|
||||||
processOptions: [], userOptions: [],
|
processOptions: [], userOptions: [],
|
||||||
actionPopup: { visible: false, type: "", task: null }, actionLoading: false, rejectReason: "", receiveRemark: "",
|
actionPopup: { visible: false, type: "", task: null }, actionLoading: false, rejectReason: "", receiveRemark: "", receiveTaskName: "",
|
||||||
transferForm: { next_task_name: "", selectedUserId: "", isWarehouse: false, note: "" },
|
transferForm: { selectedUserId: "", isWarehouse: false, note: "" },
|
||||||
spawnForm: { task_name: "", assignee_id: "", remark: "" },
|
spawnForm: { assignee_id: "", remark: "" },
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -154,7 +179,7 @@ export default {
|
|||||||
canDeleteImage() { if (!this.recordPopup.task) return true; if (!this.currentUser) return true; return this.currentUser.username === this.recordPopup.task.assignee_id; },
|
canDeleteImage() { if (!this.recordPopup.task) return true; if (!this.currentUser) return true; return this.currentUser.username === this.recordPopup.task.assignee_id; },
|
||||||
transferUserName() { const u = this.userOptions.find(u => u.id === this.transferForm.selectedUserId); return u ? u.name : ""; },
|
transferUserName() { const u = this.userOptions.find(u => u.id === this.transferForm.selectedUserId); return u ? u.name : ""; },
|
||||||
spawnUserName() { const u = this.userOptions.find(u => u.id === this.spawnForm.assignee_id); return u ? u.name : ""; },
|
spawnUserName() { const u = this.userOptions.find(u => u.id === this.spawnForm.assignee_id); return u ? u.name : ""; },
|
||||||
spawnProcessOptions() { return (this.processOptions || []).filter(o => o !== "🏭 入库 (virtual_warehouse)"); },
|
userGridOptions() { return (this.userOptions || []).map(u => ({ id: u.id, name: u.name })); },
|
||||||
hasMyActiveTask() {
|
hasMyActiveTask() {
|
||||||
const find = (tasks) => { if (!tasks) return false; for (const t of tasks) { if ((t.status === 'WIP' || t.status === 'PENDING') && (t.assignee_id == this.currentUserId || t.assignee_id == this.currentUsername)) return true; if (find(t.child_tasks)) return true; } return false; };
|
const find = (tasks) => { if (!tasks) return false; for (const t of tasks) { if ((t.status === 'WIP' || t.status === 'PENDING') && (t.assignee_id == this.currentUserId || t.assignee_id == this.currentUsername)) return true; if (find(t.child_tasks)) return true; } return false; };
|
||||||
return this.product ? find(this.product.task_tree) : false;
|
return this.product ? find(this.product.task_tree) : false;
|
||||||
@ -162,10 +187,11 @@ export default {
|
|||||||
},
|
},
|
||||||
onLoad(options) { this.loadUsers(); this.loadCurrentUser(); const sn = options.serial || ""; if (sn) this.doQuery(sn); },
|
onLoad(options) { this.loadUsers(); this.loadCurrentUser(); const sn = options.serial || ""; if (sn) this.doQuery(sn); },
|
||||||
methods: {
|
methods: {
|
||||||
|
formatName(name) { if (!name) return ""; return name.length === 2 ? name[0] + " " + name[1] : name; },
|
||||||
statusLabel(s) { return STATUS_MAP[s] || s; },
|
statusLabel(s) { return STATUS_MAP[s] || s; },
|
||||||
statusColor(s) { switch (s) { case "PENDING": return "s-yellow"; case "WIP": return "s-blue"; case "COMPLETED": return "s-green"; case "REJECTED": return "s-red"; default: return "s-gray"; } },
|
statusColor(s) { switch (s) { case "PENDING": return "s-yellow"; case "WIP": return "s-blue"; case "COMPLETED": return "s-green"; case "REJECTED": return "s-red"; default: return "s-gray"; } },
|
||||||
|
|
||||||
async doQuery(sn) { this.loading = true; this.error = ""; try { this.product = await get(`/products/scan/${sn}`); if (!this.product.overall_status) this.showStatusPicker = true; this.$nextTick(() => { this.currentMode = this.hasMyActiveTask ? 'workspace' : 'tree'; }); } catch (e) { this.error = e?.data?.detail || "查询失败"; } finally { this.loading = false; } },
|
async doQuery(sn) { this.loading = true; this.error = ""; try { this.product = await get(`/products/scan/${sn}`); this.$nextTick(() => { this.currentMode = this.hasMyActiveTask ? 'workspace' : 'tree'; if (!this.product.task_tree || !this.product.task_tree.length) { this.openCreateFirstTask(); } }); } catch (e) { this.error = e?.data?.detail || "查询失败"; } finally { this.loading = false; } },
|
||||||
toggleMode() { this.currentMode = this.currentMode === 'workspace' ? 'tree' : 'workspace'; },
|
toggleMode() { this.currentMode = this.currentMode === 'workspace' ? 'tree' : 'workspace'; },
|
||||||
|
|
||||||
async handleSetOverallStatus(status) { try { this.product = await patch(`/products/scan/${this.product.serial_number}/status`, { status }); uni.showToast({ title: `状态已更新: ${status}`, icon: "success" }); this.showStatusPicker = false; } catch {} },
|
async handleSetOverallStatus(status) { try { this.product = await patch(`/products/scan/${this.product.serial_number}/status`, { status }); uni.showToast({ title: `状态已更新: ${status}`, icon: "success" }); this.showStatusPicker = false; } catch {} },
|
||||||
@ -177,8 +203,10 @@ export default {
|
|||||||
|
|
||||||
onTaskNameChange(e) { const idx = e.detail.value; this.firstForm.taskNameIdx = idx; this.firstForm.task_name = TASK_NAME_OPTIONS[idx]; },
|
onTaskNameChange(e) { const idx = e.detail.value; this.firstForm.taskNameIdx = idx; this.firstForm.task_name = TASK_NAME_OPTIONS[idx]; },
|
||||||
onAssigneeChange(e) { const idx = e.detail.value; const u = this.users[idx]; if (u) { this.firstForm.assigneeIdx = idx; this.firstForm.assignee_id = u.username; this.firstForm.assigneeLabel = `${u.full_name} (${u.username})`; } },
|
onAssigneeChange(e) { const idx = e.detail.value; const u = this.users[idx]; if (u) { this.firstForm.assigneeIdx = idx; this.firstForm.assignee_id = u.username; this.firstForm.assigneeLabel = `${u.full_name} (${u.username})`; } },
|
||||||
openCreateFirstTask() { this.firstForm = { task_name: TASK_NAME_OPTIONS[0], taskNameIdx: 0, assignee_id: this.users.length > 0 ? this.users[0].username : "", assigneeLabel: this.users.length > 0 ? `${this.users[0].full_name} (${this.users[0].username})` : "", assigneeIdx: 0, note: "", autoReceive: true }; this.createFirstVisible = true; },
|
openCreateFirstTask() { this.isWarehouseTransfer = false; if (this.currentMode === 'tree') this.currentMode = 'workspace'; this.firstForm = { assignee_id: "", assigneeLabel: "", note: "" }; this.createFirstVisible = true; },
|
||||||
async doCreateFirstTask() { this.firstSaving = true; try { const task = await post("/tasks/", { product_id: this.product.id, task_name: this.firstForm.task_name, assignee_id: this.firstForm.assignee_id, notify_parent_on_complete: false, remark: this.firstForm.note.trim() || undefined }); if (this.firstForm.autoReceive) { try { const opId = this.currentUsername || this.currentUserId; await post(`/tasks/${task.id}/receive?operator_id=${encodeURIComponent(opId)}`, { remark: this.firstForm.note.trim() || undefined }); } catch {} } uni.showToast({ title: this.firstForm.autoReceive ? "已创建并接收" : "已创建", icon: "success" }); this.createFirstVisible = false; this.doQuery(this.product.serial_number); } catch {} finally { this.firstSaving = false; } },
|
handleOverallBarClick() { if (!this.product.task_tree || !this.product.task_tree.length) { this.openCreateFirstTask(); } else { this.showStatusPicker = true; } },
|
||||||
|
openWarehouseTransfer() { this.isWarehouseTransfer = true; this.openCreateFirstTask(); },
|
||||||
|
async doCreateFirstTask() { this.firstSaving = true; try { await post("/tasks/", { product_id: this.product.id, task_name: "待确认", assignee_id: this.firstForm.assignee_id, notify_parent_on_complete: false, remark: this.firstForm.note.trim() || undefined }); if (this.product.current_location_id === 'virtual_warehouse') { try { await patch(`/products/${this.product.id}`, { current_location_id: this.firstForm.assignee_id }); } catch {} } uni.showToast({ title: "任务已派发,待接收", icon: "success" }); this.createFirstVisible = false; this.doQuery(this.product.serial_number); } catch {} finally { this.firstSaving = false; } },
|
||||||
|
|
||||||
openRecordPopup(task) { this.recordPopup = { visible: true, task }; this.recordForm = { recordId: null, remark: "", images: [], pendingCount: 0 }; this.isUploading = false; },
|
openRecordPopup(task) { this.recordPopup = { visible: true, task }; this.recordForm = { recordId: null, remark: "", images: [], pendingCount: 0 }; this.isUploading = false; },
|
||||||
openEditRecord({ task, record }) { this.recordPopup = { visible: true, task }; this.recordForm = { recordId: record.id, remark: record.remark || "", images: record.images || [], pendingCount: 0 }; this.isUploading = false; },
|
openEditRecord({ task, record }) { this.recordPopup = { visible: true, task }; this.recordForm = { recordId: record.id, remark: record.remark || "", images: record.images || [], pendingCount: 0 }; this.isUploading = false; },
|
||||||
@ -194,25 +222,21 @@ export default {
|
|||||||
if (type === "deleteRecord") { this.doDeleteRecord(record); return; }
|
if (type === "deleteRecord") { this.doDeleteRecord(record); return; }
|
||||||
if (type === "end") { this.confirmEndBranch(task); return; }
|
if (type === "end") { this.confirmEndBranch(task); return; }
|
||||||
if (type === "transfer" || type === "spawn") { uni.showLoading({ title: "加载数据..." }); try { if (!this.userOptions.length) await this.loadUsers(); if (!this.processOptions.length) this.processOptions = ["🏭 入库 (virtual_warehouse)", ...TASK_NAME_OPTIONS]; } finally { uni.hideLoading(); } }
|
if (type === "transfer" || type === "spawn") { uni.showLoading({ title: "加载数据..." }); try { if (!this.userOptions.length) await this.loadUsers(); if (!this.processOptions.length) this.processOptions = ["🏭 入库 (virtual_warehouse)", ...TASK_NAME_OPTIONS]; } finally { uni.hideLoading(); } }
|
||||||
this.actionPopup = { visible: true, type, task }; this.rejectReason = ""; this.receiveRemark = "";
|
this.actionPopup = { visible: true, type, task }; this.rejectReason = ""; this.receiveRemark = ""; this.receiveTaskName = "";
|
||||||
this.transferForm = { next_task_name: "", selectedUserId: "", isWarehouse: false, note: "" };
|
this.transferForm = { selectedUserId: "", isWarehouse: false, note: "" };
|
||||||
this.spawnForm = { task_name: "", assignee_id: "", remark: "" };
|
this.spawnForm = { assignee_id: "", remark: "" };
|
||||||
},
|
},
|
||||||
handleViewRecords(task) { uni.navigateTo({ url: `/pages/scan/records?taskId=${task.id}` }); },
|
handleViewRecords(task) { uni.navigateTo({ url: `/pages/scan/records?taskId=${task.id}` }); },
|
||||||
async doDeleteRecord(record) { try { await request({ url: `/records/${record.id}`, method: "DELETE" }); uni.showToast({ title: "记录已删除", icon: "success" }); this.doQuery(this.product.serial_number); } catch {} },
|
async doDeleteRecord(record) { try { await request({ url: `/records/${record.id}`, method: "DELETE" }); uni.showToast({ title: "记录已删除", icon: "success" }); this.doQuery(this.product.serial_number); } catch {} },
|
||||||
confirmEndBranch(task) { uni.showModal({ title: "结束分支", content: `确定结束「${task.task_name}」吗?`, success: (res) => { if (res.confirm) this.doEndBranch(task); } }); },
|
confirmEndBranch(task) { uni.showModal({ title: "结束分支", content: `确定结束「${task.task_name}」吗?`, success: (res) => { if (res.confirm) this.doEndBranch(task); } }); },
|
||||||
async doEndBranch(task) { try { const opId = this.currentUsername || this.currentUserId; await post(`/tasks/${task.id}/end?operator_id=${encodeURIComponent(opId)}`); uni.showToast({ title: "分支已结束", icon: "success" }); this.doQuery(this.product.serial_number); } catch {} },
|
async doEndBranch(task) { try { const opId = this.currentUsername || this.currentUserId; await post(`/tasks/${task.id}/end?operator_id=${encodeURIComponent(opId)}`); uni.showToast({ title: "分支已结束", icon: "success" }); this.doQuery(this.product.serial_number); } catch {} },
|
||||||
closeActionPopup() { this.actionPopup = { visible: false, type: "", task: null }; },
|
closeActionPopup() { this.actionPopup = { visible: false, type: "", task: null }; },
|
||||||
async doReceive() { this.actionLoading = true; try { const remark = this.receiveRemark.trim() || undefined; const opId = this.currentUsername || this.currentUserId; await post(`/tasks/${this.actionPopup.task.id}/receive?operator_id=${encodeURIComponent(opId)}`, { remark }); uni.showToast({ title: "已接收", icon: "success" }); this.closeActionPopup(); this.doQuery(this.product.serial_number); } catch {} finally { this.actionLoading = false; } },
|
async doReceive() { this.actionLoading = true; try { const remark = this.receiveRemark.trim() || undefined; const opId = this.currentUsername || this.currentUserId; await post(`/tasks/${this.actionPopup.task.id}/receive?operator_id=${encodeURIComponent(opId)}`, { remark, task_name: this.receiveTaskName }); uni.showToast({ title: "已接收", icon: "success" }); this.closeActionPopup(); this.doQuery(this.product.serial_number); } catch {} finally { this.actionLoading = false; } },
|
||||||
async doReject() { this.actionLoading = true; try { const opId = this.currentUsername || this.currentUserId; await post(`/tasks/${this.actionPopup.task.id}/reject?operator_id=${encodeURIComponent(opId)}`, { reason: this.rejectReason.trim() }); uni.showToast({ title: "已驳回", icon: "success" }); this.closeActionPopup(); this.doQuery(this.product.serial_number); } catch {} finally { this.actionLoading = false; } },
|
async doReject() { this.actionLoading = true; try { const opId = this.currentUsername || this.currentUserId; await post(`/tasks/${this.actionPopup.task.id}/reject?operator_id=${encodeURIComponent(opId)}`, { reason: this.rejectReason.trim() }); uni.showToast({ title: "已驳回", icon: "success" }); this.closeActionPopup(); this.doQuery(this.product.serial_number); } catch {} finally { this.actionLoading = false; } },
|
||||||
// 转交(单线)
|
// 转交(单线)
|
||||||
onTransferProcessChange(e) { this.transferForm.next_task_name = this.processOptions[e.detail.value]; this.transferForm.isWarehouse = this.transferForm.next_task_name === "🏭 入库 (virtual_warehouse)"; this.transferForm.selectedUserId = ""; },
|
async doTransfer() { this.actionLoading = true; try { const assignees = this.transferForm.isWarehouse ? ["virtual_warehouse"] : [this.transferForm.selectedUserId]; const taskName = this.transferForm.isWarehouse ? "🏭 入库 (virtual_warehouse)" : "待确认"; await post(`/tasks/${this.actionPopup.task.id}/transfer`, { next_tasks: [{ task_name: taskName, assignees }], note: this.transferForm.note.trim() || undefined }); uni.showToast({ title: this.transferForm.isWarehouse ? "已入库" : "转交成功", icon: "success" }); this.closeActionPopup(); this.doQuery(this.product.serial_number); } catch {} finally { this.actionLoading = false; } },
|
||||||
onTransferUserChange(e) { const user = this.userOptions[e.detail.value]; if (user) this.transferForm.selectedUserId = user.id; },
|
|
||||||
async doTransfer() { this.actionLoading = true; try { const assignees = this.transferForm.isWarehouse ? ["virtual_warehouse"] : [this.transferForm.selectedUserId]; await post(`/tasks/${this.actionPopup.task.id}/transfer`, { next_tasks: [{ task_name: this.transferForm.next_task_name, assignees }], note: this.transferForm.note.trim() || undefined }); uni.showToast({ title: this.transferForm.isWarehouse ? "已入库" : "转交成功", icon: "success" }); this.closeActionPopup(); this.doQuery(this.product.serial_number); } catch {} finally { this.actionLoading = false; } },
|
|
||||||
// 派发协助分支
|
// 派发协助分支
|
||||||
onSpawnProcessChange(e) { this.spawnForm.task_name = this.spawnProcessOptions[e.detail.value]; },
|
async doSpawn() { this.actionLoading = true; try { const opId = this.currentUsername || this.currentUserId; await post(`/tasks/${this.actionPopup.task.id}/spawn?operator_id=${encodeURIComponent(opId)}`, { task_name: "待确认", assignee_id: this.spawnForm.assignee_id, remark: this.spawnForm.remark.trim() || undefined }); uni.showToast({ title: "协助分支已派发", icon: "success" }); this.closeActionPopup(); this.doQuery(this.product.serial_number); } catch {} finally { this.actionLoading = false; } },
|
||||||
onSpawnUserChange(e) { const user = this.userOptions[e.detail.value]; if (user) this.spawnForm.assignee_id = user.id; },
|
|
||||||
async doSpawn() { this.actionLoading = true; try { const opId = this.currentUsername || this.currentUserId; await post(`/tasks/${this.actionPopup.task.id}/spawn?operator_id=${encodeURIComponent(opId)}`, { task_name: this.spawnForm.task_name, assignee_id: this.spawnForm.assignee_id, remark: this.spawnForm.remark.trim() || undefined }); uni.showToast({ title: "协助分支已派发", icon: "success" }); this.closeActionPopup(); this.doQuery(this.product.serial_number); } catch {} finally { this.actionLoading = false; } },
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@ -272,9 +296,6 @@ export default {
|
|||||||
.field-label { font-size: 14px; font-weight: 600; color: #374151; margin-top: 10px; margin-bottom: 4px; }
|
.field-label { font-size: 14px; font-weight: 600; color: #374151; margin-top: 10px; margin-bottom: 4px; }
|
||||||
.required { color: #ef4444; }
|
.required { color: #ef4444; }
|
||||||
.picker-box { width: 100%; height: 42px; border: 1px solid #e5e7eb; border-radius: 10px; padding: 0 12px; font-size: 14px; color: #1f2937; line-height: 42px; box-sizing: border-box; background: #fff; }
|
.picker-box { width: 100%; height: 42px; border: 1px solid #e5e7eb; border-radius: 10px; padding: 0 12px; font-size: 14px; color: #1f2937; line-height: 42px; box-sizing: border-box; background: #fff; }
|
||||||
.switch-row { display: flex; align-items: center; justify-content: space-between; margin-top: 12px; padding: 8px 0; }
|
|
||||||
.switch-label { font-size: 15px; font-weight: 600; color: #2563eb; }
|
|
||||||
.switch-hint { font-size: 11px; color: #9ca3af; display: block; margin-top: 2px; }
|
|
||||||
.img-grid { display: flex; flex-wrap: wrap; margin: 8px -5px; }
|
.img-grid { display: flex; flex-wrap: wrap; margin: 8px -5px; }
|
||||||
.img-cell { position: relative; width: 160rpx; height: 160rpx; margin: 10rpx; }
|
.img-cell { position: relative; width: 160rpx; height: 160rpx; margin: 10rpx; }
|
||||||
.img-thumb { width: 160rpx; height: 160rpx; border-radius: 12rpx; border: 1px solid #e5e7eb; }
|
.img-thumb { width: 160rpx; height: 160rpx; border-radius: 12rpx; border: 1px solid #e5e7eb; }
|
||||||
@ -285,8 +306,12 @@ export default {
|
|||||||
.form-item { margin: 10px 0; }
|
.form-item { margin: 10px 0; }
|
||||||
.form-label { font-size: 14px; font-weight: 600; color: #374151; display: block; margin-bottom: 4px; }
|
.form-label { font-size: 14px; font-weight: 600; color: #374151; display: block; margin-bottom: 4px; }
|
||||||
.picker-value { display: flex; align-items: center; justify-content: space-between; width: 100%; height: 42px; padding: 0 12px; border: 1px solid #e5e7eb; border-radius: 10px; background: #f9fafb; font-size: 14px; box-sizing: border-box; }
|
.picker-value { display: flex; align-items: center; justify-content: space-between; width: 100%; height: 42px; padding: 0 12px; border: 1px solid #e5e7eb; border-radius: 10px; background: #f9fafb; font-size: 14px; box-sizing: border-box; }
|
||||||
.picker-placeholder { color: #9ca3af; }
|
.user-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
|
||||||
.picker-arrow { font-size: 12px; color: #9ca3af; margin-left: 8px; }
|
.user-grid-item { padding: 12px 8px; border-radius: 10px; background: #f3f4f6; text-align: center; font-size: 14px; font-weight: 600; color: #374151; border: 2px solid transparent; }
|
||||||
|
.user-grid-active { background: #dbeafe; color: #2563eb; border-color: #2563eb; }
|
||||||
.warehouse-hint { font-size: 13px; background: #ede9fe; color: #7c3aed; padding: 10px 14px; border-radius: 10px; margin: 8px 0; text-align: center; }
|
.warehouse-hint { font-size: 13px; background: #ede9fe; color: #7c3aed; padding: 10px 14px; border-radius: 10px; margin: 8px 0; text-align: center; }
|
||||||
|
.warehouse-transfer-banner { display: flex; align-items: center; gap: 12px; font-weight: 700; background: linear-gradient(135deg, #ede9fe, #dbeafe); color: #5b21b6; padding: 14px 16px; border-radius: 12px; margin-bottom: 12px; border: 2px dashed #a78bfa; }
|
||||||
|
.wt-icon { font-size: 24px; }
|
||||||
|
.wt-text { font-size: 14px; flex: 1; }
|
||||||
.preview-hint { font-size: 12px; background: #f0fdf4; color: #16a34a; padding: 8px 10px; border-radius: 8px; margin: 6px 0; }
|
.preview-hint { font-size: 12px; background: #f0fdf4; color: #16a34a; padding: 8px 10px; border-radius: 8px; margin: 6px 0; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
<!-- 手动输入 -->
|
<!-- 手动输入 -->
|
||||||
<view class="manual-input">
|
<view class="manual-input">
|
||||||
<input v-model="serialNumber" class="input" type="text" maxlength="16"
|
<input v-model="serialNumber" class="input" type="text" maxlength="16"
|
||||||
placeholder="手动输入16位序列号" @confirm="handleSearch" />
|
placeholder="手动输入16位身份证" @confirm="handleSearch" />
|
||||||
<button class="search-btn" @tap="handleSearch" :disabled="loading">
|
<button class="search-btn" @tap="handleSearch" :disabled="loading">
|
||||||
{{ loading ? '查询中' : '查询' }}
|
{{ loading ? '查询中' : '查询' }}
|
||||||
</button>
|
</button>
|
||||||
@ -24,7 +24,7 @@
|
|||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
<view v-if="!loading && !error" class="empty">
|
<view v-if="!loading && !error" class="empty">
|
||||||
<text class="empty-icon">📱</text>
|
<text class="empty-icon">📱</text>
|
||||||
<text class="empty-text">扫码或手动输入序列号查询产品进度</text>
|
<text class="empty-text">扫码或手动输入身份证查询产品进度</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@ -43,7 +43,7 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async doQuery(sn) {
|
async doQuery(sn) {
|
||||||
if (!sn || sn.length < 8) { this.error = "序列号至少需要 8 位"; return; }
|
if (!sn || sn.length < 8) { this.error = "身份证至少需要 8 位"; return; }
|
||||||
this.serialNumber = sn;
|
this.serialNumber = sn;
|
||||||
this.lastScanned = sn;
|
this.lastScanned = sn;
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|||||||
@ -2,24 +2,145 @@
|
|||||||
<view class="page">
|
<view class="page">
|
||||||
<view class="header">
|
<view class="header">
|
||||||
<text class="title">我的任务</text>
|
<text class="title">我的任务</text>
|
||||||
<text class="subtitle">待处理和进行中的任务</text>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="empty">
|
|
||||||
<text class="empty-icon">📋</text>
|
<!-- Tab 栏 — 固定顶部 -->
|
||||||
<text class="empty-text">暂无待办任务</text>
|
<view class="tabs">
|
||||||
|
<view v-for="t in TABS" :key="t.key"
|
||||||
|
:class="['tab', tab === t.key ? 'tab-active' : '']"
|
||||||
|
@tap="tab = t.key">{{ t.label }} ({{ countBy(t.key) }})</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 内容区 -->
|
||||||
|
<view v-if="loading" class="center">加载中...</view>
|
||||||
|
<template v-else>
|
||||||
|
<view v-if="filtered.length === 0" class="empty">
|
||||||
|
<text class="empty-icon">📋</text>
|
||||||
|
<text class="empty-text">{{ tab === 'all' ? '暂无待办任务' : '无此状态任务' }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-for="task in filtered" :key="task.id" class="card" @tap="goDetail(task)">
|
||||||
|
<view class="card-row">
|
||||||
|
<text class="card-name">{{ task.task_name }}</text>
|
||||||
|
<text :class="['card-status', statusColor(task.status)]">{{ statusLabel(task.status) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="card-meta">
|
||||||
|
<text>身份证: {{ task.product_sn || '—' }}</text>
|
||||||
|
<text>物料: {{ task.product_material || '—' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="card-time">创建: {{ formatTime(task.created_at) }}</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script>
|
||||||
|
import { get } from "../../utils/request";
|
||||||
|
|
||||||
|
const TABS = [
|
||||||
|
{ key: "all", label: "全部" },
|
||||||
|
{ key: "PENDING", label: "待接收" },
|
||||||
|
{ key: "WIP", label: "进行中" },
|
||||||
|
{ key: "COMPLETED", label: "已完成" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
TABS,
|
||||||
|
tab: "PENDING",
|
||||||
|
tasks: [],
|
||||||
|
loading: true,
|
||||||
|
currentUser: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filtered() {
|
||||||
|
if (this.tab === "all") return this.tasks;
|
||||||
|
return this.tasks.filter(t => t.status === this.tab);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
this.loadCurrentUser();
|
||||||
|
this.fetchTasks();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
statusLabel(s) {
|
||||||
|
const map = { PENDING: "待接收", WIP: "进行中", COMPLETED: "已完成", REJECTED: "已驳回", ARCHIVED: "已入库" };
|
||||||
|
return map[s] || s;
|
||||||
|
},
|
||||||
|
statusColor(s) {
|
||||||
|
switch (s) {
|
||||||
|
case "PENDING": return "s-yellow";
|
||||||
|
case "WIP": return "s-blue";
|
||||||
|
case "COMPLETED": return "s-green";
|
||||||
|
default: return "s-gray";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
countBy(key) {
|
||||||
|
if (key === "all") return this.tasks.length;
|
||||||
|
return this.tasks.filter(t => t.status === key).length;
|
||||||
|
},
|
||||||
|
formatTime(t) {
|
||||||
|
if (!t) return "";
|
||||||
|
const d = new Date(t);
|
||||||
|
const pad = (n) => String(n).padStart(2, "0");
|
||||||
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||||
|
},
|
||||||
|
loadCurrentUser() {
|
||||||
|
try {
|
||||||
|
const u = uni.getStorageSync("user");
|
||||||
|
if (u) this.currentUser = typeof u === "string" ? JSON.parse(u) : u;
|
||||||
|
} catch {}
|
||||||
|
},
|
||||||
|
async fetchTasks() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const username = this.currentUser?.username || "";
|
||||||
|
const res = await get("/tasks/", { assignee_id: username, limit: 100 });
|
||||||
|
this.tasks = res.tasks || [];
|
||||||
|
} catch { this.tasks = []; }
|
||||||
|
finally { this.loading = false; }
|
||||||
|
},
|
||||||
|
goDetail(task) {
|
||||||
|
const sn = task.product_sn || task.product_id;
|
||||||
|
if (sn) uni.navigateTo({ url: `/pages/scan/detail?serial=${sn}` });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page { min-height: 100vh; padding: 16px; padding-bottom: 80px; }
|
.page { min-height: 100vh; padding: 16px; padding-bottom: 100px; }
|
||||||
.header { margin-bottom: 24px; }
|
.header { margin-bottom: 12px; }
|
||||||
.title { font-size: 20px; font-weight: 700; color: #1f2937; display: block; }
|
.title { font-size: 20px; font-weight: 700; color: #1f2937; }
|
||||||
.subtitle { font-size: 13px; color: #9ca3af; margin-top: 4px; display: block; }
|
.center { text-align: center; padding: 48px 0; color: #9ca3af; }
|
||||||
.empty { display: flex; flex-direction: column; align-items: center; padding-top: 80px; }
|
.empty { display: flex; flex-direction: column; align-items: center; padding-top: 80px; }
|
||||||
.empty-icon { font-size: 64px; margin-bottom: 12px; }
|
.empty-icon { font-size: 64px; margin-bottom: 12px; }
|
||||||
.empty-text { font-size: 14px; color: #9ca3af; }
|
.empty-text { font-size: 14px; color: #9ca3af; }
|
||||||
|
|
||||||
|
/* tabs — sticky 不换行,溢出滚动 */
|
||||||
|
.tabs {
|
||||||
|
display: flex; gap: 8px; margin-bottom: 14px;
|
||||||
|
white-space: nowrap; overflow-x: auto;
|
||||||
|
position: sticky; top: 0; z-index: 999;
|
||||||
|
background: #f3f4f6; padding: 12px 0 8px;
|
||||||
|
}
|
||||||
|
.tab {
|
||||||
|
padding: 8px 16px; border-radius: 20px; font-size: 13px; font-weight: 600;
|
||||||
|
background: #fff; color: #6b7280; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.tab-active { background: #2563eb; color: #fff; }
|
||||||
|
|
||||||
|
/* card */
|
||||||
|
.card { background: #fff; border-radius: 12px; padding: 14px; margin-bottom: 10px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
|
||||||
|
.card-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
|
||||||
|
.card-name { font-size: 15px; font-weight: 700; color: #1f2937; }
|
||||||
|
.card-status { font-size: 11px; padding: 2px 10px; border-radius: 20px; font-weight: 600; }
|
||||||
|
.s-yellow { background: #fef3c7; color: #b45309; }
|
||||||
|
.s-blue { background: #dbeafe; color: #1d4ed8; }
|
||||||
|
.s-green { background: #dcfce7; color: #15803d; }
|
||||||
|
.s-gray { background: #f3f4f6; color: #6b7280; }
|
||||||
|
.card-meta { font-size: 12px; color: #6b7280; display: flex; gap: 12px; }
|
||||||
|
.card-time { font-size: 11px; color: #9ca3af; margin-top: 4px; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user