Commit eba41c1ba40e832ce36268f30dede7f88ea47ccd
1 parent
c4040263
fix: 修复小程序登录闪烁、耗卡跨月策略与开单顾客类型兜底
- 登录页设为启动页并统一 redirectToLogin,避免多接口 600 连续 reLaunch - 小程序耗卡可编辑天数/跨月策略与后台配置对齐 - 开单 gjlx 自动补全与台账顾客类型展示兜底 - 系统配置页拆分面板、短信日志抽屉样式优化 Co-authored-by: Cursor <cursoragent@cursor.com>
Showing
30 changed files
with
2377 additions
and
989 deletions
antis-ncc-admin/src/utils/consume-admin-policy.js
| @@ -41,6 +41,7 @@ export function normalizeBusinessConfigFields(form) { | @@ -41,6 +41,7 @@ export function normalizeBusinessConfigFields(form) { | ||
| 41 | } | 41 | } |
| 42 | 42 | ||
| 43 | intField('consumeEditableDays', 3) | 43 | intField('consumeEditableDays', 3) |
| 44 | + intField('consumeAllowCrossMonthEdit', 0) | ||
| 44 | intField('adminConsumeVoidEditableDays', 0) | 45 | intField('adminConsumeVoidEditableDays', 0) |
| 45 | intField('adminConsumeDeleteEditableDays', 0) | 46 | intField('adminConsumeDeleteEditableDays', 0) |
| 46 | intField('sleepRemindDays', 30) | 47 | intField('sleepRemindDays', 30) |
antis-ncc-admin/src/views/system/sysConfig/ai-config-panel.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <ConfigPanelShell | ||
| 3 | + :modules="modules" | ||
| 4 | + :loading="loading" | ||
| 5 | + :saving="saving" | ||
| 6 | + storage-key="aiConfigActiveModule" | ||
| 7 | + alert-title="通义千问(DashScope)语音识别与文本总结配置;语音与对话可使用不同密钥。" | ||
| 8 | + alert-type="info" | ||
| 9 | + @save="$emit('save')" | ||
| 10 | + > | ||
| 11 | + <template #speech> | ||
| 12 | + <el-card shadow="never" class="config-section"> | ||
| 13 | + <div slot="header" class="config-section__header"> | ||
| 14 | + <span>语音识别</span> | ||
| 15 | + <span class="config-section__tag">Paraformer 异步转写</span> | ||
| 16 | + </div> | ||
| 17 | + <p class="config-section__tip"> | ||
| 18 | + 录音文件识别专用;留空地址则使用官方默认接口。 | ||
| 19 | + <strong>禁止</strong>填写对话用的 <code>compatible-mode/v1</code>。 | ||
| 20 | + </p> | ||
| 21 | + <p class="config-section__tip config-section__tip--code"> | ||
| 22 | + 默认地址:<code>{{ defaultParaformerSubmitUrl }}</code> | ||
| 23 | + </p> | ||
| 24 | + <el-form-item label="语音服务地址" prop="aiSpeechApiUrl"> | ||
| 25 | + <el-input v-model="form.aiSpeechApiUrl" clearable | ||
| 26 | + placeholder="留空即可;请勿填 compatible-mode/v1(那是对话网关)" /> | ||
| 27 | + </el-form-item> | ||
| 28 | + <el-form-item label="语音 API Key" prop="aiSpeechApiKey"> | ||
| 29 | + <el-input v-model="form.aiSpeechApiKey" show-password clearable autocomplete="new-password" | ||
| 30 | + placeholder="语音识别使用的 API Key" /> | ||
| 31 | + </el-form-item> | ||
| 32 | + <el-form-item label="语音识别模型" prop="aiSpeechModel"> | ||
| 33 | + <el-input v-model="form.aiSpeechModel" clearable | ||
| 34 | + placeholder="paraformer-v2 或 paraformer-8k-v2" /> | ||
| 35 | + </el-form-item> | ||
| 36 | + <p class="config-section__tip config-section__tip--inline"> | ||
| 37 | + 与百炼「录音文件识别」的 model 一致;留空默认 <code>paraformer-v2</code>。禁止填 qwen / livetranslate / realtime 类模型。 | ||
| 38 | + </p> | ||
| 39 | + </el-card> | ||
| 40 | + </template> | ||
| 41 | + | ||
| 42 | + <template #chat> | ||
| 43 | + <el-card shadow="never" class="config-section"> | ||
| 44 | + <div slot="header" class="config-section__header"> | ||
| 45 | + <span>对话与文本总结</span> | ||
| 46 | + <span class="config-section__tag">OpenAI 兼容模式</span> | ||
| 47 | + </div> | ||
| 48 | + <p class="config-section__tip">用于文案总结等场景,需与模型名称搭配(如兼容模式接入千问)。</p> | ||
| 49 | + <el-form-item label="对话接口地址" prop="aiChatApiUrl"> | ||
| 50 | + <el-input v-model="form.aiChatApiUrl" clearable | ||
| 51 | + placeholder="示例:https://dashscope.aliyuncs.com/compatible-mode/v1" /> | ||
| 52 | + </el-form-item> | ||
| 53 | + <el-form-item label="模型名称" prop="aiChatModel"> | ||
| 54 | + <el-input v-model="form.aiChatModel" clearable placeholder="如 qwen-plus、qwen-turbo" /> | ||
| 55 | + </el-form-item> | ||
| 56 | + <el-form-item label="文本 API Key" prop="aiChatApiKey"> | ||
| 57 | + <el-input v-model="form.aiChatApiKey" show-password clearable autocomplete="new-password" | ||
| 58 | + placeholder="可与语音使用同一密钥或单独的子 Key" /> | ||
| 59 | + </el-form-item> | ||
| 60 | + </el-card> | ||
| 61 | + </template> | ||
| 62 | + | ||
| 63 | + <template #prompt> | ||
| 64 | + <el-card shadow="never" class="config-section"> | ||
| 65 | + <div slot="header" class="config-section__header"> | ||
| 66 | + <span>备注归纳提示词</span> | ||
| 67 | + <span class="config-section__tag">system 角色</span> | ||
| 68 | + </div> | ||
| 69 | + <p class="config-section__tip"> | ||
| 70 | + 发给千问的 system 角色,用来约束「删掉废话、保留关键」。留空则使用系统内置默认(美业健康师备注风格)。 | ||
| 71 | + </p> | ||
| 72 | + <el-form-item label="归纳提示词" prop="aiSummarizeSystemPrompt"> | ||
| 73 | + <el-input v-model="form.aiSummarizeSystemPrompt" type="textarea" | ||
| 74 | + :autosize="{ minRows: 8, maxRows: 20 }" | ||
| 75 | + placeholder="例如:你是一名资深美业咨询师,请将下方口语转写整理为一句专业、可追溯的工单备注……" /> | ||
| 76 | + </el-form-item> | ||
| 77 | + </el-card> | ||
| 78 | + </template> | ||
| 79 | + </ConfigPanelShell> | ||
| 80 | +</template> | ||
| 81 | + | ||
| 82 | +<script> | ||
| 83 | +import ConfigPanelShell from './config-panel-shell.vue' | ||
| 84 | + | ||
| 85 | +const MODULES = [ | ||
| 86 | + { key: 'speech', label: '语音识别', desc: 'Paraformer 录音转写', icon: 'el-icon-microphone', iconClass: 'module-icon--speech' }, | ||
| 87 | + { key: 'chat', label: '对话总结', desc: '千问兼容模式接口', icon: 'el-icon-chat-dot-round', iconClass: 'module-icon--chat' }, | ||
| 88 | + { key: 'prompt', label: '归纳提示词', desc: '备注整理 system 提示', icon: 'el-icon-edit-outline', iconClass: 'module-icon--prompt' } | ||
| 89 | +] | ||
| 90 | + | ||
| 91 | +export default { | ||
| 92 | + name: 'AiConfigPanel', | ||
| 93 | + components: { ConfigPanelShell }, | ||
| 94 | + props: { | ||
| 95 | + form: { type: Object, required: true }, | ||
| 96 | + loading: { type: Boolean, default: false }, | ||
| 97 | + saving: { type: Boolean, default: false }, | ||
| 98 | + defaultParaformerSubmitUrl: { type: String, default: '' } | ||
| 99 | + }, | ||
| 100 | + data() { | ||
| 101 | + return { modules: MODULES } | ||
| 102 | + } | ||
| 103 | +} | ||
| 104 | +</script> | ||
| 105 | + | ||
| 106 | +<style lang="scss" scoped> | ||
| 107 | +.config-section { | ||
| 108 | + border: 1px solid #ebeef5; | ||
| 109 | + | ||
| 110 | + &__header { | ||
| 111 | + display: flex; | ||
| 112 | + align-items: center; | ||
| 113 | + gap: 8px; | ||
| 114 | + font-weight: 500; | ||
| 115 | + } | ||
| 116 | + | ||
| 117 | + &__tag { | ||
| 118 | + padding: 0 8px; | ||
| 119 | + height: 22px; | ||
| 120 | + line-height: 22px; | ||
| 121 | + border-radius: 4px; | ||
| 122 | + background: #f4f4f5; | ||
| 123 | + color: #909399; | ||
| 124 | + font-size: 12px; | ||
| 125 | + font-weight: normal; | ||
| 126 | + } | ||
| 127 | + | ||
| 128 | + &__tip { | ||
| 129 | + margin: 0 0 12px; | ||
| 130 | + color: #909399; | ||
| 131 | + font-size: 13px; | ||
| 132 | + line-height: 1.6; | ||
| 133 | + | ||
| 134 | + &--inline { | ||
| 135 | + margin-top: 0; | ||
| 136 | + } | ||
| 137 | + | ||
| 138 | + &--code { | ||
| 139 | + margin-top: -4px; | ||
| 140 | + } | ||
| 141 | + | ||
| 142 | + code { | ||
| 143 | + padding: 1px 4px; | ||
| 144 | + border-radius: 3px; | ||
| 145 | + background: #f4f4f5; | ||
| 146 | + font-size: 12px; | ||
| 147 | + } | ||
| 148 | + } | ||
| 149 | +} | ||
| 150 | + | ||
| 151 | +.module-icon--speech { color: #409EFF; } | ||
| 152 | +.module-icon--chat { color: #67C23A; } | ||
| 153 | +.module-icon--prompt { color: #E6A23C; } | ||
| 154 | + | ||
| 155 | +.ncc-content-dark .config-section { | ||
| 156 | + border-color: rgba(255, 255, 255, 0.08); | ||
| 157 | + | ||
| 158 | + &__tag { | ||
| 159 | + background: rgba(255, 255, 255, 0.08); | ||
| 160 | + color: rgba(255, 255, 255, 0.55); | ||
| 161 | + } | ||
| 162 | + | ||
| 163 | + &__tip { | ||
| 164 | + color: rgba(255, 255, 255, 0.55); | ||
| 165 | + | ||
| 166 | + code { | ||
| 167 | + background: rgba(255, 255, 255, 0.08); | ||
| 168 | + } | ||
| 169 | + } | ||
| 170 | +} | ||
| 171 | +</style> |
antis-ncc-admin/src/views/system/sysConfig/basic-config-panel.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <ConfigPanelShell | ||
| 3 | + :modules="modules" | ||
| 4 | + :loading="loading" | ||
| 5 | + :saving="saving" | ||
| 6 | + storage-key="basicConfigActiveModule" | ||
| 7 | + alert-title="配置系统品牌标识与公司基础信息;保存后全站生效。" | ||
| 8 | + @save="$emit('save')" | ||
| 9 | + > | ||
| 10 | + <template #brand> | ||
| 11 | + <el-card shadow="never" class="config-section"> | ||
| 12 | + <div slot="header" class="config-section__header"> | ||
| 13 | + <span>品牌标识</span> | ||
| 14 | + <span class="config-section__tag">侧栏 / 浏览器标题</span> | ||
| 15 | + </div> | ||
| 16 | + <p class="config-section__tip">上传系统 Logo 与折叠图标,用于管理后台侧栏与页面标题展示。</p> | ||
| 17 | + <el-row :gutter="24" class="brand-row"> | ||
| 18 | + <el-col :xs="24" :sm="12"> | ||
| 19 | + <el-form-item label="系统 Logo"> | ||
| 20 | + <div class="img-uploader" v-loading="logoUploading"> | ||
| 21 | + <el-upload class="single-uploader" :show-file-list="false" action="#" accept="image/*" | ||
| 22 | + :http-request="onUploadLogo"> | ||
| 23 | + <img v-if="form.sysLogo" :src="logoPreview(form.sysLogo)" class="single-img"> | ||
| 24 | + <i v-else class="el-icon-plus single-icon"></i> | ||
| 25 | + </el-upload> | ||
| 26 | + <el-button v-if="form.sysLogo" type="text" class="NCC-table-delBtn" @click="form.sysLogo = ''">移除</el-button> | ||
| 27 | + </div> | ||
| 28 | + <p class="form-tip">侧栏展开时显示,建议 32×32 或等比透明图</p> | ||
| 29 | + </el-form-item> | ||
| 30 | + </el-col> | ||
| 31 | + <el-col :xs="24" :sm="12"> | ||
| 32 | + <el-form-item label="折叠图标"> | ||
| 33 | + <div class="img-uploader" v-loading="logoIconUploading"> | ||
| 34 | + <el-upload class="single-uploader" :show-file-list="false" action="#" accept="image/*" | ||
| 35 | + :http-request="onUploadLogoIcon"> | ||
| 36 | + <img v-if="form.sysLogoIcon" :src="logoPreview(form.sysLogoIcon)" class="single-img"> | ||
| 37 | + <i v-else class="el-icon-plus single-icon"></i> | ||
| 38 | + </el-upload> | ||
| 39 | + <el-button v-if="form.sysLogoIcon" type="text" class="NCC-table-delBtn" | ||
| 40 | + @click="form.sysLogoIcon = ''">移除</el-button> | ||
| 41 | + </div> | ||
| 42 | + <p class="form-tip">可选;侧栏折叠时显示,不传则使用系统 Logo</p> | ||
| 43 | + </el-form-item> | ||
| 44 | + </el-col> | ||
| 45 | + </el-row> | ||
| 46 | + <el-row :gutter="20"> | ||
| 47 | + <el-col :span="12"> | ||
| 48 | + <el-form-item label="系统名称" prop="sysName"> | ||
| 49 | + <el-input v-model="form.sysName" clearable placeholder="系统名称" /> | ||
| 50 | + </el-form-item> | ||
| 51 | + </el-col> | ||
| 52 | + <el-col :span="12"> | ||
| 53 | + <el-form-item label="系统版本" prop="sysVersion"> | ||
| 54 | + <el-input v-model="form.sysVersion" clearable placeholder="系统版本" /> | ||
| 55 | + </el-form-item> | ||
| 56 | + </el-col> | ||
| 57 | + </el-row> | ||
| 58 | + </el-card> | ||
| 59 | + </template> | ||
| 60 | + | ||
| 61 | + <template #company> | ||
| 62 | + <el-card shadow="never" class="config-section"> | ||
| 63 | + <div slot="header" class="config-section__header"> | ||
| 64 | + <span>公司信息</span> | ||
| 65 | + <span class="config-section__tag">版权与联系</span> | ||
| 66 | + </div> | ||
| 67 | + <p class="config-section__tip">用于登录页、页脚版权及系统描述展示。</p> | ||
| 68 | + <el-row :gutter="20"> | ||
| 69 | + <el-col :span="12"> | ||
| 70 | + <el-form-item label="公司名称" prop="companyName"> | ||
| 71 | + <el-input v-model="form.companyName" clearable placeholder="公司名称" /> | ||
| 72 | + </el-form-item> | ||
| 73 | + </el-col> | ||
| 74 | + <el-col :span="12"> | ||
| 75 | + <el-form-item label="公司简称" prop="companyCode"> | ||
| 76 | + <el-input v-model="form.companyCode" clearable placeholder="公司简称" /> | ||
| 77 | + </el-form-item> | ||
| 78 | + </el-col> | ||
| 79 | + <el-col :span="12"> | ||
| 80 | + <el-form-item label="版权信息" prop="copyright"> | ||
| 81 | + <el-input v-model="form.copyright" clearable placeholder="版权信息" /> | ||
| 82 | + </el-form-item> | ||
| 83 | + </el-col> | ||
| 84 | + <el-col :span="12"> | ||
| 85 | + <el-form-item label="公司法人" prop="companyContacts"> | ||
| 86 | + <el-input v-model="form.companyContacts" clearable placeholder="公司法人" /> | ||
| 87 | + </el-form-item> | ||
| 88 | + </el-col> | ||
| 89 | + <el-col :span="12"> | ||
| 90 | + <el-form-item label="公司电话" prop="companyTelePhone"> | ||
| 91 | + <el-input v-model="form.companyTelePhone" clearable placeholder="公司电话" /> | ||
| 92 | + </el-form-item> | ||
| 93 | + </el-col> | ||
| 94 | + <el-col :span="12"> | ||
| 95 | + <el-form-item label="公司邮箱" prop="companyEmail"> | ||
| 96 | + <el-input v-model="form.companyEmail" clearable placeholder="公司邮箱" /> | ||
| 97 | + </el-form-item> | ||
| 98 | + </el-col> | ||
| 99 | + <el-col :span="24"> | ||
| 100 | + <el-form-item label="公司地址" prop="companyAddress"> | ||
| 101 | + <el-input v-model="form.companyAddress" clearable placeholder="公司地址" /> | ||
| 102 | + </el-form-item> | ||
| 103 | + </el-col> | ||
| 104 | + <el-col :span="24"> | ||
| 105 | + <el-form-item label="系统描述" prop="sysDescription"> | ||
| 106 | + <el-input v-model="form.sysDescription" type="textarea" :autosize="{ minRows: 4, maxRows: 10 }" | ||
| 107 | + placeholder="系统描述" /> | ||
| 108 | + </el-form-item> | ||
| 109 | + </el-col> | ||
| 110 | + </el-row> | ||
| 111 | + </el-card> | ||
| 112 | + </template> | ||
| 113 | + </ConfigPanelShell> | ||
| 114 | +</template> | ||
| 115 | + | ||
| 116 | +<script> | ||
| 117 | +import ConfigPanelShell from './config-panel-shell.vue' | ||
| 118 | + | ||
| 119 | +const MODULES = [ | ||
| 120 | + { key: 'brand', label: '品牌标识', desc: 'Logo、系统名称与版本', icon: 'el-icon-picture-outline', iconClass: 'module-icon--brand' }, | ||
| 121 | + { key: 'company', label: '公司信息', desc: '版权、联系人与描述', icon: 'el-icon-office-building', iconClass: 'module-icon--company' } | ||
| 122 | +] | ||
| 123 | + | ||
| 124 | +export default { | ||
| 125 | + name: 'BasicConfigPanel', | ||
| 126 | + components: { ConfigPanelShell }, | ||
| 127 | + props: { | ||
| 128 | + form: { type: Object, required: true }, | ||
| 129 | + loading: { type: Boolean, default: false }, | ||
| 130 | + saving: { type: Boolean, default: false }, | ||
| 131 | + logoUploading: { type: Boolean, default: false }, | ||
| 132 | + logoIconUploading: { type: Boolean, default: false }, | ||
| 133 | + logoPreview: { type: Function, required: true }, | ||
| 134 | + onUploadLogo: { type: Function, required: true }, | ||
| 135 | + onUploadLogoIcon: { type: Function, required: true } | ||
| 136 | + }, | ||
| 137 | + data() { | ||
| 138 | + return { modules: MODULES } | ||
| 139 | + } | ||
| 140 | +} | ||
| 141 | +</script> | ||
| 142 | + | ||
| 143 | +<style lang="scss" scoped> | ||
| 144 | +.config-section { | ||
| 145 | + border: 1px solid #ebeef5; | ||
| 146 | + | ||
| 147 | + &__header { | ||
| 148 | + display: flex; | ||
| 149 | + align-items: center; | ||
| 150 | + gap: 8px; | ||
| 151 | + font-weight: 500; | ||
| 152 | + } | ||
| 153 | + | ||
| 154 | + &__tag { | ||
| 155 | + padding: 0 8px; | ||
| 156 | + height: 22px; | ||
| 157 | + line-height: 22px; | ||
| 158 | + border-radius: 4px; | ||
| 159 | + background: #f4f4f5; | ||
| 160 | + color: #909399; | ||
| 161 | + font-size: 12px; | ||
| 162 | + font-weight: normal; | ||
| 163 | + } | ||
| 164 | + | ||
| 165 | + &__tip, | ||
| 166 | + .form-tip { | ||
| 167 | + margin: 0 0 12px; | ||
| 168 | + color: #909399; | ||
| 169 | + font-size: 13px; | ||
| 170 | + line-height: 1.6; | ||
| 171 | + } | ||
| 172 | +} | ||
| 173 | + | ||
| 174 | +.module-icon--brand { color: #409EFF; } | ||
| 175 | +.module-icon--company { color: #67C23A; } | ||
| 176 | + | ||
| 177 | +.brand-row .form-tip { | ||
| 178 | + margin-top: 8px; | ||
| 179 | + margin-bottom: 0; | ||
| 180 | +} | ||
| 181 | + | ||
| 182 | +.img-uploader { | ||
| 183 | + display: flex; | ||
| 184 | + align-items: center; | ||
| 185 | + gap: 12px; | ||
| 186 | +} | ||
| 187 | + | ||
| 188 | +.single-uploader ::v-deep .el-upload { | ||
| 189 | + border: 1px dashed #d9d9d9; | ||
| 190 | + border-radius: 6px; | ||
| 191 | + cursor: pointer; | ||
| 192 | + width: 80px; | ||
| 193 | + height: 80px; | ||
| 194 | + display: flex; | ||
| 195 | + align-items: center; | ||
| 196 | + justify-content: center; | ||
| 197 | + overflow: hidden; | ||
| 198 | + | ||
| 199 | + &:hover { | ||
| 200 | + border-color: #409EFF; | ||
| 201 | + } | ||
| 202 | +} | ||
| 203 | + | ||
| 204 | +.single-icon { | ||
| 205 | + font-size: 22px; | ||
| 206 | + color: #8c939d; | ||
| 207 | +} | ||
| 208 | + | ||
| 209 | +.single-img { | ||
| 210 | + width: 80px; | ||
| 211 | + height: 80px; | ||
| 212 | + object-fit: contain; | ||
| 213 | + display: block; | ||
| 214 | +} | ||
| 215 | + | ||
| 216 | +.ncc-content-dark { | ||
| 217 | + .config-section { | ||
| 218 | + border-color: rgba(255, 255, 255, 0.08); | ||
| 219 | + | ||
| 220 | + &__tag, | ||
| 221 | + &__tip, | ||
| 222 | + .form-tip { | ||
| 223 | + color: rgba(255, 255, 255, 0.55); | ||
| 224 | + } | ||
| 225 | + | ||
| 226 | + &__tag { | ||
| 227 | + background: rgba(255, 255, 255, 0.08); | ||
| 228 | + } | ||
| 229 | + } | ||
| 230 | + | ||
| 231 | + .single-uploader ::v-deep .el-upload { | ||
| 232 | + background-color: #262626; | ||
| 233 | + border-color: rgba(255, 255, 255, 0.18); | ||
| 234 | + } | ||
| 235 | + | ||
| 236 | + .single-icon { | ||
| 237 | + color: rgba(255, 255, 255, 0.55); | ||
| 238 | + } | ||
| 239 | +} | ||
| 240 | +</style> |
antis-ncc-admin/src/views/system/sysConfig/business-config-panel.vue
| 1 | <template> | 1 | <template> |
| 2 | - <div class="business-config-panel" v-loading="loading"> | ||
| 3 | - <div class="business-config-panel__header"> | ||
| 4 | - <el-alert title="按业务模块配置规则;后续新增配置项将在此页分组展示。" type="info" :closable="false" show-icon /> | ||
| 5 | - <el-button type="primary" size="small" :loading="saving" class="business-config-panel__save" | ||
| 6 | - @click="$emit('save')">保 存</el-button> | ||
| 7 | - </div> | ||
| 8 | - | ||
| 9 | - <el-collapse v-model="activeModules" class="business-config-panel__collapse"> | ||
| 10 | - <el-collapse-item name="consume"> | ||
| 11 | - <template slot="title"> | ||
| 12 | - <span class="module-title"> | ||
| 13 | - <i class="el-icon-s-order module-icon module-icon--consume"></i> | ||
| 14 | - 耗卡管理 | ||
| 15 | - </span> | 2 | + <ConfigPanelShell |
| 3 | + :modules="modules" | ||
| 4 | + :loading="loading" | ||
| 5 | + :saving="saving" | ||
| 6 | + storage-key="businessConfigActiveModule" | ||
| 7 | + default-module="consume" | ||
| 8 | + alert-title="左侧选择业务模块,右侧仅展示当前模块配置;保存后全量生效。" | ||
| 9 | + @save="$emit('save')" | ||
| 10 | + > | ||
| 11 | + <template #consume> | ||
| 12 | + <el-card shadow="never" class="config-section"> | ||
| 13 | + <div slot="header" class="config-section__header"> | ||
| 14 | + <span>小程序端</span> | ||
| 15 | + <span class="config-section__tag">门店小程序</span> | ||
| 16 | + </div> | ||
| 17 | + <p class="config-section__tip"> | ||
| 18 | + 耗卡发生后,超过「可修改天数」将禁止「修改 / 作废 / 重开单」;关闭「允许跨月修改」时,耗卡月份与当前月不同亦禁止操作(即使仍在天数内)。 | ||
| 19 | + </p> | ||
| 20 | + <el-row :gutter="20"> | ||
| 21 | + <el-col :span="12"> | ||
| 22 | + <el-form-item label="可修改天数" prop="consumeEditableDays" label-width="100px"> | ||
| 23 | + <el-input-number v-model="form.consumeEditableDays" :min="1" :max="365" :precision="0" :step="1" | ||
| 24 | + controls-position="right" /> | ||
| 25 | + <span class="field-suffix">天</span> | ||
| 26 | + </el-form-item> | ||
| 27 | + </el-col> | ||
| 28 | + <el-col :span="12"> | ||
| 29 | + <el-form-item label="允许跨月修改" prop="consumeAllowCrossMonthEdit" label-width="110px"> | ||
| 30 | + <el-switch v-model="form.consumeAllowCrossMonthEdit" :active-value="1" :inactive-value="0" /> | ||
| 31 | + <span class="field-suffix field-suffix--text"> | ||
| 32 | + {{ Number(form.consumeAllowCrossMonthEdit) === 1 ? '已开启' : '已关闭' }} | ||
| 33 | + </span> | ||
| 34 | + </el-form-item> | ||
| 35 | + </el-col> | ||
| 36 | + </el-row> | ||
| 37 | + </el-card> | ||
| 38 | + | ||
| 39 | + <el-card shadow="never" class="config-section"> | ||
| 40 | + <div slot="header" class="config-section__header"> | ||
| 41 | + <span>管理后台</span> | ||
| 42 | + <span class="config-section__tag">会员耗卡</span> | ||
| 43 | + </div> | ||
| 44 | + <p class="config-section__tip"> | ||
| 45 | + 分别限制「作废」「删除」可操作的天数;填 0 表示不限制天数。是否显示按钮请在「菜单管理 → 按钮权限」中为角色授权。 | ||
| 46 | + </p> | ||
| 47 | + <ConfigRuleRow title="作废耗卡" description="标记为无效并级联作废品项、业绩、人次,数据保留可查询。按钮权限编码:btn_cancel。" | ||
| 48 | + :days.sync="form.adminConsumeVoidEditableDays" /> | ||
| 49 | + <ConfigRuleRow title="删除耗卡" description="物理删除耗卡及关联子表数据,不可恢复。按钮权限编码:btn_remove。" | ||
| 50 | + :days.sync="form.adminConsumeDeleteEditableDays" /> | ||
| 51 | + <el-row :gutter="20" class="config-section__row"> | ||
| 52 | + <el-col :span="12"> | ||
| 53 | + <el-form-item label="异常耗卡容差" prop="abnormalConsumeAmountTolerance" label-width="110px"> | ||
| 54 | + <el-input-number v-model="form.abnormalConsumeAmountTolerance" :min="0" :max="99999" :precision="2" | ||
| 55 | + :step="0.01" controls-position="right" /> | ||
| 56 | + <span class="field-suffix">元</span> | ||
| 57 | + </el-form-item> | ||
| 58 | + </el-col> | ||
| 59 | + </el-row> | ||
| 60 | + <p class="config-section__tip config-section__tip--inline"> | ||
| 61 | + 会员耗卡列表中,消费金额与健康师业绩合计之差额绝对值超过该值时,记为异常耗卡。 | ||
| 62 | + </p> | ||
| 63 | + </el-card> | ||
| 16 | </template> | 64 | </template> |
| 17 | 65 | ||
| 18 | - <el-card shadow="never" class="config-section"> | ||
| 19 | - <div slot="header" class="config-section__header"> | ||
| 20 | - <span>小程序端</span> | ||
| 21 | - <span class="config-section__tag">门店小程序</span> | ||
| 22 | - </div> | ||
| 23 | - <p class="config-section__tip"> | ||
| 24 | - 耗卡发生后,超过下列天数将禁止「修改 / 作废 / 重开单」。 | ||
| 25 | - </p> | ||
| 26 | - <el-row :gutter="20"> | ||
| 27 | - <el-col :span="12"> | ||
| 28 | - <el-form-item label="可修改天数" prop="consumeEditableDays" label-width="100px"> | ||
| 29 | - <el-input-number v-model="form.consumeEditableDays" :min="1" :max="365" :precision="0" :step="1" | ||
| 30 | - controls-position="right" /> | ||
| 31 | - <span class="field-suffix">天</span> | ||
| 32 | - </el-form-item> | ||
| 33 | - </el-col> | ||
| 34 | - </el-row> | ||
| 35 | - </el-card> | ||
| 36 | - | ||
| 37 | - <el-card shadow="never" class="config-section"> | ||
| 38 | - <div slot="header" class="config-section__header"> | ||
| 39 | - <span>管理后台</span> | ||
| 40 | - <span class="config-section__tag">会员耗卡</span> | ||
| 41 | - </div> | ||
| 42 | - <p class="config-section__tip"> | ||
| 43 | - 分别限制「作废」「删除」可操作的天数;填 0 表示不限制天数。是否显示按钮请在「菜单管理 → 按钮权限」中为角色授权。 | ||
| 44 | - </p> | ||
| 45 | - <ConfigRuleRow title="作废耗卡" description="标记为无效并级联作废品项、业绩、人次,数据保留可查询。按钮权限编码:btn_cancel。" | ||
| 46 | - :days.sync="form.adminConsumeVoidEditableDays" /> | ||
| 47 | - <ConfigRuleRow title="删除耗卡" description="物理删除耗卡及关联子表数据,不可恢复。按钮权限编码:btn_remove。" | ||
| 48 | - :days.sync="form.adminConsumeDeleteEditableDays" /> | ||
| 49 | - <el-row :gutter="20" class="config-section__row"> | ||
| 50 | - <el-col :span="12"> | ||
| 51 | - <el-form-item label="异常耗卡容差" prop="abnormalConsumeAmountTolerance" label-width="110px"> | ||
| 52 | - <el-input-number v-model="form.abnormalConsumeAmountTolerance" :min="0" :max="99999" :precision="2" | ||
| 53 | - :step="0.01" controls-position="right" /> | ||
| 54 | - <span class="field-suffix">元</span> | ||
| 55 | - </el-form-item> | ||
| 56 | - </el-col> | ||
| 57 | - </el-row> | ||
| 58 | - <p class="config-section__tip config-section__tip--inline"> | ||
| 59 | - 会员耗卡列表中,消费金额与健康师业绩合计之差额绝对值超过该值时,记为异常耗卡。 | ||
| 60 | - </p> | ||
| 61 | - </el-card> | ||
| 62 | - </el-collapse-item> | ||
| 63 | - | ||
| 64 | - <el-collapse-item name="tk"> | ||
| 65 | - <template slot="title"> | ||
| 66 | - <span class="module-title"> | ||
| 67 | - <i class="el-icon-s-custom module-icon module-icon--tk"></i> | ||
| 68 | - 拓客管理 | ||
| 69 | - </span> | 66 | + <template #tk> |
| 67 | + <el-card shadow="never" class="config-section"> | ||
| 68 | + <div slot="header" class="config-section__header"> | ||
| 69 | + <span>拓客赠送品项</span> | ||
| 70 | + <span class="config-section__tag">线索拓客</span> | ||
| 71 | + </div> | ||
| 72 | + <p class="config-section__tip"> | ||
| 73 | + 拓客成功后会自动创建一笔体验开单,并写入下方所选的这一个品项(金额为 0)。统计、升单、会员类型等场景会排除该品项的零金额开单。 | ||
| 74 | + </p> | ||
| 75 | + <el-row :gutter="20"> | ||
| 76 | + <el-col :span="16"> | ||
| 77 | + <el-form-item label="赠送品项" prop="tkGiftItemIds" label-width="110px"> | ||
| 78 | + <el-select v-model="tkGiftItemId" filterable remote clearable :remote-method="remoteSearchItems" | ||
| 79 | + :loading="itemLoading" placeholder="请选择拓客赠送品项" style="width: 100%" | ||
| 80 | + @visible-change="onItemSelectVisible"> | ||
| 81 | + <el-option v-for="item in itemOptions" :key="item.id" :label="formatItemLabel(item)" | ||
| 82 | + :value="String(item.id)" /> | ||
| 83 | + </el-select> | ||
| 84 | + </el-form-item> | ||
| 85 | + </el-col> | ||
| 86 | + </el-row> | ||
| 87 | + </el-card> | ||
| 70 | </template> | 88 | </template> |
| 71 | 89 | ||
| 72 | - <el-card shadow="never" class="config-section"> | ||
| 73 | - <div slot="header" class="config-section__header"> | ||
| 74 | - <span>拓客赠送品项</span> | ||
| 75 | - <span class="config-section__tag">线索拓客</span> | ||
| 76 | - </div> | ||
| 77 | - <p class="config-section__tip"> | ||
| 78 | - 拓客成功后会自动创建一笔体验开单,并写入下方所选的这一个品项(金额为 0)。统计、升单、会员类型等场景会排除该品项的零金额开单。 | ||
| 79 | - </p> | ||
| 80 | - <el-row :gutter="20"> | ||
| 81 | - <el-col :span="16"> | ||
| 82 | - <el-form-item label="赠送品项" prop="tkGiftItemIds" label-width="110px"> | ||
| 83 | - <el-select v-model="tkGiftItemId" filterable remote clearable :remote-method="remoteSearchItems" | ||
| 84 | - :loading="itemLoading" placeholder="请选择拓客赠送品项" style="width: 100%" | ||
| 85 | - @visible-change="onItemSelectVisible"> | ||
| 86 | - <el-option v-for="item in itemOptions" :key="item.id" :label="formatItemLabel(item)" | ||
| 87 | - :value="String(item.id)" /> | ||
| 88 | - </el-select> | ||
| 89 | - </el-form-item> | ||
| 90 | - </el-col> | ||
| 91 | - </el-row> | ||
| 92 | - </el-card> | ||
| 93 | - </el-collapse-item> | ||
| 94 | - | ||
| 95 | - <el-collapse-item name="sleepRemind"> | ||
| 96 | - <template slot="title"> | ||
| 97 | - <span class="module-title"> | ||
| 98 | - <i class="el-icon-bell module-icon module-icon--sleep"></i> | ||
| 99 | - 沉睡提醒 | ||
| 100 | - </span> | 90 | + <template #sleepRemind> |
| 91 | + <el-card shadow="never" class="config-section"> | ||
| 92 | + <div slot="header" class="config-section__header"> | ||
| 93 | + <span>门店PC端</span> | ||
| 94 | + <span class="config-section__tag">会员管理</span> | ||
| 95 | + </div> | ||
| 96 | + <p class="config-section__tip"> | ||
| 97 | + 设置沉睡会员提醒天数,超过该天数未消费的会员将在门店PC端显示在沉睡提醒列表中。 | ||
| 98 | + </p> | ||
| 99 | + <el-row :gutter="20"> | ||
| 100 | + <el-col :span="12"> | ||
| 101 | + <el-form-item label="沉睡天数" prop="sleepRemindDays" label-width="100px"> | ||
| 102 | + <el-input-number v-model="form.sleepRemindDays" :min="1" :max="365" :precision="0" :step="1" | ||
| 103 | + controls-position="right" /> | ||
| 104 | + <span class="field-suffix">天</span> | ||
| 105 | + </el-form-item> | ||
| 106 | + </el-col> | ||
| 107 | + </el-row> | ||
| 108 | + </el-card> | ||
| 109 | + | ||
| 110 | + <el-card shadow="never" class="config-section"> | ||
| 111 | + <div slot="header" class="config-section__header"> | ||
| 112 | + <span>报表统计</span> | ||
| 113 | + <span class="config-section__tag">沉睡回店</span> | ||
| 114 | + </div> | ||
| 115 | + <p class="config-section__tip"> | ||
| 116 | + 判定会员从沉睡状态回店的天数阈值;首次耗卡按注册日与回店日间隔判断,非首次按上次到店间隔判断。 | ||
| 117 | + </p> | ||
| 118 | + <el-row :gutter="20"> | ||
| 119 | + <el-col :span="12"> | ||
| 120 | + <el-form-item label="沉睡回店判定天数" prop="sleepReturnDays" label-width="130px"> | ||
| 121 | + <el-input-number v-model="form.sleepReturnDays" :min="1" :max="365" :precision="0" :step="1" | ||
| 122 | + controls-position="right" /> | ||
| 123 | + <span class="field-suffix">天</span> | ||
| 124 | + </el-form-item> | ||
| 125 | + </el-col> | ||
| 126 | + </el-row> | ||
| 127 | + </el-card> | ||
| 101 | </template> | 128 | </template> |
| 102 | 129 | ||
| 103 | - <el-card shadow="never" class="config-section"> | ||
| 104 | - <div slot="header" class="config-section__header"> | ||
| 105 | - <span>门店PC端</span> | ||
| 106 | - <span class="config-section__tag">会员管理</span> | ||
| 107 | - </div> | ||
| 108 | - <p class="config-section__tip"> | ||
| 109 | - 设置沉睡会员提醒天数,超过该天数未消费的会员将在门店PC端显示在沉睡提醒列表中。 | ||
| 110 | - </p> | ||
| 111 | - <el-row :gutter="20"> | ||
| 112 | - <el-col :span="12"> | ||
| 113 | - <el-form-item label="沉睡天数" prop="sleepRemindDays" label-width="100px"> | ||
| 114 | - <el-input-number v-model="form.sleepRemindDays" :min="1" :max="365" :precision="0" :step="1" | ||
| 115 | - controls-position="right" /> | ||
| 116 | - <span class="field-suffix">天</span> | ||
| 117 | - </el-form-item> | ||
| 118 | - </el-col> | ||
| 119 | - </el-row> | ||
| 120 | - </el-card> | ||
| 121 | - | ||
| 122 | - <el-card shadow="never" class="config-section"> | ||
| 123 | - <div slot="header" class="config-section__header"> | ||
| 124 | - <span>报表统计</span> | ||
| 125 | - <span class="config-section__tag">沉睡回店</span> | ||
| 126 | - </div> | ||
| 127 | - <p class="config-section__tip"> | ||
| 128 | - 判定会员从沉睡状态回店的天数阈值;首次耗卡按注册日与回店日间隔判断,非首次按上次到店间隔判断。 | ||
| 129 | - </p> | ||
| 130 | - <el-row :gutter="20"> | ||
| 131 | - <el-col :span="12"> | ||
| 132 | - <el-form-item label="沉睡回店判定天数" prop="sleepReturnDays" label-width="130px"> | ||
| 133 | - <el-input-number v-model="form.sleepReturnDays" :min="1" :max="365" :precision="0" :step="1" | ||
| 134 | - controls-position="right" /> | ||
| 135 | - <span class="field-suffix">天</span> | ||
| 136 | - </el-form-item> | ||
| 137 | - </el-col> | ||
| 138 | - </el-row> | ||
| 139 | - </el-card> | ||
| 140 | - </el-collapse-item> | ||
| 141 | - | ||
| 142 | - <el-collapse-item name="trainingCheckin"> | ||
| 143 | - <template slot="title"> | ||
| 144 | - <span class="module-title"> | ||
| 145 | - <i class="el-icon-s-check module-icon module-icon--training"></i> | ||
| 146 | - 培训签到 | ||
| 147 | - </span> | 130 | + <template #trainingCheckin> |
| 131 | + <el-card shadow="never" class="config-section"> | ||
| 132 | + <div slot="header" class="config-section__header"> | ||
| 133 | + <span>动态二维码</span> | ||
| 134 | + <span class="config-section__tag">教室投屏</span> | ||
| 135 | + </div> | ||
| 136 | + <p class="config-section__tip"> | ||
| 137 | + 教室门口投屏页二维码自动轮换间隔;学员扫码后须在有效期内完成签到或签退。 | ||
| 138 | + </p> | ||
| 139 | + <el-row :gutter="20"> | ||
| 140 | + <el-col :span="12"> | ||
| 141 | + <el-form-item label="二维码轮换间隔" prop="trainingCheckinQrRotateMinutes" label-width="120px"> | ||
| 142 | + <el-input-number v-model="form.trainingCheckinQrRotateMinutes" :min="1" :max="60" :precision="0" | ||
| 143 | + :step="1" controls-position="right" /> | ||
| 144 | + <span class="field-suffix">分钟</span> | ||
| 145 | + </el-form-item> | ||
| 146 | + </el-col> | ||
| 147 | + </el-row> | ||
| 148 | + </el-card> | ||
| 148 | </template> | 149 | </template> |
| 149 | 150 | ||
| 150 | - <el-card shadow="never" class="config-section"> | ||
| 151 | - <div slot="header" class="config-section__header"> | ||
| 152 | - <span>动态二维码</span> | ||
| 153 | - <span class="config-section__tag">教室投屏</span> | ||
| 154 | - </div> | ||
| 155 | - <p class="config-section__tip"> | ||
| 156 | - 教室门口投屏页二维码自动轮换间隔;学员扫码后须在有效期内完成签到或签退。 | ||
| 157 | - </p> | ||
| 158 | - <el-row :gutter="20"> | ||
| 159 | - <el-col :span="12"> | ||
| 160 | - <el-form-item label="二维码轮换间隔" prop="trainingCheckinQrRotateMinutes" label-width="120px"> | ||
| 161 | - <el-input-number v-model="form.trainingCheckinQrRotateMinutes" :min="1" :max="60" :precision="0" | ||
| 162 | - :step="1" controls-position="right" /> | ||
| 163 | - <span class="field-suffix">分钟</span> | ||
| 164 | - </el-form-item> | ||
| 165 | - </el-col> | ||
| 166 | - </el-row> | ||
| 167 | - </el-card> | ||
| 168 | - </el-collapse-item> | ||
| 169 | - | ||
| 170 | - <el-collapse-item name="memberTag"> | ||
| 171 | - <template slot="title"> | ||
| 172 | - <span class="module-title"> | ||
| 173 | - <i class="el-icon-collection-tag module-icon module-icon--member"></i> | ||
| 174 | - 会员标签 | ||
| 175 | - </span> | 151 | + <template #memberTag> |
| 152 | + <el-card shadow="never" class="config-section"> | ||
| 153 | + <div slot="header" class="config-section__header"> | ||
| 154 | + <span>自定义标签</span> | ||
| 155 | + <span class="config-section__tag">全公司</span> | ||
| 156 | + </div> | ||
| 157 | + <p class="config-section__tip"> | ||
| 158 | + 单名会员可绑定的自定义标签数量上限;三端打标时超过上限将拒绝添加。 | ||
| 159 | + </p> | ||
| 160 | + <el-row :gutter="20"> | ||
| 161 | + <el-col :span="12"> | ||
| 162 | + <el-form-item label="标签上限" prop="memberCustomTagMaxCount" label-width="100px"> | ||
| 163 | + <el-input-number v-model="form.memberCustomTagMaxCount" :min="1" :max="20" :precision="0" :step="1" | ||
| 164 | + controls-position="right" /> | ||
| 165 | + <span class="field-suffix">个</span> | ||
| 166 | + </el-form-item> | ||
| 167 | + </el-col> | ||
| 168 | + </el-row> | ||
| 169 | + </el-card> | ||
| 176 | </template> | 170 | </template> |
| 177 | 171 | ||
| 178 | - <el-card shadow="never" class="config-section"> | ||
| 179 | - <div slot="header" class="config-section__header"> | ||
| 180 | - <span>自定义标签</span> | ||
| 181 | - <span class="config-section__tag">全公司</span> | ||
| 182 | - </div> | ||
| 183 | - <p class="config-section__tip"> | ||
| 184 | - 单名会员可绑定的自定义标签数量上限;三端打标时超过上限将拒绝添加。 | ||
| 185 | - </p> | ||
| 186 | - <el-row :gutter="20"> | ||
| 187 | - <el-col :span="12"> | ||
| 188 | - <el-form-item label="标签上限" prop="memberCustomTagMaxCount" label-width="100px"> | ||
| 189 | - <el-input-number v-model="form.memberCustomTagMaxCount" :min="1" :max="20" :precision="0" :step="1" | ||
| 190 | - controls-position="right" /> | ||
| 191 | - <span class="field-suffix">个</span> | ||
| 192 | - </el-form-item> | ||
| 193 | - </el-col> | ||
| 194 | - </el-row> | ||
| 195 | - </el-card> | ||
| 196 | - </el-collapse-item> | ||
| 197 | - | ||
| 198 | - <el-collapse-item name="monitorCenter"> | ||
| 199 | - <template slot="title"> | ||
| 200 | - <span class="module-title"> | ||
| 201 | - <i class="el-icon-view module-icon module-icon--monitor"></i> | ||
| 202 | - 监控中心 | ||
| 203 | - </span> | 172 | + <template #monitorCenter> |
| 173 | + <el-card shadow="never" class="config-section"> | ||
| 174 | + <div slot="header" class="config-section__header"> | ||
| 175 | + <span>大客卡金异常</span> | ||
| 176 | + <span class="config-section__tag">金额阈值</span> | ||
| 177 | + </div> | ||
| 178 | + <p class="config-section__tip"> | ||
| 179 | + 开单/耗卡/退卡/转卡金额达到对应阈值时,列入「大客卡金异常」监控列表(与重复充值监控并列)。 | ||
| 180 | + </p> | ||
| 181 | + <el-row :gutter="20"> | ||
| 182 | + <el-col :span="12"> | ||
| 183 | + <el-form-item label="大额开单" prop="largeAmountBillingThreshold" label-width="100px"> | ||
| 184 | + <el-input-number v-model="form.largeAmountBillingThreshold" :min="1" :max="99999999" :precision="2" | ||
| 185 | + :step="1000" controls-position="right" /> | ||
| 186 | + <span class="field-suffix">元</span> | ||
| 187 | + </el-form-item> | ||
| 188 | + </el-col> | ||
| 189 | + <el-col :span="12"> | ||
| 190 | + <el-form-item label="大额耗卡" prop="largeAmountConsumeThreshold" label-width="100px"> | ||
| 191 | + <el-input-number v-model="form.largeAmountConsumeThreshold" :min="1" :max="99999999" :precision="2" | ||
| 192 | + :step="1000" controls-position="right" /> | ||
| 193 | + <span class="field-suffix">元</span> | ||
| 194 | + </el-form-item> | ||
| 195 | + </el-col> | ||
| 196 | + <el-col :span="12"> | ||
| 197 | + <el-form-item label="大额退卡" prop="largeAmountRefundThreshold" label-width="100px"> | ||
| 198 | + <el-input-number v-model="form.largeAmountRefundThreshold" :min="1" :max="99999999" :precision="2" | ||
| 199 | + :step="1000" controls-position="right" /> | ||
| 200 | + <span class="field-suffix">元</span> | ||
| 201 | + </el-form-item> | ||
| 202 | + </el-col> | ||
| 203 | + <el-col :span="12"> | ||
| 204 | + <el-form-item label="大额转卡" prop="largeAmountTransferThreshold" label-width="100px"> | ||
| 205 | + <el-input-number v-model="form.largeAmountTransferThreshold" :min="1" :max="99999999" :precision="2" | ||
| 206 | + :step="1000" controls-position="right" /> | ||
| 207 | + <span class="field-suffix">元</span> | ||
| 208 | + </el-form-item> | ||
| 209 | + </el-col> | ||
| 210 | + </el-row> | ||
| 211 | + </el-card> | ||
| 204 | </template> | 212 | </template> |
| 205 | 213 | ||
| 206 | - <el-card shadow="never" class="config-section"> | ||
| 207 | - <div slot="header" class="config-section__header"> | ||
| 208 | - <span>大客卡金异常</span> | ||
| 209 | - <span class="config-section__tag">金额阈值</span> | ||
| 210 | - </div> | ||
| 211 | - <p class="config-section__tip"> | ||
| 212 | - 开单/耗卡/退卡/转卡金额达到对应阈值时,列入「大客卡金异常」监控列表(与重复充值监控并列)。 | ||
| 213 | - </p> | ||
| 214 | - <el-row :gutter="20"> | ||
| 215 | - <el-col :span="12"> | ||
| 216 | - <el-form-item label="大额开单" prop="largeAmountBillingThreshold" label-width="100px"> | ||
| 217 | - <el-input-number v-model="form.largeAmountBillingThreshold" :min="1" :max="99999999" :precision="2" | ||
| 218 | - :step="1000" controls-position="right" /> | ||
| 219 | - <span class="field-suffix">元</span> | ||
| 220 | - </el-form-item> | ||
| 221 | - </el-col> | ||
| 222 | - <el-col :span="12"> | ||
| 223 | - <el-form-item label="大额耗卡" prop="largeAmountConsumeThreshold" label-width="100px"> | ||
| 224 | - <el-input-number v-model="form.largeAmountConsumeThreshold" :min="1" :max="99999999" :precision="2" | ||
| 225 | - :step="1000" controls-position="right" /> | ||
| 226 | - <span class="field-suffix">元</span> | ||
| 227 | - </el-form-item> | ||
| 228 | - </el-col> | ||
| 229 | - <el-col :span="12"> | ||
| 230 | - <el-form-item label="大额退卡" prop="largeAmountRefundThreshold" label-width="100px"> | ||
| 231 | - <el-input-number v-model="form.largeAmountRefundThreshold" :min="1" :max="99999999" :precision="2" | ||
| 232 | - :step="1000" controls-position="right" /> | ||
| 233 | - <span class="field-suffix">元</span> | ||
| 234 | - </el-form-item> | ||
| 235 | - </el-col> | ||
| 236 | - <el-col :span="12"> | ||
| 237 | - <el-form-item label="大额转卡" prop="largeAmountTransferThreshold" label-width="100px"> | ||
| 238 | - <el-input-number v-model="form.largeAmountTransferThreshold" :min="1" :max="99999999" :precision="2" | ||
| 239 | - :step="1000" controls-position="right" /> | ||
| 240 | - <span class="field-suffix">元</span> | ||
| 241 | - </el-form-item> | ||
| 242 | - </el-col> | ||
| 243 | - </el-row> | ||
| 244 | - </el-card> | ||
| 245 | - </el-collapse-item> | ||
| 246 | - | ||
| 247 | - <el-collapse-item name="goldTriangle"> | ||
| 248 | - <template slot="title"> | ||
| 249 | - <span class="module-title"> | ||
| 250 | - <i class="el-icon-s-data module-icon module-icon--gold"></i> | ||
| 251 | - 金三角业绩 | ||
| 252 | - </span> | 214 | + <template #goldTriangle> |
| 215 | + <el-card shadow="never" class="config-section"> | ||
| 216 | + <div slot="header" class="config-section__header"> | ||
| 217 | + <span>员工手机端</span> | ||
| 218 | + <span class="config-section__tag">健康师</span> | ||
| 219 | + </div> | ||
| 220 | + <p class="config-section__tip"> | ||
| 221 | + 控制个人中心「提成缺口」与金三角业绩页「当前提成档」旁的问号说明;关闭后员工端不显示问号及档位弹窗。 | ||
| 222 | + </p> | ||
| 223 | + <el-row :gutter="20"> | ||
| 224 | + <el-col :span="12"> | ||
| 225 | + <el-form-item label="档位说明问号" prop="commissionTierHelpSwitch" label-width="110px"> | ||
| 226 | + <el-switch v-model="form.commissionTierHelpSwitch" :active-value="1" :inactive-value="0" /> | ||
| 227 | + <span class="field-suffix field-suffix--text"> | ||
| 228 | + {{ Number(form.commissionTierHelpSwitch) === 1 ? '已开启' : '已关闭' }} | ||
| 229 | + </span> | ||
| 230 | + </el-form-item> | ||
| 231 | + </el-col> | ||
| 232 | + </el-row> | ||
| 233 | + <commission-tier-help-preview :enabled="Number(form.commissionTierHelpSwitch) === 1" /> | ||
| 234 | + </el-card> | ||
| 253 | </template> | 235 | </template> |
| 254 | - | ||
| 255 | - <el-card shadow="never" class="config-section"> | ||
| 256 | - <div slot="header" class="config-section__header"> | ||
| 257 | - <span>员工手机端</span> | ||
| 258 | - <span class="config-section__tag">健康师</span> | ||
| 259 | - </div> | ||
| 260 | - <p class="config-section__tip"> | ||
| 261 | - 控制个人中心「提成缺口」与金三角业绩页「当前提成档」旁的问号说明;关闭后员工端不显示问号及档位弹窗。 | ||
| 262 | - </p> | ||
| 263 | - <el-row :gutter="20"> | ||
| 264 | - <el-col :span="12"> | ||
| 265 | - <el-form-item label="档位说明问号" prop="commissionTierHelpSwitch" label-width="110px"> | ||
| 266 | - <el-switch v-model="form.commissionTierHelpSwitch" :active-value="1" :inactive-value="0" /> | ||
| 267 | - <span class="field-suffix field-suffix--text"> | ||
| 268 | - {{ Number(form.commissionTierHelpSwitch) === 1 ? '已开启' : '已关闭' }} | ||
| 269 | - </span> | ||
| 270 | - </el-form-item> | ||
| 271 | - </el-col> | ||
| 272 | - </el-row> | ||
| 273 | - <commission-tier-help-preview :enabled="Number(form.commissionTierHelpSwitch) === 1" /> | ||
| 274 | - </el-card> | ||
| 275 | - </el-collapse-item> | ||
| 276 | - </el-collapse> | ||
| 277 | - </div> | 236 | + </ConfigPanelShell> |
| 278 | </template> | 237 | </template> |
| 279 | 238 | ||
| 280 | <script> | 239 | <script> |
| 281 | import request from '@/utils/request' | 240 | import request from '@/utils/request' |
| 241 | +import ConfigPanelShell from './config-panel-shell.vue' | ||
| 282 | import ConfigRuleRow from './config-rule-row' | 242 | import ConfigRuleRow from './config-rule-row' |
| 283 | import CommissionTierHelpPreview from './commission-tier-help-preview' | 243 | import CommissionTierHelpPreview from './commission-tier-help-preview' |
| 284 | 244 | ||
| 245 | +const MODULES = [ | ||
| 246 | + { key: 'consume', label: '耗卡管理', desc: '小程序与管理后台耗卡规则', icon: 'el-icon-s-order', iconClass: 'module-icon--consume' }, | ||
| 247 | + { key: 'tk', label: '拓客管理', desc: '拓客赠送品项', icon: 'el-icon-s-custom', iconClass: 'module-icon--tk' }, | ||
| 248 | + { key: 'sleepRemind', label: '沉睡提醒', desc: '门店提醒与回店统计', icon: 'el-icon-bell', iconClass: 'module-icon--sleep' }, | ||
| 249 | + { key: 'trainingCheckin', label: '培训签到', desc: '动态二维码轮换', icon: 'el-icon-s-check', iconClass: 'module-icon--training' }, | ||
| 250 | + { key: 'memberTag', label: '会员标签', desc: '自定义标签上限', icon: 'el-icon-collection-tag', iconClass: 'module-icon--member' }, | ||
| 251 | + { key: 'monitorCenter', label: '监控中心', desc: '大客卡金异常阈值', icon: 'el-icon-view', iconClass: 'module-icon--monitor' }, | ||
| 252 | + { key: 'goldTriangle', label: '金三角业绩', desc: '提成档位说明', icon: 'el-icon-s-data', iconClass: 'module-icon--gold' } | ||
| 253 | +] | ||
| 254 | + | ||
| 285 | function parseIdList(raw) { | 255 | function parseIdList(raw) { |
| 286 | if (!raw) return [] | 256 | if (!raw) return [] |
| 287 | return String(raw) | 257 | return String(raw) |
| @@ -292,7 +262,7 @@ function parseIdList(raw) { | @@ -292,7 +262,7 @@ function parseIdList(raw) { | ||
| 292 | 262 | ||
| 293 | export default { | 263 | export default { |
| 294 | name: 'BusinessConfigPanel', | 264 | name: 'BusinessConfigPanel', |
| 295 | - components: { ConfigRuleRow, CommissionTierHelpPreview }, | 265 | + components: { ConfigPanelShell, ConfigRuleRow, CommissionTierHelpPreview }, |
| 296 | props: { | 266 | props: { |
| 297 | form: { | 267 | form: { |
| 298 | type: Object, | 268 | type: Object, |
| @@ -309,7 +279,7 @@ export default { | @@ -309,7 +279,7 @@ export default { | ||
| 309 | }, | 279 | }, |
| 310 | data() { | 280 | data() { |
| 311 | return { | 281 | return { |
| 312 | - activeModules: ['consume', 'tk', 'sleepRemind', 'trainingCheckin', 'memberTag', 'monitorCenter', 'goldTriangle'], | 282 | + modules: MODULES, |
| 313 | itemOptions: [], | 283 | itemOptions: [], |
| 314 | itemLoading: false, | 284 | itemLoading: false, |
| 315 | itemOptionsLoaded: false | 285 | itemOptionsLoaded: false |
| @@ -332,7 +302,6 @@ export default { | @@ -332,7 +302,6 @@ export default { | ||
| 332 | immediate: true, | 302 | immediate: true, |
| 333 | handler(val) { | 303 | handler(val) { |
| 334 | const ids = parseIdList(val) | 304 | const ids = parseIdList(val) |
| 335 | - // 配置页按单品项展示;若历史存了多个,只回显并保留第一项 | ||
| 336 | if (ids.length > 1) { | 305 | if (ids.length > 1) { |
| 337 | this.$set(this.form, 'tkGiftItemIds', ids[0]) | 306 | this.$set(this.form, 'tkGiftItemIds', ids[0]) |
| 338 | } | 307 | } |
| @@ -392,14 +361,14 @@ export default { | @@ -392,14 +361,14 @@ export default { | ||
| 392 | this.itemOptions.forEach(item => { | 361 | this.itemOptions.forEach(item => { |
| 393 | map[String(item.id)] = item | 362 | map[String(item.id)] = item |
| 394 | }) | 363 | }) |
| 395 | - ; (list || []).forEach(item => { | ||
| 396 | - if (!item || item.id == null) return | ||
| 397 | - map[String(item.id)] = { | ||
| 398 | - id: String(item.id), | ||
| 399 | - xmmc: item.xmmc || item.fullName || String(item.id), | ||
| 400 | - qt1: item.qt1 || '' | ||
| 401 | - } | ||
| 402 | - }) | 364 | + ;(list || []).forEach(item => { |
| 365 | + if (!item || item.id == null) return | ||
| 366 | + map[String(item.id)] = { | ||
| 367 | + id: String(item.id), | ||
| 368 | + xmmc: item.xmmc || item.fullName || String(item.id), | ||
| 369 | + qt1: item.qt1 || '' | ||
| 370 | + } | ||
| 371 | + }) | ||
| 403 | this.itemOptions = Object.keys(map).map(k => map[k]) | 372 | this.itemOptions = Object.keys(map).map(k => map[k]) |
| 404 | } | 373 | } |
| 405 | } | 374 | } |
| @@ -407,75 +376,14 @@ export default { | @@ -407,75 +376,14 @@ export default { | ||
| 407 | </script> | 376 | </script> |
| 408 | 377 | ||
| 409 | <style lang="scss" scoped> | 378 | <style lang="scss" scoped> |
| 410 | -.business-config-panel { | ||
| 411 | - padding-top: 4px; | ||
| 412 | - | ||
| 413 | - &__header { | ||
| 414 | - display: flex; | ||
| 415 | - align-items: flex-start; | ||
| 416 | - justify-content: space-between; | ||
| 417 | - gap: 16px; | ||
| 418 | - margin-bottom: 16px; | ||
| 419 | - } | ||
| 420 | - | ||
| 421 | - &__save { | ||
| 422 | - flex-shrink: 0; | ||
| 423 | - width: 100px; | ||
| 424 | - } | ||
| 425 | - | ||
| 426 | - &__collapse { | ||
| 427 | - border: none; | ||
| 428 | - | ||
| 429 | - >>>.el-collapse-item__header { | ||
| 430 | - height: 48px; | ||
| 431 | - line-height: 48px; | ||
| 432 | - font-size: 15px; | ||
| 433 | - font-weight: 500; | ||
| 434 | - border-bottom: 1px solid #ebeef5; | ||
| 435 | - } | ||
| 436 | - | ||
| 437 | - >>>.el-collapse-item__wrap { | ||
| 438 | - border-bottom: none; | ||
| 439 | - } | ||
| 440 | - | ||
| 441 | - >>>.el-collapse-item__content { | ||
| 442 | - padding: 16px 0 8px; | ||
| 443 | - } | ||
| 444 | - } | ||
| 445 | -} | ||
| 446 | - | ||
| 447 | -.module-title { | ||
| 448 | - display: inline-flex; | ||
| 449 | - align-items: center; | ||
| 450 | -} | ||
| 451 | - | ||
| 452 | .module-icon { | 379 | .module-icon { |
| 453 | - margin-right: 8px; | ||
| 454 | - font-size: 16px; | ||
| 455 | - | ||
| 456 | - &--consume { | ||
| 457 | - color: #409EFF; | ||
| 458 | - } | ||
| 459 | - | ||
| 460 | - &--sleep { | ||
| 461 | - color: #E6A23C; | ||
| 462 | - } | ||
| 463 | - | ||
| 464 | - &--billing { | ||
| 465 | - color: #67C23A; | ||
| 466 | - } | ||
| 467 | - | ||
| 468 | - &--tk { | ||
| 469 | - color: #909399; | ||
| 470 | - } | ||
| 471 | - | ||
| 472 | - &--training { | ||
| 473 | - color: #409EFF; | ||
| 474 | - } | ||
| 475 | - | ||
| 476 | - &--gold { | ||
| 477 | - color: #43a047; | ||
| 478 | - } | 380 | + &--consume { color: #409EFF; } |
| 381 | + &--sleep { color: #E6A23C; } | ||
| 382 | + &--tk { color: #909399; } | ||
| 383 | + &--training { color: #409EFF; } | ||
| 384 | + &--member { color: #9c27b0; } | ||
| 385 | + &--monitor { color: #f56c6c; } | ||
| 386 | + &--gold { color: #43a047; } | ||
| 479 | } | 387 | } |
| 480 | 388 | ||
| 481 | .config-section { | 389 | .config-section { |
| @@ -531,15 +439,8 @@ export default { | @@ -531,15 +439,8 @@ export default { | ||
| 531 | } | 439 | } |
| 532 | } | 440 | } |
| 533 | 441 | ||
| 534 | -/* 暗黑:覆盖写死的浅色边框/标签/提示 */ | ||
| 535 | -.ncc-content-dark .business-config-panel { | ||
| 536 | - &__collapse { | ||
| 537 | - >>>.el-collapse-item__header { | ||
| 538 | - border-bottom-color: rgba(255, 255, 255, 0.08); | ||
| 539 | - color: rgba(255, 255, 255, 0.85); | ||
| 540 | - } | ||
| 541 | - } | ||
| 542 | - | 442 | +/* 暗黑 */ |
| 443 | +.ncc-content-dark { | ||
| 543 | .config-section { | 444 | .config-section { |
| 544 | border-color: rgba(255, 255, 255, 0.08); | 445 | border-color: rgba(255, 255, 255, 0.08); |
| 545 | 446 |
antis-ncc-admin/src/views/system/sysConfig/config-panel-shell.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="config-panel-shell" v-loading="loading"> | ||
| 3 | + <div class="config-panel-shell__header"> | ||
| 4 | + <el-alert :title="alertTitle" :type="alertType" :closable="false" show-icon /> | ||
| 5 | + <div class="config-panel-shell__actions"> | ||
| 6 | + <slot name="header-actions" /> | ||
| 7 | + <el-button | ||
| 8 | + v-if="showSave" | ||
| 9 | + type="primary" | ||
| 10 | + size="small" | ||
| 11 | + :loading="saving" | ||
| 12 | + class="config-panel-shell__save" | ||
| 13 | + @click="$emit('save')" | ||
| 14 | + >保 存</el-button> | ||
| 15 | + </div> | ||
| 16 | + </div> | ||
| 17 | + | ||
| 18 | + <div class="config-panel-shell__body" :class="{ 'is-single': modules.length <= 1 }"> | ||
| 19 | + <aside v-if="modules.length > 1" class="config-panel-shell__nav" :class="navThemeClass"> | ||
| 20 | + <button | ||
| 21 | + v-for="item in modules" | ||
| 22 | + :key="item.key" | ||
| 23 | + type="button" | ||
| 24 | + class="nav-item" | ||
| 25 | + :class="{ 'is-active': activeModule === item.key }" | ||
| 26 | + @click="switchModule(item.key)" | ||
| 27 | + > | ||
| 28 | + <i :class="[item.icon, 'module-icon', item.iconClass]"></i> | ||
| 29 | + <span class="nav-item__text"> | ||
| 30 | + <span class="nav-item__label">{{ item.label }}</span> | ||
| 31 | + <span v-if="item.desc" class="nav-item__desc">{{ item.desc }}</span> | ||
| 32 | + </span> | ||
| 33 | + </button> | ||
| 34 | + </aside> | ||
| 35 | + | ||
| 36 | + <section class="config-panel-shell__content"> | ||
| 37 | + <header v-if="showContentHeader" class="content-header"> | ||
| 38 | + <h3 class="content-header__title"> | ||
| 39 | + <i :class="[currentModule.icon, 'module-icon', currentModule.iconClass]"></i> | ||
| 40 | + {{ currentModule.label }} | ||
| 41 | + </h3> | ||
| 42 | + <p v-if="currentModule.desc" class="content-header__desc">{{ currentModule.desc }}</p> | ||
| 43 | + </header> | ||
| 44 | + <div class="config-panel-shell__slot"> | ||
| 45 | + <slot :name="activeModule" /> | ||
| 46 | + </div> | ||
| 47 | + </section> | ||
| 48 | + </div> | ||
| 49 | + <slot name="append" /> | ||
| 50 | + </div> | ||
| 51 | +</template> | ||
| 52 | + | ||
| 53 | +<script> | ||
| 54 | +export default { | ||
| 55 | + name: 'ConfigPanelShell', | ||
| 56 | + props: { | ||
| 57 | + modules: { | ||
| 58 | + type: Array, | ||
| 59 | + default: () => [] | ||
| 60 | + }, | ||
| 61 | + loading: { | ||
| 62 | + type: Boolean, | ||
| 63 | + default: false | ||
| 64 | + }, | ||
| 65 | + saving: { | ||
| 66 | + type: Boolean, | ||
| 67 | + default: false | ||
| 68 | + }, | ||
| 69 | + showSave: { | ||
| 70 | + type: Boolean, | ||
| 71 | + default: true | ||
| 72 | + }, | ||
| 73 | + showContentHeader: { | ||
| 74 | + type: Boolean, | ||
| 75 | + default: true | ||
| 76 | + }, | ||
| 77 | + alertTitle: { | ||
| 78 | + type: String, | ||
| 79 | + default: '左侧选择模块,右侧仅展示当前配置。' | ||
| 80 | + }, | ||
| 81 | + alertType: { | ||
| 82 | + type: String, | ||
| 83 | + default: 'info' | ||
| 84 | + }, | ||
| 85 | + storageKey: { | ||
| 86 | + type: String, | ||
| 87 | + default: '' | ||
| 88 | + }, | ||
| 89 | + navTheme: { | ||
| 90 | + type: String, | ||
| 91 | + default: 'primary' | ||
| 92 | + }, | ||
| 93 | + defaultModule: { | ||
| 94 | + type: String, | ||
| 95 | + default: '' | ||
| 96 | + } | ||
| 97 | + }, | ||
| 98 | + data() { | ||
| 99 | + return { | ||
| 100 | + activeModule: this.resolveInitialModule() | ||
| 101 | + } | ||
| 102 | + }, | ||
| 103 | + computed: { | ||
| 104 | + currentModule() { | ||
| 105 | + return this.modules.find(item => item.key === this.activeModule) || this.modules[0] || { label: '', desc: '', icon: '', iconClass: '' } | ||
| 106 | + }, | ||
| 107 | + navThemeClass() { | ||
| 108 | + return `is-theme-${this.navTheme}` | ||
| 109 | + } | ||
| 110 | + }, | ||
| 111 | + watch: { | ||
| 112 | + modules: { | ||
| 113 | + immediate: true, | ||
| 114 | + handler(list) { | ||
| 115 | + if (!list || !list.length) return | ||
| 116 | + if (!list.some(item => item.key === this.activeModule)) { | ||
| 117 | + this.activeModule = list[0].key | ||
| 118 | + } | ||
| 119 | + } | ||
| 120 | + } | ||
| 121 | + }, | ||
| 122 | + methods: { | ||
| 123 | + resolveInitialModule() { | ||
| 124 | + const fallback = this.defaultModule || (this.modules[0] && this.modules[0].key) || '' | ||
| 125 | + if (!this.storageKey) return fallback | ||
| 126 | + try { | ||
| 127 | + const stored = localStorage.getItem(this.storageKey) | ||
| 128 | + if (stored && this.modules.some(item => item.key === stored)) { | ||
| 129 | + return stored | ||
| 130 | + } | ||
| 131 | + } catch (e) { | ||
| 132 | + // ignore | ||
| 133 | + } | ||
| 134 | + return fallback | ||
| 135 | + }, | ||
| 136 | + switchModule(key) { | ||
| 137 | + if (this.activeModule === key) return | ||
| 138 | + this.activeModule = key | ||
| 139 | + if (this.storageKey) { | ||
| 140 | + try { | ||
| 141 | + localStorage.setItem(this.storageKey, key) | ||
| 142 | + } catch (e) { | ||
| 143 | + // ignore | ||
| 144 | + } | ||
| 145 | + } | ||
| 146 | + this.$emit('module-change', key) | ||
| 147 | + this.$nextTick(() => { | ||
| 148 | + const content = this.$el && this.$el.querySelector('.config-panel-shell__content') | ||
| 149 | + if (content) content.scrollTop = 0 | ||
| 150 | + }) | ||
| 151 | + } | ||
| 152 | + } | ||
| 153 | +} | ||
| 154 | +</script> | ||
| 155 | + | ||
| 156 | +<style lang="scss" scoped> | ||
| 157 | +.config-panel-shell { | ||
| 158 | + display: flex; | ||
| 159 | + flex-direction: column; | ||
| 160 | + height: calc(100vh - 220px); | ||
| 161 | + min-height: 480px; | ||
| 162 | + padding-top: 4px; | ||
| 163 | + | ||
| 164 | + &__header { | ||
| 165 | + display: flex; | ||
| 166 | + align-items: flex-start; | ||
| 167 | + justify-content: space-between; | ||
| 168 | + gap: 16px; | ||
| 169 | + margin-bottom: 16px; | ||
| 170 | + flex-shrink: 0; | ||
| 171 | + } | ||
| 172 | + | ||
| 173 | + &__actions { | ||
| 174 | + display: flex; | ||
| 175 | + align-items: center; | ||
| 176 | + gap: 8px; | ||
| 177 | + flex-shrink: 0; | ||
| 178 | + } | ||
| 179 | + | ||
| 180 | + &__save { | ||
| 181 | + width: 100px; | ||
| 182 | + } | ||
| 183 | + | ||
| 184 | + &__body { | ||
| 185 | + display: flex; | ||
| 186 | + flex: 1; | ||
| 187 | + min-height: 0; | ||
| 188 | + border: 1px solid #ebeef5; | ||
| 189 | + border-radius: 8px; | ||
| 190 | + overflow: hidden; | ||
| 191 | + background: #fff; | ||
| 192 | + | ||
| 193 | + &.is-single .config-panel-shell__content { | ||
| 194 | + padding-top: 20px; | ||
| 195 | + } | ||
| 196 | + } | ||
| 197 | + | ||
| 198 | + &__nav { | ||
| 199 | + width: 220px; | ||
| 200 | + flex-shrink: 0; | ||
| 201 | + padding: 12px 8px; | ||
| 202 | + border-right: 1px solid #ebeef5; | ||
| 203 | + background: #fafafa; | ||
| 204 | + overflow-y: auto; | ||
| 205 | + } | ||
| 206 | + | ||
| 207 | + &__content { | ||
| 208 | + flex: 1; | ||
| 209 | + min-width: 0; | ||
| 210 | + padding: 16px 20px 24px; | ||
| 211 | + overflow-y: auto; | ||
| 212 | + } | ||
| 213 | + | ||
| 214 | + &__slot { | ||
| 215 | + min-height: 120px; | ||
| 216 | + } | ||
| 217 | +} | ||
| 218 | + | ||
| 219 | +.nav-item { | ||
| 220 | + display: flex; | ||
| 221 | + align-items: flex-start; | ||
| 222 | + width: 100%; | ||
| 223 | + margin-bottom: 6px; | ||
| 224 | + padding: 10px 12px; | ||
| 225 | + border: 1px solid transparent; | ||
| 226 | + border-radius: 8px; | ||
| 227 | + background: transparent; | ||
| 228 | + text-align: left; | ||
| 229 | + cursor: pointer; | ||
| 230 | + transition: background 0.15s ease, border-color 0.15s ease; | ||
| 231 | + | ||
| 232 | + &:last-child { | ||
| 233 | + margin-bottom: 0; | ||
| 234 | + } | ||
| 235 | + | ||
| 236 | + &__text { | ||
| 237 | + display: flex; | ||
| 238 | + flex-direction: column; | ||
| 239 | + min-width: 0; | ||
| 240 | + } | ||
| 241 | + | ||
| 242 | + &__label { | ||
| 243 | + color: #303133; | ||
| 244 | + font-size: 14px; | ||
| 245 | + font-weight: 500; | ||
| 246 | + line-height: 20px; | ||
| 247 | + } | ||
| 248 | + | ||
| 249 | + &__desc { | ||
| 250 | + margin-top: 2px; | ||
| 251 | + color: #909399; | ||
| 252 | + font-size: 12px; | ||
| 253 | + line-height: 18px; | ||
| 254 | + } | ||
| 255 | +} | ||
| 256 | + | ||
| 257 | +.config-panel-shell__nav.is-theme-primary .nav-item { | ||
| 258 | + &:hover { | ||
| 259 | + background: #f0f7ff; | ||
| 260 | + } | ||
| 261 | + | ||
| 262 | + &.is-active { | ||
| 263 | + background: #ecf5ff; | ||
| 264 | + border-color: #c6e2ff; | ||
| 265 | + } | ||
| 266 | +} | ||
| 267 | + | ||
| 268 | +.config-panel-shell__nav.is-theme-warning .nav-item { | ||
| 269 | + &:hover { | ||
| 270 | + background: #fdf6ec; | ||
| 271 | + } | ||
| 272 | + | ||
| 273 | + &.is-active { | ||
| 274 | + background: #fdf6ec; | ||
| 275 | + border-color: #f5dab1; | ||
| 276 | + } | ||
| 277 | +} | ||
| 278 | + | ||
| 279 | +.content-header { | ||
| 280 | + margin-bottom: 16px; | ||
| 281 | + padding-bottom: 12px; | ||
| 282 | + border-bottom: 1px solid #ebeef5; | ||
| 283 | + | ||
| 284 | + &__title { | ||
| 285 | + display: flex; | ||
| 286 | + align-items: center; | ||
| 287 | + margin: 0; | ||
| 288 | + color: #303133; | ||
| 289 | + font-size: 16px; | ||
| 290 | + font-weight: 600; | ||
| 291 | + line-height: 24px; | ||
| 292 | + } | ||
| 293 | + | ||
| 294 | + &__desc { | ||
| 295 | + margin: 6px 0 0; | ||
| 296 | + color: #909399; | ||
| 297 | + font-size: 13px; | ||
| 298 | + line-height: 1.6; | ||
| 299 | + } | ||
| 300 | +} | ||
| 301 | + | ||
| 302 | +.module-icon { | ||
| 303 | + margin-right: 8px; | ||
| 304 | + font-size: 16px; | ||
| 305 | + flex-shrink: 0; | ||
| 306 | +} | ||
| 307 | + | ||
| 308 | +.ncc-content-dark .config-panel-shell { | ||
| 309 | + &__body { | ||
| 310 | + border-color: rgba(255, 255, 255, 0.08); | ||
| 311 | + background: transparent; | ||
| 312 | + } | ||
| 313 | + | ||
| 314 | + &__nav { | ||
| 315 | + border-right-color: rgba(255, 255, 255, 0.08); | ||
| 316 | + background: rgba(255, 255, 255, 0.03); | ||
| 317 | + } | ||
| 318 | + | ||
| 319 | + .nav-item__label { | ||
| 320 | + color: rgba(255, 255, 255, 0.85); | ||
| 321 | + } | ||
| 322 | + | ||
| 323 | + .nav-item__desc { | ||
| 324 | + color: rgba(255, 255, 255, 0.55); | ||
| 325 | + } | ||
| 326 | + | ||
| 327 | + .content-header { | ||
| 328 | + border-bottom-color: rgba(255, 255, 255, 0.08); | ||
| 329 | + | ||
| 330 | + &__title { | ||
| 331 | + color: rgba(255, 255, 255, 0.85); | ||
| 332 | + } | ||
| 333 | + | ||
| 334 | + &__desc { | ||
| 335 | + color: rgba(255, 255, 255, 0.55); | ||
| 336 | + } | ||
| 337 | + } | ||
| 338 | +} | ||
| 339 | +</style> |
antis-ncc-admin/src/views/system/sysConfig/index.vue
| @@ -3,263 +3,63 @@ | @@ -3,263 +3,63 @@ | ||
| 3 | <el-form ref="baseForm" :model="baseForm" label-width="120px"> | 3 | <el-form ref="baseForm" :model="baseForm" label-width="120px"> |
| 4 | <el-tabs v-model="activeName" type="border-card" class="NCC-el_tabs"> | 4 | <el-tabs v-model="activeName" type="border-card" class="NCC-el_tabs"> |
| 5 | <el-tab-pane label="基本设置" name="first"> | 5 | <el-tab-pane label="基本设置" name="first"> |
| 6 | - <div class="basic-settings"> | ||
| 7 | - <el-divider content-position="left">品牌标识</el-divider> | ||
| 8 | - <el-row :gutter="24" class="brand-row"> | ||
| 9 | - <el-col :xs="24" :sm="12" :md="8"> | ||
| 10 | - <el-form-item label="系统 Logo"> | ||
| 11 | - <div class="img-uploader" v-loading="logoUploading"> | ||
| 12 | - <el-upload class="single-uploader" :show-file-list="false" action="#" accept="image/*" | ||
| 13 | - :http-request="uploadSysLogo"> | ||
| 14 | - <img v-if="baseForm.sysLogo" :src="logoPreview(baseForm.sysLogo)" class="single-img"> | ||
| 15 | - <i v-else class="el-icon-plus single-icon"></i> | ||
| 16 | - </el-upload> | ||
| 17 | - <el-button v-if="baseForm.sysLogo" type="text" class="NCC-table-delBtn" | ||
| 18 | - @click="baseForm.sysLogo = ''">移除</el-button> | ||
| 19 | - </div> | ||
| 20 | - <p class="form-tip">侧栏展开时显示,建议 32×32 或等比透明图</p> | ||
| 21 | - </el-form-item> | ||
| 22 | - </el-col> | ||
| 23 | - <el-col :xs="24" :sm="12" :md="8"> | ||
| 24 | - <el-form-item label="折叠图标"> | ||
| 25 | - <div class="img-uploader" v-loading="logoIconUploading"> | ||
| 26 | - <el-upload class="single-uploader" :show-file-list="false" action="#" accept="image/*" | ||
| 27 | - :http-request="uploadSysLogoIcon"> | ||
| 28 | - <img v-if="baseForm.sysLogoIcon" :src="logoPreview(baseForm.sysLogoIcon)" class="single-img"> | ||
| 29 | - <i v-else class="el-icon-plus single-icon"></i> | ||
| 30 | - </el-upload> | ||
| 31 | - <el-button v-if="baseForm.sysLogoIcon" type="text" class="NCC-table-delBtn" | ||
| 32 | - @click="baseForm.sysLogoIcon = ''">移除</el-button> | ||
| 33 | - </div> | ||
| 34 | - <p class="form-tip">可选;侧栏折叠时显示,不传则使用系统 Logo</p> | ||
| 35 | - </el-form-item> | ||
| 36 | - </el-col> | ||
| 37 | - </el-row> | ||
| 38 | - | ||
| 39 | - <el-divider content-position="left">基础信息</el-divider> | ||
| 40 | - <el-row :gutter="24"> | ||
| 41 | - <el-col :span="12"> | ||
| 42 | - <el-form-item label="系统名称" prop="sysName"> | ||
| 43 | - <el-input v-model="baseForm.sysName" clearable placeholder="系统名称" /> | ||
| 44 | - </el-form-item> | ||
| 45 | - </el-col> | ||
| 46 | - <el-col :span="12"> | ||
| 47 | - <el-form-item label="系统版本" prop="sysVersion"> | ||
| 48 | - <el-input v-model="baseForm.sysVersion" clearable placeholder="系统版本" /> | ||
| 49 | - </el-form-item> | ||
| 50 | - </el-col> | ||
| 51 | - <el-col :span="12"> | ||
| 52 | - <el-form-item label="公司名称" prop="companyName"> | ||
| 53 | - <el-input v-model="baseForm.companyName" clearable placeholder="公司名称" /> | ||
| 54 | - </el-form-item> | ||
| 55 | - </el-col> | ||
| 56 | - <el-col :span="12"> | ||
| 57 | - <el-form-item label="公司简称" prop="companyCode"> | ||
| 58 | - <el-input v-model="baseForm.companyCode" clearable placeholder="公司简称" /> | ||
| 59 | - </el-form-item> | ||
| 60 | - </el-col> | ||
| 61 | - <el-col :span="12"> | ||
| 62 | - <el-form-item label="版权信息" prop="copyright"> | ||
| 63 | - <el-input v-model="baseForm.copyright" clearable placeholder="版权信息" /> | ||
| 64 | - </el-form-item> | ||
| 65 | - </el-col> | ||
| 66 | - <el-col :span="12"> | ||
| 67 | - <el-form-item label="公司法人" prop="companyContacts"> | ||
| 68 | - <el-input v-model="baseForm.companyContacts" clearable placeholder="公司法人" /> | ||
| 69 | - </el-form-item> | ||
| 70 | - </el-col> | ||
| 71 | - <el-col :span="12"> | ||
| 72 | - <el-form-item label="公司电话" prop="companyTelePhone"> | ||
| 73 | - <el-input v-model="baseForm.companyTelePhone" clearable placeholder="公司电话" /> | ||
| 74 | - </el-form-item> | ||
| 75 | - </el-col> | ||
| 76 | - <el-col :span="12"> | ||
| 77 | - <el-form-item label="公司邮箱" prop="companyEmail"> | ||
| 78 | - <el-input v-model="baseForm.companyEmail" clearable placeholder="公司邮箱" /> | ||
| 79 | - </el-form-item> | ||
| 80 | - </el-col> | ||
| 81 | - <el-col :span="24"> | ||
| 82 | - <el-form-item label="公司地址" prop="companyAddress"> | ||
| 83 | - <el-input v-model="baseForm.companyAddress" clearable placeholder="公司地址" /> | ||
| 84 | - </el-form-item> | ||
| 85 | - </el-col> | ||
| 86 | - <el-col :span="24"> | ||
| 87 | - <el-form-item label="系统描述" prop="sysDescription"> | ||
| 88 | - <el-input v-model="baseForm.sysDescription" type="textarea" :autosize="{ minRows: 4, maxRows: 10 }" | ||
| 89 | - placeholder="系统描述" /> | ||
| 90 | - </el-form-item> | ||
| 91 | - </el-col> | ||
| 92 | - </el-row> | ||
| 93 | - | ||
| 94 | - <div class="form-actions"> | ||
| 95 | - <el-button type="primary" size="small" :loading="btnLoading" class="saveBtn" @click="submitForm()">保 | ||
| 96 | - 存</el-button> | ||
| 97 | - </div> | 6 | + <div class="config-tab-panel"> |
| 7 | + <BasicConfigPanel | ||
| 8 | + :form="baseForm" | ||
| 9 | + :loading="listLoading" | ||
| 10 | + :saving="btnLoading" | ||
| 11 | + :logo-uploading="logoUploading" | ||
| 12 | + :logo-icon-uploading="logoIconUploading" | ||
| 13 | + :logo-preview="logoPreview" | ||
| 14 | + :on-upload-logo="uploadSysLogo" | ||
| 15 | + :on-upload-logo-icon="uploadSysLogoIcon" | ||
| 16 | + @save="submitForm" | ||
| 17 | + /> | ||
| 98 | </div> | 18 | </div> |
| 99 | </el-tab-pane> | 19 | </el-tab-pane> |
| 100 | <el-tab-pane label="安全设置" name="second"> | 20 | <el-tab-pane label="安全设置" name="second"> |
| 101 | - <el-alert title="注意:系统登录安全、黑名单IP限制" type="warning" :closable="false" show-icon /> | ||
| 102 | - <el-row> | ||
| 103 | - <el-col :span="18"> | ||
| 104 | - <el-divider content-position="left">登录设置</el-divider> | ||
| 105 | - <el-form-item label="单一登录"> | ||
| 106 | - <el-select v-model="baseForm.singleLogin" placeholder="请选择"> | ||
| 107 | - <el-option label="后登录踢出先登录" value="1" /> | ||
| 108 | - </el-select> | ||
| 109 | - </el-form-item> | ||
| 110 | - <el-form-item label="超时登出"> | ||
| 111 | - <el-input-number v-model="baseForm.tokenTimeout" :min="1" :precision="0" :step="1" | ||
| 112 | - controls-position="right" /> 分钟 | ||
| 113 | - </el-form-item> | ||
| 114 | - <el-form-item label="上次登录"> | ||
| 115 | - <el-switch v-model="baseForm.lastLoginTimeSwitch" :active-value="1" :inactive-value="0" /> | ||
| 116 | - </el-form-item> | ||
| 117 | - <el-form-item> | ||
| 118 | - <el-card class="box-card" shadow="never" style="width: 300px;" :body-style="{ padding: '0px 20px' }"> | ||
| 119 | - <div slot="header"> | ||
| 120 | - <span>上次登录信息提示</span> | ||
| 121 | - <i style="float: right; padding: 11px 0" class="el-icon-close" /> | ||
| 122 | - </div> | ||
| 123 | - <div class="item"> | ||
| 124 | - <p>时间:2018-10-17 12:40</p> | ||
| 125 | - <p>地点:上海市</p> | ||
| 126 | - <p>IP:255.255.0.0</p> | ||
| 127 | - </div> | ||
| 128 | - </el-card> | ||
| 129 | - </el-form-item> | ||
| 130 | - <el-divider content-position="left">访问设置</el-divider> | ||
| 131 | - <el-form-item label="开启验证"> | ||
| 132 | - <el-switch v-model="baseForm.whitelistSwitch" :active-value="1" :inactive-value="0" /> | ||
| 133 | - </el-form-item> | ||
| 134 | - <el-form-item label="页面水印"> | ||
| 135 | - <el-switch v-model="baseForm.watermarkSwitch" :active-value="1" :inactive-value="0" | ||
| 136 | - @change="onWatermarkSwitchChange" /> | ||
| 137 | - <span class="form-tip" style="margin-left: 12px;">开启后管理后台与员工手机端均显示「部门、姓名、电话、时间」水印(请点保存持久化)</span> | ||
| 138 | - </el-form-item> | ||
| 139 | - <el-form-item label="PC水印深浅" v-show="Number(baseForm.watermarkSwitch) === 1"> | ||
| 140 | - <div class="opacity-row"> | ||
| 141 | - <el-slider v-model="baseForm.watermarkOpacityPc" :min="1" :max="100" :step="1" show-input | ||
| 142 | - input-size="small" style="width: 360px;" @change="onWatermarkOpacityChange" /> | ||
| 143 | - <span class="form-tip">仅影响管理后台,数值越大水印越深(默认 10)</span> | ||
| 144 | - </div> | ||
| 145 | - </el-form-item> | ||
| 146 | - <el-form-item label="手机水印深浅" v-show="Number(baseForm.watermarkSwitch) === 1"> | ||
| 147 | - <div class="opacity-row"> | ||
| 148 | - <el-slider v-model="baseForm.watermarkOpacityApp" :min="1" :max="100" :step="1" show-input | ||
| 149 | - input-size="small" style="width: 360px;" /> | ||
| 150 | - <span class="form-tip">仅影响员工手机端,数值越大水印越深(默认 14)</span> | ||
| 151 | - </div> | ||
| 152 | - </el-form-item> | ||
| 153 | - <el-divider content-position="left">隐私设置</el-divider> | ||
| 154 | - <el-form-item label="手机号脱敏"> | ||
| 155 | - <el-switch v-model="baseForm.privacyMobileMask" :active-value="1" :inactive-value="0" | ||
| 156 | - @change="onPrivacyMobileMaskChange" /> | ||
| 157 | - <span class="form-tip" style="margin-left: 12px;">关闭时发送日志等展示明文手机号;开启后全站列表/详情/发送日志按脱敏规则展示(请点保存持久化)</span> | ||
| 158 | - </el-form-item> | ||
| 159 | - <el-form-item label="允许访问IP"> | ||
| 160 | - <el-input type="textarea" :autosize="{ minRows: 3, maxRows: 10 }" v-model="baseForm.whiteListIp" | ||
| 161 | - placeholder="允许访问IP" /> | ||
| 162 | - </el-form-item> | ||
| 163 | - <el-form-item> | ||
| 164 | - <el-button type="primary" size="small" :loading="btnLoading" class="saveBtn" @click="submitForm()">保 | ||
| 165 | - 存</el-button> | ||
| 166 | - </el-form-item> | ||
| 167 | - </el-col> | ||
| 168 | - </el-row> | 21 | + <div class="config-tab-panel"> |
| 22 | + <SecurityConfigPanel | ||
| 23 | + :form="baseForm" | ||
| 24 | + :loading="listLoading" | ||
| 25 | + :saving="btnLoading" | ||
| 26 | + @save="submitForm" | ||
| 27 | + @watermark-switch-change="onWatermarkSwitchChange" | ||
| 28 | + @watermark-opacity-change="onWatermarkOpacityChange" | ||
| 29 | + @privacy-mobile-mask-change="onPrivacyMobileMaskChange" | ||
| 30 | + /> | ||
| 31 | + </div> | ||
| 169 | </el-tab-pane> | 32 | </el-tab-pane> |
| 170 | <el-tab-pane label="短信设置" name="sms"> | 33 | <el-tab-pane label="短信设置" name="sms"> |
| 171 | - <sms-config-panel /> | 34 | + <div class="config-tab-panel"> |
| 35 | + <sms-config-panel /> | ||
| 36 | + </div> | ||
| 172 | </el-tab-pane> | 37 | </el-tab-pane> |
| 173 | <el-tab-pane label="AI 配置" name="third"> | 38 | <el-tab-pane label="AI 配置" name="third"> |
| 174 | - <el-alert title="当前主推通义千问(阿里云 DashScope)。语音识别可与文本对话使用不同密钥;语音识别结果再走「文本总结」时使用下方对话配置。" type="info" | ||
| 175 | - :closable="false" show-icon /> | ||
| 176 | - <el-row style="margin-top: 16px"> | ||
| 177 | - <el-col :span="18"> | ||
| 178 | - <el-divider content-position="left">语音识别</el-divider> | ||
| 179 | - <p class="form-tip"> | ||
| 180 | - Paraformer<strong>异步转写(录音文件识别)</strong>专用;留空则使用官方:<code>{{ defaultParaformerSubmitUrl }}</code>。 | ||
| 181 | - <strong>禁止</strong>填写下方「对话」用的 <code>compatible-mode/v1</code>,否则会提交失败。 | ||
| 182 | - </p> | ||
| 183 | - <el-form-item label="语音服务地址" prop="aiSpeechApiUrl"> | ||
| 184 | - <el-input v-model="baseForm.aiSpeechApiUrl" clearable | ||
| 185 | - placeholder="留空即可;请勿填 compatible-mode/v1(那是对话网关)" /> | ||
| 186 | - </el-form-item> | ||
| 187 | - <el-form-item label="语音 API Key" prop="aiSpeechApiKey"> | ||
| 188 | - <el-input v-model="baseForm.aiSpeechApiKey" show-password clearable autocomplete="new-password" | ||
| 189 | - placeholder="语音识别使用的 API Key" /> | ||
| 190 | - </el-form-item> | ||
| 191 | - <p class="form-tip"> | ||
| 192 | - 与百炼「录音文件识别」里的 <code>model</code> 一致(<strong>Paraformer 系列</strong>);留空则默认 <code>paraformer-v2</code>。 | ||
| 193 | - <strong>禁止</strong>填 <code>qwen*</code>、实时/口译类模型名(如 | ||
| 194 | - <code>qwen3-livetranslate-flash-realtime-2025-09-22</code>),那些属于另一条产品线,异步文件识别用不了。 | ||
| 195 | - </p> | ||
| 196 | - <el-form-item label="语音识别模型" prop="aiSpeechModel"> | ||
| 197 | - <el-input v-model="baseForm.aiSpeechModel" clearable | ||
| 198 | - placeholder="paraformer-v2 或 paraformer-8k-v2;勿填 qwen / livetranslate / realtime" /> | ||
| 199 | - </el-form-item> | ||
| 200 | - <el-divider content-position="left">对话与文本总结</el-divider> | ||
| 201 | - <p class="form-tip">OpenAI 兼容模式:用于文案总结等,需与下方模型名搭配(如兼容模式接入千问)。</p> | ||
| 202 | - <el-form-item label="对话接口地址" prop="aiChatApiUrl"> | ||
| 203 | - <el-input v-model="baseForm.aiChatApiUrl" clearable | ||
| 204 | - placeholder="示例:https://dashscope.aliyuncs.com/compatible-mode/v1" /> | ||
| 205 | - </el-form-item> | ||
| 206 | - <el-form-item label="模型名称" prop="aiChatModel"> | ||
| 207 | - <el-input v-model="baseForm.aiChatModel" clearable placeholder="如 qwen-plus、qwen-turbo" /> | ||
| 208 | - </el-form-item> | ||
| 209 | - <el-form-item label="文本 API Key" prop="aiChatApiKey"> | ||
| 210 | - <el-input v-model="baseForm.aiChatApiKey" show-password clearable autocomplete="new-password" | ||
| 211 | - placeholder="可与语音使用同一密钥或单独的子 Key" /> | ||
| 212 | - </el-form-item> | ||
| 213 | - <el-divider content-position="left">备注归纳提示词</el-divider> | ||
| 214 | - <p class="form-tip">发给千问的 <strong>system</strong> 角色,用来约束「删掉废话、保留关键」。留空则用系统内置默认(美业健康师备注风格)。</p> | ||
| 215 | - <el-form-item label="归纳提示词" prop="aiSummarizeSystemPrompt"> | ||
| 216 | - <el-input v-model="baseForm.aiSummarizeSystemPrompt" type="textarea" | ||
| 217 | - :autosize="{ minRows: 5, maxRows: 18 }" placeholder='例如:你是一名资深美业咨询师,请将下方口语转写整理为一句专业、可追溯的工单备注……' /> | ||
| 218 | - </el-form-item> | ||
| 219 | - <el-form-item> | ||
| 220 | - <el-button type="primary" size="small" :loading="btnLoading" class="saveBtn" @click="submitForm()">保 | ||
| 221 | - 存</el-button> | ||
| 222 | - </el-form-item> | ||
| 223 | - </el-col> | ||
| 224 | - </el-row> | 39 | + <div class="config-tab-panel"> |
| 40 | + <AiConfigPanel | ||
| 41 | + :form="baseForm" | ||
| 42 | + :loading="listLoading" | ||
| 43 | + :saving="btnLoading" | ||
| 44 | + :default-paraformer-submit-url="defaultParaformerSubmitUrl" | ||
| 45 | + @save="submitForm" | ||
| 46 | + /> | ||
| 47 | + </div> | ||
| 225 | </el-tab-pane> | 48 | </el-tab-pane> |
| 226 | <el-tab-pane label="业务配置" name="business"> | 49 | <el-tab-pane label="业务配置" name="business"> |
| 227 | - <BusinessConfigPanel :form="baseForm" :loading="listLoading" :saving="btnLoading" @save="submitForm" /> | 50 | + <div class="config-tab-panel"> |
| 51 | + <BusinessConfigPanel :form="baseForm" :loading="listLoading" :saving="btnLoading" @save="submitForm" /> | ||
| 52 | + </div> | ||
| 228 | </el-tab-pane> | 53 | </el-tab-pane> |
| 229 | <el-tab-pane label="会员商城支付" name="mallPay"> | 54 | <el-tab-pane label="会员商城支付" name="mallPay"> |
| 230 | - <el-alert title="会员商城微信支付参数。未开启或参数缺失时,小程序下单走模拟支付(MockPay)便于联调;正式收款需填写商户参数并开启。" type="info" :closable="false" | ||
| 231 | - show-icon /> | ||
| 232 | - <el-row style="margin-top: 16px"> | ||
| 233 | - <el-col :span="18"> | ||
| 234 | - <el-divider content-position="left">微信支付配置</el-divider> | ||
| 235 | - <el-form-item label="启用支付"> | ||
| 236 | - <el-switch v-model="mallPayForm.PayEnabled" active-value="1" inactive-value="0" /> | ||
| 237 | - <span class="form-tip" style="margin-left: 12px;">关闭时小程序下单使用模拟支付</span> | ||
| 238 | - </el-form-item> | ||
| 239 | - <el-form-item label="小程序 AppId"> | ||
| 240 | - <el-input v-model="mallPayForm.AppId" clearable placeholder="wx 开头的小程序 AppId" /> | ||
| 241 | - </el-form-item> | ||
| 242 | - <el-form-item label="小程序 AppSecret"> | ||
| 243 | - <el-input v-model="mallPayForm.AppSecret" show-password clearable autocomplete="new-password" | ||
| 244 | - placeholder="小程序 AppSecret" /> | ||
| 245 | - </el-form-item> | ||
| 246 | - <el-form-item label="微信商户号"> | ||
| 247 | - <el-input v-model="mallPayForm.MchId" clearable placeholder="微信支付商户号 mchid" /> | ||
| 248 | - </el-form-item> | ||
| 249 | - <el-form-item label="APIv3 密钥"> | ||
| 250 | - <el-input v-model="mallPayForm.ApiV3Key" show-password clearable autocomplete="new-password" | ||
| 251 | - placeholder="微信支付 APIv3 密钥" /> | ||
| 252 | - </el-form-item> | ||
| 253 | - <el-form-item label="支付回调地址"> | ||
| 254 | - <el-input v-model="mallPayForm.NotifyUrl" clearable | ||
| 255 | - placeholder="如 https://域名/api/Extend/LqMallOrder/PayNotify" /> | ||
| 256 | - </el-form-item> | ||
| 257 | - <el-form-item> | ||
| 258 | - <el-button type="primary" size="small" :loading="mallPayLoading" class="saveBtn" | ||
| 259 | - @click="saveMallPay()">保 存</el-button> | ||
| 260 | - </el-form-item> | ||
| 261 | - </el-col> | ||
| 262 | - </el-row> | 55 | + <div class="config-tab-panel"> |
| 56 | + <MallPayConfigPanel | ||
| 57 | + v-if="mallPayReady" | ||
| 58 | + :form="mallPayForm" | ||
| 59 | + :saving="mallPayLoading" | ||
| 60 | + @save="saveMallPay" | ||
| 61 | + /> | ||
| 62 | + </div> | ||
| 263 | </el-tab-pane> | 63 | </el-tab-pane> |
| 264 | </el-tabs> | 64 | </el-tabs> |
| 265 | </el-form> | 65 | </el-form> |
| @@ -273,6 +73,10 @@ import { | @@ -273,6 +73,10 @@ import { | ||
| 273 | } from '@/api/system/sysConfig' | 73 | } from '@/api/system/sysConfig' |
| 274 | import { getPayConfig, savePayConfig } from '@/api/extend/mallConfig' | 74 | import { getPayConfig, savePayConfig } from '@/api/extend/mallConfig' |
| 275 | import BusinessConfigPanel from './business-config-panel' | 75 | import BusinessConfigPanel from './business-config-panel' |
| 76 | +import BasicConfigPanel from './basic-config-panel' | ||
| 77 | +import SecurityConfigPanel from './security-config-panel' | ||
| 78 | +import AiConfigPanel from './ai-config-panel' | ||
| 79 | +import MallPayConfigPanel from './mall-pay-config-panel' | ||
| 276 | import SmsConfigPanel from './sms-config-panel' | 80 | import SmsConfigPanel from './sms-config-panel' |
| 277 | import { normalizeBusinessConfigFields } from '@/utils/consume-admin-policy' | 81 | import { normalizeBusinessConfigFields } from '@/utils/consume-admin-policy' |
| 278 | import { ossUploadFile } from '@/utils/oss-upload' | 82 | import { ossUploadFile } from '@/utils/oss-upload' |
| @@ -281,7 +85,14 @@ import getPageTitle from '@/utils/get-page-title' | @@ -281,7 +85,14 @@ import getPageTitle from '@/utils/get-page-title' | ||
| 281 | 85 | ||
| 282 | export default { | 86 | export default { |
| 283 | name: 'system-sysConfig', | 87 | name: 'system-sysConfig', |
| 284 | - components: { BusinessConfigPanel, SmsConfigPanel }, | 88 | + components: { |
| 89 | + BusinessConfigPanel, | ||
| 90 | + BasicConfigPanel, | ||
| 91 | + SecurityConfigPanel, | ||
| 92 | + AiConfigPanel, | ||
| 93 | + MallPayConfigPanel, | ||
| 94 | + SmsConfigPanel | ||
| 95 | + }, | ||
| 285 | data() { | 96 | data() { |
| 286 | return { | 97 | return { |
| 287 | defaultParaformerSubmitUrl: 'https://dashscope.aliyuncs.com/api/v1/services/audio/asr/transcription', | 98 | defaultParaformerSubmitUrl: 'https://dashscope.aliyuncs.com/api/v1/services/audio/asr/transcription', |
| @@ -291,6 +102,7 @@ export default { | @@ -291,6 +102,7 @@ export default { | ||
| 291 | logoUploading: false, | 102 | logoUploading: false, |
| 292 | logoIconUploading: false, | 103 | logoIconUploading: false, |
| 293 | mallPayLoading: false, | 104 | mallPayLoading: false, |
| 105 | + mallPayReady: false, | ||
| 294 | mallPayForm: { | 106 | mallPayForm: { |
| 295 | MchId: '', | 107 | MchId: '', |
| 296 | ApiV3Key: '', | 108 | ApiV3Key: '', |
| @@ -354,6 +166,7 @@ export default { | @@ -354,6 +166,7 @@ export default { | ||
| 354 | aiChatApiKey: '', | 166 | aiChatApiKey: '', |
| 355 | aiSummarizeSystemPrompt: '', | 167 | aiSummarizeSystemPrompt: '', |
| 356 | consumeEditableDays: 3, | 168 | consumeEditableDays: 3, |
| 169 | + consumeAllowCrossMonthEdit: 0, | ||
| 357 | adminConsumeVoidEditableDays: 0, | 170 | adminConsumeVoidEditableDays: 0, |
| 358 | adminConsumeDeleteEditableDays: 0, | 171 | adminConsumeDeleteEditableDays: 0, |
| 359 | abnormalConsumeAmountTolerance: 0.01, | 172 | abnormalConsumeAmountTolerance: 0.01, |
| @@ -377,6 +190,7 @@ export default { | @@ -377,6 +190,7 @@ export default { | ||
| 377 | methods: { | 190 | methods: { |
| 378 | // 加载会员商城支付配置(Category=MallPayment) | 191 | // 加载会员商城支付配置(Category=MallPayment) |
| 379 | initMallPayConfig() { | 192 | initMallPayConfig() { |
| 193 | + this.mallPayLoading = true | ||
| 380 | getPayConfig().then(res => { | 194 | getPayConfig().then(res => { |
| 381 | const data = (res && res.data) || {} | 195 | const data = (res && res.data) || {} |
| 382 | this.mallPayForm = { | 196 | this.mallPayForm = { |
| @@ -387,7 +201,10 @@ export default { | @@ -387,7 +201,10 @@ export default { | ||
| 387 | NotifyUrl: data.notifyUrl || '', | 201 | NotifyUrl: data.notifyUrl || '', |
| 388 | PayEnabled: data.payEnabled === '1' || data.payEnabled === 1 ? '1' : '0' | 202 | PayEnabled: data.payEnabled === '1' || data.payEnabled === 1 ? '1' : '0' |
| 389 | } | 203 | } |
| 390 | - }).catch(() => { }) | 204 | + }).catch(() => { }).finally(() => { |
| 205 | + this.mallPayLoading = false | ||
| 206 | + this.mallPayReady = true | ||
| 207 | + }) | ||
| 391 | }, | 208 | }, |
| 392 | // 保存会员商城支付配置 | 209 | // 保存会员商城支付配置 |
| 393 | saveMallPay() { | 210 | saveMallPay() { |
| @@ -421,6 +238,9 @@ export default { | @@ -421,6 +238,9 @@ export default { | ||
| 421 | this.baseForm.commissionTierHelpSwitch = this.baseForm.commissionTierHelpSwitch == null | 238 | this.baseForm.commissionTierHelpSwitch = this.baseForm.commissionTierHelpSwitch == null |
| 422 | ? 1 | 239 | ? 1 |
| 423 | : (Number(this.baseForm.commissionTierHelpSwitch) === 1 ? 1 : 0) | 240 | : (Number(this.baseForm.commissionTierHelpSwitch) === 1 ? 1 : 0) |
| 241 | + this.baseForm.consumeAllowCrossMonthEdit = this.baseForm.consumeAllowCrossMonthEdit == null | ||
| 242 | + ? 0 | ||
| 243 | + : (Number(this.baseForm.consumeAllowCrossMonthEdit) === 1 ? 1 : 0) | ||
| 424 | this.baseForm = normalizeBusinessConfigFields(this.baseForm) | 244 | this.baseForm = normalizeBusinessConfigFields(this.baseForm) |
| 425 | this.syncSysBrand() | 245 | this.syncSysBrand() |
| 426 | this.listLoading = false | 246 | this.listLoading = false |
| @@ -493,6 +313,9 @@ export default { | @@ -493,6 +313,9 @@ export default { | ||
| 493 | payload.commissionTierHelpSwitch = payload.commissionTierHelpSwitch == null | 313 | payload.commissionTierHelpSwitch = payload.commissionTierHelpSwitch == null |
| 494 | ? 1 | 314 | ? 1 |
| 495 | : (Number(payload.commissionTierHelpSwitch) === 1 ? 1 : 0) | 315 | : (Number(payload.commissionTierHelpSwitch) === 1 ? 1 : 0) |
| 316 | + payload.consumeAllowCrossMonthEdit = payload.consumeAllowCrossMonthEdit == null | ||
| 317 | + ? 0 | ||
| 318 | + : (Number(payload.consumeAllowCrossMonthEdit) === 1 ? 1 : 0) | ||
| 496 | updateSystemConfig(payload).then(res => { | 319 | updateSystemConfig(payload).then(res => { |
| 497 | this.syncSysBrand() | 320 | this.syncSysBrand() |
| 498 | this.$message({ | 321 | this.$message({ |
| @@ -525,115 +348,23 @@ export default { | @@ -525,115 +348,23 @@ export default { | ||
| 525 | } | 348 | } |
| 526 | 349 | ||
| 527 | >>>.el-tabs--border-card>.el-tabs__content { | 350 | >>>.el-tabs--border-card>.el-tabs__content { |
| 528 | - padding: 30px 36px 10px 36px; | ||
| 529 | - } | ||
| 530 | - | ||
| 531 | - .saveBtn { | ||
| 532 | - width: 100px; | ||
| 533 | - } | ||
| 534 | - | ||
| 535 | - .basic-settings { | ||
| 536 | - max-width: 1100px; | ||
| 537 | - } | ||
| 538 | - | ||
| 539 | - .basic-settings>>>.el-divider { | ||
| 540 | - margin: 8px 0 20px; | ||
| 541 | - } | ||
| 542 | - | ||
| 543 | - .basic-settings>>>.el-divider:first-child { | ||
| 544 | - margin-top: 0; | ||
| 545 | - } | ||
| 546 | - | ||
| 547 | - .brand-row { | ||
| 548 | - margin-bottom: 4px; | ||
| 549 | - } | ||
| 550 | - | ||
| 551 | - .brand-row .el-form-item { | ||
| 552 | - margin-bottom: 8px; | ||
| 553 | - } | ||
| 554 | - | ||
| 555 | - .brand-row .form-tip { | ||
| 556 | - margin-top: 8px; | ||
| 557 | - margin-bottom: 0; | ||
| 558 | - min-height: 40px; | ||
| 559 | - } | ||
| 560 | - | ||
| 561 | - .form-actions { | ||
| 562 | - margin-top: 8px; | ||
| 563 | - padding-left: 120px; | ||
| 564 | - } | ||
| 565 | - | ||
| 566 | - .form-tip { | ||
| 567 | - color: #909399; | ||
| 568 | - font-size: 13px; | ||
| 569 | - line-height: 1.6; | ||
| 570 | - margin: 0 0 12px 0; | ||
| 571 | - padding-left: 0; | ||
| 572 | - } | ||
| 573 | - | ||
| 574 | - .opacity-row { | ||
| 575 | - display: flex; | ||
| 576 | - align-items: center; | ||
| 577 | - flex-wrap: wrap; | ||
| 578 | - gap: 12px; | ||
| 579 | - } | ||
| 580 | - | ||
| 581 | - .opacity-row .form-tip { | ||
| 582 | - margin: 0; | ||
| 583 | - } | ||
| 584 | - | ||
| 585 | - .img-uploader { | ||
| 586 | - display: flex; | ||
| 587 | - align-items: center; | ||
| 588 | - gap: 12px; | ||
| 589 | - } | ||
| 590 | - | ||
| 591 | - .single-uploader ::v-deep .el-upload { | ||
| 592 | - border: 1px dashed #d9d9d9; | ||
| 593 | - border-radius: 6px; | ||
| 594 | - cursor: pointer; | ||
| 595 | - width: 80px; | ||
| 596 | - height: 80px; | ||
| 597 | - display: flex; | ||
| 598 | - align-items: center; | ||
| 599 | - justify-content: center; | 351 | + padding: 16px 20px 10px; |
| 352 | + height: calc(100% - 40px); | ||
| 600 | overflow: hidden; | 353 | overflow: hidden; |
| 601 | - | ||
| 602 | - &:hover { | ||
| 603 | - border-color: #409EFF; | ||
| 604 | - } | ||
| 605 | } | 354 | } |
| 606 | 355 | ||
| 607 | - .single-icon { | ||
| 608 | - font-size: 22px; | ||
| 609 | - color: #8c939d; | 356 | + >>>.el-tab-pane { |
| 357 | + height: 100%; | ||
| 610 | } | 358 | } |
| 611 | 359 | ||
| 612 | - .single-img { | ||
| 613 | - width: 80px; | ||
| 614 | - height: 80px; | ||
| 615 | - object-fit: contain; | ||
| 616 | - display: block; | ||
| 617 | - } | ||
| 618 | -} | ||
| 619 | - | ||
| 620 | -/* 暗黑:覆盖本页写死的浅色上传/提示(祖先 .ncc-content-dark) */ | ||
| 621 | -.ncc-content-dark .systemConfig { | ||
| 622 | - .form-tip { | ||
| 623 | - color: rgba(255, 255, 255, 0.55); | ||
| 624 | - } | ||
| 625 | - | ||
| 626 | - .single-uploader ::v-deep .el-upload { | ||
| 627 | - background-color: #262626; | ||
| 628 | - border-color: rgba(255, 255, 255, 0.18); | ||
| 629 | - | ||
| 630 | - &:hover { | ||
| 631 | - border-color: #1890ff; | ||
| 632 | - } | 360 | + .config-tab-panel { |
| 361 | + margin: 0; | ||
| 362 | + height: 100%; | ||
| 363 | + min-height: calc(100vh - 200px); | ||
| 633 | } | 364 | } |
| 634 | 365 | ||
| 635 | - .single-icon { | ||
| 636 | - color: rgba(255, 255, 255, 0.55); | 366 | + .saveBtn { |
| 367 | + width: 100px; | ||
| 637 | } | 368 | } |
| 638 | } | 369 | } |
| 639 | </style> | 370 | </style> |
antis-ncc-admin/src/views/system/sysConfig/mall-pay-config-panel.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <ConfigPanelShell | ||
| 3 | + :modules="modules" | ||
| 4 | + :loading="loading" | ||
| 5 | + :saving="saving" | ||
| 6 | + :show-content-header="false" | ||
| 7 | + storage-key="mallPayConfigActiveModule" | ||
| 8 | + alert-title="会员商城微信支付参数。未开启或参数缺失时,小程序下单走模拟支付(MockPay)便于联调。" | ||
| 9 | + alert-type="info" | ||
| 10 | + @save="$emit('save')" | ||
| 11 | + > | ||
| 12 | + <template #payment> | ||
| 13 | + <el-card shadow="never" class="config-section"> | ||
| 14 | + <div slot="header" class="config-section__header"> | ||
| 15 | + <span>微信支付</span> | ||
| 16 | + <span class="config-section__tag">会员商城</span> | ||
| 17 | + </div> | ||
| 18 | + <p class="config-section__tip"> | ||
| 19 | + 正式收款需填写商户参数并开启;关闭时小程序下单使用模拟支付。 | ||
| 20 | + </p> | ||
| 21 | + <el-form-item label="启用支付"> | ||
| 22 | + <el-switch v-model="form.PayEnabled" active-value="1" inactive-value="0" /> | ||
| 23 | + <span class="field-suffix field-suffix--text"> | ||
| 24 | + {{ form.PayEnabled === '1' ? '已开启' : '已关闭(模拟支付)' }} | ||
| 25 | + </span> | ||
| 26 | + </el-form-item> | ||
| 27 | + <el-row :gutter="20"> | ||
| 28 | + <el-col :span="12"> | ||
| 29 | + <el-form-item label="小程序 AppId"> | ||
| 30 | + <el-input v-model="form.AppId" clearable placeholder="wx 开头的小程序 AppId" /> | ||
| 31 | + </el-form-item> | ||
| 32 | + </el-col> | ||
| 33 | + <el-col :span="12"> | ||
| 34 | + <el-form-item label="微信商户号"> | ||
| 35 | + <el-input v-model="form.MchId" clearable placeholder="微信支付商户号 mchid" /> | ||
| 36 | + </el-form-item> | ||
| 37 | + </el-col> | ||
| 38 | + <el-col :span="12"> | ||
| 39 | + <el-form-item label="小程序 AppSecret"> | ||
| 40 | + <el-input v-model="form.AppSecret" show-password clearable autocomplete="new-password" | ||
| 41 | + placeholder="小程序 AppSecret" /> | ||
| 42 | + </el-form-item> | ||
| 43 | + </el-col> | ||
| 44 | + <el-col :span="12"> | ||
| 45 | + <el-form-item label="APIv3 密钥"> | ||
| 46 | + <el-input v-model="form.ApiV3Key" show-password clearable autocomplete="new-password" | ||
| 47 | + placeholder="微信支付 APIv3 密钥" /> | ||
| 48 | + </el-form-item> | ||
| 49 | + </el-col> | ||
| 50 | + <el-col :span="24"> | ||
| 51 | + <el-form-item label="支付回调地址"> | ||
| 52 | + <el-input v-model="form.NotifyUrl" clearable | ||
| 53 | + placeholder="如 https://域名/api/Extend/LqMallOrder/PayNotify" /> | ||
| 54 | + </el-form-item> | ||
| 55 | + </el-col> | ||
| 56 | + </el-row> | ||
| 57 | + </el-card> | ||
| 58 | + </template> | ||
| 59 | + </ConfigPanelShell> | ||
| 60 | +</template> | ||
| 61 | + | ||
| 62 | +<script> | ||
| 63 | +import ConfigPanelShell from './config-panel-shell.vue' | ||
| 64 | + | ||
| 65 | +const MODULES = [ | ||
| 66 | + { key: 'payment', label: '微信支付', desc: '会员商城收款参数', icon: 'el-icon-wallet', iconClass: 'module-icon--payment' } | ||
| 67 | +] | ||
| 68 | + | ||
| 69 | +export default { | ||
| 70 | + name: 'MallPayConfigPanel', | ||
| 71 | + components: { ConfigPanelShell }, | ||
| 72 | + props: { | ||
| 73 | + form: { type: Object, required: true }, | ||
| 74 | + loading: { type: Boolean, default: false }, | ||
| 75 | + saving: { type: Boolean, default: false } | ||
| 76 | + }, | ||
| 77 | + data() { | ||
| 78 | + return { modules: MODULES } | ||
| 79 | + } | ||
| 80 | +} | ||
| 81 | +</script> | ||
| 82 | + | ||
| 83 | +<style lang="scss" scoped> | ||
| 84 | +.config-section { | ||
| 85 | + border: 1px solid #ebeef5; | ||
| 86 | + | ||
| 87 | + &__header { | ||
| 88 | + display: flex; | ||
| 89 | + align-items: center; | ||
| 90 | + gap: 8px; | ||
| 91 | + font-weight: 500; | ||
| 92 | + } | ||
| 93 | + | ||
| 94 | + &__tag { | ||
| 95 | + padding: 0 8px; | ||
| 96 | + height: 22px; | ||
| 97 | + line-height: 22px; | ||
| 98 | + border-radius: 4px; | ||
| 99 | + background: #f4f4f5; | ||
| 100 | + color: #909399; | ||
| 101 | + font-size: 12px; | ||
| 102 | + font-weight: normal; | ||
| 103 | + } | ||
| 104 | + | ||
| 105 | + &__tip { | ||
| 106 | + margin: 0 0 12px; | ||
| 107 | + color: #909399; | ||
| 108 | + font-size: 13px; | ||
| 109 | + line-height: 1.6; | ||
| 110 | + } | ||
| 111 | +} | ||
| 112 | + | ||
| 113 | +.field-suffix { | ||
| 114 | + margin-left: 8px; | ||
| 115 | + color: #909399; | ||
| 116 | + font-size: 13px; | ||
| 117 | + | ||
| 118 | + &--text { | ||
| 119 | + display: inline-block; | ||
| 120 | + min-width: 120px; | ||
| 121 | + } | ||
| 122 | +} | ||
| 123 | + | ||
| 124 | +.module-icon--payment { color: #67C23A; } | ||
| 125 | + | ||
| 126 | +.ncc-content-dark .config-section { | ||
| 127 | + border-color: rgba(255, 255, 255, 0.08); | ||
| 128 | + | ||
| 129 | + &__tag, | ||
| 130 | + &__tip, | ||
| 131 | + .field-suffix { | ||
| 132 | + color: rgba(255, 255, 255, 0.55); | ||
| 133 | + } | ||
| 134 | + | ||
| 135 | + &__tag { | ||
| 136 | + background: rgba(255, 255, 255, 0.08); | ||
| 137 | + } | ||
| 138 | +} | ||
| 139 | +</style> |
antis-ncc-admin/src/views/system/sysConfig/security-config-panel.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <ConfigPanelShell | ||
| 3 | + :modules="modules" | ||
| 4 | + :loading="loading" | ||
| 5 | + :saving="saving" | ||
| 6 | + storage-key="securityConfigActiveModule" | ||
| 7 | + default-module="login" | ||
| 8 | + nav-theme="warning" | ||
| 9 | + alert-title="左侧选择安全模块,右侧仅展示当前配置;保存后全量生效。" | ||
| 10 | + alert-type="warning" | ||
| 11 | + @save="$emit('save')" | ||
| 12 | + > | ||
| 13 | + <template #login> | ||
| 14 | + <el-card shadow="never" class="config-section"> | ||
| 15 | + <div slot="header" class="config-section__header"> | ||
| 16 | + <span>登录策略</span> | ||
| 17 | + <span class="config-section__tag">全站</span> | ||
| 18 | + </div> | ||
| 19 | + <p class="config-section__tip"> | ||
| 20 | + 控制账号登录互踢、会话超时与登录成功后的提示信息。 | ||
| 21 | + </p> | ||
| 22 | + <el-row :gutter="20"> | ||
| 23 | + <el-col :span="12"> | ||
| 24 | + <el-form-item label="单一登录"> | ||
| 25 | + <el-select v-model="form.singleLogin" placeholder="请选择" style="width: 100%"> | ||
| 26 | + <el-option label="后登录踢出先登录" value="1" /> | ||
| 27 | + </el-select> | ||
| 28 | + </el-form-item> | ||
| 29 | + </el-col> | ||
| 30 | + <el-col :span="12"> | ||
| 31 | + <el-form-item label="超时登出"> | ||
| 32 | + <el-input-number v-model="form.tokenTimeout" :min="1" :precision="0" :step="1" | ||
| 33 | + controls-position="right" /> | ||
| 34 | + <span class="field-suffix">分钟</span> | ||
| 35 | + </el-form-item> | ||
| 36 | + </el-col> | ||
| 37 | + </el-row> | ||
| 38 | + </el-card> | ||
| 39 | + | ||
| 40 | + <el-card shadow="never" class="config-section"> | ||
| 41 | + <div slot="header" class="config-section__header"> | ||
| 42 | + <span>上次登录提示</span> | ||
| 43 | + <span class="config-section__tag">登录页</span> | ||
| 44 | + </div> | ||
| 45 | + <p class="config-section__tip"> | ||
| 46 | + 开启后,用户登录成功时将展示上一次登录的时间、地点与 IP 信息,便于发现异常登录。 | ||
| 47 | + </p> | ||
| 48 | + <el-form-item label="上次登录"> | ||
| 49 | + <el-switch v-model="form.lastLoginTimeSwitch" :active-value="1" :inactive-value="0" /> | ||
| 50 | + <span class="field-suffix field-suffix--text"> | ||
| 51 | + {{ Number(form.lastLoginTimeSwitch) === 1 ? '已开启' : '已关闭' }} | ||
| 52 | + </span> | ||
| 53 | + </el-form-item> | ||
| 54 | + <div v-if="Number(form.lastLoginTimeSwitch) === 1" class="login-preview"> | ||
| 55 | + <div class="login-preview__title">效果预览</div> | ||
| 56 | + <div class="login-preview__card"> | ||
| 57 | + <div class="login-preview__card-title">上次登录信息提示</div> | ||
| 58 | + <p>时间:2018-10-17 12:40</p> | ||
| 59 | + <p>地点:上海市</p> | ||
| 60 | + <p>IP:255.255.0.0</p> | ||
| 61 | + </div> | ||
| 62 | + </div> | ||
| 63 | + </el-card> | ||
| 64 | + </template> | ||
| 65 | + | ||
| 66 | + <template #display> | ||
| 67 | + <el-card shadow="never" class="config-section"> | ||
| 68 | + <div slot="header" class="config-section__header"> | ||
| 69 | + <span>页面水印</span> | ||
| 70 | + <span class="config-section__tag">管理后台 / 手机端</span> | ||
| 71 | + </div> | ||
| 72 | + <p class="config-section__tip"> | ||
| 73 | + 开启后管理后台与员工手机端均显示「部门、姓名、电话、时间」水印;调整后请点击顶部保存持久化。 | ||
| 74 | + </p> | ||
| 75 | + <el-form-item label="页面水印"> | ||
| 76 | + <el-switch v-model="form.watermarkSwitch" :active-value="1" :inactive-value="0" | ||
| 77 | + @change="$emit('watermark-switch-change')" /> | ||
| 78 | + <span class="field-suffix field-suffix--text"> | ||
| 79 | + {{ Number(form.watermarkSwitch) === 1 ? '已开启' : '已关闭' }} | ||
| 80 | + </span> | ||
| 81 | + </el-form-item> | ||
| 82 | + <template v-if="Number(form.watermarkSwitch) === 1"> | ||
| 83 | + <el-form-item label="PC水印深浅"> | ||
| 84 | + <div class="opacity-row"> | ||
| 85 | + <el-slider v-model="form.watermarkOpacityPc" :min="1" :max="100" :step="1" show-input | ||
| 86 | + input-size="small" class="opacity-slider" @change="$emit('watermark-opacity-change')" /> | ||
| 87 | + <span class="form-tip">仅影响管理后台,数值越大越深(默认 10)</span> | ||
| 88 | + </div> | ||
| 89 | + </el-form-item> | ||
| 90 | + <el-form-item label="手机水印深浅"> | ||
| 91 | + <div class="opacity-row"> | ||
| 92 | + <el-slider v-model="form.watermarkOpacityApp" :min="1" :max="100" :step="1" show-input | ||
| 93 | + input-size="small" class="opacity-slider" /> | ||
| 94 | + <span class="form-tip">仅影响员工手机端,数值越大越深(默认 14)</span> | ||
| 95 | + </div> | ||
| 96 | + </el-form-item> | ||
| 97 | + </template> | ||
| 98 | + </el-card> | ||
| 99 | + | ||
| 100 | + <el-card shadow="never" class="config-section"> | ||
| 101 | + <div slot="header" class="config-section__header"> | ||
| 102 | + <span>手机号脱敏</span> | ||
| 103 | + <span class="config-section__tag">全站列表</span> | ||
| 104 | + </div> | ||
| 105 | + <p class="config-section__tip"> | ||
| 106 | + 关闭时发送日志等展示明文手机号;开启后全站列表、详情与发送日志按脱敏规则展示。 | ||
| 107 | + </p> | ||
| 108 | + <el-form-item label="手机号脱敏"> | ||
| 109 | + <el-switch v-model="form.privacyMobileMask" :active-value="1" :inactive-value="0" | ||
| 110 | + @change="$emit('privacy-mobile-mask-change')" /> | ||
| 111 | + <span class="field-suffix field-suffix--text"> | ||
| 112 | + {{ Number(form.privacyMobileMask) === 1 ? '已开启' : '已关闭' }} | ||
| 113 | + </span> | ||
| 114 | + </el-form-item> | ||
| 115 | + </el-card> | ||
| 116 | + </template> | ||
| 117 | + | ||
| 118 | + <template #access> | ||
| 119 | + <el-card shadow="never" class="config-section"> | ||
| 120 | + <div slot="header" class="config-section__header"> | ||
| 121 | + <span>IP 白名单</span> | ||
| 122 | + <span class="config-section__tag">访问控制</span> | ||
| 123 | + </div> | ||
| 124 | + <p class="config-section__tip"> | ||
| 125 | + 开启验证后,仅允许白名单内的 IP 访问系统;每行一个 IP,支持多行配置。 | ||
| 126 | + </p> | ||
| 127 | + <el-form-item label="开启验证"> | ||
| 128 | + <el-switch v-model="form.whitelistSwitch" :active-value="1" :inactive-value="0" /> | ||
| 129 | + <span class="field-suffix field-suffix--text"> | ||
| 130 | + {{ Number(form.whitelistSwitch) === 1 ? '已开启' : '已关闭' }} | ||
| 131 | + </span> | ||
| 132 | + </el-form-item> | ||
| 133 | + <el-form-item label="允许访问IP"> | ||
| 134 | + <el-input | ||
| 135 | + type="textarea" | ||
| 136 | + :autosize="{ minRows: 6, maxRows: 14 }" | ||
| 137 | + v-model="form.whiteListIp" | ||
| 138 | + placeholder="每行一个 IP,例如: 192.168.1.100 10.0.0.0/24" | ||
| 139 | + /> | ||
| 140 | + </el-form-item> | ||
| 141 | + </el-card> | ||
| 142 | + </template> | ||
| 143 | + </ConfigPanelShell> | ||
| 144 | +</template> | ||
| 145 | + | ||
| 146 | +<script> | ||
| 147 | +import ConfigPanelShell from './config-panel-shell.vue' | ||
| 148 | + | ||
| 149 | +const MODULES = [ | ||
| 150 | + { key: 'login', label: '登录与会话', desc: '互踢、超时与登录提示', icon: 'el-icon-user', iconClass: 'module-icon--login' }, | ||
| 151 | + { key: 'display', label: '水印与脱敏', desc: '页面水印与手机号隐私', icon: 'el-icon-view', iconClass: 'module-icon--display' }, | ||
| 152 | + { key: 'access', label: '访问控制', desc: 'IP 白名单限制', icon: 'el-icon-lock', iconClass: 'module-icon--access' } | ||
| 153 | +] | ||
| 154 | + | ||
| 155 | +export default { | ||
| 156 | + name: 'SecurityConfigPanel', | ||
| 157 | + components: { ConfigPanelShell }, | ||
| 158 | + props: { | ||
| 159 | + form: { | ||
| 160 | + type: Object, | ||
| 161 | + required: true | ||
| 162 | + }, | ||
| 163 | + loading: { | ||
| 164 | + type: Boolean, | ||
| 165 | + default: false | ||
| 166 | + }, | ||
| 167 | + saving: { | ||
| 168 | + type: Boolean, | ||
| 169 | + default: false | ||
| 170 | + } | ||
| 171 | + }, | ||
| 172 | + data() { | ||
| 173 | + return { | ||
| 174 | + modules: MODULES | ||
| 175 | + } | ||
| 176 | + } | ||
| 177 | +} | ||
| 178 | +</script> | ||
| 179 | + | ||
| 180 | +<style lang="scss" scoped> | ||
| 181 | +.module-icon { | ||
| 182 | + margin-right: 8px; | ||
| 183 | + font-size: 16px; | ||
| 184 | + flex-shrink: 0; | ||
| 185 | + | ||
| 186 | + &--login { | ||
| 187 | + color: #409EFF; | ||
| 188 | + } | ||
| 189 | + | ||
| 190 | + &--display { | ||
| 191 | + color: #67C23A; | ||
| 192 | + } | ||
| 193 | + | ||
| 194 | + &--access { | ||
| 195 | + color: #E6A23C; | ||
| 196 | + } | ||
| 197 | +} | ||
| 198 | + | ||
| 199 | +.config-section { | ||
| 200 | + margin-bottom: 16px; | ||
| 201 | + border: 1px solid #ebeef5; | ||
| 202 | + | ||
| 203 | + &:last-child { | ||
| 204 | + margin-bottom: 0; | ||
| 205 | + } | ||
| 206 | + | ||
| 207 | + &__header { | ||
| 208 | + display: flex; | ||
| 209 | + align-items: center; | ||
| 210 | + gap: 8px; | ||
| 211 | + font-weight: 500; | ||
| 212 | + } | ||
| 213 | + | ||
| 214 | + &__tag { | ||
| 215 | + padding: 0 8px; | ||
| 216 | + height: 22px; | ||
| 217 | + line-height: 22px; | ||
| 218 | + border-radius: 4px; | ||
| 219 | + background: #f4f4f5; | ||
| 220 | + color: #909399; | ||
| 221 | + font-size: 12px; | ||
| 222 | + font-weight: normal; | ||
| 223 | + } | ||
| 224 | + | ||
| 225 | + &__tip { | ||
| 226 | + margin: 0 0 12px; | ||
| 227 | + color: #909399; | ||
| 228 | + font-size: 13px; | ||
| 229 | + line-height: 1.6; | ||
| 230 | + } | ||
| 231 | +} | ||
| 232 | + | ||
| 233 | +.field-suffix { | ||
| 234 | + margin-left: 8px; | ||
| 235 | + color: #909399; | ||
| 236 | + font-size: 13px; | ||
| 237 | + | ||
| 238 | + &--text { | ||
| 239 | + display: inline-block; | ||
| 240 | + min-width: 48px; | ||
| 241 | + } | ||
| 242 | +} | ||
| 243 | + | ||
| 244 | +.form-tip { | ||
| 245 | + color: #909399; | ||
| 246 | + font-size: 13px; | ||
| 247 | + line-height: 1.6; | ||
| 248 | +} | ||
| 249 | + | ||
| 250 | +.opacity-row { | ||
| 251 | + display: flex; | ||
| 252 | + align-items: center; | ||
| 253 | + flex-wrap: wrap; | ||
| 254 | + gap: 12px; | ||
| 255 | + width: 100%; | ||
| 256 | +} | ||
| 257 | + | ||
| 258 | +.opacity-slider { | ||
| 259 | + width: 360px; | ||
| 260 | + max-width: 100%; | ||
| 261 | +} | ||
| 262 | + | ||
| 263 | +.login-preview { | ||
| 264 | + margin-top: 4px; | ||
| 265 | + padding-left: 120px; | ||
| 266 | + | ||
| 267 | + &__title { | ||
| 268 | + margin-bottom: 8px; | ||
| 269 | + color: #909399; | ||
| 270 | + font-size: 12px; | ||
| 271 | + } | ||
| 272 | + | ||
| 273 | + &__card { | ||
| 274 | + width: 300px; | ||
| 275 | + max-width: 100%; | ||
| 276 | + padding: 12px 16px; | ||
| 277 | + border: 1px solid #ebeef5; | ||
| 278 | + border-radius: 8px; | ||
| 279 | + background: #fafafa; | ||
| 280 | + | ||
| 281 | + &-title { | ||
| 282 | + margin-bottom: 8px; | ||
| 283 | + color: #303133; | ||
| 284 | + font-size: 14px; | ||
| 285 | + font-weight: 500; | ||
| 286 | + } | ||
| 287 | + | ||
| 288 | + p { | ||
| 289 | + margin: 4px 0; | ||
| 290 | + color: #606266; | ||
| 291 | + font-size: 13px; | ||
| 292 | + line-height: 1.6; | ||
| 293 | + } | ||
| 294 | + } | ||
| 295 | +} | ||
| 296 | + | ||
| 297 | +.ncc-content-dark { | ||
| 298 | + .config-section { | ||
| 299 | + border-color: rgba(255, 255, 255, 0.08); | ||
| 300 | + | ||
| 301 | + &__tag { | ||
| 302 | + background: rgba(255, 255, 255, 0.08); | ||
| 303 | + color: rgba(255, 255, 255, 0.55); | ||
| 304 | + } | ||
| 305 | + | ||
| 306 | + &__tip, | ||
| 307 | + .form-tip, | ||
| 308 | + .field-suffix { | ||
| 309 | + color: rgba(255, 255, 255, 0.55); | ||
| 310 | + } | ||
| 311 | + } | ||
| 312 | + | ||
| 313 | + .login-preview__card { | ||
| 314 | + border-color: rgba(255, 255, 255, 0.08); | ||
| 315 | + background: rgba(255, 255, 255, 0.03); | ||
| 316 | + | ||
| 317 | + &-title { | ||
| 318 | + color: rgba(255, 255, 255, 0.85); | ||
| 319 | + } | ||
| 320 | + | ||
| 321 | + p { | ||
| 322 | + color: rgba(255, 255, 255, 0.65); | ||
| 323 | + } | ||
| 324 | + } | ||
| 325 | +} | ||
| 326 | +</style> |
antis-ncc-admin/src/views/system/sysConfig/sms-config-panel.vue
| 1 | <template> | 1 | <template> |
| 2 | - <div class="sms-config-panel" v-loading="loading"> | ||
| 3 | - <!-- 短信基础配置 --> | ||
| 4 | - <el-card shadow="never" class="sms-config-card"> | ||
| 5 | - <div slot="header" class="sms-config-card__header"> | ||
| 6 | - <span>短信基础配置</span> | ||
| 7 | - <span class="sms-config-card__tag">阿里云短信</span> | ||
| 8 | - </div> | ||
| 9 | - <el-alert | ||
| 10 | - :title="configTip" | ||
| 11 | - type="info" | ||
| 12 | - :closable="false" | ||
| 13 | - show-icon | ||
| 14 | - class="sms-config-alert" | ||
| 15 | - /> | ||
| 16 | - <el-row :gutter="20"> | ||
| 17 | - <el-col :span="12"> | ||
| 18 | - <el-form-item label="AccessKeyId" label-width="120px"> | ||
| 19 | - <el-input v-model="configForm.accessKeyId" clearable placeholder="阿里云 AccessKeyId" /> | ||
| 20 | - </el-form-item> | ||
| 21 | - </el-col> | ||
| 22 | - <el-col :span="12"> | ||
| 23 | - <el-form-item label="AccessKeySecret" label-width="130px"> | 2 | + <div class="sms-config-panel"> |
| 3 | + <ConfigPanelShell | ||
| 4 | + :modules="modules" | ||
| 5 | + :loading="loading" | ||
| 6 | + :show-save="false" | ||
| 7 | + storage-key="smsConfigActiveModule" | ||
| 8 | + alert-title="左侧切换「基础配置」与「模板管理」;基础配置单独保存,模板支持增删改与测试发送。" | ||
| 9 | + alert-type="info" | ||
| 10 | + default-module="credentials" | ||
| 11 | + > | ||
| 12 | + <template #credentials> | ||
| 13 | + <el-card shadow="never" class="config-section"> | ||
| 14 | + <div slot="header" class="config-section__header"> | ||
| 15 | + <span>短信基础配置</span> | ||
| 16 | + <span class="config-section__tag">阿里云短信</span> | ||
| 17 | + </div> | ||
| 18 | + <el-alert :title="configTip" type="info" :closable="false" show-icon class="config-alert" /> | ||
| 19 | + <el-row :gutter="20"> | ||
| 20 | + <el-col :span="12"> | ||
| 21 | + <el-form-item label="AccessKeyId" label-width="120px"> | ||
| 22 | + <el-input v-model="configForm.accessKeyId" clearable placeholder="阿里云 AccessKeyId" /> | ||
| 23 | + </el-form-item> | ||
| 24 | + </el-col> | ||
| 25 | + <el-col :span="12"> | ||
| 26 | + <el-form-item label="AccessKeySecret" label-width="130px"> | ||
| 27 | + <el-input | ||
| 28 | + v-model="configForm.accessKeySecret" | ||
| 29 | + show-password | ||
| 30 | + clearable | ||
| 31 | + autocomplete="new-password" | ||
| 32 | + :placeholder="configForm.hasAccessKeySecret ? `已配置 ${configForm.accessKeySecretMasked},留空不修改` : '阿里云 AccessKeySecret'" | ||
| 33 | + /> | ||
| 34 | + </el-form-item> | ||
| 35 | + </el-col> | ||
| 36 | + <el-col :span="12"> | ||
| 37 | + <el-form-item label="默认签名" label-width="120px"> | ||
| 38 | + <el-input v-model="configForm.signName" clearable placeholder="绿纤聚财" /> | ||
| 39 | + </el-form-item> | ||
| 40 | + </el-col> | ||
| 41 | + <el-col :span="12"> | ||
| 42 | + <el-form-item label="厂商" label-width="120px"> | ||
| 43 | + <el-select v-model="configForm.company" style="width: 100%"> | ||
| 44 | + <el-option label="阿里云" value="1" /> | ||
| 45 | + <el-option label="腾讯云(仅兼容历史)" value="2" /> | ||
| 46 | + </el-select> | ||
| 47 | + </el-form-item> | ||
| 48 | + </el-col> | ||
| 49 | + <el-col :span="12"> | ||
| 50 | + <el-form-item label="Endpoint" label-width="120px"> | ||
| 51 | + <el-input v-model="configForm.endpoint" clearable placeholder="dysmsapi.aliyuncs.com" /> | ||
| 52 | + </el-form-item> | ||
| 53 | + </el-col> | ||
| 54 | + <el-col :span="12"> | ||
| 55 | + <el-form-item label="生日防重发" label-width="120px"> | ||
| 56 | + <el-switch v-model="configForm.preventDuplicate" :active-value="1" :inactive-value="0" /> | ||
| 57 | + <span class="form-tip">同会员同日同场景不重复发送</span> | ||
| 58 | + </el-form-item> | ||
| 59 | + </el-col> | ||
| 60 | + </el-row> | ||
| 61 | + <div class="panel-actions"> | ||
| 62 | + <el-button type="primary" size="small" :loading="configSaving" @click="saveConfig">保存短信配置</el-button> | ||
| 63 | + </div> | ||
| 64 | + </el-card> | ||
| 65 | + </template> | ||
| 66 | + | ||
| 67 | + <template #templates> | ||
| 68 | + <el-card shadow="never" class="config-section config-section--flush"> | ||
| 69 | + <div class="panel-actions panel-actions--between"> | ||
| 70 | + <div class="toolbar-left"> | ||
| 24 | <el-input | 71 | <el-input |
| 25 | - v-model="configForm.accessKeySecret" | ||
| 26 | - show-password | 72 | + v-model="templateQuery.name" |
| 73 | + size="small" | ||
| 27 | clearable | 74 | clearable |
| 28 | - autocomplete="new-password" | ||
| 29 | - :placeholder="configForm.hasAccessKeySecret ? `已配置 ${configForm.accessKeySecretMasked},留空不修改` : '阿里云 AccessKeySecret'" | 75 | + placeholder="名称" |
| 76 | + style="width: 140px; margin-right: 8px" | ||
| 77 | + @keyup.enter.native="searchTemplates" | ||
| 30 | /> | 78 | /> |
| 31 | - </el-form-item> | ||
| 32 | - </el-col> | ||
| 33 | - <el-col :span="12"> | ||
| 34 | - <el-form-item label="默认签名" label-width="120px"> | ||
| 35 | - <el-input v-model="configForm.signName" clearable placeholder="绿纤聚财" /> | ||
| 36 | - </el-form-item> | ||
| 37 | - </el-col> | ||
| 38 | - <el-col :span="12"> | ||
| 39 | - <el-form-item label="厂商" label-width="120px"> | ||
| 40 | - <el-select v-model="configForm.company" style="width: 100%"> | ||
| 41 | - <el-option label="阿里云" value="1" /> | ||
| 42 | - <el-option label="腾讯云(仅兼容历史)" value="2" /> | ||
| 43 | - </el-select> | ||
| 44 | - </el-form-item> | ||
| 45 | - </el-col> | ||
| 46 | - <el-col :span="12"> | ||
| 47 | - <el-form-item label="Endpoint" label-width="120px"> | ||
| 48 | - <el-input v-model="configForm.endpoint" clearable placeholder="dysmsapi.aliyuncs.com" /> | ||
| 49 | - </el-form-item> | ||
| 50 | - </el-col> | ||
| 51 | - <el-col :span="12"> | ||
| 52 | - <el-form-item label="生日防重发" label-width="120px"> | ||
| 53 | - <el-switch v-model="configForm.preventDuplicate" :active-value="1" :inactive-value="0" /> | ||
| 54 | - <span class="form-tip">同会员同日同场景不重复发送</span> | ||
| 55 | - </el-form-item> | ||
| 56 | - </el-col> | ||
| 57 | - </el-row> | ||
| 58 | - <div class="panel-actions"> | ||
| 59 | - <el-button type="primary" size="small" :loading="configSaving" @click="saveConfig">保存短信配置</el-button> | ||
| 60 | - </div> | ||
| 61 | - </el-card> | ||
| 62 | - | ||
| 63 | - <!-- 模板列表(主工作区) --> | ||
| 64 | - <el-card shadow="never" class="sms-config-card sms-config-card--main"> | ||
| 65 | - <div slot="header" class="sms-config-card__header"> | ||
| 66 | - <span>短信模板</span> | ||
| 67 | - <span class="sms-config-card__tag">主工作区</span> | ||
| 68 | - </div> | ||
| 69 | - | ||
| 70 | - <div class="panel-actions panel-actions--between"> | ||
| 71 | - <div class="toolbar-left"> | ||
| 72 | - <el-input | ||
| 73 | - v-model="templateQuery.name" | ||
| 74 | - size="small" | ||
| 75 | - clearable | ||
| 76 | - placeholder="名称" | ||
| 77 | - style="width: 140px; margin-right: 8px" | ||
| 78 | - @keyup.enter.native="searchTemplates" | ||
| 79 | - /> | ||
| 80 | - <el-input | ||
| 81 | - v-model="templateQuery.sceneCode" | ||
| 82 | - size="small" | ||
| 83 | - clearable | ||
| 84 | - placeholder="场景编码" | ||
| 85 | - style="width: 140px; margin-right: 8px" | ||
| 86 | - @keyup.enter.native="searchTemplates" | ||
| 87 | - /> | ||
| 88 | - <el-button size="small" icon="el-icon-search" @click="searchTemplates">查询</el-button> | ||
| 89 | - </div> | ||
| 90 | - <div class="toolbar-right"> | ||
| 91 | - <el-button size="small" icon="el-icon-message" @click="openTestDialog">测试发送</el-button> | ||
| 92 | - <el-button size="small" icon="el-icon-document" @click="openLogDrawer">发送日志</el-button> | ||
| 93 | - <el-button type="primary" size="small" icon="el-icon-plus" @click="openTemplateDialog()">新增模板</el-button> | 79 | + <el-input |
| 80 | + v-model="templateQuery.sceneCode" | ||
| 81 | + size="small" | ||
| 82 | + clearable | ||
| 83 | + placeholder="场景编码" | ||
| 84 | + style="width: 140px; margin-right: 8px" | ||
| 85 | + @keyup.enter.native="searchTemplates" | ||
| 86 | + /> | ||
| 87 | + <el-button size="small" icon="el-icon-search" @click="searchTemplates">查询</el-button> | ||
| 88 | + </div> | ||
| 89 | + <div class="toolbar-right"> | ||
| 90 | + <el-button size="small" icon="el-icon-message" @click="openTestDialog">测试发送</el-button> | ||
| 91 | + <el-button size="small" icon="el-icon-document" @click="openLogDrawer">发送日志</el-button> | ||
| 92 | + <el-button type="primary" size="small" icon="el-icon-plus" @click="openTemplateDialog()">新增模板</el-button> | ||
| 93 | + </div> | ||
| 94 | </div> | 94 | </div> |
| 95 | - </div> | ||
| 96 | 95 | ||
| 97 | - <NCC-table v-loading="templateLoading" :data="templateList"> | ||
| 98 | - <el-table-column prop="name" label="名称" min-width="120" show-overflow-tooltip> | ||
| 99 | - <template slot-scope="{ row }"> | ||
| 100 | - <div class="table-cell-with-icon"> | ||
| 101 | - <i class="el-icon-s-order cell-icon cell-icon--primary" /> | ||
| 102 | - <span>{{ row.name || '无' }}</span> | ||
| 103 | - </div> | ||
| 104 | - </template> | ||
| 105 | - </el-table-column> | ||
| 106 | - <el-table-column prop="templateCode" label="CODE" min-width="140" show-overflow-tooltip> | ||
| 107 | - <template slot-scope="{ row }">{{ row.templateCode || '无' }}</template> | ||
| 108 | - </el-table-column> | ||
| 109 | - <el-table-column prop="signName" label="签名" min-width="110" show-overflow-tooltip> | ||
| 110 | - <template slot-scope="{ row }">{{ row.signName || '无' }}</template> | ||
| 111 | - </el-table-column> | ||
| 112 | - <el-table-column label="类型" width="100"> | ||
| 113 | - <template slot-scope="{ row }"> | ||
| 114 | - <el-tag size="mini" :type="templateTypeTag(row.templateType)" disable-transitions> | ||
| 115 | - {{ row.templateTypeName || templateTypeName(row.templateType) }} | ||
| 116 | - </el-tag> | ||
| 117 | - </template> | ||
| 118 | - </el-table-column> | ||
| 119 | - <el-table-column prop="sceneCode" label="场景" min-width="110" show-overflow-tooltip> | ||
| 120 | - <template slot-scope="{ row }">{{ row.sceneCode || '无' }}</template> | ||
| 121 | - </el-table-column> | ||
| 122 | - <el-table-column label="启用" width="90" align="center"> | ||
| 123 | - <template slot-scope="{ row }"> | ||
| 124 | - <el-tag size="mini" :type="row.enabled === 1 ? 'success' : 'info'" disable-transitions> | ||
| 125 | - {{ row.enabled === 1 ? '启用' : '停用' }} | ||
| 126 | - </el-tag> | ||
| 127 | - </template> | ||
| 128 | - </el-table-column> | ||
| 129 | - <el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip> | ||
| 130 | - <template slot-scope="{ row }">{{ row.remark || '无' }}</template> | ||
| 131 | - </el-table-column> | ||
| 132 | - <el-table-column label="操作" width="220" fixed="right"> | ||
| 133 | - <template slot-scope="{ row }"> | ||
| 134 | - <el-button type="text" size="small" @click="openTemplateDialog(row)">编辑</el-button> | ||
| 135 | - <el-button type="text" size="small" @click="toggleEnabled(row)"> | ||
| 136 | - {{ row.enabled === 1 ? '停用' : '启用' }} | ||
| 137 | - </el-button> | ||
| 138 | - <el-button type="text" size="small" class="NCC-table-delBtn" @click="removeTemplate(row)">删除</el-button> | ||
| 139 | - </template> | ||
| 140 | - </el-table-column> | ||
| 141 | - </NCC-table> | ||
| 142 | - <pagination | ||
| 143 | - :total="templateTotal" | ||
| 144 | - :page.sync="templateQuery.currentPage" | ||
| 145 | - :limit.sync="templateQuery.pageSize" | ||
| 146 | - @pagination="loadTemplates" | ||
| 147 | - /> | ||
| 148 | - </el-card> | 96 | + <NCC-table v-loading="templateLoading" :data="templateList"> |
| 97 | + <el-table-column prop="name" label="名称" min-width="120" show-overflow-tooltip> | ||
| 98 | + <template slot-scope="{ row }"> | ||
| 99 | + <div class="table-cell-with-icon"> | ||
| 100 | + <i class="el-icon-s-order cell-icon cell-icon--primary" /> | ||
| 101 | + <span>{{ row.name || '无' }}</span> | ||
| 102 | + </div> | ||
| 103 | + </template> | ||
| 104 | + </el-table-column> | ||
| 105 | + <el-table-column prop="templateCode" label="CODE" min-width="140" show-overflow-tooltip> | ||
| 106 | + <template slot-scope="{ row }">{{ row.templateCode || '无' }}</template> | ||
| 107 | + </el-table-column> | ||
| 108 | + <el-table-column prop="signName" label="签名" min-width="110" show-overflow-tooltip> | ||
| 109 | + <template slot-scope="{ row }">{{ row.signName || '无' }}</template> | ||
| 110 | + </el-table-column> | ||
| 111 | + <el-table-column label="类型" width="100"> | ||
| 112 | + <template slot-scope="{ row }"> | ||
| 113 | + <el-tag size="mini" :type="templateTypeTag(row.templateType)" disable-transitions> | ||
| 114 | + {{ row.templateTypeName || templateTypeName(row.templateType) }} | ||
| 115 | + </el-tag> | ||
| 116 | + </template> | ||
| 117 | + </el-table-column> | ||
| 118 | + <el-table-column prop="sceneCode" label="场景" min-width="110" show-overflow-tooltip> | ||
| 119 | + <template slot-scope="{ row }">{{ row.sceneCode || '无' }}</template> | ||
| 120 | + </el-table-column> | ||
| 121 | + <el-table-column label="启用" width="90" align="center"> | ||
| 122 | + <template slot-scope="{ row }"> | ||
| 123 | + <el-tag size="mini" :type="row.enabled === 1 ? 'success' : 'info'" disable-transitions> | ||
| 124 | + {{ row.enabled === 1 ? '启用' : '停用' }} | ||
| 125 | + </el-tag> | ||
| 126 | + </template> | ||
| 127 | + </el-table-column> | ||
| 128 | + <el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip> | ||
| 129 | + <template slot-scope="{ row }">{{ row.remark || '无' }}</template> | ||
| 130 | + </el-table-column> | ||
| 131 | + <el-table-column label="操作" width="220" fixed="right"> | ||
| 132 | + <template slot-scope="{ row }"> | ||
| 133 | + <el-button type="text" size="small" @click="openTemplateDialog(row)">编辑</el-button> | ||
| 134 | + <el-button type="text" size="small" @click="toggleEnabled(row)"> | ||
| 135 | + {{ row.enabled === 1 ? '停用' : '启用' }} | ||
| 136 | + </el-button> | ||
| 137 | + <el-button type="text" size="small" class="NCC-table-delBtn" @click="removeTemplate(row)">删除</el-button> | ||
| 138 | + </template> | ||
| 139 | + </el-table-column> | ||
| 140 | + </NCC-table> | ||
| 141 | + <pagination | ||
| 142 | + :total="templateTotal" | ||
| 143 | + :page.sync="templateQuery.currentPage" | ||
| 144 | + :limit.sync="templateQuery.pageSize" | ||
| 145 | + @pagination="loadTemplates" | ||
| 146 | + /> | ||
| 147 | + </el-card> | ||
| 148 | + </template> | ||
| 149 | + </ConfigPanelShell> | ||
| 149 | 150 | ||
| 150 | <sms-template-dialog ref="templateDialog" @success="loadTemplates" /> | 151 | <sms-template-dialog ref="templateDialog" @success="loadTemplates" /> |
| 151 | <sms-test-send-dialog ref="testDialog" /> | 152 | <sms-test-send-dialog ref="testDialog" /> |
| @@ -154,6 +155,7 @@ | @@ -154,6 +155,7 @@ | ||
| 154 | </template> | 155 | </template> |
| 155 | 156 | ||
| 156 | <script> | 157 | <script> |
| 158 | +import ConfigPanelShell from './config-panel-shell.vue' | ||
| 157 | import { | 159 | import { |
| 158 | getSmsConfig, | 160 | getSmsConfig, |
| 159 | saveSmsConfig, | 161 | saveSmsConfig, |
| @@ -165,15 +167,22 @@ import SmsTemplateDialog from './sms-template-dialog.vue' | @@ -165,15 +167,22 @@ import SmsTemplateDialog from './sms-template-dialog.vue' | ||
| 165 | import SmsTestSendDialog from './sms-test-send-dialog.vue' | 167 | import SmsTestSendDialog from './sms-test-send-dialog.vue' |
| 166 | import SmsSendLogDrawer from './sms-send-log-drawer.vue' | 168 | import SmsSendLogDrawer from './sms-send-log-drawer.vue' |
| 167 | 169 | ||
| 170 | +const MODULES = [ | ||
| 171 | + { key: 'credentials', label: '基础配置', desc: 'AccessKey 与签名', icon: 'el-icon-key', iconClass: 'module-icon--credentials' }, | ||
| 172 | + { key: 'templates', label: '模板管理', desc: '场景模板与发送', icon: 'el-icon-message', iconClass: 'module-icon--templates' } | ||
| 173 | +] | ||
| 174 | + | ||
| 168 | export default { | 175 | export default { |
| 169 | name: 'SmsConfigPanel', | 176 | name: 'SmsConfigPanel', |
| 170 | components: { | 177 | components: { |
| 178 | + ConfigPanelShell, | ||
| 171 | SmsTemplateDialog, | 179 | SmsTemplateDialog, |
| 172 | SmsTestSendDialog, | 180 | SmsTestSendDialog, |
| 173 | SmsSendLogDrawer | 181 | SmsSendLogDrawer |
| 174 | }, | 182 | }, |
| 175 | data() { | 183 | data() { |
| 176 | return { | 184 | return { |
| 185 | + modules: MODULES, | ||
| 177 | loading: false, | 186 | loading: false, |
| 178 | configSaving: false, | 187 | configSaving: false, |
| 179 | templateLoading: false, | 188 | templateLoading: false, |
| @@ -310,73 +319,73 @@ export default { | @@ -310,73 +319,73 @@ export default { | ||
| 310 | </script> | 319 | </script> |
| 311 | 320 | ||
| 312 | <style lang="scss" scoped> | 321 | <style lang="scss" scoped> |
| 313 | -.sms-config-panel { | ||
| 314 | - .sms-config-card { | ||
| 315 | - margin-bottom: 16px; | ||
| 316 | - border: 1px solid #ebeef5; | 322 | +.config-section { |
| 323 | + border: 1px solid #ebeef5; | ||
| 317 | 324 | ||
| 318 | - &--main { | ||
| 319 | - margin-bottom: 0; | ||
| 320 | - } | 325 | + &--flush { |
| 326 | + border: none; | ||
| 327 | + box-shadow: none; | ||
| 321 | 328 | ||
| 322 | - &__header { | ||
| 323 | - display: flex; | ||
| 324 | - align-items: center; | ||
| 325 | - gap: 8px; | ||
| 326 | - font-weight: 500; | ||
| 327 | - } | ||
| 328 | - | ||
| 329 | - &__tag { | ||
| 330 | - padding: 0 8px; | ||
| 331 | - height: 22px; | ||
| 332 | - line-height: 22px; | ||
| 333 | - border-radius: 4px; | ||
| 334 | - background: #f4f4f5; | ||
| 335 | - color: #909399; | ||
| 336 | - font-size: 12px; | ||
| 337 | - font-weight: normal; | 329 | + ::v-deep .el-card__body { |
| 330 | + padding: 0; | ||
| 338 | } | 331 | } |
| 339 | } | 332 | } |
| 340 | 333 | ||
| 341 | - .sms-config-alert { | ||
| 342 | - margin-bottom: 12px; | ||
| 343 | - } | ||
| 344 | - | ||
| 345 | - .panel-actions { | ||
| 346 | - margin: 4px 0 0; | ||
| 347 | - } | ||
| 348 | - | ||
| 349 | - .panel-actions--between { | 334 | + &__header { |
| 350 | display: flex; | 335 | display: flex; |
| 351 | - justify-content: space-between; | ||
| 352 | align-items: center; | 336 | align-items: center; |
| 353 | - flex-wrap: wrap; | ||
| 354 | gap: 8px; | 337 | gap: 8px; |
| 355 | - margin-bottom: 12px; | 338 | + font-weight: 500; |
| 356 | } | 339 | } |
| 357 | 340 | ||
| 358 | - .toolbar-left, | ||
| 359 | - .toolbar-right { | ||
| 360 | - display: flex; | ||
| 361 | - flex-wrap: wrap; | ||
| 362 | - align-items: center; | ||
| 363 | - } | ||
| 364 | - | ||
| 365 | - .toolbar-right { | ||
| 366 | - justify-content: flex-start; | ||
| 367 | - } | ||
| 368 | - | ||
| 369 | - .form-tip { | ||
| 370 | - margin-left: 12px; | 341 | + &__tag { |
| 342 | + padding: 0 8px; | ||
| 343 | + height: 22px; | ||
| 344 | + line-height: 22px; | ||
| 345 | + border-radius: 4px; | ||
| 346 | + background: #f4f4f5; | ||
| 371 | color: #909399; | 347 | color: #909399; |
| 372 | font-size: 12px; | 348 | font-size: 12px; |
| 373 | - line-height: 1.5; | 349 | + font-weight: normal; |
| 374 | } | 350 | } |
| 375 | } | 351 | } |
| 376 | 352 | ||
| 377 | -/* 暗黑兼容 */ | ||
| 378 | -.ncc-content-dark .sms-config-panel { | ||
| 379 | - .sms-config-card { | 353 | +.module-icon--credentials { color: #409EFF; } |
| 354 | +.module-icon--templates { color: #67C23A; } | ||
| 355 | + | ||
| 356 | +.config-alert { | ||
| 357 | + margin-bottom: 12px; | ||
| 358 | +} | ||
| 359 | + | ||
| 360 | +.panel-actions { | ||
| 361 | + margin: 4px 0 0; | ||
| 362 | +} | ||
| 363 | + | ||
| 364 | +.panel-actions--between { | ||
| 365 | + display: flex; | ||
| 366 | + justify-content: space-between; | ||
| 367 | + align-items: center; | ||
| 368 | + flex-wrap: wrap; | ||
| 369 | + gap: 8px; | ||
| 370 | + margin-bottom: 12px; | ||
| 371 | +} | ||
| 372 | + | ||
| 373 | +.toolbar-left, | ||
| 374 | +.toolbar-right { | ||
| 375 | + display: flex; | ||
| 376 | + flex-wrap: wrap; | ||
| 377 | + align-items: center; | ||
| 378 | +} | ||
| 379 | + | ||
| 380 | +.form-tip { | ||
| 381 | + margin-left: 12px; | ||
| 382 | + color: #909399; | ||
| 383 | + font-size: 12px; | ||
| 384 | + line-height: 1.5; | ||
| 385 | +} | ||
| 386 | + | ||
| 387 | +.ncc-content-dark { | ||
| 388 | + .config-section { | ||
| 380 | border-color: rgba(255, 255, 255, 0.08); | 389 | border-color: rgba(255, 255, 255, 0.08); |
| 381 | 390 | ||
| 382 | &__tag { | 391 | &__tag { |
antis-ncc-admin/src/views/system/sysConfig/sms-send-log-drawer.vue
| @@ -2,7 +2,7 @@ | @@ -2,7 +2,7 @@ | ||
| 2 | <el-drawer | 2 | <el-drawer |
| 3 | title="发送日志" | 3 | title="发送日志" |
| 4 | :visible.sync="visible" | 4 | :visible.sync="visible" |
| 5 | - size="720px" | 5 | + size="75%" |
| 6 | append-to-body | 6 | append-to-body |
| 7 | :wrapper-closable="false" | 7 | :wrapper-closable="false" |
| 8 | custom-class="sms-send-log-drawer" | 8 | custom-class="sms-send-log-drawer" |
| @@ -15,7 +15,7 @@ | @@ -15,7 +15,7 @@ | ||
| 15 | size="small" | 15 | size="small" |
| 16 | clearable | 16 | clearable |
| 17 | placeholder="场景" | 17 | placeholder="场景" |
| 18 | - style="width: 140px; margin-right: 8px" | 18 | + class="toolbar-item toolbar-item--scene" |
| 19 | > | 19 | > |
| 20 | <el-option label="Birthday" value="Birthday" /> | 20 | <el-option label="Birthday" value="Birthday" /> |
| 21 | <el-option label="Login" value="Login" /> | 21 | <el-option label="Login" value="Login" /> |
| @@ -27,7 +27,7 @@ | @@ -27,7 +27,7 @@ | ||
| 27 | size="small" | 27 | size="small" |
| 28 | clearable | 28 | clearable |
| 29 | placeholder="状态" | 29 | placeholder="状态" |
| 30 | - style="width: 120px; margin-right: 8px" | 30 | + class="toolbar-item toolbar-item--status" |
| 31 | > | 31 | > |
| 32 | <el-option label="成功" :value="1" /> | 32 | <el-option label="成功" :value="1" /> |
| 33 | <el-option label="失败" :value="0" /> | 33 | <el-option label="失败" :value="0" /> |
| @@ -41,38 +41,41 @@ | @@ -41,38 +41,41 @@ | ||
| 41 | range-separator="至" | 41 | range-separator="至" |
| 42 | start-placeholder="业务日起" | 42 | start-placeholder="业务日起" |
| 43 | end-placeholder="业务日止" | 43 | end-placeholder="业务日止" |
| 44 | - style="margin-right: 8px; width: 260px" | 44 | + class="toolbar-item toolbar-item--date" |
| 45 | /> | 45 | /> |
| 46 | <el-button size="small" icon="el-icon-search" @click="search">查询</el-button> | 46 | <el-button size="small" icon="el-icon-search" @click="search">查询</el-button> |
| 47 | <el-button size="small" icon="el-icon-refresh" @click="reset">重置</el-button> | 47 | <el-button size="small" icon="el-icon-refresh" @click="reset">重置</el-button> |
| 48 | </div> | 48 | </div> |
| 49 | 49 | ||
| 50 | - <NCC-table v-loading="loading" :data="list"> | ||
| 51 | - <el-table-column label="时间" width="170"> | 50 | + <NCC-table v-loading="loading" :data="list" class="sms-send-log-table"> |
| 51 | + <el-table-column label="时间" width="165"> | ||
| 52 | <template slot-scope="{ row }"> | 52 | <template slot-scope="{ row }"> |
| 53 | {{ row.creatorTime ? ncc.dateFormat(row.creatorTime, 'YYYY-MM-DD HH:mm:ss') : '无' }} | 53 | {{ row.creatorTime ? ncc.dateFormat(row.creatorTime, 'YYYY-MM-DD HH:mm:ss') : '无' }} |
| 54 | </template> | 54 | </template> |
| 55 | </el-table-column> | 55 | </el-table-column> |
| 56 | - <el-table-column prop="sceneCode" label="场景" width="110" show-overflow-tooltip> | 56 | + <el-table-column prop="sceneCode" label="场景" width="100" show-overflow-tooltip> |
| 57 | <template slot-scope="{ row }">{{ row.sceneCode || '无' }}</template> | 57 | <template slot-scope="{ row }">{{ row.sceneCode || '无' }}</template> |
| 58 | </el-table-column> | 58 | </el-table-column> |
| 59 | - <el-table-column prop="templateCode" label="CODE" min-width="130" show-overflow-tooltip> | ||
| 60 | - <template slot-scope="{ row }">{{ row.templateCode || '无' }}</template> | ||
| 61 | - </el-table-column> | ||
| 62 | - <el-table-column prop="mobileMasked" label="手机号" width="130" show-overflow-tooltip> | 59 | + <el-table-column prop="mobileMasked" label="手机号" width="120" show-overflow-tooltip> |
| 63 | <template slot-scope="{ row }">{{ $maskMobile(row.mobileMasked) || '无' }}</template> | 60 | <template slot-scope="{ row }">{{ $maskMobile(row.mobileMasked) || '无' }}</template> |
| 64 | </el-table-column> | 61 | </el-table-column> |
| 65 | - <el-table-column prop="memberName" label="会员" width="100" show-overflow-tooltip> | 62 | + <el-table-column prop="memberName" label="会员" width="90" show-overflow-tooltip> |
| 66 | <template slot-scope="{ row }">{{ row.memberName || '无' }}</template> | 63 | <template slot-scope="{ row }">{{ row.memberName || '无' }}</template> |
| 67 | </el-table-column> | 64 | </el-table-column> |
| 68 | - <el-table-column label="测试" width="70" align="center"> | 65 | + <el-table-column prop="sendContent" label="内容" min-width="280" show-overflow-tooltip> |
| 69 | <template slot-scope="{ row }"> | 66 | <template slot-scope="{ row }"> |
| 70 | - <el-tag size="mini" :type="row.isTest === 1 ? 'warning' : 'info'" disable-transitions> | ||
| 71 | - {{ row.isTest === 1 ? '是' : '否' }} | ||
| 72 | - </el-tag> | 67 | + {{ row.sendContent || formatSendContent(row) || '无' }} |
| 68 | + </template> | ||
| 69 | + </el-table-column> | ||
| 70 | + <el-table-column prop="creatorUserName" label="发送人" width="100" show-overflow-tooltip> | ||
| 71 | + <template slot-scope="{ row }"> | ||
| 72 | + <div class="table-cell-with-icon"> | ||
| 73 | + <i class="el-icon-user cell-icon cell-icon--primary" /> | ||
| 74 | + <span>{{ row.creatorUserName || '无' }}</span> | ||
| 75 | + </div> | ||
| 73 | </template> | 76 | </template> |
| 74 | </el-table-column> | 77 | </el-table-column> |
| 75 | - <el-table-column label="状态" width="90" align="center"> | 78 | + <el-table-column label="状态" width="100" align="center"> |
| 76 | <template slot-scope="{ row }"> | 79 | <template slot-scope="{ row }"> |
| 77 | <el-tag | 80 | <el-tag |
| 78 | size="mini" | 81 | size="mini" |
| @@ -81,6 +84,7 @@ | @@ -81,6 +84,7 @@ | ||
| 81 | > | 84 | > |
| 82 | {{ row.statusName || statusName(row.status) }} | 85 | {{ row.statusName || statusName(row.status) }} |
| 83 | </el-tag> | 86 | </el-tag> |
| 87 | + <el-tag v-if="row.isTest === 1" size="mini" type="warning" disable-transitions>测试</el-tag> | ||
| 84 | </template> | 88 | </template> |
| 85 | </el-table-column> | 89 | </el-table-column> |
| 86 | <el-table-column prop="providerResult" label="结果" min-width="140" show-overflow-tooltip> | 90 | <el-table-column prop="providerResult" label="结果" min-width="140" show-overflow-tooltip> |
| @@ -177,6 +181,14 @@ export default { | @@ -177,6 +181,14 @@ export default { | ||
| 177 | statusName(status) { | 181 | statusName(status) { |
| 178 | const map = { 0: '失败', 1: '成功', 2: '跳过' } | 182 | const map = { 0: '失败', 1: '成功', 2: '跳过' } |
| 179 | return map[status] || '无' | 183 | return map[status] || '无' |
| 184 | + }, | ||
| 185 | + formatSendContent(row) { | ||
| 186 | + if (!row) return '' | ||
| 187 | + const parts = [] | ||
| 188 | + if (row.signName) parts.push(`【${row.signName}】`) | ||
| 189 | + if (row.templateCode) parts.push(`(${row.templateCode})`) | ||
| 190 | + if (row.templateParam) parts.push(row.templateParam) | ||
| 191 | + return parts.join(' ') | ||
| 180 | } | 192 | } |
| 181 | } | 193 | } |
| 182 | } | 194 | } |
| @@ -184,25 +196,129 @@ export default { | @@ -184,25 +196,129 @@ export default { | ||
| 184 | 196 | ||
| 185 | <style lang="scss" scoped> | 197 | <style lang="scss" scoped> |
| 186 | .sms-send-log-body { | 198 | .sms-send-log-body { |
| 199 | + display: flex; | ||
| 200 | + flex-direction: column; | ||
| 187 | padding: 0 16px 16px; | 201 | padding: 0 16px 16px; |
| 188 | height: 100%; | 202 | height: 100%; |
| 189 | box-sizing: border-box; | 203 | box-sizing: border-box; |
| 190 | - overflow: auto; | 204 | + overflow: hidden; |
| 191 | } | 205 | } |
| 192 | 206 | ||
| 193 | .sms-send-log-toolbar { | 207 | .sms-send-log-toolbar { |
| 194 | display: flex; | 208 | display: flex; |
| 195 | flex-wrap: wrap; | 209 | flex-wrap: wrap; |
| 196 | align-items: center; | 210 | align-items: center; |
| 211 | + gap: 8px; | ||
| 197 | margin-bottom: 12px; | 212 | margin-bottom: 12px; |
| 213 | + flex-shrink: 0; | ||
| 214 | +} | ||
| 215 | + | ||
| 216 | +.toolbar-item { | ||
| 217 | + &--scene { | ||
| 218 | + width: 120px; | ||
| 219 | + } | ||
| 220 | + | ||
| 221 | + &--status { | ||
| 222 | + width: 100px; | ||
| 223 | + } | ||
| 224 | + | ||
| 225 | + &--date { | ||
| 226 | + width: 260px; | ||
| 227 | + max-width: 100%; | ||
| 228 | + } | ||
| 198 | } | 229 | } |
| 230 | + | ||
| 199 | </style> | 231 | </style> |
| 200 | 232 | ||
| 201 | <style lang="scss"> | 233 | <style lang="scss"> |
| 234 | +/* | ||
| 235 | + * 发送日志抽屉 append-to-body,表格不在 #app 内,全局 #app .el-table 41px 不生效。 | ||
| 236 | + * 与 common.scss / NCC-table 行高规范对齐。 | ||
| 237 | + */ | ||
| 202 | .sms-send-log-drawer { | 238 | .sms-send-log-drawer { |
| 203 | .el-drawer__body { | 239 | .el-drawer__body { |
| 204 | padding: 0; | 240 | padding: 0; |
| 205 | overflow: hidden; | 241 | overflow: hidden; |
| 206 | } | 242 | } |
| 243 | + | ||
| 244 | + .sms-send-log-table { | ||
| 245 | + flex: 1; | ||
| 246 | + min-height: 0; | ||
| 247 | + | ||
| 248 | + .el-table { | ||
| 249 | + width: 100% !important; | ||
| 250 | + } | ||
| 251 | + | ||
| 252 | + .el-table__body-wrapper, | ||
| 253 | + .el-table__header-wrapper { | ||
| 254 | + overflow-x: hidden !important; | ||
| 255 | + } | ||
| 256 | + } | ||
| 257 | + | ||
| 258 | + .el-table:not(.columnTable) { | ||
| 259 | + th.el-table__cell, | ||
| 260 | + td.el-table__cell, | ||
| 261 | + .el-table__header-wrapper th, | ||
| 262 | + .el-table__body-wrapper td, | ||
| 263 | + .el-table__fixed-header-wrapper th, | ||
| 264 | + .el-table__fixed-body-wrapper td { | ||
| 265 | + padding: 0 !important; | ||
| 266 | + height: 41px !important; | ||
| 267 | + line-height: normal !important; | ||
| 268 | + box-sizing: border-box; | ||
| 269 | + vertical-align: middle !important; | ||
| 270 | + } | ||
| 271 | + | ||
| 272 | + .cell { | ||
| 273 | + box-sizing: border-box; | ||
| 274 | + display: flex; | ||
| 275 | + align-items: center; | ||
| 276 | + height: 41px !important; | ||
| 277 | + line-height: normal !important; | ||
| 278 | + white-space: nowrap !important; | ||
| 279 | + overflow: hidden; | ||
| 280 | + text-overflow: ellipsis; | ||
| 281 | + min-width: 0; | ||
| 282 | + } | ||
| 283 | + | ||
| 284 | + .el-table__header .cell { | ||
| 285 | + display: flex; | ||
| 286 | + align-items: center; | ||
| 287 | + height: 41px !important; | ||
| 288 | + line-height: normal !important; | ||
| 289 | + } | ||
| 290 | + | ||
| 291 | + td.is-center > .cell, | ||
| 292 | + th.is-center > .cell { | ||
| 293 | + justify-content: center; | ||
| 294 | + } | ||
| 295 | + | ||
| 296 | + td.is-right > .cell, | ||
| 297 | + th.is-right > .cell { | ||
| 298 | + justify-content: flex-end; | ||
| 299 | + } | ||
| 300 | + | ||
| 301 | + td.is-left > .cell, | ||
| 302 | + th.is-left > .cell { | ||
| 303 | + justify-content: flex-start; | ||
| 304 | + } | ||
| 305 | + | ||
| 306 | + .cell .el-tag { | ||
| 307 | + height: 20px; | ||
| 308 | + line-height: 18px; | ||
| 309 | + padding: 0 5px; | ||
| 310 | + font-size: 12px; | ||
| 311 | + margin: 0; | ||
| 312 | + flex-shrink: 0; | ||
| 313 | + } | ||
| 314 | + | ||
| 315 | + .cell .el-tag + .el-tag { | ||
| 316 | + margin-left: 4px; | ||
| 317 | + } | ||
| 318 | + | ||
| 319 | + .cell .table-cell-with-icon { | ||
| 320 | + height: 41px; | ||
| 321 | + } | ||
| 322 | + } | ||
| 207 | } | 323 | } |
| 208 | </style> | 324 | </style> |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqSms/LqSmsSendLogListOutput.cs
| @@ -15,11 +15,20 @@ namespace NCC.Extend.Entitys.Dto.LqSms | @@ -15,11 +15,20 @@ namespace NCC.Extend.Entitys.Dto.LqSms | ||
| 15 | public string memberId { get; set; } | 15 | public string memberId { get; set; } |
| 16 | public string memberName { get; set; } | 16 | public string memberName { get; set; } |
| 17 | public string templateParam { get; set; } | 17 | public string templateParam { get; set; } |
| 18 | + /// <summary> | ||
| 19 | + /// 发送内容摘要 | ||
| 20 | + /// </summary> | ||
| 21 | + public string sendContent { get; set; } | ||
| 18 | public int status { get; set; } | 22 | public int status { get; set; } |
| 19 | public string statusName { get; set; } | 23 | public string statusName { get; set; } |
| 20 | public string providerResult { get; set; } | 24 | public string providerResult { get; set; } |
| 21 | public DateTime? bizDate { get; set; } | 25 | public DateTime? bizDate { get; set; } |
| 22 | public int isTest { get; set; } | 26 | public int isTest { get; set; } |
| 23 | public DateTime? creatorTime { get; set; } | 27 | public DateTime? creatorTime { get; set; } |
| 28 | + public string creatorUserId { get; set; } | ||
| 29 | + /// <summary> | ||
| 30 | + /// 发送人姓名(BASE_USER.F_RealName,无则显示账号或系统) | ||
| 31 | + /// </summary> | ||
| 32 | + public string creatorUserName { get; set; } | ||
| 24 | } | 33 | } |
| 25 | } | 34 | } |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_sms_send_log/LqSmsSendLogEntity.cs
| @@ -73,6 +73,12 @@ namespace NCC.Extend.Entitys.lq_sms_send_log | @@ -73,6 +73,12 @@ namespace NCC.Extend.Entitys.lq_sms_send_log | ||
| 73 | public string TemplateParam { get; set; } | 73 | public string TemplateParam { get; set; } |
| 74 | 74 | ||
| 75 | /// <summary> | 75 | /// <summary> |
| 76 | + /// 发送内容摘要(签名 + 模板 + 参数,便于日志查看) | ||
| 77 | + /// </summary> | ||
| 78 | + [SugarColumn(ColumnName = "F_SendContent", IsNullable = true)] | ||
| 79 | + public string SendContent { get; set; } | ||
| 80 | + | ||
| 81 | + /// <summary> | ||
| 76 | /// 发送状态(见 <see cref="SmsSendStatusEnum"/>) | 82 | /// 发送状态(见 <see cref="SmsSendStatusEnum"/>) |
| 77 | /// </summary> | 83 | /// </summary> |
| 78 | [SugarColumn(ColumnName = "F_Status")] | 84 | [SugarColumn(ColumnName = "F_Status")] |
netcore/src/Modularity/Extend/NCC.Extend/Helpers/BillingCustomerTypeHelper.cs
0 → 100644
| 1 | +using System; | ||
| 2 | +using System.Threading.Tasks; | ||
| 3 | +using NCC.Code; | ||
| 4 | +using NCC.Extend.Entitys.Enum; | ||
| 5 | +using NCC.Extend.Entitys.lq_khxx; | ||
| 6 | +using NCC.Extend.Entitys.lq_kd_kdjlb; | ||
| 7 | +using SqlSugar; | ||
| 8 | + | ||
| 9 | +namespace NCC.Extend.Helpers | ||
| 10 | +{ | ||
| 11 | + /// <summary> | ||
| 12 | + /// 开单顾客类型(gjlx)解析与补全:开单时应快照会员 khlx,历史/漏写字段可从会员档案兜底。 | ||
| 13 | + /// </summary> | ||
| 14 | + public static class BillingCustomerTypeHelper | ||
| 15 | + { | ||
| 16 | + /// <summary> | ||
| 17 | + /// 取有效顾客类型编码:优先开单 gjlx,为空时用会员 khlx。 | ||
| 18 | + /// </summary> | ||
| 19 | + public static string ResolveEffectiveGjlx(string gjlx, string memberKhlx) | ||
| 20 | + { | ||
| 21 | + if (!string.IsNullOrWhiteSpace(gjlx)) | ||
| 22 | + return gjlx.Trim(); | ||
| 23 | + if (!string.IsNullOrWhiteSpace(memberKhlx)) | ||
| 24 | + return memberKhlx.Trim(); | ||
| 25 | + return null; | ||
| 26 | + } | ||
| 27 | + | ||
| 28 | + /// <summary> | ||
| 29 | + /// 解析为顾客类型展示名(线索/新客/散客/会员/归档);均无则返回「无」。 | ||
| 30 | + /// </summary> | ||
| 31 | + public static string ResolveCustomerTypeName(string gjlx, string memberKhlx = null) | ||
| 32 | + { | ||
| 33 | + var effective = ResolveEffectiveGjlx(gjlx, memberKhlx); | ||
| 34 | + if (string.IsNullOrEmpty(effective)) | ||
| 35 | + return "无"; | ||
| 36 | + | ||
| 37 | + if (int.TryParse(effective, out var typeValue)) | ||
| 38 | + { | ||
| 39 | + var desc = EnumHelper.GetEnumDesc<MemberTypeEnum>(typeValue); | ||
| 40 | + return string.IsNullOrEmpty(desc) ? effective : desc; | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + return effective; | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + /// <summary> | ||
| 47 | + /// 新建/更新开单前:gjlx 为空时从会员档案 khlx 写入快照。 | ||
| 48 | + /// </summary> | ||
| 49 | + public static async Task EnsureGjlxFromMemberAsync(ISqlSugarClient db, LqKdKdjlbEntity entity) | ||
| 50 | + { | ||
| 51 | + if (entity == null || !string.IsNullOrWhiteSpace(entity.Gjlx) || string.IsNullOrWhiteSpace(entity.Kdhy)) | ||
| 52 | + return; | ||
| 53 | + | ||
| 54 | + var khlx = await db.Queryable<LqKhxxEntity>() | ||
| 55 | + .Where(x => x.Id == entity.Kdhy) | ||
| 56 | + .Select(x => x.Khlx) | ||
| 57 | + .FirstAsync(); | ||
| 58 | + if (!string.IsNullOrWhiteSpace(khlx)) | ||
| 59 | + entity.Gjlx = khlx.Trim(); | ||
| 60 | + } | ||
| 61 | + } | ||
| 62 | +} |
netcore/src/Modularity/Extend/NCC.Extend/Helpers/ConsumeEditableDaysHelper.cs
| @@ -23,6 +23,11 @@ namespace NCC.Extend.Helpers | @@ -23,6 +23,11 @@ namespace NCC.Extend.Helpers | ||
| 23 | public const string KeyConsumeEditableDays = "consumeEditableDays"; | 23 | public const string KeyConsumeEditableDays = "consumeEditableDays"; |
| 24 | 24 | ||
| 25 | /// <summary> | 25 | /// <summary> |
| 26 | + /// 配置键:小程序端是否允许跨月修改/作废/重开单(1=允许,0=不允许) | ||
| 27 | + /// </summary> | ||
| 28 | + public const string KeyConsumeAllowCrossMonthEdit = "consumeAllowCrossMonthEdit"; | ||
| 29 | + | ||
| 30 | + /// <summary> | ||
| 26 | /// 配置键:管理后台作废耗卡允许天数 | 31 | /// 配置键:管理后台作废耗卡允许天数 |
| 27 | /// </summary> | 32 | /// </summary> |
| 28 | public const string KeyAdminConsumeVoidEditableDays = "adminConsumeVoidEditableDays"; | 33 | public const string KeyAdminConsumeVoidEditableDays = "adminConsumeVoidEditableDays"; |
| @@ -63,6 +68,11 @@ namespace NCC.Extend.Helpers | @@ -63,6 +68,11 @@ namespace NCC.Extend.Helpers | ||
| 63 | public const int DefaultDays = 3; | 68 | public const int DefaultDays = 3; |
| 64 | 69 | ||
| 65 | /// <summary> | 70 | /// <summary> |
| 71 | + /// 小程序端缺省:不允许跨月修改 | ||
| 72 | + /// </summary> | ||
| 73 | + public const int DefaultConsumeAllowCrossMonthEdit = 0; | ||
| 74 | + | ||
| 75 | + /// <summary> | ||
| 66 | /// 管理后台缺省天数:0 表示不限制 | 76 | /// 管理后台缺省天数:0 表示不限制 |
| 67 | /// </summary> | 77 | /// </summary> |
| 68 | public const int DefaultAdminOperationEditableDays = 0; | 78 | public const int DefaultAdminOperationEditableDays = 0; |
| @@ -81,12 +91,23 @@ namespace NCC.Extend.Helpers | @@ -81,12 +91,23 @@ namespace NCC.Extend.Helpers | ||
| 81 | } | 91 | } |
| 82 | 92 | ||
| 83 | /// <summary> | 93 | /// <summary> |
| 94 | + /// 小程序端是否允许跨月修改/作废/重开单 | ||
| 95 | + /// </summary> | ||
| 96 | + public static async Task<bool> GetConsumeAllowCrossMonthEditAsync(ISqlSugarClient db) | ||
| 97 | + { | ||
| 98 | + return await GetBoolConfigAsync(db, KeyConsumeAllowCrossMonthEdit, DefaultConsumeAllowCrossMonthEdit); | ||
| 99 | + } | ||
| 100 | + | ||
| 101 | + /// <summary> | ||
| 84 | /// 校验小程序端耗卡是否在允许修改/作废的时间窗口内 | 102 | /// 校验小程序端耗卡是否在允许修改/作废的时间窗口内 |
| 85 | /// </summary> | 103 | /// </summary> |
| 86 | public static async Task EnsureConsumeWithinEditableWindowAsync(ISqlSugarClient db, LqXhHyhkEntity card) | 104 | public static async Task EnsureConsumeWithinEditableWindowAsync(ISqlSugarClient db, LqXhHyhkEntity card) |
| 87 | { | 105 | { |
| 88 | var days = await GetConsumeEditableDaysAsync(db); | 106 | var days = await GetConsumeEditableDaysAsync(db); |
| 89 | EnsureWithinEditableWindow(card, days, "耗卡发生已超过 {0} 天,不允许修改/作废"); | 107 | EnsureWithinEditableWindow(card, days, "耗卡发生已超过 {0} 天,不允许修改/作废"); |
| 108 | + var allowCrossMonth = await GetConsumeAllowCrossMonthEditAsync(db); | ||
| 109 | + if (!allowCrossMonth) | ||
| 110 | + EnsureSameCalendarMonth(card, "耗卡已跨月,不允许修改/作废"); | ||
| 90 | } | 111 | } |
| 91 | 112 | ||
| 92 | /// <summary> | 113 | /// <summary> |
| @@ -178,6 +199,16 @@ namespace NCC.Extend.Helpers | @@ -178,6 +199,16 @@ namespace NCC.Extend.Helpers | ||
| 178 | throw NCCException.Oh(string.Format(messageTemplate, days)); | 199 | throw NCCException.Oh(string.Format(messageTemplate, days)); |
| 179 | } | 200 | } |
| 180 | 201 | ||
| 202 | + private static void EnsureSameCalendarMonth(LqXhHyhkEntity card, string message) | ||
| 203 | + { | ||
| 204 | + var baseTime = card.Hksj ?? card.CreateTime; | ||
| 205 | + if (!baseTime.HasValue) | ||
| 206 | + return; | ||
| 207 | + var now = DateTime.Now; | ||
| 208 | + if (baseTime.Value.Year != now.Year || baseTime.Value.Month != now.Month) | ||
| 209 | + throw NCCException.Oh(message); | ||
| 210 | + } | ||
| 211 | + | ||
| 181 | private static async Task<int> GetIntConfigWithLegacyAsync( | 212 | private static async Task<int> GetIntConfigWithLegacyAsync( |
| 182 | ISqlSugarClient db, | 213 | ISqlSugarClient db, |
| 183 | string key, | 214 | string key, |
| @@ -199,5 +230,20 @@ namespace NCC.Extend.Helpers | @@ -199,5 +230,20 @@ namespace NCC.Extend.Helpers | ||
| 199 | return defaultValue; | 230 | return defaultValue; |
| 200 | return int.TryParse(row.Value.Trim(), out var n) && n >= 0 ? n : defaultValue; | 231 | return int.TryParse(row.Value.Trim(), out var n) && n >= 0 ? n : defaultValue; |
| 201 | } | 232 | } |
| 233 | + | ||
| 234 | + private static async Task<bool> GetBoolConfigAsync(ISqlSugarClient db, string key, int defaultValue) | ||
| 235 | + { | ||
| 236 | + var row = await db.Queryable<SysConfigEntity>() | ||
| 237 | + .Where(x => x.Category == CategorySysConfig && SqlFunc.ToLower(x.Key) == key.ToLower()) | ||
| 238 | + .FirstAsync(); | ||
| 239 | + if (row == null || string.IsNullOrWhiteSpace(row.Value)) | ||
| 240 | + return defaultValue == 1; | ||
| 241 | + var value = row.Value.Trim(); | ||
| 242 | + if (value == "1" || value.Equals("true", StringComparison.OrdinalIgnoreCase)) | ||
| 243 | + return true; | ||
| 244 | + if (value == "0" || value.Equals("false", StringComparison.OrdinalIgnoreCase)) | ||
| 245 | + return false; | ||
| 246 | + return int.TryParse(value, out var n) ? n == 1 : defaultValue == 1; | ||
| 247 | + } | ||
| 202 | } | 248 | } |
| 203 | } | 249 | } |
netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs
| @@ -197,6 +197,7 @@ namespace NCC.Extend.LqKdKdjlb | @@ -197,6 +197,7 @@ namespace NCC.Extend.LqKdKdjlb | ||
| 197 | } | 197 | } |
| 198 | 198 | ||
| 199 | var memberArchiveNoDict = await BuildMemberArchiveNoDictAsync(billingRecords.Select(x => x.kdhy)); | 199 | var memberArchiveNoDict = await BuildMemberArchiveNoDictAsync(billingRecords.Select(x => x.kdhy)); |
| 200 | + var memberKhlxDict = await BuildMemberKhlxDictAsync(billingRecords.Select(x => x.kdhy)); | ||
| 200 | 201 | ||
| 201 | _logger.LogInformation($"查询到 {billingRecords.Count} 条开单记录"); | 202 | _logger.LogInformation($"查询到 {billingRecords.Count} 条开单记录"); |
| 202 | 203 | ||
| @@ -387,7 +388,7 @@ namespace NCC.Extend.LqKdKdjlb | @@ -387,7 +388,7 @@ namespace NCC.Extend.LqKdKdjlb | ||
| 387 | CustomerArchiveNo = ResolveMemberArchiveNo(billing.kdhy, memberArchiveNoDict) ?? "无", | 388 | CustomerArchiveNo = ResolveMemberArchiveNo(billing.kdhy, memberArchiveNoDict) ?? "无", |
| 388 | CustomerChannel = string.IsNullOrEmpty(billing.kdhyjdqd) ? "无" : billing.kdhyjdqd, | 389 | CustomerChannel = string.IsNullOrEmpty(billing.kdhyjdqd) ? "无" : billing.kdhyjdqd, |
| 389 | GoldTriangle = GetGoldTriangleName(billing.jsj), | 390 | GoldTriangle = GetGoldTriangleName(billing.jsj), |
| 390 | - CustomerType = ResolveCustomerTypeName(billing.gjlx), | 391 | + CustomerType = BillingCustomerTypeHelper.ResolveCustomerTypeName(billing.gjlx, ResolveMemberKhlx(billing.kdhy, memberKhlxDict)), |
| 391 | HealthCoach = "无", | 392 | HealthCoach = "无", |
| 392 | PerformanceAmount = "¥0.00", | 393 | PerformanceAmount = "¥0.00", |
| 393 | RelatedItem = ItemDisplayNameHelper.Display(exportItemNameMap, itemDetail.px, itemDetail.pxmc) ?? "无", | 394 | RelatedItem = ItemDisplayNameHelper.Display(exportItemNameMap, itemDetail.px, itemDetail.pxmc) ?? "无", |
| @@ -422,7 +423,7 @@ namespace NCC.Extend.LqKdKdjlb | @@ -422,7 +423,7 @@ namespace NCC.Extend.LqKdKdjlb | ||
| 422 | CustomerArchiveNo = ResolveMemberArchiveNo(billing.kdhy, memberArchiveNoDict) ?? "无", | 423 | CustomerArchiveNo = ResolveMemberArchiveNo(billing.kdhy, memberArchiveNoDict) ?? "无", |
| 423 | CustomerChannel = string.IsNullOrEmpty(billing.kdhyjdqd) ? "无" : billing.kdhyjdqd, | 424 | CustomerChannel = string.IsNullOrEmpty(billing.kdhyjdqd) ? "无" : billing.kdhyjdqd, |
| 424 | GoldTriangle = GetGoldTriangleName(billing.jsj), | 425 | GoldTriangle = GetGoldTriangleName(billing.jsj), |
| 425 | - CustomerType = ResolveCustomerTypeName(billing.gjlx), | 426 | + CustomerType = BillingCustomerTypeHelper.ResolveCustomerTypeName(billing.gjlx, ResolveMemberKhlx(billing.kdhy, memberKhlxDict)), |
| 426 | HealthCoach = healthTeacher.jksxm ?? "无", | 427 | HealthCoach = healthTeacher.jksxm ?? "无", |
| 427 | PerformanceAmount = FormatPerformanceAmount(healthTeacher.jksyj), | 428 | PerformanceAmount = FormatPerformanceAmount(healthTeacher.jksyj), |
| 428 | RelatedItem = ItemDisplayNameHelper.Display(exportItemNameMap, itemDetail.px, itemDetail.pxmc) ?? "无", | 429 | RelatedItem = ItemDisplayNameHelper.Display(exportItemNameMap, itemDetail.px, itemDetail.pxmc) ?? "无", |
| @@ -604,6 +605,7 @@ namespace NCC.Extend.LqKdKdjlb | @@ -604,6 +605,7 @@ namespace NCC.Extend.LqKdKdjlb | ||
| 604 | var MemberInfo = await _db.Queryable<LqKhxxEntity>().FirstAsync(p => p.Id == entity.Kdhy); | 605 | var MemberInfo = await _db.Queryable<LqKhxxEntity>().FirstAsync(p => p.Id == entity.Kdhy); |
| 605 | output.kdhyc = MemberInfo.Khmc; | 606 | output.kdhyc = MemberInfo.Khmc; |
| 606 | output.kdhysjh = MemberInfo.Sjh; | 607 | output.kdhysjh = MemberInfo.Sjh; |
| 608 | + output.gjlx = BillingCustomerTypeHelper.ResolveEffectiveGjlx(entity.Gjlx, MemberInfo?.Khlx) ?? output.gjlx; | ||
| 607 | 609 | ||
| 608 | if (!string.IsNullOrEmpty(entity.ActivityId)) | 610 | if (!string.IsNullOrEmpty(entity.ActivityId)) |
| 609 | { | 611 | { |
| @@ -816,7 +818,7 @@ namespace NCC.Extend.LqKdKdjlb | @@ -816,7 +818,7 @@ namespace NCC.Extend.LqKdKdjlb | ||
| 816 | { | 818 | { |
| 817 | var khxxMap = await _db.Queryable<LqKhxxEntity>() | 819 | var khxxMap = await _db.Queryable<LqKhxxEntity>() |
| 818 | .Where(x => kdhyIds.Contains(x.Id)) | 820 | .Where(x => kdhyIds.Contains(x.Id)) |
| 819 | - .Select(x => new { x.Id, x.Khmc, x.Sjh }) | 821 | + .Select(x => new { x.Id, x.Khmc, x.Sjh, x.Khlx }) |
| 820 | .ToListAsync(); | 822 | .ToListAsync(); |
| 821 | var khxxDict = khxxMap.ToDictionary(x => x.Id); | 823 | var khxxDict = khxxMap.ToDictionary(x => x.Id); |
| 822 | foreach (var item in data.list) | 824 | foreach (var item in data.list) |
| @@ -825,6 +827,8 @@ namespace NCC.Extend.LqKdKdjlb | @@ -825,6 +827,8 @@ namespace NCC.Extend.LqKdKdjlb | ||
| 825 | { | 827 | { |
| 826 | item.kdhyc = kh.Khmc; | 828 | item.kdhyc = kh.Khmc; |
| 827 | item.kdhysjh = kh.Sjh; | 829 | item.kdhysjh = kh.Sjh; |
| 830 | + if (string.IsNullOrWhiteSpace(item.gjlx) && !string.IsNullOrWhiteSpace(kh.Khlx)) | ||
| 831 | + item.gjlx = kh.Khlx.Trim(); | ||
| 828 | } | 832 | } |
| 829 | } | 833 | } |
| 830 | } | 834 | } |
| @@ -1348,6 +1352,7 @@ namespace NCC.Extend.LqKdKdjlb | @@ -1348,6 +1352,7 @@ namespace NCC.Extend.LqKdKdjlb | ||
| 1348 | entity.CreateTime = DateTime.Now; | 1352 | entity.CreateTime = DateTime.Now; |
| 1349 | entity.UpdateTime = DateTime.Now; | 1353 | entity.UpdateTime = DateTime.Now; |
| 1350 | entity.IsEffective = StatusEnum.有效.GetHashCode(); | 1354 | entity.IsEffective = StatusEnum.有效.GetHashCode(); |
| 1355 | + await BillingCustomerTypeHelper.EnsureGjlxFromMemberAsync(_db, entity); | ||
| 1351 | try | 1356 | try |
| 1352 | { | 1357 | { |
| 1353 | //开启事务 | 1358 | //开启事务 |
| @@ -1739,6 +1744,7 @@ namespace NCC.Extend.LqKdKdjlb | @@ -1739,6 +1744,7 @@ namespace NCC.Extend.LqKdKdjlb | ||
| 1739 | 1744 | ||
| 1740 | var entity = input.Adapt<LqKdKdjlbEntity>(); | 1745 | var entity = input.Adapt<LqKdKdjlbEntity>(); |
| 1741 | entity.Id = id; // 确保ID正确设置 | 1746 | entity.Id = id; // 确保ID正确设置 |
| 1747 | + await BillingCustomerTypeHelper.EnsureGjlxFromMemberAsync(_db, entity); | ||
| 1742 | try | 1748 | try |
| 1743 | { | 1749 | { |
| 1744 | //检查开单记录是否可以操作 | 1750 | //检查开单记录是否可以操作 |
| @@ -2477,6 +2483,13 @@ namespace NCC.Extend.LqKdKdjlb | @@ -2477,6 +2483,13 @@ namespace NCC.Extend.LqKdKdjlb | ||
| 2477 | { | 2483 | { |
| 2478 | var batchData = historyData.Skip(batchIndex * batchSize).Take(batchSize).ToList(); | 2484 | var batchData = historyData.Skip(batchIndex * batchSize).Take(batchSize).ToList(); |
| 2479 | 2485 | ||
| 2486 | + var batchMemberIds = batchData | ||
| 2487 | + .Select(x => (string)x.F_MemberId) | ||
| 2488 | + .Where(x => !string.IsNullOrEmpty(x)) | ||
| 2489 | + .Distinct() | ||
| 2490 | + .ToList(); | ||
| 2491 | + var batchMemberKhlxDict = await BuildMemberKhlxDictAsync(batchMemberIds); | ||
| 2492 | + | ||
| 2480 | var kdjlbEntities = new List<LqKdKdjlbEntity>(); | 2493 | var kdjlbEntities = new List<LqKdKdjlbEntity>(); |
| 2481 | var pxmxEntities = new List<LqKdPxmxEntity>(); | 2494 | var pxmxEntities = new List<LqKdPxmxEntity>(); |
| 2482 | 2495 | ||
| @@ -2496,6 +2509,7 @@ namespace NCC.Extend.LqKdKdjlb | @@ -2496,6 +2509,7 @@ namespace NCC.Extend.LqKdKdjlb | ||
| 2496 | Kdhyc = (string)item.会员名称, | 2509 | Kdhyc = (string)item.会员名称, |
| 2497 | Jsj = "暂无", | 2510 | Jsj = "暂无", |
| 2498 | Kdhysjh = (string)item.会员电话, | 2511 | Kdhysjh = (string)item.会员电话, |
| 2512 | + Gjlx = ResolveMemberKhlx((string)item.F_MemberId, batchMemberKhlxDict), | ||
| 2499 | Kdrq = fixedDateTime, | 2513 | Kdrq = fixedDateTime, |
| 2500 | Zdyj = Convert.ToDecimal(item.整单业绩 ?? 0), | 2514 | Zdyj = Convert.ToDecimal(item.整单业绩 ?? 0), |
| 2501 | Sfyj = Convert.ToDecimal(item.实付业绩 ?? 0), | 2515 | Sfyj = Convert.ToDecimal(item.实付业绩 ?? 0), |
| @@ -3127,6 +3141,7 @@ namespace NCC.Extend.LqKdKdjlb | @@ -3127,6 +3141,7 @@ namespace NCC.Extend.LqKdKdjlb | ||
| 3127 | 3141 | ||
| 3128 | var billingIds = billingRecords.Select(x => x.id).ToList(); | 3142 | var billingIds = billingRecords.Select(x => x.id).ToList(); |
| 3129 | var memberArchiveNoDict = await BuildMemberArchiveNoDictAsync(billingRecords.Select(x => x.kdhy)); | 3143 | var memberArchiveNoDict = await BuildMemberArchiveNoDictAsync(billingRecords.Select(x => x.kdhy)); |
| 3144 | + var memberKhlxDict = await BuildMemberKhlxDictAsync(billingRecords.Select(x => x.kdhy)); | ||
| 3130 | 3145 | ||
| 3131 | // 构建品项明细查询条件 | 3146 | // 构建品项明细查询条件 |
| 3132 | var itemDetailsQuery = _db.Queryable<LqKdPxmxEntity>() | 3147 | var itemDetailsQuery = _db.Queryable<LqKdPxmxEntity>() |
| @@ -3274,7 +3289,7 @@ namespace NCC.Extend.LqKdKdjlb | @@ -3274,7 +3289,7 @@ namespace NCC.Extend.LqKdKdjlb | ||
| 3274 | customerChannel = billing.kdhyjdqd, // 客户进店渠道 | 3289 | customerChannel = billing.kdhyjdqd, // 客户进店渠道 |
| 3275 | storeId = billing.djmd, | 3290 | storeId = billing.djmd, |
| 3276 | goldTriangle = billing.jsj, | 3291 | goldTriangle = billing.jsj, |
| 3277 | - customerType = ResolveCustomerTypeName(billing.gjlx), | 3292 | + customerType = BillingCustomerTypeHelper.ResolveCustomerTypeName(billing.gjlx, ResolveMemberKhlx(billing.kdhy, memberKhlxDict)), |
| 3278 | cooperationInstitution = billing.hgjg, | 3293 | cooperationInstitution = billing.hgjg, |
| 3279 | cooperationInstitutionName = billing.hgjgName, | 3294 | cooperationInstitutionName = billing.hgjgName, |
| 3280 | paymentHospital = billing.fkyy, // 付款医院ID | 3295 | paymentHospital = billing.fkyy, // 付款医院ID |
| @@ -3408,23 +3423,12 @@ namespace NCC.Extend.LqKdKdjlb | @@ -3408,23 +3423,12 @@ namespace NCC.Extend.LqKdKdjlb | ||
| 3408 | #endregion | 3423 | #endregion |
| 3409 | 3424 | ||
| 3410 | #region 私有方法 | 3425 | #region 私有方法 |
| 3411 | - /// <summary> | ||
| 3412 | - /// 解析开单顾客类型名称 | ||
| 3413 | - /// </summary> | ||
| 3414 | - private static string ResolveCustomerTypeName(string gjlx) | ||
| 3415 | - { | ||
| 3416 | - if (string.IsNullOrEmpty(gjlx)) | ||
| 3417 | - { | ||
| 3418 | - return "无"; | ||
| 3419 | - } | ||
| 3420 | - | ||
| 3421 | - if (int.TryParse(gjlx, out var typeValue)) | ||
| 3422 | - { | ||
| 3423 | - var desc = EnumHelper.GetEnumDesc<MemberTypeEnum>(typeValue); | ||
| 3424 | - return string.IsNullOrEmpty(desc) ? gjlx : desc; | ||
| 3425 | - } | ||
| 3426 | 3426 | ||
| 3427 | - return gjlx; | 3427 | + private static string ResolveMemberKhlx(string memberId, Dictionary<string, string> memberKhlxDict) |
| 3428 | + { | ||
| 3429 | + if (string.IsNullOrEmpty(memberId) || memberKhlxDict == null) | ||
| 3430 | + return null; | ||
| 3431 | + return memberKhlxDict.TryGetValue(memberId, out var khlx) ? khlx : null; | ||
| 3428 | } | 3432 | } |
| 3429 | 3433 | ||
| 3430 | /// <summary> | 3434 | /// <summary> |
| @@ -3521,6 +3525,22 @@ namespace NCC.Extend.LqKdKdjlb | @@ -3521,6 +3525,22 @@ namespace NCC.Extend.LqKdKdjlb | ||
| 3521 | return dict; | 3525 | return dict; |
| 3522 | } | 3526 | } |
| 3523 | 3527 | ||
| 3528 | + private async Task<Dictionary<string, string>> BuildMemberKhlxDictAsync(IEnumerable<string> memberIds) | ||
| 3529 | + { | ||
| 3530 | + var ids = memberIds.Where(x => !string.IsNullOrEmpty(x)).Distinct().ToList(); | ||
| 3531 | + if (!ids.Any()) | ||
| 3532 | + return new Dictionary<string, string>(); | ||
| 3533 | + | ||
| 3534 | + var members = await _db.Queryable<LqKhxxEntity>() | ||
| 3535 | + .Where(x => ids.Contains(x.Id)) | ||
| 3536 | + .Select(x => new { x.Id, x.Khlx }) | ||
| 3537 | + .ToListAsync(); | ||
| 3538 | + | ||
| 3539 | + return members | ||
| 3540 | + .Where(x => !string.IsNullOrEmpty(x.Id) && !string.IsNullOrWhiteSpace(x.Khlx)) | ||
| 3541 | + .ToDictionary(x => x.Id, x => x.Khlx.Trim()); | ||
| 3542 | + } | ||
| 3543 | + | ||
| 3524 | /// <summary> | 3544 | /// <summary> |
| 3525 | /// 根据开单会员字段解析客户档案号 | 3545 | /// 根据开单会员字段解析客户档案号 |
| 3526 | /// </summary> | 3546 | /// </summary> |
netcore/src/Modularity/Extend/NCC.Extend/LqSmsService.cs
| @@ -21,9 +21,11 @@ using NCC.Extend.Entitys.Enum; | @@ -21,9 +21,11 @@ using NCC.Extend.Entitys.Enum; | ||
| 21 | using NCC.Extend.Entitys.lq_khxx; | 21 | using NCC.Extend.Entitys.lq_khxx; |
| 22 | using NCC.Extend.Entitys.lq_sms_send_log; | 22 | using NCC.Extend.Entitys.lq_sms_send_log; |
| 23 | using NCC.Extend.Entitys.lq_sms_template; | 23 | using NCC.Extend.Entitys.lq_sms_template; |
| 24 | +using NCC.Extend.Entitys.lq_sms_template; | ||
| 24 | using NCC.Extend.Interfaces.BusinessOperationLog; | 25 | using NCC.Extend.Interfaces.BusinessOperationLog; |
| 25 | using NCC.Extend.Interfaces.LqSms; | 26 | using NCC.Extend.Interfaces.LqSms; |
| 26 | using NCC.FriendlyException; | 27 | using NCC.FriendlyException; |
| 28 | +using NCC.System.Entitys.Permission; | ||
| 27 | using NCC.System.Entitys.System; | 29 | using NCC.System.Entitys.System; |
| 28 | using Newtonsoft.Json; | 30 | using Newtonsoft.Json; |
| 29 | using SqlSugar; | 31 | using SqlSugar; |
| @@ -683,18 +685,37 @@ namespace NCC.Extend | @@ -683,18 +685,37 @@ namespace NCC.Extend | ||
| 683 | memberId = it.MemberId, | 685 | memberId = it.MemberId, |
| 684 | memberName = it.MemberName, | 686 | memberName = it.MemberName, |
| 685 | templateParam = it.TemplateParam, | 687 | templateParam = it.TemplateParam, |
| 688 | + sendContent = it.SendContent, | ||
| 686 | status = it.Status, | 689 | status = it.Status, |
| 687 | providerResult = it.ProviderResult, | 690 | providerResult = it.ProviderResult, |
| 688 | bizDate = it.BizDate, | 691 | bizDate = it.BizDate, |
| 689 | isTest = it.IsTest, | 692 | isTest = it.IsTest, |
| 690 | - creatorTime = it.CreatorTime | 693 | + creatorTime = it.CreatorTime, |
| 694 | + creatorUserId = it.CreatorUserId | ||
| 691 | }) | 695 | }) |
| 692 | .ToPagedListAsync(input.currentPage, input.pageSize); | 696 | .ToPagedListAsync(input.currentPage, input.pageSize); |
| 693 | 697 | ||
| 694 | var mask = await IsPrivacyMobileMaskEnabledAsync(); | 698 | var mask = await IsPrivacyMobileMaskEnabledAsync(); |
| 699 | + var userIds = data.list | ||
| 700 | + .Select(x => x.creatorUserId) | ||
| 701 | + .Where(x => !string.IsNullOrWhiteSpace(x)) | ||
| 702 | + .Distinct() | ||
| 703 | + .ToList(); | ||
| 704 | + var userMap = userIds.Count == 0 | ||
| 705 | + ? new Dictionary<string, UserEntity>() | ||
| 706 | + : (await _db.Queryable<UserEntity>() | ||
| 707 | + .Where(u => userIds.Contains(u.Id)) | ||
| 708 | + .ToListAsync()) | ||
| 709 | + .ToDictionary(u => u.Id, u => u); | ||
| 710 | + | ||
| 695 | foreach (var item in data.list) | 711 | foreach (var item in data.list) |
| 696 | { | 712 | { |
| 697 | item.statusName = GetEnumDescription((SmsSendStatusEnum)item.status); | 713 | item.statusName = GetEnumDescription((SmsSendStatusEnum)item.status); |
| 714 | + if (string.IsNullOrWhiteSpace(item.sendContent)) | ||
| 715 | + { | ||
| 716 | + item.sendContent = FormatSendContent(item.signName, null, item.templateCode, item.templateParam); | ||
| 717 | + } | ||
| 718 | + item.creatorUserName = ResolveSenderDisplayName(item.creatorUserId, userMap); | ||
| 698 | // 默认存明文;开启隐私开关时再脱敏。历史已脱敏数据保持原样。 | 719 | // 默认存明文;开启隐私开关时再脱敏。历史已脱敏数据保持原样。 |
| 699 | if (mask) | 720 | if (mask) |
| 700 | { | 721 | { |
| @@ -723,6 +744,10 @@ namespace NCC.Extend | @@ -723,6 +744,10 @@ namespace NCC.Extend | ||
| 723 | var configMap = await LoadConfigMapAsync(); | 744 | var configMap = await LoadConfigMapAsync(); |
| 724 | var prevent = ParseSwitch(GetMapValue(configMap, KeyPreventDuplicate), 1) == 1; | 745 | var prevent = ParseSwitch(GetMapValue(configMap, KeyPreventDuplicate), 1) == 1; |
| 725 | var memberId = string.IsNullOrWhiteSpace(input.memberId) ? null : input.memberId.Trim(); | 746 | var memberId = string.IsNullOrWhiteSpace(input.memberId) ? null : input.memberId.Trim(); |
| 747 | + var defaultSign = GetMapValue(configMap, KeySignName); | ||
| 748 | + var signName = string.IsNullOrWhiteSpace(template.SignName) | ||
| 749 | + ? (string.IsNullOrWhiteSpace(defaultSign) ? DefaultSignName : defaultSign) | ||
| 750 | + : template.SignName; | ||
| 726 | 751 | ||
| 727 | if (!skipDuplicate && prevent && !string.IsNullOrWhiteSpace(memberId)) | 752 | if (!skipDuplicate && prevent && !string.IsNullOrWhiteSpace(memberId)) |
| 728 | { | 753 | { |
| @@ -735,7 +760,7 @@ namespace NCC.Extend | @@ -735,7 +760,7 @@ namespace NCC.Extend | ||
| 735 | if (exists) | 760 | if (exists) |
| 736 | { | 761 | { |
| 737 | var skipLog = await InsertSendLogAsync(template, input, bizDate, | 762 | var skipLog = await InsertSendLogAsync(template, input, bizDate, |
| 738 | - (int)SmsSendStatusEnum.跳过, "同日同场景已成功发送,跳过", null); | 763 | + (int)SmsSendStatusEnum.跳过, "同日同场景已成功发送,跳过", null, signName); |
| 739 | return new | 764 | return new |
| 740 | { | 765 | { |
| 741 | logId = skipLog, | 766 | logId = skipLog, |
| @@ -758,11 +783,6 @@ namespace NCC.Extend | @@ -758,11 +783,6 @@ namespace NCC.Extend | ||
| 758 | throw NCCException.Oh("当前生日/场景发送仅支持阿里云(厂商=1),请检查短信配置"); | 783 | throw NCCException.Oh("当前生日/场景发送仅支持阿里云(厂商=1),请检查短信配置"); |
| 759 | } | 784 | } |
| 760 | 785 | ||
| 761 | - var defaultSign = GetMapValue(configMap, KeySignName); | ||
| 762 | - var signName = string.IsNullOrWhiteSpace(template.SignName) | ||
| 763 | - ? (string.IsNullOrWhiteSpace(defaultSign) ? DefaultSignName : defaultSign) | ||
| 764 | - : template.SignName; | ||
| 765 | - | ||
| 766 | var variables = input.variables ?? new Dictionary<string, string>(); | 786 | var variables = input.variables ?? new Dictionary<string, string>(); |
| 767 | if (!variables.ContainsKey("name") && !string.IsNullOrWhiteSpace(input.memberName)) | 787 | if (!variables.ContainsKey("name") && !string.IsNullOrWhiteSpace(input.memberName)) |
| 768 | { | 788 | { |
| @@ -793,7 +813,7 @@ namespace NCC.Extend | @@ -793,7 +813,7 @@ namespace NCC.Extend | ||
| 793 | 813 | ||
| 794 | var ok = string.Equals(providerResult, "OK", StringComparison.OrdinalIgnoreCase); | 814 | var ok = string.Equals(providerResult, "OK", StringComparison.OrdinalIgnoreCase); |
| 795 | var status = ok ? (int)SmsSendStatusEnum.成功 : (int)SmsSendStatusEnum.失败; | 815 | var status = ok ? (int)SmsSendStatusEnum.成功 : (int)SmsSendStatusEnum.失败; |
| 796 | - var logId = await InsertSendLogAsync(template, input, bizDate, status, providerResult, templateParam); | 816 | + var logId = await InsertSendLogAsync(template, input, bizDate, status, providerResult, templateParam, signName); |
| 797 | 817 | ||
| 798 | return new | 818 | return new |
| 799 | { | 819 | { |
| @@ -812,20 +832,23 @@ namespace NCC.Extend | @@ -812,20 +832,23 @@ namespace NCC.Extend | ||
| 812 | DateTime bizDate, | 832 | DateTime bizDate, |
| 813 | int status, | 833 | int status, |
| 814 | string providerResult, | 834 | string providerResult, |
| 815 | - string templateParam) | 835 | + string templateParam, |
| 836 | + string signName) | ||
| 816 | { | 837 | { |
| 838 | + var resolvedSign = string.IsNullOrWhiteSpace(signName) ? template.SignName : signName; | ||
| 817 | var entity = new LqSmsSendLogEntity | 839 | var entity = new LqSmsSendLogEntity |
| 818 | { | 840 | { |
| 819 | Id = YitIdHelper.NextId().ToString(), | 841 | Id = YitIdHelper.NextId().ToString(), |
| 820 | SceneCode = template.SceneCode, | 842 | SceneCode = template.SceneCode, |
| 821 | TemplateId = template.Id, | 843 | TemplateId = template.Id, |
| 822 | TemplateCode = template.TemplateCode, | 844 | TemplateCode = template.TemplateCode, |
| 823 | - SignName = template.SignName, | 845 | + SignName = resolvedSign, |
| 824 | MobileMasked = string.IsNullOrWhiteSpace(input.mobile) ? string.Empty : input.mobile.Trim(), | 846 | MobileMasked = string.IsNullOrWhiteSpace(input.mobile) ? string.Empty : input.mobile.Trim(), |
| 825 | MobileHash = HashMobile(input.mobile), | 847 | MobileHash = HashMobile(input.mobile), |
| 826 | MemberId = string.IsNullOrWhiteSpace(input.memberId) ? null : input.memberId.Trim(), | 848 | MemberId = string.IsNullOrWhiteSpace(input.memberId) ? null : input.memberId.Trim(), |
| 827 | MemberName = input.memberName, | 849 | MemberName = input.memberName, |
| 828 | TemplateParam = templateParam, | 850 | TemplateParam = templateParam, |
| 851 | + SendContent = FormatSendContent(resolvedSign, template.Name, template.TemplateCode, templateParam), | ||
| 829 | Status = status, | 852 | Status = status, |
| 830 | ProviderResult = providerResult, | 853 | ProviderResult = providerResult, |
| 831 | BizDate = bizDate.Date, | 854 | BizDate = bizDate.Date, |
| @@ -1106,6 +1129,51 @@ namespace NCC.Extend | @@ -1106,6 +1129,51 @@ namespace NCC.Extend | ||
| 1106 | return text; | 1129 | return text; |
| 1107 | } | 1130 | } |
| 1108 | 1131 | ||
| 1132 | + private static string FormatSendContent(string signName, string templateName, string templateCode, string templateParam) | ||
| 1133 | + { | ||
| 1134 | + var sb = new StringBuilder(); | ||
| 1135 | + if (!string.IsNullOrWhiteSpace(signName)) | ||
| 1136 | + { | ||
| 1137 | + sb.Append('【').Append(signName.Trim()).Append('】'); | ||
| 1138 | + } | ||
| 1139 | + if (!string.IsNullOrWhiteSpace(templateName)) | ||
| 1140 | + { | ||
| 1141 | + if (sb.Length > 0) sb.Append(' '); | ||
| 1142 | + sb.Append(templateName.Trim()); | ||
| 1143 | + } | ||
| 1144 | + if (!string.IsNullOrWhiteSpace(templateCode)) | ||
| 1145 | + { | ||
| 1146 | + if (sb.Length > 0) sb.Append(' '); | ||
| 1147 | + sb.Append('(').Append(templateCode.Trim()).Append(')'); | ||
| 1148 | + } | ||
| 1149 | + if (!string.IsNullOrWhiteSpace(templateParam)) | ||
| 1150 | + { | ||
| 1151 | + if (sb.Length > 0) sb.Append(':'); | ||
| 1152 | + sb.Append(templateParam.Trim()); | ||
| 1153 | + } | ||
| 1154 | + return sb.Length > 0 ? sb.ToString() : "无"; | ||
| 1155 | + } | ||
| 1156 | + | ||
| 1157 | + private static string ResolveSenderDisplayName(string creatorUserId, IDictionary<string, UserEntity> userMap) | ||
| 1158 | + { | ||
| 1159 | + if (string.IsNullOrWhiteSpace(creatorUserId)) | ||
| 1160 | + { | ||
| 1161 | + return "系统"; | ||
| 1162 | + } | ||
| 1163 | + if (userMap != null && userMap.TryGetValue(creatorUserId, out var user) && user != null) | ||
| 1164 | + { | ||
| 1165 | + if (!string.IsNullOrWhiteSpace(user.RealName)) | ||
| 1166 | + { | ||
| 1167 | + return user.RealName.Trim(); | ||
| 1168 | + } | ||
| 1169 | + if (!string.IsNullOrWhiteSpace(user.Account)) | ||
| 1170 | + { | ||
| 1171 | + return user.Account.Trim(); | ||
| 1172 | + } | ||
| 1173 | + } | ||
| 1174 | + return "无"; | ||
| 1175 | + } | ||
| 1176 | + | ||
| 1109 | private static string GetMapValue(Dictionary<string, string> map, string key) | 1177 | private static string GetMapValue(Dictionary<string, string> map, string key) |
| 1110 | { | 1178 | { |
| 1111 | if (map == null || string.IsNullOrWhiteSpace(key)) | 1179 | if (map == null || string.IsNullOrWhiteSpace(key)) |
netcore/src/Modularity/System/NCC.System.Entitys/Dto/System/SysConfig/SysConfigOutput.cs
| @@ -309,6 +309,11 @@ namespace NCC.System.Entitys.Dto.System.SysConfig | @@ -309,6 +309,11 @@ namespace NCC.System.Entitys.Dto.System.SysConfig | ||
| 309 | public string consumeEditableDays { get; set; } | 309 | public string consumeEditableDays { get; set; } |
| 310 | 310 | ||
| 311 | /// <summary> | 311 | /// <summary> |
| 312 | + /// 小程序端是否允许跨月修改/作废/重开单(1=允许,0=不允许);关闭时即使仍在可修改天数内,耗卡月份与当前月不同也禁止操作 | ||
| 313 | + /// </summary> | ||
| 314 | + public int? consumeAllowCrossMonthEdit { get; set; } | ||
| 315 | + | ||
| 316 | + /// <summary> | ||
| 312 | /// 管理后台作废耗卡允许天数;0 表示不限制天数(是否显示按钮由角色按钮权限控制) | 317 | /// 管理后台作废耗卡允许天数;0 表示不限制天数(是否显示按钮由角色按钮权限控制) |
| 313 | /// </summary> | 318 | /// </summary> |
| 314 | public string adminConsumeVoidEditableDays { get; set; } | 319 | public string adminConsumeVoidEditableDays { get; set; } |
绿纤uni-app/common/auth-nav.js
0 → 100644
| 1 | +import auth from '@/common/auth.js' | ||
| 2 | + | ||
| 3 | +let redirectingToLogin = false | ||
| 4 | + | ||
| 5 | +/** | ||
| 6 | + * 统一跳转登录页,避免多个接口同时 600 时连续 reLaunch 导致页面闪烁。 | ||
| 7 | + */ | ||
| 8 | +export function redirectToLogin() { | ||
| 9 | + if (redirectingToLogin) return | ||
| 10 | + | ||
| 11 | + const pages = getCurrentPages() | ||
| 12 | + const current = pages.length ? pages[pages.length - 1] : null | ||
| 13 | + const route = current && (current.route || (current.$page && current.$page.fullPath)) | ||
| 14 | + if (route && String(route).indexOf('pages/login/login') !== -1) { | ||
| 15 | + auth.clearLoginState() | ||
| 16 | + return | ||
| 17 | + } | ||
| 18 | + | ||
| 19 | + redirectingToLogin = true | ||
| 20 | + auth.clearLoginState() | ||
| 21 | + uni.reLaunch({ | ||
| 22 | + url: '/pages/login/login', | ||
| 23 | + complete: () => { | ||
| 24 | + setTimeout(() => { | ||
| 25 | + redirectingToLogin = false | ||
| 26 | + }, 800) | ||
| 27 | + } | ||
| 28 | + }) | ||
| 29 | +} | ||
| 30 | + | ||
| 31 | +export default { | ||
| 32 | + redirectToLogin | ||
| 33 | +} |
绿纤uni-app/pages.json
| @@ -7,25 +7,21 @@ | @@ -7,25 +7,21 @@ | ||
| 7 | }, | 7 | }, |
| 8 | "pages": [ | 8 | "pages": [ |
| 9 | { | 9 | { |
| 10 | - "path": "pages/home/home", | 10 | + "path": "pages/login/login", |
| 11 | "style": { | 11 | "style": { |
| 12 | - // "navigationBarTitleText": "绿纤工作台" | 12 | + "navigationBarTitleText": "绿纤协同平台", |
| 13 | "navigationStyle": "custom" | 13 | "navigationStyle": "custom" |
| 14 | } | 14 | } |
| 15 | }, | 15 | }, |
| 16 | { | 16 | { |
| 17 | - "path": "pages/index/index", | 17 | + "path": "pages/home/home", |
| 18 | "style": { | 18 | "style": { |
| 19 | - "navigationBarTitleText": "绿纤协同平台", | 19 | + // "navigationBarTitleText": "绿纤工作台" |
| 20 | "navigationStyle": "custom" | 20 | "navigationStyle": "custom" |
| 21 | } | 21 | } |
| 22 | }, | 22 | }, |
| 23 | - | ||
| 24 | - | ||
| 25 | - | ||
| 26 | - | ||
| 27 | { | 23 | { |
| 28 | - "path": "pages/login/login", | 24 | + "path": "pages/index/index", |
| 29 | "style": { | 25 | "style": { |
| 30 | "navigationBarTitleText": "绿纤协同平台", | 26 | "navigationBarTitleText": "绿纤协同平台", |
| 31 | "navigationStyle": "custom" | 27 | "navigationStyle": "custom" |
绿纤uni-app/pages/appointment-detail/appointment-detail.vue
| @@ -137,7 +137,7 @@ | @@ -137,7 +137,7 @@ | ||
| 137 | </view> | 137 | </view> |
| 138 | <view v-if="isAppointmentTransferred" class="action-buttons"> | 138 | <view v-if="isAppointmentTransferred" class="action-buttons"> |
| 139 | <button class="action-btn secondary" @click="viewConsumeDetail" :disabled="!appointmentDetail.consumeId">查看耗卡</button> | 139 | <button class="action-btn secondary" @click="viewConsumeDetail" :disabled="!appointmentDetail.consumeId">查看耗卡</button> |
| 140 | - <button class="action-btn secondary" @click="onReopenConsume">重开单</button> | 140 | + <button v-if="canReopenConsume" class="action-btn secondary" @click="onReopenConsume">重开单</button> |
| 141 | <button class="action-btn primary" @click="onCompleteAppointment">完成</button> | 141 | <button class="action-btn primary" @click="onCompleteAppointment">完成</button> |
| 142 | <button class="action-btn primary" @click="openRemarkDialog">补充说明</button> | 142 | <button class="action-btn primary" @click="openRemarkDialog">补充说明</button> |
| 143 | </view> | 143 | </view> |
| @@ -249,6 +249,11 @@ | @@ -249,6 +249,11 @@ | ||
| 249 | import request from '@/service/request.js' | 249 | import request from '@/service/request.js' |
| 250 | import appointmentApi from '@/apis/modules/appointment.js' | 250 | import appointmentApi from '@/apis/modules/appointment.js' |
| 251 | import consumeApi from '@/apis/modules/consume.js' | 251 | import consumeApi from '@/apis/modules/consume.js' |
| 252 | + import { | ||
| 253 | + canConsumeMiniProgramEdit, | ||
| 254 | + getConsumeEditablePolicy, | ||
| 255 | + loadConsumeEditablePolicy | ||
| 256 | + } from '@/utils/consume-editable-policy.js' | ||
| 252 | 257 | ||
| 253 | export default { | 258 | export default { |
| 254 | data() { | 259 | data() { |
| @@ -279,7 +284,8 @@ | @@ -279,7 +284,8 @@ | ||
| 279 | remarkSubmitting: false, | 284 | remarkSubmitting: false, |
| 280 | showCancelAppointmentDialog: false, | 285 | showCancelAppointmentDialog: false, |
| 281 | cancelAppointmentRemark: '', | 286 | cancelAppointmentRemark: '', |
| 282 | - cancelSubmitting: false | 287 | + cancelSubmitting: false, |
| 288 | + consumeEditablePolicy: getConsumeEditablePolicy() | ||
| 283 | } | 289 | } |
| 284 | }, | 290 | }, |
| 285 | computed: { | 291 | computed: { |
| @@ -306,8 +312,18 @@ | @@ -306,8 +312,18 @@ | ||
| 306 | if (s === '已取消') return 'failed'; | 312 | if (s === '已取消') return 'failed'; |
| 307 | if (s === '已完成') return 'success'; | 313 | if (s === '已完成') return 'success'; |
| 308 | return 'pending'; | 314 | return 'pending'; |
| 315 | + }, | ||
| 316 | + canReopenConsume() { | ||
| 317 | + const row = this.getRelatedConsumeRow(); | ||
| 318 | + if (!row) return false; | ||
| 319 | + return canConsumeMiniProgramEdit(row, this.consumeEditablePolicy); | ||
| 309 | } | 320 | } |
| 310 | }, | 321 | }, |
| 322 | + onShow() { | ||
| 323 | + loadConsumeEditablePolicy().then(policy => { | ||
| 324 | + this.consumeEditablePolicy = policy; | ||
| 325 | + }); | ||
| 326 | + }, | ||
| 311 | onLoad(options) { | 327 | onLoad(options) { |
| 312 | this.initializePage(options); | 328 | this.initializePage(options); |
| 313 | }, | 329 | }, |
| @@ -386,6 +402,19 @@ | @@ -386,6 +402,19 @@ | ||
| 386 | return String(val); | 402 | return String(val); |
| 387 | }, | 403 | }, |
| 388 | 404 | ||
| 405 | + getRelatedConsumeRow() { | ||
| 406 | + const detail = this.appointmentDetail; | ||
| 407 | + if (!detail) return null; | ||
| 408 | + const list = detail.relatedConsumeList || []; | ||
| 409 | + if (list.length > 0) { | ||
| 410 | + return list.find(item => item && item.hksj) || list[0]; | ||
| 411 | + } | ||
| 412 | + if (detail.consumeId && detail.consumeHksj) { | ||
| 413 | + return { id: detail.consumeId, hksj: detail.consumeHksj }; | ||
| 414 | + } | ||
| 415 | + return null; | ||
| 416 | + }, | ||
| 417 | + | ||
| 389 | formatOptionalTime(t) { | 418 | formatOptionalTime(t) { |
| 390 | if (!t) return '无'; | 419 | if (!t) return '无'; |
| 391 | try { | 420 | try { |
绿纤uni-app/pages/consume-detail/consume-detail.vue
| @@ -178,7 +178,7 @@ | @@ -178,7 +178,7 @@ | ||
| 178 | 178 | ||
| 179 | </view> | 179 | </view> |
| 180 | <!-- 操作按钮区域 --> | 180 | <!-- 操作按钮区域 --> |
| 181 | - <view class="action-buttons" v-if="consumeData && consumeData.isEffective == '1' && isCurrentMonth && newuserInfo.gw != '科技老师'"> | 181 | + <view class="action-buttons" v-if="consumeData && canShowConsumeActions && newuserInfo.gw != '科技老师'"> |
| 182 | <button | 182 | <button |
| 183 | class="action-btn cancel-btn" | 183 | class="action-btn cancel-btn" |
| 184 | @click="handleCancel" | 184 | @click="handleCancel" |
| @@ -239,6 +239,11 @@ | @@ -239,6 +239,11 @@ | ||
| 239 | <script> | 239 | <script> |
| 240 | import consumeApi from '@/apis/modules/consume.js' | 240 | import consumeApi from '@/apis/modules/consume.js' |
| 241 | import config from '@/common/config.js' | 241 | import config from '@/common/config.js' |
| 242 | + import { | ||
| 243 | + canConsumeMiniProgramEdit, | ||
| 244 | + getConsumeEditablePolicy, | ||
| 245 | + loadConsumeEditablePolicy | ||
| 246 | + } from '@/utils/consume-editable-policy.js' | ||
| 242 | 247 | ||
| 243 | export default { | 248 | export default { |
| 244 | data() { | 249 | data() { |
| @@ -253,24 +258,21 @@ | @@ -253,24 +258,21 @@ | ||
| 253 | cancelLoading: false, | 258 | cancelLoading: false, |
| 254 | reopenLoading: false, | 259 | reopenLoading: false, |
| 255 | cancelRemark: '', // 作废备注 | 260 | cancelRemark: '', // 作废备注 |
| 256 | - showCancelDialog: false // 显示作废对话框 | 261 | + showCancelDialog: false, // 显示作废对话框 |
| 262 | + consumeEditablePolicy: getConsumeEditablePolicy() | ||
| 257 | } | 263 | } |
| 258 | }, | 264 | }, |
| 259 | 265 | ||
| 260 | computed: { | 266 | computed: { |
| 261 | - // 判断耗卡日期是否在本月 | ||
| 262 | - isCurrentMonth() { | ||
| 263 | - if(this.isopen) { | ||
| 264 | - return this.isopen | ||
| 265 | - } else{ | ||
| 266 | - if (!this.consumeData || !this.consumeData.hksj) return false | ||
| 267 | - | ||
| 268 | - const consumeDate = new Date(this.consumeData.hksj) | ||
| 269 | - const currentDate = new Date() | ||
| 270 | - | ||
| 271 | - return consumeDate.getFullYear() === currentDate.getFullYear() && | ||
| 272 | - consumeDate.getMonth() === currentDate.getMonth() | 267 | + // 是否允许展示修改/作废(与后台可修改天数、跨月配置一致;菜单开关可绕过) |
| 268 | + canShowConsumeActions() { | ||
| 269 | + if (this.isopen) { | ||
| 270 | + return true | ||
| 271 | + } | ||
| 272 | + if (!this.consumeData || this.consumeData.isEffective != '1') { | ||
| 273 | + return false | ||
| 273 | } | 274 | } |
| 275 | + return canConsumeMiniProgramEdit(this.consumeData, this.consumeEditablePolicy) | ||
| 274 | } | 276 | } |
| 275 | }, | 277 | }, |
| 276 | 278 | ||
| @@ -283,6 +285,9 @@ | @@ -283,6 +285,9 @@ | ||
| 283 | }, | 285 | }, |
| 284 | onShow() { | 286 | onShow() { |
| 285 | this.getisopen() | 287 | this.getisopen() |
| 288 | + loadConsumeEditablePolicy().then(policy => { | ||
| 289 | + this.consumeEditablePolicy = policy | ||
| 290 | + }) | ||
| 286 | this.loadConsumeDetail() | 291 | this.loadConsumeDetail() |
| 287 | }, | 292 | }, |
| 288 | 293 |
绿纤uni-app/pages/home/home.vue
| @@ -94,6 +94,8 @@ | @@ -94,6 +94,8 @@ | ||
| 94 | import CustomTabBar from '@/components/custom-tab-bar/index.vue' | 94 | import CustomTabBar from '@/components/custom-tab-bar/index.vue' |
| 95 | import CustomNavbar from '@/components/custom-navbar/custom-navbar.vue' | 95 | import CustomNavbar from '@/components/custom-navbar/custom-navbar.vue' |
| 96 | import { normalizeAppPageUrl, buildWebViewPageUrl } from '@/common/page-route.js' | 96 | import { normalizeAppPageUrl, buildWebViewPageUrl } from '@/common/page-route.js' |
| 97 | + import auth from '@/common/auth.js' | ||
| 98 | + import { redirectToLogin } from '@/common/auth-nav.js' | ||
| 97 | 99 | ||
| 98 | export default { | 100 | export default { |
| 99 | components: { | 101 | components: { |
| @@ -737,15 +739,11 @@ | @@ -737,15 +739,11 @@ | ||
| 737 | }, | 739 | }, |
| 738 | // 检查登录状态 | 740 | // 检查登录状态 |
| 739 | checkLoginStatus() { | 741 | checkLoginStatus() { |
| 740 | - const token = uni.getStorageSync('token') | ||
| 741 | - if (!token) { | ||
| 742 | - uni.reLaunch({ | ||
| 743 | - url: '/pages/login/login' | ||
| 744 | - }) | 742 | + if (!auth.getToken()) { |
| 743 | + redirectToLogin() | ||
| 745 | return | 744 | return |
| 746 | - } else { | ||
| 747 | - this.userInfo = uni.getStorageSync('userInfo') | ||
| 748 | } | 745 | } |
| 746 | + this.userInfo = uni.getStorageSync('userInfo') | ||
| 749 | }, | 747 | }, |
| 750 | // 页面跳转 | 748 | // 页面跳转 |
| 751 | goToPage(url) { | 749 | goToPage(url) { |
绿纤uni-app/pages/index/index.vue
| @@ -248,6 +248,8 @@ | @@ -248,6 +248,8 @@ | ||
| 248 | import memberApi from '@/apis/modules/member.js' | 248 | import memberApi from '@/apis/modules/member.js' |
| 249 | import performanceApi from '@/apis/modules/performance.js' | 249 | import performanceApi from '@/apis/modules/performance.js' |
| 250 | import { normalizeAppPageUrl, buildWebViewPageUrl } from '@/common/page-route.js' | 250 | import { normalizeAppPageUrl, buildWebViewPageUrl } from '@/common/page-route.js' |
| 251 | + import auth from '@/common/auth.js' | ||
| 252 | + import { redirectToLogin } from '@/common/auth-nav.js' | ||
| 251 | export default { | 253 | export default { |
| 252 | data() { | 254 | data() { |
| 253 | return { | 255 | return { |
| @@ -353,16 +355,11 @@ | @@ -353,16 +355,11 @@ | ||
| 353 | 355 | ||
| 354 | // 检查登录状态 | 356 | // 检查登录状态 |
| 355 | checkLoginStatus() { | 357 | checkLoginStatus() { |
| 356 | - const token = uni.getStorageSync('token') | ||
| 357 | - | ||
| 358 | - if (!token) { | ||
| 359 | - uni.reLaunch({ | ||
| 360 | - url: '/pages/login/login' | ||
| 361 | - }) | 358 | + if (!auth.getToken()) { |
| 359 | + redirectToLogin() | ||
| 362 | return | 360 | return |
| 363 | - } else { | ||
| 364 | - this.userInfo = uni.getStorageSync('userInfo'); | ||
| 365 | } | 361 | } |
| 362 | + this.userInfo = uni.getStorageSync('userInfo'); | ||
| 366 | }, | 363 | }, |
| 367 | 364 | ||
| 368 | // 显示加载效果 | 365 | // 显示加载效果 |
绿纤uni-app/pages/login/login.vue
| @@ -191,11 +191,29 @@ export default { | @@ -191,11 +191,29 @@ export default { | ||
| 191 | forcePwd2: '', | 191 | forcePwd2: '', |
| 192 | forceShowPwd: false, | 192 | forceShowPwd: false, |
| 193 | forceSubmitting: false, | 193 | forceSubmitting: false, |
| 194 | - forcePwdUserId: '' | 194 | + forcePwdUserId: '', |
| 195 | + /** 已登录自动进首页,避免重复 reLaunch */ | ||
| 196 | + autoEntering: false | ||
| 195 | } | 197 | } |
| 196 | }, | 198 | }, |
| 197 | - onLoad() {}, | 199 | + onLoad() { |
| 200 | + this.tryAutoEnterHome() | ||
| 201 | + }, | ||
| 202 | + onShow() { | ||
| 203 | + this.tryAutoEnterHome() | ||
| 204 | + }, | ||
| 198 | methods: { | 205 | methods: { |
| 206 | + tryAutoEnterHome() { | ||
| 207 | + if (this.autoEntering) return | ||
| 208 | + if (!auth.getToken()) return | ||
| 209 | + this.autoEntering = true | ||
| 210 | + uni.reLaunch({ | ||
| 211 | + url: '/pages/home/home', | ||
| 212 | + complete: () => { | ||
| 213 | + this.autoEntering = false | ||
| 214 | + } | ||
| 215 | + }) | ||
| 216 | + }, | ||
| 199 | clearRememberedCredentials() { | 217 | clearRememberedCredentials() { |
| 200 | uni.removeStorageSync('rememberPassword') | 218 | uni.removeStorageSync('rememberPassword') |
| 201 | uni.removeStorageSync('rememberedUsername') | 219 | uni.removeStorageSync('rememberedUsername') |
绿纤uni-app/pagesA/lx/lx.vue
| @@ -784,6 +784,20 @@ | @@ -784,6 +784,20 @@ | ||
| 784 | this.supplementBillingId = options.supplementBillingId; | 784 | this.supplementBillingId = options.supplementBillingId; |
| 785 | this.kdhy = options.kdhy | 785 | this.kdhy = options.kdhy |
| 786 | this.qk = options.qk || 0 | 786 | this.qk = options.qk || 0 |
| 787 | + if (options.kdhy) { | ||
| 788 | + this.selectedValues.kdhy = options.kdhy | ||
| 789 | + try { | ||
| 790 | + const memberRes = await this.API.getMemberDetail(options.kdhy) | ||
| 791 | + if (memberRes && memberRes.code === 200 && memberRes.data) { | ||
| 792 | + this.formData.kdhy = memberRes.data.khmc || '' | ||
| 793 | + this.formData.kdhysjh = memberRes.data.sjh || '' | ||
| 794 | + this.formData.kdhyc = memberRes.data.khmc || '' | ||
| 795 | + this.formData.gjlx = memberRes.data.khlx || '' | ||
| 796 | + } | ||
| 797 | + } catch (e) { | ||
| 798 | + console.error('补缴开单加载会员类型失败', e) | ||
| 799 | + } | ||
| 800 | + } | ||
| 787 | // 设置默认日期为当前日期 | 801 | // 设置默认日期为当前日期 |
| 788 | this.formData.kdrq = this.utils.gettime().substring(0, 10); | 802 | this.formData.kdrq = this.utils.gettime().substring(0, 10); |
| 789 | // 添加默认的品项行 | 803 | // 添加默认的品项行 |
绿纤uni-app/service/request.js
| 1 | import service from './service.js' | 1 | import service from './service.js' |
| 2 | import config from '../common/config.js' | 2 | import config from '../common/config.js' |
| 3 | import auth from '@/common/auth.js' | 3 | import auth from '@/common/auth.js' |
| 4 | +import { redirectToLogin } from '@/common/auth-nav.js' | ||
| 4 | const get = function(url, data) { | 5 | const get = function(url, data) { |
| 5 | return request(url, 'GET', data); | 6 | return request(url, 'GET', data); |
| 6 | } | 7 | } |
| @@ -60,10 +61,7 @@ const request = function(url, method, data, headers, timeoutMs, options) { | @@ -60,10 +61,7 @@ const request = function(url, method, data, headers, timeoutMs, options) { | ||
| 60 | if (res.statusCode === 200) { | 61 | if (res.statusCode === 200) { |
| 61 | const biz = res.data || {} | 62 | const biz = res.data || {} |
| 62 | if (!opts.anonymous && (biz.code == 600 || biz.msg == '登录过期,请重新登录')) { | 63 | if (!opts.anonymous && (biz.code == 600 || biz.msg == '登录过期,请重新登录')) { |
| 63 | - auth.clearLoginState() | ||
| 64 | - uni.reLaunch({ | ||
| 65 | - url: '/pages/login/login' | ||
| 66 | - }); | 64 | + redirectToLogin() |
| 67 | } | 65 | } |
| 68 | if (biz.code < 0 && biz.message) { | 66 | if (biz.code < 0 && biz.message) { |
| 69 | uni.showToast({ | 67 | uni.showToast({ |
绿纤uni-app/utils/consume-editable-policy.js
0 → 100644
| 1 | +import sysConfigApi from '@/apis/modules/sys-config.js' | ||
| 2 | + | ||
| 3 | +export const CONSUME_EDITABLE_POLICY_KEY = 'consumeEditablePolicy' | ||
| 4 | + | ||
| 5 | +const DEFAULT_POLICY = { | ||
| 6 | + editableDays: 3, | ||
| 7 | + allowCrossMonth: false | ||
| 8 | +} | ||
| 9 | + | ||
| 10 | +export function parseConsumeEditablePolicy(data = {}) { | ||
| 11 | + const days = parseInt(data.consumeEditableDays, 10) | ||
| 12 | + return { | ||
| 13 | + editableDays: isNaN(days) || days < 1 ? DEFAULT_POLICY.editableDays : days, | ||
| 14 | + allowCrossMonth: Number(data.consumeAllowCrossMonthEdit) === 1 | ||
| 15 | + } | ||
| 16 | +} | ||
| 17 | + | ||
| 18 | +/** | ||
| 19 | + * 与后端 ConsumeEditableDaysHelper 口径一致:先校验天数,再校验跨月 | ||
| 20 | + */ | ||
| 21 | +export function canConsumeMiniProgramEdit(row, policy = DEFAULT_POLICY) { | ||
| 22 | + if (!row) return false | ||
| 23 | + const baseTime = parseConsumeBaseTime(row) | ||
| 24 | + if (!baseTime) return false | ||
| 25 | + | ||
| 26 | + const now = Date.now() | ||
| 27 | + if (now > baseTime.getTime() + policy.editableDays * 86400000) { | ||
| 28 | + return false | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + if (!policy.allowCrossMonth) { | ||
| 32 | + const nowDate = new Date() | ||
| 33 | + if ( | ||
| 34 | + baseTime.getFullYear() !== nowDate.getFullYear() || | ||
| 35 | + baseTime.getMonth() !== nowDate.getMonth() | ||
| 36 | + ) { | ||
| 37 | + return false | ||
| 38 | + } | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + return true | ||
| 42 | +} | ||
| 43 | + | ||
| 44 | +export function parseConsumeBaseTime(row) { | ||
| 45 | + const raw = row.hksj || row.Hksj || row.createTime || row.CreateTime | ||
| 46 | + if (!raw) return null | ||
| 47 | + const date = new Date(raw) | ||
| 48 | + return isNaN(date.getTime()) ? null : date | ||
| 49 | +} | ||
| 50 | + | ||
| 51 | +export function loadConsumeEditablePolicy(force = false) { | ||
| 52 | + if (!force) { | ||
| 53 | + const cached = uni.getStorageSync(CONSUME_EDITABLE_POLICY_KEY) | ||
| 54 | + if (cached && typeof cached === 'object') { | ||
| 55 | + refreshConsumeEditablePolicy() | ||
| 56 | + return Promise.resolve(cached) | ||
| 57 | + } | ||
| 58 | + } | ||
| 59 | + return refreshConsumeEditablePolicy() | ||
| 60 | +} | ||
| 61 | + | ||
| 62 | +export function refreshConsumeEditablePolicy() { | ||
| 63 | + return sysConfigApi.getSysConfig() | ||
| 64 | + .then(res => { | ||
| 65 | + const policy = parseConsumeEditablePolicy((res && res.data) || {}) | ||
| 66 | + uni.setStorageSync(CONSUME_EDITABLE_POLICY_KEY, policy) | ||
| 67 | + uni.$emit && uni.$emit('consume-editable-policy-change', policy) | ||
| 68 | + return policy | ||
| 69 | + }) | ||
| 70 | + .catch(() => { | ||
| 71 | + const cached = uni.getStorageSync(CONSUME_EDITABLE_POLICY_KEY) | ||
| 72 | + return cached && typeof cached === 'object' ? cached : { ...DEFAULT_POLICY } | ||
| 73 | + }) | ||
| 74 | +} | ||
| 75 | + | ||
| 76 | +export function getConsumeEditablePolicy() { | ||
| 77 | + const cached = uni.getStorageSync(CONSUME_EDITABLE_POLICY_KEY) | ||
| 78 | + return cached && typeof cached === 'object' ? cached : { ...DEFAULT_POLICY } | ||
| 79 | +} | ||
| 80 | + | ||
| 81 | +export function clearConsumeEditablePolicy() { | ||
| 82 | + uni.removeStorageSync(CONSUME_EDITABLE_POLICY_KEY) | ||
| 83 | +} | ||
| 84 | + | ||
| 85 | +export default { | ||
| 86 | + CONSUME_EDITABLE_POLICY_KEY, | ||
| 87 | + parseConsumeEditablePolicy, | ||
| 88 | + canConsumeMiniProgramEdit, | ||
| 89 | + parseConsumeBaseTime, | ||
| 90 | + loadConsumeEditablePolicy, | ||
| 91 | + refreshConsumeEditablePolicy, | ||
| 92 | + getConsumeEditablePolicy, | ||
| 93 | + clearConsumeEditablePolicy | ||
| 94 | +} |
项目文档相关/docs/数据库说明.md
| @@ -493,6 +493,7 @@ | @@ -493,6 +493,7 @@ | ||
| 493 | 493 | ||
| 494 | - **系统参数**(`BASE_SYSCONFIG`,`Category`=`SysConfig`): | 494 | - **系统参数**(`BASE_SYSCONFIG`,`Category`=`SysConfig`): |
| 495 | - `consumeEditableDays`:小程序端耗卡可修改/作废/重开单天数,默认 **3**。 | 495 | - `consumeEditableDays`:小程序端耗卡可修改/作废/重开单天数,默认 **3**。 |
| 496 | + - `consumeAllowCrossMonthEdit`:小程序端是否允许跨月修改/作废/重开单,**1=允许 / 0=不允许**,默认 **0**;关闭时耗卡月份与当前月不同即禁止(即使仍在可修改天数内)。 | ||
| 496 | - `adminConsumeVoidEditableDays`:管理后台作废耗卡允许天数,默认 **0**(0=不限制);按钮权限 `btn_cancel`。 | 497 | - `adminConsumeVoidEditableDays`:管理后台作废耗卡允许天数,默认 **0**(0=不限制);按钮权限 `btn_cancel`。 |
| 497 | - `adminConsumeDeleteEditableDays`:管理后台删除耗卡允许天数,默认 **0**;按钮权限 `btn_remove`。 | 498 | - `adminConsumeDeleteEditableDays`:管理后台删除耗卡允许天数,默认 **0**;按钮权限 `btn_remove`。 |
| 498 | - 已废弃合并项 `adminConsumeCancelEditableDays`(读取天数时兼容)。 | 499 | - 已废弃合并项 `adminConsumeCancelEditableDays`(读取天数时兼容)。 |
| @@ -606,6 +607,7 @@ | @@ -606,6 +607,7 @@ | ||
| 606 | - `F_MobileHash`:手机号哈希(防重比对) | 607 | - `F_MobileHash`:手机号哈希(防重比对) |
| 607 | - `F_MemberId` / `F_MemberName`:会员(生日等业务) | 608 | - `F_MemberId` / `F_MemberName`:会员(生日等业务) |
| 608 | - `F_TemplateParam`:实际参数 JSON | 609 | - `F_TemplateParam`:实际参数 JSON |
| 610 | + - `F_SendContent`:发送内容摘要(签名 + 模板名称/CODE + 参数,便于日志查看) | ||
| 609 | - `F_Status`:0 失败 / 1 成功 / 2 跳过 | 611 | - `F_Status`:0 失败 / 1 成功 / 2 跳过 |
| 610 | - `F_ProviderResult`:厂商返回说明 | 612 | - `F_ProviderResult`:厂商返回说明 |
| 611 | - `F_BizDate`:业务日期(生日防重按日) | 613 | - `F_BizDate`:业务日期(生日防重按日) |
项目文档相关/sql/2026-07-31/lq_kd_kdjlb_回填顾客类型gjlx.sql
0 → 100644
| 1 | +-- 开单记录 gjlx(顾客类型)为空时,从会员档案 khlx 回填快照 | ||
| 2 | +-- 原因:历史导入/部分开单入口未写入 gjlx,台账「客户类型」显示为「无」 | ||
| 3 | +-- 执行前请备份;建议在业务低峰执行 | ||
| 4 | + | ||
| 5 | +UPDATE lq_kd_kdjlb k | ||
| 6 | +INNER JOIN lq_khxx kh ON kh.F_Id = k.kdhy | ||
| 7 | +SET k.gjlx = kh.khlx | ||
| 8 | +WHERE (k.gjlx IS NULL OR TRIM(k.gjlx) = '') | ||
| 9 | + AND kh.khlx IS NOT NULL | ||
| 10 | + AND TRIM(kh.khlx) <> ''; |