Commit 19415acca8eeec4163b219f2813a5103537ac188

Authored by “wangming”
1 parent 7824135f

Enhance Form.vue with location search functionality and improved user experience…

…. Added address input for store location retrieval, integrated Tencent Maps API for geolocation, and updated form fields to include longitude, latitude, and fence polygons. Adjusted dialog and layout for better usability.
antis-ncc-admin/src/views/lqMdxx/Form.vue
@@ -107,11 +107,23 @@ @@ -107,11 +107,23 @@
107 <el-dialog title="门店地图定位" :visible.sync="locationVisible" width="800px" append-to-body class="map-dialog"> 107 <el-dialog title="门店地图定位" :visible.sync="locationVisible" width="800px" append-to-body class="map-dialog">
108 <div class="map-container-wrapper"> 108 <div class="map-container-wrapper">
109 <div class="map-header-bar"> 109 <div class="map-header-bar">
110 - <span>在地图上点击以选择门店位置(仅支持单个标记)</span> 110 + <span>在地图上点击以选择门店位置,或通过地址检索定位</span>
111 <div class="coordinate-info" v-if="tempMarker.lng"> 111 <div class="coordinate-info" v-if="tempMarker.lng">
112 当前选择:{{ tempMarker.lng.toFixed(6) }}, {{ tempMarker.lat.toFixed(6) }} 112 当前选择:{{ tempMarker.lng.toFixed(6) }}, {{ tempMarker.lat.toFixed(6) }}
113 </div> 113 </div>
114 </div> 114 </div>
  115 + <div class="location-search-bar">
  116 + <el-input
  117 + v-model="locationSearchKeyword"
  118 + placeholder="输入地址或关键词检索(如:成都市武侯区紫荆北路182号)"
  119 + clearable
  120 + size="small"
  121 + class="search-input"
  122 + @keyup.enter.native="handleSearchLocation"
  123 + >
  124 + <el-button slot="append" icon="el-icon-search" @click="handleSearchLocation" :loading="locationSearchLoading">检索</el-button>
  125 + </el-input>
  126 + </div>
115 <div id="location-map" class="map-canvas"></div> 127 <div id="location-map" class="map-canvas"></div>
116 </div> 128 </div>
117 <span slot="footer" class="dialog-footer"> 129 <span slot="footer" class="dialog-footer">
@@ -163,6 +175,29 @@ @@ -163,6 +175,29 @@
163 <script> 175 <script>
164 import request from '@/utils/request' 176 import request from '@/utils/request'
165 177
  178 +// 腾讯地图 WebService API Key(与地图脚本共用,需在腾讯控制台开启 WebServiceAPI)
  179 +const TMAP_WS_KEY = 'YRXBZ-NEV6T-K7SXH-VJPMF-G5IQF-F3FCJ'
  180 +
  181 +// JSONP 调用(绕过 CORS,腾讯 WebService API 前端需用 JSONP)
  182 +function jsonp(url) {
  183 + return new Promise((resolve, reject) => {
  184 + const cbName = '_tmap_jsonp_' + Date.now() + '_' + Math.random().toString(36).slice(2);
  185 + const script = document.createElement('script');
  186 + script.src = url + (url.indexOf('?') >= 0 ? '&' : '?') + 'output=jsonp&callback=' + cbName;
  187 + window[cbName] = function(res) {
  188 + if (script.parentNode) script.parentNode.removeChild(script);
  189 + delete window[cbName];
  190 + resolve(res);
  191 + };
  192 + script.onerror = function() {
  193 + if (script.parentNode) script.parentNode.removeChild(script);
  194 + delete window[cbName];
  195 + reject(new Error('JSONP request failed'));
  196 + };
  197 + document.body.appendChild(script);
  198 + });
  199 +}
  200 +
166 export default { 201 export default {
167 data() { 202 data() {
168 return { 203 return {
@@ -198,6 +233,8 @@ export default { @@ -198,6 +233,8 @@ export default {
198 locationMap: null, 233 locationMap: null,
199 locationMarkerLayer: null, 234 locationMarkerLayer: null,
200 tempMarker: { lng: null, lat: null }, 235 tempMarker: { lng: null, lat: null },
  236 + locationSearchKeyword: '',
  237 + locationSearchLoading: false,
201 238
202 // 围栏弹窗相关 239 // 围栏弹窗相关
203 fenceVisible: false, 240 fenceVisible: false,
@@ -237,8 +274,34 @@ export default { @@ -237,8 +274,34 @@ export default {
237 if (this.dataForm.id) { 274 if (this.dataForm.id) {
238 request({ url: '/api/Extend/LqMdxx/' + this.dataForm.id, method: 'get' }) 275 request({ url: '/api/Extend/LqMdxx/' + this.dataForm.id, method: 'get' })
239 .then(res => { 276 .then(res => {
240 - this.dataForm = res.data; 277 + const raw = res.data || {};
  278 + // 合并接口数据,保留 dataForm 默认结构
  279 + this.dataForm = {
  280 + ...this.dataForm,
  281 + ...raw,
  282 + // 经纬度:兼容后端 camelCase / PascalCase(避免 ?? 语法,兼容旧 Babel)
  283 + longitude: raw.longitude != null ? raw.longitude : (raw.Longitude != null ? raw.Longitude : this.dataForm.longitude),
  284 + latitude: raw.latitude != null ? raw.latitude : (raw.Latitude != null ? raw.Latitude : this.dataForm.latitude)
  285 + };
  286 + // fencePolygons:后端返回 JSON 字符串需解析为数组,兼容 fence_polygons 字段名
  287 + const fpRaw = raw.fencePolygons != null ? raw.fencePolygons : (raw.fence_polygons != null ? raw.fence_polygons : raw.FencePolygons);
  288 + if (typeof fpRaw === 'string') {
  289 + try {
  290 + this.dataForm.fencePolygons = JSON.parse(fpRaw) || [];
  291 + } catch (e) {
  292 + this.dataForm.fencePolygons = [];
  293 + }
  294 + } else if (Array.isArray(fpRaw)) {
  295 + this.dataForm.fencePolygons = fpRaw;
  296 + } else {
  297 + this.dataForm.fencePolygons = Array.isArray(this.dataForm.fencePolygons) ? this.dataForm.fencePolygons : [];
  298 + }
241 }); 299 });
  300 + } else {
  301 + // 新建时重置经纬度与围栏
  302 + this.dataForm.longitude = null;
  303 + this.dataForm.latitude = null;
  304 + this.dataForm.fencePolygons = [];
242 } 305 }
243 }) 306 })
244 }, 307 },
@@ -257,12 +320,111 @@ export default { @@ -257,12 +320,111 @@ export default {
257 // --- 门店定位逻辑 --- 320 // --- 门店定位逻辑 ---
258 handleOpenLocation() { 321 handleOpenLocation() {
259 this.locationVisible = true; 322 this.locationVisible = true;
260 - this.tempMarker = { lng: this.dataForm.longitude, lat: this.dataForm.latitude }; 323 + // 若已有经纬度,则以门店经纬度为中心;否则先不设置,等 IP 定位
  324 + this.tempMarker = {
  325 + lng: this.dataForm.longitude,
  326 + lat: this.dataForm.latitude
  327 + };
  328 + // 仅作为默认检索关键字,不自动发起检索
  329 + this.locationSearchKeyword = this.dataForm.dz || '';
  330 +
261 this.$nextTick(() => { 331 this.$nextTick(() => {
262 - this.initLocationMap(); 332 + // 如果已有经纬度,直接按门店坐标初始化地图
  333 + if (this.dataForm.longitude && this.dataForm.latitude) {
  334 + this.initLocationMap();
  335 + } else {
  336 + // 无经纬度时,优先用 IP 做一个大概定位,再初始化地图
  337 + this.initLocationWithIp();
  338 + }
263 }); 339 });
264 }, 340 },
265 341
  342 + // 使用腾讯地图 IP 定位作为初始中心(仅在没有门店经纬度时调用)
  343 + initLocationWithIp() {
  344 + const ipUrl = 'https://apis.map.qq.com/ws/location/v1/ip?key=' + TMAP_WS_KEY;
  345 + // 先尝试 IP 定位(JSONP),成功后设置 tempMarker 再初始化地图;失败则走默认中心
  346 + jsonp(ipUrl)
  347 + .then((res) => {
  348 + if (res.status === 0 && res.result && res.result.location) {
  349 + const loc = res.result.location;
  350 + this.tempMarker = { lng: loc.lng, lat: loc.lat };
  351 + // 同步一下城市,便于后续检索时 region 使用
  352 + if (res.result.ad_info && res.result.ad_info.city && !this.dataForm.cs) {
  353 + this.dataForm.cs = res.result.ad_info.city;
  354 + }
  355 + }
  356 + this.initLocationMap();
  357 + })
  358 + .catch(() => {
  359 + this.initLocationMap();
  360 + });
  361 + },
  362 +
  363 + // 地址检索:使用腾讯地图 WebService API(地址解析 + 地点搜索)
  364 + handleSearchLocation() {
  365 + const keyword = (this.locationSearchKeyword || '').trim();
  366 + if (!keyword) {
  367 + this.$message.warning('请输入地址或关键词');
  368 + return;
  369 + }
  370 + this.locationSearchLoading = true;
  371 + const region = this.dataForm.cs || '';
  372 +
  373 + // 1. 先调用地址解析 API(适合完整地址),使用 JSONP 绕过 CORS
  374 + const geocoderUrl = 'https://apis.map.qq.com/ws/geocoder/v1/?address=' + encodeURIComponent(keyword) + '&key=' + TMAP_WS_KEY + (region ? '&region=' + encodeURIComponent(region) : '');
  375 + jsonp(geocoderUrl)
  376 + .then((res) => {
  377 + if (res.status === 0 && res.result && res.result.location) {
  378 + const loc = res.result.location;
  379 + this.applySearchResult(loc.lat, loc.lng);
  380 + this.locationSearchLoading = false;
  381 + this.$message.success('检索成功');
  382 + } else {
  383 + this.searchByKeyword(keyword, region);
  384 + }
  385 + })
  386 + .catch(() => {
  387 + this.searchByKeyword(keyword, region);
  388 + });
  389 + },
  390 +
  391 + // 关键词搜索:使用腾讯地图地点搜索 WebService API
  392 + searchByKeyword(keyword, region) {
  393 + const cityName = region || '全国';
  394 + const boundary = 'region(' + cityName + ',1)';
  395 + const searchUrl = 'https://apis.map.qq.com/ws/place/v1/search?keyword=' + encodeURIComponent(keyword) + '&boundary=' + encodeURIComponent(boundary) + '&page_size=10&key=' + TMAP_WS_KEY;
  396 + jsonp(searchUrl)
  397 + .then((res) => {
  398 + this.locationSearchLoading = false;
  399 + if (res.status === 0 && res.data && res.data.length > 0) {
  400 + const first = res.data[0];
  401 + const loc = first.location;
  402 + this.applySearchResult(loc.lat, loc.lng);
  403 + this.$message.success('检索成功');
  404 + } else {
  405 + this.$message.warning('未找到匹配结果,请尝试其他关键词或在地图上直接点击选择');
  406 + }
  407 + })
  408 + .catch(() => {
  409 + this.locationSearchLoading = false;
  410 + this.$message.error('检索失败,请稍后再试');
  411 + });
  412 + },
  413 +
  414 + // 将检索结果应用到地图(移动中心、设置标记)
  415 + applySearchResult(lat, lng) {
  416 + this.tempMarker = { lng: lng, lat: lat };
  417 + if (window.TMap && this.locationMarkerLayer) {
  418 + const TMap = window.TMap;
  419 + this.locationMarkerLayer.setGeometries([{ id: 'm', position: new TMap.LatLng(lat, lng) }]);
  420 + }
  421 + if (window.TMap && this.locationMap) {
  422 + const TMap = window.TMap;
  423 + this.locationMap.setCenter(new TMap.LatLng(lat, lng));
  424 + this.locationMap.setZoom(16);
  425 + }
  426 + },
  427 +
266 initLocationMap() { 428 initLocationMap() {
267 this.loadTMapScript().then(() => { 429 this.loadTMapScript().then(() => {
268 const TMap = window.TMap; 430 const TMap = window.TMap;
@@ -515,10 +677,15 @@ export default { @@ -515,10 +677,15 @@ export default {
515 this.$refs['elForm'].validate((valid) => { 677 this.$refs['elForm'].validate((valid) => {
516 if (!valid) return; 678 if (!valid) return;
517 const isNew = !this.dataForm.id; 679 const isNew = !this.dataForm.id;
  680 + // 提交前:fencePolygons 若为数组则序列化为 JSON 字符串,与后端约定一致
  681 + const submitData = { ...this.dataForm };
  682 + if (Array.isArray(submitData.fencePolygons)) {
  683 + submitData.fencePolygons = JSON.stringify(submitData.fencePolygons);
  684 + }
518 request({ 685 request({
519 url: isNew ? '/api/Extend/LqMdxx' : `/api/Extend/LqMdxx/${this.dataForm.id}`, 686 url: isNew ? '/api/Extend/LqMdxx' : `/api/Extend/LqMdxx/${this.dataForm.id}`,
520 method: isNew ? 'POST' : 'PUT', 687 method: isNew ? 'POST' : 'PUT',
521 - data: this.dataForm 688 + data: submitData
522 }).then((res) => { 689 }).then((res) => {
523 this.$message({ 690 this.$message({
524 message: res.msg, 691 message: res.msg,
@@ -568,6 +735,14 @@ export default { @@ -568,6 +735,14 @@ export default {
568 font-weight: bold; 735 font-weight: bold;
569 } 736 }
570 } 737 }
  738 +
  739 + .location-search-bar {
  740 + margin-bottom: 10px;
  741 +
  742 + .search-input {
  743 + width: 100%;
  744 + }
  745 + }
571 } 746 }
572 747
573 .map-canvas { 748 .map-canvas {
docs/数据库说明.md
@@ -53,6 +53,9 @@ @@ -53,6 +53,9 @@
53 53
54 ### 1. 门店与人员关系 54 ### 1. 门店与人员关系
55 - **门店信息表**: `lq_mdxx` (门店基础信息) 55 - **门店信息表**: `lq_mdxx` (门店基础信息)
  56 + - `longitude` (DECIMAL): 经度
  57 + - `latitude` (DECIMAL): 纬度
  58 + - `fence_polygons` (JSON): 电子围栏多边形坐标,格式 `[[{lng,lat},...]]`
56 - **人员信息**: `BASE_USER` (系统用户表,包含门店ID等扩展字段) 59 - **人员信息**: `BASE_USER` (系统用户表,包含门店ID等扩展字段)
57 - **关联字段**: `BASE_USER.F_MDID` ↔ `lq_mdxx.F_Id` 60 - **关联字段**: `BASE_USER.F_MDID` ↔ `lq_mdxx.F_Id`
58 61
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqMdxx/LqMdxxCrInput.cs
1 -using System; 1 +using System;
2 using System.Collections.Generic; 2 using System.Collections.Generic;
3 3
4 namespace NCC.Extend.Entitys.Dto.LqMdxx 4 namespace NCC.Extend.Entitys.Dto.LqMdxx
@@ -44,6 +44,21 @@ namespace NCC.Extend.Entitys.Dto.LqMdxx @@ -44,6 +44,21 @@ namespace NCC.Extend.Entitys.Dto.LqMdxx
44 public string dz { get; set; } 44 public string dz { get; set; }
45 45
46 /// <summary> 46 /// <summary>
  47 + /// 经度
  48 + /// </summary>
  49 + public decimal? longitude { get; set; }
  50 +
  51 + /// <summary>
  52 + /// 纬度
  53 + /// </summary>
  54 + public decimal? latitude { get; set; }
  55 +
  56 + /// <summary>
  57 + /// 电子围栏多边形坐标,JSON 字符串,格式:[[{lng,lat},...]]
  58 + /// </summary>
  59 + public string fencePolygons { get; set; }
  60 +
  61 + /// <summary>
47 /// 姓名 62 /// 姓名
48 /// </summary> 63 /// </summary>
49 public string xm { get; set; } 64 public string xm { get; set; }
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqMdxx/LqMdxxInfoOutput.cs
1 -using System; 1 +using System;
2 using System.Collections.Generic; 2 using System.Collections.Generic;
3 using NCC.Code; 3 using NCC.Code;
4 using NCC.Extend.Entitys.Enum; 4 using NCC.Extend.Entitys.Enum;
@@ -46,6 +46,21 @@ namespace NCC.Extend.Entitys.Dto.LqMdxx @@ -46,6 +46,21 @@ namespace NCC.Extend.Entitys.Dto.LqMdxx
46 public string dz { get; set; } 46 public string dz { get; set; }
47 47
48 /// <summary> 48 /// <summary>
  49 + /// 经度
  50 + /// </summary>
  51 + public decimal? longitude { get; set; }
  52 +
  53 + /// <summary>
  54 + /// 纬度
  55 + /// </summary>
  56 + public decimal? latitude { get; set; }
  57 +
  58 + /// <summary>
  59 + /// 电子围栏多边形坐标,JSON 字符串,格式:[[{lng,lat},...]]
  60 + /// </summary>
  61 + public string fencePolygons { get; set; }
  62 +
  63 + /// <summary>
49 /// 姓名 64 /// 姓名
50 /// </summary> 65 /// </summary>
51 public string xm { get; set; } 66 public string xm { get; set; }
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_mdxx/LqMdxxEntity.cs
1 -using NCC.Common.Const; 1 +using NCC.Common.Const;
2 using SqlSugar; 2 using SqlSugar;
3 using System; 3 using System;
4 4
@@ -54,6 +54,24 @@ namespace NCC.Extend.Entitys.lq_mdxx @@ -54,6 +54,24 @@ namespace NCC.Extend.Entitys.lq_mdxx
54 public string Dz { get; set; } 54 public string Dz { get; set; }
55 55
56 /// <summary> 56 /// <summary>
  57 + /// 经度
  58 + /// </summary>
  59 + [SugarColumn(ColumnName = "longitude")]
  60 + public decimal? Longitude { get; set; }
  61 +
  62 + /// <summary>
  63 + /// 纬度
  64 + /// </summary>
  65 + [SugarColumn(ColumnName = "latitude")]
  66 + public decimal? Latitude { get; set; }
  67 +
  68 + /// <summary>
  69 + /// 电子围栏多边形坐标,JSON 格式:[[{lng,lat},...]]
  70 + /// </summary>
  71 + [SugarColumn(ColumnName = "fence_polygons")]
  72 + public string FencePolygons { get; set; }
  73 +
  74 + /// <summary>
57 /// 姓名 75 /// 姓名
58 /// </summary> 76 /// </summary>
59 [SugarColumn(ColumnName = "xm")] 77 [SugarColumn(ColumnName = "xm")]
sql/添加门店经纬度与电子围栏字段.sql 0 → 100644
  1 +-- 门店信息表添加经纬度与电子围栏字段
  2 +-- 用于门店编辑页面的地图定位与电子围栏功能
  3 +
  4 +ALTER TABLE lq_mdxx
  5 + ADD COLUMN longitude DECIMAL(10, 6) NULL COMMENT '经度' AFTER dz,
  6 + ADD COLUMN latitude DECIMAL(10, 6) NULL COMMENT '纬度' AFTER longitude,
  7 + ADD COLUMN fence_polygons JSON NULL COMMENT '电子围栏多边形坐标,格式:[[{lng,lat},...]]' AFTER latitude;
store-pc/src/components/CompanyCalendarDialog.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + :visible.sync="visibleProxy"
  4 + :show-close="false"
  5 + width="90%"
  6 + :close-on-click-modal="false"
  7 + custom-class="company-calendar-dialog"
  8 + append-to-body
  9 + >
  10 + <div class="dialog-inner">
  11 + <div class="dialog-header">
  12 + <div class="dialog-title">公司日历</div>
  13 + <div class="dialog-legend">
  14 + <span v-for="item in legendList" :key="item.type" class="legend-item">
  15 + <span class="legend-dot" :style="{ background: item.color }"></span>
  16 + {{ item.label }}
  17 + </span>
  18 + </div>
  19 + <span class="dialog-close" @click="visibleProxy = false"><i class="el-icon-close"></i></span>
  20 + </div>
  21 +
  22 + <div class="dialog-content" v-loading="loading">
  23 + <FullCalendar
  24 + ref="fullCalendar"
  25 + class="company-calendar"
  26 + defaultView="dayGridMonth"
  27 + :header="calendarHeader"
  28 + :plugins="calendarPlugins"
  29 + :weekends="true"
  30 + :events="calendarEvents"
  31 + locale="zh-cn"
  32 + :buttonText="buttonText"
  33 + :height="calendarHeight"
  34 + :eventLimit="true"
  35 + allDayText="全天"
  36 + :editable="false"
  37 + @datesRender="datesRender"
  38 + @eventClick="handleEventClick"
  39 + />
  40 + </div>
  41 + </div>
  42 + </el-dialog>
  43 +</template>
  44 +
  45 +<script>
  46 +import FullCalendar from '@fullcalendar/vue'
  47 +import dayGridPlugin from '@fullcalendar/daygrid'
  48 +import timeGridPlugin from '@fullcalendar/timegrid'
  49 +import interactionPlugin from '@fullcalendar/interaction'
  50 +
  51 +export default {
  52 + name: 'CompanyCalendarDialog',
  53 + components: { FullCalendar },
  54 + props: {
  55 + visible: { type: Boolean, default: false }
  56 + },
  57 + data() {
  58 + const today = new Date()
  59 + const y = today.getFullYear()
  60 + const m = String(today.getMonth() + 1).padStart(2, '0')
  61 + const d = String(today.getDate()).padStart(2, '0')
  62 + const todayStr = `${y}-${m}-${d}`
  63 + return {
  64 + loading: false,
  65 + // 公司层面的会议/活动数据(后续可换成接口)
  66 + companyEvents: [
  67 + // 当天 7 条示例(操作会 + 大会 + 活动),方便预览密集情况
  68 + { id: 'E001', date: todayStr, start: `${todayStr}T09:00:00`, end: `${todayStr}T10:00:00`, type: 'ops', title: '操作会 - 紫荆店' },
  69 + { id: 'E002', date: todayStr, start: `${todayStr}T10:30:00`, end: `${todayStr}T11:30:00`, type: 'ops', title: '操作会 - 西沙店' },
  70 + { id: 'E003', date: todayStr, start: `${todayStr}T13:00:00`, end: `${todayStr}T14:00:00`, type: 'training', title: '培训会 - 新员工入职营' },
  71 + { id: 'E004', date: todayStr, start: `${todayStr}T14:30:00`, end: `${todayStr}T15:30:00`, type: 'training', title: '培训会 - 科美新品讲解' },
  72 + { id: 'E005', date: todayStr, start: `${todayStr}T16:00:00`, end: `${todayStr}T17:00:00`, type: 'activity', title: '门店活动 - 紫荆店会员沙龙' },
  73 + { id: 'E006', date: todayStr, start: `${todayStr}T18:00:00`, end: `${todayStr}T19:00:00`, type: 'activity', title: '门店活动 - 西沙店体验日' },
  74 + { id: 'E007', date: todayStr, start: `${todayStr}T19:30:00`, end: `${todayStr}T21:00:00`, type: 'meeting', title: '全体大会 - 本月复盘' },
  75 + // 其它日期:尽量覆盖当月大部分天
  76 + { id: 'E008', date: `${y}-${m}-01`, start: `${y}-${m}-01T10:00:00`, end: `${y}-${m}-01T12:00:00`, type: 'meeting', title: '月初经营例会' },
  77 + { id: 'E009', date: `${y}-${m}-02`, start: `${y}-${m}-02T14:00:00`, end: `${y}-${m}-02T16:00:00`, type: 'ops', title: '操作会 - 静居寺店' },
  78 + { id: 'E010', date: `${y}-${m}-03`, start: `${y}-${m}-03T09:30:00`, end: `${y}-${m}-03T11:00:00`, type: 'training', title: '培训会 - 会员沟通话术' },
  79 + { id: 'E011', date: `${y}-${m}-04`, start: `${y}-${m}-04T15:00:00`, end: `${y}-${m}-04T17:00:00`, type: 'activity', title: '门店活动 - 双流店开放日' },
  80 + { id: 'E012', date: `${y}-${m}-05`, start: `${y}-${m}-05T10:00:00`, end: `${y}-${m}-05T11:30:00`, type: 'ops', title: '操作会 - 保利店' },
  81 + { id: 'E013', date: `${y}-${m}-06`, start: `${y}-${m}-06T14:00:00`, end: `${y}-${m}-06T15:30:00`, type: 'training', title: '培训会 - 逆龄项目进阶' },
  82 + { id: 'E014', date: `${y}-${m}-07`, start: `${y}-${m}-07T19:00:00`, end: `${y}-${m}-07T20:30:00`, type: 'activity', title: '门店活动 - 线上直播专场' },
  83 + { id: 'E015', date: `${y}-${m}-08`, start: `${y}-${m}-08T09:30:00`, end: `${y}-${m}-08T11:00:00`, type: 'meeting', title: '事业部经营会' },
  84 + { id: 'E016', date: `${y}-${m}-09`, start: `${y}-${m}-09T13:30:00`, end: `${y}-${m}-09T15:00:00`, type: 'ops', title: '操作会 - 南湖店' },
  85 + { id: 'E017', date: `${y}-${m}-10`, start: `${y}-${m}-10T10:00:00`, end: `${y}-${m}-10T11:30:00`, type: 'training', title: '培训会 - 顾问心法' },
  86 + { id: 'E018', date: `${y}-${m}-11`, start: `${y}-${m}-11T15:00:00`, end: `${y}-${m}-11T17:00:00`, type: 'activity', title: '门店活动 - 西站店沙龙' },
  87 + { id: 'E019', date: `${y}-${m}-12`, start: `${y}-${m}-12T09:30:00`, end: `${y}-${m}-12T11:00:00`, type: 'ops', title: '操作会 - 融创店' },
  88 + { id: 'E020', date: `${y}-${m}-13`, start: `${y}-${m}-13T14:00:00`, end: `${y}-${m}-13T15:30:00`, type: 'training', title: '培训会 - 经典案例分享' },
  89 + { id: 'E021', date: `${y}-${m}-14`, start: `${y}-${m}-14T19:00:00`, end: `${y}-${m}-14T20:30:00`, type: 'activity', title: '门店活动 - 会员观影会' },
  90 + { id: 'E022', date: `${y}-${m}-15`, start: `${y}-${m}-15T10:00:00`, end: `${y}-${m}-15T12:00:00`, type: 'training', title: '产品培训 - 生命之波' },
  91 + { id: 'E023', date: `${y}-${m}-16`, start: `${y}-${m}-16T09:30:00`, end: `${y}-${m}-16T11:00:00`, type: 'ops', title: '操作会 - 科技部' },
  92 + { id: 'E024', date: `${y}-${m}-17`, start: `${y}-${m}-17T15:00:00`, end: `${y}-${m}-17T17:00:00`, type: 'activity', title: '门店活动 - 会员答谢宴' },
  93 + { id: 'E025', date: `${y}-${m}-18`, start: `${y}-${m}-18T14:00:00`, end: `${y}-${m}-18T15:30:00`, type: 'training', title: '培训会 - 绩效与辅导' },
  94 + { id: 'E026', date: `${y}-${m}-19`, start: `${y}-${m}-19T10:00:00`, end: `${y}-${m}-19T11:30:00`, type: 'ops', title: '操作会 - 门店联动' },
  95 + { id: 'E027', date: `${y}-${m}-20`, start: `${y}-${m}-20T09:30:00`, end: `${y}-${m}-20T11:30:00`, type: 'meeting', title: '季度全体大会' },
  96 + { id: 'E028', date: `${y}-${m}-21`, start: `${y}-${m}-21T19:00:00`, end: `${y}-${m}-21T20:30:00`, type: 'activity', title: '门店活动 - 新品体验会' },
  97 + { id: 'E029', date: `${y}-${m}-22`, start: `${y}-${m}-22T10:00:00`, end: `${y}-${m}-22T11:30:00`, type: 'training', title: '培训会 - 工具使用' },
  98 + { id: 'E030', date: `${y}-${m}-23`, start: `${y}-${m}-23T14:00:00`, end: `${y}-${m}-23T15:30:00`, type: 'ops', title: '操作会 - 区域复盘' },
  99 + { id: 'E031', date: `${y}-${m}-24`, start: `${y}-${m}-24T15:00:00`, end: `${y}-${m}-24T16:30:00`, type: 'activity', title: '门店活动 - 老客回访日' },
  100 + { id: 'E032', date: `${y}-${m}-25`, start: `${y}-${m}-25T09:30:00`, end: `${y}-${m}-25T11:00:00`, type: 'meeting', title: '高管经营例会' }
  101 + ],
  102 + calendarPlugins: [dayGridPlugin, timeGridPlugin, interactionPlugin],
  103 + calendarEvents: [],
  104 + calendarHeader: {
  105 + left: 'prev,next today',
  106 + center: 'title',
  107 + right: 'dayGridMonth,timeGridWeek,timeGridDay'
  108 + },
  109 + buttonText: { today: '今日', month: '月', week: '周', day: '日' },
  110 + startTime: null,
  111 + endTime: null,
  112 + calendarHeight: 720,
  113 + typeColorMap: {
  114 + ops: '#6366f1',
  115 + training: '#22c55e',
  116 + meeting: '#f97316',
  117 + activity: '#ec4899'
  118 + }
  119 + }
  120 + },
  121 + computed: {
  122 + visibleProxy: {
  123 + get() {
  124 + return this.visible
  125 + },
  126 + set(v) {
  127 + this.$emit('update:visible', v)
  128 + }
  129 + },
  130 + legendList() {
  131 + return [
  132 + { type: 'ops', label: '操作会', color: this.typeColorMap.ops },
  133 + { type: 'training', label: '培训会', color: this.typeColorMap.training },
  134 + { type: 'meeting', label: '全体大会', color: this.typeColorMap.meeting },
  135 + { type: 'activity', label: '门店活动', color: this.typeColorMap.activity }
  136 + ]
  137 + }
  138 + },
  139 + watch: {
  140 + visible(v) {
  141 + if (v) {
  142 + this.$nextTick(() => {
  143 + this.calcHeight()
  144 + this.initData()
  145 + })
  146 + }
  147 + }
  148 + },
  149 + mounted() {
  150 + window.addEventListener('resize', this.calcHeight)
  151 + },
  152 + beforeDestroy() {
  153 + window.removeEventListener('resize', this.calcHeight)
  154 + },
  155 + methods: {
  156 + calcHeight() {
  157 + this.$nextTick(() => {
  158 + const el = this.$el && this.$el.querySelector('.dialog-content')
  159 + if (el) {
  160 + // 提高最小高度,让每天能展示更多事件
  161 + this.calendarHeight = Math.max(el.clientHeight - 20, 650)
  162 + }
  163 + })
  164 + },
  165 + datesRender(info) {
  166 + const view = info.view
  167 + this.startTime = view.activeStart
  168 + this.endTime = view.activeEnd
  169 + this.initData()
  170 + },
  171 + initData() {
  172 + this.loading = true
  173 + setTimeout(() => {
  174 + let filtered = [...this.companyEvents]
  175 + if (this.startTime && this.endTime) {
  176 + filtered = filtered.filter(e => {
  177 + const t = new Date(e.start).getTime()
  178 + return t >= this.startTime.getTime() && t < this.endTime.getTime()
  179 + })
  180 + }
  181 + this.calendarEvents = filtered.map(evt => {
  182 + const color = this.typeColorMap[evt.type] || '#3b82f6'
  183 + return {
  184 + id: evt.id,
  185 + title: evt.title,
  186 + start: new Date(evt.start).toISOString(),
  187 + end: new Date(evt.end).toISOString(),
  188 + color,
  189 + editable: false,
  190 + allDay: false,
  191 + extendedProps: {
  192 + type: evt.type
  193 + }
  194 + }
  195 + })
  196 + this.loading = false
  197 + }, 200)
  198 + },
  199 + handleEventClick(info) {
  200 + const evt = info && info.event
  201 + if (!evt) return
  202 + const raw = this.companyEvents.find(e => e.id === evt.id) || {}
  203 + const start = evt.start
  204 + const end = evt.end
  205 + const fmt = d => {
  206 + if (!d) return ''
  207 + const hh = String(d.getHours()).padStart(2, '0')
  208 + const mm = String(d.getMinutes()).padStart(2, '0')
  209 + return `${hh}:${mm}`
  210 + }
  211 + const timeRange = start ? `${fmt(start)} - ${fmt(end || start)}` : ''
  212 + const type = (evt.extendedProps && evt.extendedProps.type) || ''
  213 + const typeText = this.legendList.find(x => x.type === type)?.label || '安排'
  214 + const dateStr = start ? this.formatDate(start) : ''
  215 + const subject = raw.title || evt.title || '—'
  216 + const content = raw.desc || '—'
  217 + const otherParts = []
  218 + if (raw.store) otherParts.push(`门店/地点:${raw.store}`)
  219 + otherParts.push(`类型:${typeText}`)
  220 + const otherText = otherParts.join(';')
  221 +
  222 + const html = `
  223 + <div style="line-height:1.7;font-size:13px;color:#4b5563;">
  224 + <div><strong>主题:</strong>${subject}</div>
  225 + <div><strong>内容:</strong>${content}</div>
  226 + <div><strong>时间:</strong>${dateStr} ${timeRange}</div>
  227 + <div><strong>其他:</strong>${otherText}</div>
  228 + </div>
  229 + `
  230 + this.$alert(html, '日程详情', {
  231 + confirmButtonText: '知道了',
  232 + dangerouslyUseHTMLString: true
  233 + })
  234 + }
  235 + }
  236 +}
  237 +</script>
  238 +
  239 +<style lang="scss">
  240 +@import '~@fullcalendar/core/main.css';
  241 +@import '~@fullcalendar/daygrid/main.css';
  242 +@import '~@fullcalendar/timegrid/main.css';
  243 +
  244 +.company-calendar-dialog .company-calendar {
  245 + .fc-toolbar.fc-header-toolbar {
  246 + padding: 16px 20px;
  247 + margin-bottom: 0;
  248 + border-bottom: 2px solid #f1f5f9;
  249 + background: linear-gradient(135deg, rgba(59,130,246,0.02) 0%, rgba(96,165,250,0.02) 100%);
  250 + }
  251 + .fc-toolbar-title {
  252 + font-size: 18px;
  253 + font-weight: 700;
  254 + color: #1e293b;
  255 + }
  256 + .fc-button-primary {
  257 + background-color: #3b82f6;
  258 + border-color: #3b82f6;
  259 + border-radius: 10px;
  260 + font-size: 13px;
  261 + font-weight: 500;
  262 + height: 34px;
  263 + line-height: 34px;
  264 + padding: 0 14px;
  265 + transition: all 0.2s;
  266 + &:hover {
  267 + background: #2563eb;
  268 + transform: translateY(-1px);
  269 + box-shadow: 0 4px 12px rgba(59,130,246,0.3);
  270 + }
  271 + }
  272 + .fc-button-primary:not(:disabled):active,
  273 + .fc-button-primary:not(:disabled).fc-button-active {
  274 + background-color: #2563eb;
  275 + border-color: #2563eb;
  276 + box-shadow: 0 4px 12px rgba(59,130,246,0.3);
  277 + }
  278 + .fc-day-today {
  279 + background: linear-gradient(135deg, rgba(59,130,246,0.05) 0%, rgba(96,165,250,0.05) 100%);
  280 + .fc-daygrid-day-number {
  281 + background: linear-gradient(135deg, #3b82f6, #2563eb);
  282 + color: #fff;
  283 + border-radius: 6px;
  284 + box-shadow: 0 2px 8px rgba(59,130,246,0.3);
  285 + }
  286 + }
  287 + .fc-event {
  288 + cursor: pointer;
  289 + border-radius: 6px;
  290 + padding: 3px 6px;
  291 + font-size: 12px;
  292 + font-weight: 500;
  293 + border: none;
  294 + box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  295 + transition: all 0.2s;
  296 + &:hover {
  297 + transform: translateY(-1px);
  298 + box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  299 + }
  300 + }
  301 + .fc-day-header {
  302 + font-size: 14px !important;
  303 + color: #64748b !important;
  304 + font-weight: 700 !important;
  305 + background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%) !important;
  306 + border-bottom: 2px solid #e2e8f0 !important;
  307 + padding: 12px 8px !important;
  308 + }
  309 + .fc-unthemed th,
  310 + .fc-unthemed td {
  311 + border-color: #f1f5f9;
  312 + }
  313 +}
  314 +</style>
  315 +
  316 +<style lang="scss" scoped>
  317 +::v-deep .company-calendar-dialog {
  318 + max-width: 1600px;
  319 + margin-top: 3vh !important;
  320 + border-radius: 20px;
  321 + padding: 0;
  322 + background: radial-gradient(circle at 0 0, rgba(255,255,255,0.96) 0, rgba(248,250,252,0.98) 40%, rgba(241,245,249,0.98) 100%);
  323 + box-shadow: 0 24px 48px rgba(15,23,42,0.18), 0 0 0 1px rgba(255,255,255,0.9);
  324 + backdrop-filter: blur(22px);
  325 + -webkit-backdrop-filter: blur(22px);
  326 +}
  327 +::v-deep .el-dialog__header { display: none; }
  328 +::v-deep .el-dialog__body { padding: 0; }
  329 +
  330 +.dialog-inner {
  331 + display: flex;
  332 + flex-direction: column;
  333 + max-height: 92vh;
  334 + height: 88vh;
  335 +}
  336 +
  337 +.dialog-header {
  338 + flex-shrink: 0;
  339 + display: flex;
  340 + align-items: center;
  341 + justify-content: space-between;
  342 + margin: 18px 22px 0;
  343 + padding: 10px 14px;
  344 + border-radius: 14px;
  345 + background: rgba(219,234,254,0.96);
  346 +}
  347 +
  348 +.dialog-title {
  349 + font-size: 17px;
  350 + font-weight: 600;
  351 + color: #0f172a;
  352 +}
  353 +
  354 +.dialog-legend {
  355 + display: flex;
  356 + align-items: center;
  357 + gap: 10px;
  358 + font-size: 12px;
  359 + color: #6b7280;
  360 +}
  361 +
  362 +.legend-item {
  363 + display: inline-flex;
  364 + align-items: center;
  365 + gap: 4px;
  366 +}
  367 +
  368 +.legend-dot {
  369 + width: 10px;
  370 + height: 10px;
  371 + border-radius: 999px;
  372 +}
  373 +
  374 +.dialog-close {
  375 + cursor: pointer;
  376 + width: 28px;
  377 + height: 28px;
  378 + display: flex;
  379 + align-items: center;
  380 + justify-content: center;
  381 + border-radius: 999px;
  382 + color: #64748b;
  383 + transition: all 0.15s;
  384 + &:hover {
  385 + background: rgba(0,0,0,0.06);
  386 + color: #0f172a;
  387 + }
  388 +}
  389 +
  390 +.dialog-content {
  391 + flex: 1;
  392 + min-height: 0;
  393 + overflow: hidden;
  394 + padding: 0 22px 14px;
  395 +}
  396 +</style>
  397 +
  398 +
store-pc/src/components/InviteAddDialog.vue
@@ -10,7 +10,7 @@ @@ -10,7 +10,7 @@
10 <div class="invite-dialog-inner"> 10 <div class="invite-dialog-inner">
11 <div class="invite-header"> 11 <div class="invite-header">
12 <div class="invite-title-wrap"> 12 <div class="invite-title-wrap">
13 - <div class="invite-title">新增邀约记录</div> 13 + <div class="invite-title">新增沟通记录</div>
14 </div> 14 </div>
15 </div> 15 </div>
16 16
@@ -22,44 +22,28 @@ @@ -22,44 +22,28 @@
22 size="small" 22 size="small"
23 class="invite-form" 23 class="invite-form"
24 > 24 >
25 - <el-form-item label="邀约时间" prop="yysj">  
26 - <el-date-picker  
27 - v-model="form.yysj"  
28 - type="datetime"  
29 - placeholder="请选择邀约时间"  
30 - style="width: 100%"  
31 - />  
32 - </el-form-item>  
33 -  
34 - <el-form-item label="预约客户" prop="yykh"> 25 + <el-form-item label="沟通方式" prop="gtfs">
35 <el-select 26 <el-select
36 - v-model="form.yykh"  
37 - placeholder="请选择预约客户" 27 + v-model="form.gtfs"
  28 + placeholder="请选择沟通方式"
38 filterable 29 filterable
39 clearable 30 clearable
40 > 31 >
41 - <el-option  
42 - v-for="item in customerOptions"  
43 - :key="item.value"  
44 - :label="item.label"  
45 - :value="item.value"  
46 - /> 32 + <el-option label="电话" value="电话" />
  33 + <el-option label="微信" value="微信" />
47 </el-select> 34 </el-select>
48 </el-form-item> 35 </el-form-item>
49 36
50 - <el-form-item label="联系电话">  
51 - <el-input  
52 - v-model="form.dh"  
53 - placeholder="电话号码"  
54 - disabled  
55 - />  
56 - </el-form-item>  
57 -  
58 - <el-form-item label="电话是否有效" prop="dhsfyx">  
59 - <el-radio-group v-model="form.dhsfyx">  
60 - <el-radio label="是">是</el-radio>  
61 - <el-radio label="否">否</el-radio>  
62 - </el-radio-group> 37 + <el-form-item label="是否接听" prop="sfjt">
  38 + <el-select
  39 + v-model="form.sfjt"
  40 + placeholder="请选择是否接听"
  41 + clearable
  42 + >
  43 + <el-option label="是" value="是" />
  44 + <el-option label="否" value="否" />
  45 + <el-option label="未拨通" value="未拨通" />
  46 + </el-select>
63 </el-form-item> 47 </el-form-item>
64 48
65 <el-form-item label="联系记录" prop="lxjl"> 49 <el-form-item label="联系记录" prop="lxjl">
@@ -70,6 +54,15 @@ @@ -70,6 +54,15 @@
70 placeholder="请输入联系记录" 54 placeholder="请输入联系记录"
71 /> 55 />
72 </el-form-item> 56 </el-form-item>
  57 +
  58 + <el-form-item label="下次联系时间" prop="xcLxsj">
  59 + <el-date-picker
  60 + v-model="form.xcLxsj"
  61 + type="datetime"
  62 + placeholder="可选,方便下次跟进"
  63 + style="width: 100%"
  64 + />
  65 + </el-form-item>
73 </el-form> 66 </el-form>
74 67
75 <div class="invite-footer"> 68 <div class="invite-footer">
@@ -97,11 +90,10 @@ export default { @@ -97,11 +90,10 @@ export default {
97 return { 90 return {
98 submitting: false, 91 submitting: false,
99 form: { 92 form: {
100 - yysj: '',  
101 - yykh: '',  
102 - dh: '',  
103 - dhsfyx: '',  
104 - lxjl: '' 93 + gtfs: '',
  94 + sfjt: '',
  95 + lxjl: '',
  96 + xcLxsj: ''
105 }, 97 },
106 customerOptions: [ 98 customerOptions: [
107 { label: '张三', value: 'GK2020082101170', phone: '13800000001' }, 99 { label: '张三', value: 'GK2020082101170', phone: '13800000001' },
@@ -109,9 +101,8 @@ export default { @@ -109,9 +101,8 @@ export default {
109 { label: '王五', value: 'GK2020082101172', phone: '13800000003' } 101 { label: '王五', value: 'GK2020082101172', phone: '13800000003' }
110 ], 102 ],
111 rules: { 103 rules: {
112 - yysj: [{ required: true, message: '请选择邀约时间', trigger: 'change' }],  
113 - yykh: [{ required: true, message: '请选择预约客户', trigger: 'change' }],  
114 - dhsfyx: [{ required: true, message: '请选择电话是否有效', trigger: 'change' }], 104 + gtfs: [{ required: true, message: '请选择沟通方式', trigger: 'change' }],
  105 + sfjt: [{ required: true, message: '请选择是否接听', trigger: 'change' }],
115 lxjl: [ 106 lxjl: [
116 { required: true, message: '请输入联系记录', trigger: 'blur' }, 107 { required: true, message: '请输入联系记录', trigger: 'blur' },
117 { min: 2, message: '联系记录至少 2 个字', trigger: 'blur' } 108 { min: 2, message: '联系记录至少 2 个字', trigger: 'blur' }
@@ -130,19 +121,14 @@ export default { @@ -130,19 +121,14 @@ export default {
130 } 121 }
131 }, 122 },
132 watch: { 123 watch: {
133 - 'form.yykh'(val) {  
134 - const target = this.customerOptions.find(item => item.value === val)  
135 - this.form.dh = target ? target.phone : ''  
136 - }  
137 }, 124 },
138 methods: { 125 methods: {
139 resetForm() { 126 resetForm() {
140 this.form = { 127 this.form = {
141 - yysj: '',  
142 - yykh: '',  
143 - dh: '',  
144 - dhsfyx: '',  
145 - lxjl: '' 128 + gtfs: '',
  129 + sfjt: '',
  130 + lxjl: '',
  131 + xcLxsj: ''
146 } 132 }
147 this.$refs.form && this.$refs.form.clearValidate() 133 this.$refs.form && this.$refs.form.clearValidate()
148 }, 134 },
@@ -156,7 +142,7 @@ export default { @@ -156,7 +142,7 @@ export default {
156 this.submitting = true 142 this.submitting = true
157 setTimeout(() => { 143 setTimeout(() => {
158 this.submitting = false 144 this.submitting = false
159 - this.$message.success('邀约记录已保存(示例)') 145 + this.$message.success('已添加沟通记录')
160 this.visibleProxy = false 146 this.visibleProxy = false
161 this.resetForm() 147 this.resetForm()
162 }, 800) 148 }, 800)
store-pc/src/components/MemberProfileDialog.vue
@@ -297,7 +297,7 @@ @@ -297,7 +297,7 @@
297 <!-- 底部按钮 --> 297 <!-- 底部按钮 -->
298 <div class="content-footer"> 298 <div class="content-footer">
299 <span class="records-op-btn records-op-btn--ghost records-op-btn--invite" @click="emitAction('invite')"><i 299 <span class="records-op-btn records-op-btn--ghost records-op-btn--invite" @click="emitAction('invite')"><i
300 - class="el-icon-phone-outline"></i>去邀约</span> 300 + class="el-icon-phone-outline"></i>添加沟通记录</span>
301 <span class="records-op-btn records-op-btn--ghost records-op-btn--booking" @click="emitAction('booking')"><i 301 <span class="records-op-btn records-op-btn--ghost records-op-btn--booking" @click="emitAction('booking')"><i
302 class="el-icon-date"></i>去预约</span> 302 class="el-icon-date"></i>去预约</span>
303 <span class="records-op-btn records-op-btn--ghost records-op-btn--billing" @click="emitAction('billing')"><i 303 <span class="records-op-btn records-op-btn--ghost records-op-btn--billing" @click="emitAction('billing')"><i
@@ -590,7 +590,7 @@ export default { @@ -590,7 +590,7 @@ export default {
590 recordTabs() { 590 recordTabs() {
591 return [ 591 return [
592 { key: 'rights', label: '权益明细' }, 592 { key: 'rights', label: '权益明细' },
593 - { key: 'invite', label: '邀约记录' }, 593 + { key: 'invite', label: '联系记录' },
594 { key: 'booking', label: '预约记录' }, 594 { key: 'booking', label: '预约记录' },
595 { key: 'billing', label: '开单记录' }, 595 { key: 'billing', label: '开单记录' },
596 { key: 'consume', label: '消耗记录' }, 596 { key: 'consume', label: '消耗记录' },
@@ -628,7 +628,7 @@ export default { @@ -628,7 +628,7 @@ export default {
628 }, 628 },
629 currentSearchPlaceholder() { 629 currentSearchPlaceholder() {
630 switch (this.recordsTab) { 630 switch (this.recordsTab) {
631 - case 'invite': return '按联系记录/邀约人搜索' 631 + case 'invite': return '按联系记录/跟进人搜索'
632 case 'booking': return '按体验项目/健康师搜索' 632 case 'booking': return '按体验项目/健康师搜索'
633 case 'billing': return '按品项/开单人员搜索' 633 case 'billing': return '按品项/开单人员搜索'
634 case 'consume': return '按品项/操作人员搜索' 634 case 'consume': return '按品项/操作人员搜索'
store-pc/src/views/dashboard/index.vue
@@ -5,55 +5,45 @@ @@ -5,55 +5,45 @@
5 <div class="store-info"> 5 <div class="store-info">
6 <div class="store-name">{{ currentStoreName }} · 工作台</div> 6 <div class="store-name">{{ currentStoreName }} · 工作台</div>
7 <div class="store-subtitle"> 7 <div class="store-subtitle">
8 - 今日邀约 · 预约 · 开单,一眼总览 8 + 今日预约 · 开单,一眼总览
9 <span class="schedule-link" @click.stop="$store.commit('SET_SCHEDULE_DIALOG', true)">打开排班</span> 9 <span class="schedule-link" @click.stop="$store.commit('SET_SCHEDULE_DIALOG', true)">打开排班</span>
10 </div> 10 </div>
11 </div> 11 </div>
12 <div class="top-bar-right"> 12 <div class="top-bar-right">
13 - <div class="reminder-inline">  
14 - <i class="el-icon-warning-outline reminder-icon"></i>  
15 - <span class="reminder-label">待办:</span>  
16 - <span class="reminder-item" @click="handleReminderClick('goldTriangle')">请设置下月金三角</span>  
17 - <span class="reminder-sep">、</span>  
18 - <span class="reminder-item" @click="handleReminderClick('storeTarget')">请设置门店目标</span> 13 + <div class="calendar-entry" @click="companyCalendarVisible = true">
  14 + <i class="el-icon-date calendar-entry-icon"></i>
  15 + <span class="calendar-entry-text">公司日历</span>
19 </div> 16 </div>
20 <div class="member-search"> 17 <div class="member-search">
21 - <el-autocomplete  
22 - v-model="memberKeyword"  
23 - class="member-search-input"  
24 - :fetch-suggestions="fetchMemberSuggestions"  
25 - placeholder="手机号 / 姓名 / 会员卡号"  
26 - prefix-icon="el-icon-search"  
27 - value-key="displayText"  
28 - :trigger-on-focus="true"  
29 - popper-class="member-search-dropdown"  
30 - @select="handleMemberSelect"  
31 - @keyup.enter.native="handleFirstMatchOrFocus"  
32 - >  
33 - <template slot-scope="{ item }">  
34 - <div class="member-suggestion">  
35 - <div class="suggestion-main">  
36 - <span class="suggestion-name">{{ item.khmc }}</span>  
37 - <span class="suggestion-level" :class="'level--' + (item.levelKey || 'default')">{{ item.level }}</span>  
38 - </div>  
39 - <div class="suggestion-meta">  
40 - <span><i class="el-icon-phone"></i>{{ item.sjh }}</span>  
41 - <span v-if="item.dah"><i class="el-icon-postcard"></i>{{ item.dah }}</span>  
42 - <span><i class="el-icon-office-building"></i>{{ item.gsmdName }}</span>  
43 - </div>  
44 - <div class="suggestion-extra">  
45 - <span class="suggestion-row">  
46 - <span>最后到店:{{ item.lastVisit }}</span>  
47 - <span v-if="item.sleepDays > 0" class="suggestion-sleep">沉睡 {{ item.sleepDays }} 天</span>  
48 - </span>  
49 - <span class="suggestion-row">  
50 - <span>剩余权益:<b>¥{{ item.remainingAmount }}</b></span>  
51 - <span>累计消费:¥{{ item.totalConsume }}</span>  
52 - </span> 18 + <el-autocomplete v-model="memberKeyword" class="member-search-input"
  19 + :fetch-suggestions="fetchMemberSuggestions" placeholder="手机号 / 姓名 / 会员卡号" prefix-icon="el-icon-search"
  20 + value-key="displayText" :trigger-on-focus="true" popper-class="member-search-dropdown"
  21 + @select="handleMemberSelect" @keyup.enter.native="handleFirstMatchOrFocus">
  22 + <template slot-scope="{ item }">
  23 + <div class="member-suggestion">
  24 + <div class="suggestion-main">
  25 + <span class="suggestion-name">{{ item.khmc }}</span>
  26 + <span class="suggestion-level" :class="'level--' + (item.levelKey || 'default')">{{ item.level
  27 + }}</span>
  28 + </div>
  29 + <div class="suggestion-meta">
  30 + <span><i class="el-icon-phone"></i>{{ item.sjh }}</span>
  31 + <span v-if="item.dah"><i class="el-icon-postcard"></i>{{ item.dah }}</span>
  32 + <span><i class="el-icon-office-building"></i>{{ item.gsmdName }}</span>
  33 + </div>
  34 + <div class="suggestion-extra">
  35 + <span class="suggestion-row">
  36 + <span>最后到店:{{ item.lastVisit }}</span>
  37 + <span v-if="item.sleepDays > 0" class="suggestion-sleep">沉睡 {{ item.sleepDays }} 天</span>
  38 + </span>
  39 + <span class="suggestion-row">
  40 + <span>剩余权益:<b>¥{{ item.remainingAmount }}</b></span>
  41 + <span>累计消费:¥{{ item.totalConsume }}</span>
  42 + </span>
  43 + </div>
53 </div> 44 </div>
54 - </div>  
55 - </template>  
56 - </el-autocomplete> 45 + </template>
  46 + </el-autocomplete>
57 </div> 47 </div>
58 </div> 48 </div>
59 <div class="top-meta"> 49 <div class="top-meta">
@@ -104,15 +94,29 @@ @@ -104,15 +94,29 @@
104 <div class="left-column"> 94 <div class="left-column">
105 <!-- 今日概览 --> 95 <!-- 今日概览 -->
106 <div class="overview-row"> 96 <div class="overview-row">
107 - <el-row :gutter="16">  
108 - <el-col v-for="item in overviewStats" :key="item.key" :span="8">  
109 - <el-card shadow="hover" class="overview-card">  
110 - <div class="overview-meta">  
111 - <span class="label">{{ item.label }}</span>  
112 - <span class="tag" v-if="item.tag">{{ item.tag }}</span> 97 + <el-row :gutter="16" class="overview-row-inner">
  98 + <el-col
  99 + v-for="item in overviewStats"
  100 + :key="item.key"
  101 + :span="24 / overviewStats.length"
  102 + >
  103 + <el-card
  104 + shadow="hover"
  105 + :class="['overview-card', 'overview-card--' + item.key]"
  106 + >
  107 + <div class="overview-card-inner">
  108 + <div class="overview-meta">
  109 + <span class="label">{{ item.label }}</span>
  110 + <span class="tag" v-if="item.tag">{{ item.tag }}</span>
  111 + </div>
  112 + <div class="overview-main">
  113 + <div class="overview-main-value">{{ item.today }}</div>
  114 + </div>
  115 + <div class="overview-secondary">
  116 + <span class="overview-secondary-label">本月</span>
  117 + <span class="overview-secondary-value">{{ item.month }}</span>
  118 + </div>
113 </div> 119 </div>
114 - <div class="overview-value">{{ item.value }}</div>  
115 - <div class="overview-sub">{{ item.subText }}</div>  
116 </el-card> 120 </el-card>
117 </el-col> 121 </el-col>
118 </el-row> 122 </el-row>
@@ -162,8 +166,11 @@ @@ -162,8 +166,11 @@
162 </div> 166 </div>
163 <div class="module-actions-spacer"></div> 167 <div class="module-actions-spacer"></div>
164 <div class="module-actions"> 168 <div class="module-actions">
165 - <button type="button" class="primary-action" @click.stop="handleModulePrimary(module)">{{ module.primaryAction }}</button>  
166 - <span v-if="module.secondaryText" class="secondary-text" @click.stop="handleModuleSecondary(module)">{{ module.secondaryText }}</span> 169 + <button type="button" class="primary-action" @click.stop="handleModulePrimary(module)">{{
  170 + module.primaryAction }}</button>
  171 + <span v-if="module.secondaryText" class="secondary-text"
  172 + @click.stop="handleModuleSecondary(module)">{{
  173 + module.secondaryText }}</span>
167 </div> 174 </div>
168 </el-card> 175 </el-card>
169 </el-col> 176 </el-col>
@@ -171,132 +178,182 @@ @@ -171,132 +178,182 @@
171 </div> 178 </div>
172 </div> 179 </div>
173 180
174 - <!-- 右侧:今日邀约 / 预约 -->  
175 - <div class="right-column">  
176 - <el-card shadow="hover" class="today-card" :body-style="{ padding: '16px 16px 12px' }">  
177 - <el-tabs v-model="todayTab">  
178 - <el-tab-pane label="今日邀约" name="invite">  
179 - <div v-if="todayInvite.length" class="timeline-list">  
180 - <div v-for="item in todayInvite" :key="item.id" class="timeline-item">  
181 - <div class="time">{{ item.time }}</div>  
182 - <div class="content">  
183 - <div class="line-main">  
184 - <span class="name">{{ item.name }}</span>  
185 - <span class="mobile">{{ item.mobile }}</span> 181 + <!-- 提醒中心侧栏(今日邀约 / 今日预约 / 生日提醒 / 沉睡提醒) -->
  182 + <div class="reminder-center">
  183 + <!-- 右侧窄栏入口 -->
  184 + <div class="reminder-center-toggle">
  185 + <div v-for="entry in reminderEntries" :key="entry.key"
  186 + :class="['reminder-toggle-item', entry.key === 'todo' ? 'todo-entry' : '', { 'is-active': currentReminderType === entry.key && reminderCenterOpen }]"
  187 + @click="handleReminderEntryClick(entry.key)">
  188 + <!-- 提醒中心入口样式:图标在上、文字在下(角标显示数字) -->
  189 + <el-badge
  190 + :value="entry.count"
  191 + :max="99"
  192 + :hidden="entry.count === 0"
  193 + class="reminder-badge"
  194 + >
  195 + <div :class="['entry-icon-wrap', 'entry-icon-wrap--' + entry.key]">
  196 + <i :class="['entry-icon', entry.icon]"></i>
  197 + </div>
  198 + </el-badge>
  199 + <span class="label-text">{{ entry.label }}</span>
  200 + </div>
  201 + <div v-if="reminderCenterOpen" class="reminder-toggle-close" @click.stop="toggleReminderCenter(false)">
  202 + <i class="el-icon-arrow-right"></i>
  203 + </div>
  204 + </div>
  205 +
  206 + <!-- 侧栏主体 -->
  207 + <transition name="reminder-center-slide" :css="reminderFirstRenderDone">
  208 + <div v-if="reminderCenterOpen" class="reminder-center-panel">
  209 + <el-card shadow="hover" class="today-card" :body-style="{ padding: '16px 16px 12px' }">
  210 + <el-tabs v-model="todayTab" @tab-click="handleTodayTabClick">
  211 + <el-tab-pane label="我的待办" name="todo">
  212 + <div v-if="todoList.length" class="timeline-list todo-list">
  213 + <div v-for="item in todoList" :key="item.key" class="timeline-item todo-item" @click="handleTodoClick(item)">
  214 + <div class="time">{{ item.tag }}</div>
  215 + <div class="content">
  216 + <div class="line-main">
  217 + <span class="name">{{ item.title }}</span>
  218 + </div>
  219 + <div class="line-sub">
  220 + <span class="project">{{ item.desc }}</span>
  221 + <button
  222 + type="button"
  223 + class="quick-book-btn"
  224 + >
  225 + 立即处理
  226 + </button>
  227 + </div>
  228 + </div>
186 </div> 229 </div>
187 - <div class="line-sub">  
188 - <span class="project">  
189 - 电话是否有效:{{ item.dhsfyx }} · {{ item.lxjl }}  
190 - </span>  
191 - <button type="button" class="quick-book-btn" @click="handleQuickBooking(item)">  
192 - 快速预约  
193 - </button> 230 + </div>
  231 + <div v-else class="empty-tip">暂无待办事项</div>
  232 + </el-tab-pane>
  233 + <el-tab-pane label="今日预约" name="booking">
  234 + <div v-if="todayBooking.length" class="timeline-list">
  235 + <div v-for="item in todayBooking" :key="item.id" class="timeline-item">
  236 + <div class="time">{{ item.time }}</div>
  237 + <div class="content">
  238 + <div class="line-main">
  239 + <span class="name">{{ item.name }}</span>
  240 + <span class="mobile">{{ item.mobile }}</span>
  241 + </div>
  242 + <div class="line-sub">
  243 + <span class="project">
  244 + 预约项目:{{ item.project }} · 预约时间:{{ item.date }} {{ item.timeRange }}
  245 + </span>
  246 + <div class="booking-actions">
  247 + <el-button
  248 + type="text"
  249 + size="mini"
  250 + class="booking-action-btn booking-action-btn--cancel"
  251 + @click.stop="handleCancelBooking(item)"
  252 + >
  253 + 取消预约
  254 + </el-button>
  255 + <el-button
  256 + type="text"
  257 + size="mini"
  258 + class="booking-action-btn booking-action-btn--confirm"
  259 + @click.stop="handleConfirmConsume(item)"
  260 + >
  261 + 确认消耗
  262 + </el-button>
  263 + </div>
  264 + </div>
  265 + </div>
194 </div> 266 </div>
195 </div> 267 </div>
196 - </div>  
197 - </div>  
198 - <div v-else class="empty-tip">今日暂无邀约记录</div>  
199 - </el-tab-pane>  
200 - <el-tab-pane label="今日预约" name="booking">  
201 - <div v-if="todayBooking.length" class="timeline-list">  
202 - <div v-for="item in todayBooking" :key="item.id" class="timeline-item">  
203 - <div class="time">{{ item.time }}</div>  
204 - <div class="content">  
205 - <div class="line-main">  
206 - <span class="name">{{ item.name }}</span>  
207 - <span class="mobile">{{ item.mobile }}</span> 268 + <div v-else class="empty-tip">今日暂无预约记录</div>
  269 + </el-tab-pane>
  270 + <el-tab-pane label="生日提醒" name="birthday">
  271 + <div class="birthday-calendar">
  272 + <!-- 月份切换头 -->
  273 + <div class="cal-header">
  274 + <button class="cal-nav-btn" @click="prevBirthdayMonth"><i class="el-icon-arrow-left"></i></button>
  275 + <div class="cal-month-label">{{ birthdayYear }} 年 {{ birthdayMonth }} 月</div>
  276 + <button class="cal-nav-btn" @click="nextBirthdayMonth"><i
  277 + class="el-icon-arrow-right"></i></button>
208 </div> 278 </div>
209 - <div class="line-sub">  
210 - <span class="project">  
211 - 预约项目:{{ item.project }} · 预约时间:{{ item.date }} {{ item.timeRange }}  
212 - </span>  
213 - <button type="button" class="quick-order-btn" @click="handleQuickBilling(item)">  
214 - 快速开单  
215 - </button> 279 + <!-- 星期头 -->
  280 + <div class="cal-weekdays">
  281 + <div v-for="w in ['日', '一', '二', '三', '四', '五', '六']" :key="w" class="cal-weekday">{{ w }}</div>
216 </div> 282 </div>
217 - </div>  
218 - </div>  
219 - </div>  
220 - <div v-else class="empty-tip">今日暂无预约记录</div>  
221 - </el-tab-pane>  
222 - <el-tab-pane label="生日提醒" name="birthday">  
223 - <div class="birthday-calendar">  
224 - <!-- 月份切换头 -->  
225 - <div class="cal-header">  
226 - <button class="cal-nav-btn" @click="prevBirthdayMonth"><i class="el-icon-arrow-left"></i></button>  
227 - <div class="cal-month-label">{{ birthdayYear }} 年 {{ birthdayMonth }} 月</div>  
228 - <button class="cal-nav-btn" @click="nextBirthdayMonth"><i class="el-icon-arrow-right"></i></button>  
229 - </div>  
230 - <!-- 星期头 -->  
231 - <div class="cal-weekdays">  
232 - <div v-for="w in ['日','一','二','三','四','五','六']" :key="w" class="cal-weekday">{{ w }}</div>  
233 - </div>  
234 - <!-- 日期格子 -->  
235 - <div class="cal-grid">  
236 - <div v-for="(cell, idx) in calendarDays" :key="idx"  
237 - :class="['cal-cell', { 'cal-cell--empty': !cell.day, 'cal-cell--has-birthday': cell.members.length > 0, 'cal-cell--selected': cell.day === selectedBirthdayDay }]"  
238 - @click="cell.day && selectBirthdayDay(cell.day)">  
239 - <span v-if="cell.day" class="cal-day-num">{{ cell.day }}</span>  
240 - <div v-if="cell.members.length" class="cal-dots">  
241 - <span v-for="m in cell.members.slice(0, 3)" :key="m.id"  
242 - class="cal-dot" :style="{ background: levelDotColor(m.level) }" 283 + <!-- 日期格子 -->
  284 + <div class="cal-grid">
  285 + <div v-for="(cell, idx) in calendarDays" :key="idx"
  286 + :class="['cal-cell', { 'cal-cell--empty': !cell.day, 'cal-cell--has-birthday': cell.members.length > 0, 'cal-cell--selected': cell.day === selectedBirthdayDay }]"
  287 + @click="cell.day && selectBirthdayDay(cell.day)">
  288 + <span v-if="cell.day" class="cal-day-num">{{ cell.day }}</span>
  289 + <div v-if="cell.members.length" class="cal-dots">
  290 + <span v-for="m in cell.members.slice(0, 3)" :key="m.id" class="cal-dot"
  291 + :style="{ background: levelDotColor(m.level) }"
243 :title="m.name + '(' + m.level + ')'"></span> 292 :title="m.name + '(' + m.level + ')'"></span>
  293 + </div>
  294 + </div>
244 </div> 295 </div>
245 - </div>  
246 - </div>  
247 - <!-- 生日会员列表 -->  
248 - <div class="cal-list-title" v-if="birthdayMonthData.length">  
249 - <span v-if="selectedBirthdayDay != null">{{ birthdayMonth }}月{{ selectedBirthdayDay }}日 · {{ birthdayDisplayList.length }} 位</span>  
250 - <span v-else>本月共 {{ birthdayMonthData.length }} 位会员生日</span>  
251 - <span v-if="selectedBirthdayDay != null" class="cal-list-reset" @click="selectedBirthdayDay = null">查看全部</span>  
252 - </div>  
253 - <div v-if="birthdayDisplayList.length" class="timeline-list cal-member-list">  
254 - <div v-for="item in birthdayDisplayList" :key="item.id" class="birthday-item">  
255 - <div class="birthday-day-badge">{{ item.day }}日</div>  
256 - <div class="birthday-info">  
257 - <div class="birthday-name">  
258 - {{ item.name }}  
259 - <span class="birthday-level-tag" :style="levelTagStyle(item.level)">{{ item.level }}</span> 296 + <!-- 生日会员列表 -->
  297 + <div class="cal-list-title" v-if="birthdayMonthData.length">
  298 + <span v-if="selectedBirthdayDay != null">{{ birthdayMonth }}月{{ selectedBirthdayDay }}日 · {{
  299 + birthdayDisplayList.length }} 位</span>
  300 + <span v-else>本月共 {{ birthdayMonthData.length }} 位会员生日</span>
  301 + <span v-if="selectedBirthdayDay != null" class="cal-list-reset"
  302 + @click="selectedBirthdayDay = null">查看全部</span>
  303 + </div>
  304 + <div v-if="birthdayDisplayList.length" class="timeline-list cal-member-list">
  305 + <div v-for="item in birthdayDisplayList" :key="item.id" class="birthday-item">
  306 + <div class="birthday-day-badge">{{ item.day }}日</div>
  307 + <div class="birthday-info">
  308 + <div class="birthday-name">
  309 + {{ item.name }}
  310 + <span class="birthday-level-tag" :style="levelTagStyle(item.level)">{{ item.level }}</span>
  311 + </div>
  312 + <div class="birthday-phone">{{ item.phone }}</div>
  313 + <div class="birthday-amount">累计消费 ¥{{ item.totalAmount }}</div>
  314 + </div>
  315 + <button class="quick-book-btn" @click="handleBirthdayCall(item)">发祝福</button>
260 </div> 316 </div>
261 - <div class="birthday-phone">{{ item.phone }}</div>  
262 - <div class="birthday-amount">累计消费 ¥{{ item.totalAmount }}</div>  
263 </div> 317 </div>
264 - <button class="quick-book-btn" @click="handleBirthdayCall(item)">发祝福</button> 318 + <div v-else-if="birthdayMonthData.length && selectedBirthdayDay != null" class="empty-tip">{{
  319 + birthdayMonth }}月{{ selectedBirthdayDay }}日暂无会员生日</div>
  320 + <div v-else class="empty-tip">本月暂无会员生日</div>
265 </div> 321 </div>
266 - </div>  
267 - <div v-else-if="birthdayMonthData.length && selectedBirthdayDay != null" class="empty-tip">{{ birthdayMonth }}月{{ selectedBirthdayDay }}日暂无会员生日</div>  
268 - <div v-else class="empty-tip">本月暂无会员生日</div>  
269 - </div>  
270 - </el-tab-pane>  
271 - <el-tab-pane label="沉睡提醒" name="sleeping">  
272 - <div v-if="sleepingMembers.length" class="timeline-list">  
273 - <div v-for="item in sleepingMembers" :key="item.id" class="sleeping-item">  
274 - <div class="sleeping-days">{{ item.days }}天</div>  
275 - <div class="sleeping-info">  
276 - <div class="sleeping-name">{{ item.name }}  
277 - <span class="sleeping-level">{{ item.level }}</span> 322 + </el-tab-pane>
  323 + <el-tab-pane label="沉睡提醒" name="sleeping">
  324 + <div v-if="sleepingMembers.length" class="timeline-list">
  325 + <div v-for="item in sleepingMembers" :key="item.id" class="sleeping-item">
  326 + <div class="sleeping-days">{{ item.days }}天</div>
  327 + <div class="sleeping-info">
  328 + <div class="sleeping-name">
  329 + {{ item.name }}
  330 + <span class="sleeping-level">{{ item.level }}</span>
  331 + </div>
  332 + <div class="sleeping-phone">{{ item.phone }}</div>
  333 + <div class="sleeping-last">最后到店:{{ item.lastVisit }} · 余额 ¥{{ item.balance }}</div>
  334 + </div>
  335 + <button class="quick-book-btn" @click="handleSleepingCall(item)">去邀约</button>
278 </div> 336 </div>
279 - <div class="sleeping-phone">{{ item.phone }}</div>  
280 - <div class="sleeping-last">最后到店:{{ item.lastVisit }} · 余额 ¥{{ item.balance }}</div>  
281 </div> 337 </div>
282 - <button class="quick-book-btn" @click="handleSleepingCall(item)">去邀约</button>  
283 - </div>  
284 - </div>  
285 - <div v-else class="empty-tip">暂无沉睡会员</div>  
286 - </el-tab-pane>  
287 - </el-tabs>  
288 - </el-card> 338 + <div v-else class="empty-tip">暂无沉睡会员</div>
  339 + </el-tab-pane>
  340 + </el-tabs>
  341 + </el-card>
  342 + </div>
  343 + </transition>
289 </div> 344 </div>
290 </div> 345 </div>
291 - <member-profile-dialog :visible.sync="memberDialogVisible" :member="activeMember || {}" /> 346 + <member-profile-dialog
  347 + :visible.sync="memberDialogVisible"
  348 + :member="activeMember || {}"
  349 + @action="handleMemberAction"
  350 + />
292 <tuoke-lead-dialog :visible.sync="tuokeDialogVisible" /> 351 <tuoke-lead-dialog :visible.sync="tuokeDialogVisible" />
293 - <invite-add-dialog :visible.sync="inviteDialogVisible" />  
294 <booking-dialog :visible.sync="bookingDialogVisible" :prefill="bookingPrefill" /> 352 <booking-dialog :visible.sync="bookingDialogVisible" :prefill="bookingPrefill" />
295 <billing-dialog :visible.sync="billingDialogVisible" :prefill="billingPrefill" /> 353 <billing-dialog :visible.sync="billingDialogVisible" :prefill="billingPrefill" />
296 <consume-dialog :visible.sync="consumeDialogVisible" /> 354 <consume-dialog :visible.sync="consumeDialogVisible" />
297 <refund-dialog :visible.sync="refundDialogVisible" /> 355 <refund-dialog :visible.sync="refundDialogVisible" />
298 <tuoke-list-dialog :visible.sync="tuokeListVisible" /> 356 <tuoke-list-dialog :visible.sync="tuokeListVisible" />
299 - <invite-list-dialog :visible.sync="inviteListVisible" />  
300 <booking-calendar-dialog :visible.sync="bookingCalendarVisible" /> 357 <booking-calendar-dialog :visible.sync="bookingCalendarVisible" />
301 <billing-list-dialog :visible.sync="billingListVisible" /> 358 <billing-list-dialog :visible.sync="billingListVisible" />
302 <consume-list-dialog :visible.sync="consumeListVisible" /> 359 <consume-list-dialog :visible.sync="consumeListVisible" />
@@ -312,18 +369,21 @@ @@ -312,18 +369,21 @@
312 :current-store-name="currentStoreName" 369 :current-store-name="currentStoreName"
313 :month-value="storeTargetMonthValue" 370 :month-value="storeTargetMonthValue"
314 /> 371 />
  372 + <invite-add-dialog :visible.sync="contactDialogVisible" />
  373 + <company-calendar-dialog :visible.sync="companyCalendarVisible" />
315 <!-- 切换门店弹窗 --> 374 <!-- 切换门店弹窗 -->
316 - <el-dialog :visible.sync="storeSwitchDialogVisible" width="420px" append-to-body  
317 - custom-class="store-switch-dialog" :show-close="false"> 375 + <el-dialog :visible.sync="storeSwitchDialogVisible" width="420px" append-to-body custom-class="store-switch-dialog"
  376 + :show-close="false">
318 <div class="store-switch-inner"> 377 <div class="store-switch-inner">
319 <div class="store-switch-header"> 378 <div class="store-switch-header">
320 <div class="store-switch-title">切换门店</div> 379 <div class="store-switch-title">切换门店</div>
321 - <span class="store-switch-close" @click="storeSwitchDialogVisible = false"><i class="el-icon-close"></i></span> 380 + <span class="store-switch-close" @click="storeSwitchDialogVisible = false"><i
  381 + class="el-icon-close"></i></span>
322 </div> 382 </div>
323 <div class="store-switch-content"> 383 <div class="store-switch-content">
324 <div v-for="s in storeList" :key="s.id" 384 <div v-for="s in storeList" :key="s.id"
325 - :class="['store-switch-item', { 'store-switch-item--active': s.id === currentStoreId }]"  
326 - @click="handleStoreSwitch(s)"> 385 + :class="['store-switch-item', { 'store-switch-item--active': s.id === currentStoreId }]"
  386 + @click="handleStoreSwitch(s)">
327 <div class="store-switch-icon"> 387 <div class="store-switch-icon">
328 <span>{{ (s.name || '').replace(/店$/, '')[0] || '店' }}</span> 388 <span>{{ (s.name || '').replace(/店$/, '')[0] || '店' }}</span>
329 </div> 389 </div>
@@ -348,12 +408,12 @@ @@ -348,12 +408,12 @@
348 import MemberProfileDialog from '@/components/MemberProfileDialog.vue' 408 import MemberProfileDialog from '@/components/MemberProfileDialog.vue'
349 import TuokeLeadDialog from '@/components/TuokeLeadDialog.vue' 409 import TuokeLeadDialog from '@/components/TuokeLeadDialog.vue'
350 import InviteAddDialog from '@/components/InviteAddDialog.vue' 410 import InviteAddDialog from '@/components/InviteAddDialog.vue'
  411 +import CompanyCalendarDialog from '@/components/CompanyCalendarDialog.vue'
351 import BookingDialog from '@/components/BookingDialog.vue' 412 import BookingDialog from '@/components/BookingDialog.vue'
352 import BillingDialog from '@/components/BillingDialog.vue' 413 import BillingDialog from '@/components/BillingDialog.vue'
353 import ConsumeDialog from '@/components/ConsumeDialog.vue' 414 import ConsumeDialog from '@/components/ConsumeDialog.vue'
354 import RefundDialog from '@/components/RefundDialog.vue' 415 import RefundDialog from '@/components/RefundDialog.vue'
355 import TuokeListDialog from '@/components/TuokeListDialog.vue' 416 import TuokeListDialog from '@/components/TuokeListDialog.vue'
356 -import InviteListDialog from '@/components/InviteListDialog.vue'  
357 import BookingCalendarDialog from '@/components/BookingCalendarDialog.vue' 417 import BookingCalendarDialog from '@/components/BookingCalendarDialog.vue'
358 import BillingListDialog from '@/components/BillingListDialog.vue' 418 import BillingListDialog from '@/components/BillingListDialog.vue'
359 import ConsumeListDialog from '@/components/ConsumeListDialog.vue' 419 import ConsumeListDialog from '@/components/ConsumeListDialog.vue'
@@ -367,12 +427,12 @@ export default { @@ -367,12 +427,12 @@ export default {
367 MemberProfileDialog, 427 MemberProfileDialog,
368 TuokeLeadDialog, 428 TuokeLeadDialog,
369 InviteAddDialog, 429 InviteAddDialog,
  430 + CompanyCalendarDialog,
370 BookingDialog, 431 BookingDialog,
371 BillingDialog, 432 BillingDialog,
372 ConsumeDialog, 433 ConsumeDialog,
373 RefundDialog, 434 RefundDialog,
374 TuokeListDialog, 435 TuokeListDialog,
375 - InviteListDialog,  
376 BookingCalendarDialog, 436 BookingCalendarDialog,
377 BillingListDialog, 437 BillingListDialog,
378 ConsumeListDialog, 438 ConsumeListDialog,
@@ -389,6 +449,8 @@ export default { @@ -389,6 +449,8 @@ export default {
389 memberKeyword: '', 449 memberKeyword: '',
390 memberDialogVisible: false, 450 memberDialogVisible: false,
391 activeMember: null, 451 activeMember: null,
  452 + contactDialogVisible: false,
  453 + companyCalendarVisible: false,
392 storeSwitchDialogVisible: false, 454 storeSwitchDialogVisible: false,
393 currentStoreId: 'S001', 455 currentStoreId: 'S001',
394 // 用户可访问门店:通常 1 个(归属店),借调时 2 个,最多 3 个 456 // 用户可访问门店:通常 1 个(归属店),借调时 2 个,最多 3 个
@@ -415,30 +477,57 @@ export default { @@ -415,30 +477,57 @@ export default {
415 { id: 'M016', khmc: '冯丽娟', sjh: '15500155723', dah: 'GK2023081501015', level: 'B', levelKey: 'b', gsmdName: '融创店', lastVisit: '2025-12-25', sleepDays: 0, remainingAmount: '0.73万', totalConsume: '7.3万' }, 477 { id: 'M016', khmc: '冯丽娟', sjh: '15500155723', dah: 'GK2023081501015', level: 'B', levelKey: 'b', gsmdName: '融创店', lastVisit: '2025-12-25', sleepDays: 0, remainingAmount: '0.73万', totalConsume: '7.3万' },
416 { id: 'M017', khmc: '蒋雪梅', sjh: '17700177634', dah: 'GK2023102201016', level: 'A+', levelKey: 'aplus', gsmdName: '西站店', lastVisit: '2026-02-01', sleepDays: 0, remainingAmount: '2.85万', totalConsume: '28.5万' } 478 { id: 'M017', khmc: '蒋雪梅', sjh: '17700177634', dah: 'GK2023102201016', level: 'A+', levelKey: 'aplus', gsmdName: '西站店', lastVisit: '2026-02-01', sleepDays: 0, remainingAmount: '2.85万', totalConsume: '28.5万' }
417 ], 479 ],
418 - todayTab: 'invite',  
419 - monthlyDone: 86420,  
420 - monthlyTarget: 150000, 480 + todayTab: 'todo',
421 overviewStats: [ 481 overviewStats: [
422 { 482 {
423 - key: 'invite',  
424 - label: '今日邀约',  
425 - value: 12,  
426 - subText: '待联系 7 · 已到店 5',  
427 - tag: '潜客' 483 + key: 'headCount',
  484 + label: '人头数',
  485 + today: 42,
  486 + month: 260,
  487 + tag: '客流'
428 }, 488 },
429 { 489 {
430 - key: 'booking',  
431 - label: '今日预约',  
432 - value: 8,  
433 - subText: '进行中 3 · 已完成 5',  
434 - tag: '到店' 490 + key: 'visitCount',
  491 + label: '人次数',
  492 + today: 58,
  493 + month: 380,
  494 + tag: '频次'
435 }, 495 },
436 { 496 {
437 - key: 'order',  
438 - label: '今日业绩',  
439 - value: '¥18,360',  
440 - subText: '开单 6 · 客次 11',  
441 - tag: '收银' 497 + key: 'projectCount',
  498 + label: '项目数',
  499 + today: 96,
  500 + month: 1240,
  501 + tag: '项目'
  502 + },
  503 + {
  504 + key: 'consumeAmount',
  505 + label: '消耗金额',
  506 + today: '¥63,580',
  507 + month: '¥1,236,800',
  508 + tag: '消耗'
  509 + },
  510 + {
  511 + key: 'refundAmount',
  512 + label: '退卡金额',
  513 + today: '¥12,400',
  514 + month: '¥45,300',
  515 + tag: '退卡'
  516 + }
  517 + ],
  518 + // 我的待办列表(后续可扩展更多待办)
  519 + todoList: [
  520 + {
  521 + key: 'goldTriangle',
  522 + title: '请设置下月金三角',
  523 + desc: '确保下月业绩分配和责任人明确',
  524 + tag: '门店规划'
  525 + },
  526 + {
  527 + key: 'storeTarget',
  528 + title: '请设置门店目标',
  529 + desc: '设置本月/下月门店业绩目标,便于进度跟踪',
  530 + tag: '业绩目标'
442 } 531 }
443 ], 532 ],
444 modules: [ 533 modules: [
@@ -454,17 +543,6 @@ export default { @@ -454,17 +543,6 @@ export default {
454 secondaryText: '查看拓客数据' 543 secondaryText: '查看拓客数据'
455 }, 544 },
456 { 545 {
457 - key: 'invite',  
458 - title: '邀约管理',  
459 - subtitle: '',  
460 - icon: 'el-icon-phone-outline',  
461 - iconBg: 'linear-gradient(135deg, #0ea5e9, #22c55e)',  
462 - metricValue: 0,  
463 - metricLabel: '待跟进邀约',  
464 - primaryAction: '新建邀约',  
465 - secondaryText: '查看邀约记录'  
466 - },  
467 - {  
468 key: 'booking', 546 key: 'booking',
469 title: '预约管理', 547 title: '预约管理',
470 subtitle: '', 548 subtitle: '',
@@ -540,88 +618,7 @@ export default { @@ -540,88 +618,7 @@ export default {
540 secondaryText: '' 618 secondaryText: ''
541 } 619 }
542 ], 620 ],
543 - todayInvite: [  
544 - {  
545 - id: 'TI202603020001',  
546 - time: '09:10',  
547 - name: '林小纤',  
548 - mobile: '13800138000',  
549 - dhsfyx: '是',  
550 - lxjl: '邀约周末面部深层护理体验,客户有兴趣。'  
551 - },  
552 - {  
553 - id: 'TI202603020002',  
554 - time: '09:35',  
555 - name: '王丽',  
556 - mobile: '13800138001',  
557 - dhsfyx: '是',  
558 - lxjl: '介绍肩颈调理活动,已确认可到店。'  
559 - },  
560 - {  
561 - id: 'TI202603020003',  
562 - time: '10:05',  
563 - name: '张敏',  
564 - mobile: '13800138002',  
565 - dhsfyx: '否',  
566 - lxjl: '电话无人接听,准备稍后再次联系。'  
567 - },  
568 - {  
569 - id: 'TI202603020004',  
570 - time: '10:40',  
571 - name: '赵云',  
572 - mobile: '13800138003',  
573 - dhsfyx: '是',  
574 - lxjl: '邀约身体舒缓护理体验,客户需与家人确认时间。'  
575 - },  
576 - {  
577 - id: 'TI202603020005',  
578 - time: '11:20',  
579 - name: '刘芳',  
580 - mobile: '13800138004',  
581 - dhsfyx: '是',  
582 - lxjl: '眼周护理体验回访,客户反馈效果不错。'  
583 - },  
584 - {  
585 - id: 'TI202603020006',  
586 - time: '13:15',  
587 - name: '陈洁',  
588 - mobile: '13800138005',  
589 - dhsfyx: '是',  
590 - lxjl: '面部护理加购邀约,客户考虑本周安排。'  
591 - },  
592 - {  
593 - id: 'TI202603020007',  
594 - time: '14:00',  
595 - name: '周雪',  
596 - mobile: '13800138006',  
597 - dhsfyx: '否',  
598 - lxjl: '电话为空号,准备核实客户信息。'  
599 - },  
600 - {  
601 - id: 'TI202603020008',  
602 - time: '15:10',  
603 - name: '李楠',  
604 - mobile: '13800138007',  
605 - dhsfyx: '是',  
606 - lxjl: '身体护理活动邀约,客户希望下班后再联系。'  
607 - },  
608 - {  
609 - id: 'TI202603020009',  
610 - time: '16:25',  
611 - name: '何倩',  
612 - mobile: '13800138008',  
613 - dhsfyx: '是',  
614 - lxjl: '生日关怀邀约,初步定周末到店。'  
615 - },  
616 - {  
617 - id: 'TI202603020010',  
618 - time: '17:40',  
619 - name: '孙悦',  
620 - mobile: '13800138009',  
621 - dhsfyx: '是',  
622 - lxjl: '眼周护理复查邀约,客户暂不方便到店。'  
623 - }  
624 - ], 621 + todayInvite: [],
625 todayBooking: [ 622 todayBooking: [
626 { 623 {
627 id: 'TB202603020001', 624 id: 'TB202603020001',
@@ -770,7 +767,6 @@ export default { @@ -770,7 +767,6 @@ export default {
770 { id: 'S004', name: '赵海燕', days: 60, phone: '186****0023', level: 'B', lastVisit: '2025-01-06', balance: '1,600' } 767 { id: 'S004', name: '赵海燕', days: 60, phone: '186****0023', level: 'B', lastVisit: '2025-01-06', balance: '1,600' }
771 ], 768 ],
772 tuokeDialogVisible: false, 769 tuokeDialogVisible: false,
773 - inviteDialogVisible: false,  
774 bookingDialogVisible: false, 770 bookingDialogVisible: false,
775 bookingPrefill: { 771 bookingPrefill: {
776 name: '', 772 name: '',
@@ -781,12 +777,18 @@ export default { @@ -781,12 +777,18 @@ export default {
781 consumeDialogVisible: false, 777 consumeDialogVisible: false,
782 refundDialogVisible: false, 778 refundDialogVisible: false,
783 tuokeListVisible: false, 779 tuokeListVisible: false,
784 - inviteListVisible: false,  
785 bookingCalendarVisible: false, 780 bookingCalendarVisible: false,
786 billingListVisible: false, 781 billingListVisible: false,
787 consumeListVisible: false, 782 consumeListVisible: false,
788 refundListVisible: false, 783 refundListVisible: false,
789 - goldTriangleDialogVisible: false 784 + goldTriangleDialogVisible: false,
  785 + // 提醒中心侧栏状态(今日预约 / 生日提醒 / 沉睡提醒)
  786 + // reminderDefaultOpen 可作为默认展开/收起开关
  787 + reminderDefaultOpen: false,
  788 + reminderCenterOpen: false,
  789 + // 提醒中心侧栏:首屏渲染完成后再启用滑动动画
  790 + reminderFirstRenderDone: false,
  791 + currentReminderType: 'booking'
790 } 792 }
791 }, 793 },
792 computed: { 794 computed: {
@@ -849,6 +851,39 @@ export default { @@ -849,6 +851,39 @@ export default {
849 const now = new Date() 851 const now = new Date()
850 const next = new Date(now.getFullYear(), now.getMonth() + 1, 1) 852 const next = new Date(now.getFullYear(), now.getMonth() + 1, 1)
851 return `${next.getFullYear()}${String(next.getMonth() + 1).padStart(2, '0')}` 853 return `${next.getFullYear()}${String(next.getMonth() + 1).padStart(2, '0')}`
  854 + },
  855 + // 提醒中心入口数据(用于窄栏徽标统计)
  856 + reminderEntries() {
  857 + const today = new Date()
  858 + const month = today.getMonth() + 1
  859 + const day = today.getDate()
  860 + const birthdayTodayCount = this.birthdayAllData.filter(m => m.month === month && m.day === day).length
  861 + return [
  862 + {
  863 + key: 'todo',
  864 + label: '我的待办',
  865 + count: this.todoList.length,
  866 + icon: 'el-icon-warning-outline'
  867 + },
  868 + {
  869 + key: 'booking',
  870 + label: '今日预约',
  871 + count: this.todayBooking.length,
  872 + icon: 'el-icon-date'
  873 + },
  874 + {
  875 + key: 'birthday',
  876 + label: '生日提醒',
  877 + count: birthdayTodayCount,
  878 + icon: 'el-icon-gift'
  879 + },
  880 + {
  881 + key: 'sleeping',
  882 + label: '沉睡提醒',
  883 + count: this.sleepingMembers.length,
  884 + icon: 'el-icon-moon'
  885 + }
  886 + ]
852 } 887 }
853 }, 888 },
854 mounted() { 889 mounted() {
@@ -866,6 +901,23 @@ export default { @@ -866,6 +901,23 @@ export default {
866 } 901 }
867 fmt() 902 fmt()
868 this.clockTimer = setInterval(fmt, 1000) 903 this.clockTimer = setInterval(fmt, 1000)
  904 +
  905 + // 提醒中心默认展开/收起与上次状态(可配置)
  906 + const savedOpen = localStorage.getItem('store_reminder_open')
  907 + const savedType = localStorage.getItem('store_reminder_type')
  908 + const validTypes = ['todo', 'booking', 'birthday', 'sleeping']
  909 + if (savedType && validTypes.includes(savedType)) {
  910 + this.currentReminderType = savedType
  911 + this.todayTab = savedType
  912 + }
  913 + if (savedOpen !== null) {
  914 + this.reminderCenterOpen = savedOpen === '1'
  915 + } else {
  916 + this.reminderCenterOpen = this.reminderDefaultOpen
  917 + }
  918 + this.$nextTick(() => {
  919 + this.reminderFirstRenderDone = true
  920 + })
869 }, 921 },
870 beforeDestroy() { 922 beforeDestroy() {
871 document.removeEventListener('fullscreenchange', this.onFullscreenChange) 923 document.removeEventListener('fullscreenchange', this.onFullscreenChange)
@@ -874,9 +926,9 @@ export default { @@ -874,9 +926,9 @@ export default {
874 methods: { 926 methods: {
875 toggleFullscreen() { 927 toggleFullscreen() {
876 if (!document.fullscreenElement) { 928 if (!document.fullscreenElement) {
877 - document.documentElement.requestFullscreen().catch(() => {}) 929 + document.documentElement.requestFullscreen().catch(() => { })
878 } else { 930 } else {
879 - document.exitFullscreen().catch(() => {}) 931 + document.exitFullscreen().catch(() => { })
880 } 932 }
881 }, 933 },
882 onFullscreenChange() { 934 onFullscreenChange() {
@@ -893,7 +945,7 @@ export default { @@ -893,7 +945,7 @@ export default {
893 }).then(() => { 945 }).then(() => {
894 localStorage.removeItem('store_token') 946 localStorage.removeItem('store_token')
895 this.$router.push('/login') 947 this.$router.push('/login')
896 - }).catch(() => {}) 948 + }).catch(() => { })
897 } else if (cmd === 'profile') { 949 } else if (cmd === 'profile') {
898 this.$message.info('个人信息功能开发中') 950 this.$message.info('个人信息功能开发中')
899 } else if (cmd === 'password') { 951 } else if (cmd === 'password') {
@@ -905,10 +957,6 @@ export default { @@ -905,10 +957,6 @@ export default {
905 this.tuokeDialogVisible = true 957 this.tuokeDialogVisible = true
906 return 958 return
907 } 959 }
908 - if (module.key === 'invite') {  
909 - this.inviteDialogVisible = true  
910 - return  
911 - }  
912 if (module.key === 'booking') { 960 if (module.key === 'booking') {
913 this.bookingPrefill = { name: '', phone: '' } 961 this.bookingPrefill = { name: '', phone: '' }
914 this.bookingDialogVisible = true 962 this.bookingDialogVisible = true
@@ -1121,6 +1169,15 @@ export default { @@ -1121,6 +1169,15 @@ export default {
1121 } 1169 }
1122 this.memberDialogVisible = true 1170 this.memberDialogVisible = true
1123 }, 1171 },
  1172 + handleMemberAction(payload) {
  1173 + if (!payload || !payload.type) return
  1174 + const { type } = payload
  1175 + if (type === 'invite') {
  1176 + // 添加沟通记录:仅打开沟通记录表单
  1177 + this.contactDialogVisible = true
  1178 + }
  1179 + // 其它类型(booking / billing / consume / refund)暂时保持原有入口按钮逻辑
  1180 + },
1124 handleQuickBooking(item) { 1181 handleQuickBooking(item) {
1125 this.bookingPrefill = { 1182 this.bookingPrefill = {
1126 name: item.name, 1183 name: item.name,
@@ -1157,6 +1214,34 @@ export default { @@ -1157,6 +1214,34 @@ export default {
1157 handleSleepingCall(item) { 1214 handleSleepingCall(item) {
1158 this.$message.info(`正在为 ${item.name} 创建邀约...`) 1215 this.$message.info(`正在为 ${item.name} 创建邀约...`)
1159 }, 1216 },
  1217 + handleCancelBooking(item) {
  1218 + this.$confirm(`确定要取消「${item.name}」在 ${item.date} ${item.timeRange} 的预约吗?`, '取消预约确认', {
  1219 + confirmButtonText: '确定',
  1220 + cancelButtonText: '再想想',
  1221 + type: 'warning'
  1222 + }).then(() => {
  1223 + this.$message.success('预约已标记为取消(示例)')
  1224 + }).catch(() => {})
  1225 + },
  1226 + handleConfirmConsume(item) {
  1227 + this.$confirm(`确认「${item.name}」本次预约已完成并产生消耗吗?`, '确认消耗', {
  1228 + confirmButtonText: '确定',
  1229 + cancelButtonText: '再想想',
  1230 + type: 'info'
  1231 + }).then(() => {
  1232 + this.$message.success('已标记为已消耗(示例)')
  1233 + }).catch(() => {})
  1234 + },
  1235 + handleTodoClick(item) {
  1236 + if (!item || !item.key) return
  1237 + if (item.key === 'goldTriangle') {
  1238 + this.goldTriangleDialogVisible = true
  1239 + return
  1240 + }
  1241 + if (item.key === 'storeTarget') {
  1242 + this.storeTargetDialogVisible = true
  1243 + }
  1244 + },
1160 selectBirthdayDay(day) { 1245 selectBirthdayDay(day) {
1161 this.selectedBirthdayDay = this.selectedBirthdayDay === day ? null : day 1246 this.selectedBirthdayDay = this.selectedBirthdayDay === day ? null : day
1162 }, 1247 },
@@ -1212,7 +1297,6 @@ export default { @@ -1212,7 +1297,6 @@ export default {
1212 } 1297 }
1213 const map = { 1298 const map = {
1214 tuoke: 'tuokeListVisible', 1299 tuoke: 'tuokeListVisible',
1215 - invite: 'inviteListVisible',  
1216 booking: 'bookingCalendarVisible', 1300 booking: 'bookingCalendarVisible',
1217 order: 'billingListVisible', 1301 order: 'billingListVisible',
1218 consume: 'consumeListVisible', 1302 consume: 'consumeListVisible',
@@ -1220,6 +1304,29 @@ export default { @@ -1220,6 +1304,29 @@ export default {
1220 } 1304 }
1221 const key = map[module.key] 1305 const key = map[module.key]
1222 if (key) this[key] = true 1306 if (key) this[key] = true
  1307 + },
  1308 + // 提醒中心侧栏:入口点击
  1309 + handleReminderEntryClick(type) {
  1310 + if (this.reminderCenterOpen && this.currentReminderType === type) {
  1311 + this.reminderCenterOpen = false
  1312 + localStorage.setItem('store_reminder_open', '0')
  1313 + return
  1314 + }
  1315 + this.currentReminderType = type
  1316 + this.todayTab = type
  1317 + this.reminderCenterOpen = true
  1318 + localStorage.setItem('store_reminder_open', '1')
  1319 + localStorage.setItem('store_reminder_type', this.currentReminderType)
  1320 + },
  1321 + // 提醒中心侧栏:面板显隐
  1322 + toggleReminderCenter(open) {
  1323 + this.reminderCenterOpen = open
  1324 + localStorage.setItem('store_reminder_open', open ? '1' : '0')
  1325 + },
  1326 + // 提醒中心侧栏:Tab 切换时同步当前类型
  1327 + handleTodayTabClick(tab) {
  1328 + this.currentReminderType = tab.name
  1329 + localStorage.setItem('store_reminder_type', this.currentReminderType)
1223 } 1330 }
1224 } 1331 }
1225 } 1332 }
@@ -1228,12 +1335,13 @@ export default { @@ -1228,12 +1335,13 @@ export default {
1228 <style lang="scss" scoped> 1335 <style lang="scss" scoped>
1229 .dashboard-page { 1336 .dashboard-page {
1230 height: 100%; 1337 height: 100%;
1231 - padding: 24px 28px; 1338 + padding: 24px 20px 24px 28px;
1232 background: radial-gradient(circle at 10% 20%, #eef2ff 0, #eef2ff 26%, #fdf2ff 60%, #fff7ed 100%); 1339 background: radial-gradient(circle at 10% 20%, #eef2ff 0, #eef2ff 26%, #fdf2ff 60%, #fff7ed 100%);
1233 box-sizing: border-box; 1340 box-sizing: border-box;
1234 display: flex; 1341 display: flex;
1235 flex-direction: column; 1342 flex-direction: column;
1236 overflow: hidden; 1343 overflow: hidden;
  1344 + position: relative;
1237 } 1345 }
1238 1346
1239 .top-bar-right { 1347 .top-bar-right {
@@ -1242,38 +1350,8 @@ export default { @@ -1242,38 +1350,8 @@ export default {
1242 gap: 12px; 1350 gap: 12px;
1243 min-width: 0; 1351 min-width: 0;
1244 } 1352 }
1245 -.reminder-inline {  
1246 - display: flex;  
1247 - align-items: center;  
1248 - flex-shrink: 0;  
1249 - padding: 6px 12px;  
1250 - border-radius: 8px;  
1251 - background: linear-gradient(135deg, #fffbeb, #fef3c7);  
1252 - border: 1px solid #fde68a;  
1253 - font-size: 13px;  
1254 - color: #92400e;  
1255 -}  
1256 -.reminder-inline .reminder-icon {  
1257 - font-size: 14px;  
1258 - margin-right: 4px;  
1259 - color: #f59e0b;  
1260 -}  
1261 -.reminder-inline .reminder-label {  
1262 - font-weight: 600;  
1263 - margin-right: 2px;  
1264 -}  
1265 -.reminder-inline .reminder-item {  
1266 - cursor: pointer;  
1267 - color: #2563eb;  
1268 - font-weight: 500;  
1269 - &:hover {  
1270 - text-decoration: underline;  
1271 - }  
1272 -}  
1273 -.reminder-inline .reminder-sep {  
1274 - color: #92400e;  
1275 - margin: 0 1px;  
1276 -} 1353 +
  1354 +/* 顶部我的待办样式已移动到右侧工具栏 */
1277 1355
1278 .top-bar { 1356 .top-bar {
1279 display: grid; 1357 display: grid;
@@ -1289,6 +1367,36 @@ export default { @@ -1289,6 +1367,36 @@ export default {
1289 max-width: 420px; 1367 max-width: 420px;
1290 } 1368 }
1291 1369
  1370 +.calendar-entry {
  1371 + flex-shrink: 0;
  1372 + display: inline-flex;
  1373 + align-items: center;
  1374 + gap: 6px;
  1375 + padding: 8px 12px;
  1376 + border-radius: 999px;
  1377 + background: linear-gradient(135deg, #e0f2fe, #ddd6fe);
  1378 + border: 1px solid rgba(129, 140, 248, 0.55);
  1379 + color: #1d4ed8;
  1380 + font-size: 13px;
  1381 + font-weight: 500;
  1382 + cursor: pointer;
  1383 + box-shadow: 0 4px 12px rgba(79, 70, 229, 0.28);
  1384 + transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
  1385 +}
  1386 +
  1387 +.calendar-entry-icon {
  1388 + font-size: 16px;
  1389 +}
  1390 +
  1391 +.calendar-entry-text {
  1392 + white-space: nowrap;
  1393 +}
  1394 +
  1395 +.calendar-entry:hover {
  1396 + transform: translateY(-1px);
  1397 + box-shadow: 0 8px 20px rgba(79, 70, 229, 0.35);
  1398 +}
  1399 +
1292 @media (max-width: 1024px) { 1400 @media (max-width: 1024px) {
1293 .top-bar-right { 1401 .top-bar-right {
1294 flex-wrap: wrap; 1402 flex-wrap: wrap;
@@ -1307,12 +1415,16 @@ export default { @@ -1307,12 +1415,16 @@ export default {
1307 margin-top: 4px; 1415 margin-top: 4px;
1308 font-size: 14px; 1416 font-size: 14px;
1309 color: #5b6b7a; 1417 color: #5b6b7a;
  1418 +
1310 .schedule-link { 1419 .schedule-link {
1311 margin-left: 12px; 1420 margin-left: 12px;
1312 color: #6366f1; 1421 color: #6366f1;
1313 cursor: pointer; 1422 cursor: pointer;
1314 font-weight: 500; 1423 font-weight: 500;
1315 - &:hover { text-decoration: underline; } 1424 +
  1425 + &:hover {
  1426 + text-decoration: underline;
  1427 + }
1316 } 1428 }
1317 } 1429 }
1318 } 1430 }
@@ -1342,13 +1454,44 @@ export default { @@ -1342,13 +1454,44 @@ export default {
1342 font-weight: 600; 1454 font-weight: 600;
1343 flex-shrink: 0; 1455 flex-shrink: 0;
1344 } 1456 }
1345 -.suggestion-level.level--aplusplus { background: linear-gradient(135deg, #ef4444, #dc2626); color: #fff; }  
1346 -.suggestion-level.level--aplus { background: linear-gradient(135deg, #f59e0b, #d97706); color: #fff; }  
1347 -.suggestion-level.level--a { background: rgba(245,158,11,0.12); color: #d97706; border: 1px solid rgba(245,158,11,0.25); }  
1348 -.suggestion-level.level--b { background: rgba(34,197,94,0.12); color: #16a34a; border: 1px solid rgba(34,197,94,0.25); }  
1349 -.suggestion-level.level--c { background: rgba(59,130,246,0.12); color: #2563eb; border: 1px solid rgba(59,130,246,0.25); }  
1350 -.suggestion-level.level--d { background: #e5e7eb; color: #6b7280; }  
1351 -.suggestion-level.level--default { background: #e5e7eb; color: #6b7280; } 1457 +
  1458 +.suggestion-level.level--aplusplus {
  1459 + background: linear-gradient(135deg, #ef4444, #dc2626);
  1460 + color: #fff;
  1461 +}
  1462 +
  1463 +.suggestion-level.level--aplus {
  1464 + background: linear-gradient(135deg, #f59e0b, #d97706);
  1465 + color: #fff;
  1466 +}
  1467 +
  1468 +.suggestion-level.level--a {
  1469 + background: rgba(245, 158, 11, 0.12);
  1470 + color: #d97706;
  1471 + border: 1px solid rgba(245, 158, 11, 0.25);
  1472 +}
  1473 +
  1474 +.suggestion-level.level--b {
  1475 + background: rgba(34, 197, 94, 0.12);
  1476 + color: #16a34a;
  1477 + border: 1px solid rgba(34, 197, 94, 0.25);
  1478 +}
  1479 +
  1480 +.suggestion-level.level--c {
  1481 + background: rgba(59, 130, 246, 0.12);
  1482 + color: #2563eb;
  1483 + border: 1px solid rgba(59, 130, 246, 0.25);
  1484 +}
  1485 +
  1486 +.suggestion-level.level--d {
  1487 + background: #e5e7eb;
  1488 + color: #6b7280;
  1489 +}
  1490 +
  1491 +.suggestion-level.level--default {
  1492 + background: #e5e7eb;
  1493 + color: #6b7280;
  1494 +}
1352 1495
1353 .suggestion-meta { 1496 .suggestion-meta {
1354 display: flex; 1497 display: flex;
@@ -1357,11 +1500,13 @@ export default { @@ -1357,11 +1500,13 @@ export default {
1357 font-size: 12px; 1500 font-size: 12px;
1358 color: #64748b; 1501 color: #64748b;
1359 } 1502 }
  1503 +
1360 .suggestion-meta span { 1504 .suggestion-meta span {
1361 display: flex; 1505 display: flex;
1362 align-items: center; 1506 align-items: center;
1363 gap: 4px; 1507 gap: 4px;
1364 } 1508 }
  1509 +
1365 .suggestion-meta i { 1510 .suggestion-meta i {
1366 font-size: 12px; 1511 font-size: 12px;
1367 color: #94a3b8; 1512 color: #94a3b8;
@@ -1394,6 +1539,7 @@ export default { @@ -1394,6 +1539,7 @@ export default {
1394 1539
1395 .member-search-input { 1540 .member-search-input {
1396 width: 100%; 1541 width: 100%;
  1542 +
1397 ::v-deep .el-input__inner { 1543 ::v-deep .el-input__inner {
1398 border-radius: 999px; 1544 border-radius: 999px;
1399 padding-left: 40px; 1545 padding-left: 40px;
@@ -1449,6 +1595,7 @@ export default { @@ -1449,6 +1595,7 @@ export default {
1449 color: #64748b; 1595 color: #64748b;
1450 font-size: 16px; 1596 font-size: 16px;
1451 transition: all 0.2s ease; 1597 transition: all 0.2s ease;
  1598 +
1452 &:hover { 1599 &:hover {
1453 background: rgba(99, 102, 241, 0.08); 1600 background: rgba(99, 102, 241, 0.08);
1454 border-color: rgba(99, 102, 241, 0.25); 1601 border-color: rgba(99, 102, 241, 0.25);
@@ -1469,6 +1616,7 @@ export default { @@ -1469,6 +1616,7 @@ export default {
1469 border: 1px solid rgba(226, 232, 240, 0.8); 1616 border: 1px solid rgba(226, 232, 240, 0.8);
1470 background: rgba(241, 245, 249, 0.9); 1617 background: rgba(241, 245, 249, 0.9);
1471 transition: all 0.2s ease; 1618 transition: all 0.2s ease;
  1619 +
1472 &:hover { 1620 &:hover {
1473 background: rgba(99, 102, 241, 0.06); 1621 background: rgba(99, 102, 241, 0.06);
1474 border-color: rgba(99, 102, 241, 0.2); 1622 border-color: rgba(99, 102, 241, 0.2);
@@ -1520,15 +1668,20 @@ export default { @@ -1520,15 +1668,20 @@ export default {
1520 align-items: center; 1668 align-items: center;
1521 gap: 8px; 1669 gap: 8px;
1522 line-height: 1.4; 1670 line-height: 1.4;
  1671 +
1523 &:hover { 1672 &:hover {
1524 background: rgba(99, 102, 241, 0.06); 1673 background: rgba(99, 102, 241, 0.06);
1525 color: #4f46e5; 1674 color: #4f46e5;
1526 } 1675 }
1527 - i { font-size: 15px; } 1676 +
  1677 + i {
  1678 + font-size: 15px;
  1679 + }
1528 } 1680 }
1529 1681
1530 ::v-deep .user-dropdown-menu .dropdown-item--danger { 1682 ::v-deep .user-dropdown-menu .dropdown-item--danger {
1531 color: #ef4444 !important; 1683 color: #ef4444 !important;
  1684 +
1532 &:hover { 1685 &:hover {
1533 background: rgba(239, 68, 68, 0.06) !important; 1686 background: rgba(239, 68, 68, 0.06) !important;
1534 color: #dc2626 !important; 1687 color: #dc2626 !important;
@@ -1578,7 +1731,7 @@ export default { @@ -1578,7 +1731,7 @@ export default {
1578 1731
1579 .layout-grid { 1732 .layout-grid {
1580 display: grid; 1733 display: grid;
1581 - grid-template-columns: minmax(0, 2.2fr) minmax(0, 1.1fr); 1734 + grid-template-columns: minmax(0, 1fr) auto;
1582 gap: 20px; 1735 gap: 20px;
1583 flex: 1; 1736 flex: 1;
1584 min-height: 0; 1737 min-height: 0;
@@ -1593,8 +1746,15 @@ export default { @@ -1593,8 +1746,15 @@ export default {
1593 overflow-y: auto; 1746 overflow-y: auto;
1594 scrollbar-width: thin; 1747 scrollbar-width: thin;
1595 scrollbar-color: rgba(148, 163, 184, 0.3) transparent; 1748 scrollbar-color: rgba(148, 163, 184, 0.3) transparent;
1596 - &::-webkit-scrollbar { width: 4px; }  
1597 - &::-webkit-scrollbar-thumb { background: rgba(148, 163, 184, 0.3); border-radius: 2px; } 1749 +
  1750 + &::-webkit-scrollbar {
  1751 + width: 4px;
  1752 + }
  1753 +
  1754 + &::-webkit-scrollbar-thumb {
  1755 + background: rgba(148, 163, 184, 0.3);
  1756 + border-radius: 2px;
  1757 + }
1598 } 1758 }
1599 1759
1600 .modules-grid { 1760 .modules-grid {
@@ -1609,15 +1769,25 @@ export default { @@ -1609,15 +1769,25 @@ export default {
1609 .overview-card { 1769 .overview-card {
1610 border-radius: 16px; 1770 border-radius: 16px;
1611 border: none; 1771 border: none;
  1772 + background: #ffffff;
1612 box-shadow: 0 8px 20px rgba(15, 23, 42, 0.06); 1773 box-shadow: 0 8px 20px rgba(15, 23, 42, 0.06);
1613 } 1774 }
1614 } 1775 }
1615 1776
  1777 +.overview-row-inner {
  1778 + display: flex;
  1779 +}
  1780 +
  1781 +.overview-row-inner > .el-col {
  1782 + flex: 1 1 0;
  1783 + max-width: none;
  1784 +}
  1785 +
1616 .overview-meta { 1786 .overview-meta {
1617 display: flex; 1787 display: flex;
1618 align-items: center; 1788 align-items: center;
1619 justify-content: space-between; 1789 justify-content: space-between;
1620 - margin-bottom: 4px; 1790 + margin-bottom: 6px;
1621 1791
1622 .label { 1792 .label {
1623 font-size: 14px; 1793 font-size: 14px;
@@ -1634,15 +1804,91 @@ export default { @@ -1634,15 +1804,91 @@ export default {
1634 } 1804 }
1635 1805
1636 .overview-value { 1806 .overview-value {
1637 - font-size: 26px; 1807 + font-size: 18px;
1638 font-weight: 600; 1808 font-weight: 600;
1639 color: #111827; 1809 color: #111827;
  1810 + display: flex;
  1811 + align-items: baseline;
  1812 + gap: 4px;
1640 } 1813 }
1641 1814
1642 .overview-sub { 1815 .overview-sub {
1643 margin-top: 4px; 1816 margin-top: 4px;
1644 font-size: 13px; 1817 font-size: 13px;
1645 color: #9ca3af; 1818 color: #9ca3af;
  1819 + display: flex;
  1820 + align-items: baseline;
  1821 + gap: 4px;
  1822 +}
  1823 +
  1824 +.overview-card-inner {
  1825 + display: flex;
  1826 + flex-direction: column;
  1827 + row-gap: 6px;
  1828 +}
  1829 +
  1830 +.overview-main {
  1831 + display: flex;
  1832 + align-items: baseline;
  1833 + gap: 8px;
  1834 +}
  1835 +
  1836 +.overview-main-value {
  1837 + font-size: 26px;
  1838 + font-weight: 700;
  1839 + color: #0f172a;
  1840 +}
  1841 +
  1842 +.overview-secondary {
  1843 + display: flex;
  1844 + align-items: baseline;
  1845 + gap: 6px;
  1846 + font-size: 13px;
  1847 + color: #6b7280;
  1848 +}
  1849 +
  1850 +.overview-secondary-label {
  1851 + padding: 0 6px;
  1852 + border-radius: 999px;
  1853 + background: #f3f4f6;
  1854 + font-size: 11px;
  1855 +}
  1856 +
  1857 +.overview-secondary-value {
  1858 + font-size: 14px;
  1859 + font-weight: 500;
  1860 + color: #374151;
  1861 +}
  1862 +
  1863 +/* 顶部统计:不同指标的内部色彩区分(保持白底统一) */
  1864 +.overview-card--headCount {
  1865 + .overview-main-value {
  1866 + color: #1d4ed8;
  1867 + }
  1868 +}
  1869 +
  1870 +.overview-card--visitCount {
  1871 + .overview-main-value {
  1872 + color: #4f46e5;
  1873 + }
  1874 +}
  1875 +
  1876 +.overview-card--projectCount {
  1877 + .overview-main-value {
  1878 + color: #be185d;
  1879 + }
  1880 +}
  1881 +
  1882 +.overview-card--consumeAmount {
  1883 + .overview-main-value {
  1884 + color: #047857;
  1885 + }
  1886 +}
  1887 +
  1888 +.overview-card--refundAmount {
  1889 + .overview-main-value {
  1890 + color: #c2410c;
  1891 + }
1646 } 1892 }
1647 1893
1648 .modules-grid .module-card { 1894 .modules-grid .module-card {
@@ -1751,6 +1997,7 @@ export default { @@ -1751,6 +1997,7 @@ export default {
1751 box-shadow: 0 4px 10px rgba(37, 99, 235, 0.35); 1997 box-shadow: 0 4px 10px rgba(37, 99, 235, 0.35);
1752 cursor: pointer; 1998 cursor: pointer;
1753 transition: background 0.15s, box-shadow 0.15s; 1999 transition: background 0.15s, box-shadow 0.15s;
  2000 +
1754 &:hover { 2001 &:hover {
1755 background: #1d4ed8; 2002 background: #1d4ed8;
1756 box-shadow: 0 4px 12px rgba(37, 99, 235, 0.45); 2003 box-shadow: 0 4px 12px rgba(37, 99, 235, 0.45);
@@ -1763,6 +2010,7 @@ export default { @@ -1763,6 +2010,7 @@ export default {
1763 cursor: pointer; 2010 cursor: pointer;
1764 font-weight: 500; 2011 font-weight: 500;
1765 transition: color 0.15s; 2012 transition: color 0.15s;
  2013 +
1766 &:hover { 2014 &:hover {
1767 color: #1d4ed8; 2015 color: #1d4ed8;
1768 text-decoration: underline; 2016 text-decoration: underline;
@@ -1782,24 +2030,182 @@ export default { @@ -1782,24 +2030,182 @@ export default {
1782 } 2030 }
1783 } 2031 }
1784 2032
1785 -.right-column { 2033 +/* 提醒中心侧栏 - 容器 */
  2034 +.reminder-center {
  2035 + display: flex;
  2036 + flex-direction: row-reverse;
  2037 + align-items: stretch;
  2038 +}
  2039 +
  2040 +/* 提醒中心侧栏 - 窄栏入口 */
  2041 +.reminder-center-toggle {
  2042 + width: 88px;
  2043 + background: #ffffff;
  2044 + border-radius: 18px;
  2045 + box-shadow: 0 10px 28px rgba(15, 23, 42, 0.18);
1786 display: flex; 2046 display: flex;
1787 flex-direction: column; 2047 flex-direction: column;
1788 - min-height: 0; 2048 + align-items: center;
  2049 + justify-content: flex-start;
  2050 + padding: 10px 4px;
  2051 + gap: 6px;
  2052 + pointer-events: auto;
  2053 +}
  2054 +
  2055 +/* 我的待办入口样式复用提醒入口,无需额外样式 */
  2056 +
  2057 +.reminder-toggle-item {
  2058 + width: 100%;
  2059 + border-radius: 12px;
  2060 + padding: 8px 6px;
  2061 + display: flex;
  2062 + flex-direction: column;
  2063 + align-items: center;
  2064 + justify-content: flex-start;
  2065 + gap: 6px;
  2066 + cursor: pointer;
  2067 + transition: background 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
  2068 +}
  2069 +
  2070 +.reminder-toggle-item .label-text {
  2071 + font-size: 12px;
  2072 + color: #4b5563;
  2073 + letter-spacing: 0.5px;
  2074 + white-space: nowrap;
  2075 + overflow: hidden;
  2076 + text-overflow: ellipsis;
  2077 + text-align: center;
  2078 + width: 100%;
  2079 +}
  2080 +
  2081 +.entry-icon-wrap {
  2082 + width: 40px;
  2083 + height: 40px;
  2084 + border-radius: 12px;
  2085 + display: flex;
  2086 + align-items: center;
  2087 + justify-content: center;
  2088 + background: linear-gradient(135deg, #e5edff, #e0f2fe);
  2089 + box-shadow: 0 6px 14px rgba(15, 23, 42, 0.2);
  2090 + transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
  2091 +}
  2092 +
  2093 +.entry-icon-wrap .entry-icon {
  2094 + font-size: 18px;
  2095 + color: #ffffff;
  2096 +}
  2097 +
  2098 +/* 提醒中心入口样式:不同类型入口渐变色 */
  2099 +.entry-icon-wrap--invite {
  2100 + background: linear-gradient(135deg, #0ea5e9, #22c55e);
  2101 +}
  2102 +
  2103 +.entry-icon-wrap--booking {
  2104 + background: linear-gradient(135deg, #f97316, #facc15);
  2105 +}
  2106 +
  2107 +.entry-icon-wrap--birthday {
  2108 + background: linear-gradient(135deg, #ec4899, #8b5cf6);
  2109 +}
  2110 +
  2111 +.entry-icon-wrap--sleeping {
  2112 + background: linear-gradient(135deg, #6366f1, #0ea5e9);
1789 } 2113 }
1790 2114
1791 -.right-column .today-card { 2115 +/* 我的待办图标底色:橙粉渐变,区分其他入口 */
  2116 +.entry-icon-wrap--todo {
  2117 + background: linear-gradient(135deg, #f97316, #ec4899);
  2118 +}
  2119 +
  2120 +.reminder-badge {
  2121 + display: inline-block;
  2122 +}
  2123 +
  2124 +.reminder-toggle-item .reminder-badge {
  2125 + /* 提醒中心入口样式:数字角标位置微调贴近图标右上角 */
  2126 + ::v-deep .el-badge__content {
  2127 + background-color: #ef4444;
  2128 + border: none;
  2129 + box-shadow: 0 0 0 1px #fee2e2;
  2130 + right: 2px;
  2131 + top: 2px;
  2132 + padding: 0 4px;
  2133 + height: 16px;
  2134 + line-height: 16px;
  2135 + font-size: 11px;
  2136 + }
  2137 +}
  2138 +
  2139 +.entry-icon {
  2140 + flex-shrink: 0;
  2141 +}
  2142 +
  2143 +.reminder-toggle-item.is-active {
  2144 + background: linear-gradient(180deg, rgba(59, 130, 246, 0.06), rgba(59, 130, 246, 0.10));
  2145 +}
  2146 +
  2147 +.reminder-toggle-item.is-active .label-text {
  2148 + color: #1d4ed8;
  2149 + font-weight: 600;
  2150 +}
  2151 +
  2152 +.reminder-toggle-item.is-active .entry-icon-wrap {
  2153 + /* 点击后仅轻微高亮,不再增加额外阴影 */
  2154 + box-shadow: 0 6px 14px rgba(37, 99, 235, 0.35);
  2155 +}
  2156 +
  2157 +.reminder-toggle-item.todo-entry {
  2158 + /* 未选中时完全使用通用样式,不额外高亮 */
  2159 +}
  2160 +
  2161 +.reminder-toggle-item.todo-entry.is-active {
  2162 + background: linear-gradient(180deg, rgba(253, 230, 138, 1), rgba(252, 165, 165, 1));
  2163 +}
  2164 +
  2165 +.reminder-toggle-item.todo-entry .label-text {
  2166 + color: #92400e;
  2167 + font-weight: 600;
  2168 +}
  2169 +
  2170 +.reminder-toggle-close {
  2171 + margin-top: 4px;
  2172 + width: 26px;
  2173 + height: 26px;
  2174 + border-radius: 999px;
  2175 + background: rgba(15, 23, 42, 0.04);
  2176 + display: flex;
  2177 + align-items: center;
  2178 + justify-content: center;
  2179 + cursor: pointer;
  2180 + color: #6b7280;
  2181 + transition: background 0.15s ease, color 0.15s ease;
  2182 +}
  2183 +
  2184 +.reminder-toggle-close:hover {
  2185 + background: rgba(59, 130, 246, 0.16);
  2186 + color: #1d4ed8;
  2187 +}
  2188 +
  2189 +/* 提醒中心侧栏 - 主体卡片(右侧有间距滑出) */
  2190 +.reminder-center-panel {
  2191 + width: 400px;
  2192 + margin-right: 20px;
  2193 + pointer-events: auto;
  2194 + display: flex;
  2195 +}
  2196 +
  2197 +/* 提醒面板布局:独立四角圆角卡片 */
  2198 +.reminder-center-panel .today-card {
1792 border-radius: 18px; 2199 border-radius: 18px;
1793 border: none; 2200 border: none;
1794 - box-shadow: 0 10px 28px rgba(15, 23, 42, 0.08); 2201 + box-shadow: 0 10px 28px rgba(15, 23, 42, 0.18);
1795 flex: 1; 2202 flex: 1;
1796 display: flex; 2203 display: flex;
1797 flex-direction: column; 2204 flex-direction: column;
1798 min-height: 0; 2205 min-height: 0;
1799 } 2206 }
1800 2207
1801 -/* el-card 内部撑满 */  
1802 -.right-column .today-card ::v-deep .el-card__body { 2208 +.reminder-center-panel .today-card ::v-deep .el-card__body {
1803 flex: 1; 2209 flex: 1;
1804 min-height: 0; 2210 min-height: 0;
1805 display: flex; 2211 display: flex;
@@ -1807,34 +2213,56 @@ export default { @@ -1807,34 +2213,56 @@ export default {
1807 overflow: hidden; 2213 overflow: hidden;
1808 } 2214 }
1809 2215
1810 -/* el-tabs 撑满 */  
1811 -.right-column .today-card ::v-deep .el-tabs { 2216 +.reminder-center-panel .today-card ::v-deep .el-tabs {
1812 flex: 1; 2217 flex: 1;
1813 min-height: 0; 2218 min-height: 0;
1814 display: flex; 2219 display: flex;
1815 flex-direction: column; 2220 flex-direction: column;
1816 } 2221 }
1817 2222
1818 -.right-column .today-card ::v-deep .el-tabs__content { 2223 +.reminder-center-panel .today-card ::v-deep .el-tabs__content {
1819 flex: 1; 2224 flex: 1;
1820 min-height: 0; 2225 min-height: 0;
1821 overflow: hidden; 2226 overflow: hidden;
1822 } 2227 }
1823 2228
1824 -.right-column .today-card ::v-deep .el-tab-pane { 2229 +.reminder-center-panel .today-card ::v-deep .el-tab-pane {
1825 height: 100%; 2230 height: 100%;
1826 display: flex; 2231 display: flex;
1827 flex-direction: column; 2232 flex-direction: column;
1828 } 2233 }
1829 2234
  2235 +/* 提醒中心侧栏 - 进出场动画 */
  2236 +.reminder-center-slide-enter-active,
  2237 +.reminder-center-slide-leave-active {
  2238 + transition: transform 0.25s ease;
  2239 +}
  2240 +
  2241 +.reminder-center-slide-enter,
  2242 +.reminder-center-slide-leave-to {
  2243 + transform: translateX(100%);
  2244 +}
  2245 +
  2246 +.reminder-center-slide-enter-to,
  2247 +.reminder-center-slide-leave {
  2248 + transform: translateX(0);
  2249 +}
  2250 +
1830 .timeline-list { 2251 .timeline-list {
1831 flex: 1; 2252 flex: 1;
1832 min-height: 0; 2253 min-height: 0;
1833 overflow-y: auto; 2254 overflow-y: auto;
1834 scrollbar-width: thin; 2255 scrollbar-width: thin;
1835 scrollbar-color: rgba(148, 163, 184, 0.3) transparent; 2256 scrollbar-color: rgba(148, 163, 184, 0.3) transparent;
1836 - &::-webkit-scrollbar { width: 4px; }  
1837 - &::-webkit-scrollbar-thumb { background: rgba(148, 163, 184, 0.3); border-radius: 2px; } 2257 +
  2258 + &::-webkit-scrollbar {
  2259 + width: 4px;
  2260 + }
  2261 +
  2262 + &::-webkit-scrollbar-thumb {
  2263 + background: rgba(148, 163, 184, 0.3);
  2264 + border-radius: 2px;
  2265 + }
1838 } 2266 }
1839 2267
1840 .timeline-item { 2268 .timeline-item {
@@ -1859,14 +2287,18 @@ export default { @@ -1859,14 +2287,18 @@ export default {
1859 display: flex; 2287 display: flex;
1860 align-items: center; 2288 align-items: center;
1861 gap: 6px; 2289 gap: 6px;
  2290 + white-space: nowrap;
1862 2291
1863 .name { 2292 .name {
1864 color: #111827; 2293 color: #111827;
1865 font-weight: 600; 2294 font-weight: 600;
  2295 + overflow: hidden;
  2296 + text-overflow: ellipsis;
1866 } 2297 }
1867 2298
1868 .mobile { 2299 .mobile {
1869 color: #6b7280; 2300 color: #6b7280;
  2301 + flex-shrink: 0;
1870 } 2302 }
1871 } 2303 }
1872 2304
@@ -1880,10 +2312,33 @@ export default { @@ -1880,10 +2312,33 @@ export default {
1880 .project { 2312 .project {
1881 color: #6b7280; 2313 color: #6b7280;
1882 flex: 1; 2314 flex: 1;
  2315 + overflow: hidden;
  2316 + text-overflow: ellipsis;
  2317 + white-space: nowrap;
1883 } 2318 }
1884 } 2319 }
1885 } 2320 }
1886 2321
  2322 +.booking-actions {
  2323 + display: flex;
  2324 + align-items: center;
  2325 + gap: 4px;
  2326 + flex-shrink: 0;
  2327 +}
  2328 +
  2329 +.booking-action-btn {
  2330 + padding: 0 4px;
  2331 + font-size: 12px;
  2332 +}
  2333 +
  2334 +.booking-action-btn--cancel {
  2335 + color: #f97316;
  2336 +}
  2337 +
  2338 +.booking-action-btn--confirm {
  2339 + color: #16a34a;
  2340 +}
  2341 +
1887 .quick-book-btn { 2342 .quick-book-btn {
1888 flex-shrink: 0; 2343 flex-shrink: 0;
1889 border: none; 2344 border: none;
@@ -1945,7 +2400,7 @@ export default { @@ -1945,7 +2400,7 @@ export default {
1945 flex: 1; 2400 flex: 1;
1946 display: flex; 2401 display: flex;
1947 align-items: center; 2402 align-items: center;
1948 - justify-content: center; 2403 + justify-content: flex-start;
1949 font-size: 13px; 2404 font-size: 13px;
1950 color: #9ca3af; 2405 color: #9ca3af;
1951 } 2406 }
@@ -2070,7 +2525,11 @@ export default { @@ -2070,7 +2525,11 @@ export default {
2070 color: #64748b; 2525 color: #64748b;
2071 font-size: 13px; 2526 font-size: 13px;
2072 transition: all 0.2s; 2527 transition: all 0.2s;
2073 - &:hover { background: rgba(99, 102, 241, 0.08); color: #4f46e5; } 2528 +
  2529 + &:hover {
  2530 + background: rgba(99, 102, 241, 0.08);
  2531 + color: #4f46e5;
  2532 + }
2074 } 2533 }
2075 2534
2076 .cal-weekdays { 2535 .cal-weekdays {
@@ -2103,9 +2562,19 @@ export default { @@ -2103,9 +2562,19 @@ export default {
2103 align-items: center; 2562 align-items: center;
2104 padding: 3px 2px; 2563 padding: 3px 2px;
2105 transition: background 0.15s; 2564 transition: background 0.15s;
2106 - &--empty { background: transparent; }  
2107 - &:not(.cal-cell--empty) { cursor: pointer; }  
2108 - &--has-birthday { background: rgba(99, 102, 241, 0.04); } 2565 +
  2566 + &--empty {
  2567 + background: transparent;
  2568 + }
  2569 +
  2570 + &:not(.cal-cell--empty) {
  2571 + cursor: pointer;
  2572 + }
  2573 +
  2574 + &--has-birthday {
  2575 + background: rgba(99, 102, 241, 0.04);
  2576 + }
  2577 +
2109 &--selected { 2578 &--selected {
2110 background: rgba(99, 102, 241, 0.15); 2579 background: rgba(99, 102, 241, 0.15);
2111 box-shadow: 0 0 0 1px rgba(99, 102, 241, 0.3); 2580 box-shadow: 0 0 0 1px rgba(99, 102, 241, 0.3);
@@ -2147,7 +2616,10 @@ export default { @@ -2147,7 +2616,10 @@ export default {
2147 color: #6366f1; 2616 color: #6366f1;
2148 cursor: pointer; 2617 cursor: pointer;
2149 font-weight: 500; 2618 font-weight: 500;
2150 - &:hover { text-decoration: underline; } 2619 +
  2620 + &:hover {
  2621 + text-decoration: underline;
  2622 + }
2151 } 2623 }
2152 2624
2153 .cal-member-list { 2625 .cal-member-list {
@@ -2161,7 +2633,10 @@ export default { @@ -2161,7 +2633,10 @@ export default {
2161 gap: 10px; 2633 gap: 10px;
2162 padding: 8px 0; 2634 padding: 8px 0;
2163 border-bottom: 1px solid #f1f5f9; 2635 border-bottom: 1px solid #f1f5f9;
2164 - &:last-child { border-bottom: none; } 2636 +
  2637 + &:last-child {
  2638 + border-bottom: none;
  2639 + }
2165 } 2640 }
2166 2641
2167 .sleeping-item { 2642 .sleeping-item {
@@ -2170,7 +2645,10 @@ export default { @@ -2170,7 +2645,10 @@ export default {
2170 gap: 10px; 2645 gap: 10px;
2171 padding: 10px 0; 2646 padding: 10px 0;
2172 border-bottom: 1px solid #f1f5f9; 2647 border-bottom: 1px solid #f1f5f9;
2173 - &:last-child { border-bottom: none; } 2648 +
  2649 + &:last-child {
  2650 + border-bottom: none;
  2651 + }
2174 } 2652 }
2175 2653
2176 .birthday-day-badge { 2654 .birthday-day-badge {
@@ -2202,6 +2680,9 @@ export default { @@ -2202,6 +2680,9 @@ export default {
2202 display: flex; 2680 display: flex;
2203 align-items: center; 2681 align-items: center;
2204 gap: 6px; 2682 gap: 6px;
  2683 + overflow: hidden;
  2684 + text-overflow: ellipsis;
  2685 + white-space: nowrap;
2205 } 2686 }
2206 2687
2207 .birthday-level-tag { 2688 .birthday-level-tag {
@@ -2217,6 +2698,9 @@ export default { @@ -2217,6 +2698,9 @@ export default {
2217 font-size: 12px; 2698 font-size: 12px;
2218 color: #94a3b8; 2699 color: #94a3b8;
2219 margin-top: 2px; 2700 margin-top: 2px;
  2701 + overflow: hidden;
  2702 + text-overflow: ellipsis;
  2703 + white-space: nowrap;
2220 } 2704 }
2221 2705
2222 .birthday-amount, 2706 .birthday-amount,
@@ -2224,6 +2708,9 @@ export default { @@ -2224,6 +2708,9 @@ export default {
2224 font-size: 11px; 2708 font-size: 11px;
2225 color: #64748b; 2709 color: #64748b;
2226 margin-top: 1px; 2710 margin-top: 1px;
  2711 + overflow: hidden;
  2712 + text-overflow: ellipsis;
  2713 + white-space: nowrap;
2227 } 2714 }
2228 2715
2229 .sleeping-days { 2716 .sleeping-days {
@@ -2254,7 +2741,7 @@ export default { @@ -2254,7 +2741,7 @@ export default {
2254 2741
2255 @media (max-width: 1024px) { 2742 @media (max-width: 1024px) {
2256 .layout-grid { 2743 .layout-grid {
2257 - grid-template-columns: minmax(0, 1.7fr) minmax(0, 1.1fr); 2744 + grid-template-columns: minmax(0, 1fr);
2258 } 2745 }
2259 } 2746 }
2260 2747
@@ -2279,6 +2766,7 @@ export default { @@ -2279,6 +2766,7 @@ export default {
2279 .member-search-dropdown { 2766 .member-search-dropdown {
2280 min-width: 420px !important; 2767 min-width: 420px !important;
2281 max-height: 360px; 2768 max-height: 360px;
  2769 +
2282 .el-autocomplete-suggestion__list li { 2770 .el-autocomplete-suggestion__list li {
2283 padding: 10px 14px; 2771 padding: 10px 14px;
2284 line-height: 1.4; 2772 line-height: 1.4;
@@ -2289,8 +2777,8 @@ export default { @@ -2289,8 +2777,8 @@ export default {
2289 .store-switch-dialog { 2777 .store-switch-dialog {
2290 border-radius: 20px !important; 2778 border-radius: 20px !important;
2291 padding: 0 !important; 2779 padding: 0 !important;
2292 - background: radial-gradient(circle at 0 0, rgba(255,255,255,0.96) 0, rgba(248,250,252,0.98) 40%, rgba(241,245,249,0.98) 100%) !important;  
2293 - box-shadow: 0 24px 48px rgba(15,23,42,0.18), 0 0 0 1px rgba(255,255,255,0.9) !important; 2780 + background: radial-gradient(circle at 0 0, rgba(255, 255, 255, 0.96) 0, rgba(248, 250, 252, 0.98) 40%, rgba(241, 245, 249, 0.98) 100%) !important;
  2781 + box-shadow: 0 24px 48px rgba(15, 23, 42, 0.18), 0 0 0 1px rgba(255, 255, 255, 0.9) !important;
2294 backdrop-filter: blur(22px); 2782 backdrop-filter: blur(22px);
2295 -webkit-backdrop-filter: blur(22px); 2783 -webkit-backdrop-filter: blur(22px);
2296 } 2784 }
@@ -2317,7 +2805,7 @@ export default { @@ -2317,7 +2805,7 @@ export default {
2317 margin: 18px 22px 0; 2805 margin: 18px 22px 0;
2318 padding: 10px 14px; 2806 padding: 10px 14px;
2319 border-radius: 14px; 2807 border-radius: 14px;
2320 - background: rgba(219,234,254,0.96); 2808 + background: rgba(219, 234, 254, 0.96);
2321 } 2809 }
2322 2810
2323 .store-switch-title { 2811 .store-switch-title {
@@ -2336,8 +2824,9 @@ export default { @@ -2336,8 +2824,9 @@ export default {
2336 border-radius: 999px; 2824 border-radius: 999px;
2337 color: #64748b; 2825 color: #64748b;
2338 transition: all 0.15s; 2826 transition: all 0.15s;
  2827 +
2339 &:hover { 2828 &:hover {
2340 - background: rgba(0,0,0,0.06); 2829 + background: rgba(0, 0, 0, 0.06);
2341 color: #0f172a; 2830 color: #0f172a;
2342 } 2831 }
2343 } 2832 }
@@ -2359,11 +2848,16 @@ export default { @@ -2359,11 +2848,16 @@ export default {
2359 transition: all 0.2s; 2848 transition: all 0.2s;
2360 border: 1px solid transparent; 2849 border: 1px solid transparent;
2361 margin-bottom: 8px; 2850 margin-bottom: 8px;
2362 - &:last-child { margin-bottom: 0; } 2851 +
  2852 + &:last-child {
  2853 + margin-bottom: 0;
  2854 + }
  2855 +
2363 &:hover { 2856 &:hover {
2364 background: rgba(99, 102, 241, 0.06); 2857 background: rgba(99, 102, 241, 0.06);
2365 border-color: rgba(99, 102, 241, 0.2); 2858 border-color: rgba(99, 102, 241, 0.2);
2366 } 2859 }
  2860 +
2367 &--active { 2861 &--active {
2368 background: rgba(219, 234, 254, 0.5); 2862 background: rgba(219, 234, 254, 0.5);
2369 border-color: rgba(59, 130, 246, 0.3); 2863 border-color: rgba(59, 130, 246, 0.3);
@@ -2421,6 +2915,7 @@ export default { @@ -2421,6 +2915,7 @@ export default {
2421 align-items: flex-start; 2915 align-items: flex-start;
2422 gap: 4px; 2916 gap: 4px;
2423 line-height: 1.5; 2917 line-height: 1.5;
  2918 +
2424 i { 2919 i {
2425 font-size: 12px; 2920 font-size: 12px;
2426 color: #94a3b8; 2921 color: #94a3b8;