前后端差不多

This commit is contained in:
2026-04-02 20:53:17 +08:00
parent 5e2b326838
commit 032d9bf383
9 changed files with 433 additions and 132 deletions
+13
View File
@@ -0,0 +1,13 @@
import { api } from './index'
export const scheduleApi = {
getEvents(params = {}) {
return api.post('/schedule/getevents', params)
},
addEvent(data) {
return api.post('/schedule/addevent', data)
},
}
+1 -1
View File
@@ -247,7 +247,7 @@
"source_code": "Source Code",
"github": "GitHub",
"author_home": "Author",
"copy": "Copyright © 2025 Operations. All rights reserved."
"copy": "Copyright © 2025 Operations. MIT Open Source License."
},
"cost_type": {
"unit_price": "Unit Price",
+1 -1
View File
@@ -247,7 +247,7 @@
"source_code": "源码",
"github": "GitHub",
"author_home": "作者主页",
"copy": "版权 © 2025 Operations. 保留所有权利。"
"copy": "版权 © 2025 Operations. MIT开源协议。"
},
"cost_type": {
"unit_price": "单价",
+131 -57
View File
@@ -1,64 +1,79 @@
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { ref } from "vue";
import { useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
const { t, locale } = useI18n()
const { t, locale } = useI18n();
function toggleLocale() {
locale.value = locale.value === 'zh-CN' ? 'en' : 'zh-CN'
locale.value = locale.value === "zh-CN" ? "en" : "zh-CN";
}
import { useUserStore } from '@/stores/user'
import { useToastStore } from '@/stores/toast'
import { usePageTitle } from '@/composables/usePageTitle'
import { useValidation } from '@/composables'
import { authApi } from '@/api/auth'
import { IconEye, IconEyeOff } from '@tabler/icons-vue'
import { useUserStore } from "@/stores/user";
import { useToastStore } from "@/stores/toast";
import { usePageTitle } from "@/composables/usePageTitle";
import { useValidation } from "@/composables";
import { authApi } from "@/api/auth";
import { IconEye, IconEyeOff } from "@tabler/icons-vue";
usePageTitle('appname.login')
const router = useRouter()
const userStore = useUserStore()
const toast = useToastStore()
const { validate, errors, clearErrors } = useValidation()
usePageTitle("appname.login");
const router = useRouter();
const userStore = useUserStore();
const toast = useToastStore();
const { validate, errors, clearErrors } = useValidation();
const form = ref({
username: '',
password: '',
username: "",
password: "",
remember: false,
})
const showPassword = ref(false)
const loading = ref(false)
});
const showPassword = ref(false);
const loading = ref(false);
async function handleLogin() {
clearErrors()
clearErrors();
const err1 = validate('username', form.value.username, t('message.please_enter_username_and_password'))
const err2 = validate('password', form.value.password, t('message.please_enter_username_and_password'))
const err1 = validate(
"username",
form.value.username,
t("message.please_enter_username_and_password"),
);
const err2 = validate(
"password",
form.value.password,
t("message.please_enter_username_and_password"),
);
if (!err1 || !err2) return
if (!err1 || !err2) return;
loading.value = true
loading.value = true;
try {
const { errCode, data } = await authApi.login(form.value.username, form.value.password, form.value.remember)
const { errCode, data } = await authApi.login(
form.value.username,
form.value.password,
form.value.remember,
);
switch (errCode) {
case 0:
userStore.login(data.cookie)
toast.success(t('message.login_successful'))
userStore.login(data.cookie);
toast.success(t("message.login_successful"));
// 有 redirect 则跳转到原页面,否则去首页
const redirect = router.currentRoute.value.query.redirect
router.replace(redirect || '/')
break
const redirect = router.currentRoute.value.query.redirect;
router.replace(redirect || "/");
break;
case -41:
toast.warning(t("message.username_or_password_incorrect"));
break;
case -42:
toast.warning(t('message.username_or_password_incorrect'))
break
toast.warning(t("message.username_or_password_incorrect"));
break;
default:
toast.danger(t('message.server_error'))
toast.danger(t("message.server_error"));
}
} catch {
// 拦截器已处理
} finally {
loading.value = false
loading.value = false;
}
}
</script>
@@ -68,55 +83,94 @@ async function handleLogin() {
<div class="mb-8 flex items-start justify-between">
<RouterLink to="/" class="inline-flex items-center">
<img src="/logo.svg" class="h-10 w-10 rounded-lg" alt="Operations" />
<span class="ml-2.5 text-2xl font-bold text-gray-800 dark:text-dk-text">Operations</span>
<span class="ml-2.5 text-2xl font-bold text-gray-800 dark:text-dk-text"
>Operations</span
>
</RouterLink>
<button class="rounded-md border border-gray-200 px-2.5 py-1 text-xs font-semibold uppercase text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 dark:border-dk-muted dark:text-gray-400 dark:hover:bg-dk-card dark:hover:text-dk-text" @click="toggleLocale">
{{ locale === 'zh-CN' ? 'EN' : '' }}
<button
class="rounded-md border border-gray-200 px-2.5 py-1 text-xs font-semibold uppercase text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 dark:border-dk-muted dark:text-gray-400 dark:hover:bg-dk-card dark:hover:text-dk-text"
@click="toggleLocale"
>
{{ locale === "zh-CN" ? "EN" : "中" }}
</button>
</div>
<div class="rounded-xl border border-gray-200 bg-white px-8 py-8 shadow-lg dark:border-dk-muted dark:bg-dk-card">
<h2 class="mb-6 text-center text-xl font-bold text-gray-900 dark:text-white">{{ t('message.login_to_your_account') }}</h2>
<div
class="rounded-xl border border-gray-200 bg-white px-8 py-8 shadow-lg dark:border-dk-muted dark:bg-dk-card"
>
<h2
class="mb-6 text-center text-xl font-bold text-gray-900 dark:text-white"
>
{{ t("message.login_to_your_account") }}
</h2>
<!-- Username -->
<div class="mb-4">
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('message.user_name') }}</label>
<label
class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300"
>{{ t("message.user_name") }}</label
>
<input
v-model="form.username"
type="text"
class="w-full rounded-lg border border-gray-300 bg-white px-3.5 py-2.5 text-sm outline-none transition-colors focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 dark:border-dk-muted dark:bg-dk-base dark:text-white"
:class="errors.username ? 'border-red-500 focus:border-red-500 focus:ring-red-500/20' : 'border-gray-300'"
:class="
errors.username
? 'border-red-500 focus:border-red-500 focus:ring-red-500/20'
: 'border-gray-300'
"
:placeholder="t('message.your_user_name')"
@keydown.enter="handleLogin"
/>
<span v-if="errors.username" class="mt-1 block text-xs text-red-500">{{ errors.username }}</span>
<span v-if="errors.username" class="mt-1 block text-xs text-red-500">{{
errors.username
}}</span>
</div>
<!-- Password -->
<div class="mb-4">
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('message.password') }}</label>
<label
class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300"
>{{ t("message.password") }}</label
>
<div class="relative">
<input
v-model="form.password"
:type="showPassword ? 'text' : 'password'"
class="w-full rounded-lg border border-gray-300 bg-white px-3.5 py-2.5 text-sm outline-none transition-colors focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 dark:border-dk-muted dark:bg-dk-base dark:text-white"
:class="errors.password ? 'border-red-500 focus:border-red-500 focus:ring-red-500/20' : 'border-gray-300'"
:class="
errors.password
? 'border-red-500 focus:border-red-500 focus:ring-red-500/20'
: 'border-gray-300'
"
:placeholder="t('message.your_password')"
autocomplete="current-password"
@keydown.enter="handleLogin"
/>
<button type="button" class="absolute inset-y-0 right-0 flex items-center px-3 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300" @click="showPassword = !showPassword">
<button
type="button"
class="absolute inset-y-0 right-0 flex items-center px-3 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
@click="showPassword = !showPassword"
>
<IconEye v-if="!showPassword" :size="18" />
<IconEyeOff v-else :size="18" />
</button>
</div>
<span v-if="errors.password" class="mt-1 block text-xs text-red-500">{{ errors.password }}</span>
<span v-if="errors.password" class="mt-1 block text-xs text-red-500">{{
errors.password
}}</span>
</div>
<!-- Remember -->
<label class="mb-6 flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
<input v-model="form.remember" type="checkbox" class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
{{ t('message.remember_me_on_this_device') }}
<label
class="mb-6 flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400"
>
<input
v-model="form.remember"
type="checkbox"
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
{{ t("message.remember_me_on_this_device") }}
</label>
<!-- Submit -->
@@ -125,17 +179,37 @@ async function handleLogin() {
:disabled="loading"
@click="handleLogin"
>
<svg v-if="loading" class="h-4 w-4 animate-spin text-white" viewBox="0 0 24 24" fill="none">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
<svg
v-if="loading"
class="h-4 w-4 animate-spin text-white"
viewBox="0 0 24 24"
fill="none"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
/>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
{{ t('button.sign_in') }}
{{ t("button.sign_in") }}
</button>
</div>
<p class="mt-6 text-center text-sm text-gray-500">
{{ t('message.dont_have_account_yet') }}
<RouterLink to="/register" class="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400">{{ t('message.register_now') }}</RouterLink>
{{ t("message.dont_have_account_yet") }}
<RouterLink
to="/register"
class="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400"
>{{ t("message.register_now") }}</RouterLink
>
</p>
</div>
</template>
+128 -31
View File
@@ -20,6 +20,22 @@ import DatatimePickerForFullCalendar from "@/components/datatimePickerForFullCal
import { useToastStore } from "@/stores/toast";
// 用户状态管理
import { useUserStore } from "@/stores/user";
import { useRouter } from "vue-router";
import { scheduleApi } from "@/api/schedule";
import { useDateUtils } from "@/composables/useDateUtils";
const DateUtils = useDateUtils();
const router = useRouter();
// 获取用户 store 实例,用于访问和更新用户信息
const userStore = useUserStore();
const toast = useToastStore();
// 设置页面标题
@@ -30,6 +46,9 @@ const { t, locale } = useI18n();
// FullCalendar 组件的引用,用于调用日历 API
const calendarRef = ref(null);
// 当前视图的年份
const calendarNowShow = ref();
// 用于跟踪上次点击时间的响应式变量
const lastClickTime = ref(0);
// 用于跟踪上次点击event时间的响应式变量
@@ -127,6 +146,7 @@ const calendarOptions = ref({
text: t("schedule.previous_year"),
click() {
calendarRef.value.getApi().prevYear();
getEvents();
},
},
// 下一年按钮
@@ -134,6 +154,7 @@ const calendarOptions = ref({
text: t("schedule.next_year"),
click() {
calendarRef.value.getApi().nextYear();
getEvents();
},
},
// 上一个月按钮
@@ -141,6 +162,7 @@ const calendarOptions = ref({
text: t("schedule.previous_month"),
click() {
calendarRef.value.getApi().prev();
getEvents();
},
},
// 下一个月按钮
@@ -148,6 +170,7 @@ const calendarOptions = ref({
text: t("schedule.next_month"),
click() {
calendarRef.value.getApi().next();
getEvents();
},
},
// 今天按钮:跳转到今天
@@ -155,6 +178,7 @@ const calendarOptions = ref({
text: t("schedule.today"),
click() {
calendarRef.value.getApi().today();
getEvents();
},
},
// 周视图按钮:切换到周视图
@@ -169,6 +193,12 @@ const calendarOptions = ref({
// 日历事件列表(目前为空,后续可接入数据源)
events: [],
// 👇 加这个!日历渲染完成 / 切换年月都会触发
datesSet(info) {
calendarNowShow.value = info;
//console.log(info);
},
// 日期点击事件处理函数
dateClick(info) {
const nowTime = new Date().getTime();
@@ -194,7 +224,7 @@ const calendarOptions = ref({
if (info.end - info.start > 86400000) {
//选择了多日
console.log("选择了多日:", info);
openEventModal(info.startStr,info.endStr);
openEventModal(info.startStr, info.endStr);
} else {
//选择单日 无功能
//console.log("选择单日:", info);
@@ -223,7 +253,7 @@ const calendarOptions = ref({
});
// 打开模态框
const openEventModal = (dateStr,dataEnd) => {
const openEventModal = (dateStr, dataEnd) => {
eventData.value = {
title: "",
startDate: dateStr,
@@ -241,7 +271,13 @@ const closeEventModal = () => {
// 处理双击事件:打开模态框添加事件
const handleDoubleClick = (info) => {
openEventModal(info.dateStr,info.dateStr);
//先判断是否登录
if (userStore.isLoggedIn) {
openEventModal(info.dateStr, info.dateStr);
} else {
toast.warning(t("message.login_to_your_account"));
router.replace("/login?redirect=/schedule");
}
};
// 处理单机事件:显示日期详情
@@ -293,15 +329,77 @@ const saveEvent = () => {
};
// 添加到日历事件列表
calendarOptions.value.events.push(newEvent);
//提交到后端
console.log("事件添加成功:", newEvent);
toast.success(t("schedule.event_added_successfully"));
// 关闭模态框
closeEventModal();
scheduleApi
.addEvent({
title: newEvent.title,
start: newEvent.start,
end: DateUtils.toRealEnd(newEvent.end),
color: newEvent.backgroundColor,
})
.then((r) => {
//console.log(r);
if (r.errCode == 0) {
//前端提交是否错误
switch (
r.raw.err_code //后端返回是否错误
) {
case 0:
//calendarOptions.value.events.push(newEvent);
toast.success(t("schedule.event_added_successfully"));
// 关闭模态框
closeEventModal();
getEvents();
break;
default:
toast.danger(t("message.server_error"));
break;
}
}
});
};
//从后端获取events
const getEvents = () => {
//console.log(calendarNowShow.value)
scheduleApi
.getEvents({
start: DateUtils.dateToStr(calendarNowShow.value.start),
end: DateUtils.toRealEnd(calendarNowShow.value.end),
})
.then((r) => {
console.log(r);
if (r.errCode == 0) {
//前端提交是否错误
switch (
r.raw.err_code //后端返回是否错误
) {
case 0:
calendarOptions.value.events=[];
var events = r.raw.return.list;
console.log(events);
var eventstemp = [];
events.forEach((item) => {
calendarOptions.value.events.push({
id: item.ID, // 后端 ID
title: item.Title, // 标题
start: item.StartDate, // 开始日期
end: DateUtils.toCalendarEnd(item.EndDate), // 结束日期
backgroundColor: item.BgColor, // 背景色
allDay: true, // 全天事件
});
});
break;
default:
toast.danger(t("message.server_error"));
break;
}
}
});
};
// 清除日期选择
const clearDates = () => {
eventData.value.startDate = "";
@@ -350,32 +448,31 @@ watch(locale, () => {
});
onMounted(() => {
const handleKeydown = (event) => {
// Ctrl+C 事件
if (event.ctrlKey && event.key === "c") {
event.preventDefault(); // 可选:阻止默认复制行为
console.log("Ctrl+C 被按下");
// 你的业务逻辑
}
// Ctrl+V 事件
if (event.ctrlKey && event.key === "v") {
event.preventDefault(); // 可选:阻止默认粘贴行为
console.log("Ctrl+V 被按下");
// 你的业务逻辑
}
};
document.addEventListener("keydown", handleKeydown);
// 清理事件监听器
onBeforeUnmount(() => {
document.removeEventListener("keydown", handleKeydown);
});
getEvents();
// const handleKeydown = (event) => {
// // Ctrl+C 事件
// if (event.ctrlKey && event.key === "c") {
// event.preventDefault(); // 可选:阻止默认复制行为
// console.log("Ctrl+C 被按下");
// // 你的业务逻辑
// }
// // Ctrl+V 事件
// if (event.ctrlKey && event.key === "v") {
// event.preventDefault(); // 可选:阻止默认粘贴行为
// console.log("Ctrl+V 被按下");
// // 你的业务逻辑
// }
// };
// document.addEventListener("keydown", handleKeydown);
// // 清理事件监听器
// onBeforeUnmount(() => {
// document.removeEventListener("keydown", handleKeydown);
// });
});
</script>
<template>
<!-- {{userStore.userCookie.Value}} -->
<!-- 日历容器占满视口高度减去顶部导航高度 -->
<div class="flex w-full flex-col relative">
<!-- 事件编辑模态框 -->