Browse Source

ly app版本管理 优化

ly 1 week ago
parent
commit
31b6206d9f

+ 1 - 0
master/src/main/java/com/ruoyi/framework/config/SecurityConfig.java

@@ -113,6 +113,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .antMatchers("/common/download/exportDevList").anonymous()
                 .antMatchers("/common/download**").anonymous()
                 .antMatchers("/common/download/resource**").anonymous()
+                .antMatchers("/common/test/**").permitAll()
                 .antMatchers("/swagger-ui.html").anonymous()
                 .antMatchers("/swagger-resources/**").anonymous()
                 .antMatchers("/webjars/**").anonymous()

+ 18 - 0
master/src/main/java/com/ruoyi/project/common/CommonController.java

@@ -874,4 +874,22 @@ public class CommonController extends BaseController
         }
         return downloadPath;
     }
+
+//    /**
+//     * 模拟被WAF拦截的接口(用于测试前端和APP的拦截提示)
+//     */
+//    @GetMapping("/common/test/blocked")
+//    public void testBlocked(HttpServletResponse response) throws IOException
+//    {
+//        response.setStatus(200);
+//        response.setContentType("text/html;charset=utf-8");
+//        response.setHeader("X-Frame-Options", "SAMEORIGIN");
+//        response.setHeader("X-XSS-Protection", "1; mode=block");
+//        response.setHeader("X-Content-Type-Options", "nosniff");
+//        response.setHeader("Cache-Control", "no-cache");
+//        response.setHeader("Pragma", "no-cache");
+//        response.setHeader("Connection", "close");
+//        String html = "<html><head><title>Request Rejected</title></head><body>The requested URL was rejected. Please consult with your administrator.<br><br>Your support ID is: 88888888888888888888<br><br><a href='javascript:history.back();'>[Go Back]</a></body></html>";
+//        response.getWriter().write(html);
+//    }
 }

+ 17 - 2
master/src/main/java/com/ruoyi/project/reliability/domain/TRelMaintPlan.java

@@ -33,11 +33,16 @@ public class TRelMaintPlan extends BaseEntity
     @Excel(name = "设备位号")
     private String devTag;
 
-    /** 计划维修时间 */
+    /** 计划开始时间 */
     @JsonFormat(pattern = "yyyy-MM-dd" , timezone = "GMT+8")
-    @Excel(name = "计划维修时间", width = 30, dateFormat = "yyyy-MM-dd")
+    @Excel(name = "计划开始时间", width = 30, dateFormat = "yyyy-MM-dd")
     private Date planTime;
 
+    /** 计划结束时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd" , timezone = "GMT+8")
+    @Excel(name = "计划结束时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date planEndTime;
+
     /** 计划审批状态 */
     @Excel(name = "计划审批状态")
     private String approvalStatus;
@@ -144,6 +149,15 @@ public class TRelMaintPlan extends BaseEntity
     {
         return planTime;
     }
+    public void setPlanEndTime(Date planEndTime)
+    {
+        this.planEndTime = planEndTime;
+    }
+
+    public Date getPlanEndTime()
+    {
+        return planEndTime;
+    }
     public void setApprovalStatus(String approvalStatus)
     {
         this.approvalStatus = approvalStatus;
@@ -301,6 +315,7 @@ public class TRelMaintPlan extends BaseEntity
             .append("devName", getDevName())
             .append("devTag", getDevTag())
             .append("planTime", getPlanTime())
+            .append("planEndTime", getPlanEndTime())
             .append("approvalStatus", getApprovalStatus())
             .append("responsible", getResponsible())
             .append("completionStatus", getCompletionStatus())

+ 14 - 0
master/src/main/java/com/ruoyi/project/reliability/domain/TRelMaintRecord.java

@@ -94,6 +94,10 @@ public class TRelMaintRecord extends BaseEntity
     @Excel(name = "附件")
     private String attachments;
 
+    /** 照片URL(多个用逗号分隔) */
+    @Excel(name = "照片")
+    private String photoUrl;
+
     /** 删除标志(0代表存在 2代表删除) */
     private Long delFlag;
 
@@ -419,6 +423,15 @@ public class TRelMaintRecord extends BaseEntity
     {
         return attachments;
     }
+    public void setPhotoUrl(String photoUrl)
+    {
+        this.photoUrl = photoUrl;
+    }
+
+    public String getPhotoUrl()
+    {
+        return photoUrl;
+    }
     public void setDelFlag(Long delFlag)
     {
         this.delFlag = delFlag;
@@ -523,6 +536,7 @@ public class TRelMaintRecord extends BaseEntity
             .append("maintDuration", getMaintDuration())
             .append("processLoss", getProcessLoss())
             .append("attachments", getAttachments())
+            .append("photoUrl", getPhotoUrl())
             .append("delFlag", getDelFlag())
             .append("createrCode", getCreaterCode())
             .append("createdate", getCreatedate())

+ 17 - 0
master/src/main/java/com/ruoyi/project/system/mapper/SysAppVersionMapper.java

@@ -34,6 +34,23 @@ public interface SysAppVersionMapper
      */
     public SysAppVersion selectLatestVersion(String platform);
 
+    /**
+     * 查询兼容指定原生包版本的最新wgt热更新
+     *
+     * @param appVersion 用户原生包版本
+     * @param platform 平台
+     * @return APP版本信息
+     */
+    public SysAppVersion selectLatestCompatibleWgt(String appVersion, String platform);
+
+    /**
+     * 查询最新的整包更新版本(apk/ipa)
+     *
+     * @param platform 平台
+     * @return APP版本信息
+     */
+    public SysAppVersion selectLatestApk(String platform);
+
     /**
      * 新增APP版本
      *

+ 31 - 15
master/src/main/java/com/ruoyi/project/system/service/impl/SysAppVersionServiceImpl.java

@@ -59,25 +59,41 @@ public class SysAppVersionServiceImpl implements ISysAppVersionService
         Map<String, Object> result = new HashMap<>();
         result.put("needUpdate", false);
 
-        // 查询最新版本
-        SysAppVersion latestVersion = appVersionMapper.selectLatestVersion(platform);
-        if (latestVersion == null)
+        // 1. 先检查是否需要整包更新(原生包版本落后)
+        SysAppVersion latestApk = appVersionMapper.selectLatestApk(platform);
+        if (latestApk != null && StringUtils.isNotEmpty(appVersion))
         {
-            return result;
+            // 用户原生包版本 < 最新整包版本,需要整包更新
+            if (compareVersion(appVersion, latestApk.getAppVersion()) < 0)
+            {
+                result.put("needUpdate", true);
+                result.put("version", latestApk.getVersion());
+                result.put("appVersion", latestApk.getAppVersion());
+                result.put("title", latestApk.getTitle());
+                result.put("content", latestApk.getContent());
+                result.put("downloadUrl", latestApk.getDownloadUrl());
+                result.put("updateType", "apk");
+                result.put("forceUpdate", "1".equals(latestApk.getForceUpdate()));
+                return result;
+            }
         }
 
-        // 比较版本号
-        boolean needUpdate = compareVersion(version, latestVersion.getVersion()) < 0;
-
-        if (needUpdate)
+        // 2. 原生包版本兼容,检查是否有wgt热更新
+        SysAppVersion latestWgt = appVersionMapper.selectLatestCompatibleWgt(appVersion, platform);
+        if (latestWgt != null)
         {
-            result.put("needUpdate", true);
-            result.put("version", latestVersion.getVersion());
-            result.put("title", latestVersion.getTitle());
-            result.put("content", latestVersion.getContent());
-            result.put("downloadUrl", latestVersion.getDownloadUrl());
-            result.put("updateType", latestVersion.getUpdateType());
-            result.put("forceUpdate", "1".equals(latestVersion.getForceUpdate()));
+            // 用户wgt版本 < 最新兼容wgt版本,需要wgt热更新
+            if (compareVersion(version, latestWgt.getVersion()) < 0)
+            {
+                result.put("needUpdate", true);
+                result.put("version", latestWgt.getVersion());
+                result.put("title", latestWgt.getTitle());
+                result.put("content", latestWgt.getContent());
+                result.put("downloadUrl", latestWgt.getDownloadUrl());
+                result.put("updateType", "wgt");
+                result.put("forceUpdate", "1".equals(latestWgt.getForceUpdate()));
+                return result;
+            }
         }
 
         return result;

+ 5 - 1
master/src/main/resources/mybatis/reliability/TRelMaintPlanMapper.xml

@@ -10,6 +10,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="devName"    column="dev_name"    />
         <result property="devTag"    column="dev_tag"    />
         <result property="planTime"    column="plan_time"    />
+        <result property="planEndTime"    column="plan_end_time"    />
         <result property="approvalStatus"    column="approval_status"    />
         <result property="responsible"    column="responsible"    />
         <result property="completionStatus"    column="completion_status"    />
@@ -26,7 +27,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </resultMap>
 
     <sql id="selectTRelMaintPlanVo">
-        select d.plan_id, d.plant, d.dev_name, d.dev_tag, d.plan_time, d.approval_status, d.responsible, d.completion_status, d.process_id, d.approver, d.del_flag, d.creater_code, d.createdate, d.updater_code, d.updatedate, d.dept_id, d.remarks  from t_rel_maint_plan d
+        select d.plan_id, d.plant, d.dev_name, d.dev_tag, d.plan_time, d.plan_end_time, d.approval_status, d.responsible, d.completion_status, d.process_id, d.approver, d.del_flag, d.creater_code, d.createdate, d.updater_code, d.updatedate, d.dept_id, d.remarks  from t_rel_maint_plan d
       left join sys_dept s on s.dept_id = d.dept_id
     </sql>
 
@@ -70,6 +71,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="devName != null">dev_name,</if>
             <if test="devTag != null">dev_tag,</if>
             <if test="planTime != null">plan_time,</if>
+            <if test="planEndTime != null">plan_end_time,</if>
             <if test="approvalStatus != null">approval_status,</if>
             <if test="responsible != null">responsible,</if>
             <if test="completionStatus != null">completion_status,</if>
@@ -89,6 +91,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="devName != null">#{devName},</if>
             <if test="devTag != null">#{devTag},</if>
             <if test="planTime != null">#{planTime},</if>
+            <if test="planEndTime != null">#{planEndTime},</if>
             <if test="approvalStatus != null">#{approvalStatus},</if>
             <if test="responsible != null">#{responsible},</if>
             <if test="completionStatus != null">#{completionStatus},</if>
@@ -111,6 +114,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="devName != null">dev_name = #{devName},</if>
             <if test="devTag != null">dev_tag = #{devTag},</if>
             <if test="planTime != null">plan_time = #{planTime},</if>
+            <if test="planEndTime != null">plan_end_time = #{planEndTime},</if>
             <if test="approvalStatus != null">approval_status = #{approvalStatus},</if>
             <if test="responsible != null">responsible = #{responsible},</if>
             <if test="completionStatus != null">completion_status = #{completionStatus},</if>

+ 5 - 1
master/src/main/resources/mybatis/reliability/TRelMaintRecordMapper.xml

@@ -24,6 +24,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="maintDuration"    column="maint_duration"    />
         <result property="processLoss"    column="process_loss"    />
         <result property="attachments"    column="attachments"    />
+        <result property="photoUrl"    column="photo_url"    />
         <result property="delFlag"    column="del_flag"    />
         <result property="createrCode"    column="creater_code"    />
         <result property="createdate"    column="createdate"    />
@@ -37,7 +38,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </resultMap>
 
     <sql id="selectTRelMaintRecordVo">
-        select d.record_id, d.plant, d.dev_name, d.dev_tag, d.compo_name, d.compo_id, d.maint_type, d.maint_dept, d.inspector, d.inspect_content, d.inspect_time, d.responsible, d.maint_content, d.maint_result, d.maint_time, d.maint_cost, d.maint_duration, d.process_loss, d.attachments, d.del_flag, d.creater_code, d.createdate, d.updater_code, d.updatedate, d.dept_id, d.remarks, d.plan_id, d.record_status  from t_rel_maint_record d
+        select d.record_id, d.plant, d.dev_name, d.dev_tag, d.compo_name, d.compo_id, d.maint_type, d.maint_dept, d.inspector, d.inspect_content, d.inspect_time, d.responsible, d.maint_content, d.maint_result, d.maint_time, d.maint_cost, d.maint_duration, d.process_loss, d.attachments, d.photo_url, d.del_flag, d.creater_code, d.createdate, d.updater_code, d.updatedate, d.dept_id, d.remarks, d.plan_id, d.record_status  from t_rel_maint_record d
       left join sys_dept s on s.dept_id = d.dept_id
     </sql>
 
@@ -144,6 +145,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="maintDuration != null">maint_duration,</if>
             <if test="processLoss != null">process_loss,</if>
             <if test="attachments != null">attachments,</if>
+            <if test="photoUrl != null">photo_url,</if>
             <if test="delFlag != null">del_flag,</if>
             <if test="createrCode != null">creater_code,</if>
             <if test="createdate != null">createdate,</if>
@@ -174,6 +176,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="maintDuration != null">#{maintDuration},</if>
             <if test="processLoss != null">#{processLoss},</if>
             <if test="attachments != null">#{attachments},</if>
+            <if test="photoUrl != null">#{photoUrl},</if>
             <if test="delFlag != null">#{delFlag},</if>
             <if test="createrCode != null">#{createrCode},</if>
             <if test="createdate != null">#{createdate},</if>
@@ -207,6 +210,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="maintDuration != null">maint_duration = #{maintDuration},</if>
             <if test="processLoss != null">process_loss = #{processLoss},</if>
             <if test="attachments != null">attachments = #{attachments},</if>
+            <if test="photoUrl != null">photo_url = #{photoUrl},</if>
             <if test="delFlag != null">del_flag = #{delFlag},</if>
             <if test="createrCode != null">creater_code = #{createrCode},</if>
             <if test="createdate != null">createdate = #{createdate},</if>

+ 24 - 1
master/src/main/resources/mybatis/system/SysAppVersionMapper.xml

@@ -23,7 +23,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </resultMap>
 
     <sql id="selectAppVersionVo">
-        select version_id, version, app_version, platform, update_type, title, content, 
+        select version_id, version, app_version, platform, update_type, title, content,
                download_url, force_update, status, create_by, create_time, update_by, update_time, remark
         from sys_app_version
     </sql>
@@ -61,6 +61,29 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         ) where rownum = 1
     </select>
 
+    <!-- 查询兼容指定原生包版本的最新wgt热更新 -->
+    <select id="selectLatestCompatibleWgt" resultMap="SysAppVersionResult">
+        select * from (
+            <include refid="selectAppVersionVo"/>
+            where status = '0'
+            and update_type = 'wgt'
+            and (platform = #{platform} or platform = 'all')
+            and (app_version is null or app_version = '' or app_version = #{appVersion})
+            order by create_time desc
+        ) where rownum = 1
+    </select>
+
+    <!-- 查询最新的整包更新版本 -->
+    <select id="selectLatestApk" parameterType="String" resultMap="SysAppVersionResult">
+        select * from (
+            <include refid="selectAppVersionVo"/>
+            where status = '0'
+            and update_type = 'apk'
+            and (platform = #{platform} or platform = 'all')
+            order by create_time desc
+        ) where rownum = 1
+    </select>
+
     <insert id="insertAppVersion" parameterType="SysAppVersion">
         <selectKey keyProperty="versionId" order="BEFORE" resultType="long">
             select seq_sys_app_version.nextval as versionId from DUAL

+ 8 - 0
ui/src/api/common/commonfile.js

@@ -69,3 +69,11 @@ export function exportCommonfile(query) {
     params: query
   })
 }
+
+// 测试接口 - 模拟被WAF拦截(用于测试前端和APP的拦截提示)
+export function testBlocked() {
+  return request({
+    url: '/common/test/blocked',
+    method: 'get',
+  })
+}

+ 3 - 0
ui/src/views/index.vue

@@ -24,6 +24,7 @@
   import EoegHome from "@/views/eoeg/home/index.vue";
   import Ldpehome from "@/views/ldpehome.vue";
   import Ps_home from "@/views/ps/home/index.vue";
+  import { testBlocked } from '@/api/common/commonfile';
 
   export default {
     data() {
@@ -44,6 +45,8 @@
     mounted() {
       console.log(this.$store.state.user.homeType)
       this.homeType = this.$store.state.user.homeType
+      // 测试WAF拦截提示
+      // testBlocked();
     },
   };
 </script>

+ 2 - 2
ui/src/views/login.vue

@@ -324,8 +324,8 @@ export default {
   justify-content: center;
   align-items: center;
   height: 100%;
-  //background-image: url("../assets/image/CPMS20210107.jpg");
-  background-image: url("../assets/image/cpms-test.jpg");
+  background-image: url("../assets/image/CPMS20210107.jpg");
+  //background-image: url("../assets/image/cpms-test.jpg");
   background-size: cover;
 }
 

+ 5 - 1
ui/src/views/reliability/rel_maint_plan/MaintPlanDetailContent.vue

@@ -10,10 +10,14 @@
         <el-descriptions-item label="装置">{{ planData.plant || '-' }}</el-descriptions-item>
         <el-descriptions-item label="设备名称">{{ planData.devName || '-' }}</el-descriptions-item>
         <el-descriptions-item label="设备位号">{{ planData.devTag || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="计划维修时间">
+        <el-descriptions-item label="计划开始时间">
           <span v-if="planData.planTime">{{ parseTime(planData.planTime, '{y}-{m}-{d}') }}</span>
           <span v-else>-</span>
         </el-descriptions-item>
+        <el-descriptions-item label="计划结束时间">
+          <span v-if="planData.planEndTime">{{ parseTime(planData.planEndTime, '{y}-{m}-{d}') }}</span>
+          <span v-else>-</span>
+        </el-descriptions-item>
         <el-descriptions-item label="责任人">{{ getStaffNameById(planData.responsible) || '-' }}</el-descriptions-item>
         <el-descriptions-item label="审批人">{{ getStaffNameById(planData.approver) || '-' }}</el-descriptions-item>
         <el-descriptions-item label="计划审批状态">

+ 17 - 6
ui/src/views/reliability/rel_maint_plan/MaintPlanForm.vue

@@ -37,18 +37,29 @@
             </el-form-item>
           </el-col>
           <el-col :span="12">
-            <el-form-item label="计划维修时间" prop="planTime">
+            <el-form-item label="计划开始时间" prop="planTime">
               <el-date-picker
                 v-model="formData.planTime"
                 type="date"
                 value-format="yyyy-MM-dd"
-                placeholder="选择计划维修时间"
+                placeholder="选择计划开始时间"
                 style="width: 100%">
               </el-date-picker>
             </el-form-item>
           </el-col>
         </el-row>
         <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="计划结束时间" prop="planEndTime">
+              <el-date-picker
+                v-model="formData.planEndTime"
+                type="date"
+                value-format="yyyy-MM-dd"
+                placeholder="选择计划结束时间"
+                style="width: 100%">
+              </el-date-picker>
+            </el-form-item>
+          </el-col>
           <el-col :span="12">
             <el-form-item label="责任人" prop="responsible">
               <el-select
@@ -69,6 +80,8 @@
               </el-select>
             </el-form-item>
           </el-col>
+        </el-row>
+        <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="审批人" prop="approver">
               <el-select
@@ -90,9 +103,7 @@
               </el-select>
             </el-form-item>
           </el-col>
-        </el-row>
-        <el-row :gutter="20">
-          <el-col :span="24">
+          <el-col :span="12">
             <el-form-item label="备注" prop="remarks">
               <el-input v-model="formData.remarks" type="textarea" :rows="2" placeholder="请输入备注" />
             </el-form-item>
@@ -249,7 +260,7 @@ export default {
           { required: true, message: "请选择设备", trigger: "change" }
         ],
         planTime: [
-          { required: true, message: "请选择计划维修时间", trigger: "change" }
+          { required: true, message: "请选择计划开始时间", trigger: "change" }
         ],
         approver: [
           { required: true, message: "请选择审批人", trigger: "change" }

+ 8 - 3
ui/src/views/reliability/rel_maint_plan/index.vue

@@ -28,12 +28,12 @@
           @keyup.enter.native="handleQuery"
         />
       </el-form-item>
-      <el-form-item label="计划维修时间" prop="planTime">
+      <el-form-item label="计划开始时间" prop="planTime">
         <el-date-picker clearable size="small" style="width: 200px"
           v-model="queryParams.planTime"
           type="date"
           value-format="yyyy-MM-dd"
-          placeholder="选择计划维修时间">
+          placeholder="选择计划开始时间">
         </el-date-picker>
       </el-form-item>
       <el-form-item label="计划审批状态" prop="approvalStatus">
@@ -94,11 +94,16 @@
       <el-table-column label="装置" align="center" prop="plant" width="120" :show-overflow-tooltip="true"/>
       <el-table-column label="设备名称" align="center" prop="devName" width="150" :show-overflow-tooltip="true"/>
       <el-table-column label="设备位号" align="center" prop="devTag" width="150" :show-overflow-tooltip="true"/>
-      <el-table-column label="计划维修时间" align="center" prop="planTime" width="120">
+      <el-table-column label="计划开始时间" align="center" prop="planTime" width="120">
         <template slot-scope="scope">
           <span>{{ parseTime(scope.row.planTime, '{y}-{m}-{d}') }}</span>
         </template>
       </el-table-column>
+      <el-table-column label="计划结束时间" align="center" prop="planEndTime" width="120">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.planEndTime, '{y}-{m}-{d}') }}</span>
+        </template>
+      </el-table-column>
       <el-table-column label="计划审批状态" align="center" prop="approvalStatus" width="120" :show-overflow-tooltip="true">
         <template slot-scope="scope">
           <el-tag v-if="scope.row.approvalStatus === '0'" type="warning" size="mini">待审批</el-tag>

+ 122 - 1
ui/src/views/reliability/rel_maint_record/index.vue

@@ -168,7 +168,30 @@
       <el-table-column label="维修费用" align="center" prop="maintCost" :show-overflow-tooltip="true"/>
       <el-table-column label="维修时长" align="center" prop="maintDuration" :show-overflow-tooltip="true"/>
       <el-table-column label="工艺损失" align="center" prop="processLoss" :show-overflow-tooltip="true"/>
-      <el-table-column label="附件" align="center" prop="attachments" :show-overflow-tooltip="true"/>
+      <el-table-column label="照片" align="center" width="80">
+        <template slot-scope="scope">
+          <el-button
+            v-if="scope.row.photoUrl"
+            type="text"
+            icon="el-icon-picture"
+            @click="handleViewPhotos(scope.row.photoUrl)">
+            查看
+          </el-button>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="附件" align="center" width="80">
+        <template slot-scope="scope">
+          <el-button
+            v-if="scope.row.attachments"
+            type="text"
+            icon="el-icon-paperclip"
+            @click="handleViewAttachments(scope.row.attachments)">
+            查看
+          </el-button>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
 
       <el-table-column label="备注" align="center" prop="remarks" :show-overflow-tooltip="true"/>
       <el-table-column label="操作" align="center" fixed="right" width="120" class-name="small-padding fixed-width">
@@ -353,6 +376,41 @@
       </div>
     </el-dialog>
 
+    <!-- 照片预览对话框 -->
+    <el-dialog title="照片预览" :visible.sync="photoPreviewVisible" width="800px" append-to-body>
+      <div class="photo-preview-container">
+        <div v-for="(photo, index) in previewPhotoList" :key="index" class="photo-item">
+          <el-image
+            :src="photo.url"
+            :preview-src-list="previewPhotoList.map(p => p.url)"
+            fit="contain"
+            style="width: 200px; height: 200px;">
+          </el-image>
+        </div>
+      </div>
+      <div v-if="previewPhotoList.length === 0" style="text-align: center; color: #999;">暂无照片</div>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="photoPreviewVisible = false">关 闭</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 附件查看对话框 -->
+    <el-dialog title="附件列表" :visible.sync="attachmentDialogVisible" width="600px" append-to-body>
+      <el-table :data="viewAttachmentList" border size="small">
+        <el-table-column label="序号" type="index" width="60" align="center" />
+        <el-table-column label="文件名" prop="name" :show-overflow-tooltip="true" />
+        <el-table-column label="操作" width="120" align="center">
+          <template slot-scope="scope">
+            <el-button type="text" icon="el-icon-download" @click="downloadAttachment(scope.row)">下载</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div v-if="viewAttachmentList.length === 0" style="text-align: center; color: #999; padding: 20px;">暂无附件</div>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="attachmentDialogVisible = false">关 闭</el-button>
+      </div>
+    </el-dialog>
+
       <!-- 用户导入对话框 -->
       <el-dialog :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body>
           <el-upload
@@ -480,6 +538,12 @@ export default {
       // 维修计划详情对话框
       planDetailVisible: false,
       planDetailId: null,
+      // 照片预览对话框
+      photoPreviewVisible: false,
+      previewPhotoList: [],
+      // 附件查看对话框
+      attachmentDialogVisible: false,
+      viewAttachmentList: [],
       // 所有人员选项
       staffOptions: []
     };
@@ -563,6 +627,47 @@ export default {
       this.planDetailId = planId;
       this.planDetailVisible = true;
     },
+    /** 解析文件URL字符串为文件列表 */
+    parseFileList(urlStr, type) {
+      if (!urlStr) return [];
+      const urls = urlStr.split(',').filter(url => url.trim());
+      return urls.map((url, index) => {
+        const fullUrl = url.startsWith('http') ? url : process.env.VUE_APP_BASE_API + url;
+        return {
+          name: type === 'image' ? `照片${index + 1}` : this.getFileNameFromUrl(url),
+          url: fullUrl,
+          path: url
+        };
+      });
+    },
+    /** 从URL中获取文件名 */
+    getFileNameFromUrl(url) {
+      if (!url) return '';
+      const parts = url.split('/');
+      return parts[parts.length - 1] || '附件';
+    },
+    /** 查看照片 */
+    handleViewPhotos(photoUrl) {
+      this.previewPhotoList = this.parseFileList(photoUrl, 'image');
+      this.photoPreviewVisible = true;
+    },
+    /** 查看附件 */
+    handleViewAttachments(attachments) {
+      this.viewAttachmentList = this.parseFileList(attachments, 'file');
+      this.attachmentDialogVisible = true;
+    },
+    /** 下载附件 */
+    downloadAttachment(file) {
+      if (file.url) {
+        const link = document.createElement('a');
+        link.href = file.url;
+        link.download = file.name || '附件';
+        link.target = '_blank';
+        document.body.appendChild(link);
+        link.click();
+        document.body.removeChild(link);
+      }
+    },
     // 取消按钮
     cancel() {
       this.open = false;
@@ -714,3 +819,19 @@ export default {
   }
 };
 </script>
+
+<style scoped>
+/* 照片预览容器样式 */
+.photo-preview-container {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+  justify-content: flex-start;
+}
+
+.photo-preview-container .photo-item {
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  overflow: hidden;
+}
+</style>

+ 281 - 3
ui/src/views/reliability/rel_maint_record/myRecord.vue

@@ -134,7 +134,30 @@
       <el-table-column label="维修费用" align="center" prop="maintCost" :show-overflow-tooltip="true"/>
       <el-table-column label="维修时长" align="center" prop="maintDuration" :show-overflow-tooltip="true"/>
       <el-table-column label="工艺损失" align="center" prop="processLoss" :show-overflow-tooltip="true"/>
-      <el-table-column label="附件" align="center" prop="attachments" :show-overflow-tooltip="true"/>
+      <el-table-column label="照片" align="center" width="80">
+        <template slot-scope="scope">
+          <el-button
+            v-if="scope.row.photoUrl"
+            type="text"
+            icon="el-icon-picture"
+            @click="handleViewPhotos(scope.row.photoUrl)">
+            查看
+          </el-button>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="附件" align="center" width="80">
+        <template slot-scope="scope">
+          <el-button
+            v-if="scope.row.attachments"
+            type="text"
+            icon="el-icon-paperclip"
+            @click="handleViewAttachments(scope.row.attachments)">
+            查看
+          </el-button>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
 
       <el-table-column label="备注" align="center" prop="remarks" :show-overflow-tooltip="true"/>
       <el-table-column label="操作" align="center" fixed="right" width="120" class-name="small-padding fixed-width">
@@ -226,8 +249,37 @@
               </el-select>
             </el-form-item>
           </template>
+          <el-form-item label="照片" prop="photoUrl">
+            <el-upload
+              class="upload-demo"
+              :action="photoUpload.url"
+              :headers="photoUpload.headers"
+              :data="photoUpload.data"
+              :on-success="handlePhotoSuccess"
+              :on-remove="handlePhotoRemove"
+              :before-upload="beforePhotoUpload"
+              :file-list="photoFileList"
+              list-type="picture-card"
+              multiple
+              accept="image/*">
+              <i class="el-icon-plus"></i>
+              <div slot="tip" class="el-upload__tip">只能上传jpg/png/gif文件,且单个不超过15MB</div>
+            </el-upload>
+          </el-form-item>
           <el-form-item label="附件" prop="attachments">
-            <el-input v-model="processForm.attachments" placeholder="请输入附件" />
+            <el-upload
+              class="attachment-upload"
+              :action="attachmentUpload.url"
+              :headers="attachmentUpload.headers"
+              :data="attachmentUpload.data"
+              :on-success="handleAttachmentSuccess"
+              :on-remove="handleAttachmentRemove"
+              :before-upload="beforeAttachmentUpload"
+              :file-list="attachmentFileList"
+              multiple>
+              <el-button size="small" type="primary">点击上传</el-button>
+              <div slot="tip" class="el-upload__tip">可上传多个文件,单个不超过50MB</div>
+            </el-upload>
           </el-form-item>
           <el-form-item label="备注" prop="remarks">
             <el-input v-model="processForm.remarks" type="textarea" :rows="3" placeholder="请输入备注" />
@@ -347,8 +399,37 @@
               </el-col>
             </el-row>
           </template>
+          <el-form-item label="照片" prop="photoUrl">
+            <el-upload
+              class="upload-demo"
+              :action="photoUpload.url"
+              :headers="photoUpload.headers"
+              :data="photoUpload.data"
+              :on-success="handlePhotoSuccess"
+              :on-remove="handlePhotoRemove"
+              :before-upload="beforePhotoUpload"
+              :file-list="photoFileList"
+              list-type="picture-card"
+              multiple
+              accept="image/*">
+              <i class="el-icon-plus"></i>
+              <div slot="tip" class="el-upload__tip">只能上传jpg/png/gif文件,且单个不超过15MB</div>
+            </el-upload>
+          </el-form-item>
           <el-form-item label="附件" prop="attachments">
-            <el-input v-model="processForm.attachments" placeholder="请输入附件" />
+            <el-upload
+              class="attachment-upload"
+              :action="attachmentUpload.url"
+              :headers="attachmentUpload.headers"
+              :data="attachmentUpload.data"
+              :on-success="handleAttachmentSuccess"
+              :on-remove="handleAttachmentRemove"
+              :before-upload="beforeAttachmentUpload"
+              :file-list="attachmentFileList"
+              multiple>
+              <el-button size="small" type="primary">点击上传</el-button>
+              <div slot="tip" class="el-upload__tip">可上传多个文件,单个不超过50MB</div>
+            </el-upload>
           </el-form-item>
           <el-form-item label="备注" prop="remarks">
             <el-input v-model="processForm.remarks" type="textarea" :rows="3" placeholder="请输入备注" />
@@ -397,6 +478,41 @@
       </div>
     </el-dialog>
 
+    <!-- 照片预览对话框 -->
+    <el-dialog title="照片预览" :visible.sync="photoPreviewVisible" width="800px" append-to-body>
+      <div class="photo-preview-container">
+        <div v-for="(photo, index) in previewPhotoList" :key="index" class="photo-item">
+          <el-image
+            :src="photo.url"
+            :preview-src-list="previewPhotoList.map(p => p.url)"
+            fit="contain"
+            style="width: 200px; height: 200px;">
+          </el-image>
+        </div>
+      </div>
+      <div v-if="previewPhotoList.length === 0" style="text-align: center; color: #999;">暂无照片</div>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="photoPreviewVisible = false">关 闭</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 附件查看对话框 -->
+    <el-dialog title="附件列表" :visible.sync="attachmentDialogVisible" width="600px" append-to-body>
+      <el-table :data="viewAttachmentList" border size="small">
+        <el-table-column label="序号" type="index" width="60" align="center" />
+        <el-table-column label="文件名" prop="name" :show-overflow-tooltip="true" />
+        <el-table-column label="操作" width="120" align="center">
+          <template slot-scope="scope">
+            <el-button type="text" icon="el-icon-download" @click="downloadAttachment(scope.row)">下载</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div v-if="viewAttachmentList.length === 0" style="text-align: center; color: #999; padding: 20px;">暂无附件</div>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="attachmentDialogVisible = false">关 闭</el-button>
+      </div>
+    </el-dialog>
+
   </div>
 </template>
 
@@ -447,6 +563,28 @@ export default {
       // 维修计划详情对话框
       planDetailVisible: false,
       planDetailId: null,
+      // 照片上传配置
+      photoUpload: {
+        headers: { Authorization: "Bearer " + getToken() },
+        url: process.env.VUE_APP_BASE_API + "/common/uploadWithPath",
+        data: { path: "/rel/maint/photo" }
+      },
+      // 附件上传配置
+      attachmentUpload: {
+        headers: { Authorization: "Bearer " + getToken() },
+        url: process.env.VUE_APP_BASE_API + "/common/uploadWithPath",
+        data: { path: "/rel/maint/attachment" }
+      },
+      // 照片文件列表
+      photoFileList: [],
+      // 附件文件列表
+      attachmentFileList: [],
+      // 照片预览对话框
+      photoPreviewVisible: false,
+      previewPhotoList: [],
+      // 附件查看对话框
+      attachmentDialogVisible: false,
+      viewAttachmentList: [],
         // 用户导入参数
         upload: {
             // 是否显示弹出层(用户导入)
@@ -607,6 +745,9 @@ export default {
         this.processForm.cannotMaintOrReplace = false;
         this.processForm.memoReason = null;
         this.processForm.memoTime = null;
+        // 初始化照片和附件文件列表
+        this.photoFileList = this.parseFileList(this.processForm.photoUrl, 'image');
+        this.attachmentFileList = this.parseFileList(this.processForm.attachments, 'file');
         // 将数字字段的 null 值转换为 undefined,并确保是数字类型,以便 el-input-number 正常工作
         if (this.processForm.maintCost === null || this.processForm.maintCost === '' || this.processForm.maintCost === undefined) {
           this.processForm.maintCost = undefined;
@@ -652,6 +793,7 @@ export default {
         maintDuration: null,
         processLoss: null,
         attachments: null,
+        photoUrl: null,
         remarks: null,
         needMaintOrReplace: false,
         newMaintType: null,
@@ -660,6 +802,9 @@ export default {
         memoReason: null,
         memoTime: null
       };
+      // 清空文件列表
+      this.photoFileList = [];
+      this.attachmentFileList = [];
       if (this.$refs.processInspectForm) {
         this.$refs.processInspectForm.resetFields();
       }
@@ -880,6 +1025,108 @@ export default {
     handleViewPlan(planId) {
       this.planDetailId = planId;
       this.planDetailVisible = true;
+    },
+    /** 解析文件URL字符串为文件列表 */
+    parseFileList(urlStr, type) {
+      if (!urlStr) return [];
+      const urls = urlStr.split(',').filter(url => url.trim());
+      return urls.map((url, index) => {
+        const fullUrl = url.startsWith('http') ? url : process.env.VUE_APP_BASE_API + url;
+        return {
+          name: type === 'image' ? `照片${index + 1}` : this.getFileNameFromUrl(url),
+          url: fullUrl,
+          path: url
+        };
+      });
+    },
+    /** 从URL中获取文件名 */
+    getFileNameFromUrl(url) {
+      if (!url) return '';
+      const parts = url.split('/');
+      return parts[parts.length - 1] || '附件';
+    },
+    /** 照片上传前校验 */
+    beforePhotoUpload(file) {
+      const isImage = file.type.startsWith('image/');
+      const isLt15M = file.size / 1024 / 1024 < 15;
+      if (!isImage) {
+        this.$message.error('只能上传图片文件!');
+        return false;
+      }
+      if (!isLt15M) {
+        this.$message.error('上传图片大小不能超过 15MB!');
+        return false;
+      }
+      return true;
+    },
+    /** 照片上传成功 */
+    handlePhotoSuccess(response, file, fileList) {
+      if (response && response.fileName) {
+        const currentUrls = this.processForm.photoUrl ? this.processForm.photoUrl.split(',').filter(u => u) : [];
+        currentUrls.push(response.fileName);
+        this.processForm.photoUrl = currentUrls.join(',');
+        this.$message.success('照片上传成功');
+      } else {
+        this.$message.error(response.msg || '上传失败');
+      }
+    },
+    /** 照片移除 */
+    handlePhotoRemove(file, fileList) {
+      const path = file.path || file.response?.fileName;
+      if (path && this.processForm.photoUrl) {
+        const urls = this.processForm.photoUrl.split(',').filter(u => u && u !== path);
+        this.processForm.photoUrl = urls.join(',');
+      }
+    },
+    /** 附件上传前校验 */
+    beforeAttachmentUpload(file) {
+      const isLt50M = file.size / 1024 / 1024 < 50;
+      if (!isLt50M) {
+        this.$message.error('上传文件大小不能超过 50MB!');
+        return false;
+      }
+      return true;
+    },
+    /** 附件上传成功 */
+    handleAttachmentSuccess(response, file, fileList) {
+      if (response && response.fileName) {
+        const currentUrls = this.processForm.attachments ? this.processForm.attachments.split(',').filter(u => u) : [];
+        currentUrls.push(response.fileName);
+        this.processForm.attachments = currentUrls.join(',');
+        this.$message.success('附件上传成功');
+      } else {
+        this.$message.error(response.msg || '上传失败');
+      }
+    },
+    /** 附件移除 */
+    handleAttachmentRemove(file, fileList) {
+      const path = file.path || file.response?.fileName;
+      if (path && this.processForm.attachments) {
+        const urls = this.processForm.attachments.split(',').filter(u => u && u !== path);
+        this.processForm.attachments = urls.join(',');
+      }
+    },
+    /** 查看照片 */
+    handleViewPhotos(photoUrl) {
+      this.previewPhotoList = this.parseFileList(photoUrl, 'image');
+      this.photoPreviewVisible = true;
+    },
+    /** 查看附件 */
+    handleViewAttachments(attachments) {
+      this.viewAttachmentList = this.parseFileList(attachments, 'file');
+      this.attachmentDialogVisible = true;
+    },
+    /** 下载附件 */
+    downloadAttachment(file) {
+      if (file.url) {
+        const link = document.createElement('a');
+        link.href = file.url;
+        link.download = file.name || '附件';
+        link.target = '_blank';
+        document.body.appendChild(link);
+        link.click();
+        document.body.removeChild(link);
+      }
     }
   }
 };
@@ -898,4 +1145,35 @@ export default {
 ::v-deep .el-form {
   padding-top: 0;
 }
+
+/* 照片上传组件样式 */
+.upload-demo ::v-deep .el-upload--picture-card {
+  width: 100px;
+  height: 100px;
+  line-height: 100px;
+}
+
+.upload-demo ::v-deep .el-upload-list--picture-card .el-upload-list__item {
+  width: 100px;
+  height: 100px;
+}
+
+/* 附件上传组件样式 */
+.attachment-upload ::v-deep .el-upload-list__item {
+  transition: none;
+}
+
+/* 照片预览容器样式 */
+.photo-preview-container {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+  justify-content: flex-start;
+}
+
+.photo-preview-container .photo-item {
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  overflow: hidden;
+}
 </style>

+ 18 - 0
ui/src/views/system/appVersion/index.vue

@@ -87,6 +87,14 @@
         </template>
       </el-table-column>
       <el-table-column :label="$t('更新标题')" align="center" prop="title" :show-overflow-tooltip="true" />
+      <el-table-column :label="$t('安装包')" align="center" prop="downloadUrl" min-width="180" :show-overflow-tooltip="true">
+        <template slot-scope="scope">
+          <el-link v-if="scope.row.downloadUrl" type="primary" :underline="false" @click="handleDownload(scope.row.downloadUrl)">
+            {{ getFileName(scope.row.downloadUrl) }}
+          </el-link>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
       <el-table-column :label="$t('强制更新')" align="center" prop="forceUpdate" width="100">
         <template slot-scope="scope">
           <el-tag v-if="scope.row.forceUpdate === '1'" type="danger">{{ $t('是') }}</el-tag>
@@ -393,6 +401,16 @@ export default {
     /** 上传失败回调 */
     handleUploadError(err) {
       this.$message.error(this.$t('上传失败'));
+    },
+    /** 从路径中提取文件名 */
+    getFileName(path) {
+      if (!path) return '';
+      return path.substring(path.lastIndexOf('/') + 1);
+    },
+    /** 下载安装包 */
+    handleDownload(downloadUrl) {
+      const url = process.env.VUE_APP_BASE_API + downloadUrl;
+      window.open(url, '_blank');
     }
   }
 };