Browse Source

Merge branch 'v2.0' into debug

yujinwen 3 months ago
parent
commit
386ffebda9
47 changed files with 2890 additions and 311 deletions
  1. 1 0
      .gitignore
  2. 2 0
      package.json
  3. 669 2
      pnpm-lock.yaml
  4. 2 0
      src/api/etaChart/index.js
  5. 43 0
      src/api/etaChart/user.js
  6. 9 5
      src/api/index.js
  7. 16 0
      src/api/system/common.js
  8. BIN
      src/assets/icons/pop_close.png
  9. BIN
      src/assets/icons/warning.png
  10. BIN
      src/assets/imgs/login_bg.png
  11. BIN
      src/assets/imgs/logo.png
  12. BIN
      src/assets/imgs/logo_login.png
  13. 1 0
      src/assets/svg/full_screen.svg
  14. 4 0
      src/assets/svg/show_arrow.svg
  15. 3 0
      src/assets/svg/star.svg
  16. 3 0
      src/assets/svg/star_fill.svg
  17. 4 0
      src/assets/svg/user_avatar.svg
  18. 67 0
      src/auto-imports.d.ts
  19. 0 21
      src/components/Empty.vue
  20. 28 0
      src/components/EmptyWrap.vue
  21. 49 0
      src/components/SelectChartCreator.vue
  22. 15 0
      src/components/globalComponents.js
  23. 21 0
      src/hooks/userInfo.js
  24. 115 36
      src/layout/Index.vue
  25. 8 6
      src/main.js
  26. 27 0
      src/plugin/dialog.jsx
  27. 16 22
      src/router/index.js
  28. 39 0
      src/router/modules/etaChart.js
  29. 38 1
      src/styles/common.scss
  30. 52 0
      src/styles/tdesign.scss
  31. 4 1
      src/styles/var.scss
  32. 32 0
      src/utils/common.js
  33. 0 39
      src/views/AutoLogin.vue
  34. 237 0
      src/views/Login.vue
  35. 96 68
      src/views/etaChart/Index.vue
  36. 147 0
      src/views/etaChart/components/ChartDetailPop.vue
  37. 80 75
      src/views/etaChart/components/ChartWrap.vue
  38. 4 35
      src/views/etaChart/components/ClassifyWrap.vue
  39. 170 0
      src/views/etaChart/components/chartList/ChartBox.vue
  40. 85 0
      src/views/etaChart/components/chartList/Index.vue
  41. 190 0
      src/views/user/favorite/components/ClassifyWrap.vue
  42. 127 0
      src/views/user/favorite/components/CollectChart.vue
  43. 85 0
      src/views/user/favorite/components/EditClassify.vue
  44. 159 0
      src/views/user/favorite/components/MoveChart.vue
  45. 179 0
      src/views/user/favorite/etaChart.vue
  46. 8 0
      vite.config.js
  47. 55 0
      zipBuildPlugin.js

+ 1 - 0
.gitignore

@@ -14,6 +14,7 @@ dist-ssr
 *.local
 eta_forum_front
 eta_forum_front.zip
+auto-imports.d.ts
 
 # Editor directories and files
 .vscode/*

+ 2 - 0
package.json

@@ -26,6 +26,8 @@
   },
   "devDependencies": {
     "@vitejs/plugin-vue": "^5.0.4",
+    "@vitejs/plugin-vue-jsx": "^4.1.1",
+    "archiver": "^7.0.1",
     "sass": "^1.75.0",
     "unplugin-auto-import": "^0.17.5",
     "unplugin-vue-components": "^0.26.0",

File diff suppressed because it is too large
+ 669 - 2
pnpm-lock.yaml


+ 2 - 0
src/api/etaChart/index.js

@@ -1,5 +1,7 @@
 import apiETAChart from './etaChart'
+import apiETAChartUser from './user'
 
 export {
     apiETAChart,
+    apiETAChartUser
 }

+ 43 - 0
src/api/etaChart/user.js

@@ -0,0 +1,43 @@
+import {get,post} from '@/api/index'
+
+// 图表用户模块
+export default{
+  // 新增分类
+  addClassify:(params)=>{
+    return post('/chart_collect/classify/add',params)
+  },
+  // 编辑分类
+  editClassify:(params)=>{
+    return post('/chart_collect/classify/edit',params)
+  },
+  // 移动分类
+  moveClassify:(params)=>{
+    return post('/chart_collect/classify/move',params)
+  },
+  // 删除分类
+  deleteClassify:(params)=>{
+    return post('/chart_collect/classify/delete',params)
+  },
+  // 分类列表
+  classifyList:(params)=>{
+    return get('/chart_collect/classify/list',params)
+  },
+
+  // 收藏图表
+  chartCollect:(params)=>{
+    return post('/chart_collect/add',params)
+  },
+  // 取消收藏图表
+  chartCollectCancel:(params)=>{
+    return post('/chart_collect/delete',params)
+  },
+  // 收藏图表列表数据
+  chartCollectList:(params)=>{
+    return get('/chart_collect/list',params)
+  },
+  // 批量转移图表
+  chartCollectBatchMove:(params)=>{
+    return post('/chart_collect/modify_chart_classify',params)
+  },
+  
+}

+ 9 - 5
src/api/index.js

@@ -4,6 +4,9 @@ import router from "@/router";
 import {useRequestLoading} from '@/hooks/useRequestLoading'
 import { MessagePlugin } from 'tdesign-vue-next';
 import CryptoJS from '@/utils/crypto'
+import {useUserInfo} from '@/hooks/userInfo'
+
+const {token}=useUserInfo()
 
 // Full config:  https://github.com/axios/axios#request-config
 // axios.defaults.baseURL = process.env.baseURL || process.env.apiUrl || '';
@@ -35,7 +38,7 @@ _axios.interceptors.request.use(
       LOADINGCOUNT++;
     }
     
-    // config.headers.Authorization=localStorage.getItem('token')||''
+    config.headers.Authorization=token.value||''
     return config;
   },
   function (error) {
@@ -66,21 +69,22 @@ _axios.interceptors.response.use(
 
     if(response.status!==200){
       setTimeout(() => {
-        MessagePlugin.warning('网络异常')
+        MessagePlugin.error('网络异常')
       }, 100);
     }
 
     if(!data){
       setTimeout(() => {
-        MessagePlugin.warning('服务器开了个小差')
+        MessagePlugin.error('服务器开了个小差')
       }, 100);
     }
     if(data.Ret===408){//token失效
-      MessagePlugin.warning(data.Msg)
+      MessagePlugin.error(data.Msg)
+      router.replace('/login')
     }
     if(data.Ret===403){
       setTimeout(() => {
-        MessagePlugin.warning(data.Msg||'网络异常')
+        MessagePlugin.error(data.Msg||'网络异常')
       }, 100);
     }
 

+ 16 - 0
src/api/system/common.js

@@ -4,5 +4,21 @@ export default{
     //机构用户
     companyUserList:()=>{
         return get('/admin/business/admin',{})
+    },
+    // 图像验证码
+    getImgCode:()=>{
+        return get('/user_login/get_captcha',{})
+    },
+    // 手机号区号
+    getMobileAreaCode:()=>{
+        return get('/user_login/area_code/list',{})
+    },
+    // 获取手机验证码
+    getMobileVerifyCode:params=>{
+        return post('/user_login/verify_code',params)
+    },
+    // 用户登录
+    userLogin:params=>{
+        return post('/user_login/login',params)
     }
 }

BIN
src/assets/icons/pop_close.png


BIN
src/assets/icons/warning.png


BIN
src/assets/imgs/login_bg.png


BIN
src/assets/imgs/logo.png


BIN
src/assets/imgs/logo_login.png


File diff suppressed because it is too large
+ 1 - 0
src/assets/svg/full_screen.svg


+ 4 - 0
src/assets/svg/show_arrow.svg

@@ -0,0 +1,4 @@
+<svg width="16" height="48" viewBox="0 0 16 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M0.5 0.5H8C12.1421 0.5 15.5 3.85786 15.5 8V40C15.5 44.1421 12.1421 47.5 8 47.5H0.5V0.5Z" fill="white" stroke="#D8D8D8"/>
+<path d="M6.45977 28.459L5.54053 27.5397L9.08091 23.9994L5.54053 20.459L6.45977 19.5397L10.9194 23.9994L6.45977 28.459Z" fill="#999999"/>
+</svg>

+ 3 - 0
src/assets/svg/star.svg

@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11.927 7.9752L10.0018 4.0744L8.07666 7.9752L3.77186 8.60072L6.88684 11.6371L6.15149 15.9245L10.0018 13.9002L13.8521 15.9245L13.1168 11.6371L16.2318 8.60072L11.927 7.9752ZM18.166 7.61866C18.4736 7.66336 18.5964 8.04135 18.3739 8.2583L14.4599 12.0735L15.3839 17.4606C15.4364 17.7669 15.1149 18.0005 14.8398 17.8559L10.0018 15.3125L5.16388 17.8559C4.88877 18.0005 4.56723 17.7669 4.61978 17.4606L5.54374 12.0735L1.62977 8.2583C1.4072 8.04135 1.53002 7.66336 1.8376 7.61866L7.24657 6.83269L9.66554 1.93133C9.8031 1.65261 10.2005 1.65261 10.3381 1.93133L12.7571 6.83269L18.166 7.61866Z" fill="#333333"/>
+</svg>

+ 3 - 0
src/assets/svg/star_fill.svg

@@ -0,0 +1,3 @@
+<svg width="18" height="17" viewBox="0 0 18 17" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8.59783 1.06764C8.7629 0.733183 9.23983 0.733183 9.40489 1.06764L11.7566 5.83273L17.0152 6.59685C17.3843 6.65048 17.5317 7.10407 17.2646 7.36441L13.4594 11.0735L14.3577 16.3109C14.4208 16.6785 14.0349 16.9588 13.7048 16.7852L9.00136 14.3125L4.29794 16.7852C3.9678 16.9588 3.58196 16.6785 3.64501 16.3109L4.54328 11.0735L0.738132 7.36441C0.471049 7.10407 0.618429 6.65048 0.987528 6.59685L6.24612 5.83273L8.59783 1.06764Z" fill="#3D5EFF"/>
+</svg>

+ 4 - 0
src/assets/svg/user_avatar.svg

@@ -0,0 +1,4 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M13.475 7.775C13.475 9.69419 11.9192 11.25 9.99996 11.25C8.08077 11.25 6.52496 9.69419 6.52496 7.775C6.52496 5.85581 8.08077 4.3 9.99996 4.3C11.9192 4.3 13.475 5.85581 13.475 7.775ZM12.225 7.775C12.225 9.00384 11.2288 10 9.99996 10C8.77113 10 7.77496 9.00384 7.77496 7.775C7.77496 6.54617 8.77113 5.55 9.99996 5.55C11.2288 5.55 12.225 6.54617 12.225 7.775Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M10 1.25C14.8325 1.25 18.75 5.16751 18.75 10C18.75 14.3794 15.5326 18.0074 11.3325 18.6492C10.8981 18.7156 10.453 18.75 10 18.75C7.6819 18.75 5.57433 17.8486 4.00873 16.3771C2.31055 14.7811 1.25 12.5144 1.25 10C1.25 5.16751 5.16751 1.25 10 1.25ZM8.85876 17.4137C7.44629 17.1981 6.16344 16.5884 5.12588 15.7004L5.0675 15.6488L5.10563 15.5072C5.41386 14.4982 6.37305 13.75 7.5 13.75H12.5L12.691 13.7572C13.7632 13.8384 14.6497 14.5968 14.9172 15.6088L14.9263 15.6512L14.8398 15.7293C14.7241 15.8272 14.6058 15.9214 14.4849 16.0119C13.234 16.9466 11.6816 17.5 10 17.5L9.63851 17.4914C9.37573 17.479 9.11554 17.4529 8.85876 17.4137ZM15.8897 14.644C15.288 13.3764 13.9964 12.5 12.5 12.5H7.5C6.00363 12.5 4.71197 13.3764 4.11034 14.644C3.10188 13.3668 2.5 11.7537 2.5 10C2.5 5.85786 5.85786 2.5 10 2.5C14.1421 2.5 17.5 5.85786 17.5 10C17.5 11.7537 16.8981 13.3668 15.8897 14.644Z" fill="white"/>
+</svg>

+ 67 - 0
src/auto-imports.d.ts

@@ -0,0 +1,67 @@
+/* eslint-disable */
+/* prettier-ignore */
+// @ts-nocheck
+// noinspection JSUnusedGlobalSymbols
+// Generated by unplugin-auto-import
+export {}
+declare global {
+  const $confirmDialog: typeof import('./plugin/dialog.jsx')['$confirmDialog']
+  const EffectScope: typeof import('vue')['EffectScope']
+  const computed: typeof import('vue')['computed']
+  const createApp: typeof import('vue')['createApp']
+  const customRef: typeof import('vue')['customRef']
+  const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
+  const defineComponent: typeof import('vue')['defineComponent']
+  const effectScope: typeof import('vue')['effectScope']
+  const getCurrentInstance: typeof import('vue')['getCurrentInstance']
+  const getCurrentScope: typeof import('vue')['getCurrentScope']
+  const h: typeof import('vue')['h']
+  const inject: typeof import('vue')['inject']
+  const isProxy: typeof import('vue')['isProxy']
+  const isReactive: typeof import('vue')['isReactive']
+  const isReadonly: typeof import('vue')['isReadonly']
+  const isRef: typeof import('vue')['isRef']
+  const markRaw: typeof import('vue')['markRaw']
+  const nextTick: typeof import('vue')['nextTick']
+  const onActivated: typeof import('vue')['onActivated']
+  const onBeforeMount: typeof import('vue')['onBeforeMount']
+  const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
+  const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
+  const onDeactivated: typeof import('vue')['onDeactivated']
+  const onErrorCaptured: typeof import('vue')['onErrorCaptured']
+  const onMounted: typeof import('vue')['onMounted']
+  const onRenderTracked: typeof import('vue')['onRenderTracked']
+  const onRenderTriggered: typeof import('vue')['onRenderTriggered']
+  const onScopeDispose: typeof import('vue')['onScopeDispose']
+  const onServerPrefetch: typeof import('vue')['onServerPrefetch']
+  const onUnmounted: typeof import('vue')['onUnmounted']
+  const onUpdated: typeof import('vue')['onUpdated']
+  const provide: typeof import('vue')['provide']
+  const reactive: typeof import('vue')['reactive']
+  const readonly: typeof import('vue')['readonly']
+  const ref: typeof import('vue')['ref']
+  const resolveComponent: typeof import('vue')['resolveComponent']
+  const shallowReactive: typeof import('vue')['shallowReactive']
+  const shallowReadonly: typeof import('vue')['shallowReadonly']
+  const shallowRef: typeof import('vue')['shallowRef']
+  const toRaw: typeof import('vue')['toRaw']
+  const toRef: typeof import('vue')['toRef']
+  const toRefs: typeof import('vue')['toRefs']
+  const toValue: typeof import('vue')['toValue']
+  const triggerRef: typeof import('vue')['triggerRef']
+  const unref: typeof import('vue')['unref']
+  const useAttrs: typeof import('vue')['useAttrs']
+  const useCssModule: typeof import('vue')['useCssModule']
+  const useCssVars: typeof import('vue')['useCssVars']
+  const useSlots: typeof import('vue')['useSlots']
+  const watch: typeof import('vue')['watch']
+  const watchEffect: typeof import('vue')['watchEffect']
+  const watchPostEffect: typeof import('vue')['watchPostEffect']
+  const watchSyncEffect: typeof import('vue')['watchSyncEffect']
+}
+// for type re-export
+declare global {
+  // @ts-ignore
+  export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
+  import('vue')
+}

+ 0 - 21
src/components/Empty.vue

@@ -1,21 +0,0 @@
-<script setup>
-
-</script>
-
-<template>
-    <div class="empty-wrap">
-        <img src="@/assets/imgs/nodata.png" alt="">
-        <p>暂无数据</p>
-    </div>
-</template>
-
-<style lang="scss" scoped>
-.empty-wrap{
-    text-align: center;
-    color: #666;
-    img{
-        width: 200px;
-        margin-bottom: 20px;
-    }
-}
-</style>

+ 28 - 0
src/components/EmptyWrap.vue

@@ -0,0 +1,28 @@
+<script setup>
+
+const props = defineProps({
+  msg: {
+    type: String,
+    default: '暂无数据'
+  }
+})
+
+</script>
+
+<template>
+  <div class="empty-wrap">
+    <img src="@/assets/imgs/nodata.png" alt="" />
+    <p>{{props.msg}}</p>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.empty-wrap {
+  text-align: center;
+  color: #666;
+  img {
+    width: 200px;
+    margin-bottom: 20px;
+  }
+}
+</style>

+ 49 - 0
src/components/SelectChartCreator.vue

@@ -0,0 +1,49 @@
+<script setup>
+import { apiSystemCommon } from '@/api/system'
+
+const props=defineProps({
+  size:{
+    type:String,
+    default:'medium'
+  }
+})
+
+const emits=defineEmits(['change'])
+
+const userProps = {
+  label: 'RealName',
+  value: 'AdminId',
+  children: 'ChildrenList'
+}
+const userOpts = ref([])
+const userVal =defineModel('value',{type:[String,Array],default:[]})
+async function getCompanyUserData() {
+  const res = await apiSystemCommon.companyUserList()
+  if (res.Ret === 200) {
+    userOpts.value = res.Data.List || []
+  }
+}
+getCompanyUserData()
+
+function handleChange(value,context){
+  console.log('创建人选择');
+  emits('change',{value,context})
+}
+
+</script>
+
+<template>
+  <t-cascader
+    v-model="userVal"
+    :options="userOpts"
+    :keys="userProps"
+    multiple
+    :minCollapsedNum="1"
+    clearable
+    filterable
+    :showAllLevels="false"
+    placeholder="创建人"
+    :size="props.size"
+    @change="handleChange"
+  />
+</template>

+ 15 - 0
src/components/globalComponents.js

@@ -0,0 +1,15 @@
+import { defineAsyncComponent } from 'vue';
+
+// 自动注册src/components目录下的所有.vue文件
+export default function registerGlobalComponents(app) {
+  const components = import.meta.glob('./components/**/*.vue');
+
+  for (const path in components) {
+    const componentName = path
+      .split('/')
+      .pop()
+      .replace(/\.\w+$/, '');
+
+    app.component(componentName, defineAsyncComponent(components[path]));
+  }
+}

+ 21 - 0
src/hooks/userInfo.js

@@ -0,0 +1,21 @@
+// 全局接口请求loading hook
+import { ref } from "vue";
+
+const userInfo = ref(null);
+const token = ref(localStorage.getItem("token") || "");
+
+export function useUserInfo() {
+  function setToken(e){
+    token.value=e
+    localStorage.setItem('token',e)
+  }
+
+  function getUserInfo() {
+
+  }
+
+  return {
+    token,
+    setToken
+  };
+}

+ 115 - 36
src/layout/Index.vue

@@ -1,48 +1,127 @@
 <script setup>
 
+async function handleLoginOut(){
+	await $confirmDialog({
+    header:'提示',
+    body: '是否确认退出当前账号',
+		confirmBtn:'确认退出'
+  }); 
+}
+
 </script>
 
 <template>
-    <div class="layout-wrap">
-        <div class="bg-white header">
-            <span class="bread-item" v-for="item in $route.matched" :key="item.name">{{item.meta.title}}</span>
-        </div>
-        <div class="layout-content">
-            <router-view />
-        </div>
-        
+  <div class="layout-wrap">
+    <div class="flex header">
+      <img class="logo" src="@/assets/imgs/logo.png" alt="" />
+      <t-popup placement="bottom-left" overlayInnerClassName="header-userInfo-pop-wrap">
+				<template #content>
+					<div class="content">
+						<div class="top-box">
+							<div class="label-text">账号信息</div>
+							<div class="flex">
+								<span>用户名</span>
+								<span>|</span>
+								<span>手机号</span>
+							</div>
+							<div class="flex">
+								<span>所属机构</span>
+								<span>|</span>
+								<span>岗位</span>
+							</div>
+						</div>
+						<div class="flex my-fav-box" @click="$router.push('/etaChart/favorite')">
+							<t-icon name="star" style="font-size:20px"></t-icon>
+							<span>我的收藏</span>
+							<t-icon class="arrow-right" name="chevron-right-s" style="font-size:20px"></t-icon>
+						</div>
+						<div class="flex my-fav-box" style="color:#D54941" @click="handleLoginOut">
+							<t-icon name="logout" style="font-size:20px"></t-icon>
+							<span>退出登录</span>
+						</div>
+					</div>
+				</template>
+				<div class="userInfo-box">
+					<svg-icon name="user_avatar" style="font-size:20px;margin-right:3px"></svg-icon>
+					<span>账号吗</span>
+					<t-icon name="chevron-down" style="font-size:28px;margin-left:11px"></t-icon>
+				</div>
+			</t-popup>
+    </div>
+    <div class="layout-content">
+      <router-view />
     </div>
+  </div>
 </template>
 
+<style lang="scss">
+.header-userInfo-pop-wrap{
+	padding: 0;
+	width: 234px;
+	border-radius: 8px;
+	border: 1px solid var(--border-color);
+	box-shadow: 0px 4px 12px 0px #0000001A;
+	overflow: hidden;
+	.content{
+		.top-box{
+			padding: 14px;
+			background: linear-gradient(180deg, #7C55FF 0%, #4C64F7 100%);
+			color: #fff;
+			.label-text{
+				font-size: 16px;
+				line-height: 22.4px;
+			}
+			.flex{
+				gap: 0 5px;
+				margin-top: 5px;
+			}
+		}
+		.my-fav-box{
+			border-bottom: 1px solid var(--border-color);
+			height: 48px;
+			align-items: center;
+			padding: 0 14px;
+			color: #666666;
+			cursor: pointer;
+			.arrow-right{
+				margin-left: auto;
+			}
+			span{
+				display: inline-block;
+				margin-left: 12px;
+			}
+		}
+	}
+}
+</style>
 <style lang="scss" scoped>
-.layout-wrap{
-    height: 100%;
-    padding-top: 64px;
-    .header{
-        position: fixed;
-        left: 0;
-        right: 0;
-        top: 0;
-        z-index: 10;
-        height: 64px;
-        display: flex;
-        align-items: center;
-        padding-left: 24px;
-        .bread-item{
-            &::after{
-                content:'/'
-            }
-        }
-        .bread-item:last-child{
-            color: $primary-color;
-            &::after{
-                content:''
-            }
-        }
-    }
-    .layout-content{
-        padding: 20px;
-        min-height: calc(100% - 64px);
+.layout-wrap {
+  height: 100%;
+  padding-top: 70px;
+  .header {
+    position: fixed;
+    left: 0;
+    right: 0;
+    top: 0;
+    z-index: 10;
+    height: 70px;
+    background: linear-gradient(90deg, #7c54ff 0%, #4b64f7 100%);
+    align-items: center;
+    padding: 0 20px;
+    .logo {
+      width: 168px;
     }
+		.userInfo-box{
+			margin-left: auto;
+			color: #fff;
+			display: flex;
+			align-items: center;
+			cursor: pointer;
+		}
+  }
+  .layout-content {
+    padding: 20px;
+    min-height: calc(100% - 70px);
+  }
 }
 </style>

+ 8 - 6
src/main.js

@@ -3,21 +3,23 @@ import App from './App.vue'
 import router from "./router";
 import 'normalize.css'
 import './styles/common.scss'
+import './styles/tdesign.scss'
 // 引入组件库的少量全局样式变量
 import 'tdesign-vue-next/es/style/index.css';
+import {formatTime} from '@/utils/common'
 
 
-// svg图标组件
-import svgIcon from "@/components/SvgIcon.vue";
 //引入注册脚本
 import 'virtual:svg-icons-register'
+import registerGlobalComponents from '@/components/globalComponents';
 
-import EmptyWrap from '@/components/Empty.vue'
+const app= createApp(App)
 
+// 挂载一个全局格式化时间方法
+app.config.globalProperties.formatTime=formatTime
 
-const app= createApp(App)
+// 注册全局组件
+registerGlobalComponents(app)
 
-app.component('svg-icon', svgIcon)
-app.component('empty-wrap', EmptyWrap)
 app.use(router)
 app.mount('#app')

+ 27 - 0
src/plugin/dialog.jsx

@@ -0,0 +1,27 @@
+// 提示弹窗函数
+// 根据dialog-confirm-pop 类名改样式吧
+import { DialogPlugin } from 'tdesign-vue-next';
+import warningIcon from '@/assets/icons/warning.png'
+export function $confirmDialog(props){
+  const headerHtml=()=><div style="display:flex;align-items:center">
+    <img src={warningIcon} style="width:20px" />
+    <span>{props.header}</span>
+  </div>
+  return new Promise((resolve,reject)=>{
+    const confirmDia = DialogPlugin.confirm({
+      confirmBtn: '确定',
+      cancelBtn: '取消',
+      ...props,
+      className:'dialog-confirm-pop',
+      header:headerHtml,
+      onConfirm: ({ e }) => {
+        resolve(true)
+        confirmDia.destroy();
+      },
+      onClose: ({ e, trigger }) => {
+        reject(false)
+        confirmDia.destroy();
+      },
+    });
+  })
+}

+ 16 - 22
src/router/index.js

@@ -1,33 +1,27 @@
 import { createRouter, createWebHistory } from "vue-router";
-import LayoutIndex from "@/layout/Index.vue";
+
+//all routes
+const appAllRoutes = [];
+function importAllRoutes (r){
+  for(let key in r) {
+    appAllRoutes.push(...r[key].default)
+  }
+}
+importAllRoutes(import.meta.glob('./modules/*.js',{ eager: true }))
 
 const routes = [
+  ...appAllRoutes,
   {
     path:'/',
-    name:'LayoutIndex',
-    redirect: '/etaChart/index',
-    component:LayoutIndex,
+    redirect: '/etaChart/index'
+  },
+  {
+    path:'/login',
+    name:'Login',
+    component:()=>import('@/views/Login.vue'),
     meta:{
       title:'ETA社区'
     },
-    children:[
-      {
-        path:'etaChart/index',
-        name:'ETAChartIndex',
-        component:()=>import('@/views/etaChart/Index.vue'),
-        meta:{
-          title:'ETA图库'
-        },
-      },
-      {
-        path:'EDBSource',
-        name:'EDBSource',
-        component:()=>import('@/views/EDBSource.vue'),
-        meta:{
-          title:'指标溯源'
-        },
-      },
-    ]
   },
   {
     path: "/:pathMatch(.*)",

+ 39 - 0
src/router/modules/etaChart.js

@@ -0,0 +1,39 @@
+import LayoutIndex from "@/layout/Index.vue";
+
+export default[
+  {
+    path:'/etaChart',
+    name:'ETAChart',
+    redirect: '/etaChart/index',
+    component:LayoutIndex,
+    meta:{
+      title:'研究图库'
+    },
+    children:[
+      {
+        path:'index',
+        name:'ETAChartIndex',
+        component:()=>import('@/views/etaChart/Index.vue'),
+        meta:{
+          title:'ETA图库'
+        },
+      },
+      {
+        path:'EDBSource',
+        name:'EDBSource',
+        component:()=>import('@/views/EDBSource.vue'),
+        meta:{
+          title:'指标溯源'
+        },
+      },
+      {
+        path:'favorite',
+        name:'ETAChartFavorite',
+        component:()=>import('@/views/user/favorite/etaChart.vue'),
+        meta:{
+          title:'我的收藏'
+        },
+      },
+    ]
+  },
+]

+ 38 - 1
src/styles/common.scss

@@ -10,7 +10,7 @@ div,ul,li{
 }
 
 input{
-    display: block;
+    display: inline-block;
     border: none;
     box-sizing: border-box;
 }
@@ -20,6 +20,7 @@ P{
 }
 
 img {
+    object-fit: contain;
     image-rendering: -moz-crisp-edges;
     image-rendering: -o-crisp-edges;
     image-rendering: -webkit-optimize-contrast;
@@ -27,6 +28,14 @@ img {
     -ms-interpolation-mode: nearest-neighbor;
 }
 
+.flex{
+    display: flex;
+}
+.flex_col{
+    display: flex;
+    flex-direction: column;
+}
+
 .bg-white{
     background-color: #fff;
 }
@@ -35,4 +44,32 @@ img {
 .t-table thead td, .t-table th{
     color: #333 !important;
     font-weight: bold !important;
+}
+
+/* 单行省略 */
+.text-ellipsis--l1 {
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+
+/* 两行省略*/
+.text-ellipsis--l2 {
+	display: -webkit-box;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	-webkit-line-clamp: 2;
+	line-break: anywhere;
+	-webkit-box-orient: vertical;
+}
+
+/* 三行省略*/
+.text-ellipsis--l3 {
+	display: -webkit-box;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	-webkit-line-clamp: 3;
+	line-break: anywhere;
+	-webkit-box-orient: vertical;
 }

+ 52 - 0
src/styles/tdesign.scss

@@ -0,0 +1,52 @@
+// 重写tdesign样式模块
+
+.t-dialog{
+  padding: 0;
+  border: none;
+  overflow: hidden;
+  .t-dialog__header{
+    padding: var(--td-comp-paddingTB-l);
+    background-color: var(--td-brand-color);
+    color: #fff;
+    .t-dialog__close{
+      color: #fff;
+      &:hover{
+        background: transparent;
+      }
+    }
+  }
+  .t-dialog__body{
+    padding: 40px 60px;
+  }
+  .t-dialog__footer{
+    padding: 40px;
+    text-align: center;
+  }
+}
+
+.dialog-confirm-pop{
+  .t-dialog__header{
+    padding: 13px 20px;
+    background-color: #fff;
+    color: #333;
+    border-bottom: 1px solid var(--border-color);
+    .t-dialog__close{
+      color: #333;
+    }
+  }
+  .t-dialog__body{
+    padding: 30px 20px;
+  }
+  .t-dialog__footer{
+    padding: 20px;
+    text-align: right;
+  }
+}
+
+
+.t-table__header{
+  background-color: #EBEEF5;
+  tr{
+    background-color: transparent;
+  }
+}

+ 4 - 1
src/styles/var.scss

@@ -1 +1,4 @@
-$primary-color:#0052D9;
+:root{
+  --border-color:#DCDFE6;
+  --red-color:#E83737;
+}

+ 32 - 0
src/utils/common.js

@@ -0,0 +1,32 @@
+import moment from "moment";
+//验证密码的正则 产品定的规则是:8位及以上,包含数字、大写字母、小写字母、特殊字符中的三个类型
+export const patternPassWord = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9]).{8,}$/;
+export function checkPassWord(pwd) {
+  let num = 0;
+  const patternArr = [/^(?=.*[0-9])/, /^(?=.*[a-z])/, /^(?=.*[A-Z])/, /^(?=.*[@#$%^&+=.])/];
+  patternArr.forEach((pattern) => {
+    if (pattern.test(pwd)) {
+      num++;
+    }
+  });
+  if (pwd.length < 8) {
+    num = 0;
+  }
+  return num >= 3;
+}
+
+//验证手机号的正则 仅支持国内大陆的
+export const patternPhone = /0?(13|14|15|18|17)[0-9]{9}/;
+export function isMobileNo(account) {
+  //改成和后端一样的正则
+  const phonePatter = new RegExp("(^((13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(18[0-9])|(17[0-9])|(16[0-9])|(19[0-9]))\\d{8}$)");
+  return phonePatter.test(account);
+}
+//验证邮箱的正则
+export const patternEmail = /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/;
+
+// 格式话时间 t时间字符串 f要格式化的样式
+export function formatTime(t, f = "YYYY-MM-DD HH:mm:ss") {
+  if (!t) return "";
+  return moment(t).format(f);
+}

+ 0 - 39
src/views/AutoLogin.vue

@@ -1,39 +0,0 @@
-<script setup>
-import {apiSystemSet} from '@/api/system'
-import { useRoute, useRouter } from 'vue-router'
-
-const route=useRoute()
-const router=useRouter()
-
-function init(){
-    const code=route.query.code
-    if(!code) return
-    apiSystemSet.loginWithCode({
-        AuthCode:code
-    }).then(res=>{
-        if(res.Ret===200){
-            sessionStorage.setItem('token',res.Data.Authorization)
-            sessionStorage.setItem('userInfo',JSON.stringify(res.Data))
-            router.replace('/etaChart/index')
-        }
-    })
-}
-init()
-
-</script>
-
-<template>
-    <div class="auto-login-page">
-        登录中...
-    </div>
-</template>
-
-<style lang="scss" scoped>
-.auto-login-page{
-    width: 100%;
-    height: 100%;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-}
-</style>

+ 237 - 0
src/views/Login.vue

@@ -0,0 +1,237 @@
+<script setup>
+import { useTemplateRef } from "vue"
+import {apiSystemCommon} from '@/api/system'
+import { useRouter } from "vue-router"
+import {useUserInfo} from '@/hooks/userInfo'
+
+const {setToken}=useUserInfo()
+const router=useRouter()
+
+//图形验证码
+const imgCodeUrl=ref('')
+let imgCodeId=''
+async function getImgCode(){
+  const res=await apiSystemCommon.getImgCode()
+  if(res.Ret!==200) return
+  imgCodeUrl.value=res.Data.Base64Blob
+  imgCodeId=res.Data.Id
+}
+getImgCode()
+
+// 手机号区号
+const telCodeOpts = ref([])
+async function getMobileAreaCode(){
+  const res=await apiSystemCommon.getMobileAreaCode()
+  if(res.Ret!==200) return
+  telCodeOpts.value=res.Data||[]
+  if(telCodeOpts.value.length>0){
+    formData.telCode=telCodeOpts.value[0].Value
+  }
+}
+getMobileAreaCode()
+
+const keepLogin=ref(false)
+const FORM_RULES = { 
+  telVerifyCode: [{ required: true, message: '请输入验证码' }],
+  tel:[{ required: true, message: '请输入手机号' }],
+  imgVerifyCode:[{ required: true, message: '请输入图像验证码' }],
+};
+const formData = reactive({
+  tel: '',
+  telCode: '',
+  telVerifyCode: '',
+  imgVerifyCode: '',
+})
+
+const formIns=useTemplateRef('formIns')
+async function onSubmit(){
+	const validate=await formIns.value.validate()
+	if(validate!==true) return
+
+  const res=await apiSystemCommon.userLogin({
+    VerifyCode:formData.telVerifyCode,
+    Mobile:formData.tel,
+    TelAreaCode:formData.telCode,
+    IsRemember:Number(keepLogin.value)
+  })
+  if(res.Ret!==200) return
+  setToken(res.Data.Authorization)
+  router.replace('/etaChart/index')
+}
+
+
+// 获取手机号验证码
+let countDownTimer=null
+const countDownTime=ref(60)
+const isSendCode=ref(false)
+async function handleMobileVerifyCode(){
+  if(isSendCode.value) return
+  if(!formData.tel){
+    MessagePlugin.warning('请填写手机号')
+    return
+  }
+  if(!formData.imgVerifyCode){
+    MessagePlugin.warning('请填写图形验证码')
+    return
+  }
+  const res=await apiSystemCommon.getMobileVerifyCode({
+    CaptchaId:imgCodeId,
+    CaptchaCode:formData.imgVerifyCode,
+    Mobile:formData.tel,
+    TelAreaCode:formData.telCode
+  })
+  if(res.Ret!==200) return
+  isSendCode.value=true
+  countDownTime.value=60
+  countDownTimer=setInterval(()=>{
+    countDownTime.value--
+    if(countDownTime.value<0){
+      isSendCode.value=false
+      clearInterval(countDownTimer)
+    }
+  },1000)
+}
+
+
+</script>
+
+<template>
+  <div class="login-page">
+    <img class="bg-img" src="@/assets/imgs/login_bg.png" alt="" />
+    <div class="flex main-wrap">
+      <img class="logo-icon" src="@/assets/imgs/logo_login.png" alt="" />
+      <div class="login-form-wrap">
+        <div class="en-login-label">Login</div>
+        <div class="login-label">登录</div>
+        <div class="login-tips">sign in to continue</div>
+        <t-form
+          ref="formIns"
+					:rules="FORM_RULES"
+          :data="formData"
+          :colon="true"
+          :label-width="0"
+          @submit="onSubmit"
+        >
+          <t-form-item name="tel">
+            <div class="flex form-item-box">
+              <t-select v-model="formData.telCode" style="width:80px">
+                <t-option
+                  v-for="item in telCodeOpts"
+                  :key="item.Value"
+                  :label="item.Name"
+                  :value="item.Value"
+                />
+              </t-select>
+							<t-input class="tel-input" placeholder="请输入手机号" v-model="formData.tel" style="flex:1"></t-input>
+            </div>
+          </t-form-item>
+					<t-form-item name="telVerifyCode">
+            <div class="flex form-item-box">
+							<t-input placeholder="请输入验证码" v-model="formData.telVerifyCode" style="flex:1"></t-input>
+							<span class="get_telcode_btn" @click="handleMobileVerifyCode" v-if="!isSendCode">获取验证码</span>
+              <span v-else class="get_telcode_btn" style="color:#666">{{countDownTime}}s</span>
+            </div>
+          </t-form-item>
+					<t-form-item name="imgVerifyCode">
+            <div class="flex form-item-box" style="padding-top:6px;padding-bottom:6px;">
+							<t-input placeholder="请输入图形验证码" v-model="formData.imgVerifyCode" style="flex:1"></t-input>
+							<img class="img-code" :src="imgCodeUrl" alt="" @click="getImgCode">
+            </div>
+          </t-form-item>
+					<div>
+						<t-checkbox v-model="keepLogin">60天内保持登录</t-checkbox>
+					</div>
+					<t-button class="submit-btn" block shape="round" type="submit">登录</t-button>
+
+        </t-form>
+				<p class="bottom-tips">共享社区资源,共创市场领先</p>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.login-page {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  background-color: #edf2f7;
+  .bg-img {
+    position: absolute;
+    bottom: 0;
+    right: 0;
+    max-width: 100%;
+    max-height: 100%;
+  }
+  .main-wrap {
+    height: 100%;
+    padding-top: 67px;
+    padding-left: 39px;
+    position: relative;
+    z-index: 10;
+    flex-direction: column;
+    .logo-icon {
+      width: 374px;
+    }
+    .login-form-wrap {
+			margin-left: 160px;
+      width: 400px;
+      flex: 1;
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      .en-login-label {
+        font-size: 34px;
+        line-height: 47px;
+      }
+      .login-label {
+        font-size: 54px;
+        line-height: 75px;
+      }
+      .login-tips {
+        color: #999999;
+        margin-bottom: 28px;
+      }
+      .form-item-box {
+        border: 1px solid #3d5eff;
+        padding: 10px 40px 10px 16px;
+        border-radius: 40px;
+        width: 100%;
+				background-color: #fff;
+				align-items: center;
+				:deep(.t-input){
+					border: none;
+				}
+				:deep(.t-input--focused){
+					box-shadow: none;
+				}
+				.tel-input{
+					border-left: 1px solid var(--border-color);
+				}
+				.get_telcode_btn{
+					color: var(--td-brand-color);
+					cursor: pointer;
+					display: block;
+				}
+				.img-code{
+					width: 80px;
+					height: 40px;
+				}
+      }
+			.submit-btn{
+				background: linear-gradient(90deg, #7C54FF 0%, #4B64F7 100%);
+				height: 40px;
+				margin-top: 40px;
+				border: none;
+			}
+			.bottom-tips{
+				text-align: center;
+				color: #999999;
+				font-size: 20px;
+				margin-top: 60px;
+        margin-bottom: 150px;
+			}
+    }
+  }
+}
+</style>

+ 96 - 68
src/views/etaChart/Index.vue

@@ -1,7 +1,8 @@
 <script setup>
-import { ref } from 'vue'
-import ClassifyWrapVue from "./components/ClassifyWrap.vue";
+import { ref, useTemplateRef } from 'vue'
+import ClassifyWrap from "./components/ClassifyWrap.vue";
 import ChartWrap from './components/ChartWrap.vue'
+import ChartList from './components/chartList/Index.vue'
 import { apiETAChart } from '@/api/etaChart'
 import {useClassify} from './hooks/useClassify'
 
@@ -29,6 +30,8 @@ const pageSize = 50
 let finished = false
 const tableData = ref([])
 const tableLoading = ref(false)
+const chartTotal=ref(0)
+const showChartNameWrap=ref(true)
 async function getTableData() {
   tableLoading.value = true
   const res = await apiETAChart.chartList({
@@ -42,10 +45,7 @@ async function getTableData() {
     const arr = res.Data.AllNodes || []
     tableData.value = [...tableData.value, ...arr]
     finished = res.Data.Paging.IsEnd
-    // 如果是获取第一页数组则默认选中第一个图表
-    if(res.Data.Paging.IsStart&&arr.length>0){
-      activeChartId.value=arr[0].ChartInfoId
-    }
+    chartTotal.value=res.Data.Paging.Totals
   }
 }
 getTableData()
@@ -57,12 +57,16 @@ function tableScroll(opt) {
   }
 }
 
+const chartListRef=useTemplateRef('chartListRef')
+
 function handleFilterList() {
   activeChartId.value=''
   tableData.value=[]
   finished=false
   page=1
   getTableData()
+  // 更新右侧图列表的数据
+  chartListRef.value.handleRefreshList()
   // 取消掉右侧图表
   activeChartId.value=''
 }
@@ -77,42 +81,34 @@ function handleSelectChart(item) {
 
 <template>
   <div class="eta-chart-page">
-    <ClassifyWrapVue @filter="handleFilterList" @change="handleSelectChart" />
-    <div class="table-wrap" v-loading="tableLoading">
-      <t-list style="height: 100%" :onScroll="tableScroll">
-        <table class="table-box" cellpadding="0" cellspacing="0">
-          <thead class="table-head thead-sticky">
-            <tr>
-              <td
-                v-for="opt in tableColOpts"
-                :key="opt.colKey"
-                :style="{ width: opt.width }"
-              >
-                {{ opt.title }}
-              </td>
-            </tr>
-          </thead>
-          <tbody>
-            <tr
-              :class="row.ChartInfoId === activeChartId ? 'active_row' : ''"
-              v-for="row in tableData"
-              :key="row.index"
-              @click="handleSelectChart(row)"
-            >
-              <td
-                v-for="opt in tableColOpts"
-                :key="opt.colKey"
-                :style="{ width: opt.width }"
-              >
-                {{ row[opt.colKey] }}
-              </td>
-            </tr>
-          </tbody>
-        </table>
+    <ClassifyWrap @filter="handleFilterList" @change="handleSelectChart" />
+    <div class="center-wrap">
+    <svg-icon name="show_arrow" class="show_chart_name_wrap_btn" v-if="!showChartNameWrap" @click="showChartNameWrap=true"></svg-icon>
+    <div :class="['flex chart-name-list-wrap',!showChartNameWrap?'chart-name-list-wrap_close':'']" v-loading="tableLoading">
+      <div class="flex top-box">
+        <span>所选图表</span>
+        <t-icon name="chevron-left-double" style="font-size:20px;cursor: pointer;" @click="showChartNameWrap=false"></t-icon>
+      </div>
+      <t-list class="list-content" :onScroll="tableScroll">
+        <li 
+          :class="['text-ellipsis--l1 chart-name',item.ChartInfoId === activeChartId ? 'active_row' : '']" 
+          v-for="item in tableData" 
+          :key="item.ChartInfoId"
+          @click="handleSelectChart(item)"
+        >{{item.ChartClassifyName}}</li>
         <empty-wrap v-if="tableData.length===0"/>
       </t-list>
+      <div class="flex bottom-box">
+        <span>共{{chartTotal}}张图表</span>
+        <span class="clear-btn" @click="activeChartId=''">清除选择</span>
+      </div>
+    </div>
+    </div>
+    <div class="right-wrap">
+      <ChartList ref="chartListRef"/>
+      <!-- 图表详情 -->
+      <ChartWrap :chartInfoId="activeChartId" v-if="activeChartId"/>
     </div>
-    <ChartWrap :chartInfoId="activeChartId"/>
   </div>
 </template>
 
@@ -120,40 +116,72 @@ function handleSelectChart(item) {
 .eta-chart-page {
   display: flex;
   gap: 0 20px;
-  .table-wrap {
-    flex-shrink: 0;
+
+  .center-wrap{
     height: calc(100vh - 120px);
-    .table-box {
-      width: 100%;
-      border-color: #dcdfe6;
-      td,
-      th {
-        word-break: break-all;
-        word-wrap: break-word;
-        border: 1px solid #dcdfe6;
-        height: 40px;
-        text-align: center;
-        border-left: none;
-        border-top: none;
-        &:first-child {
-          border-left: 1px solid #dcdfe6;
-        }
-      }
-      .thead-sticky {
-        position: sticky;
-        top: 0;
+    position: relative;
+  }
+  .show_chart_name_wrap_btn{
+    position: absolute;
+    top: 50%;
+    left: -36px;
+    transform: translateY(-50%);
+    font-size: 48px;
+    color: #D8D8D8;
+    cursor: pointer;
+  }
+  .chart-name-list-wrap {
+    background-color: #fff;
+    flex-shrink: 0;
+    width: 200px;
+    height: 100%;
+    flex-direction: column;
+    box-shadow: 0px 4px 12px 0px #2300351F;
+    border: 1px solid var(--border-color);
+    border-radius: 4px;
+    overflow: hidden;
+    transition: all .3s;
+    .top-box{
+      color: #666;
+      justify-content: space-between;
+      padding: 10px;
+      border-bottom: 1px solid var(--border-color);
+    }
+    .bottom-box{
+      color: #666;
+      justify-content: space-between;
+      padding: 10px;
+      border-top: 1px solid var(--border-color);
+      .clear-btn{
+        color: var(--red-color);
+        cursor: pointer;
       }
-      .table-head {
-        background-color: #ebeef5;
-        font-weight: bold;
+    }
+    .list-content{
+      flex: 1;
+      padding: 10px 0;
+      .chart-name{
+        padding: 0 10px;
+        line-height: 32px;
+        cursor: pointer;
       }
-      tbody {
-        color: #666;
-        .active_row{
-          background-color: #ECF5FF;
-        }
+      .active_row{
+        background-color: #F1EBFF;
+        color: var(--td-brand-color);
       }
     }
+    
+  }
+  .chart-name-list-wrap_close{
+    visibility: hidden;
+    width: 0;
+    overflow: auto;
+  }
+
+  .right-wrap{
+    min-width: 600px;
+    flex: 1;
+    position: relative;
   }
 }
 </style>

+ 147 - 0
src/views/etaChart/components/ChartDetailPop.vue

@@ -0,0 +1,147 @@
+<script setup>
+import { apiETAChart } from '@/api/etaChart'
+import { useChartRender } from '@/hooks/chart/render'
+
+const { options, axisLimitState, chartRender, setLimitData, isUseSelfLimit } = useChartRender()
+
+const show=defineModel('show',{type:Boolean,default:false})
+const props=defineProps({
+  chartInfoId:{
+    type:Number,
+    'default':0
+  }
+})
+
+
+const chartInfo = ref(null)
+const calendarType = ref('')
+const loading = ref(false)
+
+async function getChartDetail() {
+  loading.value = true
+  const res = await apiETAChart.chartDetail({
+    ChartInfoId: props.chartInfoId,
+    Calendar: calendarType.value,
+  })
+  loading.value = false
+  if (res.Ret === 200) {
+    chartInfo.value = res.Data.ChartInfo
+    calendarType.value = res.Data.ChartInfo.Calendar
+    //初始化上下限
+    isUseSelfLimit.value = true
+    setLimitData(res.Data)
+    nextTick(() => {
+      chartRender({
+        data: {
+          ...res.Data,
+          ChartInfo: {
+            ...res.Data.ChartInfo,
+            Calendar: calendarType.value || '公历'
+          },
+        },
+        renderId: `chart-detail-pop-box`,
+        // lang:currentLang.value,
+        changeLangIsCheck: false,
+        showChartTitle: true,
+        shouldUseSelfLimit: true,
+      })
+    })
+  }
+}
+
+watch(
+  ()=>show.value,
+  (n)=>{
+    if(n){
+      getChartDetail()
+    }
+  }
+)
+
+</script>
+
+<template>
+  <t-dialog
+    v-model:visible="show"
+    attach="body"
+    width="80vw"
+    :header="false"
+    :footer="false"
+    :closeBtn="false"
+    closeOnOverlayClick
+    destroyOnClose
+    class="chart-detail-pop-wrap"
+  > 
+    <div class="flex top-box">
+      <div class="chart-name">{{chartInfo?.ChartName}}</div>
+      <svg-icon name="star" style="font-size:20px;cursor: pointer;"></svg-icon>
+    </div>
+    <div class="chart-render-wrap" v-loading="loading">
+      <div class="chart-box" id="chart-detail-pop-box"></div>
+      <div style="text-align: center">
+        <!-- 季节图 公历农历切换 -->
+        <t-radio-group
+          variant="primary-filled"
+          v-model="calendarType"
+          @change="getChartDetail"
+          v-if="chartInfo?.ChartType === 2"
+        >
+          <t-radio-button value="公历">公历</t-radio-button>
+          <t-radio-button value="农历">农历</t-radio-button>
+        </t-radio-group>
+      </div>
+      <div class="chart-source" v-if="chartInfo">
+        <span
+          v-if="
+            chartInfo.SourcesFrom && JSON.parse(chartInfo.SourcesFrom).isShow
+          "
+          :style="`color: ${
+            JSON.parse(chartInfo.SourcesFrom).color
+          };fontSize: ${JSON.parse(chartInfo.SourcesFrom).fontSize}px;
+                          `"
+          >来源:{{ JSON.parse(chartInfo.SourcesFrom).text }}</span
+        >
+      </div>
+    </div>
+  </t-dialog>
+</template>
+
+<style lang="scss" scoped>
+.chart-detail-pop-wrap{
+  :global(.chart-detail-pop-wrap .t-dialog__position::after){
+    content:'';
+    background-image: url('@/assets/icons/pop_close.png');
+    background-size: cover;
+    width: 48px;
+    height: 48px;
+    display: block;
+    position: absolute;
+    right: calc(10vw - 70px);
+    top: calc(20vh - 30px);
+  }
+  :global(.chart-detail-pop-wrap .t-dialog__header){
+    display: none;
+  }
+  :global(.chart-detail-pop-wrap .t-dialog__body){
+    padding: 0 !important;
+  }
+  .top-box{
+    padding: 22px 16px;
+    border-bottom: 1px solid var(--border-color);
+    .chart-name{
+      flex: 1;
+      font-size: 20px;
+      color: #333;
+    }
+  }
+  .chart-render-wrap{
+    height: 50vh;
+    padding: 34px 16px;
+    display: flex;
+    flex-direction: column;
+    .chart-box{
+      flex: 1;
+    }
+  }
+}
+</style>

+ 80 - 75
src/views/etaChart/components/ChartWrap.vue

@@ -18,13 +18,11 @@ const props = defineProps({
 watch(
   () => props.chartInfoId,
   (n) => {
-    if (!n) {
-      tableData.value = []
-      intro.value = ''
-      chartInfo.value = null
-      return
-    }
-    calendarType.value=''
+    tableData.value = []
+    intro.value = ''
+    chartInfo.value = null
+    calendarType.value = ''
+    if (!n) return
     getChartDetail()
   }
 )
@@ -64,17 +62,19 @@ const tableData = ref([])
 const intro = ref('')
 const chartInfo = ref(null)
 const calendarType = ref('')
+const loading = ref(false)
 async function getChartDetail() {
-
+  loading.value = true
   const res = await apiETAChart.chartDetail({
     ChartInfoId: props.chartInfoId,
     Calendar: calendarType.value,
   })
+  loading.value = false
   if (res.Ret === 200) {
     tableData.value = res.Data.EdbInfoList || []
     intro.value = res.Data.ChartInfo.Description
     chartInfo.value = res.Data.ChartInfo
-    calendarType.value=res.Data.ChartInfo.Calendar
+    calendarType.value = res.Data.ChartInfo.Calendar
     //初始化上下限
     isUseSelfLimit.value = true
     setLimitData(res.Data)
@@ -84,24 +84,25 @@ async function getChartDetail() {
           ...res.Data,
           ChartInfo: {
             ...res.Data.ChartInfo,
-            Calendar: calendarType.value||'公历'
+            Calendar: calendarType.value || '公历'
           },
         },
-        renderId: 'chart-box',
+        renderId: 'chart-detail-box',
         // lang:currentLang.value,
         changeLangIsCheck: false,
         showChartTitle: true,
-        shouldUseSelfLimit:true,
+        shouldUseSelfLimit: true,
       })
     })
   }
 }
+getChartDetail()
 
 
 // 跳转指标溯源
 function handleGoEdbSource(data) {
   const href = router.resolve({
-    path: '/EDBSource',
+    path: '/etaChart/EDBSource',
     query: {
       code: data.UniqueCode
     }
@@ -114,79 +115,83 @@ function handleGoEdbSource(data) {
 </script>
 
 <template>
-  <div class="bg-white chart-wrap">
-    <template v-if="props.chartInfoId">
-      <div class="chart-render-wrap">
-        <div class="chart-box" id="chart-box"></div>
-        <div style="text-align: center">
-          <!-- 季节图 公历农历切换 -->
-          <t-radio-group
-            variant="primary-filled"
-            v-model="calendarType"
-            @change="getChartDetail"
-            v-if="chartInfo?.ChartType === 2"
-          >
-            <t-radio-button value="公历">公历</t-radio-button>
-            <t-radio-button value="农历">农历</t-radio-button>
-          </t-radio-group>
-        </div>
+  <div class="bg-white chart-wrap" v-loading="loading">
+    <div class="chart-render-wrap">
+      <div class="chart-box" id="chart-detail-box"></div>
+      <div style="text-align: center">
+        <!-- 季节图 公历农历切换 -->
+        <t-radio-group
+          variant="primary-filled"
+          v-model="calendarType"
+          @change="getChartDetail"
+          v-if="chartInfo?.ChartType === 2"
+        >
+          <t-radio-button value="公历">公历</t-radio-button>
+          <t-radio-button value="农历">农历</t-radio-button>
+        </t-radio-group>
+      </div>
 
-        <div class="chart-source" v-if="chartInfo">
-          <span
-            v-if="
-              chartInfo.SourcesFrom && JSON.parse(chartInfo.SourcesFrom).isShow
-            "
-            :style="`color: ${
-              JSON.parse(chartInfo.SourcesFrom).color
-            };fontSize: ${JSON.parse(chartInfo.SourcesFrom).fontSize}px;
+      <div class="chart-source" v-if="chartInfo">
+        <span
+          v-if="
+            chartInfo.SourcesFrom && JSON.parse(chartInfo.SourcesFrom).isShow
+          "
+          :style="`color: ${
+            JSON.parse(chartInfo.SourcesFrom).color
+          };fontSize: ${JSON.parse(chartInfo.SourcesFrom).fontSize}px;
                           `"
-            >来源:{{ JSON.parse(chartInfo.SourcesFrom).text }}</span
-          >
-        </div>
-      </div>
-      <div class="table-wrap">
-        <t-table
-          row-key="index"
-          :data="tableData"
-          :columns="columns"
-          bordered
-          hover
-          max-height="300"
-          cell-empty-content="-"
-          resizable
+          >来源:{{ JSON.parse(chartInfo.SourcesFrom).text }}</span
         >
-          <template #SourceName="{ row }">
-            <span>{{ row.SourceName }}</span>
-            <!-- 指标溯源 -->
-            <svg-icon
-              v-if="row.EdbType === 2"
-              style="
-                font-size: 20px;
-                cursor: pointer;
-                position: relative;
-                top: 5px;
-                color:#0052D9
-              "
-              name="edb_source"
-              @click="handleGoEdbSource(row)"
-            ></svg-icon>
-          </template>
-        </t-table>
       </div>
-      <div class="instructions-wrap" v-if="intro">
-        <p>逻辑简述:</p>
-        <p>{{ intro }}</p>
-      </div>
-    </template>
+    </div>
+    <div class="table-wrap">
+      <t-table
+        row-key="index"
+        :data="tableData"
+        :columns="columns"
+        bordered
+        hover
+        max-height="300"
+        cell-empty-content="-"
+        resizable
+      >
+        <template #SourceName="{ row }">
+          <span>{{ row.SourceName }}</span>
+          <!-- 指标溯源 -->
+          <svg-icon
+            v-if="row.EdbType === 2"
+            style="
+              font-size: 20px;
+              cursor: pointer;
+              position: relative;
+              top: 5px;
+              color: #0052d9;
+            "
+            name="edb_source"
+            @click="handleGoEdbSource(row)"
+          ></svg-icon>
+        </template>
+      </t-table>
+    </div>
+    <div class="instructions-wrap" v-if="intro">
+      <p>逻辑简述:</p>
+      <p>{{ intro }}</p>
+    </div>
   </div>
 </template>
 
 <style lang="scss" scoped>
 .chart-wrap {
-  flex: 1;
-  min-width: 600px;
+  min-height: 100%;
+  position: absolute;
+  z-index: 99;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
   border: 1px solid #dcdfe6;
   padding: 30px;
+  overflow: auto;
   .chart-render-wrap {
     margin-bottom: 20px;
     .chart-box {

+ 4 - 35
src/views/etaChart/components/ClassifyWrap.vue

@@ -7,32 +7,11 @@ import { useClassify } from '../hooks/useClassify'
 
 const emits = defineEmits(['change', 'filter'])
 
-const userProps = {
-  label: 'RealName',
-  value: 'AdminId',
-  children: 'ChildrenList'
-}
-const userOpts = ref([])
 const { userVal } = useClassify()
-async function getCompanyUserData() {
-  const res = await apiSystemCommon.companyUserList()
-  if (res.Ret === 200) {
-    userOpts.value = res.Data.List || []
-  }
+function handleUserChange(value,context) {
+  emits('filter')
+  getClassify()
 }
-getCompanyUserData()
-watch(
-  ()=>userVal.value,
-  (n)=>{
-    emits('filter')
-    getClassify()
-  }
-)
-// 2024-4-26 目前组件库change事件有bug会触发多次
-// function handleUserChange(value,context) {
-//   emits('filter')
-//   getClassify()
-// }
 
 const searchSelectKeys = {
   value: 'ChartInfoId',
@@ -139,17 +118,7 @@ function handleClassifyActiveChange({ node }) {
 <template>
   <div class="bg-white classify-wrap">
     <div class="select-wrap">
-      <t-cascader
-        v-model="userVal"
-        :options="userOpts"
-        :keys="userProps"
-        multiple
-        :minCollapsedNum="1"
-        clearable
-        filterable
-        :showAllLevels="false"
-        placeholder="创建人"
-      />
+      <select-chart-creator v-model="userVal" @change="handleUserChange"/>
     </div>
     <t-select
       v-model="searchVal"

+ 170 - 0
src/views/etaChart/components/chartList/ChartBox.vue

@@ -0,0 +1,170 @@
+<script setup>
+import { apiETAChart } from '@/api/etaChart'
+import { useChartRender } from '@/hooks/chart/render'
+import { onUnmounted, useTemplateRef } from 'vue'
+import ChartDetailPop from '../ChartDetailPop.vue'
+import CollectChart from '@/views/user/favorite/components/CollectChart.vue'
+
+const { options, axisLimitState, chartRender, setLimitData, isUseSelfLimit } = useChartRender()
+
+const props=defineProps({
+  chartData:{
+    type:Object,
+    default:()=>{}
+  },
+  index:{
+    type:Number,
+    default:0
+  }
+})
+
+
+const chartInfo = ref(null)
+const calendarType = ref('')
+const loading = ref(false)
+
+async function getChartDetail() {
+  loading.value = true
+  const res = await apiETAChart.chartDetail({
+    ChartInfoId: props.chartData.ChartInfoId,
+    Calendar: calendarType.value,
+  })
+  loading.value = false
+  if (res.Ret === 200) {
+    chartInfo.value = res.Data.ChartInfo
+    calendarType.value = res.Data.ChartInfo.Calendar
+    //初始化上下限
+    isUseSelfLimit.value = true
+    setLimitData(res.Data)
+    nextTick(() => {
+      chartRender({
+        data: {
+          ...res.Data,
+          ChartInfo: {
+            ...res.Data.ChartInfo,
+            Calendar: calendarType.value || '公历'
+          },
+        },
+        renderId: `chart-list-detail-box_${props.chartData.ChartInfoId}_${props.index}`,
+        // lang:currentLang.value,
+        changeLangIsCheck: false,
+        showChartTitle: true,
+        shouldUseSelfLimit: true,
+      })
+    })
+  }
+}
+
+
+let isVisible=false
+function handleIntersect(entries){
+  if (isVisible) return
+  entries.forEach(entry => {
+    // 判断是否在可视范围内
+    if (entry.isIntersecting) {
+      isVisible = true;
+      console.log('Component is visible');
+      // 在这里你可以执行其他操作,比如懒加载数据或图片等
+      getChartDetail()
+    }
+  });
+}
+
+// 监听组件是否到页面可视区域再加载图表
+let observer=null
+const compRef=useTemplateRef('compRef')
+function createObserver(){
+  const options = {
+    root: null, // 使用浏览器可视区域为根
+    threshold: 0.1, // 当至少10%的内容进入可视区时触发回调
+  };
+  observer = new IntersectionObserver(handleIntersect, options);
+  observer.observe(compRef.value); // 监听组件
+}
+
+onMounted(()=>{
+  createObserver()
+})
+onUnmounted(()=>{
+  if(observer){
+    observer.disconnect();
+  }
+})
+
+
+const showChartDetailPop=ref(false)
+
+const showCollectChart=ref(false)
+
+</script>
+
+<template>
+  <div class="flex chart-list-item-box" ref="compRef">
+    <div class="flex top-box">
+      <div class="text-ellipsis--l1 title">{{props.chartData.ChartClassifyName}}</div>
+      <svg-icon name="star" style="font-size:20px;cursor: pointer;" @click="showCollectChart=true"></svg-icon>
+      <svg-icon name="full_screen" style="font-size:20px;margin-left:20px;cursor: pointer;" @click="showChartDetailPop=true"></svg-icon>
+    </div>
+    <div class="chart-render-wrap" v-loading="loading">
+      <div class="chart-box" :id="`chart-list-detail-box_${props.chartData.ChartInfoId}_${props.index}`"></div>
+      <div style="text-align: center">
+        <!-- 季节图 公历农历切换 -->
+        <t-radio-group
+          variant="primary-filled"
+          v-model="calendarType"
+          @change="getChartDetail"
+          v-if="chartInfo?.ChartType === 2"
+        >
+          <t-radio-button value="公历">公历</t-radio-button>
+          <t-radio-button value="农历">农历</t-radio-button>
+        </t-radio-group>
+      </div>
+      <div class="chart-source" v-if="chartInfo">
+        <span
+          v-if="
+            chartInfo.SourcesFrom && JSON.parse(chartInfo.SourcesFrom).isShow
+          "
+          :style="`color: ${
+            JSON.parse(chartInfo.SourcesFrom).color
+          };fontSize: ${JSON.parse(chartInfo.SourcesFrom).fontSize}px;
+                          `"
+          >来源:{{ JSON.parse(chartInfo.SourcesFrom).text }}</span
+        >
+      </div>
+    </div>
+  </div>
+
+  <!-- 图表详情弹窗 -->
+  <ChartDetailPop v-model:show="showChartDetailPop" :chartInfoId="chartInfo?.ChartInfoId"/>
+
+  <!-- 收藏图表 -->
+  <CollectChart v-model:show="showCollectChart" :data="chartInfo"/>
+</template>
+
+<style lang="scss" scoped>
+.chart-list-item-box{
+  width: calc(50% - 10px);
+  height: 474px;
+  background-color: #fff;
+  box-shadow: 0px 4px 12px 0px #2300351f;
+  border: 1px solid var(--border-color);
+  border-radius: 4px;
+  flex-direction: column;
+  .top-box{
+    padding: 10px 14px;
+    border-bottom: 1px solid var(--border-color);
+    .title{
+      flex: 1;
+    }
+  }
+  .chart-render-wrap{
+    flex: 1;
+    padding: 10px 14px;
+    display: flex;
+    flex-direction: column;
+    .chart-box{
+      flex: 1;
+    }
+  }
+}
+</style>

+ 85 - 0
src/views/etaChart/components/chartList/Index.vue

@@ -0,0 +1,85 @@
+<script setup>
+import ChartBox from './ChartBox.vue'
+import { apiETAChart } from '@/api/etaChart'
+import {useClassify} from '../../hooks/useClassify'
+
+const {classifyActived,userVal} =useClassify()
+
+const page=ref(1)
+const pageSize=ref(30)
+const total=ref(0)
+const list=ref([])
+async function getTableData() {
+  const res = await apiETAChart.chartList({
+    PageSize: pageSize.value,
+    CurrentIndex: page.value,
+    ChartClassifyId:classifyActived.value[0]?classifyActived.value[0]:0,
+    SysUserIds:userVal.value?.join(',')||''
+  })
+  if (res.Ret === 200) {
+    const arr = res.Data.AllNodes || []
+    list.value = arr
+    total.value=res.Data.Paging.Totals
+  }
+}
+getTableData()
+
+function handlePageChange({current}){
+  page.value=current
+  list.value=[]
+  getTableData()
+}
+
+function handleRefreshList(){
+  page.value=1
+  total.value=0
+  list.value=[]
+  getTableData()
+}
+
+defineExpose({
+  handleRefreshList
+})
+
+
+</script>
+
+<template>
+  <div class="chart-render-detail-list">
+    <div class="flex list-wrap">
+      <ChartBox 
+        v-for="item,index in list" 
+        :key="item.ChartInfoId"
+        :chartData="item"
+        :index="index"
+      />
+    </div>
+    <t-pagination
+      style="margin-top:20px"
+      :current="page"
+      :pageSize="pageSize"
+      :total="total" 
+      :totalContent="false" 
+      :showPageSize="false"
+      @change="handlePageChange" 
+      v-if="total"
+    />
+    <empty-wrap v-if="list.length===0"/>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.chart-render-detail-list {
+  height: 100%;
+  width: 100%;
+  overflow-y: auto;
+  position: absolute;
+  z-index: 50;
+  left: 0;
+  top: 0;
+  .list-wrap {
+    flex-wrap: wrap;
+    gap: 20px;
+  }
+}
+</style>

+ 190 - 0
src/views/user/favorite/components/ClassifyWrap.vue

@@ -0,0 +1,190 @@
+<script setup>
+import {apiETAChartUser} from '@/api/etaChart'
+import { useTemplateRef, watch } from "vue";
+import EditClassify from './EditClassify.vue'
+import MoveChart from './MoveChart.vue'
+import { MessagePlugin } from 'tdesign-vue-next';
+
+const emits=defineEmits(['change'])
+
+const selecClassify=ref(0)
+function handleClassifyItemClick({node}){
+  selecClassify.value=node.value
+}
+
+watch(
+  ()=>selecClassify.value,
+  ()=>{
+    console.log("分类改变");
+    emits('change',selecClassify.value)
+  }
+)
+
+const classifyActions=[
+  {
+    content:'重命名',
+    value:'rename',
+    action: (node) => {
+      activeClassifyData.value = node.data;
+      showEditClassify.value = true;
+    },
+  },
+  {
+    content:'删除',
+    value:'delete',
+    action: (node) => handleDeleteClassify(node),
+  }
+]
+const classifyList=ref([])
+// 获取分类
+async function getClassifyList(){
+  const res=await apiETAChartUser.classifyList()
+  if(res.Ret!==200)return
+  const arr=res.Data?.List||[]
+  classifyList.value=arr
+  if(arr.length>0){
+    selecClassify.value=arr[0].CollectClassifyId
+  }
+}
+getClassifyList()
+
+// 移动排序
+const treeIns=useTemplateRef('treeIns')
+// dropPosition -1放在dropNode前面 0里面 1后面
+function handleAllowDrop({dropNode, dropPosition}){
+  if(dropPosition===0) return false//不允许向里面拖动
+}
+async function handleSortEnd({node}){
+  const resArr=treeIns.value.getTreeData()//移动后的数组
+  const moveTargetIndex=treeIns.value.getIndex(node.value)//被拖动的元素拖动后的位置
+  const params={
+    PrevCollectClassifyId:moveTargetIndex===0?0:resArr[moveTargetIndex-1].CollectClassifyId,
+    NextCollectClassifyId:moveTargetIndex===resArr.length-1?0:resArr[moveTargetIndex+1].CollectClassifyId,
+    CollectClassifyId:node.value
+  }
+  const res=await apiETAChartUser.moveClassify(params)
+  if(res.Ret!==200){
+    getClassifyList()
+    return
+  }
+  MessagePlugin.success("移动成功")
+}
+
+
+// 删除分类
+async function handleDeleteClassify(node){
+  await $confirmDialog({
+    header:'提示',
+    body: '若删除该分类,则分类下关联的所有图表将被全部删除,是否继续?',
+  });
+  const res=await apiETAChartUser.deleteClassify({
+    CollectClassifyId:node.value
+  })
+  if(res.Ret!==200) return
+  MessagePlugin.success('删除成功')
+  getClassifyList()
+}
+
+const showEditClassify=ref(false)
+const activeClassifyData=ref(null)
+// 点击分类的操作
+function clickHandler(option,node){
+  option.action(node);
+}
+
+// 转移分类
+const showMoveChart=ref(false)
+
+</script>
+
+<template>
+  <div class="bg-white flex_col classify-wrap">
+    <h3 class="label-text">ETA社区图库</h3>
+    <div class="classify-tree">
+      <t-tree
+        ref="treeIns"
+        :actived="[selecClassify]"
+        :data="classifyList"
+        :keys="{value:'CollectClassifyId',label:'ClassifyName'}"
+        activable
+        transition
+        draggable
+        :allow-drop="handleAllowDrop"
+        :onDragEnd="handleSortEnd"
+        :onClick="handleClassifyItemClick"
+      >
+        <template #icon>
+          <t-icon name="drag-move" style="font-size:20px"></t-icon>
+        </template>
+        <template #operations="{node}">
+          <t-dropdown :options="classifyActions" trigger="hover" @click="clickHandler($event,node)">
+            <t-icon name="more" style="font-size:20px"></t-icon>
+          </t-dropdown>
+        </template>
+        <template #empty>
+          <div style="text-align:center">
+            <img style="width:50%" src="@/assets/imgs/nodata.png" alt="">
+            <div>暂无数据</div>
+          </div>
+        </template>
+      </t-tree>
+    </div>
+    <div>
+      <div class="opt-item" @click="activeClassifyData=null;showEditClassify=true">
+        <t-icon name="add-rectangle" size="20px"></t-icon>
+        <span>添加分类</span>
+      </div>
+      <div class="opt-item" @click="showMoveChart=true">
+        <t-icon name="swap" size="20px"></t-icon>
+        <span>转移分类</span>
+      </div>
+    </div>
+  </div>
+  <!-- 新增\编辑分类 -->
+  <EditClassify 
+    v-model:show="showEditClassify" 
+    :data="activeClassifyData" 
+    @change="getClassifyList"
+  />
+  <!-- 转移分类 -->
+  <MoveChart v-model:show="showMoveChart" :classifyOpts="classifyList"/>
+</template>
+
+<style lang="scss" scoped>
+.classify-wrap{
+  flex: 1;
+  border: 1px solid var(--border-color);
+  box-shadow: 0px 4px 12px 0px #2300351F;
+  border-radius: 4px;
+  .label-text{
+    padding: 20px;
+    margin: 0;
+    font-size: 16px;
+  }
+  .classify-tree{
+    flex: 1;
+    overflow-y: auto;
+    padding-bottom: 30px;
+    :deep(.t-is-active){
+      background-color: var(--td-brand-color-light);
+    }
+    :deep(.t-tree__icon){
+      margin-left: 20px;
+    }
+    :deep(.t-tree__operations){
+      margin-right: 20px;
+    }
+  }
+  .opt-item{
+    color: var(--td-brand-color);
+    text-align: center;
+    margin-bottom: 30px;
+    cursor: pointer;
+    span{
+      display: inline-block;
+      margin-left: 5px;
+      font-size: 16px;
+    }
+  }
+}
+</style>

+ 127 - 0
src/views/user/favorite/components/CollectChart.vue

@@ -0,0 +1,127 @@
+<script setup>
+import { apiETAChartUser } from '@/api/etaChart'
+import { MessagePlugin } from 'tdesign-vue-next'
+import EditClassify from './EditClassify.vue'
+
+
+
+const show = defineModel('show', { type: Boolean, default: false })
+const emits = defineEmits(['change'])
+
+const props = defineProps({
+  data: {
+    type: [null, Object],
+    default: null
+  }
+})
+
+const selectClassify = ref('')
+const classifyOpts = ref([])
+async function getClassifyOpts() {
+  const res = await apiETAChartUser.classifyList()
+  if (res.Ret !== 200) return
+  const arr = res.Data?.List || []
+  classifyOpts.value = arr
+}
+
+const showEditClassify=ref(false)
+
+watch(
+  () => show.value,
+  (n) => {
+    if (n) {
+      selectClassify.value = ''
+      getClassifyOpts()
+    }
+  }
+)
+
+async function handleSave(){
+  if(!selectClassify.value){
+    MessagePlugin.warning('请选择分类')
+    return
+  }
+  const res=await apiETAChartUser.chartCollect({
+    ChartInfoId:props.data.ChartInfoId,
+    CollectClassifyId:selectClassify.value
+  })
+  if(res.Ret!==200) return
+  show.value=false
+  if(res.Msg==='已收藏,可选择在ETA社区-我的收藏/ETA-我的图库中-ETA社区图库查看'){
+    await $confirmDialog({
+      header:'提示',
+      body: res.Msg,
+      closeBtn:false,
+      confirmBtn:'知道了'
+    });
+    return
+  }
+  MessagePlugin.success('收藏成功')
+  
+}
+
+
+</script>
+
+<template>
+  <t-dialog
+    v-model:visible="show"
+    attach="body"
+    width="500px"
+    :header="'选择我的分类'"
+    closeOnOverlayClick
+    destroyOnClose
+    class="collect-chart-pop"
+  >
+    <div class="classify-wrap">
+      <t-tag
+        :theme="selectClassify===item.CollectClassifyId?'primary':'default'"
+        :variant="selectClassify===item.CollectClassifyId?'':'outline'"
+        v-for="item in classifyOpts"
+        :key="item.CollectClassifyId"
+        @click="selectClassify=item.CollectClassifyId"
+        >{{ item.ClassifyName }}</t-tag
+      >
+    </div>
+    <div class="add_btn" @click="showEditClassify=true">
+      <t-icon name="add-circle"></t-icon>
+      <span>新增</span>
+    </div>
+    <template #footer>
+      <div class="bottom-btn">
+        <t-button theme="default" @click="show = false">取消</t-button>
+        <t-button type="submit" @click="handleSave">确定</t-button>
+      </div>
+    </template>
+  </t-dialog>
+
+  <!-- 新增\编辑分类 -->
+  <EditClassify 
+    v-model:show="showEditClassify" 
+    :data="null" 
+    @change="getClassifyOpts"
+  />
+</template>
+
+<style lang="scss" scoped>
+.collect-chart-pop{
+  .classify-wrap{
+    padding: 20px;
+    border-radius: 4px;
+    border: 1px dashed var(--border-color);
+    display: flex;
+    flex-wrap: wrap;
+    gap: 5px;
+  }
+  .add_btn{
+    margin-top: 20px;
+    display: flex;
+    align-items: center;
+    color: var(--td-brand-color);
+    font-size: 16px;
+    gap: 0 5px;
+    cursor: pointer;
+  }
+}
+
+</style>

+ 85 - 0
src/views/user/favorite/components/EditClassify.vue

@@ -0,0 +1,85 @@
+<script setup>
+import { useTemplateRef } from "vue"
+import {apiETAChartUser} from '@/api/etaChart'
+import {MessagePlugin} from 'tdesign-vue-next'
+
+
+
+const show = defineModel('show', { type: Boolean, default: false })
+const emits=defineEmits(['change'])
+
+const props = defineProps({
+  data: {
+    type: [null, Object],
+    default: null
+  }
+})
+
+const FORM_RULES={
+  classifyName:[{ required: true, message: '请输入分类名称' }]
+}
+const formData=reactive({
+  classifyName:''
+})
+const formIns=useTemplateRef('formIns')
+async function handleSave(){
+  const validRes=await formIns.value.validate()
+  if(validRes!==true) return
+  const res=props.data?await apiETAChartUser.editClassify({
+    ClassifyName:formData.classifyName,
+    CollectClassifyId:props.data.CollectClassifyId
+  }): await apiETAChartUser.addClassify({
+    ClassifyName:formData.classifyName
+  })
+  if(res.Ret!==200) return
+  MessagePlugin.success(props.data?'编辑成功':'新增成功')
+  show.value=false
+  emits('change')
+}
+
+
+watch(
+  ()=>show.value,
+  (n)=>{
+    if(!n){
+      formIns.value.reset()
+      return
+    }
+    if(n&&props.data){
+      formData.classifyName=props.data.ClassifyName
+    }
+  }
+)
+
+</script>
+
+<template>
+  <t-dialog
+    v-model:visible="show"
+    attach="body"
+    width="500px"
+    :header="props.data?'重命名分类':'新增分类'"
+    closeOnOverlayClick
+    destroyOnClose
+    class="classify-edit-pop"
+  >
+    <t-form
+      ref="formIns"
+      :rules="FORM_RULES"
+      :data="formData"
+    >
+      <t-form-item label="分类名称" name="classifyName">
+        <t-input
+          v-model="formData.classifyName"
+          placeholder="请输入分类名称"
+        ></t-input>
+      </t-form-item>
+    </t-form>
+    <template #footer>
+      <div class="bottom-btn">
+        <t-button theme="default" @click="show=false">取消</t-button>
+        <t-button type="submit" @click="handleSave">确定</t-button>
+      </div>
+    </template>
+  </t-dialog>
+</template>

+ 159 - 0
src/views/user/favorite/components/MoveChart.vue

@@ -0,0 +1,159 @@
+<script setup>
+import { apiETAChartUser } from '@/api/etaChart'
+import { SearchIcon } from 'tdesign-icons-vue-next'
+
+const show = defineModel('show', { type: Boolean, default: false })
+
+
+const props=defineProps({
+  classifyOpts:{
+    type:Array,
+    default:()=>[]
+  }
+})
+
+
+const selectClassify = ref('')
+const keyword = ref('')
+const checkAll = ref(false)
+
+const columns=[
+  {
+    colKey: 'row-select',
+    type: 'multiple',
+    width: 50,
+  },
+  {
+    colKey: 'ChartName',
+    title: '图表名称',
+    align: 'center'
+  },
+  {
+    colKey: 'UserId',
+    title: '创建人',
+    width: '120',
+    align: 'center'
+  },
+  
+]
+const tableData=ref([])
+const pagination = ref({
+  defaultPageSize: 20,
+  total: 0,
+  current: 1,
+  showPageSize:false
+});
+async function getChartList(){
+  const res=await apiETAChartUser.chartCollectList({
+    CollectClassifyIds:selectClassify.value,
+    Keyword:keyword.value,
+    PageSize:pagination.value.defaultPageSize,
+    CurrentIndex:pagination.value.current
+  })
+  if(res.Ret!==200) return
+  const arr=res.Data.List||[]
+  pagination.value.total=res.Data.Paging.Totals
+  tableData.value=arr
+}
+watch(
+  ()=>show.value,
+  (n)=>{
+    if(n){
+      getChartList()
+    }
+  }
+)
+
+function onPageChange(){
+
+}
+
+const selectedRowKeys = ref([]);
+function handleTableSelectChange(value, ctx){
+  selectedRowKeys.value=value
+}
+
+const newClassify=ref('')
+async function handleSave() {
+
+}
+
+
+</script>
+
+<template>
+  <t-dialog
+    v-model:visible="show"
+    attach="body"
+    width="850px"
+    header="转移分类(ETA社区分类)"
+    closeOnOverlayClick
+    destroyOnClose
+    class="chart-move-pop"
+  >
+    <div class="flex top-filter-wrap">
+      <t-select
+        v-model="selectClassify"
+        placeholder="图表分类"
+        style="max-width: 235px"
+      >
+        <t-option
+          v-for="item in props.classifyOpts"
+          :key="item.CollectClassifyId"
+          :label="item.ClassifyName"
+          :value="item.CollectClassifyId"
+        ></t-option>
+      </t-select>
+      <t-input placeholder="请输入图表名称" style="max-width: 600px">
+        <template #prefixIcon>
+          <SearchIcon />
+        </template>
+      </t-input>
+      <t-checkbox style="flex-shrink: 0" v-model="checkAll">全选</t-checkbox>
+    </div>
+    <t-table
+      row-key="ChartCollectId"
+      :data="tableData"
+      :columns="columns"
+      bordered
+      hover
+      max-height="500"
+      resizable
+      :selected-row-keys="selectedRowKeys"
+      :pagination="pagination"
+      @page-change="onPageChange"
+      @select-change="handleTableSelectChange"
+    >
+    </t-table>
+    <div class="flex" style="align-items:center;gap:0 10px">
+      <span>移动至新分类</span>
+      <t-select
+        v-model="newClassify"
+        placeholder="图表分类"
+        style="max-width: 235px"
+      >
+        <t-option
+          v-for="item in props.classifyOpts"
+          :key="item.CollectClassifyId"
+          :label="item.ClassifyName"
+          :value="item.CollectClassifyId"
+        ></t-option>
+      </t-select>
+    </div>
+    <template #footer>
+      <div class="bottom-btn" style="text-align: center">
+        <t-button theme="default" @click="show = false">取消</t-button>
+        <t-button type="submit" @click="handleSave">确认</t-button>
+      </div>
+    </template>
+  </t-dialog>
+</template>
+
+<style lang="scss" scoped>
+.chart-move-pop {
+  .top-filter-wrap {
+    gap: 10px;
+    margin-bottom: 20px;
+  }
+}
+</style>

+ 179 - 0
src/views/user/favorite/etaChart.vue

@@ -0,0 +1,179 @@
+<script setup>
+import {SearchIcon} from 'tdesign-icons-vue-next'
+import ClassifyWrap from './components/ClassifyWrap.vue'
+import {apiETAChartUser} from '@/api/etaChart'
+import ChartDetailPop from '@/views/etaChart/components/ChartDetailPop.vue'
+
+let classifyId=0
+const SysUserIds=ref([])
+const keyword=ref('')
+const chartList=ref([])
+const page=ref(1)
+const pageSize=ref(30)
+const total=ref(0)
+const finished=ref(false)
+async function getChartList(){
+  const res=await apiETAChartUser.chartCollectList({
+    CollectClassifyIds:classifyId,
+    SysUserIds:SysUserIds.value.join(','),
+    Keyword:keyword.value,
+    PageSize:pageSize.value,
+    CurrentIndex:page.value
+  })
+  if(res.Ret!==200) return
+  const arr=res.Data.List||[]
+  total.value=res.Data.Paging.Totals
+  finished.value=res.Data.Paging.IsEnd
+  chartList.value=arr
+}
+
+function onPageChange(){
+
+}
+function refreshList(){
+  page.value=1
+  chartList.value=[]
+  getChartList()
+}
+function handleClassifyChange(e){
+  classifyId=e
+  console.log('gafkdsf');
+  refreshList()
+}
+
+
+const showChartDetailPop=ref(false)
+const activeChartInfo=ref(null)
+function handleShowChartDetail(e){
+  activeChartInfo.value=e
+  showChartDetailPop.value=true
+}
+
+</script>
+
+<template>
+  <div class="flex my-favorite-chart-page">
+    <div class="flex left-wrap">
+      <div class="top-box">
+        <span @click="$router.push('/etaChart/index')">研究图库 ></span>
+        <span>我的收藏</span>
+      </div>
+      <ClassifyWrap @change="handleClassifyChange"/>
+    </div>
+    <div class="flex right-wrap">
+      <div class="flex top-filter">
+        <select-chart-creator v-model="SysUserIds" @change="refreshList" style="width:300px" size="large" />
+        <t-input 
+          v-model="keyword" 
+          placeholder="请输入图表名称" 
+          size="large" 
+          style="max-width:600px"
+          clearable
+          @change="refreshList"
+        >
+          <template #prefixIcon>
+            <SearchIcon />
+          </template>
+        </t-input>
+      </div>
+      <empty-wrap v-if="finished&&chartList.length===0" />
+      <div class="chart-list-wrap" v-else>
+        <ul class="flex chart-list">
+          <li class="bg-white chart-item" v-for="item in chartList" :key="item.ChartCollectId">
+            <div class="flex chart-name">
+              <div class="text-ellipsis--l1 name">{{item.ChartName}}</div>
+              <svg-icon name="star_fill" style="font-size:20px;cursor: pointer;"></svg-icon>
+              <svg-icon name="full_screen" style="font-size:20px;margin-left:20px;cursor: pointer;" @click="handleShowChartDetail(item)"></svg-icon>
+            </div>
+            <img class="chart-img" :src="item.ChartImage" alt="">
+            <div class="time">
+              <span>收藏时间:</span>
+              <span>{{formatTime(item.CollectTime,'YYYY-MM-DD HH:mm:ss')}}</span>
+            </div>
+          </li>
+        </ul>
+        <t-pagination
+          v-model="page"
+          :pageSize="pageSize"
+          :total="total"
+          :totalContent="false"
+          :showPageSize="false"
+          @current-change="onPageChange"
+        />
+      </div>
+    </div>
+  </div>
+
+  <!-- 图表详情弹窗 -->
+  <ChartDetailPop v-model:show="showChartDetailPop" :chartInfoId="activeChartInfo?.ChartInfoId"/>
+</template>
+
+<style lang="scss" scoped>
+.my-favorite-chart-page{
+  gap: 0 20px;
+  height: calc(100vh - 120px);
+  .left-wrap{
+    width: 310px;
+    height: 100%;
+    flex-direction: column;
+    .top-box{
+      height: 40px;
+      line-height: 40px;
+      margin-bottom: 20px;
+      span:first-child{
+        color: #666;
+        cursor: pointer;
+      }
+      span:last-child{
+        font-weight: 600;
+      }
+    }
+  }
+  .right-wrap{
+    flex: 1;
+    height: 100%;
+    flex-direction: column;
+    .top-filter{
+      margin-bottom: 20px;
+      gap: 0 20px;
+    }
+    .chart-list-wrap{
+      flex: 1;
+      overflow-y: auto;
+    }
+  }
+  .chart-list{
+    flex-wrap: wrap;
+    gap: 20px;
+    margin-block-start:0;
+    margin-block-end:0;
+    padding-inline-start:0;
+    .chart-item{
+      list-style-type: none;
+      width: calc(calc(100% - 60px)/4);
+      border: 1px solid var(--border-color);
+      box-shadow: 0px 4px 12px 0px #2300351F;
+      border-radius: 4px;
+      overflow: hidden;
+      .chart-name{
+        border-bottom: 1px solid var(--border-color);
+        padding: 10px 14px;
+        .name{
+          font-size: 16px;
+          font-weight: 600;
+          flex: 1;
+        }
+      }
+      .chart-img{
+        display: block;
+        height: 230px;
+      }
+      .time{
+        border-top: 1px solid var(--border-color);
+        padding: 10px 14px;
+        color: #999999;
+      }
+    }
+  }
+}
+</style>

+ 8 - 0
vite.config.js

@@ -5,6 +5,8 @@ import AutoImport from "unplugin-auto-import/vite";
 import Components from "unplugin-vue-components/vite";
 import { TDesignResolver } from "unplugin-vue-components/resolvers";
 import { createSvgIconsPlugin } from "vite-plugin-svg-icons";
+import zipBuildPlugin from './zipBuildPlugin';
+import vueJsx from '@vitejs/plugin-vue-jsx';
 
 // https://vitejs.dev/config/
 export default defineConfig(({ mode }) => {
@@ -14,12 +16,17 @@ export default defineConfig(({ mode }) => {
     base: ENV.VITE_APP_BASE_URL, // 若服务器不是将该项目放在根目录的则 需要此设置 和服务器上同名
     plugins: [
       vue(),
+      vueJsx(),
       AutoImport({
+        // 自动导入 Vue 相关函数,如:ref, reactive, toRef 等
+        imports: ["vue"],
+        dirs: ["./src/plugin"], // 自动扫描并导入 `src/plugin` 下的方法
         resolvers: [
           TDesignResolver({
             library: "vue-next",
           }),
         ],
+        dts: "src/auto-imports.d.ts", // 自动生成类型声明文件
       }),
       Components({
         resolvers: [
@@ -61,6 +68,7 @@ export default defineConfig(({ mode }) => {
          */
         // customDomId: '__svg__icons__dom__'
       }),
+      zipBuildPlugin()
     ],
     css: {
       // css预处理器

+ 55 - 0
zipBuildPlugin.js

@@ -0,0 +1,55 @@
+// zipPlugin.js
+import { resolve } from 'path';
+import { createWriteStream } from 'fs';
+import archiver from 'archiver';
+
+export default function zipBuildPlugin() {
+  let outDir = 'dist'; // 默认值
+
+  return {
+    name: 'zip-after-build',
+    configResolved(resolvedConfig) {
+      // 读取 Vite 配置中的 build.outDir
+      outDir = resolvedConfig.build.outDir;
+    },
+    closeBundle: {
+      sequential: true,
+      async handler() {
+        const outputDir = resolve(__dirname, outDir);
+        const zipFilePath = resolve(__dirname, `${outDir}.zip`);
+
+        // 创建文件输出流
+        const output = createWriteStream(zipFilePath);
+        const archive = archiver('zip', {
+          zlib: { level: 9 } // 设置压缩等级
+        });
+
+        return new Promise((resolve, reject) => {
+          output.on('close', () => {
+            console.log(`打包完成,共 ${archive.pointer()} 字节`);
+            resolve();
+          });
+
+          archive.on('warning', (err) => {
+            if (err.code !== 'ENOENT') {
+              reject(err);
+            }
+          });
+
+          archive.on('error', (err) => {
+            reject(err);
+          });
+
+          // 将归档数据管道化到文件
+          archive.pipe(output);
+
+          // 将 dist 文件夹及其内容都压缩进 zip
+          archive.directory(outputDir, outDir);
+
+          // 完成归档
+          archive.finalize();
+        });
+      }
+    }
+  };
+}

Some files were not shown because too many files changed in this diff