feat: 진행 보고 작성/열람 모달 UI 개편(시안 반영)
시안(진행 상태 보고서/받은 진행 보고) 기준으로 두 모달 전면 개편: - 전체 탭 + 카테고리 탭(색상 dot·건수), 전체 탭에서 카테고리별 그룹 헤더로 묶음 - 작성(작업자): 항목별 상태색 드롭다운(StatusSelect, body Teleport), 보고 메모, 하단 상태 요약(대기/진행/완료 N) - 열람(지시자·담당자): '진행 보고 도착' 태그 + 보고자 아바타/이름/점검일·시각, 읽기전용 상태 배지, 보고자 메모 박스 - 신규 StatusSelect 컴포넌트 추가, 기존 cp-modal/ws-* 스타일 정리, 미사용 DropdownSelect import 제거 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
<script setup lang="ts">
|
||||
// 진행 상태(대기/진행 중/완료) 선택 드롭다운 — 상태색 테마. 메뉴는 body Teleport(스크롤 클리핑 방지).
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import type { ChecklistWorkStatus } from '@/mock/relay.mock'
|
||||
|
||||
const props = defineProps<{ modelValue: ChecklistWorkStatus }>()
|
||||
const emit = defineEmits<{ (e: 'update:modelValue', v: ChecklistWorkStatus): void }>()
|
||||
|
||||
const META: Record<ChecklistWorkStatus, { label: string; color: string; weak: string }> = {
|
||||
todo: { label: '대기', color: '#9298a3', weak: '#eef0f2' },
|
||||
doing: { label: '진행 중', color: '#2563eb', weak: '#e9f0fd' },
|
||||
done: { label: '완료', color: '#1f9d57', weak: '#e7f5ed' },
|
||||
}
|
||||
const ORDER: ChecklistWorkStatus[] = ['todo', 'doing', 'done']
|
||||
const cur = computed(() => META[props.modelValue])
|
||||
|
||||
const open = ref(false)
|
||||
const triggerRef = ref<HTMLElement | null>(null)
|
||||
const menuRef = ref<HTMLElement | null>(null)
|
||||
const menuStyle = ref<Record<string, string>>({})
|
||||
|
||||
function positionMenu(): void {
|
||||
const el = triggerRef.value
|
||||
if (!el) return
|
||||
const r = el.getBoundingClientRect()
|
||||
menuStyle.value = {
|
||||
top: `${r.bottom + 5}px`,
|
||||
right: `${Math.max(0, window.innerWidth - r.right)}px`,
|
||||
}
|
||||
}
|
||||
function toggle(): void {
|
||||
open.value = !open.value
|
||||
if (open.value) void nextTick(positionMenu)
|
||||
}
|
||||
function pick(v: ChecklistWorkStatus): void {
|
||||
emit('update:modelValue', v)
|
||||
open.value = false
|
||||
}
|
||||
function onDoc(e: MouseEvent): void {
|
||||
if (!open.value) return
|
||||
const t = e.target as Node
|
||||
if (triggerRef.value?.contains(t) || menuRef.value?.contains(t)) return
|
||||
open.value = false
|
||||
}
|
||||
function onReposition(): void {
|
||||
if (open.value) open.value = false
|
||||
}
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', onDoc)
|
||||
window.addEventListener('scroll', onReposition, true)
|
||||
window.addEventListener('resize', onReposition)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', onDoc)
|
||||
window.removeEventListener('scroll', onReposition, true)
|
||||
window.removeEventListener('resize', onReposition)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ss">
|
||||
<button
|
||||
ref="triggerRef"
|
||||
type="button"
|
||||
class="ss-btn"
|
||||
:class="{ open }"
|
||||
:style="{ '--c': cur.color, '--w': cur.weak }"
|
||||
@click="toggle"
|
||||
>
|
||||
<span class="ss-dot" />
|
||||
<span class="ss-lbl">{{ cur.label }}</span>
|
||||
<svg
|
||||
class="ss-chev"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><path d="m6 9 6 6 6-6" /></svg>
|
||||
</button>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="open"
|
||||
ref="menuRef"
|
||||
class="ss-menu"
|
||||
:style="menuStyle"
|
||||
>
|
||||
<button
|
||||
v-for="k in ORDER"
|
||||
:key="k"
|
||||
type="button"
|
||||
class="ss-opt"
|
||||
:class="{ on: k === modelValue }"
|
||||
@click="pick(k)"
|
||||
>
|
||||
<span
|
||||
class="ss-dot"
|
||||
:style="{ background: META[k].color }"
|
||||
/>{{ META[k].label }}
|
||||
<svg
|
||||
v-if="k === modelValue"
|
||||
class="ss-check"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.4"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><path d="M20 6 9 17l-5-5" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ss {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ss-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
height: 30px;
|
||||
min-width: 100px;
|
||||
padding: 0 9px 0 11px;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 8px;
|
||||
background: var(--w);
|
||||
color: var(--c);
|
||||
font-family: inherit;
|
||||
font-size: 0.781rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ss-btn.open {
|
||||
border-color: var(--c);
|
||||
}
|
||||
.ss-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--c);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ss-btn .ss-lbl {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
}
|
||||
.ss-chev {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
.ss-menu {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
width: 9rem;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 9px;
|
||||
box-shadow: 0 8px 24px rgba(20, 24, 33, 0.16);
|
||||
padding: 5px;
|
||||
}
|
||||
.ss-opt {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: none;
|
||||
font-family: inherit;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-2);
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
.ss-opt:hover {
|
||||
background: #f4f5f7;
|
||||
}
|
||||
.ss-opt.on {
|
||||
color: var(--text);
|
||||
}
|
||||
.ss-opt .ss-dot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
}
|
||||
.ss-opt .ss-check {
|
||||
margin-left: auto;
|
||||
color: var(--text);
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user