Files
flagnote-web/src/views/ViewNote.vue
2022-12-02 17:58:20 +08:00

619 lines
16 KiB
Vue

<style scoped>
.layout {
height: 100%;
background: #dddddd;
}
.header {
background: #dddddd;
}
.ivu-layout-header {
line-height: normal;
height: auto;
padding: 0px;
}
.content {
background: #dddddd;
}
.ivu-layout-content {
padding: 0px;
}
.layout-footer-center {
background: #dddddd;
text-align: center;
}
.ivu-card-bordered {
border: 0px solid #dcdee2;
border-color: #e8eaec;
}
.fnmodal {
align-items: center;
justify-content: center;
}
@media print {
@page {
size: portrait;
margin: 20px;
}
body {
margin: 0.8cm;
background-color: #ffffff;
}
.header {
display: none;
}
.layout-footer-center {
display: none;
}
}
</style>
<style>
#qrUrl {
color: #ed4014;
font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei";
}
#qrUrl::selection {
background-color: #ed4014;
color: #ffffff;
}
#copyBtn {
color: #ed4014;
font-size: large;
}
.ivu-btn-text:focus {
margin-top: -3px;
box-shadow: none !important;
}
.tab_pre {
display: inline;
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
font-size: 14px;
}
.tab_pre::selection {
background: #ed4014;
color: #FFFFFF;
}
.tab_pre::-moz-selection {
background: #ed4014;
color: #FFFFFF;
}
.vue-contextmenu-listWrapper {
background: #ed4014 !important;
border-radius: 0px !important;
}
.vue-contextmenu-listWrapper .context-menu-list {
background: #ed4014 !important;
margin: 0px !important;
}
.context-menu-list:hover {
background: #f16643 !important;
}
.btn-wrapper-simple {
height: 24px !important;
margin-top: 1px !important;
text-align: left !important;
}
.no-child-btn {
padding: 0px 10px !important;
}
.nav-name-right {
margin: 0px 20px 0px 5px !important;
color: #ffffff !important;
font-size: 14px !important;
line-height: 24px !important;
font-family: "Bitstream Vera Sans Mono", Consolas, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei";
}
.ivu-modal-content {
border-radius: 0px !important;
}
/* #noteMenu button span{
font-size: 15px;
line-height: 20px;
margin-left: 7px !important;
margin-right: 5px !important;
} */
</style>
<template>
<div class="layout">
<Layout>
<Header class="header">
<Row>
<Col :xs="{ span: 24, offset: 0 }" :sm="{ span: 22, offset: 1 }" :md="{ span: 20, offset: 2 }"
:lg="{ span: 18, offset: 3 }" :xl="{ span: 16, offset: 4 }" :xxl="{ span: 16, offset: 4 }">
<Affix :offset-top="0">
<div style="background: white;width:100%;height:40px;">
<img style="height:40px;float:left;cursor: pointer;" alt="refresh flagnote" src="/static/logo.png"
v-on:click="refreshPage()">
<div style="float:left;width:auto;">
<Button-group size="large">
<Button aria-label="to top" v-show="toTopState" type="text"
style="margin-left:0px; border-radius: 0px; font-size: 28px;color:red;line-height: 20px;"
@click="toTop()" icon="ios-arrow-up" ghost></Button>
</Button-group>
</div>
<div style="float:right;width:auto;">
<Button-group size="large">
<Button aria-label="share" type="error"
style="margin-left:5px; border-radius: 0px;font-size: 19px; font-family: Arial, sans-serif"
@click="showShareModel()" icon="md-cloud-done">{{ state.ttlDesc }}</Button>
<Button aria-label="menu" type="error" style="margin-left:5px; border-radius: 0px;font-size: 24px;"
@click="switchMenu()" @blur.native="hideMenu()" icon="md-menu"></Button>
</Button-group>
</div>
</div>
<div id="noteMenu" :class="[showMenuState ? 'showBlock' : 'hideBlock']"
style="z-index: 100; position: absolute;top:41px;right:0px;left:auto;">
<Button-group vertical size="large">
<Button type="error" icon="md-add" style="border-radius: 0px;font-size: 24px;"
@click="createNote(); switchMenu();"></Button>
<!--
<Button type="error" icon="md-refresh" style="border-radius: 0px;font-size: 24px;"
@click="refreshPage()"></Button>
-->
<Button aria-label="download text" v-show="model.showDownloadText" type="error"
style="border-radius: 0px;font-size: 24px;" @click="downLoadText(); switchMenu();"
icon="md-download"></Button>
<Button type="error" icon="md-trash" style="border-radius: 0px;font-size: 24px;"
@click="showDeleteModel(); switchMenu();"></Button>
</Button-group>
</div>
</Affix>
</Col>
</Row>
</Header>
<Content class="content">
<div style="min-height: 650px;">
<Row>
<Col :xs="{ span: 24, offset: 0 }" :sm="{ span: 22, offset: 1 }" :md="{ span: 20, offset: 2 }"
:lg="{ span: 18, offset: 3 }" :xl="{ span: 16, offset: 4 }" :xxl="{ span: 16, offset: 4 }">
<Card :padding="0">
<div id="wrapper" style="border-left: 0px solid #FF3366;">
<div id="noteText" style="text-align: left;min-height: 650px;" class="monoFt"
v-html="noteForm.escapeText">
</div>
</div>
</Card>
</Col>
</Row>
</div>
</Content>
<Footer class="layout-footer-center">
2022 &copy; flagnote.com
</Footer>
</Layout>
<Modal v-model="model.showShare" width="360" footer-hide class-name="qrmodal" :styles="{ borderRadius: 0 }"
@on-cancel="closeShareModel">
<p style="text-align: center;
z-index: 1000;
position: absolute;
top: -2px;
left: 0px;
width: 100%;">
<Tag style="border-radius: 0px;" v-show="model.copyTip" color="#ed4014" text="">Url Copied.</Tag>
</p>
<p style="text-align: center;margin-top:20px;line-height:100%">
<span style="margin-right:3px;" id="copyBtn">
<Icon type="md-copy" />
</span><span id="qrUrl">{{ noteForm.noteUrl }}</span>
</p>
<p style="text-align: center;margin-top:5px;">
<canvas id="qrImg" class=""></canvas>
</p>
<p style="text-align: center;margin-top:10px;">
<Button type="error" style="border-radius: 0px;font-size:19px;"
@click="closeShareModel()">{{ $t("button.close") }}</Button>
</p>
</Modal>
<Modal v-model="model.showDelete" width="360" footer-hide class-name="fnmodal" :styles="{ borderRadius: 0 }">
<p style="text-align: center;font-size:medium;margin-bottom: 20px;">
{{ $t("message.askTodelete") }}
</p>
<p style="text-align: center;">
<Button type="error" :loading="model.deleting" style="border-radius: 0px;font-size:19px;" @click="dropNote()">{{
$t("button.yes")
}}</Button>
</p>
</Modal>
</div>
</template>
<script>
import { md5, unwrap } from "@/libs/secret";
import { getStoreKey, getSecretKey } from "@/api/lock";
import { deleteNote, getNoteBlob } from "@/api/note";
import storage from "@/libs/storage";
import { getEscapeText } from "@/libs/noteStorage";
import QRCode from "qrcode";
import Clipboard from "clipboard";
import { saveAs } from 'file-saver';
import { isWeixin, getNoteUrl } from "@/libs/utils";
export default {
name: 'ViewNote',
components: {},
props: {},
data() {
return {
noteForm: {
url: '',
noteUrl: '',
text: '',
escapeText: '',
key: '',
},
secret: {
storeKey: '',
secretKey: '',
cipher: '',
md5: '',
},
state: {
lock: null,
initTime: null,
initTtl: null,
ttl: null,
ttlDesc: '-- : --',
serverTime: null,
},
model: {
showDelete: false,
showShare: false,
deleting: false,
showDownloadText: false,
copyTip: false,
},
toTopState: false,
showMenuState: false,
}
},
created() {
// read $route
this.noteForm.key = this.$route.params.name;
let noteMeta = this.$route.meta.noteMeta;
//ipad chrome url not redirect
let path = location.pathname;
if ("/" == path) {
history.pushState('', '', '/' + this.noteForm.key);
}
//wx does not show downloadText
this.model.showDownloadText = !isWeixin();
this.noteForm.noteUrl = getNoteUrl(this.noteForm.key);
this.secret.storeKey = getStoreKey(this.noteForm.key);
this.secret.secretKey = getSecretKey(this.noteForm.key, this.secret.password);
if (noteMeta) {
this.state.lock = noteMeta.lock;
this.state.initTime = new Date().getTime();
this.state.initTtl = noteMeta.ttl;
this.state.ttl = noteMeta.ttl;
this.state.serverTime = noteMeta.serverTime;
this.secret.cipher = "00000000000000000000000000000000";//noteMeta.cipher; //读者有没有值可配置
this.secret.md5 = noteMeta.md5;
this.startClock();
storage.local.dynamicClear();
this.loadText();
this.bindCtrlAllEvent();
this.bindCopyUrlEvent();
this.bindToTopEvent();
} else {
alert("Unconnected.");
}
},
mounted() {
let stateInfo = storage.session.getText(this.secret.storeKey + "_share");
if (stateInfo == '1') {
storage.session.setText(this.secret.storeKey + "_share", '0');
this.showShareModel();
}
const myObserver = new ResizeObserver(entries => {
// iterate over the entries, do something.
entries.forEach(entry => {
let affix = document.querySelector('.ivu-affix');
if (affix) {
affix.setAttribute("style", "top: 0px; width: " + entry.contentRect.width + "px;");
}
});
});
const someOtherEl = document.querySelector('#wrapper');
myObserver.observe(someOtherEl);
},
updated() {
},
beforeDestroy() {
},
destroyed() {
},
computed: {},
watch: {},
methods: {
switchMenu() {
this.showMenuState = !this.showMenuState;
},
hideMenu() {
let hbt = document.querySelector('#noteMenu > div > button:hover');
if (!hbt) {
this.showMenuState = false;
}
},
downLoadText() {
var blob = new Blob([this.noteForm.text], { type: "application/octet-stream;charset=utf-8" });
saveAs(blob, this.noteForm.key + ".txt");
},
toTop() {
window.scrollTo(0, 0);
},
startClock() {
let that = this;
window.setInterval(function () {
let ittl = parseInt(that.state.ttl / 1000);
let mins = parseInt(ittl / 60);
if (mins < 0) {
mins = "00";
} else if (mins < 10) {
mins = "0" + mins;
}
let seds = parseInt(ittl % 60);
if (seds < 0) {
seds = "00";
} else if (seds < 10) {
seds = "0" + seds;
}
that.state.ttlDesc = mins + ":" + seds;
that.state.ttl = that.state.initTtl - (new Date().getTime() - that.state.initTime);
if (that.state.ttl <= 0) {
storage.local.delete(that.secret.storeKey);
location.reload();
}
}, 1000)
},
refreshPage() {
window.location.reload();
},
createNote() {
window.open("/");
},
showShareModel() {
this.model.showShare = true;
let qrimg = document.getElementById("qrImg");
let qrurl = "https://flagnote.com/" + this.noteForm.key;
var opts = {
errorCorrectionLevel: 'Q',
type: 'image/jpeg',
quality: 0.9,
height: 192,
width: 192,
margin: 1,
color: {
dark: "#ed4014",
light: "#FFFFFF"
}
}
QRCode.toCanvas(qrimg, qrurl, opts)
storage.session.setText(this.secret.storeKey + "_share", '1');
},
closeShareModel() {
if (this.model.showShare) {
this.model.showShare = false;
}
storage.session.setText(this.secret.storeKey + "_share", '0');
},
showDeleteModel() {
this.model.showDelete = true;
},
dropNote() {
this.model.deleting = true;
let that = this;
deleteNote(this.noteForm.key).then(res => {
if (res) {
storage.local.delete(that.secret.storeKey);
location.reload();
} else {
that.model.deleting = false;
}
});
},
loadText() {
let that = this;
let storeInfo = storage.local.getText(this.secret.storeKey);
let starray = [];
if (storeInfo) {
starray = storeInfo.split('|');
}
if (!storeInfo || !starray[4] || (md5(starray[4]) != this.secret.md5)) {
// local is useless
getNoteBlob(this.noteForm.key).then((res) => {
if (!res.data || res.data.size == 0) {
return;
}
let blob = new Blob([res.data], {
type: res.data.type
});
let reader = new FileReader();
reader.onload = function (e) {
if (!e.target.result || e.target.result.byteLength == 0) {
return;
}
var bytes = new Uint8Array(e.target.result);
let bytesString = bytes.join(",");
that.noteForm.text = unwrap(bytesString, that.secret.secretKey);
that.noteForm.escapeText = getEscapeText(that.noteForm.text);
// if local is enough, set local
if (storage.local.getAvailableSize() > 1 * 1024 * 1024) {
storage.local.setText(that.secret.storeKey, that.state.lock + '|' + that.secret.cipher + '|1|' + that.state.serverTime + '|' + bytesString);
}
};
reader.readAsArrayBuffer(blob);
});
} else {
starray = storeInfo.split('|');
if (!starray[4]) {
return;
}
this.noteForm.text = unwrap(starray[4], this.secret.secretKey);
this.noteForm.escapeText = getEscapeText(this.noteForm.text);
// local is usable, and set commited flag
if ("0" == starray[2]) {
storage.local.setText(this.secret.storeKey, starray[0] + "|" + starray[1] + "|1|" + starray[3] + "|" + starray[4]);
}
}
},
bindToTopEvent() {
let that = this;
window.onscroll = function () {
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
if (scrollTop >= 20) {
that.toTopState = true;
} else {
that.toTopState = false;
}
}
},
bindCopyUrlEvent() {
let that = this;
var clipboard = new Clipboard("#copyBtn", {
target: function () {
return document.querySelector('#qrUrl');
}
})
clipboard.on('success', function () {
that.model.copyTip = true;
let tipTimer = setInterval(() => {
that.model.copyTip = false;
clearInterval(tipTimer);
}, 1500);
});
clipboard.on('error', function () {
that.$Message.error('not allow to copy.');
});
},
bindCtrlAllEvent() {
if (window.getSelection && document.createRange) {
document.onkeydown = (e) => {
if ((e.ctrlKey || e.metaKey) && e.key == "a") {
e.preventDefault();
var element = document.getElementById("noteText");
let selection = window.getSelection();
let range = document.createRange();
range.selectNodeContents(element);
selection.removeAllRanges();
selection.addRange(range);
}
}
}
},
}
}
</script>