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 | 41 | } |
| 42 | 42 | |
| 43 | 43 | intField('consumeEditableDays', 3) |
| 44 | + intField('consumeAllowCrossMonthEdit', 0) | |
| 44 | 45 | intField('adminConsumeVoidEditableDays', 0) |
| 45 | 46 | intField('adminConsumeDeleteEditableDays', 0) |
| 46 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 237 | </template> |
| 279 | 238 | |
| 280 | 239 | <script> |
| 281 | 240 | import request from '@/utils/request' |
| 241 | +import ConfigPanelShell from './config-panel-shell.vue' | |
| 282 | 242 | import ConfigRuleRow from './config-rule-row' |
| 283 | 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 | 255 | function parseIdList(raw) { |
| 286 | 256 | if (!raw) return [] |
| 287 | 257 | return String(raw) |
| ... | ... | @@ -292,7 +262,7 @@ function parseIdList(raw) { |
| 292 | 262 | |
| 293 | 263 | export default { |
| 294 | 264 | name: 'BusinessConfigPanel', |
| 295 | - components: { ConfigRuleRow, CommissionTierHelpPreview }, | |
| 265 | + components: { ConfigPanelShell, ConfigRuleRow, CommissionTierHelpPreview }, | |
| 296 | 266 | props: { |
| 297 | 267 | form: { |
| 298 | 268 | type: Object, |
| ... | ... | @@ -309,7 +279,7 @@ export default { |
| 309 | 279 | }, |
| 310 | 280 | data() { |
| 311 | 281 | return { |
| 312 | - activeModules: ['consume', 'tk', 'sleepRemind', 'trainingCheckin', 'memberTag', 'monitorCenter', 'goldTriangle'], | |
| 282 | + modules: MODULES, | |
| 313 | 283 | itemOptions: [], |
| 314 | 284 | itemLoading: false, |
| 315 | 285 | itemOptionsLoaded: false |
| ... | ... | @@ -332,7 +302,6 @@ export default { |
| 332 | 302 | immediate: true, |
| 333 | 303 | handler(val) { |
| 334 | 304 | const ids = parseIdList(val) |
| 335 | - // 配置页按单品项展示;若历史存了多个,只回显并保留第一项 | |
| 336 | 305 | if (ids.length > 1) { |
| 337 | 306 | this.$set(this.form, 'tkGiftItemIds', ids[0]) |
| 338 | 307 | } |
| ... | ... | @@ -392,14 +361,14 @@ export default { |
| 392 | 361 | this.itemOptions.forEach(item => { |
| 393 | 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 | 372 | this.itemOptions = Object.keys(map).map(k => map[k]) |
| 404 | 373 | } |
| 405 | 374 | } |
| ... | ... | @@ -407,75 +376,14 @@ export default { |
| 407 | 376 | </script> |
| 408 | 377 | |
| 409 | 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 | 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 | 389 | .config-section { |
| ... | ... | @@ -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 | 444 | .config-section { |
| 544 | 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 | 3 | <el-form ref="baseForm" :model="baseForm" label-width="120px"> |
| 4 | 4 | <el-tabs v-model="activeName" type="border-card" class="NCC-el_tabs"> |
| 5 | 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 | 18 | </div> |
| 99 | 19 | </el-tab-pane> |
| 100 | 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 | 32 | </el-tab-pane> |
| 170 | 33 | <el-tab-pane label="短信设置" name="sms"> |
| 171 | - <sms-config-panel /> | |
| 34 | + <div class="config-tab-panel"> | |
| 35 | + <sms-config-panel /> | |
| 36 | + </div> | |
| 172 | 37 | </el-tab-pane> |
| 173 | 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 | 48 | </el-tab-pane> |
| 226 | 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 | 53 | </el-tab-pane> |
| 229 | 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 | 63 | </el-tab-pane> |
| 264 | 64 | </el-tabs> |
| 265 | 65 | </el-form> |
| ... | ... | @@ -273,6 +73,10 @@ import { |
| 273 | 73 | } from '@/api/system/sysConfig' |
| 274 | 74 | import { getPayConfig, savePayConfig } from '@/api/extend/mallConfig' |
| 275 | 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 | 80 | import SmsConfigPanel from './sms-config-panel' |
| 277 | 81 | import { normalizeBusinessConfigFields } from '@/utils/consume-admin-policy' |
| 278 | 82 | import { ossUploadFile } from '@/utils/oss-upload' |
| ... | ... | @@ -281,7 +85,14 @@ import getPageTitle from '@/utils/get-page-title' |
| 281 | 85 | |
| 282 | 86 | export default { |
| 283 | 87 | name: 'system-sysConfig', |
| 284 | - components: { BusinessConfigPanel, SmsConfigPanel }, | |
| 88 | + components: { | |
| 89 | + BusinessConfigPanel, | |
| 90 | + BasicConfigPanel, | |
| 91 | + SecurityConfigPanel, | |
| 92 | + AiConfigPanel, | |
| 93 | + MallPayConfigPanel, | |
| 94 | + SmsConfigPanel | |
| 95 | + }, | |
| 285 | 96 | data() { |
| 286 | 97 | return { |
| 287 | 98 | defaultParaformerSubmitUrl: 'https://dashscope.aliyuncs.com/api/v1/services/audio/asr/transcription', |
| ... | ... | @@ -291,6 +102,7 @@ export default { |
| 291 | 102 | logoUploading: false, |
| 292 | 103 | logoIconUploading: false, |
| 293 | 104 | mallPayLoading: false, |
| 105 | + mallPayReady: false, | |
| 294 | 106 | mallPayForm: { |
| 295 | 107 | MchId: '', |
| 296 | 108 | ApiV3Key: '', |
| ... | ... | @@ -354,6 +166,7 @@ export default { |
| 354 | 166 | aiChatApiKey: '', |
| 355 | 167 | aiSummarizeSystemPrompt: '', |
| 356 | 168 | consumeEditableDays: 3, |
| 169 | + consumeAllowCrossMonthEdit: 0, | |
| 357 | 170 | adminConsumeVoidEditableDays: 0, |
| 358 | 171 | adminConsumeDeleteEditableDays: 0, |
| 359 | 172 | abnormalConsumeAmountTolerance: 0.01, |
| ... | ... | @@ -377,6 +190,7 @@ export default { |
| 377 | 190 | methods: { |
| 378 | 191 | // 加载会员商城支付配置(Category=MallPayment) |
| 379 | 192 | initMallPayConfig() { |
| 193 | + this.mallPayLoading = true | |
| 380 | 194 | getPayConfig().then(res => { |
| 381 | 195 | const data = (res && res.data) || {} |
| 382 | 196 | this.mallPayForm = { |
| ... | ... | @@ -387,7 +201,10 @@ export default { |
| 387 | 201 | NotifyUrl: data.notifyUrl || '', |
| 388 | 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 | 210 | saveMallPay() { |
| ... | ... | @@ -421,6 +238,9 @@ export default { |
| 421 | 238 | this.baseForm.commissionTierHelpSwitch = this.baseForm.commissionTierHelpSwitch == null |
| 422 | 239 | ? 1 |
| 423 | 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 | 244 | this.baseForm = normalizeBusinessConfigFields(this.baseForm) |
| 425 | 245 | this.syncSysBrand() |
| 426 | 246 | this.listLoading = false |
| ... | ... | @@ -493,6 +313,9 @@ export default { |
| 493 | 313 | payload.commissionTierHelpSwitch = payload.commissionTierHelpSwitch == null |
| 494 | 314 | ? 1 |
| 495 | 315 | : (Number(payload.commissionTierHelpSwitch) === 1 ? 1 : 0) |
| 316 | + payload.consumeAllowCrossMonthEdit = payload.consumeAllowCrossMonthEdit == null | |
| 317 | + ? 0 | |
| 318 | + : (Number(payload.consumeAllowCrossMonthEdit) === 1 ? 1 : 0) | |
| 496 | 319 | updateSystemConfig(payload).then(res => { |
| 497 | 320 | this.syncSysBrand() |
| 498 | 321 | this.$message({ |
| ... | ... | @@ -525,115 +348,23 @@ export default { |
| 525 | 348 | } |
| 526 | 349 | |
| 527 | 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 | 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 | 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 | 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 | 71 | <el-input |
| 25 | - v-model="configForm.accessKeySecret" | |
| 26 | - show-password | |
| 72 | + v-model="templateQuery.name" | |
| 73 | + size="small" | |
| 27 | 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 | 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 | 151 | <sms-template-dialog ref="templateDialog" @success="loadTemplates" /> |
| 151 | 152 | <sms-test-send-dialog ref="testDialog" /> |
| ... | ... | @@ -154,6 +155,7 @@ |
| 154 | 155 | </template> |
| 155 | 156 | |
| 156 | 157 | <script> |
| 158 | +import ConfigPanelShell from './config-panel-shell.vue' | |
| 157 | 159 | import { |
| 158 | 160 | getSmsConfig, |
| 159 | 161 | saveSmsConfig, |
| ... | ... | @@ -165,15 +167,22 @@ import SmsTemplateDialog from './sms-template-dialog.vue' |
| 165 | 167 | import SmsTestSendDialog from './sms-test-send-dialog.vue' |
| 166 | 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 | 175 | export default { |
| 169 | 176 | name: 'SmsConfigPanel', |
| 170 | 177 | components: { |
| 178 | + ConfigPanelShell, | |
| 171 | 179 | SmsTemplateDialog, |
| 172 | 180 | SmsTestSendDialog, |
| 173 | 181 | SmsSendLogDrawer |
| 174 | 182 | }, |
| 175 | 183 | data() { |
| 176 | 184 | return { |
| 185 | + modules: MODULES, | |
| 177 | 186 | loading: false, |
| 178 | 187 | configSaving: false, |
| 179 | 188 | templateLoading: false, |
| ... | ... | @@ -310,73 +319,73 @@ export default { |
| 310 | 319 | </script> |
| 311 | 320 | |
| 312 | 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 | 335 | display: flex; |
| 351 | - justify-content: space-between; | |
| 352 | 336 | align-items: center; |
| 353 | - flex-wrap: wrap; | |
| 354 | 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 | 347 | color: #909399; |
| 372 | 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 | 389 | border-color: rgba(255, 255, 255, 0.08); |
| 381 | 390 | |
| 382 | 391 | &__tag { | ... | ... |
antis-ncc-admin/src/views/system/sysConfig/sms-send-log-drawer.vue
| ... | ... | @@ -2,7 +2,7 @@ |
| 2 | 2 | <el-drawer |
| 3 | 3 | title="发送日志" |
| 4 | 4 | :visible.sync="visible" |
| 5 | - size="720px" | |
| 5 | + size="75%" | |
| 6 | 6 | append-to-body |
| 7 | 7 | :wrapper-closable="false" |
| 8 | 8 | custom-class="sms-send-log-drawer" |
| ... | ... | @@ -15,7 +15,7 @@ |
| 15 | 15 | size="small" |
| 16 | 16 | clearable |
| 17 | 17 | placeholder="场景" |
| 18 | - style="width: 140px; margin-right: 8px" | |
| 18 | + class="toolbar-item toolbar-item--scene" | |
| 19 | 19 | > |
| 20 | 20 | <el-option label="Birthday" value="Birthday" /> |
| 21 | 21 | <el-option label="Login" value="Login" /> |
| ... | ... | @@ -27,7 +27,7 @@ |
| 27 | 27 | size="small" |
| 28 | 28 | clearable |
| 29 | 29 | placeholder="状态" |
| 30 | - style="width: 120px; margin-right: 8px" | |
| 30 | + class="toolbar-item toolbar-item--status" | |
| 31 | 31 | > |
| 32 | 32 | <el-option label="成功" :value="1" /> |
| 33 | 33 | <el-option label="失败" :value="0" /> |
| ... | ... | @@ -41,38 +41,41 @@ |
| 41 | 41 | range-separator="至" |
| 42 | 42 | start-placeholder="业务日起" |
| 43 | 43 | end-placeholder="业务日止" |
| 44 | - style="margin-right: 8px; width: 260px" | |
| 44 | + class="toolbar-item toolbar-item--date" | |
| 45 | 45 | /> |
| 46 | 46 | <el-button size="small" icon="el-icon-search" @click="search">查询</el-button> |
| 47 | 47 | <el-button size="small" icon="el-icon-refresh" @click="reset">重置</el-button> |
| 48 | 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 | 52 | <template slot-scope="{ row }"> |
| 53 | 53 | {{ row.creatorTime ? ncc.dateFormat(row.creatorTime, 'YYYY-MM-DD HH:mm:ss') : '无' }} |
| 54 | 54 | </template> |
| 55 | 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 | 57 | <template slot-scope="{ row }">{{ row.sceneCode || '无' }}</template> |
| 58 | 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 | 60 | <template slot-scope="{ row }">{{ $maskMobile(row.mobileMasked) || '无' }}</template> |
| 64 | 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 | 63 | <template slot-scope="{ row }">{{ row.memberName || '无' }}</template> |
| 67 | 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 | 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 | 76 | </template> |
| 74 | 77 | </el-table-column> |
| 75 | - <el-table-column label="状态" width="90" align="center"> | |
| 78 | + <el-table-column label="状态" width="100" align="center"> | |
| 76 | 79 | <template slot-scope="{ row }"> |
| 77 | 80 | <el-tag |
| 78 | 81 | size="mini" |
| ... | ... | @@ -81,6 +84,7 @@ |
| 81 | 84 | > |
| 82 | 85 | {{ row.statusName || statusName(row.status) }} |
| 83 | 86 | </el-tag> |
| 87 | + <el-tag v-if="row.isTest === 1" size="mini" type="warning" disable-transitions>测试</el-tag> | |
| 84 | 88 | </template> |
| 85 | 89 | </el-table-column> |
| 86 | 90 | <el-table-column prop="providerResult" label="结果" min-width="140" show-overflow-tooltip> |
| ... | ... | @@ -177,6 +181,14 @@ export default { |
| 177 | 181 | statusName(status) { |
| 178 | 182 | const map = { 0: '失败', 1: '成功', 2: '跳过' } |
| 179 | 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 | 196 | |
| 185 | 197 | <style lang="scss" scoped> |
| 186 | 198 | .sms-send-log-body { |
| 199 | + display: flex; | |
| 200 | + flex-direction: column; | |
| 187 | 201 | padding: 0 16px 16px; |
| 188 | 202 | height: 100%; |
| 189 | 203 | box-sizing: border-box; |
| 190 | - overflow: auto; | |
| 204 | + overflow: hidden; | |
| 191 | 205 | } |
| 192 | 206 | |
| 193 | 207 | .sms-send-log-toolbar { |
| 194 | 208 | display: flex; |
| 195 | 209 | flex-wrap: wrap; |
| 196 | 210 | align-items: center; |
| 211 | + gap: 8px; | |
| 197 | 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 | 231 | </style> |
| 200 | 232 | |
| 201 | 233 | <style lang="scss"> |
| 234 | +/* | |
| 235 | + * 发送日志抽屉 append-to-body,表格不在 #app 内,全局 #app .el-table 41px 不生效。 | |
| 236 | + * 与 common.scss / NCC-table 行高规范对齐。 | |
| 237 | + */ | |
| 202 | 238 | .sms-send-log-drawer { |
| 203 | 239 | .el-drawer__body { |
| 204 | 240 | padding: 0; |
| 205 | 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 | 324 | </style> | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqSms/LqSmsSendLogListOutput.cs
| ... | ... | @@ -15,11 +15,20 @@ namespace NCC.Extend.Entitys.Dto.LqSms |
| 15 | 15 | public string memberId { get; set; } |
| 16 | 16 | public string memberName { get; set; } |
| 17 | 17 | public string templateParam { get; set; } |
| 18 | + /// <summary> | |
| 19 | + /// 发送内容摘要 | |
| 20 | + /// </summary> | |
| 21 | + public string sendContent { get; set; } | |
| 18 | 22 | public int status { get; set; } |
| 19 | 23 | public string statusName { get; set; } |
| 20 | 24 | public string providerResult { get; set; } |
| 21 | 25 | public DateTime? bizDate { get; set; } |
| 22 | 26 | public int isTest { get; set; } |
| 23 | 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 | 73 | public string TemplateParam { get; set; } |
| 74 | 74 | |
| 75 | 75 | /// <summary> |
| 76 | + /// 发送内容摘要(签名 + 模板 + 参数,便于日志查看) | |
| 77 | + /// </summary> | |
| 78 | + [SugarColumn(ColumnName = "F_SendContent", IsNullable = true)] | |
| 79 | + public string SendContent { get; set; } | |
| 80 | + | |
| 81 | + /// <summary> | |
| 76 | 82 | /// 发送状态(见 <see cref="SmsSendStatusEnum"/>) |
| 77 | 83 | /// </summary> |
| 78 | 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 | 23 | public const string KeyConsumeEditableDays = "consumeEditableDays"; |
| 24 | 24 | |
| 25 | 25 | /// <summary> |
| 26 | + /// 配置键:小程序端是否允许跨月修改/作废/重开单(1=允许,0=不允许) | |
| 27 | + /// </summary> | |
| 28 | + public const string KeyConsumeAllowCrossMonthEdit = "consumeAllowCrossMonthEdit"; | |
| 29 | + | |
| 30 | + /// <summary> | |
| 26 | 31 | /// 配置键:管理后台作废耗卡允许天数 |
| 27 | 32 | /// </summary> |
| 28 | 33 | public const string KeyAdminConsumeVoidEditableDays = "adminConsumeVoidEditableDays"; |
| ... | ... | @@ -63,6 +68,11 @@ namespace NCC.Extend.Helpers |
| 63 | 68 | public const int DefaultDays = 3; |
| 64 | 69 | |
| 65 | 70 | /// <summary> |
| 71 | + /// 小程序端缺省:不允许跨月修改 | |
| 72 | + /// </summary> | |
| 73 | + public const int DefaultConsumeAllowCrossMonthEdit = 0; | |
| 74 | + | |
| 75 | + /// <summary> | |
| 66 | 76 | /// 管理后台缺省天数:0 表示不限制 |
| 67 | 77 | /// </summary> |
| 68 | 78 | public const int DefaultAdminOperationEditableDays = 0; |
| ... | ... | @@ -81,12 +91,23 @@ namespace NCC.Extend.Helpers |
| 81 | 91 | } |
| 82 | 92 | |
| 83 | 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 | 103 | /// </summary> |
| 86 | 104 | public static async Task EnsureConsumeWithinEditableWindowAsync(ISqlSugarClient db, LqXhHyhkEntity card) |
| 87 | 105 | { |
| 88 | 106 | var days = await GetConsumeEditableDaysAsync(db); |
| 89 | 107 | EnsureWithinEditableWindow(card, days, "耗卡发生已超过 {0} 天,不允许修改/作废"); |
| 108 | + var allowCrossMonth = await GetConsumeAllowCrossMonthEditAsync(db); | |
| 109 | + if (!allowCrossMonth) | |
| 110 | + EnsureSameCalendarMonth(card, "耗卡已跨月,不允许修改/作废"); | |
| 90 | 111 | } |
| 91 | 112 | |
| 92 | 113 | /// <summary> |
| ... | ... | @@ -178,6 +199,16 @@ namespace NCC.Extend.Helpers |
| 178 | 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 | 212 | private static async Task<int> GetIntConfigWithLegacyAsync( |
| 182 | 213 | ISqlSugarClient db, |
| 183 | 214 | string key, |
| ... | ... | @@ -199,5 +230,20 @@ namespace NCC.Extend.Helpers |
| 199 | 230 | return defaultValue; |
| 200 | 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 | 197 | } |
| 198 | 198 | |
| 199 | 199 | var memberArchiveNoDict = await BuildMemberArchiveNoDictAsync(billingRecords.Select(x => x.kdhy)); |
| 200 | + var memberKhlxDict = await BuildMemberKhlxDictAsync(billingRecords.Select(x => x.kdhy)); | |
| 200 | 201 | |
| 201 | 202 | _logger.LogInformation($"查询到 {billingRecords.Count} 条开单记录"); |
| 202 | 203 | |
| ... | ... | @@ -387,7 +388,7 @@ namespace NCC.Extend.LqKdKdjlb |
| 387 | 388 | CustomerArchiveNo = ResolveMemberArchiveNo(billing.kdhy, memberArchiveNoDict) ?? "无", |
| 388 | 389 | CustomerChannel = string.IsNullOrEmpty(billing.kdhyjdqd) ? "无" : billing.kdhyjdqd, |
| 389 | 390 | GoldTriangle = GetGoldTriangleName(billing.jsj), |
| 390 | - CustomerType = ResolveCustomerTypeName(billing.gjlx), | |
| 391 | + CustomerType = BillingCustomerTypeHelper.ResolveCustomerTypeName(billing.gjlx, ResolveMemberKhlx(billing.kdhy, memberKhlxDict)), | |
| 391 | 392 | HealthCoach = "无", |
| 392 | 393 | PerformanceAmount = "¥0.00", |
| 393 | 394 | RelatedItem = ItemDisplayNameHelper.Display(exportItemNameMap, itemDetail.px, itemDetail.pxmc) ?? "无", |
| ... | ... | @@ -422,7 +423,7 @@ namespace NCC.Extend.LqKdKdjlb |
| 422 | 423 | CustomerArchiveNo = ResolveMemberArchiveNo(billing.kdhy, memberArchiveNoDict) ?? "无", |
| 423 | 424 | CustomerChannel = string.IsNullOrEmpty(billing.kdhyjdqd) ? "无" : billing.kdhyjdqd, |
| 424 | 425 | GoldTriangle = GetGoldTriangleName(billing.jsj), |
| 425 | - CustomerType = ResolveCustomerTypeName(billing.gjlx), | |
| 426 | + CustomerType = BillingCustomerTypeHelper.ResolveCustomerTypeName(billing.gjlx, ResolveMemberKhlx(billing.kdhy, memberKhlxDict)), | |
| 426 | 427 | HealthCoach = healthTeacher.jksxm ?? "无", |
| 427 | 428 | PerformanceAmount = FormatPerformanceAmount(healthTeacher.jksyj), |
| 428 | 429 | RelatedItem = ItemDisplayNameHelper.Display(exportItemNameMap, itemDetail.px, itemDetail.pxmc) ?? "无", |
| ... | ... | @@ -604,6 +605,7 @@ namespace NCC.Extend.LqKdKdjlb |
| 604 | 605 | var MemberInfo = await _db.Queryable<LqKhxxEntity>().FirstAsync(p => p.Id == entity.Kdhy); |
| 605 | 606 | output.kdhyc = MemberInfo.Khmc; |
| 606 | 607 | output.kdhysjh = MemberInfo.Sjh; |
| 608 | + output.gjlx = BillingCustomerTypeHelper.ResolveEffectiveGjlx(entity.Gjlx, MemberInfo?.Khlx) ?? output.gjlx; | |
| 607 | 609 | |
| 608 | 610 | if (!string.IsNullOrEmpty(entity.ActivityId)) |
| 609 | 611 | { |
| ... | ... | @@ -816,7 +818,7 @@ namespace NCC.Extend.LqKdKdjlb |
| 816 | 818 | { |
| 817 | 819 | var khxxMap = await _db.Queryable<LqKhxxEntity>() |
| 818 | 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 | 822 | .ToListAsync(); |
| 821 | 823 | var khxxDict = khxxMap.ToDictionary(x => x.Id); |
| 822 | 824 | foreach (var item in data.list) |
| ... | ... | @@ -825,6 +827,8 @@ namespace NCC.Extend.LqKdKdjlb |
| 825 | 827 | { |
| 826 | 828 | item.kdhyc = kh.Khmc; |
| 827 | 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 | 1352 | entity.CreateTime = DateTime.Now; |
| 1349 | 1353 | entity.UpdateTime = DateTime.Now; |
| 1350 | 1354 | entity.IsEffective = StatusEnum.有效.GetHashCode(); |
| 1355 | + await BillingCustomerTypeHelper.EnsureGjlxFromMemberAsync(_db, entity); | |
| 1351 | 1356 | try |
| 1352 | 1357 | { |
| 1353 | 1358 | //开启事务 |
| ... | ... | @@ -1739,6 +1744,7 @@ namespace NCC.Extend.LqKdKdjlb |
| 1739 | 1744 | |
| 1740 | 1745 | var entity = input.Adapt<LqKdKdjlbEntity>(); |
| 1741 | 1746 | entity.Id = id; // 确保ID正确设置 |
| 1747 | + await BillingCustomerTypeHelper.EnsureGjlxFromMemberAsync(_db, entity); | |
| 1742 | 1748 | try |
| 1743 | 1749 | { |
| 1744 | 1750 | //检查开单记录是否可以操作 |
| ... | ... | @@ -2477,6 +2483,13 @@ namespace NCC.Extend.LqKdKdjlb |
| 2477 | 2483 | { |
| 2478 | 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 | 2493 | var kdjlbEntities = new List<LqKdKdjlbEntity>(); |
| 2481 | 2494 | var pxmxEntities = new List<LqKdPxmxEntity>(); |
| 2482 | 2495 | |
| ... | ... | @@ -2496,6 +2509,7 @@ namespace NCC.Extend.LqKdKdjlb |
| 2496 | 2509 | Kdhyc = (string)item.会员名称, |
| 2497 | 2510 | Jsj = "暂无", |
| 2498 | 2511 | Kdhysjh = (string)item.会员电话, |
| 2512 | + Gjlx = ResolveMemberKhlx((string)item.F_MemberId, batchMemberKhlxDict), | |
| 2499 | 2513 | Kdrq = fixedDateTime, |
| 2500 | 2514 | Zdyj = Convert.ToDecimal(item.整单业绩 ?? 0), |
| 2501 | 2515 | Sfyj = Convert.ToDecimal(item.实付业绩 ?? 0), |
| ... | ... | @@ -3127,6 +3141,7 @@ namespace NCC.Extend.LqKdKdjlb |
| 3127 | 3141 | |
| 3128 | 3142 | var billingIds = billingRecords.Select(x => x.id).ToList(); |
| 3129 | 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 | 3147 | var itemDetailsQuery = _db.Queryable<LqKdPxmxEntity>() |
| ... | ... | @@ -3274,7 +3289,7 @@ namespace NCC.Extend.LqKdKdjlb |
| 3274 | 3289 | customerChannel = billing.kdhyjdqd, // 客户进店渠道 |
| 3275 | 3290 | storeId = billing.djmd, |
| 3276 | 3291 | goldTriangle = billing.jsj, |
| 3277 | - customerType = ResolveCustomerTypeName(billing.gjlx), | |
| 3292 | + customerType = BillingCustomerTypeHelper.ResolveCustomerTypeName(billing.gjlx, ResolveMemberKhlx(billing.kdhy, memberKhlxDict)), | |
| 3278 | 3293 | cooperationInstitution = billing.hgjg, |
| 3279 | 3294 | cooperationInstitutionName = billing.hgjgName, |
| 3280 | 3295 | paymentHospital = billing.fkyy, // 付款医院ID |
| ... | ... | @@ -3408,23 +3423,12 @@ namespace NCC.Extend.LqKdKdjlb |
| 3408 | 3423 | #endregion |
| 3409 | 3424 | |
| 3410 | 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 | 3434 | /// <summary> |
| ... | ... | @@ -3521,6 +3525,22 @@ namespace NCC.Extend.LqKdKdjlb |
| 3521 | 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 | 3544 | /// <summary> |
| 3525 | 3545 | /// 根据开单会员字段解析客户档案号 |
| 3526 | 3546 | /// </summary> | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend/LqSmsService.cs
| ... | ... | @@ -21,9 +21,11 @@ using NCC.Extend.Entitys.Enum; |
| 21 | 21 | using NCC.Extend.Entitys.lq_khxx; |
| 22 | 22 | using NCC.Extend.Entitys.lq_sms_send_log; |
| 23 | 23 | using NCC.Extend.Entitys.lq_sms_template; |
| 24 | +using NCC.Extend.Entitys.lq_sms_template; | |
| 24 | 25 | using NCC.Extend.Interfaces.BusinessOperationLog; |
| 25 | 26 | using NCC.Extend.Interfaces.LqSms; |
| 26 | 27 | using NCC.FriendlyException; |
| 28 | +using NCC.System.Entitys.Permission; | |
| 27 | 29 | using NCC.System.Entitys.System; |
| 28 | 30 | using Newtonsoft.Json; |
| 29 | 31 | using SqlSugar; |
| ... | ... | @@ -683,18 +685,37 @@ namespace NCC.Extend |
| 683 | 685 | memberId = it.MemberId, |
| 684 | 686 | memberName = it.MemberName, |
| 685 | 687 | templateParam = it.TemplateParam, |
| 688 | + sendContent = it.SendContent, | |
| 686 | 689 | status = it.Status, |
| 687 | 690 | providerResult = it.ProviderResult, |
| 688 | 691 | bizDate = it.BizDate, |
| 689 | 692 | isTest = it.IsTest, |
| 690 | - creatorTime = it.CreatorTime | |
| 693 | + creatorTime = it.CreatorTime, | |
| 694 | + creatorUserId = it.CreatorUserId | |
| 691 | 695 | }) |
| 692 | 696 | .ToPagedListAsync(input.currentPage, input.pageSize); |
| 693 | 697 | |
| 694 | 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 | 711 | foreach (var item in data.list) |
| 696 | 712 | { |
| 697 | 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 | 720 | if (mask) |
| 700 | 721 | { |
| ... | ... | @@ -723,6 +744,10 @@ namespace NCC.Extend |
| 723 | 744 | var configMap = await LoadConfigMapAsync(); |
| 724 | 745 | var prevent = ParseSwitch(GetMapValue(configMap, KeyPreventDuplicate), 1) == 1; |
| 725 | 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 | 752 | if (!skipDuplicate && prevent && !string.IsNullOrWhiteSpace(memberId)) |
| 728 | 753 | { |
| ... | ... | @@ -735,7 +760,7 @@ namespace NCC.Extend |
| 735 | 760 | if (exists) |
| 736 | 761 | { |
| 737 | 762 | var skipLog = await InsertSendLogAsync(template, input, bizDate, |
| 738 | - (int)SmsSendStatusEnum.跳过, "同日同场景已成功发送,跳过", null); | |
| 763 | + (int)SmsSendStatusEnum.跳过, "同日同场景已成功发送,跳过", null, signName); | |
| 739 | 764 | return new |
| 740 | 765 | { |
| 741 | 766 | logId = skipLog, |
| ... | ... | @@ -758,11 +783,6 @@ namespace NCC.Extend |
| 758 | 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 | 786 | var variables = input.variables ?? new Dictionary<string, string>(); |
| 767 | 787 | if (!variables.ContainsKey("name") && !string.IsNullOrWhiteSpace(input.memberName)) |
| 768 | 788 | { |
| ... | ... | @@ -793,7 +813,7 @@ namespace NCC.Extend |
| 793 | 813 | |
| 794 | 814 | var ok = string.Equals(providerResult, "OK", StringComparison.OrdinalIgnoreCase); |
| 795 | 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 | 818 | return new |
| 799 | 819 | { |
| ... | ... | @@ -812,20 +832,23 @@ namespace NCC.Extend |
| 812 | 832 | DateTime bizDate, |
| 813 | 833 | int status, |
| 814 | 834 | string providerResult, |
| 815 | - string templateParam) | |
| 835 | + string templateParam, | |
| 836 | + string signName) | |
| 816 | 837 | { |
| 838 | + var resolvedSign = string.IsNullOrWhiteSpace(signName) ? template.SignName : signName; | |
| 817 | 839 | var entity = new LqSmsSendLogEntity |
| 818 | 840 | { |
| 819 | 841 | Id = YitIdHelper.NextId().ToString(), |
| 820 | 842 | SceneCode = template.SceneCode, |
| 821 | 843 | TemplateId = template.Id, |
| 822 | 844 | TemplateCode = template.TemplateCode, |
| 823 | - SignName = template.SignName, | |
| 845 | + SignName = resolvedSign, | |
| 824 | 846 | MobileMasked = string.IsNullOrWhiteSpace(input.mobile) ? string.Empty : input.mobile.Trim(), |
| 825 | 847 | MobileHash = HashMobile(input.mobile), |
| 826 | 848 | MemberId = string.IsNullOrWhiteSpace(input.memberId) ? null : input.memberId.Trim(), |
| 827 | 849 | MemberName = input.memberName, |
| 828 | 850 | TemplateParam = templateParam, |
| 851 | + SendContent = FormatSendContent(resolvedSign, template.Name, template.TemplateCode, templateParam), | |
| 829 | 852 | Status = status, |
| 830 | 853 | ProviderResult = providerResult, |
| 831 | 854 | BizDate = bizDate.Date, |
| ... | ... | @@ -1106,6 +1129,51 @@ namespace NCC.Extend |
| 1106 | 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 | 1177 | private static string GetMapValue(Dictionary<string, string> map, string key) |
| 1110 | 1178 | { |
| 1111 | 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 | 309 | public string consumeEditableDays { get; set; } |
| 310 | 310 | |
| 311 | 311 | /// <summary> |
| 312 | + /// 小程序端是否允许跨月修改/作废/重开单(1=允许,0=不允许);关闭时即使仍在可修改天数内,耗卡月份与当前月不同也禁止操作 | |
| 313 | + /// </summary> | |
| 314 | + public int? consumeAllowCrossMonthEdit { get; set; } | |
| 315 | + | |
| 316 | + /// <summary> | |
| 312 | 317 | /// 管理后台作废耗卡允许天数;0 表示不限制天数(是否显示按钮由角色按钮权限控制) |
| 313 | 318 | /// </summary> |
| 314 | 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 | 7 | }, |
| 8 | 8 | "pages": [ |
| 9 | 9 | { |
| 10 | - "path": "pages/home/home", | |
| 10 | + "path": "pages/login/login", | |
| 11 | 11 | "style": { |
| 12 | - // "navigationBarTitleText": "绿纤工作台" | |
| 12 | + "navigationBarTitleText": "绿纤协同平台", | |
| 13 | 13 | "navigationStyle": "custom" |
| 14 | 14 | } |
| 15 | 15 | }, |
| 16 | 16 | { |
| 17 | - "path": "pages/index/index", | |
| 17 | + "path": "pages/home/home", | |
| 18 | 18 | "style": { |
| 19 | - "navigationBarTitleText": "绿纤协同平台", | |
| 19 | + // "navigationBarTitleText": "绿纤工作台" | |
| 20 | 20 | "navigationStyle": "custom" |
| 21 | 21 | } |
| 22 | 22 | }, |
| 23 | - | |
| 24 | - | |
| 25 | - | |
| 26 | - | |
| 27 | 23 | { |
| 28 | - "path": "pages/login/login", | |
| 24 | + "path": "pages/index/index", | |
| 29 | 25 | "style": { |
| 30 | 26 | "navigationBarTitleText": "绿纤协同平台", |
| 31 | 27 | "navigationStyle": "custom" | ... | ... |
绿纤uni-app/pages/appointment-detail/appointment-detail.vue
| ... | ... | @@ -137,7 +137,7 @@ |
| 137 | 137 | </view> |
| 138 | 138 | <view v-if="isAppointmentTransferred" class="action-buttons"> |
| 139 | 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 | 141 | <button class="action-btn primary" @click="onCompleteAppointment">完成</button> |
| 142 | 142 | <button class="action-btn primary" @click="openRemarkDialog">补充说明</button> |
| 143 | 143 | </view> |
| ... | ... | @@ -249,6 +249,11 @@ |
| 249 | 249 | import request from '@/service/request.js' |
| 250 | 250 | import appointmentApi from '@/apis/modules/appointment.js' |
| 251 | 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 | 258 | export default { |
| 254 | 259 | data() { |
| ... | ... | @@ -279,7 +284,8 @@ |
| 279 | 284 | remarkSubmitting: false, |
| 280 | 285 | showCancelAppointmentDialog: false, |
| 281 | 286 | cancelAppointmentRemark: '', |
| 282 | - cancelSubmitting: false | |
| 287 | + cancelSubmitting: false, | |
| 288 | + consumeEditablePolicy: getConsumeEditablePolicy() | |
| 283 | 289 | } |
| 284 | 290 | }, |
| 285 | 291 | computed: { |
| ... | ... | @@ -306,8 +312,18 @@ |
| 306 | 312 | if (s === '已取消') return 'failed'; |
| 307 | 313 | if (s === '已完成') return 'success'; |
| 308 | 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 | 327 | onLoad(options) { |
| 312 | 328 | this.initializePage(options); |
| 313 | 329 | }, |
| ... | ... | @@ -386,6 +402,19 @@ |
| 386 | 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 | 418 | formatOptionalTime(t) { |
| 390 | 419 | if (!t) return '无'; |
| 391 | 420 | try { | ... | ... |
绿纤uni-app/pages/consume-detail/consume-detail.vue
| ... | ... | @@ -178,7 +178,7 @@ |
| 178 | 178 | |
| 179 | 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 | 182 | <button |
| 183 | 183 | class="action-btn cancel-btn" |
| 184 | 184 | @click="handleCancel" |
| ... | ... | @@ -239,6 +239,11 @@ |
| 239 | 239 | <script> |
| 240 | 240 | import consumeApi from '@/apis/modules/consume.js' |
| 241 | 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 | 248 | export default { |
| 244 | 249 | data() { |
| ... | ... | @@ -253,24 +258,21 @@ |
| 253 | 258 | cancelLoading: false, |
| 254 | 259 | reopenLoading: false, |
| 255 | 260 | cancelRemark: '', // 作废备注 |
| 256 | - showCancelDialog: false // 显示作废对话框 | |
| 261 | + showCancelDialog: false, // 显示作废对话框 | |
| 262 | + consumeEditablePolicy: getConsumeEditablePolicy() | |
| 257 | 263 | } |
| 258 | 264 | }, |
| 259 | 265 | |
| 260 | 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 | 285 | }, |
| 284 | 286 | onShow() { |
| 285 | 287 | this.getisopen() |
| 288 | + loadConsumeEditablePolicy().then(policy => { | |
| 289 | + this.consumeEditablePolicy = policy | |
| 290 | + }) | |
| 286 | 291 | this.loadConsumeDetail() |
| 287 | 292 | }, |
| 288 | 293 | ... | ... |
绿纤uni-app/pages/home/home.vue
| ... | ... | @@ -94,6 +94,8 @@ |
| 94 | 94 | import CustomTabBar from '@/components/custom-tab-bar/index.vue' |
| 95 | 95 | import CustomNavbar from '@/components/custom-navbar/custom-navbar.vue' |
| 96 | 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 | 100 | export default { |
| 99 | 101 | components: { |
| ... | ... | @@ -737,15 +739,11 @@ |
| 737 | 739 | }, |
| 738 | 740 | // 检查登录状态 |
| 739 | 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 | 744 | return |
| 746 | - } else { | |
| 747 | - this.userInfo = uni.getStorageSync('userInfo') | |
| 748 | 745 | } |
| 746 | + this.userInfo = uni.getStorageSync('userInfo') | |
| 749 | 747 | }, |
| 750 | 748 | // 页面跳转 |
| 751 | 749 | goToPage(url) { | ... | ... |
绿纤uni-app/pages/index/index.vue
| ... | ... | @@ -248,6 +248,8 @@ |
| 248 | 248 | import memberApi from '@/apis/modules/member.js' |
| 249 | 249 | import performanceApi from '@/apis/modules/performance.js' |
| 250 | 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 | 253 | export default { |
| 252 | 254 | data() { |
| 253 | 255 | return { |
| ... | ... | @@ -353,16 +355,11 @@ |
| 353 | 355 | |
| 354 | 356 | // 检查登录状态 |
| 355 | 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 | 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 | 191 | forcePwd2: '', |
| 192 | 192 | forceShowPwd: false, |
| 193 | 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 | 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 | 217 | clearRememberedCredentials() { |
| 200 | 218 | uni.removeStorageSync('rememberPassword') |
| 201 | 219 | uni.removeStorageSync('rememberedUsername') | ... | ... |
绿纤uni-app/pagesA/lx/lx.vue
| ... | ... | @@ -784,6 +784,20 @@ |
| 784 | 784 | this.supplementBillingId = options.supplementBillingId; |
| 785 | 785 | this.kdhy = options.kdhy |
| 786 | 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 | 802 | this.formData.kdrq = this.utils.gettime().substring(0, 10); |
| 789 | 803 | // 添加默认的品项行 | ... | ... |
绿纤uni-app/service/request.js
| 1 | 1 | import service from './service.js' |
| 2 | 2 | import config from '../common/config.js' |
| 3 | 3 | import auth from '@/common/auth.js' |
| 4 | +import { redirectToLogin } from '@/common/auth-nav.js' | |
| 4 | 5 | const get = function(url, data) { |
| 5 | 6 | return request(url, 'GET', data); |
| 6 | 7 | } |
| ... | ... | @@ -60,10 +61,7 @@ const request = function(url, method, data, headers, timeoutMs, options) { |
| 60 | 61 | if (res.statusCode === 200) { |
| 61 | 62 | const biz = res.data || {} |
| 62 | 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 | 66 | if (biz.code < 0 && biz.message) { |
| 69 | 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 | 493 | |
| 494 | 494 | - **系统参数**(`BASE_SYSCONFIG`,`Category`=`SysConfig`): |
| 495 | 495 | - `consumeEditableDays`:小程序端耗卡可修改/作废/重开单天数,默认 **3**。 |
| 496 | + - `consumeAllowCrossMonthEdit`:小程序端是否允许跨月修改/作废/重开单,**1=允许 / 0=不允许**,默认 **0**;关闭时耗卡月份与当前月不同即禁止(即使仍在可修改天数内)。 | |
| 496 | 497 | - `adminConsumeVoidEditableDays`:管理后台作废耗卡允许天数,默认 **0**(0=不限制);按钮权限 `btn_cancel`。 |
| 497 | 498 | - `adminConsumeDeleteEditableDays`:管理后台删除耗卡允许天数,默认 **0**;按钮权限 `btn_remove`。 |
| 498 | 499 | - 已废弃合并项 `adminConsumeCancelEditableDays`(读取天数时兼容)。 |
| ... | ... | @@ -606,6 +607,7 @@ |
| 606 | 607 | - `F_MobileHash`:手机号哈希(防重比对) |
| 607 | 608 | - `F_MemberId` / `F_MemberName`:会员(生日等业务) |
| 608 | 609 | - `F_TemplateParam`:实际参数 JSON |
| 610 | + - `F_SendContent`:发送内容摘要(签名 + 模板名称/CODE + 参数,便于日志查看) | |
| 609 | 611 | - `F_Status`:0 失败 / 1 成功 / 2 跳过 |
| 610 | 612 | - `F_ProviderResult`:厂商返回说明 |
| 611 | 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) <> ''; | ... | ... |