Pārlūkot izejas kodu

实现了k线指标和主图副图1 通用用能添加语言切换路由

Hexinkui 3 nedēļas atpakaļ
vecāks
revīzija
8a7ab2c37b

+ 75 - 41
src/locales/index.js

@@ -1,71 +1,105 @@
 import { createI18n } from 'vue-i18n'
 import { Locale } from 'vant'
 
-// 1. 同步引入中文 (兜底防乱码)
-import defaultZhCN from './lang/zh-CN'
-import defaultVantZhCN from 'vant/es/locale/lang/zh-CN'
+// 1. 静态引入:确保路径正确 (根据你的截图 ./lang/ 是对的)
+import userZhCN from './lang/zh-CN.js'
+import vantZhCN from 'vant/es/locale/lang/zh-CN'
+
+// 2. 数据清洗:确保拿到的是纯净的对象,而不是 Module
+const userData = userZhCN.default || userZhCN
+const vantData = vantZhCN.default || vantZhCN
+
+// 3. 深度合并:将 Vant 和 User 数据合并
+// ⚠️ 注意顺序:userData 在后,确保用户自定义的优先级更高
+const finalZhCN = { ...vantData, ...userData }
+
+// 🔍 再次确认日志
+console.log('🚀 i18n Data Ready:', {
+  hasCommon: !!finalZhCN.common,
+  switchLang: finalZhCN.common?.switchLang
+})
+
+const savedLang = localStorage.getItem('app-lang') || 'zh-CN'
 
 const i18n = createI18n({
   legacy: false,
-  locale: 'zh-CN', // 初始设为中文
+  locale: savedLang,
   fallbackLocale: 'zh-CN',
   globalInjection: true,
+  // 4. 初始化消息:这里是关键,确保 zh-CN 永远有值
   messages: {
-    'zh-CN': { ...defaultVantZhCN, ...defaultZhCN }
+    'zh-CN': finalZhCN
   }
 })
 
-// 2. Vant 语言包映射 (8国语言)
-const vantLocales = {
-  'en-US': () => import('vant/es/locale/lang/en-US'),
-  'de-DE': () => import('vant/es/locale/lang/de-DE'),
-  'es-ES': () => import('vant/es/locale/lang/es-ES'),
-  'fr-FR': () => import('vant/es/locale/lang/fr-FR'),
-  'pt-PT': () => import('vant/es/locale/lang/pt-BR'),
-  'id-ID': () => import('vant/es/locale/lang/id-ID'),
-  'ru-RU': () => import('vant/es/locale/lang/ru-RU'),
+// 记录已加载语言
+const loadedLanguages = ['zh-CN']
+
+// 5. 设置 Vant 语言的辅助函数
+function setVantLanguage(lang, messages) {
+  if (lang === 'zh-CN') {
+    Locale.use('zh-CN', vantData)
+  } else {
+    Locale.use(lang, messages)
+  }
 }
 
-// 3. 核心加载函数
 export async function loadLanguageAsync(lang) {
+  // 空值检查
+  if (!lang || lang === 'undefined' || lang === 'null') {
+    return loadLanguageAsync('zh-CN')
+  }
+
+  // A. 如果是中文:直接切过去,千万不要重新加载,防止覆盖
   if (lang === 'zh-CN') {
-    setI18nLanguage('zh-CN')
+    i18n.global.locale.value = 'zh-CN'
+    setVantLanguage('zh-CN')
+    document.querySelector('html').setAttribute('lang', 'zh-CN')
+    localStorage.setItem('app-lang', 'zh-CN')
     return 'zh-CN'
   }
 
-  if (i18n.global.availableLocales.includes(lang)) {
-    setI18nLanguage(lang)
+  // B. 如果已加载:直接切
+  if (loadedLanguages.includes(lang)) {
+    i18n.global.locale.value = lang
+    setVantLanguage(lang, i18n.global.getLocaleMessage(lang)) // 尝试从 i18n 实例获取 vant 数据有点难,这里简化处理
+    document.querySelector('html').setAttribute('lang', lang)
+    localStorage.setItem('app-lang', lang)
     return lang
   }
 
+  // C. 动态加载新语言
   try {
-    const [userMsg, vantMsg] = await Promise.all([
-      import(`./lang/${lang}.js`),
-      vantLocales[lang] ? vantLocales[lang]() : Promise.resolve({ default: {} })
-    ])
+    // 动态导入映射
+    // ⚠️ 确保这些文件在 src/locales/lang/ 目录下存在
+    const userImport = import(`./lang/${lang}.js`)
 
-    const messages = { ...vantMsg.default, ...userMsg.default }
-    i18n.global.setLocaleMessage(lang, messages)
+    // Vant 映射
+    let vantImportPromise = Promise.resolve({ default: {} })
+    if (lang === 'en-US') vantImportPromise = import('vant/es/locale/lang/en-US')
+    // ... 可以按需添加其他 Vant 语言 ...
 
-    setI18nLanguage(lang, vantMsg.default)
-    return lang
-  } catch (e) {
-    console.error('Language load failed:', e)
-    return 'zh-CN'
-  }
-}
+    const [userMod, vantMod] = await Promise.all([userImport, vantImportPromise])
 
-function setI18nLanguage(lang, vantMsg) {
-  i18n.global.locale.value = lang
-  localStorage.setItem('app-lang', lang)
-  document.querySelector('html').setAttribute('lang', lang)
+    const userMsg = userMod.default || userMod
+    const vantMsg = vantMod.default || vantMod
 
-  if (lang === 'zh-CN') {
-    Locale.use('zh-CN', defaultVantZhCN)
-  } else if (vantMsg) {
-    Locale.use(lang, vantMsg)
-  } else if (vantLocales[lang]) {
-    vantLocales[lang]().then(mod => Locale.use(lang, mod.default))
+    const finalMsg = { ...vantMsg, ...userMsg }
+
+    i18n.global.setLocaleMessage(lang, finalMsg)
+    loadedLanguages.push(lang)
+
+    // 切换
+    i18n.global.locale.value = lang
+    setVantLanguage(lang, vantMsg)
+    document.querySelector('html').setAttribute('lang', lang)
+    localStorage.setItem('app-lang', lang)
+
+    return lang
+  } catch (e) {
+    console.error(`❌ Load lang ${lang} failed:`, e)
+    // 失败回退到中文
+    return loadLanguageAsync('zh-CN')
   }
 }
 

+ 4 - 4
src/locales/lang/en-US.js

@@ -1,9 +1,9 @@
 export default {
   common: {
-      switchLang: '*** ENGLISH ***',
-    confirm: '*** SUBMIT ***',
-    // switchLang: 'Switch Language',
-    // confirm: 'Submit',
+      // switchLang: '*** ENGLISH ***',
+    // confirm: '*** SUBMIT ***',
+    switchLang: 'Switch Language',
+    confirm: 'Submit',
     cancel: 'Cancel',
     save: 'Save',
     loading: 'Loading...'

+ 1 - 1
src/locales/lang/zh-CN.js

@@ -1,5 +1,5 @@
 export default {
-  // 通用词汇
+  // 通用词汇common.switchLang
   common: {
     switchLang: '切换语言',
     confirm: '提交',

+ 24 - 22
src/main.js

@@ -1,40 +1,42 @@
 import { createApp } from "vue";
 import App from "./App.vue";
-import 'amfe-flexible'
+import 'amfe-flexible';
 import router from "./router";
 // import store from "./store";
 import 'vant/lib/index.css';
-import "./assets/less/index.less"; // 你的全局样式
+import "./assets/less/index.less";
 import "./assets/h5-reset.css";
 
-// 引入 i18n 和 加载函数
-import i18n, { loadLanguageAsync } from './locales/index'
+// 引入 i18n
+import i18n, { loadLanguageAsync } from './locales/index.js';
 
 import {
-  Button,
-  Popup,
-  Form,
-  Field,
-  NavBar,
-  Picker,
-  Icon,
-  Toast
+  Button, Popup, Form, Field, NavBar, Picker, Icon, Toast
 } from 'vant';
 
 import api from "./utils/api.js";
 
 const savedLang = localStorage.getItem('app-lang') || 'zh-CN';
 
-// 等待语言包加载完成后再渲染
-loadLanguageAsync(savedLang).then(() => {
-  const app = createApp(App);
+// 🚀 启动逻辑
+loadLanguageAsync(savedLang)
+  .catch(err => {
+    // 即使加载语言失败,也要强行启动 App,避免白屏
+    console.error('Failed to load language, falling back to rendering...', err);
+  })
+  .then(() => {
+    const app = createApp(App);
 
-  app.use(Button).use(Popup).use(Form).use(Field).use(NavBar).use(Picker).use(Icon).use(Toast);
-  // app.use(store);
-  app.use(router);
-  app.use(i18n);
+    // Vant 组件注册
+    app.use(Button).use(Popup).use(Form).use(Field).use(NavBar).use(Picker).use(Icon).use(Toast);
 
-  app.config.globalProperties.$api = api;
+    // 核心插件
+    app.use(i18n);   // ✅ 此时 i18n 内部应该已经有数据了
+    app.use(router);
+    // app.use(store);
 
-  app.mount("#app");
-});
+    app.config.globalProperties.$api = api;
+
+    app.mount("#app");
+    console.log('🚀 App Mounted Successfully');
+  });

+ 4 - 1
src/views/bitcoin/CommonFunctionsPopup/CommonFunctionsPopup.vue

@@ -95,7 +95,10 @@ const handleItemClick = (item) => {
       showRules.value = true
     } else if (item.name === '计算器') {
       router.push({ name: 'calculator' })
-    } else {
+    }else if(item.name === '语言切换'){
+       router.push({ name: "LanguageSwitch" })
+
+    }else {
       console.log('点击了其他:', item.name)
     }
   }, 200) // 稍微延迟一点点,让关闭动画看起来更自然(可选)

+ 66 - 44
src/views/bitcoin/lever/components/KLineChart.vue

@@ -17,7 +17,7 @@ const props = defineProps({
     default: () => ({
       up: '#2EBD85',
       down: '#F6465D',
-      grid: '#F2F4F6',
+      grid: '#F2F4F6', // 浅色网格
       text: '#929AA5',
       targetLine: '#4A6EF5'
     })
@@ -33,7 +33,7 @@ const initChart = () => {
 
   chartInstance = klinecharts.init(chartContainer.value)
 
-  // 设置精度
+  // 1. 设置精度
   const { price, volume } = props.precision
   if (chartInstance.setPriceVolumePrecision) {
     chartInstance.setPriceVolumePrecision(price, volume)
@@ -41,81 +41,103 @@ const initChart = () => {
     chartInstance.setPrecision(price, volume)
   }
 
-  // 样式配置
-  const { up, down, grid, text, targetLine } = props.colors
+  // 2. 样式配置
+  const { up, down, grid, text } = props.colors
   chartInstance.setStyleOptions({
     grid: { show: true, horizontal: { show: true, size: 1, color: grid, style: 'dash', dashValue: [5, 5] }, vertical: { show: false } },
     candle: {
       type: 'candle_solid',
       bar: { upColor: up, downColor: down, noChangeColor: up },
-      priceMark: {
-        show: true,
-        last: { show: true, upColor: up, downColor: down, line: { show: true, style: 'dash' }, text: { show: true, color: '#FFF', paddingLeft: 4, paddingRight: 4, borderRadius: 2 } }
-      },
       tooltip: { showRule: 'follow_cross', showType: 'rect', dataSource: 'none' }
     },
-    xAxis: { axisLine: { show: false }, tickLine: { show: false }, tickText: { color: text, size: 10, paddingTop: 8 } },
-    yAxis: { inside: true, axisLine: { show: false }, tickLine: { show: false }, tickText: { color: text, size: 10, paddingLeft: 8 } },
+    xAxis: { axisLine: { show: false }, tickLine: { show: false }, tickText: { color: text, size: 10, paddingTop: 4 } },
+    yAxis: { inside: true, axisLine: { show: false }, tickLine: { show: false }, tickText: { color: text, size: 10, paddingLeft: 4 } },
   })
 
-  // 默认加载 MA 和 VOL
+  // 3. 默认指标: 主图 MA
   chartInstance.createTechnicalIndicator('MA', false, { id: 'candle_pane' })
-  chartInstance.createTechnicalIndicator('VOL', false, { id: 'pane_1', heightRatio: 0.2 })
 
-  // 初始加载
+  // 4. 默认两个副图 (初始就应用权重逻辑)
+  chartInstance.createTechnicalIndicator('VOL', false, { id: 'pane_1', dragEnabled: false })
+  chartInstance.createTechnicalIndicator('MACD', false, { id: 'pane_2', dragEnabled: false })
+
+  // 🔥 关键初始化:强制分配权重
+  // 主图权重 8,副图权重 1
+  chartInstance.setPaneOptions({ id: 'candle_pane', heightRatio: 8 })
+  chartInstance.setPaneOptions({ id: 'pane_1', heightRatio: 1 })
+  chartInstance.setPaneOptions({ id: 'pane_2', heightRatio: 1 })
+
+  // 5. 加载数据
   if (props.data && props.data.length > 0) {
     chartInstance.applyNewData(toRaw(props.data))
   }
 }
 
-// --- 🎯 新增:暴露给父组件的方法 ---
-
-// 1. 设置主图指标
+// --- 设置主图指标 ---
 const setMainIndicator = (name) => {
   if (!chartInstance) return
-  // 移除常见主图指标
-  chartInstance.removeTechnicalIndicator('candle_pane', 'MA')
-  chartInstance.removeTechnicalIndicator('candle_pane', 'BOLL')
+  const mains = ['MA', 'EMA', 'BOLL', 'SAR', 'AVL', 'TRIX']
+  mains.forEach(m => chartInstance.removeTechnicalIndicator('candle_pane', m))
 
   if (name && name !== 'Hide') {
     chartInstance.createTechnicalIndicator(name, false, { id: 'candle_pane' })
   }
 }
 
-// 2. 设置副图指标
-const setSubIndicator = (name) => {
+// --- 🟢 核心修复:基于权重的动态高度控制 ---
+const setSubIndicators = (names) => {
   if (!chartInstance) return
-  // 移除常见副图指标
-  const subs = ['VOL', 'MACD', 'KDJ', 'RSI', 'WR']
-  subs.forEach(s => chartInstance.removeTechnicalIndicator('pane_1', s))
 
-  if (name && name !== 'Hide') {
-    chartInstance.createTechnicalIndicator(name, false, { id: 'pane_1', heightRatio: 0.2 })
-  }
+  // 1. 彻底清除旧指标
+  const paneIds = ['pane_1', 'pane_2', 'pane_3', 'pane_4', 'pane_5']
+  const allSubs = ['VOL', 'MACD', 'KDJ', 'RSI', 'WR', 'CCI', 'OBV', 'AO', 'DMA', 'TRIX', 'BRAR', 'VR']
+
+  paneIds.forEach(paneId => {
+    allSubs.forEach(sub => chartInstance.removeTechnicalIndicator(paneId, sub))
+  })
+
+  const validNames = names.filter(n => n && n !== 'Hide')
+
+  // 2. 重新创建并分配权重
+  // 核心思路:主图(candle_pane)永远设为 8,每个副图设为 1
+  // 这样无论加多少个副图,主图都会占据绝大部分空间
+
+  // 步骤A: 必须先显式重置主图权重,确保它是“老大”
+  chartInstance.setPaneOptions({
+    id: 'candle_pane',
+    heightRatio: 8
+  })
+
+  validNames.forEach((name, index) => {
+    const paneId = `pane_${index + 1}`
+
+    // 步骤B: 创建指标
+    chartInstance.createTechnicalIndicator(name, false, {
+      id: paneId,
+      dragEnabled: false
+    })
+
+    // 步骤C: 强制给副图分配“小弟”权重 (1)
+    chartInstance.setPaneOptions({
+      id: paneId,
+      heightRatio: 1
+    })
+  })
 }
 
 defineExpose({
   setMainIndicator,
-  setSubIndicator
+  setSubIndicators
 })
 
-// --- 原有逻辑保持不变 ---
-
+// --- 数据监听 ---
 watch(() => props.data, (newData) => {
   if (!chartInstance) return
-
   const rawData = toRaw(newData)
   const currentList = chartInstance.getDataList()
 
-  if (rawData.length === 0) {
-    chartInstance.clearData()
-    return
-  }
-
-  if (currentList.length === 0) {
-    chartInstance.applyNewData(rawData)
-    return
-  }
+  if (rawData.length === 0) { chartInstance.clearData(); return }
+  if (currentList.length === 0) { chartInstance.applyNewData(rawData); return }
 
   const firstOld = currentList[0]
   const firstNew = rawData[0]
@@ -123,7 +145,6 @@ watch(() => props.data, (newData) => {
     chartInstance.applyNewData(rawData)
     return
   }
-
   if (rawData.length > 0) {
     const lastData = rawData[rawData.length - 1]
     chartInstance.updateData(lastData)
@@ -132,8 +153,8 @@ watch(() => props.data, (newData) => {
 
 watch(() => props.precision, (val) => {
   if (chartInstance) {
-    if (chartInstance.setPriceVolumePrecision) chartInstance.setPriceVolumePrecision(val.price, val.volume)
-    else if (chartInstance.setPrecision) chartInstance.setPrecision(val.price, val.volume)
+    const method = chartInstance.setPriceVolumePrecision || chartInstance.setPrecision
+    if (method) method.call(chartInstance, val.price, val.volume)
   }
 }, { deep: true })
 
@@ -156,6 +177,7 @@ const handleResize = () => {
 </script>
 
 <style scoped>
-.kline-wrapper { width: 100%; position: relative; }
+/* 亮色背景配置 */
+.kline-wrapper { width: 100%; position: relative; background: #ffffff; }
 .kline-chart { width: 100%; height: 100%; }
 </style>

+ 1 - 1
src/views/index/components/HotCoin.vue

@@ -59,7 +59,7 @@
 </template>
 
 <script setup>
-import { GetCoins } from '@/api/index'
+import { GetCoins } from '@/api/index.js'
 import { ref, onMounted, onUnmounted } from 'vue'
 import { useRoute, useRouter } from "vue-router";
 

+ 94 - 151
src/views/market/details/MarketConditions.vue

@@ -40,7 +40,6 @@
 
     <!-- 2. 周期切换 Tab -->
     <nav class="time-tabs">
-      <!-- 常用周期 -->
       <div
         v-for="tab in visibleTabs"
         :key="tab"
@@ -50,18 +49,13 @@
         {{ getTabLabel(tab) }}
       </div>
 
-      <!-- 更多按钮 (下拉菜单) -->
-      <div class="tab-item icon-btn" @click.stop="toggleMore">
-        <span :class="{ 'active-text': isMoreActive }">更多</span>
-        <span class="triangle">◢</span>
-
-        <!-- 下拉菜单 -->
+      <div class="tab-item icon-btn" @click.stop="toggleMore" :style="isMoreActive ? { backgroundColor: '#F6465D', color: '#fff' } : {}">
+        <span :class="{ 'active-text': isMoreActive }">{{ isMoreActive ? getTabLabel(currentTab) : '更多' }}</span>
+        <span class="triangle" :style="isMoreActive ? { color: '#fff' } : {}">◢</span>
         <div class="dropdown-menu" v-show="showMoreMenu">
           <div
-            v-for="mt in moreTabs"
-            :key="mt"
-            class="drop-item"
-            :class="{ active: currentTab === mt }"
+            v-for="mt in moreTabs" :key="mt"
+            class="drop-item" :class="{ active: currentTab === mt }"
             @click.stop="switchPeriod(mt)">
             {{ getTabLabel(mt) }}
           </div>
@@ -70,29 +64,36 @@
 
       <!-- 指标按钮 -->
       <div class="tab-item icon-btn" @click.stop="toggleIndicators">
+        <!-- 保持你的图标路径 -->
         <img src="@/assets/icon/bitcoin/lishidingdan.svg" alt="" />
       </div>
     </nav>
 
-    <!-- 3. 指标设置面板 (点击指标图标弹出) -->
+    <!-- 3. 指标设置面板 -->
     <div class="indicator-panel" v-show="showIndicatorMenu" @click.stop>
+      <!-- 主图设置 (单选) -->
       <div class="panel-section">
-        <div class="section-title">主图</div>
+        <div class="section-title">主图指标</div>
         <div class="btn-group">
-          <div class="idx-btn" :class="{ active: mainIdx === 'MA' }" @click="changeMain('MA')">MA</div>
-          <div class="idx-btn" :class="{ active: mainIdx === 'BOLL' }" @click="changeMain('BOLL')">BOLL</div>
+          <div v-for="m in ['MA', 'EMA', 'BOLL', 'SAR']" :key="m"
+               class="idx-btn" :class="{ active: mainIdx === m }"
+               @click="changeMain(m)">
+            {{ m }}
+          </div>
           <div class="idx-btn" :class="{ active: mainIdx === 'Hide' }" @click="changeMain('Hide')">隐藏</div>
         </div>
       </div>
+
+      <!-- 副图设置 (多选) -->
       <div class="panel-section">
-        <div class="section-title">副图</div>
+        <div class="section-title">副图指标 (可多选)</div>
         <div class="btn-group">
-          <div class="idx-btn" :class="{ active: subIdx === 'VOL' }" @click="changeSub('VOL')">VOL</div>
-          <div class="idx-btn" :class="{ active: subIdx === 'MACD' }" @click="changeSub('MACD')">MACD</div>
-          <div class="idx-btn" :class="{ active: subIdx === 'KDJ' }" @click="changeSub('KDJ')">KDJ</div>
-          <div class="idx-btn" :class="{ active: subIdx === 'RSI' }" @click="changeSub('RSI')">RSI</div>
-          <div class="idx-btn" :class="{ active: subIdx === 'WR' }" @click="changeSub('WR')">WR</div>
-          <div class="idx-btn" :class="{ active: subIdx === 'Hide' }" @click="changeSub('Hide')">隐藏</div>
+          <div v-for="s in ['VOL', 'MACD', 'KDJ', 'RSI', 'WR', 'CCI', 'OBV']" :key="s"
+               class="idx-btn" :class="{ active: subIdx.includes(s) }"
+               @click="toggleSub(s)">
+            {{ s }}
+          </div>
+          <div class="idx-btn" :class="{ active: subIdx.length === 0 }" @click="clearSub">隐藏</div>
         </div>
       </div>
     </div>
@@ -102,11 +103,11 @@
       <KlineChart
         ref="klineRef"
         :data="kLineData"
-        height="100%"
+        height="70vh"
         :precision="{ price: getPricePrecision(marketInfo.price), volume: 2 }" />
     </div>
 
-    <!-- 5. 底部挂单/成交 -->
+    <!-- 5. 底部 -->
     <div class="notifi-classifi">
       <div class="pf600 fs14" :class="current === 'entrustingOrder' ? 'fc121212' : 'fcA8A8A8'" @click="messageChange('entrustingOrder')">委托挂单</div>
       <div class="sys-notifi pf600 fs14" :class="current === 'latestTransactions' ? 'fc121212' : 'fcA8A8A8'" @click="messageChange('latestTransactions')">最新成交</div>
@@ -117,7 +118,7 @@
 </template>
 
 <script setup>
-import { ref, computed, onMounted, onBeforeUnmount, watch } from "vue";
+import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from "vue";
 import { useRoute } from "vue-router";
 import { GetCandlestickChart } from "@/api/index.js";
 import EntrustingOrder from "./EntrustingOrder.vue";
@@ -127,42 +128,58 @@ import KlineChart from "@/views/bitcoin/lever/components/KLineChart.vue";
 const route = useRoute();
 const symbolId = computed(() => route.query.id || "6");
 
-// --- 周期设置 ---
+// 周期
 const currentTab = ref("1d");
-// 外面显示的周期
 const visibleTabs = ["15m", "1h", "4h", "1d"];
-// 下拉菜单里的周期
 const moreTabs = ["1m", "5m", "30m", "1w", "1M"];
 const isMoreActive = computed(() => moreTabs.includes(currentTab.value));
+const getTabLabel = (t) => ({ '1m':'1分', '5m':'5分', '15m':'15分', '30m':'30分', '1h':'1小时', '4h':'4小时', '1d':'日线', '1w':'周线', '1M':'月线' }[t] || t);
 
-const getTabLabel = (t) => {
-  const map = { '1m':'1分', '5m':'5分', '15m':'15分', '30m':'30分', '1h':'1小时', '4h':'4小时', '1d':'日线', '1w':'周线', '1M':'月线' };
-  return map[t] || t;
-};
-
-// --- UI 状态 ---
+// --- 指标管理 (升级版) ---
 const showMoreMenu = ref(false);
 const showIndicatorMenu = ref(false);
+const klineRef = ref(null);
+
+// 主图状态 (单选)
 const mainIdx = ref('MA');
-const subIdx = ref('VOL');
-const klineRef = ref(null); // 引用子组件
 
-// --- 交互方法 ---
-const toggleMore = () => {
-  showIndicatorMenu.value = false;
-  showMoreMenu.value = !showMoreMenu.value;
+// 副图状态 (多选,默认选两个)
+const subIdx = ref(['VOL', 'MACD']);
+
+// 切换主图
+const changeMain = (name) => {
+  mainIdx.value = name;
+  if (klineRef.value) klineRef.value.setMainIndicator(name);
 };
 
-const toggleIndicators = () => {
-  showMoreMenu.value = false;
-  showIndicatorMenu.value = !showIndicatorMenu.value;
+// 切换副图 (多选逻辑)
+const toggleSub = (name) => {
+  const index = subIdx.value.indexOf(name);
+  if (index > -1) {
+    // 已经选中 -> 取消选中
+    subIdx.value.splice(index, 1);
+  } else {
+    // 未选中 -> 添加选中 (限制最多3个防止太挤,或者不做限制)
+    if (subIdx.value.length >= 3) {
+        subIdx.value.shift(); // 移除最早选的一个,保持最多3个
+    }
+    subIdx.value.push(name);
+  }
+  // 更新图表
+  if (klineRef.value) klineRef.value.setSubIndicators(subIdx.value);
 };
 
-const closePopups = () => {
-  showMoreMenu.value = false;
-  showIndicatorMenu.value = false;
+// 隐藏所有副图
+const clearSub = () => {
+  subIdx.value = [];
+  if (klineRef.value) klineRef.value.setSubIndicators([]);
 };
 
+// --- 交互 ---
+const toggleMore = () => { showIndicatorMenu.value = false; showMoreMenu.value = !showMoreMenu.value; };
+const toggleIndicators = () => { showMoreMenu.value = false; showIndicatorMenu.value = !showIndicatorMenu.value; };
+const closePopups = () => { showMoreMenu.value = false; showIndicatorMenu.value = false; };
+
 const switchPeriod = (period) => {
   if (currentTab.value === period) return;
   currentTab.value = period;
@@ -171,25 +188,13 @@ const switchPeriod = (period) => {
   getKlineData();
 };
 
-const changeMain = (name) => {
-  mainIdx.value = name;
-  if (klineRef.value) klineRef.value.setMainIndicator(name);
-};
-
-const changeSub = (name) => {
-  subIdx.value = name;
-  if (klineRef.value) klineRef.value.setSubIndicator(name);
-};
-
-// --- 下面是 WebSocket 和数据逻辑 (保持你原有的逻辑) ---
+// --- 数据/WS (保持不变) ---
 const kLineData = ref([]);
 const socket = ref(null);
 const WS_BASE_URL = "ws://backend.66linknow.com/ws/kline/";
 const HEARTBEAT_INTERVAL = 15000;
 const RECONNECT_DELAY = 3000;
-let heartbeatTimer = null;
-let reconnectTimer = null;
-let isUnmounted = false;
+let heartbeatTimer = null, reconnectTimer = null, isUnmounted = false;
 
 const marketInfo = ref({ price: "0.00", fiatPrice: "0.00", change: 0.0, high: "0.00", low: "0.00", vol: "0", amount: "0" });
 const orderPlacement = ref();
@@ -213,12 +218,9 @@ const getKlineData = async () => {
 
       if (marketInfo.value.price === "0.00") {
         const lastBar = formattedData[formattedData.length - 1];
-        let totalVol = 0;
-        formattedData.forEach(i => totalVol += i.volume);
-        marketInfo.value = {
-          price: lastBar.close, fiatPrice: lastBar.close, change: 0,
-          high: lastBar.high, low: lastBar.low, vol: totalVol, amount: totalVol * lastBar.close
-        };
+        let totalVol = 0; formattedData.forEach(i => totalVol += i.volume);
+        const changeRate = formattedData[0].open ? ((lastBar.close - formattedData[0].open) / formattedData[0].open) * 100 : 0;
+        marketInfo.value = { price: lastBar.close, fiatPrice: lastBar.close, change: changeRate.toFixed(2), high: lastBar.high, low: lastBar.low, vol: totalVol, amount: totalVol * lastBar.close };
       }
     }
   } catch (error) { console.error("API Error", error); }
@@ -230,15 +232,9 @@ const connectWebSocket = () => {
   const url = `${WS_BASE_URL}?symbol=${symbolStr}`;
   try {
     socket.value = new WebSocket(url);
-    socket.value.onopen = () => { startHeartbeat(); };
-    socket.value.onmessage = (event) => { handleSocketMessage(event.data); };
-    socket.value.onclose = () => {
-      stopHeartbeat();
-      if (!isUnmounted) {
-        clearTimeout(reconnectTimer);
-        reconnectTimer = setTimeout(() => connectWebSocket(), RECONNECT_DELAY);
-      }
-    };
+    socket.value.onopen = () => startHeartbeat();
+    socket.value.onmessage = (event) => handleSocketMessage(event.data);
+    socket.value.onclose = () => { stopHeartbeat(); if (!isUnmounted) { clearTimeout(reconnectTimer); reconnectTimer = setTimeout(() => connectWebSocket(), RECONNECT_DELAY); } };
   } catch (e) { if (!isUnmounted) reconnectTimer = setTimeout(() => connectWebSocket(), RECONNECT_DELAY); }
 };
 
@@ -251,7 +247,6 @@ const handleSocketMessage = (msgStr) => {
     if (msgStr === "pong") return;
     const rawData = JSON.parse(msgStr);
     const msg = rawData.data || rawData;
-
     if (msg.e === "kline") {
       const k = msg.k;
       if (k.i !== currentTab.value) return;
@@ -269,7 +264,7 @@ const handleSocketMessage = (msgStr) => {
 };
 
 const updateKlineData = (newBar) => {
-  if (!kLineData.value?.length) return;
+  if (!kLineData.value?.length) { kLineData.value = [newBar]; return; }
   const lastIndex = kLineData.value.length - 1;
   const lastBar = kLineData.value[lastIndex];
   if (newBar.timestamp === lastBar.timestamp) kLineData.value.splice(lastIndex, 1, newBar);
@@ -277,7 +272,6 @@ const updateKlineData = (newBar) => {
 };
 
 const handleVisibilityChange = () => { if (document.visibilityState === "visible" && socket.value?.readyState !== 1) connectWebSocket(); };
-
 onMounted(() => { isUnmounted = false; getKlineData(); connectWebSocket(); document.addEventListener("visibilitychange", handleVisibilityChange); });
 onBeforeUnmount(() => { isUnmounted = true; closeWebSocket(); document.removeEventListener("visibilitychange", handleVisibilityChange); });
 watch(symbolId, () => { kLineData.value = []; getKlineData(); connectWebSocket(); });
@@ -294,100 +288,49 @@ const getUpDownClass = (c) => (c >= 0 ? "fc45B26B" : "fcF6465D");
 .fcF6465D { color: #f6465d !important; }
 .fc1F2937 { color: #1f2937; }
 
-/* 周期切换栏 */
 .time-tabs {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  width: 100%;
-  box-sizing: border-box;
-  padding: 10px 15px 0 15px;
-  position: relative;
-  z-index: 20;
+  display: flex; align-items: center; justify-content: space-between;
+  width: 100%; box-sizing: border-box; padding: 0 15px 0 15px; position: relative; z-index: 20;
 }
-
 .tab-item {
-  font-size: 14px;
-  color: #929aa5;
-  padding: 4px 8px;
-  border-radius: 4px;
-  cursor: pointer;
-  font-weight: 500;
-  display: flex;
-  align-items: center;
-  position: relative;
+  font-size: 14px; color: #929aa5; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-weight: 500; display: flex; align-items: center; position: relative;
 }
 .tab-item.icon-btn { padding: 4px 4px; }
-.active-text { color: #1F2937; font-weight: 600; }
+.active-text { color: #fff; font-weight: 600; }
 .triangle { font-size: 8px; margin-left: 2px; transform: scale(0.9); }
 .tab-item img { display: block; height: 16px; width: auto; }
 
-/* 更多周期下拉菜单 */
 .dropdown-menu {
-  position: absolute;
-  top: 30px;
-  left: -20px;
-  width: 80px;
-  background: #fff;
-  box-shadow: 0 4px 12px rgba(0,0,0,0.15);
-  border-radius: 6px;
-  padding: 5px 0;
-  z-index: 100;
-  display: flex;
-  flex-direction: column;
-}
-.drop-item {
-  padding: 8px 15px;
-  font-size: 13px;
-  color: #666;
-  text-align: center;
+  position: absolute; top: 30px; left: -20px; width: 80px; background: #fff;
+  box-shadow: 0 4px 12px rgba(0,0,0,0.15); border-radius: 6px; padding: 5px 0;
+  z-index: 100; display: flex; flex-direction: column;
 }
+.drop-item { padding: 8px 15px; font-size: 13px; color: #666; text-align: center; }
 .drop-item.active { color: #F6465D; background: #fff5f5; }
 
-/* 指标面板 */
 .indicator-panel {
-  position: absolute;
-  top: 155px; /* 调整此值以对齐 K 线图顶部 */
-  left: 15px;
-  right: 15px;
-  background: #fff;
-  box-shadow: 0 4px 15px rgba(0,0,0,0.1);
-  border-radius: 8px;
-  padding: 15px;
-  z-index: 99;
+  position: absolute; top: 155px; left: 15px; right: 15px; background: #fff;
+  box-shadow: 0 4px 15px rgba(0,0,0,0.1); border-radius: 8px; padding: 15px; z-index: 99;
 }
 .panel-section { margin-bottom: 15px; }
 .section-title { font-size: 12px; color: #999; margin-bottom: 8px; }
 .btn-group { display: flex; flex-wrap: wrap; gap: 10px; }
-.idx-btn {
-  padding: 4px 12px; border: 1px solid #eee; border-radius: 14px; font-size: 12px; color: #666; cursor: pointer;
-}
+.idx-btn { padding: 4px 12px; border: 1px solid #eee; border-radius: 14px; font-size: 12px; color: #666; cursor: pointer; }
 .idx-btn.active { border-color: #F6465D; color: #F6465D; background-color: #fff5f5; }
 
-/* 容器布局 */
-.market-conditions {
-  display: flex; flex-direction: column; align-items: center; margin-bottom: 50px; width: 100%; position: relative;
-}
+.market-conditions { display: flex; flex-direction: column; align-items: center; margin-bottom: 50px; width: 100%; position: relative; }
 .market-price {
-  display: flex; justify-content: space-between; margin-top: 8px; width: 100%; height: 73px; padding: 0 15px; box-sizing: border-box;
-  .price-left {
-    display: flex; flex-direction: column; width: 144px; height: 69px;
+  display: flex; justify-content: space-between; margin-top: 8px;
+  width: 100%; padding: 0 15px; box-sizing: border-box;
+  .price-left { display: flex; flex-direction: column; width: 144px; line-height: 20px;
+    height: 69px;
     .left-price { display: flex; align-items: center; height: 18px; }
     .left-number { margin-top: 5px; }
-    .left-appro { display: flex; align-items: center; margin-top: 3px; .appro { margin-left: 9px; } }
-  }
-  .price-right {
-    display: flex; flex-direction: column; height: 100%;
-    .right-number-top, .right-number-bottom {
-      display: flex; justify-content: flex-end; width: 100%; height: 32px;
-      .right-number-top-price, .right-number-top-number { margin-left: 10px; text-align: right; div { height: 16px; line-height: 16px; text-align: end; } }
-    }
-    .right-number-bottom { margin-top: 9px; }
-  }
-}
-.k-line-main { height: 50vh; min-height: 350px; width: 100%; padding: 0 15px; margin-top: 10px; }
-.notifi-classifi {
-  display: flex; align-items: flex-end; margin-top: 15px; width: 100%; padding: 0 15px; box-sizing: border-box; height: 24px; border-bottom: 1px solid #f5f5f5; padding-bottom: 5px;
-  .sys-notifi { margin-left: 47px; }
+    .left-appro { display: flex; align-items: center;margin-top: 3px;
+      .appro { margin-left: 9px; } } }
+  .price-right { display: flex; flex-direction: column; height: 100%; line-height: 20px;
+    .right-number-top, .right-number-bottom { display: flex; justify-content: flex-end; width: 100%; height: 32px; .right-number-top-price, .right-number-top-number { margin-left: 10px; text-align: right; div { height: 16px; line-height: 16px; text-align: end; } } } .right-number-bottom {  } }
 }
+.k-line-main { height: 50vh; min-height: 350px; width: 100%; padding: 0 15px; }
+.notifi-classifi { display: flex; align-items: flex-end; margin-top: 15px; width: 100%; padding: 0 15px; box-sizing: border-box; height: 24px; border-bottom: 1px solid #f5f5f5; padding-bottom: 5px; .sys-notifi { margin-left: 47px; } }
 </style>

+ 2 - 2
src/views/user/LanguageSwitch.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="page-container">
     <van-nav-bar
-      :title="$t('common.switchLang')"
+      :title="$t('common.confirm')"
       left-arrow
       fixed
       placeholder
@@ -37,7 +37,7 @@ import { ref } from 'vue';
 import { useRouter } from 'vue-router';
 import { useI18n } from 'vue-i18n';
 import { showLoadingToast, closeToast, Icon as VanIcon } from 'vant';
-import { loadLanguageAsync } from '@/locales/index';
+import { loadLanguageAsync } from '@/locales/index.js';
 
 const router = useRouter();
 const { locale } = useI18n();