| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777 |
- <template>
- <div class="project-page">
- <!-- 顶部续约预警栏 -->
- <div :class="projects.days >= 180 ? 'warning-green' : projects.days >= 90 ? 'warning-yellow' : projects.days >= 60 ? 'warning-orange' : projects.days >= 0 ? 'warning-red' : 'warning-gray'">
- <div class="warning-info">
- <div class="warning-text"> <strong v-if="projects.days >0" class="warning-danger">续标预警</strong><strong v-else>已过期</strong> · 合同起止 {{ moment(projects.c_zgxmhtksrq).format('YYYY-MM-DD') }} 至 {{ moment(projects.c_zgxmhtdqrq).format('YYYY-MM-DD') }},距本期<span v-if="projects.days >0">到期剩</span><span v-else>已过期</span> <strong class="warning-danger">{{ projects.days }}</strong> 天</div>
- <div class="warning-text-detail">服务期限 {{ moment(projects.c_zgxmhtksrq).format('YYYY-MM-DD') }} 至 {{ moment(projects.c_zgxmhtdqrq).format('YYYY-MM-DD') }},{{ projects.year }} 年服务 · 请进入项目续标跟进全流程。</div>
- </div>
- <div>
- <el-button type="primary" class="btn enter-btn" @click="handleRenewaXQlClick">
- 进入本项目续签
- </el-button>
- <el-button type="primary" class="btn enter-btn" @click="handleRenewalClick">
- 进入本项目续标
- </el-button>
- </div>
- </div>
- <!-- 进度指示器 -->
- <div class="progress-bar">
- <div class="progress-dots">
- <span class="dot active"></span>
- <span class="dot active"></span>
- <span class="dot active"></span>
- <span class="dot cur"></span>
- <span class="dot"></span>
- <span class="dot"></span>
- <span class="dot"></span>
- <span class="dot"></span>
- <span class="dot"></span>
- </div>
- <span class="progress-text">当前:合同执行,即将进入续约/投标</span>
- </div>
- <!-- 内容区域 -->
- <div class="content-grid">
- <!-- 项目与甲方 -->
- <div class="card">
- <div class="card-header">
- <span class="card-title">项目与甲方</span>
- </div>
- <div class="card-body">
- <div class="info-row">
- <span class="label">区域</span>
- <span class="value">{{ projects.c_glqynbkj }}</span>
- </div>
- <div class="info-row">
- <span class="label">金蝶编码</span>
- <span class="value">XM600161</span>
- </div>
- <div class="info-row">
- <span class="label">序号</span>
- <span class="value">{{ projects.c_xh }}</span>
- </div>
- <div class="info-row">
- <span class="label">甲方名称</span>
- <span class="value">{{ projects.c_htqyjf }}</span>
- </div>
- <div class="info-row">
- <span class="label">项目名称</span>
- <span class="value">{{ projects.c_xmmc }}</span>
- </div>
- <div class="info-row">
- <span class="label">项目地址</span>
- <span class="value">{{ projects.c_xmdz }}</span>
- </div>
- <div class="info-row">
- <span class="label">建筑面积</span>
- <span class="value">{{ projects.c_htmj }} m²</span>
- </div>
- </div>
- </div>
- <!-- 合同与服务 -->
- <div class="card">
- <div class="card-header">
- <span class="card-title">合同与服务</span>
- </div>
- <div class="card-body">
- <div class="info-row">
- <span class="label">合同起止日期</span>
- <span class="value">{{ moment(projects.c_zgxmhtksrq).format('YYYY-MM-DD') }} 至 {{ moment(projects.c_zgxmhtdqrq).format('YYYY-MM-DD') }}</span>
- </div>
- <div class="info-row">
- <span class="label">服务期限</span>
- <span class="value"> {{ projects.year }} 年服务</span>
- </div>
- <div class="info-row">
- <span class="label">委托服务</span>
- <span class="value">{{ getWTFWLabel(projects.c_wtfs) }}</span>
- </div>
- <div class="info-row">
- <span class="label">计费方式</span>
- <span class="value">{{ getJFLabel(projects.c_jffs) }}</span>
- </div>
-
- </div>
- </div>
- <!-- 合同金额 -->
- <div class="card">
- <div class="card-header">
- <span class="card-title">合同金额</span>
- </div>
- <div class="card-body">
- <div class="info-row">
- <span class="label">合同金额</span>
- <span class="value">{{ formatCurrency(projects.c_htzj/10000) }} 万元</span>
- </div>
- <div class="info-row">
- <span class="label">年化合同额</span>
- <span class="value">{{ formatCurrency(projects.c_nhhtsr/10000) }} 万元</span>
- </div>
- <div class="info-row">
- <span class="label">当年饱和收入</span>
- <span class="value">{{ formatCurrency(projects.c_dnbhsr/10000) }} 万元</span>
- </div>
- <div class="info-row">
- <span class="label">投标核算年度总成本</span>
- <span class="value">{{ formatCurrency(projects.c_tbhsndzcb/10000) }} 万元</span>
- </div>
- <div class="info-row">
- <span class="label">自用人力总成本</span>
- <span class="value">{{ formatCurrency(projects.c_zyrlzcbjss/10000) }} 万元</span>
- </div>
- <div class="info-row">
- <span class="label">综合成本</span>
- <span class="value">{{ formatCurrency(projects.c_zhcbjss/10000) }} 万元</span>
- </div>
- </div>
- </div>
- <!-- 历史投标 -->
- <div class="card">
- <div class="card-header">
- <span class="card-title">历史投标</span>
- </div>
- <div class="card-body">
- <div class="info-row" v-for="item in historyBidList" :key="item.year">
- <span class="label">{{ item.year }}年</span>
- <span class="value">中标 {{ formatCurrency(item.money/10000) }} 万元</span>
- </div>
- <!-- <div class="info-row">
- <span class="label">2024年</span>
- <span class="value">中标 6,200,000 元</span>
- </div>
- <div class="info-row">
- <span class="label">2023年</span>
- <span class="value">中标 6,800,000 元</span>
- </div>
- <div class="info-row">
- <span class="label">2020年</span>
- <span class="value">中标 3,680,000 元</span>
- </div> -->
- </div>
- </div>
- </div>
- <!-- 续约入口 -->
- <!-- <div class="renewal-section">
- <div class="section-header">
- <span class="section-title">续标入口</span>
- </div>
- <div class="section-body">
- <p class="renewal-desc">续标请从项目续标列表进入,或直接进入本项目的续标详情页。</p>
- <div class="renewal-actions">
- <el-button class="btn" @click="goToPage('/renewal')">投标列表</el-button>
- <el-button type="primary" class="btn enter-btn" @click="handleRenewalClick">进入本项目续标</el-button>
- </div>
- </div>
- </div> -->
- </div>
- </template>
- <script setup lang="ts">
- import { ref, onMounted } from 'vue'
- import api from '@/api/common'
- import moment from 'moment'
- import CONFIG from '@/utils/config'
- import { useRouter } from 'vue-router'
- const router = useRouter()
- import { ElMessage } from 'element-plus'
- const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
- const userName = userInfo?.username =='user_xtw' ? '徐天炜' : userInfo?.username || ''
- const formatCurrency = (value: string | number): string => {
- if (!value) return '0.00'
- const num = typeof value === 'string' ? parseFloat(value) : value
- if (isNaN(num)) return '0.00'
- return num.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
- }
- const projects = ref<any>({})
- const tempArr = ref<any>([])
- const wtfwOptions = ref<any>([])
- const jffsOptions = ref<any>([])
- const historyBidList = ref<any>([
- {
- year: '2019',
- money: '8585360'
- },
- {
- year: '2020',
- money: '8585360'
- },
- {
- year: '2021',
- money: '8585360'
- },
- {
- year: '2022',
- money: '8499576'
- },
- {
- year: '2023',
- money: '8499576'
- },
- {
- year: '2024',
- money: '8499576'
- },
- {
- year: '2025',
- money: '8395152'
- },
- ])
- onMounted(() => {
- initCName()
- initData()
- })
- const initData = () => {
- getProjects();
- }
- const getJFLabel = (value: string) => {
- return jffsOptions.value.find((item: any) => item.value == value)?.label || '-'
- }
- const getWTFWLabel = (value: string) => {
- return wtfwOptions.value.find((item: any) => item.value == value)?.label || '-'
- }
- const getDmsTypes = async (cName: string) => {
- const params = {
- type: 0,
- cName: cName,
- }
- const res = await api.getDmsTypes(params)
- if ((res as any).code === 200) {
- const ret = (res as any).content
- return ret.map((item: any) => ({
- ...item,
- label: item.name,
- value: item.index
- }))
- } else {
- return []
- }
- }
- const initCName = async () => {
- // 并行请求两个字典,比await串行更快
- const [wtOptions, jfOptions] = await Promise.all([
- getDmsTypes("sq_wtfs"),
- getDmsTypes("sq_jffs")
- ])
- wtfwOptions.value = wtOptions
- jffsOptions.value = jfOptions
- }
- const getProjects = async () => {
- const params: any = {
- columnId: CONFIG.COLUMNIDARR[0],
- orderBy: JSON.stringify([{ field: "update_time", orderByType: 2 }]),
- states: '0,1,2,3',
- pageSize: 1,
- page: 0
- }
- if (sessionStorage.getItem('dmsId')) {
- params.search = JSON.stringify([
- ...(params.search ? JSON.parse(params.search) : []),
- {
- field: "id",
- searchType: 2,
- content: { value: sessionStorage.getItem('dmsId') },
- },
- ]);
- }
- const res = await getDmsDataList(params)
- if ((res as any).code === 200) {
- projects.value = (res as any).content.data.map((item: any) => ({
- ...item,
- ...calculateRemaining(item.c_zgxmhtksrq, item.c_zgxmhtdqrq)
- }))[0]
- }
- }
- const calculateRemaining = (st: string, et: string): { days: string; daysClass: string; year: string } => {
- if (!et) return { days: '', daysClass: '', year: '' }
-
- const startDate = moment(moment(st).format('YYYY-MM-DD'), 'YYYY-MM-DD')
- const expireDate = moment(moment(et).add(0, 'days').format('YYYY-MM-DD'), 'YYYY-MM-DD')
-
- if (!startDate.isValid() || !expireDate.isValid()) {
- return { days: '', daysClass: '', year: '' }
- }
-
- const today = moment().startOf('day')
- const diffDays = expireDate.diff(today, 'days')
-
- let yearNum = 0
- yearNum = getServiceYear(st, et).totalYear
- const daysClass = 'ok'
- return { days: diffDays.toString(), daysClass: daysClass, year: Math.ceil(yearNum).toString() }
- }
- const getDmsDataList = async (params: any) => {
- return await api.getDmsDataList(params)
- }
- const getServiceYear = (start: string | Date, end?: string | Date) => {
- const startM = moment(start)
- const endM = end ? moment(end) : moment()
- let years = endM.year() - startM.year()
- let months = endM.month() - startM.month()
- let days = endM.date() - startM.date()
- // 日期不够,借位
- if (days < 0) {
- months--
- days += startM.clone().add(years, 'y').add(months + 1, 'M').daysInMonth()
- }
- if (months < 0) {
- years--
- months += 12
- }
- // 小数总年(用于展示 2.5 年这种)
- const totalYear = Number((years + months / 12 + days / 365).toFixed(2))
- return {
- years,
- months,
- days,
- totalYear
- }
- }
- const handleRenewalClick = () => {
- getXMProjects(projects.value.c_guid,'xmbh',CONFIG.COLUMNIDARR[2]).then(async res => {
- if(res.length>0){ // 直接进入投续标详情页面
- sessionStorage.setItem('projectName', projects.value.c_xmmc)
- sessionStorage.setItem('name', projects.value.c_xmmc)
- sessionStorage.setItem('dmsId', res[0].id)
- sessionStorage.setItem('progres', JSON.parse(res[0].c_xmjd_json)[JSON.parse(res[0].c_xmjd_json).length-1].xmjd)
- router.push(`/renewal-expire-detail/${res[0].id}`)
- }else{
- const ids = await addProject()
- if(ids){
- sessionStorage.setItem('projectName', projects.value.c_xmmc)
- sessionStorage.setItem('name', projects.value.c_xmmc)
- sessionStorage.setItem('dmsId', `${ids}`)
- sessionStorage.setItem('progres', tempArr.value[tempArr.value.length-1].xmjd)
- router.push(`/renewal-expire-detail/${ids}`)
- }else{
- ElMessage.error('续标失败')
- }
- }
- })
- }
- const handleRenewaXQlClick = () => {
- getXMProjects(projects.value.c_guid,'c_xmbh',CONFIG.COLUMNIDARR[11]).then(async res => {
- if(res.length>0){ // 直接进入投续标详情页面
- sessionStorage.setItem('projectName', projects.value.c_xmmc)
- sessionStorage.setItem('name', projects.value.c_xmmc)
- sessionStorage.setItem('dmsId', res[0].id)
- sessionStorage.setItem('progres', JSON.parse(res[0].c_xmjd_json)[JSON.parse(res[0].c_xmjd_json).length-1].xmjd)
- router.push(`/renewal-xq-detail/${res[0].id}`)
- }else{
- const ids = await addXQProject()
- if(ids){
- sessionStorage.setItem('projectName', projects.value.c_xmmc)
- sessionStorage.setItem('name', projects.value.c_xmmc)
- sessionStorage.setItem('dmsId', `${ids}`)
- sessionStorage.setItem('progres', tempArr.value[tempArr.value.length-1].xmjd)
- router.push(`/renewal-xq-detail/${ids}`)
- }else{
- ElMessage.error('续签失败')
- }
- }
- })
- }
- //获取投续标数据
- const getXMProjects = async (id: string,type: string,columnId:number) => {
- const params: any = {
- columnId: columnId,
- states: '0,1,2,3',
- pageSize: 1,
- page: 0
- }
- if (id !== "") {
- params.search = JSON.stringify([
- ...(params.search ? JSON.parse(params.search) : []),
- {
- field: type,
- searchType: 2,
- content: { value: `%${id}%` },
- },
- ]);
- }
- const res = await getDmsDataList(params)
- if ((res as any).code === 200) {
- return (res as any).content.data
- }else{
- return []
- }
- }
- const addProject = async () => {
- tempArr.value = []
- if(projects.value.days>=100){ // 大于百天,不需要预警
- for(let i = 1; i<3; i++){
- let str = {
- xmjd:i,
- type:i==2?1:0,
- create_time:moment().format('YYYY-MM-DD HH:mm'),
- username:userName,
- userid:userInfo?.id || ''
- }
- tempArr.value.push(str)
- }
- }else{//需要预警的项目
- let str = {
- xmjd:1,
- type:1,
- create_time:'',
- username:userName,
- userid:userInfo?.id || ''
- }
- tempArr.value.push(str)
- }
- let formData = {
- //项目
- title: projects.value.c_xmmc,
- content: projects.value.content,
- c_fbz: projects.value.c_htqyjf,
- // c_jdbm: projects.value.c_jdbm,
- c_zgfw: projects.value.c_wtfs,
- c_nh: projects.value.c_nhhtsr,
- c_xmbtqc: projects.value.c_xmmc,
- c_qx: projects.value.c_sq,
- c_xmjd_json:JSON.stringify(tempArr.value),
- c_kssj: moment(projects.value.c_zgxmhtksrq).format('YYYY-MM-DD HH:mm'),
- c_dqsj: moment(projects.value.c_zgxmhtdqrq).format('YYYY-MM-DD HH:mm'),
- c_xmbt: projects.value.c_xmmc,
- xmbh:projects.value.c_father_code?projects.value.c_father_code:projects.value.c_guid,
- c_glqy: projects.value.c_glqynbkj,
- c_is_xb: true,
- rwlcqd:JSON.stringify(CONFIG.TASKLIST)
-
- }
-
- const params = {
- content: JSON.stringify(formData),
- columnId: CONFIG.COLUMNIDARR[2],
- modelId: CONFIG.MODELIDARR[0],
- }
- const res = await api.addContent(params)
- if((res as any).code === 200){
- // updateProject(projects.value,2)
- return (res as any)?.content
- }else{
- return null
- // ElMessage.error('添加失败')
- }
- }
- const addXQProject = async () => {
- tempArr.value = []
- let str = {
- xmjd:1,
- type:1,
- create_time:'',
- username:userName,
- userid:userInfo?.id || ''
- }
- tempArr.value.push(str)
- let formData = {
- title: projects.value.c_xmmc,
- content: projects.value.content,
- c_wtfs: projects.value.c_wtfs,
- c_htje: projects.value.c_nhhtsr,
- c_qx: projects.value.c_sq,
- c_xmjd_json:JSON.stringify(tempArr.value),
- c_kssj: moment(projects.value.c_zgxmhtksrq).format('YYYY-MM-DD HH:mm'),
- c_jssj: moment(projects.value.c_zgxmhtdqrq).format('YYYY-MM-DD HH:mm'),
- c_xmbt: projects.value.c_xmmc,
- c_xmbh:projects.value.c_guid
- }
-
- const params = {
- content: JSON.stringify(formData),
- columnId: CONFIG.COLUMNIDARR[11],
- modelId: CONFIG.MODELIDARR[6],
- }
- const res = await api.addContent(params)
- if((res as any).code === 200){
- return (res as any)?.content
- }else{
- return null
- // ElMessage.error('添加失败')
- }
- }
- // const updateProject = async (item: any,type:number) => {
- // const params: any = {
- // columnId: CONFIG.COLUMNIDARR[3],
- // modelId: CONFIG.MODELIDARR[1],
- // content: JSON.stringify({
- // id: item.id,
- // c_bid_type: type
- // }),
- // }
- // const res = await api.updateContent(params)
- // if ((res as any).code === 200) {
- // // ElMessage.success('操作成功')
- // getProjects()
- // }else{
- // // ElMessage.error('操作失败')
- // }
- // }
- </script>
- <style scoped>
- .project-page {
- width: 100%;
- /* padding: 16px; */
- background: var(--bg);
- }
- /* 预警栏 */
- .warning-red {
- display: flex;
- justify-content: space-between;
- align-items: center;
- background: linear-gradient(90deg, #fef2f2, #fff);
- border: 1px solid #fecaca;
- border-left: 4px solid var(--danger);
- padding: 14px 16px;
- border-radius: 10px;
- margin-bottom: 16px;
- }
- .warning-green {
- display: flex;
- justify-content: space-between;
- align-items: center;
- background: linear-gradient(90deg, #ecfdf5, #fff);
- border: 1px solid #a7f3d0;
- border-left: 4px solid var(--primary);
- padding: 14px 16px;
- border-radius: 10px;
- margin-bottom: 16px;
- }
- .warning-yellow {
- display: flex;
- justify-content: space-between;
- align-items: center;
- background: linear-gradient(90deg, #f8f5ec, #fff);
- border: 1px solid #fae8ae;
- border-left: 4px solid var(--yellow);
- padding: 14px 16px;
- border-radius: 10px;
- margin-bottom: 16px;
- }
- .warning-orange {
- display: flex;
- justify-content: space-between;
- align-items: center;
- background: linear-gradient(90deg, #f8f2ec, #fff);
- border: 1px solid #f5cca5;
- border-left: 4px solid var(--orange);
- padding: 14px 16px;
- border-radius: 10px;
- margin-bottom: 16px;
- }
- .warning-gray {
- display: flex;
- justify-content: space-between;
- align-items: center;
- background: linear-gradient(90deg, #dddddd, #fff);
- border: 1px solid #dddddd;
- border-left: 4px solid #a3a0a0;
- padding: 14px 16px;
- border-radius: 10px;
- margin-bottom: 16px;
- }
- .warning-info {
- display: grid;
- align-items: center;
- gap: 8px;
- }
- /* .warning-icon {
- display: flex;
- align-items: center;
- } */
- .warning-text {
- font-size: 14px;
- }
- .warning-text-detail {
- font-size: 12px;
- color: var(--text3);
- }
- .warning-danger {
- color: var(--danger);
- font-weight: bold;
- }
- /* 进度条 */
- .progress-bar {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 12px 16px;
- background: #fff;
- border-radius: 8px;
- margin-bottom: 16px;
- }
- .progress-dots {
- display: flex;
- gap: 6px;
- }
- .dot {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- background: #e8ecef;
- }
- .dot.active {
- background: #0d9488;
- }
- .dot.cur {
- background: #2563eb;
- box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2);
- }
- .progress-text {
- font-size: 12px;
- color: var(--text3);
- }
- /* 内容网格 */
- .content-grid {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 16px;
- margin-bottom: 16px;
- }
- @media (max-width: 768px) {
- .content-grid {
- grid-template-columns: 1fr;
- }
- }
- /* 卡片样式 */
- .card {
- background: #fff;
- border-radius: 8px;
- border: 1px solid var(--border);
- }
- .card-header {
- padding: 12px 16px;
- border-bottom: 1px solid var(--border);
- }
- .card-title {
- font-size: 14px;
- font-weight: 600;
- color: var(--text);
- }
- .card-body {
- padding: 12px 16px;
- }
- .info-row {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 8px 0;
- border-bottom: 1px dashed #f0f0f0;
- }
- .info-row:last-child {
- border-bottom: none;
- }
- .label {
- font-size: 13px;
- color: var(--text3);
- }
- .value {
- font-size: 13px;
- color: var(--text);
- font-weight: 500;
- }
- /* 状态标签 */
- .status-badge {
- font-size: 11px;
- padding: 2px 8px;
- border-radius: 4px;
- }
- .status-badge.done {
- background: var(--info-soft);
- color: var(--info);
- }
- .status-badge.warn {
- background: var(--warn-soft);
- color: var(--warn);
- }
- .status-badge.danger {
- background: var(--danger-soft);
- color: var(--danger);
- }
- .status-badge.success {
- background: var(--success-soft);
- color: var(--success);
- }
- /* 续约入口 */
- .renewal-section {
- background: #fff;
- border-radius: 8px;
- border: 1px solid var(--border);
- }
- .section-header {
- padding: 12px 16px;
- border-bottom: 1px solid var(--border);
- }
- .section-title {
- font-size: 14px;
- font-weight: 600;
- color: var(--text);
- }
- .section-body {
- padding: 16px;
- }
- .renewal-desc {
- font-size: 13px;
- color: var(--text2);
- margin-bottom: 12px;
- }
- .renewal-actions {
- display: flex;
- gap: 8px;
- }
- .renewal-actions .el-button {
- font-size: 12px;
- }
- </style>
|