Commit e2c481d6b6ed3dee632a8ed4ba55cefba3b21e7e
Merge branch 'master' of http://39.98.150.180/antissoft/lvqianmeiye_ERP
Showing
14 changed files
with
4873 additions
and
618 deletions
.cursor/agents/frontend-developer.md
| 1 | 1 | --- |
| 2 | -name: 前端 | |
| 2 | +name: frontend | |
| 3 | 3 | model: fast |
| 4 | -description: 前端 UI 开发专家。Vue 2.6 + Element UI。Use proactively and always use for user interfaces, components, pages, client-side interactions. Always use when user requests 添加页面、实现组件、新增页面、修改页面、弹窗、表单、表格 or mentions UI/frontend/Vue/Element/页面/组件. | |
| 4 | +description: 前端 UI 开发专家(中文可称「前端」)。Vue 2.6 + Element UI。Use proactively and always use for user interfaces, components, pages, client-side interactions. Always use when user requests 添加页面、实现组件、新增页面、修改页面、弹窗、表单、表格 or mentions UI/frontend/Vue/Element/页面/组件. | |
| 5 | 5 | --- |
| 6 | 6 | |
| 7 | 7 | 你是前端开发专家,专注用户界面。必须遵守项目规则中的前端规范。 | ... | ... |
store-pc/package-lock.json
| ... | ... | @@ -8,6 +8,11 @@ |
| 8 | 8 | "name": "lvqian-store-pc", |
| 9 | 9 | "version": "1.0.0", |
| 10 | 10 | "dependencies": { |
| 11 | + "@fullcalendar/core": "^4.4.2", | |
| 12 | + "@fullcalendar/daygrid": "^4.4.2", | |
| 13 | + "@fullcalendar/interaction": "^4.4.2", | |
| 14 | + "@fullcalendar/timegrid": "^4.4.2", | |
| 15 | + "@fullcalendar/vue": "^4.4.2", | |
| 11 | 16 | "axios": "^0.18.1", |
| 12 | 17 | "element-ui": "^2.15.5", |
| 13 | 18 | "normalize.css": "^8.0.1", |
| ... | ... | @@ -1679,6 +1684,61 @@ |
| 1679 | 1684 | "node": ">=6.9.0" |
| 1680 | 1685 | } |
| 1681 | 1686 | }, |
| 1687 | + "node_modules/@fullcalendar/core": { | |
| 1688 | + "version": "4.4.2", | |
| 1689 | + "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-4.4.2.tgz", | |
| 1690 | + "integrity": "sha512-vq7KQGuAJ1ieFG5tUqwxwUwmXYtblFOTjHaLAVHo6iEPB52mS7DS45VJfkhaQmX4+5/+BHRpg82G1qkuAINwtg==", | |
| 1691 | + "license": "MIT" | |
| 1692 | + }, | |
| 1693 | + "node_modules/@fullcalendar/daygrid": { | |
| 1694 | + "version": "4.4.2", | |
| 1695 | + "resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-4.4.2.tgz", | |
| 1696 | + "integrity": "sha512-axjfMhxEXHShV3r2TZjf+2niJ1C6LdAxkHKmg7mVq4jXtUQHOldU5XsjV0v2lUAt1urJBFi2zajfK8798ukL3Q==", | |
| 1697 | + "license": "MIT", | |
| 1698 | + "peerDependencies": { | |
| 1699 | + "@fullcalendar/core": "~4.4.0" | |
| 1700 | + } | |
| 1701 | + }, | |
| 1702 | + "node_modules/@fullcalendar/interaction": { | |
| 1703 | + "version": "4.4.2", | |
| 1704 | + "resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-4.4.2.tgz", | |
| 1705 | + "integrity": "sha512-3ItpGFnxcYQT4NClqhq93QTQwOI8x3mlMf5M4DgK5avVaSzpv9g8p+opqeotK2yzpFeINps06cuQyB1h7vcv1Q==", | |
| 1706 | + "license": "MIT", | |
| 1707 | + "peerDependencies": { | |
| 1708 | + "@fullcalendar/core": "~4.4.0" | |
| 1709 | + } | |
| 1710 | + }, | |
| 1711 | + "node_modules/@fullcalendar/timegrid": { | |
| 1712 | + "version": "4.4.2", | |
| 1713 | + "resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-4.4.2.tgz", | |
| 1714 | + "integrity": "sha512-M5an7qii8OUmI4ogY47k5pn2j/qUbLp6sa6Vo0gO182HR5pb9YtrEZnoQhnScok+I0BkDkLFzMQoiAMTjBm2PQ==", | |
| 1715 | + "license": "MIT", | |
| 1716 | + "dependencies": { | |
| 1717 | + "@fullcalendar/daygrid": "~4.4.0" | |
| 1718 | + }, | |
| 1719 | + "peerDependencies": { | |
| 1720 | + "@fullcalendar/core": "~4.4.0" | |
| 1721 | + } | |
| 1722 | + }, | |
| 1723 | + "node_modules/@fullcalendar/vue": { | |
| 1724 | + "version": "4.4.2", | |
| 1725 | + "resolved": "https://registry.npmjs.org/@fullcalendar/vue/-/vue-4.4.2.tgz", | |
| 1726 | + "integrity": "sha512-Iq5l8s0exyUI2vicPDs1Hn6SFLy0gnFAOEINqXixmnn9+U2fHgM++ofal1yKqpU9bAWE4d58Mizu2tlDlc6NyQ==", | |
| 1727 | + "license": "MIT", | |
| 1728 | + "dependencies": { | |
| 1729 | + "@fullcalendar/core": "~4.4.0", | |
| 1730 | + "fast-deep-equal": "^2.0.1" | |
| 1731 | + }, | |
| 1732 | + "peerDependencies": { | |
| 1733 | + "vue": "^2.6.6" | |
| 1734 | + } | |
| 1735 | + }, | |
| 1736 | + "node_modules/@fullcalendar/vue/node_modules/fast-deep-equal": { | |
| 1737 | + "version": "2.0.1", | |
| 1738 | + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", | |
| 1739 | + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", | |
| 1740 | + "license": "MIT" | |
| 1741 | + }, | |
| 1682 | 1742 | "node_modules/@hapi/address": { |
| 1683 | 1743 | "version": "2.1.4", |
| 1684 | 1744 | "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", | ... | ... |
store-pc/package.json
| ... | ... | @@ -8,6 +8,11 @@ |
| 8 | 8 | "lint": "eslint --ext .js,.vue src" |
| 9 | 9 | }, |
| 10 | 10 | "dependencies": { |
| 11 | + "@fullcalendar/core": "^4.4.2", | |
| 12 | + "@fullcalendar/daygrid": "^4.4.2", | |
| 13 | + "@fullcalendar/interaction": "^4.4.2", | |
| 14 | + "@fullcalendar/timegrid": "^4.4.2", | |
| 15 | + "@fullcalendar/vue": "^4.4.2", | |
| 11 | 16 | "axios": "^0.18.1", |
| 12 | 17 | "element-ui": "^2.15.5", |
| 13 | 18 | "normalize.css": "^8.0.1", | ... | ... |
store-pc/src/components/BillingDialog.vue
0 → 100644
| 1 | +<template> | |
| 2 | + <el-dialog | |
| 3 | + :visible.sync="visibleProxy" | |
| 4 | + :show-close="false" | |
| 5 | + width="1200px" | |
| 6 | + :close-on-click-modal="false" | |
| 7 | + custom-class="billing-dialog" | |
| 8 | + append-to-body | |
| 9 | + > | |
| 10 | + <div class="billing-dialog-inner"> | |
| 11 | + <div class="billing-header"> | |
| 12 | + <div class="billing-title-wrap"> | |
| 13 | + <div class="billing-title">快速开单</div> | |
| 14 | + <div class="billing-subtitle" v-if="form.memberName">{{ form.memberName }}</div> | |
| 15 | + </div> | |
| 16 | + <div class="booking-source-tag" v-if="prefill && prefill.fromBooking"> | |
| 17 | + <i class="el-icon-date"></i> | |
| 18 | + <span>预约开单</span> | |
| 19 | + <span class="booking-source-detail"> | |
| 20 | + {{ prefill.bookingProject }} · {{ prefill.bookingDate }} {{ prefill.bookingTimeRange }} | |
| 21 | + <template v-if="prefill.bookingStaff"> · {{ prefill.bookingStaff }}</template> | |
| 22 | + </span> | |
| 23 | + </div> | |
| 24 | + <span class="billing-close" @click="handleCancel"> | |
| 25 | + <i class="el-icon-close"></i> | |
| 26 | + </span> | |
| 27 | + </div> | |
| 28 | + | |
| 29 | + <div class="billing-content"> | |
| 30 | + <el-form | |
| 31 | + ref="form" | |
| 32 | + :model="form" | |
| 33 | + :rules="rules" | |
| 34 | + label-width="96px" | |
| 35 | + size="small" | |
| 36 | + class="billing-form" | |
| 37 | + > | |
| 38 | + <!-- ===== 左栏 ===== --> | |
| 39 | + <div class="billing-left"> | |
| 40 | + <div class="section-title"><i class="el-icon-document"></i> 基础信息</div> | |
| 41 | + | |
| 42 | + <el-form-item label="开单活动"> | |
| 43 | + <el-select v-model="form.activityId" placeholder="请选择活动(可选)" filterable clearable> | |
| 44 | + <el-option v-for="a in activityOptions" :key="a.value" :label="a.label" :value="a.value" /> | |
| 45 | + </el-select> | |
| 46 | + </el-form-item> | |
| 47 | + | |
| 48 | + <el-form-item label="开单会员" prop="memberId"> | |
| 49 | + <el-select v-model="form.memberId" placeholder="搜索会员" filterable clearable @change="onMemberChange"> | |
| 50 | + <el-option v-for="m in memberOptions" :key="m.value" :label="`${m.label}(${m.phone})`" :value="m.value" /> | |
| 51 | + </el-select> | |
| 52 | + </el-form-item> | |
| 53 | + | |
| 54 | + <el-form-item label="开单日期" prop="billingDate"> | |
| 55 | + <el-date-picker v-model="form.billingDate" type="date" placeholder="选择日期" style="width:100%" /> | |
| 56 | + </el-form-item> | |
| 57 | + | |
| 58 | + <el-form-item label="整单业绩" prop="totalPerformance"> | |
| 59 | + <el-input v-model="form.totalPerformance" placeholder="请输入整单业绩"> | |
| 60 | + <template slot="prepend">¥</template> | |
| 61 | + </el-input> | |
| 62 | + </el-form-item> | |
| 63 | + | |
| 64 | + <el-row :gutter="16"> | |
| 65 | + <el-col :span="12"> | |
| 66 | + <el-form-item label="实付业绩"> | |
| 67 | + <el-input :value="actualPaid" readonly> | |
| 68 | + <template slot="prepend">¥</template> | |
| 69 | + </el-input> | |
| 70 | + </el-form-item> | |
| 71 | + </el-col> | |
| 72 | + <el-col :span="12"> | |
| 73 | + <el-form-item label="欠款"> | |
| 74 | + <el-input :value="arrears" readonly> | |
| 75 | + <template slot="prepend">¥</template> | |
| 76 | + </el-input> | |
| 77 | + </el-form-item> | |
| 78 | + </el-col> | |
| 79 | + </el-row> | |
| 80 | + | |
| 81 | + <!-- 储扣设置 --> | |
| 82 | + <div class="section-title"><i class="el-icon-wallet"></i> 储扣设置</div> | |
| 83 | + | |
| 84 | + <el-form-item label="是否储扣"> | |
| 85 | + <el-radio-group v-model="form.isDeduct"> | |
| 86 | + <el-radio label="否">否</el-radio> | |
| 87 | + <el-radio label="是">是</el-radio> | |
| 88 | + </el-radio-group> | |
| 89 | + </el-form-item> | |
| 90 | + | |
| 91 | + <template v-if="form.isDeduct === '是'"> | |
| 92 | + <div | |
| 93 | + v-for="(d, di) in form.deductItems" | |
| 94 | + :key="'deduct-' + di" | |
| 95 | + class="item-card deduct-card" | |
| 96 | + > | |
| 97 | + <div class="item-card-head"> | |
| 98 | + <span class="item-card-no">储扣 {{ di + 1 }}</span> | |
| 99 | + <el-button | |
| 100 | + v-if="form.deductItems.length > 1" | |
| 101 | + type="text" | |
| 102 | + class="item-remove-btn" | |
| 103 | + @click="removeDeductItem(di)" | |
| 104 | + > | |
| 105 | + <i class="el-icon-delete"></i> 删除 | |
| 106 | + </el-button> | |
| 107 | + </div> | |
| 108 | + <el-row :gutter="12"> | |
| 109 | + <el-col :span="12"> | |
| 110 | + <el-form-item label="品项" label-width="56px"> | |
| 111 | + <el-select v-model="d.deductId" placeholder="选择储扣品项" filterable clearable @change="onDeductChange(di)"> | |
| 112 | + <el-option v-for="dd in deductOptions" :key="dd.value" :label="dd.label" :value="dd.value" /> | |
| 113 | + </el-select> | |
| 114 | + </el-form-item> | |
| 115 | + </el-col> | |
| 116 | + <el-col :span="12"> | |
| 117 | + <el-form-item label="单价" label-width="48px"> | |
| 118 | + <el-input :value="d.price" readonly /> | |
| 119 | + </el-form-item> | |
| 120 | + </el-col> | |
| 121 | + </el-row> | |
| 122 | + <el-row :gutter="12"> | |
| 123 | + <el-col :span="12"> | |
| 124 | + <el-form-item label="数量" label-width="56px"> | |
| 125 | + <el-input-number v-model="d.quantity" :min="1" :max="d.maxQty || 999" controls-position="right" style="width:100%" /> | |
| 126 | + </el-form-item> | |
| 127 | + </el-col> | |
| 128 | + <el-col :span="12"> | |
| 129 | + <el-form-item label="小计" label-width="48px"> | |
| 130 | + <el-input :value="deductSubtotal(d)" readonly /> | |
| 131 | + </el-form-item> | |
| 132 | + </el-col> | |
| 133 | + </el-row> | |
| 134 | + </div> | |
| 135 | + | |
| 136 | + <div class="add-btn-row"> | |
| 137 | + <el-button type="text" @click="addDeductItem"> | |
| 138 | + <i class="el-icon-circle-plus-outline"></i> 添加储扣品项 | |
| 139 | + </el-button> | |
| 140 | + </div> | |
| 141 | + </template> | |
| 142 | + | |
| 143 | + <!-- 付款与其他 --> | |
| 144 | + <div class="section-title"><i class="el-icon-bank-card"></i> 付款与其他</div> | |
| 145 | + | |
| 146 | + <el-form-item label="付款方式" prop="paymentMethod"> | |
| 147 | + <el-select v-model="form.paymentMethod" placeholder="请选择付款方式"> | |
| 148 | + <el-option v-for="p in paymentOptions" :key="p.value" :label="p.label" :value="p.value" /> | |
| 149 | + </el-select> | |
| 150 | + </el-form-item> | |
| 151 | + | |
| 152 | + <el-form-item v-if="form.paymentMethod === '合作'" label="合作机构"> | |
| 153 | + <el-select v-model="form.cooperateOrg" placeholder="请选择合作机构" filterable clearable> | |
| 154 | + <el-option v-for="o in orgOptions" :key="o.value" :label="o.label" :value="o.value" /> | |
| 155 | + </el-select> | |
| 156 | + </el-form-item> | |
| 157 | + <el-form-item v-else label="结算机构"> | |
| 158 | + <el-select v-model="form.settleOrg" placeholder="请选择结算机构" filterable clearable> | |
| 159 | + <el-option v-for="o in orgOptions" :key="o.value" :label="o.label" :value="o.value" /> | |
| 160 | + </el-select> | |
| 161 | + </el-form-item> | |
| 162 | + | |
| 163 | + <el-row :gutter="16"> | |
| 164 | + <el-col :span="12"> | |
| 165 | + <el-form-item label="是否首开"> | |
| 166 | + <el-select v-model="form.isFirstOrder" placeholder="请选择"> | |
| 167 | + <el-option label="是" value="是" /> | |
| 168 | + <el-option label="否" value="否" /> | |
| 169 | + </el-select> | |
| 170 | + </el-form-item> | |
| 171 | + </el-col> | |
| 172 | + <el-col :span="12"> | |
| 173 | + <el-form-item label="简介"> | |
| 174 | + <el-input v-model="form.intro" placeholder="简要说明(可选)" /> | |
| 175 | + </el-form-item> | |
| 176 | + </el-col> | |
| 177 | + </el-row> | |
| 178 | + | |
| 179 | + <!-- 附件与备注 --> | |
| 180 | + <div class="section-title"><i class="el-icon-paperclip"></i> 附件与备注</div> | |
| 181 | + | |
| 182 | + <el-form-item label="收据文件"> | |
| 183 | + <el-upload | |
| 184 | + action="#" | |
| 185 | + :auto-upload="false" | |
| 186 | + :file-list="form.receiptFiles" | |
| 187 | + :on-change="(f, fl) => form.receiptFiles = fl" | |
| 188 | + :on-remove="(f, fl) => form.receiptFiles = fl" | |
| 189 | + multiple | |
| 190 | + > | |
| 191 | + <el-button size="mini" type="primary" plain><i class="el-icon-upload2"></i> 上传收据</el-button> | |
| 192 | + <span slot="tip" class="upload-tip">支持图片、PDF,可多个</span> | |
| 193 | + </el-upload> | |
| 194 | + </el-form-item> | |
| 195 | + | |
| 196 | + <el-form-item label="方案文件"> | |
| 197 | + <el-upload | |
| 198 | + action="#" | |
| 199 | + :auto-upload="false" | |
| 200 | + :file-list="form.otherFiles" | |
| 201 | + :on-change="(f, fl) => form.otherFiles = fl" | |
| 202 | + :on-remove="(f, fl) => form.otherFiles = fl" | |
| 203 | + multiple | |
| 204 | + > | |
| 205 | + <el-button size="mini" type="primary" plain><i class="el-icon-upload2"></i> 上传文件</el-button> | |
| 206 | + <span slot="tip" class="upload-tip">方案/其他附件</span> | |
| 207 | + </el-upload> | |
| 208 | + </el-form-item> | |
| 209 | + | |
| 210 | + <el-form-item label="备注"> | |
| 211 | + <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注信息" /> | |
| 212 | + </el-form-item> | |
| 213 | + </div> | |
| 214 | + | |
| 215 | + <!-- ===== 右栏:品项明细 ===== --> | |
| 216 | + <div class="billing-right"> | |
| 217 | + <div class="section-title"><i class="el-icon-goods"></i> 品项明细</div> | |
| 218 | + | |
| 219 | + <div | |
| 220 | + v-for="(item, idx) in form.items" | |
| 221 | + :key="'item-' + idx" | |
| 222 | + class="item-card" | |
| 223 | + > | |
| 224 | + <div class="item-card-head"> | |
| 225 | + <span class="item-card-no">品项 {{ idx + 1 }}</span> | |
| 226 | + <el-button | |
| 227 | + v-if="form.items.length > 1" | |
| 228 | + type="text" | |
| 229 | + class="item-remove-btn" | |
| 230 | + @click="removeItem(idx)" | |
| 231 | + > | |
| 232 | + <i class="el-icon-delete"></i> 删除 | |
| 233 | + </el-button> | |
| 234 | + </div> | |
| 235 | + | |
| 236 | + <el-row :gutter="12"> | |
| 237 | + <el-col :span="12"> | |
| 238 | + <el-form-item | |
| 239 | + label="品项" | |
| 240 | + :prop="'items.' + idx + '.projectId'" | |
| 241 | + :rules="[{ required: true, message: '请选择品项', trigger: 'change' }]" | |
| 242 | + label-width="56px" | |
| 243 | + > | |
| 244 | + <el-select | |
| 245 | + v-model="item.projectId" | |
| 246 | + placeholder="搜索品项" | |
| 247 | + filterable | |
| 248 | + clearable | |
| 249 | + @change="onProjectChange(idx)" | |
| 250 | + > | |
| 251 | + <el-option v-for="p in projectOptions" :key="p.value" :label="p.label" :value="p.value" /> | |
| 252 | + </el-select> | |
| 253 | + </el-form-item> | |
| 254 | + </el-col> | |
| 255 | + <el-col :span="12"> | |
| 256 | + <el-form-item label="类型" label-width="48px"> | |
| 257 | + <el-select v-model="item.type" placeholder="类型"> | |
| 258 | + <el-option label="购买" value="购买" /> | |
| 259 | + <el-option label="体验" value="体验" /> | |
| 260 | + <el-option label="赠送" value="赠送" /> | |
| 261 | + </el-select> | |
| 262 | + </el-form-item> | |
| 263 | + </el-col> | |
| 264 | + </el-row> | |
| 265 | + | |
| 266 | + <el-row :gutter="12"> | |
| 267 | + <el-col :span="8"> | |
| 268 | + <el-form-item label="总价" label-width="48px"> | |
| 269 | + <el-input v-model="item.totalPrice" placeholder="总价" /> | |
| 270 | + </el-form-item> | |
| 271 | + </el-col> | |
| 272 | + <el-col :span="8"> | |
| 273 | + <el-form-item label="数量" label-width="48px"> | |
| 274 | + <el-input-number v-model="item.quantity" :min="1" :max="999" controls-position="right" style="width:100%" /> | |
| 275 | + </el-form-item> | |
| 276 | + </el-col> | |
| 277 | + <el-col :span="8"> | |
| 278 | + <el-form-item label="单价" label-width="48px"> | |
| 279 | + <el-input :value="itemUnitPrice(item)" readonly /> | |
| 280 | + </el-form-item> | |
| 281 | + </el-col> | |
| 282 | + </el-row> | |
| 283 | + | |
| 284 | + <el-form-item label="备注" label-width="56px"> | |
| 285 | + <el-input v-model="item.remark" placeholder="品项备注(可选)" /> | |
| 286 | + </el-form-item> | |
| 287 | + | |
| 288 | + <!-- 健康师 --> | |
| 289 | + <div class="worker-section"> | |
| 290 | + <div class="worker-label"> | |
| 291 | + <span>健康师业绩分配</span> | |
| 292 | + <el-button type="text" size="mini" @click="addWorker(idx)"> | |
| 293 | + <i class="el-icon-plus"></i> 添加健康师 | |
| 294 | + </el-button> | |
| 295 | + </div> | |
| 296 | + <div v-for="(w, wi) in item.workers" :key="'w-' + wi" class="worker-row"> | |
| 297 | + <el-select v-model="w.workerId" placeholder="选择健康师" filterable size="mini" class="worker-select"> | |
| 298 | + <el-option v-for="h in healthWorkerOptions" :key="h.value" :label="h.label" :value="h.value" /> | |
| 299 | + </el-select> | |
| 300 | + <el-input v-model="w.amount" placeholder="业绩金额" size="mini" class="worker-amount"> | |
| 301 | + <template slot="prepend">¥</template> | |
| 302 | + </el-input> | |
| 303 | + <el-button | |
| 304 | + v-if="item.workers.length > 1" | |
| 305 | + type="text" | |
| 306 | + size="mini" | |
| 307 | + class="worker-remove" | |
| 308 | + @click="removeWorker(idx, wi)" | |
| 309 | + > | |
| 310 | + <i class="el-icon-close"></i> | |
| 311 | + </el-button> | |
| 312 | + </div> | |
| 313 | + </div> | |
| 314 | + </div> | |
| 315 | + | |
| 316 | + <div class="add-btn-row"> | |
| 317 | + <el-button type="text" @click="addItem"> | |
| 318 | + <i class="el-icon-circle-plus-outline"></i> 添加品项 | |
| 319 | + </el-button> | |
| 320 | + </div> | |
| 321 | + </div> | |
| 322 | + </el-form> | |
| 323 | + </div> | |
| 324 | + | |
| 325 | + <div class="billing-footer"> | |
| 326 | + <div class="footer-summary"> | |
| 327 | + <span>品项合计 <b>¥{{ itemsTotal }}</b></span> | |
| 328 | + <span v-if="form.isDeduct === '是'">储扣合计 <b>¥{{ deductTotal }}</b></span> | |
| 329 | + <span>实付 <b class="highlight">¥{{ actualPaid }}</b></span> | |
| 330 | + </div> | |
| 331 | + <div class="footer-actions"> | |
| 332 | + <el-button size="small" @click="handleCancel">取 消</el-button> | |
| 333 | + <el-button type="primary" size="small" :loading="submitting" @click="handleSubmit"> | |
| 334 | + {{ submitting ? '提交中...' : '确认开单' }} | |
| 335 | + </el-button> | |
| 336 | + </div> | |
| 337 | + </div> | |
| 338 | + </div> | |
| 339 | + </el-dialog> | |
| 340 | +</template> | |
| 341 | + | |
| 342 | +<script> | |
| 343 | +export default { | |
| 344 | + name: 'BillingDialog', | |
| 345 | + props: { | |
| 346 | + visible: { type: Boolean, default: false }, | |
| 347 | + prefill: { | |
| 348 | + type: Object, | |
| 349 | + default: () => ({}) | |
| 350 | + } | |
| 351 | + }, | |
| 352 | + data() { | |
| 353 | + return { | |
| 354 | + submitting: false, | |
| 355 | + form: this.createEmptyForm(), | |
| 356 | + activityOptions: [ | |
| 357 | + { value: '1', label: '社区拓客活动' }, | |
| 358 | + { value: '2', label: '商场联营活动' } | |
| 359 | + ], | |
| 360 | + memberOptions: [ | |
| 361 | + { value: 'cust001', label: '张三', phone: '13800000001', type: '散客' }, | |
| 362 | + { value: 'cust002', label: '李四', phone: '13800000002', type: 'VIP' }, | |
| 363 | + { value: 'cust003', label: '王五', phone: '13800000003', type: '散客' } | |
| 364 | + ], | |
| 365 | + projectOptions: [ | |
| 366 | + { value: 'proj001', label: '基础护理', price: 380 }, | |
| 367 | + { value: 'proj002', label: '深度清洁', price: 268 }, | |
| 368 | + { value: 'proj003', label: '美白护理', price: 498 }, | |
| 369 | + { value: 'proj004', label: '补水保湿', price: 168 }, | |
| 370 | + { value: 'proj005', label: '储值10000', price: 1, isStoredValue: true } | |
| 371 | + ], | |
| 372 | + healthWorkerOptions: [ | |
| 373 | + { value: 'jks001', label: '张健康师' }, | |
| 374 | + { value: 'jks002', label: '李健康师' }, | |
| 375 | + { value: 'jks003', label: '王健康师' } | |
| 376 | + ], | |
| 377 | + paymentOptions: [ | |
| 378 | + { value: '现金', label: '现金' }, | |
| 379 | + { value: '微信', label: '微信' }, | |
| 380 | + { value: '支付宝', label: '支付宝' }, | |
| 381 | + { value: '银行卡', label: '银行卡' }, | |
| 382 | + { value: '合作', label: '合作' }, | |
| 383 | + { value: '直播收款', label: '直播收款' }, | |
| 384 | + { value: '合作方退', label: '合作方退' } | |
| 385 | + ], | |
| 386 | + orgOptions: [ | |
| 387 | + { value: 'org001', label: '合作机构A' }, | |
| 388 | + { value: 'org002', label: '合作机构B' } | |
| 389 | + ], | |
| 390 | + deductOptions: [ | |
| 391 | + { value: 'd001', label: '基础护理(剩余3次)', price: 380, remaining: 3 }, | |
| 392 | + { value: 'd002', label: '深度清洁(剩余2次)', price: 268, remaining: 2 } | |
| 393 | + ], | |
| 394 | + rules: { | |
| 395 | + memberId: [{ required: true, message: '请选择开单会员', trigger: 'change' }], | |
| 396 | + billingDate: [{ required: true, message: '请选择开单日期', trigger: 'change' }], | |
| 397 | + totalPerformance: [{ required: true, message: '请输入整单业绩', trigger: 'blur' }], | |
| 398 | + paymentMethod: [{ required: true, message: '请选择付款方式', trigger: 'change' }] | |
| 399 | + } | |
| 400 | + } | |
| 401 | + }, | |
| 402 | + computed: { | |
| 403 | + visibleProxy: { | |
| 404 | + get() { return this.visible }, | |
| 405 | + set(val) { this.$emit('update:visible', val) } | |
| 406 | + }, | |
| 407 | + itemsTotal() { | |
| 408 | + return this.form.items.reduce((sum, it) => { | |
| 409 | + return sum + (parseFloat(it.totalPrice) || 0) | |
| 410 | + }, 0).toFixed(2) | |
| 411 | + }, | |
| 412 | + deductTotal() { | |
| 413 | + if (this.form.isDeduct !== '是') return '0.00' | |
| 414 | + return this.form.deductItems.reduce((sum, d) => { | |
| 415 | + return sum + (parseFloat(d.price) || 0) * (d.quantity || 0) | |
| 416 | + }, 0).toFixed(2) | |
| 417 | + }, | |
| 418 | + actualPaid() { | |
| 419 | + const items = parseFloat(this.itemsTotal) || 0 | |
| 420 | + const deduct = parseFloat(this.deductTotal) || 0 | |
| 421 | + const val = items - deduct | |
| 422 | + return (val < 0 ? 0 : val).toFixed(2) | |
| 423 | + }, | |
| 424 | + arrears() { | |
| 425 | + const total = parseFloat(this.form.totalPerformance) || 0 | |
| 426 | + const paid = parseFloat(this.actualPaid) || 0 | |
| 427 | + const val = total - paid | |
| 428 | + return (val < 0 ? 0 : val).toFixed(2) | |
| 429 | + } | |
| 430 | + }, | |
| 431 | + watch: { | |
| 432 | + visible(val) { | |
| 433 | + if (val) { | |
| 434 | + this.applyPrefill() | |
| 435 | + } | |
| 436 | + }, | |
| 437 | + prefill: { | |
| 438 | + deep: true, | |
| 439 | + handler() { | |
| 440 | + if (this.visible) this.applyPrefill() | |
| 441 | + } | |
| 442 | + } | |
| 443 | + }, | |
| 444 | + methods: { | |
| 445 | + createEmptyForm() { | |
| 446 | + return { | |
| 447 | + activityId: '', | |
| 448 | + memberId: '', | |
| 449 | + memberName: '', | |
| 450 | + billingDate: new Date(), | |
| 451 | + totalPerformance: '', | |
| 452 | + paymentMethod: '微信', | |
| 453 | + cooperateOrg: '', | |
| 454 | + settleOrg: '', | |
| 455 | + isFirstOrder: '否', | |
| 456 | + isDeduct: '否', | |
| 457 | + intro: '', | |
| 458 | + remark: '', | |
| 459 | + receiptFiles: [], | |
| 460 | + otherFiles: [], | |
| 461 | + items: [this.createEmptyItem()], | |
| 462 | + deductItems: [this.createEmptyDeduct()] | |
| 463 | + } | |
| 464 | + }, | |
| 465 | + createEmptyItem() { | |
| 466 | + return { | |
| 467 | + projectId: '', | |
| 468 | + totalPrice: '', | |
| 469 | + quantity: 1, | |
| 470 | + type: '购买', | |
| 471 | + remark: '', | |
| 472 | + workers: [{ workerId: '', amount: '' }] | |
| 473 | + } | |
| 474 | + }, | |
| 475 | + createEmptyDeduct() { | |
| 476 | + return { deductId: '', price: '', quantity: 1, maxQty: 999 } | |
| 477 | + }, | |
| 478 | + applyPrefill() { | |
| 479 | + this.form = this.createEmptyForm() | |
| 480 | + if (this.prefill) { | |
| 481 | + if (this.prefill.memberId) { | |
| 482 | + this.form.memberId = this.prefill.memberId | |
| 483 | + this.form.memberName = this.prefill.name || '' | |
| 484 | + } else if (this.prefill.name) { | |
| 485 | + const found = this.memberOptions.find(m => m.label === this.prefill.name) | |
| 486 | + if (found) { | |
| 487 | + this.form.memberId = found.value | |
| 488 | + this.form.memberName = found.label | |
| 489 | + } | |
| 490 | + } | |
| 491 | + } | |
| 492 | + this.$nextTick(() => { | |
| 493 | + this.$refs.form && this.$refs.form.clearValidate() | |
| 494 | + }) | |
| 495 | + }, | |
| 496 | + onMemberChange(val) { | |
| 497 | + const m = this.memberOptions.find(o => o.value === val) | |
| 498 | + this.form.memberName = m ? m.label : '' | |
| 499 | + }, | |
| 500 | + onProjectChange(idx) { | |
| 501 | + const item = this.form.items[idx] | |
| 502 | + const p = this.projectOptions.find(o => o.value === item.projectId) | |
| 503 | + if (p) { | |
| 504 | + item.totalPrice = String(p.price * item.quantity) | |
| 505 | + } | |
| 506 | + }, | |
| 507 | + onDeductChange(di) { | |
| 508 | + const d = this.form.deductItems[di] | |
| 509 | + const found = this.deductOptions.find(o => o.value === d.deductId) | |
| 510 | + if (found) { | |
| 511 | + d.price = found.price | |
| 512 | + d.maxQty = found.remaining | |
| 513 | + if (d.quantity > found.remaining) d.quantity = found.remaining | |
| 514 | + } | |
| 515 | + }, | |
| 516 | + itemUnitPrice(item) { | |
| 517 | + const qty = item.quantity || 1 | |
| 518 | + const total = parseFloat(item.totalPrice) || 0 | |
| 519 | + return qty > 0 ? (total / qty).toFixed(2) : '0.00' | |
| 520 | + }, | |
| 521 | + deductSubtotal(d) { | |
| 522 | + return ((parseFloat(d.price) || 0) * (d.quantity || 0)).toFixed(2) | |
| 523 | + }, | |
| 524 | + addItem() { | |
| 525 | + this.form.items.push(this.createEmptyItem()) | |
| 526 | + }, | |
| 527 | + removeItem(idx) { | |
| 528 | + this.form.items.splice(idx, 1) | |
| 529 | + }, | |
| 530 | + addWorker(idx) { | |
| 531 | + this.form.items[idx].workers.push({ workerId: '', amount: '' }) | |
| 532 | + }, | |
| 533 | + removeWorker(idx, wi) { | |
| 534 | + this.form.items[idx].workers.splice(wi, 1) | |
| 535 | + }, | |
| 536 | + addDeductItem() { | |
| 537 | + this.form.deductItems.push(this.createEmptyDeduct()) | |
| 538 | + }, | |
| 539 | + removeDeductItem(di) { | |
| 540 | + this.form.deductItems.splice(di, 1) | |
| 541 | + }, | |
| 542 | + resetForm() { | |
| 543 | + this.form = this.createEmptyForm() | |
| 544 | + this.$nextTick(() => { | |
| 545 | + this.$refs.form && this.$refs.form.clearValidate() | |
| 546 | + }) | |
| 547 | + }, | |
| 548 | + handleCancel() { | |
| 549 | + this.visibleProxy = false | |
| 550 | + this.resetForm() | |
| 551 | + }, | |
| 552 | + handleSubmit() { | |
| 553 | + this.$refs.form.validate(valid => { | |
| 554 | + if (!valid) return | |
| 555 | + this.submitting = true | |
| 556 | + setTimeout(() => { | |
| 557 | + this.submitting = false | |
| 558 | + this.$message.success('开单已保存(示例)') | |
| 559 | + this.$emit('submitted', this.form) | |
| 560 | + this.visibleProxy = false | |
| 561 | + this.resetForm() | |
| 562 | + }, 800) | |
| 563 | + }) | |
| 564 | + } | |
| 565 | + } | |
| 566 | +} | |
| 567 | +</script> | |
| 568 | + | |
| 569 | +<style lang="scss" scoped> | |
| 570 | +/* ====== 弹窗外壳(统一风格) ====== */ | |
| 571 | +::v-deep .billing-dialog { | |
| 572 | + max-width: 1200px; | |
| 573 | + margin-top: 5vh !important; | |
| 574 | + border-radius: 20px; | |
| 575 | + padding: 0; | |
| 576 | + background: radial-gradient( | |
| 577 | + circle at 0 0, | |
| 578 | + rgba(255, 255, 255, 0.96) 0, | |
| 579 | + rgba(248, 250, 252, 0.98) 40%, | |
| 580 | + rgba(241, 245, 249, 0.98) 100% | |
| 581 | + ); | |
| 582 | + box-shadow: | |
| 583 | + 0 24px 48px rgba(15, 23, 42, 0.18), | |
| 584 | + 0 0 0 1px rgba(255, 255, 255, 0.9); | |
| 585 | + backdrop-filter: blur(22px); | |
| 586 | + -webkit-backdrop-filter: blur(22px); | |
| 587 | +} | |
| 588 | + | |
| 589 | +::v-deep .el-dialog__header { | |
| 590 | + display: none; | |
| 591 | +} | |
| 592 | + | |
| 593 | +::v-deep .el-dialog__body { | |
| 594 | + padding: 0; | |
| 595 | +} | |
| 596 | + | |
| 597 | +/* ====== 内部结构 ====== */ | |
| 598 | +.billing-dialog-inner { | |
| 599 | + display: flex; | |
| 600 | + flex-direction: column; | |
| 601 | + max-height: 85vh; | |
| 602 | +} | |
| 603 | + | |
| 604 | +.billing-header { | |
| 605 | + flex-shrink: 0; | |
| 606 | + display: flex; | |
| 607 | + align-items: center; | |
| 608 | + gap: 8px; | |
| 609 | + margin: 18px 22px 0; | |
| 610 | + padding: 10px 14px; | |
| 611 | + border-radius: 14px; | |
| 612 | + background: rgba(219, 234, 254, 0.96); | |
| 613 | +} | |
| 614 | + | |
| 615 | +.billing-title-wrap { | |
| 616 | + flex: 1; | |
| 617 | +} | |
| 618 | + | |
| 619 | +.billing-title { | |
| 620 | + font-size: 17px; | |
| 621 | + font-weight: 600; | |
| 622 | + color: #0f172a; | |
| 623 | +} | |
| 624 | + | |
| 625 | +.billing-subtitle { | |
| 626 | + font-size: 12px; | |
| 627 | + color: #475569; | |
| 628 | + margin-top: 2px; | |
| 629 | +} | |
| 630 | + | |
| 631 | +.booking-source-tag { | |
| 632 | + display: flex; | |
| 633 | + align-items: center; | |
| 634 | + gap: 6px; | |
| 635 | + margin-left: auto; | |
| 636 | + margin-right: 12px; | |
| 637 | + padding: 4px 14px; | |
| 638 | + border-radius: 999px; | |
| 639 | + background: rgba(249, 115, 22, 0.12); | |
| 640 | + font-size: 12px; | |
| 641 | + color: #ea580c; | |
| 642 | + font-weight: 500; | |
| 643 | + white-space: nowrap; | |
| 644 | + | |
| 645 | + i { | |
| 646 | + font-size: 13px; | |
| 647 | + } | |
| 648 | + | |
| 649 | + .booking-source-detail { | |
| 650 | + color: #9a3412; | |
| 651 | + font-weight: 400; | |
| 652 | + } | |
| 653 | +} | |
| 654 | + | |
| 655 | +.billing-close { | |
| 656 | + flex-shrink: 0; | |
| 657 | + cursor: pointer; | |
| 658 | + width: 28px; | |
| 659 | + height: 28px; | |
| 660 | + display: flex; | |
| 661 | + align-items: center; | |
| 662 | + justify-content: center; | |
| 663 | + border-radius: 999px; | |
| 664 | + color: #64748b; | |
| 665 | + transition: all 0.15s; | |
| 666 | + | |
| 667 | + &:hover { | |
| 668 | + background: rgba(0, 0, 0, 0.06); | |
| 669 | + color: #0f172a; | |
| 670 | + } | |
| 671 | +} | |
| 672 | + | |
| 673 | +/* ====== 双栏主体 ====== */ | |
| 674 | +.billing-content { | |
| 675 | + flex: 1; | |
| 676 | + min-height: 0; | |
| 677 | + overflow: hidden; | |
| 678 | + display: flex; | |
| 679 | +} | |
| 680 | + | |
| 681 | +.billing-form { | |
| 682 | + display: flex; | |
| 683 | + flex: 1; | |
| 684 | + min-height: 0; | |
| 685 | +} | |
| 686 | + | |
| 687 | +.billing-left { | |
| 688 | + flex: 0 0 440px; | |
| 689 | + overflow-y: auto; | |
| 690 | + padding: 10px 16px 10px 22px; | |
| 691 | + border-right: 1px solid rgba(229, 231, 235, 0.6); | |
| 692 | + min-height: 0; | |
| 693 | +} | |
| 694 | + | |
| 695 | +.billing-right { | |
| 696 | + flex: 1; | |
| 697 | + overflow-y: auto; | |
| 698 | + padding: 10px 22px 10px 16px; | |
| 699 | + min-height: 0; | |
| 700 | +} | |
| 701 | + | |
| 702 | +/* ====== 分区标题 ====== */ | |
| 703 | +.section-title { | |
| 704 | + font-size: 13px; | |
| 705 | + font-weight: 600; | |
| 706 | + color: #334155; | |
| 707 | + margin: 14px 0 8px; | |
| 708 | + padding: 6px 10px; | |
| 709 | + border-radius: 8px; | |
| 710 | + background: rgba(241, 245, 249, 0.7); | |
| 711 | + | |
| 712 | + i { | |
| 713 | + margin-right: 4px; | |
| 714 | + color: #2563eb; | |
| 715 | + } | |
| 716 | + | |
| 717 | + &:first-child { | |
| 718 | + margin-top: 4px; | |
| 719 | + } | |
| 720 | +} | |
| 721 | + | |
| 722 | +/* ====== 品项 / 储扣卡片 ====== */ | |
| 723 | +.item-card { | |
| 724 | + border: 1px solid #e5e7eb; | |
| 725 | + border-radius: 12px; | |
| 726 | + padding: 10px 12px 4px; | |
| 727 | + margin-bottom: 10px; | |
| 728 | + background: rgba(255, 255, 255, 0.6); | |
| 729 | + transition: border-color 0.15s; | |
| 730 | + | |
| 731 | + &:hover { | |
| 732 | + border-color: #93c5fd; | |
| 733 | + } | |
| 734 | +} | |
| 735 | + | |
| 736 | +.deduct-card { | |
| 737 | + border-color: #fde68a; | |
| 738 | + | |
| 739 | + &:hover { | |
| 740 | + border-color: #fbbf24; | |
| 741 | + } | |
| 742 | +} | |
| 743 | + | |
| 744 | +.item-card-head { | |
| 745 | + display: flex; | |
| 746 | + align-items: center; | |
| 747 | + justify-content: space-between; | |
| 748 | + margin-bottom: 6px; | |
| 749 | +} | |
| 750 | + | |
| 751 | +.item-card-no { | |
| 752 | + font-size: 12px; | |
| 753 | + font-weight: 600; | |
| 754 | + color: #2563eb; | |
| 755 | +} | |
| 756 | + | |
| 757 | +.item-remove-btn { | |
| 758 | + color: #ef4444 !important; | |
| 759 | + font-size: 12px; | |
| 760 | + padding: 0; | |
| 761 | +} | |
| 762 | + | |
| 763 | +/* ====== 健康师行 ====== */ | |
| 764 | +.worker-section { | |
| 765 | + margin: 2px 0 6px; | |
| 766 | + padding: 8px 10px; | |
| 767 | + border-radius: 8px; | |
| 768 | + background: rgba(241, 245, 249, 0.5); | |
| 769 | +} | |
| 770 | + | |
| 771 | +.worker-label { | |
| 772 | + display: flex; | |
| 773 | + align-items: center; | |
| 774 | + justify-content: space-between; | |
| 775 | + margin-bottom: 6px; | |
| 776 | + font-size: 12px; | |
| 777 | + color: #475569; | |
| 778 | + font-weight: 500; | |
| 779 | +} | |
| 780 | + | |
| 781 | +.worker-row { | |
| 782 | + display: flex; | |
| 783 | + align-items: center; | |
| 784 | + gap: 8px; | |
| 785 | + margin-bottom: 6px; | |
| 786 | +} | |
| 787 | + | |
| 788 | +.worker-select { | |
| 789 | + flex: 1; | |
| 790 | +} | |
| 791 | + | |
| 792 | +.worker-amount { | |
| 793 | + width: 140px; | |
| 794 | + flex-shrink: 0; | |
| 795 | +} | |
| 796 | + | |
| 797 | +.worker-remove { | |
| 798 | + color: #ef4444 !important; | |
| 799 | + padding: 0; | |
| 800 | +} | |
| 801 | + | |
| 802 | +/* ====== 添加按钮行 ====== */ | |
| 803 | +.add-btn-row { | |
| 804 | + text-align: center; | |
| 805 | + margin-bottom: 6px; | |
| 806 | +} | |
| 807 | + | |
| 808 | +/* ====== 上传提示 ====== */ | |
| 809 | +.upload-tip { | |
| 810 | + font-size: 11px; | |
| 811 | + color: #9ca3af; | |
| 812 | + margin-left: 8px; | |
| 813 | +} | |
| 814 | + | |
| 815 | +/* ====== footer 固定底部 ====== */ | |
| 816 | +.billing-footer { | |
| 817 | + flex-shrink: 0; | |
| 818 | + display: flex; | |
| 819 | + align-items: center; | |
| 820 | + justify-content: space-between; | |
| 821 | + padding: 10px 22px 14px; | |
| 822 | + border-top: 1px solid rgba(229, 231, 235, 0.6); | |
| 823 | +} | |
| 824 | + | |
| 825 | +.footer-summary { | |
| 826 | + display: flex; | |
| 827 | + gap: 16px; | |
| 828 | + font-size: 12px; | |
| 829 | + color: #64748b; | |
| 830 | + | |
| 831 | + b { | |
| 832 | + color: #0f172a; | |
| 833 | + } | |
| 834 | + | |
| 835 | + .highlight { | |
| 836 | + color: #2563eb; | |
| 837 | + font-size: 14px; | |
| 838 | + } | |
| 839 | +} | |
| 840 | + | |
| 841 | +.footer-actions { | |
| 842 | + display: flex; | |
| 843 | + gap: 10px; | |
| 844 | +} | |
| 845 | + | |
| 846 | +/* ====== 统一输入框 / 按钮风格 ====== */ | |
| 847 | +::v-deep .billing-dialog .el-form-item__label { | |
| 848 | + white-space: nowrap; | |
| 849 | +} | |
| 850 | + | |
| 851 | +::v-deep .billing-dialog .el-input__inner { | |
| 852 | + border-radius: 999px; | |
| 853 | + height: 32px; | |
| 854 | + line-height: 32px; | |
| 855 | + border-color: #e5e7eb; | |
| 856 | + background-color: #f9fafb; | |
| 857 | +} | |
| 858 | + | |
| 859 | +::v-deep .billing-dialog .el-input__inner:focus { | |
| 860 | + border-color: #2563eb; | |
| 861 | + box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.18); | |
| 862 | +} | |
| 863 | + | |
| 864 | +::v-deep .billing-dialog .el-input-group__prepend { | |
| 865 | + border-radius: 999px 0 0 999px; | |
| 866 | + background: #f1f5f9; | |
| 867 | + border-color: #e5e7eb; | |
| 868 | + padding: 0 10px; | |
| 869 | + color: #64748b; | |
| 870 | +} | |
| 871 | + | |
| 872 | +::v-deep .billing-dialog .el-input-group .el-input__inner { | |
| 873 | + border-radius: 0 999px 999px 0; | |
| 874 | +} | |
| 875 | + | |
| 876 | +::v-deep .billing-dialog .el-textarea__inner { | |
| 877 | + border-radius: 12px; | |
| 878 | + border-color: #e5e7eb; | |
| 879 | + background-color: #f9fafb; | |
| 880 | +} | |
| 881 | + | |
| 882 | +::v-deep .billing-dialog .el-textarea__inner:focus { | |
| 883 | + border-color: #2563eb; | |
| 884 | + box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.18); | |
| 885 | +} | |
| 886 | + | |
| 887 | +::v-deep .billing-dialog .el-input-number { | |
| 888 | + .el-input__inner { | |
| 889 | + border-radius: 999px; | |
| 890 | + } | |
| 891 | +} | |
| 892 | + | |
| 893 | +::v-deep .billing-dialog .el-upload-list__item { | |
| 894 | + border-radius: 8px; | |
| 895 | +} | |
| 896 | + | |
| 897 | +::v-deep .billing-dialog .el-button--primary { | |
| 898 | + border-radius: 999px; | |
| 899 | + padding: 0 20px; | |
| 900 | + height: 30px; | |
| 901 | + line-height: 30px; | |
| 902 | + background: #2563eb; | |
| 903 | + border-color: #2563eb; | |
| 904 | + box-shadow: 0 4px 10px rgba(37, 99, 235, 0.35); | |
| 905 | + font-size: 12px; | |
| 906 | +} | |
| 907 | + | |
| 908 | +::v-deep .billing-dialog .el-button--primary.is-disabled { | |
| 909 | + box-shadow: none; | |
| 910 | +} | |
| 911 | + | |
| 912 | +::v-deep .billing-dialog .el-button--default { | |
| 913 | + border-radius: 999px; | |
| 914 | + padding: 0 18px; | |
| 915 | + height: 30px; | |
| 916 | + line-height: 30px; | |
| 917 | + background: rgba(239, 246, 255, 0.9); | |
| 918 | + color: #2563eb; | |
| 919 | + border-color: rgba(37, 99, 235, 0.18); | |
| 920 | + font-size: 12px; | |
| 921 | +} | |
| 922 | + | |
| 923 | +::v-deep .billing-dialog .el-button--default:hover { | |
| 924 | + background: rgba(219, 234, 254, 0.95); | |
| 925 | + color: #1d4ed8; | |
| 926 | +} | |
| 927 | + | |
| 928 | +::v-deep .billing-dialog .el-radio__input.is-checked .el-radio__inner { | |
| 929 | + border-color: #2563eb; | |
| 930 | + background: #2563eb; | |
| 931 | +} | |
| 932 | + | |
| 933 | +::v-deep .billing-dialog .el-radio__input.is-checked + .el-radio__label { | |
| 934 | + color: #2563eb; | |
| 935 | +} | |
| 936 | + | |
| 937 | +::v-deep .billing-dialog .el-form-item { | |
| 938 | + margin-bottom: 12px; | |
| 939 | +} | |
| 940 | + | |
| 941 | +::v-deep .billing-dialog .el-picker-panel { | |
| 942 | + border-radius: 12px; | |
| 943 | +} | |
| 944 | + | |
| 945 | +::v-deep .billing-dialog .el-select { | |
| 946 | + width: 100%; | |
| 947 | +} | |
| 948 | +</style> | ... | ... |
store-pc/src/components/BillingListDialog.vue
0 → 100644
| 1 | +<template> | |
| 2 | + <el-dialog | |
| 3 | + :visible.sync="visibleProxy" | |
| 4 | + :show-close="false" | |
| 5 | + width="90%" | |
| 6 | + :close-on-click-modal="false" | |
| 7 | + custom-class="billing-list-dialog" | |
| 8 | + append-to-body | |
| 9 | + > | |
| 10 | + <div class="dialog-inner"> | |
| 11 | + <div class="dialog-header"> | |
| 12 | + <div class="dialog-title">开单记录</div> | |
| 13 | + <span class="dialog-close" @click="visibleProxy = false"><i class="el-icon-close"></i></span> | |
| 14 | + </div> | |
| 15 | + | |
| 16 | + <div class="dialog-search"> | |
| 17 | + <el-form @submit.native.prevent :inline="true" size="small"> | |
| 18 | + <el-form-item label="开单日期"> | |
| 19 | + <el-date-picker v-model="query.kdrq" type="daterange" value-format="timestamp" format="yyyy-MM-dd" start-placeholder="开始" end-placeholder="结束" style="width:220px" /> | |
| 20 | + </el-form-item> | |
| 21 | + <el-form-item label="开单会员"> | |
| 22 | + <el-select v-model="query.kdhy" filterable remote reserve-keyword clearable placeholder="搜索会员" :remote-method="searchMember" :loading="memberLoading" style="width:200px"> | |
| 23 | + <el-option v-for="m in memberOptions" :key="m.value" :label="m.label" :value="m.value" /> | |
| 24 | + </el-select> | |
| 25 | + </el-form-item> | |
| 26 | + <el-form-item label="活动名称"> | |
| 27 | + <el-select v-model="query.activityId" placeholder="请选择活动" filterable clearable :loading="activityLoading" @visible-change="loadActivities" style="width:180px"> | |
| 28 | + <el-option v-for="a in activityOptions" :key="a.id" :label="a.activityName" :value="a.id" /> | |
| 29 | + </el-select> | |
| 30 | + </el-form-item> | |
| 31 | + <template v-if="showAll"> | |
| 32 | + <el-form-item label="健康师"> | |
| 33 | + <el-select v-model="query.jksId" placeholder="健康师" clearable filterable style="width:150px"> | |
| 34 | + <el-option v-for="h in jksOptions" :key="h.id" :label="h.fullName" :value="h.id" /> | |
| 35 | + </el-select> | |
| 36 | + </el-form-item> | |
| 37 | + <el-form-item label="科技老师"> | |
| 38 | + <el-select v-model="query.kjblsId" placeholder="科技老师" clearable filterable style="width:150px"> | |
| 39 | + <el-option v-for="t in kjbOptions" :key="t.id" :label="t.fullName" :value="t.id" /> | |
| 40 | + </el-select> | |
| 41 | + </el-form-item> | |
| 42 | + <el-form-item label="付款方式"> | |
| 43 | + <el-select v-model="query.fkfs" placeholder="付款方式" clearable style="width:120px"> | |
| 44 | + <el-option v-for="p in payOptions" :key="p" :label="p" :value="p" /> | |
| 45 | + </el-select> | |
| 46 | + </el-form-item> | |
| 47 | + <el-form-item label="是否首开"> | |
| 48 | + <el-select v-model="query.sfskdd" placeholder="请选择" clearable style="width:100px"> | |
| 49 | + <el-option label="是" value="是" /><el-option label="否" value="否" /> | |
| 50 | + </el-select> | |
| 51 | + </el-form-item> | |
| 52 | + <el-form-item label="是否作废"> | |
| 53 | + <el-select v-model="query.isEffective" placeholder="请选择" clearable style="width:100px"> | |
| 54 | + <el-option label="正常" value="1" /><el-option label="作废" value="-1" /> | |
| 55 | + </el-select> | |
| 56 | + </el-form-item> | |
| 57 | + </template> | |
| 58 | + <el-form-item> | |
| 59 | + <el-button type="primary" @click="search">查询</el-button> | |
| 60 | + <el-button @click="reset">重置</el-button> | |
| 61 | + <el-button type="text" @click="showAll = !showAll"> | |
| 62 | + {{ showAll ? '收起' : '展开' }} <i :class="showAll ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"></i> | |
| 63 | + </el-button> | |
| 64 | + </el-form-item> | |
| 65 | + </el-form> | |
| 66 | + </div> | |
| 67 | + | |
| 68 | + <div class="dialog-content"> | |
| 69 | + <el-table v-loading="loading" :data="list" border size="small" :header-cell-style="{ background:'#f5f7fa', color:'#606266', fontWeight:600 }"> | |
| 70 | + <el-table-column type="expand" width="50"> | |
| 71 | + <template slot-scope="{ row }"> | |
| 72 | + <div class="expand-box" v-if="row.ItemDetails && row.ItemDetails.length"> | |
| 73 | + <div class="expand-title"><i class="el-icon-goods"></i> 品项明细</div> | |
| 74 | + <el-table :data="row.ItemDetails" border size="mini" :header-cell-style="{ background:'#f5f7fa', color:'#606266' }"> | |
| 75 | + <el-table-column prop="pxmc" label="项目名称" width="180" /> | |
| 76 | + <el-table-column label="项目价格" width="120" align="right"> | |
| 77 | + <template slot-scope="s">¥{{ formatMoney(s.row.pxjg) }}</template> | |
| 78 | + </el-table-column> | |
| 79 | + <el-table-column prop="projectNumber" label="次数" width="80" align="right" /> | |
| 80 | + <el-table-column label="总价" width="120" align="right"> | |
| 81 | + <template slot-scope="s">¥{{ formatMoney(s.row.totalPrice) }}</template> | |
| 82 | + </el-table-column> | |
| 83 | + <el-table-column label="实付" width="120" align="right"> | |
| 84 | + <template slot-scope="s"><span style="color:#67C23A;font-weight:600">¥{{ formatMoney(s.row.actualPrice) }}</span></template> | |
| 85 | + </el-table-column> | |
| 86 | + <el-table-column label="来源" width="100"> | |
| 87 | + <template slot-scope="s"><el-tag size="mini" type="info">{{ s.row.sourceType || '-' }}</el-tag></template> | |
| 88 | + </el-table-column> | |
| 89 | + <el-table-column prop="remark" label="备注" show-overflow-tooltip /> | |
| 90 | + </el-table> | |
| 91 | + </div> | |
| 92 | + <div v-else class="expand-empty"><i class="el-icon-info"></i> 暂无品项明细</div> | |
| 93 | + </template> | |
| 94 | + </el-table-column> | |
| 95 | + <el-table-column prop="kdhyc" label="开单会员" width="100" show-overflow-tooltip /> | |
| 96 | + <el-table-column prop="kdhysjh" label="会员手机号" width="120" /> | |
| 97 | + <el-table-column prop="activityName" label="活动名称" width="140" show-overflow-tooltip /> | |
| 98 | + <el-table-column label="开单日期" width="110"> | |
| 99 | + <template slot-scope="{ row }">{{ formatDate(row.kdrq) }}</template> | |
| 100 | + </el-table-column> | |
| 101 | + <el-table-column prop="zdyj" label="整单业绩" width="100" align="right" /> | |
| 102 | + <el-table-column prop="sfyj" label="实付业绩" width="100" align="right" /> | |
| 103 | + <el-table-column prop="deductAmount" label="储扣金额" width="100" align="right" /> | |
| 104 | + <el-table-column prop="qk" label="欠款" width="80" align="right" /> | |
| 105 | + <el-table-column label="付款方式" width="100"> | |
| 106 | + <template slot-scope="{ row }">{{ mapOption(row.fkfs, payOptions) }}</template> | |
| 107 | + </el-table-column> | |
| 108 | + <el-table-column label="是否首开" width="90"> | |
| 109 | + <template slot-scope="{ row }">{{ row.sfskdd || '-' }}</template> | |
| 110 | + </el-table-column> | |
| 111 | + <el-table-column label="是否作废" width="90"> | |
| 112 | + <template slot-scope="{ row }">{{ row.isEffective == '1' ? '正常' : '作废' }}</template> | |
| 113 | + </el-table-column> | |
| 114 | + <el-table-column label="操作人" width="120" show-overflow-tooltip> | |
| 115 | + <template slot-scope="{ row }">{{ row.createUserName || '-' }}</template> | |
| 116 | + </el-table-column> | |
| 117 | + </el-table> | |
| 118 | + </div> | |
| 119 | + | |
| 120 | + <div class="dialog-footer"> | |
| 121 | + <el-pagination background layout="total, sizes, prev, pager, next, jumper" :total="total" :page-size.sync="listQuery.pageSize" :current-page.sync="listQuery.currentPage" :page-sizes="[10, 20, 50, 100]" @size-change="initData" @current-change="initData" /> | |
| 122 | + </div> | |
| 123 | + </div> | |
| 124 | + </el-dialog> | |
| 125 | +</template> | |
| 126 | + | |
| 127 | +<script> | |
| 128 | +const MOCK_BILLING = [ | |
| 129 | + { id: '1', djmdName: '千禧', kdhyc: '杨瑶', kdhysjh: '13516846588', activityName: '', kdrq: '2026-03-02 17:32:04', zdyj: '66.67', sfyj: '0.00', deductAmount: '66.67', qk: '0.00', fkfs: '现金', sfskdd: '否', isEffective: 1, createUserName: '郑巧玲', jksName: '黄泸娇', kjbName: '科技二部T区', ItemDetails: [{ pxmc: '胶原宝宝-单部位', pxjg: '66.67', projectNumber: '1', totalPrice: '66.67', actualPrice: '66.67', sourceType: '购买', remark: '' }] }, | |
| 130 | + { id: '2', djmdName: '红光', kdhyc: '张国菊', kdhysjh: '13893923967', activityName: '', kdrq: '2026-02-11 22:11:30', zdyj: '0.00', sfyj: '0.00', deductAmount: '0.00', qk: '0.00', fkfs: '现金', sfskdd: '否', isEffective: 1, createUserName: '马丽亚', jksName: '马丽亚', kjbName: '', ItemDetails: [] }, | |
| 131 | + { id: '3', djmdName: '南湖', kdhyc: '林小芊', kdhysjh: '15902827650', activityName: '', kdrq: '2026-02-11 21:17:58', zdyj: '0.00', sfyj: '0.00', deductAmount: '0.00', qk: '0.00', fkfs: '现金', sfskdd: '否', isEffective: 1, createUserName: '郝莉娜', jksName: '郝莉娜', kjbName: '', ItemDetails: [] }, | |
| 132 | + { id: '4', djmdName: '中和', kdhyc: '卢华瑜', kdhysjh: '13882365601', activityName: '', kdrq: '2026-02-11 19:30:27', zdyj: '1000.00', sfyj: '500.00', deductAmount: '0.00', qk: '500.00', fkfs: '微信', sfskdd: '否', isEffective: 1, createUserName: '贺慧', jksName: '李红梅,余姣姣', kjbName: '', ItemDetails: [{ pxmc: '美容套卡', pxjg: '1000.00', projectNumber: '1', totalPrice: '1000.00', actualPrice: '500.00', sourceType: '购买', remark: '' }] }, | |
| 133 | + { id: '5', djmdName: '沙河', kdhyc: '胡晗阳', kdhysjh: '13687006033', activityName: '', kdrq: '2026-02-11 19:19:51', zdyj: '1000.00', sfyj: '1000.00', deductAmount: '0.00', qk: '0.00', fkfs: '微信', sfskdd: '否', isEffective: 1, createUserName: '向郑瑶', jksName: '杨宜佳', kjbName: '', ItemDetails: [{ pxmc: '季卡', pxjg: '1000.00', projectNumber: '1', totalPrice: '1000.00', actualPrice: '1000.00', sourceType: '购买', remark: '' }] }, | |
| 134 | + { id: '6', djmdName: '荣华北路', kdhyc: '何雪梅', kdhysjh: '13648005825', activityName: '', kdrq: '2026-02-11 18:51:41', zdyj: '19.90', sfyj: '19.90', deductAmount: '0.00', qk: '0.00', fkfs: '现金', sfskdd: '是', isEffective: 1, createUserName: '谢娟', jksName: '荣华北路T区', kjbName: '', ItemDetails: [{ pxmc: '体验卡', pxjg: '19.90', projectNumber: '1', totalPrice: '19.90', actualPrice: '19.90', sourceType: '购买', remark: '' }] }, | |
| 135 | + { id: '7', djmdName: '南湖', kdhyc: '吴敏', kdhysjh: '13882088033', activityName: '', kdrq: '2026-02-11 18:19:13', zdyj: '66.60', sfyj: '0.00', deductAmount: '66.60', qk: '0.00', fkfs: '现金', sfskdd: '否', isEffective: 1, createUserName: '刘九招', jksName: '南湖T区', kjbName: '', ItemDetails: [{ pxmc: '胶原宝宝-单部位', pxjg: '66.60', projectNumber: '1', totalPrice: '66.60', actualPrice: '66.60', sourceType: '购买', remark: '' }] }, | |
| 136 | + { id: '8', djmdName: '大源', kdhyc: '李雪', kdhysjh: '15008224185', activityName: '', kdrq: '2026-02-11 18:18:01', zdyj: '1000.00', sfyj: '1000.00', deductAmount: '0.00', qk: '0.00', fkfs: '支付宝', sfskdd: '否', isEffective: 1, createUserName: '张红霞', jksName: '钟秦', kjbName: '', ItemDetails: [{ pxmc: '美容套卡', pxjg: '1000.00', projectNumber: '1', totalPrice: '1000.00', actualPrice: '1000.00', sourceType: '购买', remark: '' }] } | |
| 137 | +] | |
| 138 | +export default { | |
| 139 | + name: 'BillingListDialog', | |
| 140 | + props: { visible: { type: Boolean, default: false } }, | |
| 141 | + data() { | |
| 142 | + return { | |
| 143 | + mockData: MOCK_BILLING, | |
| 144 | + showAll: false, loading: false, memberLoading: false, activityLoading: false, | |
| 145 | + memberOptions: [], activityOptions: [], activityLoaded: false, | |
| 146 | + jksOptions: [], kjbOptions: [], | |
| 147 | + list: [], total: 0, | |
| 148 | + query: { kdrq: undefined, kdhy: undefined, activityId: undefined, jksId: undefined, kjblsId: undefined, fkfs: undefined, sfskdd: undefined, isEffective: undefined }, | |
| 149 | + listQuery: { currentPage: 1, pageSize: 10, sort: 'desc', sidx: '' }, | |
| 150 | + payOptions: ['现金', '微信', '支付宝', '银行卡', '合作', '直播收款', '合作方退'] | |
| 151 | + } | |
| 152 | + }, | |
| 153 | + computed: { | |
| 154 | + visibleProxy: { get() { return this.visible }, set(v) { this.$emit('update:visible', v) } } | |
| 155 | + }, | |
| 156 | + watch: { | |
| 157 | + visible(v) { if (v) { this.initData(); this.loadMembers(); this.loadStaff() } } | |
| 158 | + }, | |
| 159 | + methods: { | |
| 160 | + formatDate(ts) { | |
| 161 | + if (!ts) return '-' | |
| 162 | + if (typeof ts === 'string' && ts.includes('-')) return ts.substring(0, 10) | |
| 163 | + const d = new Date(typeof ts === 'number' ? ts : Number(ts)) | |
| 164 | + if (isNaN(d.getTime())) return '-' | |
| 165 | + return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}` | |
| 166 | + }, | |
| 167 | + formatMoney(v) { return v != null ? Number(v).toFixed(2) : '0.00' }, | |
| 168 | + mapOption(val, opts) { return val || '-' }, | |
| 169 | + loadMembers() { | |
| 170 | + const seen = new Map() | |
| 171 | + this.mockData.forEach(r => { if (!seen.has(r.kdhysjh)) { seen.set(r.kdhysjh, true); this.memberOptions.push({ value: r.kdhysjh, label: `${r.kdhyc}(${r.kdhysjh})` }) } }) | |
| 172 | + }, | |
| 173 | + searchMember(kw) { | |
| 174 | + if (!kw) return | |
| 175 | + this.memberLoading = true | |
| 176 | + setTimeout(() => { | |
| 177 | + const lower = kw.toLowerCase() | |
| 178 | + this.memberOptions = this.mockData | |
| 179 | + .filter(r => r.kdhyc.toLowerCase().includes(lower) || r.kdhysjh.includes(kw)) | |
| 180 | + .reduce((acc, r) => { if (!acc.find(a => a.value === r.kdhysjh)) acc.push({ value: r.kdhysjh, label: `${r.kdhyc}(${r.kdhysjh})` }); return acc }, []) | |
| 181 | + this.memberLoading = false | |
| 182 | + }, 300) | |
| 183 | + }, | |
| 184 | + loadActivities(v) { | |
| 185 | + if (!v || this.activityLoaded) return | |
| 186 | + this.activityLoading = true | |
| 187 | + setTimeout(() => { this.activityLoaded = true; this.activityOptions = []; this.activityLoading = false }, 300) | |
| 188 | + }, | |
| 189 | + loadStaff() { | |
| 190 | + const jksSet = new Map(), kjbSet = new Map() | |
| 191 | + this.mockData.forEach(r => { | |
| 192 | + if (r.jksName) r.jksName.split(',').forEach(n => { const name = n.trim(); if (name && !jksSet.has(name)) { jksSet.set(name, true); this.jksOptions.push({ id: name, fullName: name }) } }) | |
| 193 | + if (r.kjbName) r.kjbName.split(',').forEach(n => { const name = n.trim(); if (name && !kjbSet.has(name)) { kjbSet.set(name, true); this.kjbOptions.push({ id: name, fullName: name }) } }) | |
| 194 | + }) | |
| 195 | + }, | |
| 196 | + initData() { | |
| 197 | + this.loading = true | |
| 198 | + setTimeout(() => { | |
| 199 | + let filtered = [...this.mockData] | |
| 200 | + if (this.query.kdrq && this.query.kdrq.length === 2) { | |
| 201 | + const [s, e] = this.query.kdrq | |
| 202 | + filtered = filtered.filter(r => { const t = new Date(r.kdrq).getTime(); return t >= s && t <= e + 86400000 }) | |
| 203 | + } | |
| 204 | + if (this.query.kdhy) filtered = filtered.filter(r => r.kdhysjh === this.query.kdhy) | |
| 205 | + if (this.query.jksId) filtered = filtered.filter(r => r.jksName && r.jksName.includes(this.query.jksId)) | |
| 206 | + if (this.query.kjblsId) filtered = filtered.filter(r => r.kjbName && r.kjbName.includes(this.query.kjblsId)) | |
| 207 | + if (this.query.fkfs) filtered = filtered.filter(r => r.fkfs === this.query.fkfs) | |
| 208 | + if (this.query.sfskdd) filtered = filtered.filter(r => r.sfskdd === this.query.sfskdd) | |
| 209 | + if (this.query.isEffective) filtered = filtered.filter(r => String(r.isEffective) === this.query.isEffective) | |
| 210 | + this.total = filtered.length | |
| 211 | + const start = (this.listQuery.currentPage - 1) * this.listQuery.pageSize | |
| 212 | + this.list = filtered.slice(start, start + this.listQuery.pageSize) | |
| 213 | + this.loading = false | |
| 214 | + }, 300) | |
| 215 | + }, | |
| 216 | + search() { this.listQuery.currentPage = 1; this.initData() }, | |
| 217 | + reset() { for (const k in this.query) this.query[k] = undefined; this.listQuery = { currentPage: 1, pageSize: 10, sort: 'desc', sidx: '' }; this.initData() } | |
| 218 | + } | |
| 219 | +} | |
| 220 | +</script> | |
| 221 | + | |
| 222 | +<style lang="scss" scoped> | |
| 223 | +::v-deep .billing-list-dialog { max-width: 1600px; margin-top: 3vh !important; border-radius: 20px; padding: 0; background: radial-gradient(circle at 0 0, rgba(255,255,255,0.96) 0, rgba(248,250,252,0.98) 40%, rgba(241,245,249,0.98) 100%); box-shadow: 0 24px 48px rgba(15,23,42,0.18), 0 0 0 1px rgba(255,255,255,0.9); backdrop-filter: blur(22px); -webkit-backdrop-filter: blur(22px); } | |
| 224 | +::v-deep .el-dialog__header { display: none; } | |
| 225 | +::v-deep .el-dialog__body { padding: 0; } | |
| 226 | +.dialog-inner { display: flex; flex-direction: column; max-height: 92vh; } | |
| 227 | +.dialog-header { flex-shrink: 0; display: flex; align-items: center; justify-content: space-between; margin: 18px 22px 0; padding: 10px 14px; border-radius: 14px; background: rgba(219,234,254,0.96); } | |
| 228 | +.dialog-title { font-size: 17px; font-weight: 600; color: #0f172a; } | |
| 229 | +.dialog-close { cursor: pointer; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border-radius: 999px; color: #64748b; transition: all 0.15s; &:hover { background: rgba(0,0,0,0.06); color: #0f172a; } } | |
| 230 | +.dialog-search { flex-shrink: 0; padding: 12px 22px 4px; } | |
| 231 | +.dialog-content { flex: 1; min-height: 0; overflow: auto; padding: 0 22px; } | |
| 232 | +.dialog-footer { flex-shrink: 0; display: flex; align-items: center; justify-content: flex-end; padding: 10px 22px 14px; border-top: 1px solid rgba(229,231,235,0.6); } | |
| 233 | +.expand-box { padding: 14px; background: #fafafa; border-radius: 8px; margin: 6px 0; .expand-title { display: flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 600; color: #303133; margin-bottom: 10px; i { color: #409EFF; } } } | |
| 234 | +.expand-empty { padding: 16px; text-align: center; color: #909399; font-size: 13px; i { margin-right: 4px; } } | |
| 235 | +::v-deep .billing-list-dialog .el-input__inner { border-radius: 999px; height: 32px; line-height: 32px; border-color: #e5e7eb; background-color: #f9fafb; &:focus { border-color: #2563eb; box-shadow: 0 0 0 1px rgba(37,99,235,0.18); } } | |
| 236 | +::v-deep .billing-list-dialog .el-button--primary { border-radius: 999px; background: #2563eb; border-color: #2563eb; box-shadow: 0 4px 10px rgba(37,99,235,0.35); } | |
| 237 | +::v-deep .billing-list-dialog .el-button--default { border-radius: 999px; } | |
| 238 | +::v-deep .billing-list-dialog .el-form-item { margin-bottom: 8px; } | |
| 239 | +::v-deep .billing-list-dialog .el-form-item__label { white-space: nowrap; } | |
| 240 | +::v-deep .billing-list-dialog .el-range-editor.el-input__inner { border-radius: 999px; } | |
| 241 | +</style> | ... | ... |
store-pc/src/components/BookingCalendarDialog.vue
0 → 100644
| 1 | +<template> | |
| 2 | + <el-dialog | |
| 3 | + :visible.sync="visibleProxy" | |
| 4 | + :show-close="false" | |
| 5 | + width="90%" | |
| 6 | + :close-on-click-modal="false" | |
| 7 | + custom-class="booking-calendar-dialog" | |
| 8 | + append-to-body | |
| 9 | + > | |
| 10 | + <div class="dialog-inner"> | |
| 11 | + <div class="dialog-header"> | |
| 12 | + <div class="dialog-title">预约日历</div> | |
| 13 | + <span class="dialog-close" @click="visibleProxy = false"><i class="el-icon-close"></i></span> | |
| 14 | + </div> | |
| 15 | + | |
| 16 | + <div class="dialog-search"> | |
| 17 | + <el-form @submit.native.prevent :inline="true" size="small"> | |
| 18 | + <el-form-item label="预约状态"> | |
| 19 | + <el-select v-model="query.F_Status" placeholder="预约状态" clearable style="width:140px"> | |
| 20 | + <el-option label="已确认" value="已确认" /> | |
| 21 | + <el-option label="已取消" value="已取消" /> | |
| 22 | + <el-option label="已预约" value="已预约" /> | |
| 23 | + </el-select> | |
| 24 | + </el-form-item> | |
| 25 | + <el-form-item> | |
| 26 | + <el-button type="primary" @click="search">查询</el-button> | |
| 27 | + <el-button @click="reset">重置</el-button> | |
| 28 | + </el-form-item> | |
| 29 | + </el-form> | |
| 30 | + </div> | |
| 31 | + | |
| 32 | + <div class="dialog-content" v-loading="loading"> | |
| 33 | + <FullCalendar | |
| 34 | + ref="fullCalendar" | |
| 35 | + class="store-calendar" | |
| 36 | + defaultView="dayGridMonth" | |
| 37 | + :header="calendarHeader" | |
| 38 | + :plugins="calendarPlugins" | |
| 39 | + :weekends="true" | |
| 40 | + :events="calendarEvents" | |
| 41 | + locale="zh-cn" | |
| 42 | + :buttonText="buttonText" | |
| 43 | + :height="calendarHeight" | |
| 44 | + :eventLimit="true" | |
| 45 | + allDayText="全天" | |
| 46 | + :editable="false" | |
| 47 | + @datesRender="datesRender" | |
| 48 | + /> | |
| 49 | + </div> | |
| 50 | + </div> | |
| 51 | + </el-dialog> | |
| 52 | +</template> | |
| 53 | + | |
| 54 | +<script> | |
| 55 | +import FullCalendar from '@fullcalendar/vue' | |
| 56 | +import dayGridPlugin from '@fullcalendar/daygrid' | |
| 57 | +import timeGridPlugin from '@fullcalendar/timegrid' | |
| 58 | +import interactionPlugin from '@fullcalendar/interaction' | |
| 59 | + | |
| 60 | +const MOCK_BOOKING = [ | |
| 61 | + { id: '1', storeName: '保利', yyrName: '贾琳', gkxm: '范佳佳', yysj: '2026-02-11T11:00:00', yyjs: '2026-02-11T11:30:00', F_Status: '已预约', yyjksName: '贾琳' }, | |
| 62 | + { id: '2', storeName: '保利', yyrName: '贾琳', gkxm: '黄仕碧', yysj: '2026-02-11T15:00:00', yyjs: '2026-02-11T15:30:00', F_Status: '已预约', yyjksName: '贾琳' }, | |
| 63 | + { id: '3', storeName: '保利', yyrName: '贾琳', gkxm: '赵丽', yysj: '2026-02-11T17:00:00', yyjs: '2026-02-11T17:30:00', F_Status: '已确认', yyjksName: '贾琳' }, | |
| 64 | + { id: '4', storeName: '468', yyrName: '刘恬恬', gkxm: '王英', yysj: '2026-02-11T14:00:00', yyjs: '2026-02-11T14:30:00', F_Status: '已预约', yyjksName: '刘恬恬' }, | |
| 65 | + { id: '5', storeName: '保利', yyrName: '贾琳', gkxm: '罗建琼', yysj: '2026-02-11T10:30:00', yyjs: '2026-02-11T11:00:00', F_Status: '已确认', yyjksName: '贾琳' }, | |
| 66 | + { id: '6', storeName: '保利', yyrName: '贾琳', gkxm: '沈丽', yysj: '2026-02-10T17:00:00', yyjs: '2026-02-10T17:30:00', F_Status: '已预约', yyjksName: '贾琳' }, | |
| 67 | + { id: '7', storeName: '静居寺', yyrName: '董顺秀', gkxm: '陈晴', yysj: '2026-02-11T09:30:00', yyjs: '2026-02-11T10:00:00', F_Status: '已取消', yyjksName: '董顺秀' }, | |
| 68 | + { id: '8', storeName: '静居寺', yyrName: '董顺秀', gkxm: '胡蝶', yysj: '2026-02-10T17:00:00', yyjs: '2026-02-10T17:30:00', F_Status: '已预约', yyjksName: '董顺秀' }, | |
| 69 | + { id: '9', storeName: '静居寺', yyrName: '董顺秀', gkxm: '魏海燕', yysj: '2026-02-10T14:00:00', yyjs: '2026-02-10T14:30:00', F_Status: '已确认', yyjksName: '董顺秀' }, | |
| 70 | + { id: '10', storeName: '静居寺', yyrName: '董顺秀', gkxm: '肖丛娇', yysj: '2026-02-10T11:00:00', yyjs: '2026-02-10T11:30:00', F_Status: '已预约', yyjksName: '董顺秀' } | |
| 71 | +] | |
| 72 | + | |
| 73 | +export default { | |
| 74 | + name: 'BookingCalendarDialog', | |
| 75 | + components: { FullCalendar }, | |
| 76 | + props: { visible: { type: Boolean, default: false } }, | |
| 77 | + data() { | |
| 78 | + return { | |
| 79 | + loading: false, | |
| 80 | + mockData: MOCK_BOOKING, | |
| 81 | + query: { F_Status: undefined }, | |
| 82 | + calendarPlugins: [dayGridPlugin, timeGridPlugin, interactionPlugin], | |
| 83 | + calendarEvents: [], | |
| 84 | + calendarHeader: { left: 'prev,next today', center: 'title', right: 'dayGridMonth,timeGridWeek,timeGridDay' }, | |
| 85 | + buttonText: { today: '今日', month: '月', week: '周', day: '日' }, | |
| 86 | + startTime: null, | |
| 87 | + endTime: null, | |
| 88 | + calendarHeight: 600 | |
| 89 | + } | |
| 90 | + }, | |
| 91 | + computed: { | |
| 92 | + visibleProxy: { | |
| 93 | + get() { return this.visible }, | |
| 94 | + set(v) { this.$emit('update:visible', v) } | |
| 95 | + } | |
| 96 | + }, | |
| 97 | + watch: { | |
| 98 | + visible(v) { | |
| 99 | + if (v) { | |
| 100 | + this.$nextTick(() => { this.calcHeight(); this.initData() }) | |
| 101 | + } | |
| 102 | + } | |
| 103 | + }, | |
| 104 | + mounted() { | |
| 105 | + window.addEventListener('resize', this.calcHeight) | |
| 106 | + }, | |
| 107 | + beforeDestroy() { | |
| 108 | + window.removeEventListener('resize', this.calcHeight) | |
| 109 | + }, | |
| 110 | + methods: { | |
| 111 | + calcHeight() { | |
| 112 | + this.$nextTick(() => { | |
| 113 | + const el = this.$el && this.$el.querySelector('.dialog-content') | |
| 114 | + if (el) { | |
| 115 | + this.calendarHeight = Math.max(el.clientHeight - 20, 500) | |
| 116 | + } | |
| 117 | + }) | |
| 118 | + }, | |
| 119 | + datesRender(info) { | |
| 120 | + const view = info.view | |
| 121 | + this.startTime = view.activeStart | |
| 122 | + this.endTime = view.activeEnd | |
| 123 | + this.initData() | |
| 124 | + }, | |
| 125 | + initData() { | |
| 126 | + this.loading = true | |
| 127 | + setTimeout(() => { | |
| 128 | + let filtered = [...this.mockData] | |
| 129 | + if (this.query.F_Status) filtered = filtered.filter(r => r.F_Status === this.query.F_Status) | |
| 130 | + if (this.startTime && this.endTime) { | |
| 131 | + filtered = filtered.filter(r => { | |
| 132 | + const t = new Date(r.yysj).getTime() | |
| 133 | + return t >= this.startTime.getTime() && t < this.endTime.getTime() | |
| 134 | + }) | |
| 135 | + } | |
| 136 | + this.calendarEvents = filtered.map(item => { | |
| 137 | + let color = '#409EFF' | |
| 138 | + if (item.F_Status === '已确认') color = '#67C23A' | |
| 139 | + else if (item.F_Status === '已取消') color = '#F56C6C' | |
| 140 | + let title = `${item.gkxm || '无'} - ${item.yyrName || '无'}` | |
| 141 | + if (item.yyjksName) title += ` (${item.yyjksName})` | |
| 142 | + return { | |
| 143 | + id: item.id, | |
| 144 | + title, | |
| 145 | + start: item.yysj ? new Date(item.yysj).toISOString() : new Date().toISOString(), | |
| 146 | + end: item.yyjs ? new Date(item.yyjs).toISOString() : new Date().toISOString(), | |
| 147 | + color, | |
| 148 | + editable: false, | |
| 149 | + allDay: false | |
| 150 | + } | |
| 151 | + }) | |
| 152 | + this.loading = false | |
| 153 | + }, 300) | |
| 154 | + }, | |
| 155 | + search() { this.initData() }, | |
| 156 | + reset() { this.query.F_Status = undefined; this.initData() } | |
| 157 | + } | |
| 158 | +} | |
| 159 | +</script> | |
| 160 | + | |
| 161 | +<style lang="scss"> | |
| 162 | +@import '~@fullcalendar/core/main.css'; | |
| 163 | +@import '~@fullcalendar/daygrid/main.css'; | |
| 164 | +@import '~@fullcalendar/timegrid/main.css'; | |
| 165 | + | |
| 166 | +.booking-calendar-dialog .store-calendar { | |
| 167 | + .fc-toolbar.fc-header-toolbar { padding: 16px 20px; margin-bottom: 0; border-bottom: 2px solid #f1f5f9; background: linear-gradient(135deg, rgba(59,130,246,0.02) 0%, rgba(96,165,250,0.02) 100%); } | |
| 168 | + .fc-toolbar-title { font-size: 18px; font-weight: 700; color: #1e293b; } | |
| 169 | + .fc-button-primary { background-color: #3b82f6; border-color: #3b82f6; border-radius: 10px; font-size: 13px; font-weight: 500; height: 34px; line-height: 34px; padding: 0 14px; transition: all 0.2s; &:hover { background: #2563eb; transform: translateY(-1px); box-shadow: 0 4px 12px rgba(59,130,246,0.3); } } | |
| 170 | + .fc-button-primary:not(:disabled):active, .fc-button-primary:not(:disabled).fc-button-active { background-color: #2563eb; border-color: #2563eb; box-shadow: 0 4px 12px rgba(59,130,246,0.3); } | |
| 171 | + .fc-day-today { background: linear-gradient(135deg, rgba(59,130,246,0.05) 0%, rgba(96,165,250,0.05) 100%); .fc-daygrid-day-number { background: linear-gradient(135deg, #3b82f6, #2563eb); color: #fff; border-radius: 6px; box-shadow: 0 2px 8px rgba(59,130,246,0.3); } } | |
| 172 | + .fc-event { cursor: pointer; border-radius: 6px; padding: 3px 6px; font-size: 12px; font-weight: 500; border: none; box-shadow: 0 2px 4px rgba(0,0,0,0.1); transition: all 0.2s; &:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); } } | |
| 173 | + .fc-day-header { font-size: 14px !important; color: #64748b !important; font-weight: 700 !important; background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%) !important; border-bottom: 2px solid #e2e8f0 !important; padding: 12px 8px !important; } | |
| 174 | + .fc-unthemed th, .fc-unthemed td { border-color: #f1f5f9; } | |
| 175 | +} | |
| 176 | +</style> | |
| 177 | + | |
| 178 | +<style lang="scss" scoped> | |
| 179 | +::v-deep .booking-calendar-dialog { max-width: 1600px; margin-top: 3vh !important; border-radius: 20px; padding: 0; background: radial-gradient(circle at 0 0, rgba(255,255,255,0.96) 0, rgba(248,250,252,0.98) 40%, rgba(241,245,249,0.98) 100%); box-shadow: 0 24px 48px rgba(15,23,42,0.18), 0 0 0 1px rgba(255,255,255,0.9); backdrop-filter: blur(22px); -webkit-backdrop-filter: blur(22px); } | |
| 180 | +::v-deep .el-dialog__header { display: none; } | |
| 181 | +::v-deep .el-dialog__body { padding: 0; } | |
| 182 | +.dialog-inner { display: flex; flex-direction: column; max-height: 92vh; height: 88vh; } | |
| 183 | +.dialog-header { flex-shrink: 0; display: flex; align-items: center; justify-content: space-between; margin: 18px 22px 0; padding: 10px 14px; border-radius: 14px; background: rgba(219,234,254,0.96); } | |
| 184 | +.dialog-title { font-size: 17px; font-weight: 600; color: #0f172a; } | |
| 185 | +.dialog-close { cursor: pointer; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border-radius: 999px; color: #64748b; transition: all 0.15s; &:hover { background: rgba(0,0,0,0.06); color: #0f172a; } } | |
| 186 | +.dialog-search { flex-shrink: 0; padding: 12px 22px 4px; } | |
| 187 | +.dialog-content { flex: 1; min-height: 0; overflow: hidden; padding: 0 22px 14px; } | |
| 188 | +::v-deep .booking-calendar-dialog .el-input__inner { border-radius: 999px; height: 32px; line-height: 32px; border-color: #e5e7eb; background-color: #f9fafb; &:focus { border-color: #2563eb; } } | |
| 189 | +::v-deep .booking-calendar-dialog .el-button--primary { border-radius: 999px; background: #2563eb; border-color: #2563eb; box-shadow: 0 4px 10px rgba(37,99,235,0.35); } | |
| 190 | +::v-deep .booking-calendar-dialog .el-button--default { border-radius: 999px; } | |
| 191 | +::v-deep .booking-calendar-dialog .el-form-item { margin-bottom: 8px; } | |
| 192 | +::v-deep .booking-calendar-dialog .el-form-item__label { white-space: nowrap; } | |
| 193 | +</style> | ... | ... |
store-pc/src/components/ConsumeDialog.vue
0 → 100644
| 1 | +<template> | |
| 2 | + <el-dialog | |
| 3 | + :visible.sync="visibleProxy" | |
| 4 | + :show-close="false" | |
| 5 | + width="1200px" | |
| 6 | + :close-on-click-modal="false" | |
| 7 | + custom-class="consume-dialog" | |
| 8 | + append-to-body | |
| 9 | + > | |
| 10 | + <div class="consume-dialog-inner"> | |
| 11 | + <div class="consume-header"> | |
| 12 | + <div class="consume-title-wrap"> | |
| 13 | + <div class="consume-title">新建消耗</div> | |
| 14 | + <div class="consume-subtitle" v-if="form.memberName">{{ form.memberName }}</div> | |
| 15 | + </div> | |
| 16 | + <span class="consume-close" @click="handleCancel"> | |
| 17 | + <i class="el-icon-close"></i> | |
| 18 | + </span> | |
| 19 | + </div> | |
| 20 | + | |
| 21 | + <div class="consume-content"> | |
| 22 | + <el-form | |
| 23 | + ref="form" | |
| 24 | + :model="form" | |
| 25 | + :rules="rules" | |
| 26 | + label-width="96px" | |
| 27 | + size="small" | |
| 28 | + class="consume-form" | |
| 29 | + > | |
| 30 | + <!-- ===== 左栏:基础信息 ===== --> | |
| 31 | + <div class="consume-left"> | |
| 32 | + <div class="section-title"><i class="el-icon-document"></i> 基础信息</div> | |
| 33 | + | |
| 34 | + <el-form-item label="会员" prop="memberId"> | |
| 35 | + <el-select v-model="form.memberId" placeholder="搜索会员" filterable clearable @change="onMemberChange"> | |
| 36 | + <el-option v-for="m in memberOptions" :key="m.value" :label="`${m.label}(${m.phone})`" :value="m.value" /> | |
| 37 | + </el-select> | |
| 38 | + </el-form-item> | |
| 39 | + | |
| 40 | + <el-form-item label="耗卡日期" prop="consumeDate"> | |
| 41 | + <el-date-picker v-model="form.consumeDate" type="date" placeholder="选择日期" style="width:100%" /> | |
| 42 | + </el-form-item> | |
| 43 | + | |
| 44 | + <el-form-item label="消费金额"> | |
| 45 | + <el-input :value="totalConsumeAmount" readonly> | |
| 46 | + <template slot="prepend">¥</template> | |
| 47 | + </el-input> | |
| 48 | + </el-form-item> | |
| 49 | + | |
| 50 | + <el-form-item label="手工费用"> | |
| 51 | + <el-input :value="totalLaborCost" readonly> | |
| 52 | + <template slot="prepend">¥</template> | |
| 53 | + </el-input> | |
| 54 | + </el-form-item> | |
| 55 | + | |
| 56 | + <el-form-item label="是否加班"> | |
| 57 | + <el-checkbox v-model="form.isOvertime">加班</el-checkbox> | |
| 58 | + <el-select | |
| 59 | + v-if="form.isOvertime" | |
| 60 | + v-model="form.overtimeCoefficient" | |
| 61 | + placeholder="选择加班系数" | |
| 62 | + style="width: 140px; margin-left: 12px;" | |
| 63 | + > | |
| 64 | + <el-option :value="0.5" label="0.5" /> | |
| 65 | + <el-option :value="1" label="1" /> | |
| 66 | + </el-select> | |
| 67 | + </el-form-item> | |
| 68 | + | |
| 69 | + <el-form-item label="备注"> | |
| 70 | + <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注信息" /> | |
| 71 | + </el-form-item> | |
| 72 | + </div> | |
| 73 | + | |
| 74 | + <!-- ===== 右栏:品项明细 ===== --> | |
| 75 | + <div class="consume-right"> | |
| 76 | + <div class="section-title"><i class="el-icon-goods"></i> 品项明细</div> | |
| 77 | + | |
| 78 | + <div | |
| 79 | + v-for="(item, idx) in form.items" | |
| 80 | + :key="'item-' + idx" | |
| 81 | + class="item-card" | |
| 82 | + > | |
| 83 | + <div class="item-card-head"> | |
| 84 | + <span class="item-card-no">品项 {{ idx + 1 }}</span> | |
| 85 | + <el-button | |
| 86 | + v-if="form.items.length > 1" | |
| 87 | + type="text" | |
| 88 | + class="item-remove-btn" | |
| 89 | + @click="removeItem(idx)" | |
| 90 | + > | |
| 91 | + <i class="el-icon-delete"></i> 删除 | |
| 92 | + </el-button> | |
| 93 | + </div> | |
| 94 | + | |
| 95 | + <el-form-item | |
| 96 | + label="品项" | |
| 97 | + :prop="'items.' + idx + '.projectId'" | |
| 98 | + :rules="[{ required: true, message: '请选择品项', trigger: 'change' }]" | |
| 99 | + label-width="56px" | |
| 100 | + > | |
| 101 | + <el-select | |
| 102 | + v-model="item.projectId" | |
| 103 | + placeholder="搜索品项" | |
| 104 | + filterable | |
| 105 | + clearable | |
| 106 | + @change="onProjectChange(idx)" | |
| 107 | + > | |
| 108 | + <el-option v-for="p in availableItems" :key="p.value" :label="p.label" :value="p.value" /> | |
| 109 | + </el-select> | |
| 110 | + </el-form-item> | |
| 111 | + | |
| 112 | + <!-- 品项信息展示 --> | |
| 113 | + <div v-if="item.projectId" class="px-info-panel"> | |
| 114 | + <el-row :gutter="8"> | |
| 115 | + <el-col :span="8"><span class="px-tag">单价</span> <b>¥{{ item.price }}</b></el-col> | |
| 116 | + <el-col :span="8"><span class="px-tag">总购买</span> <b>{{ item.totalPurchased }}</b></el-col> | |
| 117 | + <el-col :span="8"><span class="px-tag">已消费</span> <b>{{ item.consumed }}</b></el-col> | |
| 118 | + </el-row> | |
| 119 | + <el-row :gutter="8" style="margin-top:4px"> | |
| 120 | + <el-col :span="8"><span class="px-tag">剩余</span> <b class="remaining">{{ item.remaining }}</b></el-col> | |
| 121 | + <el-col :span="8"><span class="px-tag">来源</span> <b>{{ item.sourceType || '无' }}</b></el-col> | |
| 122 | + <el-col :span="8"><span class="px-tag">健康师手工费</span> <b>{{ item.healthCoachLaborCost }}</b></el-col> | |
| 123 | + </el-row> | |
| 124 | + <el-row :gutter="8" style="margin-top:4px" v-if="item.qt2 === '科美'"> | |
| 125 | + <el-col :span="8"><span class="px-tag">科美手工费</span> <b>{{ item.techBeautyLaborCost }}</b></el-col> | |
| 126 | + </el-row> | |
| 127 | + </div> | |
| 128 | + | |
| 129 | + <el-row :gutter="12" style="margin-top:8px"> | |
| 130 | + <el-col :span="12"> | |
| 131 | + <el-form-item label="次数" label-width="56px"> | |
| 132 | + <el-input-number | |
| 133 | + v-model="item.count" | |
| 134 | + :min="1" | |
| 135 | + :max="item.remaining || 999" | |
| 136 | + controls-position="right" | |
| 137 | + style="width:100%" | |
| 138 | + @change="onCountChange(idx)" | |
| 139 | + /> | |
| 140 | + </el-form-item> | |
| 141 | + </el-col> | |
| 142 | + </el-row> | |
| 143 | + | |
| 144 | + <!-- 健康师业绩分配 --> | |
| 145 | + <div class="worker-section"> | |
| 146 | + <div class="worker-label"> | |
| 147 | + <span>健康师业绩分配</span> | |
| 148 | + <el-button type="text" size="mini" @click="addWorker(idx)"> | |
| 149 | + <i class="el-icon-plus"></i> 添加健康师 | |
| 150 | + </el-button> | |
| 151 | + </div> | |
| 152 | + <div v-for="(w, wi) in item.workers" :key="'w-' + wi" class="worker-row"> | |
| 153 | + <el-select v-model="w.workerId" placeholder="选择健康师" filterable size="mini" class="worker-select"> | |
| 154 | + <el-option v-for="h in healthWorkerOptions" :key="h.value" :label="h.label" :value="h.value" /> | |
| 155 | + </el-select> | |
| 156 | + <el-input :value="w.performance" readonly placeholder="业绩" size="mini" class="worker-field"> | |
| 157 | + <template slot="prepend">¥</template> | |
| 158 | + </el-input> | |
| 159 | + <el-input :value="w.laborCost" readonly placeholder="手工费" size="mini" class="worker-field" /> | |
| 160 | + <el-input :value="w.count" readonly placeholder="次数" size="mini" class="worker-field-sm" /> | |
| 161 | + <el-button | |
| 162 | + v-if="item.workers.length > 1" | |
| 163 | + type="text" | |
| 164 | + size="mini" | |
| 165 | + class="worker-remove" | |
| 166 | + @click="removeWorker(idx, wi)" | |
| 167 | + > | |
| 168 | + <i class="el-icon-close"></i> | |
| 169 | + </el-button> | |
| 170 | + </div> | |
| 171 | + </div> | |
| 172 | + | |
| 173 | + <!-- 科技部老师(仅科美品项显示) --> | |
| 174 | + <div class="worker-section" v-if="item.qt2 === '科美'"> | |
| 175 | + <div class="worker-label"> | |
| 176 | + <span>科技部老师</span> | |
| 177 | + <el-button type="text" size="mini" @click="addTechTeacher(idx)"> | |
| 178 | + <i class="el-icon-plus"></i> 添加科技部老师 | |
| 179 | + </el-button> | |
| 180 | + </div> | |
| 181 | + <div v-for="(t, ti) in item.techTeachers" :key="'t-' + ti" class="worker-row"> | |
| 182 | + <el-select v-model="t.teacherId" placeholder="选择老师" filterable size="mini" class="worker-select"> | |
| 183 | + <el-option v-for="tt in techTeacherOptions" :key="tt.value" :label="tt.label" :value="tt.value" /> | |
| 184 | + </el-select> | |
| 185 | + <el-input :value="t.performance" readonly placeholder="业绩" size="mini" class="worker-field"> | |
| 186 | + <template slot="prepend">¥</template> | |
| 187 | + </el-input> | |
| 188 | + <el-input :value="t.laborCost" readonly placeholder="手工费" size="mini" class="worker-field" /> | |
| 189 | + <el-input :value="t.count" readonly placeholder="次数" size="mini" class="worker-field-sm" /> | |
| 190 | + <el-button | |
| 191 | + type="text" | |
| 192 | + size="mini" | |
| 193 | + class="worker-remove" | |
| 194 | + @click="removeTechTeacher(idx, ti)" | |
| 195 | + > | |
| 196 | + <i class="el-icon-close"></i> | |
| 197 | + </el-button> | |
| 198 | + </div> | |
| 199 | + </div> | |
| 200 | + | |
| 201 | + <!-- 陪同健康师(仅 isAllowAccompanied == 1 时显示) --> | |
| 202 | + <div class="worker-section" v-if="item.isAllowAccompanied == 1"> | |
| 203 | + <div class="worker-label"> | |
| 204 | + <span>陪同健康师</span> | |
| 205 | + <el-button type="text" size="mini" @click="addAccompanied(idx)"> | |
| 206 | + <i class="el-icon-plus"></i> 添加陪同健康师 | |
| 207 | + </el-button> | |
| 208 | + </div> | |
| 209 | + <div v-for="(a, ai) in item.accompanied" :key="'a-' + ai" class="worker-row"> | |
| 210 | + <el-select v-model="a.workerId" placeholder="选择健康师" filterable size="mini" class="worker-select"> | |
| 211 | + <el-option v-for="h in healthWorkerOptions" :key="h.value" :label="h.label" :value="h.value" /> | |
| 212 | + </el-select> | |
| 213 | + <el-input-number v-model="a.count" :min="1" controls-position="right" size="mini" style="width:100px" /> | |
| 214 | + <el-button | |
| 215 | + type="text" | |
| 216 | + size="mini" | |
| 217 | + class="worker-remove" | |
| 218 | + @click="removeAccompanied(idx, ai)" | |
| 219 | + > | |
| 220 | + <i class="el-icon-close"></i> | |
| 221 | + </el-button> | |
| 222 | + </div> | |
| 223 | + </div> | |
| 224 | + </div> | |
| 225 | + | |
| 226 | + <div class="add-btn-row"> | |
| 227 | + <el-button type="text" @click="addItem"> | |
| 228 | + <i class="el-icon-circle-plus-outline"></i> 添加品项 | |
| 229 | + </el-button> | |
| 230 | + </div> | |
| 231 | + </div> | |
| 232 | + </el-form> | |
| 233 | + </div> | |
| 234 | + | |
| 235 | + <div class="consume-footer"> | |
| 236 | + <div class="footer-summary"> | |
| 237 | + <span>消费金额合计 <b class="highlight">¥{{ totalConsumeAmount }}</b></span> | |
| 238 | + <span>手工费合计 <b>¥{{ totalLaborCost }}</b></span> | |
| 239 | + </div> | |
| 240 | + <div class="footer-actions"> | |
| 241 | + <el-button size="small" @click="handleCancel">取 消</el-button> | |
| 242 | + <el-button type="primary" size="small" :loading="submitting" @click="handleSubmit"> | |
| 243 | + {{ submitting ? '提交中...' : '确认提交' }} | |
| 244 | + </el-button> | |
| 245 | + </div> | |
| 246 | + </div> | |
| 247 | + </div> | |
| 248 | + </el-dialog> | |
| 249 | +</template> | |
| 250 | + | |
| 251 | +<script> | |
| 252 | +export default { | |
| 253 | + name: 'ConsumeDialog', | |
| 254 | + props: { | |
| 255 | + visible: { type: Boolean, default: false }, | |
| 256 | + prefill: { type: Object, default: () => ({}) } | |
| 257 | + }, | |
| 258 | + data() { | |
| 259 | + return { | |
| 260 | + submitting: false, | |
| 261 | + form: this.createEmptyForm(), | |
| 262 | + memberOptions: [ | |
| 263 | + { value: 'cust001', label: '林小纤', phone: '13800138000' }, | |
| 264 | + { value: 'cust002', label: '王丽', phone: '13800138001' }, | |
| 265 | + { value: 'cust003', label: '张敏', phone: '13800138002' } | |
| 266 | + ], | |
| 267 | + memberItemsMap: { | |
| 268 | + 'cust001': [ | |
| 269 | + { value: 'item001', label: '面部深层护理(次卡)', price: 380, remaining: 6, totalPurchased: 10, consumed: 4, sourceType: '购买', qt2: '', healthCoachLaborCost: 50, techBeautyLaborCost: 0, isAllowAccompanied: 0 }, | |
| 270 | + { value: 'item002', label: '肩颈调理(疗程)', price: 268, remaining: 3, totalPurchased: 5, consumed: 2, sourceType: '购买', qt2: '科美', healthCoachLaborCost: 30, techBeautyLaborCost: 40, isAllowAccompanied: 0 }, | |
| 271 | + { value: 'item003', label: '眼周护理套餐', price: 198, remaining: 3, totalPurchased: 4, consumed: 1, sourceType: '赠送', qt2: '', healthCoachLaborCost: 25, techBeautyLaborCost: 0, isAllowAccompanied: 1 } | |
| 272 | + ], | |
| 273 | + 'cust002': [ | |
| 274 | + { value: 'item004', label: '面部深层护理(次卡)', price: 380, remaining: 4, totalPurchased: 8, consumed: 4, sourceType: '购买', qt2: '', healthCoachLaborCost: 50, techBeautyLaborCost: 0, isAllowAccompanied: 0 } | |
| 275 | + ], | |
| 276 | + 'cust003': [ | |
| 277 | + { value: 'item005', label: '肩颈调理(疗程)', price: 268, remaining: 5, totalPurchased: 5, consumed: 0, sourceType: '购买', qt2: '科美', healthCoachLaborCost: 30, techBeautyLaborCost: 40, isAllowAccompanied: 1 } | |
| 278 | + ] | |
| 279 | + }, | |
| 280 | + healthWorkerOptions: [ | |
| 281 | + { value: 'jks001', label: '张健康师' }, | |
| 282 | + { value: 'jks002', label: '李健康师' }, | |
| 283 | + { value: 'jks003', label: '王健康师' } | |
| 284 | + ], | |
| 285 | + techTeacherOptions: [ | |
| 286 | + { value: 'kjb001', label: '赵科技老师' }, | |
| 287 | + { value: 'kjb002', label: '钱科技老师' } | |
| 288 | + ], | |
| 289 | + availableItems: [], | |
| 290 | + rules: { | |
| 291 | + memberId: [{ required: true, message: '请选择会员', trigger: 'change' }], | |
| 292 | + consumeDate: [{ required: true, message: '请选择耗卡日期', trigger: 'change' }] | |
| 293 | + } | |
| 294 | + } | |
| 295 | + }, | |
| 296 | + computed: { | |
| 297 | + visibleProxy: { | |
| 298 | + get() { return this.visible }, | |
| 299 | + set(val) { this.$emit('update:visible', val) } | |
| 300 | + }, | |
| 301 | + totalConsumeAmount() { | |
| 302 | + return this.form.items.reduce((sum, it) => { | |
| 303 | + if (it.projectId && it.price) { | |
| 304 | + return sum + (it.price * (it.count || 0)) | |
| 305 | + } | |
| 306 | + return sum | |
| 307 | + }, 0).toFixed(2) | |
| 308 | + }, | |
| 309 | + totalLaborCost() { | |
| 310 | + return this.form.items.reduce((sum, it) => { | |
| 311 | + if (!it.projectId) return sum | |
| 312 | + const count = it.count || 0 | |
| 313 | + if (it.qt2 === '科美' && it.beautyType !== 'cell') { | |
| 314 | + return sum + (it.techBeautyLaborCost || 0) * count | |
| 315 | + } else if (it.qt2 === '科美' && it.beautyType === 'cell') { | |
| 316 | + if (it.techTeachers && it.techTeachers.length > 0) { | |
| 317 | + return sum + (it.techBeautyLaborCost || 0) * count | |
| 318 | + } | |
| 319 | + return sum + (it.healthCoachLaborCost || 0) * count | |
| 320 | + } | |
| 321 | + return sum + (it.healthCoachLaborCost || 0) * count | |
| 322 | + }, 0).toFixed(2) | |
| 323 | + } | |
| 324 | + }, | |
| 325 | + watch: { | |
| 326 | + visible(val) { | |
| 327 | + if (val) this.applyPrefill() | |
| 328 | + }, | |
| 329 | + prefill: { | |
| 330 | + deep: true, | |
| 331 | + handler() { | |
| 332 | + if (this.visible) this.applyPrefill() | |
| 333 | + } | |
| 334 | + } | |
| 335 | + }, | |
| 336 | + methods: { | |
| 337 | + createEmptyForm() { | |
| 338 | + return { | |
| 339 | + memberId: '', | |
| 340 | + memberName: '', | |
| 341 | + consumeDate: new Date(), | |
| 342 | + isOvertime: false, | |
| 343 | + overtimeCoefficient: 0.5, | |
| 344 | + remark: '', | |
| 345 | + items: [this.createEmptyItem()] | |
| 346 | + } | |
| 347 | + }, | |
| 348 | + createEmptyItem() { | |
| 349 | + return { | |
| 350 | + projectId: '', | |
| 351 | + price: 0, | |
| 352 | + remaining: 0, | |
| 353 | + totalPurchased: 0, | |
| 354 | + consumed: 0, | |
| 355 | + sourceType: '', | |
| 356 | + qt2: '', | |
| 357 | + beautyType: '', | |
| 358 | + healthCoachLaborCost: 0, | |
| 359 | + techBeautyLaborCost: 0, | |
| 360 | + isAllowAccompanied: 0, | |
| 361 | + count: 1, | |
| 362 | + workers: [{ workerId: '', performance: '', laborCost: '', count: '' }], | |
| 363 | + techTeachers: [], | |
| 364 | + accompanied: [] | |
| 365 | + } | |
| 366 | + }, | |
| 367 | + applyPrefill() { | |
| 368 | + this.form = this.createEmptyForm() | |
| 369 | + this.availableItems = [] | |
| 370 | + if (this.prefill && this.prefill.memberId) { | |
| 371 | + this.form.memberId = this.prefill.memberId | |
| 372 | + this.form.memberName = this.prefill.name || '' | |
| 373 | + this.loadMemberItems(this.prefill.memberId) | |
| 374 | + } | |
| 375 | + this.$nextTick(() => { | |
| 376 | + this.$refs.form && this.$refs.form.clearValidate() | |
| 377 | + }) | |
| 378 | + }, | |
| 379 | + onMemberChange(val) { | |
| 380 | + const m = this.memberOptions.find(o => o.value === val) | |
| 381 | + this.form.memberName = m ? m.label : '' | |
| 382 | + this.form.items = [this.createEmptyItem()] | |
| 383 | + this.loadMemberItems(val) | |
| 384 | + }, | |
| 385 | + loadMemberItems(memberId) { | |
| 386 | + const items = this.memberItemsMap[memberId] || [] | |
| 387 | + this.availableItems = items.map(it => ({ | |
| 388 | + ...it, | |
| 389 | + label: `${it.label}(剩余${it.remaining}次)` | |
| 390 | + })) | |
| 391 | + }, | |
| 392 | + onProjectChange(idx) { | |
| 393 | + const item = this.form.items[idx] | |
| 394 | + const p = this.availableItems.find(o => o.value === item.projectId) | |
| 395 | + if (p) { | |
| 396 | + item.price = p.price | |
| 397 | + item.remaining = p.remaining | |
| 398 | + item.totalPurchased = p.totalPurchased | |
| 399 | + item.consumed = p.consumed | |
| 400 | + item.sourceType = p.sourceType | |
| 401 | + item.qt2 = p.qt2 | |
| 402 | + item.healthCoachLaborCost = p.healthCoachLaborCost | |
| 403 | + item.techBeautyLaborCost = p.techBeautyLaborCost | |
| 404 | + item.isAllowAccompanied = p.isAllowAccompanied | |
| 405 | + item.count = 1 | |
| 406 | + item.workers = [{ workerId: '', performance: '', laborCost: '', count: '' }] | |
| 407 | + item.techTeachers = [] | |
| 408 | + item.accompanied = [] | |
| 409 | + } | |
| 410 | + this.redistributeWorkers(idx) | |
| 411 | + }, | |
| 412 | + onCountChange(idx) { | |
| 413 | + this.redistributeWorkers(idx) | |
| 414 | + this.redistributeTechTeachers(idx) | |
| 415 | + }, | |
| 416 | + redistributeWorkers(idx) { | |
| 417 | + const item = this.form.items[idx] | |
| 418 | + if (!item.workers || item.workers.length === 0) return | |
| 419 | + const totalCount = item.count || 0 | |
| 420 | + const totalPerf = item.price * totalCount | |
| 421 | + const isKemei = item.qt2 === '科美' && item.beautyType !== 'cell' | |
| 422 | + const isCell = item.qt2 === '科美' && item.beautyType === 'cell' | |
| 423 | + const hasTech = item.techTeachers && item.techTeachers.length > 0 | |
| 424 | + const n = item.workers.length | |
| 425 | + if (isKemei || (isCell && hasTech)) { | |
| 426 | + item.workers.forEach(w => { | |
| 427 | + w.performance = (totalPerf / n).toFixed(2) | |
| 428 | + w.laborCost = '0.00' | |
| 429 | + w.count = '0' | |
| 430 | + }) | |
| 431 | + } else { | |
| 432 | + const totalLabor = (item.healthCoachLaborCost || 0) * totalCount | |
| 433 | + item.workers.forEach(w => { | |
| 434 | + w.performance = (totalPerf / n).toFixed(2) | |
| 435 | + w.laborCost = (totalLabor / n).toFixed(2) | |
| 436 | + w.count = (totalCount / n).toFixed(2) | |
| 437 | + }) | |
| 438 | + } | |
| 439 | + }, | |
| 440 | + redistributeTechTeachers(idx) { | |
| 441 | + const item = this.form.items[idx] | |
| 442 | + if (!item.techTeachers || item.techTeachers.length === 0) return | |
| 443 | + const totalCount = item.count || 0 | |
| 444 | + const totalPerf = item.price * totalCount | |
| 445 | + const totalLabor = (item.techBeautyLaborCost || 0) * totalCount | |
| 446 | + const n = item.techTeachers.length | |
| 447 | + item.techTeachers.forEach(t => { | |
| 448 | + t.performance = (totalPerf / n).toFixed(2) | |
| 449 | + t.laborCost = (totalLabor / n).toFixed(2) | |
| 450 | + t.count = (totalCount / n).toFixed(2) | |
| 451 | + }) | |
| 452 | + this.redistributeWorkers(idx) | |
| 453 | + }, | |
| 454 | + addItem() { | |
| 455 | + this.form.items.push(this.createEmptyItem()) | |
| 456 | + }, | |
| 457 | + removeItem(idx) { | |
| 458 | + this.form.items.splice(idx, 1) | |
| 459 | + }, | |
| 460 | + addWorker(idx) { | |
| 461 | + this.form.items[idx].workers.push({ workerId: '', performance: '', laborCost: '', count: '' }) | |
| 462 | + this.redistributeWorkers(idx) | |
| 463 | + }, | |
| 464 | + removeWorker(idx, wi) { | |
| 465 | + this.form.items[idx].workers.splice(wi, 1) | |
| 466 | + this.redistributeWorkers(idx) | |
| 467 | + }, | |
| 468 | + addTechTeacher(idx) { | |
| 469 | + this.form.items[idx].techTeachers.push({ teacherId: '', performance: '', laborCost: '', count: '' }) | |
| 470 | + this.redistributeTechTeachers(idx) | |
| 471 | + }, | |
| 472 | + removeTechTeacher(idx, ti) { | |
| 473 | + this.form.items[idx].techTeachers.splice(ti, 1) | |
| 474 | + this.redistributeTechTeachers(idx) | |
| 475 | + }, | |
| 476 | + addAccompanied(idx) { | |
| 477 | + this.form.items[idx].accompanied.push({ workerId: '', count: 1 }) | |
| 478 | + }, | |
| 479 | + removeAccompanied(idx, ai) { | |
| 480 | + this.form.items[idx].accompanied.splice(ai, 1) | |
| 481 | + }, | |
| 482 | + resetForm() { | |
| 483 | + this.form = this.createEmptyForm() | |
| 484 | + this.availableItems = [] | |
| 485 | + this.$nextTick(() => { | |
| 486 | + this.$refs.form && this.$refs.form.clearValidate() | |
| 487 | + }) | |
| 488 | + }, | |
| 489 | + handleCancel() { | |
| 490 | + this.visibleProxy = false | |
| 491 | + this.resetForm() | |
| 492 | + }, | |
| 493 | + handleSubmit() { | |
| 494 | + this.$refs.form.validate(valid => { | |
| 495 | + if (!valid) return | |
| 496 | + this.submitting = true | |
| 497 | + setTimeout(() => { | |
| 498 | + this.submitting = false | |
| 499 | + this.$message.success('消耗记录已保存(示例)') | |
| 500 | + this.$emit('submitted', this.form) | |
| 501 | + this.visibleProxy = false | |
| 502 | + this.resetForm() | |
| 503 | + }, 800) | |
| 504 | + }) | |
| 505 | + } | |
| 506 | + } | |
| 507 | +} | |
| 508 | +</script> | |
| 509 | + | |
| 510 | +<style lang="scss" scoped> | |
| 511 | +/* ====== 弹窗外壳 ====== */ | |
| 512 | +::v-deep .consume-dialog { | |
| 513 | + max-width: 1200px; | |
| 514 | + margin-top: 5vh !important; | |
| 515 | + border-radius: 20px; | |
| 516 | + padding: 0; | |
| 517 | + background: radial-gradient( | |
| 518 | + circle at 0 0, | |
| 519 | + rgba(255, 255, 255, 0.96) 0, | |
| 520 | + rgba(248, 250, 252, 0.98) 40%, | |
| 521 | + rgba(241, 245, 249, 0.98) 100% | |
| 522 | + ); | |
| 523 | + box-shadow: | |
| 524 | + 0 24px 48px rgba(15, 23, 42, 0.18), | |
| 525 | + 0 0 0 1px rgba(255, 255, 255, 0.9); | |
| 526 | + backdrop-filter: blur(22px); | |
| 527 | + -webkit-backdrop-filter: blur(22px); | |
| 528 | +} | |
| 529 | + | |
| 530 | +::v-deep .consume-dialog .el-dialog__header { | |
| 531 | + display: none; | |
| 532 | +} | |
| 533 | + | |
| 534 | +::v-deep .consume-dialog .el-dialog__body { | |
| 535 | + padding: 0; | |
| 536 | +} | |
| 537 | + | |
| 538 | +/* ====== 内部结构 ====== */ | |
| 539 | +.consume-dialog-inner { | |
| 540 | + display: flex; | |
| 541 | + flex-direction: column; | |
| 542 | + max-height: 85vh; | |
| 543 | +} | |
| 544 | + | |
| 545 | +.consume-header { | |
| 546 | + flex-shrink: 0; | |
| 547 | + display: flex; | |
| 548 | + align-items: center; | |
| 549 | + gap: 8px; | |
| 550 | + margin: 18px 22px 0; | |
| 551 | + padding: 10px 14px; | |
| 552 | + border-radius: 14px; | |
| 553 | + background: rgba(219, 234, 254, 0.96); | |
| 554 | +} | |
| 555 | + | |
| 556 | +.consume-title-wrap { | |
| 557 | + flex: 1; | |
| 558 | +} | |
| 559 | + | |
| 560 | +.consume-title { | |
| 561 | + font-size: 17px; | |
| 562 | + font-weight: 600; | |
| 563 | + color: #0f172a; | |
| 564 | +} | |
| 565 | + | |
| 566 | +.consume-subtitle { | |
| 567 | + font-size: 12px; | |
| 568 | + color: #475569; | |
| 569 | + margin-top: 2px; | |
| 570 | +} | |
| 571 | + | |
| 572 | +.consume-close { | |
| 573 | + flex-shrink: 0; | |
| 574 | + cursor: pointer; | |
| 575 | + width: 28px; | |
| 576 | + height: 28px; | |
| 577 | + display: flex; | |
| 578 | + align-items: center; | |
| 579 | + justify-content: center; | |
| 580 | + border-radius: 999px; | |
| 581 | + color: #64748b; | |
| 582 | + transition: all 0.15s; | |
| 583 | + | |
| 584 | + &:hover { | |
| 585 | + background: rgba(0, 0, 0, 0.06); | |
| 586 | + color: #0f172a; | |
| 587 | + } | |
| 588 | +} | |
| 589 | + | |
| 590 | +/* ====== 双栏主体 ====== */ | |
| 591 | +.consume-content { | |
| 592 | + flex: 1; | |
| 593 | + min-height: 0; | |
| 594 | + overflow: hidden; | |
| 595 | + display: flex; | |
| 596 | +} | |
| 597 | + | |
| 598 | +.consume-form { | |
| 599 | + display: flex; | |
| 600 | + flex: 1; | |
| 601 | + min-height: 0; | |
| 602 | +} | |
| 603 | + | |
| 604 | +.consume-left { | |
| 605 | + flex: 0 0 440px; | |
| 606 | + overflow-y: auto; | |
| 607 | + padding: 10px 16px 10px 22px; | |
| 608 | + border-right: 1px solid rgba(229, 231, 235, 0.6); | |
| 609 | + min-height: 0; | |
| 610 | +} | |
| 611 | + | |
| 612 | +.consume-right { | |
| 613 | + flex: 1; | |
| 614 | + overflow-y: auto; | |
| 615 | + padding: 10px 22px 10px 16px; | |
| 616 | + min-height: 0; | |
| 617 | +} | |
| 618 | + | |
| 619 | +/* ====== 分区标题 ====== */ | |
| 620 | +.section-title { | |
| 621 | + font-size: 13px; | |
| 622 | + font-weight: 600; | |
| 623 | + color: #334155; | |
| 624 | + margin: 14px 0 8px; | |
| 625 | + padding: 6px 10px; | |
| 626 | + border-radius: 8px; | |
| 627 | + background: rgba(241, 245, 249, 0.7); | |
| 628 | + | |
| 629 | + i { | |
| 630 | + margin-right: 4px; | |
| 631 | + color: #2563eb; | |
| 632 | + } | |
| 633 | + | |
| 634 | + &:first-child { | |
| 635 | + margin-top: 4px; | |
| 636 | + } | |
| 637 | +} | |
| 638 | + | |
| 639 | +/* ====== 品项卡片 ====== */ | |
| 640 | +.item-card { | |
| 641 | + border: 1px solid #e5e7eb; | |
| 642 | + border-radius: 12px; | |
| 643 | + padding: 10px 12px 4px; | |
| 644 | + margin-bottom: 10px; | |
| 645 | + background: rgba(255, 255, 255, 0.6); | |
| 646 | + transition: border-color 0.15s; | |
| 647 | + | |
| 648 | + &:hover { | |
| 649 | + border-color: #93c5fd; | |
| 650 | + } | |
| 651 | +} | |
| 652 | + | |
| 653 | +.item-card-head { | |
| 654 | + display: flex; | |
| 655 | + align-items: center; | |
| 656 | + justify-content: space-between; | |
| 657 | + margin-bottom: 6px; | |
| 658 | +} | |
| 659 | + | |
| 660 | +.item-card-no { | |
| 661 | + font-size: 12px; | |
| 662 | + font-weight: 600; | |
| 663 | + color: #2563eb; | |
| 664 | +} | |
| 665 | + | |
| 666 | +.item-remove-btn { | |
| 667 | + color: #ef4444 !important; | |
| 668 | + font-size: 12px; | |
| 669 | + padding: 0; | |
| 670 | +} | |
| 671 | + | |
| 672 | +/* ====== 品项信息面板 ====== */ | |
| 673 | +.px-info-panel { | |
| 674 | + margin: 4px 0 6px; | |
| 675 | + padding: 8px 10px; | |
| 676 | + border-radius: 8px; | |
| 677 | + background: rgba(241, 245, 249, 0.5); | |
| 678 | + font-size: 12px; | |
| 679 | + color: #475569; | |
| 680 | + line-height: 1.8; | |
| 681 | + | |
| 682 | + .px-tag { | |
| 683 | + color: #94a3b8; | |
| 684 | + margin-right: 2px; | |
| 685 | + } | |
| 686 | + | |
| 687 | + b { | |
| 688 | + color: #0f172a; | |
| 689 | + font-weight: 600; | |
| 690 | + } | |
| 691 | + | |
| 692 | + .remaining { | |
| 693 | + color: #2563eb; | |
| 694 | + } | |
| 695 | +} | |
| 696 | + | |
| 697 | +/* ====== 健康师 / 科技部老师 / 陪同行 ====== */ | |
| 698 | +.worker-section { | |
| 699 | + margin: 2px 0 6px; | |
| 700 | + padding: 8px 10px; | |
| 701 | + border-radius: 8px; | |
| 702 | + background: rgba(241, 245, 249, 0.5); | |
| 703 | +} | |
| 704 | + | |
| 705 | +.worker-label { | |
| 706 | + display: flex; | |
| 707 | + align-items: center; | |
| 708 | + justify-content: space-between; | |
| 709 | + margin-bottom: 6px; | |
| 710 | + font-size: 12px; | |
| 711 | + color: #475569; | |
| 712 | + font-weight: 500; | |
| 713 | +} | |
| 714 | + | |
| 715 | +.worker-row { | |
| 716 | + display: flex; | |
| 717 | + align-items: center; | |
| 718 | + gap: 8px; | |
| 719 | + margin-bottom: 6px; | |
| 720 | +} | |
| 721 | + | |
| 722 | +.worker-select { | |
| 723 | + flex: 1; | |
| 724 | +} | |
| 725 | + | |
| 726 | +.worker-field { | |
| 727 | + width: 120px; | |
| 728 | + flex-shrink: 0; | |
| 729 | +} | |
| 730 | + | |
| 731 | +.worker-field-sm { | |
| 732 | + width: 70px; | |
| 733 | + flex-shrink: 0; | |
| 734 | +} | |
| 735 | + | |
| 736 | +.worker-remove { | |
| 737 | + color: #ef4444 !important; | |
| 738 | + padding: 0; | |
| 739 | +} | |
| 740 | + | |
| 741 | +/* ====== 添加按钮行 ====== */ | |
| 742 | +.add-btn-row { | |
| 743 | + text-align: center; | |
| 744 | + margin-bottom: 6px; | |
| 745 | +} | |
| 746 | + | |
| 747 | +/* ====== footer ====== */ | |
| 748 | +.consume-footer { | |
| 749 | + flex-shrink: 0; | |
| 750 | + display: flex; | |
| 751 | + align-items: center; | |
| 752 | + justify-content: space-between; | |
| 753 | + padding: 10px 22px 14px; | |
| 754 | + border-top: 1px solid rgba(229, 231, 235, 0.6); | |
| 755 | +} | |
| 756 | + | |
| 757 | +.footer-summary { | |
| 758 | + display: flex; | |
| 759 | + gap: 16px; | |
| 760 | + font-size: 12px; | |
| 761 | + color: #64748b; | |
| 762 | + | |
| 763 | + b { | |
| 764 | + color: #0f172a; | |
| 765 | + } | |
| 766 | + | |
| 767 | + .highlight { | |
| 768 | + color: #2563eb; | |
| 769 | + font-size: 14px; | |
| 770 | + } | |
| 771 | +} | |
| 772 | + | |
| 773 | +.footer-actions { | |
| 774 | + display: flex; | |
| 775 | + gap: 10px; | |
| 776 | +} | |
| 777 | + | |
| 778 | +/* ====== 统一输入框 / 按钮风格 ====== */ | |
| 779 | +::v-deep .consume-dialog .el-form-item__label { | |
| 780 | + white-space: nowrap; | |
| 781 | +} | |
| 782 | + | |
| 783 | +::v-deep .consume-dialog .el-input__inner { | |
| 784 | + border-radius: 999px; | |
| 785 | + height: 32px; | |
| 786 | + line-height: 32px; | |
| 787 | + border-color: #e5e7eb; | |
| 788 | + background-color: #f9fafb; | |
| 789 | +} | |
| 790 | + | |
| 791 | +::v-deep .consume-dialog .el-input__inner:focus { | |
| 792 | + border-color: #2563eb; | |
| 793 | + box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.18); | |
| 794 | +} | |
| 795 | + | |
| 796 | +::v-deep .consume-dialog .el-input-group__prepend { | |
| 797 | + border-radius: 999px 0 0 999px; | |
| 798 | + background: #f1f5f9; | |
| 799 | + border-color: #e5e7eb; | |
| 800 | + padding: 0 10px; | |
| 801 | + color: #64748b; | |
| 802 | +} | |
| 803 | + | |
| 804 | +::v-deep .consume-dialog .el-input-group .el-input__inner { | |
| 805 | + border-radius: 0 999px 999px 0; | |
| 806 | +} | |
| 807 | + | |
| 808 | +::v-deep .consume-dialog .el-textarea__inner { | |
| 809 | + border-radius: 12px; | |
| 810 | + border-color: #e5e7eb; | |
| 811 | + background-color: #f9fafb; | |
| 812 | +} | |
| 813 | + | |
| 814 | +::v-deep .consume-dialog .el-textarea__inner:focus { | |
| 815 | + border-color: #2563eb; | |
| 816 | + box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.18); | |
| 817 | +} | |
| 818 | + | |
| 819 | +::v-deep .consume-dialog .el-input-number { | |
| 820 | + .el-input__inner { | |
| 821 | + border-radius: 999px; | |
| 822 | + } | |
| 823 | +} | |
| 824 | + | |
| 825 | +::v-deep .consume-dialog .el-button--primary { | |
| 826 | + border-radius: 999px; | |
| 827 | + padding: 0 20px; | |
| 828 | + height: 30px; | |
| 829 | + line-height: 30px; | |
| 830 | + background: #2563eb; | |
| 831 | + border-color: #2563eb; | |
| 832 | + box-shadow: 0 4px 10px rgba(37, 99, 235, 0.35); | |
| 833 | + font-size: 12px; | |
| 834 | +} | |
| 835 | + | |
| 836 | +::v-deep .consume-dialog .el-button--primary.is-disabled { | |
| 837 | + box-shadow: none; | |
| 838 | +} | |
| 839 | + | |
| 840 | +::v-deep .consume-dialog .el-button--default { | |
| 841 | + border-radius: 999px; | |
| 842 | + padding: 0 18px; | |
| 843 | + height: 30px; | |
| 844 | + line-height: 30px; | |
| 845 | + background: rgba(239, 246, 255, 0.9); | |
| 846 | + color: #2563eb; | |
| 847 | + border-color: rgba(37, 99, 235, 0.18); | |
| 848 | + font-size: 12px; | |
| 849 | +} | |
| 850 | + | |
| 851 | +::v-deep .consume-dialog .el-button--default:hover { | |
| 852 | + background: rgba(219, 234, 254, 0.95); | |
| 853 | + color: #1d4ed8; | |
| 854 | +} | |
| 855 | + | |
| 856 | +::v-deep .consume-dialog .el-checkbox__input.is-checked .el-checkbox__inner { | |
| 857 | + border-color: #2563eb; | |
| 858 | + background: #2563eb; | |
| 859 | +} | |
| 860 | + | |
| 861 | +::v-deep .consume-dialog .el-checkbox__input.is-checked + .el-checkbox__label { | |
| 862 | + color: #2563eb; | |
| 863 | +} | |
| 864 | + | |
| 865 | +::v-deep .consume-dialog .el-form-item { | |
| 866 | + margin-bottom: 12px; | |
| 867 | +} | |
| 868 | + | |
| 869 | +::v-deep .consume-dialog .el-picker-panel { | |
| 870 | + border-radius: 12px; | |
| 871 | +} | |
| 872 | + | |
| 873 | +::v-deep .consume-dialog .el-select { | |
| 874 | + width: 100%; | |
| 875 | +} | |
| 876 | +</style> | ... | ... |
store-pc/src/components/ConsumeListDialog.vue
0 → 100644
| 1 | +<template> | |
| 2 | + <el-dialog | |
| 3 | + :visible.sync="visibleProxy" | |
| 4 | + :show-close="false" | |
| 5 | + width="90%" | |
| 6 | + :close-on-click-modal="false" | |
| 7 | + custom-class="consume-list-dialog" | |
| 8 | + append-to-body | |
| 9 | + > | |
| 10 | + <div class="dialog-inner"> | |
| 11 | + <div class="dialog-header"> | |
| 12 | + <div class="dialog-title">消耗记录</div> | |
| 13 | + <span class="dialog-close" @click="visibleProxy = false"><i class="el-icon-close"></i></span> | |
| 14 | + </div> | |
| 15 | + | |
| 16 | + <div class="dialog-search"> | |
| 17 | + <el-form @submit.native.prevent :inline="true" size="small"> | |
| 18 | + <el-form-item label="耗卡时间"> | |
| 19 | + <el-date-picker v-model="query.hksj" type="daterange" value-format="timestamp" format="yyyy-MM-dd" start-placeholder="开始" end-placeholder="结束" style="width:220px" /> | |
| 20 | + </el-form-item> | |
| 21 | + <el-form-item label="会员"> | |
| 22 | + <el-select v-model="query.hy" filterable remote reserve-keyword clearable placeholder="搜索会员" :remote-method="searchMember" :loading="memberLoading" style="width:200px"> | |
| 23 | + <el-option v-for="m in memberOptions" :key="m.value" :label="m.label" :value="m.value" /> | |
| 24 | + </el-select> | |
| 25 | + </el-form-item> | |
| 26 | + <template v-if="showAll"> | |
| 27 | + <el-form-item label="健康师"> | |
| 28 | + <el-select v-model="query.jksId" placeholder="健康师" clearable filterable style="width:150px"> | |
| 29 | + <el-option v-for="h in jksOptions" :key="h.id" :label="h.fullName" :value="h.id" /> | |
| 30 | + </el-select> | |
| 31 | + </el-form-item> | |
| 32 | + <el-form-item label="科技老师"> | |
| 33 | + <el-select v-model="query.kjblsId" placeholder="科技老师" clearable filterable style="width:150px"> | |
| 34 | + <el-option v-for="t in kjbOptions" :key="t.id" :label="t.fullName" :value="t.id" /> | |
| 35 | + </el-select> | |
| 36 | + </el-form-item> | |
| 37 | + <el-form-item label="消费金额"> | |
| 38 | + <el-input v-model="query.xfje" placeholder="消费金额" clearable style="width:120px" /> | |
| 39 | + </el-form-item> | |
| 40 | + <el-form-item label="手工费用"> | |
| 41 | + <el-input v-model="query.sgfy" placeholder="手工费用" clearable style="width:120px" /> | |
| 42 | + </el-form-item> | |
| 43 | + <el-form-item label="是否作废"> | |
| 44 | + <el-select v-model="query.isEffective" placeholder="请选择" clearable style="width:100px"> | |
| 45 | + <el-option label="正常" value="1" /><el-option label="作废" value="-1" /> | |
| 46 | + </el-select> | |
| 47 | + </el-form-item> | |
| 48 | + </template> | |
| 49 | + <el-form-item> | |
| 50 | + <el-button type="primary" @click="search">查询</el-button> | |
| 51 | + <el-button @click="reset">重置</el-button> | |
| 52 | + <el-button type="text" @click="showAll = !showAll"> | |
| 53 | + {{ showAll ? '收起' : '展开' }} <i :class="showAll ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"></i> | |
| 54 | + </el-button> | |
| 55 | + </el-form-item> | |
| 56 | + </el-form> | |
| 57 | + </div> | |
| 58 | + | |
| 59 | + <div class="dialog-content"> | |
| 60 | + <el-table v-loading="loading" :data="list" border size="small" :header-cell-style="{ background:'#f5f7fa', color:'#606266', fontWeight:600 }"> | |
| 61 | + <el-table-column type="expand" width="50"> | |
| 62 | + <template slot-scope="{ row }"> | |
| 63 | + <div class="expand-box" v-if="row.ConsumeDetails && row.ConsumeDetails.length"> | |
| 64 | + <div class="expand-title"><i class="el-icon-goods"></i> 消费明细</div> | |
| 65 | + <el-table :data="row.ConsumeDetails" border size="mini" :header-cell-style="{ background:'#f5f7fa', color:'#606266' }"> | |
| 66 | + <el-table-column prop="pxmc" label="项目名称" width="180" /> | |
| 67 | + <el-table-column label="项目价格" width="120" align="right"> | |
| 68 | + <template slot-scope="s">¥{{ formatMoney(s.row.pxjg) }}</template> | |
| 69 | + </el-table-column> | |
| 70 | + <el-table-column prop="projectNumber" label="次数" width="80" align="right" /> | |
| 71 | + <el-table-column label="总价" width="120" align="right"> | |
| 72 | + <template slot-scope="s">¥{{ formatMoney(s.row.totalPrice) }}</template> | |
| 73 | + </el-table-column> | |
| 74 | + <el-table-column label="来源" width="100"> | |
| 75 | + <template slot-scope="s"><el-tag size="mini" type="info">{{ s.row.sourceType || '-' }}</el-tag></template> | |
| 76 | + </el-table-column> | |
| 77 | + <el-table-column label="是否有效" width="90" align="center"> | |
| 78 | + <template slot-scope="s">{{ s.row.isEffective == 1 ? '有效' : '无效' }}</template> | |
| 79 | + </el-table-column> | |
| 80 | + </el-table> | |
| 81 | + </div> | |
| 82 | + <div v-else class="expand-empty"><i class="el-icon-info"></i> 暂无消费明细</div> | |
| 83 | + </template> | |
| 84 | + </el-table-column> | |
| 85 | + <el-table-column prop="mdmc" label="门店名称" show-overflow-tooltip /> | |
| 86 | + <el-table-column prop="hymc" label="会员名称" /> | |
| 87 | + <el-table-column prop="memberPhone" label="会员手机号" width="120" /> | |
| 88 | + <el-table-column prop="xfje" label="消费金额" width="100" align="right" /> | |
| 89 | + <el-table-column prop="sgfy" label="手工费用" width="100" align="right" /> | |
| 90 | + <el-table-column label="耗卡时间" width="110"> | |
| 91 | + <template slot-scope="{ row }">{{ formatDate(row.hksj) }}</template> | |
| 92 | + </el-table-column> | |
| 93 | + <el-table-column label="是否作废" width="90" align="center"> | |
| 94 | + <template slot-scope="{ row }">{{ row.isEffective == '1' ? '正常' : '作废' }}</template> | |
| 95 | + </el-table-column> | |
| 96 | + <el-table-column label="操作人" width="120" show-overflow-tooltip> | |
| 97 | + <template slot-scope="{ row }">{{ row.czry || '-' }}</template> | |
| 98 | + </el-table-column> | |
| 99 | + </el-table> | |
| 100 | + </div> | |
| 101 | + | |
| 102 | + <div class="dialog-footer"> | |
| 103 | + <el-pagination background layout="total, sizes, prev, pager, next, jumper" :total="total" :page-size.sync="listQuery.pageSize" :current-page.sync="listQuery.currentPage" :page-sizes="[10, 20, 50, 100]" @size-change="initData" @current-change="initData" /> | |
| 104 | + </div> | |
| 105 | + </div> | |
| 106 | + </el-dialog> | |
| 107 | +</template> | |
| 108 | + | |
| 109 | +<script> | |
| 110 | +const MOCK_CONSUME = [ | |
| 111 | + { id: '1', mdmc: '绿纤沙河店', hymc: '葛梦', memberPhone: '15114012324', xfje: '224.00', sgfy: '18.00', hksj: '2026-02-12 03:16:21', isEffective: 1, jksName: '翁玲', kjbName: '', czry: '翁玲', ConsumeDetails: [{ pxmc: 'CELL', pxjg: '224.00', projectNumber: '1', totalPrice: '224.00', sourceType: '购买', isEffective: 1 }] }, | |
| 112 | + { id: '2', mdmc: '绿纤静居寺店', hymc: '舒燕群', memberPhone: '13111871020', xfje: '98.65', sgfy: '12.00', hksj: '2026-02-12 01:33:28', isEffective: 1, jksName: '王娇', kjbName: '', czry: '王娇', ConsumeDetails: [{ pxmc: '水氧-面部', pxjg: '98.65', projectNumber: '1', totalPrice: '98.65', sourceType: '购买', isEffective: 1 }] }, | |
| 113 | + { id: '3', mdmc: '绿纤静居寺店', hymc: '李瑛', memberPhone: '18982114877', xfje: '33.00', sgfy: '12.00', hksj: '2026-02-12 01:33:03', isEffective: 1, jksName: '王娇', kjbName: '', czry: '王娇', ConsumeDetails: [{ pxmc: '基础护理', pxjg: '33.00', projectNumber: '1', totalPrice: '33.00', sourceType: '购买', isEffective: 1 }] }, | |
| 114 | + { id: '4', mdmc: '绿纤静居寺店', hymc: '刘红', memberPhone: '13658080278', xfje: '0.00', sgfy: '12.00', hksj: '2026-02-12 01:32:24', isEffective: 1, jksName: '王娇', kjbName: '', czry: '王娇', ConsumeDetails: [{ pxmc: '赠送项目', pxjg: '0.00', projectNumber: '1', totalPrice: '0.00', sourceType: '赠送', isEffective: 1 }] }, | |
| 115 | + { id: '5', mdmc: '绿纤沙河店', hymc: '胡晗阳', memberPhone: '13687006033', xfje: '100.00', sgfy: '12.00', hksj: '2026-02-12 00:03:19', isEffective: 1, jksName: '杨宜佳', kjbName: '', czry: '杨宜佳', ConsumeDetails: [{ pxmc: '胶原宝宝-双部位', pxjg: '100.00', projectNumber: '1', totalPrice: '100.00', sourceType: '购买', isEffective: 1 }] }, | |
| 116 | + { id: '6', mdmc: '绿纤沙河店', hymc: '何清', memberPhone: '13981799239', xfje: '162.00', sgfy: '28.00', hksj: '2026-02-12 00:01:47', isEffective: 1, jksName: '杨宜佳', kjbName: '', czry: '杨宜佳', ConsumeDetails: [{ pxmc: '胶原宝宝-双部位', pxjg: '81.00', projectNumber: '1', totalPrice: '81.00', sourceType: '购买', isEffective: 1 }, { pxmc: 'CELL', pxjg: '81.00', projectNumber: '1', totalPrice: '81.00', sourceType: '购买', isEffective: 1 }] }, | |
| 117 | + { id: '7', mdmc: '绿纤沙河店', hymc: '赵兰', memberPhone: '18228080822', xfje: '83.33', sgfy: '12.00', hksj: '2026-02-12 00:00:22', isEffective: 1, jksName: '杨宜佳', kjbName: '', czry: '杨宜佳', ConsumeDetails: [{ pxmc: '胶原宝宝-单部位', pxjg: '83.33', projectNumber: '1', totalPrice: '83.33', sourceType: '购买', isEffective: 1 }] }, | |
| 118 | + { id: '8', mdmc: '绿纤沙河店', hymc: '杨静', memberPhone: '17828160674', xfje: '162.00', sgfy: '26.00', hksj: '2026-02-11 23:59:48', isEffective: 1, jksName: '杨宜佳', kjbName: '', czry: '杨宜佳', ConsumeDetails: [{ pxmc: '胶原宝宝-双部位', pxjg: '81.00', projectNumber: '1', totalPrice: '81.00', sourceType: '购买', isEffective: 1 }, { pxmc: '水氧-面部', pxjg: '81.00', projectNumber: '1', totalPrice: '81.00', sourceType: '购买', isEffective: 1 }] }, | |
| 119 | + { id: '9', mdmc: '绿纤沙河店', hymc: '葛梦', memberPhone: '15114012324', xfje: '224.00', sgfy: '18.00', hksj: '2026-02-11 23:42:14', isEffective: 1, jksName: '吴飞雁', kjbName: '', czry: '吴飞雁', ConsumeDetails: [{ pxmc: 'CELL', pxjg: '224.00', projectNumber: '1', totalPrice: '224.00', sourceType: '购买', isEffective: 1 }] }, | |
| 120 | + { id: '10', mdmc: '绿纤沙河店', hymc: '何清', memberPhone: '13981799239', xfje: '0.00', sgfy: '13.00', hksj: '2026-02-11 23:41:57', isEffective: 1, jksName: '吴飞雁', kjbName: '', czry: '吴飞雁', ConsumeDetails: [{ pxmc: '赠送项目', pxjg: '0.00', projectNumber: '1', totalPrice: '0.00', sourceType: '赠送', isEffective: 1 }] } | |
| 121 | +] | |
| 122 | +export default { | |
| 123 | + name: 'ConsumeListDialog', | |
| 124 | + props: { visible: { type: Boolean, default: false } }, | |
| 125 | + data() { | |
| 126 | + return { | |
| 127 | + mockData: MOCK_CONSUME, | |
| 128 | + showAll: false, loading: false, memberLoading: false, | |
| 129 | + memberOptions: [], jksOptions: [], kjbOptions: [], | |
| 130 | + list: [], total: 0, | |
| 131 | + query: { hksj: undefined, hy: undefined, jksId: undefined, kjblsId: undefined, xfje: undefined, sgfy: undefined, isEffective: undefined }, | |
| 132 | + listQuery: { currentPage: 1, pageSize: 10, sort: 'desc', sidx: '' } | |
| 133 | + } | |
| 134 | + }, | |
| 135 | + computed: { | |
| 136 | + visibleProxy: { get() { return this.visible }, set(v) { this.$emit('update:visible', v) } } | |
| 137 | + }, | |
| 138 | + watch: { | |
| 139 | + visible(v) { if (v) { this.initData(); this.loadMembers(); this.loadStaff() } } | |
| 140 | + }, | |
| 141 | + methods: { | |
| 142 | + formatDate(ts) { | |
| 143 | + if (!ts) return '-' | |
| 144 | + if (typeof ts === 'string' && ts.includes('-')) return ts.substring(0, 10) | |
| 145 | + const d = new Date(typeof ts === 'number' ? ts : Number(ts)) | |
| 146 | + if (isNaN(d.getTime())) return '-' | |
| 147 | + return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}` | |
| 148 | + }, | |
| 149 | + formatMoney(v) { return v != null ? Number(v).toFixed(2) : '0.00' }, | |
| 150 | + loadMembers() { | |
| 151 | + const seen = new Map() | |
| 152 | + this.mockData.forEach(r => { if (!seen.has(r.memberPhone)) { seen.set(r.memberPhone, true); this.memberOptions.push({ value: r.memberPhone, label: `${r.hymc}(${r.memberPhone})` }) } }) | |
| 153 | + }, | |
| 154 | + searchMember(kw) { | |
| 155 | + if (!kw) return | |
| 156 | + this.memberLoading = true | |
| 157 | + setTimeout(() => { | |
| 158 | + const lower = kw.toLowerCase() | |
| 159 | + this.memberOptions = this.mockData | |
| 160 | + .filter(r => r.hymc.toLowerCase().includes(lower) || r.memberPhone.includes(kw)) | |
| 161 | + .reduce((acc, r) => { if (!acc.find(a => a.value === r.memberPhone)) acc.push({ value: r.memberPhone, label: `${r.hymc}(${r.memberPhone})` }); return acc }, []) | |
| 162 | + this.memberLoading = false | |
| 163 | + }, 300) | |
| 164 | + }, | |
| 165 | + loadStaff() { | |
| 166 | + const jksSet = new Map(), kjbSet = new Map() | |
| 167 | + this.mockData.forEach(r => { | |
| 168 | + if (r.jksName) r.jksName.split(',').forEach(n => { const name = n.trim(); if (name && !jksSet.has(name)) { jksSet.set(name, true); this.jksOptions.push({ id: name, fullName: name }) } }) | |
| 169 | + if (r.kjbName) r.kjbName.split(',').forEach(n => { const name = n.trim(); if (name && !kjbSet.has(name)) { kjbSet.set(name, true); this.kjbOptions.push({ id: name, fullName: name }) } }) | |
| 170 | + }) | |
| 171 | + }, | |
| 172 | + initData() { | |
| 173 | + this.loading = true | |
| 174 | + setTimeout(() => { | |
| 175 | + let filtered = [...this.mockData] | |
| 176 | + if (this.query.hksj && this.query.hksj.length === 2) { | |
| 177 | + const [s, e] = this.query.hksj | |
| 178 | + filtered = filtered.filter(r => { const t = new Date(r.hksj).getTime(); return t >= s && t <= e + 86400000 }) | |
| 179 | + } | |
| 180 | + if (this.query.hy) filtered = filtered.filter(r => r.memberPhone === this.query.hy) | |
| 181 | + if (this.query.jksId) filtered = filtered.filter(r => r.jksName && r.jksName.includes(this.query.jksId)) | |
| 182 | + if (this.query.kjblsId) filtered = filtered.filter(r => r.kjbName && r.kjbName.includes(this.query.kjblsId)) | |
| 183 | + if (this.query.xfje) filtered = filtered.filter(r => r.xfje === this.query.xfje) | |
| 184 | + if (this.query.sgfy) filtered = filtered.filter(r => r.sgfy === this.query.sgfy) | |
| 185 | + if (this.query.isEffective) filtered = filtered.filter(r => String(r.isEffective) === this.query.isEffective) | |
| 186 | + this.total = filtered.length | |
| 187 | + const start = (this.listQuery.currentPage - 1) * this.listQuery.pageSize | |
| 188 | + this.list = filtered.slice(start, start + this.listQuery.pageSize) | |
| 189 | + this.loading = false | |
| 190 | + }, 300) | |
| 191 | + }, | |
| 192 | + search() { this.listQuery.currentPage = 1; this.initData() }, | |
| 193 | + reset() { for (const k in this.query) this.query[k] = undefined; this.listQuery = { currentPage: 1, pageSize: 10, sort: 'desc', sidx: '' }; this.initData() } | |
| 194 | + } | |
| 195 | +} | |
| 196 | +</script> | |
| 197 | + | |
| 198 | +<style lang="scss" scoped> | |
| 199 | +::v-deep .consume-list-dialog { max-width: 1600px; margin-top: 3vh !important; border-radius: 20px; padding: 0; background: radial-gradient(circle at 0 0, rgba(255,255,255,0.96) 0, rgba(248,250,252,0.98) 40%, rgba(241,245,249,0.98) 100%); box-shadow: 0 24px 48px rgba(15,23,42,0.18), 0 0 0 1px rgba(255,255,255,0.9); backdrop-filter: blur(22px); -webkit-backdrop-filter: blur(22px); } | |
| 200 | +::v-deep .el-dialog__header { display: none; } | |
| 201 | +::v-deep .el-dialog__body { padding: 0; } | |
| 202 | +.dialog-inner { display: flex; flex-direction: column; max-height: 92vh; } | |
| 203 | +.dialog-header { flex-shrink: 0; display: flex; align-items: center; justify-content: space-between; margin: 18px 22px 0; padding: 10px 14px; border-radius: 14px; background: rgba(219,234,254,0.96); } | |
| 204 | +.dialog-title { font-size: 17px; font-weight: 600; color: #0f172a; } | |
| 205 | +.dialog-close { cursor: pointer; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border-radius: 999px; color: #64748b; transition: all 0.15s; &:hover { background: rgba(0,0,0,0.06); color: #0f172a; } } | |
| 206 | +.dialog-search { flex-shrink: 0; padding: 12px 22px 4px; } | |
| 207 | +.dialog-content { flex: 1; min-height: 0; overflow: auto; padding: 0 22px; } | |
| 208 | +.dialog-footer { flex-shrink: 0; display: flex; align-items: center; justify-content: flex-end; padding: 10px 22px 14px; border-top: 1px solid rgba(229,231,235,0.6); } | |
| 209 | +.expand-box { padding: 14px; background: #fafafa; border-radius: 8px; margin: 6px 0; .expand-title { display: flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 600; color: #303133; margin-bottom: 10px; i { color: #409EFF; } } } | |
| 210 | +.expand-empty { padding: 16px; text-align: center; color: #909399; font-size: 13px; } | |
| 211 | +::v-deep .consume-list-dialog .el-input__inner { border-radius: 999px; height: 32px; line-height: 32px; border-color: #e5e7eb; background-color: #f9fafb; &:focus { border-color: #2563eb; box-shadow: 0 0 0 1px rgba(37,99,235,0.18); } } | |
| 212 | +::v-deep .consume-list-dialog .el-button--primary { border-radius: 999px; background: #2563eb; border-color: #2563eb; box-shadow: 0 4px 10px rgba(37,99,235,0.35); } | |
| 213 | +::v-deep .consume-list-dialog .el-button--default { border-radius: 999px; } | |
| 214 | +::v-deep .consume-list-dialog .el-form-item { margin-bottom: 8px; } | |
| 215 | +::v-deep .consume-list-dialog .el-form-item__label { white-space: nowrap; } | |
| 216 | +::v-deep .consume-list-dialog .el-range-editor.el-input__inner { border-radius: 999px; } | |
| 217 | +</style> | ... | ... |
store-pc/src/components/InviteListDialog.vue
0 → 100644
| 1 | +<template> | |
| 2 | + <el-dialog | |
| 3 | + :visible.sync="visibleProxy" | |
| 4 | + :show-close="false" | |
| 5 | + width="90%" | |
| 6 | + :close-on-click-modal="false" | |
| 7 | + custom-class="invite-list-dialog" | |
| 8 | + append-to-body | |
| 9 | + > | |
| 10 | + <div class="dialog-inner"> | |
| 11 | + <div class="dialog-header"> | |
| 12 | + <div class="dialog-title">邀约记录</div> | |
| 13 | + <span class="dialog-close" @click="visibleProxy = false"><i class="el-icon-close"></i></span> | |
| 14 | + </div> | |
| 15 | + | |
| 16 | + <div class="dialog-search"> | |
| 17 | + <el-form @submit.native.prevent :inline="true" size="small"> | |
| 18 | + <el-form-item label="邀约时间"> | |
| 19 | + <el-date-picker v-model="query.yysj" type="daterange" value-format="timestamp" format="yyyy-MM-dd" start-placeholder="开始" end-placeholder="结束" style="width:220px" /> | |
| 20 | + </el-form-item> | |
| 21 | + <el-form-item label="邀约客户"> | |
| 22 | + <el-select v-model="query.yykh" filterable remote reserve-keyword clearable placeholder="搜索客户" :remote-method="searchMember" :loading="memberLoading" style="width:200px"> | |
| 23 | + <el-option v-for="m in memberOptions" :key="m.value" :label="m.label" :value="m.value" /> | |
| 24 | + </el-select> | |
| 25 | + </el-form-item> | |
| 26 | + <template v-if="showAll"> | |
| 27 | + <el-form-item label="电话是否有效"> | |
| 28 | + <el-select v-model="query.dhsfyx" placeholder="请选择" clearable style="width:120px"> | |
| 29 | + <el-option label="是" value="是" /><el-option label="否" value="否" /> | |
| 30 | + </el-select> | |
| 31 | + </el-form-item> | |
| 32 | + <el-form-item label="联系时间"> | |
| 33 | + <el-date-picker v-model="query.lxsj" type="daterange" value-format="timestamp" format="yyyy-MM-dd" start-placeholder="开始" end-placeholder="结束" style="width:220px" /> | |
| 34 | + </el-form-item> | |
| 35 | + </template> | |
| 36 | + <el-form-item> | |
| 37 | + <el-button type="primary" @click="search">查询</el-button> | |
| 38 | + <el-button @click="reset">重置</el-button> | |
| 39 | + <el-button type="text" @click="showAll = !showAll"> | |
| 40 | + {{ showAll ? '收起' : '展开' }} | |
| 41 | + <i :class="showAll ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"></i> | |
| 42 | + </el-button> | |
| 43 | + </el-form-item> | |
| 44 | + </el-form> | |
| 45 | + </div> | |
| 46 | + | |
| 47 | + <div class="dialog-content"> | |
| 48 | + <el-table v-loading="loading" :data="list" border size="small" :header-cell-style="{ background:'#f5f7fa', color:'#606266', fontWeight:600 }"> | |
| 49 | + <el-table-column prop="storeName" label="门店" show-overflow-tooltip /> | |
| 50 | + <el-table-column prop="yyrName" label="邀约人" /> | |
| 51 | + <el-table-column label="邀约时间" width="140"> | |
| 52 | + <template slot-scope="{ row }">{{ formatDate(row.yysj) }}</template> | |
| 53 | + </el-table-column> | |
| 54 | + <el-table-column prop="yykhxm" label="邀约客户" /> | |
| 55 | + <el-table-column label="电话是否有效" width="120"> | |
| 56 | + <template slot-scope="{ row }">{{ row.dhsfyx || '-' }}</template> | |
| 57 | + </el-table-column> | |
| 58 | + <el-table-column prop="tkbh" label="关联拓客号" width="160" show-overflow-tooltip /> | |
| 59 | + <el-table-column label="联系时间" width="140"> | |
| 60 | + <template slot-scope="{ row }">{{ formatDate(row.lxsj) }}</template> | |
| 61 | + </el-table-column> | |
| 62 | + <el-table-column prop="lxjl" label="联系记录" show-overflow-tooltip /> | |
| 63 | + </el-table> | |
| 64 | + </div> | |
| 65 | + | |
| 66 | + <div class="dialog-footer"> | |
| 67 | + <el-pagination | |
| 68 | + background | |
| 69 | + layout="total, sizes, prev, pager, next, jumper" | |
| 70 | + :total="total" | |
| 71 | + :page-size.sync="listQuery.pageSize" | |
| 72 | + :current-page.sync="listQuery.currentPage" | |
| 73 | + :page-sizes="[10, 20, 50, 100]" | |
| 74 | + @size-change="initData" | |
| 75 | + @current-change="initData" | |
| 76 | + /> | |
| 77 | + </div> | |
| 78 | + </div> | |
| 79 | + </el-dialog> | |
| 80 | +</template> | |
| 81 | + | |
| 82 | +<script> | |
| 83 | +const MOCK_INVITE = [ | |
| 84 | + { id: '1', storeName: '金沙', yyrName: '何玲', yysj: '2026-02-11 21:45:11', yykhxm: '李芳', dhsfyx: '是', lxsj: '2026-02-11 21:45:19', lxjl: '已约', tkbh: '' }, | |
| 85 | + { id: '2', storeName: '金沙', yyrName: '何玲', yysj: '2026-02-11 21:44:34', yykhxm: '欧玉蓉', dhsfyx: '是', lxsj: '2026-02-11 21:44:39', lxjl: '已约', tkbh: '' }, | |
| 86 | + { id: '3', storeName: '金沙', yyrName: '何玲', yysj: '2026-02-11 21:43:57', yykhxm: '贺憨憨', dhsfyx: '是', lxsj: '2026-02-11 21:44:02', lxjl: '已约', tkbh: '' }, | |
| 87 | + { id: '4', storeName: '金沙', yyrName: '何玲', yysj: '2026-02-11 21:43:20', yykhxm: '王好', dhsfyx: '是', lxsj: '2026-02-11 21:43:24', lxjl: '已约', tkbh: '' }, | |
| 88 | + { id: '5', storeName: '南湖', yyrName: '郝莉娜', yysj: '2026-02-11 21:18:45', yykhxm: '林小芊', dhsfyx: '是', lxsj: '2026-02-11 21:18:52', lxjl: '1', tkbh: '' }, | |
| 89 | + { id: '6', storeName: '金沙', yyrName: '蒲艳婷', yysj: '2026-02-11 20:51:40', yykhxm: '李科', dhsfyx: '是', lxsj: '2026-02-11 20:51:44', lxjl: '已约', tkbh: '' }, | |
| 90 | + { id: '7', storeName: '金沙', yyrName: '蒲艳婷', yysj: '2026-02-11 20:51:03', yykhxm: '张亚琼', dhsfyx: '是', lxsj: '2026-02-11 20:51:07', lxjl: '已约', tkbh: '' }, | |
| 91 | + { id: '8', storeName: '金沙', yyrName: '蒲艳婷', yysj: '2026-02-11 20:50:18', yykhxm: '皮丹', dhsfyx: '是', lxsj: '2026-02-11 20:50:30', lxjl: '已约', tkbh: '' }, | |
| 92 | + { id: '9', storeName: '金沙', yyrName: '蒲艳婷', yysj: '2026-02-11 20:49:35', yykhxm: '何欢', dhsfyx: '是', lxsj: '2026-02-11 20:49:40', lxjl: '已约', tkbh: '' }, | |
| 93 | + { id: '10', storeName: '明信', yyrName: '冷忠翠', yysj: '2026-02-11 19:44:42', yykhxm: '赵玲艳', dhsfyx: '是', lxsj: '2026-02-11 19:44:52', lxjl: '下午5.30', tkbh: '' } | |
| 94 | +] | |
| 95 | + | |
| 96 | +export default { | |
| 97 | + name: 'InviteListDialog', | |
| 98 | + props: { visible: { type: Boolean, default: false } }, | |
| 99 | + data() { | |
| 100 | + return { | |
| 101 | + showAll: false, | |
| 102 | + loading: false, | |
| 103 | + memberLoading: false, | |
| 104 | + memberOptions: [], | |
| 105 | + list: [], | |
| 106 | + total: 0, | |
| 107 | + mockData: MOCK_INVITE, | |
| 108 | + query: { yysj: undefined, yykh: undefined, dhsfyx: undefined, lxsj: undefined }, | |
| 109 | + listQuery: { currentPage: 1, pageSize: 10, sort: 'desc', sidx: '' } | |
| 110 | + } | |
| 111 | + }, | |
| 112 | + computed: { | |
| 113 | + visibleProxy: { | |
| 114 | + get() { return this.visible }, | |
| 115 | + set(v) { this.$emit('update:visible', v) } | |
| 116 | + } | |
| 117 | + }, | |
| 118 | + watch: { | |
| 119 | + visible(v) { if (v) { this.initData(); this.loadMembers() } } | |
| 120 | + }, | |
| 121 | + methods: { | |
| 122 | + formatDate(ts) { | |
| 123 | + if (!ts) return '-' | |
| 124 | + if (typeof ts === 'string' && ts.includes('-')) return ts.substring(0, 10) | |
| 125 | + const d = new Date(typeof ts === 'number' ? ts : Number(ts)) | |
| 126 | + if (isNaN(d.getTime())) return '-' | |
| 127 | + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` | |
| 128 | + }, | |
| 129 | + loadMembers() { | |
| 130 | + const map = new Map() | |
| 131 | + this.mockData.forEach(r => { if (!map.has(r.yykhxm)) map.set(r.yykhxm, { value: r.id, label: r.yykhxm }) }) | |
| 132 | + this.memberOptions = [...map.values()] | |
| 133 | + }, | |
| 134 | + searchMember(keyword) { | |
| 135 | + if (!keyword) { this.loadMembers(); return } | |
| 136 | + this.memberLoading = true | |
| 137 | + setTimeout(() => { | |
| 138 | + this.memberOptions = this.mockData | |
| 139 | + .filter(r => r.yykhxm.includes(keyword)) | |
| 140 | + .map(r => ({ value: r.id, label: r.yykhxm })) | |
| 141 | + .filter((v, i, a) => a.findIndex(t => t.label === v.label) === i) | |
| 142 | + this.memberLoading = false | |
| 143 | + }, 200) | |
| 144 | + }, | |
| 145 | + initData() { | |
| 146 | + this.loading = true | |
| 147 | + setTimeout(() => { | |
| 148 | + let filtered = [...this.mockData] | |
| 149 | + if (this.query.yykh) { | |
| 150 | + const selected = this.memberOptions.find(m => m.value === this.query.yykh) | |
| 151 | + if (selected) filtered = filtered.filter(r => r.yykhxm === selected.label) | |
| 152 | + } | |
| 153 | + if (this.query.dhsfyx) filtered = filtered.filter(r => r.dhsfyx === this.query.dhsfyx) | |
| 154 | + this.total = filtered.length | |
| 155 | + const start = (this.listQuery.currentPage - 1) * this.listQuery.pageSize | |
| 156 | + this.list = filtered.slice(start, start + this.listQuery.pageSize) | |
| 157 | + this.loading = false | |
| 158 | + }, 300) | |
| 159 | + }, | |
| 160 | + search() { this.listQuery.currentPage = 1; this.initData() }, | |
| 161 | + reset() { | |
| 162 | + for (const k in this.query) this.query[k] = undefined | |
| 163 | + this.listQuery = { currentPage: 1, pageSize: 10, sort: 'desc', sidx: '' } | |
| 164 | + this.initData() | |
| 165 | + } | |
| 166 | + } | |
| 167 | +} | |
| 168 | +</script> | |
| 169 | + | |
| 170 | +<style lang="scss" scoped> | |
| 171 | +::v-deep .invite-list-dialog { max-width: 1600px; margin-top: 3vh !important; border-radius: 20px; padding: 0; background: radial-gradient(circle at 0 0, rgba(255,255,255,0.96) 0, rgba(248,250,252,0.98) 40%, rgba(241,245,249,0.98) 100%); box-shadow: 0 24px 48px rgba(15,23,42,0.18), 0 0 0 1px rgba(255,255,255,0.9); backdrop-filter: blur(22px); -webkit-backdrop-filter: blur(22px); } | |
| 172 | +::v-deep .el-dialog__header { display: none; } | |
| 173 | +::v-deep .el-dialog__body { padding: 0; } | |
| 174 | +.dialog-inner { display: flex; flex-direction: column; max-height: 92vh; } | |
| 175 | +.dialog-header { flex-shrink: 0; display: flex; align-items: center; justify-content: space-between; margin: 18px 22px 0; padding: 10px 14px; border-radius: 14px; background: rgba(219,234,254,0.96); } | |
| 176 | +.dialog-title { font-size: 17px; font-weight: 600; color: #0f172a; } | |
| 177 | +.dialog-close { cursor: pointer; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border-radius: 999px; color: #64748b; transition: all 0.15s; &:hover { background: rgba(0,0,0,0.06); color: #0f172a; } } | |
| 178 | +.dialog-search { flex-shrink: 0; padding: 12px 22px 4px; } | |
| 179 | +.dialog-content { flex: 1; min-height: 0; overflow: auto; padding: 0 22px; } | |
| 180 | +.dialog-footer { flex-shrink: 0; display: flex; align-items: center; justify-content: flex-end; padding: 10px 22px 14px; border-top: 1px solid rgba(229,231,235,0.6); } | |
| 181 | +::v-deep .invite-list-dialog .el-input__inner { border-radius: 999px; height: 32px; line-height: 32px; border-color: #e5e7eb; background-color: #f9fafb; &:focus { border-color: #2563eb; box-shadow: 0 0 0 1px rgba(37,99,235,0.18); } } | |
| 182 | +::v-deep .invite-list-dialog .el-button--primary { border-radius: 999px; background: #2563eb; border-color: #2563eb; box-shadow: 0 4px 10px rgba(37,99,235,0.35); } | |
| 183 | +::v-deep .invite-list-dialog .el-button--default { border-radius: 999px; } | |
| 184 | +::v-deep .invite-list-dialog .el-form-item { margin-bottom: 8px; } | |
| 185 | +::v-deep .invite-list-dialog .el-form-item__label { white-space: nowrap; } | |
| 186 | +::v-deep .invite-list-dialog .el-range-editor.el-input__inner { border-radius: 999px; } | |
| 187 | +</style> | ... | ... |
store-pc/src/components/MemberProfileDialog.vue
| ... | ... | @@ -7,256 +7,313 @@ |
| 7 | 7 | custom-class="member-dialog" |
| 8 | 8 | append-to-body |
| 9 | 9 | > |
| 10 | + <span class="dialog-close-btn" @click="close"><i class="el-icon-close"></i></span> | |
| 11 | + | |
| 10 | 12 | <div class="member-dialog-inner"> |
| 11 | - <!-- 头部:姓名 + 等级 + 手机 + 门店 + 状态 --> | |
| 12 | - <div class="member-header"> | |
| 13 | - <div class="member-avatar"><span>{{ initials }}</span></div> | |
| 14 | - <div class="member-basic"> | |
| 15 | - <div class="name-line"> | |
| 16 | - <span class="name">{{ displayName }}</span> | |
| 17 | - <span v-if="member.consumeLevelName || member.levelName" class="level-tag">{{ member.consumeLevelName || member.levelName }}</span> | |
| 13 | + <!-- 左侧边栏 --> | |
| 14 | + <div class="sidebar"> | |
| 15 | + <!-- 会员身份区 --> | |
| 16 | + <div class="sidebar-identity"> | |
| 17 | + <div class="member-avatar"><span>{{ initials }}</span></div> | |
| 18 | + <div class="member-name">{{ displayName }}</div> | |
| 19 | + <div class="member-sub-info"> | |
| 20 | + <span v-if="member.sjh" class="member-phone"><i class="el-icon-phone"></i>{{ member.sjh }}</span> | |
| 21 | + <span v-if="member.dah" class="member-dah">档案号:{{ member.dah }}</span> | |
| 22 | + </div> | |
| 23 | + <span v-if="consumeLevelText" :class="['level-tag', consumeLevelClass]">{{ consumeLevelText }}</span> | |
| 24 | + <div class="member-type-tags" v-if="memberTypeList.length"> | |
| 25 | + <span v-for="t in memberTypeList" :key="t.type" :class="['type-tag', 'type-tag--' + t.type]">{{ t.label }}会员</span> | |
| 18 | 26 | </div> |
| 19 | - <div class="sub-line"> | |
| 27 | + <div class="member-meta"> | |
| 20 | 28 | <span v-if="member.xb">{{ member.xb }}</span> |
| 21 | - <span v-if="member.sjh"> · {{ member.sjh }}</span> | |
| 22 | - <span v-if="member.gsmdName || member.storeName"> · {{ member.gsmdName || member.storeName }}</span> | |
| 29 | + <span v-if="member.gsmdName || member.storeName">{{ member.gsmdName || member.storeName }}</span> | |
| 30 | + </div> | |
| 31 | + <div class="member-visit-row"> | |
| 32 | + <el-tooltip v-if="member.lastVisitTime || member.lastVisit" :content="member.lastVisitTime || member.lastVisit" placement="right" effect="dark"> | |
| 33 | + <span class="member-visit"><i class="el-icon-time"></i>最后到店:{{ relativeTime(member.lastVisitTime || member.lastVisit) }}</span> | |
| 34 | + </el-tooltip> | |
| 35 | + <span v-else class="member-visit"><i class="el-icon-time"></i>最后到店:—</span> | |
| 36 | + <span class="member-sleep" v-if="member.sleepDays > 0"><i class="el-icon-warning"></i>沉睡 {{ member.sleepDays }} 天</span> | |
| 23 | 37 | </div> |
| 24 | - <div class="status-sub">最后到店:{{ member.lastVisitTime || member.lastVisit || '—' }}</div> | |
| 25 | 38 | <div v-if="Array.isArray(member.tags) && member.tags.length" class="tag-row"> |
| 26 | 39 | <span v-for="tag in member.tags" :key="tag" class="member-tag">{{ tag }}</span> |
| 27 | 40 | </div> |
| 28 | 41 | </div> |
| 42 | + | |
| 43 | + <!-- 消费统计 --> | |
| 44 | + <div class="sidebar-section"> | |
| 45 | + <div class="sidebar-section-title"><i class="el-icon-wallet"></i>消费统计</div> | |
| 46 | + <div class="sidebar-row"> | |
| 47 | + <span class="sidebar-label">耗卡总金额</span> | |
| 48 | + <span class="sidebar-value sidebar-value--amount">¥{{ formatMoney(member.totalConsumeAmount) }}</span> | |
| 49 | + </div> | |
| 50 | + <div class="sidebar-row"> | |
| 51 | + <span class="sidebar-label">开卡总金额</span> | |
| 52 | + <span class="sidebar-value sidebar-value--amount">¥{{ formatMoney(member.totalBillingAmount) }}</span> | |
| 53 | + </div> | |
| 54 | + <div class="sidebar-row sidebar-row--highlight"> | |
| 55 | + <span class="sidebar-label">剩余权益</span> | |
| 56 | + <span class="sidebar-value sidebar-value--gradient">¥{{ formatMoney(member.remainingRightsAmount) }}</span> | |
| 57 | + </div> | |
| 58 | + </div> | |
| 59 | + | |
| 60 | + <!-- 基本信息 --> | |
| 61 | + <div class="sidebar-section"> | |
| 62 | + <div class="sidebar-section-title"><i class="el-icon-user"></i>基本信息</div> | |
| 63 | + <div class="sidebar-row"> | |
| 64 | + <span class="sidebar-label"><i class="el-icon-date"></i>生日</span> | |
| 65 | + <span class="sidebar-value">{{ formattedBirthday }}</span> | |
| 66 | + </div> | |
| 67 | + <div class="sidebar-row"> | |
| 68 | + <span class="sidebar-label">客户类型</span> | |
| 69 | + <span class="sidebar-value">{{ member.khlxName || '—' }}</span> | |
| 70 | + </div> | |
| 71 | + <div class="sidebar-row"> | |
| 72 | + <span class="sidebar-label">注册时间</span> | |
| 73 | + <span class="sidebar-value">{{ member.zcsj || '—' }}</span> | |
| 74 | + </div> | |
| 75 | + <div class="sidebar-row"> | |
| 76 | + <span class="sidebar-label">首次到店</span> | |
| 77 | + <span class="sidebar-value">{{ member.firstVisitTime || '—' }}</span> | |
| 78 | + </div> | |
| 79 | + <div class="sidebar-row"> | |
| 80 | + <span class="sidebar-label">进店渠道</span> | |
| 81 | + <span class="sidebar-value">{{ member.jdqd || '—' }}</span> | |
| 82 | + </div> | |
| 83 | + <div class="sidebar-row"> | |
| 84 | + <span class="sidebar-label">推荐人</span> | |
| 85 | + <span class="sidebar-value">{{ member.tjrName || '—' }}</span> | |
| 86 | + </div> | |
| 87 | + <div class="sidebar-row"> | |
| 88 | + <span class="sidebar-label">健康师</span> | |
| 89 | + <span class="sidebar-value">{{ member.mainHealthUserName || '—' }}</span> | |
| 90 | + </div> | |
| 91 | + <div class="sidebar-row"> | |
| 92 | + <span class="sidebar-label">负责顾问</span> | |
| 93 | + <span class="sidebar-value">{{ member.subHealthUserName || '—' }}</span> | |
| 94 | + </div> | |
| 95 | + <div class="sidebar-row"> | |
| 96 | + <span class="sidebar-label">拓客人员</span> | |
| 97 | + <span class="sidebar-value">{{ member.expandUserName || '—' }}</span> | |
| 98 | + </div> | |
| 99 | + <div class="sidebar-row"> | |
| 100 | + <span class="sidebar-label">联系地址</span> | |
| 101 | + <span class="sidebar-value">{{ member.lxdz || member.address || '—' }}</span> | |
| 102 | + </div> | |
| 103 | + <div class="sidebar-row sidebar-row--remark"> | |
| 104 | + <span class="sidebar-label">备注</span> | |
| 105 | + <span class="sidebar-value">{{ member.bz || member.remark || '—' }}</span> | |
| 106 | + </div> | |
| 107 | + </div> | |
| 29 | 108 | </div> |
| 30 | 109 | |
| 31 | - <div class="member-body"> | |
| 32 | - <div class="row-one"> | |
| 33 | - <div class="block basic-info"> | |
| 34 | - <div class="section-title"><i class="el-icon-user section-icon"></i><span>基本信息</span></div> | |
| 35 | - <div class="info-grid"> | |
| 36 | - <div class="info-cell"><span class="l">客户编码</span><span class="v">{{ member.id || '—' }}</span></div> | |
| 37 | - <div class="info-cell"><span class="l">档案号</span><span class="v">{{ member.dah || '—' }}</span></div> | |
| 38 | - <div class="info-cell"><span class="l">客户名称</span><span class="v">{{ member.khmc || '—' }}</span></div> | |
| 39 | - <div class="info-cell"><span class="l">手机号</span><span class="v">{{ member.sjh || '—' }}</span></div> | |
| 40 | - <div class="info-cell"><span class="l">性别</span><span class="v">{{ member.xb || '—' }}</span></div> | |
| 41 | - <div class="info-cell"><span class="l">归属门店</span><span class="v">{{ member.gsmdName || member.storeName || '—' }}</span></div> | |
| 42 | - <div class="info-cell"><span class="l">客户类型</span><span class="v">{{ member.khlxName || '—' }}</span></div> | |
| 43 | - <div class="info-cell"><span class="l">注册时间</span><span class="v">{{ member.zcsj || '—' }}</span></div> | |
| 44 | - <div class="info-cell"><span class="l">进店渠道</span><span class="v">{{ member.jdqd || '—' }}</span></div> | |
| 45 | - <div class="info-cell"><span class="l">推荐人</span><span class="v">{{ member.tjrName || '—' }}</span></div> | |
| 46 | - <div class="info-cell"><span class="l">健康师</span><span class="v">{{ member.mainHealthUserName || '—' }}</span></div> | |
| 47 | - <div class="info-cell"><span class="l">负责顾问</span><span class="v">{{ member.subHealthUserName || '—' }}</span></div> | |
| 48 | - <div class="info-cell"><span class="l">拓客人员</span><span class="v">{{ member.expandUserName || '—' }}</span></div> | |
| 49 | - <div class="info-cell"><span class="l">联系地址</span><span class="v">{{ member.lxdz || member.address || '—' }}</span></div> | |
| 50 | - <div class="info-cell remark-cell"><span class="l">备注</span><span class="v">{{ member.bz || member.remark || '—' }}</span></div> | |
| 51 | - </div> | |
| 110 | + <!-- 右侧内容区 --> | |
| 111 | + <div class="main-content"> | |
| 112 | + <!-- Tab区 + 搜索 --> | |
| 113 | + <div class="content-header"> | |
| 114 | + <div class="records-tab-list"> | |
| 115 | + <div class="records-tab-indicator" :style="recordsTabIndicatorStyle" /> | |
| 116 | + <button | |
| 117 | + v-for="tab in recordTabs" | |
| 118 | + :key="tab.key" | |
| 119 | + class="records-tab" | |
| 120 | + :class="{ 'records-tab--active': recordsTab === tab.key }" | |
| 121 | + @click="recordsTab = tab.key" | |
| 122 | + > | |
| 123 | + <span class="records-tab-label">{{ tab.label }}</span> | |
| 124 | + </button> | |
| 52 | 125 | </div> |
| 53 | - <div class="block stats-block"> | |
| 54 | - <div class="section-title"><i class="el-icon-wallet section-icon"></i><span>消费与到店</span></div> | |
| 55 | - <div class="stats-mini"> | |
| 56 | - <div class="s"><span class="sl">耗卡总金额</span><span class="sv">¥{{ formatMoney(member.totalConsumeAmount) }}</span></div> | |
| 57 | - <div class="s"><span class="sl">开卡总金额</span><span class="sv">¥{{ formatMoney(member.totalBillingAmount) }}</span></div> | |
| 58 | - <div class="s"><span class="sl">剩余权益总金额</span><span class="sv primary">¥{{ formatMoney(member.remainingRightsAmount) }}</span></div> | |
| 59 | - <div class="s"><span class="sl">到店天数</span><span class="sv">{{ member.visitDays || 0 }} 天</span></div> | |
| 60 | - <div class="s"><span class="sl">沉睡天数</span><span class="sv">{{ member.sleepDays || 0 }} 天</span></div> | |
| 61 | - <div class="s"><span class="sl">最后到店</span><span class="sv">{{ member.lastVisitTime || member.lastVisit || '—' }}</span></div> | |
| 62 | - </div> | |
| 63 | - <div class="section-title section-gap"><i class="el-icon-trophy section-icon"></i><span>等级与生日</span></div> | |
| 64 | - <div class="stats-mini"> | |
| 65 | - <div class="s"><span class="sl">消费等级</span><span class="sv">{{ member.consumeLevelName || member.consumeLevel || '—' }}</span></div> | |
| 66 | - <div class="s"><span class="sl">阳历生日</span><span class="sv">{{ member.yanglsr || '—' }}</span></div> | |
| 67 | - <div class="s"><span class="sl">阴历生日</span><span class="sv">{{ member.yinlsr || '—' }}</span></div> | |
| 68 | - <div class="s"><span class="sl">生美/医美/科技/教育</span><span class="sv">{{ memberTypeSummary }}</span></div> | |
| 69 | - </div> | |
| 126 | + <div class="records-tabs-search"> | |
| 127 | + <el-input | |
| 128 | + v-model="currentSearch" | |
| 129 | + size="mini" | |
| 130 | + clearable | |
| 131 | + :placeholder="currentSearchPlaceholder" | |
| 132 | + class="records-search" | |
| 133 | + > | |
| 134 | + <i slot="prefix" class="el-icon-search"></i> | |
| 135 | + </el-input> | |
| 70 | 136 | </div> |
| 71 | 137 | </div> |
| 72 | 138 | |
| 73 | - <div class="records-area block"> | |
| 74 | - <div class="records-tabs-header"> | |
| 75 | - <div class="records-tab-list"> | |
| 76 | - <div class="records-tab-indicator" :style="recordsTabIndicatorStyle" /> | |
| 77 | - <button | |
| 78 | - v-for="tab in recordTabs" | |
| 79 | - :key="tab.key" | |
| 80 | - class="records-tab" | |
| 81 | - :class="{ 'records-tab--active': recordsTab === tab.key }" | |
| 82 | - @click="recordsTab = tab.key" | |
| 83 | - > | |
| 84 | - <span class="records-tab-label">{{ tab.label }}</span> | |
| 85 | - </button> | |
| 139 | + <!-- 表格区 --> | |
| 140 | + <div class="content-body"> | |
| 141 | + <!-- 权益明细 --> | |
| 142 | + <div v-if="recordsTab === 'rights'" class="records-panel"> | |
| 143 | + <div class="records-table-wrap"> | |
| 144 | + <el-table :data="filteredRemainingItems" size="mini" class="rights-table" empty-text="暂无权益"> | |
| 145 | + <el-table-column prop="ItemName" label="项目名称" min-width="110" align="center" /> | |
| 146 | + <el-table-column prop="ItemPrice" label="单价" width="80" align="center"> | |
| 147 | + <template slot-scope="scope"><span class="amount-text">¥{{ formatMoney(scope.row.ItemPrice) }}</span></template> | |
| 148 | + </el-table-column> | |
| 149 | + <el-table-column prop="SourceType" label="来源" width="65" align="center" /> | |
| 150 | + <el-table-column prop="TotalPurchased" label="总购买" width="65" align="center" /> | |
| 151 | + <el-table-column prop="ConsumedCount" label="已消费" width="65" align="center" /> | |
| 152 | + <el-table-column prop="RefundedCount" label="已退款" width="65" align="center" /> | |
| 153 | + <el-table-column prop="DeductCount" label="已扣除" width="65" align="center" /> | |
| 154 | + <el-table-column prop="RemainingCount" label="剩余" width="65" align="center"> | |
| 155 | + <template slot-scope="scope"> | |
| 156 | + <span class="num-remaining">{{ scope.row.RemainingCount ?? scope.row.remainingCount ?? 0 }}</span> | |
| 157 | + </template> | |
| 158 | + </el-table-column> | |
| 159 | + <el-table-column label="剩余价值" width="90" align="center"> | |
| 160 | + <template slot-scope="scope"> | |
| 161 | + <span class="num-total">¥{{ formatMoney(remainingItemTotal(scope.row)) }}</span> | |
| 162 | + </template> | |
| 163 | + </el-table-column> | |
| 164 | + </el-table> | |
| 86 | 165 | </div> |
| 87 | - <div class="records-tabs-search"> | |
| 88 | - <el-input | |
| 89 | - v-model="currentSearch" | |
| 90 | - size="mini" | |
| 91 | - clearable | |
| 92 | - :placeholder="currentSearchPlaceholder" | |
| 93 | - class="records-search" | |
| 94 | - > | |
| 95 | - <i slot="prefix" class="el-icon-search"></i> | |
| 96 | - </el-input> | |
| 166 | + <div class="records-pagination"> | |
| 167 | + <el-pagination layout="prev, pager, next" :page-size="pageSize" :total="rightsTotalCount" :current-page.sync="rightsPage" small background /> | |
| 97 | 168 | </div> |
| 98 | 169 | </div> |
| 99 | 170 | |
| 100 | - <div v-if="recordsTab === 'invite'" class="records-panel"> | |
| 171 | + <!-- 邀约记录 --> | |
| 172 | + <div v-else-if="recordsTab === 'invite'" class="records-panel"> | |
| 101 | 173 | <div class="records-table-wrap"> |
| 102 | - <el-table :data="filteredInviteRecords" border size="mini" class="record-table" empty-text="暂无邀约记录"> | |
| 103 | - <el-table-column prop="inviteTime" label="邀约时间" width="150" align="center" /> | |
| 104 | - <el-table-column prop="inviteContent" label="邀约内容" min-width="140" align="center" /> | |
| 105 | - <el-table-column prop="inviter" label="邀约人" width="90" align="center" /> | |
| 106 | - <el-table-column prop="status" label="状态" width="80" align="center" /> | |
| 174 | + <el-table :data="filteredInviteRecords" size="mini" class="record-table" empty-text="暂无邀约记录"> | |
| 175 | + <el-table-column prop="InviteDate" label="邀约时间" width="140" align="center" /> | |
| 176 | + <el-table-column prop="StoreName" label="门店" width="80" align="center" /> | |
| 177 | + <el-table-column prop="InviterName" label="邀约人" width="75" align="center" /> | |
| 178 | + <el-table-column prop="ContactTime" label="联系时间" width="140" align="center" /> | |
| 179 | + <el-table-column prop="ContactRecord" label="联系记录" min-width="150" align="center" show-overflow-tooltip /> | |
| 180 | + <el-table-column prop="PhoneValid" label="电话有效" width="70" align="center" /> | |
| 107 | 181 | </el-table> |
| 108 | - <div class="records-pagination"> | |
| 109 | - <el-pagination | |
| 110 | - layout="prev, pager, next" | |
| 111 | - :page-size="pageSize" | |
| 112 | - :total="inviteTotalCount" | |
| 113 | - :current-page.sync="invitePage" | |
| 114 | - small | |
| 115 | - background | |
| 116 | - /> | |
| 117 | - </div> | |
| 182 | + </div> | |
| 183 | + <div class="records-pagination"> | |
| 184 | + <el-pagination layout="prev, pager, next" :page-size="pageSize" :total="inviteTotalCount" :current-page.sync="invitePage" small background /> | |
| 118 | 185 | </div> |
| 119 | 186 | </div> |
| 120 | 187 | |
| 188 | + <!-- 预约记录 --> | |
| 121 | 189 | <div v-else-if="recordsTab === 'booking'" class="records-panel"> |
| 122 | 190 | <div class="records-table-wrap"> |
| 123 | - <el-table :data="filteredBookingRecords" border size="mini" class="record-table" empty-text="暂无预约记录"> | |
| 124 | - <el-table-column prop="bookingTime" label="预约时间" width="150" align="center" /> | |
| 125 | - <el-table-column prop="projectName" label="预约项目" min-width="120" align="center" /> | |
| 126 | - <el-table-column prop="staffName" label="服务人员" width="90" align="center" /> | |
| 127 | - <el-table-column prop="status" label="状态" width="80" align="center" /> | |
| 191 | + <el-table :data="filteredBookingRecords" size="mini" class="record-table" empty-text="暂无预约记录"> | |
| 192 | + <el-table-column prop="AppointmentDate" label="预约时间" width="140" align="center" /> | |
| 193 | + <el-table-column prop="StoreName" label="门店" width="80" align="center" /> | |
| 194 | + <el-table-column prop="InviterName" label="邀约人" width="75" align="center" /> | |
| 195 | + <el-table-column prop="HealthCoachName" label="预约健康师" width="85" align="center" /> | |
| 196 | + <el-table-column prop="ExperienceItem" label="体验项目" min-width="100" align="center" show-overflow-tooltip /> | |
| 197 | + <el-table-column prop="Status" label="状态" width="70" align="center"> | |
| 198 | + <template slot-scope="scope"> | |
| 199 | + <span :class="['status-capsule', statusClass(scope.row.Status)]">{{ scope.row.Status }}</span> | |
| 200 | + </template> | |
| 201 | + </el-table-column> | |
| 202 | + <el-table-column prop="NoDealRemark" label="未成交说明" min-width="100" align="center" show-overflow-tooltip /> | |
| 128 | 203 | </el-table> |
| 129 | - <div class="records-pagination"> | |
| 130 | - <el-pagination | |
| 131 | - layout="prev, pager, next" | |
| 132 | - :page-size="pageSize" | |
| 133 | - :total="bookingTotalCount" | |
| 134 | - :current-page.sync="bookingPage" | |
| 135 | - small | |
| 136 | - background | |
| 137 | - /> | |
| 138 | - </div> | |
| 204 | + </div> | |
| 205 | + <div class="records-pagination"> | |
| 206 | + <el-pagination layout="prev, pager, next" :page-size="pageSize" :total="bookingTotalCount" :current-page.sync="bookingPage" small background /> | |
| 139 | 207 | </div> |
| 140 | 208 | </div> |
| 141 | 209 | |
| 210 | + <!-- 开单记录 --> | |
| 142 | 211 | <div v-else-if="recordsTab === 'billing'" class="records-panel"> |
| 143 | 212 | <div class="records-table-wrap"> |
| 144 | - <el-table :data="filteredBillingRecords" border size="mini" class="record-table" empty-text="暂无开单记录"> | |
| 145 | - <el-table-column prop="orderNo" label="单号" min-width="120" align="center" /> | |
| 146 | - <el-table-column prop="billingTime" label="开单时间" width="150" align="center" /> | |
| 147 | - <el-table-column prop="productName" label="项目/产品" min-width="120" align="center" /> | |
| 148 | - <el-table-column prop="amount" label="金额" width="90" align="center"> | |
| 149 | - <template slot-scope="scope">¥{{ formatMoney(scope.row.amount) }}</template> | |
| 213 | + <el-table :data="filteredBillingRecords" size="mini" class="record-table" empty-text="暂无开单记录"> | |
| 214 | + <el-table-column prop="BillingDate" label="开单日期" width="140" align="center" /> | |
| 215 | + <el-table-column prop="StoreName" label="门店" width="80" align="center" /> | |
| 216 | + <el-table-column prop="CreatorName" label="开单人员" width="75" align="center" /> | |
| 217 | + <el-table-column prop="HealthCoachNames" label="健康师" width="85" align="center" show-overflow-tooltip /> | |
| 218 | + <el-table-column prop="TechTeacherNames" label="科技老师" width="85" align="center" show-overflow-tooltip /> | |
| 219 | + <el-table-column prop="Items" label="开单品项" min-width="140" align="center" show-overflow-tooltip /> | |
| 220 | + <el-table-column label="实付金额" width="85" align="center"> | |
| 221 | + <template slot-scope="scope"><span class="amount-text">¥{{ formatMoney(scope.row.Amount) }}</span></template> | |
| 150 | 222 | </el-table-column> |
| 151 | - <el-table-column prop="operator" label="操作人" width="90" align="center" /> | |
| 223 | + <el-table-column label="欠款金额" width="85" align="center"> | |
| 224 | + <template slot-scope="scope"><span class="amount-text">¥{{ formatMoney(scope.row.DebtAmount) }}</span></template> | |
| 225 | + </el-table-column> | |
| 226 | + <el-table-column prop="ActivityName" label="活动名称" width="85" align="center" show-overflow-tooltip /> | |
| 152 | 227 | </el-table> |
| 153 | - <div class="records-pagination"> | |
| 154 | - <el-pagination | |
| 155 | - layout="prev, pager, next" | |
| 156 | - :page-size="pageSize" | |
| 157 | - :total="billingTotalCount" | |
| 158 | - :current-page.sync="billingPage" | |
| 159 | - small | |
| 160 | - background | |
| 161 | - /> | |
| 162 | - </div> | |
| 228 | + </div> | |
| 229 | + <div class="records-pagination"> | |
| 230 | + <el-pagination layout="prev, pager, next" :page-size="pageSize" :total="billingTotalCount" :current-page.sync="billingPage" small background /> | |
| 163 | 231 | </div> |
| 164 | 232 | </div> |
| 165 | 233 | |
| 234 | + <!-- 消耗记录 --> | |
| 166 | 235 | <div v-else-if="recordsTab === 'consume'" class="records-panel"> |
| 167 | 236 | <div class="records-table-wrap"> |
| 168 | - <el-table :data="filteredConsumeRecords" border size="mini" class="record-table" empty-text="暂无消耗记录"> | |
| 169 | - <el-table-column prop="consumeTime" label="消耗时间" width="150" align="center" /> | |
| 170 | - <el-table-column prop="itemName" label="项目名称" min-width="120" align="center" /> | |
| 171 | - <el-table-column prop="count" label="消耗次数" width="88" align="center" /> | |
| 172 | - <el-table-column prop="operator" label="操作人" width="90" align="center" /> | |
| 237 | + <el-table :data="filteredConsumeRecords" size="mini" class="record-table" empty-text="暂无消耗记录"> | |
| 238 | + <el-table-column prop="ConsumeDate" label="消耗日期" width="140" align="center" /> | |
| 239 | + <el-table-column prop="StoreName" label="门店" width="80" align="center" /> | |
| 240 | + <el-table-column prop="OperatorName" label="操作人员" width="75" align="center" /> | |
| 241 | + <el-table-column prop="HealthCoachNames" label="健康师" width="85" align="center" show-overflow-tooltip /> | |
| 242 | + <el-table-column prop="TechTeacherNames" label="科技老师" width="85" align="center" show-overflow-tooltip /> | |
| 243 | + <el-table-column prop="Items" label="消耗品项" min-width="140" align="center" show-overflow-tooltip /> | |
| 244 | + <el-table-column label="消耗金额" width="85" align="center"> | |
| 245 | + <template slot-scope="scope"><span class="amount-text">¥{{ formatMoney(scope.row.Amount) }}</span></template> | |
| 246 | + </el-table-column> | |
| 247 | + <el-table-column label="手工费" width="75" align="center"> | |
| 248 | + <template slot-scope="scope"><span class="amount-text">¥{{ formatMoney(scope.row.LaborCost) }}</span></template> | |
| 249 | + </el-table-column> | |
| 173 | 250 | </el-table> |
| 174 | - <div class="records-pagination"> | |
| 175 | - <el-pagination | |
| 176 | - layout="prev, pager, next" | |
| 177 | - :page-size="pageSize" | |
| 178 | - :total="consumeTotalCount" | |
| 179 | - :current-page.sync="consumePage" | |
| 180 | - small | |
| 181 | - background | |
| 182 | - /> | |
| 183 | - </div> | |
| 251 | + </div> | |
| 252 | + <div class="records-pagination"> | |
| 253 | + <el-pagination layout="prev, pager, next" :page-size="pageSize" :total="consumeTotalCount" :current-page.sync="consumePage" small background /> | |
| 184 | 254 | </div> |
| 185 | 255 | </div> |
| 186 | 256 | |
| 187 | - <div v-else-if="recordsTab === 'refund'" class="records-panel"> | |
| 257 | + <!-- 服务日志 --> | |
| 258 | + <div v-else-if="recordsTab === 'serviceLog'" class="records-panel"> | |
| 188 | 259 | <div class="records-table-wrap"> |
| 189 | - <el-table :data="filteredRefundRecords" border size="mini" class="record-table" empty-text="暂无退卡记录"> | |
| 190 | - <el-table-column prop="refundTime" label="退卡时间" width="150" align="center" /> | |
| 191 | - <el-table-column prop="orderNo" label="退卡单号" min-width="130" align="center" /> | |
| 192 | - <el-table-column prop="projectName" label="退卡项目" min-width="120" align="center" /> | |
| 193 | - <el-table-column prop="amount" label="退卡金额" width="90" align="center"> | |
| 194 | - <template slot-scope="scope">¥{{ formatMoney(scope.row.amount) }}</template> | |
| 195 | - </el-table-column> | |
| 196 | - <el-table-column prop="operator" label="操作人" width="90" align="center" /> | |
| 260 | + <el-table :data="filteredServiceLogRecords" size="mini" class="record-table" empty-text="暂无服务日志"> | |
| 261 | + <el-table-column prop="CreateTime" label="记录时间" width="140" align="center" /> | |
| 262 | + <el-table-column prop="CreatorName" label="添加人" width="75" align="center" /> | |
| 263 | + <el-table-column prop="Remark" label="备注" min-width="200" align="center" show-overflow-tooltip /> | |
| 264 | + <el-table-column prop="KjbRemark" label="科技部备注" min-width="100" align="center" show-overflow-tooltip /> | |
| 197 | 265 | </el-table> |
| 198 | - <div class="records-pagination"> | |
| 199 | - <el-pagination | |
| 200 | - layout="prev, pager, next" | |
| 201 | - :page-size="pageSize" | |
| 202 | - :total="refundTotalCount" | |
| 203 | - :current-page.sync="refundPage" | |
| 204 | - small | |
| 205 | - background | |
| 206 | - /> | |
| 207 | - </div> | |
| 266 | + </div> | |
| 267 | + <div class="records-pagination"> | |
| 268 | + <el-pagination layout="prev, pager, next" :page-size="pageSize" :total="serviceLogTotalCount" :current-page.sync="serviceLogPage" small background /> | |
| 208 | 269 | </div> |
| 209 | 270 | </div> |
| 210 | 271 | |
| 211 | - <div v-else-if="recordsTab === 'rights'" class="records-panel"> | |
| 212 | - <div class="rights-table-wrap"> | |
| 213 | - <el-table | |
| 214 | - :data="filteredRemainingItems" | |
| 215 | - border | |
| 216 | - size="mini" | |
| 217 | - class="rights-table" | |
| 218 | - empty-text="暂无权益" | |
| 219 | - > | |
| 220 | - <el-table-column prop="ItemName" label="项目名称" min-width="110" align="center" /> | |
| 221 | - <el-table-column prop="ItemPrice" label="单价" width="88" align="center"> | |
| 222 | - <template slot-scope="scope">¥{{ formatMoney(scope.row.ItemPrice) }}</template> | |
| 223 | - </el-table-column> | |
| 224 | - <el-table-column prop="SourceType" label="来源" width="72" align="center" /> | |
| 225 | - <el-table-column prop="TotalPurchased" label="总购买" width="72" align="center" /> | |
| 226 | - <el-table-column prop="ConsumedCount" label="已消费" width="72" align="center" /> | |
| 227 | - <el-table-column prop="RemainingCount" label="剩余" width="72" align="center"> | |
| 228 | - <template slot-scope="scope"> | |
| 229 | - <span class="num-remaining">{{ scope.row.RemainingCount ?? scope.row.remainingCount ?? 0 }}</span> | |
| 230 | - </template> | |
| 272 | + <!-- 旧日志 --> | |
| 273 | + <div v-else-if="recordsTab === 'oldLog'" class="records-panel"> | |
| 274 | + <div class="records-table-wrap"> | |
| 275 | + <el-table :data="filteredOldLogRecords" size="mini" class="record-table" empty-text="暂无旧日志"> | |
| 276 | + <el-table-column prop="CreateTime" label="记录时间" width="140" align="center" /> | |
| 277 | + <el-table-column prop="OrderNo" label="开单号" width="130" align="center" /> | |
| 278 | + <el-table-column prop="MemberName" label="会员名称" width="85" align="center" /> | |
| 279 | + <el-table-column prop="Remarks" label="备注" min-width="200" align="center" show-overflow-tooltip /> | |
| 280 | + </el-table> | |
| 281 | + </div> | |
| 282 | + <div class="records-pagination"> | |
| 283 | + <el-pagination layout="prev, pager, next" :page-size="pageSize" :total="oldLogTotalCount" :current-page.sync="oldLogPage" small background /> | |
| 284 | + </div> | |
| 285 | + </div> | |
| 286 | + | |
| 287 | + <!-- 退卡列表 --> | |
| 288 | + <div v-else-if="recordsTab === 'refund'" class="records-panel"> | |
| 289 | + <div class="records-table-wrap"> | |
| 290 | + <el-table :data="filteredRefundRecords" size="mini" class="record-table" empty-text="暂无退卡记录"> | |
| 291 | + <el-table-column prop="RefundDate" label="退卡日期" width="140" align="center" /> | |
| 292 | + <el-table-column prop="StoreName" label="门店" width="80" align="center" /> | |
| 293 | + <el-table-column label="退卡金额" width="85" align="center"> | |
| 294 | + <template slot-scope="scope"><span class="amount-text">¥{{ formatMoney(scope.row.RefundAmount) }}</span></template> | |
| 231 | 295 | </el-table-column> |
| 232 | - <el-table-column label="剩余合计" width="100" align="center"> | |
| 233 | - <template slot-scope="scope"> | |
| 234 | - <span class="num-total">¥{{ formatMoney(remainingItemTotal(scope.row)) }}</span> | |
| 235 | - </template> | |
| 296 | + <el-table-column label="实际退款" width="85" align="center"> | |
| 297 | + <template slot-scope="scope"><span class="amount-text">¥{{ formatMoney(scope.row.ActualRefundAmount) }}</span></template> | |
| 236 | 298 | </el-table-column> |
| 299 | + <el-table-column prop="RefundReason" label="退卡原因" min-width="120" align="center" show-overflow-tooltip /> | |
| 237 | 300 | </el-table> |
| 238 | - <div class="records-pagination"> | |
| 239 | - <el-pagination | |
| 240 | - layout="prev, pager, next" | |
| 241 | - :page-size="pageSize" | |
| 242 | - :total="rightsTotalCount" | |
| 243 | - :current-page.sync="rightsPage" | |
| 244 | - small | |
| 245 | - background | |
| 246 | - /> | |
| 247 | - </div> | |
| 301 | + </div> | |
| 302 | + <div class="records-pagination"> | |
| 303 | + <el-pagination layout="prev, pager, next" :page-size="pageSize" :total="refundTotalCount" :current-page.sync="refundPage" small background /> | |
| 248 | 304 | </div> |
| 249 | 305 | </div> |
| 250 | 306 | </div> |
| 251 | - </div> | |
| 252 | 307 | |
| 253 | - <div class="member-footer"> | |
| 254 | - <span class="records-op-btn records-op-btn--ghost records-op-btn--invite" @click="emitAction('invite')">去邀约</span> | |
| 255 | - <span class="records-op-btn records-op-btn--ghost records-op-btn--booking" @click="emitAction('booking')">去预约</span> | |
| 256 | - <span class="records-op-btn records-op-btn--ghost records-op-btn--billing" @click="emitAction('billing')">去开单</span> | |
| 257 | - <span class="records-op-btn records-op-btn--ghost records-op-btn--consume" @click="emitAction('consume')">去耗卡</span> | |
| 258 | - <span class="records-op-btn records-op-btn--ghost records-op-btn--refund" @click="emitAction('refund')">去退卡</span> | |
| 259 | - <span class="records-op-btn records-op-btn--ghost records-op-btn--close" @click="close">关闭</span> | |
| 308 | + <!-- 底部按钮 --> | |
| 309 | + <div class="content-footer"> | |
| 310 | + <span class="records-op-btn records-op-btn--ghost records-op-btn--invite" @click="emitAction('invite')"><i class="el-icon-phone-outline"></i>去邀约</span> | |
| 311 | + <span class="records-op-btn records-op-btn--ghost records-op-btn--booking" @click="emitAction('booking')"><i class="el-icon-date"></i>去预约</span> | |
| 312 | + <span class="records-op-btn records-op-btn--ghost records-op-btn--billing" @click="emitAction('billing')"><i class="el-icon-document"></i>去开单</span> | |
| 313 | + <span class="records-op-btn records-op-btn--ghost records-op-btn--consume" @click="emitAction('consume')"><i class="el-icon-s-claim"></i>去耗卡</span> | |
| 314 | + <span class="records-op-btn records-op-btn--ghost records-op-btn--refund" @click="emitAction('refund')"><i class="el-icon-refresh-left"></i>去退卡</span> | |
| 315 | + <span class="records-op-btn records-op-btn--ghost records-op-btn--close" @click="close"><i class="el-icon-close"></i>关闭</span> | |
| 316 | + </div> | |
| 260 | 317 | </div> |
| 261 | 318 | </div> |
| 262 | 319 | </el-dialog> |
| ... | ... | @@ -271,20 +328,24 @@ export default { |
| 271 | 328 | }, |
| 272 | 329 | data() { |
| 273 | 330 | return { |
| 274 | - recordsTab: 'invite', | |
| 331 | + recordsTab: 'rights', | |
| 275 | 332 | searchRights: '', |
| 276 | 333 | searchBilling: '', |
| 277 | 334 | searchConsume: '', |
| 278 | 335 | searchInvite: '', |
| 279 | 336 | searchBooking: '', |
| 280 | 337 | searchRefund: '', |
| 338 | + searchServiceLog: '', | |
| 339 | + searchOldLog: '', | |
| 281 | 340 | pageSize: 5, |
| 282 | 341 | rightsPage: 1, |
| 283 | 342 | billingPage: 1, |
| 284 | 343 | consumePage: 1, |
| 285 | 344 | invitePage: 1, |
| 286 | 345 | bookingPage: 1, |
| 287 | - refundPage: 1 | |
| 346 | + refundPage: 1, | |
| 347 | + serviceLogPage: 1, | |
| 348 | + oldLogPage: 1 | |
| 288 | 349 | } |
| 289 | 350 | }, |
| 290 | 351 | computed: { |
| ... | ... | @@ -299,7 +360,16 @@ export default { |
| 299 | 360 | const n = this.displayName |
| 300 | 361 | return n ? n[0] : '会' |
| 301 | 362 | }, |
| 302 | - // 兼容 RemainingItems(PC 接口)与 remainingItems | |
| 363 | + consumeLevelText() { | |
| 364 | + const level = this.member.consumeLevel | |
| 365 | + const map = { 0: 'D', 1: 'C', 2: 'B', 3: 'A', 4: 'A+', 5: 'A++' } | |
| 366 | + return map[level] !== undefined ? map[level] : (this.member.consumeLevelName || '') | |
| 367 | + }, | |
| 368 | + consumeLevelClass() { | |
| 369 | + const level = this.member.consumeLevel | |
| 370 | + const map = { 0: 'level--d', 1: 'level--c', 2: 'level--b', 3: 'level--a', 4: 'level--aplus', 5: 'level--aplusplus' } | |
| 371 | + return map[level] || 'level--default' | |
| 372 | + }, | |
| 303 | 373 | remainingItems() { |
| 304 | 374 | const list = this.member.RemainingItems || this.member.remainingItems || [] |
| 305 | 375 | return Array.isArray(list) ? list : [] |
| ... | ... | @@ -316,6 +386,22 @@ export default { |
| 316 | 386 | if (this.member.isEducationMember === 1) t.push('教育') |
| 317 | 387 | return t.length ? t.join(' / ') : '—' |
| 318 | 388 | }, |
| 389 | + formattedBirthday() { | |
| 390 | + const m = this.member | |
| 391 | + const yang = m.yanglsr || '' | |
| 392 | + const yin = m.yinlsr || '' | |
| 393 | + if (yang && yin) return `${yang}(${yin})` | |
| 394 | + if (yang) return yang | |
| 395 | + if (yin) return yin | |
| 396 | + return '—' | |
| 397 | + }, | |
| 398 | + memberTypeList() { | |
| 399 | + const list = [] | |
| 400 | + if (this.member.isBeautyMember === 1) list.push({ type: 'beauty', label: '生美' }) | |
| 401 | + if (this.member.isMedicalMember === 1) list.push({ type: 'medical', label: '医美' }) | |
| 402 | + if (this.member.isTechMember === 1) list.push({ type: 'tech', label: '科美' }) | |
| 403 | + return list | |
| 404 | + }, | |
| 319 | 405 | filteredRightsAll() { |
| 320 | 406 | const kw = (this.searchRights || '').trim() |
| 321 | 407 | const list = this.remainingItems |
| ... | ... | @@ -345,9 +431,9 @@ export default { |
| 345 | 431 | const list = this.billingRecords |
| 346 | 432 | if (!kw) return list |
| 347 | 433 | return list.filter(row => { |
| 348 | - const orderNo = String(row.orderNo || '') | |
| 349 | - const product = String(row.productName || '') | |
| 350 | - return orderNo.includes(kw) || product.includes(kw) | |
| 434 | + const items = String(row.Items || '') | |
| 435 | + const creator = String(row.CreatorName || '') | |
| 436 | + return items.includes(kw) || creator.includes(kw) | |
| 351 | 437 | }) |
| 352 | 438 | }, |
| 353 | 439 | billingTotalCount() { |
| ... | ... | @@ -370,8 +456,9 @@ export default { |
| 370 | 456 | const list = this.consumeRecords |
| 371 | 457 | if (!kw) return list |
| 372 | 458 | return list.filter(row => { |
| 373 | - const name = String(row.itemName || '') | |
| 374 | - return name.includes(kw) | |
| 459 | + const items = String(row.Items || '') | |
| 460 | + const operator = String(row.OperatorName || '') | |
| 461 | + return items.includes(kw) || operator.includes(kw) | |
| 375 | 462 | }) |
| 376 | 463 | }, |
| 377 | 464 | consumeTotalCount() { |
| ... | ... | @@ -394,9 +481,9 @@ export default { |
| 394 | 481 | const list = this.inviteRecords |
| 395 | 482 | if (!kw) return list |
| 396 | 483 | return list.filter(row => { |
| 397 | - const txt = String(row.inviteContent || '') | |
| 398 | - const inviter = String(row.inviter || '') | |
| 399 | - return txt.includes(kw) || inviter.includes(kw) | |
| 484 | + const record = String(row.ContactRecord || '') | |
| 485 | + const inviter = String(row.InviterName || '') | |
| 486 | + return record.includes(kw) || inviter.includes(kw) | |
| 400 | 487 | }) |
| 401 | 488 | }, |
| 402 | 489 | inviteTotalCount() { |
| ... | ... | @@ -419,9 +506,9 @@ export default { |
| 419 | 506 | const list = this.bookingRecords |
| 420 | 507 | if (!kw) return list |
| 421 | 508 | return list.filter(row => { |
| 422 | - const project = String(row.projectName || '') | |
| 423 | - const staff = String(row.staffName || '') | |
| 424 | - return project.includes(kw) || staff.includes(kw) | |
| 509 | + const item = String(row.ExperienceItem || '') | |
| 510 | + const coach = String(row.HealthCoachName || '') | |
| 511 | + return item.includes(kw) || coach.includes(kw) | |
| 425 | 512 | }) |
| 426 | 513 | }, |
| 427 | 514 | bookingTotalCount() { |
| ... | ... | @@ -444,9 +531,8 @@ export default { |
| 444 | 531 | const list = this.refundRecords |
| 445 | 532 | if (!kw) return list |
| 446 | 533 | return list.filter(row => { |
| 447 | - const orderNo = String(row.orderNo || '') | |
| 448 | - const project = String(row.projectName || '') | |
| 449 | - return orderNo.includes(kw) || project.includes(kw) | |
| 534 | + const reason = String(row.RefundReason || '') | |
| 535 | + return reason.includes(kw) | |
| 450 | 536 | }) |
| 451 | 537 | }, |
| 452 | 538 | refundTotalCount() { |
| ... | ... | @@ -460,14 +546,62 @@ export default { |
| 460 | 546 | const start = (page - 1) * size |
| 461 | 547 | return list.slice(start, start + size) |
| 462 | 548 | }, |
| 549 | + serviceLogRecords() { | |
| 550 | + const list = this.member.serviceLogRecords || [] | |
| 551 | + return Array.isArray(list) ? list : [] | |
| 552 | + }, | |
| 553 | + filteredServiceLogAll() { | |
| 554 | + const kw = (this.searchServiceLog || '').trim() | |
| 555 | + const list = this.serviceLogRecords | |
| 556 | + if (!kw) return list | |
| 557 | + return list.filter(row => { | |
| 558 | + const remark = String(row.Remark || '') | |
| 559 | + const creator = String(row.CreatorName || '') | |
| 560 | + return remark.includes(kw) || creator.includes(kw) | |
| 561 | + }) | |
| 562 | + }, | |
| 563 | + serviceLogTotalCount() { return this.filteredServiceLogAll.length }, | |
| 564 | + filteredServiceLogRecords() { | |
| 565 | + const list = this.filteredServiceLogAll | |
| 566 | + const size = this.pageSize | |
| 567 | + const maxPage = Math.max(1, Math.ceil((list.length || 1) / size)) | |
| 568 | + const page = Math.min(this.serviceLogPage || 1, maxPage) | |
| 569 | + const start = (page - 1) * size | |
| 570 | + return list.slice(start, start + size) | |
| 571 | + }, | |
| 572 | + oldLogRecords() { | |
| 573 | + const list = this.member.oldLogRecords || [] | |
| 574 | + return Array.isArray(list) ? list : [] | |
| 575 | + }, | |
| 576 | + filteredOldLogAll() { | |
| 577 | + const kw = (this.searchOldLog || '').trim() | |
| 578 | + const list = this.oldLogRecords | |
| 579 | + if (!kw) return list | |
| 580 | + return list.filter(row => { | |
| 581 | + const no = String(row.OrderNo || '') | |
| 582 | + const remarks = String(row.Remarks || '') | |
| 583 | + return no.includes(kw) || remarks.includes(kw) | |
| 584 | + }) | |
| 585 | + }, | |
| 586 | + oldLogTotalCount() { return this.filteredOldLogAll.length }, | |
| 587 | + filteredOldLogRecords() { | |
| 588 | + const list = this.filteredOldLogAll | |
| 589 | + const size = this.pageSize | |
| 590 | + const maxPage = Math.max(1, Math.ceil((list.length || 1) / size)) | |
| 591 | + const page = Math.min(this.oldLogPage || 1, maxPage) | |
| 592 | + const start = (page - 1) * size | |
| 593 | + return list.slice(start, start + size) | |
| 594 | + }, | |
| 463 | 595 | recordTabs() { |
| 464 | 596 | return [ |
| 597 | + { key: 'rights', label: '权益明细' }, | |
| 465 | 598 | { key: 'invite', label: '邀约记录' }, |
| 466 | 599 | { key: 'booking', label: '预约记录' }, |
| 467 | 600 | { key: 'billing', label: '开单记录' }, |
| 468 | 601 | { key: 'consume', label: '消耗记录' }, |
| 469 | - { key: 'refund', label: '退卡记录' }, | |
| 470 | - { key: 'rights', label: '剩余权益' } | |
| 602 | + { key: 'serviceLog', label: '服务日志' }, | |
| 603 | + { key: 'oldLog', label: '旧日志' }, | |
| 604 | + { key: 'refund', label: '退卡列表' } | |
| 471 | 605 | ] |
| 472 | 606 | }, |
| 473 | 607 | currentSearch: { |
| ... | ... | @@ -479,6 +613,8 @@ export default { |
| 479 | 613 | case 'consume': return this.searchConsume |
| 480 | 614 | case 'refund': return this.searchRefund |
| 481 | 615 | case 'rights': return this.searchRights |
| 616 | + case 'serviceLog': return this.searchServiceLog | |
| 617 | + case 'oldLog': return this.searchOldLog | |
| 482 | 618 | default: return '' |
| 483 | 619 | } |
| 484 | 620 | }, |
| ... | ... | @@ -490,17 +626,21 @@ export default { |
| 490 | 626 | case 'consume': this.searchConsume = val; break |
| 491 | 627 | case 'refund': this.searchRefund = val; break |
| 492 | 628 | case 'rights': this.searchRights = val; break |
| 629 | + case 'serviceLog': this.searchServiceLog = val; break | |
| 630 | + case 'oldLog': this.searchOldLog = val; break | |
| 493 | 631 | } |
| 494 | 632 | } |
| 495 | 633 | }, |
| 496 | 634 | currentSearchPlaceholder() { |
| 497 | 635 | switch (this.recordsTab) { |
| 498 | - case 'invite': return '按内容 / 邀约人搜索' | |
| 499 | - case 'booking': return '按项目 / 服务人员搜索' | |
| 500 | - case 'billing': return '按单号 / 项目搜索' | |
| 501 | - case 'consume': return '按项目名称搜索' | |
| 502 | - case 'refund': return '按单号 / 项目搜索' | |
| 636 | + case 'invite': return '按联系记录/邀约人搜索' | |
| 637 | + case 'booking': return '按体验项目/健康师搜索' | |
| 638 | + case 'billing': return '按品项/开单人员搜索' | |
| 639 | + case 'consume': return '按品项/操作人员搜索' | |
| 640 | + case 'refund': return '按退卡原因搜索' | |
| 503 | 641 | case 'rights': return '按项目名称搜索' |
| 642 | + case 'serviceLog': return '按备注/添加人搜索' | |
| 643 | + case 'oldLog': return '按开单号/备注搜索' | |
| 504 | 644 | default: return '输入关键字搜索' |
| 505 | 645 | } |
| 506 | 646 | }, |
| ... | ... | @@ -511,7 +651,6 @@ export default { |
| 511 | 651 | const width = 100 / count |
| 512 | 652 | return { |
| 513 | 653 | width: `${width}%`, |
| 514 | - // translateX 百分比基于自身宽度,移动一个 tab 宽度用 100% 为一步 | |
| 515 | 654 | transform: `translateX(${index * 100}%)` |
| 516 | 655 | } |
| 517 | 656 | } |
| ... | ... | @@ -533,180 +672,344 @@ export default { |
| 533 | 672 | }, |
| 534 | 673 | close() { |
| 535 | 674 | this.visibleProxy = false |
| 675 | + }, | |
| 676 | + relativeTime(dateStr) { | |
| 677 | + if (!dateStr) return '—' | |
| 678 | + const date = new Date(dateStr) | |
| 679 | + if (isNaN(date.getTime())) return dateStr | |
| 680 | + const now = new Date() | |
| 681 | + const diff = now - date | |
| 682 | + const days = Math.floor(diff / (1000 * 60 * 60 * 24)) | |
| 683 | + if (days < 0) return dateStr | |
| 684 | + if (days === 0) return '今天' | |
| 685 | + if (days === 1) return '昨天' | |
| 686 | + if (days < 7) return `${days}天前` | |
| 687 | + if (days < 30) return `${Math.floor(days / 7)}周前` | |
| 688 | + if (days < 365) return `${Math.floor(days / 30)}个月前` | |
| 689 | + return `${Math.floor(days / 365)}年前` | |
| 690 | + }, | |
| 691 | + statusClass(status) { | |
| 692 | + if (!status) return 'status--default' | |
| 693 | + const greenList = ['已到店', '已完成', '已签到', '已服务'] | |
| 694 | + const orangeList = ['待跟进', '待确认', '待服务', '进行中'] | |
| 695 | + const redList = ['已取消', '已退卡', '已退款', '已过期'] | |
| 696 | + const blueList = ['已预约', '待到店', '处理中', '已确认'] | |
| 697 | + if (greenList.includes(status)) return 'status--green' | |
| 698 | + if (orangeList.includes(status)) return 'status--orange' | |
| 699 | + if (redList.includes(status)) return 'status--red' | |
| 700 | + if (blueList.includes(status)) return 'status--blue' | |
| 701 | + return 'status--default' | |
| 536 | 702 | } |
| 537 | 703 | } |
| 538 | 704 | } |
| 539 | 705 | </script> |
| 540 | 706 | |
| 541 | 707 | <style lang="scss" scoped> |
| 542 | -.member-dialog-inner { | |
| 543 | - padding: 20px 24px 16px; | |
| 708 | +/* ========== Dialog 外壳 ========== */ | |
| 709 | +::v-deep .el-dialog { | |
| 710 | + width: 1100px; | |
| 711 | + max-width: 96vw; | |
| 712 | + height: 90vh; | |
| 713 | + margin-top: 5vh !important; | |
| 714 | + border-radius: 20px; | |
| 715 | + padding: 0; | |
| 716 | + background: radial-gradient(circle at top left, rgba(255,255,255,0.96) 0, rgba(248,250,252,0.98) 38%, rgba(241,245,249,0.98) 100%); | |
| 717 | + box-shadow: 0 24px 48px rgba(15,23,42,0.2), 0 0 0 0.5px rgba(148,163,184,0.35); | |
| 718 | + border: 1px solid rgba(226,232,240,0.9); | |
| 719 | + backdrop-filter: blur(26px); | |
| 720 | + -webkit-backdrop-filter: blur(26px); | |
| 544 | 721 | display: flex; |
| 545 | 722 | flex-direction: column; |
| 546 | - height: 100%; | |
| 547 | - box-sizing: border-box; | |
| 723 | + position: relative; | |
| 724 | + overflow: hidden; | |
| 548 | 725 | } |
| 726 | +::v-deep .el-dialog__header { display: none; } | |
| 727 | +::v-deep .el-dialog__body { padding: 0; flex: 1; overflow: hidden; } | |
| 549 | 728 | |
| 550 | -.member-header { | |
| 729 | +/* ========== 关闭按钮 ========== */ | |
| 730 | +.dialog-close-btn { | |
| 731 | + position: absolute; | |
| 732 | + top: 12px; | |
| 733 | + right: 12px; | |
| 734 | + z-index: 10; | |
| 735 | + width: 28px; | |
| 736 | + height: 28px; | |
| 737 | + border-radius: 50%; | |
| 551 | 738 | display: flex; |
| 552 | 739 | align-items: center; |
| 553 | - gap: 16px; | |
| 554 | - margin-bottom: 16px; | |
| 740 | + justify-content: center; | |
| 741 | + cursor: pointer; | |
| 742 | + background: rgba(241,245,249,0.9); | |
| 743 | + border: 1px solid rgba(226,232,240,0.6); | |
| 744 | + backdrop-filter: blur(12px); | |
| 745 | + -webkit-backdrop-filter: blur(12px); | |
| 746 | + transition: all 0.2s ease; | |
| 747 | + i { font-size: 14px; color: #64748b; transition: color 0.2s; } | |
| 748 | + &:hover { | |
| 749 | + background: rgba(239,68,68,0.1); | |
| 750 | + border-color: rgba(239,68,68,0.3); | |
| 751 | + i { color: #ef4444; } | |
| 752 | + } | |
| 555 | 753 | } |
| 556 | 754 | |
| 557 | -.member-body { | |
| 558 | - flex: 1; | |
| 755 | +/* ========== 主容器:左右分栏 ========== */ | |
| 756 | +.member-dialog-inner { | |
| 757 | + display: flex; | |
| 758 | + flex-direction: row; | |
| 759 | + height: 100%; | |
| 760 | + box-sizing: border-box; | |
| 761 | +} | |
| 762 | + | |
| 763 | +/* ========== 左侧边栏 ========== */ | |
| 764 | +.sidebar { | |
| 765 | + width: 280px; | |
| 766 | + flex-shrink: 0; | |
| 767 | + display: flex; | |
| 768 | + flex-direction: column; | |
| 769 | + background: rgba(248,250,252,0.98); | |
| 770 | + border-right: 1px solid rgba(226,232,240,0.6); | |
| 771 | + border-radius: 0 0 0 20px; | |
| 559 | 772 | overflow-y: auto; |
| 560 | - padding-right: 2px; | |
| 561 | - margin-bottom: 14px; | |
| 773 | + scrollbar-width: none; | |
| 774 | + -ms-overflow-style: none; | |
| 775 | + &::-webkit-scrollbar { display: none; } | |
| 562 | 776 | } |
| 563 | 777 | |
| 778 | +/* -- 身份区 -- */ | |
| 779 | +.sidebar-identity { | |
| 780 | + display: flex; | |
| 781 | + flex-direction: column; | |
| 782 | + align-items: center; | |
| 783 | + padding: 20px 16px 14px; | |
| 784 | +} | |
| 564 | 785 | .member-avatar { |
| 565 | - width: 48px; | |
| 566 | - height: 48px; | |
| 567 | - border-radius: 14px; | |
| 786 | + width: 56px; | |
| 787 | + height: 56px; | |
| 788 | + border-radius: 50%; | |
| 568 | 789 | background: linear-gradient(145deg, #6366f1, #a855f7); |
| 569 | 790 | display: flex; |
| 570 | 791 | align-items: center; |
| 571 | 792 | justify-content: center; |
| 572 | 793 | color: #fff; |
| 794 | + font-weight: 600; | |
| 795 | + font-size: 22px; | |
| 796 | + flex-shrink: 0; | |
| 797 | + box-shadow: 0 6px 16px rgba(99,102,241,0.3), 0 0 0 3px rgba(255,255,255,0.8); | |
| 798 | + margin-bottom: 10px; | |
| 799 | +} | |
| 800 | +.member-name { | |
| 801 | + font-size: 17px; | |
| 802 | + font-weight: 600; | |
| 803 | + color: #0f172a; | |
| 804 | + letter-spacing: 0.3px; | |
| 805 | + text-align: center; | |
| 806 | + margin-bottom: 6px; | |
| 807 | +} | |
| 808 | +.level-tag { | |
| 809 | + display: inline-block; | |
| 810 | + font-size: 11px; | |
| 811 | + padding: 2px 14px; | |
| 812 | + border-radius: 999px; | |
| 813 | + font-weight: 600; | |
| 814 | + margin-bottom: 8px; | |
| 815 | +} | |
| 816 | +.level--d { background: #e5e7eb; color: #6b7280; } | |
| 817 | +.level--c { background: rgba(59,130,246,0.1); color: #2563eb; border: 1px solid rgba(59,130,246,0.2); } | |
| 818 | +.level--b { background: rgba(34,197,94,0.1); color: #16a34a; border: 1px solid rgba(34,197,94,0.2); } | |
| 819 | +.level--a { background: rgba(245,158,11,0.1); color: #d97706; border: 1px solid rgba(245,158,11,0.2); } | |
| 820 | +.level--aplus { background: linear-gradient(135deg, #f59e0b, #d97706); color: #fff; box-shadow: 0 2px 6px rgba(245,158,11,0.3); } | |
| 821 | +.level--aplusplus { background: linear-gradient(135deg, #ef4444, #dc2626); color: #fff; box-shadow: 0 2px 6px rgba(239,68,68,0.3); } | |
| 822 | +.level--default { background: linear-gradient(135deg, #f59e0b, #d97706); color: #fff; } | |
| 823 | +.member-meta { | |
| 824 | + display: flex; | |
| 825 | + align-items: center; | |
| 826 | + justify-content: center; | |
| 827 | + font-size: 13px; | |
| 828 | + color: #64748b; | |
| 829 | + flex-wrap: wrap; | |
| 830 | + text-align: center; | |
| 831 | + span + span::before { | |
| 832 | + content: '\00b7'; | |
| 833 | + margin: 0 5px; | |
| 834 | + color: #cbd5e1; | |
| 835 | + font-weight: 700; | |
| 836 | + font-size: 14px; | |
| 837 | + } | |
| 838 | +} | |
| 839 | +.member-sub-info { | |
| 840 | + display: flex; | |
| 841 | + align-items: center; | |
| 842 | + justify-content: center; | |
| 843 | + gap: 8px; | |
| 844 | + font-size: 13px; | |
| 845 | + color: #64748b; | |
| 846 | + margin-bottom: 6px; | |
| 847 | + flex-wrap: wrap; | |
| 848 | +} | |
| 849 | +.member-phone { | |
| 850 | + color: #16a34a; | |
| 573 | 851 | font-weight: 500; |
| 574 | - font-size: 20px; | |
| 852 | + i { margin-right: 2px; font-size: 13px; } | |
| 575 | 853 | } |
| 576 | - | |
| 577 | -.member-basic { | |
| 578 | - flex: 1; | |
| 579 | - .name-line { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; } | |
| 580 | - .name { font-size: 17px; font-weight: 600; color: #0f172a; } | |
| 581 | - .level-tag { font-size: 11px; padding: 2px 8px; border-radius: 999px; background: #fef3c7; color: #c05621; } | |
| 582 | - .sub-line { font-size: 12px; color: #64748b; } | |
| 583 | - .status-sub { font-size: 11px; color: #94a3b8; margin-top: 2px; } | |
| 854 | +.member-dah { | |
| 855 | + color: #94a3b8; | |
| 856 | + font-size: 12px; | |
| 584 | 857 | } |
| 585 | - | |
| 586 | -.tag-row { | |
| 587 | - margin-top: 6px; | |
| 858 | +.member-type-tags { | |
| 588 | 859 | display: flex; |
| 589 | 860 | flex-wrap: wrap; |
| 861 | + justify-content: center; | |
| 590 | 862 | gap: 4px; |
| 863 | + margin-bottom: 4px; | |
| 591 | 864 | } |
| 592 | - | |
| 593 | -.member-tag { | |
| 594 | - padding: 2px 8px; | |
| 865 | +.type-tag { | |
| 866 | + padding: 2px 10px; | |
| 595 | 867 | border-radius: 999px; |
| 596 | 868 | font-size: 11px; |
| 597 | - background: rgba(148, 163, 184, 0.16); | |
| 598 | - color: #475569; | |
| 599 | -} | |
| 600 | - | |
| 601 | -.section-title { | |
| 602 | - display: flex; | |
| 603 | - align-items: center; | |
| 604 | - gap: 6px; | |
| 605 | - font-size: 13px; | |
| 606 | 869 | font-weight: 500; |
| 607 | - color: #111827; | |
| 608 | - margin-bottom: 10px; | |
| 609 | - .section-icon { font-size: 14px; color: #6366f1; } | |
| 610 | - &.section-gap { margin-top: 12px; } | |
| 611 | 870 | } |
| 612 | - | |
| 613 | -/* 第一行:基本信息(左) + 消费到店(右) */ | |
| 614 | -.row-one { | |
| 615 | - display: grid; | |
| 616 | - grid-template-columns: 1fr 280px; | |
| 617 | - gap: 20px; | |
| 618 | - margin-bottom: 16px; | |
| 871 | +.type-tag--beauty { | |
| 872 | + background: rgba(34,197,94,0.1); | |
| 873 | + color: #16a34a; | |
| 874 | + border: 1px solid rgba(34,197,94,0.15); | |
| 619 | 875 | } |
| 620 | - | |
| 621 | -.block { | |
| 622 | - background: linear-gradient(135deg, rgba(255, 255, 255, 0.9), rgba(248, 250, 252, 0.98)); | |
| 623 | - border-radius: 12px; | |
| 624 | - padding: 12px 14px; | |
| 625 | - box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04); | |
| 626 | - border: 1px solid rgba(226, 232, 240, 0.9); | |
| 876 | +.type-tag--medical { | |
| 877 | + background: rgba(249,115,22,0.1); | |
| 878 | + color: #ea580c; | |
| 879 | + border: 1px solid rgba(249,115,22,0.15); | |
| 627 | 880 | } |
| 628 | - | |
| 629 | -.info-grid { | |
| 630 | - display: grid; | |
| 631 | - grid-template-columns: repeat(2, minmax(0, 1fr)); | |
| 632 | - column-gap: 24px; | |
| 633 | - row-gap: 8px; | |
| 881 | +.type-tag--tech { | |
| 882 | + background: rgba(59,130,246,0.1); | |
| 883 | + color: #2563eb; | |
| 884 | + border: 1px solid rgba(59,130,246,0.15); | |
| 634 | 885 | } |
| 635 | - | |
| 636 | -.info-cell { | |
| 886 | +.member-visit-row { | |
| 637 | 887 | display: flex; |
| 638 | 888 | align-items: center; |
| 889 | + justify-content: center; | |
| 890 | + gap: 8px; | |
| 891 | + margin-top: 6px; | |
| 892 | + flex-wrap: wrap; | |
| 893 | +} | |
| 894 | +.member-visit { | |
| 639 | 895 | font-size: 12px; |
| 640 | - .l { | |
| 641 | - color: #64748b; | |
| 642 | - flex: 0 0 72px; | |
| 643 | - display: flex; | |
| 644 | - align-items: center; | |
| 645 | - white-space: nowrap; | |
| 646 | - } | |
| 647 | - .v { color: #111827; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | |
| 896 | + color: #94a3b8; | |
| 897 | + cursor: default; | |
| 898 | + i { margin-right: 3px; font-size: 12px; } | |
| 648 | 899 | } |
| 649 | - | |
| 650 | -.remark-cell { | |
| 651 | - grid-column: 1 / -1; | |
| 652 | - align-items: flex-start; | |
| 900 | +.member-sleep { | |
| 901 | + font-size: 11px; | |
| 902 | + padding: 2px 8px; | |
| 903 | + border-radius: 999px; | |
| 904 | + background: rgba(249,115,22,0.1); | |
| 905 | + color: #ea580c; | |
| 906 | + font-weight: 500; | |
| 907 | + i { margin-right: 2px; font-size: 11px; } | |
| 653 | 908 | } |
| 654 | - | |
| 655 | -.remark-cell .v { | |
| 656 | - white-space: normal; | |
| 909 | +.tag-row { | |
| 910 | + margin-top: 8px; | |
| 911 | + display: flex; | |
| 912 | + flex-wrap: wrap; | |
| 913 | + justify-content: center; | |
| 914 | + gap: 4px; | |
| 657 | 915 | } |
| 658 | - | |
| 659 | -.stats-mini { | |
| 660 | - .s { display: flex; justify-content: space-between; align-items: center; padding: 4px 0; font-size: 12px; } | |
| 661 | - .sl { color: #64748b; } | |
| 662 | - .sv { color: #111827; font-weight: 500; } | |
| 663 | - .sv.primary { color: #2563eb; } | |
| 916 | +.member-tag { | |
| 917 | + padding: 2px 8px; | |
| 918 | + border-radius: 999px; | |
| 919 | + font-size: 11px; | |
| 920 | + background: rgba(129,140,248,0.1); | |
| 921 | + color: #4f46e5; | |
| 922 | + border: 1px solid rgba(129,140,248,0.15); | |
| 664 | 923 | } |
| 665 | 924 | |
| 666 | -.rights-table-wrap { | |
| 667 | - max-height: 260px; | |
| 668 | - overflow-y: auto; | |
| 925 | +/* -- 侧边栏通用 section -- */ | |
| 926 | +.sidebar-section { | |
| 927 | + padding: 10px 16px; | |
| 928 | + border-top: 1px solid rgba(226,232,240,0.5); | |
| 669 | 929 | } |
| 670 | - | |
| 671 | -.rights-block { | |
| 672 | - margin-bottom: 0; | |
| 930 | +.sidebar-section-title { | |
| 931 | + font-size: 13px; | |
| 932 | + font-weight: 600; | |
| 933 | + color: #64748b; | |
| 934 | + margin-bottom: 6px; | |
| 935 | + display: flex; | |
| 936 | + align-items: center; | |
| 937 | + gap: 4px; | |
| 938 | + i { font-size: 13px; color: #6366f1; } | |
| 673 | 939 | } |
| 674 | - | |
| 675 | -.rights-table { | |
| 676 | - font-size: 12px; | |
| 940 | +.sidebar-row { | |
| 941 | + display: flex; | |
| 942 | + justify-content: space-between; | |
| 943 | + align-items: center; | |
| 944 | + padding: 4px 0; | |
| 945 | + min-height: 24px; | |
| 677 | 946 | } |
| 678 | - | |
| 679 | -.rights-table ::v-deep .el-table th { | |
| 680 | - padding: 6px 0; | |
| 947 | +.sidebar-row--highlight { | |
| 948 | + background: linear-gradient(135deg, rgba(37,99,235,0.04), rgba(124,58,237,0.04)); | |
| 949 | + border-radius: 6px; | |
| 950 | + padding: 4px 8px; | |
| 951 | + margin: 1px -8px; | |
| 952 | +} | |
| 953 | +.sidebar-row--remark { | |
| 954 | + align-items: flex-start; | |
| 955 | + .sidebar-value { white-space: normal; word-break: break-all; text-align: right; max-width: 160px; } | |
| 956 | +} | |
| 957 | +.sidebar-label { | |
| 958 | + color: #94a3b8; | |
| 681 | 959 | font-size: 12px; |
| 682 | - background: #f8fafc; | |
| 960 | + flex-shrink: 0; | |
| 961 | + display: flex; | |
| 962 | + align-items: center; | |
| 963 | + i { margin-right: 3px; font-size: 12px; color: #94a3b8; } | |
| 683 | 964 | } |
| 684 | - | |
| 685 | -.rights-table ::v-deep .el-table td { | |
| 686 | - padding: 5px 0; | |
| 965 | +.sidebar-value { | |
| 966 | + color: #1e293b; | |
| 967 | + font-size: 13px; | |
| 968 | + font-weight: 500; | |
| 969 | + overflow: hidden; | |
| 970 | + text-overflow: ellipsis; | |
| 971 | + white-space: nowrap; | |
| 972 | + max-width: 150px; | |
| 973 | + text-align: right; | |
| 974 | +} | |
| 975 | +.sidebar-value--amount { | |
| 976 | + font-size: 14px; | |
| 977 | + font-weight: 600; | |
| 978 | + color: #0f172a; | |
| 979 | +} | |
| 980 | +.sidebar-value--gradient { | |
| 981 | + font-size: 15px; | |
| 982 | + font-weight: 700; | |
| 983 | + background: linear-gradient(135deg, #2563eb, #7c3aed); | |
| 984 | + -webkit-background-clip: text; | |
| 985 | + -webkit-text-fill-color: transparent; | |
| 986 | + background-clip: text; | |
| 687 | 987 | } |
| 688 | 988 | |
| 689 | -.num-remaining { font-weight: 600; color: #2563eb; } | |
| 690 | -.num-total { font-weight: 600; color: #2563eb; } | |
| 691 | - | |
| 692 | -.records-area { | |
| 693 | - margin-top: 4px; | |
| 989 | +/* ========== 右侧内容区 ========== */ | |
| 990 | +.main-content { | |
| 991 | + flex: 1; | |
| 992 | + min-width: 0; | |
| 993 | + display: flex; | |
| 994 | + flex-direction: column; | |
| 995 | + padding: 12px 16px; | |
| 694 | 996 | } |
| 695 | 997 | |
| 696 | -.records-tabs-header { | |
| 998 | +/* -- 内容头部:Tab + 搜索 -- */ | |
| 999 | +.content-header { | |
| 697 | 1000 | display: flex; |
| 698 | 1001 | justify-content: space-between; |
| 699 | 1002 | align-items: center; |
| 700 | 1003 | margin-bottom: 8px; |
| 1004 | + flex-shrink: 0; | |
| 701 | 1005 | } |
| 702 | - | |
| 703 | 1006 | .records-tab-list { |
| 704 | 1007 | display: inline-flex; |
| 705 | - flex: 0 1 auto; | |
| 1008 | + flex: 1; | |
| 706 | 1009 | min-width: 0; |
| 707 | 1010 | width: 100%; |
| 708 | - max-width: 520px; | |
| 709 | - background: rgba(229, 231, 235, 0.82); | |
| 1011 | + max-width: 100%; | |
| 1012 | + background: rgba(229,231,235,0.7); | |
| 710 | 1013 | border-radius: 999px; |
| 711 | 1014 | padding: 2px; |
| 712 | 1015 | position: relative; |
| ... | ... | @@ -714,133 +1017,143 @@ export default { |
| 714 | 1017 | backdrop-filter: blur(14px); |
| 715 | 1018 | -webkit-backdrop-filter: blur(14px); |
| 716 | 1019 | } |
| 717 | - | |
| 718 | 1020 | .records-tab { |
| 719 | 1021 | border: none; |
| 720 | 1022 | outline: none; |
| 721 | 1023 | background: transparent; |
| 722 | - padding: 0 14px; | |
| 1024 | + padding: 0 8px; | |
| 723 | 1025 | border-radius: 999px; |
| 724 | - font-size: 12px; | |
| 1026 | + font-size: 13px; | |
| 725 | 1027 | color: #4b5563; |
| 726 | 1028 | cursor: pointer; |
| 727 | - transition: background-color 0.2s ease, color 0.2s ease; | |
| 1029 | + transition: color 0.25s ease; | |
| 728 | 1030 | flex: 1 1 0; |
| 729 | 1031 | text-align: center; |
| 730 | - height: 28px; | |
| 731 | - line-height: 28px; | |
| 1032 | + height: 30px; | |
| 1033 | + line-height: 30px; | |
| 732 | 1034 | position: relative; |
| 733 | 1035 | z-index: 1; |
| 734 | 1036 | } |
| 735 | - | |
| 736 | -.records-tab--active { | |
| 737 | - color: #1d4ed8; | |
| 738 | - font-weight: 600; | |
| 739 | -} | |
| 740 | - | |
| 1037 | +.records-tab--active { color: #1d4ed8; font-weight: 600; } | |
| 741 | 1038 | .records-tab-indicator { |
| 742 | 1039 | position: absolute; |
| 743 | 1040 | top: 2px; |
| 744 | 1041 | bottom: 2px; |
| 745 | 1042 | left: 0; |
| 746 | 1043 | border-radius: 999px; |
| 747 | - background: rgba(255, 255, 255, 0.98); | |
| 748 | - box-shadow: | |
| 749 | - 0 6px 16px rgba(15, 23, 42, 0.18), | |
| 750 | - 0 0 0 1px rgba(255,255,255,0.8); | |
| 1044 | + background: rgba(255,255,255,0.98); | |
| 1045 | + box-shadow: 0 3px 10px rgba(15,23,42,0.12), 0 0 0 1px rgba(255,255,255,0.8); | |
| 751 | 1046 | backdrop-filter: blur(18px); |
| 752 | 1047 | -webkit-backdrop-filter: blur(18px); |
| 753 | - transition: transform 0.24s ease; | |
| 1048 | + transition: transform 0.32s cubic-bezier(0.4, 0, 0.2, 1); | |
| 754 | 1049 | z-index: 0; |
| 755 | 1050 | } |
| 756 | - | |
| 757 | -.records-tab-icon { | |
| 758 | - font-size: 14px; | |
| 759 | - margin-right: 4px; | |
| 760 | -} | |
| 761 | - | |
| 762 | -.records-tab-label { | |
| 763 | - white-space: nowrap; | |
| 764 | -} | |
| 765 | - | |
| 766 | -.member-tag { | |
| 767 | - padding: 2px 8px; | |
| 1051 | +.records-tab-label { white-space: nowrap; } | |
| 1052 | +.records-tabs-search { flex-shrink: 0; margin-left: 10px; } | |
| 1053 | +.records-search { width: 160px; } | |
| 1054 | +.records-search ::v-deep .el-input__inner { | |
| 768 | 1055 | border-radius: 999px; |
| 769 | - font-size: 11px; | |
| 770 | - background: rgba(129, 140, 248, 0.12); | |
| 771 | - color: #4f46e5; | |
| 1056 | + height: 30px; | |
| 1057 | + line-height: 30px; | |
| 1058 | + font-size: 13px; | |
| 1059 | + border-color: #e2e8f0; | |
| 1060 | + transition: border-color 0.2s; | |
| 1061 | + &:focus { border-color: #6366f1; } | |
| 772 | 1062 | } |
| 1063 | +.records-search ::v-deep .el-input__prefix { display: flex; align-items: center; height: 100%; } | |
| 773 | 1064 | |
| 774 | -.records-toolbar { | |
| 775 | - position: absolute; | |
| 776 | - top: 2px; | |
| 777 | - right: 10px; | |
| 1065 | +/* -- 表格主体区:flex:1 填满 -- */ | |
| 1066 | +.content-body { | |
| 1067 | + flex: 1; | |
| 1068 | + min-height: 0; | |
| 778 | 1069 | display: flex; |
| 779 | - align-items: center; | |
| 780 | - margin-bottom: 0; | |
| 781 | -} | |
| 782 | - | |
| 783 | -.records-title { | |
| 784 | - font-size: 12px; | |
| 785 | - color: #111827; | |
| 786 | -} | |
| 787 | - | |
| 788 | -.records-search { | |
| 789 | - width: 220px; | |
| 790 | -} | |
| 791 | - | |
| 792 | -.records-search ::v-deep .el-input__inner { | |
| 793 | - border-radius: 999px; | |
| 794 | - height: 28px; | |
| 795 | - line-height: 28px; | |
| 1070 | + flex-direction: column; | |
| 796 | 1071 | } |
| 797 | - | |
| 798 | -.records-search ::v-deep .el-input__prefix { | |
| 1072 | +.records-panel { | |
| 1073 | + flex: 1; | |
| 1074 | + min-height: 0; | |
| 799 | 1075 | display: flex; |
| 800 | - align-items: center; | |
| 801 | - height: 100%; | |
| 1076 | + flex-direction: column; | |
| 802 | 1077 | } |
| 803 | - | |
| 804 | 1078 | .records-table-wrap { |
| 805 | - max-height: 260px; | |
| 1079 | + flex: 1; | |
| 1080 | + min-height: 0; | |
| 806 | 1081 | overflow-y: auto; |
| 1082 | + scrollbar-width: none; | |
| 1083 | + -ms-overflow-style: none; | |
| 1084 | + &::-webkit-scrollbar { display: none; } | |
| 807 | 1085 | } |
| 1086 | +.record-table, .rights-table { font-size: 13px; } | |
| 808 | 1087 | |
| 1088 | +::v-deep .el-table { | |
| 1089 | + &::before { display: none; } | |
| 1090 | + th { | |
| 1091 | + padding: 8px 0; | |
| 1092 | + font-size: 12px; | |
| 1093 | + background: #f8fafc; | |
| 1094 | + color: #64748b; | |
| 1095 | + font-weight: 500; | |
| 1096 | + border-bottom: 1px solid #e2e8f0; | |
| 1097 | + } | |
| 1098 | + td { | |
| 1099 | + padding: 7px 0; | |
| 1100 | + border-bottom: 1px solid #f1f5f9; | |
| 1101 | + } | |
| 1102 | + .el-table__row:hover > td { background: rgba(99,102,241,0.03); } | |
| 1103 | + &.el-table--border td, &.el-table--border th { border-right: none; } | |
| 1104 | + &.el-table--border::after { display: none; } | |
| 1105 | +} | |
| 809 | 1106 | .records-pagination { |
| 810 | - margin-top: 6px; | |
| 1107 | + margin-top: 4px; | |
| 811 | 1108 | text-align: right; |
| 1109 | + flex-shrink: 0; | |
| 812 | 1110 | } |
| 813 | 1111 | |
| 1112 | +/* ========== 状态胶囊 ========== */ | |
| 1113 | +.status-capsule { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 11px; font-weight: 500; line-height: 1.6; } | |
| 1114 | +.status--green { background: rgba(34,197,94,0.1); color: #16a34a; } | |
| 1115 | +.status--orange { background: rgba(249,115,22,0.1); color: #ea580c; } | |
| 1116 | +.status--red { background: rgba(239,68,68,0.1); color: #dc2626; } | |
| 1117 | +.status--blue { background: rgba(59,130,246,0.1); color: #2563eb; } | |
| 1118 | +.status--default { background: rgba(148,163,184,0.1); color: #64748b; } | |
| 1119 | + | |
| 1120 | +.amount-text { font-weight: 600; color: #2563eb; } | |
| 1121 | +.num-remaining { font-weight: 600; color: #2563eb; } | |
| 1122 | +.num-total { font-weight: 600; color: #7c3aed; } | |
| 1123 | + | |
| 1124 | +/* -- 底部操作按钮 -- */ | |
| 1125 | +.content-footer { | |
| 1126 | + display: flex; | |
| 1127 | + justify-content: flex-start; | |
| 1128 | + gap: 10px; | |
| 1129 | + padding-top: 8px; | |
| 1130 | + flex-shrink: 0; | |
| 1131 | + border-top: 1px solid rgba(226,232,240,0.5); | |
| 1132 | +} | |
| 814 | 1133 | .records-op-btn { |
| 815 | 1134 | display: inline-flex; |
| 816 | 1135 | align-items: center; |
| 817 | 1136 | justify-content: center; |
| 818 | - min-width: 96px; | |
| 819 | - height: 30px; | |
| 1137 | + gap: 4px; | |
| 1138 | + min-width: 84px; | |
| 1139 | + height: 32px; | |
| 820 | 1140 | padding: 0 14px; |
| 821 | 1141 | border-radius: 999px; |
| 822 | - font-size: 12px; | |
| 1142 | + font-size: 13px; | |
| 823 | 1143 | font-weight: 500; |
| 824 | 1144 | cursor: pointer; |
| 825 | 1145 | position: relative; |
| 826 | 1146 | overflow: hidden; |
| 827 | 1147 | transition: color 0.2s ease, box-shadow 0.2s ease, transform 0.12s ease; |
| 1148 | + i { font-size: 13px; } | |
| 828 | 1149 | } |
| 829 | - | |
| 830 | 1150 | .records-op-btn--ghost { |
| 831 | - background: rgba(239, 246, 255, 0.9); | |
| 1151 | + background: rgba(239,246,255,0.9); | |
| 832 | 1152 | color: #2563eb; |
| 833 | - border: 1px solid rgba(37, 99, 235, 0.18); | |
| 1153 | + border: 1px solid rgba(37,99,235,0.18); | |
| 834 | 1154 | } |
| 835 | - | |
| 836 | -.records-op-btn:hover { | |
| 837 | - transform: translateY(-0.5px); | |
| 838 | - box-shadow: 0 4px 10px rgba(15, 23, 42, 0.12); | |
| 839 | -} | |
| 840 | -.records-op-btn--ghost:hover { | |
| 841 | - color: #ffffff; | |
| 842 | -} | |
| 843 | - | |
| 1155 | +.records-op-btn:hover { transform: translateY(-0.5px); box-shadow: 0 4px 10px rgba(15,23,42,0.12); } | |
| 1156 | +.records-op-btn--ghost:hover { color: #ffffff; } | |
| 844 | 1157 | .records-op-btn::before { |
| 845 | 1158 | content: ''; |
| 846 | 1159 | position: absolute; |
| ... | ... | @@ -853,77 +1166,14 @@ export default { |
| 853 | 1166 | transition: transform 0.25s ease; |
| 854 | 1167 | z-index: -1; |
| 855 | 1168 | } |
| 856 | - | |
| 857 | -.records-op-btn:hover::before { | |
| 858 | - transform: translateX(0); | |
| 859 | -} | |
| 860 | - | |
| 861 | -.records-op-btn--invite::before { | |
| 862 | - background-image: linear-gradient(135deg, #0ea5e9, #22c55e); | |
| 863 | -} | |
| 864 | - | |
| 865 | -.records-op-btn--booking::before { | |
| 866 | - background-image: linear-gradient(135deg, #f97316, #facc15); | |
| 867 | -} | |
| 868 | - | |
| 869 | -.records-op-btn--billing::before { | |
| 870 | - background-image: linear-gradient(135deg, #3b82f6, #2563eb); | |
| 871 | -} | |
| 872 | - | |
| 873 | -.records-op-btn--consume::before { | |
| 874 | - background-image: linear-gradient(135deg, #22c55e, #16a34a); | |
| 875 | -} | |
| 876 | - | |
| 877 | -.records-op-btn--refund::before { | |
| 878 | - background-image: linear-gradient(135deg, #f97316, #ef4444); | |
| 879 | -} | |
| 880 | - | |
| 881 | -.records-op-btn--close::before { | |
| 882 | - background-image: linear-gradient(135deg, #e5e7eb, #cbd5f5); | |
| 883 | -} | |
| 884 | -.record-table { font-size: 12px; } | |
| 885 | -.record-table ::v-deep .el-table th { | |
| 886 | - padding: 6px 0; | |
| 887 | - font-size: 12px; | |
| 888 | - background: #f8fafc; | |
| 889 | -} | |
| 890 | -.record-table ::v-deep .el-table td { | |
| 891 | - padding: 5px 0; | |
| 892 | -} | |
| 893 | - | |
| 894 | -.member-footer { | |
| 895 | - display: flex; | |
| 896 | - justify-content: flex-end; | |
| 897 | - gap: 10px; | |
| 898 | -} | |
| 899 | - | |
| 900 | -::v-deep .el-dialog { | |
| 901 | - width: 1040px; | |
| 902 | - max-width: 96vw; | |
| 903 | - max-height: 86vh; | |
| 904 | - margin-top: 5vh !important; | |
| 905 | - border-radius: 24px; | |
| 906 | - padding: 0; | |
| 907 | - background: radial-gradient(circle at top left, rgba(255,255,255,0.96) 0, rgba(248,250,252,0.98) 38%, rgba(241,245,249,0.98) 100%); | |
| 908 | - box-shadow: | |
| 909 | - 0 32px 60px rgba(15, 23, 42, 0.22), | |
| 910 | - 0 0 0 0.5px rgba(148, 163, 184, 0.35); | |
| 911 | - border: 1px solid rgba(226, 232, 240, 0.9); | |
| 912 | - backdrop-filter: blur(26px); | |
| 913 | - -webkit-backdrop-filter: blur(26px); | |
| 914 | - display: flex; | |
| 915 | - flex-direction: column; | |
| 916 | -} | |
| 917 | - | |
| 918 | -::v-deep .el-dialog__header { display: none; } | |
| 919 | -::v-deep .el-dialog__body { | |
| 920 | - padding: 0; | |
| 921 | - flex: 1; | |
| 922 | - overflow: hidden; | |
| 923 | -} | |
| 924 | - | |
| 925 | -@media (max-width: 768px) { | |
| 926 | - .row-one { grid-template-columns: 1fr; } | |
| 927 | - .info-grid { grid-template-columns: repeat(2, 1fr); } | |
| 1169 | +.records-op-btn:hover::before { transform: translateX(0); } | |
| 1170 | +.records-op-btn--invite::before { background-image: linear-gradient(135deg, #0ea5e9, #22c55e); } | |
| 1171 | +.records-op-btn--booking::before { background-image: linear-gradient(135deg, #f97316, #facc15); } | |
| 1172 | +.records-op-btn--billing::before { background-image: linear-gradient(135deg, #3b82f6, #2563eb); } | |
| 1173 | +.records-op-btn--consume::before { background-image: linear-gradient(135deg, #22c55e, #16a34a); } | |
| 1174 | +.records-op-btn--refund::before { background-image: linear-gradient(135deg, #f97316, #ef4444); } | |
| 1175 | +.records-op-btn--close { | |
| 1176 | + margin-left: auto; | |
| 1177 | + &::before { background-image: linear-gradient(135deg, #94a3b8, #64748b); } | |
| 928 | 1178 | } |
| 929 | 1179 | </style> | ... | ... |
store-pc/src/components/RefundDialog.vue
0 → 100644
| 1 | +<template> | |
| 2 | + <el-dialog | |
| 3 | + :visible.sync="visibleProxy" | |
| 4 | + :show-close="false" | |
| 5 | + width="1200px" | |
| 6 | + :close-on-click-modal="false" | |
| 7 | + custom-class="refund-dialog" | |
| 8 | + append-to-body | |
| 9 | + > | |
| 10 | + <div class="refund-dialog-inner"> | |
| 11 | + <div class="refund-header"> | |
| 12 | + <div class="refund-title-wrap"> | |
| 13 | + <div class="refund-title">新建退卡</div> | |
| 14 | + <div class="refund-subtitle" v-if="form.memberName">{{ form.memberName }}</div> | |
| 15 | + </div> | |
| 16 | + <span class="refund-close" @click="handleCancel"> | |
| 17 | + <i class="el-icon-close"></i> | |
| 18 | + </span> | |
| 19 | + </div> | |
| 20 | + | |
| 21 | + <div class="refund-content"> | |
| 22 | + <el-form | |
| 23 | + ref="form" | |
| 24 | + :model="form" | |
| 25 | + :rules="rules" | |
| 26 | + label-width="96px" | |
| 27 | + size="small" | |
| 28 | + class="refund-form" | |
| 29 | + > | |
| 30 | + <!-- ===== 左栏:基础信息 ===== --> | |
| 31 | + <div class="refund-left"> | |
| 32 | + <div class="section-title"><i class="el-icon-document"></i> 基础信息</div> | |
| 33 | + | |
| 34 | + <el-form-item label="会员" prop="memberId"> | |
| 35 | + <el-select v-model="form.memberId" placeholder="搜索会员" filterable clearable @change="onMemberChange"> | |
| 36 | + <el-option v-for="m in memberOptions" :key="m.value" :label="`${m.label}(${m.phone})`" :value="m.value" /> | |
| 37 | + </el-select> | |
| 38 | + </el-form-item> | |
| 39 | + | |
| 40 | + <el-form-item label="退卡日期" prop="refundDate"> | |
| 41 | + <el-date-picker v-model="form.refundDate" type="date" placeholder="选择日期" style="width:100%" /> | |
| 42 | + </el-form-item> | |
| 43 | + | |
| 44 | + <el-form-item label="退款金额"> | |
| 45 | + <el-input :value="totalRefundAmount" readonly> | |
| 46 | + <template slot="prepend">¥</template> | |
| 47 | + </el-input> | |
| 48 | + </el-form-item> | |
| 49 | + | |
| 50 | + <el-form-item label="实际退款金额"> | |
| 51 | + <el-input :value="actualRefundAmount" readonly> | |
| 52 | + <template slot="prepend">¥</template> | |
| 53 | + </el-input> | |
| 54 | + </el-form-item> | |
| 55 | + | |
| 56 | + <div class="section-title"><i class="el-icon-paperclip"></i> 附件与备注</div> | |
| 57 | + | |
| 58 | + <el-form-item label="上传文件"> | |
| 59 | + <el-upload | |
| 60 | + action="#" | |
| 61 | + :auto-upload="false" | |
| 62 | + :file-list="form.files" | |
| 63 | + :on-change="(f, fl) => form.files = fl" | |
| 64 | + :on-remove="(f, fl) => form.files = fl" | |
| 65 | + multiple | |
| 66 | + > | |
| 67 | + <el-button size="mini" type="primary" plain><i class="el-icon-upload2"></i> 上传文件</el-button> | |
| 68 | + <span slot="tip" class="upload-tip">支持图片、PDF,可多个</span> | |
| 69 | + </el-upload> | |
| 70 | + </el-form-item> | |
| 71 | + | |
| 72 | + <el-form-item label="备注"> | |
| 73 | + <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注信息" /> | |
| 74 | + </el-form-item> | |
| 75 | + </div> | |
| 76 | + | |
| 77 | + <!-- ===== 右栏:品项明细 ===== --> | |
| 78 | + <div class="refund-right"> | |
| 79 | + <div class="section-title"><i class="el-icon-goods"></i> 品项明细</div> | |
| 80 | + | |
| 81 | + <div | |
| 82 | + v-for="(item, idx) in form.items" | |
| 83 | + :key="'item-' + idx" | |
| 84 | + class="item-card" | |
| 85 | + > | |
| 86 | + <div class="item-card-head"> | |
| 87 | + <span class="item-card-no">品项 {{ idx + 1 }}</span> | |
| 88 | + <el-button | |
| 89 | + v-if="form.items.length > 1" | |
| 90 | + type="text" | |
| 91 | + class="item-remove-btn" | |
| 92 | + @click="removeItem(idx)" | |
| 93 | + > | |
| 94 | + <i class="el-icon-delete"></i> 删除 | |
| 95 | + </el-button> | |
| 96 | + </div> | |
| 97 | + | |
| 98 | + <el-form-item | |
| 99 | + label="品项" | |
| 100 | + :prop="'items.' + idx + '.projectId'" | |
| 101 | + :rules="[{ required: true, message: '请选择品项', trigger: 'change' }]" | |
| 102 | + label-width="56px" | |
| 103 | + > | |
| 104 | + <el-select | |
| 105 | + v-model="item.projectId" | |
| 106 | + placeholder="搜索品项" | |
| 107 | + filterable | |
| 108 | + clearable | |
| 109 | + @change="onProjectChange(idx)" | |
| 110 | + > | |
| 111 | + <el-option v-for="p in availableItems" :key="p.value" :label="p.label" :value="p.value" /> | |
| 112 | + </el-select> | |
| 113 | + </el-form-item> | |
| 114 | + | |
| 115 | + <div v-if="item.projectId" class="px-info-panel"> | |
| 116 | + <el-row :gutter="8"> | |
| 117 | + <el-col :span="8"><span class="px-tag">单价</span> <b>¥{{ item.price }}</b></el-col> | |
| 118 | + <el-col :span="8"><span class="px-tag">总购买</span> <b>{{ item.totalPurchased }}</b></el-col> | |
| 119 | + <el-col :span="8"><span class="px-tag">已消费</span> <b>{{ item.consumed }}</b></el-col> | |
| 120 | + </el-row> | |
| 121 | + <el-row :gutter="8" style="margin-top:4px"> | |
| 122 | + <el-col :span="8"><span class="px-tag">剩余</span> <b class="remaining">{{ item.remaining }}</b></el-col> | |
| 123 | + <el-col :span="8"><span class="px-tag">来源</span> <b>{{ item.sourceType || '无' }}</b></el-col> | |
| 124 | + <el-col :span="8"><span class="px-tag">手工费</span> <b>¥{{ item.laborCost }}</b></el-col> | |
| 125 | + </el-row> | |
| 126 | + </div> | |
| 127 | + | |
| 128 | + <el-row :gutter="12" style="margin-top:8px"> | |
| 129 | + <el-col :span="12"> | |
| 130 | + <el-form-item label="退卡次数" label-width="72px"> | |
| 131 | + <el-input-number | |
| 132 | + v-model="item.refundCount" | |
| 133 | + :min="1" | |
| 134 | + :max="item.remaining || 999" | |
| 135 | + controls-position="right" | |
| 136 | + style="width:100%" | |
| 137 | + @change="onRefundCountChange(idx)" | |
| 138 | + /> | |
| 139 | + </el-form-item> | |
| 140 | + </el-col> | |
| 141 | + </el-row> | |
| 142 | + | |
| 143 | + <!-- 健康师业绩分配 --> | |
| 144 | + <div class="worker-section"> | |
| 145 | + <div class="worker-label"> | |
| 146 | + <span>健康师业绩分配</span> | |
| 147 | + <el-button type="text" size="mini" @click="addWorker(idx)"> | |
| 148 | + <i class="el-icon-plus"></i> 添加健康师 | |
| 149 | + </el-button> | |
| 150 | + </div> | |
| 151 | + <div v-for="(w, wi) in item.workers" :key="'w-' + wi" class="worker-row"> | |
| 152 | + <el-select v-model="w.workerId" placeholder="选择健康师" filterable size="mini" class="worker-select"> | |
| 153 | + <el-option v-for="h in healthWorkerOptions" :key="h.value" :label="h.label" :value="h.value" /> | |
| 154 | + </el-select> | |
| 155 | + <el-input v-model="w.jksyj" placeholder="业绩" size="mini" class="worker-field" @input="onWorkerPerformanceInput"> | |
| 156 | + <template slot="prepend">¥</template> | |
| 157 | + </el-input> | |
| 158 | + <el-input :value="w.count" readonly placeholder="次数" size="mini" class="worker-field-sm" /> | |
| 159 | + <el-button | |
| 160 | + v-if="item.workers.length > 1" | |
| 161 | + type="text" | |
| 162 | + size="mini" | |
| 163 | + class="worker-remove" | |
| 164 | + @click="removeWorker(idx, wi)" | |
| 165 | + > | |
| 166 | + <i class="el-icon-close"></i> | |
| 167 | + </el-button> | |
| 168 | + </div> | |
| 169 | + </div> | |
| 170 | + | |
| 171 | + <!-- 科技部老师(仅科美品项显示) --> | |
| 172 | + <div class="worker-section" v-if="item.qt2 === '科美'"> | |
| 173 | + <div class="worker-label"> | |
| 174 | + <span>科技部老师</span> | |
| 175 | + <el-button type="text" size="mini" @click="addTechTeacher(idx)"> | |
| 176 | + <i class="el-icon-plus"></i> 添加科技部老师 | |
| 177 | + </el-button> | |
| 178 | + </div> | |
| 179 | + <div v-for="(t, ti) in item.techTeachers" :key="'t-' + ti" class="worker-row"> | |
| 180 | + <el-select v-model="t.teacherId" placeholder="选择老师" filterable size="mini" class="worker-select"> | |
| 181 | + <el-option v-for="tt in techTeacherOptions" :key="tt.value" :label="tt.label" :value="tt.value" /> | |
| 182 | + </el-select> | |
| 183 | + <el-input v-model="t.jksyj" placeholder="业绩" size="mini" class="worker-field" @input="onWorkerPerformanceInput"> | |
| 184 | + <template slot="prepend">¥</template> | |
| 185 | + </el-input> | |
| 186 | + <el-input :value="t.count" readonly placeholder="次数" size="mini" class="worker-field-sm" /> | |
| 187 | + <el-button | |
| 188 | + type="text" | |
| 189 | + size="mini" | |
| 190 | + class="worker-remove" | |
| 191 | + @click="removeTechTeacher(idx, ti)" | |
| 192 | + > | |
| 193 | + <i class="el-icon-close"></i> | |
| 194 | + </el-button> | |
| 195 | + </div> | |
| 196 | + </div> | |
| 197 | + </div> | |
| 198 | + | |
| 199 | + <div class="add-btn-row"> | |
| 200 | + <el-button type="text" @click="addItem"> | |
| 201 | + <i class="el-icon-circle-plus-outline"></i> 添加品项 | |
| 202 | + </el-button> | |
| 203 | + </div> | |
| 204 | + </div> | |
| 205 | + </el-form> | |
| 206 | + </div> | |
| 207 | + | |
| 208 | + <div class="refund-footer"> | |
| 209 | + <div class="footer-summary"> | |
| 210 | + <span>退款金额 <b class="highlight">¥{{ totalRefundAmount }}</b></span> | |
| 211 | + <span>实际退款 <b class="highlight-orange">¥{{ actualRefundAmount }}</b></span> | |
| 212 | + </div> | |
| 213 | + <div class="footer-actions"> | |
| 214 | + <el-button size="small" @click="handleCancel">取 消</el-button> | |
| 215 | + <el-button type="primary" size="small" :loading="submitting" @click="handleSubmit"> | |
| 216 | + {{ submitting ? '提交中...' : '提交退卡' }} | |
| 217 | + </el-button> | |
| 218 | + </div> | |
| 219 | + </div> | |
| 220 | + </div> | |
| 221 | + </el-dialog> | |
| 222 | +</template> | |
| 223 | + | |
| 224 | +<script> | |
| 225 | +export default { | |
| 226 | + name: 'RefundDialog', | |
| 227 | + props: { | |
| 228 | + visible: { type: Boolean, default: false } | |
| 229 | + }, | |
| 230 | + data() { | |
| 231 | + return { | |
| 232 | + submitting: false, | |
| 233 | + form: this.createEmptyForm(), | |
| 234 | + memberOptions: [ | |
| 235 | + { value: 'cust001', label: '林小纤', phone: '13800138000' }, | |
| 236 | + { value: 'cust002', label: '王丽', phone: '13800138001' }, | |
| 237 | + { value: 'cust003', label: '张敏', phone: '13800138002' } | |
| 238 | + ], | |
| 239 | + memberItemsMap: { | |
| 240 | + 'cust001': [ | |
| 241 | + { value: 'item001', label: '面部深层护理(次卡)', price: 380, remaining: 6, totalPurchased: 10, consumed: 4, sourceType: '购买', qt2: '', laborCost: 50 }, | |
| 242 | + { value: 'item002', label: '肩颈调理(疗程)', price: 268, remaining: 3, totalPurchased: 5, consumed: 2, sourceType: '购买', qt2: '科美', laborCost: 40 }, | |
| 243 | + { value: 'item003', label: '眼周护理套餐', price: 198, remaining: 3, totalPurchased: 4, consumed: 1, sourceType: '赠送', qt2: '', laborCost: 25 } | |
| 244 | + ], | |
| 245 | + 'cust002': [ | |
| 246 | + { value: 'item004', label: '面部深层护理(次卡)', price: 380, remaining: 4, totalPurchased: 8, consumed: 4, sourceType: '购买', qt2: '', laborCost: 50 } | |
| 247 | + ], | |
| 248 | + 'cust003': [ | |
| 249 | + { value: 'item005', label: '肩颈调理(疗程)', price: 268, remaining: 5, totalPurchased: 5, consumed: 0, sourceType: '购买', qt2: '科美', laborCost: 40 } | |
| 250 | + ] | |
| 251 | + }, | |
| 252 | + healthWorkerOptions: [ | |
| 253 | + { value: 'jks001', label: '张健康师' }, | |
| 254 | + { value: 'jks002', label: '李健康师' }, | |
| 255 | + { value: 'jks003', label: '王健康师' } | |
| 256 | + ], | |
| 257 | + techTeacherOptions: [ | |
| 258 | + { value: 'kjb001', label: '赵科技老师' }, | |
| 259 | + { value: 'kjb002', label: '钱科技老师' } | |
| 260 | + ], | |
| 261 | + availableItems: [], | |
| 262 | + rules: { | |
| 263 | + memberId: [{ required: true, message: '请选择会员', trigger: 'change' }], | |
| 264 | + refundDate: [{ required: true, message: '请选择退卡日期', trigger: 'change' }] | |
| 265 | + } | |
| 266 | + } | |
| 267 | + }, | |
| 268 | + computed: { | |
| 269 | + visibleProxy: { | |
| 270 | + get() { return this.visible }, | |
| 271 | + set(val) { this.$emit('update:visible', val) } | |
| 272 | + }, | |
| 273 | + totalRefundAmount() { | |
| 274 | + return this.form.items.reduce((sum, it) => { | |
| 275 | + if (it.projectId && it.price) { | |
| 276 | + return sum + (it.price * (it.refundCount || 0)) | |
| 277 | + } | |
| 278 | + return sum | |
| 279 | + }, 0).toFixed(2) | |
| 280 | + }, | |
| 281 | + actualRefundAmount() { | |
| 282 | + let total = 0 | |
| 283 | + this.form.items.forEach(it => { | |
| 284 | + if (!it.projectId) return | |
| 285 | + it.workers.forEach(w => { | |
| 286 | + total += parseFloat(w.jksyj) || 0 | |
| 287 | + }) | |
| 288 | + if (it.techTeachers) { | |
| 289 | + it.techTeachers.forEach(t => { | |
| 290 | + total += parseFloat(t.jksyj) || 0 | |
| 291 | + }) | |
| 292 | + } | |
| 293 | + }) | |
| 294 | + return total.toFixed(2) | |
| 295 | + } | |
| 296 | + }, | |
| 297 | + watch: { | |
| 298 | + visible(val) { | |
| 299 | + if (val) this.applyPrefill() | |
| 300 | + } | |
| 301 | + }, | |
| 302 | + methods: { | |
| 303 | + createEmptyForm() { | |
| 304 | + return { | |
| 305 | + memberId: '', | |
| 306 | + memberName: '', | |
| 307 | + refundDate: new Date(), | |
| 308 | + remark: '', | |
| 309 | + files: [], | |
| 310 | + items: [this.createEmptyItem()] | |
| 311 | + } | |
| 312 | + }, | |
| 313 | + createEmptyItem() { | |
| 314 | + return { | |
| 315 | + projectId: '', | |
| 316 | + price: 0, | |
| 317 | + remaining: 0, | |
| 318 | + totalPurchased: 0, | |
| 319 | + consumed: 0, | |
| 320 | + sourceType: '', | |
| 321 | + qt2: '', | |
| 322 | + laborCost: 0, | |
| 323 | + refundCount: 1, | |
| 324 | + workers: [{ workerId: '', jksyj: '', count: '' }], | |
| 325 | + techTeachers: [] | |
| 326 | + } | |
| 327 | + }, | |
| 328 | + applyPrefill() { | |
| 329 | + this.form = this.createEmptyForm() | |
| 330 | + this.availableItems = [] | |
| 331 | + this.$nextTick(() => { | |
| 332 | + this.$refs.form && this.$refs.form.clearValidate() | |
| 333 | + }) | |
| 334 | + }, | |
| 335 | + onMemberChange(val) { | |
| 336 | + const m = this.memberOptions.find(o => o.value === val) | |
| 337 | + this.form.memberName = m ? m.label : '' | |
| 338 | + this.form.items = [this.createEmptyItem()] | |
| 339 | + this.loadMemberItems(val) | |
| 340 | + }, | |
| 341 | + loadMemberItems(memberId) { | |
| 342 | + const items = this.memberItemsMap[memberId] || [] | |
| 343 | + this.availableItems = items.map(it => ({ | |
| 344 | + ...it, | |
| 345 | + label: `${it.label}(剩余${it.remaining}次)` | |
| 346 | + })) | |
| 347 | + }, | |
| 348 | + onProjectChange(idx) { | |
| 349 | + const item = this.form.items[idx] | |
| 350 | + const p = this.availableItems.find(o => o.value === item.projectId) | |
| 351 | + if (p) { | |
| 352 | + item.price = p.price | |
| 353 | + item.remaining = p.remaining | |
| 354 | + item.totalPurchased = p.totalPurchased | |
| 355 | + item.consumed = p.consumed | |
| 356 | + item.sourceType = p.sourceType | |
| 357 | + item.qt2 = p.qt2 | |
| 358 | + item.laborCost = p.laborCost | |
| 359 | + item.refundCount = 1 | |
| 360 | + item.workers = [{ workerId: '', jksyj: '', count: '' }] | |
| 361 | + item.techTeachers = [] | |
| 362 | + } | |
| 363 | + this.redistributeWorkers(idx) | |
| 364 | + }, | |
| 365 | + onRefundCountChange(idx) { | |
| 366 | + this.redistributeWorkers(idx) | |
| 367 | + this.redistributeTechTeachers(idx) | |
| 368 | + }, | |
| 369 | + onWorkerPerformanceInput() { | |
| 370 | + this.$forceUpdate() | |
| 371 | + }, | |
| 372 | + redistributeWorkers(idx) { | |
| 373 | + const item = this.form.items[idx] | |
| 374 | + if (!item.workers || item.workers.length === 0) return | |
| 375 | + const totalCount = item.refundCount || 0 | |
| 376 | + const totalPerf = item.price * totalCount | |
| 377 | + const n = item.workers.length | |
| 378 | + item.workers.forEach(w => { | |
| 379 | + w.jksyj = (totalPerf / n).toFixed(2) | |
| 380 | + w.count = (totalCount / n).toFixed(2) | |
| 381 | + }) | |
| 382 | + }, | |
| 383 | + redistributeTechTeachers(idx) { | |
| 384 | + const item = this.form.items[idx] | |
| 385 | + if (!item.techTeachers || item.techTeachers.length === 0) return | |
| 386 | + const totalCount = item.refundCount || 0 | |
| 387 | + const totalPerf = item.price * totalCount | |
| 388 | + const n = item.techTeachers.length | |
| 389 | + item.techTeachers.forEach(t => { | |
| 390 | + t.jksyj = (totalPerf / n).toFixed(2) | |
| 391 | + t.count = (totalCount / n).toFixed(2) | |
| 392 | + }) | |
| 393 | + }, | |
| 394 | + addItem() { | |
| 395 | + this.form.items.push(this.createEmptyItem()) | |
| 396 | + }, | |
| 397 | + removeItem(idx) { | |
| 398 | + this.form.items.splice(idx, 1) | |
| 399 | + }, | |
| 400 | + addWorker(idx) { | |
| 401 | + this.form.items[idx].workers.push({ workerId: '', jksyj: '', count: '' }) | |
| 402 | + this.redistributeWorkers(idx) | |
| 403 | + }, | |
| 404 | + removeWorker(idx, wi) { | |
| 405 | + this.form.items[idx].workers.splice(wi, 1) | |
| 406 | + this.redistributeWorkers(idx) | |
| 407 | + }, | |
| 408 | + addTechTeacher(idx) { | |
| 409 | + this.form.items[idx].techTeachers.push({ teacherId: '', jksyj: '', count: '' }) | |
| 410 | + this.redistributeTechTeachers(idx) | |
| 411 | + }, | |
| 412 | + removeTechTeacher(idx, ti) { | |
| 413 | + this.form.items[idx].techTeachers.splice(ti, 1) | |
| 414 | + this.redistributeTechTeachers(idx) | |
| 415 | + }, | |
| 416 | + resetForm() { | |
| 417 | + this.form = this.createEmptyForm() | |
| 418 | + this.availableItems = [] | |
| 419 | + this.$nextTick(() => { | |
| 420 | + this.$refs.form && this.$refs.form.clearValidate() | |
| 421 | + }) | |
| 422 | + }, | |
| 423 | + handleCancel() { | |
| 424 | + this.visibleProxy = false | |
| 425 | + this.resetForm() | |
| 426 | + }, | |
| 427 | + handleSubmit() { | |
| 428 | + this.$refs.form.validate(valid => { | |
| 429 | + if (!valid) return | |
| 430 | + this.submitting = true | |
| 431 | + setTimeout(() => { | |
| 432 | + this.submitting = false | |
| 433 | + this.$message.success('退卡记录已保存(示例)') | |
| 434 | + this.$emit('submitted', this.form) | |
| 435 | + this.visibleProxy = false | |
| 436 | + this.resetForm() | |
| 437 | + }, 800) | |
| 438 | + }) | |
| 439 | + } | |
| 440 | + } | |
| 441 | +} | |
| 442 | +</script> | |
| 443 | + | |
| 444 | +<style lang="scss" scoped> | |
| 445 | +/* ====== 弹窗外壳 ====== */ | |
| 446 | +::v-deep .refund-dialog { | |
| 447 | + max-width: 1200px; | |
| 448 | + margin-top: 5vh !important; | |
| 449 | + border-radius: 20px; | |
| 450 | + padding: 0; | |
| 451 | + background: radial-gradient( | |
| 452 | + circle at 0 0, | |
| 453 | + rgba(255, 255, 255, 0.96) 0, | |
| 454 | + rgba(248, 250, 252, 0.98) 40%, | |
| 455 | + rgba(241, 245, 249, 0.98) 100% | |
| 456 | + ); | |
| 457 | + box-shadow: | |
| 458 | + 0 24px 48px rgba(15, 23, 42, 0.18), | |
| 459 | + 0 0 0 1px rgba(255, 255, 255, 0.9); | |
| 460 | + backdrop-filter: blur(22px); | |
| 461 | + -webkit-backdrop-filter: blur(22px); | |
| 462 | +} | |
| 463 | + | |
| 464 | +::v-deep .refund-dialog .el-dialog__header { | |
| 465 | + display: none; | |
| 466 | +} | |
| 467 | + | |
| 468 | +::v-deep .refund-dialog .el-dialog__body { | |
| 469 | + padding: 0; | |
| 470 | +} | |
| 471 | + | |
| 472 | +/* ====== 内部结构 ====== */ | |
| 473 | +.refund-dialog-inner { | |
| 474 | + display: flex; | |
| 475 | + flex-direction: column; | |
| 476 | + max-height: 85vh; | |
| 477 | +} | |
| 478 | + | |
| 479 | +.refund-header { | |
| 480 | + flex-shrink: 0; | |
| 481 | + display: flex; | |
| 482 | + align-items: center; | |
| 483 | + gap: 8px; | |
| 484 | + margin: 18px 22px 0; | |
| 485 | + padding: 10px 14px; | |
| 486 | + border-radius: 14px; | |
| 487 | + background: rgba(219, 234, 254, 0.96); | |
| 488 | +} | |
| 489 | + | |
| 490 | +.refund-title-wrap { | |
| 491 | + flex: 1; | |
| 492 | +} | |
| 493 | + | |
| 494 | +.refund-title { | |
| 495 | + font-size: 17px; | |
| 496 | + font-weight: 600; | |
| 497 | + color: #0f172a; | |
| 498 | +} | |
| 499 | + | |
| 500 | +.refund-subtitle { | |
| 501 | + font-size: 12px; | |
| 502 | + color: #475569; | |
| 503 | + margin-top: 2px; | |
| 504 | +} | |
| 505 | + | |
| 506 | +.refund-close { | |
| 507 | + flex-shrink: 0; | |
| 508 | + cursor: pointer; | |
| 509 | + width: 28px; | |
| 510 | + height: 28px; | |
| 511 | + display: flex; | |
| 512 | + align-items: center; | |
| 513 | + justify-content: center; | |
| 514 | + border-radius: 999px; | |
| 515 | + color: #64748b; | |
| 516 | + transition: all 0.15s; | |
| 517 | + | |
| 518 | + &:hover { | |
| 519 | + background: rgba(0, 0, 0, 0.06); | |
| 520 | + color: #0f172a; | |
| 521 | + } | |
| 522 | +} | |
| 523 | + | |
| 524 | +/* ====== 双栏主体 ====== */ | |
| 525 | +.refund-content { | |
| 526 | + flex: 1; | |
| 527 | + min-height: 0; | |
| 528 | + overflow: hidden; | |
| 529 | + display: flex; | |
| 530 | +} | |
| 531 | + | |
| 532 | +.refund-form { | |
| 533 | + display: flex; | |
| 534 | + flex: 1; | |
| 535 | + min-height: 0; | |
| 536 | +} | |
| 537 | + | |
| 538 | +.refund-left { | |
| 539 | + flex: 0 0 440px; | |
| 540 | + overflow-y: auto; | |
| 541 | + padding: 10px 16px 10px 22px; | |
| 542 | + border-right: 1px solid rgba(229, 231, 235, 0.6); | |
| 543 | + min-height: 0; | |
| 544 | +} | |
| 545 | + | |
| 546 | +.refund-right { | |
| 547 | + flex: 1; | |
| 548 | + overflow-y: auto; | |
| 549 | + padding: 10px 22px 10px 16px; | |
| 550 | + min-height: 0; | |
| 551 | +} | |
| 552 | + | |
| 553 | +/* ====== 分区标题 ====== */ | |
| 554 | +.section-title { | |
| 555 | + font-size: 13px; | |
| 556 | + font-weight: 600; | |
| 557 | + color: #334155; | |
| 558 | + margin: 14px 0 8px; | |
| 559 | + padding: 6px 10px; | |
| 560 | + border-radius: 8px; | |
| 561 | + background: rgba(241, 245, 249, 0.7); | |
| 562 | + | |
| 563 | + i { | |
| 564 | + margin-right: 4px; | |
| 565 | + color: #2563eb; | |
| 566 | + } | |
| 567 | + | |
| 568 | + &:first-child { | |
| 569 | + margin-top: 4px; | |
| 570 | + } | |
| 571 | +} | |
| 572 | + | |
| 573 | +/* ====== 品项卡片 ====== */ | |
| 574 | +.item-card { | |
| 575 | + border: 1px solid #e5e7eb; | |
| 576 | + border-radius: 12px; | |
| 577 | + padding: 10px 12px 4px; | |
| 578 | + margin-bottom: 10px; | |
| 579 | + background: rgba(255, 255, 255, 0.6); | |
| 580 | + transition: border-color 0.15s; | |
| 581 | + | |
| 582 | + &:hover { | |
| 583 | + border-color: #93c5fd; | |
| 584 | + } | |
| 585 | +} | |
| 586 | + | |
| 587 | +.item-card-head { | |
| 588 | + display: flex; | |
| 589 | + align-items: center; | |
| 590 | + justify-content: space-between; | |
| 591 | + margin-bottom: 6px; | |
| 592 | +} | |
| 593 | + | |
| 594 | +.item-card-no { | |
| 595 | + font-size: 12px; | |
| 596 | + font-weight: 600; | |
| 597 | + color: #2563eb; | |
| 598 | +} | |
| 599 | + | |
| 600 | +.item-remove-btn { | |
| 601 | + color: #ef4444 !important; | |
| 602 | + font-size: 12px; | |
| 603 | + padding: 0; | |
| 604 | +} | |
| 605 | + | |
| 606 | +/* ====== 品项信息面板 ====== */ | |
| 607 | +.px-info-panel { | |
| 608 | + margin: 4px 0 6px; | |
| 609 | + padding: 8px 10px; | |
| 610 | + border-radius: 8px; | |
| 611 | + background: rgba(241, 245, 249, 0.5); | |
| 612 | + font-size: 12px; | |
| 613 | + color: #475569; | |
| 614 | + line-height: 1.8; | |
| 615 | + | |
| 616 | + .px-tag { | |
| 617 | + color: #94a3b8; | |
| 618 | + margin-right: 2px; | |
| 619 | + } | |
| 620 | + | |
| 621 | + b { | |
| 622 | + color: #0f172a; | |
| 623 | + font-weight: 600; | |
| 624 | + } | |
| 625 | + | |
| 626 | + .remaining { | |
| 627 | + color: #2563eb; | |
| 628 | + } | |
| 629 | +} | |
| 630 | + | |
| 631 | +/* ====== 健康师 / 科技部老师行 ====== */ | |
| 632 | +.worker-section { | |
| 633 | + margin: 2px 0 6px; | |
| 634 | + padding: 8px 10px; | |
| 635 | + border-radius: 8px; | |
| 636 | + background: rgba(241, 245, 249, 0.5); | |
| 637 | +} | |
| 638 | + | |
| 639 | +.worker-label { | |
| 640 | + display: flex; | |
| 641 | + align-items: center; | |
| 642 | + justify-content: space-between; | |
| 643 | + margin-bottom: 6px; | |
| 644 | + font-size: 12px; | |
| 645 | + color: #475569; | |
| 646 | + font-weight: 500; | |
| 647 | +} | |
| 648 | + | |
| 649 | +.worker-row { | |
| 650 | + display: flex; | |
| 651 | + align-items: center; | |
| 652 | + gap: 8px; | |
| 653 | + margin-bottom: 6px; | |
| 654 | +} | |
| 655 | + | |
| 656 | +.worker-select { | |
| 657 | + flex: 1; | |
| 658 | +} | |
| 659 | + | |
| 660 | +.worker-field { | |
| 661 | + width: 120px; | |
| 662 | + flex-shrink: 0; | |
| 663 | +} | |
| 664 | + | |
| 665 | +.worker-field-sm { | |
| 666 | + width: 70px; | |
| 667 | + flex-shrink: 0; | |
| 668 | +} | |
| 669 | + | |
| 670 | +.worker-remove { | |
| 671 | + color: #ef4444 !important; | |
| 672 | + padding: 0; | |
| 673 | +} | |
| 674 | + | |
| 675 | +/* ====== 添加按钮行 ====== */ | |
| 676 | +.add-btn-row { | |
| 677 | + text-align: center; | |
| 678 | + margin-bottom: 6px; | |
| 679 | +} | |
| 680 | + | |
| 681 | +/* ====== 上传提示 ====== */ | |
| 682 | +.upload-tip { | |
| 683 | + font-size: 11px; | |
| 684 | + color: #9ca3af; | |
| 685 | + margin-left: 8px; | |
| 686 | +} | |
| 687 | + | |
| 688 | +/* ====== footer ====== */ | |
| 689 | +.refund-footer { | |
| 690 | + flex-shrink: 0; | |
| 691 | + display: flex; | |
| 692 | + align-items: center; | |
| 693 | + justify-content: space-between; | |
| 694 | + padding: 10px 22px 14px; | |
| 695 | + border-top: 1px solid rgba(229, 231, 235, 0.6); | |
| 696 | +} | |
| 697 | + | |
| 698 | +.footer-summary { | |
| 699 | + display: flex; | |
| 700 | + gap: 16px; | |
| 701 | + font-size: 12px; | |
| 702 | + color: #64748b; | |
| 703 | + | |
| 704 | + b { | |
| 705 | + color: #0f172a; | |
| 706 | + } | |
| 707 | + | |
| 708 | + .highlight { | |
| 709 | + color: #2563eb; | |
| 710 | + font-size: 14px; | |
| 711 | + } | |
| 712 | + | |
| 713 | + .highlight-orange { | |
| 714 | + color: #f97316; | |
| 715 | + font-size: 14px; | |
| 716 | + } | |
| 717 | +} | |
| 718 | + | |
| 719 | +.footer-actions { | |
| 720 | + display: flex; | |
| 721 | + gap: 10px; | |
| 722 | +} | |
| 723 | + | |
| 724 | +/* ====== 统一输入框 / 按钮风格 ====== */ | |
| 725 | +::v-deep .refund-dialog .el-form-item__label { | |
| 726 | + white-space: nowrap; | |
| 727 | +} | |
| 728 | + | |
| 729 | +::v-deep .refund-dialog .el-input__inner { | |
| 730 | + border-radius: 999px; | |
| 731 | + height: 32px; | |
| 732 | + line-height: 32px; | |
| 733 | + border-color: #e5e7eb; | |
| 734 | + background-color: #f9fafb; | |
| 735 | +} | |
| 736 | + | |
| 737 | +::v-deep .refund-dialog .el-input__inner:focus { | |
| 738 | + border-color: #2563eb; | |
| 739 | + box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.18); | |
| 740 | +} | |
| 741 | + | |
| 742 | +::v-deep .refund-dialog .el-input-group__prepend { | |
| 743 | + border-radius: 999px 0 0 999px; | |
| 744 | + background: #f1f5f9; | |
| 745 | + border-color: #e5e7eb; | |
| 746 | + padding: 0 10px; | |
| 747 | + color: #64748b; | |
| 748 | +} | |
| 749 | + | |
| 750 | +::v-deep .refund-dialog .el-input-group .el-input__inner { | |
| 751 | + border-radius: 0 999px 999px 0; | |
| 752 | +} | |
| 753 | + | |
| 754 | +::v-deep .refund-dialog .el-textarea__inner { | |
| 755 | + border-radius: 12px; | |
| 756 | + border-color: #e5e7eb; | |
| 757 | + background-color: #f9fafb; | |
| 758 | +} | |
| 759 | + | |
| 760 | +::v-deep .refund-dialog .el-textarea__inner:focus { | |
| 761 | + border-color: #2563eb; | |
| 762 | + box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.18); | |
| 763 | +} | |
| 764 | + | |
| 765 | +::v-deep .refund-dialog .el-input-number { | |
| 766 | + .el-input__inner { | |
| 767 | + border-radius: 999px; | |
| 768 | + } | |
| 769 | +} | |
| 770 | + | |
| 771 | +::v-deep .refund-dialog .el-upload-list__item { | |
| 772 | + border-radius: 8px; | |
| 773 | +} | |
| 774 | + | |
| 775 | +::v-deep .refund-dialog .el-button--primary { | |
| 776 | + border-radius: 999px; | |
| 777 | + padding: 0 20px; | |
| 778 | + height: 30px; | |
| 779 | + line-height: 30px; | |
| 780 | + background: #2563eb; | |
| 781 | + border-color: #2563eb; | |
| 782 | + box-shadow: 0 4px 10px rgba(37, 99, 235, 0.35); | |
| 783 | + font-size: 12px; | |
| 784 | +} | |
| 785 | + | |
| 786 | +::v-deep .refund-dialog .el-button--primary.is-disabled { | |
| 787 | + box-shadow: none; | |
| 788 | +} | |
| 789 | + | |
| 790 | +::v-deep .refund-dialog .el-button--default { | |
| 791 | + border-radius: 999px; | |
| 792 | + padding: 0 18px; | |
| 793 | + height: 30px; | |
| 794 | + line-height: 30px; | |
| 795 | + background: rgba(239, 246, 255, 0.9); | |
| 796 | + color: #2563eb; | |
| 797 | + border-color: rgba(37, 99, 235, 0.18); | |
| 798 | + font-size: 12px; | |
| 799 | +} | |
| 800 | + | |
| 801 | +::v-deep .refund-dialog .el-button--default:hover { | |
| 802 | + background: rgba(219, 234, 254, 0.95); | |
| 803 | + color: #1d4ed8; | |
| 804 | +} | |
| 805 | + | |
| 806 | +::v-deep .refund-dialog .el-form-item { | |
| 807 | + margin-bottom: 12px; | |
| 808 | +} | |
| 809 | + | |
| 810 | +::v-deep .refund-dialog .el-picker-panel { | |
| 811 | + border-radius: 12px; | |
| 812 | +} | |
| 813 | + | |
| 814 | +::v-deep .refund-dialog .el-select { | |
| 815 | + width: 100%; | |
| 816 | +} | |
| 817 | +</style> | ... | ... |
store-pc/src/components/RefundListDialog.vue
0 → 100644
| 1 | +<template> | |
| 2 | + <el-dialog | |
| 3 | + :visible.sync="visibleProxy" | |
| 4 | + :show-close="false" | |
| 5 | + width="90%" | |
| 6 | + :close-on-click-modal="false" | |
| 7 | + custom-class="refund-list-dialog" | |
| 8 | + append-to-body | |
| 9 | + > | |
| 10 | + <div class="dialog-inner"> | |
| 11 | + <div class="dialog-header"> | |
| 12 | + <div class="dialog-title">退卡记录</div> | |
| 13 | + <span class="dialog-close" @click="visibleProxy = false"><i class="el-icon-close"></i></span> | |
| 14 | + </div> | |
| 15 | + | |
| 16 | + <div class="dialog-search"> | |
| 17 | + <el-form @submit.native.prevent :inline="true" size="small"> | |
| 18 | + <el-form-item label="会员"> | |
| 19 | + <el-select v-model="query.hy" filterable remote reserve-keyword clearable placeholder="搜索会员" :remote-method="searchMember" :loading="memberLoading" style="width:200px"> | |
| 20 | + <el-option v-for="m in memberOptions" :key="m.value" :label="m.label" :value="m.value" /> | |
| 21 | + </el-select> | |
| 22 | + </el-form-item> | |
| 23 | + <template v-if="showAll"> | |
| 24 | + <el-form-item label="退卡时间"> | |
| 25 | + <el-date-picker v-model="query.tksj" type="daterange" value-format="timestamp" format="yyyy-MM-dd" start-placeholder="开始" end-placeholder="结束" style="width:220px" /> | |
| 26 | + </el-form-item> | |
| 27 | + <el-form-item label="健康师"> | |
| 28 | + <el-select v-model="query.jksId" placeholder="健康师" clearable filterable style="width:150px"> | |
| 29 | + <el-option v-for="h in jksOptions" :key="h.id" :label="h.fullName" :value="h.id" /> | |
| 30 | + </el-select> | |
| 31 | + </el-form-item> | |
| 32 | + <el-form-item label="科技老师"> | |
| 33 | + <el-select v-model="query.kjblsId" placeholder="科技老师" clearable filterable style="width:150px"> | |
| 34 | + <el-option v-for="t in kjbOptions" :key="t.id" :label="t.fullName" :value="t.id" /> | |
| 35 | + </el-select> | |
| 36 | + </el-form-item> | |
| 37 | + <el-form-item label="是否作废"> | |
| 38 | + <el-select v-model="query.isEffective" placeholder="请选择" clearable style="width:100px"> | |
| 39 | + <el-option label="正常" value="1" /><el-option label="作废" value="-1" /> | |
| 40 | + </el-select> | |
| 41 | + </el-form-item> | |
| 42 | + </template> | |
| 43 | + <el-form-item> | |
| 44 | + <el-button type="primary" @click="search">查询</el-button> | |
| 45 | + <el-button @click="reset">重置</el-button> | |
| 46 | + <el-button type="text" @click="showAll = !showAll"> | |
| 47 | + {{ showAll ? '收起' : '展开' }} <i :class="showAll ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"></i> | |
| 48 | + </el-button> | |
| 49 | + </el-form-item> | |
| 50 | + </el-form> | |
| 51 | + </div> | |
| 52 | + | |
| 53 | + <div class="dialog-content"> | |
| 54 | + <el-table v-loading="loading" :data="list" border size="small" :header-cell-style="{ background:'#f5f7fa', color:'#606266', fontWeight:600 }"> | |
| 55 | + <el-table-column type="expand" width="50"> | |
| 56 | + <template slot-scope="{ row }"> | |
| 57 | + <div class="expand-box" v-if="row.lqHytkMxList && row.lqHytkMxList.length"> | |
| 58 | + <div class="expand-title"><i class="el-icon-goods"></i> 退卡明细</div> | |
| 59 | + <el-table :data="row.lqHytkMxList" border size="mini" :header-cell-style="{ background:'#f5f7fa', color:'#606266' }"> | |
| 60 | + <el-table-column prop="pxmc" label="项目名称" width="180" /> | |
| 61 | + <el-table-column label="项目价格" width="120" align="right"> | |
| 62 | + <template slot-scope="s">¥{{ formatMoney(s.row.pxjg) }}</template> | |
| 63 | + </el-table-column> | |
| 64 | + <el-table-column prop="projectNumber" label="次数" width="80" align="right" /> | |
| 65 | + <el-table-column label="退款金额" width="120" align="right"> | |
| 66 | + <template slot-scope="s"><span style="color:#F56C6C;font-weight:600">¥{{ formatMoney(s.row.tkje) }}</span></template> | |
| 67 | + </el-table-column> | |
| 68 | + <el-table-column label="总价" width="120" align="right"> | |
| 69 | + <template slot-scope="s">¥{{ formatMoney(s.row.totalPrice) }}</template> | |
| 70 | + </el-table-column> | |
| 71 | + <el-table-column label="来源" width="100"> | |
| 72 | + <template slot-scope="s"><el-tag size="mini" type="info">{{ s.row.sourceType || '-' }}</el-tag></template> | |
| 73 | + </el-table-column> | |
| 74 | + <el-table-column label="是否有效" width="90" align="center"> | |
| 75 | + <template slot-scope="s">{{ s.row.isEffective == 1 ? '有效' : '无效' }}</template> | |
| 76 | + </el-table-column> | |
| 77 | + </el-table> | |
| 78 | + </div> | |
| 79 | + <div v-else class="expand-empty"><i class="el-icon-info"></i> 暂无退卡明细</div> | |
| 80 | + </template> | |
| 81 | + </el-table-column> | |
| 82 | + <el-table-column prop="mdmc" label="门店名称" show-overflow-tooltip /> | |
| 83 | + <el-table-column prop="hymc" label="会员姓名" /> | |
| 84 | + <el-table-column prop="hyPhone" label="手机号" width="120" /> | |
| 85 | + <el-table-column label="退款金额" width="110" align="right"> | |
| 86 | + <template slot-scope="{ row }"><span style="color:#409EFF;font-weight:600">{{ row.tkje || '-' }}</span></template> | |
| 87 | + </el-table-column> | |
| 88 | + <el-table-column label="实际退款金额" width="130" align="right"> | |
| 89 | + <template slot-scope="{ row }"><span style="color:#F56C6C;font-weight:600">{{ row.actualRefundAmount || '-' }}</span></template> | |
| 90 | + </el-table-column> | |
| 91 | + <el-table-column label="退卡时间" width="110"> | |
| 92 | + <template slot-scope="{ row }">{{ formatDate(row.tksj) }}</template> | |
| 93 | + </el-table-column> | |
| 94 | + <el-table-column label="是否作废" width="90" align="center"> | |
| 95 | + <template slot-scope="{ row }">{{ row.isEffective == '1' ? '正常' : '作废' }}</template> | |
| 96 | + </el-table-column> | |
| 97 | + <el-table-column prop="bz" label="备注" show-overflow-tooltip /> | |
| 98 | + <el-table-column label="操作人" width="120" show-overflow-tooltip> | |
| 99 | + <template slot-scope="{ row }">{{ row.czry || '-' }}</template> | |
| 100 | + </el-table-column> | |
| 101 | + </el-table> | |
| 102 | + </div> | |
| 103 | + | |
| 104 | + <div class="dialog-footer"> | |
| 105 | + <el-pagination background layout="total, sizes, prev, pager, next, jumper" :total="total" :page-size.sync="listQuery.pageSize" :current-page.sync="listQuery.currentPage" :page-sizes="[10, 20, 50, 100]" @size-change="initData" @current-change="initData" /> | |
| 106 | + </div> | |
| 107 | + </div> | |
| 108 | + </el-dialog> | |
| 109 | +</template> | |
| 110 | + | |
| 111 | +<script> | |
| 112 | +const MOCK_REFUND = [ | |
| 113 | + { id: '1', mdmc: '绿纤西站店', hymc: '何文慧', hyPhone: '19114671036', tkje: '133.34', actualRefundAmount: '133.00', tksj: '2026-02-11 15:48:40', bz: '', isEffective: 1, czry: '李经理', lqHytkMxList: [{ pxmc: '女神卡', pxjg: '66.67', projectNumber: '2', tkje: '133.34', totalPrice: '133.34', sourceType: '购买', isEffective: 1 }, { pxmc: '水氧-面部', pxjg: '0.00', projectNumber: '1', tkje: '0.00', totalPrice: '0.00', sourceType: '赠送', isEffective: 1 }] }, | |
| 114 | + { id: '2', mdmc: '绿纤南湖店', hymc: '沈雪莲', hyPhone: '19914258613', tkje: '1199.00', actualRefundAmount: '1199.00', tksj: '2026-02-11 15:34:05', bz: '人去了新疆', isEffective: 1, czry: '郝莉娜', lqHytkMxList: [{ pxmc: '年卡', pxjg: '1199.00', projectNumber: '1', tkje: '1199.00', totalPrice: '1199.00', sourceType: '购买', isEffective: 1 }] }, | |
| 115 | + { id: '3', mdmc: '绿纤保利店', hymc: '陈秋媛', hyPhone: '13350688821', tkje: '3054.59', actualRefundAmount: '868.00', tksj: '2026-02-11 13:48:53', bz: '', isEffective: 1, czry: '贾琳', lqHytkMxList: [{ pxmc: '美容套卡', pxjg: '1000.00', projectNumber: '2', tkje: '2000.00', totalPrice: '2000.00', sourceType: '购买', isEffective: 1 }, { pxmc: '胶原宝宝-双部位', pxjg: '527.30', projectNumber: '2', tkje: '1054.59', totalPrice: '1054.59', sourceType: '购买', isEffective: 1 }] }, | |
| 116 | + { id: '4', mdmc: '绿纤双流店', hymc: '唐糠琼', hyPhone: '15108382205', tkje: '1000.00', actualRefundAmount: '1000.00', tksj: '2026-02-08 15:36:01', bz: '', isEffective: 1, czry: '张经理', lqHytkMxList: [{ pxmc: '美容套卡', pxjg: '1000.00', projectNumber: '1', tkje: '1000.00', totalPrice: '1000.00', sourceType: '购买', isEffective: 1 }] }, | |
| 117 | + { id: '5', mdmc: '绿纤融创店', hymc: '何虹霖', hyPhone: '18200502182', tkje: '10000.00', actualRefundAmount: '10000.00', tksj: '2026-02-06 18:12:39', bz: '', isEffective: 1, czry: '王经理', lqHytkMxList: [{ pxmc: '钻石卡', pxjg: '10000.00', projectNumber: '1', tkje: '10000.00', totalPrice: '10000.00', sourceType: '购买', isEffective: 1 }] }, | |
| 118 | + { id: '6', mdmc: '绿纤骑士郡店', hymc: '陈玲', hyPhone: '17340223822', tkje: '1998.96', actualRefundAmount: '1998.96', tksj: '2026-02-05 09:32:05', bz: '转卡给会员:陈倩', isEffective: -1, czry: '刘经理', lqHytkMxList: [{ pxmc: '季卡', pxjg: '999.48', projectNumber: '2', tkje: '1998.96', totalPrice: '1998.96', sourceType: '购买', isEffective: 1 }] }, | |
| 119 | + { id: '7', mdmc: '绿纤西站店', hymc: '廖艳琼', hyPhone: '13551188224', tkje: '1000.00', actualRefundAmount: '1000.00', tksj: '2026-02-03 18:11:45', bz: '', isEffective: 1, czry: '李经理', lqHytkMxList: [{ pxmc: '美容套卡', pxjg: '1000.00', projectNumber: '1', tkje: '1000.00', totalPrice: '1000.00', sourceType: '购买', isEffective: 1 }] }, | |
| 120 | + { id: '8', mdmc: '绿纤川音店', hymc: '王雁', hyPhone: '13679057358', tkje: '10000.00', actualRefundAmount: '10000.00', tksj: '2026-02-03 17:33:54', bz: '', isEffective: 1, czry: '陈经理', lqHytkMxList: [{ pxmc: '钻石卡', pxjg: '10000.00', projectNumber: '1', tkje: '10000.00', totalPrice: '10000.00', sourceType: '购买', isEffective: 1 }] }, | |
| 121 | + { id: '9', mdmc: '绿纤凤凰山店', hymc: '陈小丽', hyPhone: '18122879216', tkje: '308.90', actualRefundAmount: '308.90', tksj: '2026-01-31 12:04:38', bz: '', isEffective: 1, czry: '周经理', lqHytkMxList: [{ pxmc: '基础护理', pxjg: '308.90', projectNumber: '1', tkje: '308.90', totalPrice: '308.90', sourceType: '购买', isEffective: 1 }] }, | |
| 122 | + { id: '10', mdmc: '绿纤龙城国际店', hymc: '蒋女士', hyPhone: '18982067793', tkje: '0.00', actualRefundAmount: '0.00', tksj: '2026-02-02 18:51:19', bz: '', isEffective: 1, czry: '赵经理', lqHytkMxList: [{ pxmc: '水氧-面部', pxjg: '0.00', projectNumber: '1', tkje: '0.00', totalPrice: '0.00', sourceType: '赠送', isEffective: 1 }] } | |
| 123 | +] | |
| 124 | +export default { | |
| 125 | + name: 'RefundListDialog', | |
| 126 | + props: { visible: { type: Boolean, default: false } }, | |
| 127 | + data() { | |
| 128 | + return { | |
| 129 | + mockData: MOCK_REFUND, | |
| 130 | + showAll: false, loading: false, memberLoading: false, | |
| 131 | + memberOptions: [], jksOptions: [], kjbOptions: [], | |
| 132 | + list: [], total: 0, | |
| 133 | + query: { hy: undefined, tksj: undefined, jksId: undefined, kjblsId: undefined, isEffective: undefined }, | |
| 134 | + listQuery: { currentPage: 1, pageSize: 10, sort: 'desc', sidx: '' } | |
| 135 | + } | |
| 136 | + }, | |
| 137 | + computed: { | |
| 138 | + visibleProxy: { get() { return this.visible }, set(v) { this.$emit('update:visible', v) } } | |
| 139 | + }, | |
| 140 | + watch: { | |
| 141 | + visible(v) { if (v) { this.initData(); this.loadMembers(); this.loadStaff() } } | |
| 142 | + }, | |
| 143 | + methods: { | |
| 144 | + formatDate(ts) { | |
| 145 | + if (!ts) return '-' | |
| 146 | + if (typeof ts === 'string' && ts.includes('-')) return ts.substring(0, 10) | |
| 147 | + const d = new Date(typeof ts === 'number' ? ts : Number(ts)) | |
| 148 | + if (isNaN(d.getTime())) return '-' | |
| 149 | + return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}` | |
| 150 | + }, | |
| 151 | + formatMoney(v) { return v != null ? Number(v).toFixed(2) : '0.00' }, | |
| 152 | + loadMembers() { | |
| 153 | + const seen = new Map() | |
| 154 | + this.mockData.forEach(r => { if (!seen.has(r.hyPhone)) { seen.set(r.hyPhone, true); this.memberOptions.push({ value: r.hyPhone, label: `${r.hymc}(${r.hyPhone})` }) } }) | |
| 155 | + }, | |
| 156 | + searchMember(kw) { | |
| 157 | + if (!kw) return | |
| 158 | + this.memberLoading = true | |
| 159 | + setTimeout(() => { | |
| 160 | + const lower = kw.toLowerCase() | |
| 161 | + this.memberOptions = this.mockData | |
| 162 | + .filter(r => r.hymc.toLowerCase().includes(lower) || r.hyPhone.includes(kw)) | |
| 163 | + .reduce((acc, r) => { if (!acc.find(a => a.value === r.hyPhone)) acc.push({ value: r.hyPhone, label: `${r.hymc}(${r.hyPhone})` }); return acc }, []) | |
| 164 | + this.memberLoading = false | |
| 165 | + }, 300) | |
| 166 | + }, | |
| 167 | + loadStaff() { | |
| 168 | + const jksSet = new Map(), kjbSet = new Map() | |
| 169 | + this.mockData.forEach(r => { | |
| 170 | + if (r.czry) { const name = r.czry.trim(); if (name && !jksSet.has(name)) { jksSet.set(name, true); this.jksOptions.push({ id: name, fullName: name }) } } | |
| 171 | + if (r.kjbName) r.kjbName.split(',').forEach(n => { const name = n.trim(); if (name && !kjbSet.has(name)) { kjbSet.set(name, true); this.kjbOptions.push({ id: name, fullName: name }) } }) | |
| 172 | + }) | |
| 173 | + }, | |
| 174 | + initData() { | |
| 175 | + this.loading = true | |
| 176 | + setTimeout(() => { | |
| 177 | + let filtered = [...this.mockData] | |
| 178 | + if (this.query.tksj && this.query.tksj.length === 2) { | |
| 179 | + const [s, e] = this.query.tksj | |
| 180 | + filtered = filtered.filter(r => { const t = new Date(r.tksj).getTime(); return t >= s && t <= e + 86400000 }) | |
| 181 | + } | |
| 182 | + if (this.query.hy) filtered = filtered.filter(r => r.hyPhone === this.query.hy) | |
| 183 | + if (this.query.jksId) filtered = filtered.filter(r => r.czry && r.czry.includes(this.query.jksId)) | |
| 184 | + if (this.query.kjblsId) filtered = filtered.filter(r => r.kjbName && r.kjbName.includes(this.query.kjblsId)) | |
| 185 | + if (this.query.isEffective) filtered = filtered.filter(r => String(r.isEffective) === this.query.isEffective) | |
| 186 | + this.total = filtered.length | |
| 187 | + const start = (this.listQuery.currentPage - 1) * this.listQuery.pageSize | |
| 188 | + this.list = filtered.slice(start, start + this.listQuery.pageSize) | |
| 189 | + this.loading = false | |
| 190 | + }, 300) | |
| 191 | + }, | |
| 192 | + search() { this.listQuery.currentPage = 1; this.initData() }, | |
| 193 | + reset() { for (const k in this.query) this.query[k] = undefined; this.listQuery = { currentPage: 1, pageSize: 10, sort: 'desc', sidx: '' }; this.initData() } | |
| 194 | + } | |
| 195 | +} | |
| 196 | +</script> | |
| 197 | + | |
| 198 | +<style lang="scss" scoped> | |
| 199 | +::v-deep .refund-list-dialog { max-width: 1600px; margin-top: 3vh !important; border-radius: 20px; padding: 0; background: radial-gradient(circle at 0 0, rgba(255,255,255,0.96) 0, rgba(248,250,252,0.98) 40%, rgba(241,245,249,0.98) 100%); box-shadow: 0 24px 48px rgba(15,23,42,0.18), 0 0 0 1px rgba(255,255,255,0.9); backdrop-filter: blur(22px); -webkit-backdrop-filter: blur(22px); } | |
| 200 | +::v-deep .el-dialog__header { display: none; } | |
| 201 | +::v-deep .el-dialog__body { padding: 0; } | |
| 202 | +.dialog-inner { display: flex; flex-direction: column; max-height: 92vh; } | |
| 203 | +.dialog-header { flex-shrink: 0; display: flex; align-items: center; justify-content: space-between; margin: 18px 22px 0; padding: 10px 14px; border-radius: 14px; background: rgba(219,234,254,0.96); } | |
| 204 | +.dialog-title { font-size: 17px; font-weight: 600; color: #0f172a; } | |
| 205 | +.dialog-close { cursor: pointer; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border-radius: 999px; color: #64748b; transition: all 0.15s; &:hover { background: rgba(0,0,0,0.06); color: #0f172a; } } | |
| 206 | +.dialog-search { flex-shrink: 0; padding: 12px 22px 4px; } | |
| 207 | +.dialog-content { flex: 1; min-height: 0; overflow: auto; padding: 0 22px; } | |
| 208 | +.dialog-footer { flex-shrink: 0; display: flex; align-items: center; justify-content: flex-end; padding: 10px 22px 14px; border-top: 1px solid rgba(229,231,235,0.6); } | |
| 209 | +.expand-box { padding: 14px; background: #fafafa; border-radius: 8px; margin: 6px 0; .expand-title { display: flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 600; color: #303133; margin-bottom: 10px; i { color: #409EFF; } } } | |
| 210 | +.expand-empty { padding: 16px; text-align: center; color: #909399; font-size: 13px; } | |
| 211 | +::v-deep .refund-list-dialog .el-input__inner { border-radius: 999px; height: 32px; line-height: 32px; border-color: #e5e7eb; background-color: #f9fafb; &:focus { border-color: #2563eb; box-shadow: 0 0 0 1px rgba(37,99,235,0.18); } } | |
| 212 | +::v-deep .refund-list-dialog .el-button--primary { border-radius: 999px; background: #2563eb; border-color: #2563eb; box-shadow: 0 4px 10px rgba(37,99,235,0.35); } | |
| 213 | +::v-deep .refund-list-dialog .el-button--default { border-radius: 999px; } | |
| 214 | +::v-deep .refund-list-dialog .el-form-item { margin-bottom: 8px; } | |
| 215 | +::v-deep .refund-list-dialog .el-form-item__label { white-space: nowrap; } | |
| 216 | +::v-deep .refund-list-dialog .el-range-editor.el-input__inner { border-radius: 999px; } | |
| 217 | +</style> | ... | ... |
store-pc/src/components/TuokeListDialog.vue
0 → 100644
| 1 | +<template> | |
| 2 | + <el-dialog | |
| 3 | + :visible.sync="visibleProxy" | |
| 4 | + :show-close="false" | |
| 5 | + width="90%" | |
| 6 | + :close-on-click-modal="false" | |
| 7 | + custom-class="tuoke-list-dialog" | |
| 8 | + append-to-body | |
| 9 | + > | |
| 10 | + <div class="dialog-inner"> | |
| 11 | + <div class="dialog-header"> | |
| 12 | + <div class="dialog-title">拓客数据</div> | |
| 13 | + <span class="dialog-close" @click="visibleProxy = false"><i class="el-icon-close"></i></span> | |
| 14 | + </div> | |
| 15 | + | |
| 16 | + <div class="dialog-search"> | |
| 17 | + <el-form @submit.native.prevent :inline="true" size="small"> | |
| 18 | + <el-form-item label="拓客时间"> | |
| 19 | + <el-date-picker v-model="query.expansionTime" type="daterange" value-format="timestamp" format="yyyy-MM-dd" start-placeholder="开始" end-placeholder="结束" style="width:220px" /> | |
| 20 | + </el-form-item> | |
| 21 | + <el-form-item label="拓客活动"> | |
| 22 | + <el-select v-model="query.eventId" placeholder="请选择" clearable filterable style="width:180px" :loading="eventLoading"> | |
| 23 | + <el-option v-for="e in eventList" :key="e.id" :label="e.eventName" :value="e.id" /> | |
| 24 | + </el-select> | |
| 25 | + </el-form-item> | |
| 26 | + <el-form-item label="顾客姓名"> | |
| 27 | + <el-input v-model="query.customerName" placeholder="顾客姓名" clearable style="width:140px" /> | |
| 28 | + </el-form-item> | |
| 29 | + <el-form-item label="电话号码"> | |
| 30 | + <el-input v-model="query.customerPhone" placeholder="电话号码" clearable style="width:140px" /> | |
| 31 | + </el-form-item> | |
| 32 | + <template v-if="showAll"> | |
| 33 | + <el-form-item label="购买张数"> | |
| 34 | + <el-input v-model="query.buyNumber" placeholder="购买张数" clearable style="width:120px" /> | |
| 35 | + </el-form-item> | |
| 36 | + <el-form-item label="支付方式"> | |
| 37 | + <el-select v-model="query.paymentMethod" placeholder="支付方式" clearable style="width:120px"> | |
| 38 | + <el-option v-for="p in payMethodOptions" :key="p" :label="p" :value="p" /> | |
| 39 | + </el-select> | |
| 40 | + </el-form-item> | |
| 41 | + <el-form-item label="是否加微信"> | |
| 42 | + <el-select v-model="query.isAddWeChat" placeholder="是否加微信" clearable style="width:120px"> | |
| 43 | + <el-option label="是" value="是" /><el-option label="否" value="否" /> | |
| 44 | + </el-select> | |
| 45 | + </el-form-item> | |
| 46 | + <el-form-item label="所属战队"> | |
| 47 | + <el-input v-model="query.teamName" placeholder="战队名称" clearable style="width:130px" /> | |
| 48 | + </el-form-item> | |
| 49 | + </template> | |
| 50 | + <el-form-item> | |
| 51 | + <el-button type="primary" @click="search">查询</el-button> | |
| 52 | + <el-button @click="reset">重置</el-button> | |
| 53 | + <el-button type="text" @click="showAll = !showAll"> | |
| 54 | + {{ showAll ? '收起' : '展开' }} | |
| 55 | + <i :class="showAll ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"></i> | |
| 56 | + </el-button> | |
| 57 | + </el-form-item> | |
| 58 | + </el-form> | |
| 59 | + </div> | |
| 60 | + | |
| 61 | + <div class="dialog-content"> | |
| 62 | + <el-table v-loading="loading" :data="list" border size="small" :header-cell-style="{ background:'#f5f7fa', color:'#606266', fontWeight:600 }"> | |
| 63 | + <el-table-column prop="eventName" label="拓客活动" show-overflow-tooltip /> | |
| 64 | + <el-table-column prop="expansionUserName" label="拓客人员" show-overflow-tooltip /> | |
| 65 | + <el-table-column prop="storeName" label="所属门店" show-overflow-tooltip /> | |
| 66 | + <el-table-column label="拓客时间" width="140"> | |
| 67 | + <template slot-scope="{ row }">{{ formatDate(row.expansionTime) }}</template> | |
| 68 | + </el-table-column> | |
| 69 | + <el-table-column prop="customerName" label="顾客姓名" show-overflow-tooltip /> | |
| 70 | + <el-table-column prop="customerPhone" label="电话号码" show-overflow-tooltip /> | |
| 71 | + <el-table-column prop="buyNumber" label="购买张数" width="90" /> | |
| 72 | + <el-table-column prop="paymentMethod" label="支付方式" width="100" /> | |
| 73 | + <el-table-column prop="isAddWeChat" label="是否加微信" width="110" /> | |
| 74 | + <el-table-column prop="teamName" label="所属战队" show-overflow-tooltip /> | |
| 75 | + <el-table-column prop="remarks" label="备注" width="260" show-overflow-tooltip /> | |
| 76 | + </el-table> | |
| 77 | + </div> | |
| 78 | + | |
| 79 | + <div class="dialog-footer"> | |
| 80 | + <el-pagination | |
| 81 | + background | |
| 82 | + layout="total, sizes, prev, pager, next, jumper" | |
| 83 | + :total="total" | |
| 84 | + :page-size.sync="listQuery.pageSize" | |
| 85 | + :current-page.sync="listQuery.currentPage" | |
| 86 | + :page-sizes="[10, 20, 50, 100]" | |
| 87 | + @size-change="initData" | |
| 88 | + @current-change="initData" | |
| 89 | + /> | |
| 90 | + </div> | |
| 91 | + </div> | |
| 92 | + </el-dialog> | |
| 93 | +</template> | |
| 94 | + | |
| 95 | +<script> | |
| 96 | +const MOCK_TUOKE = [ | |
| 97 | + { id: '1', eventName: '龙城国际20260210', teamName: '3', storeName: '龙城国际', expansionTime: '2026-02-10 17:00:51', expansionUserName: '赵倩', customerName: '李女士', customerPhone: '18571990586', buyNumber: 1, paymentMethod: '微信', isAddWeChat: '是', remarks: '年后来做' }, | |
| 98 | + { id: '2', eventName: '红光20260210', teamName: '3', storeName: '红光', expansionTime: '2026-02-10 16:41:39', expansionUserName: '赵倩', customerName: '阮女士', customerPhone: '17320569150', buyNumber: 1, paymentMethod: '微信', isAddWeChat: '否', remarks: '和王一起来,龙城国际体验' }, | |
| 99 | + { id: '3', eventName: '龙城国际20260210', teamName: '3', storeName: '龙城国际', expansionTime: '2026-02-10 16:40:53', expansionUserName: '赵倩', customerName: '王女士', customerPhone: '17674617541', buyNumber: 1, paymentMethod: '微信', isAddWeChat: '是', remarks: '' }, | |
| 100 | + { id: '4', eventName: '红光20260210', teamName: '1', storeName: '红光', expansionTime: '2026-02-10 16:18:30', expansionUserName: '宁燕', customerName: '杜女士', customerPhone: '18990421079', buyNumber: 1, paymentMethod: '微信', isAddWeChat: '是', remarks: '不要礼品,做4个项目' }, | |
| 101 | + { id: '5', eventName: '犀浦20260206', teamName: '杨钦岚', storeName: '犀浦', expansionTime: '2026-02-06 17:39:33', expansionUserName: '杨钦岚', customerName: '吕女士', customerPhone: '18708437941', buyNumber: 1, paymentMethod: '微信', isAddWeChat: '是', remarks: '' }, | |
| 102 | + { id: '6', eventName: '犀浦20260206', teamName: '杨钦岚', storeName: '犀浦', expansionTime: '2026-02-06 16:00:55', expansionUserName: '杨钦岚', customerName: '李女士', customerPhone: '13228452004', buyNumber: 1, paymentMethod: '微信', isAddWeChat: '是', remarks: '' }, | |
| 103 | + { id: '7', eventName: '犀浦20260206', teamName: '赵倩', storeName: '犀浦', expansionTime: '2026-02-06 15:36:18', expansionUserName: '赵倩', customerName: '曾女士', customerPhone: '13699459777', buyNumber: 1, paymentMethod: '微信', isAddWeChat: '否', remarks: '陈女士同事' }, | |
| 104 | + { id: '8', eventName: '犀浦20260206', teamName: '赵倩', storeName: '犀浦', expansionTime: '2026-02-06 15:35:25', expansionUserName: '赵倩', customerName: '陈女士', customerPhone: '15982056520', buyNumber: 1, paymentMethod: '微信', isAddWeChat: '是', remarks: '对面培训机构老师' }, | |
| 105 | + { id: '9', eventName: '犀浦20260206', teamName: '杨钦岚', storeName: '犀浦', expansionTime: '2026-02-06 13:36:43', expansionUserName: '杨钦岚', customerName: '阳女士', customerPhone: '18190344149', buyNumber: 1, paymentMethod: '微信', isAddWeChat: '是', remarks: '' }, | |
| 106 | + { id: '10', eventName: '犀浦20260206', teamName: '宁燕', storeName: '犀浦', expansionTime: '2026-02-06 12:36:29', expansionUserName: '宁燕', customerName: '何女士', customerPhone: '13778587844', buyNumber: 1, paymentMethod: '微信', isAddWeChat: '否', remarks: '和林女士一起到店体验' } | |
| 107 | +] | |
| 108 | + | |
| 109 | +export default { | |
| 110 | + name: 'TuokeListDialog', | |
| 111 | + props: { visible: { type: Boolean, default: false } }, | |
| 112 | + data() { | |
| 113 | + return { | |
| 114 | + showAll: false, | |
| 115 | + loading: false, | |
| 116 | + eventLoading: false, | |
| 117 | + eventList: [], | |
| 118 | + list: [], | |
| 119 | + total: 0, | |
| 120 | + mockData: MOCK_TUOKE, | |
| 121 | + query: { expansionTime: undefined, eventId: undefined, customerName: undefined, customerPhone: undefined, buyNumber: undefined, paymentMethod: undefined, isAddWeChat: undefined, teamName: undefined }, | |
| 122 | + listQuery: { currentPage: 1, pageSize: 10, sort: 'desc', sidx: '' }, | |
| 123 | + payMethodOptions: ['微信', '支付宝', '现金', '银行转账'] | |
| 124 | + } | |
| 125 | + }, | |
| 126 | + computed: { | |
| 127 | + visibleProxy: { | |
| 128 | + get() { return this.visible }, | |
| 129 | + set(v) { this.$emit('update:visible', v) } | |
| 130 | + } | |
| 131 | + }, | |
| 132 | + watch: { | |
| 133 | + visible(v) { if (v) { this.initData(); this.getEventList() } } | |
| 134 | + }, | |
| 135 | + methods: { | |
| 136 | + formatDate(ts) { | |
| 137 | + if (!ts) return '-' | |
| 138 | + if (typeof ts === 'string' && ts.includes('-')) return ts.substring(0, 10) | |
| 139 | + const d = new Date(typeof ts === 'number' ? ts : Number(ts)) | |
| 140 | + if (isNaN(d.getTime())) return '-' | |
| 141 | + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` | |
| 142 | + }, | |
| 143 | + getEventList() { | |
| 144 | + const map = new Map() | |
| 145 | + this.mockData.forEach(r => { if (!map.has(r.eventName)) map.set(r.eventName, { id: r.eventName, eventName: r.eventName }) }) | |
| 146 | + this.eventList = [...map.values()] | |
| 147 | + }, | |
| 148 | + initData() { | |
| 149 | + this.loading = true | |
| 150 | + setTimeout(() => { | |
| 151 | + let filtered = [...this.mockData] | |
| 152 | + if (this.query.eventId) filtered = filtered.filter(r => r.eventName === this.eventList.find(e => e.id === this.query.eventId)?.eventName) | |
| 153 | + if (this.query.customerName) filtered = filtered.filter(r => r.customerName.includes(this.query.customerName)) | |
| 154 | + if (this.query.customerPhone) filtered = filtered.filter(r => r.customerPhone.includes(this.query.customerPhone)) | |
| 155 | + if (this.query.paymentMethod) filtered = filtered.filter(r => r.paymentMethod === this.query.paymentMethod) | |
| 156 | + if (this.query.isAddWeChat) filtered = filtered.filter(r => r.isAddWeChat === this.query.isAddWeChat) | |
| 157 | + if (this.query.teamName) filtered = filtered.filter(r => r.teamName.includes(this.query.teamName)) | |
| 158 | + this.total = filtered.length | |
| 159 | + const start = (this.listQuery.currentPage - 1) * this.listQuery.pageSize | |
| 160 | + this.list = filtered.slice(start, start + this.listQuery.pageSize) | |
| 161 | + this.loading = false | |
| 162 | + }, 300) | |
| 163 | + }, | |
| 164 | + search() { this.listQuery.currentPage = 1; this.initData() }, | |
| 165 | + reset() { | |
| 166 | + for (const k in this.query) this.query[k] = undefined | |
| 167 | + this.listQuery = { currentPage: 1, pageSize: 10, sort: 'desc', sidx: '' } | |
| 168 | + this.initData() | |
| 169 | + } | |
| 170 | + } | |
| 171 | +} | |
| 172 | +</script> | |
| 173 | + | |
| 174 | +<style lang="scss" scoped> | |
| 175 | +::v-deep .tuoke-list-dialog { max-width: 1600px; margin-top: 3vh !important; border-radius: 20px; padding: 0; background: radial-gradient(circle at 0 0, rgba(255,255,255,0.96) 0, rgba(248,250,252,0.98) 40%, rgba(241,245,249,0.98) 100%); box-shadow: 0 24px 48px rgba(15,23,42,0.18), 0 0 0 1px rgba(255,255,255,0.9); backdrop-filter: blur(22px); -webkit-backdrop-filter: blur(22px); } | |
| 176 | +::v-deep .el-dialog__header { display: none; } | |
| 177 | +::v-deep .el-dialog__body { padding: 0; } | |
| 178 | +.dialog-inner { display: flex; flex-direction: column; max-height: 92vh; } | |
| 179 | +.dialog-header { flex-shrink: 0; display: flex; align-items: center; justify-content: space-between; margin: 18px 22px 0; padding: 10px 14px; border-radius: 14px; background: rgba(219,234,254,0.96); } | |
| 180 | +.dialog-title { font-size: 17px; font-weight: 600; color: #0f172a; } | |
| 181 | +.dialog-close { cursor: pointer; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border-radius: 999px; color: #64748b; transition: all 0.15s; &:hover { background: rgba(0,0,0,0.06); color: #0f172a; } } | |
| 182 | +.dialog-search { flex-shrink: 0; padding: 12px 22px 4px; } | |
| 183 | +.dialog-content { flex: 1; min-height: 0; overflow: auto; padding: 0 22px; } | |
| 184 | +.dialog-footer { flex-shrink: 0; display: flex; align-items: center; justify-content: flex-end; padding: 10px 22px 14px; border-top: 1px solid rgba(229,231,235,0.6); } | |
| 185 | +::v-deep .tuoke-list-dialog .el-input__inner { border-radius: 999px; height: 32px; line-height: 32px; border-color: #e5e7eb; background-color: #f9fafb; &:focus { border-color: #2563eb; box-shadow: 0 0 0 1px rgba(37,99,235,0.18); } } | |
| 186 | +::v-deep .tuoke-list-dialog .el-button--primary { border-radius: 999px; background: #2563eb; border-color: #2563eb; box-shadow: 0 4px 10px rgba(37,99,235,0.35); } | |
| 187 | +::v-deep .tuoke-list-dialog .el-button--default { border-radius: 999px; } | |
| 188 | +::v-deep .tuoke-list-dialog .el-form-item { margin-bottom: 8px; } | |
| 189 | +::v-deep .tuoke-list-dialog .el-form-item__label { white-space: nowrap; } | |
| 190 | +::v-deep .tuoke-list-dialog .el-range-editor.el-input__inner { border-radius: 999px; } | |
| 191 | +</style> | ... | ... |
store-pc/src/views/dashboard/index.vue
| ... | ... | @@ -7,14 +7,8 @@ |
| 7 | 7 | <div class="store-subtitle">今日邀约 · 预约 · 开单,一眼总览</div> |
| 8 | 8 | </div> |
| 9 | 9 | <div class="member-search"> |
| 10 | - <el-input | |
| 11 | - v-model="memberKeyword" | |
| 12 | - class="member-search-input" | |
| 13 | - clearable | |
| 14 | - placeholder="快速查询会员:手机号 / 姓名 / 会员卡号" | |
| 15 | - prefix-icon="el-icon-search" | |
| 16 | - @keyup.enter.native="handleMemberSearch" | |
| 17 | - > | |
| 10 | + <el-input v-model="memberKeyword" class="member-search-input" clearable placeholder="快速查询会员:手机号 / 姓名 / 会员卡号" | |
| 11 | + prefix-icon="el-icon-search" @keyup.enter.native="handleMemberSearch"> | |
| 18 | 12 | <el-button slot="append" type="primary" @click="handleMemberSearch">查询</el-button> |
| 19 | 13 | </el-input> |
| 20 | 14 | </div> |
| ... | ... | @@ -49,17 +43,9 @@ |
| 49 | 43 | <!-- 六大业务板块 --> |
| 50 | 44 | <div class="modules-grid"> |
| 51 | 45 | <el-row :gutter="16"> |
| 52 | - <el-col | |
| 53 | - v-for="module in modules" | |
| 54 | - :key="module.key" | |
| 55 | - :span="8" | |
| 56 | - > | |
| 57 | - <el-card | |
| 58 | - shadow="hover" | |
| 59 | - class="module-card" | |
| 60 | - :body-style="{ padding: '18px 18px 16px' }" | |
| 61 | - @click.native="goModule(module)" | |
| 62 | - > | |
| 46 | + <el-col v-for="module in modules" :key="module.key" :span="8"> | |
| 47 | + <el-card shadow="hover" class="module-card" :body-style="{ padding: '18px 18px 16px' }" | |
| 48 | + @click.native="handleModuleSecondary(module)"> | |
| 63 | 49 | <div class="module-header"> |
| 64 | 50 | <div class="module-title-wrap"> |
| 65 | 51 | <div class="module-title">{{ module.title }}</div> |
| ... | ... | @@ -74,13 +60,11 @@ |
| 74 | 60 | <div class="meta-label">{{ module.metricLabel }}</div> |
| 75 | 61 | </div> |
| 76 | 62 | <div class="module-actions"> |
| 77 | - <span | |
| 78 | - class="primary-action" | |
| 79 | - @click.stop="handleModulePrimary(module)" | |
| 80 | - > | |
| 63 | + <span class="primary-action" @click.stop="handleModulePrimary(module)"> | |
| 81 | 64 | {{ module.primaryAction }} |
| 82 | 65 | </span> |
| 83 | - <span class="secondary-text">{{ module.secondaryText }}</span> | |
| 66 | + <span class="secondary-text" @click.stop="handleModuleSecondary(module)">{{ module.secondaryText | |
| 67 | + }}</span> | |
| 84 | 68 | </div> |
| 85 | 69 | </el-card> |
| 86 | 70 | </el-col> |
| ... | ... | @@ -98,11 +82,7 @@ |
| 98 | 82 | <el-tabs v-model="todayTab"> |
| 99 | 83 | <el-tab-pane label="今日邀约" name="invite"> |
| 100 | 84 | <div v-if="todayInvite.length" class="timeline-list"> |
| 101 | - <div | |
| 102 | - v-for="item in todayInvite" | |
| 103 | - :key="item.id" | |
| 104 | - class="timeline-item" | |
| 105 | - > | |
| 85 | + <div v-for="item in todayInvite" :key="item.id" class="timeline-item"> | |
| 106 | 86 | <div class="time">{{ item.time }}</div> |
| 107 | 87 | <div class="content"> |
| 108 | 88 | <div class="line-main"> |
| ... | ... | @@ -113,11 +93,7 @@ |
| 113 | 93 | <span class="project"> |
| 114 | 94 | 电话是否有效:{{ item.dhsfyx }} · {{ item.lxjl }} |
| 115 | 95 | </span> |
| 116 | - <button | |
| 117 | - type="button" | |
| 118 | - class="quick-book-btn" | |
| 119 | - @click="handleQuickBooking(item)" | |
| 120 | - > | |
| 96 | + <button type="button" class="quick-book-btn" @click="handleQuickBooking(item)"> | |
| 121 | 97 | 快速预约 |
| 122 | 98 | </button> |
| 123 | 99 | </div> |
| ... | ... | @@ -139,11 +115,7 @@ |
| 139 | 115 | <span class="project"> |
| 140 | 116 | 预约项目:{{ item.project }} · 预约时间:{{ item.date }} {{ item.timeRange }} |
| 141 | 117 | </span> |
| 142 | - <button | |
| 143 | - type="button" | |
| 144 | - class="quick-order-btn" | |
| 145 | - @click="handleQuickBilling(item)" | |
| 146 | - > | |
| 118 | + <button type="button" class="quick-order-btn" @click="handleQuickBilling(item)"> | |
| 147 | 119 | 快速开单 |
| 148 | 120 | </button> |
| 149 | 121 | </div> |
| ... | ... | @@ -156,20 +128,19 @@ |
| 156 | 128 | </el-card> |
| 157 | 129 | </div> |
| 158 | 130 | </div> |
| 159 | - <member-profile-dialog | |
| 160 | - :visible.sync="memberDialogVisible" | |
| 161 | - :member="activeMember || {}" | |
| 162 | - /> | |
| 163 | - <tuoke-lead-dialog | |
| 164 | - :visible.sync="tuokeDialogVisible" | |
| 165 | - /> | |
| 166 | - <invite-add-dialog | |
| 167 | - :visible.sync="inviteDialogVisible" | |
| 168 | - /> | |
| 169 | - <booking-dialog | |
| 170 | - :visible.sync="bookingDialogVisible" | |
| 171 | - :prefill="bookingPrefill" | |
| 172 | - /> | |
| 131 | + <member-profile-dialog :visible.sync="memberDialogVisible" :member="activeMember || {}" /> | |
| 132 | + <tuoke-lead-dialog :visible.sync="tuokeDialogVisible" /> | |
| 133 | + <invite-add-dialog :visible.sync="inviteDialogVisible" /> | |
| 134 | + <booking-dialog :visible.sync="bookingDialogVisible" :prefill="bookingPrefill" /> | |
| 135 | + <billing-dialog :visible.sync="billingDialogVisible" :prefill="billingPrefill" /> | |
| 136 | + <consume-dialog :visible.sync="consumeDialogVisible" /> | |
| 137 | + <refund-dialog :visible.sync="refundDialogVisible" /> | |
| 138 | + <tuoke-list-dialog :visible.sync="tuokeListVisible" /> | |
| 139 | + <invite-list-dialog :visible.sync="inviteListVisible" /> | |
| 140 | + <booking-calendar-dialog :visible.sync="bookingCalendarVisible" /> | |
| 141 | + <billing-list-dialog :visible.sync="billingListVisible" /> | |
| 142 | + <consume-list-dialog :visible.sync="consumeListVisible" /> | |
| 143 | + <refund-list-dialog :visible.sync="refundListVisible" /> | |
| 173 | 144 | </div> |
| 174 | 145 | </template> |
| 175 | 146 | |
| ... | ... | @@ -178,6 +149,15 @@ import MemberProfileDialog from '@/components/MemberProfileDialog.vue' |
| 178 | 149 | import TuokeLeadDialog from '@/components/TuokeLeadDialog.vue' |
| 179 | 150 | import InviteAddDialog from '@/components/InviteAddDialog.vue' |
| 180 | 151 | import BookingDialog from '@/components/BookingDialog.vue' |
| 152 | +import BillingDialog from '@/components/BillingDialog.vue' | |
| 153 | +import ConsumeDialog from '@/components/ConsumeDialog.vue' | |
| 154 | +import RefundDialog from '@/components/RefundDialog.vue' | |
| 155 | +import TuokeListDialog from '@/components/TuokeListDialog.vue' | |
| 156 | +import InviteListDialog from '@/components/InviteListDialog.vue' | |
| 157 | +import BookingCalendarDialog from '@/components/BookingCalendarDialog.vue' | |
| 158 | +import BillingListDialog from '@/components/BillingListDialog.vue' | |
| 159 | +import ConsumeListDialog from '@/components/ConsumeListDialog.vue' | |
| 160 | +import RefundListDialog from '@/components/RefundListDialog.vue' | |
| 181 | 161 | |
| 182 | 162 | export default { |
| 183 | 163 | name: 'Dashboard', |
| ... | ... | @@ -185,7 +165,16 @@ export default { |
| 185 | 165 | MemberProfileDialog, |
| 186 | 166 | TuokeLeadDialog, |
| 187 | 167 | InviteAddDialog, |
| 188 | - BookingDialog | |
| 168 | + BookingDialog, | |
| 169 | + BillingDialog, | |
| 170 | + ConsumeDialog, | |
| 171 | + RefundDialog, | |
| 172 | + TuokeListDialog, | |
| 173 | + InviteListDialog, | |
| 174 | + BookingCalendarDialog, | |
| 175 | + BillingListDialog, | |
| 176 | + ConsumeListDialog, | |
| 177 | + RefundListDialog | |
| 189 | 178 | }, |
| 190 | 179 | data() { |
| 191 | 180 | return { |
| ... | ... | @@ -225,9 +214,8 @@ export default { |
| 225 | 214 | iconBg: 'linear-gradient(135deg, #6366f1, #a855f7)', |
| 226 | 215 | metricValue: 0, |
| 227 | 216 | metricLabel: '今日新增潜客', |
| 228 | - primaryAction: '录入拓客信息', | |
| 229 | - secondaryText: '查看拓客数据', | |
| 230 | - route: '/tuoke' | |
| 217 | + primaryAction: '新建拓客', | |
| 218 | + secondaryText: '查看拓客数据' | |
| 231 | 219 | }, |
| 232 | 220 | { |
| 233 | 221 | key: 'invite', |
| ... | ... | @@ -237,9 +225,8 @@ export default { |
| 237 | 225 | iconBg: 'linear-gradient(135deg, #0ea5e9, #22c55e)', |
| 238 | 226 | metricValue: 0, |
| 239 | 227 | metricLabel: '待跟进邀约', |
| 240 | - primaryAction: '去邀约', | |
| 241 | - secondaryText: '查看邀约记录', | |
| 242 | - route: '/invite' | |
| 228 | + primaryAction: '新建邀约', | |
| 229 | + secondaryText: '查看邀约记录' | |
| 243 | 230 | }, |
| 244 | 231 | { |
| 245 | 232 | key: 'booking', |
| ... | ... | @@ -250,8 +237,7 @@ export default { |
| 250 | 237 | metricValue: 0, |
| 251 | 238 | metricLabel: '今日预约', |
| 252 | 239 | primaryAction: '新建预约', |
| 253 | - secondaryText: '打开预约日历', | |
| 254 | - route: '/booking' | |
| 240 | + secondaryText: '打开预约日历' | |
| 255 | 241 | }, |
| 256 | 242 | { |
| 257 | 243 | key: 'order', |
| ... | ... | @@ -261,9 +247,8 @@ export default { |
| 261 | 247 | iconBg: 'linear-gradient(135deg, #3b82f6, #2563eb)', |
| 262 | 248 | metricValue: 0, |
| 263 | 249 | metricLabel: '今日开单', |
| 264 | - primaryAction: '快速开单', | |
| 265 | - secondaryText: '查看开单记录', | |
| 266 | - route: '/orders' | |
| 250 | + primaryAction: '新建开单', | |
| 251 | + secondaryText: '查看开单记录' | |
| 267 | 252 | }, |
| 268 | 253 | { |
| 269 | 254 | key: 'consume', |
| ... | ... | @@ -273,9 +258,8 @@ export default { |
| 273 | 258 | iconBg: 'linear-gradient(135deg, #22c55e, #16a34a)', |
| 274 | 259 | metricValue: 0, |
| 275 | 260 | metricLabel: '今日消耗人次', |
| 276 | - primaryAction: '记录消耗', | |
| 277 | - secondaryText: '查看消耗记录', | |
| 278 | - route: '/consume' | |
| 261 | + primaryAction: '新建消耗', | |
| 262 | + secondaryText: '查看消耗记录' | |
| 279 | 263 | }, |
| 280 | 264 | { |
| 281 | 265 | key: 'refund', |
| ... | ... | @@ -285,9 +269,8 @@ export default { |
| 285 | 269 | iconBg: 'linear-gradient(135deg, #f97316, #ef4444)', |
| 286 | 270 | metricValue: 0, |
| 287 | 271 | metricLabel: '今日退卡', |
| 288 | - primaryAction: '发起退卡', | |
| 289 | - secondaryText: '查看售后记录', | |
| 290 | - route: '/refund' | |
| 272 | + primaryAction: '新建退卡', | |
| 273 | + secondaryText: '查看退卡记录' | |
| 291 | 274 | } |
| 292 | 275 | ], |
| 293 | 276 | todayInvite: [ |
| ... | ... | @@ -500,7 +483,17 @@ export default { |
| 500 | 483 | bookingPrefill: { |
| 501 | 484 | name: '', |
| 502 | 485 | phone: '' |
| 503 | - } | |
| 486 | + }, | |
| 487 | + billingDialogVisible: false, | |
| 488 | + billingPrefill: {}, | |
| 489 | + consumeDialogVisible: false, | |
| 490 | + refundDialogVisible: false, | |
| 491 | + tuokeListVisible: false, | |
| 492 | + inviteListVisible: false, | |
| 493 | + bookingCalendarVisible: false, | |
| 494 | + billingListVisible: false, | |
| 495 | + consumeListVisible: false, | |
| 496 | + refundListVisible: false | |
| 504 | 497 | } |
| 505 | 498 | }, |
| 506 | 499 | computed: { |
| ... | ... | @@ -533,82 +526,105 @@ export default { |
| 533 | 526 | this.bookingDialogVisible = true |
| 534 | 527 | return |
| 535 | 528 | } |
| 536 | - this.goModule(module) | |
| 529 | + if (module.key === 'order') { | |
| 530 | + this.billingPrefill = {} | |
| 531 | + this.billingDialogVisible = true | |
| 532 | + return | |
| 533 | + } | |
| 534 | + if (module.key === 'consume') { | |
| 535 | + this.consumeDialogVisible = true | |
| 536 | + return | |
| 537 | + } | |
| 538 | + if (module.key === 'refund') { | |
| 539 | + this.refundDialogVisible = true | |
| 540 | + return | |
| 541 | + } | |
| 537 | 542 | }, |
| 538 | 543 | handleMemberSearch() { |
| 539 | 544 | const keyword = (this.memberKeyword || '').trim() |
| 540 | 545 | if (!keyword) return |
| 541 | 546 | this.activeMember = { |
| 542 | - id: '10001', | |
| 543 | - dah: 'LX202603010001', | |
| 544 | - khmc: '林小纤', | |
| 545 | - sjh: keyword || '13800138000', | |
| 547 | + id: 'GK2020082505128', | |
| 548 | + dah: 'GK2020082505128', | |
| 549 | + khmc: '刘泽蓉', | |
| 550 | + sjh: '15982188353', | |
| 546 | 551 | xb: '女', |
| 547 | - gsmdName: this.$store.state.storeInfo?.storeName || '绿纤门店', | |
| 548 | - khlxName: '散客', | |
| 549 | - zcsj: '2024-08-15 15:32', | |
| 550 | - jdqd: '小程序拓客', | |
| 551 | - tjrName: '老客户-王女士', | |
| 552 | - mainHealthUserName: '—', | |
| 553 | - subHealthUserName: '赵美美', | |
| 552 | + gsmdName: '静居寺', | |
| 553 | + khlxName: '高端客户', | |
| 554 | + zcsj: '2020-08-25', | |
| 555 | + jdqd: '19.9卡', | |
| 556 | + tjrName: '—', | |
| 557 | + mainHealthUserName: '董顺秀', | |
| 558 | + subHealthUserName: '张丽', | |
| 554 | 559 | expandUserName: '—', |
| 555 | - lxdz: '杭州市 · 西湖区 · 文三路', | |
| 556 | - bz: '偏好安静包间,对香味略敏感', | |
| 557 | - totalConsumeAmount: 12000, | |
| 558 | - totalBillingAmount: 15280, | |
| 559 | - remainingRightsAmount: 3280, | |
| 560 | - visitDays: 18, | |
| 561 | - sleepDays: 5, | |
| 562 | - lastVisitTime: '2026-03-02 16:20', | |
| 563 | - lastConsumeTime: '2026-03-02 16:20', | |
| 564 | - consumeLevelName: '金卡会员', | |
| 565 | - yanglsr: '08-18', | |
| 566 | - yinlsr: '七月初五', | |
| 560 | + lxdz: '--', | |
| 561 | + bz: '--', | |
| 562 | + totalConsumeAmount: 290473.63, | |
| 563 | + totalBillingAmount: 1028692.14, | |
| 564 | + remainingRightsAmount: 738218.51, | |
| 565 | + sleepDays: 6, | |
| 566 | + firstVisitTime: '2025-10-08', | |
| 567 | + lastVisitTime: '2026-02-08 14:45', | |
| 568 | + lastConsumeTime: '2026-02-08 14:45', | |
| 569 | + consumeLevel: 5, | |
| 570 | + yanglsr: '1990-06-15', | |
| 571 | + yinlsr: '五月廿三', | |
| 567 | 572 | isBeautyMember: 1, |
| 568 | - isMedicalMember: 0, | |
| 569 | - isTechMember: 0, | |
| 573 | + isMedicalMember: 1, | |
| 574 | + isTechMember: 1, | |
| 570 | 575 | isEducationMember: 0, |
| 571 | 576 | RemainingItems: [ |
| 572 | - { ItemName: '面部深层护理(次卡)', ItemPrice: 380, SourceType: '购买', TotalPurchased: 10, ConsumedCount: 4, RemainingCount: 6 }, | |
| 573 | - { ItemName: '肩颈调理(疗程)', ItemPrice: 268, SourceType: '购买', TotalPurchased: 5, ConsumedCount: 2, RemainingCount: 3 }, | |
| 574 | - { ItemName: '身体舒缓护理', ItemPrice: 498, SourceType: '购买', TotalPurchased: 3, ConsumedCount: 1, RemainingCount: 2 }, | |
| 575 | - { ItemName: '眼周护理套餐', ItemPrice: 198, SourceType: '赠送', TotalPurchased: 4, ConsumedCount: 1, RemainingCount: 3 }, | |
| 576 | - { ItemName: '颈肩放松体验', ItemPrice: 168, SourceType: '活动', TotalPurchased: 2, ConsumedCount: 0, RemainingCount: 2 } | |
| 577 | + { ItemName: '美拉-面部', ItemPrice: 2800, SourceType: '购买', TotalPurchased: 12, ConsumedCount: 8, RefundedCount: 0, DeductCount: 0, RemainingCount: 4 }, | |
| 578 | + { ItemName: '美拉-眼部', ItemPrice: 1500, SourceType: '购买', TotalPurchased: 10, ConsumedCount: 6, RefundedCount: 0, DeductCount: 0, RemainingCount: 4 }, | |
| 579 | + { ItemName: '逆龄胶原-眼部', ItemPrice: 9800, SourceType: '购买', TotalPurchased: 4, ConsumedCount: 1, RefundedCount: 0, DeductCount: 0, RemainingCount: 3 }, | |
| 580 | + { ItemName: '生命之波', ItemPrice: 680, SourceType: '购买', TotalPurchased: 20, ConsumedCount: 15, RefundedCount: 0, DeductCount: 0, RemainingCount: 5 }, | |
| 581 | + { ItemName: 'CELL神经', ItemPrice: 580, SourceType: '购买', TotalPurchased: 15, ConsumedCount: 10, RefundedCount: 0, DeductCount: 0, RemainingCount: 5 }, | |
| 582 | + { ItemName: '精雕', ItemPrice: 3500, SourceType: '购买', TotalPurchased: 6, ConsumedCount: 4, RefundedCount: 0, DeductCount: 0, RemainingCount: 2 }, | |
| 583 | + { ItemName: '微雕-面部', ItemPrice: 12000, SourceType: '购买', TotalPurchased: 3, ConsumedCount: 1, RefundedCount: 0, DeductCount: 0, RemainingCount: 2 } | |
| 584 | + ], | |
| 585 | + inviteRecords: [ | |
| 586 | + { InviteDate: '2026-01-17 14:06', StoreName: '静居寺', InviterName: '张丽', ContactTime: '2026-01-17 14:06', ContactRecord: '媳妇3:30过来', PhoneValid: '是' }, | |
| 587 | + { InviteDate: '2026-01-16 19:13', StoreName: '静居寺', InviterName: '张丽', ContactTime: '2026-01-16 19:13', ContactRecord: '14:00去468做科技部', PhoneValid: '是' }, | |
| 588 | + { InviteDate: '2025-12-21 18:52', StoreName: '静居寺', InviterName: '张丽', ContactTime: '2025-12-21 18:52', ContactRecord: '媳妇过来做', PhoneValid: '是' }, | |
| 589 | + { InviteDate: '2025-12-17 20:46', StoreName: '静居寺', InviterName: '张丽', ContactTime: '2025-12-17 20:46', ContactRecord: '去医院做项目', PhoneValid: '是' } | |
| 590 | + ], | |
| 591 | + bookingRecords: [ | |
| 592 | + { AppointmentDate: '2026-01-17 15:30', StoreName: '静居寺', InviterName: '张丽', HealthCoachName: '张丽', ExperienceItem: '美拉-面部+眼部+颈部', Status: '已确认', NoDealRemark: '' }, | |
| 593 | + { AppointmentDate: '2026-01-16 13:30', StoreName: '静居寺', InviterName: '张丽', HealthCoachName: '张丽', ExperienceItem: '逆龄胶原-眼部+颈部', Status: '已确认', NoDealRemark: '' }, | |
| 594 | + { AppointmentDate: '2025-12-21 14:30', StoreName: '静居寺', InviterName: '张丽', HealthCoachName: '张丽', ExperienceItem: 'CELL+生命之波', Status: '已预约', NoDealRemark: '' }, | |
| 595 | + { AppointmentDate: '2025-12-17 09:00', StoreName: '静居寺', InviterName: '张丽', HealthCoachName: '张丽', ExperienceItem: '微雕-面部+精雕', Status: '已预约', NoDealRemark: '' }, | |
| 596 | + { AppointmentDate: '2025-12-02 13:30', StoreName: '静居寺', InviterName: '张丽', HealthCoachName: '张丽', ExperienceItem: '精雕+太极神灸', Status: '已预约', NoDealRemark: '' } | |
| 577 | 597 | ], |
| 578 | 598 | billingRecords: [ |
| 579 | - { orderNo: 'BD202603020001', billingTime: '2026-03-02 14:30', productName: '面部深层护理(次卡)', amount: 2280, operator: '赵美美' }, | |
| 580 | - { orderNo: 'BD202602150002', billingTime: '2026-02-15 10:20', productName: '肩颈调理(疗程)', amount: 1340, operator: '赵美美' }, | |
| 581 | - { orderNo: 'BD202601300003', billingTime: '2026-01-30 16:05', productName: '身体舒缓护理', amount: 1494, operator: '李丽' }, | |
| 582 | - { orderNo: 'BD202601120004', billingTime: '2026-01-12 11:20', productName: '眼周护理套餐', amount: 792, operator: '李丽' }, | |
| 583 | - { orderNo: 'BD202512250005', billingTime: '2025-12-25 19:30', productName: '颈肩放松体验', amount: 336, operator: '王芳' } | |
| 599 | + { BillingDate: '2026-01-30 16:39', StoreName: '静居寺', CreatorName: '陈怡名', HealthCoachNames: '静居寺T区', TechTeacherNames: '科技一部T区', Items: '美拉-面部、美拉-眼部', Amount: 0, DebtAmount: 0, ActivityName: '' }, | |
| 600 | + { BillingDate: '2026-01-17 10:30', StoreName: '静居寺', CreatorName: '陈怡名', HealthCoachNames: '静居寺T区', TechTeacherNames: '科技一部T区', Items: '美拉-面部、美拉-眼部、美拉-颈部', Amount: 0, DebtAmount: 0, ActivityName: '' }, | |
| 601 | + { BillingDate: '2026-01-16 18:47', StoreName: '静居寺', CreatorName: '樊琳', HealthCoachNames: '张丽、董顺秀', TechTeacherNames: '杨琴、王方贤', Items: '逆龄胶原-眼部、逆龄胶原-颈部', Amount: 33240, DebtAmount: 0, ActivityName: '' }, | |
| 602 | + { BillingDate: '2025-12-17 16:38', StoreName: '静居寺', CreatorName: '樊琳', HealthCoachNames: '张丽、董顺秀', TechTeacherNames: '', Items: '微雕-面部、医美精雕', Amount: 96000, DebtAmount: 0, ActivityName: '' }, | |
| 603 | + { BillingDate: '2025-11-27 20:04', StoreName: '静居寺', CreatorName: '樊琳', HealthCoachNames: '董顺秀', TechTeacherNames: '', Items: '直播-精雕定金、直播-富贵包定金、直播-脂间艺术定金', Amount: 597, DebtAmount: 0, ActivityName: '' } | |
| 584 | 604 | ], |
| 585 | 605 | consumeRecords: [ |
| 586 | - { consumeTime: '2026-03-02 16:20', itemName: '面部深层护理', count: 1, operator: '小李' }, | |
| 587 | - { consumeTime: '2026-02-28 15:00', itemName: '肩颈调理', count: 1, operator: '小李' }, | |
| 588 | - { consumeTime: '2026-02-20 18:30', itemName: '身体舒缓护理', count: 1, operator: '小王' }, | |
| 589 | - { consumeTime: '2026-02-10 13:40', itemName: '眼周护理套餐', count: 1, operator: '小王' }, | |
| 590 | - { consumeTime: '2026-01-30 10:15', itemName: '颈肩放松体验', count: 1, operator: '小李' } | |
| 606 | + { ConsumeDate: '2026-02-08 22:45', StoreName: '绿纤静居寺店', OperatorName: '董顺秀', HealthCoachNames: '董顺秀', TechTeacherNames: '', Items: '生命之波、CELL神经', Amount: 1036.90, LaborCost: 38 }, | |
| 607 | + { ConsumeDate: '2026-02-05 19:32', StoreName: '绿纤静居寺店', OperatorName: '陈怡名', HealthCoachNames: '静居寺T区', TechTeacherNames: '科技一部T区', Items: '冻龄宝宝、BIO、宝马仪器、水氧-面部、水氧-眼部、水氧-颈部、维密包', Amount: 50143.96, LaborCost: 1155 }, | |
| 608 | + { ConsumeDate: '2026-01-30 16:37', StoreName: '绿纤静居寺店', OperatorName: '陈怡名', HealthCoachNames: '静居寺T区', TechTeacherNames: '', Items: '日式温背、RETVS、护理(盛世)、精雕、太极神灸、胸腺保养、水氧-面部、砭石床', Amount: 25330.57, LaborCost: 684 }, | |
| 609 | + { ConsumeDate: '2026-01-27 21:50', StoreName: '绿纤静居寺店', OperatorName: '董顺秀', HealthCoachNames: '董顺秀', TechTeacherNames: '', Items: '生命之波、鼎轩-童颜女神', Amount: 4905.73, LaborCost: 40 }, | |
| 610 | + { ConsumeDate: '2026-01-27 20:18', StoreName: '绿纤静居寺店', OperatorName: '张丽', HealthCoachNames: '张丽', TechTeacherNames: '', Items: 'CELL、鼎轩-青春美肤(9D)', Amount: 1182, LaborCost: 74 } | |
| 591 | 611 | ], |
| 592 | - inviteRecords: [ | |
| 593 | - { inviteTime: '2026-03-01 09:00', inviteContent: '周末护理体验邀约', inviter: '赵美美', status: '已到店' }, | |
| 594 | - { inviteTime: '2026-02-25 14:00', inviteContent: '春季护肤专场', inviter: '赵美美', status: '已到店' }, | |
| 595 | - { inviteTime: '2026-02-18 10:30', inviteContent: '肩颈调理舒缓活动', inviter: '李丽', status: '待跟进' }, | |
| 596 | - { inviteTime: '2026-02-05 16:20', inviteContent: '身体舒缓护理体验', inviter: '李丽', status: '未接通' }, | |
| 597 | - { inviteTime: '2026-01-28 11:10', inviteContent: '生日关怀邀约', inviter: '王芳', status: '已到店' } | |
| 612 | + serviceLogRecords: [ | |
| 613 | + { CreateTime: '2026-01-17 22:50', CreatorName: '张丽', Remark: '罗米伽 刘姐媳妇 有抗衰和医美需求 但是目前对我们不信任 需要多相处 今天店长给她送了暖心客情 送了2盒医院面膜 修复她的皮肤 还是很开心', KjbRemark: '' }, | |
| 614 | + { CreateTime: '2026-01-16 22:17', CreatorName: '张丽', Remark: '陪刘姐去468做科技部 今天消耗了美拉+淋巴+眼部框架 对效果认可 很认可王专家 成交33240 送她一次大手臂的逆龄胶原', KjbRemark: '' }, | |
| 615 | + { CreateTime: '2025-12-21 21:23', CreatorName: '张丽', Remark: '刘泽蓉媳妇 今天约到468做项目 引导了cell效果 让她坚持做 对宫寒有改善 对医美有需求 想做鼻子和下巴', KjbRemark: '' } | |
| 598 | 616 | ], |
| 599 | - bookingRecords: [ | |
| 600 | - { bookingTime: '2026-03-05 14:00', projectName: '面部深层护理', staffName: '小李', status: '待服务' }, | |
| 601 | - { bookingTime: '2026-03-02 16:00', projectName: '面部深层护理', staffName: '小李', status: '已完成' }, | |
| 602 | - { bookingTime: '2026-02-26 10:30', projectName: '肩颈调理', staffName: '小王', status: '已取消' }, | |
| 603 | - { bookingTime: '2026-02-15 19:00', projectName: '身体舒缓护理', staffName: '小王', status: '已完成' }, | |
| 604 | - { bookingTime: '2026-01-31 11:20', projectName: '眼周护理套餐', staffName: '小李', status: '待确认' } | |
| 617 | + oldLogRecords: [ | |
| 618 | + { CreateTime: '2025-12-25', OrderNo: 'SY202507190016', MemberName: '刘泽蓉', Remarks: '和张顾问一起给刘姐渲染考证 成交66600 后续又成交臻咪88000' }, | |
| 619 | + { CreateTime: '2025-12-25', OrderNo: 'SY202507190017', MemberName: '刘泽蓉', Remarks: '给她体验做生命源波 了解到想做胸部 体检正常 成交88000' }, | |
| 620 | + { CreateTime: '2025-12-25', OrderNo: 'SY202410190018', MemberName: '刘泽蓉', Remarks: '和张顾问一起做好服务' } | |
| 605 | 621 | ], |
| 606 | 622 | refundRecords: [ |
| 607 | - { refundTime: '2026-02-20 11:30', orderNo: 'TK202602200001', projectName: '肩颈调理(疗程)', amount: 536, operator: '赵美美' }, | |
| 608 | - { refundTime: '2026-02-10 15:10', orderNo: 'TK202602100002', projectName: '面部深层护理(次卡)', amount: 760, operator: '赵美美' }, | |
| 609 | - { refundTime: '2026-01-28 17:40', orderNo: 'TK202601280003', projectName: '身体舒缓护理', amount: 498, operator: '李丽' }, | |
| 610 | - { refundTime: '2026-01-15 13:05', orderNo: 'TK202601150004', projectName: '眼周护理套餐', amount: 198, operator: '李丽' }, | |
| 611 | - { refundTime: '2025-12-30 16:25', orderNo: 'TK202512300005', projectName: '颈肩放松体验', amount: 168, operator: '王芳' } | |
| 623 | + { RefundDate: '2026-02-11 15:48', StoreName: '绿纤西站店', RefundAmount: 133.34, ActualRefundAmount: 133, RefundReason: '项目不适合' }, | |
| 624 | + { RefundDate: '2026-02-11 15:34', StoreName: '绿纤南湖店', RefundAmount: 1199, ActualRefundAmount: 1199, RefundReason: '搬家' }, | |
| 625 | + { RefundDate: '2026-02-11 13:48', StoreName: '绿纤保利店', RefundAmount: 3054.59, ActualRefundAmount: 868, RefundReason: '' }, | |
| 626 | + { RefundDate: '2026-02-08 15:36', StoreName: '绿纤双流店', RefundAmount: 1000, ActualRefundAmount: 1000, RefundReason: '' }, | |
| 627 | + { RefundDate: '2026-02-06 18:12', StoreName: '绿纤融创店', RefundAmount: 10000, ActualRefundAmount: 10000, RefundReason: '' } | |
| 612 | 628 | ] |
| 613 | 629 | } |
| 614 | 630 | this.memberDialogVisible = true |
| ... | ... | @@ -621,12 +637,28 @@ export default { |
| 621 | 637 | this.bookingDialogVisible = true |
| 622 | 638 | }, |
| 623 | 639 | handleQuickBilling(item) { |
| 624 | - this.$router.push('/orders') | |
| 640 | + this.billingPrefill = { | |
| 641 | + name: item.name, | |
| 642 | + memberId: '', | |
| 643 | + fromBooking: true, | |
| 644 | + bookingProject: item.project, | |
| 645 | + bookingDate: item.date, | |
| 646 | + bookingTimeRange: item.timeRange, | |
| 647 | + bookingStaff: item.staffName | |
| 648 | + } | |
| 649 | + this.billingDialogVisible = true | |
| 625 | 650 | }, |
| 626 | - goModule(module) { | |
| 627 | - if (module.route) { | |
| 628 | - this.$router.push(module.route) | |
| 651 | + handleModuleSecondary(module) { | |
| 652 | + const map = { | |
| 653 | + tuoke: 'tuokeListVisible', | |
| 654 | + invite: 'inviteListVisible', | |
| 655 | + booking: 'bookingCalendarVisible', | |
| 656 | + order: 'billingListVisible', | |
| 657 | + consume: 'consumeListVisible', | |
| 658 | + refund: 'refundListVisible' | |
| 629 | 659 | } |
| 660 | + const key = map[module.key] | |
| 661 | + if (key) this[key] = true | |
| 630 | 662 | } |
| 631 | 663 | } |
| 632 | 664 | } |
| ... | ... | @@ -655,6 +687,7 @@ export default { |
| 655 | 687 | color: #111827; |
| 656 | 688 | letter-spacing: 0.5px; |
| 657 | 689 | } |
| 690 | + | |
| 658 | 691 | .store-subtitle { |
| 659 | 692 | margin-top: 4px; |
| 660 | 693 | font-size: 13px; |
| ... | ... | @@ -670,6 +703,7 @@ export default { |
| 670 | 703 | padding: 0 20px; |
| 671 | 704 | border-radius: 0 999px 999px 0; |
| 672 | 705 | } |
| 706 | + | |
| 673 | 707 | ::v-deep .el-input__inner { |
| 674 | 708 | border-radius: 999px 0 0 999px; |
| 675 | 709 | padding-left: 40px; |
| ... | ... | @@ -745,10 +779,12 @@ export default { |
| 745 | 779 | align-items: center; |
| 746 | 780 | justify-content: space-between; |
| 747 | 781 | margin-bottom: 4px; |
| 782 | + | |
| 748 | 783 | .label { |
| 749 | 784 | font-size: 13px; |
| 750 | 785 | color: #6b7280; |
| 751 | 786 | } |
| 787 | + | |
| 752 | 788 | .tag { |
| 753 | 789 | font-size: 11px; |
| 754 | 790 | padding: 2px 8px; |
| ... | ... | @@ -796,6 +832,7 @@ export default { |
| 796 | 832 | font-weight: 600; |
| 797 | 833 | color: #111827; |
| 798 | 834 | } |
| 835 | + | |
| 799 | 836 | .module-subtitle { |
| 800 | 837 | margin-top: 2px; |
| 801 | 838 | font-size: 12px; |
| ... | ... | @@ -811,6 +848,7 @@ export default { |
| 811 | 848 | align-items: center; |
| 812 | 849 | justify-content: center; |
| 813 | 850 | color: #fff; |
| 851 | + | |
| 814 | 852 | i { |
| 815 | 853 | font-size: 20px; |
| 816 | 854 | } |
| ... | ... | @@ -818,11 +856,13 @@ export default { |
| 818 | 856 | |
| 819 | 857 | .module-meta { |
| 820 | 858 | margin-top: 8px; |
| 859 | + | |
| 821 | 860 | .meta-value { |
| 822 | 861 | font-size: 22px; |
| 823 | 862 | font-weight: 600; |
| 824 | 863 | color: #111827; |
| 825 | 864 | } |
| 865 | + | |
| 826 | 866 | .meta-label { |
| 827 | 867 | margin-top: 2px; |
| 828 | 868 | font-size: 12px; |
| ... | ... | @@ -835,6 +875,7 @@ export default { |
| 835 | 875 | display: flex; |
| 836 | 876 | align-items: center; |
| 837 | 877 | justify-content: space-between; |
| 878 | + | |
| 838 | 879 | .primary-action { |
| 839 | 880 | display: inline-flex; |
| 840 | 881 | align-items: center; |
| ... | ... | @@ -849,9 +890,16 @@ export default { |
| 849 | 890 | font-weight: 500; |
| 850 | 891 | box-shadow: 0 4px 10px rgba(37, 99, 235, 0.35); |
| 851 | 892 | } |
| 893 | + | |
| 852 | 894 | .secondary-text { |
| 853 | 895 | font-size: 12px; |
| 854 | 896 | color: #9ca3af; |
| 897 | + cursor: pointer; | |
| 898 | + transition: color 0.15s; | |
| 899 | + | |
| 900 | + &:hover { | |
| 901 | + color: #2563eb; | |
| 902 | + } | |
| 855 | 903 | } |
| 856 | 904 | } |
| 857 | 905 | |
| ... | ... | @@ -881,8 +929,7 @@ export default { |
| 881 | 929 | color: #2563eb; |
| 882 | 930 | } |
| 883 | 931 | |
| 884 | -.timeline-list { | |
| 885 | -} | |
| 932 | +.timeline-list {} | |
| 886 | 933 | |
| 887 | 934 | .timeline-item { |
| 888 | 935 | display: grid; |
| ... | ... | @@ -892,7 +939,7 @@ export default { |
| 892 | 939 | font-size: 12px; |
| 893 | 940 | } |
| 894 | 941 | |
| 895 | -.timeline-item + .timeline-item { | |
| 942 | +.timeline-item+.timeline-item { | |
| 896 | 943 | border-top: 1px dashed #e5e7eb; |
| 897 | 944 | } |
| 898 | 945 | |
| ... | ... | @@ -906,20 +953,24 @@ export default { |
| 906 | 953 | display: flex; |
| 907 | 954 | align-items: center; |
| 908 | 955 | gap: 6px; |
| 956 | + | |
| 909 | 957 | .name { |
| 910 | 958 | color: #111827; |
| 911 | 959 | font-weight: 500; |
| 912 | 960 | } |
| 961 | + | |
| 913 | 962 | .mobile { |
| 914 | 963 | color: #6b7280; |
| 915 | 964 | } |
| 916 | 965 | } |
| 966 | + | |
| 917 | 967 | .line-sub { |
| 918 | 968 | margin-top: 2px; |
| 919 | 969 | display: flex; |
| 920 | 970 | align-items: center; |
| 921 | 971 | justify-content: space-between; |
| 922 | 972 | gap: 8px; |
| 973 | + | |
| 923 | 974 | .project { |
| 924 | 975 | color: #6b7280; |
| 925 | 976 | flex: 1; |
| ... | ... | @@ -1001,10 +1052,12 @@ export default { |
| 1001 | 1052 | .dashboard-page { |
| 1002 | 1053 | padding: 16px; |
| 1003 | 1054 | } |
| 1055 | + | |
| 1004 | 1056 | .top-bar { |
| 1005 | 1057 | grid-template-columns: minmax(0, 1fr); |
| 1006 | 1058 | row-gap: 12px; |
| 1007 | 1059 | } |
| 1060 | + | |
| 1008 | 1061 | .layout-grid { |
| 1009 | 1062 | grid-template-columns: minmax(0, 1fr); |
| 1010 | 1063 | } | ... | ... |