Jelajahi Sumber

feat:添加停服公告和密码

FanHong 4 jam lalu
induk
melakukan
55cb24182b

+ 31 - 4
shudao-vue-frontend/src/router/index.js

@@ -32,13 +32,13 @@ const routes = [
         console.log('📱 检测到移动设备,重定向到 /mobile')
         console.log('📍 原始 URL:', window.location.href)
         console.log('📍 原始查询参数:', to.query)
-        
+
         // 重定向时保留所有查询参数(包括票据)
         // 使用 replace 而不是 push,避免在历史记录中留下 /
-        next({ 
-          path: '/mobile', 
+        next({
+          path: '/mobile',
           query: to.query,  // 保留查询参数
-          replace: true 
+          replace: true
         });
       } else {
         next();
@@ -144,6 +144,33 @@ const router = createRouter({
   routes
 })
 
+// 全局前置守卫 - 系统停机维护拦截
+const isMaintenance = true; // 是否开启停机维护模式
+const MAINTENANCE_BYPASS_STORAGE_KEY = 'maintenance_developer_bypass'
+
+const hasMaintenanceBypass = () => {
+  if (typeof window === 'undefined') return false
+  return window.sessionStorage.getItem(MAINTENANCE_BYPASS_STORAGE_KEY) === 'true'
+}
+
+router.beforeEach((to, from, next) => {
+  if (isMaintenance && !hasMaintenanceBypass()) {
+    // 允许访问的维护页面(即显示了公告的主界面)
+    const allowedPaths = ['/', '/mobile'];
+
+    if (!allowedPaths.includes(to.path)) {
+      console.log('🚧 系统维护中,拦截访问:', to.path);
+      // 根据设备类型重定向到对应的主界面(带上原有参数防丢失)
+      if (isMobile()) {
+        return next({ path: '/mobile', query: to.query, replace: true });
+      } else {
+        return next({ path: '/', query: to.query, replace: true });
+      }
+    }
+  }
+  next();
+})
+
 // ===== 注释掉:登录认证守卫已废弃,改用票据认证 =====
 // 全局前置守卫 - 登录认证
 // router.beforeEach((to, from, next) => {

+ 292 - 6
shudao-vue-frontend/src/views/Index.vue

@@ -1,5 +1,43 @@
 <template>
   <div class="container">
+    <!-- 停机维护公告弹窗 (新版样式) -->
+    <div class="maintenance-overlay" v-if="showMaintenance">
+      <div class="maintenance-banner">
+        <div class="mb-top-line"></div>
+        <div class="mb-content">
+          <div class="mb-title-row">
+            <h2 class="mb-title">系统升级维护公告</h2>
+            <button
+              type="button"
+              class="mb-secret-trigger"
+              aria-label="开发者入口"
+              @click.stop="toggleDeveloperUnlock"
+            >
+              <span class="mb-secret-icon">?</span>
+            </button>
+          </div>
+          <div v-if="showDeveloperUnlock" class="mb-dev-panel">
+            <input
+              v-model="developerPassword"
+              type="password"
+              class="mb-dev-input"
+              placeholder="请输入开发者密码"
+              @keyup.enter="confirmMaintenanceUnlock"
+            >
+            <button type="button" class="mb-dev-submit" @click="confirmMaintenanceUnlock">确认</button>
+          </div>
+          <p class="mb-desc">为进一步提升系统服务质量与运行稳定性,我单位定于以下时段进行系统升级维护,<br>届时部分业务服务将暂时无法访问,敬请谅解。</p>
+          <p class="mb-time">预计恢复时间2026年6月20日12:00</p>
+          <div class="mb-status-wrapper">
+            <div class="mb-status">
+              <span class="status-dot"></span>
+              维护中 · 部分服务暂不可用
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
     <!-- 顶部logo -->
     <div class="header">
       <div class="logo">
@@ -86,7 +124,7 @@
           </div>
         </div>
 
-        <!-- 第二列:隐患提示和AI助手 -->
+        <!-- 第二列:隐患提示和AI问答 -->
         <div class="card-column">
           <div class="hazard-card" @click="goToHazardDetection">
             <div class="card-title">隐患提示</div>
@@ -94,13 +132,13 @@
           </div>
           <div class="ai-chat-card" @click="goToAIChat">
             <div class="ai-chat-icon">
-              <img src="@/assets/new_index/chat-icon.png" alt="AI助手" class="chat-icon-img">
+              <img src="@/assets/new_index/chat-icon.png" alt="AI问答" class="chat-icon-img">
             </div>
             <div class="ai-chat-images">
               <img src="@/assets/new_index/chat-img-1.png" alt="对话1" class="chat-img chat-img-back">
               <img src="@/assets/new_index/chat-img-2.png" alt="对话2" class="chat-img chat-img-front">
             </div>
-            <div class="card-title">AI助手</div>
+            <div class="card-title">AI问答</div>
             <div class="card-description">AI对话助手,智能解答您的问题</div>
             
             <!-- 波浪效果 -->
@@ -220,6 +258,17 @@ const {
 } = useSpeechRecognition()
 
 // 响应式数据
+const MAINTENANCE_UNLOCK_PASSWORD = '888888'
+const MAINTENANCE_BYPASS_STORAGE_KEY = 'maintenance_developer_bypass'
+
+const getMaintenanceBypassEnabled = () => {
+  if (typeof window === 'undefined') return false
+  return window.sessionStorage.getItem(MAINTENANCE_BYPASS_STORAGE_KEY) === 'true'
+}
+
+const showMaintenance = ref(!getMaintenanceBypassEnabled()) // 控制停机维护公告显示
+const showDeveloperUnlock = ref(false)
+const developerPassword = ref('')
 const searchText = ref('')
 const showFeedbackModal = ref(false)
 const isSending = ref(false)
@@ -251,6 +300,28 @@ const refreshQuestions = () => {
   getRecommendQuestion()
 }
 
+const toggleDeveloperUnlock = () => {
+  showDeveloperUnlock.value = !showDeveloperUnlock.value
+  if (!showDeveloperUnlock.value) {
+    developerPassword.value = ''
+  }
+}
+
+const confirmMaintenanceUnlock = () => {
+  if (developerPassword.value !== MAINTENANCE_UNLOCK_PASSWORD) {
+    ElMessage.error('开发者密码错误')
+    return
+  }
+
+  if (typeof window !== 'undefined') {
+    window.sessionStorage.setItem(MAINTENANCE_BYPASS_STORAGE_KEY, 'true')
+  }
+  showMaintenance.value = false
+  showDeveloperUnlock.value = false
+  developerPassword.value = ''
+  ElMessage.success('已进入开发者放行模式')
+}
+
 // 清空推荐问题
 const clearRecommendQuestions = () => {
   userRecommendQuestions.value = []
@@ -389,7 +460,7 @@ const handleSearch = async () => {
   
   try {
     console.log('搜索内容:', searchText.value)
-    // 跳转到AI助手页面,并传递搜索内容
+    // 跳转到AI问答页面,并传递搜索内容
     router.push({
       path: '/chat',
       query: {
@@ -458,7 +529,7 @@ const getFeedbackType = (type) => {
 // 推荐问题点击跳转
 const goToAIQuestion = (question) => {
   console.log('点击问题:', question)
-  // 跳转到AI助手页面,并传递问题内容
+  // 跳转到AI问答页面,并传递问题内容
   router.push({
     path: '/chat',
     query: {
@@ -540,6 +611,221 @@ onUnmounted(() => {
 </script>
 
 <style lang="less" scoped>
+
+/* 停机维护公告弹窗 (新版样式) */
+.maintenance-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.4);
+  backdrop-filter: blur(4px);
+  z-index: 9999;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  animation: fadeIn 0.3s ease-out;
+}
+
+.maintenance-banner {
+  background: linear-gradient(180deg, #183582 0%, #0d1e57 100%);
+  border-radius: 16px;
+  overflow: hidden;
+  box-shadow: 0 20px 50px rgba(13, 30, 87, 0.5);
+  position: relative;
+  width: 90%;
+  max-width: 680px;
+  animation: scaleUp 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+}
+
+.mb-close-btn {
+  position: absolute;
+  top: 12px;
+  right: 16px;
+  font-size: 28px;
+  color: rgba(255, 255, 255, 0.6);
+  cursor: pointer;
+  line-height: 1;
+  transition: all 0.2s;
+  z-index: 10;
+}
+
+.mb-close-btn:hover {
+  color: #fff;
+  transform: scale(1.1);
+}
+
+.mb-top-line {
+  height: 6px;
+  background-color: #ef4444; /* 顶部红线 */
+  width: 100%;
+}
+
+.mb-content {
+  padding: 40px 30px 45px 30px;
+  text-align: center;
+  color: #ffffff;
+}
+
+.mb-title-row {
+  position: relative;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.mb-dept {
+  font-size: 14px;
+  color: rgba(255, 255, 255, 0.7);
+  margin-bottom: 20px;
+  letter-spacing: 2px;
+}
+
+.mb-title {
+  font-size: 38px;
+  font-weight: bold;
+  margin: 0 0 28px 0;
+  letter-spacing: 2px;
+  font-family: "STZhongsong", "SimSun", "Songti SC", serif; /* 宋体/明朝体风格 */
+  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
+}
+
+.mb-secret-trigger {
+  position: absolute;
+  top: 50%;
+  right: 0;
+  width: 28px;
+  height: 28px;
+  transform: translate(8px, -115%);
+  border: 1px solid rgba(255, 255, 255, 0.14);
+  border-radius: 50%;
+  background: rgba(255, 255, 255, 0.06);
+  cursor: pointer;
+  opacity: 0.2;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  padding: 0;
+}
+
+.mb-secret-trigger:hover,
+.mb-secret-trigger:focus-visible {
+  opacity: 0.38;
+  outline: none;
+}
+
+.mb-secret-icon {
+  font-size: 15px;
+  line-height: 1;
+  font-weight: 600;
+  color: rgba(255, 255, 255, 0.5);
+}
+
+.mb-dev-panel {
+  margin: -8px auto 20px;
+  width: min(100%, 360px);
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.mb-dev-input {
+  flex: 1;
+  height: 42px;
+  padding: 0 14px;
+  border: 1px solid rgba(255, 255, 255, 0.22);
+  border-radius: 10px;
+  background: rgba(255, 255, 255, 0.12);
+  color: #ffffff;
+  outline: none;
+}
+
+.mb-dev-input::placeholder {
+  color: rgba(255, 255, 255, 0.62);
+}
+
+.mb-dev-input:focus {
+  border-color: rgba(255, 255, 255, 0.48);
+}
+
+.mb-dev-submit {
+  height: 42px;
+  padding: 0 18px;
+  border: none;
+  border-radius: 10px;
+  background: #fdf8eb;
+  color: #b45309;
+  font-size: 14px;
+  font-weight: 700;
+  cursor: pointer;
+  white-space: nowrap;
+}
+
+.mb-dev-submit:hover {
+  filter: brightness(0.98);
+}
+
+.mb-desc {
+  font-size: 15px;
+  color: rgba(255, 255, 255, 0.9);
+  line-height: 1.8;
+  margin: 0 0 16px 0;
+  letter-spacing: 0.5px;
+}
+
+.mb-time {
+  font-size: 15px;
+  color: rgba(255, 255, 255, 0.9);
+  margin: 0 0 36px 0;
+  letter-spacing: 0.5px;
+  font-weight: 500;
+}
+
+.mb-status-wrapper {
+  display: flex;
+  justify-content: center;
+}
+
+.mb-status {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  background-color: #fdf8eb; /* 浅黄色背景 */
+  color: #b45309; /* 橙色文字 */
+  padding: 12px 32px;
+  border-radius: 30px;
+  font-size: 16px;
+  font-weight: bold;
+  gap: 10px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+  letter-spacing: 1px;
+}
+
+.status-dot {
+  width: 10px;
+  height: 10px;
+  background-color: #d1a374; /* 圆点颜色 */
+  border-radius: 50%;
+  animation: pulse 2s infinite;
+}
+
+@keyframes pulse {
+  0% { opacity: 1; transform: scale(1); }
+  50% { opacity: 0.5; transform: scale(0.8); }
+  100% { opacity: 1; transform: scale(1); }
+}
+
+@keyframes fadeIn {
+  from { opacity: 0; }
+  to { opacity: 1; }
+}
+
+@keyframes scaleUp {
+  from { opacity: 0; transform: scale(0.9); }
+  to { opacity: 1; transform: scale(1); }
+}
+
 .container {
   width: 100%;
   height: 100vh;
@@ -1005,7 +1291,7 @@ onUnmounted(() => {
   }
 }
 
-/* AI助手卡片 */
+/* AI问答卡片 */
 .ai-chat-card {
   background: #428EFE;
   border-radius: 16px;

+ 285 - 5
shudao-vue-frontend/src/views/mobile/m-Index.vue

@@ -1,5 +1,43 @@
 <template>
   <div class="mobile-container">
+    <!-- 停机维护公告弹窗 (新版样式) -->
+    <div class="maintenance-overlay" v-if="showMaintenance">
+      <div class="maintenance-banner">
+        <div class="mb-top-line"></div>
+        <div class="mb-content">
+          <div class="mb-title-row">
+            <h2 class="mb-title">系统升级维护公告</h2>
+            <button
+              type="button"
+              class="mb-secret-trigger"
+              aria-label="开发者入口"
+              @click.stop="toggleDeveloperUnlock"
+            >
+              <span class="mb-secret-icon">?</span>
+            </button>
+          </div>
+          <div v-if="showDeveloperUnlock" class="mb-dev-panel">
+            <input
+              v-model="developerPassword"
+              type="password"
+              class="mb-dev-input"
+              placeholder="请输入开发者密码"
+              @keyup.enter="confirmMaintenanceUnlock"
+            >
+            <button type="button" class="mb-dev-submit" @click="confirmMaintenanceUnlock">确认</button>
+          </div>
+          <p class="mb-desc">为进一步提升系统服务质量与运行稳定性,我单位定于以下时段进行系统升级维护,<br>届时部分业务服务将暂时无法访问,敬请谅解。</p>
+          <p class="mb-time">预计恢复时间2026年6月20日12:00</p>
+          <div class="mb-status-wrapper">
+            <div class="mb-status">
+              <span class="status-dot"></span>
+              维护中 · 部分服务暂不可用
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
     <!-- 顶部logo -->
     <div class="mobile-header">
       <div class="logo">
@@ -88,14 +126,14 @@
 
           <div class="mobile-service-item mobile-ai-chat-item" @click="goToAIChat">
             <div class="mobile-ai-chat-icon">
-              <img src="@/assets/new_index/chat-icon.png" alt="AI助手" class="mobile-chat-icon-img">
+              <img src="@/assets/new_index/chat-icon.png" alt="AI问答" class="mobile-chat-icon-img">
             </div>
             <div class="mobile-ai-chat-images">
               <img src="@/assets/new_index/chat-img-1.png" alt="对话1" class="mobile-chat-img mobile-chat-img-back">
               <img src="@/assets/new_index/chat-img-2.png" alt="对话2" class="mobile-chat-img mobile-chat-img-front">
             </div>
             <div class="mobile-service-info mobile-service-info-large">
-              <div class="mobile-service-title mobile-service-title-large">AI助手</div>
+              <div class="mobile-service-title mobile-service-title-large">AI问答</div>
               <div class="mobile-service-desc mobile-service-desc-large">AI对话助手,智能解答您的问题</div>
             </div>
             
@@ -214,6 +252,17 @@ const {
 } = useSpeechRecognition()
 
 // 响应式数据
+const MAINTENANCE_UNLOCK_PASSWORD = '888888'
+const MAINTENANCE_BYPASS_STORAGE_KEY = 'maintenance_developer_bypass'
+
+const getMaintenanceBypassEnabled = () => {
+  if (typeof window === 'undefined') return false
+  return window.sessionStorage.getItem(MAINTENANCE_BYPASS_STORAGE_KEY) === 'true'
+}
+
+const showMaintenance = ref(!getMaintenanceBypassEnabled()) // 控制停机维护公告显示
+const showDeveloperUnlock = ref(false)
+const developerPassword = ref('')
 const searchText = ref('')
 const showFeedbackModal = ref(false)
 const isSending = ref(false)
@@ -239,6 +288,28 @@ const refreshQuestions = () => {
   getRecommendQuestion()
 }
 
+const toggleDeveloperUnlock = () => {
+  showDeveloperUnlock.value = !showDeveloperUnlock.value
+  if (!showDeveloperUnlock.value) {
+    developerPassword.value = ''
+  }
+}
+
+const confirmMaintenanceUnlock = () => {
+  if (developerPassword.value !== MAINTENANCE_UNLOCK_PASSWORD) {
+    ElMessage.error('开发者密码错误')
+    return
+  }
+
+  if (typeof window !== 'undefined') {
+    window.sessionStorage.setItem(MAINTENANCE_BYPASS_STORAGE_KEY, 'true')
+  }
+  showMaintenance.value = false
+  showDeveloperUnlock.value = false
+  developerPassword.value = ''
+  ElMessage.success('已进入开发者放行模式')
+}
+
 // 语音输入相关方法
 const handleVoiceClick = () => {
   console.log('点击语音按钮')
@@ -281,7 +352,7 @@ const handleSearch = async () => {
   isSending.value = true
   try {
     console.log('搜索内容:', searchText.value)
-    // 跳转到AI助手页面,并传递搜索内容
+    // 跳转到AI问答页面,并传递搜索内容
     router.push({
       path: '/mobile/chat',
       query: {
@@ -350,7 +421,7 @@ const getFeedbackType = (type) => {
 // 推荐问题点击跳转
 const goToAIQuestion = (question) => {
   console.log('点击问题:', question)
-  // 跳转到AI助手页面,并传递问题内容
+  // 跳转到AI问答页面,并传递问题内容
   router.push({
     path: '/mobile/chat',
     query: {
@@ -443,6 +514,215 @@ onBeforeUnmount(() => {
 </script>
 
 <style lang="less" scoped>
+
+/* 停机维护公告弹窗 (新版样式 - 移动端适配) */
+.maintenance-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.4);
+  backdrop-filter: blur(4px);
+  z-index: 9999;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  animation: fadeIn 0.3s ease-out;
+}
+
+.maintenance-banner {
+  background: linear-gradient(180deg, #183582 0%, #0d1e57 100%);
+  border-radius: 12px;
+  overflow: hidden;
+  box-shadow: 0 15px 40px rgba(13, 30, 87, 0.5);
+  position: relative;
+  width: 90%;
+  max-width: 400px;
+  animation: scaleUp 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+}
+
+.mb-close-btn {
+  position: absolute;
+  top: 8px;
+  right: 12px;
+  font-size: 24px;
+  color: rgba(255, 255, 255, 0.6);
+  cursor: pointer;
+  line-height: 1;
+  transition: all 0.2s;
+  z-index: 10;
+}
+
+.mb-close-btn:active {
+  color: #fff;
+  transform: scale(1.1);
+}
+
+.mb-top-line {
+  height: 4px;
+  background-color: #ef4444; /* 顶部红线 */
+  width: 100%;
+}
+
+.mb-content {
+  padding: 30px 20px 35px 20px;
+  text-align: center;
+  color: #ffffff;
+}
+
+.mb-title-row {
+  position: relative;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.mb-dept {
+  font-size: 12px;
+  color: rgba(255, 255, 255, 0.7);
+  margin-bottom: 16px;
+  letter-spacing: 1px;
+}
+
+.mb-title {
+  font-size: 26px;
+  font-weight: bold;
+  margin: 0 0 20px 0;
+  letter-spacing: 1px;
+  font-family: "STZhongsong", "SimSun", "Songti SC", serif; /* 宋体/明朝体风格 */
+  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
+}
+
+.mb-secret-trigger {
+  position: absolute;
+  top: 50%;
+  right: 0;
+  width: 28px;
+  height: 28px;
+  transform: translate(6px, -105%);
+  border: 1px solid rgba(255, 255, 255, 0.14);
+  border-radius: 50%;
+  background: rgba(255, 255, 255, 0.06);
+  opacity: 0.24;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  padding: 0;
+}
+
+.mb-secret-trigger:active,
+.mb-secret-trigger:focus-visible {
+  opacity: 0.42;
+  outline: none;
+}
+
+.mb-secret-icon {
+  font-size: 15px;
+  line-height: 1;
+  font-weight: 600;
+  color: rgba(255, 255, 255, 0.54);
+}
+
+.mb-dev-panel {
+  margin: -6px auto 18px;
+  width: 100%;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.mb-dev-input {
+  flex: 1;
+  height: 38px;
+  padding: 0 12px;
+  border: 1px solid rgba(255, 255, 255, 0.22);
+  border-radius: 10px;
+  background: rgba(255, 255, 255, 0.12);
+  color: #ffffff;
+  outline: none;
+}
+
+.mb-dev-input::placeholder {
+  color: rgba(255, 255, 255, 0.62);
+}
+
+.mb-dev-input:focus {
+  border-color: rgba(255, 255, 255, 0.48);
+}
+
+.mb-dev-submit {
+  height: 38px;
+  padding: 0 14px;
+  border: none;
+  border-radius: 10px;
+  background: #fdf8eb;
+  color: #b45309;
+  font-size: 13px;
+  font-weight: 700;
+  white-space: nowrap;
+}
+
+.mb-desc {
+  font-size: 13px;
+  color: rgba(255, 255, 255, 0.9);
+  line-height: 1.6;
+  margin: 0 0 12px 0;
+  letter-spacing: 0.5px;
+}
+
+.mb-time {
+  font-size: 13px;
+  color: rgba(255, 255, 255, 0.9);
+  margin: 0 0 24px 0;
+  letter-spacing: 0.5px;
+  font-weight: 500;
+}
+
+.mb-status-wrapper {
+  display: flex;
+  justify-content: center;
+}
+
+.mb-status {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  background-color: #fdf8eb; /* 浅黄色背景 */
+  color: #b45309; /* 橙色文字 */
+  padding: 10px 24px;
+  border-radius: 24px;
+  font-size: 14px;
+  font-weight: bold;
+  gap: 8px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+  letter-spacing: 1px;
+}
+
+.status-dot {
+  width: 8px;
+  height: 8px;
+  background-color: #d1a374; /* 圆点颜色 */
+  border-radius: 50%;
+  animation: pulse 2s infinite;
+}
+
+@keyframes pulse {
+  0% { opacity: 1; transform: scale(1); }
+  50% { opacity: 0.5; transform: scale(0.8); }
+  100% { opacity: 1; transform: scale(1); }
+}
+
+@keyframes fadeIn {
+  from { opacity: 0; }
+  to { opacity: 1; }
+}
+
+@keyframes scaleUp {
+  from { opacity: 0; transform: scale(0.9); }
+  to { opacity: 1; transform: scale(1); }
+}
+
 .mobile-container {
   width: 100%;
   min-height: 100vh;
@@ -890,7 +1170,7 @@ onBeforeUnmount(() => {
     }
   }
   
-  // AI助手卡片(第2个)- 使用蓝色渐变背景,与Web端一致
+  // AI问答卡片(第2个)- 使用蓝色渐变背景,与Web端一致
   .mobile-ai-chat-item {
     background: #428EFE !important;
     background-image: none !important;