Changes
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
@@ -0,0 +1,439 @@
|
||||
;(function (window, $) {
|
||||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition
|
||||
const commandWords = [
|
||||
"打开",
|
||||
"进入",
|
||||
"跳转",
|
||||
"跳到",
|
||||
"去",
|
||||
"访问",
|
||||
"帮我",
|
||||
"请",
|
||||
"页面",
|
||||
"菜单",
|
||||
"一下"
|
||||
]
|
||||
|
||||
const state = {
|
||||
recognition: null,
|
||||
listening: false,
|
||||
menus: null,
|
||||
parentMap: {},
|
||||
lastText: "",
|
||||
pendingMatches: [],
|
||||
awaitingChoice: false,
|
||||
afterRecognitionEnd: null
|
||||
}
|
||||
|
||||
function normalizePlain(text) {
|
||||
return String(text || "")
|
||||
.toLowerCase()
|
||||
.replace(/[,。!?、,.!?;;::\s]/g, "")
|
||||
}
|
||||
|
||||
function normalizeCommand(text) {
|
||||
return normalizePlain(text)
|
||||
.replace(new RegExp(commandWords.join("|"), "g"), "")
|
||||
}
|
||||
|
||||
function notify(message, type) {
|
||||
if (window.ELEMENT && ELEMENT.Message) {
|
||||
ELEMENT.Message({message, type: type || "info"})
|
||||
return
|
||||
}
|
||||
window.alert(message)
|
||||
}
|
||||
|
||||
function speak(message, onEnd) {
|
||||
if (!window.speechSynthesis || !window.SpeechSynthesisUtterance) {
|
||||
if (typeof onEnd === "function") {
|
||||
onEnd()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
window.speechSynthesis.cancel()
|
||||
const utterance = new SpeechSynthesisUtterance(message)
|
||||
let ended = false
|
||||
function finish() {
|
||||
if (ended) {
|
||||
return
|
||||
}
|
||||
ended = true
|
||||
if (typeof onEnd === "function") {
|
||||
onEnd()
|
||||
}
|
||||
}
|
||||
|
||||
utterance.lang = "zh-CN"
|
||||
utterance.rate = 1
|
||||
utterance.volume = 1
|
||||
utterance.onend = function () {
|
||||
finish()
|
||||
}
|
||||
utterance.onerror = function () {
|
||||
finish()
|
||||
}
|
||||
window.speechSynthesis.speak(utterance)
|
||||
setTimeout(finish, Math.max(2500, message.length * 220))
|
||||
}
|
||||
|
||||
function runAfterRecognitionEnd(callback) {
|
||||
state.afterRecognitionEnd = callback
|
||||
if (!state.listening && typeof state.afterRecognitionEnd === "function") {
|
||||
const next = state.afterRecognitionEnd
|
||||
state.afterRecognitionEnd = null
|
||||
setTimeout(next, 150)
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
return String(text || "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'")
|
||||
}
|
||||
|
||||
function flattenMenus(menus, parentId, result) {
|
||||
result = result || []
|
||||
;(menus || []).forEach(function (menu) {
|
||||
const id = menu.id
|
||||
const realParentId = menu.parentId || parentId || ""
|
||||
if (id) {
|
||||
state.parentMap[id] = realParentId
|
||||
}
|
||||
result.push(menu)
|
||||
if (menu.children && menu.children.length) {
|
||||
flattenMenus(menu.children, id, result)
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
function getStoreMenus() {
|
||||
try {
|
||||
return window.store && window.store.state && window.store.state.user && window.store.state.user.menus
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function getSessionMenus() {
|
||||
try {
|
||||
return JSON.parse(window.sessionStorage.getItem("zhgh_sub_app_menus") || "[]")
|
||||
} catch (e) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function getMenus() {
|
||||
const cached = state.menus
|
||||
if (cached && cached.length) {
|
||||
return $.Deferred().resolve(cached).promise()
|
||||
}
|
||||
|
||||
const storeMenus = getStoreMenus()
|
||||
if (storeMenus && storeMenus.length) {
|
||||
state.menus = flattenMenus(storeMenus, "", []).filter(function (menu) {
|
||||
return menu.href
|
||||
})
|
||||
return $.Deferred().resolve(state.menus).promise()
|
||||
}
|
||||
|
||||
return $.get("/platform/sys/user/getLogonUser").then(function (res) {
|
||||
if (res && res.code === 0 && res.data && res.data.menus) {
|
||||
state.parentMap = {}
|
||||
state.menus = flattenMenus(res.data.menus, "", []).filter(function (menu) {
|
||||
return menu.href
|
||||
})
|
||||
return state.menus
|
||||
}
|
||||
const sessionMenus = getSessionMenus()
|
||||
if (sessionMenus && sessionMenus.length) {
|
||||
state.menus = flattenMenus(sessionMenus, "", []).filter(function (menu) {
|
||||
return menu.href
|
||||
})
|
||||
return state.menus
|
||||
}
|
||||
return []
|
||||
}, function () {
|
||||
const sessionMenus = getSessionMenus()
|
||||
if (sessionMenus && sessionMenus.length) {
|
||||
state.menus = flattenMenus(sessionMenus, "", []).filter(function (menu) {
|
||||
return menu.href
|
||||
})
|
||||
return state.menus
|
||||
}
|
||||
return []
|
||||
})
|
||||
}
|
||||
|
||||
function scoreMenu(menu, rawText) {
|
||||
const query = normalizeCommand(rawText)
|
||||
const rawQuery = normalizePlain(rawText)
|
||||
const name = normalizePlain(menu.name)
|
||||
const aliasName = normalizePlain(menu.aliasName)
|
||||
const href = normalizePlain(menu.href)
|
||||
const permission = normalizePlain(menu.permission)
|
||||
|
||||
if (!query || !name) {
|
||||
return 0
|
||||
}
|
||||
if (query === name || rawQuery === name || query === aliasName || rawQuery === aliasName) {
|
||||
return 100
|
||||
}
|
||||
if (name.indexOf(query) > -1 || name.indexOf(rawQuery) > -1 || aliasName.indexOf(query) > -1 || aliasName.indexOf(rawQuery) > -1) {
|
||||
return 80
|
||||
}
|
||||
if (query.indexOf(name) > -1 || rawQuery.indexOf(name) > -1 || (aliasName && (query.indexOf(aliasName) > -1 || rawQuery.indexOf(aliasName) > -1))) {
|
||||
return 70
|
||||
}
|
||||
if (href.indexOf(query) > -1 || permission.indexOf(query) > -1) {
|
||||
return 45
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function matchMenus(text, menus) {
|
||||
return (menus || [])
|
||||
.map(function (menu) {
|
||||
return {
|
||||
menu,
|
||||
score: scoreMenu(menu, text)
|
||||
}
|
||||
})
|
||||
.filter(function (item) {
|
||||
return item.score > 0
|
||||
})
|
||||
.sort(function (a, b) {
|
||||
if (b.score !== a.score) {
|
||||
return b.score - a.score
|
||||
}
|
||||
return String(a.menu.name || "").length - String(b.menu.name || "").length
|
||||
})
|
||||
.slice(0, 5)
|
||||
}
|
||||
|
||||
function getRootMenuId(menu) {
|
||||
let id = menu.id
|
||||
let parentId = state.parentMap[id] || menu.parentId
|
||||
while (parentId) {
|
||||
id = parentId
|
||||
parentId = state.parentMap[id]
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
function getCurrentSubAppId() {
|
||||
try {
|
||||
const app = JSON.parse(window.sessionStorage.getItem("zhgh_sub_app") || "{}")
|
||||
return app.id
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function openMenu(menu) {
|
||||
state.pendingMatches = []
|
||||
state.awaitingChoice = false
|
||||
if (!menu || !menu.href) {
|
||||
notify("该菜单没有配置可打开的地址", "warning")
|
||||
return
|
||||
}
|
||||
|
||||
const targetRootId = getRootMenuId(menu)
|
||||
const currentSubAppId = getCurrentSubAppId()
|
||||
const hasSubAppContainer = $("#sub-app-container-main-content-body").length > 0
|
||||
|
||||
if (hasSubAppContainer && currentSubAppId && currentSubAppId === targetRootId && typeof commonUtil !== "undefined" && commonUtil.pjaxPush) {
|
||||
commonUtil.pjaxPush(menu.href)
|
||||
return
|
||||
}
|
||||
window.location.href = menu.href
|
||||
}
|
||||
|
||||
function showCandidateMessage(matches) {
|
||||
const items = matches
|
||||
.map(function (item, index) {
|
||||
const href = item.menu.href ? " <span style=\"color:#909399\">" + escapeHtml(item.menu.href) + "</span>" : ""
|
||||
return "<p style=\"margin:6px 0\">" + (index + 1) + ". " + escapeHtml(item.menu.name) + href + "</p>"
|
||||
})
|
||||
.join("")
|
||||
|
||||
if (window.ELEMENT && ELEMENT.MessageBox) {
|
||||
ELEMENT.MessageBox.alert(items, "找到多个菜单,请说“打开第几个”", {
|
||||
dangerouslyUseHTMLString: true,
|
||||
confirmButtonText: "知道了",
|
||||
type: "info",
|
||||
callback: function () {}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function getChoiceIndex(text) {
|
||||
const normalized = normalizePlain(text)
|
||||
const numberMap = {
|
||||
"1": 0,
|
||||
"一": 0,
|
||||
"壹": 0,
|
||||
"幺": 0,
|
||||
"2": 1,
|
||||
"二": 1,
|
||||
"两": 1,
|
||||
"贰": 1,
|
||||
"3": 2,
|
||||
"三": 2,
|
||||
"叁": 2,
|
||||
"4": 3,
|
||||
"四": 3,
|
||||
"肆": 3,
|
||||
"5": 4,
|
||||
"五": 4,
|
||||
"伍": 4
|
||||
}
|
||||
|
||||
const digitMatch = normalized.match(/第?([1-5])个?/)
|
||||
if (digitMatch) {
|
||||
return numberMap[digitMatch[1]]
|
||||
}
|
||||
|
||||
const chineseMatch = normalized.match(/第?([一二两三四五壹贰叁肆伍幺])个?/)
|
||||
if (chineseMatch) {
|
||||
return numberMap[chineseMatch[1]]
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
function handleChoice(text) {
|
||||
const index = getChoiceIndex(text)
|
||||
const matches = state.pendingMatches || []
|
||||
if (index >= 0 && index < matches.length) {
|
||||
if (window.ELEMENT && ELEMENT.MessageBox && typeof ELEMENT.MessageBox.close === "function") {
|
||||
ELEMENT.MessageBox.close()
|
||||
}
|
||||
openMenu(matches[index].menu)
|
||||
return
|
||||
}
|
||||
|
||||
notify("没有识别到有效序号,请说打开第几个", "warning")
|
||||
speak("没有识别到有效序号,请说打开第几个", function () {
|
||||
listenForChoice()
|
||||
})
|
||||
}
|
||||
|
||||
function listenForChoice() {
|
||||
state.awaitingChoice = true
|
||||
setTimeout(function () {
|
||||
start(true)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
function chooseMenu(matches) {
|
||||
if (!matches.length) {
|
||||
notify("未找到可访问的菜单,请换个名称再试", "warning")
|
||||
return
|
||||
}
|
||||
if (matches.length === 1 || matches[0].score > matches[1].score) {
|
||||
openMenu(matches[0].menu)
|
||||
return
|
||||
}
|
||||
|
||||
state.pendingMatches = matches
|
||||
state.awaitingChoice = true
|
||||
showCandidateMessage(matches)
|
||||
|
||||
const prompt = matches
|
||||
.map(function (item, index) {
|
||||
return "第" + (index + 1) + "个," + item.menu.name
|
||||
})
|
||||
.join("。")
|
||||
speak("找到多个菜单,您需要打开第几个。" + prompt, function () {
|
||||
listenForChoice()
|
||||
})
|
||||
}
|
||||
|
||||
function updateButton(listening) {
|
||||
const $button = $("#voice-menu-btn")
|
||||
$button.toggleClass("is-listening", listening)
|
||||
$button.attr("title", listening ? "正在听,请说出菜单名称" : "语音打开菜单")
|
||||
$button.find(".voice-menu-text").text(listening ? "聆听中" : "语音")
|
||||
}
|
||||
|
||||
function start(choiceMode) {
|
||||
choiceMode = choiceMode || state.awaitingChoice
|
||||
if (!SpeechRecognition) {
|
||||
notify("当前浏览器不支持语音识别,请使用 Chrome 或 Edge", "warning")
|
||||
return
|
||||
}
|
||||
if (state.listening) {
|
||||
state.recognition.stop()
|
||||
return
|
||||
}
|
||||
|
||||
const recognition = new SpeechRecognition()
|
||||
recognition.lang = "zh-CN"
|
||||
recognition.interimResults = false
|
||||
recognition.continuous = false
|
||||
recognition.maxAlternatives = 1
|
||||
|
||||
recognition.onstart = function () {
|
||||
state.listening = true
|
||||
updateButton(true)
|
||||
notify(choiceMode ? "请说打开第几个" : "请说出要打开的菜单名称", "info")
|
||||
}
|
||||
recognition.onend = function () {
|
||||
state.listening = false
|
||||
updateButton(false)
|
||||
if (typeof state.afterRecognitionEnd === "function") {
|
||||
const next = state.afterRecognitionEnd
|
||||
state.afterRecognitionEnd = null
|
||||
setTimeout(next, 150)
|
||||
}
|
||||
}
|
||||
recognition.onerror = function (event) {
|
||||
const message = event.error === "not-allowed" ? "麦克风授权失败,请确认 HTTPS 或 localhost 环境并允许浏览器使用麦克风" : "语音识别失败,请再试一次"
|
||||
notify(message, "warning")
|
||||
}
|
||||
recognition.onresult = function (event) {
|
||||
const text = event.results && event.results[0] && event.results[0][0] && event.results[0][0].transcript
|
||||
state.lastText = text || ""
|
||||
if (!state.lastText) {
|
||||
notify("没有识别到语音内容", "warning")
|
||||
return
|
||||
}
|
||||
if (choiceMode || state.awaitingChoice) {
|
||||
runAfterRecognitionEnd(function () {
|
||||
handleChoice(state.lastText)
|
||||
})
|
||||
return
|
||||
}
|
||||
getMenus().then(function (menus) {
|
||||
const matches = matchMenus(state.lastText, menus)
|
||||
runAfterRecognitionEnd(function () {
|
||||
chooseMenu(matches)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
state.recognition = recognition
|
||||
recognition.start()
|
||||
}
|
||||
|
||||
function init() {
|
||||
$(document).on("click", "#voice-menu-btn", function () {
|
||||
start()
|
||||
})
|
||||
}
|
||||
|
||||
window.voiceMenuNavigator = {
|
||||
start,
|
||||
refreshMenus: function () {
|
||||
state.menus = null
|
||||
state.parentMap = {}
|
||||
}
|
||||
}
|
||||
|
||||
$(init)
|
||||
})(window, jQuery)
|
||||
Binary file not shown.
Binary file not shown.
@@ -85,6 +85,7 @@
|
||||
<script src="${base!}/components/plugins/sysDict/DictData.js"></script>
|
||||
<script src="${base!}/assets/platform/js/util/commonUtil.js"></script>
|
||||
<script src="${base!}/assets/platform/js/util/coordinateUtil.js"></script>
|
||||
<script src="${base!}/assets/platform/js/util/voiceMenuNavigator.js"></script>
|
||||
<script src="${base!}/assets/platform/js/main.js"></script>
|
||||
<script src="${base!}/assets/platform/js/mixin/initTableMixins.js"></script>
|
||||
<script src="${base!}/assets/platform/js/mixin/styleMixin.js"></script>
|
||||
@@ -541,6 +542,33 @@
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.v4-voice-menu {
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
padding: 0 14px;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
font: inherit;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.v4-voice-menu:hover,
|
||||
.v4-voice-menu.is-listening {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.v4-voice-menu.is-listening i {
|
||||
color: #ffdf6b;
|
||||
}
|
||||
|
||||
.v4-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
@@ -606,6 +634,10 @@
|
||||
.v4-nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.voice-menu-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* 页脚样式 */
|
||||
@@ -677,6 +709,10 @@
|
||||
</div>
|
||||
|
||||
<div class="v4-user-section">
|
||||
<button type="button" class="v4-voice-menu" id="voice-menu-btn" title="语音打开菜单">
|
||||
<i class="fa fa-microphone"></i>
|
||||
<span class="voice-menu-text">语音</span>
|
||||
</button>
|
||||
<div class="v4-user-info">
|
||||
<span class="v4-user-name">${@auth.getPrincipalProperty('username')} (${@auth.getPrincipalProperty('loginname')})</span>
|
||||
<!-- <i class="fa fa-angle-down"></i> -->
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
const entry = {
|
||||
template: /*language=HTML*/ `
|
||||
<div class="entry-wrapper">
|
||||
<!-- 标签页导航 -->
|
||||
<div class="tabs">
|
||||
<div class="tab-item" :class="{ active: activeCategory === 'serv' }"
|
||||
@click="setActiveCategory('serv')">
|
||||
<i class="fa fa-star"></i> 推荐服务
|
||||
<div class="entry-section" v-for="section in entrySections" :key="section.key">
|
||||
<div class="entry-section-header">
|
||||
<div class="entry-section-title">
|
||||
<i :class="section.icon"></i>
|
||||
<span>{{ section.title }}</span>
|
||||
<em>/Applications</em>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-item" :class="{ active: activeCategory === 'app' }"
|
||||
@click="setActiveCategory('app')">
|
||||
<i class="fa fa-fire"></i> 推荐应用
|
||||
</div>
|
||||
<div class="tab-item" :class="{ active: activeCategory === 'fav' }"
|
||||
@click="setActiveCategory('fav')">
|
||||
<i class="fa fa-heart"></i> 我的收藏
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 应用块区域 -->
|
||||
<div class="apps-container">
|
||||
<div class="app-grid">
|
||||
<div class="app-item" v-for="(item, index) in currentServices" :key="index"
|
||||
@click="openService(item)">
|
||||
<div class="app-icon">
|
||||
<img :src="item.picIcon" alt="" />
|
||||
<div class="apps-container">
|
||||
<div class="app-grid">
|
||||
<div class="app-item"
|
||||
v-for="(item, index) in section.list"
|
||||
:key="item.id || index"
|
||||
@click="openService(item, section.key)">
|
||||
<div class="app-icon">
|
||||
<img :src="item.picIcon" alt="" />
|
||||
</div>
|
||||
<div class="app-name">{{ item.name }}</div>
|
||||
</div>
|
||||
<div class="app-name">{{ item.name }}</div>
|
||||
<el-empty class="app-empty"
|
||||
v-if="!section.list || section.list.length === 0"
|
||||
description="暂无数据"
|
||||
:image-size="72">
|
||||
</el-empty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -33,7 +33,6 @@ const entry = {
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
activeCategory: 'serv',
|
||||
hasMore: true,
|
||||
entries: {
|
||||
serv: [],
|
||||
@@ -43,8 +42,12 @@ const entry = {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentServices() {
|
||||
return this.entries[this.activeCategory] || [];
|
||||
entrySections() {
|
||||
return [
|
||||
{ key: 'serv', title: '推荐服务', icon: 'fa fa-star', list: this.entries.serv },
|
||||
{ key: 'app', title: '推荐应用', icon: 'fa fa-fire', list: this.entries.app },
|
||||
{ key: 'fav', title: '我的收藏', icon: 'fa fa-heart', list: this.entries.fav }
|
||||
];
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@@ -53,10 +56,6 @@ const entry = {
|
||||
this.getFavorite()
|
||||
},
|
||||
methods: {
|
||||
setActiveCategory(category) {
|
||||
this.activeCategory = category;
|
||||
},
|
||||
|
||||
// 查询推荐服务
|
||||
async getRecommendService() {
|
||||
this.$axios.post('/platform/home/listRecommendService', {platform: "PC"}).then(res => {
|
||||
@@ -84,35 +83,37 @@ const entry = {
|
||||
})
|
||||
},
|
||||
|
||||
// 点击服务
|
||||
openService(service) {
|
||||
if(this.activeCategory === 'serv'){
|
||||
if(!service.href) this.$message.error('无效的链接地址')
|
||||
// 点击服务或应用
|
||||
openService(service, category) {
|
||||
if (category === 'serv') {
|
||||
if (!service.href) {
|
||||
this.$message.error('无效的链接地址')
|
||||
return
|
||||
}
|
||||
window.open(service.href)
|
||||
return
|
||||
}
|
||||
|
||||
if(this.activeCategory === 'app'){
|
||||
// 储存到缓存
|
||||
if (category === 'app') {
|
||||
window.sessionStorage.setItem("zhgh_sub_app", JSON.stringify(service))
|
||||
// 新标签页打开
|
||||
window.open("/platform/v4/subApp?appId=" + service.id, "_blank")
|
||||
return
|
||||
}
|
||||
|
||||
if(this.activeCategory === 'fav'){
|
||||
if(!service.href) this.$message.error('无效的链接地址')
|
||||
window.open(service.href)
|
||||
return
|
||||
|
||||
if(!service.parentId){
|
||||
// 储存到缓存
|
||||
window.sessionStorage.setItem("zhgh_sub_app", JSON.stringify(app))
|
||||
// 新标签页打开
|
||||
window.open("/platform/v4/subApp?appId=" + app.id, "_blank")
|
||||
if (category === 'fav') {
|
||||
if (service.href) {
|
||||
window.open(service.href)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (!service.parentId) {
|
||||
window.sessionStorage.setItem("zhgh_sub_app", JSON.stringify(service))
|
||||
window.open("/platform/v4/subApp?appId=" + service.id, "_blank")
|
||||
return
|
||||
}
|
||||
|
||||
this.$message.error('无效的链接地址')
|
||||
}
|
||||
}
|
||||
},
|
||||
style: /*language=CSS*/ `
|
||||
@@ -121,37 +122,36 @@ const entry = {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
margin-top: 5px;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
border-bottom: 2px solid #e8e8e8;
|
||||
padding-bottom: 10px;
|
||||
.entry-section + .entry-section {
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
.entry-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.entry-section-title {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
color: #19324d;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.entry-section-title i {
|
||||
color: #409eff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.entry-section-title em {
|
||||
color: #b5c0d6;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
padding-bottom: 8px;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.tab-item:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: #1890ff;
|
||||
font-weight: bold;
|
||||
border-bottom: 2px solid #1890ff;
|
||||
}
|
||||
|
||||
.tab-item i {
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.apps-container {
|
||||
@@ -162,19 +162,17 @@ const entry = {
|
||||
|
||||
.app-grid {
|
||||
display: grid;
|
||||
/* 改为自适应列数:每格最小 80px,自动换行 */
|
||||
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
|
||||
|
||||
justify-items: center; /* 子项内容居中(图标和文字) */
|
||||
justify-content: start; /* 整体网格靠左对齐,避免居中 */
|
||||
|
||||
gap: 4px;
|
||||
padding: 12px 4px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
|
||||
justify-items: center;
|
||||
justify-content: start;
|
||||
gap: 12px 18px;
|
||||
min-height: 116px;
|
||||
padding: 18px 20px;
|
||||
background: #fff;
|
||||
border: 1px solid #edf1f7;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
overflow-x: hidden;
|
||||
|
||||
max-height: calc(350px - 74px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.app-item {
|
||||
@@ -187,6 +185,7 @@ const entry = {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 6px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
@@ -208,6 +207,7 @@ const entry = {
|
||||
}
|
||||
|
||||
.app-name {
|
||||
min-height: 34px;
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
@@ -215,31 +215,26 @@ const entry = {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
.app-empty {
|
||||
grid-column: 1 / -1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.tabs {
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
padding: 8px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.app-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 15px;
|
||||
grid-template-columns: repeat(3, minmax(72px, 1fr));
|
||||
gap: 12px;
|
||||
padding: 14px 10px;
|
||||
}
|
||||
|
||||
.app-item {
|
||||
padding: 12px;
|
||||
padding: 8px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.app-grid {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-columns: repeat(2, minmax(72px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ layout("/layouts/v4/baseLayout.html"){
|
||||
|
||||
.section-banner .banner-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.section-banner .banner-img img {
|
||||
@@ -21,6 +21,11 @@ layout("/layouts/v4/baseLayout.html"){
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* 临时屏蔽首页背景图,保留原图片节点便于恢复 */
|
||||
.section-banner .banner-img > img {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.section-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -29,7 +34,7 @@ layout("/layouts/v4/baseLayout.html"){
|
||||
width: 80%;
|
||||
margin: 20px auto;
|
||||
display: grid;
|
||||
grid-template-columns: 2.2fr 0.8fr;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 24px;
|
||||
align-items: stretch;
|
||||
row-gap: 24px;
|
||||
@@ -39,16 +44,16 @@ layout("/layouts/v4/baseLayout.html"){
|
||||
display: flex;
|
||||
/*align-items: center;*/
|
||||
min-width: 320px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
||||
padding: 16px;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.entry-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.12);
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.act-card {
|
||||
@@ -77,10 +82,6 @@ layout("/layouts/v4/baseLayout.html"){
|
||||
<user></user>
|
||||
|
||||
<div class="home-grid">
|
||||
<div class="act-card">
|
||||
<act></act>
|
||||
</div>
|
||||
|
||||
<div class="entry-card">
|
||||
<entry></entry>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ const stats = {
|
||||
@mouseenter="onMouseEnter"
|
||||
@mouseleave="onMouseLeave"
|
||||
>
|
||||
<div class="stat-icon">
|
||||
<div class="stat-icon stat-icon-hidden">
|
||||
<i class="fa fa-clock-o"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
@@ -28,7 +28,7 @@ const stats = {
|
||||
@mouseenter="onMouseEnter"
|
||||
@mouseleave="onMouseLeave"
|
||||
>
|
||||
<div class="stat-icon">
|
||||
<div class="stat-icon stat-icon-hidden">
|
||||
<i class="fa fa-check-circle"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
@@ -44,7 +44,7 @@ const stats = {
|
||||
@mouseenter="onMouseEnter"
|
||||
@mouseleave="onMouseLeave"
|
||||
>
|
||||
<div class="stat-icon">
|
||||
<div class="stat-icon stat-icon-hidden">
|
||||
<i class="fa fa-bell"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
@@ -60,7 +60,7 @@ const stats = {
|
||||
@mouseenter="onMouseEnter"
|
||||
@mouseleave="onMouseLeave"
|
||||
>
|
||||
<div class="stat-icon">
|
||||
<div class="stat-icon stat-icon-hidden">
|
||||
<i class="fa fa-file-text"></i>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
@@ -122,7 +122,7 @@ const stats = {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: -130px auto 0;
|
||||
margin: 20px auto 0;
|
||||
/*border: 1px solid #e8e8e8;*/
|
||||
}
|
||||
|
||||
@@ -131,7 +131,8 @@ const stats = {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
width: 32%;
|
||||
margin-left: 20px;
|
||||
min-width: 360px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
@@ -162,6 +163,11 @@ const stats = {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 临时屏蔽待办、已办、消息、发起四个统计图标 */
|
||||
.stat-icon-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.stat-item.pending .stat-icon,
|
||||
.stat-item.completed .stat-icon,
|
||||
.stat-item.messages .stat-icon,
|
||||
@@ -185,7 +191,7 @@ const stats = {
|
||||
.stat-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 10px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
@@ -209,5 +215,10 @@ const stats = {
|
||||
height: 190px !important;
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
/* 临时屏蔽“欢迎来到职工之家”图片,保留原节点便于恢复 */
|
||||
.stats-family {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
};
|
||||
|
||||
@@ -68,10 +68,10 @@ const user = {
|
||||
<div class="header">
|
||||
<h3>待办中心</h3>
|
||||
<el-tabs v-model="activeTab" type="card" @tab-click="handleTabClick">
|
||||
<el-tab-pane label="待办事宜" name="todo"></el-tab-pane>
|
||||
<el-tab-pane label="已办事宜" name="done"></el-tab-pane>
|
||||
<el-tab-pane label="我的发起" name="started"></el-tab-pane>
|
||||
<el-tab-pane label="办结事务" name="completed"></el-tab-pane>
|
||||
<el-tab-pane :label="tabLabel('待办事宜', taskStats.todoCount)" name="todo"></el-tab-pane>
|
||||
<el-tab-pane :label="tabLabel('已办事宜', taskStats.doneCount)" name="done"></el-tab-pane>
|
||||
<el-tab-pane :label="tabLabel('我的发起', taskStats.startedCount)" name="started"></el-tab-pane>
|
||||
<el-tab-pane :label="tabLabel('办结事务', taskStats.completedCount)" name="completed"></el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
@@ -124,6 +124,12 @@ const user = {
|
||||
pageSize: 5,
|
||||
totalCount: 0
|
||||
},
|
||||
taskStats: {
|
||||
todoCount: 0,
|
||||
doneCount: 0,
|
||||
startedCount: 0,
|
||||
completedCount: 0
|
||||
},
|
||||
// 🔆 天气相关数据
|
||||
weatherData: null,
|
||||
weatherError: false,
|
||||
@@ -228,6 +234,19 @@ const user = {
|
||||
}
|
||||
},
|
||||
// 获取任务列表
|
||||
tabLabel(title, count) {
|
||||
return title + "(" + (count || 0) + ")"
|
||||
},
|
||||
async getStatistics() {
|
||||
try {
|
||||
const res = await $.post("/flow/todoCenter/statistics")
|
||||
if (res.code === 0) {
|
||||
this.taskStats = Object.assign({}, this.taskStats, res.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取待办统计失败:", error)
|
||||
}
|
||||
},
|
||||
async getTasks() {
|
||||
this.loading = true
|
||||
try {
|
||||
@@ -245,6 +264,7 @@ const user = {
|
||||
},
|
||||
pageData() {
|
||||
this.getTasks()
|
||||
this.getStatistics()
|
||||
},
|
||||
handleTabClick(tab) {
|
||||
this.pageData()
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<div id="app" v-cloak>
|
||||
<el-card shadow="never">学习活动管理模块待完善</el-card>
|
||||
</div>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,883 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<style>
|
||||
.learning-outline-layout {
|
||||
height: calc(100vh - 235px);
|
||||
}
|
||||
.learning-outline-tree {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
.outline-context-menu {
|
||||
position: fixed;
|
||||
z-index: 3000;
|
||||
padding: 5px 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
background: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
.outline-context-menu li {
|
||||
min-width: 120px;
|
||||
padding: 7px 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.outline-context-menu li:hover {
|
||||
background: #f2f6fc;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<el-card shadow="never">
|
||||
<search :is-search-button="false">
|
||||
<search-item label="课程名称">
|
||||
<el-select
|
||||
v-model="currentCourseId"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
:remote-method="courseNameRemoteSearch"
|
||||
placeholder="请选择或搜索课程名称"
|
||||
style="width: 100%"
|
||||
@change="courseChange"
|
||||
@clear="courseClear">
|
||||
<el-option v-for="item in courseOptions" :key="item.id" :label="item.courseName" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="课程类型">
|
||||
<el-select v-model="courseQuery.courseTypeId" clearable filterable placeholder="请选择课程类型" style="width: 100%">
|
||||
<el-option v-for="item in courseTypeOptions" :key="item.id" :label="item.typeName" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="授课讲师">
|
||||
<el-input v-model="courseQuery.lecturerName" clearable placeholder="请输入授课讲师" style="width: 100%" @keyup.enter.native="loadCourses"></el-input>
|
||||
</search-item>
|
||||
<search-item label="课程状态">
|
||||
<el-select v-model="courseQuery.status" clearable placeholder="请选择课程状态" style="width: 100%">
|
||||
<el-option v-for="item in statusOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<div class="search-query">
|
||||
<el-button size="medium" type="primary" icon="el-icon-search" @click="loadCourses">搜索</el-button>
|
||||
<el-button size="medium" @click="resetCourseQuery">重置</el-button>
|
||||
</div>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-row :gutter="10" class="mt10 learning-outline-layout">
|
||||
<el-col :span="6" style="height: 100%">
|
||||
<el-card shadow="never" style="height: 100%">
|
||||
<table-tool label="课程大纲">
|
||||
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openNodeForm('chapter')">新增章</el-button>
|
||||
</table-tool>
|
||||
<div class="learning-outline-tree">
|
||||
<el-tree
|
||||
ref="outlineTree"
|
||||
:data="outlineTreeData"
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
highlight-current
|
||||
:expand-on-click-node="false"
|
||||
:props="{children:'children', label:'title'}"
|
||||
@node-click="nodeClick"
|
||||
@node-contextmenu="openContextMenu">
|
||||
<span slot-scope="{ node, data }">
|
||||
<i :class="data.nodeType === 'course' ? 'el-icon-collection' : (data.nodeType === 'chapter' ? 'el-icon-folder' : 'el-icon-document')"></i>
|
||||
<span>{{data.title}}</span>
|
||||
<el-tag v-if="data.status === 'disabled'" size="mini" type="info" style="margin-left: 6px">禁用</el-tag>
|
||||
<el-tag v-if="data.required" size="mini" type="warning" style="margin-left: 6px">必学</el-tag>
|
||||
</span>
|
||||
</el-tree>
|
||||
<el-empty v-if="treeData.length === 0" description="暂无课程大纲"></el-empty>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="18" style="height: 100%">
|
||||
<el-card shadow="never" style="height: 100%; overflow: auto">
|
||||
<el-empty v-if="!selectedNode.id" description="请选择左侧章/节节点"></el-empty>
|
||||
<el-tabs v-else v-model="activeTab">
|
||||
<el-tab-pane label="基础信息" name="basic">
|
||||
<el-form :model="nodeForm" ref="nodeForm" label-width="110px" :rules="nodeRules">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="节点类型">
|
||||
<el-tag :type="nodeForm.nodeType === 'chapter' ? 'primary' : 'success'">{{nodeForm.nodeType === 'chapter' ? '章' : '节'}}</el-tag>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序值" prop="sortOrder">
|
||||
<el-input-number v-model="nodeForm.sortOrder" :controls="false" :min="1" :precision="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input v-model="nodeForm.title" maxlength="100" placeholder="请输入章/节标题"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="副标题" prop="subtitle">
|
||||
<el-input v-model="nodeForm.subtitle" maxlength="200" placeholder="请输入副标题"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="简介" prop="description">
|
||||
<el-input v-model="nodeForm.description" type="textarea" :rows="5" maxlength="2000" show-word-limit placeholder="请输入章/节简介"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveNodeBasic">保存基础信息</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="学习资料" name="resource">
|
||||
<table-tool label="学习资料">
|
||||
<el-button size="mini" type="primary" icon="el-icon-plus" @click="openResourceForm()">新增资料</el-button>
|
||||
</table-tool>
|
||||
<el-table :data="resourceData" border @sort-change="resourcePageOrder">
|
||||
<el-table-column label="序号" type="index" width="70" :index="resourceIndex"></el-table-column>
|
||||
<el-table-column label="资料标题" prop="resourceTitle" sortable="custom" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="资料类型" prop="resourceType" sortable="custom" width="110">
|
||||
<template slot-scope="{row}">{{getResourceTypeName(row.resourceType)}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="文件格式" prop="fileExt" sortable="custom" width="110"></el-table-column>
|
||||
<el-table-column label="学习时长(分钟)" prop="durationSeconds" sortable="custom" width="140">
|
||||
<template slot-scope="{row}">{{formatDurationMinutes(row.durationSeconds)}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="排序" prop="sortOrder" sortable="custom" width="90"></el-table-column>
|
||||
<el-table-column label="必学" prop="required" sortable="custom" width="90">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag :type="row.required ? 'warning' : 'info'" size="mini">{{row.required ? '是' : '否'}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预览" prop="allowPreview" sortable="custom" width="90">
|
||||
<template slot-scope="{row}">{{row.allowPreview ? '允许' : '不允许'}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="下载" prop="allowDownload" sortable="custom" width="90">
|
||||
<template slot-scope="{row}">{{row.allowDownload ? '允许' : '不允许'}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" prop="status" sortable="custom" width="90">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag :type="row.status === 'enabled' ? 'success' : 'info'" size="mini">{{row.status === 'enabled' ? '启用' : '禁用'}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150">
|
||||
<template slot-scope="{row}">
|
||||
<el-button size="mini" type="primary" @click="openResourceForm(row)">编辑</el-button>
|
||||
<el-button size="mini" type="danger" @click="deleteResource(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
class="mt20"
|
||||
@size-change="resourceSizeChange"
|
||||
@current-change="resourceNumberChange"
|
||||
:current-page="resourcePageForm.pageNumber"
|
||||
:page-sizes="[5,10,20,30,50]"
|
||||
:page-size="resourcePageForm.pageSize"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
:total="resourcePageForm.totalCount">
|
||||
</el-pagination>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="完成规则" name="rule">
|
||||
<el-form :model="ruleForm" label-width="150px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="是否必学">
|
||||
<el-switch v-model="ruleForm.required"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="学习方式">
|
||||
<el-select v-model="ruleForm.studyMode" style="width: 100%">
|
||||
<el-option v-for="item in studyModeOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="完成规则">
|
||||
<el-select v-model="ruleForm.completionRule" style="width: 100%">
|
||||
<el-option v-for="item in completionRuleOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="完成比例">
|
||||
<el-input-number v-model="ruleForm.completePercent" :min="0" :max="100" :precision="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="最少学习时长(分钟)">
|
||||
<el-input-number v-model="ruleForm.minStudyMinutes" :min="0" :precision="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="解锁规则">
|
||||
<el-select v-model="ruleForm.unlockRule" style="width: 100%">
|
||||
<el-option v-for="item in unlockRuleOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8"><el-form-item label="允许跳过"><el-switch v-model="ruleForm.allowSkip"></el-switch></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="允许拖动"><el-switch v-model="ruleForm.allowDrag"></el-switch></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="暂停计时"><el-switch v-model="ruleForm.pauseCountTime"></el-switch></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8"><el-form-item label="隐藏计时"><el-switch v-model="ruleForm.hiddenCountTime"></el-switch></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="无操作计时"><el-switch v-model="ruleForm.inactiveCountTime"></el-switch></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveRule">保存完成规则</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="状态设置" name="status">
|
||||
<el-form :model="nodeForm" label-width="110px">
|
||||
<el-form-item label="节点状态">
|
||||
<el-radio-group v-model="nodeForm.status">
|
||||
<el-radio label="enabled">启用</el-radio>
|
||||
<el-radio label="disabled">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否必学">
|
||||
<el-switch v-model="nodeForm.required"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveNodeBasic">保存状态</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<ul v-show="contextVisible" class="outline-context-menu" :style="{left: contextLeft + 'px', top: contextTop + 'px'}">
|
||||
<li @click="openNodeForm('chapter')">新增章</li>
|
||||
<li v-if="contextNode.nodeType === 'chapter'" @click="openNodeForm('section', contextNode)">新增节</li>
|
||||
<li v-if="contextNode.nodeType !== 'course'" @click="openNodeForm(contextNode.nodeType, contextNode)">编辑</li>
|
||||
<li v-if="contextNode.nodeType !== 'course'" @click="deleteNode(contextNode)">删除</li>
|
||||
<li v-if="contextNode.nodeType !== 'course'" @click="toggleNode(contextNode)">{{contextNode.status === 'enabled' ? '禁用' : '启用'}}</li>
|
||||
<li v-if="contextNode.nodeType !== 'course'" @click="moveNode(contextNode, 'up')">上移</li>
|
||||
<li v-if="contextNode.nodeType !== 'course'" @click="moveNode(contextNode, 'down')">下移</li>
|
||||
<li v-if="contextNode.nodeType !== 'course'" @click="openResourceForm(null, contextNode)">上传资料</li>
|
||||
<li v-if="contextNode.nodeType !== 'course'" @click="setRequired(contextNode)">设置必学</li>
|
||||
</ul>
|
||||
|
||||
<el-dialog :title="nodeDialogTitle" :visible.sync="nodeDialogVisible" :close-on-click-modal="false" width="45%">
|
||||
<el-form :model="nodeDialogForm" ref="nodeDialogForm" label-width="100px" :rules="nodeRules">
|
||||
<el-form-item label="节点类型">
|
||||
<el-tag>{{nodeDialogForm.nodeType === 'chapter' ? '章' : '节'}}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="上级节点">
|
||||
<el-input v-model="nodeDialogForm.parentName" disabled placeholder="上级节点"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input v-model="nodeDialogForm.title" maxlength="100" placeholder="请输入标题"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="副标题">
|
||||
<el-input v-model="nodeDialogForm.subtitle" maxlength="200" placeholder="请输入副标题"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="简介">
|
||||
<el-input v-model="nodeDialogForm.description" type="textarea" :rows="4" maxlength="2000" show-word-limit></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序值">
|
||||
<el-input-number v-model="nodeDialogForm.sortOrder" :min="1" :precision="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否必学">
|
||||
<el-switch v-model="nodeDialogForm.required"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="nodeDialogForm.status">
|
||||
<el-radio label="enabled">启用</el-radio>
|
||||
<el-radio label="disabled">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="nodeDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitNodeDialog">确定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog :title="resourceForm.id ? '编辑学习资料' : '新增学习资料'" :visible.sync="resourceDialogVisible" :close-on-click-modal="false" width="55%">
|
||||
<el-form :model="resourceForm" ref="resourceForm" label-width="120px" :rules="resourceRules">
|
||||
<el-form-item label="所属章节">
|
||||
<el-input v-model="resourceForm.outlineName" disabled placeholder="所属章节"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="资料标题" prop="resourceTitle">
|
||||
<el-input v-model="resourceForm.resourceTitle" maxlength="100" placeholder="请输入资料标题"></el-input>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="资料类型" prop="resourceType">
|
||||
<el-select v-model="resourceForm.resourceType" placeholder="请选择资料类型" style="width: 100%" @change="resourceTypeChange">
|
||||
<el-option v-for="item in resourceTypeOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序值" prop="sortOrder">
|
||||
<el-input-number v-model="resourceForm.sortOrder" :min="1" :precision="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="学习时长(分钟)">
|
||||
<el-input-number v-model="resourceForm.durationMinutes" :min="0" :precision="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="resourceForm.status">
|
||||
<el-radio label="enabled">启用</el-radio>
|
||||
<el-radio label="disabled">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8"><el-form-item label="是否必学"><el-switch v-model="resourceForm.required"></el-switch></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="允许预览"><el-switch v-model="resourceForm.allowPreview"></el-switch></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="允许下载"><el-switch v-model="resourceForm.allowDownload"></el-switch></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-form-item label="附件" prop="fileData">
|
||||
<file-upload
|
||||
:value.sync="resourceForm.fileData"
|
||||
:upload_number="1"
|
||||
upload_mode="drag"
|
||||
upload_result_category="array"
|
||||
complete_result
|
||||
:accept="fileAccept">
|
||||
</file-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="resourceDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitResource">确定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
return {
|
||||
courseQuery: {},
|
||||
courseOptions: [],
|
||||
courseTypeOptions: [],
|
||||
currentCourseId: "",
|
||||
treeData: [],
|
||||
selectedNode: {},
|
||||
activeTab: "basic",
|
||||
nodeForm: {},
|
||||
nodeDialogVisible: false,
|
||||
nodeDialogTitle: "",
|
||||
nodeDialogForm: {},
|
||||
nodeRules: {
|
||||
title: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
sortOrder: [{ required: true, message: "必填", trigger: ["blur", "change"] }]
|
||||
},
|
||||
contextVisible: false,
|
||||
contextLeft: 0,
|
||||
contextTop: 0,
|
||||
contextNode: {},
|
||||
resourceData: [],
|
||||
resourcePageForm: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
totalCount: 0,
|
||||
pageOrderName: "",
|
||||
pageOrderBy: ""
|
||||
},
|
||||
resourceDialogVisible: false,
|
||||
resourceForm: {},
|
||||
resourceRules: {
|
||||
resourceTitle: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
resourceType: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
sortOrder: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
fileData: [{ required: true, message: "请上传附件", trigger: ["blur", "change"] }]
|
||||
},
|
||||
fileAccept: "",
|
||||
ruleForm: {},
|
||||
statusOptions: [
|
||||
{ name: "草稿", code: "draft" },
|
||||
{ name: "待发布", code: "pending" },
|
||||
{ name: "已发布", code: "published" },
|
||||
{ name: "已下架", code: "offline" }
|
||||
],
|
||||
resourceTypeOptions: [
|
||||
{ name: "视频", code: "video", accept: ".mp4,.mov" },
|
||||
{ name: "音频", code: "audio", accept: ".mp3,.wav,.aac" },
|
||||
{ name: "PDF", code: "pdf", accept: ".pdf" },
|
||||
{ name: "PPT课件", code: "ppt", accept: ".ppt,.pptx" },
|
||||
{ name: "Word文档", code: "word", accept: ".doc,.docx" },
|
||||
{ name: "图片", code: "image", accept: ".jpg,.jpeg,.png" },
|
||||
{ name: "其他", code: "other", accept: "" }
|
||||
],
|
||||
studyModeOptions: [
|
||||
{ name: "视频", code: "video" },
|
||||
{ name: "音频", code: "audio" },
|
||||
{ name: "文档", code: "document" },
|
||||
{ name: "图片", code: "image" },
|
||||
{ name: "混合资料", code: "mixed" }
|
||||
],
|
||||
completionRuleOptions: [
|
||||
{ name: "完成所有必学资料", code: "all_required_resource" },
|
||||
{ name: "按资料学习进度", code: "resource_progress" },
|
||||
{ name: "达到最少学习时长", code: "min_study_time" },
|
||||
{ name: "管理员手动确认", code: "manual" }
|
||||
],
|
||||
unlockRuleOptions: [
|
||||
{ name: "自由学习", code: "free" },
|
||||
{ name: "顺序解锁", code: "sequential" }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentCourse() {
|
||||
return this.courseOptions.find(v => v.id === this.currentCourseId)
|
||||
},
|
||||
outlineTreeData() {
|
||||
if (!this.currentCourseId || !this.currentCourse) {
|
||||
return []
|
||||
}
|
||||
return [{
|
||||
id: "course-" + this.currentCourseId,
|
||||
courseId: this.currentCourseId,
|
||||
title: this.currentCourse.courseName,
|
||||
nodeType: "course",
|
||||
children: this.treeData
|
||||
}]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetCourseQuery() {
|
||||
this.courseQuery = {}
|
||||
this.currentCourseId = ""
|
||||
this.loadCourses()
|
||||
},
|
||||
getQueryParam(name) {
|
||||
return new URLSearchParams(window.location.search).get(name) || ""
|
||||
},
|
||||
async loadCourseTypes() {
|
||||
const resp = await this.$axios.post(loc() + "/courseTypes")
|
||||
if (resp.code === 0) {
|
||||
this.courseTypeOptions = resp.data
|
||||
}
|
||||
},
|
||||
async loadCourses(keepCurrent = false) {
|
||||
const resp = await this.$axios.post(loc() + "/courses", this.courseQuery)
|
||||
if (resp.code === 0) {
|
||||
this.courseOptions = resp.data
|
||||
if (!keepCurrent && (!this.currentCourseId || !this.courseOptions.some(v => v.id === this.currentCourseId))) {
|
||||
this.currentCourseId = this.courseOptions.length ? this.courseOptions[0].id : ""
|
||||
}
|
||||
await this.loadTree()
|
||||
}
|
||||
},
|
||||
courseNameRemoteSearch(query) {
|
||||
this.courseQuery.courseName = query
|
||||
this.loadCourses(true)
|
||||
},
|
||||
courseChange() {
|
||||
this.selectedNode = {}
|
||||
this.loadTree()
|
||||
},
|
||||
courseClear() {
|
||||
this.currentCourseId = ""
|
||||
this.selectedNode = {}
|
||||
this.treeData = []
|
||||
},
|
||||
async loadTree() {
|
||||
if (!this.currentCourseId) {
|
||||
this.treeData = []
|
||||
this.selectedNode = {}
|
||||
return
|
||||
}
|
||||
const resp = await this.$axios.post(loc() + "/tree", { courseId: this.currentCourseId })
|
||||
if (resp.code === 0) {
|
||||
this.treeData = resp.data
|
||||
let nodeToSelect = null
|
||||
if (this.selectedNode.id) {
|
||||
nodeToSelect = this.findNode(this.treeData, this.selectedNode.id)
|
||||
}
|
||||
if (!nodeToSelect) {
|
||||
nodeToSelect = this.findFirstOutlineNode(this.treeData)
|
||||
}
|
||||
if (nodeToSelect) {
|
||||
this.nodeClick(nodeToSelect)
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.outlineTree) {
|
||||
this.$refs.outlineTree.setCurrentKey(nodeToSelect.id)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.selectedNode = {}
|
||||
this.nodeForm = {}
|
||||
this.resourceData = []
|
||||
}
|
||||
}
|
||||
},
|
||||
findFirstOutlineNode(nodes) {
|
||||
if (!nodes || !nodes.length) return null
|
||||
for (const node of nodes) {
|
||||
if (node.nodeType !== "course") return node
|
||||
const child = this.findFirstOutlineNode(node.children || [])
|
||||
if (child) return child
|
||||
}
|
||||
return null
|
||||
},
|
||||
findNode(nodes, id) {
|
||||
for (const node of nodes) {
|
||||
if (node.id === id) return node
|
||||
const child = this.findNode(node.children || [], id)
|
||||
if (child) return child
|
||||
}
|
||||
return null
|
||||
},
|
||||
getParentName(type, node, course, isEdit) {
|
||||
if (type === "chapter") {
|
||||
return course ? course.courseName : ""
|
||||
}
|
||||
if (!isEdit) {
|
||||
return node ? node.title : ""
|
||||
}
|
||||
const parent = node && node.parentId ? this.findNode(this.treeData, node.parentId) : null
|
||||
return parent ? parent.title : (course ? course.courseName : "")
|
||||
},
|
||||
nodeClick(data) {
|
||||
if (data.nodeType === "course") {
|
||||
this.selectedNode = {}
|
||||
this.nodeForm = {}
|
||||
this.resourceData = []
|
||||
return
|
||||
}
|
||||
this.selectedNode = data
|
||||
this.nodeForm = Object.assign({}, data)
|
||||
this.activeTab = "basic"
|
||||
this.loadResources()
|
||||
this.loadRule()
|
||||
},
|
||||
openContextMenu(event, data) {
|
||||
event.preventDefault()
|
||||
this.contextNode = data
|
||||
this.contextVisible = true
|
||||
this.contextLeft = event.clientX
|
||||
this.contextTop = event.clientY
|
||||
},
|
||||
openNodeForm(type, node = null) {
|
||||
if (!this.currentCourseId) {
|
||||
this.$message.warning("请先选择课程")
|
||||
return
|
||||
}
|
||||
const course = this.courseOptions.find(v => v.id === this.currentCourseId)
|
||||
if (node && node.id && node.nodeType === type) {
|
||||
this.nodeDialogTitle = "编辑" + (type === "chapter" ? "章" : "节")
|
||||
this.nodeDialogForm = Object.assign({}, node, {
|
||||
parentName: this.getParentName(type, node, course, true)
|
||||
})
|
||||
} else {
|
||||
this.nodeDialogTitle = "新增" + (type === "chapter" ? "章" : "节")
|
||||
this.nodeDialogForm = {
|
||||
courseId: this.currentCourseId,
|
||||
courseName: course ? course.courseName : "",
|
||||
parentId: type === "section" && node ? node.id : "",
|
||||
parentName: this.getParentName(type, node, course, false),
|
||||
nodeType: type,
|
||||
title: "",
|
||||
subtitle: "",
|
||||
description: "",
|
||||
sortOrder: 1,
|
||||
required: false,
|
||||
status: "enabled"
|
||||
}
|
||||
}
|
||||
this.nodeDialogVisible = true
|
||||
this.$nextTick(() => this.$refs.nodeDialogForm && this.$refs.nodeDialogForm.clearValidate())
|
||||
},
|
||||
submitNodeDialog() {
|
||||
this.$refs.nodeDialogForm.validate(async valid => {
|
||||
if (!valid) return
|
||||
const resp = await this.$axios.post(loc() + "/saveNode", this.nodeDialogForm)
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.nodeDialogVisible = false
|
||||
await this.loadTree()
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
},
|
||||
saveNodeBasic() {
|
||||
this.$refs.nodeForm.validate(async valid => {
|
||||
if (!valid) return
|
||||
const resp = await this.$axios.post(loc() + "/saveNode", this.nodeForm)
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.selectedNode = Object.assign({}, this.nodeForm)
|
||||
await this.loadTree()
|
||||
await this.loadRule()
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
},
|
||||
deleteNode(node) {
|
||||
this.$confirm("确定要删除该节点吗?若下级有内容将不允许删除。", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(async () => {
|
||||
const resp = await this.$axios.post(loc() + "/deleteNode", { id: node.id })
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.selectedNode = {}
|
||||
await this.loadTree()
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
},
|
||||
async toggleNode(node) {
|
||||
const status = node.status === "enabled" ? "disabled" : "enabled"
|
||||
const resp = await this.$axios.post(loc() + "/toggleNode", { id: node.id, status })
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
await this.loadTree()
|
||||
}
|
||||
},
|
||||
async moveNode(node, direction) {
|
||||
const resp = await this.$axios.post(loc() + "/moveNode", { id: node.id, direction })
|
||||
if (resp.code === 0) {
|
||||
await this.loadTree()
|
||||
}
|
||||
},
|
||||
async setRequired(node) {
|
||||
const next = !node.required
|
||||
const data = Object.assign({}, node, { required: next })
|
||||
const resp = await this.$axios.post(loc() + "/saveNode", data)
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
await this.loadTree()
|
||||
if (this.selectedNode.id === node.id) {
|
||||
this.nodeClick(Object.assign({}, node, { required: next }))
|
||||
}
|
||||
}
|
||||
},
|
||||
async loadResources() {
|
||||
if (!this.selectedNode.id) return
|
||||
const resp = await this.$axios.post(loc() + "/resourcePage", Object.assign({}, this.resourcePageForm, { outlineId: this.selectedNode.id }))
|
||||
if (resp.code === 0) {
|
||||
this.resourceData = resp.data.list
|
||||
this.resourcePageForm.totalCount = resp.data.totalCount
|
||||
}
|
||||
},
|
||||
resourcePageOrder(column) {
|
||||
this.resourcePageForm.pageOrderName = column.prop
|
||||
this.resourcePageForm.pageOrderBy = column.order
|
||||
this.loadResources()
|
||||
},
|
||||
resourceSizeChange(val) {
|
||||
this.resourcePageForm.pageSize = val
|
||||
this.loadResources()
|
||||
},
|
||||
resourceNumberChange(val) {
|
||||
this.resourcePageForm.pageNumber = val
|
||||
this.loadResources()
|
||||
},
|
||||
resourceIndex(index) {
|
||||
return index + (this.resourcePageForm.pageNumber - 1) * this.resourcePageForm.pageSize + 1
|
||||
},
|
||||
openResourceForm(row = null, node = null) {
|
||||
const target = node || this.selectedNode
|
||||
if (!target.id) {
|
||||
this.$message.warning("请先选择章/节节点")
|
||||
return
|
||||
}
|
||||
if (row) {
|
||||
this.resourceForm = Object.assign({}, row, {
|
||||
outlineName: target.title,
|
||||
fileData: this.parseFiles(row.fileData),
|
||||
durationMinutes: this.formatDurationMinutes(row.durationSeconds)
|
||||
})
|
||||
} else {
|
||||
this.resourceForm = {
|
||||
courseId: target.courseId,
|
||||
outlineId: target.id,
|
||||
outlineName: target.title,
|
||||
resourceTitle: "",
|
||||
resourceType: "video",
|
||||
fileExt: "",
|
||||
fileData: [],
|
||||
durationSeconds: 0,
|
||||
durationMinutes: 0,
|
||||
sortOrder: 1,
|
||||
required: false,
|
||||
allowPreview: true,
|
||||
allowDownload: false,
|
||||
status: "enabled"
|
||||
}
|
||||
}
|
||||
this.resourceTypeChange(this.resourceForm.resourceType)
|
||||
this.resourceDialogVisible = true
|
||||
this.$nextTick(() => this.$refs.resourceForm && this.$refs.resourceForm.clearValidate())
|
||||
},
|
||||
resourceTypeChange(val) {
|
||||
const type = this.resourceTypeOptions.find(v => v.code === val)
|
||||
this.fileAccept = type ? type.accept : ""
|
||||
this.autoFillMediaDuration()
|
||||
},
|
||||
submitResource() {
|
||||
this.$refs.resourceForm.validate(async valid => {
|
||||
if (!valid) return
|
||||
const data = Object.assign({}, this.resourceForm)
|
||||
data.durationSeconds = Math.round((data.durationMinutes || 0) * 60)
|
||||
data.fileExt = this.getFileExt(data.fileData)
|
||||
if (!this.validResourceExt(data.resourceType, data.fileExt)) {
|
||||
this.$message.warning("当前资料类型不支持上传 ." + data.fileExt + " 格式文件")
|
||||
return
|
||||
}
|
||||
delete data.outlineName
|
||||
delete data.durationMinutes
|
||||
data.fileData = JSON.stringify(data.fileData || [])
|
||||
const resp = await this.$axios.post(loc() + "/saveResource", data)
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.resourceDialogVisible = false
|
||||
this.loadResources()
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
},
|
||||
deleteResource(row) {
|
||||
this.$confirm("确定要删除该学习资料吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(async () => {
|
||||
const resp = await this.$axios.post(loc() + "/deleteResource", { id: row.id })
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.loadResources()
|
||||
}
|
||||
})
|
||||
},
|
||||
async loadRule() {
|
||||
if (!this.selectedNode.id) return
|
||||
const resp = await this.$axios.post(loc() + "/getRule", {
|
||||
courseId: this.selectedNode.courseId,
|
||||
targetType: "outline",
|
||||
targetId: this.selectedNode.id
|
||||
})
|
||||
if (resp.code === 0) {
|
||||
this.ruleForm = Object.assign({}, resp.data, {
|
||||
minStudyMinutes: this.formatDurationMinutes(resp.data.minStudySeconds)
|
||||
})
|
||||
}
|
||||
},
|
||||
async saveRule() {
|
||||
const data = Object.assign({}, this.ruleForm, {
|
||||
minStudySeconds: Math.round((this.ruleForm.minStudyMinutes || 0) * 60)
|
||||
})
|
||||
delete data.minStudyMinutes
|
||||
const resp = await this.$axios.post(loc() + "/saveRule", data)
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
await this.loadTree()
|
||||
await this.loadRule()
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
},
|
||||
parseFiles(value) {
|
||||
if (!value) return []
|
||||
if (Array.isArray(value)) return value
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch (e) {
|
||||
return []
|
||||
}
|
||||
},
|
||||
getFileUrl(files) {
|
||||
const file = Array.isArray(files) && files.length ? files[0] : null
|
||||
if (!file) return ""
|
||||
return file.url || file.response?.data || file.data || ""
|
||||
},
|
||||
getFileExt(files) {
|
||||
const file = Array.isArray(files) && files.length ? files[0] : null
|
||||
if (!file) return ""
|
||||
const name = file.name || file.url || file.response?.data || ""
|
||||
const index = name.lastIndexOf(".")
|
||||
return index > -1 ? name.substring(index + 1).toLowerCase() : ""
|
||||
},
|
||||
formatDurationMinutes(durationSeconds) {
|
||||
const seconds = Number(durationSeconds || 0)
|
||||
return seconds > 0 ? Math.ceil(seconds / 60) : 0
|
||||
},
|
||||
autoFillMediaDuration() {
|
||||
if (!["video", "audio"].includes(this.resourceForm.resourceType)) return
|
||||
const url = this.getFileUrl(this.resourceForm.fileData)
|
||||
if (!url) return
|
||||
const media = document.createElement(this.resourceForm.resourceType === "video" ? "video" : "audio")
|
||||
media.preload = "metadata"
|
||||
media.onloadedmetadata = () => {
|
||||
window.URL.revokeObjectURL(media.src)
|
||||
if (isFinite(media.duration) && media.duration > 0) {
|
||||
this.$set(this.resourceForm, "durationMinutes", Math.ceil(media.duration / 60))
|
||||
}
|
||||
}
|
||||
media.onerror = () => {
|
||||
window.URL.revokeObjectURL(media.src)
|
||||
}
|
||||
media.src = url
|
||||
},
|
||||
validResourceExt(resourceType, fileExt) {
|
||||
const item = this.resourceTypeOptions.find(v => v.code === resourceType)
|
||||
if (!item || !item.accept) return true
|
||||
return item.accept.split(",").map(v => v.replace(".", "").toLowerCase()).includes(fileExt)
|
||||
},
|
||||
getResourceTypeName(code) {
|
||||
const item = this.resourceTypeOptions.find(v => v.code === code)
|
||||
return item ? item.name : code
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"resourceForm.fileData": {
|
||||
handler() {
|
||||
this.autoFillMediaDuration()
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener("click", () => this.contextVisible = false)
|
||||
},
|
||||
async created() {
|
||||
await this.loadCourseTypes()
|
||||
const courseId = this.getQueryParam("courseId")
|
||||
if (courseId) {
|
||||
this.currentCourseId = courseId
|
||||
this.courseQuery.courseId = courseId
|
||||
await this.loadCourses(true)
|
||||
this.$delete(this.courseQuery, "courseId")
|
||||
} else {
|
||||
await this.loadCourses()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,12 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<div id="app" v-cloak>
|
||||
<el-card shadow="never">
|
||||
<div slot="header">课程介绍</div>
|
||||
<el-empty description="课程介绍页面后续设计"></el-empty>
|
||||
</el-card>
|
||||
</div>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,350 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<style>
|
||||
.course-display {
|
||||
padding: 8px 10px 22px;
|
||||
background: #f4f5f7;
|
||||
min-height: calc(100vh - 88px);
|
||||
}
|
||||
.course-filter {
|
||||
background: #fff;
|
||||
border: 1px solid #e5e8ef;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.filter-row {
|
||||
display: flex;
|
||||
min-height: 70px;
|
||||
border-bottom: 1px solid #e8ebf0;
|
||||
}
|
||||
.filter-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.filter-label {
|
||||
width: 132px;
|
||||
padding: 20px 16px;
|
||||
background: #eef5ff;
|
||||
color: #1f2d3d;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
.filter-options {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px 18px;
|
||||
padding: 16px 18px;
|
||||
}
|
||||
.filter-chip {
|
||||
min-width: 76px;
|
||||
height: 32px;
|
||||
padding: 0 18px;
|
||||
border: 0;
|
||||
border-radius: 9px;
|
||||
background: transparent;
|
||||
color: #526070;
|
||||
cursor: pointer;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
.filter-chip.active {
|
||||
background: #1e63e9;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.course-result-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.course-count {
|
||||
color: #1f2d3d;
|
||||
}
|
||||
.course-count span {
|
||||
color: #2468f2;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.course-search {
|
||||
width: 310px;
|
||||
}
|
||||
.course-list {
|
||||
background: #fff;
|
||||
border-top: 1px solid #dfe4ec;
|
||||
}
|
||||
.course-item {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding: 18px 0;
|
||||
border-bottom: 1px solid #dfe4ec;
|
||||
}
|
||||
.course-cover {
|
||||
position: relative;
|
||||
width: 270px;
|
||||
height: 150px;
|
||||
flex: 0 0 270px;
|
||||
background: #e9eef5;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.course-cover img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.course-cover-empty {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8a97a8;
|
||||
background: linear-gradient(135deg, #eaf0f8, #d8e3f2);
|
||||
font-weight: 600;
|
||||
}
|
||||
.course-cover-tag {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 12px;
|
||||
min-width: 70px;
|
||||
height: 24px;
|
||||
padding: 0 10px;
|
||||
background: #f5a400;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
border-radius: 12px 0 0 12px;
|
||||
}
|
||||
.course-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-right: 12px;
|
||||
}
|
||||
.course-title {
|
||||
display: inline-block;
|
||||
margin: 4px 0 8px;
|
||||
color: #111827;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
.course-title:hover {
|
||||
color: #006fc9;
|
||||
}
|
||||
.course-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 18px;
|
||||
color: #8b96a6;
|
||||
font-size: 13px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.course-intro {
|
||||
color: #4f5f73;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 12px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.course-time {
|
||||
color: #8b96a6;
|
||||
font-size: 13px;
|
||||
}
|
||||
.recommend-tags {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
margin-left: 8px;
|
||||
vertical-align: 2px;
|
||||
}
|
||||
.course-empty {
|
||||
background: #fff;
|
||||
padding: 80px 0;
|
||||
text-align: center;
|
||||
color: #8b96a6;
|
||||
border-top: 1px solid #dfe4ec;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" class="course-display" v-cloak>
|
||||
<div class="course-filter">
|
||||
<div class="filter-row">
|
||||
<div class="filter-label">推荐标识</div>
|
||||
<div class="filter-options">
|
||||
<button class="filter-chip" :class="{active: !query.recommendFlag}" @click="selectRecommend('')">全部</button>
|
||||
<button
|
||||
v-for="item in recommendOptions"
|
||||
:key="item.code"
|
||||
class="filter-chip"
|
||||
:class="{active: query.recommendFlag === item.code}"
|
||||
@click="selectRecommend(item.code)">
|
||||
{{item.name}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-row">
|
||||
<div class="filter-label">课程类型</div>
|
||||
<div class="filter-options">
|
||||
<button class="filter-chip" :class="{active: !query.courseTypeId}" @click="selectCourseType('')">全部</button>
|
||||
<button
|
||||
v-for="item in courseTypeOptions"
|
||||
:key="item.id"
|
||||
class="filter-chip"
|
||||
:class="{active: query.courseTypeId === item.id}"
|
||||
@click="selectCourseType(item.id)">
|
||||
{{item.typeName}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="course-result-bar">
|
||||
<div class="course-count">为您找到相关课程<span>{{pageForm.totalCount || 0}}</span>门</div>
|
||||
<el-input
|
||||
class="course-search"
|
||||
v-model="query.keyword"
|
||||
clearable
|
||||
placeholder="搜索关键字"
|
||||
prefix-icon="el-icon-search"
|
||||
@keyup.enter.native="doSearch"
|
||||
@clear="doSearch">
|
||||
<el-button slot="append" icon="el-icon-search" @click="doSearch"></el-button>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<div v-if="pageForm.list && pageForm.list.length" class="course-list">
|
||||
<div v-for="course in pageForm.list" :key="course.id" class="course-item">
|
||||
<div class="course-cover">
|
||||
<img v-if="getCoverUrl(course.cover)" :src="getCoverUrl(course.cover)" :alt="course.courseName">
|
||||
<div v-else class="course-cover-empty">{{course.courseTypeName || '课程'}}</div>
|
||||
<div v-if="firstRecommendName(course.recommendFlags)" class="course-cover-tag">{{firstRecommendName(course.recommendFlags)}}</div>
|
||||
</div>
|
||||
<div class="course-info">
|
||||
<div>
|
||||
<span class="course-title" @click="openDetail(course)">{{course.courseName}}</span>
|
||||
<span class="recommend-tags">
|
||||
<el-tag v-for="code in splitFlags(course.recommendFlags)" :key="code" size="mini" type="warning">{{getRecommendName(code)}}</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
<div class="course-meta">
|
||||
<span><i class="el-icon-office-building"></i> {{course.courseTypeName || '未设置课程类型'}}</span>
|
||||
<span><i class="el-icon-user"></i> {{course.lecturerName || '未设置讲师'}}</span>
|
||||
</div>
|
||||
<div class="course-intro">{{course.courseIntro || '暂无课程介绍'}}</div>
|
||||
<div class="course-time">
|
||||
<i class="el-icon-time"></i>
|
||||
<span v-if="course.openType === 'long_term'">长期开放</span>
|
||||
<span v-else>{{course.startTimeText || '-'}} 至 {{course.endTimeText || '-'}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="course-empty">暂无相关课程</div>
|
||||
|
||||
<el-pagination
|
||||
class="mt20"
|
||||
background
|
||||
layout="total, sizes, prev, pager, next"
|
||||
:current-page.sync="pageForm.pageNumber"
|
||||
:page-size.sync="pageForm.pageSize"
|
||||
:total="pageForm.totalCount"
|
||||
@size-change="pageData"
|
||||
@current-change="pageData">
|
||||
</el-pagination>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data() {
|
||||
return {
|
||||
query: {
|
||||
keyword: "",
|
||||
courseTypeId: "",
|
||||
recommendFlag: ""
|
||||
},
|
||||
courseTypeOptions: [],
|
||||
recommendOptions: [],
|
||||
pageForm: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
totalCount: 0,
|
||||
list: []
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async pageData() {
|
||||
const resp = await this.$axios.post(loc() + "/pageData", Object.assign({}, this.query, {
|
||||
pageNumber: this.pageForm.pageNumber,
|
||||
pageSize: this.pageForm.pageSize
|
||||
}))
|
||||
if (resp.code === 0) {
|
||||
this.pageForm = resp.data
|
||||
}
|
||||
},
|
||||
doSearch() {
|
||||
this.pageForm.pageNumber = 1
|
||||
this.pageData()
|
||||
},
|
||||
selectRecommend(code) {
|
||||
this.query.recommendFlag = code
|
||||
this.doSearch()
|
||||
},
|
||||
selectCourseType(id) {
|
||||
this.query.courseTypeId = id
|
||||
this.doSearch()
|
||||
},
|
||||
async loadOptions() {
|
||||
const typeResp = await this.$axios.post(loc() + "/courseTypes")
|
||||
if (typeResp.code === 0) {
|
||||
this.courseTypeOptions = typeResp.data
|
||||
}
|
||||
const recommendResp = await this.$axios.post(loc() + "/recommendOptions")
|
||||
if (recommendResp.code === 0) {
|
||||
this.recommendOptions = recommendResp.data
|
||||
}
|
||||
},
|
||||
getCoverUrl(cover) {
|
||||
if (!cover) return ""
|
||||
if (Array.isArray(cover)) {
|
||||
return cover.length ? (cover[0].url || cover[0].response?.data || cover[0].data || "") : ""
|
||||
}
|
||||
if (typeof cover === "string" && cover.trim().startsWith("[")) {
|
||||
try {
|
||||
const files = JSON.parse(cover)
|
||||
return files.length ? (files[0].url || files[0].response?.data || files[0].data || "") : ""
|
||||
} catch (e) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return cover
|
||||
},
|
||||
splitFlags(flags) {
|
||||
return flags ? flags.split(",").filter(Boolean) : []
|
||||
},
|
||||
getRecommendName(code) {
|
||||
const item = this.recommendOptions.find(v => v.code === code)
|
||||
return item ? item.name : code
|
||||
},
|
||||
firstRecommendName(flags) {
|
||||
const codes = this.splitFlags(flags)
|
||||
return codes.length ? this.getRecommendName(codes[0]) : ""
|
||||
},
|
||||
openDetail(course) {
|
||||
window.location.href = loc() + "/study?id=" + course.id
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
await this.loadOptions()
|
||||
await this.pageData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,755 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<style>
|
||||
#sidebar-menu,
|
||||
#menu-toggle-btn,
|
||||
#menu-overlay {
|
||||
display: none !important;
|
||||
}
|
||||
#sub-app-container-main-content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
#sub-app-container-main-content-body {
|
||||
height: 100%;
|
||||
}
|
||||
.study-page {
|
||||
min-height: calc(100vh - 50px);
|
||||
background: #eef3f8;
|
||||
}
|
||||
.study-header {
|
||||
height: 150px;
|
||||
margin: 12px 14px 0;
|
||||
padding: 20px 24px;
|
||||
color: #1f2937;
|
||||
background: linear-gradient(120deg, #ffffff 0%, #f4f8ff 58%, #eaf2ff 100%);
|
||||
border: 1px solid #dbe7f5;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 6px 18px rgba(31, 64, 116, .08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 22px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.study-back {
|
||||
align-self: flex-start;
|
||||
margin-left: auto;
|
||||
order: 5;
|
||||
color: #1e63c8;
|
||||
border-color: #b8d0f2;
|
||||
background: #fff;
|
||||
}
|
||||
.study-cover {
|
||||
width: 190px;
|
||||
height: 102px;
|
||||
object-fit: cover;
|
||||
background: #eef4fb;
|
||||
border: 1px solid #d7e3f1;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 12px rgba(31, 64, 116, .12);
|
||||
}
|
||||
.study-cover-empty {
|
||||
width: 190px;
|
||||
height: 102px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #eef4fb;
|
||||
color: #6b7d90;
|
||||
border: 1px solid #d7e3f1;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 12px rgba(31, 64, 116, .12);
|
||||
font-weight: 600;
|
||||
}
|
||||
.study-course-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.study-title {
|
||||
font-size: 28px;
|
||||
line-height: 1.2;
|
||||
font-weight: 700;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.study-title .el-tag {
|
||||
margin-left: 8px;
|
||||
vertical-align: 5px;
|
||||
}
|
||||
.study-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px 26px;
|
||||
color: #526376;
|
||||
font-size: 14px;
|
||||
}
|
||||
.study-body {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
height: calc(100vh - 212px);
|
||||
padding: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.study-tree-panel {
|
||||
width: 340px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.study-tree-title {
|
||||
height: 54px;
|
||||
padding: 0 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: #1768e5;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
border-bottom: 1px solid #edf1f7;
|
||||
}
|
||||
.study-tree {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
.study-node {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
gap: 6px;
|
||||
}
|
||||
.study-node-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.study-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.resource-head {
|
||||
min-height: 54px;
|
||||
padding: 0 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #edf1f7;
|
||||
}
|
||||
.resource-title {
|
||||
color: #1f2937;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.resource-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.study-progress {
|
||||
width: 180px;
|
||||
}
|
||||
.resource-view {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
.media-player {
|
||||
width: 100%;
|
||||
max-height: calc(100vh - 300px);
|
||||
background: #000;
|
||||
}
|
||||
.audio-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 320px;
|
||||
background: linear-gradient(135deg, #132544, #0a1224);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.audio-wrap audio {
|
||||
width: 80%;
|
||||
}
|
||||
.image-preview {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
}
|
||||
.doc-frame {
|
||||
width: 100%;
|
||||
height: calc(100vh - 275px);
|
||||
border: 0;
|
||||
background: #fff;
|
||||
}
|
||||
.file-fallback {
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
color: #667085;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" class="study-page" v-cloak>
|
||||
<div class="study-header">
|
||||
<el-button class="study-back" icon="el-icon-back" size="small" @click="goBack">返回</el-button>
|
||||
<img v-if="coverUrl" class="study-cover" :src="coverUrl" :alt="course.courseName">
|
||||
<div v-else class="study-cover-empty">课程图片</div>
|
||||
<div class="study-course-main">
|
||||
<div class="study-title">
|
||||
{{course.courseName || '课程学习'}}
|
||||
<el-tag v-for="code in splitFlags(course.recommendFlags)" :key="code" size="small" type="warning">{{getRecommendName(code)}}</el-tag>
|
||||
</div>
|
||||
<div class="study-meta">
|
||||
<span><i class="el-icon-user"></i> 授课讲师:{{course.lecturerName || '未设置'}}</span>
|
||||
<span><i class="el-icon-date"></i> {{course.openType === 'long_term' ? '长期开放' : ((course.startTimeText || '-') + ' 至 ' + (course.endTimeText || '-'))}}</span>
|
||||
<span><i class="el-icon-collection-tag"></i> 课程类型:{{course.courseTypeName || '未设置'}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="study-body">
|
||||
<div class="study-tree-panel">
|
||||
<div class="study-tree-title">
|
||||
<span>课程安排</span>
|
||||
<el-button type="text" size="mini" @click="collapseAll">收起</el-button>
|
||||
</div>
|
||||
<div class="study-tree">
|
||||
<el-tree
|
||||
ref="studyTree"
|
||||
:data="treeData"
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
highlight-current
|
||||
:expand-on-click-node="false"
|
||||
:props="{children:'children', label:'title'}"
|
||||
@node-click="nodeClick">
|
||||
<span slot-scope="{ data }" class="study-node">
|
||||
<i :class="getNodeIcon(data)"></i>
|
||||
<span class="study-node-title">{{data.title}}</span>
|
||||
<el-tag v-if="data.required" size="mini" type="warning">必学</el-tag>
|
||||
</span>
|
||||
</el-tree>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="study-content">
|
||||
<div class="resource-head">
|
||||
<div class="resource-title">{{selectedResource.title || '请选择课程资料'}}</div>
|
||||
<div class="resource-actions">
|
||||
<el-progress
|
||||
v-if="selectedResource.id"
|
||||
class="study-progress"
|
||||
:percentage="recordProgress"
|
||||
:stroke-width="8">
|
||||
</el-progress>
|
||||
<el-tag v-if="studying" size="mini" type="success">学习中 {{recordStudyTime}}</el-tag>
|
||||
<el-button v-if="selectedResource.id && !studying" size="mini" type="primary" icon="el-icon-video-play" @click="startStudy">开始学习</el-button>
|
||||
<el-button v-if="studying" size="mini" type="danger" icon="el-icon-video-pause" @click="finishStudy(false)">结束学习</el-button>
|
||||
<el-button v-if="fileUrl" size="mini" type="text" icon="el-icon-download" @click="openFile">打开原文件</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-view">
|
||||
<template v-if="selectedResource.id">
|
||||
<video
|
||||
v-if="selectedResource.resourceType === 'video'"
|
||||
ref="mediaPlayer"
|
||||
class="media-player"
|
||||
:src="fileUrl"
|
||||
controls
|
||||
controlslist="nodownload"
|
||||
@play="mediaPlay"
|
||||
@ended="mediaEnded"
|
||||
@timeupdate="saveProgress"
|
||||
@loadedmetadata="mediaReady">
|
||||
</video>
|
||||
<div v-else-if="selectedResource.resourceType === 'audio'" class="audio-wrap">
|
||||
<audio
|
||||
ref="mediaPlayer"
|
||||
:src="fileUrl"
|
||||
controls
|
||||
controlslist="nodownload"
|
||||
@play="mediaPlay"
|
||||
@ended="mediaEnded"
|
||||
@timeupdate="saveProgress"
|
||||
@loadedmetadata="mediaReady">
|
||||
</audio>
|
||||
</div>
|
||||
<img v-else-if="selectedResource.resourceType === 'image'" class="image-preview" :src="fileUrl" :alt="selectedResource.title">
|
||||
<iframe v-else-if="canInlinePreview(selectedResource)" class="doc-frame" :src="inlinePreviewUrl"></iframe>
|
||||
<div v-else class="file-fallback">
|
||||
<file-preview :files="selectedResource.fileData" complete_result></file-preview>
|
||||
<el-button class="mt20" type="primary" icon="el-icon-view" @click="openFile">打开资料</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-empty v-else description="请选择左侧课程资料"></el-empty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data() {
|
||||
return {
|
||||
courseId: "",
|
||||
apiBase: "/platform/learning/course/display",
|
||||
recordApi: "/platform/learning/study/record",
|
||||
course: {},
|
||||
treeData: [],
|
||||
selectedResource: {},
|
||||
recommendOptions: [],
|
||||
studying: false,
|
||||
currentSegmentId: "",
|
||||
recordProgress: 0,
|
||||
recordStudyTime: "0秒",
|
||||
pendingSeconds: 0,
|
||||
heartbeatTimer: null,
|
||||
startingStudy: false,
|
||||
autoResumeKey: "",
|
||||
lastPositionSyncAt: 0,
|
||||
lastActiveAt: Date.now(),
|
||||
inactiveLimit: 60000
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
coverUrl() {
|
||||
return this.getFileUrl(this.course.cover)
|
||||
},
|
||||
fileUrl() {
|
||||
return this.getFileUrl(this.selectedResource.fileData)
|
||||
},
|
||||
inlinePreviewUrl() {
|
||||
const id = this.getFileId(this.selectedResource.fileData)
|
||||
return id ? this.apiBase + "/pdfPreview?id=" + encodeURIComponent(id) : this.fileUrl
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getQuery(name) {
|
||||
return new URLSearchParams(window.location.search).get(name) || ""
|
||||
},
|
||||
async loadCourse() {
|
||||
const resp = await this.$axios.post(this.apiBase + "/courseInfo", { id: this.courseId })
|
||||
if (resp.code === 0) {
|
||||
this.course = resp.data
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
},
|
||||
async loadTree() {
|
||||
const resp = await this.$axios.post(this.apiBase + "/studyTree", { courseId: this.courseId })
|
||||
if (resp.code === 0) {
|
||||
this.treeData = resp.data || []
|
||||
const first = this.findFirstResource(this.treeData)
|
||||
if (first) {
|
||||
this.nodeClick(first)
|
||||
this.$nextTick(() => this.$refs.studyTree && this.$refs.studyTree.setCurrentKey(first.id))
|
||||
}
|
||||
}
|
||||
},
|
||||
async loadRecommendOptions() {
|
||||
const resp = await this.$axios.post(this.apiBase + "/recommendOptions")
|
||||
if (resp.code === 0) {
|
||||
this.recommendOptions = resp.data
|
||||
}
|
||||
},
|
||||
findFirstResource(nodes) {
|
||||
for (const node of nodes || []) {
|
||||
if (node.type === "resource") return node
|
||||
const child = this.findFirstResource(node.children || [])
|
||||
if (child) return child
|
||||
}
|
||||
return null
|
||||
},
|
||||
nodeClick(data) {
|
||||
if (data.type !== "resource") return
|
||||
if (this.studying && this.selectedResource.id !== data.id) {
|
||||
this.$message.warning("请先结束当前章节资料的学习")
|
||||
return
|
||||
}
|
||||
this.selectedResource = data
|
||||
this.recordProgress = data.progressPercent || 0
|
||||
this.recordStudyTime = this.formatSeconds(Number(data.studySeconds || 0))
|
||||
},
|
||||
getNodeIcon(data) {
|
||||
if (data.type === "resource") {
|
||||
const map = {
|
||||
video: "el-icon-video-camera",
|
||||
audio: "el-icon-headset",
|
||||
image: "el-icon-picture-outline",
|
||||
pdf: "el-icon-document",
|
||||
word: "el-icon-document",
|
||||
ppt: "el-icon-document"
|
||||
}
|
||||
return map[data.resourceType] || "el-icon-paperclip"
|
||||
}
|
||||
return data.nodeType === "chapter" ? "el-icon-folder" : "el-icon-notebook-2"
|
||||
},
|
||||
getFileUrl(value) {
|
||||
if (!value) return ""
|
||||
if (Array.isArray(value)) {
|
||||
return value.length ? (value[0].url || value[0].response?.data || value[0].data || "") : ""
|
||||
}
|
||||
if (typeof value === "string" && value.trim().startsWith("[")) {
|
||||
try {
|
||||
const files = JSON.parse(value)
|
||||
return files.length ? (files[0].url || files[0].response?.data || files[0].data || "") : ""
|
||||
} catch (e) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return value
|
||||
},
|
||||
getFileId(value) {
|
||||
const url = this.getFileUrl(value)
|
||||
if (!url) return ""
|
||||
const matched = url.match(/[?&]id=([^&]+)/)
|
||||
if (matched) return decodeURIComponent(matched[1])
|
||||
if (!url.includes("/") && !url.includes(".")) return url
|
||||
return ""
|
||||
},
|
||||
canInlinePreview(resource) {
|
||||
const type = resource.resourceType
|
||||
const ext = (resource.fileExt || "").toLowerCase()
|
||||
return ["pdf", "ppt", "word"].includes(type) || ["pdf", "ppt", "pptx", "doc", "docx"].includes(ext)
|
||||
},
|
||||
progressKey() {
|
||||
return "learning-progress-" + this.courseId + "-" + this.selectedResource.id
|
||||
},
|
||||
mediaReady() {
|
||||
this.askResume()
|
||||
},
|
||||
askResume() {
|
||||
if (!["video", "audio"].includes(this.selectedResource.resourceType)) return
|
||||
if (this.studying || this.startingStudy) return
|
||||
const currentKey = this.progressKey()
|
||||
if (this.autoResumeKey === currentKey) return
|
||||
this.autoResumeKey = currentKey
|
||||
const player = this.$refs.mediaPlayer
|
||||
const localSaved = Number(localStorage.getItem(this.progressKey()) || 0)
|
||||
const serverSaved = Number(this.selectedResource.lastPositionSeconds || 0)
|
||||
const saved = Math.max(localSaved, serverSaved)
|
||||
if (!player || !saved || saved < 5 || saved >= player.duration - 5) return
|
||||
this.$confirm("检测到上次学习到 " + this.formatSeconds(saved) + ",请选择继续学习或从头开始。", "继续学习", {
|
||||
confirmButtonText: "继续学习",
|
||||
cancelButtonText: "从头开始",
|
||||
type: "info"
|
||||
}).then(() => {
|
||||
this.startStudy({ autoPlay: true, seekTo: saved })
|
||||
}).catch(() => {
|
||||
localStorage.removeItem(this.progressKey())
|
||||
this.saveServerPosition(0)
|
||||
this.startStudy({ autoPlay: true, seekTo: 0 })
|
||||
})
|
||||
},
|
||||
async mediaPlay() {
|
||||
if (this.studying || this.startingStudy) return
|
||||
await this.startStudy({ fromMedia: true })
|
||||
},
|
||||
mediaEnded() {
|
||||
this.finishStudy(false, { silent: true })
|
||||
localStorage.removeItem(this.progressKey())
|
||||
},
|
||||
async playSelectedMedia(seekTo) {
|
||||
if (!["video", "audio"].includes(this.selectedResource.resourceType)) return
|
||||
await this.$nextTick()
|
||||
const player = this.$refs.mediaPlayer
|
||||
if (!player) return
|
||||
try {
|
||||
if (typeof seekTo === "number" && !Number.isNaN(seekTo)) {
|
||||
await this.seekBeforePlay(player, seekTo)
|
||||
}
|
||||
const playPromise = player.play && player.play()
|
||||
if (playPromise && playPromise.catch) {
|
||||
await playPromise.catch(() => {
|
||||
this.$message.warning("浏览器阻止了自动播放,请点击播放器播放按钮继续")
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
this.$message.warning("播放器定位失败,请手动点击播放后继续")
|
||||
}
|
||||
},
|
||||
async seekBeforePlay(player, seekTo) {
|
||||
await this.waitForMediaReady(player)
|
||||
const target = this.normalizeSeekTarget(player, seekTo)
|
||||
if (target === null) return
|
||||
if (Math.abs((player.currentTime || 0) - target) <= 1) return
|
||||
player.pause()
|
||||
player.currentTime = target
|
||||
await this.waitForSeek(player, target, 1800)
|
||||
if (Math.abs((player.currentTime || 0) - target) > 1) {
|
||||
player.currentTime = target
|
||||
await this.waitForSeek(player, target, 1200)
|
||||
}
|
||||
},
|
||||
waitForMediaReady(player) {
|
||||
if (player.readyState >= 1 && !Number.isNaN(player.duration)) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
const done = () => {
|
||||
player.removeEventListener("loadedmetadata", done)
|
||||
player.removeEventListener("durationchange", done)
|
||||
resolve()
|
||||
}
|
||||
player.addEventListener("loadedmetadata", done, { once: true })
|
||||
player.addEventListener("durationchange", done, { once: true })
|
||||
setTimeout(done, 2000)
|
||||
})
|
||||
},
|
||||
waitForSeek(player, target, timeout) {
|
||||
return new Promise(resolve => {
|
||||
const done = () => {
|
||||
player.removeEventListener("seeked", done)
|
||||
player.removeEventListener("timeupdate", done)
|
||||
resolve()
|
||||
}
|
||||
player.addEventListener("seeked", done, { once: true })
|
||||
player.addEventListener("timeupdate", done, { once: true })
|
||||
setTimeout(done, timeout)
|
||||
})
|
||||
},
|
||||
normalizeSeekTarget(player, seekTo) {
|
||||
const raw = Math.max(0, Number(seekTo || 0))
|
||||
if (Number.isNaN(raw)) return null
|
||||
if (!Number.isNaN(player.duration) && player.duration > 0) {
|
||||
return Math.min(raw, Math.max(0, player.duration - 1))
|
||||
}
|
||||
return raw
|
||||
},
|
||||
saveProgress() {
|
||||
if (!["video", "audio"].includes(this.selectedResource.resourceType)) return
|
||||
const player = this.$refs.mediaPlayer
|
||||
if (player && player.currentTime > 0) {
|
||||
const position = Math.floor(player.currentTime)
|
||||
localStorage.setItem(this.progressKey(), String(position))
|
||||
this.$set(this.selectedResource, "lastPositionSeconds", position)
|
||||
if (Date.now() - this.lastPositionSyncAt > 5000) {
|
||||
this.saveServerPosition(position)
|
||||
}
|
||||
}
|
||||
},
|
||||
saveServerPosition(position, useBeacon) {
|
||||
if (!this.selectedResource.id || !["video", "audio"].includes(this.selectedResource.resourceType)) return
|
||||
const seconds = Math.max(0, Math.floor(Number(position || 0)))
|
||||
this.lastPositionSyncAt = Date.now()
|
||||
if (useBeacon && navigator.sendBeacon) {
|
||||
const body = new URLSearchParams()
|
||||
body.append("courseId", this.courseId)
|
||||
body.append("resourceId", this.selectedResource.id)
|
||||
body.append("positionSeconds", String(seconds))
|
||||
const blob = new Blob([body.toString()], { type: "application/x-www-form-urlencoded;charset=UTF-8" })
|
||||
navigator.sendBeacon(this.recordApi + "/position", blob)
|
||||
return
|
||||
}
|
||||
this.$axios.post(this.recordApi + "/position", {
|
||||
courseId: this.courseId,
|
||||
resourceId: this.selectedResource.id,
|
||||
positionSeconds: seconds
|
||||
})
|
||||
},
|
||||
async startStudy(options) {
|
||||
const config = options && !options.target ? options : {}
|
||||
if (!this.selectedResource.id) {
|
||||
this.$message.warning("请选择左侧课程资料")
|
||||
return
|
||||
}
|
||||
if (this.studying) {
|
||||
if (config.autoPlay) {
|
||||
this.playSelectedMedia(config.seekTo)
|
||||
}
|
||||
return
|
||||
}
|
||||
this.startingStudy = true
|
||||
let resp
|
||||
try {
|
||||
resp = await this.$axios.post(this.recordApi + "/start", {
|
||||
courseId: this.courseId,
|
||||
resourceId: this.selectedResource.id,
|
||||
positionSeconds: typeof config.seekTo === "number" ? Math.floor(config.seekTo) : this.getMediaPosition()
|
||||
})
|
||||
} finally {
|
||||
this.startingStudy = false
|
||||
}
|
||||
if (resp.code !== 0) {
|
||||
this.$message.warning(resp.msg)
|
||||
return
|
||||
}
|
||||
this.currentSegmentId = resp.data.segmentId
|
||||
this.studying = true
|
||||
this.pendingSeconds = 0
|
||||
this.lastActiveAt = Date.now()
|
||||
this.applyRecordState(resp.data)
|
||||
this.startHeartbeatTimer()
|
||||
if (config.autoPlay !== false && !config.fromMedia) {
|
||||
this.playSelectedMedia(config.seekTo)
|
||||
}
|
||||
this.$message.success("已开始学习")
|
||||
},
|
||||
async heartbeat() {
|
||||
if (!this.studying || !this.currentSegmentId || this.pendingSeconds <= 0) return
|
||||
const seconds = this.pendingSeconds
|
||||
this.pendingSeconds = 0
|
||||
const resp = await this.$axios.post(this.recordApi + "/heartbeat", {
|
||||
segmentId: this.currentSegmentId,
|
||||
activeSeconds: seconds,
|
||||
positionSeconds: this.getMediaPosition()
|
||||
})
|
||||
if (resp.code === 0) {
|
||||
this.applyRecordState(resp.data)
|
||||
} else {
|
||||
this.stopHeartbeatTimer()
|
||||
this.studying = false
|
||||
this.currentSegmentId = ""
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
},
|
||||
async finishStudy(force, options) {
|
||||
const config = options || {}
|
||||
if (!this.currentSegmentId) return
|
||||
const segmentId = this.currentSegmentId
|
||||
const seconds = this.pendingSeconds
|
||||
this.pendingSeconds = 0
|
||||
this.stopHeartbeatTimer()
|
||||
this.studying = false
|
||||
this.currentSegmentId = ""
|
||||
if (!force) {
|
||||
this.pauseSelectedMedia()
|
||||
}
|
||||
if (force && navigator.sendBeacon) {
|
||||
this.saveServerPosition(this.getMediaPosition(), true)
|
||||
const formData = new FormData()
|
||||
formData.append("segmentId", segmentId)
|
||||
formData.append("activeSeconds", String(seconds))
|
||||
formData.append("positionSeconds", String(this.getMediaPosition()))
|
||||
navigator.sendBeacon(this.recordApi + "/finish", formData)
|
||||
return
|
||||
}
|
||||
const resp = await this.$axios.post(this.recordApi + "/finish", {
|
||||
segmentId: segmentId,
|
||||
activeSeconds: seconds,
|
||||
positionSeconds: this.getMediaPosition()
|
||||
})
|
||||
if (resp.code === 0 && resp.data) {
|
||||
this.applyRecordState(resp.data)
|
||||
if (!config.silent) {
|
||||
this.$message.success("学习已结束")
|
||||
}
|
||||
}
|
||||
},
|
||||
pauseSelectedMedia() {
|
||||
if (!["video", "audio"].includes(this.selectedResource.resourceType)) return
|
||||
const player = this.$refs.mediaPlayer
|
||||
if (player && !player.paused) {
|
||||
player.pause()
|
||||
}
|
||||
},
|
||||
startHeartbeatTimer() {
|
||||
this.stopHeartbeatTimer()
|
||||
this.heartbeatTimer = setInterval(() => {
|
||||
if (!this.studying) return
|
||||
if (this.isEffectiveLearning()) {
|
||||
this.pendingSeconds += 1
|
||||
}
|
||||
if (this.pendingSeconds >= 15) {
|
||||
this.heartbeat()
|
||||
}
|
||||
}, 1000)
|
||||
},
|
||||
stopHeartbeatTimer() {
|
||||
if (this.heartbeatTimer) {
|
||||
clearInterval(this.heartbeatTimer)
|
||||
this.heartbeatTimer = null
|
||||
}
|
||||
},
|
||||
isEffectiveLearning() {
|
||||
if (document.hidden) return false
|
||||
if (Date.now() - this.lastActiveAt > this.inactiveLimit) return false
|
||||
if (["video", "audio"].includes(this.selectedResource.resourceType)) {
|
||||
const player = this.$refs.mediaPlayer
|
||||
return !!player && !player.paused && !player.ended
|
||||
}
|
||||
return true
|
||||
},
|
||||
markActive() {
|
||||
this.lastActiveAt = Date.now()
|
||||
},
|
||||
applyRecordState(data) {
|
||||
this.recordProgress = Number(data.progressPercent || 0)
|
||||
this.recordStudyTime = data.studyTimeText || this.formatSeconds(Number(data.studySeconds || 0))
|
||||
if (this.selectedResource.id && data.lastPositionSeconds !== undefined) {
|
||||
this.$set(this.selectedResource, "lastPositionSeconds", Number(data.lastPositionSeconds || 0))
|
||||
}
|
||||
},
|
||||
getMediaPosition() {
|
||||
if (!["video", "audio"].includes(this.selectedResource.resourceType)) return 0
|
||||
const player = this.$refs.mediaPlayer
|
||||
return player ? Math.floor(player.currentTime || 0) : Number(this.selectedResource.lastPositionSeconds || 0)
|
||||
},
|
||||
formatSeconds(seconds) {
|
||||
const hour = Math.floor(seconds / 3600)
|
||||
const minute = Math.floor(seconds % 3600 / 60)
|
||||
const second = Math.floor(seconds % 60)
|
||||
if (hour > 0) return hour + "小时" + minute + "分" + second + "秒"
|
||||
if (minute > 0) return minute + "分" + second + "秒"
|
||||
return second + "秒"
|
||||
},
|
||||
splitFlags(flags) {
|
||||
return flags ? flags.split(",").filter(Boolean) : []
|
||||
},
|
||||
getRecommendName(code) {
|
||||
const item = this.recommendOptions.find(v => v.code === code)
|
||||
return item ? item.name : code
|
||||
},
|
||||
openFile() {
|
||||
if (this.fileUrl) window.open(this.fileUrl)
|
||||
},
|
||||
collapseAll() {
|
||||
const nodesMap = this.$refs.studyTree && this.$refs.studyTree.store.nodesMap
|
||||
Object.keys(nodesMap || {}).forEach(key => {
|
||||
nodesMap[key].expanded = false
|
||||
})
|
||||
},
|
||||
async goBack() {
|
||||
this.saveServerPosition(this.getMediaPosition())
|
||||
await this.finishStudy(false, { silent: true })
|
||||
window.location.href = this.apiBase
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.courseId = this.getQuery("id")
|
||||
window.addEventListener("mousemove", this.markActive)
|
||||
window.addEventListener("keydown", this.markActive)
|
||||
window.addEventListener("click", this.markActive)
|
||||
window.addEventListener("scroll", this.markActive, true)
|
||||
window.addEventListener("beforeunload", () => this.finishStudy(true))
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (document.hidden) {
|
||||
this.saveServerPosition(this.getMediaPosition(), true)
|
||||
}
|
||||
})
|
||||
await this.loadRecommendOptions()
|
||||
await this.loadCourse()
|
||||
await this.loadTree()
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.finishStudy(true)
|
||||
this.stopHeartbeatTimer()
|
||||
window.removeEventListener("mousemove", this.markActive)
|
||||
window.removeEventListener("keydown", this.markActive)
|
||||
window.removeEventListener("click", this.markActive)
|
||||
window.removeEventListener("scroll", this.markActive, true)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,396 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<el-card shadow="never">
|
||||
<search :is-search-button="false">
|
||||
<search-item label="课程名称">
|
||||
<el-input v-model="pageForm.courseName" clearable placeholder="请输入课程名称" style="width: 100%" @keyup.enter.native="doSearch"></el-input>
|
||||
</search-item>
|
||||
<search-item label="课程类型">
|
||||
<el-select v-model="pageForm.courseTypeId" clearable filterable placeholder="请选择课程类型" style="width: 100%">
|
||||
<el-option v-for="item in courseTypeOptions" :key="item.id" :label="item.typeName" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="开课时间">
|
||||
<el-date-picker
|
||||
v-model="pageForm.courseTime"
|
||||
type="daterange"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
range-separator="至"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
style="width: 100%">
|
||||
</el-date-picker>
|
||||
</search-item>
|
||||
<search-item label="授课讲师">
|
||||
<el-input v-model="pageForm.lecturerName" clearable placeholder="请输入授课讲师" style="width: 100%" @keyup.enter.native="doSearch"></el-input>
|
||||
</search-item>
|
||||
<search-item label="课程状态">
|
||||
<el-select v-model="pageForm.status" clearable placeholder="请选择课程状态" style="width: 100%">
|
||||
<el-option v-for="item in statusOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="推荐标识">
|
||||
<el-select v-model="pageForm.recommendFlag" clearable placeholder="请选择推荐标识" style="width: 100%">
|
||||
<el-option v-for="item in recommendOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<div class="search-query">
|
||||
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
|
||||
<el-button size="medium" @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mt10" shadow="never">
|
||||
<table-tool :app="this" label="课程列表">
|
||||
<el-button @click="openAdd" size="medium" type="primary">
|
||||
<i class="el-icon-plus"></i>
|
||||
新增课程
|
||||
</el-button>
|
||||
</table-tool>
|
||||
|
||||
<el-table
|
||||
v-loading="tableLoading"
|
||||
:data="tableData"
|
||||
:size="tableSize"
|
||||
border
|
||||
@sort-change="pageOrder">
|
||||
<el-table-column :index="indexMethod" align="center" header-align="center" label="序号" type="index" width="80"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="课程名称" prop="courseName" sortable="custom" show-overflow-tooltip min-width="180"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="课程类型" prop="courseTypeName" sortable="custom" show-overflow-tooltip width="140"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="授课讲师" prop="lecturerName" sortable="custom" show-overflow-tooltip width="140"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="开课时间" prop="startTime" sortable="custom" width="230">
|
||||
<template slot-scope="{row}">
|
||||
<span v-if="row.openType === 'long_term'">长期开放</span>
|
||||
<span v-else>{{formatTime(row.startTime)}} 至 {{formatTime(row.endTime)}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="课程状态" prop="status" sortable="custom" width="120">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag :type="getStatusType(row.status)" size="mini">{{getStatusName(row.status)}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="推荐标识" prop="recommendFlags" sortable="custom" show-overflow-tooltip min-width="140">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag
|
||||
v-for="code in splitValue(row.recommendFlags)"
|
||||
:key="code"
|
||||
:type="getRecommendType(code)"
|
||||
size="mini"
|
||||
style="margin: 2px 3px">
|
||||
{{getDictName(code, recommendOptions)}}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="排序编码" prop="sortNum" sortable="custom" width="120"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="操作" width="260">
|
||||
<template slot-scope="{row}">
|
||||
<el-button @click="openEdit(row)" size="mini" type="primary">编辑</el-button>
|
||||
<el-button @click="goChapterContent(row)" size="mini" type="success">章节内容</el-button>
|
||||
<el-button @click="doDelete(row)" size="mini" type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
</guava>
|
||||
|
||||
<el-dialog :title="title" :visible.sync="dialogVisible" :close-on-click-modal="false" width="72%">
|
||||
<el-form :model="formData" ref="courseForm" :rules="formRules" label-width="110px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="课程名称" prop="courseName">
|
||||
<el-input v-model="formData.courseName" maxlength="100" placeholder="请输入课程名称"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="课程类型" prop="courseTypeId">
|
||||
<el-select v-model="formData.courseTypeId" filterable placeholder="请选择课程类型" style="width: 100%">
|
||||
<el-option v-for="item in courseTypeOptions" :key="item.id" :label="item.typeName" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="授课讲师" prop="lecturerName">
|
||||
<el-input v-model="formData.lecturerName" maxlength="100" placeholder="请输入教师姓名"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="课程状态" prop="status">
|
||||
<el-select v-model="formData.status" placeholder="请选择课程状态" style="width: 100%">
|
||||
<el-option v-for="item in statusOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="开课方式" prop="openType">
|
||||
<el-radio-group v-model="formData.openType">
|
||||
<el-radio label="fixed">固定开课时间</el-radio>
|
||||
<el-radio label="long_term">长期开放</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="开课时间" prop="openTime" v-if="formData.openType !== 'long_term'">
|
||||
<el-date-picker
|
||||
v-model="formData.openTime"
|
||||
type="datetimerange"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
range-separator="至"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
style="width: 100%">
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="推荐标识" prop="recommendFlagList">
|
||||
<el-select v-model="formData.recommendFlagList" multiple clearable placeholder="请选择推荐标识" style="width: 100%">
|
||||
<el-option v-for="item in recommendOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序编码" prop="sortNum">
|
||||
<el-input-number v-model="formData.sortNum" :controls="false" :min="0" :precision="0" style="width: 100%" placeholder="请输入排序编码"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="课程介绍" prop="courseIntro">
|
||||
<el-input v-model="formData.courseIntro" type="textarea" maxlength="2000" :rows="4" show-word-limit placeholder="请输入课程简介"></el-input>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="适合人群" prop="suitablePeople">
|
||||
<el-input v-model="formData.suitablePeople" type="textarea" maxlength="500" :rows="3" show-word-limit placeholder="请输入适合人群"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="学习目标" prop="learningGoal">
|
||||
<el-input v-model="formData.learningGoal" type="textarea" maxlength="500" :rows="3" show-word-limit placeholder="请输入学习目标"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="课程封面" prop="cover">
|
||||
<file-upload
|
||||
:upload_number="1"
|
||||
:upload_size="5242880"
|
||||
:value.sync="formData.cover"
|
||||
accept=".jpg,.jpeg,.png"
|
||||
complete_result
|
||||
upload_mode="image"
|
||||
upload_result_category="interval">
|
||||
</file-upload>
|
||||
<div class="el-upload__tip">只能上传1个文件;只能上传.jpg,.jpeg,.png文件;单个文件大小不能超过5M。</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="subDis" @click="operation">确定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
return {
|
||||
title: "",
|
||||
dialogVisible: false,
|
||||
subDis: false,
|
||||
courseTypeOptions: [],
|
||||
recommendOptions: [],
|
||||
statusOptions: [
|
||||
{ name: "草稿", code: "draft" },
|
||||
{ name: "待发布", code: "pending" },
|
||||
{ name: "已发布", code: "published" },
|
||||
{ name: "已下架", code: "offline" }
|
||||
],
|
||||
formData: {},
|
||||
formRules: {
|
||||
courseName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
courseTypeId: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
lecturerName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
status: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
sortNum: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
cover: [{ required: true, message: "请上传课程封面", trigger: ["blur", "change"] }]
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
buildPageParams() {
|
||||
const params = Object.assign({}, this.pageForm)
|
||||
params.startTime = params.courseTime && params.courseTime.length ? params.courseTime[0] : ""
|
||||
params.endTime = params.courseTime && params.courseTime.length ? params.courseTime[1] : ""
|
||||
return params
|
||||
},
|
||||
pageData(data = null) {
|
||||
const address = this.pageDataUrl ? this.pageDataUrl : loc() + "/pageData"
|
||||
this.tableLoading = true
|
||||
this.$axios.post(address, data ? data : this.buildPageParams()).then((res) => {
|
||||
this.tableLoading = false
|
||||
if (res.code === 0) {
|
||||
this.tableData = res.data.list
|
||||
this.pageForm.totalCount = res.data.totalCount
|
||||
}
|
||||
})
|
||||
},
|
||||
resetSearch() {
|
||||
this.pageForm.courseName = ""
|
||||
this.pageForm.courseTypeId = ""
|
||||
this.pageForm.courseTime = []
|
||||
this.pageForm.lecturerName = ""
|
||||
this.pageForm.status = ""
|
||||
this.pageForm.recommendFlag = ""
|
||||
this.doSearch()
|
||||
},
|
||||
openAdd() {
|
||||
this.title = "新增课程"
|
||||
this.formData = {
|
||||
courseName: "",
|
||||
courseTypeId: "",
|
||||
lecturerName: "",
|
||||
courseIntro: "",
|
||||
suitablePeople: "",
|
||||
learningGoal: "",
|
||||
openType: "fixed",
|
||||
openTime: [],
|
||||
status: "draft",
|
||||
recommendFlagList: [],
|
||||
sortNum: 0,
|
||||
cover: ""
|
||||
}
|
||||
this.dialogVisible = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.courseForm && this.$refs.courseForm.clearValidate()
|
||||
})
|
||||
},
|
||||
openEdit(row) {
|
||||
this.title = "编辑课程"
|
||||
this.formData = Object.assign({}, row, {
|
||||
openTime: row.startTime && row.endTime ? [this.formatTime(row.startTime), this.formatTime(row.endTime)] : [],
|
||||
recommendFlagList: this.splitValue(row.recommendFlags)
|
||||
})
|
||||
this.dialogVisible = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.courseForm && this.$refs.courseForm.clearValidate()
|
||||
})
|
||||
},
|
||||
goChapterContent(row) {
|
||||
const url = "/platform/learning/chapter/content?courseId=" + encodeURIComponent(row.id || "")
|
||||
if (window.commonUtil && commonUtil.pjaxPush) {
|
||||
commonUtil.pjaxPush(url)
|
||||
} else {
|
||||
window.location.href = url
|
||||
}
|
||||
},
|
||||
operation() {
|
||||
this.$refs.courseForm.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
if (this.formData.openType !== "long_term" && (!this.formData.openTime || this.formData.openTime.length !== 2)) {
|
||||
this.$message.warning("请选择开课时间")
|
||||
return
|
||||
}
|
||||
const data = Object.assign({}, this.formData)
|
||||
data.startTime = data.openType === "long_term" ? "" : data.openTime[0]
|
||||
data.endTime = data.openType === "long_term" ? "" : data.openTime[1]
|
||||
data.recommendFlags = (data.recommendFlagList || []).join(",")
|
||||
delete data.openTime
|
||||
delete data.recommendFlagList
|
||||
const method = data.id ? "/doEdit" : "/doAdd"
|
||||
this.subDis = true
|
||||
const resp = await this.$axios.post(loc() + method, data)
|
||||
this.subDis = false
|
||||
if (resp.code === 0) {
|
||||
this.dialogVisible = false
|
||||
this.$message.success(resp.msg)
|
||||
this.pageData()
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
},
|
||||
doDelete(row) {
|
||||
this.$confirm("确定要删除该课程吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(async () => {
|
||||
const resp = await this.$axios.post(loc() + "/doDelete", { id: row.id })
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.pageData()
|
||||
} else {
|
||||
this.$message.error(resp.msg)
|
||||
}
|
||||
})
|
||||
},
|
||||
async getCourseTypes() {
|
||||
const resp = await this.$axios.post(loc() + "/courseTypes")
|
||||
if (resp.code === 0) {
|
||||
this.courseTypeOptions = resp.data
|
||||
}
|
||||
},
|
||||
async getLearningDict(name) {
|
||||
const resp = await this.$axios.post(loc() + "/learningDictOptions", { name })
|
||||
return resp.code === 0 ? resp.data : []
|
||||
},
|
||||
splitValue(value) {
|
||||
return value ? value.split(",").filter(Boolean) : []
|
||||
},
|
||||
getStatusName(code) {
|
||||
const item = this.statusOptions.find(v => v.code === code)
|
||||
return item ? item.name : ""
|
||||
},
|
||||
getStatusType(code) {
|
||||
const map = {
|
||||
draft: "info",
|
||||
pending: "warning",
|
||||
published: "success",
|
||||
offline: "danger"
|
||||
}
|
||||
return map[code] || ""
|
||||
},
|
||||
getDictName(code, options) {
|
||||
const item = options.find(v => v.code === code)
|
||||
return item ? item.name : code
|
||||
},
|
||||
getDictNames(value, options) {
|
||||
const values = this.splitValue(value)
|
||||
return values.map(code => {
|
||||
const item = options.find(v => v.code === code)
|
||||
return item ? item.name : code
|
||||
}).join("、")
|
||||
},
|
||||
getRecommendType(code) {
|
||||
const name = this.getDictName(code, this.recommendOptions)
|
||||
if (name.includes("热门") || name.includes("推荐")) return "warning"
|
||||
if (name.includes("新")) return "success"
|
||||
return ""
|
||||
},
|
||||
formatTime(time) {
|
||||
return time ? this.$moment(time).format("YYYY-MM-DD HH:mm:ss") : ""
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.pageForm.courseTime = []
|
||||
await this.getCourseTypes()
|
||||
this.recommendOptions = await this.getLearningDict("推荐标识")
|
||||
this.pageData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,189 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<el-card shadow="never">
|
||||
<search :is-search-button="false">
|
||||
<search-item label="课程类型名称">
|
||||
<el-input
|
||||
v-model="pageForm.typeName"
|
||||
clearable
|
||||
placeholder="请输入课程类型名称"
|
||||
style="width: 100%"
|
||||
@keyup.enter.native="doSearch">
|
||||
</el-input>
|
||||
</search-item>
|
||||
<div class="search-query">
|
||||
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
|
||||
<el-button size="medium" @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mt10" shadow="never">
|
||||
<table-tool :app="this" label="课程类型列表">
|
||||
<el-button @click="openAdd" size="medium" type="primary">
|
||||
<i class="el-icon-plus"></i>
|
||||
新建
|
||||
</el-button>
|
||||
</table-tool>
|
||||
|
||||
<el-table
|
||||
v-loading="tableLoading"
|
||||
:data="tableData"
|
||||
:size="tableSize"
|
||||
border>
|
||||
<el-table-column :index="indexMethod" align="center" header-align="center" label="序号" type="index" width="80"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="课程类型名称" prop="typeName" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="备注" prop="remark" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="排序编号" prop="sortNum" width="140"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="是否启用" prop="enabled" width="140">
|
||||
<template slot-scope="{row}">
|
||||
<el-switch
|
||||
v-model="row.enabled"
|
||||
active-color="#13ce66"
|
||||
inactive-color="#ff4949"
|
||||
@change="switchChange(row)">
|
||||
</el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="操作" width="180">
|
||||
<template slot-scope="{row}">
|
||||
<el-button @click="openEdit(row)" size="mini" type="primary">编辑</el-button>
|
||||
<el-button @click="doDelete(row)" size="mini" type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
</guava>
|
||||
|
||||
<el-dialog :title="title" :visible.sync="dialogVisible" :close-on-click-modal="false" width="40%">
|
||||
<el-form :model="formData" ref="courseTypeForm" :rules="formRules" label-width="120px">
|
||||
<el-form-item label="课程类型名称" prop="typeName">
|
||||
<el-input v-model="formData.typeName" maxlength="100" placeholder="请输入课程类型名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序编号" prop="sortNum">
|
||||
<el-input-number
|
||||
v-model="formData.sortNum"
|
||||
:controls="false"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
placeholder="请输入排序编号"
|
||||
style="width: 100%">
|
||||
</el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否启用" prop="enabled">
|
||||
<el-switch v-model="formData.enabled" active-color="#13ce66" inactive-color="#ff4949"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="formData.remark"
|
||||
type="textarea"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
:rows="4"
|
||||
placeholder="请输入备注">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="subDis" @click="operation">确定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
return {
|
||||
title: "",
|
||||
dialogVisible: false,
|
||||
subDis: false,
|
||||
formData: {},
|
||||
formRules: {
|
||||
typeName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
sortNum: [{ required: true, message: "必填", trigger: ["blur", "change"] }]
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetSearch() {
|
||||
this.pageForm.typeName = ""
|
||||
this.doSearch()
|
||||
},
|
||||
openAdd() {
|
||||
this.title = "新建课程类型"
|
||||
this.formData = {
|
||||
typeName: "",
|
||||
sortNum: 0,
|
||||
enabled: true,
|
||||
remark: ""
|
||||
}
|
||||
this.dialogVisible = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.courseTypeForm && this.$refs.courseTypeForm.clearValidate()
|
||||
})
|
||||
},
|
||||
openEdit(row) {
|
||||
this.title = "编辑课程类型"
|
||||
this.formData = Object.assign({}, row)
|
||||
this.dialogVisible = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.courseTypeForm && this.$refs.courseTypeForm.clearValidate()
|
||||
})
|
||||
},
|
||||
operation() {
|
||||
const method = this.formData.id ? "/doEdit" : "/doAdd"
|
||||
this.$refs.courseTypeForm.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
this.subDis = true
|
||||
const resp = await this.$axios.post(loc() + method, this.formData)
|
||||
this.subDis = false
|
||||
if (resp.code === 0) {
|
||||
this.dialogVisible = false
|
||||
this.$message.success(resp.msg)
|
||||
this.pageData()
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
},
|
||||
async switchChange(row) {
|
||||
const resp = await this.$axios.post(loc() + "/doEdit", row)
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.pageData()
|
||||
} else {
|
||||
this.$message.error(resp.msg)
|
||||
row.enabled = !row.enabled
|
||||
}
|
||||
},
|
||||
doDelete(row) {
|
||||
this.$confirm("确定要删除该课程类型吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(async () => {
|
||||
const resp = await this.$axios.post(loc() + "/doDelete", { id: row.id })
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.pageData()
|
||||
} else {
|
||||
this.$message.error(resp.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.pageData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,180 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<el-card shadow="never">
|
||||
<search :is-search-button="false">
|
||||
<search-item label="工号/姓名">
|
||||
<el-input
|
||||
v-model="pageForm.keyword"
|
||||
clearable
|
||||
placeholder="请输入工号或姓名"
|
||||
style="width: 100%"
|
||||
@keyup.enter.native="doSearch">
|
||||
</el-input>
|
||||
</search-item>
|
||||
<search-item label="所属分工会" v-if="unionOptions.length">
|
||||
<el-select v-model="pageForm.unionId" clearable filterable placeholder="请选择所属分工会" style="width: 100%">
|
||||
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="课程名称">
|
||||
<el-select v-model="pageForm.courseId" clearable filterable placeholder="请选择课程" style="width: 100%">
|
||||
<el-option v-for="item in courseOptions" :key="item.id" :label="item.courseName" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="是否完成">
|
||||
<el-select v-model="pageForm.completeStatus" clearable placeholder="请选择完成状态" style="width: 100%">
|
||||
<el-option v-for="item in statusOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<div class="search-query">
|
||||
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
|
||||
<el-button size="medium" @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mt10" shadow="never">
|
||||
<table-tool :app="this" label="学习列表"></table-tool>
|
||||
<el-table
|
||||
v-loading="tableLoading"
|
||||
:data="tableData"
|
||||
:size="tableSize"
|
||||
border
|
||||
@sort-change="pageOrder">
|
||||
<el-table-column :index="indexMethod" align="center" header-align="center" label="序号" type="index" width="70"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="工号" prop="loginName" sortable="custom" show-overflow-tooltip width="120"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="姓名" prop="userName" sortable="custom" show-overflow-tooltip width="110"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="所属分工会" prop="unionName" sortable="custom" show-overflow-tooltip min-width="150"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="课程名称" prop="courseName" sortable="custom" show-overflow-tooltip min-width="180"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="章节名称" prop="outlineName" sortable="custom" show-overflow-tooltip min-width="160"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="学习开始时间" prop="firstStudyTime" sortable="custom" width="170">
|
||||
<template slot-scope="{row}">{{formatTime(row.firstStudyTime)}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="最近学习时间" prop="latestStudyTime" sortable="custom" width="170">
|
||||
<template slot-scope="{row}">{{formatTime(row.latestStudyTime)}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="学习时长" prop="studySeconds" sortable="custom" width="130">
|
||||
<template slot-scope="{row}">{{row.studyTimeText}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="学习进度" prop="progressPercent" sortable="custom" width="180">
|
||||
<template slot-scope="{row}">
|
||||
<el-progress :percentage="Number(row.progressPercent || 0)" :stroke-width="8"></el-progress>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="是否完成" prop="completeStatus" sortable="custom" width="110">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag :type="statusType(row.completeStatus)" size="mini">{{statusName(row.completeStatus)}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="操作" width="100">
|
||||
<template slot-scope="{row}">
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteRecord(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
</guava>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
return {
|
||||
apiBase: "/platform/learning/study/record",
|
||||
courseOptions: [],
|
||||
unionOptions: [],
|
||||
statusOptions: [
|
||||
{ name: "未开始", code: "not_started", type: "info" },
|
||||
{ name: "学习中", code: "studying", type: "warning" },
|
||||
{ name: "已完成", code: "completed", type: "success" }
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
pageData() {
|
||||
this.tableLoading = true
|
||||
this.$axios.post(this.apiBase + "/pageData", this.pageForm).then(resp => {
|
||||
this.tableLoading = false
|
||||
if (resp.code === 0) {
|
||||
this.tableData = resp.data.list || []
|
||||
this.pageForm.totalCount = resp.data.totalCount
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
}).catch(() => {
|
||||
this.tableLoading = false
|
||||
})
|
||||
},
|
||||
pageOrder(column) {
|
||||
this.pageForm.pageOrderName = column.prop
|
||||
this.pageForm.pageOrderBy = column.order
|
||||
this.pageData()
|
||||
},
|
||||
doSearch() {
|
||||
this.pageForm.pageNumber = 1
|
||||
this.pageData()
|
||||
},
|
||||
resetSearch() {
|
||||
this.pageForm.keyword = ""
|
||||
this.pageForm.unionId = ""
|
||||
this.pageForm.courseId = ""
|
||||
this.pageForm.completeStatus = ""
|
||||
this.pageForm.pageNumber = 1
|
||||
this.pageData()
|
||||
},
|
||||
async loadOptions() {
|
||||
const courseResp = await this.$axios.post(this.apiBase + "/courseOptions")
|
||||
if (courseResp.code === 0) {
|
||||
this.courseOptions = courseResp.data || []
|
||||
}
|
||||
const unionResp = await this.$axios.post(this.apiBase + "/unionOptions")
|
||||
if (unionResp.code === 0) {
|
||||
this.unionOptions = unionResp.data || []
|
||||
}
|
||||
},
|
||||
statusName(code) {
|
||||
const item = this.statusOptions.find(v => v.code === code)
|
||||
return item ? item.name : "未开始"
|
||||
},
|
||||
statusType(code) {
|
||||
const item = this.statusOptions.find(v => v.code === code)
|
||||
return item ? item.type : "info"
|
||||
},
|
||||
formatTime(value) {
|
||||
return value ? this.$moment(value).format("YYYY-MM-DD HH:mm:ss") : ""
|
||||
},
|
||||
deleteRecord(row) {
|
||||
this.$confirm("确定要删除该学习记录吗?删除后该章节的学习时段也会一并清理。", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(async () => {
|
||||
const resp = await this.$axios.post(this.apiBase + "/delete", { id: row.id })
|
||||
if (resp.code === 0) {
|
||||
this.$message.success(resp.msg)
|
||||
this.pageData()
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.$set(this.pageForm, "keyword", "")
|
||||
this.$set(this.pageForm, "unionId", "")
|
||||
this.$set(this.pageForm, "courseId", "")
|
||||
this.$set(this.pageForm, "completeStatus", "")
|
||||
await this.loadOptions()
|
||||
this.pageData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,167 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<el-card shadow="never">
|
||||
<search :is-search-button="false">
|
||||
<search-item label="课程名称">
|
||||
<el-select
|
||||
v-model="pageForm.courseId"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
:remote-method="queryCourseOptions"
|
||||
placeholder="请选择课程"
|
||||
style="width: 100%">
|
||||
<el-option v-for="item in courseOptions" :key="item.id" :label="item.courseName" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="所属分工会" v-if="unionOptions.length">
|
||||
<el-select v-model="pageForm.unionId" clearable filterable placeholder="请选择所属分工会" style="width: 100%">
|
||||
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<div class="search-query">
|
||||
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
|
||||
<el-button size="medium" @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mt10" shadow="never">
|
||||
<el-tabs v-model="activeTab" @tab-click="tabChange">
|
||||
<el-tab-pane label="课程统计" name="course"></el-tab-pane>
|
||||
<el-tab-pane label="分工会统计" name="union"></el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<table-tool :app="this" :label="activeTab === 'course' ? '课程统计' : '分工会统计'"></table-tool>
|
||||
<el-table
|
||||
v-if="activeTab === 'course'"
|
||||
v-loading="tableLoading"
|
||||
:data="tableData"
|
||||
:size="tableSize"
|
||||
border
|
||||
@sort-change="pageOrder">
|
||||
<el-table-column :index="indexMethod" align="center" header-align="center" label="序号" type="index" width="70"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="课程名称" prop="courseName" sortable="custom" show-overflow-tooltip min-width="220" width="360"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="学习人数" prop="learnerCount" sortable="custom" width="130"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="完成人数" prop="completedCount" sortable="custom" width="130"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="平均学习时长" prop="avgStudySeconds" sortable="custom" width="170">
|
||||
<template slot-scope="{row}">{{row.avgStudyTimeText}}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-table
|
||||
v-else
|
||||
v-loading="tableLoading"
|
||||
:data="tableData"
|
||||
:size="tableSize"
|
||||
border
|
||||
@sort-change="pageOrder">
|
||||
<el-table-column :index="indexMethod" align="center" header-align="center" label="序号" type="index" width="70"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="课程名称" prop="courseName" sortable="custom" show-overflow-tooltip min-width="220"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="分工会名称" prop="unionName" sortable="custom" show-overflow-tooltip min-width="170"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="学习人数" prop="learnerCount" sortable="custom" width="130"></el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="学习时长" prop="studySeconds" sortable="custom" width="160">
|
||||
<template slot-scope="{row}">{{row.studyTimeText}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" header-align="center" label="完成率" prop="completeRate" sortable="custom" width="130"></el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
</guava>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
return {
|
||||
apiBase: "/platform/learning/statistics",
|
||||
activeTab: "course",
|
||||
courseOptions: [],
|
||||
unionOptions: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
pageData() {
|
||||
this.tableLoading = true
|
||||
const url = this.activeTab === "course" ? "/coursePageData" : "/unionPageData"
|
||||
this.$axios.post(this.apiBase + url, this.pageForm).then(resp => {
|
||||
this.tableLoading = false
|
||||
if (resp.code === 0) {
|
||||
this.tableData = resp.data.list || []
|
||||
this.pageForm.totalCount = resp.data.totalCount
|
||||
} else {
|
||||
this.$message.warning(resp.msg)
|
||||
}
|
||||
}).catch(() => {
|
||||
this.tableLoading = false
|
||||
})
|
||||
},
|
||||
pageOrder(column) {
|
||||
this.pageForm.pageOrderName = column.prop
|
||||
this.pageForm.pageOrderBy = column.order
|
||||
this.pageData()
|
||||
},
|
||||
async doSearch() {
|
||||
this.pageForm.pageNumber = 1
|
||||
await this.ensureUnionDefaultCourse()
|
||||
this.pageData()
|
||||
},
|
||||
async resetSearch() {
|
||||
this.pageForm.courseId = ""
|
||||
this.pageForm.unionId = ""
|
||||
this.pageForm.pageOrderName = ""
|
||||
this.pageForm.pageOrderBy = ""
|
||||
this.pageForm.pageNumber = 1
|
||||
await this.queryCourseOptions("")
|
||||
await this.ensureUnionDefaultCourse()
|
||||
this.pageData()
|
||||
},
|
||||
async tabChange() {
|
||||
this.tableData = []
|
||||
this.pageForm.pageNumber = 1
|
||||
this.pageForm.pageOrderName = ""
|
||||
this.pageForm.pageOrderBy = ""
|
||||
await this.ensureUnionDefaultCourse()
|
||||
this.pageData()
|
||||
},
|
||||
async queryCourseOptions(query) {
|
||||
const resp = await this.$axios.post(this.apiBase + "/courseOptions", { courseName: query || "" })
|
||||
if (resp.code === 0) {
|
||||
this.courseOptions = resp.data || []
|
||||
}
|
||||
},
|
||||
async ensureUnionDefaultCourse() {
|
||||
if (this.activeTab !== "union" || this.pageForm.courseId) {
|
||||
return
|
||||
}
|
||||
if (!this.courseOptions.length) {
|
||||
await this.queryCourseOptions("")
|
||||
}
|
||||
if (this.courseOptions.length) {
|
||||
this.pageForm.courseId = this.courseOptions[0].id
|
||||
}
|
||||
},
|
||||
async loadUnionOptions() {
|
||||
const resp = await this.$axios.post(this.apiBase + "/unionOptions")
|
||||
if (resp.code === 0) {
|
||||
this.unionOptions = resp.data || []
|
||||
}
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.$set(this.pageForm, "courseId", "")
|
||||
this.$set(this.pageForm, "unionId", "")
|
||||
await this.queryCourseOptions("")
|
||||
await this.loadUnionOptions()
|
||||
this.pageData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,353 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<el-card shadow="never">
|
||||
<search :is-search-button="false">
|
||||
<search-item label="年度">
|
||||
<el-date-picker
|
||||
v-model="pageForm.year"
|
||||
type="year"
|
||||
value-format="yyyy"
|
||||
placeholder="请选择年度"
|
||||
style="width: 100%">
|
||||
</el-date-picker>
|
||||
</search-item>
|
||||
<search-item label="线路名称">
|
||||
<el-input
|
||||
v-model="pageForm.lineName"
|
||||
clearable
|
||||
placeholder="请输入线路名称"
|
||||
style="width: 100%"
|
||||
@keyup.enter.native="doSearch">
|
||||
</el-input>
|
||||
</search-item>
|
||||
<search-item label="线路类型">
|
||||
<el-select v-model="pageForm.lineType" clearable filterable placeholder="请选择线路类型" style="width: 100%">
|
||||
<el-option v-for="item in lineTypeOptions" :key="item.lineType" :label="item.lineType" :value="item.lineType"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="所属工会">
|
||||
<el-select v-model="pageForm.unionId" clearable filterable placeholder="请选择所属工会" style="width: 100%">
|
||||
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<div class="search-query" style="margin-left: auto;">
|
||||
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
|
||||
<el-button size="medium" @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mt10" shadow="never">
|
||||
<table-tool :app="this" label="线路列表"></table-tool>
|
||||
|
||||
<el-table
|
||||
v-loading="tableLoading"
|
||||
:data="tableData"
|
||||
:size="tableSize"
|
||||
border
|
||||
:default-sort="{prop: 'lineName', order: 'ascending'}"
|
||||
:row-class-name="tableRowClassName"
|
||||
@sort-change="pageOrder">
|
||||
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="线路名称" prop="lineName" min-width="220" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="出行时段" prop="travelPeriod" width="260" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="事项名称" prop="matterName" min-width="200" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="所属工会" prop="unionName" min-width="180" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="线路类型" prop="lineType" width="140" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="最少成团人数" prop="minGroupPeople" width="140" sortable="custom" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
{{ row.minGroupPeople || 0 }}人
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最多成团人数" prop="maxGroupPeople" width="140" sortable="custom" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
{{ row.maxGroupPeople || 0 }}人
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="报名人数" prop="signupCount" width="120" sortable="custom" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag size="mini" :type="Number(row.signupCount || 0) > 0 ? 'success' : 'info'">{{ row.signupCount || 0 }}人</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="成团状态" prop="groupStatusName" width="120" sortable="custom" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag size="mini" :type="groupStatusType(row.groupStatus)">{{ row.groupStatusName || '未成团' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" align="center" header-align="center" fixed="right">
|
||||
<template slot-scope="{row}">
|
||||
<el-button size="mini" type="primary" @click="openView(row)">查看</el-button>
|
||||
<el-button size="mini" type="primary" @click="exportLine(row)">导出</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
|
||||
<el-drawer
|
||||
:title="viewTitle"
|
||||
:visible.sync="viewVisible"
|
||||
direction="rtl"
|
||||
size="72%"
|
||||
custom-class="tour-group-view-drawer"
|
||||
:close-on-click-modal="false">
|
||||
<div class="tour-group-view">
|
||||
<el-card shadow="never">
|
||||
<search :is-search-button="false">
|
||||
<search-item label="姓名/工号">
|
||||
<el-input
|
||||
v-model="viewForm.keyword"
|
||||
clearable
|
||||
placeholder="请输入姓名或工号"
|
||||
style="width: 100%"
|
||||
@keyup.enter.native="viewSearch">
|
||||
</el-input>
|
||||
</search-item>
|
||||
<search-item label="所属工会">
|
||||
<el-select v-model="viewForm.unionId" clearable filterable placeholder="请选择所属工会" style="width: 100%">
|
||||
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<div class="search-query" style="margin-left: auto;">
|
||||
<el-button size="medium" type="primary" icon="el-icon-search" @click="viewSearch">搜索</el-button>
|
||||
<el-button size="medium" @click="viewReset">重置</el-button>
|
||||
</div>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mt10" shadow="never">
|
||||
<div class="tour-group-view-title">报名人员列表</div>
|
||||
<el-table
|
||||
v-loading="viewLoading"
|
||||
:data="viewTableData"
|
||||
:size="tableSize"
|
||||
border
|
||||
@sort-change="viewPageOrder">
|
||||
<el-table-column label="序号" type="index" :index="viewIndexMethod" width="80" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="工号" prop="jobNo" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="姓名" prop="userName" width="120" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="手机号码" prop="mobile" width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="身份证号" prop="idCard" min-width="190" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="所属工会" prop="unionName" min-width="180" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
</el-table>
|
||||
<el-row class="el-pagination-container tour-group-view-pagination">
|
||||
<el-pagination
|
||||
background
|
||||
:current-page="viewForm.pageNumber"
|
||||
:page-size="viewForm.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="viewForm.totalCount"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="viewSizeChange"
|
||||
@current-change="viewCurrentChange">
|
||||
</el-pagination>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</guava>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
const currentYear = moment().format("YYYY")
|
||||
return {
|
||||
unionOptions: [],
|
||||
lineTypeOptions: [],
|
||||
viewVisible: false,
|
||||
viewLoading: false,
|
||||
viewRequestSeq: 0,
|
||||
viewTitle: "",
|
||||
viewRow: {},
|
||||
viewTableData: [],
|
||||
pageForm: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
totalCount: 0,
|
||||
pageOrderName: "lineName",
|
||||
pageOrderBy: "ascending",
|
||||
year: currentYear,
|
||||
lineName: "",
|
||||
lineType: "",
|
||||
unionId: ""
|
||||
},
|
||||
viewForm: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
totalCount: 0,
|
||||
pageOrderName: "signupTime",
|
||||
pageOrderBy: "descending",
|
||||
keyword: "",
|
||||
unionId: ""
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetSearch() {
|
||||
this.pageForm.year = moment().format("YYYY")
|
||||
this.pageForm.lineName = ""
|
||||
this.pageForm.lineType = ""
|
||||
this.pageForm.unionId = ""
|
||||
this.loadLineTypeOptions()
|
||||
this.doSearch()
|
||||
},
|
||||
groupStatusType(status) {
|
||||
if (status === "formed") return "success"
|
||||
if (status === "over") return "danger"
|
||||
return "info"
|
||||
},
|
||||
tableRowClassName({ row }) {
|
||||
return this.isDirectFamilyLine(row) ? "direct-family-row" : ""
|
||||
},
|
||||
isDirectFamilyLine(row) {
|
||||
if (!row) return false
|
||||
return row.directFamilyUnitLine === true
|
||||
|| row.directFamilyUnitLine === 1
|
||||
|| row.directFamilyUnitLine === "1"
|
||||
},
|
||||
openView(row) {
|
||||
this.viewRow = row || {}
|
||||
this.viewTitle = this.viewRow.lineName || "报名人员"
|
||||
this.viewVisible = true
|
||||
this.viewForm.pageNumber = 1
|
||||
this.viewForm.pageSize = 10
|
||||
this.viewForm.totalCount = 0
|
||||
this.viewForm.pageOrderName = "signupTime"
|
||||
this.viewForm.pageOrderBy = "descending"
|
||||
this.viewForm.keyword = ""
|
||||
this.viewForm.unionId = ""
|
||||
this.viewTableData = []
|
||||
this.loadViewData()
|
||||
},
|
||||
viewSearch() {
|
||||
this.viewForm.pageNumber = 1
|
||||
this.loadViewData()
|
||||
},
|
||||
viewReset() {
|
||||
this.viewForm.keyword = ""
|
||||
this.viewForm.unionId = ""
|
||||
this.viewForm.pageNumber = 1
|
||||
this.loadViewData()
|
||||
},
|
||||
viewIndexMethod(index) {
|
||||
return (this.viewForm.pageNumber - 1) * this.viewForm.pageSize + index + 1
|
||||
},
|
||||
viewSizeChange(size) {
|
||||
this.viewForm.pageSize = size
|
||||
this.viewForm.pageNumber = 1
|
||||
this.loadViewData()
|
||||
},
|
||||
viewCurrentChange(page) {
|
||||
this.viewForm.pageNumber = page
|
||||
this.loadViewData()
|
||||
},
|
||||
viewPageOrder({ prop, order }) {
|
||||
this.viewForm.pageOrderName = prop || "signupTime"
|
||||
this.viewForm.pageOrderBy = order || "descending"
|
||||
this.viewForm.pageNumber = 1
|
||||
this.loadViewData()
|
||||
},
|
||||
loadViewData() {
|
||||
if (!this.viewRow || !this.viewRow.matterId) {
|
||||
this.viewTableData = []
|
||||
this.viewForm.totalCount = 0
|
||||
return
|
||||
}
|
||||
this.viewLoading = true
|
||||
const requestSeq = ++this.viewRequestSeq
|
||||
this.$axios.post(loc() + "/signupPageData", {
|
||||
matterId: this.viewRow.matterId,
|
||||
keyword: this.viewForm.keyword,
|
||||
unionId: this.viewForm.unionId,
|
||||
pageNumber: this.viewForm.pageNumber,
|
||||
pageSize: this.viewForm.pageSize,
|
||||
pageOrderName: this.viewForm.pageOrderName,
|
||||
pageOrderBy: this.viewForm.pageOrderBy
|
||||
}).then((res) => {
|
||||
if (requestSeq !== this.viewRequestSeq) return
|
||||
if (res.code === 0) {
|
||||
const data = res.data || {}
|
||||
this.viewTableData = data.list || []
|
||||
this.viewForm.totalCount = data.totalCount || 0
|
||||
} else {
|
||||
this.$message.warning(res.msg || "查询失败")
|
||||
}
|
||||
}).finally(() => {
|
||||
if (requestSeq === this.viewRequestSeq) {
|
||||
this.viewLoading = false
|
||||
}
|
||||
})
|
||||
},
|
||||
exportLine(row) {
|
||||
if (!row || !row.matterId) {
|
||||
this.$message.warning("线路信息不完整")
|
||||
return
|
||||
}
|
||||
window.location.href = loc() + "/exportParticipants?matterId=" + encodeURIComponent(row.matterId)
|
||||
},
|
||||
loadUnionOptions() {
|
||||
this.$axios.post(loc() + "/unionOptions").then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.unionOptions = res.data || []
|
||||
}
|
||||
})
|
||||
},
|
||||
loadLineTypeOptions() {
|
||||
this.$axios.post(loc() + "/lineTypeOptions", {
|
||||
year: this.pageForm.year
|
||||
}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.lineTypeOptions = (res.data || []).filter(item => item.lineType)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadUnionOptions()
|
||||
this.loadLineTypeOptions()
|
||||
this.pageData()
|
||||
},
|
||||
watch: {
|
||||
"pageForm.year"() {
|
||||
this.pageForm.lineType = ""
|
||||
this.loadLineTypeOptions()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.tour-group-view-drawer .el-drawer__body {
|
||||
background: #f5f7fa;
|
||||
padding: 12px;
|
||||
overflow: auto;
|
||||
}
|
||||
.tour-group-view-pagination {
|
||||
margin-top: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
.tour-group-view-title {
|
||||
border-left: 4px solid #0079c2;
|
||||
color: #0079c2;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
margin-bottom: 12px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
.el-table .direct-family-row > td,
|
||||
.el-table .direct-family-row > td .cell {
|
||||
color: #f56c6c;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,653 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<el-card shadow="never">
|
||||
<search :is-search-button="false">
|
||||
<search-item label="开始年度">
|
||||
<el-date-picker
|
||||
v-model="pageForm.startYear"
|
||||
type="year"
|
||||
value-format="yyyy"
|
||||
placeholder="请选择开始年度"
|
||||
style="width: 100%">
|
||||
</el-date-picker>
|
||||
</search-item>
|
||||
<search-item label="结束年度">
|
||||
<el-date-picker
|
||||
v-model="pageForm.endYear"
|
||||
type="year"
|
||||
value-format="yyyy"
|
||||
placeholder="请选择结束年度"
|
||||
style="width: 100%">
|
||||
</el-date-picker>
|
||||
</search-item>
|
||||
<search-item label="姓名/工号">
|
||||
<el-input
|
||||
v-model="pageForm.keyword"
|
||||
clearable
|
||||
placeholder="请输入姓名或工号"
|
||||
style="width: 100%"
|
||||
@keyup.enter.native="doSearch">
|
||||
</el-input>
|
||||
</search-item>
|
||||
<search-item label="所属工会">
|
||||
<el-select v-model="pageForm.unionId" clearable filterable placeholder="请选择所属工会" style="width: 100%">
|
||||
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="线路">
|
||||
<el-select v-model="pageForm.lineId" clearable filterable placeholder="请选择线路" style="width: 100%">
|
||||
<el-option v-for="item in lineOptions" :key="item.lineId" :label="item.lineName" :value="item.lineId"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="出行时段">
|
||||
<el-select v-model="pageForm.travelPeriod" clearable filterable placeholder="请选择出行时段" style="width: 100%">
|
||||
<el-option v-for="item in travelPeriodOptions" :key="item.travelPeriod" :label="item.travelPeriod" :value="item.travelPeriod"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="线路类型">
|
||||
<dict-select
|
||||
v-model="pageForm.lineType"
|
||||
code="lineType"
|
||||
option_value="name"
|
||||
placeholder="请选择线路类型">
|
||||
</dict-select>
|
||||
</search-item>
|
||||
<div class="search-query" style="margin-left: auto;">
|
||||
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
|
||||
<el-button size="medium" @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mt10" shadow="never">
|
||||
<div class="tour-ledger-toolbar">
|
||||
<div class="tour-ledger-toolbar-title">
|
||||
<table-tool :app="this" label="台账列表"></table-tool>
|
||||
</div>
|
||||
<div class="tour-ledger-actions">
|
||||
<el-button size="medium" type="primary" icon="el-icon-upload2" @click="showImportDialog = true">参加人员导入</el-button>
|
||||
<el-button size="medium" type="primary" icon="el-icon-check" @click="setParticipants">设置参加人员</el-button>
|
||||
<el-button size="medium" type="primary" icon="el-icon-download" @click="exportUnionSignupZip">导出分工会报名压缩包</el-button>
|
||||
</div>
|
||||
<div class="tour-ledger-scope">
|
||||
<el-button
|
||||
class="tour-scope-btn"
|
||||
size="medium"
|
||||
:class="{'is-active': pageForm.directFamilyOnly}"
|
||||
@click="setDirectFamilyOnly">
|
||||
参加直系亲属单位线路人员
|
||||
</el-button>
|
||||
<el-button
|
||||
class="tour-scope-btn"
|
||||
size="medium"
|
||||
:class="{'is-active': pageForm.overCostOnly}"
|
||||
@click="setOverCostOnly">
|
||||
申请超出费用由单位承担人员
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
v-loading="tableLoading"
|
||||
:data="tableData"
|
||||
:size="tableSize"
|
||||
border
|
||||
row-key="id"
|
||||
:row-class-name="tableRowClassName"
|
||||
@selection-change="handleSelectionChange"
|
||||
@sort-change="pageOrder">
|
||||
<el-table-column type="selection" width="55" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="工号" prop="jobNo" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="姓名" prop="userName" width="120" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="所在单位" prop="unitName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="报名线路" prop="lineName" min-width="220" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="出行时段" prop="travelPeriod" min-width="260" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="线路类型" prop="lineType" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="报名时间" prop="signupTime" width="170" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="携带家属" prop="familyCount" width="120" sortable="custom" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag size="mini" :type="Number(row.familyCount || 0) > 0 ? 'success' : 'info'">{{ row.familyCount || 0 }}人</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="是否参加" prop="joined" width="120" sortable="custom" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag size="mini" :type="row.joined ? 'success' : 'info'">{{ row.joined ? '是' : '否' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="报销超出费用" prop="overCostReimbursed" width="140" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag size="mini" :type="row.overCostReimbursed ? 'success' : 'info'">{{ row.overCostReimbursed ? '是' : '否' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" align="center" header-align="center" fixed="right">
|
||||
<template slot-scope="{row}">
|
||||
<el-button size="mini" type="primary" @click="openView(row)">查看</el-button>
|
||||
<el-button size="mini" type="danger" @click="doDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
<div class="tour-ledger-tip">温馨提醒:走审核流程的报名,审核通过后才在台账中显示</div>
|
||||
</el-card>
|
||||
</guava>
|
||||
|
||||
<excel-import
|
||||
ref="excelImportRef"
|
||||
url="/platform/tour/ledger/importParticipants"
|
||||
template_url="/platform/tour/ledger/downloadTemplate"
|
||||
:visible.sync="showImportDialog"
|
||||
title="参加人员导入"
|
||||
width="700px"
|
||||
@import-success="afterImport"
|
||||
:extra_params="{}">
|
||||
</excel-import>
|
||||
|
||||
<el-dialog
|
||||
title="台账详情"
|
||||
:visible.sync="detailVisible"
|
||||
:close-on-click-modal="false"
|
||||
width="72%">
|
||||
<div class="tour-ledger-section">
|
||||
<div class="tour-ledger-title">教职工信息</div>
|
||||
<el-descriptions :column="3" border size="medium">
|
||||
<el-descriptions-item label="工号">{{ detail.jobNo || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="姓名">{{ detail.userName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="性别">{{ detail.gender || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="身份证号">{{ detail.idCard || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所在单位">{{ detail.unitName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所在工会">{{ detail.unionName || '' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div class="tour-ledger-section mt10">
|
||||
<div class="tour-ledger-title">
|
||||
报名信息
|
||||
<el-link v-if="detailHasWorkflow()" type="primary" @click="openChart">点击查看流程图</el-link>
|
||||
</div>
|
||||
<el-descriptions :column="3" border size="medium">
|
||||
<el-descriptions-item label="报名时间">{{ detail.signupTime || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="报名线路">{{ detail.lineName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="线路类型">{{ detail.lineType || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="报名酒店">{{ detail.hotelName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="报名旅行社">{{ detail.travelAgencyName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="出行时间">{{ detail.travelPeriod || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="fillBedInfo" label="床型">{{ detail.bedType || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="fillBedInfo" label="床位信息">{{ detail.bedInfo || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="fillBedInfo" label="意向拼床人">{{ detail.intendedRoommate || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="是否携带家属">{{ familyText() }}</el-descriptions-item>
|
||||
<el-descriptions-item label="是否参加">{{ detail.joined ? '是' : '否' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="是否报销">{{ detail.reimbursed ? '是' : '否' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="报销超出费用">{{ detail.overCostReimbursed ? '是' : '否' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div v-if="isDirectFamilyLine()" class="tour-ledger-section mt10">
|
||||
<div class="tour-ledger-title">直系亲属线路</div>
|
||||
<el-descriptions :column="3" border size="medium">
|
||||
<el-descriptions-item label="亲属姓名">{{ directRelative.relativeName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所在单位">{{ directRelative.unitName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="亲属关系">{{ directRelative.relationshipName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="线路名称">{{ directRelative.lineName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="出行开始日期">{{ directRelative.travelStartTime || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="出行结束日期">{{ directRelative.travelEndTime || '' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div v-if="showFamilySection()" class="tour-ledger-section mt10">
|
||||
<div class="tour-ledger-title">家属信息</div>
|
||||
<el-table :data="familyData" border :size="tableSize" empty-text="暂无家属信息">
|
||||
<el-table-column label="序号" type="index" width="70" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="家属姓名" prop="familyName" min-width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="性别" prop="gender" width="90" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="身份证号码" prop="idCard" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="年龄" prop="age" width="90" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="关系" prop="relationship" width="110" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column v-if="fillBedInfo" label="床型" prop="bedType" width="110" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column v-if="fillBedInfo" label="床位" prop="bedInfo" min-width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column v-if="fillBedInfo" label="意向拼床人" prop="intendedRoommate" min-width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<template v-if="detailHasWorkflow()" v-for="task in doneTasks">
|
||||
<div class="tour-ledger-section mt10" :key="task.id">
|
||||
<div class="tour-ledger-title">{{ task.displayName }}</div>
|
||||
<el-descriptions border :column="3" v-if="task.ext && task.ext.isFirstTaskNode">
|
||||
<el-descriptions-item label="申请用户">
|
||||
{{ task.ext.initiatorName }}({{ task.ext.initiatorAccount }})
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="申请时间">{{ task.finishTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="办理结果">
|
||||
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-descriptions border :column="3" v-else>
|
||||
<el-descriptions-item label="办理用户">
|
||||
{{ (task.taskFormData && task.taskFormData.userName) || '' }}({{ (task.taskFormData && task.taskFormData.loginName) || '' }})
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="办理时间">{{ task.finishTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="办理结果">
|
||||
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext && task.ext.submitType"></dict-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="办理意见" :span="3">
|
||||
{{ (task.taskFormData && (task.taskFormData.opinion || task.taskFormData.tf_opinion)) || '' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<snaker-chart ref="snakerChartRef"></snaker-chart>
|
||||
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="detailVisible = false">确定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.tour-ledger-section {
|
||||
padding: 0 2px;
|
||||
}
|
||||
.tour-ledger-title {
|
||||
border-left: 4px solid #0079c2;
|
||||
color: #0079c2;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
margin-bottom: 14px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
.tour-ledger-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: 24px;
|
||||
margin-bottom: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tour-ledger-toolbar-title {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.tour-ledger-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
gap: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tour-ledger-actions .el-button {
|
||||
margin: 0;
|
||||
}
|
||||
.tour-ledger-scope {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
flex-wrap: nowrap;
|
||||
gap: 12px;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tour-ledger-scope .el-button {
|
||||
margin: 0;
|
||||
}
|
||||
.tour-ledger-scope .el-button + .el-button {
|
||||
margin-left: 0;
|
||||
}
|
||||
.tour-ledger-scope .tour-scope-btn {
|
||||
background: #ecf5ff;
|
||||
border-color: #b3d8ff;
|
||||
border-radius: 4px;
|
||||
color: #0079c2;
|
||||
font-weight: 600;
|
||||
height: 34px;
|
||||
line-height: 1;
|
||||
padding: 8px 18px;
|
||||
}
|
||||
.tour-ledger-scope .tour-scope-btn:hover,
|
||||
.tour-ledger-scope .tour-scope-btn:focus {
|
||||
background: #d9ecff;
|
||||
border-color: #66b1ff;
|
||||
color: #006bb0;
|
||||
}
|
||||
.tour-ledger-scope .tour-scope-btn.is-active {
|
||||
background: #0079c2;
|
||||
border-color: #0079c2;
|
||||
box-shadow: 0 2px 6px rgba(0, 121, 194, 0.24);
|
||||
color: #fff;
|
||||
}
|
||||
.tour-ledger-scope .tour-scope-btn.is-active:hover,
|
||||
.tour-ledger-scope .tour-scope-btn.is-active:focus {
|
||||
background: #006bb0;
|
||||
border-color: #006bb0;
|
||||
color: #fff;
|
||||
}
|
||||
.tour-ledger-tip {
|
||||
color: #f56c6c;
|
||||
font-family: SimSun, "宋体", serif;
|
||||
font-size: 9pt;
|
||||
line-height: 1.6;
|
||||
margin-top: -4px;
|
||||
text-align: center;
|
||||
}
|
||||
.el-table .direct-family-row > td,
|
||||
.el-table .direct-family-row > td .cell {
|
||||
color: #f56c6c;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
mixins: [initTableMixins],
|
||||
dicts: ["PROCESS_TASK_SUBMIT_TYPE"],
|
||||
data() {
|
||||
const currentYear = moment().format("YYYY")
|
||||
return {
|
||||
detailVisible: false,
|
||||
unionOptions: [],
|
||||
lineOptions: [],
|
||||
travelPeriodOptions: [],
|
||||
detail: {},
|
||||
familyData: [],
|
||||
directRelative: {},
|
||||
directFamilyUnitLine: false,
|
||||
fillBedInfo: true,
|
||||
detailRow: {},
|
||||
doneTasks: [],
|
||||
showImportDialog: false,
|
||||
multipleSelection: [],
|
||||
filterOptionsTimer: null,
|
||||
pageForm: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
totalCount: 0,
|
||||
pageOrderName: "signupTime",
|
||||
pageOrderBy: "descending",
|
||||
startYear: currentYear,
|
||||
endYear: currentYear,
|
||||
keyword: "",
|
||||
unionId: "",
|
||||
lineId: "",
|
||||
travelPeriod: "",
|
||||
lineType: "",
|
||||
directFamilyOnly: false,
|
||||
overCostOnly: false
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toBoolean(value) {
|
||||
return value === true || value === 1 || value === "1" || value === "true" || value === "TRUE"
|
||||
},
|
||||
tableRowClassName({ row }) {
|
||||
return this.isDirectFamilyRow(row) ? "direct-family-row" : ""
|
||||
},
|
||||
isDirectFamilyRow(row) {
|
||||
return this.toBoolean(row && row.directFamilyUnitLine) || !!(row && row.directRelativeId)
|
||||
},
|
||||
handleSelectionChange(val) {
|
||||
this.multipleSelection = val || []
|
||||
},
|
||||
clearTableSelection() {
|
||||
this.multipleSelection = []
|
||||
if (this.$refs.tableRef) {
|
||||
this.$refs.tableRef.clearSelection()
|
||||
}
|
||||
},
|
||||
setParticipants() {
|
||||
if (!this.multipleSelection.length) {
|
||||
this.$message.warning("请选择要设置的参加人员")
|
||||
return
|
||||
}
|
||||
this.$confirm("确定将选中的" + this.multipleSelection.length + "条台账设置为已参加吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
this.$axios.post(loc() + "/setParticipants", {
|
||||
ids: JSON.stringify(this.multipleSelection.map(item => item.id))
|
||||
}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success("设置成功")
|
||||
this.clearTableSelection()
|
||||
this.pageData()
|
||||
} else {
|
||||
this.$message.warning(res.msg || "设置失败")
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
exportUnionSignupZip() {
|
||||
const params = new URLSearchParams()
|
||||
;["startYear", "endYear", "keyword", "unionId", "lineId", "travelPeriod", "lineType"].forEach(key => {
|
||||
const value = this.pageForm[key]
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
params.append(key, value)
|
||||
}
|
||||
})
|
||||
if (this.pageForm.directFamilyOnly) {
|
||||
params.append("directFamilyOnly", "true")
|
||||
}
|
||||
if (this.pageForm.overCostOnly) {
|
||||
params.append("overCostOnly", "true")
|
||||
}
|
||||
window.location.href = loc() + "/exportUnionSignupZip?" + params.toString()
|
||||
},
|
||||
afterImport() {
|
||||
this.showImportDialog = false
|
||||
this.clearTableSelection()
|
||||
this.pageData()
|
||||
this.loadFilterOptions()
|
||||
},
|
||||
isDirectFamilyLine() {
|
||||
return this.directFamilyUnitLine
|
||||
|| this.toBoolean(this.detail && this.detail.directFamilyUnitLine)
|
||||
|| !!(this.directRelative && this.directRelative.id)
|
||||
},
|
||||
detailHasWorkflow() {
|
||||
return !!(this.detailRow && this.detailRow.instanceId)
|
||||
},
|
||||
hasFamily() {
|
||||
return this.toBoolean(this.detail && this.detail.hasFamily) || this.familyData.length > 0
|
||||
},
|
||||
familyText() {
|
||||
if (this.isDirectFamilyLine()) {
|
||||
return "否"
|
||||
}
|
||||
if (!this.hasFamily()) {
|
||||
return "否"
|
||||
}
|
||||
const count = Number(this.familyData.length || 0)
|
||||
return count > 0 ? count + "人" : "是"
|
||||
},
|
||||
showFamilySection() {
|
||||
return this.hasFamily() && !this.isDirectFamilyLine()
|
||||
},
|
||||
loadDoneTasks() {
|
||||
this.$axios.post("/flow/common/doneTasks", { bizId: this.detailRow.id }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.doneTasks = res.data || []
|
||||
}
|
||||
})
|
||||
},
|
||||
openChart() {
|
||||
if (!this.detailRow.instanceProcessDefineId || !this.detailRow.instanceId) {
|
||||
this.$message.warning("暂无流程图信息")
|
||||
return
|
||||
}
|
||||
this.$refs.snakerChartRef.onOpenFull(this.detailRow.instanceProcessDefineId, this.detailRow.instanceId)
|
||||
},
|
||||
resetSearch() {
|
||||
this.pageForm.startYear = moment().format("YYYY")
|
||||
this.pageForm.endYear = moment().format("YYYY")
|
||||
this.pageForm.keyword = ""
|
||||
this.pageForm.unionId = ""
|
||||
this.pageForm.lineId = ""
|
||||
this.pageForm.travelPeriod = ""
|
||||
this.pageForm.lineType = ""
|
||||
this.pageForm.directFamilyOnly = false
|
||||
this.pageForm.overCostOnly = false
|
||||
this.doSearch()
|
||||
},
|
||||
openView(row) {
|
||||
this.detailRow = row || {}
|
||||
this.detail = {}
|
||||
this.familyData = []
|
||||
this.directRelative = {}
|
||||
this.directFamilyUnitLine = false
|
||||
this.fillBedInfo = true
|
||||
this.doneTasks = []
|
||||
this.$axios.post(loc() + "/detail", { id: row.id }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
const data = res.data || {}
|
||||
this.detail = data.ledger || {}
|
||||
this.detail.travelPeriod = data.travelPeriod || ""
|
||||
this.familyData = data.families || []
|
||||
this.directRelative = data.directRelative || {}
|
||||
this.directFamilyUnitLine = this.toBoolean(data.directFamilyUnitLine)
|
||||
this.fillBedInfo = data.fillBedInfo === undefined || data.fillBedInfo === null || this.toBoolean(data.fillBedInfo)
|
||||
this.detailVisible = true
|
||||
if (this.detailHasWorkflow()) {
|
||||
this.loadDoneTasks()
|
||||
}
|
||||
} else {
|
||||
this.$message.warning(res.msg || "查询失败")
|
||||
}
|
||||
})
|
||||
},
|
||||
doDelete(row) {
|
||||
this.$confirm("确定删除【" + row.userName + "】的报名台账吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
this.$axios.post(loc() + "/doDelete", { id: row.id }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success("删除成功")
|
||||
this.pageData()
|
||||
this.loadFilterOptions()
|
||||
} else {
|
||||
this.$message.warning(res.msg || "删除失败")
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
loadUnionOptions() {
|
||||
this.$axios.post(loc() + "/unionOptions").then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.unionOptions = res.data || []
|
||||
}
|
||||
})
|
||||
},
|
||||
loadFilterOptions() {
|
||||
this.loadLineOptions()
|
||||
this.loadTravelPeriodOptions()
|
||||
},
|
||||
scheduleFilterOptions(delay) {
|
||||
if (this.filterOptionsTimer) {
|
||||
clearTimeout(this.filterOptionsTimer)
|
||||
}
|
||||
this.filterOptionsTimer = setTimeout(() => {
|
||||
this.loadFilterOptions()
|
||||
this.filterOptionsTimer = null
|
||||
}, delay === undefined ? 80 : delay)
|
||||
},
|
||||
loadLineOptions() {
|
||||
this.$axios.post(loc() + "/lineOptions", {
|
||||
startYear: this.pageForm.startYear,
|
||||
endYear: this.pageForm.endYear,
|
||||
travelPeriod: this.pageForm.travelPeriod,
|
||||
lineType: this.pageForm.lineType,
|
||||
unionId: this.pageForm.unionId,
|
||||
keyword: this.pageForm.keyword,
|
||||
directFamilyOnly: this.pageForm.directFamilyOnly,
|
||||
overCostOnly: this.pageForm.overCostOnly
|
||||
}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.lineOptions = res.data || []
|
||||
if (this.pageForm.lineId && !this.lineOptions.some(item => item.lineId === this.pageForm.lineId)) {
|
||||
this.pageForm.lineId = ""
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
loadTravelPeriodOptions() {
|
||||
this.$axios.post(loc() + "/travelPeriodOptions", {
|
||||
startYear: this.pageForm.startYear,
|
||||
endYear: this.pageForm.endYear,
|
||||
lineId: this.pageForm.lineId,
|
||||
lineType: this.pageForm.lineType,
|
||||
unionId: this.pageForm.unionId,
|
||||
keyword: this.pageForm.keyword,
|
||||
directFamilyOnly: this.pageForm.directFamilyOnly,
|
||||
overCostOnly: this.pageForm.overCostOnly
|
||||
}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.travelPeriodOptions = res.data || []
|
||||
if (this.pageForm.travelPeriod && !this.travelPeriodOptions.some(item => item.travelPeriod === this.pageForm.travelPeriod)) {
|
||||
this.pageForm.travelPeriod = ""
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
doSearch() {
|
||||
this.tableKey = new Date().getTime()
|
||||
this.pageForm.pageNumber = 1
|
||||
this.clearTableSelection()
|
||||
this.pageData()
|
||||
this.scheduleFilterOptions(0)
|
||||
},
|
||||
setDirectFamilyOnly() {
|
||||
this.pageForm.directFamilyOnly = !this.pageForm.directFamilyOnly
|
||||
this.pageForm.pageNumber = 1
|
||||
this.clearTableSelection()
|
||||
this.pageData()
|
||||
this.scheduleFilterOptions(0)
|
||||
},
|
||||
setOverCostOnly() {
|
||||
this.pageForm.overCostOnly = !this.pageForm.overCostOnly
|
||||
this.pageForm.pageNumber = 1
|
||||
this.clearTableSelection()
|
||||
this.pageData()
|
||||
this.scheduleFilterOptions(0)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadUnionOptions()
|
||||
this.pageData()
|
||||
this.scheduleFilterOptions(150)
|
||||
},
|
||||
watch: {
|
||||
"pageForm.startYear"() {
|
||||
this.pageForm.lineId = ""
|
||||
this.pageForm.travelPeriod = ""
|
||||
this.scheduleFilterOptions()
|
||||
},
|
||||
"pageForm.endYear"() {
|
||||
this.pageForm.lineId = ""
|
||||
this.pageForm.travelPeriod = ""
|
||||
this.scheduleFilterOptions()
|
||||
},
|
||||
"pageForm.lineId"() {
|
||||
this.scheduleFilterOptions()
|
||||
},
|
||||
"pageForm.travelPeriod"() {
|
||||
this.scheduleFilterOptions()
|
||||
},
|
||||
"pageForm.lineType"() {
|
||||
this.scheduleFilterOptions()
|
||||
},
|
||||
"pageForm.unionId"() {
|
||||
this.scheduleFilterOptions()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,712 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<el-card shadow="never">
|
||||
<search :is-search-button="false">
|
||||
<search-item label="年度">
|
||||
<el-date-picker
|
||||
v-model="pageForm.year"
|
||||
type="year"
|
||||
value-format="yyyy"
|
||||
placeholder="请选择年度"
|
||||
style="width: 100%">
|
||||
</el-date-picker>
|
||||
</search-item>
|
||||
<search-item label="所属工会">
|
||||
<el-select v-model="pageForm.unionId" clearable filterable placeholder="请选择所属工会" style="width: 100%">
|
||||
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="事项名称">
|
||||
<el-input
|
||||
v-model="pageForm.matterName"
|
||||
clearable
|
||||
placeholder="请输入事项名称"
|
||||
style="width: 100%"
|
||||
@keyup.enter.native="doSearch">
|
||||
</el-input>
|
||||
</search-item>
|
||||
<search-item label="组织形式">
|
||||
<el-select v-model="pageForm.organizationType" clearable filterable placeholder="请选择组织形式" style="width: 100%">
|
||||
<el-option v-for="item in organizationTypeOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<div class="search-query" style="margin-left: auto;">
|
||||
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
|
||||
<el-button size="medium" @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mt10" shadow="never">
|
||||
<table-tool :app="this" label="事项列表">
|
||||
<el-button @click="openAdd" size="medium" type="primary">
|
||||
<i class="el-icon-plus"></i>
|
||||
新增
|
||||
</el-button>
|
||||
</table-tool>
|
||||
|
||||
<el-table
|
||||
v-loading="tableLoading"
|
||||
:data="tableData"
|
||||
:size="tableSize"
|
||||
border
|
||||
:row-class-name="tableRowClassName"
|
||||
@sort-change="pageOrder">
|
||||
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="年度" prop="year" width="110" sortable="custom" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="事项名称" prop="matterName" min-width="200" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="所属工会" prop="unionName" min-width="180" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="线路名称" prop="lineName" min-width="220" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="组织形式" prop="organizationTypeName" width="150" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="联系人" prop="contactName" width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="联系方式" prop="contactPhone" width="150" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="事项状态" prop="enabled" width="160" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-switch
|
||||
v-model="row.enabled"
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
@change="toggleEnabled(row)">
|
||||
</el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="260" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-button size="mini" type="primary" @click="openBatchForm(row)">选择线路</el-button>
|
||||
<el-button size="mini" type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button size="mini" type="danger" :loading="row.deleteLoading" @click="doDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
</guava>
|
||||
|
||||
<el-dialog
|
||||
:title="title"
|
||||
:visible.sync="dialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
width="54%"
|
||||
@closed="destroyForm">
|
||||
<el-form :model="formData" :rules="formRules" label-width="110px" ref="form" :disabled="viewMode">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="年度" prop="year">
|
||||
<el-date-picker
|
||||
v-model="formData.year"
|
||||
type="year"
|
||||
value-format="yyyy"
|
||||
placeholder="请选择年度"
|
||||
style="width: 100%"
|
||||
@change="yearChange">
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="事项状态" prop="enabled">
|
||||
<el-radio-group v-model="formData.enabled">
|
||||
<el-radio :label="true">启用</el-radio>
|
||||
<el-radio :label="false">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="事项名称" prop="matterName">
|
||||
<el-input v-model="formData.matterName" maxlength="30" show-word-limit placeholder="请输入事项名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建人">
|
||||
<el-input v-model="formData.creatorName" readonly placeholder="当前登录人"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="疗休养配置" prop="settingId">
|
||||
<el-select v-model="formData.settingId" clearable filterable placeholder="请选择配置" style="width: 100%">
|
||||
<el-option v-for="item in settingOptions" :key="item.id" :label="item.configName" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="组织形式" prop="organizationType">
|
||||
<el-select v-model="formData.organizationType" clearable filterable placeholder="请选择组织形式" style="width: 100%" @change="organizationTypeChange">
|
||||
<el-option v-for="item in organizationTypeOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属工会" prop="unionId">
|
||||
<el-select v-model="formData.unionId" clearable filterable placeholder="请选择所属工会" style="width: 100%" :disabled="isUnionDisabled()">
|
||||
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button v-if="!viewMode" type="primary" :loading="subDis" @click="doSubmit">保存</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
:title="batchTitle"
|
||||
:visible.sync="batchFormVisible"
|
||||
:close-on-click-modal="false"
|
||||
width="74%">
|
||||
<el-form :model="batchForm" :rules="batchRules" label-width="110px" ref="batchFormRef">
|
||||
<el-form-item label="线路" prop="lineId">
|
||||
<el-select v-model="batchForm.lineId" clearable filterable placeholder="请选择线路" style="width: 100%" @change="lineChange">
|
||||
<el-option v-for="item in lineOptions" :key="item.id" :label="lineOptionLabel(item)" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="报名开始时间" prop="signupStartTime">
|
||||
<el-date-picker v-model="batchForm.signupStartTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="选择时间" style="width: 100%"></el-date-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="报名结束时间" prop="signupEndTime">
|
||||
<el-date-picker v-model="batchForm.signupEndTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="选择时间" style="width: 100%"></el-date-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="出行开始时间" prop="travelStartTime">
|
||||
<el-date-picker v-model="batchForm.travelStartTime" type="date" value-format="yyyy-MM-dd" placeholder="选择日期" style="width: 100%" @change="travelStartChange"></el-date-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="出行结束时间" prop="travelEndTime">
|
||||
<el-date-picker v-model="batchForm.travelEndTime" type="date" value-format="yyyy-MM-dd" placeholder="选择日期" style="width: 100%" @change="travelEndChange"></el-date-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="联系人" prop="contactName">
|
||||
<el-input v-model="batchForm.contactName" maxlength="10" placeholder="请输入联系人"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="联系方式" prop="contactPhone">
|
||||
<el-input v-model="batchForm.contactPhone" maxlength="11" placeholder="请输入联系方式"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="预计费用" prop="estimatedCost">
|
||||
<el-input v-model="batchForm.estimatedCost" maxlength="10" placeholder="请输入预计费用"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="最少成团人数" prop="minGroupPeople">
|
||||
<el-input-number v-model="batchForm.minGroupPeople" :min="1" :precision="0" controls-position="right" style="width: 100%" @change="peopleChange"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="最多成团人数" prop="maxGroupPeople">
|
||||
<el-input-number v-model="batchForm.maxGroupPeople" :min="1" :precision="0" controls-position="right" style="width: 100%" @change="peopleChange"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="batchFormVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="batchSubDis" @click="submitBatch">确定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.el-table .direct-family-row > td,
|
||||
.el-table .direct-family-row > td .cell {
|
||||
color: #f56c6c;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
store,
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
const currentYear = moment().format("YYYY")
|
||||
const validateUnion = (rule, value, callback) => {
|
||||
if (this.formData.organizationType === "schoolUnion" || value) {
|
||||
callback()
|
||||
} else {
|
||||
callback(new Error("必填"))
|
||||
}
|
||||
}
|
||||
const validateMobile = (rule, value, callback) => {
|
||||
const mobileReg = /^1[3-9]\d{9}$/
|
||||
if (!value) {
|
||||
callback(new Error("必填"))
|
||||
} else if (!mobileReg.test(value)) {
|
||||
callback(new Error("手机号格式不正确"))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
const validateMoney = (rule, value, callback) => {
|
||||
const moneyReg = /^(0|[1-9]\d*)(\.\d{1,2})?$/
|
||||
if (value === "" || value === null || value === undefined) {
|
||||
callback(new Error("必填"))
|
||||
} else if (!moneyReg.test(String(value))) {
|
||||
callback(new Error("请输入非负金额,最多两位小数"))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
const validatePeople = (rule, value, callback) => {
|
||||
if (!value || value <= 0) {
|
||||
callback(new Error("人数必须大于0"))
|
||||
} else if (this.batchForm.minGroupPeople && this.batchForm.maxGroupPeople
|
||||
&& this.batchForm.minGroupPeople > this.batchForm.maxGroupPeople) {
|
||||
callback(new Error("最少人数不能大于最多人数"))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
const validateBatchTime = (rule, value, callback) => {
|
||||
if (!value) {
|
||||
callback(new Error("必填"))
|
||||
return
|
||||
}
|
||||
const form = this.batchForm
|
||||
if (form.signupStartTime && form.signupEndTime && form.signupStartTime >= form.signupEndTime) {
|
||||
callback(new Error("报名开始时间必须小于报名结束时间"))
|
||||
return
|
||||
}
|
||||
if (form.travelStartTime && form.travelEndTime && form.travelStartTime > form.travelEndTime) {
|
||||
callback(new Error("出行开始时间不能晚于出行结束时间"))
|
||||
return
|
||||
}
|
||||
if (form.signupEndTime && form.travelStartTime && form.signupEndTime >= form.travelStartTime) {
|
||||
callback(new Error("报名结束时间必须小于出行开始时间"))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
}
|
||||
return {
|
||||
title: "",
|
||||
dialogVisible: false,
|
||||
viewMode: false,
|
||||
subDis: false,
|
||||
batchFormVisible: false,
|
||||
batchSubDis: false,
|
||||
batchTitle: "",
|
||||
currentMatter: {},
|
||||
batchForm: {},
|
||||
lineOptions: [],
|
||||
settingOptions: [],
|
||||
unionOptions: [],
|
||||
organizationTypeOptions: [],
|
||||
pageForm: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
totalCount: 0,
|
||||
year: currentYear,
|
||||
matterName: "",
|
||||
unionId: "",
|
||||
organizationType: ""
|
||||
},
|
||||
formData: {},
|
||||
formRules: {
|
||||
year: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
matterName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
settingId: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
unionId: [{ validator: validateUnion, trigger: ["blur", "change"] }],
|
||||
organizationType: [{ required: true, message: "必填", trigger: ["blur", "change"] }]
|
||||
},
|
||||
batchRules: {
|
||||
lineId: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
signupStartTime: [{ validator: validateBatchTime, trigger: ["blur", "change"] }],
|
||||
signupEndTime: [{ validator: validateBatchTime, trigger: ["blur", "change"] }],
|
||||
travelStartTime: [{ validator: validateBatchTime, trigger: ["blur", "change"] }],
|
||||
travelEndTime: [{ validator: validateBatchTime, trigger: ["blur", "change"] }],
|
||||
contactName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
contactPhone: [{ validator: validateMobile, trigger: ["blur", "change"] }],
|
||||
minGroupPeople: [{ validator: validatePeople, trigger: ["blur", "change"] }],
|
||||
maxGroupPeople: [{ validator: validatePeople, trigger: ["blur", "change"] }],
|
||||
estimatedCost: [{ validator: validateMoney, trigger: ["blur", "change"] }]
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"pageForm.year"(year) {
|
||||
this.loadSettings(year)
|
||||
},
|
||||
"batchForm.lineId"() {
|
||||
if (this.batchFormVisible) {
|
||||
this.applySelectedLineDefaults(true)
|
||||
}
|
||||
},
|
||||
"batchForm.travelStartTime"() {
|
||||
if (this.batchFormVisible) {
|
||||
this.fillTravelEndByLot()
|
||||
}
|
||||
},
|
||||
"batchForm.travelEndTime"() {
|
||||
if (this.batchFormVisible) {
|
||||
this.validateTravelFields()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetSearch() {
|
||||
this.pageForm.year = moment().format("YYYY")
|
||||
this.pageForm.matterName = ""
|
||||
this.pageForm.unionId = ""
|
||||
this.pageForm.organizationType = ""
|
||||
this.loadSettings(this.pageForm.year)
|
||||
this.doSearch()
|
||||
},
|
||||
tableRowClassName({ row }) {
|
||||
return this.isDirectFamilyLine(row) ? "direct-family-row" : ""
|
||||
},
|
||||
isDirectFamilyLine(row) {
|
||||
if (!row) return false
|
||||
return row.directFamilyUnitLine === true
|
||||
|| row.directFamilyUnitLine === 1
|
||||
|| row.directFamilyUnitLine === "1"
|
||||
},
|
||||
emptyForm() {
|
||||
const user = this.currentUser()
|
||||
return {
|
||||
year: moment().format("YYYY"),
|
||||
matterName: "",
|
||||
settingId: "",
|
||||
creatorUserId: user.id || "",
|
||||
creatorName: user.username || "",
|
||||
unionId: "",
|
||||
organizationType: "",
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
openAdd() {
|
||||
this.title = "新增事项信息"
|
||||
this.viewMode = false
|
||||
this.formData = this.emptyForm()
|
||||
this.loadSettings(this.formData.year, true)
|
||||
this.dialogVisible = true
|
||||
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
|
||||
},
|
||||
openEdit(row) {
|
||||
this.title = "编辑事项信息"
|
||||
this.viewMode = false
|
||||
this.loadDetail(row.id)
|
||||
},
|
||||
openView(row) {
|
||||
this.title = "查看事项信息"
|
||||
this.viewMode = true
|
||||
this.loadDetail(row.id)
|
||||
},
|
||||
loadDetail(id) {
|
||||
this.$axios.post(loc() + "/detail", { id }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.formData = Object.assign(this.emptyForm(), res.data || {})
|
||||
this.formData.year = this.formData.year ? String(this.formData.year) : ""
|
||||
this.ensureSelectedUnionOption()
|
||||
this.loadSettings(this.formData.year)
|
||||
this.dialogVisible = true
|
||||
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
|
||||
} else {
|
||||
this.$message.warning(res.msg || "查询失败")
|
||||
}
|
||||
})
|
||||
},
|
||||
currentUser() {
|
||||
return (this.$store && this.$store.state && this.$store.state.user) || {}
|
||||
},
|
||||
currentUnion() {
|
||||
return this.currentUser().union || {}
|
||||
},
|
||||
organizationTypeChange() {
|
||||
this.applyOrganizationTypeRule()
|
||||
this.$nextTick(() => this.$refs.form && this.$refs.form.validateField("unionId"))
|
||||
},
|
||||
applyOrganizationTypeRule() {
|
||||
if (this.formData.organizationType === "schoolUnion") {
|
||||
this.formData.unionId = ""
|
||||
return
|
||||
}
|
||||
if (this.formData.organizationType === "branchUnion" || this.formData.organizationType === "personal") {
|
||||
const union = this.currentUnion()
|
||||
this.formData.unionId = union.id || ""
|
||||
this.ensureCurrentUnionOption(union)
|
||||
}
|
||||
},
|
||||
ensureCurrentUnionOption(union) {
|
||||
if (!union || !union.id) return
|
||||
const exists = this.unionOptions.some(item => item.id === union.id)
|
||||
if (!exists) {
|
||||
this.unionOptions.push({ id: union.id, name: union.name || "" })
|
||||
}
|
||||
},
|
||||
ensureSelectedUnionOption() {
|
||||
if (!this.formData.unionId) return
|
||||
const exists = this.unionOptions.some(item => item.id === this.formData.unionId)
|
||||
if (!exists) {
|
||||
this.unionOptions.push({ id: this.formData.unionId, name: this.formData.unionName || "" })
|
||||
}
|
||||
},
|
||||
isUnionDisabled() {
|
||||
return ["schoolUnion", "branchUnion", "personal"].includes(this.formData.organizationType)
|
||||
},
|
||||
yearChange(year) {
|
||||
this.formData.settingId = ""
|
||||
this.loadSettings(year, true)
|
||||
},
|
||||
loadSettings(year, autoSelectLatest) {
|
||||
this.$axios.post(loc() + "/settingOptions", { year }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.settingOptions = res.data || []
|
||||
if (autoSelectLatest && !this.formData.settingId && this.settingOptions.length > 0) {
|
||||
this.formData.settingId = this.settingOptions[0].id
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
loadUnions() {
|
||||
this.$axios.post(loc() + "/unionOptions").then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.unionOptions = res.data || []
|
||||
}
|
||||
})
|
||||
},
|
||||
loadOrganizationTypes() {
|
||||
this.$axios.post(loc() + "/organizationTypeOptions").then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.organizationTypeOptions = res.data || []
|
||||
}
|
||||
})
|
||||
},
|
||||
emptyBatchForm() {
|
||||
return {
|
||||
id: this.currentMatter.id || "",
|
||||
lineId: "",
|
||||
signupStartTime: "",
|
||||
signupEndTime: "",
|
||||
travelStartTime: "",
|
||||
travelEndTime: "",
|
||||
contactName: "",
|
||||
contactPhone: "",
|
||||
minGroupPeople: null,
|
||||
maxGroupPeople: null,
|
||||
estimatedCost: ""
|
||||
}
|
||||
},
|
||||
openBatchForm(row) {
|
||||
this.currentMatter = row
|
||||
this.loadLines(row.year)
|
||||
this.batchTitle = "选择线路"
|
||||
this.batchForm = this.emptyBatchForm()
|
||||
this.$axios.post(loc() + "/lineConfig", { matterId: row.id }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.batchForm = Object.assign(this.emptyBatchForm(), res.data || {})
|
||||
this.batchForm.travelStartTime = this.dateOnly(this.batchForm.travelStartTime)
|
||||
this.batchForm.travelEndTime = this.dateOnly(this.batchForm.travelEndTime)
|
||||
this.applySelectedLineDefaults(false)
|
||||
if (!this.batchForm.minGroupPeople || !this.batchForm.maxGroupPeople) {
|
||||
this.loadSettingPeople((data) => {
|
||||
this.batchForm.minGroupPeople = data.minGroupPeople || null
|
||||
this.batchForm.maxGroupPeople = data.maxGroupPeople || null
|
||||
})
|
||||
}
|
||||
this.batchFormVisible = true
|
||||
this.$nextTick(() => {
|
||||
this.applySelectedLineDefaults(true)
|
||||
this.$refs.batchFormRef && this.$refs.batchFormRef.clearValidate()
|
||||
})
|
||||
} else {
|
||||
this.$message.warning(res.msg || "查询失败")
|
||||
}
|
||||
})
|
||||
},
|
||||
loadLines(year) {
|
||||
this.$axios.post(loc() + "/lineOptions", { year }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.lineOptions = res.data || []
|
||||
this.applySelectedLineDefaults(true)
|
||||
}
|
||||
})
|
||||
},
|
||||
lineOptionLabel(item) {
|
||||
if (!item) return ""
|
||||
const lineName = this.lineField(item, "lineName") || ""
|
||||
const lineType = this.lineField(item, "lineType") || ""
|
||||
return lineType ? lineName + "(" + lineType + ")" : lineName
|
||||
},
|
||||
lineChange() {
|
||||
this.applySelectedLineDefaults(true)
|
||||
},
|
||||
travelStartChange() {
|
||||
this.fillTravelEndByLot()
|
||||
},
|
||||
travelEndChange() {
|
||||
this.validateTravelFields()
|
||||
},
|
||||
selectedLine() {
|
||||
return (this.lineOptions || []).find((item) => this.lineField(item, "id") === this.batchForm.lineId) || null
|
||||
},
|
||||
applySelectedLineDefaults(recalculateEnd) {
|
||||
const line = this.selectedLine()
|
||||
if (!line) return
|
||||
const activityCost = this.lineField(line, "activityCost")
|
||||
if (activityCost !== null && activityCost !== undefined && activityCost !== "") {
|
||||
this.batchForm.estimatedCost = activityCost
|
||||
}
|
||||
if (recalculateEnd) {
|
||||
this.fillTravelEndByLot()
|
||||
}
|
||||
},
|
||||
fillTravelEndByLot() {
|
||||
const line = this.selectedLine()
|
||||
const days = this.lineLotDays(line)
|
||||
if (!this.batchForm.travelStartTime || !days) return
|
||||
this.$set(this.batchForm, "travelEndTime", moment(this.batchForm.travelStartTime).add(days - 1, "days").format("YYYY-MM-DD"))
|
||||
this.validateTravelFields()
|
||||
},
|
||||
lineLotDays(line) {
|
||||
if (!line) return null
|
||||
const lotDays = this.lineField(line, "lotDays") || this.lineField(line, "lotValue")
|
||||
if (!/^\d+$/.test(String(lotDays || ""))) return null
|
||||
const days = parseInt(lotDays, 10)
|
||||
return days > 0 ? days : null
|
||||
},
|
||||
lineField(line, field) {
|
||||
if (!line) return null
|
||||
if (line[field] !== undefined) return line[field]
|
||||
const lowerField = field.toLowerCase()
|
||||
const matchedKey = Object.keys(line).find((key) => key.toLowerCase() === lowerField)
|
||||
return matchedKey ? line[matchedKey] : null
|
||||
},
|
||||
validateTravelFields() {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.batchFormRef) {
|
||||
this.$refs.batchFormRef.validateField(["travelStartTime", "travelEndTime"])
|
||||
}
|
||||
})
|
||||
},
|
||||
dateOnly(value) {
|
||||
if (!value) return ""
|
||||
return String(value).substring(0, 10)
|
||||
},
|
||||
loadSettingPeople(callback) {
|
||||
this.$axios.post(loc() + "/settingPeople", { matterId: this.currentMatter.id }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
const data = res.data || {}
|
||||
callback && callback(data)
|
||||
}
|
||||
})
|
||||
},
|
||||
peopleChange() {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.batchFormRef) {
|
||||
this.$refs.batchFormRef.validateField(["minGroupPeople", "maxGroupPeople"])
|
||||
}
|
||||
})
|
||||
},
|
||||
submitBatch() {
|
||||
this.$refs.batchFormRef.validate((valid) => {
|
||||
if (!valid) return
|
||||
this.batchSubDis = true
|
||||
this.$axios.post(loc() + "/lineConfigDoSubmit", this.batchForm).then((res) => {
|
||||
this.batchSubDis = false
|
||||
if (res.code === 0) {
|
||||
this.$message.success("保存成功")
|
||||
this.batchFormVisible = false
|
||||
this.pageData()
|
||||
} else {
|
||||
this.$message.warning(res.msg || "保存失败")
|
||||
}
|
||||
}).catch(() => {
|
||||
this.batchSubDis = false
|
||||
})
|
||||
})
|
||||
},
|
||||
doSubmit() {
|
||||
this.$refs.form.validate((valid) => {
|
||||
if (!valid) return
|
||||
this.subDis = true
|
||||
this.$axios.post(loc() + "/doSubmit", this.formData).then((res) => {
|
||||
this.subDis = false
|
||||
if (res.code === 0) {
|
||||
this.$message.success("保存成功")
|
||||
this.dialogVisible = false
|
||||
this.pageData()
|
||||
} else {
|
||||
this.$message.warning(res.msg || "保存失败")
|
||||
}
|
||||
}).catch(() => {
|
||||
this.subDis = false
|
||||
})
|
||||
})
|
||||
},
|
||||
toggleEnabled(row) {
|
||||
this.$axios.post(loc() + "/doSubmit", row).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success("状态已更新")
|
||||
} else {
|
||||
row.enabled = !row.enabled
|
||||
this.$message.warning(res.msg || "状态更新失败")
|
||||
}
|
||||
}).catch(() => {
|
||||
row.enabled = !row.enabled
|
||||
})
|
||||
},
|
||||
doDelete(row) {
|
||||
this.$set(row, "deleteLoading", true)
|
||||
this.$axios.post(loc() + "/deleteInfo", { id: row.id }).then((res) => {
|
||||
this.$set(row, "deleteLoading", false)
|
||||
if (res.code !== 0) {
|
||||
this.$message.warning(res.msg || "删除检查失败")
|
||||
return
|
||||
}
|
||||
const data = res.data || {}
|
||||
const signupCount = Number(data.signupCount || 0)
|
||||
const matterName = row.matterName || ""
|
||||
if (!data.canDelete) {
|
||||
this.$alert("事项【" + matterName + "】已有 " + signupCount + " 人报名,不能删除。", "删除提醒", {
|
||||
confirmButtonText: "知道了",
|
||||
type: "warning"
|
||||
})
|
||||
return
|
||||
}
|
||||
this.$confirm("事项【" + matterName + "】暂无人员报名,确认删除吗?", "删除提醒", {
|
||||
confirmButtonText: "确认删除",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
confirmButtonClass: "el-button--danger"
|
||||
}).then(() => {
|
||||
this.$set(row, "deleteLoading", true)
|
||||
this.$axios.post(loc() + "/doDelete", { id: row.id }).then((deleteRes) => {
|
||||
this.$set(row, "deleteLoading", false)
|
||||
if (deleteRes.code === 0) {
|
||||
this.$message.success("删除成功")
|
||||
this.pageData()
|
||||
} else {
|
||||
this.$message.warning(deleteRes.msg || "删除失败")
|
||||
}
|
||||
}).catch(() => {
|
||||
this.$set(row, "deleteLoading", false)
|
||||
})
|
||||
}).catch(() => {})
|
||||
}).catch(() => {
|
||||
this.$set(row, "deleteLoading", false)
|
||||
})
|
||||
},
|
||||
destroyForm() {
|
||||
this.formData = {}
|
||||
this.viewMode = false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadSettings(this.pageForm.year)
|
||||
this.loadUnions()
|
||||
this.loadOrganizationTypes()
|
||||
this.pageData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<el-card shadow="never">
|
||||
<el-empty description="${title!'功能建设中'}">
|
||||
<template slot="description">
|
||||
<span>${title!'功能建设中'}正在分阶段建设中</span>
|
||||
</template>
|
||||
</el-empty>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app"
|
||||
})
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,432 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<el-card shadow="never">
|
||||
<search :is-search-button="false">
|
||||
<search-item label="创建年度">
|
||||
<el-date-picker
|
||||
v-model="pageForm.year"
|
||||
type="year"
|
||||
value-format="yyyy"
|
||||
placeholder="请选择创建年度"
|
||||
style="width: 100%">
|
||||
</el-date-picker>
|
||||
</search-item>
|
||||
<search-item label="线路名称">
|
||||
<el-input
|
||||
v-model="pageForm.lineName"
|
||||
clearable
|
||||
placeholder="请输入线路名称"
|
||||
style="width: 100%"
|
||||
@keyup.enter.native="doSearch">
|
||||
</el-input>
|
||||
</search-item>
|
||||
<search-item label="时间标段">
|
||||
<el-select v-model="pageForm.lotId" clearable filterable placeholder="请选择时间标段" style="width: 100%">
|
||||
<el-option v-for="item in lotOptions" :key="item.id" :label="item.lotName" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<div class="search-query" style="margin-left: auto;">
|
||||
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
|
||||
<el-button size="medium" @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mt10" shadow="never">
|
||||
<table-tool :app="this" label="线路列表">
|
||||
<el-button @click="openAdd" size="medium" type="primary">
|
||||
<i class="el-icon-plus"></i>
|
||||
新增
|
||||
</el-button>
|
||||
</table-tool>
|
||||
|
||||
<el-table
|
||||
v-loading="tableLoading"
|
||||
:data="tableData"
|
||||
:size="tableSize"
|
||||
border
|
||||
:default-sort="{prop: 'enabled', order: 'descending'}"
|
||||
:row-class-name="tableRowClassName"
|
||||
@sort-change="pageOrder">
|
||||
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="创建年度" prop="year" width="120" sortable="custom" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="线路编号" prop="lineCode" width="140" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="线路名称" prop="lineName" min-width="220" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="时间标段" prop="lotName" width="140" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="线路类型" prop="lineType" width="140" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="创建人" prop="creatorName" width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="是否对外开放" prop="openFlag" width="160" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-switch
|
||||
v-model="row.openFlag"
|
||||
active-text="是"
|
||||
inactive-text="否"
|
||||
@change="toggleOpenFlag(row)">
|
||||
</el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="激活状态" prop="enabled" width="160" sortable="custom" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-switch
|
||||
v-model="row.enabled"
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
@change="toggleEnabled(row)">
|
||||
</el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="260" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-button size="mini" type="primary" @click="openView(row)">查看</el-button>
|
||||
<el-button size="mini" type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button size="mini" type="danger" @click="doDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
</guava>
|
||||
|
||||
<el-dialog
|
||||
:title="title"
|
||||
:visible.sync="dialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
width="72%"
|
||||
@closed="destroyEditor">
|
||||
<el-form :model="formData" :rules="formRules" label-width="110px" ref="form" :disabled="viewMode">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="创建年度" prop="year">
|
||||
<el-date-picker
|
||||
v-model="formData.year"
|
||||
type="year"
|
||||
value-format="yyyy"
|
||||
placeholder="请选择创建年度"
|
||||
style="width: 100%"
|
||||
@change="yearChange">
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="旅行社名称" prop="travelAgencyId">
|
||||
<el-select v-model="formData.travelAgencyId" clearable filterable placeholder="请选择旅行社" style="width: 100%">
|
||||
<el-option v-for="item in travelAgencyOptions" :key="item.id" :label="item.agencyName" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="线路名称" prop="lineName">
|
||||
<el-input v-model="formData.lineName" maxlength="30" show-word-limit placeholder="请输入线路名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="创建人">
|
||||
<el-input v-model="formData.creatorName" readonly placeholder="当前登录人"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="所在单位">
|
||||
<el-input v-model="formData.unitName" readonly placeholder="当前登录人所在单位"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="线路编号" prop="lineCode">
|
||||
<el-input v-model="formData.lineCode" maxlength="50" placeholder="建议年度加序号,如202601"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="激活状态" prop="enabled">
|
||||
<el-radio-group v-model="formData.enabled">
|
||||
<el-radio :label="true">启用</el-radio>
|
||||
<el-radio :label="false">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="是否对外开放" prop="openFlag">
|
||||
<el-radio-group v-model="formData.openFlag">
|
||||
<el-radio :label="true">是</el-radio>
|
||||
<el-radio :label="false">否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="直系亲属线路" prop="directFamilyUnitLine">
|
||||
<el-radio-group v-model="formData.directFamilyUnitLine">
|
||||
<el-radio :label="true">是</el-radio>
|
||||
<el-radio :label="false">否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="线路类型" prop="lineType">
|
||||
<el-select v-model="formData.lineType" clearable placeholder="请选择线路类型" style="width: 100%">
|
||||
<el-option v-for="item in lineTypeOptions" :key="item.code" :label="item.name" :value="item.name"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="时间标段" prop="lotId">
|
||||
<el-select v-model="formData.lotId" clearable filterable placeholder="请选择时间标段" style="width: 100%">
|
||||
<el-option v-for="item in lotOptions" :key="item.id" :label="item.lotName" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="线路内容" prop="lineContent">
|
||||
<text-editor v-model="formData.lineContent"></text-editor>
|
||||
</el-form-item>
|
||||
<el-form-item label="移动端缩略图" prop="mobileThumb">
|
||||
<!-- 与旅行社管理保持一致:移动端缩略图使用单图上传并保存URL。 -->
|
||||
<file-upload
|
||||
style="--upload-width: 200px;--upload-height:108px"
|
||||
:upload_number="1"
|
||||
:upload_size="20971520"
|
||||
:value.sync="formData.mobileThumb"
|
||||
accept=".jpg,.jpeg,.png"
|
||||
complete_result
|
||||
upload_mode="image"
|
||||
upload_result_category="interval">
|
||||
</file-upload>
|
||||
<div class="el-upload__tip">支持jpg、jpeg、png格式,大小不超过20MB。</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button v-if="!viewMode" type="primary" :loading="submitLoading" @click="doSubmit">保存</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.el-table .direct-family-row > td,
|
||||
.el-table .direct-family-row > td .cell {
|
||||
color: #f56c6c;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
store,
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
const currentYear = moment().format("YYYY")
|
||||
return {
|
||||
title: "",
|
||||
dialogVisible: false,
|
||||
viewMode: false,
|
||||
submitLoading: false,
|
||||
travelAgencyOptions: [],
|
||||
lotOptions: [],
|
||||
lineTypeOptions: [],
|
||||
pageForm: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
totalCount: 0,
|
||||
pageOrderName: "enabled",
|
||||
pageOrderBy: "descending",
|
||||
year: currentYear,
|
||||
lineName: "",
|
||||
lotId: ""
|
||||
},
|
||||
formData: {},
|
||||
formRules: {
|
||||
year: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
travelAgencyId: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
lineName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
lineCode: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
lineType: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
lotId: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
mobileThumb: [{ required: true, message: "请上传移动端缩略图", trigger: ["blur", "change"] }]
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetSearch() {
|
||||
this.pageForm.year = moment().format("YYYY")
|
||||
this.pageForm.lineName = ""
|
||||
this.pageForm.lotId = ""
|
||||
this.loadOptions(this.pageForm.year)
|
||||
this.doSearch()
|
||||
},
|
||||
tableRowClassName({ row }) {
|
||||
return this.isDirectFamilyLine(row) ? "direct-family-row" : ""
|
||||
},
|
||||
isDirectFamilyLine(row) {
|
||||
if (!row) return false
|
||||
return row.directFamilyUnitLine === true
|
||||
|| row.directFamilyUnitLine === 1
|
||||
|| row.directFamilyUnitLine === "1"
|
||||
},
|
||||
emptyForm() {
|
||||
const user = (this.$store && this.$store.state && this.$store.state.user) || {}
|
||||
return {
|
||||
year: moment().format("YYYY"),
|
||||
travelAgencyId: "",
|
||||
creatorUserId: user.id || "",
|
||||
creatorName: user.username || "",
|
||||
unitId: user.unit?.id || "",
|
||||
unitName: user.unit?.name || "",
|
||||
lineName: "",
|
||||
lineCode: "",
|
||||
enabled: true,
|
||||
openFlag: true,
|
||||
directFamilyUnitLine: false,
|
||||
lineType: "",
|
||||
lotId: "",
|
||||
lineContent: "",
|
||||
mobileThumb: ""
|
||||
}
|
||||
},
|
||||
openAdd() {
|
||||
this.title = "新增线路信息"
|
||||
this.viewMode = false
|
||||
this.formData = this.emptyForm()
|
||||
this.loadOptions(this.formData.year)
|
||||
this.dialogVisible = true
|
||||
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
|
||||
},
|
||||
openEdit(row) {
|
||||
this.title = "编辑线路信息"
|
||||
this.viewMode = false
|
||||
this.loadDetail(row.id)
|
||||
},
|
||||
openView(row) {
|
||||
this.title = "查看线路信息"
|
||||
this.viewMode = true
|
||||
this.loadDetail(row.id)
|
||||
},
|
||||
loadDetail(id) {
|
||||
this.$axios.post(loc() + "/detail", { id }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.formData = Object.assign(this.emptyForm(), res.data || {})
|
||||
this.formData.year = this.formData.year ? String(this.formData.year) : ""
|
||||
this.loadOptions(this.formData.year)
|
||||
this.dialogVisible = true
|
||||
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
|
||||
} else {
|
||||
this.$message.warning(res.msg || "查询失败")
|
||||
}
|
||||
})
|
||||
},
|
||||
yearChange(year) {
|
||||
this.formData.travelAgencyId = ""
|
||||
this.formData.lotId = ""
|
||||
this.loadOptions(year)
|
||||
},
|
||||
loadOptions(year) {
|
||||
this.loadTravelAgencies(year)
|
||||
this.loadLots(year)
|
||||
this.loadLineTypes()
|
||||
},
|
||||
loadTravelAgencies(year) {
|
||||
this.$axios.post(loc() + "/travelAgencyOptions", { year }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.travelAgencyOptions = res.data || []
|
||||
}
|
||||
})
|
||||
},
|
||||
loadLots(year) {
|
||||
this.$axios.post(loc() + "/lotOptions", { year }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.lotOptions = res.data || []
|
||||
}
|
||||
})
|
||||
},
|
||||
loadLineTypes() {
|
||||
this.$axios.post(loc() + "/lineTypeOptions").then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.lineTypeOptions = res.data || []
|
||||
}
|
||||
})
|
||||
},
|
||||
doSubmit() {
|
||||
this.$refs.form.validate((valid) => {
|
||||
if (!valid) return
|
||||
this.submitLoading = true
|
||||
this.$axios.post(loc() + "/doSubmit", this.formData).then((res) => {
|
||||
this.submitLoading = false
|
||||
if (res.code === 0) {
|
||||
this.$message.success("保存成功")
|
||||
this.dialogVisible = false
|
||||
this.pageData()
|
||||
} else {
|
||||
this.$message.warning(res.msg || "保存失败")
|
||||
}
|
||||
}).catch(() => {
|
||||
this.submitLoading = false
|
||||
})
|
||||
})
|
||||
},
|
||||
toggleEnabled(row) {
|
||||
this.$axios.post(loc() + "/doSubmit", row).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success("状态已更新")
|
||||
} else {
|
||||
row.enabled = !row.enabled
|
||||
this.$message.warning(res.msg || "状态更新失败")
|
||||
}
|
||||
}).catch(() => {
|
||||
row.enabled = !row.enabled
|
||||
})
|
||||
},
|
||||
toggleOpenFlag(row) {
|
||||
this.$axios.post(loc() + "/doSubmit", row).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success("对外开放状态已更新")
|
||||
} else {
|
||||
row.openFlag = !row.openFlag
|
||||
this.$message.warning(res.msg || "对外开放状态更新失败")
|
||||
}
|
||||
}).catch(() => {
|
||||
row.openFlag = !row.openFlag
|
||||
})
|
||||
},
|
||||
doDelete(row) {
|
||||
this.$confirm("确定删除【" + row.lineName + "】吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
this.$axios.post(loc() + "/doDelete", { id: row.id }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success("删除成功")
|
||||
this.pageData()
|
||||
} else {
|
||||
this.$message.warning(res.msg || "删除失败")
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
destroyEditor() {
|
||||
this.formData = {}
|
||||
this.viewMode = false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadOptions(this.pageForm.year)
|
||||
this.pageData()
|
||||
},
|
||||
watch: {
|
||||
"pageForm.year"(year) {
|
||||
this.pageForm.lotId = ""
|
||||
this.loadOptions(year)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
+547
@@ -0,0 +1,547 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<el-card shadow="never">
|
||||
<search :is-search-button="false">
|
||||
<search-item label="开始年度">
|
||||
<el-date-picker
|
||||
v-model="pageForm.startYear"
|
||||
type="year"
|
||||
value-format="yyyy"
|
||||
placeholder="请选择开始年度"
|
||||
style="width: 100%">
|
||||
</el-date-picker>
|
||||
</search-item>
|
||||
<search-item label="结束年度">
|
||||
<el-date-picker
|
||||
v-model="pageForm.endYear"
|
||||
type="year"
|
||||
value-format="yyyy"
|
||||
placeholder="请选择结束年度"
|
||||
style="width: 100%">
|
||||
</el-date-picker>
|
||||
</search-item>
|
||||
<search-item label="姓名/工号">
|
||||
<el-input
|
||||
v-model="pageForm.keyword"
|
||||
clearable
|
||||
placeholder="请输入姓名或工号"
|
||||
style="width: 100%"
|
||||
@keyup.enter.native="doSearch">
|
||||
</el-input>
|
||||
</search-item>
|
||||
<search-item label="所属工会">
|
||||
<el-select v-model="pageForm.unionId" clearable filterable placeholder="请选择所属工会" style="width: 100%">
|
||||
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="线路">
|
||||
<el-select v-model="pageForm.lineId" clearable filterable placeholder="请选择线路" style="width: 100%">
|
||||
<el-option v-for="item in lineOptions" :key="item.lineId" :label="item.lineName" :value="item.lineId"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="出行时段">
|
||||
<el-select v-model="pageForm.travelPeriod" clearable filterable placeholder="请选择出行时段" style="width: 100%">
|
||||
<el-option v-for="item in travelPeriodOptions" :key="item.travelPeriod" :label="item.travelPeriod" :value="item.travelPeriod"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="线路类型">
|
||||
<el-select v-model="pageForm.lineType" clearable filterable placeholder="请选择线路类型" style="width: 100%">
|
||||
<el-option v-for="item in lineTypeOptions" :key="item.lineType" :label="item.lineType" :value="item.lineType"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<div class="search-query" style="margin-left: auto;">
|
||||
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
|
||||
<el-button size="medium" @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mt10" shadow="never">
|
||||
<table-tool :app="this" label="校工会审核">
|
||||
<el-radio-group class="mr5" size="small" v-model="pageForm.audit" @change="changeAudit">
|
||||
<el-radio-button :label="true">已审核</el-radio-button>
|
||||
<el-radio-button :label="false">未审核</el-radio-button>
|
||||
</el-radio-group>
|
||||
</table-tool>
|
||||
<el-table
|
||||
v-loading="tableLoading"
|
||||
:data="tableData"
|
||||
:size="tableSize"
|
||||
border
|
||||
@sort-change="pageOrder">
|
||||
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="工号" prop="jobNo" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="姓名" prop="userName" width="120" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="所在单位" prop="unitName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="报名线路" prop="lineName" min-width="220" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="出行时段" prop="travelPeriod" min-width="220" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="线路类型" prop="lineType" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="报名时间" prop="signupTime" width="170" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="携带家属" prop="familyCount" width="120" sortable="custom" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag size="mini" :type="Number(row.familyCount || 0) > 0 ? 'success' : 'info'">{{ row.familyCount || 0 }}人</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="报销超出费用" prop="overCostReimbursed" width="140" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag size="mini" :type="row.overCostReimbursed ? 'success' : 'info'">{{ row.overCostReimbursed ? '是' : '否' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="当前节点" prop="curTaskName" width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="流程状态" prop="instanceState" width="120" sortable="custom" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" align="center" header-align="center" fixed="right">
|
||||
<template slot-scope="{row}">
|
||||
<el-button size="mini" type="primary" @click="openView(row)">查看</el-button>
|
||||
<el-button v-if="row.taskState === 10" size="mini" type="primary" @click="openApproval(row)">审核</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
|
||||
<template #edit>
|
||||
<tour-approval-info ref="infoRef">
|
||||
<div v-if="showApprovalForm">
|
||||
<div class="process-title">
|
||||
{{ formData.taskName }}
|
||||
</div>
|
||||
<el-form
|
||||
:model="formData"
|
||||
ref="formRef"
|
||||
label-width="0"
|
||||
label-suffix=":"
|
||||
class="flow-task-form">
|
||||
<el-form-item
|
||||
label="审批意见"
|
||||
prop="tf_opinion"
|
||||
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
|
||||
<user-opinion-textarea v-model="formData.tf_opinion"></user-opinion-textarea>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-row type="flex" justify="end">
|
||||
<el-button @click="$refs.guava.index()" size="small">取消</el-button>
|
||||
<el-button @click="handleTaskAction(6)" size="small" type="info">退回到发起人</el-button>
|
||||
<el-button @click="handleTaskAction(2)" size="small" type="danger">拒绝申请</el-button>
|
||||
<el-button @click="handleTaskAction(1)" size="small" type="primary">同意申请</el-button>
|
||||
</el-row>
|
||||
</div>
|
||||
</tour-approval-info>
|
||||
</template>
|
||||
</guava>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#app .search .search-item {
|
||||
width: calc((100% - 150px) / 4);
|
||||
}
|
||||
#app .search .search-query {
|
||||
margin-left: auto;
|
||||
}
|
||||
@media screen and (max-width: 1350px) {
|
||||
#app .search .search-item {
|
||||
width: calc((100% - 100px) / 3);
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 1200px) {
|
||||
#app .search .search-item {
|
||||
width: calc((100% - 50px) / 2);
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 992px) {
|
||||
#app .search .search-item {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.tour-approval-section {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.tour-approval-table {
|
||||
width: 100%;
|
||||
}
|
||||
.tour-approval-empty {
|
||||
color: #909399;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
const TOUR_APPROVAL_INFO = {
|
||||
template: /*language=HTML*/ `
|
||||
<div>
|
||||
<div class="process-title">
|
||||
报名信息
|
||||
<el-link type="primary" @click="openChart">点击查看流程图</el-link>
|
||||
</div>
|
||||
<el-descriptions :column="3" border class="tour-approval-section">
|
||||
<el-descriptions-item label="工号">{{ detail.jobNo || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="姓名">{{ detail.userName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="性别">{{ detail.gender || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="身份证号">{{ detail.idCard || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所在单位">{{ detail.unitName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所在工会">{{ detail.unionName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="报名时间">{{ detail.signupTime || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="报名线路">{{ detail.lineName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="出行时段">{{ detail.travelPeriod || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="线路类型">{{ detail.lineType || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="报名酒店">{{ detail.hotelName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="旅行社">{{ detail.travelAgencyName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="是否携带家属">
|
||||
{{ familyText() }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="报销超出费用">{{ detail.overCostReimbursed ? '是' : '否' }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="fillBedInfo" label="意向拼床人">{{ detail.intendedRoommate || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="fillBedInfo" label="床型">{{ detail.bedType || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="fillBedInfo" label="床位信息">{{ detail.bedInfo || '' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div v-if="isDirectFamilyLine()" class="tour-approval-section">
|
||||
<div class="process-title">直系亲属线路</div>
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item label="亲属姓名">{{ directRelative.relativeName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所在单位">{{ directRelative.unitName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="亲属关系">{{ directRelative.relationshipName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="线路名称">{{ directRelative.lineName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="出行开始日期">{{ directRelative.travelStartTime || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="出行结束日期">{{ directRelative.travelEndTime || '' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div v-if="showFamilySection()" class="tour-approval-section">
|
||||
<div class="process-title">亲属信息</div>
|
||||
<el-table :data="familyData" border size="mini" empty-text="暂无亲属信息" class="tour-approval-table">
|
||||
<el-table-column label="序号" type="index" width="70" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="家属姓名" prop="familyName" min-width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="性别" prop="gender" width="90" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="身份证号码" prop="idCard" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="年龄" prop="age" width="90" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="关系" prop="relationship" width="110" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column v-if="fillBedInfo" label="床型" prop="bedType" width="110" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column v-if="fillBedInfo" label="床位" prop="bedInfo" min-width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column v-if="fillBedInfo" label="意向拼床人" prop="intendedRoommate" min-width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<template v-for="task in doneTasks">
|
||||
<div class="mt10">
|
||||
<div class="process-title">{{ task.displayName }}</div>
|
||||
<el-descriptions border class="flow-task-form" :column="3" :key="task.id" v-if="task.ext.isFirstTaskNode">
|
||||
<el-descriptions-item label="申请用户">
|
||||
{{ task.ext.initiatorName }}({{ task.ext.initiatorAccount }})
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="申请时间">{{ task.finishTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="办理结果">
|
||||
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-descriptions border class="flow-task-form" :column="3" :key="task.id" v-else>
|
||||
<el-descriptions-item label="办理用户">
|
||||
{{ task.taskFormData.userName }}({{ task.taskFormData.loginName }})
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="办理时间">{{ task.finishTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="办理结果">
|
||||
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="办理意见" :span="3">
|
||||
{{ task.taskFormData.opinion || task.taskFormData.tf_opinion || '' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<slot></slot>
|
||||
|
||||
<snaker-chart ref="snakerChartRef"></snaker-chart>
|
||||
</div>
|
||||
`,
|
||||
dicts: ["PROCESS_TASK_SUBMIT_TYPE"],
|
||||
data() {
|
||||
return {
|
||||
row: {},
|
||||
detail: {},
|
||||
familyData: [],
|
||||
directRelative: {},
|
||||
allowFamily: false,
|
||||
fillBedInfo: true,
|
||||
directFamilyUnitLine: false,
|
||||
doneTasks: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onOpen(row) {
|
||||
this.row = row || {}
|
||||
this.detail = {}
|
||||
this.familyData = []
|
||||
this.directRelative = {}
|
||||
this.allowFamily = false
|
||||
this.fillBedInfo = true
|
||||
this.directFamilyUnitLine = false
|
||||
this.doneTasks = []
|
||||
this.info()
|
||||
this.getDoneTasks()
|
||||
},
|
||||
info() {
|
||||
this.$axios.post(loc() + "/detail", { id: this.row.id }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
const data = res.data || {}
|
||||
this.detail = data.ledger || {}
|
||||
this.detail.travelPeriod = data.travelPeriod || this.row.travelPeriod || ""
|
||||
this.familyData = data.families || []
|
||||
this.directRelative = data.directRelative || {}
|
||||
this.allowFamily = this.toBoolean(data.allowFamily)
|
||||
this.fillBedInfo = data.fillBedInfo === undefined || data.fillBedInfo === null || this.toBoolean(data.fillBedInfo)
|
||||
this.directFamilyUnitLine = this.toBoolean(data.directFamilyUnitLine)
|
||||
} else {
|
||||
this.$message.warning(res.msg || "查询失败")
|
||||
}
|
||||
})
|
||||
},
|
||||
toBoolean(value) {
|
||||
return value === true || value === 1 || value === "1"
|
||||
},
|
||||
isDirectFamilyLine() {
|
||||
return this.directFamilyUnitLine
|
||||
|| this.toBoolean(this.detail.directFamilyUnitLine)
|
||||
|| (this.directRelative && this.directRelative.id)
|
||||
},
|
||||
hasFamily() {
|
||||
return this.toBoolean(this.detail.hasFamily) || this.familyData.length > 0
|
||||
},
|
||||
showFamilySection() {
|
||||
return this.allowFamily && this.hasFamily() && !this.isDirectFamilyLine()
|
||||
},
|
||||
familyText() {
|
||||
if (this.isDirectFamilyLine()) {
|
||||
return "否"
|
||||
}
|
||||
if (!this.allowFamily) {
|
||||
return "否"
|
||||
}
|
||||
return this.hasFamily() ? "是" : "否"
|
||||
},
|
||||
getDoneTasks() {
|
||||
this.$axios.post("/flow/common/doneTasks", { bizId: this.row.id }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.doneTasks = res.data || []
|
||||
}
|
||||
})
|
||||
},
|
||||
openChart() {
|
||||
if (!this.row.instanceProcessDefineId || !this.row.instanceId) {
|
||||
this.$message.warning("暂无流程图信息")
|
||||
return
|
||||
}
|
||||
this.$refs.snakerChartRef.onOpenFull(this.row.instanceProcessDefineId, this.row.instanceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: "#app",
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
const currentYear = moment().format("YYYY")
|
||||
return {
|
||||
unionOptions: [],
|
||||
lineOptions: [],
|
||||
travelPeriodOptions: [],
|
||||
lineTypeOptions: [],
|
||||
showApprovalForm: false,
|
||||
formData: {
|
||||
tf_opinion: ""
|
||||
},
|
||||
pageForm: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
totalCount: 0,
|
||||
pageOrderName: "signupTime",
|
||||
pageOrderBy: "descending",
|
||||
audit: false,
|
||||
startYear: currentYear,
|
||||
endYear: currentYear,
|
||||
keyword: "",
|
||||
unionId: "",
|
||||
lineId: "",
|
||||
travelPeriod: "",
|
||||
lineType: ""
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetSearch() {
|
||||
this.pageForm.startYear = moment().format("YYYY")
|
||||
this.pageForm.endYear = moment().format("YYYY")
|
||||
this.pageForm.keyword = ""
|
||||
this.pageForm.unionId = ""
|
||||
this.pageForm.lineId = ""
|
||||
this.pageForm.travelPeriod = ""
|
||||
this.pageForm.lineType = ""
|
||||
this.loadOptions()
|
||||
this.doSearch()
|
||||
},
|
||||
changeAudit() {
|
||||
this.pageForm.pageNumber = 1
|
||||
this.clearCascadeFilters()
|
||||
this.loadOptions()
|
||||
this.pageData()
|
||||
},
|
||||
clearCascadeFilters() {
|
||||
this.pageForm.unionId = ""
|
||||
this.pageForm.lineId = ""
|
||||
this.pageForm.travelPeriod = ""
|
||||
this.pageForm.lineType = ""
|
||||
},
|
||||
openView(row) {
|
||||
this.$refs.guava.edit(() => {
|
||||
this.showApprovalForm = false
|
||||
this.formData = {
|
||||
tf_opinion: ""
|
||||
}
|
||||
this.$refs.infoRef.onOpen(row)
|
||||
})
|
||||
},
|
||||
openApproval(row) {
|
||||
this.$refs.guava.edit(() => {
|
||||
this.showApprovalForm = true
|
||||
this.formData = {
|
||||
processTaskId: row.taskId,
|
||||
taskName: row.curTaskName || row.taskName,
|
||||
tf_opinion: ""
|
||||
}
|
||||
this.$refs.infoRef.onOpen(row)
|
||||
})
|
||||
},
|
||||
handleTaskAction(val) {
|
||||
this.$refs.formRef.validate((valid) => {
|
||||
if (!valid) return
|
||||
this.$confirm("您确定要提交吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
const loading = createLoading()
|
||||
this.$axios.post("/flow/common/executeTask", {
|
||||
data: JSON.stringify({
|
||||
...this.formData,
|
||||
submitType: val
|
||||
})
|
||||
}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$refs.guava.index()
|
||||
this.$message.success(res.msg || "操作成功")
|
||||
this.doSearch()
|
||||
} else {
|
||||
this.$message.warning(res.msg || "操作失败")
|
||||
}
|
||||
}).finally(() => {
|
||||
loading.close()
|
||||
})
|
||||
}).catch(() => {})
|
||||
})
|
||||
},
|
||||
loadOptions() {
|
||||
this.loadUnionOptions()
|
||||
this.loadLineOptions()
|
||||
this.loadTravelPeriodOptions()
|
||||
this.loadLineTypeOptions()
|
||||
},
|
||||
baseOptionParams() {
|
||||
return {
|
||||
audit: this.pageForm.audit,
|
||||
startYear: this.pageForm.startYear,
|
||||
endYear: this.pageForm.endYear,
|
||||
keyword: this.pageForm.keyword,
|
||||
unionId: this.pageForm.unionId,
|
||||
lineId: this.pageForm.lineId,
|
||||
travelPeriod: this.pageForm.travelPeriod,
|
||||
lineType: this.pageForm.lineType
|
||||
}
|
||||
},
|
||||
loadUnionOptions() {
|
||||
this.$axios.post(loc() + "/unionOptions", this.baseOptionParams()).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.unionOptions = res.data || []
|
||||
if (this.pageForm.unionId && !this.unionOptions.some(item => item.id === this.pageForm.unionId)) {
|
||||
this.pageForm.unionId = ""
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
loadLineOptions() {
|
||||
this.$axios.post(loc() + "/lineOptions", this.baseOptionParams()).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.lineOptions = res.data || []
|
||||
if (this.pageForm.lineId && !this.lineOptions.some(item => item.lineId === this.pageForm.lineId)) {
|
||||
this.pageForm.lineId = ""
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
loadTravelPeriodOptions() {
|
||||
this.$axios.post(loc() + "/travelPeriodOptions", this.baseOptionParams()).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.travelPeriodOptions = res.data || []
|
||||
if (this.pageForm.travelPeriod && !this.travelPeriodOptions.some(item => item.travelPeriod === this.pageForm.travelPeriod)) {
|
||||
this.pageForm.travelPeriod = ""
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
loadLineTypeOptions() {
|
||||
this.$axios.post(loc() + "/lineTypeOptions", this.baseOptionParams()).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.lineTypeOptions = res.data || []
|
||||
if (this.pageForm.lineType && !this.lineTypeOptions.some(item => item.lineType === this.pageForm.lineType)) {
|
||||
this.pageForm.lineType = ""
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadOptions()
|
||||
this.pageData()
|
||||
},
|
||||
components: {
|
||||
"tour-approval-info": TOUR_APPROVAL_INFO
|
||||
},
|
||||
watch: {
|
||||
"pageForm.startYear"() {
|
||||
this.clearCascadeFilters()
|
||||
this.loadOptions()
|
||||
},
|
||||
"pageForm.endYear"() {
|
||||
this.clearCascadeFilters()
|
||||
this.loadOptions()
|
||||
},
|
||||
"pageForm.lineId"() {
|
||||
this.loadTravelPeriodOptions()
|
||||
this.loadLineTypeOptions()
|
||||
},
|
||||
"pageForm.travelPeriod"() {
|
||||
this.loadLineOptions()
|
||||
this.loadLineTypeOptions()
|
||||
},
|
||||
"pageForm.lineType"() {
|
||||
this.loadLineOptions()
|
||||
this.loadTravelPeriodOptions()
|
||||
},
|
||||
"pageForm.unionId"() {
|
||||
this.loadLineOptions()
|
||||
this.loadTravelPeriodOptions()
|
||||
this.loadLineTypeOptions()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,634 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<el-card shadow="never">
|
||||
<search :is-search-button="false">
|
||||
<search-item label="年度">
|
||||
<el-date-picker
|
||||
v-model="pageForm.year"
|
||||
type="year"
|
||||
value-format="yyyy"
|
||||
placeholder="请选择年度"
|
||||
style="width: 100%">
|
||||
</el-date-picker>
|
||||
</search-item>
|
||||
<search-item label="配置名称">
|
||||
<el-input
|
||||
v-model="pageForm.configName"
|
||||
clearable
|
||||
placeholder="请输入疗休养配置名称"
|
||||
style="width: 100%"
|
||||
@keyup.enter.native="doSearch">
|
||||
</el-input>
|
||||
</search-item>
|
||||
<div class="search-query">
|
||||
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
|
||||
<el-button size="medium" @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mt10" shadow="never">
|
||||
<table-tool :app="this" label="疗休养配置">
|
||||
<el-button type="primary" size="medium" @click="openAdd">
|
||||
<i class="el-icon-plus"></i>
|
||||
新建
|
||||
</el-button>
|
||||
</table-tool>
|
||||
|
||||
<el-table
|
||||
:data="tableData"
|
||||
:size="tableSize"
|
||||
border
|
||||
class="vi-table"
|
||||
ref="table"
|
||||
row-key="id"
|
||||
v-loading="tableLoading"
|
||||
@sort-change="pageOrder"
|
||||
>
|
||||
<el-table-column label="序号" type="index" :index="indexMethod" width="70" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="年度" prop="year" width="100" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="疗休养配置名称" prop="configName" min-width="260" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="周期开始年度" prop="cycleStartYear" width="130" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="最少成团人数" prop="minGroupPeople" width="120" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="最多成团人数" prop="maxGroupPeople" width="120" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="是否携带家属" prop="allowFamily" width="120" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag size="mini" type="success" v-if="row.allowFamily">允许</el-tag>
|
||||
<el-tag size="mini" type="info" v-else>不允许</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="是否启用" prop="enabled" width="100" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag size="mini" type="success" v-if="row.enabled">启用</el-tag>
|
||||
<el-tag size="mini" type="info" v-else>停用</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-button size="mini" type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button size="mini" type="danger" @click="doDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
</guava>
|
||||
|
||||
<el-dialog
|
||||
:title="title"
|
||||
:visible.sync="dialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
width="64.8%"
|
||||
@closed="destroyEditor">
|
||||
<el-form :model="formData" :rules="formRules" label-width="120px" ref="form">
|
||||
<div v-if="title === '新建疗休养配置'" class="tour-setting-inherit">
|
||||
<el-button type="primary" plain size="medium" icon="el-icon-copy-document" :loading="inheritLoading" @click="inheritPreviousYearInfo">延用上一年信息</el-button>
|
||||
</div>
|
||||
<el-tabs v-model="activeTab" type="card">
|
||||
<el-tab-pane label="基本信息" name="basic">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="年度" prop="year">
|
||||
<el-date-picker
|
||||
v-model="formData.year"
|
||||
type="year"
|
||||
value-format="yyyy"
|
||||
placeholder="请选择年度"
|
||||
style="width: 100%"
|
||||
></el-date-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="配置名称" prop="configName">
|
||||
<el-input v-model="formData.configName" maxlength="100" placeholder="请输入配置名称"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="疗休养类型" prop="tourType">
|
||||
<el-select v-model="formData.tourType" clearable allow-create filterable placeholder="请选择或输入类型" style="width: 100%">
|
||||
<el-option label="省内疗休养" value="省内疗休养"></el-option>
|
||||
<el-option label="省外疗休养" value="省外疗休养"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="可参加人员" prop="activityGroupId">
|
||||
<div class="tour-user-scope">
|
||||
<el-select
|
||||
v-model="formData.activityGroupId"
|
||||
placeholder="请选择可参加人员"
|
||||
filterable
|
||||
clearable
|
||||
style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in activityGroupList"
|
||||
:key="item.groupId"
|
||||
:label="item.groupName"
|
||||
:value="item.groupId">
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-button type="primary" @click="openUserScope">设置</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="最少成团人数" prop="minGroupPeople">
|
||||
<el-input-number v-model="formData.minGroupPeople" :controls="false" :min="0" :precision="0" placeholder="请输入最少成团人数" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="最多成团人数" prop="maxGroupPeople">
|
||||
<el-input-number v-model="formData.maxGroupPeople" :controls="false" :min="0" :precision="0" placeholder="请输入最多成团人数" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<div class="tour-setting-basic-divider"></div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="省外几年去一次" prop="outProvinceYears">
|
||||
<el-input-number v-model="formData.outProvinceYears" :controls="false" :min="0" :precision="0" placeholder="请输入省外间隔年限" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="省外人数占比" prop="outProvinceRatio">
|
||||
<el-input-number v-model="formData.outProvinceRatio" :controls="false" :min="0" :max="100" :precision="2" placeholder="请输入省外人数占比" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="省外占比类型" prop="outProvinceRatioType">
|
||||
<el-select v-model="formData.outProvinceRatioType" placeholder="请选择省外占比类型" style="width: 100%" @change="outProvinceRatioTypeChange">
|
||||
<el-option label="当年报名人数" value="当年报名人数"></el-option>
|
||||
<el-option label="可参加教职工人数" value="可参加教职工人数"></el-option>
|
||||
<el-option label="固定人数" value="固定人数"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-if="formData.outProvinceRatioType === '固定人数'" :span="12">
|
||||
<el-form-item label="固定人数" prop="outProvinceFixedPeople">
|
||||
<el-input-number v-model="formData.outProvinceFixedPeople" :controls="false" :min="0" :precision="0" placeholder="请输入固定人数" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<div class="tour-setting-basic-divider"></div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="周期开始年度" prop="cycleStartYear">
|
||||
<el-date-picker
|
||||
v-model="formData.cycleStartYear"
|
||||
type="year"
|
||||
value-format="yyyy"
|
||||
clearable
|
||||
placeholder="请选择周期开始年度"
|
||||
style="width: 100%">
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="周期结束年度" prop="cycleEndYear">
|
||||
<el-date-picker
|
||||
v-model="formData.cycleEndYear"
|
||||
type="year"
|
||||
value-format="yyyy"
|
||||
clearable
|
||||
placeholder="请选择周期结束年度"
|
||||
style="width: 100%">
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="周期内总费用" prop="cycleTotalCost">
|
||||
<el-input-number v-model="formData.cycleTotalCost" :controls="false" :min="0" :precision="0" placeholder="请输入周期内总费用" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="周期允许次数" prop="cycleAllowedTimes">
|
||||
<el-input-number v-model="formData.cycleAllowedTimes" :controls="false" :min="0" :precision="0" placeholder="请输入周期允许次数" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<div class="tour-setting-basic-divider"></div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="允许携带家属" prop="allowFamily">
|
||||
<el-switch v-model="formData.allowFamily" active-text="允许" inactive-text="不允许"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="填报床位信息" prop="fillBedInfo">
|
||||
<el-switch v-model="formData.fillBedInfo" active-text="是" inactive-text="否"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="是否启用" prop="enabled">
|
||||
<el-switch v-model="formData.enabled" active-text="启用" inactive-text="停用"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="标段管理" name="lots">
|
||||
<div style="text-align: right; margin-bottom: 10px;">
|
||||
<el-button type="primary" size="medium" icon="el-icon-plus" @click="addLot">增加一条</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
:data="formData.lots"
|
||||
border
|
||||
class="vi-table"
|
||||
empty-text="暂无标段"
|
||||
style="width: 100%">
|
||||
<el-table-column label="序号" type="index" width="70" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="标段名称" min-width="180" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-input v-model="row.lotName" maxlength="50" placeholder="请输入标段名称"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="标段值" min-width="160" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-input v-model="row.lotValue" maxlength="10" placeholder="请输入整数" @input="integerInput(row, 'lotValue', $event)"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="标段费用" min-width="150" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-input v-model="row.activityCost" maxlength="10" placeholder="请输入整数" @input="integerInput(row, 'activityCost', $event)"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="允许超出报销" width="140" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-checkbox v-model="row.allowOverReimbursement"></el-checkbox>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90" align="center" header-align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="danger" size="mini" icon="el-icon-delete" @click="deleteLot(scope.$index, scope.row)"></el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="服务须知" name="notice">
|
||||
<el-form-item prop="serviceNotice" label-width="0">
|
||||
<text-editor v-model="formData.serviceNotice"></text-editor>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="doSubmit">确认</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
<drawer-user-scope
|
||||
ref="drawerUserScope"
|
||||
:group_id.sync="formData.activityGroupId"
|
||||
@group_change="handleActivityGroupChange">
|
||||
</drawer-user-scope>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
// 前端先做成团人数区间校验,后端也会再兜底校验一次。
|
||||
const checkGroupPeople = (rule, value, callback) => {
|
||||
if (this.formData.minGroupPeople !== null && this.formData.maxGroupPeople !== null
|
||||
&& this.formData.minGroupPeople > this.formData.maxGroupPeople) {
|
||||
callback(new Error("最少成团人数不能大于最多成团人数"))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
const checkCycleYear = (rule, value, callback) => {
|
||||
const startYear = parseInt(this.formData.cycleStartYear, 10)
|
||||
const endYear = parseInt(this.formData.cycleEndYear, 10)
|
||||
if (!isNaN(startYear) && !isNaN(endYear) && startYear > endYear) {
|
||||
callback(new Error("周期开始年度不能大于周期结束年度"))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
const checkOutProvinceFixedPeople = (rule, value, callback) => {
|
||||
if (this.formData.outProvinceRatioType === "固定人数"
|
||||
&& (value === null || value === undefined || value === "" || value < 0)) {
|
||||
callback(new Error("固定人数必须大于等于0"))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
return {
|
||||
title: "",
|
||||
dialogVisible: false,
|
||||
activeTab: "basic",
|
||||
pageForm: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
totalCount: 0,
|
||||
year: "",
|
||||
configName: ""
|
||||
},
|
||||
activityGroupList: [],
|
||||
formData: {},
|
||||
lotDeleteList: [],
|
||||
inheritLoading: false,
|
||||
formRules: {
|
||||
year: [{required: true, message: "必填", trigger: ["blur", "change"]}],
|
||||
configName: [{required: true, message: "必填", trigger: ["blur", "change"]}],
|
||||
minGroupPeople: [{validator: checkGroupPeople, trigger: ["blur", "change"]}],
|
||||
maxGroupPeople: [{validator: checkGroupPeople, trigger: ["blur", "change"]}],
|
||||
outProvinceRatioType: [{required: true, message: "必填", trigger: ["blur", "change"]}],
|
||||
outProvinceFixedPeople: [{validator: checkOutProvinceFixedPeople, trigger: ["blur", "change"]}],
|
||||
cycleStartYear: [{validator: checkCycleYear, trigger: ["blur", "change"]}],
|
||||
cycleEndYear: [{validator: checkCycleYear, trigger: ["blur", "change"]}]
|
||||
}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
"drawer-user-scope": httpVueLoader("/components/module/activity/DrawerUserScope.vue")
|
||||
},
|
||||
methods: {
|
||||
async getActivityGroup() {
|
||||
const {data} = await this.$axios.post("/platform/activity/basic/scope/getActivityUserScopeGroup")
|
||||
this.activityGroupList = (data || []).map((item) => {
|
||||
return Object.assign({}, item, {
|
||||
groupId: item.groupId === null || item.groupId === undefined ? "" : String(item.groupId)
|
||||
})
|
||||
})
|
||||
},
|
||||
openUserScope() {
|
||||
if (this.$refs.drawerUserScope) {
|
||||
this.$refs.drawerUserScope.groupId = this.formData.activityGroupId || ""
|
||||
this.$refs.drawerUserScope.userScopeDialog = true
|
||||
}
|
||||
},
|
||||
async handleActivityGroupChange() {
|
||||
if (this.formData.activityGroupId !== null && this.formData.activityGroupId !== undefined) {
|
||||
this.formData.activityGroupId = String(this.formData.activityGroupId)
|
||||
}
|
||||
await this.getActivityGroup()
|
||||
},
|
||||
resetSearch() {
|
||||
this.pageForm.year = ""
|
||||
this.pageForm.configName = ""
|
||||
this.doSearch()
|
||||
},
|
||||
defaultFormData() {
|
||||
return {
|
||||
year: moment().format("YYYY"),
|
||||
configName: "",
|
||||
tourType: "",
|
||||
activityGroupId: "",
|
||||
sortNo: 0,
|
||||
minGroupPeople: 0,
|
||||
maxGroupPeople: 0,
|
||||
outProvinceYears: 0,
|
||||
outProvinceRatio: 0,
|
||||
outProvinceRatioType: "当年报名人数",
|
||||
outProvinceFixedPeople: 0,
|
||||
cycleStartYear: "",
|
||||
cycleEndYear: "",
|
||||
cycleTotalCost: null,
|
||||
cycleAllowedTimes: null,
|
||||
allowFamily: false,
|
||||
fillBedInfo: true,
|
||||
enabled: true,
|
||||
lots: [],
|
||||
serviceNotice: ""
|
||||
}
|
||||
},
|
||||
openAdd() {
|
||||
this.title = "新建疗休养配置"
|
||||
this.lotDeleteList = []
|
||||
this.activeTab = "basic"
|
||||
// 新建时给出默认年度和开关值,减少校工会管理员录入成本。
|
||||
this.formData = this.defaultFormData()
|
||||
this.dialogVisible = true
|
||||
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
|
||||
},
|
||||
inheritPreviousYearInfo() {
|
||||
const currentYear = parseInt(this.formData.year, 10)
|
||||
if (isNaN(currentYear)) {
|
||||
this.$message.warning("请先选择年度")
|
||||
return
|
||||
}
|
||||
this.inheritLoading = true
|
||||
this.$axios.post(loc() + "/previousYearInfo", {year: currentYear}).then((res) => {
|
||||
this.inheritLoading = false
|
||||
if (res.code !== 0) {
|
||||
this.$message.warning(res.msg || "未找到上一年度配置")
|
||||
return
|
||||
}
|
||||
const previous = res.data || {}
|
||||
const inheritedLots = (previous.lots || []).map(item => {
|
||||
return {
|
||||
lotName: item.lotName || "",
|
||||
lotValue: item.lotValue || "",
|
||||
activityCost: item.activityCost,
|
||||
allowOverReimbursement: !!item.allowOverReimbursement
|
||||
}
|
||||
})
|
||||
this.formData = Object.assign(this.defaultFormData(), previous, {
|
||||
year: String(currentYear),
|
||||
cycleStartYear: previous.cycleStartYear ? String(previous.cycleStartYear) : "",
|
||||
cycleEndYear: previous.cycleEndYear ? String(previous.cycleEndYear) : "",
|
||||
lots: inheritedLots
|
||||
})
|
||||
delete this.formData.id
|
||||
delete this.formData.createdAt
|
||||
delete this.formData.createdBy
|
||||
delete this.formData.updatedAt
|
||||
delete this.formData.updatedBy
|
||||
delete this.formData.delFlag
|
||||
if (!this.formData.outProvinceRatioType) {
|
||||
this.$set(this.formData, "outProvinceRatioType", "当年报名人数")
|
||||
}
|
||||
if (this.formData.outProvinceFixedPeople === null || this.formData.outProvinceFixedPeople === undefined) {
|
||||
this.$set(this.formData, "outProvinceFixedPeople", 0)
|
||||
}
|
||||
if (this.formData.fillBedInfo === null || this.formData.fillBedInfo === undefined) {
|
||||
this.$set(this.formData, "fillBedInfo", true)
|
||||
}
|
||||
this.lotDeleteList = []
|
||||
this.$message.success("已延用上一年配置信息")
|
||||
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
|
||||
}).catch(() => {
|
||||
this.inheritLoading = false
|
||||
})
|
||||
},
|
||||
openEdit(row) {
|
||||
this.title = "编辑疗休养配置"
|
||||
this.lotDeleteList = []
|
||||
this.activeTab = "basic"
|
||||
this.$axios.post(loc() + "/detail", {id: row.id}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.formData = Object.assign({
|
||||
activityGroupId: "",
|
||||
outProvinceRatioType: "当年报名人数",
|
||||
outProvinceFixedPeople: 0,
|
||||
cycleStartYear: "",
|
||||
cycleEndYear: "",
|
||||
cycleTotalCost: null,
|
||||
cycleAllowedTimes: null,
|
||||
allowFamily: false,
|
||||
fillBedInfo: true,
|
||||
enabled: true,
|
||||
lots: [],
|
||||
serviceNotice: ""
|
||||
}, res.data || {})
|
||||
this.formData.year = this.formData.year ? String(this.formData.year) : ""
|
||||
if (!this.formData.outProvinceRatioType) {
|
||||
this.$set(this.formData, "outProvinceRatioType", "当年报名人数")
|
||||
}
|
||||
if (this.formData.outProvinceFixedPeople === null || this.formData.outProvinceFixedPeople === undefined) {
|
||||
this.$set(this.formData, "outProvinceFixedPeople", 0)
|
||||
}
|
||||
if (this.formData.fillBedInfo === null || this.formData.fillBedInfo === undefined) {
|
||||
this.$set(this.formData, "fillBedInfo", true)
|
||||
}
|
||||
this.formData.cycleStartYear = this.formData.cycleStartYear ? String(this.formData.cycleStartYear) : ""
|
||||
this.formData.cycleEndYear = this.formData.cycleEndYear ? String(this.formData.cycleEndYear) : ""
|
||||
this.formData.lots = (this.formData.lots || []).map(item => Object.assign({}, item, {
|
||||
allowOverReimbursement: !!item.allowOverReimbursement
|
||||
}))
|
||||
this.dialogVisible = true
|
||||
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
|
||||
} else {
|
||||
this.$message.warning(res.msg || "查询失败")
|
||||
}
|
||||
})
|
||||
},
|
||||
doSubmit() {
|
||||
this.$refs.form.validate((valid) => {
|
||||
if (!valid) return
|
||||
if (!this.validateLots()) return
|
||||
this.submitLoading = true
|
||||
// Nutz 对子表集合按字符串化 JSON 绑定更稳定,和体检项目维护的提交方式保持一致。
|
||||
const submitData = JSON.parse(JSON.stringify(this.formData))
|
||||
if (submitData.outProvinceRatioType !== "固定人数") {
|
||||
submitData.outProvinceFixedPeople = 0
|
||||
}
|
||||
submitData.lots = JSON.stringify(this.formData.lots || [])
|
||||
submitData.lotDeleteList = JSON.stringify(this.lotDeleteList)
|
||||
this.$axios.post(loc() + "/doSubmit", submitData).then((res) => {
|
||||
this.submitLoading = false
|
||||
if (res.code === 0) {
|
||||
this.$message.success("保存成功")
|
||||
this.dialogVisible = false
|
||||
this.pageData()
|
||||
} else {
|
||||
this.$message.warning(res.msg || "保存失败")
|
||||
}
|
||||
}).catch(() => {
|
||||
this.submitLoading = false
|
||||
})
|
||||
})
|
||||
},
|
||||
addLot() {
|
||||
if (!this.formData.lots) {
|
||||
this.$set(this.formData, "lots", [])
|
||||
}
|
||||
this.formData.lots.push({
|
||||
lotName: "",
|
||||
lotValue: "",
|
||||
activityCost: null,
|
||||
allowOverReimbursement: false
|
||||
})
|
||||
},
|
||||
integerInput(row, field, value) {
|
||||
const nextValue = String(value || "").replace(/[^\d]/g, "")
|
||||
this.$set(row, field, nextValue)
|
||||
},
|
||||
outProvinceRatioTypeChange(value) {
|
||||
if (value !== "固定人数") {
|
||||
this.$set(this.formData, "outProvinceFixedPeople", 0)
|
||||
}
|
||||
this.$nextTick(() => this.$refs.form && this.$refs.form.validateField("outProvinceFixedPeople"))
|
||||
},
|
||||
validateLots() {
|
||||
const lots = this.formData.lots || []
|
||||
for (let i = 0; i < lots.length; i++) {
|
||||
const row = lots[i]
|
||||
const hasContent = row && (row.lotName || row.lotValue || row.activityCost !== null && row.activityCost !== undefined && row.activityCost !== "")
|
||||
if (!hasContent) continue
|
||||
if (!/^\d+$/.test(String(row.lotValue || ""))) {
|
||||
this.$message.warning("第" + (i + 1) + "行标段值必须为整数")
|
||||
return false
|
||||
}
|
||||
if (!/^\d+$/.test(String(row.activityCost === null || row.activityCost === undefined ? "" : row.activityCost))) {
|
||||
this.$message.warning("第" + (i + 1) + "行标段费用必须为整数")
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
deleteLot(index, row) {
|
||||
this.$confirm("确定删除该标段吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
if (row && row.id) {
|
||||
this.lotDeleteList.push(row.id)
|
||||
}
|
||||
this.formData.lots.splice(index, 1)
|
||||
})
|
||||
},
|
||||
doDelete(row) {
|
||||
this.$confirm("确定删除【" + row.configName + "】吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
this.$axios.post(loc() + "/doDelete", {id: row.id}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success("删除成功")
|
||||
this.pageData()
|
||||
} else {
|
||||
this.$message.warning(res.msg || "删除失败")
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
destroyEditor() {
|
||||
this.formData = {}
|
||||
this.lotDeleteList = []
|
||||
this.inheritLoading = false
|
||||
this.activeTab = "basic"
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getActivityGroup()
|
||||
this.pageData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.tour-user-scope {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tour-user-scope .el-select {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tour-setting-inherit {
|
||||
margin-bottom: -40px;
|
||||
padding-right: 96px;
|
||||
position: relative;
|
||||
text-align: right;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.tour-setting-basic-divider {
|
||||
border-top: 1px dashed #dcdfe6;
|
||||
margin: 2px 0 18px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,338 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<el-card shadow="never">
|
||||
<search :is-search-button="false">
|
||||
<search-item label="年度">
|
||||
<el-date-picker
|
||||
v-model="pageForm.year"
|
||||
type="year"
|
||||
value-format="yyyy"
|
||||
placeholder="请选择年度"
|
||||
style="width: 100%">
|
||||
</el-date-picker>
|
||||
</search-item>
|
||||
<search-item label="旅行社名称">
|
||||
<el-input
|
||||
v-model="pageForm.agencyName"
|
||||
clearable
|
||||
placeholder="请输入旅行社名称"
|
||||
style="width: 100%"
|
||||
@keyup.enter.native="doSearch">
|
||||
</el-input>
|
||||
</search-item>
|
||||
<search-item label="联系人">
|
||||
<el-input
|
||||
v-model="pageForm.contactName"
|
||||
clearable
|
||||
placeholder="请输入联系人"
|
||||
style="width: 100%"
|
||||
@keyup.enter.native="doSearch">
|
||||
</el-input>
|
||||
</search-item>
|
||||
<search-item label="联系电话">
|
||||
<el-input
|
||||
v-model="pageForm.contactPhone"
|
||||
clearable
|
||||
placeholder="请输入联系电话"
|
||||
style="width: 100%"
|
||||
@keyup.enter.native="doSearch">
|
||||
</el-input>
|
||||
</search-item>
|
||||
<div class="search-query" style="margin-left: auto;">
|
||||
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
|
||||
<el-button size="medium" @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mt10" shadow="never">
|
||||
<table-tool :app="this" label="旅行社列表">
|
||||
<el-button @click="openAdd" size="medium" type="primary">
|
||||
<i class="el-icon-plus"></i>
|
||||
新增
|
||||
</el-button>
|
||||
</table-tool>
|
||||
|
||||
<el-table
|
||||
v-loading="tableLoading"
|
||||
:data="tableData"
|
||||
:size="tableSize"
|
||||
border
|
||||
@sort-change="pageOrder">
|
||||
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="年度" prop="year" width="110" sortable="custom" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="旅行社编号" prop="agencyCode" width="120" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="旅行社名称" prop="agencyName" min-width="220" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="联系人" prop="contactName" width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="联系电话" prop="contactPhone" width="150" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="激活状态" prop="enabled" width="160" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-switch
|
||||
v-model="row.enabled"
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
@change="toggleEnabled(row)">
|
||||
</el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="260" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-button size="mini" type="primary" @click="openView(row)">查看</el-button>
|
||||
<el-button size="mini" type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button size="mini" type="danger" @click="doDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
</guava>
|
||||
|
||||
<el-dialog
|
||||
:title="title"
|
||||
:visible.sync="dialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
width="72%"
|
||||
@closed="destroyForm">
|
||||
<el-form :model="formData" :rules="formRules" label-width="110px" ref="form" :disabled="viewMode">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="年度" prop="year">
|
||||
<el-date-picker
|
||||
v-model="formData.year"
|
||||
type="year"
|
||||
value-format="yyyy"
|
||||
placeholder="请选择年度"
|
||||
style="width: 100%">
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="旅行社名称" prop="agencyName">
|
||||
<el-input v-model="formData.agencyName" maxlength="30" show-word-limit placeholder="请输入旅行社名称"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="旅行社编号" prop="agencyCode">
|
||||
<el-input v-model="formData.agencyCode" maxlength="50" placeholder="请输入旅行社编号"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="激活状态" prop="enabled">
|
||||
<el-radio-group v-model="formData.enabled">
|
||||
<el-radio :label="true">启用</el-radio>
|
||||
<el-radio :label="false">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="联系人" prop="contactName">
|
||||
<el-input v-model="formData.contactName" maxlength="10" show-word-limit placeholder="请输入联系人"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="联系人手机" prop="contactPhone">
|
||||
<el-input v-model="formData.contactPhone" maxlength="30" placeholder="请输入联系人手机"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="formData.email" maxlength="100" placeholder="请输入邮箱"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" type="textarea" maxlength="100" :rows="3" show-word-limit placeholder="请输入备注"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="移动端缩略图" prop="mobileThumb">
|
||||
<!-- 复用课程管理的封面上传逻辑:单图上传,保存URL,供移动端列表展示使用。 -->
|
||||
<file-upload
|
||||
style="--upload-width: 200px;--upload-height:108px"
|
||||
:upload_number="1"
|
||||
:upload_size="20971520"
|
||||
:value.sync="formData.mobileThumb"
|
||||
accept=".jpg,.jpeg,.png"
|
||||
complete_result
|
||||
upload_mode="image"
|
||||
upload_result_category="interval">
|
||||
</file-upload>
|
||||
<div class="el-upload__tip">支持jpg、jpeg、png格式,大小不超过20MB。</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button v-if="!viewMode" type="primary" :loading="subDis" @click="doSubmit">保存</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
const currentYear = moment().format("YYYY")
|
||||
const validateMobile = (rule, value, callback) => {
|
||||
const mobileReg = /^1[3-9]\d{9}$/
|
||||
if (!value) {
|
||||
callback(new Error("必填"))
|
||||
} else if (!mobileReg.test(value)) {
|
||||
callback(new Error("手机号格式不正确"))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
const validateEmail = (rule, value, callback) => {
|
||||
const emailReg = /^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$/
|
||||
if (!value) {
|
||||
callback(new Error("必填"))
|
||||
} else if (!emailReg.test(value)) {
|
||||
callback(new Error("邮箱格式不正确"))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
return {
|
||||
title: "",
|
||||
dialogVisible: false,
|
||||
viewMode: false,
|
||||
subDis: false,
|
||||
pageForm: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
totalCount: 0,
|
||||
year: currentYear,
|
||||
agencyName: "",
|
||||
contactName: "",
|
||||
contactPhone: ""
|
||||
},
|
||||
formData: {},
|
||||
formRules: {
|
||||
year: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
agencyName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
agencyCode: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
contactName: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
|
||||
contactPhone: [{ validator: validateMobile, trigger: ["blur", "change"] }],
|
||||
email: [{ validator: validateEmail, trigger: ["blur", "change"] }]
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetSearch() {
|
||||
this.pageForm.year = moment().format("YYYY")
|
||||
this.pageForm.agencyName = ""
|
||||
this.pageForm.contactName = ""
|
||||
this.pageForm.contactPhone = ""
|
||||
this.doSearch()
|
||||
},
|
||||
emptyForm() {
|
||||
return {
|
||||
year: moment().format("YYYY"),
|
||||
agencyName: "",
|
||||
agencyCode: "",
|
||||
contactName: "",
|
||||
contactPhone: "",
|
||||
email: "",
|
||||
remark: "",
|
||||
mobileThumb: "",
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
openAdd() {
|
||||
this.title = "新增旅行社信息"
|
||||
this.viewMode = false
|
||||
this.formData = this.emptyForm()
|
||||
this.dialogVisible = true
|
||||
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
|
||||
},
|
||||
openEdit(row) {
|
||||
this.title = "编辑旅行社信息"
|
||||
this.viewMode = false
|
||||
this.loadDetail(row.id)
|
||||
},
|
||||
openView(row) {
|
||||
this.title = "查看旅行社信息"
|
||||
this.viewMode = true
|
||||
this.loadDetail(row.id)
|
||||
},
|
||||
loadDetail(id) {
|
||||
this.$axios.post(loc() + "/detail", { id }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.formData = Object.assign(this.emptyForm(), res.data || {})
|
||||
this.formData.year = this.formData.year ? String(this.formData.year) : ""
|
||||
this.dialogVisible = true
|
||||
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
|
||||
} else {
|
||||
this.$message.warning(res.msg || "查询失败")
|
||||
}
|
||||
})
|
||||
},
|
||||
doSubmit() {
|
||||
this.$refs.form.validate((valid) => {
|
||||
if (!valid) return
|
||||
this.subDis = true
|
||||
this.$axios.post(loc() + "/doSubmit", this.formData).then((res) => {
|
||||
this.subDis = false
|
||||
if (res.code === 0) {
|
||||
this.$message.success("保存成功")
|
||||
this.dialogVisible = false
|
||||
this.pageData()
|
||||
} else {
|
||||
this.$message.warning(res.msg || "保存失败")
|
||||
}
|
||||
}).catch(() => {
|
||||
this.subDis = false
|
||||
})
|
||||
})
|
||||
},
|
||||
toggleEnabled(row) {
|
||||
this.$axios.post(loc() + "/doSubmit", row).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success("状态已更新")
|
||||
} else {
|
||||
row.enabled = !row.enabled
|
||||
this.$message.warning(res.msg || "状态更新失败")
|
||||
}
|
||||
}).catch(() => {
|
||||
row.enabled = !row.enabled
|
||||
})
|
||||
},
|
||||
doDelete(row) {
|
||||
this.$confirm("确定删除【" + row.agencyName + "】吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
this.$axios.post(loc() + "/doDelete", { id: row.id }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success("删除成功")
|
||||
this.pageData()
|
||||
} else {
|
||||
this.$message.warning(res.msg || "删除失败")
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
destroyForm() {
|
||||
this.formData = {}
|
||||
this.viewMode = false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.pageData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,547 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<el-card shadow="never">
|
||||
<search :is-search-button="false">
|
||||
<search-item label="开始年度">
|
||||
<el-date-picker
|
||||
v-model="pageForm.startYear"
|
||||
type="year"
|
||||
value-format="yyyy"
|
||||
placeholder="请选择开始年度"
|
||||
style="width: 100%">
|
||||
</el-date-picker>
|
||||
</search-item>
|
||||
<search-item label="结束年度">
|
||||
<el-date-picker
|
||||
v-model="pageForm.endYear"
|
||||
type="year"
|
||||
value-format="yyyy"
|
||||
placeholder="请选择结束年度"
|
||||
style="width: 100%">
|
||||
</el-date-picker>
|
||||
</search-item>
|
||||
<search-item label="姓名/工号">
|
||||
<el-input
|
||||
v-model="pageForm.keyword"
|
||||
clearable
|
||||
placeholder="请输入姓名或工号"
|
||||
style="width: 100%"
|
||||
@keyup.enter.native="doSearch">
|
||||
</el-input>
|
||||
</search-item>
|
||||
<search-item label="所属工会">
|
||||
<el-select v-model="pageForm.unionId" clearable filterable placeholder="请选择所属工会" style="width: 100%">
|
||||
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="线路">
|
||||
<el-select v-model="pageForm.lineId" clearable filterable placeholder="请选择线路" style="width: 100%">
|
||||
<el-option v-for="item in lineOptions" :key="item.lineId" :label="item.lineName" :value="item.lineId"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="出行时段">
|
||||
<el-select v-model="pageForm.travelPeriod" clearable filterable placeholder="请选择出行时段" style="width: 100%">
|
||||
<el-option v-for="item in travelPeriodOptions" :key="item.travelPeriod" :label="item.travelPeriod" :value="item.travelPeriod"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="线路类型">
|
||||
<el-select v-model="pageForm.lineType" clearable filterable placeholder="请选择线路类型" style="width: 100%">
|
||||
<el-option v-for="item in lineTypeOptions" :key="item.lineType" :label="item.lineType" :value="item.lineType"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<div class="search-query" style="margin-left: auto;">
|
||||
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
|
||||
<el-button size="medium" @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mt10" shadow="never">
|
||||
<table-tool :app="this" label="分工会审核">
|
||||
<el-radio-group class="mr5" size="small" v-model="pageForm.audit" @change="changeAudit">
|
||||
<el-radio-button :label="true">已审核</el-radio-button>
|
||||
<el-radio-button :label="false">未审核</el-radio-button>
|
||||
</el-radio-group>
|
||||
</table-tool>
|
||||
<el-table
|
||||
v-loading="tableLoading"
|
||||
:data="tableData"
|
||||
:size="tableSize"
|
||||
border
|
||||
@sort-change="pageOrder">
|
||||
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="工号" prop="jobNo" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="姓名" prop="userName" width="120" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="所在单位" prop="unitName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="报名线路" prop="lineName" min-width="220" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="出行时段" prop="travelPeriod" min-width="220" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="线路类型" prop="lineType" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="报名时间" prop="signupTime" width="170" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="携带家属" prop="familyCount" width="120" sortable="custom" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag size="mini" :type="Number(row.familyCount || 0) > 0 ? 'success' : 'info'">{{ row.familyCount || 0 }}人</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="报销超出费用" prop="overCostReimbursed" width="140" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag size="mini" :type="row.overCostReimbursed ? 'success' : 'info'">{{ row.overCostReimbursed ? '是' : '否' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="当前节点" prop="curTaskName" width="140" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="流程状态" prop="instanceState" width="120" sortable="custom" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<enum-tag :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" align="center" header-align="center" fixed="right">
|
||||
<template slot-scope="{row}">
|
||||
<el-button size="mini" type="primary" @click="openView(row)">查看</el-button>
|
||||
<el-button v-if="row.taskState === 10" size="mini" type="primary" @click="openApproval(row)">审核</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
|
||||
<template #edit>
|
||||
<tour-approval-info ref="infoRef">
|
||||
<div v-if="showApprovalForm">
|
||||
<div class="process-title">
|
||||
{{ formData.taskName }}
|
||||
</div>
|
||||
<el-form
|
||||
:model="formData"
|
||||
ref="formRef"
|
||||
label-width="0"
|
||||
label-suffix=":"
|
||||
class="flow-task-form">
|
||||
<el-form-item
|
||||
label="审批意见"
|
||||
prop="tf_opinion"
|
||||
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
|
||||
<user-opinion-textarea v-model="formData.tf_opinion"></user-opinion-textarea>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-row type="flex" justify="end">
|
||||
<el-button @click="$refs.guava.index()" size="small">取消</el-button>
|
||||
<el-button @click="handleTaskAction(6)" size="small" type="info">退回到发起人</el-button>
|
||||
<el-button @click="handleTaskAction(2)" size="small" type="danger">拒绝申请</el-button>
|
||||
<el-button @click="handleTaskAction(1)" size="small" type="primary">同意申请</el-button>
|
||||
</el-row>
|
||||
</div>
|
||||
</tour-approval-info>
|
||||
</template>
|
||||
</guava>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#app .search .search-item {
|
||||
width: calc((100% - 150px) / 4);
|
||||
}
|
||||
#app .search .search-query {
|
||||
margin-left: auto;
|
||||
}
|
||||
@media screen and (max-width: 1350px) {
|
||||
#app .search .search-item {
|
||||
width: calc((100% - 100px) / 3);
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 1200px) {
|
||||
#app .search .search-item {
|
||||
width: calc((100% - 50px) / 2);
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 992px) {
|
||||
#app .search .search-item {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.tour-approval-section {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.tour-approval-table {
|
||||
width: 100%;
|
||||
}
|
||||
.tour-approval-empty {
|
||||
color: #909399;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
const TOUR_APPROVAL_INFO = {
|
||||
template: /*language=HTML*/ `
|
||||
<div>
|
||||
<div class="process-title">
|
||||
报名信息
|
||||
<el-link type="primary" @click="openChart">点击查看流程图</el-link>
|
||||
</div>
|
||||
<el-descriptions :column="3" border class="tour-approval-section">
|
||||
<el-descriptions-item label="工号">{{ detail.jobNo || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="姓名">{{ detail.userName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="性别">{{ detail.gender || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="身份证号">{{ detail.idCard || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所在单位">{{ detail.unitName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所在工会">{{ detail.unionName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="报名时间">{{ detail.signupTime || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="报名线路">{{ detail.lineName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="出行时段">{{ detail.travelPeriod || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="线路类型">{{ detail.lineType || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="报名酒店">{{ detail.hotelName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="旅行社">{{ detail.travelAgencyName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="是否携带家属">
|
||||
{{ familyText() }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="报销超出费用">{{ detail.overCostReimbursed ? '是' : '否' }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="fillBedInfo" label="意向拼床人">{{ detail.intendedRoommate || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="fillBedInfo" label="床型">{{ detail.bedType || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="fillBedInfo" label="床位信息">{{ detail.bedInfo || '' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div v-if="isDirectFamilyLine()" class="tour-approval-section">
|
||||
<div class="process-title">直系亲属线路</div>
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item label="亲属姓名">{{ directRelative.relativeName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所在单位">{{ directRelative.unitName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="亲属关系">{{ directRelative.relationshipName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="线路名称">{{ directRelative.lineName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="出行开始日期">{{ directRelative.travelStartTime || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="出行结束日期">{{ directRelative.travelEndTime || '' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div v-if="showFamilySection()" class="tour-approval-section">
|
||||
<div class="process-title">亲属信息</div>
|
||||
<el-table :data="familyData" border size="mini" empty-text="暂无亲属信息" class="tour-approval-table">
|
||||
<el-table-column label="序号" type="index" width="70" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="家属姓名" prop="familyName" min-width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="性别" prop="gender" width="90" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="身份证号码" prop="idCard" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="年龄" prop="age" width="90" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="关系" prop="relationship" width="110" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column v-if="fillBedInfo" label="床型" prop="bedType" width="110" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column v-if="fillBedInfo" label="床位" prop="bedInfo" min-width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column v-if="fillBedInfo" label="意向拼床人" prop="intendedRoommate" min-width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<template v-for="task in doneTasks">
|
||||
<div class="mt10">
|
||||
<div class="process-title">{{ task.displayName }}</div>
|
||||
<el-descriptions border class="flow-task-form" :column="3" :key="task.id" v-if="task.ext.isFirstTaskNode">
|
||||
<el-descriptions-item label="申请用户">
|
||||
{{ task.ext.initiatorName }}({{ task.ext.initiatorAccount }})
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="申请时间">{{ task.finishTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="办理结果">
|
||||
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-descriptions border class="flow-task-form" :column="3" :key="task.id" v-else>
|
||||
<el-descriptions-item label="办理用户">
|
||||
{{ task.taskFormData.userName }}({{ task.taskFormData.loginName }})
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="办理时间">{{ task.finishTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="办理结果">
|
||||
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="办理意见" :span="3">
|
||||
{{ task.taskFormData.opinion || task.taskFormData.tf_opinion || '' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<slot></slot>
|
||||
|
||||
<snaker-chart ref="snakerChartRef"></snaker-chart>
|
||||
</div>
|
||||
`,
|
||||
dicts: ["PROCESS_TASK_SUBMIT_TYPE"],
|
||||
data() {
|
||||
return {
|
||||
row: {},
|
||||
detail: {},
|
||||
familyData: [],
|
||||
directRelative: {},
|
||||
allowFamily: false,
|
||||
fillBedInfo: true,
|
||||
directFamilyUnitLine: false,
|
||||
doneTasks: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onOpen(row) {
|
||||
this.row = row || {}
|
||||
this.detail = {}
|
||||
this.familyData = []
|
||||
this.directRelative = {}
|
||||
this.allowFamily = false
|
||||
this.fillBedInfo = true
|
||||
this.directFamilyUnitLine = false
|
||||
this.doneTasks = []
|
||||
this.info()
|
||||
this.getDoneTasks()
|
||||
},
|
||||
info() {
|
||||
this.$axios.post(loc() + "/detail", { id: this.row.id }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
const data = res.data || {}
|
||||
this.detail = data.ledger || {}
|
||||
this.detail.travelPeriod = data.travelPeriod || this.row.travelPeriod || ""
|
||||
this.familyData = data.families || []
|
||||
this.directRelative = data.directRelative || {}
|
||||
this.allowFamily = this.toBoolean(data.allowFamily)
|
||||
this.fillBedInfo = data.fillBedInfo === undefined || data.fillBedInfo === null || this.toBoolean(data.fillBedInfo)
|
||||
this.directFamilyUnitLine = this.toBoolean(data.directFamilyUnitLine)
|
||||
} else {
|
||||
this.$message.warning(res.msg || "查询失败")
|
||||
}
|
||||
})
|
||||
},
|
||||
toBoolean(value) {
|
||||
return value === true || value === 1 || value === "1"
|
||||
},
|
||||
isDirectFamilyLine() {
|
||||
return this.directFamilyUnitLine
|
||||
|| this.toBoolean(this.detail.directFamilyUnitLine)
|
||||
|| (this.directRelative && this.directRelative.id)
|
||||
},
|
||||
hasFamily() {
|
||||
return this.toBoolean(this.detail.hasFamily) || this.familyData.length > 0
|
||||
},
|
||||
showFamilySection() {
|
||||
return this.allowFamily && this.hasFamily() && !this.isDirectFamilyLine()
|
||||
},
|
||||
familyText() {
|
||||
if (this.isDirectFamilyLine()) {
|
||||
return "否"
|
||||
}
|
||||
if (!this.allowFamily) {
|
||||
return "否"
|
||||
}
|
||||
return this.hasFamily() ? "是" : "否"
|
||||
},
|
||||
getDoneTasks() {
|
||||
this.$axios.post("/flow/common/doneTasks", { bizId: this.row.id }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.doneTasks = res.data || []
|
||||
}
|
||||
})
|
||||
},
|
||||
openChart() {
|
||||
if (!this.row.instanceProcessDefineId || !this.row.instanceId) {
|
||||
this.$message.warning("暂无流程图信息")
|
||||
return
|
||||
}
|
||||
this.$refs.snakerChartRef.onOpenFull(this.row.instanceProcessDefineId, this.row.instanceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: "#app",
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
const currentYear = moment().format("YYYY")
|
||||
return {
|
||||
unionOptions: [],
|
||||
lineOptions: [],
|
||||
travelPeriodOptions: [],
|
||||
lineTypeOptions: [],
|
||||
showApprovalForm: false,
|
||||
formData: {
|
||||
tf_opinion: ""
|
||||
},
|
||||
pageForm: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
totalCount: 0,
|
||||
pageOrderName: "signupTime",
|
||||
pageOrderBy: "descending",
|
||||
audit: false,
|
||||
startYear: currentYear,
|
||||
endYear: currentYear,
|
||||
keyword: "",
|
||||
unionId: "",
|
||||
lineId: "",
|
||||
travelPeriod: "",
|
||||
lineType: ""
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetSearch() {
|
||||
this.pageForm.startYear = moment().format("YYYY")
|
||||
this.pageForm.endYear = moment().format("YYYY")
|
||||
this.pageForm.keyword = ""
|
||||
this.pageForm.unionId = ""
|
||||
this.pageForm.lineId = ""
|
||||
this.pageForm.travelPeriod = ""
|
||||
this.pageForm.lineType = ""
|
||||
this.loadOptions()
|
||||
this.doSearch()
|
||||
},
|
||||
changeAudit() {
|
||||
this.pageForm.pageNumber = 1
|
||||
this.clearCascadeFilters()
|
||||
this.loadOptions()
|
||||
this.pageData()
|
||||
},
|
||||
clearCascadeFilters() {
|
||||
this.pageForm.unionId = ""
|
||||
this.pageForm.lineId = ""
|
||||
this.pageForm.travelPeriod = ""
|
||||
this.pageForm.lineType = ""
|
||||
},
|
||||
openView(row) {
|
||||
this.$refs.guava.edit(() => {
|
||||
this.showApprovalForm = false
|
||||
this.formData = {
|
||||
tf_opinion: ""
|
||||
}
|
||||
this.$refs.infoRef.onOpen(row)
|
||||
})
|
||||
},
|
||||
openApproval(row) {
|
||||
this.$refs.guava.edit(() => {
|
||||
this.showApprovalForm = true
|
||||
this.formData = {
|
||||
processTaskId: row.taskId,
|
||||
taskName: row.curTaskName || row.taskName,
|
||||
tf_opinion: ""
|
||||
}
|
||||
this.$refs.infoRef.onOpen(row)
|
||||
})
|
||||
},
|
||||
handleTaskAction(val) {
|
||||
this.$refs.formRef.validate((valid) => {
|
||||
if (!valid) return
|
||||
this.$confirm("您确定要提交吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
const loading = createLoading()
|
||||
this.$axios.post("/flow/common/executeTask", {
|
||||
data: JSON.stringify({
|
||||
...this.formData,
|
||||
submitType: val
|
||||
})
|
||||
}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$refs.guava.index()
|
||||
this.$message.success(res.msg || "操作成功")
|
||||
this.doSearch()
|
||||
} else {
|
||||
this.$message.warning(res.msg || "操作失败")
|
||||
}
|
||||
}).finally(() => {
|
||||
loading.close()
|
||||
})
|
||||
}).catch(() => {})
|
||||
})
|
||||
},
|
||||
loadOptions() {
|
||||
this.loadUnionOptions()
|
||||
this.loadLineOptions()
|
||||
this.loadTravelPeriodOptions()
|
||||
this.loadLineTypeOptions()
|
||||
},
|
||||
baseOptionParams() {
|
||||
return {
|
||||
audit: this.pageForm.audit,
|
||||
startYear: this.pageForm.startYear,
|
||||
endYear: this.pageForm.endYear,
|
||||
keyword: this.pageForm.keyword,
|
||||
unionId: this.pageForm.unionId,
|
||||
lineId: this.pageForm.lineId,
|
||||
travelPeriod: this.pageForm.travelPeriod,
|
||||
lineType: this.pageForm.lineType
|
||||
}
|
||||
},
|
||||
loadUnionOptions() {
|
||||
this.$axios.post(loc() + "/unionOptions", this.baseOptionParams()).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.unionOptions = res.data || []
|
||||
if (this.pageForm.unionId && !this.unionOptions.some(item => item.id === this.pageForm.unionId)) {
|
||||
this.pageForm.unionId = ""
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
loadLineOptions() {
|
||||
this.$axios.post(loc() + "/lineOptions", this.baseOptionParams()).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.lineOptions = res.data || []
|
||||
if (this.pageForm.lineId && !this.lineOptions.some(item => item.lineId === this.pageForm.lineId)) {
|
||||
this.pageForm.lineId = ""
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
loadTravelPeriodOptions() {
|
||||
this.$axios.post(loc() + "/travelPeriodOptions", this.baseOptionParams()).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.travelPeriodOptions = res.data || []
|
||||
if (this.pageForm.travelPeriod && !this.travelPeriodOptions.some(item => item.travelPeriod === this.pageForm.travelPeriod)) {
|
||||
this.pageForm.travelPeriod = ""
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
loadLineTypeOptions() {
|
||||
this.$axios.post(loc() + "/lineTypeOptions", this.baseOptionParams()).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.lineTypeOptions = res.data || []
|
||||
if (this.pageForm.lineType && !this.lineTypeOptions.some(item => item.lineType === this.pageForm.lineType)) {
|
||||
this.pageForm.lineType = ""
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadOptions()
|
||||
this.pageData()
|
||||
},
|
||||
components: {
|
||||
"tour-approval-info": TOUR_APPROVAL_INFO
|
||||
},
|
||||
watch: {
|
||||
"pageForm.startYear"() {
|
||||
this.clearCascadeFilters()
|
||||
this.loadOptions()
|
||||
},
|
||||
"pageForm.endYear"() {
|
||||
this.clearCascadeFilters()
|
||||
this.loadOptions()
|
||||
},
|
||||
"pageForm.lineId"() {
|
||||
this.loadTravelPeriodOptions()
|
||||
this.loadLineTypeOptions()
|
||||
},
|
||||
"pageForm.travelPeriod"() {
|
||||
this.loadLineOptions()
|
||||
this.loadLineTypeOptions()
|
||||
},
|
||||
"pageForm.lineType"() {
|
||||
this.loadLineOptions()
|
||||
this.loadTravelPeriodOptions()
|
||||
},
|
||||
"pageForm.unionId"() {
|
||||
this.loadLineOptions()
|
||||
this.loadTravelPeriodOptions()
|
||||
this.loadLineTypeOptions()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,609 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<guava ref="guava">
|
||||
<el-card shadow="never">
|
||||
<search :is-search-button="false">
|
||||
<search-item label="开始年度">
|
||||
<el-date-picker
|
||||
v-model="pageForm.startYear"
|
||||
type="year"
|
||||
value-format="yyyy"
|
||||
placeholder="请选择开始年度"
|
||||
style="width: 100%">
|
||||
</el-date-picker>
|
||||
</search-item>
|
||||
<search-item label="结束年度">
|
||||
<el-date-picker
|
||||
v-model="pageForm.endYear"
|
||||
type="year"
|
||||
value-format="yyyy"
|
||||
placeholder="请选择结束年度"
|
||||
style="width: 100%">
|
||||
</el-date-picker>
|
||||
</search-item>
|
||||
<search-item label="姓名/工号">
|
||||
<el-input
|
||||
v-model="pageForm.keyword"
|
||||
clearable
|
||||
placeholder="请输入姓名或工号"
|
||||
style="width: 100%"
|
||||
@keyup.enter.native="doSearch">
|
||||
</el-input>
|
||||
</search-item>
|
||||
<search-item label="所属工会">
|
||||
<el-select v-model="pageForm.unionId" clearable filterable placeholder="请选择所属工会" style="width: 100%">
|
||||
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="线路">
|
||||
<el-select v-model="pageForm.lineId" clearable filterable placeholder="请选择线路" style="width: 100%">
|
||||
<el-option v-for="item in lineOptions" :key="item.lineId" :label="item.lineName" :value="item.lineId"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="出行时段">
|
||||
<el-select v-model="pageForm.travelPeriod" clearable filterable placeholder="请选择出行时段" style="width: 100%">
|
||||
<el-option v-for="item in travelPeriodOptions" :key="item.travelPeriod" :label="item.travelPeriod" :value="item.travelPeriod"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="线路类型">
|
||||
<el-select v-model="pageForm.lineType" clearable filterable placeholder="请选择线路类型" style="width: 100%">
|
||||
<el-option v-for="item in lineTypeOptions" :key="item.lineType" :label="item.lineType" :value="item.lineType"></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<div class="search-query" style="margin-left: auto;">
|
||||
<el-button size="medium" type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
|
||||
<el-button size="medium" @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
</search>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mt10" shadow="never">
|
||||
<div class="tour-ledger-toolbar">
|
||||
<div class="tour-ledger-toolbar-title">
|
||||
<table-tool :app="this" label="分工会台账"></table-tool>
|
||||
</div>
|
||||
<div class="tour-ledger-scope">
|
||||
<el-button
|
||||
class="tour-scope-btn"
|
||||
size="medium"
|
||||
:class="{'is-active': pageForm.scopeType === 'ownUnionJoined'}"
|
||||
@click="setScopeType('ownUnionJoined')">
|
||||
本工会参加疗休养教职工({{ summaryStats.ownUnionJoinedStaffCount || 0 }})
|
||||
</el-button>
|
||||
<el-button
|
||||
class="tour-scope-btn"
|
||||
size="medium"
|
||||
:class="{'is-active': pageForm.scopeType === 'organizedLineJoined'}"
|
||||
@click="setScopeType('organizedLineJoined')">
|
||||
参加本工会组织线路人员(含其他工会及家属)({{ summaryStats.organizedLineJoinedTotalCount || 0 }})
|
||||
</el-button>
|
||||
<el-button
|
||||
class="tour-scope-btn"
|
||||
size="medium"
|
||||
:class="{'is-active': pageForm.overCostOnly}"
|
||||
@click="setOverCostOnly">
|
||||
超出费用由单位承担
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="pageForm.overCostOnly"
|
||||
type="primary"
|
||||
size="medium"
|
||||
icon="el-icon-download"
|
||||
@click="downloadOverCostSummary">
|
||||
导出
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="tableLoading"
|
||||
:data="tableData"
|
||||
:size="tableSize"
|
||||
border
|
||||
@sort-change="pageOrder">
|
||||
<el-table-column label="序号" type="index" :index="indexMethod" width="80" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="工号" prop="jobNo" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="姓名" prop="userName" width="120" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="所在单位" prop="unitName" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="报名线路" prop="lineName" min-width="220" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="出行时段" prop="travelPeriod" min-width="260" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="线路类型" prop="lineType" width="130" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="报名时间" prop="signupTime" width="170" sortable="custom" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="携带家属" prop="familyCount" width="120" sortable="custom" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag size="mini" :type="Number(row.familyCount || 0) > 0 ? 'success' : 'info'">{{ row.familyCount || 0 }}人</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="是否参加" prop="joined" width="120" sortable="custom" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag size="mini" :type="row.joined ? 'success' : 'info'">{{ row.joined ? '是' : '否' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="报销超出费用" prop="overCostReimbursed" width="140" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
<el-tag size="mini" :type="row.overCostReimbursed ? 'success' : 'info'">{{ row.overCostReimbursed ? '是' : '否' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" align="center" header-align="center" fixed="right">
|
||||
<template slot-scope="{row}">
|
||||
<el-button size="mini" type="primary" @click="openView(row)">查看</el-button>
|
||||
<el-button size="mini" type="danger" @click="doDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
</guava>
|
||||
|
||||
<el-dialog
|
||||
title="台账详情"
|
||||
:visible.sync="detailVisible"
|
||||
:close-on-click-modal="false"
|
||||
width="72%">
|
||||
<div class="tour-ledger-section">
|
||||
<div class="tour-ledger-title">教职工信息</div>
|
||||
<el-descriptions :column="3" border size="medium">
|
||||
<el-descriptions-item label="工号">{{ detail.jobNo || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="姓名">{{ detail.userName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="性别">{{ detail.gender || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="身份证号">{{ detail.idCard || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所在单位">{{ detail.unitName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所在工会">{{ detail.unionName || '' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div class="tour-ledger-section mt10">
|
||||
<div class="tour-ledger-title">
|
||||
报名信息
|
||||
<el-link v-if="detailHasWorkflow()" type="primary" @click="openChart">点击查看流程图</el-link>
|
||||
</div>
|
||||
<el-descriptions :column="3" border size="medium">
|
||||
<el-descriptions-item label="报名时间">{{ detail.signupTime || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="报名线路">{{ detail.lineName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="线路类型">{{ detail.lineType || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="报名酒店">{{ detail.hotelName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="报名旅行社">{{ detail.travelAgencyName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="出行时间">{{ detail.travelPeriod || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="fillBedInfo" label="床型">{{ detail.bedType || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="fillBedInfo" label="床位信息">{{ detail.bedInfo || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="fillBedInfo" label="意向拼床人">{{ detail.intendedRoommate || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="是否携带家属">{{ familyText() }}</el-descriptions-item>
|
||||
<el-descriptions-item label="是否参加">{{ detail.joined ? '是' : '否' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="是否报销">{{ detail.reimbursed ? '是' : '否' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="报销超出费用">{{ detail.overCostReimbursed ? '是' : '否' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div v-if="isDirectFamilyLine()" class="tour-ledger-section mt10">
|
||||
<div class="tour-ledger-title">直系亲属线路</div>
|
||||
<el-descriptions :column="3" border size="medium">
|
||||
<el-descriptions-item label="亲属姓名">{{ directRelative.relativeName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所在单位">{{ directRelative.unitName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="亲属关系">{{ directRelative.relationshipName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="线路名称">{{ directRelative.lineName || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="出行开始日期">{{ directRelative.travelStartTime || '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="出行结束日期">{{ directRelative.travelEndTime || '' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div v-if="showFamilySection()" class="tour-ledger-section mt10">
|
||||
<div class="tour-ledger-title">家属信息</div>
|
||||
<el-table :data="familyData" border :size="tableSize" empty-text="暂无家属信息">
|
||||
<el-table-column label="序号" type="index" width="70" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="家属姓名" prop="familyName" min-width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="性别" prop="gender" width="90" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="身份证号码" prop="idCard" min-width="180" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="年龄" prop="age" width="90" align="center" header-align="center"></el-table-column>
|
||||
<el-table-column label="关系" prop="relationship" width="110" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column v-if="fillBedInfo" label="床型" prop="bedType" width="110" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column v-if="fillBedInfo" label="床位" prop="bedInfo" min-width="120" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column v-if="fillBedInfo" label="意向拼床人" prop="intendedRoommate" min-width="130" align="center" header-align="center" show-overflow-tooltip></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<template v-if="detailHasWorkflow()" v-for="task in doneTasks">
|
||||
<div class="tour-ledger-section mt10" :key="task.id">
|
||||
<div class="tour-ledger-title">{{ task.displayName }}</div>
|
||||
<el-descriptions border :column="3" v-if="task.ext && task.ext.isFirstTaskNode">
|
||||
<el-descriptions-item label="申请用户">
|
||||
{{ task.ext.initiatorName }}({{ task.ext.initiatorAccount }})
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="申请时间">{{ task.finishTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="办理结果">
|
||||
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext.submitType"></dict-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-descriptions border :column="3" v-else>
|
||||
<el-descriptions-item label="办理用户">
|
||||
{{ (task.taskFormData && task.taskFormData.userName) || '' }}({{ (task.taskFormData && task.taskFormData.loginName) || '' }})
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="办理时间">{{ task.finishTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="办理结果">
|
||||
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext && task.ext.submitType"></dict-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="办理意见" :span="3">
|
||||
{{ (task.taskFormData && (task.taskFormData.opinion || task.taskFormData.tf_opinion)) || '' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<snaker-chart ref="snakerChartRef"></snaker-chart>
|
||||
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="detailVisible = false">确定</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.tour-ledger-section {
|
||||
padding: 0 2px;
|
||||
}
|
||||
.tour-ledger-title {
|
||||
border-left: 4px solid #0079c2;
|
||||
color: #0079c2;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
margin-bottom: 14px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
.tour-ledger-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: 24px;
|
||||
margin-bottom: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tour-ledger-toolbar-title {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.tour-ledger-scope {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
flex-wrap: nowrap;
|
||||
gap: 12px;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tour-ledger-scope .el-button {
|
||||
margin: 0;
|
||||
}
|
||||
.tour-ledger-scope .el-button + .el-button {
|
||||
margin-left: 0;
|
||||
}
|
||||
.tour-ledger-scope .tour-scope-btn {
|
||||
background: #ecf5ff;
|
||||
border-color: #b3d8ff;
|
||||
border-radius: 4px;
|
||||
color: #0079c2;
|
||||
font-weight: 600;
|
||||
height: 34px;
|
||||
line-height: 1;
|
||||
padding: 8px 18px;
|
||||
}
|
||||
.tour-ledger-scope .tour-scope-btn:hover,
|
||||
.tour-ledger-scope .tour-scope-btn:focus {
|
||||
background: #d9ecff;
|
||||
border-color: #66b1ff;
|
||||
color: #006bb0;
|
||||
}
|
||||
.tour-ledger-scope .tour-scope-btn.is-active {
|
||||
background: #0079c2;
|
||||
border-color: #0079c2;
|
||||
box-shadow: 0 2px 6px rgba(0, 121, 194, 0.24);
|
||||
color: #fff;
|
||||
}
|
||||
.tour-ledger-scope .tour-scope-btn.is-active:hover,
|
||||
.tour-ledger-scope .tour-scope-btn.is-active:focus {
|
||||
background: #006bb0;
|
||||
border-color: #006bb0;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
mixins: [initTableMixins],
|
||||
dicts: ["PROCESS_TASK_SUBMIT_TYPE"],
|
||||
data() {
|
||||
const currentYear = moment().format("YYYY")
|
||||
return {
|
||||
detailVisible: false,
|
||||
unionOptions: [],
|
||||
lineOptions: [],
|
||||
travelPeriodOptions: [],
|
||||
lineTypeOptions: [],
|
||||
summaryStats: {},
|
||||
detail: {},
|
||||
familyData: [],
|
||||
directRelative: {},
|
||||
directFamilyUnitLine: false,
|
||||
fillBedInfo: true,
|
||||
detailRow: {},
|
||||
doneTasks: [],
|
||||
pageForm: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
totalCount: 0,
|
||||
pageOrderName: "signupTime",
|
||||
pageOrderBy: "descending",
|
||||
startYear: currentYear,
|
||||
endYear: currentYear,
|
||||
keyword: "",
|
||||
unionId: "",
|
||||
lineId: "",
|
||||
travelPeriod: "",
|
||||
lineType: "",
|
||||
scopeType: "",
|
||||
overCostOnly: false
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toBoolean(value) {
|
||||
return value === true || value === 1 || value === "1" || value === "true"
|
||||
},
|
||||
isDirectFamilyLine() {
|
||||
return this.directFamilyUnitLine
|
||||
|| this.toBoolean(this.detail && this.detail.directFamilyUnitLine)
|
||||
|| !!(this.directRelative && this.directRelative.id)
|
||||
},
|
||||
detailHasWorkflow() {
|
||||
return !!(this.detailRow && this.detailRow.instanceId)
|
||||
},
|
||||
hasFamily() {
|
||||
return this.toBoolean(this.detail && this.detail.hasFamily) || this.familyData.length > 0
|
||||
},
|
||||
familyText() {
|
||||
if (this.isDirectFamilyLine()) {
|
||||
return "否"
|
||||
}
|
||||
if (!this.hasFamily()) {
|
||||
return "否"
|
||||
}
|
||||
const count = Number(this.familyData.length || 0)
|
||||
return count > 0 ? count + "人" : "是"
|
||||
},
|
||||
showFamilySection() {
|
||||
return this.hasFamily() && !this.isDirectFamilyLine()
|
||||
},
|
||||
loadDoneTasks() {
|
||||
this.$axios.post("/flow/common/doneTasks", { bizId: this.detailRow.id }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.doneTasks = res.data || []
|
||||
}
|
||||
})
|
||||
},
|
||||
openChart() {
|
||||
if (!this.detailRow.instanceProcessDefineId || !this.detailRow.instanceId) {
|
||||
this.$message.warning("暂无流程图信息")
|
||||
return
|
||||
}
|
||||
this.$refs.snakerChartRef.onOpenFull(this.detailRow.instanceProcessDefineId, this.detailRow.instanceId)
|
||||
},
|
||||
resetSearch() {
|
||||
this.pageForm.startYear = moment().format("YYYY")
|
||||
this.pageForm.endYear = moment().format("YYYY")
|
||||
this.pageForm.keyword = ""
|
||||
this.pageForm.unionId = ""
|
||||
this.pageForm.lineId = ""
|
||||
this.pageForm.travelPeriod = ""
|
||||
this.pageForm.lineType = ""
|
||||
this.pageForm.scopeType = ""
|
||||
this.pageForm.overCostOnly = false
|
||||
this.loadUnionOptions()
|
||||
this.loadLineOptions()
|
||||
this.loadTravelPeriodOptions()
|
||||
this.loadLineTypeOptions()
|
||||
this.loadSummaryStats()
|
||||
this.doSearch()
|
||||
},
|
||||
openView(row) {
|
||||
this.detailRow = row || {}
|
||||
this.detail = {}
|
||||
this.familyData = []
|
||||
this.directRelative = {}
|
||||
this.directFamilyUnitLine = false
|
||||
this.fillBedInfo = true
|
||||
this.doneTasks = []
|
||||
this.$axios.post(loc() + "/detail", { id: row.id }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
const data = res.data || {}
|
||||
this.detail = data.ledger || {}
|
||||
this.detail.travelPeriod = data.travelPeriod || ""
|
||||
this.familyData = data.families || []
|
||||
this.directRelative = data.directRelative || {}
|
||||
this.directFamilyUnitLine = this.toBoolean(data.directFamilyUnitLine)
|
||||
this.fillBedInfo = data.fillBedInfo === undefined || data.fillBedInfo === null || this.toBoolean(data.fillBedInfo)
|
||||
this.detailVisible = true
|
||||
if (this.detailHasWorkflow()) {
|
||||
this.loadDoneTasks()
|
||||
}
|
||||
} else {
|
||||
this.$message.warning(res.msg || "查询失败")
|
||||
}
|
||||
})
|
||||
},
|
||||
doDelete(row) {
|
||||
this.$confirm("确定删除【" + row.userName + "】的报名台账吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
this.$axios.post(loc() + "/doDelete", { id: row.id }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success("删除成功")
|
||||
this.pageData()
|
||||
this.loadSummaryStats()
|
||||
this.loadLineOptions()
|
||||
this.loadTravelPeriodOptions()
|
||||
this.loadLineTypeOptions()
|
||||
} else {
|
||||
this.$message.warning(res.msg || "删除失败")
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
loadUnionOptions() {
|
||||
this.$axios.post(loc() + "/unionOptions", {
|
||||
startYear: this.pageForm.startYear,
|
||||
endYear: this.pageForm.endYear
|
||||
}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.unionOptions = res.data || []
|
||||
}
|
||||
})
|
||||
},
|
||||
loadLineOptions() {
|
||||
this.$axios.post(loc() + "/lineOptions", {
|
||||
startYear: this.pageForm.startYear,
|
||||
endYear: this.pageForm.endYear,
|
||||
travelPeriod: this.pageForm.travelPeriod,
|
||||
lineType: this.pageForm.lineType,
|
||||
unionId: this.pageForm.unionId,
|
||||
keyword: this.pageForm.keyword,
|
||||
scopeType: this.pageForm.scopeType
|
||||
}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.lineOptions = res.data || []
|
||||
if (this.pageForm.lineId && !this.lineOptions.some(item => item.lineId === this.pageForm.lineId)) {
|
||||
this.pageForm.lineId = ""
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
loadTravelPeriodOptions() {
|
||||
this.$axios.post(loc() + "/travelPeriodOptions", {
|
||||
startYear: this.pageForm.startYear,
|
||||
endYear: this.pageForm.endYear,
|
||||
lineId: this.pageForm.lineId,
|
||||
lineType: this.pageForm.lineType,
|
||||
unionId: this.pageForm.unionId,
|
||||
keyword: this.pageForm.keyword,
|
||||
scopeType: this.pageForm.scopeType
|
||||
}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.travelPeriodOptions = res.data || []
|
||||
if (this.pageForm.travelPeriod && !this.travelPeriodOptions.some(item => item.travelPeriod === this.pageForm.travelPeriod)) {
|
||||
this.pageForm.travelPeriod = ""
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
loadLineTypeOptions() {
|
||||
this.$axios.post(loc() + "/lineTypeOptions", {
|
||||
startYear: this.pageForm.startYear,
|
||||
endYear: this.pageForm.endYear,
|
||||
lineId: this.pageForm.lineId,
|
||||
travelPeriod: this.pageForm.travelPeriod,
|
||||
unionId: this.pageForm.unionId,
|
||||
keyword: this.pageForm.keyword,
|
||||
scopeType: this.pageForm.scopeType
|
||||
}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.lineTypeOptions = res.data || []
|
||||
if (this.pageForm.lineType && !this.lineTypeOptions.some(item => item.lineType === this.pageForm.lineType)) {
|
||||
this.pageForm.lineType = ""
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
loadSummaryStats() {
|
||||
this.$axios.post(loc() + "/summaryStats", {
|
||||
startYear: this.pageForm.startYear,
|
||||
endYear: this.pageForm.endYear,
|
||||
unionId: this.pageForm.unionId,
|
||||
lineId: this.pageForm.lineId,
|
||||
travelPeriod: this.pageForm.travelPeriod,
|
||||
lineType: this.pageForm.lineType
|
||||
}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.summaryStats = res.data || {}
|
||||
}
|
||||
})
|
||||
},
|
||||
setScopeType(scopeType) {
|
||||
this.pageForm.scopeType = this.pageForm.scopeType === scopeType ? "" : scopeType
|
||||
this.pageForm.pageNumber = 1
|
||||
this.loadLineOptions()
|
||||
this.loadTravelPeriodOptions()
|
||||
this.loadLineTypeOptions()
|
||||
this.pageData()
|
||||
},
|
||||
setOverCostOnly() {
|
||||
this.pageForm.overCostOnly = !this.pageForm.overCostOnly
|
||||
this.pageForm.pageNumber = 1
|
||||
this.pageData()
|
||||
},
|
||||
downloadOverCostSummary() {
|
||||
this.$downLoad(loc() + "/exportOverCostSummary", this.pageForm)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadUnionOptions()
|
||||
this.loadLineOptions()
|
||||
this.loadTravelPeriodOptions()
|
||||
this.loadLineTypeOptions()
|
||||
this.loadSummaryStats()
|
||||
this.pageData()
|
||||
},
|
||||
watch: {
|
||||
"pageForm.startYear"() {
|
||||
this.pageForm.lineId = ""
|
||||
this.pageForm.travelPeriod = ""
|
||||
this.pageForm.lineType = ""
|
||||
this.pageForm.unionId = ""
|
||||
this.pageForm.scopeType = ""
|
||||
this.loadUnionOptions()
|
||||
this.loadLineOptions()
|
||||
this.loadTravelPeriodOptions()
|
||||
this.loadLineTypeOptions()
|
||||
this.loadSummaryStats()
|
||||
},
|
||||
"pageForm.endYear"() {
|
||||
this.pageForm.lineId = ""
|
||||
this.pageForm.travelPeriod = ""
|
||||
this.pageForm.lineType = ""
|
||||
this.pageForm.unionId = ""
|
||||
this.pageForm.scopeType = ""
|
||||
this.loadUnionOptions()
|
||||
this.loadLineOptions()
|
||||
this.loadTravelPeriodOptions()
|
||||
this.loadLineTypeOptions()
|
||||
this.loadSummaryStats()
|
||||
},
|
||||
"pageForm.lineId"() {
|
||||
this.loadTravelPeriodOptions()
|
||||
this.loadLineTypeOptions()
|
||||
this.loadSummaryStats()
|
||||
},
|
||||
"pageForm.travelPeriod"() {
|
||||
this.loadLineOptions()
|
||||
this.loadLineTypeOptions()
|
||||
this.loadSummaryStats()
|
||||
},
|
||||
"pageForm.lineType"() {
|
||||
this.loadLineOptions()
|
||||
this.loadTravelPeriodOptions()
|
||||
this.loadSummaryStats()
|
||||
},
|
||||
"pageForm.unionId"() {
|
||||
this.loadLineOptions()
|
||||
this.loadTravelPeriodOptions()
|
||||
this.loadLineTypeOptions()
|
||||
this.loadSummaryStats()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -50,7 +50,7 @@ const basicForm = {
|
||||
onSave() {
|
||||
this.$refs.formRef.validate(valid => {
|
||||
if (valid) {
|
||||
this.$axios.post('/platform/edu/courses/' + (this.formData.id ? 'update' : 'insert'), this.formData).then(res => {
|
||||
this.$axios.post('/' + (this.formData.id ? 'update' : 'insert'), this.formData).then(res => {
|
||||
if (res.code === 0) {
|
||||
this.visible = false
|
||||
this.$message.success(res.msg)
|
||||
|
||||
+95
-69
@@ -2,47 +2,57 @@ const BASIC_TABLE_COMPONENT = {
|
||||
template: `
|
||||
<div>
|
||||
<el-row type="flex">
|
||||
<el-col></el-col>
|
||||
</el-row>
|
||||
<el-col></el-col>
|
||||
</el-row>
|
||||
<table-tool>
|
||||
<el-button type="primary" size="small" icon="el-icon-plus" @click="openAdd()">新建机构</el-button>
|
||||
<el-button type="primary" size="small" icon="el-icon-plus" @click="openAdd()">新建机构</el-button>
|
||||
</table-tool>
|
||||
<el-table key="1" :data="tableData" ref="tableRef">
|
||||
<el-table-column label="序号" width="100px" type="index" :index="indexMethod"></el-table-column>
|
||||
<el-table-column prop="name" label="机构名称"></el-table-column>
|
||||
<el-table-column prop="introduce" label="描述"></el-table-column>
|
||||
<el-table-column label="操作" width="100px">
|
||||
<el-table-column prop="code" label="机构代码"></el-table-column>
|
||||
<el-table-column prop="introduce" label="描述" min-width="220px">
|
||||
<template slot-scope="{row}">
|
||||
<el-link size="mini" type="danger" @click="del(row.id)">删除</el-link>
|
||||
<div class="institution-description">{{row.introduce}}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="140px">
|
||||
<template slot-scope="scope">
|
||||
<template>
|
||||
<el-link size="mini" type="primary" @click="openEdit(scope.row)">修改</el-link>
|
||||
</template>
|
||||
<el-divider direction="vertical"></el-divider>
|
||||
<el-tooltip v-if="isBasicInstitution(scope.$index)"
|
||||
content="你选择的组织机构是两代会基本机构,不允许删除。"
|
||||
placement="top">
|
||||
<span class="disabled-delete-link">删除</span>
|
||||
</el-tooltip>
|
||||
<el-link v-else size="mini" type="danger" @click="del(scope.row.id)">删除</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog :visible.sync="dialogFormVisible" :title="formData.id?'编辑':'新增'" width="40%">
|
||||
<el-form :model="formData" ref="formRef" :rules="rules" label-width="100px">
|
||||
<el-form-item label="机构名称" prop="ids">
|
||||
<el-cascader
|
||||
v-model="ids"
|
||||
:options="treeList"
|
||||
:props="props"
|
||||
@change="parentChange"
|
||||
placeholder="请选择机构"
|
||||
style="width: 100%"
|
||||
></el-cascader>
|
||||
</el-form-item>
|
||||
<el-form-item label="机构代码" prop="code">
|
||||
<el-input placeholder="请输入机构代码" v-model="formData.code" disabled></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="introduce">
|
||||
<el-input v-model="formData.introduce" placeholder="请输入描述"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="notes">
|
||||
<el-input v-model="formData.notes" placeholder="请输入备注"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-dialog :visible.sync="dialogFormVisible" :title="isEdit ? '编辑' : '新增'" width="40%">
|
||||
<el-form :model="formData" ref="formRef" :rules="rules" label-width="100px">
|
||||
<el-form-item label="上级机构名称">
|
||||
<el-input :value="parentName || '-'" disabled></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="机构名称" prop="name">
|
||||
<el-input placeholder="请输入机构名称" v-model="formData.name"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="机构代码" prop="code">
|
||||
<el-input placeholder="请输入机构代码" v-model="formData.code" :disabled="isEdit"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序编码" prop="location">
|
||||
<el-input-number v-model="formData.location" :min="0" :step="1" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="introduce">
|
||||
<el-input v-model="formData.introduce" placeholder="请输入描述"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="dialogFormVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="doSubmit">确 定</el-button>
|
||||
<el-button @click="dialogFormVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="doSubmit">确定</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
@@ -56,6 +66,10 @@ const BASIC_TABLE_COMPONENT = {
|
||||
parentId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
parentName: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -63,20 +77,13 @@ const BASIC_TABLE_COMPONENT = {
|
||||
rules: {
|
||||
name: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
code: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
parentId: [{ required: true, message: "必填", trigger: ["change", "blur"] }]
|
||||
parentId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
location: [{ required: true, message: "必填", trigger: ["change", "blur"] }]
|
||||
},
|
||||
parentData: {},
|
||||
parentIds: [],
|
||||
ids: [],
|
||||
treeList: [],
|
||||
treeFlat: [],
|
||||
props: {
|
||||
checkStrictly: true,
|
||||
multiple: false,
|
||||
label: "name",
|
||||
value: "id"
|
||||
},
|
||||
dialogFormVisible: false
|
||||
dialogFormVisible: false,
|
||||
isEdit: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -95,38 +102,44 @@ const BASIC_TABLE_COMPONENT = {
|
||||
})
|
||||
},
|
||||
openAdd() {
|
||||
this.isEdit = false
|
||||
this.dialogFormVisible = true
|
||||
this.formData = {}
|
||||
this.ids = []
|
||||
$.post("/platform/teacherCongress/institution/formTree", { sessionId: this.sessionId }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.treeList = res.data.treeList
|
||||
this.treeFlat = res.data.treeFlat
|
||||
}
|
||||
})
|
||||
this.formData = {
|
||||
parentId: this.parentId,
|
||||
location: 0
|
||||
}
|
||||
},
|
||||
// 上级机构选择变化
|
||||
parentChange(val) {
|
||||
const id = val[val.length - 1]
|
||||
const tree = this.treeFlat.find((tree) => tree.id === id)
|
||||
this.$set(this.formData, "code", tree?.code)
|
||||
this.$set(this.formData, "name", tree?.name)
|
||||
openEdit(row) {
|
||||
this.isEdit = true
|
||||
this.dialogFormVisible = true
|
||||
this.formData = {
|
||||
...row,
|
||||
location: row.location || 0
|
||||
}
|
||||
},
|
||||
isBasicInstitution(index) {
|
||||
const pageNumber = this.pageForm.pageNumber || 1
|
||||
const pageSize = this.pageForm.pageSize || 10
|
||||
return (pageNumber - 1) * pageSize + index < 6
|
||||
},
|
||||
doSubmit() {
|
||||
this.$refs.formRef.validate((valid) => {
|
||||
this.formData.id = this.ids[this.ids.length - 1]
|
||||
this.formData.parentId = this.ids[this.ids.length - 2] || this.parentId
|
||||
this.formData.sessionId = this.sessionId
|
||||
if (valid) {
|
||||
this.$axios.post(loc() + "/insert", this.formData).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.dialogFormVisible = false
|
||||
this.$message.success(res.msg)
|
||||
this.doSearch()
|
||||
this.$emit("refresh", null)
|
||||
}
|
||||
})
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
this.formData.sessionId = this.sessionId
|
||||
if (!this.isEdit) {
|
||||
this.formData.parentId = this.parentId
|
||||
}
|
||||
const url = this.isEdit ? loc() + "/update" : loc() + "/insert"
|
||||
this.$axios.post(url, this.formData).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.dialogFormVisible = false
|
||||
this.$message.success(res.msg)
|
||||
this.doSearch()
|
||||
this.$emit("refresh", null)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
@@ -145,5 +158,18 @@ const BASIC_TABLE_COMPONENT = {
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
style: /*language=CSS*/ `
|
||||
.institution-description {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.disabled-delete-link {
|
||||
color: #c0c4cc;
|
||||
cursor: not-allowed;
|
||||
font-size: 12px;
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
+40
-20
@@ -9,21 +9,27 @@ layout("/layouts/platform.html"){
|
||||
</el-col>
|
||||
<el-col :span="19">
|
||||
<el-card shadow="never" style="height: 100%">
|
||||
<template>
|
||||
<basic-table
|
||||
:session-id="sessionId"
|
||||
:parent-id="currentTreeData.id"
|
||||
v-if="showInstitutionTable || (currentTreeData && currentTreeData.id==='0')"
|
||||
ref="basicTableRef"
|
||||
@refresh="$refs.treeRef.listTree()"
|
||||
></basic-table>
|
||||
</template>
|
||||
<user-table
|
||||
:session-id="sessionId"
|
||||
:institution-id="currentTreeData.id"
|
||||
ref="userTableRef"
|
||||
v-if="!showInstitutionTable && sessionId"
|
||||
></user-table>
|
||||
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
||||
<el-tab-pane v-if="hasChildInstitution" label="机构列表" name="institution">
|
||||
<basic-table
|
||||
:session-id="sessionId"
|
||||
:parent-id="currentTreeData && currentTreeData.id"
|
||||
:parent-name="currentTreeData && currentTreeData.name"
|
||||
v-if="activeTab === 'institution' && sessionId && currentTreeData"
|
||||
ref="basicTableRef"
|
||||
@refresh="refreshTree"
|
||||
></basic-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="人员列表" name="user">
|
||||
<user-table
|
||||
:session-id="sessionId"
|
||||
:institution-id="currentTreeData && currentTreeData.id"
|
||||
:institution-code="currentTreeData && currentTreeData.code"
|
||||
ref="userTableRef"
|
||||
v-if="activeTab === 'user' && sessionId && currentTreeData"
|
||||
></user-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -43,16 +49,16 @@ layout("/layouts/platform.html"){
|
||||
"basic-table": BASIC_TABLE_COMPONENT
|
||||
},
|
||||
computed: {
|
||||
showInstitutionTable() {
|
||||
const hasChildren = this.currentTreeData?.children?.length > 0
|
||||
return hasChildren
|
||||
hasChildInstitution() {
|
||||
return this.currentTreeData && this.currentTreeData.children && this.currentTreeData.children.length > 0
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentTreeNode: null,
|
||||
currentTreeData: null,
|
||||
sessionId: null
|
||||
sessionId: null,
|
||||
activeTab: "institution"
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -60,14 +66,28 @@ layout("/layouts/platform.html"){
|
||||
this.currentTreeData = data
|
||||
this.currentTreeNode = node
|
||||
this.sessionId = sessionId
|
||||
if (this.hasChildInstitution) {
|
||||
this.activeTab = "institution"
|
||||
} else if (this.activeTab === "institution") {
|
||||
this.activeTab = "user"
|
||||
}
|
||||
this.refreshActiveTab()
|
||||
},
|
||||
handleTabClick() {
|
||||
this.refreshActiveTab()
|
||||
},
|
||||
refreshActiveTab() {
|
||||
this.$nextTick(() => {
|
||||
if (this.showInstitutionTable) {
|
||||
if (this.activeTab === "institution") {
|
||||
this.$refs.basicTableRef && this.$refs.basicTableRef.doSearch()
|
||||
} else {
|
||||
this.$refs.userTableRef && this.$refs.userTableRef.doSearch()
|
||||
}
|
||||
})
|
||||
},
|
||||
refreshTree() {
|
||||
this.$refs.treeRef.listTree(this.currentTreeData && this.currentTreeData.id)
|
||||
},
|
||||
openEdit(row) {}
|
||||
},
|
||||
created() {}
|
||||
|
||||
+21
-2
@@ -46,11 +46,30 @@ const TREE_COMPONENT = {
|
||||
this.$emit("node-click", data, node, this.sessionId)
|
||||
},
|
||||
filterNode() {},
|
||||
listTree() {
|
||||
findTreeNode(list, id) {
|
||||
if (!id || !list) {
|
||||
return null
|
||||
}
|
||||
for (const item of list) {
|
||||
if (item.id === id) {
|
||||
return item
|
||||
}
|
||||
const child = this.findTreeNode(item.children, id)
|
||||
if (child) {
|
||||
return child
|
||||
}
|
||||
}
|
||||
return null
|
||||
},
|
||||
listTree(selectedId) {
|
||||
this.$axios.post("/platform/teacherCongress/institution/leftTree", { sessionId: this.sessionId }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.treeData = res.data
|
||||
this.$emit("node-click", this.treeData[0], null, this.sessionId)
|
||||
if (this.treeData && this.treeData.length > 0) {
|
||||
this.treeData[0].name = "两代会组织机构"
|
||||
}
|
||||
const selectedNode = this.findTreeNode(this.treeData, selectedId) || this.treeData[0]
|
||||
this.$emit("node-click", selectedNode, null, this.sessionId)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
+74
-35
@@ -3,58 +3,73 @@ const USER_TABLE_COMPONENT = {
|
||||
<div>
|
||||
<el-card shadow="never" style="height: 100%">
|
||||
<el-row type="flex" :gutter="20">
|
||||
<el-col :span="4">
|
||||
<el-input v-model="pageForm.searchKeyword" clearable placeholder="请输入姓名或者工号">" @keyup.enter.native="doSearch">
|
||||
</el-input>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<div class="search-query">
|
||||
<el-button type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-input v-model="pageForm.searchKeyword"
|
||||
clearable
|
||||
placeholder="请输入姓名或者工号"
|
||||
@keyup.enter.native="doSearch">
|
||||
</el-input>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<div class="search-query">
|
||||
<el-button type="primary" icon="el-icon-search" @click="doSearch">搜索</el-button>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-divider class="mb10 mt10"></el-divider>
|
||||
<table-tool>
|
||||
<el-button type="primary" size="small" icon="el-icon-plus" @click="openAdd()">设置人员</el-button>
|
||||
</table-tool>
|
||||
<el-button type="primary" size="small" icon="el-icon-plus" @click="openAdd()">设置人员</el-button>
|
||||
</table-tool>
|
||||
<el-table key="1" :data="tableData" ref="tableRef">
|
||||
<el-table-column label="序号" width="100px" type="index" :index="indexMethod"></el-table-column>
|
||||
<el-table-column prop="username" label="姓名"></el-table-column>
|
||||
<el-table-column prop="loginname" label="工号"></el-table-column>
|
||||
<el-table-column prop="sex" label="性别"></el-table-column>
|
||||
<el-table-column prop="mobile" label="手机号码"></el-table-column>
|
||||
<el-table-column prop="unitName" label="单位" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="identity" label="身份" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="操作" width="100px">
|
||||
<template slot-scope="{row}">
|
||||
<el-link size="mini" type="danger" @click="del(row.id)">删除</el-link>
|
||||
</template>
|
||||
<el-table-column label="序号" width="100px" type="index" :index="indexMethod"></el-table-column>
|
||||
<el-table-column prop="username" label="姓名"></el-table-column>
|
||||
<el-table-column prop="loginname" label="工号"></el-table-column>
|
||||
<el-table-column prop="sex" label="性别"></el-table-column>
|
||||
<el-table-column prop="mobile" label="手机号码"></el-table-column>
|
||||
<el-table-column prop="unitName" label="单位" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="identity" label="身份" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="操作" width="100px">
|
||||
<template slot-scope="{row}">
|
||||
<el-link size="mini" type="danger" @click="del(row.id)">删除</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-table>
|
||||
<!--#include("/layouts/pagination.html"){}#-->
|
||||
</el-card>
|
||||
<el-dialog title="设置人员" width="50%" :visible.sync="dialogVisible">
|
||||
<el-form :model="formData" ref="formRef" label-width="120px" :rules="formRules">
|
||||
<el-form :model="formData" ref="formRef" label-width="120px" :rules="formRules">
|
||||
<el-form-item label="届次" prop="sessionId">
|
||||
<el-select v-model="formData.sessionId" disabled>
|
||||
<el-option v-for="i in sessionOptions" :label="i.fullName" :value="i.id" :key="i.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="工号或姓名" prop="userId">
|
||||
<el-form-item v-if="needRole" label="角色" prop="roleCode">
|
||||
<el-select v-model="formData.roleCode" placeholder="请选择角色" style="width: 100%">
|
||||
<el-option v-for="item in roleOptions"
|
||||
:key="item.code"
|
||||
:label="item.name"
|
||||
:value="item.code">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="工号或者姓名" prop="userId">
|
||||
<user-select
|
||||
v-model="formData.userId"
|
||||
style="width: 100%"
|
||||
api_input_key_name="query"
|
||||
:option_label_func="(item)=>{return item.username + item.loginname}"
|
||||
v-model="formData.userId"
|
||||
style="width: 100%"
|
||||
api_input_key_name="query"
|
||||
:option_label_func="(item)=>{return item.username + item.loginname}"
|
||||
></user-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="身份" prop="identity">
|
||||
<dict-select v-model="formData.identity" code="TEACHER_CONGRESS_INSTITUTION_USER_ROLE"></dict-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="doSubmit">确 定</el-button>
|
||||
</el-form>
|
||||
<div style="color: #e6a23c; line-height: 22px; margin: 0 0 12px 120px;">
|
||||
注意:如果需要跟角色绑定,请在数据字典双代会组织机构中添加对应的角色标识。
|
||||
</div>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="doSubmit">确定</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
@@ -68,6 +83,10 @@ const USER_TABLE_COMPONENT = {
|
||||
institutionId: {
|
||||
required: true,
|
||||
type: String
|
||||
},
|
||||
institutionCode: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -75,10 +94,17 @@ const USER_TABLE_COMPONENT = {
|
||||
formRules: {
|
||||
sessionId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
userId: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
identity: [{ required: true, message: "必填", trigger: ["change", "blur"] }]
|
||||
identity: [{ required: true, message: "必填", trigger: ["change", "blur"] }],
|
||||
roleCode: [{ required: true, message: "必填", trigger: ["change", "blur"] }]
|
||||
},
|
||||
dialogVisible: false,
|
||||
sessionOptions: []
|
||||
sessionOptions: [],
|
||||
roleOptions: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
needRole() {
|
||||
return ["ZWH001", "ZXWYH", "DBZGSCXZ"].includes(this.institutionCode)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -136,6 +162,18 @@ const USER_TABLE_COMPONENT = {
|
||||
})
|
||||
},
|
||||
|
||||
listRoleOptions() {
|
||||
if (!this.needRole) {
|
||||
this.roleOptions = []
|
||||
return
|
||||
}
|
||||
this.$axios.post("/platform/teacherCongress/institution/specialCommitteeRoleOptions").then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.roleOptions = res.data || []
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
openAdd() {
|
||||
this.dialogVisible = true
|
||||
this.formData = {
|
||||
@@ -143,6 +181,7 @@ const USER_TABLE_COMPONENT = {
|
||||
institutionId: this.institutionId
|
||||
}
|
||||
this.listSession()
|
||||
this.listRoleOptions()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+94
-31
@@ -3,6 +3,55 @@ layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<style scoped>
|
||||
.site-cug-audit-page {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f4f6f8;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.site-cug-audit-sticky {
|
||||
flex-shrink: 0;
|
||||
background: #f4f6f8;
|
||||
}
|
||||
.site-cug-audit-list-scroll {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 18px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.site-cug-audit-search /deep/ .van-search__content,
|
||||
.site-cug-audit-search .van-search__content {
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
.site-cug-card-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.site-cug-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 22px;
|
||||
padding: 0 8px;
|
||||
border-radius: 4px;
|
||||
background: #eef6ff;
|
||||
color: #0b75bd;
|
||||
font-size: 12px;
|
||||
}
|
||||
.site-cug-tag.gray {
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
}
|
||||
.site-cug-tag.warn {
|
||||
background: #fff3e5;
|
||||
color: #d97706;
|
||||
}
|
||||
.action-btn.disabled {
|
||||
opacity: 0.45;
|
||||
pointer-events: none;
|
||||
@@ -15,18 +64,23 @@ layout("/layouts/platform_h5.html"){
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" v-cloak>
|
||||
<div id="app" class="site-cug-audit-page" v-cloak>
|
||||
<van-nav-bar title="校工会审核" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
|
||||
|
||||
<van-sticky offset-top="46px">
|
||||
<van-sticky offset-top="46px" class="site-cug-audit-sticky">
|
||||
<van-search
|
||||
class="site-cug-audit-search"
|
||||
v-model="pageForm.searchKeyword"
|
||||
:show-action="false"
|
||||
:reverse-color="false"
|
||||
show-action
|
||||
clearable
|
||||
input-align="left"
|
||||
placeholder="请输入姓名/工号/场地搜索"
|
||||
@search="doSearch"
|
||||
></van-search>
|
||||
@clear="doSearch">
|
||||
<template #action>
|
||||
<div @click="doSearch">搜索</div>
|
||||
</template>
|
||||
</van-search>
|
||||
<van-dropdown-menu :close-on-click-outside="false" :close-on-click-overlay="false">
|
||||
<van-dropdown-item v-model="pageForm.siteType" :options="typeOptions" :multiple="false" @change="doSearch"></van-dropdown-item>
|
||||
<van-dropdown-item v-model="pageForm.siteId" :options="siteOptions" :multiple="false" @change="doSearch"></van-dropdown-item>
|
||||
@@ -37,32 +91,38 @@ layout("/layouts/platform_h5.html"){
|
||||
</van-tabs>
|
||||
</van-sticky>
|
||||
|
||||
<table-list api="/platform/siteCug/schoolUnionAudit/pageData" :page_form.sync="pageForm" ref="tableListRef" title="applyUserName" @ready="onReady">
|
||||
<template v-slot="{index,row}">
|
||||
<table-column label="场地名称">{{row.siteName}}</table-column>
|
||||
<table-column label="预约类型">{{row.reserveType === 'club' ? '协会预约' : '分工会预约'}}</table-column>
|
||||
<table-column label="预约主体">{{row.reserveType === 'club' ? (row.clubName || '-') : (row.applyUnionName || '-')}}</table-column>
|
||||
<table-column label="所属单位">{{row.applyUnitName}}</table-column>
|
||||
<table-column label="开始时间">{{row.reserveStartTime}}</table-column>
|
||||
<table-column label="结束时间">{{row.reserveEndTime}}</table-column>
|
||||
<table-column label="当前节点">{{row.curTaskName || '-'}}</table-column>
|
||||
</template>
|
||||
<template #actions="{index,row}">
|
||||
<div class="action-btn" @click="onView(row)">
|
||||
<i class="fa fa-eye"></i>
|
||||
<span>查看</span>
|
||||
</div>
|
||||
<div class="action-btn" v-if="row.taskState === 10" @click="onAudit(row)">
|
||||
<i class="fa fa-edit"></i>
|
||||
<span>审核</span>
|
||||
</div>
|
||||
<div class="action-btn delete" v-if="canRevoke(row)" :class="{ loading: revokeLoading }" @click="onRevoke(row)">
|
||||
<van-loading v-if="revokeLoading" size="14px" color="#fff"></van-loading>
|
||||
<i v-else class="fa fa-reply"></i>
|
||||
<span>撤回</span>
|
||||
</div>
|
||||
</template>
|
||||
</table-list>
|
||||
<div class="site-cug-audit-list-scroll">
|
||||
<table-list api="/platform/siteCug/schoolUnionAudit/pageData" :page_form.sync="pageForm" ref="tableListRef" title="applyUserName" @ready="onReady">
|
||||
<template v-slot="{index,row}">
|
||||
<div class="site-cug-card-tags">
|
||||
<span v-if="row.applyLoginName" class="site-cug-tag">{{ row.applyLoginName }}</span>
|
||||
<span class="site-cug-tag gray">{{ reserveTypeText(row) }}</span>
|
||||
<span v-if="row.yearlyBatch" class="site-cug-tag warn">预约本年</span>
|
||||
</div>
|
||||
<table-column label="场地名称">{{row.siteName}}</table-column>
|
||||
<table-column label="预约主体">{{row.reserveType === 'club' ? (row.clubName || '-') : (row.applyUnionName || '-')}}</table-column>
|
||||
<table-column label="所属单位">{{row.applyUnitName}}</table-column>
|
||||
<table-column label="开始时间">{{row.reserveStartTime}}</table-column>
|
||||
<table-column label="结束时间">{{row.reserveEndTime}}</table-column>
|
||||
<table-column label="当前节点">{{row.curTaskName || '-'}}</table-column>
|
||||
</template>
|
||||
<template #actions="{index,row}">
|
||||
<div class="action-btn" @click="onView(row)">
|
||||
<i class="fa fa-eye"></i>
|
||||
<span>查看</span>
|
||||
</div>
|
||||
<div class="action-btn" v-if="row.taskState === 10" @click="onAudit(row)">
|
||||
<i class="fa fa-edit"></i>
|
||||
<span>审核</span>
|
||||
</div>
|
||||
<div class="action-btn delete" v-if="canRevoke(row)" :class="{ loading: revokeLoading }" @click="onRevoke(row)">
|
||||
<van-loading v-if="revokeLoading" size="14px" color="#fff"></van-loading>
|
||||
<i v-else class="fa fa-reply"></i>
|
||||
<span>撤回</span>
|
||||
</div>
|
||||
</template>
|
||||
</table-list>
|
||||
</div>
|
||||
|
||||
<info ref="infoRef">
|
||||
<div v-if="showApprovalForm">
|
||||
@@ -140,6 +200,9 @@ layout("/layouts/platform_h5.html"){
|
||||
canRevoke(row) {
|
||||
return Number(row.instanceState) === 20
|
||||
},
|
||||
reserveTypeText(row) {
|
||||
return row && row.reserveType === 'club' ? '协会预约' : '分工会预约'
|
||||
},
|
||||
onView(row) {
|
||||
this.showApprovalForm = false
|
||||
this.$refs.infoRef.onOpen(row)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+478
@@ -0,0 +1,478 @@
|
||||
<!--#
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<style scoped>
|
||||
.tour-approval-page {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f4f6f8;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tour-approval-sticky {
|
||||
flex-shrink: 0;
|
||||
background: #f4f6f8;
|
||||
}
|
||||
|
||||
.tour-approval-list-scroll {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 18px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tour-approval-search /deep/ .van-search__content,
|
||||
.tour-approval-search .van-search__content {
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.tour-card-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tour-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 22px;
|
||||
padding: 0 8px;
|
||||
border-radius: 4px;
|
||||
background: #eef6ff;
|
||||
color: #0b75bd;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tour-tag.gray {
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.tour-tag.warn {
|
||||
background: #fff3e5;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.tour-approval-sheet {
|
||||
max-height: 86vh;
|
||||
overflow-y: auto;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
background: #f4f6f8;
|
||||
}
|
||||
|
||||
.tour-section {
|
||||
margin: 10px 12px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.tour-section-title {
|
||||
position: relative;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid #edf0f4;
|
||||
color: #0b75bd;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.tour-section-title::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 0;
|
||||
width: 30px;
|
||||
height: 4px;
|
||||
border-radius: 0 0 3px 3px;
|
||||
background: #0b75bd;
|
||||
}
|
||||
|
||||
.tour-approval-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 10px 12px 14px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.tour-process-opinion {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" class="tour-approval-page" v-cloak>
|
||||
<van-nav-bar title="校工会审核" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
|
||||
|
||||
<van-sticky offset-top="46px" class="tour-approval-sticky">
|
||||
<van-search
|
||||
class="tour-approval-search"
|
||||
v-model="pageForm.keyword"
|
||||
placeholder="请输入工号/姓名/线路搜索"
|
||||
show-action
|
||||
clearable
|
||||
input-align="left"
|
||||
@search="doSearch"
|
||||
@clear="doSearch">
|
||||
<template #action>
|
||||
<div @click="doSearch">搜索</div>
|
||||
</template>
|
||||
</van-search>
|
||||
<van-dropdown-menu :close-on-click-outside="false" :close-on-click-overlay="false">
|
||||
<van-dropdown-item :options="auditOptions" @change="doSearch" v-model="pageForm.audit"></van-dropdown-item>
|
||||
<van-dropdown-item :options="unionOptions" @change="doSearch" v-model="pageForm.unionId"></van-dropdown-item>
|
||||
<van-dropdown-item :options="lineTypeOptions" @change="doSearch" v-model="pageForm.lineType"></van-dropdown-item>
|
||||
</van-dropdown-menu>
|
||||
</van-sticky>
|
||||
|
||||
<div class="tour-approval-list-scroll">
|
||||
<table-list api="/platform/tour/schoolUnionApproval/pageData" :page_form.sync="pageForm" ref="tableListRef" title="userName" @ready="onReady">
|
||||
<template v-slot="{index,row}">
|
||||
<div class="tour-card-tags">
|
||||
<span v-if="row.jobNo" class="tour-tag">{{ row.jobNo }}</span>
|
||||
<span v-if="row.lineType" class="tour-tag gray">{{ row.lineType }}</span>
|
||||
<span v-if="row.overCostReimbursed" class="tour-tag warn">报销超出费用</span>
|
||||
</div>
|
||||
<table-column label="线路名称">{{ row.lineName || '' }}</table-column>
|
||||
<table-column label="出行时段">{{ row.travelPeriod || '' }}</table-column>
|
||||
<table-column label="报销超出费用">{{ row.overCostReimbursed ? '是' : '否' }}</table-column>
|
||||
<table-column label="当前节点">{{ row.curTaskName || '' }}</table-column>
|
||||
<table-column label="流程状态">
|
||||
<enum-tag
|
||||
v-if="row.instanceState !== null && row.instanceState !== undefined && row.instanceState !== ''"
|
||||
:value="row.instanceState"
|
||||
name="ProcessInstanceStateEnum"
|
||||
label_key="message"
|
||||
size="small">
|
||||
</enum-tag>
|
||||
</table-column>
|
||||
</template>
|
||||
<template #actions="{index,row}">
|
||||
<div class="action-btn" @click="onView(row)">
|
||||
<i class="fa fa-eye"></i>
|
||||
<span>查看</span>
|
||||
</div>
|
||||
<div class="action-btn" v-if="row.taskState === 10" @click="onApproval(row)">
|
||||
<i class="fa fa-check"></i>
|
||||
<span>审核</span>
|
||||
</div>
|
||||
</template>
|
||||
</table-list>
|
||||
</div>
|
||||
|
||||
<tour-info ref="infoRef">
|
||||
<div v-if="showApprovalForm" class="tour-section">
|
||||
<div class="tour-section-title">{{ formData.taskName || '审核' }}</div>
|
||||
<van-form ref="formRef">
|
||||
<van-field
|
||||
v-model="formData.tf_opinion"
|
||||
name="tf_opinion"
|
||||
label="审批意见"
|
||||
placeholder="请输入审批意见"
|
||||
:rules="[{ required: true, message: '请填写审批意见' }]"
|
||||
required
|
||||
type="textarea"
|
||||
rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit>
|
||||
</van-field>
|
||||
</van-form>
|
||||
<div class="tour-approval-actions">
|
||||
<van-button type="danger" block @click="handleTaskAction(6)">退回</van-button>
|
||||
<van-button type="danger" block @click="handleTaskAction(2)">拒绝</van-button>
|
||||
<van-button type="primary" block @click="handleTaskAction(1)">同意</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</tour-info>
|
||||
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
const TOUR_INFO = {
|
||||
template: /*language=HTML*/ `
|
||||
<van-action-sheet v-model="visible" title="报名详情">
|
||||
<div class="tour-approval-sheet">
|
||||
<div class="tour-section">
|
||||
<div class="tour-section-title">教职工信息</div>
|
||||
<van-cell title="工号" :value="detail.jobNo || ''"></van-cell>
|
||||
<van-cell title="姓名" :value="detail.userName || ''"></van-cell>
|
||||
<van-cell title="性别" :value="detail.gender || ''"></van-cell>
|
||||
<van-cell title="身份证号" :value="detail.idCard || ''"></van-cell>
|
||||
<van-cell title="所在单位" :value="detail.unitName || ''"></van-cell>
|
||||
<van-cell title="所在工会" :value="detail.unionName || ''"></van-cell>
|
||||
</div>
|
||||
|
||||
<div class="tour-section">
|
||||
<div class="tour-section-title">报名信息</div>
|
||||
<van-cell title="报名线路" :value="detail.lineName || ''"></van-cell>
|
||||
<van-cell title="线路类型" :value="detail.lineType || ''"></van-cell>
|
||||
<van-cell title="出行时段" :value="detail.travelPeriod || ''"></van-cell>
|
||||
<van-cell title="报名时间" :value="detail.signupTime || ''"></van-cell>
|
||||
<van-cell title="报名酒店" :value="detail.hotelName || ''"></van-cell>
|
||||
<van-cell title="旅行社" :value="detail.travelAgencyName || ''"></van-cell>
|
||||
<van-cell v-if="fillBedInfo" title="床型" :value="detail.bedType || ''"></van-cell>
|
||||
<van-cell v-if="fillBedInfo" title="床位信息" :value="detail.bedInfo || ''"></van-cell>
|
||||
<van-cell v-if="fillBedInfo" title="意向拼床人" :value="detail.intendedRoommate || ''"></van-cell>
|
||||
<van-cell title="携带家属" :value="familyText()"></van-cell>
|
||||
<van-cell title="报销超出费用" :value="detail.overCostReimbursed ? '是' : '否'"></van-cell>
|
||||
</div>
|
||||
|
||||
<div v-if="isDirectFamilyLine()" class="tour-section">
|
||||
<div class="tour-section-title">直系亲属线路</div>
|
||||
<van-cell title="亲属姓名" :value="directRelative.relativeName || ''"></van-cell>
|
||||
<van-cell title="所在单位" :value="directRelative.unitName || ''"></van-cell>
|
||||
<van-cell title="亲属关系" :value="directRelative.relationshipName || ''"></van-cell>
|
||||
<van-cell title="线路名称" :value="directRelative.lineName || ''"></van-cell>
|
||||
<van-cell title="出行开始" :value="directRelative.travelStartTime || ''"></van-cell>
|
||||
<van-cell title="出行结束" :value="directRelative.travelEndTime || ''"></van-cell>
|
||||
</div>
|
||||
|
||||
<div v-if="showFamilySection()" class="tour-section">
|
||||
<div class="tour-section-title">家属信息</div>
|
||||
<template v-for="(item,index) in familyData">
|
||||
<van-cell :key="'title_' + index" :title="'家属' + (index + 1)"></van-cell>
|
||||
<van-cell :key="'name_' + index" title="姓名" :value="item.familyName || ''"></van-cell>
|
||||
<van-cell :key="'gender_' + index" title="性别" :value="item.gender || ''"></van-cell>
|
||||
<van-cell :key="'id_' + index" title="身份证号" :value="item.idCard || ''"></van-cell>
|
||||
<van-cell :key="'rel_' + index" title="关系" :value="item.relationship || ''"></van-cell>
|
||||
<van-cell v-if="fillBedInfo" :key="'bed_' + index" title="床型" :value="item.bedType || ''"></van-cell>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="doneTasks.length > 0" class="tour-section">
|
||||
<div class="tour-section-title">流程记录</div>
|
||||
<template v-for="task in doneTasks">
|
||||
<van-cell-group :key="task.id" :title="task.displayName || task.taskName">
|
||||
<van-cell title="办理时间" :value="task.finishTime || ''"></van-cell>
|
||||
<van-cell title="办理结果">
|
||||
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext && task.ext.submitType"></dict-tag>
|
||||
</van-cell>
|
||||
<van-cell title="办理意见" v-if="task.taskFormData" class="direction-column-cell">
|
||||
<div class="tour-process-opinion">{{ task.taskFormData.opinion || task.taskFormData.tf_opinion || '' }}</div>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<slot></slot>
|
||||
</div>
|
||||
</van-action-sheet>
|
||||
`,
|
||||
dicts: ["PROCESS_TASK_SUBMIT_TYPE"],
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
row: {},
|
||||
detail: {},
|
||||
familyData: [],
|
||||
directRelative: {},
|
||||
allowFamily: false,
|
||||
fillBedInfo: true,
|
||||
directFamilyUnitLine: false,
|
||||
doneTasks: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onOpen(row) {
|
||||
this.row = row || {}
|
||||
this.visible = true
|
||||
this.detail = {}
|
||||
this.familyData = []
|
||||
this.directRelative = {}
|
||||
this.allowFamily = false
|
||||
this.fillBedInfo = true
|
||||
this.directFamilyUnitLine = false
|
||||
this.doneTasks = []
|
||||
this.info()
|
||||
this.getDoneTasks()
|
||||
},
|
||||
onClose() {
|
||||
this.visible = false
|
||||
},
|
||||
info() {
|
||||
this.$axios.post("/platform/tour/schoolUnionApproval/detail", { id: this.row.id }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
const data = res.data || {}
|
||||
this.detail = data.ledger || {}
|
||||
this.detail.travelPeriod = data.travelPeriod || this.row.travelPeriod || ""
|
||||
this.familyData = data.families || []
|
||||
this.directRelative = data.directRelative || {}
|
||||
this.allowFamily = this.toBoolean(data.allowFamily)
|
||||
this.fillBedInfo = data.fillBedInfo === undefined || data.fillBedInfo === null || this.toBoolean(data.fillBedInfo)
|
||||
this.directFamilyUnitLine = this.toBoolean(data.directFamilyUnitLine)
|
||||
} else {
|
||||
vant.Toast(res.msg || "查询失败")
|
||||
}
|
||||
})
|
||||
},
|
||||
getDoneTasks() {
|
||||
this.$axios.post("/flow/common/doneTasks", { bizId: this.row.id }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.doneTasks = res.data || []
|
||||
}
|
||||
})
|
||||
},
|
||||
toBoolean(value) {
|
||||
return value === true || value === 1 || value === "1" || value === "true" || value === "TRUE"
|
||||
},
|
||||
isDirectFamilyLine() {
|
||||
return this.directFamilyUnitLine || this.toBoolean(this.detail.directFamilyUnitLine) || !!(this.directRelative && this.directRelative.id)
|
||||
},
|
||||
hasFamily() {
|
||||
return this.toBoolean(this.detail.hasFamily) || this.familyData.length > 0
|
||||
},
|
||||
showFamilySection() {
|
||||
return this.allowFamily && this.hasFamily() && !this.isDirectFamilyLine()
|
||||
},
|
||||
familyText() {
|
||||
if (this.isDirectFamilyLine() || !this.allowFamily) {
|
||||
return "否"
|
||||
}
|
||||
return this.hasFamily() ? "是" : "否"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: "#app",
|
||||
store,
|
||||
components: {
|
||||
"tour-info": TOUR_INFO
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pageForm: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
totalCount: 0,
|
||||
pageOrderName: "signupTime",
|
||||
pageOrderBy: "descending",
|
||||
audit: false,
|
||||
keyword: "",
|
||||
unionId: "",
|
||||
lineType: ""
|
||||
},
|
||||
auditOptions: [
|
||||
{ text: "未审核", value: false },
|
||||
{ text: "已审核", value: true }
|
||||
],
|
||||
unionOptions: [
|
||||
{ text: "全部工会", value: "" }
|
||||
],
|
||||
lineTypeOptions: [
|
||||
{ text: "全部线路类型", value: "" }
|
||||
],
|
||||
formData: {},
|
||||
showApprovalForm: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
historyBack,
|
||||
onReady() {
|
||||
this.doSearch()
|
||||
},
|
||||
doSearch() {
|
||||
this.$nextTick(async () => {
|
||||
await Promise.all([this.loadUnionOptions(), this.loadLineTypeOptions()])
|
||||
this.pageForm.pageNumber = 1
|
||||
this.pageForm.totalCount = 0
|
||||
this.$refs.tableListRef.doSearch()
|
||||
})
|
||||
},
|
||||
loadUnionOptions() {
|
||||
return this.$axios.post("/platform/tour/schoolUnionApproval/unionOptions", {
|
||||
audit: this.pageForm.audit,
|
||||
keyword: this.pageForm.keyword,
|
||||
lineType: this.pageForm.lineType
|
||||
}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
const options = (res.data || []).map(item => ({
|
||||
text: item.name,
|
||||
value: item.id
|
||||
})).filter(item => item.value)
|
||||
this.unionOptions = [{ text: "全部工会", value: "" }].concat(options)
|
||||
if (this.pageForm.unionId && !this.unionOptions.some(item => item.value === this.pageForm.unionId)) {
|
||||
this.pageForm.unionId = ""
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
loadLineTypeOptions() {
|
||||
return this.$axios.post("/platform/tour/schoolUnionApproval/lineTypeOptions", {
|
||||
audit: this.pageForm.audit,
|
||||
keyword: this.pageForm.keyword,
|
||||
unionId: this.pageForm.unionId
|
||||
}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
const options = (res.data || []).map(item => ({
|
||||
text: item.lineType,
|
||||
value: item.lineType
|
||||
})).filter(item => item.value)
|
||||
this.lineTypeOptions = [{ text: "全部线路类型", value: "" }].concat(options)
|
||||
if (this.pageForm.lineType && !this.lineTypeOptions.some(item => item.value === this.pageForm.lineType)) {
|
||||
this.pageForm.lineType = ""
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
onView(row) {
|
||||
this.showApprovalForm = false
|
||||
this.$refs.infoRef.onOpen(row)
|
||||
},
|
||||
onApproval(row) {
|
||||
this.showApprovalForm = true
|
||||
this.formData = {
|
||||
processTaskId: row.taskId,
|
||||
taskName: row.curTaskName || row.taskName,
|
||||
tf_opinion: ""
|
||||
}
|
||||
this.$refs.infoRef.onOpen(row)
|
||||
},
|
||||
async handleTaskAction(val) {
|
||||
try {
|
||||
await this.$refs.formRef.validate()
|
||||
} catch (e) {
|
||||
return
|
||||
}
|
||||
this.$dialog.confirm({
|
||||
title: "提示",
|
||||
message: "您确定要提交吗?"
|
||||
}).then(() => {
|
||||
const loading = this.$toast.loading({
|
||||
message: "加载中...",
|
||||
forbidClick: true,
|
||||
overlay: true,
|
||||
duration: 0
|
||||
})
|
||||
this.$axios.post("/flow/common/executeTask", {
|
||||
data: JSON.stringify({
|
||||
...this.formData,
|
||||
submitType: val
|
||||
})
|
||||
}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$refs.infoRef.onClose()
|
||||
this.$toast.success(res.msg || "操作成功")
|
||||
this.doSearch()
|
||||
} else {
|
||||
this.$toast(res.msg || "操作失败")
|
||||
}
|
||||
}).finally(() => {
|
||||
loading.close()
|
||||
})
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,556 @@
|
||||
<!--#
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<style scoped>
|
||||
.tour-apply-page {
|
||||
min-height: 100vh;
|
||||
padding: 10px 14px 78px;
|
||||
background: #f4f6f8;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tour-apply-card {
|
||||
margin-bottom: 14px;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
|
||||
.tour-apply-title {
|
||||
position: relative;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid #edf0f4;
|
||||
color: #0b75bd;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.tour-apply-title::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 0;
|
||||
width: 30px;
|
||||
height: 4px;
|
||||
border-radius: 0 0 3px 3px;
|
||||
background: #0b75bd;
|
||||
}
|
||||
|
||||
.tour-card-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tour-card-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tour-family-tab {
|
||||
margin: 10px 12px 0;
|
||||
height: 30px;
|
||||
border-radius: 5px;
|
||||
background: #1684d2;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tour-field-tip {
|
||||
padding: 0 16px 12px;
|
||||
color: #ee2f2f;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.tour-over-cost /deep/ .van-field__label,
|
||||
.tour-over-cost /deep/ .van-field__label span,
|
||||
.tour-over-cost /deep/ .van-radio__label {
|
||||
color: #ee2f2f;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tour-submit-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 20;
|
||||
padding: 10px 14px calc(10px + env(safe-area-inset-bottom));
|
||||
border-top: 1px solid #edf0f4;
|
||||
background: #ffffff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tour-apply-empty {
|
||||
padding: 24px 0;
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app">
|
||||
<van-nav-bar title="疗休养报名" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
|
||||
|
||||
<div class="tour-apply-page">
|
||||
<van-loading v-if="pageLoading" size="24px" vertical>加载中...</van-loading>
|
||||
|
||||
<template v-else>
|
||||
<div class="tour-apply-card">
|
||||
<div class="tour-apply-title">教职工信息</div>
|
||||
<van-field label="姓名" readonly v-model="signupForm.userName"></van-field>
|
||||
<van-field label="工号" readonly v-model="signupForm.jobNo"></van-field>
|
||||
<van-field label="身份证号码" v-model="signupForm.idCard" maxlength="30" placeholder="请填写身份证号码"></van-field>
|
||||
<van-field label="手机号" v-model="signupForm.mobile" type="tel" maxlength="30" placeholder="请填写手机号"></van-field>
|
||||
<van-field label="所属工会" readonly v-model="signupForm.unionName"></van-field>
|
||||
</div>
|
||||
|
||||
<div class="tour-apply-card">
|
||||
<div class="tour-apply-title">报名信息</div>
|
||||
<van-field label="报名线路" readonly v-model="signupForm.lineName"></van-field>
|
||||
<van-field label="线路类型" readonly v-model="signupForm.lineType"></van-field>
|
||||
<van-field v-if="signupForm.travelPeriod" label="出行时间" readonly v-model="signupForm.travelPeriod"></van-field>
|
||||
<van-field label="报名酒店" v-model="signupForm.hotelName" maxlength="100" placeholder="请输入报名酒店"></van-field>
|
||||
<van-field
|
||||
v-if="fillBedInfo"
|
||||
label="床型"
|
||||
readonly
|
||||
clickable
|
||||
is-link
|
||||
placeholder="请选择房型"
|
||||
v-model="signupForm.bedType"
|
||||
@click="openBedPicker('self')">
|
||||
</van-field>
|
||||
<van-field v-if="fillBedInfo" label="床位信息" v-model="signupForm.bedInfo" maxlength="100" placeholder="请输入床位数量"></van-field>
|
||||
<van-field v-if="fillBedInfo" label="意向拼床人" v-model="signupForm.intendedRoommate" maxlength="100" placeholder="请输入意向拼床人"></van-field>
|
||||
<van-field v-if="canApplyOverReimbursement(signupForm)" class="tour-over-cost" label="报销超出费用">
|
||||
<template #input>
|
||||
<van-radio-group v-model="signupForm.overCostReimbursed" direction="horizontal">
|
||||
<van-radio :name="true">是</van-radio>
|
||||
<van-radio :name="false">否</van-radio>
|
||||
</van-radio-group>
|
||||
</template>
|
||||
</van-field>
|
||||
<div v-if="canApplyOverReimbursement(signupForm) && signupForm.overCostReimbursed" class="tour-field-tip">
|
||||
提醒:您选择了报销超出费用,会影响到下一年度的疗休养,请认真思考。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isDirectFamilyLine(signupForm)" class="tour-apply-card">
|
||||
<div class="tour-apply-title">直系亲属线路</div>
|
||||
<van-field label="亲属姓名" v-model="directRelativeForm.relativeName" maxlength="100" placeholder="请输入亲属姓名"></van-field>
|
||||
<van-field label="所在单位" v-model="directRelativeForm.unitName" maxlength="100" placeholder="请输入所在单位"></van-field>
|
||||
<van-field
|
||||
label="亲属关系"
|
||||
readonly
|
||||
clickable
|
||||
is-link
|
||||
placeholder="请选择亲属关系"
|
||||
v-model="directRelativeForm.relationshipName"
|
||||
@click="openDirectRelativePicker">
|
||||
</van-field>
|
||||
<van-field label="线路名称" v-model="directRelativeForm.lineName" maxlength="100" placeholder="请输入线路名称"></van-field>
|
||||
<van-field label="出行开始" v-model="directRelativeForm.travelStartTime" type="date" placeholder="请选择出行开始日期"></van-field>
|
||||
<van-field label="出行结束" v-model="directRelativeForm.travelEndTime" type="date" placeholder="请选择出行结束日期"></van-field>
|
||||
</div>
|
||||
|
||||
<div class="tour-apply-card">
|
||||
<div class="tour-apply-title">
|
||||
<div class="tour-card-title-row">
|
||||
<span>亲属信息</span>
|
||||
<div class="tour-card-actions">
|
||||
<van-button size="small" type="default" :disabled="familyData.length === 0" @click="removeFamily">删除亲属</van-button>
|
||||
<van-button size="small" type="info" :disabled="!allowFamily || isDirectFamilyLine(signupForm)" @click="addFamily">添加亲属</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!allowFamily || isDirectFamilyLine(signupForm)" class="tour-apply-empty">
|
||||
当前线路不支持填写亲属
|
||||
</div>
|
||||
<template v-else-if="familyData.length > 0">
|
||||
<div class="tour-family-tab">亲属{{ familyActive + 1 }}</div>
|
||||
<van-field label="姓名" v-model="currentFamily.familyName" maxlength="100" placeholder="请填写姓名"></van-field>
|
||||
<van-field label="年龄" v-model.number="currentFamily.age" type="digit" placeholder="请填写年龄"></van-field>
|
||||
<van-field label="性别">
|
||||
<template #input>
|
||||
<van-radio-group v-model="currentFamily.gender" direction="horizontal">
|
||||
<van-radio name="男">男</van-radio>
|
||||
<van-radio name="女">女</van-radio>
|
||||
</van-radio-group>
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field label="身份证号码" v-model="currentFamily.idCard" maxlength="18" placeholder="请填写身份证号码" @blur="normalizeFamilyIdCard(currentFamily)"></van-field>
|
||||
<van-field label="手机号" v-model="currentFamily.mobile" maxlength="30" placeholder="请填写手机号"></van-field>
|
||||
<van-field
|
||||
label="与本人关系"
|
||||
readonly
|
||||
clickable
|
||||
is-link
|
||||
placeholder="请选择关系"
|
||||
v-model="currentFamily.relationship"
|
||||
@click="openRelationshipPicker">
|
||||
</van-field>
|
||||
<van-field
|
||||
v-if="fillBedInfo"
|
||||
label="床型"
|
||||
readonly
|
||||
clickable
|
||||
is-link
|
||||
placeholder="请选择房型"
|
||||
v-model="currentFamily.bedType"
|
||||
@click="openBedPicker('family')">
|
||||
</van-field>
|
||||
<van-field v-if="fillBedInfo" label="床位" v-model="currentFamily.bedInfo" maxlength="100" placeholder="请输入床位数量"></van-field>
|
||||
<van-field v-if="fillBedInfo" label="意向拼床人" v-model="currentFamily.intendedRoommate" maxlength="100" placeholder="请输入意向拼床人"></van-field>
|
||||
<div v-if="fillBedInfo" class="tour-field-tip">提醒:如果跟报名人员同一房间,无须选择!</div>
|
||||
</template>
|
||||
<div v-else class="tour-apply-empty">暂无亲属,请按需添加</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="tour-submit-bar">
|
||||
<van-button type="info" block round :loading="submitLoading" @click="submitSignup">提交报名</van-button>
|
||||
</div>
|
||||
|
||||
<van-popup v-model="bedPickerVisible" position="bottom">
|
||||
<van-picker show-toolbar :columns="bedTypeColumns" @confirm="confirmBedType" @cancel="bedPickerVisible=false"></van-picker>
|
||||
</van-popup>
|
||||
|
||||
<van-popup v-model="relationshipPickerVisible" position="bottom">
|
||||
<van-picker show-toolbar :columns="relationshipColumns" @confirm="confirmRelationship" @cancel="relationshipPickerVisible=false"></van-picker>
|
||||
</van-popup>
|
||||
|
||||
<van-popup v-model="directRelativePickerVisible" position="bottom">
|
||||
<van-picker show-toolbar :columns="directRelativeColumns" @confirm="confirmDirectRelative" @cancel="directRelativePickerVisible=false"></van-picker>
|
||||
</van-popup>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
const vue = new Vue({
|
||||
el: "#app",
|
||||
store,
|
||||
data() {
|
||||
return {
|
||||
pageLoading: true,
|
||||
submitLoading: false,
|
||||
allowFamily: false,
|
||||
fillBedInfo: true,
|
||||
signupForm: {},
|
||||
directRelativeForm: {},
|
||||
familyData: [],
|
||||
familyActive: 0,
|
||||
bedTypeOptions: [],
|
||||
familyRelationshipOptions: [],
|
||||
directRelativeOptions: [],
|
||||
bedPickerVisible: false,
|
||||
relationshipPickerVisible: false,
|
||||
directRelativePickerVisible: false,
|
||||
bedPickerTarget: "self",
|
||||
matterId: GetQueryString("matterId") || ""
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentFamily() {
|
||||
return this.familyData[this.familyActive] || {}
|
||||
},
|
||||
bedTypeColumns() {
|
||||
return this.bedTypeOptions.map(item => item.name || item.code).filter(Boolean)
|
||||
},
|
||||
relationshipColumns() {
|
||||
return this.familyRelationshipOptions.map(item => item.name || item.code).filter(Boolean)
|
||||
},
|
||||
directRelativeColumns() {
|
||||
return this.directRelativeOptions.map(item => ({ text: item.name || item.code, item }))
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
if (!this.matterId) {
|
||||
vant.Dialog.alert({ title: "提示", message: "报名事项参数缺失", confirmButtonColor: "#1867b0" })
|
||||
this.pageLoading = false
|
||||
return
|
||||
}
|
||||
Promise.all([this.loadOptions(), this.loadSignupDetail()]).finally(() => {
|
||||
this.pageLoading = false
|
||||
})
|
||||
},
|
||||
loadOptions() {
|
||||
return Promise.all([
|
||||
this.$axios.post("/platform/tour/signup/bedTypeOptions").then(res => {
|
||||
if (res.code === 0) this.bedTypeOptions = res.data || []
|
||||
}),
|
||||
this.$axios.post("/platform/tour/signup/familyRelationshipOptions").then(res => {
|
||||
if (res.code === 0) this.familyRelationshipOptions = res.data || []
|
||||
}),
|
||||
this.$axios.post("/platform/tour/signup/directRelativeOptions").then(res => {
|
||||
if (res.code === 0) this.directRelativeOptions = res.data || []
|
||||
})
|
||||
])
|
||||
},
|
||||
loadSignupDetail() {
|
||||
return this.$axios.post("/platform/tour/signup/signupDetail", { matterId: this.matterId }).then((res) => {
|
||||
if (res.code !== 0) {
|
||||
vant.Dialog.alert({
|
||||
title: "提示",
|
||||
message: res.msg || "报名信息加载失败",
|
||||
confirmButtonColor: "#1867b0"
|
||||
})
|
||||
return
|
||||
}
|
||||
const data = res.data || {}
|
||||
const matter = data.matter || {}
|
||||
const staff = data.staff || {}
|
||||
const ledger = data.ledger || {}
|
||||
const directRelative = data.directRelative || {}
|
||||
this.allowFamily = matter.allowFamily === true || matter.allowFamily === 1 || matter.allowFamily === "1"
|
||||
this.fillBedInfo = matter.fillBedInfo === undefined || matter.fillBedInfo === null || matter.fillBedInfo === true || matter.fillBedInfo === 1 || matter.fillBedInfo === "1"
|
||||
this.signupForm = Object.assign({
|
||||
id: "",
|
||||
year: matter.year,
|
||||
jobNo: "",
|
||||
userName: "",
|
||||
gender: "",
|
||||
idCard: "",
|
||||
mobile: "",
|
||||
unitId: "",
|
||||
unitName: "",
|
||||
unionId: "",
|
||||
unionName: "",
|
||||
signupTime: this.$moment ? this.$moment().format("YYYY-MM-DD HH:mm:ss") : "",
|
||||
matterId: matter.matterId || "",
|
||||
lineId: matter.lineId || "",
|
||||
lineName: matter.lineName || "",
|
||||
lineType: matter.lineType || "",
|
||||
directFamilyUnitLine: matter.directFamilyUnitLine,
|
||||
hotelName: "",
|
||||
travelAgencyId: matter.travelAgencyId || "",
|
||||
travelAgencyName: matter.travelAgencyName || "",
|
||||
travelPeriod: matter.travelPeriod || "",
|
||||
travelStartTime: matter.travelStartTime || "",
|
||||
travelEndTime: matter.travelEndTime || "",
|
||||
allowOverReimbursement: matter.allowOverReimbursement,
|
||||
overCostReimbursed: false,
|
||||
hasFamily: false,
|
||||
intendedRoommate: "",
|
||||
bedType: "",
|
||||
bedInfo: ""
|
||||
}, staff, ledger, {
|
||||
year: matter.year,
|
||||
matterId: matter.matterId || ledger.matterId || "",
|
||||
lineId: matter.lineId || ledger.lineId || "",
|
||||
lineName: matter.lineName || ledger.lineName || "",
|
||||
lineType: matter.lineType || ledger.lineType || "",
|
||||
directFamilyUnitLine: matter.directFamilyUnitLine,
|
||||
travelAgencyId: matter.travelAgencyId || ledger.travelAgencyId || "",
|
||||
travelAgencyName: matter.travelAgencyName || ledger.travelAgencyName || "",
|
||||
travelPeriod: matter.travelPeriod || "",
|
||||
travelStartTime: matter.travelStartTime || "",
|
||||
travelEndTime: matter.travelEndTime || "",
|
||||
allowOverReimbursement: matter.allowOverReimbursement,
|
||||
overCostReimbursed: ledger.overCostReimbursed === true || ledger.overCostReimbursed === 1 || ledger.overCostReimbursed === "1",
|
||||
hasFamily: ledger.hasFamily === true || ledger.hasFamily === 1 || ledger.hasFamily === "1"
|
||||
})
|
||||
this.directRelativeForm = Object.assign(this.emptyDirectRelative(), {
|
||||
lineId: matter.lineId || "",
|
||||
lineName: matter.lineName || "",
|
||||
travelStartTime: matter.travelStartTime || "",
|
||||
travelEndTime: matter.travelEndTime || ""
|
||||
}, directRelative)
|
||||
this.familyData = (data.families || []).map(item => Object.assign(this.emptyFamily(), item))
|
||||
if (this.familyData.length > 0) {
|
||||
this.signupForm.hasFamily = true
|
||||
}
|
||||
if (!this.allowFamily || this.isDirectFamilyLine(this.signupForm)) {
|
||||
this.familyData = []
|
||||
this.signupForm.hasFamily = false
|
||||
}
|
||||
})
|
||||
},
|
||||
emptyFamily() {
|
||||
return {
|
||||
familyName: "",
|
||||
gender: "",
|
||||
idCard: "",
|
||||
mobile: "",
|
||||
age: null,
|
||||
bedType: "",
|
||||
bedInfo: "",
|
||||
intendedRoommate: "",
|
||||
staffJobNo: this.signupForm.jobNo || "",
|
||||
staffName: this.signupForm.userName || "",
|
||||
relationship: ""
|
||||
}
|
||||
},
|
||||
emptyDirectRelative() {
|
||||
return {
|
||||
relativeName: "",
|
||||
unitName: "",
|
||||
relationshipCode: "",
|
||||
relationshipName: "",
|
||||
lineId: this.signupForm.lineId || "",
|
||||
lineName: this.signupForm.lineName || "",
|
||||
travelStartTime: this.signupForm.travelStartTime || "",
|
||||
travelEndTime: this.signupForm.travelEndTime || ""
|
||||
}
|
||||
},
|
||||
isDirectFamilyLine(row) {
|
||||
return row && (row.directFamilyUnitLine === true || row.directFamilyUnitLine === 1 || row.directFamilyUnitLine === "1")
|
||||
},
|
||||
canApplyOverReimbursement(row) {
|
||||
return row && (row.allowOverReimbursement === true || row.allowOverReimbursement === 1 || row.allowOverReimbursement === "1")
|
||||
},
|
||||
addFamily() {
|
||||
if (!this.allowFamily || this.isDirectFamilyLine(this.signupForm)) return
|
||||
this.familyData.push(this.emptyFamily())
|
||||
this.familyActive = this.familyData.length - 1
|
||||
this.signupForm.hasFamily = true
|
||||
},
|
||||
removeFamily() {
|
||||
if (this.familyData.length === 0) return
|
||||
this.familyData.splice(this.familyActive, 1)
|
||||
this.familyActive = Math.max(0, this.familyActive - 1)
|
||||
this.signupForm.hasFamily = this.familyData.length > 0
|
||||
},
|
||||
openBedPicker(target) {
|
||||
if (this.bedTypeColumns.length === 0) {
|
||||
vant.Toast("暂无床型选项")
|
||||
return
|
||||
}
|
||||
this.bedPickerTarget = target
|
||||
this.bedPickerVisible = true
|
||||
},
|
||||
confirmBedType(value) {
|
||||
if (this.bedPickerTarget === "family") {
|
||||
this.currentFamily.bedType = value
|
||||
} else {
|
||||
this.signupForm.bedType = value
|
||||
}
|
||||
this.bedPickerVisible = false
|
||||
},
|
||||
openRelationshipPicker() {
|
||||
if (this.relationshipColumns.length === 0) {
|
||||
vant.Toast("暂无关系选项")
|
||||
return
|
||||
}
|
||||
this.relationshipPickerVisible = true
|
||||
},
|
||||
confirmRelationship(value) {
|
||||
this.currentFamily.relationship = value
|
||||
this.relationshipPickerVisible = false
|
||||
},
|
||||
openDirectRelativePicker() {
|
||||
if (this.directRelativeColumns.length === 0) {
|
||||
vant.Toast("暂无亲属关系选项")
|
||||
return
|
||||
}
|
||||
this.directRelativePickerVisible = true
|
||||
},
|
||||
confirmDirectRelative(value) {
|
||||
const item = value && value.item ? value.item : {}
|
||||
this.directRelativeForm.relationshipCode = item.code || ""
|
||||
this.directRelativeForm.relationshipName = item.name || value.text || ""
|
||||
this.directRelativePickerVisible = false
|
||||
},
|
||||
normalizeFamilyIdCard(row) {
|
||||
if (!row || !row.idCard) return
|
||||
row.idCard = String(row.idCard).trim().toUpperCase()
|
||||
},
|
||||
isValidIdCard(value) {
|
||||
if (!value) return false
|
||||
const idCard = String(value).trim().toUpperCase()
|
||||
return /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dX]$/.test(idCard)
|
||||
},
|
||||
validateFamilies() {
|
||||
if (!this.allowFamily || !this.signupForm.hasFamily) return true
|
||||
if (this.familyData.length === 0) {
|
||||
vant.Toast("请添加亲属信息")
|
||||
return false
|
||||
}
|
||||
const invalid = this.familyData.some(item => !item.familyName || !item.gender || !item.idCard || !item.relationship)
|
||||
if (invalid) {
|
||||
vant.Toast("请完善亲属姓名、性别、身份证号码和关系")
|
||||
return false
|
||||
}
|
||||
const invalidIdCard = this.familyData.some(item => !this.isValidIdCard(item.idCard))
|
||||
if (invalidIdCard) {
|
||||
vant.Toast("请输入正确的亲属身份证号码")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
validateDirectRelative() {
|
||||
if (!this.isDirectFamilyLine(this.signupForm)) return true
|
||||
if (!this.directRelativeForm.relativeName) {
|
||||
vant.Toast("请输入亲属姓名")
|
||||
return false
|
||||
}
|
||||
if (!this.directRelativeForm.unitName) {
|
||||
vant.Toast("请输入亲属所在单位")
|
||||
return false
|
||||
}
|
||||
if (!this.directRelativeForm.relationshipCode) {
|
||||
vant.Toast("请选择亲属关系")
|
||||
return false
|
||||
}
|
||||
if (!this.directRelativeForm.lineName) {
|
||||
vant.Toast("请输入直系亲属线路名称")
|
||||
return false
|
||||
}
|
||||
if (!this.directRelativeForm.travelStartTime || !this.directRelativeForm.travelEndTime) {
|
||||
vant.Toast("请选择直系亲属出行时间")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
submitSignup() {
|
||||
if (this.pageLoading || this.submitLoading) return
|
||||
if (!this.validateFamilies()) return
|
||||
if (!this.validateDirectRelative()) return
|
||||
const families = this.allowFamily && this.signupForm.hasFamily ? this.familyData : []
|
||||
const form = Object.assign({}, this.signupForm, {
|
||||
families: JSON.stringify(families),
|
||||
directRelative: this.isDirectFamilyLine(this.signupForm) ? JSON.stringify(this.directRelativeForm) : ""
|
||||
})
|
||||
this.submitLoading = true
|
||||
this.$axios.post("/platform/tour/signup/doSignup", form).then((res) => {
|
||||
this.submitLoading = false
|
||||
if (res.code === 0) {
|
||||
vant.Dialog.alert({
|
||||
title: "提示",
|
||||
message: res.msg || "报名成功",
|
||||
confirmButtonColor: "#1867b0"
|
||||
}).then(() => {
|
||||
window.location.href = "/platform/tour/signup/h5/signup"
|
||||
})
|
||||
} else {
|
||||
vant.Dialog.alert({
|
||||
title: "提示",
|
||||
message: res.msg || "报名失败",
|
||||
confirmButtonColor: "#1867b0"
|
||||
})
|
||||
}
|
||||
}).catch(() => {
|
||||
this.submitLoading = false
|
||||
vant.Toast("报名提交失败")
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,210 @@
|
||||
<!--#
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<style scoped>
|
||||
.tour-signup-h5 {
|
||||
min-height: 100vh;
|
||||
background: #f5f7fb;
|
||||
padding-bottom: 80px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tour-signup-banner {
|
||||
position: relative;
|
||||
height: 193px;
|
||||
overflow: hidden;
|
||||
background: #0f74bc;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.tour-signup-swipe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tour-signup-swipe img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 193px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.tour-signup-banner::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(90deg, rgba(5, 83, 143, 0.72) 0%, rgba(5, 83, 143, 0.32) 48%, rgba(5, 83, 143, 0.08) 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tour-signup-banner__text {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
bottom: 20px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.tour-signup-title {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.tour-signup-subtitle {
|
||||
margin-top: 8px;
|
||||
color: rgba(255, 255, 255, 0.86);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.tour-signup-card {
|
||||
margin: 10px 12px 0;
|
||||
padding: 14px 14px 16px;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.tour-signup-card__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #eef2f7;
|
||||
}
|
||||
|
||||
.tour-signup-card__title {
|
||||
color: #111827;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.tour-signup-card__year {
|
||||
flex-shrink: 0;
|
||||
color: #0f74bc;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tour-signup-notice {
|
||||
margin-top: 14px;
|
||||
color: #334155;
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.tour-signup-notice /deep/ img,
|
||||
.tour-signup-notice /deep/ video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.tour-signup-notice /deep/ #pdf-container {
|
||||
min-height: 560px;
|
||||
}
|
||||
|
||||
.tour-signup-empty {
|
||||
padding: 34px 0 26px;
|
||||
}
|
||||
|
||||
.tour-signup-footer {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
box-shadow: 0 -6px 18px rgba(15, 23, 42, 0.08);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" class="tour-signup-h5">
|
||||
<van-nav-bar title="疗休养报名" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
|
||||
|
||||
<div class="tour-signup-banner">
|
||||
<van-swipe class="tour-signup-swipe" :autoplay="3500" indicator-color="white">
|
||||
<van-swipe-item v-for="item in bannerList" :key="item">
|
||||
<img :src="item" alt="疗休养报名">
|
||||
</van-swipe-item>
|
||||
</van-swipe>
|
||||
<div class="tour-signup-banner__text">
|
||||
<div class="tour-signup-title">疗休养报名</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tour-signup-card">
|
||||
<div class="tour-signup-card__header">
|
||||
<div class="tour-signup-card__title">服务须知</div>
|
||||
<div class="tour-signup-card__year">{{ setting.year || currentYear }}年度</div>
|
||||
</div>
|
||||
|
||||
<van-skeleton title :row="8" :loading="loading">
|
||||
<div v-if="setting.serviceNotice" class="tour-signup-notice">
|
||||
<pdf-preview :key="serviceNoticeKey" :content="setting.serviceNotice"></pdf-preview>
|
||||
</div>
|
||||
<van-empty v-else class="tour-signup-empty" description="暂无服务须知"></van-empty>
|
||||
</van-skeleton>
|
||||
</div>
|
||||
|
||||
<div class="tour-signup-footer">
|
||||
<van-button block type="info" color="#0f74bc" round @click="confirmRead">我已阅读</van-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
const vue = new Vue({
|
||||
el: "#app",
|
||||
store,
|
||||
components: {
|
||||
"pdf-preview": httpVueLoader("/components/plugins/sysFilePreview/PdfIndex.vue?v=" + new Date().getTime())
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
currentYear: new Date().getFullYear(),
|
||||
serviceNoticeKey: "",
|
||||
bannerList: [
|
||||
"/assets/platform/images/tour/tour-h5-banner-1.jpg",
|
||||
"/assets/platform/images/tour/tour-h5-banner-2.jpg"
|
||||
],
|
||||
setting: {
|
||||
year: "",
|
||||
configName: "",
|
||||
serviceNotice: ""
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadServiceNotice() {
|
||||
this.loading = true
|
||||
this.$axios.post("/platform/tour/signup/h5/serviceNotice").then((res) => {
|
||||
this.loading = false
|
||||
if (res.code === 0) {
|
||||
this.setting = Object.assign({}, this.setting, res.data || {})
|
||||
this.serviceNoticeKey = (this.setting.year || this.currentYear) + "_" + new Date().getTime()
|
||||
} else {
|
||||
vant.Toast(res.msg || "服务须知加载失败")
|
||||
}
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
confirmRead() {
|
||||
window.location.href = "/platform/tour/signup/h5/signup"
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadServiceNotice()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,559 @@
|
||||
<!--#
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<style scoped>
|
||||
#app {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tour-detail-page {
|
||||
height: calc(100vh - 46px);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px 10px 70px;
|
||||
background: #f4f6f8;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tour-detail-header {
|
||||
padding: 14px 12px;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
|
||||
.tour-detail-title {
|
||||
color: #0f172a;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.tour-detail-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.tour-line-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
padding: 0 7px;
|
||||
border-radius: 4px;
|
||||
background: #0b75bd;
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.tour-detail-meta {
|
||||
margin-top: 10px;
|
||||
color: #334155;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.tour-detail-meta-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tour-detail-meta-label {
|
||||
width: 72px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: #64748b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tour-detail-meta-value {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tour-detail-section {
|
||||
margin-top: 10px;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.05);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tour-detail-section-title {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 8px;
|
||||
padding-left: 8px;
|
||||
border-left: 3px solid #0b75bd;
|
||||
color: #0f172a;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.tour-detail-content /deep/ img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.tour-detail-section-scroll {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.tour-pdf-preview {
|
||||
min-height: 220px;
|
||||
}
|
||||
|
||||
.tour-pdf-status {
|
||||
padding: 16px 0;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tour-pdf-toolbar {
|
||||
margin-bottom: 8px;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.tour-pdf-pages {
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.tour-pdf-page {
|
||||
margin-bottom: 10px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: #f8fafc;
|
||||
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.tour-pdf-page canvas {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tour-detail-empty {
|
||||
padding: 18px 0;
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tour-detail-footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
padding: 9px 12px calc(9px + env(safe-area-inset-bottom));
|
||||
border-top: 1px solid #edf0f4;
|
||||
background: #ffffff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app">
|
||||
<van-nav-bar title="线路详情" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
|
||||
|
||||
<div class="tour-detail-page">
|
||||
<van-loading v-if="detailLoading" size="24px" vertical>加载中...</van-loading>
|
||||
<template v-else>
|
||||
<div class="tour-detail-header">
|
||||
<div class="tour-detail-title">{{ lineDetail.lineName || signupDetail.lineName || '线路详情' }}</div>
|
||||
<div class="tour-detail-tags">
|
||||
<span v-if="lineDetail.lotName || signupDetail.lotName" class="tour-line-tag">{{ lineDetail.lotName || signupDetail.lotName }}</span>
|
||||
<span v-if="lineDetail.lineType || signupDetail.lineType" class="tour-line-tag">{{ lineDetail.lineType || signupDetail.lineType }}</span>
|
||||
<span v-if="signupDetail.unionName" class="tour-line-tag">{{ signupDetail.unionName }}</span>
|
||||
</div>
|
||||
<div class="tour-detail-meta">
|
||||
<div v-if="travelPeriod(signupDetail)" class="tour-detail-meta-row">
|
||||
<span class="tour-detail-meta-label"><span>出行时间</span><span>:</span></span>
|
||||
<span class="tour-detail-meta-value">{{ travelPeriod(signupDetail) }}</span>
|
||||
</div>
|
||||
<div class="tour-detail-meta-row">
|
||||
<span class="tour-detail-meta-label"><span>已报人数</span><span>:</span></span>
|
||||
<span class="tour-detail-meta-value">{{ signupDetail.signupCount || 0 }}(家属{{ signupDetail.familyCount || 0 }}人)</span>
|
||||
</div>
|
||||
<div class="tour-detail-meta-row">
|
||||
<span class="tour-detail-meta-label"><span>联系人</span><span>:</span></span>
|
||||
<span class="tour-detail-meta-value">{{ signupDetail.contactName || '暂无' }}</span>
|
||||
</div>
|
||||
<div class="tour-detail-meta-row">
|
||||
<span class="tour-detail-meta-label"><span>联系方式</span><span>:</span></span>
|
||||
<span class="tour-detail-meta-value">{{ lineDetail.contactPhone || signupDetail.contactPhone || '暂无' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tour-detail-section">
|
||||
<div class="tour-detail-section-title">线路介绍</div>
|
||||
<div class="tour-detail-section-scroll">
|
||||
<div v-if="lineDetail.lineContent && lineContentHref" class="tour-pdf-preview">
|
||||
<div v-if="pdfPageCount" class="tour-pdf-toolbar">
|
||||
已加载 {{ pdfRenderedPages }} / {{ pdfPageCount }} 页
|
||||
</div>
|
||||
<div ref="pdfPreviewContainer" class="tour-pdf-pages"></div>
|
||||
<div v-if="pdfLoading && pdfRenderedPages === 0" class="tour-pdf-status">
|
||||
<van-loading size="22px" vertical>正在加载第一页...</van-loading>
|
||||
</div>
|
||||
<div v-else-if="pdfLoading" class="tour-pdf-status">
|
||||
正在继续加载后续页面...
|
||||
</div>
|
||||
<div v-if="pdfError" class="tour-pdf-status">{{ pdfError }}</div>
|
||||
</div>
|
||||
<div v-else-if="lineDetail.lineContent" class="tour-detail-content" v-html="lineDetail.lineContent">
|
||||
</div>
|
||||
<div v-else class="tour-detail-empty">暂无线路介绍</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="tour-detail-footer">
|
||||
<van-button :type="detailActionType()" block round :disabled="detailActionDisabled()" @click="handleDetailAction">
|
||||
{{ detailActionText() }}
|
||||
</van-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
const vue = new Vue({
|
||||
el: "#app",
|
||||
store,
|
||||
data() {
|
||||
return {
|
||||
detailLoading: true,
|
||||
lineDetail: {},
|
||||
signupDetail: {},
|
||||
lineContentHref: "",
|
||||
pdfDoc: null,
|
||||
pdfLoadingTask: null,
|
||||
pdfLoading: false,
|
||||
pdfError: "",
|
||||
pdfPageCount: 0,
|
||||
pdfRenderedPages: 0,
|
||||
pdfRenderToken: 0,
|
||||
matterId: GetQueryString("matterId") || "",
|
||||
lineId: GetQueryString("lineId") || ""
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
travelPeriod(row) {
|
||||
if (!row) {
|
||||
return ""
|
||||
}
|
||||
if (row.travelStartTime && row.travelEndTime) {
|
||||
return row.travelStartTime + " 至 " + row.travelEndTime
|
||||
}
|
||||
return row.travelPeriod || row.travelStartTime || row.travelEndTime || ""
|
||||
},
|
||||
toBoolean(value) {
|
||||
return value === true || value === 1 || value === "1" || value === "true" || value === "TRUE"
|
||||
},
|
||||
isSigned() {
|
||||
return !!(this.signupDetail && this.signupDetail.signed)
|
||||
},
|
||||
isApprovalSignup() {
|
||||
return this.isSigned() && !!this.signupDetail.instanceId
|
||||
},
|
||||
canRevokeSignup() {
|
||||
return this.isApprovalSignup() && this.toBoolean(this.signupDetail.canRevoke) && !!this.signupDetail.startTaskId
|
||||
},
|
||||
isFinishedSignup() {
|
||||
return this.isApprovalSignup() && Number(this.signupDetail.instanceState) === 20
|
||||
},
|
||||
isRejectedSignup() {
|
||||
return this.isApprovalSignup() && Number(this.signupDetail.instanceState) === 45
|
||||
},
|
||||
canModifySignup() {
|
||||
if (!this.isSigned()) {
|
||||
return true
|
||||
}
|
||||
if (!this.isApprovalSignup()) {
|
||||
return true
|
||||
}
|
||||
return !this.isFinishedSignup() && !this.isRejectedSignup() && this.signupDetail.taskKey === "startTask"
|
||||
},
|
||||
detailActionText() {
|
||||
if (this.isSigned()) {
|
||||
if (this.canRevokeSignup()) {
|
||||
return "撤回"
|
||||
}
|
||||
if (this.isFinishedSignup()) {
|
||||
return "已完成"
|
||||
}
|
||||
if (this.isRejectedSignup()) {
|
||||
return "已拒绝"
|
||||
}
|
||||
return this.canModifySignup() ? "修改报名" : "审核中"
|
||||
}
|
||||
return "我要报名"
|
||||
},
|
||||
detailActionType() {
|
||||
return this.canRevokeSignup() ? "danger" : "info"
|
||||
},
|
||||
detailActionDisabled() {
|
||||
return this.isSigned() && this.isApprovalSignup() && (this.isFinishedSignup() || this.isRejectedSignup() || (!this.canRevokeSignup() && !this.canModifySignup()))
|
||||
},
|
||||
handleDetailAction() {
|
||||
if (this.detailActionDisabled()) {
|
||||
return
|
||||
}
|
||||
if (this.canRevokeSignup()) {
|
||||
this.revokeSignup()
|
||||
return
|
||||
}
|
||||
this.onApply()
|
||||
},
|
||||
parseLineContentHref(content) {
|
||||
if (!content) {
|
||||
return ""
|
||||
}
|
||||
try {
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(content, "text/html")
|
||||
const link = doc.querySelector("a[href]")
|
||||
return link ? link.getAttribute("href") : ""
|
||||
} catch (e) {
|
||||
return ""
|
||||
}
|
||||
},
|
||||
getPdfLib() {
|
||||
return window.pdfjsLib || window["pdfjs-dist/build/pdf"]
|
||||
},
|
||||
clearPdfPreview() {
|
||||
this.pdfRenderToken += 1
|
||||
this.pdfDoc = null
|
||||
this.pdfLoading = false
|
||||
this.pdfError = ""
|
||||
this.pdfPageCount = 0
|
||||
this.pdfRenderedPages = 0
|
||||
if (this.pdfLoadingTask && this.pdfLoadingTask.destroy) {
|
||||
try {
|
||||
this.pdfLoadingTask.destroy()
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
this.pdfLoadingTask = null
|
||||
if (this.$refs.pdfPreviewContainer) {
|
||||
this.$refs.pdfPreviewContainer.innerHTML = ""
|
||||
}
|
||||
},
|
||||
renderLinePdf() {
|
||||
this.clearPdfPreview()
|
||||
if (!this.lineContentHref) {
|
||||
return
|
||||
}
|
||||
const pdfLib = this.getPdfLib()
|
||||
if (!pdfLib || !pdfLib.getDocument) {
|
||||
this.pdfError = "PDF预览组件加载失败"
|
||||
return
|
||||
}
|
||||
if (pdfLib.GlobalWorkerOptions) {
|
||||
pdfLib.GlobalWorkerOptions.workerSrc = "/assets/platform/plugins/pdfJs/h5/pdf.worker.js"
|
||||
}
|
||||
const token = this.pdfRenderToken
|
||||
this.pdfLoading = true
|
||||
this.pdfLoadingTask = pdfLib.getDocument({
|
||||
url: this.lineContentHref,
|
||||
disableAutoFetch: false,
|
||||
disableStream: false,
|
||||
disableRange: false
|
||||
})
|
||||
this.pdfLoadingTask.promise.then((pdf) => {
|
||||
if (token !== this.pdfRenderToken) {
|
||||
return
|
||||
}
|
||||
this.pdfDoc = pdf
|
||||
this.pdfPageCount = pdf.numPages || 0
|
||||
return this.renderPdfPage(1, token)
|
||||
}).then(() => {
|
||||
if (token !== this.pdfRenderToken) {
|
||||
return
|
||||
}
|
||||
this.renderRestPdfPages(2, token)
|
||||
}).catch(() => {
|
||||
if (token !== this.pdfRenderToken) {
|
||||
return
|
||||
}
|
||||
this.pdfLoading = false
|
||||
this.pdfError = "PDF加载失败,请稍后重试"
|
||||
})
|
||||
},
|
||||
renderPdfPage(pageNumber, token) {
|
||||
if (!this.pdfDoc || token !== this.pdfRenderToken || pageNumber > this.pdfPageCount) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
return this.pdfDoc.getPage(pageNumber).then((page) => {
|
||||
if (token !== this.pdfRenderToken || !this.$refs.pdfPreviewContainer) {
|
||||
return
|
||||
}
|
||||
const container = this.$refs.pdfPreviewContainer
|
||||
const baseViewport = page.getViewport({scale: 1})
|
||||
const containerWidth = Math.max(container.clientWidth || 0, window.innerWidth - 44)
|
||||
const cssScale = containerWidth / baseViewport.width
|
||||
const outputScale = Math.min(window.devicePixelRatio || 1, 2)
|
||||
const viewport = page.getViewport({scale: cssScale})
|
||||
const pageEl = document.createElement("div")
|
||||
pageEl.className = "tour-pdf-page"
|
||||
const canvas = document.createElement("canvas")
|
||||
const context = canvas.getContext("2d")
|
||||
canvas.width = Math.floor(viewport.width * outputScale)
|
||||
canvas.height = Math.floor(viewport.height * outputScale)
|
||||
canvas.style.width = Math.floor(viewport.width) + "px"
|
||||
canvas.style.height = Math.floor(viewport.height) + "px"
|
||||
pageEl.appendChild(canvas)
|
||||
container.appendChild(pageEl)
|
||||
const renderContext = {
|
||||
canvasContext: context,
|
||||
viewport: viewport,
|
||||
transform: outputScale !== 1 ? [outputScale, 0, 0, outputScale, 0, 0] : null
|
||||
}
|
||||
return page.render(renderContext).promise.then(() => {
|
||||
if (token === this.pdfRenderToken) {
|
||||
this.pdfRenderedPages = Math.max(this.pdfRenderedPages, pageNumber)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
renderRestPdfPages(pageNumber, token) {
|
||||
if (token !== this.pdfRenderToken || !this.pdfDoc || pageNumber > this.pdfPageCount) {
|
||||
this.pdfLoading = false
|
||||
return
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.renderPdfPage(pageNumber, token).then(() => {
|
||||
this.renderRestPdfPages(pageNumber + 1, token)
|
||||
}).catch(() => {
|
||||
if (token === this.pdfRenderToken) {
|
||||
this.pdfLoading = false
|
||||
this.pdfError = "部分页面加载失败"
|
||||
}
|
||||
})
|
||||
}, 120)
|
||||
},
|
||||
loadDetail() {
|
||||
if (!this.matterId || !this.lineId) {
|
||||
this.detailLoading = false
|
||||
vant.Dialog.alert({
|
||||
title: "提示",
|
||||
message: "线路参数不完整",
|
||||
confirmButtonColor: "#1867b0"
|
||||
})
|
||||
return
|
||||
}
|
||||
Promise.all([
|
||||
this.$axios.post("/platform/tour/signup/lineDetail", { lineId: this.lineId }),
|
||||
this.$axios.post("/platform/tour/signup/signupDetail", { matterId: this.matterId })
|
||||
]).then(([lineRes, signupRes]) => {
|
||||
this.detailLoading = false
|
||||
if (lineRes.code !== 0) {
|
||||
vant.Toast(lineRes.msg || "线路详情加载失败")
|
||||
} else {
|
||||
this.lineDetail = lineRes.data || {}
|
||||
this.lineContentHref = this.parseLineContentHref(this.lineDetail.lineContent)
|
||||
this.$nextTick(() => {
|
||||
this.renderLinePdf()
|
||||
})
|
||||
}
|
||||
if (signupRes.code !== 0) {
|
||||
vant.Toast(signupRes.msg || "报名信息加载失败")
|
||||
} else {
|
||||
const data = signupRes.data || {}
|
||||
const process = data.process || {}
|
||||
this.signupDetail = Object.assign({}, data.matter || {}, {
|
||||
signed: !!(data.ledger && data.ledger.id)
|
||||
}, process)
|
||||
}
|
||||
}).catch(() => {
|
||||
this.detailLoading = false
|
||||
vant.Toast("线路详情加载失败")
|
||||
})
|
||||
},
|
||||
onApply() {
|
||||
if (!this.matterId) {
|
||||
vant.Toast("报名事项信息不完整")
|
||||
return
|
||||
}
|
||||
const goApply = () => {
|
||||
window.location.href = "/platform/tour/signup/h5/apply?matterId=" + encodeURIComponent(this.matterId)
|
||||
}
|
||||
this.$axios.post("/platform/tour/signup/signupEligibilityNotice", { matterId: this.matterId }).then((res) => {
|
||||
if (res.code !== 0) {
|
||||
vant.Dialog.alert({
|
||||
title: "温馨提醒",
|
||||
message: res.msg || "当前不能报名",
|
||||
confirmButtonColor: "#1867b0"
|
||||
})
|
||||
return
|
||||
}
|
||||
const data = res.data || {}
|
||||
if (!data.noticeRequired) {
|
||||
goApply()
|
||||
return
|
||||
}
|
||||
if (data.canApply) {
|
||||
vant.Dialog.confirm({
|
||||
title: "温馨提醒",
|
||||
message: data.message || "当前可报名,是否继续报名?",
|
||||
confirmButtonText: "继续报名",
|
||||
cancelButtonText: "稍后报名",
|
||||
confirmButtonColor: "#1867b0"
|
||||
}).then(goApply).catch(() => {})
|
||||
} else {
|
||||
vant.Dialog.alert({
|
||||
title: "温馨提醒",
|
||||
message: data.message || "当前不能报名",
|
||||
confirmButtonColor: "#1867b0"
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
revokeSignup() {
|
||||
vant.Dialog.confirm({
|
||||
title: "提示",
|
||||
message: "您确定要撤回吗?",
|
||||
confirmButtonColor: "#ee2f2f"
|
||||
}).then(() => {
|
||||
this.$axios.post("/flow/common/revokeTask", { taskId: this.signupDetail.startTaskId }).then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
vant.Toast.success(resp.msg || "撤回成功")
|
||||
this.loadDetail()
|
||||
} else {
|
||||
vant.Dialog.alert({ title: "提示", message: resp.msg || "撤回失败", confirmButtonColor: "#1867b0" })
|
||||
}
|
||||
})
|
||||
}).catch(() => {})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadDetail()
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.clearPdfPreview()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,787 @@
|
||||
<!--#
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<style scoped>
|
||||
.tour-line-page {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f4f6f8;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tour-line-scroll {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 18px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tour-line-toolbar {
|
||||
padding: 10px 0 8px 16px;
|
||||
background: #f4f6f8;
|
||||
}
|
||||
|
||||
.tour-line-toolbar /deep/ .van-search {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tour-line-toolbar .van-search {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tour-line-toolbar /deep/ .van-search__content {
|
||||
height: 38px;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.tour-line-toolbar .van-search__content {
|
||||
height: 38px;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.tour-line-toolbar /deep/ .van-search__action {
|
||||
padding: 0 0 0 6px;
|
||||
}
|
||||
|
||||
.tour-line-toolbar .van-search__action {
|
||||
padding: 0 0 0 6px;
|
||||
}
|
||||
|
||||
.tour-line-search-button {
|
||||
width: 42px;
|
||||
height: 38px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: #1e88e5;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tour-line-search-row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tour-line-union-entry {
|
||||
width: 48px;
|
||||
min-height: 42px;
|
||||
flex-shrink: 0;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
font-size: 11px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.tour-line-union-entry .van-icon {
|
||||
color: #64748b;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tour-line-union-entry--active,
|
||||
.tour-line-union-entry--active .van-icon {
|
||||
color: #0b75bd;
|
||||
}
|
||||
|
||||
.tour-line-union-popup {
|
||||
max-height: 68vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tour-line-union-popup__title {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
padding: 14px 16px 10px;
|
||||
background: #ffffff;
|
||||
color: #0f172a;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tour-line-union-popup__list {
|
||||
overflow-y: auto;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.tour-line-union-popup__item {
|
||||
min-height: 48px;
|
||||
transition: background-color 0.18s ease, color 0.18s ease;
|
||||
}
|
||||
|
||||
.tour-line-union-popup__item:hover,
|
||||
.tour-line-union-popup__item:active {
|
||||
background: rgba(30, 136, 229, 0.1);
|
||||
}
|
||||
|
||||
.tour-line-union-popup__item:hover /deep/ .van-cell__title,
|
||||
.tour-line-union-popup__item:active /deep/ .van-cell__title {
|
||||
color: #0b75bd;
|
||||
}
|
||||
|
||||
.tour-line-union-popup__item--active /deep/ .van-cell__title {
|
||||
color: #0b75bd;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tour-line-shortcuts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.tour-line-shortcut {
|
||||
min-height: 76px;
|
||||
padding: 12px 8px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.05);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tour-line-shortcut--active {
|
||||
border-color: #0b75bd;
|
||||
background: rgba(11, 117, 189, 0.08);
|
||||
}
|
||||
|
||||
.tour-line-shortcut__icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.tour-line-shortcut__text {
|
||||
min-width: 0;
|
||||
color: #0f172a;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tour-line-shortcut__hint {
|
||||
margin-top: 2px;
|
||||
color: #64748b;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.tour-line-list {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.tour-line-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
min-height: 112px;
|
||||
margin: 8px 0;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.05);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tour-line-thumb {
|
||||
width: 145px;
|
||||
height: 98px;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
background: #e5edf6;
|
||||
}
|
||||
|
||||
.tour-line-thumb img,
|
||||
.tour-line-thumb .van-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tour-line-thumb-empty {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8a98a8;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tour-line-main {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tour-line-signed-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
background: #a78bfa;
|
||||
color: #ffffff;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
box-shadow: 0 2px 5px rgba(167, 139, 250, 0.32);
|
||||
}
|
||||
|
||||
.tour-line-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tour-line-title {
|
||||
min-width: 0;
|
||||
color: #0f172a;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.tour-line-type-badge {
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
padding: 0 5px;
|
||||
border: 1px solid #ffd6a8;
|
||||
border-radius: 4px;
|
||||
background: #fff3e5;
|
||||
color: #d97706;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 17px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tour-line-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.tour-line-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
padding: 0 7px;
|
||||
border-radius: 4px;
|
||||
background: #39a9ed;
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.tour-line-info {
|
||||
margin-top: 5px;
|
||||
color: #111827;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.tour-line-info span {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.tour-line-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.tour-line-action {
|
||||
min-width: 70px;
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #1e88e5;
|
||||
border-radius: 14px;
|
||||
background: #1e88e5;
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.tour-line-action--danger {
|
||||
border-color: #ee2f2f;
|
||||
background: #ee2f2f;
|
||||
}
|
||||
|
||||
.tour-line-action--disabled {
|
||||
border-color: #d1d5db;
|
||||
background: #e5e7eb;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.tour-line-card {
|
||||
gap: 9px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.tour-line-thumb {
|
||||
width: 126px;
|
||||
height: 92px;
|
||||
}
|
||||
|
||||
.tour-line-title {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" class="tour-line-page">
|
||||
<van-nav-bar title="疗休养线路" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
|
||||
|
||||
<div class="tour-line-toolbar">
|
||||
<div class="tour-line-search-row">
|
||||
<van-search
|
||||
v-model="pageForm.lineName"
|
||||
show-action
|
||||
clearable
|
||||
placeholder="请输入线路名称搜索"
|
||||
@search="doSearch"
|
||||
@clear="doSearch">
|
||||
<template #action>
|
||||
<button type="button" class="tour-line-search-button" @click="doSearch">
|
||||
<van-icon name="search"></van-icon>
|
||||
</button>
|
||||
</template>
|
||||
</van-search>
|
||||
<button
|
||||
type="button"
|
||||
class="tour-line-union-entry"
|
||||
:class="{'tour-line-union-entry--active': !!pageForm.unionId}"
|
||||
@click="openUnionPopup">
|
||||
<van-icon name="friends-o"></van-icon>
|
||||
<span>分工会</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="tour-line-shortcuts">
|
||||
<div
|
||||
class="tour-line-shortcut"
|
||||
:class="{'tour-line-shortcut--active': pageForm.lineType === '省内线路' && pageForm.directFamilyUnitLine === null}"
|
||||
@click="toggleLineType('省内线路')">
|
||||
<img class="tour-line-shortcut__icon" src="/assets/platform/images/tour/tour-in-province.png" alt="省内线路">
|
||||
<div class="tour-line-shortcut__text">
|
||||
<div>省内线路</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tour-line-shortcut"
|
||||
:class="{'tour-line-shortcut--active': pageForm.lineType === '省外线路' && pageForm.directFamilyUnitLine === null}"
|
||||
@click="toggleLineType('省外线路')">
|
||||
<img class="tour-line-shortcut__icon" src="/assets/platform/images/tour/tour-out-province.png" alt="省外线路">
|
||||
<div class="tour-line-shortcut__text">
|
||||
<div>省外线路</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tour-line-shortcut"
|
||||
:class="{'tour-line-shortcut--active': pageForm.directFamilyUnitLine === true}"
|
||||
@click="toggleDirectFamilyLine">
|
||||
<img class="tour-line-shortcut__icon" src="/assets/platform/images/tour/tour-direct-family.jpg" alt="直系亲属">
|
||||
<div class="tour-line-shortcut__text">
|
||||
<div>直系亲属</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tour-line-scroll">
|
||||
<van-list
|
||||
v-model="loading"
|
||||
:finished="finished"
|
||||
finished-text="没有更多了"
|
||||
@load="loadData">
|
||||
<div class="tour-line-list">
|
||||
<div class="tour-line-card" v-for="row in list" :key="row.matterId" @click="onLine(row)">
|
||||
<div class="tour-line-thumb">
|
||||
<van-image v-if="thumbUrl(row)" :src="thumbUrl(row)" fit="cover"></van-image>
|
||||
<div v-else class="tour-line-thumb-empty">暂无图片</div>
|
||||
</div>
|
||||
<div class="tour-line-main">
|
||||
<div class="tour-line-title-row">
|
||||
<div class="tour-line-title">{{ row.lineName || '未命名线路' }}</div>
|
||||
<span v-if="lineTypeBadge(row.lineType)" class="tour-line-type-badge">{{ lineTypeBadge(row.lineType) }}</span>
|
||||
<span v-if="isSigned(row)" class="tour-line-signed-icon">
|
||||
<van-icon name="good-job"></van-icon>
|
||||
</span>
|
||||
</div>
|
||||
<div class="tour-line-tags">
|
||||
<span v-if="row.lotName" class="tour-line-tag">{{ row.lotName }}</span>
|
||||
<span v-if="row.unionName" class="tour-line-tag">{{ row.unionName }}</span>
|
||||
</div>
|
||||
<div class="tour-line-info">
|
||||
<div><span>出行开始时间:</span>{{ row.travelStartTime || '暂无' }}</div>
|
||||
<div><span>出行结束时间:</span>{{ row.travelEndTime || '暂无' }}</div>
|
||||
<!-- 临时屏蔽旅行社信息,保留字段和接口便于后续恢复。
|
||||
<div><span>旅行社:</span>{{ row.travelAgencyName || '暂无' }}</div>
|
||||
-->
|
||||
</div>
|
||||
<div class="tour-line-actions">
|
||||
<button
|
||||
v-if="showSignupAction(row)"
|
||||
type="button"
|
||||
class="tour-line-action"
|
||||
:class="{
|
||||
'tour-line-action--danger': canRevokeSignup(row),
|
||||
'tour-line-action--disabled': signupActionDisabled(row)
|
||||
}"
|
||||
:disabled="signupActionDisabled(row)"
|
||||
@click.stop="handleSignupAction(row)">
|
||||
{{ signupActionText(row) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-list>
|
||||
|
||||
<van-empty v-if="!loading && list.length === 0" description="暂无线路"></van-empty>
|
||||
</div>
|
||||
|
||||
<van-popup v-model="showUnionPopup" position="bottom" round>
|
||||
<div class="tour-line-union-popup">
|
||||
<div class="tour-line-union-popup__title">选择分工会</div>
|
||||
<div class="tour-line-union-popup__list">
|
||||
<van-cell
|
||||
v-for="item in unionList"
|
||||
:key="item.id || 'all'"
|
||||
clickable
|
||||
class="tour-line-union-popup__item"
|
||||
:class="{'tour-line-union-popup__item--active': pageForm.unionId === item.id}"
|
||||
:title="item.name"
|
||||
@click="selectUnion(item)">
|
||||
<template #right-icon>
|
||||
<van-icon v-if="pageForm.unionId === item.id" name="success" color="#0b75bd"></van-icon>
|
||||
</template>
|
||||
</van-cell>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
const vue = new Vue({
|
||||
el: "#app",
|
||||
store,
|
||||
data() {
|
||||
return {
|
||||
list: [],
|
||||
loading: false,
|
||||
finished: false,
|
||||
lineChecking: false,
|
||||
showUnionPopup: false,
|
||||
unionList: [
|
||||
{id: "", name: "全部分工会"}
|
||||
],
|
||||
pageForm: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
totalCount: 0,
|
||||
year: new Date().getFullYear(),
|
||||
lineName: "",
|
||||
lineType: "",
|
||||
unionId: "",
|
||||
directFamilyUnitLine: null,
|
||||
pageOrderName: "lineName",
|
||||
pageOrderBy: "ascending"
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
thumbUrl(row) {
|
||||
return row.lineMobileThumb || row.agencyMobileThumb || ""
|
||||
},
|
||||
lineTypeBadge(lineType) {
|
||||
if (lineType === "省内线路" || lineType === "省内") {
|
||||
return "省内"
|
||||
}
|
||||
if (lineType === "省外线路" || lineType === "省外") {
|
||||
return "省外"
|
||||
}
|
||||
return ""
|
||||
},
|
||||
isSigned(row) {
|
||||
return !!(row && (row.signed === true || row.signed === 1 || row.signed === "1" || row.ledgerId))
|
||||
},
|
||||
isApprovalSignup(row) {
|
||||
return !!(row && this.isSigned(row) && row.instanceId)
|
||||
},
|
||||
canRevokeSignup(row) {
|
||||
return this.isApprovalSignup(row) && this.toBoolean(row.canRevoke) && !!row.startTaskId
|
||||
},
|
||||
isFinishedSignup(row) {
|
||||
return this.isApprovalSignup(row) && Number(row.instanceState) === 20
|
||||
},
|
||||
isRejectedSignup(row) {
|
||||
return this.isApprovalSignup(row) && Number(row.instanceState) === 45
|
||||
},
|
||||
canModifySignup(row) {
|
||||
if (!row || !this.isSigned(row)) {
|
||||
return true
|
||||
}
|
||||
if (!this.isApprovalSignup(row)) {
|
||||
return true
|
||||
}
|
||||
return !this.isFinishedSignup(row) && !this.isRejectedSignup(row) && row.taskKey === "startTask"
|
||||
},
|
||||
signupActionText(row) {
|
||||
if (this.isSigned(row)) {
|
||||
if (this.canRevokeSignup(row)) {
|
||||
return "撤回"
|
||||
}
|
||||
if (this.isRejectedSignup(row)) {
|
||||
return "已拒绝"
|
||||
}
|
||||
return this.canModifySignup(row) ? "修改" : "审核中"
|
||||
}
|
||||
return "报名"
|
||||
},
|
||||
showSignupAction(row) {
|
||||
return this.isSigned(row) && this.canModifySignup(row)
|
||||
},
|
||||
signupActionDisabled(row) {
|
||||
return !!(this.isSigned(row) && this.isApprovalSignup(row) && (this.isRejectedSignup(row) || (!this.canRevokeSignup(row) && !this.canModifySignup(row))))
|
||||
},
|
||||
toBoolean(value) {
|
||||
return value === true || value === 1 || value === "1" || value === "true" || value === "TRUE"
|
||||
},
|
||||
handleSignupAction(row) {
|
||||
if (this.signupActionDisabled(row)) {
|
||||
return
|
||||
}
|
||||
if (this.canRevokeSignup(row)) {
|
||||
this.revokeSignup(row)
|
||||
return
|
||||
}
|
||||
this.goApply(row)
|
||||
},
|
||||
goApply(row) {
|
||||
if (!row || !row.matterId) {
|
||||
vant.Toast("报名事项信息不完整")
|
||||
return
|
||||
}
|
||||
const go = () => {
|
||||
window.location.href = "/platform/tour/signup/h5/apply?matterId=" + encodeURIComponent(row.matterId)
|
||||
}
|
||||
this.$axios.post("/platform/tour/signup/signupEligibilityNotice", { matterId: row.matterId }).then((res) => {
|
||||
if (res.code !== 0) {
|
||||
vant.Dialog.alert({
|
||||
title: "温馨提醒",
|
||||
message: res.msg || "当前不能报名",
|
||||
confirmButtonColor: "#1867b0"
|
||||
})
|
||||
return
|
||||
}
|
||||
const data = res.data || {}
|
||||
if (!data.noticeRequired) {
|
||||
go()
|
||||
return
|
||||
}
|
||||
if (data.canApply) {
|
||||
vant.Dialog.confirm({
|
||||
title: "温馨提醒",
|
||||
message: data.message || "当前可报名,是否继续报名?",
|
||||
confirmButtonText: "继续报名",
|
||||
cancelButtonText: "稍后报名",
|
||||
confirmButtonColor: "#1867b0"
|
||||
}).then(go).catch(() => {})
|
||||
} else {
|
||||
vant.Dialog.alert({
|
||||
title: "温馨提醒",
|
||||
message: data.message || "当前不能报名",
|
||||
confirmButtonColor: "#1867b0"
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
revokeSignup(row) {
|
||||
vant.Dialog.confirm({
|
||||
title: "提示",
|
||||
message: "您确定要撤回吗?",
|
||||
confirmButtonColor: "#ee2f2f"
|
||||
}).then(() => {
|
||||
this.$axios.post("/flow/common/revokeTask", { taskId: row.startTaskId }).then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
vant.Toast.success(resp.msg || "撤回成功")
|
||||
this.doSearch()
|
||||
} else {
|
||||
vant.Dialog.alert({ title: "提示", message: resp.msg || "撤回失败", confirmButtonColor: "#1867b0" })
|
||||
}
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
doSearch() {
|
||||
this.pageForm.pageNumber = 1
|
||||
this.pageForm.totalCount = 0
|
||||
this.list = []
|
||||
this.finished = false
|
||||
this.loadData()
|
||||
},
|
||||
toggleLineType(lineType) {
|
||||
const active = this.pageForm.lineType === lineType && this.pageForm.directFamilyUnitLine === null
|
||||
this.pageForm.lineType = active ? "" : lineType
|
||||
this.pageForm.directFamilyUnitLine = null
|
||||
this.doSearch()
|
||||
},
|
||||
toggleDirectFamilyLine() {
|
||||
const active = this.pageForm.directFamilyUnitLine === true
|
||||
this.pageForm.directFamilyUnitLine = active ? null : true
|
||||
this.pageForm.lineType = ""
|
||||
this.doSearch()
|
||||
},
|
||||
openUnionPopup() {
|
||||
this.showUnionPopup = true
|
||||
if (this.unionList.length <= 1) {
|
||||
this.loadUnionOptions()
|
||||
}
|
||||
},
|
||||
selectUnion(item) {
|
||||
const unionId = item && item.id ? item.id : ""
|
||||
this.showUnionPopup = false
|
||||
if (this.pageForm.unionId === unionId) {
|
||||
return
|
||||
}
|
||||
this.pageForm.unionId = unionId
|
||||
this.doSearch()
|
||||
},
|
||||
loadUnionOptions() {
|
||||
this.$axios.post("/platform/tour/signup/unionOptions").then((res) => {
|
||||
if (res.code !== 0) {
|
||||
return
|
||||
}
|
||||
const rows = res.data || []
|
||||
this.unionList = [{id: "", name: "全部分工会"}].concat(rows.map((item) => {
|
||||
return {
|
||||
id: item.id || "",
|
||||
name: item.name || "未命名分工会"
|
||||
}
|
||||
}))
|
||||
})
|
||||
},
|
||||
loadData() {
|
||||
if (this.finished) {
|
||||
this.loading = false
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
this.$axios.post("/platform/tour/signup/h5/pageData", this.pageForm).then((res) => {
|
||||
this.loading = false
|
||||
if (res.code !== 0) {
|
||||
vant.Toast(res.msg || "线路加载失败")
|
||||
this.finished = true
|
||||
return
|
||||
}
|
||||
const data = res.data || {}
|
||||
const rows = data.list || []
|
||||
this.pageForm.totalCount = data.totalCount || 0
|
||||
this.list = this.pageForm.pageNumber === 1 ? rows : this.list.concat(rows)
|
||||
if (this.list.length >= this.pageForm.totalCount || rows.length === 0) {
|
||||
this.finished = true
|
||||
} else {
|
||||
this.pageForm.pageNumber += 1
|
||||
}
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
this.finished = true
|
||||
})
|
||||
},
|
||||
onLine(row) {
|
||||
if (!row || !row.lineId || !row.matterId) {
|
||||
vant.Toast("线路信息不完整")
|
||||
return
|
||||
}
|
||||
if (this.lineChecking) {
|
||||
return
|
||||
}
|
||||
this.lineChecking = true
|
||||
axios.post("/platform/tour/signup/signupDetail", $.param({ matterId: row.matterId }), {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
|
||||
"x-requested-with": "XMLHttpRequest"
|
||||
}
|
||||
}).then((resp) => {
|
||||
this.lineChecking = false
|
||||
const res = resp.data || {}
|
||||
if (res.code !== 0) {
|
||||
vant.Dialog.alert({
|
||||
title: "温馨提醒",
|
||||
message: res.msg || "当前线路暂不能报名",
|
||||
confirmButtonColor: "#1867b0"
|
||||
})
|
||||
return
|
||||
}
|
||||
window.location.href = "/platform/tour/signup/h5/lineInfo?matterId="
|
||||
+ encodeURIComponent(row.matterId)
|
||||
+ "&lineId="
|
||||
+ encodeURIComponent(row.lineId)
|
||||
}).catch((err) => {
|
||||
this.lineChecking = false
|
||||
vant.Dialog.alert({
|
||||
title: "温馨提醒",
|
||||
message: err && err.msg ? err.msg : "当前线路暂不能报名",
|
||||
confirmButtonColor: "#1867b0"
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadUnionOptions()
|
||||
this.loadData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,453 @@
|
||||
<!--#
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<style scoped>
|
||||
.tour-approval-page {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f4f6f8;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tour-approval-sticky {
|
||||
flex-shrink: 0;
|
||||
background: #f4f6f8;
|
||||
}
|
||||
|
||||
.tour-approval-list-scroll {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 18px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tour-approval-search /deep/ .van-search__content,
|
||||
.tour-approval-search .van-search__content {
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.tour-card-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tour-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 22px;
|
||||
padding: 0 8px;
|
||||
border-radius: 4px;
|
||||
background: #eef6ff;
|
||||
color: #0b75bd;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tour-tag.gray {
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.tour-tag.warn {
|
||||
background: #fff3e5;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.tour-approval-sheet {
|
||||
max-height: 86vh;
|
||||
overflow-y: auto;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
background: #f4f6f8;
|
||||
}
|
||||
|
||||
.tour-section {
|
||||
margin: 10px 12px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.tour-section-title {
|
||||
position: relative;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid #edf0f4;
|
||||
color: #0b75bd;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.tour-section-title::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 0;
|
||||
width: 30px;
|
||||
height: 4px;
|
||||
border-radius: 0 0 3px 3px;
|
||||
background: #0b75bd;
|
||||
}
|
||||
|
||||
.tour-approval-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 10px 12px 14px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.tour-process-opinion {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" class="tour-approval-page" v-cloak>
|
||||
<van-nav-bar title="分工会审核" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
|
||||
|
||||
<van-sticky offset-top="46px" class="tour-approval-sticky">
|
||||
<van-search
|
||||
class="tour-approval-search"
|
||||
v-model="pageForm.keyword"
|
||||
placeholder="请输入工号/姓名/线路搜索"
|
||||
show-action
|
||||
clearable
|
||||
input-align="left"
|
||||
@search="doSearch"
|
||||
@clear="doSearch">
|
||||
<template #action>
|
||||
<div @click="doSearch">搜索</div>
|
||||
</template>
|
||||
</van-search>
|
||||
<van-dropdown-menu :close-on-click-outside="false" :close-on-click-overlay="false">
|
||||
<van-dropdown-item :options="auditOptions" @change="doSearch" v-model="pageForm.audit"></van-dropdown-item>
|
||||
<van-dropdown-item :options="lineTypeOptions" @change="doSearch" v-model="pageForm.lineType"></van-dropdown-item>
|
||||
</van-dropdown-menu>
|
||||
</van-sticky>
|
||||
|
||||
<div class="tour-approval-list-scroll">
|
||||
<table-list api="/platform/tour/unionApproval/pageData" :page_form.sync="pageForm" ref="tableListRef" title="userName" @ready="onReady">
|
||||
<template v-slot="{index,row}">
|
||||
<div class="tour-card-tags">
|
||||
<span v-if="row.jobNo" class="tour-tag">{{ row.jobNo }}</span>
|
||||
<span v-if="row.lineType" class="tour-tag gray">{{ row.lineType }}</span>
|
||||
<span v-if="row.overCostReimbursed" class="tour-tag warn">报销超出费用</span>
|
||||
</div>
|
||||
<table-column label="线路名称">{{ row.lineName || '' }}</table-column>
|
||||
<table-column label="出行时段">{{ row.travelPeriod || '' }}</table-column>
|
||||
<table-column label="报销超出费用">{{ row.overCostReimbursed ? '是' : '否' }}</table-column>
|
||||
<table-column label="当前节点">{{ row.curTaskName || '' }}</table-column>
|
||||
<table-column label="流程状态">
|
||||
<enum-tag
|
||||
v-if="row.instanceState !== null && row.instanceState !== undefined && row.instanceState !== ''"
|
||||
:value="row.instanceState"
|
||||
name="ProcessInstanceStateEnum"
|
||||
label_key="message"
|
||||
size="small">
|
||||
</enum-tag>
|
||||
</table-column>
|
||||
</template>
|
||||
<template #actions="{index,row}">
|
||||
<div class="action-btn" @click="onView(row)">
|
||||
<i class="fa fa-eye"></i>
|
||||
<span>查看</span>
|
||||
</div>
|
||||
<div class="action-btn" v-if="row.taskState === 10" @click="onApproval(row)">
|
||||
<i class="fa fa-check"></i>
|
||||
<span>审核</span>
|
||||
</div>
|
||||
</template>
|
||||
</table-list>
|
||||
</div>
|
||||
|
||||
<tour-info ref="infoRef">
|
||||
<div v-if="showApprovalForm" class="tour-section">
|
||||
<div class="tour-section-title">{{ formData.taskName || '审核' }}</div>
|
||||
<van-form ref="formRef">
|
||||
<van-field
|
||||
v-model="formData.tf_opinion"
|
||||
name="tf_opinion"
|
||||
label="审批意见"
|
||||
placeholder="请输入审批意见"
|
||||
:rules="[{ required: true, message: '请填写审批意见' }]"
|
||||
required
|
||||
type="textarea"
|
||||
rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit>
|
||||
</van-field>
|
||||
</van-form>
|
||||
<div class="tour-approval-actions">
|
||||
<van-button type="danger" block @click="handleTaskAction(6)">退回</van-button>
|
||||
<van-button type="danger" block @click="handleTaskAction(2)">拒绝</van-button>
|
||||
<van-button type="primary" block @click="handleTaskAction(1)">同意</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</tour-info>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
const TOUR_INFO = {
|
||||
template: /*language=HTML*/ `
|
||||
<van-action-sheet v-model="visible" title="报名详情">
|
||||
<div class="tour-approval-sheet">
|
||||
<div class="tour-section">
|
||||
<div class="tour-section-title">教职工信息</div>
|
||||
<van-cell title="工号" :value="detail.jobNo || ''"></van-cell>
|
||||
<van-cell title="姓名" :value="detail.userName || ''"></van-cell>
|
||||
<van-cell title="性别" :value="detail.gender || ''"></van-cell>
|
||||
<van-cell title="身份证号" :value="detail.idCard || ''"></van-cell>
|
||||
<van-cell title="所在单位" :value="detail.unitName || ''"></van-cell>
|
||||
<van-cell title="所在工会" :value="detail.unionName || ''"></van-cell>
|
||||
</div>
|
||||
|
||||
<div class="tour-section">
|
||||
<div class="tour-section-title">报名信息</div>
|
||||
<van-cell title="报名线路" :value="detail.lineName || ''"></van-cell>
|
||||
<van-cell title="线路类型" :value="detail.lineType || ''"></van-cell>
|
||||
<van-cell title="出行时段" :value="detail.travelPeriod || ''"></van-cell>
|
||||
<van-cell title="报名时间" :value="detail.signupTime || ''"></van-cell>
|
||||
<van-cell title="报名酒店" :value="detail.hotelName || ''"></van-cell>
|
||||
<van-cell title="旅行社" :value="detail.travelAgencyName || ''"></van-cell>
|
||||
<van-cell v-if="fillBedInfo" title="床型" :value="detail.bedType || ''"></van-cell>
|
||||
<van-cell v-if="fillBedInfo" title="床位信息" :value="detail.bedInfo || ''"></van-cell>
|
||||
<van-cell v-if="fillBedInfo" title="意向拼床人" :value="detail.intendedRoommate || ''"></van-cell>
|
||||
<van-cell title="携带家属" :value="familyText()"></van-cell>
|
||||
<van-cell title="报销超出费用" :value="detail.overCostReimbursed ? '是' : '否'"></van-cell>
|
||||
</div>
|
||||
|
||||
<div v-if="isDirectFamilyLine()" class="tour-section">
|
||||
<div class="tour-section-title">直系亲属线路</div>
|
||||
<van-cell title="亲属姓名" :value="directRelative.relativeName || ''"></van-cell>
|
||||
<van-cell title="所在单位" :value="directRelative.unitName || ''"></van-cell>
|
||||
<van-cell title="亲属关系" :value="directRelative.relationshipName || ''"></van-cell>
|
||||
<van-cell title="线路名称" :value="directRelative.lineName || ''"></van-cell>
|
||||
<van-cell title="出行开始" :value="directRelative.travelStartTime || ''"></van-cell>
|
||||
<van-cell title="出行结束" :value="directRelative.travelEndTime || ''"></van-cell>
|
||||
</div>
|
||||
|
||||
<div v-if="showFamilySection()" class="tour-section">
|
||||
<div class="tour-section-title">家属信息</div>
|
||||
<template v-for="(item,index) in familyData">
|
||||
<van-cell :key="'title_' + index" :title="'家属' + (index + 1)"></van-cell>
|
||||
<van-cell :key="'name_' + index" title="姓名" :value="item.familyName || ''"></van-cell>
|
||||
<van-cell :key="'gender_' + index" title="性别" :value="item.gender || ''"></van-cell>
|
||||
<van-cell :key="'id_' + index" title="身份证号" :value="item.idCard || ''"></van-cell>
|
||||
<van-cell :key="'rel_' + index" title="关系" :value="item.relationship || ''"></van-cell>
|
||||
<van-cell v-if="fillBedInfo" :key="'bed_' + index" title="床型" :value="item.bedType || ''"></van-cell>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="doneTasks.length > 0" class="tour-section">
|
||||
<div class="tour-section-title">流程记录</div>
|
||||
<template v-for="task in doneTasks">
|
||||
<van-cell-group :key="task.id" :title="task.displayName || task.taskName">
|
||||
<van-cell title="办理时间" :value="task.finishTime || ''"></van-cell>
|
||||
<van-cell title="办理结果">
|
||||
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE" :value="task.ext && task.ext.submitType"></dict-tag>
|
||||
</van-cell>
|
||||
<van-cell title="办理意见" v-if="task.taskFormData" class="direction-column-cell">
|
||||
<div class="tour-process-opinion">{{ task.taskFormData.opinion || task.taskFormData.tf_opinion || '' }}</div>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<slot></slot>
|
||||
</div>
|
||||
</van-action-sheet>
|
||||
`,
|
||||
dicts: ["PROCESS_TASK_SUBMIT_TYPE"],
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
row: {},
|
||||
detail: {},
|
||||
familyData: [],
|
||||
directRelative: {},
|
||||
allowFamily: false,
|
||||
fillBedInfo: true,
|
||||
directFamilyUnitLine: false,
|
||||
doneTasks: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onOpen(row) {
|
||||
this.row = row || {}
|
||||
this.visible = true
|
||||
this.detail = {}
|
||||
this.familyData = []
|
||||
this.directRelative = {}
|
||||
this.allowFamily = false
|
||||
this.fillBedInfo = true
|
||||
this.directFamilyUnitLine = false
|
||||
this.doneTasks = []
|
||||
this.info()
|
||||
this.getDoneTasks()
|
||||
},
|
||||
onClose() {
|
||||
this.visible = false
|
||||
},
|
||||
info() {
|
||||
this.$axios.post("/platform/tour/unionApproval/detail", { id: this.row.id }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
const data = res.data || {}
|
||||
this.detail = data.ledger || {}
|
||||
this.detail.travelPeriod = data.travelPeriod || this.row.travelPeriod || ""
|
||||
this.familyData = data.families || []
|
||||
this.directRelative = data.directRelative || {}
|
||||
this.allowFamily = this.toBoolean(data.allowFamily)
|
||||
this.fillBedInfo = data.fillBedInfo === undefined || data.fillBedInfo === null || this.toBoolean(data.fillBedInfo)
|
||||
this.directFamilyUnitLine = this.toBoolean(data.directFamilyUnitLine)
|
||||
} else {
|
||||
vant.Toast(res.msg || "查询失败")
|
||||
}
|
||||
})
|
||||
},
|
||||
getDoneTasks() {
|
||||
this.$axios.post("/flow/common/doneTasks", { bizId: this.row.id }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.doneTasks = res.data || []
|
||||
}
|
||||
})
|
||||
},
|
||||
toBoolean(value) {
|
||||
return value === true || value === 1 || value === "1" || value === "true" || value === "TRUE"
|
||||
},
|
||||
isDirectFamilyLine() {
|
||||
return this.directFamilyUnitLine || this.toBoolean(this.detail.directFamilyUnitLine) || !!(this.directRelative && this.directRelative.id)
|
||||
},
|
||||
hasFamily() {
|
||||
return this.toBoolean(this.detail.hasFamily) || this.familyData.length > 0
|
||||
},
|
||||
showFamilySection() {
|
||||
return this.allowFamily && this.hasFamily() && !this.isDirectFamilyLine()
|
||||
},
|
||||
familyText() {
|
||||
if (this.isDirectFamilyLine() || !this.allowFamily) {
|
||||
return "否"
|
||||
}
|
||||
return this.hasFamily() ? "是" : "否"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: "#app",
|
||||
store,
|
||||
components: {
|
||||
"tour-info": TOUR_INFO
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pageForm: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
totalCount: 0,
|
||||
pageOrderName: "signupTime",
|
||||
pageOrderBy: "descending",
|
||||
audit: false,
|
||||
keyword: "",
|
||||
lineType: ""
|
||||
},
|
||||
auditOptions: [
|
||||
{ text: "未审核", value: false },
|
||||
{ text: "已审核", value: true }
|
||||
],
|
||||
lineTypeOptions: [
|
||||
{ text: "全部线路类型", value: "" }
|
||||
],
|
||||
formData: {},
|
||||
showApprovalForm: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
historyBack,
|
||||
onReady() {
|
||||
this.doSearch()
|
||||
},
|
||||
doSearch() {
|
||||
this.$nextTick(async () => {
|
||||
await this.loadLineTypeOptions()
|
||||
this.pageForm.pageNumber = 1
|
||||
this.pageForm.totalCount = 0
|
||||
this.$refs.tableListRef.doSearch()
|
||||
})
|
||||
},
|
||||
loadLineTypeOptions() {
|
||||
return this.$axios.post("/platform/tour/unionApproval/lineTypeOptions", {
|
||||
audit: this.pageForm.audit,
|
||||
keyword: this.pageForm.keyword
|
||||
}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
const options = (res.data || []).map(item => ({
|
||||
text: item.lineType,
|
||||
value: item.lineType
|
||||
})).filter(item => item.value)
|
||||
this.lineTypeOptions = [{ text: "全部线路类型", value: "" }].concat(options)
|
||||
if (this.pageForm.lineType && !this.lineTypeOptions.some(item => item.value === this.pageForm.lineType)) {
|
||||
this.pageForm.lineType = ""
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
onView(row) {
|
||||
this.showApprovalForm = false
|
||||
this.$refs.infoRef.onOpen(row)
|
||||
},
|
||||
onApproval(row) {
|
||||
this.showApprovalForm = true
|
||||
this.formData = {
|
||||
processTaskId: row.taskId,
|
||||
taskName: row.curTaskName || row.taskName,
|
||||
tf_opinion: ""
|
||||
}
|
||||
this.$refs.infoRef.onOpen(row)
|
||||
},
|
||||
async handleTaskAction(val) {
|
||||
try {
|
||||
await this.$refs.formRef.validate()
|
||||
} catch (e) {
|
||||
return
|
||||
}
|
||||
this.$dialog.confirm({
|
||||
title: "提示",
|
||||
message: "您确定要提交吗?"
|
||||
}).then(() => {
|
||||
const loading = this.$toast.loading({
|
||||
message: "加载中...",
|
||||
forbidClick: true,
|
||||
overlay: true,
|
||||
duration: 0
|
||||
})
|
||||
this.$axios.post("/flow/common/executeTask", {
|
||||
data: JSON.stringify({
|
||||
...this.formData,
|
||||
submitType: val
|
||||
})
|
||||
}).then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$refs.infoRef.onClose()
|
||||
this.$toast.success(res.msg || "操作成功")
|
||||
this.doSearch()
|
||||
} else {
|
||||
this.$toast(res.msg || "操作失败")
|
||||
}
|
||||
}).finally(() => {
|
||||
loading.close()
|
||||
})
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,612 @@
|
||||
<!--#
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<style>
|
||||
.learning-course-page {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f4f7fb;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.learning-course-page .van-nav-bar {
|
||||
background: #f4f7fb;
|
||||
}
|
||||
.learning-banner-wrap {
|
||||
flex-shrink: 0;
|
||||
padding: 10px 14px 0;
|
||||
}
|
||||
.learning-banner {
|
||||
height: 128px;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
background: #dce7f5;
|
||||
}
|
||||
.learning-banner-item {
|
||||
position: relative;
|
||||
height: 128px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #0b75bd, #33b3a6);
|
||||
}
|
||||
.learning-banner-item img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.learning-banner-fallback {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 0 22px;
|
||||
color: #ffffff;
|
||||
box-sizing: border-box;
|
||||
background:
|
||||
radial-gradient(circle at 14% 20%, rgba(255,255,255,.26), transparent 22%),
|
||||
linear-gradient(135deg, #0b75bd, #35b6a8 55%, #f6b24d);
|
||||
}
|
||||
.learning-banner-title {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 28px 14px 12px;
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
background: linear-gradient(to top, rgba(15, 23, 42, .64), transparent);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.learning-banner-fallback .learning-banner-title {
|
||||
position: static;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
.learning-filter-card {
|
||||
flex-shrink: 0;
|
||||
margin: 14px 14px 0;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
.learning-list-scroll {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.learning-filter-group {
|
||||
display: grid;
|
||||
grid-template-columns: 112px minmax(0, 1fr);
|
||||
min-height: 72px;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
background: #eef6ff;
|
||||
}
|
||||
.learning-filter-group + .learning-filter-group {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.learning-filter-category {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
color: #ffffff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.learning-filter-category-text {
|
||||
min-width: 0;
|
||||
}
|
||||
.learning-filter-category-title {
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.learning-filter-options {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
background: rgba(255, 255, 255, .55);
|
||||
}
|
||||
.learning-filter-options::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.learning-filter-item {
|
||||
flex: 0 0 72px;
|
||||
min-width: 72px;
|
||||
padding: 8px 2px 7px;
|
||||
border-left: 1px solid rgba(255, 255, 255, .72);
|
||||
text-align: center;
|
||||
color: #5f6b7a;
|
||||
font-size: 11px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.learning-filter-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin: 0 auto 5px;
|
||||
border-radius: 50%;
|
||||
color: #4f87c8;
|
||||
font-size: 18px;
|
||||
background: rgba(255, 255, 255, .78);
|
||||
}
|
||||
.learning-filter-item.no-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 58px;
|
||||
color: #0b75bd;
|
||||
font-weight: 700;
|
||||
}
|
||||
.learning-filter-item.active {
|
||||
color: #0b75bd;
|
||||
font-weight: 700;
|
||||
background: rgba(255, 255, 255, .86);
|
||||
}
|
||||
.learning-filter-item.active .learning-filter-icon {
|
||||
color: #ffffff;
|
||||
background: #0b75bd;
|
||||
}
|
||||
.learning-filter-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.learning-section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 18px 16px 10px;
|
||||
}
|
||||
.learning-section-title {
|
||||
color: #202733;
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.learning-section-total {
|
||||
color: #8b96a6;
|
||||
font-size: 13px;
|
||||
}
|
||||
.learning-course-list {
|
||||
padding: 0 12px 18px;
|
||||
}
|
||||
.learning-course-card {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
min-height: 104px;
|
||||
margin-bottom: 12px;
|
||||
padding: 14px;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.learning-course-cover {
|
||||
position: relative;
|
||||
flex: 0 0 142px;
|
||||
width: 142px;
|
||||
height: 82px;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
background: #e6edf6;
|
||||
}
|
||||
.learning-course-cover img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.learning-course-cover-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 8px;
|
||||
color: #ffffff;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(135deg, #3a7bd5, #8b5cf6);
|
||||
}
|
||||
.learning-course-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.learning-course-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
min-width: 0;
|
||||
}
|
||||
.learning-course-title-text {
|
||||
min-width: 0;
|
||||
color: #202733;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.learning-course-badge {
|
||||
flex-shrink: 0;
|
||||
max-width: 72px;
|
||||
padding: 1px 6px;
|
||||
border: 1px solid #ffd1a8;
|
||||
border-radius: 4px;
|
||||
color: #f28a35;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
background: #fff7ee;
|
||||
}
|
||||
.learning-course-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 9px;
|
||||
}
|
||||
.learning-course-intro {
|
||||
margin-top: 8px;
|
||||
color: #7a8798;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.learning-empty {
|
||||
padding: 22px 0 36px;
|
||||
}
|
||||
@media (max-width: 360px) {
|
||||
.learning-filter-card {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
padding: 8px;
|
||||
}
|
||||
.learning-filter-group {
|
||||
grid-template-columns: 94px minmax(0, 1fr);
|
||||
}
|
||||
.learning-filter-category {
|
||||
padding: 10px 8px;
|
||||
}
|
||||
.learning-filter-category-title {
|
||||
font-size: 13px;
|
||||
}
|
||||
.learning-filter-icon {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
font-size: 17px;
|
||||
}
|
||||
.learning-filter-item {
|
||||
font-size: 11px;
|
||||
}
|
||||
.learning-course-cover {
|
||||
flex-basis: 120px;
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" class="learning-course-page" v-cloak>
|
||||
<van-nav-bar title="学习教育" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
|
||||
|
||||
<div class="learning-banner-wrap">
|
||||
<van-swipe class="learning-banner" :autoplay="3500" indicator-color="#ffffff">
|
||||
<van-swipe-item v-for="course in bannerCourses" :key="course.id" @click="openCourse(course)">
|
||||
<div class="learning-banner-item">
|
||||
<img v-if="getCoverUrl(course.cover)" :src="getCoverUrl(course.cover)" :alt="course.courseName">
|
||||
<div v-else class="learning-banner-fallback">
|
||||
<div class="learning-banner-title">{{ course.courseName || '学习教育' }}</div>
|
||||
</div>
|
||||
<div v-if="getCoverUrl(course.cover)" class="learning-banner-title">{{ course.courseName || '学习教育' }}</div>
|
||||
</div>
|
||||
</van-swipe-item>
|
||||
<van-swipe-item v-if="!bannerCourses.length">
|
||||
<div class="learning-banner-item learning-banner-fallback">
|
||||
<div class="learning-banner-title">学习教育</div>
|
||||
</div>
|
||||
</van-swipe-item>
|
||||
</van-swipe>
|
||||
</div>
|
||||
|
||||
<div class="learning-filter-card">
|
||||
<div class="learning-filter-group" v-for="group in filterGroups" :key="group.key">
|
||||
<div class="learning-filter-category" :style="{background: group.color}">
|
||||
<div class="learning-filter-category-text">
|
||||
<div class="learning-filter-category-title">{{ group.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="learning-filter-options">
|
||||
<div
|
||||
v-for="item in group.items"
|
||||
:key="item.key"
|
||||
class="learning-filter-item"
|
||||
:class="{active: isFilterActive(item), 'no-icon': !item.icon}"
|
||||
@click="selectFilter(item)">
|
||||
<div v-if="item.icon" class="learning-filter-icon">
|
||||
<van-icon :name="item.icon"></van-icon>
|
||||
</div>
|
||||
<div class="learning-filter-name">{{ item.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="learning-list-scroll">
|
||||
<div class="learning-section-head">
|
||||
<div class="learning-section-title">{{ listTitle }}</div>
|
||||
<div class="learning-section-total">共 {{ pageForm.totalCount || 0 }} 门</div>
|
||||
</div>
|
||||
|
||||
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
||||
<van-list
|
||||
v-model="loading"
|
||||
:finished="finished"
|
||||
:immediate-check="false"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad">
|
||||
<div class="learning-course-list">
|
||||
<div
|
||||
class="learning-course-card"
|
||||
v-for="course in courseList"
|
||||
:key="course.id"
|
||||
@click="openCourse(course)">
|
||||
<div class="learning-course-cover">
|
||||
<img v-if="getCoverUrl(course.cover)" :src="getCoverUrl(course.cover)" :alt="course.courseName">
|
||||
<div v-else class="learning-course-cover-empty">{{ course.courseTypeName || '课程' }}</div>
|
||||
</div>
|
||||
<div class="learning-course-info">
|
||||
<div class="learning-course-title">
|
||||
<div class="learning-course-title-text">{{ course.courseName || '未命名课程' }}</div>
|
||||
<div v-if="firstRecommendName(course.recommendFlags)" class="learning-course-badge">{{ firstRecommendName(course.recommendFlags) }}</div>
|
||||
</div>
|
||||
<div class="learning-course-meta">
|
||||
<van-tag v-if="course.courseTypeName" plain type="primary">{{ course.courseTypeName }}</van-tag>
|
||||
<van-tag v-if="course.lecturerName" plain type="success">{{ course.lecturerName }}</van-tag>
|
||||
</div>
|
||||
<div class="learning-course-intro">{{ course.courseIntro || '暂无课程介绍' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-list>
|
||||
</van-pull-refresh>
|
||||
|
||||
<van-empty v-if="!loading && !courseList.length" class="learning-empty" description="暂无相关课程"></van-empty>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
store,
|
||||
data() {
|
||||
return {
|
||||
apiBase: "/platform/learning/course/display",
|
||||
query: {
|
||||
keyword: "",
|
||||
courseTypeId: "",
|
||||
recommendFlag: ""
|
||||
},
|
||||
pageForm: {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
totalCount: 0
|
||||
},
|
||||
courseList: [],
|
||||
bannerCourses: [],
|
||||
courseTypeOptions: [],
|
||||
recommendOptions: [],
|
||||
loading: false,
|
||||
finished: false,
|
||||
refreshing: false,
|
||||
requestSeq: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filterGroups() {
|
||||
const recommendItems = this.recommendOptions.map((item, index) => ({
|
||||
key: "recommend_" + item.code,
|
||||
type: "recommend",
|
||||
value: item.code,
|
||||
name: item.name,
|
||||
icon: this.recommendIcon(index)
|
||||
}))
|
||||
const typeItems = this.courseTypeOptions.map((item, index) => ({
|
||||
key: "type_" + item.id,
|
||||
type: "type",
|
||||
value: item.id,
|
||||
name: item.typeName,
|
||||
icon: this.typeIcon(index)
|
||||
}))
|
||||
return [
|
||||
{
|
||||
key: "recommend",
|
||||
name: "推荐标识",
|
||||
color: "linear-gradient(135deg, #ff8a3d, #ffbf5e)",
|
||||
items: recommendItems
|
||||
},
|
||||
{
|
||||
key: "type",
|
||||
name: "课程类型",
|
||||
color: "linear-gradient(135deg, #2878d7, #64a8f4)",
|
||||
items: typeItems
|
||||
}
|
||||
]
|
||||
},
|
||||
listTitle() {
|
||||
const recommend = this.recommendOptions.find(item => item.code === this.query.recommendFlag)
|
||||
const type = this.courseTypeOptions.find(item => item.id === this.query.courseTypeId)
|
||||
if (recommend && type) {
|
||||
return recommend.name + " · " + type.typeName
|
||||
}
|
||||
if (recommend) {
|
||||
return recommend.name
|
||||
}
|
||||
if (type) {
|
||||
return type.typeName
|
||||
}
|
||||
return "我的课堂"
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
historyBack,
|
||||
async loadOptions() {
|
||||
const [typeResp, recommendResp] = await Promise.all([
|
||||
this.$axios.post(this.apiBase + "/courseTypes"),
|
||||
this.$axios.post(this.apiBase + "/recommendOptions")
|
||||
])
|
||||
if (typeResp.code === 0) {
|
||||
this.courseTypeOptions = typeResp.data || []
|
||||
}
|
||||
if (recommendResp.code === 0) {
|
||||
this.recommendOptions = recommendResp.data || []
|
||||
}
|
||||
},
|
||||
onRefresh() {
|
||||
this.finished = false
|
||||
this.pageForm.pageNumber = 1
|
||||
this.courseList = []
|
||||
this.onLoad()
|
||||
},
|
||||
onLoad() {
|
||||
this.fetchCourses()
|
||||
},
|
||||
fetchCourses() {
|
||||
this.loading = true
|
||||
const requestSeq = ++this.requestSeq
|
||||
this.$axios.post(this.apiBase + "/pageData", {
|
||||
keyword: this.query.keyword,
|
||||
courseTypeId: this.query.courseTypeId,
|
||||
recommendFlag: this.query.recommendFlag,
|
||||
pageNumber: this.pageForm.pageNumber,
|
||||
pageSize: this.pageForm.pageSize
|
||||
}).then((res) => {
|
||||
if (requestSeq !== this.requestSeq) return
|
||||
if (res.code === 0) {
|
||||
const data = res.data || {}
|
||||
const list = data.list || []
|
||||
if (this.refreshing || this.pageForm.pageNumber === 1) {
|
||||
this.courseList = []
|
||||
}
|
||||
this.courseList = this.courseList.concat(list)
|
||||
this.pageForm.totalCount = data.totalCount || 0
|
||||
this.pageForm.pageNumber += 1
|
||||
this.finished = this.courseList.length >= this.pageForm.totalCount
|
||||
this.syncBannerCourses()
|
||||
} else {
|
||||
this.finished = true
|
||||
this.$toast(res.msg || "查询失败")
|
||||
}
|
||||
}).finally(() => {
|
||||
if (requestSeq === this.requestSeq) {
|
||||
this.loading = false
|
||||
this.refreshing = false
|
||||
}
|
||||
})
|
||||
},
|
||||
doSearch() {
|
||||
this.pageForm.pageNumber = 1
|
||||
this.pageForm.totalCount = 0
|
||||
this.courseList = []
|
||||
this.finished = false
|
||||
this.loading = true
|
||||
this.onLoad()
|
||||
},
|
||||
selectFilter(item) {
|
||||
if (!item) return
|
||||
if (item.type === "recommend") {
|
||||
this.query.recommendFlag = item.value || ""
|
||||
} else if (item.type === "type") {
|
||||
this.query.courseTypeId = item.value || ""
|
||||
}
|
||||
this.doSearch()
|
||||
},
|
||||
isFilterActive(item) {
|
||||
if (!item) return false
|
||||
if (item.type === "recommend") {
|
||||
return (this.query.recommendFlag || "") === (item.value || "")
|
||||
}
|
||||
if (item.type === "type") {
|
||||
return (this.query.courseTypeId || "") === (item.value || "")
|
||||
}
|
||||
return false
|
||||
},
|
||||
syncBannerCourses() {
|
||||
const withCover = this.courseList.filter(item => this.getCoverUrl(item.cover)).slice(0, 5)
|
||||
this.bannerCourses = (withCover.length ? withCover : this.courseList.slice(0, 5))
|
||||
},
|
||||
getCoverUrl(cover) {
|
||||
if (!cover) return ""
|
||||
if (Array.isArray(cover)) {
|
||||
return cover.length ? (cover[0].url || (cover[0].response && cover[0].response.data) || cover[0].data || "") : ""
|
||||
}
|
||||
if (typeof cover === "string") {
|
||||
const text = cover.trim()
|
||||
if (!text) return ""
|
||||
if (text.startsWith("[")) {
|
||||
try {
|
||||
const files = JSON.parse(text)
|
||||
return files.length ? (files[0].url || (files[0].response && files[0].response.data) || files[0].data || "") : ""
|
||||
} catch (e) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return text
|
||||
}
|
||||
return ""
|
||||
},
|
||||
openCourse(course) {
|
||||
if (!course || !course.id) return
|
||||
pjaxReplace("/platform/learning/course/h5/study?id=" + course.id)
|
||||
},
|
||||
splitFlags(flags) {
|
||||
return flags ? flags.split(",").filter(Boolean) : []
|
||||
},
|
||||
getRecommendName(code) {
|
||||
const item = this.recommendOptions.find(v => v.code === code)
|
||||
return item ? item.name : code
|
||||
},
|
||||
firstRecommendName(flags) {
|
||||
const codes = this.splitFlags(flags)
|
||||
return codes.length ? this.getRecommendName(codes[0]) : ""
|
||||
},
|
||||
recommendIcon(index) {
|
||||
const icons = ["fire-o", "star-o", "award-o", "gem-o", "flag-o", "good-job-o"]
|
||||
return icons[index % icons.length]
|
||||
},
|
||||
typeIcon(index) {
|
||||
const icons = ["video-o", "records-o", "description-o", "bookmark-o", "cluster-o", "desktop-o"]
|
||||
return icons[index % icons.length]
|
||||
},
|
||||
palette(index) {
|
||||
const colors = ["#28a6df", "#f2b84b", "#f46d5f", "#7b8fda", "#59bbb2", "#2d7db8", "#ee755b", "#f3c256", "#ef9461", "#8ba4dc"]
|
||||
return colors[index % colors.length]
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadOptions()
|
||||
this.onLoad()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,711 @@
|
||||
<!--#
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<style>
|
||||
.learning-study-page {
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
.study-hero {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
height: 195px;
|
||||
background: linear-gradient(135deg, #7554e8, #48b7f0);
|
||||
overflow: hidden;
|
||||
}
|
||||
.study-hero img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.study-hero-empty {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 28px;
|
||||
color: #ffffff;
|
||||
font-size: 26px;
|
||||
font-weight: 800;
|
||||
line-height: 1.35;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.study-back {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 8px;
|
||||
z-index: 2;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 50%;
|
||||
color: #ffffff;
|
||||
font-size: 24px;
|
||||
background: rgba(15, 23, 42, .22);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.study-title-block {
|
||||
flex-shrink: 0;
|
||||
padding: 16px 18px 10px;
|
||||
background: #ffffff;
|
||||
}
|
||||
.study-title {
|
||||
color: #202733;
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.study-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
margin-top: 14px;
|
||||
color: #8a93a3;
|
||||
font-size: 14px;
|
||||
}
|
||||
.study-meta-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.study-tabs {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #ffffff;
|
||||
}
|
||||
.study-tabs .van-tabs__content {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
.study-tabs .van-tabs__wrap {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.study-tabs .van-tabs__track {
|
||||
height: 100%;
|
||||
}
|
||||
.study-tabs .van-tab__pane {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.study-intro-scroll,
|
||||
.study-catalog-scroll {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
.study-summary {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 18px;
|
||||
color: #8b94a3;
|
||||
font-size: 16px;
|
||||
}
|
||||
.study-summary strong {
|
||||
color: #202733;
|
||||
font-size: 20px;
|
||||
}
|
||||
.study-card {
|
||||
margin: 18px;
|
||||
padding: 14px 0;
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8px 20px rgba(15, 23, 42, .04);
|
||||
overflow: hidden;
|
||||
}
|
||||
.study-chapter {
|
||||
padding: 0 18px;
|
||||
}
|
||||
.study-chapter-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 0 16px;
|
||||
color: #202733;
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.study-chapter-dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid #ff9b2f;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.study-resource {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 14px 0 12px 18px;
|
||||
border-top: 1px solid #f0f2f5;
|
||||
}
|
||||
.study-resource-icon {
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
margin-top: 2px;
|
||||
border-radius: 3px;
|
||||
color: #ffffff;
|
||||
font-size: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.study-resource-icon.video {
|
||||
background: #ffbd2e;
|
||||
}
|
||||
.study-resource-icon.audio {
|
||||
background: #33b3a6;
|
||||
}
|
||||
.study-resource-icon.image {
|
||||
background: #5a94f2;
|
||||
}
|
||||
.study-resource-icon.doc {
|
||||
background: #ff6868;
|
||||
}
|
||||
.study-resource-main {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.study-resource-title {
|
||||
color: #5c6470;
|
||||
font-size: 15px;
|
||||
line-height: 1.45;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.study-resource-status {
|
||||
margin-top: 8px;
|
||||
color: #a2a9b5;
|
||||
font-size: 15px;
|
||||
}
|
||||
.study-intro {
|
||||
padding: 18px;
|
||||
color: #4e5969;
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
background: #ffffff;
|
||||
min-height: 180px;
|
||||
}
|
||||
.study-player-popup {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 0;
|
||||
background: #000000;
|
||||
}
|
||||
.study-player-head {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 14px;
|
||||
color: #ffffff;
|
||||
background: #101828;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, .08);
|
||||
}
|
||||
.study-player-title {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.study-player-body {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
background: #000000;
|
||||
}
|
||||
.study-media {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: none;
|
||||
background: #000000;
|
||||
border-radius: 0;
|
||||
object-fit: contain;
|
||||
}
|
||||
.study-audio-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
min-height: 220px;
|
||||
border-radius: 0;
|
||||
background: linear-gradient(135deg, #10233f, #0f766e);
|
||||
}
|
||||
.study-audio-wrap audio {
|
||||
width: 88%;
|
||||
}
|
||||
.study-image-preview {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
border-radius: 0;
|
||||
background: #ffffff;
|
||||
}
|
||||
.study-doc-frame {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: #ffffff;
|
||||
}
|
||||
.study-file-fallback {
|
||||
padding: 46px 18px;
|
||||
color: #7a8798;
|
||||
font-size: 15px;
|
||||
text-align: center;
|
||||
background: #ffffff;
|
||||
border-radius: 0;
|
||||
}
|
||||
.study-player-exit {
|
||||
flex-shrink: 0;
|
||||
margin-left: 12px;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
}
|
||||
.study-player-actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-left: 12px;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" class="learning-study-page" v-cloak>
|
||||
<div class="study-hero">
|
||||
<div class="study-back" @click="goBack"><van-icon name="arrow-left"></van-icon></div>
|
||||
<img v-if="coverUrl" :src="coverUrl" :alt="course.courseName">
|
||||
<div v-else class="study-hero-empty">{{ course.courseName || '课程学习' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="study-title-block">
|
||||
<div class="study-title">{{ course.courseName || '课程学习' }}</div>
|
||||
<div class="study-meta">
|
||||
<span class="study-meta-item"><van-icon name="eye-o"></van-icon>{{ studyCountText }}</span>
|
||||
<span class="study-meta-item"><van-icon name="bookmark-o"></van-icon>收藏</span>
|
||||
<span class="study-meta-item"><van-icon name="share-o"></van-icon>分享</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<van-tabs v-model="activeTab" class="study-tabs" color="#2d9cff" line-width="34px" title-active-color="#202733">
|
||||
<van-tab title="课程介绍" name="intro">
|
||||
<div class="study-intro-scroll">
|
||||
<div class="study-intro">{{ course.courseIntro || '暂无课程介绍' }}</div>
|
||||
</div>
|
||||
</van-tab>
|
||||
<van-tab title="课程目录" name="catalog">
|
||||
<div class="study-catalog-scroll">
|
||||
<div class="study-summary">
|
||||
<div>共 <strong>{{ chapterCount }}</strong> 个章节,<strong>{{ totalMinutes }}</strong> 分钟</div>
|
||||
<div>总学习进度 <strong>{{ totalProgress }}%</strong></div>
|
||||
</div>
|
||||
|
||||
<div v-for="chapter in catalogList" :key="chapter.id" class="study-card">
|
||||
<div class="study-chapter">
|
||||
<div class="study-chapter-head" @click="selectChapter(chapter)">
|
||||
<span class="study-chapter-dot"></span>
|
||||
<span>{{ chapter.title }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="resource in chapter.resources"
|
||||
:key="resource.id"
|
||||
class="study-resource"
|
||||
@click="openResource(resource)">
|
||||
<div class="study-resource-icon" :class="resourceIconClass(resource)">
|
||||
<van-icon :name="resourceIcon(resource)"></van-icon>
|
||||
</div>
|
||||
<div class="study-resource-main">
|
||||
<div class="study-resource-title">{{ resource.title }}</div>
|
||||
<div class="study-resource-status">{{ statusText(resource) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<van-empty v-if="!catalogList.length" description="暂无课程目录"></van-empty>
|
||||
</div>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
|
||||
<van-popup v-model="playerVisible" position="bottom" class="study-player-popup" :overlay="false">
|
||||
<div class="study-player-head">
|
||||
<div class="study-player-title">{{ selectedResource.title || '学习资料' }}</div>
|
||||
<div class="study-player-actions">
|
||||
<span v-if="selectedResource.resourceType === 'video'" @click="enterVideoFullscreen">横屏</span>
|
||||
<span @click="closePlayer">退出</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="study-player-body">
|
||||
<video
|
||||
v-if="selectedResource.resourceType === 'video'"
|
||||
ref="mediaPlayer"
|
||||
class="study-media"
|
||||
:src="fileUrl"
|
||||
controls
|
||||
playsinline
|
||||
webkit-playsinline
|
||||
x5-video-player-type="h5"
|
||||
x5-video-orientation="landscape|portrait"
|
||||
x-webkit-airplay="allow"
|
||||
@play="startStudy"
|
||||
@timeupdate="saveProgress"
|
||||
@ended="finishStudy(false)">
|
||||
</video>
|
||||
<div v-else-if="selectedResource.resourceType === 'audio'" class="study-audio-wrap">
|
||||
<audio
|
||||
ref="mediaPlayer"
|
||||
:src="fileUrl"
|
||||
controls
|
||||
@play="startStudy"
|
||||
@timeupdate="saveProgress"
|
||||
@ended="finishStudy(false)">
|
||||
</audio>
|
||||
</div>
|
||||
<img v-else-if="selectedResource.resourceType === 'image'" class="study-image-preview" :src="fileUrl" :alt="selectedResource.title">
|
||||
<iframe v-else-if="canInlinePreview(selectedResource)" class="study-doc-frame" :src="inlinePreviewUrl"></iframe>
|
||||
<div v-else class="study-file-fallback">
|
||||
<p>该资料暂不支持内嵌预览</p>
|
||||
<van-button type="info" size="small" @click="openFile">打开资料</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data() {
|
||||
return {
|
||||
courseId: "",
|
||||
apiBase: "/platform/learning/course/display",
|
||||
recordApi: "/platform/learning/study/record",
|
||||
activeTab: "catalog",
|
||||
course: {},
|
||||
treeData: [],
|
||||
selectedResource: {},
|
||||
playerVisible: false,
|
||||
currentSegmentId: "",
|
||||
studying: false,
|
||||
pendingSeconds: 0,
|
||||
heartbeatTimer: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
coverUrl() {
|
||||
return this.getFileUrl(this.course.cover)
|
||||
},
|
||||
fileUrl() {
|
||||
return this.getFileUrl(this.selectedResource.fileData)
|
||||
},
|
||||
inlinePreviewUrl() {
|
||||
const id = this.getFileId(this.selectedResource.fileData)
|
||||
return id ? this.apiBase + "/pdfPreview?id=" + encodeURIComponent(id) : this.fileUrl
|
||||
},
|
||||
catalogList() {
|
||||
const result = []
|
||||
;(this.treeData || []).forEach(node => this.appendCatalogNode(node, result))
|
||||
return result
|
||||
},
|
||||
chapterCount() {
|
||||
return this.catalogList.length
|
||||
},
|
||||
totalMinutes() {
|
||||
const seconds = this.catalogList.reduce((sum, chapter) => {
|
||||
return sum + chapter.resources.reduce((value, item) => value + Number(item.durationSeconds || 0), 0)
|
||||
}, 0)
|
||||
return Math.max(0, Math.ceil(seconds / 60))
|
||||
},
|
||||
totalProgress() {
|
||||
const resources = []
|
||||
this.catalogList.forEach(chapter => resources.push.apply(resources, chapter.resources))
|
||||
if (!resources.length) return 0
|
||||
const total = resources.reduce((sum, item) => sum + Number(item.progressPercent || 0), 0)
|
||||
return Math.floor(total / resources.length)
|
||||
},
|
||||
studyCountText() {
|
||||
return this.course.lecturerName ? this.course.lecturerName : "开始学习"
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getQuery(name) {
|
||||
return new URLSearchParams(window.location.search).get(name) || ""
|
||||
},
|
||||
async loadCourse() {
|
||||
const resp = await this.$axios.post(this.apiBase + "/courseInfo", { id: this.courseId })
|
||||
if (resp.code === 0) {
|
||||
this.course = resp.data || {}
|
||||
} else {
|
||||
this.$toast(resp.msg || "课程不存在")
|
||||
}
|
||||
},
|
||||
async loadTree() {
|
||||
const resp = await this.$axios.post(this.apiBase + "/studyTree", { courseId: this.courseId })
|
||||
if (resp.code === 0) {
|
||||
this.treeData = resp.data || []
|
||||
}
|
||||
},
|
||||
appendCatalogNode(node, result) {
|
||||
if (!node) return
|
||||
if (node.type === "resource") return
|
||||
const resources = this.collectResources(node.children || [])
|
||||
if (node.nodeType === "chapter" || resources.length) {
|
||||
result.push({
|
||||
id: node.id,
|
||||
title: node.title,
|
||||
resources: resources
|
||||
})
|
||||
}
|
||||
;(node.children || []).forEach(child => {
|
||||
if (child.type !== "resource" && child.nodeType === "chapter") {
|
||||
this.appendCatalogNode(child, result)
|
||||
}
|
||||
})
|
||||
},
|
||||
collectResources(nodes) {
|
||||
const resources = []
|
||||
;(nodes || []).forEach(node => {
|
||||
if (node.type === "resource") {
|
||||
resources.push(node)
|
||||
} else {
|
||||
resources.push.apply(resources, this.collectResources(node.children || []))
|
||||
}
|
||||
})
|
||||
return resources
|
||||
},
|
||||
selectChapter(chapter) {
|
||||
if (chapter && chapter.resources && chapter.resources.length) {
|
||||
this.openResource(chapter.resources[0])
|
||||
}
|
||||
},
|
||||
openResource(resource) {
|
||||
if (!resource || !resource.id) return
|
||||
this.pauseMedia()
|
||||
this.finishStudy(false)
|
||||
this.selectedResource = resource
|
||||
this.playerVisible = true
|
||||
if (!["video", "audio"].includes(resource.resourceType)) {
|
||||
this.$nextTick(() => this.startStudy())
|
||||
} else {
|
||||
this.$nextTick(() => this.resumeMediaPosition())
|
||||
}
|
||||
},
|
||||
resumeMediaPosition() {
|
||||
const player = this.$refs.mediaPlayer
|
||||
const position = Number(this.selectedResource.lastPositionSeconds || 0)
|
||||
if (player && position > 0) {
|
||||
player.currentTime = position
|
||||
}
|
||||
},
|
||||
async startStudy() {
|
||||
if (!this.selectedResource.id || this.studying) return
|
||||
const resp = await this.$axios.post(this.recordApi + "/start", {
|
||||
courseId: this.courseId,
|
||||
resourceId: this.selectedResource.id,
|
||||
positionSeconds: this.getMediaPosition()
|
||||
})
|
||||
if (resp.code !== 0) {
|
||||
this.$toast(resp.msg || "开始学习失败")
|
||||
return
|
||||
}
|
||||
this.currentSegmentId = resp.data.segmentId
|
||||
this.studying = true
|
||||
this.pendingSeconds = 0
|
||||
this.applyRecordState(resp.data)
|
||||
this.startHeartbeatTimer()
|
||||
},
|
||||
async heartbeat() {
|
||||
if (!this.currentSegmentId || this.pendingSeconds <= 0) return
|
||||
const seconds = this.pendingSeconds
|
||||
this.pendingSeconds = 0
|
||||
const resp = await this.$axios.post(this.recordApi + "/heartbeat", {
|
||||
segmentId: this.currentSegmentId,
|
||||
activeSeconds: seconds,
|
||||
positionSeconds: this.getMediaPosition()
|
||||
})
|
||||
if (resp.code === 0) {
|
||||
this.applyRecordState(resp.data)
|
||||
}
|
||||
},
|
||||
async finishStudy(force) {
|
||||
this.pauseMedia()
|
||||
if (!this.currentSegmentId) return
|
||||
const segmentId = this.currentSegmentId
|
||||
const seconds = this.pendingSeconds
|
||||
this.pendingSeconds = 0
|
||||
this.stopHeartbeatTimer()
|
||||
this.studying = false
|
||||
this.currentSegmentId = ""
|
||||
await this.$axios.post(this.recordApi + "/finish", {
|
||||
segmentId: segmentId,
|
||||
activeSeconds: seconds,
|
||||
positionSeconds: this.getMediaPosition()
|
||||
})
|
||||
if (force) return
|
||||
this.loadTree()
|
||||
},
|
||||
startHeartbeatTimer() {
|
||||
this.stopHeartbeatTimer()
|
||||
this.heartbeatTimer = setInterval(() => {
|
||||
if (!this.studying || document.hidden) return
|
||||
if (["video", "audio"].includes(this.selectedResource.resourceType)) {
|
||||
const player = this.$refs.mediaPlayer
|
||||
if (!player || player.paused || player.ended) return
|
||||
}
|
||||
this.pendingSeconds += 1
|
||||
if (this.pendingSeconds >= 15) {
|
||||
this.heartbeat()
|
||||
}
|
||||
}, 1000)
|
||||
},
|
||||
stopHeartbeatTimer() {
|
||||
if (this.heartbeatTimer) {
|
||||
clearInterval(this.heartbeatTimer)
|
||||
this.heartbeatTimer = null
|
||||
}
|
||||
},
|
||||
saveProgress() {
|
||||
const position = this.getMediaPosition()
|
||||
if (!position || !this.selectedResource.id) return
|
||||
this.$set(this.selectedResource, "lastPositionSeconds", position)
|
||||
},
|
||||
applyRecordState(data) {
|
||||
if (!this.selectedResource.id || !data) return
|
||||
this.$set(this.selectedResource, "progressPercent", Number(data.progressPercent || 0))
|
||||
this.$set(this.selectedResource, "studySeconds", Number(data.studySeconds || 0))
|
||||
this.$set(this.selectedResource, "completeStatus", data.completeStatus || "studying")
|
||||
if (data.lastPositionSeconds !== undefined) {
|
||||
this.$set(this.selectedResource, "lastPositionSeconds", Number(data.lastPositionSeconds || 0))
|
||||
}
|
||||
},
|
||||
getMediaPosition() {
|
||||
if (!["video", "audio"].includes(this.selectedResource.resourceType)) return 0
|
||||
const player = this.$refs.mediaPlayer
|
||||
return player ? Math.floor(player.currentTime || 0) : Number(this.selectedResource.lastPositionSeconds || 0)
|
||||
},
|
||||
statusText(resource) {
|
||||
if (!resource || resource.completeStatus === "not_started") return "未学习"
|
||||
if (resource.completeStatus === "completed") return "已完成"
|
||||
return "学习中 " + Number(resource.progressPercent || 0) + "%"
|
||||
},
|
||||
resourceIcon(resource) {
|
||||
if (resource.resourceType === "video") return "video"
|
||||
if (resource.resourceType === "audio") return "music-o"
|
||||
if (resource.resourceType === "image") return "photo-o"
|
||||
return "description"
|
||||
},
|
||||
resourceIconClass(resource) {
|
||||
if (resource.resourceType === "video") return "video"
|
||||
if (resource.resourceType === "audio") return "audio"
|
||||
if (resource.resourceType === "image") return "image"
|
||||
return "doc"
|
||||
},
|
||||
getFileUrl(value) {
|
||||
if (!value) return ""
|
||||
if (Array.isArray(value)) {
|
||||
return value.length ? (value[0].url || (value[0].response && value[0].response.data) || value[0].data || "") : ""
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
const text = value.trim()
|
||||
if (!text) return ""
|
||||
if (text.startsWith("[")) {
|
||||
try {
|
||||
const files = JSON.parse(text)
|
||||
return files.length ? (files[0].url || (files[0].response && files[0].response.data) || files[0].data || "") : ""
|
||||
} catch (e) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return text
|
||||
}
|
||||
return ""
|
||||
},
|
||||
getFileId(value) {
|
||||
const url = this.getFileUrl(value)
|
||||
if (!url) return ""
|
||||
const matched = url.match(/[?&]id=([^&]+)/)
|
||||
if (matched) return decodeURIComponent(matched[1])
|
||||
if (!url.includes("/") && !url.includes(".")) return url
|
||||
return ""
|
||||
},
|
||||
canInlinePreview(resource) {
|
||||
const type = resource.resourceType || ""
|
||||
const ext = (resource.fileExt || "").toLowerCase()
|
||||
return ["pdf", "ppt", "word"].includes(type) || ["pdf", "ppt", "pptx", "doc", "docx"].includes(ext)
|
||||
},
|
||||
openFile() {
|
||||
if (this.fileUrl) {
|
||||
window.open(this.fileUrl)
|
||||
}
|
||||
},
|
||||
pauseMedia() {
|
||||
const player = this.$refs.mediaPlayer
|
||||
if (player && player.pause) {
|
||||
player.pause()
|
||||
}
|
||||
},
|
||||
enterVideoFullscreen() {
|
||||
const player = this.$refs.mediaPlayer
|
||||
if (!player) return
|
||||
const requestFullscreen = player.requestFullscreen || player.webkitRequestFullscreen || player.mozRequestFullScreen || player.msRequestFullscreen
|
||||
if (requestFullscreen) {
|
||||
requestFullscreen.call(player)
|
||||
} else if (player.webkitEnterFullscreen) {
|
||||
player.webkitEnterFullscreen()
|
||||
}
|
||||
if (screen.orientation && screen.orientation.lock) {
|
||||
screen.orientation.lock("landscape").catch(() => {})
|
||||
}
|
||||
},
|
||||
closePlayer() {
|
||||
this.pauseMedia()
|
||||
this.finishStudy(false)
|
||||
this.playerVisible = false
|
||||
},
|
||||
goBack() {
|
||||
this.finishStudy(true)
|
||||
pjaxReplace("/platform/learning/course/h5")
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.courseId = this.getQuery("id")
|
||||
if (!this.courseId) {
|
||||
this.$toast("请选择课程")
|
||||
return
|
||||
}
|
||||
await this.loadCourse()
|
||||
await this.loadTree()
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.finishStudy(true)
|
||||
this.stopHeartbeatTimer()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -0,0 +1,371 @@
|
||||
<!--#
|
||||
layout("/layouts/platform_h5.html"){
|
||||
#-->
|
||||
|
||||
<style>
|
||||
.my-study-page {
|
||||
min-height: 100vh;
|
||||
background: #f3f7fb;
|
||||
color: #202733;
|
||||
}
|
||||
.my-study-hero {
|
||||
position: relative;
|
||||
padding: 86px 20px 72px;
|
||||
color: #ffffff;
|
||||
background: linear-gradient(135deg, #0ca7dd 0%, #0699d6 58%, #0f8fd7 100%);
|
||||
overflow: hidden;
|
||||
}
|
||||
.my-study-hero::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: -74px;
|
||||
bottom: -108px;
|
||||
width: 260px;
|
||||
height: 260px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, .08);
|
||||
}
|
||||
.my-study-status {
|
||||
display: block;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.my-study-time {
|
||||
font-size: 15px;
|
||||
color: rgba(255, 255, 255, .82);
|
||||
}
|
||||
.my-study-time strong {
|
||||
margin: 0 3px;
|
||||
color: #ffffff;
|
||||
font-size: 26px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.my-study-stats {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0;
|
||||
margin-top: 26px;
|
||||
text-align: center;
|
||||
}
|
||||
.my-study-stat-value {
|
||||
font-size: 23px;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.my-study-stat-label {
|
||||
margin-top: 7px;
|
||||
color: rgba(255, 255, 255, .78);
|
||||
font-size: 13px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.my-study-main {
|
||||
position: relative;
|
||||
margin-top: -36px;
|
||||
padding: 0 20px 28px;
|
||||
z-index: 2;
|
||||
}
|
||||
.my-study-assistant {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
min-height: 70px;
|
||||
padding: 13px 14px;
|
||||
border-radius: 8px;
|
||||
background: #eef6ff;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 8px 18px rgba(17, 101, 166, .08);
|
||||
}
|
||||
.my-study-ai {
|
||||
flex-shrink: 0;
|
||||
font-size: 27px;
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
color: #1a75ff;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.my-study-ai span {
|
||||
color: #6b46ff;
|
||||
}
|
||||
.my-study-assistant-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: #30445c;
|
||||
font-size: 15px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.my-study-assistant-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 7px 16px;
|
||||
border-radius: 18px;
|
||||
color: #5d8fd8;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
background: #ffffff;
|
||||
}
|
||||
.my-study-section {
|
||||
margin-top: 20px;
|
||||
padding: 16px 0;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
.my-study-section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 14px 14px;
|
||||
}
|
||||
.my-study-section-title {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.my-study-all {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: #9aa3b1;
|
||||
font-size: 13px;
|
||||
}
|
||||
.my-study-course-scroll {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding: 0 14px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.my-study-course-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.my-study-course-card {
|
||||
flex: 0 0 154px;
|
||||
width: 154px;
|
||||
overflow: hidden;
|
||||
border-radius: 5px;
|
||||
background: #f7fbff;
|
||||
}
|
||||
.my-study-cover {
|
||||
width: 154px;
|
||||
height: 88px;
|
||||
background: linear-gradient(135deg, #8657f2, #56c4ec);
|
||||
}
|
||||
.my-study-cover img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.my-study-cover-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.my-study-course-name {
|
||||
height: 48px;
|
||||
padding: 8px 10px 0;
|
||||
color: #293241;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.my-study-course-foot {
|
||||
padding: 8px 10px 10px;
|
||||
color: #b5bdc8;
|
||||
font-size: 13px;
|
||||
text-align: right;
|
||||
}
|
||||
.my-study-list {
|
||||
margin-top: 18px;
|
||||
}
|
||||
.my-study-list-card {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
.my-study-list-cover {
|
||||
flex: 0 0 108px;
|
||||
width: 108px;
|
||||
height: 66px;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #8657f2, #56c4ec);
|
||||
}
|
||||
.my-study-list-cover img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.my-study-list-info {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.my-study-list-title {
|
||||
color: #202733;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.my-study-list-meta {
|
||||
margin-top: 10px;
|
||||
color: #8f99a8;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="app" class="my-study-page" v-cloak>
|
||||
<div class="my-study-hero">
|
||||
<div class="my-study-status">
|
||||
<div class="my-study-time">
|
||||
累计学习时长 <strong>{{ summary.studyHour || 0 }}</strong> 小时 <strong>{{ summary.studyMinute || 0 }}</strong> 分钟
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-study-stats">
|
||||
<div v-for="item in statItems" :key="item.label">
|
||||
<div class="my-study-stat-value">{{ item.value }}</div>
|
||||
<div class="my-study-stat-label">{{ item.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-study-main">
|
||||
<div class="my-study-assistant">
|
||||
<div class="my-study-ai"><span>AI</span>学助手</div>
|
||||
<div class="my-study-assistant-text">来问问AI学习助手,获取专属学习计划</div>
|
||||
<div class="my-study-assistant-btn">查看</div>
|
||||
</div>
|
||||
|
||||
<div class="my-study-section">
|
||||
<div class="my-study-section-head">
|
||||
<div class="my-study-section-title">我学习的课程</div>
|
||||
<div class="my-study-all" @click="scrollToList">全部 <van-icon name="arrow"></van-icon></div>
|
||||
</div>
|
||||
<div class="my-study-course-scroll">
|
||||
<div
|
||||
v-for="course in courses"
|
||||
:key="course.courseId"
|
||||
class="my-study-course-card"
|
||||
@click="openCourse(course)">
|
||||
<div class="my-study-cover">
|
||||
<img v-if="getCoverUrl(course.cover)" :src="getCoverUrl(course.cover)" :alt="course.courseName">
|
||||
<div v-else class="my-study-cover-empty">{{ course.courseName || '课程' }}</div>
|
||||
</div>
|
||||
<div class="my-study-course-name">{{ course.courseName || '未命名课程' }}</div>
|
||||
<div class="my-study-course-foot">已学完{{ course.completedOutlineCount || 0 }}讲</div>
|
||||
</div>
|
||||
</div>
|
||||
<van-empty v-if="!loading && !courses.length" description="暂无学习课程"></van-empty>
|
||||
</div>
|
||||
|
||||
<div ref="courseList" class="my-study-list">
|
||||
<div
|
||||
v-for="course in courses"
|
||||
:key="'list_' + course.courseId"
|
||||
class="my-study-list-card"
|
||||
@click="openCourse(course)">
|
||||
<div class="my-study-list-cover">
|
||||
<img v-if="getCoverUrl(course.cover)" :src="getCoverUrl(course.cover)" :alt="course.courseName">
|
||||
<div v-else class="my-study-cover-empty">{{ course.courseName || '课程' }}</div>
|
||||
</div>
|
||||
<div class="my-study-list-info">
|
||||
<div class="my-study-list-title">{{ course.courseName || '未命名课程' }}</div>
|
||||
<div class="my-study-list-meta">学习进度 {{ course.progressPercent || 0 }}% · {{ course.studyTimeText || '0秒' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
new Vue({
|
||||
el: "#app",
|
||||
data() {
|
||||
return {
|
||||
apiBase: "/platform/learning/study/record",
|
||||
summary: {},
|
||||
courses: [],
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
statItems() {
|
||||
return [
|
||||
{ label: "已学完课程", value: this.summary.completedCourseCount || 0 },
|
||||
{ label: "学习专题", value: 0 }
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
try {
|
||||
const [summaryResp, courseResp] = await Promise.all([
|
||||
this.$axios.post(this.apiBase + "/h5Summary"),
|
||||
this.$axios.post(this.apiBase + "/h5Courses")
|
||||
])
|
||||
if (summaryResp.code === 0) {
|
||||
this.summary = summaryResp.data || {}
|
||||
}
|
||||
if (courseResp.code === 0) {
|
||||
this.courses = courseResp.data || []
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
getCoverUrl(cover) {
|
||||
if (!cover) return ""
|
||||
if (Array.isArray(cover)) {
|
||||
return cover.length ? (cover[0].url || (cover[0].response && cover[0].response.data) || cover[0].data || "") : ""
|
||||
}
|
||||
if (typeof cover === "string") {
|
||||
const text = cover.trim()
|
||||
if (!text) return ""
|
||||
if (text.startsWith("[")) {
|
||||
try {
|
||||
const files = JSON.parse(text)
|
||||
return files.length ? (files[0].url || (files[0].response && files[0].response.data) || files[0].data || "") : ""
|
||||
} catch (e) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return text
|
||||
}
|
||||
return ""
|
||||
},
|
||||
openCourse(course) {
|
||||
if (!course || !course.courseId) return
|
||||
pjaxReplace("/platform/learning/course/h5/study?id=" + course.courseId)
|
||||
},
|
||||
scrollToList() {
|
||||
this.$refs.courseList && this.$refs.courseList.scrollIntoView({ behavior: "smooth", block: "start" })
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
@@ -35,9 +35,12 @@ const apps = {
|
||||
|
||||
<!-- 右侧内容 -->
|
||||
<div class="tree-content">
|
||||
<!-- 应用列表 -->
|
||||
<div v-if="loading" class="module-loading">
|
||||
<van-loading size="24px">加载中...</van-loading>
|
||||
</div>
|
||||
|
||||
<div :class="['app-grid', { 'app-grid-single': applications.length === 1 }]"
|
||||
v-if="applications.length > 0">
|
||||
v-else-if="isFixedCategory && applications.length > 0">
|
||||
<div v-for="app in applications"
|
||||
:key="app.id"
|
||||
class="app-grid-item"
|
||||
@@ -46,17 +49,35 @@ const apps = {
|
||||
@touchstart="onTouchStart(app)"
|
||||
@touchend="onTouchEnd"
|
||||
@touchmove="onTouchMove">
|
||||
<!-- 图标 -->
|
||||
<div class="app-grid-icon">
|
||||
<van-icon v-if="app.picIcon" :name="app.picIcon" size="45"></van-icon>
|
||||
<van-icon v-else-if="app.icon" :name="app.icon" size="28"></van-icon>
|
||||
<van-icon v-else name="apps-o" size="28"></van-icon>
|
||||
</div>
|
||||
<!-- 名称 -->
|
||||
<div class="app-grid-name">{{ app.name }}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-else-if="!isFixedCategory && moduleSections.length > 0" class="module-sections">
|
||||
<div v-for="section in moduleSections" :key="section.id" class="module-section">
|
||||
<div class="module-title">{{ section.name }}</div>
|
||||
<div :class="['module-grid', { 'module-grid-single': section.menus.length === 1 }]">
|
||||
<div
|
||||
v-for="menu in section.menus"
|
||||
:key="menu.id"
|
||||
class="module-feature"
|
||||
@click="navigateToMenu(menu)"
|
||||
>
|
||||
<img v-if="menu.picIcon" :src="menu.picIcon" :alt="menu.name || ''" class="module-feature-img"/>
|
||||
<div v-else class="module-feature-icon">
|
||||
<van-icon :name="menu.icon || 'apps-o'" size="24"></van-icon>
|
||||
</div>
|
||||
<div class="module-feature-name">{{ menu.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<van-empty v-else description="暂无应用"></van-empty>
|
||||
|
||||
@@ -93,6 +114,7 @@ const apps = {
|
||||
return {
|
||||
// 应用列表
|
||||
applications: [],
|
||||
moduleSections: [],
|
||||
// 固定分类列表
|
||||
categories: [
|
||||
{id: "all", name: "全部应用", icon: "fa fa-th-large"},
|
||||
@@ -111,6 +133,7 @@ const apps = {
|
||||
showMenuPopup: false,
|
||||
currentApp: {},
|
||||
currentMenus: [],
|
||||
requestSeq: 0,
|
||||
|
||||
touchTimer: null,
|
||||
isTouchMoved: false,
|
||||
@@ -124,6 +147,9 @@ const apps = {
|
||||
// 当前选中的分类ID
|
||||
currentCategoryId() {
|
||||
return this.allCategories[this.activeTab]?.id || "all"
|
||||
},
|
||||
isFixedCategory() {
|
||||
return ["all", "favorites", "recommended"].includes(this.currentCategoryId)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -145,7 +171,9 @@ const apps = {
|
||||
|
||||
// 加载应用数据
|
||||
loadApps() {
|
||||
const requestSeq = ++this.requestSeq
|
||||
this.loading = true
|
||||
this.moduleSections = []
|
||||
|
||||
// 构建请求参数
|
||||
const params = {
|
||||
@@ -172,12 +200,14 @@ const apps = {
|
||||
// 如果是"推荐应用"分类
|
||||
if (this.currentCategoryId === "recommended") {
|
||||
this.$axios.post("/platform/home/listRecommendApp", { platform: "H5" }).then((result) => {
|
||||
if (requestSeq !== this.requestSeq) return
|
||||
if (result.code === 0) {
|
||||
this.applications = result.data || []
|
||||
this.finishLoadApps(requestSeq)
|
||||
} else {
|
||||
console.error("获取推荐应用失败:", result.msg)
|
||||
this.loading = false
|
||||
}
|
||||
this.loading = false
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -185,28 +215,105 @@ const apps = {
|
||||
// 发送请求获取普通应用列表
|
||||
this.$axios.post("/platform/v4/apps/list?" + queryString)
|
||||
.then((result) => {
|
||||
if (requestSeq !== this.requestSeq) return
|
||||
if (result.code === 0) {
|
||||
this.applications = result.data || []
|
||||
this.finishLoadApps(requestSeq)
|
||||
} else {
|
||||
console.error("获取应用列表失败:", result.msg)
|
||||
this.loading = false
|
||||
}
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
|
||||
// 加载收藏的应用
|
||||
loadFavorites() {
|
||||
const requestSeq = this.requestSeq
|
||||
this.$axios.post("/platform/v4/apps/favorite", {platform: 'H5'})
|
||||
.then((result) => {
|
||||
if (requestSeq !== this.requestSeq) return
|
||||
if (result.code === 0) {
|
||||
this.applications = result.data || []
|
||||
this.finishLoadApps(requestSeq)
|
||||
} else {
|
||||
console.error("获取收藏应用失败:", result.msg)
|
||||
this.loading = false
|
||||
}
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
|
||||
finishLoadApps(requestSeq) {
|
||||
if (requestSeq !== this.requestSeq) return
|
||||
if (this.isFixedCategory) {
|
||||
this.moduleSections = []
|
||||
this.loading = false
|
||||
return
|
||||
}
|
||||
this.loadModuleSections(this.applications, requestSeq)
|
||||
},
|
||||
|
||||
// 加载每个模块下的功能菜单
|
||||
loadModuleSections(apps, requestSeq) {
|
||||
const list = apps || []
|
||||
if (!list.length) {
|
||||
this.moduleSections = []
|
||||
this.loading = false
|
||||
return
|
||||
}
|
||||
Promise.all(list.map(app => {
|
||||
return this.$axios.post("/platform/sys/user/subAppMenus", {appId: app.id}).then((res) => {
|
||||
const menus = res.code === 0 ? (res.data || []) : []
|
||||
return {
|
||||
id: app.id,
|
||||
name: app.name,
|
||||
app: app,
|
||||
menus: this.resolveModuleMenus(app, menus)
|
||||
}
|
||||
}).catch(() => {
|
||||
return {
|
||||
id: app.id,
|
||||
name: app.name,
|
||||
app: app,
|
||||
menus: this.resolveModuleMenus(app, [])
|
||||
}
|
||||
})
|
||||
})).then(sections => {
|
||||
if (requestSeq !== this.requestSeq) return
|
||||
this.moduleSections = sections.filter(section => section.menus.length > 0)
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
|
||||
resolveModuleMenus(app, menus) {
|
||||
const items = this.flattenMenus(menus || [])
|
||||
if (items.length > 0) {
|
||||
return items
|
||||
}
|
||||
if (app && app.href) {
|
||||
return [{
|
||||
id: app.id,
|
||||
name: app.name,
|
||||
href: app.href,
|
||||
icon: app.icon,
|
||||
picIcon: app.picIcon,
|
||||
aliasName: app.aliasName
|
||||
}]
|
||||
}
|
||||
return []
|
||||
},
|
||||
|
||||
flattenMenus(menus) {
|
||||
const result = []
|
||||
;(menus || []).forEach(menu => {
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
result.push.apply(result, this.flattenMenus(menu.children))
|
||||
} else if (menu.href) {
|
||||
result.push(menu)
|
||||
}
|
||||
})
|
||||
return result
|
||||
},
|
||||
|
||||
// 切换应用收藏状态
|
||||
toggleFavorite(appId) {
|
||||
const app = this.applications.find((a) => a.id === appId)
|
||||
@@ -254,6 +361,7 @@ const apps = {
|
||||
// 搜索
|
||||
onSearch() {
|
||||
this.applications = []
|
||||
this.moduleSections = []
|
||||
this.loadApps()
|
||||
},
|
||||
|
||||
@@ -267,6 +375,7 @@ const apps = {
|
||||
onClickNav(index) {
|
||||
this.activeTab = index
|
||||
this.applications = []
|
||||
this.moduleSections = []
|
||||
this.loadApps()
|
||||
},
|
||||
|
||||
@@ -321,12 +430,15 @@ const apps = {
|
||||
.apps-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
height: calc(100vh - 50px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 自定义树形选择组件样式 */
|
||||
.custom-tree-select {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
height: calc(100vh - 150px);
|
||||
background: #fff;
|
||||
}
|
||||
@@ -403,10 +515,109 @@ const apps = {
|
||||
|
||||
.tree-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.module-loading {
|
||||
min-height: 180px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8a94a6;
|
||||
}
|
||||
|
||||
.module-sections {
|
||||
padding: 12px 12px 18px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.module-section {
|
||||
padding: 0 0 18px;
|
||||
}
|
||||
|
||||
.module-section + .module-section {
|
||||
border-top: 1px solid #f1f3f6;
|
||||
padding-top: 14px;
|
||||
}
|
||||
|
||||
.module-title {
|
||||
position: relative;
|
||||
padding-left: 10px;
|
||||
margin-bottom: 10px;
|
||||
color: #202733;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.module-title::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 4px;
|
||||
width: 3px;
|
||||
height: 14px;
|
||||
border-radius: 2px;
|
||||
background: #1989fa;
|
||||
}
|
||||
|
||||
.module-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px 8px;
|
||||
}
|
||||
|
||||
.module-grid-single {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.module-feature {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 8px 2px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.module-feature:active {
|
||||
background: #f2f7ff;
|
||||
}
|
||||
|
||||
.module-feature-img {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
object-fit: contain;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.module-feature-icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 6px;
|
||||
color: #ffffff;
|
||||
background: linear-gradient(135deg, #2f8af5 0%, #49c3dc 100%);
|
||||
}
|
||||
|
||||
.module-feature-name {
|
||||
width: 100%;
|
||||
color: #323233;
|
||||
font-size: 13px;
|
||||
line-height: 1.25;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.app-grid {
|
||||
display: grid;
|
||||
/* 改为自适应列数:每格最小 80px,自动换行 */
|
||||
|
||||
Reference in New Issue
Block a user