wip
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"tokens": {},
|
||||||
|
"components": {}
|
||||||
|
}
|
||||||
54
src/components/common/SearchInput.vue
Normal file
54
src/components/common/SearchInput.vue
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<template>
|
||||||
|
<el-input
|
||||||
|
v-model="keyword"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
clearable
|
||||||
|
class="search-input"
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
@clear="handleSearch"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { Search } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: String, default: '' },
|
||||||
|
placeholder: { type: String, default: '搜索网站、备注...' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'search'])
|
||||||
|
|
||||||
|
const keyword = ref(props.modelValue)
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (val) => {
|
||||||
|
keyword.value = val
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
emit('update:modelValue', keyword.value)
|
||||||
|
emit('search', keyword.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.search-input {
|
||||||
|
width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input :deep(.el-input__wrapper) {
|
||||||
|
border-radius: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1400px) {
|
||||||
|
.search-input { width: 260px; }
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.search-input { width: 100%; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -34,14 +34,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="关键词" class="search-keyword">
|
<el-form-item label="关键词" class="search-keyword">
|
||||||
<el-input v-model="localFilters.search" placeholder="搜索网站、备注" clearable @keyup.enter="handleSearch"
|
<SearchInput v-model="localFilters.search" @search="handleSearch" />
|
||||||
class="wide-search-input">
|
|
||||||
<template #prefix>
|
|
||||||
<el-icon>
|
|
||||||
<Search />
|
|
||||||
</el-icon>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,6 +63,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, watch, computed } from "vue";
|
import { reactive, watch, computed } from "vue";
|
||||||
import { Search, Sort } from "@element-plus/icons-vue";
|
import { Search, Sort } from "@element-plus/icons-vue";
|
||||||
|
import SearchInput from "@/components/common/SearchInput.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
filters: { type: Object, required: true },
|
filters: { type: Object, required: true },
|
||||||
@@ -159,8 +153,6 @@ const setSort = (order) => {
|
|||||||
:deep(.el-form-item__label) { color: var(--text-secondary); font-weight: 500; }
|
:deep(.el-form-item__label) { color: var(--text-secondary); font-weight: 500; }
|
||||||
|
|
||||||
.wide-select { width: 180px; }
|
.wide-select { width: 180px; }
|
||||||
.wide-search-input { width: 320px; }
|
|
||||||
|
|
||||||
.filter-main :deep(.el-input__wrapper),
|
.filter-main :deep(.el-input__wrapper),
|
||||||
.filter-main :deep(.el-button) { border-radius: 20px !important; }
|
.filter-main :deep(.el-button) { border-radius: 20px !important; }
|
||||||
:deep(.el-select-dropdown) { border-radius: 12px !important; overflow: hidden; }
|
:deep(.el-select-dropdown) { border-radius: 12px !important; overflow: hidden; }
|
||||||
@@ -175,11 +167,10 @@ const setSort = (order) => {
|
|||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1400px) { .wide-search-input { width: 260px; } }
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.search-container { flex-direction: column; }
|
.search-container { flex-direction: column; }
|
||||||
.input-group-row { flex-direction: column; align-items: flex-start; }
|
.input-group-row { flex-direction: column; align-items: flex-start; }
|
||||||
.divider-wrapper { display: none; }
|
.divider-wrapper { display: none; }
|
||||||
.wide-select, .wide-search-input { width: 100%; }
|
.wide-select { width: 100%; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -62,6 +62,12 @@
|
|||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="任务进度">
|
||||||
|
<div class="progress-input-wrap">
|
||||||
|
<el-slider v-model="form.progress" :min="0" :max="100" :step="5" show-input />
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="备注" prop="remark">
|
<el-form-item label="备注" prop="remark">
|
||||||
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="填写注意事项" resize="none" />
|
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="填写注意事项" resize="none" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -97,6 +103,7 @@ const props = defineProps({
|
|||||||
const dialogVisible = ref(false);
|
const dialogVisible = ref(false);
|
||||||
const formRef = ref();
|
const formRef = ref();
|
||||||
const submitting = ref(false);
|
const submitting = ref(false);
|
||||||
|
const isInitializing = ref(false);
|
||||||
|
|
||||||
// 核心表单对象:customer_id 已包含在内
|
// 核心表单对象:customer_id 已包含在内
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
@@ -106,6 +113,7 @@ const form = reactive({
|
|||||||
operator_id: null,
|
operator_id: null,
|
||||||
user_id: null,
|
user_id: null,
|
||||||
status: 0,
|
status: 0,
|
||||||
|
progress: 0,
|
||||||
remark: "",
|
remark: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -123,6 +131,17 @@ const handleCustomerChange = () => {
|
|||||||
form.operator_id = null;
|
form.operator_id = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 状态变为已完成(2)或已完结(3)时,自动将进度设为100
|
||||||
|
watch(
|
||||||
|
() => form.status,
|
||||||
|
(val) => {
|
||||||
|
if (isInitializing.value) return;
|
||||||
|
if (val === 2 || val === 3) {
|
||||||
|
form.progress = 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// --- 监听弹窗与回显 ---
|
// --- 监听弹窗与回显 ---
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
@@ -130,6 +149,7 @@ watch(
|
|||||||
dialogVisible.value = val;
|
dialogVisible.value = val;
|
||||||
if (val) { // 只在弹窗打开时候,执行初始化
|
if (val) { // 只在弹窗打开时候,执行初始化
|
||||||
nextTick(() => { // 等待DOM更新完成后再操作表单数据
|
nextTick(() => { // 等待DOM更新完成后再操作表单数据
|
||||||
|
isInitializing.value = true;
|
||||||
resetForm(); // 先重置
|
resetForm(); // 先重置
|
||||||
if (props.isEditing && props.formData) {
|
if (props.isEditing && props.formData) {
|
||||||
const d = props.formData;
|
const d = props.formData;
|
||||||
@@ -167,6 +187,7 @@ watch(
|
|||||||
if (u) form.user_id = u.id;
|
if (u) form.user_id = u.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
isInitializing.value = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,6 +208,7 @@ const resetForm = () => {
|
|||||||
operator_id: null,
|
operator_id: null,
|
||||||
user_id: null,
|
user_id: null,
|
||||||
status: 0,
|
status: 0,
|
||||||
|
progress: 0,
|
||||||
remark: "",
|
remark: "",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -261,6 +283,10 @@ const handleSubmit = async () => {
|
|||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progress-input-wrap {
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.el-form-item__label) {
|
:deep(.el-form-item__label) {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|||||||
@@ -3,12 +3,14 @@
|
|||||||
<el-table
|
<el-table
|
||||||
ref="tableRef"
|
ref="tableRef"
|
||||||
:data="tickets"
|
:data="tickets"
|
||||||
|
row-key="id"
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
header-cell-class-name="table-header"
|
header-cell-class-name="table-header"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
@selection-change="$emit('selection-change', $event)"
|
@select="onSelect"
|
||||||
|
@select-all="onSelectAll"
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" width="50" />
|
<el-table-column type="selection" width="50" :reserve-selection="true" />
|
||||||
<el-table-column label="网站地址" min-width="180">
|
<el-table-column label="网站地址" min-width="180">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-link
|
<el-link
|
||||||
@@ -147,21 +149,43 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from "vue";
|
import { ref, reactive } from "vue";
|
||||||
import { formatUrl, formatDateTime } from "@/utils/format";
|
import { formatUrl, formatDateTime } from "@/utils/format";
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
tickets: { type: Array, default: () => [] },
|
tickets: { type: Array, default: () => [] },
|
||||||
loading: { type: Boolean, default: false },
|
loading: { type: Boolean, default: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits(["edit", "delete", "selection-change"]);
|
const emit = defineEmits(["edit", "delete", "selection-change"]);
|
||||||
|
|
||||||
const tableRef = ref(null);
|
const tableRef = ref(null);
|
||||||
|
const selectedMap = reactive(new Map());
|
||||||
|
|
||||||
|
const emitSelection = () => {
|
||||||
|
emit("selection-change", Array.from(selectedMap.values()));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelect = (selection, row) => {
|
||||||
|
if (selection.some((r) => r.id === row.id)) {
|
||||||
|
selectedMap.set(row.id, row);
|
||||||
|
} else {
|
||||||
|
selectedMap.delete(row.id);
|
||||||
|
}
|
||||||
|
emitSelection();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelectAll = (selection) => {
|
||||||
|
props.tickets.forEach((t) => selectedMap.delete(t.id));
|
||||||
|
selection.forEach((row) => selectedMap.set(row.id, row));
|
||||||
|
emitSelection();
|
||||||
|
};
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
clearSelection() {
|
clearSelection() {
|
||||||
tableRef.value?.clearSelection();
|
tableRef.value?.clearSelection();
|
||||||
|
selectedMap.clear();
|
||||||
|
emit("selection-change", []);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ function createService(basePath) {
|
|||||||
service.interceptors.response.use(
|
service.interceptors.response.use(
|
||||||
response => response.data,
|
response => response.data,
|
||||||
|
|
||||||
|
|
||||||
error => {
|
error => {
|
||||||
const resData = error.response?.data
|
const resData = error.response?.data
|
||||||
const status = error.response?.status
|
const status = error.response?.status
|
||||||
|
|||||||
@@ -7,36 +7,23 @@
|
|||||||
<p class="desc">登录以继续使用系统</p>
|
<p class="desc">登录以继续使用系统</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-form
|
<el-form ref="formRef" :model="form" :rules="rules" class="login-form" @keyup.enter="handleLogin">
|
||||||
ref="formRef"
|
|
||||||
:model="form"
|
|
||||||
:rules="rules"
|
|
||||||
class="login-form"
|
|
||||||
@keyup.enter="handleLogin"
|
|
||||||
>
|
|
||||||
<el-form-item prop="username">
|
<el-form-item prop="username">
|
||||||
<el-input
|
<el-input v-model="form.username" placeholder="员工编号" size="large" clearable>
|
||||||
v-model="form.username"
|
|
||||||
placeholder="员工编号"
|
|
||||||
size="large"
|
|
||||||
clearable
|
|
||||||
>
|
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<el-icon><User /></el-icon>
|
<el-icon>
|
||||||
|
<User />
|
||||||
|
</el-icon>
|
||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item prop="password">
|
<el-form-item prop="password">
|
||||||
<el-input
|
<el-input v-model="form.password" type="password" placeholder="访问密钥" size="large" show-password>
|
||||||
v-model="form.password"
|
|
||||||
type="password"
|
|
||||||
placeholder="访问密钥"
|
|
||||||
size="large"
|
|
||||||
show-password
|
|
||||||
>
|
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<el-icon><Lock /></el-icon>
|
<el-icon>
|
||||||
|
<Lock />
|
||||||
|
</el-icon>
|
||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -45,13 +32,7 @@
|
|||||||
<el-checkbox v-model="rememberMe">保持登录</el-checkbox>
|
<el-checkbox v-model="rememberMe">保持登录</el-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-button
|
<el-button type="primary" size="large" class="submit-btn" :loading="loading" @click="handleLogin">
|
||||||
type="primary"
|
|
||||||
size="large"
|
|
||||||
class="submit-btn"
|
|
||||||
:loading="loading"
|
|
||||||
@click="handleLogin"
|
|
||||||
>
|
|
||||||
登 录
|
登 录
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|||||||
@@ -60,6 +60,7 @@
|
|||||||
<div class="batch-bar" v-if="selectedTickets.length">
|
<div class="batch-bar" v-if="selectedTickets.length">
|
||||||
<span class="batch-info">已选 <strong>{{ selectedTickets.length }}</strong> 个工单</span>
|
<span class="batch-info">已选 <strong>{{ selectedTickets.length }}</strong> 个工单</span>
|
||||||
<el-button type="warning" @click="handleBatchFinish">一键已完结</el-button>
|
<el-button type="warning" @click="handleBatchFinish">一键已完结</el-button>
|
||||||
|
<el-button type="primary" @click="handleBatchExport">一键导出</el-button>
|
||||||
<el-button link type="info" @click="ticketListRef.clearSelection()">取消选择</el-button>
|
<el-button link type="info" @click="ticketListRef.clearSelection()">取消选择</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -89,6 +90,7 @@ import {
|
|||||||
CircleCheck,
|
CircleCheck,
|
||||||
Checked,
|
Checked,
|
||||||
} from "@element-plus/icons-vue";
|
} from "@element-plus/icons-vue";
|
||||||
|
import * as XLSX from "xlsx";
|
||||||
|
|
||||||
// API 引入
|
// API 引入
|
||||||
import { ticketApi } from "@/api/ticket";
|
import { ticketApi } from "@/api/ticket";
|
||||||
@@ -272,6 +274,7 @@ const handleAdd = () => {
|
|||||||
operator_id: null,
|
operator_id: null,
|
||||||
user_id: null,
|
user_id: null,
|
||||||
status: 0,
|
status: 0,
|
||||||
|
progress: 0,
|
||||||
remark: "",
|
remark: "",
|
||||||
};
|
};
|
||||||
showAddDialog.value = true;
|
showAddDialog.value = true;
|
||||||
@@ -356,6 +359,32 @@ const handleBatchFinish = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBatchExport = () => {
|
||||||
|
const data = selectedTickets.value.map((item) => ({
|
||||||
|
域名: item.website,
|
||||||
|
数据量: item.datanum || 0,
|
||||||
|
运营人员: item.operator_name,
|
||||||
|
采集人员: item.username,
|
||||||
|
备注: item.remark,
|
||||||
|
开始时间: item.create_time,
|
||||||
|
结束时间: item.lock_time,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const worksheet = XLSX.utils.json_to_sheet(data);
|
||||||
|
const keys = Object.keys(data[0] || {});
|
||||||
|
worksheet["!cols"] = keys.map((key) => ({
|
||||||
|
wch: Math.max(
|
||||||
|
key.replace(/[^\x00-\xff]/g, "00").length,
|
||||||
|
...data.map((r) => String(r[key] ?? "").replace(/[^\x00-\xff]/g, "00").length)
|
||||||
|
) + 3,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const workbook = XLSX.utils.book_new();
|
||||||
|
XLSX.utils.book_append_sheet(workbook, worksheet, "工单导出");
|
||||||
|
XLSX.writeFile(workbook, `工单导出_${new Date().toISOString().slice(0, 10)}.xlsx`);
|
||||||
|
ElMessage.success(`已导出 ${data.length} 条工单`);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSizeChange = (val) => {
|
const handleSizeChange = (val) => {
|
||||||
pagination.limit = val;
|
pagination.limit = val;
|
||||||
loadTickets();
|
loadTickets();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user