This commit is contained in:
zdz
2026-03-19 15:02:23 +08:00
commit 13b28ceec9
121 changed files with 16531 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
<template>
<div class="page-content">
<div class="normal-title f-b text-center inboxTitle p-10">{{ workInbox.inboxTitle }}</div>
<div class="text-center">
<van-tag type="primary" size="medium" v-if="workInbox.stockCode">
{{ workInbox.stockCode }}{{ workInbox.stockName }}
</van-tag>
<van-tag type="success" size="medium" class="ms-10">{{ workInbox.inboxTypeSecondName }}</van-tag>
</div>
<div class="content text-end mb-5 pe-16">{{ workInbox.firstChargeUserName }}</div>
<div class="content text-end pe-16">{{ workInbox.createTime }}</div>
<div v-html="workInbox.inboxContent" class="p-16 inboxContent content"></div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import emitter from '@/utils/mitt'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const init = () => {
if (route.query && route.query.inboxId) {
emitter.emit('showLoading', '')
let inboxId = route.query.inboxId
getWorkInboxDetail(inboxId)
} else {
router.push({ name: '404' })
}
}
let workInbox = ref({
inboxTitle: '',
stockCode: '',
stockName: '',
inboxTypeSecondName: '',
firstChargeUserName: '',
createTime: '',
inboxContent: ''
})
import { workInboxInfo } from '@/utils/api'
const getWorkInboxDetail = async (inboxId: any) => {
const { data } = await workInboxInfo(inboxId)
workInbox.value = data.workInbox
emitter.emit('setTitle', { title: workInbox.value.inboxTitle, type: 'comment' })
emitter.emit('hiddenLoading', '')
}
init()
</script>
<style lang='scss' scoped>
.inboxContent {
margin: 12px 16px 0;
width: calc(100vw - 32px);
line-height: 20px;
font-size: 16px;
overflow-y: auto;
height: calc(100vh - 140px);
@media screen and (min-width: 678px) {
margin: 12px 0;
width: calc(674px);
}
}
:deep(table) {
border-top: 1px solid #ccc;
border-left: 1px solid #ccc;
}
:deep(td) {
border-bottom: 1px solid #ccc;
border-right: 1px solid #ccc;
white-space: nowrap;
}
:deep(img) {
width: 100%;
}
:deep(pre) {
white-space: pre-wrap;
word-wrap: break-word;
}
.inboxTitle {
max-width: 678px;
word-break: break-all;
}
</style>

125
src/views/comment/list.vue Normal file
View File

@@ -0,0 +1,125 @@
<template>
<div class="page-content" ref="pageContent">
<div class="head">
<van-image class="bg" src="/img/commentBg.jpg" fit="cover" />
<div class="headDiv p-16">
<div class="headTop">
<div class="d-flex justify-content-between">
<div>
<div class="border-bottom-1">
<span class="day">{{ day }}</span>
<span class="month1 ms-10">{{ month1 }}</span>
</div>
<div class="textSize text-end">{{ year }}</div>
</div>
<van-image class="logo border-radius-4" src="/img/completeLogo.jpg" />
</div>
</div>
<div class="d-flex justify-content-end">
<div class="border-radius-4 border-1 text-center p-8">
<div>{{ month2 }}</div>
<div>{{ week }}</div>
</div>
</div>
<div class="textSize">
<div>第一上海</div>
<div class="mt-2">精选点评让你了解更多行业新闻</div>
</div>
</div>
</div>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh" v-if="list.length > 0">
<van-list class="mt-10" v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad"
:immediate-check="false">
<van-cell v-for="item in list" :key="item.inboxId"
@click="$router.push({ name: 'comment-detail', query: { inboxId: item.inboxId } })">
<div class="normal-title text-start balck-text-color f-b ellipsis-one">{{ item.inboxTitle }}</div>
<div class="d-flex justify-content-between">
<div class="content">{{ item.stockName ? item.stockName : '' }}</div>
<div class="content">{{ item.createTime }}</div>
</div>
</van-cell>
</van-list>
</van-pull-refresh>
<van-empty v-else />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { getWeek } from '@/utils/index'
import moment from 'moment'
import { listLoadAndRefresh } from '@/mixins/list-load-and-refresh'
import { scrollList } from '@/mixins/scroll-list'
import { onBeforeRouteLeave } from 'vue-router'
let year = ref(moment().format('YYYY'))
let month1 = ref(moment().format('MMM'))
let month2 = ref(moment().format('M') + '月')
let week = ref(getWeek(moment().format('d')))
let day = ref(moment().format('DD'))
let pageContent = ref(null)
const scrollPosition = ref(0)
const refresh = () => {
onRefresh()
}
import { workInboxList } from '@/utils/api'
const getData = async () => {
const { data } = await workInboxList({
curPage: curPage.value,
limit: 20,
inboxType: 4
})
return data
}
const { refreshing, finished, loading, curPage, list, onLoad, onRefresh } = listLoadAndRefresh(getData, 'comment')
const { setScrollTop, setScrollPositionAndRefreshFlag } = scrollList(pageContent, scrollPosition, refresh)
setScrollTop()
onBeforeRouteLeave((to, from) => {
setScrollPositionAndRefreshFlag(to, 'comment-detail')
})
</script>
<style lang='scss' scoped>
.page-content {
.head {
position: relative;
.bg {
width: 100%;
height: 220px;
}
.headDiv {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
color: #fff;
background: #171b2eb3;
.headTop {
.day {
font-size: 60px;
}
.month1 {
font-size: 26px;
}
.logo {
height: 57px;
width: 132px;
border: 2px solid #fff;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,11 @@
<template>
<div class="page-content">
<van-empty description="404" image="network">
<van-button size="mini" type="primary" plain @click="$router.go(-1)" class="bottom-button">返回上一页</van-button>
<van-button size="mini" type="success" @click="$router.push({ name: 'home' })"
class="bottom-button">进入首页</van-button>
</van-empty>
</div>
</template>
<script setup lang="ts"></script>
<style lang="scss"></style>

View File

@@ -0,0 +1,195 @@
<template>
<div>
<div class="loading-page">
<div>
<div class="grid">
<div class="item-animation item-animation1">
<img
src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCABOAE4DASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD8jP8AgoJ/yfd+26f+ruf2i/8A1bXjcmvj9OEUegr69/4KBnH7df7bmP8Ao7r9pDHf/mrfxDr5Fr/Q/gWl9Y4VyWpdr2WX0KNremui7ee5+R5pFPFVJX118+i6/JdAooor7E8sKKKKACiiigAooooA+uf+CgX/ACfV+25/2dz+0hj/AMO18Qq+Rq+uf+CgX/J9X7bn/Z3P7SGP/DtfEKvkavlfD3/kk8q/7F+G/FQv9/U9bM/94q/P8kFFFFfWT+J/L8keCFFFFSAUUUUFRjKXw/f0t/X6BRSbh69P0+n4+n9aVgyAMykK33T6/hUykox53s/61+86eSV+Va+l2n6W37H1z/wUC/5Pq/bc/wCzuf2kMf8Ah2viFXyNX1z/AMFAv+T6v23P+zuf2kMf+Ha+IVfI1fL+Hv8AySeVf9i/DflA9PM/94q/P8kFFFFfWT+J/L8keCFFFISByakBakKpjjtz87GP/P5VGeOv93d+H+NbGh6BqniO/j07SLYXdxJ/D/cP4e+P/wBVeLnef5TkOCqY/NMUqFGjd7pW7vz9PmtbH0/CvB+fcZ5pSyrIcvxWOxddpf7NGTs20ldJXS1/rdZI2YUMvnSfwr/q404P/Lbjzv07e+PVfCvwi8VeI7U6hdCHTbCVEazOpZg83PeCMHhAu7nHpivbPBXwh0rw8IdQ1lYtS1hfLf7LN+8s7KT/AKYw/wDPfP5dOTXsG+TAwqZT5BHLb/aEROo8uE5NuOmU4zX8GeMv0uKGBqVco4RvXqU5wpuurO6hKLe7s9I/8u9P0/2X+jD+zVrY/CYXiTxTp4iPt6EauHylx5nH2lPlUnFa07XTaqau1vZ7TPFf+CgX/J9X7bn/AGdz+0hj/wAO18Qq+Rq+uf8AgoF/yfV+25/2dz+0hj/w7XxCr5FJA5Nf3H4ev/jE8q1/5l+GX4QuvzP8X8z/AN4q/P8AJC0UAEjIBx/jQflCs3yq/wB33+gr6p2XtZtrS1tfxvf+vuPChCdSyppt6W5Vd72Vl66eQhIHJpRkHgf+O+Z9P89+3StrQtC1TxDfx2Gl2v2qZvvLj92nf/Pp29a+uPBXwb0zw0tvqOq7dU1LbvX7c3/Er0qTsIbP/ltPjr3x+OfwfxT8cuGeActxKqYpV8xoL9/gMO1e6tZ+2Xpr31fp/X/0efoeeI/jpmuEeFwssr4fbTxOJxEWnXV021dLpqeH+CPhBq3iAW2oa0ZNF0eRt8fmR+bf6lx/zx/5c7Hp/pPpk19WaLoOj+G7SOz0e0jhWNf3k7RxeZqX/TX9/wD8v3X/AImH46PWoVCHajLt4T92vlx9Mev4elWIVBaPJ4xv9/8A9eT3r/L3xR8f+KPETFVMNDFSwWXapYfDtq97fxnp5f8ADH/Qv9Hf6H/h94FZXQdLJsLmXEFsP9ZzfE4aMrPS/sLq6336eQRQvIxluB/uxnoo/Lj8KuBVUkgY3HJ+tOor8HScv3lVt1nu229Hbvfo9Xc/salCNGKVOySSSsklbpZbHz//AMFAc/8ADdP7bZP/AEdz+0f/AOra+IX9c18j5xz6c/lX1x/wUBGf26/22/8As7j9pA/l8VPiEa+U7C0k1C6htYmRHlb5Xk3EJ/3yCent/wDW/wB+OD8ywuT8DYHMMa5rDUMLQuqcXOT91NpRSf4K2up/xIUMqx3EGf4bJcpoUq+YYrFLC0oVakMNTqSuk60qkmoxe+kn8mVYx8w2n5t//Hu3+r7enb9e3sPbvAvwk1bxIY77WYm03R5GjfbNH5dxc/8AXGL/AJY/nn3r1rwD8I9D0eC31fVRHq+osAyeYpFrb8fwxHb5/wD212V7Cz+XEkq5+zj5UiJ+ZV9M8j9f5V/Evjj9K3E4L22TcG06tBK6liqsJ061a7Sd3NR5eyv8z/aD6Kf7OfA4iGX8YeJFXBYqm/q2Iw+VYepCvTTdn+/lTco1ddNX/wAHO0PQtG8N2UdrolvDawwrsaHb+8eTj9753/Lacdv09a0mYzZVj8uOVB/yf/rVErBxkDHTAA4/+tx+tL3x9f0x/jX+fee8UZzxFjMTjs1x+IrVcZdu8r21X5X8v1P9mOFeDuGuCsBgcu4cyzB5ZRwcY4e+Hw8Y3SSXq7oNoX5R0H/66UsTwTSv9446f/WptfLrT+v8vQ+55ufSVWXS2n/A0/r5lFFFTzx7/g/8hn//2Q==" />
</div>
<div class="item-animation item-animation2">
<img
src="data:image/jpeg;base64,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" />
</div>
<div class="item-animation item-animation3">
<img
src="data:image/jpeg;base64,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" />
</div>
<div class="item-animation item-animation4">
<img
src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCABPAE4DASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD8if2/yP8Ahuv9tsf9Xb/tHj65+MHjiP8A9FyOv456gEeU/Bd/C8Ws39z4jv7azuoLWP8AsiG4mNut5dy+fxG/2W9iUJsgyJVZf3uCOMV6n/wUA/5Pu/bcBHP/AA1x+0hznoP+FyeNscfSvkVRuiKdN5+VxkFPu5KkYKngHKkEYBBr+36WQVOKPDXC5FHEVML9ay+hy1qU3TnGTVm1KLTTemq7nieH/HFLw78Q8p4sr5ZTzdZZNTlSq2cOZNO9pK19NXo/VM/SNda025xs1jT3XBUKk9oFmGBxIsSxK/1cMe2fWKTVdLh/12qWITaF2GaExYPbG/g+/BNfnPHLNGAFnmwOmZZDj6bmP+Oeae1xO33ppW9mdmH5EkfpX8v1/oXYXE1/rFfiByqO/vzoylNXafxSpdP8Vn8z/ULC/tW8wwlCjQocFYOlTowhCEaeIUIxUVFRUYxqpRSvolaz/D9Ev+Eg0Ij5dZ0UDOMfa4lH/jxP1/rT/wC1NEJ51fRzx21S3P8AJsfyr88C+eCM/U5/mDUeIx0jUf5+lYf8SRZO7c3EUlbZey0+y9O3X5fI61+1pztbcI4VNtS0xX2vd1v7bfz6389P0Im8SeGoRmbxBo9u38QutRhhDf8AXIgSg/j196rP418HRLvbxd4aZT90Lrmku7fWJbz7QPwt6+AFWJQQIl59cnH+6Tyv4EVWLNncCQDjhPlP1BBBH4f/AF66v+JH+H5RpL+2qjejbS1e2/Xo15nmVv2sfFkqnNT4awTimtI4iX93rff166XV0forYa/oeqy+VpeqWGoyMcCKyuVupB9BExz+p/rrOMMRgD2Bz+tfnHpupX2k3K3lhcm3uY+YphHFIkR/6ehMjqOvpX6F+HtQXWvDej63GskP9o2sM7xsQ7RSPGGZDwe/PtjAwMV/Lfjx9H2fhZ9VzDD1p18sxVath1Gm3Kv7SHsuSSjL3eWSbellq9nv/oN9Dn6ZdH6ReOzHKcXl8stzTA4RYitTnKnOm0rO9Ne1dvkk9O1zx7/goF/yfr+26vb/AIa4/aQwP+6yeMu/XoW/P6Y+RwMcCvrf/goH/wAn8ftt/wDZ2/7SX/q5PGFfJFf618Be9wvkl9bZdh7X17H/AC75zpiqltLp3t1slv3Ciiivuo04uKvr2vZ2+/8A4B4yhFpPr36r/htnfUdvb1/Qf4Ub29f0H+FNop+zi9/0/wAheyh2X3L/AC8l9w7e3r+g/wAKYcnocH6D+VLRVRpQ5lovLRaemnkNU4rbT0sv0GuSqcHjHzjAxJ/vjo3/AALP1r9Afh0zL4G8OqpAA061YfKccm4Q+g+7Gg9eMnkmvz8lAKNnsM/jX6E/Dcf8UZ4dU8j+ybfAJPG24vFHOc8Af5PNfw99NuCfBOBdklDHUFG32b+wuo9k76pWuf64fsnYr/iI2fNbvLq0b315YqdlddNtPTpY8x/4KB/8n8ftt/8AZ2/7SX/q5PGFfJFfW/8AwUCB/wCG+P23Dnj/AIa4/aQ49AfjJ41/PO1fp+dfJFf1FwH/AMktkn/Yuw/5H+VGdf71P0l+SCiiivuYt2Wr2XXyPIh8K+f5sKKKKd33f3soKKKKqLfMtX977AMk+430r9Avhs7HwZ4cOTzpEHUAH/j6vfavz9k+430r9APht/yJnhz/ALBFv/6U3tfxL9Nn/kisIun1+H4PD2+7p2P9bP2UH/Jw8+/7F9f/ANvPOv8AgoFn/hvH9tt+/wDw1z+0eM/T4y+OuMdP4V7dvrXyMOQPoK+uf+CgJ/4zv/bbHb/hrr9pA/8AmZfHgr5EQkopPXFf0/wL/wAkvkv/AGL6H5H+VOcfx6j68qe3oPooor7eOy9F+R48PhXz/NhRRRTKCiiihtpNrRpO33Ey+F/11EIB4NffnwzJPgnw2TyTo9vn/wACr2vgSvvr4Zf8iT4a/wCwPb/+lV7j86/h36a1ST4NwCeq+tYd/OXsbve2v+Vz/W/9lLp4hZ+1o/7On07xlf7+p55/wUB/5Pw/bb/7O5/aQ/8AVy+PK+RY/uL9K+uv+CgP/J+H7bf/AGdz+0h/6uXx5XyLH9xfpX9VcC/8kvkv/Yvofkf5UZv/ABqn+FfoPooor7eOy9F+R48PhXz/ADYUUUUygooopS2fo/yJn8L+X5oUckfUV99fDAhvBfhgMMj+xIcgMRyJ5SDxju78e/sK+BV6j6j+dffPwvP/ABRvhoYxjRov/RzV/Dn01f8Aki8E+qxGF/8AcR/rd+ym/wCTgZ5/2LZf+kVD/9k=">
</div>
<div class="item-animation item-animation5">
<img
src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCABPAE8DASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD8/V+6Px/madSKflUcYGcHnnnqc9z37Z6cUtfwMk4pRakmkk1JJSTS2klon3S0T20P+yhapenZL8E2l6Jtdmwoooof2/8AAvzYpbP0f5Dk/wBT/wBs7r9EjYfqoP4elfmZ0jP1ev0zT/Un/rnd/wDotK/Mw/cP1ev9FfoK7Z3/ANe4/wDpVE/wv/a5/wALgn1q/wDpuqIn3F/3V/kKdTU+4v8Aur/IU6v9IofCvn+bP8LofBH/AAx/JBRRRVPRc3Rfovx2/wA9y0m2l1ey9f8Ahz9MWG2R0HRTx178nk89fUnHQcUlPkA3Fu7Elj6n+Q/DFMr/AJyZq0pLzP8Auao/waf+CP5BRRRUP7f+BfmzSWz9H+Q5P9Sf+ud3/wCi0r8zD9w/V6/TNP8AUn/rnd/+i0r8zD9w/V6/0V+grtnf/XuP/pVE/wAL/wBrn/C4J9av/puqIn3F/wB1f5CnU1PuL/ur/IU6v9IofCvn+bP8LofBH/DH8kFFFFXL4ZLpyJ/O0dfUab5oPrz27aXa/BJfcfpkxJCk+/8AOm04/dX/AIF/Om1/zkz+L5f5n/c3Q/hQ/wAMf/SUFFFFZv7f+BfmzWWz9H+Q5P8AUn/rnd/+i0r8zD9w/V6/TNP9V/2zuv1EK/yJ/nX5mDmLPfLfqDX+iv0Fds7/AOvcf/SqJ/hf+1z/AIXBPrV/9N1RE+4v+6v8hTqRfur9B/Klr/SKHwr5/mz/AAuh8Ef8MfyQUUUVctpf4F+US1vD/G/yv/Xz7n6ZNwcdh0/Hk02nP94/h/IU2v8AnIk7v5LTtpsf9zOH/hQ/wx/9JiFFFFQ/t/4F+bNZbP0f5Dk/1X/bO5/9oV+Zg/1P4t/I1+maf6nP/TO7/wDQIz/MCvzM6Rkdsv8ApX+in0Fn/wAj/wAoK33Rf5pP5Lsj/C/9rn/C4J9av/puqC9B9B/Klpq/dX/dH8qdX+j9Nv3dXt38j/Cq7UYWuvdXXyX5fqFFFFa1nalNrR81r+V0rehth/elDm1/ePfXrbr5H6XqSY0Y8kg5Prg4Ht0paRP9VH9D/Olr/nJe/wAl+SP+5yj/AAof4V+QUUUVL+3/AIF+bLls/R/kOT/Un/rnd/8AotK/Mw/cP1ev0zT/AFJ/653f/otK/Mw/cP1ev9FPoLb5/wD4F+SP8Mv2uX8LgjTrV/Kp/XzET7i/7q/yFOpqfcX/AHV/kKdX+j1PePp+h/hRLaPp/kFFFFbV/wCFP/GvzRrhfjh/18f5n//Z">
</div>
<div class="item-animation item-animation6">
<img
src="data:image/jpeg;base64,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">
</div>
<div class="item-animation item-animation7">
<img
src="data:image/jpeg;base64,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">
</div>
<div class="item-animation item-animation8">
<img
src="data:image/jpeg;base64,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">
</div>
<div class="item-animation item-animation9">
<img
src="data:image/jpeg;base64,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">
</div>
</div>
<div class="normal-title f-b mb-5 mt-5">第一上海證券有限公司/第一上海期貸有限公司</div>
<div class="sub-title mb-5">FIRST SHANGHAI SECURITIES LIMITED</div>
<div class="tip mb-5">香港中環德輔道中 71 號永安集團大廈 19 樓D</div>
<div class="tip mb-5">19/F, Wing On House, 71 Des Voeux Road Central, Hong Kong</div>
<div class="tip mb-5">電話 Tel:8522532 1580 傳真 Fax:8522537 6911</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
import userInfoStore from '@/stores/userInfo'
const userInfo = userInfoStore()
const getCode = () => {
const code = getUrlParam('code') // 截取路径中的code如果没有就去微信授权如果已经获取到了就直接传code给后台获取openId
const local = window.location.href
if (code == null || code === '') {
window.location.href = 'https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx1d52f91f259a691f&redirect_uri=' + encodeURIComponent(local) + '&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect'
} else {
userInfo.code = code
window.location.href = 'http://h5.szzztec.com/login'
}
}
const getUrlParam = (name: string) => {
var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)')
var r = window.location.search.substr(1).match(reg)
if (r != null) return unescape(r[2])
return null
}
getCode()
</script>
<style lang='scss' scoped>
.loading-page {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
max-width: 678px;
height: 100vh;
margin: 0 auto;
.grid {
height: 120px;
width: 120px;
margin: 0 auto;
.item-animation {
width: 33%;
height: 33%;
float: left;
-webkit-animation: itemFlicker 1.3s infinite ease-in-out;
animation: itemFlicker 1.3s infinite ease-in-out;
background-size: cover;
img {
width: 100%;
}
}
.item-animation1 {
-webkit-animation-delay: 0.2s;
animation-delay: 0.2s;
}
.item-animation2 {
-webkit-animation-delay: 0.3s;
animation-delay: 0.3s;
}
.item-animation3 {
-webkit-animation-delay: 0.4s;
animation-delay: 0.4s;
}
.item-animation4 {
-webkit-animation-delay: 0.1s;
animation-delay: 0.1s;
}
.item-animation5 {
-webkit-animation-delay: 0.2s;
animation-delay: 0.2s;
}
.item-animation6 {
-webkit-animation-delay: 0.3s;
animation-delay: 0.3s;
}
.item-animation7 {
-webkit-animation-delay: 0;
animation-delay: 0;
}
.item-animation8 {
-webkit-animation-delay: 0.1s;
animation-delay: 0.1s;
}
.item-animation9 {
-webkit-animation-delay: 0.2s;
animation-delay: 0.2s;
}
}
.text1 {
font-size: 18px;
margin-top: 20px;
font-weight: bold;
}
.text2 {
font-size: 14px;
}
.text3 {
font-size: 12px;
}
}
@-webkit-keyframes itemFlicker {
0%,
70%,
100% {
-webkit-transform: scale3D(1, 1, 1);
transform: scale3D(1, 1, 1);
}
35% {
-webkit-transform: scale3D(0, 0, 1);
transform: scale3D(0, 0, 1);
}
}
@keyframes itemFlicker {
0%,
70%,
100% {
-webkit-transform: scale3D(1, 1, 1);
transform: scale3D(1, 1, 1);
}
35% {
-webkit-transform: scale3D(0, 0, 1);
transform: scale3D(0, 0, 1);
}
}
</style>

112
src/views/common/log.vue Normal file
View File

@@ -0,0 +1,112 @@
<template>
<div class="page-content">
<div class="title">請求詳情</div>
<div class="request">
<div class="request-detail">
<div class="name">note</div>
<div class="content">{{ logData.note }}</div>
</div>
<div class="request-detail">
<div class="name">userName</div>
<div class="content">{{ logData.userName }}</div>
</div>
</div>
<div class="title mt-16">日志详情</div>
<div class="card mt-16" v-for="item in logList" :key="item.id">
<div class="d-flex pb-16 border-bottom-1 justify-content-between">
<div>类型<span :style="{ color: item.logLevelColor }">{{ item.logLevelName }}</span></div>
<div class="create-time">创建时间{{ item.createTime }}</div>
</div>
<div class="d-flex mt-16">
详情
<div class="note flex-1" :class="!item.showAll ? 'text-ellipsis' : ''">{{ item.note }}
<div style="margin-top: 4px;" v-if="item.logLevel === 40000">错误信息{{ item.errorMsg }}</div>
</div>
<span class="activeColor" @click="item.showAll = !item.showAll">{{ item.showAll ? '收起' : '展开' }}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { type IUserLog } from '@/utils/types'
const logData = reactive<IUserLog>({
note: '',
userName: ''
})
const logList = ref<any[]>([])
import emitter from '@/utils/mitt'
import { sysLog } from '@/utils/api'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const requestId = ref()
const init = () => {
if (route.query && route.query.requestId) {
requestId.value = route.query.requestId
getData()
} else {
router.push({ name: '404' })
}
}
const getData = async () => {
emitter.emit('showLoading', '')
const { data } = await sysLog(requestId.value)
logData.note = data.sysUserLog.note
logData.userName = data.sysUserLog.userName
logList.value = data.sysUserLogDetail.map((ele: any) => {
ele.showAll = false
return ele
})
emitter.emit('hiddenLoading', '')
}
init()
</script>
<style lang="scss" scoped>
.page-content {
padding: 10px;
font-size: 13px;
}
.pb-10{
padding-bottom: 10px;
}
table {
width: 100vw;
overflow: auto;
}
.create-time {
font-size: 12px;
color: #909090;
}
.title {
font-weight: bold;
text-align: center;
padding: 4px 0;
background-color: #d9d9d9;
}
.request {
border: 1px solid #e2e2e2;
.request-detail {
display: flex;
border-bottom: 1px solid #e2e2e2;
.name {
width: 100px;
padding: 10px;
text-align: center;
border-right: 1px solid #e2e2e2;
}
.content {
word-break: break-all;
padding: 10px;
flex: 1;
text-align: center;
color: #1989fa;
}
}
}
.note {
word-break: break-all;
margin-right: 4px;
}
</style>

182
src/views/common/login.vue Normal file
View File

@@ -0,0 +1,182 @@
<template>
<div class="page-div">
<div class="form-container d-flex align-center">
<div class="form">
<div class="text-center pt-20">
<van-image height="100" width="100" src="/img/smallLogo.jpg" round />
</div>
<div class="text-center">
<h2 class="big-title f-b mt-16">第一上海</h2>
<div class="sub-title mt-16">欢迎登录数据管理系统</div>
</div>
<van-form class="control-area" @submit="onSubmit" validate-trigger="onSubmit">
<van-field v-model="username" name="username" placeholder="请输入您的账户" label="账户"
:rules="[{ required: true, message: '请输入您的账户' }]" autocomplete="new-password" />
<van-field v-model="password" name="password" placeholder="请输入您的密码" label="密码"
:rules="[{ required: true, message: '请输入您的密码' }]" type="password" autocomplete="new-password" />
<div class="opr-area">
<van-button round block type="primary" :disabled="submitFlag" native-type="submit">
确定
</van-button>
</div>
</van-form>
</div>
</div>
<div class="company-info">
<div class="text-center mt-16 mb-16">
<div class="mb-16 normal-title">第一上海证券有限公司</div>
<div class="mb-8 sub-title">FIRST SHANGHAI SECURITIES LIMITED</div>
<div class="mb-5 tip">香港中環德輔道中 71 號永安集團大廈 19 樓D</div>
<div class="mb-5 tip">19/F, Wing On House, 71 Des Voeux Road Central, Hong Kong</div>
<div class="mb-5 tip">电话 Tel:8522532 1580 传真 Fax:8522537 6911</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import userInfoStore from '@/stores/userInfo'
const userInfo = userInfoStore()
const router = useRouter()
const route = useRoute()
let username = ref(userInfo.username || '')
let password = ref(userInfo.password || '')
let submitFlag = ref(false)
import { encrypt } from '@/utils/index'
import { sysLogin, sysUserInfo, sysMenuNav } from '@/utils/api'
const onSubmit = async () => {
submitFlag.value = true
const data: any = await sysLogin(
{
username: username.value,
password: encrypt(password.value),
}
)
if (data && data.code === 0) {
userInfo.username = username.value
userInfo.password = password.value
userInfo.token = data.token
getUserInfo()
} else {
submitFlag.value = false
showToast('账号密码错误')
}
}
const getUserInfo = async () => {
const data: any = await sysUserInfo()
userInfo.id = data.user.userId
userInfo.realname = data.user.realname
userInfo.headPic = data.user.headPic
userInfo.roleId = data.user.roleId + ''
getsysMenuNav()
}
const getsysMenuNav = async () => {
const data: any = await sysMenuNav()
userInfo.permissions = data.permissions
let redirect = route.query.redirect || '/'
if (typeof redirect !== 'string') {
redirect = '/'
}
router.replace(redirect)
}
</script>
<style lang='scss' scoped>
.form-container {
flex: 1;
height: 100%;
.form {
width: 100%;
background-color: #fff;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
padding: 16px;
margin: 16px;
@media screen and (min-width: 768px) {
padding: 40px;
margin: 40px;
}
.pt-20 {
padding-top: 20px;
@media screen and (min-width: 768px) {
padding-top: 30px;
}
}
.control-area {
margin: 32px auto;
padding: 0 16px;
width: 100%;
@media screen and (min-width: 768px) {
width: 100%;
margin: 40px auto;
}
.van-cell {
padding: 16px;
font-size: 16px;
margin: 0 0 16px;
width: 100%;
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, .1);
@media screen and (min-width: 768px) {
padding: 20px;
font-size: 18px;
margin-bottom: 20px;
}
&:last-child {
margin-bottom: 0;
}
:deep(.van-field__label) {
padding-right: 16px;
label {
max-width: 5em;
margin-right: 8px;
}
}
}
.opr-area {
padding: 16px 0;
.van-button {
height: 32px;
line-height: 32px;
@media screen and (min-width: 768px) {
height: 40px;
line-height: 40px;
font-size: 16px;
}
}
.van-button--disabled {
color: rgba(0, 0, 0, 0.15);
background-color: rgba(0, 0, 0, 0.05);
height: 32px;
line-height: 32px;
}
}
}
}
}
.company-info {
margin-top: 30px;
width: 100%;
padding: 20px;
@media screen and (min-width: 768px) {
padding: 30px;
}
}
</style>

View File

@@ -0,0 +1,53 @@
<template>
<div class="page-content">
<van-list class="pt-10">
<van-cell v-for="item in subscriptionList" :key="item.id" :title="item.subscriptionItemName">
<template #value>
<van-switch size="16" :disabled="item.disabled" v-model="item.isSubscription" @change="changeSub(item)" />
</template>
</van-cell>
</van-list>
</div>
</template>
<script setup lang="ts">
import { getSubscriptionList, delSubscription, saveSubscription } from '@/utils/api'
import { ref } from 'vue'
const subscriptionList = ref()
const getSubList = async () => {
const { data } = await getSubscriptionList()
subscriptionList.value = data.map((item: any) => {
return {
...item,
disabled: false,
isSubscription: !!item.userId
}
})
}
getSubList()
const changeSub = async (item: any) => {
item.disabled = true
if (!item.isSubscription) {
await delSubscription(item.id)
} else {
await saveSubscription(item.subscriptionItem)
}
await getSubList()
item.disabled = false
}
</script>
<style lang="scss" scoped>
.pt-10 {
padding-top: 10px;
}
.van-cell {
margin: 12px 16px 0;
width: calc(100vw - 32px);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
@media screen and (min-width: 678px) {
width: 646px;
}
}
</style>

View File

@@ -0,0 +1,449 @@
<template>
<div class="page-content">
<div class="add-cusUser-detail detailClass">
<van-form @submit="onSubmit" validate-trigger="onSubmit">
<van-field name="cusName" v-model="popObj.cusName" label="机构名" placeholder="请输入机构名" :required="true"
:rules="[{ required: true, message: '请填写机构名' }]" />
<van-field clickable name="cusLevelName" v-model="popObj.cusLevelName" label="机构评级" placeholder="点击选择机构评级"
@click="popObj.handleShowPop1('cusLevel', '机构评级')" readonly />
<van-popup v-model:show="popObj.showPop1" position="bottom">
<van-picker show-toolbar :title="popObj.popTitle" :columns="popObj[`${popObj.type}List`]"
@confirm="popObj.onConfirm1($event)" @cancel="popObj.showPop1 = false" v-model="popObj.value1" />
</van-popup>
<van-field clickable name="cusUserName" v-model="popObj.cusUserName" label="核心联系人" placeholder="点击选择机构"
@click="popObj.handleShowPop2('cusUser', '核心联系人')" readonly />
<van-popup v-model:show="popObj.showPop2" position="bottom">
<div class="d-flex align-items-center">
<van-field v-model="popObj.searchValue" :placeholder="'请输入' + popObj.popTitle"
@input="popObj.onSearchInput" />
<van-button block type="success" size="small" native-type="button" @click="toAddCusUser"
style="width: 60px; margin: 12px 16px 0 0">
新增
</van-button>
</div>
<van-picker show-toolbar :title="popObj.popTitle" :columns="popObj.showPop2List"
@confirm="popObj.onConfirm2($event)" @cancel="popObj.showPop2 = false" />
</van-popup>
<van-field clickable name="companyTypeName" v-model="popObj.companyTypeName" label="机构类型" placeholder="点击选择机构类型"
@click="popObj.handleShowPop1('companyType', '机构类型')" readonly />
<van-field clickable name="cityName" v-model="popObj.cityName" label="城市" placeholder="点击选择城市"
@click="popObj.handleShowPop1('city', '城市')" readonly />
<van-field name="address" v-model="popObj.address" label="地址" placeholder="请输入地址" />
<van-field name="radio" label="是否开户">
<template #input>
<van-radio-group v-model="popObj.accountStatus" direction="horizontal">
<van-radio name="0"></van-radio>
<van-radio name="1"></van-radio>
</van-radio-group>
</template>
</van-field>
<van-field v-show="popObj.accountStatus === '1'" clickable name="accountTime" v-model="popObj.accountTime"
label="开户时间" placeholder="点击选择日期" @click="popObj.handleShowPop4('accountTime', '开户时间')" readonly />
<van-popup v-model:show="popObj.showPop4" position="bottom">
<van-date-picker v-model="popObj.value4" :title="popObj.popTitle" @confirm="popObj.onConfirm4($event)"
@cancel="popObj.showPop4 = false" />
</van-popup>
<van-field v-show="popObj.accountStatus === '1'" clickable name="accountTypeNames"
v-model="popObj.accountTypeNames" label="开户类型" placeholder="点击选择开户类型"
@click="popObj.handleShowPop3('accountType', '开户类型')" readonly />
<van-popup v-model:show="popObj.showPop3" position="bottom">
<div class="p-16 box-popup">
<van-row class="mb-16">
<van-col span="8">
<van-button class="van-picker__cancel" native-type="button"
@click="popObj.showPop3 = false">取消</van-button>
</van-col>
<van-col span="8" class="text-center">
<span class="sub-title f-b">请选择{{ popObj.popTitle }}</span>
</van-col>
<van-col span="8" class="text-end">
<van-button class="van-picker__confirm" native-type="button" @click="popObj.onConfirm3">确认</van-button>
</van-col>
</van-row>
<van-checkbox-group v-model="popObj.value3" direction="horizontal">
<van-checkbox v-for="(item, index) in popObj[`${popObj.type}List`]" :key="index" :name="item.value"
shape="square">
{{ item.text }}
</van-checkbox>
</van-checkbox-group>
</div>
</van-popup>
<van-field v-show="popObj.accountStatus === '1'" name="radio" label="是否交易">
<template #input>
<van-radio-group v-model="popObj.isMoney" direction="horizontal">
<van-radio name="0"></van-radio>
<van-radio name="1"></van-radio>
</van-radio-group>
</template>
</van-field>
<van-field v-show="popObj.accountStatus === '1' && popObj.isMoney === '1'" name="tradeMoney"
v-model="popObj.tradeMoney" label="交易总金额" placeholder="请输入交易总金额" />
<van-field v-show="popObj.accountStatus === '1' && popObj.isMoney === '1'" clickable name="capitalScaleName"
v-model="popObj.capitalScaleName" label="资金规模(亿)" placeholder="点击选择资金规模"
@click="popObj.handleShowPop1('capitalScale', '资金规模')" readonly />
<van-field v-show="popObj.accountStatus === '1' && popObj.isMoney === '1'" name="capitalScaleValue"
v-model="popObj.capitalScaleValue" label="资金规模(亿)" placeholder="请输入资金规模" @blur="popObj.setAccountStatus" />
<van-field name="fundNum" v-model="popObj.fundNum" label="基金数量" placeholder="请输入基金数量" />
<van-field name="investOverseasPer" v-model="popObj.investOverseasPer" label="海外投资占比" placeholder="请输入海外投资占比" />
<van-field name="investResearchNum" v-model="popObj.investResearchNum" label="投研人数" placeholder="请输入投研人数" />
<van-field clickable name="investChannelName" v-model="popObj.investChannelName" label="投资渠道"
placeholder="点击选择投资渠道" @click="popObj.handleShowPop1('investChannel', '投资渠道')" readonly />
<van-field name="website" v-model="popObj.website" label="公司网站" placeholder="请输入公司网站" />
<van-field clickable name="companyCreateTime" v-model="popObj.companyCreateTime" label="开户时间"
placeholder="点击选择公司创建时间" @click="popObj.handleShowPop4('companyCreateTime', '公司创建时间')" readonly />
<van-field name="introduction" v-model="popObj.introduction" label="公司简介" rows="3" type="textarea" autosize />
<van-field clickable name="saleUserNames" v-model="popObj.saleUserNames" label="负责人" placeholder="点击选择负责人"
@click="popObj.handleShowPop3('saleUser', '负责人')" readonly />
<div class="m-16 d-flex justify-content-center">
<van-button round block type="primary" size="small" native-type="submit"
:disabled="submitFlag">提交</van-button>
</div>
</van-form>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import moment from 'moment'
import emitter from '@/utils/mitt'
import { sysDicListByType, sysUserGetSaleAndManagerList, cusUserLookUp, cusInfoSave } from '@/utils/api'
let submitFlag = ref(false)
import { debounceThrottle } from '@/mixins/debounce-throttle'
const { throttle } = debounceThrottle()
import { type CusObj } from '@/utils/types'
const popObj: CusObj = reactive({
showPop1: false,
showPop2: false,
showPop3: false,
showPop4: false,
popTitle: '',
type: '',
value1: [],
value3: [],
value4: [],
cusName: '',
cusLevelName: '',
selectcusLevel: '',
cusLevelList: [],
cusUserName: '',
selectcusUser: '',
searchValue: '',
showPop2List: [],
companyTypeName: '',
selectcompanyType: '',
companyTypeList: [],
cityName: '',
selectcity: '',
cityList: [],
address: '',
accountStatus: '1',
accountTime: '',
selectaccountTime: [],
accountTypeNames: '',
selectaccountTypeIds: [],
accountTypeList: [],
isMoney: '0',
tradeMoney: '',
capitalScaleName: '',
selectcapitalScale: '',
capitalScaleList: [],
capitalScaleValue: '',
fundNum: '',
investOverseasPer: '',
investResearchNum: '',
investChannelName: '',
selectinvestChannel: '',
investChannelList: [],
website: '',
companyCreateTime: '',
selectcompanyCreateTime: [],
introduction: '',
saleUserNames: '',
selectsaleUserIds: [],
saleUserList: [],
handleShowPop1: (val: string, title: string) => {
popObj.showPop1 = true
popObj.popTitle = title
popObj.type = val
popObj.value1 = [popObj[`select${val}`]]
},
handleShowPop2: (val: string, title: string) => {
popObj.showPop2 = true
popObj.popTitle = title
popObj.type = val
popObj.searchValue = popObj[`${val}Name`]
popObj.showPop2List = popObj[`select${val}`] ? [{ text: popObj[`${val}Name`], value: popObj[`select${val}`] }] : []
},
handleShowPop3: (val: string, title: string) => {
popObj.showPop3 = true
popObj.popTitle = title
popObj.type = val
popObj.value3 = popObj[`select${val}Ids`]
},
handleShowPop4: (val: string, title: string) => {
popObj.popTitle = title
popObj.type = val
if (popObj[`select${val}`].length === 0) {
let nowDay = moment().format('YYYY-MM-DD HH:mm:ss')
popObj.value4 = nowDay.slice(0, 10).split('-')
} else {
popObj.value4 = popObj[`select${val}`]
}
popObj.showPop4 = true
},
onConfirm1: ({ selectedOptions }) => {
if (selectedOptions) {
popObj[`select${popObj.type}`] = selectedOptions[0].value
popObj[`${popObj.type}Name`] = selectedOptions[0].text
if (popObj.type === 'capitalScale') {
if (popObj.capitalScale === '1') {
if (Number(popObj.capitalScaleValue) > 10) {
popObj.capitalScaleValue = ''
}
} else if (popObj.capitalScale === '2') {
if (Number(popObj.capitalScaleValue) > 100 || Number(popObj.capitalScale) < 10) {
popObj.capitalScaleValue = ''
}
} else if (popObj.capitalScale === '3') {
if (Number(popObj.capitalScaleValue) < 100) {
popObj.capitalScaleValue = ''
}
}
}
}
popObj.showPop1 = false
},
onSearchInput: throttle(
async () => {
if (popObj.searchValue) {
const { data } = await cusUserLookUp({ cusUserName: popObj.searchValue })
popObj.showPop2List = data.map((ele: any) => {
ele.text = `${ele.cusUserName}${ele.positionName ? '【' + ele.positionName + '】' : ''}`
ele.value = ele.cusUserId
return ele
})
} else {
popObj.showPop2List = []
}
},
500
),
onConfirm2: ({ selectedOptions }) => {
if (selectedOptions && selectedOptions[0]) {
popObj[`select${popObj.type}`] = selectedOptions[0].value
popObj[`${popObj.type}Name`] = selectedOptions[0].text
popObj.showPop2 = false
} else {
showToast(`请选择${popObj.popTitle}后,再确认`)
}
},
onConfirm3: () => {
popObj[`select${popObj.type}Ids`] = popObj.value3
const sameList = popObj[`${popObj.type}List`].filter((ele: any) => popObj.value3.some((res: any) => {
return ele.value === res
}))
let names = ''
sameList.forEach((ele: any) => {
names += `${ele.text},`
})
if (names) {
names = names.substring(0, names.length - 1)
}
popObj[`${popObj.type}Names`] = names
popObj.showPop3 = false
},
onConfirm4: ({ selectedValues }) => {
if (selectedValues) {
popObj[popObj.type] = `${selectedValues[0]}-${selectedValues[1]}-${selectedValues[2]}`
popObj[`select${popObj.type}`] = selectedValues
}
popObj.showPop4 = false
},
setAccountStatus: () => {
if (Number(popObj.capitalScaleValue) >= 0 && Number(popObj.capitalScaleValue) < 10) {
popObj.selectcapitalScale = '1'
} else if (Number(popObj.capitalScaleValue) >= 10 && Number(popObj.capitalScaleValue) < 100) {
popObj.selectcapitalScale = '2'
} else if (Number(popObj.capitalScaleValue) >= 100) {
popObj.selectcapitalScale = '3'
} else {
popObj.selectcapitalScale = ''
}
let index = popObj.capitalScaleList.findIndex((ele: any) => {
return ele.dicKey === popObj.selectcapitalScale
})
if (index > -1) {
popObj.capitalScaleName = popObj.capitalScaleList[index].dicValue
} else {
popObj.capitalScaleName = ''
}
}
})
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
import saveCusInfoStore from '@/stores/saveCusInfo'
const saveCusInfo: any = saveCusInfoStore()
import saveInfoStore from '@/stores/saveInfo'
const saveInfo: any = saveInfoStore()
let linkId: any = ''
const getSelectList = async () => {
linkId = route.query.linkId
emitter.emit('showLoading', '')
const data1 = await sysDicListByType({ dicType: 'cus_level' })
popObj.cusLevelList = data1.data.map((ele: any) => {
ele.text = ele.dicValue
ele.value = ele.dicKey
return ele
})
const data2 = await sysDicListByType({ dicType: 'sale_company_type' })
popObj.companyTypeList = data2.data.map((ele: any) => {
ele.text = ele.dicValue
ele.value = ele.dicKey
return ele
})
const data3 = await sysDicListByType({ dicType: 'cus_city' })
popObj.cityList = data3.data.map((ele: any) => {
ele.text = ele.dicValue
ele.value = ele.dicKey
return ele
})
const data4 = await sysDicListByType({ dicType: 'account_type' })
popObj.accountTypeList = data4.data.map((ele: any) => {
ele.text = ele.dicValue
ele.value = ele.dicKey
return ele
})
const data5 = await sysDicListByType({ dicType: 'invest_channel' })
popObj.investChannelList = data5.data.map((ele: any) => {
ele.text = ele.dicValue
ele.value = ele.dicKey
return ele
})
const data6 = await sysUserGetSaleAndManagerList()
popObj.saleUserList = data6.data.map((ele: any) => {
ele.value = ele.userId
ele.text = ele.userName
return ele
})
const data7 = await sysDicListByType({ dicType: 'sale_capital_scale' })
popObj.capitalScaleList = data7.data.map((ele: any) => {
ele.text = ele.dicValue
ele.value = ele.dicKey
return ele
})
for (let i in saveCusInfo) {
if (Object.prototype.hasOwnProperty.call(popObj, i)) {
popObj[i] = saveCusInfo[i]
}
}
if (saveInfo.type === 'cusUser') {
popObj.cusUserName = `${saveInfo.name}${saveInfo.positionName ? '【' + saveInfo.positionName + '】' : ''}`
popObj.selectcusUser = saveInfo.id
saveInfo.$reset()
}
emitter.emit('hiddenLoading', '')
}
const handleKeyUp = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
event.preventDefault()
}
}
onMounted(() => {
window.addEventListener('keyup', handleKeyUp)
})
onUnmounted(() => {
window.removeEventListener('keyup', handleKeyUp)
})
const toAddCusUser = () => {
for (let i in saveCusInfo) {
if (Object.prototype.hasOwnProperty.call(popObj, i)) {
saveCusInfo[i] = popObj[i]
}
}
if (linkId) {
router.push({ name: 'add-cusUser', query: { linkId: linkId, from: 'cus' } })
} else {
router.push({ name: 'add-cusUser', query: { linkId: linkId, from: 'cus' } })
}
}
const onSubmit = async () => {
submitFlag.value = true
const res: any = {
cusName: popObj.cusName,
cusLevel: popObj.selectcusLevel || 'E',
cusUserId: popObj.selectcusUser || null,
cusUserIdList: popObj.selectcusUser ? [popObj.selectcusUser] : [],
companyType: popObj.selectcompanyType || null,
city: popObj.selectcity || null,
address: popObj.address,
accountStatus: popObj.accountStatus,
accountTypeList: popObj.accountStatus === '1' && popObj.selectaccountTypeIds.length > 0 ?
popObj.selectaccountTypeIds : null,
accountTime: popObj.accountStatus === '1' && popObj.accountTime ?
popObj.accountTime : null,
tradeMoney: popObj.accountStatus === '1' && (popObj.tradeMoney === '1' || popObj.tradeMoney === '0') ?
popObj.tradeMoney : null,
capitalScaleValue: popObj.capitalScaleValue || null,
capitalScale: popObj.selectcapitalScale || null,
fundNum: popObj.fundNum || null,
investOverseasPer: popObj.investOverseasPer || null,
investResearchNum: popObj.investResearchNum || null,
investChannel: popObj.selectinvestChannel || null,
website: popObj.website || null,
companyCreateTime: popObj.companyCreateTime || null,
introduction: popObj.introduction || null,
saleUserIdList: popObj.selectsaleUserIds || null
}
const data: any = await cusInfoSave(res)
if (data.code === 0) {
showToast('提交成功')
saveInfo.id = data.data.cusId
saveInfo.name = data.data.cusName
saveInfo.type = 'cus'
saveCusInfo.$reset()
router.go(-1)
} else {
showToast(data.msg)
submitFlag.value = false
}
}
getSelectList()
</script>
<style lang='scss' scoped>
.add-cusUser-detail {
height: 100vh;
overflow-y: auto;
}
.detailClass {
.van-cell {
margin: 12px 16px 0;
width: calc(100vw - 32px);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
@media screen and (min-width: 678px) {
width: 646px;
}
}
}
:deep(.van-field__control) {
color: #000 !important;
}
:deep(.van-checkbox) {
margin-right: 0 !important;
width: 50%;
margin-bottom: 5px;
}
</style>

View File

@@ -0,0 +1,222 @@
<template>
<div class="page-content">
<div class="add-cus-detail detailClass">
<van-form @submit="onSubmit" validate-trigger="onSubmit">
<van-field name="cusUserName" v-model="popObj.cusUserName" label="联系人" placeholder="请输入联系人" :required="true"
:rules="[{ required: true, message: '请填写联系人' }]" />
<van-field clickable name="positionName" v-model="popObj.positionName" label="职位" placeholder="点击选择职位"
@click="popObj.handleShowPop1('position', '职位')" readonly :required="true"
:rules="[{ required: true, message: '请填写职位' }]" />
<van-popup v-model:show="popObj.showPop1" position="bottom">
<van-picker show-toolbar :title="popObj.popTitle" :columns="popObj[`${popObj.type}List`]"
@confirm="popObj.onConfirm1($event)" @cancel="popObj.showPop1 = false" v-model="popObj.value1" />
</van-popup>
<van-field clickable name="saleUserNames" v-model="popObj.saleUserNames" label="跟进人" placeholder="点击选择跟进人"
@click="popObj.handleShowPop3('saleUser', '跟进人')" readonly />
<van-popup v-model:show="popObj.showPop3" position="bottom">
<div class="p-16 box-popup">
<van-row class="mb-16">
<van-col span="8">
<van-button class="van-picker__cancel" native-type="button"
@click="popObj.showPop3 = false">取消</van-button>
</van-col>
<van-col span="8" class="text-center">
<span class="sub-title f-b">请选择{{ popObj.popTitle }}</span>
</van-col>
<van-col span="8" class="text-end">
<van-button class="van-picker__confirm" native-type="button" @click="popObj.onConfirm3">确认</van-button>
</van-col>
</van-row>
<van-checkbox-group v-model="popObj.value3" direction="horizontal">
<van-checkbox v-for="(item, index) in popObj[`${popObj.type}List`]" :key="index" :name="item.value"
shape="square">
{{ item.text }}
</van-checkbox>
</van-checkbox-group>
</div>
</van-popup>
<van-field name="phone" v-model="popObj.phone" label="座机号" placeholder="请输入座机号" />
<van-field name="mobile" v-model="popObj.mobile" label="手机号" placeholder="请输入手机号" />
<van-field name="email" v-model="popObj.email" label="邮箱" placeholder="请输入邮箱" />
<van-field name="wxName" v-model="popObj.wxName" label="微信名" placeholder="请输入微信名" />
<van-field name="address" v-model="popObj.address" label="地址" placeholder="请输入地址" />
<van-field name="preference" v-model="popObj.preference" label="擅长" placeholder="请输入擅长" />
<div class="m-16 d-flex justify-content-center">
<van-button round block type="primary" size="small" native-type="submit"
:disabled="submitFlag">提交</van-button>
</div>
</van-form>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import emitter from '@/utils/mitt'
import { cusUserCusPositionList, sysUserGetSaleAndManagerList, cusUserSave } from '@/utils/api'
let submitFlag = ref(false)
import { type CusUserObj } from '@/utils/types'
const popObj: CusUserObj = reactive({
showPop1: false,
showPop3: false,
popTitle: '',
type: '',
value1: [],
value3: [],
cusUserName: '',
positionName: '',
selectposition: '',
positionList: [],
saleUserNames: '',
selectsaleUserIds: [],
saleUserList: [],
selectcusUser: '',
phone: '',
mobile: '',
email: '',
wxName: '',
address: '',
preference: '',
handleShowPop1: (val: string, title: string) => {
popObj.showPop1 = true
popObj.popTitle = title
popObj.type = val
popObj.value1 = [popObj[`select${val}`]]
},
handleShowPop3: (val: string, title: string) => {
popObj.showPop3 = true
popObj.popTitle = title
popObj.type = val
popObj.value3 = popObj[`select${val}Ids`]
},
onConfirm1: ({ selectedOptions }) => {
if (selectedOptions) {
popObj[`select${popObj.type}`] = selectedOptions[0].value
popObj[`${popObj.type}Name`] = selectedOptions[0].text
}
popObj.showPop1 = false
},
onConfirm3: () => {
popObj[`select${popObj.type}Ids`] = popObj.value3
const sameList = popObj[`${popObj.type}List`].filter((ele: any) => popObj.value3.some((res: any) => {
return ele.value === res
}))
let names = ''
sameList.forEach((ele: any) => {
names += `${ele.text},`
})
if (names) {
names = names.substring(0, names.length - 1)
}
popObj[`${popObj.type}Names`] = names
popObj.showPop3 = false
}
})
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
let cusId: any = ''
let linkId: any = ''
let from: any = ''
const getSelectList = async () => {
if (route.query && route.query.from) {
cusId = route.query && route.query.cusId ? route.query.cusId : ''
linkId = route.query && route.query.linkId ? route.query.linkId : ''
from = route.query.from
emitter.emit('showLoading', '')
const data1 = await cusUserCusPositionList()
popObj.positionList = data1.data.map((ele: any) => {
ele.text = ele.dicValue
ele.value = ele.dicKey
return ele
})
const data2 = await sysUserGetSaleAndManagerList()
popObj.saleUserList = data2.data.map((ele: any) => {
ele.value = ele.userId
ele.text = ele.userName
return ele
})
emitter.emit('hiddenLoading', '')
} else {
router.push({ name: '404' })
}
}
const handleKeyUp = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
event.preventDefault()
}
}
onMounted(() => {
window.addEventListener('keyup', handleKeyUp)
})
onUnmounted(() => {
window.removeEventListener('keyup', handleKeyUp)
})
import saveInfoStore from '@/stores/saveInfo'
const saveInfo: any = saveInfoStore()
const onSubmit = async () => {
submitFlag.value = true
const res: any = {
cusUserName: popObj.cusUserName,
positionId: popObj.selectposition || null,
saleUserIdList: popObj.selectsaleUserIds || null,
phone: popObj.phone || null,
mobile: popObj.mobile || null,
email: popObj.email || null,
wxName: popObj.wxName || null,
address: popObj.address || null,
preference: popObj.preference || null
}
if (cusId) {
res.cusId = cusId
}
const data: any = await cusUserSave(res)
if (data.code === 0) {
showToast('提交成功')
saveInfo.id = data.data.cusUserId
saveInfo.name = data.data.cusUserName
saveInfo.positionName = data.data.positionName
saveInfo.type = 'cusUser'
router.go(-1)
} else {
submitFlag.value = false
}
}
getSelectList()
</script>
<style lang='scss' scoped>
.add-cus-detail {
height: 100vh;
overflow-y: auto;
}
.detailClass {
.van-cell {
margin: 12px 16px 0;
width: calc(100vw - 32px);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
@media screen and (min-width: 678px) {
width: 646px;
}
}
}
:deep(.van-field__control) {
color: #000 !important;
}
:deep(.van-checkbox) {
margin-right: 0 !important;
width: 50%;
margin-bottom: 5px;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,254 @@
<template>
<div class="page-content top-select-list" ref="pageContent">
<div v-if="userInfo.roleId === '1' || userInfo.roleId === '9' || userInfo.roleId === '10' || userInfo.roleId === '6'
|| userInfo.roleId === '7' || userInfo.roleId === '16'" class="addDiv success-background-color"
@click="$router.push({ name: 'communicate-detail', query: { addDay: selectDay } })">
<div class="text-center addDiv1">新增</div>
<div class="text-center addDiv2">沟通</div>
</div>
<div class="top-select border-bottom-1">
<van-dropdown-menu>
<van-dropdown-item :title="cusName" ref="itemRef">
<van-field v-model="searchValue" placeholder="请输入机构" @input="onSearchInput"
:right-icon="searchValue ? 'clear' : ''" @click-right-icon.stop="clearSearch" />
<van-picker show-toolbar title="机构列表" :columns="list" @confirm="onConfirm" @cancel="onCancel" />
</van-dropdown-item>
<van-dropdown-item v-model="linkType" :options="linkTypeList" @change="getLinkList" />
<van-dropdown-item v-model="saleId" :options="saleList" @change="getLinkList" />
</van-dropdown-menu>
</div>
<van-calendar title="" :poppable="false" :show-confirm="false" switch-mode="year-month" first-day-of-week="1"
:style="{ height: '410px' }" :formatter="formatter" :default-date="defaultDate" @panel-change="panelChange"
@select="onDateConfirm" />
<communicate-item class="pb-16" :list="list" />
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { scrollList } from '@/mixins/scroll-list'
import { convertToUrl } from '@/utils'
import userInfoStore from '@/stores/userInfo'
const userInfo = userInfoStore()
import { cusLinkInfo, cusLinkList, sysUserGetSaleAndManagerList, cusInfoLookUp } from '@/utils/api'
let cusName = ref('机构')
let cusId = ref('')
let searchValue = ref('')
let itemRef = ref(null)
import { debounceThrottle } from '@/mixins/debounce-throttle'
const { throttle } = debounceThrottle()
const onSearchInput = throttle(
async () => {
if (searchValue.value) {
const { data } = await cusInfoLookUp({ cusName: searchValue.value })
list.value = data.map((ele: any) => {
ele.text = ele.cusName
ele.value = ele.cusId
return ele
})
} else {
list.value = []
}
},
500
)
const clearSearch = () => {
searchValue.value = ''
}
const onConfirm = (e: any) => {
console.log(e)
if (e.selectedValues.length > 0) {
cusName.value = e.selectedOptions[0].cusName
cusId.value = e.selectedOptions[0].cusId
searchValue.value = e.selectedOptions[0].cusName
getLinkList()
}
itemRef.value.toggle()
}
const onCancel = () => {
itemRef.value.toggle()
}
let linkType = ref('31,32,34')
let linkTypeList: any = ref([{
text: '全部类型',
value: '31,32,34'
}, {
text: '上市公司按需会议',
value: '31'
}, {
text: '机构路演',
value: '32'
}, {
text: '机构沟通',
value: '34'
}])
let saleId = ref('')
let saleList: any = ref([])
const getSaleList = async () => {
const { data } = await sysUserGetSaleAndManagerList()
saleList.value = data.map((ele: any) => {
ele.text = ele.userName
ele.value = ele.userId
return ele
})
saleList.value.unshift({ text: '全部负责人', value: '' })
}
getSaleList()
let defaultDate = ref()
import moment from 'moment'
const panelChange = (e: any) => {
dateBegin.value = moment(e.date).startOf('month').format('YYYY-MM-DD')
dateEnd.value = moment(e.date).endOf('month').format('YYYY-MM-DD')
getLinkList()
}
const onDateConfirm = (e: any) => {
defaultDate.value = e
getList()
}
let list: any = ref([])
let selectDay = ref('')
const getList = () => {
list.value = []
selectDay.value = moment(defaultDate.value).format('YYYY-MM-DD')
linkList.value.forEach((ele: any) => {
if (ele.sysDay === selectDay.value) {
list.value = ele.list
}
})
}
const formatter = computed(() => {
if (linkList.value.length === 0) {
return (day: any) => day
}
return (day: any) => {
let time = moment(day.date).format('YYYY-MM-DD')
linkList.value.forEach((ele: any) => {
if (ele.sysDay === time) {
day.topInfo = ele.num
}
})
return day
}
})
const dateBegin = ref()
const dateEnd = ref()
const linkList: any = ref([])
const getLinkList = async () => {
let url = getLinkParams()
const data = await cusLinkList(url)
let map = new Map()
data.forEach((item: any, index: any, arr: any) => {
if (!map.has(item.sysDay)) {
map.set(
item.sysDay,
arr.filter((a: any) => a.sysDay == item.sysDay)
)
}
})
let res = Array.from(map).map((item) => [...item[1]])
linkList.value = []
res.forEach((ele: any) => {
linkList.value.push({ sysDay: ele[0].sysDay, num: ele.length, list: ele })
})
getList()
}
const getLinkParams = () => {
const res = {
dateBegin: dateBegin.value,
dateEnd: dateEnd.value,
limit: 999,
dataStatus: 1,
curPage: 1,
saleId: saleId.value,
linkTypes: linkType.value,
cusId: cusId.value
}
const url = convertToUrl(res)
return url
}
let pageContent = ref(null)
const scrollPosition = ref(0)
const refresh = async (type: any, id: any) => {
if (type === 'delete' || type === 'refresh') {
let dayIndex = linkList.value.findIndex((ele: any) => {
return ele.sysDay === selectDay.value
})
if (dayIndex > -1) {
let index = linkList.value[dayIndex].list.findIndex((ele: any) => {
return Number(ele.linkId) === Number(id)
})
if (type === 'delete') {
if (index > -1) {
linkList.value[dayIndex].list.splice(index, 1)
linkList.value[dayIndex].num -= 1
}
} else {
if (index > -1) {
const { data } = await cusLinkInfo(id)
switch (data.linkTypeMenu) {
case '32':
data.tagColor = 'danger-background-color'
break
case '34':
data.tagColor = 'primary-background-color'
break
case '31':
data.tagColor = 'success-background-color'
break
}
linkList.value[dayIndex].list[index] = data
}
getList()
}
}
} else {
defaultDate.value = new Date()
dateBegin.value = moment().startOf('month').format('YYYY-MM-DD')
dateEnd.value = moment().endOf('month').format('YYYY-MM-DD')
getLinkList()
}
}
const { setScrollTop, setScrollPositionAndRefreshFlag } = scrollList(pageContent, scrollPosition, refresh)
setScrollTop()
import { onBeforeRouteLeave } from 'vue-router'
onBeforeRouteLeave((to, from) => {
setScrollPositionAndRefreshFlag(to, 'communicate-detail')
})
</script>
<style lang='scss' scoped>
:deep(.van-calendar__header-title) {
display: none;
}
:deep(.van-calendar__top-info) {
width: 16px;
height: 16px;
left: calc(100vw / 7 - 20px);
border-radius: 100%;
background-color: #f56c6c;
color: #fff;
@media screen and (min-width: 678px) {
left: 58px;
}
}
:deep(.van-calendar__day) {
border: 1px solid #f2f2f2;
margin-bottom: 0;
}
</style>

View File

@@ -0,0 +1,90 @@
<template>
<div class="page-content">
<div class="tabbar-list" ref="pageContent">
<van-pull-refresh v-model="refreshing" @refresh="onRefresh" v-if="list.length > 0">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad"
:immediate-check="false">
<van-grid :border="false" :clickable="true" :column-num="3">
<van-grid-item v-for="item in list" :key="item.id"
@click="$router.push({ name: 'flow', query: { stockCode: item.stockCode } })">
<div class="itemImage">
<svg class="stockIcon" aria-hidden="true">
<use :xlink:href="`#icon-${item.companyIcon}`"></use>
</svg>
</div>
<div class="itemName">{{ item.stockName }}</div>
</van-grid-item>
</van-grid>
</van-list>
</van-pull-refresh>
<van-empty v-else />
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
import { stockCompanyFlowInfoListCompany } from '@/utils/api'
let pageContent = ref(null)
const scrollPosition = ref(0)
const getData = async () => {
const { data } = await stockCompanyFlowInfoListCompany()
return { list: data, totalCount: data.length }
}
const refresh = () => {
onRefresh()
}
import { listLoadAndRefresh } from '@/mixins/list-load-and-refresh'
const { refreshing, finished, loading, list, onLoad, onRefresh } = listLoadAndRefresh(getData, 'stock')
import { scrollList } from '@/mixins/scroll-list'
const { setScrollTop, setScrollPositionAndRefreshFlag } = scrollList(pageContent, scrollPosition, refresh)
setScrollTop()
onBeforeRouteLeave((to, from) => {
setScrollPositionAndRefreshFlag(to, 'track')
})
</script>
<style lang='scss' scoped>
.tabbar-list {
padding: 0 20px;
}
:deep(.van-grid-item) {
padding-right: 20px;
padding-bottom: 26px;
}
:deep(.van-grid-item:nth-child(3n)) {
padding-right: 0;
}
:deep(.van-grid-item__content) {
padding: 0;
}
.itemImage {
margin-top: 18px;
height: 36px;
.stockIcon {
max-height: 36px;
width: auto;
max-width: 100%;
}
}
.itemName {
height: 20px;
line-height: 20px;
color: #666;
font-size: 12px;
text-align: center;
margin-top: 15px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
width: 100%;
}
</style>

View File

@@ -0,0 +1,47 @@
<template>
<div class="page-content p-10">
<div class="d-flex justify-content-between">
<van-tag type="primary" size="large">{{ flowData.followType }}</van-tag>
<div>{{ flowData.sysDate }}</div>
</div>
<div class="mt-10" v-if="flowData.fileId">
<audio :id="`auto${flowData.id}`" :src="flowData.fileUrl" controls></audio>
</div>
<div class="mt-10">
{{ flowData.introduce }}
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import emitter from '@/utils/mitt'
const route = useRoute()
const router = useRouter()
let id: any = ref('')
let flowData: any = ref({})
const init = () => {
if (route.query && route.query.id) {
id.value = route.query.id
emitter.emit('showLoading', '')
getData()
} else {
router.push({ name: '404' })
}
}
import { stockCompanyFlowInfoInfo } from '@/utils/api'
const getData = async () => {
const { data }: any = await stockCompanyFlowInfoInfo(id.value)
data.playFlag = false
flowData.value = data
emitter.emit('setTitle', { title: data.sysDate + data.stockName + data.flowType, type: 'comment' })
emitter.emit('hiddenLoading', '')
}
init()
</script>
<style lang='scss' scoped>
audio {
height: 36px;
}
</style>

60
src/views/flow/flow.vue Normal file
View File

@@ -0,0 +1,60 @@
<template>
<div class="page-content">
<van-pull-refresh v-model="refreshing" @refresh="onRefresh" v-if="list.length > 0">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad"
:immediate-check="false">
<van-cell v-for="item in list" :key="item.id"
@click="$router.push({ name: 'flow-detail', query: { id: item.id } })">
<div class="text-start"><van-tag type="primary" size="large">{{ item.followType }}</van-tag></div>
<van-text-ellipsis class="text-start mt-10" :content="item.introduce" rows="3" expand-text="展开"
collapse-text="收起" @click-action="showOrHiddenText" />
<div>{{ item.sysDate }}</div>
</van-cell>
</van-list>
</van-pull-refresh>
<van-empty v-else />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router'
let pageContent = ref(null)
const scrollPosition = ref(0)
const route = useRoute()
const router = useRouter()
let stockCode: any = ref('')
const refresh = () => {
if (route.query && route.query.stockCode) {
stockCode.value = route.query.stockCode
onRefresh()
} else {
router.push({ name: '404' })
}
}
import { stockCompanyFlowInfoList } from '@/utils/api'
const getData = async () => {
const { data }: any = await stockCompanyFlowInfoList({
stockCode: stockCode.value,
curPage: curPage.value,
limit: 20
})
data.list.map((ele: any) => {
ele.playFlag = false
})
return data
}
const showOrHiddenText = (event: any) => {
event.stopPropagation()
}
import { listLoadAndRefresh } from '@/mixins/list-load-and-refresh'
const { refreshing, finished, loading, curPage, list, onLoad, onRefresh } = listLoadAndRefresh(getData, 'stock')
import { scrollList } from '@/mixins/scroll-list'
const { setScrollTop, setScrollPositionAndRefreshFlag } = scrollList(pageContent, scrollPosition, refresh)
setScrollTop()
onBeforeRouteLeave((to, from) => {
setScrollPositionAndRefreshFlag(to, 'track-detail')
})
</script>
<style lang='scss' scoped></style>

202
src/views/home/index.vue Normal file
View File

@@ -0,0 +1,202 @@
<template>
<div class="page-content tabbar-list">
<div class="home-top" :class="isNight ? 'night' : ''">
<div class="title">{{ tips }}</div>
<div class="top-content">
<van-image class="banner" src="/img/home-banner.jpg" alt="" />
<div class="name">第一上海</div>
<div class="welcome">数据管理系统欢迎您</div>
</div>
</div>
<van-grid :column-num="4" :border=false>
<van-grid-item v-if="userInfo.roleId === '10' || userInfo.roleId === '1'" class="btn"
@click="$router.push({ name: 'organization-list' })">
<div class="icon-box organization"><van-icon name="newspaper-o" size="24" /></div>
<span>机构</span>
</van-grid-item>
<van-grid-item class="btn" @click="$router.push({ name: 'meeting-list' })">
<div class="icon-box meeting"><van-icon name="calendar-o" size="24" /></div>
<span>会议</span>
</van-grid-item>
<van-grid-item class="btn" @click="$router.push({ name: 'communicate-list' })">
<div class="icon-box communicate"><van-icon name="comment-o" size="24" /></div>
<span>沟通</span>
</van-grid-item>
<van-grid-item class="btn" @click="$router.push({ name: 'comment-list' })">
<div class="icon-box comment"><van-icon name="diamond-o" size="24" /></div>
<span>点评</span>
</van-grid-item>
<van-grid-item class="btn" @click="$router.push({ name: 'report-list' })">
<div class="icon-box report"><van-icon name="description-o" size="24" /></div>
<span>报告</span>
</van-grid-item>
<van-grid-item class="btn" @click="$router.push({ name: 'target-table' })">
<div class="icon-box cash"><van-icon name="guide-o" size="24" /></div>
<span>指标</span>
</van-grid-item>
</van-grid>
<div class="card">
<div class="card-title">今日沟通</div>
<communicate-item :list="communicateList" />
</div>
<tabbar />
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import userInfoStore from '@/stores/userInfo'
const userInfo = userInfoStore()
const time = moment().hour()
const isNight = computed(() => time < 6 || time >= 22)
const tips = computed(() => {
if (time < 6 || time >= 22) {
return '夜深了,早点休息'
} else if (time >= 19) {
return `晚上好,${userInfo.realname}`
} else if (time >= 12) {
return `下午好,${userInfo.realname}`
} else {
return `早上好,${userInfo.realname}`
}
})
import { cusLinkList } from '@/utils/api'
import { convertToUrl } from '@/utils'
import moment from 'moment'
const isLogin = computed(() => !!userInfo.token)
const communicateList = ref([])
const getCusLink = async () => {
if (!isLogin.value) return
const url = convertToUrl({
limit: 3,
curPage: 1,
dateBegin: moment().format('YYYY-MM-DD')
})
const data = await cusLinkList(url)
communicateList.value = data
}
getCusLink()
import { workMeetingList } from '@/utils/api'
const meetingList = ref<Array<any>>([])
const getMeetingList = async () => {
if (!isLogin.value) return
const url = convertToUrl({
dateBegin: moment().format('YYYY-MM-DD'),
dateEnd: moment().format('YYYY-MM-DD'),
meetingType: '1,2,3,16,17,4,5,15,6,10,11,12,13,14,7'
})
const { data } = await workMeetingList(url)
meetingList.value = data.slice(0, 3)
}
getMeetingList()
</script>
<style lang="scss" scoped>
.night {
background-image: url('/img/night.png');
background-repeat: no-repeat;
padding-top: 30px;
color: #fff;
}
.home-top {
padding: 20px 10px 0;
.top-content {
height: 120px;
z-index: 3;
position: relative;
@media screen and (min-width: 678px) {
height: 160px;
}
}
.title {
font-size: 16px;
margin-bottom: 20px;
font-weight: 600;
}
.name {
padding-top: 30px;
font-size: 18px;
color: #fff;
margin-left: 20px;
}
.welcome {
font-size: 14px;
color: #eee;
margin-left: 40px;
margin-top: 10px;
}
.banner {
border-radius: 6px;
width: 100%;
position: absolute;
border-radius: 6px;
overflow: hidden;
z-index: -1;
top: 0;
bottom: 0;
left: 0;
}
}
.btn {
text-align: center;
font-size: 13px;
transition: all 0.2s ease;
&:active {
transform: scale(0.95);
opacity: 0.8;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.icon-box {
width: 36px;
height: 36px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
margin-bottom: 4px;
}
.organization {
background-color: #ff8f00;
}
.meeting {
background-color: #a3cf7d;
}
.communicate {
background-color: #ffc700;
padding-top: 2px;
}
.comment {
background-color: #ff758f;
}
.report {
background-color: #5bb1ff;
}
.cash {
background-color: #f15b6c;
}
}
.card {
padding: 10px 0;
// margin-top: 10px;
margin: 10px 12px;
.card-title {
margin-left: 16px;
padding: 0 4px;
font-weight: 600;
border-left: 4px solid #5bb1ff;
margin-bottom: 10px;
font-size: 16px;
}
}
.empty {
color: #808080;
font-size: 14px;
text-align: center;
}
:deep(.van-empty__image) {
width: 80px;
height: 80px;
}
</style>

View File

@@ -0,0 +1,135 @@
<template>
<div class="page-content">
<div class="tabbar-list top-select-list" ref="pageContent">
<div class="top-select border-bottom-1">
<van-grid :border="false" :gutter="10" :column-num="2">
<van-grid-item>
<top-select1 :type="'speak_type'" :initialName="'类型'" @refresh="refreshBySpeakType"
:selectName="speakTypeName" :selectValue="speakType" />
</van-grid-item>
</van-grid>
</div>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh" v-if="list.length > 0">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad"
:immediate-check="false">
<van-cell v-for="(item, index) in list" :key="item.speakId" @click="showDetail(item, index)"
:id="item.speakId">
<div class="d-flex justify-content-between">
<div class="name1 text-start ">
<van-tag :class="item.tagColor" size="medium">{{ item.speakTypeName }}</van-tag>
</div>
<div class="name1 stockNames balck-text-color">{{ item.sysDate }}</div>
</div>
</van-cell>
</van-list>
</van-pull-refresh>
<van-empty v-else />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick, onUnmounted } from 'vue'
import TopSelect1 from '@/components/top-select1.vue'
import emitter from '@/utils/mitt'
import { workSpeakList } from '@/utils/api'
import speakInfoStore from '@/stores/speakInfo'
const speakInfo = speakInfoStore()
import { listLoadAndRefresh } from '@/mixins/list-load-and-refresh'
const scrollPage = () => {
nextTick(() => {
if (speakInfo.speakId && speakInfo.index) {
speakTypeName.value = speakInfo.speakTypeName
speakType.value = speakInfo.speakType
let pageIndex1 = speakInfo.index / 20
let pageIndex2 = speakInfo.index % 20
if (pageIndex2 > 1) {
pageIndex1++
}
if (pageIndex1 > curPage.value) {
onLoad()
} else {
const el = document.getElementById(speakInfo.speakId)
if (el) {
el.scrollIntoView({ behavior: 'smooth' })
}
speakInfo.speakId = ''
speakInfo.index = 0
emitter.emit('hiddenLoading', '')
}
} else {
emitter.emit('hiddenLoading', '')
}
})
}
const getData = async () => {
const { data } = await workSpeakList({
curPage: curPage.value,
limit: 20,
speakType: speakType.value || null
})
data.list.map((ele: any) => {
switch (ele.speakType) {
case 'CH':
ele.tagColor = 'primary-background-color'
break
case 'CF':
ele.tagColor = 'danger-background-color'
break
case 'YJZH':
ele.tagColor = 'success-background-color'
break
case 'SALE':
ele.tagColor = 'warning-background-color'
break
}
})
return data
}
const { refreshing, finished, loading, list, onLoad, onRefresh, curPage } = listLoadAndRefresh(getData, 'internal', scrollPage)
let speakTypeName = ref('类型')
let speakType = ref('')
const refreshBySpeakType = (e: any) => {
speakType.value = e.value
speakTypeName.value = e.name
onRefresh()
}
import { showFile } from '@/mixins/show-file'
const { loadingFile } = showFile()
const showDetail = (item: any, index: number) => {
speakInfo.index = index
speakInfo.speakId = item.speakId
speakInfo.speakTypeName = speakTypeName.value
speakInfo.speakType = speakType.value
loadingFile(item.tencentRecordUrl)
}
const onRefresh1 = () => {
speakTypeName.value = speakInfo.speakTypeName
speakType.value = speakInfo.speakType
onRefresh()
}
onRefresh1()
onUnmounted(() => {
speakInfo.speakTypeName = '类型'
speakInfo.speakType = ''
})
</script>
<style lang='scss' scoped>
.grid-item-text {
max-width: calc(50vw - 28px) !important;
@media screen and (min-width: 678px) {
max-width: 310px !important;
}
}
</style>

View File

@@ -0,0 +1,683 @@
<template>
<div class="page-content">
<div class="add-meeting" :class="disabledFlag ? 'detailClass2' : 'detailClass1'">
<van-form @submit="onSubmit" validate-trigger="onSubmit">
<van-field name="meetingTitle" v-model="popObj.meetingTitle" label="会议标题"
:placeholder="disabledFlag ? '' : '请输入会议标题'" :required="true"
:rules="[{ required: true, message: '请填写会议标题' }]" :disabled="disabledFlag" />
<van-field clickable name="sysDay" v-model="popObj.sysDay" label="会议日期"
:placeholder="disabledFlag ? '' : '点击选择日期'" @click="popObj.handleShowPop4('sysDay', '选择日期', 4)"
:required="true" :rules="[{ required: true, message: '请选择会议日期' }]" readonly :disabled="disabledFlag" />
<van-popup v-model:show="popObj.showPop4" position="bottom">
<van-date-picker v-model="popObj.value4" :title="popObj.popTitle" @confirm="popObj.onConfirm4($event)"
@cancel="popObj.showPop4 = false" />
</van-popup>
<van-field clickable name="sysTime" v-model="popObj.sysTime" label="会议时间"
:placeholder="disabledFlag ? '' : '点击选择时间'" @click="popObj.handleShowPop4('sysTime', '会议时间', 5)"
:required="true" :rules="[{ required: true, message: '请选择会议时间' }]" readonly :disabled="disabledFlag" />
<van-popup v-model:show="popObj.showPop5" position="bottom">
<van-time-picker v-model="popObj.value5" :title="popObj.popTitle" @confirm="popObj.onConfirm4($event)"
@cancel="popObj.showPop5 = false" />
</van-popup>
<van-field clickable name="meetingTypeName" v-model="popObj.meetingTypeName" label="会议类型"
:placeholder="disabledFlag ? '' : '点击选择会议类型'" @click="popObj.handleShowPop1('meetingType', '会议类型')"
:required="true" :rules="[{ required: true, message: '请选择会议类型' }]" readonly :disabled="disabledFlag" />
<van-popup v-model:show="popObj.showPop1" position="bottom">
<van-picker show-toolbar :title="popObj.popTitle" :columns="popObj[`${popObj.type}List`]"
@confirm="popObj.onConfirm1($event)" @cancel="popObj.showPop1 = false" v-model="popObj.value1" />
</van-popup>
<van-field clickable name="meetingByName" v-model="popObj.meetingByName" label="会议形式"
:placeholder="disabledFlag ? '' : '点击选择会议形式'" @click="popObj.handleShowPop1('meetingBy', '会议形式')"
:required="true" :rules="[{ required: true, message: '请选择会议形式' }]" readonly :disabled="disabledFlag" />
<div v-for="(item, index) in popObj.stockCodeList" :key="index">
<van-field clickable name="text" v-model="item.text" :label="`股票代码${index + 1}`"
:placeholder="disabledFlag ? '' : '点击选择股票'" @click="popObj.handleShowPop6('stockCode', '股票', index)"
type="text" autosize readonly :right-icon="disabledFlag ? '' : 'close'"
@click-right-icon.stop="popObj.deleteItemByType('stockCode', index)" :disabled="disabledFlag" />
</div>
<van-field clickable name="newItem" v-model="newItem" label="股票代码" placeholder="点击新增股票"
@click="popObj.handleShowPop6('stockCode', '股票', -1)" readonly v-show="!disabledFlag" />
<van-popup v-model:show="popObj.showPop2" position="bottom">
<van-field v-model="popObj.searchValue" :placeholder="'请输入' + popObj.popTitle"
@input="popObj.onSearchInput" />
<van-picker show-toolbar :title="popObj.popTitle" :columns="popObj.showPop2List"
@confirm="popObj.onConfirm2($event)" @cancel="popObj.showPop2 = false" />
</van-popup>
<div v-for="(item, index) in popObj.meetingGuestList" :key="index">
<van-field clickable name="text" v-model="item.text" :label="`嘉宾${index + 1}`"
:placeholder="disabledFlag ? '' : '点击选择宾'" @click="popObj.handleShowPop6('meetingGuest', '嘉宾', index)"
type="text" autosize readonly :right-icon="disabledFlag ? '' : 'close'"
@click-right-icon.stop="popObj.deleteItemByType('meetingGuest', index)" :disabled="disabledFlag" />
</div>
<van-field clickable name="newItem" v-model="newItem" label="嘉宾" placeholder="点击新增嘉宾"
@click="popObj.handleShowPop6('meetingGuest', '嘉宾', -1)" readonly v-show="!disabledFlag" />
<van-field clickable name="linkFromName" v-model="popObj.linkFromName" label="发起人"
:placeholder="disabledFlag ? '' : '点击添加发起人'" @click="popObj.handleShowPop5('linkFrom', '发起人')" readonly
:disabled="disabledFlag" />
<van-popup v-model:show="popObj.showPop6" position="bottom">
<div class="p-16 box-popup">
<van-row class="mb-16">
<van-col span="8">
<van-button class="van-picker__cancel" native-type="button"
@click="popObj.showPop6 = false">取消</van-button>
</van-col>
<van-col span="8" class="text-center">
<span class="sub-title f-b">请选择{{ popObj.popTitle }}</span>
</van-col>
<van-col span="8" class="text-end">
<van-button class="van-picker__confirm" native-type="button" @click="popObj.onConfirm5">确认</van-button>
</van-col>
</van-row>
<van-radio-group v-model="popObj.linkFrom" @click="popObj.changeLinkFrom">
<van-radio :name="1">
<div class="d-flex align-items-center">
<div class="radioLabel">机构人员</div>
<van-field class="flex-1 radioContent1 border-1 ms-10" v-model="popObj.linkFromField"
placeholder="录入发起人" :disabled="popObj.linkFrom !== 1" />
</div>
</van-radio>
<van-radio :name="2" class="align-items-start-radio">
<div class="d-flex align-items-start mt-10">
<div class="radioLabel">第一上海</div>
<div class="flex-1 radioContent2">
<van-radio-group class="ms-10" v-model="popObj.linkFromRadio" direction="horizontal"
:disabled="popObj.linkFrom !== 2">
<van-radio v-for="(item, index) in popObj.linkFromList" :key="index" :name="item.userId">
{{ item.userName }}
</van-radio>
</van-radio-group>
</div>
</div>
</van-radio>
</van-radio-group>
</div>
</van-popup>
<van-field name="phoneBy" v-model="popObj.phoneBy" label="电话拨入方式" :placeholder="disabledFlag ? '' : '请输入电话拨入方式'"
:disabled="disabledFlag" />
<van-field name="netBy" v-model="popObj.netBy" label="网络端登录方式" :placeholder="disabledFlag ? '' : '请输入网络端登录方式'"
:disabled="disabledFlag" />
<van-field v-if="popObj.netBy" name="meetingPassword" v-model="popObj.meetingPassword" label="会议密码"
placeholder="请输入会议密码" :disabled="disabledFlag" />
<van-field clickable name="organizerNames" v-model="popObj.organizerNames" label="相关研究员"
:placeholder="disabledFlag ? '' : '点击选择相关研究员'" @click="popObj.handleShowPop3('organizer', '相关研究员')" readonly
:disabled="disabledFlag" />
<van-popup v-model:show="popObj.showPop3" position="bottom">
<div class="p-16 box-popup">
<van-row class="mb-16">
<van-col span="8">
<van-button class="van-picker__cancel" native-type="button"
@click="popObj.showPop3 = false">取消</van-button>
</van-col>
<van-col span="8" class="text-center">
<span class="sub-title f-b">请选择{{ popObj.popTitle }}</span>
</van-col>
<van-col span="8" class="text-end">
<van-button class="van-picker__confirm" native-type="button" @click="popObj.onConfirm3">确认</van-button>
</van-col>
</van-row>
<van-checkbox-group v-model="popObj.value3" direction="horizontal">
<van-checkbox v-for="(item, index) in popObj[`${popObj.type}List`]" :key="index" :name="item.value"
shape="square">
{{ item.text }}
</van-checkbox>
</van-checkbox-group>
</div>
</van-popup>
<van-field name="content" v-model="popObj.content" label="备注" rows="3" type="textarea" autosize
:disabled="disabledFlag" />
<van-field clickable name="meetingByName" v-model="popObj.visibleName" label="可见范围"
:placeholder="disabledFlag ? '' : '点击选择可见范围'" @click="popObj.handleShowPop1('visible', '可见范围')" readonly
:disabled="disabledFlag" />
<van-field v-if="popObj.selectvisible === '3'" clickable name="staffNames" v-model="popObj.staffNames"
label="可见员工" :placeholder="disabledFlag ? '' : '点击选择可见员工'" @click="popObj.handleShowPop3('staff', '可见员工')"
readonly :rules="[{ required: popObj.selectvisible === '3', message: '请选择可见员工' }]"
:required="popObj.selectvisible === '3'" :disabled="disabledFlag" />
<div class="m-16 d-flex justify-content-center">
<van-button round block type="warning" size="small" native-type="button" @click="changeDisabled"
v-if="meetingId">
{{ disabledFlag ? '修改' : '取消' }}
</van-button>
<van-button v-show="!disabledFlag" round block type="primary" size="small" native-type="submit"
:disabled="submitFlag">提交</van-button>
</div>
</van-form>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import emitter from '@/utils/mitt'
import {
sysDicListByType, sysUserGetStaffAndManagerList, sysUserGetCompanyUserList, stockStockInfoLookUp,
cusUserLookUp, workMeetingUpdateOrSave, workMeetingInfo, workMeetingUserList
} from '@/utils/api'
let submitFlag = ref(false)
let disabledFlag = ref(true)
let newItem = ref('')
let listIndex = ref(0)
let meetingId: any = ref('')
import { debounceThrottle } from '@/mixins/debounce-throttle'
const { throttle } = debounceThrottle()
import { type MeetingObj } from '@/utils/types'
const popObj: MeetingObj = reactive({
showPop1: false,
showPop2: false,
showPop3: false,
showPop4: false,
showPop5: false,
showPop6: false,
popTitle: '',
type: '',
value1: [],
value3: [],
meetingTitle: '',
sysDay: '',
selectsysDay: [],
sysTime: '',
selectsysTime: [],
meetingTypeName: '',
selectmeetingType: '',
meetingTypeList: [],
meetingByName: '',
selectmeetingBy: '',
meetingByList: [],
stockCodeList: [],
searchValue: '',
showPop2List: [],
meetingGuestList: [],
linkFromName: '',
linkFrom: 1,
linkFromRadio: '',
linkFromField: '',
linkFromList: [],
phoneBy: '',
netBy: '',
meetingPassword: '',
organizerNames: '',
selectorganizerIds: [],
organizerList: [],
content: '',
visibleName: '公开',
selectvisible: '1',
visibleList: [{
value: '1',
text: '公开'
}, {
value: '3',
text: '员工'
}],
staffNames: '',
staffList: [],
selectstaffIds: [],
handleShowPop1: (val: string, title: string) => {
if (!disabledFlag.value) {
popObj.showPop1 = true
popObj.popTitle = title
popObj.type = val
popObj.value1 = [popObj[`select${val}`]]
}
},
handleShowPop3: (val: string, title: string) => {
if (!disabledFlag.value) {
popObj.showPop3 = true
popObj.popTitle = title
popObj.type = val
popObj.value3 = popObj[`select${val}Ids`]
}
},
handleShowPop4: (val: string, title: string, index: number) => {
if (!disabledFlag.value) {
popObj.popTitle = title
popObj.type = val
popObj[`value${index}`] = popObj[`select${val}`]
popObj[`showPop${index}`] = true
}
},
handleShowPop5: (val: string, title: string) => {
if (!disabledFlag.value) {
if (popObj.linkFrom === 1) {
popObj.linkFromRadio = ''
popObj.linkFromField = popObj.linkFromName
} else {
const index = popObj.linkFromList.findIndex((ele: any) => {
return ele.userName === popObj.linkFromName
})
if (index > -1) {
popObj.linkFromRadio = popObj.linkFromList[index].userId
}
}
popObj.showPop6 = true
popObj.popTitle = title
popObj.type = val
}
},
handleShowPop6: (val: string, title: string, index: number) => {
if (!disabledFlag.value) {
popObj.showPop2 = true
popObj.popTitle = title
popObj.type = val
popObj.searchValue = index > -1 ?
popObj[`${val}List`][index][`${val === 'stockCode' ? 'value' : 'cusUserName'}`] : ''
popObj.showPop2List = index > -1 && popObj[`${val}List`][index].value ?
[{ text: popObj[`${val}List`][index].text, value: popObj[`${val}List`][index].value }] : []
listIndex.value = index
}
},
onSearchInput: throttle(
async () => {
if (popObj.searchValue) {
if (popObj.type === 'stockCode') {
const { data } = await stockStockInfoLookUp({ stockCode: popObj.searchValue })
popObj.showPop2List = data.map((ele: any) => {
ele.text = `${ele.stockCode}${ele.stockName}`
ele.value = ele.stockCode
return ele
})
} else if (popObj.type === 'meetingGuest') {
const { data } = await cusUserLookUp({ cusUserName: popObj.searchValue })
popObj.showPop2List = data.map((ele: any) => {
ele.text = `${ele.cusUserName}${ele.cusName ? '【' + ele.cusName + '】' : ''}`
ele.value = ele.cusUserId
return ele
})
}
} else {
popObj.showPop2List = []
}
},
500
),
deleteItemByType: (type: string, index: number) => {
if (!disabledFlag.value) {
popObj[`${type}List`].splice(index, 1)
}
},
onConfirm1: ({ selectedOptions }) => {
if (selectedOptions) {
popObj[`select${popObj.type}`] = selectedOptions[0].value
popObj[`${popObj.type}Name`] = selectedOptions[0].text
if (popObj.type === 'meetingBy') {
if (selectedOptions[0].value === '1') {
popObj.phoneBy = '香港852-3005-1318\n大陆400-810-5222\n'
} else if (selectedOptions[0].value == 2) {
popObj.phoneBy = '+8675536550000\n+85230018898'
} else {
popObj.phoneBy = ''
}
}
}
popObj.showPop1 = false
},
onConfirm2: ({ selectedOptions }) => {
if (selectedOptions && selectedOptions[0]) {
if (listIndex.value > -1) {
popObj[`${popObj.type}List`][listIndex.value].value = selectedOptions[0].value
popObj.stockCodeList[listIndex.value][`${popObj.type === 'stockCode' ? 'stockName' : 'cusUserName'}`] =
selectedOptions[0][`${popObj.type === 'stockCode' ? 'stockName' : 'cusUserName'}`]
popObj[`${popObj.type}List`][listIndex.value].text = selectedOptions[0].text
} else {
let data: any = {
value: selectedOptions[0].value,
text: selectedOptions[0].text
}
if (popObj.type === 'stockCode') {
data.stockName = selectedOptions[0].stockName
} else if (popObj.type === 'meetingGuest') {
data.cusUserName = selectedOptions[0].cusUserName
}
popObj[`${popObj.type}List`].push(data)
}
popObj.showPop2 = false
} else {
if (popObj.type === 'stockCode') {
showToast(`请选择${popObj.popTitle}后,再确认`)
} else {
popObj.meetingGuestList.push({
value: '',
cusUserName: popObj.searchValue,
text: popObj.searchValue
})
popObj.showPop2 = false
}
}
},
onConfirm3: () => {
popObj[`select${popObj.type}Ids`] = popObj.value3
const sameList = popObj[`${popObj.type}List`].filter((ele: any) => popObj.value3.some((res: any) => {
return ele.value === res
}))
let names = ''
sameList.forEach((ele: any) => {
names += `${ele.text},`
})
if (names) {
names = names.substring(0, names.length - 1)
}
popObj[`${popObj.type}Names`] = names
popObj.showPop3 = false
},
onConfirm4: ({ selectedValues }) => {
if (selectedValues) {
popObj[popObj.type] = `${selectedValues[0]}${popObj.type === 'sysDay' ? '-' + selectedValues[1] + '-' + selectedValues[2] : ':' + selectedValues[1]}`
popObj[`select${popObj.type}`] = selectedValues
}
if (popObj.type === 'sysDay') {
popObj.showPop4 = false
} else {
popObj.showPop5 = false
}
},
changeLinkFrom: () => {
if (popObj.linkFrom === 1) {
popObj.linkFromRadio = ''
} else if (popObj.linkFrom === 2) {
popObj.linkFromField = ''
}
},
onConfirm5: () => {
if (popObj.linkFrom === 1) {
popObj.linkFromName = popObj.linkFromField
popObj.linkFromRadio = ''
popObj.showPop6 = false
} else if (popObj.linkFrom === 2) {
popObj.linkFromField = ''
const index = popObj.linkFromList.findIndex((ele: any) => {
return ele.userId === popObj.linkFromRadio
})
if (index > -1) {
popObj.linkFromName = popObj.linkFromList[index].userName
popObj.showPop6 = false
} else {
showToast('请选择发起人')
}
}
}
})
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
import moment from 'moment'
const getSelectList = async () => {
emitter.emit('showLoading', '')
const data1 = await sysDicListByType({ dicType: 'meeting_type' })
popObj.meetingTypeList = data1.data.map((ele: any) => {
ele.text = ele.dicValue
ele.value = ele.dicKey
return ele
})
const data2 = await sysDicListByType({ dicType: 'meeting_by' })
popObj.meetingByList = data2.data.map((ele: any) => {
ele.text = ele.dicValue
ele.value = ele.dicKey
return ele
})
const data3 = await sysUserGetStaffAndManagerList()
popObj.organizerList = data3.data.map((ele: any) => {
ele.value = ele.userId
ele.text = ele.userName
return ele
})
const data4 = await sysUserGetCompanyUserList()
popObj.staffList = data4.data.map((ele: any) => {
ele.value = ele.userId
ele.text = ele.userName
return ele
})
popObj.linkFromList = data4.data
if (route.query && route.query.addDay) {
disabledFlag.value = false
popObj.sysDay = route.query.addDay
popObj.selectsysDay = popObj.sysDay.split('-')
popObj.sysTime = '08:00'
popObj.selectsysTime = ['08', '00']
emitter.emit('hiddenLoading', '')
} else if (route.query && route.query.meetingId) {
disabledFlag.value = true
meetingId.value = route.query.meetingId
getMeeingInfo()
emitter.emit('setTitle', { title: '修改会议', type: 'meetingType' })
} else {
disabledFlag.value = false
let nowDay = moment().format('YYYY-MM-DD')
popObj.sysDay = nowDay
popObj.selectsysDay = nowDay.split('-')
popObj.sysTime = '08:00'
popObj.selectsysTime = ['08', '00']
emitter.emit('setTitle', { title: '新增会议', type: 'meetingType' })
emitter.emit('hiddenLoading', '')
}
}
const getMeeingInfo = async () => {
const { data } = await workMeetingInfo(meetingId.value)
popObj.meetingTitle = data.meetingTitle
popObj.sysDay = data.meetingTime.slice(0, 10)
popObj.selectsysDay = data.meetingTime.split('-')
popObj.sysTime = data.meetingTime.slice(11, 16)
popObj.selectsysTime = data.meetingTime.split(':')
popObj.meetingTypeName = data.meetingTypeName
popObj.selectmeetingType = data.meetingType + ''
popObj.meetingByName = data.meetingByName
popObj.selectmeetingBy = data.meetingBy + ''
let codeList = data.stockCodes ? data.stockCodes.split(',') : []
let nameList = data.stockNames ? data.stockNames.split(',') : []
popObj.stockCodeList = codeList.map((ele: any, index: number) => {
return {
value: ele,
text: `${ele}${nameList.length > index ? '【' + nameList[index] + '】' : ''}`,
stockName: nameList.length > index ? nameList[index] : '',
}
})
let guestNameList = data.meetingGuestNames ? data.meetingGuestNames.split(',') : []
let guestIdList = data.meetingGuestIds ? data.meetingGuestIds.split(',') : []
popObj.meetingGuestList = guestNameList.map((ele: any, index: number) => {
return {
value: guestIdList.length > index ? guestIdList[index] : '',
text: ele,
cusUserName: ele,
}
})
popObj.linkFrom = data.oranizerId && data.oranizerId !== -1 ? 2 : 1
popObj.linkFromName = data.oranizerName
popObj.phoneBy = data.phoneBy
popObj.netBy = data.netBy
popObj.meetingPassword = data.meetingPassword
popObj.organizerNames = data.oranizerName
popObj.selectorganizerIds = data.oranizerId ? data.oranizerId.split(',').map(Number) : []
popObj.content = data.content
popObj.visibleName = data.visible === 1 ? '公开' : '员工'
popObj.selectvisible = data.visible + ''
if (data.visible === 3) {
getVisible()
}
emitter.emit('hiddenLoading', '')
}
const getVisible = async () => {
const { data } = await workMeetingUserList({ meetingId: meetingId.value })
let staffNames = ''
let selectstaffIds: any = []
data.forEach((ele: any, index: number) => {
staffNames += `${ele.userName}${index < data.length - 1 ? ',' : ''}`
selectstaffIds.push(ele.userId)
})
popObj.staffNames = staffNames
popObj.selectstaffIds = selectstaffIds
}
const handleKeyUp = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
event.preventDefault()
}
}
onMounted(() => {
window.addEventListener('keyup', handleKeyUp)
})
onUnmounted(() => {
window.removeEventListener('keyup', handleKeyUp)
})
import listRefreshInfoStore from '@/stores/listRefreshInfo'
const listRefreshInfo = listRefreshInfoStore()
const onSubmit = async () => {
submitFlag.value = true
let stockCodeList: any = []
popObj.stockCodeList.forEach((ele: any) => {
if (ele.value) {
stockCodeList.push({
stockCode: ele.value,
stockName: ele.stockName
})
}
})
let meetingGuestList: any = []
popObj.meetingGuestList.forEach((ele: any) => {
if (ele.cusUserName) {
meetingGuestList.push({
cusUserId: ele.value || -1,
cusUserName: ele.cusUserName
})
}
})
const res: any = {
meetingTitle: popObj.meetingTitle,
meetingTime: `${popObj.sysDay} ${popObj.sysTime}:00`,
meetingType: popObj.selectmeetingType,
meetingBy: popObj.selectmeetingBy,
stockCodeList: stockCodeList || [],
meetingGuestList: meetingGuestList || [],
linkFrom: popObj.linkFromName || null,
linkFromId: popObj.linkFrom === 2 && popObj.linkFromRadio ? popObj.linkFromRadio : null,
phoneBy: popObj.phoneBy || null,
netBy: popObj.netBy || null,
meetingPassword: popObj.netBy && popObj.meetingPassword ? popObj.meetingPassword : null,
organizerIdList: popObj.selectorganizerIds || [],
content: popObj.content || null,
visible: popObj.selectvisible,
userIdList: popObj.selectvisible === '3' ? popObj.selectstaffIds : [],
meetingId: meetingId.value || null
}
const data: any = await workMeetingUpdateOrSave(meetingId.value, res)
if (data.code === 0) {
showToast('提交成功')
if (meetingId.value) {
listRefreshInfo.refreshId = meetingId.value
} else {
listRefreshInfo.addId = '1'
}
router.go(-1)
} else {
submitFlag.value = false
}
}
const changeDisabled = () => {
disabledFlag.value = !disabledFlag.value
if (disabledFlag.value) {
getMeeingInfo()
}
}
getSelectList()
</script>
<style lang='scss' scoped>
.add-meeting {
height: 100vh;
overflow-y: auto;
}
.detailClass1 {
.van-cell {
margin: 12px 16px 0;
width: calc(100vw - 32px);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
@media screen and (min-width: 678px) {
width: 646px;
}
}
}
.detailClass2 {
.van-cell {
margin: 0 16px;
width: calc(100vw - 32px);
box-shadow: 0 0;
@media screen and (min-width: 678px) {
width: 646px;
}
}
}
:deep(.van-field--disabled) .van-field__label {
color: #646566;
}
:deep(.van-field__control:disabled) {
-webkit-text-fill-color: #000 !important;
}
:deep(.van-field__control) {
color: #000 !important;
}
:deep(.van-checkbox) {
margin-right: 0 !important;
width: 50%;
margin-bottom: 5px;
}
:deep(.van-radio) {
width: 100%;
}
.radioLabel {
width: 60px;
}
.radioContent1 {
max-width: 220px;
margin-left: 10px !important;
}
.radioContent2 {
max-height: 400px;
overflow-y: auto;
.van-radio {
margin-right: 0 !important;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
width: 50%;
margin-bottom: 5px;
}
}
:deep(.align-items-start-radio) {
align-items: flex-start !important;
max-height: 300px;
.van-radio__icon {
margin-top: 10px;
}
}
.van-radio-group {
.van-cell {
margin-top: 0;
}
}
:deep(.ms-10) {
.van-radio__icon {
margin-top: 0;
}
}
</style>

View File

@@ -0,0 +1,436 @@
<template>
<div class="page-content">
<div class="meeting-detail">
<div class="normal-title text-center p-10 f-b">
{{ meetingData.meetingTitle }}
<van-icon v-if="isAuth('calendar:update') || userInfo.id === meetingData.createUser" name="edit"
class="warning-text-color"
@click="$router.push({ name: 'add-or-update-meeting', query: { meetingId: meetingId } })" />
<van-icon v-if="isAuth('calendar:update') || userInfo.id === meetingData.createUser" name="delete-o"
class="danger-text-color" @click="deleteMeeting" />
</div>
<div class="text-center m-5 f-b">{{ meetingData.meetingTime }}</div>
<div class=" text-center m-5">
<van-tag type="success" size="medium">{{ meetingData.meetingTypeName }}</van-tag>
</div>
<van-grid class="baseData" :column-num="2">
<van-grid-item>会议形式</van-grid-item>
<van-grid-item>{{ meetingData.meetingByName }}</van-grid-item>
<van-grid-item>股票名称</van-grid-item>
<van-grid-item>
<div class="grid-item-text">{{ meetingData.stockNames }}</div>
</van-grid-item>
<van-grid-item>股票代码</van-grid-item>
<van-grid-item>
<div class="grid-item-text">{{ meetingData.stockCodes }}</div>
</van-grid-item>
<van-grid-item>电话拨入方式</van-grid-item>
<van-grid-item>
<div class="grid-item-text">{{ meetingData.phoneBy }}</div>
</van-grid-item>
<van-grid-item>网络端登录方式</van-grid-item>
<van-grid-item>
<div class="grid-item-text">{{ meetingData.netBy }}</div>
</van-grid-item>
<van-grid-item>会议密码</van-grid-item>
<van-grid-item>
<div class="grid-item-text">{{ meetingData.meetingPassword }}</div>
</van-grid-item>
<van-grid-item>嘉宾</van-grid-item>
<van-grid-item>
<div class="grid-item-text">{{ meetingData.meetingGuestList }}</div>
</van-grid-item>
<van-grid-item>研究员</van-grid-item>
<van-grid-item>
<div class="grid-item-text">{{ meetingData.oranizerName }}</div>
</van-grid-item>
<van-grid-item>发起人</van-grid-item>
<van-grid-item>
<div class="grid-item-text">{{ meetingData.organizer }}</div>
</van-grid-item>
<van-grid-item>IS参会人数</van-grid-item>
<van-grid-item>{{ meetingData.meetingCusUserNum }}</van-grid-item>
<van-grid-item>IS提问人数</van-grid-item>
<van-grid-item>{{ meetingData.questionNum }}</van-grid-item>
<van-grid-item>总参会人数</van-grid-item>
<van-grid-item>{{ meetingData.userNumHigh }}</van-grid-item>
<van-grid-item>总提问人数</van-grid-item>
<van-grid-item>{{ meetingData.queationNumAll }}</van-grid-item>
<van-grid-item>可见范围</van-grid-item>
<van-grid-item>
<div class="grid-item-text">{{ meetingData.visible === 1 ? '公开' : meetingData.visibleName }}</div>
</van-grid-item>
<van-grid-item>备注</van-grid-item>
<van-grid-item>
<div class="grid-item-text">{{ meetingData.content }}</div>
</van-grid-item>
</van-grid>
<div class="sub-title m-16">会议资料</div>
<file-list ref="meetingFile" />
<div class="sub-title m-16">参会资料</div>
<file-list ref="joinMeetingFile" />
<div class="sub-title m-16 d-flex align-items-center">
参会人员
<van-button type="primary" size="mini" @click="addJoinPerson(1)" style="margin-left: 4px">报名</van-button>
<van-button type="primary" size="mini" @click="addJoinPerson(2)">签到</van-button>
</div>
<van-popup v-model:show="showPersonPopup" position="bottom">
<div class="p-10 box-popup">
<div class="personTitle">当前添加联系人{{ addPerson }}</div>
<van-field v-model="searchCuseUser" placeholder="查找联系人" @input="onSearchInput"
class="border-1 border-radius-8" />
<div class="tip">
<van-row v-if="personType === 2 || personType === 3">
<van-col span="12">
<van-field name="switch" label="签到">
<template #input>
<van-switch v-model="attend" size="20" :disabled="personType === 2" />
</template>
</van-field>
</van-col>
<van-col span="12">
<van-field name="switch" label="提问" v-show="attend">
<template #input>
<van-switch v-model="question" size="20" />
</template>
</van-field>
</van-col>
</van-row>
<van-picker show-toolbar title="" :columns="cusUserList" @click-option="onCusUserConfirm"
@cancel="showPersonPopup = false">
<template #confirm>
<span></span>
</template>
</van-picker>
</div>
</div>
</van-popup>
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<van-swipe-cell v-for="(item, index) in joinPersonList" :key="item.cusUserId">
<van-cell>
<div class="sub-title text-start balck-text-color">{{ item.cusUserName }}</div>
<div class="d-flex">
<div class="flex-1">
<div class="content text-start">{{ item.cusName }}</div>
<div class="content text-start">{{ item.positionName }}</div>
</div>
<div class="flex-1 text-end">
<div class="content" :class="item.attend ? 'primary-text-color' : ''">
{{ item.attend ? '已到场' : '已报名' }}
</div>
<div class="content" v-if="item.attend" :class="item.question ? 'danger-text-color' : ''">
{{ item.attend ? item.question ? '已提问' : '未提问' : '' }}
</div>
</div>
</div>
</van-cell>
<template #right>
<van-button square type="danger" text="删除" @click="deleteJoinPerson(item, index)" />
<van-button square type="primary" text="修改" @click="changeJoinPerson(item)" />
</template>
</van-swipe-cell>
</van-list>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import FileList from '@/components/file-list.vue'
import emitter from '@/utils/mitt'
import { isAuth } from '@/utils/index'
import userInfoStore from '@/stores/userInfo'
const userInfo = userInfoStore()
const route = useRoute()
const router = useRouter()
let meetingData = ref({
meetingTime: '',
meetingTitle: '',
meetingTypeName: '',
meetingByName: '',
stockCodes: '',
stockNames: '',
phoneBy: '',
netBy: '',
meetingPassword: '',
meetingCusUserNum: '',
questionNum: '',
userNumHigh: '',
queationNumAll: '',
meetingGuestList: '',
organizer: '',
oranizerName: '',
visible: undefined,
visibleName: '',
content: '',
createUser: ''
})
let meetingId: any = ref('')
const meetingFile: any = ref(null)
const joinMeetingFile: any = ref(null)
import fileInfoStore from '@/stores/fileInfo'
const fileInfo = fileInfoStore()
const init = () => {
if (route.query && route.query.meetingId) {
meetingId.value = route.query.meetingId
emitter.emit('showLoading', '')
getMeetingDetail()
} else {
router.push({ name: '404' })
}
}
import {
workMeetingInfo, workMeetingUserList, workMeetingGetFileId, workMeetinGetUserFileMenuId,
cusUserMeetingLogList, cusUserMeetingLogDelete, cusUserLookUp, cusUserMeetingLogUpdateOrSave,
workMeetingDelete
} from '@/utils/api'
const getMeetingDetail = async () => {
const data1: any = await workMeetingInfo(meetingId.value)
let meetingGuestNameList = data1.data.meetingGuestNames
? data1.data.meetingGuestNames.split(',')
: []
let meetingGuestList = ''
meetingGuestNameList.forEach((item: any, index: number) => {
meetingGuestList +=
`
${item}
${index < meetingGuestNameList.length - 1 ? ',' : ''}
`
})
data1.data.meetingGuestList = meetingGuestList
meetingData.value = data1.data
emitter.emit('setTitle', { title: meetingData.value.meetingTitle, type: 'meeting' })
if (data1.data.visible === 3) {
getVisible()
}
const data2: any = await workMeetingGetFileId({ meetingId: meetingId.value })
if (meetingFile.value) {
meetingFile.value.init(data2.data)
}
const data3: any = await workMeetinGetUserFileMenuId({ meetingId: meetingId.value })
if (joinMeetingFile.value) {
joinMeetingFile.value.init(data3.data)
}
nextTick(() => {
if (fileInfo.fileId) {
const el = document.getElementById(fileInfo.fileId)
if (el) {
el.scrollIntoView({ behavior: 'smooth' })
}
fileInfo.fileId = ''
emitter.emit('hiddenLoading', '')
} else {
emitter.emit('hiddenLoading', '')
}
})
}
const getVisible = async () => {
const { data } = await workMeetingUserList({ meetingId: meetingId.value })
let name = ''
data.forEach((ele: any, index: number) => {
name += `${ele.userName}${index < data.length - 1 ? ',' : ''}`
})
meetingData.value.visibleName = name
}
let finished = ref(false)
let loading = ref(false)
let curPage = ref(1)
let joinPersonList = ref<{ cusUserId: string, cusUserName: string, cusName: string, positionName: string, attend: boolean, question: boolean }[]>([])
const onLoad = async () => {
loading.value = true
const { data } = await cusUserMeetingLogList({
curPage: curPage.value,
limit: 20,
meetingId: meetingId.value
})
data.list.map((ele: any) => {
ele.question = !!ele.question
ele.attend = !!ele.attend
})
joinPersonList.value = joinPersonList.value.concat(data.list)
if (joinPersonList.value.length >= data.totalCount) {
finished.value = true
} else {
curPage.value++
}
loading.value = false
}
let attend = ref(false)
let question = ref(false)
let searchCuseUser = ref('')
let meetingUserId = ref('')
let personType = ref()
let showPersonPopup = ref(false)
let addPerson = ref('')
let cusUserList = ref<{ text: string, value: string, cusUserName: string, cusName: string, positionName: string, cusUserId: string }[]>([])
const addJoinPerson = (index: number) => {
personType.value = index
searchCuseUser.value = ''
attend.value = false
if (index === 2) {
attend.value = true
}
question.value = false
cusUserList.value = []
meetingUserId.value = ''
showPersonPopup.value = true
}
const changeJoinPerson = (e: any) => {
personType.value = 3
searchCuseUser.value = e.cusUserName
attend.value = e.attend
question.value = e.question
cusUserList.value = [{
cusUserName: e.cusUserName,
cusName: e.cusName,
positionName: e.positionName,
cusUserId: e.cusUserId,
text: `${e.cusUserName}${e.cusName ? '【' + e.cusName + '】' : ''}${e.positionName ? '(' + e.positionName + ')' : ''}`,
value: e.cusUserId
}]
meetingUserId.value = e.meetingUserId
showPersonPopup.value = true
}
import { debounceThrottle } from '@/mixins/debounce-throttle'
const { throttle } = debounceThrottle()
const onSearchInput = throttle(
async () => {
if (searchCuseUser.value) {
const { data } = await cusUserLookUp({ cusUserName: searchCuseUser.value })
cusUserList.value = data.map((ele: any) => {
ele.text = `${ele.cusUserName}${ele.cusName ? '【' + ele.cusName + '】' : ''}${ele.positionName ? '(' + ele.positionName + ')' : ''}`
ele.value = ele.cusUserId
return ele
})
if (data.length === 1) {
const res = {
cusUserId: data[0].cusUserId,
attend: attend.value ? 1 : 0,
question: question.value ? 1 : 0,
meetingId: meetingId.value,
meetingUserId: meetingUserId.value || null
}
await cusUserMeetingLogUpdateOrSave(meetingUserId.value, res)
addPerson.value = `${data[0].cusUserName},${addPerson.value}`
searchCuseUser.value = ''
cusUserList.value = []
}
} else {
cusUserList.value = []
}
},
500
)
const onCusUserConfirm = async (e: any) => {
if (e.selectedIndexes[0] > -1) {
const res = {
cusUserId: e.selectedOptions[0].value,
attend: attend.value ? 1 : 0,
question: question.value ? 1 : 0,
meetingId: meetingId.value,
meetingUserId: meetingUserId.value || null
}
await cusUserMeetingLogUpdateOrSave(meetingUserId.value, res)
addPerson.value = `${e.selectedOptions[0].cusUserName},${addPerson.value}`
searchCuseUser.value = ''
cusUserList.value = []
}
}
watch(() => showPersonPopup.value, (val) => {
if (!val) {
curPage.value = 1
joinPersonList.value = []
finished.value = false
addPerson.value = ''
onLoad()
}
})
const deleteJoinPerson = (e: any, index: number) => {
showConfirmDialog({
title: '提示',
message:
`是否确认删除${e.cusUserName}参加${meetingData.value.meetingTime} ${meetingData.value.meetingTitle}记录`,
}).then(async () => {
const data: any = await cusUserMeetingLogDelete([e.meetingUserId])
if (data.code === 0) {
showToast('删除成功')
joinPersonList.value.splice(index, 1)
}
}).catch(() => {
})
}
init()
import listRefreshInfoStore from '@/stores/listRefreshInfo'
const listRefreshInfo = listRefreshInfoStore()
const deleteMeeting = () => {
showConfirmDialog({
title: '提示',
message:
`是否确认删除会议${meetingData.value.meetingTitle}`,
}).then(async () => {
const data: any = await workMeetingDelete(meetingId.value)
if (data.code === 0) {
showToast('删除成功')
listRefreshInfo.deleteId = meetingId.value
router.go(-1)
}
}).catch(() => {
})
}
</script>
<style lang='scss' scoped>
.meeting-detail {
height: 100vh;
overflow-y: auto;
}
.baseData {
:deep(.van-grid-item:nth-child(2n)) {
.van-grid-item__content {
color: #1989fa;
}
}
:deep(.van-grid-item__content) {
font-size: 14px;
font-weight: 400;
padding: 8px 4px;
font-size: 12px;
}
}
.grid-item-text {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-align: center;
max-width: calc(50vw - 16px);
@media screen and (min-width: 678px) {
max-width: 320px;
}
}
:deep(.van-swipe-cell__right) {
.van-button {
height: 100%;
}
}
.personTitle {
height: 60px;
overflow-y: auto;
}
</style>

237
src/views/meeting/list.vue Normal file
View File

@@ -0,0 +1,237 @@
<template>
<div class="page-content tabbar-list top-select-list" ref="pageContent">
<div class="addDiv success-background-color" v-if="isAuth('calendar:update')"
@click=" $router.push({ name: 'add-or-update-meeting' , query: { addDay: selectDay } })">
<div class="text-center addDiv1">新增</div>
<div class="text-center addDiv2">会议</div>
</div>
<div class="top-select border-bottom-1">
<van-dropdown-menu>
<van-dropdown-item :title="searchContent" ref="itemRef">
<van-field v-model="search" placeholder="搜索" :right-icon="search ? 'clear' : ''"
@click-right-icon.stop="clearSearch" />
<div style="padding: 5px 16px;">
<van-button type="primary" size="small" block round @click="onConfirm">
确认
</van-button>
</div>
</van-dropdown-item>
<van-dropdown-item v-model="meetingType" :options="meetingTypeList" @change="getMeetingList" />
<van-dropdown-item v-model="createUser" :options="createUserList" @change="getMeetingList" />
</van-dropdown-menu>
</div>
<van-calendar title="" :poppable="false" :show-confirm="false" switch-mode="year-month" first-day-of-week="1"
:style="{ height: '410px' }" :formatter="formatter" :default-date="defaultDate" @panel-change="panelChange"
@select="onDateConfirm">
</van-calendar>
<div v-if="list.length > 0">
<van-cell v-for="item in list" :key="item.meetingId"
@click="$router.push({ name: 'meeting-detail', query: { meetingId: item.meetingId } })">
<div class="text-start activeColor sub-title">{{ item.meetingHour }}</div>
<div class="normal-title text-start balck-text-color mb-5 mt-5">{{ item.meetingTitle }}</div>
<div class="text-end">
<van-tag type="primary" size="medium">{{ item.meetingTypeName }}</van-tag>
<van-tag type="danger" class="ms-10" size="medium">{{ item.meetingByName }}</van-tag>
</div>
</van-cell>
</div>
<van-empty v-else description="暂无会议" />
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { sysDicListByType, workMeetingList, workMeetingInfo, workStaffLookup } from '@/utils/api'
import { convertToUrl } from '@/utils'
import { scrollList } from '@/mixins/scroll-list'
import { isAuth } from '@/utils/index'
let search = ref('')
let searchContent = ref('搜索')
let itemRef = ref()
const clearSearch = () => {
search.value = ''
searchContent.value = '搜索'
}
const onConfirm = () => {
searchContent.value = search.value || '搜索'
itemRef.value.toggle()
getMeetingList()
}
let meetingType = ref('1,2,3,16,17,4,5,15,6,10,11,12,13,14,7')
let meetingTypeList: any = ref([])
const getMeetingTypeList = async () => {
const { data } = await sysDicListByType({ dicType: 'meeting_type' })
meetingTypeList.value = data.map((ele: any) => {
ele.text = ele.dicValue
ele.value = ele.dicKey
return ele
})
meetingTypeList.value.unshift({ text: '全部类型', value: '1,2,3,16,17,4,5,15,6,10,11,12,13,14,7' })
}
getMeetingTypeList()
let createUser = ref('')
let createUserList: any = ref([])
const getCreateUserList = async () => {
const { data } = await workStaffLookup()
createUserList.value = data.map((ele: any) => {
ele.text = ele.staffName
ele.value = ele.staffId
return ele
})
createUserList.value.unshift({ text: '全部创建人', value: '' })
}
getCreateUserList()
let defaultDate = ref()
import moment from 'moment'
const panelChange = (e: any) => {
dateBegin.value = moment(e.date).startOf('month').format('YYYY-MM-DD')
dateEnd.value = moment(e.date).endOf('month').format('YYYY-MM-DD')
getMeetingList()
}
const onDateConfirm = (e: any) => {
defaultDate.value = e
getList()
}
let list: any = ref([])
let selectDay = ref('')
const getList = () => {
list.value = []
selectDay.value = moment(defaultDate.value).format('YYYY-MM-DD')
meetingList.value.forEach((ele: any) => {
if (ele.meetingDate === selectDay.value) {
list.value = ele.list
}
})
}
const formatter = computed(() => {
if (meetingList.value.length === 0) {
return (day: any) => day
}
return (day: any) => {
let time = moment(day.date).format('YYYY-MM-DD')
meetingList.value.forEach((ele: any) => {
if (ele.meetingDate === time) {
day.topInfo = ele.num
}
})
return day
}
})
const dateBegin = ref()
const dateEnd = ref()
const meetingList: any = ref([])
const getMeetingList = async () => {
let url = getMeetingParams()
const { data } = await workMeetingList(url)
let map = new Map()
data.forEach((item: any, index: any, arr: any) => {
if (!map.has(item.meetingDate)) {
map.set(
item.meetingDate,
arr.filter((a: any) => a.meetingDate == item.meetingDate)
)
}
})
let res = Array.from(map).map((item) => [...item[1]])
meetingList.value = []
res.forEach((ele: any) => {
meetingList.value.push({ meetingDate: ele[0].meetingDate, num: ele.length, list: ele })
})
getList()
}
const getMeetingParams = () => {
const res = {
dateBegin: dateBegin.value,
dateEnd: dateEnd.value,
createUser: createUser.value,
cusName: search.value,
meetingType: meetingType.value
}
const url = convertToUrl(res)
return url
}
let pageContent = ref(null)
const scrollPosition = ref(0)
const refresh = async (type: any, id: any) => {
if (type === 'delete' || type === 'refresh') {
let dayIndex = meetingList.value.findIndex((ele: any) => {
return ele.meetingDate === selectDay.value
})
if (dayIndex > -1) {
let index = meetingList.value[dayIndex].list.findIndex((ele: any) => {
return Number(ele.meetingId) === Number(id)
})
if (type === 'delete') {
if (index > -1) {
meetingList.value[dayIndex].list.splice(index, 1)
meetingList.value[dayIndex].num -= 1
}
} else {
if (index > -1) {
const { data } = await workMeetingInfo(id)
meetingList.value[dayIndex].list[index] = data
}
getList()
}
}
} else {
defaultDate.value = new Date()
dateBegin.value = moment().startOf('month').format('YYYY-MM-DD')
dateEnd.value = moment().endOf('month').format('YYYY-MM-DD')
getMeetingList()
}
}
const { setScrollTop, setScrollPositionAndRefreshFlag } = scrollList(pageContent, scrollPosition, refresh)
setScrollTop()
import { onBeforeRouteLeave } from 'vue-router'
onBeforeRouteLeave((to, from) => {
setScrollPositionAndRefreshFlag(to, 'meeting-detail')
})
</script>
<style lang='scss' scoped>
:deep(.van-calendar__header-title) {
display: none;
}
:deep(.van-calendar__day) {
border: 1px solid #f2f2f2;
margin-bottom: 0;
}
:deep(.van-calendar__top-info) {
width: 14px;
left: calc(100vw / 7 - 18px);
border-radius: 100%;
background-color: #f56c6c;
color: #fff;
@media screen and (min-width: 678px) {
left: 74px;
}
}
:deep(.van-calendar__selected-day) {
width: 100%;
height: 100%;
}
.item {
width: 100%;
text-align: center;
}
.meetingList {
overflow-y: auto;
height: calc(100vh - 160px);
}
</style>

View File

@@ -0,0 +1,487 @@
<template>
<div class="page-content">
<van-tabs v-model:active="active" sticky @change="changeTab">
<van-tab v-for="item in tabs" :key="item.title" :title="item.title">
<div class="p-10" v-if="active === 0">
<div class="base-info">
<div class="d-flex base-item">
<span class="base-title">机构名称</span>
<span class="base-desc">{{ cusInfo.cusName }}</span>
</div>
<div class="d-flex base-item">
<span class="base-title">机构类型</span>
<span class="base-desc">{{ cusInfo.companyTypeName }}</span>
</div>
<div class="d-flex base-item">
<span class="base-title">核心联系人</span>
<span class="base-desc">{{ cusInfo.cusUserName }}</span>
</div>
<div class="d-flex base-item">
<span class="base-title">跟进人</span>
<span class="base-desc">{{ cusInfo.saleUserName }}</span>
</div>
<div class="d-flex base-item">
<span class="base-title">城市</span>
<span class="base-desc">{{ cusInfo.cityName }}</span>
</div>
<div class="d-flex base-item">
<span class="base-title">城市</span>
<span class="base-desc">{{ cusInfo.cityName }}</span>
</div>
<div class="d-flex base-item">
<span class="base-title">是否开户</span>
<span class="base-desc">{{ cusInfo.accountStatusFlag ? '是' : '否' }}</span>
</div>
<div class="d-flex base-item">
<span class="base-title">开户时间</span>
<span class="base-desc">{{ cusInfo.accountStatusFlag && cusInfo.accountTime ? cusInfo.accountTime : ''
}}</span>
</div>
<div class="d-flex base-item">
<span class="base-title">开户类型</span>
<span class="base-desc">{{ cusInfo.accountStatusFlag && cusInfo.accountTypeName ? cusInfo.accountTypeName
: '' }}</span>
</div>
<div class="d-flex base-item">
<span class="base-title">是否交易</span>
<span class="base-desc">{{ cusInfo.accountStatusFlag && cusInfo.isMoneyFlag ? '是' : '否' }}</span>
</div>
<div class="d-flex base-item">
<span class="base-title">交易总金额</span>
<span class="base-desc">{{ cusInfo.isMoneyFlag && cusInfo.accountStatusFlag && cusInfo.tradeMoney ?
cusInfo.tradeMoney : '' }}</span>
</div>
<div class="d-flex base-item">
<span class="base-title">资金规模</span>
<span class="base-desc">{{ cusInfo.capitalScaleValue ? cusInfo.capitalScaleValue :
cusInfo.capitalScaleName ? cusInfo.capitalScaleName : '' }}</span>
</div>
</div>
<div class="title">参会趋势</div>
<div class="chart d-flex align-items-center justify-content-center">
<van-loading v-show="chartLoading" type="spinner" color="#1989fa" class="loading" />
<div id="meeting_chart" class="chartDiv"></div>
</div>
</div>
<div v-if="active === 1">
<van-list v-if="cusUserList.length" finished-text="没有更多了" :immediate-check="false">
<van-cell v-for="item in cusUserList" :key="item.cusId">
<div class="name1 text-start stockNames balck-text-color">{{ item.cusUserName }}<span
class="mx-10">|</span>{{
item.positionName }}</div>
<div class="d-flex justify-content-between">
<div class="font-12">跟进人{{ item.saleNames }}
<span v-if="item.entryTime">({{ item.entryTime }}入职)</span>
</div>
</div>
<div class="d-flex font-12">
<div>更新时间</div>
<div class="activeColor">{{ item.updateTime }}</div>
</div>
<div class="d-flex font-12 justify-content-between flex-wrap">
<div class=" d-flex w-half">
<div>参会次数</div>
<div class="activeColor">{{ item.meetingNum || 0 }}</div>
</div>
<div class="d-flex justify-content-end w-half">
<div>沟通次数</div>
<div class="activeColor">{{ item.linkNum || 0 }}</div>
</div>
</div>
</van-cell>
</van-list>
<van-empty v-else />
</div>
<div v-if="active === 2">
<van-cell v-if="workList?.length" v-for="(item, index) in workList" :key="index"
@click="showDetail(item.linkNote)">
<div class="f-b activeColor text-start mb-5 mt-5">{{ item.saleNames }} - {{ item.linkTypeMenuName }}
</div>
<div class="balck-text-color text-ellipsis">{{ item.linkNote }}</div>
<div class=" d-flex font-12 flex-wrap">
<div>相关研究人{{ item.staffName }}</div>
<div>联系人{{ item.cusUserName }}</div>
</div>
<div class="d-flex justify-content-end">
<div class="activeColor font-12 text-start flex-1">{{ item.sysDate }}</div>
<van-tag type="danger" class="ms-10" size="medium">{{ item.linkTypeName }}</van-tag>
</div>
</van-cell>
<van-empty v-else />
</div>
<van-overlay :show="showOverLay" @click="showOverLay = false">
<div class="wrapper">
<div class="block" @click.stop>{{ note }}</div>
</div>
</van-overlay>
<div v-if="active === 3">
<van-cell v-if="meetingList?.length" v-for="item in meetingList" :key="item.meetingId"
@click="$router.push({ name: 'meeting-detail', query: { meetingId: item.meetingId } })">
<div class="f-b text-start balck-text-color mb-5 mt-5">{{ item.meetingTitle }}</div>
<div class="d-flex">
<div class="font-12 text-start activeColor flex-1">{{ item.meetingTime }}</div>
<van-tag type="primary" size="medium">{{ item.meetingTypeName }}</van-tag>
<van-tag type="danger" class="ms-10" size="medium">{{ item.meetingByName }}</van-tag>
</div>
</van-cell>
<van-empty v-else description="暂无会议" />
</div>
<div v-if="active === 4">
<communicate-item v-if="communicateList" class="pb-16" :list="communicateList" />
<van-empty v-else />
</div>
<div v-if="active === 5">
<van-cell v-if="serviceList?.length" v-for="item in serviceList" :key="item.id"
@click="showDetail(item.content)">
<div class="d-flex align-items-center">
<div class="f-b activeColor text-ellipsis">{{ item.content }}</div>
</div>
<div class="d-flex justify-content-between">
<div class="text-start font-12">开始时间<span class="activeColor">{{ item.serviceBeginDate }}</span></div>
<div class="text-start font-12">状态<span class="activeColor">{{ item.serviceStatus }}</span>
</div>
</div>
</van-cell>
<van-empty v-else description="暂无数据" />
</div>
<div v-if="active === 6">
<van-cell v-if="yearList?.length" v-for="(item, index) in yearList" :key="index">
<div class="d-flex align-items-center">
<div class="f-b activeColor">{{ item.sysYear }}</div>
</div>
<div class="d-flex justify-content-between">
<div class="text-start font-12">目标销售额<span class="activeColor">{{ item.targetMoney || 0 }}</span></div>
<div class="text-start font-12">年度交易额<span class="activeColor">{{ item.tradeMoneyYear || 0 }}</span>
</div>
</div>
<div class="text-start font-12 ">
完成率<span class="activeColor">{{ item.finishPer || 0 }}%</span>
</div>
</van-cell>
<van-empty v-else description="暂无数据" />
</div>
<div v-if="active === 7">
<van-cell v-if="accountList?.length" v-for="(item, index) in accountList" :key="index">
<div class="f-b text-start balck-text-color mb-5">姓名{{ item.accountName }}</div>
<div class="text-start font-12">账号{{ item.accountNo }}</div>
</van-cell>
<van-empty v-else description="暂无数据" />
</div>
</van-tab>
</van-tabs>
</div>
</template>
<script setup lang="ts">
import { nextTick, ref } from 'vue'
const showOverLay = ref(false)
const note = ref()
const showDetail = (value: string) => {
note.value = value
showOverLay.value = true
}
const active = ref(0)
const tabs = [{
name: 'base',
title: '基本信息'
}, {
name: 'cusUser',
title: '员工列表'
}, {
name: 'work',
title: '工作日报'
}, {
name: 'meeting',
title: '参会记录'
}, {
name: 'communicate',
title: '沟通记录'
}, {
name: 'service',
title: '服务目标'
}, {
name: 'year',
title: '年度数据'
}, {
name: 'account',
title: '账号列表'
}, {
name: '关注股票',
title: '关注股票'
}]
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
import emitter from '@/utils/mitt'
const cusId = ref()
const init = () => {
if (route.query && route.query.cusId) {
cusId.value = route.query.cusId
getData()
} else {
router.push({ name: '404' })
}
}
import {
cusInfoInfo, cusUserListByCusId, cusLinkList, cusServiceGetByCusId,
cusYearList, cusAccountList, cusMeetingList, cusInfoCountByMonth
} from '@/utils/api'
import { convertToUrl } from '@/utils'
const getData = async () => {
emitter.emit('showLoading', '')
try {
getCusInfo()
getChartList()
getCusUserList()
getMeetingList()
getCusLinkList()
getWorkList()
getServiceList()
getYearList()
getAccountList()
} finally {
emitter.emit('hiddenLoading', '')
}
}
import * as echarts from 'echarts'
const chartLoading = ref(false)
const getChartList = async () => {
chartLoading.value = false
const { data } = await cusInfoCountByMonth({ cusId: cusId.value })
const sysDate: any = []
const seriesList: any = []
const legendList = ['FSH电话会议+调研+其他', '周末会+精英会', '按需会议', '机构路演', '机构沟通', '交易量']
const colorList = ['#f26522', '#45b97c', '#009ad6', '#8552a1', '#6c4c49', '#ed1941']
legendList.forEach((ele, index) => {
if (index < 5) {
seriesList.push({
type: 'line',
name: ele,
data: [],
yAxisIndex: 0,
xAxisIndex: 0,
connectNulls: false,
symbol: 'none',
symbolSize: 0
})
} else {
seriesList.push({
type: 'bar',
name: ele,
data: [],
emphasis: {
focus: 'series'
},
yAxisIndex: 1,
xAxisIndex: 1,
showEmptyData: false,
barWidth: 10
})
}
})
data.forEach((ele: any) => {
sysDate.push(ele.sysMonth)
seriesList[0].data.push(ele.meetingNum34 + ele.meetingNum7)
seriesList[1].data.push(ele.meetingNum5 + ele.meetingNum6)
seriesList[2].data.push(ele.linkNum31)
seriesList[3].data.push(ele.linkNum32)
seriesList[4].data.push(ele.linkNum34)
seriesList[5].data.push(ele.tradeMoney)
})
const xAxis = {
type: 'category',
name: '',
data: [],
show: true,
axisLabel: {
show: true,
showMaxLabel: true,
formatter: '{value}'
},
gridIndex: 0
}
const yAxis = {
type: 'value',
name: '',
nameLocation: 'end',
scale: true,
show: true,
axisLabel: {
show: true,
formatter: '{value}'
},
axisLine: {
show: true,
lineStyle: {
color: data.color || '#000'
}
},
position: 'left',
}
const option = {
yAxis: [{ ...yAxis, name: '次数', minInterval: 1 }, { ...yAxis, name: '交易数', minInterval: 1, nameLocation: 'start', gridIndex: 1 }],
xAxis: [{ ...xAxis, data: sysDate, name: '时间' }, { ...xAxis, data: sysDate, showFlag: false, gridIndex: 1 }],
series: seriesList,
tooltip: {
show: true,
trigger: 'axis',
formatter: function (data: any) {
let res = `${data[0].name}</br>`
data.forEach((ele: any) => {
res += `<span style="color: ${ele.color}">${ele.seriesName}:${ele.value}${ele.componentSubType === 'bar' ? '百万' : ''}</span></br>`
})
return res
}
},
legend: { data: legendList, top: '10px' },
color: colorList,
grid: [{
top: '84px',
bottom: '35%',
left: '40px',
right: '40px'
}, {
top: '72%',
bottom: '40px',
left: '40px',
right: '40px'
}]
}
nextTick(() => {
const chart = echarts.init(document.getElementById('meeting_chart'))
chart.setOption(option)
chartLoading.value = false
})
}
const cusInfo = ref<any>({})
const getCusInfo = async () => {
const { data } = await cusInfoInfo(cusId.value)
cusInfo.value = data
emitter.emit('setTitle', { title: cusInfo.value.cusName, type: 'meeting' })
}
const cusUserList = ref<any[]>([])
const getCusUserList = async () => {
const { data } = await cusUserListByCusId({ cusId: cusId.value })
cusUserList.value = data
}
const workList = ref<any[]>()
const getWorkList = async () => {
const url = convertToUrl({
limit: 50,
cusId: cusId.value,
requestFrom: 'DETAIL_RB'
})
const data = await cusLinkList(url)
workList.value = data
}
const communicateList = ref<any[]>()
const getCusLinkList = async () => {
const url = convertToUrl({
limit: 100,
cusId: cusId.value,
requestFrom: 'DETAIL_GTJL',
dataStatus: 1
})
const data = await cusLinkList(url)
communicateList.value = data
}
const meetingList = ref<any[]>()
const getMeetingList = async () => {
const { data } = await cusMeetingList({ cusId: cusId.value, meetingType: '' })
meetingList.value = data
}
const serviceList = ref<any[]>()
const getServiceList = async () => {
const { data } = await cusServiceGetByCusId({ cusId: cusId.value })
data.forEach((ele: any) => {
ele.selectId = ele.id
ele.serviceStatus = ele.serviceStatus === 0 ? '历史' : '当前'
})
serviceList.value = data
}
const yearList = ref<any[]>()
const getYearList = async () => {
const { data } = await cusYearList({ cusId: cusId.value, curPage: 1, limit: 999 })
yearList.value = data.list
}
const accountList = ref<any[]>()
const getAccountList = async () => {
const { data } = await cusAccountList({ cusId: cusId.value, curPage: 1, limit: 999 })
accountList.value = data.list
}
init()
const changeTab = (num: number) => {
if (num === 0) {
getChartList()
}
}
</script>
<style lang="scss" scoped>
$border-color: #e0e0e0;
.base-info {
overflow: hidden;
border-radius: 4px;
border: 1px solid $border-color;
.base-item {
font-size: 13px;
border-bottom: 1px solid $border-color;
&:last-child {
border-bottom: 0;
}
}
.base-title {
width: 30%;
border-right: 1px solid $border-color;
padding: 6px 10px;
background-color: #fafafa;
}
.base-desc {
background-color: #f2f4f7;
padding: 6px 10px;
width: 70%;
color: #1989fa;
}
}
.chartDiv {
height: 50vh;
width: 100vh;
}
.title {
margin-top: 20px;
font-size: 14px;
font-weight: 600;
}
:deep(.van-tab--active) {
.van-tab__text {
color: #1989fa;
}
}
.w-half {
width: 50%;
}
.wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
.block {
width: 80%;
height: 50%;
overflow: auto;
padding: 10px;
font-size: 14px;
background-color: #fff;
}
}
</style>

View File

@@ -0,0 +1,112 @@
<template>
<div class="page-content top-select-list" ref="pageContent">
<div class="top-select border-bottom-1">
<van-dropdown-menu>
<van-dropdown-item :title="searchContent" ref="itemRef">
<van-field v-model="search" placeholder="搜索" :right-icon="search ? 'clear' : ''"
@click-right-icon.stop="clearSearch" />
<div style="padding: 5px 16px;">
<van-button type="primary" size="small" block round @click="onConfirm">
确认
</van-button>
</div>
</van-dropdown-item>
<van-dropdown-item v-model="companyType" :options="companyTypeList" @change="onRefresh" />
<van-dropdown-item v-model="cusLevel" :options="cusLevelList" @change="onRefresh" />
</van-dropdown-menu>
</div>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh" v-if="list.length > 0">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad"
:immediate-check="false">
<van-cell v-for="item in list" :key="item.cusId"
@click="$router.push({ name: 'organization-detail', query: { cusId: item.cusId } })">
<div class="name1 text-start stockNames balck-text-color">{{ item.cusName }}</div>
<div class="d-flex justify-content-between">
<div class="font-12">负责人{{ item.saleUserName }}</div>
<div class="name1 text-start flex1">
<van-tag :style="{ backgroundColor: item.cusLevelColor}" size="medium">{{ item.companyTypeName }}</van-tag>
<van-tag class="tag" size="medium">{{ item.cusLevelName }}</van-tag>
</div>
</div>
</van-cell>
</van-list>
</van-pull-refresh>
<van-empty v-else />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { cusInfoList, sysDicListByType } from '@/utils/api'
let searchContent = ref('搜索')
let itemRef = ref()
const clearSearch = () => {
search.value = ''
searchContent.value = '搜索'
}
const onConfirm = () => {
searchContent.value = search.value || '搜索'
itemRef.value.toggle()
onRefresh()
}
const getData = async () => {
const { data } = await cusInfoList({
companyType: companyType.value || null,
cusLevel: cusLevel.value || null,
cusName: search.value,
curPage: curPage.value,
limit: 20
})
console.log(data);
return data
}
const companyType = ref('')
const cusLevel = ref('')
const search = ref('')
const companyTypeList = ref<any[]>([])
const cusLevelList = ref<any[]>([])
const getDicList = async () => {
const { data } = await sysDicListByType({ dicType: 'sale_company_type' })
companyTypeList.value = data.map((ele: any) => {
ele.text = ele.dicValue
ele.value = ele.dicKey
return ele
})
companyTypeList.value.unshift({ text: '全部类型', value: '' })
const { data: data1 } = await sysDicListByType({ dicType: 'sale_customer_type' })
cusLevelList.value = data1.map((ele: any) => {
ele.text = ele.dicValue
ele.value = ele.dicKey
return ele
})
cusLevelList.value.unshift({ text: '全部评级', value: '' })
}
getDicList()
import { listLoadAndRefresh } from '@/mixins/list-load-and-refresh'
const { refreshing, finished, loading, curPage, list, onLoad, onRefresh } = listLoadAndRefresh(getData, 'organization')
import { scrollList } from '@/mixins/scroll-list'
const pageContent = ref()
const scrollPosition = ref(0)
const { setScrollTop, setScrollPositionAndRefreshFlag } = scrollList(pageContent, scrollPosition, onRefresh)
setScrollTop()
import { onBeforeRouteLeave } from 'vue-router'
onBeforeRouteLeave((to, from) => {
setScrollPositionAndRefreshFlag(to, 'organization-detail')
})
</script>
<style lang='scss' scoped>
.top-select-list {
padding-top: 60px;
}
.item {
width: 100%;
text-align: center;
}
.tag {
margin-left: 10px;
background-color: #ff758f;
}
</style>

267
src/views/report/list.vue Normal file
View File

@@ -0,0 +1,267 @@
<template>
<div class="page-content top-select-list">
<div class="top-select border-bottom-1">
<van-grid :border="false" :gutter="10" :column-num="3">
<van-grid-item>
<top-select1 :type="'report_type'" :initialName="'类型'" @refresh="refreshByReportType"
:selectName="reportTypeName" :selectValue="reportType" />
</van-grid-item>
<van-grid-item>
<top-date1 :initialName="'日期'" @refresh="refreshByDate" :selectDate="date" />
</van-grid-item>
<van-grid-item>
<top-select2 :type="'stock'" :initialName="'股票'" @refresh="refreshByStock" :selectName="stockName"
:selectValue="stockCode" />
</van-grid-item>
</van-grid>
</div>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh" v-if="list.length > 0">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad"
:immediate-check="false">
<van-cell class="cell" v-for="(item, index) in list" :key="index" :id="item.reportId"
@click="showDetail(item, index)">
<div class="img-box">
<van-image :src="item.url" class="img"></van-image>
<div class="title">{{ item.reportName }}</div>
</div>
<div class="road-info flex-1 text-start d-flex justify-content-between">
<div class="title">{{ item.reportName }}</div>
<div class="flex-1">
<div>
<span>{{ item.stockName }}</span>
<span class="line">|</span>
<van-tag size="medium" :class="item.reportColor" class="white-text-color">
{{ item.reportTypeName }}
</van-tag>
</div>
<div class="report-desc">
<div class="detail text-ellipsis" v-for="(row, idx) in item.reportDescList" :key="idx">{{ row }}</div>
</div>
</div>
<div class="report-time">{{ item.reportDate }}</div>
</div>
</van-cell>
</van-list>
</van-pull-refresh>
<van-empty v-else />
</div>
</template>
<script setup lang="ts">
import { ref, nextTick, onUnmounted } from 'vue'
import TopSelect1 from '@/components/top-select1.vue'
import TopSelect2 from '@/components/top-select2.vue'
import TopDate1 from '@/components/top-date1.vue'
import emitter from '@/utils/mitt'
import { stockReportList } from '@/utils/api'
import { listLoadAndRefresh } from '@/mixins/list-load-and-refresh'
const getData = async () => {
const { data } = await stockReportList(`${reportType.value ? '?reportType=' + reportType.value : ''}`, {
curPage: curPage.value,
limit: 20,
stockCode: stockCode.value || null,
reportDate: date.value !== '日期' ? date.value : null
})
data.list.map((ele: any) => {
ele.url = `/img/${ele.reportType}.jpeg`
ele.reportDescList = ele.reportTitle?.split('') || []
ele.reportName = ele.reportName
? ele.reportName.substring(0, ele.reportName.length - 4)
: ''
switch (ele.reportType) {
case 'SFBG':
ele.reportColor = 'primary-background-color'
break
case 'GXBG':
ele.reportColor = 'danger-background-color'
break
case 'XLL':
ele.reportColor = 'success-background-color'
break
case 'CLBG':
ele.reportColor = 'warning-background-color'
break
}
})
return data
}
import reportInfoStore from '@/stores/reportInfo'
const reportInfo = reportInfoStore()
const scrollPage = () => {
nextTick(() => {
if (reportInfo.reportId && reportInfo.index) {
stockCode.value = reportInfo.stockCode
stockName.value = reportInfo.stockName
date.value = reportInfo.date
reportTypeName.value = reportInfo.reportTypeName
reportType.value = reportInfo.reportType
let pageIndex1 = reportInfo.index / 20
let pageIndex2 = reportInfo.index % 20
if (pageIndex2 > 1) {
pageIndex1++
}
if (pageIndex1 > curPage.value) {
onLoad()
} else {
const el = document.getElementById(reportInfo.reportId)
if (el) {
el.scrollIntoView({ behavior: 'smooth' })
}
reportInfo.reportId = ''
reportInfo.index = 0
emitter.emit('hiddenLoading', '')
}
} else {
emitter.emit('hiddenLoading', '')
}
})
}
const { refreshing, finished, loading, list, onLoad, onRefresh, curPage } = listLoadAndRefresh(getData, 'report', scrollPage)
import { showFile } from '@/mixins/show-file'
import userInfoStore from '@/stores/userInfo'
const { loadingFile } = showFile()
const showDetail = (item: any, index: number) => {
reportInfo.index = index
reportInfo.reportId = item.reportId
reportInfo.stockCode = stockCode.value
reportInfo.stockName = stockName.value
reportInfo.date = date.value
reportInfo.reportTypeName = reportTypeName.value
reportInfo.reportType = reportType.value
const userInfo = userInfoStore()
loadingFile(`${import.meta.env.VITE_BASE_URL}/stock/report/${item.reportId}?token=${userInfo.token}`)
}
let reportType = ref('')
let reportTypeName = ref('类型')
let date = ref('日期')
let stockCode = ref('')
let stockName = ref('股票')
const refreshByReportType = (e: any) => {
reportType.value = e.value
reportTypeName.value = e.name
onRefresh()
}
const refreshByDate = (e: any) => {
date.value = e
onRefresh()
}
const refreshByStock = (e: any) => {
stockCode.value = e.value
stockName.value = e.name
onRefresh()
}
const onRefresh1 = () => {
stockCode.value = reportInfo.stockCode
stockName.value = reportInfo.stockName
date.value = reportInfo.date
reportTypeName.value = reportInfo.reportTypeName
reportType.value = reportInfo.reportType
onRefresh()
}
onRefresh1()
onUnmounted(() => {
reportInfo.stockCode = ''
reportInfo.stockName = '股票'
reportInfo.date = '日期'
reportInfo.reportTypeName = '类型'
reportInfo.reportType = ''
})
</script>
<style lang='scss' scoped>
.van-cell {
width: 100%;
max-width: 678px;
margin: 0;
box-shadow: 0 0;
}
.cell {
:deep(.van-cell__value) {
display: flex;
}
.img-box {
width: 30%;
max-width: 100PX;
max-height: 130PX;
position: relative;
.img {
width: 100%;
height: 100%;
}
.title {
position: absolute;
top: 50%;
transform: translateY(-50%);
padding: 10px;
zoom: 0.8;
height: 40px;
overflow: hidden;
color: #fff;
text-align: center;
font-size: 12px;
}
}
.road-info {
margin-left: 10px;
flex-direction: column;
width: 70%;
.report-time {
font-size: 12px;
line-height: 16px;
}
.title {
color: #000;
font-weight: 600;
}
.report-desc {
color: #999;
height: 48px;
margin-top: 6px;
overflow: hidden;
font-size: 12px;
.detail {
line-height: 16px;
height: 16px;
padding-left: 8px;
position: relative;
&::before {
content: '';
width: 4px;
height: 4px;
border-radius: 50%;
background-color: #999;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
}
}
}
.line {
margin: 0 10px;
}
}
}
</style>

189
src/views/stock/detail.vue Normal file
View File

@@ -0,0 +1,189 @@
<template>
<div class="page-content">
<div class="stock-detail" ref="pageContent">
<van-steps direction="vertical" :active="-1" v-if="eventList.length > 0" @click-step="showInfo">
<van-step v-for="item in eventList" :key="item.id" :id="item.id">
<template v-slot:inactive-icon>
<div style="background-color: #000" class="slotIcon"></div>
</template>
<div class="stepItem">
<van-row class="mb-16">
<van-col span="12">
<van-tag size="medium" :stlye="{ backgroundColor: item.eventTypeColor }">
{{ item.eventTypeName }}
</van-tag>
</van-col>
<van-col span="12">
<div class="text-end tip">{{ item.sysDate }}</div>
</van-col>
</van-row>
<div v-if="item.eventType === 'CHDP'">
<div v-if="item.eventNote">
<div v-if="!item.playFlag" class="audioDiv d-flex align-items-center">
<van-icon name="play-circle" @click="playAudio(item)" />
</div>
<audio v-else :id="`auto${item.id}`" :src="item.eventNote" controls @play="playAudio(item)"></audio>
</div>
</div>
<div class="eventNote" v-else>{{ item.eventNote }}</div>
</div>
</van-step>
</van-steps>
<van-empty v-else />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import emitter from '@/utils/mitt'
const route = useRoute()
const router = useRouter()
import stockInfoStore from '@/stores/stockInfo'
const stockInfo = stockInfoStore()
let stockCode: any = ref('')
const init = () => {
if (route.query && route.query.stockCode) {
stockCode.value = route.query.stockCode
emitter.emit('showLoading', '')
getStockEventList()
} else {
router.push({ name: '404' })
}
}
let eventList: any = ref([])
import { stockStockinfoEventList } from '@/utils/api'
const getStockEventList = async () => {
const { data }: any = await stockStockinfoEventList({
stockCode: stockCode.value
})
data.map((ele: any) => {
ele.playFlag = false
})
eventList.value = data
nextTick(() => {
if (stockInfo.id) {
const el = document.getElementById(stockInfo.id)
if (el) {
el.scrollIntoView({ behavior: 'smooth' })
}
stockInfo.id = ''
emitter.emit('hiddenLoading', '')
} else {
emitter.emit('hiddenLoading', '')
}
})
}
init()
import { showFile } from '@/mixins/show-file'
const { loadingFile } = showFile()
const showInfo = (e: any) => {
if (eventList.value[e].eventType === 'DP') {
router.push({ name: 'comment-detail', query: { inboxId: eventList.value[e].inboxId } })
} else if (eventList.value[e].eventType === 'UPDATE' || eventList.value[e].eventType === 'FIRST') {
stockInfo.id = eventList.value[e].id
loadingFile(`${import.meta.env.VITE_BASE_URL}/api/file/${eventList.value[e].reportFileId}`)
}
}
import { scrollList } from '@/mixins/scroll-list'
import { onBeforeRouteLeave } from 'vue-router'
let pageContent = ref(null)
const scrollPosition = ref(0)
const refresh = () => {
init()
}
const { setScrollTop, setScrollPositionAndRefreshFlag } = scrollList(pageContent, scrollPosition, refresh)
setScrollTop()
onBeforeRouteLeave((to, from) => {
setScrollPositionAndRefreshFlag(to, 'comment-detail')
})
const playAudio = (item: any) => {
eventList.value.map((ele: any) => {
if (ele.eventType === 'CHDP' && ele.eventNote) {
if (item.id === ele.id) {
ele.playFlag = true
nextTick(() => {
const el = document.getElementById(`auto${item.id}`)
if (el) {
el.play()
}
})
} else {
if (ele.playFlag) {
const el = document.getElementById(`auto${item.id}`)
if (el) {
el.pause()
}
ele.playFlag = false
}
}
}
})
}
</script>
<style lang='scss' scoped>
.stock-detail {
height: 100vh;
overflow-y: auto;
}
:deep(.van-step__circle-container) {
top: 29px
}
.van-steps--vertical {
padding-left: 48px;
}
.van-steps__items {
padding-right: 32px
}
.slotIcon {
height: 14px;
width: 14px;
border-radius: 100%;
}
.stepItem {
padding: 8px 16px;
}
.van-step--vertical:not(:last-child):after {
border-color: #b2b2b2;
margin-right: 46px;
margin-left: 16px;
}
.eventNote {
font-weight: 400;
font-size: 14px;
line-height: 20px;
}
.audioDiv {
height: 28px;
width: 300px;
border-radius: 32px;
background-color: #f1f3f4;
padding-left: 10px;
margin: 4px 0;
.van-icon {
color: #000;
}
}
audio {
height: 28px;
margin: 4px 0;
}
</style>

121
src/views/stock/list.vue Normal file
View File

@@ -0,0 +1,121 @@
<template>
<div class="page-content">
<div class="tabbar-list" ref="pageContent">
<van-pull-refresh v-model="refreshing" @refresh="onRefresh" v-if="list.length > 0">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad"
:immediate-check="false">
<van-cell v-for="item in list" :key="item.stockCode"
@click="$router.push({ name: 'stock-detail', query: { stockCode: item.stockCode } })">
<van-row class="mb-1">
<van-col span="12">
<div class="text-start">
<van-tag :class="item.eventTypeColor" size="medium">{{ item.eventTypeName }}</van-tag>
</div>
</van-col>
<van-col span="12">
<div class="text-end">
<div class="sub-title balck-text-color">
{{ item.stockCode }}{{ item.stockName ? '【' + item.stockName + '】' : '' }}
</div>
</div>
</van-col>
</van-row>
<div v-if="item.eventType === 'CHDP'">
<div v-if="item.eventNote">
<div class="audioDiv d-flex align-items-center">
<van-icon name="play-circle" />
</div>
</div>
</div>
<div class="eventNote text-start" v-else>{{ item.eventNote }}</div>
<div class="text-end tip">{{ item.sysDate }}</div>
</van-cell>
</van-list>
</van-pull-refresh>
<van-empty v-else />
<tabbar />
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { listLoadAndRefresh } from '@/mixins/list-load-and-refresh'
import { scrollList } from '@/mixins/scroll-list'
import { onBeforeRouteLeave } from 'vue-router'
import Tabbar from '@/components/tabbar.vue'
let pageContent = ref(null)
const scrollPosition = ref(0)
const refresh = () => {
onRefresh()
}
import { stockStockinfoEventNews } from '@/utils/api'
const getData = async () => {
const { data } = await stockStockinfoEventNews({
curPage: curPage.value,
limit: 20
})
data.list.map((ele: any) => {
switch (ele.eventType) {
case 'DP':
ele.eventTypeColor = 'primary-background-color'
break
case 'SFBG':
ele.eventTypeColor = 'danger-background-color'
break
case 'GXBG':
ele.eventTypeColor = 'warning-background-color'
break
case 'XLL':
ele.eventTypeColor = 'pink-background-color'
break
case 'CLBG':
ele.eventTypeColor = 'other-background-color'
break
case 'JOIN':
ele.eventTypeColor = 'success-background-color'
break
case 'CHDP':
ele.eventTypeColor = 'purple-background-color'
break
case 'DATA':
ele.eventTypeColor = 'yellow-background-color'
break
}
})
return data
}
const { refreshing, finished, loading, curPage, list, onLoad, onRefresh } = listLoadAndRefresh(getData, 'stock')
const { setScrollTop, setScrollPositionAndRefreshFlag } = scrollList(pageContent, scrollPosition, refresh)
setScrollTop()
onBeforeRouteLeave((to, from) => {
setScrollPositionAndRefreshFlag(to, 'stock-detail')
})
</script>
<style lang='scss' scoped>
.eventNote {
font-weight: 400;
font-size: 14px;
line-height: 20px;
}
.audioDiv {
height: 28px;
width: 300px;
border-radius: 32px;
background-color: #f1f3f4;
padding-left: 10px;
margin: 4px 0;
.van-icon {
color: #000;
}
}
</style>

View File

@@ -0,0 +1,770 @@
<template>
<div class="page-div">
<back-button />
<div class="header">
<div class="header-content">
<h1 class="title">{{ economyName }}</h1>
</div>
</div>
<div class="info-card" v-for="item in chartList" :key="item.id">
<div class="card-title">{{ item.name }}</div>
<div class="chart-container">
<van-loading v-show="item.loading" type="spinner" color="#1989fa" class="loading" />
<div :id="`${item.id}`" class="chartDiv"></div>
</div>
<div class="value-list-container">
<div v-for="(res, index) in item.valueList" :key="index" class="value-item">
<span class="value-name">{{ res.name }}:</span>
<span class="value-value">{{ res.value }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang='ts'>
import BackButton from '@/components/back-button.vue'
import moment from 'moment'
import { ref, onMounted, nextTick } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const economyName = ref('')
let chartList = ref<any[]>([])
import { chartMixins } from '@/mixins/chart-mixins'
const { charts, destroyedFlag } = chartMixins()
const init = async () => {
const id = route.params.id + ''
if (id === '1') {
economyName.value = '储蓄专题'
charts.value = [null, null, null, null, null, null, null]
chartList.value = [{
id: 'yoyReduce',
loading: true,
name: '储蓄增速VSM2 Spread(%)',
valueList: []
}, {
id: 'deposit1',
loading: true,
name: '沪深市值/住户存款(%)',
valueList: []
}, {
id: 'deposit2',
loading: true,
name: '(沪深京市值+港股通南向余额)/住户存款(%)',
valueList: []
}, {
id: 'deposit',
loading: true,
name: '住户存款(万亿)',
valueList: []
}, {
id: 'depositYoy',
loading: true,
name: '居民存储同比增速(%)',
valueList: []
}, {
id: 'm2',
loading: true,
name: '货币和准货币(M2)(万亿)',
valueList: []
}, {
id: 'm2Yoy',
loading: true,
name: '货币和准货币(M2)增速Yoy(%)',
valueList: []
}]
} else if (id === '2') {
economyName.value = '两融专题'
charts.value = [null, null, null]
chartList.value = [{
id: 'twoFinancingData',
loading: true,
name: '两融数据',
valueList: []
}, {
id: 'twoFinancingRatiosHs',
loading: true,
name: '两融比沪深(%)',
valueList: []
}, {
id: 'twoFinancingInflow',
loading: true,
name: '两融净流入(亿)',
valueList: []
}]
} else if (id === '3') {
economyName.value = 'A股专题'
charts.value = [null, null, null, null, null, null]
chartList.value = [{
id: 'totalShares',
loading: true,
name: '发行总股本(万亿)',
valueList: []
}, {
id: 'totalMarket',
loading: true,
name: '总市值(万亿)',
valueList: []
}, {
id: 'dealAmount',
loading: true,
name: '月成交金额(万亿)',
valueList: []
}, {
id: 'turnoverRate',
loading: true,
name: '换手率(%)',
valueList: []
}, {
id: 'volume',
loading: true,
name: '月成交量(万亿)',
valueList: []
}, {
id: 'szNum',
loading: true,
name: '月开户数量(万)',
valueList: []
}]
} else if (id === '4') {
economyName.value = '指数专题'
charts.value = [null, null, null, null]
chartList.value = [{
id: 'hsIndex',
loading: true,
name: 'Hang Seng Index and Sub-indexes (HSI)(派息率)',
valueList: []
}, {
id: 'hs300',
loading: true,
name: '沪深300',
valueList: []
}, {
id: 'zz500',
loading: true,
name: '中证500',
valueList: []
}, {
id: 'zz1000',
loading: true,
name: '中证1000',
valueList: []
}]
}
if (id === '1' || id === '2' || id === '3' || id === '4') {
nextTick(() => {
getChartData()
})
}
}
import * as echarts from 'echarts'
import { getLineSeries, getBarSeries, getBaseOption, getXAxis, getYAxis } from '@/utils/chart'
import {
reportEconomyMonthDepositVsM2, reportEconomyMonthMarketHSVsDeposit, reportEconomyMonthMarketHSJVsDeposit,
reportEconomyMonthListByItem, stockStockrzrqQueryRzrq, stockStockrzrqQueryRzToTotalMarket, stockStockrzrqQueryRzjme,
reportEconomyMonthStockShares, reportEconomyMonthStockMarket, reportEconomyMonthStockAmount,
reportEconomyMonthStockTurnoverRate, reportEconomyMonthStockVolume, reportEconomyMonthNewAccountNum, stockStockHsList
} from '@/utils/api'
const getChartData = async () => {
const id = route.params.id + ''
if (id === '1') {
const { data: data0 } = await reportEconomyMonthDepositVsM2()
const sysDate0: any = []
const seriesList0: any = [getLineSeries({ name: '储蓄增速VSM2 Spread' }), getLineSeries({ name: '储蓄增速VSM2 Spread前三月均值' }), getLineSeries({ name: '储蓄增速VSM2 Spread后三月均值' })]
data0.map((ele: any) => {
sysDate0.push(ele.sysDate)
seriesList0[0].data.push({ value: ele.depositYoy && ele.m2Yoy ? Math.round((ele.depositYoy - ele.m2Yoy) * 10000) / 100 : null })
seriesList0[1].data.push({ value: ele.before3 ? Math.round(ele.before3 * 100) / 100 : null })
seriesList0[2].data.push({ value: ele.after3 ? Math.round(ele.after3 * 100) / 100 : null })
})
const tooltipFormatter0 = function (data: any) {
let backText = `日期:${data[0].name}<br/>`
data.forEach((ele: any) => {
backText += `<span style='color: ${ele.color}'>${ele.seriesName}</span>`
if (ele.value > 0) {
backText += `<span style='color: red'>${ele.value ? ele.value + '%' : ''}</span><br/>`
} else if (ele.value < 0) {
backText += `<span style='color: green'>${ele.value ? ele.value + '%' : ''}</span><br/>`
} else {
backText += `<span>${ele.value ? ele.value + '%' : ''}</span><br/>`
}
})
return backText
}
chartList.value[0].valueList = [
{ name: '日期', value: sysDate0[sysDate0.length - 1] },
{ name: '储蓄增速VSM2 Spread', value: seriesList0[0].data[seriesList0[0].data.length - 1].value + '%' },
{ name: '储蓄增速VSM2 Spread前三月均值', value: seriesList0[1].data[seriesList0[1].data.length - 1].value + '%' },
{ name: '储蓄增速VSM2 Spread后三月均值', value: seriesList0[2].data[seriesList0[2].data.length - 1].value ? seriesList0[2].data[seriesList0[2].data.length - 1].value + '%' : '' }
]
setChartOption(
0,
[getYAxis({ })],
sysDate0,
seriesList0,
tooltipFormatter0,
{ data: ['储蓄增速VSM2 Spread', '储蓄增速VSM2 Spread前三月均值', '储蓄增速VSM2 Spread后三月均值'], top: '10px' },
[{ top: '80px', bottom: '20px', left: '40px', right: '40px' }]
)
const { data: data1 } = await reportEconomyMonthMarketHSVsDeposit()
const chartData1 = getChartData2(data1, 1)
chartList.value[1].valueList = [
{ name: '日期', value: chartData1.sysDate[chartData1.sysDate.length - 1] },
{ name: '总市值', value: chartData1.seriesList[0].data[chartData1.seriesList[0].data.length - 1].totalMarket + '万亿' },
{ name: '住房存款', value: chartData1.seriesList[0].data[chartData1.seriesList[0].data.length - 1].deposit + '万亿' },
{ name: '比例', value: chartData1.seriesList[0].data[chartData1.seriesList[0].data.length - 1].value + '%' }
]
setChartOption(1, [getYAxis({ })], chartData1.sysDate, chartData1.seriesList, chartData1.tooltipFormatter)
const { data: data2 } = await reportEconomyMonthMarketHSJVsDeposit()
const chartData2 = getChartData2(data2, 2)
chartList.value[2].valueList = [
{ name: '日期', value: chartData2.sysDate[chartData2.sysDate.length - 1] },
{ name: '总市值', value: chartData2.seriesList[0].data[chartData2.seriesList[0].data.length - 1].totalMarket + '万亿' },
{ name: '住房存款', value: chartData2.seriesList[0].data[chartData2.seriesList[0].data.length - 1].deposit + '万亿' },
{ name: '南方余额', value: chartData2.seriesList[0].data[chartData2.seriesList[0].data.length - 1].southBalance + '亿' },
{ name: '比例', value: chartData2.seriesList[0].data[chartData2.seriesList[0].data.length - 1].value + '%' }
]
setChartOption(2, [getYAxis({ })], chartData2.sysDate, chartData2.seriesList, chartData2.tooltipFormatter)
const chartData3: any = await getChartData3('deposit', '万亿', 10000)
chartList.value[3].valueList = [
{ name: '日期', value: chartData3.sysDate[chartData3.sysDate.length - 1] },
{ name: '住户存款', value: chartData3.seriesList[0].data[chartData3.seriesList[0].data.length - 1].value + '万亿' }
]
setChartOption(3, [getYAxis({ })], chartData3.sysDate, chartData3.seriesList, chartData3.tooltipFormatter)
const chartData4: any = await getChartData3('deposit_yoy', '%', 100)
chartList.value[4].valueList = [
{ name: '日期', value: chartData4.sysDate[chartData4.sysDate.length - 1] },
{ name: '居民存储同比增速', value: chartData4.seriesList[0].data[chartData4.seriesList[0].data.length - 1].value + '%' }
]
setChartOption(4, [getYAxis({ })], chartData4.sysDate, chartData4.seriesList, chartData4.tooltipFormatter)
const chartData5: any = await getChartData3('m2', '万亿', 10000)
chartList.value[5].valueList = [
{ name: '日期', value: chartData5.sysDate[chartData5.sysDate.length - 1] },
{ name: '货币和准货币(M2)', value: chartData5.seriesList[0].data[chartData5.seriesList[0].data.length - 1].value + '万亿' }
]
setChartOption(5, [getYAxis({ })], chartData5.sysDate, chartData5.seriesList, chartData5.tooltipFormatter)
const chartData6: any = await getChartData3('m2_yoy', '%', 100)
chartList.value[6].valueList = [
{ name: '日期', value: chartData6.sysDate[chartData6.sysDate.length - 1] },
{ name: '货币和准货币(M2)增速Yoy', value: chartData6.seriesList[0].data[chartData6.seriesList[0].data.length - 1].value + '%' }
]
setChartOption(6, [getYAxis({ })], chartData6.sysDate, chartData6.seriesList, chartData6.tooltipFormatter)
} else if (id === '2') {
const { data: data0 } = await stockStockrzrqQueryRzrq()
const sysDate0: any = []
const seriesList0: any = [
getLineSeries({ name: '沪深300', yAxisIndex: 2 }),
getLineSeries({ name: '中证100', yAxisIndex: 2 }),
getLineSeries({ name: '中证500', yAxisIndex: 2 }),
getLineSeries({ name: '中证1000', yAxisIndex: 2 }),
getLineSeries({ name: '沪深股票数', yAxisIndex: 2 }),
getLineSeries({ name: '沪深总市值', yAxisIndex: 1 }),
getLineSeries({ name: '沪深总股数', yAxisIndex: 1 }),
getLineSeries({ name: '融资余额' }),
getLineSeries({ name: '较去年融资增长率', yAxisIndex: 3 }),
getLineSeries({ name: '去年平均融资额' })
]
data0.map((ele: any) => {
sysDate0.push(ele.sysDate)
seriesList0[0].data.push({ value: ele.hs300 ? Math.round(ele.hs300 * 100) / 100 : null })
seriesList0[1].data.push({ value: ele.zz100 ? Math.round(ele.zz100 * 100) / 100 : null })
seriesList0[2].data.push({ value: ele.zz500 ? Math.round(ele.zz500 * 100) / 100 : null })
seriesList0[3].data.push({ value: ele.zz1000 ? Math.round(ele.zz1000 * 100) / 100 : null })
seriesList0[4].data.push({ value: ele.totalStock ? Math.round(ele.totalStock * 100) / 100 : null })
seriesList0[5].data.push({ value: ele.totalMarket ? Math.round(ele.totalMarket / 1000000) / 100 : null })
seriesList0[6].data.push({ value: ele.totalShares ? Math.round(ele.totalShares / 100000) / 100 : null })
seriesList0[7].data.push({ value: ele.rzye ? Math.round(ele.rzye / 100) / 100 : null })
seriesList0[8].data.push({ value: ele.rzyeLastYearAvg ? Math.round((ele.rzye - ele.rzyeLastYearAvg) / ele.rzyeLastYearAvg * 10000) / 100 : null })
seriesList0[9].data.push({ value: ele.rzyeLastYearAvg ? Math.round(ele.rzyeLastYearAvg / 100) / 100 : null })
})
const tooltipFormatter0 = function (data: any) {
let backText = `日期:${data[0].name}<br/>`
data.forEach((ele: any) => {
let value = ele.value
if (ele.seriesName === '沪深总股数') {
value = ele.value ? Math.round(ele.value * 10) / 100 + '万亿' : ''
} else if (ele.seriesName === '沪深总市值' || ele.seriesName === '融资余额' || ele.seriesName === '去年平均融资额') {
value = ele.value ? ele.value + '万亿' : ''
} else if (ele.seriesName === '较去年融资增长率') {
value = ele.value ? ele.value + '%' : ''
}
backText += `<div><span style="color: ${ele.color}">${ele.seriesName}</span>:${value}</div>`
})
return backText
}
chartList.value[0].valueList = [
{ name: '日期', value: sysDate0[sysDate0.length - 1] },
{ name: '沪深300', value: seriesList0[0].data[seriesList0[0].data.length - 1].value },
{ name: '中证100', value: seriesList0[1].data[seriesList0[1].data.length - 1].value },
{ name: '中证500', value: seriesList0[2].data[seriesList0[2].data.length - 1].value },
{ name: '中证1000', value: seriesList0[3].data[seriesList0[3].data.length - 1].value },
{ name: '沪深股票数', value: seriesList0[4].data[seriesList0[4].data.length - 1].value },
{ name: '沪深总市值', value: seriesList0[5].data[seriesList0[5].data.length - 1].value + '万亿' },
{ name: '沪深总股数', value: seriesList0[6].data[seriesList0[6].data.length - 1].value ? Math.round(seriesList0[6].data[seriesList0[6].data.length - 1].value * 10) / 100 + '万亿' : '' },
{ name: '融资余额', value: seriesList0[7].data[seriesList0[7].data.length - 1].value + '万亿' },
{ name: '较去年融资增长率', value: seriesList0[8].data[seriesList0[8].data.length - 1].value + '%' },
{ name: '去年平均融资额', value: seriesList0[9].data[seriesList0[9].data.length - 1].value + '万亿' }
]
setChartOption(
0,
[getYAxis({ }), getYAxis({ showFlag: false, position: 'right' }), getYAxis({ showFlag: false, position: 'right' }), getYAxis({ showFlag: false, position: 'right' })],
sysDate0,
seriesList0,
tooltipFormatter0,
{
data: ['沪深300', '中证100', '中证500', '中证1000', '沪深股票数', '沪深总市值', '沪深总股数', '融资余额', '较去年融资增长率', '去年平均融资额'],
top: '10px',
selected: {
沪深300: false,
中证100: false,
中证500: false,
中证1000: false,
沪深股票数: false,
沪深总市值: false,
沪深总股数: false,
融资余额: true,
较去年融资增长率: true,
去年平均融资额: true
}
},
[{ top: '100px', bottom: '20px', left: '40px', right: '40px' }]
)
const { data: data1 } = await stockStockrzrqQueryRzToTotalMarket()
const sysDate1: any = []
const seriesList1: any = [
getLineSeries({ name: '比重' }),
getLineSeries({ name: '创业板指数', yAxisIndex: 1 }),
getLineSeries({ name: '沪深300', yAxisIndex: 1 }),
getLineSeries({ name: '融资融券余额', yAxisIndex: 2 })
]
data1.map((ele: any) => {
sysDate1.push(ele.sysDate)
seriesList1[0].data.push({
value: Math.round(ele.per * 100) / 100,
totalMarket: Math.round(ele.totalMarket / 1000000) / 100
})
seriesList1[1].data.push({ value: Math.round(ele.cybNav * 100) / 100 })
seriesList1[2].data.push({ value: Math.round(ele.hs300Nav * 100) / 100 })
seriesList1[3].data.push({ value: Math.round(ele.rzrqye / 100) / 100 })
})
const tooltipFormatter1 = function (data: any) {
let backText = `日期:${data[0].name}<br/>`
data.forEach((ele: any) => {
if (ele.value) {
if (ele.seriesName === '比重') {
backText += `<br/><span style="color: blue">比重</span>:<span style="color: red">${ele.value}%</span><br/>`
backText += `<span style="color: blue">沪深市值</span>:<span style="color: red">${ele.data.totalMarket ? ele.data.totalMarket + '万亿' : ''}</span><br/>`
} else if (ele.seriesName === '融资融券余额') {
backText += `<span style="color: blue">${ele.seriesName}</span>:<span style="color: red">${ele.value + '万亿'}</span><br/>`
} else {
backText += `<span style="color: blue">${ele.seriesName}</span>:<span style="color: red">${ele.value}</span><br/>`
}
}
})
return backText
}
chartList.value[1].valueList = [
{ name: '日期', value: sysDate1[sysDate1.length - 1] },
{ name: '沪深市值', value: seriesList1[0].data[seriesList1[0].data.length - 1].totalMarket + '万亿' },
{ name: '比重', value: seriesList1[0].data[seriesList1[0].data.length - 1].value + '%' },
{ name: '创业板指数', value: seriesList1[1].data[seriesList1[1].data.length - 1].value },
{ name: '沪深300', value: seriesList1[2].data[seriesList1[2].data.length - 1].value },
{ name: '融资融券余额', value: seriesList1[3].data[seriesList1[3].data.length - 1].value + '万亿' }
]
setChartOption(
1,
[getYAxis({ }), getYAxis({ position: 'right', showFlag: false }), getYAxis({ position: 'right', showFlag: false })],
sysDate1,
seriesList1,
tooltipFormatter1
)
const { data: data2 } = await stockStockrzrqQueryRzjme({
beginDate: moment().subtract(1, 'years').format('YYYY-MM-DD'),
endDate: moment().format('YYYY-MM-DD')
})
const sysDate2: any = []
const seriesList2: any = [
getBarSeries({ name: '两融净流入', labelFlag: false }),
getLineSeries({ name: '沪深300', yAxisIndex: 1 }),
]
let yMin1 = 0
let yMax1 = 0
let yMin2 = 0
let yMax2 = 0
let min = null
data2.map((ele: any, index: number) => {
sysDate2.push(ele.sys_date)
seriesList2[0].data.push({ value: ele.rzjme })
seriesList2[1].data.push({ value: ele.hs300 })
if (index === 0) {
yMin1 = ele.rzjme
yMax1 = ele.rzjme
} else {
if (ele.rzjme < yMin1) {
yMin1 = ele.rzjme
}
if (ele.rzjme > yMax1) {
yMax1 = ele.rzjme
}
}
if (index === 0) {
yMin2 = ele.hs300
yMax2 = ele.hs300
} else {
if (ele.hs300 < yMin2) {
yMin2 = ele.hs300
}
if (ele.hs300 > yMax2) {
yMax2 = ele.hs300
}
}
})
if (yMin1 < 0) {
yMax1 = Math.ceil(yMax1 / 100) * 100
yMax2 = Math.ceil(yMax2 / 100) * 100
yMin1 = Math.floor(yMin1 / 100) * 100
yMin2 = Math.floor(yMin2 / 100) * 100
min = yMin2 - (yMax2 - yMin2) * ((0 - yMin1) / (yMax1 - yMin1) + 0.1)
min = Math.floor(min / 100) * 100
}
const tooltipFormatter2 = function (data: any) {
let backText = `日期:${data[0].name}<br/>`
data.forEach((ele: any) => {
if (ele.value) {
backText += `<span style="color: blue">${ele.seriesName}</span><span style="color: red">${ele.value}${ele.seriesName === '两融净流入' ? '亿' : ''}</span><br/>`
}
})
return backText
}
chartList.value[2].valueList = [
{ name: '日期', value: sysDate2[sysDate2.length - 1] },
{ name: '两融净流入', value: seriesList2[0].data[seriesList2[0].data.length - 1].value + '亿' },
{ name: '沪深300', value: seriesList2[1].data[seriesList2[1].data.length - 1].value },
]
setChartOption(
2,
yMin1 < 0 ?
[getYAxis({ }), getYAxis({ position: 'right', showFlag: false, min: min, max: yMax2 })]:
[getYAxis({ }), getYAxis({ position: 'right', showFlag: false })],
sysDate2,
seriesList2,
tooltipFormatter2,
{ data: ['两融净流入', '沪深300'], top: '10px' },
[{ top: '40px', bottom: '20px', left: '40px', right: '40px' }]
)
} else if (id === '3') {
const { data: data0 } = await reportEconomyMonthStockShares()
const chartData0: any = getChartData4(data0, '万亿', ['totalShares'], 10000)
chartList.value[0].valueList = [
{ name: '日期', value: chartData0.sysDate[chartData0.sysDate.length - 1] },
{ name: '发行总股本', value: chartData0.seriesList[0].data[chartData0.seriesList[0].data.length - 1].value + '万亿' }
]
setChartOption(0, [getYAxis({ })], chartData0.sysDate, chartData0.seriesList, chartData0.tooltipFormatter)
const { data: data1 } = await reportEconomyMonthStockMarket()
const chartData1: any = getChartData4(data1, '万亿', ['totalMarket'], 10000)
chartList.value[1].valueList = [
{ name: '日期', value: chartData1.sysDate[chartData1.sysDate.length - 1] },
{ name: '总市值', value: chartData1.seriesList[0].data[chartData1.seriesList[0].data.length - 1].value + '万亿' }
]
setChartOption(1, [getYAxis({ })], chartData1.sysDate, chartData1.seriesList, chartData1.tooltipFormatter)
const { data: data2 } = await reportEconomyMonthStockAmount()
const chartData2: any = getChartData4(data2, '', ['dealAmount'], 10000)
chartList.value[2].valueList = [
{ name: '日期', value: chartData2.sysDate[chartData2.sysDate.length - 1] },
{ name: '月成交金额', value: chartData2.seriesList[0].data[chartData2.seriesList[0].data.length - 1].value + '万亿' }
]
setChartOption(2, [getYAxis({ })], chartData2.sysDate, chartData2.seriesList, chartData2.tooltipFormatter)
const { data: data3 } = await reportEconomyMonthStockTurnoverRate()
const chartData3: any = getChartData4(data3, '%', ['turnoverRate'])
chartList.value[3].valueList = [
{ name: '日期', value: chartData3.sysDate[chartData3.sysDate.length - 1] },
{ name: '换手率', value: chartData3.seriesList[0].data[chartData3.seriesList[0].data.length - 1].value + '%' }
]
setChartOption(3, [getYAxis({ })], chartData3.sysDate, chartData3.seriesList, chartData3.tooltipFormatter)
const { data: data4 } = await reportEconomyMonthStockVolume()
const chartData4: any = getChartData4(data4, '万亿', ['volume'], 10000)
chartList.value[4].valueList = [
{ name: '日期', value: chartData4.sysDate[chartData4.sysDate.length - 1] },
{ name: '月成交量', value: chartData4.seriesList[0].data[chartData4.seriesList[0].data.length - 1].value + '万亿' }
]
setChartOption(4, [getYAxis({ })], chartData4.sysDate, chartData4.seriesList, chartData4.tooltipFormatter)
const { data: data5 } = await reportEconomyMonthNewAccountNum()
const chartData5: any = getChartData4(data5, '万', ['totalNewAccountNum', 'shNewAccountNum'], 10000)
chartList.value[5].valueList = [
{ name: '日期', value: chartData5.sysDate[chartData5.sysDate.length - 1] },
{ name: '总开户数', value: chartData5.seriesList[0].data[chartData5.seriesList[0].data.length - 1].value + '万' },
{ name: '上证开户数', value: chartData5.seriesList[1].data[chartData5.seriesList[1].data.length - 1].value + '万' }
]
setChartOption(
5,
[getYAxis({ })],
chartData5.sysDate,
chartData5.seriesList,
chartData5.tooltipFormatter,
{ data: ['总开户数', '上证开户数'], top: '10px' },
[{ top: '40px', bottom: '20px', left: '40px', right: '40px' }]
)
} else if (id === '4') {
const { data: data0 } = await stockStockHsList()
const sysDate0: any = []
const seriesList0: any = [
getLineSeries({ name: 'Hang Seng Index(%)' }),
getLineSeries({ name: 'HSI-Finance' }),
getLineSeries({ name: 'HSI-Utilities' }),
getLineSeries({ name: 'HSI-Properties' }),
getLineSeries({ name: 'HSI-Com & Ind' }),
getLineSeries({ name: 'Hang Seng Index', yAxisIndex: 1 })
]
data0.monthList.forEach((ele: any) => {
sysDate0.push(ele.hsMonth)
seriesList0[0].data.push({ value: ele.hsi })
seriesList0[1].data.push({ value: ele.hsiFinance })
seriesList0[2].data.push({ value: ele.hsiUtilities })
seriesList0[3].data.push({ value: ele.hsiProperties })
seriesList0[4].data.push({ value: ele.hsiComInd })
seriesList0[5].data.push({ value: ele.hsNav })
})
data0.dayList.forEach((ele: any) => {
sysDate0.push(ele.sysDate)
seriesList0[0].data.push({ value: ele.hsi })
seriesList0[1].data.push({ value: ele.hsiFinance })
seriesList0[2].data.push({ value: ele.hsiUtilities })
seriesList0[3].data.push({ value: ele.hsiProperties })
seriesList0[4].data.push({ value: ele.hsiComInd })
seriesList0[5].data.push({ value: ele.hsNav })
})
if (data0.monthList.length > 0 && data0.dayList.length > 0) {
seriesList0[5].markArea = {
itemStyle: {
color: '#afdfe4'
},
data: [
[
{ name: '本月', xAxis: data0.monthList[data0.monthList.length - 1].hsMonth },
{ xAxis: data0.dayList[data0.dayList.length - 1].sysDate }
]
]
}
}
const tooltipFormatter0 = function (data: any) {
let backText = `日期:${data[0].name}<br/>`
data.forEach((ele: any) => {
backText += `<span style="color: ${ele.color}">${ele.seriesName}</span>:${ele.value ? ele.value : '暂无数据'}<br/>`
})
return backText
}
chartList.value[0].valueList = [
{ name: '日期', value: sysDate0[sysDate0.length - 1] },
{ name: 'Hang Seng Index(%)', value: seriesList0[0].data[seriesList0[0].data.length - 1].value },
{ name: 'HSI-Finance', value: seriesList0[1].data[seriesList0[1].data.length - 1].value },
{ name: 'HSI-Utilities', value: seriesList0[2].data[seriesList0[2].data.length - 1].value },
{ name: 'HSI-Properties', value: seriesList0[3].data[seriesList0[3].data.length - 1].value },
{ name: 'HSI-Com & Ind', value: seriesList0[4].data[seriesList0[4].data.length - 1].value },
{ name: 'Hang Seng Index', value: seriesList0[5].data[seriesList0[5].data.length - 1].value }
]
setChartOption(
0,
[getYAxis({ }), getYAxis({ position: 'right', showFlag: false })],
sysDate0,
seriesList0,
tooltipFormatter0,
{ data: ['Hang Seng Index(%)', 'HSI-Finance', 'HSI-Utilities', 'HSI-Properties', 'HSI-Com & Ind', 'Hang Seng Index'], top: '10px' },
[{ top: '100px', bottom: '20px', left: '40px', right: '40px' }]
)
const chartData1: any = await getChartData3('hs300', '')
chartList.value[1].valueList = [
{ name: '日期', value: chartData1.sysDate[chartData1.sysDate.length - 1] },
{ name: '中证300', value: chartData1.seriesList[0].data[chartData1.seriesList[0].data.length - 1].value }
]
setChartOption(1, [getYAxis({ })], chartData1.sysDate, chartData1.seriesList, chartData1.tooltipFormatter)
const chartData2: any = await getChartData3('zz500', '')
chartList.value[2].valueList = [
{ name: '日期', value: chartData2.sysDate[chartData2.sysDate.length - 1] },
{ name: '中证500', value: chartData2.seriesList[0].data[chartData2.seriesList[0].data.length - 1].value }
]
setChartOption(2, [getYAxis({ })], chartData2.sysDate, chartData2.seriesList, chartData2.tooltipFormatter)
const chartData3: any = await getChartData3('zz1000', '')
chartList.value[3].valueList = [
{ name: '日期', value: chartData3.sysDate[chartData3.sysDate.length - 1] },
{ name: '中证1000', value: chartData3.seriesList[0].data[chartData3.seriesList[0].data.length - 1].value }
]
setChartOption(3, [getYAxis({ })], chartData3.sysDate, chartData3.seriesList, chartData3.tooltipFormatter)
}
}
const getChartData2 = (data: any, index: number) => {
const sysDate: any = []
const seriesList: any = [getLineSeries({ })]
data.map((ele: any) => {
sysDate.push(ele.sysDate)
const item: any = {
value: Math.round(ele.rateValue * 100) / 100,
deposit: Math.round(ele.deposit * 100) / 100,
totalMarket: Math.round(ele.totalMarket * 100) / 100
}
if (index === 2) {
item.southBalance = Math.round(ele.southBalance * 100) / 100
}
seriesList[0].data.push(item)
})
const tooltipFormatter = function (data: any) {
let backText = `日期:${data[0].name}<br/>`
data.forEach((ele: any) => {
backText += `<span style="color: blue">总市值:</span><span style="color: red">${ele.data.totalMarket}万亿</span><br/>`
backText += `<span style="color: blue">住房存款:</span><span style="color: red">${ele.data.deposit}万亿</span><br/>`
if (index === 2 && ele.data.southBalance) {
backText += `<span style="color: blue">南方余额:</span><span style="color: red">${ele.data.southBalance}亿</span><br/>`
}
backText += `<span style="color: blue">比例:</span><span style="color: red">${ele.data.value}%</span>`
})
return backText
}
return { sysDate: sysDate, seriesList: seriesList, tooltipFormatter: tooltipFormatter }
}
const getChartData3 = async(type: string, unit: string, scale?: number) => {
const { data } = await reportEconomyMonthListByItem({ items: type })
const sysDate: any = []
const seriesList: any = [getLineSeries({ })]
data.map((ele: any) => {
sysDate.push(ele.sysDate)
if (scale === 10000) {
seriesList[0].data.push({ value: Math.round(ele[type] / 100) / 100 })
} else if (scale === 100) {
seriesList[0].data.push({ value: Math.round(ele[type] * 10000) / 100 })
} else {
seriesList[0].data.push({ value: Math.round(ele[type] * 100) / 100 })
}
})
const tooltipFormatter = function (data: any) {
let backText = `日期:${data[0].name}<br/>`
data.forEach((ele: any) => {
backText += `<span style='color: ${ele.color}'>${ele.value ? ele.value + unit : '暂无数据'}</span>`
})
return backText
}
return { sysDate: sysDate, seriesList: seriesList, tooltipFormatter: tooltipFormatter }
}
const getChartData4 = (data: any, unit: string, type: string[], scale?: number) => {
const sysDate: any = []
let seriesList: any = []
if (type.length === 2) {
seriesList = [getLineSeries({ name: '总开户数' }), getLineSeries({ name: '上证开户数' })]
} else {
seriesList = [getLineSeries({ })]
}
data.map((ele: any) => {
sysDate.push(ele.sysDate)
type.forEach((res, index) => {
if (scale === 10000) {
seriesList[index].data.push({ value: ele[res] ? Math.round(ele[res] / 100) / 100 : null })
} else {
seriesList[index].data.push({ value: Math.round(ele[res] * 100) / 100 })
}
})
})
const tooltipFormatter = function (data: any) {
let backText = `日期:${data[0].name}<br/>`
data.forEach((ele: any) => {
if (type.length === 2) {
backText += `<span style="color: ${ele.color}">${ele.seriesName}</span>:${ele.value ? ele.value + unit : '暂无数据'}<br/>`
} else {
backText += `<span style='color: ${ele.color}'>${ele.value ? ele.value + unit : '暂无数据'}</span><br/>`
}
})
return backText
}
return { sysDate: sysDate, seriesList: seriesList, tooltipFormatter: tooltipFormatter }
}
const setChartOption = (index: number, yAxis: any, sysDate: any, series: any, tooltipFormatter: any, legend?: any, grid?: any) => {
const option = getBaseOption({
yAxis: yAxis,
xAxis: [getXAxis({ data: sysDate })],
series: series,
legend: legend,
tooltipFormatter: tooltipFormatter,
grid: grid ? grid : [{ top: '20px', bottom: '20px', left: '40px', right: '40px' }]
})
if (!destroyedFlag.value && document.getElementById(chartList.value[index].id)) {
const chart = echarts.init(document.getElementById(chartList.value[index].id))
chart.setOption(option)
charts.value[index] = chart
chartList.value[index].loading = false
}
}
onMounted (() =>{
init()
})
</script>
<style lang='scss' scoped>
.page-div {
padding-top: 86px;
height: auto;
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
@media screen and (min-width: 992px) {
left: calc(50vw - 496px);
right: calc(50vw - 496px);
}
}
.chart-container {
height: 30vh;
@media screen and (min-width: 768px) {
height: 40vh;
}
}
}
.value-list-container {
padding: 12px 15px;
background-color: #fff;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
margin-top: 10px;
}
.value-item {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px dashed #eee;
transition: background-color 0.2s;
}
.value-item:last-child {
border-bottom: none;
}
.value-item:hover {
background-color: #f9f9f9;
}
.value-name {
color: #666;
font-weight: 500;
flex: 1;
}
.value-value {
color: #333;
font-weight: bold;
text-align: right;
min-width: 80px;
}
</style>

View File

@@ -0,0 +1,52 @@
<template>
<div class="page-div">
<back-button />
<div class="header">
<h1 class="title">经济专题</h1>
</div>
<div class="grid">
<van-grid :column-num="1" :gutter="16" :border="false">
<van-grid-item v-for="item in list" :key="item.id">
<div @touchstart="onTouchStart(item.economyName)" @touchend="onTouchEnd(item.economyName, 'economy-detail', item.id)" class="item"
@touchcancel="onTouchCancel(item.economyName)" :class="{ 'tapped': tappedItem === item.economyName }">
<div class="card-content" :style="{ backgroundColor: item.bgColor, borderLeftColor: item.color }">
<div class="card-code" :style="{ color: item.color }">{{ item.economyName }}</div>
</div>
</div>
</van-grid-item>
</van-grid>
</div>
</div>
</template>
<script setup lang="ts">
import BackButton from '@/components/back-button.vue'
import { tapMixins } from '@/mixins/tap-mixins'
const { tappedItem, onTouchStart, onTouchEnd, onTouchCancel } = tapMixins()
import { ref, onMounted } from 'vue'
let list = ref<any[]>([])
import { colorList } from '@/utils/colorList'
const init = async () => {
list.value = [
{ economyName: '储蓄专题', id: 1 },
{ economyName: '两融专题', id: 2 },
{ economyName: 'A股专题', id: 3 },
{ economyName: '指数专题', id: 4 }
]
list.value.forEach((ele, index) => {
ele.color = colorList[index % colorList.length].color
ele.bgColor = colorList[index % colorList.length].bgColor
})
}
onMounted (() =>{
init()
})
</script>
<style lang='scss' scoped>
</style>

View File

@@ -0,0 +1,663 @@
<template>
<div class="page-div">
<back-button />
<div class="header">
<div class="header-content">
<h1 class="title">{{ fundData?.etfCode }}</h1>
<p class="subtitle">{{ fundData?.etfName || '指数详情' }}</p>
</div>
</div>
<div class="info-card" v-if="chartList.length > 0">
<div class="chart-container">
<van-loading v-show="chartList[0].loading" type="spinner" color="#1989fa" class="loading" />
<div :id="`${chartList[0].id}`" class="chartDiv"></div>
</div>
</div>
<div class="info-card" v-if="!virtualCurrency && chartList.length > 0">
<div class="chart-container">
<van-loading v-show="chartList[1].loading" type="spinner" color="#1989fa" class="loading" />
<div :id="`${chartList[1].id}`" class="chartDiv"></div>
</div>
</div>
<div class="info-card" v-if="!virtualCurrency && chartList.length > 0">
<div class="chart-container">
<van-loading v-show="chartList[2].loading" type="spinner" color="#1989fa" class="loading" />
<div :id="`${chartList[2].id}`" class="chartDiv"></div>
</div>
</div>
<div class="info-card" v-if="virtualCurrency">
<div class="tableDiv">
<table border="1">
<thead>
<tr>
<th class="td--column--1">时间</th>
<th>股本(变化)</th>
<th>{{ route.params.id === 'IBIT' ? '比特币数量' : '以太坊数量' }}(数量变化)</th>
<th>股本/{{ route.params.id === 'IBIT' ? '比特币' : '以太坊' }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(coin, index) in coinList" :key="index">
<td class="td--column--1">{{ coin.sysDate }}</td>
<td>
{{ coin.shares }}<br/>
{{ coin.sharesChange }}<br/>
<span :class="coin.sharesChangePer > 0 ? 'red-color' : coin.sharesChangePer < 0 ? 'green-color' : ''"
v-if="coin.sharesChangePer">({{ coin.sharesChangePer }}%)</span>
</td>
<td>
{{ coin.coinNum }}<br/>
{{ coin.coinNumChange }}<br/>
<span :class="coin.coinNumChangePer > 0 ? 'red-color' : coin.coinNumChangePer < 0 ? 'green-color' : ''"
v-if="coin.coinNumChangePer">({{ coin.coinNumChangePer }}%)</span>
</td>
<td>{{ coin.per }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="info-card" v-if="!virtualCurrency">
<h3 class="card-title">持仓列表</h3>
<div> 最新日期<span class="blue-color">{{ holdList.length > 0 ? holdList[0].sysDate : '暂无'}}</span>,
本季开始日期<span class="red-color">{{ holdList.length > 0 ? holdList[0].lastQuarterDate : '暂无'}}</span></div>
<div class="tableDiv">
<table border="1">
<thead>
<tr>
<th class="td--column--1">名称</th>
<th>上期权重</th>
<th>当前权重</th>
<th>相对变化</th>
<th>市值</th>
</tr>
</thead>
<tbody>
<tr v-for="(hold, index) in holdList" :key="index" @click="showStockChart(hold)">
<td class="td--column--1">
{{ hold.ticker }}
<div class="van-ellipsis firstTd">{{ hold.tickerName }}</div>
</td>
<td class="blue-color">{{ hold.lastQuarterWeight }}%</td>
<td>
<div :class="hold.weight > hold.lastQuarterWeight ? 'red-color' : hold.weight < hold.lastQuarterWeight ? 'green-color' : ''">{{ hold.weight }}%</div>
</td>
<td>
<div :style="{color: hold.weightChangePer > 0 ? 'red' : hold.weightChangePer < 0 ? 'green' : ''}">
{{ hold.weightChangePer > 0 ? '+' + hold.weightChangePer : hold.weightChangePer }}%
</div>
</td>
<td>{{ hold.marketValue }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<van-popup v-model:show="showStockChartPopup" position="bottom" :style="{ height: '50%' }">
<div class="p-16">
<div class="d-flex justify-content-between items-center mb-16">
<div class="sub-title f-b">{{ stockCode }}{{ stockName }}权重趋势</div>
<van-icon name="close" @click="closeStockChart" />
</div>
<div class="popup-card">
<div class="chart-container">
<van-loading v-show="chartList[3].loading" type="spinner" color="#1989fa" class="loading" />
<div :id="`${chartList[3].id}`" class="chartDiv"></div>
</div>
</div>
</div>
</van-popup>
</div>
</template>
<script setup lang='ts'>
import BackButton from '@/components/back-button.vue'
import { ref, onMounted, computed, onUnmounted, nextTick } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const virtualCurrency = computed(() => {
return route.params.id === 'ETHA' || route.params.id === 'IBIT'
})
import { indexFundGetByEtfCode, indexFundHisQueryEtfShareHis, indexFundDetailDayCoinList, indexFundDetailDayList, indexFundDetailQuarterCircleByExchange, indexFundDetailQuarterHisByExchange, indexFundDetailQuarterGetChart } from '@/utils/api'
const fundData: any = ref(null)
import { addOrSubtractTime, formatNumberByUnit, formatNumber } from '@/utils'
import * as echarts from 'echarts'
import { getLineSeries, getBaseOption, getXAxis, getYAxis } from '@/utils/chart'
const init = async () => {
try {
const { data } = await indexFundGetByEtfCode({ etfCode: route.params.id + '' })
fundData.value = data
const { data: navList } = await indexFundHisQueryEtfShareHis({
timeE: new Date().toISOString().split('T')[0],
timeB: addOrSubtractTime(new Date(), -10, 'year'),
etfCode: route.params.id + ''
})
setLineChart(navList.dataList)
if (virtualCurrency.value) {
getCoinList()
} else {
getChartData()
getHoldList()
}
} catch (error) {
console.error('初始化数据失败:', error)
}
}
const setLineChart = (data: any) => {
const sysDate: any = []
const unit = ['万', '亿', '']
let seriesList: any = []
let yAxis: any = []
let xAxis: any = []
let grid: any = []
seriesList.push(getLineSeries({ name: '股本' }))
seriesList.push(getLineSeries({ name: '市值', yAxisIndex: 1 }))
seriesList.push(getLineSeries({ name: 'NAV', yAxisIndex: 2 }))
if (virtualCurrency.value) {
seriesList.push(getLineSeries({ name: '股本回撤', xAxisIndex: 1, yAxisIndex: 3 }))
seriesList.push(getLineSeries({ name: 'NAV回撤', xAxisIndex: 1, yAxisIndex: 3 }))
grid = [{ bottom: '54%', left: '0', right: '0', top: '40px' }, { top: '62%', bottom: '20px', left: '0', right: '0' }]
yAxis = [
getYAxis({ axisLabel: '{value} 万', color: '#FF0000', showFlag: false }),
getYAxis({ axisLabel: '{value} 亿', position: 'right', color: '#3E8EF7', showFlag: false }),
getYAxis({ position: 'left', offset: 90, color: '#000000', showFlag: false }),
getYAxis({ gridIndex: 1, axisLabel: '{value}%', showFlag: false })
]
const drawdowns1: any = []
const drawdowns2: any = []
let maxDrawdown1 = 0
let maxDrawdown2 = 0
for (let j = 0; j < data.length; j++) {
let drawdown1 = 0
const sharesOutstanding = Number(data[j].sharesOutstanding)
if (sharesOutstanding > maxDrawdown1) {
maxDrawdown1 = sharesOutstanding
drawdown1 = 0
} else {
drawdown1 = Math.round((sharesOutstanding - maxDrawdown1) / maxDrawdown1 * 10000) / 100
}
drawdowns1.push(drawdown1)
let drawdown2 = 0
const NAV = Number(data[j].NAV)
if (NAV > maxDrawdown2) {
maxDrawdown2 = NAV
drawdown2 = 0
} else {
drawdown2 = Math.round((NAV - maxDrawdown2) / maxDrawdown2 * 10000) / 100
}
drawdowns2.push(drawdown2)
}
data.forEach((ele: any, index: number) => {
sysDate.push(ele.sysDate)
seriesList[0].data.push({ value: ele.sharesOutstanding, sysDate: ele.sysDate, drawdowns: drawdowns1[index] })
seriesList[1].data.push({ value: ele.market, sysDate: ele.sysDate })
seriesList[2].data.push({ value: ele.NAV, sysDate: ele.sysDate, drawdowns: drawdowns2[index] })
seriesList[3].data.push({ value: drawdowns1[index], sysDate: ele.sysDate, nav: ele.sharesOutstanding })
seriesList[4].data.push({ value: drawdowns2[index], sysDate: ele.sysDate, nav: ele.NAV })
})
xAxis = [getXAxis({ data: sysDate }), getXAxis({ data: sysDate, gridIndex: 1, showFlag: false })]
} else{
grid = [{ left: '40px', right: '40px', bottom: '20px', top: '20px' }]
yAxis = [
getYAxis({ axisLabel: '{value} 万', color: '#FF0000', showFlag: false }),
getYAxis({ axisLabel: '{value} 亿', position: 'right', color: '#3E8EF7', showFlag: false }),
getYAxis({ position: 'left', offset: 90, color: '#000000', showFlag: false })
]
data.forEach((ele: any) => {
sysDate.push(ele.sysDate)
seriesList[0].data.push({ value: ele.sharesOutstanding })
seriesList[1].data.push({ value: ele.market })
seriesList[2].data.push({ value: ele.NAV })
})
xAxis = [getXAxis({ data: sysDate })]
}
if (!destroyedFlag.value) {
let chart = echarts.init(document.getElementById(`${chartList.value[0].id}`))
const option = getBaseOption({
series: seriesList,
yAxis: yAxis,
xAxis: xAxis,
tooltipFormatter: function (params: any) {
const data = Array.isArray(params) ? params : [params]
let backText = `日期:${data[0].name}<br/>`
if (virtualCurrency.value) {
if (data[0].seriesName.includes('回撤')) {
data.forEach((ele, index) => {
backText +=
`<div style="color: ${ele.color}">${ele.seriesName.slice(0, -2)}:${ele.data.nav || 0}${ele.data.nav ? unit[index] : ''}</div>`
backText +=
`<div style="color: ${ele.color}">${ele.seriesName}:${ele.value || 0}</div>`
})
} else {
data.forEach((ele, index) => {
backText +=
`<div style="color: ${ele.color}">${ele.seriesName}:${ele.value || '0'}${ele.value ? unit[index] : ''}</div>`
if (ele.seriesName !== '市值') {
backText +=
`<div style="color: ${ele.color}">${ele.seriesName + '回撤'}:${ele.data.drawdowns || '0'}</div>`
}
})
}
} else {
data.forEach((ele) => {
backText += `<span style="color: ${ele.color}">${ele.seriesName}</span>:<span class="f-b">${formatNumberByUnit(ele.value)}</span><br/>`
})
}
return backText
},
grid: grid,
color: ['red', 'blue', 'black'],
legend: {
data: ['股本', '市值', 'NAV'],
top: '0px',
show: true
}
})
chart.setOption(option)
charts.value[0] = chart
chartList.value[0].loading = false
}
}
const coinList = ref<any[]>([])
const getCoinList = async () => {
const { data } = await indexFundDetailDayCoinList({ etfCode: route.params.id + '' })
data.map((ele: any, index: number) => {
if (index < data.length - 1) {
const sharesChange = ele.shares - data[index + 1].shares
ele.sharesChangePer = Math.round(sharesChange / ele.shares * 10000) / 100
ele.sharesChange = formatNumber(sharesChange)
const coinNumChange = Math.round((ele.coinNum - data[index + 1].coinNum) * 100) / 100
ele.coinNumChangePer = Math.round(coinNumChange / ele.coinNum * 10000) / 100
ele.coinNumChange = formatNumber(coinNumChange)
} else {
ele.sharesChange = 0
ele.sharesChangePer = 0
ele.coinNumChange = 0
ele.coinNumChangePer = 0
}
ele.per = Math.round(ele.shares / ele.coinNum * 100) / 100
ele.shares = formatNumber(ele.shares)
ele.coinNum = formatNumber(ele.coinNum)
})
coinList.value = data
}
const colors = ['#5673cc', '#9fe080', '#fdd85e', '#ee6666', '#73c0de', '#3ba272', '#ff915a']
const getChartData = async () => {
const { data } = await indexFundDetailQuarterCircleByExchange({ etfCode: route.params.id + '' })
data.sort(function (a: any, b: any) {
return b.marketValue - a.marketValue
})
const series: any = []
let weight = 0
let stockNum = 0
data.forEach((ele: any, index: number) => {
if (index < 6) {
series.push({ value: ele.weight, name: ele.exchange, stockNum: ele.stockNum })
} else {
weight = Math.round((weight + ele.weight) * 100) / 100
stockNum = Math.round(stockNum + ele.stockNum)
if (index === data.length - 1) {
series.push({ value: weight, name: '其他', stockNum: stockNum })
}
}
})
const color = colors.slice(0, series.length)
const option = {
color: color,
tooltip: { trigger: 'item' },
legend: { orient: 'vertical', left: 'left' },
series: [{
type: 'pie',
radius: '50%',
data: series,
label: {
formatter: function (data: any) {
return `{b|${data.name}}\n{hr|}\n权重{per|${data.value}%}股票數量{per|${data.data.stockNum}}`
},
backgroundColor: '#F6F8FC',
borderColor: '#8C8D8E',
borderWidth: 1,
borderRadius: 4,
rich: {
a: { color: '#6E7079', lineHeight: 22, align: 'center' },
hr: { borderColor: '#8C8D8E', width: '100%', borderWidth: 1, height: 0 },
b: { color: '#4C5058', fontSize: 14, fontWeight: 'bold', lineHeight: 33 },
per: { color: '#fff', backgroundColor: '#4C5058', padding: [3, 4], borderRadius: 4, lineHeight: 33 }
}
},
emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } }
}]
}
if (!destroyedFlag.value && document.getElementById(chartList.value[1].id)) {
let chart = echarts.init(document.getElementById(`${chartList.value[1].id}`))
chart.setOption(option)
charts.value[1] = chart
chartList.value[1].loading = false
setMarketLineChart1(data, color)
chart.on('legendselectchanged', (params: any) => {
setMarketLineChart2(color, params.selected)
})
}
}
const minTime = ref('')
const maxTime = ref('')
const seriesList = ref<any[]>([])
const legendList = ref<any[]>([])
const setMarketLineChart1 = async (res: any, color: any) => {
const series: any = []
const legends: any = []
let min = ''
let max = ''
for (let i = 0; i < res.length; i++) {
if (i < 6) {
legends.push(res[i].exchange)
series.push(getLineSeries({ name: res[i].exchange }))
const { data } = await indexFundDetailQuarterHisByExchange({ etfCode: route.params.id + '', exchange: res[i].exchange })
data.forEach((ele: any) => {
series[i].data.push([ele.sysDate, ele.weight, ele.marketValue, ele.stockNum])
})
if (data.length > 0) {
if (i === 0) {
min = data[0].sysDate
max = data[data.length - 1].sysDate
} else {
if (new Date(data[0].sysDate).getTime() < new Date(min).getTime()) {
min = data[0].sysDate
}
if (new Date(data[data.length - 1].sysDate).getTime() > new Date(max).getTime()) {
max = data[data.length - 1].sysDate
}
}
}
} else {
if (i === 6) {
legends.push('其他')
series.push(getLineSeries({ name: '其他' }))
}
const { data } = await indexFundDetailQuarterHisByExchange({ etfCode: route.params.id + '', exchange: res[i].exchange })
if (data.length > 0) {
if (new Date(data[0].sysDate).getTime() < new Date(min).getTime()) {
min = data[0].sysDate
}
if (new Date(data[data.length - 1].sysDate).getTime() > new Date(max).getTime()) {
max = data[data.length - 1].sysDate
}
}
data.forEach((ele: any) => {
const item = series[6].data.find((res: any) => {
return res[0] === ele.sysDate
})
if (item) {
item[1] += ele.weight
item[2] += ele.marketValue
item[3] += ele.stockNum
} else {
series[6].data.push([ele.sysDate, ele.weight, ele.marketValue, ele.stockNum])
}
})
}
}
minTime.value = min
maxTime.value = max
seriesList.value = series
legendList.value = legends
nextTick(() => {
setMarketLineChart(color, { data: legends, top: '30px', show: false })
})
}
const setMarketLineChart2 = (color: any, selected: any) => {
if (charts.value[2]) {
charts.value[2].dispose()
}
nextTick(() => {
setMarketLineChart(color,
{
data: legendList.value,
top: '30px',
show: false,
selected
})
})
}
const setMarketLineChart = (color: any, legend: any) => {
const option = getBaseOption({
color: color,
yAxis: [getYAxis({ })],
xAxis: [getXAxis({
type: 'time',
min: minTime.value,
max: maxTime.value,
axisLabel: function (value: any) {
var date = new Date(value)
var texts = `${date.getFullYear()}\n${(date.getMonth() + 1)}-${date.getDate()}`
return texts
}
})],
series: JSON.parse(JSON.stringify(seriesList.value)),
tooltipFormatter: function (params: any) {
const data = Array.isArray(params) ? params : [params]
let res = data[0].data[0] + '</br>'
data.forEach((ele: any) => {
res += `${ele.seriesName}:</br>权重:${Math.round(ele.data[1] * 100) / 100}%</br>股票:${ele.data[3]}支</br>市值:${Math.round(ele.data[2] * 100) / 100}</br>`
})
return res
},
grid: [{
left: '60px',
right: '40px'
}],
legend: legend
})
if (!destroyedFlag.value && document.getElementById(chartList.value[2].id)) {
let chart = echarts.init(document.getElementById(`${chartList.value[2].id}`))
chart.setOption(option)
charts.value[2] = chart
chartList.value[2].loading = false
}
}
const curPage = ref(1)
const holdList = ref<any[]>([])
const totalPages = ref(1)
const hasMore = ref(true)
const isLoading = ref(false)
const getHoldList = async () => {
if (!hasMore.value || isLoading.value) return
isLoading.value = true
try {
const { data } = await indexFundDetailDayList({ etfCode: route.params.id + '', curPage: curPage.value, limit: 50 })
totalPages.value = data.totalPage || 1
hasMore.value = curPage.value < totalPages.value
data.list.forEach((ele: any) => {
ele.shares = formatNumberByUnit(ele.shares)
ele.marketValue = formatNumberByUnit(ele.marketValue)
})
holdList.value = [...holdList.value, ...data.list]
} catch (error) {
console.error('加载失败:', error)
} finally {
isLoading.value = false
}
}
const loadNextPage = () => {
if (hasMore.value) {
curPage.value++
getHoldList()
}
}
const handleScroll = () => {
if (!virtualCurrency.value) {
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop
const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight
const clientHeight = document.documentElement.clientHeight || window.innerHeight
if (scrollTop + clientHeight >= scrollHeight - 100 && hasMore.value && !isLoading.value) {
loadNextPage()
}
}
}
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll)
})
const showStockChartPopup = ref(false)
const stockCode = ref('')
const stockName = ref('')
const showStockChart = async (item: any) => {
showStockChartPopup.value = true
stockCode.value = item.stockCode
stockName.value = item.tickerName
const { data } = await indexFundDetailQuarterGetChart({ stockCode: item.stockCode, etfCode: route.params.id + '', exchange: item.exchange })
const sysDate: any = []
const seriesList = [getLineSeries({ name: '权重' }), getLineSeries({ name: '股本', yAxisIndex: 1, lineType: 'dashed' })]
let gt = 0
const lte = data.length
data.forEach((ele: any, index: number) => {
sysDate.push(ele.sysDate)
seriesList[0].data.push({ value: ele.weight })
seriesList[1].data.push({ value: ele.shares })
if (index > 0 && ele.dataType === 'D' && data[index - 1].dataType === 'Q') {
gt = index
}
})
nextTick(() => {
if (!destroyedFlag.value) {
let chart = echarts.init(document.getElementById(`${chartList.value[3].id}`))
const option = getBaseOption({
series: seriesList,
yAxis: [getYAxis({ axisLabel: '{value} %' }), getYAxis({ position: 'right' })],
xAxis: [getXAxis({ data: sysDate })],
tooltipFormatter: function (params: any) {
const data = Array.isArray(params) ? params : [params]
let backText = `${data[0].name}:`
data.forEach((ele) => {
backText += `<span class="f-b">${ele.value}${ele.seriesName === '权重' ? '%' : ''}</span><br/>`
})
return backText
},
grid: [{ top: '20px', left: '50px', right: '50px', bottom: '20px' }],
legend: {
data: [{
name: '权重',
itemStyle: {
color: '#ff0000'
},
icon: 'path://M802.59 532.76H221.4c-11.47 0-20.76-9.3-20.76-20.76s9.29-20.76 20.76-20.76h581.19c11.47 0 20.76 9.3 20.76 20.76s-9.29 20.76-20.76 20.76z'
}, {
name: '股本',
itemStyle: {
color: '#ff0000'
},
icon: 'path://M234.666667 490.666667h-153.6a25.6 25.6 0 1 0 0 51.2h153.6a25.6 25.6 0 1 0 0-51.2zM473.6 490.666667h-153.6a25.6 25.6 0 1 0 0 51.2h153.6a25.6 25.6 0 1 0 0-51.2zM934.4 490.666667h-136.533333a25.6 25.6 0 1 0 0 51.2h136.533333a25.6 25.6 0 1 0 0-51.2zM712.533333 490.666667h-153.6a25.6 25.6 0 1 0 0 51.2h153.6a25.6 25.6 0 1 0 0-51.2z'
}],
top: '30px'
},
gt: gt,
lte: lte
})
chart.setOption(option)
charts.value[3] = chart
chartList.value[3].loading = false
}
})
}
const closeStockChart = () =>{
if (charts.value[3]) {
charts.value[3].dispose()
charts.value[3] = null
}
showStockChartPopup.value = false
}
import { chartMixins } from '@/mixins/chart-mixins'
const { charts, destroyedFlag } = chartMixins()
let chartList = ref<any[]>([])
onMounted (() =>{
charts.value = virtualCurrency.value ? [null] : [null, null, null, null]
chartList.value = virtualCurrency.value ?
[{ id: 'fund_chart', loading: true }] :
[{ id: 'fund_chart', loading: true }, { id: 'fund_pie_chart', loading: true }, { id: 'fund_line_chart', loading: true }, { id: 'stock_chart', loading: true }]
init()
window.addEventListener('scroll', handleScroll)
})
</script>
<style lang='scss' scoped>
.page-div {
padding-top: 105px;
height: auto;
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
@media screen and (min-width: 992px) {
left: calc(50vw - 496px);
right: calc(50vw - 496px);
}
}
.chart-container {
height: 26vh;
@media screen and (min-width: 768px) {
height: calc(50vh - 226px);
}
}
.tableDiv {
th {
width: 150px;
}
.firstTd {
min-width: 80px;
max-width: 120px;
width: auto;
@media screen and (min-width: 768px) {
min-width: 150px;
max-width: 400px;
}
}
}
}
.popup-card {
.chart-container {
display: flex;
align-items: center;
justify-content: center;
height: 30vh;
@media screen and (min-width: 768px) {
height: 40vh;
}
.loading {
position: absolute;
z-index: 2;
}
.chartDiv {
width: 100%;
height: 100%;
}
}
}
</style>

View File

@@ -0,0 +1,362 @@
<template>
<div class="page-div">
<back-button />
<div class="header">
<h1 class="title">{{ fredCode }}</h1>
<p class="subtitle">{{ fredName }}</p>
</div>
<div class="chart-header">
<div class="time-range-buttons">
<van-button
v-for="range in timeRanges"
:key="range.value"
:plain="selectedTimeRange !== range.value"
:hairline="selectedTimeRange !== range.value"
:type="selectedTimeRange === range.value ? 'primary' : 'default'"
@click="setTimeRange(range.value)"
style="margin-left: 8px;"
>{{ range.label }}</van-button>
</div>
</div>
<div class="info-card">
<div class="chart-container">
<van-loading v-show="loading" type="spinner" color="#1989fa" class="loading" />
<div :id="`${fredCode}_chart`" class="chartDiv"></div>
</div>
</div>
<div class="chartItem d-flex align-items-center justify-content-center">
</div>
<div class="listDiv">
<div class="data-list-container" v-if="!loading">
<div v-if="list.length > 0">
<div class="list-header">
<div class="list-item">
<div class="list-col date-col">日期</div>
<template v-if="route.params.id">
<div class="list-col value-col">(%)</div>
</template>
<template v-else>
<div class="list-col aaa-col">AAA(%)</div>
<div class="list-col bbb-col">BBB(%)</div>
<div class="list-col spread-col">BBB-AAA(%)</div>
</template>
</div>
</div>
<RecycleScroller
class="list-body"
:items="list"
:item-size="1"
key-field="sysDate"
>
<template v-slot="{ item }">
<div class="list-item">
<div class="list-col date-col">{{ item.sysDate }}</div>
<template v-if="route.params.id">
<div class="list-col value-col">{{ item.fredValue }}</div>
</template>
<template v-else>
<div class="list-col aaa-col">{{ item.AaaFredValue }}</div>
<div class="list-col bbb-col">{{ item.BbbFredValue }}</div>
<div class="list-col spread-col">{{ (item.BbbFredValue - item.AaaFredValue).toFixed(2) }}</div>
</template>
</div>
</template>
</RecycleScroller>
</div>
<div class="no-data" v-else>暂无数据</div>
</div>
</div>
</div>
</template>
<script setup lang='ts'>
import BackButton from '@/components/back-button.vue'
import { ref, nextTick, onMounted } from 'vue'
import { RecycleScroller } from 'vue-virtual-scroller'
import { useRoute } from 'vue-router'
import { fredInfoInfo, fredDetailQueryFred, fredDetailQueryAaaAndBbb } from '@/utils/api'
let fredCode = ref('')
let fredName = ref('')
let beginTime = ref('')
let endTime = ref('')
let selectedTimeRange = ref('3Y')
const timeRanges = [
{ label: '1M', value: '1M', unit: 'month', offset: -1 },
{ label: '3M', value: '3M', unit: 'month', offset: -3 },
{ label: '1Y', value: '1Y', unit: 'year', offset: -1 },
{ label: '3Y', value: '3Y', unit: 'year', offset: -3 },
]
const route = useRoute()
const init = async () => {
const id = route.params.id
if (id) {
const { data } = await fredInfoInfo(id + '')
fredCode.value = data.fredCode
fredName.value = data.fredName
} else {
fredCode.value = 'BBB-AAA'
fredName.value = 'Option-Adjusted Spread'
}
setTimeRange('3Y')
charts.value = [null]
}
import { addOrSubtractTime } from '@/utils'
const setTimeRange = (range: string) => {
selectedTimeRange.value = range
endTime.value = new Date().toISOString().split('T')[0]
const rangeConfig = timeRanges.find(item => item.value === range)
if (rangeConfig) {
beginTime.value = addOrSubtractTime(new Date(), rangeConfig.offset, rangeConfig.unit as 'month' | 'year')
}
disposeAll()
nextTick(() => {
const id = route.params.id
getData(id ? 1 : 0)
})
}
let loading = ref(true)
let list = ref<any[]>([])
import * as echarts from 'echarts'
import { chartMixins } from '@/mixins/chart-mixins'
const { charts, destroyedFlag, disposeAll } = chartMixins()
import { getLineSeries, getBaseOption, getXAxis, getYAxis } from '@/utils/chart'
const getData = async (index: number) => {
loading.value = true
const sysDate: any = []
let seriesList: any = []
let color = []
const legend = {
data: [] as string[],
top: '30px',
show: false
}
if (index === 0) {
const { data } = await fredDetailQueryAaaAndBbb({
begin: beginTime.value,
end: endTime.value
})
color = ['green', 'blue', 'red']
legend.show = true
legend.data = ['AAA', 'BBB', 'BBB-AAA']
seriesList = [getLineSeries({ name: 'AAA' }), getLineSeries({ name: 'BBB' }), getLineSeries({ name: 'BBB-AAA' })]
data.forEach((ele: any) => {
sysDate.push(ele.sysDate)
seriesList[0].data.push({ value: ele.AaaFredValue })
seriesList[1].data.push({ value: ele.BbbFredValue })
seriesList[2].data.push({ value: Math.round((ele.BbbFredValue - ele.AaaFredValue) * 100) / 100 })
list.value.unshift(ele)
})
} else{
const { data } = await fredDetailQueryFred({
fredCode: fredCode.value,
begin: beginTime.value,
end: endTime.value
})
color = ['blue']
seriesList = [getLineSeries({ name: fredCode.value })]
data.forEach((ele: any) => {
sysDate.push(ele.sysDate)
seriesList[0].data.push({ value: ele.fredValue })
list.value.unshift(ele)
})
}
if (!destroyedFlag.value) {
let chart = echarts.init(document.getElementById(`${fredCode.value}_chart`))
const option = getBaseOption({
series: seriesList,
yAxis: [getYAxis({ axisLabel: '{value} %' })],
xAxis: [getXAxis({ data: sysDate })],
tooltipFormatter: function (params: any) {
const data = Array.isArray(params) ? params : [params]
let backText = `日期:${data[0].name}<br/>`
data.forEach((ele) => {
backText += `<span style="color: ${ele.color}">${ele.seriesName}</span>:<span class="f-b">${ele.value}%</span><br/>`
})
return backText
},
grid: [{ top: '10px', left: '40px', right: '40px', bottom: '20px' }],
legend: legend,
color: color
})
chart.setOption(option)
charts.value[0] = chart
loading.value = false
}
}
onMounted (() =>{
init()
})
</script>
<style lang='scss' scoped>
@import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
.page-div {
box-sizing: border-box;
overflow-y: auto;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
padding-top: 102px;
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
@media screen and (min-width: 992px) {
left: calc(50vw - 496px);
right: calc(50vw - 496px);
}
}
.chart-header {
display: flex;
flex-direction: column;
align-items: flex-end;
width: 100%;
margin-bottom: 16px;
.chart-title {
display: none;
}
.time-range-buttons {
display: flex;
justify-content: flex-end;
padding: 0 16px;
flex-wrap: wrap;
gap: 8px;
}
}
.chart-container {
width: 100%;
height: 30vh;
@media screen and (min-width: 768px) {
height: 40vh;
}
}
.listDiv {
padding: 0 16px;
.data-list-container {
width: 100%;
border-radius: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
background-color: #fff;
max-height: none !important;
overflow: visible !important;
height: auto !important;
-webkit-overflow-scrolling: auto;
.list-header {
background-color: #f5f7fa;
border-bottom: 1px solid #e8ebed;
}
.list-item {
display: flex;
align-items: center;
padding: 8px 16px;
border-bottom: 1px solid #e8ebed;
height: 40px;
box-sizing: border-box;
&:last-child {
border-bottom: none;
}
}
.list-body {
overflow: visible !important;
max-height: none !important;
height: auto !important;
}
.list-col {
flex: 1;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 0 8px;
font-size: 14px;
color: #333;
&.date-col {
flex: 0 0 30%;
text-align: left;
font-weight: 500;
}
&.value-col,
&.aaa-col,
&.bbb-col,
&.spread-col {
font-family: monospace;
}
&.spread-col {
color: #1989fa;
font-weight: 500;
}
}
.no-data {
padding: 30px 0;
text-align: center;
color: #999;
background-color: #fafafa;
}
}
}
@media screen and (max-width: 768px) {
.chart-header {
margin-bottom: 12px;
.time-range-buttons {
padding: 0 12px;
button {
font-size: 12px !important;
padding: 4px 8px !important;
}
}
}
.chart-container {
height: 25vh;
}
.listDiv {
padding: 0 12px;
.data-list-container {
.list-item {
padding: 10px 12px;
height: 44px;
}
.list-col {
font-size: 12px;
padding: 0 4px;
&.date-col {
flex: 0 0 35%;
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,58 @@
<template>
<div class="page-div">
<back-button />
<div class="header">
<h1 class="title">FRED 经济指标</h1>
<p class="subtitle">美国联邦储备经济数据</p>
</div>
<div class="info-card">
<p class="info-text">
FRED (Federal Reserve Economic Data) 提供了美国经济的关键指标数据帮助您做出更明智的投资决策
</p>
</div>
<div class="grid">
<van-grid :column-num="1" :gutter="16" :border="false">
<van-grid-item v-for="item in list" :key="item.fredCode">
<div @touchstart="onTouchStart(item.fredName)" @touchend="onTouchEnd(item.fredName, 'fred-detail', item.id)" class="item"
@touchcancel="onTouchCancel(item.fredName)" :class="{ 'tapped': tappedItem === item.fredName }">
<div class="card-content" :style="{ backgroundColor: item.bgColor, borderLeftColor: item.color }">
<div class="card-code" :style="{ color: item.color }">{{ item.fredCode }}</div>
<div class="card-name">{{ item.fredName }}</div>
</div>
</div>
</van-grid-item>
</van-grid>
</div>
</div>
</template>
<script setup lang="ts">
import BackButton from '@/components/back-button.vue'
import { tapMixins } from '@/mixins/tap-mixins'
const { tappedItem, onTouchStart, onTouchEnd, onTouchCancel } = tapMixins()
import { ref, onMounted } from 'vue'
let list = ref<any[]>([])
import { fredInfoList } from '@/utils/api'
import { colorList } from '@/utils/colorList'
const init = async () => {
const { data } = await fredInfoList()
list.value = [...[
{ fredCode: 'BBB-AAA', fredName: 'Option-Adjusted Spread', id: '' }
], ...data]
list.value.forEach((ele, index) => {
ele.color = colorList[index % colorList.length].color
ele.bgColor = colorList[index % colorList.length].bgColor
})
}
onMounted (() =>{
init()
})
</script>
<style lang='scss' scoped>
</style>

View File

@@ -0,0 +1,769 @@
<template>
<div class="page-div">
<back-button />
<div class="header">
<div class="header-content">
<h1 class="title">{{ fundData?.fundName }}</h1>
</div>
</div>
<div class="info-card">
<div class="d-flex justify-content-between align-items-center">
<div class="f-b">相关文件</div>
<div v-if="fileList.length > 0" @click="showFileList" class="blue-color">
查看详情
</div>
<div v-else class="red-color">暂无数据</div>
</div>
</div>
<div class="info-card" v-if="chartList.length > 0">
<div class="chart-container">
<van-loading v-show="chartList[0].loading" type="spinner" color="#1989fa" class="loading" />
<div :id="`${chartList[0].id}`" class="chartDiv"></div>
</div>
</div>
<div class="info-card" v-if="chartList.length > 1">
<div class="chart-container">
<van-loading v-show="chartList[1].loading" type="spinner" color="#1989fa" class="loading" />
<div :id="`${chartList[1].id}`" class="chartDiv"></div>
</div>
</div>
<div class="listDiv">
<div class="data-list-container">
<div v-if="tableList.length > 0">
<div class="list-header">
<div class="list-item">
<div class="list-col date-col">日期</div>
<div class="list-col value-col">净值</div>
<div class="list-col value-col">月涨幅</div>
<div class="list-col value-col">今年以来收益</div>
</div>
</div>
<RecycleScroller
class="list-body"
:items="tableList"
:item-size="1"
key-field="realSysDate"
>
<template v-slot="{ item }">
<div class="list-item">
<div class="list-col date-col" :class="{ 'f-b' : item.boldFlag }">{{ item.realSysDate }}</div>
<div class="list-col value-col">
<div v-if="item.budget" class="tag-div"><van-tag type="success" plain>预测</van-tag></div>
<span :class="{ 'f-b' : item.boldFlag, 'budgetHeight': item.budget }">{{ item.nav }}</span>
</div>
<div class="list-col value-col">
<div v-if="item.budget" class="tag-div"><van-tag type="success" plain>预测</van-tag></div>
<span :class="{
'f-b' : item.boldFlag,
'budgetHeight': item.budget,
'red-color': item.navPercent > 0,
'green-color' : item.navPercent < 0
}">{{ item.navPercent || 0 }}%</span>
</div>
<div class="list-col value-col">
<div v-if="item.budget" class="tag-div"><van-tag type="success" plain>预测</van-tag></div>
<span :class="{
'f-b' : item.boldFlag,
'budgetHeight': item.budget,
'red-color': item.navPercent > 0,
'green-color' : item.navPercent < 0
}">{{ item.yearIncomeRate || 0 }}%</span>
</div>
</div>
</template>
</RecycleScroller>
</div>
<div class="no-data" v-else>暂无数据</div>
</div>
</div>
</div>
<van-popup v-model:show="showFileListPopup" position="bottom" :style="{ height: '70%' }">
<div class="p-16">
<div class="d-flex justify-content-between items-center mb-16">
<div class="sub-title f-b">文件列表</div>
<van-icon name="close" @click="showFileListPopup = false" />
</div>
<div class="file-list">
<div v-for="(file, index) in fileList" :key="index" class="file-item">
<a :href="file.fileUrl" target="_blank" class="file-link">
<i class="iconfont" :class="`icon-${file.type}`"></i>
<span class="file-title van-ellipsis">{{ file.fileName }}</span>
</a>
</div>
</div>
</div>
</van-popup>
</template>
<script setup lang='ts'>
import BackButton from '@/components/back-button.vue'
import { ref, onMounted, nextTick } from 'vue'
import { RecycleScroller } from 'vue-virtual-scroller'
import { useRoute } from 'vue-router'
const route = useRoute()
import { stockAFundInfo, stockAFundFileList, stockAFundHisFundHisList, stockAFundListFund } from '@/utils/api'
const fundData: any = ref(null)
const fileList = ref<any[]>([])
import { padZeroAfterDecimal } from '@/utils'
const init = async () => {
try {
const data: any = await stockAFundInfo(route.params.id + '')
fundData.value = data.data
const { data: file } = await stockAFundFileList({ fundId: route.params.id + '', curPage: 1, limit: 999 })
const reg1 = /(\.xls|\.xlsx)$/
const reg2 = /(\.pdf)$/
const reg3 = /(\.doc|\.docx)$/
const reg4 = /(\.ppt|\.pptx)$/
file.list.map((ele: any) => {
if (ele.fileUrl) {
ele.type = 'lianjie'
} else {
if (ele.fileId) {
if (reg1.test(ele.fileName)) {
ele.type = 'excel'
}
if (reg2.test(ele.fileName)) {
ele.type = 'pdf'
}
if (reg3.test(ele.fileName)) {
ele.type = 'word'
}
if (reg4.test(ele.fileName)) {
ele.type = 'ppt'
}
}
ele.fileUrl = `${import.meta.env.VITE_BASE_URL}/file/${ele.fileId}`
}
})
fileList.value = file.list
const { data: nav } = await stockAFundHisFundHisList({ fundId: route.params.id + '' })
nav.map((ele: any) => {
ele.selectId = ele.id
ele.nav = ele.nav ? padZeroAfterDecimal(ele.nav) : ''
const month = ele.sysDate.slice(5, 7)
ele.boldFlag = false
if (month === '12') {
ele.boldFlag = true
}
ele.budget = false
ele.realSysDate = ele.sysDate
ele.holdingList = []
})
const info2 = data.preData && data.preData.sysDate ? data.preData : null
if (info2 && nav.length > 0) {
let sysDate = ''
let fundId = ''
nav.forEach((ele: any) => {
if (ele.holdingNum > 0) {
sysDate = ele.sysDate
fundId = ele.fundId
}
})
const month = info2.sysDate.slice(5, 7)
nav.push({
sysDate: sysDate,
fundId: fundId,
realSysDate: info2.sysDate,
nav: info2.price ? padZeroAfterDecimal(info2.price) : '',
navPercent: info2.percent,
yearIncomeRate: info2.yearIncomeRate,
holdingNum: info2.holdingList.length,
boldFlag: month === '12',
budget: true,
holdingList: info2.holdingList
})
}
setLineChart(nav, data.fundConfig)
} catch (error) {
console.error('初始化数据失败:', error)
}
}
let chartList = ref<any[]>([])
import { chartMixins } from '@/mixins/chart-mixins'
const { charts, destroyedFlag } = chartMixins()
import * as echarts from 'echarts'
import { getLineSeries, getBaseOption, getXAxis, getYAxis } from '@/utils/chart'
const tableList = ref<any[]>([])
const setLineChart = (nav: any, fundConfig: any) => {
if (fundData.value.configFlag === 1 && fundData.value.fundType === 'fund' && fundConfig.length > 0) {
charts.value = [null, null]
chartList.value = [{
id: 'nav_chart',
loading: true,
}, {
id: 'nav_list_chart',
loading: true,
}]
} else {
charts.value = [null]
chartList.value = [{
id: 'nav_chart',
loading: true,
}]
}
const list = nav.sort(function (a: any, b: any) {
return new Date(a.realSysDate).getTime() - new Date(b.realSysDate).getTime()
})
const drawdowns: any = []
let maxDrawdown = 0
const tabledata: any = []
for (let j = 0; j < list.length; j++) {
tabledata.unshift(list[j])
let drawdown = 0
const nav = Number(list[j].nav)
if (nav > maxDrawdown) {
maxDrawdown = nav
drawdown = 0
} else {
drawdown = Math.round((nav - maxDrawdown) / maxDrawdown * 10000) / 100
}
drawdowns.push(drawdown)
}
const sysDate: any = []
const seriesData: any = []
const drawdownsData: any = []
list.forEach((ele: any, index: number) => {
sysDate.push(ele.realSysDate)
seriesData.push({ value: ele.nav, drawdown: drawdowns[index] })
drawdownsData.push({ value: drawdowns[index], nav: ele.nav, sysDate: ele.realSysDate })
})
const series = [
getLineSeries({ data: seriesData, name: '历史净值', areaStyle: { color: '#FAE5E3' }, color: '#FF3732' }),
getLineSeries({ data: drawdownsData, yAxisIndex: 1, xAxisIndex: 1, name: '回撤走势', areaStyle: { color: '#FAE5E3' }, color: '#FF3732' })
]
if (drawdownsData.length > 3) {
const minTime = new Date(drawdownsData[0].sysDate).getTime()
const maxTime = new Date(drawdownsData[drawdownsData.length - 1].sysDate).getTime()
const range = Math.round((maxTime - minTime) / 6)
const min1 = getPoint(drawdownsData)
const left = []
const right = []
for (let i = 0; i < drawdownsData.length; i++) {
if (new Date(min1.sysDate).getTime() - new Date(drawdownsData[i].sysDate).getTime() - range > 0) {
left.push(drawdownsData[i])
}
if (new Date(drawdownsData[i].sysDate).getTime() - range - new Date(min1.sysDate).getTime() > 0) {
right.push(drawdownsData[i])
}
}
let min2: any = {}
let min3: any = {}
if (left.length > 0 && right.length > 0) {
min2 = getPoint(left)
min3 = getPoint(right)
} else if (left.length > 0 && right.length === 0) {
min2 = getPoint(left)
const left1 = []
const right1 = []
for (let i = 0; i < left.length; i++) {
if (new Date(min2.sysDate).getTime() - new Date(left[i].sysDate).getTime() - range > 0) {
left1.push(left[i])
}
if (new Date(left[i].sysDate).getTime() - range - new Date(min2.sysDate).getTime() > 0) {
right1.push(left[i])
}
}
min3 = getPoint([...left1, ...right1])
} else if (left.length === 0 && right.length > 0) {
min2 = getPoint(right)
const left2 = []
const right2 = []
for (let i = 0; i < right.length; i++) {
if (new Date(min2.sysDate).getTime() - new Date(right[i].sysDate).getTime() - range > 0) {
left2.push(right[i])
}
if (new Date(right[i].sysDate).getTime() - range - new Date(min2.sysDate).getTime() > 0) {
right2.push(right[i])
}
}
min3 = getPoint([...left2, ...right2])
}
series[1].markPoint = {
data: [{
name: 1,
value: min1.num,
coord: [min1.sysDate, min1.num],
itemStyle: {
color: 'rgba(0,0,0,0)'
},
label: {
position: 'bottom',
color: '#f58220'
}
}, {
name: 2,
value: min2.num,
coord: [min2.sysDate, min2.num],
itemStyle: {
color: 'rgba(0,0,0,0)'
},
label: {
position: 'bottom',
color: '#0000ff'
}
}, {
name: 3,
value: min3.num,
coord: [min3.sysDate, min3.num],
itemStyle: {
color: 'rgba(0,0,0,0)'
},
label: {
position: 'bottom',
color: '#65c294'
}
}]
}
} else {
series[1].markPoint = {
data: drawdownsData.map((ele: any, i: number) => ({
name: `${i + 1}`,
value: ele.value,
coord: [sysDate[i], ele.value],
itemStyle: {
color: 'rgba(0,0,0,0)'
},
label: {
position: 'bottom',
color: i === 0 ? '#f58220' : i === 1 ? '#0000ff' : '#65c294'
}
}))
}
}
const option = getBaseOption({
yAxis: [getYAxis({ }), getYAxis({ gridIndex: 1, axisLabel: '{value}%' })],
xAxis: [getXAxis({ data: sysDate }), getXAxis({ data: sysDate, gridIndex: 1, showFlag: false })],
series: series,
title: [{ text: '历史净值' }, { text: '回撤情况', top: '54%' }],
grid: [{ bottom: '54%', left: '40px', right: '40px', top: '40px' }, { top: '66%', bottom: '20px', left: '40px', right: '40px' }],
formatter: function (data: any) {
let res = `${data[0].name}<br/>`
data.forEach((ele: any) => {
res += `历史净值:${ele.seriesName === '历史净值' ? ele.data.value : ele.data.nav}<br/>`
res += `回撤:${ele.seriesName === '历史净值' ? ele.data.drawdown : ele.data.value}%<br/>`
})
return res
}
})
nextTick(() =>{
if (!destroyedFlag.value && document.getElementById(chartList.value[0].id)) {
const chart = echarts.init(document.getElementById(chartList.value[0].id))
chart.setOption(option)
charts.value[0] = chart
chartList.value[0].loading = false
}
if (fundData.value.configFlag === 1 && fundData.value.fundType === 'fund' && fundConfig.length > 0) {
getFundList(fundConfig)
tableList.value = tabledata
} else {
tableList.value = tabledata
}
})
}
const getPoint = (drawdownsData: any) => {
const minValue = {
num: drawdownsData[0].value,
sysDate: drawdownsData[0].sysDate
}
for (let i = 1; i < drawdownsData.length; i++) {
if (drawdownsData[i].value < minValue.num) {
minValue.num = drawdownsData[i].value
minValue.sysDate = drawdownsData[i].sysDate
}
}
return minValue
}
const getFundList = async(fundConfig: any) => {
const { data } = await stockAFundListFund()
const multipleSelection: any = []
data.fundList.forEach((ele: any) => {
const fund = fundConfig.find((res: any) => {
return ele.fundId === res.configFundId
})
if (fund && fund.sysDate && fund.weight) {
multipleSelection.push({
weight: fund.weight,
sysDate: fund.sysDate,
stockPriceList: ele.stockPriceList,
fundName: ele.fundName,
fundId: ele.fundId
})
}
})
getChart(multipleSelection)
}
const getChart = (multipleSelection: any) => {
const colorPalette = generateColors(multipleSelection.length + 1)
const legend: any = []
const seriesList: any = []
const sysDate: any = []
multipleSelection.sort((a: any, b: any) => new Date(a.sysDate).getTime() - new Date(b.sysDate).getTime())
multipleSelection.forEach((ele: any, index: number) => {
legend.push(ele.fundName)
seriesList.push(getLineSeries({ name: ele.fundName, color: colorPalette[index] }))
seriesList.push(getLineSeries({ name: ele.fundName, yAxisIndex: 1, xAxisIndex: 1, color: colorPalette[index] }))
const priceIndex = ele.stockPriceList.findIndex((res: any) => {
return new Date(res.sysDate).getTime() >= new Date(ele.sysDate).getTime()
})
const stockPriceList = ele.stockPriceList.slice(priceIndex)
const per = stockPriceList.length > 0 ? stockPriceList[0].nav : 1
let maxDrawdown = 0
stockPriceList.forEach((res: any) => {
if (!sysDate.includes(res.sysDate)) {
sysDate.push(res.sysDate)
}
const nav = res.nav ? Math.round(res.nav / per * 10000) / 10000 : 0
let drawdown = 0
if (nav > maxDrawdown) {
maxDrawdown = nav
drawdown = 0
} else {
drawdown = Math.round((nav - maxDrawdown) / maxDrawdown * 10000) / 100
}
seriesList[index * 2].data.push([res.sysDate, res.nav, drawdown])
seriesList[index * 2 + 1].data.push([res.sysDate, drawdown, res.nav])
})
})
sysDate.sort((a: any, b: any) => new Date(a).getTime() - new Date(b).getTime())
const moneyList: any = []
const navList: any = []
sysDate.forEach((ele: any) => {
const countList: any = []
seriesList.forEach((res: any, index: number) => {
if (index % 2 === 0) {
if (new Date(multipleSelection[index / 2].sysDate).getTime() <= new Date(ele).getTime()) {
const navItem = res.data.find((item: any) => {
return item[0] === ele
})
if (navItem) {
countList.push({
weight: Number(multipleSelection[index / 2].weight),
nav: navItem[1],
marketValue: 0,
fundId: multipleSelection[index / 2].fundId
})
} else {
countList.push({
weight: Number(multipleSelection[index / 2].weight),
nav: 0,
marketValue: 0,
fundId: multipleSelection[index / 2].fundId
})
}
}
}
})
let weights = 0
countList.forEach((ele: any) => {
weights += ele.weight
})
let marketValue = 0
if (moneyList.length === 0) {
countList.map((res: any) => {
res.marketValue = Math.round(Number(fundData.value.initAmount) * res.weight / weights * 100) / 100
marketValue += res.marketValue
})
} else {
if (countList.length > moneyList[moneyList.length - 1].length) {
countList.forEach((res: any, index: number) => {
if (index < moneyList[moneyList.length - 1].length) {
if (res.nav) {
marketValue += Math.round(moneyList[moneyList.length - 1][index].marketValue / moneyList[moneyList.length - 1][index].nav * res.nav * 100) / 100
} else {
marketValue += moneyList[moneyList.length - 1][index].marketValue
}
}
})
countList.map((res: any, index: number) => {
if (index >= moneyList[moneyList.length - 1].length) {
res.marketValue = Math.round(marketValue * res.weight / weights * 100) / 100
} else {
res.marketValue = Math.round(marketValue * res.weight / weights * 100) / 100
}
})
} else {
countList.map((res: any, index: number) => {
if (res.nav) {
res.marketValue = Math.round(moneyList[moneyList.length - 1][index].marketValue / moneyList[moneyList.length - 1][index].nav * res.nav * 100) / 100
} else {
res.marketValue = moneyList[moneyList.length - 1][index].marketValue
}
marketValue += res.marketValue
})
}
}
moneyList.push(countList)
navList.push({ sysDate: sysDate[0], nav: Math.round(marketValue / Number(fundData.value.initAmount) * 10000) / 10000 })
})
legend.push('本基金')
seriesList.push(getLineSeries({ name: '本基金', color: colorPalette[colorPalette.length - 1] }))
seriesList.push(getLineSeries({ name: '本基金', yAxisIndex: 1, xAxisIndex: 1, color: colorPalette[colorPalette.length - 1] }))
let maxDrawdown = 0
navList.forEach((ele: any) => {
let drawdown = 0
const nav = Number(ele.nav)
if (nav > maxDrawdown) {
maxDrawdown = nav
drawdown = 0
} else {
drawdown = Math.round((nav - maxDrawdown) / maxDrawdown * 10000) / 100
}
seriesList[seriesList.length - 2].data.push([ele.sysDate, nav, drawdown])
seriesList[seriesList.length - 1].data.push([ele.sysDate, drawdown, nav])
})
const option = getBaseOption({
yAxis: [getYAxis({ }), getYAxis({ gridIndex: 1, axisLabel: '{value}%' })],
xAxis: [getXAxis({ data: sysDate }), getXAxis({ data: sysDate, gridIndex: 1, showFlag: false })],
series: seriesList,
formatter: function (data: any) {
let res = `${data[0].value[0]}<br/>`
data.forEach((ele: any) => {
res += `<span style="color: ${ele.color}">${ele.seriesName}:历史净值${ele.axisIndex % 2 === 0 ? ele.value[1] : ele.value[2]},回撤${ele.axisIndex % 2 === 0 ? ele.value[2] : ele.value[1]}%</span><br/>`
})
return res
},
legend: { data: legend, top: '40px' },
title: [{ text: '历史净值' }, { text: '回撤情况', top: '54%' }],
grid: [{ bottom: '54%', left: '40px', right: '40px', top: '90px' }, { top: '66%', bottom: '20px', left: '40px', right: '40px' }],
color: colorPalette
})
if (!destroyedFlag.value && document.getElementById(chartList.value[1].id)) {
const chart = echarts.init(document.getElementById(chartList.value[1].id))
chart.setOption(option)
charts.value[1] = chart
chartList.value[1].loading = false
}
}
const generateColors = (numLines: number) => {
const colors = []
const m = numLines - 1
if (m > 0) {
const startHue = 30
const endHue = 330
const step = m === 1 ? 0 : (endHue - startHue) / (m - 1)
for (let i = 0; i < m; i++) {
const h = startHue + (step * i)
const s = 70
const l = 70
colors.push(hslToHex(h, s, l))
}
}
colors.push('#ff0000')
return colors
}
const hslToHex = (h : number, s: number, l: number) => {
h /= 360
s /= 100
l /= 100
const c = (1 - Math.abs(2 * l - 1)) * s
const x = c * (1 - Math.abs((h * 6) % 2 - 1))
const m = l - c / 2
let r, g, b
const i = Math.floor(h * 6)
switch (i) {
case 0: r = c; g = x; b = 0; break
case 1: r = x; g = c; b = 0; break
case 2: r = 0; g = c; b = x; break
case 3: r = 0; g = x; b = c; break
case 4: r = x; g = 0; b = c; break
default: r = c; g = 0; b = x; break
}
r = Math.round((r + m) * 255)
g = Math.round((g + m) * 255)
b = Math.round((b + m) * 255)
return '#' + [r, g, b].map(v => v.toString(16).padStart(2, '0')).join('')
}
const showFileListPopup = ref(false)
const showFileList = () => {
showFileListPopup.value = true
}
onMounted (() =>{
init()
})
</script>
<style lang='scss' scoped>
@import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
.page-div {
box-sizing: border-box;
overflow-y: auto;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
padding-top: 86px;
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
@media screen and (min-width: 992px) {
left: calc(50vw - 496px);
right: calc(50vw - 496px);
}
}
.chart-container {
height: 40vh;
@media screen and (min-width: 768px) {
height: 60vh;
}
}
.listDiv {
padding: 0 16px;
.data-list-container {
width: 100%;
border-radius: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
background-color: #fff;
max-height: none !important;
overflow: visible !important;
height: auto !important;
-webkit-overflow-scrolling: auto;
.list-header {
background-color: #f5f7fa;
border-bottom: 1px solid #e8ebed;
}
.list-item {
display: flex;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #e8ebed;
height: 48px;
box-sizing: border-box;
&:last-child {
border-bottom: none;
}
}
.list-body {
overflow: visible !important;
max-height: none !important;
height: auto !important;
}
.list-col {
flex: 1;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 0 8px;
font-size: 14px;
position: relative;
color: #333;
&.date-col {
flex: 0 0 30%;
text-align: left;
font-weight: 500;
}
.budgetHeight {
height: 48px;
line-height: 48px;
}
.tag-div {
position: absolute;
right: 0;
top: 2px;
}
}
.no-data {
padding: 30px 0;
text-align: center;
color: #999;
background-color: #fafafa;
}
}
}
@media screen and (max-width: 768px) {
.listDiv {
padding: 0 12px;
.data-list-container {
.list-item {
padding: 10px 12px;
height: 44px;
}
.list-col {
font-size: 12px;
padding: 0 4px;
color: #333;
&.date-col {
flex: 0 0 25%;
}
}
}
}
}
}
.file-list {
max-height: calc(100% - 60px);
overflow-y: auto;
padding-bottom: 20px;
.file-item {
padding: 12px 0;
border-bottom: 1px solid #f5f5f5;
}
.file-link {
display: flex;
align-items: center;
color: #333;
text-decoration: none;
i {
font-size: 20px;
margin-right: 10px;
}
.file-title {
flex: 1;
font-size: 14px;
}
}
@media screen and (min-width: 768px) {
padding: 0 16px 20px;
.file-item {
padding: 16px 0;
}
.file-link {
i {
font-size: 24px;
margin-right: 16px;
}
.file-title {
font-size: 16px;
}
}
}
}
</style>

View File

@@ -0,0 +1,260 @@
<template>
<div class="page-div">
<back-button />
<div class="header">
<h1 class="title">基金列表</h1>
</div>
<div class="info-card">
<div class="tab-container">
<button v-for="tab in tabList" :key="tab.dicKey" @click="activeTab = tab.dicKey"
:class="{ 'active': activeTab === tab.dicKey }" class="tab-button">
{{ tab.dicValue }}
</button>
</div>
</div>
<div class="grid">
<van-grid :column-num="1" :gutter="16" :border="false">
<van-grid-item v-for="item in fundList" :key="item.indexId">
<div @touchstart="onTouchStart(item.fundName)" @touchend="onTouchEnd(item.fundName, 'fund-detail', item.fundId)" class="item"
@touchcancel="onTouchCancel(item.fundName)" :class="{ 'tapped': tappedItem === item.fundName }">
<div class="card-content" :style="{ backgroundColor: item.bgColor, borderLeftColor: item.color }">
<div class="card-code" :style="{ color: item.color }">{{ item.fundName }}</div>
<div class="d-flex">
<div class="leftValue d-flex justify-center align-center pe-16">
<div class="text-center">
<div class="blue-color cagr">{{ item.cagr ? item.cagr + '%' : '暂无' }}</div>
<div class="tip">复合年增长率</div>
</div>
</div>
<div class="rightChart flex-1 d-flex justify-center align-center">
<van-loading v-show="item.loading" type="spinner" color="#1989fa" class="loading" />
<div style="width: 100%;">
<div class="chart-div" :id="item.fundId + ''"></div>
<div class="d-flex justify-between">
<div class="tip">{{ item.beginDay }}</div>
<div class="tip">{{ item.endDay }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</van-grid-item>
</van-grid>
</div>
</div>
</template>
<script setup lang="ts">
import BackButton from '@/components/back-button.vue'
import { tapMixins } from '@/mixins/tap-mixins'
const { tappedItem, onTouchStart, onTouchEnd, onTouchCancel } = tapMixins()
import { ref, onMounted, watch, nextTick } from 'vue'
let tabList = ref<any[]>([])
import { sysDicListByType, stockAFundList, stockAFundInfo, stockAFundHisFundHisChart } from '@/utils/api'
const activeTab = ref('')
const init = async () => {
const { data } = await sysDicListByType({ dicType: 'fund_type' })
tabList.value = data
activeTab.value = data[0].dicKey
}
watch(activeTab, () => {
disposeAll()
charts.value = []
fundList.value = []
handleTabChange()
})
import { colorList } from '@/utils/colorList'
let fundList = ref<any[]>([])
const handleTabChange = async () => {
const { data } = await stockAFundList({ curPage: 1, limit: 999, fundType: activeTab.value })
data.list.map((ele: any, index: number) => {
ele.loading = true
const colorIndex = index % colorList.length
ele.color = colorList[colorIndex].color
ele.bgColor = colorList[colorIndex].bgColor
ele.cagr = ''
ele.beginDay = ''
ele.endDay = ''
})
charts.value = new Array(data.list.length).fill(null)
fundList.value = data.list
nextTick(() =>{
setData()
})
}
import { chartMixins } from '@/mixins/chart-mixins'
const { charts, destroyedFlag, disposeAll } = chartMixins()
import * as echarts from 'echarts'
import { getLineSeries, getBaseOption, getXAxis, getYAxis } from '@/utils/chart'
const setData = async() => {
for (let i = 0 ; i < fundList.value.length ; i++) {
if (fundList.value && fundList.value.length > i && !fundList.value[i].cagr) {
const { data: info } = await stockAFundInfo(fundList.value[i].fundId)
if (info && fundList.value[i]) {
fundList.value[i].cagr = info.cagr
}
}
if (fundList.value && fundList.value.length > i && fundList.value[i].loading) {
const { data: chartData } = await stockAFundHisFundHisChart({ fundId: fundList.value[i].fundId })
const sysDate: any = []
const seriesData: any = []
chartData.forEach((ele: any) => {
sysDate.push(ele.sysDate)
seriesData.push({ value: ele.nav })
})
const markPoint = []
if (seriesData.length > 0) {
if (fundList.value && fundList.value.length > i) {
fundList.value[i].beginDay = sysDate[0]
fundList.value[i].endDay = sysDate[sysDate.length - 1]
}
markPoint.push({
symbolSize: 30,
coord: [0, seriesData[0].value],
label: {
formatter: function () {
return seriesData[0].value
},
color: '#ff0000',
},
itemStyle: {
color: 'rgba(0,0,0,0)'
}
})
markPoint.push({
symbolSize: 30,
coord: [sysDate.length - 1, seriesData[seriesData.length - 1].value],
label: {
formatter: function () {
return seriesData[seriesData.length - 1].value
},
color: '#ff0000',
},
itemStyle: {
color: 'rgba(0,0,0,0)'
}
})
}
const series = [
getLineSeries({ data: seriesData, name: '历史净值', markPoint: { data : markPoint } }),
]
const option = getBaseOption({
yAxis: [getYAxis({ showFlag: false })],
xAxis: [getXAxis({ data: sysDate, showFlag: false })],
series: series,
grid: [{ top: '20px', bottom: '1px', left: '20px', right: '20px' }]
})
if (!destroyedFlag.value && fundList.value && fundList.value.length > i && document.getElementById(fundList.value[i].fundId)) {
if (charts.value[i]) {
charts.value[i]?.dispose()
charts.value[i] = null
}
const chart = echarts.init(document.getElementById(fundList.value[i].fundId))
chart.setOption(option)
charts.value[i] = chart
fundList.value[i].loading = false
}
}
}
}
import { useRoute } from 'vue-router'
const route = useRoute()
watch(
() => route.fullPath,
(newPath, oldPath) => {
if (oldPath?.includes('target-table')) {
nextTick(() => {
handleTabChange()
})
} else if (oldPath?.includes('fund-detail')) {
nextTick(() => {
setData()
})
}
if (newPath?.includes('target-table')) {
disposeAll()
charts.value = []
fundList.value = []
}
},
{ immediate: true }
)
onMounted (() =>{
init()
})
</script>
<style lang='scss' scoped>
.card-code {
margin-bottom: 16px !important;
}
.leftValue {
width: 96px;
height: 100px;
.cagr {
font-size: 18px;
}
.tip {
color: #666;
margin-top: 12px;
}
}
.rightChart {
width: 100%;
height: 100px;
position: relative;
.loading {
position: absolute;
z-index: 2;
}
.chart-div {
height: 80px;
width: 100%;
}
}
@media screen and (min-width: 768px) {
.grid {
padding: 0 24px;
}
.card-content {
padding: 24px;
}
.card-code {
margin-bottom: 24px !important;
font-size: 18px;
}
.leftValue {
width: 240px;
.cagr {
font-size: 24px;
}
.tip {
margin-top: 16px;
font-size: 14px;
}
}
.chart-div {
height: 80px;
}
}
</style>

View File

@@ -0,0 +1,356 @@
<template>
<div class="page-div">
<back-button />
<div class="header">
<div class="header-content">
<h1 class="title">{{ groupData?.stockGroupName }}</h1>
</div>
</div>
<div class="info-card financial-data">
<div class="data-item">
<div class="data-label">总资产</div>
<div class="data-value">{{ groupData?.totalAmount }}</div>
</div>
<div class="data-item">
<div class="data-label">总盈亏</div>
<div class="data-value">
<span :class="groupData?.totalIncome >= 0 ? 'red-color' : 'green-color'">{{ groupData?.totalIncome }}</span>
<span :class="groupData?.totalIncomeRate >= 0 ? 'red-color' : 'green-color'">({{ groupData?.totalIncomeRate }}%)</span>
</div>
</div>
<div class="data-item">
<div class="data-label">当日盈亏</div>
<div class="data-value">
<span :class="groupData?.todayIncome >= 0 ? 'red-color' : 'green-color'">{{ groupData?.todayIncome }}</span>
<span :class="groupData?.todayIncomeRate >= 0 ? 'red-color' : 'green-color'">({{ groupData?.todayIncomeRate }}%)</span>
</div>
</div>
<div class="data-item">
<div class="data-label">总市值</div>
<div class="data-value">{{ groupData?.hkTotalMarket }}</div>
</div>
<div class="data-item">
<div class="data-label">可用</div>
<div class="data-value">{{ groupData?.totalCash }}</div>
</div>
</div>
<div class="info-card" v-if="chartList.length > 0">
<div class="chart-container">
<van-loading v-show="chartList[0].loading" type="spinner" color="#1989fa" class="loading" />
<div :id="`${chartList[0].id}`" class="chartDiv"></div>
</div>
</div>
<div class="info-card">
<div class="tableDiv">
<table border="1">
<thead>
<tr>
<th class="td--column--1">股票/市值</th>
<th>盈亏</th>
<th>持仓</th>
<th>成本/现价</th>
</tr>
</thead>
<tbody>
<tr v-for="(hold, index) in holdList" :key="index" @click="showTradingRecord(hold)">
<td class="td--column--1">
<div>{{ hold.stockName }}</div>
<div>{{ hold.hkStockMarket }}</div>
</td>
<td>
<div>{{ hold.income }}</div>
<div>{{ hold.incomeRate }}%</div>
</td>
<td>
{{ hold.shares }}<br/>
</td>
<td>
<div>{{ hold.costPrice }}</div>
<div>{{ hold.hkStockPrice }}</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<van-popup v-model:show="showTradingRecordPopup" position="bottom" :style="{ height: '70%' }">
<div class="p-16">
<div class="d-flex justify-content-between items-center mb-16">
<div class="sub-title f-b">{{ stockCode }}{{ stockName }}交易记录</div>
<van-icon name="close" @click="showTradingRecordPopup = false" />
</div>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad"
:immediate-check="false">
<van-cell v-for="(item, index) in tradeList" :key="index" :class="['trade-item', `trade-type-${item.tradeType}`]">
<div class="trade-header d-flex justify-content-between items-center f-b">
<div class="trade-date">{{ item.tradeDate }}</div>
<div class="trade-type-label f-b">{{ item.tradeType === 'dividend' ? '分红' : item.tradeType === 'cash' ? '派息' : item.tradeType === 'buy' ? '买入' : '卖出' }}</div>
</div>
<div class="trade-content">
<div v-if="item.tradeType === 'dividend'" class="dividend-content">
<div class="trade-amount">送股数量: {{ item.sharesChange }}</div>
</div>
<div v-else-if="item.tradeType === 'cash'" class="cash-content">
<div class="trade-amount">每股派息: {{ item.dps }}{{ item.dividendUnit }}</div>
</div>
<div v-else-if="item.tradeType === 'buy' || item.tradeType === 'sell'" class="trade-content-main">
<div class="trade-stock-info">
<span class="stock-price">价格: {{ item.stockPrice }}</span>
</div>
<div class="trade-shares">数量: {{ item.sharesChange }}</div>
<div class="trade-market">金额: {{ item.tradeMarket }}</div>
</div>
</div>
</van-cell>
</van-list>
</van-pull-refresh>
</div>
</van-popup>
</div>
</template>
<script setup lang='ts'>
import BackButton from '@/components/back-button.vue'
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
import { groupInfo, groupHisChartIncomeRate, groupHoldList, groupTradeList } from '@/utils/api'
const groupData: any = ref(null)
import { formatNumber } from '@/utils'
import * as echarts from 'echarts'
import { getLineSeries, getBaseOption, getXAxis, getYAxis } from '@/utils/chart'
const init = async () => {
try {
const {data } = await groupInfo(route.params.id + '')
groupData.value = {
stockGroupName: data.stockGroup && data.stockGroup.stockGroupName ? data.stockGroup.stockGroupName : '',
totalAmount: data.holdInfo && data.holdInfo.totalAmount ? formatNumber(data.holdInfo.totalAmount) : '',
totalIncome: data.holdInfo && data.holdInfo.totalIncome ? formatNumber(data.holdInfo.totalIncome) : '',
totalIncomeRate: data.holdInfo && data.holdInfo.totalIncomeRate ? data.holdInfo.totalIncomeRate : '',
todayIncome: data.holdInfo && data.holdInfo.todayIncome ? formatNumber(data.holdInfo.todayIncome) : '',
todayIncomeRate: data.holdInfo && data.holdInfo.todayIncomeRate ? data.holdInfo.todayIncomeRate : '',
hkTotalMarket: data.holdInfo && data.holdInfo.hkTotalMarket ? formatNumber(data.holdInfo.hkTotalMarket) : '',
totalCash: data.holdInfo && data.holdInfo.totalCash ? formatNumber(data.holdInfo.totalCash) : ''
}
getChartData()
getTableData()
} catch (error) {
console.error('初始化数据失败:', error)
}
}
const getChartData = async () => {
const { data } = await groupHisChartIncomeRate({ groupId: route.params.id + '' })
const sysDate: any = []
const seriesData: any = []
data.forEach((ele: any) => {
sysDate.push(ele.sysDate)
seriesData.push({ value: ele.nav })
})
const option = getBaseOption({
yAxis: [getYAxis({ })],
xAxis: [getXAxis({ data: sysDate })],
series: [getLineSeries({ data: seriesData, name: '累计收益率' })],
grid: [{ top: '20px', bottom: '20px', left: '40px', right: '40px' }]
})
if (!destroyedFlag.value && document.getElementById(chartList.value[0].id)) {
let chart = echarts.init(document.getElementById(`${chartList.value[0].id}`))
chart.setOption(option)
charts.value[0] = chart
chartList.value[0].loading = false
}
}
const holdList = ref<any[]>([])
const getTableData = async () => {
const { data } = await groupHoldList({ curPage: 1, limit: 999, groupId: route.params.id + '' })
let hkStockMarket = 0
let shares = 0
let incomeAll = 0
let holdCostALL = 0
data.list.map((ele: any) => {
ele.stockPrice = formatNumber(ele.stockPrice)
if (ele.stockCode !== 'CASH-HKD') {
const income = Math.round((ele.hkStockMarket - ele.holdCost) * 100) / 100
incomeAll += income
holdCostALL += ele.holdCost
ele.incomeRate = ele.holdCost ? Math.round(income / ele.holdCost * 10000) / 100 : 0
ele.income = formatNumber(income)
hkStockMarket += ele.hkStockMarket
shares += ele.shares
}
ele.hkStockMarket = formatNumber(ele.hkStockMarket)
ele.shares = formatNumber(ele.shares)
})
incomeAll = Math.round(incomeAll * 100) / 100
data.list.push({
stockName: '总计',
hkStockMarket: formatNumber(Math.round(hkStockMarket * 100) / 100),
shares: formatNumber(Math.round(shares * 100) / 100),
income: formatNumber(incomeAll),
incomeRate: holdCostALL ? Math.round(incomeAll / holdCostALL * 10000) / 100 : 0
})
holdList.value = data.list
}
const showTradingRecordPopup = ref(false)
const stockCode = ref('')
const stockName = ref('')
const showTradingRecord = (item: any) => {
showTradingRecordPopup.value = true
stockCode.value = item.stockCode
stockName.value = item.stockName
onRefresh()
}
const curPage = ref(1)
const tradeList = ref<any[]>([])
const onRefresh = () => {
curPage.value = 1
tradeList.value = []
onLoad()
}
const loading = ref(false)
const totalCount = ref(0)
const refreshing = ref(false)
const finished = ref(false)
const onLoad = async () => {
loading.value = true
try {
const { data } = await groupTradeList({ groupId: route.params.id + '', stockCode: stockCode.value, curPage: curPage.value, limit: 20 })
totalCount.value = data.totalCount
if (refreshing.value) {
tradeList.value = data.list
refreshing.value = false
} else {
tradeList.value = tradeList.value.concat(data.list)
}
if (tradeList.value.length >= data.totalCount) {
finished.value = true
} else {
curPage.value++
}
} catch (error) {
console.log(error)
}
}
import { chartMixins } from '@/mixins/chart-mixins'
const { charts, destroyedFlag } = chartMixins()
let chartList = ref([{ id: 'group_chart', loading: true }])
onMounted (() =>{
charts.value = [null]
init()
})
</script>
<style lang='scss' scoped>
.page-div {
padding-top: 86px;
height: auto;
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
@media screen and (min-width: 992px) {
left: calc(50vw - 496px);
right: calc(50vw - 496px);
}
}
.chart-container {
height: 26vh;
@media screen and (min-width: 768px) {
height: calc(50vh - 226px);
}
}
.tableDiv {
th {
width: 150px;
}
.firstTd {
min-width: 80px;
max-width: 120px;
width: auto;
@media screen and (min-width: 768px) {
min-width: 150px;
max-width: 400px;
}
}
}
}
.trade-item {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
}
.trade-header {
margin-bottom: 8px;
.trade-date {
color: #333;
font-size: 14px;
}
.trade-type-label {
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
}
.trade-type-buy .trade-type-label {
background-color: #e1f3d8;
color: #52c41a;
}
.trade-type-sell .trade-type-label {
background-color: #fff1f0;
color: #f5222d;
}
.trade-type-dividend .trade-type-label,
.trade-type-cash .trade-type-label {
background-color: #e6f7ff;
color: #1890ff;
}
.trade-content {
display: flex;
flex-direction: column;
gap: 4px;
.trade-stock-info {
display: flex;
justify-content: space-between;
align-items: center;
}
.trade-shares,
.trade-market,
.trade-amount {
font-size: 14px;
color: #666;
}
.trade-price {
font-size: 14px;
color: #333;
}
}
</style>

View File

@@ -0,0 +1,213 @@
<template>
<div class="page-div">
<back-button />
<div class="header">
<h1 class="title">组合列表</h1>
</div>
<div class="grid">
<van-grid :column-num="1" :gutter="16" :border="false">
<van-grid-item v-for="item in list" :key="item.id">
<div @touchstart="onTouchStart(item.stockGroupName)" @touchend="onTouchEnd(item.stockGroupName, 'group-detail', item.id)" class="item"
@touchcancel="onTouchCancel(item.stockGroupName)" :class="{ 'tapped': tappedItem === item.stockGroupName }">
<div class="card-content" :style="{ backgroundColor: item.bgColor, borderLeftColor: item.color }">
<div class="card-code" :style="{ color: item.color }">{{ item.stockGroupName }}</div>
<div class="d-flex">
<div class="leftValue d-flex justify-center align-center pe-16">
<div>
<div class="card-name">累计收益率
<span :class="item.totalIncomeRate > 0 ? 'red-color' : item.totalIncomeRate < 0 ? 'green-color' : ''">{{ item.totalIncomeRate }}%</span></div>
<div class="card-note">建仓日期{{ item.createDate }}</div>
</div>
</div>
<div class="rightChart flex-1 d-flex justify-center align-center">
<van-loading v-show="item.loading" type="spinner" color="#1989fa" class="loading" />
<div style="width: 100%;">
<div class="chart-div" :id="item.id + ''"></div>
<div class="d-flex justify-between">
<div class="tip">{{ item.beginDay }}</div>
<div class="tip">{{ item.endDay }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</van-grid-item>
</van-grid>
</div>
</div>
</template>
<script setup lang="ts">
import BackButton from '@/components/back-button.vue'
import { tapMixins } from '@/mixins/tap-mixins'
const { tappedItem, onTouchStart, onTouchEnd, onTouchCancel } = tapMixins()
import { ref, onMounted, nextTick, watch } from 'vue'
let list = ref<any[]>([])
import { groupList, groupHisChartIncomeRate } from '@/utils/api'
import { colorList } from '@/utils/colorList'
const init = async () => {
const { data } = await groupList('curPage=1&limit=999&isPublic=1,2')
data.list.map((ele: any, index: number) => {
const colorIndex = index % colorList.length
ele.color = colorList[colorIndex].color
ele.bgColor = colorList[colorIndex].bgColor
ele.totalIncomeRate = ele.holdInfo && ele.holdInfo.totalIncomeRate ? ele.holdInfo.totalIncomeRate : ''
ele.loading = true
ele.beginDay = ''
ele.endDay = ''
})
charts.value = new Array(data.list.length).fill(null)
list.value = data.list
nextTick(() =>{
setData()
})
}
import { chartMixins } from '@/mixins/chart-mixins'
const { charts, destroyedFlag, disposeAll } = chartMixins()
import * as echarts from 'echarts'
import { getLineSeries, getBaseOption, getXAxis, getYAxis } from '@/utils/chart'
const setData = async() => {
for (let i = 0 ; i < list.value.length ; i++) {
if (list.value && list.value.length > i && list.value[i].loading) {
const { data: chartData } = await groupHisChartIncomeRate({ groupId: list.value[i].id })
const sysDate: any = []
const seriesData: any = []
chartData.forEach((ele: any) => {
sysDate.push(ele.sysDate)
seriesData.push({ value: ele.nav })
})
const markPoint = []
if (seriesData.length > 0) {
if (list.value && list.value.length > i) {
list.value[i].beginDay = sysDate[0]
list.value[i].endDay = sysDate[sysDate.length - 1]
}
markPoint.push({
symbolSize: 30,
coord: [0, seriesData[0].value],
label: {
formatter: function () {
return seriesData[0].value
},
color: '#ff0000',
},
itemStyle: {
color: 'rgba(0,0,0,0)'
}
})
markPoint.push({
symbolSize: 30,
coord: [sysDate.length - 1, seriesData[seriesData.length - 1].value],
label: {
formatter: function () {
return seriesData[seriesData.length - 1].value
},
color: '#ff0000',
},
itemStyle: {
color: 'rgba(0,0,0,0)'
}
})
}
const series = [
getLineSeries({ data: seriesData, name: '累计收益率', markPoint: { data : markPoint } }),
]
const option = getBaseOption({
yAxis: [getYAxis({ showFlag: false })],
xAxis: [getXAxis({ data: sysDate, showFlag: false })],
series: series,
grid: [{ top: '20px', bottom: '1px', left: '20px', right: '20px' }]
})
if (!destroyedFlag.value && list.value && list.value.length > i && document.getElementById(list.value[i].id)) {
if (charts.value[i]) {
charts.value[i]?.dispose()
charts.value[i] = null
}
const chart = echarts.init(document.getElementById(list.value[i].id))
chart.setOption(option)
charts.value[i] = chart
list.value[i].loading = false
}
}
}
}
import { useRoute } from 'vue-router'
const route = useRoute()
watch(
() => route.fullPath,
(newPath, oldPath) => {
if (oldPath?.includes('target-table')) {
nextTick(() => {
init()
})
} else if (oldPath?.includes('group-detail')) {
nextTick(() => {
setData()
})
}
if (newPath?.includes('target-table')) {
disposeAll()
charts.value = []
list.value = []
}
},
{ immediate: true }
)
onMounted (() =>{
init()
})
</script>
<style lang='scss' scoped>
.leftValue {
width: 160px;
height: 100px;
}
.rightChart {
width: 100%;
height: 100px;
position: relative;
.loading {
position: absolute;
z-index: 2;
}
.chart-div {
height: 80px;
width: 100%;
}
}
@media screen and (min-width: 768px) {
.grid {
padding: 0 24px;
}
.card-content {
padding: 24px;
}
.card-code {
margin-bottom: 24px !important;
font-size: 18px;
}
.leftValue {
width: 280px;
}
.chart-div {
height: 80px;
}
}
</style>

View File

@@ -0,0 +1,473 @@
<template>
<div class="page-div">
<back-button />
<div class="header">
<div class="header-content">
<h1 class="title">{{ indexData?.indexCode }}</h1>
<p class="subtitle">{{ indexData?.indexName || '指数详情' }}</p>
</div>
</div>
<div class="info-card">
<div class="d-flex justify-content-between align-items-center">
<div class="f-b">相关文件</div>
<div v-if="fileList.length > 0" @click="handleViewDetails" class="blue-color">
查看详情
</div>
<div v-else class="red-color">暂无数据</div>
</div>
</div>
<div class="info-card">
<h3 class="card-title">指数走势图</h3>
<div class="chart-container">
<van-loading v-show="chartList[0].loading" type="spinner" color="#1989fa" class="loading" />
<div :id="`${chartList[0].id}`" class="chartDiv"></div>
</div>
</div>
<div class="info-card">
<h3 class="card-title">追踪汇总图</h3>
<div class="chart-container">
<van-loading v-show="chartList[1].loading" type="spinner" color="#1989fa" class="loading" />
<div :id="`${chartList[1].id}`" class="chartDiv"></div>
</div>
</div>
<div class="info-card">
<div class="tab-container">
<button @click="activeTab = 0" :class="{ 'active': activeTab === 0 }" class="tab-button">行业</button>
<button @click="activeTab = 1" :class="{ 'active': activeTab === 1 }" class="tab-button">指数持仓</button>
<button @click="activeTab = 2" :class="{ 'active': activeTab === 2 }" class="tab-button">跟踪基金</button>
</div>
</div>
<div v-if="activeTab === 0">
<div class="info-card">
<h3 class="card-title">一级行业</h3>
<div class="chart-container">
<van-loading v-show="chartList[2].loading" type="spinner" color="#1989fa" class="loading" />
<div :id="`${chartList[2].id}`" class="chartDiv"></div>
</div>
</div>
<div class="info-card">
<h3 class="card-title">二级行业</h3>
<div class="chart-container">
<van-loading v-show="chartList[3].loading" type="spinner" color="#1989fa" class="loading" />
<div :id="`${chartList[3].id}`" class="chartDiv"></div>
</div>
</div>
</div>
<div v-if="activeTab === 1">
<div class="info-card">
<div class="tableDiv">
<table border="1">
<thead>
<tr>
<th class="td--column--1">名称</th>
<th>上期权重</th>
<th>本期权重</th>
<th>权重变化</th>
<th>排名</th>
</tr>
</thead>
<tbody>
<tr v-for="index in indexDetailList" :key="index.id">
<td class="td--column--1">
{{ index.ticker }}
<div class="van-ellipsis firstTd">{{ index.tickerName }}</div>
</td>
<td>{{ index.lastQuarterWeight ? index.lastQuarterWeight + '%' : '' }}</td>
<td>{{ index.weight ? index.weight + '%' : '' }}</td>
<td :class="index.weightChange > 0 ? 'red-color' : index.weightChange < 0 ? 'green-color' : ''">
{{ index.weightChange ? index.weightChange + '%' : '' }}</td>
<td>{{ index.rankNum }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div v-if="activeTab === 2">
<div class="info-card">
<div class="tableDiv">
<table border="1">
<thead>
<tr>
<th class="td--column--1">名称</th>
<th>净资产(亿)</th>
<th>净值</th>
<th>股本(亿)</th>
<th>调整日</th>
</tr>
</thead>
<tbody>
<tr v-for="fund in fundList" :key="fund.id" @click="$router.push({ name: 'etf-detail', params: { id: fund.etfCode } })">
<td class="td--column--1">
{{ fund.etfCode }}
<div class="van-ellipsis firstTd">{{ fund.etfName }}</div>
</td>
<td>{{ fund.netAssets }}</td>
<td>{{ fund.nav }}</td>
<td>{{ fund.sharesOutstanding }}</td>
<td><div v-html="fund.day"></div></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<van-popup v-model:show="showFileListPopup" position="bottom" :style="{ height: '70%' }">
<div class="p-16">
<div class="d-flex justify-content-between items-center mb-16">
<div class="sub-title f-b">文件列表</div>
<van-icon name="close" @click="showFileListPopup = false" />
</div>
<div class="file-list">
<div v-for="(file, index) in fileList" :key="index" class="file-item">
<a :href="file.link" target="_blank" class="file-link">
<i class="iconfont" :class="`icon-${file.type}`"></i>
<span class="file-title van-ellipsis">{{ file.title }}</span>
</a>
</div>
</div>
</div>
</van-popup>
</template>
<script setup lang='ts'>
import BackButton from '@/components/back-button.vue'
import { ref, onMounted, watch, nextTick } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const loadFlag = ref(false)
watch(
() => route.fullPath,
(newPath, oldPath) => {
if (oldPath?.includes('index-list') && newPath?.includes('index-detail') && loadFlag.value) {
activeTab.value = 0
charts.value = [null, null, null, null]
nextTick(() => {
init()
})
}
if (newPath?.includes('index-list')) {
disposeAll()
}
},
{ immediate: true }
)
import { indexInfo, indexInfoIndexHistChart, indexFundHisQueryFundHis, indexInfoIndustry1Chart, indexInfoIndustry2Chart, indexFundList } from '@/utils/api'
const indexData: any = ref(null)
const fileList = ref<any[]>([])
const indexDetailList = ref<any[]>([])
const fundList = ref<any[]>([])
import { addOrSubtractTime, formatNumberByUnit } from '@/utils'
const firstIndustry = ref<any[]>([])
const secondIndustry = ref<any[]>([])
import * as echarts from 'echarts'
import { getLineSeries, getBaseOption, getXAxis, getYAxis } from '@/utils/chart'
const init = async () => {
try {
const { data } = await indexInfo(route.params.id + '')
indexData.value = data
if (data.indexDescription) {
const list = JSON.parse(data.indexDescription).data
const reg1 = /(\.xls|\.xlsx)$/
const reg2 = /(\.pdf)$/
const reg3 = /(\.doc|\.docx)$/
const reg4 = /(\.ppt|\.pptx)$/
list.map((ele: any) => {
if (reg1.test(ele.link)) {
ele.type = 'excel'
}
if (reg2.test(ele.link)) {
ele.type = 'pdf'
}
if (reg3.test(ele.link)) {
ele.type = 'word'
}
if (reg4.test(ele.link)) {
ele.type = 'ppt'
}
})
fileList.value = list
}
indexDetailList.value = data.indexDetailList
const { data: indexHist } = await indexInfoIndexHistChart(route.params.id + '')
setLineChart(indexHist, 0)
const { data: fundsHis } = await indexFundHisQueryFundHis({
indexId: route.params.id + '',
timeB: addOrSubtractTime(new Date(), -3, 'year'),
timeE: new Date().toISOString().split('T')[0]
})
setLineChart(fundsHis, 1)
const { data: firstIndustryData } = await indexInfoIndustry1Chart(route.params.id + '')
firstIndustryData.map((ele: any) => {
ele.name = ele.industry1_name
ele.value = ele.weight
})
firstIndustry.value = firstIndustryData
const { data: secondIndustryData } = await indexInfoIndustry2Chart(route.params.id + '')
secondIndustryData.map((ele: any) => {
ele.name = ele.industry2_name
ele.value = ele.weight
})
secondIndustry.value = secondIndustryData
handleTabChange(activeTab.value)
const { data: fundData } = await indexFundList({ indexId: route.params.id + '' })
fundData.map((ele: any) =>{
ele.netAssets = ele.netAssets ? Math.round(ele.netAssets / 100000000) / 100 : ''
ele.sharesOutstanding = ele.sharesOutstanding ? Math.round(ele.sharesOutstanding / 100000000) / 100 : ''
ele.day = `${ele.lastQuarterDate ? ele.lastQuarterDate.slice(5,10) : ''}<br/>${ele.quarterDate ? ele.quarterDate.slice(5,10) : ''}`
})
fundList.value = fundData
} catch (error) {
console.error('初始化数据失败:', error)
}
}
const setLineChart = (data: any, index: number) => {
const sysDate = [] as string[]
let seriesList = [] as any[]
let color = []
const legend = {
data: [] as string[],
top: '0px',
show: false
}
let yAxis = [] as any[]
if (index === 0) {
color = ['blue']
legend.data = ['指数']
seriesList = [getLineSeries({ name: '指数' })]
yAxis = [getYAxis({ })]
data.forEach((ele: any) => {
sysDate.push(ele.sysDate)
seriesList[0].data.push({ value: ele.navPerShare })
})
} else{
color = ['blue', 'red']
legend.show = true
legend.data = ['净资产', '股本']
seriesList = [getLineSeries({ name: '净资产' }), getLineSeries({ name: '股本', yAxisIndex: 1 })]
yAxis = [getYAxis({ axisLabel: function (data: any) { return formatNumberByUnit(data) }}), getYAxis({ axisLabel: function (data: any) { return formatNumberByUnit(data) }, position: 'right' })]
data.forEach((ele: any) => {
sysDate.push(ele.sysDate)
seriesList[0].data.push({ value: ele.totalNetAssets })
seriesList[1].data.push({ value: ele.sharesOutstanding })
})
}
if (!destroyedFlag.value) {
let chart = echarts.init(document.getElementById(`${chartList.value[index].id}`))
const option = getBaseOption({
series: seriesList,
yAxis: yAxis,
xAxis: [getXAxis({ data: sysDate })],
tooltipFormatter: function (params: any) {
const data = Array.isArray(params) ? params : [params]
let backText = `日期:${data[0].name}<br/>`
data.forEach((ele) => {
backText += `<span style="color: ${ele.color}">${ele.seriesName}</span>:<span class="font-bold">${formatNumberByUnit(ele.value)}</span><br/>`
})
return backText
},
grid: [{ top: '20px', left: '50px', right: '50px', bottom: '20px' }],
legend: legend,
color: color
})
chart.setOption(option)
charts.value[index] = chart
chartList.value[index].loading = false
}
}
const showFileListPopup = ref(false)
const handleViewDetails = () => {
showFileListPopup.value = true
}
const activeTab = ref(0)
watch(activeTab, (newValue) => {
handleTabChange(newValue)
})
const handleTabChange = (tabIndex: number) => {
if (charts.value[2]) {
charts.value[2].dispose()
charts.value[2] = null
}
if (charts.value[3]) {
charts.value[3].dispose()
charts.value[3] = null
}
switch (tabIndex) {
case 0:
nextTick(() =>{
setTreemapChart(firstIndustry.value, 2)
setTreemapChart(secondIndustry.value, 3)
})
break
case 1:
break
case 2:
break
default:
break
}
}
const setTreemapChart = (list: any, index: number) => {
if (!destroyedFlag.value) {
let chart = echarts.init(document.getElementById(`${chartList.value[index].id}`))
const option = getBaseOption({
series: [{
type: 'treemap',
visibleMin: 300,
nodeClick: 'false',
label: {
show: true,
formatter: function (data: any) {
return `${data.data.name}(${data.data.value}%)`
}
},
scaleLimit: {
min: 1,
max: 1
},
itemStyle: {
borderColor: '#fff'
},
levels: [{
itemStyle: {
borderWidth: 0,
gapWidth: 3
}
}],
breadcrumb: { show: false },
data: list,
left: 0,
right: 0,
top: 0,
bottom: 0
}]
})
chart.setOption(option)
charts.value[index] = chart
chartList.value[index].loading = false
}
}
import { chartMixins } from '@/mixins/chart-mixins'
const { charts, destroyedFlag, disposeAll } = chartMixins()
let chartList = ref([{
id: 'index_hist_chart',
loading: true,
name: '指数走势'
}, {
id: 'funds_his_chart',
loading: true,
name: '追踪汇总'
}, {
id: 'first_industry_chart',
loading: true,
name: '一级行业'
}, {
id: 'second_industry_chart',
loading: true,
name: '二级行业'
}])
onMounted (() =>{
charts.value = [null, null, null, null]
init()
loadFlag.value = true
})
</script>
<style lang='scss' scoped>
.page-div {
overflow-y: auto;
padding-top: 105px;
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
@media screen and (min-width: 992px) {
left: calc(50vw - 496px);
right: calc(50vw - 496px);
}
}
.chart-container {
height: calc(50vh - 206px);
@media screen and (min-width: 768px) {
height: calc(50vh - 226px);
}
}
.firstTd {
min-width: 80px;
max-width: 120px;
width: auto;
@media screen and (min-width: 768px) {
min-width: 150px;
max-width: 400px;
}
}
}
.file-list {
max-height: calc(100% - 60px);
overflow-y: auto;
padding-bottom: 20px;
.file-item {
padding: 12px 0;
border-bottom: 1px solid #f5f5f5;
}
.file-link {
display: flex;
align-items: center;
color: #333;
text-decoration: none;
i {
font-size: 20px;
margin-right: 10px;
}
.file-title {
flex: 1;
font-size: 14px;
}
}
@media screen and (min-width: 768px) {
padding: 0 16px 20px;
.file-item {
padding: 16px 0;
}
.file-link {
i {
font-size: 24px;
margin-right: 16px;
}
.file-title {
font-size: 16px;
}
}
}
}
</style>

View File

@@ -0,0 +1,68 @@
<template>
<div class="page-div">
<back-button />
<div class="header flex items-center justify-center">
<h1 class="title">{{ seriesName }}</h1>
</div>
<div class="grid">
<van-grid :column-num="1" :gutter="16" :border="false">
<van-grid-item v-for="item in list" :key="item.indexId">
<div @touchstart="onTouchStart(item.indexName)" @touchend="onTouchEnd(item.indexName, 'index-detail', item.indexId)" class="item"
@touchcancel="onTouchCancel(item.indexName)" :class="{ 'tapped': tappedItem === item.indexName }">
<div class="card-content" :style="{ backgroundColor: item.bgColor, borderLeftColor: item.color }">
<div class="card-code" :style="{ color: item.color }">{{ item.indexCode }}</div>
<div class="card-name">{{ item.indexName }}</div>
<div class="card-note">{{ item.indexNote }}</div>
</div>
</div>
</van-grid-item>
</van-grid>
</div>
</div>
</template>
<script setup lang="ts">
import BackButton from '@/components/back-button.vue'
import { tapMixins } from '@/mixins/tap-mixins'
const { tappedItem, onTouchStart, onTouchEnd, onTouchCancel } = tapMixins()
import { ref, onMounted, watch, nextTick } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
let seriesName = ref('')
let list = ref<any[]>([])
import { indexInfoSeries, sysDicListByType } from '@/utils/api'
import { colorList } from '@/utils/colorList'
const init = async () => {
const { data: series } = await sysDicListByType({ dicType: 'index_series' })
const seriesItem = series.find((item: any) => { return item.dicKey === route.params.id })
seriesName.value = seriesItem.dicValue
const { data } = await indexInfoSeries({ seriesCode: route.params.id })
list.value = data.fundList
list.value.forEach((ele, index) => {
ele.color = colorList[index % colorList.length].color
ele.bgColor = colorList[index % colorList.length].bgColor
})
}
onMounted (() =>{
init()
})
watch(
() => route.fullPath,
(newPath, oldPath) => {
if (oldPath?.includes('series-list') && newPath?.includes('index-list')) {
nextTick(() => {
init()
})
}
},
{ immediate: true }
)
</script>
<style lang='scss' scoped>
</style>

View File

@@ -0,0 +1,53 @@
<template>
<div class="page-div">
<back-button />
<div class="header">
<h1 class="title">全球主要指数系列</h1>
</div>
<div class="info-card">
<p class="info-text">
本页面展示了全球主要指数系列包括国际知名指数如MSCIFTSES&P纳斯达克等和中国市场重要指数如中证系列恒指系列等
这些指数涵盖了全球主要金融市场帮助投资者了解全球市场趋势把握投资机会
</p>
</div>
<div class="grid">
<van-grid :column-num="2" :gutter="16" :border="false">
<van-grid-item class="indicator-card" v-for="item in list" :key="item.dicKey">
<div @touchstart="onTouchStart(item.dicValue)" @touchend="onTouchEnd(item.dicValue, 'index-list', item.dicKey)" class="item"
@touchcancel="onTouchCancel(item.dicValue)" :class="{ 'tapped': tappedItem === item.dicValue }">
<div class="card-content" :style="{ backgroundColor: item.bgColor, borderLeftColor: item.color }">
<div class="card-name">{{ item.dicValue }}</div>
</div>
</div>
</van-grid-item>
</van-grid>
</div>
</div>
</template>
<script setup lang="ts">
import BackButton from '@/components/back-button.vue'
import { tapMixins } from '@/mixins/tap-mixins'
const { tappedItem, onTouchStart, onTouchEnd, onTouchCancel } = tapMixins()
import { ref, onMounted } from 'vue'
let list = ref<any[]>([])
import { sysDicListByType } from '@/utils/api'
import { colorList } from '@/utils/colorList'
const init = async () => {
const { data } = await sysDicListByType({ dicType: 'index_series' })
list.value = data
list.value.forEach((ele, index) => {
ele.color = colorList[index % colorList.length].color
ele.bgColor = colorList[index % colorList.length].bgColor
})
}
onMounted (() =>{
init()
})
</script>
<style lang='scss' scoped>
</style>

122
src/views/target/table.vue Normal file
View File

@@ -0,0 +1,122 @@
<template>
<div class="page-content">
<van-image class="img" src="/img/indicate.jpeg" />
<div class="grid">
<van-grid :column-num="3" :gutter="16">
<van-grid-item v-for="item in tableList" :key="item.name">
<div @touchstart="onTouchStart(item.name)" @touchend="onTouchEnd(item.name, item.url)" class="item"
@touchcancel="onTouchCancel(item.name)" :class="{ 'tapped': tappedItem === item.name }">
<div class="icon d-flex justify-content-center align-items-center" :style="{ backgroundColor: item.bgColor }">
<van-icon :style="{ color: item.color }" :name="item.icon" size="28" />
</div>
<div>{{item.name}}</div>
</div>
</van-grid-item>
</van-grid>
</div>
<tabbar />
</div>
</template>
<script setup lang="ts">
import { colorList } from '@/utils/colorList'
import { tapMixins } from '@/mixins/tap-mixins'
const { tappedItem, onTouchStart, onTouchEnd, onTouchCancel } = tapMixins()
const tableList = [
{
url: 'economy-list',
name: '全球经济',
icon: 'chart-trending-o',
color: colorList[0].color,
bgColor: colorList[0].bgColor
},
{
url: '',
name: '股票数据',
icon: 'bar-chart-o',
color: colorList[1].color,
bgColor: colorList[1].bgColor
},
{
url: 'fred-list',
name: 'FRED专题',
icon: 'diamond-o',
color: colorList[2].color,
bgColor: colorList[2].bgColor
},
{
url: 'series-list',
name: '指数专题',
icon: 'discount-o',
color: colorList[3].color,
bgColor: colorList[3].bgColor
},
{
url: 'fund-list',
name: '基金',
icon: 'after-sale',
color: colorList[4].color,
bgColor: colorList[4].bgColor
},
{
url: 'group-list',
name: '精选组合',
icon: 'fire-o',
color: colorList[5].color,
bgColor: colorList[5].bgColor
}
]
</script>
<style lang='scss' scoped>
.img {
width: 100%;
height: 200px;
}
:deep(.van-grid) {
background-color: #F2F3F5;
padding: 16px 0;
}
:deep(.van-grid-item) {
text-align: center;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
transition: transform 0.15s ease, opacity 0.15s ease, box-shadow 0.15s ease, font-size 0.2s ease;
.van-grid-item__content {
border-radius: 16px;
height: 100%;
padding: 0;
background-color: rgba(240, 240, 240, 0.5);
.item {
padding: 16px 0;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: #ffffff;
&.tapped {
transform: scale(0.9);
opacity: 0.7;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
background-color: rgba(240, 240, 240, 0.5);
}
.icon {
margin-bottom: 12px;
width: 56px;
height: 56px;
border-radius: 16px;
display: flex;
justify-content: center;
align-items: center;
}
}
}
}
</style>

128
src/views/user/index.vue Normal file
View File

@@ -0,0 +1,128 @@
<template>
<div class="page-content tabbar-list">
<div class="user-top">
<van-image class="user-bg" src="/img/user-bg.jpeg" fit="cover" />
<van-image class="img" :src="isLogin && userInfo.headPic ? userInfo.headPic : '/img/user.png'" />
<div>
<div class="title" v-if="isLogin">{{ userInfo.realname }}</div>
<div class="desc">{{ isLogin ? userInfo.username : '您还未登录' }}</div>
</div>
</div>
<div class="content">
<div class="card">
<div class="title">我的沟通</div>
<communicate-item :list="list" />
</div>
</div>
<van-button class="exit" round block :type="isLogin ? 'danger' : 'primary'" size="small" native-type="submit"
@click="handleButton">{{ isLogin ? '退出登录' : '立即登录' }}</van-button>
<tabbar />
</div>
</template>
<script setup lang="ts">
import userInfoStore from '@/stores/userInfo'
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const userInfo = userInfoStore()
console.log(userInfo);
const isLogin = computed(() => !!userInfo.token)
const handleButton = async () => {
if (isLogin.value) {
await showConfirmDialog({
title: '提示',
message: '确定退出吗',
})
userInfo.token = ''
userInfo.headPic = ''
list.value = []
} else {
router.push({
name: 'login',
query: {
redirect: router.currentRoute.value.fullPath
}
})
}
}
import { cusLinkList } from '@/utils/api'
import { convertToUrl } from '@/utils'
const list = ref<any>([])
const getCusLink = async () => {
if (!isLogin.value) return
const url = convertToUrl({
limit: 3,
dataStatus: 1,
curPage: 1,
createUser: userInfo.id
})
const data = await cusLinkList(url)
list.value = data
}
getCusLink()
</script>
<style lang="scss" scoped>
.user-top {
position: relative;
display: flex;
z-index: 3;
padding: 30px 30px 80px;
.user-bg {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: -1;
}
.img {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
margin-right: 12px;
}
.title {
font-size: 16px;
font-weight: 600;
}
.desc {
color: #808080;
margin-top: 4px;
font-size: 14px;
}
}
.page-content {
height: calc(100vh - 60px);
display: flex;
flex-direction: column;
background-color: #f6f8fa;
position: relative;
.content {
position: relative;
height: calc(100% - 210px);
overflow: auto;
padding-top: 20px;
.card {
background-color: #fff;
border-radius: 10px;
margin: 10px;
margin-bottom: 20px;
padding: 16px 0;
.title {
font-weight: 600;
margin-left: 16px;
margin-bottom: 8px;
padding-bottom: 8px;
}
}
}
.exit {
width: 80%;
position: absolute;
bottom: 80px;
left: 10%;
}
}
</style>