This commit is contained in:
何德超 2025-07-23 22:46:47 +08:00
parent 15d50b997d
commit b028f2a7bd
23 changed files with 1510 additions and 382 deletions

View File

@ -4,8 +4,8 @@
VITE_BUILD_MOCK = false
# 接口地址
VITE_API_BASE_URL = 'https://api.continew.top'
VITE_API_WS_URL = 'wss://api.continew.top'
VITE_API_BASE_URL = 'http://pms.dtyx.net:9158/'
VITE_API_WS_URL = 'ws://localhost:8000'
# 地址前缀
VITE_BASE = '/'

View File

@ -0,0 +1,20 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- 风机塔筒 -->
<rect x="47" y="40" width="6" height="45" fill="#4a5568" rx="3"/>
<!-- 风机机舱 -->
<ellipse cx="50" cy="40" rx="8" ry="5" fill="#2d3748"/>
<!-- 风机叶片 -->
<g transform="translate(50, 40)">
<!-- 叶片1 -->
<path d="M0,0 L-25,-15 Q-35,-20 -40,-30" stroke="#4a5568" stroke-width="3" fill="none"/>
<!-- 叶片2 -->
<path d="M0,0 L15,-30 Q20,-40 30,-45" stroke="#4a5568" stroke-width="3" fill="none"/>
<!-- 叶片3 -->
<path d="M0,0 L30,10 Q40,15 45,25" stroke="#4a5568" stroke-width="3" fill="none"/>
</g>
<!-- 风机底座 -->
<ellipse cx="50" cy="85" rx="12" ry="3" fill="#2d3748"/>
</svg>

After

Width:  |  Height:  |  Size: 761 B

View File

@ -0,0 +1,13 @@
import http from '@/utils/http'
import type { AttendanceRecordReq, AttendanceRecordResp } from './type'
const BASE_URL = '/attendance-record'
/** 新增考勤记录 */
export function addAttendanceRecord(data: AttendanceRecordReq) {
return http.post<AttendanceRecordResp>(BASE_URL, data, {
headers: {
'Content-Type': 'application/json'
}
})
}

View File

@ -0,0 +1,15 @@
/** 新增考勤记录请求体 */
export interface AttendanceRecordReq {
recordImage?: string
recordPosition?: string
recordPositionLabel?: string
}
/** 新增考勤记录响应体 */
export interface AttendanceRecordResp {
code: number
data: object
msg: string
status: number
success: boolean
}

View File

@ -0,0 +1,40 @@
import http from '@/utils/http'
import type { PerformanceDimension, PerformanceRule, DimensionQuery, RuleQuery } from './type'
/** 维度相关 */
export function getDimensionList(params?: DimensionQuery) {
return http.get<PerformanceDimension[]>('/performance-dimension/list', params)
}
export function getDimensionDetail(id: string) {
return http.get<PerformanceDimension>(`/performance-dimension/${id}`)
}
export function addDimension(data: Partial<PerformanceDimension>) {
return http.post('/performance-dimension', data)
}
export function updateDimension(id: string, data: Partial<PerformanceDimension>) {
return http.put(`/performance-dimension/${id}`, data)
}
export function deleteDimension(id: string) {
return http.del(`/performance-dimension/${id}`)
}
/** 细则相关 */
export function getRuleList(params?: RuleQuery) {
return http.get<PerformanceRule[]>('/performance-rule/list', params)
}
export function getRuleDetail(id: string) {
return http.get<PerformanceRule>(`/performance-rule/${id}`)
}
export function addRule(data: Partial<PerformanceRule>) {
return http.post('/performance-rule', data)
}
export function updateRule(id: string, data: Partial<PerformanceRule>) {
return http.put(`/performance-rule/${id}`, data)
}
export function deleteRule(id: string) {
return http.del(`/performance-rule/${id}`)
}
// 我的绩效
export function getMyEvaluation() {
return http.get('/performance-evaluation/my')
}

View File

@ -0,0 +1,39 @@
/** 绩效维度 */
export interface PerformanceDimension {
dimensionId: string
dimensionName: string
description?: string
deptName: string
status: 0 | 1
createBy?: string
createTime?: string
updateBy?: string
updateTime?: string
}
/** 绩效细则 */
export interface PerformanceRule {
ruleId: string
ruleName: string
description?: string
dimensionName: string
bonus?: string
score?: number
weight?: number
status: 0 | 1
createBy?: string
createTime?: string
updateBy?: string
updateTime?: string
}
/** 查询参数 */
export interface DimensionQuery {
dimensionName?: string
status?: 0 | 1
}
export interface RuleQuery {
dimensionName?: string
ruleName?: string
status?: 0 | 1
}

View File

@ -0,0 +1,234 @@
<template>
<div class="turbine-grid-container">
<div class="turbine-grid">
<div v-for="turbine in turbines" :key="turbine.id" class="turbine-card"
:class="getStatusClass(turbine.status)">
<div class="turbine-status-badge" :class="`status-${turbine.status}`">
{{ getStatusText(turbine.status) }}
</div>
<div class="turbine-icon">
<img src="/static/images/wind-turbine-icon.svg" alt="风机图标" class="turbine-image" />
</div>
<div class="turbine-info">
<div class="turbine-number">
<a-input v-model="turbine.turbineNo" size="small" class="turbine-input" placeholder="请输入机组编号"
@change="handleTurbineNoChange(turbine)" />
</div>
</div>
<div class="turbine-actions">
<a-button type="text" size="mini" @click="openMapModal(turbine)" title="地图选点">
<template #icon><icon-location /></template>
</a-button>
<a-button type="text" size="mini" @click="editTurbine(turbine)" title="编辑">
<template #icon><icon-edit /></template>
</a-button>
</div>
</div>
</div>
<!-- 添加新机组按钮 -->
<div v-if="showAddButton" class="turbine-card add-turbine-card" @click="addTurbine">
<div class="add-icon">
<icon-plus />
</div>
<div class="add-text">添加机组</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Message } from '@arco-design/web-vue'
interface Turbine {
id: number
turbineNo: string
status: 0 | 1 | 2 // 0: , 1: , 2:
lat?: number
lng?: number
}
interface Props {
turbines: Turbine[]
showAddButton?: boolean
}
interface Emits {
(e: 'update:turbines', turbines: Turbine[]): void
(e: 'turbine-change', turbine: Turbine): void
(e: 'add-turbine'): void
}
const props = withDefaults(defineProps<Props>(), {
showAddButton: false
})
const emit = defineEmits<Emits>()
const getStatusText = (status: number) => {
const statusMap = {
0: '待施工',
1: '施工中',
2: '已完成'
}
return statusMap[status] || '未知状态'
}
const getStatusClass = (status: number) => {
return `status-${status}`
}
const handleTurbineNoChange = (turbine: Turbine) => {
emit('turbine-change', turbine)
emit('update:turbines', props.turbines)
}
const openMapModal = (turbine: Turbine) => {
Message.info(`地图选点功能待开发,当前机组编号:${turbine.turbineNo}`)
}
const editTurbine = (turbine: Turbine) => {
//
Message.info(`编辑机组:${turbine.turbineNo}`)
}
const addTurbine = () => {
emit('add-turbine')
}
</script>
<style scoped>
.turbine-grid-container {
padding: 16px;
}
.turbine-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 16px;
}
.turbine-card {
position: relative;
background: white;
border: 1px solid #e5e5e5;
border-radius: 8px;
padding: 16px;
transition: all 0.3s ease;
cursor: pointer;
}
.turbine-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.turbine-card.status-0 {
border-left: 4px solid #ff7d00;
}
.turbine-card.status-1 {
border-left: 4px solid #165dff;
}
.turbine-card.status-2 {
border-left: 4px solid #00b42a;
}
.turbine-status-badge {
position: absolute;
top: 8px;
left: 8px;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
color: white;
}
.turbine-status-badge.status-0 {
background-color: #ff7d00;
}
.turbine-status-badge.status-1 {
background-color: #165dff;
}
.turbine-status-badge.status-2 {
background-color: #00b42a;
}
.turbine-icon {
display: flex;
justify-content: center;
margin: 20px 0;
}
.turbine-image {
width: 80px;
height: 80px;
object-fit: contain;
}
.turbine-info {
text-align: center;
margin-bottom: 12px;
}
.turbine-input {
text-align: center;
font-weight: 500;
}
.turbine-actions {
display: flex;
justify-content: center;
gap: 8px;
}
.add-turbine-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 2px dashed #d9d9d9;
background-color: #fafafa;
color: #666;
min-height: 200px;
}
.add-turbine-card:hover {
border-color: #165dff;
color: #165dff;
background-color: #f0f7ff;
}
.add-icon {
font-size: 32px;
margin-bottom: 8px;
}
.add-text {
font-size: 14px;
}
@media (max-width: 768px) {
.turbine-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 12px;
}
.turbine-card {
padding: 12px;
}
.turbine-image {
width: 60px;
height: 60px;
}
}
</style>

View File

@ -73,7 +73,7 @@ export const systemRoutes: RouteRecordRaw[] = [
{
path: '/organization/hr/performance/dimention',
name: 'Dimention',
component: () => import('@/views/performance/dimension.vue'),
component: () => import('@/views/performance/setting/index.vue'),
meta: { title: '绩效维度', icon: 'performance', hidden: false },
},
@ -1144,7 +1144,7 @@ export const systemRoutes: RouteRecordRaw[] = [
{
path: '/',
redirect: '/project-management/contract/revenue-contract3',
redirect: '/project-management/project-template/project-aproval',
meta: { hidden: true },
},
{

View File

@ -57,6 +57,7 @@ declare global {
const useCssVars: typeof import('vue')['useCssVars']
const useId: typeof import('vue')['useId']
const useLink: typeof import('vue-router')['useLink']
const useModel: typeof import('vue')['useModel']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSlots: typeof import('vue')['useSlots']
@ -69,6 +70,6 @@ declare global {
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

View File

@ -61,6 +61,7 @@ declare module 'vue' {
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']

View File

@ -38,7 +38,7 @@ const IconMap: Record<number, Component> = {
const router = useRouter()
//
const back = () => {
router.replace({ path: '/project-management/bidding/tender-documents' })
router.replace({ path: '/asset-management/intellectual-property' })
}
</script>
@ -58,6 +58,7 @@ const back = () => {
flex-direction: column;
align-items: center;
}
&__img {
width: 100%;
position: relative;
@ -66,14 +67,17 @@ const back = () => {
justify-content: center;
align-items: center;
}
&__icon {
max-width: 90%;
height: 50vh;
}
&__tip {
display: flex;
flex-direction: column;
align-items: center;
&--a {
margin-bottom: 20px;
font-size: 32px;
@ -85,6 +89,7 @@ const back = () => {
animation-duration: 0.5s;
animation-fill-mode: forwards;
}
&--b {
margin-bottom: 10px;
font-size: 20px;
@ -97,6 +102,7 @@ const back = () => {
animation-delay: 0.1s;
animation-fill-mode: forwards;
}
&--c {
padding: 0 30px;
margin-bottom: 20px;
@ -112,11 +118,13 @@ const back = () => {
}
}
}
@keyframes slideUp {
0% {
opacity: 0;
transform: translateY(60px);
}
100% {
opacity: 1;
transform: translateY(0);

View File

@ -0,0 +1,73 @@
<template>
<GiPageLayout :margin="true">
<template #header>
<h2>新增考勤记录</h2>
</template>
<a-form
ref="formRef"
:model="form"
:rules="rules"
layout="vertical"
@submit="onSubmit"
>
<a-form-item label="打卡照片" name="recordImage">
<a-input v-model="form.recordImage" placeholder="请输入打卡照片URL" />
<!-- 你可以替换为图片上传组件 -->
</a-form-item>
<a-form-item label="打卡地点(经纬度)" name="recordPosition">
<a-input v-model="form.recordPosition" placeholder="请输入经纬度" />
</a-form-item>
<a-form-item label="打卡地点(中文描述)" name="recordPositionLabel">
<a-input v-model="form.recordPositionLabel" placeholder="请输入地点描述" />
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit" :loading="loading">提交</a-button>
</a-form-item>
</a-form>
<a-alert v-if="resultMsg" :type="resultType" :show-icon="true" style="margin-top: 16px;">
{{ resultMsg }}
</a-alert>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { addAttendanceRecord } from '@/apis/attendance-record/index'
import type { AttendanceRecordReq } from '@/apis/attendance-record/type'
const formRef = ref()
const form = ref<AttendanceRecordReq>({
recordImage: '',
recordPosition: '',
recordPositionLabel: '',
})
const rules = {
//
}
const loading = ref(false)
const resultMsg = ref('')
const resultType = ref<'success' | 'error'>('success')
const onSubmit = async () => {
loading.value = true
resultMsg.value = ''
try {
const res = await addAttendanceRecord(form.value)
if (res.success) {
resultMsg.value = '考勤记录提交成功'
resultType.value = 'success'
form.value = { recordImage: '', recordPosition: '', recordPositionLabel: '' }
} else {
resultMsg.value = res.msg || '提交失败'
resultType.value = 'error'
}
} catch (e: any) {
resultMsg.value = e?.message || '提交异常'
resultType.value = 'error'
} finally {
loading.value = false
}
}
</script>

View File

@ -1,13 +1,6 @@
<template>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col-style="{ display: 'none' }"
:wrapper-col-style="{ flex: 1 }"
size="large"
@submit="handleLogin"
>
<a-form ref="formRef" :model="form" :rules="rules" :label-col-style="{ display: 'none' }"
:wrapper-col-style="{ flex: 1 }" size="large" @submit="handleLogin">
<a-form-item field="account" hide-label>
<a-input v-model="form.account" placeholder="请输入用户名" allow-clear />
</a-form-item>
@ -108,7 +101,7 @@ const handleLogin = async () => {
const { rememberMe } = loginConfig.value
loginConfig.value.account = rememberMe ? form.account : ''
await router.push({
path: (redirect as string) || '/project-management/projects/initiation',
path: (redirect as string) || '/asset-management/intellectual-property',
query: {
...othersQuery,
},

View File

@ -1,6 +1,6 @@
<template>
<a-card title="我的绩效">
<a-table :data="data" :columns="columns" row-key="id">
<a-table :data="data" :columns="columns" row-key="evaluationId">
<template #action="{ record }">
<a-button type="text" @click="openDetail(record)">查看详情</a-button>
</template>
@ -8,41 +8,43 @@
</a-card>
<a-modal v-model:visible="visible" title="绩效详情">
<EvaluateDetail v-if="selected" :data="selected" @feedback="handleFeedback" />
<div v-if="selected">
<div>维度{{ selected.dimensionName }}</div>
<div>周期{{ selected.periodName }}</div>
<div>评分{{ selected.score }}</div>
<div>奖金{{ selected.bonus }}</div>
<div>评价{{ selected.comment }}</div>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import EvaluateDetail from './components/EvaluateDetail.vue'
import { getMyEvaluate } from '@/apis/performance'
import type { EvaluateResp } from '@/apis/performance/type'
import { getMyEvaluation } from '@/apis/performance-setting'
import type { PerformanceRule } from '@/apis/performance-setting/type'
const data = ref<EvaluateResp[]>([])
const data = ref<any[]>([])
const visible = ref(false)
const selected = ref<EvaluateResp>()
const selected = ref<any>()
const columns = [
{ title: '维度', dataIndex: 'dimensionName' },
{ title: '周期', dataIndex: 'periodName' },
{ title: '评分', dataIndex: 'score' },
{ title: '奖金', dataIndex: 'bonus' },
{ title: '评价', dataIndex: 'comment' },
{ title: '操作', slotName: 'action' },
]
const load = async () => {
const response = await getMyEvaluate()
data.value = response.data?.list || []
const res = await getMyEvaluation()
data.value = res.data || []
}
const openDetail = (row: EvaluateResp) => {
const openDetail = (row: any) => {
selected.value = row
visible.value = true
}
const handleFeedback = () => {
visible.value = false
load()
}
onMounted(load)
</script>
</script>

View File

@ -0,0 +1,58 @@
<template>
<a-drawer v-model:visible="visible" :title="isEdit ? '编辑维度' : '新增维度'" @before-ok="save">
<a-form ref="formRef" :model="form" auto-label-width>
<a-form-item label="维度名称" field="dimensionName" required>
<a-input v-model="form.dimensionName" />
</a-form-item>
<a-form-item label="岗位" field="deptName" required>
<a-input v-model="form.deptName" />
</a-form-item>
<a-form-item label="描述" field="description">
<a-textarea v-model="form.description" :rows="3" />
</a-form-item>
<a-form-item label="状态" field="status">
<a-switch v-model="form.status" :checked-value="0" :unchecked-value="1" />
</a-form-item>
</a-form>
</a-drawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { addDimension, updateDimension } from '@/apis/performance-setting'
import type { PerformanceDimension } from '@/apis/performance-setting/type'
const emit = defineEmits(['success'])
const visible = ref(false)
const isEdit = ref(false)
const formRef = ref()
const form = ref<Partial<PerformanceDimension>>({
dimensionName: '',
deptName: '',
description: '',
status: 0,
})
const open = (record?: PerformanceDimension) => {
if (record) {
Object.assign(form.value, record)
isEdit.value = true
} else {
Object.assign(form.value, { dimensionName: '', deptName: '', description: '', status: 0 })
isEdit.value = false
}
visible.value = true
}
const save = async () => {
if (isEdit.value && form.value.dimensionId) {
await updateDimension(form.value.dimensionId, form.value)
} else {
await addDimension(form.value)
}
visible.value = false
emit('success')
}
defineExpose({ open })
</script>

View File

@ -0,0 +1,86 @@
<template>
<a-drawer v-model:visible="visible" :title="isEdit ? '编辑细则' : '新增细则'" @before-ok="save">
<a-form ref="formRef" :model="form" auto-label-width>
<a-form-item label="细则名称" field="ruleName" required>
<a-input v-model="form.ruleName" />
</a-form-item>
<a-form-item label="描述" field="description">
<a-textarea v-model="form.description" :rows="3" />
</a-form-item>
<a-form-item label="分数" field="score">
<a-input-number v-model="form.score" :min="0" />
</a-form-item>
<a-form-item label="权重" field="weight">
<a-input-number v-model="form.weight" :min="0" :max="100" />
</a-form-item>
<a-form-item label="奖金" field="bonus">
<a-input v-model="form.bonus" />
</a-form-item>
<a-form-item label="状态" field="status">
<a-switch v-model="form.status" :checked-value="0" :unchecked-value="1" />
</a-form-item>
</a-form>
</a-drawer>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { addRule, updateRule } from '@/apis/performance-setting'
import type { PerformanceRule, PerformanceDimension } from '@/apis/performance-setting/type'
const props = defineProps<{ dimension?: PerformanceDimension }>()
const emit = defineEmits(['success'])
const visible = ref(false)
const isEdit = ref(false)
const formRef = ref()
const form = ref<Partial<PerformanceRule>>({
ruleName: '',
description: '',
score: 0,
weight: 0,
bonus: '',
status: 0,
dimensionName: '',
})
watch(
() => props.dimension,
(val) => {
if (val && !isEdit.value) {
form.value.dimensionName = val.dimensionName
}
},
{ immediate: true }
)
const open = (record?: PerformanceRule) => {
if (record) {
Object.assign(form.value, record)
isEdit.value = true
} else {
Object.assign(form.value, {
ruleName: '',
description: '',
score: 0,
weight: 0,
bonus: '',
status: 0,
dimensionName: props.dimension?.dimensionName || '',
})
isEdit.value = false
}
visible.value = true
}
const save = async () => {
if (isEdit.value && form.value.ruleId) {
await updateRule(form.value.ruleId, form.value)
} else {
await addRule(form.value)
}
visible.value = false
emit('success')
}
defineExpose({ open })
</script>

View File

@ -0,0 +1,64 @@
<template>
<a-drawer v-model:visible="visible" :title="`管理细则 - ${dimension?.dimensionName || ''}`">
<a-button type="primary" @click="openRuleDrawer()">新增细则</a-button>
<a-table :data="ruleList" :columns="ruleColumns" row-key="ruleId" style="margin-top: 16px;">
<template #status="{ record }">
<a-tag :color="record.status === 0 ? 'green' : 'red'">
{{ record.status === 0 ? '启用' : '禁用' }}
</a-tag>
</template>
<template #action="{ record }">
<a-space>
<a-button type="text" @click="openRuleDrawer(record)">编辑</a-button>
<a-button type="text" status="danger" @click="handleDeleteRule(record.ruleId)">删除</a-button>
</a-space>
</template>
</a-table>
<RuleDrawer ref="ruleDrawerRef" @success="loadRules" :dimension="dimension" />
</a-drawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { getRuleList, deleteRule } from '@/apis/performance-setting'
import RuleDrawer from './RuleDrawer.vue'
import type { PerformanceRule, PerformanceDimension } from '@/apis/performance-setting/type'
const visible = ref(false)
const dimension = ref<PerformanceDimension>()
const ruleList = ref<PerformanceRule[]>([])
const ruleDrawerRef = ref()
const ruleColumns = [
{ title: '细则名称', dataIndex: 'ruleName' },
{ title: '描述', dataIndex: 'description' },
{ title: '分数', dataIndex: 'score' },
{ title: '权重', dataIndex: 'weight' },
{ title: '奖金', dataIndex: 'bonus' },
{ title: '状态', slotName: 'status' },
{ title: '操作', slotName: 'action' },
]
const open = (dim: PerformanceDimension) => {
dimension.value = dim
visible.value = true
loadRules()
}
const loadRules = async () => {
if (!dimension.value) return
const res = await getRuleList({ dimensionName: dimension.value.dimensionName })
ruleList.value = res.data || []
}
const openRuleDrawer = (record?: PerformanceRule) => {
ruleDrawerRef.value.open(record)
}
const handleDeleteRule = async (id: string) => {
await deleteRule(id)
loadRules()
}
defineExpose({ open })
</script>

View File

@ -0,0 +1,67 @@
<template>
<a-card title="绩效维度管理">
<template #extra>
<a-button type="primary" @click="openDimensionDrawer()">新增维度</a-button>
</template>
<a-table :data="dimensionList" :columns="dimensionColumns" row-key="dimensionId">
<template #status="{ record }">
<a-tag :color="record.status === 0 ? 'green' : 'red'">
{{ record.status === 0 ? '启用' : '禁用' }}
</a-tag>
</template>
<template #action="{ record }">
<a-space>
<a-button type="text" @click="openDimensionDrawer(record)">编辑</a-button>
<a-button type="text" status="danger" @click="handleDeleteDimension(record.dimensionId)">删除</a-button>
<a-button type="text" @click="openRuleList(record)">管理细则</a-button>
</a-space>
</template>
</a-table>
</a-card>
<!-- 维度抽屉 -->
<DimensionDrawer ref="dimensionDrawerRef" @success="loadDimensions" />
<!-- 细则管理 -->
<RuleList ref="ruleListRef" />
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import DimensionDrawer from './components/DimensionDrawer.vue'
import RuleList from './components/RuleList.vue'
import { deleteDimension, getDimensionList } from '@/apis/performance-setting/index'
import type { PerformanceDimension } from '@/apis/performance-setting/type.ts'
const dimensionList = ref<PerformanceDimension[]>([])
const dimensionDrawerRef = ref()
const ruleListRef = ref()
const dimensionColumns = [
{ title: '维度名称', dataIndex: 'dimensionName' },
{ title: '岗位', dataIndex: 'deptName' },
{ title: '描述', dataIndex: 'description' },
{ title: '状态', slotName: 'status' },
{ title: '操作', slotName: 'action' },
]
const loadDimensions = async () => {
const res = await getDimensionList()
dimensionList.value = res.data || []
}
const openDimensionDrawer = (record?: PerformanceDimension) => {
dimensionDrawerRef.value.open(record)
}
const handleDeleteDimension = async (id: string) => {
await deleteDimension(id)
loadDimensions()
}
const openRuleList = (dimension: PerformanceDimension) => {
ruleListRef.value.open(dimension)
}
onMounted(loadDimensions)
</script>

View File

@ -0,0 +1,88 @@
<script setup lang="ts">
import { computed } from 'vue'
import WindTurbine from './icons/WindTurbine.vue'
interface Turbine {
id: number
turbineNo: string
status: 0 | 1 | 2 // 0 1 2
lat?: number
lng?: number
}
const props = defineProps<{ modelValue: Turbine }>()
const emit = defineEmits<{
(e: 'update:modelValue', v: Turbine): void
(e: 'map'): void
}>()
const turbine = computed({
get: () => props.modelValue,
set: v => emit('update:modelValue', v)
})
/* 状态文字 & 颜色 */
const statusTextMap = { 0: '待施工', 1: '施工中', 2: '已完成' }
const statusColorMap = { 0: '#FF7D00', 1: '#165DFF', 2: '#00B42A' }
/* 点击循环切换 */
function toggleStatus() {
const next = ((turbine.value.status + 1) % 3) as 0 | 1 | 2
turbine.value = { ...turbine.value, status: next }
}
function updateNo(val: string) {
turbine.value = { ...turbine.value, turbineNo: val }
}
</script>
<template>
<div class="turbine-card">
<!-- 可点击的状态标签 -->
<div class="status-tag" :style="{ backgroundColor: statusColorMap[turbine.status] }" @click="toggleStatus">
{{ statusTextMap[turbine.status] }}
</div>
<!-- 风机图标 -->
<WindTurbine />
<!-- 机组编号输入框 -->
<a-input :model-value="turbine.turbineNo" @update:model-value="updateNo" size="small" class="turbine-input"
placeholder="编号" />
<!-- 地图选点按钮 -->
<a-button size="mini" @click="$emit('map')">
<template #icon><icon-location /></template>
</a-button>
</div>
</template>
<style scoped>
.turbine-card {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 8px;
border: 1px solid var(--color-border-2);
border-radius: 4px;
background: var(--color-bg-2);
}
.status-tag {
position: absolute;
top: 4px;
left: 4px;
font-size: 10px;
color: #fff;
padding: 2px 4px;
border-radius: 2px;
cursor: pointer;
user-select: none;
}
.turbine-input {
margin-top: 8px;
text-align: center;
}
</style>

View File

@ -0,0 +1,41 @@
<script setup lang="ts">
import { computed } from 'vue'
import TurbineCard from './TurbineCard.vue'
interface Turbine {
id: number
turbineNo: string
status: 0 | 1 | 2
lat?: number
lng?: number
}
const props = defineProps<{
modelValue: Turbine[]
}>()
const emit = defineEmits<{
(e: 'update:modelValue', v: Turbine[]): void
(e: 'map', turbine: Turbine): void
}>()
const turbines = computed({
get: () => props.modelValue,
set: v => emit('update:modelValue', v)
})
function updateTurbine(index: number, t: Turbine) {
const arr = [...turbines.value]
arr.splice(index, 1, t)
turbines.value = arr
}
</script>
<template>
<a-row v-if="turbines.length" :gutter="[16, 16]" wrap>
<a-col v-for="(t, i) in turbines" :key="t.id" :xs="12" :sm="8" :md="6" :lg="4" :xl="3">
<TurbineCard v-model="turbines[i]" @update:model-value="v => updateTurbine(i, v)" @map="$emit('map', t)" />
</a-col>
</a-row>
<a-empty v-else description="请先设置项目规模" />
</template>

View File

@ -7,7 +7,9 @@
<template #icon><icon-arrow-left /></template>
</a-button>
<h2 class="ml-2">{{ projectTitle }}</h2>
<a-tag class="ml-2" :color="getStatusColor(projectData.status)" v-if="projectData.status">{{ projectData.status }}</a-tag>
<a-tag class="ml-2" :color="getStatusColor(projectData.status)" v-if="projectData.status">{{
projectData.status
}}</a-tag>
</div>
<div class="flex items-center">
<a-button v-permission="['project:update']" type="primary" class="mr-2" @click="editProject">
@ -24,17 +26,18 @@
<a-tab-pane key="1" title="详细信息">
<a-card class="general-card" title="项目基本信息">
<a-descriptions :data="projectInfos" layout="horizontal" bordered column="3" />
<a-divider />
<div class="card-header">
<div class="card-title">项目收支情况</div>
</div>
<div class="finance-cards grid grid-cols-3 gap-4 mb-6">
<a-card class="finance-card">
<div class="finance-title">利润</div>
<div class="finance-amount" :class="{ 'text-danger': finance.profit < 0 }">{{ finance.profit.toFixed(2) }}</div>
<div class="finance-amount" :class="{ 'text-danger': finance.profit < 0 }">{{ finance.profit.toFixed(2) }}
</div>
</a-card>
<a-card class="finance-card">
<div class="finance-title">收入</div>
@ -45,11 +48,11 @@
<div class="finance-amount text-warning">{{ finance.expense.toFixed(2) }}</div>
</a-card>
</div>
<a-descriptions title="合同金额" :data="contractInfos" layout="horizontal" bordered />
</a-card>
</a-tab-pane>
<a-tab-pane key="2" title="执行情况">
<a-card class="general-card" title="项目进度跟踪">
<div class="flex items-center justify-between mb-6">
@ -66,7 +69,7 @@
</a-button>
</div>
</div>
<a-card :bordered="false" class="mt-4">
<a-row :gutter="16" class="task-container">
<a-col :span="6" v-for="(column, index) in taskColumns" :key="index">
@ -75,38 +78,34 @@
<span class="column-title">{{ column.title }}</span>
<a-tag>{{ column.tasks.length }}</a-tag>
</div>
<div class="task-list">
<div
v-for="task in column.tasks"
:key="task.id"
class="task-card"
@click="openTaskDetail(task)"
>
<div v-for="task in column.tasks" :key="task.id" class="task-card" @click="openTaskDetail(task)">
<div class="task-card-title">{{ task.taskName }}</div>
<div class="task-card-desc text-gray-500 text-xs mt-1">{{ task.description || '暂无描述' }}</div>
<div class="task-card-footer flex items-center justify-between mt-2">
<div class="flex items-center">
<icon-calendar class="mr-1 text-gray-500" />
<span class="text-xs">{{ formatDate(task.taskPeriod?.[0]) }} - {{ formatDate(task.taskPeriod?.[1]) }}</span>
<span class="text-xs">{{ formatDate(task.taskPeriod?.[0]) }} - {{
formatDate(task.taskPeriod?.[1])
}}</span>
</div>
<div class="text-xs">
<a-progress :percent="task.progress" size="small" />
</div>
</div>
<div class="task-card-members mt-2">
<a-avatar :size="24" v-for="(user, idx) in task.participants?.slice(0, 3)" :key="idx">{{ user.substring(0, 1) }}</a-avatar>
<a-avatar :size="24" v-if="task.participants?.length > 3">+{{ task.participants.length - 3 }}</a-avatar>
<a-avatar :size="24" v-for="(user, idx) in task.participants?.slice(0, 3)" :key="idx">{{
user.substring(0, 1) }}</a-avatar>
<a-avatar :size="24" v-if="task.participants?.length > 3">+{{ task.participants.length - 3
}}</a-avatar>
</div>
</div>
<div
class="add-task-placeholder"
@click="handleAddTaskClick(column.status)"
>
<icon-plus />
<span>添加任务</span>
</div>
<div class="add-task-placeholder" @click="handleAddTaskClick(column.status)">
<icon-plus />
<span>添加任务</span>
</div>
</div>
</div>
</a-col>
@ -114,15 +113,11 @@
</a-card>
</a-card>
</a-tab-pane>
<a-tab-pane key="3" title="附件">
<a-card class="general-card" title="项目附件">
<div class="attachment-section">
<a-upload
list-type="picture-card"
:file-list="attachments"
@change="handleAttachmentChange"
>
<a-upload list-type="picture-card" :file-list="attachments" @change="handleAttachmentChange">
<template #upload-button>
<div class="upload-button-content">
<icon-plus />
@ -136,13 +131,9 @@
</a-tabs>
<!-- 新增任务弹窗 -->
<a-modal
v-model:visible="addTaskModalVisible"
title="新增任务"
@cancel="resetTaskForm"
@before-ok="handleTaskSubmit"
>
<a-form ref="taskFormRef" :model="taskForm" label-position="left" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }">
<a-modal v-model:visible="addTaskModalVisible" title="新增任务" @cancel="resetTaskForm" @before-ok="handleTaskSubmit">
<a-form ref="taskFormRef" :model="taskForm" label-position="left" :label-col-props="{ span: 6 }"
:wrapper-col-props="{ span: 18 }">
<a-form-item field="taskName" label="任务名称" required>
<a-input v-model="taskForm.taskName" placeholder="请输入" />
</a-form-item>
@ -172,13 +163,10 @@
</a-modal>
<!-- 新增任务组弹窗 -->
<a-modal
v-model:visible="addTaskGroupModalVisible"
title="新增任务组"
@cancel="resetTaskGroupForm"
@before-ok="handleTaskGroupSubmit"
>
<a-form ref="taskGroupFormRef" :model="taskGroupForm" label-position="left" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }">
<a-modal v-model:visible="addTaskGroupModalVisible" title="新增任务组" @cancel="resetTaskGroupForm"
@before-ok="handleTaskGroupSubmit">
<a-form ref="taskGroupFormRef" :model="taskGroupForm" label-position="left" :label-col-props="{ span: 6 }"
:wrapper-col-props="{ span: 18 }">
<a-form-item field="groupName" label="组名称" required>
<a-input v-model="taskGroupForm.groupName" placeholder="请输入" />
</a-form-item>
@ -186,18 +174,14 @@
</a-modal>
<!-- 任务详情弹窗 -->
<a-modal
v-model:visible="taskDetailModalVisible"
title="任务详情"
@cancel="closeTaskDetail"
>
<a-modal v-model:visible="taskDetailModalVisible" title="任务详情" @cancel="closeTaskDetail">
<div v-if="currentTask">
<a-descriptions :data="taskDetailInfos" layout="horizontal" bordered />
<div class="mt-4">
<h4>任务进度</h4>
<a-progress :percent="currentTask.progress || 0" />
<a-form class="mt-4">
<a-form-item label="更新进度">
<a-input-number v-model="updateProgress" :min="0" :max="100" style="width: 100%" />
@ -289,7 +273,7 @@ const contractInfos = computed(() => [
const taskDetailInfos = computed(() => {
if (!currentTask.value) return []
return [
{ label: '任务名称', value: currentTask.value.taskName },
{ label: '任务编号', value: currentTask.value.taskCode },
@ -350,19 +334,19 @@ const fetchProjectData = async () => {
const fetchTaskData = async () => {
try {
const res = await listTask({
const res = await listTask({
projectId: projectId.value,
page: 1,
size: 100
})
//
taskColumns.value.forEach(column => {
column.tasks = []
})
const tasks = res.data?.list || []
//
tasks.forEach((task: any) => {
const column = taskColumns.value.find(col => col.status === task.status)
@ -494,10 +478,10 @@ const submitProgressUpdate = async () => {
try {
await updateTaskProgress({ progress: updateProgress.value }, currentTask.value.id)
Message.success('更新进度成功')
//
currentTask.value.progress = updateProgress.value
//
await fetchTaskData()
} catch (error) {
@ -516,33 +500,41 @@ onMounted(() => {
.finance-card {
text-align: center;
}
.finance-title {
color: #7f7f7f;
font-size: 14px;
}
.finance-amount {
font-size: 24px;
font-weight: bold;
}
.text-danger {
color: #f53f3f;
}
.text-success {
color: #00b42a;
}
.text-warning {
color: #ff7d00;
}
.task-container {
overflow-x: auto;
min-height: 600px;
}
.task-column {
background-color: #f5f5f5;
border-radius: 4px;
height: 100%;
min-height: 600px;
}
.task-column-header {
padding: 12px;
border-radius: 4px 4px 0 0;
@ -551,9 +543,11 @@ onMounted(() => {
align-items: center;
font-weight: bold;
}
.task-list {
padding: 8px;
}
.task-card {
background-color: white;
border-radius: 4px;
@ -563,16 +557,20 @@ onMounted(() => {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.3s;
}
.task-card:hover {
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.2);
}
.task-card-title {
font-weight: bold;
}
.task-card-members {
display: flex;
gap: 4px;
}
.add-task-placeholder {
display: flex;
align-items: center;
@ -584,26 +582,31 @@ onMounted(() => {
cursor: pointer;
gap: 8px;
}
.add-task-placeholder:hover {
background-color: #d9d9d9;
}
.upload-button-content {
display: flex;
flex-direction: column;
align-items: center;
color: #7f7f7f;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.card-title {
font-size: 16px;
font-weight: bold;
}
.general-card {
margin-bottom: 16px;
}
</style>
</style>

View File

@ -0,0 +1,358 @@
<template>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px" width="80px" height="160px" viewBox="0 0 210 234" enable-background="new 0 0 210 234"
xml:space="preserve">
<image id="image0" width="210" height="234" x="0" y="0" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANIAAADqCAMAAADgbuuwAAAABGdBTUEAALGPC/xhBQAAACBjSFJN
AAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAACc1BMVEXx8fH29vb8+/z////+
/v74+Pjz8/Py8vLv7+/39/fs7Oz6+vrw8PD19fX5+fn9/f3q6up+fn60tLTu7u7p6ekPDw9SUVLr
6+vo6OiZmZkHBwcBAQHMzMwAAAA7Ozvn5+fk5OSrq6vj4+Pl5eUkJCRvb2/c3Nx4eHgFBQWysrIQ
EBANDQ03Nzevr68MDAxHR0cXFxeQkJAKCgoDAwMYGBjFxcUJCQlxcXEICAixsbEVFRVBQUECAgIG
BgaamprV1dVbW1sUFBTJycmNjY0TExO4uLgbGxvd3d2lpaXh4eFjY2O+vr5/f38rKytmZmZFRUUt
LS23t7epqana2tqWlpbGxsaIiIiwsLB5eXmBgYGLi4uSkpKzs7PBwcEREREODg4zMzNWVlYjIyN2
dnZYWFibm5vS0tLb29vPz884ODhAQEDe3t7U1NQcHBx7e3u2trZtbW3T09MpKSlMTEykpKTZ2dks
LCxqamq7u7teXl4/Pz9sbGzNzc1kZGQWFhaPj4+hoaF9fX3IyMgdHR1iYmK9vb2AgIDi4uLCwsLX
19fExMSOjo5LS0tdXV2npqesrKwiIiKoqKg5OTmHh4coKCi/v78vLy/g4OA2NjZoaGgxMTFPT08m
JiYhISFfX19CQkI+Pj5ycnLQ0NBpaWlhYWEeHh7Hx8dwcHBzc3OXl5fAwMA9PT1VVVWUlJR6enpr
a2tERES5ublTU1NaWlqVlZUfHx8aGhpJSUmGhoaTk5Otra2ioqLOzs6dnZ26urpcXFxlZWWgoKBN
TU2FhYWJiYkyMjIwMDDW1tZQUFCEhISMjIxXV1dOTk4lJSVISEiCgoJUVFSQ4NxnAAAAAWJLR0QD
EQxM8gAAAAd0SU1FB+kHFwcmCqQ1L2MAAEkTSURBVHjazb2LnyXbVR62d712PXZVHVxnqEMfFz2a
hqvjO2gQc9CoJbqRr/VCCeAABoRB94JsBEIgE/FKiCwhIFxQYuOA4JpHHBSQkYgBB5PEIQlxiO28
H3+S1/etvetUd5/umXsRmPPTT/f0dNfj24+1vvXcxtgkTbO8cM6Z0uTyQ5pW8jG1aTL5pN4W+Bn/
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" />
</svg>
</template>
<style scoped>
.turbine-svg {
width: 100%;
height: 100%;
pointer-events: none;
}
</style>

View File

@ -12,20 +12,12 @@
-->
<template>
<GiPageLayout>
<GiTable
row-key="id"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1500 }"
:pagination="pagination"
:disabled-tools="['size']"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
>
<GiTable row-key="id" :data="dataList" :columns="tableColumns" :loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1500 }" :pagination="pagination" :disabled-tools="['size']"
@page-change="onPageChange" @page-size-change="onPageSizeChange" @refresh="search">
<template #top>
<GiForm v-model="searchForm" search :columns="queryFormColumns" size="medium" @search="search" @reset="reset"></GiForm>
<GiForm v-model="searchForm" search :columns="queryFormColumns" size="medium" @search="search" @reset="reset">
</GiForm>
</template>
<template #toolbar-left>
<a-button v-permission="['project:create']" type="primary" @click="openAddModal">
@ -71,12 +63,7 @@
<a-space>
<a-link v-permission="['project:detail']" title="详情" @click="viewDetail(record)">详情</a-link>
<a-link v-permission="['project:update']" title="修改" @click="openEditModal(record)">修改</a-link>
<a-link
v-permission="['project:delete']"
status="danger"
title="删除"
@click="confirmDelete(record)"
>
<a-link v-permission="['project:delete']" status="danger" title="删除" @click="confirmDelete(record)">
删除
</a-link>
</a-space>
@ -84,22 +71,10 @@
</GiTable>
<!-- 新增/编辑项目弹窗 -->
<a-modal
v-model:visible="addModalVisible"
:title="modalTitle"
@cancel="resetForm"
:ok-button-props="{ loading: submitLoading }"
@ok="handleSubmit"
width="800px"
modal-class="project-form-modal"
>
<a-form
ref="formRef"
:model="form"
:rules="formRules"
layout="vertical"
:style="{ maxHeight: '70vh', overflow: 'auto', padding: '0 10px' }"
>
<a-modal v-model:visible="addModalVisible" :title="modalTitle" @cancel="resetForm"
:ok-button-props="{ loading: submitLoading }" @ok="handleSubmit" width="800px" modal-class="project-form-modal">
<a-form ref="formRef" :model="form" :rules="formRules" layout="vertical"
:style="{ maxHeight: '70vh', overflow: 'auto', padding: '0 10px' }">
<!-- 基本信息 -->
<a-divider orientation="left">基本信息</a-divider>
<a-row :gutter="16">
@ -109,19 +84,61 @@
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="projectManagerId" label="项目经理" required>
<a-select v-model="form.projectManagerId" placeholder="请选择项目经理" :loading="userLoading">
<a-option v-for="user in userOptions" :key="user.value" :value="user.value">
{{ user.label }}
</a-option>
</a-select>
<a-form-item field="farmAddress" label="地址">
<a-input v-model="form.farmAddress" placeholder="请输入地址" />
</a-form-item>
</a-col>
<a-col><a-button size="mini" @click="() => { Message.info(`待开发`) }">
<template #icon><icon-location /></template>
地图选点
</a-button></a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="inspectionUnit" label="业主">
<a-input v-model="form.inspectionUnit" placeholder="请输入业主单位" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="inspectionContact" label="业主单位联系人">
<a-input v-model="form.inspectionContact" placeholder="请输入联系人" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="status" label="项目状态" >
<a-form-item field="inspectionPhone" label="业主单位联系电话">
<a-input v-model="form.inspectionPhone" placeholder="请输入联系电话" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="client" label="委托单位">
<a-input v-model="form.client" placeholder="请输入委托单位" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="clientContact" label="委托单位联系人">
<a-input v-model="form.clientContact" placeholder="请输入联系人" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="clientPhone" label="委托单位联系电话">
<a-input v-model="form.clientPhone" placeholder="请输入联系电话" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-form-item field="projectContent" label="项目内容">
<a-textarea v-model="form.coverUrl" placeholder="请输入项目内容" :rows="4" />
</a-form-item>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="status" label="项目状态">
<a-select v-model="form.status" placeholder="请选择状态">
<a-option v-for="option in PROJECT_STATUS_OPTIONS" :key="option.value" :value="option.value">
{{ option.label }}
@ -130,166 +147,57 @@
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="scale" label="项目规模" >
<a-input v-model="form.scale" placeholder="请输入项目规模" />
<a-form-item field="scale" label="项目规模">
<a-input-number v-model="form.scale" placeholder="请输入项目规模" :min="0" :max="999" :step="1" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="startDate" label="开始时间" >
<a-form-item field="startDate" label="开始时间">
<a-date-picker v-model="form.startDate" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="endDate" label="结束时间" >
<a-form-item field="endDate" label="结束时间">
<a-date-picker v-model="form.endDate" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="24">
<a-form-item field="coverUrl" label="项目封面">
<a-input v-model="form.coverUrl" placeholder="请输入项目封面URL" />
</a-form-item>
</a-col>
</a-row>
<!-- 委托方信息 -->
<a-divider orientation="left">委托方信息</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="client" label="委托单位" >
<a-input v-model="form.client" placeholder="请输入委托单位" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="clientContact" label="委托单位联系人" >
<a-input v-model="form.clientContact" placeholder="请输入联系人" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="clientPhone" label="委托单位联系电话" >
<a-input v-model="form.clientPhone" placeholder="请输入联系电话" />
</a-form-item>
</a-col>
</a-row>
<!-- 检查方信息 -->
<a-divider orientation="left">检查方信息</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="inspectionUnit" label="检查单位" >
<a-input v-model="form.inspectionUnit" placeholder="请输入检查单位" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="inspectionContact" label="检查单位联系人" >
<a-input v-model="form.inspectionContact" placeholder="请输入联系人" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="inspectionPhone" label="检查单位联系电话" >
<a-input v-model="form.inspectionPhone" placeholder="请输入联系电话" />
</a-form-item>
</a-col>
</a-row>
<!-- 风场信息 -->
<a-divider orientation="left">风场信息</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="farmName" label="风场名称" >
<a-input v-model="form.farmName" placeholder="请输入风场名称" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="farmAddress" label="风场地址" >
<a-input v-model="form.farmAddress" placeholder="请输入风场地址" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="turbineModel" label="风机型号" >
<a-form-item field="turbineModel" label="风机型号">
<a-input v-model="form.turbineModel" placeholder="请输入风机型号" />
</a-form-item>
</a-col>
</a-row>
<!-- 项目团队 -->
<a-divider orientation="left">项目团队</a-divider>
<!-- 风场信息 -->
<a-divider orientation="left">风场信息可视化</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="constructionTeamLeaderId" label="施工组长" >
<a-select v-model="form.constructionTeamLeaderId" placeholder="请选择施工组长" :loading="userLoading">
<a-option v-for="user in userOptions" :key="user.value" :value="user.value">
{{ user.label }}
</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="constructorIds" label="施工人员" >
<a-select v-model="form.constructorIds" multiple placeholder="请选择施工人员" :loading="userLoading">
<a-option v-for="user in userOptions" :key="user.value" :value="user.value">
{{ user.label }}
</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="qualityOfficerId" label="质量员" >
<a-select v-model="form.qualityOfficerId" placeholder="请选择质量员" :loading="userLoading">
<a-option v-for="user in userOptions" :key="user.value" :value="user.value">
{{ user.label }}
</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="auditorId" label="安全员" >
<a-select v-model="form.auditorId" placeholder="请选择安全员" :loading="userLoading">
<a-option v-for="user in userOptions" :key="user.value" :value="user.value">
{{ user.label }}
</a-option>
</a-select>
<a-col :span="24">
<a-form-item label="机组网格布局">
<a-space direction="vertical" style="width: 100%">
<TurbineGrid v-model:="form.turbineList"></TurbineGrid>
</a-space>
</a-form-item>
</a-col>
</a-row>
<a-divider orientation="middle">地图</a-divider>
</a-form>
</a-modal>
<!-- 导入项目弹窗 -->
<a-modal
v-model:visible="importModalVisible"
title="导入文件"
@cancel="handleCancelImport"
@before-ok="handleImport"
>
<a-modal v-model:visible="importModalVisible" title="导入文件" @cancel="handleCancelImport" @before-ok="handleImport">
<div class="flex flex-col items-center justify-center p-8">
<div class="text-primary text-4xl mb-4">
<icon-file-upload />
</div>
<div class="text-lg font-medium mb-2">批量导入文件</div>
<div class="text-gray-500 mb-4">拖动文件到此处或点击下方按钮上传</div>
<a-upload
:file-list="fileList"
:limit="1"
@change="handleFileChange"
>
<a-upload :file-list="fileList" :limit="1" @change="handleFileChange">
<template #upload-button>
<a-button type="primary">选择文件</a-button>
</template>
@ -311,7 +219,7 @@ import type { ColumnItem } from '@/components/GiForm'
import type { TableColumnData } from '@arco-design/web-vue'
import type { ProjectResp, ProjectPageQuery } from '@/apis/project/type'
import * as T from '@/apis/project/type'
import TurbineGrid from './TurbineGrid.vue'
defineOptions({ name: 'ProjectManagement' })
// (API)
@ -324,7 +232,7 @@ const PROJECT_STATUS = {
//
const PROJECT_STATUS_MAP = {
0: '待施工',
1: '施工中',
1: '施工中',
2: '已完成'
} as const
@ -379,11 +287,11 @@ const queryFormColumns: ColumnItem[] = reactive([
},
{
type: 'input',
label: '风场名称',
field: 'fieldName', // 使fieldNameAPI使
label: '业主',
field: 'inspectionUnit',
span: { xs: 24, sm: 8, xxl: 8 },
props: {
placeholder: '请输入风场名称',
placeholder: '请输入业主名称',
},
},
{
@ -419,7 +327,8 @@ const form = reactive({
constructionTeamLeaderId: '', // id
constructorIds: '', // id
qualityOfficerId: '', // id
auditorId: '' // id
auditorId: '', // id
turbineList: [] as { id: number; turbineNo: string; lat?: number; lng?: number; status: 0 | 1 | 2 }[],
})
const pagination = reactive({
@ -430,7 +339,9 @@ const pagination = reactive({
showJumper: true,
showPageSize: true
})
const openMapModal = (item: any) => {
Message.info(`地图选点功能待开发,当前机组编号:${item.turbineNo}`)
}
const tableColumns = ref<TableColumnData[]>([
{
title: '序号',
@ -439,91 +350,91 @@ const tableColumns = ref<TableColumnData[]>([
render: ({ rowIndex }) => rowIndex + 1 + (pagination.current - 1) * pagination.pageSize,
fixed: !isMobile() ? 'left' : undefined,
},
{
title: '项目编号',
dataIndex: 'projectCode',
width: 120,
ellipsis: true,
{
title: '项目编号',
dataIndex: 'projectCode',
width: 120,
ellipsis: true,
tooltip: true,
fixed: !isMobile() ? 'left' : undefined,
},
{
title: '项目名称',
dataIndex: 'projectName',
minWidth: 140,
ellipsis: true,
{
title: '项目名称',
dataIndex: 'projectName',
minWidth: 140,
ellipsis: true,
tooltip: true,
fixed: !isMobile() ? 'left' : undefined,
},
{
title: '风场名称/风场地址',
slotName: 'fieldInfo',
minWidth: 180,
ellipsis: true,
tooltip: true
{
title: '地点',
slotName: 'fieldInfo',
minWidth: 180,
ellipsis: true,
tooltip: true
},
{
title: '状态',
dataIndex: 'status',
slotName: 'status',
align: 'center',
width: 100
{
title: '状态',
dataIndex: 'status',
slotName: 'status',
align: 'center',
width: 100
},
{
title: '委托单位',
dataIndex: 'commissionUnit',
minWidth: 140,
ellipsis: true,
tooltip: true
{
title: '委托单位',
dataIndex: 'commissionUnit',
minWidth: 140,
ellipsis: true,
tooltip: true
},
{
title: '委托单位联系人/电话',
slotName: 'commissionInfo',
minWidth: 160,
ellipsis: true,
tooltip: true
{
title: '委托单位联系人/电话',
slotName: 'commissionInfo',
minWidth: 160,
ellipsis: true,
tooltip: true
},
{
title: '检查单位',
dataIndex: 'inspectionUnit',
minWidth: 140,
ellipsis: true,
tooltip: true
{
title: '业主',
dataIndex: 'inspectionUnit',
minWidth: 140,
ellipsis: true,
tooltip: true
},
{
title: '检查单位联系人/电话',
slotName: 'inspectionInfo',
minWidth: 160,
ellipsis: true,
tooltip: true
{
title: '业主联系人/电话',
slotName: 'inspectionInfo',
minWidth: 160,
ellipsis: true,
tooltip: true
},
{
title: '项目规模',
dataIndex: 'projectScale',
width: 100,
ellipsis: true,
tooltip: true
{
title: '项目规模',
dataIndex: 'projectScale',
width: 100,
ellipsis: true,
tooltip: true
},
{
title: '机组型号',
dataIndex: 'orgNumber',
width: 100,
ellipsis: true,
tooltip: true
{
title: '机组型号',
dataIndex: 'orgNumber',
width: 100,
ellipsis: true,
tooltip: true
},
{
title: '项目经理/施工人员',
slotName: 'projectManager',
minWidth: 160,
ellipsis: true,
tooltip: true
{
title: '项目经理/施工人员',
slotName: 'projectManager',
minWidth: 160,
ellipsis: true,
tooltip: true
},
{
title: '项目周期',
slotName: 'projectPeriod',
minWidth: 180,
ellipsis: true,
tooltip: true
{
title: '项目周期',
slotName: 'projectPeriod',
minWidth: 180,
ellipsis: true,
tooltip: true
},
{
title: '操作',
@ -533,7 +444,20 @@ const tableColumns = ref<TableColumnData[]>([
fixed: !isMobile() ? 'right' : undefined,
},
])
watch(() => form.scale, (newVal) => {
const count = Number(newVal)
if (count > 0 && count <= 999) {
form.turbineList = Array.from({ length: count }, (_, i) => ({
id: i + 1,
turbineNo: `${String(i + 1).padStart(3, '0')}`,
lat: undefined,
lng: undefined,
status: form.status,
}))
} else {
form.turbineList = []
}
}, { immediate: true })
const modalTitle = computed(() => isEdit.value ? '编辑项目' : '新增项目')
const getStatusColor = (status: number) => {
@ -557,37 +481,37 @@ const fetchData = async () => {
page: pagination.current,
size: pagination.pageSize
}
const res = await listProject(params)
if (res.success && res.data) {
// API
const projects = Array.isArray(res.data) ? res.data : []
//
dataList.value = projects.map((item: any) => {
const mappedItem: T.ProjectResp = {
...item,
//
id: item.projectId,
fieldName: item.farmName,
fieldLocation: item.farmAddress,
commissionUnit: item.client,
commissionContact: item.clientContact,
commissionPhone: item.clientPhone,
orgNumber: item.turbineModel,
projectManager: item.projectManagerName,
...item,
//
id: item.projectId,
fieldName: item.farmName,
fieldLocation: item.farmAddress,
commissionUnit: item.client,
commissionContact: item.clientContact,
commissionPhone: item.clientPhone,
orgNumber: item.turbineModel,
projectManager: item.projectManagerName,
projectScale: item.scale,
//
//
projectPeriod: item.startDate && item.endDate ? [item.startDate, item.endDate] : []
};
return mappedItem;
})
// APItotal使
// total
pagination.total = projects.length
//
if (projects.length < pagination.pageSize) {
pagination.total = (pagination.current - 1) * pagination.pageSize + projects.length
@ -619,7 +543,7 @@ const reset = () => {
fieldName: '', // 使fieldNameAPI使
status: undefined,
})
//
pagination.current = 1
search()
@ -661,7 +585,7 @@ const resetForm = () => {
qualityOfficerId: '', // id
auditorId: '' // id
})
isEdit.value = false
currentId.value = null
}
@ -674,13 +598,13 @@ const openAddModal = () => {
const openEditModal = (record: T.ProjectResp) => {
isEdit.value = true
currentId.value = record.id || record.projectId || null
//
Object.keys(form).forEach(key => {
// @ts-ignore
form[key] = ''
})
//
Object.keys(form).forEach(key => {
if (key in record && record[key as keyof T.ProjectResp] !== undefined) {
@ -688,7 +612,7 @@ const openEditModal = (record: T.ProjectResp) => {
form[key] = record[key as keyof T.ProjectResp]
}
})
//
if (record.farmName) form.farmName = record.farmName
if (record.farmAddress) form.farmAddress = record.farmAddress
@ -697,11 +621,11 @@ const openEditModal = (record: T.ProjectResp) => {
if (record.clientPhone) form.clientPhone = record.clientPhone
if (record.turbineModel) form.turbineModel = record.turbineModel
if (record.scale) form.scale = record.scale
//
if (record.startDate) form.startDate = record.startDate
if (record.endDate) form.endDate = record.endDate
addModalVisible.value = true
}
@ -716,12 +640,12 @@ const submitLoading = ref(false)
const handleSubmit = async () => {
console.log('表单提交开始', form)
submitLoading.value = true
try {
//
console.log('开始验证表单', formRef.value)
await formRef.value.validate()
//
const submitData = {
...form,
@ -735,7 +659,7 @@ const handleSubmit = async () => {
}
console.log('提交数据:', submitData)
let res
if (isEdit.value && currentId.value) {
//
@ -748,16 +672,16 @@ const handleSubmit = async () => {
res = await addProject(submitData)
Message.success('添加成功')
}
console.log('API响应结果:', res)
//
if (res && res.success === false) {
Message.error(res.msg || '操作失败')
submitLoading.value = false
return
}
addModalVisible.value = false
fetchData()
} catch (error: any) {
@ -787,16 +711,16 @@ const deleteItem = async (record: T.ProjectResp) => {
Message.error('项目ID不存在')
return
}
try {
const res = await deleteProject(projectId)
//
if (res && res.success === false) {
Message.error(res.msg || '删除失败')
return
}
Message.success('删除成功')
fetchData()
} catch (error) {
@ -811,7 +735,7 @@ const viewDetail = (record: T.ProjectResp) => {
Message.error('项目ID不存在')
return
}
router.push({
name: 'ProjectDetail',
params: {
@ -839,24 +763,24 @@ const handleImport = async () => {
Message.warning('请选择文件')
return false
}
try {
const fileItem = fileList.value[0] as any
const file = fileItem?.file || fileItem
if (!file) {
Message.warning('请选择有效的文件')
return false
}
// API
const res = await importProject(file)
if (res && res.success === false) {
Message.error(res.msg || '导入失败')
return false
}
Message.success('导入成功')
handleCancelImport()
fetchData() //
@ -875,7 +799,7 @@ const exportData = async () => {
status: searchForm.status,
fieldName: searchForm.fieldName, // 使fieldNameAPI使
}
await exportProject(params)
Message.success('导出成功')
} catch (error) {
@ -922,11 +846,11 @@ onMounted(() => {
.arco-form-item {
margin-bottom: 16px;
}
.arco-divider {
margin: 16px 0;
font-weight: 500;
color: var(--color-text-2);
}
}
</style>
</style>