Commit 19415acca8eeec4163b219f2813a5103537ac188
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.
Showing
10 changed files
with
1533 additions
and
421 deletions
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 ? '®ion=' + 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; |