| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859 |
- <template>
- <div class="page-container">
- <!-- 搜索区域 -->
- <el-card class="search-card">
- <el-form :model="queryForm" inline>
- <el-form-item label="姓名/手机号">
- <el-input v-model="queryForm.keyword" placeholder="请输入姓名或手机号" clearable style="width:200px" />
- </el-form-item>
- <el-form-item label="设备编号">
- <el-input v-model="queryForm.deviceCode" placeholder="请输入设备编号" clearable style="width:180px" />
- </el-form-item>
- <el-form-item label="性别">
- <el-select v-model="queryForm.gender" placeholder="全部" clearable style="width:100px">
- <el-option label="男" :value="1" />
- <el-option label="女" :value="2" />
- </el-select>
- </el-form-item>
- <el-form-item label="用户类别">
- <el-select v-model="queryForm.userType" placeholder="全部" clearable style="width:120px">
- <el-option
- v-for="item in userCategoryOptions"
- :key="item.code"
- :label="item.name"
- :value="item.code"
- />
- </el-select>
- </el-form-item>
- <el-form-item label="省">
- <el-select-v2
- v-model="queryForm.provinceCode"
- :options="provinceList"
- placeholder="全部"
- clearable
- filterable
- style="width:160px"
- @change="onQueryProvinceChange"
- />
- </el-form-item>
- <el-form-item label="市">
- <el-select-v2
- v-model="queryForm.cityCode"
- :options="queryCityList"
- placeholder="全部"
- clearable
- filterable
- style="width:200px"
- @change="onQueryCityChange"
- />
- </el-form-item>
- <el-form-item label="区/县">
- <el-select-v2
- v-model="queryForm.districtCode"
- :options="queryDistrictList"
- placeholder="全部"
- clearable
- filterable
- style="width:240px"
- />
- </el-form-item>
- <el-form-item label="注册时间">
- <el-date-picker
- v-model="queryForm.dateRange"
- type="daterange"
- range-separator="至"
- start-placeholder="开始日期"
- end-placeholder="结束日期"
- format="YYYY-MM-DD HH:mm:ss"
- value-format="YYYY-MM-DD HH:mm:ss"
- :default-time="[new Date(0,0,0,0,0,0), new Date(0,0,0,23,59,59)]"
- style="width:360px"
- />
- </el-form-item>
- <el-form-item label="穴位表">
- <el-select v-model="queryForm.acupointStatus" placeholder="全部" clearable style="width:110px">
- <el-option label="已生成" :value="1" />
- <el-option label="未生成" :value="0" />
- </el-select>
- </el-form-item>
- <el-form-item>
- <el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
- <el-button :icon="Refresh" @click="handleReset">重置</el-button>
- </el-form-item>
- </el-form>
- </el-card>
- <!-- 表格区域 -->
- <el-card class="table-card">
- <template #header>
- <div class="card-header">
- <span class="title">用户管理</span>
- <div class="btn-group">
- <ExcelImport :on-import="handleImport" button-text="导入" @success="onImportSuccess" />
- <ExcelExport
- :headers="exportHeaders"
- :data="tableData"
- file-name="用户列表"
- />
- <el-button type="primary" :icon="Plus" @click="handleAdd">新增</el-button>
- <el-button
- type="danger"
- :icon="Delete"
- :disabled="selectedIds.length === 0"
- @click="handleBatchDelete"
- >批量删除</el-button>
- </div>
- </div>
- </template>
- <el-table
- :data="tableData"
- v-loading="loading"
- border
- stripe
- @selection-change="handleSelectionChange"
- >
- <el-table-column type="selection" width="50" align="center" />
- <el-table-column type="index" label="序号" width="60" align="center" />
- <el-table-column prop="id" label="用户ID" width="80" align="center" />
- <el-table-column prop="name" label="姓名" width="100" />
- <el-table-column prop="gender" label="性别" width="70" align="center">
- <template #default="{ row }">
- <el-tag :type="row.gender === 1 ? 'primary' : 'danger'" size="small" v-if="row.gender">
- {{ row.gender === 1 ? '男' : '女' }}
- </el-tag>
- <span v-else>-</span>
- </template>
- </el-table-column>
- <el-table-column prop="age" label="年龄" width="65" align="center">
- <template #default="{ row }">{{ row.age != null ? row.age + '岁' : '-' }}</template>
- </el-table-column>
- <el-table-column prop="phone" label="手机号" width="130" />
- <el-table-column prop="userType" label="用户类别" width="95" align="center">
- <template #default="{ row }">
- <el-tag :type="userTypeTagType(row.userType)" size="small" v-if="row.userType">
- {{ userTypeText(row.userType) }}
- </el-tag>
- <span v-else>-</span>
- </template>
- </el-table-column>
- <el-table-column label="所在地区" min-width="180">
- <template #default="{ row }">
- <span v-if="row.provinceCode">{{ formatRegion(row) }}</span>
- <span v-else class="text-gray">-</span>
- </template>
- </el-table-column>
- <el-table-column prop="address" label="详细地址" min-width="160" show-overflow-tooltip>
- <template #default="{ row }">
- <span v-if="row.address">{{ row.address }}</span>
- <span v-else class="text-gray">-</span>
- </template>
- </el-table-column>
- <el-table-column prop="deviceCodes" label="设备编号" min-width="140">
- <template #default="{ row }">
- <span v-if="row.deviceCodes">{{ row.deviceCodes }}</span>
- <span v-else class="text-gray">-</span>
- </template>
- </el-table-column>
- <el-table-column prop="shoulderWidth" label="肩宽(cm)" width="90" align="center">
- <template #default="{ row }">{{ row.shoulderWidth ?? '-' }}</template>
- </el-table-column>
- <el-table-column prop="bodyHeight" label="身长(cm)" width="90" align="center">
- <template #default="{ row }">{{ row.bodyHeight ?? '-' }}</template>
- </el-table-column>
- <el-table-column prop="spineLength" label="脊柱长(cm)" width="100" align="center">
- <template #default="{ row }">{{ row.spineLength ?? '-' }}</template>
- </el-table-column>
- <el-table-column prop="height" label="身高(cm)" width="90" align="center">
- <template #default="{ row }">{{ row.height ?? '-' }}</template>
- </el-table-column>
- <el-table-column prop="weight" label="体重(kg)" width="90" align="center">
- <template #default="{ row }">{{ row.weight ?? '-' }}</template>
- </el-table-column>
- <el-table-column label="穴位表" width="90" align="center">
- <template #default="{ row }">
- <el-tag v-if="row.acupointTableId" type="success" size="small">已生成</el-tag>
- <el-tag v-else type="info" size="small">未生成</el-tag>
- </template>
- </el-table-column>
- <el-table-column prop="createTime" label="注册时间" width="170" :formatter="dateTimeFormatter" />
- <el-table-column label="操作" width="200" align="center" fixed="right">
- <template #default="{ row }">
- <el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
- <el-button size="small" type="success" link @click="handleDeviceManager(row)">设备管理</el-button>
- <el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
- </template>
- </el-table-column>
- </el-table>
- <!-- 分页 -->
- <el-pagination
- v-model:current-page="queryForm.pageNum"
- v-model:page-size="queryForm.pageSize"
- :page-sizes="[10, 20, 50]"
- :total="total"
- layout="total, sizes, prev, pager, next, jumper"
- class="pagination"
- @size-change="loadData"
- @current-change="loadData"
- />
- </el-card>
- <!-- 新增/编辑弹窗 -->
- <el-dialog
- v-model="dialogVisible"
- :title="dialogTitle"
- width="680px"
- destroy-on-close
- >
- <el-form :model="formData" :rules="formRules" ref="formRef" label-width="100px">
- <el-row :gutter="16">
- <el-col :span="12">
- <el-form-item label="姓名" prop="name">
- <el-input v-model="formData.name" placeholder="请输入姓名" maxlength="50" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="性别" prop="gender">
- <el-radio-group v-model="formData.gender">
- <el-radio :label="1">男</el-radio>
- <el-radio :label="2">女</el-radio>
- </el-radio-group>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="年龄" prop="age">
- <el-input-number v-model="formData.age" :min="1" :max="120" style="width:100%" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="手机号" prop="phone">
- <el-input v-model="formData.phone" placeholder="请输入手机号" maxlength="11" :disabled="!!formData.id" />
- </el-form-item>
- </el-col>
- <el-col v-if="formData.id" :span="12">
- <el-form-item label="用户类别" prop="userType">
- <el-select v-model="formData.userType" placeholder="请选择用户类别" style="width:100%">
- <el-option
- v-for="item in userCategoryOptions"
- :key="item.code"
- :label="item.name"
- :value="item.code"
- />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="所在地区" prop="regionCodes">
- <el-cascader
- v-model="formData.regionCodes"
- :options="regionOptions"
- :props="regionCascaderProps"
- placeholder="请选择省/市/区"
- clearable
- filterable
- style="width:100%"
- />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="详细地址" prop="address">
- <el-input v-model="formData.address" placeholder="请输入详细地址" maxlength="200" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="肩宽(cm)" prop="shoulderWidth">
- <el-input-number v-model="formData.shoulderWidth" :min="0" :precision="1" style="width:100%" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="身长(cm)" prop="bodyHeight">
- <el-input-number v-model="formData.bodyHeight" :min="0" :precision="1" style="width:100%" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="脊柱长(cm)" prop="spineLength">
- <el-input-number v-model="formData.spineLength" :step="0.1" :precision="1" style="width:100%" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="身高(cm)" prop="height">
- <el-input-number v-model="formData.height" :min="0" :precision="1" style="width:100%" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="体重(kg)" prop="weight">
- <el-input-number v-model="formData.weight" :min="0" :precision="1" style="width:100%" />
- </el-form-item>
- </el-col>
- </el-row>
- <el-divider content-position="left"><span class="divider-text">指宽(选填,提高穴位精度)</span></el-divider>
- <el-row :gutter="16">
- <el-col :span="8">
- <el-form-item label="1寸(cm)" prop="fingerWidth1" label-width="70px">
- <el-input-number
- v-model="formData.fingerWidth1"
- :min="formData.gender ? fingerLimits.fw1.min : 0"
- :max="formData.gender ? fingerLimits.fw1.max : 99"
- :step="0.01"
- :precision="2"
- controls-position="right"
- style="width:100%"
- />
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="1.5寸(cm)" prop="fingerWidth15" label-width="75px">
- <el-input-number
- v-model="formData.fingerWidth15"
- :min="formData.gender ? fingerLimits.fw15.min : 0"
- :max="formData.gender ? fingerLimits.fw15.max : 99"
- :step="0.01"
- :precision="2"
- controls-position="right"
- style="width:100%"
- />
- </el-form-item>
- </el-col>
- <el-col :span="8">
- <el-form-item label="3寸(cm)" prop="fingerWidth3" label-width="70px">
- <el-input-number
- v-model="formData.fingerWidth3"
- :min="formData.gender ? fingerLimits.fw3.min : 0"
- :max="formData.gender ? fingerLimits.fw3.max : 99"
- :step="0.01"
- :precision="2"
- controls-position="right"
- style="width:100%"
- />
- </el-form-item>
- </el-col>
- </el-row>
- </el-form>
- <template #footer>
- <el-button @click="dialogVisible = false">取消</el-button>
- <el-button
- v-if="formData.id"
- type="warning"
- :loading="generateLoading"
- @click="handleGenerateAcupoint"
- >生成用户穴位</el-button>
- <el-button type="primary" :loading="submitLoading" @click="handleSubmit">保存</el-button>
- </template>
- </el-dialog>
- </div>
- </template>
- <script setup>
- import { ref, reactive, onMounted, computed, watch } from 'vue'
- import { ElMessage, ElMessageBox } from 'element-plus'
- import { useRouter } from 'vue-router'
- import { Search, Refresh, Plus, Delete } from '@element-plus/icons-vue'
- import { regionData, codeToText } from 'element-china-area-data'
- import ExcelExport from '@/components/ExcelExport.vue'
- import ExcelImport from '@/components/ExcelImport.vue'
- import { dateTimeFormatter } from '@/utils/date'
- import {
- getProfilePage,
- addProfile,
- updateProfile,
- deleteProfile,
- batchDeleteProfile,
- batchImportProfile,
- generateUserAcupoint
- } from '@/api/user'
- import { getUserCategoryList } from '@/api/userCategory'
- const router = useRouter()
- // ======================== 列表查询 ========================
- const queryForm = reactive({
- keyword: '',
- deviceCode: '',
- gender: null,
- userType: null,
- provinceCode: '',
- cityCode: '',
- districtCode: '',
- dateRange: null,
- acupointStatus: null,
- pageNum: 1,
- pageSize: 10
- })
- const tableData = ref([])
- const total = ref(0)
- const loading = ref(false)
- const selectedIds = ref([])
- // Excel 导出列配置
- const exportHeaders = [
- { label: '用户ID', prop: 'id' },
- { label: '姓名', prop: 'name' },
- { label: '性别', prop: 'genderText' },
- { label: '年龄', prop: 'age' },
- { label: '手机号', prop: 'phone' },
- { label: '用户类别', prop: 'userTypeLabel' },
- { label: '所在地区', prop: 'regionText' },
- { label: '详细地址', prop: 'address' },
- { label: '设备编号', prop: 'deviceCodes' },
- { label: '肩宽(cm)', prop: 'shoulderWidth' },
- { label: '身长(cm)', prop: 'bodyHeight' },
- { label: '脊柱长度(cm)', prop: 'spineLength' },
- { label: '身高(cm)', prop: 'height' },
- { label: '体重(kg)', prop: 'weight' },
- { label: '穴位表', prop: 'acupointText' },
- { label: '注册时间', prop: 'createTime' }
- ]
- // 中国三级行政区划数据(省/市/区)
- const regionOptions = regionData
- const regionCascaderProps = { value: 'value', label: 'label', children: 'children' }
- // 查询区域:省列表、联动的市/区列表
- const provinceList = computed(() => regionData.map(p => ({ value: p.value, label: p.label })))
- // 预计算“全国所有市”与“全国所有区”(模块级常量,仅计算一次),
- // 避免清空上级时 computed 重复重建上千条数组导致卡顿
- const ALL_CITIES = []
- const ALL_DISTRICTS = []
- regionData.forEach(p => {
- ;(p.children || []).forEach(c => {
- ALL_CITIES.push({ value: c.value, label: `${p.label} / ${c.label}` })
- ;(c.children || []).forEach(d => {
- ALL_DISTRICTS.push({ value: d.value, label: `${p.label} / ${c.label} / ${d.label}` })
- })
- })
- })
- /** 市列表:选了省则返回该省市;未选省则返回全国所有市(加上省名以便识别) */
- const queryCityList = computed(() => {
- if (queryForm.provinceCode) {
- const prov = regionData.find(p => p.value === queryForm.provinceCode)
- return (prov?.children || []).map(c => ({ value: c.value, label: c.label }))
- }
- return ALL_CITIES
- })
- /** 区/县列表:选了市则返回该市下区;只选省返回该省下区;都未选返回全国区(带上上级以便识别) */
- const queryDistrictList = computed(() => {
- if (queryForm.cityCode) {
- for (const p of regionData) {
- const city = (p.children || []).find(c => c.value === queryForm.cityCode)
- if (city) {
- return (city.children || []).map(d => ({ value: d.value, label: d.label }))
- }
- }
- return []
- }
- if (queryForm.provinceCode) {
- const prov = regionData.find(p => p.value === queryForm.provinceCode)
- const all = []
- ;(prov?.children || []).forEach(c => {
- (c.children || []).forEach(d => {
- all.push({ value: d.value, label: `${c.label} / ${d.label}` })
- })
- })
- return all
- }
- return ALL_DISTRICTS
- })
- /** 选了省:如果已选的市/区不属于该省则清空 */
- function onQueryProvinceChange(val) {
- if (!val) {
- return
- }
- const prov = regionData.find(p => p.value === val)
- const cities = prov?.children || []
- if (queryForm.cityCode && !cities.find(c => c.value === queryForm.cityCode)) {
- queryForm.cityCode = ''
- queryForm.districtCode = ''
- } else if (queryForm.districtCode) {
- const city = cities.find(c => c.value === queryForm.cityCode)
- if (city && !(city.children || []).find(d => d.value === queryForm.districtCode)) {
- queryForm.districtCode = ''
- }
- }
- }
- /** 选了市:如果已选的区不属于该市则清空 */
- function onQueryCityChange(val) {
- if (!val) {
- return
- }
- for (const p of regionData) {
- const city = (p.children || []).find(c => c.value === val)
- if (city) {
- if (queryForm.districtCode && !(city.children || []).find(d => d.value === queryForm.districtCode)) {
- queryForm.districtCode = ''
- }
- return
- }
- }
- }
- /** 用户类别选项(从后台 user_category 动态加载) */
- const userCategoryOptions = ref([])
- /** 用户类别文本(优先从动态加载的类别表查) */
- function userTypeText(type) {
- if (type == null) return '-'
- const item = userCategoryOptions.value.find(c => c.code === type)
- return item ? item.name : '-'
- }
- /** 用户类别标签颜色(优先使用后台配置的 tagType) */
- function userTypeTagType(type) {
- if (type == null) return 'info'
- const item = userCategoryOptions.value.find(c => c.code === type)
- return item?.tagType || 'primary'
- }
- /** 根据行政区划编码还原为 “省/市/区” 文本 */
- function formatRegion(row) {
- const parts = [row.provinceCode, row.cityCode, row.districtCode]
- .filter(c => !!c)
- .map(c => codeToText[c] || '')
- .filter(t => !!t)
- return parts.join(' / ')
- }
- async function loadData() {
- loading.value = true
- try {
- const params = {
- keyword: queryForm.keyword || undefined,
- deviceCode: queryForm.deviceCode || undefined,
- gender: queryForm.gender,
- userType: queryForm.userType,
- provinceCode: queryForm.provinceCode || undefined,
- cityCode: queryForm.cityCode || undefined,
- districtCode: queryForm.districtCode || undefined,
- startTime: queryForm.dateRange?.[0] || undefined,
- endTime: queryForm.dateRange?.[1] || undefined,
- acupointStatus: queryForm.acupointStatus,
- pageNum: queryForm.pageNum,
- pageSize: queryForm.pageSize
- }
- const res = await getProfilePage(params)
- // 附加显示文本字段(用于 Excel 导出)
- const records = (res.data?.records || []).map(r => ({
- ...r,
- genderText: r.gender === 1 ? '男' : r.gender === 2 ? '女' : '-',
- userTypeLabel: userTypeText(r.userType),
- regionText: formatRegion(r),
- acupointText: r.acupointTableId ? '已生成' : '未生成'
- }))
- tableData.value = records
- total.value = res.data?.total || 0
- } finally {
- loading.value = false
- }
- }
- function handleSearch() {
- queryForm.pageNum = 1
- loadData()
- }
- function handleReset() {
- queryForm.keyword = ''
- queryForm.deviceCode = ''
- queryForm.gender = null
- queryForm.userType = null
- queryForm.provinceCode = ''
- queryForm.cityCode = ''
- queryForm.districtCode = ''
- queryForm.dateRange = null
- queryForm.acupointStatus = null
- queryForm.pageNum = 1
- loadData()
- }
- function handleSelectionChange(selection) {
- selectedIds.value = selection.map(r => r.id)
- }
- // ======================== 新增/编辑 ========================
- const dialogVisible = ref(false)
- const dialogTitle = ref('')
- const submitLoading = ref(false)
- const generateLoading = ref(false)
- const formRef = ref(null)
- const formData = reactive({
- id: null,
- name: '',
- gender: null,
- age: null,
- phone: '',
- userType: 1,
- regionCodes: [],
- address: '',
- shoulderWidth: null,
- bodyHeight: null,
- spineLength: null,
- height: null,
- weight: null,
- fingerWidth1: null,
- fingerWidth15: null,
- fingerWidth3: null
- })
- const SPINE_LENGTH_MIN = 30
- const SPINE_LENGTH_MAX = 60
- // 各指宽字段在不同性别下的有效范围
- const fingerLimits = computed(() => {
- const male = formData.gender === 1
- return {
- fw1: { label: '1寸', min: male ? 1.5 : 1.3, max: male ? 2.8 : 2.4 },
- fw15: { label: '1.5寸', min: male ? 2.2 : 2.0, max: male ? 4.0 : 3.5 },
- fw3: { label: '3寸', min: male ? 4.5 : 4.0, max: male ? 8.0 : 7.0 }
- }
- })
- /**
- * 生成指宽字段的自定义 validator(选填,填了才按性别范围校验)
- * @param {string} field fw1 | fw15 | fw3
- */
- function makeFingerValidator(field) {
- return (rule, value, callback) => {
- if (value === null || value === undefined) {
- return callback()
- }
- if (!formData.gender) {
- return callback(new Error('请先选择性别再填写指宽'))
- }
- const { label, min, max } = fingerLimits.value[field]
- const genderText = formData.gender === 1 ? '男性' : '女性'
- if (value < min || value > max) {
- return callback(new Error(`${genderText}${label}指宽应在 ${min}~${max} cm 之间`))
- }
- callback()
- }
- }
- function validateSpineLength(rule, value, callback) {
- if (value === null || value === undefined || value === '') {
- return callback(new Error('请输入C7-S4脊柱长度'))
- }
- const numericValue = Number(value)
- if (!Number.isFinite(numericValue)) {
- return callback(new Error('脊柱长度格式不正确'))
- }
- if (numericValue < SPINE_LENGTH_MIN || numericValue > SPINE_LENGTH_MAX) {
- return callback(new Error(`C7-S4脊柱长度需在${SPINE_LENGTH_MIN}-${SPINE_LENGTH_MAX}cm之间`))
- }
- callback()
- }
- const formRules = {
- name: [
- { required: true, message: '请输入姓名', trigger: 'blur' },
- { max: 50, message: '姓名不超过50个字符', trigger: 'blur' }
- ],
- gender: [{ required: true, message: '请选择性别', trigger: 'change' }],
- age: [{ required: true, message: '请输入年龄', trigger: 'blur' }],
- phone: [
- { required: true, message: '请输入手机号', trigger: 'blur' },
- { pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
- ],
- shoulderWidth: [{ required: true, message: '请输入肩宽', trigger: 'blur' }],
- bodyHeight: [{ required: true, message: '请输入身长', trigger: 'blur' }],
- spineLength: [{ required: true, message: '请输入C7-S4脊柱长度', trigger: 'blur' }],
- fingerWidth1: [{ validator: makeFingerValidator('fw1'), trigger: 'change' }],
- fingerWidth15: [{ validator: makeFingerValidator('fw15'), trigger: 'change' }],
- fingerWidth3: [{ validator: makeFingerValidator('fw3'), trigger: 'change' }],
- spineLength: [
- { required: true, message: '请输入C7-S4脊柱长度', trigger: 'blur' },
- { validator: validateSpineLength, trigger: 'change' }
- ]
- }
- function handleAdd() {
- Object.assign(formData, {
- id: null, name: '', gender: null, age: null, phone: '',
- userType: 1, regionCodes: [], address: '',
- shoulderWidth: null, bodyHeight: null, spineLength: null, height: null, weight: null,
- fingerWidth1: null, fingerWidth15: null, fingerWidth3: null
- })
- dialogTitle.value = '新增用户'
- dialogVisible.value = true
- }
- function handleEdit(row) {
- Object.assign(formData, {
- id: row.id,
- name: row.name,
- gender: row.gender,
- age: row.age,
- phone: row.phone,
- userType: row.userType ?? 1,
- regionCodes: [row.provinceCode, row.cityCode, row.districtCode].filter(c => !!c),
- address: row.address || '',
- shoulderWidth: row.shoulderWidth,
- bodyHeight: row.bodyHeight,
- spineLength: row.spineLength,
- height: row.height,
- weight: row.weight,
- fingerWidth1: row.fingerWidth1,
- fingerWidth15: row.fingerWidth15,
- fingerWidth3: row.fingerWidth3
- })
- dialogTitle.value = '编辑用户'
- dialogVisible.value = true
- }
- // 性别切换时重新校验指宽字段(避免已填值在新性别下超出范围却无提示)
- watch(() => formData.gender, () => {
- if (formRef.value) {
- formRef.value.validateField(['fingerWidth1', 'fingerWidth15', 'fingerWidth3'])
- }
- })
- async function handleSubmit() {
- await formRef.value.validate()
- submitLoading.value = true
- try {
- const codes = formData.regionCodes || []
- const payload = {
- ...formData,
- provinceCode: codes[0] || null,
- cityCode: codes[1] || null,
- districtCode: codes[2] || null
- }
- delete payload.regionCodes
- if (formData.id) {
- await updateProfile(payload)
- } else {
- await addProfile(payload)
- }
- ElMessage.success('操作成功')
- dialogVisible.value = false
- loadData()
- } finally {
- submitLoading.value = false
- }
- }
- // ======================== 删除 ========================
- async function handleDelete(row) {
- await ElMessageBox.confirm(`确认删除用户「${row.name}」吗?`, '提示', { type: 'warning' })
- await deleteProfile(row.id)
- ElMessage.success('删除成功')
- loadData()
- }
- async function handleBatchDelete() {
- await ElMessageBox.confirm(`确认批量删除选中的 ${selectedIds.value.length} 条记录吗?`, '提示', { type: 'warning' })
- await batchDeleteProfile(selectedIds.value)
- ElMessage.success('批量删除成功')
- selectedIds.value = []
- loadData()
- }
- // ======================== 导入 ========================
- async function handleImport(data) {
- // data 为解析后的数组,字段名需与 DTO 对应
- await batchImportProfile(data)
- }
- function onImportSuccess() {
- ElMessage.success('导入成功')
- loadData()
- }
- // ======================== 设备管理(跳转独立页面) ========================
- /**
- * 跳转到用户设备管理页面
- */
- function handleDeviceManager(row) {
- router.push({
- path: '/user/device',
- query: { userId: row.id, userName: row.name }
- })
- }
- // ======================== 生成用户穴位 ========================
- /**
- * 手动触发为当前编辑用户生成穴位坐标
- * 编辑弹窗中点击「生成用户穴位」按钮时调用
- */
- async function handleGenerateAcupoint() {
- if (!formData.id) {
- ElMessage.warning('请先保存用户后再生成穴位')
- return
- }
- generateLoading.value = true
- try {
- await generateUserAcupoint(formData.id)
- ElMessage.success('穴位坐标生成成功')
- loadData()
- } finally {
- generateLoading.value = false
- }
- }
- async function loadUserCategories() {
- try {
- const res = await getUserCategoryList()
- userCategoryOptions.value = res.data || []
- } catch (e) {
- userCategoryOptions.value = []
- }
- }
- onMounted(() => {
- loadUserCategories()
- loadData()
- })
- </script>
- <style lang="scss" scoped>
- .page-container {
- display: flex;
- flex-direction: column;
- gap: 16px;
- }
- .search-card {
- :deep(.el-card__body) {
- padding: 16px 20px 4px;
- }
- }
- .table-card {
- .card-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- .title {
- font-weight: 600;
- font-size: 15px;
- }
- .btn-group {
- display: flex;
- gap: 8px;
- align-items: center;
- }
- }
- }
- .pagination {
- margin-top: 16px;
- justify-content: flex-end;
- }
- .text-gray {
- color: #aaa;
- }
- .divider-text {
- font-size: 12px;
- color: #999;
- }
- </style>
|