hookehuyr

feat(登录): 实现登录模块及访问控制功能

添加登录页面和戒子详情页面,实现手机号验证码登录功能
在路由守卫中添加登录状态校验,未登录用户访问受保护路由将被重定向
更新API接口添加登录相关方法,移除旧的用户存储实现
在首页添加照片查询入口,根据登录状态跳转不同页面
...@@ -154,6 +154,22 @@ API 请求基于 Axios 封装,配置文件位于 `src/utils/request.js`,支 ...@@ -154,6 +154,22 @@ API 请求基于 Axios 封装,配置文件位于 `src/utils/request.js`,支
154 - **LoadingSpinner** - 加载动画组件 154 - **LoadingSpinner** - 加载动画组件
155 - **EmptyState** - 空状态组件 155 - **EmptyState** - 空状态组件
156 156
157 +## 登录模块(Mock)
158 +
159 +- 页面:`src/views/Login.vue`
160 +- 功能:手机号 + 验证码登录;发送验证码含 60 秒倒计时;登录成功写入 Pinia 并跳转首页。
161 +- 接口:
162 + - `send_sms_mock(phone)`:发送验证码,模拟成功返回(约 500ms)。
163 + - `login_mock(phone, code)`:验证码登录,返回 `token``userInfo`(约 600ms)。
164 +- 说明:暂不加样式,待设计稿出具后统一视觉优化。
165 +
166 +## 访问控制(登录态)
167 +
168 +- 入口逻辑:首页“照片查询和下载”入口点击时将根据登录态跳转。
169 + - 未登录:跳转 `#/login`
170 + - 已登录:跳转 `#/studentInfo`
171 +- 路由守卫:对 `#/studentInfo` 做登录校验,未登录访问将被重定向到登录页并附带原始目标路径,登录后可返回。
172 +
157 ## 开发规范 173 ## 开发规范
158 174
159 ### 代码风格 175 ### 代码风格
......
1 /* 1 /*
2 * @Date: 2022-06-17 14:54:29 2 * @Date: 2022-06-17 14:54:29
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2023-06-25 09:33:28 4 + * @LastEditTime: 2025-11-11 09:19:52
5 - * @FilePath: /huizhu/src/api/common.js 5 + * @FilePath: /stdj_h5/src/api/common.js
6 * @Description: 通用接口 6 * @Description: 通用接口
7 */ 7 */
8 import { fn, fetch, uploadFn } from '@/api/fn'; 8 import { fn, fetch, uploadFn } from '@/api/fn';
......
1 /* 1 /*
2 * @Date: 2022-05-18 22:56:08 2 * @Date: 2022-05-18 22:56:08
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-11-04 21:18:39 4 + * @LastEditTime: 2025-11-11 13:31:09
5 * @FilePath: /stdj_h5/src/api/fn.js 5 * @FilePath: /stdj_h5/src/api/fn.js
6 * @Description: 文件描述 6 * @Description: 文件描述
7 */ 7 */
...@@ -22,7 +22,7 @@ export const fn = (api) => { ...@@ -22,7 +22,7 @@ export const fn = (api) => {
22 } else { 22 } else {
23 // tslint:disable-next-line: no-console 23 // tslint:disable-next-line: no-console
24 console.warn(res); 24 console.warn(res);
25 - if (!res.data.show) return false; 25 + // if (!res.data.show) return false;
26 showFailToast(res.data.msg); 26 showFailToast(res.data.msg);
27 return false; 27 return false;
28 } 28 }
......
...@@ -11,6 +11,7 @@ const Api = { ...@@ -11,6 +11,7 @@ const Api = {
11 GET_HOME: '/srv/?a=home', 11 GET_HOME: '/srv/?a=home',
12 GET_ARTICLE: '/srv/?a=get_article', 12 GET_ARTICLE: '/srv/?a=get_article',
13 GET_ARTICLE_FILE: '/srv/?a=get_article&t=file', 13 GET_ARTICLE_FILE: '/srv/?a=get_article&t=file',
14 + POST_LOGIN: '/srv/?a=login',
14 }; 15 };
15 16
16 /** 17 /**
...@@ -86,3 +87,16 @@ export const getImgStreamAPI = (params) => fn(fetch.get(Api.GET_ARTICLE_FILE, pa ...@@ -86,3 +87,16 @@ export const getImgStreamAPI = (params) => fn(fetch.get(Api.GET_ARTICLE_FILE, pa
86 * @property string data.file_list.photo.value 照片地址 87 * @property string data.file_list.photo.value 照片地址
87 */ 88 */
88 export const getArticleDetailAPI = (params) => fn(fetch.get(Api.GET_ARTICLE, params)); 89 export const getArticleDetailAPI = (params) => fn(fetch.get(Api.GET_ARTICLE, params));
90 +
91 +/**
92 + * @description: 登录接口
93 + * @param {String} phone 手机号
94 + * @param {String} code 验证码
95 + * @returns {Object} data
96 + * @property string data.token 登录凭证
97 + * @property object data.user 用户信息
98 + * @property string data.user.id 用户id
99 + * @property string data.user.nickname 昵称
100 + * @property string data.user.avatar 头像
101 + */
102 +export const loginAPI = (params) => fn(fetch.post(Api.POST_LOGIN, params));
......
1 /* 1 /*
2 * @Date: 2025-10-30 10:29:15 2 * @Date: 2025-10-30 10:29:15
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-11-04 20:52:25 4 + * @LastEditTime: 2025-11-11 12:56:04
5 * @FilePath: /stdj_h5/src/router/index.js 5 * @FilePath: /stdj_h5/src/router/index.js
6 * @Description: 文件描述 6 * @Description: 文件描述
7 */ 7 */
8 import { createRouter, createWebHashHistory } from 'vue-router' 8 import { createRouter, createWebHashHistory } from 'vue-router'
9 import { useLoadingStore } from '@/stores/loading.js' 9 import { useLoadingStore } from '@/stores/loading.js'
10 +import Cookies from 'js-cookie'
10 11
11 const routes = [ 12 const routes = [
12 { 13 {
...@@ -40,6 +41,16 @@ const routes = [ ...@@ -40,6 +41,16 @@ const routes = [
40 component: () => import('../views/Students.vue') 41 component: () => import('../views/Students.vue')
41 }, 42 },
42 { 43 {
44 + path: '/studentInfo',
45 + name: '戒子详情',
46 + component: () => import('../views/StudentInfo.vue')
47 + },
48 + {
49 + path: '/login',
50 + name: '登录',
51 + component: () => import('../views/Login.vue')
52 + },
53 + {
43 path: '/volunteers', 54 path: '/volunteers',
44 name: '义工', 55 name: '义工',
45 component: () => import('../views/Volunteers.vue') 56 component: () => import('../views/Volunteers.vue')
...@@ -86,7 +97,26 @@ router.beforeEach((to, from, next) => { ...@@ -86,7 +97,26 @@ router.beforeEach((to, from, next) => {
86 void e 97 void e
87 } 98 }
88 99
89 - // 这里可以添加权限验证逻辑 100 + /**
101 + * 访问控制:仅在进入 /studentInfo 时校验登录状态
102 + * 说明:优先读取 Cookie 中的 token;若不存在则回退读取本地存储 token,避免重复登录。
103 + */
104 + const has_token_cookie = !!Cookies.get('token-stdj')
105 + const is_login = has_token_cookie
106 +
107 + if (to.path === '/studentInfo' && !is_login) {
108 + // 关键:在重定向前关闭或重置上一跳的loading,避免计数残留
109 + try {
110 + const loading = useLoadingStore()
111 + loading.reset()
112 + } catch (e) {
113 + void e
114 + }
115 + next({ name: '登录', query: { redirect: to.fullPath } })
116 + return
117 + }
118 +
119 + // 其他页面不做登录校验
90 next() 120 next()
91 }) 121 })
92 122
......
1 -/*
2 - * @Date: 2025-10-30 10:30:17
3 - * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-10-30 10:30:26
5 - * @FilePath: /itomix/h5_vite_template/src/stores/user.js
6 - * @Description: 文件描述
7 - */
8 -import { defineStore } from 'pinia'
9 -import { userApi } from '@/api'
10 -
11 -export const useUserStore = defineStore('user', {
12 - state: () => ({
13 - userInfo: null,
14 - token: localStorage.getItem('token') || '',
15 - isLogin: false
16 - }),
17 -
18 - getters: {
19 - // 获取用户名
20 - username: (state) => state.userInfo?.username || '',
21 -
22 - // 获取用户头像
23 - avatar: (state) => state.userInfo?.avatar || '',
24 -
25 - // 是否已登录
26 - hasLogin: (state) => !!state.token && !!state.userInfo
27 - },
28 -
29 - actions: {
30 - // 设置 token
31 - setToken(token) {
32 - this.token = token
33 - localStorage.setItem('token', token)
34 - },
35 -
36 - // 清除 token
37 - clearToken() {
38 - this.token = ''
39 - localStorage.removeItem('token')
40 - },
41 -
42 - // 设置用户信息
43 - setUserInfo(userInfo) {
44 - this.userInfo = userInfo
45 - this.isLogin = true
46 - },
47 -
48 - // 清除用户信息
49 - clearUserInfo() {
50 - this.userInfo = null
51 - this.isLogin = false
52 - },
53 -
54 - // 登录
55 - async login(loginData) {
56 - const response = await userApi.login(loginData)
57 - const { token, userInfo } = response.data
58 -
59 - this.setToken(token)
60 - this.setUserInfo(userInfo)
61 -
62 - return response
63 - },
64 -
65 - // 获取用户信息
66 - async getUserInfo() {
67 - try {
68 - const response = await userApi.getUserInfo()
69 - this.setUserInfo(response.data)
70 - return response
71 - } catch (error) {
72 - // 如果获取用户信息失败,清除本地存储
73 - this.logout()
74 - throw error
75 - }
76 - },
77 -
78 - // 登出
79 - async logout() {
80 - try {
81 - await userApi.logout()
82 - } catch (error) {
83 - console.error('登出失败:', error)
84 - } finally {
85 - this.clearToken()
86 - this.clearUserInfo()
87 - }
88 - }
89 - }
90 -})
...\ No newline at end of file ...\ No newline at end of file
...@@ -121,6 +121,15 @@ ...@@ -121,6 +121,15 @@
121 </div> 121 </div>
122 </div> 122 </div>
123 123
124 + <div style="background: #E5DAC2; padding-bottom: 1rem;">
125 + <div class="photo-section" @click="handlePhotoClick">
126 + <div class="photo-content">
127 + <div class="photo-title">照片查询下载</div>
128 + <div class="more-button"><span class="more-text">身份验证</span></div>
129 + </div>
130 + </div>
131 + </div>
132 +
124 <!-- 相关新闻 --> 133 <!-- 相关新闻 -->
125 <div class="news-section"> 134 <div class="news-section">
126 <!-- 标题 --> 135 <!-- 标题 -->
...@@ -175,6 +184,7 @@ import VideoPlayer from '@/components/VideoPlayer.vue' ...@@ -175,6 +184,7 @@ import VideoPlayer from '@/components/VideoPlayer.vue'
175 import { useTitle } from '@vueuse/core'; 184 import { useTitle } from '@vueuse/core';
176 import { useRouter } from 'vue-router' 185 import { useRouter } from 'vue-router'
177 import { showToast } from 'vant' 186 import { showToast } from 'vant'
187 +import Cookies from 'js-cookie'
178 188
179 // 导入接口 189 // 导入接口
180 import { homePageAPI } from '@/api/index.js' 190 import { homePageAPI } from '@/api/index.js'
...@@ -637,6 +647,23 @@ const showSwipeHint = ref(true) ...@@ -637,6 +647,23 @@ const showSwipeHint = ref(true)
637 const dismissSwipeHint = () => { 647 const dismissSwipeHint = () => {
638 showSwipeHint.value = false 648 showSwipeHint.value = false
639 } 649 }
650 +
651 +/**
652 + * 处理照片入口点击
653 + * 说明:未登录跳转登录页;已登录进入戒子信息页面。
654 + * @returns {void}
655 + */
656 +// 登录状态获取
657 +const hasLogin = computed(() => Boolean(Cookies.get('token-stdj')))
658 +
659 +const handlePhotoClick = () => {
660 + // 判断是否已登录
661 + if (hasLogin.value) {
662 + router.push('/studentInfo')
663 + } else {
664 + router.push('/login')
665 + }
666 +}
640 </script> 667 </script>
641 668
642 <style scoped> 669 <style scoped>
...@@ -735,6 +762,50 @@ const dismissSwipeHint = () => { ...@@ -735,6 +762,50 @@ const dismissSwipeHint = () => {
735 background: #E5DAC2; 762 background: #E5DAC2;
736 } 763 }
737 764
765 +/* 照片查询下载模块样式 */
766 +.photo-section {
767 + margin: 0 1rem;
768 + height: 3.5rem;
769 + border-radius: 1rem;
770 + background-image: url('https://cdn.ipadbiz.cn/stdj/images/%E7%85%A7%E7%89%87%E4%B8%8B%E8%BD%BD@2x.png');
771 + background-size: cover;
772 + background-position: center;
773 + background-repeat: no-repeat;
774 + display: flex;
775 + align-items: center;
776 + justify-content: center;
777 + position: relative;
778 +}
779 +
780 +.photo-content {
781 + width: 100%;
782 + padding: 0 1.5rem;
783 + display: flex;
784 + align-items: center;
785 + justify-content: space-between;
786 +}
787 +
788 +.photo-title {
789 + color: #FFFFFF;
790 + font-size: 1rem; /* 约18px,符合移动端标题视觉 */
791 + font-weight: 600;
792 + text-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.35);
793 +}
794 +
795 +.photo-section .more-button {
796 + position: absolute;
797 + top: 50%;
798 + right: 1.5rem;
799 + left: auto;
800 + bottom: auto;
801 + height: 2rem; /* 复用查看更多按钮风格并调整高度 */
802 + transform: translateY(-50%);
803 +}
804 +
805 +.photo-section .more-text {
806 + font-size: 0.75rem;
807 +}
808 +
738 /* 通用标题样式 */ 809 /* 通用标题样式 */
739 .common-title { 810 .common-title {
740 text-align: center; 811 text-align: center;
......
1 +<!--
2 + * @Date: 2025-11-10 18:08:59
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-11-11 13:31:30
5 + * @FilePath: /stdj_h5/src/views/Login.vue
6 + * @Description: 登录页
7 +-->
8 +<template>
9 + <div class="login-page">
10 + <!-- 顶部LOGO标题 -->
11 + <div class="logo-title">
12 + <img class="logo-img" src="https://cdn.ipadbiz.cn/stdj/images/logo@2x.png" alt="Logo">
13 + </div>
14 +
15 + <!-- 戒子身份验证容器 -->
16 + <div class="auth-card">
17 + <div class="card-title">戒子身份验证</div>
18 +
19 + <!-- 登录表单:手机号 -->
20 + <div class="form-item">
21 + <div class="input-with-icon phone">
22 + <input class="input" type="tel" placeholder="请输入手机号" v-model="phone" maxlength="11" />
23 + </div>
24 + </div>
25 +
26 + <!-- 登录表单:验证码 + 获取验证码 -->
27 + <div class="form-item">
28 + <div class="code-row">
29 + <div class="input-with-icon code">
30 + <input class="input" type="tel" placeholder="请输入验证码" v-model="code" maxlength="6" />
31 + </div>
32 + <div class="btn send" :class="{ disabled: send_disabled }" @click="on_click_send_sms">
33 + <span v-if="countdown === 0">获取验证码</span>
34 + <span v-else>{{ countdown }}s后重试</span>
35 + </div>
36 + </div>
37 + </div>
38 +
39 + <!-- 立即验证按钮 -->
40 + <div class="form-item">
41 + <div class="btn primary" :class="{ disabled: login_disabled }" @click="on_click_login">立即验证</div>
42 + </div>
43 + </div>
44 + </div>
45 +</template>
46 +
47 +<script setup>
48 +import { ref, onUnmounted, computed } from 'vue'
49 +import { useRoute, useRouter } from 'vue-router'
50 +import { useTitle } from '@vueuse/core'
51 +import { smsAPI } from '@/api/common.js'
52 +import { loginAPI } from '@/api/index.js'
53 +import { showToast, showFailToast, showSuccessToast } from 'vant'
54 +import Cookies from 'js-cookie'
55 +
56 +useTitle('戒子身份验证')
57 +
58 +const route = useRoute()
59 +const router = useRouter()
60 +
61 +// 表单状态
62 +const phone = ref('')
63 +const code = ref('')
64 +const countdown = ref(0)
65 +const timer_id = ref(null)
66 +const sending = ref(false)
67 +const logging = ref(false)
68 +
69 +/**
70 + * 发送验证码按钮禁用条件
71 + * 说明:倒计时进行中或正在发送或手机号无效时不可操作
72 + */
73 +const send_disabled = computed(function () {
74 + return countdown.value > 0 || sending.value || !is_valid_phone(phone.value)
75 +})
76 +
77 +/**
78 + * 登录按钮禁用条件
79 + * 说明:登录进行中或手机号/验证码未通过校验时不可操作
80 + */
81 +const login_disabled = computed(function () {
82 + return (
83 + logging.value ||
84 + !is_valid_phone(phone.value) ||
85 + !/^\d{4,6}$/.test(String(code.value || '').trim())
86 + )
87 +})
88 +
89 +/**
90 + * 点击发送验证码包装函数
91 + * 说明:在禁用态下不触发真实发送逻辑
92 + * @returns {void}
93 + */
94 +const on_click_send_sms = function () {
95 + if (send_disabled.value) return
96 + on_send_sms()
97 +}
98 +
99 +/**
100 + * 点击登录包装函数
101 + * 说明:在禁用态下不触发真实登录逻辑
102 + * @returns {void}
103 + */
104 +const on_click_login = function () {
105 + if (login_disabled.value) return
106 + on_login()
107 +}
108 +
109 +/**
110 + * 校验手机号格式
111 + * 说明:仅支持中国大陆11位手机号校验
112 + * @param {string} v 手机号
113 + * @returns {boolean} 是否有效
114 + */
115 +const is_valid_phone = function (v) {
116 + return /^1\d{10}$/.test(String(v || '').trim())
117 +}
118 +
119 +/**
120 + * 启动60秒倒计时
121 + * 说明:发送验证码成功后启动,期间禁用发送按钮
122 + * @returns {void}
123 + */
124 +const start_countdown = function () {
125 + countdown.value = 60
126 + if (timer_id.value) {
127 + clearInterval(timer_id.value)
128 + timer_id.value = null
129 + }
130 + timer_id.value = setInterval(function () {
131 + countdown.value = countdown.value - 1
132 + if (countdown.value <= 0) {
133 + clearInterval(timer_id.value)
134 + timer_id.value = null
135 + countdown.value = 0
136 + }
137 + }, 1000)
138 +}
139 +
140 +/**
141 + * 发送验证码
142 + * 说明:调用接口,成功后开始倒计时
143 + * @returns {Promise<void>}
144 + */
145 +const on_send_sms = async function () {
146 + if (sending.value || countdown.value > 0) return
147 + if (!is_valid_phone(phone.value)) {
148 + showFailToast('请输入有效的手机号')
149 + return
150 + }
151 + try {
152 + sending.value = true
153 + const { code } = await smsAPI({ phone: phone.value })
154 + if (code) {
155 + showToast('验证码已发送')
156 + start_countdown()
157 + }
158 + } catch (e) {
159 + showFailToast('网络异常,请稍后重试')
160 + } finally {
161 + sending.value = false
162 + }
163 +}
164 +
165 +/**
166 + * 登录
167 + * @returns {Promise<void>}
168 + */
169 +const on_login = async function () {
170 + if (logging.value) return
171 + if (!is_valid_phone(phone.value)) {
172 + showFailToast('请输入有效的手机号')
173 + return
174 + }
175 + if (!/^\d{4,6}$/.test(String(code.value || '').trim())) {
176 + showFailToast('请输入4~6位数字验证码')
177 + return
178 + }
179 + try {
180 + logging.value = true
181 + const { code, data } = await loginAPI({ phone: phone.value, code: code.value })
182 + if (code) {
183 + // 登录成功后,将token存储到cookie中
184 + Cookies.set('token-stdj', data.token, { expires: 7 })
185 + showSuccessToast('登录成功')
186 + // 跳转戒子详情页
187 + router.replace({ path: route.query.redirect })
188 + }
189 + } catch (e) {
190 + showFailToast('网络异常,请稍后重试')
191 + } finally {
192 + logging.value = false
193 + }
194 +}
195 +
196 +// 组件卸载时清理计时器
197 +onUnmounted(function () {
198 + if (timer_id.value) {
199 + clearInterval(timer_id.value)
200 + timer_id.value = null
201 + }
202 +})
203 +</script>
204 +
205 +<style lang="less" scoped>
206 +// 页面背景与布局
207 +.login-page {
208 + min-height: 100vh;
209 + background-color: #FCF8F1; // 整个页面背景色
210 + display: flex;
211 + flex-direction: column;
212 + align-items: center;
213 + padding: 6rem 1rem;
214 + background-image: url('https://cdn.ipadbiz.cn/stdj/images/bg002@2x.png');
215 + background-size: cover;
216 + background-position: center;
217 +}
218 +
219 +// 顶部Logo标题
220 +.logo-title {
221 + margin-bottom: 2rem;
222 +}
223 +.logo-img {
224 + height: 5.5rem;
225 + object-fit: contain;
226 + margin-top: 1rem;
227 +}
228 +
229 +// 验证容器
230 +.auth-card {
231 + max-width: 26rem;
232 + background-color: rgba(174, 155, 99, 0.14); // 容器背景色
233 + border-radius: 0.75rem;
234 + padding: 1rem;
235 + box-shadow: 0 0.25rem 1rem rgba(0, 0, 0, 0.06);
236 +}
237 +.card-title {
238 + text-align: center;
239 + color: #432C0E;
240 + font-size: 1.125rem;
241 + font-weight: 700;
242 + margin-top: 0.75rem;
243 + margin-bottom: 1.25rem;
244 +}
245 +
246 +// 表单项布局
247 +.form-item {
248 + margin-top: 1.25rem;
249 + margin-bottom: 0.75rem;
250 +}
251 +.code-row {
252 + display: flex;
253 + gap: 0.5rem;
254 + align-items: center;
255 +}
256 +
257 +// 输入框:带左侧图标
258 +.input-with-icon {
259 + position: relative;
260 +}
261 +.input-with-icon::before {
262 + content: '';
263 + position: absolute;
264 + left: 0.75rem;
265 + top: 50%;
266 + width: 1rem;
267 + height: 1rem;
268 + background-size: contain;
269 + background-position: center;
270 + background-repeat: no-repeat;
271 + transform: translateY(-50%);
272 +}
273 +.input-with-icon.phone::before {
274 + background-image: url('https://cdn.ipadbiz.cn/stdj/images/%E6%89%8B%E6%9C%BA@2x.png');
275 +}
276 +.input-with-icon.code::before {
277 + background-image: url('https://cdn.ipadbiz.cn/stdj/images/%E9%AA%8C%E8%AF%81%E7%A0%81@2x-1.png');
278 +}
279 +
280 +.input {
281 + width: 100%;
282 + height: 2.5rem;
283 + border-radius: 0.5rem;
284 + border: none;
285 + background-color: #FFFFFF;
286 + padding: 0 0.75rem 0 2.25rem; // 为左侧图标预留空间
287 + box-shadow: inset 0 0 0 0.0625rem rgba(0, 0, 0, 0.08);
288 +}
289 +.input::placeholder {
290 + color: #999999;
291 +}
292 +
293 +// 按钮样式
294 +.btn {
295 + height: 2.5rem;
296 + padding: 0 0.75rem;
297 + border: none;
298 + border-radius: 0.5rem;
299 + background-color: #A67939; // 获取验证码与立即验证背景色
300 + color: #FFFFFF;
301 + font-weight: 600;
302 + text-align: center;
303 + line-height: 2.5rem;
304 +}
305 +.btn.send {
306 + white-space: nowrap;
307 +}
308 +.btn.primary {
309 + width: 100%;
310 +}
311 +.btn.disabled {
312 + // 不可操作态:明显的灰显与禁止样式
313 + opacity: 0.5;
314 + cursor: not-allowed;
315 + pointer-events: none;
316 + filter: grayscale(30%);
317 + box-shadow: none;
318 +}
319 +</style>
This diff is collapsed. Click to expand it.