Commit e2c481d6b6ed3dee632a8ed4ba55cefba3b21e7e

Authored by 李宇
2 parents 3258d9a8 ed6ada0d

Merge branch 'master' of http://39.98.150.180/antissoft/lvqianmeiye_ERP

.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 &#39;@/components/MemberProfileDialog.vue&#39;
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 }
... ...