login-sheet.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  1. <template>
  2. <view class="login-sheet-overlay" :class="{ 'login-sheet-overlay--visible': visible }" @click="handleOverlayClick">
  3. <view class="login-sheet-panel" :class="{ 'login-sheet-panel--show': panelShow }" @click.stop>
  4. <view class="login-sheet-header">
  5. <view class="login-sheet-handle" aria-hidden="true"></view>
  6. <view class="login-sheet-title">欢迎回来</view>
  7. </view>
  8. <scroll-view scroll-y class="login-sheet-scroll" :show-scrollbar="false">
  9. <view class="login-sheet-content">
  10. <!-- 品牌区:logo / 品牌名 / 身份 / 主打句(记忆点) -->
  11. <view class="brand-section">
  12. <image class="logo" src="/static/logo.png" mode="aspectFit" @error="handleImageError"></image>
  13. <text class="app-name">浠艾福</text>
  14. <text class="identity">您的专属家庭主动健康管理师</text>
  15. <text class="slogan">家里每个人的状态,你真的<text class="slogan-accent">看见了?</text></text>
  16. </view>
  17. <!-- 理念卡:四步闭环(细边框表面区分,无单侧彩条) -->
  18. <view class="concept-card">
  19. <view class="concept-desc">
  20. 不等问题发生,先一步<text class="concept-highlight">识别风险 · 主动干预 · 持续评估</text>,把全家健康管理融入每一天。
  21. </view>
  22. <view class="loop-row">
  23. <view class="loop-step">
  24. <view class="loop-glyph loop-glyph-wisdom" aria-hidden="true"></view>
  25. <text class="loop-step-text">提前识别</text>
  26. </view>
  27. <view class="loop-connector" aria-hidden="true"></view>
  28. <view class="loop-step">
  29. <view class="loop-glyph loop-glyph-success" aria-hidden="true"></view>
  30. <text class="loop-step-text">风险管控</text>
  31. </view>
  32. <view class="loop-connector" aria-hidden="true"></view>
  33. <view class="loop-step">
  34. <view class="loop-glyph loop-glyph-wealth" aria-hidden="true"></view>
  35. <text class="loop-step-text">主动干预</text>
  36. </view>
  37. <view class="loop-connector" aria-hidden="true"></view>
  38. <view class="loop-step">
  39. <view class="loop-glyph loop-glyph-danger" aria-hidden="true"></view>
  40. <text class="loop-step-text">效果评估</text>
  41. </view>
  42. </view>
  43. </view>
  44. <!-- 五维标签带:统一描边容器 + 细分割线,色块仅作身份记号(非等权重填充 pills) -->
  45. <view class="dims-band">
  46. <view class="dim-cell">
  47. <view class="dim-glyph dim-glyph-body" aria-hidden="true"></view>
  48. <text class="dim-label">身·健康</text>
  49. </view>
  50. <view class="dim-cell">
  51. <view class="dim-glyph dim-glyph-wisdom" aria-hidden="true"></view>
  52. <text class="dim-label">智·认知</text>
  53. </view>
  54. <view class="dim-cell">
  55. <view class="dim-glyph dim-glyph-wealth" aria-hidden="true"></view>
  56. <text class="dim-label">富·创富</text>
  57. </view>
  58. <view class="dim-cell">
  59. <view class="dim-glyph dim-glyph-action" aria-hidden="true"></view>
  60. <text class="dim-label">行·和谐</text>
  61. </view>
  62. <view class="dim-cell">
  63. <view class="dim-glyph dim-glyph-mind" aria-hidden="true"></view>
  64. <text class="dim-label">心·大爱</text>
  65. </view>
  66. </view>
  67. <!-- 登录表单 -->
  68. <view class="login-form">
  69. <view class="phone-section">
  70. <button
  71. class="phone-btn"
  72. :class="{ 'phone-btn--error': errorState, 'phone-btn--success': successState }"
  73. open-type="getPhoneNumber"
  74. @getphonenumber="getPhoneNumber"
  75. @tap="onPhoneBtnTap"
  76. :loading="loading"
  77. :disabled="!agreed || loading || tapLock"
  78. >
  79. <view class="phone-btn-icon" aria-hidden="true"></view>
  80. <text>手机号快捷登录</text>
  81. </button>
  82. </view>
  83. <view class="agreement">
  84. <checkbox-group @change="onAgreementChange">
  85. <label>
  86. <checkbox value="agree" :checked="agreed" />
  87. <text class="agreement-text">我已阅读并同意</text>
  88. </label>
  89. </checkbox-group>
  90. <text class="link" @click="showAgreement">《用户协议》</text>
  91. <text class="agreement-text">和</text>
  92. <text class="link" @click="showPrivacy">《隐私政策》</text>
  93. </view>
  94. <view class="skip-section">
  95. <view class="skip-btn" @click="handleSkipLogin">暂不登录,先逛逛</view>
  96. </view>
  97. </view>
  98. </view>
  99. </scroll-view>
  100. </view>
  101. </view>
  102. </template>
  103. <script>
  104. import { wechatPhoneLogin } from '../utils/api.js'
  105. export default {
  106. name: 'login-sheet',
  107. data() {
  108. return {
  109. visible: false,
  110. panelShow: false,
  111. loading: false,
  112. // tapLock:在「点击」瞬间上锁,用于关闭授权弹窗弹出前的连点窗口(见 onPhoneBtnTap)
  113. tapLock: false,
  114. agreed: false,
  115. nickname: '',
  116. avatar: '',
  117. redirectUrl: '',
  118. _tapLockTimer: null,
  119. // 八状态视觉位(R2 修复):默认均为布尔 false,不触发任何 class;登录逻辑与外部契约不变
  120. errorState: false,
  121. successState: false
  122. }
  123. },
  124. beforeDestroy() {
  125. this.clearTimers()
  126. },
  127. methods: {
  128. clearTimers() {
  129. if (this._tapLockTimer) {
  130. clearTimeout(this._tapLockTimer)
  131. this._tapLockTimer = null
  132. }
  133. },
  134. // 由宿主页面通过 mixin openLoginSheet() 调用,或 pages/login/login.vue 兜底壳调用
  135. open(options) {
  136. options = options || {}
  137. // 处理邀请码
  138. if (options.inviteCode) {
  139. uni.setStorageSync('inviteCode', options.inviteCode)
  140. uni.setStorageSync('inviteType', options.inviteType || 'family')
  141. }
  142. // 处理推荐码(refCode)
  143. if (options.refCode) {
  144. uni.setStorageSync('refCode', options.refCode)
  145. }
  146. // 存储登录后跳转地址
  147. if (options.redirect) {
  148. this.redirectUrl = decodeURIComponent(options.redirect)
  149. }
  150. this.visible = true
  151. // 通知宿主页面(页面可借此隐藏可能盖住浮层的原生 canvas 等)
  152. this.$emit('open')
  153. this.$nextTick(() => {
  154. setTimeout(() => {
  155. this.panelShow = true
  156. }, 10)
  157. })
  158. },
  159. close() {
  160. this.panelShow = false
  161. setTimeout(() => {
  162. this.visible = false
  163. // 浮层完全淡出后再通知,避免宿主提前恢复被隐藏的原生 canvas 造成闪烁
  164. this.$emit('close')
  165. }, 250)
  166. },
  167. handleOverlayClick() {
  168. this.close()
  169. },
  170. handleImageError() {
  171. console.log('Logo 加载失败,使用默认显示')
  172. },
  173. onAgreementChange(e) {
  174. this.agreed = e.detail.value.length > 0
  175. },
  176. showAgreement() {
  177. uni.navigateTo({
  178. url: '/pages/policy/agreement'
  179. })
  180. },
  181. showPrivacy() {
  182. uni.navigateTo({
  183. url: '/pages/policy/privacy'
  184. })
  185. },
  186. handleSkipLogin() {
  187. // 不登录直接关闭浮层,停留当前页
  188. this.close()
  189. },
  190. // 点击瞬间上锁,关闭「授权弹窗弹出前」的连点窗口
  191. onPhoneBtnTap() {
  192. if (this.loading || this.tapLock) return
  193. this.tapLock = true
  194. if (this._tapLockTimer) clearTimeout(this._tapLockTimer)
  195. this._tapLockTimer = setTimeout(() => {
  196. this.tapLock = false
  197. this._tapLockTimer = null
  198. }, 3000)
  199. },
  200. getPhoneNumber(e) {
  201. // 授权流程已结束,释放 tap 锁与兜底定时器,按钮恢复可点
  202. if (this._tapLockTimer) {
  203. clearTimeout(this._tapLockTimer)
  204. this._tapLockTimer = null
  205. }
  206. this.tapLock = false
  207. if (!this.agreed) {
  208. uni.showToast({ title: '请先同意用户协议', icon: 'none' })
  209. return
  210. }
  211. if (!e.detail || !e.detail.code) {
  212. uni.showToast({ title: '授权失败,请重试', icon: 'none' })
  213. return
  214. }
  215. var self = this
  216. self.loading = true
  217. // 点击授权时实时获取 login code(微信 code 一次性有效,不能复用旧 code)
  218. uni.login({
  219. provider: 'weixin',
  220. success: function(loginRes) {
  221. if (!loginRes.code) {
  222. uni.showToast({ title: '网络异常,请重试', icon: 'none' })
  223. self.loading = false
  224. return
  225. }
  226. wechatPhoneLogin({ code: loginRes.code, phoneCode: e.detail.code }).then(function(res) {
  227. self.completeLogin(res.data)
  228. }).catch(function(err) {
  229. uni.showToast({ title: err.message || '登录失败', icon: 'none' })
  230. self.loading = false
  231. })
  232. },
  233. fail: function() {
  234. uni.showToast({ title: '网络异常,请重试', icon: 'none' })
  235. self.loading = false
  236. }
  237. })
  238. },
  239. completeLogin(data) {
  240. if (!data) return
  241. // 写入登录态
  242. if (data.token) uni.setStorageSync('token', data.token)
  243. if (data.userId) uni.setStorageSync('userId', data.userId)
  244. if (data.openid) uni.setStorageSync('openid', data.openid)
  245. if (data.nickname) {
  246. this.nickname = data.nickname
  247. uni.setStorageSync('nickname', data.nickname)
  248. }
  249. if (data.avatar) {
  250. this.avatar = data.avatar
  251. uni.setStorageSync('avatar', data.avatar)
  252. }
  253. // 通知宿主页面(页面通过 @success="onSheetLoginSuccess" 刷新数据)
  254. this.$emit('success', data)
  255. // 关闭浮层,停留当前页
  256. this.close()
  257. }
  258. }
  259. }
  260. </script>
  261. <style scoped lang="scss">
  262. /* =====================================================================
  263. login-sheet 视觉令牌(局部)
  264. 来源:全部复用 uni.scss 共享令牌;仅本组件专有的色值以 $ls- 前缀声明,
  265. 组件体内零裸 hex / 裸 px / 裸毫秒。
  266. 注意:mp-weixin WXML 不支持模板内联 <svg>(仓库既有约定,
  267. components/wuxing-sandbox.vue 注释在案),故图标一律用
  268. data:image/svg+xml 作为 CSS background-image,颜色显式写入 SVG 文档。
  269. ===================================================================== */
  270. // ---- 图标工具:构建 data-URI SVG(颜色显式编码为 %23hex)----
  271. // str-replace 使维度色令牌可注入 data-URI(# 编码为 %23),令牌成为唯一事实源
  272. @function str-replace($string, $search, $replace) {
  273. $index: str-index($string, $search);
  274. @if $index {
  275. @return str-slice($string, 1, $index - 1) + $replace +
  276. str-replace(str-slice($string, $index + str-length($search)), $search, $replace);
  277. }
  278. @return $string;
  279. }
  280. @function ls-icon($color, $inner) {
  281. @return url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='#{str-replace($color, '#', '%23')}' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E#{$inner}%3C/svg%3E");
  282. }
  283. // 图标路径(feather 线性几何,24×24;只编码 < > 与 #,空格/单引号合法)
  284. $ls-i-search: "%3Ccircle cx='11' cy='11' r='7'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E";
  285. $ls-i-shield: "%3Cpath d='M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z'/%3E%3Cpath d='m9 12 2 2 4-4'/%3E";
  286. $ls-i-zap: "%3Cpath d='M13 2 3 14h9l-1 8 10-12h-9l1-8z'/%3E";
  287. $ls-i-chart: "%3Cline x1='18' y1='20' x2='18' y2='10'/%3E%3Cline x1='12' y1='20' x2='12' y2='4'/%3E%3Cline x1='6' y1='20' x2='6' y2='14'/%3E";
  288. $ls-i-user: "%3Cpath d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'/%3E%3Ccircle cx='12' cy='7' r='4'/%3E";
  289. $ls-i-bulb: "%3Cpath d='M9 18h6'/%3E%3Cpath d='M10 22h4'/%3E%3Cpath d='M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5.76.76 1.23 1.52 1.41 2.5'/%3E";
  290. $ls-i-trend: "%3Cpolyline points='23 6 13.5 15.5 8.5 10.5 1 18'/%3E%3Cpolyline points='17 6 23 6 23 12'/%3E";
  291. $ls-i-activity: "%3Cpolyline points='22 12 18 12 15 21 9 3 6 12 2 12'/%3E";
  292. $ls-i-heart: "%3Cpath d='M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z'/%3E";
  293. $ls-i-chev: "%3Cpolyline points='9 18 15 12 9 6'/%3E";
  294. $ls-i-phone: "%3Cpath d='M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6A19.79 19.79 0 0 1 2.08 4.18 2 2 0 0 1 4.06 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z'/%3E";
  295. // ---- 局部令牌:底色 / 卡片 / 文字 / 描边 ----
  296. $ls-bg-top: #FFFDFB; // 面板暖白·顶(较原 #fffaf3 更干净)
  297. $ls-bg-bottom: #FBF4EA; // 面板暖白·底(降饱和)
  298. $ls-card: #FFFFFF; // 卡片表面
  299. $ls-card-border: #F0E7DB; // 暖发丝描边
  300. $ls-divider: #F1EAE0; // 发丝分割线
  301. $ls-overlay: rgba(20, 15, 8, 0.48); // 暖调遮罩,非纯黑
  302. $ls-ink-strong: $facade-text-strong; // 主文字 #333
  303. $ls-ink-body: $facade-text-body; // 正文/次级 #666
  304. $ls-ink-slate: $facade-text-slate-600; // 弱文字 #5A6478(全面板 ≥4.5:1,含底部 #FBF4EA)
  305. $ls-handle: #8A8A8A; // 抓握条(UI affordance,对面板顶 3.40:1 ≥3:1)
  306. $ls-connector: $facade-text-slate; // 步骤连接箭头 #6B7280(对白卡 4.83:1 ≥3:1)
  307. $ls-brand-deep: $brand-primary-deep; // 品牌橙·文字/主CTA #C2410C(对白 5.18:1,AA)
  308. $ls-ink-on-brand:$ink-on-dark; // 橙底白字 #FFFFFF
  309. $ls-radius-card: $radius-lg; // 24rpx 卡片档
  310. $ls-radius-btn: $radius-lg; // 24rpx 大按钮档
  311. // 维度标识色(深档,保证白底 ≥3:1;与 uni.scss §12 深档一一对应)
  312. $ls-dim-body: #9C4A1A; // 身·赭石
  313. $ls-dim-wisdom: $dim-wisdom-deep; // 智·靛蓝(实际编译值 #4F46E5,见 uni.scss L588 二次声明覆盖;对白 6.29:1)
  314. $ls-dim-wealth: $dim-wealth-deep; // 富·黄土 #92400E
  315. $ls-dim-action: $dim-action-deep; // 行·森林绿深档 #047857(token 复用)
  316. $ls-dim-mind: $dim-mind-deep; // 心·玫红 #9D174D
  317. // 维度浅底(uni.scss §13 tint;四步闭环用)
  318. $ls-dim-wisdom-tint: $dim-wisdom-tint; // 智浅底 #F0F0FF
  319. $ls-dim-wealth-tint: $dim-wealth-tint; // 富浅底 #FFFBEB
  320. /* ===== 浮层遮罩 ===== */
  321. .login-sheet-overlay {
  322. position: fixed;
  323. top: 0;
  324. left: 0;
  325. right: 0;
  326. bottom: 0;
  327. background: $ls-overlay; // 暖调遮罩,非纯黑
  328. z-index: 99999;
  329. opacity: 0;
  330. visibility: hidden;
  331. transition: opacity $uni-transition-base;
  332. }
  333. .login-sheet-overlay--visible {
  334. opacity: 1;
  335. visibility: visible;
  336. }
  337. /* ===== 底部面板 ===== */
  338. .login-sheet-panel {
  339. position: fixed;
  340. left: 0;
  341. right: 0;
  342. bottom: 0;
  343. height: 85vh;
  344. max-height: 85vh;
  345. background: linear-gradient(180deg, $ls-bg-top 0%, $ls-bg-bottom 100%);
  346. border-radius: $ls-radius-card $ls-radius-card 0 0;
  347. overflow: hidden;
  348. transform: translateY(100%);
  349. transition: transform $uni-transition-slow;
  350. display: flex;
  351. flex-direction: column;
  352. }
  353. .login-sheet-panel--show {
  354. transform: translateY(0);
  355. }
  356. .login-sheet-header {
  357. padding: $uni-space-lg $uni-space-xl $uni-space-xs;
  358. display: flex;
  359. flex-direction: column;
  360. align-items: center;
  361. }
  362. // 抓握条:底部浮层的拖拽 affordance(居中,独立于下方左对齐内容)
  363. .login-sheet-handle {
  364. width: 64rpx;
  365. height: 8rpx;
  366. border-radius: 999rpx;
  367. background: $ls-handle;
  368. }
  369. .login-sheet-title {
  370. margin-top: $uni-space-md;
  371. align-self: flex-start; // R5:标题与品牌区同左锚点
  372. font-size: calc($uni-font-size-sm * var(--font-scale, 1));
  373. font-weight: $uni-font-weight-medium;
  374. letter-spacing: 6rpx;
  375. color: $ls-ink-slate;
  376. }
  377. /* ===== 滚动区域 ===== */
  378. .login-sheet-scroll {
  379. flex: 1;
  380. min-height: 0;
  381. width: 100%;
  382. box-sizing: border-box;
  383. }
  384. .login-sheet-content {
  385. padding: $uni-space-lg $uni-space-xl $uni-space-4xl;
  386. box-sizing: border-box;
  387. }
  388. /* ===== 品牌区:清晰层级(R5 不对称锚点:左对齐,打破万物居中) ===== */
  389. .brand-section {
  390. display: flex;
  391. flex-direction: column;
  392. align-items: flex-start;
  393. padding: $uni-space-md 0 $uni-space-xs;
  394. }
  395. .logo {
  396. width: 120rpx;
  397. height: 120rpx;
  398. border-radius: $radius-md;
  399. }
  400. .app-name {
  401. margin-top: $uni-space-lg;
  402. font-size: calc($uni-font-size-3xl * var(--font-scale, 1)); // 52rpx 主标题(正文 28rpx 的 1.86x + 收窄行宽)
  403. font-weight: $uni-font-weight-black;
  404. color: $ls-ink-strong;
  405. letter-spacing: 2rpx;
  406. }
  407. .identity {
  408. margin-top: $uni-space-xs;
  409. font-size: calc($uni-font-size-base * var(--font-scale, 1)); // 28rpx 副标语
  410. font-weight: $uni-font-weight-medium;
  411. color: $ls-brand-deep;
  412. letter-spacing: 2rpx;
  413. }
  414. .slogan {
  415. margin-top: $uni-space-md;
  416. max-width: 460rpx; // 行宽收窄,形成视觉记忆点
  417. font-size: calc($uni-font-size-lg * var(--font-scale, 1)); // 32rpx 主打句
  418. line-height: 1.6;
  419. color: $ls-ink-body;
  420. text-align: left; // 与品牌区左对齐(R5)
  421. }
  422. .slogan-accent {
  423. color: $ls-brand-deep;
  424. font-weight: $uni-font-weight-bold;
  425. }
  426. /* ===== 理念卡(细边框表面区分,无单侧彩条) ===== */
  427. .concept-card {
  428. background: $ls-card;
  429. border: 1rpx solid $ls-card-border;
  430. border-radius: $ls-radius-card;
  431. padding: $uni-space-xl;
  432. margin-top: $uni-space-2xl;
  433. }
  434. .concept-desc {
  435. font-size: calc($uni-font-size-base * var(--font-scale, 1));
  436. color: $ls-ink-body;
  437. line-height: 1.7;
  438. }
  439. .concept-highlight {
  440. color: $ls-brand-deep;
  441. font-weight: $uni-font-weight-bold;
  442. }
  443. .loop-row {
  444. display: flex;
  445. flex-direction: row;
  446. align-items: center;
  447. justify-content: space-between;
  448. margin-top: $uni-space-xl;
  449. flex-wrap: nowrap; // 保持四步一行
  450. }
  451. .loop-step {
  452. display: flex;
  453. flex-direction: column;
  454. align-items: center;
  455. flex-shrink: 0;
  456. }
  457. .loop-glyph {
  458. width: 56rpx;
  459. height: 56rpx;
  460. border-radius: 50%;
  461. background-repeat: no-repeat;
  462. background-position: center;
  463. background-size: 28rpx 28rpx;
  464. }
  465. .loop-step-text {
  466. margin-top: $uni-space-xs;
  467. font-size: calc($uni-font-size-sm * var(--font-scale, 1));
  468. color: $ls-ink-slate;
  469. white-space: nowrap;
  470. }
  471. // 四步闭环的色相:仅图标圆底着色,文字统一中性(去彩虹)
  472. .loop-glyph-wisdom {
  473. background-color: $ls-dim-wisdom-tint;
  474. background-image: ls-icon($ls-dim-wisdom, $ls-i-search);
  475. }
  476. .loop-glyph-success {
  477. background-color: $status-success-tint;
  478. background-image: ls-icon($status-success-deep, $ls-i-shield);
  479. }
  480. .loop-glyph-wealth {
  481. background-color: $ls-dim-wealth-tint;
  482. background-image: ls-icon($ls-dim-wealth, $ls-i-zap);
  483. }
  484. .loop-glyph-danger {
  485. background-color: $status-danger-tint-2;
  486. background-image: ls-icon($status-danger-strong, $ls-i-chart);
  487. }
  488. .loop-connector {
  489. width: 20rpx;
  490. height: 20rpx;
  491. flex-shrink: 0;
  492. background-repeat: no-repeat;
  493. background-position: center;
  494. background-size: 16rpx 16rpx;
  495. background-image: ls-icon($ls-connector, $ls-i-chev);
  496. }
  497. /* ===== 五维标签带:统一容器 + 发丝分割 + 色块记号 ===== */
  498. .dims-band {
  499. display: flex;
  500. flex-direction: row;
  501. align-items: stretch;
  502. background: $ls-card;
  503. border: 1rpx solid $ls-card-border;
  504. border-radius: $ls-radius-card;
  505. margin-top: $uni-space-lg;
  506. padding: $uni-space-md 0;
  507. }
  508. .dim-cell {
  509. flex: 1;
  510. display: flex;
  511. flex-direction: column;
  512. align-items: center;
  513. justify-content: center;
  514. }
  515. .dim-cell + .dim-cell {
  516. border-left: 1rpx solid $ls-divider;
  517. }
  518. .dim-glyph {
  519. width: 36rpx;
  520. height: 36rpx;
  521. background-repeat: no-repeat;
  522. background-position: center;
  523. background-size: 30rpx 30rpx;
  524. }
  525. .dim-label {
  526. margin-top: $uni-space-xs;
  527. font-size: calc($uni-font-size-sm * var(--font-scale, 1));
  528. color: $ls-ink-body;
  529. white-space: nowrap;
  530. }
  531. .dim-glyph-body { background-image: ls-icon($ls-dim-body, $ls-i-user); }
  532. .dim-glyph-wisdom { background-image: ls-icon($ls-dim-wisdom, $ls-i-bulb); }
  533. .dim-glyph-wealth { background-image: ls-icon($ls-dim-wealth, $ls-i-trend); }
  534. .dim-glyph-action { background-image: ls-icon($ls-dim-action, $ls-i-activity); }
  535. .dim-glyph-mind { background-image: ls-icon($ls-dim-mind, $ls-i-heart); }
  536. // 主CTA(白字需 4.5:1:品牌橙 #F97316 仅 2.80:1 不达标,取深橙档;渐变更暗端保证全段达标)
  537. $ls-cta-from: #D1440B; // 深橙·亮端(白字 4.62:1 达标)
  538. $ls-cta-to: $brand-primary-deep; // 深橙·暗端 #C2410C(白字 5.18:1 达标)
  539. // hover / active 降档(白字 ≥6.0:1)
  540. $ls-cta-hover-from: #B93D0B;
  541. $ls-cta-hover-to: #A9330A;
  542. $ls-cta-press-from: #A9330A;
  543. $ls-cta-press-to: #962D08;
  544. // 禁用态:共享 $btn-disabled-bg(#D9D9D9) 对面板仅 1.39:1 不可辨、$btn-disabled-ink 3.78:1 不达 4.5
  545. // 故在组件局部覆盖为「暖灰底+深字」(不改全局令牌):底 #8E857A 对面板顶/底 3.58/3.32:1 达 ≥3、字 #1A1A1A 达 ≥4.5
  546. $ls-btn-disabled-bg: #8E857A;
  547. $ls-btn-disabled-ink: $text-charcoal; // #1A1A1A(uni.scss 既有令牌)
  548. // error/success 视觉态底色(R3 修复:白字需 ≥4.5)
  549. $ls-btn-error-bg: $status-danger-strong; // #B91C1C(白字 6.47:1 达标)
  550. $ls-btn-success-bg: $status-success-strong; // #065F46(白字 7.68:1 达标)
  551. /* ===== 登录表单 ===== */
  552. .login-form {
  553. margin-top: $uni-space-2xl;
  554. }
  555. .phone-section {
  556. display: flex;
  557. justify-content: center;
  558. }
  559. .phone-btn {
  560. width: 100%;
  561. height: 96rpx;
  562. border-radius: $ls-radius-btn;
  563. background: linear-gradient(135deg, $ls-cta-from 0%, $ls-cta-to 100%);
  564. color: $ls-ink-on-brand;
  565. font-size: calc($uni-font-size-lg * var(--font-scale, 1));
  566. font-weight: $uni-font-weight-bold;
  567. display: flex;
  568. flex-direction: row;
  569. align-items: center;
  570. justify-content: center;
  571. border: none;
  572. box-sizing: border-box;
  573. // 八态:default/hover/active/disabled/loading 走令牌;error/success 为待接线 class
  574. transition: background-color $duration-fast $ease-standard,
  575. transform $duration-fast $ease-standard,
  576. opacity $duration-fast $ease-standard;
  577. }
  578. .phone-btn::after {
  579. border: none;
  580. }
  581. .phone-btn:hover {
  582. background: linear-gradient(135deg, $ls-cta-hover-from 0%, $ls-cta-hover-to 100%); // H5 悬停:再降一档
  583. }
  584. .phone-btn:active {
  585. transform: scale(0.98);
  586. background: linear-gradient(135deg, $ls-cta-press-from 0%, $ls-cta-press-to 100%);
  587. }
  588. // R2 修复:键盘焦点可见环(H5 端)。mp-weixin 无键盘焦点,不影响小程序端。
  589. // 用品牌深橙描边对面板顶 5.10:1 ≥3:1(不纯黑);2px 描边 + 外扩,非内凹模糊
  590. .phone-btn:focus-visible {
  591. outline: 4rpx solid $ls-brand-deep;
  592. outline-offset: 4rpx;
  593. }
  594. .phone-btn[loading] {
  595. background: linear-gradient(135deg, $ls-cta-from 0%, $ls-cta-to 100%); // 保持与 default 一致,spinner 由组件自带
  596. }
  597. .phone-btn[disabled] {
  598. background: $ls-btn-disabled-bg !important;
  599. color: $ls-btn-disabled-ink !important;
  600. }
  601. // error/success 视觉态(R2/R3 接线:模板 :class 绑定 errorState/successState,默认 false 不触发)
  602. .phone-btn--error {
  603. background: $ls-btn-error-bg !important;
  604. }
  605. .phone-btn--success {
  606. background: $ls-btn-success-bg !important;
  607. }
  608. .phone-btn[disabled].phone-btn--error,
  609. .phone-btn[disabled].phone-btn--success {
  610. background: $ls-btn-disabled-bg !important; // 禁用态优先,error/success 不覆盖
  611. }
  612. .phone-btn[disabled] .phone-btn-icon {
  613. background-image: ls-icon($ls-btn-disabled-ink, $ls-i-phone);
  614. }
  615. .phone-btn--error .phone-btn-icon,
  616. .phone-btn--success .phone-btn-icon {
  617. background-image: ls-icon($ls-ink-on-brand, $ls-i-phone); // 深底白图标,与白字一致
  618. }
  619. .phone-btn-icon {
  620. width: 36rpx;
  621. height: 36rpx;
  622. margin-right: $uni-space-sm;
  623. background-repeat: no-repeat;
  624. background-position: center;
  625. background-size: 32rpx 32rpx;
  626. background-image: ls-icon($ls-ink-on-brand, $ls-i-phone);
  627. }
  628. /* ===== 协议(R5:与品牌区同左锚点,不再居中) ===== */
  629. .agreement {
  630. display: flex;
  631. flex-direction: row;
  632. align-items: center;
  633. justify-content: flex-start;
  634. flex-wrap: wrap;
  635. margin-top: $uni-space-lg;
  636. font-size: calc($uni-font-size-sm * var(--font-scale, 1));
  637. color: $ls-ink-slate;
  638. }
  639. .link {
  640. color: $ls-brand-deep;
  641. font-weight: $uni-font-weight-medium;
  642. }
  643. /* ===== 暂不登录(R5:左对齐) ===== */
  644. .skip-section {
  645. display: flex;
  646. justify-content: flex-start;
  647. margin-top: $uni-space-lg;
  648. }
  649. .skip-btn {
  650. font-size: calc($uni-font-size-base * var(--font-scale, 1));
  651. color: $ls-ink-slate;
  652. text-decoration: underline;
  653. padding: $uni-space-xs $uni-space-md; // 扩大点击区
  654. }
  655. /* ===== 动效降级:尊重 reduced-motion ===== */
  656. @media (prefers-reduced-motion: reduce) {
  657. .login-sheet-overlay,
  658. .login-sheet-panel,
  659. .phone-btn {
  660. transition-duration: 0.01ms;
  661. }
  662. }
  663. </style>