批量推送
This commit is contained in:
@@ -20,6 +20,10 @@
|
|||||||
<groupId>com.ruoyi</groupId>
|
<groupId>com.ruoyi</groupId>
|
||||||
<artifactId>ruoyi-common</artifactId>
|
<artifactId>ruoyi-common</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.ruoyi</groupId>
|
||||||
|
<artifactId>ruoyi-framework</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.ruoyi</groupId>
|
<groupId>com.ruoyi</groupId>
|
||||||
<artifactId>ruoyi-system</artifactId>
|
<artifactId>ruoyi-system</artifactId>
|
||||||
|
|||||||
+69
@@ -0,0 +1,69 @@
|
|||||||
|
package com.ruoyi.business.controller;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import com.ruoyi.common.annotation.Log;
|
||||||
|
import com.ruoyi.common.core.controller.BaseController;
|
||||||
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
|
import com.ruoyi.common.core.page.TableDataInfo;
|
||||||
|
import com.ruoyi.common.enums.BusinessType;
|
||||||
|
import com.ruoyi.business.domain.BizArticle;
|
||||||
|
import com.ruoyi.business.service.IBizArticleService;
|
||||||
|
import com.ruoyi.common.utils.SecurityUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台协议/隐私政策文章 Controller (后台 admin)
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/business/article")
|
||||||
|
public class BizArticleController extends BaseController
|
||||||
|
{
|
||||||
|
@Autowired
|
||||||
|
private IBizArticleService articleService;
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('business:article:list')")
|
||||||
|
@GetMapping("/list")
|
||||||
|
public TableDataInfo list(BizArticle entity)
|
||||||
|
{
|
||||||
|
startPage();
|
||||||
|
List<BizArticle> list = articleService.selectList(entity);
|
||||||
|
return getDataTable(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('business:article:query')")
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public AjaxResult getInfo(@PathVariable("id") Long id)
|
||||||
|
{
|
||||||
|
return success(articleService.getById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('business:article:add')")
|
||||||
|
@Log(title = "平台协议", businessType = BusinessType.INSERT)
|
||||||
|
@PostMapping
|
||||||
|
public AjaxResult add(@RequestBody BizArticle entity)
|
||||||
|
{
|
||||||
|
if (entity.getStatus() == null) entity.setStatus("0");
|
||||||
|
entity.setCreateBy(SecurityUtils.getUsername());
|
||||||
|
entity.setUpdateBy(SecurityUtils.getUsername());
|
||||||
|
return toAjax(articleService.insert(entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('business:article:edit')")
|
||||||
|
@Log(title = "平台协议", businessType = BusinessType.UPDATE)
|
||||||
|
@PutMapping
|
||||||
|
public AjaxResult edit(@RequestBody BizArticle entity)
|
||||||
|
{
|
||||||
|
entity.setUpdateBy(SecurityUtils.getUsername());
|
||||||
|
return toAjax(articleService.updateByPrimaryKey(entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('business:article:remove')")
|
||||||
|
@Log(title = "平台协议", businessType = BusinessType.DELETE)
|
||||||
|
@DeleteMapping("/{ids}")
|
||||||
|
public AjaxResult remove(@PathVariable Long[] ids)
|
||||||
|
{
|
||||||
|
return toAjax(articleService.deleteByPrimaryKeys(ids));
|
||||||
|
}
|
||||||
|
}
|
||||||
+102
@@ -2,6 +2,9 @@ package com.ruoyi.business.controller;
|
|||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
@@ -11,9 +14,19 @@ import com.ruoyi.business.domain.BizOrg;
|
|||||||
import com.ruoyi.business.dto.SmsValidForm;
|
import com.ruoyi.business.dto.SmsValidForm;
|
||||||
import com.ruoyi.business.service.IBizOrgService;
|
import com.ruoyi.business.service.IBizOrgService;
|
||||||
import com.ruoyi.business.service.SysSmsService;
|
import com.ruoyi.business.service.SysSmsService;
|
||||||
|
import com.ruoyi.common.constant.Constants;
|
||||||
import com.ruoyi.common.core.controller.BaseController;
|
import com.ruoyi.common.core.controller.BaseController;
|
||||||
import com.ruoyi.common.core.domain.AjaxResult;
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||||
|
import com.ruoyi.common.core.domain.model.LoginUser;
|
||||||
|
import com.ruoyi.common.enums.UserStatus;
|
||||||
|
import com.ruoyi.common.utils.ip.IpUtils;
|
||||||
|
import com.ruoyi.common.utils.DateUtils;
|
||||||
|
import com.ruoyi.framework.manager.AsyncManager;
|
||||||
|
import com.ruoyi.framework.manager.factory.AsyncFactory;
|
||||||
|
import com.ruoyi.framework.web.service.SysPermissionService;
|
||||||
|
import com.ruoyi.framework.web.service.TokenService;
|
||||||
|
import com.ruoyi.system.mapper.SysUserMapper;
|
||||||
import com.ruoyi.system.service.ISysUserService;
|
import com.ruoyi.system.service.ISysUserService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,6 +40,10 @@ import com.ruoyi.system.service.ISysUserService;
|
|||||||
* 注: 主账号不建 biz_person
|
* 注: 主账号不建 biz_person
|
||||||
*
|
*
|
||||||
* 2026-08-16 重构: 删 biz_user_role_bind, 统一用 sys_user.role_type (见迁移脚本)
|
* 2026-08-16 重构: 删 biz_user_role_bind, 统一用 sys_user.role_type (见迁移脚本)
|
||||||
|
*
|
||||||
|
* 2026-08-16 新增: 短信验证码登录 (loginMode=sms):
|
||||||
|
* 1. smsSendCode 发送短信 (校验手机号已注册)
|
||||||
|
* 2. smsLogin 校验短信验证码 + 颁发 JWT token (不走 AuthenticationManager, 因为无密码)
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/business/auth")
|
@RequestMapping("/business/auth")
|
||||||
@@ -44,6 +61,15 @@ public class BizAuthController extends BaseController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private BCryptPasswordEncoder passwordEncoder;
|
private BCryptPasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SysUserMapper userMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TokenService tokenService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SysPermissionService permissionService;
|
||||||
|
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public AjaxResult login(@RequestBody Map<String, Object> body) {
|
public AjaxResult login(@RequestBody Map<String, Object> body) {
|
||||||
String username = (String) body.get("username");
|
String username = (String) body.get("username");
|
||||||
@@ -52,6 +78,82 @@ public class BizAuthController extends BaseController {
|
|||||||
return success().put("roleType", roleType).put("token", "mock-token");
|
return success().put("roleType", roleType).put("token", "mock-token");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录用 - 发送短信验证码 (手机号必须已注册)
|
||||||
|
*/
|
||||||
|
@PostMapping("/smsSendCode")
|
||||||
|
public AjaxResult smsSendCode(@RequestBody Map<String, String> body) {
|
||||||
|
String phone = body == null ? null : body.get("phone");
|
||||||
|
if (phone == null || !phone.matches("^1\\d{10}$")) {
|
||||||
|
return error("请输入正确的手机号");
|
||||||
|
}
|
||||||
|
// 登录反义: 手机号未注册则不发
|
||||||
|
if (!userService.isPhoneRegistered(phone)) {
|
||||||
|
return error("该手机号未注册, 请先注册账号");
|
||||||
|
}
|
||||||
|
String uuid = smsService.sendCode(phone);
|
||||||
|
return AjaxResult.success("验证码已发送", uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 短信验证码登录 (无密码)
|
||||||
|
* 1. 校验手机号 + 短信验证码
|
||||||
|
* 2. 查用户 (按手机号)
|
||||||
|
* 3. 校验用户状态
|
||||||
|
* 4. 构造 LoginUser, 颁发 JWT token (SecurityContext 一并设置, 兼容后续鉴权)
|
||||||
|
* 5. 记录登录信息 + logininfor
|
||||||
|
*/
|
||||||
|
@PostMapping("/smsLogin")
|
||||||
|
public AjaxResult smsLogin(@RequestBody Map<String, String> body) {
|
||||||
|
String phone = body == null ? null : body.get("phone");
|
||||||
|
String code = body == null ? null : body.get("smsCode");
|
||||||
|
String uuid = body == null ? null : body.get("uuid");
|
||||||
|
|
||||||
|
// 1. 基础校验
|
||||||
|
if (phone == null || !phone.matches("^1\\d{10}$")) return error("手机号格式错误");
|
||||||
|
if (code == null || code.isEmpty()) return error("请输入短信验证码");
|
||||||
|
|
||||||
|
// 2. 校验短信验证码
|
||||||
|
SmsValidForm smsForm = new SmsValidForm();
|
||||||
|
smsForm.setPhone(phone);
|
||||||
|
smsForm.setSmsCode(code);
|
||||||
|
smsForm.setUuid(uuid);
|
||||||
|
try {
|
||||||
|
smsService.verifyCode(smsForm);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return error("验证码错误或已过期: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 查用户 (mapper.checkPhoneUnique 返回 SysUser 或 null)
|
||||||
|
SysUser user = userMapper.checkPhoneUnique(phone);
|
||||||
|
if (user == null || user.getUserId() == null) {
|
||||||
|
return error("该手机号未注册");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 校验用户状态
|
||||||
|
if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
|
||||||
|
return error("账号已被删除");
|
||||||
|
}
|
||||||
|
if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
|
||||||
|
return error("账号已被停用");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 构造 LoginUser 并设 SecurityContext (兼容后续 spring security 鉴权)
|
||||||
|
LoginUser loginUser = new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
|
||||||
|
Authentication authentication = new UsernamePasswordAuthenticationToken(
|
||||||
|
loginUser, null, loginUser.getAuthorities());
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
|
||||||
|
// 6. 记录登录信息 + logininfor
|
||||||
|
AsyncManager.me().execute(AsyncFactory.recordLogininfor(user.getUserName(), Constants.LOGIN_SUCCESS,
|
||||||
|
"短信验证码登录成功"));
|
||||||
|
userService.updateLoginInfo(user.getUserId(), IpUtils.getIpAddr(), DateUtils.getNowDate());
|
||||||
|
|
||||||
|
// 7. 颁发 token
|
||||||
|
String token = tokenService.createToken(loginUser);
|
||||||
|
return AjaxResult.success("登录成功").put(Constants.TOKEN, token);
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/registerExecutor")
|
@PostMapping("/registerExecutor")
|
||||||
public AjaxResult registerExecutor(@RequestBody Map<String, Object> body) {
|
public AjaxResult registerExecutor(@RequestBody Map<String, Object> body) {
|
||||||
String username = (String) body.get("username");
|
String username = (String) body.get("username");
|
||||||
|
|||||||
+2
-1
@@ -39,8 +39,9 @@ public class BizExecutorController extends BaseController
|
|||||||
@RequestParam(required = false, defaultValue = "50") Integer pageSize)
|
@RequestParam(required = false, defaultValue = "50") Integer pageSize)
|
||||||
{
|
{
|
||||||
StringBuilder sql = new StringBuilder()
|
StringBuilder sql = new StringBuilder()
|
||||||
|
// 仅返回 MAIN 主账号: 排除 SUB 子账号, 避免项目分配下拉混入下属执行人
|
||||||
.append("SELECT user_id, dept_id, user_name, nick_name, email, phonenumber, role_type, status, create_time ")
|
.append("SELECT user_id, dept_id, user_name, nick_name, email, phonenumber, role_type, status, create_time ")
|
||||||
.append("FROM sys_user WHERE del_flag = '0' AND status = '0' AND role_type = ?");
|
.append("FROM sys_user WHERE del_flag = '0' AND status = '0' AND role_type = ? AND account_type = 'MAIN'");
|
||||||
List<Object> args = new ArrayList<>();
|
List<Object> args = new ArrayList<>();
|
||||||
args.add(roleType);
|
args.add(roleType);
|
||||||
if (userId != null) {
|
if (userId != null) {
|
||||||
|
|||||||
+90
@@ -0,0 +1,90 @@
|
|||||||
|
package com.ruoyi.business.controller;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import com.ruoyi.business.dto.SmsValidForm;
|
||||||
|
import com.ruoyi.business.service.SysSmsService;
|
||||||
|
import com.ruoyi.common.core.controller.BaseController;
|
||||||
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
|
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||||
|
import com.ruoyi.system.mapper.SysUserMapper;
|
||||||
|
import com.ruoyi.system.service.ISysUserService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 忘记密码 - 通过短信验证码重置密码 (匿名访问)
|
||||||
|
*
|
||||||
|
* 流程:
|
||||||
|
* 1. forgotSendSms 发送短信 (校验手机号已注册)
|
||||||
|
* 2. resetPassword 校验短信验证码 + 重置密码 (一次提交, 简化前端)
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/business/auth")
|
||||||
|
public class BizForgotController extends BaseController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SysSmsService smsService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ISysUserService userService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SysUserMapper userMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BCryptPasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
@PostMapping("/forgotSendSms")
|
||||||
|
public AjaxResult forgotSendSms(@RequestBody Map<String, String> body) {
|
||||||
|
String phone = body == null ? null : body.get("phone");
|
||||||
|
if (phone == null || !phone.matches("^1\\d{10}$")) {
|
||||||
|
return error("请输入正确的手机号");
|
||||||
|
}
|
||||||
|
// 手机号必须已注册 (忘记密码反义: 该号查不到则不发)
|
||||||
|
if (!userService.isPhoneRegistered(phone)) {
|
||||||
|
return error("该手机号未注册, 请先注册账号");
|
||||||
|
}
|
||||||
|
String uuid = smsService.sendCode(phone);
|
||||||
|
return AjaxResult.success("验证码已发送", uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/resetPassword")
|
||||||
|
public AjaxResult resetPassword(@RequestBody Map<String, Object> body) {
|
||||||
|
String phone = (String) body.get("phone");
|
||||||
|
String code = (String) body.get("smsCode");
|
||||||
|
String uuid = (String) body.get("uuid");
|
||||||
|
String password = (String) body.get("password");
|
||||||
|
String confirmPassword = (String) body.get("confirmPassword");
|
||||||
|
|
||||||
|
// 1. 基础校验
|
||||||
|
if (phone == null || !phone.matches("^1\\d{10}$")) return error("手机号格式错误");
|
||||||
|
if (code == null || code.isEmpty()) return error("请输入短信验证码");
|
||||||
|
if (password == null || password.length() < 6 || password.length() > 20) return error("密码长度 6-20 位");
|
||||||
|
if (!password.equals(confirmPassword)) return error("两次密码输入不一致");
|
||||||
|
|
||||||
|
// 2. 校验短信验证码
|
||||||
|
SmsValidForm smsForm = new SmsValidForm();
|
||||||
|
smsForm.setPhone(phone);
|
||||||
|
smsForm.setSmsCode(code);
|
||||||
|
smsForm.setUuid(uuid);
|
||||||
|
try {
|
||||||
|
smsService.verifyCode(smsForm);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return error("验证码错误或已过期: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 查用户 (mapper.checkPhoneUnique 返回 SysUser 或 null, IService 包装成 boolean 不能用)
|
||||||
|
SysUser info = userMapper.checkPhoneUnique(phone);
|
||||||
|
if (info == null || info.getUserId() == null) {
|
||||||
|
return error("该手机号未注册");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 重置密码
|
||||||
|
int rows = userService.resetUserPwd(info.getUserId(), passwordEncoder.encode(password));
|
||||||
|
return rows > 0 ? success("密码重置成功, 请用新密码登录") : error("密码重置失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -10,14 +10,18 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import com.ruoyi.common.core.controller.BaseController;
|
import com.ruoyi.common.core.controller.BaseController;
|
||||||
import com.ruoyi.common.core.domain.AjaxResult;
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
|
import com.ruoyi.business.domain.BizArticle;
|
||||||
import com.ruoyi.business.domain.BizProject;
|
import com.ruoyi.business.domain.BizProject;
|
||||||
import com.ruoyi.business.domain.BizSupportLetter;
|
import com.ruoyi.business.domain.BizSupportLetter;
|
||||||
import com.ruoyi.business.domain.BizInvitation;
|
import com.ruoyi.business.domain.BizInvitation;
|
||||||
import com.ruoyi.business.domain.BizProjectPlan;
|
import com.ruoyi.business.domain.BizProjectPlan;
|
||||||
|
import com.ruoyi.business.service.IBizArticleService;
|
||||||
import com.ruoyi.business.service.IBizProjectService;
|
import com.ruoyi.business.service.IBizProjectService;
|
||||||
import com.ruoyi.business.service.IBizSupportLetterService;
|
import com.ruoyi.business.service.IBizSupportLetterService;
|
||||||
import com.ruoyi.business.service.IBizInvitationService;
|
import com.ruoyi.business.service.IBizInvitationService;
|
||||||
import com.ruoyi.business.service.IBizProjectPlanService;
|
import com.ruoyi.business.service.IBizProjectPlanService;
|
||||||
|
import com.ruoyi.business.service.IBizSpecialPlanService;
|
||||||
|
import com.ruoyi.business.domain.BizSpecialPlan;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 公开门户接口(无需登录)
|
* 公开门户接口(无需登录)
|
||||||
@@ -30,6 +34,38 @@ public class BizPublicController extends BaseController {
|
|||||||
@Autowired private IBizSupportLetterService supportLetterService;
|
@Autowired private IBizSupportLetterService supportLetterService;
|
||||||
@Autowired private IBizInvitationService invitationService;
|
@Autowired private IBizInvitationService invitationService;
|
||||||
@Autowired private IBizProjectPlanService projectPlanService;
|
@Autowired private IBizProjectPlanService projectPlanService;
|
||||||
|
@Autowired private IBizArticleService articleService;
|
||||||
|
@Autowired private IBizSpecialPlanService specialPlanService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按类型获取已发布的协议/政策 (供注册页底部链接打开)
|
||||||
|
* type: agreement | privacy
|
||||||
|
*/
|
||||||
|
@GetMapping("/article/{type}")
|
||||||
|
public AjaxResult articleByType(@PathVariable("type") String type)
|
||||||
|
{
|
||||||
|
BizArticle article = articleService.getByType(type);
|
||||||
|
return success(article);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公开接口 - 七大专项计划 (首页用)
|
||||||
|
*/
|
||||||
|
@GetMapping("/specialPlan/list")
|
||||||
|
public AjaxResult specialPlanList() {
|
||||||
|
return success(specialPlanService.selectPublicList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公开接口 - 专项计划详情 (供详情页)
|
||||||
|
* 注意: id 可能是临时id (小于0 = 新建未保存), 也支持按 序号 (sortOrder) 查
|
||||||
|
*/
|
||||||
|
@GetMapping("/specialPlan/{id}")
|
||||||
|
public AjaxResult specialPlanById(@PathVariable("id") Long id) {
|
||||||
|
BizSpecialPlan plan = specialPlanService.getById(id);
|
||||||
|
if (plan == null) return error("专项计划不存在");
|
||||||
|
return success(plan);
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/index")
|
@GetMapping("/index")
|
||||||
public AjaxResult index() {
|
public AjaxResult index() {
|
||||||
|
|||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
package com.ruoyi.business.controller;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import com.ruoyi.common.annotation.Log;
|
||||||
|
import com.ruoyi.common.core.controller.BaseController;
|
||||||
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
|
import com.ruoyi.common.core.page.TableDataInfo;
|
||||||
|
import com.ruoyi.common.enums.BusinessType;
|
||||||
|
import com.ruoyi.common.utils.SecurityUtils;
|
||||||
|
import com.ruoyi.business.domain.BizSpecialPlan;
|
||||||
|
import com.ruoyi.business.service.IBizSpecialPlanService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 七大专项计划 Controller (后台 admin)
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/business/specialPlan")
|
||||||
|
public class BizSpecialPlanController extends BaseController
|
||||||
|
{
|
||||||
|
@Autowired
|
||||||
|
private IBizSpecialPlanService specialPlanService;
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('business:specialPlan:list')")
|
||||||
|
@GetMapping("/list")
|
||||||
|
public TableDataInfo list(BizSpecialPlan entity)
|
||||||
|
{
|
||||||
|
startPage();
|
||||||
|
List<BizSpecialPlan> list = specialPlanService.selectList(entity);
|
||||||
|
return getDataTable(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('business:specialPlan:query')")
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public AjaxResult getInfo(@PathVariable("id") Long id)
|
||||||
|
{
|
||||||
|
return success(specialPlanService.getById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('business:specialPlan:add')")
|
||||||
|
@Log(title = "七大专项计划", businessType = BusinessType.INSERT)
|
||||||
|
@PostMapping
|
||||||
|
public AjaxResult add(@RequestBody BizSpecialPlan entity)
|
||||||
|
{
|
||||||
|
if (entity.getContentType() == null) entity.setContentType("rich");
|
||||||
|
if (entity.getStatus() == null) entity.setStatus("0");
|
||||||
|
if (entity.getSortOrder() == null) entity.setSortOrder(0);
|
||||||
|
entity.setCreateBy(SecurityUtils.getUsername());
|
||||||
|
entity.setUpdateBy(SecurityUtils.getUsername());
|
||||||
|
return toAjax(specialPlanService.insert(entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('business:specialPlan:edit')")
|
||||||
|
@Log(title = "七大专项计划", businessType = BusinessType.UPDATE)
|
||||||
|
@PutMapping
|
||||||
|
public AjaxResult edit(@RequestBody BizSpecialPlan entity)
|
||||||
|
{
|
||||||
|
entity.setUpdateBy(SecurityUtils.getUsername());
|
||||||
|
return toAjax(specialPlanService.updateByPrimaryKey(entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('business:specialPlan:remove')")
|
||||||
|
@Log(title = "七大专项计划", businessType = BusinessType.DELETE)
|
||||||
|
@DeleteMapping("/{ids}")
|
||||||
|
public AjaxResult remove(@PathVariable Long[] ids)
|
||||||
|
{
|
||||||
|
return toAjax(specialPlanService.deleteByPrimaryKeys(ids));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.ruoyi.business.domain;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import com.ruoyi.common.annotation.Excel;
|
||||||
|
import com.ruoyi.common.core.domain.BaseEntity;
|
||||||
|
|
||||||
|
/** 平台协议/隐私政策文章对象 BizArticle */
|
||||||
|
public class BizArticle extends BaseEntity
|
||||||
|
{
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** 文章ID */
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 标题 */
|
||||||
|
@Excel(name = "标题")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/** 类型 agreement=用户协议 privacy=隐私政策 */
|
||||||
|
@Excel(name = "类型", readConverterExp = "agreement=用户协议,privacy=隐私政策")
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
/** 正文 (HTML/富文本) */
|
||||||
|
@Excel(name = "正文")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
/** 状态 0启用 1停用 */
|
||||||
|
@Excel(name = "状态", readConverterExp = "0=启用,1=停用")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
public String getTitle() { return title; }
|
||||||
|
public void setTitle(String title) { this.title = title; }
|
||||||
|
public String getType() { return type; }
|
||||||
|
public void setType(String type) { this.type = type; }
|
||||||
|
public String getContent() { return content; }
|
||||||
|
public void setContent(String content) { this.content = content; }
|
||||||
|
public String getStatus() { return status; }
|
||||||
|
public void setStatus(String status) { this.status = status; }
|
||||||
|
}
|
||||||
@@ -58,7 +58,7 @@ public class BizMeeting extends BaseEntity {
|
|||||||
private Long projectId;
|
private Long projectId;
|
||||||
/** 项目名称 */
|
/** 项目名称 */
|
||||||
private String projectName;
|
private String projectName;
|
||||||
/** 所属公司名称 (冗余字段, 由 biz_project.org_name 同步) */
|
/** 所属公司名称 (冗余字段, 由 biz_project.sponsor_admin_user_name 同步) */
|
||||||
private String orgName;
|
private String orgName;
|
||||||
/** 监察意见 */
|
/** 监察意见 */
|
||||||
private String supervisionOpinion;
|
private String supervisionOpinion;
|
||||||
|
|||||||
@@ -49,12 +49,11 @@ public class BizProject extends BaseEntity {
|
|||||||
private BigDecimal ratingQ3;
|
private BigDecimal ratingQ3;
|
||||||
/** 评分维度: 合规安全 */
|
/** 评分维度: 合规安全 */
|
||||||
private BigDecimal ratingQ4;
|
private BigDecimal ratingQ4;
|
||||||
/** org_name (赞助方/执行方, 由 org_type 区分) */
|
/** 赞助方负责人用户名(冗余) — 原 org_name */
|
||||||
@Excel(name = "org_name")
|
@Excel(name = "sponsor_admin_user_name")
|
||||||
private String orgName;
|
private String sponsorAdminUserName;
|
||||||
/** org_type: sponsor 赞助方 / executor 执行方 */
|
/** 服务机构名称 (查询条件, 仅匹配 org_type='executor' + org_name LIKE) */
|
||||||
@Excel(name = "org_type")
|
private String execOrgName;
|
||||||
private String orgType;
|
|
||||||
/** project_form */
|
/** project_form */
|
||||||
@Excel(name = "project_form")
|
@Excel(name = "project_form")
|
||||||
private String projectForm;
|
private String projectForm;
|
||||||
@@ -64,9 +63,9 @@ public class BizProject extends BaseEntity {
|
|||||||
/** is_settled */
|
/** is_settled */
|
||||||
@Excel(name = "is_settled")
|
@Excel(name = "is_settled")
|
||||||
private String isSettled;
|
private String isSettled;
|
||||||
/** org_id (赞助方/执行方, 由 org_type 区分) */
|
/** 赞助方负责人用户ID (sys_user.user_id, role_type=sponsor) — 原 org_id */
|
||||||
@Excel(name = "org_id")
|
@Excel(name = "sponsor_admin_user_id")
|
||||||
private Long orgId;
|
private Long sponsorAdminUserId;
|
||||||
/** create_by */
|
/** create_by */
|
||||||
@Excel(name = "create_by")
|
@Excel(name = "create_by")
|
||||||
private String createBy;
|
private String createBy;
|
||||||
@@ -149,18 +148,18 @@ public class BizProject extends BaseEntity {
|
|||||||
public void setRatingQ3(BigDecimal ratingQ3) { this.ratingQ3 = ratingQ3; }
|
public void setRatingQ3(BigDecimal ratingQ3) { this.ratingQ3 = ratingQ3; }
|
||||||
public BigDecimal getRatingQ4() { return ratingQ4; }
|
public BigDecimal getRatingQ4() { return ratingQ4; }
|
||||||
public void setRatingQ4(BigDecimal ratingQ4) { this.ratingQ4 = ratingQ4; }
|
public void setRatingQ4(BigDecimal ratingQ4) { this.ratingQ4 = ratingQ4; }
|
||||||
public String getOrgName() { return orgName; }
|
public String getSponsorAdminUserName() { return sponsorAdminUserName; }
|
||||||
public void setOrgName(String orgName) { this.orgName = orgName; }
|
public void setSponsorAdminUserName(String sponsorAdminUserName) { this.sponsorAdminUserName = sponsorAdminUserName; }
|
||||||
public String getOrgType() { return orgType; }
|
public String getExecOrgName() { return execOrgName; }
|
||||||
public void setOrgType(String orgType) { this.orgType = orgType; }
|
public void setExecOrgName(String execOrgName) { this.execOrgName = execOrgName; }
|
||||||
public String getProjectForm() { return projectForm; }
|
public String getProjectForm() { return projectForm; }
|
||||||
public void setProjectForm(String projectForm) { this.projectForm = projectForm; }
|
public void setProjectForm(String projectForm) { this.projectForm = projectForm; }
|
||||||
public String getIsFinished() { return isFinished; }
|
public String getIsFinished() { return isFinished; }
|
||||||
public void setIsFinished(String isFinished) { this.isFinished = isFinished; }
|
public void setIsFinished(String isFinished) { this.isFinished = isFinished; }
|
||||||
public String getIsSettled() { return isSettled; }
|
public String getIsSettled() { return isSettled; }
|
||||||
public void setIsSettled(String isSettled) { this.isSettled = isSettled; }
|
public void setIsSettled(String isSettled) { this.isSettled = isSettled; }
|
||||||
public Long getOrgId() { return orgId; }
|
public Long getSponsorAdminUserId() { return sponsorAdminUserId; }
|
||||||
public void setOrgId(Long orgId) { this.orgId = orgId; }
|
public void setSponsorAdminUserId(Long sponsorAdminUserId) { this.sponsorAdminUserId = sponsorAdminUserId; }
|
||||||
public String getCreateBy() { return createBy; }
|
public String getCreateBy() { return createBy; }
|
||||||
public void setCreateBy(String createBy) { this.createBy = createBy; }
|
public void setCreateBy(String createBy) { this.createBy = createBy; }
|
||||||
public Date getCreateTime() { return createTime; }
|
public Date getCreateTime() { return createTime; }
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.ruoyi.business.domain;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import com.ruoyi.common.annotation.Excel;
|
||||||
|
import com.ruoyi.common.core.domain.BaseEntity;
|
||||||
|
|
||||||
|
/** 七大专项计划对象 BizSpecialPlan */
|
||||||
|
public class BizSpecialPlan extends BaseEntity
|
||||||
|
{
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** 专项计划ID */
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/** 专项计划名称 */
|
||||||
|
@Excel(name = "名称")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/** 内容类型 rich=富文本 file=上传文件 */
|
||||||
|
@Excel(name = "类型", readConverterExp = "rich=富文本,file=上传文件")
|
||||||
|
private String contentType;
|
||||||
|
|
||||||
|
/** 富文本内容 */
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
/** 上传文件URL (PDF/PNG) */
|
||||||
|
private String fileUrl;
|
||||||
|
|
||||||
|
/** 排序 */
|
||||||
|
@Excel(name = "排序")
|
||||||
|
private Integer sortOrder;
|
||||||
|
|
||||||
|
/** 状态 0启用 1停用 */
|
||||||
|
@Excel(name = "状态", readConverterExp = "0=启用,1=停用")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
public String getTitle() { return title; }
|
||||||
|
public void setTitle(String title) { this.title = title; }
|
||||||
|
public String getContentType() { return contentType; }
|
||||||
|
public void setContentType(String contentType) { this.contentType = contentType; }
|
||||||
|
public String getContent() { return content; }
|
||||||
|
public void setContent(String content) { this.content = content; }
|
||||||
|
public String getFileUrl() { return fileUrl; }
|
||||||
|
public void setFileUrl(String fileUrl) { this.fileUrl = fileUrl; }
|
||||||
|
public Integer getSortOrder() { return sortOrder; }
|
||||||
|
public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; }
|
||||||
|
public String getStatus() { return status; }
|
||||||
|
public void setStatus(String status) { this.status = status; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.ruoyi.business.mapper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import com.ruoyi.business.domain.BizArticle;
|
||||||
|
|
||||||
|
public interface BizArticleMapper
|
||||||
|
{
|
||||||
|
BizArticle selectByPrimaryKey(Long id);
|
||||||
|
List<BizArticle> selectList(BizArticle entity);
|
||||||
|
BizArticle selectByType(String type);
|
||||||
|
int insert(BizArticle entity);
|
||||||
|
int updateByPrimaryKey(BizArticle entity);
|
||||||
|
int deleteByPrimaryKey(Long id);
|
||||||
|
int deleteByPrimaryKeys(Long[] ids);
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package com.ruoyi.business.mapper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import com.ruoyi.business.domain.BizSpecialPlan;
|
||||||
|
|
||||||
|
public interface BizSpecialPlanMapper
|
||||||
|
{
|
||||||
|
BizSpecialPlan selectByPrimaryKey(Long id);
|
||||||
|
|
||||||
|
/** 仅查 status='0' 的, 用于首页 / 公开页 */
|
||||||
|
List<BizSpecialPlan> selectPublicList();
|
||||||
|
|
||||||
|
/** 后台管理: 按条件查全部 */
|
||||||
|
List<BizSpecialPlan> selectList(BizSpecialPlan entity);
|
||||||
|
|
||||||
|
int insert(BizSpecialPlan entity);
|
||||||
|
int updateByPrimaryKey(BizSpecialPlan entity);
|
||||||
|
int deleteByPrimaryKey(Long id);
|
||||||
|
int deleteByPrimaryKeys(Long[] ids);
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package com.ruoyi.business.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import com.ruoyi.business.domain.BizArticle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台协议/隐私政策文章 Service 接口
|
||||||
|
*/
|
||||||
|
public interface IBizArticleService
|
||||||
|
{
|
||||||
|
BizArticle getById(Long id);
|
||||||
|
List<BizArticle> selectList(BizArticle entity);
|
||||||
|
BizArticle getByType(String type);
|
||||||
|
int insert(BizArticle entity);
|
||||||
|
int updateByPrimaryKey(BizArticle entity);
|
||||||
|
int deleteByPrimaryKey(Long id);
|
||||||
|
int deleteByPrimaryKeys(Long[] ids);
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package com.ruoyi.business.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import com.ruoyi.business.domain.BizSpecialPlan;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 七大专项计划 Service 接口
|
||||||
|
*/
|
||||||
|
public interface IBizSpecialPlanService
|
||||||
|
{
|
||||||
|
BizSpecialPlan getById(Long id);
|
||||||
|
List<BizSpecialPlan> selectPublicList();
|
||||||
|
List<BizSpecialPlan> selectList(BizSpecialPlan entity);
|
||||||
|
int insert(BizSpecialPlan entity);
|
||||||
|
int updateByPrimaryKey(BizSpecialPlan entity);
|
||||||
|
int deleteByPrimaryKey(Long id);
|
||||||
|
int deleteByPrimaryKeys(Long[] ids);
|
||||||
|
}
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
package com.ruoyi.business.service.impl;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import com.ruoyi.business.domain.BizArticle;
|
||||||
|
import com.ruoyi.business.mapper.BizArticleMapper;
|
||||||
|
import com.ruoyi.business.service.IBizArticleService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台协议/隐私政策文章 Service 业务层
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class BizArticleServiceImpl implements IBizArticleService
|
||||||
|
{
|
||||||
|
@Autowired
|
||||||
|
private BizArticleMapper articleMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BizArticle getById(Long id)
|
||||||
|
{
|
||||||
|
return articleMapper.selectByPrimaryKey(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<BizArticle> selectList(BizArticle entity)
|
||||||
|
{
|
||||||
|
return articleMapper.selectList(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BizArticle getByType(String type)
|
||||||
|
{
|
||||||
|
return articleMapper.selectByType(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int insert(BizArticle entity)
|
||||||
|
{
|
||||||
|
return articleMapper.insert(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int updateByPrimaryKey(BizArticle entity)
|
||||||
|
{
|
||||||
|
return articleMapper.updateByPrimaryKey(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int deleteByPrimaryKey(Long id)
|
||||||
|
{
|
||||||
|
return articleMapper.deleteByPrimaryKey(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int deleteByPrimaryKeys(Long[] ids)
|
||||||
|
{
|
||||||
|
return articleMapper.deleteByPrimaryKeys(ids);
|
||||||
|
}
|
||||||
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
package com.ruoyi.business.service.impl;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import com.ruoyi.business.domain.BizSpecialPlan;
|
||||||
|
import com.ruoyi.business.mapper.BizSpecialPlanMapper;
|
||||||
|
import com.ruoyi.business.service.IBizSpecialPlanService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 七大专项计划 Service 业务层
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class BizSpecialPlanServiceImpl implements IBizSpecialPlanService
|
||||||
|
{
|
||||||
|
@Autowired
|
||||||
|
private BizSpecialPlanMapper specialPlanMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BizSpecialPlan getById(Long id) { return specialPlanMapper.selectByPrimaryKey(id); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<BizSpecialPlan> selectPublicList() { return specialPlanMapper.selectPublicList(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<BizSpecialPlan> selectList(BizSpecialPlan entity) { return specialPlanMapper.selectList(entity); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int insert(BizSpecialPlan entity) { return specialPlanMapper.insert(entity); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int updateByPrimaryKey(BizSpecialPlan entity) { return specialPlanMapper.updateByPrimaryKey(entity); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int deleteByPrimaryKey(Long id) { return specialPlanMapper.deleteByPrimaryKey(id); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int deleteByPrimaryKeys(Long[] ids) { return specialPlanMapper.deleteByPrimaryKeys(ids); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.ruoyi.business.mapper.BizArticleMapper">
|
||||||
|
<resultMap id="BaseResultMap" type="BizArticle">
|
||||||
|
<id property="id" column="id" />
|
||||||
|
<result property="title" column="title" />
|
||||||
|
<result property="type" column="type" />
|
||||||
|
<result property="content" column="content" />
|
||||||
|
<result property="status" column="status" />
|
||||||
|
<result property="createBy" column="create_by" />
|
||||||
|
<result property="createTime" column="create_time" />
|
||||||
|
<result property="updateBy" column="update_by" />
|
||||||
|
<result property="updateTime" column="update_time" />
|
||||||
|
<result property="remark" column="remark" />
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<sql id="selectFields">
|
||||||
|
select id, title, type, content, status, create_by, create_time, update_by, update_time, remark
|
||||||
|
from biz_article
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="selectByPrimaryKey" parameterType="Long" resultMap="BaseResultMap">
|
||||||
|
<include refid="selectFields"/>
|
||||||
|
where id = #{id}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectList" parameterType="BizArticle" resultMap="BaseResultMap">
|
||||||
|
<include refid="selectFields"/>
|
||||||
|
<where>
|
||||||
|
<if test="type != null and type != ''"> and type = #{type}</if>
|
||||||
|
<if test="title != null and title != ''"> and title like concat('%', #{title}, '%')</if>
|
||||||
|
<if test="status != null and status != ''"> and status = #{status}</if>
|
||||||
|
</where>
|
||||||
|
order by type asc, create_time desc
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectByType" parameterType="String" resultMap="BaseResultMap">
|
||||||
|
<include refid="selectFields"/>
|
||||||
|
where type = #{type} and status = '0'
|
||||||
|
limit 1
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insert" parameterType="BizArticle" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
insert into biz_article
|
||||||
|
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||||
|
<if test="title != null and title != ''">title,</if>
|
||||||
|
<if test="type != null and type != ''">type,</if>
|
||||||
|
<if test="content != null">content,</if>
|
||||||
|
<if test="status != null">status,</if>
|
||||||
|
<if test="createBy != null">create_by,</if>
|
||||||
|
create_time,
|
||||||
|
<if test="updateBy != null">update_by,</if>
|
||||||
|
update_time,
|
||||||
|
<if test="remark != null">remark,</if>
|
||||||
|
</trim>
|
||||||
|
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||||
|
<if test="title != null and title != ''">#{title},</if>
|
||||||
|
<if test="type != null and type != ''">#{type},</if>
|
||||||
|
<if test="content != null">#{content},</if>
|
||||||
|
<if test="status != null">#{status},</if>
|
||||||
|
<if test="createBy != null">#{createBy},</if>
|
||||||
|
sysdate(),
|
||||||
|
<if test="updateBy != null">#{updateBy},</if>
|
||||||
|
sysdate(),
|
||||||
|
<if test="remark != null">#{remark},</if>
|
||||||
|
</trim>
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="updateByPrimaryKey" parameterType="BizArticle">
|
||||||
|
update biz_article
|
||||||
|
<trim prefix="SET" suffixOverrides=",">
|
||||||
|
<if test="title != null and title != ''">title = #{title},</if>
|
||||||
|
<if test="type != null and type != ''">type = #{type},</if>
|
||||||
|
<if test="content != null">content = #{content},</if>
|
||||||
|
<if test="status != null">status = #{status},</if>
|
||||||
|
<if test="updateBy != null">update_by = #{updateBy},</if>
|
||||||
|
update_time = sysdate(),
|
||||||
|
<if test="remark != null">remark = #{remark},</if>
|
||||||
|
</trim>
|
||||||
|
where id = #{id}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<delete id="deleteByPrimaryKey" parameterType="Long">
|
||||||
|
delete from biz_article where id = #{id}
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
<delete id="deleteByPrimaryKeys" parameterType="Long[]">
|
||||||
|
delete from biz_article where id in
|
||||||
|
<foreach collection="array" item="id" open="(" separator="," close=")">
|
||||||
|
#{id}
|
||||||
|
</foreach>
|
||||||
|
</delete>
|
||||||
|
</mapper>
|
||||||
@@ -17,12 +17,12 @@
|
|||||||
<result property="ratingQ2" column="rating_q2" />
|
<result property="ratingQ2" column="rating_q2" />
|
||||||
<result property="ratingQ3" column="rating_q3" />
|
<result property="ratingQ3" column="rating_q3" />
|
||||||
<result property="ratingQ4" column="rating_q4" />
|
<result property="ratingQ4" column="rating_q4" />
|
||||||
<result property="orgName" column="org_name" />
|
<result property="sponsorAdminUserName" column="sponsor_admin_user_name" />
|
||||||
<result property="orgType" column="org_type" />
|
<result property="sponsorAdminUserId" column="sponsor_admin_user_id" />
|
||||||
<result property="projectForm" column="project_form" />
|
<result property="projectForm" column="project_form" />
|
||||||
<result property="isFinished" column="is_finished" />
|
<result property="isFinished" column="is_finished" />
|
||||||
<result property="isSettled" column="is_settled" />
|
<result property="isSettled" column="is_settled" />
|
||||||
<result property="orgId" column="org_id" />
|
<result property="sponsorAdminUserId" column="sponsor_admin_user_id" />
|
||||||
<result property="createBy" column="create_by" />
|
<result property="createBy" column="create_by" />
|
||||||
<result property="createTime" column="create_time" />
|
<result property="createTime" column="create_time" />
|
||||||
<result property="updateBy" column="update_by" />
|
<result property="updateBy" column="update_by" />
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
<result property="announcementType" column="announcement_type" />
|
<result property="announcementType" column="announcement_type" />
|
||||||
</resultMap>
|
</resultMap>
|
||||||
<sql id="selectFields">
|
<sql id="selectFields">
|
||||||
select project_id, project_no, project_name, total_sessions, done_sessions, todo_sessions, total_amount, available_amount, paid_labor_amount, paid_meeting_amount, rating_score, rating_q1, rating_q2, rating_q3, rating_q4, org_name, org_type, project_form, is_finished, is_settled, org_id, create_by, create_time, update_by, update_time, manage_fee, start_time, end_time, submit_deadline_days, support_contract_url, execute_contract_url, invitation_url, support_letter_url, publish_url, notice_url, schedule_url, is_published, publish_time, announcement_type
|
select project_id, project_no, project_name, total_sessions, done_sessions, todo_sessions, total_amount, available_amount, paid_labor_amount, paid_meeting_amount, rating_score, rating_q1, rating_q2, rating_q3, rating_q4, sponsor_admin_user_name, project_form, is_finished, is_settled, sponsor_admin_user_id, create_by, create_time, update_by, update_time, manage_fee, start_time, end_time, submit_deadline_days, support_contract_url, execute_contract_url, invitation_url, support_letter_url, publish_url, notice_url, schedule_url, is_published, publish_time, announcement_type
|
||||||
from biz_project
|
from biz_project
|
||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
@@ -51,8 +51,8 @@
|
|||||||
<sql id="selectFieldsForSponsor">
|
<sql id="selectFieldsForSponsor">
|
||||||
select p.project_id, p.project_no, p.project_name, p.total_sessions, p.done_sessions, p.todo_sessions,
|
select p.project_id, p.project_no, p.project_name, p.total_sessions, p.done_sessions, p.todo_sessions,
|
||||||
p.total_amount, p.available_amount, p.paid_labor_amount, p.paid_meeting_amount,
|
p.total_amount, p.available_amount, p.paid_labor_amount, p.paid_meeting_amount,
|
||||||
p.org_name, p.org_type, p.project_form, p.is_finished, p.is_settled,
|
p.sponsor_admin_user_name, p.project_form, p.is_finished, p.is_settled,
|
||||||
p.org_id,
|
p.sponsor_admin_user_id,
|
||||||
p.create_by, p.create_time, p.update_by, p.update_time, p.manage_fee,
|
p.create_by, p.create_time, p.update_by, p.update_time, p.manage_fee,
|
||||||
p.start_time, p.end_time, p.submit_deadline_days,
|
p.start_time, p.end_time, p.submit_deadline_days,
|
||||||
p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url,
|
p.support_contract_url, p.execute_contract_url, p.invitation_url, p.support_letter_url,
|
||||||
@@ -107,8 +107,26 @@
|
|||||||
<if test="projectNo != null and projectNo != ''">and project_no like concat('%', #{projectNo}, '%')</if>
|
<if test="projectNo != null and projectNo != ''">and project_no like concat('%', #{projectNo}, '%')</if>
|
||||||
<if test="projectName != null and projectName != ''">and project_name like concat('%', #{projectName}, '%')</if>
|
<if test="projectName != null and projectName != ''">and project_name like concat('%', #{projectName}, '%')</if>
|
||||||
<if test="projectForm != null and projectForm != ''">and project_form = #{projectForm}</if>
|
<if test="projectForm != null and projectForm != ''">and project_form = #{projectForm}</if>
|
||||||
<if test="orgName != null and orgName != ''">and org_name like concat('%', #{orgName}, '%')</if>
|
<if test="sponsorAdminUserName != null and sponsorAdminUserName != ''">
|
||||||
<if test="orgType != null and orgType != ''">and org_type = #{orgType}</if>
|
and exists (
|
||||||
|
select 1 from sys_user su
|
||||||
|
where su.user_id = sponsor_admin_user_id
|
||||||
|
and su.role_type = 'sponsor'
|
||||||
|
and (su.user_name like concat('%', #{sponsorAdminUserName}, '%')
|
||||||
|
or su.nick_name like concat('%', #{sponsorAdminUserName}, '%'))
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="execOrgName != null and execOrgName != ''">
|
||||||
|
and exists (
|
||||||
|
select 1 from biz_project_assign bpa
|
||||||
|
join sys_user su on su.user_id = bpa.exec_user_id
|
||||||
|
where bpa.project_id = project_id
|
||||||
|
and su.role_type = 'executor'
|
||||||
|
and (su.user_name like concat('%', #{execOrgName}, '%')
|
||||||
|
or su.nick_name like concat('%', #{execOrgName}, '%')
|
||||||
|
or bpa.exec_org like concat('%', #{execOrgName}, '%'))
|
||||||
|
)
|
||||||
|
</if>
|
||||||
<if test="isFinished != null and isFinished != ''">and is_finished = #{isFinished}</if>
|
<if test="isFinished != null and isFinished != ''">and is_finished = #{isFinished}</if>
|
||||||
<if test="isSettled != null and isSettled != ''">and is_settled = #{isSettled}</if>
|
<if test="isSettled != null and isSettled != ''">and is_settled = #{isSettled}</if>
|
||||||
<if test="isPublished != null and isPublished != ''">and is_published = #{isPublished}</if>
|
<if test="isPublished != null and isPublished != ''">and is_published = #{isPublished}</if>
|
||||||
@@ -133,12 +151,11 @@
|
|||||||
<if test="ratingQ2 != null">rating_q2,</if>
|
<if test="ratingQ2 != null">rating_q2,</if>
|
||||||
<if test="ratingQ3 != null">rating_q3,</if>
|
<if test="ratingQ3 != null">rating_q3,</if>
|
||||||
<if test="ratingQ4 != null">rating_q4,</if>
|
<if test="ratingQ4 != null">rating_q4,</if>
|
||||||
<if test="orgName != null and orgName != ''">org_name,</if>
|
<if test="sponsorAdminUserName != null and sponsorAdminUserName != ''">sponsor_admin_user_name,</if>
|
||||||
<if test="orgType != null and orgType != ''">org_type,</if>
|
|
||||||
<if test="projectForm != null and projectForm != ''">project_form,</if>
|
<if test="projectForm != null and projectForm != ''">project_form,</if>
|
||||||
<if test="isFinished != null and isFinished != ''">is_finished,</if>
|
<if test="isFinished != null and isFinished != ''">is_finished,</if>
|
||||||
<if test="isSettled != null and isSettled != ''">is_settled,</if>
|
<if test="isSettled != null and isSettled != ''">is_settled,</if>
|
||||||
<if test="orgId != null">org_id,</if>
|
<if test="sponsorAdminUserId != null">sponsor_admin_user_id,</if>
|
||||||
<if test="createBy != null">create_by,</if>
|
<if test="createBy != null">create_by,</if>
|
||||||
<if test="createTime != null">create_time,</if>
|
<if test="createTime != null">create_time,</if>
|
||||||
<if test="updateBy != null">update_by,</if>
|
<if test="updateBy != null">update_by,</if>
|
||||||
@@ -174,12 +191,11 @@
|
|||||||
<if test="ratingQ2 != null">#{ratingQ2},</if>
|
<if test="ratingQ2 != null">#{ratingQ2},</if>
|
||||||
<if test="ratingQ3 != null">#{ratingQ3},</if>
|
<if test="ratingQ3 != null">#{ratingQ3},</if>
|
||||||
<if test="ratingQ4 != null">#{ratingQ4},</if>
|
<if test="ratingQ4 != null">#{ratingQ4},</if>
|
||||||
<if test="orgName != null and orgName != ''">#{orgName},</if>
|
<if test="sponsorAdminUserName != null and sponsorAdminUserName != ''">#{sponsorAdminUserName},</if>
|
||||||
<if test="orgType != null and orgType != ''">#{orgType},</if>
|
|
||||||
<if test="projectForm != null and projectForm != ''">#{projectForm},</if>
|
<if test="projectForm != null and projectForm != ''">#{projectForm},</if>
|
||||||
<if test="isFinished != null and isFinished != ''">#{isFinished},</if>
|
<if test="isFinished != null and isFinished != ''">#{isFinished},</if>
|
||||||
<if test="isSettled != null and isSettled != ''">#{isSettled},</if>
|
<if test="isSettled != null and isSettled != ''">#{isSettled},</if>
|
||||||
<if test="orgId != null">#{orgId},</if>
|
<if test="sponsorAdminUserId != null">#{sponsorAdminUserId},</if>
|
||||||
<if test="createBy != null">#{createBy},</if>
|
<if test="createBy != null">#{createBy},</if>
|
||||||
<if test="createTime != null">#{createTime},</if>
|
<if test="createTime != null">#{createTime},</if>
|
||||||
<if test="updateBy != null">#{updateBy},</if>
|
<if test="updateBy != null">#{updateBy},</if>
|
||||||
@@ -231,12 +247,11 @@
|
|||||||
<if test="ratingQ2 != null">rating_q2 = #{ratingQ2},</if>
|
<if test="ratingQ2 != null">rating_q2 = #{ratingQ2},</if>
|
||||||
<if test="ratingQ3 != null">rating_q3 = #{ratingQ3},</if>
|
<if test="ratingQ3 != null">rating_q3 = #{ratingQ3},</if>
|
||||||
<if test="ratingQ4 != null">rating_q4 = #{ratingQ4},</if>
|
<if test="ratingQ4 != null">rating_q4 = #{ratingQ4},</if>
|
||||||
<if test="orgName != null and orgName != ''">org_name = #{orgName},</if>
|
<if test="sponsorAdminUserName != null and sponsorAdminUserName != ''">sponsor_admin_user_name = #{sponsorAdminUserName},</if>
|
||||||
<if test="orgType != null and orgType != ''">org_type = #{orgType},</if>
|
|
||||||
<if test="projectForm != null and projectForm != ''">project_form = #{projectForm},</if>
|
<if test="projectForm != null and projectForm != ''">project_form = #{projectForm},</if>
|
||||||
<if test="isFinished != null and isFinished != ''">is_finished = #{isFinished},</if>
|
<if test="isFinished != null and isFinished != ''">is_finished = #{isFinished},</if>
|
||||||
<if test="isSettled != null and isSettled != ''">is_settled = #{isSettled},</if>
|
<if test="isSettled != null and isSettled != ''">is_settled = #{isSettled},</if>
|
||||||
<if test="orgId != null">org_id = #{orgId},</if>
|
<if test="sponsorAdminUserId != null">sponsor_admin_user_id = #{sponsorAdminUserId},</if>
|
||||||
<if test="createBy != null">create_by = #{createBy},</if>
|
<if test="createBy != null">create_by = #{createBy},</if>
|
||||||
<if test="createTime != null">create_time = #{createTime},</if>
|
<if test="createTime != null">create_time = #{createTime},</if>
|
||||||
<if test="updateBy != null">update_by = #{updateBy},</if>
|
<if test="updateBy != null">update_by = #{updateBy},</if>
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.ruoyi.business.mapper.BizSpecialPlanMapper">
|
||||||
|
<resultMap id="BaseResultMap" type="BizSpecialPlan">
|
||||||
|
<id property="id" column="id" />
|
||||||
|
<result property="title" column="title" />
|
||||||
|
<result property="contentType" column="content_type" />
|
||||||
|
<result property="content" column="content" />
|
||||||
|
<result property="fileUrl" column="file_url" />
|
||||||
|
<result property="sortOrder" column="sort_order" />
|
||||||
|
<result property="status" column="status" />
|
||||||
|
<result property="createBy" column="create_by" />
|
||||||
|
<result property="createTime" column="create_time" />
|
||||||
|
<result property="updateBy" column="update_by" />
|
||||||
|
<result property="updateTime" column="update_time" />
|
||||||
|
<result property="remark" column="remark" />
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<sql id="selectFields">
|
||||||
|
select id, title, content_type, content, file_url, sort_order, status,
|
||||||
|
create_by, create_time, update_by, update_time, remark
|
||||||
|
from biz_special_plan
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="selectByPrimaryKey" parameterType="Long" resultMap="BaseResultMap">
|
||||||
|
<include refid="selectFields"/>
|
||||||
|
where id = #{id}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 公开: 仅启用 (status=0), 按 sort_order 升序 -->
|
||||||
|
<select id="selectPublicList" resultMap="BaseResultMap">
|
||||||
|
<include refid="selectFields"/>
|
||||||
|
where status = '0'
|
||||||
|
order by sort_order asc, create_time asc
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 后台管理: 支持 title/status 模糊/精确 -->
|
||||||
|
<select id="selectList" parameterType="BizSpecialPlan" resultMap="BaseResultMap">
|
||||||
|
<include refid="selectFields"/>
|
||||||
|
<where>
|
||||||
|
<if test="title != null and title != ''"> and title like concat('%', #{title}, '%')</if>
|
||||||
|
<if test="contentType != null and contentType != ''"> and content_type = #{contentType}</if>
|
||||||
|
<if test="status != null and status != ''"> and status = #{status}</if>
|
||||||
|
</where>
|
||||||
|
order by sort_order asc, id asc
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insert" parameterType="BizSpecialPlan" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
insert into biz_special_plan
|
||||||
|
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||||
|
<if test="title != null and title != ''">title,</if>
|
||||||
|
<if test="contentType != null and contentType != ''">content_type,</if>
|
||||||
|
<if test="content != null">content,</if>
|
||||||
|
<if test="fileUrl != null">file_url,</if>
|
||||||
|
<if test="sortOrder != null">sort_order,</if>
|
||||||
|
<if test="status != null">status,</if>
|
||||||
|
<if test="createBy != null">create_by,</if>
|
||||||
|
create_time,
|
||||||
|
<if test="updateBy != null">update_by,</if>
|
||||||
|
update_time,
|
||||||
|
<if test="remark != null">remark,</if>
|
||||||
|
</trim>
|
||||||
|
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||||
|
<if test="title != null and title != ''">#{title},</if>
|
||||||
|
<if test="contentType != null and contentType != ''">#{contentType},</if>
|
||||||
|
<if test="content != null">#{content},</if>
|
||||||
|
<if test="fileUrl != null">#{fileUrl},</if>
|
||||||
|
<if test="sortOrder != null">#{sortOrder},</if>
|
||||||
|
<if test="status != null">#{status},</if>
|
||||||
|
<if test="createBy != null">#{createBy},</if>
|
||||||
|
sysdate(),
|
||||||
|
<if test="updateBy != null">#{updateBy},</if>
|
||||||
|
sysdate(),
|
||||||
|
<if test="remark != null">#{remark},</if>
|
||||||
|
</trim>
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="updateByPrimaryKey" parameterType="BizSpecialPlan">
|
||||||
|
update biz_special_plan
|
||||||
|
<trim prefix="SET" suffixOverrides=",">
|
||||||
|
<if test="title != null and title != ''">title = #{title},</if>
|
||||||
|
<if test="contentType != null and contentType != ''">content_type = #{contentType},</if>
|
||||||
|
<if test="content != null">content = #{content},</if>
|
||||||
|
<if test="fileUrl != null">file_url = #{fileUrl},</if>
|
||||||
|
<if test="sortOrder != null">sort_order = #{sortOrder},</if>
|
||||||
|
<if test="status != null">status = #{status},</if>
|
||||||
|
<if test="updateBy != null">update_by = #{updateBy},</if>
|
||||||
|
update_time = sysdate(),
|
||||||
|
<if test="remark != null">remark = #{remark},</if>
|
||||||
|
</trim>
|
||||||
|
where id = #{id}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<delete id="deleteByPrimaryKey" parameterType="Long">
|
||||||
|
delete from biz_special_plan where id = #{id}
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
<delete id="deleteByPrimaryKeys" parameterType="Long[]">
|
||||||
|
delete from biz_special_plan where id in
|
||||||
|
<foreach collection="array" item="id" open="(" separator="," close=")">
|
||||||
|
#{id}
|
||||||
|
</foreach>
|
||||||
|
</delete>
|
||||||
|
</mapper>
|
||||||
Generated
+493
@@ -9,6 +9,8 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.0",
|
"@element-plus/icons-vue": "^2.3.0",
|
||||||
|
"@wangeditor/editor": "^5.1.23",
|
||||||
|
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"element-plus": "^2.4.0",
|
"element-plus": "^2.4.0",
|
||||||
"pinia": "^2.1.0",
|
"pinia": "^2.1.0",
|
||||||
@@ -63,6 +65,14 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/types": {
|
"node_modules/@babel/types": {
|
||||||
"version": "7.29.8",
|
"version": "7.29.8",
|
||||||
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.8.tgz",
|
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.8.tgz",
|
||||||
@@ -1169,12 +1179,22 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@transloadit/prettier-bytes": {
|
||||||
|
"version": "0.0.7",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@transloadit/prettier-bytes/-/prettier-bytes-0.0.7.tgz",
|
||||||
|
"integrity": "sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA=="
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz",
|
||||||
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/event-emitter": {
|
||||||
|
"version": "0.3.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/event-emitter/-/event-emitter-0.3.5.tgz",
|
||||||
|
"integrity": "sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ=="
|
||||||
|
},
|
||||||
"node_modules/@types/lodash": {
|
"node_modules/@types/lodash": {
|
||||||
"version": "4.17.25",
|
"version": "4.17.25",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.25.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.25.tgz",
|
||||||
@@ -1193,6 +1213,56 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
||||||
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="
|
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@uppy/companion-client": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@uppy/companion-client/-/companion-client-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==",
|
||||||
|
"dependencies": {
|
||||||
|
"@uppy/utils": "^4.1.2",
|
||||||
|
"namespace-emitter": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@uppy/core": {
|
||||||
|
"version": "2.3.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@uppy/core/-/core-2.3.4.tgz",
|
||||||
|
"integrity": "sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@transloadit/prettier-bytes": "0.0.7",
|
||||||
|
"@uppy/store-default": "^2.1.1",
|
||||||
|
"@uppy/utils": "^4.1.3",
|
||||||
|
"lodash.throttle": "^4.1.1",
|
||||||
|
"mime-match": "^1.0.2",
|
||||||
|
"namespace-emitter": "^2.0.1",
|
||||||
|
"nanoid": "^3.1.25",
|
||||||
|
"preact": "^10.5.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@uppy/store-default": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@uppy/store-default/-/store-default-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ=="
|
||||||
|
},
|
||||||
|
"node_modules/@uppy/utils": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@uppy/utils/-/utils-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw==",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash.throttle": "^4.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@uppy/xhr-upload": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@uppy/xhr-upload/-/xhr-upload-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@uppy/companion-client": "^2.2.2",
|
||||||
|
"@uppy/utils": "^4.1.2",
|
||||||
|
"nanoid": "^3.1.25"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@uppy/core": "^2.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vitejs/plugin-vue": {
|
"node_modules/@vitejs/plugin-vue": {
|
||||||
"version": "5.2.4",
|
"version": "5.2.4",
|
||||||
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
|
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
|
||||||
@@ -1335,6 +1405,156 @@
|
|||||||
"vue": "^3.5.0"
|
"vue": "^3.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@wangeditor/basic-modules": {
|
||||||
|
"version": "1.1.7",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@wangeditor/basic-modules/-/basic-modules-1.1.7.tgz",
|
||||||
|
"integrity": "sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==",
|
||||||
|
"dependencies": {
|
||||||
|
"is-url": "^1.2.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@wangeditor/core": "1.x",
|
||||||
|
"dom7": "^3.0.0",
|
||||||
|
"lodash.throttle": "^4.1.1",
|
||||||
|
"nanoid": "^3.2.0",
|
||||||
|
"slate": "^0.72.0",
|
||||||
|
"snabbdom": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@wangeditor/code-highlight": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@wangeditor/code-highlight/-/code-highlight-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-iazHwO14XpCuIWJNTQTikqUhGKyqj+dUNWJ9288Oym9M2xMVHvnsOmDU2sgUDWVy+pOLojReMPgXCsvvNlOOhw==",
|
||||||
|
"dependencies": {
|
||||||
|
"prismjs": "^1.23.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@wangeditor/core": "1.x",
|
||||||
|
"dom7": "^3.0.0",
|
||||||
|
"slate": "^0.72.0",
|
||||||
|
"snabbdom": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@wangeditor/core": {
|
||||||
|
"version": "1.1.19",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@wangeditor/core/-/core-1.1.19.tgz",
|
||||||
|
"integrity": "sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/event-emitter": "^0.3.3",
|
||||||
|
"event-emitter": "^0.3.5",
|
||||||
|
"html-void-elements": "^2.0.0",
|
||||||
|
"i18next": "^20.4.0",
|
||||||
|
"scroll-into-view-if-needed": "^2.2.28",
|
||||||
|
"slate-history": "^0.66.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@uppy/core": "^2.1.1",
|
||||||
|
"@uppy/xhr-upload": "^2.0.3",
|
||||||
|
"dom7": "^3.0.0",
|
||||||
|
"is-hotkey": "^0.2.0",
|
||||||
|
"lodash.camelcase": "^4.3.0",
|
||||||
|
"lodash.clonedeep": "^4.5.0",
|
||||||
|
"lodash.debounce": "^4.0.8",
|
||||||
|
"lodash.foreach": "^4.5.0",
|
||||||
|
"lodash.isequal": "^4.5.0",
|
||||||
|
"lodash.throttle": "^4.1.1",
|
||||||
|
"lodash.toarray": "^4.4.0",
|
||||||
|
"nanoid": "^3.2.0",
|
||||||
|
"slate": "^0.72.0",
|
||||||
|
"snabbdom": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@wangeditor/editor": {
|
||||||
|
"version": "5.1.23",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@wangeditor/editor/-/editor-5.1.23.tgz",
|
||||||
|
"integrity": "sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@uppy/core": "^2.1.1",
|
||||||
|
"@uppy/xhr-upload": "^2.0.3",
|
||||||
|
"@wangeditor/basic-modules": "^1.1.7",
|
||||||
|
"@wangeditor/code-highlight": "^1.0.3",
|
||||||
|
"@wangeditor/core": "^1.1.19",
|
||||||
|
"@wangeditor/list-module": "^1.0.5",
|
||||||
|
"@wangeditor/table-module": "^1.1.4",
|
||||||
|
"@wangeditor/upload-image-module": "^1.0.2",
|
||||||
|
"@wangeditor/video-module": "^1.1.4",
|
||||||
|
"dom7": "^3.0.0",
|
||||||
|
"is-hotkey": "^0.2.0",
|
||||||
|
"lodash.camelcase": "^4.3.0",
|
||||||
|
"lodash.clonedeep": "^4.5.0",
|
||||||
|
"lodash.debounce": "^4.0.8",
|
||||||
|
"lodash.foreach": "^4.5.0",
|
||||||
|
"lodash.isequal": "^4.5.0",
|
||||||
|
"lodash.throttle": "^4.1.1",
|
||||||
|
"lodash.toarray": "^4.4.0",
|
||||||
|
"nanoid": "^3.2.0",
|
||||||
|
"slate": "^0.72.0",
|
||||||
|
"snabbdom": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@wangeditor/editor-for-vue": {
|
||||||
|
"version": "5.1.12",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@wangeditor/editor-for-vue/-/editor-for-vue-5.1.12.tgz",
|
||||||
|
"integrity": "sha512-0Ds3D8I+xnpNWezAeO7HmPRgTfUxHLMd9JKcIw+QzvSmhC5xUHbpCcLU+KLmeBKTR/zffnS5GQo6qi3GhTMJWQ==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@wangeditor/editor": ">=5.1.0",
|
||||||
|
"vue": "^3.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@wangeditor/list-module": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@wangeditor/list-module/-/list-module-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-uDuYTP6DVhcYf7mF1pTlmNn5jOb4QtcVhYwSSAkyg09zqxI1qBqsfUnveeDeDqIuptSJhkh81cyxi+MF8sEPOQ==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@wangeditor/core": "1.x",
|
||||||
|
"dom7": "^3.0.0",
|
||||||
|
"slate": "^0.72.0",
|
||||||
|
"snabbdom": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@wangeditor/table-module": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@wangeditor/table-module/-/table-module-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-5saanU9xuEocxaemGdNi9t8MCDSucnykEC6jtuiT72kt+/Hhh4nERYx1J20OPsTCCdVr7hIyQenFD1iSRkIQ6w==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@wangeditor/core": "1.x",
|
||||||
|
"dom7": "^3.0.0",
|
||||||
|
"lodash.isequal": "^4.5.0",
|
||||||
|
"lodash.throttle": "^4.1.1",
|
||||||
|
"nanoid": "^3.2.0",
|
||||||
|
"slate": "^0.72.0",
|
||||||
|
"snabbdom": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@wangeditor/upload-image-module": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@wangeditor/upload-image-module/-/upload-image-module-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-z81lk/v71OwPDYeQDxj6cVr81aDP90aFuywb8nPD6eQeECtOymrqRODjpO6VGvCVxVck8nUxBHtbxKtjgcwyiA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@uppy/core": "^2.0.3",
|
||||||
|
"@uppy/xhr-upload": "^2.0.3",
|
||||||
|
"@wangeditor/basic-modules": "1.x",
|
||||||
|
"@wangeditor/core": "1.x",
|
||||||
|
"dom7": "^3.0.0",
|
||||||
|
"lodash.foreach": "^4.5.0",
|
||||||
|
"slate": "^0.72.0",
|
||||||
|
"snabbdom": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@wangeditor/video-module": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@wangeditor/video-module/-/video-module-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@uppy/core": "^2.1.4",
|
||||||
|
"@uppy/xhr-upload": "^2.0.7",
|
||||||
|
"@wangeditor/core": "1.x",
|
||||||
|
"dom7": "^3.0.0",
|
||||||
|
"nanoid": "^3.2.0",
|
||||||
|
"slate": "^0.72.0",
|
||||||
|
"snabbdom": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.18.0",
|
"version": "8.18.0",
|
||||||
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.18.0.tgz",
|
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.18.0.tgz",
|
||||||
@@ -1537,6 +1757,11 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/compute-scroll-into-view": {
|
||||||
|
"version": "1.0.20",
|
||||||
|
"resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
|
||||||
|
"integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg=="
|
||||||
|
},
|
||||||
"node_modules/confbox": {
|
"node_modules/confbox": {
|
||||||
"version": "0.1.8",
|
"version": "0.1.8",
|
||||||
"resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz",
|
"resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz",
|
||||||
@@ -1548,6 +1773,18 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/d": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/d/-/d-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
|
||||||
|
"dependencies": {
|
||||||
|
"es5-ext": "^0.10.64",
|
||||||
|
"type": "^2.7.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dayjs": {
|
"node_modules/dayjs": {
|
||||||
"version": "1.11.21",
|
"version": "1.11.21",
|
||||||
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.21.tgz",
|
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.21.tgz",
|
||||||
@@ -1600,6 +1837,14 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
"resolved": "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="
|
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/dom7": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/dom7/-/dom7-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==",
|
||||||
|
"dependencies": {
|
||||||
|
"ssr-window": "^3.0.0-alpha.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
@@ -1695,6 +1940,43 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es5-ext": {
|
||||||
|
"version": "0.10.64",
|
||||||
|
"resolved": "https://registry.npmmirror.com/es5-ext/-/es5-ext-0.10.64.tgz",
|
||||||
|
"integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"es6-iterator": "^2.0.3",
|
||||||
|
"es6-symbol": "^3.1.3",
|
||||||
|
"esniff": "^2.0.1",
|
||||||
|
"next-tick": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es6-iterator": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/es6-iterator/-/es6-iterator-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
|
||||||
|
"dependencies": {
|
||||||
|
"d": "1",
|
||||||
|
"es5-ext": "^0.10.35",
|
||||||
|
"es6-symbol": "^3.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es6-symbol": {
|
||||||
|
"version": "3.1.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/es6-symbol/-/es6-symbol-3.1.4.tgz",
|
||||||
|
"integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
|
||||||
|
"dependencies": {
|
||||||
|
"d": "^1.0.2",
|
||||||
|
"ext": "^1.7.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.21.5",
|
"version": "0.21.5",
|
||||||
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz",
|
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz",
|
||||||
@@ -1745,17 +2027,48 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/esniff": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/esniff/-/esniff-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
|
||||||
|
"dependencies": {
|
||||||
|
"d": "^1.0.1",
|
||||||
|
"es5-ext": "^0.10.62",
|
||||||
|
"event-emitter": "^0.3.5",
|
||||||
|
"type": "^2.7.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/estree-walker": {
|
"node_modules/estree-walker": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
|
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
|
||||||
},
|
},
|
||||||
|
"node_modules/event-emitter": {
|
||||||
|
"version": "0.3.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/event-emitter/-/event-emitter-0.3.5.tgz",
|
||||||
|
"integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
|
||||||
|
"dependencies": {
|
||||||
|
"d": "1",
|
||||||
|
"es5-ext": "~0.10.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/exsolve": {
|
"node_modules/exsolve": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.1.1.tgz",
|
"resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.1.1.tgz",
|
||||||
"integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==",
|
"integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/ext": {
|
||||||
|
"version": "1.7.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/ext/-/ext-1.7.0.tgz",
|
||||||
|
"integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
|
||||||
|
"dependencies": {
|
||||||
|
"type": "^2.7.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-glob": {
|
"node_modules/fast-glob": {
|
||||||
"version": "3.3.3",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz",
|
"resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||||
@@ -1963,6 +2276,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html-void-elements": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/https-proxy-agent": {
|
"node_modules/https-proxy-agent": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||||
@@ -1975,6 +2297,23 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/i18next": {
|
||||||
|
"version": "20.6.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/i18next/-/i18next-20.6.1.tgz",
|
||||||
|
"integrity": "sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/immer": {
|
||||||
|
"version": "9.0.21",
|
||||||
|
"resolved": "https://registry.npmmirror.com/immer/-/immer-9.0.21.tgz",
|
||||||
|
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/immutable": {
|
"node_modules/immutable": {
|
||||||
"version": "5.1.9",
|
"version": "5.1.9",
|
||||||
"resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.9.tgz",
|
"resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.9.tgz",
|
||||||
@@ -2037,6 +2376,11 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-hotkey": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/is-hotkey/-/is-hotkey-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw=="
|
||||||
|
},
|
||||||
"node_modules/is-number": {
|
"node_modules/is-number": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz",
|
||||||
@@ -2046,6 +2390,19 @@
|
|||||||
"node": ">=0.12.0"
|
"node": ">=0.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-plain-object": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-url": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/is-url/-/is-url-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww=="
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "9.0.1",
|
"version": "9.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz",
|
||||||
@@ -2099,6 +2456,42 @@
|
|||||||
"lodash-es": "*"
|
"lodash-es": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.camelcase": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="
|
||||||
|
},
|
||||||
|
"node_modules/lodash.clonedeep": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
|
||||||
|
},
|
||||||
|
"node_modules/lodash.debounce": {
|
||||||
|
"version": "4.0.8",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||||
|
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
|
||||||
|
},
|
||||||
|
"node_modules/lodash.foreach": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ=="
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isequal": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
|
||||||
|
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead."
|
||||||
|
},
|
||||||
|
"node_modules/lodash.throttle": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="
|
||||||
|
},
|
||||||
|
"node_modules/lodash.toarray": {
|
||||||
|
"version": "4.4.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
|
||||||
|
"integrity": "sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw=="
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
@@ -2162,6 +2555,14 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mime-match": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/mime-match/-/mime-match-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==",
|
||||||
|
"dependencies": {
|
||||||
|
"wildcard": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mime-types": {
|
"node_modules/mime-types": {
|
||||||
"version": "2.1.35",
|
"version": "2.1.35",
|
||||||
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
|
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
@@ -2205,6 +2606,11 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/namespace-emitter": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/namespace-emitter/-/namespace-emitter-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g=="
|
||||||
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.18",
|
"version": "3.3.18",
|
||||||
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.18.tgz",
|
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.18.tgz",
|
||||||
@@ -2222,6 +2628,11 @@
|
|||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/next-tick": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/next-tick/-/next-tick-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
|
||||||
|
},
|
||||||
"node_modules/node-addon-api": {
|
"node_modules/node-addon-api": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
"resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||||
@@ -2380,6 +2791,31 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/preact": {
|
||||||
|
"version": "10.29.8",
|
||||||
|
"resolved": "https://registry.npmmirror.com/preact/-/preact-10.29.8.tgz",
|
||||||
|
"integrity": "sha512-ej2aVZ+vZ8WO7tvlQWRM9N63A0KzF9q4mWJfDUHgYaIofWY9hu74QdnQrjoPMmZi2/nZ5gN0bJCQF49xQqx09Q==",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/preact"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"preact-render-to-string": ">=5"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"preact-render-to-string": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prismjs": {
|
||||||
|
"version": "1.30.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/prismjs/-/prismjs-1.30.0.tgz",
|
||||||
|
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||||
@@ -2585,6 +3021,14 @@
|
|||||||
"@parcel/watcher": "^2.4.1"
|
"@parcel/watcher": "^2.4.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/scroll-into-view-if-needed": {
|
||||||
|
"version": "2.2.31",
|
||||||
|
"resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",
|
||||||
|
"integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==",
|
||||||
|
"dependencies": {
|
||||||
|
"compute-scroll-into-view": "^1.0.20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/scule": {
|
"node_modules/scule": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz",
|
"resolved": "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz",
|
||||||
@@ -2596,6 +3040,35 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
|
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/slate": {
|
||||||
|
"version": "0.72.8",
|
||||||
|
"resolved": "https://registry.npmmirror.com/slate/-/slate-0.72.8.tgz",
|
||||||
|
"integrity": "sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==",
|
||||||
|
"dependencies": {
|
||||||
|
"immer": "^9.0.6",
|
||||||
|
"is-plain-object": "^5.0.0",
|
||||||
|
"tiny-warning": "^1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/slate-history": {
|
||||||
|
"version": "0.66.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/slate-history/-/slate-history-0.66.0.tgz",
|
||||||
|
"integrity": "sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==",
|
||||||
|
"dependencies": {
|
||||||
|
"is-plain-object": "^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"slate": ">=0.65.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/snabbdom": {
|
||||||
|
"version": "3.6.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/snabbdom/-/snabbdom-3.6.4.tgz",
|
||||||
|
"integrity": "sha512-VmxEfuw1/Y/eFj5VtMhYnukExpYiPkNzoo3+N3qwAOUDMl8wXgbli5ebR+j0knE3lZ/0eYskLxNcX64uy10N9w==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.17.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -2604,6 +3077,11 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ssr-window": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/ssr-window/-/ssr-window-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA=="
|
||||||
|
},
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
|
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
|
||||||
@@ -2652,6 +3130,11 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tiny-warning": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
|
||||||
|
},
|
||||||
"node_modules/to-regex-range": {
|
"node_modules/to-regex-range": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
@@ -2664,6 +3147,11 @@
|
|||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/type": {
|
||||||
|
"version": "2.7.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/type/-/type-2.7.3.tgz",
|
||||||
|
"integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="
|
||||||
|
},
|
||||||
"node_modules/ufo": {
|
"node_modules/ufo": {
|
||||||
"version": "1.6.4",
|
"version": "1.6.4",
|
||||||
"resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.4.tgz",
|
"resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.4.tgz",
|
||||||
@@ -3013,6 +3501,11 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz",
|
||||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="
|
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/wildcard": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/wildcard/-/wildcard-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng=="
|
||||||
|
},
|
||||||
"node_modules/wrap-ansi": {
|
"node_modules/wrap-ansi": {
|
||||||
"version": "6.2.0",
|
"version": "6.2.0",
|
||||||
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.0",
|
"@element-plus/icons-vue": "^2.3.0",
|
||||||
|
"@wangeditor/editor": "^5.1.23",
|
||||||
|
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"element-plus": "^2.4.0",
|
"element-plus": "^2.4.0",
|
||||||
"pinia": "^2.1.0",
|
"pinia": "^2.1.0",
|
||||||
|
|||||||
@@ -18,4 +18,14 @@ export function getCaptcha() {
|
|||||||
|
|
||||||
export function getRouters() {
|
export function getRouters() {
|
||||||
return request({ url: '/getRouters', method: 'get' })
|
return request({ url: '/getRouters', method: 'get' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 短信登录 - 发送验证码 (手机号必须已注册)
|
||||||
|
export function sendLoginSms(phone) {
|
||||||
|
return request({ url: '/business/auth/smsSendCode', method: 'post', data: { phone } })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 短信登录 - 校验验证码并登录, 返回 res.data 含 token
|
||||||
|
export function smsLogin(data) {
|
||||||
|
return request({ url: '/business/auth/smsLogin', method: 'post', data })
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
<!--
|
||||||
|
通用 OSS 文件上传控件 (支持 PDF / 图片 / 自定义 accept)
|
||||||
|
与 OssImageUploader 不同: 这里不强制要求图片类型, 显示上传的文件名/图标
|
||||||
|
用法:
|
||||||
|
<oss-file-uploader
|
||||||
|
v-model="form.fileUrl"
|
||||||
|
:dir="'ry8080/special-plan/'"
|
||||||
|
accept=".pdf,.png,.jpg,.jpeg"
|
||||||
|
placeholder="点击上传文件"
|
||||||
|
/>
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div class="ht-file-upload" :class="{ 'has-file': modelValue, 'is-block': block, readonly }" @click="handleClick">
|
||||||
|
<div v-if="!modelValue && !readonly" class="ht-file-placeholder">
|
||||||
|
<span class="placeholder-text">{{ placeholder }}</span>
|
||||||
|
<span class="placeholder-hint" v-if="hint">{{ hint }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="modelValue" class="ht-file-info">
|
||||||
|
<el-icon class="file-icon" :size="22"><svg viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-1 7V3.5L18.5 9H13z"/>
|
||||||
|
</svg></el-icon>
|
||||||
|
<div class="file-meta">
|
||||||
|
<a class="file-name" :href="modelValue" target="_blank" @click.stop>{{ fileName }}</a>
|
||||||
|
<span class="file-type">{{ ext.toUpperCase() }} · {{ fileSizeText }}</span>
|
||||||
|
</div>
|
||||||
|
<el-button v-if="!readonly" link type="danger" size="small" class="remove-btn" @click.stop="onRemove">移除</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref="fileInput"
|
||||||
|
:accept="accept"
|
||||||
|
style="display:none"
|
||||||
|
@change="onFileChange"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { uploadToOss } from '@/utils/oss'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: String, default: '' },
|
||||||
|
dir: { type: String, default: 'ry8080/file/' },
|
||||||
|
placeholder: { type: String, default: '点击上传文件' },
|
||||||
|
hint: { type: String, default: '' },
|
||||||
|
accept: { type: String, default: '.pdf,.png,.jpg,.jpeg' },
|
||||||
|
maxSize: { type: Number, default: 10 }, // MB
|
||||||
|
block: { type: Boolean, default: false },
|
||||||
|
readonly: { type: Boolean, default: false }
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const fileInput = ref(null)
|
||||||
|
const uploading = ref(false)
|
||||||
|
|
||||||
|
const fileName = computed(() => {
|
||||||
|
if (!props.modelValue) return ''
|
||||||
|
// 取 URL 最后一段, 去 query 参数
|
||||||
|
const url = props.modelValue.split('?')[0]
|
||||||
|
return url.substring(url.lastIndexOf('/') + 1)
|
||||||
|
})
|
||||||
|
const ext = computed(() => {
|
||||||
|
const n = fileName.value
|
||||||
|
const i = n.lastIndexOf('.')
|
||||||
|
return i >= 0 ? n.substring(i + 1) : ''
|
||||||
|
})
|
||||||
|
const fileSizeText = computed(() => '已上传')
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
if (props.readonly || uploading.value) return
|
||||||
|
fileInput.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onFileChange(e) {
|
||||||
|
const file = e.target.files?.[0]
|
||||||
|
e.target.value = ''
|
||||||
|
if (!file) return
|
||||||
|
// 类型校验 (按 accept 列表)
|
||||||
|
const allowed = props.accept.split(',').map(s => s.trim().toLowerCase()).filter(Boolean)
|
||||||
|
if (allowed.length) {
|
||||||
|
const lower = file.name.toLowerCase()
|
||||||
|
const ok = allowed.some(rule => {
|
||||||
|
if (rule.startsWith('.')) return lower.endsWith(rule)
|
||||||
|
if (rule.endsWith('/*')) return file.type.startsWith(rule.slice(0, -1))
|
||||||
|
return file.type === rule
|
||||||
|
})
|
||||||
|
if (!ok) return ElMessage.warning('文件类型不符, 允许: ' + props.accept)
|
||||||
|
}
|
||||||
|
if (file.size / 1024 / 1024 > props.maxSize) {
|
||||||
|
return ElMessage.warning(`文件大小不能超过 ${props.maxSize}MB`)
|
||||||
|
}
|
||||||
|
uploading.value = true
|
||||||
|
try {
|
||||||
|
const url = await uploadToOss(file, props.dir)
|
||||||
|
emit('update:modelValue', url)
|
||||||
|
ElMessage.success('上传成功')
|
||||||
|
} catch (err) {
|
||||||
|
ElMessage.error(err.message || '上传失败')
|
||||||
|
} finally {
|
||||||
|
uploading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRemove() {
|
||||||
|
emit('update:modelValue', '')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ht-file-upload {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
border: 1px dashed #d9d9d9;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fafafa;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 12px 14px;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
.ht-file-upload:hover { border-color: #1890ff; }
|
||||||
|
.ht-file-upload.has-file { border-style: solid; border-color: #52c41a; background: #fff; }
|
||||||
|
.ht-file-upload.is-block { width: 100%; }
|
||||||
|
.ht-file-upload.readonly { cursor: default; }
|
||||||
|
|
||||||
|
.ht-file-placeholder {
|
||||||
|
flex: 1;
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
align-items: center; justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.placeholder-text { font-size: 14px; color: #8c8c8c; }
|
||||||
|
.placeholder-hint { font-size: 12px; color: #c0c4cc; }
|
||||||
|
|
||||||
|
.ht-file-info {
|
||||||
|
display: flex; align-items: center; gap: 12px; flex: 1; min-width: 0;
|
||||||
|
}
|
||||||
|
.file-icon { color: #1890ff; flex-shrink: 0; }
|
||||||
|
.file-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
||||||
|
.file-name {
|
||||||
|
font-size: 14px; color: #1a1a1a; font-weight: 500;
|
||||||
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.file-name:hover { color: #1890ff; text-decoration: underline; }
|
||||||
|
.file-type { font-size: 12px; color: #909399; }
|
||||||
|
.remove-btn { flex-shrink: 0; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rich-editor-wrap">
|
||||||
|
<Toolbar
|
||||||
|
:editor="editorRef"
|
||||||
|
:default-config="toolbarConfig"
|
||||||
|
:mode="mode"
|
||||||
|
class="rich-editor-toolbar"
|
||||||
|
/>
|
||||||
|
<Editor
|
||||||
|
v-model="editorValue"
|
||||||
|
:default-config="editorConfig"
|
||||||
|
:mode="mode"
|
||||||
|
style="height: 400px; overflow-y: hidden;"
|
||||||
|
@on-created="onCreated"
|
||||||
|
@on-change="handleChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, shallowRef, computed, onBeforeUnmount } from 'vue'
|
||||||
|
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||||
|
import '@wangeditor/editor/dist/css/style.css'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: String, default: '' },
|
||||||
|
placeholder: { type: String, default: '请输入内容...' },
|
||||||
|
mode: { type: String, default: 'default' } // 'default' | 'simple'
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:modelValue', 'change'])
|
||||||
|
|
||||||
|
// prop 是只读的, 用 computed 中转
|
||||||
|
const editorValue = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val) => emit('update:modelValue', val)
|
||||||
|
})
|
||||||
|
|
||||||
|
const editorRef = shallowRef(null)
|
||||||
|
|
||||||
|
const toolbarConfig = {
|
||||||
|
excludeKeys: [
|
||||||
|
'group-video', // 视频上传, 避免服务器无视频上传接口
|
||||||
|
'insertVideo',
|
||||||
|
'uploadVideo'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
const editorConfig = {
|
||||||
|
placeholder: props.placeholder,
|
||||||
|
MENU_CONF: {
|
||||||
|
uploadImage: {
|
||||||
|
// 简化: 不接图片上传接口, 用户直接粘贴/拖拽本地图片由浏览器转 base64
|
||||||
|
customUpload: () => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCreated(editor) {
|
||||||
|
editorRef.value = editor
|
||||||
|
}
|
||||||
|
function handleChange(editor) {
|
||||||
|
emit('change', editor.getHtml())
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
const editor = editorRef.value
|
||||||
|
if (editor) editor.destroy()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.rich-editor-wrap {
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.rich-editor-toolbar {
|
||||||
|
border-bottom: 1px solid #dcdfe6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -6,10 +6,24 @@
|
|||||||
<div class="logo-sub">BAHIM 工作台</div>
|
<div class="logo-sub">BAHIM 工作台</div>
|
||||||
</div>
|
</div>
|
||||||
<el-menu :default-active="route.path" router class="aside-menu">
|
<el-menu :default-active="route.path" router class="aside-menu">
|
||||||
<el-menu-item v-for="m in menu" :key="m.path" :index="m.path">
|
<template v-for="m in menu" :key="m.path">
|
||||||
<el-icon><component :is="m.icon" /></el-icon>
|
<!-- 二级菜单: 含 children 的用 el-sub-menu -->
|
||||||
<span>{{ m.title }}</span>
|
<el-sub-menu v-if="m.children" :index="m.path">
|
||||||
</el-menu-item>
|
<template #title>
|
||||||
|
<el-icon><component :is="m.icon" /></el-icon>
|
||||||
|
<span>{{ m.title }}</span>
|
||||||
|
</template>
|
||||||
|
<el-menu-item v-for="c in m.children" :key="c.path" :index="c.path">
|
||||||
|
<el-icon><component :is="c.icon" /></el-icon>
|
||||||
|
<span>{{ c.title }}</span>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-sub-menu>
|
||||||
|
<!-- 一级菜单 -->
|
||||||
|
<el-menu-item v-else :index="m.path">
|
||||||
|
<el-icon><component :is="m.icon" /></el-icon>
|
||||||
|
<span>{{ m.title }}</span>
|
||||||
|
</el-menu-item>
|
||||||
|
</template>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</el-aside>
|
</el-aside>
|
||||||
<el-container>
|
<el-container>
|
||||||
@@ -41,7 +55,7 @@ import { computed } from 'vue'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useUserStore } from '@/store/user'
|
import { useUserStore } from '@/store/user'
|
||||||
import { logout as logoutApi } from '@/api/auth'
|
import { logout as logoutApi } from '@/api/auth'
|
||||||
import { House, Document, Calendar, User, List, OfficeBuilding, Setting, Bell, EditPen, DataAnalysis, Tickets, CaretBottom, Folder, Medal, UserFilled, Connection } from '@element-plus/icons-vue'
|
import { House, Document, Calendar, User, List, OfficeBuilding, Setting, Bell, EditPen, DataAnalysis, Tickets, CaretBottom, Folder, Medal, UserFilled, Connection, Box, Star, Grid, Files, Compass } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -58,11 +72,17 @@ const MENU = {
|
|||||||
{ path: '/admin/roles', title: '角色管理', icon: Setting },
|
{ path: '/admin/roles', title: '角色管理', icon: Setting },
|
||||||
{ path: '/admin/projects', title: '项目全览', icon: Document },
|
{ path: '/admin/projects', title: '项目全览', icon: Document },
|
||||||
{ path: '/admin/meetings', title: '会议全览', icon: Calendar },
|
{ path: '/admin/meetings', title: '会议全览', icon: Calendar },
|
||||||
{ path: '/admin/department', title: '科室管理', icon: Folder },
|
{ path: '/admin/library', title: '资料库管理', icon: Box, children: [
|
||||||
{ path: '/admin/title', title: '职称管理', icon: Medal },
|
{ path: '/admin/experts', title: '专家管理', icon: UserFilled },
|
||||||
{ path: '/admin/experts', title: '专家管理', icon: UserFilled },
|
{ path: '/admin/sponsor-orgs', title: '支持单位管理', icon: Star },
|
||||||
{ path: '/admin/sponsor-orgs', title: '支持单位管理', icon: Connection },
|
{ path: '/admin/executor-orgs', title: '服务机构管理', icon: OfficeBuilding },
|
||||||
{ path: '/admin/executor-orgs', title: '服务机构管理', icon: OfficeBuilding },
|
{ path: '/admin/department', title: '科室管理', icon: Grid },
|
||||||
|
{ path: '/admin/title', title: '职称管理', icon: Medal }
|
||||||
|
]},
|
||||||
|
{ path: '/admin/manage', title: '网站管理', icon: Setting, children: [
|
||||||
|
{ path: '/admin/article', title: '协议管理', icon: Files },
|
||||||
|
{ path: '/admin/special-plan', title: '七大专项计划', icon: Compass }
|
||||||
|
]},
|
||||||
{ path: '/admin/account', title: '账号信息', icon: User }
|
{ path: '/admin/account', title: '账号信息', icon: User }
|
||||||
],
|
],
|
||||||
leader: [
|
leader: [
|
||||||
@@ -124,8 +144,38 @@ const onCommand = (cmd) => { if (cmd === 'logout') logout() }
|
|||||||
.logo-text { color: #fff; font-size: 16px; font-weight: 600; }
|
.logo-text { color: #fff; font-size: 16px; font-weight: 600; }
|
||||||
.logo-sub { color: #8a99b3; font-size: 12px; }
|
.logo-sub { color: #8a99b3; font-size: 12px; }
|
||||||
.aside-menu { background: #001529; border: none; }
|
.aside-menu { background: #001529; border: none; }
|
||||||
:deep(.el-menu-item) { color: #c0c4cc; }
|
|
||||||
:deep(.el-menu-item.is-active) { background: #1890ff; color: #fff; }
|
/* 一级菜单 + 二级父标题: 统一默认/hover/active 样式 */
|
||||||
|
:deep(.el-menu-item),
|
||||||
|
:deep(.el-sub-menu__title) {
|
||||||
|
color: #c0c4cc;
|
||||||
|
}
|
||||||
|
:deep(.el-menu-item:hover),
|
||||||
|
:deep(.el-sub-menu__title:hover) {
|
||||||
|
background-color: #1f3a5f;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
/* 子项激活: 蓝底白字 */
|
||||||
|
:deep(.el-menu-item.is-active) {
|
||||||
|
background: #1890ff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
/* 父标题在子项激活时高亮 (Element Plus 自动给 el-sub-menu 加 is-active) */
|
||||||
|
:deep(.el-sub-menu.is-active > .el-sub-menu__title) {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 二级菜单容器: 跟随侧边栏深蓝底, 不要 Element Plus 默认白底 */
|
||||||
|
:deep(.el-menu--inline),
|
||||||
|
:deep(.el-menu--inline .el-menu-item) {
|
||||||
|
background-color: #001529;
|
||||||
|
}
|
||||||
|
:deep(.el-menu--inline .el-menu-item:hover) {
|
||||||
|
background-color: #1f3a5f;
|
||||||
|
}
|
||||||
|
:deep(.el-menu--inline .el-menu-item.is-active) {
|
||||||
|
background: #1890ff;
|
||||||
|
}
|
||||||
.topbar { background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; border-bottom: 1px solid #ebeef5; }
|
.topbar { background: #fff; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; border-bottom: 1px solid #ebeef5; }
|
||||||
.topbar-left { display: flex; align-items: center; }
|
.topbar-left { display: flex; align-items: center; }
|
||||||
.page-title { font-size: 16px; font-weight: 600; }
|
.page-title { font-size: 16px; font-weight: 600; }
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ const routes = [
|
|||||||
{ path: 'register-expert', name: 'register-expert', component: () => import('@/views/auth/RegisterExpert.vue'), meta: { title: '专家注册' } },
|
{ path: 'register-expert', name: 'register-expert', component: () => import('@/views/auth/RegisterExpert.vue'), meta: { title: '专家注册' } },
|
||||||
{ path: 'register-executor', name: 'register-executor', component: () => import('@/views/auth/RegisterExecutor.vue'), meta: { title: '执行单位(供应商)注册' } },
|
{ path: 'register-executor', name: 'register-executor', component: () => import('@/views/auth/RegisterExecutor.vue'), meta: { title: '执行单位(供应商)注册' } },
|
||||||
{ path: 'register-sponsor', name: 'register-sponsor', component: () => import('@/views/auth/RegisterSponsor.vue'), meta: { title: '赞助方注册' } },
|
{ path: 'register-sponsor', name: 'register-sponsor', component: () => import('@/views/auth/RegisterSponsor.vue'), meta: { title: '赞助方注册' } },
|
||||||
{ path: 'login', name: 'login', component: () => import('@/views/auth/Login.vue'), meta: { title: '登录' } }
|
{ path: 'login', name: 'login', component: () => import('@/views/auth/Login.vue'), meta: { title: '登录' } },
|
||||||
|
{ path: 'article/:type', name: 'portal-article', component: () => import('@/views/portal/ArticleView.vue'), meta: { title: '协议' } },
|
||||||
|
{ path: 'special-plan/:id', name: 'portal-special-plan', component: () => import('@/views/portal/SpecialPlanDetail.vue'), meta: { title: '专项计划详情' } }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
// 各角色后台(登录后)
|
// 各角色后台(登录后)
|
||||||
@@ -40,6 +42,10 @@ const routes = [
|
|||||||
{ path: 'executor-people/new', name: 'admin-executor-people-new', component: () => import('@/views/admin/ExecutorPersonNew.vue'), meta: { title: '新建人员' } },
|
{ path: 'executor-people/new', name: 'admin-executor-people-new', component: () => import('@/views/admin/ExecutorPersonNew.vue'), meta: { title: '新建人员' } },
|
||||||
{ path: 'executor-people/edit/:id', name: 'admin-executor-people-edit', component: () => import('@/views/admin/ExecutorPersonNew.vue'), meta: { title: '编辑人员' } },
|
{ path: 'executor-people/edit/:id', name: 'admin-executor-people-edit', component: () => import('@/views/admin/ExecutorPersonNew.vue'), meta: { title: '编辑人员' } },
|
||||||
{ path: 'orgs', name: 'admin-orgs', component: () => import('@/views/admin/Orgs.vue'), meta: { title: '公司管理' } },
|
{ path: 'orgs', name: 'admin-orgs', component: () => import('@/views/admin/Orgs.vue'), meta: { title: '公司管理' } },
|
||||||
|
{ path: 'article', name: 'admin-article', component: () => import('@/views/admin/BizArticleAdmin.vue'), meta: { title: '协议管理' } },
|
||||||
|
{ path: 'article/edit/:id', name: 'admin-article-edit', component: () => import('@/views/admin/BizArticleEdit.vue'), meta: { title: '编辑文章' } },
|
||||||
|
{ path: 'special-plan', name: 'admin-special-plan', component: () => import('@/views/admin/BizSpecialPlanAdmin.vue'), meta: { title: '七大专项计划' } },
|
||||||
|
{ path: 'special-plan/edit/:id', name: 'admin-special-plan-edit', component: () => import('@/views/admin/BizSpecialPlanEdit.vue'), meta: { title: '编辑专项计划' } },
|
||||||
{ path: 'account', name: 'admin-account', component: () => import('@/views/admin/Account.vue'), meta: { title: '账号信息' } }
|
{ path: 'account', name: 'admin-account', component: () => import('@/views/admin/Account.vue'), meta: { title: '账号信息' } }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步操作防重入锁 (UI 层面的"防抖")
|
||||||
|
*
|
||||||
|
* 适用场景: 接口响应慢, 用户在等待期间可重复点击同一按钮, 导致并发请求.
|
||||||
|
* - 第一次调用 run(): locked 置 true, 执行 fn, 完成后 (无论成败) 解锁
|
||||||
|
* - 锁定期间的调用: 立即 return, 不发起任何请求
|
||||||
|
*
|
||||||
|
* 用法:
|
||||||
|
* const { locked, run } = useAsyncLock()
|
||||||
|
*
|
||||||
|
* async function sendSms() {
|
||||||
|
* if (!valid) return
|
||||||
|
* await run(async () => {
|
||||||
|
* const r = await request(...)
|
||||||
|
* // ... 业务处理 ...
|
||||||
|
* })
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* 模板绑定:
|
||||||
|
* <el-button :loading="locked" :disabled="locked || ..." @click="sendSms">发送验证码</el-button>
|
||||||
|
*
|
||||||
|
* 注意: 这是"按钮重入锁", 不是传统 lodash.debounce (后者会延迟执行). 这里
|
||||||
|
* 第一次点击立即发出, 但锁定期间所有点击被丢弃 — 这才是"防重复请求"的正确语义.
|
||||||
|
*/
|
||||||
|
export function useAsyncLock() {
|
||||||
|
const locked = ref(false)
|
||||||
|
|
||||||
|
async function run(fn) {
|
||||||
|
if (locked.value) return undefined
|
||||||
|
locked.value = true
|
||||||
|
try {
|
||||||
|
return await fn()
|
||||||
|
} finally {
|
||||||
|
locked.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { locked, run }
|
||||||
|
}
|
||||||
@@ -2,15 +2,6 @@
|
|||||||
<div class="page-card admin-account">
|
<div class="page-card admin-account">
|
||||||
<div class="breadcrumb">首页 / 账号信息</div>
|
<div class="breadcrumb">首页 / 账号信息</div>
|
||||||
|
|
||||||
<el-descriptions :column="2" border style="margin-bottom: 24px">
|
|
||||||
<el-descriptions-item label="账号">{{ profile.userName }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="姓名">{{ profile.nickName || profile.userName }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="角色">{{ roleLabel }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="手机号">{{ profile.phonenumber || '-' }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="邮箱">{{ profile.email || '-' }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="最近登录">{{ profile.loginDate || '-' }}</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
|
|
||||||
<el-tabs v-model="tab">
|
<el-tabs v-model="tab">
|
||||||
<el-tab-pane label="修改资料" name="profile">
|
<el-tab-pane label="修改资料" name="profile">
|
||||||
<el-form :model="form" label-width="100px" class="account-form">
|
<el-form :model="form" label-width="100px" class="account-form">
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-card admin-article">
|
||||||
|
<div class="breadcrumb">首页 / 协议管理</div>
|
||||||
|
|
||||||
|
<!-- 筛选 -->
|
||||||
|
<el-form inline :model="q" class="filter-form">
|
||||||
|
<el-form-item label="类型">
|
||||||
|
<el-select v-model="q.type" placeholder="全部" clearable style="width:160px">
|
||||||
|
<el-option label="用户协议" value="agreement" />
|
||||||
|
<el-option label="隐私政策" value="privacy" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="标题">
|
||||||
|
<el-input v-model="q.title" placeholder="输入标题" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-select v-model="q.status" placeholder="全部" clearable style="width:120px">
|
||||||
|
<el-option label="启用" value="0" />
|
||||||
|
<el-option label="停用" value="1" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="load">查询</el-button>
|
||||||
|
<el-button @click="reset">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<!-- 列表 -->
|
||||||
|
<el-table :data="rows" border stripe v-loading="loading">
|
||||||
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
<el-table-column prop="title" label="标题" min-width="200" />
|
||||||
|
<el-table-column label="类型" width="120">
|
||||||
|
<template #default="{ row }">{{ TYPE_LABEL[row.type] || row.type }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.status === '0' ? 'success' : 'danger'">{{ row.status === '0' ? '启用' : '停用' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="updateTime" label="更新时间" width="170" />
|
||||||
|
<el-table-column label="操作" width="160" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button size="small" link type="primary" @click="$router.push('/admin/article/edit/' + row.id)">编辑</el-button>
|
||||||
|
<el-button size="small" link type="primary" :disabled="row.status !== '0'" @click="preview(row)" target="_blank">预览</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="pager">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="q.pageNum"
|
||||||
|
v-model:page-size="q.pageSize"
|
||||||
|
:total="total"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="load"
|
||||||
|
@size-change="load"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
const TYPE_LABEL = { agreement: '用户协议', privacy: '隐私政策' }
|
||||||
|
|
||||||
|
const q = reactive({ type: '', title: '', status: '', pageNum: 1, pageSize: 20 })
|
||||||
|
const rows = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const r = await request({ url: '/business/article/list', method: 'get', params: { ...q } })
|
||||||
|
rows.value = (r.data && r.data.rows) || r.rows || []
|
||||||
|
total.value = (r.data && r.data.total) || r.total || 0
|
||||||
|
} finally { loading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
q.type = ''; q.title = ''; q.status = ''
|
||||||
|
q.pageNum = 1
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
|
function preview(row) {
|
||||||
|
// 启用的文章才允许预览(实际注册页只看启用的), 这里做软拦截
|
||||||
|
window.open(`/#/article/${row.type}`, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-article { padding: 16px; }
|
||||||
|
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
||||||
|
.filter-form { margin-bottom: 12px; }
|
||||||
|
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-card biz-article-edit">
|
||||||
|
<div class="header">
|
||||||
|
<el-button :icon="ArrowLeft" @click="$router.back()">返回</el-button>
|
||||||
|
<span class="title">{{ isEdit ? '编辑文章' : '新建文章' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form :model="form" label-width="100px" v-loading="loading">
|
||||||
|
<el-form-item label="标题" required>
|
||||||
|
<el-input v-model="form.title" placeholder="请输入标题" maxlength="200" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="类型" required>
|
||||||
|
<el-select v-model="form.type" style="width:200px" :disabled="isEdit">
|
||||||
|
<el-option label="用户协议" value="agreement" />
|
||||||
|
<el-option label="隐私政策" value="privacy" />
|
||||||
|
</el-select>
|
||||||
|
<span class="form-tip" v-if="isEdit">类型不可修改</span>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="状态" required>
|
||||||
|
<el-radio-group v-model="form.status">
|
||||||
|
<el-radio value="0">启用</el-radio>
|
||||||
|
<el-radio value="1">停用</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="正文" required>
|
||||||
|
<RichEditor v-model="form.content" placeholder="请输入正文, 支持富文本格式" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="可选, 内部备注" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" :loading="saving" @click="save">保存</el-button>
|
||||||
|
<el-button @click="$router.back()">取消</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
import RichEditor from '@/components/RichEditor.vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const isEdit = computed(() => !!route.params.id)
|
||||||
|
const loading = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
id: null,
|
||||||
|
title: '',
|
||||||
|
type: 'agreement',
|
||||||
|
content: '',
|
||||||
|
status: '0',
|
||||||
|
remark: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadDetail() {
|
||||||
|
if (!isEdit.value) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const r = await request({ url: `/business/article/${route.params.id}`, method: 'get' })
|
||||||
|
const data = (r.data && r.data.data) || r.data || {}
|
||||||
|
Object.assign(form, data)
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('加载失败')
|
||||||
|
} finally { loading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
if (!form.title.trim()) return ElMessage.warning('请输入标题')
|
||||||
|
if (!form.content.trim()) return ElMessage.warning('请输入正文')
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
if (isEdit.value) {
|
||||||
|
await request({ url: '/business/article', method: 'put', data: { ...form } })
|
||||||
|
ElMessage.success('保存成功')
|
||||||
|
} else {
|
||||||
|
await request({ url: '/business/article', method: 'post', data: { ...form } })
|
||||||
|
ElMessage.success('新建成功')
|
||||||
|
}
|
||||||
|
router.push('/admin/article')
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e?.msg || '保存失败')
|
||||||
|
} finally { saving.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadDetail)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.biz-article-edit { padding: 16px; }
|
||||||
|
.header { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; }
|
||||||
|
.header .title { font-size: 18px; font-weight: 600; }
|
||||||
|
.form-tip { margin-left: 12px; font-size: 12px; color: #909399; }
|
||||||
|
:deep(.w-e-text-container) { background: #fff; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-card admin-plan">
|
||||||
|
<div class="breadcrumb">首页 / 网站管理 / 七大专项计划</div>
|
||||||
|
|
||||||
|
<!-- 筛选 -->
|
||||||
|
<el-form inline :model="q" class="filter-form">
|
||||||
|
<el-form-item label="标题">
|
||||||
|
<el-input v-model="q.title" placeholder="输入标题" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="类型">
|
||||||
|
<el-select v-model="q.contentType" placeholder="全部" clearable style="width:140px">
|
||||||
|
<el-option label="富文本" value="rich" />
|
||||||
|
<el-option label="上传文件" value="file" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-select v-model="q.status" placeholder="全部" clearable style="width:120px">
|
||||||
|
<el-option label="启用" value="0" />
|
||||||
|
<el-option label="停用" value="1" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="load">查询</el-button>
|
||||||
|
<el-button @click="reset">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<!-- 列表 -->
|
||||||
|
<el-table :data="rows" border stripe v-loading="loading">
|
||||||
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
<el-table-column prop="title" label="标题" min-width="220" />
|
||||||
|
<el-table-column label="类型" width="110">
|
||||||
|
<template #default="{ row }">{{ TYPE_LABEL[row.contentType] || row.contentType }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="sortOrder" label="排序" width="80" sortable />
|
||||||
|
<el-table-column label="状态" width="90">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.status === '0' ? 'success' : 'danger'">{{ row.status === '0' ? '启用' : '停用' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="updateTime" label="更新时间" width="170" />
|
||||||
|
<el-table-column label="操作" width="140" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button size="small" link type="primary" @click="$router.push('/admin/special-plan/edit/' + row.id)">编辑</el-button>
|
||||||
|
<el-button size="small" link type="primary" :disabled="row.status !== '0'" @click="preview(row)">预览</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="pager">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="q.pageNum"
|
||||||
|
v-model:page-size="q.pageSize"
|
||||||
|
:total="total"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="load"
|
||||||
|
@size-change="load"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { reactive, ref, onMounted } from 'vue'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
const TYPE_LABEL = { rich: '富文本', file: '上传文件' }
|
||||||
|
|
||||||
|
const q = reactive({ title: '', contentType: '', status: '', pageNum: 1, pageSize: 20 })
|
||||||
|
const rows = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const r = await request({ url: '/business/specialPlan/list', method: 'get', params: { ...q } })
|
||||||
|
rows.value = (r.data && r.data.rows) || r.rows || []
|
||||||
|
total.value = (r.data && r.data.total) || r.total || 0
|
||||||
|
} finally { loading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
q.title = ''; q.contentType = ''; q.status = ''
|
||||||
|
q.pageNum = 1
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
|
function preview(row) {
|
||||||
|
window.open(`/#/special-plan/${row.id}`, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-plan { padding: 16px; }
|
||||||
|
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
||||||
|
.filter-form { margin-bottom: 12px; }
|
||||||
|
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-card biz-plan-edit">
|
||||||
|
<div class="header">
|
||||||
|
<el-button :icon="ArrowLeft" @click="$router.back()">返回</el-button>
|
||||||
|
<span class="title">{{ isEdit ? '编辑专项计划' : '新建专项计划' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form :model="form" label-width="100px" v-loading="loading">
|
||||||
|
<el-form-item label="标题" required>
|
||||||
|
<el-input v-model="form.title" placeholder="请输入专项计划名称" maxlength="200" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="内容类型" required>
|
||||||
|
<el-radio-group v-model="form.contentType" @change="onTypeChange">
|
||||||
|
<el-radio value="rich">富文本</el-radio>
|
||||||
|
<el-radio value="file">上传文件 (PDF / PNG)</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="正文" :required="form.contentType === 'rich'" v-if="form.contentType === 'rich'">
|
||||||
|
<RichEditor v-model="form.content" placeholder="请输入专项计划正文" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="文件" :required="form.contentType === 'file'" v-else>
|
||||||
|
<OssFileUploader v-model="form.fileUrl" dir="ry8080/special-plan/" accept=".pdf,.png" placeholder="点击上传 PDF / PNG" hint="仅支持 PDF 或 PNG, ≤10MB" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="排序">
|
||||||
|
<el-input-number v-model="form.sortOrder" :min="0" :step="1" />
|
||||||
|
<span class="form-tip">数字越小越靠前</span>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="状态" required>
|
||||||
|
<el-radio-group v-model="form.status">
|
||||||
|
<el-radio value="0">启用</el-radio>
|
||||||
|
<el-radio value="1">停用</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="可选, 内部备注" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" :loading="saving" @click="save">保存</el-button>
|
||||||
|
<el-button @click="$router.back()">取消</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
import RichEditor from '@/components/RichEditor.vue'
|
||||||
|
import OssFileUploader from '@/components/OssFileUploader.vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const isEdit = computed(() => !!route.params.id)
|
||||||
|
const loading = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
id: null,
|
||||||
|
title: '',
|
||||||
|
contentType: 'rich',
|
||||||
|
content: '',
|
||||||
|
fileUrl: '',
|
||||||
|
sortOrder: 0,
|
||||||
|
status: '0',
|
||||||
|
remark: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadDetail() {
|
||||||
|
if (!isEdit.value) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const r = await request({ url: `/business/specialPlan/${route.params.id}`, method: 'get' })
|
||||||
|
const data = (r.data && r.data.data) || r.data || {}
|
||||||
|
Object.assign(form, data)
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('加载失败')
|
||||||
|
} finally { loading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTypeChange(val) {
|
||||||
|
// 切换内容类型时清空另一字段, 避免脏数据
|
||||||
|
if (val === 'rich') form.fileUrl = ''
|
||||||
|
else form.content = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
if (!form.title.trim()) return ElMessage.warning('请输入标题')
|
||||||
|
if (form.contentType === 'rich' && !form.content.trim()) return ElMessage.warning('请输入正文')
|
||||||
|
if (form.contentType === 'file' && !form.fileUrl) return ElMessage.warning('请上传文件')
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
if (isEdit.value) {
|
||||||
|
await request({ url: '/business/specialPlan', method: 'put', data: { ...form } })
|
||||||
|
ElMessage.success('保存成功')
|
||||||
|
} else {
|
||||||
|
await request({ url: '/business/specialPlan', method: 'post', data: { ...form } })
|
||||||
|
ElMessage.success('新建成功')
|
||||||
|
}
|
||||||
|
router.push('/admin/special-plan')
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e?.msg || '保存失败')
|
||||||
|
} finally { saving.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadDetail)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.biz-plan-edit { padding: 16px; }
|
||||||
|
.header { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; }
|
||||||
|
.header .title { font-size: 18px; font-weight: 600; }
|
||||||
|
.form-tip { margin-left: 12px; font-size: 12px; color: #909399; }
|
||||||
|
</style>
|
||||||
@@ -30,10 +30,15 @@
|
|||||||
<!-- 右侧登录卡片 -->
|
<!-- 右侧登录卡片 -->
|
||||||
<section class="login-section">
|
<section class="login-section">
|
||||||
<div class="login-card">
|
<div class="login-card">
|
||||||
<h1 class="login-title">账号登录</h1>
|
<h1 class="login-title">{{ loginMode === 'password' ? '账号登录' : '手机号登录' }}</h1>
|
||||||
<p class="login-subtitle">请输入您的账号信息</p>
|
<p class="login-subtitle">{{ loginMode === 'password' ? '请输入您的账号信息' : '请输入手机号接收验证码' }}</p>
|
||||||
|
|
||||||
<form id="loginForm" @submit.prevent="onSubmit">
|
<div class="login-tabs">
|
||||||
|
<span class="tab" :class="{ active: loginMode === 'password' }" @click="switchMode('password')">账号密码登录</span>
|
||||||
|
<span class="tab" :class="{ active: loginMode === 'sms' }" @click="switchMode('sms')">手机验证码登录</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form v-if="loginMode === 'password'" id="loginForm" @submit.prevent="onSubmit">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
||||||
@@ -65,6 +70,29 @@
|
|||||||
<a class="bottom-link" @click="onForgot">忘记密码</a>
|
<a class="bottom-link" @click="onForgot">忘记密码</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<form v-else id="smsLoginForm" @submit.prevent="onSmsLoginSubmit">
|
||||||
|
<div class="form-group">
|
||||||
|
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
|
||||||
|
</svg>
|
||||||
|
<input v-model="smsForm.phone" type="text" class="form-input" placeholder="请输入手机号" maxlength="11" autocomplete="tel">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group sms-group">
|
||||||
|
<input v-model="smsForm.smsCode" type="text" class="form-input sms-input" placeholder="短信验证码" maxlength="6">
|
||||||
|
<button type="button" class="sms-btn" :disabled="smsSmsLocked || smsSmsCountdown > 0 || !smsForm.phone" @click="sendLoginSmsCode">
|
||||||
|
{{ smsSmsLocked ? '发送中...' : smsSmsCountdown > 0 ? `${smsSmsCountdown}s 后重试` : '获取验证码' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="login-btn" :disabled="smsLoginLocked">{{ smsLoginLocked ? '登录中...' : '登 录' }}</button>
|
||||||
|
|
||||||
|
<div class="form-bottom-links">
|
||||||
|
<a class="bottom-link" @click="onRegister">新用户注册</a>
|
||||||
|
<a class="bottom-link" @click="onForgot">忘记密码</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
@@ -94,15 +122,68 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 忘记密码弹窗 -->
|
||||||
|
<div class="modal-overlay" :class="{ active: showForgotModal }" @click.self="closeForgot">
|
||||||
|
<div class="modal">
|
||||||
|
<h2 class="modal-title">忘记密码</h2>
|
||||||
|
<p class="modal-subtitle">通过注册时填写的手机号验证后重置密码</p>
|
||||||
|
|
||||||
|
<form @submit.prevent="onResetSubmit">
|
||||||
|
<div class="form-group">
|
||||||
|
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
|
||||||
|
</svg>
|
||||||
|
<input v-model="forgotForm.phone" type="text" class="form-input" placeholder="请输入注册时的手机号" maxlength="11" autocomplete="tel">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group sms-group">
|
||||||
|
<input v-model="forgotForm.smsCode" type="text" class="form-input sms-input" placeholder="短信验证码" maxlength="6">
|
||||||
|
<button type="button" class="sms-btn" :disabled="forgotSmsLocked || forgotSmsCountdown > 0 || !forgotForm.phone" @click="sendForgotSms">
|
||||||
|
{{ forgotSmsLocked ? '发送中...' : forgotSmsCountdown > 0 ? `${forgotSmsCountdown}s 后重试` : '获取验证码' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
||||||
|
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
||||||
|
</svg>
|
||||||
|
<input v-model="forgotForm.password" type="password" class="form-input" placeholder="新密码 (6-20 位)" autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
||||||
|
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
||||||
|
</svg>
|
||||||
|
<input v-model="forgotForm.confirmPassword" type="password" class="form-input" placeholder="确认新密码" autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="modal-btn secondary" type="button" @click="closeForgot">取消</button>
|
||||||
|
<button class="modal-btn primary" type="submit" :disabled="forgotLoading">{{ forgotLoading ? '提交中...' : '重置密码' }}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, ref, onMounted } from 'vue'
|
import { reactive, ref, onMounted, onUnmounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { login, getInfo, getCaptcha } from '@/api/auth'
|
import request from '@/utils/request'
|
||||||
|
import { login, getInfo, getCaptcha, sendLoginSms, smsLogin } from '@/api/auth'
|
||||||
import { useUserStore } from '@/store/user'
|
import { useUserStore } from '@/store/user'
|
||||||
|
import { useAsyncLock } from '@/utils/useAsyncLock'
|
||||||
|
|
||||||
|
// 图形验证码刷新 + 忘记密码短信 + 短信登录 各自独立锁 (避免重复请求)
|
||||||
|
const { locked: captchaLocked, run: runCaptchaOnce } = useAsyncLock()
|
||||||
|
const { locked: forgotSmsLocked, run: runForgotSmsOnce } = useAsyncLock()
|
||||||
|
const { locked: smsSmsLocked, run: runSmsOnce } = useAsyncLock()
|
||||||
|
const { locked: smsLoginLocked, run: runSmsLoginOnce } = useAsyncLock()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
@@ -114,6 +195,12 @@ const loading = ref(false)
|
|||||||
const showRoleModal = ref(false)
|
const showRoleModal = ref(false)
|
||||||
const registerUserType = ref('')
|
const registerUserType = ref('')
|
||||||
|
|
||||||
|
// 登录模式: 'password' | 'sms'
|
||||||
|
const loginMode = ref('password')
|
||||||
|
const smsForm = reactive({ phone: '', smsCode: '', uuid: '' })
|
||||||
|
const smsSmsCountdown = ref(0)
|
||||||
|
let smsSmsTimer = null
|
||||||
|
|
||||||
const registerTypes = [
|
const registerTypes = [
|
||||||
{ value: 'expert', name: '项目参与专家', desc: '负责项目方案设计与评审' },
|
{ value: 'expert', name: '项目参与专家', desc: '负责项目方案设计与评审' },
|
||||||
{ value: 'executor', name: '项目执行单位(供应商)', desc: '承担项目执行,需走入库流程' },
|
{ value: 'executor', name: '项目执行单位(供应商)', desc: '承担项目执行,需走入库流程' },
|
||||||
@@ -130,16 +217,18 @@ const userTypes = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
async function loadCaptcha() {
|
async function loadCaptcha() {
|
||||||
try {
|
await runCaptchaOnce(async () => {
|
||||||
const res = await getCaptcha()
|
try {
|
||||||
captchaEnabled.value = res?.captchaEnabled !== false
|
const res = await getCaptcha()
|
||||||
if (res?.uuid) {
|
captchaEnabled.value = res?.captchaEnabled !== false
|
||||||
form.uuid = res.uuid
|
if (res?.uuid) {
|
||||||
captchaImg.value = 'data:image/png;base64,' + (res.img || '')
|
form.uuid = res.uuid
|
||||||
|
captchaImg.value = 'data:image/png;base64,' + (res.img || '')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
captchaImg.value = ''
|
||||||
}
|
}
|
||||||
} catch (e) {
|
})
|
||||||
captchaImg.value = ''
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSubmit() {
|
async function onSubmit() {
|
||||||
@@ -154,36 +243,7 @@ async function onSubmit() {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await login({ username: form.username, password: form.password, code: form.code || '', uuid: form.uuid || '' })
|
const res = await login({ username: form.username, password: form.password, code: form.code || '', uuid: form.uuid || '' })
|
||||||
// username 推断仅作兜底, 真实 role 以 /getInfo 返回的 sys_user.role_type 为准
|
await afterLogin(res.token, form.username, autoRole(form.username))
|
||||||
const fallbackRole = autoRole(form.username)
|
|
||||||
userStore.setToken(res.token)
|
|
||||||
// /login 接口只返回 token; 调 /getInfo 拿完整 user (含 accountType/parentUserId/roleType, 让 isMain/isSub/role 都从后端真实值取)
|
|
||||||
let role = fallbackRole
|
|
||||||
try {
|
|
||||||
const info = await getInfo()
|
|
||||||
const u = info.user || {}
|
|
||||||
role = u.roleType || fallbackRole
|
|
||||||
userStore.setUser({
|
|
||||||
userId: u.userId,
|
|
||||||
userName: u.userName || form.username,
|
|
||||||
nickName: u.nickName || form.username,
|
|
||||||
accountType: u.accountType || 'MAIN',
|
|
||||||
parentUserId: u.parentUserId || null,
|
|
||||||
role
|
|
||||||
})
|
|
||||||
} catch {
|
|
||||||
// /getInfo 失败时回退: 只存基本信息
|
|
||||||
userStore.setUser({
|
|
||||||
userId: res.userId,
|
|
||||||
userName: form.username,
|
|
||||||
nickName: form.username,
|
|
||||||
accountType: 'MAIN',
|
|
||||||
parentUserId: null,
|
|
||||||
role: fallbackRole
|
|
||||||
})
|
|
||||||
}
|
|
||||||
ElMessage.success(`欢迎,${form.username}(${userTypes.find(u => u.value === role)?.name || role})`)
|
|
||||||
router.replace(roleHome[role] || '/leader/home')
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 错误提示已由 utils/request.js 拦截器统一弹 (ElMessage.error), 这里只刷新验证码
|
// 错误提示已由 utils/request.js 拦截器统一弹 (ElMessage.error), 这里只刷新验证码
|
||||||
loadCaptcha()
|
loadCaptcha()
|
||||||
@@ -192,6 +252,97 @@ async function onSubmit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录后置: 存 token → 调 /getInfo 拿真实 user → 跳角色首页
|
||||||
|
* 密码登录和短信登录共用
|
||||||
|
*/
|
||||||
|
async function afterLogin(token, displayName, fallbackRole) {
|
||||||
|
userStore.setToken(token)
|
||||||
|
let role = fallbackRole
|
||||||
|
try {
|
||||||
|
const info = await getInfo()
|
||||||
|
const u = info.user || {}
|
||||||
|
role = u.roleType || fallbackRole
|
||||||
|
userStore.setUser({
|
||||||
|
userId: u.userId,
|
||||||
|
userName: u.userName || displayName,
|
||||||
|
nickName: u.nickName || displayName,
|
||||||
|
accountType: u.accountType || 'MAIN',
|
||||||
|
parentUserId: u.parentUserId || null,
|
||||||
|
role
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
// /getInfo 失败时回退: 只存基本信息
|
||||||
|
userStore.setUser({
|
||||||
|
userId: null,
|
||||||
|
userName: displayName,
|
||||||
|
nickName: displayName,
|
||||||
|
accountType: 'MAIN',
|
||||||
|
parentUserId: null,
|
||||||
|
role: fallbackRole
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ElMessage.success(`欢迎,${displayName}(${userTypes.find(u => u.value === role)?.name || role})`)
|
||||||
|
router.replace(roleHome[role] || '/leader/home')
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchMode(mode) {
|
||||||
|
if (loginMode.value === mode) return
|
||||||
|
loginMode.value = mode
|
||||||
|
// 切换时清空两条表单, 避免脏数据
|
||||||
|
if (mode === 'password') {
|
||||||
|
smsForm.phone = ''
|
||||||
|
smsForm.smsCode = ''
|
||||||
|
smsForm.uuid = ''
|
||||||
|
if (smsSmsTimer) { clearInterval(smsSmsTimer); smsSmsTimer = null }
|
||||||
|
smsSmsCountdown.value = 0
|
||||||
|
} else {
|
||||||
|
form.username = ''
|
||||||
|
form.password = ''
|
||||||
|
form.code = ''
|
||||||
|
// 切到短信登录时不需要图形验证码, 不再 loadCaptcha
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendLoginSmsCode() {
|
||||||
|
if (!/^1\d{10}$/.test(smsForm.phone)) {
|
||||||
|
return ElMessage.warning('请输入正确的手机号')
|
||||||
|
}
|
||||||
|
await runSmsOnce(async () => {
|
||||||
|
try {
|
||||||
|
const r = await sendLoginSms(smsForm.phone)
|
||||||
|
smsForm.uuid = r.data || ''
|
||||||
|
ElMessage.success('验证码已发送')
|
||||||
|
smsSmsCountdown.value = 60
|
||||||
|
smsSmsTimer = setInterval(() => {
|
||||||
|
smsSmsCountdown.value--
|
||||||
|
if (smsSmsCountdown.value <= 0) {
|
||||||
|
clearInterval(smsSmsTimer)
|
||||||
|
smsSmsTimer = null
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
} catch (e) {
|
||||||
|
// request.js 已弹错误, 这里只重置 uuid
|
||||||
|
smsForm.uuid = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSmsLoginSubmit() {
|
||||||
|
if (!/^1\d{10}$/.test(smsForm.phone)) return ElMessage.warning('请输入正确的手机号')
|
||||||
|
if (!smsForm.smsCode) return ElMessage.warning('请输入短信验证码')
|
||||||
|
if (!smsForm.uuid) return ElMessage.warning('请先获取验证码')
|
||||||
|
await runSmsLoginOnce(async () => {
|
||||||
|
try {
|
||||||
|
const res = await smsLogin({ phone: smsForm.phone, smsCode: smsForm.smsCode, uuid: smsForm.uuid })
|
||||||
|
// 短信登录后无 username, 用 phone 作为显示名; role 兜底默认 leader
|
||||||
|
await afterLogin(res.token, smsForm.phone, 'leader')
|
||||||
|
} catch (e) {
|
||||||
|
// request.js 已弹错误
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function autoRole(username) {
|
function autoRole(username) {
|
||||||
const u = (username || '').toLowerCase()
|
const u = (username || '').toLowerCase()
|
||||||
if (u === 'admin' || u === 'ry') return 'admin'
|
if (u === 'admin' || u === 'ry') return 'admin'
|
||||||
@@ -241,10 +392,88 @@ function confirmRegister() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onForgot() {
|
function onForgot() {
|
||||||
ElMessage.info('请联系系统管理员重置密码')
|
showForgotModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 忘记密码弹窗 =====
|
||||||
|
const showForgotModal = ref(false)
|
||||||
|
const forgotLoading = ref(false)
|
||||||
|
const forgotSmsCountdown = ref(0)
|
||||||
|
let forgotSmsTimer = null
|
||||||
|
const forgotForm = reactive({ phone: '', smsCode: '', password: '', confirmPassword: '' })
|
||||||
|
let forgotSmsUuid = ''
|
||||||
|
|
||||||
|
function closeForgot() {
|
||||||
|
showForgotModal.value = false
|
||||||
|
forgotForm.phone = ''
|
||||||
|
forgotForm.smsCode = ''
|
||||||
|
forgotForm.password = ''
|
||||||
|
forgotForm.confirmPassword = ''
|
||||||
|
forgotSmsUuid = ''
|
||||||
|
if (forgotSmsTimer) { clearInterval(forgotSmsTimer); forgotSmsTimer = null }
|
||||||
|
forgotSmsCountdown.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendForgotSms() {
|
||||||
|
if (!/^1\d{10}$/.test(forgotForm.phone)) {
|
||||||
|
return ElMessage.warning('请输入正确的手机号')
|
||||||
|
}
|
||||||
|
await runForgotSmsOnce(async () => {
|
||||||
|
try {
|
||||||
|
const r = await request({ url: '/business/auth/forgotSendSms', method: 'post', data: { phone: forgotForm.phone } })
|
||||||
|
forgotSmsUuid = r.data || ''
|
||||||
|
ElMessage.success('验证码已发送')
|
||||||
|
forgotSmsCountdown.value = 60
|
||||||
|
forgotSmsTimer = setInterval(() => {
|
||||||
|
forgotSmsCountdown.value--
|
||||||
|
if (forgotSmsCountdown.value <= 0) {
|
||||||
|
clearInterval(forgotSmsTimer)
|
||||||
|
forgotSmsTimer = null
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
} catch (e) {
|
||||||
|
// request.js 拦截器已弹 ElMessage.error, 这里只重置
|
||||||
|
forgotSmsUuid = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onResetSubmit() {
|
||||||
|
if (!/^1\d{10}$/.test(forgotForm.phone)) return ElMessage.warning('请输入正确的手机号')
|
||||||
|
if (!forgotForm.smsCode) return ElMessage.warning('请输入短信验证码')
|
||||||
|
if (!forgotForm.password || forgotForm.password.length < 6 || forgotForm.password.length > 20) {
|
||||||
|
return ElMessage.warning('密码长度 6-20 位')
|
||||||
|
}
|
||||||
|
if (forgotForm.password !== forgotForm.confirmPassword) {
|
||||||
|
return ElMessage.warning('两次密码输入不一致')
|
||||||
|
}
|
||||||
|
forgotLoading.value = true
|
||||||
|
try {
|
||||||
|
await request({
|
||||||
|
url: '/business/auth/resetPassword',
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
phone: forgotForm.phone,
|
||||||
|
smsCode: forgotForm.smsCode,
|
||||||
|
uuid: forgotSmsUuid,
|
||||||
|
password: forgotForm.password,
|
||||||
|
confirmPassword: forgotForm.confirmPassword
|
||||||
|
}
|
||||||
|
})
|
||||||
|
ElMessage.success('密码重置成功, 请用新密码登录')
|
||||||
|
closeForgot()
|
||||||
|
} catch (e) {
|
||||||
|
// request.js 已弹错误
|
||||||
|
} finally {
|
||||||
|
forgotLoading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => { loadCaptcha() })
|
onMounted(() => { loadCaptcha() })
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (forgotSmsTimer) clearInterval(forgotSmsTimer)
|
||||||
|
if (smsSmsTimer) clearInterval(smsSmsTimer)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -396,6 +625,30 @@ onMounted(() => { loadCaptcha() })
|
|||||||
margin-bottom: 28px;
|
margin-bottom: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 登录模式切换 tabs */
|
||||||
|
.login-page .login-tabs{
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
.login-page .login-tabs .tab{
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
transition: color 0.2s, border-color 0.2s;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.login-page .login-tabs .tab:hover{ color: #1e3a8a; }
|
||||||
|
.login-page .login-tabs .tab.active{
|
||||||
|
color: #1e3a8a;
|
||||||
|
border-bottom-color: #1e3a8a;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.login-page .form-group{
|
.login-page .form-group{
|
||||||
margin-bottom: 18px;
|
margin-bottom: 18px;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -689,4 +942,41 @@ onMounted(() => { loadCaptcha() })
|
|||||||
.modal-btn.secondary:hover { background: #ebeef5; }
|
.modal-btn.secondary:hover { background: #ebeef5; }
|
||||||
.modal-btn.primary { background: var(--brand-primary, #1890ff); color: #fff; }
|
.modal-btn.primary { background: var(--brand-primary, #1890ff); color: #fff; }
|
||||||
.modal-btn.primary:hover { background: var(--brand-primary-deep, #096dd9); }
|
.modal-btn.primary:hover { background: var(--brand-primary-deep, #096dd9); }
|
||||||
|
|
||||||
|
/* ===== 忘记密码弹窗 ===== */
|
||||||
|
.modal-subtitle {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6b7280;
|
||||||
|
text-align: center;
|
||||||
|
margin: -12px 0 20px;
|
||||||
|
}
|
||||||
|
.sms-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.sms-group .sms-input {
|
||||||
|
flex: 1;
|
||||||
|
padding-left: 14px;
|
||||||
|
}
|
||||||
|
.sms-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 110px;
|
||||||
|
height: 44px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
background: #f9fafb;
|
||||||
|
color: #1e3a8a;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.sms-btn:hover:not(:disabled) {
|
||||||
|
border-color: #1e3a8a;
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
.sms-btn:disabled {
|
||||||
|
color: #9ca3af;
|
||||||
|
background: #f3f4f6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -38,8 +38,8 @@
|
|||||||
<el-form-item label="短信验证码" prop="smsCode">
|
<el-form-item label="短信验证码" prop="smsCode">
|
||||||
<div class="sms-row">
|
<div class="sms-row">
|
||||||
<el-input v-model="form.smsCode" placeholder="请输入收到的验证码" maxlength="6" />
|
<el-input v-model="form.smsCode" placeholder="请输入收到的验证码" maxlength="6" />
|
||||||
<el-button class="sms-btn" :disabled="smsCountdown > 0 || !form.phone" @click="sendSms">
|
<el-button class="sms-btn" :disabled="smsLocked || smsCountdown > 0 || !form.phone" @click="sendSms">
|
||||||
{{ smsCountdown > 0 ? `${smsCountdown}s 后重试` : '获取验证码' }}
|
{{ smsLocked ? '发送中...' : smsCountdown > 0 ? `${smsCountdown}s 后重试` : '获取验证码' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -53,7 +53,11 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-checkbox v-model="agreed">已阅读并接受 <a href="javascript:void(0)" @click="showAgreement = true">《平台协议》</a></el-checkbox>
|
<el-checkbox v-model="agreed">我已阅读并同意
|
||||||
|
<a href="#/article/agreement" target="_blank">《用户协议》</a>
|
||||||
|
和
|
||||||
|
<a href="#/article/privacy" target="_blank">《隐私政策》</a>
|
||||||
|
</el-checkbox>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
@@ -78,16 +82,6 @@
|
|||||||
已有账号? <a href="javascript:void(0)" @click="$router.push('/login')">立即登录</a>
|
已有账号? <a href="javascript:void(0)" @click="$router.push('/login')">立即登录</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 平台协议弹窗 -->
|
|
||||||
<el-dialog v-model="showAgreement" title="平台协议" width="560px">
|
|
||||||
<div class="agreement-content">
|
|
||||||
<p>1. 本平台仅提供项目撮合与流程管理服务, 不参与任何具体项目的执行.</p>
|
|
||||||
<p>2. 注册企业需提供真实有效的资质材料, 平台有权随时核查.</p>
|
|
||||||
<p>3. 平台对企业的商业信息负有保密义务.</p>
|
|
||||||
<p>4. 注册即表示同意上述条款.</p>
|
|
||||||
</div>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -96,12 +90,14 @@ import { reactive, ref, onUnmounted } from 'vue'
|
|||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
import { registerExecutor } from '@/api/public'
|
import { registerExecutor } from '@/api/public'
|
||||||
|
import { useAsyncLock } from '@/utils/useAsyncLock'
|
||||||
|
|
||||||
|
const { locked: smsLocked, run: sendSmsOnce } = useAsyncLock()
|
||||||
|
|
||||||
const step = ref(0)
|
const step = ref(0)
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const agreed = ref(false)
|
const agreed = ref(false)
|
||||||
const showAgreement = ref(false)
|
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
username: '',
|
username: '',
|
||||||
@@ -150,25 +146,27 @@ async function sendSms() {
|
|||||||
if (!/^1\d{10}$/.test(form.phone)) {
|
if (!/^1\d{10}$/.test(form.phone)) {
|
||||||
return ElMessage.warning('请先输入正确的手机号')
|
return ElMessage.warning('请先输入正确的手机号')
|
||||||
}
|
}
|
||||||
try {
|
await sendSmsOnce(async () => {
|
||||||
const resp = await request({
|
try {
|
||||||
url: '/business/auth/registerSendSms',
|
const resp = await request({
|
||||||
method: 'post',
|
url: '/business/auth/registerSendSms',
|
||||||
data: { phone: form.phone }
|
method: 'post',
|
||||||
})
|
data: { phone: form.phone }
|
||||||
smsUuid.value = resp?.data || ''
|
})
|
||||||
ElMessage.success('验证码已发送')
|
smsUuid.value = resp?.data || ''
|
||||||
smsCountdown.value = 60
|
ElMessage.success('验证码已发送')
|
||||||
smsTimer = setInterval(() => {
|
smsCountdown.value = 60
|
||||||
smsCountdown.value--
|
smsTimer = setInterval(() => {
|
||||||
if (smsCountdown.value <= 0) {
|
smsCountdown.value--
|
||||||
clearInterval(smsTimer)
|
if (smsCountdown.value <= 0) {
|
||||||
smsTimer = null
|
clearInterval(smsTimer)
|
||||||
}
|
smsTimer = null
|
||||||
}, 1000)
|
}
|
||||||
} catch (e) {
|
}, 1000)
|
||||||
ElMessage.warning(e?.msg || '验证码发送失败')
|
} catch (e) {
|
||||||
}
|
ElMessage.warning(e?.msg || '验证码发送失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 提交 =====
|
// ===== 提交 =====
|
||||||
|
|||||||
@@ -36,8 +36,8 @@
|
|||||||
<el-form-item label="短信验证码" prop="code">
|
<el-form-item label="短信验证码" prop="code">
|
||||||
<div class="sms-row">
|
<div class="sms-row">
|
||||||
<el-input v-model="form.code" placeholder="请输入收到的验证码" maxlength="6" />
|
<el-input v-model="form.code" placeholder="请输入收到的验证码" maxlength="6" />
|
||||||
<el-button :disabled="smsCountdown > 0 || !form.phone" @click="sendCode">
|
<el-button :disabled="smsLocked || smsCountdown > 0 || !form.phone" @click="sendCode">
|
||||||
{{ smsCountdown > 0 ? `${smsCountdown}s 后重新获取` : '获取验证码' }}
|
{{ smsLocked ? '发送中...' : smsCountdown > 0 ? `${smsCountdown}s 后重新获取` : '获取验证码' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -55,7 +55,11 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-checkbox v-model="agreed">已阅读并接受 <a href="javascript:void(0)" @click="showAgreement = true">《隐私保护条款及授权书》</a></el-checkbox>
|
<el-checkbox v-model="agreed">我已阅读并同意
|
||||||
|
<a href="#/article/agreement" target="_blank">《用户协议》</a>
|
||||||
|
和
|
||||||
|
<a href="#/article/privacy" target="_blank">《隐私政策》</a>
|
||||||
|
</el-checkbox>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
@@ -72,16 +76,6 @@
|
|||||||
</el-result>
|
</el-result>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 隐私协议弹窗 -->
|
|
||||||
<el-dialog v-model="showAgreement" title="隐私保护条款及授权书" width="600px">
|
|
||||||
<div class="agreement-content">
|
|
||||||
<p>1. 我们仅在为您提供专家注册、项目评审等服务的过程中,收集您的姓名、手机号、工作单位、职称等必要信息。</p>
|
|
||||||
<p>2. 您的个人信息将严格保密,仅用于项目流程内部使用,不会向任何第三方披露。</p>
|
|
||||||
<p>3. 您有权随时查看、更正、删除您的个人信息。</p>
|
|
||||||
<p>4. 注册即表示您同意上述条款。</p>
|
|
||||||
</div>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -94,6 +88,9 @@ import request from '@/utils/request'
|
|||||||
import DoctorDeptSelect from '@/components/DoctorDeptSelect.vue'
|
import DoctorDeptSelect from '@/components/DoctorDeptSelect.vue'
|
||||||
import DoctorTitleSelect from '@/components/DoctorTitleSelect.vue'
|
import DoctorTitleSelect from '@/components/DoctorTitleSelect.vue'
|
||||||
import OssImageUploader from '@/components/OssImageUploader.vue'
|
import OssImageUploader from '@/components/OssImageUploader.vue'
|
||||||
|
import { useAsyncLock } from '@/utils/useAsyncLock'
|
||||||
|
|
||||||
|
const { locked: smsLocked, run: sendSmsOnce } = useAsyncLock()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const step = ref(0)
|
const step = ref(0)
|
||||||
@@ -139,7 +136,6 @@ const rules = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const agreed = ref(false)
|
const agreed = ref(false)
|
||||||
const showAgreement = ref(false)
|
|
||||||
|
|
||||||
const smsCountdown = ref(0)
|
const smsCountdown = ref(0)
|
||||||
let smsTimer = null
|
let smsTimer = null
|
||||||
@@ -148,21 +144,23 @@ async function sendCode() {
|
|||||||
if (!/^1\d{10}$/.test(form.phone)) {
|
if (!/^1\d{10}$/.test(form.phone)) {
|
||||||
return ElMessage.warning('请先输入正确的手机号')
|
return ElMessage.warning('请先输入正确的手机号')
|
||||||
}
|
}
|
||||||
try {
|
await sendSmsOnce(async () => {
|
||||||
const resp = await request({ url: '/business/auth/registerSendSms', method: 'post', data: { phone: form.phone } })
|
try {
|
||||||
smsUuid.value = resp?.data || ''
|
const resp = await request({ url: '/business/auth/registerSendSms', method: 'post', data: { phone: form.phone } })
|
||||||
ElMessage.success('验证码已发送')
|
smsUuid.value = resp?.data || ''
|
||||||
smsCountdown.value = 60
|
ElMessage.success('验证码已发送')
|
||||||
smsTimer = setInterval(() => {
|
smsCountdown.value = 60
|
||||||
smsCountdown.value--
|
smsTimer = setInterval(() => {
|
||||||
if (smsCountdown.value <= 0) {
|
smsCountdown.value--
|
||||||
clearInterval(smsTimer)
|
if (smsCountdown.value <= 0) {
|
||||||
smsTimer = null
|
clearInterval(smsTimer)
|
||||||
}
|
smsTimer = null
|
||||||
}, 1000)
|
}
|
||||||
} catch (e) {
|
}, 1000)
|
||||||
ElMessage.warning(e?.msg || '验证码发送失败')
|
} catch (e) {
|
||||||
}
|
ElMessage.warning(e?.msg || '验证码发送失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSubmit() {
|
async function onSubmit() {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<el-form :model="form" label-width="100px"><el-form-item label="用户名"><el-input v-model="form.username" /></el-form-item><el-form-item label="密码"><el-input v-model="form.password" type="password" /></el-form-item></el-form>
|
<el-form :model="form" label-width="100px"><el-form-item label="用户名"><el-input v-model="form.username" /></el-form-item><el-form-item label="密码"><el-input v-model="form.password" type="password" /></el-form-item></el-form>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<el-form :model="form" label-width="100px"><el-form-item label="单位名称"><el-input v-model="form.unitName" /></el-form-item><el-form-item label="联系人"><el-input v-model="form.contact" /></el-form-item><el-form-item label="联系电话"><el-input v-model="form.phone" /></el-form-item><el-form-item label="赞助意向"><el-input v-model="form.intent" type="textarea" /></el-form-item></el-form>
|
<el-form :model="form" label-width="100px"><el-form-item label="单位名称"><el-input v-model="form.unitName" /></el-form-item><el-form-item label="联系人"><el-input v-model="form.contact" /></el-form-item><el-form-item label="联系电话"><el-input v-model="form.phone" /></el-form-item><el-form-item label="赞助意向"><el-input v-model="form.intent" type="textarea" /></el-form-item><el-form-item><el-checkbox v-model="agreed">我已阅读并同意 <a href="#/article/agreement" target="_blank">《用户协议》</a> 和 <a href="#/article/privacy" target="_blank">《隐私政策》</a></el-checkbox></el-form-item></el-form>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align:center;margin-top:16px">
|
<div style="text-align:center;margin-top:16px">
|
||||||
@@ -27,9 +27,15 @@ import { registerSponsor } from '@/api/public'
|
|||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
const step = ref(0)
|
const step = ref(0)
|
||||||
const form = reactive({ username: '', password: '', unitName: '', contact: '', phone: '', intent: '' })
|
const form = reactive({ username: '', password: '', unitName: '', contact: '', phone: '', intent: '' })
|
||||||
|
const agreed = ref(false)
|
||||||
const prev = () => step.value = Math.max(0, step.value - 1)
|
const prev = () => step.value = Math.max(0, step.value - 1)
|
||||||
const next = async () => {
|
const next = async () => {
|
||||||
if (step.value === 1) { try { await registerSponsor(form); ElMessage.success('已提交') } catch { /* demo */ } step.value = 2; return }
|
if (step.value === 1) {
|
||||||
|
if (!agreed.value) return ElMessage.warning('请先阅读并同意用户协议和隐私政策')
|
||||||
|
try { await registerSponsor(form); ElMessage.success('已提交') } catch { /* demo */ }
|
||||||
|
step.value = 2
|
||||||
|
return
|
||||||
|
}
|
||||||
step.value++
|
step.value++
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -147,8 +147,8 @@
|
|||||||
<el-form-item label="短信验证码" :required="true">
|
<el-form-item label="短信验证码" :required="true">
|
||||||
<div class="sms-row">
|
<div class="sms-row">
|
||||||
<el-input v-model="phoneForm.smsCode" placeholder="请输入验证码" />
|
<el-input v-model="phoneForm.smsCode" placeholder="请输入验证码" />
|
||||||
<el-button class="btn-sms" :disabled="phoneSmsCountdown > 0" @click="sendSms">
|
<el-button class="btn-sms" :disabled="phoneSmsLocked || phoneSmsCountdown > 0" @click="sendSms">
|
||||||
{{ phoneSmsCountdown > 0 ? `${phoneSmsCountdown}s 后重试` : '发送验证码' }}
|
{{ phoneSmsLocked ? '发送中...' : phoneSmsCountdown > 0 ? `${phoneSmsCountdown}s 后重试` : '发送验证码' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -174,6 +174,9 @@ import DoctorDeptSelect from '@/components/DoctorDeptSelect.vue'
|
|||||||
import DoctorTitleSelect from '@/components/DoctorTitleSelect.vue'
|
import DoctorTitleSelect from '@/components/DoctorTitleSelect.vue'
|
||||||
import IdCardUploader from '@/components/IdCardUploader.vue'
|
import IdCardUploader from '@/components/IdCardUploader.vue'
|
||||||
import OssImageUploader from '@/components/OssImageUploader.vue'
|
import OssImageUploader from '@/components/OssImageUploader.vue'
|
||||||
|
import { useAsyncLock } from '@/utils/useAsyncLock'
|
||||||
|
|
||||||
|
const { locked: phoneSmsLocked, run: sendPhoneSmsOnce } = useAsyncLock()
|
||||||
|
|
||||||
const store = useUserStore()
|
const store = useUserStore()
|
||||||
const tab = ref('profile')
|
const tab = ref('profile')
|
||||||
@@ -409,25 +412,27 @@ async function sendSms() {
|
|||||||
if (!/^(1[0-9])\d{9}$/.test(phone)) {
|
if (!/^(1[0-9])\d{9}$/.test(phone)) {
|
||||||
return ElMessage.warning('请输入正确的手机号')
|
return ElMessage.warning('请输入正确的手机号')
|
||||||
}
|
}
|
||||||
try {
|
await sendPhoneSmsOnce(async () => {
|
||||||
const res = await request({
|
try {
|
||||||
url: '/system/user/profile/sendSmsCode',
|
const res = await request({
|
||||||
method: 'post',
|
url: '/system/user/profile/sendSmsCode',
|
||||||
params: { phone }
|
method: 'post',
|
||||||
})
|
params: { phone }
|
||||||
phoneSmsUuid.value = res.uuid || ''
|
})
|
||||||
ElMessage.success('验证码已发送')
|
phoneSmsUuid.value = res.uuid || ''
|
||||||
phoneSmsCountdown.value = 60
|
ElMessage.success('验证码已发送')
|
||||||
phoneSmsTimer = setInterval(() => {
|
phoneSmsCountdown.value = 60
|
||||||
phoneSmsCountdown.value--
|
phoneSmsTimer = setInterval(() => {
|
||||||
if (phoneSmsCountdown.value <= 0) {
|
phoneSmsCountdown.value--
|
||||||
clearInterval(phoneSmsTimer)
|
if (phoneSmsCountdown.value <= 0) {
|
||||||
phoneSmsTimer = null
|
clearInterval(phoneSmsTimer)
|
||||||
}
|
phoneSmsTimer = null
|
||||||
}, 1000)
|
}
|
||||||
} catch (e) {
|
}, 1000)
|
||||||
ElMessage.error(e?.msg || '发送失败')
|
} catch (e) {
|
||||||
}
|
ElMessage.error(e?.msg || '发送失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onChangePhone() {
|
async function onChangePhone() {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
<el-descriptions-item label="项目编号">{{ detail.projectNo }}</el-descriptions-item>
|
<el-descriptions-item label="项目编号">{{ detail.projectNo }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="项目形式">{{ detail.projectForm || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="项目形式">{{ detail.projectForm || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="项目名称" :span="2">{{ detail.projectName }}</el-descriptions-item>
|
<el-descriptions-item label="项目名称" :span="2">{{ detail.projectName }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="赞助公司" :span="2">{{ detail.orgName || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="赞助方负责人" :span="2">{{ detail.sponsorAdminUserName || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="总场次">{{ detail.totalSessions || 0 }}</el-descriptions-item>
|
<el-descriptions-item label="总场次">{{ detail.totalSessions || 0 }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="已执行">{{ detail.doneSessions || 0 }}</el-descriptions-item>
|
<el-descriptions-item label="已执行">{{ detail.doneSessions || 0 }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="未执行">{{ detail.todoSessions || 0 }}</el-descriptions-item>
|
<el-descriptions-item label="未执行">{{ detail.todoSessions || 0 }}</el-descriptions-item>
|
||||||
|
|||||||
@@ -124,7 +124,7 @@
|
|||||||
<el-descriptions-item label="项目编号">{{ detail.projectNo }}</el-descriptions-item>
|
<el-descriptions-item label="项目编号">{{ detail.projectNo }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="项目形式">{{ detail.projectForm }}</el-descriptions-item>
|
<el-descriptions-item label="项目形式">{{ detail.projectForm }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="项目名称" :span="2">{{ detail.projectName }}</el-descriptions-item>
|
<el-descriptions-item label="项目名称" :span="2">{{ detail.projectName }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="赞助公司" :span="2">{{ detail.orgName || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="赞助方负责人" :span="2">{{ detail.sponsorAdminUserName || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="总场次/总期数">{{ detail.totalSessions }}/{{ detail.totalPeriods }}</el-descriptions-item>
|
<el-descriptions-item label="总场次/总期数">{{ detail.totalSessions }}/{{ detail.totalPeriods }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="已执行">{{ detail.doneSessions || 0 }}</el-descriptions-item>
|
<el-descriptions-item label="已执行">{{ detail.doneSessions || 0 }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="未执行">{{ detail.todoSessions || 0 }}</el-descriptions-item>
|
<el-descriptions-item label="未执行">{{ detail.todoSessions || 0 }}</el-descriptions-item>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<span class="info-value">{{ fmtDate(row.endTime) }}</span>
|
<span class="info-value">{{ fmtDate(row.endTime) }}</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="赞助公司">
|
<el-form-item label="赞助公司">
|
||||||
<span class="info-value">{{ row.orgName || '-' }}</span>
|
<span class="info-value">{{ row.sponsorAdminUserName || '-' }}</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="项目形式">
|
<el-form-item label="项目形式">
|
||||||
<span class="info-value">{{ row.projectForm || '-' }}</span>
|
<span class="info-value">{{ row.projectForm || '-' }}</span>
|
||||||
|
|||||||
@@ -3,18 +3,13 @@
|
|||||||
<div class="breadcrumb">首页 / 项目管理</div>
|
<div class="breadcrumb">首页 / 项目管理</div>
|
||||||
|
|
||||||
<el-form inline :model="q" class="filter-form">
|
<el-form inline :model="q" class="filter-form">
|
||||||
<el-form-item label="赞助公司:"><el-input v-model="q.orgName" placeholder="输入赞助公司" clearable /></el-form-item>
|
<el-form-item label="赞助方负责人:"><el-input v-model="q.sponsorAdminUserName" placeholder="输入账号/昵称" clearable /></el-form-item>
|
||||||
<el-form-item label="公司类型:">
|
<el-form-item label="服务机构:"><el-input v-model="q.execOrgName" placeholder="输入服务机构" clearable /></el-form-item>
|
||||||
<el-select v-model="q.orgType" placeholder="全部" clearable style="width:120px">
|
|
||||||
<el-option label="赞助方" value="sponsor" />
|
|
||||||
<el-option label="执行方" value="executor" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="项目编号:"><el-input v-model="q.projectNo" placeholder="输入项目编号" clearable /></el-form-item>
|
<el-form-item label="项目编号:"><el-input v-model="q.projectNo" placeholder="输入项目编号" clearable /></el-form-item>
|
||||||
<el-form-item label="项目名称:"><el-input v-model="q.projectName" placeholder="输入项目名称" clearable /></el-form-item>
|
<el-form-item label="项目名称:"><el-input v-model="q.projectName" placeholder="输入项目名称" clearable /></el-form-item>
|
||||||
<el-form-item label="项目形式:">
|
<el-form-item label="项目形式:">
|
||||||
<el-select v-model="q.projectForm" placeholder="请选择" clearable>
|
<el-select v-model="q.projectForm" placeholder="请选择" clearable style="width:140px">
|
||||||
<el-option label="线上" value="线上" /><el-option label="线下" value="下线" />
|
<el-option label="线上" value="线上" /><el-option label="线下" value="线下" />
|
||||||
<el-option label="线上+线下" value="线上+线下" /><el-option label="其他" value="其他" />
|
<el-option label="线上+线下" value="线上+线下" /><el-option label="其他" value="其他" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -24,12 +19,12 @@
|
|||||||
<el-date-picker v-model="q.endTime" type="date" placeholder="结束日期" value-format="YYYY-MM-DD HH:mm:ss" />
|
<el-date-picker v-model="q.endTime" type="date" placeholder="结束日期" value-format="YYYY-MM-DD HH:mm:ss" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="是否结题:">
|
<el-form-item label="是否结题:">
|
||||||
<el-select v-model="q.isFinished" placeholder="请选择" clearable>
|
<el-select v-model="q.isFinished" placeholder="请选择" clearable style="width:120px">
|
||||||
<el-option label="已结题" value="1" /><el-option label="未结题" value="0" />
|
<el-option label="已结题" value="1" /><el-option label="未结题" value="0" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="是否结算:">
|
<el-form-item label="是否结算:">
|
||||||
<el-select v-model="q.isSettled" placeholder="请选择" clearable>
|
<el-select v-model="q.isSettled" placeholder="请选择" clearable style="width:120px">
|
||||||
<el-option label="已结算" value="1" /><el-option label="未结算" value="0" />
|
<el-option label="已结算" value="1" /><el-option label="未结算" value="0" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -69,14 +64,7 @@
|
|||||||
<span v-else style="color:#c0c4cc">-</span>
|
<span v-else style="color:#c0c4cc">-</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="orgName" label="赞助公司" width="160" show-overflow-tooltip />
|
<el-table-column prop="sponsorAdminUserName" label="赞助方负责人" width="160" show-overflow-tooltip />
|
||||||
<el-table-column prop="orgType" label="公司类型" width="100" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag :type="row.orgType === 'sponsor' ? 'success' : 'primary'" disable-transitions>
|
|
||||||
{{ row.orgType === 'sponsor' ? '赞助方' : '执行方' }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="projectForm" label="项目形式" width="100" align="center" />
|
<el-table-column prop="projectForm" label="项目形式" width="100" align="center" />
|
||||||
<el-table-column prop="isFinished" label="是否结题" width="100" align="center">
|
<el-table-column prop="isFinished" label="是否结题" width="100" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
@@ -133,11 +121,26 @@
|
|||||||
<div style="color:#606266;font-size:13px">共 <b>{{ assignBatchProjects.length }}</b> 个项目</div>
|
<div style="color:#606266;font-size:13px">共 <b>{{ assignBatchProjects.length }}</b> 个项目</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="支持方">
|
<el-form-item label="支持方">
|
||||||
<el-select v-model="assignForm.orgId" placeholder="请选择赞助公司" filterable :filter-method="searchSupporters" clearable style="width:100%" @change="onSupporterPick">
|
<el-select v-model="assignForm.sponsorAdminUserId" placeholder="请选择赞助方负责人" filterable :filter-method="searchSupporters" clearable style="width:100%" @change="onSupporterPick">
|
||||||
<el-option v-for="u in supporterOptions" :key="u.userId" :label="`${u.userName} (${u.nickName || ''})`" :value="u.userId" />
|
<el-option v-for="u in supporterOptions" :key="u.userId" :label="`${u.userName} (${u.nickName || ''})`" :value="u.userId" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-divider>执行方分配</el-divider>
|
<el-divider>执行方分配</el-divider>
|
||||||
|
<!-- 单选模式: 显示项目总场次 / 总金额(元) vs 已分配 (实时) -->
|
||||||
|
<div v-if="!assignBatchMode" class="assign-sessions-bar">
|
||||||
|
<span>项目总场次: <b>{{ assignForm.totalSessions || 0 }}</b></span>
|
||||||
|
<span :class="{ 'is-over': singleSessionOver }">已分配场次: <b>{{ assignedSessions }}</b></span>
|
||||||
|
<span v-if="singleSessionOver" class="over-warn">⚠ 已超出 {{ assignedSessions - assignForm.totalSessions }} 场</span>
|
||||||
|
<span class="sep">|</span>
|
||||||
|
<span>总金额(元): <b>{{ assignForm.totalAmount || 0 }}</b></span>
|
||||||
|
<span :class="{ 'is-over': singleAmountOver }">已分配金额: <b>¥ {{ assignedAmount }}</b></span>
|
||||||
|
<span v-if="singleAmountOver" class="over-warn">⚠ 已超出 ¥ {{ Math.round((assignedAmount - assignForm.totalAmount) * 100) / 100 }}</span>
|
||||||
|
</div>
|
||||||
|
<!-- 批量模式: 提示保存时按各项目自身总场次 / 总金额逐项校验 -->
|
||||||
|
<div v-else class="assign-sessions-bar">
|
||||||
|
<span>本次分配: <b>{{ assignedSessions }}</b> 场 / <b>¥ {{ assignedAmount }}</b></span>
|
||||||
|
<span class="hint">(保存时按各项目自身的总场次和总金额逐项校验)</span>
|
||||||
|
</div>
|
||||||
<el-table :data="assignForm.execRows" border>
|
<el-table :data="assignForm.execRows" border>
|
||||||
<el-table-column type="index" label="#" width="50" />
|
<el-table-column type="index" label="#" width="50" />
|
||||||
<el-table-column label="执行方名称" min-width="220">
|
<el-table-column label="执行方名称" min-width="220">
|
||||||
@@ -264,7 +267,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted, watch } from 'vue'
|
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { bizList, bizAdd, bizUpdate, bizDelete } from '@/api/public'
|
import { bizList, bizAdd, bizUpdate, bizDelete } from '@/api/public'
|
||||||
import { listExecutor, listSupporters } from '@/api/system'
|
import { listExecutor, listSupporters } from '@/api/system'
|
||||||
@@ -275,7 +278,7 @@ import Preview from '@/components/Preview.vue'
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const q = ref({ orgName: '', orgType: '', projectNo: '', projectName: '', projectForm: '', startTime: '', endTime: '', isFinished: '', isSettled: '', ratingScore: '' })
|
const q = ref({ sponsorAdminUserName: '', execOrgName: '', projectNo: '', projectName: '', projectForm: '', startTime: '', endTime: '', isFinished: '', isSettled: '', ratingScore: '' })
|
||||||
const rows = ref([])
|
const rows = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
|
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
|
||||||
@@ -327,7 +330,7 @@ async function doDelete(row) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function reset() { q.value = { orgName:'', orgType:'', projectNo:'', projectName:'', projectForm:'', startTime:'', endTime:'', isFinished:'', isSettled:'', ratingScore:'' }; page.pageNum = 1; load() }
|
function reset() { q.value = { sponsorAdminUserName:'', execOrgName:'', projectNo:'', projectName:'', projectForm:'', startTime:'', endTime:'', isFinished:'', isSettled:'', ratingScore:'' }; page.pageNum = 1; load() }
|
||||||
|
|
||||||
// 导出
|
// 导出
|
||||||
function exportProjects() { ElMessage.info('导出项目功能开发中') }
|
function exportProjects() { ElMessage.info('导出项目功能开发中') }
|
||||||
@@ -563,20 +566,38 @@ const assignBatchProjects = ref([]) // 批量模式下的项目列表 [
|
|||||||
const assignSubmitting = ref(false)
|
const assignSubmitting = ref(false)
|
||||||
const assignForm = reactive({
|
const assignForm = reactive({
|
||||||
projectId: '',
|
projectId: '',
|
||||||
orgId: null,
|
sponsorAdminUserId: null,
|
||||||
orgName: '',
|
sponsorAdminUserName: '',
|
||||||
|
totalSessions: 0, // 项目总场次 (来自项目本身, 不可被分配覆盖)
|
||||||
|
totalAmount: 0, // 项目总金额(元) (来自项目本身, 不可被分配覆盖)
|
||||||
execRows: [], // 动态加
|
execRows: [], // 动态加
|
||||||
deadlineDays: 30
|
deadlineDays: 30
|
||||||
})
|
})
|
||||||
const supporterOptions = ref([])
|
const supporterOptions = ref([])
|
||||||
let _supporterTimer = null
|
let _supporterTimer = null
|
||||||
|
|
||||||
|
// 已分配场次合计 (实时)
|
||||||
|
const assignedSessions = computed(() =>
|
||||||
|
assignForm.execRows.reduce((s, r) => s + Number(r.sessions || 0), 0)
|
||||||
|
)
|
||||||
|
const singleSessionOver = computed(() =>
|
||||||
|
!assignBatchMode.value && Number(assignForm.totalSessions || 0) > 0 && assignedSessions.value > Number(assignForm.totalSessions)
|
||||||
|
)
|
||||||
|
// 已分配金额合计 (实时, 保留 2 位小数避免浮点漂移)
|
||||||
|
const assignedAmount = computed(() => {
|
||||||
|
const sum = assignForm.execRows.reduce((s, r) => s + Number(r.amount || 0), 0)
|
||||||
|
return Math.round(sum * 100) / 100
|
||||||
|
})
|
||||||
|
const singleAmountOver = computed(() =>
|
||||||
|
!assignBatchMode.value && Number(assignForm.totalAmount || 0) > 0 && assignedAmount.value > Number(assignForm.totalAmount)
|
||||||
|
)
|
||||||
|
|
||||||
function makeEmptyExecRow() {
|
function makeEmptyExecRow() {
|
||||||
return { _loading: false, _timer: null, _options: [], execUserId: null, execUserName: '', execNickName: '', execOrg: '', sessions: 0, amount: 0, remark: '' }
|
return { _loading: false, _timer: null, _options: [], execUserId: null, execUserName: '', execNickName: '', execOrg: '', sessions: 0, amount: 0, remark: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetAssignForm() {
|
function resetAssignForm() {
|
||||||
Object.assign(assignForm, { projectId:'', orgId:null, orgName:'', execRows:[makeEmptyExecRow()], deadlineDays:30 })
|
Object.assign(assignForm, { projectId:'', sponsorAdminUserId:null, sponsorAdminUserName:'', totalSessions:0, totalAmount:0, execRows:[makeEmptyExecRow()], deadlineDays:30 })
|
||||||
supporterOptions.value = []
|
supporterOptions.value = []
|
||||||
assignBatchMode.value = false
|
assignBatchMode.value = false
|
||||||
assignBatchProjects.value = []
|
assignBatchProjects.value = []
|
||||||
@@ -592,8 +613,10 @@ function addExecRow() {
|
|||||||
function onAssignProjectChange(v) {
|
function onAssignProjectChange(v) {
|
||||||
const proj = rows.value.find(r => r.projectId === v)
|
const proj = rows.value.find(r => r.projectId === v)
|
||||||
if (proj) {
|
if (proj) {
|
||||||
assignForm.orgId = proj.orgId || null
|
assignForm.sponsorAdminUserId = proj.sponsorAdminUserId || null
|
||||||
assignForm.orgName = proj.orgName || ''
|
assignForm.sponsorAdminUserName = proj.sponsorAdminUserName || ''
|
||||||
|
assignForm.totalSessions = Number(proj.totalSessions || 0)
|
||||||
|
assignForm.totalAmount = Number(proj.totalAmount || 0)
|
||||||
}
|
}
|
||||||
// 切换项目时重新拉已分配记录 (回显)
|
// 切换项目时重新拉已分配记录 (回显)
|
||||||
loadAssigns(v)
|
loadAssigns(v)
|
||||||
@@ -611,9 +634,9 @@ async function loadSupporters(query) {
|
|||||||
const r = await listSupporters(params)
|
const r = await listSupporters(params)
|
||||||
supporterOptions.value = r.data || []
|
supporterOptions.value = r.data || []
|
||||||
// 已选项不在结果里时单独拉取并加首位
|
// 已选项不在结果里时单独拉取并加首位
|
||||||
if (assignForm.orgId && !supporterOptions.value.find(u => u.userId === assignForm.orgId)) {
|
if (assignForm.sponsorAdminUserId && !supporterOptions.value.find(u => u.userId === assignForm.sponsorAdminUserId)) {
|
||||||
const r2 = await listSupporters({ userId: assignForm.orgId })
|
const r2 = await listSupporters({ userId: assignForm.sponsorAdminUserId })
|
||||||
const sel = (r2.data || []).find(u => u.userId === assignForm.orgId)
|
const sel = (r2.data || []).find(u => u.userId === assignForm.sponsorAdminUserId)
|
||||||
if (sel) supporterOptions.value = [sel, ...supporterOptions.value]
|
if (sel) supporterOptions.value = [sel, ...supporterOptions.value]
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -627,7 +650,7 @@ function searchSupporters(q) { loadSupporters(q) }
|
|||||||
// 选中支持方后回填名称
|
// 选中支持方后回填名称
|
||||||
function onSupporterPick(userId) {
|
function onSupporterPick(userId) {
|
||||||
const u = supporterOptions.value.find(x => x.userId === userId)
|
const u = supporterOptions.value.find(x => x.userId === userId)
|
||||||
if (u) assignForm.orgName = u.nickName || u.userName
|
if (u) assignForm.sponsorAdminUserName = u.nickName || u.userName
|
||||||
}
|
}
|
||||||
|
|
||||||
// 拉取项目已分配的执行方
|
// 拉取项目已分配的执行方
|
||||||
@@ -698,8 +721,34 @@ async function submitAssign() {
|
|||||||
if (!assignBatchMode.value && !assignForm.projectId) return ElMessage.warning('请选择项目')
|
if (!assignBatchMode.value && !assignForm.projectId) return ElMessage.warning('请选择项目')
|
||||||
const valid = assignForm.execRows.filter(r => r.execUserId && Number(r.sessions) > 0)
|
const valid = assignForm.execRows.filter(r => r.execUserId && Number(r.sessions) > 0)
|
||||||
if (!valid.length) return ElMessage.warning('请至少选择 1 个执行方并填写场次')
|
if (!valid.length) return ElMessage.warning('请至少选择 1 个执行方并填写场次')
|
||||||
const amount = valid.reduce((s, r) => s + Number(r.amount || 0), 0)
|
const amount = Math.round(valid.reduce((s, r) => s + Number(r.amount || 0), 0) * 100) / 100
|
||||||
const sessions = valid.reduce((s, r) => s + Number(r.sessions || 0), 0)
|
const sessions = valid.reduce((s, r) => s + Number(r.sessions || 0), 0)
|
||||||
|
|
||||||
|
// 校验: 各执行方分配场次 / 金额总和 <= 项目总场次 / 总金额
|
||||||
|
// (项目自身的 total_sessions / total_amount 是创建时定的, 不再被分配覆盖)
|
||||||
|
if (assignBatchMode.value) {
|
||||||
|
for (const p of assignBatchProjects.value) {
|
||||||
|
const proj = rows.value.find(r => r.projectId === p.projectId)
|
||||||
|
const capS = proj ? Number(proj.totalSessions || 0) : 0
|
||||||
|
const capA = proj ? Number(proj.totalAmount || 0) : 0
|
||||||
|
if (capS > 0 && sessions > capS) {
|
||||||
|
return ElMessage.warning(`项目 ${p.projectNo} 已分配 ${sessions} 场, 超过总场次 ${capS}, 请调整`)
|
||||||
|
}
|
||||||
|
if (capA > 0 && amount > capA) {
|
||||||
|
return ElMessage.warning(`项目 ${p.projectNo} 已分配 ¥${amount}, 超过总金额 ¥${capA}, 请调整`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const capS = Number(assignForm.totalSessions || 0)
|
||||||
|
const capA = Number(assignForm.totalAmount || 0)
|
||||||
|
if (capS > 0 && sessions > capS) {
|
||||||
|
return ElMessage.warning(`已分配 ${sessions} 场, 超过项目总场次 ${capS}, 请调整`)
|
||||||
|
}
|
||||||
|
if (capA > 0 && amount > capA) {
|
||||||
|
return ElMessage.warning(`已分配 ¥${amount}, 超过项目总金额 ¥${capA}, 请调整`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const assignBody = valid.map(r => ({
|
const assignBody = valid.map(r => ({
|
||||||
execUserId: r.execUserId,
|
execUserId: r.execUserId,
|
||||||
execUserName: r.execUserName,
|
execUserName: r.execUserName,
|
||||||
@@ -718,10 +767,8 @@ async function submitAssign() {
|
|||||||
try {
|
try {
|
||||||
await bizUpdate('project', {
|
await bizUpdate('project', {
|
||||||
projectId: p.projectId,
|
projectId: p.projectId,
|
||||||
orgId: assignForm.orgId,
|
sponsorAdminUserId: assignForm.sponsorAdminUserId,
|
||||||
orgName: assignForm.orgName,
|
sponsorAdminUserName: assignForm.sponsorAdminUserName,
|
||||||
totalAmount: amount,
|
|
||||||
totalSessions: sessions,
|
|
||||||
submitDeadlineDays: assignForm.deadlineDays,
|
submitDeadlineDays: assignForm.deadlineDays,
|
||||||
})
|
})
|
||||||
await request({
|
await request({
|
||||||
@@ -740,10 +787,8 @@ async function submitAssign() {
|
|||||||
// 单条模式
|
// 单条模式
|
||||||
await bizUpdate('project', {
|
await bizUpdate('project', {
|
||||||
projectId: assignForm.projectId,
|
projectId: assignForm.projectId,
|
||||||
orgId: assignForm.orgId,
|
sponsorAdminUserId: assignForm.sponsorAdminUserId,
|
||||||
orgName: assignForm.orgName,
|
sponsorAdminUserName: assignForm.sponsorAdminUserName,
|
||||||
totalAmount: amount,
|
|
||||||
totalSessions: sessions,
|
|
||||||
submitDeadlineDays: assignForm.deadlineDays,
|
submitDeadlineDays: assignForm.deadlineDays,
|
||||||
})
|
})
|
||||||
await request({
|
await request({
|
||||||
@@ -830,5 +875,18 @@ watch(() => route.path + (route.query._t || ''), () => load())
|
|||||||
.score-item { display: flex; align-items: center; gap: 8px; }
|
.score-item { display: flex; align-items: center; gap: 8px; }
|
||||||
.score-item span { width: 70px; color: #606266; font-size: 13px; }
|
.score-item span { width: 70px; color: #606266; font-size: 13px; }
|
||||||
|
|
||||||
|
/* 分配 dialog 总场次条 */
|
||||||
|
.assign-sessions-bar {
|
||||||
|
display: flex; align-items: center; gap: 18px;
|
||||||
|
margin: -4px 0 12px; padding: 10px 14px;
|
||||||
|
background: #f5f7fa; border: 1px solid #ebeef5; border-radius: 4px;
|
||||||
|
font-size: 13px; color: #606266;
|
||||||
|
}
|
||||||
|
.assign-sessions-bar b { color: #303133; font-size: 14px; margin: 0 2px; }
|
||||||
|
.assign-sessions-bar .is-over b { color: #f56c6c; }
|
||||||
|
.assign-sessions-bar .over-warn { color: #f56c6c; font-weight: 600; }
|
||||||
|
.assign-sessions-bar .hint { color: #909399; font-size: 12px; }
|
||||||
|
.assign-sessions-bar .sep { color: #dcdfe6; margin: 0 4px; }
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="支持单位">
|
<el-form-item label="支持单位">
|
||||||
<el-select v-model="form.orgId" placeholder="请选择赞助公司" filterable clearable style="width:100%" @change="onSupporterPick">
|
<el-select v-model="form.sponsorAdminUserId" placeholder="请选择赞助方负责人" filterable clearable style="width:100%" @change="onSupporterPick">
|
||||||
<el-option v-for="u in supporterOptions" :key="u.userId"
|
<el-option v-for="u in supporterOptions" :key="u.userId"
|
||||||
:label="`${u.userName}${u.nickName ? ' (' + u.nickName + ')' : ''}`"
|
:label="`${u.userName}${u.nickName ? ' (' + u.nickName + ')' : ''}`"
|
||||||
:value="u.userId" />
|
:value="u.userId" />
|
||||||
@@ -104,37 +104,21 @@
|
|||||||
|
|
||||||
<!-- ========== 第三区块:公告文件 ========== -->
|
<!-- ========== 第三区块:公告文件 ========== -->
|
||||||
<div class="new-card-title">公告文件</div>
|
<div class="new-card-title">公告文件</div>
|
||||||
<p class="hint-text">*勾选公告类型后,上传对应文件;发布公告时可同时发布多个类型</p>
|
<p class="hint-text">*支持 PDF / 图片, 单文件 ≤ 10MB; 名称规则: 公告文件类型 + 项目编号后两段 + 项目名称</p>
|
||||||
<div class="notice-list">
|
<div class="notice-list">
|
||||||
<div v-for="(n, idx) in form.notices" :key="idx" class="notice-row">
|
<div v-for="(n, idx) in form.notices" :key="idx" class="notice-row">
|
||||||
<el-checkbox v-model="n.enabled" :label="n.key" class="notice-check">
|
<span class="notice-label">{{ n.label }}</span>
|
||||||
{{ n.label }}
|
<oss-file-uploader
|
||||||
</el-checkbox>
|
|
||||||
<el-upload
|
|
||||||
v-if="n.enabled"
|
|
||||||
class="notice-uploader"
|
class="notice-uploader"
|
||||||
:show-file-list="true"
|
v-model="n.url"
|
||||||
:file-list="getFileList(n)"
|
:dir="`ry8080/project/${n.key}/`"
|
||||||
:before-upload="(file) => beforeUpload(file, n)"
|
accept=".pdf,.png,.jpg,.jpeg,.gif,.webp"
|
||||||
:http-request="(opts) => httpUpload(opts, n, idx)"
|
:placeholder="`点击上传${n.label}`"
|
||||||
:on-remove="(file) => onRemove(n, file)"
|
hint="支持 PDF / 图片"
|
||||||
:on-preview="(file) => onPreview(file)"
|
block
|
||||||
accept=".pdf,.doc,.docx"
|
/>
|
||||||
drag
|
|
||||||
action="#"
|
|
||||||
>
|
|
||||||
<i class="el-icon-upload"></i>
|
|
||||||
<div class="el-upload__text">
|
|
||||||
将文件拖到此处,或<em>点击上传</em>
|
|
||||||
</div>
|
|
||||||
<div class="el-upload__tip" slot="tip">
|
|
||||||
支持 PDF / Word,单文件 ≤ 20MB
|
|
||||||
</div>
|
|
||||||
</el-upload>
|
|
||||||
<div v-else class="notice-disabled">勾选「{{ n.label }}」后可上传文件</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="hint-text">*名称规则:公告文件类型+取项目编号的后两段+项目名称</p>
|
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<!-- 底部按钮 -->
|
<!-- 底部按钮 -->
|
||||||
@@ -152,8 +136,8 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
import { bizAdd, bizUpdate, bizGet } from '@/api/public'
|
import { bizAdd, bizUpdate, bizGet } from '@/api/public'
|
||||||
import { listExecutor, listSupporters } from '@/api/system'
|
import { listExecutor, listSupporters } from '@/api/system'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { uploadToOss } from '@/utils/oss'
|
|
||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
|
import OssFileUploader from '@/components/OssFileUploader.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -161,12 +145,10 @@ const isEdit = !!route.params.projectId
|
|||||||
const projectId = route.params.projectId || null
|
const projectId = route.params.projectId || null
|
||||||
const formRef = ref(null)
|
const formRef = ref(null)
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const uploadingIdx = ref(-1)
|
|
||||||
const currentUploadN = ref(null)
|
|
||||||
const fileInputRef = ref(null)
|
const fileInputRef = ref(null)
|
||||||
|
|
||||||
function defaultRoleRow() { return { role: '', customName: '', amount: 0 } }
|
function defaultRoleRow() { return { role: '', customName: '', amount: 0 } }
|
||||||
function defaultNotice(key, label) { return { key, label, url: '', enabled: false, name: '' } }
|
function defaultNotice(key, label) { return { key, label, url: '', name: '' } }
|
||||||
|
|
||||||
// 支持方候选 (sponsor 角色)
|
// 支持方候选 (sponsor 角色)
|
||||||
const supporterOptions = ref([])
|
const supporterOptions = ref([])
|
||||||
@@ -175,19 +157,24 @@ const form = reactive({
|
|||||||
projectName: '', projectNo: '', projectForm: '',
|
projectName: '', projectNo: '', projectForm: '',
|
||||||
totalSessions: 0, totalAmount: 0, manageFee: 0,
|
totalSessions: 0, totalAmount: 0, manageFee: 0,
|
||||||
startTime: '', endTime: '',
|
startTime: '', endTime: '',
|
||||||
// 赞助公司 (TODO 后续: 下拉源应改为 biz_org,orgType='sponsor' 列表, 当前是 sys_user)
|
// 赞助方负责人 (sys_user.role_type='sponsor', 原 org_id/org_name 字段已重命名)
|
||||||
orgId: null,
|
sponsorAdminUserId: null,
|
||||||
orgName: '',
|
sponsorAdminUserName: '',
|
||||||
// 提交截止(天)
|
// 提交截止(天)
|
||||||
submitDeadlineDays: 30,
|
submitDeadlineDays: 30,
|
||||||
roleRows: Array(6).fill(0).map(() => defaultRoleRow()),
|
roleRows: [
|
||||||
// 公告附件(每个 notice 含 enabled/type/label/url/name)
|
{ role: '主席', customName: '', amount: 0 },
|
||||||
// enabled: 是否启用该公告类型(对应原型 checkbox 邀请函/支持函/通知/日程)
|
{ role: '主持', customName: '', amount: 0 },
|
||||||
|
{ role: '讲者', customName: '', amount: 0 },
|
||||||
|
{ role: '点评/评审', customName: '', amount: 0 },
|
||||||
|
{ role: '讨论', customName: '', amount: 0 }
|
||||||
|
],
|
||||||
|
// 公告附件 (key/label/url/name, 没有 url 即为空, 不参与提交)
|
||||||
notices: [
|
notices: [
|
||||||
{ key: 'invitation', label: '邀请函', enabled: false, url: '', name: '' },
|
{ key: 'invitation', label: '邀请函', url: '', name: '' },
|
||||||
{ key: 'supportLetter', label: '支持函', enabled: false, url: '', name: '' },
|
{ key: 'supportLetter', label: '支持函', url: '', name: '' },
|
||||||
{ key: 'notice', label: '通知', enabled: false, url: '', name: '' },
|
{ key: 'notice', label: '通知', url: '', name: '' },
|
||||||
{ key: 'schedule', label: '日程', enabled: false, url: '', name: '' }
|
{ key: 'schedule', label: '日程', url: '', name: '' }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -235,8 +222,8 @@ async function loadProject() {
|
|||||||
form.manageFee = p.manageFee || 0
|
form.manageFee = p.manageFee || 0
|
||||||
form.startTime = p.startTime || ''
|
form.startTime = p.startTime || ''
|
||||||
form.endTime = p.endTime || ''
|
form.endTime = p.endTime || ''
|
||||||
form.orgId = p.orgId || null
|
form.sponsorAdminUserId = p.sponsorAdminUserId || null
|
||||||
form.orgName = p.orgName || ''
|
form.sponsorAdminUserName = p.sponsorAdminUserName || ''
|
||||||
form.submitDeadlineDays = p.submitDeadlineDays || 30
|
form.submitDeadlineDays = p.submitDeadlineDays || 30
|
||||||
// 公告文件反显: 优先 publicityFiles JSON 数组, 否则兼容旧字段
|
// 公告文件反显: 优先 publicityFiles JSON 数组, 否则兼容旧字段
|
||||||
const files = []
|
const files = []
|
||||||
@@ -248,7 +235,7 @@ async function loadProject() {
|
|||||||
// 按 type 反显到对应 notice 行
|
// 按 type 反显到对应 notice 行
|
||||||
for (const n of form.notices) {
|
for (const n of form.notices) {
|
||||||
const m = files.find(f => f.type === n.key)
|
const m = files.find(f => f.type === n.key)
|
||||||
if (m) { n.url = m.url || ''; n.name = m.name || ''; n.enabled = !!m.url }
|
if (m) { n.url = m.url || ''; n.name = m.name || '' }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 兼容老数据
|
// 兼容老数据
|
||||||
@@ -262,7 +249,6 @@ async function loadProject() {
|
|||||||
for (const n of form.notices) {
|
for (const n of form.notices) {
|
||||||
n.url = noticeMap[n.key] || ''
|
n.url = noticeMap[n.key] || ''
|
||||||
n.name = n.url ? (n.url.split('/').pop() || '') : ''
|
n.name = n.url ? (n.url.split('/').pop() || '') : ''
|
||||||
n.enabled = !!n.url
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 加载已有执行方分配 (暂不实现, 留 hook)
|
// 加载已有执行方分配 (暂不实现, 留 hook)
|
||||||
@@ -284,7 +270,7 @@ async function loadSupporters(query = '') {
|
|||||||
|
|
||||||
function onSupporterPick(userId) {
|
function onSupporterPick(userId) {
|
||||||
const u = supporterOptions.value.find(x => x.userId === userId)
|
const u = supporterOptions.value.find(x => x.userId === userId)
|
||||||
if (u) form.orgName = u.userName || ''
|
if (u) form.sponsorAdminUserName = u.userName || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -294,81 +280,6 @@ onMounted(() => {
|
|||||||
loadProject()
|
loadProject()
|
||||||
loadSupporters('')
|
loadSupporters('')
|
||||||
})
|
})
|
||||||
function beforeUpload(file, n) {
|
|
||||||
// 文件大小校验: 限制 20MB
|
|
||||||
const maxSize = 20 * 1024 * 1024
|
|
||||||
if (file.size > maxSize) {
|
|
||||||
ElMessage.error('文件大小不能超过 20MB')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// 文件类型校验
|
|
||||||
const accept = ['.pdf', '.doc', '.docx', '.png', '.jpg', '.jpeg']
|
|
||||||
const ext = '.' + file.name.split('.').pop().toLowerCase()
|
|
||||||
if (!accept.includes(ext)) {
|
|
||||||
ElMessage.error('仅支持 PDF/Word/图片格式')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
async function httpUpload(opts, n, idx) {
|
|
||||||
const file = opts.file
|
|
||||||
uploadingIdx.value = idx
|
|
||||||
currentUploadN.value = n
|
|
||||||
const dir = 'ry8080/project/' + n.key + '/'
|
|
||||||
try {
|
|
||||||
const url = await uploadToOss(file, dir)
|
|
||||||
n.url = url
|
|
||||||
n.name = file.name
|
|
||||||
n.enabled = true
|
|
||||||
ElMessage.success(`${n.label} 上传成功`)
|
|
||||||
} catch (err) {
|
|
||||||
ElMessage.error(`上传失败: ${err.message || err}`)
|
|
||||||
n.url = ''
|
|
||||||
n.name = ''
|
|
||||||
} finally {
|
|
||||||
uploadingIdx.value = -1
|
|
||||||
currentUploadN.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// el-upload 内置 file-list 需要从 n.url 转换成 {name, url} 对象
|
|
||||||
function getFileList(n) {
|
|
||||||
if (!n.url) return []
|
|
||||||
// 从 url 截取文件名
|
|
||||||
const url = n.url
|
|
||||||
const name = url.split('/').pop() || `${n.label}.pdf`
|
|
||||||
return [{ name, url }]
|
|
||||||
}
|
|
||||||
|
|
||||||
function onPreview(file) {
|
|
||||||
if (file.url) window.open(file.url, '_blank')
|
|
||||||
}
|
|
||||||
|
|
||||||
function onRemove(n, file) {
|
|
||||||
n.url = ''
|
|
||||||
}
|
|
||||||
function previewFile(n) {
|
|
||||||
if (n.url) window.open(n.url, '_blank')
|
|
||||||
}
|
|
||||||
function downloadFile(n) {
|
|
||||||
if (!n.url) return
|
|
||||||
// 触发浏览器下载
|
|
||||||
const a = document.createElement('a')
|
|
||||||
a.href = n.url
|
|
||||||
a.download = n.name || (n.label + '.pdf')
|
|
||||||
a.target = '_blank'
|
|
||||||
document.body.appendChild(a)
|
|
||||||
a.click()
|
|
||||||
document.body.removeChild(a)
|
|
||||||
}
|
|
||||||
function removeNotice(idx) {
|
|
||||||
const n = form.notices[idx]
|
|
||||||
if (!n) return
|
|
||||||
ElMessageBox.confirm(`确定删除"${n.label}"附件?`, '提示', { type: 'warning' })
|
|
||||||
.then(() => { n.url = ''; ElMessage.success('已删除') })
|
|
||||||
.catch(() => {})
|
|
||||||
}
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
// 带 _t 强制 Projects.vue watch 触发 reload, 即便路由一样也重载
|
// 带 _t 强制 Projects.vue watch 触发 reload, 即便路由一样也重载
|
||||||
router.push({ path: '/manager/projects', query: { _t: Date.now() } })
|
router.push({ path: '/manager/projects', query: { _t: Date.now() } })
|
||||||
@@ -390,8 +301,8 @@ async function submit(mode = 'save') {
|
|||||||
manageFee: form.manageFee,
|
manageFee: form.manageFee,
|
||||||
startTime: form.startTime,
|
startTime: form.startTime,
|
||||||
endTime: form.endTime,
|
endTime: form.endTime,
|
||||||
orgId: form.orgId,
|
sponsorAdminUserId: form.sponsorAdminUserId,
|
||||||
orgName: form.orgName,
|
sponsorAdminUserName: form.sponsorAdminUserName,
|
||||||
submitDeadlineDays: form.submitDeadlineDays
|
submitDeadlineDays: form.submitDeadlineDays
|
||||||
}
|
}
|
||||||
// 公告文件以 publicityFiles JSON 数组存储 (替代旧 invitationUrl/supportLetterUrl/noticeUrl/scheduleUrl)
|
// 公告文件以 publicityFiles JSON 数组存储 (替代旧 invitationUrl/supportLetterUrl/noticeUrl/scheduleUrl)
|
||||||
@@ -473,30 +384,22 @@ async function submit(mode = 'save') {
|
|||||||
.role-custom { flex: 1; max-width: 180px; }
|
.role-custom { flex: 1; max-width: 180px; }
|
||||||
.role-amount { width: 180px; }
|
.role-amount { width: 180px; }
|
||||||
.row-actions { display: flex; gap: 8px; }
|
.row-actions { display: flex; gap: 8px; }
|
||||||
.notice-list { display: flex; flex-direction: column; gap: 12px; }
|
.notice-list { display: flex; flex-direction: column; gap: 10px; }
|
||||||
.notice-row { display: flex; align-items: stretch; gap: 16px; margin-bottom: 16px; }
|
.notice-row { display: flex; align-items: stretch; gap: 12px; margin-bottom: 12px; }
|
||||||
.notice-label {
|
.notice-label {
|
||||||
width: 100px;
|
width: 90px;
|
||||||
display: flex; align-items: center; justify-content: flex-end;
|
display: flex; align-items: center; justify-content: flex-end;
|
||||||
color: #606266; font-size: 14px;
|
color: #606266; font-size: 14px;
|
||||||
}
|
}
|
||||||
.notice-uploader { flex: 1; }
|
.notice-uploader { flex: 1; }
|
||||||
.notice-uploader :deep(.el-upload) { width: 100%; }
|
/* 紧凑版 (覆盖 OssFileUploader 默认尺寸) */
|
||||||
.notice-uploader :deep(.el-upload-dragger) {
|
.notice-uploader :deep(.ht-file-upload) { padding: 6px 12px; min-height: 40px; gap: 8px; }
|
||||||
padding: 12px 16px;
|
.notice-uploader :deep(.placeholder-text) { font-size: 13px; }
|
||||||
display: flex; align-items: center; gap: 12px;
|
.notice-uploader :deep(.placeholder-hint) { font-size: 11px; }
|
||||||
}
|
.notice-uploader :deep(.file-name) { font-size: 13px; }
|
||||||
.notice-uploader :deep(.el-upload-dragger .el-upload__text) {
|
.notice-uploader :deep(.file-type) { font-size: 11px; }
|
||||||
font-size: 13px; color: #606266;
|
.notice-uploader :deep(.file-icon) { font-size: 18px; }
|
||||||
}
|
.notice-uploader :deep(.file-icon svg) { width: 18px; height: 18px; }
|
||||||
.notice-uploader :deep(.el-upload-dragger .el-upload__tip) {
|
|
||||||
font-size: 12px; color: #909399; margin-top: 2px;
|
|
||||||
}
|
|
||||||
.notice-uploader :deep(.el-upload__tip) { margin-top: 0; }
|
|
||||||
.notice-uploader :deep(.el-upload-list) { margin: 0; }
|
|
||||||
.notice-actions { display: flex; flex-direction: column; justify-content: center; gap: 4px; min-width: 70px; }
|
|
||||||
.notice-actions .el-button { padding: 2px 6px; font-size: 12px; }
|
|
||||||
.notice-disabled { flex: 1; color: #c0c4cc; font-size: 13px; padding: 8px 0; }
|
|
||||||
|
|
||||||
/* 底部按钮 */
|
/* 底部按钮 */
|
||||||
.form-actions { display: flex; justify-content: center; gap: 16px; padding: 20px 0 4px; border-top: 1px solid #f0f0f0; margin-top: 8px; }
|
.form-actions { display: flex; justify-content: center; gap: 16px; padding: 20px 0 4px; border-top: 1px solid #f0f0f0; margin-top: 8px; }
|
||||||
|
|||||||
@@ -0,0 +1,374 @@
|
|||||||
|
<template>
|
||||||
|
<div class="detail-page">
|
||||||
|
<header class="top-nav" :class="topNavClass">
|
||||||
|
<a class="logo" title="返回首页" @click.prevent="goHome">
|
||||||
|
<div class="logo-icon">医</div>
|
||||||
|
<div class="logo-text">
|
||||||
|
<span class="logo-title">北京整合医学学会</span>
|
||||||
|
<span class="logo-subtitle">Beijing Association of Holistic Integrative Medicine</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<ul class="nav-list">
|
||||||
|
<li class="nav-item"><a class="nav-link" @click.prevent="goHome">年度项目规划</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" @click.prevent="goPublicity">项目公示</a></li>
|
||||||
|
</ul>
|
||||||
|
<div class="top-tools">
|
||||||
|
<template v-if="!loggedIn">
|
||||||
|
<span class="login-btn" @click="goLogin">登录</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<el-dropdown trigger="click" @command="onUserCmd">
|
||||||
|
<a class="user-link" @click.prevent>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
||||||
|
<circle cx="12" cy="7" r="4"/>
|
||||||
|
</svg>
|
||||||
|
<span>{{ userName }}</span>
|
||||||
|
</a>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="account">我的主页</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<article class="article-card" v-loading="loading">
|
||||||
|
<header class="article-head">
|
||||||
|
<h1 class="article-title">{{ article.title || (type === 'agreement' ? '用户服务协议' : '隐私政策') }}</h1>
|
||||||
|
<div class="article-meta" v-if="article.updateTime">最后更新: {{ fmt(article.updateTime) }}</div>
|
||||||
|
</header>
|
||||||
|
<div class="article-body" v-html="article.content || '<p class=\'empty\'>暂无内容</p>'" />
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container">
|
||||||
|
<div class="footer-main">
|
||||||
|
<div class="footer-brand">
|
||||||
|
<div class="brand-row">
|
||||||
|
<div class="footer-logo-icon">医</div>
|
||||||
|
<div>
|
||||||
|
<div class="footer-brand-name">北京整合医学学会</div>
|
||||||
|
<div class="footer-brand-en">Beijing Association of Holistic Integrative Medicine</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="footer-desc">依托"健康中国2030"战略,整合医学资源,推动健康科普与公益事业,助力全民健康素养提升,共建共享健康中国。</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer-col">
|
||||||
|
<h4>快速导航</h4>
|
||||||
|
<a @click.prevent="goHome">首页</a>
|
||||||
|
<a @click.prevent="goHome">年度项目规划</a>
|
||||||
|
<a @click.prevent="goPublicity">项目公示</a>
|
||||||
|
</div>
|
||||||
|
<div class="footer-col">
|
||||||
|
<h4>学会项目</h4>
|
||||||
|
<a>百姓巡常行</a>
|
||||||
|
<a>专病联盟暨全国专家智库</a>
|
||||||
|
<a>乡村幸福安康</a>
|
||||||
|
<a>临床科研资助计划</a>
|
||||||
|
</div>
|
||||||
|
<div class="footer-col">
|
||||||
|
<h4>联系方式</h4>
|
||||||
|
<p>电话: 010-82089470</p>
|
||||||
|
<p>邮箱: contactus@bahim.org.cn</p>
|
||||||
|
<p>地址: 北京市海淀区知春路1号学院国际大厦908-2室</p>
|
||||||
|
<p>邮编: 100083</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer-qr">
|
||||||
|
<div class="qr-image">
|
||||||
|
<svg width="52" height="52" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M3 11h8V3H3v8zm2-6h4v4H5V5zm8-2v8h8V3h-8zm6 6h-4V5h4v4zM3 21h8v-8H3v8zm2-6h4v4H5v-4zm13-2h-2v2h2v-2zm-2 2h-2v2h2v-2zm2 2h-2v2h2v-2zm2-2h-2v2h2v-2zm-2-2h-2v2h2v-2zm-2-2h-2v2h2v-2zm-2-2h-2v2h2v-2zm-2-2h-2v2h2v-2zm0 0h-2v2h2v-2z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="qr-label">公众号二维码</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer-bottom">
|
||||||
|
<span>© 北京整合医学学会 BAHIM</span>
|
||||||
|
<span>京ICP备2020035479号-1 京公网安备11010802034820</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
import { logout as logoutApi } from '@/api/auth'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const isScrolled = ref(false)
|
||||||
|
const topNavClass = computed(() => isScrolled.value ? 'is-scrolled' : '')
|
||||||
|
const loggedIn = computed(() => !!userStore.token)
|
||||||
|
const userName = computed(() => userStore.user?.userName || '用户')
|
||||||
|
|
||||||
|
const article = ref({})
|
||||||
|
const loading = ref(false)
|
||||||
|
const type = ref('')
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
const t = route.params.type
|
||||||
|
type.value = t
|
||||||
|
if (!t) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const r = await request({ url: `/business/public/article/${t}`, method: 'get' })
|
||||||
|
article.value = (r.data && r.data.data) || r.data || {}
|
||||||
|
} catch (e) {
|
||||||
|
article.value = {}
|
||||||
|
} finally { loading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmt(d) {
|
||||||
|
if (!d) return ''
|
||||||
|
const date = new Date(d)
|
||||||
|
const y = date.getFullYear()
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
return `${y}-${m}-${day}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function goHome() { router.push('/') }
|
||||||
|
function goPublicity() { router.push('/publicity') }
|
||||||
|
function goLogin() { router.push('/login') }
|
||||||
|
async function goLogout() { try { await logoutApi() } catch {}; userStore.logout(); router.replace('/login') }
|
||||||
|
async function onUserCmd(cmd) {
|
||||||
|
if (cmd === 'logout') return goLogout()
|
||||||
|
if (cmd === 'account') {
|
||||||
|
const r = (userStore.user?.role || '')
|
||||||
|
const map = { admin: '/leader/home', leader: '/leader/home', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/meetings', sponsor: '/sponsor/home' }
|
||||||
|
router.push(map[r] || '/leader/home')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleScroll() { isScrolled.value = window.scrollY > 10 }
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||||
|
load()
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('scroll', handleScroll)
|
||||||
|
})
|
||||||
|
watch(() => route.params.type, load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* ========== 基础 ========== */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
color: #1f2937;
|
||||||
|
background: #f5f6f8;
|
||||||
|
min-width: 1354px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
a { color: inherit; text-decoration: none; }
|
||||||
|
|
||||||
|
/* ========== 顶部导航 ========== */
|
||||||
|
.top-nav {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
height: 72px;
|
||||||
|
padding: 0 60px;
|
||||||
|
background: var(--brand-primary);
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
transition: box-shadow 0.3s;
|
||||||
|
}
|
||||||
|
.top-nav.is-scrolled { box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25); }
|
||||||
|
|
||||||
|
.logo { display: flex; align-items: center; gap: 12px; }
|
||||||
|
.logo-icon {
|
||||||
|
width: 36px; height: 36px;
|
||||||
|
background: #fff;
|
||||||
|
color: var(--brand-primary);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
font-size: 18px; font-weight: 600;
|
||||||
|
}
|
||||||
|
.logo-text { display: flex; flex-direction: column; line-height: 1.2; }
|
||||||
|
.logo-title { font-size: 16px; font-weight: 600; color: #fff; letter-spacing: 0.5px; }
|
||||||
|
.logo-subtitle { font-size: 11px; color: rgba(255, 255, 255, 0.6); margin-top: 2px; letter-spacing: 0.3px; }
|
||||||
|
|
||||||
|
.nav-list {
|
||||||
|
flex: 1; display: flex; align-items: center; justify-content: center;
|
||||||
|
gap: 36px; list-style: none;
|
||||||
|
}
|
||||||
|
.nav-item { position: relative; height: 72px; display: flex; align-items: center; }
|
||||||
|
.nav-link {
|
||||||
|
font-size: 15px; font-weight: 500; color: rgba(255, 255, 255, 0.85);
|
||||||
|
cursor: pointer; transition: color 0.25s; position: relative;
|
||||||
|
height: 72px; display: flex; align-items: center;
|
||||||
|
}
|
||||||
|
.nav-link::after {
|
||||||
|
content: ''; position: absolute; left: 50%; bottom: 0;
|
||||||
|
width: 0; height: 2px; background: #fff;
|
||||||
|
transform: translateX(-50%); transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
.nav-item:hover .nav-link, .nav-link.active { color: #fff; }
|
||||||
|
.nav-item:hover .nav-link::after, .nav-link.active::after { width: 100%; }
|
||||||
|
|
||||||
|
.top-tools { display: flex; align-items: center; gap: 16px; }
|
||||||
|
.login-btn {
|
||||||
|
padding: 7px 20px;
|
||||||
|
background: #fff;
|
||||||
|
color: var(--brand-primary);
|
||||||
|
font-size: 13px; font-weight: 500;
|
||||||
|
cursor: pointer; transition: background 0.2s; letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
.login-btn:hover { background: #f3f4f6; }
|
||||||
|
.user-link {
|
||||||
|
display: inline-flex; align-items: center; gap: 6px;
|
||||||
|
color: #fff; font-size: 14px; cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 主体 ========== */
|
||||||
|
.container {
|
||||||
|
max-width: 1354px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 40px 60px 60px; /* 顶部加 40px 留白, 拉开与 sticky navcat 的距离 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link {
|
||||||
|
display: inline-flex; align-items: center; gap: 6px;
|
||||||
|
margin: 28px 0 20px;
|
||||||
|
font-size: 13px; color: #6b7280;
|
||||||
|
cursor: pointer; transition: color 0.2s;
|
||||||
|
}
|
||||||
|
.back-link:hover { color: var(--brand-primary); }
|
||||||
|
|
||||||
|
/* ========== 文章卡片 ========== */
|
||||||
|
.article-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||||
|
padding: 56px 80px 64px;
|
||||||
|
min-height: 480px;
|
||||||
|
}
|
||||||
|
.article-head {
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 1px solid #eef0f3;
|
||||||
|
padding-bottom: 28px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
.article-title {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.article-meta {
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
.article-body {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 2;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
.article-body :deep(h1),
|
||||||
|
.article-body :deep(h2),
|
||||||
|
.article-body :deep(h3) {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
margin: 24px 0 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.article-body :deep(h1) { font-size: 22px; text-align: center; }
|
||||||
|
.article-body :deep(h2) { font-size: 18px; }
|
||||||
|
.article-body :deep(h3) { font-size: 16px; }
|
||||||
|
.article-body :deep(p) {
|
||||||
|
margin: 10px 0;
|
||||||
|
text-indent: 2em;
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
.article-body :deep(ul),
|
||||||
|
.article-body :deep(ol) {
|
||||||
|
margin: 12px 0 12px 2em;
|
||||||
|
}
|
||||||
|
.article-body :deep(li) { margin: 6px 0; }
|
||||||
|
.article-body :deep(.empty) {
|
||||||
|
text-align: center;
|
||||||
|
color: #c0c4cc;
|
||||||
|
text-indent: 0;
|
||||||
|
padding: 60px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 底部 ========== */
|
||||||
|
.footer {
|
||||||
|
background: var(--brand-primary-darker);
|
||||||
|
color: rgba(255, 255, 255, 0.65);
|
||||||
|
padding: 48px 0 0;
|
||||||
|
}
|
||||||
|
.footer-main {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.4fr 1fr 1fr 1.2fr auto;
|
||||||
|
gap: 48px;
|
||||||
|
padding-bottom: 36px;
|
||||||
|
}
|
||||||
|
.footer-brand .brand-row { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
||||||
|
.footer-logo-icon {
|
||||||
|
width: 36px; height: 36px;
|
||||||
|
background: #fff;
|
||||||
|
color: var(--brand-primary);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
font-size: 18px; font-weight: 600; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.footer-brand-name { font-size: 16px; font-weight: 600; color: #fff; letter-spacing: 1px; }
|
||||||
|
.footer-brand-en { font-size: 10px; color: rgba(255, 255, 255, 0.4); letter-spacing: 0.5px; margin-top: 2px; }
|
||||||
|
.footer-desc { font-size: 13px; line-height: 1.9; color: rgba(255, 255, 255, 0.55); }
|
||||||
|
.footer-col h4 {
|
||||||
|
font-size: 14px; font-weight: 600; color: #fff;
|
||||||
|
letter-spacing: 1px; margin-bottom: 16px; padding-bottom: 10px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.footer-col h4::after {
|
||||||
|
content: ''; position: absolute; left: 0; bottom: 0;
|
||||||
|
width: 24px; height: 2px; background: #93c5fd;
|
||||||
|
}
|
||||||
|
.footer-col a, .footer-col p {
|
||||||
|
display: block; font-size: 13px; color: rgba(255, 255, 255, 0.6);
|
||||||
|
line-height: 2.1; transition: color 0.2s;
|
||||||
|
}
|
||||||
|
.footer-col a { cursor: pointer; }
|
||||||
|
.footer-col a:hover { color: #fff; }
|
||||||
|
.footer-qr { text-align: center; }
|
||||||
|
.qr-image {
|
||||||
|
width: 100px; height: 100px;
|
||||||
|
background: #fff;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
.qr-label { font-size: 12px; color: rgba(255, 255, 255, 0.5); margin-top: 10px; }
|
||||||
|
.footer-bottom {
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
padding: 18px 0;
|
||||||
|
display: flex; justify-content: space-between;
|
||||||
|
font-size: 12px; color: rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.container { padding: 0 24px 40px; }
|
||||||
|
.article-card { padding: 32px 24px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -77,83 +77,16 @@
|
|||||||
<div class="specialty-inner">
|
<div class="specialty-inner">
|
||||||
<div class="section-title">七大专项计划</div>
|
<div class="section-title">七大专项计划</div>
|
||||||
<div class="specialty-list">
|
<div class="specialty-list">
|
||||||
<div class="specialty-card">
|
<div v-for="(p, i) in specialPlans" :key="p.id" class="specialty-card" @click="openPlan(p.id)">
|
||||||
<div class="specialty-no">01</div>
|
<div class="specialty-no">{{ String(i + 1).padStart(2, '0') }}</div>
|
||||||
<div class="specialty-body">
|
<div class="specialty-body">
|
||||||
<h3 class="specialty-title">《规范化诊疗及医疗质量提升专项计划(2026年)》</h3>
|
<h3 class="specialty-title">《{{ p.title }}》</h3>
|
||||||
<p class="specialty-desc">聚焦临床诊疗规范制定与质量提升,推动各级医疗机构诊疗标准化、同质化。</p>
|
|
||||||
<div class="specialty-action">
|
<div class="specialty-action">
|
||||||
<span class="specialty-link">查看详情</span>
|
<span class="specialty-link">查看详情</span>
|
||||||
<span class="specialty-link">项目提案</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="specialty-card">
|
|
||||||
<div class="specialty-no">02</div>
|
|
||||||
<div class="specialty-body">
|
|
||||||
<h3 class="specialty-title">《科研创新专项行动计划(2026-2030年)》</h3>
|
|
||||||
<p class="specialty-desc">支持医学科研创新,推动整合医学研究成果转化与产业化应用,构建协同创新生态。</p>
|
|
||||||
<div class="specialty-action">
|
|
||||||
<span class="specialty-link">查看详情</span>
|
|
||||||
<span class="specialty-link">项目提案</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="specialty-card">
|
|
||||||
<div class="specialty-no">03</div>
|
|
||||||
<div class="specialty-body">
|
|
||||||
<h3 class="specialty-title">《医疗卫生人才培育专项行动计划(2026-2030年)》</h3>
|
|
||||||
<p class="specialty-desc">建立多层次医学人才培养体系,重点加强基层医疗人才与跨学科复合型人才建设。</p>
|
|
||||||
<div class="specialty-action">
|
|
||||||
<span class="specialty-link">查看详情</span>
|
|
||||||
<span class="specialty-link">项目提案</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="specialty-card">
|
|
||||||
<div class="specialty-no">04</div>
|
|
||||||
<div class="specialty-body">
|
|
||||||
<h3 class="specialty-title">《医院管理及高质量发展促进专项计划(2026-2030年)》</h3>
|
|
||||||
<p class="specialty-desc">推动医院管理现代化,提升运营效率与服务水平,助力公立医院高质量发展。</p>
|
|
||||||
<div class="specialty-action">
|
|
||||||
<span class="specialty-link">查看详情</span>
|
|
||||||
<span class="specialty-link">项目提案</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="specialty-card">
|
|
||||||
<div class="specialty-no">05</div>
|
|
||||||
<div class="specialty-body">
|
|
||||||
<h3 class="specialty-title">《社会公益与可及性提升专项计划(2026-2030年)》</h3>
|
|
||||||
<p class="specialty-desc">聚焦医疗服务的可及性与公平性,开展公益救助、基层帮扶与健康科普活动。</p>
|
|
||||||
<div class="specialty-action">
|
|
||||||
<span class="specialty-link">查看详情</span>
|
|
||||||
<span class="specialty-link">项目提案</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="specialty-card">
|
|
||||||
<div class="specialty-no">06</div>
|
|
||||||
<div class="specialty-body">
|
|
||||||
<h3 class="specialty-title">《政学协作综合项目专项计划(2026-2030年)》</h3>
|
|
||||||
<p class="specialty-desc">搭建政府与学术机构协同平台,推动政策研究与决策支持,促进政产学研用深度融合。</p>
|
|
||||||
<div class="specialty-action">
|
|
||||||
<span class="specialty-link">查看详情</span>
|
|
||||||
<span class="specialty-link">项目提案</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="specialty-card">
|
|
||||||
<div class="specialty-no">07</div>
|
|
||||||
<div class="specialty-body">
|
|
||||||
<h3 class="specialty-title">《组织建设与内部治理专项计划(2026-2030年)》</h3>
|
|
||||||
<p class="specialty-desc">完善学会组织架构,健全内部治理机制,提升规范化、专业化、信息化管理水平。</p>
|
|
||||||
<div class="specialty-action">
|
|
||||||
<span class="specialty-link">查看详情</span>
|
|
||||||
<span class="specialty-link">项目提案</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="!specialPlans.length" class="specialty-empty">暂无专项计划</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -215,11 +148,26 @@ import { useRouter } from 'vue-router'
|
|||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { useUserStore } from '@/store/user'
|
import { useUserStore } from '@/store/user'
|
||||||
import { logout as logoutApi } from '@/api/auth'
|
import { logout as logoutApi } from '@/api/auth'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
const isScrolled = ref(false)
|
const isScrolled = ref(false)
|
||||||
|
const specialPlans = ref([])
|
||||||
|
|
||||||
|
async function loadSpecialPlans() {
|
||||||
|
try {
|
||||||
|
const r = await request({ url: '/business/public/specialPlan/list', method: 'get' })
|
||||||
|
specialPlans.value = (r.data && r.data.data) || r.data || []
|
||||||
|
} catch (e) {
|
||||||
|
specialPlans.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPlan(id) {
|
||||||
|
window.open(`/#/special-plan/${id}`, '_blank')
|
||||||
|
}
|
||||||
const topNavClass = computed(() => isScrolled.value ? 'is-scrolled' : '')
|
const topNavClass = computed(() => isScrolled.value ? 'is-scrolled' : '')
|
||||||
const loggedIn = computed(() => !!userStore.token)
|
const loggedIn = computed(() => !!userStore.token)
|
||||||
const userName = computed(() => userStore.user?.userName || '用户')
|
const userName = computed(() => userStore.user?.userName || '用户')
|
||||||
@@ -266,12 +214,6 @@ const planSvg = `<svg viewBox="0 150 1200 370" xmlns="http://www.w3.org/2000/svg
|
|||||||
<g transform="translate(904 362)"><polygon points="-30,0 -15,-26 15,-26 30,0 15,26 -15,26" fill="url(#hexGrad)"/><text y="6" font-size="19">07</text></g>
|
<g transform="translate(904 362)"><polygon points="-30,0 -15,-26 15,-26 30,0 15,26 -15,26" fill="url(#hexGrad)"/><text y="6" font-size="19">07</text></g>
|
||||||
<g transform="translate(986 468)"><polygon points="-34,0 -17,-29 17,-29 34,0 17,29 -17,29" fill="url(#hexGrad)"/><text y="7" font-size="22">08</text></g>
|
<g transform="translate(986 468)"><polygon points="-34,0 -17,-29 17,-29 34,0 17,29 -17,29" fill="url(#hexGrad)"/><text y="7" font-size="22">08</text></g>
|
||||||
</g>
|
</g>
|
||||||
<g transform="translate(600 360)">
|
|
||||||
<circle cx="0" cy="0" r="40" fill="#fff" stroke="#e0e7ff" stroke-width="2"/>
|
|
||||||
<circle cx="0" cy="0" r="30" fill="url(#centerGrad)"/>
|
|
||||||
<rect x="-16" y="-5" width="32" height="10" fill="#fff"/>
|
|
||||||
<rect x="-5" y="-16" width="10" height="32" fill="#fff"/>
|
|
||||||
</g>
|
|
||||||
<text x="600" y="430" fill="var(--brand-primary)" text-anchor="middle" font-family="Microsoft YaHei, sans-serif" font-size="32" font-weight="bold" letter-spacing="6">2025-2030年</text>
|
<text x="600" y="430" fill="var(--brand-primary)" text-anchor="middle" font-family="Microsoft YaHei, sans-serif" font-size="32" font-weight="bold" letter-spacing="6">2025-2030年</text>
|
||||||
<line x1="540" y1="447" x2="600" y2="447" stroke="var(--brand-primary)" stroke-width="1.5" opacity="0.4"/>
|
<line x1="540" y1="447" x2="600" y2="447" stroke="var(--brand-primary)" stroke-width="1.5" opacity="0.4"/>
|
||||||
<line x1="600" y1="447" x2="660" y2="447" stroke="var(--brand-primary)" stroke-width="1.5" opacity="0.4"/>
|
<line x1="600" y1="447" x2="660" y2="447" stroke="var(--brand-primary)" stroke-width="1.5" opacity="0.4"/>
|
||||||
@@ -291,7 +233,10 @@ function handleScroll() {
|
|||||||
isScrolled.value = window.scrollY > 10
|
isScrolled.value = window.scrollY > 10
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => window.addEventListener('scroll', handleScroll, { passive: true }))
|
onMounted(() => {
|
||||||
|
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||||
|
loadSpecialPlans()
|
||||||
|
})
|
||||||
onBeforeUnmount(() => window.removeEventListener('scroll', handleScroll))
|
onBeforeUnmount(() => window.removeEventListener('scroll', handleScroll))
|
||||||
|
|
||||||
function onHome() { isScrolled.value = false }
|
function onHome() { isScrolled.value = false }
|
||||||
@@ -632,8 +577,16 @@ a { color: inherit; text-decoration: none; }
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
transition: border-color 0.2s, transform 0.2s;
|
transition: border-color 0.2s, transform 0.2s;
|
||||||
}
|
}
|
||||||
|
.specialty-empty {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
text-align: center;
|
||||||
|
color: #9ca3af;
|
||||||
|
padding: 60px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.specialty-card:hover {
|
.specialty-card:hover {
|
||||||
border-color: var(--brand-primary);
|
border-color: var(--brand-primary);
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
<span class="notice-item-no">{{ String((currentPage - 1) * PAGE_SIZE + i + 1).padStart(3, '0') }}</span>
|
<span class="notice-item-no">{{ String((currentPage - 1) * PAGE_SIZE + i + 1).padStart(3, '0') }}</span>
|
||||||
<span class="notice-date">{{ fmtDate(n.date) }}</span>
|
<span class="notice-date">{{ fmtDate(n.date) }}</span>
|
||||||
<span class="notice-title">
|
<span class="notice-title">
|
||||||
<span v-if="n.projectName" style="color:#909399;margin-right:8px;font-size:13px">【{{ n.projectName }}】</span>
|
<span v-if="n.projectNo" style="color:#909399;margin-right:8px;font-size:13px">【{{ n.projectNo }}】</span>
|
||||||
{{ n.title }}
|
{{ n.title }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -172,8 +172,9 @@ async function load() {
|
|||||||
annId: r.projectId,
|
annId: r.projectId,
|
||||||
date: r.publishTime,
|
date: r.publishTime,
|
||||||
type,
|
type,
|
||||||
title: r.projectName,
|
projectNo: r.projectNo || '',
|
||||||
projectName: r.projectName,
|
projectName: r.projectName,
|
||||||
|
title: r.projectName,
|
||||||
fileUrl: urlMap[type] || r.invitationUrl || r.supportLetterUrl || r.publishUrl || ''
|
fileUrl: urlMap[type] || r.invitationUrl || r.supportLetterUrl || r.publishUrl || ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -156,28 +156,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<!-- 分享二维码弹窗 -->
|
<!-- 分享二维码弹窗 (el-dialog 自带右上角 X 关闭按钮 + Esc + 遮罩点击) -->
|
||||||
<div class="modal-overlay" :class="showQr ? 'active' : ''" @click.self="showQr = false">
|
<el-dialog v-model="showQr" title="分享本页" width="420px" align-center destroy-on-close>
|
||||||
<div class="modal" @click.stop>
|
<div class="modal-qr-wrap">
|
||||||
<h3 class="modal-title">分享本页</h3>
|
<span class="qr-corner qr-corner-tl"></span>
|
||||||
<div class="modal-qr-wrap">
|
<span class="qr-corner qr-corner-tr"></span>
|
||||||
<span class="qr-corner qr-corner-tl"></span>
|
<span class="qr-corner qr-corner-bl"></span>
|
||||||
<span class="qr-corner qr-corner-tr"></span>
|
<span class="qr-corner qr-corner-br"></span>
|
||||||
<span class="qr-corner qr-corner-bl"></span>
|
<div class="modal-qr">
|
||||||
<span class="qr-corner qr-corner-br"></span>
|
<img v-if="qrUrl" :src="qrUrl" alt="分享二维码" class="qr-img" />
|
||||||
<div class="modal-qr">
|
<span v-else class="qr-loading">
|
||||||
<img v-if="qrUrl" :src="qrUrl" alt="分享二维码" class="qr-img" />
|
<span class="qr-dot"></span><span class="qr-dot"></span><span class="qr-dot"></span>
|
||||||
<span v-else class="qr-loading">
|
生成中...
|
||||||
<span class="qr-dot"></span><span class="qr-dot"></span><span class="qr-dot"></span>
|
</span>
|
||||||
生成中...
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="qr-brand">北京整合医学学会</div>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="modal-tip">扫码查看「{{ ann?.projectName || '项目公示' }}」公示详情</p>
|
<div class="qr-brand">北京整合医学学会</div>
|
||||||
<button class="modal-close" @click="showQr = false">关 闭</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<p class="modal-tip">扫码查看「{{ ann?.projectName || '项目公示' }}」公示详情</p>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" :disabled="!qrUrl" @click="onDownloadQr">下载二维码</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -388,6 +387,17 @@ async function onSignup() {
|
|||||||
function onKeydown(e) { if (e.key === 'Escape') showQr.value = false }
|
function onKeydown(e) { if (e.key === 'Escape') showQr.value = false }
|
||||||
function handleScroll() { isScrolled.value = window.scrollY > 10 }
|
function handleScroll() { isScrolled.value = window.scrollY > 10 }
|
||||||
|
|
||||||
|
// 下载二维码: qrUrl 是 QRCode.toDataURL 生成的 data:image/png;base64,... 直接 <a download> 即可
|
||||||
|
function onDownloadQr() {
|
||||||
|
if (!qrUrl.value) return
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = qrUrl.value
|
||||||
|
a.download = `${(ann.value?.projectName || '公示')}_分享二维码.png`
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
document.body.removeChild(a)
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('scroll', handleScroll, { passive: true })
|
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||||
window.addEventListener('keydown', onKeydown)
|
window.addEventListener('keydown', onKeydown)
|
||||||
@@ -981,39 +991,11 @@ a { color: inherit; text-decoration: none; }
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========== 二维码弹窗 ========== */
|
/* ========== 二维码弹窗 (el-dialog 自带标题/X/footer, 这里只写内部 QR 样式) ========== */
|
||||||
.modal-overlay {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
display: none;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 2000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-overlay.active { display: flex; }
|
|
||||||
|
|
||||||
.modal {
|
|
||||||
background: #fff;
|
|
||||||
padding: 32px 36px;
|
|
||||||
width: 320px;
|
|
||||||
text-align: center;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1f2937;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-qr-wrap {
|
.modal-qr-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 220px;
|
width: 220px;
|
||||||
margin: 0 auto 20px;
|
margin: 0 auto 16px;
|
||||||
padding: 12px 12px 8px;
|
padding: 12px 12px 8px;
|
||||||
background: linear-gradient(135deg, #ffffff 0%, #f7faff 100%);
|
background: linear-gradient(135deg, #ffffff 0%, #f7faff 100%);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@@ -1076,22 +1058,10 @@ a { color: inherit; text-decoration: none; }
|
|||||||
.modal-tip {
|
.modal-tip {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
margin-bottom: 20px;
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-close {
|
|
||||||
width: 100%;
|
|
||||||
height: 38px;
|
|
||||||
background: var(--brand-primary);
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
font-size: 14px;
|
|
||||||
cursor: pointer;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-close:hover { background: var(--brand-primary-deep); }
|
|
||||||
|
|
||||||
/* ========== 底部 ========== */
|
/* ========== 底部 ========== */
|
||||||
.footer {
|
.footer {
|
||||||
background: var(--brand-primary-darker);
|
background: var(--brand-primary-darker);
|
||||||
|
|||||||
@@ -0,0 +1,341 @@
|
|||||||
|
<template>
|
||||||
|
<div class="detail-page">
|
||||||
|
<header class="top-nav" :class="topNavClass">
|
||||||
|
<a class="logo" title="返回首页" @click.prevent="goHome">
|
||||||
|
<div class="logo-icon">医</div>
|
||||||
|
<div class="logo-text">
|
||||||
|
<span class="logo-title">北京整合医学学会</span>
|
||||||
|
<span class="logo-subtitle">Beijing Association of Holistic Integrative Medicine</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<ul class="nav-list">
|
||||||
|
<li class="nav-item"><a class="nav-link" @click.prevent="goHome">年度项目规划</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" @click.prevent="goPublicity">项目公示</a></li>
|
||||||
|
</ul>
|
||||||
|
<div class="top-tools">
|
||||||
|
<template v-if="!loggedIn">
|
||||||
|
<span class="login-btn" @click="goLogin">登录</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<el-dropdown trigger="click" @command="onUserCmd">
|
||||||
|
<a class="user-link" @click.prevent>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
||||||
|
<circle cx="12" cy="7" r="4"/>
|
||||||
|
</svg>
|
||||||
|
<span>{{ userName }}</span>
|
||||||
|
</a>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="account">我的主页</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<article class="plan-card" v-loading="loading">
|
||||||
|
<header class="plan-head">
|
||||||
|
<h1 class="plan-title">{{ plan.title || '专项计划' }}</h1>
|
||||||
|
<div class="plan-meta" v-if="plan.updateTime">最后更新: {{ fmt(plan.updateTime) }}</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 富文本内容 -->
|
||||||
|
<div v-if="!plan.contentType || plan.contentType === 'rich'"
|
||||||
|
class="plan-body plan-body-rich"
|
||||||
|
v-html="plan.content || '<p class=\'empty\'>暂无内容</p>'" />
|
||||||
|
|
||||||
|
<!-- 上传文件: PDF / PNG -->
|
||||||
|
<div v-else class="plan-body plan-body-file">
|
||||||
|
<iframe v-if="isPdf(plan.fileUrl)"
|
||||||
|
:src="proxyUrl(plan.fileUrl)"
|
||||||
|
class="content-frame"
|
||||||
|
frameborder="0"></iframe>
|
||||||
|
<img v-else-if="isImg(plan.fileUrl)"
|
||||||
|
:src="proxyUrl(plan.fileUrl)"
|
||||||
|
class="content-img"
|
||||||
|
alt="专项计划文件" />
|
||||||
|
<div v-else class="content-empty">
|
||||||
|
<el-icon size="64"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zM6 20V4h7v5h5v11H6z"/></svg></el-icon>
|
||||||
|
<p>暂无可查看的文件</p>
|
||||||
|
<p class="hint">请上传 PDF 或 PNG 格式</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container">
|
||||||
|
<div class="footer-main">
|
||||||
|
<div class="footer-brand">
|
||||||
|
<div class="brand-row">
|
||||||
|
<div class="footer-logo-icon">医</div>
|
||||||
|
<div>
|
||||||
|
<div class="footer-brand-name">北京整合医学学会</div>
|
||||||
|
<div class="footer-brand-en">Beijing Association of Holistic Integrative Medicine</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="footer-desc">依托"健康中国2030"战略,整合医学资源,推动健康科普与公益事业,助力全民健康素养提升,共建共享健康中国。</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer-col">
|
||||||
|
<h4>快速导航</h4>
|
||||||
|
<a @click.prevent="goHome">首页</a>
|
||||||
|
<a @click.prevent="goHome">年度项目规划</a>
|
||||||
|
<a @click.prevent="goPublicity">项目公示</a>
|
||||||
|
</div>
|
||||||
|
<div class="footer-col">
|
||||||
|
<h4>学会项目</h4>
|
||||||
|
<a>百姓巡常行</a>
|
||||||
|
<a>专病联盟暨全国专家智库</a>
|
||||||
|
<a>乡村幸福安康</a>
|
||||||
|
<a>临床科研资助计划</a>
|
||||||
|
</div>
|
||||||
|
<div class="footer-col">
|
||||||
|
<h4>联系方式</h4>
|
||||||
|
<p>电话: 010-82089470</p>
|
||||||
|
<p>邮箱: contactus@bahim.org.cn</p>
|
||||||
|
<p>地址: 北京市海淀区知春路1号学院国际大厦908-2室</p>
|
||||||
|
<p>邮编: 100083</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer-qr">
|
||||||
|
<div class="qr-image">
|
||||||
|
<svg width="52" height="52" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M3 11h8V3H3v8zm2-6h4v4H5V5zm8-2v8h8V3h-8zm6 6h-4V5h4v4zM3 21h8v-8H3v8zm2-6h4v4H5v-4zm13-2h-2v2h2v-2zm-2 2h-2v2h2v-2zm2 2h-2v2h2v-2zm2-2h-2v2h2v-2zm-2-2h-2v2h2v-2zm-2-2h-2v2h2v-2zm-2-2h-2v2h2v-2zm-2-2h-2v2h2v-2zm0 0h-2v2h2v-2z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="qr-label">公众号二维码</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer-bottom">
|
||||||
|
<span>© 北京整合医学学会 BAHIM</span>
|
||||||
|
<span>京ICP备2020035479号-1 京公网安备11010802034820</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
import { logout as logoutApi } from '@/api/auth'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const isScrolled = ref(false)
|
||||||
|
const topNavClass = computed(() => isScrolled.value ? 'is-scrolled' : '')
|
||||||
|
const loggedIn = computed(() => !!userStore.token)
|
||||||
|
const userName = computed(() => userStore.user?.userName || '用户')
|
||||||
|
|
||||||
|
const plan = ref({})
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
const id = route.params.id
|
||||||
|
if (!id) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const r = await request({ url: `/business/public/specialPlan/${id}`, method: 'get' })
|
||||||
|
plan.value = (r.data && r.data.data) || r.data || {}
|
||||||
|
} catch (e) {
|
||||||
|
plan.value = {}
|
||||||
|
} finally { loading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmt(d) {
|
||||||
|
if (!d) return ''
|
||||||
|
const date = new Date(d)
|
||||||
|
const y = date.getFullYear()
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
return `${y}-${m}-${day}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPdf(url) {
|
||||||
|
if (!url) return false
|
||||||
|
return /\.pdf(\?|$)/i.test(url.split('?')[0])
|
||||||
|
}
|
||||||
|
function isImg(url) {
|
||||||
|
if (!url) return false
|
||||||
|
return /\.(png|jpg|jpeg|gif|webp)(\?|$)/i.test(url.split('?')[0])
|
||||||
|
}
|
||||||
|
// OSS bucket 默认 Content-Disposition: attachment, PDF 会被强制下载.
|
||||||
|
// 走 /common/oss/proxy 后端代理改写为 inline.
|
||||||
|
// 参考 PublicityDetail.vue 的 proxyUrl 逻辑
|
||||||
|
function proxyUrl(url) {
|
||||||
|
if (!url) return url
|
||||||
|
if (url.includes('hwtossbamlorgcn.oss-cn-beijing.aliyuncs.com')) {
|
||||||
|
return '/dev-api/common/oss/proxy?url=' + encodeURIComponent(url) + '#toolbar=0&zoom=page-width'
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
function goHome() { router.push('/') }
|
||||||
|
function goPublicity() { router.push('/publicity') }
|
||||||
|
function goLogin() { router.push('/login') }
|
||||||
|
async function goLogout() { try { await logoutApi() } catch {}; userStore.logout(); router.replace('/login') }
|
||||||
|
async function onUserCmd(cmd) {
|
||||||
|
if (cmd === 'logout') return goLogout()
|
||||||
|
if (cmd === 'account') {
|
||||||
|
const r = (userStore.user?.role || '')
|
||||||
|
const map = { admin: '/leader/home', leader: '/leader/home', manager: '/manager/workbench', doctor: '/doctor/home', executor: '/executor/meetings', sponsor: '/sponsor/home' }
|
||||||
|
router.push(map[r] || '/leader/home')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleScroll() { isScrolled.value = window.scrollY > 10 }
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||||
|
load()
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('scroll', handleScroll)
|
||||||
|
})
|
||||||
|
watch(() => route.params.id, load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
color: #1f2937;
|
||||||
|
background: #f5f6f8;
|
||||||
|
min-width: 1354px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
a { color: inherit; text-decoration: none; }
|
||||||
|
|
||||||
|
/* ===== 顶部导航 (与 PublicityDetail 一致) ===== */
|
||||||
|
.top-nav {
|
||||||
|
position: sticky; top: 0; z-index: 1000;
|
||||||
|
height: 72px; padding: 0 60px;
|
||||||
|
background: var(--brand-primary);
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||||||
|
display: flex; align-items: center;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,0.15);
|
||||||
|
transition: box-shadow 0.3s;
|
||||||
|
}
|
||||||
|
.top-nav.is-scrolled { box-shadow: 0 4px 20px rgba(0,0,0,0.25); }
|
||||||
|
.logo { display: flex; align-items: center; gap: 12px; }
|
||||||
|
.logo-icon {
|
||||||
|
width: 36px; height: 36px;
|
||||||
|
background: #fff; color: var(--brand-primary);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
font-size: 18px; font-weight: 600;
|
||||||
|
}
|
||||||
|
.logo-text { display: flex; flex-direction: column; line-height: 1.2; }
|
||||||
|
.logo-title { font-size: 16px; font-weight: 600; color: #fff; letter-spacing: 0.5px; }
|
||||||
|
.logo-subtitle { font-size: 11px; color: rgba(255,255,255,0.6); margin-top: 2px; letter-spacing: 0.3px; }
|
||||||
|
.nav-list { flex: 1; display: flex; align-items: center; justify-content: center; gap: 36px; list-style: none; }
|
||||||
|
.nav-item { position: relative; height: 72px; display: flex; align-items: center; }
|
||||||
|
.nav-link {
|
||||||
|
font-size: 15px; font-weight: 500; color: rgba(255,255,255,0.85);
|
||||||
|
cursor: pointer; transition: color 0.25s; position: relative;
|
||||||
|
height: 72px; display: flex; align-items: center;
|
||||||
|
}
|
||||||
|
.nav-link::after {
|
||||||
|
content: ''; position: absolute; left: 50%; bottom: 0;
|
||||||
|
width: 0; height: 2px; background: #fff;
|
||||||
|
transform: translateX(-50%); transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
.nav-item:hover .nav-link, .nav-link.active { color: #fff; }
|
||||||
|
.nav-item:hover .nav-link::after, .nav-link.active::after { width: 100%; }
|
||||||
|
.top-tools { display: flex; align-items: center; gap: 16px; }
|
||||||
|
.login-btn {
|
||||||
|
padding: 7px 20px; background: #fff; color: var(--brand-primary);
|
||||||
|
font-size: 13px; font-weight: 500;
|
||||||
|
cursor: pointer; transition: background 0.2s; letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
.login-btn:hover { background: #f3f4f6; }
|
||||||
|
.user-link { display: inline-flex; align-items: center; gap: 6px; color: #fff; font-size: 14px; cursor: pointer; }
|
||||||
|
|
||||||
|
/* ===== 主体 ===== */
|
||||||
|
.container { max-width: 1354px; margin: 0 auto; padding: 28px 60px 60px; }
|
||||||
|
|
||||||
|
.plan-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,0.04);
|
||||||
|
padding: 56px 80px 64px;
|
||||||
|
min-height: 480px;
|
||||||
|
}
|
||||||
|
.plan-head { text-align: center; border-bottom: 1px solid #eef0f3; padding-bottom: 28px; margin-bottom: 32px; }
|
||||||
|
.plan-title { font-size: 26px; font-weight: 700; color: #1f2937; letter-spacing: 1px; line-height: 1.4; }
|
||||||
|
.plan-meta { margin-top: 12px; font-size: 13px; color: #9ca3af; }
|
||||||
|
|
||||||
|
/* 富文本 (与 ArticleView 一致) */
|
||||||
|
.plan-body-rich {
|
||||||
|
font-size: 15px; line-height: 2; color: #374151;
|
||||||
|
}
|
||||||
|
.plan-body-rich :deep(h1),
|
||||||
|
.plan-body-rich :deep(h2),
|
||||||
|
.plan-body-rich :deep(h3) { font-weight: 700; color: #1f2937; margin: 24px 0 14px; line-height: 1.5; }
|
||||||
|
.plan-body-rich :deep(h1) { font-size: 22px; text-align: center; }
|
||||||
|
.plan-body-rich :deep(h2) { font-size: 18px; }
|
||||||
|
.plan-body-rich :deep(h3) { font-size: 16px; }
|
||||||
|
.plan-body-rich :deep(p) { margin: 10px 0; text-indent: 2em; text-align: justify; }
|
||||||
|
.plan-body-rich :deep(ul),
|
||||||
|
.plan-body-rich :deep(ol) { margin: 12px 0 12px 2em; }
|
||||||
|
.plan-body-rich :deep(li) { margin: 6px 0; }
|
||||||
|
.plan-body-rich :deep(.empty) { text-align: center; color: #c0c4cc; text-indent: 0; padding: 60px 0; }
|
||||||
|
|
||||||
|
/* 文件预览 */
|
||||||
|
.plan-body-file { padding: 0; min-height: 600px; }
|
||||||
|
.content-frame { width: 100%; height: 700px; display: block; border: 0; }
|
||||||
|
.content-img { width: 100%; max-width: 100%; display: block; margin: 0 auto; }
|
||||||
|
.content-empty { padding: 80px 20px; text-align: center; color: #c0c4cc; }
|
||||||
|
.content-empty p { margin: 8px 0 0; }
|
||||||
|
.content-empty .hint { font-size: 12px; }
|
||||||
|
|
||||||
|
/* ===== 底部 ===== */
|
||||||
|
.footer { background: var(--brand-primary-darker); color: rgba(255,255,255,0.65); padding: 48px 0 0; }
|
||||||
|
.footer-main { display: grid; grid-template-columns: 1.4fr 1fr 1fr 1.2fr auto; gap: 48px; padding-bottom: 36px; }
|
||||||
|
.footer-brand .brand-row { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
||||||
|
.footer-logo-icon {
|
||||||
|
width: 36px; height: 36px;
|
||||||
|
background: #fff; color: var(--brand-primary);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
font-size: 18px; font-weight: 600; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.footer-brand-name { font-size: 16px; font-weight: 600; color: #fff; letter-spacing: 1px; }
|
||||||
|
.footer-brand-en { font-size: 10px; color: rgba(255,255,255,0.4); letter-spacing: 0.5px; margin-top: 2px; }
|
||||||
|
.footer-desc { font-size: 13px; line-height: 1.9; color: rgba(255,255,255,0.55); }
|
||||||
|
.footer-col h4 {
|
||||||
|
font-size: 14px; font-weight: 600; color: #fff;
|
||||||
|
letter-spacing: 1px; margin-bottom: 16px; padding-bottom: 10px; position: relative;
|
||||||
|
}
|
||||||
|
.footer-col h4::after { content: ''; position: absolute; left: 0; bottom: 0; width: 24px; height: 2px; background: #93c5fd; }
|
||||||
|
.footer-col a, .footer-col p {
|
||||||
|
display: block; font-size: 13px; color: rgba(255,255,255,0.6);
|
||||||
|
line-height: 2.1; transition: color 0.2s;
|
||||||
|
}
|
||||||
|
.footer-col a { cursor: pointer; }
|
||||||
|
.footer-col a:hover { color: #fff; }
|
||||||
|
.footer-qr { text-align: center; }
|
||||||
|
.qr-image {
|
||||||
|
width: 100px; height: 100px;
|
||||||
|
background: #fff;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
.qr-label { font-size: 12px; color: rgba(255,255,255,0.5); margin-top: 10px; }
|
||||||
|
.footer-bottom {
|
||||||
|
border-top: 1px solid rgba(255,255,255,0.12);
|
||||||
|
padding: 18px 0;
|
||||||
|
display: flex; justify-content: space-between;
|
||||||
|
font-size: 12px; color: rgba(255,255,255,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.container { padding: 0 24px 40px; }
|
||||||
|
.plan-card { padding: 32px 24px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -112,8 +112,8 @@
|
|||||||
<el-form-item label="短信验证码" :required="true">
|
<el-form-item label="短信验证码" :required="true">
|
||||||
<div class="sms-row">
|
<div class="sms-row">
|
||||||
<el-input v-model="phoneForm.smsCode" placeholder="请输入验证码" />
|
<el-input v-model="phoneForm.smsCode" placeholder="请输入验证码" />
|
||||||
<el-button class="btn-sms" :disabled="phoneSmsCountdown > 0" @click="sendSms">
|
<el-button class="btn-sms" :disabled="phoneSmsLocked || phoneSmsCountdown > 0" @click="sendSms">
|
||||||
{{ phoneSmsCountdown > 0 ? `${phoneSmsCountdown}s 后重试` : '发送验证码' }}
|
{{ phoneSmsLocked ? '发送中...' : phoneSmsCountdown > 0 ? `${phoneSmsCountdown}s 后重试` : '发送验证码' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -134,6 +134,9 @@ import { EditPen } from '@element-plus/icons-vue'
|
|||||||
import { useUserStore } from '@/store/user'
|
import { useUserStore } from '@/store/user'
|
||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
import { uploadToOss } from '@/utils/oss'
|
import { uploadToOss } from '@/utils/oss'
|
||||||
|
import { useAsyncLock } from '@/utils/useAsyncLock'
|
||||||
|
|
||||||
|
const { locked: phoneSmsLocked, run: sendPhoneSmsOnce } = useAsyncLock()
|
||||||
|
|
||||||
const store = useUserStore()
|
const store = useUserStore()
|
||||||
const tab = ref('profile')
|
const tab = ref('profile')
|
||||||
@@ -350,25 +353,27 @@ async function sendSms() {
|
|||||||
if (!/^(1[0-9])\d{9}$/.test(phone)) {
|
if (!/^(1[0-9])\d{9}$/.test(phone)) {
|
||||||
return ElMessage.warning('请输入正确的手机号')
|
return ElMessage.warning('请输入正确的手机号')
|
||||||
}
|
}
|
||||||
try {
|
await sendPhoneSmsOnce(async () => {
|
||||||
const res = await request({
|
try {
|
||||||
url: '/system/user/profile/sendSmsCode',
|
const res = await request({
|
||||||
method: 'post',
|
url: '/system/user/profile/sendSmsCode',
|
||||||
params: { phone }
|
method: 'post',
|
||||||
})
|
params: { phone }
|
||||||
phoneSmsUuid.value = res.uuid || ''
|
})
|
||||||
ElMessage.success('验证码已发送')
|
phoneSmsUuid.value = res.uuid || ''
|
||||||
phoneSmsCountdown.value = 60
|
ElMessage.success('验证码已发送')
|
||||||
phoneSmsTimer = setInterval(() => {
|
phoneSmsCountdown.value = 60
|
||||||
phoneSmsCountdown.value--
|
phoneSmsTimer = setInterval(() => {
|
||||||
if (phoneSmsCountdown.value <= 0) {
|
phoneSmsCountdown.value--
|
||||||
clearInterval(phoneSmsTimer)
|
if (phoneSmsCountdown.value <= 0) {
|
||||||
phoneSmsTimer = null
|
clearInterval(phoneSmsTimer)
|
||||||
}
|
phoneSmsTimer = null
|
||||||
}, 1000)
|
}
|
||||||
} catch (e) {
|
}, 1000)
|
||||||
ElMessage.error(e?.msg || '发送失败')
|
} catch (e) {
|
||||||
}
|
ElMessage.error(e?.msg || '发送失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onChangePhone() {
|
async function onChangePhone() {
|
||||||
|
|||||||
@@ -172,12 +172,8 @@
|
|||||||
<el-descriptions-item label="项目编号">{{ detail.projectNo }}</el-descriptions-item>
|
<el-descriptions-item label="项目编号">{{ detail.projectNo }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="项目形式">{{ detail.projectForm }}</el-descriptions-item>
|
<el-descriptions-item label="项目形式">{{ detail.projectForm }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="项目名称" :span="2">{{ detail.projectName }}</el-descriptions-item>
|
<el-descriptions-item label="项目名称" :span="2">{{ detail.projectName }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="赞助公司" :span="2">{{ detail.orgName || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="赞助方负责人" :span="2">{{ detail.sponsorAdminUserName || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="公司类型" :span="2">
|
<el-descriptions-item label="公司类型" :span="2">赞助方</el-descriptions-item>
|
||||||
<el-tag :type="detail.orgType === 'sponsor' ? 'success' : 'primary'" disable-transitions>
|
|
||||||
{{ detail.orgType === 'sponsor' ? '赞助方' : '执行方' }}
|
|
||||||
</el-tag>
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="总场次/总期数">{{ detail.totalSessions || 0 }}</el-descriptions-item>
|
<el-descriptions-item label="总场次/总期数">{{ detail.totalSessions || 0 }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="已执行/未执行">{{ detail.doneSessions || 0 }} / {{ detail.todoSessions || 0 }}</el-descriptions-item>
|
<el-descriptions-item label="已执行/未执行">{{ detail.doneSessions || 0 }} / {{ detail.todoSessions || 0 }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="总金额">¥{{ formatMoney(detail.totalAmount) }}</el-descriptions-item>
|
<el-descriptions-item label="总金额">¥{{ formatMoney(detail.totalAmount) }}</el-descriptions-item>
|
||||||
@@ -280,8 +276,8 @@ function onCreateMeeting(row) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onSupportUnit(row) {
|
function onSupportUnit(row) {
|
||||||
if (!row.orgId) { ElMessage.warning('该项目未关联赞助公司'); return }
|
if (!row.sponsorAdminUserId) { ElMessage.warning('该项目未关联赞助方负责人'); return }
|
||||||
ElMessage.info(`查看赞助公司: ${row.orgName || row.orgId}`)
|
ElMessage.info(`查看赞助方负责人: ${row.sponsorAdminUserName || row.sponsorAdminUserId}`)
|
||||||
}
|
}
|
||||||
async function onDeleteNotice(row) {
|
async function onDeleteNotice(row) {
|
||||||
try { await ElMessageBox.confirm(`确定删除「${row.projectName}」的所有公告吗?删除后无法恢复`, '删除公告', { type: 'warning' }) } catch { return }
|
try { await ElMessageBox.confirm(`确定删除「${row.projectName}」的所有公告吗?删除后无法恢复`, '删除公告', { type: 'warning' }) } catch { return }
|
||||||
|
|||||||
+36
-17
@@ -3,20 +3,22 @@
|
|||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
|
|
||||||
-- 项目
|
-- 项目
|
||||||
|
-- 2026-08-16: org_id/org_name/org_type 重命名为 sponsor_admin_user_id/sponsor_admin_user_name (存 sys_user.user_id, 不是公司)
|
||||||
|
-- sponsor01=钱七, user_id=104
|
||||||
INSERT INTO biz_project(project_no, project_name, project_form, total_sessions, done_sessions, todo_sessions,
|
INSERT INTO biz_project(project_no, project_name, project_form, total_sessions, done_sessions, todo_sessions,
|
||||||
total_amount, available_amount, paid_labor_amount, paid_meeting_amount, manage_fee,
|
total_amount, available_amount, paid_labor_amount, paid_meeting_amount, manage_fee,
|
||||||
is_finished, is_settled, rating_score, org_name, org_type,
|
is_finished, is_settled, rating_score, sponsor_admin_user_id, sponsor_admin_user_name,
|
||||||
start_time, end_time, create_by, create_time)
|
start_time, end_time, create_by, create_time)
|
||||||
VALUES
|
VALUES
|
||||||
('ZH-2026-658', '小牛血清创新应用研讨会', '线上', 26, 16, 10, 3112000.00, 1112000.00, 1000000.00, 1000000.00, 1112000.00, '0', '0', 5.0, '北京XXXX有限公司', 'sponsor', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()),
|
('ZH-2026-658', '小牛血清创新应用研讨会', '线上', 26, 16, 10, 3112000.00, 1112000.00, 1000000.00, 1000000.00, 1112000.00, '0', '0', 5.0, 104, '赞助方钱七', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()),
|
||||||
('ZH-2026-659', '整合医学学会项目评审会', '线下', 18, 12, 6, 2500000.00, 980000.00, 900000.00, 620000.00, 980000.00, '0', '0', 4.5, '北京XXXX有限公司', 'sponsor', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()),
|
('ZH-2026-659', '整合医学学会项目评审会', '线下', 18, 12, 6, 2500000.00, 980000.00, 900000.00, 620000.00, 980000.00, '0', '0', 4.5, 104, '赞助方钱七', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()),
|
||||||
('ZH-2026-650', '智慧医院建设项目方案论证会', '线上+线下', 12, 7, 5, 1800000.00, 760000.00, 700000.00, 340000.00, 760000.00, '0', '0', 4.0, '北京XXXX有限公司', 'sponsor', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()),
|
('ZH-2026-650', '智慧医院建设项目方案论证会', '线上+线下', 12, 7, 5, 1800000.00, 760000.00, 700000.00, 340000.00, 760000.00, '0', '0', 4.0, 104, '赞助方钱七', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()),
|
||||||
('ZH-2026-645', '基层医疗改革试点方案中期评估会', '线下', 20, 14, 6, 2800000.00, 1050000.00, 950000.00, 800000.00, 1050000.00, '0', '0', 4.8, '北京XXXX有限公司', 'sponsor', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()),
|
('ZH-2026-645', '基层医疗改革试点方案中期评估会', '线下', 20, 14, 6, 2800000.00, 1050000.00, 950000.00, 800000.00, 1050000.00, '0', '0', 4.8, 104, '赞助方钱七', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()),
|
||||||
('ZH-2026-640', '数字化医疗转型方案评审会', '线上', 22, 15, 7, 3000000.00, 1200000.00, 1100000.00, 700000.00, 1200000.00, '0', '0', 4.2, '北京XXXX有限公司', 'sponsor', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()),
|
('ZH-2026-640', '数字化医疗转型方案评审会', '线上', 22, 15, 7, 3000000.00, 1200000.00, 1100000.00, 700000.00, 1200000.00, '0', '0', 4.2, 104, '赞助方钱七', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()),
|
||||||
('ZH-2026-635', '临床医学研究方案评议会', '线下', 14, 9, 5, 2200000.00, 820000.00, 740000.00, 640000.00, 820000.00, '1', '1', 4.6, '北京XXXX有限公司', 'sponsor', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()),
|
('ZH-2026-635', '临床医学研究方案评议会', '线下', 14, 9, 5, 2200000.00, 820000.00, 740000.00, 640000.00, 820000.00, '1', '1', 4.6, 104, '赞助方钱七', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()),
|
||||||
('ZH-2026-628', '公共卫生应急体系建设研讨会', '线下', 16, 11, 5, 2400000.00, 900000.00, 820000.00, 680000.00, 900000.00, '0', '0', 4.4, '北京XXXX有限公司', 'sponsor', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()),
|
('ZH-2026-628', '公共卫生应急体系建设研讨会', '线下', 16, 11, 5, 2400000.00, 900000.00, 820000.00, 680000.00, 900000.00, '0', '0', 4.4, 104, '赞助方钱七', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()),
|
||||||
('ZH-2026-622', '中医诊疗标准化研究论坛', '线下', 10, 6, 4, 1500000.00, 560000.00, 500000.00, 440000.00, 560000.00, '0', '0', 4.7, '北京XXXX有限公司', 'sponsor', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()),
|
('ZH-2026-622', '中医诊疗标准化研究论坛', '线下', 10, 6, 4, 1500000.00, 560000.00, 500000.00, 440000.00, 560000.00, '0', '0', 4.7, 104, '赞助方钱七', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()),
|
||||||
('ZH-2026-618', '医疗 AI 辅助诊断应用论坛', '线上+线下', 8, 4, 4, 1200000.00, 480000.00, 420000.00, 300000.00, 480000.00, '0', '0', 4.9, '北京XXXX有限公司', 'sponsor', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW());
|
('ZH-2026-618', '医疗 AI 辅助诊断应用论坛', '线上+线下', 8, 4, 4, 1200000.00, 480000.00, 420000.00, 300000.00, 480000.00, '0', '0', 4.9, 104, '赞助方钱七', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW());
|
||||||
|
|
||||||
-- 项目角色劳务设置
|
-- 项目角色劳务设置
|
||||||
INSERT INTO biz_project_labor_role(project_id, role_name, labor_amount)
|
INSERT INTO biz_project_labor_role(project_id, role_name, labor_amount)
|
||||||
@@ -140,13 +142,19 @@ SELECT 'ZH-2026-658', '小牛血清创新应用研讨会', '孙八', '北京XXXX
|
|||||||
SELECT 'ZH-2026-658', '小牛血清创新应用研讨会', '周九', '北京XXXXXX公司', '华北大区', '项目负责人', '13512321603', '已入库', NOW() UNION ALL
|
SELECT 'ZH-2026-658', '小牛血清创新应用研讨会', '周九', '北京XXXXXX公司', '华北大区', '项目负责人', '13512321603', '已入库', NOW() UNION ALL
|
||||||
SELECT 'ZH-2026-658', '小牛血清创新应用研讨会', '吴十', '北京XXXXXX公司', '华北大区', '项目负责人', '13512321604', '已入库', NOW();
|
SELECT 'ZH-2026-658', '小牛血清创新应用研讨会', '吴十', '北京XXXXXX公司', '华北大区', '项目负责人', '13512321604', '已入库', NOW();
|
||||||
|
|
||||||
-- 公示公告
|
-- 公示公告 (2026-08-16: biz_announcement → biz_publicity, ann_type → announce_type)
|
||||||
INSERT INTO biz_announcement(title, ann_type, publish_time, project_no, create_by, create_time)
|
INSERT INTO biz_publicity(title, announce_type, file_url, publish_time, project_id, project_no, project_name, status, create_by, create_time)
|
||||||
VALUES
|
SELECT a.title, a.announce_type, NULL, a.publish_time,
|
||||||
('邀请函 2026第2323号 "恺启新生—胃癌 CAR-T 细胞治疗系列会"', '邀请函', '2026-05-06 10:00:00', 'ZH-2026-658', 'admin', NOW()),
|
(SELECT project_id FROM biz_project WHERE project_no=a.project_no LIMIT 1),
|
||||||
('会议通知 2026第2277号 北京同仁医院建院140周年学术会议', '通知', '2026-05-06 10:00:00', 'ZH-2026-659', 'admin', NOW()),
|
a.project_no,
|
||||||
('支持函 2026第2278号 项目启动支持', '支持函', '2026-05-06 10:00:00', 'ZH-2026-650', 'admin', NOW()),
|
(SELECT project_name FROM biz_project WHERE project_no=a.project_no LIMIT 1),
|
||||||
('会议日程 2026第2279号 整合医学学会项目评审会', '日程', '2026-05-06 10:00:00', 'ZH-2026-659', 'admin', NOW());
|
'1', a.create_by, a.create_time
|
||||||
|
FROM (
|
||||||
|
SELECT '邀请函 2026第2323号 "恺启新生—胃癌 CAR-T 细胞治疗系列会"' AS title, 'invitation' AS announce_type, '2026-05-06 10:00:00' AS publish_time, 'ZH-2026-658' AS project_no, 'admin' AS create_by, NOW() AS create_time UNION ALL
|
||||||
|
SELECT '会议通知 2026第2277号 北京同仁医院建院140周年学术会议', 'notice', '2026-05-06 10:00:00', 'ZH-2026-659', 'admin', NOW() UNION ALL
|
||||||
|
SELECT '支持函 2026第2278号 项目启动支持', 'support', '2026-05-06 10:00:00', 'ZH-2026-650', 'admin', NOW() UNION ALL
|
||||||
|
SELECT '会议日程 2026第2279号 整合医学学会项目评审会', 'agenda', '2026-05-06 10:00:00', 'ZH-2026-659', 'admin', NOW()
|
||||||
|
) a;
|
||||||
|
|
||||||
-- 邀请函
|
-- 邀请函
|
||||||
INSERT INTO biz_invitation(project_id, project_no, title, publish_time)
|
INSERT INTO biz_invitation(project_id, project_no, title, publish_time)
|
||||||
@@ -211,3 +219,14 @@ VALUES
|
|||||||
INSERT INTO sys_user_role(user_id, role_id)
|
INSERT INTO sys_user_role(user_id, role_id)
|
||||||
SELECT user_id, 2 FROM sys_user WHERE user_name IN ('leader01','manager01','doctor01','executor01','sponsor01');
|
SELECT user_id, 2 FROM sys_user WHERE user_name IN ('leader01','manager01','doctor01','executor01','sponsor01');
|
||||||
-- admin 已在原 RuoYi 脚本中,保留
|
-- admin 已在原 RuoYi 脚本中,保留
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- 平台协议/隐私政策 初始数据 (2026-08-16)
|
||||||
|
-- ============================================================================
|
||||||
|
INSERT INTO biz_article(title, type, content, status, create_by, create_time, remark) VALUES
|
||||||
|
('用户服务协议', 'agreement',
|
||||||
|
'<h2>用户服务协议</h2><p>欢迎使用 BAHIM 项目管理平台。请仔细阅读本协议, 注册即视为同意全部条款。</p><p>1. 用户应保证所提供资料真实有效。</p><p>2. 用户应妥善保管账号密码。</p><p>3. 平台保留最终解释权。</p>',
|
||||||
|
'0', 'admin', NOW(), '注册页底部 [我已阅读并同意《用户协议》] 链接指向此处'),
|
||||||
|
('隐私政策', 'privacy',
|
||||||
|
'<h2>隐私政策</h2><p>BAHIM 平台高度重视用户隐私, 严格按照法律法规要求保护您的个人信息。</p><p>1. 收集信息范围: 注册手机号、姓名、单位名称等业务必要字段。</p><p>2. 信息用途: 仅用于项目协作, 不会用于商业推广或对外披露。</p><p>3. 您的权利: 可随时在账号信息页查看和更正个人信息。</p>',
|
||||||
|
'0', 'admin', NOW(), '注册页底部 [《隐私政策》] 链接指向此处');
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- 重新灌 5 张被清空的表 (2026-08-16, biz_schema.sql 误执行后)
|
||||||
|
-- 仅插入被清空的 5 张表: biz_project / biz_meeting / biz_expert / biz_project_assign / biz_publicity
|
||||||
|
-- 其他表已有数据, 不动
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- 1. biz_project (9 条, sponsor_admin_user_id=104=sponsor01, sponsor_admin_user_name='赞助方钱七')
|
||||||
|
INSERT INTO biz_project(project_no, project_name, project_form, total_sessions, done_sessions, todo_sessions,
|
||||||
|
total_amount, available_amount, paid_labor_amount, paid_meeting_amount, manage_fee,
|
||||||
|
is_finished, is_settled, rating_score, sponsor_admin_user_id, sponsor_admin_user_name,
|
||||||
|
start_time, end_time, create_by, create_time)
|
||||||
|
VALUES
|
||||||
|
('ZH-2026-658', '小牛血清创新应用研讨会', '线上', 26, 16, 10, 3112000.00, 1112000.00, 1000000.00, 1000000.00, 1112000.00, '0', '0', 5.0, 104, '赞助方钱七', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()),
|
||||||
|
('ZH-2026-659', '整合医学学会项目评审会', '线下', 18, 12, 6, 2500000.00, 980000.00, 900000.00, 620000.00, 980000.00, '0', '0', 4.5, 104, '赞助方钱七', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()),
|
||||||
|
('ZH-2026-650', '智慧医院建设项目方案论证会', '线上+线下', 12, 7, 5, 1800000.00, 760000.00, 700000.00, 340000.00, 760000.00, '0', '0', 4.0, 104, '赞助方钱七', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()),
|
||||||
|
('ZH-2026-645', '基层医疗改革试点方案中期评估会', '线下', 20, 14, 6, 2800000.00, 1050000.00, 950000.00, 800000.00, 1050000.00, '0', '0', 4.8, 104, '赞助方钱七', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()),
|
||||||
|
('ZH-2026-640', '数字化医疗转型方案评审会', '线上', 22, 15, 7, 3000000.00, 1200000.00, 1100000.00, 700000.00, 1200000.00, '0', '0', 4.2, 104, '赞助方钱七', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()),
|
||||||
|
('ZH-2026-635', '临床医学研究方案评议会', '线下', 14, 9, 5, 2200000.00, 820000.00, 740000.00, 640000.00, 820000.00, '1', '1', 4.6, 104, '赞助方钱七', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()),
|
||||||
|
('ZH-2026-628', '公共卫生应急体系建设研讨会', '线下', 16, 11, 5, 2400000.00, 900000.00, 820000.00, 680000.00, 900000.00, '0', '0', 4.4, 104, '赞助方钱七', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()),
|
||||||
|
('ZH-2026-622', '中医诊疗标准化研究论坛', '线下', 10, 6, 4, 1500000.00, 560000.00, 500000.00, 440000.00, 560000.00, '0', '0', 4.7, 104, '赞助方钱七', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW()),
|
||||||
|
('ZH-2026-618', '医疗 AI 辅助诊断应用论坛', '线上+线下', 8, 4, 4, 1200000.00, 480000.00, 420000.00, 300000.00, 480000.00, '0', '0', 4.9, 104, '赞助方钱七', '2026-05-11 00:00:00', '2026-06-30 00:00:00', 'admin', NOW());
|
||||||
|
|
||||||
|
-- 2. biz_meeting (5 条, 关联 biz_project)
|
||||||
|
INSERT INTO biz_meeting(business_id, project_id, project_no, project_name, meeting_name, period_no, total_periods, project_form, start_time, end_time, org_name, current_stage, create_by, create_time)
|
||||||
|
SELECT '5643145673', project_id, 'ZH-2026-658', '小牛血清创新应用研讨会', '小牛血清创新应用研讨会', 3, 26, '线上', '2026-05-11 11:30:00', '2026-05-11 14:30:00', '北京XXXX有限公司', '待监管', 'admin', NOW() FROM biz_project WHERE project_no='ZH-2026-658' UNION ALL
|
||||||
|
SELECT '5643145674', project_id, 'ZH-2026-659', '整合医学学会项目评审会', '整合医学学会项目评审会', 18, 18, '线下', '2026-04-01 09:00:00', '2026-04-01 17:00:00', '北京XXXX有限公司', '监管通过', 'admin', NOW() FROM biz_project WHERE project_no='ZH-2026-659' UNION ALL
|
||||||
|
SELECT '5643145675', project_id, 'ZH-2026-650', '智慧医院建设项目方案论证会', '智慧医院建设项目方案论证会', 7, 12, '线上+线下', '2026-03-15 14:00:00', '2026-03-15 16:30:00', '北京XXXX有限公司', '待整改', 'admin', NOW() FROM biz_project WHERE project_no='ZH-2026-650' UNION ALL
|
||||||
|
SELECT '5643145676', project_id, 'ZH-2026-645', '基层医疗改革试点中期评估会', '基层医疗改革试点中期评估会', 14, 20, '线上', '2026-02-01 09:30:00', '2026-02-01 12:00:00', '北京XXXX有限公司', '已结算', 'admin', NOW() FROM biz_project WHERE project_no='ZH-2026-645' UNION ALL
|
||||||
|
SELECT '5643145677', project_id, 'ZH-2026-640', '数字化医疗转型方案评审会', '数字化医疗转型方案评审会', 15, 22, '线下', '2026-01-10 10:00:00', '2026-01-10 17:00:00', '北京XXXX有限公司', '已结题', 'admin', NOW() FROM biz_project WHERE project_no='ZH-2026-640';
|
||||||
|
|
||||||
|
-- 3. biz_expert (9 条)
|
||||||
|
INSERT INTO biz_expert(name, phone, work_unit, department, title, region, id_card, audit_status, audit_by, audit_time, status, create_by, create_time)
|
||||||
|
VALUES
|
||||||
|
('张三', '13534621147', '北京XXXXXXX医院', '呼吸科', '主任医师', '北京', '110101198501011234', '1', 'admin', NOW(), 'Y', 'admin', NOW()),
|
||||||
|
('李四', '13534621148', '北京XXXXXXX医院', '血液科', '副主任医师', '北京', '110101198801012345', '2', 'admin', NOW(), 'Y', 'admin', NOW()),
|
||||||
|
('王五', '13534621149', '北京XXXXXXX医院', '心血管科', '主治医师', '上海', '310101198901013456', '1', 'admin', NOW(), 'Y', 'admin', NOW()),
|
||||||
|
('赵六', '13534621150', '北京XXXXXXX医院', '神经科', '副主任医师', '广东', '440101198701014567', '3', 'admin', NOW(), 'Y', 'admin', NOW()),
|
||||||
|
('钱七', '13534621151', '北京XXXXXXX医院', '内分泌科', '主任医师', '北京', '110101199001015678', '2', 'admin', NOW(), 'Y', 'admin', NOW()),
|
||||||
|
('陈教授', '13534621152', '北京XXXXXXX医院', '消化科', '副主任医师', '北京', '110101198601016789', '2', 'admin', NOW(), 'Y', 'admin', NOW()),
|
||||||
|
('王教授', '13534621153', '北京XXXXXXX医院', '心血管科', '主任医师', '上海', '310101198401017890', '2', 'admin', NOW(), 'Y', 'admin', NOW()),
|
||||||
|
('李教授', '13534621154', '北京XXXXXXX医院', '神经科', '主治医师', '北京', '110101199201018901', '2', 'admin', NOW(), 'Y', 'admin', NOW()),
|
||||||
|
('张教授', '13534621155', '北京XXXXXXX医院', '骨科', '主任医师', '北京', '110101198501019012', '3', 'admin', NOW(), 'Y', 'admin', NOW());
|
||||||
|
|
||||||
|
-- 4. biz_project_assign (10 条, 关联前 5 个项目和前 5 个执行方单位, 每个项目 2 个执行单位)
|
||||||
|
INSERT INTO biz_project_assign(project_id, execution_unit_id, execution_unit_name, sessions, amount)
|
||||||
|
SELECT p.project_id, e.unit_id, e.unit_name, e.sessions, e.amount
|
||||||
|
FROM biz_project p
|
||||||
|
JOIN (
|
||||||
|
SELECT 'ZH-2026-658' AS project_no, 9 AS unit_id, '北京XXXXXXX公司' AS unit_name, 26 AS sessions, 800000.00 AS amount UNION ALL
|
||||||
|
SELECT 'ZH-2026-658', 10, '上海XXXXXXX公司', 26, 800000.00 UNION ALL
|
||||||
|
SELECT 'ZH-2026-659', 9, '北京XXXXXXX公司', 18, 700000.00 UNION ALL
|
||||||
|
SELECT 'ZH-2026-659', 11, '广州XXXXXXX公司', 18, 600000.00 UNION ALL
|
||||||
|
SELECT 'ZH-2026-650', 10, '上海XXXXXXX公司', 12, 500000.00 UNION ALL
|
||||||
|
SELECT 'ZH-2026-650', 12, '深圳XXXXXXX公司', 12, 500000.00 UNION ALL
|
||||||
|
SELECT 'ZH-2026-645', 9, '北京XXXXXXX公司', 20, 900000.00 UNION ALL
|
||||||
|
SELECT 'ZH-2026-645', 13, '北京XXXXXXX公司', 20, 700000.00 UNION ALL
|
||||||
|
SELECT 'ZH-2026-640', 10, '上海XXXXXXX公司', 22, 1100000.00 UNION ALL
|
||||||
|
SELECT 'ZH-2026-640', 14, '北京XXXXXXX公司', 22, 800000.00
|
||||||
|
) e ON e.project_no = p.project_no;
|
||||||
|
|
||||||
|
-- 5. biz_publicity (4 条, 原 biz_announcement, 列名也改了: ann_type→announce_type)
|
||||||
|
INSERT INTO biz_publicity(title, announce_type, file_url, publish_time, project_id, project_no, project_name, status, create_by, create_time)
|
||||||
|
SELECT a.title, a.announce_type, NULL, a.publish_time,
|
||||||
|
(SELECT project_id FROM biz_project WHERE project_no=a.project_no LIMIT 1),
|
||||||
|
a.project_no,
|
||||||
|
(SELECT project_name FROM biz_project WHERE project_no=a.project_no LIMIT 1),
|
||||||
|
'1', a.create_by, a.create_time
|
||||||
|
FROM (
|
||||||
|
SELECT '邀请函 2026第2323号 "恺启新生—胃癌 CAR-T 细胞治疗系列会"' AS title, 'invitation' AS announce_type, '2026-05-06 10:00:00' AS publish_time, 'ZH-2026-658' AS project_no, 'admin' AS create_by, NOW() AS create_time UNION ALL
|
||||||
|
SELECT '会议通知 2026第2277号 北京同仁医院建院140周年学术会议', 'notice', '2026-05-06 10:00:00', 'ZH-2026-659', 'admin', NOW() UNION ALL
|
||||||
|
SELECT '支持函 2026第2278号 项目启动支持', 'support', '2026-05-06 10:00:00', 'ZH-2026-650', 'admin', NOW() UNION ALL
|
||||||
|
SELECT '会议日程 2026第2279号 整合医学学会项目评审会', 'agenda', '2026-05-06 10:00:00', 'ZH-2026-659', 'admin', NOW()
|
||||||
|
) a;
|
||||||
+43
-15
@@ -23,9 +23,8 @@ CREATE TABLE biz_project (
|
|||||||
is_finished CHAR(1) DEFAULT '0' COMMENT '是否结题 0否 1是',
|
is_finished CHAR(1) DEFAULT '0' COMMENT '是否结题 0否 1是',
|
||||||
is_settled CHAR(1) DEFAULT '0' COMMENT '是否结算 0否 1是',
|
is_settled CHAR(1) DEFAULT '0' COMMENT '是否结算 0否 1是',
|
||||||
rating_score DECIMAL(3,1) DEFAULT NULL COMMENT '项目评价分数',
|
rating_score DECIMAL(3,1) DEFAULT NULL COMMENT '项目评价分数',
|
||||||
org_id BIGINT DEFAULT NULL COMMENT '公司ID (赞助方或执行方, 由 org_type 区分)',
|
sponsor_admin_user_id BIGINT DEFAULT NULL COMMENT '赞助方负责人用户ID (sys_user.user_id, role_type=sponsor)',
|
||||||
org_name VARCHAR(200) DEFAULT NULL COMMENT '公司名称(冗余)',
|
sponsor_admin_user_name VARCHAR(200) DEFAULT NULL COMMENT '赞助方负责人用户名(冗余)',
|
||||||
org_type VARCHAR(20) DEFAULT 'sponsor' COMMENT '公司类型 sponsor赞助方/executor执行方',
|
|
||||||
start_time DATETIME DEFAULT NULL COMMENT '项目开始时间',
|
start_time DATETIME DEFAULT NULL COMMENT '项目开始时间',
|
||||||
end_time DATETIME DEFAULT NULL COMMENT '项目结束时间',
|
end_time DATETIME DEFAULT NULL COMMENT '项目结束时间',
|
||||||
submit_deadline_days INT DEFAULT 0 COMMENT '提交材料截止天数',
|
submit_deadline_days INT DEFAULT 0 COMMENT '提交材料截止天数',
|
||||||
@@ -82,18 +81,6 @@ CREATE TABLE biz_project_assign (
|
|||||||
KEY idx_assign_unit (execution_unit_id)
|
KEY idx_assign_unit (execution_unit_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项目-执行单位分配表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项目-执行单位分配表';
|
||||||
|
|
||||||
DROP TABLE IF EXISTS biz_project_supervisor;
|
|
||||||
CREATE TABLE biz_project_supervisor (
|
|
||||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
|
||||||
project_id BIGINT NOT NULL COMMENT '项目ID',
|
|
||||||
user_id BIGINT NOT NULL COMMENT '监督员(用户)ID',
|
|
||||||
user_name VARCHAR(50) DEFAULT NULL COMMENT '监督员姓名(冗余)',
|
|
||||||
create_time DATETIME DEFAULT NULL COMMENT '创建时间',
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
KEY idx_ps_project (project_id),
|
|
||||||
KEY idx_ps_user (user_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项目监督员表';
|
|
||||||
|
|
||||||
DROP TABLE IF EXISTS biz_project_labor_role;
|
DROP TABLE IF EXISTS biz_project_labor_role;
|
||||||
CREATE TABLE biz_project_labor_role (
|
CREATE TABLE biz_project_labor_role (
|
||||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||||
@@ -395,6 +382,47 @@ CREATE TABLE biz_labor_voucher (
|
|||||||
-- 注: 2026-08-16 重构, 删 biz_user_role_bind 表, 业务角色统一存 sys_user.role_type
|
-- 注: 2026-08-16 重构, 删 biz_user_role_bind 表, 业务角色统一存 sys_user.role_type
|
||||||
-- sys_user 表在 RuoYi 标准 schema 里已含 role_type 列 (admin/leader/manager/doctor/executor/sponsor)
|
-- sys_user 表在 RuoYi 标准 schema 里已含 role_type 列 (admin/leader/manager/doctor/executor/sponsor)
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- 平台协议/隐私政策文章表 (2026-08-16 新增)
|
||||||
|
-- type: agreement=用户协议, privacy=隐私政策
|
||||||
|
-- ============================================================================
|
||||||
|
CREATE TABLE biz_article (
|
||||||
|
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '文章ID',
|
||||||
|
title VARCHAR(200) NOT NULL COMMENT '标题',
|
||||||
|
type VARCHAR(20) NOT NULL COMMENT '类型 agreement=用户协议 privacy=隐私政策',
|
||||||
|
content MEDIUMTEXT NOT NULL COMMENT '正文 (HTML/富文本)',
|
||||||
|
status CHAR(1) DEFAULT '0' COMMENT '状态 0启用 1停用',
|
||||||
|
create_by VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||||
|
create_time DATETIME DEFAULT NULL COMMENT '创建时间',
|
||||||
|
update_by VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||||
|
update_time DATETIME DEFAULT NULL COMMENT '更新时间',
|
||||||
|
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_biz_article_type (type, status)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台协议及隐私政策文章表';
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- 七大专项计划表 (2026-08-16 新增)
|
||||||
|
-- 首页 "七大专项计划" 区块改由 admin 后台维护
|
||||||
|
-- content_type: rich=富文本 / file=上传文件 (PDF/PNG)
|
||||||
|
-- ============================================================================
|
||||||
|
CREATE TABLE biz_special_plan (
|
||||||
|
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '专项计划ID',
|
||||||
|
title VARCHAR(200) NOT NULL COMMENT '专项计划名称',
|
||||||
|
content_type VARCHAR(20) NOT NULL DEFAULT 'rich' COMMENT '内容类型 rich=富文本 file=上传文件',
|
||||||
|
content MEDIUMTEXT DEFAULT NULL COMMENT '富文本内容',
|
||||||
|
file_url VARCHAR(500) DEFAULT NULL COMMENT '上传文件URL (PDF/PNG)',
|
||||||
|
sort_order INT DEFAULT 0 COMMENT '排序, 从小到大',
|
||||||
|
status CHAR(1) DEFAULT '0' COMMENT '状态 0启用 1停用',
|
||||||
|
create_by VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||||
|
create_time DATETIME DEFAULT NULL COMMENT '创建时间',
|
||||||
|
update_by VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||||
|
update_time DATETIME DEFAULT NULL COMMENT '更新时间',
|
||||||
|
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_special_plan_status (status, sort_order)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='七大专项计划表';
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS = 1;
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- 平台协议/隐私政策文章表 (2026-08-16 新增)
|
||||||
|
-- type: agreement=用户协议, privacy=隐私政策
|
||||||
|
-- ============================================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS biz_article (
|
||||||
|
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '文章ID',
|
||||||
|
title VARCHAR(200) NOT NULL COMMENT '标题',
|
||||||
|
type VARCHAR(20) NOT NULL COMMENT '类型 agreement=用户协议 privacy=隐私政策',
|
||||||
|
content MEDIUMTEXT NOT NULL COMMENT '正文 (HTML/富文本)',
|
||||||
|
status CHAR(1) DEFAULT '0' COMMENT '状态 0启用 1停用',
|
||||||
|
create_by VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||||
|
create_time DATETIME DEFAULT NULL COMMENT '创建时间',
|
||||||
|
update_by VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||||
|
update_time DATETIME DEFAULT NULL COMMENT '更新时间',
|
||||||
|
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_biz_article_type (type, status)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台协议及隐私政策文章表';
|
||||||
|
|
||||||
|
-- 初始数据
|
||||||
|
INSERT INTO biz_article(title, type, content, status, create_by, create_time, remark) VALUES
|
||||||
|
('用户服务协议', 'agreement',
|
||||||
|
'<h2>用户服务协议</h2><p>欢迎使用 BAHIM 项目管理平台。请仔细阅读本协议, 注册即视为同意全部条款。</p><p>1. 用户应保证所提供资料真实有效。</p><p>2. 用户应妥善保管账号密码。</p><p>3. 平台保留最终解释权。</p>',
|
||||||
|
'0', 'admin', NOW(), '注册页底部 [我已阅读并同意《用户协议》] 链接指向此处'),
|
||||||
|
('隐私政策', 'privacy',
|
||||||
|
'<h2>隐私政策</h2><p>BAHIM 平台高度重视用户隐私, 严格按照法律法规要求保护您的个人信息。</p><p>1. 收集信息范围: 注册手机号、姓名、单位名称等业务必要字段。</p><p>2. 信息用途: 仅用于项目协作, 不会用于商业推广或对外披露。</p><p>3. 您的权利: 可随时在账号信息页查看和更正个人信息。</p>',
|
||||||
|
'0', 'admin', NOW(), '注册页底部 [《隐私政策》] 链接指向此处');
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- 七大专项计划表 (2026-08-16 新增)
|
||||||
|
-- 首页 "七大专项计划" 区块改由 admin 后台维护, 内容可选 富文本/上传文件
|
||||||
|
-- content_type: rich=富文本 / file=上传文件 (PDF/PNG)
|
||||||
|
-- ============================================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS biz_special_plan (
|
||||||
|
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '专项计划ID',
|
||||||
|
title VARCHAR(200) NOT NULL COMMENT '专项计划名称',
|
||||||
|
content_type VARCHAR(20) NOT NULL DEFAULT 'rich' COMMENT '内容类型 rich=富文本 file=上传文件',
|
||||||
|
content MEDIUMTEXT DEFAULT NULL COMMENT '富文本内容',
|
||||||
|
file_url VARCHAR(500) DEFAULT NULL COMMENT '上传文件URL (PDF/PNG)',
|
||||||
|
sort_order INT DEFAULT 0 COMMENT '排序, 从小到大',
|
||||||
|
status CHAR(1) DEFAULT '0' COMMENT '状态 0启用 1停用',
|
||||||
|
create_by VARCHAR(64) DEFAULT '' COMMENT '创建者',
|
||||||
|
create_time DATETIME DEFAULT NULL COMMENT '创建时间',
|
||||||
|
update_by VARCHAR(64) DEFAULT '' COMMENT '更新者',
|
||||||
|
update_time DATETIME DEFAULT NULL COMMENT '更新时间',
|
||||||
|
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_special_plan_status (status, sort_order)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='七大专项计划表';
|
||||||
|
|
||||||
|
-- 初始化 7 个专项 (标题参考首页原硬编码, sort_order 与首页 01-07 对齐)
|
||||||
|
INSERT INTO biz_special_plan(title, content_type, content, sort_order, status, create_by, create_time) VALUES
|
||||||
|
('规范化诊疗及医疗质量提升专项计划', 'rich',
|
||||||
|
'<h3>《规范化诊疗及医疗质量提升专项计划(2026年)》</h3><p>推进临床路径标准化, 加强诊疗规范培训与质量评估, 建立多学科协作机制, 持续提升医疗服务水平。</p>',
|
||||||
|
1, '0', 'admin', NOW()),
|
||||||
|
('科研创新专项行动计划', 'rich',
|
||||||
|
'<h3>《科研创新专项行动计划(2026-2030年)》</h3><p>支持医学科研创新,推动整合医学研究成果转化与产业化应用,构建协同创新生态。</p>',
|
||||||
|
2, '0', 'admin', NOW()),
|
||||||
|
('医疗卫生人才培育专项行动计划', 'rich',
|
||||||
|
'<h3>《医疗卫生人才培育专项行动计划(2026-2030年)》</h3><p>建立多层次医学人才培养体系,重点加强基层医疗人才与跨学科复合型人才建设。</p>',
|
||||||
|
3, '0', 'admin', NOW()),
|
||||||
|
('医院管理及高质量发展促进专项计划', 'rich',
|
||||||
|
'<h3>《医院管理及高质量发展促进专项计划(2026-2030年)》</h3><p>聚焦医院管理创新与运营效率, 推广现代化管理工具, 促进医疗机构高质量发展。</p>',
|
||||||
|
4, '0', 'admin', NOW()),
|
||||||
|
('社会公益与可及性提升专项计划', 'rich',
|
||||||
|
'<h3>《社会公益与可及性提升专项计划(2026-2030年)》</h3><p>扩大优质医疗资源覆盖, 推动健康知识普及, 提升基层医疗可及性与公平性。</p>',
|
||||||
|
5, '0', 'admin', NOW()),
|
||||||
|
('政学协作综合项目专项计划', 'rich',
|
||||||
|
'<h3>《政学协作综合项目专项计划(2026-2030年)》</h3><p>深化政府、学会、医疗机构三方协作, 推动政策落地与学术成果转化, 打造协同创新示范。</p>',
|
||||||
|
6, '0', 'admin', NOW()),
|
||||||
|
('组织建设与内部治理专项计划', 'rich',
|
||||||
|
'<h3>《组织建设与内部治理专项计划(2026-2030年)》</h3><p>完善学会组织架构与制度体系, 加强内部治理与人才培养, 提升学会综合服务能力。</p>',
|
||||||
|
7, '0', 'admin', NOW());
|
||||||
Reference in New Issue
Block a user