Browse Source

接历史数据接口,差21,22,23,24和点击历史记录出结果

master
no99 5 days ago
parent
commit
a682082040
  1. 8
      .env.development
  2. 38
      src/api/AIxiaocaishen.js
  3. 4
      src/views/AIchat.vue
  4. 687
      src/views/components/HistoryRecord.vue
  5. 15
      src/views/homePage.vue

8
.env.development

@ -5,10 +5,10 @@ VITE_OUTPUT_DIR = 'dev'
VITE_PUBLIC_PATH = /
#新数据接口
# VITE_APP_API_BASE_URL = "http://39.101.133.168:8828/link"
VITE_APP_API_BASE_URL = "https://api.homilychart.com/link"
# VITE_APP_API_BASE_CAZE_URL = "http://39.101.133.168:8828/link"
VITE_APP_API_BASE_CAZE_URL = "https://api.homilychart.com/link"
VITE_APP_API_BASE_URL = "http://39.101.133.168:8828/link"
# VITE_APP_API_BASE_URL = "https://api.homilychart.com/link"
VITE_APP_API_BASE_CAZE_URL = "http://39.101.133.168:8828/link"
# VITE_APP_API_BASE_CAZE_URL = "https://api.homilychart.com/link"
VITE_APP_IMG_API_BASE_URL = "http://39.101.133.168:8828/hljw/api/aws/upload"
#MJ API

38
src/api/AIxiaocaishen.js

@ -1,7 +1,7 @@
import request from "../utils/request";
const APIurl = import.meta.env.VITE_APP_API_BASE_URL;
const cozeAPIurl= import.meta.env.VITE_APP_API_BASE_CAZE_URL;
const cozeAPIurl = import.meta.env.VITE_APP_API_BASE_CAZE_URL;
const MJAPIurl = import.meta.env.VITE_APP_MJ_API_BASE_URL;
//各个模块权限code接口
export const pessionAPI = function (params) {
@ -267,3 +267,39 @@ export const dbqbSecondFourAPI = function (params) {
data: params,
});
};
// 历史记录 列表
export const getHistoryListAPI = function (params) {
return request({
url: `${APIurl}/api/workflow/listHistory`,
method: "POST",
data: params,
});
};
export const changeTopAPI = function (params) {
return request({
url: `${APIurl}/api/workflow/topRecord`,
method: "POST",
data: params,
headers: {
token: localStorage.getItem("localToken"),
},
});
};
export const deleteRecordAPI = function (params) {
return request({
url: `${APIurl}/api/workflow/deleteRecord`,
method: "POST",
data: params,
});
};
export const clickRecordAPI = function (params) {
return request({
url: `${APIurl}/api/workflow/clickRecord`,
method: "POST",
data: params,
});
};

4
src/views/AIchat.vue

@ -686,10 +686,12 @@ watch(
const userStore = useUserStore();
const params1 = {
content: newVal[newVal.length - 1].content,
language: "cn",
marketList: "usa,sg,my,hk,cn,can,vi,th,in",
content: newVal[newVal.length - 1].content,
token: localStorage.getItem("localToken"),
model: 1,
// language: "cn",
// marketList: "hk,cn,usa,my,sg,vi,in,gb"
// token: "+SsksARQgUHIbIG3rRnnbZi0+fEeMx8pywnIlrmTxo5EOPR/wjWDV7w7+ZUseiBtf9kFa/atmNx6QfSpv5w",

687
src/views/components/HistoryRecord.vue

@ -32,7 +32,6 @@
<div class="collapsed-bottom-btn" @click="handleAnnouncementClick">
<img
class="collapsed-bottom-announcement"
@click="openHistory"
src="https://d31zlh4on95l9h.cloudfront.net/images/c51c7fbb68671729801fb10d65bd7789.png"
alt="icon"
/>
@ -60,96 +59,99 @@
</div>
<!-- 历史记录列表 -->
<div class="history-list">
<div
v-for="record in historyRecords"
:key="record.id"
class="history-item"
@click="selectRecord(record)"
:class="{ active: selectedRecordId === record.id }"
>
<div class="record-content">
<div class="record-img">
<img :src="marketList[record.market]" :alt="record.market" />
</div>
<div class="record-msg">
<div class="record-text">{{ record.code }}</div>
<div class="record-time">
{{ moment(record.date).format("D/M/YYYY") }}
</div>
</div>
<div v-for="history in categoryHistory" :key="history.name">
<div class="categoryName">
{{ history.name }}
</div>
<div class="record-actions">
<el-popover
class="box-item"
placement="right-start"
trigger="click"
>
<template #reference>
<el-icon class="more-btn"><MoreFilled /></el-icon>
</template>
<div class="popover-content">
<div class="popover-item">
<img
class="popover-icon"
src="https://d31zlh4on95l9h.cloudfront.net/images/9ad3617c94955bcb76e1b11db70bb80b.png"
alt=""
/>
数据更新时间{{
moment(record.updateTime).format("D/M/YYYY")
}}
</div>
<div v-if="record.isTop" class="popover-item popover-btn">
<img
class="popover-icon"
src="https://d31zlh4on95l9h.cloudfront.net/images/a458305d8275734cc96bf6cad29864bf.png"
alt=""
/>
取消置顶
</div>
<div v-else class="popover-item popover-btn">
<img
class="popover-icon"
src="https://d31zlh4on95l9h.cloudfront.net/images/a458305d8275734cc96bf6cad29864bf.png"
alt=""
/>
置顶
</div>
<div class="popover-item popover-btn">
<img
class="popover-icon"
src="https://d31zlh4on95l9h.cloudfront.net/images/027718d41523375a69e9cac927601cf8.png"
alt=""
/>
删除
<div
v-for="record in history.list"
:key="record.id"
class="history-item"
@click="selectRecord(record)"
:class="{ active: selectedRecordId === record.id }"
>
<div class="record-content" @click="selectRecord(record)">
<div class="record-img">
<img
:src="marketList[record.stockMarket]"
:alt="record.stockMarket"
/>
</div>
<div class="record-msg">
<div class="record-text">{{ record.stockCode }}</div>
<div class="record-time">
{{ moment(record.updatedTime).format("YYYY-MM-DD HH:mm:ss") }}
</div>
</div>
</el-popover>
<!-- <button
</div>
<div class="record-actions">
<el-popover
class="box-item"
placement="right-start"
trigger="click"
>
<template #reference>
<el-icon class="more-btn"><MoreFilled /></el-icon>
</template>
<div class="popover-content">
<div class="popover-item">
<img
class="popover-icon"
src="https://d31zlh4on95l9h.cloudfront.net/images/9ad3617c94955bcb76e1b11db70bb80b.png"
alt=""
/>
数据更新时间{{ moment(record.date).format("D/M/YYYY") }}
</div>
<div
v-if="record.isTop"
class="popover-item popover-btn"
@click="changeTopStatus(record.isTop, record.id)"
>
<img
class="popover-icon"
src="https://d31zlh4on95l9h.cloudfront.net/images/a458305d8275734cc96bf6cad29864bf.png"
alt=""
/>
取消置顶
</div>
<div
v-else
class="popover-item popover-btn"
@click="changeTopStatus(record.isTop, record.id)"
>
<img
class="popover-icon"
src="https://d31zlh4on95l9h.cloudfront.net/images/a458305d8275734cc96bf6cad29864bf.png"
alt=""
/>
置顶
</div>
<div
class="popover-item popover-btn"
@click="deleteRecord(record.id)"
>
<img
class="popover-icon"
src="https://d31zlh4on95l9h.cloudfront.net/images/027718d41523375a69e9cac927601cf8.png"
alt=""
/>
删除
</div>
</div>
</el-popover>
<!-- <button
@click="openDetail(record)"
title="更多"
></button> -->
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="historyRecords.length === 0" class="empty-state">
<div class="empty-icon">
<el-icon class="documentDelete"><DocumentDelete /></el-icon>
<!-- <svg
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 2C13.1 2 14 2.9 14 4C14 5.1 13.1 6 12 6C10.9 6 10 5.1 10 4C10 2.9 10.9 2 12 2ZM21 9V7L15 1H5C3.89 1 3 1.89 3 3V21C3 22.1 3.89 23 5 23H19C20.1 23 21 22.1 21 21V9M19 9H14V4"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg> -->
</div>
<p class="empty-text">暂无历史记录</p>
</div>
@ -166,7 +168,6 @@
<div class="bottom-btn" @click="handleAnnouncementClick">
<img
class="bottom-announcement"
@click="openHistory"
src="https://d31zlh4on95l9h.cloudfront.net/images/c51c7fbb68671729801fb10d65bd7789.png"
alt="icon"
/>
@ -203,64 +204,98 @@
</div>
<!-- 历史记录列表 -->
<div class="history-list">
<div
v-for="record in historyRecords"
:key="record.id"
class="history-item"
@click="selectRecord(record)"
>
<div class="record-content">
<div class="record-img">
<span class="type-badge" :class="record.type">
{{ record.type === "AIchat" ? "夺宝奇兵" : "AI情绪" }}
</span>
</div>
<div class="record-text">{{ record.question }}</div>
<div class="record-time">{{ formatTime(record.timestamp) }}</div>
<div v-for="history in categoryHistory" :key="history.name">
<div class="categoryName">
{{ history.name }}
</div>
<div class="record-actions">
<button
class="more-btn"
@click.stop="deleteRecord(record.id)"
title="删除"
>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18 6L6 18M6 6L18 18"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
<div
v-for="record in history.list"
:key="record.id"
class="history-item"
:class="{ active: selectedRecordId === record.id }"
>
<div class="record-content" @click="selectRecord(record)">
<div class="record-img">
<img
:src="marketList[record.stockMarket]"
:alt="record.stockMarket"
/>
</svg>
</button>
</div>
<div class="record-msg">
<div class="record-text">{{ record.stockCode }}</div>
<div class="record-time">
{{ moment(record.updatedTime).format("YYYY-MM-DD HH:mm:ss") }}
</div>
</div>
</div>
<div class="record-actions">
<el-popover
class="box-item"
placement="right-start"
trigger="click"
>
<template #reference>
<el-icon class="more-btn"><MoreFilled /></el-icon>
</template>
<div class="popover-content">
<div class="popover-item">
<img
class="popover-icon"
src="https://d31zlh4on95l9h.cloudfront.net/images/9ad3617c94955bcb76e1b11db70bb80b.png"
alt=""
/>
数据更新时间{{ moment(record.date).format("D/M/YYYY") }}
</div>
<div
v-if="record.isTop"
class="popover-item popover-btn"
@click="changeTopStatus(record.isTop, record.id)"
>
<img
class="popover-icon"
src="https://d31zlh4on95l9h.cloudfront.net/images/a458305d8275734cc96bf6cad29864bf.png"
alt=""
/>
取消置顶
</div>
<div
v-else
class="popover-item popover-btn"
@click="changeTopStatus(record.isTop, record.id)"
>
<img
class="popover-icon"
src="https://d31zlh4on95l9h.cloudfront.net/images/a458305d8275734cc96bf6cad29864bf.png"
alt=""
/>
置顶
</div>
<div
class="popover-item popover-btn"
@click="deleteRecord(record.id)"
>
<img
class="popover-icon"
src="https://d31zlh4on95l9h.cloudfront.net/images/027718d41523375a69e9cac927601cf8.png"
alt=""
/>
删除
</div>
</div>
</el-popover>
<!-- <button
@click="openDetail(record)"
title="更多"
></button> -->
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="historyRecords.length === 0" class="empty-state">
<div class="empty-icon">
<el-icon class="documentDelete"><DocumentDelete /></el-icon>
<!-- <svg
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 2C13.1 2 14 2.9 14 4C14 5.1 13.1 6 12 6C10.9 6 10 5.1 10 4C10 2.9 10.9 2 12 2ZM21 9V7L15 1H5C3.89 1 3 1.89 3 3V21C3 22.1 3.89 23 5 23H19C20.1 23 21 22.1 21 21V9M19 9H14V4"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg> -->
</div>
<p class="empty-text">暂无历史记录</p>
</div>
@ -278,7 +313,6 @@
<div class="mobile-bottom-btn" @click="handleAnnouncementClick">
<img
class="mobile-bottom-announcement"
@click="openHistory"
src="https://d31zlh4on95l9h.cloudfront.net/images/c51c7fbb68671729801fb10d65bd7789.png"
alt="icon"
/>
@ -287,11 +321,31 @@
</div>
</div>
</div>
<el-dialog v-model="deleteDialogVisible" title="永久删除记录" width="500">
<span>删除后该记录将不可恢复确认删除吗</span>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeDeleteDialog()">取消</el-button>
<el-button type="primary" @click="deleteRecordConfirm()">
删除
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, computed, onMounted, watch } from "vue";
import {
getHistoryListAPI,
changeTopAPI,
deleteRecordAPI,
clickRecordAPI,
} from "../../api/AIxiaocaishen";
import moment from "moment";
import { ElMessage } from "element-plus";
// Props
const props = defineProps({
currentType: {
@ -326,122 +380,141 @@ const marketList = ref({
});
const isCollapsed = ref(false);
const selectedRecordId = ref(null);
const historyRecords = ref([
//
{
market: "cn",
code: "1A0001",
date: "2023-01-01",
isTop: "1",
updateTime: "2025-01-01",
},
//
{
market: "usa",
code: "AAPL",
date: "2025-01-15",
isTop: "1",
updateTime: "2025-01-15",
},
{
market: "hk",
code: "00700",
date: "2025-01-15",
isTop: "0",
updateTime: "2025-01-15",
},
const delObj = ref({});
const deleteDialogVisible = ref(false);
//
{
market: "cn",
code: "000001",
date: "2025-01-14",
isTop: "0",
updateTime: "2025-01-14",
},
{
market: "usa",
code: "TSLA",
date: "2025-01-13",
isTop: "1",
updateTime: "2025-01-13",
},
{
market: "hk",
code: "09988",
date: "2025-01-12",
isTop: "0",
updateTime: "2025-01-12",
},
const openDeleteDialog = () => {
deleteDialogVisible.value = true;
};
const closeDeleteDialog = () => {
delObj.value = {};
deleteDialogVisible.value = false;
};
//
{
market: "sg",
code: "D05",
date: "2025-01-13",
isTop: "0",
updateTime: "2025-01-13",
},
{
market: "cn",
code: "600519",
date: "2025-01-14",
isTop: "1",
updateTime: "2025-01-14",
},
{
market: "usa",
code: "NVDA",
date: "2025-01-10",
isTop: "0",
updateTime: "2025-01-10",
},
const historyRecords = ref([]);
const categoryHistory = ref([]);
const getHistoryList = async (params) => {
try {
const result = await getHistoryListAPI(params);
historyRecords.value = result.data.list;
let remainingRecords = result.data.list; //
// 1.
let topList = remainingRecords.filter((record) => record.isTop === 1);
remainingRecords = remainingRecords.filter((record) => record.isTop !== 1);
// 2.
let todayList = remainingRecords.filter((record) => {
const today = moment().format("YYYY-MM-DD");
const recordDate = moment(record.updateTime).format("YYYY-MM-DD");
return recordDate === today;
});
remainingRecords = remainingRecords.filter((record) => {
const today = moment().format("YYYY-MM-DD");
const recordDate = moment(record.updateTime).format("YYYY-MM-DD");
return recordDate !== today;
});
// 3. 3
let recent3DaysList = remainingRecords.filter((record) => {
const threeDaysAgo = moment().subtract(3, "days").startOf("day");
const yesterday = moment().subtract(1, "days").endOf("day");
const recordDate = moment(record.updateTime);
return recordDate.isAfter(threeDaysAgo) && recordDate.isBefore(yesterday);
});
remainingRecords = remainingRecords.filter((record) => {
const threeDaysAgo = moment().subtract(3, "days").startOf("day");
const yesterday = moment().subtract(1, "days").endOf("day");
const recordDate = moment(record.updateTime);
return !(
recordDate.isAfter(threeDaysAgo) && recordDate.isBefore(yesterday)
);
});
// 4. 73
let recent7DaysList = remainingRecords.filter((record) => {
const sevenDaysAgo = moment().subtract(7, "days").startOf("day");
const recordDate = moment(record.updateTime);
return recordDate.isAfter(sevenDaysAgo);
});
remainingRecords = remainingRecords.filter((record) => {
const sevenDaysAgo = moment().subtract(7, "days").startOf("day");
const recordDate = moment(record.updateTime);
return !recordDate.isAfter(sevenDaysAgo);
});
// 5. 30
let recent30DaysList = remainingRecords.filter((record) => {
const thirtyDaysAgo = moment().subtract(30, "days").startOf("day");
const recordDate = moment(record.updateTime);
return recordDate.isAfter(thirtyDaysAgo);
});
remainingRecords = remainingRecords.filter((record) => {
const thirtyDaysAgo = moment().subtract(30, "days").startOf("day");
const recordDate = moment(record.updateTime);
return !recordDate.isAfter(thirtyDaysAgo);
});
historyRecords.value = result.data.list;
categoryHistory.value = [
{
name: "置顶",
list: topList,
},
{
name: "今日",
list: todayList,
},
{
name: "近3日",
list: recent3DaysList,
},
{
name: "近7日",
list: recent7DaysList,
},
{
name: "近30日",
list: recent30DaysList,
},
];
console.log("historyRecords", historyRecords.value);
console.log("categoryHistory", categoryHistory.value);
} catch (e) {
console.error("获取历史记录出错", e);
}
};
//
{
market: "my",
code: "MAYBANK",
date: "2025-01-08",
isTop: "0",
updateTime: "2025-01-08",
},
{
market: "th",
code: "PTT",
date: "2025-01-05",
isTop: "1",
updateTime: "2025-01-05",
},
{
market: "vi",
code: "VIC",
date: "2025-01-03",
isTop: "0",
updateTime: "2025-01-03",
},
{
market: "can",
code: "SHOP",
date: "2025-01-02",
isTop: "0",
updateTime: "2025-01-02",
},
{
market: "hk",
code: "01299",
date: "2025-01-01",
isTop: "1",
updateTime: "2025-01-01",
},
{
market: "cn",
code: "000858",
date: "2025-01-06",
isTop: "0",
updateTime: "2025-01-06",
},
]);
const changeTop = async (param) => {
try {
await changeTopAPI(param);
} catch (e) {
console.error("置顶或取消置顶失败", e);
}
};
const changeTopStatus = async (isTop, id) => {
try {
if (isTop == 0 && categoryHistory.value[0].list.length >= 3) {
console.log("超过置顶上线");
ElMessage.warning("最多置顶三条内容,已达上限!");
return;
}
await changeTop({
model: props.currentType == "AIchat" ? 1 : 2,
recordId: id,
isTop: isTop == 1 ? 0 : 1,
});
await getHistoryList({
model: props.currentType == "AIchat" ? 1 : 2,
token: localStorage.getItem("localToken"),
});
} catch (error) {
console.error("操作失败:", error);
}
};
//
const toggleCollapse = () => {
@ -451,6 +524,10 @@ const toggleCollapse = () => {
};
const openHistory = () => {
// getHistoryList({
// model: props.currentType == "AIchat" ? 1 : 2,
// token: localStorage.getItem("localToken"),
// });
isCollapsed.value = false;
};
@ -462,90 +539,39 @@ const openDetail = (record) => {
console.log("record", record);
};
const selectRecord = (record) => {
// selectedRecordId.value = record.id;
// emit("selectRecord", record);
};
const deleteRecord = (recordId) => {
const index = historyRecords.value.findIndex(
(record) => record.id === recordId
);
if (index > -1) {
historyRecords.value.splice(index, 1);
saveToLocalStorage();
//
if (selectedRecordId.value === recordId) {
selectedRecordId.value = null;
}
}
};
const clearHistory = () => {
if (confirm("确定要清空所有历史记录吗?")) {
historyRecords.value = [];
selectedRecordId.value = null;
saveToLocalStorage();
}
};
const formatTime = (timestamp) => {
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
// 1
if (diff < 60000) {
return "刚刚";
}
// 1
if (diff < 3600000) {
return `${Math.floor(diff / 60000)}分钟前`;
}
// 1
if (diff < 86400000) {
return `${Math.floor(diff / 3600000)}小时前`;
}
// 1
if (diff < 604800000) {
return `${Math.floor(diff / 86400000)}天前`;
const historyData = ref({});
const selectRecord = async (record) => {
try {
const result = await clickRecordAPI({
model: props.currentType == "AIchat" ? 1 : 2,
parentId: record.parentId,
recordId: record.id,
});
historyData.value;
} catch (e) {
console.error("获取历史记录数据失败", e);
}
//
return date.toLocaleDateString("zh-CN", {
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
};
const saveToLocalStorage = () => {
localStorage.setItem(
"aiChatHistoryRecords",
JSON.stringify(historyRecords.value)
);
const deleteRecord = (id) => {
delObj.value.id = id;
openDeleteDialog();
};
const loadFromLocalStorage = () => {
const deleteRecordConfirm = async () => {
try {
const saved = localStorage.getItem("aiChatHistoryRecords");
if (saved) {
historyRecords.value = JSON.parse(saved);
}
//
const collapsedState = localStorage.getItem("historyRecordCollapsed");
if (collapsedState !== null) {
isCollapsed.value = collapsedState === "true";
}
} catch (error) {
console.error("加载历史记录失败:", error);
historyRecords.value = [];
const result = await deleteRecordAPI({
model: props.currentType == "AIchat" ? 1 : 2,
recordId: delObj.value.id,
});
console.log(result.msg);
closeDeleteDialog();
await getHistoryList({
model: props.currentType == "AIchat" ? 1 : 2,
token: localStorage.getItem("localToken"),
});
} catch (e) {
console.error("删除失败", e);
}
};
@ -561,24 +587,18 @@ const handleFeedbackClick = () => {
//
defineExpose({
clearHistory,
isCollapsed,
toggleCollapse,
getHistoryList,
});
//
onMounted(() => {
loadFromLocalStorage();
getHistoryList({
model: props.currentType == "AIchat" ? 1 : 2,
token: localStorage.getItem("localToken"),
});
});
//
watch(
historyRecords,
() => {
saveToLocalStorage();
},
{ deep: true }
);
</script>
<style scoped>
@ -805,6 +825,11 @@ watch(
background: rgba(255, 255, 255, 0.5);
}
.categoryName {
color: white;
padding: 12px;
}
.history-item {
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;

15
src/views/homePage.vue

@ -327,6 +327,13 @@ watch(
activeTab,
async () => {
console.log("activeTab变化了", activeTab.value);
if (activeTab.value == "AIchat" || activeTab.value == "AiEmotion") {
historyRecordRef.value.getHistoryList({
model: activeTab.value == "AIchat" ? 1 : 2,
token: localStorage.getItem("localToken"),
});
}
if (activeTab.value === "AIchat") {
isScrolling.value = false; //
setTimeout(() => {
@ -554,6 +561,12 @@ const throttledJudgeDevice = _.throttle(judgeDevice, 300, {
});
const expandHistory = () => {
// if (activeTab.value == "AIchat" || activeTab.value == "AiEmotion") {
// historyRecordRef.value.getHistoryList({
// token: localStorage.getItem("localToken"),
// model: activeTab.value == "AIchat" ? 1 : 2,
// });
// }
if (historyRecordRef) {
console.log("存在");
historyRecordRef.value.isCollapsed = !historyRecordRef.value.isCollapsed;
@ -603,7 +616,7 @@ onUnmounted(() => {
ref="historyRecordRef"
:current-type="activeTab"
@selectRecord="handleHistorySelect"
:is-mobile="isMobile"
:isMobile="isMobile"
@showAnnouncement="showAnnouncement"
@showFeedback="showFeedback"
/>

Loading…
Cancel
Save