Browse Source

feat: update web admin pages with slider captcha and format utils

Update cfc-web admin/teacher views, add SliderCaptcha component and format utility.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
User 4 months ago
parent
commit
48d1cbbc82

+ 1 - 1
cfc-web/.env.development

@@ -1 +1 @@
-VUE_APP_BASE_API=http://192.168.16.18:8080
+VUE_APP_BASE_API=http://192.168.16.18:9082

+ 222 - 0
cfc-web/src/components/SliderCaptcha.vue

@@ -0,0 +1,222 @@
+<template>
+  <div class="slider-captcha" :class="statusClass">
+    <div
+      class="slider-track"
+      ref="track"
+      @mousedown.prevent="onDragStart"
+      @touchstart.prevent="onDragStart"
+    >
+      <div class="slider-fill" :style="{ width: fillPercent + '%' }"></div>
+      <div
+        class="slider-thumb"
+        :style="{ left: thumbLeft + 'px' }"
+        :class="{ 'is-dragging': dragging }"
+      >
+        <i class="el-icon-d-arrow-right"></i>
+      </div>
+    </div>
+    <div class="captcha-hint">
+      <template v-if="status === 'pending'">
+        <span class="hint-arrow">»</span>
+        <span>请按住滑块拖动到最右侧完成验证</span>
+      </template>
+      <template v-else-if="status === 'success'">
+        <i class="el-icon-success" style="color:#4CAF50"></i>
+        <span style="color:#4CAF50">验证通过</span>
+      </template>
+      <template v-else-if="status === 'fail'">
+        <i class="el-icon-error" style="color:#EF5350"></i>
+        <span style="color:#EF5350">验证失败,请重试</span>
+      </template>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'SliderCaptcha',
+  data() {
+    return {
+      trackWidth: 0,
+      currentOffset: 0,
+      dragging: false,
+      status: 'pending' // pending | dragging | success | fail
+    }
+  },
+  computed: {
+    fillPercent() {
+      if (this.trackWidth === 0) return 0
+      return (this.currentOffset / this.trackWidth) * 100
+    },
+    thumbLeft() { return this.currentOffset },
+    statusClass() {
+      return {
+        'is-success': this.status === 'success',
+        'is-fail': this.status === 'fail',
+        'is-dragging': this.status === 'dragging'
+      }
+    }
+  },
+  mounted() {
+    this.$nextTick(this.initCaptcha)
+  },
+  methods: {
+    initCaptcha() {
+      if (!this.$refs.track) return
+      this.trackWidth = this.$refs.track.offsetWidth
+      this.currentOffset = 0
+      this.status = 'pending'
+    },
+    onDragStart(e) {
+      if (this.status === 'success') return
+      this.dragging = true
+      this.status = 'dragging'
+
+      var startX = e.clientX || (e.touches && e.touches[0].clientX)
+      var startOffset = this.currentOffset
+      var self = this
+
+      var onMove = function(ev) {
+        var clientX = ev.clientX || (ev.touches && ev.touches[0].clientX)
+        if (clientX === undefined) return
+        var newOffset = startOffset + (clientX - startX)
+        if (newOffset < 0) newOffset = 0
+        if (newOffset > self.trackWidth) newOffset = self.trackWidth
+        self.currentOffset = newOffset
+      }
+
+      var onEnd = function() {
+        document.removeEventListener('mousemove', onMove)
+        document.removeEventListener('mouseup', onEnd)
+        document.removeEventListener('touchmove', onMove)
+        document.removeEventListener('touchend', onEnd)
+        self.dragging = false
+        self.onDragEnd()
+      }
+
+      document.addEventListener('mousemove', onMove)
+      document.addEventListener('mouseup', onEnd)
+      document.addEventListener('touchmove', onMove, { passive: true })
+      document.addEventListener('touchend', onEnd)
+    },
+    onDragEnd() {
+      if (this.currentOffset >= this.trackWidth * 0.88) {
+        this.status = 'success'
+        this.currentOffset = this.trackWidth
+        this.$emit('success')
+      } else {
+        this.status = 'fail'
+        this.$emit('fail')
+        var self = this
+        setTimeout(function() { self.initCaptcha() }, 1000)
+      }
+    },
+    reset() {
+      this.initCaptcha()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.slider-captcha {
+  user-select: none;
+  -webkit-user-select: none;
+}
+
+.slider-track {
+  position: relative;
+  height: 40px;
+  background: #EDF2F7;
+  border-radius: 20px;
+  cursor: pointer;
+  overflow: hidden;
+  border: 1px solid #E2E8F0;
+  transition: border-color 0.3s;
+}
+
+.slider-track:hover { border-color: #5B9BD5; }
+
+.slider-fill {
+  position: absolute;
+  left: 0;
+  top: 0;
+  height: 100%;
+  background: linear-gradient(90deg, #8FC5E8, #5B9BD5);
+  border-radius: 20px;
+  pointer-events: none;
+}
+
+.slider-thumb {
+  position: absolute;
+  top: -2px;
+  width: 44px;
+  height: 44px;
+  background: linear-gradient(135deg, #5B9BD5, #3A7CC4);
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: grab;
+  box-shadow: 0 2px 8px rgba(91, 155, 213, 0.4);
+  z-index: 2;
+  margin-left: -22px;
+  transition: box-shadow 0.2s;
+}
+
+.slider-thumb i {
+  color: #fff;
+  font-size: 18px;
+  font-weight: bold;
+}
+
+.slider-thumb:hover { box-shadow: 0 3px 12px rgba(91, 155, 213, 0.6); }
+.slider-thumb.is-dragging {
+  cursor: grabbing;
+  box-shadow: 0 3px 16px rgba(91, 155, 213, 0.7);
+}
+
+.captcha-hint {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-size: 13px;
+  color: #909399;
+  margin-top: 8px;
+  justify-content: center;
+}
+
+.hint-arrow {
+  color: #5B9BD5;
+  font-weight: bold;
+  animation: hintPulse 1.6s ease-in-out infinite;
+}
+
+@keyframes hintPulse {
+  0%, 100% { opacity: 0.3; transform: translateX(0); }
+  50% { opacity: 1; transform: translateX(4px); }
+}
+
+.is-success .slider-track { border-color: #4CAF50; }
+.is-success .slider-fill { background: linear-gradient(90deg, #81C784, #4CAF50); }
+.is-success .slider-thumb {
+  background: linear-gradient(135deg, #81C784, #4CAF50);
+  box-shadow: 0 2px 8px rgba(76, 175, 80, 0.4);
+  cursor: default;
+}
+.is-success .slider-thumb i { display: none; }
+.is-success .slider-thumb::before {
+  content: '✓';
+  color: #fff;
+  font-size: 20px;
+  font-weight: bold;
+  position: absolute;
+}
+
+.is-fail .slider-track { border-color: #EF5350; }
+.is-fail .slider-fill { background: linear-gradient(90deg, #E57373, #EF5350); }
+.is-fail .slider-thumb {
+  background: linear-gradient(135deg, #E57373, #EF5350);
+  box-shadow: 0 2px 8px rgba(239, 83, 80, 0.4);
+}
+</style>

+ 6 - 0
cfc-web/src/main.js

@@ -4,9 +4,15 @@ import router from './router'
 import store from './store'
 import ElementUI from 'element-ui'
 import 'element-ui/lib/theme-chalk/index.css'
+import { formatPrice, formatPriceWithSymbol, formatRate, formatScore } from './utils/format'
 
 Vue.use(ElementUI)
 
+Vue.prototype.formatPrice = formatPrice
+Vue.prototype.formatPriceWithSymbol = formatPriceWithSymbol
+Vue.prototype.formatRate = formatRate
+Vue.prototype.formatScore = formatScore
+
 // Fix Element UI 2.15.x el-submenu removeTabindex null error
 // https://github.com/ElemeFE/element/issues/21965
 // Also fixes: v-if 切换菜单项后 el-submenu 残留 DOM 遮罩导致无法点击的问题

+ 41 - 0
cfc-web/src/utils/format.js

@@ -0,0 +1,41 @@
+/**
+ * Integer cents → display format utilities
+ * All backend prices now stored as Integer cents (1元 = 100)
+ * All backend rates stored as Integer bps (100% = 10000)
+ * All backend scores stored as Integer percentile (5.00 = 500)
+ */
+
+/**
+ * Format cents to yuan string (no symbol)
+ * 9900 → '99.00'
+ */
+export function formatPrice (cents) {
+  if (cents === null || cents === undefined) return '0.00'
+  return (Number(cents) / 100).toFixed(2)
+}
+
+/**
+ * Format cents to yuan with ¥ symbol
+ * 9900 → '¥99.00'
+ */
+export function formatPriceWithSymbol (cents) {
+  return '¥' + formatPrice(cents)
+}
+
+/**
+ * Format bps to percentage string
+ * 235 → '2.35%'   1000 → '10.00%'
+ */
+export function formatRate (bps) {
+  if (bps === null || bps === undefined) return '0.00%'
+  return (Number(bps) / 100).toFixed(2) + '%'
+}
+
+/**
+ * Format percentile score to decimal display
+ * 500 → '5.00'   435 → '4.35'
+ */
+export function formatScore (percentile) {
+  if (percentile === null || percentile === undefined) return '0.00'
+  return (Number(percentile) / 100).toFixed(2)
+}

+ 1 - 1
cfc-web/src/views/GuideAudit.vue

@@ -182,7 +182,7 @@ export default {
           this.rejectDialogVisible = false
           this.loadData()
         } else {
-          this.$message.error(res.data?.message || '操作失败')
+          this.$message.error((res.data && res.data.message) || '操作失败')
         }
       } catch (e) {
         this.$message.error('操作失败')

+ 1 - 1
cfc-web/src/views/GuidePackages.vue

@@ -33,7 +33,7 @@
           <template slot-scope="{ row }">{{ row.cycleDays }}天</template>
         </el-table-column>
         <el-table-column prop="price" label="价格" width="80">
-          <template slot-scope="{ row }">¥{{ row.price }}</template>
+          <template slot-scope="{ row }">{{ formatPriceWithSymbol(row.price) }}</template>
         </el-table-column>
         <el-table-column prop="category" label="分类" width="80"></el-table-column>
         <el-table-column prop="status" label="状态" width="100">

+ 632 - 66
cfc-web/src/views/Login.vue

@@ -1,44 +1,272 @@
 <template>
-  <div class="login-container">
-    <el-card class="login-card">
-      <h2 class="title">浠艾福管理后台</h2>
-      <el-form :model="loginForm" :rules="rules" ref="loginForm">
-        <el-form-item prop="username">
-          <el-input 
-            v-model="loginForm.username" 
-            placeholder="请输入用户名" 
-            prefix-icon="el-icon-user"
-          ></el-input>
-        </el-form-item>
-        <el-form-item prop="password">
-          <el-input 
-            v-model="loginForm.password" 
-            type="password"
-            placeholder="请输入密码" 
-            prefix-icon="el-icon-lock"
-            @keyup.enter.native="handleLogin"
-          ></el-input>
-        </el-form-item>
-        <el-form-item>
-          <el-button 
-            type="primary" 
-            class="login-btn" 
-            @click="handleLogin" 
-            :loading="loading"
-          >
-            登录
-          </el-button>
-        </el-form-item>
-      </el-form>
-    </el-card>
+  <div class="login-page">
+    <!-- 左侧品牌展示区 — 五行相生相克沙盘 -->
+    <div class="brand-panel">
+      <div class="brand-content">
+
+        <!-- ===== 五维能量沙盘 · 相生相克图 ===== -->
+        <div class="sand-table-wrap">
+          <svg class="sand-table-svg" viewBox="0 0 340 370" xmlns="http://www.w3.org/2000/svg">
+            <defs>
+              <!-- 相生箭头(userSpaceOnUse 精确定位,箭头尖在圆边) -->
+              <marker id="arrowGen" markerWidth="52" markerHeight="10" refX="48" refY="5" orient="auto" markerUnits="userSpaceOnUse">
+                <polygon points="7,5 0,0 0,10" fill="#FFE066" />
+              </marker>
+              <!-- 相克箭头 -->
+              <marker id="arrowOve" markerWidth="45" markerHeight="12" refX="41" refY="6" orient="auto" markerUnits="userSpaceOnUse">
+                <polygon points="7,6 0,0 0,12" fill="#FF8A80" />
+              </marker>
+
+              <!-- 节点径向渐变 -->
+              <radialGradient id="gradEarth" cx="40%" cy="35%" r="60%">
+                <stop offset="0%"   stop-color="#D4B896" />
+                <stop offset="100%" stop-color="#9A7B4F" />
+              </radialGradient>
+              <radialGradient id="gradMetal" cx="40%" cy="35%" r="60%">
+                <stop offset="0%"   stop-color="#F0DA70" />
+                <stop offset="100%" stop-color="#D4A843" />
+              </radialGradient>
+              <radialGradient id="gradWater" cx="40%" cy="35%" r="60%">
+                <stop offset="0%"   stop-color="#8FC5E8" />
+                <stop offset="100%" stop-color="#5B9BD5" />
+              </radialGradient>
+              <radialGradient id="gradWood" cx="40%" cy="35%" r="60%">
+                <stop offset="0%"   stop-color="#81C784" />
+                <stop offset="100%" stop-color="#4CAF50" />
+              </radialGradient>
+              <radialGradient id="gradFire" cx="40%" cy="35%" r="60%">
+                <stop offset="0%"   stop-color="#FFAB91" />
+                <stop offset="100%" stop-color="#F08A7A" />
+              </radialGradient>
+
+              <!-- 外环渐变轨道 -->
+              <linearGradient id="ringGrad" x1="0%" y1="0%" x2="100%" y2="100%">
+                <stop offset="0%"   stop-color="#FFD700" stop-opacity="0.25" />
+                <stop offset="25%"  stop-color="#FFE066" stop-opacity="0.15" />
+                <stop offset="50%"  stop-color="#FFD700" stop-opacity="0.08" />
+                <stop offset="75%"  stop-color="#FFE066" stop-opacity="0.15" />
+                <stop offset="100%" stop-color="#FFD700" stop-opacity="0.25" />
+              </linearGradient>
+
+              <!-- 发光滤镜 -->
+              <filter id="glowStrong">
+                <feGaussianBlur stdDeviation="4" result="blur"/>
+                <feMerge>
+                  <feMergeNode in="blur"/>
+                  <feMergeNode in="blur"/>
+                  <feMergeNode in="SourceGraphic"/>
+                </feMerge>
+              </filter>
+              <filter id="glowNode">
+                <feGaussianBlur stdDeviation="2.5" result="blur"/>
+                <feMerge>
+                  <feMergeNode in="blur"/>
+                  <feMergeNode in="blur"/>
+                  <feMergeNode in="SourceGraphic"/>
+                </feMerge>
+              </filter>
+            </defs>
+
+            <!-- 背景光晕 -->
+            <circle cx="170" cy="190" r="140" fill="url(#ringGrad)" />
+
+            <!-- 外层装饰轨道 -->
+            <circle cx="170" cy="190" r="132" fill="none" stroke="rgba(255,255,255,0.06)" stroke-width="1" />
+            <circle cx="170" cy="190" r="129" fill="none" stroke="rgba(255,215,0,0.10)" stroke-width="0.5" stroke-dasharray="2,4" class="orbital-ring" />
+
+            <!-- 轨道粒子 -->
+            <circle cx="170" cy="58"  r="2" fill="#FFE066" opacity="0.5" class="particle" />
+            <circle cx="271" cy="133" r="2" fill="#FFE066" opacity="0.5" class="particle" />
+            <circle cx="240" cy="280" r="2" fill="#FFE066" opacity="0.5" class="particle" />
+            <circle cx="100" cy="280" r="2" fill="#FFE066" opacity="0.5" class="particle" />
+            <circle cx="69"  cy="133" r="2" fill="#FFE066" opacity="0.5" class="particle" />
+
+            <!-- ===== 相生环 ===== -->
+            <!-- 土→金 -->
+            <path d="M 170,85 A 105,105 0 0,1 270,158" fill="none" stroke="#FFD700" stroke-width="2.5" stroke-opacity="0.8" class="arc-gen" marker-end="url(#arrowGen)" />
+            <!-- 金→水 -->
+            <path d="M 270,158 A 105,105 0 0,1 232,275" fill="none" stroke="#FFD700" stroke-width="2.5" stroke-opacity="0.8" class="arc-gen" marker-end="url(#arrowGen)" />
+            <!-- 水→木 -->
+            <path d="M 232,275 A 105,105 0 0,1 108,275" fill="none" stroke="#FFD700" stroke-width="2.5" stroke-opacity="0.8" class="arc-gen" marker-end="url(#arrowGen)" />
+            <!-- 木→火 -->
+            <path d="M 108,275 A 105,105 0 0,1 70,158" fill="none" stroke="#FFD700" stroke-width="2.5" stroke-opacity="0.8" class="arc-gen" marker-end="url(#arrowGen)" />
+            <!-- 火→土 -->
+            <path d="M 70,158 A 105,105 0 0,1 170,85" fill="none" stroke="#FFD700" stroke-width="2.5" stroke-opacity="0.8" class="arc-gen" marker-end="url(#arrowGen)" />
+
+            <!-- ===== 相克线(内五角星) ===== -->
+            <!-- 木克土  行→身(静态线,无脉冲闪烁) -->
+            <line x1="108" y1="275" x2="170" y2="85" stroke="#FF8A80" stroke-width="1.5" stroke-dasharray="6,6" stroke-opacity="0.45" class="line-ove-quiet" marker-end="url(#arrowOve)" />
+            <!-- 土克水  身→富 -->
+            <line x1="170" y1="85" x2="232" y2="275" stroke="#FF8A80" stroke-width="2" stroke-dasharray="6,6" class="line-ove" marker-end="url(#arrowOve)" />
+            <!-- 水克火  富→心 -->
+            <line x1="232" y1="275" x2="70" y2="158" stroke="#FF8A80" stroke-width="2" stroke-dasharray="6,6" class="line-ove" marker-end="url(#arrowOve)" />
+            <!-- 火克金  心→智 -->
+            <line x1="70" y1="158" x2="270" y2="158" stroke="#FF8A80" stroke-width="2" stroke-dasharray="6,6" class="line-ove" marker-end="url(#arrowOve)" />
+            <!-- 金克木  智→行 -->
+            <line x1="270" y1="158" x2="108" y2="275" stroke="#FF8A80" stroke-width="2" stroke-dasharray="6,6" class="line-ove" marker-end="url(#arrowOve)" />
+
+            <!-- ===== 五维节点 ===== -->
+
+            <!-- 身(土) — 上 -->
+            <g class="node-group">
+              <circle cx="170" cy="85" r="34" fill="url(#gradEarth)" stroke="rgba(212,184,150,0.5)" stroke-width="2" filter="url(#glowNode)" />
+              <circle cx="170" cy="85" r="30" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="0.5" />
+              <text x="170" y="79" text-anchor="middle" fill="#fff" font-size="18">🌏</text>
+              <text x="170" y="100" text-anchor="middle" fill="#fff" font-size="15" font-weight="600">身</text>
+              <text x="170" y="114" text-anchor="middle" fill="rgba(255,255,255,0.45)" font-size="9">土</text>
+            </g>
+
+            <!-- 智(金) — 右上 -->
+            <g class="node-group">
+              <circle cx="270" cy="158" r="34" fill="url(#gradMetal)" stroke="rgba(240,218,112,0.5)" stroke-width="2" filter="url(#glowNode)" />
+              <circle cx="270" cy="158" r="30" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="0.5" />
+              <text x="270" y="152" text-anchor="middle" fill="#fff" font-size="18">⚔️</text>
+              <text x="270" y="173" text-anchor="middle" fill="#fff" font-size="15" font-weight="600">智</text>
+              <text x="270" y="187" text-anchor="middle" fill="rgba(255,255,255,0.45)" font-size="9">金</text>
+            </g>
+
+            <!-- 富(水) — 右下 -->
+            <g class="node-group">
+              <circle cx="232" cy="275" r="34" fill="url(#gradWater)" stroke="rgba(143,197,232,0.5)" stroke-width="2" filter="url(#glowNode)" />
+              <circle cx="232" cy="275" r="30" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="0.5" />
+              <text x="232" y="269" text-anchor="middle" fill="#fff" font-size="18">💧</text>
+              <text x="232" y="290" text-anchor="middle" fill="#fff" font-size="15" font-weight="600">富</text>
+              <text x="232" y="304" text-anchor="middle" fill="rgba(255,255,255,0.45)" font-size="9">水</text>
+            </g>
+
+            <!-- 行(木) — 左下 -->
+            <g class="node-group">
+              <circle cx="108" cy="275" r="34" fill="url(#gradWood)" stroke="rgba(129,199,132,0.5)" stroke-width="2" filter="url(#glowNode)" />
+              <circle cx="108" cy="275" r="30" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="0.5" />
+              <text x="108" y="269" text-anchor="middle" fill="#fff" font-size="18">🌿</text>
+              <text x="108" y="290" text-anchor="middle" fill="#fff" font-size="15" font-weight="600">行</text>
+              <text x="108" y="304" text-anchor="middle" fill="rgba(255,255,255,0.45)" font-size="9">木</text>
+            </g>
+
+            <!-- 心(火) — 左上 -->
+            <g class="node-group">
+              <circle cx="70" cy="158" r="34" fill="url(#gradFire)" stroke="rgba(255,171,145,0.5)" stroke-width="2" filter="url(#glowNode)" />
+              <circle cx="70" cy="158" r="30" fill="none" stroke="rgba(255,255,255,0.08)" stroke-width="0.5" />
+              <text x="70" y="152" text-anchor="middle" fill="#fff" font-size="18">🔥</text>
+              <text x="70" y="173" text-anchor="middle" fill="#fff" font-size="15" font-weight="600">心</text>
+              <text x="70" y="187" text-anchor="middle" fill="rgba(255,255,255,0.45)" font-size="9">火</text>
+            </g>
+
+            <!-- 中心五行文字 -->
+            <text x="170" y="188" text-anchor="middle" fill="rgba(255,255,255,0.07)" font-size="13" letter-spacing="5">五行 · 相生相克</text>
+
+            <!-- 图例 -->
+            <g transform="translate(170, 345)">
+              <line x1="-50" y1="0" x2="-22" y2="0" stroke="#FFD700" stroke-width="2" />
+              <polygon points="-22,0 -17,-3 -17,3" fill="#FFD700" />
+              <text x="-8" y="4" text-anchor="start" fill="rgba(255,255,255,0.5)" font-size="9">相生</text>
+              <line x1="44" y1="0" x2="72" y2="0" stroke="#FF8A80" stroke-width="1.5" stroke-dasharray="4,3" />
+              <polygon points="72,0 77,-3 77,3" fill="#FF8A80" />
+              <text x="80" y="4" text-anchor="start" fill="rgba(255,255,255,0.5)" font-size="9">相克</text>
+            </g>
+          </svg>
+        </div>
+
+        <!-- Logo + 品牌 -->
+        <div class="brand-mid">
+          <div class="brand-logo-row">
+            <img src="/logo.png" alt="浠艾福" class="logo-img" />
+            <span class="brand-name">浠艾福</span>
+            <span class="brand-en">Care Family</span>
+          </div>
+        </div>
+
+        <!-- 底部 -->
+        <div class="brand-bottom">
+          <p class="tagline">给全家的一站式幸福提案</p>
+          <div class="role-tags">
+            <span class="role-tag" style="border-color:#5B9BD5">家长</span>
+            <span class="role-tag" style="border-color:#F08A7A">服务商</span>
+            <span class="role-tag" style="border-color:#4CAF50">运营</span>
+          </div>
+        </div>
+
+      </div>
+    </div>
+
+    <!-- 右侧登录表单区 -->
+    <div class="form-panel">
+      <div class="form-card">
+        <div class="form-header">
+          <div class="header-icon">
+            <i class="el-icon-user"></i>
+          </div>
+          <h2 class="form-title">管理后台登录</h2>
+          <p class="form-desc">请使用管理员或规划师账号登录</p>
+        </div>
+
+        <el-form
+          :model="loginForm"
+          :rules="rules"
+          ref="loginForm"
+          class="login-form"
+          @submit.native.prevent="handleLogin"
+        >
+          <el-form-item prop="username">
+            <el-input
+              v-model="loginForm.username"
+              placeholder="请输入用户名 / 手机号"
+              size="large"
+              :disabled="loading"
+            >
+              <i slot="prefix" class="el-icon-user input-icon"></i>
+            </el-input>
+          </el-form-item>
+
+          <el-form-item prop="password">
+            <el-input
+              v-model="loginForm.password"
+              type="password"
+              placeholder="请输入密码"
+              size="large"
+              :disabled="loading"
+              show-password
+            >
+              <i slot="prefix" class="el-icon-lock input-icon"></i>
+            </el-input>
+          </el-form-item>
+
+          <el-form-item prop="captcha" class="captcha-item">
+            <SliderCaptcha
+              ref="sliderCaptcha"
+              @success="onCaptchaSuccess"
+              @fail="onCaptchaFail"
+            />
+          </el-form-item>
+
+          <el-form-item>
+            <el-button
+              type="primary"
+              class="login-btn"
+              @click="handleLogin"
+              :loading="loading"
+              :disabled="!captchaPassed"
+            >
+              {{ loading ? '登录中...' : '登 录' }}
+            </el-button>
+          </el-form-item>
+        </el-form>
+
+        <div class="form-footer">
+          <span class="copyright">&copy; 2026 亿图腾(北京)科技有限公司</span>
+        </div>
+      </div>
+    </div>
   </div>
 </template>
 
 <script>
 import { adminLogin } from '@/api/auth'
+import SliderCaptcha from '@/components/SliderCaptcha'
 
 export default {
   name: 'Login',
+  components: { SliderCaptcha },
   data() {
     return {
       loginForm: {
@@ -47,39 +275,55 @@ export default {
       },
       rules: {
         username: [
-          { required: true, message: '请输入用户名', trigger: 'blur' }
+          { required: true, message: '请输入用户名', trigger: 'blur' },
+          { min: 2, message: '用户名至少2个字符', trigger: 'blur' }
         ],
         password: [
-          { required: true, message: '请输入密码', trigger: 'blur' }
+          { required: true, message: '请输入密码', trigger: 'blur' },
+          { min: 4, message: '密码至少4个字符', trigger: 'blur' }
         ]
       },
-      loading: false
+      loading: false,
+      captchaPassed: false
     }
   },
   methods: {
+    onCaptchaSuccess() {
+      this.captchaPassed = true
+    },
+    onCaptchaFail() {
+      this.captchaPassed = false
+    },
+    resetCaptcha() {
+      if (this.$refs.sliderCaptcha) this.$refs.sliderCaptcha.reset()
+      this.captchaPassed = false
+    },
     handleLogin() {
-      this.$refs.loginForm.validate(async valid => {
-        if (valid) {
-          this.loading = true
-          try {
-      const res = await adminLogin({
-        phone: this.loginForm.username,
-        password: this.loginForm.password
-      })
-      localStorage.setItem('token', res.data.token)
-      localStorage.setItem('adminId', res.data.adminId)
-      localStorage.setItem('adminName', res.data.realName)
-      const role = res.data.role || 'admin'
-      localStorage.setItem('role', role)
-      console.log('Login successful, role:', role)
-      this.$message.success('登录成功,角色:' + role)
-      await this.$nextTick()
-      this.$router.push({ path: '/' }, () => {})
-          } catch (e) {
-            this.$message.error(e.message || '登录失败')
-          } finally {
-            this.loading = false
-          }
+      if (!this.captchaPassed) {
+        this.$message.warning('请先完成滑动验证')
+        return
+      }
+      this.$refs.loginForm.validate(async (valid) => {
+        if (!valid) return
+        this.loading = true
+        try {
+          const res = await adminLogin({
+            phone: this.loginForm.username,
+            password: this.loginForm.password
+          })
+          localStorage.setItem('token', res.data.token)
+          localStorage.setItem('adminId', res.data.adminId)
+          localStorage.setItem('adminName', res.data.realName)
+          const role = res.data.role || 'admin'
+          localStorage.setItem('role', role)
+          this.$message.success('登录成功')
+          await this.$nextTick()
+          this.$router.push({ path: '/' })
+        } catch (e) {
+          this.$message.error(e.message || '登录失败,请检查账号密码')
+          this.resetCaptcha()
+        } finally {
+          this.loading = false
         }
       })
     }
@@ -88,26 +332,348 @@ export default {
 </script>
 
 <style scoped>
-.login-container {
+/* ============================================================
+   登录页面 — 浠艾福 (Care Family)
+   品牌色: #5B9BD5 / #F08A7A
+   五维序: 身(土)→智(金)→富(水)→行(木)→心(火)
+   ============================================================ */
+
+.login-page {
+  display: flex;
+  height: 100vh;
+  width: 100vw;
+  overflow: hidden;
+  font-family: 'Helvetica Neue', Helvetica, 'PingFang SC',
+    'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;
+}
+
+/* ==================== 左侧品牌区 ==================== */
+.brand-panel {
+  flex: 0 0 55%;
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: linear-gradient(165deg, #3A7CC4 0%, #5B9BD5 30%, #6BA8DE 60%, #7BBDE0 100%);
+  overflow: hidden;
+}
+
+.brand-content {
+  position: relative;
+  z-index: 2;
   display: flex;
+  flex-direction: column;
+  align-items: center;
   justify-content: center;
+  max-width: 480px;
+  width: 100%;
+  padding: 16px 40px;
+  color: #fff;
+  min-height: 100vh;
+  box-sizing: border-box;
+}
+
+/* -------- 五维能量沙盘 -------- */
+.sand-table-wrap {
+  flex: 1;
+  display: flex;
   align-items: center;
-  height: 100vh;
-  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  justify-content: center;
+  width: 100%;
+  min-height: 0;
+}
+
+.sand-table-svg {
+  width: 100%;
+  max-width: 340px;
+  height: auto;
+  display: block;
+}
+
+/* ===== 相生环 · 连续流动 ===== */
+.sand-table-svg .arc-gen {
+  stroke-dasharray: 10 8;
+  animation: flowArc 1.2s linear infinite;
+}
+.sand-table-svg .arc-gen:nth-child(4)  { animation-delay: 0.00s; }
+.sand-table-svg .arc-gen:nth-child(5)  { animation-delay: 0.24s; }
+.sand-table-svg .arc-gen:nth-child(6)  { animation-delay: 0.48s; }
+.sand-table-svg .arc-gen:nth-child(7)  { animation-delay: 0.72s; }
+.sand-table-svg .arc-gen:nth-child(8)  { animation-delay: 0.96s; }
+
+@keyframes flowArc {
+  0%   { stroke-dashoffset: 0; }
+  100% { stroke-dashoffset: -18; }
+}
+
+/* ===== 相克线 · 脉冲 ===== */
+.sand-table-svg .line-ove {
+  animation: pulseLine 2.2s ease-in-out infinite;
+}
+.sand-table-svg .line-ove:nth-child(10) { animation-delay: 0.00s; }
+.sand-table-svg .line-ove:nth-child(11) { animation-delay: 0.44s; }
+.sand-table-svg .line-ove:nth-child(12) { animation-delay: 0.88s; }
+.sand-table-svg .line-ove:nth-child(13) { animation-delay: 1.32s; }
+
+@keyframes pulseLine {
+  0%, 100% { stroke-opacity: 0.35; stroke-width: 1.5; }
+  50%      { stroke-opacity: 0.75; stroke-width: 2.5; }
+}
+
+/* 木→土线(静态,无脉冲) */
+.sand-table-svg .line-ove-quiet {
+  stroke-opacity: 0.35;
+}
+
+/* ===== 节点呼吸 ===== */
+.sand-table-svg .node-group {
+  animation: nodeBreathe 3.2s ease-in-out infinite;
+}
+.sand-table-svg .node-group:nth-child(14) { animation-delay: 0.0s; }
+.sand-table-svg .node-group:nth-child(15) { animation-delay: 0.64s; }
+.sand-table-svg .node-group:nth-child(16) { animation-delay: 1.28s; }
+.sand-table-svg .node-group:nth-child(17) { animation-delay: 1.92s; }
+.sand-table-svg .node-group:nth-child(18) { animation-delay: 2.56s; }
+
+@keyframes nodeBreathe {
+  0%, 100% { opacity: 0.85; transform: scale(1); }
+  50%      { opacity: 1;   transform: scale(1.03); }
+}
+
+/* ===== 轨道粒子公转 ===== */
+.sand-table-svg .particle {
+  animation: orbitParticle 8s linear infinite;
+  transform-origin: 170px 190px;
+}
+.sand-table-svg .particle:nth-child(3) { animation-delay: 0.0s; }
+
+@keyframes orbitParticle {
+  0%   { opacity: 0;   transform: rotate(0deg)   translateX(132px) rotate(0deg); }
+  12%  { opacity: 0.6; }
+  88%  { opacity: 0.6; }
+  100% { opacity: 0;   transform: rotate(360deg) translateX(132px) rotate(-360deg); }
+}
+
+/* -------- Logo + 品牌 -------- */
+.brand-mid {
+  flex-shrink: 0;
+  margin: 6px 0 10px;
+}
+
+.brand-logo-row {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 10px;
+}
+
+.logo-img {
+  width: 36px;
+  height: 36px;
+  display: block;
+}
+
+.brand-name {
+  font-size: 22px;
+  font-weight: 700;
+  letter-spacing: 3px;
+  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+
+.brand-en {
+  font-size: 11px;
+  opacity: 0.6;
+  letter-spacing: 2px;
+  font-weight: 300;
+  margin-left: -2px;
+}
+
+/* -------- 底部 -------- */
+.brand-bottom {
+  flex-shrink: 0;
+  text-align: center;
+}
+
+.tagline {
+  font-size: 13px;
+  opacity: 0.65;
+  letter-spacing: 2px;
+  font-weight: 300;
+  margin: 0 0 10px;
+}
+
+.role-tags {
+  display: flex;
+  gap: 8px;
+  justify-content: center;
+}
+
+.role-tag {
+  display: inline-block;
+  padding: 2px 12px;
+  font-size: 11px;
+  border: 1px solid;
+  border-radius: 20px;
+  opacity: 0.45;
+  transition: opacity 0.3s;
 }
 
-.login-card {
-  width: 400px;
-  padding: 20px;
+.role-tag:hover {
+  opacity: 0.85;
 }
 
-.title {
+/* ==================== 右侧表单区 ==================== */
+.form-panel {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #F5F9FC;
+  padding: 40px;
+}
+
+.form-card {
+  width: 100%;
+  max-width: 400px;
+  background: #fff;
+  border-radius: 16px;
+  padding: 40px 36px 32px;
+  box-shadow: 0 8px 40px rgba(91, 155, 213, 0.08),
+              0 1px 4px rgba(0, 0, 0, 0.04);
+}
+
+.form-header {
   text-align: center;
-  color: #333;
-  margin-bottom: 30px;
+  margin-bottom: 32px;
+}
+
+.header-icon {
+  width: 48px;
+  height: 48px;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #D6EAF8, #8FC5E8);
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 16px;
+}
+
+.header-icon i {
+  font-size: 24px;
+  color: #3A7CC4;
+}
+
+.form-title {
+  font-size: 22px;
+  font-weight: 600;
+  color: #1E293B;
+  margin: 0 0 6px;
+}
+
+.form-desc {
+  font-size: 13px;
+  color: #94A3B8;
+  margin: 0;
+}
+
+.login-form {
+  margin-bottom: 4px;
+}
+
+.login-form >>> .el-input__inner {
+  height: 44px;
+  line-height: 44px;
+  border-radius: 10px;
+  border-color: #E2E8F0;
+  background: #F8FAFC;
+  transition: all 0.25s;
+  font-size: 14px;
+  padding-left: 42px;
+}
+
+.login-form >>> .el-input__inner:focus {
+  border-color: #5B9BD5;
+  background: #fff;
+  box-shadow: 0 0 0 3px rgba(91, 155, 213, 0.1);
+}
+
+.login-form >>> .el-input__inner:hover {
+  border-color: #8FC5E8;
+}
+
+.login-form >>> .el-input__prefix {
+  left: 0;
+  width: 42px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.input-icon {
+  font-size: 18px;
+  color: #94A3B8;
+  transition: color 0.25s;
+}
+
+.login-form >>> .el-input.is-focused .input-icon {
+  color: #5B9BD5;
+}
+
+.login-form >>> .el-form-item {
+  margin-bottom: 22px;
+}
+
+.captcha-item {
+  margin-bottom: 18px !important;
 }
 
 .login-btn {
   width: 100%;
+  height: 44px;
+  font-size: 16px;
+  font-weight: 500;
+  letter-spacing: 6px;
+  border-radius: 10px;
+  background: linear-gradient(135deg, #5B9BD5, #3A7CC4);
+  border: none;
+  transition: all 0.3s;
+}
+
+.login-btn:hover {
+  background: linear-gradient(135deg, #6BA8DE, #4A86CE);
+  box-shadow: 0 4px 16px rgba(91, 155, 213, 0.35);
+  transform: translateY(-1px);
+}
+
+.login-btn:active {
+  transform: translateY(0);
+}
+
+.login-btn.is-disabled {
+  background: #C0C4CC !important;
+  box-shadow: none !important;
+}
+
+.form-footer {
+  text-align: center;
+  margin-top: 12px;
+}
+
+.copyright {
+  font-size: 12px;
+  color: #C0C4CC;
+}
+
+/* ==================== 响应式 ==================== */
+@media screen and (max-width: 900px) {
+  .brand-panel { display: none; }
+  .form-panel {
+    flex: 1;
+    padding: 20px;
+    background: linear-gradient(160deg, #3A7CC4, #6BA8DE);
+  }
+  .form-card {
+    box-shadow: 0 8px 40px rgba(0, 0, 0, 0.12);
+  }
 }
 </style>

+ 2 - 2
cfc-web/src/views/PackageAudit.vue

@@ -29,7 +29,7 @@
           <template slot-scope="{ row }">{{ row.cycleDays }}天</template>
         </el-table-column>
         <el-table-column prop="price" label="价格" width="80">
-          <template slot-scope="{ row }">¥{{ row.price }}</template>
+          <template slot-scope="{ row }">{{ formatPriceWithSymbol(row.price) }}</template>
         </el-table-column>
         <el-table-column prop="category" label="分类" width="80"></el-table-column>
         <el-table-column prop="status" label="状态" width="100">
@@ -67,7 +67,7 @@
       <el-form v-if="currentRow" label-width="100px">
         <el-form-item label="套餐名称">{{ currentRow.name }}</el-form-item>
         <el-form-item label="周期">{{ currentRow.cycleDays }}天</el-form-item>
-        <el-form-item label="价格">¥{{ currentRow.price }}</el-form-item>
+        <el-form-item label="价格">{{ formatPriceWithSymbol(currentRow.price) }}</el-form-item>
         <el-form-item label="分类">{{ currentRow.category }}</el-form-item>
         <el-form-item label="描述">{{ currentRow.description }}</el-form-item>
         <el-form-item label="状态">

+ 1 - 1
cfc-web/src/views/Wishes.vue

@@ -248,7 +248,7 @@
     <el-dialog title="设置积分" :visible.sync="priceDialogVisible" width="400px">
       <el-form label-width="100px">
         <el-form-item label="心愿名称">
-          <span>{{ currentWish?.title }}</span>
+          <span>{{ currentWish && currentWish.title }}</span>
         </el-form-item>
         <el-form-item label="所需积分">
           <el-input-number v-model="priceValue" :min="1" :max="10000"></el-input-number>

+ 7 - 7
cfc-web/src/views/admin/OrderManage.vue

@@ -50,11 +50,11 @@
       </el-table-column>
       <el-table-column label="订单金额" width="100">
         <template slot-scope="{ row }">
-          <template v-if="sourceTab === 'danshop'">¥{{ row.totalAmount || '0.00' }}</template>
-          <template v-else>¥{{ row.amount ? (row.amount / 100).toFixed(2) : '0.00' }}</template>
+          <template v-if="sourceTab === 'danshop'">{{ formatPriceWithSymbol(row.totalAmount || 0) }}</template>
+          <template v-else>{{ formatPriceWithSymbol(row.amount || 0) }}</template>
         </template>
       </el-table-column>
-      <el-table-column label="订单状态" width="100">
+      <el-table-column label="状态" width="100">
         <template slot-scope="{ row }">
           <el-tag :type="sourceTab === 'danshop' ? danShopStatusType(row.status) : statusType(row.status)">
             {{ sourceTab === 'danshop' ? danShopStatusLabel(row.status) : statusLabel(row.status) }}
@@ -97,12 +97,12 @@
         <el-descriptions-item label="商品名称">{{ detail.productName || '-' }}</el-descriptions-item>
         <el-descriptions-item label="商品类型">{{ sourceTab === 'danshop' ? (detail.productType || '-') : typeLabel(detail.productType) }}</el-descriptions-item>
         <el-descriptions-item label="订单金额">
-          <template v-if="sourceTab === 'danshop'">¥{{ detail.totalAmount || '0.00' }}</template>
-          <template v-else>¥{{ detail.amount ? (detail.amount / 100).toFixed(2) : '0.00' }}</template>
+          <template v-if="sourceTab === 'danshop'">{{ formatPriceWithSymbol(detail.totalAmount || 0) }}</template>
+          <template v-else>{{ formatPriceWithSymbol(detail.amount || 0) }}</template>
         </el-descriptions-item>
         <el-descriptions-item label="实际支付">
-          <template v-if="sourceTab === 'danshop'">¥{{ detail.totalAmount || '0.00' }}</template>
-          <template v-else>¥{{ detail.paidAmount ? (detail.paidAmount / 100).toFixed(2) : '0.00' }}</template>
+          <template v-if="sourceTab === 'danshop'">{{ formatPriceWithSymbol(detail.totalAmount || 0) }}</template>
+          <template v-else>{{ formatPriceWithSymbol(detail.paidAmount || 0) }}</template>
         </el-descriptions-item>
         <el-descriptions-item label="买家昵称">{{ detail.buyerNickname || '-' }}</el-descriptions-item>
         <el-descriptions-item label="买家手机">{{ detail.buyerPhone || '-' }}</el-descriptions-item>

+ 3 - 3
cfc-web/src/views/admin/PackageTemplates.vue

@@ -10,10 +10,10 @@
         <el-table-column prop="name" label="模板名称"></el-table-column>
         <el-table-column prop="validityMonths" label="有效期(月)" width="100"></el-table-column>
         <el-table-column prop="referencePrice" label="参考价格" width="100">
-          <template slot-scope="scope">¥{{ scope.row.referencePrice }}</template>
+          <template slot-scope="scope">{{ formatPriceWithSymbol(scope.row.referencePrice) }}</template>
         </el-table-column>
         <el-table-column prop="minPrice" label="最低售价" width="100">
-          <template slot-scope="scope">¥{{ scope.row.minPrice }}</template>
+          <template slot-scope="scope">{{ formatPriceWithSymbol(scope.row.minPrice) }}</template>
         </el-table-column>
         <el-table-column prop="description" label="描述"></el-table-column>
         <el-table-column prop="status" label="状态" width="100">
@@ -68,7 +68,7 @@
             <span class="service-name">{{ service.serviceName }}</span>
             <span class="service-qty">{{ service.quantity }} {{ service.unit }}</span>
           </div>
-          <span class="service-price">¥{{ service.unitPrice }}</span>
+          <span class="service-price">{{ formatPriceWithSymbol(service.unitPrice) }}</span>
         </div>
       </div>
       <div slot="footer">

+ 3 - 3
cfc-web/src/views/admin/ProductManage.vue

@@ -43,7 +43,7 @@
       </el-table-column>
       <el-table-column label="售价(元)" width="90">
         <template slot-scope="{ row }">
-          {{ sourceTab === 'danshop' ? ('¥' + (row.price || '0.00')) : ('¥' + (row.price / 100).toFixed(2)) }}
+          {{ formatPriceWithSymbol(row.price || 0) }}
         </template>
       </el-table-column>
       <el-table-column prop="stock" label="库存" width="70" />
@@ -101,8 +101,8 @@
       <el-descriptions :column="2" border v-if="detail">
         <el-descriptions-item label="商品名称">{{ detail.name }}</el-descriptions-item>
         <el-descriptions-item label="商品类型">{{ sourceTab === 'danshop' ? danShopTypeLabel(detail.productType) : typeLabel(detail.productType) }}</el-descriptions-item>
-        <el-descriptions-item label="售价">{{ sourceTab === 'danshop' ? ('¥' + (detail.price || '0.00')) : ('¥' + (detail.price / 100).toFixed(2)) }}</el-descriptions-item>
-        <el-descriptions-item label="会员价">{{ detail.memberPrice ? (sourceTab === 'danshop' ? ('¥' + detail.memberPrice) : ('¥' + (detail.memberPrice / 100).toFixed(2))) : '-' }}</el-descriptions-item>
+        <el-descriptions-item label="售价">{{ formatPriceWithSymbol(detail.price || 0) }}</el-descriptions-item>
+        <el-descriptions-item label="会员价">{{ detail.memberPrice ? formatPriceWithSymbol(detail.memberPrice) : '-' }}</el-descriptions-item>
         <el-descriptions-item label="库存">{{ detail.stock }}</el-descriptions-item>
         <el-descriptions-item label="销量">{{ detail.salesCount || 0 }}</el-descriptions-item>
         <el-descriptions-item label="供应商">{{ detail.vendorName || detail.brandName || '-' }}</el-descriptions-item>

+ 4 - 5
cfc-web/src/views/admin/ProductProfitRate.vue

@@ -30,12 +30,12 @@
       </el-table-column>
       <el-table-column prop="price" label="售价(元)" width="90">
         <template slot-scope="{ row }">
-          ¥{{ (row.price / 100).toFixed(2) }}
+          {{ formatPriceWithSymbol(row.price) }}
         </template>
       </el-table-column>
       <el-table-column label="当前利润率" width="120">
         <template slot-scope="{ row }">
-          <span v-if="row.profitRate != null">{{ (row.profitRate * 100).toFixed(1) }}%</span>
+          <span v-if="row.profitRate != null">{{ formatRate(row.profitRate) }}</span>
           <span v-else class="text-muted">未设置</span>
         </template>
       </el-table-column>
@@ -45,9 +45,8 @@
             v-if="editId === row.id"
             v-model="editRate"
             :min="0"
-            :max="1"
-            :step="0.01"
-            :precision="2"
+            :max="10000"
+            :step="100"
             size="small"
             style="width: 140px;"
           />

+ 1 - 1
cfc-web/src/views/admin/ServiceContents.vue

@@ -32,7 +32,7 @@
         <el-table-column prop="duration" label="时长(分钟)" width="100"></el-table-column>
         <el-table-column prop="ageRange" label="适用年龄" width="100"></el-table-column>
         <el-table-column prop="price" label="参考价格" width="100">
-          <template slot-scope="scope">¥{{ scope.row.price }}</template>
+          <template slot-scope="scope">{{ formatPriceWithSymbol(scope.row.price) }}</template>
         </el-table-column>
         <el-table-column prop="status" label="状态" width="100">
           <template slot-scope="scope">

+ 2 - 2
cfc-web/src/views/admin/WithdrawAudit.vue

@@ -19,7 +19,7 @@
       </el-table-column>
       <el-table-column label="提现金额" width="120">
         <template slot-scope="{ row }">
-          <span class="amount">¥{{ (row.amount / 100).toFixed(2) }}</span>
+          <span class="amount">{{ formatPriceWithSymbol(row.amount) }}</span>
         </template>
       </el-table-column>
       <el-table-column label="账户信息" min-width="160" show-overflow-tooltip>
@@ -85,7 +85,7 @@
       <el-descriptions :column="2" border v-if="detail">
         <el-descriptions-item label="申请ID">{{ detail.id }}</el-descriptions-item>
         <el-descriptions-item label="用户昵称">{{ detail.nickname || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="提现金额">¥{{ (detail.amount / 100).toFixed(2) }}</el-descriptions-item>
+        <el-descriptions-item label="提现金额">{{ formatPriceWithSymbol(detail.amount) }}</el-descriptions-item>
         <el-descriptions-item label="账户信息">{{ detail.accountInfo || '-' }}</el-descriptions-item>
         <el-descriptions-item label="申请备注">{{ detail.remark || '-' }}</el-descriptions-item>
         <el-descriptions-item label="审核状态">

+ 2 - 2
cfc-web/src/views/teacher/TeacherAssessment.vue

@@ -118,8 +118,8 @@ export default {
           getGuideFamilyAssessmentList(),
           getFamilyList({})
         ])
-        this.familyConfigs = configsRes.data?.records || configsRes.data || []
-        this.availableFamilies = familiesRes.data?.records || familiesRes.data || []
+        this.familyConfigs = (configsRes.data && configsRes.data.records) || configsRes.data || []
+        this.availableFamilies = (familiesRes.data && familiesRes.data.records) || familiesRes.data || []
       } catch (e) {
         console.error('加载失败', e)
       } finally {

+ 6 - 6
cfc-web/src/views/teacher/TeacherDashboard.vue

@@ -36,7 +36,7 @@
             <i class="el-icon-coin"></i>
           </div>
           <div class="stat-content">
-            <div class="stat-value">¥{{ stats.monthlySales }}</div>
+            <div class="stat-value">{{ formatPriceWithSymbol(stats.monthlySales) }}</div>
             <div class="stat-label">本月销售额</div>
             <div class="stat-trend up">
               <i class="el-icon-top"></i> 23%
@@ -50,7 +50,7 @@
             <i class="el-icon-wallet"></i>
           </div>
           <div class="stat-content">
-            <div class="stat-value">¥{{ stats.myCommission }}</div>
+            <div class="stat-value">{{ formatPriceWithSymbol(stats.myCommission) }}</div>
             <div class="stat-label">我的佣金</div>
             <div class="stat-trend neutral">
               <i class="el-icon-minus"></i> 0%
@@ -177,7 +177,7 @@
                 </div>
               </div>
               <div class="member-performance">
-                <div class="performance-value">¥{{ member.monthlySales || 0 }}</div>
+                <div class="performance-value">{{ formatPriceWithSymbol(member.monthlySales || 0) }}</div>
                 <div class="performance-label">本月业绩</div>
               </div>
               <div class="member-trend" :class="member.trend >= 0 ? 'up' : 'down'">
@@ -223,7 +223,7 @@
             </el-table-column>
             <el-table-column prop="amount" label="金额" width="120">
               <template slot-scope="scope">
-                <span class="amount-value">¥{{ scope.row.amount }}</span>
+                <span class="amount-value">{{ formatPriceWithSymbol(scope.row.amount) }}</span>
               </template>
             </el-table-column>
             <el-table-column prop="status" label="状态" width="100">
@@ -297,8 +297,8 @@ export default {
         if (orderRes.data && orderRes.data.records) {
           this.recentOrders = orderRes.data.records.slice(0, 10)
           const totalSales = orderRes.data.records.reduce((sum, o) => sum + (o.amount || 0), 0)
-          this.stats.monthlySales = totalSales.toFixed(2)
-          this.stats.myCommission = (totalSales * 0.1).toFixed(2)
+          this.stats.monthlySales = totalSales
+          this.stats.myCommission = Math.round(totalSales * 0.1)
           this.todayStats.newOrders = orderRes.data.records.filter(o => {
             const today = new Date().toDateString()
             return new Date(o.createdAt).toDateString() === today

+ 3 - 3
cfc-web/src/views/teacher/TeacherFamilies.vue

@@ -105,7 +105,7 @@
         </el-table-column>
         <el-table-column prop="servicePrice" label="服务价格" width="120">
           <template slot-scope="scope">
-            <span class="price">¥{{ scope.row.servicePrice }}</span>
+            <span class="price">{{ formatPriceWithSymbol(scope.row.servicePrice) }}</span>
           </template>
         </el-table-column>
         <el-table-column prop="childrenCount" label="孩子数" width="80">
@@ -139,7 +139,7 @@
     </el-card>
 
     <!-- 家庭详情抽屉 -->
-    <el-drawer :visible.sync="drawerVisible" :title="currentFamily?.familyName || '家庭详情'" size="500px" direction="rtl">
+    <el-drawer :visible.sync="drawerVisible" :title="(currentFamily && currentFamily.familyName) || '家庭详情'" size="500px" direction="rtl">
       <div class="drawer-content" v-if="currentFamily">
         <el-divider content-position="left">基本信息</el-divider>
         <div class="detail-row">
@@ -156,7 +156,7 @@
         </div>
         <div class="detail-row">
           <span class="detail-label">服务价格</span>
-          <span class="detail-value price">¥{{ currentFamily.servicePrice }}</span>
+          <span class="detail-value price">{{ formatPriceWithSymbol(currentFamily.servicePrice) }}</span>
         </div>
         <div class="detail-row">
           <span class="detail-label">绑定时间</span>

+ 9 - 9
cfc-web/src/views/teacher/TeacherOrders.vue

@@ -21,7 +21,7 @@
             <i class="el-icon-document"></i>
           </div>
           <div class="stat-content">
-            <div class="stat-value">¥{{ stats.totalRevenue }}</div>
+            <div class="stat-value">{{ formatPriceWithSymbol(stats.totalRevenue) }}</div>
             <div class="stat-label">累计收入</div>
           </div>
         </div>
@@ -32,7 +32,7 @@
             <i class="el-icon-circle-check"></i>
           </div>
           <div class="stat-content">
-            <div class="stat-value">¥{{ stats.completedRevenue }}</div>
+            <div class="stat-value">{{ formatPriceWithSymbol(stats.completedRevenue) }}</div>
             <div class="stat-label">已到账</div>
           </div>
         </div>
@@ -43,7 +43,7 @@
             <i class="el-icon-time"></i>
           </div>
           <div class="stat-content">
-            <div class="stat-value">¥{{ stats.pendingAmount }}</div>
+            <div class="stat-value">{{ formatPriceWithSymbol(stats.pendingAmount) }}</div>
             <div class="stat-label">待到账</div>
           </div>
         </div>
@@ -54,7 +54,7 @@
             <i class="el-icon-wallet"></i>
           </div>
           <div class="stat-content">
-            <div class="stat-value">¥{{ stats.actualCommission }}</div>
+            <div class="stat-value">{{ formatPriceWithSymbol(stats.actualCommission) }}</div>
             <div class="stat-label">实际佣金</div>
           </div>
         </div>
@@ -127,12 +127,12 @@
         </el-table-column>
         <el-table-column prop="amount" label="订单金额" width="120">
           <template slot-scope="scope">
-            <span class="amount">¥{{ scope.row.amount }}</span>
+            <span class="amount">{{ formatPriceWithSymbol(scope.row.amount) }}</span>
           </template>
         </el-table-column>
         <el-table-column prop="commission" label="佣金" width="100">
           <template slot-scope="scope">
-            <span class="commission">+¥{{ scope.row.commission || (scope.row.amount * 0.1).toFixed(2) }}</span>
+            <span class="commission">+{{ formatPriceWithSymbol(scope.row.commission || Math.round(scope.row.amount * 0.1)) }}</span>
           </template>
         </el-table-column>
         <el-table-column prop="status" label="状态" width="100">
@@ -170,11 +170,11 @@
         </div>
         <div class="detail-row">
           <span class="detail-label">订单金额</span>
-          <span class="detail-value price">¥{{ currentOrder.amount }}</span>
+          <span class="detail-value price">{{ formatPriceWithSymbol(currentOrder.amount) }}</span>
         </div>
         <div class="detail-row">
           <span class="detail-label">佣金</span>
-          <span class="detail-value commission">+¥{{ currentOrder.commission || (currentOrder.amount * 0.1).toFixed(2) }}</span>
+          <span class="detail-value commission">+{{ formatPriceWithSymbol(currentOrder.commission || Math.round(currentOrder.amount * 0.1)) }}</span>
         </div>
         <div class="detail-row">
           <span class="detail-label">订单状态</span>
@@ -247,7 +247,7 @@ export default {
       if (this.dateRange && this.dateRange.length === 2) {
         const [start, end] = this.dateRange
         result = result.filter(o => {
-          const date = o.createdAt?.split(' ')[0]
+          var date = (o.createdAt && o.createdAt.split) ? o.createdAt.split(' ')[0] : ''
           return date >= start && date <= end
         })
       }

+ 15 - 8
cfc-web/src/views/teacher/TeacherPackages.vue

@@ -43,7 +43,7 @@
             <i class="el-icon-price-tag"></i>
           </div>
           <div class="stat-info">
-            <div class="stat-value">¥{{ stats.avgPrice }}</div>
+            <div class="stat-value">{{ formatPriceWithSymbol(stats.avgPrice) }}</div>
             <div class="stat-label">平均价格</div>
           </div>
         </div>
@@ -54,7 +54,7 @@
             <i class="el-icon-wallet"></i>
           </div>
           <div class="stat-info">
-            <div class="stat-value">¥{{ stats.totalRevenue }}</div>
+            <div class="stat-value">{{ formatPriceWithSymbol(stats.totalRevenue) }}</div>
             <div class="stat-label">套餐收入</div>
           </div>
         </div>
@@ -86,12 +86,12 @@
         </el-table-column>
         <el-table-column prop="price" label="价格" width="120">
           <template slot-scope="scope">
-            <span class="price">¥{{ scope.row.price }}</span>
+            <span class="price">{{ formatPriceWithSymbol(scope.row.price) }}</span>
           </template>
         </el-table-column>
         <el-table-column prop="minPrice" label="最低价" width="100">
           <template slot-scope="scope">
-            <span class="min-price">¥{{ scope.row.minPrice || 0 }}</span>
+            <span class="min-price">{{ formatPriceWithSymbol(scope.row.minPrice || 0) }}</span>
           </template>
         </el-table-column>
         <el-table-column prop="salesCount" label="销量" width="100">
@@ -124,7 +124,7 @@
             <el-option v-for="t in templates" :key="t.id" :label="t.name" :value="t.id">
               <span style="display: flex; justify-content: space-between">
                 <span>{{ t.name }}</span>
-                <span style="color: #F59E0B">¥{{ t.referencePrice }}</span>
+                <span style="color: #F59E0B">{{ formatPriceWithSymbol(t.referencePrice) }}</span>
               </span>
             </el-option>
           </el-select>
@@ -148,11 +148,11 @@
           <span class="form-tip">个家庭</span>
         </el-form-item>
         <el-form-item label="销售价格">
-          <el-input v-model="form.price" type="number" placeholder="请输入销售价格" class="full-width">
+          <el-input v-model="form.priceYuan" type="number" placeholder="请输入销售价格(元)" class="full-width">
             <template slot="prepend">¥</template>
           </el-input>
           <div class="price-tip" v-if="selectedTemplate">
-            <i class="el-icon-info"></i> 系统建议最低价: ¥{{ selectedTemplate.minPrice }}
+            <i class="el-icon-info"></i> 系统建议最低价: {{ formatPriceWithSymbol(selectedTemplate.minPrice) }}
           </div>
         </el-form-item>
         <el-form-item label="套餐描述">
@@ -188,6 +188,7 @@ export default {
         serviceMonths: 12,
         serviceLimit: 50,
         price: null,
+        priceYuan: null,
         description: ''
       }
     }
@@ -237,6 +238,7 @@ export default {
       this.selectedTemplate = this.templates.find(t => t.id === val)
       if (this.selectedTemplate) {
         this.form.price = this.selectedTemplate.minPrice
+        this.form.priceYuan = this.selectedTemplate.minPrice ? (this.selectedTemplate.minPrice / 100) : null
         this.form.packageName = this.selectedTemplate.name
         this.form.level = this.selectedTemplate.level || 'junior'
         this.form.serviceLimit = this.selectedTemplate.defaultServiceLimit || 50
@@ -252,6 +254,7 @@ export default {
         serviceMonths: 12,
         serviceLimit: 50,
         price: null,
+        priceYuan: null,
         description: ''
       }
       this.selectedTemplate = null
@@ -267,6 +270,7 @@ export default {
         serviceMonths: row.serviceMonths || 12,
         serviceLimit: row.serviceLimit || 50,
         price: row.price,
+        priceYuan: row.price ? (row.price / 100) : null,
         description: row.description || ''
       }
       this.dialogVisible = true
@@ -281,6 +285,7 @@ export default {
         serviceMonths: row.serviceMonths || 12,
         serviceLimit: row.serviceLimit || 50,
         price: row.price,
+        priceYuan: row.price ? (row.price / 100) : null,
         description: row.description || ''
       }
       this.dialogVisible = true
@@ -295,10 +300,12 @@ export default {
       }
     },
     async handleSubmit() {
-      if (!this.form.packageName || !this.form.price) {
+      if (!this.form.packageName || !this.form.priceYuan) {
         this.$message.warning('请填写完整信息')
         return
       }
+      // Convert yuan to cents for submission
+      this.form.price = Math.round(Number(this.form.priceYuan) * 100)
       if (this.selectedTemplate && this.form.price < this.selectedTemplate.minPrice) {
         this.$message.warning('价格不能低于最低价')
         return

+ 7 - 7
cfc-web/src/views/teacher/TeacherTeam.vue

@@ -43,7 +43,7 @@
             <i class="el-icon-s-goods"></i>
           </div>
           <div class="stat-info">
-            <div class="stat-value">¥{{ teamStats.totalSales }}</div>
+            <div class="stat-value">{{ formatPriceWithSymbol(teamStats.totalSales) }}</div>
             <div class="stat-label">团队业绩</div>
           </div>
         </div>
@@ -54,7 +54,7 @@
             <i class="el-icon-wallet"></i>
           </div>
           <div class="stat-info">
-            <div class="stat-value">¥{{ teamStats.myCommission }}</div>
+            <div class="stat-value">{{ formatPriceWithSymbol(teamStats.myCommission) }}</div>
             <div class="stat-label">我的佣金</div>
           </div>
         </div>
@@ -115,17 +115,17 @@
         </el-table-column>
         <el-table-column prop="monthlySales" label="本月业绩" width="120">
           <template slot-scope="scope">
-            <span class="sales-value">¥{{ scope.row.monthlySales || 0 }}</span>
+            <span class="sales-value">{{ formatPriceWithSymbol(scope.row.monthlySales || 0) }}</span>
           </template>
         </el-table-column>
         <el-table-column prop="totalSales" label="累计业绩" width="120">
           <template slot-scope="scope">
-            <span class="total-value">¥{{ scope.row.totalSales || 0 }}</span>
+            <span class="total-value">{{ formatPriceWithSymbol(scope.row.totalSales || 0) }}</span>
           </template>
         </el-table-column>
         <el-table-column prop="commission" label="贡献佣金" width="100">
           <template slot-scope="scope">
-            <span class="commission-value">+¥{{ (scope.row.monthlySales * 0.1 || 0).toFixed(2) }}</span>
+            <span class="commission-value">+{{ formatPriceWithSymbol(Math.round((scope.row.monthlySales || 0) * 0.1)) }}</span>
           </template>
         </el-table-column>
         <el-table-column label="操作" width="120" fixed="right">
@@ -161,13 +161,13 @@
         <el-row :gutter="12">
           <el-col :span="12">
             <div class="mini-stat">
-              <div class="mini-stat-value" style="color: #3B82F6">¥{{ currentMember.monthlySales || 0 }}</div>
+              <div class="mini-stat-value" style="color: #3B82F6">{{ formatPriceWithSymbol(currentMember.monthlySales || 0) }}</div>
               <div class="mini-stat-label">本月业绩</div>
             </div>
           </el-col>
           <el-col :span="12">
             <div class="mini-stat">
-              <div class="mini-stat-value" style="color: #10B981">¥{{ currentMember.totalSales || 0 }}</div>
+              <div class="mini-stat-value" style="color: #10B981">{{ formatPriceWithSymbol(currentMember.totalSales || 0) }}</div>
               <div class="mini-stat-label">累计业绩</div>
             </div>
           </el-col>