307 lines
21 KiB
Vue
307 lines
21 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onMounted, reactive, ref } from 'vue'
|
||
import { Check, EditPen, Refresh, Search } from '@element-plus/icons-vue'
|
||
import http, { apiErrorMessage } from '../api/http'
|
||
import { useAuthStore } from '../stores/auth'
|
||
import { academicTermLabel, academicTermOptionClass, defaultAcademicTermId } from '../utils/academicTerms'
|
||
|
||
const auth = useAuthStore()
|
||
const isWarningManager = computed(() => auth.user?.roles.some(r => ['SuperAdmin', 'AcademicAdmin'].includes(r)) ?? false)
|
||
const isCounselor = computed(() => auth.user?.roles.includes('Counselor') && !auth.user?.roles.some(r => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'].includes(r)))
|
||
const isStudent = computed(() => auth.user?.roles.includes('Student') && !auth.user?.roles.some(r => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor'].includes(r)))
|
||
|
||
const terms = ref<any[]>([])
|
||
const termId = ref('')
|
||
const records = ref<any[]>([])
|
||
const myWarnings = ref<any[]>([])
|
||
const myRiskCases = ref<any[]>([])
|
||
const loading = ref(false)
|
||
const detecting = ref(false)
|
||
const filterType = ref('')
|
||
const recordsPage = ref(1)
|
||
const recordsCursors = ref<string[]>([])
|
||
const nextRecordsCursor = ref<string>()
|
||
const recordsHasMore = ref(false)
|
||
const recordsPageSize = 20
|
||
const riskCases = ref<any[]>([])
|
||
const riskDialogVisible = ref(false)
|
||
const selectedRiskCase = ref<any>(null)
|
||
const riskEntry = reactive({ type: 1, content: '', occurredAt: '' })
|
||
|
||
const typeLabels: Record<number, string> = { 1: '不及格学分', 2: '低绩点', 3: '缺勤', 4: '延毕风险' }
|
||
const statusLabels: Record<number, string> = { 1: '生效中', 2: '已确认', 3: '已处理', 4: '已忽略' }
|
||
|
||
function tagType(type: number): 'danger' | 'warning' | 'info' {
|
||
return type === 1 ? 'danger' : type === 2 ? 'warning' : 'info'
|
||
}
|
||
function thresholdUnit(type: number): string {
|
||
if (type === 1) return '门 不及格'
|
||
if (type === 2) return 'GPA 下限'
|
||
if (type === 3) return '次 缺勤/迟到'
|
||
return '门 累计不及格'
|
||
}
|
||
|
||
const weekOptions = [{ value: 0, label: '每天' }, { value: 1, label: '周一' }, { value: 2, label: '周二' }, { value: 3, label: '周三' }, { value: 4, label: '周四' }, { value: 5, label: '周五' }, { value: 6, label: '周六' }, { value: 7, label: '周日' }]
|
||
|
||
const ruleRows = reactive([
|
||
{ type: 1 as number, name: '不及格学分', threshold: 2, isEnabled: true, notifyStudent: true, notifyCounselor: true, description: '', autoCheckEnabled: false, checkDayOfWeek: 0, checkHour: 8, checkMinute: 0, lastCheckAt: null as string | null },
|
||
{ type: 2, name: '低绩点', threshold: 2, isEnabled: true, notifyStudent: true, notifyCounselor: true, description: '', autoCheckEnabled: false, checkDayOfWeek: 0, checkHour: 8, checkMinute: 0, lastCheckAt: null as string | null },
|
||
{ type: 3, name: '缺勤', threshold: 3, isEnabled: true, notifyStudent: true, notifyCounselor: true, description: '', autoCheckEnabled: false, checkDayOfWeek: 0, checkHour: 8, checkMinute: 0, lastCheckAt: null as string | null },
|
||
{ type: 4, name: '延毕风险', threshold: 5, isEnabled: true, notifyStudent: true, notifyCounselor: true, description: '', autoCheckEnabled: false, checkDayOfWeek: 0, checkHour: 8, checkMinute: 0, lastCheckAt: null as string | null },
|
||
])
|
||
|
||
async function load(resetRecordsPage = false) {
|
||
if (resetRecordsPage) {
|
||
recordsPage.value = 1
|
||
recordsCursors.value = []
|
||
nextRecordsCursor.value = undefined
|
||
}
|
||
loading.value = true
|
||
try {
|
||
if (isWarningManager.value && termId.value) {
|
||
const serverRules = (await http.get('/warnings/rules', { params: { academicTermId: termId.value } })).data
|
||
// Merge server values into reactive rows
|
||
for (const row of ruleRows) {
|
||
const sr = serverRules.find((r: any) => Number(r.type) === row.type)
|
||
if (sr) {
|
||
row.threshold = sr.threshold
|
||
row.isEnabled = sr.isEnabled
|
||
row.notifyStudent = sr.notifyStudent
|
||
row.notifyCounselor = sr.notifyCounselor
|
||
row.description = sr.description || ''
|
||
row.autoCheckEnabled = sr.autoCheckEnabled
|
||
row.checkDayOfWeek = sr.checkDayOfWeek
|
||
row.checkHour = sr.checkHour
|
||
row.checkMinute = sr.checkMinute
|
||
row.lastCheckAt = sr.lastCheckAt
|
||
}
|
||
}
|
||
}
|
||
if (isWarningManager.value || isCounselor.value)
|
||
{
|
||
const response = await http.get('/warnings/records', {
|
||
params: {
|
||
academicTermId: termId.value || undefined,
|
||
type: filterType.value || undefined,
|
||
cursor: recordsCursors.value.at(-1),
|
||
pageSize: recordsPageSize,
|
||
},
|
||
})
|
||
records.value = response.data.items
|
||
recordsHasMore.value = response.data.hasMore
|
||
nextRecordsCursor.value = response.data.nextCursor
|
||
riskCases.value = (await http.get('/warnings/risk-cases', { params: { mineOnly: isCounselor.value } })).data
|
||
}
|
||
if (isStudent.value) {
|
||
myWarnings.value = (await http.get('/warnings/my-warnings')).data
|
||
myRiskCases.value = (await http.get('/warnings/my-risk-cases')).data
|
||
}
|
||
} catch (e) { ElMessage.error(apiErrorMessage(e)) } finally { loading.value = false }
|
||
}
|
||
|
||
async function nextRecordsPage() {
|
||
if (!nextRecordsCursor.value || loading.value) return
|
||
recordsCursors.value.push(nextRecordsCursor.value)
|
||
recordsPage.value++
|
||
await load()
|
||
}
|
||
|
||
async function previousRecordsPage() {
|
||
if (!recordsCursors.value.length || loading.value) return
|
||
recordsCursors.value.pop()
|
||
recordsPage.value--
|
||
await load()
|
||
}
|
||
|
||
async function saveRules() {
|
||
try {
|
||
const payload = ruleRows.map(r => ({ type: r.type, name: r.name, threshold: r.threshold, isEnabled: r.isEnabled, notifyStudent: r.notifyStudent, notifyCounselor: r.notifyCounselor, description: r.description, autoCheckEnabled: r.autoCheckEnabled, checkDayOfWeek: r.checkDayOfWeek, checkHour: r.checkHour, checkMinute: r.checkMinute }))
|
||
await http.put('/warnings/rules', payload, { params: { academicTermId: termId.value } })
|
||
await load()
|
||
ElMessage.success('预警规则已保存')
|
||
} catch (e) { ElMessage.error(apiErrorMessage(e)) }
|
||
}
|
||
|
||
async function detectWarnings() {
|
||
try {
|
||
await ElMessageBox.confirm('系统将扫描当前学期全部在籍学生,产生的预警将自动通知学生及辅导员。', '执行学业预警检测', { type: 'warning', confirmButtonText: '开始检测' })
|
||
detecting.value = true
|
||
const res = await http.post('/warnings/detect', null, { params: { academicTermId: termId.value } })
|
||
ElMessage.success(`检测完成,生成 ${res.data.generated} 条预警`)
|
||
await load()
|
||
} catch (e: any) { if (e !== 'cancel' && e !== 'close') ElMessage.error(apiErrorMessage(e)) } finally { detecting.value = false }
|
||
}
|
||
|
||
async function acknowledge(w: any) {
|
||
try {
|
||
const { value } = await ElMessageBox.prompt('确认已知晓此预警?(可选填备注)', '确认预警', { confirmButtonText: '确认', inputType: 'textarea' })
|
||
await http.post(`/warnings/${w.id}/acknowledge`, { comment: value || null }); ElMessage.success('已确认'); await load()
|
||
} catch (e: any) { if (e !== 'cancel' && e !== 'close') ElMessage.error(apiErrorMessage(e)) }
|
||
}
|
||
|
||
async function openRiskCase(row: any) {
|
||
try { selectedRiskCase.value = (await http.get(`/warnings/risk-cases/${row.id}`)).data; riskEntry.type = 1; riskEntry.content = ''; riskEntry.occurredAt = ''; riskDialogVisible.value = true }
|
||
catch (e) { ElMessage.error(apiErrorMessage(e)) }
|
||
}
|
||
async function saveRiskCase() {
|
||
try {
|
||
const c = selectedRiskCase.value
|
||
await http.put(`/warnings/risk-cases/${c.id}`, { status: c.status, followUpDueDate: c.followUpDueDate, reviewDate: c.reviewDate || null })
|
||
ElMessage.success('跟进任务已更新'); await load()
|
||
} catch (e) { ElMessage.error(apiErrorMessage(e)) }
|
||
}
|
||
async function addRiskEntry() {
|
||
if (!riskEntry.content.trim()) { ElMessage.warning('请填写记录内容'); return }
|
||
try {
|
||
await http.post(`/warnings/risk-cases/${selectedRiskCase.value.id}/entries`, { type: riskEntry.type, content: riskEntry.content, occurredAt: riskEntry.occurredAt || null })
|
||
await openRiskCase(selectedRiskCase.value); await load(); ElMessage.success('处置记录已保存')
|
||
} catch (e) { ElMessage.error(apiErrorMessage(e)) }
|
||
}
|
||
|
||
onMounted(async () => {
|
||
try { terms.value = (await http.get('/base-data/terms')).data; termId.value = defaultAcademicTermId(terms.value) ?? ''; await load() }
|
||
catch (e) { ElMessage.error(apiErrorMessage(e)) }
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="page-stack warn-page">
|
||
<section class="page-intro">
|
||
<div><span class="section-kicker">ACADEMIC WARNING</span><h2>{{ isStudent ? '我的预警' : '学业预警' }}</h2><p>{{ isWarningManager ? '配置预警规则,执行检测,查看预警记录。' : isCounselor ? '查看所管学生的学业预警情况。' : '查看并确认您的学业预警通知。' }}</p></div>
|
||
<div style="display:flex;gap:8px;align-items:center">
|
||
<el-select v-model="termId" clearable @change="load(true)" style="width:240px"><el-option v-for="t in terms" :key="t.id" :label="academicTermLabel(t)" :value="t.id" :class="academicTermOptionClass(t)" /></el-select>
|
||
<el-button :icon="Refresh" @click="() => load()">刷新</el-button>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Admin: Rule config -->
|
||
<section v-if="isWarningManager && termId" class="warn-rules">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
||
<h3 style="margin:0">预警规则配置</h3>
|
||
<div>
|
||
<el-button type="primary" @click="saveRules">保存规则</el-button>
|
||
<el-button :icon="Search" :loading="detecting" type="warning" @click="detectWarnings">执行检测</el-button>
|
||
</div>
|
||
</div>
|
||
<el-table :data="ruleRows" size="small">
|
||
<el-table-column label="类型" width="100"><template #default="{row}"><b>{{ row.name }}</b></template></el-table-column>
|
||
<el-table-column label="阈值" width="160">
|
||
<template #default="{row}">
|
||
<el-input-number v-model="row.threshold" :min="1" :max="row.type===4?20:row.type===3?50:10" :precision="row.type===2?1:0" size="small" />
|
||
<span style="margin-left:4px;font-size:12px;color:var(--muted)">{{ thresholdUnit(row.type) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="开关" width="200">
|
||
<template #default="{row}">
|
||
<el-switch v-model="row.isEnabled" size="small" active-text="启用" style="margin-right:12px" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="通知" width="180">
|
||
<template #default="{row}">
|
||
<el-checkbox v-model="row.notifyStudent" :disabled="!row.isEnabled">学生</el-checkbox>
|
||
<el-checkbox v-model="row.notifyCounselor" :disabled="!row.isEnabled" style="margin-left:8px">辅导员</el-checkbox>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="自动检测" min-width="290">
|
||
<template #default="{row}">
|
||
<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap">
|
||
<el-switch v-model="row.autoCheckEnabled" size="small" :disabled="!row.isEnabled" />
|
||
<template v-if="row.autoCheckEnabled">
|
||
<span style="font-size:11px">每</span>
|
||
<el-select v-model="row.checkDayOfWeek" size="small" style="width:75px">
|
||
<el-option v-for="o in weekOptions" :key="String(o.value)" :label="o.label" :value="o.value" />
|
||
</el-select>
|
||
<el-input-number v-model="row.checkHour" :min="0" :max="23" size="small" :controls="false" style="width:56px" />
|
||
<span style="font-size:11px">时</span>
|
||
<el-input-number v-model="row.checkMinute" :min="0" :max="59" size="small" :controls="false" style="width:56px" />
|
||
<span style="font-size:11px">分</span>
|
||
<span v-if="row.lastCheckAt" style="font-size:10px;color:var(--muted)">上次: {{ new Date(row.lastCheckAt).toLocaleString('zh-CN') }}</span>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="说明"><template #default="{row}"><el-input v-model="row.description" size="small" placeholder="可选" maxlength="300" /></template></el-table-column>
|
||
</el-table>
|
||
</section>
|
||
|
||
<!-- Records -->
|
||
<section v-if="isWarningManager || isCounselor" v-loading="loading" class="warn-records">
|
||
<div style="display:flex;gap:12px;align-items:center;margin-bottom:12px">
|
||
<h3 style="margin:0">预警记录</h3>
|
||
<el-select v-model="filterType" clearable placeholder="全部类型" @change="load(true)" style="width:140px"><el-option v-for="(v,k) in typeLabels" :key="k" :label="v" :value="Number(k)" /></el-select>
|
||
</div>
|
||
<el-table :data="records" size="small" v-if="records.length">
|
||
<el-table-column label="学生" min-width="150"><template #default="{row}"><b>{{ row.studentName }}</b><span style="font-size:11px;color:var(--muted);margin-left:6px">{{ row.studentNumber }}</span></template></el-table-column>
|
||
<el-table-column label="班级" prop="className" width="120" />
|
||
<el-table-column label="类型" width="100"><template #default="{row}"><el-tag size="small" :type="tagType(row.type)">{{ typeLabels[row.type] }}</el-tag></template></el-table-column>
|
||
<el-table-column label="阈值/详情" min-width="200"><template #default="{row}"><span>{{ row.triggerValue }}</span><p style="margin:0;font-size:11px;color:var(--muted)">{{ row.detail }}</p></template></el-table-column>
|
||
<el-table-column label="状态" width="100"><template #default="{row}"><el-tag size="small" :type="row.status===1?'danger':row.status===2?'success':'info'">{{ statusLabels[row.status] }}</el-tag></template></el-table-column>
|
||
</el-table>
|
||
<div v-if="recordsCursors.length || recordsHasMore" class="table-pagination cursor-pager">
|
||
<el-button :disabled="!recordsCursors.length || loading" @click="previousRecordsPage">上一批</el-button>
|
||
<span>第 {{ recordsPage }} 批</span>
|
||
<el-button type="primary" :disabled="!recordsHasMore || loading" @click="nextRecordsPage">下一批</el-button>
|
||
</div>
|
||
<el-empty v-if="!records.length" description="暂无预警记录,请先执行检测。" />
|
||
</section>
|
||
|
||
<section v-if="isWarningManager || isCounselor" class="risk-tasks">
|
||
<div class="risk-head"><div><h3>风险处置跟进</h3><p>谈话和帮扶记录为敏感信息,仅被指派辅导员和学业管理人员可见。</p></div><el-tag type="danger">待办 {{ riskCases.filter(x => x.status !== 4).length }}</el-tag></div>
|
||
<el-table :data="riskCases" size="small">
|
||
<el-table-column label="学生" min-width="140"><template #default="{row}"><b>{{ row.studentName }}</b><small>{{ row.studentNumber }}</small></template></el-table-column>
|
||
<el-table-column label="预警类型" width="110"><template #default="{row}">{{ typeLabels[row.warningType] }}</template></el-table-column>
|
||
<el-table-column label="跟进截止" width="120"><template #default="{row}">{{ row.followUpDueDate }}</template></el-table-column>
|
||
<el-table-column label="复核日期" width="120"><template #default="{row}">{{ row.reviewDate || '待设置' }}</template></el-table-column>
|
||
<el-table-column label="状态" width="110"><template #default="{row}">{{ ['','待跟进','跟进中','待复核','已结案'][row.status] }}</template></el-table-column>
|
||
<el-table-column label="操作" width="110"><template #default="{row}"><el-button size="small" type="primary" :icon="EditPen" @click="openRiskCase(row)">处置</el-button></template></el-table-column>
|
||
</el-table>
|
||
<el-empty v-if="!riskCases.length" description="暂无需要跟进的风险任务" :image-size="70" />
|
||
</section>
|
||
|
||
<!-- Student -->
|
||
<section v-if="isStudent" v-loading="loading" class="warn-mine">
|
||
<article v-for="w in myWarnings" :key="w.id" class="warn-card" :class="{ active: w.status===1 }">
|
||
<div><el-tag size="small" :type="(w.type===1?'danger':w.type===2?'warning':'info') as 'danger'|'warning'|'info'">{{ typeLabels[w.type] }}</el-tag></div>
|
||
<div class="warn-body"><b>{{ w.detail }}</b><small>{{ new Date(w.createdAt).toLocaleString('zh-CN') }}</small></div>
|
||
<div class="warn-status">
|
||
<el-tag size="small" :type="w.status===1?'danger':w.status===2?'success':'info'">{{ statusLabels[w.status] }}</el-tag>
|
||
<el-button v-if="w.status===1" size="small" type="primary" :icon="Check" @click="acknowledge(w)">确认</el-button>
|
||
</div>
|
||
</article>
|
||
<el-empty v-if="!myWarnings.length" description="暂无学业预警" />
|
||
<section v-if="myRiskCases.length" class="my-risk-progress">
|
||
<div><h3>我的帮扶与复核进度</h3><p>这里展示辅导员针对本人的谈话、帮扶和复核记录。</p></div>
|
||
<article v-for="riskCase in myRiskCases" :key="riskCase.id" class="my-risk-case">
|
||
<div class="risk-case-dates"><span>跟进截止:{{ riskCase.followUpDueDate }}</span><span>复核日期:{{ riskCase.reviewDate || '待安排' }}</span></div>
|
||
<el-timeline><el-timeline-item v-for="entry in riskCase.entries" :key="entry.id" :timestamp="new Date(entry.occurredAt).toLocaleString('zh-CN')"><b>{{ ['','谈话记录','帮扶措施','复核记录'][entry.type] }}</b><p>{{ entry.content }}</p><small>记录人:{{ entry.recordedByName }}</small></el-timeline-item></el-timeline>
|
||
<el-empty v-if="!riskCase.entries.length" description="辅导员暂未填写处置记录" :image-size="55" />
|
||
</article>
|
||
</section>
|
||
</section>
|
||
</div>
|
||
<el-dialog v-model="riskDialogVisible" title="学业风险处置(敏感)" width="680px" destroy-on-close>
|
||
<template v-if="selectedRiskCase">
|
||
<p class="risk-warning"><b>{{ selectedRiskCase.studentName }}</b>({{ selectedRiskCase.studentNumber }}) · {{ selectedRiskCase.warningDetail }}</p>
|
||
<el-form label-width="90px" class="risk-form"><el-form-item label="跟进状态"><el-select v-model="selectedRiskCase.status"><el-option :value="1" label="待跟进"/><el-option :value="2" label="跟进中"/><el-option :value="3" label="待复核"/><el-option :value="4" label="已结案"/></el-select></el-form-item><el-form-item label="跟进截止"><el-date-picker v-model="selectedRiskCase.followUpDueDate" type="date" value-format="YYYY-MM-DD" /></el-form-item><el-form-item label="复核日期"><el-date-picker v-model="selectedRiskCase.reviewDate" type="date" clearable value-format="YYYY-MM-DD" /></el-form-item><el-form-item><el-button type="primary" @click="saveRiskCase">保存任务</el-button></el-form-item></el-form>
|
||
<el-divider>新增处置记录</el-divider>
|
||
<el-form label-width="90px"><el-form-item label="记录类别"><el-radio-group v-model="riskEntry.type"><el-radio :value="1">谈话记录</el-radio><el-radio :value="2">帮扶措施</el-radio><el-radio :value="3">复核记录</el-radio></el-radio-group></el-form-item><el-form-item label="发生时间"><el-date-picker v-model="riskEntry.occurredAt" type="datetime" clearable value-format="YYYY-MM-DDTHH:mm:ss" /></el-form-item><el-form-item label="内容"><el-input v-model="riskEntry.content" type="textarea" maxlength="2000" show-word-limit :rows="4" placeholder="记录谈话要点、帮扶措施或复核结论" /></el-form-item><el-form-item><el-button type="primary" @click="addRiskEntry">保存记录</el-button></el-form-item></el-form>
|
||
<el-divider>处置历史</el-divider><el-timeline><el-timeline-item v-for="entry in selectedRiskCase.entries" :key="entry.id" :timestamp="new Date(entry.occurredAt).toLocaleString('zh-CN')"><b>{{ ['','谈话记录','帮扶措施','复核记录'][entry.type] }}</b> · {{ entry.recordedByName }}<p>{{ entry.content }}</p></el-timeline-item></el-timeline>
|
||
</template>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.warn-rules { background:#fff; border:1px solid #e4e7ed; border-radius:8px; padding:16px 20px; margin-bottom:20px; }
|
||
.warn-records { background:#fff; border:1px solid #e4e7ed; border-radius:8px; padding:16px 20px; }
|
||
.warn-mine { display:grid; gap:10px; }
|
||
.warn-card { display:flex; align-items:center; gap:14px; padding:14px 18px; background:#fff; border:1px solid #e4e7ed; border-radius:8px; }
|
||
.warn-card.active { border-left:4px solid #f56c6c; }
|
||
.warn-body { flex:1; }
|
||
.warn-body b { font-size:14px; display:block; margin-bottom:4px; }
|
||
.warn-body small { font-size:11px; color:var(--muted); }
|
||
.warn-status { display:flex; align-items:center; gap:8px; }
|
||
.risk-tasks { background:#fff; border:1px solid #e4e7ed; border-radius:8px; padding:16px 20px; margin-top:20px; }
|
||
.risk-head { display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:12px; }.risk-head h3 { margin:0 0 4px; }.risk-head p { margin:0; font-size:12px; color:var(--muted); }.risk-tasks small { display:block; color:var(--muted); font-size:11px; }.risk-warning { background:#fff7e6; padding:10px 12px; border-radius:4px; }.risk-form { max-width:420px; }
|
||
.my-risk-progress { background:#fff; border:1px solid #e4e7ed; border-radius:8px; padding:16px 20px; }.my-risk-progress h3 { margin:0 0 4px; }.my-risk-progress > div > p { margin:0 0 14px; color:var(--muted); font-size:12px; }.my-risk-case { border-top:1px solid #f0f1f3; padding-top:14px; margin-top:14px; }.my-risk-case:first-of-type { border-top:0; padding-top:0; margin-top:0; }.risk-case-dates { display:flex; flex-wrap:wrap; gap:16px; margin-bottom:12px; font-size:12px; color:var(--muted); }.my-risk-case p { margin:4px 0; white-space:pre-wrap; }.my-risk-case small { color:var(--muted); }
|
||
</style>
|