leisure-core 0.6.91 → 0.6.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/PROJECT_GUIDE.md CHANGED
@@ -47,14 +47,13 @@ leisure-core/
47
47
  │ └── directives/src/ # (预留指令目录,当前为空)
48
48
  ├── le-theme/ # 主题资源 + 一键换肤服务
49
49
  │ ├── common/
50
- │ │ ├── var.scss # SCSS 变量(编译期用)
51
50
  │ │ └── vars.css # CSS 自定义属性层(运行时换肤用)+ Element UI primary 覆盖
52
51
  │ ├── iconfont/ # 字体图标(css + ttf/woff/woff2)
53
52
  │ ├── pages/index.css # 页面样式
54
53
  │ └── index.js # LeTheme 主题服务(set/register/init)
55
54
  └── le-*/ # 各功能组件(共 36 个,见下文)
56
55
  ├── index.js # 组件注册入口(标准 install 模式)
57
- └── src/main.vue # 组件实现(部分含 rmixins.js 等辅助文件)
56
+ └── src/main.vue # 组件实现(部分含 LeHeaderMenu.vue / rmixins.js 等辅助文件)
58
57
  ```
59
58
 
60
59
  ## 4. 架构约定
@@ -152,7 +151,6 @@ export default LeXxx;
152
151
 
153
152
  | 文件 | 作用 |
154
153
  | ----------------- | ----------------------------------------------------------------------------------------- |
155
- | `common/var.scss` | SCSS 变量(编译期用) |
156
154
  | `common/vars.css` | **CSS 自定义属性层**(运行时换肤的核心),定义全部语义变量 + 覆盖 Element UI primary 色族 |
157
155
  | `pages/index.css` | 页面级样式 |
158
156
  | `iconfont/` | 字体图标 |
@@ -160,32 +158,40 @@ export default LeXxx;
160
158
 
161
159
  ### 6.2 CSS 变量层(`common/vars.css`)
162
160
 
163
- 在 `:root` 上定义约 30 个语义化变量,分为 4 组:
161
+ 在 `:root` 上定义约 40 个语义化变量,分为 6 组:
164
162
 
165
163
  **品牌色族**
166
164
 
167
- - `--le-color-primary`、`--le-color-primary-light`、`--le-color-primary-dark`、`--le-color-primary-shadow`
165
+ - `--le-color-primary`、`--le-color-primary-2`、`--le-color-primary-light`、`--le-color-primary-light-1`、`--le-color-primary-light-9`
166
+ - `--le-color-primary-shadow`、`--le-color-primary-shadow-strong`、`--le-color-primary-shadow-soft`
168
167
 
169
- **功能色族**
168
+ **功能色**
170
169
 
171
- - `--le-color-success`、`--le-color-warning`、`--le-color-danger`、`--le-color-info`
170
+ - `--le-color-success`、`--le-color-success-2`、`--le-color-success-shadow`
172
171
 
173
- **文本 / 边框 / 容器**
172
+ **文本**
174
173
 
175
- - `--le-color-text-primary`、`--le-color-text-regular`、`--le-color-text-secondary`
176
- - `--le-color-border`、`--le-color-container`、`--le-color-bg`
174
+ - `--le-color-text-strong`、`--le-color-text-regular`、`--le-color-text-muted`
175
+ - `--le-color-text-form`、`--le-color-input-text`、`--le-color-on-dark`
176
+
177
+ **边框 / 容器 / 卡片 / 装饰**
178
+
179
+ - `--le-color-border`、`--le-color-border-hover`、`--le-color-border-dark`
180
+ - `--le-color-card-bg`、`--le-color-card-border`、`--le-color-card-shadow`
181
+ - `--le-color-input-bg`、`--le-color-placeholder`
182
+ - `--le-color-section-bg-from`、`--le-color-section-bg-to`
177
183
 
178
184
  **le-home 框架色**(一键换肤必覆盖)
179
185
 
180
186
  - `--le-color-header-bg`、`--le-color-aside-bg`、`--le-color-menu-bg`
181
187
  - `--le-color-aside-text`、`--le-color-aside-active`
182
188
  - `--le-color-aside-handle-hover`、`--le-color-aside-handle-active`
183
- - `--le-color-aside-toggle-bg`、`--le-color-aside-toggle-color`、`--le-color-aside-toggle-active-bg`
184
- - `--le-color-contextmenu-bg`、`--le-color-contextmenu-color`、`--le-color-contextmenu-hover`
189
+ - `--le-color-aside-toggle-bg`、`--le-color-aside-toggle-hover`
190
+ - `--le-color-contextmenu-bg`、`--le-color-contextmenu-border`、`--le-color-contextmenu-text`、`--le-color-contextmenu-hover`、`--le-color-contextmenu-shadow`
185
191
 
186
192
  **Element UI primary 覆盖**(让 `type="primary"` 按钮跟随换肤)
187
193
 
188
- - `.el-button--primary`、`.el-tag--primary`、`.el-checkbox`、`.el-radio`、`.el-switch`、`.el-pagination`、`.el-tabs`、`.el-link` 等选择器的 `color/background/border` 均绑定到 `--le-color-primary*` 变量
194
+ - `.el-button--primary`、`.el-tag--primary`、`.el-checkbox`、`.el-radio`、`.el-switch`、`.el-slider`、`.el-pagination`、`.el-tabs`、`.el-link`、`.el-loading-spinner` 等选择器的 `color/background/border` 均绑定到 `--le-color-primary*` 变量
189
195
 
190
196
  ### 6.3 LeTheme 服务(`index.js`)
191
197
 
@@ -193,11 +199,11 @@ export default LeXxx;
193
199
 
194
200
  **预置皮肤**
195
201
 
196
- | 皮肤名 | 主色 | 用途 |
197
- | --------- | -------------- | -------- |
198
- | `default` | 蓝色 `#3b82f6` | 当前默认 |
199
- | `dark` | 深色背景 | 暗色模式 |
200
- | `green` | 绿色 `#22c55e` | 绿色主色 |
202
+ | 皮肤名 | 主色 | 用途 |
203
+ | --------- | ----------------- | ----------------------------------------- |
204
+ | `default` | `#409eff`(蓝色) | 当前默认(依赖 `:root` 变量,无额外覆盖) |
205
+ | `dark` | `#3b82f6`(深蓝) | 暗色模式 |
206
+ | `green` | `#00b96b`(翠绿) | 绿色主色 |
201
207
 
202
208
  **API**
203
209
 
@@ -230,16 +236,27 @@ this.$leTheme.current; // "dark"
230
236
 
231
237
  ```js
232
238
  // 方式 A:CSS 直接消费(静态样式)
233
- .hello { color: var(--le-color-text-primary); }
239
+ .hello { color: var(--le-color-text-strong); }
234
240
 
235
241
  // 方式 B:el-menu 等需要 prop 传值的场景
242
+ // getCssVar 必须定义在 methods 里(Vue 2 computed 不支持形参)
243
+ methods: {
244
+ getCssVar(name, fallback) {
245
+ try {
246
+ const v = getComputedStyle(document.documentElement)
247
+ .getPropertyValue(name).trim();
248
+ return v || fallback;
249
+ } catch (e) { return fallback; }
250
+ }
251
+ },
236
252
  computed: {
237
253
  menuBackgroundColor() {
238
254
  return this.getCssVar('--le-color-menu-bg', '#3a3a3a');
239
255
  }
240
256
  },
241
257
  mounted() {
242
- window.addEventListener('le-theme-change', () => this.$forceUpdate());
258
+ this._themeChangeHandler = () => this.$forceUpdate();
259
+ window.addEventListener('le-theme-change', this._themeChangeHandler);
243
260
  }
244
261
  ```
245
262
 
@@ -432,13 +449,25 @@ mounted() {
432
449
 
433
450
  ### 8.21 le-home
434
451
 
435
- - **Props**:`extraDropdownItems`、`companyInfo`、`showClose`
436
- - **Slots**:`otherContainer`
452
+ - **Props**:`extraDropdownItems`、`companyInfo`、`showClose`、`headerMenu`、`showHeaderMenu`
453
+ - **Slots**:`otherContainer`、`rightComment`
437
454
  - **Events**:`homeInit`、`command(command)`(修改密码/退出之外的命令)、`userChangePwd(params, callback)`
455
+ - **子组件**:`LeHeaderMenu`(`src/LeHeaderMenu.vue`,递归菜单组件,支持任意深度嵌套)
438
456
  - **行为**:挂载后注册键盘/鼠标事件,内置修改密码弹窗
457
+ - **Header 布局**:
458
+ - 三栏结构:左(品牌名)/ 中(横向菜单,可选)/ 右(用户下拉)
459
+ - 有 headerMenu 时 span 为 `4:12:8`,无时为 `8:0:16`
460
+ - 通过 `showHeaderMenu` prop 显式控制是否显示横向菜单
461
+ - **headerMenu 数据结构**:`[{ id, name, title, icon, children: [...] }]`,支持任意深度嵌套
462
+ - **关键方法**:
463
+ - `_filterHiddenMenus(list)`:递归过滤 `ishide === 1` 的菜单项
464
+ - `handleHeaderMenuSelect(item)`:复用侧边栏导航逻辑
465
+ - `getCssVar(name, fallback)`:读 CSS 变量的 helper(定义在 methods 里)
466
+ - **关键 computed**:`effectiveHeaderMenu`、`headerMenuBgColor`、`headerMenuTextColor`、`headerMenuActiveTextColor`
439
467
  - **样式换肤支持**:
440
468
  - 顶部 header 背景 / aside 背景 / 拖动条 / aside_open_close / contextmenu 全部使用 `var(--le-color-*)`
441
- - `el-menu` 通过 `background-color` / `text-color` / `active-text-color` prop 动态绑定 CSS 变量(computed + `getCssVar`)
469
+ - el-menu 通过 `background-color` / `text-color` / `active-text-color` prop 动态绑定 CSS 变量(computed + `getCssVar`)
470
+ - 横向菜单弹出层通过全局 `.le-header-menu-popper` 样式绑定 header 背景色
442
471
  - 监听 `window` 级 `le-theme-change` 事件,回调里 `$forceUpdate` 强制刷新 el-menu 的 prop
443
472
 
444
473
  ### 8.22 le-area
@@ -0,0 +1,60 @@
1
+ <template>
2
+ <!-- 有子项:渲染为 submenu(一级横向 / 下级下拉均支持) -->
3
+ <el-submenu
4
+ v-if="hasChildren"
5
+ :index="item.name"
6
+ popper-class="le-header-menu-popper"
7
+ >
8
+ <template slot="title">
9
+ <i v-if="item.icon" :class="item.icon"></i>
10
+ <span>{{ item.title }}</span>
11
+ </template>
12
+ <!-- 递归渲染子节点 -->
13
+ <le-header-menu
14
+ v-for="child in visibleChildren"
15
+ :key="child.id || child.name"
16
+ :item="child"
17
+ :level="level + 1"
18
+ :on-select="onSelect"
19
+ ></le-header-menu>
20
+ </el-submenu>
21
+ <!-- 叶子节点:渲染为 menu-item -->
22
+ <el-menu-item v-else :index="item.name" @click="handleClick">
23
+ <i v-if="item.icon" :class="item.icon"></i>
24
+ <span>{{ item.title }}</span>
25
+ </el-menu-item>
26
+ </template>
27
+
28
+ <script>
29
+ export default {
30
+ name: "LeHeaderMenu",
31
+ props: {
32
+ item: {
33
+ type: Object,
34
+ required: true,
35
+ },
36
+ level: {
37
+ type: Number,
38
+ default: 0,
39
+ },
40
+ onSelect: {
41
+ type: Function,
42
+ default: () => {},
43
+ },
44
+ },
45
+ computed: {
46
+ hasChildren() {
47
+ return Array.isArray(this.item.children) && this.item.children.length > 0;
48
+ },
49
+ visibleChildren() {
50
+ if (!this.item.children) return [];
51
+ return this.item.children.filter((child) => child.ishide !== 1);
52
+ },
53
+ },
54
+ methods: {
55
+ handleClick() {
56
+ this.onSelect(this.item);
57
+ },
58
+ },
59
+ };
60
+ </script>
@@ -1,15 +1,41 @@
1
1
  <template>
2
2
  <el-container class="le-home-cp-container">
3
3
  <el-header class="header">
4
- <el-row>
5
- <el-col :span="14" class="leftsection">
4
+ <el-row type="flex" align="middle" class="header-row">
5
+ <el-col :span="effectiveHeaderMenu.length ? 4 : 8" class="leftsection">
6
6
  <div class="grid-content bg-purple">
7
7
  <span class="userinfo-inner" @click="clickLeftTop">{{
8
8
  companyInfo.name
9
9
  }}</span>
10
10
  </div>
11
11
  </el-col>
12
- <el-col :span="10" class="rightsection">
12
+ <el-col
13
+ v-if="effectiveHeaderMenu.length"
14
+ :span="12"
15
+ class="middlesection"
16
+ >
17
+ <el-menu
18
+ mode="horizontal"
19
+ class="header-menu"
20
+ :background-color="headerMenuBgColor"
21
+ :text-color="headerMenuTextColor"
22
+ :active-text-color="headerMenuActiveTextColor"
23
+ :unique-opened="true"
24
+ popper-class="le-header-menu-popper"
25
+ >
26
+ <le-header-menu
27
+ v-for="item in effectiveHeaderMenu"
28
+ :key="item.id || item.name"
29
+ :item="item"
30
+ :level="0"
31
+ :on-select="handleHeaderMenuSelect"
32
+ ></le-header-menu>
33
+ </el-menu>
34
+ </el-col>
35
+ <el-col
36
+ :span="effectiveHeaderMenu.length ? 8 : 16"
37
+ class="rightsection"
38
+ >
13
39
  <div class="grid-content bg-purple-light">
14
40
  <slot name="rightComment"> </slot>
15
41
  <el-dropdown size="medium" trigger="click" @command="handleCommand">
@@ -19,8 +45,9 @@
19
45
  <i class="el-icon-arrow-down el-icon--right"></i>
20
46
  </span>
21
47
  <el-dropdown-menu slot="dropdown">
22
- <el-dropdown-item command="1">修改密码</el-dropdown-item>
23
- <!-- <el-dropdown-item command="3">个人信息</el-dropdown-item> -->
48
+ <el-dropdown-item command="openSysSettings"
49
+ >系统设置</el-dropdown-item
50
+ >
24
51
  <el-dropdown-item
25
52
  v-for="item in extraDropdownItems"
26
53
  :key="item.command"
@@ -29,7 +56,9 @@
29
56
  >
30
57
  {{ item.label }}
31
58
  </el-dropdown-item>
32
- <el-dropdown-item command="2">退出</el-dropdown-item>
59
+ <el-dropdown-item command="2" divided
60
+ >退出系统</el-dropdown-item
61
+ >
33
62
  </el-dropdown-menu>
34
63
  </el-dropdown>
35
64
  </div>
@@ -44,6 +73,7 @@
44
73
  ref="asideRef"
45
74
  >
46
75
  <el-menu
76
+ ref="sideMenu"
47
77
  :unique-opened="true"
48
78
  class="menu"
49
79
  :background-color="menuBackgroundColor"
@@ -108,7 +138,7 @@
108
138
  </div>
109
139
  </el-main>
110
140
  </el-container>
111
- <el-dialog
141
+ <!-- <el-dialog
112
142
  title="修改密码"
113
143
  ref="elDialog"
114
144
  :visible.sync="showChangePwd"
@@ -161,8 +191,8 @@
161
191
  >确认修改</el-button
162
192
  >
163
193
  </div>
164
- </el-dialog>
165
- <le-dialog-container :showDialog="showPerInfo" title="个人信息" width="30%">
194
+ </el-dialog> -->
195
+ <!-- <le-dialog-container :showDialog="showPerInfo" title="个人信息" width="30%">
166
196
  <le-list-form
167
197
  :formColumns="formColumns"
168
198
  :formData="perInfo"
@@ -171,7 +201,119 @@
171
201
  @closeDialog="cancelChangePerInfo"
172
202
  @saveData="savePerInfo"
173
203
  ></le-list-form>
174
- </le-dialog-container>
204
+ </le-dialog-container> -->
205
+
206
+ <!-- 系统设置对话框 -->
207
+ <el-dialog
208
+ :visible.sync="sysSettingsVisible"
209
+ title="系统设置"
210
+ width="560px"
211
+ :close-on-click-modal="false"
212
+ custom-class="rounded-dialog"
213
+ append-to-body
214
+ >
215
+ <el-tabs v-model="sysSettingsTab" type="border-card">
216
+ <!-- 修改密码 -->
217
+ <el-tab-pane label="修改密码" name="pwd">
218
+ <el-form
219
+ ref="sysPwdForm"
220
+ :model="sysPwdForm"
221
+ :rules="sysPwdRules"
222
+ label-width="100px"
223
+ style="padding: 20px 0"
224
+ >
225
+ <el-form-item label="原密码" prop="oldPwd">
226
+ <el-input
227
+ v-model="sysPwdForm.oldPwd"
228
+ type="password"
229
+ placeholder="请输入原密码"
230
+ show-password
231
+ ></el-input>
232
+ </el-form-item>
233
+ <el-form-item label="新密码" prop="newPwd">
234
+ <el-input
235
+ v-model="sysPwdForm.newPwd"
236
+ type="password"
237
+ placeholder="请输入新密码"
238
+ show-password
239
+ ></el-input>
240
+ </el-form-item>
241
+ <el-form-item label="确认密码" prop="confirmPwd">
242
+ <el-input
243
+ v-model="sysPwdForm.confirmPwd"
244
+ type="password"
245
+ placeholder="请再次输入新密码"
246
+ show-password
247
+ ></el-input>
248
+ </el-form-item>
249
+ </el-form>
250
+ </el-tab-pane>
251
+
252
+ <!-- 修改个人信息 -->
253
+ <el-tab-pane label="修改个人信息" name="person">
254
+ <el-form
255
+ ref="sysPerForm"
256
+ :model="sysPerForm"
257
+ :rules="sysPerRules"
258
+ label-width="100px"
259
+ style="padding: 20px 0"
260
+ >
261
+ <el-form-item label="昵称" prop="nick">
262
+ <el-input
263
+ v-model="sysPerForm.nick"
264
+ placeholder="请输入昵称"
265
+ ></el-input>
266
+ </el-form-item>
267
+ <el-form-item label="手机号" prop="phone">
268
+ <el-input
269
+ v-model="sysPerForm.phone"
270
+ placeholder="请输入手机号"
271
+ maxlength="11"
272
+ ></el-input>
273
+ </el-form-item>
274
+ </el-form>
275
+ </el-tab-pane>
276
+
277
+ <!-- 设置皮肤 -->
278
+ <el-tab-pane label="设置皮肤" name="skin">
279
+ <div class="skin-picker">
280
+ <div
281
+ v-for="skin in availableSkins"
282
+ :key="skin.name"
283
+ :class="['skin-item', { active: currentSkin === skin.name }]"
284
+ @click="selectSkin(skin.name)"
285
+ >
286
+ <div class="skin-color" :style="{ background: skin.color }"></div>
287
+ <div class="skin-info">
288
+ <div class="skin-name">{{ skin.label }}</div>
289
+ <div class="skin-desc" v-if="skin.name === currentSkin">
290
+ 当前使用
291
+ </div>
292
+ </div>
293
+ <i
294
+ v-if="currentSkin === skin.name"
295
+ class="el-icon-check skin-check"
296
+ ></i>
297
+ </div>
298
+ </div>
299
+ </el-tab-pane>
300
+ </el-tabs>
301
+ <!-- 对话框底部按钮:根据当前 tab 显示不同按钮 -->
302
+ <div slot="footer">
303
+ <template v-if="sysSettingsTab === 'pwd'">
304
+ <el-button @click="sysSettingsVisible = false">取消</el-button>
305
+ <el-button type="primary" @click="submitSysPwd">确定</el-button>
306
+ </template>
307
+ <template v-else-if="sysSettingsTab === 'person'">
308
+ <el-button @click="sysSettingsVisible = false">取消</el-button>
309
+ <el-button type="primary" @click="submitSysPer">确定</el-button>
310
+ </template>
311
+ <template v-else-if="sysSettingsTab === 'skin'">
312
+ <el-button @click="sysSettingsVisible = false">关闭</el-button>
313
+ </template>
314
+ </div>
315
+ </el-dialog>
316
+
175
317
  <div v-show="contextMenuVisible">
176
318
  <ul
177
319
  :style="{ left: menuLeft + 'px', top: menuTop + 'px' }"
@@ -203,8 +345,10 @@
203
345
  <script>
204
346
  import { cloneDeep } from "lodash";
205
347
  import { throttle } from "lodash"; // 可选,用于性能优化
348
+ import LeHeaderMenu from "./LeHeaderMenu.vue";
206
349
  export default {
207
350
  name: "le-home",
351
+ components: { LeHeaderMenu },
208
352
  props: {
209
353
  extraDropdownItems: {
210
354
  type: Array,
@@ -218,32 +362,45 @@ export default {
218
362
  type: Boolean,
219
363
  default: true,
220
364
  },
365
+ // 顶部横向菜单数据,支持多级嵌套
366
+ // 数据结构: [{ id, name, title, icon, children: [...] }]
367
+ headerMenu: {
368
+ type: Array,
369
+ default: () => [],
370
+ },
371
+ // 是否显示顶部横向菜单
372
+ // true: 只要 headerMenu 有数据就显示
373
+ // false: 即使 headerMenu 有数据也不显示
374
+ showHeaderMenu: {
375
+ type: Boolean,
376
+ default: false,
377
+ },
221
378
  },
222
379
 
223
380
  data() {
224
- var validatePass = (rule, value, callback) => {
225
- if (value === "") {
226
- callback(new Error("请输入密码"));
227
- } else {
228
- if (value.length < 8) {
229
- callback(new Error("密码至少8位"));
230
- } else {
231
- if (this.ruleForm.confirmPwd !== "") {
232
- this.$refs.ruleForm.validateField("confirmPwd ");
233
- }
234
- callback();
235
- }
236
- }
237
- };
238
- var validatePass2 = (rule, value, callback) => {
239
- if (value === "") {
240
- callback(new Error("请再次输入密码"));
241
- } else if (value !== this.ruleForm.pwd) {
242
- callback(new Error("两次输入密码不一致!"));
243
- } else {
244
- callback();
245
- }
246
- };
381
+ // var validatePass = (rule, value, callback) => {
382
+ // if (value === "") {
383
+ // callback(new Error("请输入密码"));
384
+ // } else {
385
+ // if (value.length < 8) {
386
+ // callback(new Error("密码至少8位"));
387
+ // } else {
388
+ // if (this.ruleForm.confirmPwd !== "") {
389
+ // this.$refs.ruleForm.validateField("confirmPwd ");
390
+ // }
391
+ // callback();
392
+ // }
393
+ // }
394
+ // };
395
+ // var validatePass2 = (rule, value, callback) => {
396
+ // if (value === "") {
397
+ // callback(new Error("请再次输入密码"));
398
+ // } else if (value !== this.ruleForm.pwd) {
399
+ // callback(new Error("两次输入密码不一致!"));
400
+ // } else {
401
+ // callback();
402
+ // }
403
+ // };
247
404
  return {
248
405
  style: {
249
406
  display: "block",
@@ -253,29 +410,29 @@ export default {
253
410
  aside_open_close: true,
254
411
  menuData: [],
255
412
  title: "",
256
- cacheView: [],
257
- showChangePwd: false,
258
- showPerInfo: false,
413
+ // cacheView: [],
414
+ // showChangePwd: false,
415
+ // showPerInfo: false,
259
416
  username: "",
260
- changeUid: "",
417
+ // changeUid: "",
261
418
  tabNameCurrent: "",
262
419
  tabsAll: [],
263
- ruleForm: {
264
- pwd: "",
265
- confirmPwd: "",
266
- },
267
- formColumns: [
268
- { prop: "nick", label: "昵称" },
269
- { prop: "phone", label: "手机号" },
270
- ],
271
- perInfo: {
272
- nick: this.$store.getters.userinfo.nick,
273
- phone: this.$store.getters.userinfo.phone,
274
- },
275
- rules: {
276
- pwd: [{ validator: validatePass, trigger: "blur" }],
277
- confirmPwd: [{ validator: validatePass2, trigger: "blur" }],
278
- },
420
+ // ruleForm: {
421
+ // pwd: "",
422
+ // confirmPwd: "",
423
+ // },
424
+ // formColumns: [
425
+ // { prop: "nick", label: "昵称" },
426
+ // { prop: "phone", label: "手机号" },
427
+ // ],
428
+ // perInfo: {
429
+ // nick: this.$store.getters.userinfo.nick,
430
+ // phone: this.$store.getters.userinfo.phone,
431
+ // },
432
+ // rules: {
433
+ // pwd: [{ validator: validatePass, trigger: "blur" }],
434
+ // confirmPwd: [{ validator: validatePass2, trigger: "blur" }],
435
+ // },
279
436
  //右键菜单
280
437
  contextMenuVisible: false,
281
438
  menuLeft: 0,
@@ -286,16 +443,66 @@ export default {
286
443
  isDragging: false,
287
444
  asideWidth: 200, // 默认宽度,与原宽度一致
288
445
  dragStartInfo: null, // {startX, startWidth}
446
+ // 系统设置对话框
447
+ sysSettingsVisible: false,
448
+ sysSettingsTab: "pwd",
449
+ sysPwdForm: {
450
+ oldPwd: "",
451
+ newPwd: "",
452
+ confirmPwd: "",
453
+ },
454
+ sysPwdRules: {
455
+ oldPwd: [{ required: true, message: "请输入原密码", trigger: "blur" }],
456
+ newPwd: [
457
+ { required: true, message: "请输入新密码", trigger: "blur" },
458
+ { min: 8, message: "密码至少8位", trigger: "blur" },
459
+ ],
460
+ confirmPwd: [
461
+ { required: true, message: "请再次输入新密码", trigger: "blur" },
462
+ {
463
+ validator: (rule, value, callback) => {
464
+ if (value !== this.sysPwdForm.newPwd) {
465
+ callback(new Error("两次输入密码不一致"));
466
+ } else {
467
+ callback();
468
+ }
469
+ },
470
+ trigger: "blur",
471
+ },
472
+ ],
473
+ },
474
+ sysPerForm: {
475
+ nick: "",
476
+ phone: "",
477
+ },
478
+ sysPerRules: {
479
+ nick: [{ required: true, message: "请输入昵称", trigger: "blur" }],
480
+ phone: [
481
+ { required: true, message: "请输入手机号", trigger: "blur" },
482
+ {
483
+ pattern: /^1[3-9]\d{9}$/,
484
+ message: "请输入正确的手机号",
485
+ trigger: "blur",
486
+ },
487
+ ],
488
+ },
489
+ // 当前皮肤名(响应式,供皮肤选择器和 UI 刷新使用)
490
+ currentSkin: "default",
289
491
  };
290
492
  },
291
493
  computed: {
292
494
  asideStyle() {
495
+ // 依赖 currentSkin 让 Vue 知道需要重算(getCssVar 读取 DOM 不是响应式的)
496
+ if (this.currentSkin) {
497
+ // 触发访问,建立依赖关系
498
+ }
293
499
  if (this.isHideMenu) {
294
500
  return {
295
501
  width: "0px",
296
502
  minWidth: "0px",
297
503
  maxWidth: "0px",
298
504
  flex: "0 0 0px",
505
+ backgroundColor: this.getCssVar("--le-color-aside-bg", "#3A3A3A"),
299
506
  };
300
507
  }
301
508
  return {
@@ -304,19 +511,48 @@ export default {
304
511
  maxWidth: `${this.asideWidth}px`,
305
512
  flex: `0 0 ${this.asideWidth}px`,
306
513
  transition: this.isDragging ? "none" : "width 0.3s",
514
+ backgroundColor: this.getCssVar("--le-color-aside-bg", "#3A3A3A"),
307
515
  };
308
516
  },
309
- // 读取 CSS 变量的 helper,返回指定变量值(回退到 fallback)
310
- // 注意:此函数必须放在 methods 里,不能放在 computed 里,否则会被当成无参 computed 而非方法
311
517
  // el-menu 的三个颜色 prop,绑定到 CSS 变量,实现主题切换
312
518
  menuBackgroundColor() {
313
- return this.getCssVar("--le-color-menu-bg", "#3A3A3A");
519
+ return this.currentSkin, this.getCssVar("--le-color-menu-bg", "#3A3A3A");
314
520
  },
315
521
  menuTextColor() {
316
- return this.getCssVar("--le-color-aside-text", "#fff");
522
+ return this.currentSkin, this.getCssVar("--le-color-aside-text", "#fff");
317
523
  },
318
524
  menuActiveTextColor() {
319
- return this.getCssVar("--le-color-aside-active", "#ffd04b");
525
+ return (
526
+ this.currentSkin, this.getCssVar("--le-color-aside-active", "#ffd04b")
527
+ );
528
+ },
529
+ // header 横向菜单:综合判断 showHeaderMenu + headerMenu 数据 + 过滤隐藏项
530
+ effectiveHeaderMenu() {
531
+ if (!this.showHeaderMenu) return [];
532
+ if (!this.headerMenu || !this.headerMenu.length) return [];
533
+ return this._filterHiddenMenus(this.headerMenu);
534
+ },
535
+ // header 横向菜单颜色(跟随主题)
536
+ headerMenuBgColor() {
537
+ return (
538
+ this.currentSkin, this.getCssVar("--le-color-header-bg", "#212121")
539
+ );
540
+ },
541
+ headerMenuTextColor() {
542
+ return this.currentSkin, this.getCssVar("--le-color-aside-text", "#fff");
543
+ },
544
+ headerMenuActiveTextColor() {
545
+ return (
546
+ this.currentSkin, this.getCssVar("--le-color-aside-active", "#ffd04b")
547
+ );
548
+ },
549
+ // 皮肤列表(供下拉子菜单渲染)
550
+ availableSkins() {
551
+ return [
552
+ { name: "default", label: "默认蓝", color: "#409eff" },
553
+ { name: "dark", label: "深色", color: "#0f172a" },
554
+ { name: "green", label: "翠绿", color: "#00b96b" },
555
+ ];
320
556
  },
321
557
  },
322
558
  mounted() {
@@ -332,18 +568,18 @@ export default {
332
568
  this.$store.commit("setCname", this.companyInfo.name);
333
569
  }
334
570
 
335
- let dict = [
336
- {
337
- title: "是",
338
- value: 1,
339
- },
340
- {
341
- title: "否",
342
- value: 0,
343
- },
344
- ];
571
+ // let dict = [
572
+ // {
573
+ // title: "是",
574
+ // value: 1,
575
+ // },
576
+ // {
577
+ // title: "否",
578
+ // value: 0,
579
+ // },
580
+ // ];
345
581
 
346
- this.$store.commit("setDict", dict);
582
+ // this.$store.commit("setDict", dict);
347
583
 
348
584
  const menus = cloneDeep(this.$store.getters.menu);
349
585
  this.menuData = menus.filter(function (item) {
@@ -371,12 +607,20 @@ export default {
371
607
  document.addEventListener("mousemove", this.handleDrag);
372
608
  document.addEventListener("mouseup", this.stopDrag);
373
609
 
374
- // 2026-08-26:监听主题切换事件,强制刷新 el-menu 的颜色 prop 绑定
375
- this._themeChangeHandler = () => {
376
- // 通过递增版本号触发 computed 重新求值(否则 computed 依赖不变不会重算)
377
- this._themeVersion = (this._themeVersion || 0) + 1;
378
- this.$forceUpdate();
610
+ // 2026-08-26:监听主题切换事件,更新响应式皮肤名触发 computed 重算
611
+ this._themeChangeHandler = (e) => {
612
+ const skinName =
613
+ (e && e.detail) ||
614
+ (this.$leTheme && this.$leTheme.current) ||
615
+ "default";
616
+ // 更新 currentSkin 后,所有依赖 currentSkin 的 computed 会自动重算
617
+ // 包括 asideStyle, menuBackgroundColor, menuTextColor, headerMenuBgColor 等
618
+ this.currentSkin = skinName;
379
619
  };
620
+ // 初始化当前皮肤名
621
+ if (this.$leTheme) {
622
+ this.currentSkin = this.$leTheme.current;
623
+ }
380
624
  if (typeof window !== "undefined") {
381
625
  window.addEventListener("le-theme-change", this._themeChangeHandler);
382
626
  }
@@ -401,6 +645,21 @@ export default {
401
645
  this.stopDrag();
402
646
  },
403
647
  methods: {
648
+ // 递归过滤隐藏菜单项(ishide === 1 的项被过滤,其子项也一并丢弃)
649
+ _filterHiddenMenus(list) {
650
+ if (!Array.isArray(list)) return [];
651
+ return list
652
+ .filter((item) => item.ishide !== 1)
653
+ .map((item) => {
654
+ if (item.children && item.children.length) {
655
+ return {
656
+ ...item,
657
+ children: this._filterHiddenMenus(item.children),
658
+ };
659
+ }
660
+ return item;
661
+ });
662
+ },
404
663
  // 2026-08-26:读取 CSS 变量的 helper,返回指定变量值(回退到 fallback)
405
664
  getCssVar(name, fallback) {
406
665
  try {
@@ -412,6 +671,10 @@ export default {
412
671
  return fallback;
413
672
  }
414
673
  },
674
+ // header 横向菜单项点击处理(复用 clickMenuItem 的导航逻辑)
675
+ handleHeaderMenuSelect(item) {
676
+ this.clickMenuItem(item);
677
+ },
415
678
  handleKeyDown(e) {
416
679
  // F5 键的 keyCode 是 116
417
680
  if (e.keyCode === 116) {
@@ -456,76 +719,129 @@ export default {
456
719
  }
457
720
  },
458
721
  handleCommand(command) {
459
- if (command === "1") this.changPwd();
460
- else if (command === "2") this.signout();
461
- else if (command === "3") this.changPerson();
462
- else this.$emit("command", command); // 抛出外部命令
463
- },
464
- changPwd() {
465
- this.changeUid = this.$store.getters.userinfo.id;
466
- this.ruleForm.pwd = "";
467
- this.ruleForm.confirmPwd = "";
468
- this.showChangePwd = true;
469
- },
470
- changPerson() {
471
- this.changeUid = this.$store.getters.userinfo.id;
472
- this.perInfo.nick = this.$store.getters.userinfo.nick;
473
- this.perInfo.phone = this.$store.getters.userinfo.phone;
474
- this.showPerInfo = true;
475
- },
476
- validatePhone(phone) {
477
- const phoneRegex = /^1[3-9]\d{9}$/;
478
- return phoneRegex.test(phone);
722
+ if (command === "openSysSettings") {
723
+ // 打开系统设置对话框,默认密码 tab
724
+ this.sysSettingsTab = "pwd";
725
+ this.sysPwdForm = { oldPwd: "", newPwd: "", confirmPwd: "" };
726
+ this.sysPerForm = {
727
+ nick: this.$store.getters.userinfo.nick || "",
728
+ phone: this.$store.getters.userinfo.phone || "",
729
+ };
730
+ this.sysSettingsVisible = true;
731
+ } else if (command === "2") {
732
+ this.signout();
733
+ } else {
734
+ this.$emit("command", command); // 抛出外部命令
735
+ }
479
736
  },
480
- cancelChangePwd() {
481
- this.showChangePwd = false;
482
- this.changeUid = "";
483
- this.ruleForm.pwd = "";
484
- this.ruleForm.confirmPwd = "";
485
- },
486
- cancelChangePerInfo() {
487
- this.showPerInfo = false;
488
- this.perInfo.nick = "";
489
- this.perInfo.phone = "";
490
- },
491
- savePerInfo(param) {
492
- if (param.phone) {
493
- if (!this.validatePhone(param.phone)) {
494
- this.$message.error("请输入有效的手机号");
495
- return;
737
+ // 系统设置 - 提交修改密码
738
+ submitSysPwd() {
739
+ this.$refs.sysPwdForm.validate((valid) => {
740
+ if (valid) {
741
+ const payload = {
742
+ uid: this.$store.getters.userinfo.id,
743
+ pwd: this.sysPwdForm.newPwd,
744
+ confirmPwd: this.sysPwdForm.confirmPwd,
745
+ };
746
+ // 方案一:emit 给宿主,由宿主决定如何调用接口
747
+ this.$emit("userChangePwd", payload, this.changePwdAfter);
496
748
  }
497
- }
498
-
499
- if (!param.nick && !param.phone) {
500
- this.$message.error("昵称和手机号至少填写一项");
501
- return;
502
- }
503
-
504
- let params = {
505
- uid: this.$store.getters.userinfo.id,
506
- nick: param.nick,
507
- phone: param.phone,
508
- };
509
- this.$emit("savePerInfo", params);
749
+ });
510
750
  },
511
- saveChangePwd(formName) {
512
- this.$refs[formName].validate((valid) => {
751
+ // 系统设置 - 提交修改个人信息
752
+ submitSysPer() {
753
+ this.$refs.sysPerForm.validate((valid) => {
513
754
  if (valid) {
514
- let params = {
515
- pwd: this.ruleForm.pwd,
516
- confirmPwd: this.ruleForm.confirmPwd,
517
- uid: this.changeUid,
755
+ const payload = {
756
+ uid: this.$store.getters.userinfo.id,
757
+ nick: this.sysPerForm.nick,
758
+ phone: this.sysPerForm.phone,
518
759
  };
519
- this.$emit("userChangePwd", params, this.changePwdAfter);
520
- } else {
521
- console.log("error submit!!");
522
- return false;
760
+ this.$emit("savePerInfo", payload);
761
+ this.sysSettingsVisible = false;
762
+ try {
763
+ let userinfo = this.$store.getters.userinfo;
764
+ console.log("userinfo", userinfo);
765
+ if (userinfo) {
766
+ userinfo.nick = payload.nick;
767
+ userinfo.phone = payload.phone;
768
+ this.$store.commit("setUserInfo", userinfo);
769
+ }
770
+ } catch (e) {}
523
771
  }
524
772
  });
525
773
  },
774
+ // 系统设置 - 选择皮肤
775
+ selectSkin(skinName) {
776
+ this.$leTheme.set(skinName);
777
+ this.$message.success("皮肤切换成功");
778
+ },
779
+ // changPwd() {
780
+ // this.changeUid = this.$store.getters.userinfo.id;
781
+ // this.ruleForm.pwd = "";
782
+ // this.ruleForm.confirmPwd = "";
783
+ // this.showChangePwd = true;
784
+ // },
785
+ // changPerson() {
786
+ // this.changeUid = this.$store.getters.userinfo.id;
787
+ // this.perInfo.nick = this.$store.getters.userinfo.nick;
788
+ // this.perInfo.phone = this.$store.getters.userinfo.phone;
789
+ // this.showPerInfo = true;
790
+ // },
791
+ validatePhone(phone) {
792
+ const phoneRegex = /^1[3-9]\d{9}$/;
793
+ return phoneRegex.test(phone);
794
+ },
795
+ // cancelChangePwd() {
796
+ // this.showChangePwd = false;
797
+ // this.changeUid = "";
798
+ // this.ruleForm.pwd = "";
799
+ // this.ruleForm.confirmPwd = "";
800
+ // },
801
+ // cancelChangePerInfo() {
802
+ // this.showPerInfo = false;
803
+ // this.perInfo.nick = "";
804
+ // this.perInfo.phone = "";
805
+ // },
806
+ // savePerInfo(param) {
807
+ // if (param.phone) {
808
+ // if (!this.validatePhone(param.phone)) {
809
+ // this.$message.error("请输入有效的手机号");
810
+ // return;
811
+ // }
812
+ // }
813
+
814
+ // if (!param.nick && !param.phone) {
815
+ // this.$message.error("昵称和手机号至少填写一项");
816
+ // return;
817
+ // }
818
+
819
+ // let params = {
820
+ // uid: this.$store.getters.userinfo.id,
821
+ // nick: param.nick,
822
+ // phone: param.phone,
823
+ // };
824
+ // this.$emit("savePerInfo", params);
825
+ // },
826
+ // saveChangePwd(formName) {
827
+ // this.$refs[formName].validate((valid) => {
828
+ // if (valid) {
829
+ // let params = {
830
+ // pwd: this.ruleForm.pwd,
831
+ // confirmPwd: this.ruleForm.confirmPwd,
832
+ // uid: this.changeUid,
833
+ // };
834
+ // this.$emit("userChangePwd", params, this.changePwdAfter);
835
+ // } else {
836
+ // console.log("error submit!!");
837
+ // return false;
838
+ // }
839
+ // });
840
+ // },
526
841
  changePwdAfter(res) {
527
842
  if (res && res.data && res.data.code == "10000") {
528
843
  this.$message.success("更新成功");
844
+ this.sysSettingsVisible = false;
529
845
  localStorage.clear();
530
846
  this.$store.commit("setUserInfo", null);
531
847
  this.$router.replace({ path: "/" });
@@ -731,9 +1047,91 @@ export default {
731
1047
  font-size: 15px;
732
1048
 
733
1049
  .header {
734
- background: var(--le-color-header-bg);
1050
+ background-color: var(--le-color-header-bg) !important;
735
1051
  color: #fff;
736
1052
  height: 60px;
1053
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
1054
+
1055
+ /* 清除 Element UI el-row / el-col 默认背景 */
1056
+ .el-row,
1057
+ .el-col {
1058
+ background-color: transparent !important;
1059
+ }
1060
+
1061
+ /* 清除 grid-content / bg-purple 等示例类的默认背景 */
1062
+ .grid-content,
1063
+ .bg-purple,
1064
+ .bg-purple-light {
1065
+ background-color: transparent !important;
1066
+ }
1067
+
1068
+ .header-row {
1069
+ height: 60px;
1070
+ }
1071
+
1072
+ .leftsection,
1073
+ .middlesection,
1074
+ .rightsection {
1075
+ height: 60px;
1076
+ line-height: 60px;
1077
+ display: flex;
1078
+ align-items: center;
1079
+ }
1080
+
1081
+ .leftsection {
1082
+ .userinfo-inner {
1083
+ font-size: 18px;
1084
+ font-weight: bold;
1085
+ cursor: pointer;
1086
+ }
1087
+ }
1088
+
1089
+ .middlesection {
1090
+ flex: 1;
1091
+ display: flex;
1092
+ align-items: center;
1093
+ padding: 0 16px;
1094
+
1095
+ .header-menu {
1096
+ border-bottom: none;
1097
+ height: 60px;
1098
+ line-height: 60px;
1099
+ width: 100%;
1100
+
1101
+ /* 菜单项悬停/激活色 */
1102
+ .el-menu-item,
1103
+ .el-submenu__title {
1104
+ height: 60px;
1105
+ line-height: 60px;
1106
+ border-bottom: none;
1107
+ font-size: 15px;
1108
+ min-width: 100px;
1109
+ text-align: center;
1110
+
1111
+ &:hover {
1112
+ background-color: rgba(255, 255, 255, 0.1) !important;
1113
+ }
1114
+ }
1115
+
1116
+ .el-submenu__icon-arrow {
1117
+ font-size: 12px;
1118
+ margin-left: 4px;
1119
+ }
1120
+
1121
+ .el-menu-item.is-active {
1122
+ border-bottom: 2px solid var(--le-color-aside-active);
1123
+ }
1124
+ }
1125
+ }
1126
+
1127
+ .rightsection {
1128
+ justify-content: flex-end;
1129
+
1130
+ .el-dropdown-link {
1131
+ cursor: pointer;
1132
+ color: #fff;
1133
+ }
1134
+ }
737
1135
  }
738
1136
 
739
1137
  /* 修改 .aside 样式 - 移除固定宽度 !important */
@@ -955,4 +1353,114 @@ export default {
955
1353
  border-radius: 12px !important;
956
1354
  overflow: hidden;
957
1355
  }
1356
+
1357
+ /* 皮肤选择器样式 */
1358
+ .skin-picker {
1359
+ display: flex;
1360
+ flex-direction: column;
1361
+ gap: 12px;
1362
+ padding: 16px 0;
1363
+ }
1364
+
1365
+ .skin-item {
1366
+ display: flex;
1367
+ align-items: center;
1368
+ padding: 12px 16px;
1369
+ border: 2px solid var(--le-color-border);
1370
+ border-radius: 8px;
1371
+ cursor: pointer;
1372
+ transition: all 0.2s ease;
1373
+
1374
+ &:hover {
1375
+ border-color: var(--le-color-primary);
1376
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1377
+ }
1378
+
1379
+ &.active {
1380
+ border-color: var(--le-color-primary);
1381
+ background-color: var(--le-color-primary-light-9);
1382
+ }
1383
+ }
1384
+
1385
+ .skin-color {
1386
+ width: 32px;
1387
+ height: 32px;
1388
+ border-radius: 50%;
1389
+ margin-right: 16px;
1390
+ flex-shrink: 0;
1391
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
1392
+ }
1393
+
1394
+ .skin-info {
1395
+ flex: 1;
1396
+ }
1397
+
1398
+ .skin-name {
1399
+ font-size: 15px;
1400
+ font-weight: 600;
1401
+ color: var(--le-color-text-strong);
1402
+ }
1403
+
1404
+ .skin-desc {
1405
+ font-size: 12px;
1406
+ color: var(--le-color-primary);
1407
+ margin-top: 2px;
1408
+ }
1409
+
1410
+ .skin-check {
1411
+ font-size: 20px;
1412
+ color: var(--le-color-primary);
1413
+ }
1414
+ </style>
1415
+ <style lang="scss">
1416
+ /* header 菜单弹出层样式(挂在 body 上,需全局样式)
1417
+ 弹出层不受 el-menu prop 控制,需手动设置背景色与 header 一致 */
1418
+ .le-header-menu-popper {
1419
+ background-color: var(--le-color-header-bg) !important;
1420
+
1421
+ .el-menu {
1422
+ border-right: none;
1423
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.3);
1424
+ min-width: 160px;
1425
+ background-color: var(--le-color-header-bg) !important;
1426
+ }
1427
+
1428
+ .el-menu-item,
1429
+ .el-submenu__title {
1430
+ height: 40px;
1431
+ line-height: 40px;
1432
+ border-bottom: none;
1433
+ padding: 0 20px;
1434
+ min-width: 160px;
1435
+ font-size: 14px;
1436
+ background-color: var(--le-color-header-bg) !important;
1437
+ color: var(--le-color-aside-text) !important;
1438
+
1439
+ &:hover {
1440
+ background-color: rgba(255, 255, 255, 0.1) !important;
1441
+ }
1442
+ }
1443
+
1444
+ .el-menu-item.is-active {
1445
+ color: var(--le-color-aside-active) !important;
1446
+ background-color: rgba(255, 255, 255, 0.1) !important;
1447
+ }
1448
+
1449
+ .el-submenu__icon-arrow {
1450
+ float: right;
1451
+ margin-right: 0;
1452
+ color: var(--le-color-aside-text) !important;
1453
+ }
1454
+ }
1455
+
1456
+ /* 用户下拉菜单(系统设置/皮肤选择)全局样式 */
1457
+ .el-dropdown-menu {
1458
+ .el-dropdown-menu__item {
1459
+ position: relative;
1460
+
1461
+ &.skin-active {
1462
+ color: var(--le-color-aside-active);
1463
+ }
1464
+ }
1465
+ }
958
1466
  </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leisure-core",
3
- "version": "0.6.91",
3
+ "version": "0.6.92",
4
4
  "description": "leisure-core是京心数据基于vue2.x开发的一套后台管理系统桌面端组件库,封装了大量实用的UI控件模板,非常方便开发者快速搭建前端应用",
5
5
  "private": false,
6
6
  "author": "北方乐逍遥(zcx7878)",
@@ -1,17 +0,0 @@
1
- -------------------------- */
2
- $--all-transition: all .3s cubic-bezier(.645,.045,.355,1) !default;
3
- $--fade-transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1) !default;
4
- $--fade-linear-transition: opacity 200ms linear !default;
5
- $--md-fade-transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1) !default;
6
- $--border-transition-base: border-color .2s cubic-bezier(.645,.045,.355,1) !default;
7
- $--color-transition-base: color .2s cubic-bezier(.645,.045,.355,1) !default;
8
-
9
- /* Color
10
- -------------------------- */
11
- /// color|1|Brand Color|0
12
- $--color-primary: #409EFF !default;
13
- /// color|1|Background Color|4
14
- $--color-white: #FFFFFF !default;
15
- /// color|1|Background Color|4
16
- $--color-black: #000000 !default;
17
- $--color-primary-light-1: mix($--color-white, $--color-primary, 10%) !default; /* 53a8ff */