index.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836
  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 requestParams = {
  440. columnId: systemConfig.columnIds[1], // 应用中心栏目id
  441. states: 0,
  442. orderBy: JSON.stringify([{ field: "create_time", orderByType: 2 }]),
  443. pageSize: 9999,
  444. page: 0,
  445. };
  446. appCenter.getDmsDataList(requestParams).then((res) => {
  447. if (res.code === 200) {
  448. this.itemApplications = res.content.data.map((item) => ({
  449. ...item,
  450. status:
  451. item.status === 0
  452. ? "待审核"
  453. : item.status === 1
  454. ? "待发布"
  455. : item.status === 2
  456. ? "未完成"
  457. : "已完成",
  458. buffName:
  459. this.buffOptions.find((info) => info.label == item.appbuff.trim())?.value || "",
  460. appstautName:
  461. this.statusOptions.find((info) => info.label == item.appstauts.trim())?.value || "",
  462. createTime: moment(item.create_time).format("YYYY-MM-DD HH:mm:ss"),
  463. }));
  464. this.applications = this.itemApplications;
  465. // this.selectAapplications = this.applications.filter((item) => item.yylx == "0"); //yylx 0:内部 1:外部
  466. this.newApplications = this.applications.slice(0, 10); // 获取前十个元素
  467. // this.selectedAppRange = this.applications[0].title;
  468. this.getTypeArrs();
  469. this.getCountAppInfo();
  470. } else {
  471. this.applications = [];
  472. this.selectAapplications = [];
  473. this.selectedAppRange = "";
  474. }
  475. });
  476. let param = {
  477. start: "2000-01-01",
  478. end: moment().format("YYYY-MM-DD"),
  479. };
  480. getAppList(param.start, param.end).then((res) => {
  481. console.log("=======getAppList======"+res);
  482. this.selectAapplications = res;
  483. this.selectedAppRange = this.selectAapplications[0].name;
  484. });
  485. },
  486. getTypeArrs() {
  487. let that = this;
  488. that.statusOptions.forEach((item) => {
  489. let arr = this.applications.filter((info) => info.appstauts == item.label);
  490. let name = item.value,
  491. length = arr.length;
  492. let str = { name: name, value: length };
  493. that.typeArrs.push(str);
  494. });
  495. this.initStatusChart();
  496. },
  497. initStatusChart() {
  498. let colors = ["#52C41A", "#FAAD14", "#F5222D", "#1E90FF"];
  499. let seriesData = this.typeArrs.map((item, index) => ({
  500. // value: item.value, name: item.name, itemStyle: { color: colors[index] } //自定义颜色
  501. value: item.value,
  502. name: item.name, // 随机颜色
  503. }));
  504. seriesData = seriesData.filter((item) => item.value > 0); // 过滤掉值为0的项
  505. this.statusChart = echarts.init(this.$refs.statusChart);
  506. const option = {
  507. tooltip: {
  508. trigger: "item",
  509. backgroundColor: "rgba(0, 25, 50, 0.8)",
  510. borderColor: "#1E90FF",
  511. textStyle: {
  512. color: "#fff",
  513. },
  514. },
  515. legend: {
  516. orient: "horizontal",
  517. bottom: 0,
  518. textStyle: {
  519. color: "#a3b6c7",
  520. },
  521. },
  522. series: [
  523. {
  524. name: "应用状态分布",
  525. type: "pie",
  526. radius: ["40%", "70%"],
  527. avoidLabelOverlap: false,
  528. itemStyle: {
  529. borderRadius: 10,
  530. borderColor: "#0A192F",
  531. borderWidth: 2,
  532. },
  533. label: {
  534. show: false,
  535. position: "center",
  536. },
  537. emphasis: {
  538. label: {
  539. show: true,
  540. fontSize: 20,
  541. fontWeight: "bold",
  542. color: "#fff",
  543. },
  544. },
  545. labelLine: {
  546. show: false,
  547. },
  548. data: seriesData,
  549. },
  550. ],
  551. };
  552. this.statusChart.setOption(option);
  553. },
  554. // 获取应用图标的背景颜色
  555. getAppColor(type) {
  556. const colorMap = {
  557. green: "rgba(103, 194, 58, 0.2)",
  558. yellow: "rgba(230, 162, 60, 0.2)",
  559. blue: "rgba(64, 158, 255, 0.2)",
  560. };
  561. return colorMap[type] || "rgba(64, 158, 255, 0.2)";
  562. },
  563. },
  564. };
  565. </script>
  566. <style lang="less" scoped>
  567. .application-monitor {
  568. width: 100%;
  569. background-color: #00002a;
  570. color: #ffffff;
  571. }
  572. /* 右侧主内容区 */
  573. .main-content {
  574. flex: 1;
  575. // padding: 20px 30px;
  576. padding: 20px 30px 100px 20px;
  577. overflow-y: auto;
  578. }
  579. /* 统计卡片 */
  580. .stats-cards {
  581. display: flex;
  582. gap: 20px;
  583. margin-bottom: 20px;
  584. flex-wrap: wrap;
  585. }
  586. .stat-card {
  587. flex: 1;
  588. min-width: 200px;
  589. height: 100px;
  590. background-color: rgba(255, 255, 255, 0.05);
  591. border-radius: 8px;
  592. padding: 20px;
  593. display: flex;
  594. align-items: center;
  595. gap: 15px;
  596. transition: all 0.3s ease;
  597. &:hover {
  598. background-color: rgba(255, 255, 255, 0.08);
  599. }
  600. }
  601. .stat-icon {
  602. width: 60px;
  603. height: 60px;
  604. border-radius: 50%;
  605. display: flex;
  606. align-items: center;
  607. justify-content: center;
  608. font-size: 24px;
  609. &.approved {
  610. background-color: rgba(64, 158, 255, 0.2);
  611. color: #409eff;
  612. }
  613. &.pending {
  614. background-color: rgba(230, 162, 60, 0.2);
  615. color: #e6a23c;
  616. }
  617. &.online {
  618. background-color: rgba(103, 194, 58, 0.2);
  619. color: #67c23a;
  620. }
  621. &.offline {
  622. background-color: rgba(245, 108, 108, 0.2);
  623. color: #f56c6c;
  624. }
  625. }
  626. .stat-content {
  627. display: flex;
  628. flex-direction: column;
  629. gap: 5px;
  630. }
  631. .stat-label {
  632. font-size: 14px;
  633. color: rgba(255, 255, 255, 0.6);
  634. }
  635. .stat-number {
  636. font-size: 28px;
  637. font-weight: bold;
  638. color: #ffffff;
  639. }
  640. /* 内容模块 */
  641. .content-modules {
  642. display: grid;
  643. grid-template-columns: 1fr 1fr;
  644. grid-template-rows: 1fr 1fr;
  645. gap: 20px;
  646. }
  647. /* 模块通用样式 */
  648. .module {
  649. background-color: rgba(255, 255, 255, 0.05);
  650. border-radius: 8px;
  651. overflow: hidden;
  652. }
  653. .module-header {
  654. padding: 15px 20px;
  655. display: flex;
  656. justify-content: space-between;
  657. align-items: center;
  658. border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  659. }
  660. .module-title {
  661. font-size: 16px;
  662. font-weight: bold;
  663. margin: 0;
  664. color: #ffffff;
  665. }
  666. .module-more {
  667. font-size: 12px;
  668. color: rgba(255, 255, 255, 0.6);
  669. text-decoration: none;
  670. &:hover {
  671. color: #409eff;
  672. }
  673. }
  674. .module-content {
  675. padding: 20px;
  676. height: 320px;
  677. overflow: auto;
  678. }
  679. /* 应用状态分布图 */
  680. .chart-container {
  681. width: 100%;
  682. height: 300px;
  683. }
  684. /* 快捷应用 */
  685. .quick-app {
  686. margin-bottom: 15px;
  687. }
  688. .app-item {
  689. display: flex;
  690. align-items: center;
  691. gap: 10px;
  692. padding: 10px;
  693. background-color: rgba(255, 255, 255, 0.05);
  694. border-radius: 6px;
  695. transition: all 0.3s ease;
  696. &:hover {
  697. background-color: rgba(255, 255, 255, 0.08);
  698. }
  699. }
  700. .app-icon {
  701. width: 32px;
  702. height: 32px;
  703. border-radius: 4px;
  704. display: flex;
  705. align-items: center;
  706. justify-content: center;
  707. color: #ffffff;
  708. font-size: 14px;
  709. }
  710. .app-name {
  711. font-size: 14px;
  712. color: rgba(255, 255, 255, 0.8);
  713. }
  714. /* 应用上新 */
  715. .new-app {
  716. // margin-bottom: 15px;
  717. // padding-bottom: 15px;
  718. padding: 6px 0px 6px 0px;
  719. border-bottom: 1px solid rgba(255, 255, 255, 0.05);
  720. &:last-child {
  721. margin-bottom: 0;
  722. padding-bottom: 0;
  723. border-bottom: none;
  724. }
  725. }
  726. .app-info {
  727. display: flex;
  728. justify-content: space-between;
  729. align-items: center;
  730. }
  731. .app-date {
  732. font-size: 12px;
  733. color: rgba(255, 255, 255, 0.6);
  734. }
  735. /* 使用热度排行 */
  736. .hot-app {
  737. margin-bottom: 20px;
  738. display: flex;
  739. flex-direction: column;
  740. gap: 8px;
  741. }
  742. .app-rank {
  743. display: flex;
  744. align-items: center;
  745. gap: 10px;
  746. }
  747. .rank-number {
  748. width: 20px;
  749. height: 20px;
  750. border-radius: 50%;
  751. background-color: rgba(255, 255, 255, 0.1);
  752. display: flex;
  753. align-items: center;
  754. justify-content: center;
  755. font-size: 12px;
  756. font-weight: bold;
  757. }
  758. .app-stats {
  759. display: flex;
  760. align-items: center;
  761. gap: 10px;
  762. }
  763. .stat-bar {
  764. flex: 1;
  765. height: 6px;
  766. background-color: #409eff;
  767. border-radius: 3px;
  768. }
  769. .stat-count {
  770. font-size: 12px;
  771. color: rgba(255, 255, 255, 0.6);
  772. min-width: 100px;
  773. text-align: right;
  774. }
  775. /* 响应式设计 */
  776. @media (max-width: 1200px) {
  777. .content-modules {
  778. grid-template-columns: 1fr;
  779. grid-template-rows: repeat(4, auto);
  780. }
  781. .stats-cards {
  782. flex-direction: column;
  783. }
  784. .stat-card {
  785. min-width: auto;
  786. }
  787. }
  788. </style>