ProjectDetail.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777
  1. <template>
  2. <div class="project-page">
  3. <!-- 顶部续约预警栏 -->
  4. <div :class="projects.days >= 180 ? 'warning-green' : projects.days >= 90 ? 'warning-yellow' : projects.days >= 60 ? 'warning-orange' : projects.days >= 0 ? 'warning-red' : 'warning-gray'">
  5. <div class="warning-info">
  6. <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>
  7. <div class="warning-text-detail">服务期限 {{ moment(projects.c_zgxmhtksrq).format('YYYY-MM-DD') }} 至 {{ moment(projects.c_zgxmhtdqrq).format('YYYY-MM-DD') }},{{ projects.year }} 年服务 · 请进入项目续标跟进全流程。</div>
  8. </div>
  9. <div>
  10. <el-button type="primary" class="btn enter-btn" @click="handleRenewaXQlClick">
  11. 进入本项目续签
  12. </el-button>
  13. <el-button type="primary" class="btn enter-btn" @click="handleRenewalClick">
  14. 进入本项目续标
  15. </el-button>
  16. </div>
  17. </div>
  18. <!-- 进度指示器 -->
  19. <div class="progress-bar">
  20. <div class="progress-dots">
  21. <span class="dot active"></span>
  22. <span class="dot active"></span>
  23. <span class="dot active"></span>
  24. <span class="dot cur"></span>
  25. <span class="dot"></span>
  26. <span class="dot"></span>
  27. <span class="dot"></span>
  28. <span class="dot"></span>
  29. <span class="dot"></span>
  30. </div>
  31. <span class="progress-text">当前:合同执行,即将进入续约/投标</span>
  32. </div>
  33. <!-- 内容区域 -->
  34. <div class="content-grid">
  35. <!-- 项目与甲方 -->
  36. <div class="card">
  37. <div class="card-header">
  38. <span class="card-title">项目与甲方</span>
  39. </div>
  40. <div class="card-body">
  41. <div class="info-row">
  42. <span class="label">区域</span>
  43. <span class="value">{{ projects.c_glqynbkj }}</span>
  44. </div>
  45. <div class="info-row">
  46. <span class="label">金蝶编码</span>
  47. <span class="value">XM600161</span>
  48. </div>
  49. <div class="info-row">
  50. <span class="label">序号</span>
  51. <span class="value">{{ projects.c_xh }}</span>
  52. </div>
  53. <div class="info-row">
  54. <span class="label">甲方名称</span>
  55. <span class="value">{{ projects.c_htqyjf }}</span>
  56. </div>
  57. <div class="info-row">
  58. <span class="label">项目名称</span>
  59. <span class="value">{{ projects.c_xmmc }}</span>
  60. </div>
  61. <div class="info-row">
  62. <span class="label">项目地址</span>
  63. <span class="value">{{ projects.c_xmdz }}</span>
  64. </div>
  65. <div class="info-row">
  66. <span class="label">建筑面积</span>
  67. <span class="value">{{ projects.c_htmj }} m²</span>
  68. </div>
  69. </div>
  70. </div>
  71. <!-- 合同与服务 -->
  72. <div class="card">
  73. <div class="card-header">
  74. <span class="card-title">合同与服务</span>
  75. </div>
  76. <div class="card-body">
  77. <div class="info-row">
  78. <span class="label">合同起止日期</span>
  79. <span class="value">{{ moment(projects.c_zgxmhtksrq).format('YYYY-MM-DD') }} 至 {{ moment(projects.c_zgxmhtdqrq).format('YYYY-MM-DD') }}</span>
  80. </div>
  81. <div class="info-row">
  82. <span class="label">服务期限</span>
  83. <span class="value"> {{ projects.year }} 年服务</span>
  84. </div>
  85. <div class="info-row">
  86. <span class="label">委托服务</span>
  87. <span class="value">{{ getWTFWLabel(projects.c_wtfs) }}</span>
  88. </div>
  89. <div class="info-row">
  90. <span class="label">计费方式</span>
  91. <span class="value">{{ getJFLabel(projects.c_jffs) }}</span>
  92. </div>
  93. </div>
  94. </div>
  95. <!-- 合同金额 -->
  96. <div class="card">
  97. <div class="card-header">
  98. <span class="card-title">合同金额</span>
  99. </div>
  100. <div class="card-body">
  101. <div class="info-row">
  102. <span class="label">合同金额</span>
  103. <span class="value">{{ formatCurrency(projects.c_htzj/10000) }} 万元</span>
  104. </div>
  105. <div class="info-row">
  106. <span class="label">年化合同额</span>
  107. <span class="value">{{ formatCurrency(projects.c_nhhtsr/10000) }} 万元</span>
  108. </div>
  109. <div class="info-row">
  110. <span class="label">当年饱和收入</span>
  111. <span class="value">{{ formatCurrency(projects.c_dnbhsr/10000) }} 万元</span>
  112. </div>
  113. <div class="info-row">
  114. <span class="label">投标核算年度总成本</span>
  115. <span class="value">{{ formatCurrency(projects.c_tbhsndzcb/10000) }} 万元</span>
  116. </div>
  117. <div class="info-row">
  118. <span class="label">自用人力总成本</span>
  119. <span class="value">{{ formatCurrency(projects.c_zyrlzcbjss/10000) }} 万元</span>
  120. </div>
  121. <div class="info-row">
  122. <span class="label">综合成本</span>
  123. <span class="value">{{ formatCurrency(projects.c_zhcbjss/10000) }} 万元</span>
  124. </div>
  125. </div>
  126. </div>
  127. <!-- 历史投标 -->
  128. <div class="card">
  129. <div class="card-header">
  130. <span class="card-title">历史投标</span>
  131. </div>
  132. <div class="card-body">
  133. <div class="info-row" v-for="item in historyBidList" :key="item.year">
  134. <span class="label">{{ item.year }}年</span>
  135. <span class="value">中标 {{ formatCurrency(item.money/10000) }} 万元</span>
  136. </div>
  137. <!-- <div class="info-row">
  138. <span class="label">2024年</span>
  139. <span class="value">中标 6,200,000 元</span>
  140. </div>
  141. <div class="info-row">
  142. <span class="label">2023年</span>
  143. <span class="value">中标 6,800,000 元</span>
  144. </div>
  145. <div class="info-row">
  146. <span class="label">2020年</span>
  147. <span class="value">中标 3,680,000 元</span>
  148. </div> -->
  149. </div>
  150. </div>
  151. </div>
  152. <!-- 续约入口 -->
  153. <!-- <div class="renewal-section">
  154. <div class="section-header">
  155. <span class="section-title">续标入口</span>
  156. </div>
  157. <div class="section-body">
  158. <p class="renewal-desc">续标请从项目续标列表进入,或直接进入本项目的续标详情页。</p>
  159. <div class="renewal-actions">
  160. <el-button class="btn" @click="goToPage('/renewal')">投标列表</el-button>
  161. <el-button type="primary" class="btn enter-btn" @click="handleRenewalClick">进入本项目续标</el-button>
  162. </div>
  163. </div>
  164. </div> -->
  165. </div>
  166. </template>
  167. <script setup lang="ts">
  168. import { ref, onMounted } from 'vue'
  169. import api from '@/api/common'
  170. import moment from 'moment'
  171. import CONFIG from '@/utils/config'
  172. import { useRouter } from 'vue-router'
  173. const router = useRouter()
  174. import { ElMessage } from 'element-plus'
  175. const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
  176. const userName = userInfo?.username =='user_xtw' ? '徐天炜' : userInfo?.username || ''
  177. const formatCurrency = (value: string | number): string => {
  178. if (!value) return '0.00'
  179. const num = typeof value === 'string' ? parseFloat(value) : value
  180. if (isNaN(num)) return '0.00'
  181. return num.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
  182. }
  183. const projects = ref<any>({})
  184. const tempArr = ref<any>([])
  185. const wtfwOptions = ref<any>([])
  186. const jffsOptions = ref<any>([])
  187. const historyBidList = ref<any>([
  188. {
  189. year: '2019',
  190. money: '8585360'
  191. },
  192. {
  193. year: '2020',
  194. money: '8585360'
  195. },
  196. {
  197. year: '2021',
  198. money: '8585360'
  199. },
  200. {
  201. year: '2022',
  202. money: '8499576'
  203. },
  204. {
  205. year: '2023',
  206. money: '8499576'
  207. },
  208. {
  209. year: '2024',
  210. money: '8499576'
  211. },
  212. {
  213. year: '2025',
  214. money: '8395152'
  215. },
  216. ])
  217. onMounted(() => {
  218. initCName()
  219. initData()
  220. })
  221. const initData = () => {
  222. getProjects();
  223. }
  224. const getJFLabel = (value: string) => {
  225. return jffsOptions.value.find((item: any) => item.value == value)?.label || '-'
  226. }
  227. const getWTFWLabel = (value: string) => {
  228. return wtfwOptions.value.find((item: any) => item.value == value)?.label || '-'
  229. }
  230. const getDmsTypes = async (cName: string) => {
  231. const params = {
  232. type: 0,
  233. cName: cName,
  234. }
  235. const res = await api.getDmsTypes(params)
  236. if ((res as any).code === 200) {
  237. const ret = (res as any).content
  238. return ret.map((item: any) => ({
  239. ...item,
  240. label: item.name,
  241. value: item.index
  242. }))
  243. } else {
  244. return []
  245. }
  246. }
  247. const initCName = async () => {
  248. // 并行请求两个字典,比await串行更快
  249. const [wtOptions, jfOptions] = await Promise.all([
  250. getDmsTypes("sq_wtfs"),
  251. getDmsTypes("sq_jffs")
  252. ])
  253. wtfwOptions.value = wtOptions
  254. jffsOptions.value = jfOptions
  255. }
  256. const getProjects = async () => {
  257. const params: any = {
  258. columnId: CONFIG.COLUMNIDARR[0],
  259. orderBy: JSON.stringify([{ field: "update_time", orderByType: 2 }]),
  260. states: '0,1,2,3',
  261. pageSize: 1,
  262. page: 0
  263. }
  264. if (sessionStorage.getItem('dmsId')) {
  265. params.search = JSON.stringify([
  266. ...(params.search ? JSON.parse(params.search) : []),
  267. {
  268. field: "id",
  269. searchType: 2,
  270. content: { value: sessionStorage.getItem('dmsId') },
  271. },
  272. ]);
  273. }
  274. const res = await getDmsDataList(params)
  275. if ((res as any).code === 200) {
  276. projects.value = (res as any).content.data.map((item: any) => ({
  277. ...item,
  278. ...calculateRemaining(item.c_zgxmhtksrq, item.c_zgxmhtdqrq)
  279. }))[0]
  280. }
  281. }
  282. const calculateRemaining = (st: string, et: string): { days: string; daysClass: string; year: string } => {
  283. if (!et) return { days: '', daysClass: '', year: '' }
  284. const startDate = moment(moment(st).format('YYYY-MM-DD'), 'YYYY-MM-DD')
  285. const expireDate = moment(moment(et).add(0, 'days').format('YYYY-MM-DD'), 'YYYY-MM-DD')
  286. if (!startDate.isValid() || !expireDate.isValid()) {
  287. return { days: '', daysClass: '', year: '' }
  288. }
  289. const today = moment().startOf('day')
  290. const diffDays = expireDate.diff(today, 'days')
  291. let yearNum = 0
  292. yearNum = getServiceYear(st, et).totalYear
  293. const daysClass = 'ok'
  294. return { days: diffDays.toString(), daysClass: daysClass, year: Math.ceil(yearNum).toString() }
  295. }
  296. const getDmsDataList = async (params: any) => {
  297. return await api.getDmsDataList(params)
  298. }
  299. const getServiceYear = (start: string | Date, end?: string | Date) => {
  300. const startM = moment(start)
  301. const endM = end ? moment(end) : moment()
  302. let years = endM.year() - startM.year()
  303. let months = endM.month() - startM.month()
  304. let days = endM.date() - startM.date()
  305. // 日期不够,借位
  306. if (days < 0) {
  307. months--
  308. days += startM.clone().add(years, 'y').add(months + 1, 'M').daysInMonth()
  309. }
  310. if (months < 0) {
  311. years--
  312. months += 12
  313. }
  314. // 小数总年(用于展示 2.5 年这种)
  315. const totalYear = Number((years + months / 12 + days / 365).toFixed(2))
  316. return {
  317. years,
  318. months,
  319. days,
  320. totalYear
  321. }
  322. }
  323. const handleRenewalClick = () => {
  324. getXMProjects(projects.value.c_guid,'xmbh',CONFIG.COLUMNIDARR[2]).then(async res => {
  325. if(res.length>0){ // 直接进入投续标详情页面
  326. sessionStorage.setItem('projectName', projects.value.c_xmmc)
  327. sessionStorage.setItem('name', projects.value.c_xmmc)
  328. sessionStorage.setItem('dmsId', res[0].id)
  329. sessionStorage.setItem('progres', JSON.parse(res[0].c_xmjd_json)[JSON.parse(res[0].c_xmjd_json).length-1].xmjd)
  330. router.push(`/renewal-expire-detail/${res[0].id}`)
  331. }else{
  332. const ids = await addProject()
  333. if(ids){
  334. sessionStorage.setItem('projectName', projects.value.c_xmmc)
  335. sessionStorage.setItem('name', projects.value.c_xmmc)
  336. sessionStorage.setItem('dmsId', `${ids}`)
  337. sessionStorage.setItem('progres', tempArr.value[tempArr.value.length-1].xmjd)
  338. router.push(`/renewal-expire-detail/${ids}`)
  339. }else{
  340. ElMessage.error('续标失败')
  341. }
  342. }
  343. })
  344. }
  345. const handleRenewaXQlClick = () => {
  346. getXMProjects(projects.value.c_guid,'c_xmbh',CONFIG.COLUMNIDARR[11]).then(async res => {
  347. if(res.length>0){ // 直接进入投续标详情页面
  348. sessionStorage.setItem('projectName', projects.value.c_xmmc)
  349. sessionStorage.setItem('name', projects.value.c_xmmc)
  350. sessionStorage.setItem('dmsId', res[0].id)
  351. sessionStorage.setItem('progres', JSON.parse(res[0].c_xmjd_json)[JSON.parse(res[0].c_xmjd_json).length-1].xmjd)
  352. router.push(`/renewal-xq-detail/${res[0].id}`)
  353. }else{
  354. const ids = await addXQProject()
  355. if(ids){
  356. sessionStorage.setItem('projectName', projects.value.c_xmmc)
  357. sessionStorage.setItem('name', projects.value.c_xmmc)
  358. sessionStorage.setItem('dmsId', `${ids}`)
  359. sessionStorage.setItem('progres', tempArr.value[tempArr.value.length-1].xmjd)
  360. router.push(`/renewal-xq-detail/${ids}`)
  361. }else{
  362. ElMessage.error('续签失败')
  363. }
  364. }
  365. })
  366. }
  367. //获取投续标数据
  368. const getXMProjects = async (id: string,type: string,columnId:number) => {
  369. const params: any = {
  370. columnId: columnId,
  371. states: '0,1,2,3',
  372. pageSize: 1,
  373. page: 0
  374. }
  375. if (id !== "") {
  376. params.search = JSON.stringify([
  377. ...(params.search ? JSON.parse(params.search) : []),
  378. {
  379. field: type,
  380. searchType: 2,
  381. content: { value: `%${id}%` },
  382. },
  383. ]);
  384. }
  385. const res = await getDmsDataList(params)
  386. if ((res as any).code === 200) {
  387. return (res as any).content.data
  388. }else{
  389. return []
  390. }
  391. }
  392. const addProject = async () => {
  393. tempArr.value = []
  394. if(projects.value.days>=100){ // 大于百天,不需要预警
  395. for(let i = 1; i<3; i++){
  396. let str = {
  397. xmjd:i,
  398. type:i==2?1:0,
  399. create_time:moment().format('YYYY-MM-DD HH:mm'),
  400. username:userName,
  401. userid:userInfo?.id || ''
  402. }
  403. tempArr.value.push(str)
  404. }
  405. }else{//需要预警的项目
  406. let str = {
  407. xmjd:1,
  408. type:1,
  409. create_time:'',
  410. username:userName,
  411. userid:userInfo?.id || ''
  412. }
  413. tempArr.value.push(str)
  414. }
  415. let formData = {
  416. //项目
  417. title: projects.value.c_xmmc,
  418. content: projects.value.content,
  419. c_fbz: projects.value.c_htqyjf,
  420. // c_jdbm: projects.value.c_jdbm,
  421. c_zgfw: projects.value.c_wtfs,
  422. c_nh: projects.value.c_nhhtsr,
  423. c_xmbtqc: projects.value.c_xmmc,
  424. c_qx: projects.value.c_sq,
  425. c_xmjd_json:JSON.stringify(tempArr.value),
  426. c_kssj: moment(projects.value.c_zgxmhtksrq).format('YYYY-MM-DD HH:mm'),
  427. c_dqsj: moment(projects.value.c_zgxmhtdqrq).format('YYYY-MM-DD HH:mm'),
  428. c_xmbt: projects.value.c_xmmc,
  429. xmbh:projects.value.c_father_code?projects.value.c_father_code:projects.value.c_guid,
  430. c_glqy: projects.value.c_glqynbkj,
  431. c_is_xb: true,
  432. rwlcqd:JSON.stringify(CONFIG.TASKLIST)
  433. }
  434. const params = {
  435. content: JSON.stringify(formData),
  436. columnId: CONFIG.COLUMNIDARR[2],
  437. modelId: CONFIG.MODELIDARR[0],
  438. }
  439. const res = await api.addContent(params)
  440. if((res as any).code === 200){
  441. // updateProject(projects.value,2)
  442. return (res as any)?.content
  443. }else{
  444. return null
  445. // ElMessage.error('添加失败')
  446. }
  447. }
  448. const addXQProject = async () => {
  449. tempArr.value = []
  450. let str = {
  451. xmjd:1,
  452. type:1,
  453. create_time:'',
  454. username:userName,
  455. userid:userInfo?.id || ''
  456. }
  457. tempArr.value.push(str)
  458. let formData = {
  459. title: projects.value.c_xmmc,
  460. content: projects.value.content,
  461. c_wtfs: projects.value.c_wtfs,
  462. c_htje: projects.value.c_nhhtsr,
  463. c_qx: projects.value.c_sq,
  464. c_xmjd_json:JSON.stringify(tempArr.value),
  465. c_kssj: moment(projects.value.c_zgxmhtksrq).format('YYYY-MM-DD HH:mm'),
  466. c_jssj: moment(projects.value.c_zgxmhtdqrq).format('YYYY-MM-DD HH:mm'),
  467. c_xmbt: projects.value.c_xmmc,
  468. c_xmbh:projects.value.c_guid
  469. }
  470. const params = {
  471. content: JSON.stringify(formData),
  472. columnId: CONFIG.COLUMNIDARR[11],
  473. modelId: CONFIG.MODELIDARR[6],
  474. }
  475. const res = await api.addContent(params)
  476. if((res as any).code === 200){
  477. return (res as any)?.content
  478. }else{
  479. return null
  480. // ElMessage.error('添加失败')
  481. }
  482. }
  483. // const updateProject = async (item: any,type:number) => {
  484. // const params: any = {
  485. // columnId: CONFIG.COLUMNIDARR[3],
  486. // modelId: CONFIG.MODELIDARR[1],
  487. // content: JSON.stringify({
  488. // id: item.id,
  489. // c_bid_type: type
  490. // }),
  491. // }
  492. // const res = await api.updateContent(params)
  493. // if ((res as any).code === 200) {
  494. // // ElMessage.success('操作成功')
  495. // getProjects()
  496. // }else{
  497. // // ElMessage.error('操作失败')
  498. // }
  499. // }
  500. </script>
  501. <style scoped>
  502. .project-page {
  503. width: 100%;
  504. /* padding: 16px; */
  505. background: var(--bg);
  506. }
  507. /* 预警栏 */
  508. .warning-red {
  509. display: flex;
  510. justify-content: space-between;
  511. align-items: center;
  512. background: linear-gradient(90deg, #fef2f2, #fff);
  513. border: 1px solid #fecaca;
  514. border-left: 4px solid var(--danger);
  515. padding: 14px 16px;
  516. border-radius: 10px;
  517. margin-bottom: 16px;
  518. }
  519. .warning-green {
  520. display: flex;
  521. justify-content: space-between;
  522. align-items: center;
  523. background: linear-gradient(90deg, #ecfdf5, #fff);
  524. border: 1px solid #a7f3d0;
  525. border-left: 4px solid var(--primary);
  526. padding: 14px 16px;
  527. border-radius: 10px;
  528. margin-bottom: 16px;
  529. }
  530. .warning-yellow {
  531. display: flex;
  532. justify-content: space-between;
  533. align-items: center;
  534. background: linear-gradient(90deg, #f8f5ec, #fff);
  535. border: 1px solid #fae8ae;
  536. border-left: 4px solid var(--yellow);
  537. padding: 14px 16px;
  538. border-radius: 10px;
  539. margin-bottom: 16px;
  540. }
  541. .warning-orange {
  542. display: flex;
  543. justify-content: space-between;
  544. align-items: center;
  545. background: linear-gradient(90deg, #f8f2ec, #fff);
  546. border: 1px solid #f5cca5;
  547. border-left: 4px solid var(--orange);
  548. padding: 14px 16px;
  549. border-radius: 10px;
  550. margin-bottom: 16px;
  551. }
  552. .warning-gray {
  553. display: flex;
  554. justify-content: space-between;
  555. align-items: center;
  556. background: linear-gradient(90deg, #dddddd, #fff);
  557. border: 1px solid #dddddd;
  558. border-left: 4px solid #a3a0a0;
  559. padding: 14px 16px;
  560. border-radius: 10px;
  561. margin-bottom: 16px;
  562. }
  563. .warning-info {
  564. display: grid;
  565. align-items: center;
  566. gap: 8px;
  567. }
  568. /* .warning-icon {
  569. display: flex;
  570. align-items: center;
  571. } */
  572. .warning-text {
  573. font-size: 14px;
  574. }
  575. .warning-text-detail {
  576. font-size: 12px;
  577. color: var(--text3);
  578. }
  579. .warning-danger {
  580. color: var(--danger);
  581. font-weight: bold;
  582. }
  583. /* 进度条 */
  584. .progress-bar {
  585. display: flex;
  586. align-items: center;
  587. gap: 12px;
  588. padding: 12px 16px;
  589. background: #fff;
  590. border-radius: 8px;
  591. margin-bottom: 16px;
  592. }
  593. .progress-dots {
  594. display: flex;
  595. gap: 6px;
  596. }
  597. .dot {
  598. width: 8px;
  599. height: 8px;
  600. border-radius: 50%;
  601. background: #e8ecef;
  602. }
  603. .dot.active {
  604. background: #0d9488;
  605. }
  606. .dot.cur {
  607. background: #2563eb;
  608. box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2);
  609. }
  610. .progress-text {
  611. font-size: 12px;
  612. color: var(--text3);
  613. }
  614. /* 内容网格 */
  615. .content-grid {
  616. display: grid;
  617. grid-template-columns: repeat(2, 1fr);
  618. gap: 16px;
  619. margin-bottom: 16px;
  620. }
  621. @media (max-width: 768px) {
  622. .content-grid {
  623. grid-template-columns: 1fr;
  624. }
  625. }
  626. /* 卡片样式 */
  627. .card {
  628. background: #fff;
  629. border-radius: 8px;
  630. border: 1px solid var(--border);
  631. }
  632. .card-header {
  633. padding: 12px 16px;
  634. border-bottom: 1px solid var(--border);
  635. }
  636. .card-title {
  637. font-size: 14px;
  638. font-weight: 600;
  639. color: var(--text);
  640. }
  641. .card-body {
  642. padding: 12px 16px;
  643. }
  644. .info-row {
  645. display: flex;
  646. justify-content: space-between;
  647. align-items: center;
  648. padding: 8px 0;
  649. border-bottom: 1px dashed #f0f0f0;
  650. }
  651. .info-row:last-child {
  652. border-bottom: none;
  653. }
  654. .label {
  655. font-size: 13px;
  656. color: var(--text3);
  657. }
  658. .value {
  659. font-size: 13px;
  660. color: var(--text);
  661. font-weight: 500;
  662. }
  663. /* 状态标签 */
  664. .status-badge {
  665. font-size: 11px;
  666. padding: 2px 8px;
  667. border-radius: 4px;
  668. }
  669. .status-badge.done {
  670. background: var(--info-soft);
  671. color: var(--info);
  672. }
  673. .status-badge.warn {
  674. background: var(--warn-soft);
  675. color: var(--warn);
  676. }
  677. .status-badge.danger {
  678. background: var(--danger-soft);
  679. color: var(--danger);
  680. }
  681. .status-badge.success {
  682. background: var(--success-soft);
  683. color: var(--success);
  684. }
  685. /* 续约入口 */
  686. .renewal-section {
  687. background: #fff;
  688. border-radius: 8px;
  689. border: 1px solid var(--border);
  690. }
  691. .section-header {
  692. padding: 12px 16px;
  693. border-bottom: 1px solid var(--border);
  694. }
  695. .section-title {
  696. font-size: 14px;
  697. font-weight: 600;
  698. color: var(--text);
  699. }
  700. .section-body {
  701. padding: 16px;
  702. }
  703. .renewal-desc {
  704. font-size: 13px;
  705. color: var(--text2);
  706. margin-bottom: 12px;
  707. }
  708. .renewal-actions {
  709. display: flex;
  710. gap: 8px;
  711. }
  712. .renewal-actions .el-button {
  713. font-size: 12px;
  714. }
  715. </style>