Kaynağa Gözat

我的股票功能更新

Zhangbw 3 ay önce
ebeveyn
işleme
60bcb6f451

+ 0 - 0
STOCK_LIST_FEATURE.md


+ 21 - 2
dist/dev/mp-weixin/common/vendor.js

@@ -2406,6 +2406,9 @@ function isShallow(value) {
     /* ReactiveFlags.IS_SHALLOW */
   ]);
 }
+function isProxy(value) {
+  return isReactive(value) || isReadonly(value);
+}
 function toRaw(observed) {
   const raw = observed && observed[
     "__v_raw"
@@ -2841,8 +2844,8 @@ const resolvedPromise = /* @__PURE__ */ Promise.resolve();
 let currentFlushPromise = null;
 const RECURSION_LIMIT = 100;
 function nextTick$1(fn) {
-  const p = currentFlushPromise || resolvedPromise;
-  return fn ? p.then(this ? fn.bind(this) : fn) : p;
+  const p2 = currentFlushPromise || resolvedPromise;
+  return fn ? p2.then(this ? fn.bind(this) : fn) : p2;
 }
 function findInsertionIndex(id) {
   let start = flushIndex + 1;
@@ -4741,6 +4744,12 @@ const Static = Symbol("Static");
 function isVNode(value) {
   return value ? value.__v_isVNode === true : false;
 }
+const InternalObjectKey = `__vInternal`;
+function guardReactiveProps(props) {
+  if (!props)
+    return null;
+  return isProxy(props) || InternalObjectKey in props ? extend({}, props) : props;
+}
 const emptyAppContext = createAppContext();
 let uid = 0;
 function createComponentInstance(vnode, parent, suspense) {
@@ -5786,6 +5795,11 @@ function initApp(app) {
   }
 }
 const propsCaches = /* @__PURE__ */ Object.create(null);
+function renderProps(props) {
+  const { uid: uid2, __counter } = getCurrentInstance();
+  const propsId = (propsCaches[uid2] || (propsCaches[uid2] = [])).push(guardReactiveProps(props)) - 1;
+  return uid2 + "," + propsId + "," + __counter;
+}
 function pruneComponentPropsCache(uid2) {
   delete propsCaches[uid2];
 }
@@ -5956,6 +5970,7 @@ const f = (source, renderItem) => vFor(source, renderItem);
 const e = (target, ...sources) => extend(target, ...sources);
 const n = (value) => normalizeClass(value);
 const t = (val) => toDisplayString(val);
+const p = (props) => renderProps(props);
 const sr = (ref2, id, opts) => setRef(ref2, id, opts);
 function createApp$1(rootComponent, rootProps = null) {
   rootComponent && (rootComponent.mpType = "app");
@@ -6797,15 +6812,19 @@ exports._export_sfc = _export_sfc;
 exports.createSSRApp = createSSRApp;
 exports.e = e;
 exports.f = f;
+exports.getCurrentInstance = getCurrentInstance;
 exports.index = index;
 exports.n = n;
+exports.nextTick$1 = nextTick$1;
 exports.o = o;
 exports.onHide = onHide;
 exports.onLoad = onLoad;
 exports.onMounted = onMounted;
 exports.onShow = onShow;
 exports.onUnload = onUnload;
+exports.p = p;
 exports.ref = ref;
 exports.resolveComponent = resolveComponent;
 exports.sr = sr;
 exports.t = t;
+exports.watch = watch;

+ 182 - 0
dist/dev/mp-weixin/components/StockListItem.js

@@ -0,0 +1,182 @@
+"use strict";
+const common_vendor = require("../common/vendor.js");
+const _sfc_main = {
+  __name: "StockListItem",
+  props: {
+    stock: {
+      type: Object,
+      required: true
+    },
+    showDelete: {
+      type: Boolean,
+      default: false
+    }
+  },
+  emits: ["delete"],
+  setup(__props, { emit }) {
+    const props = __props;
+    let componentInstance = null;
+    const deleteWidth = 60;
+    const moveX = common_vendor.ref(0);
+    let currentX = 0;
+    const canvasId = common_vendor.ref(`chart-${props.stock.code}-${Math.random().toString(36).slice(2, 11)}`);
+    const getMarketTag = (code) => {
+      if (code.startsWith("6"))
+        return "沪";
+      if (code.startsWith("0"))
+        return "深";
+      if (code.startsWith("3"))
+        return "创";
+      return "沪";
+    };
+    const getMarketClass = (code) => {
+      if (code.startsWith("6"))
+        return "market-sh";
+      if (code.startsWith("0"))
+        return "market-sz";
+      if (code.startsWith("3"))
+        return "market-cy";
+      return "market-sh";
+    };
+    const getChangeClass = (changePercent) => {
+      if (!changePercent)
+        return "";
+      const str = String(changePercent).replace("%", "").replace("+", "");
+      const value = parseFloat(str);
+      if (value > 0)
+        return "change-up";
+      if (value < 0)
+        return "change-down";
+      return "";
+    };
+    const formatChangePercent = (changePercent) => {
+      if (!changePercent)
+        return "--";
+      return String(changePercent);
+    };
+    const formatPrice = (price) => {
+      if (!price)
+        return "--";
+      return parseFloat(price).toFixed(2);
+    };
+    const drawTrendChart = (instance) => {
+      console.log("[趋势图] 开始绘制:", props.stock.code, canvasId.value);
+      let trendData = props.stock.trendData;
+      if (!trendData || !Array.isArray(trendData) || trendData.length === 0) {
+        console.log("[趋势图] 使用模拟数据");
+        trendData = generateMockTrendData();
+      } else {
+        console.log("[趋势图] 使用真实数据,数据点数:", trendData.length);
+      }
+      const ctx = common_vendor.index.createCanvasContext(canvasId.value, instance);
+      const width = 100;
+      const height = 30;
+      const padding = 2;
+      const maxValue = Math.max(...trendData);
+      const minValue = Math.min(...trendData);
+      const range = maxValue - minValue || 1;
+      console.log("[趋势图] 数据范围:", { min: minValue, max: maxValue, range });
+      const changePercent = parseFloat(String(props.stock.changePercent || "0").replace("%", "").replace("+", ""));
+      const isUp = changePercent >= 0;
+      const lineColor = isUp ? "#FF3B30" : "#34C759";
+      const fillColor = isUp ? "rgba(255, 59, 48, 0.1)" : "rgba(52, 199, 89, 0.1)";
+      console.log("[趋势图] 颜色:", { isUp, lineColor, changePercent });
+      ctx.beginPath();
+      trendData.forEach((value, index) => {
+        const x = padding + index / (trendData.length - 1) * (width - padding * 2);
+        const y = height - padding - (value - minValue) / range * (height - padding * 2);
+        if (index === 0) {
+          ctx.moveTo(x, y);
+        } else {
+          ctx.lineTo(x, y);
+        }
+      });
+      ctx.setStrokeStyle(lineColor);
+      ctx.setLineWidth(1);
+      ctx.stroke();
+      ctx.lineTo(width - padding, height - padding);
+      ctx.lineTo(padding, height - padding);
+      ctx.closePath();
+      ctx.setFillStyle(fillColor);
+      ctx.fill();
+      ctx.draw();
+      console.log("[趋势图] 绘制完成");
+    };
+    const generateMockTrendData = () => {
+      const changePercent = parseFloat(String(props.stock.changePercent || "0").replace("%", "").replace("+", ""));
+      const points = 30;
+      const data = [];
+      let baseValue = 100;
+      const trend = changePercent / 100;
+      for (let i = 0; i < points; i++) {
+        const randomChange = (Math.random() - 0.5) * 2;
+        const trendChange = i / points * trend * 100;
+        baseValue = baseValue + randomChange + trendChange / points;
+        data.push(baseValue);
+      }
+      return data;
+    };
+    const handleMoveChange = (e) => {
+      currentX = e.detail.x;
+    };
+    const handleMoveEnd = () => {
+      if (!props.showDelete)
+        return;
+      if (currentX < -deleteWidth / 3) {
+        moveX.value = -deleteWidth;
+      } else {
+        moveX.value = 0;
+      }
+    };
+    const handleDelete = () => {
+      moveX.value = 0;
+      emit("delete");
+    };
+    common_vendor.onMounted(() => {
+      console.log("[趋势图] 组件挂载:", props.stock.code);
+      componentInstance = common_vendor.getCurrentInstance();
+      common_vendor.nextTick$1(() => {
+        setTimeout(() => {
+          drawTrendChart(componentInstance);
+        }, 300);
+      });
+    });
+    common_vendor.watch(() => props.stock.trendData, (newData) => {
+      if (newData && componentInstance) {
+        console.log("[趋势图] 数据更新,重新绘制:", props.stock.code);
+        common_vendor.nextTick$1(() => {
+          drawTrendChart(componentInstance);
+        });
+      }
+    }, { deep: true });
+    common_vendor.watch(() => props.stock.changePercent, () => {
+      if (componentInstance) {
+        common_vendor.nextTick$1(() => {
+          drawTrendChart(componentInstance);
+        });
+      }
+    });
+    return (_ctx, _cache) => {
+      return common_vendor.e({
+        a: common_vendor.t(__props.stock.name),
+        b: common_vendor.t(getMarketTag(__props.stock.code)),
+        c: common_vendor.n(getMarketClass(__props.stock.code)),
+        d: common_vendor.t(__props.stock.code),
+        e: canvasId.value,
+        f: canvasId.value,
+        g: common_vendor.t(formatChangePercent(__props.stock.changePercent)),
+        h: common_vendor.n(getChangeClass(__props.stock.changePercent)),
+        i: common_vendor.t(formatPrice(__props.stock.currentPrice)),
+        j: __props.showDelete
+      }, __props.showDelete ? {
+        k: common_vendor.o(handleDelete)
+      } : {}, {
+        l: moveX.value,
+        m: common_vendor.o(handleMoveChange),
+        n: common_vendor.o(handleMoveEnd)
+      });
+    };
+  }
+};
+const Component = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["__scopeId", "data-v-29af7fd7"], ["__file", "D:/program/gupiao-wx/src/components/StockListItem.vue"]]);
+wx.createComponent(Component);

+ 4 - 0
dist/dev/mp-weixin/components/StockListItem.json

@@ -0,0 +1,4 @@
+{
+  "component": true,
+  "usingComponents": {}
+}

+ 1 - 0
dist/dev/mp-weixin/components/StockListItem.wxml

@@ -0,0 +1 @@
+<view class="stock-item-wrapper data-v-29af7fd7"><movable-area class="movable-area data-v-29af7fd7"><movable-view class="movable-view data-v-29af7fd7" direction="horizontal" x="{{l}}" damping="{{40}}" friction="{{5}}" out-of-bounds="{{false}}" bindchange="{{m}}" bindtouchend="{{n}}"><view class="stock-list-item data-v-29af7fd7"><view class="stock-left data-v-29af7fd7"><view class="stock-name-row data-v-29af7fd7"><text class="stock-name data-v-29af7fd7">{{a}}</text><text class="{{['data-v-29af7fd7', 'stock-tag', c]}}">{{b}}</text></view><text class="stock-code data-v-29af7fd7">{{d}}</text></view><view class="stock-chart data-v-29af7fd7"><block wx:if="{{r0}}"><canvas canvas-id="{{e}}" id="{{f}}" class="trend-canvas data-v-29af7fd7"></canvas></block></view><view class="stock-right data-v-29af7fd7"><view class="{{['data-v-29af7fd7', 'change-percent', h]}}">{{g}}</view><text class="stock-price data-v-29af7fd7">{{i}}</text></view><view wx:if="{{j}}" class="delete-action data-v-29af7fd7" catchtap="{{k}}"><view class="delete-icon-wrapper data-v-29af7fd7"><text class="delete-icon data-v-29af7fd7">✕</text></view></view></view></movable-view></movable-area></view>

+ 134 - 0
dist/dev/mp-weixin/components/StockListItem.wxss

@@ -0,0 +1,134 @@
+
+.stock-item-wrapper.data-v-29af7fd7 {
+  position: relative;
+  height: 120rpx;
+  overflow: hidden;
+  background: #ffffff;
+}
+.movable-area.data-v-29af7fd7 {
+  width: 100%;
+  height: 100%;
+}
+.movable-view.data-v-29af7fd7 {
+  width: calc(100% + 120rpx);
+  height: 100%;
+}
+.stock-list-item.data-v-29af7fd7 {
+  display: flex;
+  align-items: center;
+  padding: 24rpx 0;
+  height: 100%;
+  box-sizing: border-box;
+  background: #ffffff;
+  border-bottom: 1rpx solid #f1f2f6;
+}
+.stock-item-wrapper:last-child .stock-list-item.data-v-29af7fd7 {
+  border-bottom: none;
+}
+
+/* 滑动删除按钮 */
+.delete-action.data-v-29af7fd7 {
+  flex-shrink: 0;
+  width: 120rpx;
+  height: 100%;
+  background: #ffffff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.delete-icon-wrapper.data-v-29af7fd7 {
+  width: 64rpx;
+  height: 64rpx;
+  background: #FF3B30;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 4rpx 12rpx rgba(255, 59, 48, 0.3);
+}
+.delete-icon.data-v-29af7fd7 {
+  font-size: 32rpx;
+  color: #ffffff;
+  font-weight: bold;
+}
+
+/* 左侧股票信息 */
+.stock-left.data-v-29af7fd7 {
+  flex-shrink: 0;
+  width: 160rpx;
+  display: flex;
+  flex-direction: column;
+}
+.stock-name-row.data-v-29af7fd7 {
+  display: flex;
+  align-items: center;
+  margin-bottom: 8rpx;
+}
+.stock-name.data-v-29af7fd7 {
+  font-size: 26rpx;
+  font-weight: 600;
+  color: #222222;
+  margin-right: 8rpx;
+}
+.stock-tag.data-v-29af7fd7 {
+  font-size: 18rpx;
+  padding: 2rpx 6rpx;
+  border-radius: 4rpx;
+  color: #ffffff;
+  font-weight: 500;
+}
+.market-sh.data-v-29af7fd7 {
+  background: #FF3B30;
+}
+.market-sz.data-v-29af7fd7 {
+  background: #34C759;
+}
+.market-cy.data-v-29af7fd7 {
+  background: #FF9500;
+}
+.stock-code.data-v-29af7fd7 {
+  font-size: 22rpx;
+  color: #9ca2b5;
+}
+
+/* 中间趋势图 */
+.stock-chart.data-v-29af7fd7 {
+  flex: 1;
+  height: 60rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin: 0 16rpx;
+}
+.trend-canvas.data-v-29af7fd7 {
+  width: 200rpx;
+  height: 60rpx;
+}
+
+/* 右侧涨跌幅和价格 */
+.stock-right.data-v-29af7fd7 {
+  flex-shrink: 0;
+  width: 120rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+}
+.change-percent.data-v-29af7fd7 {
+  font-size: 26rpx;
+  font-weight: 700;
+  padding: 4rpx 12rpx;
+  border-radius: 6rpx;
+  margin-bottom: 8rpx;
+}
+.change-up.data-v-29af7fd7 {
+  background: #FF3B30;
+  color: #ffffff;
+}
+.change-down.data-v-29af7fd7 {
+  background: #34C759;
+  color: #ffffff;
+}
+.stock-price.data-v-29af7fd7 {
+  font-size: 22rpx;
+  color: #666a7f;
+}

+ 179 - 76
dist/dev/mp-weixin/pages/rank/rank.js

@@ -2,12 +2,87 @@
 const common_vendor = require("../../common/vendor.js");
 const utils_auth = require("../../utils/auth.js");
 const utils_api = require("../../utils/api.js");
+if (!Math) {
+  StockListItem();
+}
+const StockListItem = () => "../../components/StockListItem.js";
 const _sfc_main = {
   __name: "rank",
   setup(__props) {
     const isLoggedIn = common_vendor.ref(false);
     const myStocks = common_vendor.ref([]);
+    const viewMode = common_vendor.ref("list");
+    const toggleViewMode = () => {
+      viewMode.value = viewMode.value === "list" ? "table" : "list";
+    };
+    const indexData = common_vendor.ref({
+      stockCode: "000001",
+      stockName: "上证指数",
+      currentPrice: null,
+      priceChange: null,
+      changePercent: null
+    });
     let refreshTimer = null;
+    const fetchIndexData = async () => {
+      try {
+        const res = await utils_api.getIndexQuote("000001");
+        if (res.code === 200 && res.data) {
+          indexData.value = { ...indexData.value, ...res.data };
+        }
+      } catch (e) {
+        console.error("[上证指数] 获取失败:", e.message);
+      }
+    };
+    const formatIndexPrice = (price) => {
+      if (!price)
+        return "--";
+      return parseFloat(price).toFixed(2);
+    };
+    const formatPrice = (price) => {
+      if (!price)
+        return "--";
+      return parseFloat(price).toFixed(2);
+    };
+    const getIndexChangeClass = (changePercent) => {
+      if (!changePercent)
+        return "";
+      const str = String(changePercent).replace("%", "").replace("+", "");
+      const value = parseFloat(str);
+      if (value > 0)
+        return "index-up";
+      if (value < 0)
+        return "index-down";
+      return "";
+    };
+    const getProfitClass = (profitPercent) => {
+      if (!profitPercent)
+        return "";
+      const str = String(profitPercent).replace("%", "").replace("+", "");
+      const value = parseFloat(str);
+      if (value > 0)
+        return "profit-up";
+      if (value < 0)
+        return "profit-down";
+      return "";
+    };
+    const getMarketTag = (code) => {
+      if (code.startsWith("6"))
+        return "沪";
+      if (code.startsWith("0"))
+        return "深";
+      if (code.startsWith("3"))
+        return "创";
+      return "沪";
+    };
+    const getMarketClass = (code) => {
+      if (code.startsWith("6"))
+        return "market-sh";
+      if (code.startsWith("0"))
+        return "market-sz";
+      if (code.startsWith("3"))
+        return "market-cy";
+      return "market-sh";
+    };
     const loadMyStocks = async () => {
       if (!isLoggedIn.value) {
         myStocks.value = [];
@@ -15,56 +90,79 @@ const _sfc_main = {
         return;
       }
       try {
-        const stocks = common_vendor.index.getStorageSync("my_stocks") || [];
-        myStocks.value = stocks;
-        console.log("[我的股票] 加载股票列表:", stocks.length, "只");
-        if (stocks.length === 0) {
-          stopAutoRefresh();
-          return;
+        const res = await utils_api.getUserStocks();
+        console.log("[我的股票] 服务器返回:", JSON.stringify(res));
+        if (res.code === 200 && res.data) {
+          myStocks.value = res.data.map((item) => ({
+            code: item.stockCode,
+            name: item.stockName,
+            addPrice: item.addPrice,
+            addDate: item.addDate,
+            currentPrice: item.currentPrice,
+            profitPercent: item.profitPercent,
+            priceChange: item.priceChange,
+            changePercent: item.changePercent,
+            trendData: item.trendData
+          }));
+        } else {
+          const localStocks = common_vendor.index.getStorageSync("my_stocks") || [];
+          myStocks.value = localStocks;
         }
-        await refreshAllQuotes();
+        await fetchIndexData();
+        if (myStocks.value.length > 0) {
+          await refreshAllQuotes();
+        }
+        startAutoRefresh();
       } catch (e) {
         console.error("加载股票列表失败:", e);
-        myStocks.value = [];
-        stopAutoRefresh();
+        const localStocks = common_vendor.index.getStorageSync("my_stocks") || [];
+        myStocks.value = localStocks;
+        startAutoRefresh();
       }
     };
     const refreshAllQuotes = async () => {
-      if (myStocks.value.length === 0) {
-        console.log("[我的股票] 股票列表为空,停止定时刷新");
-        stopAutoRefresh();
+      if (myStocks.value.length === 0)
         return;
-      }
       try {
         const codes = myStocks.value.map((stock) => stock.code).join(",");
-        console.log("[我的股票] 刷新行情:", codes);
         const quoteRes = await utils_api.getStockQuotes(codes);
-        console.log("[我的股票] API返回:", JSON.stringify(quoteRes));
         if (quoteRes.code === 200 && quoteRes.data && quoteRes.data.length > 0) {
           quoteRes.data.forEach((quoteData) => {
             const index = myStocks.value.findIndex((stock) => stock.code === quoteData.stockCode);
             if (index !== -1) {
-              myStocks.value[index].priceChange = quoteData.priceChange;
-              myStocks.value[index].changePercent = quoteData.changePercent;
-              myStocks.value[index].stockName = quoteData.stockName || myStocks.value[index].name;
+              const stock = myStocks.value[index];
+              stock.priceChange = quoteData.priceChange;
+              stock.changePercent = quoteData.changePercent;
+              stock.currentPrice = quoteData.currentPrice;
+              stock.name = quoteData.stockName || stock.name;
+              stock.trendData = quoteData.trendData || null;
+              if (stock.addPrice && quoteData.currentPrice) {
+                const addPrice = parseFloat(stock.addPrice);
+                const currentPrice = parseFloat(quoteData.currentPrice);
+                if (addPrice > 0) {
+                  const profit = ((currentPrice - addPrice) / addPrice * 100).toFixed(2);
+                  stock.profitPercent = profit >= 0 ? `+${profit}%` : `${profit}%`;
+                }
+              }
             }
           });
           common_vendor.index.setStorageSync("my_stocks", myStocks.value);
-          console.log("[我的股票] 刷新成功");
-        } else {
-          console.error("[我的股票] 刷新失败:", quoteRes);
         }
       } catch (e) {
         console.error("[我的股票] 刷新异常:", e.message);
       }
     };
     const startAutoRefresh = () => {
+      if (!isLoggedIn.value)
+        return;
       stopAutoRefresh();
       const scheduleNextRefresh = () => {
-        const delay = 2e3 + Math.random() * 1e3;
-        console.log(`[我的股票] 下次刷新将在 ${Math.round(delay)}ms 后执行`);
+        const delay = 3e3 + Math.random() * 1e3;
         refreshTimer = setTimeout(async () => {
-          await refreshAllQuotes();
+          await fetchIndexData();
+          if (myStocks.value.length > 0) {
+            await refreshAllQuotes();
+          }
           scheduleNextRefresh();
         }, delay);
       };
@@ -74,28 +172,32 @@ const _sfc_main = {
       if (refreshTimer) {
         clearTimeout(refreshTimer);
         refreshTimer = null;
-        console.log("[我的股票] 停止自动刷新");
       }
     };
     const goToLogin = () => {
-      common_vendor.index.navigateTo({
-        url: "/pages/login/login"
-      });
+      common_vendor.index.navigateTo({ url: "/pages/login/login" });
+    };
+    const handleStockClick = (stockItem, idx) => {
+      console.log("点击股票:", stockItem.name, idx);
+      common_vendor.index.showToast({ title: "股票详情功能开发中", icon: "none" });
     };
-    const removeStock = (index) => {
+    const removeStock = async (idx) => {
+      const stock = myStocks.value[idx];
       common_vendor.index.showModal({
         title: "确认删除",
-        content: `确定要删除 ${myStocks.value[index].name} 吗?`,
+        content: `确定要删除 ${stock.name} 吗?`,
         confirmText: "删除",
         cancelText: "取消",
-        success: (res) => {
+        success: async (res) => {
           if (res.confirm) {
-            myStocks.value.splice(index, 1);
+            try {
+              await utils_api.deleteUserStock(stock.code);
+            } catch (e) {
+              console.error("删除失败:", e);
+            }
+            myStocks.value.splice(idx, 1);
             common_vendor.index.setStorageSync("my_stocks", myStocks.value);
-            common_vendor.index.showToast({
-              title: "删除成功",
-              icon: "success"
-            });
+            common_vendor.index.showToast({ title: "删除成功", icon: "success" });
             if (myStocks.value.length === 0) {
               stopAutoRefresh();
             }
@@ -103,40 +205,14 @@ const _sfc_main = {
         }
       });
     };
-    const getChangeClass = (value) => {
-      if (!value || value === "--")
-        return "";
-      if (value.startsWith("+") || value.match(/^[\d.]/) && !value.startsWith("-")) {
-        return "change-up";
-      }
-      if (value.startsWith("-")) {
-        return "change-down";
-      }
-      return "";
-    };
-    const formatDate = (timestamp) => {
-      const date = new Date(timestamp);
-      const year = date.getFullYear();
-      const month = String(date.getMonth() + 1).padStart(2, "0");
-      const day = String(date.getDate()).padStart(2, "0");
-      const hours = String(date.getHours()).padStart(2, "0");
-      const minutes = String(date.getMinutes()).padStart(2, "0");
-      return `${year}-${month}-${day} ${hours}:${minutes}`;
-    };
     common_vendor.onLoad(() => {
       isLoggedIn.value = utils_auth.isLoggedIn();
-      console.log("[我的股票] 登录状态:", isLoggedIn.value);
       loadMyStocks();
     });
     common_vendor.onShow(() => {
       isLoggedIn.value = utils_auth.isLoggedIn();
       loadMyStocks();
       common_vendor.index.setNavigationBarTitle({ title: "量化交易大师" });
-      if (isLoggedIn.value && myStocks.value.length > 0) {
-        startAutoRefresh();
-      } else {
-        stopAutoRefresh();
-      }
     });
     common_vendor.onHide(() => {
       stopAutoRefresh();
@@ -146,26 +222,53 @@ const _sfc_main = {
     });
     return (_ctx, _cache) => {
       return common_vendor.e({
-        a: myStocks.value.length > 0
-      }, myStocks.value.length > 0 ? {
-        b: common_vendor.f(myStocks.value, (stock, index, i0) => {
+        a: common_vendor.t(formatIndexPrice(indexData.value.currentPrice)),
+        b: common_vendor.n(getIndexChangeClass(indexData.value.changePercent)),
+        c: common_vendor.t(indexData.value.priceChange || "--"),
+        d: common_vendor.n(getIndexChangeClass(indexData.value.changePercent)),
+        e: common_vendor.t(indexData.value.stockName || "上证指数"),
+        f: common_vendor.t(indexData.value.changePercent || "--"),
+        g: common_vendor.n(getIndexChangeClass(indexData.value.changePercent)),
+        h: common_vendor.t(viewMode.value === "list" ? "📊" : "📋"),
+        i: common_vendor.o(toggleViewMode),
+        j: viewMode.value === "list" && myStocks.value.length > 0
+      }, viewMode.value === "list" && myStocks.value.length > 0 ? {
+        k: common_vendor.f(myStocks.value, (stock, index, i0) => {
+          return {
+            a: stock.code,
+            b: common_vendor.o(($event) => removeStock(index), stock.code),
+            c: common_vendor.o(($event) => handleStockClick(stock, index), stock.code),
+            d: "2482292d-0-" + i0,
+            e: common_vendor.p({
+              stock,
+              ["show-delete"]: true
+            })
+          };
+        })
+      } : {}, {
+        l: viewMode.value === "table" && myStocks.value.length > 0
+      }, viewMode.value === "table" && myStocks.value.length > 0 ? {
+        m: common_vendor.f(myStocks.value, (stock, index, i0) => {
           return {
             a: common_vendor.t(stock.name),
-            b: common_vendor.t(stock.code),
-            c: common_vendor.o(($event) => removeStock(index), index),
-            d: common_vendor.t(stock.priceChange || "--"),
-            e: common_vendor.n(getChangeClass(stock.priceChange)),
-            f: common_vendor.t(stock.changePercent || "--"),
-            g: common_vendor.n(getChangeClass(stock.changePercent)),
-            h: common_vendor.t(formatDate(stock.addTime)),
-            i: index
+            b: common_vendor.t(getMarketTag(stock.code)),
+            c: common_vendor.n(getMarketClass(stock.code)),
+            d: common_vendor.t(stock.code),
+            e: common_vendor.t(stock.addDate || "--"),
+            f: common_vendor.t(formatPrice(stock.addPrice)),
+            g: common_vendor.t(stock.profitPercent || "--"),
+            h: common_vendor.n(getProfitClass(stock.profitPercent)),
+            i: stock.code,
+            j: common_vendor.o(($event) => handleStockClick(stock, index), stock.code)
           };
         })
       } : {}, {
-        c: !isLoggedIn.value ? 1 : "",
-        d: !isLoggedIn.value
+        n: myStocks.value.length === 0
+      }, myStocks.value.length === 0 ? {} : {}, {
+        o: !isLoggedIn.value ? 1 : "",
+        p: !isLoggedIn.value
       }, !isLoggedIn.value ? {
-        e: common_vendor.o(goToLogin)
+        q: common_vendor.o(goToLogin)
       } : {});
     };
   }

+ 3 - 1
dist/dev/mp-weixin/pages/rank/rank.json

@@ -1,4 +1,6 @@
 {
   "navigationBarTitleText": "量化交易大师",
-  "usingComponents": {}
+  "usingComponents": {
+    "stock-list-item": "../../components/StockListItem"
+  }
 }

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 1
dist/dev/mp-weixin/pages/rank/rank.wxml


+ 145 - 68
dist/dev/mp-weixin/pages/rank/rank.wxss

@@ -14,112 +14,189 @@
   min-height: 100%;
 }
 
-/* 股票列表 */
-.stock-list {
+/* 上证指数卡片 */
+.index-card {
   display: flex;
-  flex-direction: column;
-  gap: 24rpx;
-}
-.stock-card {
+  align-items: center;
+  justify-content: space-between;
   background: #ffffff;
   border-radius: 24rpx;
   padding: 32rpx;
+  margin-bottom: 24rpx;
   box-shadow: 0 8rpx 24rpx rgba(37, 52, 94, 0.08);
 }
-.stock-header {
+.index-left {
   display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 24rpx;
-  padding-bottom: 24rpx;
-  border-bottom: 1rpx solid #f1f2f6;
+  flex-direction: column;
 }
-.stock-title {
+.index-price-row {
   display: flex;
-  flex-direction: column;
-  gap: 8rpx;
+  align-items: baseline;
+  margin-bottom: 8rpx;
 }
-.stock-name {
-  font-size: 34rpx;
+.index-price {
+  font-size: 48rpx;
   font-weight: 700;
-  color: #222222;
+  margin-right: 16rpx;
 }
-.stock-code {
+.index-change {
+  font-size: 28rpx;
+  font-weight: 600;
+}
+.index-name-row {
+  display: flex;
+  align-items: center;
+}
+.index-name {
   font-size: 26rpx;
-  color: #5d55e8;
-  font-weight: 500;
+  color: #666666;
+  margin-right: 12rpx;
 }
-.delete-btn {
-  width: 56rpx;
-  height: 56rpx;
-  background: #f16565;
-  border-radius: 50%;
+.index-percent {
+  font-size: 26rpx;
+  font-weight: 600;
+}
+.index-up {
+  color: #FF3B30;
+}
+.index-down {
+  color: #34C759;
+}
+
+/* 切换按钮 */
+.view-switch {
+  display: flex;
+  justify-content: flex-end;
+  margin-bottom: 16rpx;
+}
+.switch-btn {
+  width: 64rpx;
+  height: 64rpx;
+  background: #ffffff;
+  border-radius: 12rpx;
   display: flex;
   align-items: center;
   justify-content: center;
-  box-shadow: 0 4rpx 12rpx rgba(241, 101, 101, 0.3);
+  box-shadow: 0 4rpx 12rpx rgba(37, 52, 94, 0.08);
 }
-.delete-icon {
-  font-size: 40rpx;
-  font-weight: 700;
-  color: #ffffff;
-  line-height: 1;
+.switch-btn:active {
+  background: #f5f6fb;
+}
+.switch-icon {
+  font-size: 28rpx;
 }
-.stock-body {
+
+/* 股票列表 */
+.stock-list {
   display: flex;
   flex-direction: column;
-  gap: 12rpx;
+  background: #ffffff;
+  border-radius: 24rpx;
+  padding: 0 32rpx;
+  box-shadow: 0 8rpx 24rpx rgba(37, 52, 94, 0.08);
+}
+
+/* 表格视图 */
+.stock-table {
+  background: #ffffff;
+  border-radius: 24rpx;
+  padding: 0 24rpx;
+  box-shadow: 0 8rpx 24rpx rgba(37, 52, 94, 0.08);
 }
-.info-item {
+.table-header {
   display: flex;
-  justify-content: space-between;
   align-items: center;
+  padding: 24rpx 0;
+  border-bottom: 1rpx solid #f1f2f6;
 }
-.info-label {
-  font-size: 26rpx;
-  color: #666a7f;
-}
-.info-value {
-  font-size: 26rpx;
-  color: #222222;
+.table-header text {
+  font-size: 24rpx;
+  color: #999999;
   font-weight: 500;
 }
-
-/* 涨跌信息样式 */
-.quote-row {
+.th-name {
+  flex: 1;
+}
+.th-date {
+  width: 160rpx;
+  text-align: center;
+}
+.th-price {
+  width: 140rpx;
+  text-align: center;
+}
+.th-profit {
+  width: 140rpx;
+  text-align: right;
+}
+.table-row {
   display: flex;
-  justify-content: space-between;
-  gap: 24rpx;
-  margin-bottom: 16rpx;
-  padding: 20rpx;
-  background: #f7f8fc;
-  border-radius: 16rpx;
+  align-items: center;
+  padding: 24rpx 0;
+  border-bottom: 1rpx solid #f1f2f6;
+}
+.table-row:last-child {
+  border-bottom: none;
 }
-.quote-item {
+.td-name {
   flex: 1;
   display: flex;
   flex-direction: column;
+}
+.td-name .stock-name {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #222222;
+  margin-bottom: 4rpx;
+}
+.stock-code-row {
+  display: flex;
   align-items: center;
 }
-.quote-label {
+.stock-tag {
+  font-size: 18rpx;
+  padding: 2rpx 6rpx;
+  border-radius: 4rpx;
+  color: #ffffff;
+  font-weight: 500;
+  margin-right: 8rpx;
+}
+.market-sh {
+  background: #FF3B30;
+}
+.market-sz {
+  background: #34C759;
+}
+.market-cy {
+  background: #FF9500;
+}
+.stock-code {
+  font-size: 22rpx;
+  color: #9ca2b5;
+}
+.td-date {
+  width: 160rpx;
+  text-align: center;
   font-size: 24rpx;
-  color: #666a7f;
-  margin-bottom: 8rpx;
+  color: #666666;
 }
-.quote-value {
-  font-size: 32rpx;
-  font-weight: 700;
-  color: #222222;
+.td-price {
+  width: 140rpx;
+  text-align: center;
+  font-size: 26rpx;
+  color: #333333;
 }
-
-/* 涨(红色) */
-.change-up {
-  color: #f16565 !important;
+.td-profit {
+  width: 140rpx;
+  text-align: right;
+  font-size: 26rpx;
+  font-weight: 600;
 }
-
-/* 跌(绿色) */
-.change-down {
-  color: #3abf81 !important;
+.profit-up {
+  color: #FF3B30;
+}
+.profit-down {
+  color: #34C759;
 }
 
 /* 空状态 */

+ 32 - 25
dist/dev/mp-weixin/pages/strong/strong.js

@@ -116,42 +116,49 @@ const _sfc_main = {
       });
     };
     const addToMyStocks = async (stock) => {
+      if (!utils_auth.isLoggedIn()) {
+        common_vendor.index.showModal({
+          title: "登录提示",
+          content: "添加自选股票需要登录,是否前往登录?",
+          confirmText: "去登录",
+          cancelText: "取消",
+          success: (res) => {
+            if (res.confirm) {
+              common_vendor.index.navigateTo({ url: "/pages/login/login" });
+            }
+          }
+        });
+        return;
+      }
       try {
-        const myStocks = common_vendor.index.getStorageSync("my_stocks") || [];
-        const exists = myStocks.some((item) => item.code === stock.code);
-        if (exists) {
-          common_vendor.index.showToast({
-            title: "该股票已在列表中",
-            icon: "none"
-          });
-          return;
-        }
         common_vendor.index.showLoading({ title: "获取行情..." });
-        let priceChange = null;
-        let changePercent = null;
+        let currentPrice = null;
         try {
           const quoteRes = await utils_api.getStockQuotes(stock.code);
           if (quoteRes.code === 200 && quoteRes.data && quoteRes.data.length > 0) {
             const quoteData = quoteRes.data[0];
-            priceChange = quoteData.priceChange;
-            changePercent = quoteData.changePercent;
+            currentPrice = quoteData.currentPrice;
           }
         } catch (e) {
           console.error("获取行情数据失败:", e);
         }
-        common_vendor.index.hideLoading();
-        myStocks.push({
-          name: stock.name,
-          code: stock.code,
-          priceChange,
-          changePercent,
-          addTime: Date.now()
-        });
-        common_vendor.index.setStorageSync("my_stocks", myStocks);
-        common_vendor.index.showToast({
-          title: "添加成功",
-          icon: "success"
+        const addRes = await utils_api.addUserStock({
+          stockCode: stock.code,
+          stockName: stock.name,
+          currentPrice
         });
+        common_vendor.index.hideLoading();
+        if (addRes.code === 200) {
+          common_vendor.index.showToast({
+            title: "添加成功",
+            icon: "success"
+          });
+        } else {
+          common_vendor.index.showToast({
+            title: addRes.message || "添加失败",
+            icon: "none"
+          });
+        }
       } catch (e) {
         common_vendor.index.hideLoading();
         console.error("添加股票失败:", e);

+ 0 - 29
dist/dev/mp-weixin/pages/strong/strong.wxss

@@ -107,37 +107,11 @@
   font-size: 24rpx;
   color: #9ca2b5;
 }
-.stock-price-row {
-  display: flex;
-  align-items: center;
-}
-.stock-price-label {
-  font-size: 24rpx;
-  color: #9ca2b5;
-  margin-right: 8rpx;
-}
-.stock-price {
-  font-size: 26rpx;
-  font-weight: 600;
-  color: #222222;
-}
 .stock-right {
   display: flex;
   align-items: center;
   gap: 16rpx;
 }
-.stock-score-badge {
-  background: #e7f7ef;
-  border-radius: 20rpx;
-  padding: 8rpx 16rpx;
-  min-width: 80rpx;
-  text-align: center;
-}
-.stock-score {
-  font-size: 26rpx;
-  font-weight: 700;
-  color: #3abf81;
-}
 .stock-actions {
   display: flex;
   gap: 12rpx;
@@ -154,9 +128,6 @@
 .buy-btn {
   background: #3abf81;
 }
-.sell-btn {
-  background: #f16565;
-}
 .action-icon {
   font-size: 32rpx;
   font-weight: 700;

+ 33 - 0
dist/dev/mp-weixin/utils/api.js

@@ -132,9 +132,42 @@ const getStockQuotes = (codes) => {
     data: { codes }
   });
 };
+const getIndexQuote = (code) => {
+  return request({
+    url: "/api/stock/index",
+    method: "GET",
+    data: { code }
+  });
+};
+const getUserStocks = () => {
+  return request({
+    url: "/v1/user/stock/list",
+    method: "GET"
+  });
+};
+const addUserStock = (data) => {
+  return request({
+    url: "/v1/user/stock/add",
+    method: "POST",
+    header: {
+      "content-type": "application/json"
+    },
+    data
+  });
+};
+const deleteUserStock = (stockCode) => {
+  return request({
+    url: `/v1/user/stock/delete?stockCode=${encodeURIComponent(stockCode)}`,
+    method: "DELETE"
+  });
+};
+exports.addUserStock = addUserStock;
+exports.deleteUserStock = deleteUserStock;
+exports.getIndexQuote = getIndexQuote;
 exports.getStockQuotes = getStockQuotes;
 exports.getSuggestions = getSuggestions;
 exports.getUserInfoApi = getUserInfoApi;
+exports.getUserStocks = getUserStocks;
 exports.searchStocks = searchStocks;
 exports.updateUserProfile = updateUserProfile;
 exports.uploadFile = uploadFile;

+ 417 - 0
src/components/StockListItem.vue

@@ -0,0 +1,417 @@
+<template>
+  <view class="stock-item-wrapper">
+    <!-- 可滑动的内容区域 -->
+    <movable-area class="movable-area">
+      <movable-view 
+        class="movable-view"
+        direction="horizontal"
+        :x="moveX"
+        :damping="40"
+        :friction="5"
+        :out-of-bounds="false"
+        @change="handleMoveChange"
+        @touchend="handleMoveEnd"
+      >
+        <view class="stock-list-item">
+          <!-- 左侧:股票信息 -->
+          <view class="stock-left">
+            <view class="stock-name-row">
+              <text class="stock-name">{{ stock.name }}</text>
+              <text :class="['stock-tag', getMarketClass(stock.code)]">{{ getMarketTag(stock.code) }}</text>
+            </view>
+            <text class="stock-code">{{ stock.code }}</text>
+          </view>
+
+          <!-- 中间:涨跌趋势图 -->
+          <view class="stock-chart">
+            <canvas 
+              :canvas-id="canvasId" 
+              :id="canvasId"
+              class="trend-canvas"
+            ></canvas>
+          </view>
+
+          <!-- 右侧:涨跌幅和价格 -->
+          <view class="stock-right">
+            <view :class="['change-percent', getChangeClass(stock.changePercent)]">
+              {{ formatChangePercent(stock.changePercent) }}
+            </view>
+            <text class="stock-price">{{ formatPrice(stock.currentPrice) }}</text>
+          </view>
+          
+          <!-- 删除按钮(在内容右侧) -->
+          <view v-if="showDelete" class="delete-action" @click.stop="handleDelete">
+            <view class="delete-icon-wrapper">
+              <text class="delete-icon">✕</text>
+            </view>
+          </view>
+        </view>
+      </movable-view>
+    </movable-area>
+  </view>
+</template>
+
+<script setup>
+import { onMounted, nextTick, getCurrentInstance, ref, watch } from 'vue'
+
+const props = defineProps({
+  stock: {
+    type: Object,
+    required: true
+  },
+  showDelete: {
+    type: Boolean,
+    default: false
+  }
+})
+
+const emit = defineEmits(['delete'])
+
+// 保存组件实例引用
+let componentInstance = null
+
+// 滑动删除相关
+const deleteWidth = 60 // 删除按钮宽度(px)
+const moveX = ref(0)
+let currentX = 0
+
+// 生成稳定的 canvas ID(只在组件创建时生成一次)
+const canvasId = ref(`chart-${props.stock.code}-${Math.random().toString(36).slice(2, 11)}`)
+
+// 判断市场类型(沪市/深市/创业板)
+const getMarketTag = (code) => {
+  if (code.startsWith('6')) return '沪'
+  if (code.startsWith('0')) return '深'
+  if (code.startsWith('3')) return '创'
+  return '沪'
+}
+
+const getMarketClass = (code) => {
+  if (code.startsWith('6')) return 'market-sh'
+  if (code.startsWith('0')) return 'market-sz'
+  if (code.startsWith('3')) return 'market-cy'
+  return 'market-sh'
+}
+
+// 获取涨跌样式类
+const getChangeClass = (changePercent) => {
+  if (!changePercent) return ''
+  const str = String(changePercent).replace('%', '').replace('+', '')
+  const value = parseFloat(str)
+  if (value > 0) return 'change-up'
+  if (value < 0) return 'change-down'
+  return ''
+}
+
+// 格式化涨跌幅
+const formatChangePercent = (changePercent) => {
+  if (!changePercent) return '--'
+  return String(changePercent)
+}
+
+// 格式化价格
+const formatPrice = (price) => {
+  if (!price) return '--'
+  return parseFloat(price).toFixed(2)
+}
+
+// 绘制趋势图
+const drawTrendChart = (instance) => {
+  console.log('[趋势图] 开始绘制:', props.stock.code, canvasId.value)
+  
+  // 获取趋势数据
+  let trendData = props.stock.trendData
+  
+  // 如果没有趋势数据,生成模拟数据
+  if (!trendData || !Array.isArray(trendData) || trendData.length === 0) {
+    console.log('[趋势图] 使用模拟数据')
+    trendData = generateMockTrendData()
+  } else {
+    console.log('[趋势图] 使用真实数据,数据点数:', trendData.length)
+  }
+  
+  // 使用 uni.createCanvasContext 创建画布上下文
+  // 在 setup 中需要传入组件实例
+  const ctx = uni.createCanvasContext(canvasId.value, instance)
+  
+  // 画布尺寸(rpx)
+  const width = 100  // 实际像素
+  const height = 30  // 实际像素
+  const padding = 2
+  
+  // 计算数据范围
+  const maxValue = Math.max(...trendData)
+  const minValue = Math.min(...trendData)
+  const range = maxValue - minValue || 1
+  
+  console.log('[趋势图] 数据范围:', { min: minValue, max: maxValue, range })
+  
+  // 判断涨跌颜色
+  const changePercent = parseFloat(String(props.stock.changePercent || '0').replace('%', '').replace('+', ''))
+  const isUp = changePercent >= 0
+  const lineColor = isUp ? '#FF3B30' : '#34C759'  // 红涨绿跌
+  const fillColor = isUp ? 'rgba(255, 59, 48, 0.1)' : 'rgba(52, 199, 89, 0.1)'
+  
+  console.log('[趋势图] 颜色:', { isUp, lineColor, changePercent })
+  
+  // 绘制折线
+  ctx.beginPath()
+  
+  trendData.forEach((value, index) => {
+    const x = padding + (index / (trendData.length - 1)) * (width - padding * 2)
+    const y = height - padding - ((value - minValue) / range) * (height - padding * 2)
+    
+    if (index === 0) {
+      ctx.moveTo(x, y)
+    } else {
+      ctx.lineTo(x, y)
+    }
+  })
+  
+  // 绘制折线
+  ctx.setStrokeStyle(lineColor)
+  ctx.setLineWidth(1)
+  ctx.stroke()
+  
+  // 绘制填充区域
+  ctx.lineTo(width - padding, height - padding)
+  ctx.lineTo(padding, height - padding)
+  ctx.closePath()
+  ctx.setFillStyle(fillColor)
+  ctx.fill()
+  
+  // 绘制到画布
+  ctx.draw()
+  
+  console.log('[趋势图] 绘制完成')
+}
+
+// 生成模拟趋势数据
+const generateMockTrendData = () => {
+  const changePercent = parseFloat(String(props.stock.changePercent || '0').replace('%', '').replace('+', ''))
+  const points = 30  // 30个数据点
+  const data = []
+  
+  // 基于涨跌幅生成趋势数据
+  let baseValue = 100
+  const trend = changePercent / 100  // 总体趋势
+  
+  for (let i = 0; i < points; i++) {
+    // 添加随机波动
+    const randomChange = (Math.random() - 0.5) * 2
+    const trendChange = (i / points) * trend * 100
+    baseValue = baseValue + randomChange + trendChange / points
+    data.push(baseValue)
+  }
+  
+  return data
+}
+
+// 滑动变化
+const handleMoveChange = (e) => {
+  currentX = e.detail.x
+}
+
+// 滑动结束
+const handleMoveEnd = () => {
+  if (!props.showDelete) return
+  
+  // 滑动超过三分之一就显示删除按钮
+  if (currentX < -deleteWidth / 3) {
+    moveX.value = -deleteWidth
+  } else {
+    moveX.value = 0
+  }
+}
+
+// 处理删除
+const handleDelete = () => {
+  moveX.value = 0
+  emit('delete')
+}
+
+// 组件挂载后绘制图表
+onMounted(() => {
+  console.log('[趋势图] 组件挂载:', props.stock.code)
+  componentInstance = getCurrentInstance()
+  nextTick(() => {
+    // 延迟绘制,确保 canvas 已渲染
+    setTimeout(() => {
+      drawTrendChart(componentInstance)
+    }, 300)
+  })
+})
+
+// 监听股票数据变化,重新绘制趋势图
+watch(() => props.stock.trendData, (newData) => {
+  if (newData && componentInstance) {
+    console.log('[趋势图] 数据更新,重新绘制:', props.stock.code)
+    nextTick(() => {
+      drawTrendChart(componentInstance)
+    })
+  }
+}, { deep: true })
+
+// 监听涨跌幅变化,更新颜色
+watch(() => props.stock.changePercent, () => {
+  if (componentInstance) {
+    nextTick(() => {
+      drawTrendChart(componentInstance)
+    })
+  }
+})
+</script>
+
+<style scoped>
+.stock-item-wrapper {
+  position: relative;
+  height: 120rpx;
+  overflow: hidden;
+  background: #ffffff;
+}
+
+.movable-area {
+  width: 100%;
+  height: 100%;
+}
+
+.movable-view {
+  width: calc(100% + 120rpx);
+  height: 100%;
+}
+
+.stock-list-item {
+  display: flex;
+  align-items: center;
+  padding: 24rpx 0;
+  height: 100%;
+  box-sizing: border-box;
+  background: #ffffff;
+  border-bottom: 1rpx solid #f1f2f6;
+}
+
+.stock-item-wrapper:last-child .stock-list-item {
+  border-bottom: none;
+}
+
+/* 滑动删除按钮 */
+.delete-action {
+  flex-shrink: 0;
+  width: 120rpx;
+  height: 100%;
+  background: #ffffff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.delete-icon-wrapper {
+  width: 64rpx;
+  height: 64rpx;
+  background: #FF3B30;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 4rpx 12rpx rgba(255, 59, 48, 0.3);
+}
+
+.delete-icon {
+  font-size: 32rpx;
+  color: #ffffff;
+  font-weight: bold;
+}
+
+/* 左侧股票信息 */
+.stock-left {
+  flex-shrink: 0;
+  width: 160rpx;
+  display: flex;
+  flex-direction: column;
+}
+
+.stock-name-row {
+  display: flex;
+  align-items: center;
+  margin-bottom: 8rpx;
+}
+
+.stock-name {
+  font-size: 26rpx;
+  font-weight: 600;
+  color: #222222;
+  margin-right: 8rpx;
+}
+
+.stock-tag {
+  font-size: 18rpx;
+  padding: 2rpx 6rpx;
+  border-radius: 4rpx;
+  color: #ffffff;
+  font-weight: 500;
+}
+
+.market-sh {
+  background: #FF3B30;
+}
+
+.market-sz {
+  background: #34C759;
+}
+
+.market-cy {
+  background: #FF9500;
+}
+
+.stock-code {
+  font-size: 22rpx;
+  color: #9ca2b5;
+}
+
+/* 中间趋势图 */
+.stock-chart {
+  flex: 1;
+  height: 60rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin: 0 16rpx;
+}
+
+.trend-canvas {
+  width: 200rpx;
+  height: 60rpx;
+}
+
+/* 右侧涨跌幅和价格 */
+.stock-right {
+  flex-shrink: 0;
+  width: 120rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+}
+
+.change-percent {
+  font-size: 26rpx;
+  font-weight: 700;
+  padding: 4rpx 12rpx;
+  border-radius: 6rpx;
+  margin-bottom: 8rpx;
+}
+
+.change-up {
+  background: #FF3B30;
+  color: #ffffff;
+}
+
+.change-down {
+  background: #34C759;
+  color: #ffffff;
+}
+
+.stock-price {
+  font-size: 22rpx;
+  color: #666a7f;
+}
+</style>

+ 383 - 195
src/pages/rank/rank.vue

@@ -2,52 +2,78 @@
   <view class="page-rank">
     <scroll-view class="scroll-view" scroll-y>
       <view class="content-wrapper" :class="{ 'blur-content': !isLoggedIn }">
-        <!-- 股票列表 -->
-        <view v-if="myStocks.length > 0" class="stock-list">
+        <!-- 上证指数卡片 -->
+        <view class="index-card">
+          <view class="index-left">
+            <view class="index-price-row">
+              <text :class="['index-price', getIndexChangeClass(indexData.changePercent)]">
+                {{ formatIndexPrice(indexData.currentPrice) }}
+              </text>
+              <text :class="['index-change', getIndexChangeClass(indexData.changePercent)]">
+                {{ indexData.priceChange || '--' }}
+              </text>
+            </view>
+            <view class="index-name-row">
+              <text class="index-name">{{ indexData.stockName || '上证指数' }}</text>
+              <text :class="['index-percent', getIndexChangeClass(indexData.changePercent)]">
+                {{ indexData.changePercent || '--' }}
+              </text>
+            </view>
+          </view>
+        </view>
+
+        <!-- 切换按钮 -->
+        <view class="view-switch">
+          <view class="switch-btn" @click="toggleViewMode">
+            <text class="switch-icon">{{ viewMode === 'list' ? '📊' : '📋' }}</text>
+          </view>
+        </view>
+
+        <!-- 列表视图 -->
+        <view v-if="viewMode === 'list' && myStocks.length > 0" class="stock-list">
+          <stock-list-item 
+            v-for="(stock, index) in myStocks" 
+            :key="stock.code"
+            :stock="stock"
+            :show-delete="true"
+            @delete="removeStock(index)"
+            @click="handleStockClick(stock, index)"
+          />
+        </view>
+
+        <!-- 表格视图 -->
+        <view v-if="viewMode === 'table' && myStocks.length > 0" class="stock-table">
+          <!-- 表头 -->
+          <view class="table-header">
+            <text class="th-name">股票</text>
+            <text class="th-date">自选日</text>
+            <text class="th-price">自选价</text>
+            <text class="th-profit">自选收益</text>
+          </view>
+          <!-- 表格内容 -->
           <view 
             v-for="(stock, index) in myStocks" 
-            :key="index"
-            class="stock-card"
+            :key="stock.code"
+            class="table-row"
+            @click="handleStockClick(stock, index)"
           >
-            <view class="stock-header">
-              <view class="stock-title">
-                <text class="stock-name">{{ stock.name }}</text>
+            <view class="td-name">
+              <text class="stock-name">{{ stock.name }}</text>
+              <view class="stock-code-row">
+                <text :class="['stock-tag', getMarketClass(stock.code)]">{{ getMarketTag(stock.code) }}</text>
                 <text class="stock-code">{{ stock.code }}</text>
               </view>
-              <view class="delete-btn" @click="removeStock(index)">
-                <text class="delete-icon">×</text>
-              </view>
-            </view>
-            
-            <view class="stock-body">
-              <!-- 涨跌信息 -->
-              <view class="quote-row">
-                <view class="quote-item">
-                  <text class="quote-label">涨跌额</text>
-                  <text 
-                    class="quote-value" 
-                    :class="getChangeClass(stock.priceChange)"
-                  >{{ stock.priceChange || '--' }}</text>
-                </view>
-                <view class="quote-item">
-                  <text class="quote-label">涨跌幅</text>
-                  <text 
-                    class="quote-value" 
-                    :class="getChangeClass(stock.changePercent)"
-                  >{{ stock.changePercent || '--' }}</text>
-                </view>
-              </view>
-              
-              <view class="info-item">
-                <text class="info-label">添加时间</text>
-                <text class="info-value">{{ formatDate(stock.addTime) }}</text>
-              </view>
             </view>
+            <text class="td-date">{{ stock.addDate || '--' }}</text>
+            <text class="td-price">{{ formatPrice(stock.addPrice) }}</text>
+            <text :class="['td-profit', getProfitClass(stock.profitPercent)]">
+              {{ stock.profitPercent || '--' }}
+            </text>
           </view>
         </view>
 
         <!-- 空状态 -->
-        <view v-else class="empty-content">
+        <view v-if="myStocks.length === 0" class="empty-content">
           <view class="empty-icon">📊</view>
           <text class="empty-text">暂无收藏股票</text>
           <text class="empty-desc">在强势池中点击"+"按钮添加股票</text>
@@ -64,12 +90,7 @@
         <view class="lock-icon">🔒</view>
         <text class="prompt-title">登录后查看我的股票</text>
         <text class="prompt-desc">使用微信授权快速登录</text>
-        
-        <!-- 跳转到登录页按钮 -->
-        <button 
-          class="login-button-native" 
-          @click="goToLogin"
-        >
+        <button class="login-button-native" @click="goToLogin">
           <text>登录</text>
         </button>
       </view>
@@ -81,70 +102,170 @@
 import { ref } from 'vue'
 import { onLoad, onShow, onHide, onUnload } from '@dcloudio/uni-app'
 import { isLoggedIn as checkLoginStatus } from '../../utils/auth.js'
-import { getStockQuotes } from '../../utils/api.js'
+import { getStockQuotes, getIndexQuote, getUserStocks, deleteUserStock } from '../../utils/api.js'
+import StockListItem from '../../components/StockListItem.vue'
 
 const isLoggedIn = ref(false)
 const myStocks = ref([])
-let refreshTimer = null // 定时刷新定时器
+const viewMode = ref('list') // 'list' 或 'table'
+
+// 切换视图模式
+const toggleViewMode = () => {
+  viewMode.value = viewMode.value === 'list' ? 'table' : 'list'
+}
+
+const indexData = ref({
+  stockCode: '000001',
+  stockName: '上证指数',
+  currentPrice: null,
+  priceChange: null,
+  changePercent: null
+})
+let refreshTimer = null
+
+// 获取上证指数数据
+const fetchIndexData = async () => {
+  try {
+    const res = await getIndexQuote('000001')
+    if (res.code === 200 && res.data) {
+      indexData.value = { ...indexData.value, ...res.data }
+    }
+  } catch (e) {
+    console.error('[上证指数] 获取失败:', e.message)
+  }
+}
+
+// 格式化指数价格
+const formatIndexPrice = (price) => {
+  if (!price) return '--'
+  return parseFloat(price).toFixed(2)
+}
 
-// 加载我的股票列表
+// 格式化价格
+const formatPrice = (price) => {
+  if (!price) return '--'
+  return parseFloat(price).toFixed(2)
+}
+
+// 获取指数涨跌样式类
+const getIndexChangeClass = (changePercent) => {
+  if (!changePercent) return ''
+  const str = String(changePercent).replace('%', '').replace('+', '')
+  const value = parseFloat(str)
+  if (value > 0) return 'index-up'
+  if (value < 0) return 'index-down'
+  return ''
+}
+
+// 获取收益样式类
+const getProfitClass = (profitPercent) => {
+  if (!profitPercent) return ''
+  const str = String(profitPercent).replace('%', '').replace('+', '')
+  const value = parseFloat(str)
+  if (value > 0) return 'profit-up'
+  if (value < 0) return 'profit-down'
+  return ''
+}
+
+// 获取市场标签
+const getMarketTag = (code) => {
+  if (code.startsWith('6')) return '沪'
+  if (code.startsWith('0')) return '深'
+  if (code.startsWith('3')) return '创'
+  return '沪'
+}
+
+const getMarketClass = (code) => {
+  if (code.startsWith('6')) return 'market-sh'
+  if (code.startsWith('0')) return 'market-sz'
+  if (code.startsWith('3')) return 'market-cy'
+  return 'market-sh'
+}
+
+// 加载我的股票列表(从服务器)
 const loadMyStocks = async () => {
-  // 只有登录后才加载股票列表
   if (!isLoggedIn.value) {
     myStocks.value = []
-    stopAutoRefresh() // 未登录时停止刷新
+    stopAutoRefresh()
     return
   }
   
   try {
-    const stocks = uni.getStorageSync('my_stocks') || []
-    myStocks.value = stocks
-    console.log('[我的股票] 加载股票列表:', stocks.length, '只')
+    // 从服务器获取用户自选股票
+    const res = await getUserStocks()
+    console.log('[我的股票] 服务器返回:', JSON.stringify(res))
+    
+    if (res.code === 200 && res.data) {
+      // 转换数据格式
+      myStocks.value = res.data.map(item => ({
+        code: item.stockCode,
+        name: item.stockName,
+        addPrice: item.addPrice,
+        addDate: item.addDate,
+        currentPrice: item.currentPrice,
+        profitPercent: item.profitPercent,
+        priceChange: item.priceChange,
+        changePercent: item.changePercent,
+        trendData: item.trendData
+      }))
+    } else {
+      // 如果服务器没有数据,尝试从本地存储加载
+      const localStocks = uni.getStorageSync('my_stocks') || []
+      myStocks.value = localStocks
+    }
+    
+    // 获取上证指数
+    await fetchIndexData()
     
-    // 如果没有股票数据,停止定时刷新
-    if (stocks.length === 0) {
-      stopAutoRefresh()
-      return
+    // 如果有股票数据,刷新行情
+    if (myStocks.value.length > 0) {
+      await refreshAllQuotes()
     }
     
-    // 自动刷新所有股票的实时行情
-    await refreshAllQuotes()
+    // 登录后启动定时刷新
+    startAutoRefresh()
   } catch (e) {
     console.error('加载股票列表失败:', e)
-    myStocks.value = []
-    stopAutoRefresh() // 加载失败时停止刷新
+    // 失败时从本地存储加载
+    const localStocks = uni.getStorageSync('my_stocks') || []
+    myStocks.value = localStocks
+    startAutoRefresh()
   }
 }
 
 // 批量刷新所有股票行情
 const refreshAllQuotes = async () => {
-  if (myStocks.value.length === 0) {
-    console.log('[我的股票] 股票列表为空,停止定时刷新')
-    stopAutoRefresh()
-    return
-  }
+  if (myStocks.value.length === 0) return
   
   try {
     const codes = myStocks.value.map(stock => stock.code).join(',')
-    console.log('[我的股票] 刷新行情:', codes)
-    
     const quoteRes = await getStockQuotes(codes)
-    console.log('[我的股票] API返回:', JSON.stringify(quoteRes))
     
     if (quoteRes.code === 200 && quoteRes.data && quoteRes.data.length > 0) {
       quoteRes.data.forEach(quoteData => {
         const index = myStocks.value.findIndex(stock => stock.code === quoteData.stockCode)
         if (index !== -1) {
-          myStocks.value[index].priceChange = quoteData.priceChange
-          myStocks.value[index].changePercent = quoteData.changePercent
-          myStocks.value[index].stockName = quoteData.stockName || myStocks.value[index].name
+          const stock = myStocks.value[index]
+          stock.priceChange = quoteData.priceChange
+          stock.changePercent = quoteData.changePercent
+          stock.currentPrice = quoteData.currentPrice
+          stock.name = quoteData.stockName || stock.name
+          stock.trendData = quoteData.trendData || null
+          
+          // 计算自选收益(当前价格相对于加入价格的涨跌幅)
+          if (stock.addPrice && quoteData.currentPrice) {
+            const addPrice = parseFloat(stock.addPrice)
+            const currentPrice = parseFloat(quoteData.currentPrice)
+            if (addPrice > 0) {
+              const profit = ((currentPrice - addPrice) / addPrice * 100).toFixed(2)
+              stock.profitPercent = profit >= 0 ? `+${profit}%` : `${profit}%`
+            }
+          }
         }
       })
       
+      // 同步到本地存储
       uni.setStorageSync('my_stocks', myStocks.value)
-      console.log('[我的股票] 刷新成功')
-    } else {
-      console.error('[我的股票] 刷新失败:', quoteRes)
     }
   } catch (e) {
     console.error('[我的股票] 刷新异常:', e.message)
@@ -153,17 +274,17 @@ const refreshAllQuotes = async () => {
 
 // 启动定时刷新
 const startAutoRefresh = () => {
-  // 清除旧的定时器
+  if (!isLoggedIn.value) return
   stopAutoRefresh()
   
   const scheduleNextRefresh = () => {
-    // 2秒 + 0-1秒的随机数 = 2000-3000ms
-    const delay = 2000 + Math.random() * 1000
-    console.log(`[我的股票] 下次刷新将在 ${Math.round(delay)}ms 后执行`)
-    
+    const delay = 3000 + Math.random() * 1000
     refreshTimer = setTimeout(async () => {
-      await refreshAllQuotes()
-      scheduleNextRefresh() // 递归调度下一次刷新
+      await fetchIndexData()
+      if (myStocks.value.length > 0) {
+        await refreshAllQuotes()
+      }
+      scheduleNextRefresh()
     }, delay)
   }
   
@@ -175,35 +296,42 @@ const stopAutoRefresh = () => {
   if (refreshTimer) {
     clearTimeout(refreshTimer)
     refreshTimer = null
-    console.log('[我的股票] 停止自动刷新')
   }
 }
 
 // 跳转到登录页
 const goToLogin = () => {
-  uni.navigateTo({
-    url: '/pages/login/login'
-  })
+  uni.navigateTo({ url: '/pages/login/login' })
+}
+
+// 点击股票项
+const handleStockClick = (stockItem, idx) => {
+  console.log('点击股票:', stockItem.name, idx)
+  uni.showToast({ title: '股票详情功能开发中', icon: 'none' })
 }
 
 // 删除股票
-const removeStock = (index) => {
+const removeStock = async (idx) => {
+  const stock = myStocks.value[idx]
   uni.showModal({
     title: '确认删除',
-    content: `确定要删除 ${myStocks.value[index].name} 吗?`,
+    content: `确定要删除 ${stock.name} 吗?`,
     confirmText: '删除',
     cancelText: '取消',
-    success: (res) => {
+    success: async (res) => {
       if (res.confirm) {
-        myStocks.value.splice(index, 1)
-        // 保存到本地存储
+        try {
+          // 调用服务器删除接口
+          await deleteUserStock(stock.code)
+        } catch (e) {
+          console.error('删除失败:', e)
+        }
+        
+        // 从本地列表删除
+        myStocks.value.splice(idx, 1)
         uni.setStorageSync('my_stocks', myStocks.value)
-        uni.showToast({
-          title: '删除成功',
-          icon: 'success'
-        })
+        uni.showToast({ title: '删除成功', icon: 'success' })
         
-        // 如果删除后列表为空,停止定时刷新
         if (myStocks.value.length === 0) {
           stopAutoRefresh()
         }
@@ -212,35 +340,8 @@ const removeStock = (index) => {
   })
 }
 
-// 根据涨跌值返回样式类名
-const getChangeClass = (value) => {
-  if (!value || value === '--') return ''
-  // 判断是否为正数(涨)
-  if (value.startsWith('+') || (value.match(/^[\d.]/) && !value.startsWith('-'))) {
-    return 'change-up'
-  }
-  // 判断是否为负数(跌)
-  if (value.startsWith('-')) {
-    return 'change-down'
-  }
-  return ''
-}
-
-// 格式化日期
-const formatDate = (timestamp) => {
-  const date = new Date(timestamp)
-  const year = date.getFullYear()
-  const month = String(date.getMonth() + 1).padStart(2, '0')
-  const day = String(date.getDate()).padStart(2, '0')
-  const hours = String(date.getHours()).padStart(2, '0')
-  const minutes = String(date.getMinutes()).padStart(2, '0')
-  
-  return `${year}-${month}-${day} ${hours}:${minutes}`
-}
-
 onLoad(() => {
   isLoggedIn.value = checkLoginStatus()
-  console.log('[我的股票] 登录状态:', isLoggedIn.value)
   loadMyStocks()
 })
 
@@ -248,21 +349,12 @@ onShow(() => {
   isLoggedIn.value = checkLoginStatus()
   loadMyStocks()
   uni.setNavigationBarTitle({ title: '量化交易大师' })
-  
-  // 只有登录且有股票数据时才启动定时刷新
-  if (isLoggedIn.value && myStocks.value.length > 0) {
-    startAutoRefresh()
-  } else {
-    stopAutoRefresh()
-  }
 })
 
-// 页面隐藏时停止刷新
 onHide(() => {
   stopAutoRefresh()
 })
 
-// 页面卸载时停止刷新
 onUnload(() => {
   stopAutoRefresh()
 })
@@ -286,126 +378,222 @@ onUnload(() => {
   min-height: 100%;
 }
 
-/* 股票列表 */
-.stock-list {
+/* 上证指数卡片 */
+.index-card {
   display: flex;
-  flex-direction: column;
-  gap: 24rpx;
-}
-
-.stock-card {
+  align-items: center;
+  justify-content: space-between;
   background: #ffffff;
   border-radius: 24rpx;
   padding: 32rpx;
+  margin-bottom: 24rpx;
   box-shadow: 0 8rpx 24rpx rgba(37, 52, 94, 0.08);
 }
 
-.stock-header {
+.index-left {
   display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 24rpx;
-  padding-bottom: 24rpx;
-  border-bottom: 1rpx solid #f1f2f6;
+  flex-direction: column;
 }
 
-.stock-title {
+.index-price-row {
   display: flex;
-  flex-direction: column;
-  gap: 8rpx;
+  align-items: baseline;
+  margin-bottom: 8rpx;
 }
 
-.stock-name {
-  font-size: 34rpx;
+.index-price {
+  font-size: 48rpx;
   font-weight: 700;
-  color: #222222;
+  margin-right: 16rpx;
 }
 
-.stock-code {
+.index-change {
+  font-size: 28rpx;
+  font-weight: 600;
+}
+
+.index-name-row {
+  display: flex;
+  align-items: center;
+}
+
+.index-name {
   font-size: 26rpx;
-  color: #5d55e8;
-  font-weight: 500;
+  color: #666666;
+  margin-right: 12rpx;
+}
+
+.index-percent {
+  font-size: 26rpx;
+  font-weight: 600;
 }
 
-.delete-btn {
-  width: 56rpx;
-  height: 56rpx;
-  background: #f16565;
-  border-radius: 50%;
+.index-up {
+  color: #FF3B30;
+}
+
+.index-down {
+  color: #34C759;
+}
+
+/* 切换按钮 */
+.view-switch {
+  display: flex;
+  justify-content: flex-end;
+  margin-bottom: 16rpx;
+}
+
+.switch-btn {
+  width: 64rpx;
+  height: 64rpx;
+  background: #ffffff;
+  border-radius: 12rpx;
   display: flex;
   align-items: center;
   justify-content: center;
-  box-shadow: 0 4rpx 12rpx rgba(241, 101, 101, 0.3);
+  box-shadow: 0 4rpx 12rpx rgba(37, 52, 94, 0.08);
 }
 
-.delete-icon {
-  font-size: 40rpx;
-  font-weight: 700;
-  color: #ffffff;
-  line-height: 1;
+.switch-btn:active {
+  background: #f5f6fb;
+}
+
+.switch-icon {
+  font-size: 28rpx;
 }
 
-.stock-body {
+/* 股票列表 */
+.stock-list {
   display: flex;
   flex-direction: column;
-  gap: 12rpx;
+  background: #ffffff;
+  border-radius: 24rpx;
+  padding: 0 32rpx;
+  box-shadow: 0 8rpx 24rpx rgba(37, 52, 94, 0.08);
 }
 
-.info-item {
+/* 表格视图 */
+.stock-table {
+  background: #ffffff;
+  border-radius: 24rpx;
+  padding: 0 24rpx;
+  box-shadow: 0 8rpx 24rpx rgba(37, 52, 94, 0.08);
+}
+
+.table-header {
   display: flex;
-  justify-content: space-between;
   align-items: center;
+  padding: 24rpx 0;
+  border-bottom: 1rpx solid #f1f2f6;
 }
 
-.info-label {
-  font-size: 26rpx;
-  color: #666a7f;
+.table-header text {
+  font-size: 24rpx;
+  color: #999999;
+  font-weight: 500;
 }
 
-.info-value {
-  font-size: 26rpx;
-  color: #222222;
-  font-weight: 500;
+.th-name {
+  flex: 1;
 }
 
-/* 涨跌信息样式 */
-.quote-row {
+.th-date {
+  width: 160rpx;
+  text-align: center;
+}
+
+.th-price {
+  width: 140rpx;
+  text-align: center;
+}
+
+.th-profit {
+  width: 140rpx;
+  text-align: right;
+}
+
+.table-row {
   display: flex;
-  justify-content: space-between;
-  gap: 24rpx;
-  margin-bottom: 16rpx;
-  padding: 20rpx;
-  background: #f7f8fc;
-  border-radius: 16rpx;
+  align-items: center;
+  padding: 24rpx 0;
+  border-bottom: 1rpx solid #f1f2f6;
 }
 
-.quote-item {
+.table-row:last-child {
+  border-bottom: none;
+}
+
+.td-name {
   flex: 1;
   display: flex;
   flex-direction: column;
+}
+
+.td-name .stock-name {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #222222;
+  margin-bottom: 4rpx;
+}
+
+.stock-code-row {
+  display: flex;
   align-items: center;
 }
 
-.quote-label {
+.stock-tag {
+  font-size: 18rpx;
+  padding: 2rpx 6rpx;
+  border-radius: 4rpx;
+  color: #ffffff;
+  font-weight: 500;
+  margin-right: 8rpx;
+}
+
+.market-sh {
+  background: #FF3B30;
+}
+
+.market-sz {
+  background: #34C759;
+}
+
+.market-cy {
+  background: #FF9500;
+}
+
+.stock-code {
+  font-size: 22rpx;
+  color: #9ca2b5;
+}
+
+.td-date {
+  width: 160rpx;
+  text-align: center;
   font-size: 24rpx;
-  color: #666a7f;
-  margin-bottom: 8rpx;
+  color: #666666;
 }
 
-.quote-value {
-  font-size: 32rpx;
-  font-weight: 700;
-  color: #222222;
+.td-price {
+  width: 140rpx;
+  text-align: center;
+  font-size: 26rpx;
+  color: #333333;
+}
+
+.td-profit {
+  width: 140rpx;
+  text-align: right;
+  font-size: 26rpx;
+  font-weight: 600;
 }
 
-/* 涨(红色) */
-.change-up {
-  color: #f16565 !important;
+.profit-up {
+  color: #FF3B30;
 }
 
-/* 跌(绿色) */
-.change-down {
-  color: #3abf81 !important;
+.profit-down {
+  color: #34C759;
 }
 
 /* 空状态 */

+ 36 - 69
src/pages/strong/strong.vue

@@ -148,7 +148,7 @@
 import { ref } from 'vue'
 import { onLoad, onShow } from '@dcloudio/uni-app'
 import { isLoggedIn as checkLoginStatus } from '../../utils/auth.js'
-import { getStockQuotes } from '../../utils/api.js'
+import { getStockQuotes, addUserStock } from '../../utils/api.js'
 
 // 购买状态
 const isPurchased = ref(false)
@@ -308,56 +308,58 @@ const onHistorySearch = () => {
 
 // 添加到我的股票
 const addToMyStocks = async (stock) => {
+  // 检查登录状态
+  if (!checkLoginStatus()) {
+    uni.showModal({
+      title: '登录提示',
+      content: '添加自选股票需要登录,是否前往登录?',
+      confirmText: '去登录',
+      cancelText: '取消',
+      success: (res) => {
+        if (res.confirm) {
+          uni.navigateTo({ url: '/pages/login/login' })
+        }
+      }
+    })
+    return
+  }
+  
   try {
-    // 获取现有的股票列表
-    const myStocks = uni.getStorageSync('my_stocks') || []
-    
-    // 检查是否已存在
-    const exists = myStocks.some(item => item.code === stock.code)
-    if (exists) {
-      uni.showToast({
-        title: '该股票已在列表中',
-        icon: 'none'
-      })
-      return
-    }
-    
     // 显示加载提示
     uni.showLoading({ title: '获取行情...' })
     
     // 获取股票实时行情数据
-    let priceChange = null
-    let changePercent = null
+    let currentPrice = null
     try {
       const quoteRes = await getStockQuotes(stock.code)
-      
       if (quoteRes.code === 200 && quoteRes.data && quoteRes.data.length > 0) {
         const quoteData = quoteRes.data[0]
-        priceChange = quoteData.priceChange
-        changePercent = quoteData.changePercent
+        currentPrice = quoteData.currentPrice
       }
     } catch (e) {
       console.error('获取行情数据失败:', e)
     }
     
-    uni.hideLoading()
-    
-    // 添加新股票(包含行情数据)
-    myStocks.push({
-      name: stock.name,
-      code: stock.code,
-      priceChange: priceChange,
-      changePercent: changePercent,
-      addTime: Date.now()
+    // 调用后端接口保存到数据库
+    const addRes = await addUserStock({
+      stockCode: stock.code,
+      stockName: stock.name,
+      currentPrice: currentPrice
     })
     
-    // 保存到本地存储
-    uni.setStorageSync('my_stocks', myStocks)
+    uni.hideLoading()
     
-    uni.showToast({
-      title: '添加成功',
-      icon: 'success'
-    })
+    if (addRes.code === 200) {
+      uni.showToast({
+        title: '添加成功',
+        icon: 'success'
+      })
+    } else {
+      uni.showToast({
+        title: addRes.message || '添加失败',
+        icon: 'none'
+      })
+    }
   } catch (e) {
     uni.hideLoading()
     console.error('添加股票失败:', e)
@@ -513,43 +515,12 @@ onShow(() => {
   color: #9ca2b5;
 }
 
-.stock-price-row {
-  display: flex;
-  align-items: center;
-}
-
-.stock-price-label {
-  font-size: 24rpx;
-  color: #9ca2b5;
-  margin-right: 8rpx;
-}
-
-.stock-price {
-  font-size: 26rpx;
-  font-weight: 600;
-  color: #222222;
-}
-
 .stock-right {
   display: flex;
   align-items: center;
   gap: 16rpx;
 }
 
-.stock-score-badge {
-  background: #e7f7ef;
-  border-radius: 20rpx;
-  padding: 8rpx 16rpx;
-  min-width: 80rpx;
-  text-align: center;
-}
-
-.stock-score {
-  font-size: 26rpx;
-  font-weight: 700;
-  color: #3abf81;
-}
-
 .stock-actions {
   display: flex;
   gap: 12rpx;
@@ -569,10 +540,6 @@ onShow(() => {
   background: #3abf81;
 }
 
-.sell-btn {
-  background: #f16565;
-}
-
 .action-icon {
   font-size: 32rpx;
   font-weight: 700;

+ 65 - 0
src/utils/api.js

@@ -243,3 +243,68 @@ export const getStockQuotes = (codes) => {
   })
 }
 
+/**
+ * 获取指数数据(上证指数、深证成指、创业板指)
+ * @param {string} code - 指数代码 (000001=上证指数, 399001=深证成指, 399006=创业板指)
+ * @returns {Promise} 返回指数数据
+ */
+export const getIndexQuote = (code) => {
+  return request({
+    url: '/api/stock/index',
+    method: 'GET',
+    data: { code }
+  })
+}
+
+/**
+ * 获取用户自选股票列表
+ * @returns {Promise} 返回用户自选股票列表
+ */
+export const getUserStocks = () => {
+  return request({
+    url: '/v1/user/stock/list',
+    method: 'GET'
+  })
+}
+
+/**
+ * 添加自选股票
+ * @param {object} data - { stockCode, stockName, currentPrice }
+ * @returns {Promise} 返回添加结果
+ */
+export const addUserStock = (data) => {
+  return request({
+    url: '/v1/user/stock/add',
+    method: 'POST',
+    header: {
+      'content-type': 'application/json'
+    },
+    data: data
+  })
+}
+
+/**
+ * 删除自选股票
+ * @param {string} stockCode - 股票代码
+ * @returns {Promise} 返回删除结果
+ */
+export const deleteUserStock = (stockCode) => {
+  return request({
+    url: `/v1/user/stock/delete?stockCode=${encodeURIComponent(stockCode)}`,
+    method: 'DELETE'
+  })
+}
+
+/**
+ * 检查股票是否已添加
+ * @param {string} stockCode - 股票代码
+ * @returns {Promise} 返回是否已添加
+ */
+export const checkUserStock = (stockCode) => {
+  return request({
+    url: '/v1/user/stock/check',
+    method: 'GET',
+    data: { stockCode }
+  })
+}
+

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor