14 changed files with 3137 additions and 102 deletions
@ -0,0 +1,697 @@ |
|||||
|
<template> |
||||
|
<el-dialog |
||||
|
v-model="showDialog" |
||||
|
title="完整合同查看" |
||||
|
width="1200px" |
||||
|
:destroy-on-close="true" |
||||
|
:close-on-click-modal="false" |
||||
|
class="contract-detail-dialog" |
||||
|
> |
||||
|
<div class="contract-detail-content" v-loading="loading"> |
||||
|
<!-- 合同基础信息 --> |
||||
|
<div class="contract-basic-info" v-if="contractData?.contract"> |
||||
|
<h3>{{ contractData.contract.contract_name }}</h3> |
||||
|
<div class="info-grid"> |
||||
|
<div class="info-item"> |
||||
|
<label>合同类型:</label> |
||||
|
<span>{{ contractData.contract.contract_type }}</span> |
||||
|
</div> |
||||
|
<div class="info-item"> |
||||
|
<label>签署状态:</label> |
||||
|
<el-tag :type="getStatusType(contractData.status)"> |
||||
|
{{ getStatusLabel(contractData.status) }} |
||||
|
</el-tag> |
||||
|
</div> |
||||
|
<div class="info-item"> |
||||
|
<label>创建时间:</label> |
||||
|
<span>{{ formatDate(contractData.created_at) }}</span> |
||||
|
</div> |
||||
|
<div class="info-item" v-if="contractData.sign_time"> |
||||
|
<label>签署时间:</label> |
||||
|
<span>{{ formatDate(contractData.sign_time) }}</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 合同字段表单 --> |
||||
|
<div class="contract-fields" v-if="contractData?.field_configs"> |
||||
|
<h4>合同字段信息</h4> |
||||
|
|
||||
|
<!-- 字段分组:按类型分组显示 --> |
||||
|
<div class="fields-container"> |
||||
|
<!-- 基本信息字段组 --> |
||||
|
<div class="field-group" v-if="basicFields.length > 0"> |
||||
|
<div class="group-header"> |
||||
|
<span class="group-title">基本信息</span> |
||||
|
<el-tag size="small" type="info">{{ basicFields.length }} 项</el-tag> |
||||
|
</div> |
||||
|
<div class="fields-grid"> |
||||
|
<div |
||||
|
v-for="field in basicFields" |
||||
|
:key="field.placeholder" |
||||
|
class="field-card" |
||||
|
> |
||||
|
<div class="field-header"> |
||||
|
<span class="field-name">{{ field.placeholder }}</span> |
||||
|
<div class="field-tags"> |
||||
|
<el-tag size="small" :type="getFieldTypeColor(field.data_type)"> |
||||
|
{{ getFieldTypeLabel(field.data_type) }} |
||||
|
</el-tag> |
||||
|
<el-tag size="small" type="info" v-if="field.sign_party"> |
||||
|
{{ field.sign_party === 'party_a' ? '甲方' : '乙方' }} |
||||
|
</el-tag> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="field-content"> |
||||
|
<!-- 用户输入字段 --> |
||||
|
<el-input |
||||
|
v-if="field.data_type === 'user_input' && field.is_editable" |
||||
|
v-model="formData[field.placeholder]" |
||||
|
:placeholder="`请输入${field.placeholder}`" |
||||
|
size="small" |
||||
|
/> |
||||
|
<div v-else class="field-display"> |
||||
|
<!-- 数据库字段和系统字段 --> |
||||
|
<span v-if="field.data_type === 'database' || field.data_type === 'system'" class="value-text"> |
||||
|
{{ field.display_value || '未获取' }} |
||||
|
</span> |
||||
|
<!-- 用户输入只读字段 --> |
||||
|
<span v-else-if="field.data_type === 'user_input'" class="value-text"> |
||||
|
{{ field.display_value || '未填写' }} |
||||
|
</span> |
||||
|
<!-- 签名图片字段 --> |
||||
|
<div v-else-if="field.data_type === 'signature' || field.data_type === 'sign_img'" class="signature-display"> |
||||
|
<el-image |
||||
|
v-if="field.display_value" |
||||
|
:src="field.display_value" |
||||
|
style="width: 100px; height: 50px;" |
||||
|
:preview-src-list="[field.display_value]" |
||||
|
fit="contain" |
||||
|
class="signature-image" |
||||
|
/> |
||||
|
<span v-else class="no-signature">未签名</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 日期时间字段组 --> |
||||
|
<div class="field-group" v-if="dateFields.length > 0"> |
||||
|
<div class="group-header"> |
||||
|
<span class="group-title">日期信息</span> |
||||
|
<el-tag size="small" type="info">{{ dateFields.length }} 项</el-tag> |
||||
|
</div> |
||||
|
<div class="fields-grid"> |
||||
|
<div |
||||
|
v-for="field in dateFields" |
||||
|
:key="field.placeholder" |
||||
|
class="field-card" |
||||
|
> |
||||
|
<div class="field-header"> |
||||
|
<span class="field-name">{{ field.placeholder }}</span> |
||||
|
<div class="field-tags"> |
||||
|
<el-tag size="small" :type="getFieldTypeColor(field.data_type)"> |
||||
|
{{ getFieldTypeLabel(field.data_type) }} |
||||
|
</el-tag> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="field-content"> |
||||
|
<span class="value-text">{{ field.display_value || '未获取' }}</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 签名相关字段组 --> |
||||
|
<div class="field-group" v-if="signatureFields.length > 0"> |
||||
|
<div class="group-header"> |
||||
|
<span class="group-title">签名信息</span> |
||||
|
<el-tag size="small" type="info">{{ signatureFields.length }} 项</el-tag> |
||||
|
</div> |
||||
|
<div class="fields-grid signature-grid"> |
||||
|
<div |
||||
|
v-for="field in signatureFields" |
||||
|
:key="field.placeholder" |
||||
|
class="field-card signature-card" |
||||
|
> |
||||
|
<div class="field-header"> |
||||
|
<span class="field-name">{{ field.placeholder }}</span> |
||||
|
<div class="field-tags"> |
||||
|
<el-tag size="small" :type="getFieldTypeColor(field.data_type)"> |
||||
|
{{ getFieldTypeLabel(field.data_type) }} |
||||
|
</el-tag> |
||||
|
<el-tag size="small" type="info" v-if="field.sign_party"> |
||||
|
{{ field.sign_party === 'party_a' ? '甲方' : '乙方' }} |
||||
|
</el-tag> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="field-content"> |
||||
|
<div class="signature-display"> |
||||
|
<el-image |
||||
|
v-if="field.display_value" |
||||
|
:src="field.display_value" |
||||
|
style="width: 120px; height: 60px;" |
||||
|
:preview-src-list="[field.display_value]" |
||||
|
fit="contain" |
||||
|
class="signature-image" |
||||
|
/> |
||||
|
<span v-else class="no-signature">未签名</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 其他字段组 --> |
||||
|
<div class="field-group" v-if="otherFields.length > 0"> |
||||
|
<div class="group-header"> |
||||
|
<span class="group-title">其他信息</span> |
||||
|
<el-tag size="small" type="info">{{ otherFields.length }} 项</el-tag> |
||||
|
</div> |
||||
|
<div class="fields-grid"> |
||||
|
<div |
||||
|
v-for="field in otherFields" |
||||
|
:key="field.placeholder" |
||||
|
class="field-card" |
||||
|
> |
||||
|
<div class="field-header"> |
||||
|
<span class="field-name">{{ field.placeholder }}</span> |
||||
|
<div class="field-tags"> |
||||
|
<el-tag size="small" :type="getFieldTypeColor(field.data_type)"> |
||||
|
{{ getFieldTypeLabel(field.data_type) }} |
||||
|
</el-tag> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="field-content"> |
||||
|
<el-input |
||||
|
v-if="field.data_type === 'user_input' && field.is_editable" |
||||
|
v-model="formData[field.placeholder]" |
||||
|
:placeholder="`请输入${field.placeholder}`" |
||||
|
size="small" |
||||
|
/> |
||||
|
<span v-else class="value-text">{{ field.display_value || '未填写' }}</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 合同内容预览 --> |
||||
|
<div class="contract-content-preview" v-if="contractData?.contract?.contract_content"> |
||||
|
<h4>合同内容预览</h4> |
||||
|
<div class="content-display"> |
||||
|
{{ contractData.contract.contract_content }} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<template #footer> |
||||
|
<span class="dialog-footer"> |
||||
|
<el-button @click="showDialog = false">关闭</el-button> |
||||
|
<el-button |
||||
|
v-if="hasEditableFields" |
||||
|
type="primary" |
||||
|
:loading="saving" |
||||
|
@click="handleSaveFields" |
||||
|
> |
||||
|
保存字段 |
||||
|
</el-button> |
||||
|
<el-button |
||||
|
v-if="contractData?.status === 1" |
||||
|
type="success" |
||||
|
:loading="generating" |
||||
|
@click="handleGenerateDocument" |
||||
|
> |
||||
|
生成Word文档 |
||||
|
</el-button> |
||||
|
</span> |
||||
|
</template> |
||||
|
</el-dialog> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts" setup> |
||||
|
import { ref, computed, watch } from 'vue' |
||||
|
import { ElMessage, ElForm } from 'element-plus' |
||||
|
import { updateContractSignStatus } from '@/app/api/contract_sign' |
||||
|
|
||||
|
interface Props { |
||||
|
show: boolean |
||||
|
contractData: any |
||||
|
personnelId: number | string |
||||
|
} |
||||
|
|
||||
|
const props = defineProps<Props>() |
||||
|
const emit = defineEmits(['update:show', 'success']) |
||||
|
|
||||
|
// 响应式数据 |
||||
|
const showDialog = computed({ |
||||
|
get: () => props.show, |
||||
|
set: (value) => emit('update:show', value) |
||||
|
}) |
||||
|
|
||||
|
const loading = ref(false) |
||||
|
const saving = ref(false) |
||||
|
const generating = ref(false) |
||||
|
const formRef = ref<InstanceType<typeof ElForm>>() |
||||
|
const formData = ref<Record<string, any>>({}) |
||||
|
|
||||
|
// 计算属性 |
||||
|
const hasEditableFields = computed(() => { |
||||
|
if (!props.contractData?.field_configs) return false |
||||
|
return props.contractData.field_configs.some((field: any) => field.is_editable) |
||||
|
}) |
||||
|
|
||||
|
// 字段分组计算 |
||||
|
const fieldGroups = computed(() => { |
||||
|
if (!props.contractData?.field_configs) return { |
||||
|
basicFields: [], |
||||
|
dateFields: [], |
||||
|
signatureFields: [], |
||||
|
otherFields: [] |
||||
|
} |
||||
|
|
||||
|
const fields = props.contractData.field_configs |
||||
|
const basicFields: any[] = [] |
||||
|
const dateFields: any[] = [] |
||||
|
const signatureFields: any[] = [] |
||||
|
const otherFields: any[] = [] |
||||
|
|
||||
|
fields.forEach((field: any) => { |
||||
|
// 签名相关字段 |
||||
|
if (field.data_type === 'signature' || field.data_type === 'sign_img') { |
||||
|
signatureFields.push(field) |
||||
|
} |
||||
|
// 日期相关字段 |
||||
|
else if (field.placeholder.includes('年') || field.placeholder.includes('月') || field.placeholder.includes('日') || |
||||
|
field.placeholder.includes('year') || field.placeholder.includes('month') || field.placeholder.includes('day') || |
||||
|
field.placeholder.includes('时间') || field.placeholder.includes('期')) { |
||||
|
dateFields.push(field) |
||||
|
} |
||||
|
// 基本信息字段(姓名、角色、联系方式等) |
||||
|
else if (field.placeholder.includes('姓名') || field.placeholder.includes('乙方') || field.placeholder.includes('甲方') || |
||||
|
field.placeholder.includes('岗位') || field.placeholder.includes('角色') || field.placeholder.includes('电话') || |
||||
|
field.placeholder.includes('地址') || field.placeholder.includes('证件') || field.placeholder.includes('代码')) { |
||||
|
basicFields.push(field) |
||||
|
} |
||||
|
// 其他字段 |
||||
|
else { |
||||
|
otherFields.push(field) |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
return { |
||||
|
basicFields, |
||||
|
dateFields, |
||||
|
signatureFields, |
||||
|
otherFields |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
const basicFields = computed(() => fieldGroups.value.basicFields) |
||||
|
const dateFields = computed(() => fieldGroups.value.dateFields) |
||||
|
const signatureFields = computed(() => fieldGroups.value.signatureFields) |
||||
|
const otherFields = computed(() => fieldGroups.value.otherFields) |
||||
|
|
||||
|
// 状态配置 |
||||
|
const statusConfig = { |
||||
|
1: { label: '未签署', type: 'warning' }, |
||||
|
2: { label: '已签署', type: 'success' }, |
||||
|
3: { label: '已生效', type: 'primary' }, |
||||
|
4: { label: '已失效', type: 'info' } |
||||
|
} |
||||
|
|
||||
|
// 方法 |
||||
|
const getStatusLabel = (status?: number) => { |
||||
|
if (!status) return '未签署' |
||||
|
return statusConfig[status as keyof typeof statusConfig]?.label || '未知' |
||||
|
} |
||||
|
|
||||
|
const getStatusType = (status?: number) => { |
||||
|
if (!status) return 'warning' |
||||
|
return statusConfig[status as keyof typeof statusConfig]?.type || 'info' |
||||
|
} |
||||
|
|
||||
|
const getFieldTypeLabel = (dataType: string) => { |
||||
|
const labels = { |
||||
|
'user_input': '用户输入', |
||||
|
'database': '数据库', |
||||
|
'system': '系统函数', |
||||
|
'signature': '手写签名', |
||||
|
'sign_img': '签名图片' |
||||
|
} |
||||
|
return labels[dataType as keyof typeof labels] || dataType |
||||
|
} |
||||
|
|
||||
|
const getFieldTypeColor = (dataType: string) => { |
||||
|
const colors = { |
||||
|
'user_input': 'primary', |
||||
|
'database': 'success', |
||||
|
'system': 'info', |
||||
|
'signature': 'warning', |
||||
|
'sign_img': 'warning' |
||||
|
} |
||||
|
return colors[dataType as keyof typeof colors] || 'info' |
||||
|
} |
||||
|
|
||||
|
const formatDate = (dateStr: string) => { |
||||
|
if (!dateStr) return '未设置' |
||||
|
try { |
||||
|
const date = new Date(dateStr) |
||||
|
return date.toLocaleDateString('zh-CN', { |
||||
|
year: 'numeric', |
||||
|
month: '2-digit', |
||||
|
'day': '2-digit', |
||||
|
hour: '2-digit', |
||||
|
minute: '2-digit' |
||||
|
}) |
||||
|
} catch (error) { |
||||
|
return dateStr |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 初始化表单数据 |
||||
|
const initFormData = () => { |
||||
|
if (!props.contractData?.field_configs) return |
||||
|
|
||||
|
formData.value = {} |
||||
|
props.contractData.field_configs.forEach((field: any) => { |
||||
|
if (field.is_editable && field.data_type === 'user_input') { |
||||
|
formData.value[field.placeholder] = field.value || field.default_value || '' |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// 保存字段数据 |
||||
|
const handleSaveFields = async () => { |
||||
|
if (!formRef.value) return |
||||
|
|
||||
|
try { |
||||
|
saving.value = true |
||||
|
|
||||
|
// 这里需要调用API保存字段数据到 fill_data |
||||
|
const fillData = { ...props.contractData.fill_data, ...formData.value } |
||||
|
|
||||
|
// TODO: 调用更新fill_data的API |
||||
|
// await updateContractFillData(props.contractData.id, { fill_data: JSON.stringify(fillData) }) |
||||
|
|
||||
|
ElMessage.success('字段保存成功') |
||||
|
emit('success') |
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error('保存字段失败:', error) |
||||
|
ElMessage.error('保存字段失败') |
||||
|
} finally { |
||||
|
saving.value = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 生成Word文档 |
||||
|
const handleGenerateDocument = async () => { |
||||
|
try { |
||||
|
generating.value = true |
||||
|
|
||||
|
// TODO: 调用生成Word文档的API |
||||
|
// await generateContractDocument(props.contractData.id) |
||||
|
|
||||
|
ElMessage.success('Word文档生成成功') |
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error('生成文档失败:', error) |
||||
|
ElMessage.error('生成文档失败') |
||||
|
} finally { |
||||
|
generating.value = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 监听弹窗显示状态 |
||||
|
watch(() => props.show, (visible) => { |
||||
|
if (visible && props.contractData) { |
||||
|
initFormData() |
||||
|
} |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.contract-detail-dialog { |
||||
|
:deep(.el-dialog) { |
||||
|
border-radius: 8px; |
||||
|
} |
||||
|
|
||||
|
:deep(.el-dialog__header) { |
||||
|
padding: 20px 24px 16px; |
||||
|
border-bottom: 1px solid #f0f0f0; |
||||
|
background: #fafafa; |
||||
|
border-radius: 8px 8px 0 0; |
||||
|
|
||||
|
.el-dialog__title { |
||||
|
font-size: 18px; |
||||
|
font-weight: 600; |
||||
|
color: #1f2937; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:deep(.el-dialog__body) { |
||||
|
padding: 24px; |
||||
|
max-height: 80vh; |
||||
|
overflow-y: auto; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.contract-detail-content { |
||||
|
.contract-basic-info { |
||||
|
margin-bottom: 24px; |
||||
|
padding: 20px; |
||||
|
background: #f8f9fa; |
||||
|
border-radius: 8px; |
||||
|
border: 1px solid #e9ecef; |
||||
|
|
||||
|
h3 { |
||||
|
margin: 0 0 16px; |
||||
|
font-size: 18px; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
.info-grid { |
||||
|
display: grid; |
||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); |
||||
|
gap: 20px; |
||||
|
|
||||
|
.info-item { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
padding: 12px 16px; |
||||
|
background: #fff; |
||||
|
border-radius: 8px; |
||||
|
border: 1px solid #e4e7ed; |
||||
|
transition: all 0.3s ease; |
||||
|
|
||||
|
&:hover { |
||||
|
border-color: #409eff; |
||||
|
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1); |
||||
|
} |
||||
|
|
||||
|
label { |
||||
|
font-weight: 600; |
||||
|
color: #666; |
||||
|
margin-right: 12px; |
||||
|
min-width: 90px; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
span { |
||||
|
color: #333; |
||||
|
font-size: 14px; |
||||
|
flex: 1; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.contract-fields { |
||||
|
margin-bottom: 24px; |
||||
|
|
||||
|
h4 { |
||||
|
margin: 0 0 24px; |
||||
|
font-size: 18px; |
||||
|
color: #333; |
||||
|
padding-bottom: 12px; |
||||
|
border-bottom: 2px solid #e9ecef; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
|
||||
|
.fields-container { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 24px; |
||||
|
} |
||||
|
|
||||
|
.field-group { |
||||
|
background: #fafbfc; |
||||
|
border-radius: 12px; |
||||
|
padding: 20px; |
||||
|
border: 1px solid #e9ecef; |
||||
|
|
||||
|
.group-header { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
margin-bottom: 16px; |
||||
|
padding-bottom: 12px; |
||||
|
border-bottom: 1px solid #e9ecef; |
||||
|
|
||||
|
.group-title { |
||||
|
font-size: 16px; |
||||
|
font-weight: 600; |
||||
|
color: #333; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.fields-grid { |
||||
|
display: grid; |
||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); |
||||
|
gap: 16px; |
||||
|
|
||||
|
// 签名网格特殊布局 |
||||
|
&.signature-grid { |
||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.field-card { |
||||
|
background: #fff; |
||||
|
border-radius: 8px; |
||||
|
padding: 16px; |
||||
|
border: 1px solid #e4e7ed; |
||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); |
||||
|
transition: all 0.3s ease; |
||||
|
|
||||
|
&:hover { |
||||
|
border-color: #409eff; |
||||
|
box-shadow: 0 4px 8px rgba(64, 158, 255, 0.1); |
||||
|
transform: translateY(-2px); |
||||
|
} |
||||
|
|
||||
|
&.signature-card { |
||||
|
min-height: 120px; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: space-between; |
||||
|
} |
||||
|
|
||||
|
.field-header { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
margin-bottom: 12px; |
||||
|
|
||||
|
.field-name { |
||||
|
font-weight: 600; |
||||
|
color: #333; |
||||
|
font-size: 14px; |
||||
|
flex: 1; |
||||
|
margin-right: 8px; |
||||
|
} |
||||
|
|
||||
|
.field-tags { |
||||
|
display: flex; |
||||
|
gap: 4px; |
||||
|
flex-shrink: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.field-content { |
||||
|
.field-display { |
||||
|
.value-text { |
||||
|
display: block; |
||||
|
padding: 8px 12px; |
||||
|
background: #f8f9fa; |
||||
|
border: 1px solid #e9ecef; |
||||
|
border-radius: 6px; |
||||
|
color: #333; |
||||
|
font-size: 14px; |
||||
|
min-height: 20px; |
||||
|
line-height: 20px; |
||||
|
} |
||||
|
|
||||
|
.signature-display { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
|
||||
|
.signature-image { |
||||
|
border: 1px solid #e9ecef; |
||||
|
border-radius: 6px; |
||||
|
background: #fff; |
||||
|
} |
||||
|
|
||||
|
.no-signature { |
||||
|
color: #999; |
||||
|
font-style: italic; |
||||
|
padding: 16px 24px; |
||||
|
background: #f8f9fa; |
||||
|
border: 1px dashed #d9d9d9; |
||||
|
border-radius: 6px; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.value-text { |
||||
|
display: block; |
||||
|
padding: 8px 12px; |
||||
|
background: #f8f9fa; |
||||
|
border: 1px solid #e9ecef; |
||||
|
border-radius: 6px; |
||||
|
color: #333; |
||||
|
font-size: 14px; |
||||
|
min-height: 20px; |
||||
|
line-height: 20px; |
||||
|
} |
||||
|
|
||||
|
:deep(.el-input) { |
||||
|
.el-input__wrapper { |
||||
|
border-radius: 6px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.contract-content-preview { |
||||
|
h4 { |
||||
|
margin: 0 0 16px; |
||||
|
font-size: 16px; |
||||
|
color: #333; |
||||
|
padding-bottom: 8px; |
||||
|
border-bottom: 2px solid #e9ecef; |
||||
|
} |
||||
|
|
||||
|
.content-display { |
||||
|
padding: 20px; |
||||
|
background: #fff; |
||||
|
border: 1px solid #e9ecef; |
||||
|
border-radius: 8px; |
||||
|
max-height: 300px; |
||||
|
overflow-y: auto; |
||||
|
font-size: 14px; |
||||
|
line-height: 1.6; |
||||
|
color: #666; |
||||
|
white-space: pre-wrap; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.dialog-footer { |
||||
|
.el-button { |
||||
|
padding: 10px 24px; |
||||
|
font-size: 14px; |
||||
|
font-weight: 500; |
||||
|
border-radius: 6px; |
||||
|
margin-left: 8px; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
File diff suppressed because it is too large
@ -0,0 +1,765 @@ |
|||||
|
<template> |
||||
|
<div class="contract-sign-tab"> |
||||
|
<div class="contracts-wrapper"> |
||||
|
<!-- 左侧:筛选区域和合同列表 --> |
||||
|
<div class="contracts-sidebar"> |
||||
|
<!-- 头部筛选区域 --> |
||||
|
<div class="filter-section"> |
||||
|
<div class="filter-header"> |
||||
|
<h4>合同状态筛选</h4> |
||||
|
<el-button |
||||
|
type="primary" |
||||
|
size="small" |
||||
|
:loading="refreshing" |
||||
|
@click="refreshData" |
||||
|
> |
||||
|
刷新 |
||||
|
</el-button> |
||||
|
</div> |
||||
|
<el-radio-group v-model="activeStatus" size="small" @change="handleStatusChange"> |
||||
|
<el-radio-button :label="null">全部</el-radio-button> |
||||
|
<el-radio-button :label="1">未签署</el-radio-button> |
||||
|
<el-radio-button :label="2">已签署</el-radio-button> |
||||
|
<el-radio-button :label="3">已生效</el-radio-button> |
||||
|
<el-radio-button :label="4">已失效</el-radio-button> |
||||
|
</el-radio-group> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 合同列表 --> |
||||
|
<div class="contract-list" v-loading="loading"> |
||||
|
<div |
||||
|
v-for="contract in filteredContracts" |
||||
|
:key="contract.id" |
||||
|
class="contract-item" |
||||
|
:class="{ 'active': selectedContract?.id === contract.id }" |
||||
|
@click="selectContract(contract)" |
||||
|
> |
||||
|
<div class="contract-header"> |
||||
|
<div class="contract-title"> |
||||
|
{{ contract.contract_name }} |
||||
|
<el-tag v-if="contract.personnel_name" type="info" size="small" class="personnel-tag"> |
||||
|
{{ contract.personnel_name }} |
||||
|
</el-tag> |
||||
|
</div> |
||||
|
<div class="contract-status"> |
||||
|
<el-tag |
||||
|
:type="getStatusType(contract.sign_status)" |
||||
|
size="small" |
||||
|
> |
||||
|
{{ getStatusLabel(contract.sign_status) }} |
||||
|
</el-tag> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="contract-meta"> |
||||
|
<span class="contract-type">{{ contract.contract_type }}</span> |
||||
|
<span class="contract-date">{{ formatDate(contract.created_at) }}</span> |
||||
|
<span v-if="contract.file_size" class="file-size"> |
||||
|
{{ formatFileSize(contract.file_size) }} |
||||
|
</span> |
||||
|
</div> |
||||
|
|
||||
|
<div v-if="contract.remarks" class="contract-remarks"> |
||||
|
{{ contract.remarks }} |
||||
|
</div> |
||||
|
|
||||
|
<div class="contract-actions" v-if="contract.sign_status === 1"> |
||||
|
<el-button |
||||
|
type="primary" |
||||
|
size="small" |
||||
|
@click.stop="handleSignContract(contract)" |
||||
|
> |
||||
|
签署合同 |
||||
|
</el-button> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 空状态 --> |
||||
|
<div v-if="filteredContracts.length === 0" class="empty-state"> |
||||
|
<el-empty description="暂无合同数据" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 右侧:合同详情区域 --> |
||||
|
<div class="contract-detail" v-if="selectedContract"> |
||||
|
<div class="detail-header"> |
||||
|
<h3>{{ selectedContract.contract_name }}</h3> |
||||
|
<div class="detail-actions"> |
||||
|
<el-button @click="viewFullContract">查看完整合同</el-button> |
||||
|
<el-button |
||||
|
v-if="selectedContract.sign_status === 1" |
||||
|
type="primary" |
||||
|
@click="handleSignContract(selectedContract)" |
||||
|
> |
||||
|
签署合同 |
||||
|
</el-button> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="detail-content"> |
||||
|
<div class="contract-info"> |
||||
|
<el-descriptions :column="2" border> |
||||
|
<el-descriptions-item label="合同名称"> |
||||
|
{{ selectedContract.contract_name }} |
||||
|
</el-descriptions-item> |
||||
|
<el-descriptions-item label="合同类型"> |
||||
|
<el-tag :type="selectedContract.contract_type === '内部' ? 'success' : 'primary'" size="small"> |
||||
|
{{ selectedContract.contract_type }} |
||||
|
</el-tag> |
||||
|
</el-descriptions-item> |
||||
|
<el-descriptions-item label="签署状态"> |
||||
|
<el-tag :type="getStatusType(selectedContract.sign_status)"> |
||||
|
{{ getStatusLabel(selectedContract.sign_status) }} |
||||
|
</el-tag> |
||||
|
</el-descriptions-item> |
||||
|
<el-descriptions-item label="员工姓名" v-if="selectedContract.personnel_name"> |
||||
|
<el-tag type="info" size="small"> |
||||
|
{{ selectedContract.personnel_name }} |
||||
|
</el-tag> |
||||
|
</el-descriptions-item> |
||||
|
<el-descriptions-item label="创建时间"> |
||||
|
{{ formatDate(selectedContract.created_at) }} |
||||
|
</el-descriptions-item> |
||||
|
<el-descriptions-item label="签署时间" v-if="selectedContract.sign_time"> |
||||
|
{{ formatDate(selectedContract.sign_time) }} |
||||
|
</el-descriptions-item> |
||||
|
<el-descriptions-item label="文件大小" v-if="selectedContract.file_size"> |
||||
|
{{ formatFileSize(selectedContract.file_size) }} |
||||
|
</el-descriptions-item> |
||||
|
<el-descriptions-item label="原始文件名" v-if="selectedContract.original_filename"> |
||||
|
{{ selectedContract.original_filename }} |
||||
|
</el-descriptions-item> |
||||
|
<el-descriptions-item label="签名图片" v-if="selectedContract.signature_image"> |
||||
|
<el-image |
||||
|
:src="selectedContract.signature_image" |
||||
|
style="width: 80px; height: 40px;" |
||||
|
:preview-src-list="[selectedContract.signature_image]" |
||||
|
fit="contain" |
||||
|
/> |
||||
|
</el-descriptions-item> |
||||
|
</el-descriptions> |
||||
|
</div> |
||||
|
|
||||
|
<div v-if="selectedContract.remarks" class="contract-remarks-detail"> |
||||
|
<h4>合同备注</h4> |
||||
|
<div class="remarks-content">{{ selectedContract.remarks }}</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="contract-preview" v-if="selectedContract.contract_content"> |
||||
|
<div class="preview-header"> |
||||
|
<h4>合同内容预览</h4> |
||||
|
</div> |
||||
|
<div class="content-preview"> |
||||
|
{{ getContractPreview(selectedContract.contract_content) }} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 合同签署弹窗 --> |
||||
|
<ContractSignDialog |
||||
|
v-model:show="showSignDialog" |
||||
|
:contract-data="currentSignContract" |
||||
|
:personnel-id="personnelId" |
||||
|
@success="handleSignSuccess" |
||||
|
/> |
||||
|
|
||||
|
<!-- 合同详情弹窗 --> |
||||
|
<ContractDetailDialog |
||||
|
v-model:show="showContractDetailDialogFlag" |
||||
|
:contract-data="contractDetailData" |
||||
|
:personnel-id="personnelId" |
||||
|
@success="handleSignSuccess" |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts" setup> |
||||
|
import { ref, computed, onMounted, watch } from 'vue' |
||||
|
import { ElMessage } from 'element-plus' |
||||
|
import { getContractSignList, getContractSignInfo } from '@/app/api/contract_sign' |
||||
|
import ContractSignDialog from './ContractSignDialog.vue' |
||||
|
import ContractDetailDialog from './ContractDetailDialog.vue' |
||||
|
|
||||
|
interface Props { |
||||
|
personnelId: number | string |
||||
|
} |
||||
|
|
||||
|
interface ContractItem { |
||||
|
id: number |
||||
|
contract_name: string |
||||
|
contract_type: string |
||||
|
contract_status: string |
||||
|
created_at: string |
||||
|
sign_status?: number |
||||
|
sign_time?: string |
||||
|
signature_image?: string |
||||
|
source_type?: string |
||||
|
contract_content?: string |
||||
|
} |
||||
|
|
||||
|
const props = defineProps<Props>() |
||||
|
const emit = defineEmits(['refresh']) |
||||
|
|
||||
|
// 响应式数据 |
||||
|
const loading = ref(false) |
||||
|
const refreshing = ref(false) |
||||
|
const contractList = ref<ContractItem[]>([]) |
||||
|
const selectedContract = ref<ContractItem | null>(null) |
||||
|
const activeStatus = ref<number | null>(null) |
||||
|
const showSignDialog = ref(false) |
||||
|
const currentSignContract = ref<ContractItem | null>(null) |
||||
|
const showContractDetailDialogFlag = ref(false) |
||||
|
const contractDetailData = ref<any>(null) |
||||
|
|
||||
|
// 状态配置 |
||||
|
const statusConfig = { |
||||
|
1: { label: '未签署', type: 'warning' }, |
||||
|
2: { label: '已签署', type: 'success' }, |
||||
|
3: { label: '已生效', type: 'primary' }, |
||||
|
4: { label: '已失效', type: 'info' } |
||||
|
} |
||||
|
|
||||
|
const sourceTypeConfig = { |
||||
|
manual: '手动分发', |
||||
|
auto_course: '自动课程分发' |
||||
|
} |
||||
|
|
||||
|
// 计算属性 |
||||
|
const filteredContracts = computed(() => { |
||||
|
if (activeStatus.value === null) { |
||||
|
return contractList.value |
||||
|
} |
||||
|
return contractList.value.filter(contract => { |
||||
|
// 没有签署记录的算作未签署 |
||||
|
const status = contract.sign_status || 1 |
||||
|
return status === activeStatus.value |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
// 格式化日期 |
||||
|
const formatDate = (dateStr: string) => { |
||||
|
if (!dateStr) return '未设置' |
||||
|
try { |
||||
|
const date = new Date(dateStr) |
||||
|
return date.toLocaleDateString('zh-CN', { |
||||
|
year: 'numeric', |
||||
|
month: '2-digit', |
||||
|
'day': '2-digit' |
||||
|
}) |
||||
|
} catch (error) { |
||||
|
return dateStr |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 格式化文件大小 |
||||
|
const formatFileSize = (size: number) => { |
||||
|
if (!size || size === 0) return '' |
||||
|
const units = ['B', 'KB', 'MB', 'GB'] |
||||
|
let fileSize = size |
||||
|
let unitIndex = 0 |
||||
|
|
||||
|
while (fileSize >= 1024 && unitIndex < units.length - 1) { |
||||
|
fileSize /= 1024 |
||||
|
unitIndex++ |
||||
|
} |
||||
|
|
||||
|
return `${fileSize.toFixed(1)} ${units[unitIndex]}` |
||||
|
} |
||||
|
|
||||
|
// 方法 |
||||
|
const getStatusLabel = (status?: number) => { |
||||
|
if (!status) return '未签署' |
||||
|
return statusConfig[status as keyof typeof statusConfig]?.label || '未知' |
||||
|
} |
||||
|
|
||||
|
const getStatusType = (status?: number) => { |
||||
|
if (!status) return 'warning' |
||||
|
return statusConfig[status as keyof typeof statusConfig]?.type || 'info' |
||||
|
} |
||||
|
|
||||
|
const getSourceTypeLabel = (sourceType: string) => { |
||||
|
return sourceTypeConfig[sourceType as keyof typeof sourceTypeConfig] || sourceType |
||||
|
} |
||||
|
|
||||
|
const getContractPreview = (content: string) => { |
||||
|
if (!content) return '' |
||||
|
// 完整显示合同内容 |
||||
|
return content |
||||
|
} |
||||
|
|
||||
|
const loadContractList = async () => { |
||||
|
if (!props.personnelId) return |
||||
|
|
||||
|
loading.value = true |
||||
|
try { |
||||
|
const params = { |
||||
|
personnel_id: props.personnelId, |
||||
|
page: 1, |
||||
|
limit: 100 |
||||
|
} |
||||
|
|
||||
|
const response = await getContractSignList(params) |
||||
|
console.log('API响应数据:', response) // 调试日志 |
||||
|
|
||||
|
// 处理分页格式数据结构 |
||||
|
let dataList = [] |
||||
|
if (response.data && response.data.data && Array.isArray(response.data.data)) { |
||||
|
// 分页格式:{data: {data: [], total: 100}} |
||||
|
dataList = response.data.data |
||||
|
} else { |
||||
|
console.warn('未找到有效的数组数据,原始响应:', response) |
||||
|
dataList = [] |
||||
|
} |
||||
|
|
||||
|
if (dataList.length > 0) { |
||||
|
contractList.value = dataList.map((item: any) => ({ |
||||
|
id: item.id, // 这是合同签署记录的ID |
||||
|
contract_id: item.contract_id, |
||||
|
personnel_id: props.personnelId, |
||||
|
// 直接使用模型关联绑定返回的字段 |
||||
|
contract_name: item.contract_id_name || '未知合同', |
||||
|
contract_type: item.contract_type || '外部', |
||||
|
contract_status: item.contract_status || 'active', |
||||
|
created_at: item.created_at, |
||||
|
sign_status: item.status, |
||||
|
sign_time: item.sign_time, |
||||
|
signature_image: item.signature_image, |
||||
|
source_type: item.source_type, |
||||
|
contract_content: item.contract_content || '', |
||||
|
remarks: item.remarks || '', |
||||
|
original_filename: item.original_filename || '', |
||||
|
file_size: item.file_size || 0, |
||||
|
// 关联的员工信息 |
||||
|
personnel_name: item.personnel_id_name || '' |
||||
|
})) |
||||
|
} else { |
||||
|
contractList.value = [] |
||||
|
} |
||||
|
|
||||
|
console.log('处理后的合同列表:', contractList.value) // 调试日志 |
||||
|
} catch (error) { |
||||
|
console.error('获取合同列表失败:', error) |
||||
|
ElMessage.error('获取合同列表失败') |
||||
|
} finally { |
||||
|
loading.value = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const refreshData = async () => { |
||||
|
refreshing.value = true |
||||
|
await loadContractList() |
||||
|
refreshing.value = false |
||||
|
ElMessage.success('数据已刷新') |
||||
|
} |
||||
|
|
||||
|
const handleStatusChange = () => { |
||||
|
// 状态改变时,清空选中状态 |
||||
|
selectedContract.value = null |
||||
|
} |
||||
|
|
||||
|
const selectContract = (contract: ContractItem) => { |
||||
|
selectedContract.value = contract |
||||
|
} |
||||
|
|
||||
|
const viewFullContract = async () => { |
||||
|
console.log('=== viewFullContract 开始 ===') |
||||
|
console.log('选中的合同:', selectedContract.value) |
||||
|
|
||||
|
if (!selectedContract.value) return |
||||
|
|
||||
|
try { |
||||
|
// 找到对应的合同签署记录ID |
||||
|
const contractSignRecord = contractList.value.find( |
||||
|
item => item.contract_id === selectedContract.value?.contract_id |
||||
|
) |
||||
|
|
||||
|
if (!contractSignRecord?.id) { |
||||
|
// 如果没有签署记录,使用合同ID创建一个临时的查看 |
||||
|
console.log('没有签署记录,显示临时合同详情') |
||||
|
showContractDetailDialog(selectedContract.value) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 调用API获取完整的合同信息 |
||||
|
const response = await getContractSignInfo(contractSignRecord.id) |
||||
|
|
||||
|
if (response.data) { |
||||
|
console.log('获取到完整合同信息,显示详情弹窗') |
||||
|
showContractDetailDialog(response.data) |
||||
|
} else { |
||||
|
ElMessage.error('获取合同详情失败') |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error('查看完整合同失败:', error) |
||||
|
ElMessage.error('查看完整合同失败') |
||||
|
} |
||||
|
console.log('=== viewFullContract 结束 ===') |
||||
|
} |
||||
|
|
||||
|
const handleSignContract = (contract: ContractItem) => { |
||||
|
// 防止意外调用 |
||||
|
if (!contract) { |
||||
|
ElMessage.warning('请选择合同') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
console.log('=== handleSignContract 开始 ===') |
||||
|
console.log('传入的合同数据:', contract) |
||||
|
|
||||
|
currentSignContract.value = contract |
||||
|
console.log('currentSignContract 已设置:', currentSignContract.value) |
||||
|
|
||||
|
// 确保另一个弹窗是关闭的 |
||||
|
showContractDetailDialogFlag.value = false |
||||
|
console.log('已关闭详情弹窗') |
||||
|
|
||||
|
showSignDialog.value = true |
||||
|
console.log('showSignDialog 已设置为:', showSignDialog.value) |
||||
|
console.log('=== handleSignContract 结束 ===') |
||||
|
} |
||||
|
|
||||
|
const handleSignSuccess = () => { |
||||
|
// 签署成功后刷新数据 |
||||
|
loadContractList() |
||||
|
showSignDialog.value = false |
||||
|
currentSignContract.value = null |
||||
|
emit('refresh') |
||||
|
} |
||||
|
|
||||
|
const showContractDetailDialog = (data: any) => { |
||||
|
contractDetailData.value = data |
||||
|
showContractDetailDialogFlag.value = true |
||||
|
} |
||||
|
|
||||
|
const copyContractContent = async () => { |
||||
|
if (!selectedContract.value?.contract_content) return |
||||
|
|
||||
|
try { |
||||
|
// 使用 Clipboard API 复制内容 |
||||
|
await navigator.clipboard.writeText(selectedContract.value.contract_content) |
||||
|
ElMessage.success('合同内容已复制到剪贴板') |
||||
|
} catch (error) { |
||||
|
// 降级方案:使用传统的复制方法 |
||||
|
const textArea = document.createElement('textarea') |
||||
|
textArea.value = selectedContract.value.contract_content |
||||
|
textArea.style.position = 'fixed' |
||||
|
textArea.style.opacity = '0' |
||||
|
document.body.appendChild(textArea) |
||||
|
textArea.focus() |
||||
|
textArea.select() |
||||
|
|
||||
|
try { |
||||
|
document.execCommand('copy') |
||||
|
ElMessage.success('合同内容已复制到剪贴板') |
||||
|
} catch (err) { |
||||
|
ElMessage.error('复制失败,请手动选择复制') |
||||
|
} |
||||
|
|
||||
|
document.body.removeChild(textArea) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 监听人员ID变化 |
||||
|
watch(() => props.personnelId, (newId) => { |
||||
|
if (newId) { |
||||
|
loadContractList() |
||||
|
} |
||||
|
}, { immediate: true }) |
||||
|
|
||||
|
// 生命周期 - 移除重复调用 |
||||
|
onMounted(() => { |
||||
|
// 不再在这里调用,因为watch已经有immediate: true |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.contract-sign-tab { |
||||
|
display: flex; |
||||
|
height: 100%; |
||||
|
gap: 20px; |
||||
|
} |
||||
|
|
||||
|
.contracts-wrapper { |
||||
|
display: flex; |
||||
|
flex: 1; |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.contracts-sidebar { |
||||
|
flex: 0 0 400px; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
height: calc(100vh - 300px); |
||||
|
border: 1px solid #e9ecef; |
||||
|
border-radius: 8px; |
||||
|
background: #fff; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.filter-section { |
||||
|
padding: 16px; |
||||
|
background: #fafbfc; |
||||
|
border-bottom: 1px solid #e9ecef; |
||||
|
} |
||||
|
|
||||
|
.filter-header { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
margin-bottom: 12px; |
||||
|
|
||||
|
h4 { |
||||
|
margin: 0; |
||||
|
font-size: 14px; |
||||
|
font-weight: 600; |
||||
|
color: #333; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.filter-section .el-radio-group { |
||||
|
display: flex; |
||||
|
flex-wrap: wrap; |
||||
|
gap: 8px; |
||||
|
} |
||||
|
|
||||
|
.contract-list { |
||||
|
flex: 1; |
||||
|
overflow-y: auto; |
||||
|
background: #fff; |
||||
|
|
||||
|
.contract-item { |
||||
|
padding: 16px; |
||||
|
border-bottom: 1px solid #f0f0f0; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.2s ease; |
||||
|
|
||||
|
&:hover { |
||||
|
background: #f8f9fa; |
||||
|
} |
||||
|
|
||||
|
&.active { |
||||
|
background: #e3f2fd; |
||||
|
border-color: #2196f3; |
||||
|
} |
||||
|
|
||||
|
&:last-child { |
||||
|
border-bottom: none; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.contract-header { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: flex-start; |
||||
|
margin-bottom: 8px; |
||||
|
|
||||
|
.contract-title { |
||||
|
flex: 1; |
||||
|
font-weight: 500; |
||||
|
color: #333; |
||||
|
font-size: 14px; |
||||
|
line-height: 1.4; |
||||
|
margin-right: 8px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.contract-meta { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
margin-bottom: 8px; |
||||
|
|
||||
|
.contract-type { |
||||
|
font-size: 12px; |
||||
|
color: #666; |
||||
|
background: #f5f5f5; |
||||
|
padding: 2px 6px; |
||||
|
border-radius: 3px; |
||||
|
} |
||||
|
|
||||
|
.contract-date { |
||||
|
font-size: 12px; |
||||
|
color: #999; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.contract-actions { |
||||
|
text-align: right; |
||||
|
} |
||||
|
|
||||
|
.empty-state { |
||||
|
padding: 40px 20px; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.personnel-tag { |
||||
|
margin-left: 8px; |
||||
|
} |
||||
|
|
||||
|
.file-size { |
||||
|
font-size: 12px; |
||||
|
color: #999; |
||||
|
background: #f0f0f0; |
||||
|
padding: 2px 6px; |
||||
|
border-radius: 3px; |
||||
|
} |
||||
|
|
||||
|
.contract-remarks { |
||||
|
margin-top: 8px; |
||||
|
padding: 8px; |
||||
|
background: #f8f9fa; |
||||
|
border-radius: 4px; |
||||
|
font-size: 12px; |
||||
|
color: #666; |
||||
|
line-height: 1.4; |
||||
|
border-left: 3px solid #e9ecef; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.contract-detail { |
||||
|
flex: 1; |
||||
|
min-width: 0; |
||||
|
height: calc(100vh - 300px); |
||||
|
overflow-y: auto; |
||||
|
border: 1px solid #e9ecef; |
||||
|
border-radius: 8px; |
||||
|
background: #fff; |
||||
|
|
||||
|
.detail-header { |
||||
|
padding: 20px; |
||||
|
border-bottom: 1px solid #f0f0f0; |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
|
||||
|
h3 { |
||||
|
margin: 0; |
||||
|
font-size: 18px; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
.detail-actions { |
||||
|
display: flex; |
||||
|
gap: 12px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.detail-content { |
||||
|
padding: 20px; |
||||
|
|
||||
|
.contract-info { |
||||
|
margin-bottom: 24px; |
||||
|
} |
||||
|
|
||||
|
.contract-preview { |
||||
|
.preview-header { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
margin-bottom: 12px; |
||||
|
|
||||
|
h4 { |
||||
|
margin: 0; |
||||
|
font-size: 16px; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
.preview-actions { |
||||
|
display: flex; |
||||
|
gap: 8px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.content-preview { |
||||
|
padding: 20px; |
||||
|
background: #f8f9fa; |
||||
|
border: 1px solid #e9ecef; |
||||
|
border-radius: 6px; |
||||
|
font-size: 14px; |
||||
|
line-height: 1.8; |
||||
|
color: #333; |
||||
|
white-space: pre-wrap; |
||||
|
word-wrap: break-word; |
||||
|
min-height: 400px; |
||||
|
max-height: 70vh; |
||||
|
overflow-y: auto; |
||||
|
|
||||
|
// 美化滚动条样式 |
||||
|
&::-webkit-scrollbar { |
||||
|
width: 8px; |
||||
|
} |
||||
|
|
||||
|
&::-webkit-scrollbar-track { |
||||
|
background: #f1f1f1; |
||||
|
border-radius: 4px; |
||||
|
} |
||||
|
|
||||
|
&::-webkit-scrollbar-thumb { |
||||
|
background: #c1c1c1; |
||||
|
border-radius: 4px; |
||||
|
|
||||
|
&:hover { |
||||
|
background: #a8a8a8; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 美化下划线样式,使其更明显 |
||||
|
:deep(_) { |
||||
|
color: #1890ff; |
||||
|
font-weight: 500; |
||||
|
background: #e6f7ff; |
||||
|
padding: 0 2px; |
||||
|
border-radius: 2px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.contract-remarks-detail { |
||||
|
margin-bottom: 24px; |
||||
|
|
||||
|
h4 { |
||||
|
margin: 0 0 12px; |
||||
|
font-size: 16px; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
.remarks-content { |
||||
|
padding: 12px 16px; |
||||
|
background: #f8f9fa; |
||||
|
border: 1px solid #e9ecef; |
||||
|
border-radius: 6px; |
||||
|
font-size: 14px; |
||||
|
line-height: 1.6; |
||||
|
color: #666; |
||||
|
white-space: pre-wrap; |
||||
|
border-left: 4px solid #007bff; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 美化滚动条 |
||||
|
.contract-list, |
||||
|
.contract-detail { |
||||
|
&::-webkit-scrollbar { |
||||
|
width: 6px; |
||||
|
} |
||||
|
|
||||
|
&::-webkit-scrollbar-track { |
||||
|
background: #f1f1f1; |
||||
|
border-radius: 3px; |
||||
|
} |
||||
|
|
||||
|
&::-webkit-scrollbar-thumb { |
||||
|
background: #c1c1c1; |
||||
|
border-radius: 3px; |
||||
|
|
||||
|
&:hover { |
||||
|
background: #a8a8a8; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
Loading…
Reference in new issue