29 Commits
Author SHA1 Message Date
lianlonggangandClaude 8152c4c559 feat(Banner管理): 轮播图新增跳转方式与跳转链接字段
- 列表新增"跳转方式"、"跳转链接"两列
- 表单新增跳转方式单选(不跳转/链接/app内),选"链接"时跳转链接必填

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-01 11:47:01 +08:00
lianlonggang 141cf2b856 feat(personalData): 去掉客户相关字样 2026-08-06 17:54:41 +08:00
lianlonggang 97d51c05ea feat(personalData): 去掉客户相关字样 2026-08-05 19:27:49 +08:00
lianlonggangandClaude 380c495f40 feat(消息管理): 新增员工电话列展示与搜索项
- 列表新增「员工电话」列,从esContent提取3~4~4格式手机号显示纯数字
- 搜索表单新增「员工电话」项,参数传后台由后端还原波浪线后模糊查询

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-16 16:01:46 +08:00
weixin 1da2e17514 fix(消息管理): 消息列表和搜索表单"接收人"字段更名为"发送人"
- 消息管理模块:列表列标题 esReceiver 从"接收人"改为"发送人",搜索表单 esReceiver 字段标签同步修正
2026-07-16 13:39:21 +08:00
lianlonggang 0241e7b083 feat: 新增 apple-app-site-association 配置 iOS Universal Links 2026-07-14 11:38:34 +08:00
wanghao 5d7df50e25 feat(emergency): 急救宣教推送改为后端推 article 卡片
前端不再通过 postMessage 推送自定义消息,改由后端 push/send
接口直接推送 businessID=article 的自定义消息到 IM 会话。iweb
端新增 article 卡片渲染分支(左缩略图 + 右标题摘要横向布局,
max-width 280px),并加消息接收调试日志。

同时切换开发环境后台地址到 10.10.10.228:19996。
2026-07-07 17:40:50 +08:00
weixin 48eaec6393 fix(健康检测-用户数据): 睡眠折线图按阶段分段显示,各段间不再连续连线
每段睡眠数据(深睡/浅睡/清醒/零星小睡)之间插入 null 断点,
ECharts 折线图在 null 处自动断开,使不同睡眠阶段呈现为独立线段。
2026-06-30 17:54:49 +08:00
wanghao 375011991e refactor(label): 清理标签分类页面未使用代码与废弃函数
删除弹窗状态变量、reactive 导入、废弃函数,提升可读性。
2026-06-26 14:43:54 +08:00
wanghao 9c6680146f feat(resource): 对接急救宣教资源管理 V3 接口
- 审批与上架分离:已通过状态显示「已通过」+「上架」操作
- 分类扁平化:分类表单去掉父级分类字段
- 标签从分类 tags 字段联动取,不再调独立标签接口
- 标签表单加所属分类必填,支持从分类「+标签」按钮预设锁定
- 审核记录字段对齐后端:resourceTitle/auditRejectReason
- 资源表单 footer 隐藏默认按钮,写操作 API 关全局提示避免重复弹
2026-06-25 10:30:22 +08:00
wanghao c01fd0741d Merge remote-tracking branch 'origin/xc-new' into xc-new 2026-06-24 18:01:14 +08:00
wanghao 6bb4b97ff4 feat(emergency): 实现急救宣教推送到 IM 会话功能
- 新增 FirstAidPush 组件:分类筛选 + 搜索 + 单条/一键推送
- IM 端切换会话时 postMessage 携带 orderId 通知父页面
- 父页面监听 sessionChanged 联动推送按钮与后端调用
- 推送调后端 push/send 记录状态 + IM postMessage 推卡片
- 资源管理正文改富文本编辑器,修批量操作重复提示
- 重建 iweb 产物
2026-06-24 17:23:36 +08:00
wanghao a2a0e274f4 feat(resource): 新增急救宣教资源管理、分类标签、审核管理模块
- 资源管理:列表筛选 + 新增/编辑弹窗 + 移动端预览 + 版本历史,已接入后端 RESTful 接口
- 分类与标签:左侧分类树(支持新增子分类/编辑/删除)+ 右侧标签云管理
- 审核管理:待审核/已通过/已驳回/审核记录四个 tab,驳回原因弹窗已接入审核接口
- 开发环境 API 地址切换为同事本地后台
2026-06-23 17:54:28 +08:00
lianlonggangandClaude 0fa2e605a5 feat(watch): 部门数据统计表格新增关爱人员期初未佩戴人数列
Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-18 17:25:17 +08:00
lianlonggang a47c2c7400 feat(personalData): 去掉部分电话规则校验 2026-05-15 16:46:33 +08:00
lianlonggang 8e7b857dbd feat(personalData): 优化个人数据弹窗布局与无数据提示展示
- 调整顶部操作栏布局为 space-between,增加垂直居中对齐
- 新增无数据提示文字(noDataTip)展示区域,位于日期切换按钮左侧
2026-05-15 09:46:33 +08:00
lianlonggang 8044ce0461 style(healthMonitor,user): 统一代码格式,清理多余空白字符
- 规范 sectoralData.data.ts 文件的代码缩进与空行
- 规范 HealthUserEmployeeEx.data.ts 文件的代码格式
- 规范 HealthUserEmployeeExList.vue 文件的模板格式
2026-05-14 10:03:38 +08:00
lianlonggang 592ffe1f50 fix(healthMonitor): 最后一次更新 2026-05-09 20:37:19 +08:00
lianlonggang eeb1496483 feat(upload): 配置文件上传组件的业务路径和文本提示
- 在 JImageUpload 组件中添加 bizPath 属性以指定上传路径
- 为头像上传功能统一设置 bizPath: 'avatar'
- 为图片上传功能设置 bizPath: 'img'
- 为视频上传功能设置 bizPath: 'video'
- 为PDF文件上传设置 bizPath: 'bizfile'
- 为APP文件上传设置 bizPath: 'appfile'
- 添加 text 属性统一显示 '上传头像' 提示文本
- 在系统横幅管理中配置图片上传的业务路径
- 为新资源管理模块添加完整的数据结构定义
2026-04-30 08:56:14 +08:00
lianlonggang 3da41ae2d3 fix(emergency): 解决消息解析异常问题
- 添加了对消息数据的类型检查
- 添加了JSON解析的异常捕获处理
- 防止非字符串数据导致的解析错误
- 确保消息处理的健壮性
2026-04-10 14:43:29 +08:00
lianlonggang 7106e99a7b fix(chat): 修复消息气泡组件头像加载及位置服务地图链接
- 实现头像加载失败处理机制,添加默认头像回退方案
- 使用 ref 状态追踪头像加载失败情况,避免 watchEffect 重置问题
- 更新位置服务地图链接,从腾讯地图切换到高德地图服务
- 配置 Vue 构建依赖转译,解决 element-plus 兼容性问题
- 移除多个无用的 SVG 图标文件,精简项目资源
2026-04-10 14:43:08 +08:00
lianlonggang c1a56aefd9 style(UserSelectByDepModal): 优化部门选择用户弹窗样式
- 将左侧部门树卡片的 minHeight 改为固定 height: 613px
- 将 overflow: auto 改为 overflowY: auto,避免横向滚动条
2026-04-01 15:31:19 +08:00
lianlonggang 4f848e7ce9 fix(emergency,healthMonitor): 修复通知消息弹窗重复提交问题并优化页面细节
- 为通知消息弹窗的\"保存并发送\"和\"保存\"按钮添加 loading 状态和 disabled 禁用,防止重复提交
- 优化应急管理列表页面和 AED 设备地图页面的代码格式
2026-04-01 14:48:34 +08:00
lianlonggang a9db38e5e1 修复手表数据日历展示问题,屏蔽非当前月的日期,切换日期发生年份变化时获取新年份月数据 2026-03-30 18:53:07 +08:00
weixin 80627d24c0 update 2026-03-24 10:14:26 +08:00
weixin a24c936cde update
1.修改健康银行访问链接,测试环境截取掉:+端口
2026-03-24 10:03:33 +08:00
weixin 5db0c7effc update
1.修改健康银行访问链接
2026-03-24 09:21:27 +08:00
weixin 28707b346e update
1.修改员工工号正则,emp_sysno为EMP_SYSNO
2026-03-19 16:09:23 +08:00
lianlonggang 8eff9119c7 信创改造,处理写死的表名 2026-03-16 17:37:48 +08:00
99 changed files with 29785 additions and 309 deletions
+2 -2
View File
@@ -10,7 +10,7 @@ VITE_GLOB_APP_TITLE_JD = 健康干预后台
VITE_GLOB_APP_SHORT_NAME = CqytHealthAdmin
# 单点登录服务端地址
VITE_GLOB_APP_CAS_BASE_URL=http://cas.test.com:8443/cas
VITE_GLOB_APP_CAS_BASE_URL=
# 是否开启单点登录
VITE_GLOB_APP_OPEN_SSO = false
@@ -19,6 +19,6 @@ VITE_GLOB_APP_OPEN_SSO = false
VITE_GLOB_APP_OPEN_QIANKUN=true
# 文件预览地址
VITE_GLOB_ONLINE_VIEW_URL=http://fileview.jeecg.com/onlinePreview
VITE_GLOB_ONLINE_VIEW_URL=
+2 -2
View File
@@ -16,10 +16,10 @@ VITE_BUILD_COMPRESS = 'gzip'
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false
#后台接口父地址(必填)
VITE_GLOB_API_URL=http://cqyt.dev.yg.dt.io
VITE_GLOB_API_URL=http://10.8.0.4:29990
#后台接口全路径地址(必填)
VITE_GLOB_DOMAIN_URL=http://cqyt.dev.yg.dt.io
VITE_GLOB_DOMAIN_URL=http://10.8.0.4:29990
#食堂接口前缀
VITE_GLOB_ST_DOMAIN_URL=http://113.200.64.105:9002
+4 -2
View File
@@ -11,12 +11,14 @@ VITE_PUBLIC_PATH = /
VITE_DROP_CONSOLE = false
#后台接口父地址(必填)
VITE_GLOB_API_URL=https://xj-api.mcrm.vip:8888
VITE_GLOB_API_URL=http://10.10.10.228:19996
#VITE_GLOB_API_URL=https://xj-api.mcrm.vip:8888
#VITE_GLOB_API_URL=http://192.168.1.223
#VITE_GLOB_API_URL=http://cqyt.test.yg.dt.io
#后台接口全路径地址(必填)
VITE_GLOB_DOMAIN_URL=https://xj-api.mcrm.vip:8888
VITE_GLOB_DOMAIN_URL=http://10.10.10.228:19996
#VITE_GLOB_DOMAIN_URL=https://xj-api.mcrm.vip:8888
#VITE_GLOB_DOMAIN_URL=http://192.168.1.223
#VITE_GLOB_DOMAIN_URL=http://cqyt.test.yg.dt.io
# 接口前缀
+6 -6
View File
@@ -16,14 +16,14 @@ VITE_BUILD_COMPRESS = 'gzip'
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false
#后台接口父地址(必填)
VITE_GLOB_API_URL=https://xj-api.mcrm.vip:8888
VITE_GLOB_API_URL=https://test.xjygjk.com
#后台接口全路径地址(必填)
VITE_GLOB_DOMAIN_URL=https://xj-api.mcrm.vip:8888
VITE_GLOB_DOMAIN_URL=https://test.xjygjk.com
#食堂接口前缀
VITE_GLOB_ST_DOMAIN_URL=https://yyjk.cqygjk.com/gateway
VITE_GLOB_ST_DOMAIN_URL=
#西安、银川应急大屏
VITE_EMERGENCY_SCREEN=https://screen.cqygjk.com/
VITE_EMERGENCY_SCREEN=
# 接口父路径前缀
VITE_GLOB_API_URL_PREFIX=
@@ -37,8 +37,8 @@ VITE_USE_PWA = false
# 是否兼容旧浏览器
VITE_LEGACY = false
# 油田平台CODE
# 平台CODE
VITE_PLATFORM = 'YT'
# 跳转外部链接
VITE_GLOB_OUT_URL=https://console-jkglpt.iosp.ydpt.tech/hb/healthadmin/index.html?param=
VITE_GLOB_OUT_URL=https://test.com
+1
View File
@@ -3,6 +3,7 @@ node_modules
.github
.gitee
dist
renkang
.npmrc
.cache
+7 -3
View File
@@ -3,9 +3,9 @@ MAINTAINER jeecgos@163.com
VOLUME /tmp
ENV LANG en_US.UTF-8
ENV VITE_GLOB_API_URL https://xj-api.mcrm.vip:8888
ENV VITE_GLOB_DOMAIN_URL https://xj-api.mcrm.vip:8888
ENV VITE_GLOB_OUT_URL https://gstest.superwx.cn/healthapiadmin/index.html?param=
ENV VITE_GLOB_API_URL http://10.10.10.228:19996
ENV VITE_GLOB_DOMAIN_URL http://10.10.10.228:19996
ENV VITE_GLOB_OUT_URL https://gstest.superwx.cn/healthcheckadmin/index.html?param=
RUN echo "server { \
#解决Router(mode: 'history')模式下,刷新路由地址不能找到页面的问题 \
@@ -17,6 +17,10 @@ RUN echo "server { \
break; \
} \
} \
location = /.well-known/apple-app-site-association { \
root /var/www/html; \
default_type application/json; \
} \
access_log /var/log/nginx/access.log ; \
} " > /etc/nginx/conf.d/default.conf \
&& mkdir -p /var/www \
@@ -6,8 +6,7 @@
<!-- onerror="this.src='https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"-->
<!-- alt=""-->
<!-- />-->
<img class="avatar" v-if="getSrc(message)" :src="getSrc(message)" alt="" />
<img class="avatar" v-else src="@/assets/image/default_avatar.png" alt="" />
<img class="avatar" :src="avatarError ? defaultAvatar : (message?.avatar || defaultAvatar)" @error="onAvatarError" alt="" />
<main class="message-area">
<label class="name" v-if="message.flow === 'in' && message.conversationType === 'GROUP'">
{{ message.nameCard || message.nick || message.from }}
@@ -86,6 +85,7 @@
import MessageReference from './message-reference.vue';
import { Message } from '../interface';
import { TUIEnv } from '../../../../../TUIKit/TUIPlugin';
import defaultAvatar from '@/assets/image/default_avatar.png';
import MessageEmojiReact from './message-emoji-react.vue';
import TIM from '../../../../TUICore/tim/index';
@@ -145,6 +145,15 @@
needEmojiReact: false,
});
// 追踪头像加载失败状态,避免 watchEffect 重置时覆盖 @error 的修复
const avatarError = ref(false);
watch(() => (props.data as any)?.ID, () => {
avatarError.value = false;
});
const onAvatarError = () => {
avatarError.value = true;
};
watchEffect(() => {
data.type = constant;
data.messagesList = props.messagesList;
@@ -376,6 +385,9 @@
return {
getSrc,
defaultAvatar,
avatarError,
onAvatarError,
...toRefs(data),
toggleDialog,
htmlRefHook,
@@ -94,6 +94,15 @@
</div>
</div>
</template>
<template v-else-if="isCustom.businessID === constant.typeArticle">
<div class="article-card">
<img v-if="isCustom?.messageImageUrl" :src="isCustom?.messageImageUrl" class="article-cover" />
<div class="article-info">
<div class="article-title">{{ isCustom?.title }}</div>
<div v-if="isCustom?.content" class="article-content">{{ isCustom?.content }}</div>
</div>
</div>
</template>
<template v-else-if="isCustom.businessID === constant.typeBusinessCard">
<div style="cursor: pointer; min-width: 180px">
<div class="business-type">
@@ -372,6 +381,56 @@
}
}
.article-card {
display: flex;
align-items: center;
gap: 8px;
max-width: 280px;
padding: 8px;
border-radius: 8px;
background: #f7f7f7;
border: 1px solid #ececec;
.article-cover {
width: 60px;
height: 60px;
border-radius: 6px;
flex-shrink: 0;
object-fit: cover;
}
.article-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.article-title {
font-size: 14px;
font-weight: 500;
color: #333;
line-height: 1.3;
word-break: break-all;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.article-content {
font-size: 12px;
color: #7d7d7d;
line-height: 1.3;
word-break: break-all;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
.img-list {
img {
width: 100px;
@@ -115,6 +115,14 @@ export default class TUIChatServer extends IComponentServer {
}
private handleMessageReceived(event: any) {
// 调试日志:打印收到的所有消息,看后端推送的是什么格式
console.log('[IM 收到消息]', event?.data?.map((m: any) => ({
type: m?.type,
text: m?.payload?.text,
data: m?.payload?.data,
description: m?.payload?.description,
extension: m?.payload?.extension,
})));
event?.data?.forEach((message: Message) => {
if (message?.conversationID === this?.store?.conversation?.conversationID) {
this.currentStore.messageList = [...this.currentStore.messageList, message];
@@ -269,14 +269,16 @@ export function handleLocationMessageShowContext(item: any) {
};
location.lon = item.payload.longitude.toFixed(6);
location.lat = item.payload.latitude.toFixed(6);
// 跳转高德地图标记点页面
location.href =
'https://map.qq.com/?type=marker&isopeninfowin=1&markertype=1&' +
`pointx=${location.lon}&pointy=${location.lat}&name=${item.payload.description}`;
'https://uri.amap.com/marker?' +
`position=${location.lon},${location.lat}&name=${item.payload.description}&src=mypage&coordinate=gaode&callnative=0`;
// 高德静态地图图片
location.url =
'https://apis.map.qq.com/ws/staticmap/v2/?' +
`center=${location.lat},${location.lon}&zoom=10&size=300*150&maptype=roadmap&` +
`markers=size:large|color:0xFFCCFF|label:k|${location.lat},${location.lon}&` +
'key=UBNBZ-PTP3P-TE7DB-LHRTI-Y4YLE-VWBBD';
'https://restapi.amap.com/v3/staticmap?' +
`location=${location.lon},${location.lat}&zoom=10&size=300*150&` +
`markers=large,,A:${location.lon},${location.lat}&` +
'key=8498ba3ba280654a7f6891b617f7d6ef';
location.description = item.payload.description;
return location;
}
@@ -151,6 +151,19 @@
store.commit('setOrderId', item.children.orderId);
store.commit('setSessionId', item.children.sessionId);
store.commit('setConsultInfo', { consultType: item.type, consultStatus: item?.children?.contentStatus });
// 通知父页面会话切换,携带 orderId 用于急救宣教推送等场景
try {
window.parent.postMessage(
JSON.stringify({
code: 'sessionChanged',
orderId: item.children.orderId,
sessionId: item.children.sessionId,
}),
'*'
);
} catch (e) {
console.log(e);
}
const it = item.children;
if (hrefParams.isSpecialized === 'expert') {
store.commit(
@@ -31,6 +31,7 @@ const constant: any = {
typeIphone: 'iphone',
typeMini: 'miniprogram',
typeSystem: 'system',
typeArticle: 'article',
typeBusinessCard: 'business_card',
typeArchives: 'archives',
typeMedicalExaminationReport: 'medical_examination_report',
+1
View File
@@ -9,6 +9,7 @@ module.exports = {
outputDir: '../public/static/im-web',
productionSourceMap: false,
lintOnSave: false,
transpileDependencies: ['element-plus', 'vue-router', '@ctrl/tinycolor'],
chainWebpack: (config) => {
config.resolve.alias.set('@', resolve('src')).set('tim', resolve('src/tim.js'));
// 删除预加载
+1 -1
View File
@@ -5,7 +5,7 @@
<van-collapse-item name="1">
<template #title>
<CardTitle title="我的岗位健康">
<div class="Depart-info">采气工 财务部主任</div>
<div class="Depart-info">财务部主任</div>
</CardTitle>
</template>
<div class="user-unit-content" :class="[toggleCard ? 'fold' : 'unfold']">
@@ -221,7 +221,7 @@
break;
case '否':
state.questionnaireIndex = state.questionnaireData.qsList.findIndex((item) => {
return item.qsDes === '您是否感觉因油田工作与社会脱节';
return item.qsDes === '您是否感觉因工作与社会脱节';
});
break;
default:
+26425
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
{
"applinks": {
"apps": [],
"details": [
{
"appID": "A7CC5LW224.com.questionTools.XinjiangProject",
"paths": ["*"]
}
]
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 731 B

After

Width:  |  Height:  |  Size: 731 B

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48px" height="48px" viewBox="0 0 48 48" version="1.1">
<title>ic_back_white</title>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="02.-查看信息-示例好友" transform="translate(-32.000000, -200.000000)">
<g id="编组-6" transform="translate(0.000000, 176.000000)">
<g id="ic_back_white" transform="translate(32.000000, 24.000000)">
<g id="ic_back_black" fill="#444444" fill-rule="nonzero">
<path d="M9.51231071,24 L23.5198286,38.7290411 C23.785378,39.0082689 23.7753168,39.4496082 23.4973187,39.7164448 L21.6255915,41.5130226 C21.3466827,41.7807332 20.9035602,41.7716553 20.6358496,41.4927466 C20.6351032,41.491969 20.6343586,41.4911896 20.6336157,41.4904085 L4.45876399,24.4823948 C4.2017544,24.2121467 4.2017544,23.7878533 4.45876399,23.5176052 L20.6336157,6.50959152 C20.900035,6.22944906 21.3431109,6.21832422 21.6232533,6.48474351 C21.6240344,6.48548634 21.6248138,6.48623097 21.6255915,6.48697741 L23.4973187,8.28355524 C23.7753168,8.55039175 23.785378,8.99173114 23.5198286,9.27095893 L9.51231071,24 L9.51231071,24 Z" id="Path-2"></path>
</g>
<rect id="矩形" x="0" y="0" width="48" height="48"></rect>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 16 16" version="1.1">
<title>清除</title>
<g id="页面-2备份" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="创建群聊" transform="translate(-1243.000000, -413.000000)">
<g id="Group-1364" transform="translate(650.000000, 343.000000)">
<g id="Group-1363" transform="translate(290.000000, 46.000000)">
<g id="Group-1358" transform="translate(0.000000, 24.000000)">
<g id="清除" transform="translate(303.000000, 0.000000)">
<path d="M0,0 L16,0 L16,16 L0,16 L0,0 Z" id="矩形"></path>
<path d="M1,8 C1,11.8659999 4.13400006,15 8,15 C11.8659999,15 15,11.8659999 15,8 C15,4.13400006 11.8659999,1 8,1 L8,1 C4.13400006,1 1,4.13400006 1,8 Z" fill="#999999"></path>
<g id="编组" transform="translate(8.000000, 8.000000) rotate(-315.000000) translate(-8.000000, -8.000000) translate(4.000000, 4.000000)" fill="#FFFFFF">
<rect id="矩形备份" transform="translate(4.000000, 4.000000) rotate(-90.000000) translate(-4.000000, -4.000000) " x="1.8189894e-12" y="3.5" width="8" height="1" rx="0.5"></rect>
<rect id="矩形备份" transform="translate(4.000000, 4.000000) rotate(-360.000000) translate(-4.000000, -4.000000) " x="0" y="3.5" width="8" height="1" rx="0.5"></rect>
</g>
<rect id="矩形" x="0" y="0" width="16" height="16"></rect>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

Before

Width:  |  Height:  |  Size: 536 B

After

Width:  |  Height:  |  Size: 536 B

Before

Width:  |  Height:  |  Size: 520 B

After

Width:  |  Height:  |  Size: 520 B

Before

Width:  |  Height:  |  Size: 783 B

After

Width:  |  Height:  |  Size: 783 B

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before

Width:  |  Height:  |  Size: 801 B

After

Width:  |  Height:  |  Size: 801 B

Before

Width:  |  Height:  |  Size: 711 B

After

Width:  |  Height:  |  Size: 711 B

Before

Width:  |  Height:  |  Size: 513 B

After

Width:  |  Height:  |  Size: 513 B

Before

Width:  |  Height:  |  Size: 517 B

After

Width:  |  Height:  |  Size: 517 B

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before

Width:  |  Height:  |  Size: 937 B

After

Width:  |  Height:  |  Size: 937 B

Before

Width:  |  Height:  |  Size: 647 B

After

Width:  |  Height:  |  Size: 647 B

@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 16 16" version="1.1">
<title>编组 14</title>
<defs>
<filter x="-10.6%" y="-5.4%" width="121.2%" height="110.9%" filterUnits="objectBoundingBox" id="filter-1">
<feOffset dx="0" dy="7" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="10" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
</defs>
<g id="new" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="自定义消息" transform="translate(-458.000000, -318.000000)">
<g id="编组-32" filter="url(#filter-1)" transform="translate(44.000000, 60.000000)">
<g id="编组-24" transform="translate(30.000000, 250.000000)">
<g id="编组-14" transform="translate(384.000000, 8.000000)">
<circle id="椭圆形" fill="#006EFF" fill-rule="nonzero" cx="8" cy="8" r="8"></circle>
<polyline id="路径-4" stroke="#FFFFFF" stroke-width="2" transform="translate(8.042641, 6.242641) rotate(-315.000000) translate(-8.042641, -6.242641) " points="6.04264069 10.2426407 10.0426407 10.2426407 10.0426407 2.24264069"></polyline>
</g>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 281 B

After

Width:  |  Height:  |  Size: 281 B

+2 -2
View File
@@ -1,4 +1,4 @@
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"><link rel="icon" href="faviconnew.png"><title>即时通信</title><link href="css/vendors~app.03806786.css" rel="stylesheet"><link href="css/app.0de947d6.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but 腾讯云即时通信 Web-IM doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="./TCaptcha.js"></script><script src="./aegis.min.js"></script><script>/**
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"><link rel="icon" href="faviconnew.png"><title>即时通信</title><link href="css/vendors~app.3f294141.css" rel="stylesheet"><link href="css/app.f83e8594.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but 腾讯云即时通信 Web-IM doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="./TCaptcha.js"></script><script src="./aegis.min.js"></script><script>/**
* 根据基础字体大小 和 效果图宽度设置 根节点字体大小
* @param baseFontSize 浏览器默认字体大小 [注]浏览器最小字体为12像素,所以该值必需大于等于12
* @param baseWidth 设计稿的尺寸
@@ -53,4 +53,4 @@
},
false
);
};</script><script src="js/vendors~app.de88239f.js"></script><script src="js/app.8ff38855.js"></script></body></html>
};</script><script src="js/vendors~app.be028cd5.js"></script><script src="js/app.ece2c62c.js"></script></body></html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+12 -13
View File
@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<title>健康油我用户协议</title>
<title>用户协议</title>
</head>
<style>
html,body {
@@ -38,17 +38,16 @@
</style>
<body>
<div id="zwb">
<h3 class="title-one">健康油我用户协议</h3>
<h3 class="title-one">软件使用许可协议(健康油我)</h3>
<h3 class="title-one">软件使用用户协议</h3>
<h3 class="title-one">软件使用许可协议</h3>
<div>欢迎使用我们的服务</div>
<div>本软件许可使用协议《健康油我平台服务协议》(以下称"本协议")由您与中国石油天然气股份有限公司新疆油田分公司行政事务管理处(以下称“新疆油田”)共同签署。</div>
<div>在使用“健康油我”软件(以下称许可软件)之前,请仔细阅读本协议。一旦您下载安装使用许可软件,即表示您同意接受本协议所有条款和条件的约束。如您不同意本协议条款和条件,请勿使用许可软件,并请销毁所有许可软件副本。</div>
<div>健康油我有权根据实际运营的情况对本协议不时进行调整,并以平台公告或电子邮件等书面形式通知您。修改后的协议于公示或邮件送达时生效。协议条款修改后,您继续使用许可软件的,即视为您已阅读并接受修改后的协议。</div>
<div>本软件许可使用协议《平台服务协议》(以下称"本协议")由您与本司共同签署。</div>
<div>在使用本软件软件(以下称许可软件)之前,请仔细阅读本协议。一旦您下载安装使用许可软件,即表示您同意接受本协议所有条款和条件的约束。如您不同意本协议条款和条件,请勿使用许可软件,并请销毁所有许可软件副本。</div>
<div>本软件有权根据实际运营的情况对本协议不时进行调整,并以平台公告或电子邮件等书面形式通知您。修改后的协议于公示或邮件送达时生效。协议条款修改后,您继续使用许可软件的,即视为您已阅读并接受修改后的协议。</div>
<div>一、定义</div>
<div>1. 健康油我APP:是指由新疆油田开发的,供您从应用下载平台(以下称下载平台)下载,并仅限在手机等手持移动终端(以下称移动终端)中安装、使用的软件系统。</div>
<div>2. 健康油我服务:由新疆油田为您提供的服务。您可以通过许可软件在手持移动终端使用健康油我服务。</div>
<div>3. 用户:指阅读并同意本协议内容,经过健康油我在线注册程序成为健康油我在线各项服务使用者。(以下更多称为“您”)</div>
<div>1. APP:是指由本司开发的,供您从应用下载平台(以下称下载平台)下载,并仅限在手机等手持移动终端(以下称移动终端)中安装、使用的软件系统。</div>
<div>2. 本软件服务:由本司为您提供的服务。您可以通过许可软件在手持移动终端使用本软件服务。</div>
<div>3. 用户:指阅读并同意本协议内容,经过本软件在线注册程序成为在线各项服务使用者。(以下更多称为“您”)</div>
<div>二、服务说明</div>
<div>1. 本平台是一个集健康咨询、应急就医、健康档案、体检服务等功能的综合个人健康管理平台,为确保您能正常地使用平台的各项服务,您应当阅读并遵守本协议。请您务必审慎阅读、充分理解各条款内容,以及开通或使用某项服务的单独协议,并选择接受或不接受。</div>
<div>2. 除非您已阅读并接受本协议所有条款,否则您无权使用平台的各项服务(以下简称"本服务”)。您在平台的登录、查看、发布信息等行为即视为您已阅读并同意本协议的约束,包括接受更新后的本协议条款。当您与平台发生争议时,应以最新的服务协议为准。</div>
@@ -58,7 +57,7 @@
<div>6. 本协议的条款适用于平台提供的各种服务,但当您使用平台某特定服务时,如该服务另有单独的服务条款、指引或规则,您应遵守本协议及平台随时公布的与该服务相关的服务条款、指引或规则等。前述所有的指引和规则,均构成本协议的一部分。除非本协议另有其他明示规定,新推出的服务、增加或强化目前本服务的新功能,均受到本协议之规范。</div>
<div>7. 如对本协议内容有任何疑问、意见或建议,您可与我们联系。</div>
<div>三、本协议的确认和接纳</div>
<div>1、平台的各项服务的所有权和运作权归新疆油田所有。您同意所有注册协议条款,才能成为平台平台的正式用户。您确认:本协议条款是处理双方权利义务的契约,始终有效,法律另有强制性规定或双方另有特别约定的,依其规定。</div>
<div>1、平台的各项服务的所有权和运作权归本司所有。您同意所有注册协议条款,才能成为平台平台的正式用户。您确认:本协议条款是处理双方权利义务的契约,始终有效,法律另有强制性规定或双方另有特别约定的,依其规定。</div>
<div>2、您点击注册登录,即视为您确认自己具有享受平台服务相应的权利能力和行为能力,能够独立承担法律责任。</div>
<div>3、平台通过互联网为您提供各项服务,并承担本协议和其它服务协议中对您的责任和义务。为使用本服务,您必须能够自行通过有法律资格的第三方对您提供互联网接入服务,并自行承担以下内容:</div>
<div>(1)自行配备上网所需的设备,包括手机、个人电脑、调制解调器及其他必要的设备装置。</div>
@@ -95,8 +94,8 @@
<div>2、 请您在支付费用时,仔细阅读收费服务的相关协议,除协议约定的情况外,否则,平台将不予退费。退费的方式视支付方式的不同而不同。员工应严格依据退费方式及退费周期收回退款。</div>
<div>3、平台可能根据实际需要对收费服务的收费标准、方式进行修改和变更,平台也可能会对部分免费服务变更收费。如果您不同意上述修改、变更或付费内容,则应停止使用该服务。</div>
<div>八、服务暂停、变更与中止条款</div>
<div>1、鉴于移动互联网服务的特殊性,健康油我平台有权随时变更、中止或终上部分或全部的服务。如变更、中止或终止的服务属于免费服务,平台无需通知您,也无需对您或任何第三方承担任何责任。</div>
<div>2、您理解,因不可抗力、网络状况、通讯线路、用户自身过错等技术原因,或平台需要进行定期或不定期地对所提供的服务进行维护升级、对相关的设备进行检修或者维护,如因此类情况而造成本服务在合理时间内的中断,健康油我平台无需为此承担任何责任。</div>
<div>1、鉴于移动互联网服务的特殊性,平台有权随时变更、中止或终上部分或全部的服务。如变更、中止或终止的服务属于免费服务,平台无需通知您,也无需对您或任何第三方承担任何责任。</div>
<div>2、您理解,因不可抗力、网络状况、通讯线路、用户自身过错等技术原因,或平台需要进行定期或不定期地对所提供的服务进行维护升级、对相关的设备进行检修或者维护,如因此类情况而造成本服务在合理时间内的中断,平台无需为此承担任何责任。</div>
<div>3、 如发生下列任何一种情形,平台有权随时中断或终止向您提供本协议项下的服务而无需对您或任何第三方承担任何责任:</div>
<div>(1)您违反国家有关法律法规或平台规定,侵害他人合法权益的;</div>
<div>(2)您提供的个人资料不真实;</div>
@@ -114,7 +113,7 @@
<div>1、 您一旦接受本协议,即表明您在任何时间在平台发表的任何形式的信息内容(包括但不限于员工评价、员工咨询、各类话题文章等信息内容)产生的权利,如著作权财产权(包括并不限于:复制权、发行权、出租权、展览权、表演权、放映权、广播权、信息网络传播权、摄制权、改编权、翻译权、汇编权以及应当由著作权人享有的其他可转让权利), 全部独家且不可撤销地转让给平台所有,您同意平台有权就任何主体侵权而单独提起诉讼。</div>
<div>2、本协议已经构成《中华人民共和国著作权法》第二十五条(条文序号依照2011年版著作权法确定)及相关法律规定的著作财产权等权利转让书面协议,其效力及于您在平台上发布的任何受著作权法保护的作品内容,无论该等内容形成于本协议订立前还是本协议订立后。</div>
<div>3、您同意并已充分了解本协议的条款,承诺不将已发表于平台的信息,以任何形式发布或授权其它主体以任何方式使用(包括但限于在各类网站、媒体上使用)、</div>
<div>4、新疆油田是平台的制作者,拥有平台内容及资源的著作权等合法权利,受国家法律保护,有权不时地对本协议及平台的内容进行修改,并在平台张贴,无须另行通知您。在法律允许的最大限度范围内,平台对本协议及的内容拥有解释权。</div>
<div>4、本司是平台的制作者,拥有平台内容及资源的著作权等合法权利,受国家法律保护,有权不时地对本协议及平台的内容进行修改,并在平台张贴,无须另行通知您。在法律允许的最大限度范围内,平台对本协议及的内容拥有解释权。</div>
<div>5、除法律另有强制性规定外,未经平台明确的特别书面许可,任何单位或个人不得以任何方式非法地全部或部分复制、转载、引用、链接、抓取或以其他方式使用平台的信息内容,否则,平台有权追究其法律责任。</div>
<div>6、平台所刊登的资料信息(诸如文字、图表、标识、按钮图标、图像、声音文件片段、数字下载、数据编辑和软件),均是平台或其内容提供者的财产,受中国和国际版权法的保护。平台上所有内容的汇编是平台的排他财产,受中国和国际版权法的保护。平台上的所有软件都是平台的财产,受中国和国际版权法的保护。</div>
<div>十二、特别授权</div>
@@ -6,8 +6,6 @@
<div class="anticon" :class="getAppLogoClass" @click="goHome">
<div class="ml-2 truncate md:opacity-100" v-show="showTitle">
<!-- <div class="logo-left" v-if="getEnvInfo().VITE_PLATFORM === 'YT'">-->
<!-- <div>长庆</div>-->
<!-- <div>油田</div>-->
<!-- </div>-->
<div>{{ getTitle(title) }}</div>
</div>
@@ -3,7 +3,7 @@
<BasicModal v-bind="$attrs" @register="register" :title="modalTitle" width="1200px" @ok="handleOk" destroyOnClose @visible-change="visibleChange">
<a-row :gutter="10">
<a-col :md="7" :sm="24">
<a-card :style="{ minHeight: '613px', overflow: 'auto' }">
<a-card :style="{ height: '613px', overflowY: 'auto' }">
<!--组织机构-->
<BasicTree
ref="treeRef"
+6 -6
View File
@@ -45,7 +45,7 @@
import { getToken } from '/@/utils/auth';
import { useLoading } from '/@/components/Loading';
import { useGlobSetting } from '/@/hooks/setting';
const { viteOutUrl } = useGlobSetting();
const { viteOutUrl, apiUrl } = useGlobSetting();
export default defineComponent({
name: 'BasicMenu',
@@ -130,16 +130,16 @@
async function handleMenuClick({ item, key }: { item: any; key: string; keyPath: string[] }) {
if (item.title === '健康银行') {
open();
const url = viteOutUrl;
console.log(url);
console.log(useGlobSetting());
await defHttp
.get({
url: '/health-system/api/sys/encryptInfo',
params: {
str: {
scheme: url.split('://')[0],
host: url.split('://')[1],
scheme: apiUrl.split('://')[0],
host:
apiUrl.split('://')[1].indexOf(':') !== -1
? apiUrl.split('://')[1].substring(0, apiUrl.split('://')[1].indexOf(':'))
: apiUrl.split('://')[1],
tokenF: getToken(),
},
},
+1 -1
View File
@@ -20,7 +20,7 @@
fontSize: props.size ?? '30px',
opacity: 0.5,
transition: 'opacity 0.5s',
display: 'none',
display: 'block',
});
const bodyStyle = ref({
+2 -2
View File
@@ -26,12 +26,12 @@ export function isShowIdCard() {
return (res.VITE_PLATFORM !== 'JD') as boolean;
}
// 判断是否为青海
// 判断是否为
export function isQH() {
const res = getEnvInfo();
return (res.VITE_PLATFORM === 'QH') as boolean;
}
// 判断是否为油田
// 判断是否为
export function isYT() {
const res = getEnvInfo();
return (res.VITE_PLATFORM === 'YT') as boolean;
+17
View File
@@ -0,0 +1,17 @@
import { defHttp } from '/@/utils/http/axios';
enum Api {
pendingList = '/health-emergency/emergency/firstAid/audit/pending',
passedList = '/health-emergency/emergency/firstAid/audit/passed',
rejectedList = '/health-emergency/emergency/firstAid/audit/rejected',
recordsList = '/health-emergency/emergency/firstAid/audit/records',
approve = '/health-emergency/emergency/firstAid/audit/approve',
reject = '/health-emergency/emergency/firstAid/audit/reject',
}
export const pendingListApi = (params: any) => defHttp.get({ url: Api.pendingList, params });
export const passedListApi = (params: any) => defHttp.get({ url: Api.passedList, params });
export const rejectedListApi = (params: any) => defHttp.get({ url: Api.rejectedList, params });
export const recordsListApi = (params: any) => defHttp.get({ url: Api.recordsList, params });
export const approveApi = (params: any) => defHttp.post({ url: Api.approve, params });
export const rejectApi = (params: any) => defHttp.post({ url: Api.reject, params });
@@ -0,0 +1,43 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" title="填写驳回原因" :width="520" :minHeight="300" @ok="handleSubmit">
<a-form layout="vertical">
<a-form-item label="驳回原因" required>
<a-textarea v-model:value="reason" :rows="5" placeholder="请填写驳回原因,将通知内容编辑人员修改..." />
</a-form-item>
</a-form>
</BasicModal>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { useMessage } from '/@/hooks/web/useMessage';
import { rejectApi } from '../check.api';
const emit = defineEmits(['success', 'register']);
const { createMessage } = useMessage();
const reason = ref('');
const record = ref<any>({});
const submitting = ref(false);
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
record.value = data?.record || {};
reason.value = '';
});
async function handleSubmit() {
if (!reason.value.trim()) {
createMessage.warning('请填写驳回原因');
return;
}
setModalProps({ confirmLoading: true });
try {
await rejectApi({ id: record.value.id, reason: reason.value });
createMessage.success('已驳回,通知已发送');
emit('success');
closeModal();
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>
+336
View File
@@ -0,0 +1,336 @@
<template>
<div class="check-page">
<a-tabs v-model:activeKey="activeTab" @change="onTabChange">
<a-tab-pane key="pending">
<template #tab>
待审核
<a-badge :count="pendingTotal" :number-style="{ backgroundColor: '#fa8c16' }" :overflow-count="999" />
</template>
<div class="filter-bar">
<a-input v-model:value="pendingFilter.title" placeholder="搜索标题" allow-clear style="width: 240px" @press-enter="loadPending" />
<a-button type="primary" @click="loadPending">查询</a-button>
</div>
<a-spin :spinning="pendingLoading">
<div class="review-list">
<div v-for="item in pendingList" :key="item.id" class="review-card">
<div class="review-card-top">
<div class="review-card-main">
<div class="review-title">{{ item.title }}</div>
<div class="review-meta">
提交人{{ item.createBy || '-' }} | 创建时间{{ item.createTime || '-' }} | 分类{{ item.categoryName || '-' }} | 类型{{ getTypeLabel(item.contentType) }}
</div>
</div>
<div class="review-actions">
<a-button size="small" @click="handlePreview(item)">预览</a-button>
<a-button type="primary" size="small" @click="handlePass(item)">通过</a-button>
<a-button danger size="small" @click="handleReject(item)">驳回</a-button>
</div>
</div>
</div>
<a-empty v-if="!pendingLoading && pendingList.length === 0" description="暂无待审核资源" />
</div>
</a-spin>
</a-tab-pane>
<a-tab-pane key="passed" tab="已通过">
<div class="filter-bar">
<a-input v-model:value="passedFilter.title" placeholder="搜索标题" allow-clear style="width: 240px" @press-enter="loadPassed" />
<a-button type="primary" @click="loadPassed">查询</a-button>
</div>
<a-table
:columns="passedColumns"
:data-source="passedList"
:loading="passedLoading"
:pagination="false"
row-key="id"
size="middle"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a-button type="link" size="small" @click="handlePreview(record)">查看</a-button>
<a-button type="link" size="small" @click="handlePublish(record)">上架</a-button>
</template>
</template>
</a-table>
</a-tab-pane>
<a-tab-pane key="rejected" tab="已驳回">
<div class="filter-bar">
<a-input v-model:value="rejectedFilter.title" placeholder="搜索标题" allow-clear style="width: 240px" @press-enter="loadRejected" />
<a-button type="primary" @click="loadRejected">查询</a-button>
</div>
<a-table
:columns="rejectedColumns"
:data-source="rejectedList"
:loading="rejectedLoading"
:pagination="false"
row-key="id"
size="middle"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a-button type="link" size="small" @click="handleReEdit(record)">重新编辑</a-button>
</template>
</template>
</a-table>
</a-tab-pane>
<a-tab-pane key="records" tab="审核记录">
<div class="filter-bar">
<a-input v-model:value="recordsFilter.title" placeholder="搜索标题" allow-clear style="width: 200px" @press-enter="loadRecords" />
<a-select v-model:value="recordsFilter.auditStatus" placeholder="审核操作" allow-clear style="width: 140px">
<a-select-option :value="10">全部</a-select-option>
<a-select-option :value="11">已通过</a-select-option>
<a-select-option :value="12">已驳回</a-select-option>
</a-select>
<a-range-picker v-model:value="recordsFilter.dateRange" style="width: 240px" />
<a-button type="primary" @click="loadRecords">查询</a-button>
</div>
<a-table
:columns="recordColumns"
:data-source="recordsList"
:loading="recordsLoading"
:pagination="false"
row-key="id"
size="middle"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a-tag :color="record.auditStatus === 11 ? 'green' : 'red'">
{{ record.auditStatus === 11 ? '通过' : '驳回' }}
</a-tag>
</template>
</template>
</a-table>
</a-tab-pane>
</a-tabs>
<PreviewModal @register="registerPreviewModal" />
<RejectReasonModal @register="registerRejectModal" @success="loadPending" />
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { useMessage } from '/@/hooks/web/useMessage';
import { useModal } from '/@/components/Modal';
import {
pendingListApi,
passedListApi,
rejectedListApi,
recordsListApi,
approveApi,
rejectApi,
} from './check.api';
import { publishApi } from '/@/views/resource/resource.api';
import dayjs from 'dayjs';
import { CONTENT_TYPE } from '/@/views/resource/resource.data';
import PreviewModal from '/@/views/resource/components/PreviewModal.vue';
import RejectReasonModal from './components/RejectReasonModal.vue';
const { createMessage, createConfirm } = useMessage();
const activeTab = ref('pending');
const pendingList = ref<any[]>([]);
const pendingTotal = ref(0);
const pendingLoading = ref(false);
const pendingFilter = reactive({ title: '' });
const passedList = ref<any[]>([]);
const passedLoading = ref(false);
const passedFilter = reactive({ title: '' });
const rejectedList = ref<any[]>([]);
const rejectedLoading = ref(false);
const rejectedFilter = reactive({ title: '' });
const recordsList = ref<any[]>([]);
const recordsLoading = ref(false);
const recordsFilter = reactive({
title: '',
auditStatus: undefined as number | undefined,
dateRange: undefined as any,
});
const [registerPreviewModal, { openModal: openPreviewModal }] = useModal();
const [registerRejectModal, { openModal: openRejectModal }] = useModal();
const passedColumns = [
{ title: '标题', dataIndex: 'title' },
{ title: '分类', dataIndex: 'categoryName', align: 'center', width: 120 },
{ title: '审核人', dataIndex: 'auditBy', align: 'center', width: 120 },
{ title: '审核时间', dataIndex: 'auditTime', align: 'center', width: 170 },
{ title: '审核意见', dataIndex: 'auditOpinion' },
{ title: '操作', dataIndex: 'action', align: 'center', width: 100 },
];
const rejectedColumns = [
{ title: '标题', dataIndex: 'title' },
{ title: '分类', dataIndex: 'categoryName', align: 'center', width: 120 },
{ title: '驳回人', dataIndex: 'auditBy', align: 'center', width: 120 },
{ title: '驳回时间', dataIndex: 'auditTime', align: 'center', width: 170 },
{ title: '驳回原因', dataIndex: 'auditRejectReason' },
{ title: '操作', dataIndex: 'action', align: 'center', width: 120 },
];
const recordColumns = [
{ title: '资源名称', dataIndex: 'resourceTitle' },
{ title: '操作', dataIndex: 'action', align: 'center', width: 100 },
{ title: '审核人', dataIndex: 'auditBy', align: 'center', width: 120 },
{ title: '时间', dataIndex: 'auditTime', align: 'center', width: 170 },
{ title: '意见', dataIndex: 'auditRejectReason' },
];
function getTypeLabel(type: string) {
return CONTENT_TYPE[type as keyof typeof CONTENT_TYPE]?.label || type || '-';
}
function handlePreview(record: any) {
openPreviewModal(true, { record });
}
function handlePass(record: any) {
createConfirm({
iconType: 'info',
title: '确认审核通过',
content: `是否通过《${record.title}》?`,
onOk: async () => {
await approveApi({ id: record.id });
createMessage.success('已审核通过');
loadPending();
},
});
}
function handleReject(record: any) {
openRejectModal(true, { record });
}
function handleReEdit(record: any) {
createMessage.info(`重新编辑:${record.title}(跳转到资源管理编辑)`);
}
// V3:已通过的资源需手动上架
async function handlePublish(record: any) {
await publishApi({ id: record.id });
createMessage.success('已上架');
loadPassed();
}
async function loadPending() {
pendingLoading.value = true;
try {
const res: any = await pendingListApi({ pageNo: 1, pageSize: 100, title: pendingFilter.title || undefined });
pendingList.value = res?.records || [];
pendingTotal.value = res?.total || 0;
} finally {
pendingLoading.value = false;
}
}
async function loadPassed() {
passedLoading.value = true;
try {
const res: any = await passedListApi({ pageNo: 1, pageSize: 100, title: passedFilter.title || undefined });
passedList.value = res?.records || [];
} finally {
passedLoading.value = false;
}
}
async function loadRejected() {
rejectedLoading.value = true;
try {
const res: any = await rejectedListApi({ pageNo: 1, pageSize: 100, title: rejectedFilter.title || undefined });
rejectedList.value = res?.records || [];
} finally {
rejectedLoading.value = false;
}
}
async function loadRecords() {
recordsLoading.value = true;
try {
const params: any = {
pageNo: 1,
pageSize: 100,
title: recordsFilter.title || undefined,
auditStatus: recordsFilter.auditStatus ?? undefined,
startDate: recordsFilter.dateRange?.[0] ? dayjs(recordsFilter.dateRange[0]).format('YYYY-MM-DD') : undefined,
endDate: recordsFilter.dateRange?.[1] ? dayjs(recordsFilter.dateRange[1]).format('YYYY-MM-DD') : undefined,
};
const res: any = await recordsListApi(params);
recordsList.value = res?.records || [];
} finally {
recordsLoading.value = false;
}
}
function onTabChange(key: string) {
if (key === 'records' && recordsList.value.length === 0) {
loadRecords();
}
}
onMounted(() => {
loadPending();
loadPassed();
loadRejected();
});
</script>
<style lang="less" scoped>
.check-page {
background: #fff;
padding: 16px 20px;
}
.filter-bar {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.review-list {
display: flex;
flex-direction: column;
gap: 8px;
padding-top: 8px;
}
.review-card {
border: 1px solid #f0f0f0;
border-radius: 2px;
padding: 16px 20px;
transition: all 0.2s;
background: #fff;
&:hover {
border-color: #1890ff;
box-shadow: 0 1px 2px rgba(24, 144, 255, 0.08);
}
}
.review-card-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
.review-card-main {
flex: 1;
}
.review-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 4px;
color: rgba(0, 0, 0, 0.85);
}
.review-meta {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
}
.review-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
margin-left: 16px;
}
</style>
@@ -102,6 +102,7 @@ export const formSchema: FormSchema[] = [
field: 'image',
component: 'JImageUpload',
componentProps: {
bizPath: 'img',
fileMax: 1,
},
},
@@ -266,6 +266,8 @@ export const formSchema: FormSchema[] = [
component: 'JImageUpload',
// rules: [{ required: true, message: '请选择头像' }],
componentProps: {
text: '上传头像',
bizPath: 'avatar',
//@ts-ignore
maxCount: 1,
},
@@ -190,6 +190,8 @@ export const formSchema: FormSchema[] = [
label: '头像',
component: 'JImageUpload',
componentProps: {
text: '上传头像',
bizPath: 'avatar',
// @ts-ignore
maxCount: 1,
},
@@ -169,6 +169,7 @@ export const formSchema: FormSchema[] = [
field: 'img',
component: 'JImageUpload',
componentProps: {
bizPath: 'img',
fileMax: 1,
},
rules: [{ required: false }],
+12 -1
View File
@@ -40,7 +40,7 @@
/>
<div class="bottom-top">
<div class="top-title"> <img :src="aedP" alt="" /> {{ showWindowInfo?.name }} </div>
<div class="top-title-bottom"> 距离{{ showWindowInfo?.dis ? showWindowInfo?.dis : 0 }}m </div>
<div class="top-title-bottom"> 距离{{ formatDistance(showWindowInfo?.dis) }} </div>
<div class="top-title-describe"> {{ showWindowInfo?.installAddress ? showWindowInfo?.installAddress : '无' }} </div>
<div class="table-list">
<div class="label-d"> 设备厂家 </div>
@@ -411,6 +411,17 @@
window.open(getFileAccessHttpUrl(v), '_blank');
}
/**
* 格式化距离:超过1000米换算为千米,保留两位小数
*/
function formatDistance(dis) {
if (!dis) return '0m';
if (dis >= 1000) {
return (dis / 1000).toFixed(2) + 'km';
}
return dis + 'm';
}
function getName(val, list) {
if (!val && val !== 0) return '';
for (let i = 0; i < list.length; i++) {
@@ -306,6 +306,7 @@ export const getFormSchema = () => {
component: 'JUpload',
componentProps: () => {
return {
bizPath: 'video',
maxCount: 1,
accept: '.mp4,video/mp4',
tipText: '支持MP4格式,最大200M',
@@ -319,6 +320,7 @@ export const getFormSchema = () => {
component: 'JUpload',
componentProps: () => {
return {
bizPath: 'bizfile',
maxCount: 1,
accept: 'application/pdf',
tipText: '仅支持PDF格式文件,最大200M',
@@ -0,0 +1,398 @@
<template>
<div class="firstaid-section">
<div class="firstaid-header">
<span class="firstaid-title">急救宣教推送</span>
<button
class="firstaid-push-all-btn"
:disabled="list.length === 0 || batchLoading"
@click="pushAll"
>
{{ batchLoading ? '推送中...' : '一键推送' }}
</button>
</div>
<div class="firstaid-search-wrap">
<input
v-model="keyword"
placeholder="搜索宣教内容、分类或关键词…"
@input="onSearch"
@keyup.enter="loadList"
/>
<button v-if="keyword" class="firstaid-search-clear" title="清空" @click="clearSearch">×</button>
</div>
<!-- 分类标签 -->
<div class="firstaid-tabs">
<span
class="firstaid-tab"
:class="{ active: !currentCategoryId }"
@click="switchCategory('')"
>
全部
</span>
<span
v-for="cat in categoryList"
:key="cat.id"
class="firstaid-tab"
:class="{ active: currentCategoryId === cat.id }"
@click="switchCategory(cat.id)"
>
{{ cat.name }}
</span>
</div>
<!-- 宣教列表 -->
<div class="firstaid-list">
<a-spin :spinning="loading">
<div v-for="item in list" :key="item.id" class="firstaid-item">
<div class="firstaid-item-info">
<div class="firstaid-item-title" :title="item.title">{{ item.title }}</div>
<div class="firstaid-item-meta">
<span class="firstaid-type-tag" :style="getTypeTagStyle(item.contentType)">
{{ getTypeLabel(item.contentType) }}
</span>
<span v-if="item.categoryName">{{ item.categoryName }}</span>
</div>
</div>
<button
class="firstaid-push-btn"
:class="{ pushed: item.pushed }"
:disabled="item.pushed || pushingId === item.id"
@click="pushSingle(item)"
>
{{ item.pushed ? '已推送' : '推送' }}
</button>
</div>
<div v-if="!loading && list.length === 0" class="firstaid-empty">未找到相关宣教内容</div>
</a-spin>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue';
import { useMessage } from '/@/hooks/web/useMessage';
import { pushResourceListApi, pushSendApi, pushBatchSendApi } from './firstAidPush.api';
import { categoryListAllApi } from '/@/views/resource/resource.api';
import { CONTENT_TYPE } from '/@/views/resource/resource.data';
const props = defineProps({
orderId: { type: String, default: '' },
hasInteractedWithIm: { type: Boolean, default: false },
});
const { createMessage } = useMessage();
const keyword = ref('');
const currentCategoryId = ref('');
const categoryList = ref<any[]>([]);
const list = ref<any[]>([]);
const loading = ref(false);
const pushingId = ref('');
const batchLoading = ref(false);
let searchTimer: any = null;
function getTypeLabel(type: string) {
return CONTENT_TYPE[type as keyof typeof CONTENT_TYPE]?.label || type;
}
function getTypeTagStyle(type: string) {
const color = CONTENT_TYPE[type as keyof typeof CONTENT_TYPE]?.color || 'default';
const colorMap: Record<string, string> = {
blue: '#1890ff',
red: '#f5222d',
orange: '#fa8c16',
default: '#8c8c8c',
};
const c = colorMap[color] || colorMap.default;
return {
color: c,
borderColor: c,
};
}
async function loadList() {
loading.value = true;
try {
const res: any = await pushResourceListApi({
orderId: props.orderId || undefined,
keyword: keyword.value || undefined,
categoryId: currentCategoryId.value || undefined,
pageNo: 1,
pageSize: 50,
});
list.value = res?.records || [];
} finally {
loading.value = false;
}
}
function onSearch() {
if (searchTimer) clearTimeout(searchTimer);
searchTimer = setTimeout(loadList, 300);
}
function clearSearch() {
keyword.value = '';
loadList();
}
function switchCategory(id: string) {
currentCategoryId.value = id;
loadList();
}
async function pushSingle(item: any) {
if (!props.hasInteractedWithIm) {
createMessage.warning('请先在左侧选择会话');
return;
}
if (!props.orderId) {
createMessage.warning('当前会话未关联工单,无法记录推送状态');
return;
}
pushingId.value = item.id;
try {
// 调后端接口推送 article 卡片到当前会话(后端调用 IM 推送自定义消息)
await pushSendApi({ resourceId: item.id, orderId: props.orderId });
item.pushed = true;
createMessage.success(`已推送:${item.title}`);
} finally {
pushingId.value = '';
}
}
async function pushAll() {
if (!props.hasInteractedWithIm) {
createMessage.warning('请先在左侧选择会话');
return;
}
if (!props.orderId) {
createMessage.warning('当前会话未关联工单,无法记录推送状态');
return;
}
const unpushed = list.value.filter((item) => !item.pushed);
if (unpushed.length === 0) {
createMessage.info('当前列表已全部推送');
return;
}
batchLoading.value = true;
try {
// 调后端接口批量推送(后端按条件查出资源并触发 IM 推送 article 卡片)
await pushBatchSendApi({
orderId: props.orderId,
resourceIds: unpushed.map((item) => item.id),
});
list.value.forEach((item) => (item.pushed = true));
createMessage.success(`已推送 ${unpushed.length} 条宣教内容`);
} finally {
batchLoading.value = false;
}
}
watch(
() => props.orderId,
() => {
// 切换对话后重新拉取,刷新每条资源的 pushed 状态
loadList();
}
);
onMounted(async () => {
try {
categoryList.value = (await categoryListAllApi()) || [];
} catch {
categoryList.value = [];
}
loadList();
});
</script>
<style lang="less" scoped>
.firstaid-section {
margin: 10px 15px 0 15px;
border-top: 1px solid #e8e8e8;
padding-top: 10px;
}
.firstaid-header {
font-size: 16px;
font-weight: bold;
margin-bottom: 8px;
display: flex;
align-items: center;
justify-content: space-between;
}
.firstaid-title {
color: rgba(0, 0, 0, 0.85);
}
.firstaid-push-all-btn {
font-size: 12px;
background: #1890ff;
color: white;
border: none;
border-radius: 4px;
padding: 3px 10px;
cursor: pointer;
transition: background 0.2s;
&:hover:not(:disabled) {
background: #096dd9;
}
&:disabled {
background: #d9d9d9;
color: rgba(255, 255, 255, 0.8);
cursor: not-allowed;
}
}
.firstaid-search-wrap {
display: flex;
align-items: center;
margin-bottom: 8px;
background: #f7f7f7;
border-radius: 6px;
padding: 0 8px;
border: 1px solid #ddd;
input {
flex: 1;
border: none;
background: transparent;
outline: none;
font-size: 13px;
padding: 6px 0;
color: #333;
&::placeholder {
color: #bbb;
}
}
}
.firstaid-search-clear {
border: none;
background: none;
cursor: pointer;
padding: 0;
color: #bbb;
font-size: 18px;
line-height: 1;
&:hover {
color: #999;
}
}
.firstaid-tabs {
display: flex;
gap: 6px;
margin-bottom: 8px;
flex-wrap: wrap;
}
.firstaid-tab {
font-size: 11px;
padding: 2px 8px;
border-radius: 12px;
border: 1px solid #d9d9d9;
background: #fff;
cursor: pointer;
color: #666;
transition: all 0.2s;
user-select: none;
&:hover {
border-color: #1890ff;
color: #1890ff;
}
&.active {
background: #1890ff;
color: #fff;
border-color: #1890ff;
}
}
.firstaid-list {
max-height: 260px;
overflow-y: auto;
border: 1px solid #e8e8e8;
border-radius: 6px;
background: #fafafa;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
}
&::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 2px;
}
}
.firstaid-item {
display: flex;
align-items: center;
padding: 7px 10px;
border-bottom: 1px solid #f0f0f0;
font-size: 12px;
background: #fff;
transition: background 0.15s;
&:last-child {
border-bottom: none;
}
&:hover {
background: #f0f7ff;
}
}
.firstaid-item-info {
flex: 1;
overflow: hidden;
}
.firstaid-item-title {
font-weight: 500;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 13px;
}
.firstaid-item-meta {
color: #999;
font-size: 11px;
margin-top: 2px;
display: flex;
align-items: center;
gap: 4px;
}
.firstaid-type-tag {
border-radius: 3px;
padding: 0 4px;
font-size: 10px;
border: 1px solid;
background: #fff;
}
.firstaid-push-btn {
flex-shrink: 0;
margin-left: 8px;
background: #52c41a;
color: white;
border: none;
border-radius: 4px;
padding: 3px 8px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
min-width: 42px;
&:hover:not(:disabled) {
background: #389e0d;
}
&:disabled {
background: #b7eb8f;
color: #389e0d;
cursor: default;
}
}
.firstaid-empty {
text-align: center;
padding: 20px;
color: #bbb;
font-size: 13px;
}
</style>
@@ -13,7 +13,7 @@ const api = {
stationSave: '/health-emergency/api/emergency/order/stationSendOrder', // 驻场派单保存
secondDepart: '/sys/sysDepart/allSecondaryDeparts', // 查询所有二级部门
employList: '/sys/healthUserEmployeeEx/list', // 查询二级部门下的员工
listByMc: '/sys/healthUserEmployeeEx/listByMc', // 日常诊疗,长庆员工列表最新接口 2024年4月19日09:42:30
listByMc: '/sys/healthUserEmployeeEx/listByMc', // 日常诊疗,员工列表最新接口 2024年4月19日09:42:30
employListNew: '/health-system/sys/healthUserEmployeeEx/listUserBackByOrg', // 查询二级部门下的员工新
updateSalvageUser: '/health-emergency/api/emergency/order/updateSalvageUser', // 驻场派单保存
stat: '/health-emergency/api/emergency/stat', //数据统计
@@ -0,0 +1,19 @@
import { defHttp } from '/@/utils/http/axios';
/**
* 急救宣教推送 - 应急工单场景下推送给求助者
*/
enum Api {
resourceList = '/health-emergency/emergency/firstAid/push/resourceList',
send = '/health-emergency/emergency/firstAid/push/send',
batchSend = '/health-emergency/emergency/firstAid/push/batchSend',
}
// 可推送资源列表(只返回已发布+审核通过+时间有效,每条带 pushed 字段)
export const pushResourceListApi = (params: any) => defHttp.get({ url: Api.resourceList, params });
// 推送单条 { resourceId, orderId }
export const pushSendApi = (params: any) => defHttp.post({ url: Api.send, params }, { successNeedMessage: false });
// 批量推送 { resourceIds: [], orderId } 或 { orderId, filterParams: {...} }
export const pushBatchSendApi = (params: any) => defHttp.post({ url: Api.batchSend, params }, { successNeedMessage: false });
@@ -153,16 +153,18 @@
</a-row>
<forHelpAdv :isSpecialized="true" ref="forHelpAdvRef" />
<detail ref="detailRef" />
<FirstAidPush :orderId="orderId" :hasInteractedWithIm="hasInteractedWithIm" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ref, onMounted } from 'vue';
import { Dayjs } from 'dayjs';
import detail from './detail.vue';
import { stat, scheduleRecordApi, scheduleRecordNewApi } from './commApi';
import forHelpAdv from './forHelpAdv.vue';
import FirstAidPush from './FirstAidPush.vue';
import moment from 'moment';
import qs from 'qs';
import { imAddressSrc } from '/@/utils/imAddressSrc';
@@ -175,6 +177,20 @@
});
const route = useRoute();
// 当前应急工单IDIM 会话 sessionId),用于急救宣教推送
// 初始值取 URL 中的 sessionId;后续用户在 IM 内切换会话时由 postMessage 更新
const orderId = ref<string>((route.query?.sessionId as string) || '');
// 用户是否已与 IM iframe 交互(粗略判断是否已选会话)
// 跨域 iframe 内部点击事件无法冒泡到父页面,用 window blur + activeElement 近似判断
const hasInteractedWithIm = ref(false);
onMounted(() => {
window.addEventListener('blur', () => {
if (document.activeElement && (document.activeElement as HTMLElement).tagName === 'IFRAME') {
hasInteractedWithIm.value = true;
}
});
});
moment.updateLocale('en', {
weekdaysMin: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
@@ -184,6 +200,7 @@
const statInfo = ref<Object>({});
const dayS = ref<Array>();
const loadingVisible = ref<Boolean>(true); // loading
const iframeRef = ref<HTMLIFrameElement>();
const forHelpDetail = (res) => {
detailRef.value.showDrawer(res);
};
@@ -217,6 +234,10 @@
case 'forHelpDetail':
forHelpDetail(result.userId);
break;
case 'sessionChanged':
// 用户在 IM 内切换会话,更新当前工单 ID
orderId.value = result.orderId || '';
break;
case 'closeLoading':
closeLoading();
break;
@@ -212,17 +212,17 @@
const [registerTable, { reload, getForm, getDataSource, setTableData }] = tableContext;
</script>
<style scoped lang="less">
:deep(.ant-table-cell-fix-left, .ant-table-cell-fix-right) {
position: relative;
border-bottom: none !important;
&:after {
position: absolute;
content: '';
width: 100%;
top: 0;
left: 0;
border-bottom: 1px solid #f0f0f0;
}
}
//:deep(.ant-table-cell-fix-left, .ant-table-cell-fix-right) {
// position: relative;
// //border-bottom: none !important;
// border-bottom: 2px solid #f0f0f0 !important;
// &:after {
// position: absolute;
// content: '';
// width: 100%;
// top: 0;
// left: 0;
// border-bottom: 1px solid #f0f0f0;
// }
//}
</style>
+7 -1
View File
@@ -172,7 +172,13 @@
loadingVisible.value = false;
};
window.onmessage = async (msg) => {
const result = JSON.parse(msg.data);
if (!msg.data || typeof msg.data !== 'string') return;
let result: any;
try {
result = JSON.parse(msg.data);
} catch (e) {
return;
}
switch (result.code) {
case 'closeLoading':
closeLoading();
@@ -141,6 +141,8 @@ export const formSchema = function (showGoodAt: boolean): FormSchema[] {
component: 'JImageUpload',
required: true,
componentProps: () => ({
text: '上传头像',
bizPath: 'avatar',
maxCount: 1,
}),
},
@@ -8,7 +8,7 @@
</template>
</BasicTable>
</template>
<script setup lang="ts">
<script setup lang="ts" name="bindRecords-name">
import BasicTable from '/@/components/Table/src/BasicTable.vue';
import { useListPage } from '/@/hooks/system/useListPage';
import { bindRecordColumns, bindRecordSchema } from '/@/views/healthMonitor/healMonitorManage/monitorToll/bindRecords/bindRecords.data';
@@ -7,8 +7,8 @@
</BasicForm>
<template #footer v-if="title !== '查看'">
<a-button @click="handleCanel">取消</a-button>
<a-button type="primary" @click="handleSubmit(1)">保存并发送</a-button>
<a-button type="primary" @click="handleSubmit(2)">保存</a-button>
<a-button type="primary" :loading="submitting" :disabled="submitting" @click="handleSubmit(1)">保存并发送</a-button>
<a-button type="primary" :loading="submitting" :disabled="submitting" @click="handleSubmit(2)">保存</a-button>
</template>
</BasicModal>
<ChooseSome
@@ -35,6 +35,7 @@
// Emits声明
const emit = defineEmits(['register', 'success', 'userData']);
const isUpdate = ref(true);
const submitting = ref(false);
const [examinationModal, { openDrawer }] = useDrawer();
//设置标题
const title = ref(String);
@@ -100,6 +101,7 @@
//表单提交事件
async function handleSubmit(type: number) {
try {
submitting.value = true;
let values = await validate();
setModalProps({ confirmLoading: true });
//提交表单
@@ -109,6 +111,7 @@
//刷新列表
emit('success');
} finally {
submitting.value = false;
setModalProps({ confirmLoading: false });
}
}
@@ -19,7 +19,7 @@
<notification-news-modal @register="registerModal" @success="handleSuccess" />
</div>
</template>
<script setup lang="ts">
<script setup lang="ts" name="healthMonitor-notificationNews">
import { BasicTable, TableAction } from '/@/components/Table';
import { useListPage } from '/@/hooks/system/useListPage';
import { list, deleteOne, batchDelete, queryByID, reSend } from './notificationNews.api';
@@ -10,7 +10,8 @@
<!-- <div>数据日期{{ userInfo.dataDate }}</div>-->
</div>
<div style="width: 75%">
<div style="display: flex; justify-content: flex-end; padding-right: 30px">
<div style="display: flex; justify-content: space-between; align-items: center; padding-right: 30px">
<div style="color: #faad14; font-size: 13px; padding-left: 4px">{{ noDataTip }}</div>
<div style="display: flex; font-size: 20px; align-items: center">
<a-button size="small" @click="changeDate('0')">
<LeftOutlined />
@@ -23,6 +24,7 @@
format="YYYY-MM-DD"
@change="changeValue"
:allow-clear="false"
dropdownClassName="personal-data-no-other-month"
style="opacity: 0; z-index: 99; position: absolute; top: 0; left: 0"
>
<template #dateRender="{ current }">
@@ -47,23 +49,27 @@
</BasicModal>
</template>
<script setup lang="ts" name="personalDataModal">
import { computed, ref, watch } from 'vue';
import { computed, ref, watch, onMounted, onUnmounted, nextTick } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import AllLook from '/@/views/healthMonitor/healMonitorManage/monitorToll/userInfo/componets/allLook.vue';
import { getDetail, userDataDayByMonth } from '/@/views/healthMonitor/healMonitorManage/monitorToll/personalData/personalData.api';
import dayjs from 'dayjs';
import { Menu, message } from 'ant-design-vue';
import { Menu } from 'ant-design-vue';
import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
const userInfo = ref({});
const infoData = ref({});
const userId = ref('');
const haveInfo = ref('');
const nowDate = ref(dayjs());
const noDataTip = ref('');
const loadedYear = ref(dayjs().format('YYYY'));
const isRight = computed(() => dayjs(nowDate.value).format('YYYY-MM-DD') != dayjs().format('YYYY-MM-DD'));
//表单赋值
const [registerModal, { setModalProps }] = useModalInner(async (data) => {
userInfo.value = data.record;
userId.value = data.record.userId;
// 重置已加载年份,避免上次切换年份的残留状态
loadedYear.value = dayjs().format('YYYY');
await userDataDayByMonth({ userId: data.record.userId, year: dayjs().format('YYYY') }).then((res) => {
let year = dayjs().format('YYYY');
let month = '';
@@ -82,7 +88,13 @@
haveInfo.value = res;
});
await getInfo();
if (getDisabled(nowDate.value)) {
await getInfo();
} else {
// 本地记录无数据:清空展示数据并行内提示具体日期
infoData.value = {};
noDataTip.value = `${dayjs(nowDate.value).format('YYYY-MM-DD')}】暂无监测数据`;
}
setModalProps({ footer: false });
});
@@ -94,6 +106,8 @@
function getDisabled(current) {
let flag = false;
// 年份不匹配时不渲染 bgc
if (dayjs(current).format('YYYY') !== loadedYear.value) return false;
for (let i = 0; i < Object.keys(haveInfo.value).length; i++) {
if (Object.keys(haveInfo.value)[i] == dayjs(current).format('MM') * 1) {
@@ -109,15 +123,18 @@
}
async function getInfo() {
const dateStr = dayjs(nowDate.value).format('YYYY-MM-DD');
await getDetail({
userId: userId.value,
dataDate: dayjs(nowDate.value).format('YYYY-MM-DD'),
dataDate: dateStr,
}).then((res) => {
// openModal(true, {
// record,
// dataInfo: res,
// });
infoData.value = res;
// API 返回空数据时行内提示,否则清空提示
if (!res || Object.keys(res).length === 0) {
noDataTip.value = `${dateStr}】暂无监测数据`;
} else {
noDataTip.value = '';
}
});
}
@@ -132,7 +149,9 @@
if (getDisabled(nowDate.value)) {
getInfo();
} else {
message.info('当日暂无监测数据');
// 本地记录无数据:清空展示数据并行内提示具体日期
infoData.value = {};
noDataTip.value = `${dayjs(nowDate.value).format('YYYY-MM-DD')}】暂无监测数据`;
}
}
@@ -146,8 +165,39 @@
);
function changeValue() {
getInfo();
if (getDisabled(nowDate.value)) {
getInfo();
} else {
// 本地记录无数据:清空展示数据并行内提示具体日期
infoData.value = {};
noDataTip.value = `${dayjs(nowDate.value).format('YYYY-MM-DD')}】暂无监测数据`;
}
}
// 加载指定年份的 bgc 标记数据
async function loadYearData(year: string) {
if (year === loadedYear.value) return;
await userDataDayByMonth({ userId: userId.value, year }).then((res) => {
haveInfo.value = res;
loadedYear.value = year;
});
}
// 监听面板内点击,DOM 更新后从年份按钮读取当前展示的年份
function onPickerPanelClick(e: MouseEvent) {
const panel = document.querySelector('.personal-data-no-other-month');
if (!panel?.contains(e.target as Node)) return;
nextTick(() => {
const yearBtn = panel.querySelector('.ant-picker-year-btn');
if (!yearBtn) return;
// 按钮文本可能为 "2024年" 或 "2024",提取4位数字
const year = yearBtn.textContent?.replace(/[^0-9]/g, '').slice(0, 4) || '';
if (year.length === 4) loadYearData(year);
});
}
onMounted(() => document.addEventListener('click', onPickerPanelClick));
onUnmounted(() => document.removeEventListener('click', onPickerPanelClick));
</script>
<style scoped lang="less">
@@ -185,3 +235,11 @@
cursor: pointer !important;
}
</style>
<style>
/* 隐藏日历面板中非当前月份的日期格,仅对该 picker 生效 */
.personal-data-no-other-month .ant-picker-cell:not(.ant-picker-cell-in-view) {
visibility: hidden;
pointer-events: none;
}
</style>
@@ -151,13 +151,39 @@ export const columnsNew: BasicColumn[] = [
},
},
{
title: '分配设备数',
title: '分配设备数',
dataIndex: ['assignedDeviceNums'],
customRender: ({ text }) => {
if (!text) return 0;
return text;
},
},
{
title: '未分配设备数',
dataIndex: 'undistributedDeviceNums',
customRender: ({ record }) => {
const total = record.totalAssignedDeviceNums ?? 0;
const assigned = record.assignedDeviceNums ?? 0;
return Math.max(0, total - assigned);
},
},
{
title: '已绑定设备数',
dataIndex: ['assignedDeviceNums'],
customRender: ({ text }) => {
if (!text) return 0;
return text;
},
},
{
title: '未绑定设备数',
dataIndex: 'undistributedDeviceNums',
customRender: ({ record }) => {
const total = record.totalAssignedDeviceNums ?? 0;
const assigned = record.assignedDeviceNums ?? 0;
return Math.max(0, total - assigned);
},
},
{
title: '佩戴人员数',
dataIndex: ['assignedUserNums'],
@@ -166,6 +192,15 @@ export const columnsNew: BasicColumn[] = [
return text;
},
},
{
title: '关爱人员佩戴人数',
dataIndex: ['careWearNums'],
width: 150,
customRender: ({ text }) => {
if (!text) return 0;
return text;
},
},
{
title: '未佩戴人员数',
dataIndex: ['undistributedUserNums'],
@@ -174,6 +209,47 @@ export const columnsNew: BasicColumn[] = [
return text;
},
},
{
title: '关爱人员未佩戴人数',
dataIndex: ['careUnwearNums'],
width: 160,
customRender: ({ text }) => {
if (!text) return 0;
return text;
},
},
{
title: '关爱人员未佩戴人数\n(仅计入统计日期起期已绑定的人数)',
dataIndex: ['careUnwearAtStartNums'],
width: 180,
customHeaderCell: () => {
return {
style: {
whiteSpace: 'pre-wrap',
},
};
},
customRender: ({ text }) => {
if (!text) return 0;
return text;
},
},
{
title: '非关爱人员数量',
dataIndex: ['nonCareNums'],
customRender: ({ text }) => {
if (!text) return 0;
return text;
},
},
{
title: '关爱人员数量',
dataIndex: ['careNums'],
customRender: ({ text }) => {
if (!text) return 0;
return text;
},
},
{
title: '心率异常人数',
dataIndex: ['heartRateNums'],
@@ -206,6 +282,7 @@ export const columnsNew: BasicColumn[] = [
return text;
},
},
];
const startTime = dayjs().subtract(6, 'day').format('YYYY-MM-DD');
@@ -350,6 +427,16 @@ export const detailColumns = (arr: BasicColumn[] = []) => {
title: '手表所属单位',
dataIndex: 'watchOrgName',
},
{
title: '非关爱人员',
dataIndex: 'nonCareFlag',
width: 100,
customRender: ({ text }) => {
if (text === 1) return '是';
if (text === 0) return '否';
return '';
},
},
...arr,
{
title: '应佩戴天数',
@@ -168,32 +168,30 @@
let xArr: any[] = [];
let yArr: any[] = [];
res.timePeriods &&
res.timePeriods.map((item) => {
res.timePeriods.forEach((item) => {
switch (item.flag) {
case 1:
// 浅睡
yArr.push(1);
yArr.push(1);
yArr.push(1, 1, null);
break;
case 3:
// 深睡
yArr.push(0);
yArr.push(0);
yArr.push(0, 0, null);
break;
case 4:
// 清醒
yArr.push(3);
yArr.push(3);
yArr.push(3, 3, null);
break;
case 5:
// 零星小睡
yArr.push(2);
yArr.push(2);
yArr.push(2, 2, null);
break;
}
xArr.push(dayjs(item.start).format('HH:mm'));
xArr.push(dayjs(item.end).format('HH:mm'));
xArr.push(dayjs(item.start).format('HH:mm'), dayjs(item.end).format('HH:mm'), '');
});
// 去掉最后多余的 null
if (yArr.length > 0) yArr.pop();
if (xArr.length > 0) xArr.pop();
const option = {
xAxis: {
type: 'category',
@@ -8,7 +8,7 @@
<user-info-modal @register="registerModal" />
</div>
</template>
<script setup lang="ts">
<script setup lang="ts" name="userInfoList-name">
import BasicTable from '/@/components/Table/src/BasicTable.vue';
import { useListPage } from '/@/hooks/system/useListPage';
import { columns, searchSchema } from '/@/views/healthMonitor/healMonitorManage/monitorToll/userInfo/userInfo.data';
@@ -0,0 +1,75 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :title="title" :width="520" :minHeight="300" @ok="handleSubmit">
<a-form layout="vertical">
<a-form-item label="分类名称" required>
<a-input v-model:value="formState.name" placeholder="请输入分类名称" :maxlength="50" />
</a-form-item>
<a-form-item label="排序号">
<a-input-number v-model:value="formState.sortNo" :min="0" style="width: 100%" />
</a-form-item>
<a-form-item label="描述">
<a-textarea v-model:value="formState.description" :rows="3" placeholder="可选" :maxlength="200" />
</a-form-item>
</a-form>
</BasicModal>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { useMessage } from '/@/hooks/web/useMessage';
import { categoryAddApi, categoryEditApi } from '../label.api';
const emit = defineEmits(['success', 'register']);
const { createMessage } = useMessage();
const title = ref('新增分类');
// V3 parentId
const formState = reactive({
id: '',
name: '',
sortNo: 1,
description: '',
});
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
const isUpdate = data?.isUpdate;
title.value = isUpdate ? '编辑分类' : '新增分类';
formState.id = data?.record?.id || '';
formState.name = data?.record?.name || '';
formState.sortNo = data?.record?.sortNo ?? 1;
formState.description = data?.record?.description || '';
});
function validate(): boolean {
if (!formState.name) {
createMessage.warning('请输入分类名称');
return false;
}
return true;
}
async function handleSubmit() {
if (!validate()) return;
setModalProps({ confirmLoading: true });
try {
const params = {
id: formState.id || undefined,
name: formState.name,
sortNo: formState.sortNo,
description: formState.description,
};
if (formState.id) {
await categoryEditApi(params);
createMessage.success('已更新分类');
} else {
await categoryAddApi(params);
createMessage.success('已新增分类');
}
emit('success');
closeModal();
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>
+106
View File
@@ -0,0 +1,106 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :title="title" :width="480" :minHeight="260" @ok="handleSubmit">
<a-form layout="vertical">
<a-form-item label="所属分类" required>
<a-select
v-model:value="formState.parentId"
:options="categoryOptions"
:field-names="{ label: 'name', value: 'id' }"
placeholder="请选择所属分类"
:disabled="!!lockParentId"
allow-clear
/>
</a-form-item>
<a-form-item label="标签名称" required>
<a-input v-model:value="formState.name" placeholder="请输入标签名称" :maxlength="20" />
</a-form-item>
<a-form-item label="排序号">
<a-input-number v-model:value="formState.sortNo" :min="0" style="width: 100%" />
</a-form-item>
<a-form-item label="描述">
<a-textarea v-model:value="formState.description" :rows="3" placeholder="可选" :maxlength="200" />
</a-form-item>
</a-form>
</BasicModal>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { useMessage } from '/@/hooks/web/useMessage';
import { tagAddApi, tagEditApi } from '../label.api';
import { categoryListAllApi } from '/@/views/resource/resource.api';
const emit = defineEmits(['success', 'register']);
const { createMessage } = useMessage();
const title = ref('新增标签');
const categoryOptions = ref<any[]>([]);
// +
const lockParentId = ref('');
const formState = reactive({
id: '',
name: '',
parentId: '' as string,
sortNo: 1,
description: '',
});
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
const isUpdate = data?.isUpdate;
title.value = isUpdate ? '编辑标签' : '新增标签';
formState.id = data?.record?.id || '';
formState.name = data?.record?.name || '';
// V3+ parentId
const presetParentId = data?.parentId || data?.record?.parentId || '';
formState.parentId = presetParentId;
lockParentId.value = data?.parentId ? presetParentId : '';
formState.sortNo = data?.record?.sortNo ?? 1;
formState.description = data?.record?.description || '';
});
function validate(): boolean {
if (!formState.parentId) {
createMessage.warning('请选择所属分类');
return false;
}
if (!formState.name) {
createMessage.warning('请输入标签名称');
return false;
}
return true;
}
async function handleSubmit() {
if (!validate()) return;
setModalProps({ confirmLoading: true });
try {
const params = {
id: formState.id || undefined,
name: formState.name,
parentId: formState.parentId,
sortNo: formState.sortNo,
description: formState.description,
};
if (formState.id) {
await tagEditApi(params);
createMessage.success('已更新标签');
} else {
await tagAddApi(params);
createMessage.success('已新增标签');
}
emit('success');
closeModal();
} finally {
setModalProps({ confirmLoading: false });
}
}
onMounted(async () => {
try {
categoryOptions.value = (await categoryListAllApi()) || [];
} catch {
categoryOptions.value = [];
}
});
</script>
+302
View File
@@ -0,0 +1,302 @@
<template>
<div class="label-page">
<a-row :gutter="16">
<!-- 左侧分类树 -->
<a-col :span="7">
<a-card title="分类管理" :bordered="false" class="card-section">
<template #extra>
<a-button type="primary" size="small" @click="handleAddCategory()">
<template #icon><PlusOutlined /></template>
新增
</a-button>
</template>
<a-input-search v-model:value="categorySearch" placeholder="搜索分类" style="margin-bottom: 12px" allow-clear />
<a-spin :spinning="categoryLoading">
<div class="tree-wrap">
<a-tree
v-if="filteredTreeData.length > 0"
v-model:expandedKeys="expandedKeys"
v-model:selectedKeys="selectedKeys"
:tree-data="filteredTreeData"
:field-names="{ title: 'name', key: 'id', children: 'children' }"
block-node
@select="onSelectCategory"
>
<template #title="{ name, id, status, resourceCount }">
<div class="tree-node-content">
<span class="tree-name">{{ name }}</span>
<span v-if="resourceCount != null" class="tree-count">({{ resourceCount }})</span>
<a-tag v-if="status === 2" color="red" style="margin-left: 4px">冻结</a-tag>
<span class="tree-actions">
<a-button type="link" size="small" @click.stop="handleEditCategory({ id, name })"></a-button>
<a-button type="link" size="small" @click.stop="handleAddTagUnderCategory(id)">+标签</a-button>
<a-button type="link" size="small" danger @click.stop="handleDeleteCategory({ id, name })"></a-button>
</span>
</div>
</template>
</a-tree>
<a-empty v-else description="暂无分类" />
</div>
</a-spin>
</a-card>
</a-col>
<!-- 右侧标签云 + 权限矩阵 -->
<a-col :span="17">
<a-card title="标签管理" :bordered="false" class="card-section">
<template #extra>
<a-button type="primary" size="small" @click="handleAddTag">
<template #icon><PlusOutlined /></template>
新增标签
</a-button>
</template>
<a-input-search v-model:value="tagSearch" placeholder="搜索标签" style="width: 240px; margin-bottom: 12px" allow-clear />
<a-spin :spinning="tagLoading">
<div class="tag-cloud">
<a-tag
v-for="tag in filteredTags"
:key="tag.id"
:color="getTagColor(tag.id)"
class="tag-item-cloud"
closable
@close.prevent="handleDeleteTag(tag)"
>
{{ tag.name }}
<span v-if="tag.resourceCount != null" class="tag-count">{{ tag.resourceCount }}</span>
</a-tag>
<a-empty v-if="filteredTags.length === 0 && !tagLoading" description="暂无标签" />
</div>
</a-spin>
</a-card>
<a-card title="分类权限配置" :bordered="false" class="card-section">
<!-- <a-alert message="权限矩阵接口后端尚未提供,当前为占位展示" type="info" show-icon style="margin-bottom: 12px" />-->
<a-table :columns="permissionColumns" :data-source="permissionList" :pagination="false" size="middle" bordered>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex !== 'role'">
<a-tag :color="getPermissionColor(record.permissions[column.idx])">
{{ getPermissionText(record.permissions[column.idx]) }}
</a-tag>
</template>
</template>
</a-table>
</a-card>
</a-col>
</a-row>
<!-- 分类新增/编辑弹窗 -->
<CategoryFormModal @register="registerCategoryModal" @success="loadCategoryTree" />
<!-- 标签新增/编辑弹窗 -->
<TagFormModal @register="registerTagModal" @success="loadCategoryTree" />
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import { useMessage } from '/@/hooks/web/useMessage';
import { useModal } from '/@/components/Modal';
import { categoryListAllApi, categoryDeleteApi, tagDeleteApi, buildTree } from './label.api';
import CategoryFormModal from './components/CategoryFormModal.vue';
import TagFormModal from './components/TagFormModal.vue';
const { createMessage } = useMessage();
const [registerCategoryModal, { openModal: openCategoryModal }] = useModal();
const [registerTagModal, { openModal: openTagModal }] = useModal();
// ============ ============
const categoryList = ref<any[]>([]);
const categoryLoading = ref(false);
const categorySearch = ref('');
const expandedKeys = ref<string[]>([]);
const selectedKeys = ref<string[]>([]);
const categoryTree = computed(() => buildTree(categoryList.value));
const filteredTreeData = computed(() => {
if (!categorySearch.value) return categoryTree.value;
const kw = categorySearch.value.toLowerCase();
const filter = (list: any[]): any[] =>
list
.map((item) => {
const children = item.children ? filter(item.children) : undefined;
if (item.name.toLowerCase().includes(kw) || (children && children.length > 0)) {
return { ...item, children };
}
return null;
})
.filter(Boolean);
return filter(categoryTree.value);
});
function onSelectCategory(keys: string[]) {
// V3 tags
if (keys.length > 0) {
const category = categoryList.value.find((c) => c.id === keys[0]);
tagList.value = category?.tags || [];
} else {
tagList.value = [];
}
}
function handleAddCategory(parentId?: string) {
openCategoryModal(true, { isUpdate: false, parentId });
}
// V3+
function handleAddTagUnderCategory(parentId: string) {
openTagModal(true, { isUpdate: false, parentId });
}
function handleEditCategory(record: any) {
openCategoryModal(true, { isUpdate: true, record });
}
function handleDeleteCategory(record: any) {
categoryDeleteApi({ id: record.id }, () => {
createMessage.success('已删除分类');
loadCategoryTree();
});
}
// ============ ============
const tagList = ref<any[]>([]);
const tagLoading = ref(false);
const tagSearch = ref('');
const filteredTags = computed(() => {
if (!tagSearch.value) return tagList.value;
return tagList.value.filter((t) => t.name.includes(tagSearch.value));
});
const tagColorPool = ['blue', 'green', 'orange', 'purple', 'red', 'default'];
function getTagColor(id: string) {
let hash = 0;
for (let i = 0; i < id.length; i++) {
hash = id.charCodeAt(i) + ((hash << 5) - hash);
}
return tagColorPool[Math.abs(hash) % tagColorPool.length];
}
function handleAddTag() {
openTagModal(true, { isUpdate: false });
}
function handleDeleteTag(tag: any) {
tagDeleteApi({ id: tag.id }, () => {
createMessage.success('已删除标签');
loadCategoryTree();
});
}
// ============ mock ============
const permissionList = ref<any[]>([
{ role: '系统管理员', permissions: ['rw', 'rw', 'rw', 'rw', 'rw'] },
{ role: '内容编辑', permissions: ['r', 'rw', 'rw', 'r', 'rw'] },
{ role: '审核员', permissions: ['r', 'r', 'r', 'r', 'r'] },
{ role: '普通用户', permissions: ['n', 'n', 'n', 'n', 'n'] },
]);
const permissionColumns = [
{ title: '角色', dataIndex: 'role', align: 'center', width: 120 },
{ title: '心肺复苏', dataIndex: 'p0', align: 'center', idx: 0 },
{ title: '创伤处理', dataIndex: 'p1', align: 'center', idx: 1 },
{ title: '中暑急救', dataIndex: 'p2', align: 'center', idx: 2 },
{ title: '骨折固定', dataIndex: 'p3', align: 'center', idx: 3 },
{ title: '烧烫伤', dataIndex: 'p4', align: 'center', idx: 4 },
];
function getPermissionColor(code: string) {
if (code === 'rw') return 'blue';
if (code === 'r') return 'default';
if (code === 'n') return 'red';
return 'default';
}
function getPermissionText(code: string) {
if (code === 'rw') return '读写';
if (code === 'r') return '只读';
if (code === 'n') return '无权限';
return code;
}
// ============ ============
async function loadCategoryTree() {
categoryLoading.value = true;
try {
const res = await categoryListAllApi();
categoryList.value = res || [];
//
if (expandedKeys.value.length === 0 && categoryTree.value.length > 0) {
expandedKeys.value = [categoryTree.value[0].id];
}
// V3 tags
if (selectedKeys.value.length === 0 && categoryList.value.length > 0) {
const first = categoryList.value[0];
selectedKeys.value = [first.id];
tagList.value = first?.tags || [];
} else if (selectedKeys.value.length > 0) {
// //
const selected = categoryList.value.find((c) => c.id === selectedKeys.value[0]);
tagList.value = selected?.tags || [];
}
} finally {
categoryLoading.value = false;
}
}
onMounted(() => {
loadCategoryTree();
});
</script>
<style lang="less" scoped>
.label-page {
padding: 0;
}
.card-section {
margin-bottom: 16px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
}
.tree-wrap {
min-height: 200px;
}
.tree-node-content {
display: flex;
align-items: center;
gap: 4px;
width: 100%;
}
.tree-name {
flex: 1;
}
.tree-count {
font-size: 11px;
color: #999;
margin-left: 4px;
}
.tree-actions {
display: none;
margin-left: auto;
}
:deep(.ant-tree-node-content-wrapper:hover) .tree-actions,
:deep(.ant-tree-treenode-selected) .tree-actions {
display: inline-flex;
}
.tag-cloud {
display: flex;
flex-wrap: wrap;
gap: 8px;
min-height: 60px;
}
.tag-item-cloud {
font-size: 13px;
padding: 4px 14px;
cursor: default;
}
.tag-count {
opacity: 0.5;
font-size: 11px;
margin-left: 4px;
}
</style>
+118
View File
@@ -0,0 +1,118 @@
import { defHttp } from '/@/utils/http/axios';
import { useMessage } from '/@/hooks/web/useMessage';
const { createConfirm } = useMessage();
enum Api {
// 分类
categoryList = '/health-emergency/emergency/firstAid/category/list',
categoryListAll = '/health-emergency/emergency/firstAid/category/listAll',
categoryListRoot = '/health-emergency/emergency/firstAid/category/listRoot',
categoryListChildren = '/health-emergency/emergency/firstAid/category/listChildren',
categoryQueryById = '/health-emergency/emergency/firstAid/category/queryById',
categoryAdd = '/health-emergency/emergency/firstAid/category/add',
categoryEdit = '/health-emergency/emergency/firstAid/category/edit',
categoryDelete = '/health-emergency/emergency/firstAid/category/delete',
categoryDeleteBatch = '/health-emergency/emergency/firstAid/category/deleteBatch',
// 标签
tagList = '/health-emergency/emergency/firstAid/tag/list',
tagListAll = '/health-emergency/emergency/firstAid/tag/listAll',
tagQueryById = '/health-emergency/emergency/firstAid/tag/queryById',
tagAdd = '/health-emergency/emergency/firstAid/tag/add',
tagEdit = '/health-emergency/emergency/firstAid/tag/edit',
tagDelete = '/health-emergency/emergency/firstAid/tag/delete',
tagDeleteBatch = '/health-emergency/emergency/firstAid/tag/deleteBatch',
}
// ============ 分类 ============
export const categoryListApi = (params: any) => defHttp.get({ url: Api.categoryList, params });
export const categoryListAllApi = (params?: any) => defHttp.get({ url: Api.categoryListAll, params });
export const categoryListRootApi = (params?: any) => defHttp.get({ url: Api.categoryListRoot, params });
export const categoryListChildrenApi = (params: any) => defHttp.get({ url: Api.categoryListChildren, params });
export const categoryQueryByIdApi = (params: any) => defHttp.get({ url: Api.categoryQueryById, params });
export const categoryAddApi = (params: any) => defHttp.post({ url: Api.categoryAdd, params }, { successNeedMessage: false });
export const categoryEditApi = (params: any) => defHttp.post({ url: Api.categoryEdit, params }, { successNeedMessage: false });
export const categoryDeleteApi = (params: any, handleSuccess: any) => {
createConfirm({
iconType: 'warning',
title: '确认删除',
content: '是否删除该分类?',
onOk: async () => {
await defHttp.post({ url: Api.categoryDelete, params }, { successNeedMessage: false });
handleSuccess();
},
});
};
export const categoryDeleteBatchApi = (params: any, handleSuccess: any) => {
createConfirm({
iconType: 'warning',
title: '批量删除',
content: `是否删除选中的 ${params?.length || 0} 个分类?`,
onOk: async () => {
await defHttp.post({ url: Api.categoryDeleteBatch, params }, { successNeedMessage: false });
handleSuccess();
},
});
};
// ============ 标签 ============
export const tagListApi = (params: any) => defHttp.get({ url: Api.tagList, params });
export const tagListAllApi = (params?: any) => defHttp.get({ url: Api.tagListAll, params });
export const tagQueryByIdApi = (params: any) => defHttp.get({ url: Api.tagQueryById, params });
export const tagAddApi = (params: any) => defHttp.post({ url: Api.tagAdd, params }, { successNeedMessage: false });
export const tagEditApi = (params: any) => defHttp.post({ url: Api.tagEdit, params }, { successNeedMessage: false });
export const tagDeleteApi = (params: any, handleSuccess: any) => {
createConfirm({
iconType: 'warning',
title: '确认删除',
content: '是否删除该标签?',
onOk: async () => {
await defHttp.post({ url: Api.tagDelete, params }, { successNeedMessage: false });
handleSuccess();
},
});
};
export const tagDeleteBatchApi = (params: any, handleSuccess: any) => {
createConfirm({
iconType: 'warning',
title: '批量删除',
content: `是否删除选中的 ${params?.length || 0} 个标签?`,
onOk: async () => {
await defHttp.post({ url: Api.tagDeleteBatch, params }, { successNeedMessage: false });
handleSuccess();
},
});
};
// ============ 工具:扁平列表转树 ============
export function buildTree(list: any[]): any[] {
const map = new Map<string, any>();
const roots: any[] = [];
list.forEach((item) => {
map.set(item.id, { ...item, children: [] });
});
list.forEach((item) => {
const node = map.get(item.id);
if (item.parentId && map.has(item.parentId)) {
map.get(item.parentId).children.push(node);
} else {
roots.push(node);
}
});
// 移除空 children
const clean = (nodes: any[]) => {
nodes.forEach((n) => {
if (n.children.length === 0) {
delete n.children;
} else {
clean(n.children);
}
});
return nodes;
};
return clean(roots);
}
@@ -0,0 +1,143 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" title="移动端预览" :width="420" :minHeight="500" :showOkBtn="false" cancelText="关闭">
<div class="preview-wrap">
<div class="preview-phone">
<div class="phone-bar">
<span class="phone-status">急救宣教资源库</span>
<span class="phone-time">9:41</span>
</div>
<div class="phone-content">
<div class="phone-cover">
<img v-if="record.coverImage" :src="record.coverImage" class="cover-img" />
<span v-else>{{ typeIcon }}</span>
</div>
<div class="phone-tags">
<a-tag v-if="record.categoryName" color="blue">{{ record.categoryName }}</a-tag>
<a-tag v-if="record.contentType">{{ typeLabel }}</a-tag>
</div>
<div class="phone-title">{{ record.title }}</div>
<div class="phone-meta">
<span>🕐 {{ record.createTime || '-' }}</span>
<span style="margin-left: 8px">👁 {{ record.viewCount || 0 }}次阅读</span>
</div>
<div class="phone-body">
{{ record.summary || '暂无摘要' }}
</div>
<div class="phone-action">
<a-button type="primary" block> 播放视频</a-button>
</div>
<div class="phone-footer">
<span>👍 {{ Math.floor((record.viewCount || 0) / 6) }}</span>
<span> {{ record.favoriteCount || 0 }}</span>
<span>🔗 分享</span>
</div>
</div>
</div>
</div>
</BasicModal>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { CONTENT_TYPE } from '../resource.data';
const record = ref<any>({});
const typeIcon = computed(() => {
return CONTENT_TYPE[record.value.contentType as keyof typeof CONTENT_TYPE]?.icon || '📄';
});
const typeLabel = computed(() => {
return CONTENT_TYPE[record.value.contentType as keyof typeof CONTENT_TYPE]?.label || record.value.contentType;
});
const [registerModal] = useModalInner(async (data) => {
record.value = data?.record || {};
});
</script>
<style lang="less" scoped>
.preview-wrap {
background: #f5f5f5;
padding: 24px;
display: flex;
justify-content: center;
}
.preview-phone {
width: 300px;
border: 10px solid #001529;
border-radius: 32px;
background: #fff;
overflow: hidden;
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px rgba(0, 0, 0, 0.08);
}
.phone-bar {
background: #001529;
height: 40px;
display: flex;
align-items: center;
padding: 0 16px;
}
.phone-status {
color: rgba(255, 255, 255, 0.65);
font-size: 12px;
flex: 1;
}
.phone-time {
color: #fff;
font-size: 12px;
}
.phone-content {
padding: 16px;
}
.phone-cover {
width: 100%;
height: 140px;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
border-radius: 2px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 32px;
margin-bottom: 12px;
overflow: hidden;
}
.cover-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.phone-tags {
margin-bottom: 8px;
}
.phone-title {
font-size: 15px;
font-weight: 600;
margin-bottom: 8px;
color: rgba(0, 0, 0, 0.85);
}
.phone-meta {
font-size: 11px;
color: rgba(0, 0, 0, 0.45);
margin-bottom: 12px;
}
.phone-body {
font-size: 13px;
color: rgba(0, 0, 0, 0.65);
line-height: 1.8;
}
.phone-action {
margin-top: 16px;
}
.phone-footer {
display: flex;
gap: 20px;
margin-top: 14px;
padding-top: 10px;
border-top: 1px solid #f0f0f0;
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
}
</style>
@@ -0,0 +1,280 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :title="title" :width="900" :minHeight="500" :useDefaultFooter="false" :showCancelBtn="false" :showOkBtn="false">
<div class="resource-form">
<a-form layout="vertical" :model="formState">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="标题" required>
<a-input v-model:value="formState.title" placeholder="请输入资源标题" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="内容类型" required>
<a-select v-model:value="formState.contentType" :options="typeOptions" placeholder="请选择内容类型" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="摘要">
<a-input v-model:value="formState.summary" placeholder="请输入资源摘要(一句话简介)" />
</a-form-item>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="所属分类" required>
<a-select v-model:value="formState.categoryId" :options="categoryOptions" placeholder="请选择分类" :field-names="{ label: 'name', value: 'id' }" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="适用场景">
<a-select v-model:value="formState.applicableScenario" :options="sceneOptions" placeholder="请选择适用场景" allow-clear />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="标签">
<a-select
v-model:value="formState.tagIdList"
mode="multiple"
:options="currentCategoryTags"
placeholder="请先选择分类"
:field-names="{ label: 'name', value: 'id' }"
:disabled="!formState.categoryId"
allow-clear
/>
</a-form-item>
<a-form-item label="正文内容" required>
<JEditor v-model:value="formState.content" />
</a-form-item>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="封面图">
<JImageUpload
v-model:value="formState.coverImage"
bizPath="firstAid/cover"
text="上传封面图"
tipText="支持 JPG/PNG,建议尺寸 750×420px"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="附件/媒体文件">
<JUpload
v-model:value="fileList"
bizPath="firstAid/file"
:multiple="true"
text="上传文件"
tipText="支持 MP4/MP3/PDF,单文件最大 500MB"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="生效时间">
<a-date-picker v-model:value="formState.effectiveTime" showTime valueFormat="YYYY-MM-DD HH:mm:ss" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="失效时间">
<a-date-picker v-model:value="formState.expiryTime" showTime valueFormat="YYYY-MM-DD HH:mm:ss" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
</a-form>
</div>
<template #appendFooter>
<a-button @click="closeModal">取消</a-button>
<a-button @click="handleSaveDraft">保存草稿</a-button>
<a-button type="primary" @click="handleSubmit">提交审核</a-button>
</template>
</BasicModal>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { useMessage } from '/@/hooks/web/useMessage';
import { TYPE_OPTIONS, SCENE_OPTIONS } from '../resource.data';
import { addApi, editApi, submitForReviewApi, categoryListAllApi } from '../resource.api';
import JImageUpload from '/@/components/Form/src/jeecg/components/JImageUpload.vue';
import JUpload from '/@/components/Form/src/jeecg/components/JUpload/JUpload.vue';
import JEditor from '/@/components/Form/src/jeecg/components/JEditor.vue';
const emit = defineEmits(['success', 'register']);
const { createMessage } = useMessage();
const title = ref('新增急救宣教资源');
const categoryOptions = ref<any[]>([]);
// JUpload array JSON.stringify fileUrl
const fileList = ref<any[]>([]);
// V3 tags
const currentCategoryTags = computed(() => {
if (!formState.categoryId) return [];
const category = categoryOptions.value.find((c) => c.id === formState.categoryId);
return category?.tags || [];
});
const formState = reactive({
id: '',
title: '',
summary: '',
contentType: 'IMAGE_TEXT',
categoryId: '' as string,
applicableScenario: '' as string,
tagIdList: [] as string[],
content: '',
coverImage: '',
fileUrl: '',
effectiveTime: null as any,
expiryTime: null as any,
});
const typeOptions = TYPE_OPTIONS;
const sceneOptions = SCENE_OPTIONS;
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
if (data?.isUpdate) {
title.value = '编辑急救宣教资源';
Object.assign(formState, data.record);
// tags
if (formState.tagIdList && typeof (formState as any).tags === 'string') {
formState.tagIdList = ((formState as any).tags || '').split(',').filter(Boolean);
}
// fileUrl JSON parse JUpload
try {
fileList.value = formState.fileUrl ? JSON.parse(formState.fileUrl) : [];
} catch {
fileList.value = [];
}
} else {
title.value = '新增急救宣教资源';
resetForm();
}
});
function resetForm() {
formState.id = '';
formState.title = '';
formState.summary = '';
formState.contentType = 'IMAGE_TEXT';
formState.categoryId = '';
formState.applicableScenario = '';
formState.tagIdList = [];
formState.content = '';
formState.coverImage = '';
formState.fileUrl = '';
formState.effectiveTime = null;
formState.expiryTime = null;
fileList.value = [];
}
function buildPayload(): any {
return {
id: formState.id || undefined,
title: formState.title,
summary: formState.summary,
contentType: formState.contentType,
categoryId: formState.categoryId,
applicableScenario: formState.applicableScenario,
tags: formState.tagIdList.join(','),
content: formState.content,
coverImage: formState.coverImage,
fileUrl: JSON.stringify(fileList.value || []),
effectiveTime: formState.effectiveTime,
expiryTime: formState.expiryTime,
};
}
function validate(): boolean {
if (!formState.title) {
createMessage.warning('请输入资源标题');
return false;
}
if (!formState.categoryId) {
createMessage.warning('请选择所属分类');
return false;
}
if (!formState.content) {
createMessage.warning('请输入正文内容');
return false;
}
return true;
}
async function handleSaveDraft() {
if (!formState.title) {
createMessage.warning('请输入资源标题');
return;
}
setModalProps({ confirmLoading: true });
try {
if (formState.id) {
await editApi(buildPayload());
} else {
await addApi(buildPayload());
}
createMessage.success('已保存为草稿');
closeModal();
emit('success');
} finally {
setModalProps({ confirmLoading: false });
}
}
async function handleSubmit() {
if (!validate()) return;
setModalProps({ confirmLoading: true });
try {
const id = formState.id;
if (id) {
await editApi(buildPayload());
await submitForReviewApi({ id });
} else {
const res: any = await addApi(buildPayload());
await submitForReviewApi({ id: res?.id || res });
}
createMessage.success('已提交审核');
closeModal();
emit('success');
} finally {
setModalProps({ confirmLoading: false });
}
}
onMounted(async () => {
// V3 tags
categoryOptions.value = (await categoryListAllApi()) || [];
});
</script>
<style lang="less" scoped>
.resource-form {
padding: 4px 4px 0;
}
.upload-area {
border: 1px dashed #d9d9d9;
border-radius: 2px;
padding: 24px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
background: #fafafa;
&:hover {
border-color: #1890ff;
background: #e6f7ff;
}
}
.upload-icon {
font-size: 32px;
color: rgba(0, 0, 0, 0.25);
}
.upload-text {
font-size: 14px;
color: rgba(0, 0, 0, 0.65);
margin-top: 8px;
}
.upload-hint {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
margin-top: 4px;
}
</style>
@@ -0,0 +1,252 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :title="modalTitle" :width="modalWidth" :minHeight="400" :showOkBtn="false" cancelText="关闭">
<!-- 版本列表视图 -->
<a-spin :spinning="loading" v-if="!diffVisible">
<div class="version-list">
<div v-for="(item, idx) in versions" :key="item.id" class="version-item">
<div class="version-num" :class="{ 'version-old': idx > 0 }">v{{ item.versionNo }}</div>
<div class="version-main">
<div class="version-op">{{ item.changeDescription || item.title }}</div>
<div class="version-meta">
{{ item.createBy }} · {{ item.createTime }}
<a v-if="idx < versions.length - 1" @click="handleDiff(item, idx)">查看差异</a>
<a v-if="idx > 0" @click="handleRollback(item)">回滚</a>
</div>
</div>
</div>
<a-empty v-if="!loading && versions.length === 0" description="暂无版本记录" />
</div>
</a-spin>
<!-- 差异对比视图 -->
<div v-else>
<div class="diff-toolbar">
<a-button size="small" @click="diffVisible = false"> 返回版本列表</a-button>
<span class="diff-toolbar-tip">v{{ diffCurrentVersionNo }} v{{ diffOlderVersionNo }}</span>
</div>
<a-spin :spinning="diffLoading">
<a-row :gutter="16" v-if="diffData">
<a-col :span="12">
<div class="diff-panel diff-old">
<div class="diff-panel-title">
v{{ diffData.version1?.versionNo }} · {{ diffData.version1?.createTime }}
</div>
<div class="diff-panel-sub">{{ diffData.version1?.title }}</div>
<div class="diff-panel-desc">{{ diffData.version1?.changeDescription }}</div>
<div class="diff-panel-body" v-html="diffData.version1?.content || '无内容'"></div>
</div>
</a-col>
<a-col :span="12">
<div class="diff-panel diff-new">
<div class="diff-panel-title">
v{{ diffData.version2?.versionNo }} · {{ diffData.version2?.createTime }}
</div>
<div class="diff-panel-sub">{{ diffData.version2?.title }}</div>
<div class="diff-panel-desc">{{ diffData.version2?.changeDescription }}</div>
<div class="diff-panel-body" v-html="diffData.version2?.content || '无内容'"></div>
</div>
</a-col>
</a-row>
<a-empty v-else description="无差异数据" />
</a-spin>
</div>
</BasicModal>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { useMessage } from '/@/hooks/web/useMessage';
import { versionListApi, versionRollbackApi, versionDiffApi } from '../resource.api';
const { createMessage, createConfirm } = useMessage();
const record = ref<any>({});
const versions = ref<any[]>([]);
const loading = ref(false);
const diffVisible = ref(false);
const diffLoading = ref(false);
const diffData = ref<any>(null);
const diffCurrentVersionNo = ref('');
const diffOlderVersionNo = ref('');
const modalTitle = computed(() => (diffVisible.value ? '版本差异对比' : '版本历史'));
const modalWidth = computed(() => (diffVisible.value ? 900 : 560));
const [registerModal] = useModalInner(async (data) => {
record.value = data?.record || {};
diffVisible.value = false;
await loadVersions();
});
async function loadVersions() {
if (!record.value.id) return;
loading.value = true;
try {
const res = await versionListApi({ resourceId: record.value.id });
versions.value = res || [];
} finally {
loading.value = false;
}
}
async function handleDiff(item: any, idx: number) {
const older = versions.value[idx + 1];
if (!older) {
createMessage.info('无更早版本可对比');
return;
}
diffCurrentVersionNo.value = item.versionNo;
diffOlderVersionNo.value = older.versionNo;
diffVisible.value = true;
diffLoading.value = true;
diffData.value = null;
try {
const res: any = await versionDiffApi({ versionId1: item.id, versionId2: older.id });
// {version1, version2} / [v1, v2] / {old, new}
let v1: any = null;
let v2: any = null;
if (Array.isArray(res)) {
[v1, v2] = res;
} else if (res?.version1 || res?.version2) {
v1 = res.version1;
v2 = res.version2;
} else if (res?.old || res?.new) {
v1 = res.old;
v2 = res.new;
} else if (res?.result) {
const inner = res.result;
if (Array.isArray(inner)) [v1, v2] = inner;
else { v1 = inner.version1 || inner.old; v2 = inner.version2 || inner.new; }
}
diffData.value = { version1: v1, version2: v2 };
} finally {
diffLoading.value = false;
}
}
function handleRollback(item: any) {
createConfirm({
iconType: 'warning',
title: '确认回滚',
content: `是否回滚至 v${item.versionNo}?回滚前会自动保存当前状态为新版本。`,
onOk: async () => {
await versionRollbackApi({ id: item.id });
createMessage.success('已回滚');
await loadVersions();
},
});
}
</script>
<style lang="less" scoped>
.version-list {
padding: 4px 0;
}
.version-item {
display: flex;
gap: 14px;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
}
.version-num {
font-size: 12px;
background: #e6f7ff;
color: #1890ff;
padding: 0 7px;
border-radius: 2px;
white-space: nowrap;
border: 1px solid #91d5ff;
line-height: 20px;
height: fit-content;
}
.version-old {
background: #fafafa;
color: rgba(0, 0, 0, 0.45);
border-color: #d9d9d9;
}
.version-main {
flex: 1;
}
.version-op {
font-size: 14px;
color: rgba(0, 0, 0, 0.85);
}
.version-meta {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
margin-top: 3px;
a {
color: #1890ff;
cursor: pointer;
margin-left: 8px;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
.diff-wrap {
margin-top: 8px;
}
.diff-toolbar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.diff-toolbar-tip {
font-size: 13px;
color: #1890ff;
font-weight: 500;
}
.diff-panel {
border: 1px solid #f0f0f0;
border-radius: 2px;
padding: 12px;
max-height: 500px;
overflow-y: auto;
&.diff-old {
background: #fafafa;
}
&.diff-new {
background: #f6ffed;
border-color: #b7eb8f;
}
}
.diff-panel-title {
font-size: 13px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
margin-bottom: 6px;
}
.diff-panel-sub {
font-size: 13px;
color: rgba(0, 0, 0, 0.65);
margin-bottom: 4px;
}
.diff-panel-desc {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px dashed #e8e8e8;
}
.diff-panel-body {
font-size: 13px;
color: rgba(0, 0, 0, 0.65);
line-height: 1.8;
:deep(img) {
max-width: 100%;
}
}
</style>
+362
View File
@@ -0,0 +1,362 @@
<template>
<BasicTable @register="registerTable" :rowSelection="{ type: 'checkbox', onChange: onSelectChange }">
<template #tableTitle>
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleAdd"> 新增资源 </a-button>
<a-button type="primary" preIcon="ant-design:export-outlined" @click="handleExport"> 导出 </a-button>
<template v-if="selectedRowKeys.length > 0">
<a-button type="primary" preIcon="ant-design:cloud-upload-outlined" @click="handleBatchPublish"> 批量发布 </a-button>
<a-button preIcon="ant-design:cloud-download-outlined" @click="handleBatchOffline"> 批量下架 </a-button>
<a-button danger preIcon="ant-design:delete-outlined" @click="handleBatchDelete"> 批量删除 </a-button>
<span class="batch-tip">已选中 {{ selectedRowKeys.length }} </span>
</template>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'coverImage'">
<div class="cover-box" :style="coverStyle(record)">
<img v-if="record.coverImage" :src="record.coverImage" class="cover-img" />
<span v-else>{{ typeIcon(record.contentType) }}</span>
</div>
</template>
<template v-if="column.dataIndex === 'title'">
<div class="title-cell">
<div class="title-main">{{ record.title }}</div>
<div class="title-sub">{{ record.summary }}</div>
</div>
</template>
<template v-if="column.dataIndex === 'categoryName'">
<a-tag v-if="record.categoryName" color="blue">{{ record.categoryName }}</a-tag>
</template>
<template v-if="column.dataIndex === 'contentType'">
<a-tag :color="typeColor(record.contentType)">{{ typeIcon(record.contentType) }} {{ typeLabel(record.contentType) }}</a-tag>
</template>
<template v-if="column.dataIndex === 'tagNameList'">
<a-tag v-for="tag in (record.tagNameList || [])" :key="tag" color="default">{{ tag }}</a-tag>
</template>
<template v-if="column.dataIndex === 'statusDisplay'">
<a-badge :status="getStatusInfo(record).color" :text="getStatusInfo(record).text" />
</template>
<template v-if="column.dataIndex === 'viewFavorite'">
<span>{{ record.viewCount || 0 }} / {{ record.favoriteCount || 0 }}</span>
</template>
</template>
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
</template>
</BasicTable>
<ResourceFormModal @register="registerFormModal" @success="reload" />
<PreviewModal @register="registerPreviewModal" />
<VersionHistoryModal @register="registerVersionModal" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import BasicTable from '/@/components/Table/src/BasicTable.vue';
import { TableAction } from '/@/components/Table';
import { useListPage } from '/@/hooks/system/useListPage';
import { useMessage } from '/@/hooks/web/useMessage';
import { useModal } from '/@/components/Modal';
import {
columns,
searchSchema,
CONTENT_TYPE,
STATUS,
AUDIT_STATUS,
getResourceDisplayStatus,
} from './resource.data';
import {
listApi,
deleteApi,
batchDeleteApi,
submitForReviewApi,
takeDownApi,
publishApi,
copyApi,
batchPublishApi,
batchTakeDownApi,
exportXlsApi,
} from './resource.api';
import ResourceFormModal from './components/ResourceFormModal.vue';
import PreviewModal from './components/PreviewModal.vue';
import VersionHistoryModal from './components/VersionHistoryModal.vue';
const { createMessage, createConfirm } = useMessage();
const selectedRowKeys = ref<string[]>([]);
const [registerFormModal, { openModal: openFormModal }] = useModal();
const [registerPreviewModal, { openModal: openPreviewModal }] = useModal();
const [registerVersionModal, { openModal: openVersionModal }] = useModal();
const { tableContext } = useListPage({
tableProps: {
title: '急救宣教资源列表',
api: listApi,
columns,
canResize: false,
showIndexColumn: false,
rowKey: 'id',
formConfig: {
schemas: searchSchema,
autoSubmitOnEnter: true,
showAdvancedButton: false,
},
actionColumn: {
width: 280,
fixed: 'right',
},
afterFetch: (data) => {
data.forEach((item: any) => {
const display = getResourceDisplayStatus(item.status, item.auditStatus);
item.statusDisplay = display;
});
return data;
},
},
});
const [registerTable, { reload, getForm }] = tableContext;
function onSelectChange(keys: string[]) {
selectedRowKeys.value = keys;
}
function coverStyle(record: Recordable) {
const t = CONTENT_TYPE[record.contentType as keyof typeof CONTENT_TYPE];
const bgMap: Record<string, string> = {
IMAGE_TEXT: '#fa8c16',
VIDEO: '#001529',
AUDIO: '#8c8c8c',
PDF: '#434343',
};
return { background: bgMap[record.contentType] || '#001529' };
}
function typeColor(type: string) {
return CONTENT_TYPE[type as keyof typeof CONTENT_TYPE]?.color || 'default';
}
function typeIcon(type: string) {
return CONTENT_TYPE[type as keyof typeof CONTENT_TYPE]?.icon || '';
}
function typeLabel(type: string) {
return CONTENT_TYPE[type as keyof typeof CONTENT_TYPE]?.label || type;
}
function getStatusInfo(record: Recordable) {
return getResourceDisplayStatus(record.status, record.auditStatus);
}
//
const isPublished = (r: Recordable) => r.status === STATUS.PUBLISHED;
const isOffline = (r: Recordable) => r.status === STATUS.OFFLINE;
const isPending = (r: Recordable) => r.auditStatus === AUDIT_STATUS.PENDING;
const isRejected = (r: Recordable) => r.auditStatus === AUDIT_STATUS.REJECTED;
const isDraft = (r: Recordable) => r.status === STATUS.DRAFT && r.auditStatus == null;
function getTableAction(record: Recordable) {
const actions: any[] = [
{
label: '预览',
onClick: () => openPreviewModal(true, { record }),
},
{
label: '编辑',
onClick: () => openFormModal(true, { isUpdate: true, record }),
},
];
if (isPublished(record)) {
actions.push({
label: '下架',
color: 'error',
popConfirm: {
title: `是否下架《${record.title}》?`,
confirm: () => handleOffline(record),
},
});
} else if (isOffline(record)) {
actions.push({
label: '上架',
onClick: () => handlePublish(record),
});
} else if (isPending(record)) {
//
} else if (isRejected(record)) {
actions.push({
label: '提交审核',
onClick: () => handleSubmitReview(record),
});
} else if (isDraft(record)) {
actions.push({
label: '提交审核',
onClick: () => handleSubmitReview(record),
});
actions.push({
label: '删除',
color: 'error',
popConfirm: {
title: `是否删除《${record.title}》?`,
confirm: () => handleDelete(record),
},
});
} else if (record.status === STATUS.DRAFT && record.auditStatus === AUDIT_STATUS.PASSED) {
// V3
actions.push({
label: '上架',
onClick: () => handlePublish(record),
});
}
return actions;
}
function getDropDownAction(record: Recordable) {
const dropdown: any[] = [
{
label: '版本历史',
onClick: () => openVersionModal(true, { record }),
},
];
// 稿
if (!isDraft(record)) {
dropdown.push({
label: '复制',
onClick: () => handleCopy(record),
});
}
//
if (isRejected(record) || isOffline(record)) {
dropdown.push({
label: '删除',
color: 'error',
popConfirm: {
title: `是否删除《${record.title}》?`,
confirm: () => handleDelete(record),
},
});
}
return dropdown;
}
function handleAdd() {
openFormModal(true, { isUpdate: false });
}
async function handleExport() {
const formValues = getForm().getFieldsValue() || {};
const params: any = {
title: formValues.title || undefined,
categoryId: formValues.categoryId || undefined,
contentType: formValues.contentType || undefined,
// combinedStatus status/auditStatus9 status10 auditStatus
status: formValues.combinedStatus != null && formValues.combinedStatus <= 9 ? formValues.combinedStatus : undefined,
auditStatus: formValues.combinedStatus != null && formValues.combinedStatus >= 10 ? formValues.combinedStatus : undefined,
//
selections: selectedRowKeys.value.length > 0 ? selectedRowKeys.value.join(',') : undefined,
};
Object.keys(params).forEach((k) => params[k] === undefined && delete params[k]);
await exportXlsApi(params);
createMessage.success('导出成功');
}
async function handleOffline(record: Recordable) {
await takeDownApi({ id: record.id });
createMessage.success('已下架');
reload();
}
async function handlePublish(record: Recordable) {
await publishApi({ id: record.id });
createMessage.success('已上架');
reload();
}
async function handleSubmitReview(record: Recordable) {
await submitForReviewApi({ id: record.id });
createMessage.success('已提交审核');
reload();
}
async function handleCopy(record: Recordable) {
await copyApi({ id: record.id });
createMessage.success('已复制为草稿');
reload();
}
function handleDelete(record: Recordable) {
deleteApi({ id: record.id }, () => {
createMessage.success('已删除');
reload();
});
}
function handleBatchPublish() {
createConfirm({
iconType: 'info',
title: '批量发布',
content: `是否发布选中的 ${selectedRowKeys.value.length} 条资源?`,
onOk: async () => {
await batchPublishApi(selectedRowKeys.value);
createMessage.success('批量发布成功');
selectedRowKeys.value = [];
reload();
},
});
}
function handleBatchOffline() {
createConfirm({
iconType: 'warning',
title: '批量下架',
content: `是否下架选中的 ${selectedRowKeys.value.length} 条资源?`,
onOk: async () => {
await batchTakeDownApi(selectedRowKeys.value);
createMessage.success('批量下架成功');
selectedRowKeys.value = [];
reload();
},
});
}
function handleBatchDelete() {
batchDeleteApi(selectedRowKeys.value, () => {
createMessage.success('已批量删除');
selectedRowKeys.value = [];
reload();
});
}
</script>
<style lang="less" scoped>
.batch-tip {
margin-left: 8px;
color: #1890ff;
font-size: 13px;
}
.cover-box {
width: 50px;
height: 36px;
border-radius: 2px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
margin: 0 auto;
overflow: hidden;
}
.cover-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.title-cell {
.title-main {
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
}
.title-sub {
font-size: 11px;
color: #999;
margin-top: 2px;
}
}
</style>
+83
View File
@@ -0,0 +1,83 @@
import { defHttp } from '/@/utils/http/axios';
import { useMessage } from '/@/hooks/web/useMessage';
import { downloadFile } from '/@/api/common/api';
const { createConfirm } = useMessage();
enum Api {
// 资源 CRUD
list = '/health-emergency/emergency/firstAid/resource/list',
queryById = '/health-emergency/emergency/firstAid/resource/queryById',
add = '/health-emergency/emergency/firstAid/resource/add',
edit = '/health-emergency/emergency/firstAid/resource/edit',
delete = '/health-emergency/emergency/firstAid/resource/delete',
deleteBatch = '/health-emergency/emergency/firstAid/resource/deleteBatch',
submitForReview = '/health-emergency/emergency/firstAid/resource/submitForReview',
takeDown = '/health-emergency/emergency/firstAid/resource/takeDown',
publish = '/health-emergency/emergency/firstAid/resource/publish',
batchTakeDown = '/health-emergency/emergency/firstAid/resource/batchTakeDown',
batchPublish = '/health-emergency/emergency/firstAid/resource/batchPublish',
incrementView = '/health-emergency/emergency/firstAid/resource/incrementView',
copy = '/health-emergency/emergency/firstAid/resource/copy',
exportXls = '/health-emergency/emergency/firstAid/resource/exportXls',
// 版本
versionList = '/health-emergency/emergency/firstAid/version/list',
versionRollback = '/health-emergency/emergency/firstAid/version/rollback',
versionDiff = '/health-emergency/emergency/firstAid/version/diff',
// 分类 & 标签(下拉用)
categoryListAll = '/health-emergency/emergency/firstAid/category/listAll',
tagListAll = '/health-emergency/emergency/firstAid/tag/listAll',
}
export const listApi = (params: any) => defHttp.get({ url: Api.list, params });
export const queryByIdApi = (params: any) => defHttp.get({ url: Api.queryById, params });
export const addApi = (params: any) => defHttp.post({ url: Api.add, params }, { successNeedMessage: false });
export const editApi = (params: any) => defHttp.post({ url: Api.edit, params }, { successNeedMessage: false });
export const submitForReviewApi = (params: any) => defHttp.post({ url: Api.submitForReview, params }, { successNeedMessage: false });
export const takeDownApi = (params: any) => defHttp.post({ url: Api.takeDown, params }, { successNeedMessage: false });
export const publishApi = (params: any) => defHttp.post({ url: Api.publish, params }, { successNeedMessage: false });
export const batchTakeDownApi = (params: any) => defHttp.post({ url: Api.batchTakeDown, params }, { successNeedMessage: false });
export const batchPublishApi = (params: any) => defHttp.post({ url: Api.batchPublish, params }, { successNeedMessage: false });
export const incrementViewApi = (params: any) => defHttp.post({ url: Api.incrementView, params });
export const copyApi = (params: any) => defHttp.post({ url: Api.copy, params }, { successNeedMessage: false });
/**
* Excel
* selectionsID+
*/
export const exportXlsApi = (params: any) => {
return downloadFile(Api.exportXls, '急救宣教资源报表.xls', params);
};
export const versionListApi = (params: any) => defHttp.get({ url: Api.versionList, params });
export const versionRollbackApi = (params: any) => defHttp.post({ url: Api.versionRollback, params }, { successNeedMessage: false });
export const versionDiffApi = (params: any) => defHttp.get({ url: Api.versionDiff, params });
export const categoryListAllApi = (params?: any) => defHttp.get({ url: Api.categoryListAll, params });
export const tagListAllApi = (params?: any) => defHttp.get({ url: Api.tagListAll, params });
export const deleteApi = (params: any, handleSuccess: any) => {
createConfirm({
iconType: 'warning',
title: '确认删除',
content: '是否将选中资源移入回收站?',
okText: '确认',
cancelText: '取消',
onOk: async () => {
await defHttp.post({ url: Api.delete, params }, { successNeedMessage: false });
handleSuccess();
},
});
};
export const batchDeleteApi = (params: any, handleSuccess: any) => {
createConfirm({
iconType: 'warning',
title: '批量删除',
content: `是否将选中的 ${params?.length || 0} 条资源移入回收站?`,
okText: '确认',
cancelText: '取消',
onOk: async () => {
await defHttp.post({ url: Api.deleteBatch, params }, { successNeedMessage: false });
handleSuccess();
},
});
};
+168
View File
@@ -0,0 +1,168 @@
import { BasicColumn, FormSchema } from '/@/components/Table';
// 发布状态
export const STATUS = {
DRAFT: 0,
PUBLISHED: 1,
OFFLINE: 2,
};
// 审核状态
export const AUDIT_STATUS = {
PENDING: 10,
PASSED: 11,
REJECTED: 12,
};
// 复合状态(搜索用)
export const COMBINED_STATUS = {
DRAFT: '0',
PUBLISHED: '1',
OFFLINE: '2',
PENDING: '10',
PASSED: '11',
REJECTED: '12',
};
// 内容类型
export const CONTENT_TYPE = {
IMAGE_TEXT: { label: '图文', color: 'orange', icon: '📄' },
VIDEO: { label: '视频', color: 'purple', icon: '🎬' },
AUDIO: { label: '音频', color: 'green', icon: '🎧' },
PDF: { label: 'PDF', color: 'red', icon: '📑' },
};
// 根据发布状态+审核状态返回展示信息
// V3:审核通过但未上架 (auditStatus=11 && status=0) 显示为「已通过」
export function getResourceDisplayStatus(status: number, auditStatus: number | null) {
if (status === STATUS.PUBLISHED) return { text: '已发布', color: 'success' };
if (status === STATUS.OFFLINE) return { text: '已下架', color: 'error' };
if (auditStatus === AUDIT_STATUS.PENDING) return { text: '待审核', color: 'warning' };
if (auditStatus === AUDIT_STATUS.PASSED) return { text: '已通过', color: 'processing' };
if (auditStatus === AUDIT_STATUS.REJECTED) return { text: '已驳回', color: 'error' };
return { text: '草稿', color: 'default' };
}
// 内容类型选项
export const TYPE_OPTIONS = [
{ label: '图文', value: 'IMAGE_TEXT' },
{ label: '视频', value: 'VIDEO' },
{ label: '音频', value: 'AUDIO' },
{ label: 'PDF', value: 'PDF' },
];
// 复合状态选项
export const STATUS_OPTIONS = [
{ label: '草稿', value: '0' },
{ label: '已发布', value: '1' },
{ label: '已下架', value: '2' },
{ label: '待审核', value: '10' },
{ label: '已通过', value: '11' },
{ label: '已驳回', value: '12' },
];
// 适用场景
export const SCENE_OPTIONS = [
{ label: '工作现场', value: '工作现场' },
{ label: '日常生活', value: '日常生活' },
{ label: '高原环境', value: '高原环境' },
{ label: '高温环境', value: '高温环境' },
];
// 表格列定义
export const columns: BasicColumn[] = [
{
title: '封面',
dataIndex: 'coverImage',
width: 80,
align: 'center',
},
{
title: '标题',
dataIndex: 'title',
align: 'left',
},
{
title: '分类',
dataIndex: 'categoryName',
width: 100,
align: 'center',
},
{
title: '类型',
dataIndex: 'contentType',
width: 90,
align: 'center',
},
{
title: '标签',
dataIndex: 'tagNameList',
width: 160,
align: 'left',
},
{
title: '状态',
dataIndex: 'statusDisplay',
width: 100,
align: 'center',
},
{
title: '阅读/收藏',
dataIndex: 'viewFavorite',
width: 110,
align: 'center',
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 170,
align: 'center',
},
];
// 搜索表单
export const searchSchema: FormSchema[] = [
{
field: 'title',
label: '标题',
component: 'Input',
componentProps: {
placeholder: '搜索标题',
},
colProps: { span: 6 },
},
{
field: 'categoryId',
label: '分类',
component: 'ApiSelect',
componentProps: {
api: () => import('./resource.api').then((m) => m.categoryListAllApi()),
labelField: 'name',
valueField: 'id',
placeholder: '全部分类',
},
colProps: { span: 6 },
},
{
field: 'combinedStatus',
label: '状态',
component: 'Select',
componentProps: {
options: STATUS_OPTIONS,
placeholder: '全部状态',
allowClear: true,
},
colProps: { span: 6 },
},
{
field: 'contentType',
label: '类型',
component: 'Select',
componentProps: {
options: TYPE_OPTIONS,
placeholder: '全部类型',
allowClear: true,
},
colProps: { span: 6 },
},
];
+1 -1
View File
@@ -73,7 +73,7 @@ export const formSchema: FormSchema[] = [
dynamicDisabled: ({ values }) => {
return !!values.id;
},
dynamicRules: ({ model, schema }) => rules.duplicateCheckRule('sys_dict', 'dict_code', model, schema, true),
dynamicRules: ({ model, schema }) => rules.duplicateCheckRule('QH_SYS_DICT', 'dict_code', model, schema, true),
},
{
label: '描述',
@@ -12,6 +12,17 @@ export const columns: BasicColumn[] = [
width: 200,
// slots: { customRender: 'esContent' },
},
{
title: '员工电话',
dataIndex: 'employeePhone',
width: 140,
// 从发送内容中提取 3位~4位~4位 格式的手机号(如 185~9920~5354),取第一个匹配项并去掉分隔符显示纯数字,匹配不到则显示空
// 分隔符兼容半角~、全角~、连字符-
customRender: ({ record }) => {
const match = record?.esContent?.match(/\d{3}[~\-]\d{4}[~\-]\d{4}/);
return match ? match[0].replace(/[~\-]/g, '') : '';
},
},
{
title: '接收人',
dataIndex: 'esReceiver',
@@ -50,6 +61,12 @@ export const searchFormSchema: FormSchema[] = [
field: 'esReceiver',
component: 'Input',
},
{
// 员工电话: 传到后台后由后台拼回波浪线再模糊查询 esContent
label: '员工电话',
field: 'employeePhone',
component: 'Input',
},
{
label: '发送状态',
field: 'esSendStatus',
@@ -77,7 +77,7 @@ export const formSchemas: FormSchema[] = [
field: 'templateCode',
component: 'Input',
dynamicRules: ({ model, schema }) => {
return [...rules.duplicateCheckRule('sys_sms_template', 'template_code', model, schema, true)];
return [...rules.duplicateCheckRule('QH_SYS_SMS_TEMPLATE', 'template_code', model, schema, true)];
},
// 编辑模式下不可修改编码
dynamicDisabled: (params) => !!params.values.id,
@@ -48,6 +48,22 @@ export const columns: BasicColumn[] = [
align: 'center',
dataIndex: 'isLongTermEffective_dictText',
},
{
title: '跳转方式',
align: 'center',
dataIndex: 'tfJump',
customRender: ({ text }) => {
// 跳转方式文案映射:0-不跳转 1-链接 2-app内
const map = { '0': '不跳转', '1': '链接', '2': 'app内' };
return map[text] ?? '';
},
},
{
title: '跳转链接',
align: 'center',
dataIndex: 'jumpUrl',
ellipsis: true,
},
];
//查询数据
export const searchFormSchema: FormSchema[] = [
@@ -74,6 +90,8 @@ export const searchFormSchema: FormSchema[] = [
},
];
//表单数据
// @ts-ignore
// @ts-ignore
export const formSchema: FormSchema[] = [
{
label: '序号',
@@ -91,6 +109,10 @@ export const formSchema: FormSchema[] = [
label: '图片',
field: 'photoUrl',
component: 'JImageUpload',
componentProps: {
bizPath: 'img',
maxCount: 1,
},
rules: [{ required: true, message: '请上传图片' }],
},
{
@@ -102,6 +124,33 @@ export const formSchema: FormSchema[] = [
},
rules: [{ required: true }],
},
{
label: '跳转方式',
field: 'tfJump',
component: 'RadioButtonGroup',
defaultValue: '0',
componentProps: {
options: [
{ label: '不跳转', value: '0' },
{ label: '链接', value: '1' },
{ label: 'app内', value: '2' },
],
},
rules: [{ required: true, message: '请选择跳转方式' }],
},
{
label: '跳转链接',
field: 'jumpUrl',
component: 'Input',
componentProps: {
placeholder: '请输入跳转链接',
},
// 仅跳转方式为"链接"时显示且必填
ifShow: ({ model }) => model.tfJump === '1',
dynamicRules: ({ model }) => {
return [{ required: model.tfJump === '1', message: '请输入跳转链接' }];
},
},
{
label: '是否有效',
field: 'status',
@@ -85,6 +85,13 @@ export const columns: BasicColumn[] = [
dataIndex: ['departTree', '3'],
width: 180,
},
{
title: '非关爱人员',
align: 'center',
dataIndex: ['extension', 'nonCareFlag'],
width: 100,
slots: { customRender: 'nonCareTag' },
},
{
title: '出生日期',
align: 'center',
@@ -160,6 +167,17 @@ export const searchFormSchema: FormSchema[] = [
field: 'phone',
component: 'Input',
},
{
label: '非关爱人员',
field: 'nonCareFlag',
component: 'Select',
componentProps: {
options: [
{ label: '否', value: 0 },
{ label: '是', value: 1 },
],
},
},
{
label: '单位名称',
field: 'orgCode',
@@ -311,7 +329,7 @@ export const formSchema: (isUpdate: boolean) => FormSchema[] = (isUpdate) => {
};
},
dynamicRules: ({ model, schema }) => {
let rule = rules.duplicateCheckRuleAndDelFlag('health_user_employee_ex', 'emp_sysno', 'false', model, schema, false);
let rule = rules.duplicateCheckRuleAndDelFlag('QH_HLT_USER_EMPLOYEE_EX', 'EMP_SYSNO', 'false', model, schema, false);
rule[0] = { ...rule[0], required: false };
return rule;
},
@@ -330,6 +348,8 @@ export const formSchema: (isUpdate: boolean) => FormSchema[] = (isUpdate) => {
field: 'avatar',
component: 'JImageUpload',
componentProps: {
text: '上传头像',
bizPath: 'avatar',
maxCount: 1,
},
},
@@ -674,28 +694,44 @@ export const formSchema: (isUpdate: boolean) => FormSchema[] = (isUpdate) => {
label: '关爱联络员姓名',
field: 'careLiaisonName',
component: 'Input',
show: false,
},
{
label: '关爱联络员职务',
field: 'careLiaisonJob',
component: 'Input',
show: false,
},
{
label: '关爱联络员电话',
field: 'careLiaisonPhone',
component: 'Input',
rules: [{ required: false, pattern: /^1[3456789]\d{9}$/, message: '手机号码格式有误', trigger: 'blur' }],
// rules: [{ required: false, pattern: /^1[3456789]\d{9}$/, message: '手机号码格式有误', trigger: 'blur' }],
show: false,
},
{
label: '急救人员姓名',
field: 'lifeguardName',
component: 'Input',
show: false,
},
{
label: '急救人员电话',
field: 'lifeguardPhone',
component: 'Input',
rules: [{ required: false, pattern: /^1[3456789]\d{9}$/, message: '手机号码格式有误', trigger: 'blur' }],
// rules: [{ required: false, pattern: /^1[3456789]\d{9}$/, message: '手机号码格式有误', trigger: 'blur' }],
show: false,
},
{
label: '非关爱人员',
field: 'nonCareFlag',
component: 'RadioButtonGroup',
componentProps: {
options: [
{ label: '否', value: 0 },
{ label: '是', value: 1 },
],
},
},
{
label: '',
@@ -109,6 +109,10 @@
{{ record.status_dictText }}
</Tag>
</template>
<template #nonCareTag="{ record }">
<Tag v-if="record.extension?.nonCareFlag === 1" color="orange"></Tag>
<Tag v-else-if="record.extension?.nonCareFlag === 0" color="blue"></Tag>
</template>
</BasicTable>
<!-- 表单区域 -->
<HealthUserEmployeeExDrawer :showFooter="showFooter" @register="registerDrawer" @success="handleSuccess" />
@@ -207,6 +207,8 @@ export const formSchema: FormSchema[] = [
label: '头像',
component: 'JImageUpload',
componentProps: {
text: '上传头像',
bizPath: 'avatar',
maxCount: 1,
},
},
@@ -141,6 +141,8 @@ export const formSchema = function (showGoodAt: boolean): FormSchema[] {
component: 'JImageUpload',
required: true,
componentProps: () => ({
text: '上传头像',
bizPath: 'avatar',
maxCount: 1,
}),
},
+4 -4
View File
@@ -219,7 +219,7 @@ export const formSchema: FormSchema[] = [
},
dynamicRules: ({ model, schema }) => {
if (model.id) {
return rules.duplicateCheckRule('sys_user', 'username', model, schema, true);
return rules.duplicateCheckRule('QH_SYS_USER', 'username', model, schema, true);
} else {
return Promise.resolve();
}
@@ -266,7 +266,7 @@ export const formSchema: FormSchema[] = [
field: 'workNo',
required: true,
component: 'Input',
dynamicRules: ({ model, schema }) => rules.duplicateCheckRule('sys_user', 'work_no', model, schema, true),
dynamicRules: ({ model, schema }) => rules.duplicateCheckRule('QH_SYS_USER', 'work_no', model, schema, true),
},
{
label: '职务',
@@ -379,7 +379,7 @@ export const formSchema: FormSchema[] = [
field: 'email',
component: 'Input',
dynamicRules: ({ model, schema }) => {
return [{ ...rules.duplicateCheckRule('sys_user', 'email', model, schema, true)[0] }, { ...rules.rule('email', false)[0] }];
return [{ ...rules.duplicateCheckRule('QH_SYS_USER', 'email', model, schema, true)[0] }, { ...rules.rule('email', false)[0] }];
},
},
{
@@ -388,7 +388,7 @@ export const formSchema: FormSchema[] = [
component: 'Input',
dynamicRules: ({ model, schema }) => {
return [
{ ...rules.duplicateCheckRule('sys_user', 'phone', model, schema, true)[0] },
{ ...rules.duplicateCheckRule('QH_SYS_USER', 'phone', model, schema, true)[0] },
{ pattern: /^1[3456789]\d{9}$/, message: '手机号码格式有误' },
];
},
@@ -35,7 +35,7 @@
const validatorRules: Record<string, Rule[]> = {
email: [
{ ...rules.duplicateCheckRule('sys_user', 'email', formState, { label: '邮箱' })[0] },
{ ...rules.duplicateCheckRule('QH_SYS_USER', 'email', formState, { label: '邮箱' })[0] },
{ required: true, type: 'email', message: '邮箱格式不正确' },
],
};
@@ -40,7 +40,7 @@
const validatorRules: Record<string, Rule[]> = {
phone: [
{ ...rules.duplicateCheckRule('sys_user', 'phone', formState, { label: '手机号' })[0] },
{ ...rules.duplicateCheckRule('QH_SYS_USER', 'phone', formState, { label: '手机号' })[0] },
{ pattern: /^1[3456789]\d{9}$/, message: '手机号码格式有误' },
],
smscode: [{ required: true, message: '请输入验证码' }],
@@ -154,6 +154,7 @@ export const itemFormSchema: FormSchema[] = [
component: 'JUpload',
componentProps: () => {
return {
bizPath: 'appfile',
maxCount: 1,
};
},