Browse Source

feat: 时间轴版本2、根据服务查询项目、区分不同服务

test2
xuesinan 3 years ago
parent
commit
c612768a93
  1. 3
      CHANGELOG.md
  2. 2
      apis/tall.js
  3. 1
      common/js/config.js
  4. 2
      components/Projects/ProjectItem.vue
  5. 24
      components/TimeLine/TimeLine.vue
  6. 145
      hooks/project/useGetTasks.js
  7. 146
      hooks/project/useGetTasks222.js
  8. 17
      pages/index/index.vue

3
CHANGELOG.md

@ -1,4 +1,4 @@
# 1.0.0 (2022-03-15)
# 1.0.0 (2022-03-16)
### 🌟 新功能
范围|描述|commitId
@ -20,6 +20,7 @@
- | 服务、插件缓存、导入选择服务列表、 | [cc8004b](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/cc8004b)
- | 给财务条传参数 | [5ff7706](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/5ff7706)
- | 给财务条详情页传参 | [fa92a11](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/fa92a11)
- | 根据id查找插件配置信息 | [bee8705](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/bee8705)
- | 更新代码 | [392c8cc](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/392c8cc)
- | 更新了工作台的图片 | [885c6e2](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/885c6e2)
- | 工作台按钮 | [6e434e6](https://101.201.226.163:50022/TALL/TALL-MUI-4/commits/6e434e6)

2
apis/tall.js

@ -31,7 +31,7 @@ export function setupTall(app) {
// 修改用户信息
uni.$u.api.updateUserInfo = params => uni.$u.http.post(`${tall}/users/userInfo`, params);
// 获取项目列表
uni.$u.api.getProjects = (startTime, endTime) => uni.$u.post(`${tall}/project/query`, { startTime, endTime });
uni.$u.api.getProjects = (startTime, endTime, codes) => uni.$u.post(`${tall}/project/query`, { startTime, endTime, codes: Config.serviceList });
// 查询日历是否有小红点
uni.$u.api.findRedPoint = (startTime, endTime) => uni.$u.post(`${tall}/project/day`, { startTime, endTime });
// 设置项目顺序

1
common/js/config.js

@ -8,6 +8,7 @@ var config = {
apiUrl: 'http://101.201.226.163/gateway',
msgUrl: 'ws://101.201.226.163:8196/message/v4.0/ws',
projectPath: 'https://101.201.226.163/tall-project',
serviceList: ['ZERO', 'CONTEST', 'PT'],
// baseUrl: 'https://www.tall.wiki',
// apiUrl: 'https://www.tall.wiki/gateway',

2
components/Projects/ProjectItem.vue

@ -1,5 +1,5 @@
<template>
<view class="w-full">
<view class="w-full" :style="{background: item.styleColor}">
<!-- 有子项目 父项目 -->
<view class="flex items-center justify-between p-3">
<u-icon @click="openMenu(item, index)" class="mover" name="https://www.tall.wiki/staticrec/drag.svg" size="48"></u-icon>

24
components/TimeLine/TimeLine.vue

@ -61,12 +61,12 @@ async function handleScrollTop() {
store.commit('task/updateTasks', props.tasks)
console.warn('滚动到顶部: ');
if (timeLineType.value === 1) {
const startTime = props.tasks[0].planStart - 0;
store.commit('task/setCurrUpTimeNode', startTime);
const addTasks = uni.$task.setPlaceholderTasks(startTime, true, timeGranularity.value);
store.commit('task/setUpTasks', addTasks);
}
// if (timeLineType.value === 1) {
// const startTime = props.tasks[0].planStart - 0;
// store.commit('task/setCurrUpTimeNode', startTime);
// const addTasks = uni.$task.setPlaceholderTasks(startTime, true, timeGranularity.value);
// store.commit('task/setUpTasks', addTasks);
// }
let params = { pageNum: upNextPage.value, queryType: 0 };
getTasksHook.dataRender(params);
@ -79,12 +79,12 @@ async function handleScrollBottom() {
store.commit('task/updateTasks', props.tasks);
console.warn('滚动到底部: ');
if (timeLineType.value === 1) {
const startTime = dayjs(+props.tasks[props.tasks.length - 1].planStart).add(1, timeGranularity.value).valueOf();
store.commit('task/setCurrDownTimeNode', startTime);
const addTasks = uni.$task.setPlaceholderTasks(startTime, false, timeGranularity.value);
store.commit('task/setDownTasks', addTasks);
}
// if (timeLineType.value === 1) {
// const startTime = dayjs(+props.tasks[props.tasks.length - 1].planStart).add(1, timeGranularity.value).valueOf();
// store.commit('task/setCurrDownTimeNode', startTime);
// const addTasks = uni.$task.setPlaceholderTasks(startTime, false, timeGranularity.value);
// store.commit('task/setDownTasks', addTasks);
// }
let params = { pageNum: downNextPage.value, queryType: 1 };
getTasksHook.dataRender(params);

145
hooks/project/useGetTasks.js

@ -30,8 +30,9 @@ export default function useGetTasks() {
// 初始化 定期任务
async function initPlanTasks() {
if (timeLineType.value === 1) setNextPlaceholderTasks({});
console.log('查询定期任务');
// if (timeLineType.value === 1) setNextPlaceholderTasks({});
uni.$ui.showLoading();
console.log('查询定期任务11111111111111111');
await getTasks({}); // 获取初始数据
// await dataRender({});
}
@ -62,6 +63,7 @@ export default function useGetTasks() {
* @param {number} query.queryType 0向上查找 1向下查找(默认) 下查包含自己上查不包含
*/
function getTasks(query) {
uni.$ui.showLoading();
store.commit('task/setShowSkeleton', false);
const params = generateGetTaskParam(query);
@ -89,12 +91,17 @@ export default function useGetTasks() {
params.queryType === 0 ? store.commit('task/setUpNextPage', arr[index].upNextPage) : store.commit('task/setDownNextPage', arr[index].downNextPage); // 下一页
// 如果第一次渲染但没有空数据则加载空数据
if (!currRoleShowTasks.value || !currRoleShowTasks.value.length && timeLineType.value === 1) {
setNextPlaceholderTasks(params);
}
// if (!currRoleShowTasks.value || !currRoleShowTasks.value.length && timeLineType.value === 1) {
// setNextPlaceholderTasks(params);
// }
// 数据处理
dataRender(params);
// nextTick(() => {
// uni.$ui.hideLoading();
// console.log('-------------------------------')
// });
}
});
}
@ -156,82 +163,71 @@ export default function useGetTasks() {
}
async function handleTasksData(params, centerData, realTasks) {
/**
* 3查找的任务数量是否>=15
* 3-1
* 判断时间跨度是否>=15
* 3-1-1显示时间刻度范围内的任务
* 3-1-2显示全部任务并删除多余的刻度
* 3-2
* 判断时间跨度是否>=15
* 3-2-1显示时间刻度范围内的任务
* 3-2-2
* 下一页是否为0
* 3-2-2-1无下一页显示任务和刻度之后继续展示刻度
* 3-2-2-1查找下一页数据并重复上述步骤
*/
let showTasks = currRoleShowTasks.value; // 显示的数据
let firstTime = showTasks.length > 0 ? dayjs(+showTasks[0].planStart).subtract(1, timeGranularity.value) : new Date().getTime(); // 显示的数据第一个数据的时间
let lastTime = showTasks.length > 0 ? dayjs(+showTasks[showTasks.length - 1].planStart).add(1, timeGranularity.value) : new Date().getTime(); // 显示的数据最后一个数据的时间
let upTargetTime = dayjs(+firstTime).subtract(params.pageSize - 1, timeGranularity.value);
let downTargetTime = dayjs(+lastTime).add(params.pageSize - 1, timeGranularity.value);
const nextPage = params.queryType === 0 ? upNextPage.value : downNextPage.value; // 下一页的值
if (centerData.length) {
let centerDataTime = '', // 中间数据的时间
scaleTime = '', // 空时间节点的开始时间
centerTime = '', // 中间数据+/-15后的数据
let arr = [];
centerData.forEach(v => {
let centerTime = '', // 中间数据+/-15后的数据
isExceed = false; // 时间跨度是否大于15
if (params.queryType) {
centerDataTime = centerData[centerData.length - 1].planStart;
scaleTime = currDownTimeNode.value;
centerTime = dayjs(+centerDataTime).subtract(params.pageSize, timeGranularity.value);
isExceed = dayjs(+centerTime).isSame(+scaleTime, timeGranularity.value) || dayjs(+centerTime).isAfter(+scaleTime, timeGranularity.value)
isExceed = dayjs(+downTargetTime).isAfter(+v.planStart, timeGranularity.value)
} else {
centerDataTime = centerData[0].planStart;
scaleTime = currUpTimeNode.value;
centerTime = dayjs(+centerDataTime).add(params.pageSize, timeGranularity.value);
isExceed = dayjs(+centerTime).isSame(+scaleTime, timeGranularity.value) || dayjs(+centerTime).isBefore(+scaleTime, timeGranularity.value)
isExceed = dayjs(+upTargetTime).isBefore(+v.planStart, timeGranularity.value)
}
const firstDetailIndex = showTasks.findIndex(task => task.detailId); // 显示任务中存在真实任务
const firstId = firstDetailIndex > -1 ? showTasks[firstDetailIndex].id : '';
let lastDetailIndex = -1;
showTasks.forEach((item, index) => {
if (item.detailId) {
lastDetailIndex = index;
if (isExceed) {
arr.push(v);
}
})
const lastId = lastDetailIndex > -1 ? showTasks[lastDetailIndex].id : '';
showTasks.forEach((task, index) => {
const arr = centerData.filter(item => dayjs(+item.planStart).isSame(+task.planStart, timeGranularity.value));
if (arr.length) {
if (params.queryType === 1 && task.id === lastId) {
showTasks.splice(index + 1, 0, [...arr])
} else if (params.queryType === 0 && task.id === firstId) {
showTasks.splice(index, 0, [...arr])
if (!arr.length) {
params.queryType === 0 ? setPrevPlaceholderTasks(params) : setNextPlaceholderTasks(params);
} else {
showTasks.splice(index, 1, [...arr])
}
if (arr.length === centerData.length && centerData.length < params.pageSize && nextPage > 0) {
getTasks({pageNum: nextPage, queryType: params.queryType});
} else {
if (arr.length < centerData.length || (arr.length === centerData.length && centerData.length < params.pageSize && nextPage === 0)) {
let newArr = [];
const time = params.queryType === 1 ? lastTime : upTargetTime;
let currTime = params.queryType === 0 ? upTargetTime : lastTime;
const firstTime = params.queryType === 0 ? dayjs(+upTargetTime).add(1, timeGranularity.value) : dayjs(+lastTime).subtract(1, timeGranularity.value);
let firstArr = arr.filter(item => dayjs(+item.planStart).isSame(+firstTime, timeGranularity.value));
if (firstArr.length) {
newArr = params.queryType === 0 ? [...firstArr, ...newArr] : [...newArr, ...firstArr];
}
})
showTasks = flatten(showTasks); // 1维拍平
for (let i = 0; i < params.pageSize; i++) {
let termArr = arr.filter(item => dayjs(+item.planStart).isSame(+currTime, timeGranularity.value));
if (!isExceed) {
if (centerData.length >= params.pageSize) {
let len = -1;
let data = params.queryType === 0 ? centerData[0] : centerData[centerData.length - 1];
showTasks.forEach((item, index) => {
if (item.id === data.id) {
len = index;
if (termArr.length === 0) {
const newTasks = uni.$task.setPlaceholderTasks(+currTime, false, timeGranularity.value, 1);
newArr = [...newArr, ...newTasks];
} else {
newArr = [...newArr, ...termArr];
}
currTime = dayjs(+currTime).add(1, timeGranularity.value);
}
})
if (len > -1) {
showTasks = params.queryType === 0 ? showTasks.slice(len) : showTasks.slice(0, len + 1);
showTasks = params.queryType === 0 ? [...newArr, ...showTasks] : [...showTasks, ...newArr];
} else {
showTasks = params.queryType === 0 ? [...arr, ...showTasks] : [...showTasks, ...arr];
}
} else if (nextPage > 0) {
console.log('数据不为空,时间跨度小于15')
getTasks({pageNum: nextPage, queryType: params.queryType});
}
showTasks = flatten(showTasks); // 1维拍平
store.commit('task/clearTasks');
params.queryType === 0 ? store.commit('task/setUpTasks', showTasks) : store.commit('task/setDownTasks', showTasks);
}
} else {
if (nextPage > 0) {
@ -242,12 +238,11 @@ export default function useGetTasks() {
}
}
uni.$ui.hideLoading();
console.log('------------')
// if (showTasks.length > 30) {
// showTasks = params.queryType === 0 ? showTasks.slice(0, 80) : showTasks.slice(showTasks.length - 80);
// }
store.commit('task/clearTasks');
params.queryType === 0 ? store.commit('task/setUpTasks', showTasks) : store.commit('task/setDownTasks', showTasks);
}
// 任务模式
@ -270,9 +265,9 @@ export default function useGetTasks() {
getTasks({pageNum: 1, queryType: 0});
}
if (showTasks.length > 80) {
showTasks = params.queryType === 0 ? showTasks.slice(0, 80) : showTasks.slice(showTasks.length - 80);
}
// if (showTasks.length > 80) {
// showTasks = params.queryType === 0 ? showTasks.slice(0, 80) : showTasks.slice(showTasks.length - 80);
// }
store.commit('task/clearTasks');
params.queryType === 0 ? store.commit('task/setUpTasks', showTasks) : store.commit('task/setDownTasks', showTasks);
@ -302,13 +297,13 @@ export default function useGetTasks() {
startTime = dayjs(+currRoleShowTasks.value[currRoleShowTasks.value.length - 1].planStart).add(1, timeGranularity.value).valueOf();
}
if (params.taskId) {
currRoleRealTasks.value.forEach(item => {
if (item.id === params.taskId) {
startTime = Number(item.planStart);
}
})
}
// if (params.taskId) {
// currRoleRealTasks.value.forEach(item => {
// if (item.id === params.taskId) {
// startTime = Number(item.planStart);
// }
// })
// }
const initData = uni.$task.setPlaceholderTasks(startTime, false, timeGranularity.value);
store.commit('task/setCurrDownTimeNode', startTime);

146
hooks/project/useGetTasks111.js → hooks/project/useGetTasks222.js

@ -30,8 +30,8 @@ export default function useGetTasks() {
// 初始化 定期任务
async function initPlanTasks() {
// if (timeLineType.value === 1) setNextPlaceholderTasks({});
console.log('查询定期任务11111111111111111');
if (timeLineType.value === 1) setNextPlaceholderTasks({});
console.log('查询定期任务');
await getTasks({}); // 获取初始数据
// await dataRender({});
}
@ -62,7 +62,6 @@ export default function useGetTasks() {
* @param {number} query.queryType 0向上查找 1向下查找(默认) 下查包含自己上查不包含
*/
function getTasks(query) {
uni.$ui.showLoading();
store.commit('task/setShowSkeleton', false);
const params = generateGetTaskParam(query);
@ -90,17 +89,12 @@ export default function useGetTasks() {
params.queryType === 0 ? store.commit('task/setUpNextPage', arr[index].upNextPage) : store.commit('task/setDownNextPage', arr[index].downNextPage); // 下一页
// 如果第一次渲染但没有空数据则加载空数据
// if (!currRoleShowTasks.value || !currRoleShowTasks.value.length && timeLineType.value === 1) {
// setNextPlaceholderTasks(params);
// }
if (!currRoleShowTasks.value || !currRoleShowTasks.value.length && timeLineType.value === 1) {
setNextPlaceholderTasks(params);
}
// 数据处理
dataRender(params);
// nextTick(() => {
// uni.$ui.hideLoading();
// console.log('-------------------------------')
// });
}
});
}
@ -177,72 +171,66 @@ export default function useGetTasks() {
* 3-2-2-1查找下一页数据并重复上述步骤
*/
let showTasks = currRoleShowTasks.value; // 显示的数据
let firstTime = showTasks.length > 0 ? showTasks[0].planStart : new Date().getTime(); // 显示的数据第一个数据的时间
let lastTime = showTasks.length > 0 ? showTasks[showTasks.length - 1].planStart : new Date().getTime(); // 显示的数据最后一个数据的时间
const nextPage = params.queryType === 0 ? upNextPage.value : downNextPage.value; // 下一页的值
if (centerData.length) {
let addNum = 0;
centerData.forEach(v => {
let showFirstTime = showTasks.length > 0 ? showTasks[0].planStart : new Date().getTime(); // 显示的数据第一个数据的时间
let showLastTime = new Date().getTime(); // 显示的数据最后一个数据的时间
if (showTasks.length) {
showLastTime = dayjs(+showTasks[showTasks.length - 1].planStart).add(1, timeGranularity.value);
}
let dayNum = 0; // 两个日期中间相差的天数
let centerTime = '', // 中间数据+/-15后的数据
let centerDataTime = '', // 中间数据的时间
scaleTime = '', // 空时间节点的开始时间
centerTime = '', // 中间数据+/-15后的数据
isExceed = false; // 时间跨度是否大于15
if (params.queryType) {
dayNum = dayjs(+v.planStart).diff(+showLastTime, timeGranularity.value, true);
// if (!showTasks.length) dayNum++;
centerTime = dayjs(+v.planStart).subtract(params.pageSize, timeGranularity.value);
isExceed = dayjs(+centerTime).isAfter(+showLastTime, timeGranularity.value)
centerDataTime = centerData[centerData.length - 1].planStart;
scaleTime = currDownTimeNode.value;
centerTime = dayjs(+centerDataTime).subtract(params.pageSize, timeGranularity.value);
isExceed = dayjs(+centerTime).isSame(+scaleTime, timeGranularity.value) || dayjs(+centerTime).isAfter(+scaleTime, timeGranularity.value)
} else {
dayNum = dayjs(+v.planStart).diff(+showFirstTime, timeGranularity.value);
centerTime = dayjs(+v.planStart).add(params.pageSize, timeGranularity.value);
isExceed = dayjs(+centerTime).isBefore(+showFirstTime, timeGranularity.value)
centerDataTime = centerData[0].planStart;
scaleTime = currUpTimeNode.value;
centerTime = dayjs(+centerDataTime).add(params.pageSize, timeGranularity.value);
isExceed = dayjs(+centerTime).isSame(+scaleTime, timeGranularity.value) || dayjs(+centerTime).isBefore(+scaleTime, timeGranularity.value)
}
dayNum = dayjs(+'1647513785000').get('month');
console.log(dayNum, v.planStart, showLastTime);
if (!isExceed) {
addNum++;
let time = params.queryType === 0 ? showFirstTime : showLastTime;
const newTasks = uni.$task.setPlaceholderTasks(+time, !params.queryType, timeGranularity.value, dayNum);
params.queryType === 0 ? showTasks.splice(0, 0, newTasks) : showTasks.splice(showTasks.length, 0, newTasks);
params.queryType === 0 ? showTasks.splice(0, 0, v) : showTasks.splice(showTasks.length, 0, v);
// showTasks = flatten(showTasks); // 1维拍平
console.log('isExceed', showTasks, v)
const firstDetailIndex = showTasks.findIndex(task => task.detailId); // 显示任务中存在真实任务
const firstId = firstDetailIndex > -1 ? showTasks[firstDetailIndex].id : '';
let lastDetailIndex = -1;
showTasks.forEach((item, index) => {
if (item.detailId) {
lastDetailIndex = index;
}
})
showTasks = flatten(showTasks); // 1维拍平
if (!addNum) {
params.queryType === 0 ? setPrevPlaceholderTasks(params) : setNextPlaceholderTasks(params);
const lastId = lastDetailIndex > -1 ? showTasks[lastDetailIndex].id : '';
showTasks.forEach((task, index) => {
const arr = centerData.filter(item => dayjs(+item.planStart).isSame(+task.planStart, timeGranularity.value));
if (arr.length) {
if (params.queryType === 1 && task.id === lastId) {
showTasks.splice(index + 1, 0, [...arr])
} else if (params.queryType === 0 && task.id === firstId) {
showTasks.splice(index, 0, [...arr])
} else {
if (addNum === centerData.length && centerData.length < params.pageSize && nextPage > 0) {
getTasks({pageNum: nextPage, queryType: params.queryType});
showTasks.splice(index, 1, [...arr])
}
}
})
if (addNum < centerData.length || (addNum === centerData.length && centerData.length < params.pageSize && nextPage === 0)) {
// 补齐刻度
// const obj = { tasks: showTasks, data, timeGranularity: timeGranularity.value };
// showTasks = fillPlaceholderTask(obj);
let num = '';
if (params.queryType === 0) {
num = dayjs(+showTasks[0].planStart).diff(+firstTime, timeGranularity.value);
} else {
num = dayjs(+showTasks[showTasks.length - 1].planStart).diff(+lastTime, timeGranularity.value);
showTasks = flatten(showTasks); // 1维拍平
if (!isExceed) {
if (centerData.length >= params.pageSize) {
let len = -1;
let data = params.queryType === 0 ? centerData[0] : centerData[centerData.length - 1];
showTasks.forEach((item, index) => {
if (item.id === data.id) {
len = index;
}
console.log('2222222222', firstTime, lastTime, num)
})
if (addNum < centerData.length) {
console.log('11111111111111111')
renderScaleTask(params);
if (len > -1) {
showTasks = params.queryType === 0 ? showTasks.slice(len) : showTasks.slice(0, len + 1);
}
} else if (nextPage > 0) {
console.log('数据不为空,时间跨度小于15')
getTasks({pageNum: nextPage, queryType: params.queryType});
}
}
} else {
@ -282,30 +270,14 @@ export default function useGetTasks() {
getTasks({pageNum: 1, queryType: 0});
}
// if (showTasks.length > 80) {
// showTasks = params.queryType === 0 ? showTasks.slice(0, 80) : showTasks.slice(showTasks.length - 80);
// }
if (showTasks.length > 80) {
showTasks = params.queryType === 0 ? showTasks.slice(0, 80) : showTasks.slice(showTasks.length - 80);
}
store.commit('task/clearTasks');
params.queryType === 0 ? store.commit('task/setUpTasks', showTasks) : store.commit('task/setDownTasks', showTasks);
}
/**
* 超出旧数据上下限 补齐时间刻度到新数据的起始时间颗粒度
*/
function fillPlaceholderTask(obj) {
const { prev, next } = uni.$task.computeFillPlaceholderTaskCount(obj);
if (prev) {
const newTasks = uni.$task.setPlaceholderTasks(+obj.tasks[0].planStart, true, obj.timeGranularity, prev);
store.commit('task/setUpTasks', newTasks);
}
if (next) {
const newTasks = uni.$task.setPlaceholderTasks(+obj.tasks[obj.tasks.length - 1].planStart, false, obj.timeGranularity, next);
store.commit('task/setDownTasks', newTasks);
}
return tasks.value;
}
// 设置时间轴向上的空数据
function setPrevPlaceholderTasks() {
store.commit('task/setTopEnd', true);
@ -330,13 +302,13 @@ export default function useGetTasks() {
startTime = dayjs(+currRoleShowTasks.value[currRoleShowTasks.value.length - 1].planStart).add(1, timeGranularity.value).valueOf();
}
// if (params.taskId) {
// currRoleRealTasks.value.forEach(item => {
// if (item.id === params.taskId) {
// startTime = Number(item.planStart);
// }
// })
// }
if (params.taskId) {
currRoleRealTasks.value.forEach(item => {
if (item.id === params.taskId) {
startTime = Number(item.planStart);
}
})
}
const initData = uni.$task.setPlaceholderTasks(startTime, false, timeGranularity.value);
store.commit('task/setCurrDownTimeNode', startTime);

17
pages/index/index.vue

@ -60,6 +60,8 @@
calendar: null,
});
const colorList = ['#eee', '#ddd', '#ccc', '#bbb'];
const user = uni.$storage.getStorageSync('user');
if (!userInfo.value && user) {
store.commit('user/setUser', JSON.parse(user));
@ -74,9 +76,24 @@
if (err) {
console.error('err: ', err);
} else {
let arr = [];
data.forEach(item => {
item.show = false;
const index = arr.findIndex(v => v.businessCode === item.businessCode);
if (index === -1) {
arr.push({businessCode: item.businessCode});
}
});
data.forEach(item => {
arr.forEach((v, k) => {
if (item.businessCode === v.businessCode) {
item.styleColor = colorList[(k + 1) % colorList.length - 1];
}
})
})
store.commit('project/setProjects', data);
}
});

Loading…
Cancel
Save