feat: timeline

This commit is contained in:
Alisa | Side.one 2023-05-15 00:08:59 +08:00
parent 1e1472e0d5
commit ba4ec2abf2

View File

@ -1,318 +1,355 @@
<script lang="ts" setup> <script lang="ts" setup>
import ObjectElement from '@/components/dynamic/ObjectElement.vue'; import ObjectElement from '@/components/dynamic/ObjectElement.vue';
import { useBaseStore, useFormatter, useGovStore, useStakingStore, useTxDialog } from '@/stores'; import {
import type { GovProposal, GovVote, PaginabledAccounts, PaginatedProposalDeposit, PaginatedProposalVotes, Pagination } from '@/types'; useBaseStore,
import { ref , reactive} from 'vue'; useFormatter,
useGovStore,
useStakingStore,
useTxDialog,
} from '@/stores';
import type {
GovProposal,
GovVote,
PaginabledAccounts,
PaginatedProposalDeposit,
PaginatedProposalVotes,
Pagination,
} from '@/types';
import { ref, reactive } from 'vue';
import Countdown from '@/components/Countdown.vue'; import Countdown from '@/components/Countdown.vue';
import { computed } from '@vue/reactivity'; import { computed } from '@vue/reactivity';
const props = defineProps(["proposal_id", "chain"]); const props = defineProps(['proposal_id', 'chain']);
const proposal = ref({} as GovProposal) const proposal = ref({} as GovProposal);
const format = useFormatter() const format = useFormatter();
const store = useGovStore() const store = useGovStore();
const dialog = useTxDialog() const dialog = useTxDialog();
store.fetchProposal(props.proposal_id).then((res) => { store.fetchProposal(props.proposal_id).then((res) => {
const proposalDetail = reactive(res.proposal) const proposalDetail = reactive(res.proposal);
// when status under the voting, final_tally_result are no data, should request fetchTally // when status under the voting, final_tally_result are no data, should request fetchTally
if (res.proposal?.status === 'PROPOSAL_STATUS_VOTING_PERIOD'){ if (res.proposal?.status === 'PROPOSAL_STATUS_VOTING_PERIOD') {
store.fetchTally(props.proposal_id).then((tallRes)=>{ store.fetchTally(props.proposal_id).then((tallRes) => {
proposalDetail.final_tally_result = tallRes?.tally proposalDetail.final_tally_result = tallRes?.tally;
}) });
} }
proposal.value = proposalDetail proposal.value = proposalDetail;
}) });
const color = computed(() => { const color = computed(() => {
if (proposal.value.status==='PROPOSAL_STATUS_PASSED') { if (proposal.value.status === 'PROPOSAL_STATUS_PASSED') {
return "success" return 'success';
}else if (proposal.value.status==='PROPOSAL_STATUS_REJECTED') { } else if (proposal.value.status === 'PROPOSAL_STATUS_REJECTED') {
return "error" return 'error';
} }
return "" return '';
}) });
const status = computed(() => { const status = computed(() => {
if(proposal.value.status) { if (proposal.value.status) {
return proposal.value.status.replace("PROPOSAL_STATUS_", "") return proposal.value.status.replace('PROPOSAL_STATUS_', '');
} }
return "" return '';
}) });
const deposit = ref({} as PaginatedProposalDeposit) const deposit = ref({} as PaginatedProposalDeposit);
store.fetchProposalDeposits(props.proposal_id).then(x => deposit.value = x) store.fetchProposalDeposits(props.proposal_id).then((x) => (deposit.value = x));
const votes = ref({} as GovVote[]) const votes = ref({} as GovVote[]);
const votePage = ref({} as Pagination) const votePage = ref({} as Pagination);
const loading = ref(false) const loading = ref(false);
store.fetchProposalVotes(props.proposal_id).then(x => { store.fetchProposalVotes(props.proposal_id).then((x) => {
votes.value = x.votes votes.value = x.votes;
votePage.value = x.pagination votePage.value = x.pagination;
}) });
function loadMore() { function loadMore() {
if(votePage.value.next_key) { if (votePage.value.next_key) {
loading.value = true loading.value = true;
store.fetchProposalVotes(props.proposal_id, votePage.value.next_key).then(x => { store
votes.value = votes.value.concat(x.votes) .fetchProposalVotes(props.proposal_id, votePage.value.next_key)
votePage.value = x.pagination .then((x) => {
loading.value = false votes.value = votes.value.concat(x.votes);
}) votePage.value = x.pagination;
} loading.value = false;
});
}
} }
function shortTime(v: string) { function shortTime(v: string) {
if(v) { if (v) {
return format.toDay(v, "from") return format.toDay(v, 'from');
} }
return "" return '';
} }
const votingCountdown = computed((): number => { const votingCountdown = computed((): number => {
const now = new Date(); const now = new Date();
const end = new Date(proposal.value.voting_end_time) const end = new Date(proposal.value.voting_end_time);
return end.getTime() - now.getTime() return end.getTime() - now.getTime();
}) });
const upgradeCountdown = computed((): number => { const upgradeCountdown = computed((): number => {
const height = Number(proposal.value.content?.plan?.height || 0) const height = Number(proposal.value.content?.plan?.height || 0);
if(height > 0) { if (height > 0) {
const base = useBaseStore() const base = useBaseStore();
const current = Number(base.latest?.block?.header?.height || 0) const current = Number(base.latest?.block?.header?.height || 0);
return (height - current) * 6 * 1000 return (height - current) * 6 * 1000;
} }
const now = new Date(); const now = new Date();
const end = new Date(proposal.value.content?.plan?.time || "") const end = new Date(proposal.value.content?.plan?.time || '');
return end.getTime() - now.getTime() return end.getTime() - now.getTime();
}) });
const total = computed(()=> { const total = computed(() => {
const tally = proposal.value.final_tally_result const tally = proposal.value.final_tally_result;
let sum = 0 let sum = 0;
if(tally) { if (tally) {
sum += Number(tally.abstain || 0) sum += Number(tally.abstain || 0);
sum += Number(tally.yes || 0) sum += Number(tally.yes || 0);
sum += Number(tally.no || 0) sum += Number(tally.no || 0);
sum += Number(tally.no_with_veto || 0) sum += Number(tally.no_with_veto || 0);
}
} return sum;
return sum });
})
const turnout = computed(() => { const turnout = computed(() => {
if (total.value > 0) { if (total.value > 0) {
const bonded = useStakingStore().pool?.bonded_tokens || "1" const bonded = useStakingStore().pool?.bonded_tokens || '1';
return format.percent(total.value / Number(bonded)) return format.percent(total.value / Number(bonded));
} }
return 0 return 0;
}) });
const yes = computed(()=> { const yes = computed(() => {
if(total.value > 0) { if (total.value > 0) {
const yes = proposal.value?.final_tally_result?.yes || 0 const yes = proposal.value?.final_tally_result?.yes || 0;
return format.percent(Number(yes) / total.value ) return format.percent(Number(yes) / total.value);
} }
return 0 return 0;
}) });
const no = computed(()=> { const no = computed(() => {
if(total.value > 0) { if (total.value > 0) {
const value = proposal.value?.final_tally_result?.no || 0 const value = proposal.value?.final_tally_result?.no || 0;
return format.percent(Number(value) / total.value) return format.percent(Number(value) / total.value);
} }
return 0 return 0;
}) });
const veto = computed(()=> { const veto = computed(() => {
if(total.value > 0) { if (total.value > 0) {
const value = proposal.value?.final_tally_result?.no_with_veto || 0 const value = proposal.value?.final_tally_result?.no_with_veto || 0;
return format.percent(Number(value) / total.value) return format.percent(Number(value) / total.value);
} }
return 0 return 0;
}) });
const abstain = computed(()=> { const abstain = computed(() => {
if(total.value > 0) { if (total.value > 0) {
const value = proposal.value?.final_tally_result?.abstain || 0 const value = proposal.value?.final_tally_result?.abstain || 0;
return format.percent(Number(value) / total.value) return format.percent(Number(value) / total.value);
} }
return 0 return 0;
}) });
const processList = computed(()=>{ const processList = computed(() => {
return [ return [
{name: 'Turnout', value : turnout.value, class: 'bg-info' }, { name: 'Turnout', value: turnout.value, class: 'bg-info' },
{name: 'Yes', value : yes.value, class: 'bg-success' }, { name: 'Yes', value: yes.value, class: 'bg-success' },
{name: 'No', value : no.value, class: 'bg-error' }, { name: 'No', value: no.value, class: 'bg-error' },
{name: 'No With Veto', value : veto.value, class: 'bg-primary' }, { name: 'No With Veto', value: veto.value, class: 'bg-primary' },
{name: 'Abstain', value : abstain.value, class: 'bg-warning' } { name: 'Abstain', value: abstain.value, class: 'bg-warning' },
] ];
}) });
</script> </script>
<template> <template>
<div> <div>
<div class="bg-base-100 px-4 pt-3 pb-4 rounded mb-4 shadow"> <div class="bg-base-100 px-4 pt-3 pb-4 rounded mb-4 shadow">
<h2 class="card-title flex flex-col md:justify-between md:flex-row"> <h2 class="card-title flex flex-col md:justify-between md:flex-row">
<p class="truncate w-full">{{ proposal_id }}. {{ proposal.content?.title }} </p> <p class="truncate w-full">
<div {{ proposal_id }}. {{ proposal.content?.title }}
class="badge badge-ghost" </p>
:class=" <div
color === 'success' class="badge badge-ghost"
? 'text-yes' :class="
: color === 'error' color === 'success'
? 'text-no' ? 'text-yes'
: 'text-info' : color === 'error'
" ? 'text-no'
>{{ status }}</div> : 'text-info'
</h2> "
<div class=""> >
<ObjectElement :value="proposal.content"/> {{ status }}
</div> </div>
</h2>
<div class="">
<ObjectElement :value="proposal.content" />
</div>
</div> </div>
<!-- grid lg:grid-cols-3 auto-rows-max--> <!-- grid lg:grid-cols-3 auto-rows-max-->
<!-- flex-col lg:flex-row flex --> <!-- flex-col lg:flex-row flex -->
<div class="gap-4 mb-4 grid lg:grid-cols-3 auto-rows-max "> <div class="gap-4 mb-4 grid lg:grid-cols-3 auto-rows-max">
<!-- flex-1 --> <!-- flex-1 -->
<div class="bg-base-100 px-4 pt-3 pb-4 rounded shadow "> <div class="bg-base-100 px-4 pt-3 pb-4 rounded shadow">
<h2 class="card-title">Tally</h2> <h2 class="card-title mb-1">Tally</h2>
<div v-for="(item,index) of processList" :key="index"> <div class="mb-1" v-for="(item, index) of processList" :key="index">
<label class="block">{{item.name }}</label> <label class="block text-sm mb-1">{{ item.name }}</label>
<div class="h-6 w-full relative"> <div class="h-5 w-full relative">
<div class="absolute inset-x-0 inset-y-0 w-full opacity-10 rounded-sm" :class="`${item.class}`"></div> <div
<div class="absolute inset-x-0 inset-y-0 rounded-sm" :class="`${item.class}`" :style="`width: ${item.value}`"></div> class="absolute inset-x-0 inset-y-0 w-full opacity-10 rounded-sm"
<p class="absolute inset-x-0 inset-y-0 text-center text-sm text-[#666] dark:text-[#eee] flex items-center justify-center">{{ item.value }}</p> :class="`${item.class}`"
</div> ></div>
</div> <div
<div> class="absolute inset-x-0 inset-y-0 rounded-sm"
<label for="vote" class="btn btn-primary float-right btn-sm mx-1" @click="dialog.open('vote', {proposal_id})">Vote</label> :class="`${item.class}`"
<label for="deposit" class="btn btn-primary float-right btn-sm mx-1" @click="dialog.open('deposit', {proposal_id})">Deposit</label> :style="`width: ${item.value}`"
</div> ></div>
</div> <p
<!-- lg:col-span-2 --> class="absolute inset-x-0 inset-y-0 text-center text-sm text-[#666] dark:text-[#eee] flex items-center justify-center"
<!-- lg:flex-[2_2_0%] -->
<div class="h-max bg-base-100 px-4 pt-3 pb-4 rounded shadow lg:col-span-2">
<h2 class="card-title">Timeline</h2>
<VTimeline
class="mt-2"
side="end"
align="start"
line-inset="8"
truncate-line="both"
density="compact"
> >
<VTimelineItem {{ item.value }}
dot-color="error" </p>
size="x-small" </div>
>
<!-- 👉 Header -->
<div class="d-flex justify-space-between flex-wrap mb-3">
<h6 class="text-base font-weight-medium me-3">
Submited at: {{ format.toDay(proposal.submit_time) }}
</h6>
<small class="text-xs text-disabled my-1">{{ shortTime(proposal.submit_time) }}</small>
</div>
</VTimelineItem>
<VTimelineItem
size="x-small"
dot-color="primary"
>
<!-- 👉 Header -->
<div class="d-flex justify-space-between flex-wrap mb-3">
<h6 class="text-base font-weight-medium me-3">
Deposited at: {{ format.toDay(proposal.status==="PROPOSAL_STATUS_DEPOSIT_PERIOD"?proposal.deposit_end_time: proposal.voting_start_time) }}
</h6>
<small class="text-xs text-disabled text-no-wrap my-1">{{ shortTime(proposal.status==="PROPOSAL_STATUS_DEPOSIT_PERIOD"?proposal.deposit_end_time: proposal.voting_start_time) }}</small>
</div>
<p class="mb-0">
<div v-for="x of deposit.deposits">
{{ x.depositor }} {{ format.formatTokens(x.amount) }}
</div>
</p>
</VTimelineItem>
<VTimelineItem
size="x-small"
dot-color="success"
>
<!-- 👉 Header -->
<div class="d-flex justify-space-between flex-wrap mb-3">
<h6 class="text-base font-weight-medium me-3">
Voting start from {{ format.toDay(proposal.voting_start_time) }}
</h6>
<small class="text-xs text-disabled text-no-wrap my-1">{{ shortTime(proposal.voting_start_time) }}</small>
</div>
<!-- 👉 Content -->
<p class="mb-0">
<Countdown :time="votingCountdown"/>
</p>
</VTimelineItem>
<VTimelineItem
size="x-small"
dot-color="success"
>
<!-- 👉 Header -->
<div class="d-flex justify-space-between flex-wrap mb-3">
<h6 class="text-base font-weight-medium me-3">
Voting end {{ format.toDay(proposal.voting_end_time) }}
</h6>
<small class="text-xs text-disabled text-no-wrap my-1">{{ shortTime(proposal.voting_end_time) }}</small>
</div>
<!-- 👉 Content -->
<p class="mb-0">
Current Status: {{ proposal.status }}
</p>
</VTimelineItem>
<VTimelineItem
v-if="proposal.content && proposal.content['@type'].endsWith('SoftwareUpgradeProposal')"
size="x-small"
dot-color="success"
>
<!-- 👉 Header -->
<div class="d-flex justify-space-between flex-wrap mb-3">
<h6 class="text-base font-weight-medium me-3">
Upgrade Plan:
<span v-if="Number(proposal.content?.plan?.height||'0') > 0"> (EST)</span>
<span v-else>{{ format.toDay(proposal.content?.plan?.time) }}</span>
</h6>
<small class="text-xs text-disabled text-no-wrap my-1">{{ shortTime(proposal.voting_end_time) }}</small>
</div>
<!-- 👉 Content -->
<p class="mb-0">
<Countdown :time="upgradeCountdown"/>
</p>
</VTimelineItem>
</VTimeline>
</div> </div>
<div class="mt-6 grid grid-cols-2">
<label
for="vote"
class="btn btn-primary float-right btn-sm mx-1"
@click="dialog.open('vote', { proposal_id })"
>Vote</label
>
<label
for="deposit"
class="btn btn-primary float-right btn-sm mx-1"
@click="dialog.open('deposit', { proposal_id })"
>Deposit</label
>
</div>
</div>
<div
class="bg-base-100 px-4 pt-3 pb-5 rounded shadow lg:col-span-2"
>
<h2 class="card-title">Timeline</h2>
<div class="px-1">
<div class="flex items-center mb-4 mt-2">
<div class="w-2 h-2 rounded-full bg-error mr-3"></div>
<div class="text-base flex-1 text-main">
Submited at: {{ format.toDay(proposal.submit_time) }}
</div>
<div class="text-sm">{{ shortTime(proposal.submit_time) }}</div>
</div>
<div class="flex items-center mb-4">
<div class="w-2 h-2 rounded-full bg-primary mr-3"></div>
<div class="text-base flex-1 text-main">
Deposited at:
{{
format.toDay(
proposal.status === 'PROPOSAL_STATUS_DEPOSIT_PERIOD'
? proposal.deposit_end_time
: proposal.voting_start_time
)
}}
</div>
<div class="text-sm">
{{
shortTime(
proposal.status === 'PROPOSAL_STATUS_DEPOSIT_PERIOD'
? proposal.deposit_end_time
: proposal.voting_start_time
)
}}
</div>
</div>
<div class="mb-4">
<div class="flex items-center">
<div class="w-2 h-2 rounded-full bg-yes mr-3"></div>
<div class="text-base flex-1 text-main">
Voting start from {{ format.toDay(proposal.voting_start_time) }}
</div>
<div class="text-sm">
{{ shortTime(proposal.voting_start_time) }}
</div>
</div>
<div class="pl-5 text-sm mt-2">
<Countdown :time="votingCountdown" />
</div>
</div>
<div>
<div class="flex items-center mb-1">
<div class="w-2 h-2 rounded-full bg-success mr-3"></div>
<div class="text-base flex-1 text-main">
Voting end {{ format.toDay(proposal.voting_end_time) }}
</div>
<div class="text-sm">
{{ shortTime(proposal.voting_end_time) }}
</div>
</div>
<div class="pl-5 text-sm">
Current Status: {{ proposal.status }}
</div>
</div>
<div
class="mt-4"
v-if="
proposal?.content?.['@type']?.endsWith('SoftwareUpgradeProposal')
"
>
<div class="flex items-center">
<div class="w-2 h-2 rounded-full bg-warning mr-3"></div>
<div class="text-base flex-1 text-main">
Upgrade Plan:
<span v-if="Number(proposal.content?.plan?.height || '0') > 0">
(EST)</span
>
<span v-else>{{
format.toDay(proposal.content?.plan?.time)
}}</span>
</div>
<div class="text-sm">
{{ shortTime(proposal.voting_end_time) }}
</div>
</div>
<div class="pl-5 text-sm mt-2">
<Countdown :time="upgradeCountdown" />
</div>
</div>
</div>
</div>
</div> </div>
<div class="bg-base-100 px-4 pt-3 pb-4 rounded mb-4 shadow"> <div class="bg-base-100 px-4 pt-3 pb-4 rounded mb-4 shadow">
<h2 class="card-title">Votes</h2> <h2 class="card-title">Votes</h2>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="table w-full"> <table class="table w-full table-zebra">
<tbody> <tbody>
<tr v-for="(item,index) of votes" :key="index"> <tr v-for="(item, index) of votes" :key="index">
<td>{{ item.voter }}</td> <td class="py-2 text-sm">{{ item.voter }}</td>
<td>{{ item.option }}</td> <td
</tr> class="py-2 text-sm"
</tbody> :class="{
</table> 'text-yes': item.option === 'VOTE_OPTION_YES',
'text-gray-400': item.option === 'VOTE_OPTION_ABSTAIN',
}"
>
{{ item.option }}
</td>
</tr>
</tbody>
</table>
<button <button
v-if="votePage.next_key" @click="loadMore()"
@click="loadMore()" v-if="votePage.next_key"
:disabled="loading" :disabled="loading"
class="btn btn-outline btn-primary w-full" class="btn btn-outline btn-primary w-full mt-4"
style="border: 1px solid hsl(var(--p));" >
>Load more</button> Load more
</div> </button>
</div>
</div> </div>
</div> </div>
</template> </template>