This commit is contained in:
LINING-PC\lining
2025-06-28 21:20:50 +08:00
commit 5ba84d5309
57 changed files with 15687 additions and 0 deletions
+773
View File
@@ -0,0 +1,773 @@
<script setup lang="ts">
import { ref, reactive, watch, computed } from 'vue'
import { useI18n } from '../composables/useI18n'
import DatePicker from './DatePicker.vue'
import GanttConfirmDialog from './GanttConfirmDialog.vue'
import type { Milestone } from '../models/classes/Milestone'
import '../styles/app.css'
interface Props {
visible: boolean
milestone?: Milestone | null
}
const props = defineProps<Props>()
const emit = defineEmits<{
'update:visible': [visible: boolean]
close: []
save: [milestone: Milestone]
delete: [milestoneId: number]
}>()
// 表单数据
const formData = reactive<Milestone>({
name: '',
startDate: '',
assignee: '',
type: 'milestone',
icon: 'diamond',
description: '',
})
// 表单验证错误
const errors = reactive({
name: '',
startDate: '',
})
// 下拉菜单状态
const dropdownOpen = ref(false)
// 删除确认状态
const showDeleteConfirm = ref(false)
// 描述文本框引用
const descriptionTextarea = ref<HTMLTextAreaElement | null>(null)
// 自动调整文本框高度
const adjustTextareaHeight = () => {
const textarea = descriptionTextarea.value
if (textarea) {
textarea.style.height = 'auto'
textarea.style.height = `${Math.min(textarea.scrollHeight, 120)}px`
}
}
// 监听里程碑变化,初始化表单数据
watch(
() => props.milestone,
newMilestone => {
if (newMilestone) {
Object.assign(formData, {
id: newMilestone.id,
name: newMilestone.name || '',
startDate: newMilestone.startDate || '',
assignee: newMilestone.assignee || '',
type: newMilestone.type || 'milestone',
icon: newMilestone.icon || 'diamond',
description: newMilestone.description || '',
})
} else {
// 新建里程碑时重置表单
Object.assign(formData, {
id: undefined,
name: '',
startDate: '',
assignee: '',
type: 'milestone',
icon: 'diamond',
description: '',
})
}
// 清空错误
errors.name = ''
errors.startDate = ''
},
{ immediate: true },
)
// 表单验证
const validateForm = () => {
errors.name = ''
errors.startDate = ''
let isValid = true
if (!formData.name.trim()) {
errors.name = t('milestoneNameRequired')
isValid = false
}
if (!formData.startDate) {
errors.startDate = t('milestoneDateRequired')
isValid = false
}
return isValid
}
// 表单是否有效
const isFormValid = computed(() => {
return formData.name.trim() && formData.startDate
})
// 选择图标
const selectIcon = (icon: string) => {
formData.icon = icon
dropdownOpen.value = false
}
// 保存处理
const handleSave = () => {
if (validateForm()) {
emit('save', { ...formData })
closeDialog()
}
}
// 删除处理
const handleDelete = () => {
if (formData.id) {
showDeleteConfirm.value = true
}
}
// 确认删除
const confirmDelete = () => {
if (formData.id) {
emit('delete', formData.id)
showDeleteConfirm.value = false
closeDialog()
}
}
// 取消删除
const cancelDelete = () => {
showDeleteConfirm.value = false
}
// 是否为编辑模式
const isEditMode = computed(() => {
return props.milestone && props.milestone.id
})
// 关闭对话框
const closeDialog = () => {
dropdownOpen.value = false
emit('update:visible', false)
emit('close')
}
// 点击遮罩层关闭
const handleOverlayClick = () => {
closeDialog()
}
// 多语言
const { t: globalT } = useI18n()
// 直接用全局 t 获取翻译
const t = (key: string) => {
const globalValue = (globalT.value as any)[key]
return globalValue || key
}
</script>
<template>
<div v-if="visible" class="milestone-dialog-overlay" @click="handleOverlayClick">
<div class="milestone-dialog" @click.stop>
<div class="milestone-dialog-header">
<h3 class="milestone-dialog-title">
<svg
class="milestone-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<g transform="rotate(45 12 12)">
<rect
x="4"
y="4"
width="16"
height="16"
rx="4"
ry="4"
fill="currentColor"
opacity="0.1"
/>
<rect
x="4"
y="4"
width="16"
height="16"
rx="4"
ry="4"
stroke="currentColor"
fill="none"
/>
</g>
</svg>
{{ isEditMode ? globalT.editMilestone : globalT.newMilestone }}
</h3>
<button class="milestone-dialog-close" :title="t('close')" @click="closeDialog">×</button>
</div>
<div class="milestone-dialog-content">
<form class="milestone-form" @submit.prevent="handleSave">
<!-- 第一行里程碑名称 -->
<div class="milestone-form-row">
<div class="milestone-form-item milestone-form-item-full">
<label class="milestone-form-label required" for="milestone-name">{{
t('milestoneName')
}}</label>
<input
id="milestone-name"
v-model="formData.name"
type="text"
class="milestone-form-input"
:class="{ error: errors.name }"
:placeholder="t('enterMilestoneName')"
required
/>
<span v-if="errors.name" class="milestone-form-error">{{ errors.name }}</span>
</div>
</div>
<!-- 第二行里程碑日期和图标 -->
<div class="milestone-form-row">
<div class="milestone-form-item">
<label class="milestone-form-label required" for="milestone-date">{{
t('milestoneDate')
}}</label>
<DatePicker
id="milestone-date"
v-model="formData.startDate"
type="date"
placeholder="请选择里程碑日期"
:class="{ error: errors.startDate }"
/>
<span v-if="errors.startDate" class="milestone-form-error">{{
errors.startDate
}}</span>
</div>
<div class="milestone-form-item">
<label class="milestone-form-label" for="milestone-icon">{{
t('milestoneIcon')
}}</label>
<div class="milestone-icon-dropdown" :class="{ active: dropdownOpen }">
<button
id="milestone-icon"
type="button"
class="milestone-icon-trigger"
:aria-expanded="dropdownOpen"
aria-haspopup="listbox"
@click="dropdownOpen = !dropdownOpen"
>
<div class="selected-icon">
<svg
v-if="formData.icon === 'diamond'"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<g transform="rotate(45 12 12)">
<rect
x="6"
y="6"
width="12"
height="12"
rx="3"
ry="3"
fill="currentColor"
/>
</g>
</svg>
<div v-else-if="formData.icon === 'rocket'" class="rocket-emoji-mini">🚀</div>
<span>{{ t(formData.icon || 'diamond') }}</span>
</div>
<svg
class="dropdown-arrow"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="6,9 12,15 18,9"></polyline>
</svg>
</button>
<div v-if="dropdownOpen" class="milestone-icon-options">
<div
class="icon-option"
:class="{ selected: formData.icon === 'diamond' }"
@click="selectIcon('diamond')"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<g transform="rotate(45 12 12)">
<rect
x="6"
y="6"
width="12"
height="12"
rx="3"
ry="3"
fill="currentColor"
/>
</g>
</svg>
<span>{{ t('diamond') }}</span>
</div>
<div
class="icon-option"
:class="{ selected: formData.icon === 'rocket' }"
@click="selectIcon('rocket')"
>
<div class="rocket-emoji-option">🚀</div>
<span>{{ t('rocket') }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 第三行描述 -->
<div class="milestone-form-row">
<div class="milestone-form-item milestone-form-item-full">
<label class="milestone-form-label" for="milestone-description">{{
t('description')
}}</label>
<div class="textarea-wrapper">
<textarea
id="milestone-description"
ref="descriptionTextarea"
v-model="formData.description"
class="milestone-form-textarea"
:placeholder="t('enterDescription')"
rows="4"
maxlength="500"
@input="adjustTextareaHeight"
></textarea>
<div class="textarea-footer">
<span class="char-count">{{ formData.description?.length || 0 }}/500</span>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="milestone-dialog-footer">
<div class="milestone-dialog-footer-left">
<button v-if="isEditMode" type="button" class="btn btn-danger" @click="handleDelete">
{{ globalT.delete }}
</button>
</div>
<div class="milestone-dialog-footer-right">
<button type="button" class="btn btn-default" @click="closeDialog">
{{ globalT.cancel }}
</button>
<button
type="button"
class="btn btn-primary"
:disabled="!isFormValid"
@click="handleSave"
>
{{ globalT.confirm }}
</button>
</div>
</div>
</div>
<GanttConfirmDialog
:visible="showDeleteConfirm"
:title="globalT.delete"
:message="globalT.confirmDelete"
:confirm-text="globalT.confirm"
:cancel-text="globalT.cancel"
@confirm="confirmDelete"
@cancel="cancelDelete"
/>
</div>
</template>
<style scoped>
@import '../styles/theme-variables.css';
.milestone-dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000; /* 确保在全屏模式下也能正常显示 */
}
.milestone-dialog {
background: var(--gantt-bg-primary, #ffffff);
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
width: 90%;
max-width: 600px;
max-height: 90vh;
overflow: hidden;
border: 1px solid var(--gantt-border-color, #dcdfe6);
}
.milestone-dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px;
border-bottom: 1px solid var(--gantt-border-color, #dcdfe6);
background: var(--gantt-bg-secondary, #f8f9fa);
}
.milestone-dialog-title {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--gantt-text-primary, #303133);
}
.milestone-icon {
width: 20px;
height: 20px;
color: var(--gantt-danger, #f56c6c);
filter: drop-shadow(0 0 4px var(--gantt-danger, #f56c6c));
}
.milestone-dialog-close {
width: 32px;
height: 32px;
border: none;
background: transparent;
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: var(--gantt-text-secondary, #909399);
transition: all 0.2s ease;
font-size: 24px;
font-weight: bold;
line-height: 1;
}
.milestone-dialog-content {
padding: 24px;
max-height: 60vh;
overflow-y: auto;
}
/* 表单样式 */
.milestone-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.milestone-form-row {
display: flex;
gap: 16px;
align-items: flex-start;
}
.milestone-form-item {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.milestone-form-item-full {
flex: 1 1 100%;
}
.milestone-form-label {
font-size: 14px;
font-weight: 500;
color: var(--gantt-text-secondary, #606266);
line-height: 1.4;
margin: 0;
}
.milestone-form-label.required::after {
content: '*';
color: var(--gantt-danger, #f56c6c);
margin-left: 4px;
}
.milestone-form-input {
padding: 12px 16px;
border: 1px solid var(--gantt-border-color, #dcdfe6);
border-radius: 4px;
font-size: 14px;
color: var(--gantt-text-primary, #303133);
background: var(--gantt-bg-primary, #ffffff);
transition: all 0.2s ease;
box-sizing: border-box;
height: 44px;
}
.milestone-form-input:focus {
outline: none;
border-color: var(--gantt-primary, #409eff);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
.milestone-form-input.error {
border-color: var(--gantt-danger, #f56c6c);
box-shadow: 0 0 0 2px rgba(245, 108, 108, 0.1);
}
.milestone-form-input::placeholder {
color: var(--gantt-text-placeholder, #c0c4cc);
}
.milestone-form-textarea {
width: 100%;
box-sizing: border-box;
padding: 12px 16px;
border: 1px solid var(--gantt-border-color, #dcdfe6);
border-radius: 4px;
font-size: 14px;
color: var(--gantt-text-primary, #303133);
background: var(--gantt-bg-primary, #ffffff);
transition: all 0.2s ease;
resize: none;
min-height: 80px;
max-height: 120px;
font-family: inherit;
line-height: 1.5;
overflow-y: auto;
}
.milestone-form-textarea:focus {
outline: none;
border-color: var(--gantt-primary, #409eff);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
.milestone-form-textarea::placeholder {
color: var(--gantt-text-placeholder, #c0c4cc);
}
.textarea-wrapper {
position: relative;
}
.textarea-footer {
display: flex;
justify-content: flex-end;
margin-top: 4px;
}
.char-count {
font-size: 12px;
color: var(--gantt-text-secondary, #909399);
}
.milestone-form-error {
font-size: 12px;
color: var(--gantt-danger, #f56c6c);
margin-top: 4px;
}
/* 图标下拉菜单样式 */
.milestone-icon-dropdown {
position: relative;
}
.milestone-icon-trigger {
width: 100%;
height: 44px; /* 固定高度 */
padding: 12px 16px;
border: 1px solid var(--gantt-border-color, #dcdfe6);
border-radius: 4px;
background: var(--gantt-bg-primary, #ffffff);
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
transition: all 0.2s ease;
box-sizing: border-box;
}
.milestone-icon-trigger:hover {
border-color: var(--gantt-primary, #409eff);
}
.milestone-icon-dropdown.active .milestone-icon-trigger {
border-color: var(--gantt-primary, #409eff);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
.selected-icon {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: var(--gantt-text-primary, #303133);
}
.selected-icon svg {
width: 16px;
height: 16px;
color: var(--gantt-danger, #f56c6c);
}
.rocket-emoji-mini {
font-size: 16px;
transform: rotate(-45deg);
display: inline-block;
}
.dropdown-arrow {
width: 16px;
height: 16px;
color: var(--gantt-text-secondary, #909399);
transition: transform 0.2s ease;
}
.milestone-icon-dropdown.active .dropdown-arrow {
transform: rotate(180deg);
}
.milestone-icon-options {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--gantt-bg-primary, #ffffff);
border: 1px solid var(--gantt-border-color, #dcdfe6);
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 1000;
margin-top: 4px;
}
.icon-option {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
cursor: pointer;
transition: all 0.2s ease;
border-bottom: 1px solid var(--gantt-border-light, #e4e7ed);
}
.icon-option:last-child {
border-bottom: none;
}
.icon-option:hover {
background: var(--gantt-bg-light, #f5f7fa);
}
.icon-option.selected {
background: var(--gantt-primary-lightest, #ecf5ff);
color: var(--gantt-primary, #409eff);
}
.icon-option svg {
width: 16px;
height: 16px;
color: var(--gantt-danger, #f56c6c);
}
.rocket-emoji-option {
font-size: 16px;
transform: rotate(-45deg);
display: inline-block;
}
.icon-option span {
font-size: 14px;
}
/* 对话框底部 */
.milestone-dialog-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
border-top: 1px solid var(--gantt-border-color, #dcdfe6);
background: var(--gantt-bg-secondary, #f8f9fa);
}
.milestone-dialog-footer-left {
display: flex;
align-items: center;
}
.milestone-dialog-footer-right {
display: flex;
align-items: center;
gap: 12px;
}
/* 删除确认弹窗样式 */
.milestone-confirm-dialog {
background: var(--gantt-bg-primary, #ffffff);
border-radius: 8px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
width: 90%;
max-width: 400px;
border: 1px solid var(--gantt-border-color, #dcdfe6);
}
.milestone-confirm-header {
padding: 20px 24px 0;
}
.milestone-confirm-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--gantt-text-primary, #303133);
}
.milestone-confirm-content {
padding: 16px 24px;
}
.milestone-confirm-content p {
margin: 0;
font-size: 14px;
color: var(--gantt-text-secondary, #606266);
line-height: 1.5;
}
.milestone-confirm-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
padding: 16px 24px;
border-top: 1px solid var(--gantt-border-color, #dcdfe6);
background: var(--gantt-bg-secondary, #f8f9fa);
}
/* 暗黑模式下的确认弹窗 */
:global(html[data-theme='dark']) .milestone-confirm-dialog {
background: var(--gantt-bg-dark, #1d1e1f);
border-color: var(--gantt-border-dark, #3c3e40);
}
:global(html[data-theme='dark']) .milestone-confirm-footer {
background: var(--gantt-bg-darker, #141414);
border-color: var(--gantt-border-dark, #3c3e40);
}
</style>