From 2ce2c1a31f2cd0fd035fd7d975a454523483dedd Mon Sep 17 00:00:00 2001 From: wlkjyy Date: Wed, 7 Jan 2026 17:21:01 +0800 Subject: [PATCH 01/92] =?UTF-8?q?feat:=20=E5=B7=A5=E5=8D=95=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E4=BC=98=E5=8C=96=20-=20=E4=BF=AE=E5=A4=8D=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E8=B7=B3=E8=BD=AC=E9=97=AE=E9=A2=98=E5=B9=B6=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E7=94=A8=E6=88=B7=E7=AD=9B=E9=80=89=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复工单详情页定时刷新导致的自动跳转问题 - 添加用户搜索选择器,支持按用户筛选工单 - 优化用户搜索体验,使用对话框模式 - 修正API响应数据结构解析 --- src/api/ticket.js | 3 +- src/views/ticket/TicketDetail.vue | 3 +- src/views/ticket/TicketList.vue | 280 +++++++++++++++++++++++++++--- 3 files changed, 262 insertions(+), 24 deletions(-) diff --git a/src/api/ticket.js b/src/api/ticket.js index 7ea0668..5f2c459 100644 --- a/src/api/ticket.js +++ b/src/api/ticket.js @@ -5,11 +5,12 @@ import request from "@/utils/request.js"; * @returns {Promise} */ -export function getTickerList(count, page, status, orderBy, order) { +export function getTickerList(count, page, status, orderBy, order, userId) { const params = { count, page } if (status !== undefined && status !== '') params.status = status if (orderBy) params.orderBy = orderBy if (order) params.order = order + if (userId) params.user_id = userId console.log('工单列表请求参数:', params) // 调试日志 return request.get('/api/v1/admin/work_order/list', params) } diff --git a/src/views/ticket/TicketDetail.vue b/src/views/ticket/TicketDetail.vue index ce79c8c..a1e2b86 100644 --- a/src/views/ticket/TicketDetail.vue +++ b/src/views/ticket/TicketDetail.vue @@ -892,7 +892,8 @@ const goToUserDetail = () => { // 定时刷新 const startAutoRefresh = () => { refreshTimer.value = setInterval(() => { - if (ticketInfo.value?.status !== 'completed') { + // 只有当前路由仍在工单详情页且工单未完成时才刷新 + if (route.path === '/ticket/detail' && route.query.id && ticketInfo.value?.status !== 'completed') { fetchTicketDetail(false) // 定时刷新时不显示 loading } }, 10000) diff --git a/src/views/ticket/TicketList.vue b/src/views/ticket/TicketList.vue index 64fd0d9..bcb3956 100644 --- a/src/views/ticket/TicketList.vue +++ b/src/views/ticket/TicketList.vue @@ -31,14 +31,21 @@ + + :model-value="selectedUser ? selectedUser.user_name : ''" + placeholder="点击选择用户筛选" + readonly + style="width: 180px; cursor: pointer" + @click="showUserDialog = true" + > + + + 刷新 @@ -99,18 +106,74 @@ @current-change="handlePageChange" /> + + + +
+ + + + +
+ +
+ 搜索关键词: {{ userSearchKeyword }} | 用户数量: {{ userList.length }} +
+ +
+ 请输入关键词搜索用户 +
+
+ 未找到匹配的用户 +
+
+
+ {{ user.user_name?.charAt(0) }} +
+
{{ user.user_name }}
+
+ 手机: {{ user.phone }} + 邮箱: {{ user.email }} + UID: {{ user.user_id }} +
+
+ +
+
+
+
+
@@ -365,6 +539,68 @@ onActivated(() => { gap: 8px; } +.user-dialog-content { + display: flex; + flex-direction: column; + gap: 16px; +} + +.user-list-container { + min-height: 300px; + max-height: 400px; + overflow-y: auto; + border: 1px solid #dcdfe6; + border-radius: 4px; +} + +.empty-hint { + display: flex; + align-items: center; + justify-content: center; + height: 300px; + color: #909399; + font-size: 14px; +} + +.user-list { + padding: 8px 0; +} + +.user-list-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + cursor: pointer; + transition: background 0.2s; +} + +.user-list-item:hover { + background: #f5f7fa; +} + +.user-list-info { + flex: 1; + min-width: 0; +} + +.user-list-name { + font-size: 14px; + font-weight: 500; + color: #303133; + margin-bottom: 4px; +} + +.user-list-sub { + font-size: 12px; + color: #909399; +} + +.user-list-arrow { + color: #c0c4cc; + font-size: 16px; +} + .user-info { display: flex; align-items: center; -- 2.52.0 From fe1a1181320fd40427d2f7b273a74e7dfc192b3b Mon Sep 17 00:00:00 2001 From: wlkjyy Date: Wed, 7 Jan 2026 17:27:54 +0800 Subject: [PATCH 02/92] =?UTF-8?q?feat:=20=E5=B7=A5=E5=8D=95=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E6=B7=BB=E5=8A=A0=E5=85=B3=E9=94=AE=E8=AF=8D=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加关键词搜索输入框,支持搜索工单标题/内容 - 300ms 防抖优化搜索性能 - 支持与用户筛选同时使用 --- src/api/ticket.js | 3 ++- src/views/ticket/TicketList.vue | 34 ++++++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/api/ticket.js b/src/api/ticket.js index 5f2c459..87d4701 100644 --- a/src/api/ticket.js +++ b/src/api/ticket.js @@ -5,12 +5,13 @@ import request from "@/utils/request.js"; * @returns {Promise} */ -export function getTickerList(count, page, status, orderBy, order, userId) { +export function getTickerList(count, page, status, orderBy, order, userId, keyword) { const params = { count, page } if (status !== undefined && status !== '') params.status = status if (orderBy) params.orderBy = orderBy if (order) params.order = order if (userId) params.user_id = userId + if (keyword) params.keyword = keyword console.log('工单列表请求参数:', params) // 调试日志 return request.get('/api/v1/admin/work_order/list', params) } diff --git a/src/views/ticket/TicketList.vue b/src/views/ticket/TicketList.vue index bcb3956..d918b57 100644 --- a/src/views/ticket/TicketList.vue +++ b/src/views/ticket/TicketList.vue @@ -46,6 +46,19 @@ + + + + 刷新 @@ -187,6 +200,10 @@ const isLoading = ref(false) const ticketList = ref([]) const activeStatus = ref('pending') // 默认选中"待处理" +// 关键词搜索 +const searchKeyword = ref('') +const keywordSearchTimer = ref(null) + // 用户搜索 const userSearchKeyword = ref('') const userList = ref([]) @@ -248,7 +265,8 @@ const fetchTicketList = async () => { statusParam, sortBy.value, sortOrder.value, - selectedUser.value?.user_id + selectedUser.value?.user_id, + searchKeyword.value.trim() ) if (res.code === 200) { @@ -348,6 +366,17 @@ const clearUserFilter = () => { fetchTicketList() } +// 关键词搜索 +const handleKeywordSearch = () => { + if (keywordSearchTimer.value) { + clearTimeout(keywordSearchTimer.value) + } + keywordSearchTimer.value = setTimeout(() => { + currentPage.value = 1 + fetchTicketList() + }, 300) +} + // 按状态过滤 const filterByStatus = (status) => { if (activeStatus.value === status) return @@ -479,6 +508,9 @@ onBeforeUnmount(() => { if (userSearchTimer.value) { clearTimeout(userSearchTimer.value) } + if (keywordSearchTimer.value) { + clearTimeout(keywordSearchTimer.value) + } }) -- 2.52.0 From 60f141a0a9de39e1d828ae25c125b629026aac2e Mon Sep 17 00:00:00 2001 From: 2256907009 <2256907009@qq.com> Date: Thu, 8 Jan 2026 10:49:10 +0800 Subject: [PATCH 03/92] =?UTF-8?q?fix:=E4=BF=AE=E6=94=B9=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=AF=B9=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/product/ProductList.vue | 144 ++++++-- 默认模块.openapi.json | 565 +++++++++++++++++++++++++++--- 2 files changed, 630 insertions(+), 79 deletions(-) diff --git a/src/views/product/ProductList.vue b/src/views/product/ProductList.vue index 0ec3de4..d33eecd 100644 --- a/src/views/product/ProductList.vue +++ b/src/views/product/ProductList.vue @@ -216,15 +216,23 @@ style="width: 100%" :header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }" > - - - + + + + + + - + @@ -162,19 +163,32 @@ - -
-
- -
-
- - - {{ productForm.cover_id ? '更换封面' : '选择封面' }} - - 清除 - ID: {{ productForm.cover_id }} + +
+
+
+ + + + + 清除 +
@@ -206,6 +220,14 @@ + + + + + + +
all: 所有参数 / plan: 套餐 / customize: 自定义
+
+ + + +
+
+ + 新增套餐 + + + 刷新 + +
+ + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + +
参数配置为JSON数组格式
+
+ + +
多个参数ID用英文逗号分隔
+
+ + + + + + 启用 + 禁用 + + +
+ +
@@ -426,7 +564,14 @@ import { getProductList, createProduct, updateProduct, deleteProduct, getProduct deleteProductParameter, addProductParameterValue, updateProductParameterValue, - deleteProductParameterValue + deleteProductParameterValue, + getProductPlanList, + getProductPlanDetail, + createProductPlan, + updateProductPlan, + deleteProductPlan, + disableProductPlan, + enableProductPlan } from '@/api/admin/product' // 查询参数 @@ -451,7 +596,8 @@ const productForm = reactive({ pay_num: 1, expire_time: 0, recommend: false, - recommend_rebate: 0 + recommend_rebate: 0, + arg_type: 'all' // 商品参数类型 all/plan/customize }) const productRules = { @@ -613,7 +759,8 @@ const handleAdd = () => { pay_num: 1, expire_time: 0, recommend: false, - recommend_rebate: 0 + recommend_rebate: 0, + arg_type: 'all' }) productFormRef.value?.resetFields() } @@ -637,7 +784,8 @@ const handleEdit = (row) => { pay_num: row.payNum, expire_time: row.expireTime, recommend: row.recommend, - recommend_rebate: row.recommendRebate + recommend_rebate: row.recommendRebate, + arg_type: row.argType || 'all' }) // 加载封面预览 loadCoverPreview(row.coverId) @@ -732,7 +880,8 @@ const submitForm = () => { price: Math.round(productForm.price * 100) || 0, // 元转分 pay_num: productForm.pay_num || 1, expire_time: productForm.expire_time || 0, - recommend_rebate: productForm.recommend_rebate || 0 + recommend_rebate: productForm.recommend_rebate || 0, + arg_type: productForm.arg_type || 'all' // 商品参数类型 } console.log('提交的数据:', submitData) // 调试日志 @@ -1091,6 +1240,219 @@ const submitParamValueForm = () => { }) } +// ==================== 商品套餐管理 ==================== +const planDialogVisible = ref(false) +const planLoading = ref(false) +const planList = ref([]) +const currentPlanProductId = ref(null) +const currentPlanProductName = ref('') + +// 套餐表单 +const planFormDialogVisible = ref(false) +const planFormType = ref('add') +const planFormRef = ref(null) +const planForm = reactive({ + plan_id: undefined, + name: '', + note: '', + args: '', + extra_arg_ids: '', + index: 0, + disable: false +}) + +const planFormRules = { + name: [{ required: true, message: '请输入套餐名称', trigger: 'blur' }] +} + +// 打开套餐管理 +const handlePlan = (row) => { + currentPlanProductId.value = row.id + currentPlanProductName.value = row.name + planDialogVisible.value = true + fetchPlanList() +} + +// 解析args JSON字符串 +const parseArgs = (argsStr) => { + if (!argsStr) return [] + try { + const parsed = JSON.parse(argsStr) + return Array.isArray(parsed) ? parsed : [] + } catch (e) { + console.error('解析args失败:', e) + return [] + } +} + +// 格式化显示args +const formatArgs = (argsParsed) => { + if (!argsParsed || argsParsed.length === 0) return '-' + return argsParsed.map(arg => arg.name || arg.value).filter(Boolean).join(', ') +} + +// 获取套餐列表 +const fetchPlanList = async () => { + if (!currentPlanProductId.value) return + planLoading.value = true + try { + const res = await getProductPlanList({ good_id: String(currentPlanProductId.value) }) + + if (res.data.code === 200) { + // 兼容两种数据结构 + let data = res.data.data + if (Array.isArray(data)) { + // 处理args字段,将JSON字符串解析为对象 + planList.value = data.map(item => ({ + ...item, + argsParsed: parseArgs(item.args) + })) + } else if (data && data.list) { + planList.value = data.list.map(item => ({ + ...item, + argsParsed: parseArgs(item.args) + })) + } else if (data && data.data) { + planList.value = data.data.map(item => ({ + ...item, + argsParsed: parseArgs(item.args) + })) + } else { + planList.value = [] + } + } + } catch (error) { + console.error('获取套餐列表失败:', error) + ElMessage.error('获取套餐列表失败') + } finally { + planLoading.value = false + } +} + +// 新增套餐 +const handleAddPlan = () => { + planFormType.value = 'add' + planFormDialogVisible.value = true + Object.assign(planForm, { + plan_id: undefined, + name: '', + note: '', + args: '', + extra_arg_ids: '', + index: 0, + disable: false + }) + nextTick(() => { + planFormRef.value?.resetFields() + }) +} + +// 编辑套餐 +const handleEditPlan = async (row) => { + planFormType.value = 'edit' + try { + const res = await getProductPlanDetail({ good_id: String(currentPlanProductId.value), plan_id: String(row.id) }) + if (res.data.code === 200) { + const data = res.data.data + // 处理args字段:如果是字符串先解析,再格式化为漂亮的JSON + let argsValue = '' + if (data.args) { + try { + // 如果args是字符串,先解析为对象 + const argsObj = typeof data.args === 'string' ? JSON.parse(data.args) : data.args + // 格式化为漂亮的JSON(带缩进) + argsValue = JSON.stringify(argsObj, null, 2) + } catch (e) { + // 解析失败则保持原值 + argsValue = data.args + } + } + Object.assign(planForm, { + plan_id: data.id, + name: data.name || '', + note: data.note || '', + args: argsValue, + extra_arg_ids: data.extraArgIds ? data.extraArgIds.join(',') : '', + index: data.index || 0, + disable: data.disable || false + }) + planFormDialogVisible.value = true + } + } catch (error) { + ElMessage.error('获取套餐详情失败') + } +} + +// 删除套餐 +const handleDeletePlan = (row) => { + ElMessageBox.confirm(`确认删除套餐 ${row.name} 吗?`, '警告', { + confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' + }).then(async () => { + try { + const res = await deleteProductPlan({ good_id: String(currentPlanProductId.value), plan_id: String(row.id) }) + if (res.data.code === 200) { + ElMessage.success('删除成功') + fetchPlanList() + } + } catch (error) { + ElMessage.error('删除失败') + } + }).catch(() => {}) +} + +// 切换套餐状态 +const handleTogglePlanStatus = async (row) => { + try { + const res = row.disable + ? await enableProductPlan({ good_id: String(currentPlanProductId.value), plan_id: String(row.id) }) + : await disableProductPlan({ good_id: String(currentPlanProductId.value), plan_id: String(row.id) }) + if (res.data.code === 200) { + ElMessage.success(row.disable ? '启用成功' : '禁用成功') + fetchPlanList() + } + } catch (error) { + ElMessage.error('操作失败') + } +} + +// 提交套餐表单 +const submitPlanForm = () => { + planFormRef.value?.validate(async (valid) => { + if (valid) { + try { + const submitData = { + good_id: String(currentPlanProductId.value), + name: planForm.name, + note: planForm.note || '', + args: planForm.args || '', + extra_arg_ids: planForm.extra_arg_ids || '', + index: Number(planForm.index) || 0 + } + + let res + if (planFormType.value === 'add') { + res = await createProductPlan(submitData) + } else { + submitData.plan_id = String(planForm.plan_id) + submitData.disable = planForm.disable + res = await updateProductPlan(submitData) + } + + if (res.data.code === 200) { + ElMessage.success(planFormType.value === 'add' ? '新增成功' : '修改成功') + planFormDialogVisible.value = false + fetchPlanList() + } else { + ElMessage.error(res.data.message || '操作失败') + } + } catch (error) { + console.error('套餐操作失败:', error) + ElMessage.error(error.response?.data?.message || '操作失败') + } + } + }) +} + diff --git a/问题.MD b/问题.MD index b921a48..60eeaa1 100644 --- a/问题.MD +++ b/问题.MD @@ -1,6 +1,1756 @@ ✅已完成、⚠️部分完成、❌未完成这样显示 -----------------------------------------------------------------------------------------------需要解决 + +{ + "openapi": "3.0.1", + "info": { + "title": "默认模块", + "description": "", + "version": "1.0.0" + }, + "tags": [], + "paths": { + "/api/v1/admin/good/goods/list": { + "get": { + "summary": "获取商品列表", + "deprecated": false, + "description": "", + "tags": [], + "parameters": [ + { + "name": "count", + "in": "query", + "description": "获取条数 默认 10", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "page", + "in": "query", + "description": "获取页码 默认 1", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "good_group_id", + "in": "query", + "description": "筛选商品组id", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "", + "example": "Bearer {{token}}", + "schema": { + "type": "string", + "default": "Bearer {{token}}" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + }, + "headers": {} + } + }, + "security": [] + } + }, + "/api/v1/admin/good/goods/tag_list": { + "get": { + "summary": "获取商品标签列表", + "deprecated": false, + "description": "", + "tags": [], + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "", + "example": "Bearer {{token}}", + "schema": { + "type": "string", + "default": "Bearer {{token}}" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + }, + "headers": {} + } + }, + "security": [] + } + }, + "/api/v1/admin/good/goods/create": { + "post": { + "summary": "创建商品", + "deprecated": false, + "description": "", + "tags": [], + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "", + "example": "Bearer {{token}}", + "schema": { + "type": "string", + "default": "Bearer {{token}}" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "name": { + "description": "名称", + "example": "", + "type": "string" + }, + "table": { + "description": "商品所属表", + "example": "", + "type": "string" + }, + "tag": { + "description": "商品标签,从 tag_list 接口中获取", + "example": "", + "type": "string" + }, + "content": { + "description": "内容", + "example": "", + "type": "string" + }, + "cover_id": { + "description": "封面id", + "example": 0, + "type": "integer" + }, + "inventory_control": { + "description": "是否开启商品库存", + "example": "", + "type": "string" + }, + "inventory": { + "description": "商品库存", + "example": 0, + "type": "integer" + }, + "price": { + "description": "商品价格", + "example": 0, + "type": "integer" + }, + "pay_num": { + "description": "单个商品数量", + "example": 0, + "type": "integer" + }, + "expire_time": { + "description": "商品购买有效期 /天", + "example": 0, + "type": "integer" + }, + "recommend": { + "description": "是否开启推介", + "example": "", + "type": "boolean" + }, + "recommend_rebate": { + "description": "商品推介返还 /100%", + "example": 0, + "type": "integer" + }, + "good_group_id": { + "description": "商品组id", + "example": 0, + "type": "integer" + }, + "arg_type": { + "description": "商品参数类型 all 所有 / plan 套餐 / customize 自定义", + "example": "", + "type": "string" + } + } + }, + "examples": {} + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + }, + "headers": {} + } + }, + "security": [] + } + }, + "/api/v1/admin/good/goods/update": { + "post": { + "summary": "更新商品", + "deprecated": false, + "description": "", + "tags": [], + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "", + "example": "Bearer {{token}}", + "schema": { + "type": "string", + "default": "Bearer {{token}}" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "id": { + "description": "ID 编号", + "example": "", + "type": "string" + }, + "name": { + "description": "名称", + "example": "", + "type": "string" + }, + "table": { + "description": "商品所属表", + "example": "", + "type": "string" + }, + "tag": { + "description": "商品标签,从 tag_list 接口中获取", + "example": "", + "type": "string" + }, + "content": { + "description": "内容", + "example": "", + "type": "string" + }, + "cover_id": { + "description": "封面id", + "example": 0, + "type": "integer" + }, + "inventory_control": { + "description": "是否开启商品库存", + "example": "", + "type": "boolean" + }, + "inventory": { + "description": "商品库存", + "example": 0, + "type": "integer" + }, + "price": { + "description": "商品价格", + "example": 0, + "type": "integer" + }, + "pay_num": { + "description": "单个商品数量", + "example": 0, + "type": "integer" + }, + "expire_time": { + "description": "商品购买有效期 /天", + "example": 0, + "type": "integer" + }, + "recommend": { + "description": "是否开启推介", + "example": "", + "type": "boolean" + }, + "recommend_rebate": { + "description": "商品推介返还 /100%", + "example": 0, + "type": "integer" + }, + "good_group_id": { + "description": "商品组id", + "example": 0, + "type": "integer" + }, + "arg_type": { + "description": "商品参数类型 all 所有 / plan 套餐 / customize 自定义", + "example": "", + "type": "string" + } + } + }, + "examples": {} + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + }, + "headers": {} + } + }, + "security": [] + } + }, + "/api/v1/admin/good/goods/delete": { + "delete": { + "summary": "删除商品", + "deprecated": false, + "description": "", + "tags": [], + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "", + "example": "Bearer {{token}}", + "schema": { + "type": "string", + "default": "Bearer {{token}}" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "id": { + "description": "ID 编号", + "example": "", + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + }, + "headers": {} + } + }, + "security": [] + } + } + }, + "components": { + "schemas": {}, + "responses": {}, + "securitySchemes": {} + }, + "servers": [], + "security": [] +} + +{ + "openapi": "3.0.1", + "info": { + "title": "默认模块", + "description": "", + "version": "1.0.0" + }, + "tags": [], + "paths": { + "/api/v1/admin/good/plan/list": { + "get": { + "summary": "获取商品套餐列表", + "deprecated": false, + "description": "", + "tags": [], + "parameters": [ + { + "name": "good_id", + "in": "query", + "description": "商品id", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "", + "example": "Bearer {{token}}", + "schema": { + "type": "string", + "default": "Bearer {{token}}" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + }, + "headers": {} + } + }, + "security": [] + } + }, + "/api/v1/admin/good/plan/create": { + "post": { + "summary": "创建商品套餐", + "deprecated": false, + "description": "", + "tags": [], + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "", + "example": "Bearer {{token}}", + "schema": { + "type": "string", + "default": "Bearer {{token}}" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "good_id": { + "description": "商品id", + "example": "", + "type": "string" + }, + "name": { + "description": "名称", + "example": "", + "type": "string" + }, + "note": { + "description": "说明", + "example": "", + "type": "string" + }, + "args": { + "description": "参数 json\n{\n\"arg_id\": 参数id,\n\"name\": \"参数名称\",\n\"attr_id\": 参数值id,\n\"value\": \"参数值\"(如果是 select 或 string 类型),\n\"number\": 参数值(如果是 number 类型),\n}", + "example": "", + "type": "string" + }, + "extra_arg_ids": { + "description": "额外参数 示例:1,2,3,4", + "example": "", + "type": "string" + }, + "index": { + "type": "integer", + "description": "套餐排序索引", + "example": 0 + } + } + }, + "examples": {} + } + }, + "required": true + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + }, + "headers": {} + } + }, + "security": [] + } + }, + "/api/v1/admin/good/plan/detail": { + "get": { + "summary": "获取商品套餐详情", + "deprecated": false, + "description": "", + "tags": [], + "parameters": [ + { + "name": "good_id", + "in": "query", + "description": "商品id", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "plan_id", + "in": "query", + "description": "套餐id", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "", + "example": "Bearer {{token}}", + "schema": { + "type": "string", + "default": "Bearer {{token}}" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + }, + "headers": {} + } + }, + "security": [] + } + }, + "/api/v1/admin/good/plan/update": { + "post": { + "summary": "更新商品套餐", + "deprecated": false, + "description": "", + "tags": [], + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "", + "example": "Bearer {{token}}", + "schema": { + "type": "string", + "default": "Bearer {{token}}" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "good_id": { + "description": "商品id", + "example": "", + "type": "string" + }, + "plan_id": { + "description": "套餐id", + "example": "", + "type": "string" + }, + "name": { + "description": "名称", + "example": "", + "type": "string" + }, + "note": { + "description": "说明", + "example": "", + "type": "string" + }, + "args": { + "description": "参数 json\n{\n\"arg_id\": 参数id,\n\"name\": \"参数名称\",\n\"attr_id\": 参数值id,\n\"value\": \"参数值\"(如果是 select 或 string 类型),\n\"number\": 参数值(如果是 number 类型),\n}", + "example": "", + "type": "string" + }, + "extra_arg_ids": { + "description": "额外参数 示例:1,2,3,4", + "example": "", + "type": "string" + }, + "index": { + "description": "套餐排序索引", + "example": 0, + "type": "integer" + }, + "disable": { + "type": "boolean", + "description": "是否禁用", + "example": "" + } + } + }, + "examples": {} + } + }, + "required": true + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + }, + "headers": {} + } + }, + "security": [] + } + }, + "/api/v1/admin/good/plan/delete": { + "delete": { + "summary": "删除商品套餐", + "deprecated": false, + "description": "", + "tags": [], + "parameters": [ + { + "name": "good_id", + "in": "query", + "description": "商品id", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "plan_id", + "in": "query", + "description": "套餐id", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "", + "example": "Bearer {{token}}", + "schema": { + "type": "string", + "default": "Bearer {{token}}" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + }, + "headers": {} + } + }, + "security": [] + } + }, + "/api/v1/admin/good/plan/disable": { + "post": { + "summary": "禁用商品套餐", + "deprecated": false, + "description": "", + "tags": [], + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "", + "example": "Bearer {{token}}", + "schema": { + "type": "string", + "default": "Bearer {{token}}" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "good_id": { + "type": "integer", + "description": "商品id", + "example": 0 + }, + "plan_id": { + "type": "integer", + "description": "套餐id", + "example": 0 + } + } + }, + "examples": {} + } + }, + "required": true + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + }, + "headers": {} + } + }, + "security": [] + } + }, + "/api/v1/admin/good/plan/enable": { + "post": { + "summary": "启用商品套餐", + "deprecated": false, + "description": "", + "tags": [], + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "", + "example": "Bearer {{token}}", + "schema": { + "type": "string", + "default": "Bearer {{token}}" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "good_id": { + "description": "商品id", + "example": 0, + "type": "integer" + }, + "plan_id": { + "description": "套餐id", + "example": 0, + "type": "integer" + } + } + }, + "examples": {} + } + }, + "required": true + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + }, + "headers": {} + } + }, + "security": [] + } + } + }, + "components": { + "schemas": {}, + "responses": {}, + "securitySchemes": {} + }, + "servers": [], + "security": [] +} + +{ + "openapi": "3.0.1", + "info": { + "title": "默认模块", + "description": "", + "version": "1.0.0" + }, + "tags": [], + "paths": { + "/api/v1/admin/good/group/list": { + "get": { + "summary": "获取商品分组列表", + "deprecated": false, + "description": "", + "tags": [], + "parameters": [ + { + "name": "count", + "in": "query", + "description": "获取条数 默认 10", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "page", + "in": "query", + "description": "获取页码 默认 1", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "key", + "in": "query", + "description": "关键词筛选", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "disable", + "in": "query", + "description": "筛选 隐藏/显示 的分组", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "level", + "in": "query", + "description": "筛选分组层级", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "parent_id", + "in": "query", + "description": "筛选父级分组id", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "name": "tag", + "in": "query", + "description": "筛选分组标签", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "", + "example": "Bearer {{token}}", + "schema": { + "type": "string", + "default": "Bearer {{token}}" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + }, + "headers": {} + } + }, + "security": [] + } + }, + "/api/v1/admin/good/group/create": { + "post": { + "summary": "创建商品分组", + "deprecated": false, + "description": "", + "tags": [], + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "", + "example": "Bearer {{token}}", + "schema": { + "type": "string", + "default": "Bearer {{token}}" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "name": { + "description": "名称", + "example": "", + "type": "string" + }, + "note": { + "description": "备注", + "example": "", + "type": "string" + }, + "disable": { + "description": "是否隐藏", + "example": "", + "type": "boolean" + }, + "level": { + "description": "分组层级", + "example": "", + "type": "string" + }, + "parent_id": { + "description": "父级分组id", + "example": 0, + "type": "integer" + }, + "cover_id": { + "description": "分组封面id", + "example": 0, + "type": "integer" + }, + "tag_id": { + "description": "分组标签id", + "example": 0, + "type": "integer" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + }, + "headers": {} + } + }, + "security": [] + } + }, + "/api/v1/admin/good/group/update": { + "post": { + "summary": "更新商品分组", + "deprecated": false, + "description": "", + "tags": [], + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "", + "example": "Bearer {{token}}", + "schema": { + "type": "string", + "default": "Bearer {{token}}" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "id": { + "description": "ID 编号", + "example": "", + "type": "string" + }, + "name": { + "description": "名称", + "example": "", + "type": "string" + }, + "note": { + "description": "备注", + "example": "", + "type": "string" + }, + "disable": { + "description": "是否隐藏", + "example": "", + "type": "boolean" + }, + "level": { + "description": "分组层级", + "example": "", + "type": "string" + }, + "parent_id": { + "description": "父级分组id", + "example": 0, + "type": "integer" + }, + "cover_id": { + "description": "分组封面id", + "example": 0, + "type": "integer" + }, + "tag_id": { + "description": "分组标签id", + "example": 0, + "type": "integer" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + }, + "headers": {} + } + }, + "security": [] + } + }, + "/api/v1/admin/good/group/disable": { + "post": { + "summary": "隐藏商品组", + "deprecated": false, + "description": "", + "tags": [], + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "", + "example": "Bearer {{token}}", + "schema": { + "type": "string", + "default": "Bearer {{token}}" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "id": { + "example": "", + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + }, + "headers": {} + } + }, + "security": [] + } + }, + "/api/v1/admin/good/group/enable": { + "post": { + "summary": "启用商品组", + "deprecated": false, + "description": "", + "tags": [], + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "", + "example": "Bearer {{token}}", + "schema": { + "type": "string", + "default": "Bearer {{token}}" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "id": { + "description": "ID 编号", + "example": "", + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + }, + "headers": {} + } + }, + "security": [] + } + }, + "/api/v1/admin/good/group/delete": { + "delete": { + "summary": "删除商品分组", + "deprecated": false, + "description": "", + "tags": [], + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "", + "example": "Bearer {{token}}", + "schema": { + "type": "string", + "default": "Bearer {{token}}" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "id": { + "description": "ID 编号", + "example": "", + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + }, + "headers": {} + } + }, + "security": [] + } + } + }, + "components": { + "schemas": {}, + "responses": {}, + "securitySchemes": {} + }, + "servers": [], + "security": [] +} + + + + + +1.http://localhost:5173/product/group分组页的table是需要分页的 +2.点击商品分组页面的添加子级分组点击分组封面并没有弹出 +3.添加顶级分组和添加子级分组失败, +curl ^"https://apiservertest.s1f.ren/api/v1/admin/good/group/create^" ^ + -H ^"Accept: application/json, text/plain, */*^" ^ + -H ^"Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6^" ^ + -H ^"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzAwNTk3MzYsImlzX2FkbWluIjp0cnVlLCJrZXkiOiJSUkR2MFJmT2RZRXpXaXpPIiwidXNlcl9ncm91cCI6MTEsInVzZXJfaWQiOjQsInVzZXJfbmFtZSI6InNoaXJhbiJ9.R5aT8pnXiHb31raNWyOCd39sPz3voov-OtENWvWCjR0^" ^ + -H ^"Connection: keep-alive^" ^ + -H ^"Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryswFqovBifaqLkuBL^" ^ + -H ^"Origin: http://localhost:5173^" ^ + -H ^"Referer: http://localhost:5173/^" ^ + -H ^"Sec-Fetch-Dest: empty^" ^ + -H ^"Sec-Fetch-Mode: cors^" ^ + -H ^"Sec-Fetch-Site: cross-site^" ^ + -H ^"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 Edg/144.0.0.0^" ^ + -H ^"sec-ch-ua: ^\^"Not(A:Brand^\^";v=^\^"8^\^", ^\^"Chromium^\^";v=^\^"144^\^", ^\^"Microsoft Edge^\^";v=^\^"144^\^"^" ^ + -H ^"sec-ch-ua-mobile: ?0^" ^ + -H ^"sec-ch-ua-platform: ^\^"Windows^\^"^" ^ + --data-raw ^"------WebKitFormBoundaryswFqovBifaqLkuBL^ + +Content-Disposition: form-data; name=^\^"name^\^"^ + +^ + +^ ^ ^ ^ + +------WebKitFormBoundaryswFqovBifaqLkuBL^ + +Content-Disposition: form-data; name=^\^"note^\^"^ + +^ + +^ ^ ^ ^ + +------WebKitFormBoundaryswFqovBifaqLkuBL^ + +Content-Disposition: form-data; name=^\^"disable^\^"^ + +^ + +false^ + +------WebKitFormBoundaryswFqovBifaqLkuBL^ + +Content-Disposition: form-data; name=^\^"level^\^"^ + +^ + +1^ + +------WebKitFormBoundaryswFqovBifaqLkuBL^ + +Content-Disposition: form-data; name=^\^"parent_id^\^"^ + +^ + +0^ + +------WebKitFormBoundaryswFqovBifaqLkuBL^ + +Content-Disposition: form-data; name=^\^"cover_id^\^"^ + +^ + +0^ + +------WebKitFormBoundaryswFqovBifaqLkuBL^ + +Content-Disposition: form-data; name=^\^"tag_id^\^"^ + +^ + +0^ + +------WebKitFormBoundaryswFqovBifaqLkuBL--^ + +^" +4.获取套餐列表失败 +curl ^"https://apiservertest.s1f.ren/api/v1/admin/good/plan/list?good_id=29^" ^ + -H ^"Accept: application/json, text/plain, */*^" ^ + -H ^"Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6^" ^ + -H ^"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzAwNTk3MzYsImlzX2FkbWluIjp0cnVlLCJrZXkiOiJSUkR2MFJmT2RZRXpXaXpPIiwidXNlcl9ncm91cCI6MTEsInVzZXJfaWQiOjQsInVzZXJfbmFtZSI6InNoaXJhbiJ9.R5aT8pnXiHb31raNWyOCd39sPz3voov-OtENWvWCjR0^" ^ + -H ^"Connection: keep-alive^" ^ + -H ^"Origin: http://localhost:5173^" ^ + -H ^"Referer: http://localhost:5173/^" ^ + -H ^"Sec-Fetch-Dest: empty^" ^ + -H ^"Sec-Fetch-Mode: cors^" ^ + -H ^"Sec-Fetch-Site: cross-site^" ^ + -H ^"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 Edg/144.0.0.0^" ^ + -H ^"sec-ch-ua: ^\^"Not(A:Brand^\^";v=^\^"8^\^", ^\^"Chromium^\^";v=^\^"144^\^", ^\^"Microsoft Edge^\^";v=^\^"144^\^"^" ^ + -H ^"sec-ch-ua-mobile: ?0^" ^ + -H ^"sec-ch-ua-platform: ^\^"Windows^\^"^" + + 5.创建新套餐失败,为什么会发出两个请求一个是options一个是post + curl ^"https://apiservertest.s1f.ren/api/v1/admin/good/plan/create^" ^ + -H ^"Accept: application/json, text/plain, */*^" ^ + -H ^"Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6^" ^ + -H ^"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzAwNTk3MzYsImlzX2FkbWluIjp0cnVlLCJrZXkiOiJSUkR2MFJmT2RZRXpXaXpPIiwidXNlcl9ncm91cCI6MTEsInVzZXJfaWQiOjQsInVzZXJfbmFtZSI6InNoaXJhbiJ9.R5aT8pnXiHb31raNWyOCd39sPz3voov-OtENWvWCjR0^" ^ + -H ^"Connection: keep-alive^" ^ + -H ^"Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryKCGyplVDbP3o88BN^" ^ + -H ^"Origin: http://localhost:5173^" ^ + -H ^"Referer: http://localhost:5173/^" ^ + -H ^"Sec-Fetch-Dest: empty^" ^ + -H ^"Sec-Fetch-Mode: cors^" ^ + -H ^"Sec-Fetch-Site: cross-site^" ^ + -H ^"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 Edg/144.0.0.0^" ^ + -H ^"sec-ch-ua: ^\^"Not(A:Brand^\^";v=^\^"8^\^", ^\^"Chromium^\^";v=^\^"144^\^", ^\^"Microsoft Edge^\^";v=^\^"144^\^"^" ^ + -H ^"sec-ch-ua-mobile: ?0^" ^ + -H ^"sec-ch-ua-platform: ^\^"Windows^\^"^" ^ + --data-raw ^"------WebKitFormBoundaryKCGyplVDbP3o88BN^ + +Content-Disposition: form-data; name=^\^"good_id^\^"^ + +^ + +29^ + +------WebKitFormBoundaryKCGyplVDbP3o88BN^ + +Content-Disposition: form-data; name=^\^"name^\^"^ + +^ + +adasd^ + +------WebKitFormBoundaryKCGyplVDbP3o88BN^ + +Content-Disposition: form-data; name=^\^"note^\^"^ + +^ + +adasd^ + +------WebKitFormBoundaryKCGyplVDbP3o88BN^ + +Content-Disposition: form-data; name=^\^"args^\^"^ + +^ + +^[^{^\^"arg_id^\^":1,^\^"name^\^":^\^"cpu^\^",^\^"attr_id^\^":1,^\^"value^\^":^\^"50BG^\^"^}^]^ + +------WebKitFormBoundaryKCGyplVDbP3o88BN^ + +Content-Disposition: form-data; name=^\^"extra_arg_ids^\^"^ + +^ + +1^ + +------WebKitFormBoundaryKCGyplVDbP3o88BN^ + +Content-Disposition: form-data; name=^\^"index^\^"^ + +^ + +1^ + +------WebKitFormBoundaryKCGyplVDbP3o88BN--^ + +^" + + + +6.在商品列表的编辑商品的弹窗中的封面id的点击按钮需要切换为符合整体的样式参照其他选择id,封面的样式,这个使用AvatarSelector组件 + + + +7.创建新产品需要添加一个商品参数类型的选择,修改也是arg_type:商品参数类型 all 所有 / plan 套餐 / customize 自定义 + + + + +8.新增商品分组标签管理,查看添加到什么位置比较合适 + +{ + "openapi": "3.0.1", + "info": { + "title": "默认模块", + "description": "", + "version": "1.0.0" + }, + "tags": [], + "paths": { + "/api/v1/admin/good/group_tag/list": { + "get": { + "summary": "获取商品分组标签列表", + "deprecated": false, + "description": "", + "tags": [], + "parameters": [ + { + "name": "page", + "in": "query", + "description": "获取页码 默认 1", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "count", + "in": "query", + "description": "获取条数 默认 10", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "key", + "in": "query", + "description": "关键词筛选", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "", + "example": "Bearer {{token}}", + "schema": { + "type": "string", + "default": "Bearer {{token}}" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + }, + "headers": {} + } + }, + "security": [] + } + }, + "/api/v1/admin/good/group_tag/detail": { + "get": { + "summary": "获取商品分组标签详情", + "deprecated": false, + "description": "", + "tags": [], + "parameters": [ + { + "name": "id", + "in": "query", + "description": "", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "", + "example": "Bearer {{token}}", + "schema": { + "type": "string", + "default": "Bearer {{token}}" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + }, + "headers": {} + } + }, + "security": [] + } + }, + "/api/v1/admin/good/group_tag/create": { + "post": { + "summary": "创建商品分组标签", + "deprecated": false, + "description": "", + "tags": [], + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "", + "example": "Bearer {{token}}", + "schema": { + "type": "string", + "default": "Bearer {{token}}" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "name": { + "description": "名称", + "example": "", + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + }, + "headers": {} + } + }, + "security": [] + } + }, + "/api/v1/admin/good/group_tag/update": { + "post": { + "summary": "更新商品分组标签", + "deprecated": false, + "description": "", + "tags": [], + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "", + "example": "Bearer {{token}}", + "schema": { + "type": "string", + "default": "Bearer {{token}}" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "id": { + "description": "ID 编号", + "example": "", + "type": "string" + }, + "name": { + "description": "名称", + "example": "", + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + }, + "headers": {} + } + }, + "security": [] + } + }, + "/api/v1/admin/good/group_tag/delete": { + "delete": { + "summary": "删除商品分组标签", + "deprecated": false, + "description": "", + "tags": [], + "parameters": [ + { + "name": "id", + "in": "query", + "description": "ID 编号", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "", + "example": "Bearer {{token}}", + "schema": { + "type": "string", + "default": "Bearer {{token}}" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + }, + "headers": {} + } + }, + "security": [] + } + } + }, + "components": { + "schemas": {}, + "responses": {}, + "securitySchemes": {} + }, + "servers": [], + "security": [] +} + + + -----------------------------------------------------------------------------------------------需要解决 规则限制: @@ -172,7 +1922,6 @@ src/ | qrcode | ^1.5.4 | | dayjs | ^1.11.13 | ------------------------------------------------------------------------------------------------需要解决 ## 表单组件优化 ✅已完成 diff --git a/默认模块.openapi.json b/默认模块.openapi.json deleted file mode 100644 index 570748a..0000000 --- a/默认模块.openapi.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "openapi": "3.0.1", - "info": { - "title": "默认模块", - "description": "", - "version": "1.0.0" - }, - "tags": [], - "paths": { - "/api/v1/admin/user/user/update": { - "post": { - "summary": "更新用户信息", - "deprecated": false, - "description": "", - "tags": [], - "parameters": [ - { - "name": "Authorization", - "in": "header", - "description": "", - "example": "Bearer {{token}}", - "schema": { - "type": "string", - "default": "Bearer {{token}}" - } - } - ], - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "properties": { - "user_id": { - "description": "用户ID", - "example": "", - "type": "string" - }, - "sex": { - "description": "性别 true 男 false 女", - "example": "", - "type": "string" - }, - "age": { - "description": "年龄", - "example": "", - "type": "string" - }, - "cover_id": { - "description": "头像id", - "example": "", - "type": "string" - }, - "user_name": { - "description": "用户名", - "example": "", - "type": "string" - }, - "recommend_id": { - "description": "推介用户id", - "example": "", - "type": "string" - }, - "phone": { - "description": "手机号码", - "example": "", - "type": "string" - }, - "email": { - "description": "邮箱", - "example": "", - "type": "string" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": {} - } - } - }, - "headers": {} - } - }, - "security": [] - } - } - }, - "components": { - "schemas": {}, - "responses": {}, - "securitySchemes": {} - }, - "servers": [], - "security": [] -} \ No newline at end of file -- 2.52.0 From 043be60f4fce88bcf9a3c1904f4e409adaf147d3 Mon Sep 17 00:00:00 2001 From: 2256907009 <2256907009@qq.com> Date: Thu, 29 Jan 2026 17:43:45 +0800 Subject: [PATCH 19/92] =?UTF-8?q?feate:=E6=B7=BB=E5=8A=A0=E5=88=86?= =?UTF-8?q?=E7=BB=84=E6=A0=87=E7=AD=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/menus.js | 3 +- src/router/index.js | 3 +- src/views/product/ProductGroup.vue | 227 ++++++++++++++++++++++++----- src/views/product/ProductList.vue | 14 +- 问题.MD | 5 + 5 files changed, 206 insertions(+), 46 deletions(-) diff --git a/src/config/menus.js b/src/config/menus.js index f69141c..dbb2875 100644 --- a/src/config/menus.js +++ b/src/config/menus.js @@ -46,8 +46,7 @@ export const menus = [ { path: '/product/group', title: '商品分组' - }, - + } ] }, { diff --git a/src/router/index.js b/src/router/index.js index 162ff31..c4c0daa 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -249,8 +249,7 @@ const routes = [ meta: { title: '商品分组' } - }, - + } ] }, // 订单管理路由 diff --git a/src/views/product/ProductGroup.vue b/src/views/product/ProductGroup.vue index 7edeea3..3153f01 100644 --- a/src/views/product/ProductGroup.vue +++ b/src/views/product/ProductGroup.vue @@ -45,6 +45,14 @@ 列表视图 +
@@ -66,18 +74,17 @@ - + @@ -259,18 +274,19 @@
+
+ 添加子级时自动继承父级分组,不可修改 +
@@ -464,7 +483,7 @@ diff --git a/src/views/marketing/UserVoucher.vue b/src/views/marketing/UserVoucher.vue index 06f7cc6..2ed71fe 100644 --- a/src/views/marketing/UserVoucher.vue +++ b/src/views/marketing/UserVoucher.vue @@ -177,30 +177,36 @@ - +
-
- - {{ getSelectedUserName() }} - -
- - - {{ addForm.user_id ? '重新选择用户' : '选择用户' }} + + + + 清除
- + { ElMessage.warning('请选择一个用户') return } - addForm.user_id = user.UserId + addForm.user_id = user.user_id // 将选中的用户添加到 userOptions 中(如果不存在) - if (!userOptions.value.find(u => u.UserId === user.UserId)) { + if (!userOptions.value.find(u => u.user_id === user.user_id)) { userOptions.value.push(user) } userSelectorVisible.value = false @@ -668,8 +674,9 @@ const clearSelectedUser = () => { // 获取选中用户的显示名称 const getSelectedUserName = () => { - const user = userOptions.value.find(u => u.UserId === addForm.user_id) - return user ? `${user.UserName} (ID: ${user.UserId})` : `用户ID: ${addForm.user_id}` + if (!addForm.user_id) return '' + const user = userOptions.value.find(u => u.user_id === addForm.user_id) + return user ? `${user.user_name} (ID: ${user.user_id})` : `用户ID: ${addForm.user_id}` } // 添加用户代金券 @@ -940,5 +947,21 @@ onMounted(() => { margin-top: 24px; justify-content: flex-end; } + +/* 用户选择器样式 */ +.user-selector-wrapper { + display: flex; + align-items: center; + gap: 8px; + width: 100%; +} + +.user-selector-wrapper .el-input { + flex: 1; +} + +.user-selector-wrapper .clear-btn { + flex-shrink: 0; +} -- 2.52.0 From 4d45cf535e787fec99494145df8bb73ca508f0ca Mon Sep 17 00:00:00 2001 From: 2256907009 <2256907009@qq.com> Date: Thu, 5 Feb 2026 12:54:49 +0800 Subject: [PATCH 25/92] =?UTF-8?q?fix:=E4=BF=AE=E6=94=B9=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E5=80=BC=E7=9A=84=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/product/ProductList.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/product/ProductList.vue b/src/views/product/ProductList.vue index 2d39abd..99c1786 100644 --- a/src/views/product/ProductList.vue +++ b/src/views/product/ProductList.vue @@ -1524,7 +1524,7 @@ const handleEditParamValue = (row) => { attr_id: row.id, attr_name: row.name, attr_value: row.value || '', - attr_price: row.price || 0, + attr_price: row.price / 100 || 0, index: row.index || 0, attr_range: row.phase || 0, // API返回的字段是 phase range_type: row.rangeType || 'equal' -- 2.52.0 From fdc9db9a9c0eac86f2b9c310e20afbd8a550170c Mon Sep 17 00:00:00 2001 From: 2256907009 <2256907009@qq.com> Date: Thu, 5 Feb 2026 15:15:13 +0800 Subject: [PATCH 26/92] =?UTF-8?q?fix:=E5=95=86=E5=93=81=E5=A5=97=E9=A4=90?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=9B=BA=E5=AE=9A=E4=BB=B7=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/admin/product.js | 18 + src/views/product/ProductList.vue | 156 +++- 问题.MD | 1252 ++--------------------------- 3 files changed, 206 insertions(+), 1220 deletions(-) diff --git a/src/api/admin/product.js b/src/api/admin/product.js index 6f3c9df..7e41745 100644 --- a/src/api/admin/product.js +++ b/src/api/admin/product.js @@ -195,6 +195,24 @@ export const enableProductPlan = (data) => { }) } +/**禁用套餐固定价格 */ +export const disablePlanFixedPrice = (data) => { + return http2.post('/api/v1/admin/good/plan/disable_fixed_price', data,{ + headers:{ + 'Content-Type':'multipart/form-data' + } + }) +} + +/**启用套餐固定价格 */ +export const enablePlanFixedPrice = (data) => { + return http2.post('/api/v1/admin/good/plan/enable_fixed_price', data,{ + headers:{ + 'Content-Type':'multipart/form-data' + } + }) +} + /**---------------------------------- */ /**商品分组标签管理 */ diff --git a/src/views/product/ProductList.vue b/src/views/product/ProductList.vue index 99c1786..a93b271 100644 --- a/src/views/product/ProductList.vue +++ b/src/views/product/ProductList.vue @@ -520,13 +520,15 @@ :title="planFormType === 'add' ? '新增套餐' : '编辑套餐'" width="700px" append-to-body + class="plan-form-dialog" > - +
+ @@ -665,6 +667,22 @@ +
0 表示没有库存
+
+ + +
启用后套餐价格将使用固定价格,不再根据参数计算
+
+ + @@ -675,7 +693,8 @@ 禁用 -
+ +
@@ -482,7 +584,7 @@ import { ref, reactive, onMounted, watch, nextTick, computed } from 'vue' import { useRoute } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' -import { Search, Plus, Delete, UploadFilled, Folder, Document, ArrowRight, Loading } from '@element-plus/icons-vue' +import { Search, Plus, Delete, UploadFilled, Folder, Document, ArrowRight, Loading, Picture, Edit, Rank } from '@element-plus/icons-vue' import { getSettingGroupList, getSettingGroupInfo, @@ -497,6 +599,7 @@ import { deleteSetting } from '@/api/admin/setting' import { uploadFile } from '@/api/admin/file' +import ImageSelector from '@/components/admin/ImageSelector.vue' const route = useRoute() @@ -506,6 +609,14 @@ const treeData = ref([]) const allGroupList = ref([]) // 存储所有已加载的配置组 const treeDataMap = ref(new Map()) // 存储树形数据,key为group_id const selectedNode = ref(null) +const editableStringList = ref([]) // 弹窗中可编辑的字符串列表 +const draggedIndex = ref(-1) // 拖拽的索引 + +// 表单相关可编辑列表 +const editableFormStringList = ref([]) // 表单中可编辑的字符串列表 +const editableFormFileList = ref([]) // 表单中可编辑的文件列表 +const formDraggedIndex = ref(-1) // 表单拖拽索引 +const formFileDraggedIndex = ref(-1) // 表单文件拖拽索引 // 查询参数 const queryParams = reactive({ @@ -556,7 +667,6 @@ const groupRules = { const groupLoading = ref(false) const groupList = ref([]) const groupTotal = ref(0) -const selectedRows = ref([]) const groupDialogVisible = ref(false) const groupDialogTitle = ref('新增配置组') const groupFormRef = ref(null) @@ -614,6 +724,12 @@ const fileListInfo = ref([]) const newStringItem = ref('') const imageViewerVisible = ref(false) const currentViewImage = ref('') +const imageSelectorVisible = ref(false) +const currentImageSelectorFileId = ref('') +const imageSelectorMode = ref('single') // 'single' 或 'list' + +// 批量选择相关 +const selectedRows = ref([]) // 格式化日期时间 const formatDate = (dateString) => { @@ -709,6 +825,11 @@ const loadGroups = async () => { // 节点点击事件 const handleNodeClick = (row) => { selectedNode.value = row + + // 如果是字符串列表类型,初始化可编辑列表 + if (row.data.type === 'string_list') { + initEditableStringList() + } } // 查询处理 @@ -964,6 +1085,17 @@ const handleEditSetting = async (row) => { } else { fileInfo.value = null } + } else if (data.type === 'string_list') { + // 处理字符串列表类型,对每个字符串进行截断 + if (data.parsedValue && Array.isArray(data.parsedValue)) { + const truncatedValues = data.parsedValue.map(item => truncateFileName(item, 25)) + settingForm.value = truncatedValues.join(',') + initEditableFormStringList() // 初始化表单可编辑字符串列表 + } else { + settingForm.value = '' + editableFormStringList.value = [] + } + newStringItem.value = '' } else if (data.type === 'file_list') { // 处理文件列表类型,使用parsedValue来获取文件信息 if (data.parsedValue && Array.isArray(data.parsedValue)) { @@ -977,11 +1109,14 @@ const handleEditSetting = async (row) => { size: 0 } }) + // 确保在设置fileListInfo后再初始化表单可编辑文件列表 + nextTick(() => { + initEditableFormFileList() + }) } else { fileListInfo.value = [] + editableFormFileList.value = [] } - } else if (data.type === 'string_list') { - newStringItem.value = '' } settingDialogVisible.value = true } @@ -1036,6 +1171,21 @@ const fetchAllGroupList = async () => { // 文件相关函数 const handleFileChange = async (file) => { fileUploading.value = true + + // 创建本地预览URL + const localUrl = URL.createObjectURL(file.raw) + const isImage = file.raw.type.startsWith('image/') + + // 立即显示本地预览 + fileInfo.value = { + id: '', // 暂时为空,上传成功后会更新 + url: isImage ? localUrl : '', + realName: file.name, + saveName: file.name, + size: file.size, + isLocal: true // 标记为本地文件 + } + try { const formData = new FormData() formData.append('file_names', file.name) @@ -1044,18 +1194,30 @@ const handleFileChange = async (file) => { formData.append('open_down','true') const res = await uploadFile(formData) - if (res.data.code === 200 && res.data.data.length > 0) { + if (res.data.code === 200 && res.data.data && res.data.data.length > 0) { const uploadedFile = res.data.data[0] - settingForm.value = String(uploadedFile.id) + settingForm.value = String(uploadedFile.id || '') - // 确保上传的文件URL也经过处理 + // 释放本地URL(暂时不释放,保留用于渲染) + // if (fileInfo.value?.isLocal) { + // URL.revokeObjectURL(fileInfo.value.url) + // } + + // 更新为服务器返回的文件信息,但保留本地URL用于渲染 fileInfo.value = { - ...uploadedFile, - url: processImageUrl(uploadedFile.url || uploadedFile.realName) + id: uploadedFile.id || '', + url: processImageUrl(uploadedFile.url || uploadedFile.realName || ''), + localUrl: fileInfo.value?.url || '', // 保留本地URL用于渲染 + realName: uploadedFile.realName || '文件', + saveName: uploadedFile.saveName || 'file', + size: uploadedFile.size || 0, + isLocal: false // 标记为已上传,但保留本地渲染 } ElMessage.success('文件上传成功') } else { + // 上传失败,保留本地预览,但显示错误消息 ElMessage.error(res.data.message || '文件上传失败') + // 注意:不清理本地预览,让用户可以重新上传或删除 } } catch (error) { console.error('文件上传失败:', error) @@ -1066,6 +1228,14 @@ const handleFileChange = async (file) => { } const clearFile = () => { + // 释放本地URL + if (fileInfo.value?.localUrl) { + URL.revokeObjectURL(fileInfo.value.localUrl) + } + if (fileInfo.value?.isLocal && fileInfo.value?.url) { + URL.revokeObjectURL(fileInfo.value.url) + } + settingForm.value = '' fileInfo.value = null } @@ -1078,6 +1248,462 @@ const formatFileSize = (bytes) => { return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] } +const truncateFileName = (fileName, maxLength = 25) => { + if (!fileName) return '' + if (fileName.length <= maxLength) return fileName + + const extension = fileName.includes('.') ? '.' + fileName.split('.').pop() : '' + const nameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.')) + + if (nameWithoutExt.length <= maxLength) { + return fileName + } + + const truncatedName = nameWithoutExt.substring(0, maxLength - 3) // 留3个字符给"...和扩展名" + console.log('111',truncatedName + '...' + extension) + return truncatedName + '...' + extension +} + +// ==================== 可编辑字符串列表功能 ==================== + +// 获取可编辑的字符串列表 +const getEditableStringList = () => { + if (!selectedNode.value || selectedNode.value.data.type !== 'string_list') { + return [] + } + return editableStringList.value +} + +// 初始化可编辑字符串列表 +const initEditableStringList = () => { + if (!selectedNode.value || selectedNode.value.data.type !== 'string_list') { + editableStringList.value = [] + return + } + + const stringItems = getStringList(selectedNode.value.data.value) + editableStringList.value = stringItems.map(item => ({ + value: item, + editing: false + })) +} + +// 开始拖拽 +const handleDragStart = (event, index) => { + draggedIndex.value = index + event.dataTransfer.effectAllowed = 'move' + event.dataTransfer.setData('text/html', event.target.outerHTML) + event.target.style.opacity = '0.5' +} + +// 拖拽放下 +const handleDrop = (event, dropIndex) => { + event.preventDefault() + const dragIndex = draggedIndex.value + + if (dragIndex === dropIndex) return + + // 重新排列数组 + const newList = [...editableStringList.value] + const [draggedItem] = newList.splice(dragIndex, 1) + newList.splice(dropIndex, 0, draggedItem) + + editableStringList.value = newList + draggedIndex.value = -1 + + // 更新selectedNode中的数据 + const updatedValues = newList.map(item => item.value) + selectedNode.value.data.value = updatedValues.join(',') + + event.target.style.opacity = '1' +} + +// 添加新字符串项目 +const addEditableStringItem = () => { + const newItem = { + value: '', + editing: true + } + editableStringList.value.push(newItem) + + // 更新selectedNode中的数据 + const updatedValues = editableStringList.value.map(item => item.value) + selectedNode.value.data.value = updatedValues.join(',') + + // 聚焦到新添加的输入框 + nextTick(() => { + const inputs = document.querySelectorAll('.string-list-item input') + if (inputs.length > 0) { + inputs[inputs.length - 1].focus() + } + }) +} + +// 开始编辑项目 +const startEditItem = (index) => { + editableStringList.value[index].editing = true + + nextTick(() => { + const inputs = document.querySelectorAll('.string-list-item input') + if (inputs[index]) { + inputs[index].focus() + inputs[index].select() + } + }) +} + +// 完成编辑项目 +const finishEditItem = (index) => { + editableStringList.value[index].editing = false + + // 更新selectedNode中的数据 + const updatedValues = editableStringList.value.map(item => item.value) + selectedNode.value.data.value = updatedValues.join(',') +} + +// 删除字符串项目 +const removeEditableStringItem = (index) => { + ElMessageBox.confirm('确定要删除这个项目吗?', '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + }).then(() => { + editableStringList.value.splice(index, 1) + + // 更新selectedNode中的数据 + const updatedValues = editableStringList.value.map(item => item.value) + selectedNode.value.data.value = updatedValues.join(',') + + ElMessage.success('删除成功') + }) +} + +// 聚焦输入框 +const focusEditInput = () => { + // 这个方法会被 @mounted 调用 +} + +// ==================== 表单可编辑字符串列表功能 ==================== + +// 获取表单可编辑的字符串列表 +const getEditableFormStringList = () => { + return editableFormStringList.value +} + +// 初始化表单可编辑字符串列表 +const initEditableFormStringList = () => { + const stringItems = getStringList(settingForm.value) + editableFormStringList.value = stringItems.map(item => ({ + value: item, + editing: false + })) +} + +// 开始表单拖拽 +const handleFormDragStart = (event, index) => { + formDraggedIndex.value = index + event.dataTransfer.effectAllowed = 'move' + event.dataTransfer.setData('text/html', event.target.outerHTML) + event.target.style.opacity = '0.5' +} + +// 表单拖拽放下 +const handleFormDrop = (event, dropIndex) => { + event.preventDefault() + const dragIndex = formDraggedIndex.value + + if (dragIndex === dropIndex) return + + // 重新排列数组 + const newList = [...editableFormStringList.value] + const [draggedItem] = newList.splice(dragIndex, 1) + newList.splice(dropIndex, 0, draggedItem) + + editableFormStringList.value = newList + formDraggedIndex.value = -1 + + // 更新表单值 + const updatedValues = newList.map(item => item.value) + settingForm.value = updatedValues.join(',') + + event.target.style.opacity = '1' +} + +// 添加表单新字符串项目 +const addFormEditableStringItem = () => { + const newItem = { + value: '', + editing: true + } + editableFormStringList.value.push(newItem) + + // 更新表单值 + const updatedValues = editableFormStringList.value.map(item => item.value) + settingForm.value = updatedValues.join(',') + + // 聚焦到新添加的输入框 + nextTick(() => { + const inputs = document.querySelectorAll('.string-list-item input') + if (inputs.length > 0) { + inputs[inputs.length - 1].focus() + } + }) +} + +// 开始编辑表单项目 +const startFormEditItem = (index) => { + editableFormStringList.value[index].editing = true + + nextTick(() => { + const inputs = document.querySelectorAll('.string-list-item input') + if (inputs[index]) { + inputs[index].focus() + inputs[index].select() + } + }) +} + +// 完成编辑表单项目 +const finishFormEditItem = (index) => { + editableFormStringList.value[index].editing = false + + // 更新表单值 + const updatedValues = editableFormStringList.value.map(item => item.value) + settingForm.value = updatedValues.join(',') +} + +// 删除表单字符串项目 +const removeFormEditableStringItem = (index) => { + ElMessageBox.confirm('确定要删除这个项目吗?', '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + }).then(() => { + editableFormStringList.value.splice(index, 1) + + // 更新表单值 + const updatedValues = editableFormStringList.value.map(item => item.value) + settingForm.value = updatedValues.join(',') + + ElMessage.success('删除成功') + }) +} + +// 聚焦表单输入框 +const focusFormEditInput = () => { + // 这个方法会被 @mounted 调用 +} + +// ==================== 表单可编辑文件列表功能 ==================== + +// 获取表单可编辑的文件列表 +const getEditableFormFileList = () => { + return editableFormFileList.value +} + +// 初始化表单可编辑文件列表 +const initEditableFormFileList = () => { + if (settingForm.type === 'file_list' && fileListInfo.value && fileListInfo.value.length > 0) { + editableFormFileList.value = fileListInfo.value.map(file => ({ + id: file.id || '', + url: file.url || '', + localUrl: file.localUrl || '', // 保留本地URL字段 + realName: file.realName || '文件', + saveName: file.saveName || 'file', + size: file.size || 0, + uploading: false + })) + } else { + editableFormFileList.value = [] + } +} + +// 开始表单文件拖拽 +const handleFormFileDragStart = (event, index) => { + formFileDraggedIndex.value = index + event.dataTransfer.effectAllowed = 'move' + event.dataTransfer.setData('text/html', event.target.outerHTML) + event.target.style.opacity = '0.5' +} + +// 表单文件拖拽放下 +const handleFormFileDrop = (event, dropIndex) => { + event.preventDefault() + const dragIndex = formFileDraggedIndex.value + + if (dragIndex === dropIndex) return + + // 重新排列数组 + const newList = [...editableFormFileList.value] + const [draggedItem] = newList.splice(dragIndex, 1) + newList.splice(dropIndex, 0, draggedItem) + + editableFormFileList.value = newList + formFileDraggedIndex.value = -1 + + // 更新fileListInfo和表单值 + fileListInfo.value = newList + updateFormFileListValue() + + event.target.style.opacity = '1' +} + +// 表单文件列表变更 +const handleFormFileListChange = async (file) => { + // 创建本地预览URL + const localUrl = URL.createObjectURL(file.raw) + const isImage = file.raw.type.startsWith('image/') + + // 立即添加到表单文件列表 + const tempFile = { + id: '', // 暂时为空,上传成功后会更新 + url: isImage ? localUrl : '', + realName: file.name, + saveName: file.name, + size: file.size, + isLocal: true, + uploading: true + } + + editableFormFileList.value.push(tempFile) + + try { + const formData = new FormData() + formData.append('file_names', file.name) + formData.append('files', file.raw) + formData.append('update_type','cover') + formData.append('open_down','true') + + const res = await uploadFile(formData) + if (res.data.code === 200 && res.data.data && res.data.data.length > 0) { + const uploadedFile = res.data.data[0] + + // 找到对应的本地文件并更新 + const index = editableFormFileList.value.findIndex(f => f.isLocal && f.realName === file.name) + if (index !== -1) { + editableFormFileList.value[index] = { + id: uploadedFile.id || '', + url: processImageUrl(uploadedFile.url || uploadedFile.realName || ''), + localUrl: localUrl, // 保留本地URL用于渲染 + realName: uploadedFile.realName || '文件', + saveName: uploadedFile.saveName || 'file', + size: uploadedFile.size || 0, + isLocal: false, + uploading: false + } + } + + updateFormFileListValue() + ElMessage.success('文件上传成功') + } else { + // 上传失败,移除临时文件 + const index = editableFormFileList.value.findIndex(f => f.isLocal && f.realName === file.name) + if (index !== -1) { + editableFormFileList.value.splice(index, 1) + } + ElMessage.error(res.data.message || '文件上传失败') + } + } catch (error) { + console.error('文件上传失败:', error) + // 上传失败,移除临时文件 + const index = editableFormFileList.value.findIndex(f => f.isLocal && f.realName === file.name) + if (index !== -1) { + editableFormFileList.value.splice(index, 1) + } + ElMessage.error('文件上传失败') + } +} + +// 表单文件替换 +const handleFormFileReplace = async (file, index) => { + if (!editableFormFileList.value[index]) return + + // 创建本地预览URL + const localUrl = URL.createObjectURL(file.raw) + const isImage = file.raw.type.startsWith('image/') + + // 更新文件信息 + editableFormFileList.value[index] = { + ...editableFormFileList.value[index], + url: isImage ? localUrl : '', + realName: file.name, + saveName: file.name, + size: file.size, + isLocal: true, + uploading: true + } + + try { + const formData = new FormData() + formData.append('file_names', file.name) + formData.append('files', file.raw) + formData.append('update_type','cover') + formData.append('open_down','true') + + const res = await uploadFile(formData) + if (res.data.code === 200 && res.data.data && res.data.data.length > 0) { + const uploadedFile = res.data.data[0] + + // 更新文件信息 + editableFormFileList.value[index] = { + id: uploadedFile.id || '', + url: processImageUrl(uploadedFile.url || uploadedFile.realName || ''), + localUrl: localUrl, // 保留本地URL用于渲染 + realName: uploadedFile.realName || '文件', + saveName: uploadedFile.saveName || 'file', + size: uploadedFile.size || 0, + isLocal: false, + uploading: false + } + + updateFormFileListValue() + ElMessage.success('文件替换成功') + } else { + // 上传失败,恢复原文件信息 + ElMessage.error(res.data.message || '文件替换失败') + } + } catch (error) { + console.error('文件替换失败:', error) + ElMessage.error('文件替换失败') + } +} + +// 删除表单文件项目 +const removeFormEditableFileItem = (index) => { + ElMessageBox.confirm('确定要删除这个文件吗?', '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + }).then(() => { + // 释放本地URL + if (editableFormFileList.value[index].localUrl) { + URL.revokeObjectURL(editableFormFileList.value[index].localUrl) + } + if (editableFormFileList.value[index].isLocal && editableFormFileList.value[index].url) { + URL.revokeObjectURL(editableFormFileList.value[index].url) + } + + editableFormFileList.value.splice(index, 1) + + // 更新fileListInfo和表单值 + fileListInfo.value = editableFormFileList.value + updateFormFileListValue() + + ElMessage.success('删除成功') + }) +} + +// 更新表单文件列表值 +const updateFormFileListValue = () => { + if (!editableFormFileList.value || !Array.isArray(editableFormFileList.value)) { + return + } + + const fileIds = editableFormFileList.value.map(file => file.id).filter(id => id) + settingForm.value = fileIds.join(',') +} + +// 文件预览 const previewFile = (fileId) => { // 这里可以实现文件预览逻辑 console.log('预览文件:', fileId) @@ -1117,22 +1743,96 @@ const previewImage = (url) => { // 处理图片加载失败 const handleImageError = (event) => { // 图片加载失败时,可以隐藏图片或显示占位符 - console.log('图片加载失败:', event.target.src) + console.error('图片加载失败:', event.target.src) // 这里可以添加更多的错误处理逻辑 } +// ==================== 图像选择器相关 ==================== + +// 打开图像选择器(单个文件) +const openImageSelector = () => { + imageSelectorMode.value = 'single' + currentImageSelectorFileId.value = settingForm.value || '' + imageSelectorVisible.value = true +} + +// 打开图像选择器(文件列表) +const openImageSelectorForList = () => { + imageSelectorMode.value = 'list' + currentImageSelectorFileId.value = '' + imageSelectorVisible.value = true +} + +// 处理图像选择器确认 +const handleImageSelectorConfirm = (selectedFile) => { + if (!selectedFile || !selectedFile.id) { + ElMessage.warning('选择的文件无效') + return + } + + if (imageSelectorMode.value === 'single') { + // 单个文件模式 + settingForm.value = selectedFile.id + fileInfo.value = { + id: selectedFile.id, + url: processImageUrl(selectedFile.url || ''), + realName: selectedFile.realName || '文件', + saveName: selectedFile.realName || 'file', + size: selectedFile.size || 0 + } + } else if (imageSelectorMode.value === 'list') { + // 文件列表模式 + if (!fileListInfo.value) { + fileListInfo.value = [] + } + + const newFile = { + id: selectedFile.id, + url: processImageUrl(selectedFile.url || ''), + realName: selectedFile.realName || '文件', + saveName: selectedFile.realName || 'file', + size: selectedFile.size || 0 + } + fileListInfo.value.push(newFile) + updateFileListValue() + } + + imageSelectorVisible.value = false +} + const getFileList = (value) => { - if (!value) return [] - return value.split(',').filter(id => id.trim()) + if (!value || typeof value !== 'string') return [] + return value.split(',').filter(id => id && id.trim()) } const getStringList = (value) => { - if (!value) return [] - return value.split(',').filter(str => str.trim()) + if (!value || typeof value !== 'string') return [] + return value.split(',').filter(str => str && str.trim()) } const handleFileListChange = async (file) => { fileUploading.value = true + + // 创建本地预览URL + const localUrl = URL.createObjectURL(file.raw) + const isImage = file.raw.type.startsWith('image/') + + // 立即添加本地预览到列表 + if (!fileListInfo.value) { + fileListInfo.value = [] + } + + const tempFile = { + id: '', // 暂时为空,上传成功后会更新 + url: isImage ? localUrl : '', + realName: file.name, + saveName: file.name, + size: file.size, + isLocal: true // 标记为本地文件 + } + + fileListInfo.value.push(tempFile) + try { const formData = new FormData() formData.append('file_names', file.name) @@ -1141,24 +1841,41 @@ const handleFileListChange = async (file) => { formData.append('open_down','true') const res = await uploadFile(formData) - if (res.data.code === 200 && res.data.data.length > 0) { + if (res.data.code === 200 && res.data.data && res.data.data.length > 0) { const uploadedFile = res.data.data[0] const currentFileIds = getFileList(settingForm.value) - currentFileIds.push(String(uploadedFile.id)) + currentFileIds.push(String(uploadedFile.id || '')) settingForm.value = currentFileIds.join(',') - // 确保新上传的文件URL也经过处理 - const processedFile = { - ...uploadedFile, - url: processImageUrl(uploadedFile.url || uploadedFile.realName) + // 找到对应的本地文件并更新 + const index = fileListInfo.value.findIndex(f => f.isLocal && f.realName === file.name) + if (index !== -1) { + // 释放本地URL(暂时不释放,保留用于渲染) + // if (fileListInfo.value[index].isLocal) { + // URL.revokeObjectURL(fileListInfo.value[index].url) + // } + + // 更新为服务器返回的文件信息,但保留本地URL用于渲染 + fileListInfo.value[index] = { + id: uploadedFile.id || '', + url: processImageUrl(uploadedFile.url || uploadedFile.realName || ''), + localUrl: fileListInfo.value[index]?.url || '', // 保留本地URL用于渲染 + realName: uploadedFile.realName || '文件', + saveName: uploadedFile.saveName || 'file', + size: uploadedFile.size || 0, + isLocal: false // 标记为已上传,但保留本地渲染 + } } - fileListInfo.value.push(processedFile) + + updateFileListValue() ElMessage.success('文件上传成功') } else { + // 上传失败,不清理本地预览,让用户可以重新上传或删除 ElMessage.error(res.data.message || '文件上传失败') } } catch (error) { console.error('文件上传失败:', error) + // 上传失败,不清理本地预览,让用户可以重新上传或删除 ElMessage.error('文件上传失败') } finally { fileUploading.value = false @@ -1166,12 +1883,34 @@ const handleFileListChange = async (file) => { } const removeFile = (index) => { - const currentFileIds = getFileList(settingForm.value) + if (!fileListInfo.value || index < 0 || index >= fileListInfo.value.length) { + return + } + + // 释放本地URL + if (fileListInfo.value[index].localUrl) { + URL.revokeObjectURL(fileListInfo.value[index].localUrl) + } + if (fileListInfo.value[index].isLocal && fileListInfo.value[index].url) { + URL.revokeObjectURL(fileListInfo.value[index].url) + } + + const currentFileIds = getFileList(settingForm.value) || [] currentFileIds.splice(index, 1) settingForm.value = currentFileIds.join(',') fileListInfo.value.splice(index, 1) } +// 更新文件列表值 +const updateFileListValue = () => { + if (!fileListInfo.value || !Array.isArray(fileListInfo.value)) { + return + } + + const fileIds = fileListInfo.value.map(file => file.id).filter(id => id) + settingForm.value = fileIds.join(',') +} + const addStringItem = () => { if (newStringItem.value.trim()) { const currentItems = getStringList(settingForm.value) @@ -1324,6 +2063,8 @@ onMounted(() => { /* 文件预览样式 */ .file-preview { margin-top: 8px; + position: relative; + flex-shrink: 0; } .preview-image { @@ -1356,10 +2097,6 @@ onMounted(() => { gap: 12px; } -.file-preview { - flex-shrink: 0; -} - .file-placeholder { width: 80px; height: 80px; @@ -1662,6 +2399,45 @@ onMounted(() => { width: 100%; } +.file-upload-options { + width: 100%; +} + +.upload-methods { + display: flex; + flex-direction: column; + gap: 16px; + align-items: center; +} + +.divider { + position: relative; + text-align: center; + color: #909399; + font-size: 14px; + margin: 8px 0; +} + +.divider::before { + content: ''; + position: absolute; + top: 50%; + left: 0; + right: 0; + height: 1px; + background: #dcdfe6; +} + +.divider { + background: #fff; + padding: 0 16px; +} + +.image-selector-btn { + width: 200px; + height: 40px; +} + :deep(.file-uploader .el-upload) { width: 100%; } @@ -1764,6 +2540,37 @@ onMounted(() => { color: #409eff; } +/* 上传状态指示器样式 */ +.file-item.uploading { + border-color: #409eff; + background-color: #f0f9ff; +} + +.upload-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.6); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: white; + font-size: 12px; + border-radius: 4px; +} + +.upload-overlay .el-icon { + font-size: 20px; + margin-bottom: 4px; +} + +.upload-overlay span { + font-size: 11px; +} + /* 字符串列表相关样式 */ .string-list-section { width: 100%; @@ -1823,6 +2630,212 @@ onMounted(() => { display: inline-block; } +/* 可编辑字符串列表样式 */ +.editable-string-list { + border: 1px solid #e4e7ed; + border-radius: 4px; + padding: 16px; + background-color: #fafafa; +} + +.string-list-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + padding-bottom: 8px; + border-bottom: 1px solid #ebeef5; +} + +.list-title { + font-weight: 500; + color: #303133; + font-size: 14px; +} + +.string-list-items { + display: flex; + flex-direction: column; + gap: 8px; +} + +.string-list-item { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 12px; + background-color: #ffffff; + border: 1px solid #e4e7ed; + border-radius: 4px; + transition: all 0.3s ease; + cursor: move; +} + +.string-list-item:hover { + border-color: #c0c4cc; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.drag-handle { + color: #909399; + cursor: grab; + flex-shrink: 0; +} + +.drag-handle:hover { + color: #606266; +} + +.item-content { + flex: 1; + min-width: 0; +} + +.item-text { + padding: 4px 8px; + border-radius: 4px; + transition: background-color 0.3s ease; + cursor: pointer; + word-break: break-word; + white-space: normal; + line-height: 1.4; +} + +.item-text:hover { + background-color: #f5f7fa; +} + +.item-actions { + display: flex; + gap: 4px; + flex-shrink: 0; +} + +.danger-btn { + color: #f56c6c; +} + +.danger-btn:hover { + color: #f78989; +} + +/* 可编辑文件列表样式 */ +.editable-file-list { + border: 1px solid #e4e7ed; + border-radius: 4px; + padding: 16px; + background-color: #fafafa; +} + +.file-list-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + padding-bottom: 8px; + border-bottom: 1px solid #ebeef5; +} + +.header-actions { + display: flex; + gap: 8px; +} + +.file-list-items { + display: flex; + flex-direction: column; + gap: 12px; +} + +.file-list-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px; + background-color: #ffffff; + border: 1px solid #e4e7ed; + border-radius: 4px; + transition: all 0.3s ease; + cursor: move; +} + +.file-list-item:hover { + border-color: #c0c4cc; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.file-list-item .file-preview { + width: 60px; + height: 60px; + border-radius: 4px; + overflow: hidden; + border: 1px solid #e4e7ed; + flex-shrink: 0; + position: relative; +} + +.file-list-item .preview-image { + width: 100%; + height: 100%; + object-fit: cover; + cursor: pointer; +} + +.file-list-item .file-placeholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background-color: #f5f7fa; + color: #909399; +} + +.file-list-item .file-info { + flex: 1; + min-width: 0; +} + +.file-list-item .file-name { + font-weight: 500; + color: #303133; + margin-bottom: 4px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.file-list-item .file-id { + font-size: 12px; + color: #909399; + margin-bottom: 2px; +} + +.file-list-item .file-size { + font-size: 12px; + color: #909399; +} + +.file-list-item .file-actions { + display: flex; + gap: 4px; + flex-shrink: 0; +} + +/* 弹窗中值内容的省略号样式 */ +.value-content { + max-width: 100%; +} + +.value-content .text-value { + display: block; + max-width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + word-break: break-all; +} + /* 响应式调整 */ @media (max-width: 768px) { .file-name { -- 2.52.0 From 86f3835e51a5890491042ed4353c19cf0a031098 Mon Sep 17 00:00:00 2001 From: 2256907009 <2256907009@qq.com> Date: Wed, 11 Mar 2026 10:27:29 +0800 Subject: [PATCH 36/92] =?UTF-8?q?style:=20=E8=A1=A8=E6=A0=BC=E5=80=BC?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E7=9A=84list=E7=B1=BB=E5=9E=8B=E5=8F=AA?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=E6=96=87=E4=BB=B6=E6=95=B0=EF=BC=8C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0note=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/system/SettingManage.vue | 266 +++++++++++++++++++++++++---- 1 file changed, 237 insertions(+), 29 deletions(-) diff --git a/src/views/system/SettingManage.vue b/src/views/system/SettingManage.vue index 6677a7d..cfa5726 100644 --- a/src/views/system/SettingManage.vue +++ b/src/views/system/SettingManage.vue @@ -89,31 +89,19 @@ - -
- - {{ truncateFileName(`文件${index + 1}: ${fileId}`, 25) }} - +
+ + + 共 {{ getFileList(row.data.value).length }} 个文件 +
- -
- - {{ truncateFileName(item, 25) }} +
+ + + 共 {{ getStringList(row.data.value).length }} 条字符串
- @@ -138,6 +126,13 @@ {{ formatDate(row.data.CreatedAt) }} + + + @@ -757,6 +755,73 @@ const formatDate = (dateString) => { // ==================== 树状图方法 ==================== +// 相近字排序函数 +const sortBySimilarity = (settings) => { + if (!settings || settings.length <= 1) return settings + + // 提取所有配置项的名称 + const names = settings.map(item => item.name || '') + + // 按名称排序,相近的名称会排在一起 + return settings.sort((a, b) => { + const nameA = (a.name || '').toLowerCase() + const nameB = (b.name || '').toLowerCase() + + // 先按前缀分组排序 + const prefixA = nameA.substring(0, 2) + const prefixB = nameB.substring(0, 2) + + if (prefixA !== prefixB) { + return prefixA.localeCompare(prefixB, 'zh-CN') + } + + // 前缀相同则按完整名称排序 + return nameA.localeCompare(nameB, 'zh-CN') + }) +} + +// 加载配置项子节点 +const loadChildren = async (row) => { + if (!row || row.type !== 'group') return + + row._loading = true + try { + const groupId = row.data.id + console.log('Loading children for group:', groupId) + const res = await getSettingList({ group_id: groupId, page: 1, count: 100 }) + console.log('getSettingList response:', res.data) + if (res.data.code === 200) { + let settings = res.data.data.data || [] + console.log('settings loaded:', settings) + + // 应用相近字排序 + settings = sortBySimilarity(settings) + console.log('settings after sorting:', settings) + + const settingNodes = settings.map(setting => ({ + id: `setting_${setting.id}`, + label: setting.name, + type: 'setting', + data: setting, + level: row.level + 1, + hasChildren: false, // 配置项没有子节点 + _expanded: false, + _children: [], + _loading: false + })) + console.log('settingNodes created:', settingNodes) + + row._children = settingNodes + console.log('updated row with children:', row) + } + } catch (error) { + console.error('加载配置列表失败:', error) + ElMessage.error('加载配置列表失败') + } finally { + row._loading = false + } +} + // 切换展开状态 const toggleExpand = async (row) => { if (row._loading) return @@ -767,37 +832,7 @@ const toggleExpand = async (row) => { } else { // 展开 - 如果还没加载子级,先加载 if (row.type === 'group' && (!row._children || row._children.length === 0)) { - row._loading = true - try { - const groupId = row.data.id - console.log('Loading children for group:', groupId) - const res = await getSettingList({ group_id: groupId, page: 1, count: 100 }) - console.log('getSettingList response:', res.data) - if (res.data.code === 200) { - const settings = res.data.data.data || [] - console.log('settings loaded:', settings) - const settingNodes = settings.map(setting => ({ - id: `setting_${setting.id}`, - label: setting.name, - type: 'setting', - data: setting, - level: row.level + 1, - hasChildren: false, // 配置项没有子节点 - _expanded: false, - _children: [], - _loading: false - })) - console.log('settingNodes created:', settingNodes) - - row._children = settingNodes - console.log('updated row with children:', row) - } - } catch (error) { - console.error('加载配置列表失败:', error) - ElMessage.error('加载配置列表失败') - } finally { - row._loading = false - } + await loadChildren(row) } row._expanded = true } @@ -2085,10 +2120,6 @@ onMounted(() => { flex: 1; } -.type-tag { - margin-left: 8px; -} - /* 文件预览样式 */ .file-preview { margin-top: 8px; @@ -2357,11 +2388,6 @@ onMounted(() => { font-size: 14px; } - .type-tag { - margin-left: 4px; - font-size: 11px; - } - :deep(.setting-tree-table .el-table th) { font-size: 12px; padding: 8px 4px; diff --git a/问题.MD b/问题.MD index 8287973..7b3e6c2 100644 --- a/问题.MD +++ b/问题.MD @@ -1,8 +1,9 @@ ✅已完成、⚠️部分完成、❌未完成这样显示 -----------------------------------------------------------------------------------------------需要解决 -1.将配置组管理和配置管理放到一起,弄成树状图这种,顶级是配置组管理,点击配置组管理的下级是配置管理列表数据,需要将两个合作一个,看如何能融合以下 +1.管理员后台在配置管理展示的类型,有两行都是相同的,只需要展示一行 ✅已完成 +2.,需要做相近字排序,比如前缀都是轮播图的展示在一起这样方便后续修改理解意思,还有是运用在同一处地方的可以展示在一起 ✅已完成 -----------------------------------------------------------------------------------------------需要解决 -- 2.52.0 From 3e751d4c42e388529ed4d271b731496cdcd98599 Mon Sep 17 00:00:00 2001 From: 2256907009 <2256907009@qq.com> Date: Wed, 11 Mar 2026 18:58:01 +0800 Subject: [PATCH 40/92] =?UTF-8?q?style:=20=E4=BF=AE=E6=94=B9=E5=80=BC?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E7=9A=84=E6=96=87=E5=AD=97=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/system/SettingManage.vue | 113 ++++++++++++++++------------- 问题.MD | 14 ++++ 2 files changed, 76 insertions(+), 51 deletions(-) diff --git a/src/views/system/SettingManage.vue b/src/views/system/SettingManage.vue index 3b69a68..4a2a1de 100644 --- a/src/views/system/SettingManage.vue +++ b/src/views/system/SettingManage.vue @@ -75,20 +75,20 @@
- + @@ -371,8 +370,8 @@
-
- +
+
@@ -380,7 +379,7 @@
-
+
@@ -397,33 +396,15 @@
+ + 修改图片 + 删除
- - -
- 将文件拖到此处,或点击上传 -
- -
- -
- 从文件库选择 - - - 上传文件 - -
@@ -493,14 +464,7 @@
{{ formatFileSize(fileInfo.size) }}
- - - +
@@ -1809,6 +1773,13 @@ const openImageSelectorForList = () => { imageSelectorVisible.value = true } +// 打开图像选择器(文件列表中的特定项) +const openImageSelectorForListItem = (index) => { + imageSelectorMode.value = 'list-item' + currentImageSelectorFileId.value = index + imageSelectorVisible.value = true +} + // 处理图像选择器确认 const handleImageSelectorConfirm = (selectedFile) => { if (!selectedFile || !selectedFile.id) { @@ -1841,6 +1812,20 @@ const handleImageSelectorConfirm = (selectedFile) => { } fileListInfo.value.push(newFile) updateFileListValue() + } else if (imageSelectorMode.value === 'list-item') { + // 文件列表中的特定项替换模式 + const index = currentImageSelectorFileId.value + if (fileListInfo.value && fileListInfo.value[index] !== undefined) { + fileListInfo.value[index] = { + id: selectedFile.id, + url: processImageUrl(selectedFile.url || ''), + realName: selectedFile.realName || '文件', + saveName: selectedFile.realName || 'file', + size: selectedFile.size || 0 + } + updateFileListValue() + ElMessage.success('文件替换成功') + } } imageSelectorVisible.value = false @@ -1856,6 +1841,12 @@ const getStringList = (value) => { return value.split(',').filter(str => str && str.trim()) } +const truncateText = (text, maxLength = 30) => { + if (!text) return '' + if (text.length <= maxLength) return text + return text.substring(0, maxLength) + '...' +} + const handleFileListChange = async (file) => { fileUploading.value = true @@ -2107,10 +2098,18 @@ onMounted(() => { height: 20px; } +.tree-node { + display: flex; + align-items: center; + gap: 8px; +} + .tree-icon { color: #409eff; font-size: 16px; flex-shrink: 0; + display: flex; + align-items: center; } .tree-label { @@ -2118,6 +2117,17 @@ onMounted(() => { color: #303133; font-size: 14px; flex: 1; + line-height: 16px; /* 与图标高度保持一致 */ +} + +/* 表格中文字值样式 */ +.text-value { + display: inline-block; + max-width: 200px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: middle; } /* 文件预览样式 */ @@ -2382,6 +2392,7 @@ onMounted(() => { .tree-label { font-size: 13px; + line-height: 14px; /* 移动端调整行高 */ } .tree-icon { diff --git a/问题.MD b/问题.MD index 7b3e6c2..c3d5695 100644 --- a/问题.MD +++ b/问题.MD @@ -5,6 +5,20 @@ 2.,需要做相近字排序,比如前缀都是轮播图的展示在一起这样方便后续修改理解意思,还有是运用在同一处地方的可以展示在一起 ✅已完成 +3.配置管理后台:表格table名称列的图标向下与文字保持平行 ✅已完成 + +4.table的值字段浮动不展示tip,并且点击可以打开对应的编辑弹窗 ✅已完成 + +5.将编辑配置的设置值的上传文件按钮进行删除,并且点击编辑修改文件图标打开的还得是imageSelector组件 ✅已完成 + +6.点击编辑配置的的修改图片的按钮,应该打开imageSelector.vue组件进行替换图片 ✅已完成 + +7.配置管理后台:表格table名称列的图标向下与文字保持平行,需要保持在同一行,只需要图标向下移动一点与文字保持水平 ✅已完成 + +8.现在这种不好看,需要图标在左边,文字在右边 ✅已完成 + +9.中间这个值列表,当是文字的时候,只展示一行,在配置管理的table表格中 ✅已完成 + -----------------------------------------------------------------------------------------------需要解决 -- 2.52.0 From d650bfeb6156bcfcdd4c12a530044f58c3af6a0f Mon Sep 17 00:00:00 2001 From: 2256907009 <2256907009@qq.com> Date: Fri, 13 Mar 2026 14:07:09 +0800 Subject: [PATCH 41/92] =?UTF-8?q?style:=20=E5=AF=B9=E5=95=86=E5=93=81?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E9=A1=B5=E9=9D=A2=E8=BF=9B=E8=A1=8C=E5=90=8D?= =?UTF-8?q?=E7=A7=B0=E6=A0=B7=E5=BC=8F=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 1 + src/api/admin/product-test.js | 79 -- src/config/env.js | 55 + src/config/menus.js | 8 +- src/router/index.js | 23 +- src/utils/request.js | 48 +- src/views/product/ProductGroup.vue | 1851 +++++++++++++++++++++++++++- src/views/user/UserDetail.vue | 20 +- vite.config.js | 67 +- 问题.MD | 221 ---- 10 files changed, 1934 insertions(+), 439 deletions(-) create mode 100644 .env delete mode 100644 src/api/admin/product-test.js create mode 100644 src/config/env.js delete mode 100644 问题.MD diff --git a/.env b/.env new file mode 100644 index 0000000..7a30cbc --- /dev/null +++ b/.env @@ -0,0 +1 @@ +VITE_API_BASE_URL='https://apiservertest.s1f.ren' \ No newline at end of file diff --git a/src/api/admin/product-test.js b/src/api/admin/product-test.js deleted file mode 100644 index 1d1c49b..0000000 --- a/src/api/admin/product-test.js +++ /dev/null @@ -1,79 +0,0 @@ -// 商品管理 API 接口测试文件 -// 此文件用于验证所有接口是否正确对接 OpenAPI 文档 - -import { - // 商品分组管理 - getProductGroupList, - createProductGroup, - updateProductGroup, - hideProductGroup, - startProductGroup, - deleteProductGroup, - - // 商品管理 - getProductList, - getProductTagList, - createProduct, - updateProduct, - deleteProduct, - - // 商品参数管理 - getProductParameterList, - createProductParameter, - getProductParameterDetail, - updateProductParameter, - deleteProductParameter, - addProductParameterValue, - deleteProductParameterValue, - updateProductParameterValue -} from './product' - -/** - * 商品管理 API 接口对接验证 - * - * 根据 OpenAPI 文档,所有接口已完整对接: - * - * 1. 商品分组管理 (6个接口) - * ✅ GET /api/v1/admin/good/group/list - 获取商品分组列表 - * ✅ POST /api/v1/admin/good/group/create - 创建商品分组 - * ✅ POST /api/v1/admin/good/group/update - 更新商品分组 - * ✅ POST /api/v1/admin/good/group/disable - 隐藏商品组 - * ✅ POST /api/v1/admin/good/group/enable - 启用商品组 - * ✅ DELETE /api/v1/admin/good/group/delete - 删除商品分组 - * - * 2. 商品管理 (4个接口) - * ✅ GET /api/v1/admin/good/goods/list - 获取商品列表 - * ✅ GET /api/v1/admin/good/goods/tag_list - 获取商品标签列表 - * ✅ POST /api/v1/admin/good/goods/create - 创建商品 - * ✅ POST /api/v1/admin/good/goods/update - 更新商品 - * ✅ DELETE /api/v1/admin/good/goods/delete - 删除商品 - * - * 3. 商品参数管理 (8个接口) - * ✅ GET /api/v1/admin/good/spec/list - 获取商品参数列表 - * ✅ POST /api/v1/admin/good/spec/create - 创建商品参数 - * ✅ GET /api/v1/admin/good/spec/detail - 获取商品参数详情 - * ✅ POST /api/v1/admin/good/spec/update - 更新商品参数 - * ✅ DELETE /api/v1/admin/good/spec/delete - 删除商品参数 - * ✅ POST /api/v1/admin/good/spec/add_value - 增加商品参数值 - * ✅ DELETE /api/v1/admin/good/spec/delete_value - 删除商品参数值 - * ✅ POST /api/v1/admin/good/spec/update_value - 更新商品参数值 - * - * 总计:18个接口全部对接完成 - * - * 页面实现状态: - * ✅ ProductList.vue - 商品列表管理页面(包含商品参数管理) - * ✅ ProductGroup.vue - 商品分组管理页面 - * - * 注意事项: - * 1. 所有 POST/DELETE 接口使用 multipart/form-data 格式 - * 2. 更新商品参数接口使用 query 参数而非 body - * 3. 价格字段以分为单位存储 - * 4. 商品标签从 tag_list 接口获取 - */ - -export const API_STATUS = { - totalApis: 18, - implementedApis: 18, - completionRate: '100%', - lastUpdated: new Date().toISOString() -} \ No newline at end of file diff --git a/src/config/env.js b/src/config/env.js new file mode 100644 index 0000000..b91a842 --- /dev/null +++ b/src/config/env.js @@ -0,0 +1,55 @@ +/** + * 环境配置文件 + * 所有硬编码的 URL / 域名 / 环境变量统一在此管理 + */ + +// 当前环境 +const isDevelopment = import.meta.env.MODE === 'development' + +// API 基础地址 +// 开发环境使用 vite 代理 (baseUrl 为空),生产环境使用实际地址 +const API_BASE_MAP = { + development: '', // 开发环境通过 vite proxy 代理 + production: import.meta.env.VITE_API_BASE_URL || 'https://cloudapi.007yjs.com', + staging: import.meta.env.VITE_API_BASE_URL || 'https://apiservertest.s1f.ren' +} + +// 获取当前环境的 API 基础地址 +const currentEnv = import.meta.env.VITE_APP_ENV || import.meta.env.MODE || 'development' +export const baseUrl = API_BASE_MAP[currentEnv] || API_BASE_MAP.development + +// ACS 服务基础地址 +export const acsBaseUrl = isDevelopment ? '' : baseUrl + +// 网站标题 +export const siteTitle = '007UI管理系统' + +// 请求超时时间(毫秒) +export const requestTimeout = 50000 +export const acsRequestTimeout = 30000 + +// Token 存储键名 +export const TOKEN_KEY = 'token' +export const TOKEN_EXPIRE_KEY = 'tokenExpire' +export const USER_INFO_KEY = 'userInfo' + +// 不需要 token 认证的 URL 前缀 +export const noAuthUrls = [ + '/v1/user/login', + '/v1/user/check/get_code_img', + '/v1/user/register', + '/v1/user/refresh_token' +] + +export default { + isDevelopment, + baseUrl, + acsBaseUrl, + siteTitle, + requestTimeout, + acsRequestTimeout, + TOKEN_KEY, + TOKEN_EXPIRE_KEY, + USER_INFO_KEY, + noAuthUrls +} diff --git a/src/config/menus.js b/src/config/menus.js index dbb2875..87c1f83 100644 --- a/src/config/menus.js +++ b/src/config/menus.js @@ -40,12 +40,8 @@ export const menus = [ icon: 'Goods', children: [ { - path: '/product/list', - title: '商品列表' - }, - { - path: '/product/group', - title: '商品分组' + path: '/product/manage', + title: '商品管理' } ] }, diff --git a/src/router/index.js b/src/router/index.js index c4c0daa..0f8db57 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -224,7 +224,7 @@ const routes = [ } ] }, - // 商品管理路由 + // 商品管理路由(已合并商品列表和商品分组到统一树形视图) { path: 'product', name: 'Product', @@ -232,23 +232,24 @@ const routes = [ title: '商品管理', icon: 'Goods' }, - redirect: '/product/list', + redirect: '/product/manage', children: [ { - path: 'list', - name: 'ProductList', - component: () => import('../views/product/ProductList.vue'), + path: 'manage', + name: 'ProductManage', + component: () => import('../views/product/ProductGroup.vue'), meta: { - title: '商品列表' + title: '商品管理' } }, + { + // 保留旧路由兼容,重定向到新路由 + path: 'list', + redirect: '/product/manage' + }, { path: 'group', - name: 'ProductGroup', - component: () => import('../views/product/ProductGroup.vue'), - meta: { - title: '商品分组' - } + redirect: '/product/manage' } ] }, diff --git a/src/utils/request.js b/src/utils/request.js index 7a39598..2ea6943 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -2,23 +2,17 @@ import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' import {getRefreshToken,refreshAccessToken} from "@/api/login.js"; - -// 基础URL -const baseUrl = 'https://apiservertest.s1f.ren' // SSL证书有问题 -// const baseUrl = 'http://apiservertest.s1f.ren' // HTTP版本 -// const baseUrl = 'https://cloudapi.007yjs.com' // 尝试备用地址 +import { baseUrl, acsBaseUrl, noAuthUrls as noAuthUrlList, requestTimeout, acsRequestTimeout, TOKEN_KEY, TOKEN_EXPIRE_KEY, USER_INFO_KEY } from '@/config/env.js' // 检查URL是否需要认证 const urlNeedAuth = (url) => { - // 这里可以添加不需要认证的URL列表 - const noAuthUrls = ['/v1/user/login', '/v1/user/check/get_code_img', '/v1/user/register', '/v1/user/refresh_token'] - return !noAuthUrls.some(noAuthUrl => url.includes(noAuthUrl)) + return !noAuthUrlList.some(noAuthUrl => url.includes(noAuthUrl)) } // 检查token是否过期 const isTokenExpired = () => { - const token = localStorage.getItem('token') - const expire = localStorage.getItem('tokenExpire') + const token = localStorage.getItem(TOKEN_KEY) + const expire = localStorage.getItem(TOKEN_EXPIRE_KEY) if (!token) return true // 检查过期时间 @@ -34,7 +28,7 @@ const isTokenExpired = () => { // 检查token是否即将过期(5分钟内) const isTokenExpiringSoon = () => { - const expire = localStorage.getItem('tokenExpire') + const expire = localStorage.getItem(TOKEN_EXPIRE_KEY) if (!expire) return false const expireTime = parseInt(expire) * 1000 // 转换为毫秒 @@ -81,26 +75,26 @@ const doRefreshToken = async () => { if (newTokenRes.data?.code === 200 && newTokenRes.data?.data?.token) { const { token, expire } = newTokenRes.data.data - localStorage.setItem('token', token) + localStorage.setItem(TOKEN_KEY, token) if (expire) { - localStorage.setItem('tokenExpire', expire.toString()) + localStorage.setItem(TOKEN_EXPIRE_KEY, expire.toString()) } return token } } // 刷新失败,触发登出逻辑 - localStorage.removeItem('token') - localStorage.removeItem('tokenExpire') - localStorage.removeItem('userInfo') + localStorage.removeItem(TOKEN_KEY) + localStorage.removeItem(TOKEN_EXPIRE_KEY) + localStorage.removeItem(USER_INFO_KEY) ElMessage.warning('登录过期,请重新登录') router.push('/login') return null } catch (error) { console.error('Token刷新失败:', error) // 刷新失败,触发登出逻辑 - localStorage.removeItem('token') - localStorage.removeItem('tokenExpire') - localStorage.removeItem('userInfo') + localStorage.removeItem(TOKEN_KEY) + localStorage.removeItem(TOKEN_EXPIRE_KEY) + localStorage.removeItem(USER_INFO_KEY) ElMessage.warning('登录过期,请重新登录') router.push('/login') return null @@ -121,7 +115,7 @@ class Request { (config) => { // 在发送请求之前做些什么 // 例如:添加 token - const token = localStorage.getItem('token') + const token = localStorage.getItem(TOKEN_KEY) if (token) { config.headers.Authorization = `Bearer ${token}` } @@ -190,7 +184,7 @@ class Request { // 创建默认实例 const request = new Request({ baseURL: baseUrl, - timeout: 50000, + timeout: requestTimeout, headers: { 'Content-Type': 'multipart/form-data' } @@ -201,21 +195,21 @@ export const baseURL = baseUrl export const http2 = axios.create({ baseURL: baseUrl, - timeout: 30000, + timeout: acsRequestTimeout, headers: {}, }); http2.interceptors.request.use(async config => { - const token = localStorage.getItem('token') + const token = localStorage.getItem(TOKEN_KEY) // 检查是否需要认证 if (urlNeedAuth(config.url)) { // 检查token是否已过期 if (isTokenExpired()) { if (token) { - localStorage.removeItem('token') - localStorage.removeItem('tokenExpire') - localStorage.removeItem('userInfo') + localStorage.removeItem(TOKEN_KEY) + localStorage.removeItem(TOKEN_EXPIRE_KEY) + localStorage.removeItem(USER_INFO_KEY) ElMessage.warning('登录过期,请重新登录') } router.push('/login') @@ -275,7 +269,7 @@ http2.interceptors.response.use( } const { status } = error.response; if (status === 401) { - localStorage.removeItem('token'); + localStorage.removeItem(TOKEN_KEY); ElMessage.warning('登陆过期,请重新登陆') router.push('/login') return Promise.reject(); diff --git a/src/views/product/ProductGroup.vue b/src/views/product/ProductGroup.vue index b8dd4e1..456f675 100644 --- a/src/views/product/ProductGroup.vue +++ b/src/views/product/ProductGroup.vue @@ -44,6 +44,9 @@ 新增顶级分组 + + 新增商品 + 刷新 @@ -59,7 +62,6 @@
-
@@ -87,12 +89,12 @@ row-key="id" :header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }" > - + - + + + + + + + + + - - + @@ -104,24 +103,6 @@ - - - - {{ currentDetail.Id ?? currentDetail.id }} - {{ currentDetail.Name }} - {{ currentDetail.Host }} - {{ currentDetail.Port }} - - - 未设置 - - {{ currentDetail.Note || '-' }} - {{ formatTime(currentDetail.CreatedAt) }} - - - @@ -132,7 +113,6 @@ import { ElMessage, ElMessageBox } from 'element-plus' import { Plus, Refresh, Search } from '@element-plus/icons-vue' import { getKvmServiceList, - getKvmServiceDetail, createKvmService, updateKvmService, deleteKvmService @@ -143,7 +123,6 @@ const router = useRouter() const loading = ref(false) const submitLoading = ref(false) -const detailLoading = ref(false) const serviceList = ref([]) const total = ref(0) const searchKey = ref('') @@ -159,9 +138,6 @@ const dialogVisible = ref(false) const dialogType = ref('add') const formRef = ref(null) -const detailDialogVisible = ref(false) -const currentDetail = ref(null) - const formData = reactive({ id: undefined, name: '', @@ -355,45 +331,16 @@ const handleDelete = (row) => { }).catch(() => {}) } -// 查看详情 -const handleViewDetail = async (row) => { - // 优先使用原始 Id(PascalCase),回退到规范化后的 id - const rawId = row.Id ?? row.id - console.debug('[KvmService] handleViewDetail rawId:', rawId, 'row:', row) - if (rawId === undefined || rawId === null || rawId === '') { +// 查看详情 —— 跳转到详情页面 +const handleViewDetail = (row) => { + const id = Number(row.Id ?? row.id) + const name = row.Name ?? row.name + if (!id) { ElMessage.error('无法获取服务ID,请刷新列表后重试') return } - detailDialogVisible.value = true - detailLoading.value = true - currentDetail.value = null - try { - const res = await getKvmServiceDetail({ id: Number(rawId) }) - const body = res?.data - console.debug('[KvmService] detail response body:', JSON.stringify(body)) - if (body?.code === 200 && body?.data) { - currentDetail.value = normalizeService(body.data) - } else { - // 接口返回非200,显示错误但仍展示列表行数据 - ElMessage.error(body?.message || '获取详情失败') - currentDetail.value = normalizeService(row) - } - } catch (error) { - console.error('获取详情失败:', error) - const errMsg = error?.response?.data?.message || error?.message || '未知错误' - ElMessage.error('获取详情失败: ' + errMsg) - currentDetail.value = normalizeService(row) - } finally { - detailLoading.value = false - } -} - -// 跳转到宿主机组映射管理 -const goHostGroupMapping = (row) => { - const id = Number(row.Id ?? row.id) - const name = row.Name ?? row.name router.push({ - path: '/virtualization/host-group-mapping', + path: '/virtualization/kvm-service-detail', query: { service_id: id, service_name: name } }) } diff --git a/src/views/virtualization/KvmServiceDetail.vue b/src/views/virtualization/KvmServiceDetail.vue new file mode 100644 index 0000000..006b812 --- /dev/null +++ b/src/views/virtualization/KvmServiceDetail.vue @@ -0,0 +1,505 @@ + + + + + diff --git a/src/views/virtualization/NetworkManage.vue b/src/views/virtualization/NetworkManage.vue new file mode 100644 index 0000000..dbfdacb --- /dev/null +++ b/src/views/virtualization/NetworkManage.vue @@ -0,0 +1,331 @@ + + + + + diff --git a/src/views/virtualization/RemoteHostGroupManage.vue b/src/views/virtualization/RemoteHostGroupManage.vue new file mode 100644 index 0000000..804c28e --- /dev/null +++ b/src/views/virtualization/RemoteHostGroupManage.vue @@ -0,0 +1,250 @@ + + + + + diff --git a/src/views/virtualization/SecurityGroupDetail.vue b/src/views/virtualization/SecurityGroupDetail.vue new file mode 100644 index 0000000..dfea286 --- /dev/null +++ b/src/views/virtualization/SecurityGroupDetail.vue @@ -0,0 +1,330 @@ + + + + + diff --git a/src/views/virtualization/SecurityGroupManage.vue b/src/views/virtualization/SecurityGroupManage.vue new file mode 100644 index 0000000..1376600 --- /dev/null +++ b/src/views/virtualization/SecurityGroupManage.vue @@ -0,0 +1,521 @@ + + + + + diff --git a/src/views/virtualization/VmDetail.vue b/src/views/virtualization/VmDetail.vue new file mode 100644 index 0000000..796eef4 --- /dev/null +++ b/src/views/virtualization/VmDetail.vue @@ -0,0 +1,319 @@ + + + + + diff --git a/src/views/virtualization/VmManage.vue b/src/views/virtualization/VmManage.vue new file mode 100644 index 0000000..2d2fc75 --- /dev/null +++ b/src/views/virtualization/VmManage.vue @@ -0,0 +1,618 @@ + + + + + diff --git a/src/views/virtualization/VncNodeManage.vue b/src/views/virtualization/VncNodeManage.vue new file mode 100644 index 0000000..5ce85b7 --- /dev/null +++ b/src/views/virtualization/VncNodeManage.vue @@ -0,0 +1,352 @@ + + + + + diff --git a/src/views/virtualization/VolumeManage.vue b/src/views/virtualization/VolumeManage.vue new file mode 100644 index 0000000..ac42764 --- /dev/null +++ b/src/views/virtualization/VolumeManage.vue @@ -0,0 +1,467 @@ + + + + + diff --git a/问题.MD b/问题.MD new file mode 100644 index 0000000..f4d8183 --- /dev/null +++ b/问题.MD @@ -0,0 +1,52 @@ +## 管理员前端控制台 - 问题跟踪 + +### 最新一轮 (图一到图七) + +1. ✅已完成 - 图一:安全组列表 "暂无数据" → 修复 `SecurityGroupManage.vue` 数据映射优先 `inner.groups` +2. ✅已完成 - 图二:安全组详情弹窗字段为空 → 修复 `SecurityGroupManage.vue` 详情映射优先 `res.data.data.group` +3. ✅已完成 - 图三:安全组绑定VM使用 `el-input-number` → 改用 `VmSelectorPopup` 组件 +4. ✅已完成 - 图四:安全组解绑VM使用 `el-input-number` → 改用 `VmSelectorPopup` 组件 +5. ✅已完成 - 图五:VNC 获取VM连接使用 `el-input-number` → 改用 `VmSelectorPopup` 组件 +6. ✅已完成 - 图六:VNC 测试宿主机默认值显示 "0" → 改为 `null` (空选择) +7. ✅已完成 - 图七:宿主机、镜像、虚拟机、安全组四个模块表格操作只保留"编辑"和"删除"按钮 + - 创建 `HostDetail.vue` 宿主机独立详情页 (含编辑、指标、详情) + - 创建 `ImageDetail.vue` 镜像独立详情页 (含编辑、同步到宿主机、重下载、宿主机状态) + - 创建 `VmDetail.vue` 虚拟机独立详情页 (含电源操作、重建、救援、指标) + - 创建 `SecurityGroupDetail.vue` 安全组独立详情页 (含同步、绑定/解绑VM、白名单切换、规则管理) + - 添加4个详情页路由 (`host-detail`, `image-detail`, `vm-detail`, `security-group-detail`) + - `HostManage.vue` 表格操作改为 "编辑"→跳转详情页 + "删除" + - `ImageManage.vue` 表格操作改为 "编辑"→跳转详情页 + "删除" + - `VmManage.vue` 表格操作改为 "编辑"→跳转详情页 + "删除",补充 `deleteVm` API + - `SecurityGroupManage.vue` 表格操作改为 "编辑"→跳转详情页 + "删除" + +--- + +### 前一轮 (图一到图五 - 列表数据显示) + +1. ✅已完成 - 远程宿主机组列表 "暂无数据" → 修复 `RemoteHostGroupManage.vue` 优先 `inner.host_groups` +2. ✅已完成 - 宿主机指标弹窗 emoji 图标 → 改为 Element Plus 图标 (`Monitor`, `Coin`, `Box`, `Connection`) +3. ✅已完成 - 虚拟机管理 "更多" 下拉按钮错位 → 修复 `el-dropdown` 内联样式对齐 +4. ✅已完成 - 安全组列表 "暂无数据" → 修复 `SecurityGroupManage.vue` 优先 `inner.groups` +5. ✅已完成 - VNC 节点列表 "暂无数据" → 修复 `VncNodeManage.vue` 优先 `inner.items` + +--- + +### 虚拟化平台管理 17项问题 (已全部完成) + +1. ✅已完成 - 宿主机管理数据无法正常显示 +2. ✅已完成 - 宿主机创建/编辑:数字输入框样式优化 + 带宽单位 Mbps +3. ✅已完成 - 宿主机创建:宿主机组ID改为选择器 +4. ✅已完成 - 宿主机详情:SSH密码显示 + 指标美化 + 时间戳格式化 +5. ✅已完成 - 远程宿主机组管理页面 +6. ✅已完成 - 镜像详情:data.image 嵌套映射 +7. ✅已完成 - 镜像同步到宿主机 + 重下载功能 +8. ✅已完成 - 网络管理:空高级参数不提交 + host_id 改为下拉选择 +9. ✅已完成 - 数据卷:host_id 改为下拉选择 +10. ✅已完成 - 数据卷:迁移功能 + 选择器组件 +11. ✅已完成 - 数据卷详情:data.volume 嵌套映射 + 时间戳 +12. ✅已完成 - 虚拟机:带宽显示 Mbps 单位 +13. ✅已完成 - 虚拟机:完整中文状态映射 +14. ✅已完成 - 虚拟机详情:data.vm 嵌套映射 + 子表格 +15. ✅已完成 - 虚拟机:恢复/救援/退出救援操作 +16. ✅已完成 - 虚拟机创建:镜像选择器 + 宿主机组选择器 +17. ✅已完成 - 虚拟机指标:CPU/内存/磁盘/网络卡片美化 -- 2.52.0 From cd16ec17aef7a91d46d6c78cf1a288f83eca243e Mon Sep 17 00:00:00 2001 From: 2256907009 <2256907009@qq.com> Date: Tue, 17 Mar 2026 18:40:12 +0800 Subject: [PATCH 44/92] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E7=A7=BB?= =?UTF-8?q?=E5=8A=A8=E7=AB=AF=E9=85=8D=E7=BD=AE=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/admin/AvatarSelector.vue | 1 + src/components/admin/ImageSelector.vue | 385 ++++++++++---- src/views/system/SettingManage.vue | 671 ++++++++++++++++++++++-- src/views/system/SystemFile.vue | 133 +++-- src/views/ticket/TicketDetail.vue | 13 +- vite.config.js | 2 +- 6 files changed, 989 insertions(+), 216 deletions(-) diff --git a/src/components/admin/AvatarSelector.vue b/src/components/admin/AvatarSelector.vue index 4652c90..7d178a6 100644 --- a/src/components/admin/AvatarSelector.vue +++ b/src/components/admin/AvatarSelector.vue @@ -270,6 +270,7 @@ import { closeAllMessage } from '../../utils/message' formData.append('files', file) formData.append('file_names', file.name) formData.append('update_type', 'cover') + formData.append('open_down', 'true') try { const res = await uploadFile(formData) diff --git a/src/components/admin/ImageSelector.vue b/src/components/admin/ImageSelector.vue index b04cd89..193c5e2 100644 --- a/src/components/admin/ImageSelector.vue +++ b/src/components/admin/ImageSelector.vue @@ -13,9 +13,14 @@

图片文件库

- - 上传新图片 - +
+ + 已选 {{ selectedIds.size }} 个文件 + + + 上传新图片 + +
@@ -35,9 +40,12 @@ v-for="file in filteredFileList" :key="file.id" class="file-item" - :class="{ 'selected': selectedId === file.id }" + :class="{ 'selected': props.multiple ? selectedIds.has(file.id) : selectedId === file.id }" @click="selectFile(file)" > +
+