Commit 5b6e99a524d1c9a0cf9081efccd76d79569997b3

Authored by 李宇
1 parent 0c63f3b7

```

feat(attendance): 新增考勤数据批量删除和导入功能

新增了考勤数据的批量删除和 Excel 导入功能,包括对应的弹窗组件 BatchDeleteDialog 和 ImportDialog。
同时在门店管理模块中增加了门店类别、门店类型、新店阶段等字段及相应接口支持,并完善了相关表单与列表展示逻辑。
此外,在项目资料模块中添加了“是否有效”字段用于标识数据有效性状态。
```
antis-ncc-admin/src/views/attendance/BatchDeleteDialog.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + title="批量删除考勤数据"
  4 + :visible.sync="visible"
  5 + width="400px"
  6 + :close-on-click-modal="false"
  7 + @close="close"
  8 + >
  9 + <div>
  10 + <el-form :model="form" label-width="80px" label-position="right" :rules="rules" ref="form">
  11 + <el-form-item label="年份" prop="year">
  12 + <el-select v-model="form.year" placeholder="请选择年份" style="width: 100%">
  13 + <el-option v-for="year in yearOptions" :key="year" :label="year + '年'" :value="year" />
  14 + </el-select>
  15 + </el-form-item>
  16 + <el-form-item label="月份" prop="month">
  17 + <el-select v-model="form.month" placeholder="请选择月份" style="width: 100%">
  18 + <el-option v-for="month in monthOptions" :key="month" :label="month + '月'" :value="month" />
  19 + </el-select>
  20 + </el-form-item>
  21 + </el-form>
  22 +
  23 + <el-alert
  24 + title="警告"
  25 + type="warning"
  26 + description="此操作将删除指定年月下的所有考勤数据,删除后无法恢复,请谨慎操作!"
  27 + show-icon
  28 + :closable="false"
  29 + style="margin-top: 20px;"
  30 + />
  31 + </div>
  32 +
  33 + <div slot="footer" class="dialog-footer">
  34 + <el-button @click="close">取消</el-button>
  35 + <el-button type="danger" @click="confirmDelete" :loading="deleting">
  36 + 确定删除
  37 + </el-button>
  38 + </div>
  39 + </el-dialog>
  40 +</template>
  41 +
  42 +<script>
  43 +import request from '@/utils/request'
  44 +
  45 +export default {
  46 + name: 'BatchDeleteDialog',
  47 + data() {
  48 + return {
  49 + visible: false,
  50 + deleting: false,
  51 + form: {
  52 + year: null,
  53 + month: null
  54 + },
  55 + rules: {
  56 + year: [
  57 + { required: true, message: '请选择年份', trigger: 'change' }
  58 + ],
  59 + month: [
  60 + { required: true, message: '请选择月份', trigger: 'change' }
  61 + ]
  62 + },
  63 + yearOptions: [],
  64 + monthOptions: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
  65 + }
  66 + },
  67 + methods: {
  68 + init() {
  69 + this.visible = true
  70 + this.initYearOptions()
  71 + this.resetForm()
  72 + },
  73 + initYearOptions() {
  74 + const currentYear = new Date().getFullYear()
  75 + this.yearOptions = []
  76 + for (let i = currentYear - 5; i <= currentYear + 1; i++) {
  77 + this.yearOptions.push(i)
  78 + }
  79 + },
  80 + resetForm() {
  81 + this.form = {
  82 + year: null,
  83 + month: null
  84 + }
  85 + this.deleting = false
  86 + this.$nextTick(() => {
  87 + if (this.$refs.form) {
  88 + this.$refs.form.clearValidate()
  89 + }
  90 + })
  91 + },
  92 + confirmDelete() {
  93 + this.$refs.form.validate((valid) => {
  94 + if (valid) {
  95 + this.$confirm(
  96 + `确定要删除 ${this.form.year}年${this.form.month}月 的所有考勤数据吗?此操作不可恢复!`,
  97 + '确认删除',
  98 + {
  99 + confirmButtonText: '确定删除',
  100 + cancelButtonText: '取消',
  101 + type: 'warning'
  102 + }
  103 + ).then(() => {
  104 + this.performDelete()
  105 + }).catch(() => {
  106 + // 用户取消删除
  107 + })
  108 + }
  109 + })
  110 + },
  111 + performDelete() {
  112 + this.deleting = true
  113 +
  114 + request({
  115 + url: `/api/Extend/lqattendancesummary/DeleteByMonth/${this.form.year}/${this.form.month}`,
  116 + method: 'DELETE'
  117 + }).then(res => {
  118 + this.deleting = false
  119 + if (res.code === 200) {
  120 + this.$message.success(res.msg || '删除成功')
  121 + this.close()
  122 + this.$emit('refresh', true)
  123 + } else {
  124 + this.$message.error(res.msg || '删除失败')
  125 + }
  126 + }).catch(err => {
  127 + this.deleting = false
  128 + this.$message.error('删除失败,请重试')
  129 + console.error('删除失败:', err)
  130 + })
  131 + },
  132 + close() {
  133 + this.visible = false
  134 + this.resetForm()
  135 + }
  136 + }
  137 +}
  138 +</script>
  139 +
  140 +<style scoped>
  141 +.dialog-footer {
  142 + text-align: right;
  143 +}
  144 +</style>
... ...
antis-ncc-admin/src/views/attendance/ImportDialog.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + title="导入考勤数据"
  4 + :visible.sync="visible"
  5 + width="500px"
  6 + :close-on-click-modal="false"
  7 + @close="close"
  8 + >
  9 + <div>
  10 + <el-form label-width="100px" label-position="right">
  11 + <el-form-item label="选择文件">
  12 + <el-upload
  13 + ref="upload"
  14 + :action="uploadUrl"
  15 + :headers="uploadHeaders"
  16 + :data="uploadData"
  17 + :file-list="fileList"
  18 + :before-upload="beforeUpload"
  19 + :on-success="onUploadSuccess"
  20 + :on-error="onUploadError"
  21 + :on-change="handleFileChange"
  22 + :auto-upload="false"
  23 + :limit="1"
  24 + :on-exceed="handleExceed"
  25 + accept=".xlsx,.xls"
  26 + drag
  27 + >
  28 + <i class="el-icon-upload"></i>
  29 + <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
  30 + <div class="el-upload__tip" slot="tip">只能上传一个xlsx/xls文件,且不超过10MB</div>
  31 + </el-upload>
  32 + </el-form-item>
  33 + </el-form>
  34 +
  35 + <div v-if="uploadResult" class="upload-result">
  36 + <el-alert
  37 + :title="uploadResult.title"
  38 + :type="uploadResult.type"
  39 + :description="uploadResult.description"
  40 + show-icon
  41 + :closable="false"
  42 + />
  43 + </div>
  44 + </div>
  45 +
  46 + <div slot="footer" class="dialog-footer">
  47 + <el-button @click="close">取消</el-button>
  48 + <el-button type="primary" @click="submitUpload" :loading="uploading" >
  49 + 确定导入
  50 + </el-button>
  51 + </div>
  52 + </el-dialog>
  53 +</template>
  54 +
  55 +<script>
  56 +import request from '@/utils/request'
  57 +
  58 +export default {
  59 + name: 'ImportDialog',
  60 + data() {
  61 + return {
  62 + visible: false,
  63 + uploading: false,
  64 + fileList: [],
  65 + uploadUrl: '',
  66 + uploadHeaders: {},
  67 + uploadData: {},
  68 + uploadResult: null
  69 + }
  70 + },
  71 + methods: {
  72 + init() {
  73 + this.visible = true
  74 + this.resetForm()
  75 + },
  76 + resetForm() {
  77 + this.fileList = []
  78 + this.uploadResult = null
  79 + this.uploading = false
  80 + },
  81 + beforeUpload(file) {
  82 + const isExcel = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
  83 + file.type === 'application/vnd.ms-excel'
  84 + const isLt10M = file.size / 1024 / 1024 < 10
  85 +
  86 + if (!isExcel) {
  87 + this.$message.error('只能上传Excel文件!')
  88 + return false
  89 + }
  90 + if (!isLt10M) {
  91 + this.$message.error('上传文件大小不能超过 10MB!')
  92 + return false
  93 + }
  94 + return true
  95 + },
  96 + submitUpload() {
  97 + console.log('fileList:', this.fileList)
  98 + console.log('fileList.length:', this.fileList.length)
  99 +
  100 + if (!this.fileList.length || !this.fileList[0]) {
  101 + this.$message.warning('请选择要上传的文件')
  102 + return
  103 + }
  104 +
  105 + const file = this.fileList[0]
  106 + console.log('selected file:', file)
  107 +
  108 + if (!file.raw) {
  109 + this.$message.warning('文件信息异常,请重新选择文件')
  110 + return
  111 + }
  112 +
  113 + this.uploading = true
  114 + this.uploadResult = null
  115 +
  116 + const formData = new FormData()
  117 + formData.append('file', file.raw)
  118 +
  119 + request({
  120 + url: '/api/Extend/lqattendancesummary/ImportAttendanceDataFromExcel',
  121 + method: 'POST',
  122 + data: formData,
  123 + headers: {
  124 + 'Content-Type': 'multipart/form-data'
  125 + }
  126 + }).then(res => {
  127 + this.uploading = false
  128 + if (res.code === 200) {
  129 + this.uploadResult = {
  130 + type: 'success',
  131 + title: '导入成功',
  132 + description: res.msg || '数据导入完成'
  133 + }
  134 + this.$message.success('导入成功')
  135 + setTimeout(() => {
  136 + this.close()
  137 + this.$emit('refresh', true)
  138 + }, 2000)
  139 + } else {
  140 + this.uploadResult = {
  141 + type: 'error',
  142 + title: '导入失败',
  143 + description: res.msg || '数据导入失败,请检查文件格式'
  144 + }
  145 + }
  146 + }).catch(err => {
  147 + this.uploading = false
  148 + this.uploadResult = {
  149 + type: 'error',
  150 + title: '导入失败',
  151 + description: err.message || '网络错误,请重试'
  152 + }
  153 + this.$message.error('导入失败')
  154 + })
  155 + },
  156 + onUploadSuccess(response, file, fileList) {
  157 + // 这里不需要处理,因为我们使用手动上传
  158 + },
  159 + onUploadError(err, file, fileList) {
  160 + this.uploading = false
  161 + this.uploadResult = {
  162 + type: 'error',
  163 + title: '上传失败',
  164 + description: '文件上传失败,请重试'
  165 + }
  166 + },
  167 + handleExceed(files, fileList) {
  168 + this.$message.warning('只能上传一个文件,请先删除已选择的文件')
  169 + },
  170 + handleFileChange(file, fileList) {
  171 + console.log('文件变化:', file, fileList)
  172 + this.fileList = fileList
  173 + },
  174 + close() {
  175 + this.visible = false
  176 + this.resetForm()
  177 + }
  178 + }
  179 +}
  180 +</script>
  181 +
  182 +<style scoped>
  183 +.upload-result {
  184 + margin-top: 20px;
  185 +}
  186 +
  187 +.el-upload-dragger {
  188 + width: 100%;
  189 +}
  190 +</style>
... ...
antis-ncc-admin/src/views/attendance/index.vue 0 → 100644
  1 +<template>
  2 + <div class="NCC-common-layout">
  3 + <div class="NCC-common-layout-center">
  4 + <el-row class="NCC-common-search-box" :gutter="16">
  5 + <el-form @submit.native.prevent>
  6 + <el-col :span="6">
  7 + <el-form-item label="员工姓名">
  8 + <el-input v-model="query.userName" placeholder="员工姓名" clearable />
  9 + </el-form-item>
  10 + </el-col>
  11 + <el-col :span="6">
  12 + <el-form-item label="年份">
  13 + <el-select v-model="query.year" placeholder="年份" clearable>
  14 + <el-option v-for="year in yearOptions" :key="year" :label="year + '年'" :value="year" />
  15 + </el-select>
  16 + </el-form-item>
  17 + </el-col>
  18 + <el-col :span="6">
  19 + <el-form-item label="月份">
  20 + <el-select v-model="query.month" placeholder="月份" clearable>
  21 + <el-option v-for="month in monthOptions" :key="month" :label="month + '月'" :value="month" />
  22 + </el-select>
  23 + </el-form-item>
  24 + </el-col>
  25 + <el-col :span="6">
  26 + <el-form-item label="员工状态">
  27 + <el-select v-model="query.employeeStatus" placeholder="员工状态" clearable>
  28 + <el-option label="在职" :value="1" />
  29 + <el-option label="离职" :value="0" />
  30 + </el-select>
  31 + </el-form-item>
  32 + </el-col>
  33 + <el-col :span="6">
  34 + <el-form-item>
  35 + <el-button type="primary" icon="el-icon-search" @click="search()">查询</el-button>
  36 + <el-button icon="el-icon-refresh-right" @click="reset()">重置</el-button>
  37 + </el-form-item>
  38 + </el-col>
  39 + </el-form>
  40 + </el-row>
  41 + <div class="NCC-common-layout-main NCC-flex-main">
  42 + <div class="NCC-common-head">
  43 + <div>
  44 + <el-button type="primary" icon="el-icon-upload2" @click="handleImport()">导入</el-button>
  45 + <el-button type="text" icon="el-icon-download" @click="downloadTemplate()">导入模板</el-button>
  46 + <el-button type="text" icon="el-icon-delete" @click="handleBatchDelete()">批量删除</el-button>
  47 + </div>
  48 + <div class="NCC-common-head-right">
  49 + <el-tooltip effect="dark" content="刷新" placement="top">
  50 + <el-link icon="icon-ym icon-ym-Refresh NCC-common-head-icon" :underline="false" @click="reset()" />
  51 + </el-tooltip>
  52 + <screenfull isContainer />
  53 + </div>
  54 + </div>
  55 + <NCC-table v-loading="listLoading" :data="list" has-c @selection-change="handleSelectionChange">
  56 + <el-table-column prop="userName" label="员工姓名" align="left" />
  57 + <el-table-column prop="year" label="年份" align="left" />
  58 + <el-table-column prop="month" label="月份" align="left" />
  59 + <el-table-column label="员工状态" prop="employeeStatus" align="left">
  60 + <template slot-scope="scope">
  61 + <el-tag :type="scope.row.employeeStatus === 1 ? 'success' : 'danger'" size="small">
  62 + {{ scope.row.employeeStatus === 1 ? '在职' : '离职' }}
  63 + </el-tag>
  64 + </template>
  65 + </el-table-column>
  66 + <el-table-column prop="workDays" label="工作天数" align="left" />
  67 + <el-table-column prop="leaveDays" label="请假天数" align="left" />
  68 + <el-table-column prop="restDays" label="休息天数" align="left" />
  69 + <el-table-column prop="remark" label="备注" align="left" />
  70 + <el-table-column label="创建时间" prop="createTime" align="left" width="150">
  71 + <template slot-scope="scope">
  72 + {{ formatTime(scope.row.createTime) }}
  73 + </template>
  74 + </el-table-column>
  75 + <el-table-column label="更新时间" prop="updateTime" align="left" width="150">
  76 + <template slot-scope="scope">
  77 + {{ formatTime(scope.row.updateTime) }}
  78 + </template>
  79 + </el-table-column>
  80 + </NCC-table>
  81 + <pagination :total="total" :page.sync="listQuery.currentPage" :limit.sync="listQuery.pageSize" @pagination="initData" />
  82 + </div>
  83 + </div>
  84 +
  85 + <!-- 导入弹窗 -->
  86 + <ImportDialog v-if="importVisible" ref="ImportDialog" @refresh="refresh" />
  87 +
  88 + <!-- 批量删除弹窗 -->
  89 + <BatchDeleteDialog v-if="batchDeleteVisible" ref="BatchDeleteDialog" @refresh="refresh" />
  90 + </div>
  91 +</template>
  92 +
  93 +<script>
  94 + import request from '@/utils/request'
  95 + import ImportDialog from './ImportDialog'
  96 + import BatchDeleteDialog from './BatchDeleteDialog'
  97 +
  98 + export default {
  99 + components: { ImportDialog, BatchDeleteDialog },
  100 + data() {
  101 + return {
  102 + query: {
  103 + userName: undefined,
  104 + year: undefined,
  105 + month: undefined,
  106 + employeeStatus: undefined,
  107 + },
  108 + list: [],
  109 + listLoading: true,
  110 + multipleSelection: [],
  111 + total: 0,
  112 + listQuery: {
  113 + currentPage: 1,
  114 + pageSize: 20,
  115 + sort: "desc",
  116 + sidx: "",
  117 + },
  118 + importVisible: false,
  119 + batchDeleteVisible: false,
  120 + yearOptions: [],
  121 + monthOptions: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
  122 + }
  123 + },
  124 + computed: {},
  125 + created() {
  126 + this.initYearOptions()
  127 + this.initData()
  128 + },
  129 + methods: {
  130 + initYearOptions() {
  131 + const currentYear = new Date().getFullYear()
  132 + for (let i = currentYear - 5; i <= currentYear + 1; i++) {
  133 + this.yearOptions.push(i)
  134 + }
  135 + },
  136 + initData() {
  137 + this.listLoading = true;
  138 + let _query = {
  139 + ...this.listQuery,
  140 + ...this.query
  141 + };
  142 + let query = {}
  143 + for (let key in _query) {
  144 + if (Array.isArray(_query[key])) {
  145 + query[key] = _query[key].join()
  146 + } else {
  147 + query[key] = _query[key]
  148 + }
  149 + }
  150 + request({
  151 + url: `/api/Extend/lqattendancesummary`,
  152 + method: 'GET',
  153 + data: query
  154 + }).then(res => {
  155 + this.list = res.data.list
  156 + this.total = res.data.pagination.total
  157 + this.listLoading = false
  158 + })
  159 + },
  160 + handleDel(id) {
  161 + this.$confirm('此操作将永久删除该数据, 是否继续?', '提示', {
  162 + type: 'warning'
  163 + }).then(() => {
  164 + request({
  165 + url: `/api/Extend/lqattendancesummary/${id}`,
  166 + method: 'DELETE'
  167 + }).then(res => {
  168 + this.$message({
  169 + type: 'success',
  170 + message: res.msg,
  171 + onClose: () => {
  172 + this.initData()
  173 + }
  174 + });
  175 + })
  176 + }).catch(() => {
  177 + });
  178 + },
  179 + handleSelectionChange(val) {
  180 + const res = val.map(item => item.id)
  181 + this.multipleSelection = res
  182 + },
  183 + handleImport() {
  184 + this.importVisible = true
  185 + this.$nextTick(() => {
  186 + this.$refs.ImportDialog.init()
  187 + })
  188 + },
  189 + handleBatchDelete() {
  190 + this.batchDeleteVisible = true
  191 + this.$nextTick(() => {
  192 + this.$refs.BatchDeleteDialog.init()
  193 + })
  194 + },
  195 + downloadTemplate() {
  196 + window.open('https://erp.lvqianmeiye.com/FileTemplate/考勤统计导入模板.xlsx', '_blank')
  197 + },
  198 + formatTime(timestamp) {
  199 + if (!timestamp) return '无'
  200 + const date = new Date(timestamp)
  201 + return date.toLocaleString('zh-CN', {
  202 + year: 'numeric',
  203 + month: '2-digit',
  204 + day: '2-digit',
  205 + hour: '2-digit',
  206 + minute: '2-digit',
  207 + second: '2-digit'
  208 + })
  209 + },
  210 + search() {
  211 + this.listQuery = {
  212 + currentPage: 1,
  213 + pageSize: 20,
  214 + sort: "desc",
  215 + sidx: "",
  216 + }
  217 + this.initData()
  218 + },
  219 + refresh(isrRefresh) {
  220 + this.importVisible = false
  221 + this.batchDeleteVisible = false
  222 + if (isrRefresh) this.reset()
  223 + },
  224 + reset() {
  225 + for (let key in this.query) {
  226 + this.query[key] = undefined
  227 + }
  228 + this.listQuery = {
  229 + currentPage: 1,
  230 + pageSize: 20,
  231 + sort: "desc",
  232 + sidx: "",
  233 + }
  234 + this.initData()
  235 + }
  236 + }
  237 + }
  238 +</script>
... ...
antis-ncc-admin/src/views/lqMdxx/Form.vue
... ... @@ -94,6 +94,20 @@
94 94 </el-select>
95 95 </el-form-item>
96 96 </el-col>
  97 + <el-col :span="24">
  98 + <el-form-item label="门店类别" prop="storeCategory">
  99 + <el-select v-model="dataForm.storeCategory" placeholder="请选择" clearable :style='{"width":"100%"}' >
  100 + <el-option v-for="(item, index) in storeCategoryOptions" :key="index" :label="item.Name" :value="item.Value" ></el-option>
  101 + </el-select>
  102 + </el-form-item>
  103 + </el-col>
  104 + <el-col :span="24">
  105 + <el-form-item label="门店类型" prop="storeType">
  106 + <el-select v-model="dataForm.storeType" placeholder="请选择" clearable :style='{"width":"100%"}' >
  107 + <el-option v-for="(item, index) in storeTypeOptions" :key="index" :label="item.Name" :value="item.Value" ></el-option>
  108 + </el-select>
  109 + </el-form-item>
  110 + </el-col>
97 111 </el-form>
98 112 </el-row>
99 113 <span slot="footer" class="dialog-footer">
... ... @@ -131,16 +145,22 @@
131 145 gsmc:undefined,
132 146 fr:undefined,
133 147 ywsb:undefined,
  148 + storeCategory:undefined,
  149 + storeType:undefined,
134 150 },
135 151 rules: {
136 152 },
137 153 zxztOptions:[{"fullName":"开店","id":"开店"},{"fullName":"闭店","id":"闭店"}],
138 154 ywsbOptions:[{"fullName":"有","id":"有"},{"fullName":"无","id":"无"}],
  155 + storeCategoryOptions:[],
  156 + storeTypeOptions:[],
139 157 }
140 158 },
141 159 computed: {},
142 160 watch: {},
143 161 created() {
  162 + this.loadStoreCategoryOptions();
  163 + this.loadStoreTypeOptions();
144 164 },
145 165 mounted() {
146 166 },
... ... @@ -148,6 +168,30 @@
148 168 goBack() {
149 169 this.$emit('refresh')
150 170 },
  171 + // 加载门店类别选项
  172 + loadStoreCategoryOptions() {
  173 + request({
  174 + url: '/api/Extend/lqmdxx/Selector/StoreCategory',
  175 + method: 'get'
  176 + }).then(res => {
  177 + this.storeCategoryOptions = res.data || [];
  178 + }).catch(err => {
  179 + console.error('加载门店类别选项失败:', err);
  180 + this.storeCategoryOptions = [];
  181 + });
  182 + },
  183 + // 加载门店类型选项
  184 + loadStoreTypeOptions() {
  185 + request({
  186 + url: '/api/Extend/lqmdxx/Selector/StoreType',
  187 + method: 'get'
  188 + }).then(res => {
  189 + this.storeTypeOptions = res.data || [];
  190 + }).catch(err => {
  191 + console.error('加载门店类型选项失败:', err);
  192 + this.storeTypeOptions = [];
  193 + });
  194 + },
151 195 init(id, isDetail) {
152 196 this.dataForm.id = id || 0;
153 197 this.visible = true;
... ...
antis-ncc-admin/src/views/lqMdxx/SetNewStoreDialog.vue
... ... @@ -31,6 +31,13 @@
31 31 style="width: 100%">
32 32 </el-date-picker>
33 33 </el-form-item>
  34 + <el-form-item label="阶段" prop="stage">
  35 + <el-select v-model="form.stage" placeholder="请选择阶段" style="width: 100%" clearable>
  36 + <el-option label="第一阶段" :value="1"></el-option>
  37 + <el-option label="第二阶段" :value="2"></el-option>
  38 + <el-option label="第三阶段" :value="3"></el-option>
  39 + </el-select>
  40 + </el-form-item>
34 41 <el-form-item label="说明" prop="sm">
35 42 <el-input
36 43 v-model="form.sm"
... ... @@ -69,7 +76,8 @@ export default {
69 76 bhkssj: '',
70 77 bhjssj: '',
71 78 sm: '',
72   - sfqy: 1
  79 + sfqy: 1,
  80 + stage: null
73 81 },
74 82 rules: {
75 83 mdid: [
... ... @@ -81,6 +89,9 @@ export default {
81 89 bhjssj: [
82 90 { required: true, message: '请选择保护结束时间', trigger: 'change' }
83 91 ],
  92 + stage: [
  93 + { required: true, message: '请选择阶段', trigger: 'change' }
  94 + ],
84 95 sfqy: [
85 96 { required: true, message: '请选择是否启用', trigger: 'change' }
86 97 ]
... ... @@ -153,12 +164,13 @@ export default {
153 164 method: 'POST',
154 165 data: submitData
155 166 }).then(res => {
156   - this.$message.success('设置成功')
157   - this.handleClose()
158   - this.$emit('refresh')
159   - }).catch(err => {
160   - this.$message.error('设置失败:' + (err.msg || '未知错误'))
161   - console.error(err)
  167 + if(res.code == 200) {
  168 + this.$message.success('设置成功')
  169 + this.handleClose()
  170 + this.$emit('refresh')
  171 + } else {
  172 + this.$message.error(res.msg)
  173 + }
162 174 }).finally(() => {
163 175 this.submitLoading = false
164 176 })
... ...
antis-ncc-admin/src/views/lqMdxx/ViewNewStoreDialog.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + title="查看新店设置"
  4 + :visible.sync="visible"
  5 + width="800px"
  6 + :close-on-click-modal="false"
  7 + @close="close"
  8 + >
  9 + <div v-loading="loading">
  10 + <el-table :data="list" border stripe>
  11 + <el-table-column label="保护开始时间" width="150">
  12 + <template slot-scope="scope">
  13 + {{ formatTime(scope.row.bhkssj) }}
  14 + </template>
  15 + </el-table-column>
  16 + <el-table-column label="保护结束时间" width="150">
  17 + <template slot-scope="scope">
  18 + {{ formatTime(scope.row.bhjssj) }}
  19 + </template>
  20 + </el-table-column>
  21 + <el-table-column prop="sm" label="说明" />
  22 + <el-table-column label="创建时间" width="150">
  23 + <template slot-scope="scope">
  24 + {{ formatTime(scope.row.cjsj) }}
  25 + </template>
  26 + </el-table-column>
  27 + <el-table-column label="是否启用" width="100">
  28 + <template slot-scope="scope">
  29 + <el-tag :type="scope.row.sfqy === 1 ? 'success' : 'danger'" size="small">
  30 + {{ scope.row.sfqy === 1 ? '启用' : '禁用' }}
  31 + </el-tag>
  32 + </template>
  33 + </el-table-column>
  34 + <el-table-column label="阶段" width="80">
  35 + <template slot-scope="scope">
  36 + {{ scope.row.stage==1 ? '第一阶段' : scope.row.stage==2 ? '第二阶段' : scope.row.stage == 3 ? '第三阶段' : '无' }}
  37 + </template>
  38 + </el-table-column>
  39 + </el-table>
  40 +
  41 + <div v-if="list.length === 0" class="empty-data">
  42 + <i class="el-icon-info"></i>
  43 + <p>暂无新店设置数据</p>
  44 + </div>
  45 + </div>
  46 +
  47 + <div slot="footer" class="dialog-footer">
  48 + <el-button @click="close">关闭</el-button>
  49 + </div>
  50 + </el-dialog>
  51 +</template>
  52 +
  53 +<script>
  54 +import request from '@/utils/request'
  55 +
  56 +export default {
  57 + name: 'ViewNewStoreDialog',
  58 + data() {
  59 + return {
  60 + visible: false,
  61 + loading: false,
  62 + list: [],
  63 + storeId: null
  64 + }
  65 + },
  66 + methods: {
  67 + init(storeId) {
  68 + this.storeId = storeId
  69 + this.visible = true
  70 + this.loadData()
  71 + },
  72 + loadData() {
  73 + this.loading = true
  74 + request({
  75 + url: '/api/Extend/lqmdxdbhsj',
  76 + method: 'GET',
  77 + data: {
  78 + mdid: this.storeId
  79 + }
  80 + }).then(res => {
  81 + this.list = res.data.list || []
  82 + this.loading = false
  83 + }).catch(err => {
  84 + this.$message.error('获取数据失败')
  85 + this.loading = false
  86 + })
  87 + },
  88 + formatTime(timestamp) {
  89 + if (!timestamp) return '无'
  90 + const date = new Date(timestamp)
  91 + return date.toLocaleString('zh-CN', {
  92 + year: 'numeric',
  93 + month: '2-digit',
  94 + day: '2-digit',
  95 + hour: '2-digit',
  96 + minute: '2-digit',
  97 + second: '2-digit'
  98 + })
  99 + },
  100 + close() {
  101 + this.visible = false
  102 + this.list = []
  103 + this.storeId = null
  104 + }
  105 + }
  106 +}
  107 +</script>
  108 +
  109 +<style scoped>
  110 +.empty-data {
  111 + text-align: center;
  112 + padding: 40px 0;
  113 + color: #909399;
  114 +}
  115 +
  116 +.empty-data i {
  117 + font-size: 48px;
  118 + margin-bottom: 16px;
  119 + display: block;
  120 +}
  121 +
  122 +.empty-data p {
  123 + margin: 0;
  124 + font-size: 14px;
  125 +}
  126 +</style>
... ...
antis-ncc-admin/src/views/lqMdxx/index.vue
... ... @@ -3,7 +3,7 @@
3 3 <div class="NCC-common-layout-center">
4 4 <el-row class="NCC-common-search-box" :gutter="16">
5 5 <el-form @submit.native.prevent>
6   - <el-col :span="6">
  6 + <!-- <el-col :span="6">
7 7 <el-form-item label="主键">
8 8 <el-input v-model="query.id" placeholder="主键" clearable />
9 9 </el-form-item>
... ... @@ -17,9 +17,8 @@
17 17 <el-form-item label="单据门店编号">
18 18 <el-input v-model="query.djmdbh" placeholder="单据门店编号" clearable />
19 19 </el-form-item>
20   - </el-col>
21   - <template v-if="showAll">
22   - <el-col :span="6">
  20 + </el-col> -->
  21 + <el-col :span="6">
23 22 <el-form-item label="单据门店">
24 23 <el-input v-model="query.djmd" placeholder="单据门店" clearable />
25 24 </el-form-item>
... ... @@ -34,11 +33,13 @@
34 33 <el-input v-model="query.cs" placeholder="城市" clearable />
35 34 </el-form-item>
36 35 </el-col>
37   - <el-col :span="6">
  36 + <template v-if="showAll">
  37 +
  38 + <!-- <el-col :span="6">
38 39 <el-form-item label="地址">
39 40 <el-input v-model="query.dz" placeholder="地址" clearable />
40 41 </el-form-item>
41   - </el-col>
  42 + </el-col> -->
42 43 <el-col :span="6">
43 44 <el-form-item label="姓名">
44 45 <el-input v-model="query.xm" placeholder="姓名" clearable />
... ... @@ -110,34 +111,40 @@
110 111 </div>
111 112 </div>
112 113 <NCC-table v-loading="listLoading" :data="list" has-c @selection-change="handleSelectionChange">
113   - <el-table-column prop="id" label="主键" align="left" />
114   - <el-table-column prop="mdbm" label="门店编码" align="left" />
115   - <el-table-column prop="djmdbh" label="单据门店编号" align="left" />
  114 + <!-- <el-table-column prop="id" label="主键" align="left" /> -->
  115 + <!-- <el-table-column prop="mdbm" label="门店编码" align="left" />
  116 + <el-table-column prop="djmdbh" label="门店编号" align="left" /> -->
116 117 <el-table-column prop="djmd" label="单据门店" align="left" />
117 118 <el-table-column prop="dm" label="店名" align="left" />
118 119 <el-table-column prop="cs" label="城市" align="left" />
119   - <el-table-column prop="dz" label="地址" align="left" />
  120 + <!-- <el-table-column prop="dz" label="地址" align="left" /> -->
120 121 <el-table-column prop="xm" label="姓名" align="left" />
121 122 <el-table-column prop="dhhm" label="电话号码" align="left" />
122 123 <el-table-column prop="zj" label="座机" align="left" />
123   - <el-table-column prop="kysj" label="开业时间" align="left" />
  124 + <el-table-column prop="kysj" width="150" label="开业时间" align="left" :formatter="ncc.tableDateFormat"/>
124 125 <el-table-column label="最新状态" prop="zxzt" align="left">
125 126 <template slot-scope="scope">{{ scope.row.zxzt | dynamicText(zxztOptions) }}</template>
126 127 </el-table-column>
127 128 <el-table-column prop="gsmc" label="工商名称" align="left" />
128 129 <el-table-column prop="fr" label="法人" align="left" />
129   - <el-table-column label="有无社保" prop="ywsb" align="left">
  130 + <!-- <el-table-column label="有无社保" prop="ywsb" align="left">
130 131 <template slot-scope="scope">{{ scope.row.ywsb | dynamicText(ywsbOptions) }}</template>
131   - </el-table-column>
  132 + </el-table-column> -->
132 133 <el-table-column label="是否新店" prop="isNewStore" align="left">
133 134 <template slot-scope="scope">
134 135 <el-tag v-if="scope.row.isNewStore" type="success" size="small">新店</el-tag>
135 136 <el-tag v-else type="info" size="small">老店</el-tag>
136 137 </template>
137 138 </el-table-column>
  139 + <el-table-column label="阶段" prop="stage" align="left">
  140 + <template slot-scope="scope">
  141 + {{ scope.row.stage==1 ? '第一阶段' : scope.row.stage==2 ? '第二阶段' : scope.row.stage == 3 ? '第三阶段' : '无' }}
  142 + </template>
  143 + </el-table-column>
138 144 <el-table-column label="操作" fixed="right" width="150">
139 145 <template slot-scope="scope">
140 146 <el-button type="text" @click="addOrUpdateHandle(scope.row.id)" >编辑</el-button>
  147 + <el-button type="text" @click="viewNewStoreHandle(scope.row)">查看新店设置</el-button>
141 148 <el-button type="text" @click="setNewStoreHandle(scope.row)" >设置新店</el-button>
142 149 <el-button type="text" @click="handleDel(scope.row.id)" class="NCC-table-delBtn" >删除</el-button>
143 150 </template>
... ... @@ -149,6 +156,7 @@
149 156 <NCC-Form v-if="formVisible" ref="NCCForm" @refresh="refresh" />
150 157 <ExportBox v-if="exportBoxVisible" ref="ExportBox" @download="download" />
151 158 <SetNewStoreDialog v-if="setNewStoreVisible" ref="SetNewStoreDialog" @refresh="refresh" />
  159 + <ViewNewStoreDialog v-if="viewNewStoreVisible" ref="ViewNewStoreDialog" />
152 160 </div>
153 161 </template>
154 162 <script>
... ... @@ -157,9 +165,10 @@
157 165 import NCCForm from './Form'
158 166 import ExportBox from './ExportBox'
159 167 import SetNewStoreDialog from './SetNewStoreDialog'
  168 + import ViewNewStoreDialog from './ViewNewStoreDialog'
160 169 import { previewDataInterface } from '@/api/systemData/dataInterface'
161 170 export default {
162   - components: { NCCForm, ExportBox, SetNewStoreDialog },
  171 + components: { NCCForm, ExportBox, SetNewStoreDialog, ViewNewStoreDialog },
163 172 data() {
164 173 return {
165 174 showAll: false,
... ... @@ -192,6 +201,7 @@
192 201 formVisible: false,
193 202 exportBoxVisible: false,
194 203 setNewStoreVisible: false,
  204 + viewNewStoreVisible: false,
195 205 columnList: [
196 206 { prop: 'id', label: '主键' },
197 207 { prop: 'mdbm', label: '门店编码' },
... ... @@ -314,6 +324,12 @@
314 324 this.$refs.SetNewStoreDialog.init(storeData)
315 325 })
316 326 },
  327 + viewNewStoreHandle(storeData) {
  328 + this.viewNewStoreVisible = true
  329 + this.$nextTick(() => {
  330 + this.$refs.ViewNewStoreDialog.init(storeData.id)
  331 + })
  332 + },
317 333 checkNewStoreStatus() {
318 334 // 为每个门店检查是否在新店保护期内
319 335 this.list.forEach(store => {
... ... @@ -322,8 +338,10 @@
322 338 method: 'GET'
323 339 }).then(res => {
324 340 this.$set(store, 'isNewStore', res.data.hasProtection)
  341 + this.$set(store, 'stage', res.data.data.stage)
325 342 }).catch(err => {
326 343 this.$set(store, 'isNewStore', false)
  344 + this.$set(store, 'stage', 0)
327 345 })
328 346 })
329 347 },
... ...
antis-ncc-admin/src/views/lqXmzl/Form.vue
... ... @@ -168,6 +168,18 @@
168 168 </el-select>
169 169 </el-form-item>
170 170 </el-col>
  171 + <el-col :span="24">
  172 + <el-form-item label="是否有效" prop="isEffective">
  173 + <el-select v-model="dataForm.isEffective" placeholder="请选择">
  174 + <el-option
  175 + v-for="item in effectiveOptions"
  176 + :key="item.value"
  177 + :label="item.label"
  178 + :value="item.value">
  179 + </el-option>
  180 + </el-select>
  181 + </el-form-item>
  182 + </el-col>
171 183 </el-form>
172 184 </el-row>
173 185 <span slot="footer" class="dialog-footer">
... ... @@ -208,6 +220,7 @@ import { init } from &#39;echarts/lib/echarts&#39;;
208 220 qt1:undefined,
209 221 qt2:undefined,
210 222 beautyType:undefined,
  223 + isEffective: 1, // 默认有效
211 224 },
212 225 rules: {
213 226 },
... ... @@ -219,6 +232,7 @@ import { init } from &#39;echarts/lib/echarts&#39;;
219 232 options6:[],
220 233 options7:[],
221 234 options8:[],
  235 + effectiveOptions: [],
222 236 }
223 237 },
224 238 computed: {},
... ... @@ -335,6 +349,20 @@ import { init } from &#39;echarts/lib/echarts&#39;;
335 349 this.options8 = []
336 350 }
337 351 })
  352 + // 获取状态枚举数据
  353 + request({
  354 + url: '/api/Extend/lqkdkdjlb/status-enum',
  355 + method: 'get',
  356 + }).then(res => {
  357 + if (res.code == 200 && res.data) {
  358 + this.effectiveOptions = res.data.map(item => ({
  359 + label: item.Name,
  360 + value: item.Value
  361 + }));
  362 + } else {
  363 + this.effectiveOptions = []
  364 + }
  365 + })
338 366 },
339 367 goBack() {
340 368 this.$emit('refresh')
... ... @@ -351,6 +379,7 @@ import { init } from &#39;echarts/lib/echarts&#39;;
351 379 method: 'get'
352 380 }).then(res =>{
353 381 this.dataForm = res.data;
  382 + this.dataForm.isEffective = res.data.isEffective?res.data.isEffective:1;
354 383 })
355 384 }
356 385 })
... ...
antis-ncc-admin/src/views/lqXmzl/index.vue
... ... @@ -140,6 +140,18 @@
140 140 </el-select>
141 141 </el-form-item>
142 142 </el-col>
  143 + <el-col :span="6">
  144 + <el-form-item label="是否有效">
  145 + <el-select v-model="query.isEffective" placeholder="请选择状态" clearable>
  146 + <el-option
  147 + v-for="item in effectiveSearchOptions"
  148 + :key="item.value"
  149 + :label="item.label"
  150 + :value="item.value">
  151 + </el-option>
  152 + </el-select>
  153 + </el-form-item>
  154 + </el-col>
143 155 </template>
144 156 <el-col :span="6">
145 157 <el-form-item>
... ... @@ -182,10 +194,18 @@
182 194 <el-table-column prop="fl" label="分类" align="left" />
183 195 <el-table-column prop="qt1" label="其它1" align="left" />
184 196 <el-table-column prop="qt2" label="其它2" align="left" />
  197 + <el-table-column prop="isEffective" label="是否有效" align="left">
  198 + <template slot-scope="scope">
  199 + <span v-if="scope.row.isEffective === 1" style="color: #67C23A;">有效</span>
  200 + <span v-else-if="scope.row.isEffective === -1" style="color: #F56C6C;">无效</span>
  201 + <span v-else-if="scope.row.isEffective === 99" style="color: #909399;">删除</span>
  202 + <span v-else>无</span>
  203 + </template>
  204 + </el-table-column>
185 205 <el-table-column label="操作" fixed="right" width="100">
186 206 <template slot-scope="scope">
187 207 <el-button type="text" @click="addOrUpdateHandle(scope.row.id)" >编辑</el-button>
188   - <el-button type="text" @click="handleDel(scope.row.id)" class="NCC-table-delBtn" >删除</el-button>
  208 + <!-- <el-button type="text" @click="handleDel(scope.row.id)" class="NCC-table-delBtn" >删除</el-button> -->
189 209 </template>
190 210 </el-table-column>
191 211 </NCC-table>
... ... @@ -224,6 +244,7 @@
224 244 qt1:undefined,
225 245 qt2:undefined,
226 246 beautyType:undefined,
  247 + isEffective:undefined,
227 248 },
228 249 list: [],
229 250 listLoading: true,
... ... @@ -252,6 +273,7 @@
252 273 { prop: 'fl', label: '分类' },
253 274 { prop: 'qt1', label: '其它1' },
254 275 { prop: 'qt2', label: '其它2' },
  276 + { prop: 'isEffective', label: '是否有效' },
255 277 ],
256 278 // 搜索选项数据
257 279 searchOptions1: [],
... ... @@ -262,6 +284,7 @@
262 284 searchOptions6: [],
263 285 searchOptions7: [],
264 286 searchOptions8: [],
  287 + effectiveSearchOptions: [],
265 288 }
266 289 },
267 290 computed: {},
... ... @@ -390,6 +413,21 @@
390 413 this.searchOptions8 = []
391 414 }
392 415 })
  416 +
  417 + // 获取状态枚举选项
  418 + request({
  419 + url: '/api/Extend/lqkdkdjlb/status-enum',
  420 + method: 'get',
  421 + }).then(res => {
  422 + if (res.code == 200 && res.data) {
  423 + this.effectiveSearchOptions = res.data.map(item => ({
  424 + label: item.Name,
  425 + value: item.Value
  426 + }));
  427 + } else {
  428 + this.effectiveSearchOptions = []
  429 + }
  430 + })
393 431 },
394 432 initData() {
395 433 this.listLoading = true;
... ...
antis-ncc-admin/src/views/statisticsList/form1.vue 0 → 100644
  1 +<template>
  2 + <div class="NCC-common-layout">
  3 + <div class="NCC-common-layout-center">
  4 + <!-- 筛选条件 -->
  5 + <el-row class="NCC-common-search-box" :gutter="16">
  6 + <el-form @submit.native.prevent>
  7 + <el-col :span="6">
  8 + <el-form-item label="门店" required>
  9 + <el-select v-model="query.storeId" placeholder="请选择门店" clearable filterable>
  10 + <el-option v-for="store in storeOptions" :key="store.id" :label="store.dm" :value="store.id" />
  11 + </el-select>
  12 + </el-form-item>
  13 + </el-col>
  14 + <el-col :span="6">
  15 + <el-form-item label="开始时间">
  16 + <el-date-picker
  17 + v-model="query.startTime"
  18 + type="datetime"
  19 + value-format="timestamp"
  20 + format="yyyy-MM-dd HH:mm:ss"
  21 + placeholder="开始时间"
  22 + >
  23 + </el-date-picker>
  24 + <!-- :picker-options="startTimePickerOptions" -->
  25 + </el-form-item>
  26 + </el-col>
  27 + <el-col :span="6">
  28 + <el-form-item label="结束时间">
  29 + <el-date-picker
  30 + v-model="query.endTime"
  31 + type="datetime"
  32 + value-format="timestamp"
  33 + format="yyyy-MM-dd HH:mm:ss"
  34 + placeholder="结束时间"
  35 + >
  36 + <!-- :picker-options="endTimePickerOptions" -->
  37 + </el-date-picker>
  38 + </el-form-item>
  39 + </el-col>
  40 + <el-col :span="6">
  41 + <el-form-item>
  42 + <el-button type="primary" icon="el-icon-search" @click="search()">查询</el-button>
  43 + <el-button icon="el-icon-refresh-right" @click="reset()">重置</el-button>
  44 + </el-form-item>
  45 + </el-col>
  46 + </el-form>
  47 + </el-row>
  48 +
  49 + <!-- 统计卡片 -->
  50 + <div class="statistics-cards" v-if="summaryData">
  51 + <el-row :gutter="16">
  52 + <el-col :span="6">
  53 + <div class="statistics-card">
  54 + <div class="card-icon">
  55 + <i class="el-icon-document"></i>
  56 + </div>
  57 + <div class="card-content">
  58 + <div class="card-title">总开单数</div>
  59 + <div class="card-value">{{ summaryData.totalCount || 0 }}</div>
  60 + </div>
  61 + </div>
  62 + </el-col>
  63 + <el-col :span="6">
  64 + <div class="statistics-card">
  65 + <div class="card-icon">
  66 + <i class="el-icon-money"></i>
  67 + </div>
  68 + <div class="card-content">
  69 + <div class="card-title">总金额</div>
  70 + <div class="card-value">¥{{ formatMoney(summaryData.totalAmount) }}</div>
  71 + </div>
  72 + </div>
  73 + </el-col>
  74 + <el-col :span="6">
  75 + <div class="statistics-card">
  76 + <div class="card-icon">
  77 + <i class="el-icon-success"></i>
  78 + </div>
  79 + <div class="card-content">
  80 + <div class="card-title">已付金额</div>
  81 + <div class="card-value">¥{{ formatMoney(summaryData.totalPaidAmount) }}</div>
  82 + </div>
  83 + </div>
  84 + </el-col>
  85 + <el-col :span="6">
  86 + <div class="statistics-card">
  87 + <div class="card-icon">
  88 + <i class="el-icon-warning"></i>
  89 + </div>
  90 + <div class="card-content">
  91 + <div class="card-title">欠款金额</div>
  92 + <div class="card-value">¥{{ formatMoney(summaryData.totalDebt) }}</div>
  93 + </div>
  94 + </div>
  95 + </el-col>
  96 + </el-row>
  97 + <el-row :gutter="16" style="margin-top: 16px;">
  98 + <el-col :span="6">
  99 + <div class="statistics-card">
  100 + <div class="card-icon">
  101 + <i class="el-icon-goods"></i>
  102 + </div>
  103 + <div class="card-content">
  104 + <div class="card-title">购买项目</div>
  105 + <div class="card-value">{{ summaryData.totalPurchasedItems || 0 }}</div>
  106 + </div>
  107 + </div>
  108 + </el-col>
  109 + <el-col :span="6">
  110 + <div class="statistics-card">
  111 + <div class="card-icon">
  112 + <i class="el-icon-present"></i>
  113 + </div>
  114 + <div class="card-content">
  115 + <div class="card-title">赠送项目</div>
  116 + <div class="card-value">{{ summaryData.totalGiftedItems || 0 }}</div>
  117 + </div>
  118 + </div>
  119 + </el-col>
  120 + <el-col :span="6">
  121 + <div class="statistics-card">
  122 + <div class="card-icon">
  123 + <i class="el-icon-star-on"></i>
  124 + </div>
  125 + <div class="card-content">
  126 + <div class="card-title">体验项目</div>
  127 + <div class="card-value">{{ summaryData.totalExperienceItems || 0 }}</div>
  128 + </div>
  129 + </div>
  130 + </el-col>
  131 + <el-col :span="6">
  132 + <div class="statistics-card">
  133 + <div class="card-icon">
  134 + <i class="el-icon-user"></i>
  135 + </div>
  136 + <div class="card-content">
  137 + <div class="card-title">健康师人数</div>
  138 + <div class="card-value">{{ summaryData.totalHealthTeachers || 0 }}</div>
  139 + </div>
  140 + </div>
  141 + </el-col>
  142 + </el-row>
  143 + </div>
  144 +
  145 + <!-- 数据表格 -->
  146 + <div class="NCC-common-layout-main NCC-flex-main">
  147 + <!-- <div class="NCC-common-head">
  148 + <div>
  149 + <el-button type="text" icon="el-icon-download" @click="exportData()">导出</el-button>
  150 + </div>
  151 + <div class="NCC-common-head-right">
  152 + <el-tooltip effect="dark" content="刷新" placement="top">
  153 + <el-link icon="icon-ym icon-ym-Refresh NCC-common-head-icon" :underline="false" @click="reset()" />
  154 + </el-tooltip>
  155 + <screenfull isContainer />
  156 + </div>
  157 + </div> -->
  158 +
  159 + <NCC-table v-loading="listLoading" :data="records" has-c>
  160 + <el-table-column prop="date" label="开单日期" align="left" width="120">
  161 + <template slot-scope="scope">
  162 + {{ formatDate(scope.row.date) }}
  163 + </template>
  164 + </el-table-column>
  165 + <el-table-column prop="customerName" label="客户姓名" align="left" width="100" />
  166 + <el-table-column prop="customerPhone" label="客户电话" align="left" width="120" />
  167 + <el-table-column prop="goldTriangle" label="金三角" align="left" width="100" />
  168 + <el-table-column prop="customerType" label="客户类型" align="left" width="100">
  169 + <template slot-scope="scope">
  170 + <el-tag :type="scope.row.customerType === '会员' ? 'success' : 'info'" size="small">
  171 + {{ scope.row.customerType }}
  172 + </el-tag>
  173 + </template>
  174 + </el-table-column>
  175 + <el-table-column label="购买项目" align="left" min-width="200">
  176 + <template slot-scope="scope">
  177 + <div v-if="scope.row.purchasedItems && scope.row.purchasedItems.length > 0">
  178 + <div v-for="item in scope.row.purchasedItems" :key="item.id" class="item-row">
  179 + <span class="item-name">{{ item.itemName }}</span>
  180 + <span class="item-info">({{ item.projectNumber }}次 × ¥{{ item.price }})</span>
  181 + </div>
  182 + </div>
  183 + <span v-else class="no-data">无</span>
  184 + </template>
  185 + </el-table-column>
  186 + <el-table-column label="赠送项目" align="left" min-width="200">
  187 + <template slot-scope="scope">
  188 + <div v-if="scope.row.giftedItems && scope.row.giftedItems.length > 0">
  189 + <div v-for="item in scope.row.giftedItems" :key="item.id" class="item-row">
  190 + <span class="item-name">{{ item.itemName }}</span>
  191 + <span class="item-info">({{ item.projectNumber }}次)</span>
  192 + <span v-if="item.remark" class="item-remark">{{ item.remark }}</span>
  193 + </div>
  194 + </div>
  195 + <span v-else class="no-data">无</span>
  196 + </template>
  197 + </el-table-column>
  198 + <el-table-column label="体验项目" align="left" min-width="200">
  199 + <template slot-scope="scope">
  200 + <div v-if="scope.row.experienceItems && scope.row.experienceItems.length > 0">
  201 + <div v-for="item in scope.row.experienceItems" :key="item.id" class="item-row">
  202 + <span class="item-name">{{ item.itemName }}</span>
  203 + <span class="item-info">({{ item.projectNumber }}次)</span>
  204 + </div>
  205 + </div>
  206 + <span v-else class="no-data">无</span>
  207 + </template>
  208 + </el-table-column>
  209 + <el-table-column label="健康师" align="left" min-width="150">
  210 + <template slot-scope="scope">
  211 + <div v-if="scope.row.healthTeachers && scope.row.healthTeachers.length > 0">
  212 + <div v-for="teacher in scope.row.healthTeachers" :key="teacher.teacherId" class="teacher-row">
  213 + <span class="teacher-name">{{ teacher.teacherName }}</span>
  214 + <span class="teacher-performance">业绩: ¥{{ teacher.performance }}</span>
  215 + </div>
  216 + </div>
  217 + <span v-else class="no-data">无</span>
  218 + </template>
  219 + </el-table-column>
  220 + <el-table-column prop="paidAmount" label="已付金额" align="left" width="100">
  221 + <template slot-scope="scope">
  222 + <span class="amount-paid">¥{{ formatMoney(scope.row.paidAmount) }}</span>
  223 + </template>
  224 + </el-table-column>
  225 + <el-table-column prop="debtAmount" label="欠款金额" align="left" width="100">
  226 + <template slot-scope="scope">
  227 + <span class="amount-debt">¥{{ formatMoney(scope.row.debtAmount) }}</span>
  228 + </template>
  229 + </el-table-column>
  230 + <el-table-column prop="totalAmount" label="总金额" align="left" width="100">
  231 + <template slot-scope="scope">
  232 + <span class="amount-total">¥{{ formatMoney(scope.row.totalAmount) }}</span>
  233 + </template>
  234 + </el-table-column>
  235 + <el-table-column prop="paymentMethod" label="支付方式" align="left" width="100" />
  236 + <el-table-column prop="remark" label="备注" align="left" min-width="150">
  237 + <template slot-scope="scope">
  238 + {{ scope.row.remark || '无' }}
  239 + </template>
  240 + </el-table-column>
  241 + <el-table-column label="开单时间" prop="createTime" align="left" width="150">
  242 + <template slot-scope="scope">
  243 + {{ formatTime(scope.row.createTime) }}
  244 + </template>
  245 + </el-table-column>
  246 + </NCC-table>
  247 + </div>
  248 + </div>
  249 + </div>
  250 +</template>
  251 +
  252 +<script>
  253 +import request from '@/utils/request'
  254 +
  255 +export default {
  256 + name: 'BillingRecordSummary',
  257 + data() {
  258 + return {
  259 + query: {
  260 + storeId: undefined,
  261 + startTime: undefined,
  262 + endTime: undefined
  263 + },
  264 + storeOptions: [],
  265 + summaryData: null,
  266 + records: [],
  267 + listLoading: false,
  268 + startTimePickerOptions: {
  269 + disabledDate: (time) => {
  270 + const timeMs = time.getTime()
  271 + const nowMs = Date.now()
  272 + const ninetyDaysMs = 90 * 24 * 60 * 60 * 1000
  273 + // 不能选择未来日期
  274 + if (timeMs > nowMs) return true
  275 + // 若已选择结束时间:开始时间需在 [end-90天, end] 区间内
  276 + if (this.query.endTime) {
  277 + const minStart = this.query.endTime - ninetyDaysMs
  278 + return timeMs < minStart || timeMs > this.query.endTime
  279 + }
  280 + return false
  281 + }
  282 + },
  283 + endTimePickerOptions: {
  284 + disabledDate: (time) => {
  285 + const timeMs = time.getTime()
  286 + const nowMs = Date.now()
  287 + const ninetyDaysMs = 90 * 24 * 60 * 60 * 1000
  288 + // 不能选择未来日期
  289 + if (timeMs > nowMs) return true
  290 + // 若已选择开始时间:结束时间需在 [start, start+90天] 区间内
  291 + if (this.query.startTime) {
  292 + const maxEnd = this.query.startTime + ninetyDaysMs
  293 + return timeMs < this.query.startTime || timeMs > maxEnd
  294 + }
  295 + return false
  296 + }
  297 + }
  298 + }
  299 + },
  300 + created() {
  301 + this.initStoreOptions()
  302 + this.setDefaultTimeRange()
  303 + },
  304 + methods: {
  305 + // 初始化门店选项
  306 + initStoreOptions() {
  307 + request({
  308 + url: '/api/Extend/LqMdxx',
  309 + method: 'GET',
  310 + data: {
  311 + currentPage: 1,
  312 + pageSize: 1000
  313 + }
  314 + }).then(res => {
  315 + this.storeOptions = res.data.list || []
  316 + }).catch(err => {
  317 + console.error('获取门店列表失败:', err)
  318 + })
  319 + },
  320 +
  321 + // 设置默认时间范围(最近三个月)
  322 + setDefaultTimeRange() {
  323 + const now = new Date()
  324 + const threeMonthsAgo = new Date()
  325 + threeMonthsAgo.setMonth(now.getMonth() - 3)
  326 +
  327 + this.query.startTime = threeMonthsAgo.getTime()
  328 + this.query.endTime = now.getTime()
  329 + },
  330 +
  331 + // 查询数据
  332 + search() {
  333 + if (!this.query.storeId) {
  334 + this.$message({
  335 + type: 'warning',
  336 + message: '请选择门店',
  337 + duration: 1500
  338 + })
  339 + return
  340 + }
  341 +
  342 + this.listLoading = true
  343 +
  344 + const params = {
  345 + storeId: this.query.storeId,
  346 + startTime: this.query.startTime ? this.formatDateTime(this.query.startTime) : undefined,
  347 + endTime: this.query.endTime ? this.formatDateTime(this.query.endTime) : undefined
  348 + }
  349 +
  350 + request({
  351 + url: '/api/Extend/lqkdkdjlb/GetBillingRecordSummaryByStoreId',
  352 + method: 'GET',
  353 + data: params
  354 + }).then(res => {
  355 + if (res.data && res.data.data) {
  356 + this.summaryData = res.data.data.summary
  357 + this.records = res.data.data.records || []
  358 + } else {
  359 + this.summaryData = null
  360 + this.records = []
  361 + }
  362 + this.listLoading = false
  363 + }).catch(err => {
  364 + console.error('查询失败:', err)
  365 + this.$message({
  366 + type: 'error',
  367 + message: '查询失败,请重试',
  368 + duration: 1500
  369 + })
  370 + this.listLoading = false
  371 + })
  372 + },
  373 +
  374 + // 重置查询条件
  375 + reset() {
  376 + this.query = {
  377 + storeId: undefined,
  378 + startTime: undefined,
  379 + endTime: undefined
  380 + }
  381 + this.summaryData = null
  382 + this.records = []
  383 + this.setDefaultTimeRange()
  384 + },
  385 +
  386 + // 导出数据
  387 + exportData() {
  388 + if (!this.query.storeId) {
  389 + this.$message({
  390 + type: 'warning',
  391 + message: '请先选择门店并查询数据',
  392 + duration: 1500
  393 + })
  394 + return
  395 + }
  396 +
  397 + const params = {
  398 + storeId: this.query.storeId,
  399 + startTime: this.query.startTime,
  400 + endTime: this.query.endTime
  401 + }
  402 +
  403 + // 这里可以调用导出接口
  404 + this.$message({
  405 + type: 'info',
  406 + message: '导出功能开发中...',
  407 + duration: 1500
  408 + })
  409 + },
  410 +
  411 + // 格式化金额
  412 + formatMoney(amount) {
  413 + if (!amount && amount !== 0) return '0.00'
  414 + return Number(amount).toFixed(2)
  415 + },
  416 +
  417 + // 格式化日期
  418 + formatDate(dateStr) {
  419 + if (!dateStr) return '无'
  420 + return dateStr
  421 + },
  422 +
  423 + // 格式化时间
  424 + formatTime(timestamp) {
  425 + if (!timestamp) return '无'
  426 + const date = new Date(timestamp)
  427 + return date.toLocaleString('zh-CN', {
  428 + year: 'numeric',
  429 + month: '2-digit',
  430 + day: '2-digit',
  431 + hour: '2-digit',
  432 + minute: '2-digit',
  433 + second: '2-digit'
  434 + })
  435 + },
  436 +
  437 + // 格式化日期时间(用于API传参)
  438 + formatDateTime(timestamp) {
  439 + if (!timestamp) return ''
  440 + const date = new Date(timestamp)
  441 + const year = date.getFullYear()
  442 + const month = String(date.getMonth() + 1).padStart(2, '0')
  443 + const day = String(date.getDate()).padStart(2, '0')
  444 + const hours = String(date.getHours()).padStart(2, '0')
  445 + const minutes = String(date.getMinutes()).padStart(2, '0')
  446 + const seconds = String(date.getSeconds()).padStart(2, '0')
  447 + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
  448 + }
  449 + }
  450 +}
  451 +</script>
  452 +
  453 +<style lang="scss" scoped>
  454 +.statistics-cards {
  455 + margin-bottom: 20px;
  456 +}
  457 +
  458 +.statistics-card {
  459 + height: 100px;
  460 + padding: 12px;
  461 + border-radius: 12px;
  462 + background: #fff;
  463 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  464 + display: flex;
  465 + align-items: center;
  466 +
  467 + .card-icon {
  468 + width: 60px;
  469 + height: 60px;
  470 + border-radius: 8px;
  471 + background: linear-gradient(135deg, #409EFF, #67C23A);
  472 + display: flex;
  473 + align-items: center;
  474 + justify-content: center;
  475 + margin-right: 16px;
  476 +
  477 + i {
  478 + font-size: 24px;
  479 + color: #fff;
  480 + }
  481 + }
  482 +
  483 + .card-content {
  484 + flex: 1;
  485 +
  486 + .card-title {
  487 + font-size: 14px;
  488 + color: #909399;
  489 + margin-bottom: 8px;
  490 + }
  491 +
  492 + .card-value {
  493 + font-size: 24px;
  494 + font-weight: bold;
  495 + color: #303133;
  496 + }
  497 + }
  498 +}
  499 +
  500 +.item-row, .teacher-row {
  501 + margin-bottom: 4px;
  502 +
  503 + &:last-child {
  504 + margin-bottom: 0;
  505 + }
  506 +}
  507 +
  508 +.item-name, .teacher-name {
  509 + font-weight: 500;
  510 + color: #303133;
  511 +}
  512 +
  513 +.item-info, .teacher-performance {
  514 + font-size: 12px;
  515 + color: #909399;
  516 + margin-left: 8px;
  517 +}
  518 +
  519 +.item-remark {
  520 + font-size: 12px;
  521 + color: #F56C6C;
  522 + margin-left: 8px;
  523 +}
  524 +
  525 +.no-data {
  526 + color: #C0C4CC;
  527 + font-style: italic;
  528 +}
  529 +
  530 +.amount-paid {
  531 + color: #67C23A;
  532 + font-weight: 500;
  533 +}
  534 +
  535 +.amount-debt {
  536 + color: #F56C6C;
  537 + font-weight: 500;
  538 +}
  539 +
  540 +.amount-total {
  541 + color: #409EFF;
  542 + font-weight: 500;
  543 +}
  544 +
  545 +// 响应式设计
  546 +@media (max-width: 768px) {
  547 + .statistics-cards {
  548 + .el-col {
  549 + margin-bottom: 16px;
  550 + }
  551 + }
  552 +
  553 + .statistics-card {
  554 + height: 80px;
  555 + padding: 8px;
  556 +
  557 + .card-icon {
  558 + width: 50px;
  559 + height: 50px;
  560 + margin-right: 12px;
  561 +
  562 + i {
  563 + font-size: 20px;
  564 + }
  565 + }
  566 +
  567 + .card-content {
  568 + .card-title {
  569 + font-size: 12px;
  570 + }
  571 +
  572 + .card-value {
  573 + font-size: 18px;
  574 + }
  575 + }
  576 + }
  577 +}
  578 +</style>
... ...