index.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840
  1. <template>
  2. <div class="application-monitor">
  3. <!-- 右侧主内容区 -->
  4. <div class="main-content">
  5. <!-- 应用统计卡片 -->
  6. <div class="stats-cards">
  7. <div class="stat-card" v-for="(item, index) in getLimitedList()" :key="item.name">
  8. <div class="stat-icon approved">
  9. <el-icon>
  10. <CircleCheckFilled v-if="index == 0" style="color: orange" />
  11. <Clock v-else-if="index == 1" style="color: greenyellow" />
  12. <UploadFilled v-else-if="index == 2" style="color: yellow" />
  13. <DeleteFilled v-else style="color: cadetblue" />
  14. </el-icon>
  15. </div>
  16. <div class="stat-content">
  17. <div class="stat-label">{{ item.name }}</div>
  18. <div class="stat-number"><NumberScroll :value="item.value" :duration="2000" /> 个</div>
  19. </div>
  20. </div>
  21. <!-- <div class="stat-card">
  22. <div class="stat-icon approved">
  23. <el-icon><CircleCheckFilled /></el-icon>
  24. </div>
  25. <div class="stat-content">
  26. <div class="stat-label">已审批应用</div>
  27. <div class="stat-number">490个</div>
  28. </div>
  29. </div>
  30. <div class="stat-card">
  31. <div class="stat-icon pending">
  32. <el-icon><Clock /></el-icon>
  33. </div>
  34. <div class="stat-content">
  35. <div class="stat-label">待上架应用</div>
  36. <div class="stat-number">168个</div>
  37. </div>
  38. </div>
  39. <div class="stat-card">
  40. <div class="stat-icon online">
  41. <el-icon><UploadFilled /></el-icon>
  42. </div>
  43. <div class="stat-content">
  44. <div class="stat-label">已上架应用</div>
  45. <div class="stat-number">43个</div>
  46. </div>
  47. </div>
  48. <div class="stat-card">
  49. <div class="stat-icon offline">
  50. <el-icon><DeleteFilled /></el-icon>
  51. </div>
  52. <div class="stat-content">
  53. <div class="stat-label">已下架应用</div>
  54. <div class="stat-number">20个</div>
  55. </div>
  56. </div> -->
  57. </div>
  58. <!-- 内容模块 -->
  59. <div class="content-modules">
  60. <!-- 应用状态分布 -->
  61. <div class="module">
  62. <div class="module-header">
  63. <h3 class="module-title">应用状态分布</h3>
  64. </div>
  65. <div class="module-content">
  66. <div ref="statusChart" class="chart-container"></div>
  67. </div>
  68. </div>
  69. <!-- 快捷应用 -->
  70. <div class="module">
  71. <div class="module-header">
  72. <h3 class="module-title">应用趋势图</h3>
  73. <el-select
  74. v-model="selectedAppRange"
  75. size="small"
  76. style="width: 120px"
  77. @change="handleAppChange"
  78. >
  79. <el-option
  80. v-for="item in selectAapplications"
  81. :key="item.name"
  82. :label="item.name"
  83. :value="item.name"
  84. />
  85. </el-select>
  86. <div>
  87. <el-date-picker
  88. size="small"
  89. :clearable="false"
  90. v-model="datetimeValue"
  91. type="daterange"
  92. @change="handleDateChange"
  93. :shortcuts="shortcuts"
  94. format="YYYY-MM-DD"
  95. range-separator="到"
  96. start-placeholder="开始时间"
  97. end-placeholder="结束时间"
  98. />
  99. </div>
  100. <!-- <a href="#" class="module-more">更多</a> -->
  101. </div>
  102. <div class="module-content">
  103. <div ref="lineChartRes" class="chart-container"></div>
  104. <!-- <div class="quick-app" v-for="(app, index) in quickApps" :key="index">
  105. <div class="app-item">
  106. <span class="app-icon" :style="{ backgroundColor: getAppColor(app.type) }"
  107. ><el-icon><Promotion /></el-icon
  108. ></span>
  109. <span class="app-name">{{ app.name }}</span>
  110. </div>
  111. </div> -->
  112. </div>
  113. </div>
  114. <!-- 应用上新 -->
  115. <div class="module">
  116. <div class="module-header">
  117. <h3 class="module-title">应用上新</h3>
  118. <!-- <a href="#" class="module-more">更多</a> -->
  119. </div>
  120. <div class="module-content">
  121. <div class="new-app" v-for="(app, index) in newApplications" :key="index">
  122. <div class="app-info">
  123. <span class="app-name">{{ app.title }} </span>
  124. <span class="app-date">{{ app.createTime }}</span>
  125. </div>
  126. </div>
  127. </div>
  128. </div>
  129. <!-- 应用热度排行TOP5 -->
  130. <div class="module">
  131. <div class="module-header">
  132. <h3 class="module-title">应用热度排行TOP5</h3>
  133. <!-- <a href="#" class="module-more">更多</a> -->
  134. </div>
  135. <div class="module-content">
  136. <div class="hot-app" v-for="(app, index) in hotApps" :key="index">
  137. <div class="app-rank">
  138. <span class="rank-number">{{ index + 1 }}</span>
  139. <span class="app-name">{{ app.name }}</span>
  140. </div>
  141. <div class="app-stats">
  142. <!-- <div class="stat-bar" :style="{ width: app.percentage + '%' }"></div> -->
  143. <div style="width: 100%">
  144. <el-progress :percentage="app.percentage" />
  145. </div>
  146. <span class="stat-count">{{ app.count }}次</span>
  147. </div>
  148. </div>
  149. </div>
  150. </div>
  151. </div>
  152. </div>
  153. </div>
  154. </template>
  155. <script>
  156. import * as echarts from "echarts";
  157. import appCenter from "@/api/appCenter";
  158. import { topApp, countAppInfo, getAppList } from "@/api/count";
  159. import moment from "moment";
  160. import NumberScroll from "@/components/AppVue/numberScroll.vue";
  161. export default {
  162. name: "ApplicationMonitor",
  163. components: {
  164. NumberScroll,
  165. },
  166. data() {
  167. return {
  168. statusChart: null,
  169. lineChart: null,
  170. itemApplications: [],
  171. applications: [],
  172. newApplications: [],
  173. statusOptions: [],
  174. buffOptions: [],
  175. typeArrs: [],
  176. selectAapplications: [],
  177. selectedAppRange: "",
  178. datetimeValue: [
  179. new Date(new Date(new Date().setMonth(new Date().getMonth() - 1))).setDate(
  180. new Date().getDate() + 1
  181. ),
  182. new Date(),
  183. ], // 默认显示最近一个月
  184. shortcuts: [
  185. {
  186. text: "当天",
  187. value: () => {
  188. const end = new Date();
  189. const start = new Date();
  190. start.setDate(start.getDate());
  191. return [start, end];
  192. },
  193. },
  194. {
  195. text: "最近一周",
  196. value: () => {
  197. const end = new Date();
  198. const start = new Date();
  199. start.setDate(start.getDate() - 7);
  200. start.setDate(start.getDate() + 1);
  201. return [start, end];
  202. },
  203. },
  204. {
  205. text: "最近1月",
  206. value: () => {
  207. const end = new Date();
  208. const start = new Date();
  209. start.setMonth(start.getMonth() - 1);
  210. start.setDate(start.getDate() + 1);
  211. return [start, end];
  212. },
  213. },
  214. {
  215. text: "最近1年",
  216. value: () => {
  217. const end = new Date();
  218. const start = new Date();
  219. start.setFullYear(start.getFullYear() - 1);
  220. start.setDate(start.getDate() + 1);
  221. return [start, end];
  222. },
  223. },
  224. // {
  225. // text: 'Last 3 months',
  226. // value: () => {
  227. // const end = new Date()
  228. // const start = new Date()
  229. // start.setMonth(start.getMonth() - 3)
  230. // return [start, end]
  231. // },
  232. // },
  233. ],
  234. // 快捷应用数据
  235. quickApps: [
  236. { name: "农产品安全管控示范应用场", type: "green" },
  237. { name: "崇明区乡村振兴一张图", type: "yellow" },
  238. { name: "数字孪生黄浦", type: "blue" },
  239. ],
  240. // 应用热度排行数据
  241. hotApps: [],
  242. };
  243. },
  244. mounted() {
  245. this.initData();
  246. window.addEventListener("resize", this.handleResize);
  247. },
  248. beforeUnmount() {
  249. window.removeEventListener("resize", this.handleResize);
  250. this.destroyCharts();
  251. },
  252. methods: {
  253. initData() {
  254. this.getDmsCNameAType("applevel");
  255. this.getDmsCNameAType("appstatus");
  256. this.getDmsDataList();
  257. let param = {
  258. num: 6,
  259. start: "2000-01-01",
  260. end: moment().format("YYYY-MM-DD"),
  261. };
  262. this.getAppCount(param);
  263. },
  264. // 获取应用统计数据
  265. getAppCount(param) {
  266. let that = this;
  267. that.hotApps = [];
  268. topApp(param.num, param.start, param.end).then((res) => {
  269. let maxv = 100;
  270. res.forEach((item, index) => {
  271. if (index < 5) {
  272. if (index === 0) {
  273. maxv = item.count;
  274. }
  275. item.percentage = ((item.count / maxv) * 100).toFixed(1);
  276. let str = {
  277. name: item.name.trim() == "" ? "其他" : item.name,
  278. count: item.count,
  279. percentage: item.percentage,
  280. };
  281. that.hotApps.push(str);
  282. }
  283. });
  284. // console.log(that.hotApps);
  285. });
  286. },
  287. handleAppChange() {
  288. // console.log(this.selectedAppRange);
  289. this.getCountAppInfo();
  290. },
  291. handleDateChange() {
  292. // console.log(this.datetimeValue);
  293. this.getCountAppInfo();
  294. },
  295. //获取应用统计Charts图表
  296. getCountAppInfo() {
  297. let start = moment(this.datetimeValue[0]).format("YYYY-MM-DD 00:00:00");
  298. let end = moment(this.datetimeValue[1]).format("YYYY-MM-DD 23:59:59");
  299. countAppInfo(start, end, this.selectedAppRange).then((res) => {
  300. // console.log(res);
  301. this.drawLineChart(res);
  302. });
  303. },
  304. drawLineChart(data) {
  305. let that = this;
  306. let times = [];
  307. let values = [];
  308. data.forEach((item, index) => {
  309. times.push(moment(item.time).format("YYYY-MM-DD"));
  310. values.push(item.count);
  311. });
  312. // 基于准备好的dom,初始化echarts实例
  313. let lineChart = echarts.init(this.$refs.lineChartRes);
  314. const option = {
  315. tooltip: {
  316. trigger: "axis",
  317. axisPointer: { type: "shadow" },
  318. backgroundColor: "rgba(0, 25, 50, 0.8)",
  319. borderColor: "#1E90FF",
  320. textStyle: {
  321. color: "#fff",
  322. },
  323. formatter: function (params) {
  324. let str = "";
  325. params.forEach((item) => {
  326. str += `${item.name} <br/> ${item.seriesName}: ${item.value}次<br/>`;
  327. });
  328. return str;
  329. },
  330. },
  331. grid: {
  332. left: "3%",
  333. right: "4%",
  334. bottom: "3%",
  335. containLabel: true,
  336. },
  337. xAxis: {
  338. type: "category",
  339. data: times,
  340. axisLabel: {
  341. color: "#a3b6c7",
  342. },
  343. axisLine: {
  344. lineStyle: {
  345. color: "#1E90FF",
  346. },
  347. },
  348. },
  349. yAxis: {
  350. type: "value",
  351. axisLabel: {
  352. color: "#a3b6c7",
  353. },
  354. axisLine: {
  355. lineStyle: {
  356. color: "#1E90FF",
  357. },
  358. },
  359. splitLine: {
  360. lineStyle: {
  361. color: "rgba(30, 144, 255, 0.2)",
  362. },
  363. },
  364. },
  365. series: [
  366. {
  367. name: "调用次数",
  368. type: "line",
  369. data: values,
  370. itemStyle: {
  371. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  372. { offset: 0, color: "#1E90FF" },
  373. { offset: 1, color: "#00BFFF" },
  374. ]),
  375. },
  376. },
  377. ],
  378. };
  379. // 绘制图表
  380. lineChart.setOption(option);
  381. that.lineChart = lineChart;
  382. },
  383. handleResize() {
  384. if (this.statusChart) this.statusChart.resize();
  385. if (this.lineChart) this.lineChart.resize();
  386. },
  387. destroyCharts() {
  388. if (this.statusChart) {
  389. this.statusChart.dispose();
  390. this.statusChart = null;
  391. }
  392. if (this.lineChart) {
  393. this.lineChart.dispose();
  394. this.lineChart = null;
  395. }
  396. },
  397. getLimitedList() {
  398. return this.typeArrs.slice(0, 4); // 获取前四个元素
  399. },
  400. getDmsCNameAType(param) {
  401. if (param === "applevel") {
  402. this.buffOptions = [];
  403. let obj = this.$getDmsTypes("applevel");
  404. for (let key in obj) {
  405. this.buffOptions.push({
  406. label: key,
  407. value: obj[key],
  408. });
  409. }
  410. } else if (param === "appstatus") {
  411. this.statusOptions = [];
  412. let obj = this.$getDmsTypes("appstatus");
  413. for (let key in obj) {
  414. this.statusOptions.push({
  415. label: key,
  416. value: obj[key],
  417. });
  418. }
  419. }
  420. // let requestParams = {
  421. // cName: param,
  422. // type: 0,
  423. // };
  424. // appCenter.getDmsCNameAType(requestParams).then((res) => {
  425. // if (res.code === 200 && res.content) {
  426. // let option = res.content.map((item) => ({
  427. // label: item.index,
  428. // value: item.name,
  429. // }));
  430. // if (param === "applevel") {
  431. // this.buffOptions = option;
  432. // } else if (param === "appstatus") {
  433. // this.statusOptions = option;
  434. // }
  435. // }
  436. // });
  437. },
  438. getDmsDataList() {
  439. let param = {
  440. start: "2000-01-01",
  441. end: moment().format("YYYY-MM-DD"),
  442. };
  443. getAppList(param.start, param.end).then((res) => {
  444. // console.log("=======getAppList======"+res);
  445. this.selectAapplications = res;
  446. this.selectAapplications.forEach((item) => {
  447. if(item.name.includes("华新镇")){
  448. this.selectedAppRange = item.name;
  449. return;
  450. }
  451. });
  452. let requestParams = {
  453. columnId: systemConfig.columnIds[1], // 应用中心栏目id
  454. states: 0,
  455. orderBy: JSON.stringify([{ field: "create_time", orderByType: 2 }]),
  456. pageSize: 9999,
  457. page: 0,
  458. };
  459. appCenter.getDmsDataList(requestParams).then((res) => {
  460. if (res.code === 200) {
  461. this.itemApplications = res.content.data.map((item) => ({
  462. ...item,
  463. status:
  464. item.status === 0
  465. ? "待审核"
  466. : item.status === 1
  467. ? "待发布"
  468. : item.status === 2
  469. ? "未完成"
  470. : "已完成",
  471. buffName:
  472. this.buffOptions.find((info) => info.label == item.appbuff.trim())?.value || "",
  473. appstautName:
  474. this.statusOptions.find((info) => info.label == item.appstauts.trim())?.value || "",
  475. createTime: moment(item.create_time).format("YYYY-MM-DD HH:mm:ss"),
  476. }));
  477. this.applications = this.itemApplications;
  478. // this.selectAapplications = this.applications.filter((item) => item.yylx == "0"); //yylx 0:内部 1:外部
  479. this.newApplications = this.applications.slice(0, 10); // 获取前十个元素
  480. // this.selectedAppRange = this.applications[0].title;
  481. this.getTypeArrs();
  482. this.getCountAppInfo();
  483. } else {
  484. this.applications = [];
  485. this.selectAapplications = [];
  486. this.selectedAppRange = "";
  487. }
  488. });
  489. });
  490. },
  491. getTypeArrs() {
  492. let that = this;
  493. that.statusOptions.forEach((item) => {
  494. let arr = this.applications.filter((info) => info.appstauts == item.label);
  495. let name = item.value,
  496. length = arr.length;
  497. let str = { name: name, value: length };
  498. that.typeArrs.push(str);
  499. });
  500. this.initStatusChart();
  501. },
  502. initStatusChart() {
  503. let colors = ["#52C41A", "#FAAD14", "#F5222D", "#1E90FF"];
  504. let seriesData = this.typeArrs.map((item, index) => ({
  505. // value: item.value, name: item.name, itemStyle: { color: colors[index] } //自定义颜色
  506. value: item.value,
  507. name: item.name, // 随机颜色
  508. }));
  509. seriesData = seriesData.filter((item) => item.value > 0); // 过滤掉值为0的项
  510. this.statusChart = echarts.init(this.$refs.statusChart);
  511. const option = {
  512. tooltip: {
  513. trigger: "item",
  514. backgroundColor: "rgba(0, 25, 50, 0.8)",
  515. borderColor: "#1E90FF",
  516. textStyle: {
  517. color: "#fff",
  518. },
  519. },
  520. legend: {
  521. orient: "horizontal",
  522. bottom: 0,
  523. textStyle: {
  524. color: "#a3b6c7",
  525. },
  526. },
  527. series: [
  528. {
  529. name: "应用状态分布",
  530. type: "pie",
  531. radius: ["40%", "70%"],
  532. avoidLabelOverlap: false,
  533. itemStyle: {
  534. borderRadius: 10,
  535. borderColor: "#0A192F",
  536. borderWidth: 2,
  537. },
  538. label: {
  539. show: false,
  540. position: "center",
  541. },
  542. emphasis: {
  543. label: {
  544. show: true,
  545. fontSize: 20,
  546. fontWeight: "bold",
  547. color: "#fff",
  548. },
  549. },
  550. labelLine: {
  551. show: false,
  552. },
  553. data: seriesData,
  554. },
  555. ],
  556. };
  557. this.statusChart.setOption(option);
  558. },
  559. // 获取应用图标的背景颜色
  560. getAppColor(type) {
  561. const colorMap = {
  562. green: "rgba(103, 194, 58, 0.2)",
  563. yellow: "rgba(230, 162, 60, 0.2)",
  564. blue: "rgba(64, 158, 255, 0.2)",
  565. };
  566. return colorMap[type] || "rgba(64, 158, 255, 0.2)";
  567. },
  568. },
  569. };
  570. </script>
  571. <style lang="less" scoped>
  572. .application-monitor {
  573. width: 100%;
  574. background-color: #00002a;
  575. color: #ffffff;
  576. }
  577. /* 右侧主内容区 */
  578. .main-content {
  579. flex: 1;
  580. // padding: 20px 30px;
  581. padding: 20px 30px 100px 20px;
  582. overflow-y: auto;
  583. }
  584. /* 统计卡片 */
  585. .stats-cards {
  586. display: flex;
  587. gap: 20px;
  588. margin-bottom: 20px;
  589. flex-wrap: wrap;
  590. }
  591. .stat-card {
  592. flex: 1;
  593. min-width: 200px;
  594. height: 100px;
  595. background-color: rgba(255, 255, 255, 0.05);
  596. border-radius: 8px;
  597. padding: 20px;
  598. display: flex;
  599. align-items: center;
  600. gap: 15px;
  601. transition: all 0.3s ease;
  602. &:hover {
  603. background-color: rgba(255, 255, 255, 0.08);
  604. }
  605. }
  606. .stat-icon {
  607. width: 60px;
  608. height: 60px;
  609. border-radius: 50%;
  610. display: flex;
  611. align-items: center;
  612. justify-content: center;
  613. font-size: 24px;
  614. &.approved {
  615. background-color: rgba(64, 158, 255, 0.2);
  616. color: #409eff;
  617. }
  618. &.pending {
  619. background-color: rgba(230, 162, 60, 0.2);
  620. color: #e6a23c;
  621. }
  622. &.online {
  623. background-color: rgba(103, 194, 58, 0.2);
  624. color: #67c23a;
  625. }
  626. &.offline {
  627. background-color: rgba(245, 108, 108, 0.2);
  628. color: #f56c6c;
  629. }
  630. }
  631. .stat-content {
  632. display: flex;
  633. flex-direction: column;
  634. gap: 5px;
  635. }
  636. .stat-label {
  637. font-size: 14px;
  638. color: rgba(255, 255, 255, 0.6);
  639. }
  640. .stat-number {
  641. font-size: 28px;
  642. font-weight: bold;
  643. color: #ffffff;
  644. }
  645. /* 内容模块 */
  646. .content-modules {
  647. display: grid;
  648. grid-template-columns: 1fr 1fr;
  649. grid-template-rows: 1fr 1fr;
  650. gap: 20px;
  651. }
  652. /* 模块通用样式 */
  653. .module {
  654. background-color: rgba(255, 255, 255, 0.05);
  655. border-radius: 8px;
  656. overflow: hidden;
  657. }
  658. .module-header {
  659. padding: 15px 20px;
  660. display: flex;
  661. justify-content: space-between;
  662. align-items: center;
  663. border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  664. }
  665. .module-title {
  666. font-size: 16px;
  667. font-weight: bold;
  668. margin: 0;
  669. color: #ffffff;
  670. }
  671. .module-more {
  672. font-size: 12px;
  673. color: rgba(255, 255, 255, 0.6);
  674. text-decoration: none;
  675. &:hover {
  676. color: #409eff;
  677. }
  678. }
  679. .module-content {
  680. padding: 20px;
  681. height: 320px;
  682. overflow: auto;
  683. }
  684. /* 应用状态分布图 */
  685. .chart-container {
  686. width: 100%;
  687. height: 300px;
  688. }
  689. /* 快捷应用 */
  690. .quick-app {
  691. margin-bottom: 15px;
  692. }
  693. .app-item {
  694. display: flex;
  695. align-items: center;
  696. gap: 10px;
  697. padding: 10px;
  698. background-color: rgba(255, 255, 255, 0.05);
  699. border-radius: 6px;
  700. transition: all 0.3s ease;
  701. &:hover {
  702. background-color: rgba(255, 255, 255, 0.08);
  703. }
  704. }
  705. .app-icon {
  706. width: 32px;
  707. height: 32px;
  708. border-radius: 4px;
  709. display: flex;
  710. align-items: center;
  711. justify-content: center;
  712. color: #ffffff;
  713. font-size: 14px;
  714. }
  715. .app-name {
  716. font-size: 14px;
  717. color: rgba(255, 255, 255, 0.8);
  718. }
  719. /* 应用上新 */
  720. .new-app {
  721. // margin-bottom: 15px;
  722. // padding-bottom: 15px;
  723. padding: 6px 0px 6px 0px;
  724. border-bottom: 1px solid rgba(255, 255, 255, 0.05);
  725. &:last-child {
  726. margin-bottom: 0;
  727. padding-bottom: 0;
  728. border-bottom: none;
  729. }
  730. }
  731. .app-info {
  732. display: flex;
  733. justify-content: space-between;
  734. align-items: center;
  735. }
  736. .app-date {
  737. font-size: 12px;
  738. color: rgba(255, 255, 255, 0.6);
  739. }
  740. /* 使用热度排行 */
  741. .hot-app {
  742. margin-bottom: 20px;
  743. display: flex;
  744. flex-direction: column;
  745. gap: 8px;
  746. }
  747. .app-rank {
  748. display: flex;
  749. align-items: center;
  750. gap: 10px;
  751. }
  752. .rank-number {
  753. width: 20px;
  754. height: 20px;
  755. border-radius: 50%;
  756. background-color: rgba(255, 255, 255, 0.1);
  757. display: flex;
  758. align-items: center;
  759. justify-content: center;
  760. font-size: 12px;
  761. font-weight: bold;
  762. }
  763. .app-stats {
  764. display: flex;
  765. align-items: center;
  766. gap: 10px;
  767. }
  768. .stat-bar {
  769. flex: 1;
  770. height: 6px;
  771. background-color: #409eff;
  772. border-radius: 3px;
  773. }
  774. .stat-count {
  775. font-size: 12px;
  776. color: rgba(255, 255, 255, 0.6);
  777. min-width: 100px;
  778. text-align: right;
  779. }
  780. /* 响应式设计 */
  781. @media (max-width: 1200px) {
  782. .content-modules {
  783. grid-template-columns: 1fr;
  784. grid-template-rows: repeat(4, auto);
  785. }
  786. .stats-cards {
  787. flex-direction: column;
  788. }
  789. .stat-card {
  790. min-width: auto;
  791. }
  792. }
  793. </style>