Commit a8965d32df47cee9376900620d2cb46f77a96a5b

Authored by “wangming”
1 parent e5f8ae55

```

feat: 添加系统更新日志功能和沉睡会员提醒功能

- 在导航栏右侧添加更新日志图标,点击可查看系统更新日志
- 实现更新日志弹窗组件,支持分页加载和分类展示
- 新增沉睡会员提醒配置项,可在系统配置中设置沉睡天数
- 实现门店PC端沉睡会员列表查询功能,按最后消费时间排序
- 优化上传记录页面,添加来源模块标签和时间格式化显示
- 修复农历生日计算中的边界问题和异常处理
- 调整文件上传大小限制从10MB提升至20MB
- 添加mysql2依赖包以支持数据库相关功能
```
Showing 32 changed files with 4522 additions and 173 deletions
antis-ncc-admin/src/api/extend/changelog.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +
  3 +export function getChangelogList(params) {
  4 + return request({
  5 + url: '/api/Extend/Changelog/GetList',
  6 + method: 'get',
  7 + params
  8 + })
  9 +}
  10 +
  11 +export function getPublishedList(params) {
  12 + return request({
  13 + url: '/api/Extend/Changelog/GetPublishedList',
  14 + method: 'get',
  15 + params
  16 + })
  17 +}
  18 +
  19 +export function getLatestChangelog() {
  20 + return request({
  21 + url: '/api/Extend/Changelog/GetLatest',
  22 + method: 'get'
  23 + })
  24 +}
  25 +
  26 +export function getChangelogDetail(id) {
  27 + return request({
  28 + url: `/api/Extend/Changelog/GetDetail/${id}`,
  29 + method: 'get'
  30 + })
  31 +}
  32 +
  33 +export function addChangelog(data) {
  34 + return request({
  35 + url: '/api/Extend/Changelog/Add',
  36 + method: 'post',
  37 + data
  38 + })
  39 +}
  40 +
  41 +export function updateChangelog(data) {
  42 + return request({
  43 + url: '/api/Extend/Changelog/Update',
  44 + method: 'post',
  45 + data
  46 + })
  47 +}
  48 +
  49 +export function deleteChangelog(id) {
  50 + return request({
  51 + url: `/api/Extend/Changelog/Delete/${id}`,
  52 + method: 'post'
  53 + })
  54 +}
  55 +
  56 +export function togglePublish(id) {
  57 + return request({
  58 + url: `/api/Extend/Changelog/TogglePublish/${id}`,
  59 + method: 'post'
  60 + })
  61 +}
... ...
antis-ncc-admin/src/api/extend/lqCompanyCalendar.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +
  3 +/**
  4 + * 获取公司日历事件列表(分页)
  5 + */
  6 +export function getCalendarEvents(data) {
  7 + return request({
  8 + url: '/api/Extend/LqCompanyCalendar/GetCalendarEvents',
  9 + method: 'post',
  10 + data
  11 + })
  12 +}
  13 +
  14 +/**
  15 + * 获取公司日历事件列表(不分页,用于日历展示)
  16 + */
  17 +export function getCalendarEventList(data) {
  18 + return request({
  19 + url: '/api/Extend/LqCompanyCalendar/GetCalendarEventList',
  20 + method: 'post',
  21 + data
  22 + })
  23 +}
  24 +
  25 +/**
  26 + * 创建公司日历事件
  27 + */
  28 +export function createCalendarEvent(data) {
  29 + return request({
  30 + url: '/api/Extend/LqCompanyCalendar/CreateCalendarEvent',
  31 + method: 'post',
  32 + data
  33 + })
  34 +}
  35 +
  36 +/**
  37 + * 更新公司日历事件
  38 + */
  39 +export function updateCalendarEvent(data) {
  40 + return request({
  41 + url: '/api/Extend/LqCompanyCalendar/UpdateCalendarEvent',
  42 + method: 'post',
  43 + data
  44 + })
  45 +}
  46 +
  47 +/**
  48 + * 删除公司日历事件
  49 + */
  50 +export function deleteCalendarEvent(id) {
  51 + return request({
  52 + url: `/api/Extend/LqCompanyCalendar/DeleteCalendarEvent/${id}`,
  53 + method: 'post'
  54 + })
  55 +}
  56 +
  57 +/**
  58 + * 获取事件类型列表
  59 + */
  60 +export function getEventTypeList(data) {
  61 + return request({
  62 + url: '/api/Extend/LqCompanyCalendar/GetEventTypeList',
  63 + method: 'post',
  64 + data
  65 + })
  66 +}
  67 +
  68 +/**
  69 + * 创建事件类型
  70 + */
  71 +export function createEventType(data) {
  72 + return request({
  73 + url: '/api/Extend/LqCompanyCalendar/CreateEventType',
  74 + method: 'post',
  75 + data
  76 + })
  77 +}
  78 +
  79 +/**
  80 + * 更新事件类型
  81 + */
  82 +export function updateEventType(data) {
  83 + return request({
  84 + url: '/api/Extend/LqCompanyCalendar/UpdateEventType',
  85 + method: 'post',
  86 + data
  87 + })
  88 +}
  89 +
  90 +/**
  91 + * 删除事件类型
  92 + */
  93 +export function deleteEventType(id) {
  94 + return request({
  95 + url: `/api/Extend/LqCompanyCalendar/DeleteEventType/${id}`,
  96 + method: 'post'
  97 + })
  98 +}
... ...
antis-ncc-admin/src/layout/components/NavbarRight.vue
... ... @@ -50,6 +50,11 @@
50 50 </el-dropdown-item>
51 51 </el-dropdown-menu>
52 52 </el-dropdown>
  53 + <el-tooltip content="更新日志" placement="bottom">
  54 + <div class="right-menu-item hover-effect" @click="openChangelog">
  55 + <i class="el-icon-notebook-2"></i>
  56 + </div>
  57 + </el-tooltip>
53 58 <el-tooltip :content="$t('navbar.setting')" placement="bottom">
54 59 <div class="right-menu-item hover-effect" @click="$refs.Settings.init()" v-if="showSettings">
55 60 <i class="icon-ym icon-ym-header-pannel"></i>
... ... @@ -73,6 +78,32 @@
73 78 <MessageList ref="MessageList" @read='read' />
74 79 <Settings ref="Settings" />
75 80 <UserList ref="UserList" @changeTwinkle='changeTwinkle' />
  81 +
  82 + <!-- 更新日志弹窗 -->
  83 + <el-dialog
  84 + title="系统更新日志"
  85 + :visible.sync="changelogVisible"
  86 + width="600px"
  87 + class="changelog-dialog"
  88 + >
  89 + <div v-loading="changelogLoading" class="changelog-list">
  90 + <div v-if="changelogList.length === 0" class="empty-tip">暂无更新日志</div>
  91 + <div v-for="item in changelogList" :key="item.id" class="changelog-item">
  92 + <div class="changelog-item-header">
  93 + <el-tag size="small" effect="plain">v{{ item.version }}</el-tag>
  94 + <el-tag :type="getChangelogTypeTag(item.type)" size="small">
  95 + {{ getChangelogTypeText(item.type) }}
  96 + </el-tag>
  97 + <span class="changelog-time">{{ formatDate(item.publishedTime) }}</span>
  98 + </div>
  99 + <h4 class="changelog-title">{{ item.title }}</h4>
  100 + <div class="changelog-content">{{ item.content }}</div>
  101 + </div>
  102 + </div>
  103 + <div v-if="changelogTotal > changelogList.length" class="changelog-more">
  104 + <el-button type="text" @click="loadMoreChangelog">加载更多</el-button>
  105 + </div>
  106 + </el-dialog>
76 107 </div>
77 108 </template>
78 109  
... ... @@ -87,6 +118,7 @@ import Settings from &#39;./settings&#39;
87 118 import UserList from './userList/UserList'
88 119 import dragDialog from "@/directive/el-drag-dialog";
89 120 import ReconnectingWebSocket from 'reconnecting-websocket'
  121 +import { getPublishedList } from '@/api/extend/changelog'
90 122  
91 123 export default {
92 124 directives: { dragDialog },
... ... @@ -112,7 +144,12 @@ export default {
112 144 visible: false,
113 145 isTwinkle: false,
114 146 messageCount: 0,
115   - userList: []
  147 + userList: [],
  148 + changelogVisible: false,
  149 + changelogLoading: false,
  150 + changelogList: [],
  151 + changelogTotal: 0,
  152 + changelogPage: 1
116 153 }
117 154 },
118 155 created() {
... ... @@ -255,6 +292,45 @@ export default {
255 292 },
256 293 changeTwinkle(boo) {
257 294 this.isTwinkle = boo
  295 + },
  296 + openChangelog() {
  297 + this.changelogVisible = true
  298 + this.changelogPage = 1
  299 + this.changelogList = []
  300 + this.loadChangelog()
  301 + },
  302 + loadChangelog() {
  303 + this.changelogLoading = true
  304 + getPublishedList({ currentPage: this.changelogPage, pageSize: 10 }).then(res => {
  305 + if (res.data && res.data.list) {
  306 + this.changelogList = [...this.changelogList, ...res.data.list]
  307 + this.changelogTotal = res.data.pagination.total
  308 + }
  309 + this.changelogLoading = false
  310 + }).catch(() => {
  311 + this.changelogLoading = false
  312 + })
  313 + },
  314 + loadMoreChangelog() {
  315 + this.changelogPage++
  316 + this.loadChangelog()
  317 + },
  318 + getChangelogTypeText(type) {
  319 + const map = { 1: '功能更新', 2: 'Bug修复', 3: '性能优化', 4: '安全更新' }
  320 + return map[type] || '其他'
  321 + },
  322 + getChangelogTypeTag(type) {
  323 + const map = { 1: 'primary', 2: 'danger', 3: 'success', 4: 'warning' }
  324 + return map[type] || 'info'
  325 + },
  326 + formatDate(dateStr) {
  327 + if (!dateStr) return ''
  328 + const date = new Date(dateStr)
  329 + if (isNaN(date.getTime())) return dateStr
  330 + const y = date.getFullYear()
  331 + const m = String(date.getMonth() + 1).padStart(2, '0')
  332 + const d = String(date.getDate()).padStart(2, '0')
  333 + return `${y}-${m}-${d}`
258 334 }
259 335 }
260 336 }
... ... @@ -377,4 +453,60 @@ export default {
377 453 .twinkle {
378 454 animation: blink 0.5s linear infinite;
379 455 }
  456 +
  457 +.changelog-dialog {
  458 + .changelog-list {
  459 + max-height: 500px;
  460 + overflow-y: auto;
  461 + }
  462 +
  463 + .empty-tip {
  464 + text-align: center;
  465 + color: #909399;
  466 + padding: 40px 0;
  467 + }
  468 +
  469 + .changelog-item {
  470 + padding: 16px 0;
  471 + border-bottom: 1px solid #f0f0f0;
  472 +
  473 + &:last-child {
  474 + border-bottom: none;
  475 + }
  476 +
  477 + .changelog-item-header {
  478 + display: flex;
  479 + align-items: center;
  480 + gap: 8px;
  481 + margin-bottom: 8px;
  482 +
  483 + .changelog-time {
  484 + margin-left: auto;
  485 + color: #909399;
  486 + font-size: 12px;
  487 + }
  488 + }
  489 +
  490 + .changelog-title {
  491 + margin: 0 0 8px 0;
  492 + font-size: 15px;
  493 + color: #303133;
  494 + }
  495 +
  496 + .changelog-content {
  497 + color: #606266;
  498 + font-size: 13px;
  499 + line-height: 1.6;
  500 + white-space: pre-wrap;
  501 + background: #f5f7fa;
  502 + padding: 12px;
  503 + border-radius: 6px;
  504 + }
  505 + }
  506 +
  507 + .changelog-more {
  508 + text-align: center;
  509 + padding: 12px 0 0;
  510 + }
  511 +}
380 512 </style>
381 513 \ No newline at end of file
... ...
antis-ncc-admin/src/views/extend/companyCalendar/components/EventTypeDialog.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + title="事件类型管理"
  4 + :visible.sync="visibleProxy"
  5 + width="700px"
  6 + :close-on-click-modal="false"
  7 + append-to-body
  8 + >
  9 + <div class="type-manage-container">
  10 + <div class="type-manage-header">
  11 + <el-button type="primary" size="small" icon="el-icon-plus" @click="handleAdd">新增类型</el-button>
  12 + </div>
  13 +
  14 + <el-table :data="typeList" border size="small" style="width: 100%">
  15 + <el-table-column prop="TypeName" label="类型名称" align="center" />
  16 + <el-table-column prop="TypeCode" label="类型编码" align="center" />
  17 + <el-table-column label="颜色" align="center" width="100">
  18 + <template slot-scope="scope">
  19 + <span class="color-preview" :style="{ background: scope.row.Color }"></span>
  20 + </template>
  21 + </el-table-column>
  22 + <el-table-column prop="SortCode" label="排序" align="center" width="80" />
  23 + <el-table-column label="操作" align="center" width="150">
  24 + <template slot-scope="scope">
  25 + <el-button type="text" size="small" @click="handleEdit(scope.row)">编辑</el-button>
  26 + <el-button type="text" size="small" class="NCC-common-error-color" @click="handleDelete(scope.row)">删除</el-button>
  27 + </template>
  28 + </el-table-column>
  29 + </el-table>
  30 + </div>
  31 +
  32 + <!-- 新增/编辑类型弹窗 -->
  33 + <el-dialog
  34 + :title="typeDialogTitle"
  35 + :visible.sync="typeDialogVisible"
  36 + width="450px"
  37 + :close-on-click-modal="false"
  38 + append-to-body
  39 + @close="resetTypeForm"
  40 + >
  41 + <el-form ref="typeForm" :model="typeForm" :rules="typeRules" label-width="80px" size="small">
  42 + <el-form-item label="类型名称" prop="typeName">
  43 + <el-input v-model="typeForm.typeName" placeholder="请输入类型名称" />
  44 + </el-form-item>
  45 + <el-form-item label="类型编码" prop="typeCode">
  46 + <el-input v-model="typeForm.typeCode" placeholder="请输入类型编码(英文)" />
  47 + </el-form-item>
  48 + <el-form-item label="颜色" prop="color">
  49 + <div class="color-picker-wrapper">
  50 + <el-color-picker
  51 + v-model="typeForm.color"
  52 + :predefine="predefineColors"
  53 + show-alpha
  54 + />
  55 + <span class="color-value">{{ typeForm.color }}</span>
  56 + </div>
  57 + </el-form-item>
  58 + <el-form-item label="排序" prop="sortCode">
  59 + <el-input-number v-model="typeForm.sortCode" :min="0" :max="999" controls-position="right" />
  60 + </el-form-item>
  61 + </el-form>
  62 + <div slot="footer" class="dialog-footer">
  63 + <el-button size="small" @click="typeDialogVisible = false">取 消</el-button>
  64 + <el-button type="primary" size="small" :loading="typeSubmitLoading" @click="handleTypeSubmit">确 定</el-button>
  65 + </div>
  66 + </el-dialog>
  67 + </el-dialog>
  68 +</template>
  69 +
  70 +<script>
  71 +import {
  72 + getEventTypeList,
  73 + createEventType,
  74 + updateEventType,
  75 + deleteEventType
  76 +} from '@/api/extend/lqCompanyCalendar'
  77 +
  78 +export default {
  79 + name: 'EventTypeDialog',
  80 + props: {
  81 + visible: {
  82 + type: Boolean,
  83 + default: false
  84 + }
  85 + },
  86 + data() {
  87 + return {
  88 + typeList: [],
  89 + typeDialogVisible: false,
  90 + isTypeEdit: false,
  91 + typeSubmitLoading: false,
  92 + typeForm: {
  93 + id: '',
  94 + typeName: '',
  95 + typeCode: '',
  96 + color: '#3b82f6',
  97 + sortCode: 0
  98 + },
  99 + typeRules: {
  100 + typeName: [{ required: true, message: '请输入类型名称', trigger: 'blur' }],
  101 + typeCode: [{ required: true, message: '请输入类型编码', trigger: 'blur' }],
  102 + color: [{ required: true, message: '请选择颜色', trigger: 'change' }]
  103 + },
  104 + predefineColors: [
  105 + '#ff4500',
  106 + '#ff8c00',
  107 + '#ffd700',
  108 + '#90ee90',
  109 + '#00ced1',
  110 + '#1e90ff',
  111 + '#c71585',
  112 + '#6366f1',
  113 + '#22c55e',
  114 + '#f97316',
  115 + '#ec4899',
  116 + '#8b5cf6',
  117 + '#06b6d4',
  118 + '#84cc16',
  119 + '#f59e0b',
  120 + '#ef4444'
  121 + ]
  122 + }
  123 + },
  124 + computed: {
  125 + visibleProxy: {
  126 + get() {
  127 + return this.visible
  128 + },
  129 + set(v) {
  130 + this.$emit('update:visible', v)
  131 + }
  132 + },
  133 + typeDialogTitle() {
  134 + return this.isTypeEdit ? '编辑类型' : '新增类型'
  135 + }
  136 + },
  137 + created() {
  138 + this.loadTypeList()
  139 + },
  140 + methods: {
  141 + // 加载类型列表
  142 + loadTypeList() {
  143 + getEventTypeList({}).then(response => {
  144 + if (response.code === 200) {
  145 + this.typeList = response.data || []
  146 + }
  147 + }).catch(() => {})
  148 + },
  149 +
  150 + // 新增类型
  151 + handleAdd() {
  152 + this.isTypeEdit = false
  153 + this.typeForm = {
  154 + id: '',
  155 + typeName: '',
  156 + typeCode: '',
  157 + color: '#3b82f6',
  158 + sortCode: 0
  159 + }
  160 + this.typeDialogVisible = true
  161 + this.$nextTick(() => {
  162 + this.$refs.typeForm && this.$refs.typeForm.clearValidate()
  163 + })
  164 + },
  165 +
  166 + // 编辑类型
  167 + handleEdit(row) {
  168 + this.isTypeEdit = true
  169 + this.typeForm = {
  170 + id: row.Id,
  171 + typeName: row.TypeName,
  172 + typeCode: row.TypeCode,
  173 + color: row.Color,
  174 + sortCode: row.SortCode
  175 + }
  176 + this.typeDialogVisible = true
  177 + this.$nextTick(() => {
  178 + this.$refs.typeForm && this.$refs.typeForm.clearValidate()
  179 + })
  180 + },
  181 +
  182 + // 删除类型
  183 + handleDelete(row) {
  184 + this.$confirm('确定要删除该事件类型吗?', '提示', {
  185 + confirmButtonText: '确定',
  186 + cancelButtonText: '取消',
  187 + type: 'warning'
  188 + }).then(async () => {
  189 + try {
  190 + await deleteEventType(row.Id)
  191 + this.$message.success('删除成功')
  192 + this.loadTypeList()
  193 + this.$emit('refresh')
  194 + } catch (e) {
  195 + console.error('删除失败:', e)
  196 + this.$message.error(e.message || '删除失败')
  197 + }
  198 + }).catch(() => {})
  199 + },
  200 +
  201 + // 提交类型表单
  202 + handleTypeSubmit() {
  203 + this.$refs.typeForm.validate(async (valid) => {
  204 + if (!valid) return
  205 +
  206 + this.typeSubmitLoading = true
  207 + try {
  208 + if (this.isTypeEdit) {
  209 + await updateEventType(this.typeForm)
  210 + this.$message.success('更新成功')
  211 + } else {
  212 + await createEventType(this.typeForm)
  213 + this.$message.success('创建成功')
  214 + }
  215 + this.typeDialogVisible = false
  216 + this.loadTypeList()
  217 + this.$emit('refresh')
  218 + } catch (e) {
  219 + console.error('保存失败:', e)
  220 + this.$message.error(e.message || '保存失败')
  221 + } finally {
  222 + this.typeSubmitLoading = false
  223 + }
  224 + })
  225 + },
  226 +
  227 + // 重置类型表单
  228 + resetTypeForm() {
  229 + this.typeForm = {
  230 + id: '',
  231 + typeName: '',
  232 + typeCode: '',
  233 + color: '#3b82f6',
  234 + sortCode: 0
  235 + }
  236 + }
  237 + }
  238 +}
  239 +</script>
  240 +
  241 +<style lang="scss" scoped>
  242 +.type-manage-container {
  243 + max-height: 500px;
  244 + overflow-y: auto;
  245 +}
  246 +
  247 +.type-manage-header {
  248 + margin-bottom: 15px;
  249 +}
  250 +
  251 +.color-preview {
  252 + display: inline-block;
  253 + width: 30px;
  254 + height: 30px;
  255 + border-radius: 4px;
  256 + border: 1px solid #dcdfe6;
  257 +}
  258 +
  259 +.color-picker-wrapper {
  260 + display: flex;
  261 + align-items: center;
  262 + gap: 10px;
  263 +}
  264 +
  265 +.color-value {
  266 + font-size: 12px;
  267 + color: #606266;
  268 +}
  269 +
  270 +.dialog-footer {
  271 + text-align: right;
  272 +}
  273 +</style>
... ...
antis-ncc-admin/src/views/extend/companyCalendar/index.vue 0 → 100644
  1 +<template>
  2 + <div class="NCC-common-layout">
  3 + <div class="NCC-common-layout-center">
  4 + <!-- 搜索区域 -->
  5 + <el-row class="NCC-common-search-box" :gutter="16">
  6 + <el-form @submit.native.prevent>
  7 + <el-col :span="6">
  8 + <el-form-item label="事件标题">
  9 + <el-input v-model="query.title" placeholder="请输入事件标题" clearable />
  10 + </el-form-item>
  11 + </el-col>
  12 + <el-col :span="6">
  13 + <el-form-item label="事件类型">
  14 + <el-select v-model="query.eventType" placeholder="请选择事件类型" clearable>
  15 + <el-option
  16 + v-for="item in eventTypeList"
  17 + :key="item.Id"
  18 + :label="item.TypeName"
  19 + :value="item.Id"
  20 + >
  21 + <span class="type-dot" :style="{ background: item.Color }"></span>
  22 + {{ item.TypeName }}
  23 + </el-option>
  24 + </el-select>
  25 + </el-form-item>
  26 + </el-col>
  27 + <el-col :span="6">
  28 + <el-form-item label="开始时间">
  29 + <el-date-picker v-model="query.startTime" type="date" placeholder="选择开始时间"
  30 + value-format="yyyy-MM-dd" clearable>
  31 + </el-date-picker>
  32 + </el-form-item>
  33 + </el-col>
  34 + <el-col :span="6">
  35 + <el-form-item>
  36 + <el-button type="primary" icon="el-icon-search" @click="search()">查询</el-button>
  37 + <el-button icon="el-icon-refresh-right" @click="reset()">重置</el-button>
  38 + </el-form-item>
  39 + </el-col>
  40 + </el-form>
  41 + </el-row>
  42 +
  43 + <!-- 主要内容区域 -->
  44 + <div class="NCC-common-layout-main NCC-flex-main">
  45 + <!-- 操作按钮区域 -->
  46 + <div class="NCC-common-head">
  47 + <div>
  48 + <el-button type="primary" icon="el-icon-plus" @click="handleAdd()">新增事件</el-button>
  49 + <el-button type="success" icon="el-icon-setting" @click="handleManageType()">事件类型管理</el-button>
  50 + <el-button type="text" icon="el-icon-download" @click="exportData()">导出</el-button>
  51 + </div>
  52 + <div class="NCC-common-head-right">
  53 + <el-tooltip effect="dark" content="刷新" placement="top">
  54 + <el-link icon="icon-ym icon-ym-Refresh NCC-common-head-icon" :underline="false"
  55 + @click="reset()" />
  56 + </el-tooltip>
  57 + <screenfull isContainer />
  58 + </div>
  59 + </div>
  60 +
  61 + <!-- 事件列表表格 -->
  62 + <NCC-table v-loading="listLoading" :data="list" has-c @selection-change="handleSelectionChange">
  63 + <el-table-column prop="Title" label="事件标题" align="left" show-overflow-tooltip />
  64 + <el-table-column label="事件类型" align="center" width="120">
  65 + <template slot-scope="scope">
  66 + <span class="type-dot" :style="{ background: scope.row.EventTypeColor }"></span>
  67 + {{ scope.row.EventTypeName }}
  68 + </template>
  69 + </el-table-column>
  70 + <el-table-column prop="Date" label="日期" align="center" width="120" />
  71 + <el-table-column label="时间" align="center" width="150">
  72 + <template slot-scope="scope">
  73 + {{ formatTimeRange(scope.row.Start, scope.row.End) }}
  74 + </template>
  75 + </el-table-column>
  76 + <el-table-column prop="Store" label="门店/地点" align="center" width="150" show-overflow-tooltip />
  77 + <el-table-column prop="Remark" label="备注" align="left" show-overflow-tooltip />
  78 + <el-table-column prop="CreateTime" label="创建时间" align="center" width="160"
  79 + :formatter="ncc.tableDateTimeFormat" />
  80 + <el-table-column label="操作" align="center" width="180" fixed="right">
  81 + <template slot-scope="scope">
  82 + <el-button type="text" @click="handleView(scope.row)">查看</el-button>
  83 + <el-button type="text" @click="handleEdit(scope.row)">编辑</el-button>
  84 + <el-button type="text" class="NCC-common-error-color" @click="handleDelete(scope.row)">删除</el-button>
  85 + </template>
  86 + </el-table-column>
  87 + </NCC-table>
  88 +
  89 + <!-- 分页组件 -->
  90 + <pagination v-show="total > 0" :total="total" :page.sync="query.currentPage"
  91 + :limit.sync="query.pageSize" @pagination="getList" />
  92 + </div>
  93 + </div>
  94 +
  95 + <!-- 新增/编辑事件弹窗 -->
  96 + <el-dialog
  97 + :title="dialogTitle"
  98 + :visible.sync="dialogVisible"
  99 + width="600px"
  100 + :close-on-click-modal="false"
  101 + append-to-body
  102 + >
  103 + <el-form ref="eventForm" :model="eventForm" :rules="eventRules" label-width="100px" size="small">
  104 + <el-form-item label="事件标题" prop="title">
  105 + <el-input v-model="eventForm.title" placeholder="请输入事件标题" />
  106 + </el-form-item>
  107 + <el-form-item label="事件类型" prop="eventType">
  108 + <el-select v-model="eventForm.eventType" placeholder="请选择事件类型" style="width: 100%">
  109 + <el-option
  110 + v-for="item in eventTypeList"
  111 + :key="item.Id"
  112 + :label="item.TypeName"
  113 + :value="item.Id"
  114 + >
  115 + <span class="type-dot" :style="{ background: item.Color }"></span>
  116 + {{ item.TypeName }}
  117 + </el-option>
  118 + </el-select>
  119 + </el-form-item>
  120 + <el-form-item label="开始时间" prop="startTime">
  121 + <el-date-picker
  122 + v-model="eventForm.startTime"
  123 + type="datetime"
  124 + placeholder="选择开始时间"
  125 + style="width: 100%"
  126 + value-format="yyyy-MM-dd HH:mm:ss"
  127 + />
  128 + </el-form-item>
  129 + <el-form-item label="结束时间" prop="endTime">
  130 + <el-date-picker
  131 + v-model="eventForm.endTime"
  132 + type="datetime"
  133 + placeholder="选择结束时间"
  134 + style="width: 100%"
  135 + value-format="yyyy-MM-dd HH:mm:ss"
  136 + />
  137 + </el-form-item>
  138 + <el-form-item label="门店/地点">
  139 + <el-select
  140 + v-model="eventForm.storeId"
  141 + placeholder="请选择门店"
  142 + filterable
  143 + clearable
  144 + style="width: 100%"
  145 + @change="handleStoreChange"
  146 + >
  147 + <el-option
  148 + v-for="item in storeList"
  149 + :key="item.id"
  150 + :label="item.fullName"
  151 + :value="item.id"
  152 + />
  153 + </el-select>
  154 + </el-form-item>
  155 + <el-form-item label="事件描述">
  156 + <el-input v-model="eventForm.description" type="textarea" :rows="3" placeholder="请输入事件描述" />
  157 + </el-form-item>
  158 + <el-form-item label="备注">
  159 + <el-input v-model="eventForm.remark" type="textarea" :rows="2" placeholder="请输入备注" />
  160 + </el-form-item>
  161 + <el-form-item label="附件">
  162 + <el-upload
  163 + :action="uploadUrl"
  164 + :headers="uploadHeaders"
  165 + :file-list="fileList"
  166 + :on-success="handleUploadSuccess"
  167 + :on-remove="handleUploadRemove"
  168 + :before-upload="beforeUpload"
  169 + multiple
  170 + >
  171 + <el-button size="small" type="primary">点击上传</el-button>
  172 + <div slot="tip" class="el-upload__tip">支持上传图片、文档等文件</div>
  173 + </el-upload>
  174 + </el-form-item>
  175 + </el-form>
  176 + <div slot="footer" class="dialog-footer">
  177 + <el-button size="small" @click="dialogVisible = false">取 消</el-button>
  178 + <el-button type="primary" size="small" :loading="submitLoading" @click="handleSubmit">确 定</el-button>
  179 + </div>
  180 + </el-dialog>
  181 +
  182 + <!-- 查看详情弹窗 -->
  183 + <el-dialog
  184 + title="事件详情"
  185 + :visible.sync="detailVisible"
  186 + width="520px"
  187 + append-to-body
  188 + >
  189 + <div v-if="selectedEvent" class="detail-content">
  190 + <div class="detail-row">
  191 + <span class="detail-label">主题</span>
  192 + <span class="detail-value">{{ selectedEvent.Title || '无' }}</span>
  193 + </div>
  194 + <div class="detail-row">
  195 + <span class="detail-label">类型</span>
  196 + <span class="detail-value">
  197 + <span class="type-dot" :style="{ background: selectedEvent.EventTypeColor }"></span>
  198 + {{ selectedEvent.EventTypeName }}
  199 + </span>
  200 + </div>
  201 + <div class="detail-row">
  202 + <span class="detail-label">日期</span>
  203 + <span class="detail-value">{{ selectedEvent.Date }}</span>
  204 + </div>
  205 + <div class="detail-row">
  206 + <span class="detail-label">时间</span>
  207 + <span class="detail-value">{{ formatTimeRange(selectedEvent.Start, selectedEvent.End) }}</span>
  208 + </div>
  209 + <div class="detail-row" v-if="selectedEvent.Store">
  210 + <span class="detail-label">门店/地点</span>
  211 + <span class="detail-value">{{ selectedEvent.Store }}</span>
  212 + </div>
  213 + <div class="detail-row" v-if="selectedEvent.Desc">
  214 + <span class="detail-label">内容</span>
  215 + <span class="detail-value detail-value--multiline">{{ selectedEvent.Desc }}</span>
  216 + </div>
  217 + <div class="detail-row" v-if="selectedEvent.Remark">
  218 + <span class="detail-label">备注</span>
  219 + <span class="detail-value detail-value--multiline">{{ selectedEvent.Remark }}</span>
  220 + </div>
  221 + <div class="detail-row" v-if="selectedEvent.Attachments">
  222 + <span class="detail-label">附件</span>
  223 + <span class="detail-value">
  224 + <div v-for="(att, idx) in parseAttachments(selectedEvent.Attachments)" :key="idx" class="attachment-item">
  225 + <i class="el-icon-document"></i>
  226 + <a v-if="att.url" :href="att.url" target="_blank">{{ att.name || att.fileName }}</a>
  227 + <span v-else>{{ att.name || att.fileName }}</span>
  228 + </div>
  229 + <span v-if="!parseAttachments(selectedEvent.Attachments).length">无</span>
  230 + </span>
  231 + </div>
  232 + </div>
  233 + <div slot="footer" class="dialog-footer">
  234 + <el-button size="small" @click="handleEdit(selectedEvent)">编 辑</el-button>
  235 + <el-button size="small" @click="detailVisible = false">关 闭</el-button>
  236 + </div>
  237 + </el-dialog>
  238 +
  239 + <!-- 事件类型管理弹窗 -->
  240 + <EventTypeDialog
  241 + v-if="eventTypeDialogVisible"
  242 + :visible.sync="eventTypeDialogVisible"
  243 + @refresh="loadEventTypeList"
  244 + />
  245 + </div>
  246 +</template>
  247 +
  248 +<script>
  249 +import {
  250 + getCalendarEvents,
  251 + createCalendarEvent,
  252 + updateCalendarEvent,
  253 + deleteCalendarEvent,
  254 + getEventTypeList
  255 +} from '@/api/extend/lqCompanyCalendar'
  256 +import { getStoreSelector } from '@/api/extend/store'
  257 +import Pagination from '@/components/Pagination'
  258 +import EventTypeDialog from './components/EventTypeDialog.vue'
  259 +import { getToken } from '@/utils/auth'
  260 +
  261 +export default {
  262 + name: 'CompanyCalendar',
  263 + components: {
  264 + Pagination,
  265 + EventTypeDialog
  266 + },
  267 + data() {
  268 + return {
  269 + query: {
  270 + title: '',
  271 + eventType: '',
  272 + startTime: '',
  273 + currentPage: 1,
  274 + pageSize: 20
  275 + },
  276 + list: [],
  277 + total: 0,
  278 + listLoading: true,
  279 + dialogVisible: false,
  280 + detailVisible: false,
  281 + eventTypeDialogVisible: false,
  282 + isEdit: false,
  283 + submitLoading: false,
  284 + eventForm: {
  285 + id: '',
  286 + title: '',
  287 + eventType: '',
  288 + startTime: '',
  289 + endTime: '',
  290 + storeId: '',
  291 + storeName: '',
  292 + description: '',
  293 + remark: '',
  294 + attachments: ''
  295 + },
  296 + eventRules: {
  297 + title: [{ required: true, message: '请输入事件标题', trigger: 'blur' }],
  298 + eventType: [{ required: true, message: '请选择事件类型', trigger: 'change' }],
  299 + startTime: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
  300 + endTime: [{ required: true, message: '请选择结束时间', trigger: 'change' }]
  301 + },
  302 + selectedEvent: null,
  303 + selectedIds: [],
  304 + eventTypeList: [],
  305 + storeList: [],
  306 + fileList: [],
  307 + uploadUrl: '/api/Extend/OssDirectUpload/Upload',
  308 + uploadHeaders: {}
  309 + }
  310 + },
  311 + computed: {
  312 + dialogTitle() {
  313 + return this.isEdit ? '编辑事件' : '新增事件'
  314 + }
  315 + },
  316 + created() {
  317 + this.uploadHeaders = { Authorization: 'Bearer ' + getToken() }
  318 + this.getList()
  319 + this.loadEventTypeList()
  320 + this.loadStoreList()
  321 + },
  322 + methods: {
  323 + // 获取事件列表
  324 + getList() {
  325 + this.listLoading = true
  326 + getCalendarEvents(this.query).then(response => {
  327 + if (response.code === 200) {
  328 + this.list = response.data.List || []
  329 + this.total = response.data.Total || 0
  330 + } else {
  331 + this.$message.error(response.msg || '获取事件列表失败')
  332 + }
  333 + this.listLoading = false
  334 + }).catch(() => {
  335 + this.listLoading = false
  336 + })
  337 + },
  338 +
  339 + // 加载事件类型列表
  340 + loadEventTypeList() {
  341 + getEventTypeList({}).then(response => {
  342 + if (response.code === 200) {
  343 + this.eventTypeList = response.data || []
  344 + }
  345 + }).catch(() => {})
  346 + },
  347 +
  348 + // 加载门店列表
  349 + loadStoreList() {
  350 + getStoreSelector().then(response => {
  351 + if (response.code === 200) {
  352 + this.storeList = response.data.list || []
  353 + }
  354 + }).catch(() => {})
  355 + },
  356 +
  357 + // 搜索
  358 + search() {
  359 + this.query.currentPage = 1
  360 + this.getList()
  361 + },
  362 +
  363 + // 重置
  364 + reset() {
  365 + this.query = {
  366 + title: '',
  367 + eventType: '',
  368 + startTime: '',
  369 + currentPage: 1,
  370 + pageSize: 20
  371 + }
  372 + this.getList()
  373 + },
  374 +
  375 + // 新增事件
  376 + handleAdd() {
  377 + this.isEdit = false
  378 + this.eventForm = {
  379 + id: '',
  380 + title: '',
  381 + eventType: '',
  382 + startTime: '',
  383 + endTime: '',
  384 + storeId: '',
  385 + storeName: '',
  386 + description: '',
  387 + remark: '',
  388 + attachments: ''
  389 + }
  390 + this.fileList = []
  391 + this.dialogVisible = true
  392 + this.$nextTick(() => {
  393 + this.$refs.eventForm && this.$refs.eventForm.clearValidate()
  394 + })
  395 + },
  396 +
  397 + // 查看详情
  398 + handleView(row) {
  399 + this.selectedEvent = row
  400 + this.detailVisible = true
  401 + },
  402 +
  403 + // 编辑事件
  404 + handleEdit(row) {
  405 + this.isEdit = true
  406 + this.eventForm = {
  407 + id: row.Id,
  408 + title: row.Title,
  409 + eventType: row.EventType,
  410 + startTime: row.Start,
  411 + endTime: row.End,
  412 + storeId: row.StoreId,
  413 + storeName: row.Store,
  414 + description: row.Desc,
  415 + remark: row.Remark,
  416 + attachments: row.Attachments
  417 + }
  418 + this.fileList = this.parseAttachments(row.Attachments)
  419 + this.detailVisible = false
  420 + this.dialogVisible = true
  421 + this.$nextTick(() => {
  422 + this.$refs.eventForm && this.$refs.eventForm.clearValidate()
  423 + })
  424 + },
  425 +
  426 + // 删除事件
  427 + handleDelete(row) {
  428 + this.$confirm('确定要删除该事件吗?', '提示', {
  429 + confirmButtonText: '确定',
  430 + cancelButtonText: '取消',
  431 + type: 'warning'
  432 + }).then(async () => {
  433 + try {
  434 + await deleteCalendarEvent(row.Id)
  435 + this.$message.success('删除成功')
  436 + this.getList()
  437 + } catch (e) {
  438 + console.error('删除失败:', e)
  439 + this.$message.error('删除失败')
  440 + }
  441 + }).catch(() => {})
  442 + },
  443 +
  444 + // 提交表单
  445 + handleSubmit() {
  446 + this.$refs.eventForm.validate(async (valid) => {
  447 + if (!valid) return
  448 +
  449 + if (new Date(this.eventForm.endTime) <= new Date(this.eventForm.startTime)) {
  450 + this.$message.warning('结束时间必须大于开始时间')
  451 + return
  452 + }
  453 +
  454 + this.submitLoading = true
  455 + try {
  456 + const data = {
  457 + ...this.eventForm,
  458 + attachments: JSON.stringify(this.fileList)
  459 + }
  460 +
  461 + if (this.isEdit) {
  462 + await updateCalendarEvent(data)
  463 + this.$message.success('更新成功')
  464 + } else {
  465 + await createCalendarEvent(data)
  466 + this.$message.success('创建成功')
  467 + }
  468 + this.dialogVisible = false
  469 + this.getList()
  470 + } catch (e) {
  471 + console.error('保存失败:', e)
  472 + this.$message.error('保存失败')
  473 + } finally {
  474 + this.submitLoading = false
  475 + }
  476 + })
  477 + },
  478 +
  479 + // 门店选择变化
  480 + handleStoreChange(storeId) {
  481 + const store = this.storeList.find(x => x.id === storeId)
  482 + this.eventForm.storeName = store ? store.fullName : ''
  483 + },
  484 +
  485 + // 上传成功
  486 + handleUploadSuccess(response, file, fileList) {
  487 + if (response.code === 200) {
  488 + this.fileList.push({
  489 + name: file.name,
  490 + url: response.data.url,
  491 + fileName: file.name
  492 + })
  493 + } else {
  494 + this.$message.error('上传失败')
  495 + }
  496 + },
  497 +
  498 + // 移除文件
  499 + handleUploadRemove(file, fileList) {
  500 + this.fileList = fileList.map(f => ({
  501 + name: f.name,
  502 + url: f.url || (f.response && f.response.data && f.response.data.url),
  503 + fileName: f.name
  504 + }))
  505 + },
  506 +
  507 + // 上传前校验
  508 + beforeUpload(file) {
  509 + const maxSize = 10 * 1024 * 1024 // 10MB
  510 + if (file.size > maxSize) {
  511 + this.$message.error('文件大小不能超过10MB')
  512 + return false
  513 + }
  514 + return true
  515 + },
  516 +
  517 + // 解析附件
  518 + parseAttachments(attachments) {
  519 + if (!attachments) return []
  520 + try {
  521 + return typeof attachments === 'string' ? JSON.parse(attachments) : attachments
  522 + } catch {
  523 + return []
  524 + }
  525 + },
  526 +
  527 + // 格式化时间范围
  528 + formatTimeRange(start, end) {
  529 + if (!start) return '--'
  530 + const fmt = (dt) => {
  531 + const d = new Date(dt)
  532 + return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
  533 + }
  534 + return `${fmt(start)} - ${fmt(end)}`
  535 + },
  536 +
  537 + // 管理事件类型
  538 + handleManageType() {
  539 + this.eventTypeDialogVisible = true
  540 + },
  541 +
  542 + // 选择变化
  543 + handleSelectionChange(selection) {
  544 + this.selectedIds = selection.map(item => item.id)
  545 + },
  546 +
  547 + // 导出数据
  548 + exportData() {
  549 + this.$message.info('导出功能开发中')
  550 + }
  551 + }
  552 +}
  553 +</script>
  554 +
  555 +<style lang="scss" scoped>
  556 +.type-dot {
  557 + display: inline-block;
  558 + width: 10px;
  559 + height: 10px;
  560 + border-radius: 50%;
  561 + margin-right: 6px;
  562 + vertical-align: middle;
  563 +}
  564 +
  565 +.detail-content {
  566 + padding: 10px 0;
  567 +}
  568 +
  569 +.detail-row {
  570 + display: flex;
  571 + padding: 10px 0;
  572 + border-bottom: 1px solid #f1f5f9;
  573 + font-size: 14px;
  574 +
  575 + &:last-child {
  576 + border-bottom: none;
  577 + }
  578 +}
  579 +
  580 +.detail-label {
  581 + width: 90px;
  582 + color: #64748b;
  583 + flex-shrink: 0;
  584 +}
  585 +
  586 +.detail-value {
  587 + flex: 1;
  588 + color: #111827;
  589 +
  590 + &--multiline {
  591 + white-space: pre-wrap;
  592 + word-break: break-word;
  593 + }
  594 +}
  595 +
  596 +.attachment-item {
  597 + display: flex;
  598 + align-items: center;
  599 + gap: 6px;
  600 + padding: 4px 0;
  601 +
  602 + i {
  603 + color: #409eff;
  604 + }
  605 +
  606 + a {
  607 + color: #409eff;
  608 + text-decoration: none;
  609 +
  610 + &:hover {
  611 + text-decoration: underline;
  612 + }
  613 + }
  614 +}
  615 +
  616 +.dialog-footer {
  617 + text-align: right;
  618 +}
  619 +</style>
... ...
antis-ncc-admin/src/views/lqChangelog/index.vue 0 → 100644
  1 +<template>
  2 + <div class="NCC-common-layout">
  3 + <div class="NCC-common-layout-center">
  4 + <!-- 统计卡片 -->
  5 + <el-row :gutter="16" class="stat-cards">
  6 + <el-col :span="6" v-for="card in statCards" :key="card.key">
  7 + <div
  8 + class="stat-card"
  9 + :class="{ active: activeStatus === card.value }"
  10 + @click="handleCardClick(card.value)"
  11 + >
  12 + <div class="stat-card__icon" :style="{ backgroundColor: card.bgColor }">
  13 + <i :class="card.icon" :style="{ color: card.color }"></i>
  14 + </div>
  15 + <div class="stat-card__info">
  16 + <div class="stat-card__count">{{ card.count }}</div>
  17 + <div class="stat-card__label">{{ card.label }}</div>
  18 + </div>
  19 + </div>
  20 + </el-col>
  21 + </el-row>
  22 +
  23 + <!-- 筛选区 -->
  24 + <el-row class="NCC-common-search-box" :gutter="16">
  25 + <el-form @submit.native.prevent>
  26 + <el-col :span="6">
  27 + <el-form-item label="关键字">
  28 + <el-input
  29 + v-model="query.keyword"
  30 + placeholder="搜索标题/版本号"
  31 + clearable
  32 + @keyup.enter.native="search"
  33 + />
  34 + </el-form-item>
  35 + </el-col>
  36 + <el-col :span="6">
  37 + <el-form-item label="更新类型">
  38 + <el-select v-model="query.type" placeholder="全部" clearable>
  39 + <el-option label="功能更新" :value="1" />
  40 + <el-option label="Bug修复" :value="2" />
  41 + <el-option label="性能优化" :value="3" />
  42 + <el-option label="安全更新" :value="4" />
  43 + </el-select>
  44 + </el-form-item>
  45 + </el-col>
  46 + <el-col :span="6">
  47 + <el-form-item label="发布状态">
  48 + <el-select v-model="query.isPublished" placeholder="全部" clearable>
  49 + <el-option label="已发布" :value="true" />
  50 + <el-option label="草稿" :value="false" />
  51 + </el-select>
  52 + </el-form-item>
  53 + </el-col>
  54 + <el-col :span="6">
  55 + <el-form-item>
  56 + <el-button type="primary" icon="el-icon-search" @click="search">查询</el-button>
  57 + <el-button icon="el-icon-refresh-right" @click="reset">重置</el-button>
  58 + </el-form-item>
  59 + </el-col>
  60 + </el-form>
  61 + </el-row>
  62 +
  63 + <!-- 表格区 -->
  64 + <div class="NCC-common-layout-main NCC-flex-main">
  65 + <div class="NCC-common-head">
  66 + <div>
  67 + <el-button type="primary" icon="el-icon-plus" @click="handleAdd">新增日志</el-button>
  68 + </div>
  69 + <div class="NCC-common-head-right">
  70 + <el-tooltip effect="dark" content="刷新" placement="top">
  71 + <el-link
  72 + icon="icon-ym icon-ym-Refresh NCC-common-head-icon"
  73 + :underline="false"
  74 + @click="reset"
  75 + />
  76 + </el-tooltip>
  77 + </div>
  78 + </div>
  79 +
  80 + <el-table v-loading="listLoading" :data="list" border style="width: 100%">
  81 + <el-table-column prop="version" label="版本号" width="100" align="center">
  82 + <template slot-scope="scope">
  83 + <el-tag size="small" effect="plain">v{{ scope.row.version }}</el-tag>
  84 + </template>
  85 + </el-table-column>
  86 + <el-table-column prop="title" label="更新标题" width="180" show-overflow-tooltip />
  87 + <el-table-column prop="content" label="内容预览" min-width="250" show-overflow-tooltip>
  88 + <template slot-scope="scope">
  89 + <span class="content-preview">{{ getContentPreview(scope.row.content) }}</span>
  90 + </template>
  91 + </el-table-column>
  92 + <el-table-column prop="type" label="类型" width="100" align="center">
  93 + <template slot-scope="scope">
  94 + <el-tag :type="getTypeTagType(scope.row.type)" size="small">
  95 + {{ getTypeText(scope.row.type) }}
  96 + </el-tag>
  97 + </template>
  98 + </el-table-column>
  99 + <el-table-column prop="isPublished" label="状态" width="80" align="center">
  100 + <template slot-scope="scope">
  101 + <el-tag :type="scope.row.isPublished ? 'success' : 'info'" size="small">
  102 + {{ scope.row.isPublished ? '已发布' : '草稿' }}
  103 + </el-tag>
  104 + </template>
  105 + </el-table-column>
  106 + <el-table-column prop="publishedTime" label="发布时间" width="160">
  107 + <template slot-scope="scope">
  108 + {{ scope.row.publishedTime ? formatDateTime(scope.row.publishedTime) : '-' }}
  109 + </template>
  110 + </el-table-column>
  111 + <el-table-column label="操作" width="220" fixed="right" align="center">
  112 + <template slot-scope="scope">
  113 + <el-button type="text" size="small" @click="handleView(scope.row)">查看</el-button>
  114 + <el-button type="text" size="small" @click="handleEdit(scope.row)">编辑</el-button>
  115 + <el-button type="text" size="small" @click="handleTogglePublish(scope.row)">
  116 + {{ scope.row.isPublished ? '取消发布' : '发布' }}
  117 + </el-button>
  118 + <el-button type="text" size="small" class="NCC-table-danger"
  119 + @click="handleDelete(scope.row)">删除</el-button>
  120 + </template>
  121 + </el-table-column>
  122 + </el-table>
  123 +
  124 + <pagination
  125 + :total="total"
  126 + :page.sync="listQuery.currentPage"
  127 + :limit.sync="listQuery.pageSize"
  128 + @pagination="initData"
  129 + />
  130 + </div>
  131 + </div>
  132 +
  133 + <!-- 新增/编辑弹窗 -->
  134 + <el-dialog
  135 + :title="dialogTitle"
  136 + :visible.sync="dialogVisible"
  137 + width="700px"
  138 + class="NCC-dialog"
  139 + :close-on-click-modal="false"
  140 + >
  141 + <el-form :model="formData" :rules="rules" ref="form" label-width="100px">
  142 + <el-form-item label="版本号" prop="version">
  143 + <el-input v-model="formData.version" placeholder="如:3.2.1" style="width: 200px" />
  144 + </el-form-item>
  145 + <el-form-item label="更新标题" prop="title">
  146 + <el-input v-model="formData.title" placeholder="请输入更新标题" maxlength="50" show-word-limit />
  147 + </el-form-item>
  148 + <el-form-item label="更新类型" prop="type">
  149 + <el-select v-model="formData.type" placeholder="请选择">
  150 + <el-option label="功能更新" :value="1" />
  151 + <el-option label="Bug修复" :value="2" />
  152 + <el-option label="性能优化" :value="3" />
  153 + <el-option label="安全更新" :value="4" />
  154 + </el-select>
  155 + </el-form-item>
  156 + <el-form-item label="更新内容" prop="content">
  157 + <el-input
  158 + v-model="formData.content"
  159 + type="textarea"
  160 + :rows="8"
  161 + placeholder="请输入更新内容,支持换行"
  162 + />
  163 + </el-form-item>
  164 + <el-form-item label="立即发布">
  165 + <el-switch v-model="formData.isPublished" />
  166 + </el-form-item>
  167 + </el-form>
  168 + <div slot="footer" class="dialog-footer">
  169 + <el-button @click="dialogVisible = false">取 消</el-button>
  170 + <el-button type="primary" @click="handleSubmit" :loading="submitLoading">确 定</el-button>
  171 + </div>
  172 + </el-dialog>
  173 +
  174 + <!-- 查看详情弹窗 -->
  175 + <el-dialog
  176 + title="更新日志详情"
  177 + :visible.sync="detailVisible"
  178 + width="600px"
  179 + class="NCC-dialog"
  180 + >
  181 + <div v-if="detailData" class="changelog-detail">
  182 + <div class="detail-header">
  183 + <el-tag size="small" effect="plain">v{{ detailData.version }}</el-tag>
  184 + <el-tag :type="getTypeTagType(detailData.type)" size="small">
  185 + {{ getTypeText(detailData.type) }}
  186 + </el-tag>
  187 + <el-tag :type="detailData.isPublished ? 'success' : 'info'" size="small">
  188 + {{ detailData.isPublished ? '已发布' : '草稿' }}
  189 + </el-tag>
  190 + </div>
  191 + <h3 class="detail-title">{{ detailData.title }}</h3>
  192 + <div class="detail-time">
  193 + <span v-if="detailData.publishedTime">发布时间:{{ formatDateTime(detailData.publishedTime) }}</span>
  194 + <span>创建时间:{{ formatDateTime(detailData.createTime) }}</span>
  195 + </div>
  196 + <div class="detail-content">{{ detailData.content }}</div>
  197 + </div>
  198 + </el-dialog>
  199 + </div>
  200 +</template>
  201 +
  202 +<script>
  203 +import { getChangelogList, addChangelog, updateChangelog, deleteChangelog, togglePublish } from '@/api/extend/changelog'
  204 +
  205 +export default {
  206 + name: 'LqChangelog',
  207 + data() {
  208 + return {
  209 + activeStatus: null,
  210 + stats: { total: 0, published: 0, draft: 0, type1: 0 },
  211 + query: {
  212 + keyword: undefined,
  213 + type: undefined,
  214 + isPublished: undefined
  215 + },
  216 + list: [],
  217 + total: 0,
  218 + listLoading: false,
  219 + listQuery: {
  220 + currentPage: 1,
  221 + pageSize: 20
  222 + },
  223 + dialogVisible: false,
  224 + dialogTitle: '新增更新日志',
  225 + formData: {
  226 + id: '',
  227 + version: '',
  228 + title: '',
  229 + content: '',
  230 + type: 1,
  231 + isPublished: false
  232 + },
  233 + rules: {
  234 + version: [{ required: true, message: '请输入版本号', trigger: 'blur' }],
  235 + title: [{ required: true, message: '请输入更新标题', trigger: 'blur' }],
  236 + content: [{ required: true, message: '请输入更新内容', trigger: 'blur' }],
  237 + type: [{ required: true, message: '请选择更新类型', trigger: 'change' }]
  238 + },
  239 + submitLoading: false,
  240 + detailVisible: false,
  241 + detailData: null
  242 + }
  243 + },
  244 + computed: {
  245 + statCards() {
  246 + return [
  247 + {
  248 + key: 'total',
  249 + label: '全部日志',
  250 + value: null,
  251 + count: this.stats.total,
  252 + icon: 'el-icon-document',
  253 + color: '#409EFF',
  254 + bgColor: 'rgba(64,158,255,0.1)'
  255 + },
  256 + {
  257 + key: 'published',
  258 + label: '已发布',
  259 + value: true,
  260 + count: this.stats.published,
  261 + icon: 'el-icon-circle-check',
  262 + color: '#67C23A',
  263 + bgColor: 'rgba(103,194,58,0.1)'
  264 + },
  265 + {
  266 + key: 'draft',
  267 + label: '草稿',
  268 + value: false,
  269 + count: this.stats.draft,
  270 + icon: 'el-icon-edit-outline',
  271 + color: '#909399',
  272 + bgColor: 'rgba(144,147,153,0.1)'
  273 + },
  274 + {
  275 + key: 'feature',
  276 + label: '功能更新',
  277 + value: 'feature',
  278 + count: this.stats.type1,
  279 + icon: 'el-icon-star-off',
  280 + color: '#E6A23C',
  281 + bgColor: 'rgba(230,162,60,0.1)'
  282 + }
  283 + ]
  284 + }
  285 + },
  286 + created() {
  287 + this.initData()
  288 + },
  289 + methods: {
  290 + loadStats() {
  291 + const allParams = { currentPage: 1, pageSize: 1 }
  292 + getChangelogList(allParams).then(res => {
  293 + this.stats.total = res.data.pagination.total
  294 + })
  295 + getChangelogList({ ...allParams, isPublished: true }).then(res => {
  296 + this.stats.published = res.data.pagination.total
  297 + })
  298 + getChangelogList({ ...allParams, isPublished: false }).then(res => {
  299 + this.stats.draft = res.data.pagination.total
  300 + })
  301 + getChangelogList({ ...allParams, type: 1 }).then(res => {
  302 + this.stats.type1 = res.data.pagination.total
  303 + })
  304 + },
  305 + initData() {
  306 + this.listLoading = true
  307 + this.loadStats()
  308 + const params = {
  309 + currentPage: this.listQuery.currentPage,
  310 + pageSize: this.listQuery.pageSize
  311 + }
  312 + if (this.query.keyword) params.keyword = this.query.keyword
  313 + if (this.query.type !== undefined && this.query.type !== null) params.type = this.query.type
  314 + if (this.query.isPublished !== undefined && this.query.isPublished !== null) params.isPublished = this.query.isPublished
  315 +
  316 + getChangelogList(params).then(res => {
  317 + this.list = res.data.list
  318 + this.total = res.data.pagination.total
  319 + this.listLoading = false
  320 + }).catch(() => {
  321 + this.listLoading = false
  322 + })
  323 + },
  324 + handleCardClick(value) {
  325 + if (value === 'feature') {
  326 + this.activeStatus = null
  327 + this.query.isPublished = undefined
  328 + this.query.type = 1
  329 + } else {
  330 + this.activeStatus = value
  331 + this.query.isPublished = value
  332 + this.query.type = undefined
  333 + }
  334 + this.listQuery.currentPage = 1
  335 + this.initData()
  336 + },
  337 + search() {
  338 + this.listQuery.currentPage = 1
  339 + this.initData()
  340 + },
  341 + reset() {
  342 + this.activeStatus = null
  343 + this.query = { keyword: undefined, type: undefined, isPublished: undefined }
  344 + this.listQuery = { currentPage: 1, pageSize: 20 }
  345 + this.initData()
  346 + },
  347 + handleAdd() {
  348 + this.dialogTitle = '新增更新日志'
  349 + this.formData = {
  350 + id: '',
  351 + version: '',
  352 + title: '',
  353 + content: '',
  354 + type: 1,
  355 + isPublished: false
  356 + }
  357 + this.dialogVisible = true
  358 + this.$nextTick(() => {
  359 + this.$refs.form && this.$refs.form.clearValidate()
  360 + })
  361 + },
  362 + handleEdit(row) {
  363 + this.dialogTitle = '编辑更新日志'
  364 + this.formData = {
  365 + id: row.id,
  366 + version: row.version,
  367 + title: row.title,
  368 + content: row.content,
  369 + type: row.type,
  370 + isPublished: row.isPublished
  371 + }
  372 + this.dialogVisible = true
  373 + this.$nextTick(() => {
  374 + this.$refs.form && this.$refs.form.clearValidate()
  375 + })
  376 + },
  377 + handleView(row) {
  378 + this.detailData = row
  379 + this.detailVisible = true
  380 + },
  381 + handleSubmit() {
  382 + this.$refs.form.validate(valid => {
  383 + if (!valid) return
  384 + this.submitLoading = true
  385 + const request = this.formData.id ? updateChangelog(this.formData) : addChangelog(this.formData)
  386 + request.then(res => {
  387 + this.$message.success('操作成功')
  388 + this.dialogVisible = false
  389 + this.initData()
  390 + }).finally(() => {
  391 + this.submitLoading = false
  392 + })
  393 + })
  394 + },
  395 + handleTogglePublish(row) {
  396 + const text = row.isPublished ? '取消发布' : '发布'
  397 + this.$confirm(`确定要${text}该日志吗?`, '提示', {
  398 + type: 'warning'
  399 + }).then(() => {
  400 + togglePublish(row.id).then(res => {
  401 + this.$message.success('操作成功')
  402 + this.initData()
  403 + })
  404 + }).catch(() => {})
  405 + },
  406 + handleDelete(row) {
  407 + this.$confirm('确定要删除该日志吗?删除后不可恢复!', '提示', {
  408 + type: 'warning'
  409 + }).then(() => {
  410 + deleteChangelog(row.id).then(res => {
  411 + this.$message.success('删除成功')
  412 + this.initData()
  413 + })
  414 + }).catch(() => {})
  415 + },
  416 + formatDateTime(dateTime) {
  417 + if (!dateTime) return '-'
  418 + const date = new Date(dateTime)
  419 + if (isNaN(date.getTime())) return dateTime
  420 + const y = date.getFullYear()
  421 + const m = String(date.getMonth() + 1).padStart(2, '0')
  422 + const d = String(date.getDate()).padStart(2, '0')
  423 + const h = String(date.getHours()).padStart(2, '0')
  424 + const min = String(date.getMinutes()).padStart(2, '0')
  425 + const s = String(date.getSeconds()).padStart(2, '0')
  426 + return `${y}-${m}-${d} ${h}:${min}:${s}`
  427 + },
  428 + getTypeText(type) {
  429 + const map = { 1: '功能更新', 2: 'Bug修复', 3: '性能优化', 4: '安全更新' }
  430 + return map[type] || '其他'
  431 + },
  432 + getTypeTagType(type) {
  433 + const map = { 1: 'primary', 2: 'danger', 3: 'success', 4: 'warning' }
  434 + return map[type] || 'info'
  435 + },
  436 + getContentPreview(content) {
  437 + if (!content) return '-'
  438 + return content.replace(/[\r\n]+/g, ' ').substring(0, 60) + (content.length > 60 ? '...' : '')
  439 + }
  440 + }
  441 +}
  442 +</script>
  443 +
  444 +<style lang="scss" scoped>
  445 +.stat-cards {
  446 + margin-bottom: 16px;
  447 +}
  448 +
  449 +.stat-card {
  450 + display: flex;
  451 + align-items: center;
  452 + height: 100px;
  453 + padding: 12px;
  454 + background: #fff;
  455 + border-radius: 12px;
  456 + border: 1px solid #ebeef5;
  457 + cursor: pointer;
  458 + transition: all 0.3s;
  459 +
  460 + &:hover {
  461 + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
  462 + }
  463 +
  464 + &.active {
  465 + border-color: #409eff;
  466 + box-shadow: 0 2px 12px rgba(64, 158, 255, 0.2);
  467 + }
  468 +
  469 + &__icon {
  470 + display: flex;
  471 + align-items: center;
  472 + justify-content: center;
  473 + width: 56px;
  474 + height: 56px;
  475 + border-radius: 12px;
  476 + flex-shrink: 0;
  477 +
  478 + i {
  479 + font-size: 28px;
  480 + }
  481 + }
  482 +
  483 + &__info {
  484 + margin-left: 16px;
  485 + display: flex;
  486 + flex-direction: column;
  487 + justify-content: center;
  488 + }
  489 +
  490 + &__count {
  491 + font-size: 28px;
  492 + font-weight: 600;
  493 + color: #303133;
  494 + line-height: 1.2;
  495 + }
  496 +
  497 + &__label {
  498 + font-size: 14px;
  499 + color: #909399;
  500 + margin-top: 4px;
  501 + }
  502 +}
  503 +
  504 +.content-preview {
  505 + color: #909399;
  506 + font-size: 13px;
  507 +}
  508 +
  509 +.changelog-detail {
  510 + .detail-header {
  511 + display: flex;
  512 + align-items: center;
  513 + gap: 8px;
  514 + margin-bottom: 16px;
  515 + }
  516 +
  517 + .detail-title {
  518 + margin: 0 0 12px 0;
  519 + font-size: 18px;
  520 + font-weight: 600;
  521 + color: #303133;
  522 + }
  523 +
  524 + .detail-time {
  525 + display: flex;
  526 + gap: 20px;
  527 + margin-bottom: 16px;
  528 + font-size: 13px;
  529 + color: #909399;
  530 + }
  531 +
  532 + .detail-content {
  533 + color: #606266;
  534 + line-height: 1.8;
  535 + white-space: pre-wrap;
  536 + background: #f5f7fa;
  537 + padding: 16px;
  538 + border-radius: 8px;
  539 + max-height: 400px;
  540 + overflow-y: auto;
  541 + }
  542 +}
  543 +</style>
... ...
antis-ncc-admin/src/views/lqUploadRecord/index.vue
... ... @@ -96,11 +96,22 @@
96 96 {{ scope.row.auditReason || '无' }}
97 97 </template>
98 98 </el-table-column>
99   - <el-table-column prop="userName" label="上传人" width="120" show-overflow-tooltip />
100   - <el-table-column prop="createTime" label="上传时间" width="160" />
  99 + <el-table-column prop="userName" label="上传人" width="100" show-overflow-tooltip />
  100 + <el-table-column prop="fileType" label="来源模块" width="120" align="center">
  101 + <template slot-scope="scope">
  102 + <el-tag size="small" :type="getFileTypeTagType(scope.row.fileType)">
  103 + {{ getFileTypeText(scope.row.fileType) }}
  104 + </el-tag>
  105 + </template>
  106 + </el-table-column>
  107 + <el-table-column prop="createTime" label="上传时间" width="160">
  108 + <template slot-scope="scope">
  109 + {{ formatDateTime(scope.row.createTime) }}
  110 + </template>
  111 + </el-table-column>
101 112 <el-table-column prop="auditTime" label="审核时间" width="160">
102 113 <template slot-scope="scope">
103   - {{ scope.row.auditTime || '无' }}
  114 + {{ scope.row.auditTime ? formatDateTime(scope.row.auditTime) : '-' }}
104 115 </template>
105 116 </el-table-column>
106 117 <el-table-column label="操作" width="120" fixed="right">
... ... @@ -258,6 +269,40 @@ export default {
258 269 if (row.imageUrl) {
259 270 window.open(row.imageUrl, '_blank')
260 271 }
  272 + },
  273 + formatDateTime(dateTime) {
  274 + if (!dateTime) return '-'
  275 + const date = new Date(dateTime)
  276 + if (isNaN(date.getTime())) return dateTime
  277 + const y = date.getFullYear()
  278 + const m = String(date.getMonth() + 1).padStart(2, '0')
  279 + const d = String(date.getDate()).padStart(2, '0')
  280 + const h = String(date.getHours()).padStart(2, '0')
  281 + const min = String(date.getMinutes()).padStart(2, '0')
  282 + const s = String(date.getSeconds()).padStart(2, '0')
  283 + return `${y}-${m}-${d} ${h}:${min}:${s}`
  284 + },
  285 + getFileTypeText(fileType) {
  286 + const map = {
  287 + 'annexpic': '附件图片',
  288 + 'attendancepic': '考勤打卡',
  289 + 'workFlow': '工作流',
  290 + 'aiVoice': 'AI语音',
  291 + 'member': '会员资料',
  292 + 'store': '门店资料'
  293 + }
  294 + return map[fileType] || fileType || '未知'
  295 + },
  296 + getFileTypeTagType(fileType) {
  297 + const map = {
  298 + 'annexpic': '',
  299 + 'attendancepic': 'success',
  300 + 'workFlow': 'warning',
  301 + 'aiVoice': 'info',
  302 + 'member': 'danger',
  303 + 'store': ''
  304 + }
  305 + return map[fileType] || 'info'
261 306 }
262 307 }
263 308 }
... ...
antis-ncc-admin/src/views/system/sysConfig/business-config-panel.vue
... ... @@ -70,6 +70,40 @@
70 70 />
71 71 </el-card>
72 72 </el-collapse-item>
  73 +
  74 + <el-collapse-item name="sleepRemind">
  75 + <template slot="title">
  76 + <span class="module-title">
  77 + <i class="el-icon-bell module-icon module-icon--sleep"></i>
  78 + 沉睡提醒
  79 + </span>
  80 + </template>
  81 +
  82 + <el-card shadow="never" class="config-section">
  83 + <div slot="header" class="config-section__header">
  84 + <span>门店PC端</span>
  85 + <span class="config-section__tag">会员管理</span>
  86 + </div>
  87 + <p class="config-section__tip">
  88 + 设置沉睡会员提醒天数,超过该天数未消费的会员将在门店PC端显示在沉睡提醒列表中。
  89 + </p>
  90 + <el-row :gutter="20">
  91 + <el-col :span="12">
  92 + <el-form-item label="沉睡天数" prop="sleepRemindDays" label-width="100px">
  93 + <el-input-number
  94 + v-model="form.sleepRemindDays"
  95 + :min="1"
  96 + :max="365"
  97 + :precision="0"
  98 + :step="1"
  99 + controls-position="right"
  100 + />
  101 + <span class="field-suffix">天</span>
  102 + </el-form-item>
  103 + </el-col>
  104 + </el-row>
  105 + </el-card>
  106 + </el-collapse-item>
73 107 </el-collapse>
74 108 </div>
75 109 </template>
... ... @@ -152,6 +186,10 @@ export default {
152 186 &--consume {
153 187 color: #409EFF;
154 188 }
  189 +
  190 + &--sleep {
  191 + color: #E6A23C;
  192 + }
155 193 }
156 194  
157 195 .config-section {
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/Changelog/ChangelogDto.cs 0 → 100644
  1 +namespace NCC.Extend.Entitys.Dto.Changelog
  2 +{
  3 + /// <summary>
  4 + /// 更新日志列表查询输入
  5 + /// </summary>
  6 + public class ChangelogListQueryInput
  7 + {
  8 + /// <summary>
  9 + /// 当前页码
  10 + /// </summary>
  11 + public int currentPage { get; set; } = 1;
  12 +
  13 + /// <summary>
  14 + /// 每页条数
  15 + /// </summary>
  16 + public int pageSize { get; set; } = 20;
  17 +
  18 + /// <summary>
  19 + /// 关键字搜索(标题、版本号)
  20 + /// </summary>
  21 + public string keyword { get; set; }
  22 +
  23 + /// <summary>
  24 + /// 是否发布
  25 + /// </summary>
  26 + public bool? isPublished { get; set; }
  27 +
  28 + /// <summary>
  29 + /// 更新类型
  30 + /// </summary>
  31 + public int? type { get; set; }
  32 + }
  33 +
  34 + /// <summary>
  35 + /// 更新日志列表输出
  36 + /// </summary>
  37 + public class ChangelogListOutput
  38 + {
  39 + /// <summary>
  40 + /// 主键 ID
  41 + /// </summary>
  42 + public string id { get; set; }
  43 +
  44 + /// <summary>
  45 + /// 版本号
  46 + /// </summary>
  47 + public string version { get; set; }
  48 +
  49 + /// <summary>
  50 + /// 更新标题
  51 + /// </summary>
  52 + public string title { get; set; }
  53 +
  54 + /// <summary>
  55 + /// 更新内容
  56 + /// </summary>
  57 + public string content { get; set; }
  58 +
  59 + /// <summary>
  60 + /// 更新类型(1=功能更新 2=Bug修复 3=性能优化 4=安全更新)
  61 + /// </summary>
  62 + public int type { get; set; }
  63 +
  64 + /// <summary>
  65 + /// 是否发布
  66 + /// </summary>
  67 + public bool isPublished { get; set; }
  68 +
  69 + /// <summary>
  70 + /// 发布时间
  71 + /// </summary>
  72 + public System.DateTime? publishedTime { get; set; }
  73 +
  74 + /// <summary>
  75 + /// 创建时间
  76 + /// </summary>
  77 + public System.DateTime createTime { get; set; }
  78 +
  79 + /// <summary>
  80 + /// 修改时间
  81 + /// </summary>
  82 + public System.DateTime? modifyTime { get; set; }
  83 + }
  84 +
  85 + /// <summary>
  86 + /// 更新日志添加输入
  87 + /// </summary>
  88 + public class ChangelogAddInput
  89 + {
  90 + /// <summary>
  91 + /// 版本号
  92 + /// </summary>
  93 + public string Version { get; set; }
  94 +
  95 + /// <summary>
  96 + /// 更新标题
  97 + /// </summary>
  98 + public string Title { get; set; }
  99 +
  100 + /// <summary>
  101 + /// 更新内容
  102 + /// </summary>
  103 + public string Content { get; set; }
  104 +
  105 + /// <summary>
  106 + /// 更新类型(1=功能更新 2=Bug修复 3=性能优化 4=安全更新)
  107 + /// </summary>
  108 + public int Type { get; set; }
  109 +
  110 + /// <summary>
  111 + /// 是否发布
  112 + /// </summary>
  113 + public bool IsPublished { get; set; }
  114 + }
  115 +
  116 + /// <summary>
  117 + /// 更新日志修改输入
  118 + /// </summary>
  119 + public class ChangelogUpdateInput
  120 + {
  121 + /// <summary>
  122 + /// 主键 ID
  123 + /// </summary>
  124 + public string Id { get; set; }
  125 +
  126 + /// <summary>
  127 + /// 版本号
  128 + /// </summary>
  129 + public string Version { get; set; }
  130 +
  131 + /// <summary>
  132 + /// 更新标题
  133 + /// </summary>
  134 + public string Title { get; set; }
  135 +
  136 + /// <summary>
  137 + /// 更新内容
  138 + /// </summary>
  139 + public string Content { get; set; }
  140 +
  141 + /// <summary>
  142 + /// 更新类型
  143 + /// </summary>
  144 + public int Type { get; set; }
  145 +
  146 + /// <summary>
  147 + /// 是否发布
  148 + /// </summary>
  149 + public bool IsPublished { get; set; }
  150 + }
  151 +}
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqCompanyCalendar/LqCompanyCalendarDto.cs 0 → 100644
  1 +using System;
  2 +using System.Collections.Generic;
  3 +
  4 +namespace NCC.Extend.Entitys.Dto.LqCompanyCalendar
  5 +{
  6 + /// <summary>
  7 + /// 公司日历查询输入
  8 + /// </summary>
  9 + public class LqCompanyCalendarQueryInput
  10 + {
  11 + /// <summary>
  12 + /// 开始时间
  13 + /// </summary>
  14 + public DateTime? StartTime { get; set; }
  15 +
  16 + /// <summary>
  17 + /// 结束时间
  18 + /// </summary>
  19 + public DateTime? EndTime { get; set; }
  20 +
  21 + /// <summary>
  22 + /// 事件类型
  23 + /// </summary>
  24 + public string EventType { get; set; }
  25 +
  26 + /// <summary>
  27 + /// 门店ID
  28 + /// </summary>
  29 + public string StoreId { get; set; }
  30 +
  31 + /// <summary>
  32 + /// 当前页码
  33 + /// </summary>
  34 + public int CurrentPage { get; set; } = 1;
  35 +
  36 + /// <summary>
  37 + /// 每页条数
  38 + /// </summary>
  39 + public int PageSize { get; set; } = 20;
  40 + }
  41 +
  42 + /// <summary>
  43 + /// 公司日历创建输入
  44 + /// </summary>
  45 + public class LqCompanyCalendarCreateInput
  46 + {
  47 + /// <summary>
  48 + /// 事件标题
  49 + /// </summary>
  50 + public string Title { get; set; }
  51 +
  52 + /// <summary>
  53 + /// 事件类型编码
  54 + /// </summary>
  55 + public string EventType { get; set; }
  56 +
  57 + /// <summary>
  58 + /// 开始时间
  59 + /// </summary>
  60 + public DateTime StartTime { get; set; }
  61 +
  62 + /// <summary>
  63 + /// 结束时间
  64 + /// </summary>
  65 + public DateTime EndTime { get; set; }
  66 +
  67 + /// <summary>
  68 + /// 事件描述
  69 + /// </summary>
  70 + public string Description { get; set; }
  71 +
  72 + /// <summary>
  73 + /// 门店ID
  74 + /// </summary>
  75 + public string StoreId { get; set; }
  76 +
  77 + /// <summary>
  78 + /// 门店名称
  79 + /// </summary>
  80 + public string StoreName { get; set; }
  81 +
  82 + /// <summary>
  83 + /// 备注
  84 + /// </summary>
  85 + public string Remark { get; set; }
  86 +
  87 + /// <summary>
  88 + /// 附件列表JSON
  89 + /// </summary>
  90 + public string Attachments { get; set; }
  91 + }
  92 +
  93 + /// <summary>
  94 + /// 公司日历更新输入
  95 + /// </summary>
  96 + public class LqCompanyCalendarUpdateInput
  97 + {
  98 + /// <summary>
  99 + /// 主键ID
  100 + /// </summary>
  101 + public string Id { get; set; }
  102 +
  103 + /// <summary>
  104 + /// 事件标题
  105 + /// </summary>
  106 + public string Title { get; set; }
  107 +
  108 + /// <summary>
  109 + /// 事件类型编码
  110 + /// </summary>
  111 + public string EventType { get; set; }
  112 +
  113 + /// <summary>
  114 + /// 开始时间
  115 + /// </summary>
  116 + public DateTime StartTime { get; set; }
  117 +
  118 + /// <summary>
  119 + /// 结束时间
  120 + /// </summary>
  121 + public DateTime EndTime { get; set; }
  122 +
  123 + /// <summary>
  124 + /// 事件描述
  125 + /// </summary>
  126 + public string Description { get; set; }
  127 +
  128 + /// <summary>
  129 + /// 门店ID
  130 + /// </summary>
  131 + public string StoreId { get; set; }
  132 +
  133 + /// <summary>
  134 + /// 门店名称
  135 + /// </summary>
  136 + public string StoreName { get; set; }
  137 +
  138 + /// <summary>
  139 + /// 备注
  140 + /// </summary>
  141 + public string Remark { get; set; }
  142 +
  143 + /// <summary>
  144 + /// 附件列表JSON
  145 + /// </summary>
  146 + public string Attachments { get; set; }
  147 + }
  148 +
  149 + /// <summary>
  150 + /// 附件项
  151 + /// </summary>
  152 + public class AttachmentItem
  153 + {
  154 + /// <summary>
  155 + /// 附件名称
  156 + /// </summary>
  157 + public string Name { get; set; }
  158 +
  159 + /// <summary>
  160 + /// 附件URL
  161 + /// </summary>
  162 + public string Url { get; set; }
  163 +
  164 + /// <summary>
  165 + /// 文件名
  166 + /// </summary>
  167 + public string FileName { get; set; }
  168 + }
  169 +
  170 + /// <summary>
  171 + /// 公司日历输出
  172 + /// </summary>
  173 + public class LqCompanyCalendarOutput
  174 + {
  175 + /// <summary>
  176 + /// 主键ID
  177 + /// </summary>
  178 + public string Id { get; set; }
  179 +
  180 + /// <summary>
  181 + /// 事件标题
  182 + /// </summary>
  183 + public string Title { get; set; }
  184 +
  185 + /// <summary>
  186 + /// 事件类型编码
  187 + /// </summary>
  188 + public string EventType { get; set; }
  189 +
  190 + /// <summary>
  191 + /// 事件类型名称
  192 + /// </summary>
  193 + public string EventTypeName { get; set; }
  194 +
  195 + /// <summary>
  196 + /// 事件类型颜色
  197 + /// </summary>
  198 + public string EventTypeColor { get; set; }
  199 +
  200 + /// <summary>
  201 + /// 日期字符串(YYYY-MM-DD)
  202 + /// </summary>
  203 + public string Date { get; set; }
  204 +
  205 + /// <summary>
  206 + /// 开始时间(ISO格式)
  207 + /// </summary>
  208 + public string Start { get; set; }
  209 +
  210 + /// <summary>
  211 + /// 结束时间(ISO格式)
  212 + /// </summary>
  213 + public string End { get; set; }
  214 +
  215 + /// <summary>
  216 + /// 事件描述
  217 + /// </summary>
  218 + public string Desc { get; set; }
  219 +
  220 + /// <summary>
  221 + /// 门店ID
  222 + /// </summary>
  223 + public string StoreId { get; set; }
  224 +
  225 + /// <summary>
  226 + /// 门店名称
  227 + /// </summary>
  228 + public string Store { get; set; }
  229 +
  230 + /// <summary>
  231 + /// 备注
  232 + /// </summary>
  233 + public string Remark { get; set; }
  234 +
  235 + /// <summary>
  236 + /// 附件JSON
  237 + /// </summary>
  238 + public string Attachments { get; set; }
  239 +
  240 + /// <summary>
  241 + /// 创建人
  242 + /// </summary>
  243 + public string CreateUser { get; set; }
  244 +
  245 + /// <summary>
  246 + /// 创建时间
  247 + /// </summary>
  248 + public DateTime CreateTime { get; set; }
  249 + }
  250 +
  251 + /// <summary>
  252 + /// 公司日历分页输出
  253 + /// </summary>
  254 + public class LqCompanyCalendarPageOutput
  255 + {
  256 + /// <summary>
  257 + /// 列表
  258 + /// </summary>
  259 + public List<LqCompanyCalendarOutput> List { get; set; }
  260 +
  261 + /// <summary>
  262 + /// 总数
  263 + /// </summary>
  264 + public int Total { get; set; }
  265 + }
  266 +
  267 + /// <summary>
  268 + /// 事件类型查询输入
  269 + /// </summary>
  270 + public class LqCompanyCalendarTypeQueryInput
  271 + {
  272 + /// <summary>
  273 + /// 类型名称
  274 + /// </summary>
  275 + public string TypeName { get; set; }
  276 + }
  277 +
  278 + /// <summary>
  279 + /// 事件类型创建输入
  280 + /// </summary>
  281 + public class LqCompanyCalendarTypeCreateInput
  282 + {
  283 + /// <summary>
  284 + /// 类型名称
  285 + /// </summary>
  286 + public string TypeName { get; set; }
  287 +
  288 + /// <summary>
  289 + /// 类型编码
  290 + /// </summary>
  291 + public string TypeCode { get; set; }
  292 +
  293 + /// <summary>
  294 + /// 颜色
  295 + /// </summary>
  296 + public string Color { get; set; }
  297 +
  298 + /// <summary>
  299 + /// 排序码
  300 + /// </summary>
  301 + public int SortCode { get; set; }
  302 + }
  303 +
  304 + /// <summary>
  305 + /// 事件类型更新输入
  306 + /// </summary>
  307 + public class LqCompanyCalendarTypeUpdateInput
  308 + {
  309 + /// <summary>
  310 + /// 主键ID
  311 + /// </summary>
  312 + public string Id { get; set; }
  313 +
  314 + /// <summary>
  315 + /// 类型名称
  316 + /// </summary>
  317 + public string TypeName { get; set; }
  318 +
  319 + /// <summary>
  320 + /// 类型编码
  321 + /// </summary>
  322 + public string TypeCode { get; set; }
  323 +
  324 + /// <summary>
  325 + /// 颜色
  326 + /// </summary>
  327 + public string Color { get; set; }
  328 +
  329 + /// <summary>
  330 + /// 排序码
  331 + /// </summary>
  332 + public int SortCode { get; set; }
  333 + }
  334 +
  335 + /// <summary>
  336 + /// 事件类型输出
  337 + /// </summary>
  338 + public class LqCompanyCalendarTypeOutput
  339 + {
  340 + /// <summary>
  341 + /// 主键ID
  342 + /// </summary>
  343 + public string Id { get; set; }
  344 +
  345 + /// <summary>
  346 + /// 类型名称
  347 + /// </summary>
  348 + public string TypeName { get; set; }
  349 +
  350 + /// <summary>
  351 + /// 类型编码
  352 + /// </summary>
  353 + public string TypeCode { get; set; }
  354 +
  355 + /// <summary>
  356 + /// 颜色
  357 + /// </summary>
  358 + public string Color { get; set; }
  359 +
  360 + /// <summary>
  361 + /// 排序码
  362 + /// </summary>
  363 + public int SortCode { get; set; }
  364 +
  365 + /// <summary>
  366 + /// 创建时间
  367 + /// </summary>
  368 + public DateTime CreateTime { get; set; }
  369 + }
  370 +}
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqStoreDashboard/SleepingMembersInput.cs 0 → 100644
  1 +using System.ComponentModel.DataAnnotations;
  2 +
  3 +namespace NCC.Extend.Entitys.Dto.LqStoreDashboard
  4 +{
  5 + /// <summary>
  6 + /// 沉睡会员查询输入
  7 + /// </summary>
  8 + public class SleepingMembersInput
  9 + {
  10 + /// <summary>
  11 + /// 门店ID(必填)
  12 + /// </summary>
  13 + [Required(ErrorMessage = "门店ID不能为空")]
  14 + public string StoreId { get; set; }
  15 +
  16 + /// <summary>
  17 + /// 页码(从1开始,默认1)
  18 + /// </summary>
  19 + public int Page { get; set; } = 1;
  20 +
  21 + /// <summary>
  22 + /// 每页数量(默认20)
  23 + /// </summary>
  24 + public int PageSize { get; set; } = 20;
  25 + }
  26 +}
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_changelog/LqChangelogEntity.cs 0 → 100644
  1 +using SqlSugar;
  2 +using System;
  3 +
  4 +namespace NCC.Extend.Entitys.lq_changelog
  5 +{
  6 + /// <summary>
  7 + /// 系统更新日志
  8 + /// </summary>
  9 + [SugarTable("lq_changelog")]
  10 + public class LqChangelogEntity
  11 + {
  12 + /// <summary>
  13 + /// 主键 ID
  14 + /// </summary>
  15 + [SugarColumn(ColumnName = "F_Id", IsPrimaryKey = true)]
  16 + public string Id { get; set; }
  17 +
  18 + /// <summary>
  19 + /// 版本号(如 3.2.1)
  20 + /// </summary>
  21 + [SugarColumn(ColumnName = "F_Version", ColumnDataType = "varchar(50)")]
  22 + public string Version { get; set; }
  23 +
  24 + /// <summary>
  25 + /// 更新标题
  26 + /// </summary>
  27 + [SugarColumn(ColumnName = "F_Title", ColumnDataType = "varchar(200)")]
  28 + public string Title { get; set; }
  29 +
  30 + /// <summary>
  31 + /// 更新内容(支持富文本)
  32 + /// </summary>
  33 + [SugarColumn(ColumnName = "F_Content", ColumnDataType = "text")]
  34 + public string Content { get; set; }
  35 +
  36 + /// <summary>
  37 + /// 更新类型(1=功能更新 2=Bug修复 3=性能优化 4=安全更新)
  38 + /// </summary>
  39 + [SugarColumn(ColumnName = "F_Type")]
  40 + public int Type { get; set; }
  41 +
  42 + /// <summary>
  43 + /// 是否发布(true=发布 false=草稿)
  44 + /// </summary>
  45 + [SugarColumn(ColumnName = "F_IsPublished")]
  46 + public bool IsPublished { get; set; }
  47 +
  48 + /// <summary>
  49 + /// 发布时间
  50 + /// </summary>
  51 + [SugarColumn(ColumnName = "F_PublishedTime", IsNullable = true)]
  52 + public DateTime? PublishedTime { get; set; }
  53 +
  54 + /// <summary>
  55 + /// 创建人 ID
  56 + /// </summary>
  57 + [SugarColumn(ColumnName = "F_CreateUserId", ColumnDataType = "varchar(50)", IsNullable = true)]
  58 + public string CreateUserId { get; set; }
  59 +
  60 + /// <summary>
  61 + /// 创建时间
  62 + /// </summary>
  63 + [SugarColumn(ColumnName = "F_CreateTime")]
  64 + public DateTime CreateTime { get; set; }
  65 +
  66 + /// <summary>
  67 + /// 修改时间
  68 + /// </summary>
  69 + [SugarColumn(ColumnName = "F_ModifyTime", IsNullable = true)]
  70 + public DateTime? ModifyTime { get; set; }
  71 + }
  72 +}
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/lq_company_calendar/LqCompanyCalendarEntity.cs 0 → 100644
  1 +using System;
  2 +using SqlSugar;
  3 +
  4 +namespace NCC.Extend.Entitys.lq_company_calendar
  5 +{
  6 + /// <summary>
  7 + /// 公司日历事件表
  8 + /// </summary>
  9 + [SugarTable("lq_company_calendar")]
  10 + public class LqCompanyCalendarEntity
  11 + {
  12 + /// <summary>
  13 + /// 主键ID
  14 + /// </summary>
  15 + [SugarColumn(IsPrimaryKey = true, ColumnName = "F_Id")]
  16 + public string Id { get; set; }
  17 +
  18 + /// <summary>
  19 + /// 事件标题
  20 + /// </summary>
  21 + [SugarColumn(ColumnName = "Title")]
  22 + public string Title { get; set; }
  23 +
  24 + /// <summary>
  25 + /// 事件类型ID
  26 + /// </summary>
  27 + [SugarColumn(ColumnName = "EventType")]
  28 + public string EventType { get; set; }
  29 +
  30 + /// <summary>
  31 + /// 开始时间
  32 + /// </summary>
  33 + [SugarColumn(ColumnName = "StartTime")]
  34 + public DateTime StartTime { get; set; }
  35 +
  36 + /// <summary>
  37 + /// 结束时间
  38 + /// </summary>
  39 + [SugarColumn(ColumnName = "EndTime")]
  40 + public DateTime EndTime { get; set; }
  41 +
  42 + /// <summary>
  43 + /// 事件描述
  44 + /// </summary>
  45 + [SugarColumn(ColumnName = "Description", ColumnDataType = "text", IsNullable = true)]
  46 + public string Description { get; set; }
  47 +
  48 + /// <summary>
  49 + /// 门店ID
  50 + /// </summary>
  51 + [SugarColumn(ColumnName = "StoreId", IsNullable = true)]
  52 + public string StoreId { get; set; }
  53 +
  54 + /// <summary>
  55 + /// 门店名称
  56 + /// </summary>
  57 + [SugarColumn(ColumnName = "StoreName", IsNullable = true)]
  58 + public string StoreName { get; set; }
  59 +
  60 + /// <summary>
  61 + /// 备注
  62 + /// </summary>
  63 + [SugarColumn(ColumnName = "Remark", ColumnDataType = "text", IsNullable = true)]
  64 + public string Remark { get; set; }
  65 +
  66 + /// <summary>
  67 + /// 附件JSON
  68 + /// </summary>
  69 + [SugarColumn(ColumnName = "Attachments", ColumnDataType = "text", IsNullable = true)]
  70 + public string Attachments { get; set; }
  71 +
  72 + /// <summary>
  73 + /// 是否有效:1-有效, 0-无效
  74 + /// </summary>
  75 + [SugarColumn(ColumnName = "IsEffective")]
  76 + public int IsEffective { get; set; } = 1;
  77 +
  78 + /// <summary>
  79 + /// 创建人
  80 + /// </summary>
  81 + [SugarColumn(ColumnName = "CreateUser", IsNullable = true)]
  82 + public string CreateUser { get; set; }
  83 +
  84 + /// <summary>
  85 + /// 创建时间
  86 + /// </summary>
  87 + [SugarColumn(ColumnName = "CreateTime")]
  88 + public DateTime CreateTime { get; set; }
  89 +
  90 + /// <summary>
  91 + /// 修改人
  92 + /// </summary>
  93 + [SugarColumn(ColumnName = "UpdateUser", IsNullable = true)]
  94 + public string UpdateUser { get; set; }
  95 +
  96 + /// <summary>
  97 + /// 修改时间
  98 + /// </summary>
  99 + [SugarColumn(ColumnName = "UpdateTime", IsNullable = true)]
  100 + public DateTime? UpdateTime { get; set; }
  101 + }
  102 +}
... ...
netcore/src/Modularity/Extend/NCC.Extend.Entitys/lq_company_calendar/LqCompanyCalendarTypeEntity.cs 0 → 100644
  1 +using System;
  2 +using SqlSugar;
  3 +
  4 +namespace NCC.Extend.Entitys.lq_company_calendar
  5 +{
  6 + /// <summary>
  7 + /// 公司日历事件类型配置表
  8 + /// </summary>
  9 + [SugarTable("lq_company_calendar_type")]
  10 + public class LqCompanyCalendarTypeEntity
  11 + {
  12 + /// <summary>
  13 + /// 主键ID
  14 + /// </summary>
  15 + [SugarColumn(IsPrimaryKey = true, ColumnName = "F_Id")]
  16 + public string Id { get; set; }
  17 +
  18 + /// <summary>
  19 + /// 类型名称
  20 + /// </summary>
  21 + [SugarColumn(ColumnName = "TypeName")]
  22 + public string TypeName { get; set; }
  23 +
  24 + /// <summary>
  25 + /// 类型编码
  26 + /// </summary>
  27 + [SugarColumn(ColumnName = "TypeCode")]
  28 + public string TypeCode { get; set; }
  29 +
  30 + /// <summary>
  31 + /// 颜色
  32 + /// </summary>
  33 + [SugarColumn(ColumnName = "Color")]
  34 + public string Color { get; set; }
  35 +
  36 + /// <summary>
  37 + /// 排序码
  38 + /// </summary>
  39 + [SugarColumn(ColumnName = "SortCode")]
  40 + public int SortCode { get; set; }
  41 +
  42 + /// <summary>
  43 + /// 是否有效:1-有效, 0-无效
  44 + /// </summary>
  45 + [SugarColumn(ColumnName = "IsEffective")]
  46 + public int IsEffective { get; set; } = 1;
  47 +
  48 + /// <summary>
  49 + /// 创建人
  50 + /// </summary>
  51 + [SugarColumn(ColumnName = "CreateUser", IsNullable = true)]
  52 + public string CreateUser { get; set; }
  53 +
  54 + /// <summary>
  55 + /// 创建时间
  56 + /// </summary>
  57 + [SugarColumn(ColumnName = "CreateTime")]
  58 + public DateTime CreateTime { get; set; }
  59 +
  60 + /// <summary>
  61 + /// 修改人
  62 + /// </summary>
  63 + [SugarColumn(ColumnName = "UpdateUser", IsNullable = true)]
  64 + public string UpdateUser { get; set; }
  65 +
  66 + /// <summary>
  67 + /// 修改时间
  68 + /// </summary>
  69 + [SugarColumn(ColumnName = "UpdateTime", IsNullable = true)]
  70 + public DateTime? UpdateTime { get; set; }
  71 + }
  72 +}
... ...
netcore/src/Modularity/Extend/NCC.Extend.Interfaces/LqCompanyCalendar/ILqCompanyCalendarService.cs 0 → 100644
  1 +using System.Collections.Generic;
  2 +using System.Threading.Tasks;
  3 +using NCC.Extend.Entitys.Dto.LqCompanyCalendar;
  4 +
  5 +namespace NCC.Extend.Interfaces.LqCompanyCalendar
  6 +{
  7 + /// <summary>
  8 + /// 公司日历服务接口
  9 + /// </summary>
  10 + public interface ILqCompanyCalendarService
  11 + {
  12 + #region 事件类型
  13 +
  14 + /// <summary>
  15 + /// 获取事件类型列表
  16 + /// </summary>
  17 + Task<List<LqCompanyCalendarTypeOutput>> GetEventTypeListAsync(LqCompanyCalendarTypeQueryInput input);
  18 +
  19 + /// <summary>
  20 + /// 创建事件类型
  21 + /// </summary>
  22 + Task<string> CreateEventTypeAsync(LqCompanyCalendarTypeCreateInput input);
  23 +
  24 + /// <summary>
  25 + /// 更新事件类型
  26 + /// </summary>
  27 + Task<bool> UpdateEventTypeAsync(LqCompanyCalendarTypeUpdateInput input);
  28 +
  29 + /// <summary>
  30 + /// 删除事件类型
  31 + /// </summary>
  32 + Task<bool> DeleteEventTypeAsync(string id);
  33 +
  34 + #endregion
  35 +
  36 + #region 事件
  37 +
  38 + /// <summary>
  39 + /// 获取公司日历事件列表(分页)
  40 + /// </summary>
  41 + Task<LqCompanyCalendarPageOutput> GetCalendarEventsAsync(LqCompanyCalendarQueryInput input);
  42 +
  43 + /// <summary>
  44 + /// 获取公司日历事件列表(不分页,用于日历展示)
  45 + /// </summary>
  46 + Task<List<LqCompanyCalendarOutput>> GetCalendarEventListAsync(LqCompanyCalendarQueryInput input);
  47 +
  48 + /// <summary>
  49 + /// 创建公司日历事件
  50 + /// </summary>
  51 + Task<string> CreateCalendarEventAsync(LqCompanyCalendarCreateInput input);
  52 +
  53 + /// <summary>
  54 + /// 更新公司日历事件
  55 + /// </summary>
  56 + Task<bool> UpdateCalendarEventAsync(LqCompanyCalendarUpdateInput input);
  57 +
  58 + /// <summary>
  59 + /// 删除公司日历事件
  60 + /// </summary>
  61 + Task<bool> DeleteCalendarEventAsync(string id);
  62 +
  63 + #endregion
  64 + }
  65 +}
... ...
netcore/src/Modularity/Extend/NCC.Extend/ChangelogService.cs 0 → 100644
  1 +using System;
  2 +using System.Collections.Generic;
  3 +using System.Linq;
  4 +using System.Threading.Tasks;
  5 +using Microsoft.AspNetCore.Mvc;
  6 +using NCC.Common.Core.Manager;
  7 +using NCC.Common.Filter;
  8 +using NCC.DynamicApiController;
  9 +using NCC.Extend.Entitys.Dto.Changelog;
  10 +using NCC.Extend.Entitys.lq_changelog;
  11 +using NCC.Dependency;
  12 +using SqlSugar;
  13 +using Yitter.IdGenerator;
  14 +
  15 +namespace NCC.Extend
  16 +{
  17 + /// <summary>
  18 + /// 系统更新日志服务
  19 + /// </summary>
  20 + [ApiDescriptionSettings(Tag = "Common", Name = "Changelog", Order = 170)]
  21 + [Route("api/Extend/[controller]")]
  22 + public class ChangelogService : IDynamicApiController, ITransient
  23 + {
  24 + private readonly ISqlSugarClient _db;
  25 + private readonly IUserManager _userManager;
  26 +
  27 + public ChangelogService(ISqlSugarClient db, IUserManager userManager)
  28 + {
  29 + _db = db;
  30 + _userManager = userManager;
  31 + _db.CodeFirst.InitTables(typeof(LqChangelogEntity));
  32 + }
  33 +
  34 + /// <summary>
  35 + /// 获取更新日志列表(分页)
  36 + /// </summary>
  37 + [HttpGet("GetList")]
  38 + public async Task<dynamic> GetList([FromQuery] ChangelogListQueryInput input)
  39 + {
  40 + var data = await _db.Queryable<LqChangelogEntity>()
  41 + .WhereIF(input.isPublished.HasValue, x => x.IsPublished == input.isPublished.Value)
  42 + .WhereIF(input.type.HasValue, x => x.Type == input.type.Value)
  43 + .WhereIF(!string.IsNullOrWhiteSpace(input.keyword),
  44 + x => x.Title.Contains(input.keyword) || x.Version.Contains(input.keyword))
  45 + .OrderBy(x => x.PublishedTime, OrderByType.Desc)
  46 + .OrderBy(x => x.CreateTime, OrderByType.Desc)
  47 + .Select(x => new ChangelogListOutput
  48 + {
  49 + id = x.Id,
  50 + version = x.Version,
  51 + title = x.Title,
  52 + content = x.Content,
  53 + type = x.Type,
  54 + isPublished = x.IsPublished,
  55 + publishedTime = x.PublishedTime,
  56 + createTime = x.CreateTime,
  57 + modifyTime = x.ModifyTime
  58 + })
  59 + .ToPagedListAsync(input.currentPage, input.pageSize);
  60 +
  61 + return PageResult<ChangelogListOutput>.SqlSugarPageResult(data);
  62 + }
  63 +
  64 + /// <summary>
  65 + /// 获取已发布的更新日志列表(前端用)
  66 + /// </summary>
  67 + [HttpGet("GetPublishedList")]
  68 + public async Task<dynamic> GetPublishedList([FromQuery] int currentPage = 1, int pageSize = 20)
  69 + {
  70 + var data = await _db.Queryable<LqChangelogEntity>()
  71 + .Where(x => x.IsPublished)
  72 + .OrderBy(x => x.PublishedTime, OrderByType.Desc)
  73 + .Select(x => new ChangelogListOutput
  74 + {
  75 + id = x.Id,
  76 + version = x.Version,
  77 + title = x.Title,
  78 + content = x.Content,
  79 + type = x.Type,
  80 + isPublished = x.IsPublished,
  81 + publishedTime = x.PublishedTime,
  82 + createTime = x.CreateTime,
  83 + modifyTime = x.ModifyTime
  84 + })
  85 + .ToPagedListAsync(currentPage, pageSize);
  86 +
  87 + return PageResult<ChangelogListOutput>.SqlSugarPageResult(data);
  88 + }
  89 +
  90 + /// <summary>
  91 + /// 获取最新一条已发布更新日志(右上角红点提示用)
  92 + /// </summary>
  93 + [HttpGet("GetLatest")]
  94 + public async Task<dynamic> GetLatest()
  95 + {
  96 + var latest = await _db.Queryable<LqChangelogEntity>()
  97 + .Where(x => x.IsPublished)
  98 + .OrderBy(x => x.PublishedTime, OrderByType.Desc)
  99 + .FirstAsync();
  100 +
  101 + return latest;
  102 + }
  103 +
  104 + /// <summary>
  105 + /// 获取更新日志详情
  106 + /// </summary>
  107 + [HttpGet("GetDetail/{id}")]
  108 + public async Task<dynamic> GetDetail(string id)
  109 + {
  110 + var entity = await _db.Queryable<LqChangelogEntity>()
  111 + .Where(x => x.Id == id)
  112 + .FirstAsync();
  113 +
  114 + if (entity == null)
  115 + throw new Exception("更新日志不存在");
  116 +
  117 + return entity;
  118 + }
  119 +
  120 + /// <summary>
  121 + /// 添加更新日志
  122 + /// </summary>
  123 + [HttpPost("Add")]
  124 + public async Task<dynamic> Add([FromBody] ChangelogAddInput input)
  125 + {
  126 + var entity = new LqChangelogEntity
  127 + {
  128 + Id = YitIdHelper.NextId().ToString(),
  129 + Version = input.Version,
  130 + Title = input.Title,
  131 + Content = input.Content,
  132 + Type = input.Type,
  133 + IsPublished = input.IsPublished,
  134 + PublishedTime = input.IsPublished ? DateTime.Now : null,
  135 + CreateUserId = _userManager?.UserId,
  136 + CreateTime = DateTime.Now
  137 + };
  138 +
  139 + await _db.Insertable(entity).ExecuteCommandAsync();
  140 +
  141 + return new { success = true, id = entity.Id };
  142 + }
  143 +
  144 + /// <summary>
  145 + /// 修改更新日志
  146 + /// </summary>
  147 + [HttpPost("Update")]
  148 + public async Task<dynamic> Update([FromBody] ChangelogUpdateInput input)
  149 + {
  150 + var entity = await _db.Queryable<LqChangelogEntity>()
  151 + .Where(x => x.Id == input.Id)
  152 + .FirstAsync();
  153 +
  154 + if (entity == null)
  155 + throw new Exception("更新日志不存在");
  156 +
  157 + entity.Version = input.Version ?? entity.Version;
  158 + entity.Title = input.Title ?? entity.Title;
  159 + entity.Content = input.Content ?? entity.Content;
  160 + entity.Type = input.Type;
  161 + entity.ModifyTime = DateTime.Now;
  162 +
  163 + if (input.IsPublished && !entity.IsPublished)
  164 + {
  165 + entity.IsPublished = true;
  166 + entity.PublishedTime = DateTime.Now;
  167 + }
  168 + else if (!input.IsPublished)
  169 + {
  170 + entity.IsPublished = false;
  171 + entity.PublishedTime = null;
  172 + }
  173 +
  174 + await _db.Updateable(entity).ExecuteCommandAsync();
  175 +
  176 + return new { success = true };
  177 + }
  178 +
  179 + /// <summary>
  180 + /// 删除更新日志
  181 + /// </summary>
  182 + [HttpPost("Delete/{id}")]
  183 + public async Task<dynamic> Delete(string id)
  184 + {
  185 + await _db.Deleteable<LqChangelogEntity>(id).ExecuteCommandAsync();
  186 +
  187 + return new { success = true };
  188 + }
  189 +
  190 + /// <summary>
  191 + /// 切换发布状态
  192 + /// </summary>
  193 + [HttpPost("TogglePublish/{id}")]
  194 + public async Task<dynamic> TogglePublish(string id)
  195 + {
  196 + var entity = await _db.Queryable<LqChangelogEntity>()
  197 + .Where(x => x.Id == id)
  198 + .FirstAsync();
  199 +
  200 + if (entity == null)
  201 + throw new Exception("更新日志不存在");
  202 +
  203 + entity.IsPublished = !entity.IsPublished;
  204 + entity.PublishedTime = entity.IsPublished ? DateTime.Now : null;
  205 + entity.ModifyTime = DateTime.Now;
  206 +
  207 + await _db.Updateable(entity)
  208 + .UpdateColumns(x => new { x.IsPublished, x.PublishedTime, x.ModifyTime })
  209 + .ExecuteCommandAsync();
  210 +
  211 + return new { success = true, isPublished = entity.IsPublished };
  212 + }
  213 + }
  214 +}
... ...
netcore/src/Modularity/Extend/NCC.Extend/Helpers/ConsumeEditableDaysHelper.cs
... ... @@ -33,6 +33,11 @@ namespace NCC.Extend.Helpers
33 33 public const string KeyAdminConsumeDeleteEditableDays = "adminConsumeDeleteEditableDays";
34 34  
35 35 /// <summary>
  36 + /// 配置键:沉睡提醒天数(门店PC),超过该天数未消费的会员将显示在沉睡提醒列表中
  37 + /// </summary>
  38 + public const string KeySleepRemindDays = "sleepRemindDays";
  39 +
  40 + /// <summary>
36 41 /// 已废弃:合并配置键(读取天数时兼容)
37 42 /// </summary>
38 43 public const string KeyAdminConsumeCancelEditableDaysLegacy = "adminConsumeCancelEditableDays";
... ... @@ -110,6 +115,14 @@ namespace NCC.Extend.Helpers
110 115 DefaultAdminOperationEditableDays);
111 116 }
112 117  
  118 + /// <summary>
  119 + /// 沉睡提醒天数;默认 30
  120 + /// </summary>
  121 + public static async Task<int> GetSleepRemindDaysAsync(ISqlSugarClient db)
  122 + {
  123 + return await GetIntConfigAsync(db, KeySleepRemindDays, 30);
  124 + }
  125 +
113 126 private static void EnsureWithinEditableWindow(LqXhHyhkEntity card, int days, string messageTemplate)
114 127 {
115 128 var baseTime = card.Hksj ?? card.CreateTime;
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqCompanyCalendarService.cs 0 → 100644
  1 +using System;
  2 +using System.Collections.Generic;
  3 +using System.Linq;
  4 +using System.Threading.Tasks;
  5 +using Microsoft.AspNetCore.Mvc;
  6 +using Microsoft.Extensions.Logging;
  7 +using NCC.Common.Core.Manager;
  8 +using NCC.Common.Enum;
  9 +using NCC.Dependency;
  10 +using NCC.DynamicApiController;
  11 +using NCC.Extend.Entitys.Dto.LqCompanyCalendar;
  12 +using NCC.Extend.Entitys.Enum;
  13 +using NCC.Extend.Entitys.lq_company_calendar;
  14 +using NCC.Extend.Interfaces.LqCompanyCalendar;
  15 +using NCC.FriendlyException;
  16 +using SqlSugar;
  17 +using Yitter.IdGenerator;
  18 +
  19 +namespace NCC.Extend
  20 +{
  21 + /// <summary>
  22 + /// 公司日历服务
  23 + /// </summary>
  24 + [ApiDescriptionSettings(Tag = "绿纤公司日历管理", Name = "LqCompanyCalendar", Order = 200)]
  25 + [Route("api/Extend/LqCompanyCalendar")]
  26 + public class LqCompanyCalendarService : IDynamicApiController, ITransient, ILqCompanyCalendarService
  27 + {
  28 + private readonly IUserManager _userManager;
  29 + private readonly ILogger<LqCompanyCalendarService> _logger;
  30 + private readonly ISqlSugarClient _db;
  31 +
  32 + /// <summary>
  33 + /// 构造函数
  34 + /// </summary>
  35 + public LqCompanyCalendarService(IUserManager userManager, ILogger<LqCompanyCalendarService> logger, ISqlSugarClient db)
  36 + {
  37 + _userManager = userManager;
  38 + _logger = logger;
  39 + _db = db;
  40 + }
  41 +
  42 + #region 事件类型管理
  43 +
  44 + /// <summary>
  45 + /// 获取事件类型列表
  46 + /// </summary>
  47 + [HttpPost("GetEventTypeList")]
  48 + public async Task<List<LqCompanyCalendarTypeOutput>> GetEventTypeListAsync([FromBody] LqCompanyCalendarTypeQueryInput input)
  49 + {
  50 + try
  51 + {
  52 + var query = _db.Queryable<LqCompanyCalendarTypeEntity>()
  53 + .Where(x => x.IsEffective == StatusEnum.有效.GetHashCode());
  54 +
  55 + if (!string.IsNullOrWhiteSpace(input?.TypeName))
  56 + {
  57 + query = query.Where(x => x.TypeName.Contains(input.TypeName));
  58 + }
  59 +
  60 + var list = await query.OrderBy(x => x.SortCode).ToListAsync();
  61 +
  62 + return list.Select(x => new LqCompanyCalendarTypeOutput
  63 + {
  64 + Id = x.Id,
  65 + TypeName = x.TypeName,
  66 + TypeCode = x.TypeCode,
  67 + Color = x.Color,
  68 + SortCode = x.SortCode,
  69 + CreateTime = x.CreateTime
  70 + }).ToList();
  71 + }
  72 + catch (Exception ex)
  73 + {
  74 + _logger.LogError(ex, "获取事件类型列表失败");
  75 + throw NCCException.Oh($"获取失败:{ex.Message}");
  76 + }
  77 + }
  78 +
  79 + /// <summary>
  80 + /// 创建事件类型
  81 + /// </summary>
  82 + [HttpPost("CreateEventType")]
  83 + public async Task<string> CreateEventTypeAsync([FromBody] LqCompanyCalendarTypeCreateInput input)
  84 + {
  85 + try
  86 + {
  87 + var exists = await _db.Queryable<LqCompanyCalendarTypeEntity>()
  88 + .Where(x => x.TypeCode == input.TypeCode && x.IsEffective == StatusEnum.有效.GetHashCode())
  89 + .AnyAsync();
  90 +
  91 + if (exists)
  92 + {
  93 + throw NCCException.Oh("类型编码已存在");
  94 + }
  95 +
  96 + var id = YitIdHelper.NextId().ToString();
  97 + var entity = new LqCompanyCalendarTypeEntity
  98 + {
  99 + Id = id,
  100 + TypeName = input.TypeName,
  101 + TypeCode = input.TypeCode,
  102 + Color = input.Color,
  103 + SortCode = input.SortCode,
  104 + IsEffective = StatusEnum.有效.GetHashCode(),
  105 + CreateUser = _userManager.UserId,
  106 + CreateTime = DateTime.Now
  107 + };
  108 +
  109 + await _db.Insertable(entity).ExecuteCommandAsync();
  110 +
  111 + return id;
  112 + }
  113 + catch (Exception ex)
  114 + {
  115 + _logger.LogError(ex, "创建事件类型失败");
  116 + throw NCCException.Oh($"创建失败:{ex.Message}");
  117 + }
  118 + }
  119 +
  120 + /// <summary>
  121 + /// 更新事件类型
  122 + /// </summary>
  123 + [HttpPost("UpdateEventType")]
  124 + public async Task<bool> UpdateEventTypeAsync([FromBody] LqCompanyCalendarTypeUpdateInput input)
  125 + {
  126 + try
  127 + {
  128 + var entity = await _db.Queryable<LqCompanyCalendarTypeEntity>()
  129 + .Where(x => x.Id == input.Id && x.IsEffective == StatusEnum.有效.GetHashCode())
  130 + .FirstAsync();
  131 +
  132 + if (entity == null)
  133 + {
  134 + throw NCCException.Oh("事件类型不存在或已失效");
  135 + }
  136 +
  137 + var codeExists = await _db.Queryable<LqCompanyCalendarTypeEntity>()
  138 + .Where(x => x.TypeCode == input.TypeCode && x.Id != input.Id && x.IsEffective == StatusEnum.有效.GetHashCode())
  139 + .AnyAsync();
  140 +
  141 + if (codeExists)
  142 + {
  143 + throw NCCException.Oh("类型编码已存在");
  144 + }
  145 +
  146 + entity.TypeName = input.TypeName;
  147 + entity.TypeCode = input.TypeCode;
  148 + entity.Color = input.Color;
  149 + entity.SortCode = input.SortCode;
  150 + entity.UpdateUser = _userManager.UserId;
  151 + entity.UpdateTime = DateTime.Now;
  152 +
  153 + await _db.Updateable(entity).ExecuteCommandAsync();
  154 +
  155 + return true;
  156 + }
  157 + catch (Exception ex)
  158 + {
  159 + _logger.LogError(ex, "更新事件类型失败");
  160 + throw NCCException.Oh($"更新失败:{ex.Message}");
  161 + }
  162 + }
  163 +
  164 + /// <summary>
  165 + /// 删除事件类型
  166 + /// </summary>
  167 + [HttpPost("DeleteEventType/{id}")]
  168 + public async Task<bool> DeleteEventTypeAsync(string id)
  169 + {
  170 + try
  171 + {
  172 + var hasEvents = await _db.Queryable<LqCompanyCalendarEntity>()
  173 + .Where(x => x.EventType == id && x.IsEffective == StatusEnum.有效.GetHashCode())
  174 + .AnyAsync();
  175 +
  176 + if (hasEvents)
  177 + {
  178 + throw NCCException.Oh("该类型下存在事件,无法删除");
  179 + }
  180 +
  181 + var entity = await _db.Queryable<LqCompanyCalendarTypeEntity>()
  182 + .Where(x => x.Id == id && x.IsEffective == StatusEnum.有效.GetHashCode())
  183 + .FirstAsync();
  184 +
  185 + if (entity == null)
  186 + {
  187 + throw NCCException.Oh("事件类型不存在或已失效");
  188 + }
  189 +
  190 + entity.IsEffective = StatusEnum.无效.GetHashCode();
  191 + entity.UpdateUser = _userManager.UserId;
  192 + entity.UpdateTime = DateTime.Now;
  193 +
  194 + await _db.Updateable(entity).ExecuteCommandAsync();
  195 +
  196 + return true;
  197 + }
  198 + catch (Exception ex)
  199 + {
  200 + _logger.LogError(ex, "删除事件类型失败");
  201 + throw NCCException.Oh($"删除失败:{ex.Message}");
  202 + }
  203 + }
  204 +
  205 + #endregion
  206 +
  207 + #region 事件管理
  208 +
  209 + /// <summary>
  210 + /// 获取公司日历事件列表(分页)
  211 + /// </summary>
  212 + [HttpPost("GetCalendarEvents")]
  213 + public async Task<LqCompanyCalendarPageOutput> GetCalendarEventsAsync([FromBody] LqCompanyCalendarQueryInput input)
  214 + {
  215 + try
  216 + {
  217 + var page = input.CurrentPage < 1 ? 1 : input.CurrentPage;
  218 + var pageSize = input.PageSize < 1 ? 20 : (input.PageSize > 100 ? 100 : input.PageSize);
  219 +
  220 + var query = _db.Queryable<LqCompanyCalendarEntity>()
  221 + .Where(x => x.IsEffective == StatusEnum.有效.GetHashCode());
  222 +
  223 + if (input.StartTime.HasValue)
  224 + {
  225 + query = query.Where(x => x.EndTime >= input.StartTime.Value);
  226 + }
  227 +
  228 + if (input.EndTime.HasValue)
  229 + {
  230 + query = query.Where(x => x.StartTime <= input.EndTime.Value);
  231 + }
  232 +
  233 + if (!string.IsNullOrWhiteSpace(input.EventType))
  234 + {
  235 + query = query.Where(x => x.EventType == input.EventType);
  236 + }
  237 +
  238 + if (!string.IsNullOrWhiteSpace(input.StoreId))
  239 + {
  240 + query = query.Where(x => x.StoreId == input.StoreId);
  241 + }
  242 +
  243 + var total = await query.CountAsync();
  244 + var list = await query.OrderBy(x => x.StartTime, OrderByType.Desc)
  245 + .Skip((page - 1) * pageSize)
  246 + .Take(pageSize)
  247 + .ToListAsync();
  248 +
  249 + var typeIds = list.Select(x => x.EventType).Distinct().ToList();
  250 + var types = await _db.Queryable<LqCompanyCalendarTypeEntity>()
  251 + .Where(x => typeIds.Contains(x.Id) && x.IsEffective == StatusEnum.有效.GetHashCode())
  252 + .ToListAsync();
  253 +
  254 + var typeMap = types.ToDictionary(x => x.Id, x => x);
  255 +
  256 + return new LqCompanyCalendarPageOutput
  257 + {
  258 + List = list.Select(x =>
  259 + {
  260 + var type = typeMap.ContainsKey(x.EventType) ? typeMap[x.EventType] : null;
  261 + return new LqCompanyCalendarOutput
  262 + {
  263 + Id = x.Id,
  264 + Title = x.Title,
  265 + EventType = x.EventType,
  266 + EventTypeName = type?.TypeName ?? "",
  267 + EventTypeColor = type?.Color ?? "#3b82f6",
  268 + Date = x.StartTime.ToString("yyyy-MM-dd"),
  269 + Start = x.StartTime.ToString("yyyy-MM-ddTHH:mm:ss"),
  270 + End = x.EndTime.ToString("yyyy-MM-ddTHH:mm:ss"),
  271 + Desc = x.Description,
  272 + StoreId = x.StoreId,
  273 + Store = x.StoreName,
  274 + Remark = x.Remark,
  275 + Attachments = x.Attachments,
  276 + CreateUser = x.CreateUser,
  277 + CreateTime = x.CreateTime
  278 + };
  279 + }).ToList(),
  280 + Total = total
  281 + };
  282 + }
  283 + catch (Exception ex)
  284 + {
  285 + _logger.LogError(ex, "获取公司日历事件列表失败");
  286 + throw NCCException.Oh($"获取失败:{ex.Message}");
  287 + }
  288 + }
  289 +
  290 + /// <summary>
  291 + /// 获取公司日历事件列表(不分页,用于日历展示)
  292 + /// </summary>
  293 + [HttpPost("GetCalendarEventList")]
  294 + public async Task<List<LqCompanyCalendarOutput>> GetCalendarEventListAsync([FromBody] LqCompanyCalendarQueryInput input)
  295 + {
  296 + try
  297 + {
  298 + var query = _db.Queryable<LqCompanyCalendarEntity>()
  299 + .Where(x => x.IsEffective == StatusEnum.有效.GetHashCode());
  300 +
  301 + if (input.StartTime.HasValue)
  302 + {
  303 + query = query.Where(x => x.EndTime >= input.StartTime.Value);
  304 + }
  305 +
  306 + if (input.EndTime.HasValue)
  307 + {
  308 + query = query.Where(x => x.StartTime <= input.EndTime.Value);
  309 + }
  310 +
  311 + if (!string.IsNullOrWhiteSpace(input.EventType))
  312 + {
  313 + query = query.Where(x => x.EventType == input.EventType);
  314 + }
  315 +
  316 + if (!string.IsNullOrWhiteSpace(input.StoreId))
  317 + {
  318 + query = query.Where(x => x.StoreId == input.StoreId);
  319 + }
  320 +
  321 + var list = await query.OrderBy(x => x.StartTime).ToListAsync();
  322 +
  323 + var typeIds = list.Select(x => x.EventType).Distinct().ToList();
  324 + var types = await _db.Queryable<LqCompanyCalendarTypeEntity>()
  325 + .Where(x => typeIds.Contains(x.Id) && x.IsEffective == StatusEnum.有效.GetHashCode())
  326 + .ToListAsync();
  327 +
  328 + var typeMap = types.ToDictionary(x => x.Id, x => x);
  329 +
  330 + return list.Select(x =>
  331 + {
  332 + var type = typeMap.ContainsKey(x.EventType) ? typeMap[x.EventType] : null;
  333 + return new LqCompanyCalendarOutput
  334 + {
  335 + Id = x.Id,
  336 + Title = x.Title,
  337 + EventType = x.EventType,
  338 + EventTypeName = type?.TypeName ?? "",
  339 + EventTypeColor = type?.Color ?? "#3b82f6",
  340 + Date = x.StartTime.ToString("yyyy-MM-dd"),
  341 + Start = x.StartTime.ToString("yyyy-MM-ddTHH:mm:ss"),
  342 + End = x.EndTime.ToString("yyyy-MM-ddTHH:mm:ss"),
  343 + Desc = x.Description,
  344 + StoreId = x.StoreId,
  345 + Store = x.StoreName,
  346 + Remark = x.Remark,
  347 + Attachments = x.Attachments,
  348 + CreateUser = x.CreateUser,
  349 + CreateTime = x.CreateTime
  350 + };
  351 + }).ToList();
  352 + }
  353 + catch (Exception ex)
  354 + {
  355 + _logger.LogError(ex, "获取公司日历事件列表失败");
  356 + throw NCCException.Oh($"获取失败:{ex.Message}");
  357 + }
  358 + }
  359 +
  360 + /// <summary>
  361 + /// 创建公司日历事件
  362 + /// </summary>
  363 + [HttpPost("CreateCalendarEvent")]
  364 + public async Task<string> CreateCalendarEventAsync([FromBody] LqCompanyCalendarCreateInput input)
  365 + {
  366 + try
  367 + {
  368 + var id = YitIdHelper.NextId().ToString();
  369 + var entity = new LqCompanyCalendarEntity
  370 + {
  371 + Id = id,
  372 + Title = input.Title,
  373 + EventType = input.EventType,
  374 + StartTime = input.StartTime,
  375 + EndTime = input.EndTime,
  376 + Description = input.Description,
  377 + StoreId = input.StoreId,
  378 + StoreName = input.StoreName,
  379 + Remark = input.Remark,
  380 + Attachments = input.Attachments,
  381 + IsEffective = StatusEnum.有效.GetHashCode(),
  382 + CreateUser = _userManager.UserId,
  383 + CreateTime = DateTime.Now
  384 + };
  385 +
  386 + await _db.Insertable(entity).ExecuteCommandAsync();
  387 +
  388 + return id;
  389 + }
  390 + catch (Exception ex)
  391 + {
  392 + _logger.LogError(ex, "创建公司日历事件失败");
  393 + throw NCCException.Oh($"创建失败:{ex.Message}");
  394 + }
  395 + }
  396 +
  397 + /// <summary>
  398 + /// 更新公司日历事件
  399 + /// </summary>
  400 + [HttpPost("UpdateCalendarEvent")]
  401 + public async Task<bool> UpdateCalendarEventAsync([FromBody] LqCompanyCalendarUpdateInput input)
  402 + {
  403 + try
  404 + {
  405 + var entity = await _db.Queryable<LqCompanyCalendarEntity>()
  406 + .Where(x => x.Id == input.Id && x.IsEffective == StatusEnum.有效.GetHashCode())
  407 + .FirstAsync();
  408 +
  409 + if (entity == null)
  410 + {
  411 + throw NCCException.Oh("事件不存在或已失效");
  412 + }
  413 +
  414 + entity.Title = input.Title;
  415 + entity.EventType = input.EventType;
  416 + entity.StartTime = input.StartTime;
  417 + entity.EndTime = input.EndTime;
  418 + entity.Description = input.Description;
  419 + entity.StoreId = input.StoreId;
  420 + entity.StoreName = input.StoreName;
  421 + entity.Remark = input.Remark;
  422 + entity.Attachments = input.Attachments;
  423 + entity.UpdateUser = _userManager.UserId;
  424 + entity.UpdateTime = DateTime.Now;
  425 +
  426 + await _db.Updateable(entity).ExecuteCommandAsync();
  427 +
  428 + return true;
  429 + }
  430 + catch (Exception ex)
  431 + {
  432 + _logger.LogError(ex, "更新公司日历事件失败");
  433 + throw NCCException.Oh($"更新失败:{ex.Message}");
  434 + }
  435 + }
  436 +
  437 + /// <summary>
  438 + /// 删除公司日历事件
  439 + /// </summary>
  440 + [HttpPost("DeleteCalendarEvent/{id}")]
  441 + public async Task<bool> DeleteCalendarEventAsync(string id)
  442 + {
  443 + try
  444 + {
  445 + var entity = await _db.Queryable<LqCompanyCalendarEntity>()
  446 + .Where(x => x.Id == id && x.IsEffective == StatusEnum.有效.GetHashCode())
  447 + .FirstAsync();
  448 +
  449 + if (entity == null)
  450 + {
  451 + throw NCCException.Oh("事件不存在或已失效");
  452 + }
  453 +
  454 + entity.IsEffective = StatusEnum.无效.GetHashCode();
  455 + entity.UpdateUser = _userManager.UserId;
  456 + entity.UpdateTime = DateTime.Now;
  457 +
  458 + await _db.Updateable(entity).ExecuteCommandAsync();
  459 +
  460 + return true;
  461 + }
  462 + catch (Exception ex)
  463 + {
  464 + _logger.LogError(ex, "删除公司日历事件失败");
  465 + throw NCCException.Oh($"删除失败:{ex.Message}");
  466 + }
  467 + }
  468 +
  469 + #endregion
  470 + }
  471 +}
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqKhxxService.cs
... ... @@ -3398,6 +3398,10 @@ WHERE kh.F_IsEffective = 1&quot;;
3398 3398 {
3399 3399 var birthday = member.Yanglsr.Value;
3400 3400 birthDateForAge = birthday;
  3401 +
  3402 + if (birthday.Day > DateTime.DaysInMonth(now.Year, birthday.Month))
  3403 + continue;
  3404 +
3401 3405 var thisYearBirthday = new DateTime(now.Year, birthday.Month, birthday.Day);
3402 3406 var nextYearBirthday = new DateTime(now.Year + 1, birthday.Month, birthday.Day);
3403 3407  
... ... @@ -3614,7 +3618,10 @@ WHERE kh.F_IsEffective = 1&quot;;
3614 3618 int calendarMonth;
3615 3619 if (isLeap)
3616 3620 {
3617   - calendarMonth = baseMonth + 1;
  3621 + if (leapMonth > 0 && leapMonth == baseMonth)
  3622 + calendarMonth = baseMonth + 1;
  3623 + else
  3624 + return (null, null);
3618 3625 }
3619 3626 else if (leapMonth > 0 && baseMonth > leapMonth)
3620 3627 {
... ... @@ -3637,7 +3644,7 @@ WHERE kh.F_IsEffective = 1&quot;;
3637 3644 {
3638 3645 return cal.ToDateTime(lunarYear, lunarMonth, lunarDay, 0, 0, 0, 0);
3639 3646 }
3640   - catch (ArgumentOutOfRangeException)
  3647 + catch (Exception)
3641 3648 {
3642 3649 return null;
3643 3650 }
... ...
netcore/src/Modularity/Extend/NCC.Extend/LqStoreDashboardService.cs
... ... @@ -780,6 +780,164 @@ namespace NCC.Extend
780 780 throw NCCException.Oh($"获取门店一周运营热力图数据失败:{ex.Message}");
781 781 }
782 782 }
  783 +
  784 + /// <summary>
  785 + /// 获取沉睡会员列表
  786 + /// </summary>
  787 + /// <remarks>
  788 + /// 查询指定门店中超过配置天数未消费的会员列表。
  789 + /// 沉睡天数通过系统配置 sleepRemindDays 控制,默认30天。
  790 + ///
  791 + /// 示例请求:
  792 + /// ```json
  793 + /// {
  794 + /// "storeId": "1649328471923847169"
  795 + /// }
  796 + /// ```
  797 + ///
  798 + /// 返回数据说明:
  799 + /// - id: 会员ID
  800 + /// - name: 会员姓名
  801 + /// - phone: 手机号(脱敏)
  802 + /// - level: 会员等级
  803 + /// - days: 沉睡天数
  804 + /// - lastVisit: 最后到店日期
  805 + /// - balance: 账户余额
  806 + /// </remarks>
  807 + /// <param name="input">查询参数</param>
  808 + /// <returns>沉睡会员列表</returns>
  809 + /// <response code="200">成功返回沉睡会员列表</response>
  810 + [HttpPost("GetSleepingMembers")]
  811 + public async Task<SleepingMemberPageOutput> GetSleepingMembers([FromBody] SleepingMembersInput input)
  812 + {
  813 + try
  814 + {
  815 + if (input == null || string.IsNullOrWhiteSpace(input.StoreId))
  816 + {
  817 + throw NCCException.Oh("门店ID不能为空");
  818 + }
  819 +
  820 + var page = input.Page < 1 ? 1 : input.Page;
  821 + var pageSize = input.PageSize < 1 ? 20 : (input.PageSize > 100 ? 100 : input.PageSize);
  822 + var offset = (page - 1) * pageSize;
  823 +
  824 + // 获取沉睡天数配置
  825 + var sleepDays = await Helpers.ConsumeEditableDaysHelper.GetSleepRemindDaysAsync(_db);
  826 + var cutoffDate = DateTime.Now.AddDays(-sleepDays);
  827 +
  828 + _logger.LogInformation("查询沉睡会员,门店ID:{StoreId},沉睡天数:{SleepDays},页码:{Page},每页:{PageSize}", input.StoreId, sleepDays, page, pageSize);
  829 +
  830 + // 查询总数
  831 + var countSql = $@"
  832 + SELECT COUNT(*) FROM (
  833 + SELECT kh.F_Id
  834 + FROM lq_khxx kh
  835 + INNER JOIN lq_xh_hyhk xh ON kh.F_Id = xh.Hy AND xh.F_IsEffective = 1
  836 + WHERE xh.Md = '{input.StoreId}'
  837 + GROUP BY kh.F_Id
  838 + HAVING MAX(xh.Hksj) < '{cutoffDate:yyyy-MM-dd HH:mm:ss}' OR MAX(xh.Hksj) IS NULL
  839 + ) t";
  840 + var total = await _db.Ado.SqlQueryAsync<int>(countSql);
  841 + var totalCount = total.FirstOrDefault();
  842 +
  843 + // 查询沉睡会员:最后消费时间早于截止日期的会员
  844 + var sql = $@"
  845 + SELECT
  846 + kh.F_Id as Id,
  847 + kh.khmc as Name,
  848 + kh.sjh as Phone,
  849 + kh.khjd as Level,
  850 + DATEDIFF(NOW(), MAX(xh.Hksj)) as Days,
  851 + MAX(xh.Hksj) as LastVisit,
  852 + kh.F_RemainingRightsAmount as Balance
  853 + FROM lq_khxx kh
  854 + INNER JOIN lq_xh_hyhk xh ON kh.F_Id = xh.Hy AND xh.F_IsEffective = 1
  855 + WHERE xh.Md = '{input.StoreId}'
  856 + GROUP BY kh.F_Id, kh.khmc, kh.sjh, kh.khjd, kh.F_RemainingRightsAmount
  857 + HAVING MAX(xh.Hksj) < '{cutoffDate:yyyy-MM-dd HH:mm:ss}' OR MAX(xh.Hksj) IS NULL
  858 + ORDER BY Days DESC
  859 + LIMIT {pageSize} OFFSET {offset}";
  860 +
  861 + var result = await _db.Ado.SqlQueryAsync<SleepingMemberOutput>(sql);
  862 +
  863 + // 手机号脱敏
  864 + foreach (var item in result)
  865 + {
  866 + if (!string.IsNullOrWhiteSpace(item.Phone) && item.Phone.Length >= 7)
  867 + {
  868 + item.Phone = item.Phone.Substring(0, 3) + "****" + item.Phone.Substring(item.Phone.Length - 4);
  869 + }
  870 + }
  871 +
  872 + return new SleepingMemberPageOutput
  873 + {
  874 + Items = result,
  875 + Total = totalCount
  876 + };
  877 + }
  878 + catch (Exception ex)
  879 + {
  880 + _logger.LogError(ex, "获取沉睡会员列表失败,门店ID:{StoreId}", input?.StoreId);
  881 + throw NCCException.Oh($"获取沉睡会员列表失败:{ex.Message}");
  882 + }
  883 + }
  884 + }
  885 +
  886 + /// <summary>
  887 + /// 沉睡会员输出
  888 + /// </summary>
  889 + public class SleepingMemberOutput
  890 + {
  891 + /// <summary>
  892 + /// 会员ID
  893 + /// </summary>
  894 + public string Id { get; set; }
  895 +
  896 + /// <summary>
  897 + /// 会员姓名
  898 + /// </summary>
  899 + public string Name { get; set; }
  900 +
  901 + /// <summary>
  902 + /// 手机号(脱敏)
  903 + /// </summary>
  904 + public string Phone { get; set; }
  905 +
  906 + /// <summary>
  907 + /// 会员等级
  908 + /// </summary>
  909 + public string Level { get; set; }
  910 +
  911 + /// <summary>
  912 + /// 沉睡天数
  913 + /// </summary>
  914 + public int Days { get; set; }
  915 +
  916 + /// <summary>
  917 + /// 最后到店日期
  918 + /// </summary>
  919 + public DateTime? LastVisit { get; set; }
  920 +
  921 + /// <summary>
  922 + /// 账户余额
  923 + /// </summary>
  924 + public decimal? Balance { get; set; }
  925 + }
  926 +
  927 + /// <summary>
  928 + /// 沉睡会员分页输出
  929 + /// </summary>
  930 + public class SleepingMemberPageOutput
  931 + {
  932 + /// <summary>
  933 + /// 沉睡会员列表
  934 + /// </summary>
  935 + public List<SleepingMemberOutput> Items { get; set; }
  936 +
  937 + /// <summary>
  938 + /// 总数
  939 + /// </summary>
  940 + public int Total { get; set; }
783 941 }
784 942 }
785 943  
... ...
netcore/src/Modularity/Extend/NCC.Extend/OssDirectUploadService.cs
... ... @@ -141,7 +141,7 @@ namespace NCC.Extend
141 141 expiration = expireTime.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"),
142 142 conditions = new object[]
143 143 {
144   - new object[] { "content-length-range", 0, 10485760 },
  144 + new object[] { "content-length-range", 0, 20971520 },
145 145 new object[] { "starts-with", "$key", objectKeyPrefix }
146 146 }
147 147 };
... ...
netcore/src/Modularity/System/NCC.System.Entitys/Dto/System/SysConfig/SysConfigOutput.cs
1   -using NCC.Dependency;
  1 +using NCC.Dependency;
2 2 using System;
3 3 using System.Collections.Generic;
4 4 using System.Linq;
... ... @@ -287,5 +287,10 @@ namespace NCC.System.Entitys.Dto.System.SysConfig
287 287 /// 管理后台删除耗卡允许天数;0 表示不限制天数(是否显示按钮由角色按钮权限 btn_remove 控制)
288 288 /// </summary>
289 289 public string adminConsumeDeleteEditableDays { get; set; }
  290 +
  291 + /// <summary>
  292 + /// 沉睡提醒天数(门店PC),超过该天数未消费的会员将显示在沉睡提醒列表中;默认 30(sys_config:Category=SysConfig,Key=sleepRemindDays)
  293 + /// </summary>
  294 + public string sleepRemindDays { get; set; }
290 295 }
291 296 }
... ...
package-lock.json
... ... @@ -5,13 +5,156 @@
5 5 "packages": {
6 6 "": {
7 7 "dependencies": {
8   - "@qiun/ucharts": "^2.5.0-20230101"
  8 + "@qiun/ucharts": "^2.5.0-20230101",
  9 + "mysql2": "^3.22.5"
9 10 }
10 11 },
11 12 "node_modules/@qiun/ucharts": {
12 13 "version": "2.5.0-20230101",
13 14 "resolved": "https://registry.npmmirror.com/@qiun/ucharts/-/ucharts-2.5.0-20230101.tgz",
14 15 "integrity": "sha512-C7ccBgfPuGF6dxTRuMW0NPPMSCf1k/kh3I9zkRVBc5PaivudX/rPL+jd2Wty6gn5ya5L3Ob+YmYe09V5xw66Cw=="
  16 + },
  17 + "node_modules/@types/node": {
  18 + "version": "25.9.3",
  19 + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz",
  20 + "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==",
  21 + "license": "MIT",
  22 + "peer": true,
  23 + "dependencies": {
  24 + "undici-types": ">=7.24.0 <7.24.7"
  25 + }
  26 + },
  27 + "node_modules/aws-ssl-profiles": {
  28 + "version": "1.1.2",
  29 + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
  30 + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
  31 + "license": "MIT",
  32 + "engines": {
  33 + "node": ">= 6.0.0"
  34 + }
  35 + },
  36 + "node_modules/denque": {
  37 + "version": "2.1.0",
  38 + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
  39 + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
  40 + "license": "Apache-2.0",
  41 + "engines": {
  42 + "node": ">=0.10"
  43 + }
  44 + },
  45 + "node_modules/generate-function": {
  46 + "version": "2.3.1",
  47 + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
  48 + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
  49 + "license": "MIT",
  50 + "dependencies": {
  51 + "is-property": "^1.0.2"
  52 + }
  53 + },
  54 + "node_modules/iconv-lite": {
  55 + "version": "0.7.2",
  56 + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
  57 + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
  58 + "license": "MIT",
  59 + "dependencies": {
  60 + "safer-buffer": ">= 2.1.2 < 3.0.0"
  61 + },
  62 + "engines": {
  63 + "node": ">=0.10.0"
  64 + },
  65 + "funding": {
  66 + "type": "opencollective",
  67 + "url": "https://opencollective.com/express"
  68 + }
  69 + },
  70 + "node_modules/is-property": {
  71 + "version": "1.0.2",
  72 + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
  73 + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
  74 + "license": "MIT"
  75 + },
  76 + "node_modules/long": {
  77 + "version": "5.3.2",
  78 + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
  79 + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
  80 + "license": "Apache-2.0"
  81 + },
  82 + "node_modules/lru.min": {
  83 + "version": "1.1.4",
  84 + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz",
  85 + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==",
  86 + "license": "MIT",
  87 + "engines": {
  88 + "bun": ">=1.0.0",
  89 + "deno": ">=1.30.0",
  90 + "node": ">=8.0.0"
  91 + },
  92 + "funding": {
  93 + "type": "github",
  94 + "url": "https://github.com/sponsors/wellwelwel"
  95 + }
  96 + },
  97 + "node_modules/mysql2": {
  98 + "version": "3.22.5",
  99 + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.22.5.tgz",
  100 + "integrity": "sha512-95uZ2TrPWAZdwpB3vvvDbmEMcNG8yIeNCyu6GUcr/QnWEE/wXm7+mhOCsdQfWQDTV7qYT/PDUZ4U4UPP4AsXqQ==",
  101 + "license": "MIT",
  102 + "dependencies": {
  103 + "aws-ssl-profiles": "^1.1.2",
  104 + "denque": "^2.1.0",
  105 + "generate-function": "^2.3.1",
  106 + "iconv-lite": "^0.7.2",
  107 + "long": "^5.3.2",
  108 + "lru.min": "^1.1.4",
  109 + "named-placeholders": "^1.1.6",
  110 + "sql-escaper": "^1.3.3"
  111 + },
  112 + "engines": {
  113 + "node": ">= 8.0"
  114 + },
  115 + "peerDependencies": {
  116 + "@types/node": ">= 8"
  117 + }
  118 + },
  119 + "node_modules/named-placeholders": {
  120 + "version": "1.1.6",
  121 + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
  122 + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
  123 + "license": "MIT",
  124 + "dependencies": {
  125 + "lru.min": "^1.1.0"
  126 + },
  127 + "engines": {
  128 + "node": ">=8.0.0"
  129 + }
  130 + },
  131 + "node_modules/safer-buffer": {
  132 + "version": "2.1.2",
  133 + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
  134 + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
  135 + "license": "MIT"
  136 + },
  137 + "node_modules/sql-escaper": {
  138 + "version": "1.3.3",
  139 + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz",
  140 + "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==",
  141 + "license": "MIT",
  142 + "engines": {
  143 + "bun": ">=1.0.0",
  144 + "deno": ">=2.0.0",
  145 + "node": ">=12.0.0"
  146 + },
  147 + "funding": {
  148 + "type": "github",
  149 + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1"
  150 + }
  151 + },
  152 + "node_modules/undici-types": {
  153 + "version": "7.24.6",
  154 + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
  155 + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
  156 + "license": "MIT",
  157 + "peer": true
15 158 }
16 159 },
17 160 "dependencies": {
... ... @@ -19,6 +162,95 @@
19 162 "version": "2.5.0-20230101",
20 163 "resolved": "https://registry.npmmirror.com/@qiun/ucharts/-/ucharts-2.5.0-20230101.tgz",
21 164 "integrity": "sha512-C7ccBgfPuGF6dxTRuMW0NPPMSCf1k/kh3I9zkRVBc5PaivudX/rPL+jd2Wty6gn5ya5L3Ob+YmYe09V5xw66Cw=="
  165 + },
  166 + "@types/node": {
  167 + "version": "25.9.3",
  168 + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz",
  169 + "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==",
  170 + "peer": true,
  171 + "requires": {
  172 + "undici-types": ">=7.24.0 <7.24.7"
  173 + }
  174 + },
  175 + "aws-ssl-profiles": {
  176 + "version": "1.1.2",
  177 + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
  178 + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="
  179 + },
  180 + "denque": {
  181 + "version": "2.1.0",
  182 + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
  183 + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="
  184 + },
  185 + "generate-function": {
  186 + "version": "2.3.1",
  187 + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
  188 + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
  189 + "requires": {
  190 + "is-property": "^1.0.2"
  191 + }
  192 + },
  193 + "iconv-lite": {
  194 + "version": "0.7.2",
  195 + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
  196 + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
  197 + "requires": {
  198 + "safer-buffer": ">= 2.1.2 < 3.0.0"
  199 + }
  200 + },
  201 + "is-property": {
  202 + "version": "1.0.2",
  203 + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
  204 + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="
  205 + },
  206 + "long": {
  207 + "version": "5.3.2",
  208 + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
  209 + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="
  210 + },
  211 + "lru.min": {
  212 + "version": "1.1.4",
  213 + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz",
  214 + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="
  215 + },
  216 + "mysql2": {
  217 + "version": "3.22.5",
  218 + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.22.5.tgz",
  219 + "integrity": "sha512-95uZ2TrPWAZdwpB3vvvDbmEMcNG8yIeNCyu6GUcr/QnWEE/wXm7+mhOCsdQfWQDTV7qYT/PDUZ4U4UPP4AsXqQ==",
  220 + "requires": {
  221 + "aws-ssl-profiles": "^1.1.2",
  222 + "denque": "^2.1.0",
  223 + "generate-function": "^2.3.1",
  224 + "iconv-lite": "^0.7.2",
  225 + "long": "^5.3.2",
  226 + "lru.min": "^1.1.4",
  227 + "named-placeholders": "^1.1.6",
  228 + "sql-escaper": "^1.3.3"
  229 + }
  230 + },
  231 + "named-placeholders": {
  232 + "version": "1.1.6",
  233 + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
  234 + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
  235 + "requires": {
  236 + "lru.min": "^1.1.0"
  237 + }
  238 + },
  239 + "safer-buffer": {
  240 + "version": "2.1.2",
  241 + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
  242 + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
  243 + },
  244 + "sql-escaper": {
  245 + "version": "1.3.3",
  246 + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz",
  247 + "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw=="
  248 + },
  249 + "undici-types": {
  250 + "version": "7.24.6",
  251 + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
  252 + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
  253 + "peer": true
22 254 }
23 255 }
24 256 }
... ...
package.json
1 1 {
2 2 "dependencies": {
3   - "@qiun/ucharts": "^2.5.0-20230101"
  3 + "@qiun/ucharts": "^2.5.0-20230101",
  4 + "mysql2": "^3.22.5"
4 5 }
5 6 }
... ...
store-pc/src/api/lqKhxx.js
... ... @@ -31,6 +31,15 @@ export function getMemberOrderType(memberId) {
31 31 })
32 32 }
33 33  
  34 +/** 获取即将过生日的会员列表 */
  35 +export function getUpcomingBirthdays(data) {
  36 + return request({
  37 + url: '/api/Extend/LqKhxx/GetUpcomingBirthdays',
  38 + method: 'get',
  39 + data
  40 + })
  41 +}
  42 +
34 43 /** 健康师金三角(按月) */
35 44 export function getJsjInfo(userId, dateYmd) {
36 45 return request({
... ...
store-pc/src/api/storeDashboard.js
... ... @@ -26,3 +26,30 @@ export function getStorePerformanceCompletion(data) {
26 26 data
27 27 })
28 28 }
  29 +
  30 +/** 获取沉睡会员列表(门店PC) */
  31 +export function getSleepingMembers(data) {
  32 + return request({
  33 + url: '/api/Extend/LqStoreDashboard/GetSleepingMembers',
  34 + method: 'post',
  35 + data
  36 + })
  37 +}
  38 +
  39 +/** 获取公司日历事件列表(不分页,用于日历展示) */
  40 +export function getCalendarEventList(data) {
  41 + return request({
  42 + url: '/api/Extend/LqCompanyCalendar/GetCalendarEventList',
  43 + method: 'post',
  44 + data
  45 + })
  46 +}
  47 +
  48 +/** 获取事件类型列表 */
  49 +export function getEventTypeList(data) {
  50 + return request({
  51 + url: '/api/Extend/LqCompanyCalendar/GetEventTypeList',
  52 + method: 'post',
  53 + data
  54 + })
  55 +}
... ...
store-pc/src/components/BookingCalendarDialog.vue
... ... @@ -9,14 +9,26 @@
9 9 >
10 10 <div class="dialog-inner">
11 11 <div class="dialog-header">
12   - <div class="dialog-title">预约日历</div>
  12 + <div class="dialog-title">
  13 + <i class="el-icon-date"></i> 预约日历
  14 + </div>
  15 + <div class="view-switcher">
  16 + <el-button-group>
  17 + <el-button :type="currentView === 'calendar' ? 'primary' : ''" size="mini" @click="currentView = 'calendar'">
  18 + <i class="el-icon-calendar"></i> 日历
  19 + </el-button>
  20 + <el-button :type="currentView === 'list' ? 'primary' : ''" size="mini" @click="currentView = 'list'">
  21 + <i class="el-icon-s-order"></i> 列表
  22 + </el-button>
  23 + </el-button-group>
  24 + </div>
13 25 <span class="dialog-close" @click="visibleProxy = false"><i class="el-icon-close"></i></span>
14 26 </div>
15 27  
16 28 <div class="dialog-search">
17 29 <el-form @submit.native.prevent :inline="true" size="small">
18 30 <el-form-item label="预约状态">
19   - <el-select v-model="query.status" placeholder="预约状态" clearable style="width:160px">
  31 + <el-select v-model="query.status" placeholder="全部状态" clearable style="width:150px">
20 32 <el-option label="已预约" value="booked" />
21 33 <el-option label="服务中" value="serving" />
22 34 <el-option label="已完成/已转耗卡" value="converted" />
... ... @@ -24,32 +36,95 @@
24 36 </el-select>
25 37 </el-form-item>
26 38 <el-form-item>
27   - <el-button type="primary" @click="search">查询</el-button>
28   - <el-button @click="reset">重置</el-button>
  39 + <el-button type="primary" icon="el-icon-search" @click="search">查询</el-button>
  40 + <el-button icon="el-icon-refresh" @click="reset">重置</el-button>
29 41 </el-form-item>
30 42 </el-form>
31 43 </div>
32 44  
33 45 <div class="dialog-content" v-loading="loading">
34   - <FullCalendar
35   - ref="fullCalendar"
36   - class="store-calendar"
37   - defaultView="dayGridMonth"
38   - :header="calendarHeader"
39   - :plugins="calendarPlugins"
40   - :weekends="true"
41   - :events="calendarEvents"
42   - locale="zh-cn"
43   - :buttonText="buttonText"
44   - :height="calendarHeight"
45   - :eventLimit="true"
46   - allDayText="全天"
47   - :editable="false"
48   - :dayRender="onDayRender"
49   - @datesRender="datesRender"
50   - @dateClick="handleDateClick"
51   - @eventClick="handleEventClick"
52   - />
  46 + <!-- 日历视图 -->
  47 + <div v-show="currentView === 'calendar'" class="calendar-view">
  48 + <FullCalendar
  49 + ref="fullCalendar"
  50 + class="store-calendar"
  51 + defaultView="dayGridMonth"
  52 + :header="calendarHeader"
  53 + :plugins="calendarPlugins"
  54 + :weekends="true"
  55 + :events="calendarEvents"
  56 + locale="zh-cn"
  57 + :buttonText="buttonText"
  58 + :height="calendarHeight"
  59 + :eventLimit="true"
  60 + allDayText="全天"
  61 + :editable="false"
  62 + :dayRender="onDayRender"
  63 + @datesRender="datesRender"
  64 + @dateClick="handleDateClick"
  65 + @eventClick="handleEventClick"
  66 + />
  67 + </div>
  68 +
  69 + <!-- 列表视图 -->
  70 + <div v-show="currentView === 'list'" class="list-view">
  71 + <div class="list-view-header">
  72 + <div class="list-header-left">
  73 + <span class="list-date-range">{{ dateRangeText }}</span>
  74 + <span class="list-total">共 <strong>{{ bookingList.length }}</strong> 条预约</span>
  75 + </div>
  76 + <div class="list-header-right">
  77 + <el-tag size="small" type="primary" effect="plain">已预约 {{ statusCount('booked') }}</el-tag>
  78 + <el-tag size="small" type="success" effect="plain">服务中 {{ statusCount('serving') }}</el-tag>
  79 + <el-tag size="small" type="info" effect="plain">已完成 {{ statusCount('converted') }}</el-tag>
  80 + <el-tag size="small" type="danger" effect="plain">已取消 {{ statusCount('cancelled') }}</el-tag>
  81 + </div>
  82 + </div>
  83 + <el-table
  84 + :data="bookingList"
  85 + style="width: 100%"
  86 + :row-class-name="tableRowClassName"
  87 + @row-click="handleListRowClick"
  88 + >
  89 + <el-table-column label="预约时间" width="180">
  90 + <template slot-scope="scope">
  91 + <div class="time-cell">
  92 + <div class="date-text">{{ scope.row.date }}</div>
  93 + <div class="time-text">{{ scope.row.startTime }} - {{ scope.row.endTime }}</div>
  94 + </div>
  95 + </template>
  96 + </el-table-column>
  97 + <el-table-column prop="memberName" label="会员姓名" width="120">
  98 + <template slot-scope="scope">
  99 + <div class="member-cell">
  100 + <div class="member-avatar">{{ (scope.row.memberName || '').charAt(0) }}</div>
  101 + <span>{{ scope.row.memberName }}</span>
  102 + </div>
  103 + </template>
  104 + </el-table-column>
  105 + <el-table-column prop="roomName" label="房间" width="100" />
  106 + <el-table-column label="状态" width="110">
  107 + <template slot-scope="scope">
  108 + <el-tag :type="getStatusType(scope.row.status)" size="small" effect="light">
  109 + {{ statusText(scope.row.status) }}
  110 + </el-tag>
  111 + </template>
  112 + </el-table-column>
  113 + <el-table-column prop="projectName" label="项目" min-width="150" show-overflow-tooltip />
  114 + <el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip>
  115 + <template slot-scope="scope">
  116 + <span class="remark-text">{{ scope.row.remark || '-' }}</span>
  117 + </template>
  118 + </el-table-column>
  119 + <el-table-column label="操作" width="80" fixed="right" align="center">
  120 + <template slot-scope="scope">
  121 + <el-button type="primary" size="mini" link @click.stop="openBookingDetail(scope.row)">
  122 + 详情
  123 + </el-button>
  124 + </template>
  125 + </el-table-column>
  126 + </el-table>
  127 + </div>
53 128 </div>
54 129 </div>
55 130  
... ... @@ -98,6 +173,7 @@ export default {
98 173 data() {
99 174 return {
100 175 loading: false,
  176 + currentView: 'calendar',
101 177 query: { status: undefined },
102 178 calendarPlugins: [dayGridPlugin, timeGridPlugin, interactionPlugin],
103 179 calendarEvents: [],
... ... @@ -118,6 +194,15 @@ export default {
118 194 visibleProxy: {
119 195 get() { return this.visible },
120 196 set(v) { this.$emit('update:visible', v) }
  197 + },
  198 + dateRangeText() {
  199 + if (this.startTime && this.endTime) {
  200 + const start = this.formatDate(this.startTime)
  201 + const end = this.formatDate(this.endTime)
  202 + if (start === end) return start
  203 + return `${start} 至 ${end}`
  204 + }
  205 + return ''
121 206 }
122 207 },
123 208 watch: {
... ... @@ -171,6 +256,21 @@ export default {
171 256 statusText(status) {
172 257 return appointmentStatusText(status)
173 258 },
  259 + statusCount(status) {
  260 + return (this.bookingList || []).filter(r => r.status === status).length
  261 + },
  262 + tableRowClassName({ row }) {
  263 + if (row.status === 'cancelled') return 'cancelled-row'
  264 + if (row.status === 'serving') return 'serving-row'
  265 + return ''
  266 + },
  267 + getStatusType(status) {
  268 + if (status === 'booked') return 'primary'
  269 + if (status === 'serving') return 'success'
  270 + if (status === 'converted') return 'info'
  271 + if (status === 'cancelled') return 'danger'
  272 + return 'info'
  273 + },
174 274 statusColor(status) {
175 275 if (status === 'booked') return '#409EFF'
176 276 if (status === 'serving') return '#67C23A'
... ... @@ -225,13 +325,18 @@ export default {
225 325 return
226 326 }
227 327 try {
228   - const rangeStart = this.startTime || new Date()
229   - const rangeEnd = this.endTime || new Date()
  328 + let rangeStart = this.startTime
  329 + let rangeEnd = this.endTime
  330 + if (!rangeStart || !rangeEnd) {
  331 + const now = new Date()
  332 + rangeStart = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0, 0)
  333 + rangeEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999)
  334 + }
230 335 const yysjRange = buildYysjTimestampRange(rangeStart, rangeEnd)
231 336 const params = {
232 337 djmd: storeId,
233 338 currentPage: 1,
234   - pageSize: 1000,
  339 + pageSize: 500,
235 340 sidx: 'yysj',
236 341 sort: 'asc'
237 342 }
... ... @@ -415,6 +520,9 @@ export default {
415 520 const rec = (this.bookingList || []).find(x => x && x.id === id)
416 521 if (rec) this.openBookingDetail(rec)
417 522 },
  523 + handleListRowClick(row) {
  524 + this.openBookingDetail(row)
  525 + },
418 526 async openBookingDetail(rec) {
419 527 try {
420 528 const detail = await this.loadBookingDetail(rec.id)
... ... @@ -552,18 +660,289 @@ export default {
552 660 </style>
553 661  
554 662 <style lang="scss" scoped>
555   -::v-deep .booking-calendar-dialog { max-width: 1600px; margin-top: 3vh !important; border-radius: 20px; padding: 0; 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%); box-shadow: 0 24px 48px rgba(15,23,42,0.18), 0 0 0 1px rgba(255,255,255,0.9); backdrop-filter: blur(22px); -webkit-backdrop-filter: blur(22px); }
  663 +::v-deep .booking-calendar-dialog {
  664 + max-width: 1600px;
  665 + margin-top: 3vh !important;
  666 + border-radius: 20px;
  667 + padding: 0;
  668 + background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
  669 + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(0, 0, 0, 0.05);
  670 + overflow: hidden;
  671 +}
  672 +
556 673 ::v-deep .el-dialog__header { display: none; }
557 674 ::v-deep .el-dialog__body { padding: 0; }
558   -.dialog-inner { display: flex; flex-direction: column; max-height: 92vh; height: 88vh; }
559   -.dialog-header { flex-shrink: 0; display: flex; align-items: center; justify-content: space-between; margin: 18px 22px 0; padding: 10px 14px; border-radius: 14px; background: rgba(219,234,254,0.96); }
560   -.dialog-title { font-size: 17px; font-weight: 600; color: #0f172a; }
561   -.dialog-close { cursor: pointer; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border-radius: 999px; color: #64748b; transition: all 0.15s; &:hover { background: rgba(0,0,0,0.06); color: #0f172a; } }
562   -.dialog-search { flex-shrink: 0; padding: 12px 22px 4px; }
563   -.dialog-content { flex: 1; min-height: 0; overflow: hidden; padding: 0 22px 14px; }
564   -::v-deep .booking-calendar-dialog .el-input__inner { border-radius: 999px; height: 32px; line-height: 32px; border-color: #e5e7eb; background-color: #f9fafb; &:focus { border-color: #2563eb; } }
565   -::v-deep .booking-calendar-dialog .el-button--primary { border-radius: 999px; background: #2563eb; border-color: #2563eb; box-shadow: 0 4px 10px rgba(37,99,235,0.35); }
566   -::v-deep .booking-calendar-dialog .el-button--default { border-radius: 999px; }
567   -::v-deep .booking-calendar-dialog .el-form-item { margin-bottom: 8px; }
568   -::v-deep .booking-calendar-dialog .el-form-item__label { white-space: nowrap; }
  675 +
  676 +.dialog-inner {
  677 + display: flex;
  678 + flex-direction: column;
  679 + max-height: 90vh;
  680 + height: 85vh;
  681 +}
  682 +
  683 +.dialog-header {
  684 + flex-shrink: 0;
  685 + display: flex;
  686 + align-items: center;
  687 + justify-content: space-between;
  688 + margin: 16px 20px 0;
  689 + padding: 14px 20px;
  690 + border-radius: 12px;
  691 + background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
  692 + border: 1px solid rgba(59, 130, 246, 0.1);
  693 +}
  694 +
  695 +.dialog-title {
  696 + font-size: 18px;
  697 + font-weight: 700;
  698 + color: #1e293b;
  699 + display: flex;
  700 + align-items: center;
  701 + gap: 8px;
  702 +
  703 + i {
  704 + color: #3b82f6;
  705 + font-size: 20px;
  706 + }
  707 +}
  708 +
  709 +.dialog-close {
  710 + cursor: pointer;
  711 + width: 32px;
  712 + height: 32px;
  713 + display: flex;
  714 + align-items: center;
  715 + justify-content: center;
  716 + border-radius: 8px;
  717 + color: #64748b;
  718 + transition: all 0.2s;
  719 +
  720 + &:hover {
  721 + background: rgba(0, 0, 0, 0.08);
  722 + color: #0f172a;
  723 + }
  724 +}
  725 +
  726 +.dialog-search {
  727 + flex-shrink: 0;
  728 + padding: 16px 20px 8px;
  729 + border-bottom: 1px solid #f1f5f9;
  730 +}
  731 +
  732 +.dialog-content {
  733 + flex: 1;
  734 + min-height: 0;
  735 + overflow: hidden;
  736 + padding: 16px 20px 20px;
  737 +}
  738 +
  739 +::v-deep .booking-calendar-dialog {
  740 + .el-input__inner {
  741 + border-radius: 8px;
  742 + height: 32px;
  743 + line-height: 32px;
  744 + border-color: #e2e8f0;
  745 + background-color: #ffffff;
  746 + transition: all 0.2s;
  747 +
  748 + &:focus {
  749 + border-color: #3b82f6;
  750 + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
  751 + }
  752 + }
  753 +
  754 + .el-button--primary {
  755 + border-radius: 8px;
  756 + background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
  757 + border-color: #3b82f6;
  758 + box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3);
  759 + font-weight: 500;
  760 +
  761 + &:hover {
  762 + background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
  763 + box-shadow: 0 4px 12px rgba(37, 99, 235, 0.4);
  764 + }
  765 + }
  766 +
  767 + .el-button--default {
  768 + border-radius: 8px;
  769 + font-weight: 500;
  770 + }
  771 +
  772 + .el-form-item {
  773 + margin-bottom: 0;
  774 + margin-right: 12px;
  775 + }
  776 +
  777 + .el-form-item__label {
  778 + white-space: nowrap;
  779 + color: #475569;
  780 + font-weight: 500;
  781 + }
  782 +}
  783 +
  784 +.view-switcher {
  785 + flex: 1;
  786 + display: flex;
  787 + justify-content: center;
  788 +
  789 + .el-button-group {
  790 + .el-button {
  791 + border-radius: 6px;
  792 + font-size: 13px;
  793 + font-weight: 500;
  794 + padding: 8px 16px;
  795 + transition: all 0.2s;
  796 +
  797 + &:first-child {
  798 + border-top-right-radius: 0;
  799 + border-bottom-right-radius: 0;
  800 + }
  801 +
  802 + &:last-child {
  803 + border-top-left-radius: 0;
  804 + border-bottom-left-radius: 0;
  805 + }
  806 +
  807 + i {
  808 + margin-right: 4px;
  809 + }
  810 + }
  811 + }
  812 +}
  813 +
  814 +.calendar-view, .list-view {
  815 + height: 100%;
  816 + display: flex;
  817 + flex-direction: column;
  818 + overflow: hidden;
  819 +}
  820 +
  821 +.list-view {
  822 + padding: 0;
  823 +
  824 + .list-view-header {
  825 + display: flex;
  826 + justify-content: space-between;
  827 + align-items: center;
  828 + padding: 14px 16px;
  829 + background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
  830 + border-radius: 10px;
  831 + margin-bottom: 12px;
  832 + flex-shrink: 0;
  833 + border: 1px solid #e2e8f0;
  834 +
  835 + .list-header-left {
  836 + display: flex;
  837 + align-items: center;
  838 + gap: 16px;
  839 +
  840 + .list-date-range {
  841 + font-size: 15px;
  842 + font-weight: 600;
  843 + color: #1e293b;
  844 + }
  845 +
  846 + .list-total {
  847 + font-size: 13px;
  848 + color: #64748b;
  849 +
  850 + strong {
  851 + color: #3b82f6;
  852 + font-weight: 600;
  853 + }
  854 + }
  855 + }
  856 +
  857 + .list-header-right {
  858 + display: flex;
  859 + gap: 8px;
  860 + }
  861 + }
  862 +
  863 + .el-table {
  864 + flex: 1;
  865 + overflow: auto;
  866 + border-radius: 8px;
  867 + border: 1px solid #e2e8f0;
  868 +
  869 + &::before {
  870 + display: none;
  871 + }
  872 +
  873 + th {
  874 + background: #f8fafc !important;
  875 + color: #475569;
  876 + font-weight: 600;
  877 + font-size: 13px;
  878 + border-bottom: 1px solid #e2e8f0;
  879 + }
  880 +
  881 + td {
  882 + padding: 10px 0;
  883 + border-bottom: 1px solid #f1f5f9;
  884 + font-size: 13px;
  885 + }
  886 +
  887 + tr:hover > td {
  888 + background: #f8fafc !important;
  889 + }
  890 + }
  891 +
  892 + .cancelled-row {
  893 + opacity: 0.6;
  894 +
  895 + &:hover {
  896 + opacity: 0.8;
  897 + }
  898 + }
  899 +
  900 + .serving-row {
  901 + background: rgba(34, 197, 94, 0.04);
  902 + }
  903 +
  904 + .time-cell {
  905 + .date-text {
  906 + font-size: 13px;
  907 + color: #1e293b;
  908 + font-weight: 500;
  909 + }
  910 +
  911 + .time-text {
  912 + font-size: 12px;
  913 + color: #64748b;
  914 + margin-top: 2px;
  915 + }
  916 + }
  917 +
  918 + .member-cell {
  919 + display: flex;
  920 + align-items: center;
  921 + gap: 8px;
  922 +
  923 + .member-avatar {
  924 + width: 28px;
  925 + height: 28px;
  926 + border-radius: 50%;
  927 + background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
  928 + color: white;
  929 + display: flex;
  930 + align-items: center;
  931 + justify-content: center;
  932 + font-size: 12px;
  933 + font-weight: 600;
  934 + flex-shrink: 0;
  935 + }
  936 +
  937 + span {
  938 + color: #1e293b;
  939 + font-weight: 500;
  940 + }
  941 + }
  942 +
  943 + .remark-text {
  944 + color: #94a3b8;
  945 + font-size: 12px;
  946 + }
  947 +}
569 948 </style>
... ...
store-pc/src/components/CompanyCalendarDialog.vue
... ... @@ -11,9 +11,9 @@
11 11 <div class="dialog-header">
12 12 <div class="dialog-title">公司日历</div>
13 13 <div class="dialog-legend">
14   - <span v-for="item in legendList" :key="item.type" class="legend-item">
  14 + <span v-for="item in eventTypeList" :key="item.id" class="legend-item">
15 15 <span class="legend-dot" :style="{ background: item.color }"></span>
16   - {{ item.label }}
  16 + {{ item.typeName }}
17 17 </span>
18 18 </div>
19 19 <span class="dialog-close" @click="visibleProxy = false"><i class="el-icon-close"></i></span>
... ... @@ -58,8 +58,8 @@
58 58 <div class="row">
59 59 <span class="label">类型</span>
60 60 <span class="value">
61   - <span class="type-dot" :style="{ background: typeColorMap[selectedEvent.type] || '#3b82f6' }"></span>
62   - {{ selectedEventTypeLabel }}
  61 + <span class="type-dot" :style="{ background: selectedEvent.typeColor || '#3b82f6' }"></span>
  62 + {{ selectedEvent.typeName || '无' }}
63 63 </span>
64 64 </div>
65 65 <div class="row"><span class="label">日期</span><span class="value">{{ selectedEventDateStr }}</span></div>
... ... @@ -114,6 +114,7 @@ import FullCalendar from &#39;@fullcalendar/vue&#39;
114 114 import dayGridPlugin from '@fullcalendar/daygrid'
115 115 import timeGridPlugin from '@fullcalendar/timegrid'
116 116 import interactionPlugin from '@fullcalendar/interaction'
  117 +import { getCalendarEventList, getEventTypeList } from '@/api/storeDashboard'
117 118  
118 119 export default {
119 120 name: 'CompanyCalendarDialog',
... ... @@ -122,50 +123,11 @@ export default {
122 123 visible: { type: Boolean, default: false }
123 124 },
124 125 data() {
125   - const today = new Date()
126   - const y = today.getFullYear()
127   - const m = String(today.getMonth() + 1).padStart(2, '0')
128   - const d = String(today.getDate()).padStart(2, '0')
129   - const todayStr = `${y}-${m}-${d}`
130 126 return {
131 127 loading: false,
132   - // 公司层面的会议/活动数据(后续可换成接口)
133   - companyEvents: [
134   - // 当天 7 条示例(操作会 + 大会 + 活动),方便预览密集情况
135   - { id: 'E001', date: todayStr, start: `${todayStr}T09:00:00`, end: `${todayStr}T10:00:00`, type: 'ops', title: '操作会 - 紫荆店' },
136   - { id: 'E002', date: todayStr, start: `${todayStr}T10:30:00`, end: `${todayStr}T11:30:00`, type: 'ops', title: '操作会 - 西沙店' },
137   - { id: 'E003', date: todayStr, start: `${todayStr}T13:00:00`, end: `${todayStr}T14:00:00`, type: 'training', title: '培训会 - 新员工入职营', remark: '请提前准备入职材料', attachments: [{ name: '入职手册.pdf' }, { name: '培训PPT.pptx' }] },
138   - { id: 'E004', date: todayStr, start: `${todayStr}T14:30:00`, end: `${todayStr}T15:30:00`, type: 'training', title: '培训会 - 科美新品讲解' },
139   - { id: 'E005', date: todayStr, start: `${todayStr}T16:00:00`, end: `${todayStr}T17:00:00`, type: 'activity', title: '门店活动 - 紫荆店会员沙龙', remark: '需准备茶点与签到礼' },
140   - { id: 'E006', date: todayStr, start: `${todayStr}T18:00:00`, end: `${todayStr}T19:00:00`, type: 'activity', title: '门店活动 - 西沙店体验日' },
141   - { id: 'E007', date: todayStr, start: `${todayStr}T19:30:00`, end: `${todayStr}T21:00:00`, type: 'meeting', title: '全体大会 - 本月复盘', remark: '各店长携带本月数据报表', attachments: [{ name: '本月经营数据.xlsx' }, { name: '会议议程.pdf' }] },
142   - // 其它日期:尽量覆盖当月大部分天
143   - { id: 'E008', date: `${y}-${m}-01`, start: `${y}-${m}-01T10:00:00`, end: `${y}-${m}-01T12:00:00`, type: 'meeting', title: '月初经营例会' },
144   - { id: 'E009', date: `${y}-${m}-02`, start: `${y}-${m}-02T14:00:00`, end: `${y}-${m}-02T16:00:00`, type: 'ops', title: '操作会 - 静居寺店' },
145   - { id: 'E010', date: `${y}-${m}-03`, start: `${y}-${m}-03T09:30:00`, end: `${y}-${m}-03T11:00:00`, type: 'training', title: '培训会 - 会员沟通话术' },
146   - { id: 'E011', date: `${y}-${m}-04`, start: `${y}-${m}-04T15:00:00`, end: `${y}-${m}-04T17:00:00`, type: 'activity', title: '门店活动 - 双流店开放日' },
147   - { id: 'E012', date: `${y}-${m}-05`, start: `${y}-${m}-05T10:00:00`, end: `${y}-${m}-05T11:30:00`, type: 'ops', title: '操作会 - 保利店' },
148   - { id: 'E013', date: `${y}-${m}-06`, start: `${y}-${m}-06T14:00:00`, end: `${y}-${m}-06T15:30:00`, type: 'training', title: '培训会 - 逆龄项目进阶' },
149   - { id: 'E014', date: `${y}-${m}-07`, start: `${y}-${m}-07T19:00:00`, end: `${y}-${m}-07T20:30:00`, type: 'activity', title: '门店活动 - 线上直播专场' },
150   - { id: 'E015', date: `${y}-${m}-08`, start: `${y}-${m}-08T09:30:00`, end: `${y}-${m}-08T11:00:00`, type: 'meeting', title: '事业部经营会' },
151   - { id: 'E016', date: `${y}-${m}-09`, start: `${y}-${m}-09T13:30:00`, end: `${y}-${m}-09T15:00:00`, type: 'ops', title: '操作会 - 南湖店' },
152   - { id: 'E017', date: `${y}-${m}-10`, start: `${y}-${m}-10T10:00:00`, end: `${y}-${m}-10T11:30:00`, type: 'training', title: '培训会 - 顾问心法' },
153   - { id: 'E018', date: `${y}-${m}-11`, start: `${y}-${m}-11T15:00:00`, end: `${y}-${m}-11T17:00:00`, type: 'activity', title: '门店活动 - 西站店沙龙' },
154   - { id: 'E019', date: `${y}-${m}-12`, start: `${y}-${m}-12T09:30:00`, end: `${y}-${m}-12T11:00:00`, type: 'ops', title: '操作会 - 融创店' },
155   - { id: 'E020', date: `${y}-${m}-13`, start: `${y}-${m}-13T14:00:00`, end: `${y}-${m}-13T15:30:00`, type: 'training', title: '培训会 - 经典案例分享' },
156   - { id: 'E021', date: `${y}-${m}-14`, start: `${y}-${m}-14T19:00:00`, end: `${y}-${m}-14T20:30:00`, type: 'activity', title: '门店活动 - 会员观影会' },
157   - { id: 'E022', date: `${y}-${m}-15`, start: `${y}-${m}-15T10:00:00`, end: `${y}-${m}-15T12:00:00`, type: 'training', title: '产品培训 - 生命之波' },
158   - { id: 'E023', date: `${y}-${m}-16`, start: `${y}-${m}-16T09:30:00`, end: `${y}-${m}-16T11:00:00`, type: 'ops', title: '操作会 - 科技部' },
159   - { id: 'E024', date: `${y}-${m}-17`, start: `${y}-${m}-17T15:00:00`, end: `${y}-${m}-17T17:00:00`, type: 'activity', title: '门店活动 - 会员答谢宴' },
160   - { id: 'E025', date: `${y}-${m}-18`, start: `${y}-${m}-18T14:00:00`, end: `${y}-${m}-18T15:30:00`, type: 'training', title: '培训会 - 绩效与辅导' },
161   - { id: 'E026', date: `${y}-${m}-19`, start: `${y}-${m}-19T10:00:00`, end: `${y}-${m}-19T11:30:00`, type: 'ops', title: '操作会 - 门店联动' },
162   - { id: 'E027', date: `${y}-${m}-20`, start: `${y}-${m}-20T09:30:00`, end: `${y}-${m}-20T11:30:00`, type: 'meeting', title: '季度全体大会', remark: '全体参加,线上同步', attachments: [{ name: '季度总结报告.pdf' }, { name: '下季目标分解.xlsx' }] },
163   - { id: 'E028', date: `${y}-${m}-21`, start: `${y}-${m}-21T19:00:00`, end: `${y}-${m}-21T20:30:00`, type: 'activity', title: '门店活动 - 新品体验会' },
164   - { id: 'E029', date: `${y}-${m}-22`, start: `${y}-${m}-22T10:00:00`, end: `${y}-${m}-22T11:30:00`, type: 'training', title: '培训会 - 工具使用' },
165   - { id: 'E030', date: `${y}-${m}-23`, start: `${y}-${m}-23T14:00:00`, end: `${y}-${m}-23T15:30:00`, type: 'ops', title: '操作会 - 区域复盘' },
166   - { id: 'E031', date: `${y}-${m}-24`, start: `${y}-${m}-24T15:00:00`, end: `${y}-${m}-24T16:30:00`, type: 'activity', title: '门店活动 - 老客回访日' },
167   - { id: 'E032', date: `${y}-${m}-25`, start: `${y}-${m}-25T09:30:00`, end: `${y}-${m}-25T11:00:00`, type: 'meeting', title: '高管经营例会' }
168   - ],
  128 + companyEvents: [],
  129 + eventTypeList: [],
  130 + typeColorMap: {},
169 131 calendarPlugins: [dayGridPlugin, timeGridPlugin, interactionPlugin],
170 132 calendarEvents: [],
171 133 calendarHeader: {
... ... @@ -178,13 +140,7 @@ export default {
178 140 endTime: null,
179 141 calendarHeight: 720,
180 142 detailVisible: false,
181   - selectedEvent: null,
182   - typeColorMap: {
183   - ops: '#6366f1',
184   - training: '#22c55e',
185   - meeting: '#f97316',
186   - activity: '#ec4899'
187   - }
  143 + selectedEvent: null
188 144 }
189 145 },
190 146 computed: {
... ... @@ -196,19 +152,6 @@ export default {
196 152 this.$emit('update:visible', v)
197 153 }
198 154 },
199   - legendList() {
200   - return [
201   - { type: 'ops', label: '操作会', color: this.typeColorMap.ops },
202   - { type: 'training', label: '培训会', color: this.typeColorMap.training },
203   - { type: 'meeting', label: '全体大会', color: this.typeColorMap.meeting },
204   - { type: 'activity', label: '门店活动', color: this.typeColorMap.activity }
205   - ]
206   - },
207   - selectedEventTypeLabel() {
208   - if (!this.selectedEvent || !this.selectedEvent.type) return '—'
209   - const item = this.legendList.find(x => x.type === this.selectedEvent.type)
210   - return item ? item.label : this.selectedEvent.type
211   - },
212 155 selectedEventDateStr() {
213 156 if (!this.selectedEvent || !this.selectedEvent.start) return '—'
214 157 return this.formatDate(new Date(this.selectedEvent.start))
... ... @@ -222,8 +165,14 @@ export default {
222 165 },
223 166 selectedEventAttachments() {
224 167 if (!this.selectedEvent || !this.selectedEvent.attachments) return []
225   - const list = Array.isArray(this.selectedEvent.attachments) ? this.selectedEvent.attachments : []
226   - return list.filter(a => a && (a.name || a.fileName))
  168 + try {
  169 + const list = typeof this.selectedEvent.attachments === 'string'
  170 + ? JSON.parse(this.selectedEvent.attachments)
  171 + : this.selectedEvent.attachments
  172 + return Array.isArray(list) ? list.filter(a => a && (a.name || a.fileName)) : []
  173 + } catch {
  174 + return []
  175 + }
227 176 }
228 177 },
229 178 watch: {
... ... @@ -231,6 +180,7 @@ export default {
231 180 if (v) {
232 181 this.$nextTick(() => {
233 182 this.calcHeight()
  183 + this.loadEventTypeList()
234 184 this.initData()
235 185 })
236 186 }
... ... @@ -247,7 +197,6 @@ export default {
247 197 this.$nextTick(() => {
248 198 const el = this.$el && this.$el.querySelector('.dialog-content')
249 199 if (el) {
250   - // 提高最小高度,让每天能展示更多事件
251 200 this.calendarHeight = Math.max(el.clientHeight - 20, 650)
252 201 }
253 202 })
... ... @@ -258,33 +207,56 @@ export default {
258 207 this.endTime = view.activeEnd
259 208 this.initData()
260 209 },
261   - initData() {
  210 + async loadEventTypeList() {
  211 + try {
  212 + const res = await getEventTypeList({})
  213 + const list = res.data || res || []
  214 + this.eventTypeList = Array.isArray(list) ? list : []
  215 + this.typeColorMap = {}
  216 + this.eventTypeList.forEach(item => {
  217 + this.typeColorMap[item.Id] = item.Color
  218 + })
  219 + } catch (e) {
  220 + console.error('[公司日历] 加载事件类型失败:', e)
  221 + }
  222 + },
  223 + async initData() {
262 224 this.loading = true
263   - setTimeout(() => {
264   - let filtered = [...this.companyEvents]
265   - if (this.startTime && this.endTime) {
266   - filtered = filtered.filter(e => {
267   - const t = new Date(e.start).getTime()
268   - return t >= this.startTime.getTime() && t < this.endTime.getTime()
269   - })
  225 + try {
  226 + const params = {}
  227 + if (this.startTime) {
  228 + params.startTime = this.startTime.toISOString()
270 229 }
271   - this.calendarEvents = filtered.map(evt => {
272   - const color = this.typeColorMap[evt.type] || '#3b82f6'
  230 + if (this.endTime) {
  231 + params.endTime = this.endTime.toISOString()
  232 + }
  233 +
  234 + const res = await getCalendarEventList(params)
  235 + const list = res.data || res || []
  236 + this.companyEvents = Array.isArray(list) ? list : []
  237 +
  238 + this.calendarEvents = this.companyEvents.map(evt => {
  239 + const color = evt.EventTypeColor || this.typeColorMap[evt.EventType] || '#3b82f6'
273 240 return {
274   - id: evt.id,
275   - title: evt.title,
276   - start: new Date(evt.start).toISOString(),
277   - end: new Date(evt.end).toISOString(),
  241 + id: evt.Id,
  242 + title: evt.Title,
  243 + start: evt.Start,
  244 + end: evt.End,
278 245 color,
279 246 editable: false,
280 247 allDay: false,
281 248 extendedProps: {
282   - type: evt.type
  249 + type: evt.EventType
283 250 }
284 251 }
285 252 })
  253 + } catch (e) {
  254 + console.error('[公司日历] 加载失败:', e)
  255 + this.companyEvents = []
  256 + this.calendarEvents = []
  257 + } finally {
286 258 this.loading = false
287   - }, 200)
  259 + }
288 260 },
289 261 formatDate(d) {
290 262 const dt = d instanceof Date ? d : new Date(d)
... ... @@ -310,17 +282,19 @@ export default {
310 282 handleEventClick(info) {
311 283 const evt = info && info.event
312 284 if (!evt) return
313   - const raw = this.companyEvents.find(e => e.id === evt.id) || {}
  285 + const raw = this.companyEvents.find(e => e.Id === evt.id) || {}
314 286 this.selectedEvent = {
315   - id: raw.id,
316   - title: raw.title || evt.title,
317   - type: (evt.extendedProps && evt.extendedProps.type) || raw.type,
318   - start: raw.start,
319   - end: raw.end,
320   - desc: raw.desc,
321   - store: raw.store,
322   - remark: raw.remark,
323   - attachments: raw.attachments ? [...raw.attachments] : []
  287 + id: raw.Id || evt.id,
  288 + title: raw.Title || evt.title,
  289 + type: (evt.extendedProps && evt.extendedProps.type) || raw.EventType,
  290 + typeName: raw.EventTypeName || '',
  291 + typeColor: raw.EventTypeColor || this.typeColorMap[(evt.extendedProps && evt.extendedProps.type) || raw.EventType] || '#3b82f6',
  292 + start: raw.Start,
  293 + end: raw.End,
  294 + desc: raw.Desc,
  295 + store: raw.Store,
  296 + remark: raw.Remark,
  297 + attachments: raw.Attachments
324 298 }
325 299 this.detailVisible = true
326 300 }
... ... @@ -698,5 +672,3 @@ export default {
698 672 box-shadow: 0 4px 10px rgba(37, 99, 235, 0.35);
699 673 }
700 674 </style>
701   -
702   -
... ...
store-pc/src/views/dashboard/index.vue
... ... @@ -304,7 +304,7 @@
304 304 <div v-else class="empty-tip">今日暂无预约记录</div>
305 305 </el-tab-pane>
306 306 <el-tab-pane label="生日提醒" name="birthday">
307   - <div class="birthday-calendar">
  307 + <div class="birthday-calendar" v-loading="birthdayLoading">
308 308 <!-- 月份切换头 -->
309 309 <div class="cal-header">
310 310 <button class="cal-nav-btn" @click="prevBirthdayMonth"><i class="el-icon-arrow-left"></i></button>
... ... @@ -360,7 +360,7 @@
360 360 </div>
361 361 </el-tab-pane>
362 362 <el-tab-pane label="沉睡提醒" name="sleeping">
363   - <div v-if="sleepingMembers.length" class="timeline-list">
  363 + <div v-if="sleepingMembers.length || sleepingLoading" class="timeline-list" @scroll="handleSleepingScroll">
364 364 <div v-for="item in sleepingMembers" :key="item.id" class="sleeping-item">
365 365 <div class="sleeping-days">{{ item.days }}天</div>
366 366 <div class="sleeping-info">
... ... @@ -373,6 +373,8 @@
373 373 </div>
374 374 <button class="quick-book-btn" @click="handleSleepingCall(item)">去沟通</button>
375 375 </div>
  376 + <div v-if="sleepingLoading" class="loading-more">加载中...</div>
  377 + <div v-else-if="sleepingMembers.length >= sleepingTotal && sleepingTotal > 0" class="no-more">没有更多了</div>
376 378 </div>
377 379 <div v-else class="empty-tip">暂无沉睡会员</div>
378 380 </el-tab-pane>
... ... @@ -461,7 +463,8 @@ import request from &#39;@/utils/request&#39;
461 463 import {
462 464 getStoreDashboardStatistics,
463 465 getStoreDailyStatistics,
464   - getStorePerformanceCompletion
  466 + getStorePerformanceCompletion,
  467 + getSleepingMembers
465 468 } from '@/api/storeDashboard'
466 469 import { getTkjlbList } from '@/api/lqTkjlb'
467 470 import { getKdKdjlbList } from '@/api/lqKdKdjlb'
... ... @@ -497,7 +500,7 @@ import ConsumeListDialog from &#39;@/components/ConsumeListDialog.vue&#39;
497 500 import RefundListDialog from '@/components/RefundListDialog.vue'
498 501 import GoldTriangleDialog from '@/components/GoldTriangleDialog.vue'
499 502 import StoreTargetDialog from '@/components/StoreTargetDialog.vue'
500   -import { getKhxxList } from '@/api/lqKhxx'
  503 +import { getKhxxList, getUpcomingBirthdays } from '@/api/lqKhxx'
501 504 import { getMyProfile } from '@/api/lqStoreAuth'
502 505 import { mapKhxxListToSuggestion } from '@/utils/memberProfileHelper'
503 506  
... ... @@ -649,28 +652,13 @@ export default {
649 652 birthdayYear: new Date().getFullYear(),
650 653 birthdayMonth: new Date().getMonth() + 1,
651 654 selectedBirthdayDay: null,
652   - birthdayAllData: [
653   - { id: 'B001', name: '林晓薇', phone: '138****8801', level: 'A++', totalAmount: '38,600', month: 3, day: 5 },
654   - { id: 'B002', name: '陈美玲', phone: '139****2233', level: 'A+', totalAmount: '22,400', month: 3, day: 12 },
655   - { id: 'B003', name: '张芳芳', phone: '158****9901', level: 'A', totalAmount: '15,800', month: 3, day: 12 },
656   - { id: 'B004', name: '王晓燕', phone: '137****4456', level: 'A++', totalAmount: '45,200', month: 3, day: 18 },
657   - { id: 'B005', name: '李明珠', phone: '186****3322', level: 'B', totalAmount: '8,900', month: 3, day: 22 },
658   - { id: 'B006', name: '周丽', phone: '133****7788', level: 'A', totalAmount: '18,300', month: 3, day: 22 },
659   - { id: 'B007', name: '刘思雨', phone: '159****6678', level: 'C', totalAmount: '5,200', month: 3, day: 28 },
660   - { id: 'B008', name: '赵美华', phone: '181****2234', level: 'A+', totalAmount: '31,800', month: 4, day: 6 },
661   - { id: 'B009', name: '孙晴晴', phone: '135****8890', level: 'B', totalAmount: '11,200', month: 4, day: 15 },
662   - { id: 'B010', name: '钱雯雯', phone: '189****0012', level: 'D', totalAmount: '2,800', month: 4, day: 15 },
663   - { id: 'B011', name: '吴佳敏', phone: '136****5567', level: 'A++', totalAmount: '52,400', month: 5, day: 3 },
664   - { id: 'B012', name: '郑晓红', phone: '182****3345', level: 'A', totalAmount: '19,600', month: 5, day: 20 },
665   - { id: 'B013', name: '冯丽娟', phone: '155****7723', level: 'B', totalAmount: '7,300', month: 1, day: 8 },
666   - { id: 'B014', name: '蒋雪梅', phone: '177****6634', level: 'A+', totalAmount: '28,500', month: 2, day: 14 }
667   - ],
668   - sleepingMembers: [
669   - { id: 'S001', name: '周丽娟', days: 187, phone: '133****7788', level: 'A++', lastVisit: '2024-09-01', balance: '12,800' },
670   - { id: 'S002', name: '王晓燕', days: 95, phone: '137****4456', level: 'A+', lastVisit: '2024-12-01', balance: '8,500' },
671   - { id: 'S003', name: '刘倩', days: 63, phone: '159****3312', level: 'A', lastVisit: '2025-01-03', balance: '3,200' },
672   - { id: 'S004', name: '赵海燕', days: 60, phone: '186****0023', level: 'B', lastVisit: '2025-01-06', balance: '1,600' }
673   - ],
  655 + birthdayAllData: [],
  656 + birthdayLoading: false,
  657 + sleepingMembers: [],
  658 + sleepingPage: 1,
  659 + sleepingTotal: 0,
  660 + sleepingLoading: false,
  661 + sleepingPageSize: 20,
674 662 tuokeDialogVisible: false,
675 663 bookingDialogVisible: false,
676 664 bookingPrefill: {
... ... @@ -799,7 +787,7 @@ export default {
799 787 {
800 788 key: 'sleeping',
801 789 label: '沉睡提醒',
802   - count: this.sleepingMembers.length,
  790 + count: this.sleepingTotal,
803 791 icon: 'el-icon-moon'
804 792 }
805 793 ]
... ... @@ -918,6 +906,8 @@ export default {
918 906 this.loadScheduleOnDutyCount()
919 907 this.loadTodoList()
920 908 this.loadTodoCount()
  909 + this.loadSleepingMembers(true)
  910 + this.loadBirthdayData()
921 911 },
922 912 formatTodoDueLabel(dt) {
923 913 return formatTodoDueLabelText(dt)
... ... @@ -1485,6 +1475,59 @@ export default {
1485 1475 handleBirthdayCall(item) {
1486 1476 this.$message.success(`已为 ${item.name} 发送生日祝福短信`)
1487 1477 },
  1478 + async loadBirthdayData() {
  1479 + const storeId = this.resolveStoreId()
  1480 + if (!storeId) return
  1481 + this.birthdayLoading = true
  1482 + try {
  1483 + const y = this.birthdayYear
  1484 + const m = this.birthdayMonth
  1485 + const lastDay = new Date(y, m, 0).getDate()
  1486 + const startDate = `${y}-${String(m).padStart(2, '0')}-01`
  1487 + const endDate = `${y}-${String(m).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`
  1488 + const res = await getUpcomingBirthdays({
  1489 + storeId,
  1490 + showSolar: true,
  1491 + showLunar: true,
  1492 + startDate,
  1493 + endDate
  1494 + })
  1495 + const apiData = res.data || {}
  1496 + const items = apiData.data || apiData || []
  1497 + const list = (Array.isArray(items) ? items : []).map(item => {
  1498 + const fullDate = item.birthdayFullDate
  1499 + let month = 0
  1500 + let day = 0
  1501 + if (fullDate) {
  1502 + const d = new Date(fullDate)
  1503 + if (!isNaN(d.getTime())) {
  1504 + month = d.getMonth() + 1
  1505 + day = d.getDate()
  1506 + }
  1507 + }
  1508 + return {
  1509 + id: item.id,
  1510 + name: item.khmc || '',
  1511 + phone: item.sjh || '',
  1512 + level: item.consumeLevelName || '',
  1513 + month,
  1514 + day,
  1515 + totalAmount: item.remainingRightsAmount != null ? Number(item.remainingRightsAmount).toLocaleString('zh-CN', { maximumFractionDigits: 0 }) : '0',
  1516 + birthdayType: item.birthdayType,
  1517 + yinlsr: item.yinlsr || '',
  1518 + yanglsr: item.yanglsr || '',
  1519 + age: item.age,
  1520 + gsmdName: item.gsmdName || '',
  1521 + bz: item.bz || ''
  1522 + }
  1523 + })
  1524 + this.birthdayAllData = list.filter(m => m.month > 0 && m.day > 0)
  1525 + } catch (e) {
  1526 + console.error('[生日提醒]', e.message || e)
  1527 + } finally {
  1528 + this.birthdayLoading = false
  1529 + }
  1530 + },
1488 1531 handleSleepingCall(item) {
1489 1532 this.activeTodoId = ''
1490 1533 this.currentContactCustomer = {
... ... @@ -1494,6 +1537,61 @@ export default {
1494 1537 }
1495 1538 this.contactDialogVisible = true
1496 1539 },
  1540 + async loadSleepingMembers(reset = false) {
  1541 + const storeId = this.resolveStoreId()
  1542 + if (!storeId) return
  1543 + if (reset) {
  1544 + this.sleepingPage = 1
  1545 + this.sleepingMembers = []
  1546 + this.sleepingTotal = 0
  1547 + }
  1548 + if (this.sleepingLoading) return
  1549 + this.sleepingLoading = true
  1550 + try {
  1551 + const res = await getSleepingMembers({
  1552 + storeId,
  1553 + page: this.sleepingPage,
  1554 + pageSize: this.sleepingPageSize
  1555 + })
  1556 + const raw = res.data || res || {}
  1557 + const items = raw.Items || raw.items || []
  1558 + const total = raw.Total ?? raw.total ?? 0
  1559 + const list = (Array.isArray(items) ? items : []).map(item => ({
  1560 + id: item.Id || item.id || '',
  1561 + name: item.Name || item.name || '',
  1562 + days: item.Days || item.days || 0,
  1563 + phone: item.Phone || item.phone || '',
  1564 + level: item.Level || item.level || '',
  1565 + lastVisit: (() => {
  1566 + const lv = item.LastVisit || item.lastVisit
  1567 + if (!lv) return '--'
  1568 + const d = new Date(typeof lv === 'number' ? lv : lv)
  1569 + if (isNaN(d.getTime())) return '--'
  1570 + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
  1571 + })(),
  1572 + balance: item.Balance || item.balance || 0
  1573 + }))
  1574 + if (reset) {
  1575 + this.sleepingMembers = list
  1576 + } else {
  1577 + this.sleepingMembers = [...this.sleepingMembers, ...list]
  1578 + }
  1579 + this.sleepingTotal = total > 0 ? total : this.sleepingMembers.length
  1580 + } catch (e) {
  1581 + console.error('[沉睡提醒] 报错:', e.message || e)
  1582 + } finally {
  1583 + this.sleepingLoading = false
  1584 + }
  1585 + },
  1586 + handleSleepingScroll(e) {
  1587 + const el = e.target
  1588 + if (el.scrollTop + el.clientHeight >= el.scrollHeight - 50) {
  1589 + if (this.sleepingMembers.length < this.sleepingTotal && !this.sleepingLoading) {
  1590 + this.sleepingPage++
  1591 + this.loadSleepingMembers()
  1592 + }
  1593 + }
  1594 + },
1497 1595 handleContactCancel() {
1498 1596 this.activeTodoId = ''
1499 1597 },
... ... @@ -1650,6 +1748,7 @@ export default {
1650 1748 } else {
1651 1749 this.birthdayMonth--
1652 1750 }
  1751 + this.loadBirthdayData()
1653 1752 },
1654 1753 nextBirthdayMonth() {
1655 1754 this.selectedBirthdayDay = null
... ... @@ -1659,6 +1758,7 @@ export default {
1659 1758 } else {
1660 1759 this.birthdayMonth++
1661 1760 }
  1761 + this.loadBirthdayData()
1662 1762 },
1663 1763 levelDotColor(level) {
1664 1764 const map = { 'A++': '#ef4444', 'A+': '#f59e0b', 'A': '#d97706', 'B': '#22c55e', 'C': '#3b82f6', 'D': '#94a3b8' }
... ... @@ -2595,6 +2695,8 @@ export default {
2595 2695 flex: 0 0 400px;
2596 2696 position: relative;
2597 2697 z-index: 1;
  2698 + min-height: 0;
  2699 + height: 100%;
2598 2700 }
2599 2701  
2600 2702 /* 提醒面板布局:独立四角圆角卡片 */
... ... @@ -2654,10 +2756,15 @@ export default {
2654 2756 flex: 1;
2655 2757 min-height: 0;
2656 2758 overflow: hidden;
  2759 + position: relative;
2657 2760 }
2658 2761  
2659 2762 .reminder-center-panel .today-card ::v-deep .el-tab-pane {
2660   - height: 100%;
  2763 + position: absolute;
  2764 + top: 0;
  2765 + left: 0;
  2766 + right: 0;
  2767 + bottom: 0;
2661 2768 display: flex;
2662 2769 flex-direction: column;
2663 2770 }
... ...
项目文档相关/sql/2026-06-16/公司日历菜单配置.sql 0 → 100644
  1 +-- 公司日历菜单配置脚本
  2 +
  3 +SET @AdminRoleId = '94e3a9bb0fce4547886972998fddba1c'; -- 系统管理员角色ID
  4 +
  5 +-- 1. 清理旧数据 (防止重复执行报错)
  6 +DELETE FROM BASE_MODULE WHERE F_Id IN ('company-calendar', 'company-calendar-type');
  7 +DELETE FROM BASE_AUTHORIZE WHERE F_ItemId IN ('company-calendar', 'company-calendar-type');
  8 +
  9 +-- 2. 创建菜单: 公司日历 (父级: 扩展功能 725873504657868037)
  10 +-- 请根据实际的父级菜单ID修改 F_ParentId
  11 +INSERT INTO BASE_MODULE (F_Id, F_ParentId, F_Type, F_FullName, F_EnCode, F_UrlAddress, F_Icon, F_SortCode, F_EnabledMark, F_Category, F_DeleteMark, F_LinkTarget, F_PropertyJson, F_IsButtonAuthorize, F_IsColumnAuthorize, F_IsDataAuthorize, F_IsFormAuthorize, F_CreatorTime)
  12 +VALUES
  13 +('company-calendar', '725873504657868037', 2, '公司日历', 'companyCalendar', 'extend/companyCalendar', 'icon-ym icon-ym-calendar', 12, 1, 'Web', NULL, '_self', '{"moduleId":"","iconBackgroundColor":"","isTree":0}', 1, 1, 1, 1, NOW());
  14 +
  15 +-- 3. 授权给 系统管理员 角色
  16 +INSERT INTO BASE_AUTHORIZE (F_Id, F_ItemType, F_ItemId, F_ObjectType, F_ObjectId, F_SortCode, F_CreatorTime, F_CreatorUserId)
  17 +VALUES
  18 +(REPLACE(UUID(), '-', ''), 'module', 'company-calendar', 'Role', @AdminRoleId, 1, NOW(), 'admin');
... ...
项目文档相关/sql/2026-06-16/创建公司日历事件类型表.sql 0 → 100644
  1 +-- 创建公司日历事件类型配置表
  2 +CREATE TABLE IF NOT EXISTS `lq_company_calendar_type` (
  3 + `F_Id` varchar(50) NOT NULL COMMENT '主键ID',
  4 + `TypeName` varchar(50) NOT NULL COMMENT '类型名称',
  5 + `TypeCode` varchar(50) NOT NULL COMMENT '类型编码',
  6 + `Color` varchar(20) NOT NULL DEFAULT '#3b82f6' COMMENT '颜色',
  7 + `SortCode` int NOT NULL DEFAULT 0 COMMENT '排序码',
  8 + `IsEffective` int NOT NULL DEFAULT 1 COMMENT '是否有效:1-有效, 0-无效',
  9 + `CreateUser` varchar(50) DEFAULT NULL COMMENT '创建人',
  10 + `CreateTime` datetime DEFAULT NULL COMMENT '创建时间',
  11 + `UpdateUser` varchar(50) DEFAULT NULL COMMENT '修改人',
  12 + `UpdateTime` datetime DEFAULT NULL COMMENT '修改时间',
  13 + PRIMARY KEY (`F_Id`),
  14 + UNIQUE KEY `uk_type_code` (`TypeCode`)
  15 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='公司日历事件类型配置表';
  16 +
  17 +-- 插入默认事件类型
  18 +INSERT INTO `lq_company_calendar_type` (`F_Id`, `TypeName`, `TypeCode`, `Color`, `SortCode`, `IsEffective`, `CreateTime`) VALUES
  19 +('1', '操作会', 'ops', '#6366f1', 1, 1, NOW()),
  20 +('2', '培训会', 'training', '#22c55e', 2, 1, NOW()),
  21 +('3', '全体大会', 'meeting', '#f97316', 3, 1, NOW()),
  22 +('4', '门店活动', 'activity', '#ec4899', 4, 1, NOW());
... ...
项目文档相关/sql/2026-06-16/创建公司日历表.sql 0 → 100644
  1 +-- 创建公司日历事件表
  2 +CREATE TABLE IF NOT EXISTS `lq_company_calendar` (
  3 + `F_Id` varchar(50) NOT NULL COMMENT '主键ID',
  4 + `Title` varchar(200) NOT NULL COMMENT '事件标题',
  5 + `EventType` varchar(50) NOT NULL COMMENT '事件类型ID',
  6 + `StartTime` datetime NOT NULL COMMENT '开始时间',
  7 + `EndTime` datetime NOT NULL COMMENT '结束时间',
  8 + `Description` text DEFAULT NULL COMMENT '事件描述',
  9 + `StoreId` varchar(50) DEFAULT NULL COMMENT '门店ID',
  10 + `StoreName` varchar(100) DEFAULT NULL COMMENT '门店名称',
  11 + `Remark` text DEFAULT NULL COMMENT '备注',
  12 + `Attachments` text DEFAULT NULL COMMENT '附件JSON',
  13 + `IsEffective` int NOT NULL DEFAULT 1 COMMENT '是否有效:1-有效, 0-无效',
  14 + `CreateUser` varchar(50) DEFAULT NULL COMMENT '创建人',
  15 + `CreateTime` datetime DEFAULT NULL COMMENT '创建时间',
  16 + `UpdateUser` varchar(50) DEFAULT NULL COMMENT '修改人',
  17 + `UpdateTime` datetime DEFAULT NULL COMMENT '修改时间',
  18 + PRIMARY KEY (`F_Id`),
  19 + KEY `idx_event_type` (`EventType`),
  20 + KEY `idx_start_time` (`StartTime`),
  21 + KEY `idx_end_time` (`EndTime`),
  22 + KEY `idx_store_id` (`StoreId`),
  23 + KEY `idx_is_effective` (`IsEffective`)
  24 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='公司日历事件表';
  25 +
  26 +-- 创建公司日历事件类型配置表
  27 +CREATE TABLE IF NOT EXISTS `lq_company_calendar_type` (
  28 + `F_Id` varchar(50) NOT NULL COMMENT '主键ID',
  29 + `TypeName` varchar(50) NOT NULL COMMENT '类型名称',
  30 + `TypeCode` varchar(50) NOT NULL COMMENT '类型编码',
  31 + `Color` varchar(20) NOT NULL DEFAULT '#3b82f6' COMMENT '颜色',
  32 + `SortCode` int NOT NULL DEFAULT 0 COMMENT '排序码',
  33 + `IsEffective` int NOT NULL DEFAULT 1 COMMENT '是否有效:1-有效, 0-无效',
  34 + `CreateUser` varchar(50) DEFAULT NULL COMMENT '创建人',
  35 + `CreateTime` datetime DEFAULT NULL COMMENT '创建时间',
  36 + `UpdateUser` varchar(50) DEFAULT NULL COMMENT '修改人',
  37 + `UpdateTime` datetime DEFAULT NULL COMMENT '修改时间',
  38 + PRIMARY KEY (`F_Id`),
  39 + UNIQUE KEY `uk_type_code` (`TypeCode`)
  40 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='公司日历事件类型配置表';
  41 +
  42 +-- 插入默认事件类型
  43 +INSERT INTO `lq_company_calendar_type` (`F_Id`, `TypeName`, `TypeCode`, `Color`, `SortCode`, `IsEffective`, `CreateTime`) VALUES
  44 +('1', '操作会', 'ops', '#6366f1', 1, 1, NOW()),
  45 +('2', '培训会', 'training', '#22c55e', 2, 1, NOW()),
  46 +('3', '全体大会', 'meeting', '#f97316', 3, 1, NOW()),
  47 +('4', '门店活动', 'activity', '#ec4899', 4, 1, NOW());
... ...