前端部分更新
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="!dataForm.id ? '新增数据' : '修改数据'" width="900px" :close-on-click-modal="false">
|
||||
<el-form :model="dataForm" :rules="rules" ref="dataFormRef" label-width="140px" style="padding-right: 20px">
|
||||
<el-row :gutter="24">
|
||||
<el-divider content-position="left">基本信息</el-divider>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="站点ID" prop="stationId">
|
||||
<el-select v-if="!manualInput" v-model="dataForm.stationId" filterable placeholder="从列表选择或输入" style="width: 100%" clearable>
|
||||
<el-option v-for="s in stationOptions" :key="s.stationCode" :label="`${s.stationCode} ${s.stationName || ''}`" :value="s.stationCode" />
|
||||
<template #empty>
|
||||
<div style="padding: 8px; text-align: center">
|
||||
<el-button size="small" type="primary" link @click="manualInput = true">未找到?点击手动输入</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-select>
|
||||
<el-input v-else v-model="dataForm.stationId" placeholder="手动输入区站号">
|
||||
<template #suffix>
|
||||
<el-button size="small" link @click="manualInput = false">列表选择</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="观测日期" prop="observeDate">
|
||||
<el-date-picker v-model="dataForm.observeDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD 00:00:00" style="width: 100%"></el-date-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-divider content-position="left">温湿度与气压</el-divider>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="相对湿度" prop="avgTemp" label-width="80px">
|
||||
<el-input-number v-model="dataForm.relativeHumidity" :precision="1" :step="0.1" controls-position="right" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="气压" prop="avgTemp" label-width="80px">
|
||||
<el-input-number v-model="dataForm.atmospheres" :precision="1" :step="0.1" controls-position="right" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="平均气温" prop="avgTemp" label-width="80px">
|
||||
<el-input-number v-model="dataForm.avgTemp" :precision="1" :step="0.1" controls-position="right" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="最高气温" prop="maxTemp" label-width="80px">
|
||||
<el-input-number v-model="dataForm.maxTemp" :precision="1" :step="0.1" controls-position="right" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="最低气温" prop="minTemp" label-width="80px">
|
||||
<el-input-number v-model="dataForm.minTemp" :precision="1" :step="0.1" controls-position="right" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="最高气温出现时间" prop="maxTempTime" label-width="125px">
|
||||
<el-input v-model="dataForm.maxTempTime" placeholder="HHmm"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="最低气温出现时间" prop="minTempTime" label-width="125px">
|
||||
<el-input v-model="dataForm.minTempTime" placeholder="HHmm"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-divider content-position="left">降水与风力</el-divider>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="20-20时降水量" prop="rain2020">
|
||||
<el-input-number v-model="dataForm.rain2020" :precision="1" :min="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="08-08时降水量" prop="rain0808">
|
||||
<el-input-number v-model="dataForm.rain0808" :precision="1" :min="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="日平均风速" prop="maxWindSpeed">
|
||||
<el-input-number v-model="dataForm.dayAvgWindSpeed" :precision="1" :min="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="日平均风向" prop="maxWindSpeed">
|
||||
<el-input-number v-model="dataForm.dayAvgWindDirection" :precision="1" :min="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="最大风速" prop="maxWindSpeed">
|
||||
<el-input-number v-model="dataForm.maxWindSpeed" :precision="1" :min="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="最大风速风向" prop="maxWindDirection">
|
||||
<el-input-number v-model="dataForm.maxWindDirection" :min="0" :max="360" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="极大风速" prop="maxWindSpeed">
|
||||
<el-input-number v-model="dataForm.extremeWindSpeed" :precision="1" :min="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="极大风速风向" prop="maxWindSpeed">
|
||||
<el-input-number v-model="dataForm.extremeWindDirection" :precision="1" :min="0" style="width: 100%"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="极大风速出现时间" prop="minTempTime" label-width="125px">
|
||||
<el-input v-model="dataForm.extremeWindTime" placeholder="HHmm"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="最大风速出现时间" prop="minTempTime" label-width="125px">
|
||||
<el-input v-model="dataForm.maxWindTime" placeholder="HHmm"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="dataFormSubmitHandle()">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, onMounted } from "vue";
|
||||
import baseService from "@/service/baseService";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
const emit = defineEmits(["refreshDataList"]);
|
||||
const visible = ref(false);
|
||||
const dataFormRef = ref();
|
||||
const stationOptions = ref<any[]>([]);
|
||||
const manualInput = ref(false);
|
||||
|
||||
const dataForm = reactive({
|
||||
id: "",
|
||||
stationId: "",
|
||||
observeDate: "",
|
||||
avgTemp: 0,
|
||||
maxTemp: 0,
|
||||
maxTempTime: "",
|
||||
minTemp: 0,
|
||||
minTempTime: "",
|
||||
rain2020: 0,
|
||||
rain0808: 0,
|
||||
relativeHumidity: 0,
|
||||
atmospheres: 0,
|
||||
dayAvgWindSpeed: 0,
|
||||
dayAvgWindDirection: 0,
|
||||
maxWindSpeed: 0,
|
||||
maxWindDirection: 0,
|
||||
maxWindTime: "",
|
||||
extremeWindSpeed: 0,
|
||||
extremeWindDirection: 0,
|
||||
extremeWindTime: "",
|
||||
createDate: ""
|
||||
});
|
||||
|
||||
// 校验规则优化
|
||||
const rules = ref({
|
||||
stationId: [{ required: true, message: "站点不能为空", trigger: "blur" }],
|
||||
observeDate: [{ required: true, message: "观测日期不能为空", trigger: "change" }],
|
||||
// 时刻格式简单校验 (HHmm)
|
||||
maxTempTime: [{ pattern: /^([01]\d|2[0-3])[0-5]\d$/, message: "格式应为HHmm(如0715)", trigger: "blur" }],
|
||||
minTempTime: [{ pattern: /^([01]\d|2[0-3])[0-5]\d$/, message: "格式应为HHmm(如1430)", trigger: "blur" }]
|
||||
});
|
||||
|
||||
function getStationList() {
|
||||
return baseService.get("/station/weatherstation/list").then((res) => {
|
||||
stationOptions.value = res.data || [];
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getStationList();
|
||||
});
|
||||
|
||||
const init = (id?: number) => {
|
||||
visible.value = true;
|
||||
manualInput.value = false;
|
||||
dataForm.id = "";
|
||||
Object.assign(dataForm, {
|
||||
avgTemp: 0,
|
||||
maxTemp: 0,
|
||||
minTemp: 0,
|
||||
rain2020: 0,
|
||||
rain0808: 0,
|
||||
maxWindSpeed: 0,
|
||||
maxWindDirection: 0,
|
||||
extremeWindSpeed: 0
|
||||
});
|
||||
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
}
|
||||
|
||||
if (id) {
|
||||
getInfo(id);
|
||||
}
|
||||
};
|
||||
|
||||
const getInfo = (id: number) => {
|
||||
baseService.get("/dailyweather/weatherdailydata/" + id).then((res) => {
|
||||
Object.assign(dataForm, res.data);
|
||||
});
|
||||
};
|
||||
|
||||
const dataFormSubmitHandle = () => {
|
||||
dataFormRef.value.validate((valid: boolean) => {
|
||||
if (!valid) return false;
|
||||
|
||||
const action = !dataForm.id ? baseService.post : baseService.put;
|
||||
action("/dailyweather/weatherdailydata", dataForm).then((res) => {
|
||||
ElMessage.success({
|
||||
message: "提交成功",
|
||||
duration: 800,
|
||||
onClose: () => {
|
||||
visible.value = false;
|
||||
emit("refreshDataList");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({ init });
|
||||
</script>
|
||||
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="批量导入气象数据" width="450px" :close-on-click-modal="false">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
class="upload-demo"
|
||||
drag
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
:on-change="handleChange"
|
||||
:http-request="uploadFileRequest"
|
||||
:limit="1"
|
||||
:on-exceed="handleExceed"
|
||||
accept=".xlsx, .xls"
|
||||
:disabled="loading"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">将文件拖到此处,或 <em>点击选择</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip" style="text-align: center;">
|
||||
只能上传 Excel 文件
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button :disabled="loading" @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="dataFormSubmitHandle">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import baseService from "@/service/baseService";
|
||||
import { useImportTaskStore } from "@/store/importTasks";
|
||||
|
||||
const taskStore = useImportTaskStore();
|
||||
const POLL_INTERVAL = 3000;
|
||||
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const uploadRef = ref();
|
||||
const fileRaw = ref<File | null>(null);
|
||||
const emit = defineEmits(['refreshDataList']);
|
||||
|
||||
const init = () => {
|
||||
visible.value = true;
|
||||
fileRaw.value = null;
|
||||
if(uploadRef.value) uploadRef.value.clearFiles();
|
||||
};
|
||||
|
||||
const handleChange = (file: any) => {
|
||||
fileRaw.value = file.raw;
|
||||
};
|
||||
|
||||
const handleExceed = (files: any) => {
|
||||
uploadRef.value!.clearFiles();
|
||||
const file = files[0];
|
||||
uploadRef.value!.handleStart(file);
|
||||
};
|
||||
|
||||
function pollProgress(taskId: string, localTaskId: string) {
|
||||
const timer = setInterval(async () => {
|
||||
try {
|
||||
const res = await baseService.get(`/dailyweather/weatherdailydata/import/progress/${taskId}`);
|
||||
if (res?.code === 0 && res.data) {
|
||||
const { totalRows, processedRows, errorMessage } = res.data;
|
||||
const st = (res.data.status || "").toUpperCase();
|
||||
taskStore.updateTask(localTaskId, {
|
||||
totalRows: totalRows || 0,
|
||||
processedRows: typeof processedRows === "object" ? 0 : (processedRows || 0)
|
||||
});
|
||||
if (st === "COMPLETED") {
|
||||
clearInterval(timer);
|
||||
taskStore.updateTask(localTaskId, { status: "success", message: "导入成功", completedAt: Date.now() });
|
||||
emit("refreshDataList");
|
||||
} else if (st === "FAILED" || st === "ERROR") {
|
||||
clearInterval(timer);
|
||||
taskStore.updateTask(localTaskId, { status: "error", message: errorMessage || "导入失败", completedAt: Date.now() });
|
||||
} else {
|
||||
const processed = typeof processedRows === "object" ? 0 : (processedRows || 0);
|
||||
const total = totalRows || 0;
|
||||
const pct = total > 0 ? Math.round((processed / total) * 100) : 0;
|
||||
taskStore.updateTask(localTaskId, { message: `正在导入数据 ${pct}% (${processed}/${total})` });
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// keep polling on network error
|
||||
}
|
||||
}, POLL_INTERVAL);
|
||||
return timer;
|
||||
}
|
||||
|
||||
const dataFormSubmitHandle = () => {
|
||||
if (!fileRaw.value) {
|
||||
return ElMessage.warning("请先选择文件");
|
||||
}
|
||||
|
||||
const localTaskId = Date.now().toString() + Math.random().toString(36).slice(2, 6);
|
||||
taskStore.addTask({
|
||||
id: localTaskId,
|
||||
fileName: fileRaw.value.name,
|
||||
status: "uploading",
|
||||
message: "正在上传文件...",
|
||||
totalRows: 0,
|
||||
processedRows: 0,
|
||||
createdAt: Date.now(),
|
||||
completedAt: null,
|
||||
backendTaskId: ""
|
||||
});
|
||||
|
||||
visible.value = false;
|
||||
ElMessage.success("导入已开始,可在通知中心查看进度");
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", fileRaw.value);
|
||||
|
||||
baseService.upload("/dailyweather/weatherdailydata/import", formData)
|
||||
.then((res: any) => {
|
||||
if (res?.code === 0) {
|
||||
const backendTaskId = res.data?.taskId || res.data?.toString() || "";
|
||||
if (backendTaskId) {
|
||||
taskStore.updateTask(localTaskId, {
|
||||
status: "processing",
|
||||
message: "正在导入数据...",
|
||||
backendTaskId
|
||||
});
|
||||
pollProgress(backendTaskId, localTaskId);
|
||||
} else {
|
||||
taskStore.updateTask(localTaskId, { status: "success", message: "导入成功", completedAt: Date.now() });
|
||||
emit("refreshDataList");
|
||||
}
|
||||
} else {
|
||||
taskStore.updateTask(localTaskId, { status: "error", message: res?.msg || "导入失败", completedAt: Date.now() });
|
||||
ElMessage.error(res?.msg || "导入失败");
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
taskStore.updateTask(localTaskId, { status: "error", message: err?.message || "网络错误", completedAt: Date.now() });
|
||||
console.error("上传细节错误:", err);
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({ init });
|
||||
</script>
|
||||
@@ -0,0 +1,293 @@
|
||||
<template>
|
||||
<div class="mod-dailyweather__weatherdailydata">
|
||||
<el-form :inline="true" :model="state.dataForm" @keyup.enter="state.getDataList()">
|
||||
<el-form-item>
|
||||
<el-select v-model="realDataForm.stationId" multiple filterable remote reserve-keyword clearable placeholder="请输入区站名称" :remote-method="remoteSearchStation" :loading="loading" style="width: 300px">
|
||||
<el-option v-for="item in stationOptions" :key="item.stationCode" :label="`${item.stationName}`" :value="item.stationCode" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- <el-form-item>-->
|
||||
<!-- <el-date-picker v-model="realDataForm.observeDate" type="datetimerange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" value-format="YYYY-MM-DD" :default-time="[new Date(2000, 1, 1), new Date(2000, 1, 1)]" style="width: 360px" />-->
|
||||
<!-- </el-form-item>-->
|
||||
|
||||
<el-form-item>
|
||||
<el-button @click="state.getDataList">查询</el-button>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button v-if="state.hasPermission('dailyweather:weatherdailydata:save')" type="primary" icon="Plus" @click="addOrUpdateHandle()">新增</el-button>
|
||||
<el-button v-if="state.hasPermission('dailyweather:weatherdailydata:delete')" type="danger" icon="Delete" @click="state.deleteHandle()">批量删除</el-button>
|
||||
<el-button type="success" icon="Upload" @click="importHandle()">批量导入</el-button>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item style="float: right; margin-right: 0">
|
||||
<el-popover placement="bottom-end" :width="300" trigger="click">
|
||||
<template #reference>
|
||||
<el-button icon="Grid">展示列设置</el-button>
|
||||
</template>
|
||||
<div class="column-setting-container">
|
||||
<div v-for="group in columnGroups" :key="group.title" class="column-group">
|
||||
<div class="group-title">{{ group.title }}</div>
|
||||
<el-checkbox-group v-model="selectedColumns">
|
||||
<el-checkbox v-for="col in group.columns" :key="col.prop" :label="col.prop" style="width: 120px">
|
||||
{{ col.label }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
<el-divider style="margin: 8px 0" />
|
||||
</div>
|
||||
<div style="text-align: right">
|
||||
<el-button size="small" type="primary" link @click="resetColumns">恢复默认</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table v-loading="state.dataListLoading" :data="state.dataList" border @selection-change="state.dataListSelectionChangeHandle" style="width: 100%">
|
||||
<el-table-column type="selection" header-align="center" align="center" width="50" fixed="left"></el-table-column>
|
||||
|
||||
<template v-for="group in columnGroups">
|
||||
<template v-for="item in group.columns">
|
||||
<el-table-column v-if="selectedColumns.includes(item.prop)" :key="item.prop" :prop="item.prop" :label="item.label" header-align="center" align="center" min-width="120" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<span v-if="item.prop === 'observeDate'">
|
||||
{{ formatObserveDate(scope.row.observeDate) }}
|
||||
</span>
|
||||
|
||||
<span v-else-if="item.prop.endsWith('Time') && item.prop !== 'createTime'">
|
||||
<el-tag v-if="isValidTime(scope.row[item.prop])" size="small" effect="plain" type="info">
|
||||
{{ formatWeatherTime(scope.row[item.prop]) }}
|
||||
</el-tag>
|
||||
<span v-else>--</span>
|
||||
</span>
|
||||
|
||||
<span v-else-if="item.prop === 'dayAvgWindDirection' || item.prop === 'maxWindDirection'">
|
||||
{{ windDirectionLabel(scope.row[item.prop]) }}
|
||||
</span>
|
||||
<span v-else>{{ scope.row[item.prop] ?? "--" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<el-table-column label="操作" fixed="right" header-align="center" align="center" width="150">
|
||||
<template v-slot="scope">
|
||||
<el-button v-if="state.hasPermission('dailyweather:weatherdailydata:update')" type="primary" link @click="addOrUpdateHandle(scope.row.id)">修改</el-button>
|
||||
<el-button v-if="state.hasPermission('dailyweather:weatherdailydata:delete')" type="primary" link @click="state.deleteHandle(scope.row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination :current-page="state.page" :page-sizes="[10, 20, 50, 100]" :page-size="state.limit" :total="state.total" layout="total, sizes, prev, pager, next, jumper" @size-change="state.pageSizeChangeHandle" @current-change="state.pageCurrentChangeHandle" />
|
||||
|
||||
<add-or-update ref="addOrUpdateRef" @refreshDataList="state.getDataList" />
|
||||
<import-excel ref="importExcelRef" @refreshDataList="state.getDataList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useView from "@/hooks/useView";
|
||||
import { reactive, ref, toRefs, watch, onMounted, computed } from "vue";
|
||||
import AddOrUpdate from "./weatherdailydata-add-or-update.vue";
|
||||
import ImportExcel from "./weatherdailydata-import.vue";
|
||||
import baseService from "@/service/baseService";
|
||||
import { useAppStore } from "@/store";
|
||||
|
||||
const _store = useAppStore();
|
||||
function _windDirectionCode(deg: any) {
|
||||
if (deg == null || deg === "" || isNaN(deg)) return null;
|
||||
const d = Number(deg);
|
||||
if (d < 0 || d > 360) return null;
|
||||
return ["N", "NE", "E", "SE", "S", "SW", "W", "NW"][Math.floor((d + 22.5) / 45) % 8];
|
||||
}
|
||||
function windDirectionLabel(deg: any) {
|
||||
const code = _windDirectionCode(deg);
|
||||
if (!code) return "—";
|
||||
const type = _store.state.dicts.find((d: any) => d.dictType === "wind_direction_type");
|
||||
const entry = type?.dataList?.find((e: any) => e.dictValue === code);
|
||||
return entry?.dictLabel || code;
|
||||
}
|
||||
|
||||
const columnGroups = [
|
||||
{
|
||||
title: "基础信息",
|
||||
columns: [
|
||||
{ prop: "stationId", label: "区站号", default: true },
|
||||
{ prop: "stationName", label: "站名", default: true },
|
||||
{ prop: "observeDate", label: "观测日期", default: true },
|
||||
{ prop: "createDate", label: "创建时间", default: false },
|
||||
{ prop: "id", label: "系统编号", default: false }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "温湿度气压数据",
|
||||
columns: [
|
||||
{ prop: "avgTemp", label: "平均气温", default: true },
|
||||
{ prop: "maxTemp", label: "最高气温", default: true },
|
||||
{ prop: "minTemp", label: "最低气温", default: true },
|
||||
{ prop: "maxTempTime", label: "最高气温时间", default: false },
|
||||
{ prop: "minTempTime", label: "最低气温时间", default: false },
|
||||
{ prop: "relativeHumidity", label: "相对湿度", default: true },
|
||||
{ prop: "atmospheres", label: "气压", default: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "降水/风力数据",
|
||||
columns: [
|
||||
{ prop: "rain2020", label: "20-20降水", default: false },
|
||||
{ prop: "rain0808", label: "08-08降水", default: false },
|
||||
{ prop: "dayAvgWindSpeed", label: "日平均风速", default: true },
|
||||
{ prop: "dayAvgWindDirection", label: "日平均风向", default: true },
|
||||
{ prop: "maxWindSpeed", label: "最大风速", default: false },
|
||||
{ prop: "maxWindDirection", label: "最大风向", default: false },
|
||||
{ prop: "maxWindTime", label: "最大风时间", default: false },
|
||||
{ prop: "extremeWindSpeed", label: "极大风速", default: false },
|
||||
{ prop: "extremeWindTime", label: "极大风时间", default: false }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// 智能日期截取
|
||||
const formatObserveDate = (dateStr: any) => {
|
||||
if (!dateStr) return "--";
|
||||
const str = String(dateStr);
|
||||
const separator = str.includes("T") ? "T" : " ";
|
||||
return str.split(separator)[0];
|
||||
};
|
||||
|
||||
// 校验有效时间数字
|
||||
const isValidTime = (val: any) => {
|
||||
return val !== null && val !== undefined && val !== "" && !isNaN(Number(val));
|
||||
};
|
||||
|
||||
// 时刻格式化:712 -> 07:12
|
||||
const formatWeatherTime = (time: any) => {
|
||||
let timeStr = String(time);
|
||||
if (timeStr.length === 3) timeStr = "0" + timeStr;
|
||||
if (timeStr.length === 4) {
|
||||
return `${timeStr.substring(0, 2)}:${timeStr.substring(2, 4)}`;
|
||||
}
|
||||
return timeStr;
|
||||
};
|
||||
interface WeatherStation {
|
||||
stationName: string;
|
||||
stationCode: string;
|
||||
}
|
||||
const loading = ref(false);
|
||||
const stationOptions = ref<WeatherStation[]>([]); // 明确这是一个 WeatherStation 对象的数组
|
||||
/**
|
||||
* 远程搜索区站
|
||||
*/
|
||||
const remoteSearchStation = async (keyword: string) => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await baseService.get("/station/weatherstation/list");
|
||||
if (res.code === 0) {
|
||||
// TypeScript 自动知道 item 是 WeatherStation 类型
|
||||
stationOptions.value = res.data.filter((item: { stationName: string | string[]; stationCode: string | string[] }) => !keyword || item.stationName.includes(keyword) || item.stationCode.includes(keyword));
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 1. 定义真实的表单数据(保持数组,供 <el-select multiple> 正常绑定)
|
||||
const realDataForm = reactive({
|
||||
stationId: "",
|
||||
// observeDate: Date,
|
||||
// startTime: "",
|
||||
// endTime:""
|
||||
});
|
||||
|
||||
// 2. 使用 computed 动态派生出“用于请求的 dataForm”
|
||||
// 只要 realDataForm 发生变化,这个 computed 就会自动重新计算
|
||||
const computedDataForm = computed(() => {
|
||||
const params = { ...realDataForm };
|
||||
|
||||
// 核心转换逻辑:如果 stationId 是数组,就 join 成字符串
|
||||
if (Array.isArray(params.stationId)) {
|
||||
params.stationId = params.stationId.join(",");
|
||||
}
|
||||
// 处理时间区间:将数组拆分为两个独立字段
|
||||
if (Array.isArray(params.observeDate) && params.observeDate.length === 2) {
|
||||
params.startTime = params.observeDate[0];
|
||||
params.endTime = params.observeDate[1];
|
||||
} else {
|
||||
params.startTime = "";
|
||||
params.endTime = "";
|
||||
}
|
||||
|
||||
return params;
|
||||
});
|
||||
|
||||
const view = reactive({
|
||||
deleteIsBatch: true,
|
||||
getDataListURL: "/dailyweather/weatherdailydata/page",
|
||||
getDataListIsPage: true,
|
||||
exportURL: "/dailyweather/weatherdailydata/export",
|
||||
deleteURL: "/dailyweather/weatherdailydata",
|
||||
dataForm: computedDataForm
|
||||
});
|
||||
const state = reactive({ ...useView(view), ...toRefs(view) });
|
||||
|
||||
const getDefaultColumns = () => {
|
||||
const defaultCols: string[] = [];
|
||||
columnGroups.forEach((g) => {
|
||||
g.columns.forEach((c) => {
|
||||
if (c.default) defaultCols.push(c.prop);
|
||||
});
|
||||
});
|
||||
return defaultCols;
|
||||
};
|
||||
|
||||
const selectedColumns = ref<string[]>(getDefaultColumns());
|
||||
|
||||
watch(
|
||||
selectedColumns,
|
||||
(newVal) => {
|
||||
localStorage.setItem("weather_column_pref", JSON.stringify(newVal));
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
const cache = localStorage.getItem("weather_column_pref");
|
||||
if (cache) {
|
||||
selectedColumns.value = JSON.parse(cache);
|
||||
}
|
||||
});
|
||||
|
||||
const resetColumns = () => {
|
||||
selectedColumns.value = getDefaultColumns();
|
||||
};
|
||||
|
||||
const addOrUpdateRef = ref();
|
||||
const addOrUpdateHandle = (id?: number) => {
|
||||
addOrUpdateRef.value.init(id);
|
||||
};
|
||||
|
||||
const importExcelRef = ref();
|
||||
const importHandle = () => {
|
||||
importExcelRef.value.init();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.column-setting-container {
|
||||
padding: 5px;
|
||||
}
|
||||
.group-title {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
color: #409eff;
|
||||
}
|
||||
.column-group {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
:deep(.el-checkbox) {
|
||||
margin-right: 15px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import { IObject } from "@/types/interface";
|
||||
import { defineComponent } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
interface ITip {
|
||||
title: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { to } = route.query;
|
||||
const tips: IObject = {
|
||||
404: {
|
||||
title: "404",
|
||||
message: "访问页面不存在"
|
||||
},
|
||||
error: {
|
||||
title: "错误",
|
||||
message: "访问出错了"
|
||||
}
|
||||
};
|
||||
const tip: ITip = tips[to?.toString() ?? "error"];
|
||||
const onBack = () => {
|
||||
router.back();
|
||||
};
|
||||
const onToHome = () => {
|
||||
router.replace("/");
|
||||
};
|
||||
return { tip, onBack, onToHome };
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rr-error">
|
||||
<el-result>
|
||||
<template v-slot:icon>
|
||||
<el-icon style="font-size: 64px; color: #f5222d"><warning /></el-icon>
|
||||
</template>
|
||||
<template v-slot:title>
|
||||
<span style="font-size: 48px; font-weight: 800">{{ tip.title }}</span>
|
||||
</template>
|
||||
<template v-slot:subTitle>
|
||||
<span style="font-size: 36px">{{ tip.message }}</span>
|
||||
</template>
|
||||
<template v-slot:extra>
|
||||
<el-space :size="30">
|
||||
<el-button type="info" plain @click="onBack"> 返回 </el-button>
|
||||
<el-button type="primary" @click="onToHome"> 主页 </el-button>
|
||||
</el-space>
|
||||
</template>
|
||||
</el-result>
|
||||
</div>
|
||||
</template>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div v-loading="loading">
|
||||
<iframe class="iframe" :src="url" @load="load"></iframe>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, ref, watch } from "vue";
|
||||
import { RouteLocationNormalizedLoaded, useRoute } from "vue-router";
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const route = useRoute();
|
||||
const url = ref("about:_blank");
|
||||
const loading = ref(false);
|
||||
|
||||
watch(
|
||||
() => route,
|
||||
(vl) => {
|
||||
if (vl.path === "/iframe") {
|
||||
setUrl(vl);
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
setUrl(route);
|
||||
});
|
||||
const setUrl = (route: RouteLocationNormalizedLoaded): void => {
|
||||
const { meta, query } = route;
|
||||
loading.value = true;
|
||||
if (query.url) {
|
||||
url.value = query.url as string;
|
||||
} else {
|
||||
if (meta && meta.isIframe) {
|
||||
url.value = meta.url as string;
|
||||
}
|
||||
}
|
||||
};
|
||||
const load = () => {
|
||||
loading.value = false;
|
||||
};
|
||||
return { url, loading, load };
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.iframe {
|
||||
min-height: calc(100vh - 130px);
|
||||
width: 100%;
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="!dataForm.id ? '新增' : '修改'" :close-on-click-modal="false" :close-on-press-escape="false">
|
||||
<el-form :model="dataForm" :rules="rules" ref="dataFormRef" @keyup.enter="dataFormSubmitHandle()" label-width="120px">
|
||||
<el-form-item prop="beanName" label="bean名称">
|
||||
<el-input v-model="dataForm.beanName" placeholder="spring bean名称, 如: testTask"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="params" label="参数">
|
||||
<el-input v-model="dataForm.params" placeholder="参数"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="cronExpression" label="cron表达式">
|
||||
<el-input v-model="dataForm.cronExpression" placeholder="如: 0 0 12 * * ?"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="remark" label="备注">
|
||||
<el-input v-model="dataForm.remark" placeholder="备注"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template v-slot:footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="dataFormSubmitHandle()">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from "vue";
|
||||
import baseService from "@/service/baseService";
|
||||
import { ElMessage } from "element-plus";
|
||||
const emit = defineEmits(["refreshDataList"]);
|
||||
|
||||
const visible = ref(false);
|
||||
const dataFormRef = ref();
|
||||
|
||||
const dataForm = reactive({
|
||||
id: "",
|
||||
beanName: "",
|
||||
params: "",
|
||||
cronExpression: "",
|
||||
remark: "",
|
||||
status: 0
|
||||
});
|
||||
|
||||
const rules = ref({
|
||||
beanName: [{ required: true, message: "必填项不能为空", trigger: "blur" }],
|
||||
cronExpression: [{ required: true, message: "必填项不能为空", trigger: "blur" }]
|
||||
});
|
||||
|
||||
const init = (id?: number) => {
|
||||
visible.value = true;
|
||||
dataForm.id = "";
|
||||
|
||||
// 重置表单数据
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
}
|
||||
|
||||
if (id) {
|
||||
getInfo(id);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取信息
|
||||
const getInfo = (id: number) => {
|
||||
baseService.get(`/sys/schedule/${id}`).then((res) => {
|
||||
Object.assign(dataForm, res.data);
|
||||
});
|
||||
};
|
||||
|
||||
// 表单提交
|
||||
const dataFormSubmitHandle = () => {
|
||||
dataFormRef.value.validate((valid: boolean) => {
|
||||
if (!valid) {
|
||||
return false;
|
||||
}
|
||||
const fn = !dataForm.id ? baseService.post("/sys/schedule", dataForm) : baseService.put("/sys/schedule", dataForm);
|
||||
fn.then((res) => {
|
||||
ElMessage.success({
|
||||
message: "成功",
|
||||
duration: 500,
|
||||
onClose: () => {
|
||||
visible.value = false;
|
||||
emit("refreshDataList");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
init
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.schedule-cron {
|
||||
&-popover {
|
||||
width: auto !important;
|
||||
min-width: 550px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="日志列表" :close-on-click-modal="false" :close-on-press-escape="false" width="75%">
|
||||
<el-form :inline="true" :model="state.dataForm" @keyup.enter="state.getDataList()">
|
||||
<el-form-item>
|
||||
<el-input v-model="state.dataForm.jobId" placeholder="任务ID" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="state.getDataList()">查询</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table v-loading="state.dataListLoading" :data="state.dataList" border @sort-change="state.dataListSortChangeHandle" height="460" style="width: 100%">
|
||||
<el-table-column prop="jobId" label="任务ID" header-align="center" align="center" width="180"></el-table-column>
|
||||
<el-table-column prop="beanName" label="bean名称" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="params" label="参数" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="status" label="状态" header-align="center" align="center">
|
||||
<template v-slot="scope">
|
||||
<el-tag v-if="scope.row.status === 1" size="small">成功</el-tag>
|
||||
<el-tag v-else type="danger" size="small" @click="showErrorInfo(scope.row.id)" style="cursor: pointer">失败</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="times" label="耗时(单位: 毫秒)" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="createDate" label="执行时间" header-align="center" align="center" width="180"></el-table-column>
|
||||
</el-table>
|
||||
<el-pagination :current-page="state.page" :page-sizes="[10, 20, 50, 100]" :page-size="state.limit" :total="state.total" layout="total, sizes, prev, pager, next, jumper" @size-change="state.pageSizeChangeHandle" @current-change="state.pageCurrentChangeHandle"> </el-pagination>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useView from "@/hooks/useView";
|
||||
import { reactive, ref, toRefs } from "vue";
|
||||
import baseService from "@/service/baseService";
|
||||
import { ElMessageBox } from "element-plus";
|
||||
|
||||
const visible = ref(false);
|
||||
|
||||
const view = reactive({
|
||||
getDataListURL: "/sys/scheduleLog/page",
|
||||
getDataListIsPage: true,
|
||||
dataForm: {
|
||||
jobId: ""
|
||||
}
|
||||
});
|
||||
|
||||
const state = reactive({ ...useView(view), ...toRefs(view) });
|
||||
|
||||
const init = () => {
|
||||
visible.value = true;
|
||||
state.getDataList();
|
||||
};
|
||||
|
||||
// 失败信息
|
||||
const showErrorInfo = (id: string) => {
|
||||
baseService.get(`/sys/scheduleLog/${id}`).then((res) => {
|
||||
ElMessageBox.alert(res.data.error);
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
init
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,182 @@
|
||||
<template>
|
||||
<div class="mod-job__schedule">
|
||||
<el-form :inline="true" :model="state.dataForm" @keyup.enter="state.getDataList()">
|
||||
<el-form-item>
|
||||
<el-input v-model="state.dataForm.beanName" placeholder="bean名称" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="state.getDataList()">查询</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button v-if="state.hasPermission('sys:schedule:save')" type="primary" @click="addOrUpdateHandle()">新增</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button v-if="state.hasPermission('sys:schedule:delete')" type="danger" @click="state.deleteHandle()">删除</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button v-if="state.hasPermission('sys:schedule:pause')" type="danger" @click="pauseHandle()">暂停</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button v-if="state.hasPermission('sys:schedule:resume')" type="danger" @click="resumeHandle()">恢复</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button v-if="state.hasPermission('sys:schedule:run')" type="danger" @click="runHandle()">执行</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button v-if="state.hasPermission('sys:schedule:log')" type="success" @click="logHandle()">日志列表</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table v-loading="state.dataListLoading" :data="state.dataList" border @selection-change="state.dataListSelectionChangeHandle" @sort-change="state.dataListSortChangeHandle" style="width: 100%">
|
||||
<el-table-column type="selection" header-align="center" align="center" width="50"></el-table-column>
|
||||
<el-table-column prop="beanName" label="bean名称" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="params" label="参数" show-overflow-tooltip header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="cronExpression" label="cron表达式" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="remark" label="备注" show-overflow-tooltip header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="status" label="状态" sortable="custom" header-align="center" align="center">
|
||||
<template v-slot="scope">
|
||||
<el-tag v-if="scope.row.status === 1" size="small">正常</el-tag>
|
||||
<el-tag v-else size="small" type="danger">暂停</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" fixed="right" header-align="center" align="center" width="248">
|
||||
<template v-slot="scope">
|
||||
<el-button v-if="state.hasPermission('sys:schedule:update')" type="primary" link @click="addOrUpdateHandle(scope.row.id)">修改</el-button>
|
||||
<el-button v-if="state.hasPermission('sys:schedule:pause')" type="primary" link @click="pauseHandle(scope.row.id)">暂停</el-button>
|
||||
<el-button v-if="state.hasPermission('sys:schedule:resume')" type="primary" link @click="resumeHandle(scope.row.id)">恢复</el-button>
|
||||
<el-button v-if="state.hasPermission('sys:schedule:run')" type="primary" link @click="runHandle(scope.row.id)">执行</el-button>
|
||||
<el-button v-if="state.hasPermission('sys:schedule:delete')" type="primary" link @click="state.deleteHandle(scope.row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination :current-page="state.page" :page-sizes="[10, 20, 50, 100]" :page-size="state.limit" :total="state.total" layout="total, sizes, prev, pager, next, jumper" @size-change="state.pageSizeChangeHandle" @current-change="state.pageCurrentChangeHandle"> </el-pagination>
|
||||
<!-- 弹窗, 新增 / 修改 -->
|
||||
<add-or-update ref="addOrUpdateRef" @refreshDataList="state.getDataList"></add-or-update>
|
||||
<!-- 弹窗, 日志列表 -->
|
||||
<log ref="logRef"></log>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useView from "@/hooks/useView";
|
||||
import { reactive, ref, toRefs } from "vue";
|
||||
import baseService from "@/service/baseService";
|
||||
import { IObject } from "@/types/interface";
|
||||
import AddOrUpdate from "./schedule-add-or-update.vue";
|
||||
import Log from "./schedule-log.vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
const emit = defineEmits(["refreshDataList"]);
|
||||
|
||||
const logRef = ref();
|
||||
|
||||
const view = reactive({
|
||||
getDataListURL: "/sys/schedule/page",
|
||||
getDataListIsPage: true,
|
||||
deleteURL: "/sys/schedule",
|
||||
deleteIsBatch: true,
|
||||
dataForm: {
|
||||
beanName: ""
|
||||
}
|
||||
});
|
||||
|
||||
const state = reactive({ ...useView(view), ...toRefs(view) });
|
||||
|
||||
// 暂停
|
||||
const pauseHandle = (id?: string) => {
|
||||
if (!id && state.dataListSelections && state.dataListSelections.length <= 0) {
|
||||
return ElMessage({
|
||||
message: "请选择操作项",
|
||||
type: "warning",
|
||||
duration: 500
|
||||
});
|
||||
}
|
||||
ElMessageBox.confirm("确定进行[暂停]操作?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
})
|
||||
.then(() => {
|
||||
baseService.put("/sys/schedule/pause", id ? [id] : state.dataListSelections && state.dataListSelections.map((item: IObject) => item.id)).then((res) => {
|
||||
ElMessage.success({
|
||||
message: "成功",
|
||||
duration: 500,
|
||||
onClose: () => {
|
||||
state.getDataList();
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
//
|
||||
});
|
||||
};
|
||||
|
||||
// 恢复
|
||||
const resumeHandle = (id?: string) => {
|
||||
if (!id && state.dataListSelections && state.dataListSelections.length <= 0) {
|
||||
return ElMessage({
|
||||
message: "请选择操作项",
|
||||
type: "warning",
|
||||
duration: 500
|
||||
});
|
||||
}
|
||||
ElMessageBox.confirm("确定进行[恢复]操作?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
})
|
||||
.then(() => {
|
||||
baseService.put("/sys/schedule/resume", id ? [id] : state.dataListSelections && state.dataListSelections.map((item: IObject) => item.id)).then((res) => {
|
||||
ElMessage.success({
|
||||
message: "成功",
|
||||
duration: 500,
|
||||
onClose: () => {
|
||||
state.getDataList();
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
//
|
||||
});
|
||||
};
|
||||
|
||||
// 执行
|
||||
const runHandle = (id?: string) => {
|
||||
if (!id && state.dataListSelections && state.dataListSelections.length <= 0) {
|
||||
return ElMessage({
|
||||
message: "请选择操作项",
|
||||
type: "warning",
|
||||
duration: 500
|
||||
});
|
||||
}
|
||||
ElMessageBox.confirm("确定进行[执行]操作?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
})
|
||||
.then(() => {
|
||||
baseService.put("/sys/schedule/run", id ? [id] : state.dataListSelections && state.dataListSelections.map((item: IObject) => item.id)).then((res) => {
|
||||
ElMessage.success({
|
||||
message: "成功",
|
||||
duration: 500,
|
||||
onClose: () => {
|
||||
state.getDataList();
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
//
|
||||
});
|
||||
};
|
||||
|
||||
// 日志列表
|
||||
const logHandle = () => {
|
||||
logRef.value.init();
|
||||
};
|
||||
|
||||
const addOrUpdateRef = ref();
|
||||
const addOrUpdateHandle = (id?: number) => {
|
||||
addOrUpdateRef.value.init(id);
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<div class="rr-login">
|
||||
<div class="rr-login-wrap">
|
||||
<div class="rr-login-left hidden-sm-and-down">
|
||||
<p class="rr-login-left-title">气象数据管理系统</p>
|
||||
</div>
|
||||
|
||||
<div class="rr-login-right">
|
||||
<div class="rr-login-right-main">
|
||||
<h4 class="rr-login-right-main-title">登录</h4>
|
||||
<el-form ref="formRef" label-width="80px" :status-icon="true" :model="login" :rules="rules" @keyup.enter="onLogin">
|
||||
<el-form-item label-width="0" prop="username">
|
||||
<el-input v-model="login.username" placeholder="用户名" prefix-icon="user" autocomplete="off"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label-width="0" prop="password">
|
||||
<el-input placeholder="密码" v-model="login.password" prefix-icon="lock" autocomplete="off" show-password></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label-width="0" prop="captcha">
|
||||
<el-space class="rr-login-right-main-code">
|
||||
<el-input v-model="login.captcha" placeholder="验证码" prefix-icon="first-aid-kit"></el-input>
|
||||
<img style="vertical-align: middle; height: 40px; cursor: pointer" :src="state.captchaUrl" @click="onRefreshCode" alt="" />
|
||||
</el-space>
|
||||
</el-form-item>
|
||||
<el-form-item label-width="0">
|
||||
<el-button type="primary" size="small" :disabled="state.loading" @click="onLogin" class="rr-login-right-main-btn"> 登录 </el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="login-footer">
|
||||
<p></p>
|
||||
<p></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { CacheToken } from "@/constants/cacheKey";
|
||||
import baseService from "@/service/baseService";
|
||||
import { setCache } from "@/utils/cache";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { getUuid, isAlphanumeric } from "@/utils/utils";
|
||||
import app from "@/constants/app";
|
||||
import SvgIcon from "@/components/base/svg-icon/index";
|
||||
import { useAppStore } from "@/store";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const store = useAppStore();
|
||||
const router = useRouter();
|
||||
|
||||
const state = reactive({
|
||||
captchaUrl: "",
|
||||
loading: false,
|
||||
year: new Date().getFullYear()
|
||||
});
|
||||
|
||||
const login = reactive({ username: "", password: "", captcha: "", uuid: "" });
|
||||
|
||||
onMounted(() => {
|
||||
//清理数据
|
||||
store.logout();
|
||||
getCaptchaUrl();
|
||||
});
|
||||
const formRef = ref();
|
||||
|
||||
const validateUsername = (rule: any, value: string, callback: (e?: Error) => any): any => {
|
||||
if (value && !isAlphanumeric(value)) {
|
||||
return callback(new Error("用户名只能为英文或数字组合"));
|
||||
}
|
||||
callback();
|
||||
};
|
||||
const rules = ref({
|
||||
username: [
|
||||
{ required: true, message: "必填项不能为空", trigger: "blur" },
|
||||
{ validator: validateUsername, trigger: "blur" }
|
||||
],
|
||||
password: [{ required: true, message: "必填项不能为空", trigger: "blur" }],
|
||||
captcha: [{ required: true, message: "必填项不能为空", trigger: "blur" }]
|
||||
});
|
||||
|
||||
const getCaptchaUrl = () => {
|
||||
login.uuid = getUuid();
|
||||
state.captchaUrl = `${app.api}/captcha?uuid=${login.uuid}`;
|
||||
};
|
||||
|
||||
const onRefreshCode = () => {
|
||||
getCaptchaUrl();
|
||||
};
|
||||
|
||||
const onLogin = () => {
|
||||
formRef.value.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
state.loading = true;
|
||||
baseService
|
||||
.post("/login", login)
|
||||
.then((res) => {
|
||||
state.loading = false;
|
||||
if (res.code === 0) {
|
||||
setCache(CacheToken, res.data, true);
|
||||
ElMessage.success("登录成功");
|
||||
router.push("/");
|
||||
} else {
|
||||
ElMessage.error(res.msg);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
state.loading = false;
|
||||
onRefreshCode();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import url("@/assets/theme/base.less");
|
||||
.rr-login {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: #019ec4;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.rr-login-wrap {
|
||||
width: 96% !important;
|
||||
}
|
||||
.rr-login-right {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
&-wrap {
|
||||
margin: 0 auto;
|
||||
width: 1000px;
|
||||
box-shadow: -4px 5px 10px rgba(0, 0, 0, 0.4);
|
||||
animation-duration: 1s;
|
||||
animation-fill-mode: both;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&-left {
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
background-color: @--color-primary;
|
||||
color: #fff;
|
||||
float: left;
|
||||
width: 50%;
|
||||
|
||||
&-title {
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-weight: 300;
|
||||
letter-spacing: 2px;
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
&-right {
|
||||
border-left: none;
|
||||
color: #fff;
|
||||
background-color: #fff;
|
||||
width: 50%;
|
||||
float: left;
|
||||
|
||||
&-main {
|
||||
margin: 0 auto;
|
||||
width: 65%;
|
||||
&-title {
|
||||
color: #333;
|
||||
margin-bottom: 40px;
|
||||
font-weight: 500;
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
|
||||
&-lang .iconfont {
|
||||
font-size: 20px;
|
||||
color: #606266;
|
||||
font-weight: 800;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
border-width: 0;
|
||||
border-radius: 0;
|
||||
border-bottom: 1px solid #dcdfe6;
|
||||
}
|
||||
|
||||
&-code {
|
||||
width: 100%;
|
||||
.el-space__item:first-child {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
&-btn {
|
||||
width: 100%;
|
||||
height: 45px;
|
||||
font-size: 18px !important;
|
||||
letter-spacing: 2px;
|
||||
font-weight: 300 !important;
|
||||
cursor: pointer;
|
||||
margin-top: 30px;
|
||||
font-family: neo, sans-serif;
|
||||
transition: 0.25s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
padding: 20px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
p {
|
||||
margin: 10px 0;
|
||||
}
|
||||
a {
|
||||
padding: 0 5px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-left,
|
||||
&-right {
|
||||
position: relative;
|
||||
min-height: 500px;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@keyframes animate-down {
|
||||
0%,
|
||||
60%,
|
||||
75%,
|
||||
90%,
|
||||
to {
|
||||
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
|
||||
}
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, -3000px, 0);
|
||||
}
|
||||
60% {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 25px, 0);
|
||||
}
|
||||
75% {
|
||||
transform: translate3d(0, -10px, 0);
|
||||
}
|
||||
90% {
|
||||
transform: translate3d(0, 5px, 0);
|
||||
}
|
||||
to {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-down {
|
||||
animation-name: animate-down;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="云存储配置" :close-on-click-modal="false" :close-on-press-escape="false">
|
||||
<el-form :model="dataForm" :rules="rules" ref="dataFormRef" @keyup.enter="dataFormSubmitHandle()" label-width="140px">
|
||||
<el-form-item label="类型">
|
||||
<el-radio-group v-model="dataForm.type">
|
||||
<el-radio :label="1">七牛</el-radio>
|
||||
<el-radio :label="2">阿里云</el-radio>
|
||||
<el-radio :label="3">腾讯云</el-radio>
|
||||
<el-radio :label="4">自定义</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<template v-if="dataForm.type === 1">
|
||||
<el-form-item prop="qiniuDomain" label="域名">
|
||||
<el-input v-model="dataForm.qiniuDomain" placeholder="七牛绑定的域名"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="qiniuPrefix" label="路径前缀">
|
||||
<el-input v-model="dataForm.qiniuPrefix" placeholder="不设置默认为空"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="qiniuAccessKey" label="AccessKey">
|
||||
<el-input v-model="dataForm.qiniuAccessKey" placeholder="七牛AccessKey"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="qiniuSecretKey" label="SecretKey">
|
||||
<el-input v-model="dataForm.qiniuSecretKey" placeholder="七牛SecretKey"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="qiniuBucketName" label="空间名">
|
||||
<el-input v-model="dataForm.qiniuBucketName" placeholder="七牛存储空间名"></el-input>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<template v-else-if="dataForm.type === 2">
|
||||
<el-form-item prop="aliyunDomain" label="域名">
|
||||
<el-input v-model="dataForm.aliyunDomain" placeholder="阿里云绑定的域名,如:http://cdn.123.io"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="aliyunPrefix" label="路径前缀">
|
||||
<el-input v-model="dataForm.aliyunPrefix" placeholder="不设置默认为空"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="aliyunEndPoint" label="EndPoint">
|
||||
<el-input v-model="dataForm.aliyunEndPoint" placeholder="阿里云EndPoint"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="aliyunAccessKeyId" label="AccessKeyId">
|
||||
<el-input v-model="dataForm.aliyunAccessKeyId" placeholder="阿里云AccessKeyId"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="aliyunAccessKeySecret" label="AccessKeySecret">
|
||||
<el-input v-model="dataForm.aliyunAccessKeySecret" placeholder="阿里云AccessKeySecret"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="aliyunBucketName" label="BucketName">
|
||||
<el-input v-model="dataForm.aliyunBucketName" placeholder="阿里云BucketName"></el-input>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<template v-else-if="dataForm.type === 3">
|
||||
<el-form-item prop="qcloudDomain" label="域名">
|
||||
<el-input v-model="dataForm.qcloudDomain" placeholder="腾讯云绑定的域名"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="qcloudPrefix" label="路径前缀">
|
||||
<el-input v-model="dataForm.qcloudPrefix" placeholder="不设置默认为空"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="qcloudAppId" label="AppId">
|
||||
<el-input v-model="dataForm.qcloudAppId" placeholder="腾讯云AppId"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="qcloudSecretId" label="SecretId">
|
||||
<el-input v-model="dataForm.qcloudSecretId" placeholder="腾讯云SecretId"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="qcloudSecretKey" label="SecretKey">
|
||||
<el-input v-model="dataForm.qcloudSecretKey" placeholder="腾讯云SecretKey"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="qcloudBucketName" label="BucketName">
|
||||
<el-input v-model="dataForm.qcloudBucketName" placeholder="腾讯云BucketName"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="qcloudRegion" label="所属地区">
|
||||
<el-select v-model="dataForm.qcloudRegion" clearable placeholder="请选择" class="w-percent-100">
|
||||
<el-option value="ap-beijing-1" label="北京一区(华北)"></el-option>
|
||||
<el-option value="ap-beijing" label="北京"></el-option>
|
||||
<el-option value="ap-shanghai" label="上海(华东)"></el-option>
|
||||
<el-option value="ap-guangzhou" label="广州(华南)"></el-option>
|
||||
<el-option value="ap-chengdu" label="成都(西南)"></el-option>
|
||||
<el-option value="ap-chongqing" label="重庆"></el-option>
|
||||
<el-option value="ap-singapore" label="新加坡"></el-option>
|
||||
<el-option value="ap-hongkong" label="香港"></el-option>
|
||||
<el-option value="na-toronto" label="多伦多"></el-option>
|
||||
<el-option value="eu-frankfurt" label="法兰克福"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<template v-else-if="dataForm.type === 4">
|
||||
<el-form-item prop="aliyunDomain" label="域名">
|
||||
<el-input v-model="dataForm.aliyunDomain" placeholder="阿里云绑定的域名,如:http://cdn.123.io"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="aliyunPrefix" label="路径前缀">
|
||||
<el-input v-model="dataForm.aliyunPrefix" placeholder="不设置默认为空"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="aliyunEndPoint" label="EndPoint">
|
||||
<el-input v-model="dataForm.aliyunEndPoint" placeholder="阿里云EndPoint"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="aliyunAccessKeyId" label="AccessKeyId">
|
||||
<el-input v-model="dataForm.aliyunAccessKeyId" placeholder="阿里云AccessKeyId"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="aliyunAccessKeySecret" label="AccessKeySecret">
|
||||
<el-input v-model="dataForm.aliyunAccessKeySecret" placeholder="阿里云AccessKeySecret"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="aliyunBucketName" label="BucketName">
|
||||
<el-input v-model="dataForm.aliyunBucketName" placeholder="阿里云BucketName"></el-input>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
<template v-slot:footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="dataFormSubmitHandle()">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from "vue";
|
||||
import baseService from "@/service/baseService";
|
||||
import { ElMessage } from "element-plus";
|
||||
const emit = defineEmits(["refreshDataList"]);
|
||||
|
||||
const visible = ref(false);
|
||||
const dataFormRef = ref();
|
||||
|
||||
const dataForm = reactive({
|
||||
type: 0,
|
||||
qiniuDomain: "",
|
||||
qiniuPrefix: "",
|
||||
qiniuAccessKey: "",
|
||||
qiniuSecretKey: "",
|
||||
qiniuBucketName: "",
|
||||
aliyunDomain: "",
|
||||
aliyunPrefix: "",
|
||||
aliyunEndPoint: "",
|
||||
aliyunAccessKeyId: "",
|
||||
aliyunAccessKeySecret: "",
|
||||
aliyunBucketName: "",
|
||||
qcloudDomain: "",
|
||||
qcloudPrefix: "",
|
||||
qcloudAppId: 0,
|
||||
qcloudSecretId: "",
|
||||
qcloudSecretKey: "",
|
||||
qcloudBucketName: "",
|
||||
qcloudRegion: ""
|
||||
});
|
||||
|
||||
const rules = ref({
|
||||
qiniuDomain: [{ required: true, message: "必填项不能为空", trigger: "blur" }],
|
||||
qiniuAccessKey: [{ required: true, message: "必填项不能为空", trigger: "blur" }],
|
||||
qiniuSecretKey: [{ required: true, message: "必填项不能为空", trigger: "blur" }],
|
||||
qiniuBucketName: [{ required: true, message: "必填项不能为空", trigger: "blur" }],
|
||||
aliyunDomain: [{ required: true, message: "必填项不能为空", trigger: "blur" }],
|
||||
aliyunEndPoint: [{ required: true, message: "必填项不能为空", trigger: "blur" }],
|
||||
aliyunAccessKeyId: [{ required: true, message: "必填项不能为空", trigger: "blur" }],
|
||||
aliyunAccessKeySecret: [{ required: true, message: "必填项不能为空", trigger: "blur" }],
|
||||
aliyunBucketName: [{ required: true, message: "必填项不能为空", trigger: "blur" }],
|
||||
qcloudDomain: [{ required: true, message: "必填项不能为空", trigger: "blur" }],
|
||||
qcloudAppId: [{ required: true, message: "必填项不能为空", trigger: "blur" }],
|
||||
qcloudSecretId: [{ required: true, message: "必填项不能为空", trigger: "blur" }],
|
||||
qcloudSecretKey: [{ required: true, message: "必填项不能为空", trigger: "blur" }],
|
||||
qcloudBucketName: [{ required: true, message: "必填项不能为空", trigger: "blur" }],
|
||||
qcloudRegion: [{ required: true, message: "必填项不能为空", trigger: "blur" }]
|
||||
});
|
||||
|
||||
const init = () => {
|
||||
visible.value = true;
|
||||
|
||||
// 重置表单数据
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
}
|
||||
|
||||
getInfo();
|
||||
};
|
||||
|
||||
// 获取信息
|
||||
const getInfo = () => {
|
||||
baseService.get("/sys/oss/info").then((res) => {
|
||||
Object.assign(dataForm, res.data);
|
||||
});
|
||||
};
|
||||
|
||||
// 表单提交
|
||||
const dataFormSubmitHandle = () => {
|
||||
dataFormRef.value.validate((valid: boolean) => {
|
||||
if (!valid) {
|
||||
return false;
|
||||
}
|
||||
baseService.post("/sys/oss", dataForm).then((res) => {
|
||||
ElMessage.success({
|
||||
message: "成功",
|
||||
duration: 500,
|
||||
onClose: () => {
|
||||
visible.value = false;
|
||||
emit("refreshDataList");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
init
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="上传文件" :close-on-click-modal="false" :close-on-press-escape="false">
|
||||
<el-upload :action="url" :file-list="fileList" drag multiple :before-upload="beforeUploadHandle" :on-success="successHandle" class="text-center">
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">将文件拖到此处,或点击上传</div>
|
||||
<template v-slot:tip>
|
||||
<div class="el-upload__tip">只支持jpg、png、gif格式文件!</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
import { getToken } from "@/utils/cache";
|
||||
import { IObject } from "@/types/interface";
|
||||
import app from "@/constants/app";
|
||||
import { ElMessage } from "element-plus";
|
||||
const emit = defineEmits(["refreshDataList"]);
|
||||
|
||||
const visible = ref(false);
|
||||
const url = ref("");
|
||||
const num = ref(0);
|
||||
const fileList = ref<IObject[]>();
|
||||
|
||||
const init = () => {
|
||||
visible.value = true;
|
||||
url.value = `${app.api}/sys/oss/upload?token=${getToken()}`;
|
||||
num.value = 0;
|
||||
fileList.value = [];
|
||||
};
|
||||
|
||||
// 上传之前
|
||||
const beforeUploadHandle = (file: IObject) => {
|
||||
if (file.type !== "image/jpg" && file.type !== "image/jpeg" && file.type !== "image/png" && file.type !== "image/gif") {
|
||||
ElMessage.error("只支持jpg、png、gif格式文件!");
|
||||
return false;
|
||||
}
|
||||
num.value++;
|
||||
};
|
||||
|
||||
// 上传成功
|
||||
const successHandle = (res: IObject, file: IObject, list: IObject[]) => {
|
||||
if (res.code !== 0) {
|
||||
return ElMessage.error(res.msg);
|
||||
}
|
||||
|
||||
fileList.value = list;
|
||||
num.value--;
|
||||
if (num.value === 0) {
|
||||
ElMessage.success({
|
||||
message: "成功",
|
||||
duration: 500,
|
||||
onClose: () => {
|
||||
visible.value = false;
|
||||
emit("refreshDataList");
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
init
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div class="mod-oss__oss">
|
||||
<el-form :inline="true" :model="state.dataForm">
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="configHandle()">云存储配置</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="uploadHandle()">上传文件</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="danger" @click="state.deleteHandle()">删除</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table v-loading="state.dataListLoading" :data="state.dataList" border @selection-change="state.dataListSelectionChangeHandle" @sort-change="state.dataListSortChangeHandle" style="width: 100%">
|
||||
<el-table-column type="selection" header-align="center" align="center" width="50"></el-table-column>
|
||||
<el-table-column prop="url" label="URL地址" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="createDate" label="创建时间" sortable="custom" header-align="center" align="center" width="180"></el-table-column>
|
||||
<el-table-column label="操作" fixed="right" header-align="center" align="center" width="150">
|
||||
<template v-slot="scope">
|
||||
<el-button type="primary" link @click="state.deleteHandle(scope.row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination :current-page="state.page" :page-sizes="[10, 20, 50, 100]" :page-size="state.limit" :total="state.total" layout="total, sizes, prev, pager, next, jumper" @size-change="state.pageSizeChangeHandle" @current-change="state.pageCurrentChangeHandle"> </el-pagination>
|
||||
<!-- 弹窗, 云存储配置 -->
|
||||
<config v-if="state.configVisible" ref="configRef"></config>
|
||||
<!-- 弹窗, 上传文件 -->
|
||||
<upload v-if="state.uploadVisible" ref="uploadRef" @refreshDataList="state.getDataList"></upload>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useView from "@/hooks/useView";
|
||||
import { nextTick, reactive, ref, toRefs } from "vue";
|
||||
import Config from "./oss-config.vue";
|
||||
import Upload from "./oss-upload.vue";
|
||||
|
||||
const configRef = ref();
|
||||
const uploadRef = ref();
|
||||
|
||||
const view = reactive({
|
||||
getDataListURL: "/sys/oss/page",
|
||||
getDataListIsPage: true,
|
||||
deleteURL: "/sys/oss",
|
||||
deleteIsBatch: true,
|
||||
dataForm: {},
|
||||
configVisible: false,
|
||||
uploadVisible: false
|
||||
});
|
||||
|
||||
const state = reactive({ ...useView(view), ...toRefs(view) });
|
||||
|
||||
// 云存储配置
|
||||
const configHandle = () => {
|
||||
state.configVisible = true;
|
||||
nextTick(() => {
|
||||
configRef.value.init();
|
||||
});
|
||||
};
|
||||
|
||||
// 上传文件
|
||||
const uploadHandle = () => {
|
||||
state.uploadVisible = true;
|
||||
nextTick(() => {
|
||||
uploadRef.value.init();
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div class="region-viewer" style="padding: 20px;">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span style="font-weight: bold;">地区数据</span>
|
||||
<el-tag type="info">总数据量: {{ rawListCount }} 条</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 树形表格 -->
|
||||
<el-table
|
||||
v-if="treeData.length > 0"
|
||||
:data="treeData"
|
||||
style="width: 100%; max-height: 600px; overflow: auto;"
|
||||
row-key="id"
|
||||
border
|
||||
:tree-props="{ children: 'children' }"
|
||||
>
|
||||
<el-table-column prop="id" label="编号 (邮编)" width="220" />
|
||||
<el-table-column prop="name" label="地名" />
|
||||
</el-table>
|
||||
|
||||
<el-empty v-else :description="loadingStatus" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
|
||||
const rawList = ref<any[]>([]);
|
||||
const treeData = ref<any[]>([]);
|
||||
const loadingStatus = ref('正在解析数据...');
|
||||
|
||||
const rawListCount = computed(() => rawList.value.length);
|
||||
|
||||
// 优化后的构建树函数
|
||||
const buildTreeMap = (list: any[]) => {
|
||||
const map = new Map<number, any[]>();
|
||||
|
||||
// 构建 parentId -> children 的 Map
|
||||
list.forEach(item => {
|
||||
const pid = item.parentid;
|
||||
if (!map.has(pid)) map.set(pid, []);
|
||||
map.get(pid)!.push({ ...item });
|
||||
});
|
||||
|
||||
// 递归构建树
|
||||
const createTree = (pId: number): any[] => {
|
||||
const children = map.get(pId) || [];
|
||||
children.forEach(child => {
|
||||
const sub = createTree(child.id);
|
||||
if (sub.length > 0) child.children = sub;
|
||||
});
|
||||
return children;
|
||||
};
|
||||
|
||||
// 自动探测根节点
|
||||
const rootParentId = Math.min(...list.map(r => r.parentid));
|
||||
return createTree(rootParentId);
|
||||
};
|
||||
|
||||
const initRegionTree = async () => {
|
||||
try {
|
||||
const res = await fetch("/region.csv");
|
||||
if (!res.ok) throw new Error("无法读取数据,请检查文件");
|
||||
|
||||
const text = await res.text();
|
||||
const lines = text.split("\n").map(l => l.trim()).filter(l => l);
|
||||
if (lines.length < 2) throw new Error("内容为空或格式错误");
|
||||
|
||||
const headers = lines[0].split(",").map(h => h.trim().toLowerCase());
|
||||
|
||||
rawList.value = lines.slice(1).map(line => {
|
||||
const cols = line.split(",").map(c => c.trim());
|
||||
const obj: any = {};
|
||||
headers.forEach((h, i) => {
|
||||
obj[h] = h === 'name' ? cols[i] : Number(cols[i]);
|
||||
});
|
||||
return obj;
|
||||
});
|
||||
|
||||
// 构建树
|
||||
const tree = buildTreeMap(rawList.value);
|
||||
if (tree.length === 0) {
|
||||
loadingStatus.value = '构建失败:根节点下无数据';
|
||||
} else {
|
||||
treeData.value = tree;
|
||||
loadingStatus.value = '加载完成';
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
loadingStatus.value = `错误: ${error.message}`;
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(initRegionTree);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.region-viewer .el-table {
|
||||
/* 虚拟滚动样式 */
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,289 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="!dataForm.id ? '新增气象站' : '修改气象站'"
|
||||
width="720px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form ref="dataFormRef" :model="dataForm" :rules="rules" label-width="110px">
|
||||
<!-- ... 前面经纬度等字段保持不变 ... -->
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="气象站号" prop="stationCode">
|
||||
<el-input v-model="dataForm.stationCode" placeholder="例如 57972"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="站点名称" prop="stationName">
|
||||
<el-input v-model="dataForm.stationName"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="测站级别" prop="stationLevel">
|
||||
<el-select v-model="dataForm.stationLevel" placeholder="选择级别">
|
||||
<el-option label="国家基准气候站(11)" :value="11"/>
|
||||
<el-option label="国家基本气象站(12)" :value="12"/>
|
||||
<el-option label="国家一般气象站(13)" :value="13"/>
|
||||
<el-option label="区域气象站(14)" :value="14"/>
|
||||
<el-option label="其他气象站(15)" :value="15"/>
|
||||
<el-option label="国家站(16)" :value="16"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="海拔高度(m)" prop="elevation">
|
||||
<el-input-number
|
||||
v-model="dataForm.elevation"
|
||||
:precision="1"
|
||||
:step="1"
|
||||
style="width:100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="纬度" prop="latitude">
|
||||
<el-input-number
|
||||
v-model="dataForm.latitude"
|
||||
:precision="6"
|
||||
:step="0.0001"
|
||||
:min="-90"
|
||||
:max="90"
|
||||
style="width:100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="经度" prop="longitude">
|
||||
<el-input-number
|
||||
v-model="dataForm.longitude"
|
||||
:precision="6"
|
||||
:step="0.0001"
|
||||
:min="-180"
|
||||
:max="180"
|
||||
style="width:100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider>行政区域</el-divider>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="省份" prop="provinceName">
|
||||
<el-select v-model="dataForm.provinceName" placeholder="请选择省" clearable @change="onProvinceChange">
|
||||
<el-option
|
||||
v-for="item in provinceOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="String(item.id)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="地市" prop="cityName">
|
||||
<el-select v-model="dataForm.cityName" placeholder="请选择市" clearable @change="onCityChange">
|
||||
<el-option
|
||||
v-for="item in cityOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="String(item.id)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="区县" prop="countyName">
|
||||
<el-select v-model="dataForm.countyName" placeholder="请选择区" clearable>
|
||||
<el-option
|
||||
v-for="item in countyOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="String(item.id)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="乡镇名称">
|
||||
<el-input v-model="dataForm.townName" placeholder="具体乡镇"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider>管理部门</el-divider>
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="选择部门" prop="deptId">
|
||||
<el-tree :data="deptList" :props="{ label: 'name', children: 'children' }" node-key="id" ref="deptTreeRef" accordion highlight-current @node-click="onDeptClick" style="max-height: 240px; overflow-y: auto; width: 100%; border: 1px solid var(--border); border-radius: 6px; padding: 8px"></el-tree>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible=false">取消</el-button>
|
||||
<el-button type="primary" @click="dataFormSubmitHandle">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, computed, nextTick } from "vue";
|
||||
import baseService from "@/service/baseService";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
// 接收父页面传来的 CSV 原始数组和 Map 索引
|
||||
const props = defineProps<{
|
||||
regionsData: any[]
|
||||
regionMap: Map<number, any>
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["refreshDataList"]);
|
||||
|
||||
const visible = ref(false);
|
||||
const dataFormRef = ref();
|
||||
const deptTreeRef = ref();
|
||||
const deptList = ref<any[]>([]);
|
||||
|
||||
const dataForm = reactive({
|
||||
id: "",
|
||||
stationCode: "",
|
||||
stationName: "",
|
||||
stationLevel: "",
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
elevation: null,
|
||||
provinceName: "", // 存放省级 ID 字符串
|
||||
cityName: "", // 存放市级 ID 字符串
|
||||
countyName: "", // 存放区级 ID 字符串
|
||||
townName: "",
|
||||
deptId: ""
|
||||
});
|
||||
|
||||
const rules = {
|
||||
stationCode: [{ required: true, message: "请输入气象站号", trigger: "blur" }],
|
||||
stationName: [{ required: true, message: "请输入站点名称", trigger: "blur" }],
|
||||
stationLevel: [{ required: true, message: "请选择测站级别", trigger: "change" }]
|
||||
};
|
||||
|
||||
// ----------------- 下拉框联动逻辑 -----------------
|
||||
|
||||
// 1. 获取省级列表 (根据你说的:parentId 为 1 是省级)
|
||||
const provinceOptions = computed(() => {
|
||||
const list = props.regionsData.filter(r => Number(r.parentId) === 1);
|
||||
console.log("省级加载检查,匹配数量:", list.length);
|
||||
return list;
|
||||
});
|
||||
|
||||
// 市、区列表:动态变化
|
||||
const cityOptions = ref<any[]>([]);
|
||||
const countyOptions = ref<any[]>([]);
|
||||
|
||||
// 切换省:清空市/区,加载新市列表
|
||||
const onProvinceChange = (val: string) => {
|
||||
dataForm.cityName = "";
|
||||
dataForm.countyName = "";
|
||||
cityOptions.value = [];
|
||||
countyOptions.value = [];
|
||||
|
||||
if (val) {
|
||||
cityOptions.value = props.regionsData.filter(r => Number(r.parentId) === Number(val));
|
||||
}
|
||||
};
|
||||
|
||||
// 切换市:清空区,加载新区列表
|
||||
const onCityChange = (val: string) => {
|
||||
dataForm.countyName = "";
|
||||
countyOptions.value = [];
|
||||
|
||||
if (val) {
|
||||
countyOptions.value = props.regionsData.filter(r => Number(r.parentId) === Number(val));
|
||||
}
|
||||
};
|
||||
|
||||
function getDeptList() {
|
||||
return baseService.get("/sys/dept/list").then((res) => {
|
||||
deptList.value = res.data || [];
|
||||
});
|
||||
}
|
||||
|
||||
function onDeptClick(data: any) {
|
||||
dataForm.deptId = data.id;
|
||||
}
|
||||
|
||||
// ----------------- 弹窗操作 -----------------
|
||||
|
||||
const init = (id?: number) => {
|
||||
visible.value = true;
|
||||
|
||||
// 重置表单和选项
|
||||
cityOptions.value = [];
|
||||
countyOptions.value = [];
|
||||
|
||||
nextTick(() => {
|
||||
if (dataFormRef.value) dataFormRef.value.resetFields();
|
||||
Object.assign(dataForm, {
|
||||
id: "",
|
||||
provinceName: "",
|
||||
cityName: "",
|
||||
countyName: "",
|
||||
townName: "",
|
||||
deptId: ""
|
||||
});
|
||||
|
||||
getDeptList().then(() => {
|
||||
if (id) {
|
||||
getInfo(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 编辑回显
|
||||
const getInfo = async (id: number) => {
|
||||
const res = await baseService.get(`/station/weatherstation/${id}`);
|
||||
const data = res.data;
|
||||
|
||||
// 1. 基础数据赋值
|
||||
Object.assign(dataForm, data);
|
||||
|
||||
// 2. 关键:手动触发联动列表加载,否则 Select 只有 ID 没有数据源,无法显示名字
|
||||
if (data.provinceName) {
|
||||
cityOptions.value = props.regionsData.filter(r => Number(r.parentId) === Number(data.provinceName));
|
||||
}
|
||||
if (data.cityName) {
|
||||
countyOptions.value = props.regionsData.filter(r => Number(r.parentId) === Number(data.cityName));
|
||||
}
|
||||
|
||||
// 3. 回显选择的管理部门
|
||||
if (data.deptId) {
|
||||
nextTick(() => {
|
||||
deptTreeRef.value?.setCurrentKey(data.deptId);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 提交
|
||||
const dataFormSubmitHandle = () => {
|
||||
dataFormRef.value.validate(valid => {
|
||||
if (!valid) return;
|
||||
|
||||
const request = !dataForm.id
|
||||
? baseService.post("/station/weatherstation", dataForm)
|
||||
: baseService.put("/station/weatherstation", dataForm);
|
||||
|
||||
request.then(() => {
|
||||
ElMessage.success("操作成功");
|
||||
visible.value = false;
|
||||
emit("refreshDataList");
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({ init });
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div class="mod-station__weatherstation">
|
||||
<el-form :inline="true" :model="state.dataForm" @keyup.enter="state.getDataList()">
|
||||
<el-form-item>
|
||||
<el-input v-model="state.dataForm.stationName" placeholder="区站名称" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input v-model="state.dataForm.stationCode" placeholder="区站编号" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="state.getDataList()">查询</el-button>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button v-if="state.hasPermission('station:weatherstation:save')" type="primary" @click="addOrUpdateHandle()">新增</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button v-if="state.hasPermission('station:weatherstation:delete')" type="danger" @click="state.deleteHandle()">删除</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table v-loading="state.dataListLoading" :data="state.dataList" border @selection-change="state.dataListSelectionChangeHandle" style="width: 100%">
|
||||
<el-table-column type="selection" header-align="center" align="center" width="50"></el-table-column>
|
||||
<el-table-column prop="id" label="编号" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="stationCode" label="区站号" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="stationName" label="站点" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column label="归属部门" header-align="center" align="center">
|
||||
<template #default="{ row }">{{ deptMap.get(Number(row.deptId)) || "" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="stationLevel" label="测站级别" header-align="center" align="center">
|
||||
<template v-slot="row">
|
||||
{{ state.getDictLabel("station_level", row.row.stationLevel) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="latitude" label="纬度" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="longitude" label="经度" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="elevation" label="测站高度(米)" header-align="center" align="center"></el-table-column>
|
||||
<!-- 使用 map 转换 id -> 中文 -->
|
||||
<el-table-column label="省名" header-align="center" align="center">
|
||||
<template #default="{ row }">{{ regionMap.get(Number(row.provinceName))?.name || "" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="地市名" header-align="center" align="center">
|
||||
<template #default="{ row }">{{ regionMap.get(Number(row.cityName))?.name || "" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="区县名" header-align="center" align="center">
|
||||
<template #default="{ row }">{{ regionMap.get(Number(row.countyName))?.name || "" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="townName" label="乡镇名" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="createDate" label="创建时间" header-align="center" align="center"></el-table-column>
|
||||
|
||||
<el-table-column label="操作" fixed="right" header-align="center" align="center" width="150">
|
||||
<template v-slot="scope">
|
||||
<el-button v-if="state.hasPermission('station:weatherstation:update')" type="primary" link @click="addOrUpdateHandle(scope.row.id)">修改</el-button>
|
||||
<el-button v-if="state.hasPermission('station:weatherstation:delete')" type="primary" link @click="state.deleteHandle(scope.row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination :current-page="state.page" :page-sizes="[10, 20, 50, 100]" :page-size="state.limit" :total="state.total" layout="total, sizes, prev, pager, next, jumper" @size-change="state.pageSizeChangeHandle" @current-change="state.pageCurrentChangeHandle"> </el-pagination>
|
||||
|
||||
<!-- 弹窗, 新增 / 修改 -->
|
||||
<add-or-update ref="addOrUpdateRef" :regions-data="regionsData" :region-map="regionMap" @refreshDataList="state.getDataList"> </add-or-update>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useView from "@/hooks/useView";
|
||||
import { reactive, ref, toRefs, onMounted } from "vue";
|
||||
import AddOrUpdate from "./weatherstation-add-or-update.vue";
|
||||
import baseService from "@/service/baseService";
|
||||
|
||||
// 表格 state
|
||||
const view = reactive({
|
||||
deleteIsBatch: true,
|
||||
getDataListURL: "/station/weatherstation/page",
|
||||
getDataListIsPage: true,
|
||||
exportURL: "/station/weatherstation/export",
|
||||
deleteURL: "/station/weatherstation",
|
||||
dataForm: {
|
||||
id: "",
|
||||
stationName: "",
|
||||
stationCode: ""
|
||||
}
|
||||
});
|
||||
const state = reactive({
|
||||
...useView(view),
|
||||
...toRefs(view)
|
||||
});
|
||||
|
||||
// 弹窗 ref
|
||||
const addOrUpdateRef = ref();
|
||||
const addOrUpdateHandle = (id?: number) => {
|
||||
addOrUpdateRef.value.init(id);
|
||||
};
|
||||
// const batchInsert = ref();
|
||||
// const batchInsert() = >{
|
||||
//
|
||||
// }
|
||||
|
||||
// ---------------- CSV 数据加载 ----------------
|
||||
const regionsData = ref<any[]>([]);
|
||||
const regionMap = reactive(new Map<number, any>());
|
||||
|
||||
// ---------------- 部门数据加载 ----------------
|
||||
const deptMap = reactive(new Map<number, string>());
|
||||
async function loadDepts() {
|
||||
try {
|
||||
const res = await baseService.get("/sys/dept/list");
|
||||
const flat = (list: any[]) => {
|
||||
list.forEach((d) => {
|
||||
deptMap.set(Number(d.id), d.name);
|
||||
if (d.children) flat(d.children);
|
||||
});
|
||||
};
|
||||
flat(res.data || []);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
loadDepts();
|
||||
const res = await fetch("/region.csv");
|
||||
const text = await res.text();
|
||||
const parseCSV = (csvText: string) => {
|
||||
const lines = csvText.split("\n").filter((l) => l.trim());
|
||||
const headers = lines[0].split(",");
|
||||
return lines.slice(1).map((line) => {
|
||||
const cols = line.split(",");
|
||||
const obj: any = {};
|
||||
headers.forEach((h, i) => (obj[h.trim()] = cols[i].trim()));
|
||||
return obj;
|
||||
});
|
||||
};
|
||||
const data = parseCSV(text);
|
||||
data.forEach((r) => {
|
||||
const id = Number(r.id);
|
||||
const parentId = Number(r.parentId);
|
||||
const level = Number(r.level);
|
||||
const name = r.name;
|
||||
regionMap.set(id, { id, parentId, level, name });
|
||||
regionsData.value.push({ id, parentId, level, name });
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="!dataForm.id ? '新增' : '修改'" :close-on-click-modal="false" :close-on-press-escape="false">
|
||||
<el-form :model="dataForm" :rules="rules" ref="dataFormRef" @keyup.enter="dataFormSubmitHandle()" label-width="120px">
|
||||
<el-form-item prop="name" label="名称">
|
||||
<el-input v-model="dataForm.name" placeholder="名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="parentName" label="上级部门" class="dept-list">
|
||||
<el-popover :width="400" ref="deptListPopover" placement="bottom-start" trigger="click" popper-class="popover-pop">
|
||||
<template v-slot:reference>
|
||||
<el-input v-model="dataForm.parentName" :readonly="true" placeholder="上级部门">
|
||||
<template v-slot:suffix>
|
||||
<el-icon v-if="user.superAdmin === 1 && dataForm.pid !== '0'" @click.stop="deptListTreeSetDefaultHandle()" class="el-input__icon"><circle-close /></el-icon>
|
||||
</template> </el-input
|
||||
></template>
|
||||
<div class="popover-pop-body"><el-tree :data="deptList" :props="{ label: 'name', children: 'children' }" node-key="id" ref="deptListTree" :highlight-current="true" :expand-on-click-node="false" accordion @current-change="deptListTreeCurrentChangeHandle"> </el-tree></div>
|
||||
</el-popover>
|
||||
</el-form-item>
|
||||
<el-form-item prop="sort" label="排序">
|
||||
<el-input-number v-model="dataForm.sort" controls-position="right" :min="0" label="排序"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template v-slot:footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="dataFormSubmitHandle()">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import baseService from "@/service/baseService";
|
||||
import { IObject } from "@/types/interface";
|
||||
import { useAppStore } from "@/store";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
const store = useAppStore();
|
||||
|
||||
const emit = defineEmits(["refreshDataList"]);
|
||||
const visible = ref(false);
|
||||
const dataFormRef = ref();
|
||||
const deptList = ref([]);
|
||||
const deptListPopover = ref();
|
||||
const deptListTree = ref();
|
||||
|
||||
const dataForm = reactive({
|
||||
id: "",
|
||||
name: "",
|
||||
pid: "",
|
||||
parentName: "",
|
||||
sort: 0
|
||||
});
|
||||
|
||||
const user = computed(() => store.state.user);
|
||||
|
||||
const rules = ref({
|
||||
name: [{ required: true, message: "必填项不能为空", trigger: "blur" }],
|
||||
parentName: [{ required: true, message: "必填项不能为空", trigger: "change" }]
|
||||
});
|
||||
|
||||
const init = (id?: number) => {
|
||||
visible.value = true;
|
||||
dataForm.id = "";
|
||||
|
||||
// 重置表单数据
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
}
|
||||
|
||||
getDeptList().then(() => {
|
||||
if (id) {
|
||||
getInfo(id);
|
||||
} else if (store.state.user.superAdmin === 1) {
|
||||
deptListTreeSetDefaultHandle();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 获取部门列表
|
||||
const getDeptList = () => {
|
||||
return baseService.get("/sys/dept/list").then((res) => {
|
||||
if (res.code !== 0) {
|
||||
return ElMessage.error(res.msg);
|
||||
}
|
||||
deptList.value = res.data;
|
||||
});
|
||||
};
|
||||
|
||||
// 获取信息
|
||||
const getInfo = (id: number) => {
|
||||
baseService.get(`/sys/dept/${id}`).then((res) => {
|
||||
Object.assign(dataForm, res.data);
|
||||
|
||||
if (dataForm.pid === "0") {
|
||||
return deptListTreeSetDefaultHandle();
|
||||
}
|
||||
deptListTree.value.setCurrentKey(dataForm.pid);
|
||||
});
|
||||
};
|
||||
|
||||
// 上级部门树, 设置默认值
|
||||
const deptListTreeSetDefaultHandle = () => {
|
||||
dataForm.pid = "0";
|
||||
dataForm.parentName = "一级部门";
|
||||
};
|
||||
|
||||
// 上级部门树, 选中
|
||||
const deptListTreeCurrentChangeHandle = (data: IObject) => {
|
||||
dataForm.pid = data.id;
|
||||
dataForm.parentName = data.name;
|
||||
deptListPopover.value.hide();
|
||||
};
|
||||
|
||||
// 表单提交
|
||||
const dataFormSubmitHandle = () => {
|
||||
dataFormRef.value.validate((valid: boolean) => {
|
||||
if (!valid) {
|
||||
return false;
|
||||
}
|
||||
(!dataForm.id ? baseService.post : baseService.put)("/sys/dept", dataForm).then((res) => {
|
||||
ElMessage.success({
|
||||
message: "成功",
|
||||
duration: 500,
|
||||
onClose: () => {
|
||||
visible.value = false;
|
||||
emit("refreshDataList");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
init
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.mod-sys__dept {
|
||||
.dept-list {
|
||||
.el-input__inner,
|
||||
.el-input__suffix {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="mod-sys__dept">
|
||||
<el-form :inline="true" :model="state.dataForm" @keyup.enter="state.getDataList()">
|
||||
<el-form-item>
|
||||
<el-button v-if="state.hasPermission('sys:dept:save')" type="primary" @click="addOrUpdateHandle()">新增</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table v-loading="state.dataListLoading" :data="state.dataList" row-key="id" border style="width: 100%">
|
||||
<el-table-column prop="name" label="名称" header-align="center" min-width="150"></el-table-column>
|
||||
<el-table-column prop="parentName" label="上级部门" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="sort" label="排序" header-align="center" align="center" width="80"></el-table-column>
|
||||
<el-table-column label="操作" fixed="right" header-align="center" align="center" width="150">
|
||||
<template v-slot="scope">
|
||||
<el-button v-if="state.hasPermission('sys:dept:update')" type="primary" link @click="addOrUpdateHandle(scope.row.id)">修改</el-button>
|
||||
<el-button v-if="state.hasPermission('sys:dept:delete')" type="primary" link @click="state.deleteHandle(scope.row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 弹窗, 新增 / 修改 -->
|
||||
<add-or-update ref="addOrUpdateRef" @refreshDataList="state.getDataList"></add-or-update>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, toRefs } from "vue";
|
||||
import AddOrUpdate from "./dept-add-or-update.vue";
|
||||
import useView from "@/hooks/useView";
|
||||
|
||||
const view = reactive({
|
||||
getDataListURL: "/sys/dept/list",
|
||||
deleteURL: "/sys/dept"
|
||||
});
|
||||
|
||||
const state = reactive({ ...useView(view), ...toRefs(view) });
|
||||
|
||||
const addOrUpdateRef = ref();
|
||||
const addOrUpdateHandle = (id?: number) => {
|
||||
addOrUpdateRef.value.init(id);
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="!dataForm.id ? '新增' : '修改'" :close-on-click-modal="false" :close-on-press-escape="false">
|
||||
<el-form :model="dataForm" :rules="rules" ref="dataFormRef" @keyup.enter="dataFormSubmitHandle()" label-width="120px">
|
||||
<el-form-item prop="dictValue" label="字典值">
|
||||
<el-input v-model="dataForm.dictValue" placeholder="字典值"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="dictLabel" label="字典标签">
|
||||
<el-input v-model="dataForm.dictLabel" placeholder="字典标签"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="sort" label="排序">
|
||||
<el-input-number v-model="dataForm.sort" controls-position="right" :min="0" label="排序"></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item prop="remark" label="备注">
|
||||
<el-input v-model="dataForm.remark" placeholder="备注"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template v-slot:footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="dataFormSubmitHandle()">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from "vue";
|
||||
import baseService from "@/service/baseService";
|
||||
import { ElMessage } from "element-plus";
|
||||
const emit = defineEmits(["refreshDataList"]);
|
||||
|
||||
const visible = ref(false);
|
||||
const dataFormRef = ref();
|
||||
|
||||
const dataForm = reactive({
|
||||
id: "",
|
||||
dictTypeId: "",
|
||||
dictLabel: "",
|
||||
dictValue: "",
|
||||
sort: 0,
|
||||
remark: ""
|
||||
});
|
||||
|
||||
const rules = ref({
|
||||
dictLabel: [{ required: true, message: "必填项不能为空", trigger: "blur" }],
|
||||
dictValue: [{ required: true, message: "必填项不能为空", trigger: "blur" }],
|
||||
sort: [{ required: true, message: "必填项不能为空", trigger: "blur" }]
|
||||
});
|
||||
|
||||
const init = (id?: number) => {
|
||||
visible.value = true;
|
||||
dataForm.id = "";
|
||||
|
||||
// 重置表单数据
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
}
|
||||
|
||||
if (id) {
|
||||
getInfo(id);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取信息
|
||||
const getInfo = (id: number) => {
|
||||
baseService.get(`/sys/dict/data/${id}`).then((res) => {
|
||||
Object.assign(dataForm, res.data);
|
||||
});
|
||||
};
|
||||
|
||||
// 表单提交
|
||||
const dataFormSubmitHandle = () => {
|
||||
dataFormRef.value.validate((valid: boolean) => {
|
||||
if (!valid) {
|
||||
return false;
|
||||
}
|
||||
(!dataForm.id ? baseService.post : baseService.put)("/sys/dict/data", dataForm).then((res) => {
|
||||
ElMessage.success({
|
||||
message: "成功",
|
||||
duration: 500,
|
||||
onClose: () => {
|
||||
visible.value = false;
|
||||
emit("refreshDataList");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
init,
|
||||
dataForm
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div class="mod-sys__dict">
|
||||
<el-form :inline="true" :model="state.dataForm" @keyup.enter="state.getDataList()">
|
||||
<el-form-item>
|
||||
<el-input v-model="state.dataForm.dictValue" placeholder="字典值" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input v-model="state.dataForm.dictLabel" placeholder="字典标签" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="state.getDataList()">查询</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button v-if="state.hasPermission('sys:dict:save')" type="primary" @click="addOrUpdateHandle()">新增</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button v-if="state.hasPermission('sys:dict:delete')" type="danger" @click="state.deleteHandle()">删除</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table v-loading="state.dataListLoading" :data="state.dataList" border @selection-change="state.dataListSelectionChangeHandle" @sort-change="state.dataListSortChangeHandle" style="width: 100%">
|
||||
<el-table-column type="selection" header-align="center" align="center" width="50"></el-table-column>
|
||||
<el-table-column prop="dictValue" label="字典值" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="dictLabel" label="字典标签" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="sort" label="排序" sortable="custom" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="remark" label="备注" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="createDate" label="创建时间" sortable="custom" header-align="center" align="center" width="180"></el-table-column>
|
||||
<el-table-column label="操作" fixed="right" header-align="center" align="center" width="150">
|
||||
<template v-slot="scope">
|
||||
<el-button v-if="state.hasPermission('sys:dict:update')" type="primary" link @click="addOrUpdateHandle(scope.row.id)">修改</el-button>
|
||||
<el-button v-if="state.hasPermission('sys:dict:delete')" type="primary" link @click="state.deleteHandle(scope.row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination :current-page="state.page" :page-sizes="[10, 20, 50, 100]" :page-size="state.limit" :total="state.total" layout="total, sizes, prev, pager, next, jumper" @size-change="state.pageSizeChangeHandle" @current-change="state.pageCurrentChangeHandle"> </el-pagination>
|
||||
<!-- 弹窗, 新增 / 修改 -->
|
||||
<add-or-update ref="addOrUpdateRef" @refreshDataList="state.getDataList"></add-or-update>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useView from "@/hooks/useView";
|
||||
import { reactive, ref, toRefs } from "vue";
|
||||
import AddOrUpdate from "./dict-data-add-or-update.vue";
|
||||
|
||||
const props = defineProps({
|
||||
dictTypeId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const view = reactive({
|
||||
getDataListURL: "/sys/dict/data/page",
|
||||
getDataListIsPage: true,
|
||||
deleteURL: "/sys/dict/data",
|
||||
deleteIsBatch: true,
|
||||
dataForm: {
|
||||
dictTypeId: props.dictTypeId,
|
||||
dictLabel: "",
|
||||
dictValue: ""
|
||||
}
|
||||
});
|
||||
|
||||
const state = reactive({ ...useView(view), ...toRefs(view) });
|
||||
|
||||
const addOrUpdateRef = ref();
|
||||
const addOrUpdateHandle = (id?: number) => {
|
||||
addOrUpdateRef.value.dataForm.dictTypeId = props.dictTypeId;
|
||||
addOrUpdateRef.value.init(id);
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="!dataForm.id ? '新增' : '修改'" :close-on-click-modal="false" :close-on-press-escape="false">
|
||||
<el-form :model="dataForm" :rules="rules" ref="dataFormRef" @keyup.enter="dataFormSubmitHandle()" label-width="120px">
|
||||
<el-form-item prop="dictName" label="字典名称">
|
||||
<el-input v-model="dataForm.dictName" placeholder="字典名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="dictType" label="字典类型">
|
||||
<el-input v-model="dataForm.dictType" placeholder="字典类型"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="sort" label="排序">
|
||||
<el-input-number v-model="dataForm.sort" controls-position="right" :min="0" label="排序"></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item prop="remark" label="备注">
|
||||
<el-input v-model="dataForm.remark" placeholder="备注"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template v-slot:footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="dataFormSubmitHandle()">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from "vue";
|
||||
import baseService from "@/service/baseService";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
const emit = defineEmits(["refreshDataList"]);
|
||||
const visible = ref(false);
|
||||
const dataFormRef = ref();
|
||||
|
||||
const dataForm = reactive({
|
||||
id: "",
|
||||
dictName: "",
|
||||
dictType: "",
|
||||
sort: 0,
|
||||
remark: ""
|
||||
});
|
||||
|
||||
const init = (id?: number) => {
|
||||
visible.value = true;
|
||||
dataForm.id = "";
|
||||
|
||||
// 重置表单数据
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
}
|
||||
|
||||
if (id) {
|
||||
getInfo(id);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取信息
|
||||
const getInfo = (id: number) => {
|
||||
baseService.get(`/sys/dict/type/${id}`).then((res) => {
|
||||
Object.assign(dataForm, res.data);
|
||||
});
|
||||
};
|
||||
|
||||
const rules = ref({
|
||||
dictName: [{ required: true, message: "必填项不能为空", trigger: "blur" }],
|
||||
dictType: [{ required: true, message: "必填项不能为空", trigger: "blur" }],
|
||||
sort: [{ required: true, message: "必填项不能为空", trigger: "blur" }]
|
||||
});
|
||||
|
||||
// 表单提交
|
||||
const dataFormSubmitHandle = () => {
|
||||
dataFormRef.value.validate((valid: boolean) => {
|
||||
if (!valid) {
|
||||
return false;
|
||||
}
|
||||
(!dataForm.id ? baseService.post : baseService.put)("/sys/dict/type", dataForm).then((res) => {
|
||||
ElMessage.success({
|
||||
message: "成功",
|
||||
duration: 500,
|
||||
onClose: () => {
|
||||
visible.value = false;
|
||||
emit("refreshDataList");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
init
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div class="mod-sys__dict">
|
||||
<el-form :inline="true" :model="state.dataForm" @keyup.enter="state.getDataList()">
|
||||
<el-form-item>
|
||||
<el-input v-model="state.dataForm.dictName" placeholder="字典名称" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input v-model="state.dataForm.dictType" placeholder="字典类型" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="state.getDataList()">查询</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button v-if="state.hasPermission('sys:dict:save')" type="primary" @click="addOrUpdateHandle()">新增</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button v-if="state.hasPermission('sys:dict:delete')" type="danger" @click="state.deleteHandle()">删除</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table v-loading="state.dataListLoading" :data="state.dataList" border @selection-change="state.dataListSelectionChangeHandle" @sort-change="state.dataListSortChangeHandle" style="width: 100%">
|
||||
<el-table-column type="selection" header-align="center" align="center" width="50"></el-table-column>
|
||||
<el-table-column prop="dictName" label="字典名称" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="dictType" label="字典类型" header-align="center" align="center">
|
||||
<template v-slot="scope">
|
||||
<el-button type="primary" link @click="showTypeList(scope.row)">{{ scope.row.dictType }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" label="排序" sortable="custom" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="remark" label="备注" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="createDate" label="创建时间" sortable="custom" header-align="center" align="center" width="180"></el-table-column>
|
||||
<el-table-column label="操作" fixed="right" header-align="center" align="center" width="180">
|
||||
<template v-slot="scope">
|
||||
<el-button type="primary" link @click="showTypeList(scope.row)">字典配置</el-button>
|
||||
<el-button v-if="state.hasPermission('sys:dict:update')" type="primary" link @click="addOrUpdateHandle(scope.row.id)">修改</el-button>
|
||||
<el-button v-if="state.hasPermission('sys:dict:delete')" type="primary" link @click="state.deleteHandle(scope.row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination :current-page="state.page" :page-sizes="[10, 20, 50, 100]" :page-size="state.limit" :total="state.total" layout="total, sizes, prev, pager, next, jumper" @size-change="state.pageSizeChangeHandle" @current-change="state.pageCurrentChangeHandle"> </el-pagination>
|
||||
<!-- 弹窗, 新增 / 修改 -->
|
||||
<add-or-update ref="addOrUpdateRef" @refreshDataList="state.getDataList"></add-or-update>
|
||||
<!-- 字典类型数据 -->
|
||||
<el-drawer v-if="dictDataVisible" v-model="dictDataVisible" :title="state.focusDictTypeTitle" :size="800" :close-on-press-escape="false" class="rr-drawer"> <DictTypeList :dictTypeId="state.focusDictTypeId"></DictTypeList></el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useView from "@/hooks/useView";
|
||||
import { reactive, ref, toRefs } from "vue";
|
||||
import AddOrUpdate from "./dict-type-add-or-update.vue";
|
||||
import DictTypeList from "./dict-data.vue";
|
||||
import { IObject } from "@/types/interface";
|
||||
import { useRoute } from "vue-router";
|
||||
const route = useRoute();
|
||||
|
||||
const dictDataVisible = ref(false);
|
||||
|
||||
const view = reactive({
|
||||
getDataListURL: "/sys/dict/type/page",
|
||||
getDataListIsPage: true,
|
||||
deleteURL: "/sys/dict/type",
|
||||
deleteIsBatch: true,
|
||||
dataForm: {
|
||||
id: "0",
|
||||
dictName: "",
|
||||
dictType: ""
|
||||
},
|
||||
focusDictTypeId: "",
|
||||
focusDictTypeTitle: ""
|
||||
});
|
||||
|
||||
const state = reactive({ ...useView(view), ...toRefs(view) });
|
||||
|
||||
const showTypeList = (row: IObject) => {
|
||||
dictDataVisible.value = true;
|
||||
state.focusDictTypeId = row.id;
|
||||
state.focusDictTypeTitle = `${route.meta.title} - ${row.dictType}`;
|
||||
};
|
||||
|
||||
const addOrUpdateRef = ref();
|
||||
const addOrUpdateHandle = (id?: number) => {
|
||||
addOrUpdateRef.value.init(id);
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="mod-sys__log-error">
|
||||
<el-form :inline="true" :model="state.dataForm" @keyup.enter="state.getDataList()">
|
||||
<el-form-item>
|
||||
<el-button type="info" @click="state.exportHandle()">导出</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table v-loading="state.dataListLoading" :data="state.dataList" border @sort-change="state.dataListSortChangeHandle" style="width: 100%">
|
||||
<el-table-column prop="requestUri" label="请求URI" header-align="center" align="center" :show-overflow-tooltip="true"></el-table-column>
|
||||
<el-table-column prop="requestMethod" label="请求方式" header-align="center" align="center" :show-overflow-tooltip="true"></el-table-column>
|
||||
<el-table-column prop="requestParams" label="请求参数" header-align="center" align="center" width="150" :show-overflow-tooltip="true"></el-table-column>
|
||||
<el-table-column prop="ip" label="操作IP" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="userAgent" label="用户代理" header-align="center" align="center" width="150" :show-overflow-tooltip="true"></el-table-column>
|
||||
<el-table-column prop="createDate" label="创建时间" sortable="custom" header-align="center" align="center" width="180"></el-table-column>
|
||||
<el-table-column label="操作" fixed="right" header-align="center" align="center" width="150">
|
||||
<template v-slot="scope">
|
||||
<el-button type="primary" link @click="infoHandle(scope.row.errorInfo)">异常信息</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination :current-page="state.page" :page-sizes="[10, 20, 50, 100]" :page-size="state.limit" :total="state.total" layout="total, sizes, prev, pager, next, jumper" @size-change="state.pageSizeChangeHandle" @current-change="state.pageCurrentChangeHandle"> </el-pagination>
|
||||
|
||||
<el-dialog v-model="infoDialogVisible" title="异常信息" center>
|
||||
<span>{{ infoMessage }}</span>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="infoDialogVisible = false">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useView from "@/hooks/useView";
|
||||
import { reactive, toRefs, ref } from "vue";
|
||||
|
||||
const infoDialogVisible = ref(false)
|
||||
const infoMessage = ref('')
|
||||
const view = reactive({
|
||||
getDataListURL: "/sys/log/error/page",
|
||||
getDataListIsPage: true,
|
||||
exportURL: "/sys/log/error/export"
|
||||
});
|
||||
|
||||
const state = reactive({ ...useView(view), ...toRefs(view) });
|
||||
|
||||
const infoHandle = (info: string) => {
|
||||
infoMessage.value = info
|
||||
infoDialogVisible.value = true
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.mod-sys__log-error {
|
||||
&-view-info {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="mod-sys__log-login">
|
||||
<el-form :inline="true" :model="state.dataForm" @keyup.enter="state.getDataList()">
|
||||
<el-form-item>
|
||||
<el-input v-model="state.dataForm.creatorName" placeholder="用户名" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-select v-model="state.dataForm.status" placeholder="状态" clearable>
|
||||
<el-option label="失败" :value="0"></el-option>
|
||||
<el-option label="成功" :value="1"></el-option>
|
||||
<el-option label="账号已锁定" :value="2"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="state.getDataList()">查询</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="info" @click="state.exportHandle()">导出</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table v-loading="state.dataListLoading" :data="state.dataList" border @sort-change="state.dataListSortChangeHandle" style="width: 100%">
|
||||
<el-table-column prop="creatorName" label="用户名" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="operation" label="操作类型" header-align="center" align="center">
|
||||
<template v-slot="scope">
|
||||
{{ scope.row.operation === 0 ? "登录" : "退出" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" sortable="custom" header-align="center" align="center">
|
||||
<template v-slot="scope">
|
||||
<el-tag v-if="scope.row.status === 0" size="small" type="danger">失败</el-tag>
|
||||
<el-tag v-else-if="scope.row.status === 1" size="small" type="success">成功</el-tag>
|
||||
<el-tag v-else size="small" type="warning">账号已锁定</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="ip" label="操作IP" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="userAgent" label="用户代理" header-align="center" align="center" width="150" :show-overflow-tooltip="true"></el-table-column>
|
||||
<el-table-column prop="createDate" label="创建时间" sortable="custom" header-align="center" align="center" width="180"></el-table-column>
|
||||
</el-table>
|
||||
<el-pagination :current-page="state.page" :page-sizes="[10, 20, 50, 100]" :page-size="state.limit" :total="state.total" layout="total, sizes, prev, pager, next, jumper" @size-change="state.pageSizeChangeHandle" @current-change="state.pageCurrentChangeHandle"> </el-pagination>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useView from "@/hooks/useView";
|
||||
import { reactive, toRefs } from "vue";
|
||||
|
||||
const view = reactive({
|
||||
getDataListURL: "/sys/log/login/page",
|
||||
getDataListIsPage: true,
|
||||
exportURL: "/sys/log/login/export",
|
||||
dataForm: {
|
||||
creatorName: "",
|
||||
status: ""
|
||||
}
|
||||
});
|
||||
|
||||
const state = reactive({ ...useView(view), ...toRefs(view) });
|
||||
</script>
|
||||
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="mod-sys__log-operation">
|
||||
<el-form :inline="true" :model="state.dataForm" @keyup.enter="state.getDataList()">
|
||||
<el-form-item>
|
||||
<el-select v-model="state.dataForm.status" placeholder="状态" clearable>
|
||||
<el-option label="失败" :value="0"></el-option>
|
||||
<el-option label="成功" :value="1"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="state.getDataList()">查询</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="info" @click="state.exportHandle()">导出</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table v-loading="state.dataListLoading" :data="state.dataList" border @sort-change="state.dataListSortChangeHandle" style="width: 100%">
|
||||
<el-table-column prop="creatorName" label="用户名" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="operation" label="用户操作" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="requestUri" label="请求URI" header-align="center" align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="requestMethod" label="请求方式" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="requestParams" label="请求参数" header-align="center" align="center" width="150" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="requestTime" label="请求时长" sortable="custom" header-align="center" align="center">
|
||||
<template v-slot="scope">
|
||||
{{ `${scope.row.requestTime}ms` }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" sortable="custom" header-align="center" align="center">
|
||||
<template v-slot="scope">
|
||||
<el-tag v-if="scope.row.status === 0" size="small" type="danger">失败</el-tag>
|
||||
<el-tag v-else size="small" type="success">成功</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="ip" label="操作IP" header-align="center" align="center" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="userAgent" label="用户代理" header-align="center" align="center" width="150" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="createDate" label="创建时间" sortable="custom" header-align="center" align="center" width="180"></el-table-column>
|
||||
</el-table>
|
||||
<el-pagination :current-page="state.page" :page-sizes="[10, 20, 50, 100]" :page-size="state.limit" :total="state.total" layout="total, sizes, prev, pager, next, jumper" @size-change="state.pageSizeChangeHandle" @current-change="state.pageCurrentChangeHandle"> </el-pagination>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useView from "@/hooks/useView";
|
||||
import { reactive, toRefs } from "vue";
|
||||
|
||||
const view = reactive({
|
||||
getDataListURL: "/sys/log/operation/page",
|
||||
getDataListIsPage: true,
|
||||
exportURL: "/sys/log/operation/export",
|
||||
dataForm: {
|
||||
status: ""
|
||||
}
|
||||
});
|
||||
|
||||
const state = reactive({ ...useView(view), ...toRefs(view) });
|
||||
</script>
|
||||
@@ -0,0 +1,236 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="!dataForm.id ? '新增' : '修改'" :close-on-click-modal="false" :close-on-press-escape="false">
|
||||
<el-form :model="dataForm" :rules="rules" ref="dataFormRef" @keyup.enter="dataFormSubmitHandle()" label-width="120px">
|
||||
<el-form-item prop="menuType" label="类型">
|
||||
<el-radio-group v-model="dataForm.menuType" :disabled="!!dataForm.id">
|
||||
<el-radio :label="0">菜单</el-radio>
|
||||
<el-radio :label="1">按钮</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item prop="name" label="名称">
|
||||
<el-input v-model="dataForm.name" placeholder="名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="parentName" label="上级菜单" class="menu-list">
|
||||
<el-popover ref="menuListPopover" placement="bottom-start" trigger="click" :width="400" popper-class="popover-pop">
|
||||
<template v-slot:reference>
|
||||
<el-input v-model="dataForm.parentName" :readonly="true" placeholder="上级菜单">
|
||||
<template v-slot:suffix>
|
||||
<el-icon v-if="dataForm.pid !== '0'" @click.stop="deptListTreeSetDefaultHandle()" class="el-input__icon"><circle-close /></el-icon
|
||||
></template>
|
||||
</el-input>
|
||||
</template>
|
||||
<div class="popover-pop-body">
|
||||
<el-tree :data="menuList" :props="{ label: 'name', children: 'children' }" node-key="id" ref="menuListTree" :highlight-current="true" :expand-on-click-node="false" accordion @current-change="menuListTreeCurrentChangeHandle"> </el-tree>
|
||||
</div>
|
||||
</el-popover>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="dataForm.menuType === 0" prop="url" label="路由">
|
||||
<el-input v-model="dataForm.url" placeholder="路由"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="sort" label="排序">
|
||||
<el-input-number v-model="dataForm.sort" controls-position="right" :min="0" label="排序"></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="dataForm.menuType === 0" prop="openStyle" label="打开方式">
|
||||
<el-radio-group v-model="dataForm.openStyle">
|
||||
<el-radio :label="0">内部打开</el-radio>
|
||||
<el-radio :label="1">外部打开</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item prop="permissions" label="授权标识">
|
||||
<el-input v-model="dataForm.permissions" placeholder="多个用逗号分隔,如:sys:menu:save,sys:menu:update"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="dataForm.menuType === 0" prop="icon" label="图标" class="icon-list">
|
||||
<el-popover ref="iconListPopover" placement="top-start" trigger="click" popper-class="popover-pop mod-sys__menu-icon-popover">
|
||||
<template v-slot:reference> <el-input v-model="dataForm.icon" :readonly="true" placeholder="图标"></el-input></template>
|
||||
<div class="mod-sys__menu-icon-inner">
|
||||
<div class="mod-sys__menu-icon-list">
|
||||
<el-button v-for="(item, index) in iconList" :key="index" @click="iconListCurrentChangeHandle(item)" :class="{ 'is-active': dataForm.icon === item }">
|
||||
<svg class="icon-svg" aria-hidden="true"><use :xlink:href="`#${item}`"></use></svg>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template v-slot:footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="dataFormSubmitHandle()">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from "vue";
|
||||
import baseService from "@/service/baseService";
|
||||
import { getIconList } from "@/utils/utils";
|
||||
import { IObject } from "@/types/interface";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
const emit = defineEmits(["refreshDataList"]);
|
||||
|
||||
const visible = ref(false);
|
||||
const menuList = ref([]);
|
||||
const iconList = ref<string[]>([]);
|
||||
const dataFormRef = ref();
|
||||
const menuListTree = ref();
|
||||
const menuListPopover = ref();
|
||||
const iconListPopover = ref();
|
||||
|
||||
const dataForm = reactive({
|
||||
id: "",
|
||||
menuType: 0,
|
||||
name: "",
|
||||
pid: "0",
|
||||
parentName: "",
|
||||
url: "",
|
||||
permissions: "",
|
||||
sort: 0,
|
||||
icon: "",
|
||||
openStyle: 0
|
||||
});
|
||||
|
||||
const rules = ref({
|
||||
name: [{ required: true, message: "必填项不能为空", trigger: "blur" }],
|
||||
parentName: [{ required: true, message: "必填项不能为空", trigger: "change" }]
|
||||
});
|
||||
|
||||
const init = (id: number) => {
|
||||
visible.value = true;
|
||||
dataForm.id = "";
|
||||
|
||||
// 重置表单数据
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
}
|
||||
|
||||
iconList.value = getIconList();
|
||||
|
||||
dataForm.pid = "0";
|
||||
dataForm.parentName = "一级菜单";
|
||||
|
||||
getMenuList().then(() => {
|
||||
if (id) {
|
||||
getInfo(id);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const init2 = (row: IObject) => {
|
||||
visible.value = true;
|
||||
|
||||
// 重置表单数据
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
}
|
||||
|
||||
iconList.value = getIconList();
|
||||
|
||||
dataForm.id = "";
|
||||
dataForm.pid = row.id;
|
||||
dataForm.parentName = row.name;
|
||||
|
||||
getMenuList();
|
||||
};
|
||||
|
||||
// 获取菜单列表
|
||||
const getMenuList = () => {
|
||||
return baseService.get("/sys/menu/list?type=0").then((res) => {
|
||||
menuList.value = res.data;
|
||||
});
|
||||
};
|
||||
|
||||
// 获取信息
|
||||
const getInfo = (id: number) => {
|
||||
baseService.get(`/sys/menu/${id}`).then((res) => {
|
||||
Object.assign(dataForm, res.data);
|
||||
if (dataForm.pid === "0") {
|
||||
return deptListTreeSetDefaultHandle();
|
||||
}
|
||||
menuListTree.value.setCurrentKey(dataForm.pid);
|
||||
});
|
||||
};
|
||||
|
||||
// 上级菜单树, 设置默认值
|
||||
const deptListTreeSetDefaultHandle = () => {
|
||||
dataForm.pid = "0";
|
||||
dataForm.parentName = "一级菜单";
|
||||
};
|
||||
|
||||
// 上级菜单树, 选中
|
||||
const menuListTreeCurrentChangeHandle = (data: IObject) => {
|
||||
dataForm.pid = data.id;
|
||||
dataForm.parentName = data.name;
|
||||
menuListPopover.value.hide();
|
||||
};
|
||||
|
||||
// 图标, 选中
|
||||
const iconListCurrentChangeHandle = (icon: string) => {
|
||||
dataForm.icon = icon;
|
||||
iconListPopover.value.hide();
|
||||
};
|
||||
|
||||
// 表单提交
|
||||
const dataFormSubmitHandle = () => {
|
||||
dataFormRef.value.validate((valid: boolean) => {
|
||||
if (!valid) {
|
||||
return false;
|
||||
}
|
||||
(!dataForm.id ? baseService.post : baseService.put)("/sys/menu", dataForm).then((res) => {
|
||||
ElMessage.success({
|
||||
message: "成功",
|
||||
duration: 500,
|
||||
onClose: () => {
|
||||
visible.value = false;
|
||||
emit("refreshDataList");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
init,
|
||||
init2
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.el-popover.el-popper {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.mod-sys__menu {
|
||||
.menu-list,
|
||||
.icon-list {
|
||||
.el-input__inner,
|
||||
.el-input__suffix {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
&-icon-popover {
|
||||
width: 458px !important;
|
||||
overflow-y: hidden !important;
|
||||
}
|
||||
&-icon-inner {
|
||||
width: 100%;
|
||||
max-height: 260px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
&-icon-list {
|
||||
width: 458px !important;
|
||||
padding: 0;
|
||||
margin: -8px 0 0 -8px;
|
||||
> .el-button {
|
||||
padding: 8px;
|
||||
margin: 8px 0 0 8px;
|
||||
> span {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div class="mod-sys__menu">
|
||||
<el-form :inline="true" :model="state.dataForm" @keyup.enter="state.getDataList()">
|
||||
<el-form-item>
|
||||
<el-button v-if="state.hasPermission('sys:menu:save')" type="primary" @click="addOrUpdateHandle()">新增</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table v-loading="state.dataListLoading" :data="state.dataList" show-overflow-tooltip row-key="id" border style="width: 100%">
|
||||
<el-table-column prop="name" label="名称" header-align="center" min-width="150"></el-table-column>
|
||||
<el-table-column prop="icon" label="图标" header-align="center" align="center">
|
||||
<template v-slot="scope">
|
||||
<svg class="iconfont" aria-hidden="true">
|
||||
<use :xlink:href="`#${scope.row.icon}`"></use>
|
||||
</svg>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="menuType" label="类型" header-align="center" align="center">
|
||||
<template v-slot="scope">
|
||||
<el-tag v-if="scope.row.menuType === 0">菜单</el-tag>
|
||||
<el-tag v-else type="info">按钮</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="openStyle" label="打开方式" header-align="center" align="center">
|
||||
<template v-slot="scope">
|
||||
<span v-if="scope.row.menuType !== 0"></span>
|
||||
<el-tag v-else-if="scope.row.openStyle === 1">外部打开</el-tag>
|
||||
<el-tag v-else type="info">内部打开</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" label="排序" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="url" label="路由" header-align="center" align="center" width="150"></el-table-column>
|
||||
<el-table-column prop="permissions" label="授权标识" header-align="center" align="center" width="150"></el-table-column>
|
||||
<el-table-column label="操作" fixed="right" header-align="center" align="center" width="170">
|
||||
<template v-slot="scope">
|
||||
<el-button v-if="state.hasPermission('sys:menu:save') && scope.row.menuType === 0" type="primary" link @click="addHandle(scope.row)">新增</el-button>
|
||||
<el-button v-if="state.hasPermission('sys:menu:update')" type="primary" link @click="addOrUpdateHandle(scope.row.id)">修改</el-button>
|
||||
<el-button v-if="state.hasPermission('sys:menu:delete')" type="primary" link @click="state.deleteHandle(scope.row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 弹窗, 新增 / 修改 -->
|
||||
<add-or-update ref="addOrUpdateRef" @refreshDataList="state.getDataList"></add-or-update>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useView from "@/hooks/useView";
|
||||
import { reactive, ref, toRefs } from "vue";
|
||||
import AddOrUpdate from "./menu-add-or-update.vue";
|
||||
|
||||
const view = reactive({
|
||||
getDataListURL: "/sys/menu/list",
|
||||
deleteURL: "/sys/menu"
|
||||
});
|
||||
|
||||
const state = reactive({ ...useView(view), ...toRefs(view) });
|
||||
|
||||
const addOrUpdateRef = ref();
|
||||
const addOrUpdateHandle = (id?: number) => {
|
||||
addOrUpdateRef.value.init(id);
|
||||
};
|
||||
|
||||
const addHandle = (row: any) => {
|
||||
addOrUpdateRef.value.init2(row);
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="!dataForm.id ? '新增' : '修改'" :close-on-click-modal="false" :close-on-press-escape="false">
|
||||
<el-form :model="dataForm" :rules="rules" ref="dataFormRef" @keyup.enter="dataFormSubmitHandle()" label-width="120px">
|
||||
<el-form-item prop="paramCode" label="编码">
|
||||
<el-input v-model="dataForm.paramCode" placeholder="编码"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="paramValue" label="值">
|
||||
<el-input v-model="dataForm.paramValue" placeholder="值"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="remark" label="备注">
|
||||
<el-input v-model="dataForm.remark" placeholder="备注"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template v-slot:footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="dataFormSubmitHandle()">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from "vue";
|
||||
import baseService from "@/service/baseService";
|
||||
import { ElMessage } from "element-plus";
|
||||
const emit = defineEmits(["refreshDataList"]);
|
||||
|
||||
const visible = ref(false);
|
||||
const dataFormRef = ref();
|
||||
|
||||
const dataForm = reactive({
|
||||
id: "",
|
||||
paramCode: "",
|
||||
paramValue: "",
|
||||
remark: ""
|
||||
});
|
||||
|
||||
const rules = ref({
|
||||
paramCode: [{ required: true, message: "必填项不能为空", trigger: "blur" }],
|
||||
paramValue: [{ required: true, message: "必填项不能为空", trigger: "blur" }]
|
||||
});
|
||||
|
||||
const init = (id?: number) => {
|
||||
visible.value = true;
|
||||
dataForm.id = "";
|
||||
|
||||
// 重置表单数据
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
}
|
||||
|
||||
if (id) {
|
||||
getInfo(id);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取信息
|
||||
const getInfo = (id: number) => {
|
||||
baseService.get(`/sys/params/${id}`).then((res) => {
|
||||
Object.assign(dataForm, res.data);
|
||||
});
|
||||
};
|
||||
|
||||
// 表单提交
|
||||
const dataFormSubmitHandle = () => {
|
||||
dataFormRef.value.validate((valid: boolean) => {
|
||||
if (!valid) {
|
||||
return false;
|
||||
}
|
||||
(!dataForm.id ? baseService.post : baseService.put)("/sys/params", dataForm).then((res) => {
|
||||
ElMessage.success({
|
||||
message: "成功",
|
||||
duration: 500,
|
||||
onClose: () => {
|
||||
visible.value = false;
|
||||
emit("refreshDataList");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
init
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="mod-sys__params">
|
||||
<el-form :inline="true" :model="state.dataForm" @keyup.enter="state.getDataList()">
|
||||
<el-form-item>
|
||||
<el-input v-model="state.dataForm.paramCode" placeholder="编码" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="state.getDataList()">查询</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button v-if="state.hasPermission('sys:params:save')" type="primary" @click="addOrUpdateHandle()">新增</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button v-if="state.hasPermission('sys:params:delete')" type="danger" @click="state.deleteHandle()">删除</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table v-loading="state.dataListLoading" :data="state.dataList" border @selection-change="state.dataListSelectionChangeHandle" style="width: 100%">
|
||||
<el-table-column type="selection" header-align="center" align="center" width="50"></el-table-column>
|
||||
<el-table-column prop="paramCode" label="编码" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="paramValue" label="值" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="remark" label="备注" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column label="操作" fixed="right" header-align="center" align="center" width="150">
|
||||
<template v-slot="scope">
|
||||
<el-button v-if="state.hasPermission('sys:params:update')" type="primary" link @click="addOrUpdateHandle(scope.row.id)">修改</el-button>
|
||||
<el-button v-if="state.hasPermission('sys:params:delete')" type="primary" link @click="state.deleteHandle(scope.row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination :current-page="state.page" :page-sizes="[10, 20, 50, 100]" :page-size="state.limit" :total="state.total" layout="total, sizes, prev, pager, next, jumper" @size-change="state.pageSizeChangeHandle" @current-change="state.pageCurrentChangeHandle"> </el-pagination>
|
||||
<!-- 弹窗, 新增 / 修改 -->
|
||||
<add-or-update ref="addOrUpdateRef" @refreshDataList="state.getDataList"></add-or-update>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useView from "@/hooks/useView";
|
||||
import { reactive, ref, toRefs } from "vue";
|
||||
import AddOrUpdate from "./params-add-or-update.vue";
|
||||
|
||||
const view = reactive({
|
||||
getDataListURL: "/sys/params/page",
|
||||
getDataListIsPage: true,
|
||||
deleteURL: "/sys/params",
|
||||
deleteIsBatch: true,
|
||||
dataForm: {
|
||||
paramCode: ""
|
||||
}
|
||||
});
|
||||
|
||||
const state = reactive({ ...useView(view), ...toRefs(view) });
|
||||
|
||||
const addOrUpdateRef = ref();
|
||||
const addOrUpdateHandle = (id?: number) => {
|
||||
addOrUpdateRef.value.init(id);
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="!dataForm.id ? '新增' : '修改'" :close-on-click-modal="false" :close-on-press-escape="false">
|
||||
<el-form :model="dataForm" :rules="rules" ref="dataFormRef" @keyup.enter="dataFormSubmitHandle()" label-width="120px">
|
||||
<el-form-item prop="name" label="名称">
|
||||
<el-input v-model="dataForm.name" placeholder="名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="remark" label="备注">
|
||||
<el-input v-model="dataForm.remark" placeholder="备注"></el-input>
|
||||
</el-form-item>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item size="small" label="菜单授权">
|
||||
<el-tree :data="menuList" :props="{ label: 'name', children: 'children' }" node-key="id" ref="menuListTree" accordion show-checkbox> </el-tree>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item size="small" label="数据授权">
|
||||
<el-tree :data="deptList" :props="{ label: 'name', children: 'children' }" node-key="id" ref="deptListTree" accordion show-checkbox> </el-tree>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template v-slot:footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="dataFormSubmitHandle()">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, reactive, ref } from "vue";
|
||||
import baseService from "@/service/baseService";
|
||||
import { IObject } from "@/types/interface";
|
||||
import { ElMessage } from "element-plus";
|
||||
const emit = defineEmits(["refreshDataList"]);
|
||||
|
||||
const visible = ref(false);
|
||||
const menuList = ref([]);
|
||||
const deptList = ref([]);
|
||||
const dataFormRef = ref();
|
||||
const menuListTree = ref();
|
||||
const deptListTree = ref();
|
||||
|
||||
const dataForm = reactive({
|
||||
id: "",
|
||||
name: "",
|
||||
menuIdList: [] as IObject[],
|
||||
deptIdList: [],
|
||||
remark: ""
|
||||
});
|
||||
|
||||
const rules = ref({
|
||||
name: [{ required: true, message: "必填项不能为空", trigger: "blur" }]
|
||||
});
|
||||
|
||||
const init = (id?: number) => {
|
||||
visible.value = true;
|
||||
dataForm.id = "";
|
||||
|
||||
// 重置表单数据
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
if (menuListTree.value) {
|
||||
menuListTree.value.setCheckedKeys([]);
|
||||
}
|
||||
|
||||
if (deptListTree.value) {
|
||||
deptListTree.value.setCheckedKeys([]);
|
||||
}
|
||||
|
||||
Promise.all([getMenuList(), getDeptList()]).then(() => {
|
||||
if (id) {
|
||||
getInfo(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 获取菜单列表
|
||||
const getMenuList = () => {
|
||||
return baseService.get("/sys/menu/select").then((res) => {
|
||||
menuList.value = res.data;
|
||||
});
|
||||
};
|
||||
|
||||
// 获取部门列表
|
||||
const getDeptList = () => {
|
||||
return baseService.get("/sys/dept/list").then((res) => {
|
||||
deptList.value = res.data;
|
||||
});
|
||||
};
|
||||
|
||||
// 获取信息
|
||||
const getInfo = (id: number) => {
|
||||
baseService.get(`/sys/role/${id}`).then((res) => {
|
||||
Object.assign(dataForm, res.data);
|
||||
|
||||
dataForm.menuIdList.forEach((item: IObject) => menuListTree.value.setChecked(item, true));
|
||||
deptListTree.value.setCheckedKeys(dataForm.deptIdList);
|
||||
});
|
||||
};
|
||||
|
||||
// 表单提交
|
||||
const dataFormSubmitHandle = () => {
|
||||
dataFormRef.value.validate((valid: boolean) => {
|
||||
if (!valid) {
|
||||
return false;
|
||||
}
|
||||
dataForm.menuIdList = [...menuListTree.value.getHalfCheckedKeys(), ...menuListTree.value.getCheckedKeys()];
|
||||
dataForm.deptIdList = deptListTree.value.getCheckedKeys();
|
||||
(!dataForm.id ? baseService.post : baseService.put)("/sys/role", dataForm).then((res) => {
|
||||
ElMessage.success({
|
||||
message: "成功",
|
||||
duration: 500,
|
||||
onClose: () => {
|
||||
visible.value = false;
|
||||
emit("refreshDataList");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
init
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="mod-sys__role">
|
||||
<el-form :inline="true" :model="state.dataForm" @keyup.enter="state.getDataList()">
|
||||
<el-form-item>
|
||||
<el-input v-model="state.dataForm.name" placeholder="名称" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="state.getDataList()">查询</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button v-if="state.hasPermission('sys:role:save')" type="primary" @click="addOrUpdateHandle()">新增</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button v-if="state.hasPermission('sys:role:delete')" type="danger" @click="state.deleteHandle()">删除</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table v-loading="state.dataListLoading" :data="state.dataList" border @selection-change="state.dataListSelectionChangeHandle" @sort-change="state.dataListSortChangeHandle" style="width: 100%">
|
||||
<el-table-column type="selection" header-align="center" align="center" width="50"></el-table-column>
|
||||
<el-table-column prop="name" label="名称" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="remark" label="备注" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="createDate" label="创建时间" sortable="custom" header-align="center" align="center" width="180"></el-table-column>
|
||||
<el-table-column label="操作" fixed="right" header-align="center" align="center" width="150">
|
||||
<template v-slot="scope">
|
||||
<el-button v-if="state.hasPermission('sys:role:update')" type="primary" link @click="addOrUpdateHandle(scope.row.id)">修改</el-button>
|
||||
<el-button v-if="state.hasPermission('sys:role:delete')" type="primary" link @click="state.deleteHandle(scope.row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination :current-page="state.page" :page-sizes="[10, 20, 50, 100]" :page-size="state.limit" :total="state.total" layout="total, sizes, prev, pager, next, jumper" @size-change="state.pageSizeChangeHandle" @current-change="state.pageCurrentChangeHandle"> </el-pagination>
|
||||
<!-- 弹窗, 新增 / 修改 -->
|
||||
<add-or-update ref="addOrUpdateRef" @refreshDataList="state.getDataList"></add-or-update>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useView from "@/hooks/useView";
|
||||
import { reactive, ref, toRefs } from "vue";
|
||||
import AddOrUpdate from "./role-add-or-update.vue";
|
||||
|
||||
const view = reactive({
|
||||
getDataListURL: "/sys/role/page",
|
||||
getDataListIsPage: true,
|
||||
deleteURL: "/sys/role",
|
||||
deleteIsBatch: true,
|
||||
dataForm: {
|
||||
name: ""
|
||||
}
|
||||
});
|
||||
|
||||
const state = reactive({ ...useView(view), ...toRefs(view) });
|
||||
|
||||
const addOrUpdateRef = ref();
|
||||
const addOrUpdateHandle = (id?: number) => {
|
||||
addOrUpdateRef.value.init(id);
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,189 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="!dataForm.id ? '新增' : '修改'" :close-on-click-modal="false" :close-on-press-escape="false">
|
||||
<el-form :model="dataForm" :rules="rules" ref="dataFormRef" @keyup.enter="dataFormSubmitHandle()" label-width="120px">
|
||||
<el-form-item prop="username" label="用户名">
|
||||
<el-input v-model="dataForm.username" placeholder="用户名"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="deptName" label="所属部门">
|
||||
<sys-dept-tree v-model="dataForm.deptId" placeholder="选择部门" v-model:deptName="dataForm.deptName"></sys-dept-tree>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password" label="密码" :class="{ 'is-required': !dataForm.id }">
|
||||
<el-input v-model="dataForm.password" type="password" placeholder="密码"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="confirmPassword" label="确认密码" :class="{ 'is-required': !dataForm.id }">
|
||||
<el-input v-model="dataForm.confirmPassword" type="password" placeholder="确认密码"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="realName" label="真实姓名">
|
||||
<el-input v-model="dataForm.realName" placeholder="真实姓名"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="gender" label="性别">
|
||||
<sys-radio-group v-model="dataForm.gender" dict-type="gender"></sys-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item prop="email" label="邮箱">
|
||||
<el-input v-model="dataForm.email" placeholder="邮箱"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="mobile" label="手机号">
|
||||
<el-input v-model="dataForm.mobile" placeholder="手机号"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="roleIdList" label="角色配置" class="role-list">
|
||||
<el-select v-model="dataForm.roleIdList" multiple placeholder="角色配置">
|
||||
<el-option v-for="role in roleList" :key="role.id" :label="role.name" :value="role.id"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="status" label="状态">
|
||||
<el-radio-group v-model="dataForm.status">
|
||||
<el-radio :label="0">停用</el-radio>
|
||||
<el-radio :label="1">正常</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template v-slot:footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="dataFormSubmitHandle()">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from "vue";
|
||||
import baseService from "@/service/baseService";
|
||||
import { isAlphanumeric, isEmail, isMobile } from "@/utils/utils";
|
||||
import { IObject } from "@/types/interface";
|
||||
import { ElMessage } from "element-plus";
|
||||
const emit = defineEmits(["refreshDataList"]);
|
||||
|
||||
const visible = ref(false);
|
||||
const roleList = ref<any[]>([]);
|
||||
const dataFormRef = ref();
|
||||
|
||||
const dataForm = reactive({
|
||||
id: "",
|
||||
username: "",
|
||||
deptId: "",
|
||||
deptName: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
realName: "",
|
||||
gender: 0,
|
||||
email: "",
|
||||
mobile: "",
|
||||
roleIdList: [] as IObject[],
|
||||
highRoleIdList: [] as IObject[],
|
||||
status: 1
|
||||
});
|
||||
|
||||
const validateUsername = (rule: any, value: string, callback: (e?: Error) => any): any => {
|
||||
if (value && !isAlphanumeric(value)) {
|
||||
return callback(new Error("用户名只能为英文或数字组合"));
|
||||
}
|
||||
callback();
|
||||
};
|
||||
const validatePassword = (rule: any, value: string, callback: (e?: Error) => any): any => {
|
||||
if (!dataForm.id && !/\S/.test(value)) {
|
||||
return callback(new Error("必填项不能为空"));
|
||||
}
|
||||
callback();
|
||||
};
|
||||
const validateConfirmPassword = (rule: any, value: string, callback: (e?: Error) => any): any => {
|
||||
if (!dataForm.id && !/\S/.test(value)) {
|
||||
return callback(new Error("必填项不能为空"));
|
||||
}
|
||||
if (dataForm.password !== value) {
|
||||
return callback(new Error("确认密码与密码输入不一致"));
|
||||
}
|
||||
callback();
|
||||
};
|
||||
const validateEmail = (rule: any, value: string, callback: (e?: Error) => any): any => {
|
||||
if (value && !isEmail(value)) {
|
||||
return callback(new Error("邮箱格式错误"));
|
||||
}
|
||||
callback();
|
||||
};
|
||||
const validateMobile = (rule: any, value: string, callback: (e?: Error) => any): any => {
|
||||
if (value && !isMobile(value)) {
|
||||
return callback(new Error("手机格式错误"));
|
||||
}
|
||||
callback();
|
||||
};
|
||||
const rules = ref({
|
||||
username: [
|
||||
{ required: true, message: "必填项不能为空", trigger: "blur" },
|
||||
{ validator: validateUsername, trigger: "blur" }
|
||||
],
|
||||
deptName: [{ required: true, message: "必填项不能为空", trigger: "change" }],
|
||||
password: [{ validator: validatePassword, trigger: "blur" }],
|
||||
confirmPassword: [{ validator: validateConfirmPassword, trigger: "blur" }],
|
||||
realName: [{ required: true, message: "必填项不能为空", trigger: "blur" }],
|
||||
email: [{ validator: validateEmail, trigger: "blur" }],
|
||||
mobile: [{ validator: validateMobile, trigger: "blur" }]
|
||||
});
|
||||
|
||||
const init = (id?: number) => {
|
||||
visible.value = true;
|
||||
dataForm.id = "";
|
||||
dataForm.deptId = "";
|
||||
|
||||
// 重置表单数据
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
}
|
||||
|
||||
Promise.all([getRoleList()]).then(() => {
|
||||
if (id) {
|
||||
getInfo(id);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 获取角色列表
|
||||
const getRoleList = () => {
|
||||
return baseService.get("/sys/role/list").then((res) => {
|
||||
roleList.value = res.data;
|
||||
});
|
||||
};
|
||||
|
||||
// 获取信息
|
||||
const getInfo = (id: number) => {
|
||||
baseService.get(`/sys/user/${id}`).then((res) => {
|
||||
Object.assign(dataForm, res.data);
|
||||
dataForm.highRoleIdList = dataForm.roleIdList.filter((id) => !roleList.value.some((role) => role.id === id));
|
||||
dataForm.roleIdList = dataForm.roleIdList.filter((id) => !dataForm.highRoleIdList.includes(id));
|
||||
});
|
||||
};
|
||||
|
||||
// 表单提交
|
||||
const dataFormSubmitHandle = () => {
|
||||
dataFormRef.value.validate((valid: boolean) => {
|
||||
if (!valid) {
|
||||
return false;
|
||||
}
|
||||
(!dataForm.id ? baseService.post : baseService.put)("/sys/user", {
|
||||
...dataForm,
|
||||
roleIdList: [...dataForm.roleIdList, ...dataForm.highRoleIdList]
|
||||
}).then((res) => {
|
||||
ElMessage.success({
|
||||
message: "成功",
|
||||
duration: 500,
|
||||
onClose: () => {
|
||||
visible.value = false;
|
||||
emit("refreshDataList");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
init
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.mod-sys__user {
|
||||
.role-list {
|
||||
.el-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<el-form :model="dataForm" :rules="rules" ref="dataFormRef" @keyup.enter="dataFormSubmitHandle()" label-width="120px">
|
||||
<el-form-item label="账号">
|
||||
<span>{{ user.username }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password" label="原密码">
|
||||
<el-input v-model="dataForm.password" type="password" placeholder="原密码"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="newPassword" label="新密码">
|
||||
<el-input v-model="dataForm.newPassword" type="password" placeholder="新密码"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="confirmPassword" label="确认密码">
|
||||
<el-input v-model="dataForm.confirmPassword" type="password" placeholder="确认密码"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="dataFormSubmitHandle">确定</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { IObject } from "@/types/interface";
|
||||
import baseService from "@/service/baseService";
|
||||
import { useAppStore } from "@/store";
|
||||
import { useRouter } from "vue-router";
|
||||
import { ElMessage } from "element-plus";
|
||||
const router = useRouter();
|
||||
|
||||
const dataFormRef = ref();
|
||||
const dataForm = reactive({
|
||||
password: "",
|
||||
newPassword: "",
|
||||
confirmPassword: ""
|
||||
});
|
||||
|
||||
const store = useAppStore();
|
||||
const user = computed(() => store.state.user);
|
||||
|
||||
const validateConfirmPassword = (rule: IObject, value: string, callback: (e?: Error) => any) => {
|
||||
if (dataForm.newPassword !== value) {
|
||||
return callback(new Error("确认密码与新密码输入不一致"));
|
||||
}
|
||||
callback();
|
||||
};
|
||||
|
||||
const rules = ref({
|
||||
password: [{ required: true, message: "必填项不能为空", trigger: "blur" }],
|
||||
newPassword: [{ required: true, message: "必填项不能为空", trigger: "blur" }],
|
||||
confirmPassword: [
|
||||
{ required: true, message: "必填项不能为空", trigger: "blur" },
|
||||
{ validator: validateConfirmPassword, trigger: "blur" }
|
||||
]
|
||||
});
|
||||
|
||||
// 表单提交
|
||||
const dataFormSubmitHandle = () => {
|
||||
dataFormRef.value.validate((valid: boolean) => {
|
||||
if (!valid) {
|
||||
return false;
|
||||
}
|
||||
baseService.put("/sys/user/password", dataForm).then((res) => {
|
||||
ElMessage.success({
|
||||
message: "成功",
|
||||
duration: 500,
|
||||
onClose: () => {
|
||||
router.replace("/login");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div class="mod-sys__user">
|
||||
<el-form :inline="true" :model="state.dataForm" @keyup.enter="state.getDataList()">
|
||||
<el-form-item>
|
||||
<el-input v-model="state.dataForm.username" placeholder="用户名" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<sys-select v-model="state.dataForm.gender" dict-type="gender" placeholder="性别"></sys-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<sys-dept-tree v-model="state.dataForm.deptId" placeholder="选择部门" :query="true"></sys-dept-tree>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="state.getDataList()">查询</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button v-if="state.hasPermission('sys:user:save')" type="primary" @click="addOrUpdateHandle()">新增</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button v-if="state.hasPermission('sys:user:delete')" type="danger" @click="state.deleteHandle()">删除</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button v-if="state.hasPermission('sys:user:export')" type="info" @click="state.exportHandle()">导出</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table v-loading="state.dataListLoading" :data="state.dataList" border @selection-change="state.dataListSelectionChangeHandle" @sort-change="state.dataListSortChangeHandle" style="width: 100%">
|
||||
<el-table-column type="selection" header-align="center" align="center" width="50"></el-table-column>
|
||||
<el-table-column prop="username" label="用户名" sortable="custom" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="deptName" label="所属部门" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="email" label="邮箱" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="mobile" label="手机号" sortable="custom" header-align="center" align="center"></el-table-column>
|
||||
<el-table-column prop="gender" label="性别" sortable="custom" header-align="center" align="center">
|
||||
<template v-slot="scope">
|
||||
{{ state.getDictLabel("gender", scope.row.gender) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" sortable="custom" header-align="center" align="center">
|
||||
<template v-slot="scope">
|
||||
<el-tag v-if="scope.row.status === 0" size="small" type="danger">停用</el-tag>
|
||||
<el-tag v-else size="small" type="success">正常</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createDate" label="创建时间" sortable="custom" header-align="center" align="center" width="180"></el-table-column>
|
||||
<el-table-column label="操作" fixed="right" header-align="center" align="center" width="150">
|
||||
<template v-slot="scope">
|
||||
<el-button v-if="state.hasPermission('sys:user:update')" type="primary" link @click="addOrUpdateHandle(scope.row.id)">修改</el-button>
|
||||
<el-button v-if="state.hasPermission('sys:user:delete')" type="primary" link @click="state.deleteHandle(scope.row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination :current-page="state.page" :page-sizes="[10, 20, 50, 100]" :page-size="state.limit" :total="state.total" layout="total, sizes, prev, pager, next, jumper" @size-change="state.pageSizeChangeHandle" @current-change="state.pageCurrentChangeHandle"> </el-pagination>
|
||||
<!-- 弹窗, 新增 / 修改 -->
|
||||
<add-or-update ref="addOrUpdateRef" @refreshDataList="state.getDataList"></add-or-update>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useView from "@/hooks/useView";
|
||||
import { reactive, ref, toRefs } from "vue";
|
||||
import AddOrUpdate from "./user-add-or-update.vue";
|
||||
|
||||
const view = reactive({
|
||||
getDataListURL: "/sys/user/page",
|
||||
getDataListIsPage: true,
|
||||
deleteURL: "/sys/user",
|
||||
deleteIsBatch: true,
|
||||
exportURL: "/sys/user/export",
|
||||
dataForm: {
|
||||
username: "",
|
||||
deptId: "",
|
||||
postId: "",
|
||||
gender: ""
|
||||
}
|
||||
});
|
||||
|
||||
const state = reactive({ ...useView(view), ...toRefs(view) });
|
||||
|
||||
const addOrUpdateRef = ref();
|
||||
const addOrUpdateHandle = (id?: number) => {
|
||||
addOrUpdateRef.value.init(id);
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<el-card class="color-card">
|
||||
<h3>调色盘</h3>
|
||||
|
||||
<div class="color-picker-row">
|
||||
<div class="static-picker-container">
|
||||
<el-color-picker
|
||||
v-model="color"
|
||||
show-alpha
|
||||
:predefine="predefineColors"
|
||||
:teleported="false"
|
||||
popper-class="inline-picker-popper"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="color-info">
|
||||
<el-input
|
||||
v-model="hexInput"
|
||||
placeholder="输入 HEX"
|
||||
@change="updateColorFromHex"
|
||||
size="small"
|
||||
style="margin-bottom: 10px;"
|
||||
/>
|
||||
<div class="preview" :style="{ background: color }"></div>
|
||||
<p>HEX: {{ toHex(color) }}</p>
|
||||
<p>RGB: {{ hexToRgb(color) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from "vue"
|
||||
|
||||
// 注意:这里不需要 import 任何 Panel,让插件自动处理 el-color-picker
|
||||
const color = ref("#409EFF")
|
||||
const hexInput = ref(color.value)
|
||||
const predefineColors = ["#409EFF", "#67C23A", "#E6A23C", "#F56C6C", "#909399"]
|
||||
|
||||
// --- 转换逻辑保持不变 ---
|
||||
watch(color, (val) => {
|
||||
hexInput.value = toHex(val)
|
||||
})
|
||||
|
||||
function updateColorFromHex() {
|
||||
if (/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(hexInput.value)) {
|
||||
color.value = hexInput.value
|
||||
}
|
||||
}
|
||||
|
||||
function hexToRgb(hex) {
|
||||
if (!hex || typeof hex !== 'string') return ''
|
||||
if (hex.startsWith('rgb')) return hex
|
||||
let c = hex.substring(1).split('')
|
||||
if (c.length === 3) c = [c[0], c[0], c[1], c[1], c[2], c[2]]
|
||||
const r = parseInt(c[0] + c[1], 16)
|
||||
const g = parseInt(c[2] + c[3], 16)
|
||||
const b = parseInt(c[4] + c[5], 16)
|
||||
return `rgb(${r}, ${g}, ${b})`
|
||||
}
|
||||
|
||||
function toHex(value) {
|
||||
if (!value || typeof value !== 'string') return ''
|
||||
if (value.startsWith("rgb")) {
|
||||
const nums = value.match(/\d+/g)
|
||||
if (nums && nums.length >= 3) {
|
||||
return "#" + nums.slice(0, 3).map(n => parseInt(n).toString(16).padStart(2, "0")).join("").toUpperCase()
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 1. 隐藏选择器本身的按钮小方块 */
|
||||
.static-picker-container :deep(.el-color-picker__trigger) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 2. 强行让本应弹出的面板显示出来 */
|
||||
.static-picker-container :deep(.el-color-picker__panel),
|
||||
.static-picker-container :deep(.inline-picker-popper) {
|
||||
display: block !important;
|
||||
position: relative !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
box-shadow: none !important;
|
||||
border: 1px solid #dcdfe6;
|
||||
}
|
||||
|
||||
/* 3. 布局微调 */
|
||||
.color-picker-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.preview {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ddd;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.color-card {
|
||||
max-width: 100%;
|
||||
margin: 20px auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,596 @@
|
||||
<template>
|
||||
<div class="mod-weather__real-time-monitoring" v-loading="state.loading">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2 class="page-title">631气象信息</h2>
|
||||
</div>
|
||||
<div class="page-header-actions">
|
||||
<el-tag type="info">层级 {{ state.selectedPath.length }} 级</el-tag>
|
||||
<el-tag type="success">文件 {{ state.fileItems.length }}</el-tag>
|
||||
<el-button type="primary" plain icon="Refresh" @click="refresh">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-alert v-if="state.errorMessage" :title="state.errorMessage" type="error" :closable="false" show-icon class="mb-16" />
|
||||
|
||||
<div class="dept-bar" v-if="levelRows.length">
|
||||
<div class="level-row" v-for="(row, rowIdx) in levelRows" :key="rowIdx">
|
||||
<span class="level-label" v-if="row.label">{{ row.label }}</span>
|
||||
<div class="level-tabs">
|
||||
<div v-for="tab in row.tabs" :key="tab.id" class="level-tab" :class="[{ 'is-active': row.selectedId === tab.id }, `level-${rowIdx}`]" @click="selectTab(rowIdx, tab)">
|
||||
{{ tab.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-body" v-if="state.fileItems.length">
|
||||
<aside class="file-list-panel">
|
||||
<div v-for="(item, index) in state.fileItems" :key="item.anchorId" class="file-list-item" :class="{ 'is-active': state.selectedFileIndex === index }" @click="selectFile(index)">
|
||||
<div class="file-list-item__thumb">
|
||||
<span class="file-list-item__icon file-list-item__icon--txt">文</span>
|
||||
</div>
|
||||
<span class="file-list-item__name">{{ item.displayName || `文件-${item.fileId}` }}</span>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="preview-panel" v-if="previewItem">
|
||||
<div class="preview-panel__header">
|
||||
<h3 class="preview-panel__title">{{ previewItem.displayName || `文件-${previewItem.fileId}` }}</h3>
|
||||
<el-button type="primary" plain size="small" :loading="state.copyingId === previewItem.anchorId" @click="copyText(previewItem)"> 复制全部文本 </el-button>
|
||||
</div>
|
||||
|
||||
<div class="preview-panel__body">
|
||||
<div class="preview-panel__text preview-panel__text--full">{{ previewItem.content || "暂无文本内容" }}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="preview-panel preview-panel--empty" v-else>
|
||||
<el-empty description="请在左侧选择要预览的文件" :image-size="120" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-else-if="!state.loading && !state.errorMessage" description="暂无可供展示的文件" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import app from "@/constants/app";
|
||||
import baseService from "@/service/baseService";
|
||||
import { copyToClipboard } from "@/utils/utils";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { computed, onMounted, reactive } from "vue";
|
||||
|
||||
interface DeptNode {
|
||||
id: string;
|
||||
name: string;
|
||||
children?: DeptNode[];
|
||||
}
|
||||
|
||||
interface DeptTab {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface DeptFileGroupVO {
|
||||
deptId: string;
|
||||
deptName: string;
|
||||
fileList: { displayName: string; fileId: string; type: string; content: string }[];
|
||||
}
|
||||
|
||||
interface FileItem {
|
||||
deptId: string;
|
||||
deptName: string;
|
||||
displayName: string;
|
||||
fileId: string;
|
||||
type: string;
|
||||
content: string;
|
||||
anchorId: string;
|
||||
}
|
||||
|
||||
const IMAGE_EXTS = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg"];
|
||||
|
||||
const isImageFile = (type: string) => {
|
||||
if (!type) return true;
|
||||
const lower = type.toLowerCase();
|
||||
return IMAGE_EXTS.some((ext) => lower.includes(ext));
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
loading: true,
|
||||
errorMessage: "",
|
||||
fileItems: [] as FileItem[],
|
||||
copyingId: "",
|
||||
deptTreeData: [] as DeptNode[],
|
||||
selectedPath: [] as string[],
|
||||
selectedFileIndex: 0
|
||||
});
|
||||
|
||||
const previewItem = computed(() => {
|
||||
if (state.selectedFileIndex >= 0 && state.selectedFileIndex < state.fileItems.length) {
|
||||
return state.fileItems[state.selectedFileIndex];
|
||||
}
|
||||
return state.fileItems.length > 0 ? state.fileItems[0] : null;
|
||||
});
|
||||
|
||||
const transformGroups = (list: DeptFileGroupVO[]) => {
|
||||
const items: FileItem[] = [];
|
||||
|
||||
list.forEach((group) => {
|
||||
(group.fileList || []).forEach((file) => {
|
||||
if (isImageFile(file.type)) return;
|
||||
items.push({
|
||||
deptId: group.deptId,
|
||||
deptName: group.deptName,
|
||||
displayName: file.displayName,
|
||||
fileId: file.fileId,
|
||||
type: file.type,
|
||||
content: file.content || "",
|
||||
anchorId: `txt-${file.fileId}`
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
state.fileItems = items;
|
||||
state.selectedFileIndex = 0;
|
||||
};
|
||||
|
||||
const activeDeptId = computed(() => {
|
||||
if (state.selectedPath.length > 0) {
|
||||
return state.selectedPath[state.selectedPath.length - 1];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
interface LevelRow {
|
||||
tabs: DeptTab[];
|
||||
selectedId: string | null;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const LEVEL_LABELS = ["一级", "二级", "三级", "四级", "五级", "六级"];
|
||||
|
||||
const levelRows = computed<LevelRow[]>(() => {
|
||||
const rows: LevelRow[] = [];
|
||||
let nodes: DeptNode[] = state.deptTreeData;
|
||||
|
||||
for (let i = 0; i < state.selectedPath.length; i++) {
|
||||
const selId = state.selectedPath[i];
|
||||
rows.push({
|
||||
tabs: nodes.map((n) => ({ id: n.id, name: n.name })),
|
||||
selectedId: selId,
|
||||
label: LEVEL_LABELS[i] || `${i + 1}级`
|
||||
});
|
||||
const selNode = nodes.find((n) => n.id === selId);
|
||||
if (selNode?.children?.length) {
|
||||
nodes = selNode.children;
|
||||
} else {
|
||||
nodes = [];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (nodes.length > 0) {
|
||||
rows.push({
|
||||
tabs: nodes.map((n) => ({ id: n.id, name: n.name })),
|
||||
selectedId: null,
|
||||
label: LEVEL_LABELS[rows.length] || `${rows.length + 1}级`
|
||||
});
|
||||
}
|
||||
|
||||
return rows;
|
||||
});
|
||||
|
||||
const loadData = async (deptId: string) => {
|
||||
state.loading = true;
|
||||
state.errorMessage = "";
|
||||
try {
|
||||
const res = await baseService.get("/filescan/record/tree", { deptId });
|
||||
transformGroups((res.data || []) as DeptFileGroupVO[]);
|
||||
selectFile(0);
|
||||
} catch (error: any) {
|
||||
state.errorMessage = error?.message || "加载失败";
|
||||
state.fileItems = [];
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadDeptTree = async (): Promise<string | null> => {
|
||||
try {
|
||||
const res = await baseService.get("/sys/dept/list");
|
||||
const treeData = (res.data || []) as DeptNode[];
|
||||
state.deptTreeData = treeData;
|
||||
|
||||
if (!treeData.length) return null;
|
||||
|
||||
const firstRoot = treeData[0];
|
||||
const firstChild = firstRoot.children?.length ? firstRoot.children[0] : firstRoot;
|
||||
|
||||
const buildPath = (nodes: DeptNode[], targetId: string, path: string[]): string[] | null => {
|
||||
for (const node of nodes) {
|
||||
const newPath = [...path, node.id];
|
||||
if (node.id === targetId) return newPath;
|
||||
if (node.children?.length) {
|
||||
const found = buildPath(node.children, targetId, newPath);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const path = buildPath(treeData, firstChild.id, []);
|
||||
state.selectedPath = path || [firstRoot.id];
|
||||
return firstChild.id;
|
||||
} catch {
|
||||
state.deptTreeData = [];
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const selectTab = (levelIdx: number, tab: DeptTab) => {
|
||||
state.selectedPath = [...state.selectedPath.slice(0, levelIdx), tab.id];
|
||||
state.fileItems = [];
|
||||
state.selectedFileIndex = 0;
|
||||
loadData(tab.id);
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
if (activeDeptId.value !== null) {
|
||||
loadData(activeDeptId.value);
|
||||
}
|
||||
};
|
||||
|
||||
const selectFile = (index: number) => {
|
||||
state.selectedFileIndex = index;
|
||||
};
|
||||
|
||||
const copyText = (item: FileItem) => {
|
||||
state.copyingId = item.anchorId;
|
||||
copyToClipboard(item.content || "");
|
||||
ElMessage.success("文本已复制成功");
|
||||
state.copyingId = "";
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
const deptId = await loadDeptTree();
|
||||
if (deptId !== null) {
|
||||
loadData(deptId);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mod-weather__real-time-monitoring {
|
||||
min-height: calc(100vh - 140px);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
margin: 8px 0 0;
|
||||
color: #909399;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.page-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.mb-16 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* ==== 部门选择栏 ==== */
|
||||
.dept-bar {
|
||||
margin-bottom: 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
padding: 10px 16px 8px;
|
||||
}
|
||||
|
||||
.level-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.level-row:last-child {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.level-label {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
flex-shrink: 0;
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
.level-tabs {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.level-tab {
|
||||
padding: 4px 16px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
background: #f5f7fa;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: color 0.2s, background 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.level-tab + .level-tab {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.level-tab:hover {
|
||||
color: var(--el-color-primary);
|
||||
background: #ecf5ff;
|
||||
}
|
||||
|
||||
.level-tab.is-active {
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.level-tab.is-active.level-0 {
|
||||
background: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.level-tab.is-active.level-1 {
|
||||
background: var(--el-color-primary-light-3);
|
||||
border-color: var(--el-color-primary-light-3);
|
||||
}
|
||||
|
||||
.level-tab.is-active.level-2 {
|
||||
background: var(--el-color-success);
|
||||
border-color: var(--el-color-success);
|
||||
}
|
||||
|
||||
.level-tab.is-active.level-3 {
|
||||
background: var(--el-color-warning);
|
||||
border-color: var(--el-color-warning);
|
||||
}
|
||||
|
||||
.level-tab.is-active.level-4 {
|
||||
background: var(--el-color-danger);
|
||||
border-color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.level-tab.is-active.level-5 {
|
||||
background: #8b5cf6;
|
||||
border-color: #8b5cf6;
|
||||
}
|
||||
|
||||
/* ==== 主体两栏 ==== */
|
||||
.page-body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* ==== 左侧文件列表 ==== */
|
||||
.file-list-panel {
|
||||
position: sticky;
|
||||
top: 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
max-height: calc(100vh - 320px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.file-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.file-list-item:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.file-list-item.is-active {
|
||||
background: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.file-list-item__thumb {
|
||||
flex-shrink: 0;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: #f0f2f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.file-list-item__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.file-list-item__icon {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.file-list-item__icon--img {
|
||||
background: var(--el-color-success);
|
||||
}
|
||||
|
||||
.file-list-item__icon--txt {
|
||||
background: var(--el-color-warning);
|
||||
}
|
||||
|
||||
.file-list-item__icon--other {
|
||||
background: #c0c4cc;
|
||||
}
|
||||
|
||||
.file-list-item__name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ==== 右侧预览面板 ==== */
|
||||
.preview-panel {
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.preview-panel--empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.preview-panel__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.preview-panel__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.preview-panel__title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
color: #303133;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.preview-panel__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.preview-panel__loading {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
padding: 60px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.preview-zoom-area {
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background: #f5f7fa;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.preview-panel__img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 60vh;
|
||||
border-radius: 8px;
|
||||
user-select: none;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.preview-panel__text--full {
|
||||
min-height: 200px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.preview-panel__text {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
line-height: 1.8;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
padding: 12px 14px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.image-card__error,
|
||||
.image-card__unsupported {
|
||||
color: #e6a23c;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
padding: 12px 14px;
|
||||
background: #fdf6ec;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* ==== 响应式 ==== */
|
||||
@media (max-width: 992px) {
|
||||
.page-body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-header,
|
||||
.preview-panel__header,
|
||||
.preview-panel__actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.level-tab {
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,583 @@
|
||||
<template>
|
||||
<div class="mod-weather__prediction" v-loading="state.loading">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2 class="page-title">中尺度模式回波预报</h2>
|
||||
</div>
|
||||
<div class="page-header-actions">
|
||||
<el-tag type="success">文件 {{ state.fileItems.length }}</el-tag>
|
||||
<el-button type="primary" plain icon="Refresh" @click="loadData">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-alert v-if="state.errorMessage" :title="state.errorMessage" type="error" :closable="false" show-icon class="mb-16" />
|
||||
|
||||
<div class="page-body" v-if="state.fileItems.length">
|
||||
<aside class="file-list-panel">
|
||||
<div v-for="(item, index) in state.fileItems" :key="item.anchorId" class="file-list-item" :class="{ 'is-active': state.selectedFileIndex === index }" @click="selectFile(index)">
|
||||
<div class="file-list-item__thumb">
|
||||
<img v-if="blobUrlCache[item.fileId]" :src="blobUrlCache[item.fileId]" class="file-list-item__img" alt="{{item.displayName}}}" />
|
||||
<span v-else-if="isImageFile(item.type)" class="file-list-item__icon file-list-item__icon--img">GIF</span>
|
||||
<span v-else class="file-list-item__icon file-list-item__icon--other">—</span>
|
||||
</div>
|
||||
<span class="file-list-item__name">{{ item.displayName || `文件-${item.fileId}` }}</span>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="preview-panel" v-if="previewItem">
|
||||
<div class="preview-panel__header">
|
||||
<h3 class="preview-panel__title">{{ previewItem.displayName || `文件-${previewItem.fileId}` }}</h3>
|
||||
<div class="preview-panel__actions">
|
||||
<el-button type="primary" plain size="small" :loading="state.copyingId === previewItem.anchorId" @click="copyImage(previewItem)"> 复制图片 </el-button>
|
||||
<el-button type="success" plain size="small" @click="downloadFile(previewItem)"> 下载原图 </el-button>
|
||||
<el-button type="warning" plain size="small" @click="openFullscreen"> 全屏预览 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-panel__body">
|
||||
<div v-if="!previewBlobUrl && selectedFileId" class="preview-panel__loading">加载中...</div>
|
||||
<div v-show="previewBlobUrl" class="preview-zoom-area">
|
||||
<img :src="previewBlobUrl" :alt="previewItem.displayName" class="preview-panel__img" draggable="false" @click="openFullscreen" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="preview-panel preview-panel--empty" v-else>
|
||||
<el-empty description="请在左侧选择要预览的文件" :image-size="120" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-else-if="!state.loading && !state.errorMessage" description="暂无可供展示的文件" />
|
||||
|
||||
<!-- 全屏预览 -->
|
||||
<teleport to="body">
|
||||
<div v-if="fullscreenVisible" class="fullscreen-overlay" @click="closeFullscreen">
|
||||
<div class="fullscreen-overlay__toolbar">
|
||||
<span class="fullscreen-overlay__hint">滚轮缩放 · 拖拽移动 · 双击重置</span>
|
||||
<el-button circle size="large" @click="closeFullscreen">✕</el-button>
|
||||
</div>
|
||||
<img
|
||||
:src="previewBlobUrl"
|
||||
:alt="previewItem?.displayName"
|
||||
:style="fsZoomStyle"
|
||||
class="fullscreen-overlay__img"
|
||||
@click.stop
|
||||
@wheel.prevent="fsOnWheel"
|
||||
@mousedown="fsOnPanStart"
|
||||
@dblclick="fsResetZoom"
|
||||
draggable="false"
|
||||
/>
|
||||
</div>
|
||||
</teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import app from "@/constants/app";
|
||||
import { CacheToken } from "@/constants/cacheKey";
|
||||
import baseService from "@/service/baseService";
|
||||
import { getCache } from "@/utils/cache";
|
||||
import { ElMessage } from "element-plus";
|
||||
import axios from "axios";
|
||||
import { computed, onMounted, onUnmounted, reactive, ref } from "vue";
|
||||
|
||||
interface ModelFileItemVO {
|
||||
type: string;
|
||||
display_name: string;
|
||||
file_name: string;
|
||||
file_id: string;
|
||||
}
|
||||
|
||||
interface ModelFileGroupVO {
|
||||
type: string;
|
||||
file_list: ModelFileItemVO[];
|
||||
}
|
||||
|
||||
interface FileItem {
|
||||
type: string;
|
||||
displayName: string;
|
||||
fileId: string;
|
||||
anchorId: string;
|
||||
}
|
||||
|
||||
const state = reactive({
|
||||
loading: true,
|
||||
errorMessage: "",
|
||||
fileItems: [] as FileItem[],
|
||||
copyingId: "",
|
||||
selectedFileIndex: 0
|
||||
});
|
||||
|
||||
const blobUrlCache = {} as Record<string, string>;
|
||||
const blobPromises = {} as Record<string, Promise<string> | undefined>;
|
||||
const previewBlobUrl = ref("");
|
||||
const selectedFileId = ref("");
|
||||
|
||||
const IMAGE_EXTS = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg"];
|
||||
|
||||
const isImageFile = (type: string) => {
|
||||
if (!type) return true;
|
||||
const lower = type.toLowerCase();
|
||||
return IMAGE_EXTS.some((ext) => lower.includes(ext));
|
||||
};
|
||||
|
||||
const previewItem = computed(() => {
|
||||
if (state.selectedFileIndex >= 0 && state.selectedFileIndex < state.fileItems.length) {
|
||||
return state.fileItems[state.selectedFileIndex];
|
||||
}
|
||||
return state.fileItems.length > 0 ? state.fileItems[0] : null;
|
||||
});
|
||||
|
||||
const loadBlobUrl = (fileId: string): Promise<string> => {
|
||||
if (blobUrlCache[fileId]) return Promise.resolve(blobUrlCache[fileId]);
|
||||
if (blobPromises[fileId]) return blobPromises[fileId];
|
||||
|
||||
const base = (app.api || "").replace(/\/$/, "");
|
||||
const url = `${base}/filescan/file/display/${fileId}`;
|
||||
const token = getCache(CacheToken, { isSessionStorage: true }, {})?.token;
|
||||
const headers: Record<string, string> = {};
|
||||
if (token) headers.token = token;
|
||||
|
||||
const promise = axios
|
||||
.get(url, { responseType: "blob", headers })
|
||||
.then((res) => {
|
||||
const objectUrl = URL.createObjectURL(res.data as Blob);
|
||||
blobUrlCache[fileId] = objectUrl;
|
||||
return objectUrl;
|
||||
})
|
||||
.finally(() => {
|
||||
delete blobPromises[fileId];
|
||||
});
|
||||
|
||||
blobPromises[fileId] = promise;
|
||||
return promise;
|
||||
};
|
||||
|
||||
const selectFile = async (index: number) => {
|
||||
state.selectedFileIndex = index;
|
||||
const item = state.fileItems[index];
|
||||
if (!item) return;
|
||||
|
||||
selectedFileId.value = item.fileId;
|
||||
previewBlobUrl.value = "";
|
||||
|
||||
previewBlobUrl.value = await loadBlobUrl(item.fileId);
|
||||
};
|
||||
|
||||
// 全屏预览
|
||||
const fullscreenVisible = ref(false);
|
||||
|
||||
const fsZoomScale = ref(1);
|
||||
const fsPanX = ref(0);
|
||||
const fsPanY = ref(0);
|
||||
const fsIsPanning = ref(false);
|
||||
let fsPanStartX = 0;
|
||||
let fsPanStartY = 0;
|
||||
let fsPanOriginX = 0;
|
||||
let fsPanOriginY = 0;
|
||||
|
||||
const fsZoomStyle = computed(() => ({
|
||||
transform: `translate(${fsPanX.value}px, ${fsPanY.value}px) scale(${fsZoomScale.value})`,
|
||||
cursor: fsIsPanning.value ? "grabbing" : fsZoomScale.value > 1 ? "grab" : "default",
|
||||
transition: fsIsPanning.value ? "none" : "transform 0.15s ease"
|
||||
}));
|
||||
|
||||
const fsResetZoom = () => {
|
||||
fsZoomScale.value = 1;
|
||||
fsPanX.value = 0;
|
||||
fsPanY.value = 0;
|
||||
};
|
||||
|
||||
const openFullscreen = () => {
|
||||
fsResetZoom();
|
||||
fullscreenVisible.value = true;
|
||||
};
|
||||
|
||||
const closeFullscreen = () => {
|
||||
fullscreenVisible.value = false;
|
||||
};
|
||||
|
||||
const fsOnWheel = (e: WheelEvent) => {
|
||||
e.preventDefault();
|
||||
const delta = e.deltaY > 0 ? -0.15 : 0.15;
|
||||
const newScale = Math.max(0.2, Math.min(8, fsZoomScale.value + delta));
|
||||
fsZoomScale.value = newScale;
|
||||
if (newScale <= 1) {
|
||||
fsPanX.value = 0;
|
||||
fsPanY.value = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const fsOnPanStart = (e: MouseEvent) => {
|
||||
if (fsZoomScale.value <= 1) return;
|
||||
fsIsPanning.value = true;
|
||||
fsPanStartX = e.clientX;
|
||||
fsPanStartY = e.clientY;
|
||||
fsPanOriginX = fsPanX.value;
|
||||
fsPanOriginY = fsPanY.value;
|
||||
document.addEventListener("mousemove", fsOnPanMove);
|
||||
document.addEventListener("mouseup", fsOnPanEnd);
|
||||
};
|
||||
|
||||
const fsOnPanMove = (e: MouseEvent) => {
|
||||
if (!fsIsPanning.value) return;
|
||||
fsPanX.value = fsPanOriginX + (e.clientX - fsPanStartX);
|
||||
fsPanY.value = fsPanOriginY + (e.clientY - fsPanStartY);
|
||||
};
|
||||
|
||||
const fsOnPanEnd = () => {
|
||||
fsIsPanning.value = false;
|
||||
document.removeEventListener("mousemove", fsOnPanMove);
|
||||
document.removeEventListener("mouseup", fsOnPanEnd);
|
||||
};
|
||||
|
||||
const downloadFile = async (item: FileItem) => {
|
||||
const filename = item.displayName || `file-${item.fileId}`;
|
||||
await loadBlobUrl(item.fileId);
|
||||
const a = document.createElement("a");
|
||||
a.href = blobUrlCache[item.fileId];
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
};
|
||||
|
||||
const copyImage = async (item: FileItem) => {
|
||||
if (!isImageFile(item.type)) {
|
||||
ElMessage.warning("当前文件不是图片类型,无法复制");
|
||||
return;
|
||||
}
|
||||
state.copyingId = item.anchorId;
|
||||
try {
|
||||
await loadBlobUrl(item.fileId);
|
||||
const blobUrl = blobUrlCache[item.fileId];
|
||||
const res = await fetch(blobUrl);
|
||||
const blob = await res.blob();
|
||||
try {
|
||||
await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
|
||||
} catch (e: any) {
|
||||
if (e.name === "NotAllowedError" || e.message?.includes?.("not supported")) {
|
||||
await convertToPngAndCopy(blob);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
ElMessage.success("图片已复制成功");
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || "复制图片失败");
|
||||
} finally {
|
||||
state.copyingId = "";
|
||||
}
|
||||
};
|
||||
|
||||
const convertToPngAndCopy = (blob: Blob): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = img.naturalWidth;
|
||||
canvas.height = img.naturalHeight;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return reject(new Error("Canvas 不可用"));
|
||||
ctx.drawImage(img, 0, 0);
|
||||
canvas.toBlob((pngBlob) => {
|
||||
if (pngBlob) {
|
||||
navigator.clipboard
|
||||
.write([new ClipboardItem({ "image/png": pngBlob })])
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
} else {
|
||||
reject(new Error("转 PNG 失败"));
|
||||
}
|
||||
}, "image/png");
|
||||
};
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
reject(new Error("图片加载失败"));
|
||||
};
|
||||
img.src = url;
|
||||
});
|
||||
};
|
||||
|
||||
const loadData = async () => {
|
||||
state.loading = true;
|
||||
state.errorMessage = "";
|
||||
try {
|
||||
const res = await baseService.get("/filescan/record/model");
|
||||
const data = res.data as ModelFileGroupVO;
|
||||
const items: FileItem[] = (data.file_list || []).map((file) => ({
|
||||
type: file.type || "",
|
||||
displayName: file.display_name || file.file_name || "",
|
||||
fileId: String(file.file_id),
|
||||
anchorId: `pred-${file.file_id}`
|
||||
}));
|
||||
|
||||
state.fileItems = items;
|
||||
state.selectedFileIndex = 0;
|
||||
|
||||
const preloads = items.filter((item) => isImageFile(item.type)).map((item) => loadBlobUrl(item.fileId));
|
||||
if (items.length > 0) {
|
||||
selectFile(0);
|
||||
}
|
||||
await Promise.allSettled(preloads);
|
||||
} catch (error: any) {
|
||||
state.errorMessage = error?.message || "加载模式预报文件失败";
|
||||
state.fileItems = [];
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
Object.values(blobUrlCache).forEach((url) => URL.revokeObjectURL(url));
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mod-weather__prediction {
|
||||
min-height: calc(100vh - 140px);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.page-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.mb-16 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* ==== 主体两栏 ==== */
|
||||
.page-body {
|
||||
display: grid;
|
||||
grid-template-columns: 25% minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* ==== 左侧文件列表 ==== */
|
||||
.file-list-panel {
|
||||
position: sticky;
|
||||
top: 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
max-height: calc(100vh - 220px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.file-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.file-list-item:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.file-list-item.is-active {
|
||||
background: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.file-list-item__thumb {
|
||||
flex-shrink: 0;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: #f0f2f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.file-list-item__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.file-list-item__icon {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.file-list-item__icon--img {
|
||||
background: var(--el-color-success);
|
||||
}
|
||||
|
||||
.file-list-item__icon--other {
|
||||
background: #c0c4cc;
|
||||
}
|
||||
|
||||
.file-list-item__name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ==== 右侧预览面板 ==== */
|
||||
.preview-panel {
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.preview-panel--empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.preview-panel__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.preview-panel__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.preview-panel__title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
color: #303133;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.preview-panel__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.preview-panel__loading {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
padding: 60px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.preview-zoom-area {
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background: #f5f7fa;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.preview-panel__img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 70vh;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ==== 全屏预览遮罩 ==== */
|
||||
.fullscreen-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 3000;
|
||||
background: rgba(0, 0, 0, 0.92);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.fullscreen-overlay__toolbar {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 3001;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.fullscreen-overlay__hint {
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
font-size: 13px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.fullscreen-overlay__img {
|
||||
display: block;
|
||||
max-width: 95vw;
|
||||
max-height: 95vh;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* ==== 响应式 ==== */
|
||||
@media (max-width: 992px) {
|
||||
.page-body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.file-list-panel {
|
||||
position: static;
|
||||
max-height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-header,
|
||||
.preview-panel__header,
|
||||
.preview-panel__actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,815 @@
|
||||
<template>
|
||||
<div class="mod-weather__real-time-monitoring" v-loading="state.loading">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2 class="page-title">气候实时监测</h2>
|
||||
</div>
|
||||
<div class="page-header-actions">
|
||||
<el-tag type="info">层级 {{ state.selectedPath.length }} 级</el-tag>
|
||||
<el-tag type="success">文件 {{ state.imageItems.length }}</el-tag>
|
||||
<el-button type="primary" plain icon="Refresh" @click="refresh">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-alert v-if="state.errorMessage" :title="state.errorMessage" type="error" :closable="false" show-icon class="mb-16" />
|
||||
|
||||
<div class="dept-bar" v-if="levelRows.length">
|
||||
<div class="level-row" v-for="(row, rowIdx) in levelRows" :key="rowIdx">
|
||||
<span class="level-label" v-if="row.label">{{ row.label }}</span>
|
||||
<div class="level-tabs">
|
||||
<div v-for="tab in row.tabs" :key="tab.id" class="level-tab" :class="[{ 'is-active': row.selectedId === tab.id }, `level-${rowIdx}`]" @click="selectTab(rowIdx, tab)">
|
||||
{{ tab.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-body" v-if="state.imageItems.length">
|
||||
<aside class="file-list-panel">
|
||||
<div v-for="(item, index) in state.imageItems" :key="item.anchorId" class="file-list-item" :class="{ 'is-active': state.selectedFileIndex === index }" @click="selectFile(index)">
|
||||
<div class="file-list-item__thumb">
|
||||
<img v-if="blobUrlCache[item.fileId]" :src="blobUrlCache[item.fileId]" class="file-list-item__img" />
|
||||
<span v-else-if="isImageFile(item.type)" class="file-list-item__icon file-list-item__icon--img">图</span>
|
||||
<span v-else-if="isTextFile(item.type)" class="file-list-item__icon file-list-item__icon--txt">文</span>
|
||||
<span v-else class="file-list-item__icon file-list-item__icon--other">—</span>
|
||||
</div>
|
||||
<span class="file-list-item__name">{{ item.displayName || `文件-${item.fileId}` }}</span>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="preview-panel" v-if="previewItem">
|
||||
<div class="preview-panel__header">
|
||||
<h3 class="preview-panel__title">{{ previewItem.displayName || `文件-${previewItem.fileId}` }}</h3>
|
||||
<div class="preview-panel__actions">
|
||||
<el-button v-if="fileMode === 'image'" type="primary" plain size="small" :loading="state.copyingId === previewItem.anchorId" @click="copyImage(previewItem)"> 复制图片 </el-button>
|
||||
<el-button v-else-if="fileMode === 'text'" type="primary" plain size="small" :loading="state.copyingId === previewItem.anchorId" @click="copyText(previewItem)"> 复制全部文本 </el-button>
|
||||
<el-button v-if="fileMode === 'image'" type="success" plain size="small" @click="downloadFile(previewItem)"> 下载原图 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-panel__body" v-if="fileMode === 'image'">
|
||||
<div v-if="!previewBlobUrl && selectedFileId" class="preview-panel__loading">加载中...</div>
|
||||
<div v-show="previewBlobUrl" class="preview-zoom-area" @wheel.prevent="onWheel" @mousedown="onPanStart" @dblclick="resetZoom">
|
||||
<img :src="previewBlobUrl" :alt="previewItem.displayName" :style="zoomStyle" class="preview-panel__img" draggable="false" />
|
||||
</div>
|
||||
<div v-if="previewItem.content" class="preview-panel__text">{{ previewItem.content }}</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-panel__body" v-else-if="fileMode === 'text'">
|
||||
<div class="preview-panel__text preview-panel__text--full">{{ previewItem.content || "暂无文本内容" }}</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-panel__body" v-else-if="fileMode === 'unsupported'">
|
||||
<div class="image-card__unsupported">当前文件类型暂不支持预览</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="preview-panel preview-panel--empty" v-else>
|
||||
<el-empty description="请在左侧选择要预览的文件" :image-size="120" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-else-if="!state.loading && !state.errorMessage" description="暂无可展示图片" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import app from "@/constants/app";
|
||||
import { CacheToken } from "@/constants/cacheKey";
|
||||
import baseService from "@/service/baseService";
|
||||
import { getCache } from "@/utils/cache";
|
||||
import { copyToClipboard } from "@/utils/utils";
|
||||
import { ElMessage } from "element-plus";
|
||||
import axios from "axios";
|
||||
import { computed, onMounted, onUnmounted, reactive, ref } from "vue";
|
||||
|
||||
interface DeptNode {
|
||||
id: string;
|
||||
name: string;
|
||||
children?: DeptNode[];
|
||||
}
|
||||
|
||||
interface DeptTab {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface DeptFileItemVO {
|
||||
displayName: string;
|
||||
fileId: string;
|
||||
type: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface DeptFileGroupVO {
|
||||
deptId: string;
|
||||
deptName: string;
|
||||
fileList: DeptFileItemVO[];
|
||||
}
|
||||
|
||||
interface ImageItem {
|
||||
deptId: string;
|
||||
deptName: string;
|
||||
displayName: string;
|
||||
fileId: string;
|
||||
type: string;
|
||||
content: string;
|
||||
anchorId: string;
|
||||
}
|
||||
|
||||
const state = reactive({
|
||||
loading: true,
|
||||
errorMessage: "",
|
||||
imageItems: [] as ImageItem[],
|
||||
copyingId: "",
|
||||
deptTreeData: [] as DeptNode[],
|
||||
selectedPath: [] as string[],
|
||||
selectedFileIndex: 0
|
||||
});
|
||||
|
||||
const blobUrlCache = {} as Record<string, string>;
|
||||
const blobPromises = {} as Record<string, Promise<string> | undefined>;
|
||||
const previewBlobUrl = ref("");
|
||||
|
||||
const loadBlobUrl = (fileId: string): Promise<string> => {
|
||||
if (blobUrlCache[fileId]) return Promise.resolve(blobUrlCache[fileId]);
|
||||
if (blobPromises[fileId]) return blobPromises[fileId];
|
||||
|
||||
const base = (app.api || "").replace(/\/$/, "");
|
||||
const url = `${base}/filescan/file/display/${fileId}`;
|
||||
const token = getCache(CacheToken, { isSessionStorage: true }, {})?.token;
|
||||
const headers: Record<string, string> = {};
|
||||
if (token) headers.token = token;
|
||||
|
||||
const promise = axios
|
||||
.get(url, { responseType: "blob", headers })
|
||||
.then((res) => {
|
||||
const objectUrl = URL.createObjectURL(res.data as Blob);
|
||||
blobUrlCache[fileId] = objectUrl;
|
||||
return objectUrl;
|
||||
})
|
||||
.finally(() => {
|
||||
delete blobPromises[fileId];
|
||||
});
|
||||
|
||||
blobPromises[fileId] = promise;
|
||||
return promise;
|
||||
};
|
||||
|
||||
const activeDeptId = computed(() => {
|
||||
if (state.selectedPath.length > 0) {
|
||||
return state.selectedPath[state.selectedPath.length - 1];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
interface LevelRow {
|
||||
tabs: DeptTab[];
|
||||
selectedId: string | null;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const LEVEL_LABELS = ["一级", "二级", "三级", "四级", "五级", "六级"];
|
||||
|
||||
const levelRows = computed<LevelRow[]>(() => {
|
||||
const rows: LevelRow[] = [];
|
||||
let nodes: DeptNode[] = state.deptTreeData;
|
||||
|
||||
for (let i = 0; i < state.selectedPath.length; i++) {
|
||||
const selId = state.selectedPath[i];
|
||||
rows.push({
|
||||
tabs: nodes.map((n) => ({ id: n.id, name: n.name })),
|
||||
selectedId: selId,
|
||||
label: LEVEL_LABELS[i] || `${i + 1}级`
|
||||
});
|
||||
const selNode = nodes.find((n) => n.id === selId);
|
||||
if (selNode?.children?.length) {
|
||||
nodes = selNode.children;
|
||||
} else {
|
||||
nodes = [];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (nodes.length > 0) {
|
||||
rows.push({
|
||||
tabs: nodes.map((n) => ({ id: n.id, name: n.name })),
|
||||
selectedId: null,
|
||||
label: LEVEL_LABELS[rows.length] || `${rows.length + 1}级`
|
||||
});
|
||||
}
|
||||
|
||||
return rows;
|
||||
});
|
||||
|
||||
const previewItem = computed(() => {
|
||||
if (state.selectedFileIndex >= 0 && state.selectedFileIndex < state.imageItems.length) {
|
||||
return state.imageItems[state.selectedFileIndex];
|
||||
}
|
||||
return state.imageItems.length > 0 ? state.imageItems[0] : null;
|
||||
});
|
||||
|
||||
const IMAGE_EXTS = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg"];
|
||||
const TEXT_EXTS = ["txt", "md", "log", "csv", "json", "xml", "html", "htm", "yaml", "yml"];
|
||||
|
||||
const isImageFile = (type: string) => {
|
||||
if (!type) return true;
|
||||
const lower = type.toLowerCase();
|
||||
return IMAGE_EXTS.some((ext) => lower.includes(ext));
|
||||
};
|
||||
|
||||
const isTextFile = (type: string) => {
|
||||
if (!type) return false;
|
||||
const lower = type.toLowerCase();
|
||||
return TEXT_EXTS.some((ext) => lower.includes(ext));
|
||||
};
|
||||
|
||||
const fileMode = computed(() => {
|
||||
const item = previewItem.value;
|
||||
if (!item) return "none";
|
||||
if (isImageFile(item.type)) return "image";
|
||||
if (isTextFile(item.type)) return "text";
|
||||
return "unsupported";
|
||||
});
|
||||
|
||||
const zoomScale = ref(1);
|
||||
const panX = ref(0);
|
||||
const panY = ref(0);
|
||||
const isPanning = ref(false);
|
||||
let panStartX = 0;
|
||||
let panStartY = 0;
|
||||
let panOriginX = 0;
|
||||
let panOriginY = 0;
|
||||
|
||||
const zoomStyle = computed(() => ({
|
||||
transform: `translate(${panX.value}px, ${panY.value}px) scale(${zoomScale.value})`,
|
||||
cursor: isPanning.value ? "grabbing" : "grab",
|
||||
transition: isPanning.value ? "none" : "transform 0.15s ease"
|
||||
}));
|
||||
|
||||
const resetZoom = () => {
|
||||
zoomScale.value = 1;
|
||||
panX.value = 0;
|
||||
panY.value = 0;
|
||||
};
|
||||
|
||||
const onWheel = (e: WheelEvent) => {
|
||||
e.preventDefault();
|
||||
const delta = e.deltaY > 0 ? -0.15 : 0.15;
|
||||
const newScale = Math.max(0.2, Math.min(5, zoomScale.value + delta));
|
||||
zoomScale.value = newScale;
|
||||
if (newScale <= 1) {
|
||||
panX.value = 0;
|
||||
panY.value = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const onPanStart = (e: MouseEvent) => {
|
||||
if (zoomScale.value <= 1) return;
|
||||
isPanning.value = true;
|
||||
panStartX = e.clientX;
|
||||
panStartY = e.clientY;
|
||||
panOriginX = panX.value;
|
||||
panOriginY = panY.value;
|
||||
document.addEventListener("mousemove", onPanMove);
|
||||
document.addEventListener("mouseup", onPanEnd);
|
||||
};
|
||||
|
||||
const onPanMove = (e: MouseEvent) => {
|
||||
if (!isPanning.value) return;
|
||||
panX.value = panOriginX + (e.clientX - panStartX);
|
||||
panY.value = panOriginY + (e.clientY - panStartY);
|
||||
};
|
||||
|
||||
const onPanEnd = () => {
|
||||
isPanning.value = false;
|
||||
document.removeEventListener("mousemove", onPanMove);
|
||||
document.removeEventListener("mouseup", onPanEnd);
|
||||
};
|
||||
|
||||
const transformGroups = (list: DeptFileGroupVO[]) => {
|
||||
const imageItems: ImageItem[] = [];
|
||||
|
||||
list.forEach((group) => {
|
||||
(group.fileList || []).forEach((file) => {
|
||||
imageItems.push({
|
||||
deptId: group.deptId,
|
||||
deptName: group.deptName,
|
||||
displayName: file.displayName,
|
||||
fileId: file.fileId,
|
||||
type: file.type,
|
||||
content: file.content || "",
|
||||
anchorId: `img-${file.fileId}`
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
state.imageItems = imageItems;
|
||||
state.selectedFileIndex = 0;
|
||||
};
|
||||
|
||||
const loadData = async (deptId: string) => {
|
||||
state.loading = true;
|
||||
state.errorMessage = "";
|
||||
try {
|
||||
const res = await baseService.get("/filescan/record/tree", { deptId });
|
||||
transformGroups((res.data || []) as DeptFileGroupVO[]);
|
||||
const preloads = state.imageItems.filter((item) => isImageFile(item.type)).map((item) => loadBlobUrl(item.fileId));
|
||||
selectFile(0);
|
||||
await Promise.allSettled(preloads);
|
||||
} catch (error: any) {
|
||||
state.errorMessage = error?.message || "加载实时监测图片失败";
|
||||
state.imageItems = [];
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadDeptTree = async (): Promise<string | null> => {
|
||||
try {
|
||||
const res = await baseService.get("/sys/dept/list");
|
||||
const treeData = (res.data || []) as DeptNode[];
|
||||
state.deptTreeData = treeData;
|
||||
|
||||
if (!treeData.length) return null;
|
||||
|
||||
const firstRoot = treeData[0];
|
||||
const firstChild = firstRoot.children?.length ? firstRoot.children[0] : firstRoot;
|
||||
|
||||
const buildPath = (nodes: DeptNode[], targetId: string, path: string[]): string[] | null => {
|
||||
for (const node of nodes) {
|
||||
const newPath = [...path, node.id];
|
||||
if (node.id === targetId) return newPath;
|
||||
if (node.children?.length) {
|
||||
const found = buildPath(node.children, targetId, newPath);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const path = buildPath(treeData, firstChild.id, []);
|
||||
state.selectedPath = path || [firstRoot.id];
|
||||
return firstChild.id;
|
||||
} catch {
|
||||
state.deptTreeData = [];
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const selectTab = (levelIdx: number, tab: DeptTab) => {
|
||||
state.selectedPath = [...state.selectedPath.slice(0, levelIdx), tab.id];
|
||||
state.imageItems = [];
|
||||
state.selectedFileIndex = 0;
|
||||
loadData(tab.id);
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
if (activeDeptId.value !== null) {
|
||||
loadData(activeDeptId.value);
|
||||
}
|
||||
};
|
||||
|
||||
const selectedFileId = ref("");
|
||||
|
||||
const selectFile = async (index: number) => {
|
||||
state.selectedFileIndex = index;
|
||||
const item = state.imageItems[index];
|
||||
if (!item) return;
|
||||
|
||||
selectedFileId.value = item.fileId;
|
||||
resetZoom();
|
||||
|
||||
if (isImageFile(item.type)) {
|
||||
previewBlobUrl.value = "";
|
||||
const objectUrl = await loadBlobUrl(item.fileId);
|
||||
previewBlobUrl.value = objectUrl;
|
||||
} else {
|
||||
previewBlobUrl.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const downloadFile = async (item: ImageItem) => {
|
||||
const filename = item.displayName || `file-${item.fileId}`;
|
||||
await loadBlobUrl(item.fileId);
|
||||
const a = document.createElement("a");
|
||||
a.href = blobUrlCache[item.fileId];
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
};
|
||||
|
||||
const copyText = (item: ImageItem) => {
|
||||
state.copyingId = item.anchorId;
|
||||
copyToClipboard(item.content || "");
|
||||
ElMessage.success("文本已复制成功");
|
||||
state.copyingId = "";
|
||||
};
|
||||
|
||||
const copyImage = async (item: ImageItem) => {
|
||||
if (!isImageFile(item.type)) {
|
||||
ElMessage.warning("当前文件不是图片类型,无法复制");
|
||||
return;
|
||||
}
|
||||
state.copyingId = item.anchorId;
|
||||
try {
|
||||
await loadBlobUrl(item.fileId);
|
||||
const blobUrl = blobUrlCache[item.fileId];
|
||||
const res = await fetch(blobUrl);
|
||||
const blob = await res.blob();
|
||||
try {
|
||||
await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
|
||||
} catch (e: any) {
|
||||
if (e.name === "NotAllowedError" || e.message?.includes?.("not supported")) {
|
||||
await convertToPngAndCopy(blob);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
ElMessage.success("图片已复制成功");
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || "复制图片失败");
|
||||
} finally {
|
||||
state.copyingId = "";
|
||||
}
|
||||
};
|
||||
|
||||
const convertToPngAndCopy = (blob: Blob): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = img.naturalWidth;
|
||||
canvas.height = img.naturalHeight;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return reject(new Error("Canvas 不可用"));
|
||||
ctx.drawImage(img, 0, 0);
|
||||
canvas.toBlob((pngBlob) => {
|
||||
if (pngBlob) {
|
||||
navigator.clipboard.write([new ClipboardItem({ "image/png": pngBlob })]).then(resolve).catch(reject);
|
||||
} else {
|
||||
reject(new Error("转 PNG 失败"));
|
||||
}
|
||||
}, "image/png");
|
||||
};
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
reject(new Error("图片加载失败"));
|
||||
};
|
||||
img.src = url;
|
||||
});
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
Object.values(blobUrlCache).forEach((url) => URL.revokeObjectURL(url));
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
const deptId = await loadDeptTree();
|
||||
if (deptId !== null) {
|
||||
loadData(deptId);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mod-weather__real-time-monitoring {
|
||||
min-height: calc(100vh - 140px);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
margin: 8px 0 0;
|
||||
color: #909399;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.page-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.mb-16 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* ==== 部门选择栏 ==== */
|
||||
.dept-bar {
|
||||
margin-bottom: 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
padding: 10px 16px 8px;
|
||||
}
|
||||
|
||||
.level-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.level-row:last-child {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.level-label {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
flex-shrink: 0;
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
.level-tabs {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.level-tab {
|
||||
padding: 4px 16px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
background: #f5f7fa;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: color 0.2s, background 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.level-tab + .level-tab {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.level-tab:hover {
|
||||
color: var(--el-color-primary);
|
||||
background: #ecf5ff;
|
||||
}
|
||||
|
||||
.level-tab.is-active {
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.level-tab.is-active.level-0 {
|
||||
background: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.level-tab.is-active.level-1 {
|
||||
background: var(--el-color-primary-light-3);
|
||||
border-color: var(--el-color-primary-light-3);
|
||||
}
|
||||
|
||||
.level-tab.is-active.level-2 {
|
||||
background: var(--el-color-success);
|
||||
border-color: var(--el-color-success);
|
||||
}
|
||||
|
||||
.level-tab.is-active.level-3 {
|
||||
background: var(--el-color-warning);
|
||||
border-color: var(--el-color-warning);
|
||||
}
|
||||
|
||||
.level-tab.is-active.level-4 {
|
||||
background: var(--el-color-danger);
|
||||
border-color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.level-tab.is-active.level-5 {
|
||||
background: #8b5cf6;
|
||||
border-color: #8b5cf6;
|
||||
}
|
||||
|
||||
/* ==== 主体两栏 ==== */
|
||||
.page-body {
|
||||
display: grid;
|
||||
grid-template-columns: 25% minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* ==== 左侧文件列表 ==== */
|
||||
.file-list-panel {
|
||||
position: sticky;
|
||||
top: 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
max-height: calc(100vh - 320px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.file-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.file-list-item:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.file-list-item.is-active {
|
||||
background: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.file-list-item__thumb {
|
||||
flex-shrink: 0;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: #f0f2f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.file-list-item__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.file-list-item__icon {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.file-list-item__icon--img {
|
||||
background: var(--el-color-success);
|
||||
}
|
||||
|
||||
.file-list-item__icon--txt {
|
||||
background: var(--el-color-warning);
|
||||
}
|
||||
|
||||
.file-list-item__icon--other {
|
||||
background: #c0c4cc;
|
||||
}
|
||||
|
||||
.file-list-item__name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ==== 右侧预览面板 ==== */
|
||||
.preview-panel {
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.preview-panel--empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.preview-panel__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.preview-panel__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.preview-panel__title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
color: #303133;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.preview-panel__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.preview-panel__loading {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
padding: 60px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.preview-zoom-area {
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background: #f5f7fa;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.preview-panel__img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 60vh;
|
||||
border-radius: 8px;
|
||||
user-select: none;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.preview-panel__text--full {
|
||||
min-height: 200px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.preview-panel__text {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
line-height: 1.8;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
padding: 12px 14px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.image-card__error,
|
||||
.image-card__unsupported {
|
||||
color: #e6a23c;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
padding: 12px 14px;
|
||||
background: #fdf6ec;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* ==== 响应式 ==== */
|
||||
@media (max-width: 992px) {
|
||||
.page-body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.file-list-panel {
|
||||
position: static;
|
||||
max-height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-header,
|
||||
.preview-panel__header,
|
||||
.preview-panel__actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.level-tab {
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user