Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8152c4c559 | ||
|
|
141cf2b856 | ||
|
|
97d51c05ea | ||
|
|
380c495f40 | ||
|
|
1da2e17514 | ||
|
|
0241e7b083 | ||
|
|
5d7df50e25 | ||
|
|
48eaec6393 | ||
|
|
375011991e | ||
|
|
9c6680146f | ||
|
|
c01fd0741d | ||
|
|
6bb4b97ff4 | ||
|
|
a2a0e274f4 | ||
|
|
0fa2e605a5 | ||
|
|
a47c2c7400 | ||
|
|
8e7b857dbd | ||
|
|
8044ce0461 | ||
|
|
592ffe1f50 | ||
|
|
eeb1496483 | ||
|
|
3da41ae2d3 | ||
|
|
7106e99a7b | ||
|
|
c1a56aefd9 | ||
|
|
4f848e7ce9 | ||
|
|
a9db38e5e1 | ||
|
|
80627d24c0 | ||
|
|
a24c936cde | ||
|
|
5db0c7effc | ||
|
|
28707b346e | ||
|
|
8eff9119c7 |
@@ -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=
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
# 接口前缀
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,6 +3,7 @@ node_modules
|
||||
.github
|
||||
.gitee
|
||||
dist
|
||||
renkang
|
||||
.npmrc
|
||||
.cache
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'));
|
||||
// 删除预加载
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"applinks": {
|
||||
"apps": [],
|
||||
"details": [
|
||||
{
|
||||
"appID": "A7CC5LW224.com.questionTools.XinjiangProject",
|
||||
"paths": ["*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
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 |
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
fontSize: props.size ?? '30px',
|
||||
opacity: 0.5,
|
||||
transition: 'opacity 0.5s',
|
||||
display: 'none',
|
||||
display: 'block',
|
||||
});
|
||||
|
||||
const bodyStyle = ref({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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 }],
|
||||
|
||||
@@ -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();
|
||||
// 当前应急工单ID(IM 会话 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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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/auditStatus:≤9 查 status,≥10 查 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>
|
||||
@@ -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
|
||||
* 同步导出,返回文件流。参数支持 selections(勾选ID,逗号分隔)+ 筛选条件
|
||||
*/
|
||||
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();
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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 },
|
||||
},
|
||||
];
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
|
||||