重复提交的问题,及去掉i18n

V0.1
yujialong 9 months ago
parent 250c14b3da
commit 149e71e481
  1. 4
      .env
  2. 110
      src/components/Panel/index.vue
  3. 6
      src/components/Tinymce/index.vue
  4. 118
      src/components/Upload/BaseUpload.vue
  5. 136
      src/components/Upload/FileListUpload.vue
  6. 84
      src/components/Upload/ImageCropper.vue
  7. 157
      src/components/Upload/ImageListUpload.vue
  8. 149
      src/components/Upload/ImageUpload.vue
  9. 4
      src/components/Upload/index.ts
  10. 1
      src/layout/components/AppSidebar/index.vue
  11. 2
      src/main.ts
  12. 6
      src/utils/getPageTitle.ts
  13. 14
      src/utils/request.ts
  14. 11
      src/views/403.vue
  15. 3
      src/views/bankProduct/index.vue
  16. 2
      src/views/report/Index.vue

@ -2,8 +2,8 @@ VITE_APP_TITLE=金融产品设计及数字化营销沙盘
VITE_PORT=9520 VITE_PORT=9520
VITE_PROXY=http://192.168.31.125:8080 VITE_PROXY=http://192.168.31.125:8080
VITE_PUBLIC_PATH=./ VITE_PUBLIC_PATH=./
VITE_BASE_API=http://192.168.31.51:9000 # VITE_BASE_API=http://192.168.31.217:9000
# VITE_BASE_API=http://121.37.12.51 # VITE_BASE_API=http://121.37.12.51
# VITE_BASE_API=https://www.occupationlab.com VITE_BASE_API=https://www.occupationlab.com
VITE_I18N_LOCALE=zh-cn VITE_I18N_LOCALE=zh-cn
VITE_I18N_FALLBACK_LOCALE=zh-cn VITE_I18N_FALLBACK_LOCALE=zh-cn

@ -222,12 +222,12 @@
class="z-[199] fixed top-[64px] right-0 bottom-0 left-0 bg-[rgba(0,0,0,.3)]"></div> class="z-[199] fixed top-[64px] right-0 bottom-0 left-0 bg-[rgba(0,0,0,.3)]"></div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted, inject, computed, watch } from 'vue'; import { ref, reactive, onMounted, inject, computed, watch, onUnmounted } from 'vue';
import { submitOpe } from '@/api/bank'; import { submitOpe } from '@/api/bank';
import { getSandTableLastCache, deleteOperationData } from '@/api/judgment'; import { getSandTableLastCache, deleteOperationData } from '@/api/judgment';
import { getProjectBySystemId, getProjectDetail, getDetailById, getCompetition, getStartTime } from '@/api/system'; import { getProjectBySystemId, getProjectDetail, getDetailById, getCompetition, getStartTime, heartbeatDetection } from '@/api/system';
import Settings from '@/settings'; import Settings from '@/settings';
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute, beforeRouteLeave } from 'vue-router';
import type { Action } from 'element-plus'; import type { Action } from 'element-plus';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { Close, Check, Rank } from '@element-plus/icons-vue'; import { Close, Check, Rank } from '@element-plus/icons-vue';
@ -253,6 +253,7 @@ const isReport = ref<boolean>(true);
const grade = ref<string | number>('00'); const grade = ref<string | number>('00');
const text = ref<string>(''); // const text = ref<string>(''); //
const counterTimer = ref<any>(null); const counterTimer = ref<any>(null);
const heartBeatTimer = ref<any>(null);
const day = ref<number | string>(0); const day = ref<number | string>(0);
const seconds = ref<number | string>(0); const seconds = ref<number | string>(0);
const minutes = ref<number | string>(0); const minutes = ref<number | string>(0);
@ -271,7 +272,6 @@ const countVal = ref<any>('');
const getLevel = ref(); const getLevel = ref();
const container = ref<HTMLElement | null>(null); const container = ref<HTMLElement | null>(null);
const handle = ref<HTMLElement | null>(null); const handle = ref<HTMLElement | null>(null);
const isFirst = ref<boolean>(true);
// //
const { x, y, style } = useDraggable(container, { const { x, y, style } = useDraggable(container, {
initialValue: { x: 0, y: 200 }, initialValue: { x: 0, y: 200 },
@ -283,6 +283,7 @@ const { x, y, style } = useDraggable(container, {
}, },
}); });
console.log('最先:', route, route.path, route.query.token, param.token);
if (param.token) { if (param.token) {
// urlcookiecookie // urlcookiecookie
param.token && Cookies.set('sand-token', param.token); param.token && Cookies.set('sand-token', param.token);
@ -342,7 +343,7 @@ watch(
); );
// //
const getSumTime = (reset?: number): Promise<any> => { let getSumTime = (reset?: number): Promise<any> => {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
const res = await getStartTime({ const res = await getStartTime({
permissions: per.value, permissions: per.value,
@ -353,7 +354,7 @@ const getSumTime = (reset?: number): Promise<any> => {
}); });
}; };
// //
const getEntryTime = async (resetTime?: number) => { let getEntryTime = async (resetTime?: number) => {
let now = await getSumTime(resetTime); // let now = await getSumTime(resetTime); //
if (!now) now = await getNow(); if (!now) now = await getNow();
entryTime.value = now; entryTime.value = now;
@ -363,7 +364,7 @@ const timeFormat = (num: number): string | number => {
return num < 10 ? `0${num}` : num; return num < 10 ? `0${num}` : num;
}; };
// //
const reloadCount = () => { let reloadCount = () => {
clearInterval(counterTimer.value); clearInterval(counterTimer.value);
countVal.value = ''; countVal.value = '';
day.value = '00'; day.value = '00';
@ -372,19 +373,25 @@ const reloadCount = () => {
hour.value = '00'; hour.value = '00';
}; };
// //
const counter = (counterTime: number) => { let counter = async (counterTime: number) => {
if (counterTime <= 0) { if (counterTime <= 0) {
if (per.value) { if (per.value) {
clearInterval(counterTimer.value); clearInterval(counterTimer.value);
// / // /
submit(); if (!Cookies.get('sand-submit')) {
Cookies.set('sand-submit', 'true');
await submit();
submiting.value = true;
ElMessageBox.alert(`${per.value === 1 ? '考核' : '竞赛'}时间已到,系统已自动交卷`, '提示', { ElMessageBox.alert(`${per.value === 1 ? '考核' : '竞赛'}时间已到,系统已自动交卷`, '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
closeOnClickModal: false,
showClose: false,
callback: (action: Action) => { callback: (action: Action) => {
logout(); logout();
}, },
}); });
} }
}
} else { } else {
const leave1 = counterTime % (24 * 3600); // const leave1 = counterTime % (24 * 3600); //
const leave2 = leave1 % 3600; // const leave2 = leave1 % 3600; //
@ -397,24 +404,25 @@ const counter = (counterTime: number) => {
} }
}; };
// //
const startCount = () => { let startCount = () => {
clearInterval(counterTimer.value); clearInterval(counterTimer.value);
counterTimer.value = setInterval(() => { counterTimer.value = setInterval(() => {
console.log('counter');
counter(per.value ? countVal.value-- : countVal.value++); counter(per.value ? countVal.value-- : countVal.value++);
}, 1000); }, 1000);
}; };
// //
const setSubmit = (val: boolean) => { let setSubmit = (val: boolean) => {
isSubmit.value = val; isSubmit.value = val;
Cookies.set('sand-submit', val); val ? Cookies.set('sand-submit', val) : Cookies.remove('sand-submit');
}; };
// //
const getAssList = async () => { let getAssList = async () => {
await getAssStatus(); await getAssStatus();
await getProDetail(); await getProDetail();
}; };
// //
const getAssStatus = async () => { let getAssStatus = async () => {
// //
if (!isSubmit.value) { if (!isSubmit.value) {
const { data } = await getDetailById(param.assessmentId); const { data } = await getDetailById(param.assessmentId);
@ -424,6 +432,8 @@ const getAssStatus = async () => {
submit(); submit();
ElMessageBox.alert(`考核时间已到,系统已自动交卷`, '提示', { ElMessageBox.alert(`考核时间已到,系统已自动交卷`, '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
closeOnClickModal: false,
showClose: false,
callback: (action: Action) => { callback: (action: Action) => {
logout(); logout();
}, },
@ -433,7 +443,7 @@ const getAssStatus = async () => {
}; };
// //
const getCompetitionStatus = async () => { let getCompetitionStatus = async () => {
// //
if (!isSubmit.value) { if (!isSubmit.value) {
const { competition } = await getCompetition(param.competitionId); const { competition } = await getCompetition(param.competitionId);
@ -447,6 +457,8 @@ const getCompetitionStatus = async () => {
submit(); submit();
ElMessageBox.alert(`竞赛时间已到,系统已自动交卷`, '提示', { ElMessageBox.alert(`竞赛时间已到,系统已自动交卷`, '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
closeOnClickModal: false,
showClose: false,
callback: (action: Action) => { callback: (action: Action) => {
logout(); logout();
}, },
@ -460,7 +472,7 @@ const getCompetitionStatus = async () => {
} }
}; };
// //
const delCache = async () => { let delCache = async () => {
await deleteOperationData({ await deleteOperationData({
cid: param.cid, cid: param.cid,
projectId: param.projectId, projectId: param.projectId,
@ -469,7 +481,7 @@ const delCache = async () => {
}); });
}; };
// //
const setNewProject = (reloadPage?: number) => { let setNewProject = (reloadPage?: number) => {
Cookies.set('sand-projectId', param.projectId); Cookies.set('sand-projectId', param.projectId);
getProDetail(); getProDetail();
reload(); reload();
@ -485,7 +497,7 @@ const setNewProject = (reloadPage?: number) => {
} }
}; };
// //
const getCache = async (reloadPage?: number) => { let getCache = async (reloadPage?: number) => {
// //
const res = await getSandTableLastCache({ const res = await getSandTableLastCache({
cid: param.cid, cid: param.cid,
@ -528,7 +540,7 @@ const getCache = async (reloadPage?: number) => {
setNewProject(); setNewProject();
} }
}; };
const handleCache = () => { let handleCache = () => {
// //
if (!isSubmit.value) { if (!isSubmit.value) {
param.cid && getEntryTime(); param.cid && getEntryTime();
@ -540,7 +552,7 @@ const toReport = () => {
router.push('/report'); router.push('/report');
}; };
// //
const reload = async (fromReload?: number) => { let reload = async (fromReload?: number) => {
if (fromReload) { if (fromReload) {
getEntryTime(1); getEntryTime(1);
await delCache(); // await delCache(); //
@ -556,13 +568,16 @@ const reload = async (fromReload?: number) => {
} }
}; };
// //
const submit = async () => { let submit = async () => {
console.log('submit:', submiting.value);
if (!submiting.value) {
submiting.value = true; submiting.value = true;
const checkpointId = Cookies.get('sand-level') ?? ''; const checkpointId = Cookies.get('sand-level') ?? '';
const date = new Date(); const date = await getNow();
const timeSum = Math.ceil((date.getTime() - entryTime.value.getTime()) / 60000); // const timeSum = Math.ceil((date.getTime() - entryTime.value.getTime()) / 60000); //
const submitTime = dayjs(date).format('YYYY-MM-DD HH:mm:ss'); const submitTime = dayjs(date).format('YYYY-MM-DD HH:mm:ss');
reloadCount(); reloadCount();
try {
const { retMap } = await submitOpe({ const { retMap } = await submitOpe({
classId: param.classId ? param.classId : '', classId: param.classId ? param.classId : '',
className: param.className ? param.className : '', className: param.className ? param.className : '',
@ -585,7 +600,6 @@ const submit = async () => {
}); });
setSubmit(true); setSubmit(true);
let score = 0;
// //
taskList.value.map((e) => { taskList.value.map((e) => {
const item = retMap?.scoreInfo.find((n) => n.lcId === e.judgmentId); const item = retMap?.scoreInfo.find((n) => n.lcId === e.judgmentId);
@ -593,13 +607,13 @@ const submit = async () => {
if (item) { if (item) {
e.examScore = item.questionScore; e.examScore = item.questionScore;
e.finishedResult = item.calculate; // 12 e.finishedResult = item.calculate; // 12
score += item.questionScore; //
} else { } else {
e.examScore = 0; e.examScore = 0;
} }
} catch (e) {} } catch (e) {}
}); });
grade.value = score < 10 ? '0' + score : score; const score = retMap.totalScore;
grade.value = score < 10 && !(score % 1) ? '0' + score : score;
reportId.value = retMap.reportId; reportId.value = retMap.reportId;
Cookies.set('sand-reportId', retMap.reportId); Cookies.set('sand-reportId', retMap.reportId);
Cookies.set('sand-score', grade.value); Cookies.set('sand-score', grade.value);
@ -609,15 +623,21 @@ const submit = async () => {
// //
per.value && per.value &&
ElMessageBox.alert(`提交成功${param.resultAnnouncementTime != 0 ? ',成绩将在' + param.resultAnnouncementTime + '小时后发布,请去参赛信息模块查看' : ''}`, '提示', { ElMessageBox.alert(`提交成功${param.resultAnnouncementTime ? ',成绩将在' + param.resultAnnouncementTime + '小时后发布,请去参赛信息模块查看' : ''}`, '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
closeOnClickModal: false,
showClose: false,
callback: (action: Action) => { callback: (action: Action) => {
logout(); logout();
}, },
}); });
} catch (e) {
submiting.value = false;
}
}
}; };
// //
const confirmSubmit = () => { let confirmSubmit = () => {
if (isSubmit.value) return false; if (isSubmit.value) return false;
if (!Cookies.get('sand-level')) return ElMessage.error('请选择关卡'); if (!Cookies.get('sand-level')) return ElMessage.error('请选择关卡');
ElMessageBox.confirm('此操作将视为结束考试,是否继续?', '提示', { ElMessageBox.confirm('此操作将视为结束考试,是否继续?', '提示', {
@ -633,7 +653,7 @@ const confirmSubmit = () => {
}; };
// //
const getProDetail = async () => { let getProDetail = async () => {
const res = await getProjectDetail({ const res = await getProjectDetail({
projectId: param.projectId, projectId: param.projectId,
stuAssessent: 1, stuAssessent: 1,
@ -720,9 +740,15 @@ const initSocket = () => {
// socket // socket
socket.onmessage = getMessage; socket.onmessage = getMessage;
}; };
//
let setHeartbeatDetection = () => {
heartBeatTimer.value = setInterval(async () => {
await heartbeatDetection();
}, 58 * 1000);
};
// //
const init = async () => { const init = async () => {
getLevel.value = inject('getLevel'); // console.log('init');
per.value = param.assessmentId ? 1 : param.competitionId ? 2 : 0; per.value = param.assessmentId ? 1 : param.competitionId ? 2 : 0;
if (param.assessmentId) { if (param.assessmentId) {
@ -730,11 +756,13 @@ const init = async () => {
await getAssList(); await getAssList();
handleCache(); handleCache();
initSocket(); initSocket();
setHeartbeatDetection();
} else if (param.competitionId) { } else if (param.competitionId) {
// //
getCompetitionStatus(); getCompetitionStatus();
handleCache(); handleCache();
initSocket(); initSocket();
setHeartbeatDetection();
} else { } else {
// //
param.cid && getList(); param.cid && getList();
@ -749,6 +777,30 @@ const init = async () => {
} }
}; };
onMounted(init); onMounted(init);
onUnmounted(() => {
counter = null;
submit = null;
setHeartbeatDetection = null;
getProDetail = null;
getCache = null;
getAssList = null;
getAssStatus = null;
getCompetitionStatus = null;
confirmSubmit = null;
handleCache = null;
getSumTime = null;
getEntryTime = null;
reloadCount = null;
delCache = null;
setNewProject = null;
reload = null;
startCount = null;
setSubmit = null;
clearInterval(counterTimer.value);
clearInterval(heartBeatTimer.value);
console.log('onUnmounted');
// next();
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

@ -7,7 +7,6 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, toRefs, watch, onMounted, onBeforeUnmount, onActivated, onDeactivated, PropType } from 'vue'; import { defineComponent, ref, toRefs, watch, onMounted, onBeforeUnmount, onActivated, onDeactivated, PropType } from 'vue';
import { useI18n } from 'vue-i18n';
import { getAuthHeaders } from '@/utils/auth'; import { getAuthHeaders } from '@/utils/auth';
import { imageUploadUrl } from '@/api/system'; import { imageUploadUrl } from '@/api/system';
@ -78,7 +77,6 @@ export default defineComponent({
}, },
setup(props, ctx) { setup(props, ctx) {
const { disabled, modelValue } = toRefs(props); const { disabled, modelValue } = toRefs(props);
const { t } = useI18n();
const element = ref<any>(); const element = ref<any>();
let vueEditor: any = null; let vueEditor: any = null;
const elementId: string = props.id || uuid('tiny-vue'); const elementId: string = props.id || uuid('tiny-vue');
@ -127,7 +125,7 @@ export default defineComponent({
images_upload_handler(blobInfo: any, success: any, failure: any, progress: any) { images_upload_handler(blobInfo: any, success: any, failure: any, progress: any) {
const fileSizeLimitByte = 0; const fileSizeLimitByte = 0;
if (fileSizeLimitByte > 0 && blobInfo.blob().size > fileSizeLimitByte) { if (fileSizeLimitByte > 0 && blobInfo.blob().size > fileSizeLimitByte) {
failure(t('error.fileMaxSize', { size: `${fileSizeLimitByte / 1024 / 1024}MB` }), { remove: true }); failure(`文件大小不能超过 ${`${fileSizeLimitByte / 1024 / 1024}MB`}`, { remove: true });
return; return;
} }
@ -210,7 +208,7 @@ export default defineComponent({
const file = files?.item(0); const file = files?.item(0);
if (!file) return; if (!file) return;
if (fileSizeLimtByte > 0 && file.size > fileSizeLimtByte) { if (fileSizeLimtByte > 0 && file.size > fileSizeLimtByte) {
tinymce.activeEditor.windowManager.alert(t('error.fileMaxSize', { size: `${fileSizeLimtByte / 1024 / 1024}MB` })); tinymce.activeEditor.windowManager.alert(`文件大小不能超过 ${`${fileSizeLimtByte / 1024 / 1024}MB`}`);
return; return;
} }
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();

@ -1,118 +0,0 @@
<template>
<el-upload
:action="action"
:headers="{ ...getAuthHeaders(), ...getSiteHeaders() }"
:accept="accept"
:before-upload="beforeUpload"
:on-progress="(event, file) => (progressFile = file)"
:show-file-list="false"
:disabled="disabled"
:multiple="multiple"
>
<!--
//
action="https://jsonplaceholder.typicode.com/posts/"
-->
<el-button type="primary">{{ $t('clickToUpload') }}</el-button>
</el-upload>
<el-progress v-if="progressFile.status === 'uploading'" :percentage="parseInt(progressFile.percentage, 10)"></el-progress>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, toRefs, computed } from 'vue';
import { ElMessage } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { getAuthHeaders } from '@/utils/auth';
import { getSiteHeaders } from '@/utils/common';
import { imageUploadUrl, videoUploadUrl, docUploadUrl, fileUploadUrl, queryGlobalSettings } from '@/api/config';
export default defineComponent({
name: 'BaseUpload',
props: {
type: {
type: String,
default: 'file',
validator: (value: string) => ['image', 'video', 'doc', 'file'].includes(value),
},
uploadAction: { type: String },
fileAccept: { type: String },
fileMaxSize: { type: Number },
multiple: { type: Boolean },
disabled: { type: Boolean, default: false },
'on-success': { type: Function },
},
setup(props) {
const { type, uploadAction, fileAccept, fileMaxSize } = toRefs(props);
const { t } = useI18n();
const progressFile = ref<any>({});
const global = ref<any>();
const fetchGlobalSettings = async () => {
global.value = await queryGlobalSettings();
};
onMounted(() => {
fetchGlobalSettings();
});
const action = computed(() => {
if (uploadAction?.value != null) {
return uploadAction.value;
}
switch (type.value) {
case 'image':
return imageUploadUrl;
case 'video':
return videoUploadUrl;
case 'doc':
return docUploadUrl;
case 'file':
return fileUploadUrl;
default:
throw new Error(`Type not support: ${type.value}`);
}
});
const accept = computed(() => {
if (fileAccept?.value != null) {
return fileAccept.value;
}
switch (type.value) {
case 'image':
return global?.value?.upload?.imageInputAccept ?? '.jpg,.jpeg,.png,.gif';
case 'video':
return global?.value?.upload?.videoInputAccept ?? '.mp4,.m3u8';
case 'doc':
return global?.value?.upload?.docInputAccept ?? '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx';
case 'file':
return global?.value?.upload?.fileInputAccept ?? '.zip,.7z,.gz,.bz2,.iso,.rar,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png,.gif,.mp4,.m3u8,.mp3,.ogg';
default:
throw new Error(`Type not support: ${type.value}`);
}
});
const maxSize = computed(() => {
if (fileMaxSize?.value != null) {
return fileMaxSize.value;
}
switch (type.value) {
case 'image':
return global?.value?.upload?.imageLimitByte ?? 0;
case 'video':
return global?.value?.upload?.videoLimitByte ?? 0;
case 'doc':
return global?.value?.upload?.docLimitByte ?? 0;
case 'file':
return global?.value?.upload?.fileLimitByte ?? 0;
default:
throw new Error(`Type not support: ${type.value}`);
}
});
const beforeUpload = (file: any) => {
if (maxSize.value > 0 && file.size > maxSize.value) {
ElMessage.error(t('error.fileMaxSize', { size: `${maxSize.value / 1024 / 1024}MB` }));
return false;
}
return true;
};
return { progressFile, getAuthHeaders, getSiteHeaders, action, accept, beforeUpload };
},
});
</script>
<style lang="scss" scoped></style>

@ -1,136 +0,0 @@
<template>
<div class="w-full">
<el-upload :action="fileUploadUrl"
:headers="{ ...getAuthHeaders(), ...getSiteHeaders() }"
:accept="accept"
:before-upload="beforeUpload"
:on-success="(res) => fileList.push({ name: res.name, url: res.url, length: res.size })"
:on-progress="(event, file) => (progressFile = file)"
:show-file-list="false"
multiple>
<!--
action="https://jsonplaceholder.typicode.com/posts/"
-->
<el-button type="primary">{{ $t('clickToUpload') }}</el-button>
</el-upload>
<el-progress v-if="progressFile.status === 'uploading'"
:percentage="parseInt(progressFile.percentage, 10)"></el-progress>
<transition-group tag="ul"
:class="['el-upload-list', 'el-upload-list--text', { 'is-disabled': disabled }]"
name="el-list">
<li v-for="file in fileList"
:key="file.url"
class="el-upload-list__item is-success">
<a class="el-upload-list__item-name"
@click="handlePreview(file)">
<el-icon class="el-icon--document">
<Document />
</el-icon>{{ file.name }}
</a>
<label class="el-upload-list__item-status-label">
<el-icon class="el-icon--upload-success el-icon--circle-check">
<CircleCheck />
</el-icon>
</label>
<el-icon v-if="!disabled"
class="el-icon--close"
@click="fileList.splice(fileList.indexOf(file), 1)">
<Close />
</el-icon>
</li>
</transition-group>
<el-dialog :title="$t('article.fileList.attribute')"
v-model="previewVisible"
top="5vh"
:width="768"
append-to-body>
<el-form ref="form"
:model="previewFile"
label-width="150px">
<el-form-item prop="name"
:label="$t('name')"
:rules="{ required: true, message: () => $t('v.required') }">
<el-input v-model="previewFile.name"
maxlength="100"></el-input>
</el-form-item>
<el-form-item prop="length"
:label="$t('size')"
:rules="{ required: true, message: () => $t('v.required') }">
<el-input v-model="previewFile.length"
maxlength="19">
<template #append>Byte</template>
</el-input>
</el-form-item>
<el-form-item prop="url"
label="URL"
:rules="{ required: true, message: () => $t('v.required') }">
<el-input v-model="previewFile.url"
maxlength="255"></el-input>
</el-form-item>
<el-button @click.prevent="handleSubmit()"
type="primary"
native-type="submit">{{ $t('submit') }}</el-button>
</el-form>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, toRefs, computed } from 'vue';
import { ElMessage } from 'element-plus';
import { Close, Document, CircleCheck } from '@element-plus/icons-vue';
import { useI18n } from 'vue-i18n';
import { getAuthHeaders } from '@/utils/auth';
import { getSiteHeaders } from '@/utils/common';
import { fileUploadUrl, queryGlobalSettings } from '@/api/config';
const props = defineProps({
modelValue: { type: Array, default: () => [] },
fileAccept: { type: String },
fileMaxSize: { type: Number },
disabled: { type: Boolean, default: false },
});
const emit = defineEmits({ 'update:modelValue': null });
const { fileAccept, fileMaxSize } = toRefs(props);
const { t } = useI18n();
const { modelValue } = toRefs(props);
const progressFile = ref<any>({});
const fileList = computed({
get: (): any[] => modelValue.value,
set: (val) => emit('update:modelValue', val),
});
const previewVisible = ref<boolean>(false);
const previewFile = ref<any>({});
const form = ref<any>();
const handlePreview = (file: any) => {
previewFile.value = file;
previewVisible.value = true;
};
const handleSubmit = () => {
form.value.validate(async (valid: boolean) => {
if (!valid) return;
previewVisible.value = false;
});
};
const global = ref<any>();
const fetchGlobalSettings = async () => {
global.value = await queryGlobalSettings();
};
onMounted(() => {
fetchGlobalSettings();
});
const defaultAccept = '.zip,.7z,.gz,.bz2,.iso,.rar,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png,.gif,.mp4,.m3u8,.mp3,.ogg';
const accept = computed(() => fileAccept?.value ?? global?.value?.upload?.fileInputAccept ?? defaultAccept);
const maxSize = computed(() => fileMaxSize?.value ?? global?.value?.upload?.fileLimitByte ?? 0);
const beforeUpload = (file: any) => {
if (maxSize.value > 0 && file.size > maxSize.value) {
ElMessage.error(t('error.fileMaxSize', { size: `${maxSize.value / 1024 / 1024}MB` }));
return false;
}
return true;
};
</script>
<style lang="scss" scoped></style>

@ -1,84 +0,0 @@
<template>
<el-dialog :title="$t('imageCrop')" v-model="visible" @closed="destroyCropper()" top="5vh" :width="768" destroy-on-close append-to-body>
<div class="text-center">
<img ref="imgRef" @load="initCropper()" :src="src" alt="" class="inline" style="max-height:410px" />
</div>
<div class="text-right">
<el-button @click.prevent="handleSubmit()" type="primary" native-type="submit" class="mt-4">{{ $t('submit') }}</el-button>
</div>
</el-dialog>
</template>
<script lang="ts">
import { computed, defineComponent, ref, toRefs } from 'vue';
import Cropper from 'cropperjs';
import 'cropperjs/dist/cropper.css';
import { cropImage } from '@/api/config';
export default defineComponent({
name: 'ImageCropper',
props: {
modelValue: { type: Boolean, required: true },
src: { type: String, default: null },
width: { type: Number },
height: { type: Number },
thumbnailWidth: { type: Number },
thumbnailHeight: { type: Number },
},
emits: { 'update:modelValue': null, success: null },
setup(props, { emit }) {
const { modelValue, src, width, height, thumbnailWidth, thumbnailHeight } = toRefs(props);
const visible = computed({
get: () => modelValue.value,
set: (val) => emit('update:modelValue', val),
});
const imgRef = ref<any>();
const cropper = ref<any>();
const cropParam = ref<any>({});
const initCropper = () => {
if (imgRef.value) {
cropper.value = new Cropper(imgRef.value, {
aspectRatio: width?.value && height?.value ? width.value / height.value : NaN,
autoCropArea: width?.value && height?.value ? 1 : 0.8,
viewMode: 1,
minCropBoxWidth: width?.value ?? 16,
minCropBoxHeight: height?.value ?? 16,
zoomable: false,
crop(event) {
cropParam.value.url = src.value;
cropParam.value.x = Math.floor(event.detail.x);
cropParam.value.y = Math.floor(event.detail.y);
cropParam.value.width = Math.floor(event.detail.width);
cropParam.value.height = Math.floor(event.detail.height);
cropParam.value.maxWidth = width?.value;
cropParam.value.maxHeight = height?.value;
cropParam.value.thumbnailWidth = thumbnailWidth?.value;
cropParam.value.thumbnailHeight = thumbnailHeight?.value;
},
});
}
};
const destroyCropper = () => {
if (cropper.value) {
cropper.value.destroy();
}
};
const handleSubmit = async () => {
visible.value = false;
emit('success', (await cropImage(cropParam.value)).url);
};
return { imgRef, visible, initCropper, destroyCropper, handleSubmit };
},
});
</script>
<style lang="scss" scoped>
/* Ensure the size of the image fit the container perfectly */
:deep(img) {
display: block;
/* This rule is very important, please don't ignore this */
max-width: 100%;
}
</style>

@ -1,157 +0,0 @@
<template>
<div>
<!-- <transition-group tag="ul" :class="['el-upload-list', 'el-upload-list--picture-card', { 'is-disabled': disabled }]" name="el-list"> -->
<ul :class="['el-upload-list', 'el-upload-list--picture-card', { 'is-disabled': disabled }]">
<li v-for="file in fileList"
:key="file.url"
class="el-upload-list__item is-success">
<div class="w-full h-full bg-gray-50 flex justify-center items-center">
<img class="max-w-full max-h-full block"
:src="file.url"
alt="" />
<div class="full-flex-center absolute rounded-md cursor-default bg-black bg-opacity-50 opacity-0 hover:opacity-100 space-x-4"
@click.stop>
<el-icon class="image-action"
@click="(cropperVisible = true), (currentFile = file)"
:title="$t('cropImage')">
<Crop />
</el-icon>
<el-icon class="image-action"
@click="handlePreview(file)"
:title="$t('previewImage')">
<View />
</el-icon>
<el-icon class="image-action"
@click="fileList.splice(fileList.indexOf(file), 1)"
:title="$t('deleteImage')">
<Delete />
</el-icon>
</div>
</div>
</li>
</ul>
<!-- </transition-group> -->
<el-upload :action="imageUploadUrl"
:headers="{ ...getAuthHeaders(), ...getSiteHeaders() }"
:data="getData()"
:accept="accept"
:before-upload="beforeUpload"
:on-success="(res, file) => fileList.push({ name: res.name, url: res.url })"
:on-progress="(event, file) => (progressFile = file)"
:show-file-list="false"
multiple
class="inline-block">
<el-progress v-if="progressFile.status === 'uploading'"
type="circle"
:percentage="parseInt(progressFile.percentage, 10)" />
<div v-else
class="el-upload--picture-card">
<el-icon>
<Plus />
</el-icon>
</div>
</el-upload>
<div>
<el-dialog v-model="previewVisible"
top="5vh"
:width="768">
<el-input v-model="previewFile.url"
maxlength="255">
<template #prepend>URL</template>
</el-input>
<el-input v-model="previewFile.description"
type="textarea"
:rows="2"
:placeholder="$t('article.imageList.description')"
class="mt-1"></el-input>
<img :src="previewFile.url"
alt=""
class="mt-1 border border-gray-300" />
</el-dialog>
</div>
<image-cropper v-model="cropperVisible"
:src="currentFile.url"
:thumbnailWidth="thumbnailWidth"
:thumbnailHeight="thumbnailHeight"
@success="(url) => (currentFile.url = url)"></image-cropper>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, toRefs, computed } from 'vue';
import { ElMessage } from 'element-plus';
import { Plus, Crop, View, Delete } from '@element-plus/icons-vue';
import { useI18n } from 'vue-i18n';
import { getAuthHeaders } from '@/utils/auth';
import { getSiteHeaders } from '@/utils/common';
import { imageUploadUrl, queryGlobalSettings } from '@/api/config';
import ImageCropper from './ImageCropper.vue';
const props = defineProps({
modelValue: { type: Array, default: () => [] },
fileAccept: { type: String },
fileMaxSize: { type: Number },
maxWidth: { type: Number },
maxHeight: { type: Number },
disabled: { type: Boolean, default: false },
});
const emit = defineEmits({ 'update:modelValue': null });
const { modelValue, maxWidth, maxHeight, fileAccept, fileMaxSize } = toRefs(props);
const { t } = useI18n();
const progressFile = ref<any>({});
const currentFile = ref<any>({});
const previewVisible = ref<boolean>(false);
const cropperVisible = ref<boolean>(false);
const previewFile = ref<any>({ src: 'data:;base64,=' });
const fileList = computed({
get: (): any => modelValue.value,
set: (val) => emit('update:modelValue', val),
});
const handlePreview = (file: any) => {
previewFile.value = file;
previewVisible.value = true;
};
const thumbnailWidth = 300;
const thumbnailHeight = 300;
const getData = () => {
const data: any = { isWatermark: true, thumbnailWidth, thumbnailHeight };
if (maxWidth?.value != null) {
data.maxWidth = maxWidth.value;
}
if (maxHeight?.value != null) {
data.maxHeight = maxHeight.value;
}
return data;
};
const global = ref<any>();
const fetchGlobalSettings = async () => {
global.value = await queryGlobalSettings();
};
onMounted(() => {
fetchGlobalSettings();
});
const defaultAccept = 'image/jpg,image/jpeg,image/png,image/gif';
const accept = computed(() => fileAccept?.value ?? global?.value?.upload?.imageInputAccept ?? defaultAccept);
const maxSize = computed(() => fileMaxSize?.value ?? global?.value?.upload?.imageLimitByte ?? 0);
const beforeUpload = (file: any) => {
if (maxSize.value > 0 && file.size > maxSize.value) {
ElMessage.error(t('error.fileMaxSize', { size: `${maxSize.value / 1024 / 1024}MB` }));
return false;
}
return true;
};
</script>
<style lang="scss" scoped>
:deep(.el-dialog__headerbtn) {
top: 4px;
}
.full-flex-center {
@apply w-full h-full flex justify-center items-center;
}
.image-action {
@apply cursor-pointer text-xl text-white;
}
</style>

@ -1,149 +0,0 @@
<template>
<el-upload :action="imageUploadUrl"
:headers="{ ...getAuthHeaders(), ...getSiteHeaders() }"
:accept="accept"
:before-upload="beforeUpload"
:data="data"
:show-file-list="false"
:on-success="(res) => ((src = res.url), (cropperVisible = mode === 'manual'))"
:on-progress="(event, file) => (progressFile = file)">
<!--
//
action="https://jsonplaceholder.typicode.com/posts/"
-->
<div v-if="src"
class="full-flex-center rounded-border relative hover:border-opacity-0">
<img :src="src"
class="max-w-full max-h-full block" />
<div class="full-flex-center absolute rounded-md cursor-default bg-black bg-opacity-50 opacity-0 hover:opacity-100 space-x-4"
@click.stop>
<el-icon class="image-action"
@click="cropperVisible = true"
:title="$t('cropImage')">
<Crop />
</el-icon>
<el-icon class="image-action"
@click="previewVisible = true"
:title="$t('previewImage')">
<View />
</el-icon>
<el-icon class="image-action"
@click="src = undefined"
:title="$t('deleteImage')">
<Delete />
</el-icon>
</div>
</div>
<el-progress v-else-if="progressFile.status === 'uploading'"
type="circle"
:percentage="parseInt(progressFile.percentage, 10)" />
<div v-else
class="el-upload--picture-card">
<el-icon>
<plus />
</el-icon>
</div>
</el-upload>
<div>
<el-dialog v-model="previewVisible"
top="5vh"
:width="768"
append-to-body
destroy-on-close>
<el-input v-model="src">
<template #prepend>URL</template>
</el-input>
<img :src="src"
alt=""
class="mt-1 border border-gray-300" />
</el-dialog>
</div>
<image-cropper v-model="cropperVisible"
:src="src"
:width="width"
:height="height"
@success="(url) => (src = url)"></image-cropper>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, toRefs } from 'vue';
import { ElMessage } from 'element-plus';
import { Plus, Crop, View, Delete } from '@element-plus/icons-vue';
import { useI18n } from 'vue-i18n';
import { getAuthHeaders } from '@/utils/auth';
import { getSiteHeaders } from '@/utils/common';
import { imageUploadUrl, queryGlobalSettings } from '@/api/config';
import ImageCropper from './ImageCropper.vue';
// 'image/jpg,image/jpeg,image/png,image/gif'
const props = defineProps({
modelValue: { type: String, default: null },
fileAccept: { type: String },
fileMaxSize: { type: Number },
width: { type: Number },
height: { type: Number },
mode: { type: String, default: 'none' },
});
const emit = defineEmits({ 'update:modelValue': null });
const { modelValue, width, height, mode, fileAccept, fileMaxSize } = toRefs(props);
const { t } = useI18n();
const progressFile = ref<any>({});
const previewVisible = ref<boolean>(false);
const cropperVisible = ref<boolean>(false);
const src = computed({
get: (): string | undefined => modelValue.value,
set: (val: string | undefined) => emit('update:modelValue', val),
});
const resizable = computed(() => ['cut', 'resize'].includes(mode.value));
const data = computed(() => {
const params: any = { resizeMode: mode.value === 'cut' ? 'cut' : 'normal' };
if (width?.value != null) {
// 0
params.maxWidth = resizable.value ? width.value : 0;
}
if (height?.value != null) {
// 0
params.maxHeight = resizable.value ? height.value : 0;
}
return params;
});
const global = ref<any>();
const fetchGlobalSettings = async () => {
global.value = await queryGlobalSettings();
};
onMounted(() => {
fetchGlobalSettings();
});
const accept = computed(() => fileAccept?.value ?? global?.value?.upload?.imageInputAccept ?? 'image/jpg,image/jpeg,image/png,image/gif');
const maxSize = computed(() => fileMaxSize?.value ?? global?.value?.upload?.imageLimitByte ?? 0);
const beforeUpload = (file: any) => {
if (maxSize.value > 0 && file.size > maxSize.value) {
ElMessage.error(t('error.fileMaxSize', { size: `${maxSize.value / 1024 / 1024}MB` }));
return false;
}
return true;
};
</script>
<style lang="scss" scoped>
:deep(.el-dialog__headerbtn) {
top: 4px;
}
:deep(.el-upload) {
width: 148px;
height: 148px;
}
.full-flex-center {
@apply w-full h-full flex justify-center items-center;
}
.rounded-border {
border: 1px solid #c0ccda;
@apply rounded-md bg-gray-50;
}
.image-action {
@apply cursor-pointer text-xl text-white;
}
</style>

@ -1,4 +0,0 @@
export { default as ImageUpload } from './ImageUpload.vue';
export { default as ImageListUpload } from './ImageListUpload.vue';
export { default as FileListUpload } from './FileListUpload.vue';
export { default as BaseUpload } from './BaseUpload.vue';

@ -47,7 +47,6 @@ import { useRouter, useRoute } from 'vue-router';
import Menus from './Menu.vue'; import Menus from './Menu.vue';
import Settings from '@/settings'; import Settings from '@/settings';
import { getOperationTime, saveOperationTime } from '@/api/config'; import { getOperationTime, saveOperationTime } from '@/api/config';
import { appState } from '@/store/useAppState';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { getNow } from '@/utils/common'; import { getNow } from '@/utils/common';

@ -1,5 +1,6 @@
import { createApp } from 'vue'; import { createApp } from 'vue';
import ElementPlus from 'element-plus'; import ElementPlus from 'element-plus';
// import Panel from '@/components/Panel/index.vue';
import App from './App.vue'; import App from './App.vue';
import router from './router'; import router from './router';
import i18n from './i18n'; import i18n from './i18n';
@ -14,4 +15,5 @@ const app = createApp(App)
// tinymce 对话框的层级太低,必须调低 ElementPlus 的 对话框层级(默认为2000) // tinymce 对话框的层级太低,必须调低 ElementPlus 的 对话框层级(默认为2000)
.use(ElementPlus, { zIndex: 500 }) .use(ElementPlus, { zIndex: 500 })
.use(i18n); .use(i18n);
// app.component('Panel', Panel);
app.mount('#app'); app.mount('#app');

@ -1,14 +1,10 @@
import defaultSettings from '@/settings'; import defaultSettings from '@/settings';
import i18n from '@/i18n';
const { title } = defaultSettings; const { title } = defaultSettings;
export default function getPageTitle(pageTitle: string | undefined): string { export default function getPageTitle(pageTitle: string | undefined): string {
if (pageTitle) { if (pageTitle) {
const { return `${pageTitle} - ${title}`;
global: { t },
} = i18n;
return `${t(pageTitle)} - ${title}`;
} }
return `${title}`; return `${title}`;
} }

@ -1,8 +1,6 @@
import { h } from 'vue';
import axios from 'axios'; import axios from 'axios';
import { ElMessageBox, ElMessage } from 'element-plus'; import { ElMessageBox, ElMessage } from 'element-plus';
import { getAuthHeaders } from '@/utils/auth'; import { getAuthHeaders } from '@/utils/auth';
import i18n from '@/i18n';
import { logout } from '@/store/useCurrentUser'; import { logout } from '@/store/useCurrentUser';
const service = axios.create({ const service = axios.create({
@ -33,23 +31,15 @@ service.interceptors.response.use(
(e) => { (e) => {
const { const {
response: { response: {
data: { timestamp, message, path, error, exception, trace }, data: { message, error },
status, status,
}, },
} = e; } = e;
const {
global: { t },
} = i18n;
if (status === 401) { if (status === 401) {
ElMessageBox.confirm(t('confirmLogin'), { confirmButtonText: t('loginAgain'), type: 'warning' }).then(() => { ElMessageBox.alert('登录状态已过期,请重新登录', { confirmButtonText: '重新登录', type: 'warning', closeOnClickModal: false, showClose: false }).then(() => {
// 未登录 // 未登录
logout(); logout();
}); });
} else if (status === 403) {
ElMessageBox({
title: status,
message: h('div', null, [h('p', { class: 'text-lg' }, t('error.forbidden')), h('p', { class: 'mt-2' }, message)]),
});
} else if (message) { } else if (message) {
ElMessage.error(message); ElMessage.error(message);
} }

@ -2,9 +2,11 @@
<div class="h-full p-4 bg-gray-200"> <div class="h-full p-4 bg-gray-200">
<div class="p-4 rounded shadow bg-white"> <div class="p-4 rounded shadow bg-white">
<h1 class="font-bold text-3xl">403</h1> <h1 class="font-bold text-3xl">403</h1>
<p class="mt-4">{{ message }}</p> <p class="mt-4">对不起您没有该页面的访问权限</p>
<p class="mt-4"> <p class="mt-4">
<el-button type="primary" @click="handleLogout()" plain>{{ $t('logout') }}</el-button> <el-button type="primary"
@click="handleLogout()"
plain>退出</el-button>
</p> </p>
</div> </div>
</div> </div>
@ -13,20 +15,17 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, ref } from 'vue'; import { defineComponent, ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { logout } from '@/store/useCurrentUser'; import { logout } from '@/store/useCurrentUser';
export default defineComponent({ export default defineComponent({
name: 'Page403', name: 'Page403',
setup() { setup() {
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const message = ref(t('error.forbidden'));
const handleLogout = () => { const handleLogout = () => {
logout(); logout();
router.push('/login'); router.push('/login');
}; };
return { message, handleLogout }; return { handleLogout };
}, },
}); });
</script> </script>

@ -60,13 +60,11 @@
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { Search } from '@element-plus/icons-vue'; import { Search } from '@element-plus/icons-vue';
import { useI18n } from 'vue-i18n';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { perm } from '@/store/useCurrentUser'; import { perm } from '@/store/useCurrentUser';
import { pageSizes, pageLayout, toParams, resetParams } from '@/utils/common'; import { pageSizes, pageLayout, toParams, resetParams } from '@/utils/common';
import { deleteUser, queryUserPage } from '@/api/user'; import { deleteUser, queryUserPage } from '@/api/user';
const { t } = useI18n();
const params = ref<any>({}); const params = ref<any>({});
const sort = ref<any>(); const sort = ref<any>();
const currentPage = ref<number>(1); const currentPage = ref<number>(1);
@ -129,7 +127,6 @@ const handleEdit = (id: number) => {
const handleDelete = async (ids: number[]) => { const handleDelete = async (ids: number[]) => {
await deleteUser(ids); await deleteUser(ids);
fetchData(); fetchData();
ElMessage.success(t('success'));
}; };
const deletable = (bean: any) => bean.id > 1; const deletable = (bean: any) => bean.id > 1;
</script> </script>

@ -205,12 +205,10 @@
</div> </div>
</div> </div>
</div> </div>
<Panel />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import Panel from '@/components/Panel/index.vue';
import { logout } from '@/store/useCurrentUser'; import { logout } from '@/store/useCurrentUser';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';

Loading…
Cancel
Save