Bläddra i källkod

be 完成柱状图

master
Chunxian Zhang 2 år sedan
förälder
incheckning
cd2418c5f4
7 ändrade filer med 348 tillägg och 52 borttagningar
  1. +17
    -15
      lazy-timer-be/app.py
  2. +113
    -0
      lazy-timer-fe/package-lock.json
  3. +2
    -0
      lazy-timer-fe/package.json
  4. +1
    -1
      lazy-timer-fe/src/App.js
  5. +213
    -32
      lazy-timer-fe/src/pages/Charts/Bar.jsx
  6. +0
    -3
      lazy-timer-fe/src/pages/Ecommerce.jsx
  7. +2
    -1
      lazy-timer-fe/src/pages/Orders.jsx

+ 17
- 15
lazy-timer-be/app.py Visa fil

@ -50,7 +50,6 @@ class Win(db.Model):
@app.route('/getDayScreenUseTime', methods=['POST'])
@cross_origin()
def getDayScreenUseTime():
# datetimeStr = data.datetimeStr
datetimeStr1 = json.loads(request.data)
datetimeStr = datetimeStr1['datetimeStr']
struct_time = datetime.strptime(datetimeStr, "%Y-%m-%d").date() # 获取请求日期, struct_time数据类型为dateTime
@ -60,19 +59,22 @@ def getDayScreenUseTime():
if time.timeStart.date() == struct_time and time.timeEnd.date() == struct_time:
selectedTimeList.append(time)
timeAmount = 0
for time in selectedTimeList:
if time.timeEnd.date() == time.timeStart.date():
time_diff = time.timeEnd - time.timeStart
timeAmount = timeAmount + time_diff.total_seconds()
if (len(selectedTimeList) == 0):
sendJson = {'getDayScreenUseTimeH': 0, 'getDayScreenUseTimeM': 0, 'getDayScreenUseTimeS': 0, 'firstScreenTime': '', 'lastScreenTime': '', 'getScreenTimeSpanH': 0, 'getScreenTimeSpanM':0, 'getScreenTimeSpanS': 0 }
else:
timeAmount = 0
for time in selectedTimeList:
if time.timeEnd.date() == time.timeStart.date():
time_diff = time.timeEnd - time.timeStart
timeAmount = timeAmount + time_diff.total_seconds()
timeAmount = int(timeAmount)
getDayScreenUseTimeM, getDayScreenUseTimeS = divmod(timeAmount, 60) # 每日屏幕使用时长, 分时秒
getDayScreenUseTimeH, getDayScreenUseTimeM = divmod(getDayScreenUseTimeM, 60)
firstScreenTime = selectedTimeList[0].timeStart.strftime('%H时%M分%S秒') # 第一次屏幕使用时刻,
lastScreenTime = selectedTimeList[-1].timeEnd.strftime('%H时%M分%S秒') # 最后一次屏幕使用时刻
screenTimeSpan = int((selectedTimeList[-1].timeEnd - selectedTimeList[0].timeStart).total_seconds()) # 持续时间
getScreenTimeSpanM, getScreenTimeSpanS = divmod(screenTimeSpan, 60) # 每日屏幕使用时长, 分时秒
getScreenTimeSpanH, getScreenTimeSpanM = divmod(getScreenTimeSpanM, 60)
sendJson = {'getDayScreenUseTimeH': getDayScreenUseTimeH, 'getDayScreenUseTimeM': getDayScreenUseTimeM, 'getDayScreenUseTimeS': getDayScreenUseTimeS, 'firstScreenTime': firstScreenTime, 'lastScreenTime': lastScreenTime, 'getScreenTimeSpanH': getScreenTimeSpanH, 'getScreenTimeSpanM': getScreenTimeSpanM, 'getScreenTimeSpanS': getScreenTimeSpanS }
timeAmount = int(timeAmount)
getDayScreenUseTimeM, getDayScreenUseTimeS = divmod(timeAmount, 60) # 每日屏幕使用时长, 分时秒
getDayScreenUseTimeH, getDayScreenUseTimeM = divmod(getDayScreenUseTimeM, 60)
firstScreenTime = selectedTimeList[0].timeStart.strftime('%H时%M分%S秒') # 第一次屏幕使用时刻,
lastScreenTime = selectedTimeList[-1].timeEnd.strftime('%H时%M分%S秒') # 最后一次屏幕使用时刻
screenTimeSpan = int((selectedTimeList[-1].timeEnd - selectedTimeList[0].timeStart).total_seconds()) # 持续时间
getScreenTimeSpanM, getScreenTimeSpanS = divmod(screenTimeSpan, 60) # 每日屏幕使用时长, 分时秒
getScreenTimeSpanH, getScreenTimeSpanM = divmod(getScreenTimeSpanM, 60)
sendJson = {'getDayScreenUseTimeH': getDayScreenUseTimeH, 'getDayScreenUseTimeM': getDayScreenUseTimeM, 'getDayScreenUseTimeS': getDayScreenUseTimeS, 'firstScreenTime': firstScreenTime, 'lastScreenTime': lastScreenTime, 'getScreenTimeSpanH': getScreenTimeSpanH, 'getScreenTimeSpanM': getScreenTimeSpanM, 'getScreenTimeSpanS': getScreenTimeSpanS }
return json.dumps(sendJson, indent=4)

+ 113
- 0
lazy-timer-fe/package-lock.json Visa fil

@ -19,6 +19,8 @@
"@syncfusion/ej2-react-richtexteditor": "^19.4.50",
"@syncfusion/ej2-react-schedule": "^19.4.50",
"axios": "^0.27.2",
"echarts-for-react": "^3.0.2",
"moment": "^2.29.4",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-icons": "^4.3.1",
@ -6491,6 +6493,35 @@
"resolved": "https://registry.npmmirror.com/duplexer/-/duplexer-0.1.2.tgz",
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
},
"node_modules/echarts": {
"version": "5.3.3",
"resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.3.3.tgz",
"integrity": "sha512-BRw2serInRwO5SIwRviZ6Xgm5Lb7irgz+sLiFMmy/HOaf4SQ+7oYqxKzRHAKp4xHQ05AuHw1xvoQWJjDQq/FGw==",
"peer": true,
"dependencies": {
"tslib": "2.3.0",
"zrender": "5.3.2"
}
},
"node_modules/echarts-for-react": {
"version": "3.0.2",
"resolved": "https://registry.npmmirror.com/echarts-for-react/-/echarts-for-react-3.0.2.tgz",
"integrity": "sha512-DRwIiTzx8JfwPOVgGttDytBqdp5VzCSyMRIxubgU/g2n9y3VLUmF2FK7Icmg/sNVkv4+rktmrLN9w22U2yy3fA==",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"size-sensor": "^1.0.1"
},
"peerDependencies": {
"echarts": "^3.0.0 || ^4.0.0 || ^5.0.0",
"react": "^15.0.0 || >=16.0.0"
}
},
"node_modules/echarts/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"peer": true
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
@ -11318,6 +11349,14 @@
"mkdirp": "bin/cmd.js"
}
},
"node_modules/moment": {
"version": "2.29.4",
"resolved": "https://registry.npmmirror.com/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
"engines": {
"node": "*"
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz",
@ -14135,6 +14174,11 @@
"resolved": "https://registry.npmmirror.com/sisteransi/-/sisteransi-1.0.5.tgz",
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="
},
"node_modules/size-sensor": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/size-sensor/-/size-sensor-1.0.1.tgz",
"integrity": "sha512-QTy7MnuugCFXIedXRpUSk9gUnyNiaxIdxGfUjr8xxXOqIB3QvBUYP9+b51oCg2C4dnhaeNk/h57TxjbvoJrJUA=="
},
"node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz",
@ -15989,6 +16033,21 @@
"engines": {
"node": ">=10"
}
},
"node_modules/zrender": {
"version": "5.3.2",
"resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.3.2.tgz",
"integrity": "sha512-8IiYdfwHj2rx0UeIGZGGU4WEVSDEdeVCaIg/fomejg1Xu6OifAL1GVzIPHg2D+MyUkbNgPWji90t0a8IDk+39w==",
"peer": true,
"dependencies": {
"tslib": "2.3.0"
}
},
"node_modules/zrender/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"peer": true
}
},
"dependencies": {
@ -20975,6 +21034,33 @@
"resolved": "https://registry.npmmirror.com/duplexer/-/duplexer-0.1.2.tgz",
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
},
"echarts": {
"version": "5.3.3",
"resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.3.3.tgz",
"integrity": "sha512-BRw2serInRwO5SIwRviZ6Xgm5Lb7irgz+sLiFMmy/HOaf4SQ+7oYqxKzRHAKp4xHQ05AuHw1xvoQWJjDQq/FGw==",
"peer": true,
"requires": {
"tslib": "2.3.0",
"zrender": "5.3.2"
},
"dependencies": {
"tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"peer": true
}
}
},
"echarts-for-react": {
"version": "3.0.2",
"resolved": "https://registry.npmmirror.com/echarts-for-react/-/echarts-for-react-3.0.2.tgz",
"integrity": "sha512-DRwIiTzx8JfwPOVgGttDytBqdp5VzCSyMRIxubgU/g2n9y3VLUmF2FK7Icmg/sNVkv4+rktmrLN9w22U2yy3fA==",
"requires": {
"fast-deep-equal": "^3.1.3",
"size-sensor": "^1.0.1"
}
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
@ -24710,6 +24796,11 @@
"minimist": "^1.2.6"
}
},
"moment": {
"version": "2.29.4",
"resolved": "https://registry.npmmirror.com/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz",
@ -26740,6 +26831,11 @@
"resolved": "https://registry.npmmirror.com/sisteransi/-/sisteransi-1.0.5.tgz",
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="
},
"size-sensor": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/size-sensor/-/size-sensor-1.0.1.tgz",
"integrity": "sha512-QTy7MnuugCFXIedXRpUSk9gUnyNiaxIdxGfUjr8xxXOqIB3QvBUYP9+b51oCg2C4dnhaeNk/h57TxjbvoJrJUA=="
},
"slash": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz",
@ -28229,6 +28325,23 @@
"version": "0.1.0",
"resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
},
"zrender": {
"version": "5.3.2",
"resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.3.2.tgz",
"integrity": "sha512-8IiYdfwHj2rx0UeIGZGGU4WEVSDEdeVCaIg/fomejg1Xu6OifAL1GVzIPHg2D+MyUkbNgPWji90t0a8IDk+39w==",
"peer": true,
"requires": {
"tslib": "2.3.0"
},
"dependencies": {
"tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"peer": true
}
}
}
}
}

+ 2
- 0
lazy-timer-fe/package.json Visa fil

@ -14,6 +14,8 @@
"@syncfusion/ej2-react-richtexteditor": "^19.4.50",
"@syncfusion/ej2-react-schedule": "^19.4.50",
"axios": "^0.27.2",
"echarts-for-react": "^3.0.2",
"moment": "^2.29.4",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-icons": "^4.3.1",

+ 1
- 1
lazy-timer-fe/src/App.js Visa fil

@ -69,7 +69,7 @@ const App = () => {
<Route path="/overview" element={(<Ecommerce />)} />
{/* pages */}
<Route path="/orders" element={<Orders />} />
<Route path="/orders" element={<Bar/>} />
<Route path="/employees" element={<Employees />} />
<Route path="/customers" element={<Customers />} />

+ 213
- 32
lazy-timer-fe/src/pages/Charts/Bar.jsx Visa fil

@ -1,35 +1,216 @@
import React from 'react';
import { ChartComponent, SeriesCollectionDirective, SeriesDirective, Inject, Legend, Category, Tooltip, ColumnSeries, DataLabel } from '@syncfusion/ej2-react-charts';
import React, { useEffect, useState } from 'react'
import ReactECharts from 'echarts-for-react'
import moment from 'moment'
import { ChartComponent, SeriesCollectionDirective, SeriesDirective, Inject, Legend, Category, Tooltip, ColumnSeries, DataLabel } from '@syncfusion/ej2-react-charts'
import { barCustomSeries, barPrimaryXAxis, barPrimaryYAxis } from '../../data/dummy';
import { ChartsHeader } from '../../components';
import { useStateContext } from '../../contexts/ContextProvider';
import { barCustomSeries, barPrimaryXAxis, barPrimaryYAxis } from '../../data/dummy'
import { ChartsHeader } from '../../components'
import { useStateContext } from '../../contexts/ContextProvider'
import { getDayScreenUseTime } from '../../service/getDayScreenUseTime/index'
const Bar = () => {
const { currentMode } = useStateContext();
return (
<div className="m-4 md:m-10 mt-24 p-10 bg-white dark:bg-secondary-dark-bg rounded-3xl">
<ChartsHeader category="Bar" title="Olympic Medal Counts - RIO" />
<div className=" w-full">
<ChartComponent
id="charts"
primaryXAxis={barPrimaryXAxis}
primaryYAxis={barPrimaryYAxis}
chartArea={{ border: { width: 0 } }}
tooltip={{ enable: true }}
background={currentMode === 'Dark' ? '#33373E' : '#fff'}
legendSettings={{ background: 'white' }}
>
<Inject services={[ColumnSeries, Legend, Tooltip, Category, DataLabel]} />
<SeriesCollectionDirective>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
{barCustomSeries.map((item, index) => <SeriesDirective key={index} {...item} />)}
</SeriesCollectionDirective>
</ChartComponent>
</div>
</div>
);
};
export default Bar;
const { currentColor, currentMode } = useStateContext()
let [option, setOption] = useState({})
/* 生成本周日期, 格式为 MM-DD */
function convertDate(date) {
//
var yyyy = date.getFullYear().toString()
var mm = (date.getMonth() + 1).toString()
var dd = date.getDate().toString()
var mmChars = mm.split('')
var ddChars = dd.split('')
return (mmChars[1] ? mm : '0' + mmChars[0]) + '-' + (ddChars[1] ? dd : '0' + ddChars[0])
}
let currentWeekDates = new Array(7)
for (let i = 0; i < 7; i++) {
const curr = new Date()
currentWeekDates[i] = convertDate(new Date(curr.setDate(curr.getDate() - curr.getDay() + i)))
}
let currentWeekScreenTime = new Array(7)
useEffect(async () => {
for (let i = 0; i < 7; i++) {
let receivedJson = await getDayScreenUseTime('2022-' + currentWeekDates[i])
currentWeekScreenTime[i] = receivedJson.data.getDayScreenUseTimeH * 3600 + receivedJson.data.getDayScreenUseTimeM * 60 + receivedJson.data.getDayScreenUseTimeS
}
setOption({
backgroundColor: '#031245',
color: ['#63caff', '#49beff', '#03387a', '#03387a', '#03387a', '#6c93ee', '#a9abff', '#f7a23f', '#27bae7', '#ff6d9d', '#cb79ff', '#f95b5a', '#ccaf27', '#38b99c', '#93d0ff', '#bd74e0', '#fd77da', '#dea700'],
grid: {
containLabel: true,
left: 20,
right: 20,
bottom: 10,
top: 40
},
xAxis: {
axisLabel: {
color: '#c0c3cd',
fontSize: 14,
interval: 0
},
axisTick: {
lineStyle: {
color: '#384267'
},
show: true
},
splitLine: {
show: false
},
axisLine: {
lineStyle: {
color: '#384267',
width: 1,
type: 'dashed'
},
show: true
},
data: [currentWeekDates[0] + ' 周一', currentWeekDates[1] + ' 周二', currentWeekDates[2] + ' 周三', currentWeekDates[3] + ' 周四', currentWeekDates[4] + ' 周五', currentWeekDates[5] + ' 周六', currentWeekDates[6] + ' 周日'].map(function (str) {
return str.replace(' ', '\n')
}),
type: 'category'
},
yAxis: {
axisLabel: {
color: '#c0c3cd',
fontSize: 14,
formatter: (value, index, format) => {
return Math.floor(value / 3600) + '小时'
}
},
max: 86400,
axisTick: {
lineStyle: {
color: '#384267',
width: 1
},
show: true
},
splitLine: {
show: true,
lineStyle: {
color: '#384267',
type: 'dashed'
}
},
axisLine: {
lineStyle: {
color: '#384267',
width: 1,
type: 'dashed'
},
show: true
},
name: ''
},
series: [
{
data: currentWeekScreenTime, //
type: 'bar',
barMaxWidth: 'auto',
barWidth: 30,
itemStyle: {
color: {
x: 0,
y: 0,
x2: 0,
y2: 1,
type: 'linear',
global: false,
colorStops: [
{
offset: 0,
color: '#0b9eff'
},
{
offset: 1,
color: '#63caff'
}
]
}
},
// label: {
// show: false,
// position: 'top',
// distance: 10,
// color: '#fff',
// formatter: '{c}'
// }
},
{
data: [1, 1, 1, 1, 1, 1, 1, 1],
type: 'pictorialBar',
barMaxWidth: '20',
symbol: 'diamond',
symbolOffset: [0, '50%'],
symbolSize: [30, 15]
},
{
data: currentWeekScreenTime,
type: 'pictorialBar',
barMaxWidth: '20',
symbolPosition: 'end',
symbol: 'diamond',
symbolOffset: [0, '-50%'],
symbolSize: [30, 12],
zlevel: 2
},
{
data: [86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400],
type: 'bar',
barMaxWidth: 'auto',
barWidth: 30,
barGap: '-100%',
zlevel: -1
},
{
data: [1, 1, 1, 1, 1, 1, 1, 1],
type: 'pictorialBar',
barMaxWidth: '20',
symbol: 'diamond',
symbolOffset: [0, '50%'],
symbolSize: [30, 15],
zlevel: -2
},
{
data: [86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400],
type: 'pictorialBar',
barMaxWidth: '20',
symbolPosition: 'end',
symbol: 'diamond',
symbolOffset: [0, '-50%'],
symbolSize: [30, 12],
zlevel: -1
}
],
tooltip: {
trigger: 'axis',
show: true,
axisPointer: {
type: 'shadow'
},
formatter: function(params) {
let sum = params[0]['data']
return Math.floor((sum / 3600)) + '小时' + Math.floor((sum % 3600 / 60)) + '分钟' + Math.floor((sum % 60)) + '秒'
}
}
})
})
return (
<div className="m-4 md:m-10 mt-24 p-10 bg-white dark:bg-secondary-dark-bg rounded-3xl">
<ChartsHeader category="Bar" title="屏幕使用时间" />
<div className=" w-full">
<ReactECharts option={option} style={{ height: '600px' }}></ReactECharts>
</div>
</div>
)
}
export default Bar

+ 0
- 3
lazy-timer-fe/src/pages/Ecommerce.jsx Visa fil

@ -29,7 +29,6 @@ const Ecommerce = () => {
var mm = String(today.getMonth() + 1).padStart(2, '0');
var yyyy = today.getFullYear();
const receivedJson = await getDayScreenUseTime(yyyy + '-' + mm + '-' + dd);
console.log(receivedJson);
setEarningData2([
{
icon: <BsFillLaptopFill />,
@ -71,8 +70,6 @@ const Ecommerce = () => {
])
}, [])
console.log(earningData2);
return (
<div className="mt-24">
{/* 第一行四个盒子 */}

+ 2
- 1
lazy-timer-fe/src/pages/Orders.jsx Visa fil

@ -1,5 +1,6 @@
import React from 'react';
import { GridComponent, ColumnsDirective, ColumnDirective, Resize, Sort, ContextMenu, Filter, Page, ExcelExport, PdfExport, Edit, Inject } from '@syncfusion/ej2-react-grids';
import { Bar } from './Charts/Bar'
import { ordersData, contextMenuItems, ordersGrid } from '../data/dummy';
import { Header } from '../components';
@ -8,7 +9,7 @@ const Orders = () => {
const editing = { allowDeleting: true, allowEditing: true };
return (
<div className="m-2 md:m-10 mt-24 p-2 md:p-10 bg-white rounded-3xl">
<Header category="Page" title="Orders" />
<Header category="" title="屏幕使用时间对比" />
<GridComponent
id="gridcomp"
dataSource={ordersData}

Laddar…
Avbryt
Spara