This commit is contained in:
Mr.j 2025-08-12 17:49:48 +08:00
commit 05f09ad207
11 changed files with 4642 additions and 1347 deletions

View File

@ -117,17 +117,22 @@ export interface ProjectKanbanStats {
/** 项目看板数据 */
export interface ProjectKanbanData {
inProgressProjects: never[]
inProgressProjects: ProjectCard[]
preparingProjects: ProjectCard[]
ongoingProjects: ProjectCard[]
pendingProjects: ProjectCard[]
suspendedProjects: ProjectCard[]
completedProjects: ProjectCard[]
acceptanceProjects: ProjectCard[]
collectionProjects: ProjectCard[]
settledProjects: ProjectCard[]
}
/** 项目卡片信息 */
export interface ProjectCard {
id: string | number
name: string
status: 'preparing' | 'ongoing' | 'pending'
status: number | 'preparing' | 'ongoing' | 'pending' | 'inProgress' | 'suspended' | 'completed' | 'acceptance' | 'collection' | 'settled'
budget: number
manager: string
teamSize: number
@ -139,6 +144,9 @@ export interface ProjectCard {
alerts?: ProjectAlert[]
teamMembers: TeamMemberResp[]
requirements: ProjectRequirementResp[]
farmName?: string
scale?: string
createTime?: string
}
/** 项目异常信息 */

View File

@ -70,6 +70,6 @@ declare global {
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
import('vue')
}

View File

@ -7,7 +7,69 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
ApprovalAssistant: typeof import('./../components/ApprovalAssistant/index.vue')['default']
ApprovalMessageItem: typeof import('./../components/NotificationCenter/ApprovalMessageItem.vue')['default']
Avatar: typeof import('./../components/Avatar/index.vue')['default']
Breadcrumb: typeof import('./../components/Breadcrumb/index.vue')['default']
CellCopy: typeof import('./../components/CellCopy/index.vue')['default']
Chart: typeof import('./../components/Chart/index.vue')['default']
ColumnSetting: typeof import('./../components/GiTable/src/components/ColumnSetting.vue')['default']
CronForm: typeof import('./../components/GenCron/CronForm/index.vue')['default']
CronModal: typeof import('./../components/GenCron/CronModal/index.vue')['default']
DateRangePicker: typeof import('./../components/DateRangePicker/index.vue')['default']
DayForm: typeof import('./../components/GenCron/CronForm/component/day-form.vue')['default']
FilePreview: typeof import('./../components/FilePreview/index.vue')['default']
GiCellAvatar: typeof import('./../components/GiCell/GiCellAvatar.vue')['default']
GiCellGender: typeof import('./../components/GiCell/GiCellGender.vue')['default']
GiCellStatus: typeof import('./../components/GiCell/GiCellStatus.vue')['default']
GiCellTag: typeof import('./../components/GiCell/GiCellTag.vue')['default']
GiCellTags: typeof import('./../components/GiCell/GiCellTags.vue')['default']
GiCodeView: typeof import('./../components/GiCodeView/index.vue')['default']
GiDot: typeof import('./../components/GiDot/index.tsx')['default']
GiEditTable: typeof import('./../components/GiEditTable/GiEditTable.vue')['default']
GiFooter: typeof import('./../components/GiFooter/index.vue')['default']
GiForm: typeof import('./../components/GiForm/src/GiForm.vue')['default']
GiIconBox: typeof import('./../components/GiIconBox/index.vue')['default']
GiIconSelector: typeof import('./../components/GiIconSelector/index.vue')['default']
GiIframe: typeof import('./../components/GiIframe/index.vue')['default']
GiOption: typeof import('./../components/GiOption/index.vue')['default']
GiOptionItem: typeof import('./../components/GiOptionItem/index.vue')['default']
GiPageLayout: typeof import('./../components/GiPageLayout/index.vue')['default']
GiSpace: typeof import('./../components/GiSpace/index.vue')['default']
GiSplitButton: typeof import('./../components/GiSplitButton/index.vue')['default']
GiSplitPane: typeof import('./../components/GiSplitPane/index.vue')['default']
GiSplitPaneFlexibleBox: typeof import('./../components/GiSplitPane/components/GiSplitPaneFlexibleBox.vue')['default']
GiSvgIcon: typeof import('./../components/GiSvgIcon/index.vue')['default']
GiTable: typeof import('./../components/GiTable/src/GiTable.vue')['default']
GiTag: typeof import('./../components/GiTag/index.tsx')['default']
GiThemeBtn: typeof import('./../components/GiThemeBtn/index.vue')['default']
HourForm: typeof import('./../components/GenCron/CronForm/component/hour-form.vue')['default']
Icon403: typeof import('./../components/icons/Icon403.vue')['default']
Icon404: typeof import('./../components/icons/Icon404.vue')['default']
Icon500: typeof import('./../components/icons/Icon500.vue')['default']
IconBorders: typeof import('./../components/icons/IconBorders.vue')['default']
IconTableSize: typeof import('./../components/icons/IconTableSize.vue')['default']
IconTreeAdd: typeof import('./../components/icons/IconTreeAdd.vue')['default']
IconTreeReduce: typeof import('./../components/icons/IconTreeReduce.vue')['default']
ImageImport: typeof import('./../components/ImageImport/index.vue')['default']
ImageImportWizard: typeof import('./../components/ImageImportWizard/index.vue')['default']
IndustrialImageList: typeof import('./../components/IndustrialImageList/index.vue')['default']
JsonPretty: typeof import('./../components/JsonPretty/index.vue')['default']
MinuteForm: typeof import('./../components/GenCron/CronForm/component/minute-form.vue')['default']
MonthForm: typeof import('./../components/GenCron/CronForm/component/month-form.vue')['default']
NotificationCenter: typeof import('./../components/NotificationCenter/index.vue')['default']
ParentView: typeof import('./../components/ParentView/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SecondForm: typeof import('./../components/GenCron/CronForm/component/second-form.vue')['default']
SplitPanel: typeof import('./../components/SplitPanel/index.vue')['default']
TextCopy: typeof import('./../components/TextCopy/index.vue')['default']
TurbineGrid: typeof import('./../components/TurbineGrid/index.vue')['default']
UserSelect: typeof import('./../components/UserSelect/index.vue')['default']
Verify: typeof import('./../components/Verify/index.vue')['default']
VerifyPoints: typeof import('./../components/Verify/Verify/VerifyPoints.vue')['default']
VerifySlide: typeof import('./../components/Verify/Verify/VerifySlide.vue')['default']
WeekForm: typeof import('./../components/GenCron/CronForm/component/week-form.vue')['default']
YearForm: typeof import('./../components/GenCron/CronForm/component/year-form.vue')['default']
}
}

View File

@ -4,10 +4,9 @@
<a-layout-sider
width="260"
:collapsed-width="80"
theme="light"
theme="dark"
class="folder-sidebar"
:collapsed="sidebarCollapsed"
collapsible
@collapse="handleSidebarCollapse"
@expand="handleSidebarExpand"
>
@ -102,29 +101,7 @@
</div>
</div>
<!-- 侧边栏底部分页控件 -->
<div class="sidebar-footer" v-if="!sidebarCollapsed && folderList.length > 0">
<div class="pagination-info">
<a-typography-text type="secondary" size="small">
{{ totalFolders }} 个文件夹
</a-typography-text>
</div>
<!-- 隐藏分页控件因为现在获取所有文件夹 -->
<!-- <div class="pagination-controls">
<a-pagination
:current="currentPage"
:page-size="pageSize"
:total="totalFolders"
:show-size-changer="true"
:page-size-options="['10', '20', '50', '100']"
@change="handlePageChange"
@showSizeChange="handlePageSizeChange"
size="small"
show-total
/>
</div> -->
</div>
</a-layout-sider>
<a-layout>
@ -305,7 +282,7 @@
<!-- 大小列 -->
<a-col :span="3" class="table-column size-column">
<div class="cell-content">{{ formatFileSize(file.fileSize || file.size) }}</div>
<div class="cell-content">{{ formatFileListSize(file.fileSize || file.size) }}</div>
</a-col>
<!-- 时间列 -->
@ -682,7 +659,7 @@ const fileListTemp = ref([]);
const folderFormRef = ref(null);
const uploadFormRef = ref(null);
const uploadRef = ref(null);
const folderColor = '#165DFF';
const folderColor = 'var(--color-primary)';
const refreshing = ref(false);
const folderSubmitting = ref(false);
const uploading = ref(false);
@ -1624,7 +1601,7 @@ const fileColor = (extension) => {
bmp: '#722ed1',
webp: '#13c2c2'
};
return colorMap[extension.toLowerCase()] || '#8c8c8c';
return colorMap[extension.toLowerCase()] || 'var(--color-text-3)';
};
@ -1881,8 +1858,8 @@ const showTextPreview = async (blob, fileName) => {
maxWidth: '100%',
maxHeight: '70vh',
overflow: 'auto',
backgroundColor: '#f8f9fa',
border: '1px solid #e9ecef',
backgroundColor: 'var(--color-fill-1)',
border: '1px solid var(--color-border)',
borderRadius: '8px',
padding: '20px',
fontFamily: "'Consolas', 'Monaco', 'Courier New', monospace",
@ -1890,7 +1867,7 @@ const showTextPreview = async (blob, fileName) => {
lineHeight: '1.6',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
color: '#333',
color: 'var(--color-text-1)',
textAlign: 'left'
}
}, text)
@ -2336,6 +2313,21 @@ const formatFileSize = (fileSize) => {
return `${(size / (1024 * 1024 * 1024)).toFixed(1)} GB`;
};
// KB
const formatFileListSize = (fileSize) => {
const size = Number(fileSize);
if (isNaN(size) || size < 0) return '未知';
// KB
if (size < 1024) {
return `${size} KB`;
} else if (size < 1024 * 1024) {
return `${(size / 1024).toFixed(1)} MB`;
} else {
return `${(size / (1024 * 1024)).toFixed(1)} GB`;
}
};
const fileTypeText = (type) => {
@ -2458,7 +2450,7 @@ onMounted(() => {
<style scoped>
.knowledge-container {
height: 100vh;
background-color: var(--color-bg-2);
background-color: var(--color-bg-1);
}
@ -2470,13 +2462,15 @@ onMounted(() => {
transition: all 0.3s ease;
overflow: hidden;
position: relative;
background: linear-gradient(180deg, #ffffff 0%, #fafbfc 100%);
background: var(--color-bg-1);
display: flex;
flex-direction: column;
}
.sidebar-header {
padding: 20px 16px;
border-bottom: 1px solid var(--color-border);
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
background: var(--color-bg-1);
position: relative;
&::after {
@ -2492,12 +2486,18 @@ onMounted(() => {
.folder-content {
padding: 16px 0;
height: calc(100vh - 320px); /* 为底部分页控件留出更多空间,因为文件夹项现在更高 */
flex: 1;
overflow-y: auto;
scrollbar-width: thin;
background: rgba(255, 255, 255, 0.6);
background: var(--color-bg-1);
display: flex;
flex-direction: column;
min-height: 0;
max-height: calc(100vh - 200px);
}
.folder-content::-webkit-scrollbar {
width: 8px;
}
@ -2537,7 +2537,7 @@ onMounted(() => {
border: 1px solid transparent;
&:hover {
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
background: linear-gradient(135deg, var(--color-fill-2) 0%, var(--color-fill-3) 100%);
border-color: var(--color-primary-light-2);
transform: translateX(2px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
@ -2606,7 +2606,7 @@ onMounted(() => {
justify-content: space-between;
align-items: center;
padding: 0 24px;
background: var(--color-bg-2);
background: var(--color-bg-1);
border-bottom: 1px solid var(--color-border);
height: 64px;
}
@ -2623,10 +2623,12 @@ onMounted(() => {
display: flex;
flex-direction: column;
padding: 24px;
overflow: auto;
overflow: hidden;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
background: var(--color-bg-2);
background: var(--color-bg-1);
min-height: 0;
max-height: calc(100vh - 120px);
}
.file-card {
@ -2637,6 +2639,7 @@ onMounted(() => {
flex-direction: column;
position: relative;
height: 100%;
overflow: hidden;
}
/* 表格容器 */
@ -2649,7 +2652,9 @@ onMounted(() => {
overflow-y: auto;
background-color: var(--color-bg-1);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
padding-bottom: 80px; /* 为分页留出空间 */
margin-bottom: 0;
min-height: 300px;
max-height: calc(100vh - 300px);
}
/* 表头行样式 */
@ -2729,8 +2734,8 @@ onMounted(() => {
}
.folder-icon {
color: #165DFF;
background-color: #E8F3FF;
color: var(--color-primary);
background-color: var(--color-primary-light-1);
}
.file-name {
@ -2744,7 +2749,7 @@ onMounted(() => {
}
.table-data-row:hover .file-name {
color: #165DFF;
color: var(--color-primary);
}
.type-column, .size-column, .time-column {
@ -2789,7 +2794,7 @@ onMounted(() => {
white-space: nowrap;
.table-data-row:hover & {
color: #165DFF;
color: var(--color-primary);
}
}
@ -2922,6 +2927,35 @@ onMounted(() => {
}
}
/* 浏览器缩放调整 */
@media (max-height: 800px) {
.folder-content {
max-height: calc(100vh - 180px);
}
.file-content {
max-height: calc(100vh - 100px);
}
.file-grid-container {
max-height: calc(100vh - 280px);
}
}
@media (max-height: 600px) {
.folder-content {
max-height: calc(100vh - 160px);
}
.file-content {
max-height: calc(100vh - 80px);
}
.file-grid-container {
max-height: calc(100vh - 260px);
}
}
/* 空状态样式 */
.initial-state, .empty-state {
display: flex;
@ -2930,7 +2964,7 @@ onMounted(() => {
justify-content: center;
padding: 64px 0;
color: var(--color-text-3);
background-color: #fafafa;
background-color: var(--color-fill-1);
border-radius: 8px;
text-align: center;
}
@ -2944,7 +2978,7 @@ onMounted(() => {
:deep(.empty-state .arco-btn) {
margin-top: 16px;
padding: 8px 16px;
background-color: #165DFF;
background-color: var(--color-primary);
color: white;
border-radius: 4px;
border: none;
@ -2954,7 +2988,7 @@ onMounted(() => {
}
:deep(.empty-state .arco-btn:hover) {
background-color: #0E42D2;
background-color: var(--color-primary-dark-1);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
@ -3076,50 +3110,7 @@ onMounted(() => {
border-top: 1px solid var(--color-border);
}
/* 侧边栏底部分页样式 */
.sidebar-footer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(180deg, #f8fafc 0%, #e2e8f0 100%);
border-top: 1px solid var(--color-border);
padding: 20px 16px;
z-index: 10;
backdrop-filter: blur(10px);
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
}
.pagination-info {
margin-bottom: 16px;
text-align: center;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.8);
border-radius: 8px;
border: 1px solid var(--color-border);
}
.pagination-controls {
display: flex;
justify-content: center;
:deep(.arco-pagination) {
.arco-pagination-item {
border-radius: 6px;
transition: all 0.2s ease;
&:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
}
}
}
/* 确保文件夹内容区域不被底部分页遮挡 */
.folder-content {
/* 高度已调整无需额外padding */
}
/* 动画效果 */
:deep(.arco-icon-refresh.spin) {
@ -3250,8 +3241,8 @@ onMounted(() => {
border: 1px solid #e2e8f0;
&:hover {
background: #e2e8f0;
color: #165DFF;
background: var(--color-fill-2);
color: var(--color-primary);
}
}
@ -3260,8 +3251,8 @@ onMounted(() => {
transition: all 0.2s ease;
&:hover {
border-color: #165DFF;
color: #165DFF;
border-color: var(--color-primary);
color: var(--color-primary);
}
&:active {
@ -3370,7 +3361,7 @@ onMounted(() => {
border-radius: 8px;
border: 1px solid var(--color-border);
transition: all 0.3s ease;
background: rgba(255, 255, 255, 0.9);
background: rgba(var(--color-bg-1-rgb), 0.9);
&:hover {
border-color: var(--color-primary-light-2);
@ -3388,7 +3379,7 @@ onMounted(() => {
.search-result-tip {
padding: 12px 16px;
margin: 12px 16px;
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
background: linear-gradient(135deg, var(--color-primary-light-1) 0%, var(--color-primary-light-2) 100%);
border-radius: 8px;
border-left: 4px solid var(--color-primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
@ -3402,7 +3393,7 @@ onMounted(() => {
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(45deg, transparent 30%, rgba(255, 255, 255, 0.1) 50%, transparent 70%);
background: linear-gradient(45deg, transparent 30%, rgba(var(--color-bg-1-rgb), 0.1) 50%, transparent 70%);
animation: shimmer 2s infinite;
}
}
@ -3440,7 +3431,7 @@ onMounted(() => {
margin-top: 16px;
border: 1px solid var(--color-border);
border-radius: 8px;
background: var(--color-bg-2);
background: var(--color-bg-1);
max-height: 300px;
overflow-y: auto;
}
@ -3612,17 +3603,18 @@ onMounted(() => {
/* 文件分页样式 */
.file-pagination {
position: absolute;
position: sticky;
bottom: 0;
left: 0;
right: 0;
z-index: 10;
margin-top: 0;
margin-top: 16px;
padding: 16px 0;
display: flex;
justify-content: center;
border-top: 1px solid var(--color-border);
background: var(--color-bg-1);
flex-shrink: 0;
z-index: 10;
.arco-pagination {
.arco-pagination-total {
@ -3662,7 +3654,7 @@ onMounted(() => {
/* 树形文件夹结构 */
.folder-tree-container {
padding: 8px;
background: var(--color-bg-2);
background: var(--color-bg-1);
border-radius: 6px;
margin: 8px;
overflow: hidden;

View File

@ -35,11 +35,11 @@
<a-col :span="12">
<a-form-item field="contractStatus" label="合同状态">
<a-select v-model="contractData.contractStatus">
<a-option value="未确认">未确认</a-option>
<a-option value="待审批">待审批</a-option>
<a-option value="已签署">已签署</a-option>
<a-option value="未执行">未执行</a-option>
<a-option value="执行中">执行中</a-option>
<a-option value="已完成">已完成</a-option>
<a-option value="验收中">验收中</a-option>
<a-option value="结算中">结算中</a-option>
<a-option value="已结算">已结算</a-option>
<a-option value="已终止">已终止</a-option>
</a-select>
</a-form-item>

View File

@ -48,18 +48,23 @@
<span class="font-medium text-green-600">{{ (record.amount || 0).toLocaleString() }}</span>
</template>
<!-- 收款金额 -->
<template #receivedAmount="{ record }">
<span class="font-medium text-blue-600">{{ (record.receivedAmount || 0).toLocaleString() }}</span>
<!-- 结算金额支出合同 -->
<template #settlementAmount="{ record }">
<span class="font-medium text-blue-600">{{ (record.settlementAmount || record.receivedAmount || 0).toLocaleString() }}</span>
</template>
<template #action="{ record }">
<a-space>
<a-link @click="viewDetail(record)">查看</a-link>
<a-link @click="editRecord(record)">编辑</a-link>
<a-link :disabled="record.contractStatus === '已结算'" @click="!(record.contractStatus === '已结算') && openSettlement(record)">结算</a-link>
<a-link status="danger" @click="deleteContract(record)">删除</a-link>
</a-space>
</template>
<!-- 日期展示仅年月日放在 action 模板之外作为独立列插槽 -->
<template #signDate="{ record }">{{ (record.signDate || '').slice(0,10) }}</template>
<template #performanceDeadline="{ record }">{{ (record.performanceDeadline || '').slice(0,10) }}</template>
<template #paymentDate="{ record }">{{ (record.paymentDate || '').slice(0,10) }}</template>
</GiTable>
<!-- 合同详情弹窗 -->
@ -102,6 +107,15 @@
@update:contract-data="handleNewContractDataUpdate"
/>
</a-modal>
<!-- 合同结算弹窗 -->
<a-modal v-model:visible="showSettlementModal" title="合同结算" :width="520" @before-ok="submitSettlement">
<a-form :model="settlementForm" layout="vertical">
<a-form-item field="amount" label="结算金额"><a-input-number v-model="settlementForm.amount" style="width:100%" /></a-form-item>
<a-form-item field="paymentDate" label="付款日期"><a-date-picker v-model="settlementForm.paymentDate" show-time value-format="YYYY-MM-DD HH:mm:ss" style="width:100%"/></a-form-item>
<a-form-item field="paymentPeriod" label="账期"><a-date-picker v-model="settlementForm.paymentPeriod" show-time value-format="YYYY-MM-DD HH:mm:ss" style="width:100%"/></a-form-item>
<a-form-item field="notes" label="备注"><a-textarea v-model="settlementForm.notes" /></a-form-item>
</a-form>
</a-modal>
</GiPageLayout>
</template>
@ -173,11 +187,11 @@ const queryFormColumns = [
props: {
placeholder: '请选择合同状态',
options: [
{ label: '未确认', value: '未确认' },
{ label: '待审批', value: '待审批' },
{ label: '已签署', value: '已签署' },
{ label: '未执行', value: '未执行' },
{ label: '执行中', value: '执行中' },
{ label: '已完成', value: '已完成' },
{ label: '验收中', value: '验收中' },
{ label: '结算中', value: '结算中' },
{ label: '已结算', value: '已结算' },
{ label: '已终止', value: '已终止' },
],
},
@ -187,9 +201,8 @@ const queryFormColumns = [
label: '签署时间',
type: 'range-picker' as const,
props: {
placeholder: ['开始时间', '结束时间'],
showTime: true,
format: 'YYYY-MM-DD HH:mm:ss',
placeholder: ['开始日期', '结束日期'],
format: 'YYYY-MM-DD',
},
},
]
@ -200,11 +213,11 @@ const tableColumns: TableColumnData[] = [
{ title: '项目名称', dataIndex: 'projectName', width: 250, ellipsis: true, tooltip: true },
{ title: '客户名称', dataIndex: 'customer', width: 200, ellipsis: true, tooltip: true },
{ title: '合同金额', dataIndex: 'amount', slotName: 'contractAmount', width: 120 },
{ title: '已收款金额', dataIndex: 'receivedAmount', slotName: 'receivedAmount', width: 120 },
{ title: '未收款金额', dataIndex: 'pendingAmount', width: 120 },
{ title: '签署日期', dataIndex: 'signDate', width: 120 },
{ title: '履约期限', dataIndex: 'performanceDeadline', width: 120 },
{ title: '付款日期', dataIndex: 'paymentDate', width: 120 },
{ title: '已结算金额', dataIndex: 'settlementAmount', slotName: 'settlementAmount', width: 120 },
{ title: '未结算金额', dataIndex: 'pendingAmount', width: 120 },
{ title: '签署日期', dataIndex: 'signDate', slotName: 'signDate', width: 120 },
{ title: '履约期限', dataIndex: 'performanceDeadline', slotName: 'performanceDeadline', width: 120 },
{ title: '付款日期', dataIndex: 'paymentDate', slotName: 'paymentDate', width: 120 },
{ title: '合同状态', dataIndex: 'contractStatus', slotName: 'status', width: 100 },
{ title: '销售人员', dataIndex: 'salespersonName', width: 100 },
{ title: '销售部门', dataIndex: 'salespersonDeptName', width: 100 },
@ -252,10 +265,10 @@ const fetchContractList = async () => {
}
}
//
//
dataList.value = filtered.map((item: ContractItem) => ({
...item,
pendingAmount: (item.amount || 0) - (item.receivedAmount || 0),
pendingAmount: (item.amount || 0) - ((item.settlementAmount || item.receivedAmount || 0)),
}))
//
@ -284,11 +297,11 @@ const pagination = reactive({
//
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
未确认: 'gray',
待审批: 'orange',
已签署: 'blue',
未执行: 'gray',
执行中: 'cyan',
已完成: 'green',
验收中: 'arcoblue',
结算中: 'orange',
已结算: 'green',
已终止: 'red',
}
return colorMap[status] || 'gray'
@ -381,7 +394,7 @@ const openAddModal = () => {
amount: 0,
accountNumber: '',
notes: '',
contractStatus: '未确认',
contractStatus: '未执行',
contractText: '',
projectName: '',
salespersonName: null,
@ -477,8 +490,8 @@ const editRecord = (record: ContractItem) => {
...record,
amount: record.amount || 0,
projectId: record.projectId || '',
type: record.type || '收入合同',
contractStatus: record.contractStatus || '未确认',
type: record.type || '支出合同',
contractStatus: record.contractStatus || '未执行',
}
selectedContractData.value = completeRecord
@ -548,6 +561,91 @@ const handleEditSubmit = async () => {
}
}
//
const showSettlementModal = ref(false)
const settlementForm = reactive({
//
contractId: '',
amount: 0,
paymentDate: '',
paymentPeriod: '',
notes: '',
//
accountNumber: '',
code: '',
customer: '',
departmentId: '',
duration: '',
productService: '',
projectId: '',
salespersonId: '',
settlementId: '',
settlementStatus: '',
})
const settlementRecord = ref<ContractItem | null>(null)
const openSettlement = (record: ContractItem) => {
if (record.contractStatus === '已结算') return
settlementRecord.value = record
Object.assign(settlementForm, {
//
contractId: record.contractId,
amount: record.amount || 0,
paymentDate: record.paymentDate || '',
paymentPeriod: '',
notes: '',
// 便
accountNumber: (record as any).accountNumber || '',
code: (record as any).code || '',
customer: (record as any).customer || '',
departmentId: (record as any).departmentId || '',
duration: (record as any).duration || '',
productService: (record as any).productService || '',
projectId: (record as any).projectId || '',
salespersonId: (record as any).salespersonId || '',
settlementId: (record as any).settlementId || '',
settlementStatus: (record as any).settlementStatus || '',
})
showSettlementModal.value = true
}
const submitSettlement = async () => {
try {
const payload:any = {
accountNumber: settlementForm.accountNumber || '',
amount: settlementForm.amount,
code: settlementForm.code || '',
contractId: settlementForm.contractId,
customer: settlementForm.customer || '',
departmentId: settlementForm.departmentId || '',
duration: settlementForm.duration || '',
notes: settlementForm.notes || '',
paymentDate: settlementForm.paymentDate || null,
paymentPeriod: settlementForm.paymentPeriod || '',
productService: settlementForm.productService || '',
projectId: settlementForm.projectId || '',
salespersonId: settlementForm.salespersonId || '',
settlementId: settlementForm.settlementId || '',
settlementStatus: settlementForm.settlementStatus || '',
}
const res = await http.post('/contract-settlement', payload)
if (res.code === 200 || (res as any).status === 200) {
Message.success('合同结算成功')
// 0 0 => /
const origin = settlementRecord.value
const settledAmount = (origin?.settlementAmount ?? origin?.receivedAmount ?? 0) + (settlementForm.amount || 0)
const pendingAfter = Math.max((origin?.amount || 0) - settledAmount, 0)
const targetStatus = pendingAfter === 0 ? '已结算' : '结算中'
await http.put('/contract', { contractId: settlementForm.contractId, contractStatus: targetStatus, type: origin?.type || '支出合同' })
showSettlementModal.value = false
search()
return true
}
Message.error(res.msg || '合同结算失败')
return false
} catch (e:any) {
Message.error(e?.message || '合同结算失败')
return false
}
}
//
const deleteContract = (record: ContractItem) => {

View File

@ -35,11 +35,11 @@
<a-col :span="12">
<a-form-item field="contractStatus" label="合同状态">
<a-select v-model="contractData.contractStatus">
<a-option value="未确认">未确认</a-option>
<a-option value="待审批">待审批</a-option>
<a-option value="已签署">已签署</a-option>
<a-option value="未执行">未执行</a-option>
<a-option value="执行中">执行中</a-option>
<a-option value="已完成">已完成</a-option>
<a-option value="验收中">验收中</a-option>
<a-option value="结算中">结算中</a-option>
<a-option value="已结算">已结算</a-option>
<a-option value="已终止">已终止</a-option>
</a-select>
</a-form-item>

View File

@ -48,18 +48,24 @@
<span class="font-medium text-green-600">{{ (record.amount || 0).toLocaleString() }}</span>
</template>
<!-- 收款金额 -->
<!-- 回款金额收入合同 -->
<template #receivedAmount="{ record }">
<span class="font-medium text-blue-600">{{ (record.receivedAmount || 0).toLocaleString() }}</span>
<span class="font-medium text-blue-600">{{ (record.receivedAmount || record.settlementAmount || 0).toLocaleString() }}</span>
</template>
<template #action="{ record }">
<a-space>
<a-link @click="viewDetail(record)">查看</a-link>
<a-link @click="editRecord(record)">编辑</a-link>
<a-link :disabled="record.contractStatus === '已结算'" @click="!(record.contractStatus === '已结算') && openSettlement(record)">结算</a-link>
<a-link status="danger" @click="deleteContract(record)">删除</a-link>
</a-space>
</template>
<!-- 日期展示仅年月日放在 action 模板之外作为独立列插槽 -->
<template #signDate="{ record }">{{ (record.signDate || '').slice(0,10) }}</template>
<template #performanceDeadline="{ record }">{{ (record.performanceDeadline || '').slice(0,10) }}</template>
<template #paymentDate="{ record }">{{ (record.paymentDate || '').slice(0,10) }}</template>
</GiTable>
<!-- 合同详情弹窗 -->
@ -87,8 +93,6 @@
@update:contract-data="handleContractDataUpdate"
/>
</a-modal>
</GiPageLayout>
<!-- 新建合同弹窗 -->
<a-modal
v-model:visible="showAddModal"
@ -102,7 +106,18 @@
:contract-data="newContractData"
@update:contract-data="handleNewContractDataUpdate"
/>
</a-modal>
<!-- 合同结算弹窗 -->
<a-modal v-model:visible="showSettlementModal" title="合同结算" :width="520" @before-ok="submitSettlement">
<a-form :model="settlementForm" layout="vertical">
<a-form-item field="amount" label="结算金额"><a-input-number v-model="settlementForm.amount" style="width:100%" /></a-form-item>
<a-form-item field="paymentDate" label="付款日期"><a-date-picker v-model="settlementForm.paymentDate" show-time value-format="YYYY-MM-DD HH:mm:ss" style="width:100%"/></a-form-item>
<a-form-item field="paymentPeriod" label="账期"><a-date-picker v-model="settlementForm.paymentPeriod" show-time value-format="YYYY-MM-DD HH:mm:ss" style="width:100%"/></a-form-item>
<a-form-item field="notes" label="备注"><a-textarea v-model="settlementForm.notes" /></a-form-item>
</a-form>
</a-modal>
</GiPageLayout>
</template>
<script setup lang="ts">
@ -162,17 +177,17 @@ const queryFormColumns = [
{ field: 'status', label: '合同状态', type: 'select' as const, props: {
placeholder: '请选择合同状态',
options: [
{ label: '未确认', value: '未确认' },
{ label: '待审批', value: '待审批' },
{ label: '已签署', value: '已签署' },
{ label: '未执行', value: '未执行' },
{ label: '执行中', value: '执行中' },
{ label: '已完成', value: '已完成' },
{ label: '验收中', value: '验收中' },
{ label: '结算中', value: '结算中' },
{ label: '已结算', value: '已结算' },
{ label: '已终止', value: '已终止' },
],
},
},
{ field: 'signDateRange', label: '签署时间', type: 'range-picker' as const, props: {
placeholder: ['开始时间', '结束时间'], showTime: true, format: 'YYYY-MM-DD HH:mm:ss',
placeholder: ['开始日期', '结束日期'], format: 'YYYY-MM-DD',
}
},
]
@ -185,16 +200,105 @@ const tableColumns: TableColumnData[] = [
{ title: '合同金额', dataIndex: 'amount', slotName: 'contractAmount', width: 120 },
{ title: '已收款金额', dataIndex: 'receivedAmount', slotName: 'receivedAmount', width: 120 },
{ title: '未收款金额', dataIndex: 'pendingAmount', width: 120 },
{ title: '签署日期', dataIndex: 'signDate', width: 120 },
{ title: '履约期限', dataIndex: 'performanceDeadline', width: 120 },
{ title: '付款日期', dataIndex: 'paymentDate', width: 120 },
{ title: '合同状态', dataIndex: 'contractStatus', slotName: 'status', width: 100 },
{ title: '签署日期', dataIndex: 'signDate', slotName: 'signDate', width: 120 },
{ title: '履约期限', dataIndex: 'performanceDeadline', slotName: 'performanceDeadline', width: 120 },
{ title: '付款日期', dataIndex: 'paymentDate', slotName: 'paymentDate', width: 120 },
{ title: '合同状态', dataIndex: 'contractStatus', slotName: 'status', width: 120 },
{ title: '销售人员', dataIndex: 'salespersonName', width: 100 },
{ title: '销售部门', dataIndex: 'salespersonDeptName', width: 100 },
{ title: '产品服务', dataIndex: 'productService', width: 120, ellipsis: true, tooltip: true },
{ title: '备注', dataIndex: 'notes', width: 200, ellipsis: true, tooltip: true },
{ title: '操作', slotName: 'action', width: 200, fixed: 'right' },
{ title: '备注', dataIndex: 'notes', width: 220, ellipsis: true, tooltip: true },
{ title: '操作', slotName: 'action', width: 240, fixed: 'right' },
]
//
const showSettlementModal = ref(false)
const settlementForm = reactive({
//
contractId: '',
amount: 0,
paymentDate: '',
paymentPeriod: '',
notes: '',
//
accountNumber: '',
code: '',
customer: '',
departmentId: '',
duration: '',
productService: '',
projectId: '',
salespersonId: '',
settlementId: '',
settlementStatus: '',
})
const settlementRecord = ref<ContractItem | null>(null)
const openSettlement = (record: ContractItem) => {
if (record.contractStatus === '已结算') return
settlementRecord.value = record
Object.assign(settlementForm, {
//
contractId: record.contractId,
amount: record.amount || 0,
paymentDate: record.paymentDate || '',
paymentPeriod: '',
notes: '',
// 便
accountNumber: (record as any).accountNumber || '',
code: (record as any).code || '',
customer: (record as any).customer || '',
departmentId: (record as any).departmentId || '',
duration: (record as any).duration || '',
productService: (record as any).productService || '',
projectId: (record as any).projectId || '',
salespersonId: (record as any).salespersonId || '',
settlementId: (record as any).settlementId || '',
settlementStatus: (record as any).settlementStatus || '',
})
showSettlementModal.value = true
}
const submitSettlement = async () => {
try {
const payload:any = {
accountNumber: settlementForm.accountNumber || '',
amount: settlementForm.amount,
code: settlementForm.code || '',
contractId: settlementForm.contractId,
customer: settlementForm.customer || '',
departmentId: settlementForm.departmentId || '',
duration: settlementForm.duration || '',
notes: settlementForm.notes || '',
paymentDate: settlementForm.paymentDate || null,
paymentPeriod: settlementForm.paymentPeriod || '',
productService: settlementForm.productService || '',
projectId: settlementForm.projectId || '',
salespersonId: settlementForm.salespersonId || '',
settlementId: settlementForm.settlementId || '',
settlementStatus: settlementForm.settlementStatus || '',
}
const res = await http.post('/contract-settlement', payload)
if (res.code === 200 || (res as any).status === 200) {
Message.success('合同结算成功')
// 0 0 =>
const origin = settlementRecord.value
const settledAmount = (origin?.settlementAmount ?? origin?.receivedAmount ?? 0) + (settlementForm.amount || 0)
const pendingAfter = Math.max((origin?.amount || 0) - settledAmount, 0)
const targetStatus = pendingAfter === 0 ? '已结算' : '结算中'
await http.put('/contract', { contractId: settlementForm.contractId, contractStatus: targetStatus, type: origin?.type || '收入合同' })
showSettlementModal.value = false
search()
return true
}
Message.error(res.msg || '合同结算失败')
return false
} catch (e:any) {
Message.error(e?.message || '合同结算失败')
return false
}
}
//
const loading = ref(false)
const dataList = ref<ContractItem[]>([])
@ -235,7 +339,7 @@ const fetchContractList = async () => {
dataList.value = revenueContracts.map((item: ContractItem) => ({
...item,
pendingAmount: (item.amount || 0) - (item.receivedAmount || 0),
pendingAmount: (item.amount || 0) - ((item.receivedAmount || item.settlementAmount || 0)),
}))
pagination.total = dataList.value.length
@ -263,11 +367,11 @@ const pagination = reactive({
//
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
未确认: 'gray',
待审批: 'orange',
已签署: 'blue',
未执行: 'gray',
执行中: 'cyan',
已完成: 'green',
验收中: 'arcoblue',
结算中: 'orange',
已结算: 'green',
已终止: 'red',
}
return colorMap[status] || 'gray'
@ -318,7 +422,7 @@ const newContractData = ref<ContractItem>({
contractId: '', customer: '', code: '', projectId: '', type: '收入合同',
productService: '', paymentDate: null, performanceDeadline: null,
paymentAddress: '', amount: 0, accountNumber: '', notes: '',
contractStatus: '未确认', contractText: '', projectName: '',
contractStatus: '未执行', contractText: '', projectName: '',
salespersonName: null, salespersonDeptName: '', settlementAmount: null,
receivedAmount: null, contractStatusLabel: null, createBy: null, updateBy: null,
createTime: '', updateTime: '', page: 1, pageSize: 10, signDate: '', duration: '',
@ -329,7 +433,7 @@ const openAddModal = () => {
contractId: '', customer: '', code: '', projectId: '', type: '收入合同',
productService: '', paymentDate: null, performanceDeadline: null,
paymentAddress: '', amount: 0, accountNumber: '', notes: '',
contractStatus: '未确认', contractText: '', projectName: '',
contractStatus: '未执行', contractText: '', projectName: '',
salespersonName: null, salespersonDeptName: '', settlementAmount: null,
receivedAmount: null, contractStatusLabel: null, createBy: null, updateBy: null,
createTime: '', updateTime: '', page: 1, pageSize: 10, signDate: '', duration: '',
@ -347,7 +451,7 @@ const handleAddSubmit = async () => {
accountNumber: newContractData.value.accountNumber || '',
amount: newContractData.value.amount || 0,
code: newContractData.value.code || '',
contractStatus: newContractData.value.contractStatus || '',
contractStatus: newContractData.value.contractStatus || '未执行',
contractText: newContractData.value.contractText || '',
customer: newContractData.value.customer || '',
departmentId: (newContractData.value as any).departmentId || '',
@ -391,7 +495,7 @@ const editRecord = (record: ContractItem) => {
amount: record.amount || 0,
projectId: record.projectId || '',
type: record.type || '收入合同',
contractStatus: record.contractStatus || '未确认',
contractStatus: record.contractStatus || '未执行',
}
selectedContractData.value = completeRecord
@ -418,7 +522,7 @@ const handleEditSubmit = async () => {
amount: editedContractData.value.amount || 0,
code: editedContractData.value.code || '',
contractId: editedContractData.value.contractId,
contractStatus: editedContractData.value.contractStatus || '',
contractStatus: editedContractData.value.contractStatus || '未执行',
contractText: editedContractData.value.contractText || '',
customer: editedContractData.value.customer || '',
departmentId: editedContractData.value.departmentId || '',

View File

@ -1,11 +1,163 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import type { TableColumnData } from '@arco-design/web-vue'
//
type OrderStatus = '待支付' | '进行中' | '已完成' | '已取消'
interface OrderItem { id: string; orderNo: string; customer: string; amount: number; status: OrderStatus; createTime: string }
const StatusOptions: { label: string; value: OrderStatus }[] = [
{ label: '待支付', value: '待支付' },
{ label: '进行中', value: '进行中' },
{ label: '已完成', value: '已完成' },
{ label: '已取消', value: '已取消' },
]
//
const allOrders = ref<OrderItem[]>([])
const stats = computed(() => ({
total: allOrders.value.length,
pending: allOrders.value.filter(o=>o.status==='待支付').length,
progress: allOrders.value.filter(o=>o.status==='进行中').length,
done: allOrders.value.filter(o=>o.status==='已完成').length,
cancel: allOrders.value.filter(o=>o.status==='已取消').length,
}))
//
const statCards = computed(() => [
{ key: 'total', title: '订单总数', value: stats.value.total, color: '#2f54eb', iconChar: 'T' },
{ key: 'pending', title: '待支付', value: stats.value.pending, color: '#faad14', iconChar: 'P' },
{ key: 'progress', title: '进行中', value: stats.value.progress, color: '#1677ff', iconChar: 'R' },
{ key: 'done', title: '已完成', value: stats.value.done, color: '#52c41a', iconChar: 'D' },
{ key: 'cancel', title: '已取消', value: stats.value.cancel, color: '#8c8c8c', iconChar: 'C' },
])
//
const searchForm = reactive({ orderNo: '', status: '' as ''|OrderStatus, timeRange: [] as [string,string]|[], page: 1, size: 10 })
const filtered = computed(() => {
let list = allOrders.value.slice()
if (searchForm.orderNo) list = list.filter(o=>o.orderNo.includes(searchForm.orderNo.trim()))
if (searchForm.status) list = list.filter(o=>o.status===searchForm.status)
if (Array.isArray(searchForm.timeRange) && searchForm.timeRange.length===2) {
const [s,e] = searchForm.timeRange; const S=+new Date(s as any), E=+new Date(e as any)
list = list.filter(o=>{ const t=+new Date(o.createTime as any); return t>=S && t<=E })
}
return list
})
const paged = computed(() => {
const start = (searchForm.page-1)*searchForm.size
return filtered.value.slice(start, start+searchForm.size)
})
const pagination = reactive({ current: 1, pageSize: 10, total: 0, showTotal: true, showPageSize: true })
//
const columns: TableColumnData[] = [
{ title: '订单编号', dataIndex: 'orderNo', width: 180 },
{ title: '客户', dataIndex: 'customer', width: 160 },
{ title: '金额', dataIndex: 'amount', width: 120, render: ({record}:any)=>`${(record.amount||0).toLocaleString()}` },
{ title: '状态', dataIndex: 'status', width: 100, slotName: 'status' },
{ title: '下单时间', dataIndex: 'createTime', width: 180 },
{ title: '操作', slotName: 'action', width: 140, fixed: 'right' },
]
//
const loading = ref(false)
const search = () => { pagination.total = filtered.value.length }
const reset = () => { Object.assign(searchForm, { orderNo: '', status: '', timeRange: [], page: 1, size: 10 }); pagination.current=1; pagination.pageSize=10; search() }
const onPageChange = (p:number)=>{ searchForm.page=p; pagination.current=p }
const onPageSizeChange=(s:number)=>{ searchForm.size=s; searchForm.page=1; pagination.pageSize=s; pagination.current=1; search() }
const getStatusColor = (s:OrderStatus)=>({ '待支付':'orange','进行中':'blue','已完成':'green','已取消':'gray' }[s])
//
const showAdd = ref(false)
const newOrder = reactive<{orderNo:string;customer:string;amount:number;status:OrderStatus;createTime:string}>(
{ orderNo:'', customer:'', amount:0, status:'待支付', createTime:'' }
)
const openAdd = ()=>{ Object.assign(newOrder,{ orderNo:'',customer:'',amount:0,status:'待支付',createTime:new Date().toISOString().slice(0,19).replace('T',' ') }); showAdd.value=true }
const submitAdd = ()=>{
const id = Math.random().toString(36).slice(2)
allOrders.value.unshift({ id, ...newOrder })
showAdd.value=false; search()
}
//
const genMock = (n=60)=>{
const pick=<T,>(arr:T[])=>arr[Math.floor(Math.random()*arr.length)]
const now=Date.now()
return Array.from({length:n}).map((_,i)=>{
const offset = Math.floor(Math.random()*60)*86400000
const dt = new Date(now-offset).toISOString().slice(0,19).replace('T',' ')
const st = pick<OrderStatus>(['待支付','进行中','已完成','已取消'])
return { id:`O${i+1}`, orderNo:`NO${100000+i}`, customer:`客户${(i%20)+1}`, amount: Math.floor(Math.random()*50000)+1000, status: st, createTime: dt }
})
}
onMounted(()=>{ allOrders.value = genMock(88); search() })
</script>
<template>
<GiPageLayout>
<!-- 顶部统计美化 + 居中 + 独立容器 -->
<a-row :gutter="12" class="mb-3 stats-row">
<a-col :span="24">
<div class="stats-wrap">
<a-row :gutter="12" justify="center">
<a-col v-for="card in statCards" :key="card.key" :xs="12" :sm="12" :md="6" :lg="4">
<a-card hoverable class="stat-card">
<div class="stat-inner">
<div class="stat-icon" :style="{ background: card.color }">
<span>{{ card.iconChar }}</span>
</div>
<div class="stat-value">{{ card.value }}</div>
<div class="stat-title">{{ card.title }}</div>
</div>
</a-card>
</a-col>
</a-row>
</div>
</a-col>
</a-row>
<!-- 查询表单 + 新增按钮 -->
<GiForm :columns="[
{ field:'orderNo', label:'订单编号', type:'input', props:{placeholder:'支持模糊查询'} },
{ field:'status', label:'订单状态', type:'select', props:{ placeholder:'全部', options: StatusOptions } },
{ field:'timeRange', label:'订单时间', type:'range-picker', props:{ showTime:true, format:'YYYY-MM-DD HH:mm:ss' } },
]" v-model="searchForm" search size="medium" @search="search" @reset="reset">
<template #extra>
<a-button type="primary" @click="openAdd"><icon-plus/> 新增订单</a-button>
</template>
</GiForm>
<!-- 订单列表 -->
<GiTable :data="paged" :columns="columns" :loading="loading" :pagination="pagination"
@page-change="onPageChange" @page-size-change="onPageSizeChange" @refresh="search">
<template #status="{ record }"><a-tag :color="getStatusColor(record.status)">{{ record.status }}</a-tag></template>
<template #action="{ record }"><a-space>
<a-link @click="()=>{}">查看</a-link>
<a-link @click="()=>{}">编辑</a-link>
</a-space></template>
</GiTable>
<!-- 新增订单弹窗本地 -->
<a-modal v-model:visible="showAdd" title="新增订单" :width="520" @before-ok="submitAdd">
<a-form :model="newOrder" layout="vertical">
<a-form-item field="orderNo" label="订单编号"><a-input v-model="newOrder.orderNo" placeholder="NO123456"/></a-form-item>
<a-form-item field="customer" label="客户"><a-input v-model="newOrder.customer"/></a-form-item>
<a-form-item field="amount" label="金额"><a-input-number v-model="newOrder.amount" style="width:100%"/></a-form-item>
<a-form-item field="status" label="状态"><a-select v-model="newOrder.status" :options="StatusOptions"/></a-form-item>
<a-form-item field="createTime" label="下单时间"><a-date-picker v-model="newOrder.createTime" show-time style="width:100%"/></a-form-item>
</a-form>
</a-modal>
</GiPageLayout>
</template>
<style scoped lang="scss">
<style scoped>
.mb-3{ margin-bottom:12px; }
.stats-row{ }
.stats-wrap{ max-width: 1200px; margin: 0 auto; }
.stat-card{ border: 1px solid var(--color-border-2,#f0f0f0); background: linear-gradient(180deg,#ffffff 0%, #fafbff 100%); }
.stat-inner{ display:flex; flex-direction:column; align-items:center; justify-content:center; padding:18px 10px; text-align:center; }
.stat-icon{ width:44px; height:44px; border-radius:12px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; margin-bottom:8px; box-shadow: 0 6px 16px rgba(0,0,0,0.08); }
.stat-value{ font-size:28px; font-weight:700; color:#1d2129; line-height:1.2; }
.stat-title{ margin-top:6px; color:#86909c; }
</style>

View File

@ -41,20 +41,6 @@
<!-- 简洁搜索区域 -->
<div class="search-section">
<a-card :bordered="false" class="search-card">
<!-- 搜索状态提示 -->
<div v-if="hasActiveFilters" class="search-status">
<icon-info-circle style="color: #667eea; margin-right: 8px;" />
<span>当前应用了搜索筛选条件</span>
<a-button
type="text"
size="small"
@click="handleReset"
style="margin-left: 8px; color: #667eea;"
>
清除所有筛选
</a-button>
</div>
<a-form layout="inline" :model="searchForm">
<a-form-item label="姓名">
<a-input
@ -63,7 +49,6 @@
style="width: 180px"
allow-clear
@press-enter="handleSearch"
@clear="handleSearch"
/>
</a-form-item>
<a-form-item label="项目岗位">
@ -72,7 +57,6 @@
placeholder="请选择项目岗位"
style="width: 150px"
allow-clear
@change="handleSearch"
>
<a-option
v-for="option in ROLE_TYPE_OPTIONS"
@ -89,7 +73,6 @@
placeholder="请选择状态"
style="width: 120px"
allow-clear
@change="handleSearch"
>
<a-option
v-for="option in STATUS_OPTIONS"
@ -102,12 +85,7 @@
</a-form-item>
<a-form-item>
<a-space>
<a-button
type="primary"
@click="handleSearch"
:loading="loading"
:disabled="!hasActiveFilters"
>
<a-button type="primary" @click="handleSearch">
<template #icon><icon-search /></template>
搜索
</a-button>
@ -121,7 +99,6 @@
</a-card>
</div>
<!-- 数据表格 -->
<GiTable
row-key="id"
@ -135,23 +112,6 @@
@page-size-change="onPageSizeChange"
@refresh="loadData"
>
<!-- 空状态显示 -->
<template #empty>
<div class="empty-state">
<icon-user-group style="font-size: 48px; color: #c9cdd4; margin-bottom: 16px;" />
<div class="empty-text">
<h3>{{ hasActiveFilters ? '没有找到匹配的团队成员' : '暂无团队成员数据' }}</h3>
<p v-if="hasActiveFilters">
请尝试调整搜索条件或
<a-link @click="handleReset">清除所有筛选</a-link>
</p>
<p v-else>
点击右上角的"新增成员"按钮来添加第一个团队成员
</p>
</div>
</div>
</template>
<!-- 项目岗位列 -->
<template #roleType="{ record }">
<span class="role-type-text">
@ -161,9 +121,9 @@
<!-- 状态列 -->
<template #status="{ record }">
<span class="status-tag" :class="`status-${record.status || 'INACTIVE'}`">
<a-tag :color="getStatusColor(record.status || 'INACTIVE')">
{{ getStatusText(record.status || 'INACTIVE') }}
</span>
</a-tag>
</template>
@ -380,7 +340,7 @@ const ROLE_TYPE_OPTIONS = [
const STATUS_OPTIONS = [
{ label: '可用', value: 'ACTIVE' },
{ label: '忙碌', value: 'SUSPENDED' },
{ label: '忙碌', value: 'SUSPENDEN' },
{ label: '离线', value: 'INACTIVE' }
]
@ -441,7 +401,7 @@ const statusForm = reactive<{
id: string | number
name: string
currentStatus: string
newStatus: 'ACTIVE' | 'SUSPENDED' | 'INACTIVE'
newStatus: 'ACTIVE' | 'SUSPENDEN' | 'INACTIVE'
}>({
id: '',
name: '',
@ -463,80 +423,56 @@ const loadData = async () => {
try {
console.log('正在加载项目团队成员数据项目ID:', projectId)
//
//
const queryParams: TeamMemberQuery = {
projectId: projectId,
page: pagination.current,
pageSize: pagination.pageSize,
name: searchForm.name || undefined,
position: searchForm.roleType || undefined, // 使position
position: searchForm.roleType || undefined,
status: searchForm.status || undefined
}
console.log('查询参数:', queryParams)
const response = await getProjectTeamMembers(queryParams)
console.log('API响应数据:', response)
console.log('API响应数据:', response.data)
//
let mappedData: TeamMemberResp[] = []
let total = 0
// response.data
const rawData = Array.isArray(response.data) ? response.data : [response.data]
if (response.data) {
//
if (response.data.list && Array.isArray(response.data.list)) {
mappedData = response.data.list.map((item: BackendTeamMemberResp) => {
return {
console.log('处理后的原始数据:', rawData)
//
const mappedData = rawData.map((item: BackendTeamMemberResp) => {
const mappedItem: TeamMemberResp = {
id: item.memberId,
name: item.name || '',
phone: item.phone || '',
email: item.email || '',
roleType: item.roleType || item.position || '', // 使roleTypefallbackposition
status: (item.status === 'ACTIVE' ? 'ACTIVE' : item.status === 'SUSPENDED' ? 'SUSPENDED' : 'INACTIVE') as 'ACTIVE' | 'SUSPENDED' | 'INACTIVE',
roleType: item.roleType || '', //
status: (item.status === 'ACTIVE' ? 'ACTIVE' : item.status === 'SUSPENDEN' ? 'SUSPENDEN' : 'INACTIVE') as 'ACTIVE' | 'SUSPENDEN' | 'INACTIVE',
joinDate: item.joinDate || '',
remark: item.remark || '',
avatar: item.userAvatar || ''
}
})
total = response.data.total || mappedData.length
}
//
else if (Array.isArray(response.data)) {
mappedData = response.data.map((item: BackendTeamMemberResp) => {
return {
id: item.memberId,
name: item.name || '',
phone: item.phone || '',
email: item.email || '',
roleType: item.roleType || item.position || '',
status: (item.status === 'ACTIVE' ? 'ACTIVE' : item.status === 'SUSPENDED' ? 'SUSPENDED' : 'INACTIVE') as 'ACTIVE' | 'SUSPENDED' | 'INACTIVE',
joinDate: item.joinDate || '',
remark: item.remark || '',
avatar: item.userAvatar || ''
}
})
total = mappedData.length
}
}
console.log('映射后的数据项:', mappedItem)
return mappedItem
})
dataList.value = mappedData
pagination.total = total
pagination.total = mappedData.length
console.log('团队成员数据加载完成,显示数据:', dataList.value.length, '条,总计:', pagination.total, '条')
} catch (error) {
console.error('团队成员数据加载失败:', error)
Message.error('团队成员数据加载失败')
dataList.value = []
pagination.total = 0
} finally {
loading.value = false
}
}
const handleSearch = async () => {
console.log('执行搜索,搜索条件:', searchForm)
pagination.current = 1
await loadData()
}
@ -549,18 +485,15 @@ const handleReset = () => {
status: ''
})
pagination.current = 1
//
loadData()
}
const onPageChange = (page: number) => {
console.log('页码变化:', page)
pagination.current = page
loadData()
}
const onPageSizeChange = (pageSize: number) => {
console.log('每页大小变化:', pageSize)
pagination.pageSize = pageSize
pagination.current = 1
loadData()
@ -664,7 +597,7 @@ const openStatusModal = (record: TeamMemberResp) => {
const saveStatus = async () => {
try {
await updateTeamMember(statusForm.id, {
status: statusForm.newStatus as 'ACTIVE' | 'SUSPENDED' | 'INACTIVE'
status: statusForm.newStatus as 'ACTIVE' | 'SUSPENDEN' | 'INACTIVE'
})
Message.success('状态更新成功')
statusModalVisible.value = false
@ -783,7 +716,7 @@ const getRoleTypeText = (roleType: string) => {
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
ACTIVE: 'success',
SUSPENDED: 'warning',
SUSPENDEN: 'warning',
INACTIVE: 'danger'
}
return colorMap[status] || 'danger'
@ -792,17 +725,12 @@ const getStatusColor = (status: string) => {
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
ACTIVE: '可用',
SUSPENDED: '忙碌',
SUSPENDEN: '忙碌',
INACTIVE: '离线'
}
return textMap[status] || '未知'
}
//
const hasActiveFilters = computed(() => {
return searchForm.name || searchForm.roleType || searchForm.status
})
//
onMounted(() => {
console.log('团队成员管理页面加载项目ID:', projectId)
@ -1011,29 +939,6 @@ onMounted(() => {
padding: 24px;
}
//
.search-status {
display: flex;
align-items: center;
margin-bottom: 16px;
padding: 12px 16px;
background: rgba(102, 126, 234, 0.1);
border-radius: 8px;
border-left: 4px solid #667eea;
font-size: 14px;
color: #4e5969;
.arco-btn {
padding: 2px 8px;
height: auto;
line-height: 1.4;
&:hover {
background: rgba(102, 126, 234, 0.2);
}
}
}
:deep(.arco-form-item) {
margin-bottom: 0;
margin-right: 20px;
@ -1064,7 +969,7 @@ onMounted(() => {
}
:deep(.arco-btn) {
border-radius: 8px;
border-radius: 8px;
font-weight: 600;
transition: all 0.3s ease;
@ -1072,78 +977,87 @@ onMounted(() => {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
&:hover {
transform: none;
box-shadow: none;
}
}
}
}
}
//
.search-results-info {
margin-bottom: 16px;
//
:deep(.gi-table) {
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
margin-bottom: 24px;
overflow: hidden;
.results-card {
background: linear-gradient(135deg, #f8f9ff 0%, #f0f2ff 100%);
border-radius: 12px;
box-shadow: 0 2px 12px rgba(102, 126, 234, 0.1);
border: 1px solid rgba(102, 126, 234, 0.1);
.arco-table-container {
overflow-x: auto;
overflow-y: auto;
}
:deep(.arco-card-body) {
padding: 16px 20px;
}
.arco-table {
overflow: visible;
}
.results-summary {
display: flex;
align-items: center;
font-size: 14px;
color: #4e5969;
.arco-table-body {
overflow-y: auto;
}
strong {
color: #667eea;
//
.arco-table-thead {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
.arco-table-th {
background: transparent !important;
border-bottom: 2px solid rgba(255, 255, 255, 0.2);
.arco-table-th-item-title {
color: white !important;
font-weight: 600;
font-size: 14px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
}
}
//
.arco-table-tbody {
.arco-table-tr {
transition: all 0.3s ease;
&:hover {
background: linear-gradient(135deg, #f8f9ff 0%, #f0f2ff 100%);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.1);
}
.arco-table-td {
border-bottom: 1px solid #f0f0f0;
padding: 16px 12px;
vertical-align: middle;
}
}
//
.arco-table-tr:nth-child(even) {
background: #fafbfc;
&:hover {
background: linear-gradient(135deg, #f8f9ff 0%, #f0f2ff 100%);
}
}
}
//
.arco-table-fixed-left,
.arco-table-fixed-right {
.arco-table-td {
background: white;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
}
}
//
.empty-state {
text-align: center;
padding: 60px 20px;
color: #c9cdd4;
.empty-text {
h3 {
margin: 0 0 12px 0;
font-size: 18px;
font-weight: 600;
color: #4e5969;
}
p {
margin: 8px 0;
font-size: 14px;
line-height: 1.6;
.arco-link {
color: #667eea;
font-weight: 500;
&:hover {
color: #4c5fd9;
}
}
}
}
}
.remark-content {
max-width: 180px;
@ -1196,58 +1110,11 @@ onMounted(() => {
transition: all 0.3s ease;
&:hover {
transform: scale(1.05);
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
}
}
//
.status-tag {
display: inline-block;
padding: 6px 12px;
border-radius: 20px;
font-weight: 600;
font-size: 12px;
text-align: center;
min-width: 60px;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
&:hover {
transform: scale(1.05);
}
&.status-ACTIVE {
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
color: white;
box-shadow: 0 2px 8px rgba(82, 196, 26, 0.3);
&:hover {
box-shadow: 0 4px 16px rgba(82, 196, 26, 0.4);
}
}
&.status-SUSPENDED {
background: linear-gradient(135deg, #faad14 0%, #ffc53d 100%);
color: white;
box-shadow: 0 2px 8px rgba(250, 173, 20, 0.3);
&:hover {
box-shadow: 0 4px 16px rgba(250, 173, 20, 0.4);
}
}
&.status-INACTIVE {
background: linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%);
color: white;
box-shadow: 0 2px 8px rgba(255, 77, 79, 0.3);
&:hover {
box-shadow: 0 4px 16px rgba(255, 77, 79, 0.4);
}
}
}
.member-form {
.form-row {
display: grid;
@ -1440,80 +1307,4 @@ onMounted(() => {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
//
:deep(.gi-table) {
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
margin-bottom: 24px;
overflow: hidden;
.arco-table-container {
overflow-x: auto;
overflow-y: auto;
}
.arco-table {
overflow: visible;
}
.arco-table-body {
overflow-y: auto;
}
//
.arco-table-thead {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
.arco-table-th {
background: transparent !important;
border-bottom: 2px solid rgba(255, 255, 255, 0.2);
.arco-table-th-item-title {
color: white !important;
font-weight: 600;
font-size: 14px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
}
}
//
.arco-table-tbody {
.arco-table-tr {
transition: all 0.3s ease;
&:hover {
background: linear-gradient(135deg, #f8f9ff 0%, #f0f2ff 100%);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.1);
}
.arco-table-td {
border-bottom: 1px solid #f0f0f0;
padding: 16px 12px;
vertical-align: middle;
}
}
//
.arco-table-tr:nth-child(even) {
background: #fafbfc;
&:hover {
background: linear-gradient(135deg, #f8f9ff 0%, #f0f2ff 100%);
}
}
}
//
.arco-table-fixed-left,
.arco-table-fixed-right {
.arco-table-td {
background: white;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
}
}
</style>

File diff suppressed because it is too large Load Diff