Layout.vue 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748
  1. <template>
  2. <el-container class="layout-container">
  3. <el-aside width="200px">
  4. <div class="logo">浠艾福</div>
  5. <el-scrollbar wrap-class="sidebar-scrollbar-wrap">
  6. <el-menu
  7. :default-active="activeMenu"
  8. class="sidebar-menu"
  9. background-color="transparent"
  10. text-color="#64748B"
  11. active-text-color="#6366F1"
  12. @select="handleMenuSelect"
  13. >
  14. <template v-for="item in menuItems">
  15. <el-submenu v-if="item.children && hasPerm(item.perm)" :key="item.title || item.path" :index="item.title || item.path">
  16. <template slot="title">
  17. <i :class="item.icon"></i>
  18. <span>{{ item.label || item.title }}</span>
  19. </template>
  20. <template v-for="child in item.children">
  21. <!-- 子项有 children = 三级菜单 -->
  22. <el-submenu v-if="child.children && hasPerm(child.perm)" :key="child.title" :index="child.title">
  23. <template slot="title">
  24. <i :class="child.icon"></i>
  25. <span>{{ child.title }}</span>
  26. </template>
  27. <el-menu-item
  28. v-for="grandson in child.children"
  29. :key="grandson.path"
  30. :index="grandson.path"
  31. >
  32. <i :class="grandson.icon"></i>
  33. <span slot="title">{{ grandson.label }}</span>
  34. </el-menu-item>
  35. </el-submenu>
  36. <!-- 普通二级菜单项 -->
  37. <el-menu-item v-else-if="hasPerm(child.perm)" :key="child.path" :index="child.path">
  38. <i :class="child.icon"></i>
  39. <span slot="title">{{ child.label }}</span>
  40. </el-menu-item>
  41. </template>
  42. </el-submenu>
  43. <el-menu-item v-else-if="hasPerm(item.perm)" :key="item.path" :index="getMenuIndex(item)">
  44. <i :class="item.icon"></i>
  45. <span slot="title">{{ item.label }}</span>
  46. </el-menu-item>
  47. </template>
  48. </el-menu>
  49. </el-scrollbar>
  50. </el-aside>
  51. <el-container>
  52. <el-header>
  53. <div class="header-right">
  54. <el-badge :value="unreadMessageCount" :hidden="unreadMessageCount === 0" class="msg-badge">
  55. <el-button type="text" class="msg-btn" @click="handleMessageClick" icon="el-icon-bell"></el-button>
  56. </el-badge>
  57. <el-button v-if="hasPerm('service:*')" type="text" class="invite-code-btn" @click="showInviteCode" icon="el-icon-share">
  58. 邀请码
  59. </el-button>
  60. <el-dropdown trigger="click" @command="handleProfileCommand">
  61. <span class="el-dropdown-link username">
  62. {{ displayUserLabel }}<i class="el-icon-arrow-down el-icon--right"></i>
  63. </span>
  64. <el-dropdown-menu slot="dropdown">
  65. <el-dropdown-item command="profile" icon="el-icon-user">个人信息</el-dropdown-item>
  66. <el-dropdown-item command="password" icon="el-icon-key">修改密码</el-dropdown-item>
  67. <el-dropdown-item command="logout" icon="el-icon-switch-button" divided>退出登录</el-dropdown-item>
  68. </el-dropdown-menu>
  69. </el-dropdown>
  70. </div>
  71. <!-- 邀请码 dialog -->
  72. <el-dialog title="我的邀请码" :visible.sync="inviteCodeVisible" width="500px" :close-on-click-modal="false">
  73. <div v-loading="inviteCodeLoading">
  74. <div v-if="inviteCodeData" class="invite-code-content">
  75. <div class="invite-code-section">
  76. <div class="invite-code-label">团队邀请码</div>
  77. <div class="invite-code-value">
  78. <span class="code-text">{{ inviteCodeData.code || '无' }}</span>
  79. <el-button size="mini" type="primary" @click="copyCode(inviteCodeData.code)" plain>复制</el-button>
  80. </div>
  81. <div class="invite-code-desc">
  82. 家庭用户通过此码加入您的团队
  83. </div>
  84. </div>
  85. </div>
  86. <div v-else-if="!inviteCodeLoading" class="no-invite-code">
  87. <i class="el-icon-warning-outline"></i>
  88. <p>暂无邀请码,请先生成</p>
  89. <el-button type="primary" @click="generateInviteCode">生成邀请码</el-button>
  90. </div>
  91. </div>
  92. </el-dialog>
  93. </el-header>
  94. <!-- 个人信息 dialog -->
  95. <el-dialog title="个人信息" :visible.sync="profileDialogVisible" width="400px">
  96. <el-form :model="profileForm" label-width="80px">
  97. <el-form-item label="用户名">
  98. <el-input v-model="profileForm.username" disabled></el-input>
  99. </el-form-item>
  100. <el-form-item label="真实姓名">
  101. <el-input v-model="profileForm.realName" disabled></el-input>
  102. </el-form-item>
  103. <el-form-item label="昵称">
  104. <el-input v-model="profileForm.nickname"></el-input>
  105. </el-form-item>
  106. <el-form-item label="手机号">
  107. <el-input v-model="profileForm.phone"></el-input>
  108. </el-form-item>
  109. <el-form-item label="邮箱">
  110. <el-input v-model="profileForm.email"></el-input>
  111. </el-form-item>
  112. </el-form>
  113. <span slot="footer">
  114. <el-button @click="profileDialogVisible = false">取消</el-button>
  115. <el-button type="primary" @click="confirmUpdateInfo" :loading="submitting">保存</el-button>
  116. </span>
  117. </el-dialog>
  118. <!-- 修改密码 dialog -->
  119. <el-dialog title="修改密码" :visible.sync="passwordDialogVisible" width="400px">
  120. <el-form :model="passwordForm" :rules="passwordRules" ref="passwordForm" label-width="100px">
  121. <el-form-item label="原密码" prop="oldPassword">
  122. <el-input v-model="passwordForm.oldPassword" type="password" show-password></el-input>
  123. </el-form-item>
  124. <el-form-item label="新密码" prop="newPassword">
  125. <el-input v-model="passwordForm.newPassword" type="password" show-password></el-input>
  126. </el-form-item>
  127. <el-form-item label="确认密码" prop="confirmPassword">
  128. <el-input v-model="passwordForm.confirmPassword" type="password" show-password></el-input>
  129. </el-form-item>
  130. </el-form>
  131. <span slot="footer">
  132. <el-button @click="passwordDialogVisible = false">取消</el-button>
  133. <el-button type="primary" @click="confirmChangePassword" :loading="submitting">确定</el-button>
  134. </span>
  135. </el-dialog>
  136. <el-main>
  137. <div class="main-wrapper">
  138. <router-view />
  139. </div>
  140. </el-main>
  141. </el-container>
  142. </el-container>
  143. </template>
  144. <script>
  145. import { getAdminInfo, changePassword } from '@/api/auth'
  146. import { updateAdminInfo } from '@/api/admin'
  147. import { getMyInviteCode } from '@/api/guide'
  148. import { getUnreadMessageCount } from '@/api/teacherMessage'
  149. import { getEffectivePermissions, hasPermission, getRoleLabel } from '@/utils/permissions'
  150. export default {
  151. data() {
  152. return {
  153. menuItems: [
  154. // ===== 1. 首页 (ALL) =====
  155. { path: '/dashboard', label: '首页', icon: 'el-icon-s-home', perm: 'dashboard' },
  156. // ===== 2. 家庭运营 (admin) =====
  157. { title: '家庭运营', icon: 'el-icon-s-custom', perm: 'operation',
  158. children: [
  159. { path: '/families', label: '家庭列表', icon: 'el-icon-s-custom', perm: 'family:list' },
  160. { path: '/children', label: '孩子管理', icon: 'el-icon-user-solid', perm: 'family:children' },
  161. { path: '/points', label: '积分管理', icon: 'el-icon-s-finance', perm: 'family:points' },
  162. { path: '/points-log', label: '积分记录', icon: 'el-icon-document', perm: 'family:points' },
  163. { path: '/wishes', label: '心愿管理', icon: 'el-icon-star-off', perm: 'family:wishes' },
  164. { path: '/rewards', label: '奖励管理', icon: 'el-icon-s-goods', perm: 'reward:list' },
  165. ]},
  166. // ===== 3. 任务中心 (admin) =====
  167. { title: '任务中心', icon: 'el-icon-s-order', perm: 'task:*',
  168. children: [
  169. { path: '/tasks', label: '任务列表', icon: 'el-icon-s-order', perm: 'task:list' },
  170. { path: '/task-templates', label: '任务模板', icon: 'el-icon-document', perm: 'task:templates' },
  171. { path: '/growth-task', label: '成长任务管理', icon: 'el-icon-s-management', perm: 'task:list' },
  172. ]},
  173. // ===== 4. 审核中心 (admin) =====
  174. { title: '审核中心', icon: 'el-icon-s-check', perm: 'audit',
  175. children: [
  176. { path: '/package-audit', label: '套餐审核', icon: 'el-icon-document-checked', perm: 'audit:package' },
  177. { path: '/guide-audit', label: '规划师审核', icon: 'el-icon-document-checked', perm: 'audit:guide' },
  178. { path: '/vendor-review', label: '服务商审核', icon: 'el-icon-document-checked', perm: 'audit:vendor' },
  179. { path: '/withdraw-audit', label: '提现审核', icon: 'el-icon-document-checked', perm: 'audit:withdraw' },
  180. ]},
  181. // ===== 5. 商城营销 (admin) =====
  182. { title: '商城营销', icon: 'el-icon-s-goods', perm: 'commerce',
  183. children: [
  184. { path: '/product-manage', label: '商品管理', icon: 'el-icon-s-goods', perm: 'commerce:products' },
  185. { path: '/order-manage', label: '订单管理', icon: 'el-icon-s-order', perm: 'commerce:orders' },
  186. { path: '/pending-refund', label: '待退款管理', icon: 'el-icon-warning', perm: 'commerce:orders' },
  187. { path: '/product-profit-rate', label: '产品利润率', icon: 'el-icon-data-line', perm: 'commerce:profit' },
  188. { path: '/coupon', label: '优惠券管理', icon: 'el-icon-ticket', perm: 'marketing:coupon' },
  189. { path: '/promotion', label: '推广管理', icon: 'el-icon-s-marketing', perm: 'marketing:promotion' },
  190. ]},
  191. // ===== 6. 知识中心 (admin + article_manager + activity_manager) =====
  192. { title: '知识中心', icon: 'el-icon-document', perm: 'content',
  193. children: [
  194. { path: '/article-categories', label: '知识分类', icon: 'el-icon-folder', perm: 'articles:categories' },
  195. { path: '/article-manage', label: '知识管理', icon: 'el-icon-document', perm: 'articles:manage' },
  196. { path: '/knowledge-tags', label: '知识标签', icon: 'el-icon-price-tag', perm: 'articles:categories' },
  197. { path: '/knowledge-base', label: '知识库', icon: 'el-icon-reading', perm: 'system:config' },
  198. { path: '/activities', label: '活动列表', icon: 'el-icon-date', perm: 'activity:list' },
  199. { path: '/activity-review', label: '活动审核', icon: 'el-icon-document-checked', perm: 'activity:review' },
  200. { path: '/activity-registration-review', label: '活动报名审核', icon: 'el-icon-document-checked', perm: 'activity:review' },
  201. ]},
  202. // ===== 7. 健康饮食 (admin + nutritionist) =====
  203. { title: '健康饮食', icon: 'el-icon-first-aid-kit', perm: 'health',
  204. children: [
  205. { path: '/health-reports', label: '健康报告', icon: 'el-icon-document', perm: 'health:reports' },
  206. { path: '/health-report-audit', label: '报告审核', icon: 'el-icon-document-checked', perm: 'health:reports' },
  207. { path: '/health-indicators', label: '健康指标', icon: 'el-icon-data-line', perm: 'health:indicators' },
  208. { path: '/health-checkins', label: '健康打卡', icon: 'el-icon-s-order', perm: 'health:checkins' },
  209. { path: '/emotion-alert', label: '情绪告警', icon: 'el-icon-warning', perm: 'health:checkins' },
  210. { path: '/nutrition-mappings', label: '营养素映射', icon: 'el-icon-connection', perm: 'health:mappings' },
  211. { path: '/energy-sandbox', label: '五维能量', icon: 'el-icon-data-line', perm: 'energy' },
  212. { path: '/energy-rule', label: '能量规则管理', icon: 'el-icon-setting', perm: 'energy' },
  213. { path: '/foods', label: '食材管理', icon: 'el-icon-apple', perm: 'diet:foods' },
  214. { path: '/recipes', label: '食谱管理', icon: 'el-icon-dish', perm: 'diet:recipes' },
  215. { path: '/seasonal-foods', label: '应季食材', icon: 'el-icon-sunny', perm: 'diet:seasonal' },
  216. ]},
  217. { title: '家庭服务', icon: 'el-icon-s-custom', perm: 'service:*',
  218. children: [
  219. { path: '/my-families', label: '我的家庭', icon: 'el-icon-s-custom', perm: 'service:family' },
  220. { path: '/teacher-team', label: '我的团队', icon: 'el-icon-s-custom', perm: 'service:team' },
  221. ]},
  222. { title: '业务管理', icon: 'el-icon-s-marketing', perm: 'biz:*',
  223. children: [
  224. { path: '/teacher-packages', label: '套餐管理', icon: 'el-icon-s-goods', perm: 'biz:packages' },
  225. { path: '/teacher-orders', label: '订单佣金', icon: 'el-icon-s-order', perm: 'biz:orders' },
  226. ]},
  227. { title: '测评咨询', icon: 'el-icon-edit', perm: 'assessment:*',
  228. children: [
  229. { path: '/teacher-assessment', label: 'DAN测评', icon: 'el-icon-edit', perm: 'assessment:dan' },
  230. { path: '/teacher-consult', label: '家长咨询', icon: 'el-icon-chat-dot-round', perm: 'assessment:consult' },
  231. { path: '/growth-records', label: '成长记录', icon: 'el-icon-document', perm: 'growth:records' },
  232. { path: '/growth-plans', label: '成长计划', icon: 'el-icon-document', perm: 'growth:plans' },
  233. ]},
  234. // ===== 9. 系统配置 (admin) =====
  235. { title: '系统配置', icon: 'el-icon-s-tools', perm: 'system:*',
  236. children: [
  237. // --- 基础管理 ---
  238. { title: '基础管理', icon: 'el-icon-s-tools', perm: 'system:config',
  239. children: [
  240. { path: '/sys-config', label: '系统配置', icon: 'el-icon-s-tools', perm: 'system:config' },
  241. { path: '/users', label: '用户管理', icon: 'el-icon-user', perm: 'system:users' },
  242. { path: '/operation-logs', label: '操作日志', icon: 'el-icon-s-order', perm: 'system:logs' },
  243. { path: '/membership-center', label: '会员中心', icon: 'el-icon-s-custom', perm: 'system:config' },
  244. ]},
  245. // --- 配置中心 ---
  246. { title: '配置中心', icon: 'el-icon-setting', perm: 'system:config',
  247. children: [
  248. { path: '/dimension-config', label: '维度配置', icon: 'el-icon-data-line', perm: 'system:config' },
  249. { path: '/relationship-types', label: '关系类型', icon: 'el-icon-share', perm: 'system:config' },
  250. { path: '/service-types', label: '服务类型', icon: 'el-icon-s-management', perm: 'system:config' },
  251. { path: '/service-contents', label: '服务内容', icon: 'el-icon-s-management', perm: 'system:config' },
  252. { path: '/package-templates', label: '套餐模板', icon: 'el-icon-s-management', perm: 'system:config' },
  253. ]},
  254. // --- 教育体系 ---
  255. { title: '教育体系', icon: 'el-icon-s-management', perm: 'system:education',
  256. children: [
  257. { path: '/education-systems', label: '教育体系', icon: 'el-icon-s-management', perm: 'system:education' },
  258. ]},
  259. // --- 供应商体系 ---
  260. { title: '供应商体系', icon: 'el-icon-s-management', perm: 'system:supply',
  261. children: [
  262. { path: '/supply-system', label: '供应商体系', icon: 'el-icon-s-management', perm: 'system:supply' },
  263. ]},
  264. // --- 供应商管理 (supplier_admin) ---
  265. { title: '供应商管理', icon: 'el-icon-s-shop', perm: 'supply:manage',
  266. children: [
  267. { path: '/supply-manage', label: '供应商管理', icon: 'el-icon-s-shop', perm: 'supply:manage' },
  268. ]},
  269. // --- 健康配置 ---
  270. { title: '健康配置', icon: 'el-icon-first-aid-kit', perm: 'system:config',
  271. children: [
  272. { path: '/health-norm-config', label: '健康常模', icon: 'el-icon-data-line', perm: 'system:config' },
  273. { path: '/health-data-source', label: '健康数据源', icon: 'el-icon-data-line', perm: 'system:config' },
  274. { path: '/health-energy-config', label: '七维能量配置', icon: 'el-icon-data-line', perm: 'system:config' },
  275. ]},
  276. // --- 人生维度 ---
  277. { title: '人生维度', icon: 'el-icon-s-custom', perm: 'system:config',
  278. children: [
  279. { path: '/zodiac-configs', label: '星座配置', icon: 'el-icon-s-management', perm: 'system:config' },
  280. { path: '/bazi-configs', label: '八字配置', icon: 'el-icon-s-management', perm: 'system:config' },
  281. { path: '/blood-type-configs', label: '血型配置', icon: 'el-icon-s-management', perm: 'system:config' },
  282. ]},
  283. // --- 测评管理 ---
  284. { title: '测评管理', icon: 'el-icon-edit', perm: 'assessment:dan',
  285. children: [
  286. { path: '/assessment-admin', label: '测评管理', icon: 'el-icon-edit', perm: 'assessment:dan' },
  287. ]},
  288. // --- 虚拟团队 ---
  289. { title: '虚拟团队', icon: 'el-icon-s-custom', perm: 'system:config',
  290. children: [
  291. { path: '/virtual-teams', label: '虚拟团队', icon: 'el-icon-s-custom', perm: 'system:config' },
  292. ]},
  293. ]},
  294. ],
  295. profileDialogVisible: false,
  296. passwordDialogVisible: false,
  297. submitting: false,
  298. profileForm: {
  299. nickname: '',
  300. phone: '',
  301. email: '',
  302. username: '',
  303. realName: ''
  304. },
  305. passwordForm: {
  306. oldPassword: '',
  307. newPassword: '',
  308. confirmPassword: ''
  309. },
  310. inviteCodeVisible: false,
  311. inviteCodeLoading: false,
  312. inviteCodeData: null,
  313. unreadMessageCount: 0,
  314. passwordRules: {
  315. oldPassword: [
  316. { required: true, message: '请输入原密码', trigger: 'blur' }
  317. ],
  318. newPassword: [
  319. { required: true, message: '请输入新密码', trigger: 'blur' },
  320. { min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
  321. ],
  322. confirmPassword: [
  323. { required: true, message: '请确认新密码', trigger: 'blur' },
  324. {
  325. validator: (rule, value, callback) => {
  326. if (value !== this.passwordForm.newPassword) {
  327. callback(new Error('两次输入密码不一致'))
  328. } else {
  329. callback()
  330. }
  331. },
  332. trigger: 'blur'
  333. }
  334. ]
  335. }
  336. }
  337. },
  338. computed: {
  339. activeMenu() {
  340. return this.$route.path
  341. },
  342. currentRole() {
  343. return localStorage.getItem('role') || 'admin'
  344. },
  345. vendorType() {
  346. return localStorage.getItem('vendorType') || ''
  347. },
  348. effectivePerms() {
  349. const rolesStr = localStorage.getItem('roles')
  350. const roles = rolesStr ? JSON.parse(rolesStr) : [this.currentRole]
  351. return getEffectivePermissions(roles)
  352. },
  353. displayRoles() {
  354. const rolesStr = localStorage.getItem('roles')
  355. const roles = rolesStr ? JSON.parse(rolesStr) : [this.currentRole]
  356. return roles.map(function(r) { return getRoleLabel(r) }).join(' / ')
  357. },
  358. displayUserLabel() {
  359. const adminName = localStorage.getItem('adminName')
  360. if (adminName) {
  361. return adminName + ' (' + this.displayRoles + ')'
  362. }
  363. return this.displayRoles
  364. }
  365. },
  366. mounted() {
  367. // 清除可能的 beforeunload 处理,防止页面刷新弹出"离开网站?"提示
  368. window.onbeforeunload = null
  369. // 有消息权限(老师/营养师角色)才加载未读数,管理员不调用
  370. if (hasPermission(this.effectivePerms, 'messages') && this.currentRole !== 'admin') {
  371. this.fetchUnreadCount()
  372. }
  373. },
  374. methods: {
  375. hasPerm(required) {
  376. return hasPermission(this.effectivePerms, required)
  377. },
  378. getMenuIndex(item) {
  379. return item.path === '/dashboard' && this.currentRole === 'teacher'
  380. ? '/teacher-dashboard'
  381. : item.path
  382. },
  383. handleMenuSelect(index) {
  384. if (index && index.startsWith('/')) {
  385. this.$router.push(index)
  386. }
  387. },
  388. handleProfileCommand(command) {
  389. if (command === 'profile') {
  390. this.loadAdminInfo()
  391. this.profileDialogVisible = true
  392. } else if (command === 'password') {
  393. this.passwordForm = { oldPassword: '', newPassword: '', confirmPassword: '' }
  394. this.passwordDialogVisible = true
  395. } else if (command === 'logout') {
  396. this.handleLogout()
  397. }
  398. },
  399. async loadAdminInfo() {
  400. try {
  401. const res = await getAdminInfo()
  402. this.profileForm = {
  403. nickname: res.data.nickname || '',
  404. phone: res.data.phone || '',
  405. email: res.data.email || '',
  406. username: res.data.username || '',
  407. realName: res.data.realName || ''
  408. }
  409. } catch (e) {
  410. this.$message.error('加载个人信息失败')
  411. }
  412. },
  413. async confirmUpdateInfo() {
  414. this.submitting = true
  415. try {
  416. await updateAdminInfo({
  417. nickname: this.profileForm.nickname,
  418. phone: this.profileForm.phone,
  419. email: this.profileForm.email
  420. })
  421. if (this.profileForm.nickname) {
  422. localStorage.setItem('adminName', this.profileForm.nickname)
  423. }
  424. this.$message.success('保存成功')
  425. this.profileDialogVisible = false
  426. } catch (e) {
  427. this.$message.error(e.message || '保存失败')
  428. } finally {
  429. this.submitting = false
  430. }
  431. },
  432. async confirmChangePassword() {
  433. this.$refs.passwordForm.validate(async (valid) => {
  434. if (!valid) return
  435. this.submitting = true
  436. try {
  437. await changePassword({
  438. oldPassword: this.passwordForm.oldPassword,
  439. newPassword: this.passwordForm.newPassword
  440. })
  441. this.$message.success('密码修改成功')
  442. this.passwordDialogVisible = false
  443. } catch (e) {
  444. this.$message.error(e.message || '密码修改失败')
  445. } finally {
  446. this.submitting = false
  447. }
  448. })
  449. },
  450. async showInviteCode() {
  451. this.inviteCodeVisible = true
  452. this.inviteCodeLoading = true
  453. this.inviteCodeData = null
  454. try {
  455. const res = await getMyInviteCode()
  456. if (res.data) {
  457. this.inviteCodeData = res.data
  458. }
  459. } catch (e) {
  460. this.$message.error('获取邀请码失败')
  461. } finally {
  462. this.inviteCodeLoading = false
  463. }
  464. },
  465. async generateInviteCode() {
  466. this.inviteCodeLoading = true
  467. try {
  468. const res = await getMyInviteCode()
  469. if (res.data) {
  470. this.inviteCodeData = res.data
  471. this.$message.success('邀请码已生成')
  472. }
  473. } catch (e) {
  474. this.$message.error('生成邀请码失败')
  475. } finally {
  476. this.inviteCodeLoading = false
  477. }
  478. },
  479. copyCode(code) {
  480. if (!code) return
  481. const input = document.createElement('input')
  482. input.value = code
  483. document.body.appendChild(input)
  484. input.select()
  485. document.execCommand('copy')
  486. document.body.removeChild(input)
  487. this.$message.success('已复制邀请码')
  488. },
  489. async fetchUnreadCount() {
  490. try {
  491. const res = await getUnreadMessageCount()
  492. if (res.data !== undefined) {
  493. this.unreadMessageCount = res.data || 0
  494. }
  495. } catch (e) {
  496. // silent: not critical
  497. }
  498. },
  499. handleMessageClick() {
  500. this.$router.push('/teacher-messages')
  501. },
  502. handleLogout() {
  503. // 不使用 modal 遮罩(全局 .v-modal { display:none } 会与 $confirm 的遮罩冲突)
  504. this.$confirm('确定要退出登录吗?', '提示', {
  505. confirmButtonText: '确定',
  506. cancelButtonText: '取消',
  507. type: 'warning',
  508. modal: false,
  509. customClass: 'cfc-logout-confirm'
  510. }).then(() => {
  511. localStorage.removeItem('token')
  512. localStorage.removeItem('role')
  513. localStorage.removeItem('roles')
  514. localStorage.removeItem('adminId')
  515. localStorage.removeItem('adminName')
  516. this.$router.push('/login')
  517. })
  518. }
  519. }
  520. }
  521. </script>
  522. <style>
  523. /* 全局防御:防止 Element UI 弹窗或组件残留 DOM 产生遮罩覆盖 */
  524. .v-modal {
  525. display: none !important;
  526. }
  527. .layout-container {
  528. height: 100vh;
  529. }
  530. .el-aside {
  531. background-color: #304156;
  532. overflow: visible;
  533. display: flex;
  534. flex-direction: column;
  535. }
  536. .logo {
  537. height: 60px;
  538. line-height: 60px;
  539. text-align: center;
  540. color: #fff;
  541. font-size: 20px;
  542. font-weight: bold;
  543. background-color: #2b3a4a;
  544. flex-shrink: 0;
  545. }
  546. /* 侧边栏滚动条样式 */
  547. .sidebar-scrollbar-wrap {
  548. height: calc(100vh - 60px) !important;
  549. overflow-x: hidden !important;
  550. overflow-y: auto !important;
  551. }
  552. .sidebar-scrollbar-wrap .el-scrollbar__view {
  553. height: 100%;
  554. }
  555. .sidebar-scrollbar-wrap::-webkit-scrollbar {
  556. width: 6px;
  557. }
  558. .sidebar-scrollbar-wrap::-webkit-scrollbar-track {
  559. background: #304156;
  560. }
  561. .sidebar-scrollbar-wrap::-webkit-scrollbar-thumb {
  562. background: #4a5568;
  563. border-radius: 3px;
  564. }
  565. .sidebar-scrollbar-wrap::-webkit-scrollbar-thumb:hover {
  566. background: #5a6578;
  567. }
  568. .el-header {
  569. background-color: #fff;
  570. box-shadow: 0 1px 4px rgba(0,21,41,.08);
  571. display: flex;
  572. justify-content: flex-end;
  573. align-items: center;
  574. padding: 0 20px;
  575. }
  576. .header-right {
  577. display: flex;
  578. align-items: center;
  579. gap: 12px;
  580. }
  581. .username {
  582. margin-right: 20px;
  583. color: #606266;
  584. }
  585. .invite-code-btn {
  586. color: #6366F1;
  587. font-size: 13px;
  588. }
  589. .invite-code-btn:hover {
  590. color: #4F46E5;
  591. }
  592. .msg-badge {
  593. margin-right: 4px;
  594. }
  595. .msg-badge .el-badge__content.is-fixed {
  596. top: 10px;
  597. right: 6px;
  598. }
  599. .msg-btn {
  600. font-size: 18px;
  601. color: #606266;
  602. padding: 4px;
  603. }
  604. .msg-btn:hover {
  605. color: #6366F1;
  606. }
  607. .invite-code-content {
  608. padding: 8px 0;
  609. }
  610. .invite-code-section {
  611. background: #F8FAFC;
  612. border: 1px solid #E2E8F0;
  613. border-radius: 8px;
  614. padding: 20px;
  615. text-align: center;
  616. }
  617. .invite-code-label {
  618. font-size: 14px;
  619. color: #64748B;
  620. margin-bottom: 12px;
  621. }
  622. .invite-code-value {
  623. display: flex;
  624. align-items: center;
  625. justify-content: center;
  626. gap: 12px;
  627. margin-bottom: 8px;
  628. }
  629. .code-text {
  630. font-size: 28px;
  631. font-weight: 700;
  632. letter-spacing: 6px;
  633. color: #1E293B;
  634. font-family: monospace;
  635. }
  636. .no-invite-code {
  637. text-align: center;
  638. padding: 24px;
  639. color: #94A3B8;
  640. }
  641. .no-invite-code i {
  642. font-size: 48px;
  643. margin-bottom: 12px;
  644. }
  645. .no-invite-code p {
  646. margin-bottom: 16px;
  647. }
  648. .el-main {
  649. background-color: #f0f2f5;
  650. padding: 20px;
  651. position: relative;
  652. z-index: 1;
  653. display: flex;
  654. flex-direction: column;
  655. min-height: 0; /* flexbox overflow fix */
  656. }
  657. .main-wrapper {
  658. flex: 1;
  659. display: flex;
  660. flex-direction: column;
  661. min-height: 0;
  662. overflow-y: auto;
  663. overflow-x: hidden;
  664. }
  665. /* ========== 全局 .admin-page 样式 ========== */
  666. /* 所有后台维护页面根元素使用此 class,使页面内容充满可视区 */
  667. .admin-page {
  668. flex: 1;
  669. display: flex;
  670. flex-direction: column;
  671. min-height: 0;
  672. }
  673. /* 当 admin-page 的直接子元素是 el-card 时让卡片自动填充 */
  674. .admin-page > .el-card {
  675. flex: 1;
  676. display: flex;
  677. flex-direction: column;
  678. min-height: 0;
  679. }
  680. .admin-page > .el-card .el-card__body {
  681. flex: 1;
  682. display: flex;
  683. flex-direction: column;
  684. min-height: 0;
  685. padding: 20px;
  686. overflow-y: auto;
  687. }
  688. /* 当 admin-page > div > el-card(如 ArticleManage 的嵌套 div) */
  689. .admin-page > div:only-child {
  690. flex: 1;
  691. display: flex;
  692. flex-direction: column;
  693. min-height: 0;
  694. }
  695. .admin-page > div:only-child > .el-card {
  696. flex: 1;
  697. display: flex;
  698. flex-direction: column;
  699. min-height: 0;
  700. }
  701. .admin-page > div:only-child > .el-card .el-card__body {
  702. flex: 1;
  703. display: flex;
  704. flex-direction: column;
  705. min-height: 0;
  706. padding: 20px;
  707. overflow-y: auto;
  708. }
  709. </style>