Authored by Bad

fix

@@ -23,17 +23,17 @@ export interface ExercisesVO { @@ -23,17 +23,17 @@ export interface ExercisesVO {
23 // 创建/更新动作的请求参数结构 23 // 创建/更新动作的请求参数结构
24 export interface ExercisesSaveReqVO { 24 export interface ExercisesSaveReqVO {
25 id?: number 25 id?: number
26 - name: string  
27 - categoryId: number  
28 - equipmentId: number  
29 - exerciseType: number 26 + name?: string
  27 + categoryId?: number
  28 + equipmentId?: number
  29 + exerciseType?: number
30 urlImage?: string 30 urlImage?: string
31 - url3dAnimation: string  
32 - urlRealPerson: string  
33 - urlTutorial: string  
34 - stepDescription: string  
35 - primaryMuscles: ''  
36 - secondaryMuscles: '' 31 + url3dAnimation?: string
  32 + urlRealPerson?: string
  33 + urlTutorial?: string
  34 + stepDescription?: string
  35 + primaryMuscles?: string[]
  36 + secondaryMuscles?: string[]
37 } 37 }
38 38
39 // 分页/导出查询参数 39 // 分页/导出查询参数
1 <template> 1 <template>
2 - <!-- 直接用 defineModel 生成的 dialogVisible 做 v-model -->  
3 - <el-dialog v-model="dialogVisible" :title="title" width="800px" append-to-body>  
4 - <!-- 表单直接绑定 formDataModel(自动双向同步) -->  
5 - <el-form ref="formRef" :model="formDataModel" :rules="formRules" label-width="100px">  
6 - <el-form-item label="动作名称:" prop="name" label-suffix="*">  
7 - <el-input v-model="formDataModel.name" placeholder="请输入动作名称" maxlength="50" show-word-limit  
8 - style="width: 100%" /> 2 + <!-- 采用芋道全局封装的 Dialog 组件,内置滚动和自适应处理 -->
  3 + <Dialog v-model="dialogVisible" :title="dialogTitle" width="800px">
  4 + <el-form
  5 + ref="formRef"
  6 + v-loading="formLoading"
  7 + :model="formData"
  8 + :rules="formRules"
  9 + label-width="120px"
  10 + >
  11 + <el-form-item label="动作名称" prop="name">
  12 + <el-input
  13 + v-model="formData.name"
  14 + placeholder="请输入动作名称"
  15 + maxlength="50"
  16 + show-word-limit
  17 + />
9 </el-form-item> 18 </el-form-item>
10 - <el-form-item label="动作类型:" prop="exerciseType" label-suffix="*">  
11 - <el-select v-model="formDataModel.exerciseType" placeholder="请选择动作类型" style="width: 100%"> 19 +
  20 + <el-form-item label="动作类型" prop="exerciseType">
  21 + <el-select v-model="formData.exerciseType" placeholder="请选择动作类型" class="w-full">
12 <el-option label="无氧运动" :value="0" /> 22 <el-option label="无氧运动" :value="0" />
13 <el-option label="有氧运动" :value="1" /> 23 <el-option label="有氧运动" :value="1" />
14 <el-option label="计次训练" :value="2" /> 24 <el-option label="计次训练" :value="2" />
@@ -18,58 +28,77 @@ @@ -18,58 +28,77 @@
18 <el-option label="间歇训练" :value="6" /> 28 <el-option label="间歇训练" :value="6" />
19 </el-select> 29 </el-select>
20 </el-form-item> 30 </el-form-item>
21 - <!-- 部位分类:下拉选择框 -->  
22 - <el-form-item label="部位分类:" prop="categoryId" label-suffix="*">  
23 - <el-select v-model="formDataModel.categoryId" placeholder="请选择部位分类" style="width: 100%"  
24 - :loading="categoryList.length === 0">  
25 - <el-option v-for="item in categoryList" :key="item.id" :label="item.name" :value="Number(item.id)" /> 31 +
  32 + <el-form-item label="部位分类" prop="categoryId">
  33 + <el-select v-model="formData.categoryId" placeholder="请选择部位分类" class="w-full">
  34 + <el-option
  35 + v-for="item in categoryList"
  36 + :key="item.id"
  37 + :label="item.name"
  38 + :value="Number(item.id)"
  39 + />
26 </el-select> 40 </el-select>
27 </el-form-item> 41 </el-form-item>
28 - <el-form-item label="用具分类:" prop="equipmentId" label-suffix="*">  
29 - <el-select v-model="formDataModel.equipmentId" placeholder="请选择用具分类" style="width: 100%"> 42 +
  43 + <el-form-item label="用具分类" prop="equipmentId">
  44 + <el-select v-model="formData.equipmentId" placeholder="请选择用具分类" class="w-full">
30 <el-option v-for="item in toolList" :key="item.id" :label="item.name" :value="item.id" /> 45 <el-option v-for="item in toolList" :key="item.id" :label="item.name" :value="item.id" />
31 </el-select> 46 </el-select>
32 </el-form-item> 47 </el-form-item>
33 - <el-form-item label="主要训练部位:" prop="primaryMuscles" label-suffix="*">  
34 - <el-select v-model="formDataModel.primaryMuscles" multiple placeholder="请输入主要训练部位" style="width: 100%">  
35 - <el-option v-for="item in primaryMuscleOptions" :key="item.id" :label="item.name" :value="item.id" /> 48 +
  49 + <el-form-item label="主要训练部位" prop="primaryMuscles">
  50 + <el-select
  51 + v-model="formData.primaryMuscles"
  52 + multiple
  53 + placeholder="请选择主要训练部位"
  54 + class="w-full"
  55 + >
  56 + <el-option v-for="item in muscleList" :key="item.id" :label="item.name" :value="item.id" />
36 </el-select> 57 </el-select>
37 </el-form-item> 58 </el-form-item>
38 - <el-form-item label="次要训练部位:" prop="secondaryMuscles" label-suffix="*">  
39 - <el-select v-model="formDataModel.secondaryMuscles" multiple placeholder="请输入次要训练部位" style="width: 100%">  
40 - <el-option v-for="item in secondaryMuscleOptions" :key="item.id" :label="item.name" :value="item.id" /> 59 +
  60 + <el-form-item label="次要训练部位" prop="secondaryMuscles">
  61 + <el-select
  62 + v-model="formData.secondaryMuscles"
  63 + multiple
  64 + placeholder="请选择次要训练部位"
  65 + class="w-full"
  66 + >
  67 + <el-option v-for="item in muscleList" :key="item.id" :label="item.name" :value="item.id" />
41 </el-select> 68 </el-select>
42 </el-form-item> 69 </el-form-item>
43 - <!-- 锻炼部位图 -->  
44 - <el-form-item label="锻炼部位图" prop="urlImage" class="form-item-margin upload-img-item">  
45 - <UploadImg v-model="formDataModel.urlImage" /> 70 +
  71 + <el-form-item label="锻炼部位图" prop="urlImage">
  72 + <UploadImg v-model="formData.urlImage" />
46 </el-form-item> 73 </el-form-item>
47 - <el-form-item label="3D动画地址" prop="url3dAnimation" label-suffix="*" class="upload-img-item upload-3d-item">  
48 - <UploadImg v-model="formDataModel.url3dAnimation" /> 74 +
  75 + <el-form-item label="3D动画地址" prop="url3dAnimation">
  76 + <UploadImg v-model="formData.url3dAnimation" />
49 </el-form-item> 77 </el-form-item>
50 78
51 - <el-form-item label="真人演示地址" prop="urlRealPerson" label-suffix="*">  
52 - <UploadVideo v-model="formDataModel.urlRealPerson" /> 79 + <el-form-item label="真人演示地址" prop="urlRealPerson">
  80 + <UploadVideo v-model="formData.urlRealPerson" />
53 </el-form-item> 81 </el-form-item>
54 82
55 - <el-form-item label="教程地址" prop="urlTutorial" label-suffix="*">  
56 - <UploadVideo v-model="formDataModel.urlTutorial" /> 83 + <el-form-item label="教程地址" prop="urlTutorial">
  84 + <UploadVideo v-model="formData.urlTutorial" />
57 </el-form-item> 85 </el-form-item>
58 - <el-form-item label="动作步骤描述:" prop="stepDescription" label-suffix="*" class="desc-item">  
59 - <Editor v-model="formDataModel.stepDescription" placeholder="请输入动作步骤描述" /> 86 +
  87 + <el-form-item label="动作步骤描述" prop="stepDescription">
  88 + <Editor v-model="formData.stepDescription" placeholder="请输入动作步骤描述" />
60 </el-form-item> 89 </el-form-item>
61 </el-form> 90 </el-form>
  91 +
62 <template #footer> 92 <template #footer>
63 - <el-button @click="handleCancel">取消</el-button>  
64 - <el-button type="primary" :loading="formLoading" @click="handleSubmit">保存</el-button> 93 + <el-button :disabled="formLoading" type="primary" @click="submitForm">确定</el-button>
  94 + <el-button @click="dialogVisible = false">取消</el-button>
65 </template> 95 </template>
66 - </el-dialog> 96 + </Dialog>
67 </template> 97 </template>
68 98
69 -<script setup>  
70 -  
71 -import { ref, reactive, watch, onMounted, nextTick, computed } from 'vue'  
72 -import { ElMessage } from 'element-plus' 99 +<script setup lang="ts">
  100 +import { ref, reactive } from 'vue'
  101 +import type { FormInstance, FormRules } from 'element-plus'
73 import { MotionCategoryApi } from '@/api/store/training/partCategory' 102 import { MotionCategoryApi } from '@/api/store/training/partCategory'
74 import { MusclesApi } from '@/api/store/training/muscle' 103 import { MusclesApi } from '@/api/store/training/muscle'
75 import { EquipmentsApi } from '@/api/store/training/tool' 104 import { EquipmentsApi } from '@/api/store/training/tool'
@@ -77,204 +106,113 @@ import { Editor } from '@/components/Editor' @@ -77,204 +106,113 @@ import { Editor } from '@/components/Editor'
77 import UploadVideo from '@/components/UploadFile/src/UploadVideo.vue' 106 import UploadVideo from '@/components/UploadFile/src/UploadVideo.vue'
78 import { ExercisesApi } from '@/api/store/training/pose' 107 import { ExercisesApi } from '@/api/store/training/pose'
79 108
80 -// 1. 自定义 v-model:弹窗显隐(对应父组件 v-model:visible)  
81 -const dialogVisible = defineModel('visible', { type: Boolean, default: false })  
82 -  
83 -const defaultImg = 'https://fitness-hcxtec-bucket.oss-cn-shenzhen.aliyuncs.com/20260424/muscle_back_1777018976173.png'  
84 -  
85 -// 2. 自定义 v-model:表单数据(对应父组件 v-model:formData)  
86 -const formDataModel = defineModel('formData', {  
87 - type: Object, // 新增这一行  
88 - default: () => ({  
89 - id: undefined,  
90 - name: '',  
91 - categoryId: undefined,  
92 - equipmentId: undefined,  
93 - exerciseType: undefined,  
94 - // 封面图  
95 - urlImage: '',  
96 - url3dAnimation: '',  
97 - urlRealPerson: '',  
98 - urlTutorial: '',  
99 - stepDescription: '',  
100 - primaryMuscles: [],  
101 - secondaryMuscles: []  
102 - })  
103 -})  
104 -  
105 -const muscleList = ref([]) // 肌肉列表(弹窗选择用)  
106 -const categoryList = ref([]) // 部位分类列表(弹窗选择用)  
107 -const toolList = ref([]) // 用具列表(弹窗选择用)  
108 -  
109 -// 加这个监听,看是否收到父组件的 visible=true  
110 -watch(dialogVisible, async (newVal) => {  
111 - console.log('子组件收到 visible 变化:', newVal)  
112 - if (newVal) {  
113 - // 弹窗打开时,重新加载列表,确保选项数据存在  
114 - await Promise.all([  
115 - loadCategoryList(),  
116 - loadToolList(),  
117 - loadMuscleList()  
118 - ])  
119 - const form = formDataModel.value  
120 - // 调试打印  
121 - console.log('解析前-原始数据:', form)  
122 - console.log('解析前-primaryMuscles:', form.primaryMuscles, typeof form.primaryMuscles)  
123 - console.log('解析前-secondaryMuscles:', form.secondaryMuscles, typeof form.secondaryMuscles)  
124 - if (typeof form.primaryMuscles === 'string') {  
125 - try {  
126 - // 解析字符串 "[17, 16]" → 数组 [17, 16]  
127 - formDataModel.value.primaryMuscles = JSON.parse(form.primaryMuscles)  
128 - } catch (err) {  
129 - // 如果解析失败(比如空字符串、非法格式),就默认设为空数组  
130 - console.warn('primaryMuscles 解析失败,已重置为空数组', err)  
131 - formDataModel.value.primaryMuscles = []  
132 - }  
133 - }  
134 -  
135 - // 处理 secondaryMuscles(和上面逻辑一样)  
136 - if (typeof form.secondaryMuscles === 'string') {  
137 - try {  
138 - formDataModel.value.secondaryMuscles = JSON.parse(form.secondaryMuscles)  
139 - } catch (err) {  
140 - console.warn('secondaryMuscles 解析失败,已重置为空数组', err)  
141 - formDataModel.value.secondaryMuscles = []  
142 - }  
143 - }  
144 - // -------------------------------------------------------------  
145 -  
146 - // 解析完成后,再打印一次,确认变成了数组  
147 - console.log('解析后-primaryMuscles:', formDataModel.value.primaryMuscles, typeof formDataModel.value.primaryMuscles)  
148 - console.log('解析后-secondaryMuscles:', formDataModel.value.secondaryMuscles, typeof formDataModel.value.secondaryMuscles)  
149 -  
150 - // 列表加载完后,强制更新表单,触发el-select回显  
151 - nextTick(() => {  
152 - if (formRef.value) {  
153 - formRef.value.clearValidate()  
154 -  
155 -  
156 - }  
157 - })  
158 - }  
159 -})  
160 -  
161 -// 接收父组件传的标题(普通 prop)  
162 -const props = defineProps({  
163 - title: { type: String, required: true, default: '新增动作' } 109 +defineOptions({ name: 'ActionFormDialog' })
  110 +
  111 +const message = useMessage() // 引入芋道全局提示组件
  112 +
  113 +const dialogVisible = ref(false) // 弹窗可见性
  114 +const dialogTitle = ref('') // 弹窗标题
  115 +const formLoading = ref(false) // 遮罩状态
  116 +const formType = ref('') // 表单行为:'create' 或 'update'
  117 +
  118 +const formRef = ref<FormInstance>()
  119 +const formData = ref<any>({
  120 + id: undefined,
  121 + name: '',
  122 + categoryId: undefined,
  123 + equipmentId: undefined,
  124 + exerciseType: undefined,
  125 + urlImage: '',
  126 + url3dAnimation: '',
  127 + urlRealPerson: '',
  128 + urlTutorial: '',
  129 + stepDescription: '',
  130 + primaryMuscles: [],
  131 + secondaryMuscles: []
164 }) 132 })
165 133
166 -// 只定义提交事件(取消/关闭由 v-model 自动处理)  
167 -const emit = defineEmits(['submit'])  
168 -  
169 -// 表单相关  
170 -const formLoading = ref(false)  
171 -const formRef = ref() 134 +// 基础下拉选项数据集
  135 +const muscleList = ref<any[]>([])
  136 +const categoryList = ref<any[]>([])
  137 +const toolList = ref<any[]>([])
172 138
173 -// 表单校验规则(不变)  
174 -const formRules = reactive({ 139 +const formRules = reactive<FormRules>({
175 name: [{ required: true, message: '请输入动作名称', trigger: 'blur' }], 140 name: [{ required: true, message: '请输入动作名称', trigger: 'blur' }],
176 categoryId: [{ required: true, message: '请选择部位分类', trigger: 'change' }], 141 categoryId: [{ required: true, message: '请选择部位分类', trigger: 'change' }],
177 equipmentId: [{ required: true, message: '请选择用具分类', trigger: 'change' }], 142 equipmentId: [{ required: true, message: '请选择用具分类', trigger: 'change' }],
178 exerciseType: [{ required: true, message: '请选择动作类型', trigger: 'change' }], 143 exerciseType: [{ required: true, message: '请选择动作类型', trigger: 'change' }],
179 - primaryMuscles: [{ required: true, message: '请输入主要训练部位', trigger: 'change' }],  
180 - secondaryMuscles: [{ required: true, message: '请输入次要训练部位', trigger: 'change' }],  
181 - urlImage: [{ required: false, message: '请上传图片', trigger: 'blur' }], 144 + primaryMuscles: [{ required: true, message: '请选择主要训练部位', trigger: 'change' }],
  145 + secondaryMuscles: [{ required: true, message: '请选择次要训练部位', trigger: 'change' }],
182 urlRealPerson: [{ required: true, message: '请上传真人演示地址', trigger: 'blur' }], 146 urlRealPerson: [{ required: true, message: '请上传真人演示地址', trigger: 'blur' }],
183 urlTutorial: [{ required: true, message: '请上传教程地址', trigger: 'blur' }], 147 urlTutorial: [{ required: true, message: '请上传教程地址', trigger: 'blur' }],
184 url3dAnimation: [{ required: true, message: '请上传3D动图', trigger: 'blur' }], 148 url3dAnimation: [{ required: true, message: '请上传3D动图', trigger: 'blur' }],
185 stepDescription: [{ required: true, message: '请输入动作步骤', trigger: 'blur' }], 149 stepDescription: [{ required: true, message: '请输入动作步骤', trigger: 'blur' }],
186 }) 150 })
187 151
188 -// 次要部位选项 = 全部肌肉 - 已选主要部位  
189 -const secondaryMuscleOptions = computed(() => {  
190 - const primaryIds = formDataModel.value.primaryMuscles || []  
191 - return muscleList.value.filter(item => !primaryIds.includes(item.id))  
192 -})  
193 -  
194 -// 主要部位选项 = 全部肌肉 - 已选次要部位  
195 -const primaryMuscleOptions = computed(() => {  
196 - const secondaryIds = formDataModel.value.secondaryMuscles || []  
197 - return muscleList.value.filter(item => !secondaryIds.includes(item.id))  
198 -}) 152 +// 安全解析 JSON,避免前端异常崩溃
  153 +const safeParseArray = (val: any): any[] => {
  154 + if (!val) return []
  155 + if (Array.isArray(val)) return val
  156 + try {
  157 + const parsed = typeof val === 'string' ? JSON.parse(val) : val
  158 + return Array.isArray(parsed) ? parsed : []
  159 + } catch {
  160 + return []
  161 + }
  162 +}
199 163
  164 +// 异步按需加载字典数据
  165 +const loadOptions = async () => {
  166 + if (muscleList.value.length === 0) {
  167 + const res = await MusclesApi.getMusclesPage({ pageNo: 1, pageSize: 100, name: '' })
  168 + muscleList.value = res.data?.list || res.list || []
  169 + }
  170 + if (categoryList.value.length === 0) {
  171 + const res = await MotionCategoryApi.getMotionCategoryPage({ pageNo: 1, pageSize: 100, name: '' })
  172 + categoryList.value = res.data?.list || res.list || []
  173 + }
  174 + if (toolList.value.length === 0) {
  175 + const res = await EquipmentsApi.getEquipmentsPage({ pageNo: 1, pageSize: 100, name: '' })
  176 + toolList.value = res.data?.list || res.list || []
  177 + }
  178 +}
200 179
201 -// 取消按钮:直接关闭弹窗(自动同步给父组件)  
202 -const handleCancel = () => { 180 +// 暴露给父组件调用的 open 方法(标准芋道设计模式)
  181 +const open = async (type: string, id?: number) => {
  182 + dialogVisible.value = true
  183 + dialogTitle.value = type === 'create' ? '新增动作' : '编辑动作'
  184 + formType.value = type
203 resetForm() 185 resetForm()
204 - dialogVisible.value = false // 自动触发 update:visible,父组件数据同步更新  
205 -}  
206 186
207 -// 提交表单:只需要把数据传给父组件  
208 -const handleSubmit = async () => {  
209 - if (!formRef.value) return  
210 - try {  
211 - console.log('123456'); 187 + // 1. 加载分类等选择数据
  188 + await loadOptions()
212 189
213 - await formRef.value.validate()  
214 - console.log('123456'); 190 + // 2. 如果是编辑动作,异步拉取最新后端行数据并格式化回显
  191 + if (id) {
215 formLoading.value = true 192 formLoading.value = true
216 - console.log('原始primaryMuscles:', formDataModel.value.primaryMuscles)  
217 - console.log('原始secondaryMuscles:', formDataModel.value.secondaryMuscles)  
218 -  
219 - const form = formDataModel.value  
220 - const submitData = {  
221 - ...formDataModel.value,  
222 - urlImage: form.urlImage || defaultImg,  
223 - // primaryMuscles: formDataModel.value.primaryMuscles || [],  
224 - // secondaryMuscles: formDataModel.value.secondaryMuscles || [],  
225 - primaryMuscles: JSON.stringify(formDataModel.value.primaryMuscles || []),  
226 - secondaryMuscles: JSON.stringify(formDataModel.value.secondaryMuscles || []),  
227 - }  
228 - console.log('------------------');  
229 -  
230 - if (submitData.id) {  
231 - await ExercisesApi.updateExercises(submitData) // 编辑:调用更新接口  
232 - console.log('动作编辑接口数据', submitData)  
233 - ElMessage.success('动作编辑成功!')  
234 - } else {  
235 - console.log('动作新增接口数据', submitData)  
236 - await ExercisesApi.addExercises(submitData) // 新增:调用创建接口  
237 - ElMessage.success('动作新增成功!') 193 + try {
  194 + const data = await ExercisesApi.getExercises(id)
  195 + console.log(data,"getExercises");
  196 + // 处理回显:将后端保存的 String (如 "[1,2]") 格式化为 el-select 所需的 Array
  197 + data.primaryMuscles = safeParseArray(JSON.parse(data.primaryMuscles || '[]'))
  198 + data.secondaryMuscles = safeParseArray(JSON.parse(data.secondaryMuscles || '[]'))
  199 + formData.value = data
  200 + } catch (err) {
  201 + console.error(err)
  202 + } finally {
  203 + formLoading.value = false
238 } 204 }
239 - // 触发提交事件,数据已通过 v-model 同步,直接传即可  
240 - emit('submit')  
241 - } catch (error) {  
242 - ElMessage.error('表单校验失败,请检查必填项')  
243 - } finally {  
244 - formLoading.value = false  
245 - // resetForm()  
246 } 205 }
247 } 206 }
248 207
249 -  
250 -// const handleFormSubmit = async (submitData) => {  
251 -// try {  
252 -// // ✅ 直接传数组,axios 自动转成 JSON 字符串  
253 -// const formatData = {  
254 -// ...submitData,  
255 -// primaryMuscles: JSON.stringify(submitData.primaryMuscles || []),  
256 -// secondaryMuscles: JSON.stringify(submitData.secondaryMuscles || [])  
257 -// }  
258 -// console.log('提交的数据', formatData);  
259 -  
260 -// // 关闭弹窗  
261 -// handleDialogClose()  
262 -// // 重新查询数据  
263 -// handleQuery()  
264 -// } catch (err) {  
265 -// console.error('提交失败:', err)  
266 -// ElMessage.error(submitData.id ? '更新失败,请稍后重试' : '新增失败,请稍后重试')  
267 -// }  
268 -// }  
269 -  
270 -// 重置表单(只操作本地 model,自动同步父组件) 208 +// 重置表单值
271 const resetForm = () => { 209 const resetForm = () => {
272 - Object.assign(formDataModel.value, { 210 + formData.value = {
273 id: undefined, 211 id: undefined,
274 name: '', 212 name: '',
275 categoryId: undefined, 213 categoryId: undefined,
276 equipmentId: undefined, 214 equipmentId: undefined,
277 - exerciseType: 0, 215 + exerciseType: undefined,
278 urlImage: '', 216 urlImage: '',
279 url3dAnimation: '', 217 url3dAnimation: '',
280 urlRealPerson: '', 218 urlRealPerson: '',
@@ -282,131 +220,53 @@ const resetForm = () => { @@ -282,131 +220,53 @@ const resetForm = () => {
282 stepDescription: '', 220 stepDescription: '',
283 primaryMuscles: [], 221 primaryMuscles: [],
284 secondaryMuscles: [] 222 secondaryMuscles: []
285 - }) 223 + }
286 formRef.value?.resetFields() 224 formRef.value?.resetFields()
287 } 225 }
288 226
289 -//=====================加载数据====================  
290 -//加载肌肉列表(弹窗选择肌肉)  
291 -const loadMuscleList = async () => {  
292 - try {  
293 - const res = await MusclesApi.getMusclesPage({ pageNo: '1', pageSize: '100', name: '' })  
294 - const data = res.data || res  
295 - muscleList.value = data.list || []  
296 - } catch (err) {  
297 - console.error('加载肌肉列表失败:', err)  
298 - ElMessage.error('加载肌肉列表失败')  
299 - }  
300 -} 227 +// 暴露 open 方法供父组件利用 ref 调用
  228 +defineExpose({ open })
301 229
302 -/**  
303 - * 加载部位分类列表(弹窗选择部位分类)  
304 - */  
305 -const loadCategoryList = async () => {  
306 - try {  
307 - const res = await MotionCategoryApi.getMotionCategoryPage({ pageNo: '1', pageSize: '100', name: '' })  
308 - const data = res.data || res  
309 - categoryList.value = data.list || []  
310 - console.log('部位分类:', categoryList.value)  
311 - } catch (err) {  
312 - console.error('加载部位分类失败:', err)  
313 - ElMessage.error('加载部位分类失败')  
314 - }  
315 -} 230 +// 定义 success 回调事件,向父页面告知操作结果
  231 +const emit = defineEmits(['success'])
316 232
317 -const loadToolList = async () => { 233 +// 表单提交保存
  234 +const submitForm = async () => {
  235 + if (!formRef.value) return
  236 + const valid = await formRef.value.validate()
  237 + if (!valid) return
  238 +
  239 + formLoading.value = true
318 try { 240 try {
319 - const res = await EquipmentsApi.getEquipmentsPage({ pageNo: '1', pageSize: '100', name: '' })  
320 - const data = res.list || []  
321 - toolList.value = data  
322 - console.log('用具分类:', toolList.value) 241 + // 组装提交参数:将选中数组 JSON 序列化为后端所需的 String 格式
  242 + const submitData = {
  243 + ...formData.value,
  244 + primaryMuscles: JSON.stringify(formData.value.primaryMuscles || []),
  245 + secondaryMuscles: JSON.stringify(formData.value.secondaryMuscles || [])
  246 + }
  247 +
  248 + console.log(submitData,"submitData");
  249 +
  250 + if (formType.value === 'create') {
  251 + await ExercisesApi.addExercises(submitData)
  252 + message.success('新增成功')
  253 + } else {
  254 + await ExercisesApi.updateExercises(submitData)
  255 + message.success('修改成功')
  256 + }
  257 + dialogVisible.value = false
  258 + emit('success') // 触发父页面重新加载列表
323 } catch (err) { 259 } catch (err) {
324 - console.error('加载用具列表失败:', err)  
325 - ElMessage.error('加载用具列表失败') 260 + console.error(err)
  261 + } finally {
  262 + formLoading.value = false
326 } 263 }
327 } 264 }
328 -  
329 -onMounted(() => {  
330 - loadMuscleList()  
331 - loadCategoryList()  
332 - loadToolList()  
333 -})  
334 -  
335 -// 暴露重置方法  
336 -defineExpose({ resetForm })  
337 </script> 265 </script>
338 266
339 <style scoped> 267 <style scoped>
340 -/* 1. 强制表单使用 flex 布局,label 和内容水平并排 */  
341 -:deep(.el-form-item) {  
342 - display: flex !important;  
343 - align-items: flex-start !important;  
344 -}  
345 -  
346 -/* 2. 统一设置 label 宽度和对齐方式,同时给右侧留固定间距 */  
347 -:deep(.el-form-item__label) {  
348 - float: none !important;  
349 - width: 120px !important;  
350 - padding: 0 !important;  
351 - margin-right: 20px !important;  
352 - line-height: 32px;  
353 - text-align: right;  
354 - white-space: nowrap;  
355 -}  
356 -  
357 -/* 3. 让内容区域自动占满剩余宽度 */  
358 -:deep(.el-form-item__content) {  
359 - margin-left: 0 !important;  
360 - line-height: 32px;  
361 - flex: 1;  
362 -}  
363 -  
364 -/* 4. 给所有上传类表单项单独适配垂直对齐 */  
365 -:deep(.el-form-item.upload-img-item .el-form-item__label) {  
366 - /* 上传组件高度高,让 label 顶部对齐 */  
367 - line-height: 120px !important;  
368 -  
369 -}  
370 -  
371 -/* .upload-img-item {  
372 - margin-bottom: 20rpx;  
373 -} */  
374 -  
375 -/* 单独给封面图增加上下间距 */  
376 -.form-item-margin {  
377 - margin: 30px 5px !important;  
378 -}  
379 -  
380 -.upload-3d-item {  
381 - margin-top: 20px !important;  
382 -}  
383 -  
384 -.upload-box {  
385 - display: flex;  
386 - width: 120px;  
387 - height: 120px;  
388 - cursor: pointer;  
389 - background: #f7f8fa;  
390 - border: 1px dashed #dcdfe6;  
391 - border-radius: 4px;  
392 - flex-direction: column;  
393 - align-items: center;  
394 - justify-content: center;  
395 -}  
396 -  
397 -.plus {  
398 - margin-bottom: 4px;  
399 - font-size: 28px;  
400 - color: #999;  
401 -}  
402 -  
403 -.tip {  
404 - font-size: 12px;  
405 - color: #999;  
406 -}  
407 -  
408 -.url-text {  
409 - margin-left: 10px;  
410 - line-height: 120px; 268 +/* 使用简单的全局通用间距配置,移除过多深层样式覆盖 */
  269 +.w-full {
  270 + width: 100%;
411 } 271 }
412 </style> 272 </style>
1 <template> 1 <template>
2 - <!-- 整个页面容器 -->  
3 <div class="page-container"> 2 <div class="page-container">
4 - <!-- 搜索栏区域:匹配参考图布局 --> 3 + <!-- 搜索栏区域:芋道行内标准表单 -->
5 <ContentWrap> 4 <ContentWrap>
6 - <div class="search-bar-wrapper">  
7 - <div class="search-row">  
8 - <span class="search-label">动作名称</span>  
9 - <el-input v-model="searchForm.name" placeholder="请输入" class="search-input" clearable 5 + <el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="68px" class="-mb-15px">
  6 + <el-form-item label="动作名称" prop="name">
  7 + <el-input v-model="queryParams.name" placeholder="请输入动作名称" clearable class="!w-240px"
10 @keyup.enter="handleQuery" /> 8 @keyup.enter="handleQuery" />
11 - <el-button type="primary" class="search-btn" @click="handleQuery">  
12 - <el-icon>  
13 - <Search />  
14 - </el-icon>  
15 - 搜索 9 + </el-form-item>
  10 + <el-form-item>
  11 + <el-button @click="handleQuery">
  12 + <Icon icon="ep:search" class="mr-5px" /> 搜索
16 </el-button> 13 </el-button>
17 - <el-button class="reset-btn" @click="resetQuery">  
18 - <el-icon>  
19 - <Refresh />  
20 - </el-icon>  
21 - 重置 14 + <el-button @click="resetQuery">
  15 + <Icon icon="ep:refresh" class="mr-5px" /> 重置
22 </el-button> 16 </el-button>
23 - <el-button class="add-btn" @click="handleAdd">  
24 - <el-icon>  
25 - <Plus />  
26 - </el-icon>  
27 - 新增 17 + <el-button type="primary" plain @click="openForm('create')">
  18 + <Icon icon="ep:plus" class="mr-5px" /> 新增
28 </el-button> 19 </el-button>
29 - </div>  
30 - </div> 20 + </el-form-item>
  21 + </el-form>
31 </ContentWrap> 22 </ContentWrap>
  23 +
32 <!-- 表格区域 --> 24 <!-- 表格区域 -->
33 <ContentWrap> 25 <ContentWrap>
34 - <el-table :data="tableData" stripe style="width: 100%" v-loading="loading" empty-text="暂无动作数据" 26 + <el-table v-loading="loading" :data="list" stripe style="width: 100%" empty-text="暂无动作数据"
35 :header-cell-style="{ background: '#f5f7fa', textAlign: 'center' }" 27 :header-cell-style="{ background: '#f5f7fa', textAlign: 'center' }"
36 :cell-style="{ textAlign: 'center', justifyContent: 'center' }"> 28 :cell-style="{ textAlign: 'center', justifyContent: 'center' }">
37 <el-table-column prop="name" label="动作名称" min-width="120" /> 29 <el-table-column prop="name" label="动作名称" min-width="120" />
38 - <!-- 封面 --> 30 +
39 <el-table-column prop="url3dAnimation" label="封面" align="center" min-width="120"> 31 <el-table-column prop="url3dAnimation" label="封面" align="center" min-width="120">
40 <template #default="scope"> 32 <template #default="scope">
41 <div class="cover-wrapper"> 33 <div class="cover-wrapper">
42 - <!-- 优先显示行数据封面,无则显示占位图 -->  
43 - <img class="cover-img" :src="scope.row.url3dAnimation || lostImg" alt="模板封面" /> 34 + <el-image class="cover-img" :src="scope.row.url3dAnimation || lostImg"
  35 + :preview-src-list="scope.row.url3dAnimation ? [scope.row.url3dAnimation] : [lostImg]" preview-teleported
  36 + fit="cover" />
44 </div> 37 </div>
45 </template> 38 </template>
46 </el-table-column> 39 </el-table-column>
47 - <!-- 用具分类 --> 40 +
48 <el-table-column label="用具分类" prop="equipmentName" min-width="100" /> 41 <el-table-column label="用具分类" prop="equipmentName" min-width="100" />
49 - <!-- 部位分类 -->  
50 <el-table-column label="部位分类" min-width="100" prop="categoryName" /> 42 <el-table-column label="部位分类" min-width="100" prop="categoryName" />
51 <el-table-column prop="createTime" label="创建时间" min-width="180" :formatter="dateFormatter" /> 43 <el-table-column prop="createTime" label="创建时间" min-width="180" :formatter="dateFormatter" />
52 <el-table-column prop="updateTime" label="更新时间" min-width="180" :formatter="dateFormatter" /> 44 <el-table-column prop="updateTime" label="更新时间" min-width="180" :formatter="dateFormatter" />
53 - <!-- 操作列 --> 45 +
54 <el-table-column label="操作" width="140" fixed="right"> 46 <el-table-column label="操作" width="140" fixed="right">
55 <template #default="scope"> 47 <template #default="scope">
56 - <el-button type="text" class="text-blue-600" @click="handleEdit(scope.row)">编辑</el-button>  
57 - <el-button type="text" class="text-red-600" @click="handleDelete(scope.row)">删除</el-button> 48 + <el-button link type="primary" @click="openForm('update', scope.row.id)">
  49 + 编辑
  50 + </el-button>
  51 + <el-button link type="danger" @click="handleDelete(scope.row.id)">
  52 + 删除
  53 + </el-button>
58 </template> 54 </template>
59 </el-table-column> 55 </el-table-column>
60 </el-table> 56 </el-table>
61 57
62 <!-- 分页组件 --> 58 <!-- 分页组件 -->
63 - <div class="pagination-wrapper" style="padding: 12px 16px;">  
64 - <!-- <span class="goto-text">共 {{ total }} 条</span> -->  
65 - <!-- 共 {{ total }} 条 -->  
66 - <el-pagination v-model:current-page="pagination.pageNo" v-model:page-size="pagination.pageSize" :total="total"  
67 - :page-sizes="[10, 20, 50]" background layout="total, sizes, prev, pager, next, jumper"  
68 - @size-change="handleSizeChange" @current-change="handleCurrentChange" />  
69 - </div> 59 + <Pagination v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" :total="total"
  60 + @pagination="getList" />
70 </ContentWrap> 61 </ContentWrap>
71 62
72 - <!-- 引入独立的弹窗组件 -->  
73 - <template>  
74 - <ActionFormDialog v-model:visible="dialogVisible" v-model:formData="formData" :title="dialogTitle"  
75 - @submit="handleFormSubmit" />  
76 - </template> 63 + <!-- 独立的弹窗组件:不再需要复杂的双向绑定 -->
  64 + <ActionFormDialog ref="formRef" @success="getList" />
77 </div> 65 </div>
78 </template> 66 </template>
79 67
80 <script setup lang="ts"> 68 <script setup lang="ts">
81 -// 导入依赖模块  
82 import { ref, reactive, onMounted } from 'vue' 69 import { ref, reactive, onMounted } from 'vue'
83 -import { ElMessage, ElMessageBox } from 'element-plus'  
84 -import { Search, Refresh, Plus } from '@element-plus/icons-vue'  
85 -import { dateFormatter } from '@/utils/formatTime' // 工具函数:时间格式化  
86 -import lostImg from '@/assets/imgs/lost.png' // 丢失图片占位图  
87 -// 导入新创建的弹窗组件 70 +import { dateFormatter } from '@/utils/formatTime'
  71 +import lostImg from '@/assets/imgs/lost.png'
88 import ActionFormDialog from '@/views/store/training/pose-category/ActionFormDialog.vue' 72 import ActionFormDialog from '@/views/store/training/pose-category/ActionFormDialog.vue'
89 -import { ExercisesApi, type PageParam, type ExercisesVO, type ExercisesSaveReqVO } from '@/api/store/training/pose' 73 +import { ExercisesApi } from '@/api/store/training/pose'
90 74
91 -// 组件名称定义  
92 defineOptions({ name: 'SystemActionManage' }) 75 defineOptions({ name: 'SystemActionManage' })
93 76
94 -// 表格核心数据模块  
95 -const loading = ref<boolean>(false) // 表格加载状态  
96 -const tableData = ref<ExercisesVO[]>([]) // 表格数据源  
97 -const total = ref<number>(0) // 数据总条数 77 +const message = useMessage() // 芋道消息组件
98 78
99 -// 分页参数模块  
100 -const pagination = reactive<{ pageNo: number; pageSize: number }>({  
101 - pageNo: 1, // 当前页码  
102 - pageSize: 10 // 每页条数  
103 -}) 79 +const queryFormRef = ref()
  80 +const formRef = ref() // 弹窗组件的 ref 句柄
  81 +const loading = ref<boolean>(false)
  82 +const list = ref<any[]>([])
  83 +const total = ref<number>(0)
104 84
105 -// 动作名称搜索关键词  
106 -const searchForm = reactive<{ name: string }>({ 85 +const queryParams = reactive({
  86 + pageNo: 1,
  87 + pageSize: 10,
107 name: '' 88 name: ''
108 }) 89 })
109 90
110 -// 弹窗相关模块(简化)  
111 -const dialogVisible = ref<boolean>(false) // 弹窗显隐控制  
112 -const dialogTitle = ref('新增动作') // 弹窗标题(新增/编辑)  
113 -const formData = reactive<ExercisesSaveReqVO>({ // 表单数据模型  
114 - id: undefined,  
115 - name: '',  
116 - categoryId: undefined,  
117 - equipmentId: undefined,  
118 - exerciseType: undefined,  
119 - urlImage: '',  
120 - url3dAnimation: '',  
121 - urlRealPerson: '',  
122 - urlTutorial: '',  
123 - stepDescription: '',  
124 - primaryMuscles: '',  
125 - secondaryMuscles: ''  
126 -})  
127 -// 查询动作列表(核心接口调用)  
128 -const handleQuery = async () => { 91 +// 获取列表数据
  92 +const getList = async () => {
129 loading.value = true 93 loading.value = true
130 try { 94 try {
131 - const params: PageParam = { pageNo: pagination.pageNo, pageSize: pagination.pageSize, name: searchForm.name }  
132 - const res = await ExercisesApi.getExercisesPage(params)  
133 - console.log('✅ 动作接口请求成功:', res)  
134 - tableData.value = res.list || [] 95 + const res = await ExercisesApi.getExercisesPage(queryParams)
  96 + list.value = res.list || []
135 total.value = res.total || 0 97 total.value = res.total || 0
136 } catch (err) { 98 } catch (err) {
137 - console.error('查询失败:', err)  
138 - ElMessage.error('查询失败,请稍后重试')  
139 - tableData.value = [] 99 + console.error(err)
  100 + list.value = []
140 total.value = 0 101 total.value = 0
141 } finally { 102 } finally {
142 loading.value = false 103 loading.value = false
143 } 104 }
144 } 105 }
145 106
146 -// 重置搜索条件  
147 -const resetQuery = () => {  
148 - searchForm.name = ''  
149 - pagination.pageNo = 1  
150 - handleQuery() 107 +// 触发查询
  108 +const handleQuery = () => {
  109 + queryParams.pageNo = 1
  110 + getList()
151 } 111 }
152 112
153 -// 新增动作:打开弹窗  
154 -const handleAdd = () => {  
155 - console.log('点击新增,准备打开弹窗') // 加日志调试  
156 - // 重置表单数据  
157 - resetFormData()  
158 - dialogTitle.value = '新增动作'  
159 - dialogVisible.value = true 113 +// 重置查询条件
  114 +const resetQuery = () => {
  115 + queryFormRef.value?.resetFields()
  116 + handleQuery()
160 } 117 }
161 118
162 -// 编辑动作:打开弹窗并回显数据  
163 -const handleEdit = (row: ExercisesVO) => {  
164 - // 重置表单数据后赋值  
165 - resetFormData()  
166 - // ✅ 关键修复:把后端返回的字符串转成数组,再赋值  
167 - const formatRow = {  
168 - ...row,  
169 - // 解析 JSON 字符串为数组  
170 - primaryMuscles: row.primaryMuscles ? JSON.parse(row.primaryMuscles) : [],  
171 - secondaryMuscles: row.secondaryMuscles ? JSON.parse(row.secondaryMuscles) : []  
172 - }  
173 - Object.assign(formData, formatRow)  
174 - dialogTitle.value = '编辑动作'  
175 - dialogVisible.value = true  
176 - console.log('点击新增,dialogVisible =', dialogVisible.value) // 加日志调试 119 +// 标准打开弹窗方式(通过组件 ref 句柄触发)
  120 +const openForm = (type: string, id?: number) => {
  121 + formRef.value.open(type, id)
177 } 122 }
178 123
179 -// 删除动作:确认后调用删除接口  
180 -const handleDelete = async (row: ExercisesVO) => { 124 +// 标准删除动作(采用 useMessage)
  125 +const handleDelete = async (id: number) => {
181 try { 126 try {
182 - await ElMessageBox.confirm(`确定要删除动作「${row.name}」吗?删除后不可恢复!`, '提示', {  
183 - confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'  
184 - })  
185 - await ExercisesApi.deleteExercises(row.id)  
186 - ElMessage.success('删除成功')  
187 - handleQuery() 127 + await message.confirm('确定要删除当前动作吗?删除后不可恢复!')
  128 + await ExercisesApi.deleteExercises(id)
  129 + message.success('删除成功')
  130 + getList()
188 } catch (err) { 131 } catch (err) {
189 - if (err !== 'cancel') {  
190 - console.error('删除失败:', err)  
191 - ElMessage.error('删除失败,请稍后重试')  
192 - } 132 + console.warn(err)
193 } 133 }
194 } 134 }
195 135
196 -// 处理表单提交(新增/编辑)  
197 -// const handleFormSubmit = async (submitData: ExercisesSaveReqVO) => {  
198 -// try {  
199 -// // ✅ 直接传数组,axios 自动转成 JSON 字符串  
200 -// const formatData = {  
201 -// ...submitData,  
202 -// primaryMuscles: JSON.stringify(submitData.primaryMuscles || []),  
203 -// secondaryMuscles: JSON.stringify(submitData.secondaryMuscles || [])  
204 -// }  
205 -// console.log('提交的数据', formatData);  
206 -// if (submitData.id) {  
207 -// await ExercisesApi.updateExercises(formatData) // 编辑:调用更新接口  
208 -// console.log('动作编辑接口数据', submitData)  
209 -// ElMessage.success('动作编辑成功!')  
210 -// } else {  
211 -// console.log('动作新增接口数据', submitData)  
212 -// await ExercisesApi.addExercises(formatData) // 新增:调用创建接口  
213 -// ElMessage.success('动作新增成功!')  
214 -// }  
215 -// // 关闭弹窗  
216 -// handleDialogClose()  
217 -// // 重新查询数据  
218 -// handleQuery()  
219 -// } catch (err) {  
220 -// console.error('提交失败:', err)  
221 -// ElMessage.error(submitData.id ? '更新失败,请稍后重试' : '新增失败,请稍后重试')  
222 -// }  
223 -// }  
224 -  
225 -const handleFormSubmit = () => {  
226 - handleQuery()  
227 - dialogVisible.value = false  
228 - resetFormData()  
229 -}  
230 -  
231 -// 关闭弹窗  
232 -// const handleDialogClose = () => {  
233 -// dialogVisible.value = false  
234 -// resetFormData()  
235 -// }  
236 -  
237 -// 重置表单数据  
238 -const resetFormData = () => {  
239 - Object.assign(formData, {  
240 - id: undefined,  
241 - name: '',  
242 - categoryId: 0,  
243 - equipmentId: 0,  
244 - exerciseType: 0,  
245 - url3dAnimation: '',  
246 - urlRealPerson: '',  
247 - urlTutorial: '',  
248 - stepDescription: '',  
249 - primaryMuscles: [],  
250 - secondaryMuscles: []  
251 - })  
252 -}  
253 -  
254 -//------------------分页模块-----------------  
255 -// 每页条数变更事件  
256 -const handleSizeChange = (val: number) => {  
257 - pagination.pageSize = val  
258 - handleQuery()  
259 -}  
260 -  
261 -// 页码变更事件  
262 -const handleCurrentChange = (val: number) => {  
263 - pagination.pageNo = val  
264 - handleQuery()  
265 -}  
266 -  
267 -// 图片加载失败处理(备用图)  
268 -const handleImgError = (e: Event) => {  
269 - const img = e.target as HTMLImageElement  
270 - img.src = lostImg  
271 -}  
272 -  
273 -// 生命周期模块  
274 -onMounted(async () => {  
275 - handleQuery() 136 +// 生命周期挂载
  137 +onMounted(() => {
  138 + getList()
276 }) 139 })
277 </script> 140 </script>
278 141
279 <style scoped> 142 <style scoped>
280 -/* 页面样式模块(保持不变) */  
281 .page-container { 143 .page-container {
282 width: 100%; 144 width: 100%;
283 padding: 16px; 145 padding: 16px;
@@ -285,101 +147,21 @@ onMounted(async () => { @@ -285,101 +147,21 @@ onMounted(async () => {
285 box-sizing: border-box; 147 box-sizing: border-box;
286 } 148 }
287 149
288 -.search-bar-wrapper {  
289 - padding: 16px;  
290 - margin-bottom: 16px;  
291 - background: #fff;  
292 - border-radius: 4px;  
293 - box-shadow: 0 1px 2px rgb(0 0 0 / 5%)  
294 -}  
295 -  
296 -.search-row {  
297 - display: flex;  
298 - align-items: center;  
299 - gap: 12px;  
300 -}  
301 -  
302 -.search-label {  
303 - font-size: 14px;  
304 - color: #303133;  
305 - white-space: nowrap;  
306 -}  
307 -  
308 -.search-input {  
309 - width: 280px;  
310 - height: 32px;  
311 -}  
312 -  
313 -.search-btn {  
314 - height: 32px;  
315 - padding: 0 16px;  
316 - font-size: 14px;  
317 - color: #fff;  
318 - background-color: #409eff;  
319 - border-color: #409eff;  
320 -}  
321 -  
322 -.reset-btn {  
323 - height: 32px;  
324 - padding: 0 16px;  
325 - font-size: 14px;  
326 - color: #606266;  
327 - background-color: #fff;  
328 - border-color: #dcdfe6;  
329 -}  
330 -  
331 -.add-btn {  
332 - height: 32px;  
333 - padding: 0 16px;  
334 - font-size: 14px;  
335 - color: #409eff;  
336 - background-color: #fff;  
337 - border-color: #409eff;  
338 -}  
339 -  
340 -.table-container {  
341 - padding: 16px;  
342 - background: #fff;  
343 - border-radius: 4px;  
344 - box-shadow: 0 1px 2px rgb(0 0 0 / 5%);  
345 -}  
346 -  
347 -.cover-wrap { 150 +.cover-wrapper {
348 display: flex; 151 display: flex;
349 - width: 60px;  
350 - height: 60px; 152 + width: 100px;
  153 + height: 50px;
351 margin: 0 auto; 154 margin: 0 auto;
352 border: 1px solid #e4e7ed; 155 border: 1px solid #e4e7ed;
353 - border-radius: 4px; 156 + border-radius: 6px;
354 align-items: center; 157 align-items: center;
355 justify-content: center; 158 justify-content: center;
  159 + overflow: hidden;
  160 + background-color: #f5f7fa;
356 } 161 }
357 162
358 .cover-img { 163 .cover-img {
359 - width: 100px;  
360 - height: 50px !important;  
361 - border-radius: 6px;  
362 - object-fit: cover;  
363 -}  
364 -  
365 -.pagination-wrapper {  
366 - display: flex;  
367 - align-items: center;  
368 - justify-content: flex-end;  
369 - margin-top: 16px;  
370 -}  
371 -  
372 -.goto-text {  
373 - margin-right: 12px;  
374 - font-size: 14px;  
375 - color: #606266;  
376 -}  
377 -  
378 -.text-blue-600 {  
379 - color: #1677ff;  
380 -}  
381 -  
382 -.text-red-600 {  
383 - color: #f5222d; 164 + width: 100%;
  165 + height: 100%;
384 } 166 }
385 </style> 167 </style>