This commit is contained in:
2026-03-31 15:46:15 +08:00
parent 654724a213
commit 6d79836682
68 changed files with 3076 additions and 4488 deletions
-82
View File
@@ -1,82 +0,0 @@
<script setup>
import { onMounted, watch, ref } from "vue";
import { useI18n } from "vue-i18n";
import { useRouter } from "vue-router";
const { t, locale } = useI18n();
const router = useRouter();
function goback() {
router.back();
}
function functionupdataTitle() {
document.title = "Operations." + t("errorpage.404_title");
}
// 监听语言变化,更新标题
watch(locale, () => {
functionupdataTitle();
});
onMounted(() => {
//console.log("account mounted");
//username.value.value="Kevin";
functionupdataTitle();
});
</script>
<template>
<div>
<div class="page page-center">
<div class="container-tight py-4">
<div class="empty">
<div class="empty-header">404</div>
<p class="empty-title">{{ t("errorpage.404_msg_title") }}</p>
<p class="empty-subtitle text-secondary">
{{ t("errorpage.404_msg") }}
</p>
<div class="empty-action">
<button class="btn btn-outline-secondary m-3" @click="goback">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="icon icon-tabler icons-tabler-outline icon-tabler-arrow-left"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M5 12l14 0" />
<path d="M5 12l6 6" />
<path d="M5 12l6 -6" />
</svg>
{{ t("errorpage.404_previous_page") }}
</button>
<router-link to="/" class="btn btn-primary">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="icon icon-tabler icons-tabler-outline icon-tabler-home"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M5 12l-2 0l9 -9l9 9l-2 0" />
<path d="M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7" />
<path d="M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6" />
</svg>
{{ t("errorpage.404_back_home") }}
</router-link>
</div>
</div>
</div>
</div>
</div>
</template>
@@ -1,15 +0,0 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<style>
@media (min-width: 1024px) {
.about {
min-height: 100vh;
display: flex;
align-items: center;
}
}
</style>
@@ -0,0 +1,65 @@
<script setup>
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useToastStore } from '@/stores/toast'
import { usePageTitle } from '@/composables/usePageTitle'
import { useValidation } from '@/composables'
import { IconMail } from '@tabler/icons-vue'
usePageTitle('appname.forgot_password')
const { t } = useI18n()
const toast = useToastStore()
const { validate, errors, clearErrors } = useValidation()
const form = ref({ username: '' })
const loading = ref(false)
async function handleReset() {
const err = validate('username', form.value.username, t('message.please_enter_your_username'))
if (!err) return
// 功能未开发
toast.warning(t('message.functionality_not_yet_developed'))
}
</script>
<template>
<div class="mx-auto max-w-sm px-8">
<div class="mb-8 text-center">
<RouterLink to="/">
<img src="/static/logo.svg" width="110" height="32" alt="Operations" class="mx-auto" />
</RouterLink>
</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-2 text-center text-xl font-bold text-gray-900 dark:text-white">{{ t('message.forgot_password') }}</h2>
<p class="mb-6 text-center text-sm text-gray-500">{{ t('message.enter_your_username_to_reset_password') }}</p>
<div class="mb-6">
<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'"
:placeholder="t('message.your_user_name')"
@keydown.enter="handleReset"
/>
<span v-if="errors.username" class="mt-1 block text-xs text-red-500">{{ errors.username }}</span>
</div>
<button
class="flex w-full items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-blue-500/20 focus:outline-none disabled:active:scale-60"
:disabled="loading"
@click="handleReset"
>
<IconMail :size="18" />
{{ t('button.send_me_new_password') }}
</button>
</div>
<p class="mt-6 text-center text-sm text-gray-500">
<RouterLink to="/login" class="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400">{{ t('message.back_to_login') }}</RouterLink>
</p>
</div>
</template>
+27 -12
View File
@@ -1,19 +1,34 @@
<script setup>
import { ref } from "vue";
import { my_network_func } from "@/my_network_func";
import { useUserStore } from "@/stores/user";
import MyOffcanvas from "@/components/MyOffcanvas.vue";
import imageCropper from "@/components/imageCropper.vue";
const user = useUserStore();
const mos = ref();
<script setup>
import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/stores/user'
import { usePageTitle } from '@/composables/usePageTitle'
usePageTitle('appname.home')
const { t } = useI18n()
const userStore = useUserStore()
const features = computed(() => [
{ title: t('appname.purchase'), desc: '—' },
{ title: t('appname.schedule'), desc: '—' },
{ title: t('appname.warehouse'), desc: '—' },
])
import { computed } from 'vue'
</script>
<template>
<div class="mx-auto max-w-6xl px-6 py-6">
<h2 class="mb-6 text-2xl font-bold text-gray-900 dark:text-white">{{ t('message.welcome') }}</h2>
<MyOffcanvas ref="mos" />
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<div
v-for="feature in features"
:key="feature.title"
class="rounded-xl border border-gray-200 bg-white px-5 py-4 transition-shadow hover:shadow-md dark:border-dk-muted dark:bg-dk-card"
>
<p class="mb-1 text-sm text-gray-500">{{ feature.title }}</p>
<p class="text-lg font-bold text-gray-900 dark:text-white">{{ feature.desc }}</p>
</div>
</div>
</div>
</template>
@@ -0,0 +1,22 @@
<script setup>
import { useI18n } from 'vue-i18n'
import { usePageTitle } from '@/composables/usePageTitle'
import { IconAlertTriangle } from '@tabler/icons-vue'
usePageTitle('errorpage.404_title')
const { t } = useI18n()
</script>
<template>
<div class="flex min-h-[60vh] flex-col items-center justify-center px-4">
<div class="text-center">
<IconAlertTriangle :size="64" class="mx-auto mb-4 text-yellow-400" stroke-width="1.5" />
<h1 class="mb-2 text-5xl font-bold text-blue-600">404</h1>
<h2 class="mb-3 text-2xl font-bold text-gray-900 dark:text-white">{{ t('errorpage.404_msg_title') }}</h2>
<p class="mb-6 text-gray-500">{{ t('errorpage.404_msg') }}</p>
<RouterLink to="/" class="inline-block rounded-lg bg-blue-600 px-5 py-2.5 font-medium text-white transition-colors hover:bg-blue-700">
{{ t('errorpage.404_back_home') }}
</RouterLink>
</div>
</div>
</template>
@@ -0,0 +1,16 @@
<script setup>
import { useI18n } from 'vue-i18n'
import { usePageTitle } from '@/composables/usePageTitle'
usePageTitle('appname.warehouse')
const { t } = useI18n()
</script>
<template>
<div class="mx-auto max-w-6xl px-6 py-6">
<h2 class="mb-6 text-2xl font-bold text-gray-900 dark:text-white">{{ t('appname.warehouse') }}</h2>
<div class="rounded-xl border border-gray-200 bg-white px-12 py-12 text-center shadow-lg dark:border-dk-muted dark:bg-dk-card">
<p class="text-gray-400">{{ t('message.functionality_not_yet_developed') }}</p>
</div>
</div>
</template>
+15 -2
View File
@@ -1,3 +1,16 @@
<script setup>
import { useI18n } from 'vue-i18n'
import { usePageTitle } from '@/composables/usePageTitle'
usePageTitle('message.administrator')
const { t } = useI18n()
</script>
<template>
</template>
<div class="mx-auto max-w-6xl px-6 py-6">
<h2 class="mb-6 text-2xl font-bold text-gray-900 dark:text-white">{{ t('message.administrator') }}</h2>
<div class="rounded-xl border border-gray-200 bg-white px-12 py-12 text-center shadow-lg dark:border-dk-muted dark:bg-dk-card">
<p class="text-gray-400">{{ t('message.functionality_not_yet_developed') }}</p>
</div>
</div>
</template>
@@ -1,97 +0,0 @@
<script setup>
import { onMounted, watch, ref } from 'vue'
import MyOffcanvas from '@/components/MyOffcanvas.vue'
import { myfuncs } from '@/myfunc.js'
import { useI18n } from 'vue-i18n'
// 使用 vue-i18n 的 Composition API
const { t, locale } = useI18n()
const email = ref()
const mos = ref()
function resetPassword() {
// 在这里处理重置密码逻辑
const emailValue = email.value?.value
if (emailValue === undefined || emailValue.trim() === '') {
mos.value?.showAlert('info', t('message.please_enter_your_username'), 5000)
return
}
// if (!myfuncs.isValidEmail(emailValue)) {
// mos.value?.showAlert('warning', t('message.this_not_email'), 5000)
// return
// }
mos.value?.showAlert('warning', "功能未开发", 5000)
console.log('sending password reset to:', emailValue)
}
function functionupdataTitle() {
document.title = 'Operations.' + t('appname.forgot_password')
}
onMounted(() => {
functionupdataTitle()
})
// 监听语言变化,更新标题
watch(locale, () => {
functionupdataTitle()
})
</script>
<template>
<div class="container container-tight py-4">
<div class="text-center mb-4">
<a href="." class="navbar-brand navbar-brand-autodark">
<img
src="/static/logo.svg"
width="110"
height="32"
alt="Tabler"
class="navbar-brand-image"
/>
</a>
</div>
<div class="card card-md">
<div class="card-body">
<h2 class="card-title text-center mb-4">{{ t('message.forgot_password') }}</h2>
<p class="text-secondary mb-4">
{{ t('message.enter_your_username_to_reset_password') }}
</p>
<div class="mb-3">
<label class="form-label">{{ t('message.user_name') }}</label>
<input
ref="email"
type="text"
class="form-control"
:placeholder="t('message.your_user_name')"
/>
</div>
<div class="form-footer">
<button @click="resetPassword" class="btn btn-primary w-100">
<!-- Download SVG icon from http://tabler-icons.io/i/mail -->
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M3 7a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10z"
/>
<path d="M3 7l9 6l9 -6" />
</svg>
{{ t('button.send_me_new_password') }}
</button>
</div>
</div>
</div>
<div class="text-center text-secondary mt-3">
<router-link to="/login">{{ t('message.back_to_login') }}</router-link>
</div>
</div>
<MyOffcanvas ref="mos" />
</template>
+124 -251
View File
@@ -1,267 +1,140 @@
<script setup>
import { onMounted, watch, ref } from "vue";
import { useRouter } from "vue-router";
import { useUserStore } from "@/stores/user";
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { my_network_func } from "@/my_network_func";
import { myfuncs } from "@/myfunc.js";
import MyOffcanvas from "@/components/MyOffcanvas.vue";
import { useI18n } from "vue-i18n";
// 使用 vue-i18n 的 Composition API
const { t, locale } = useI18n();
const router = useRouter();
const userStore = useUserStore();
const { t, locale } = useI18n()
const mos = ref();
const username = ref();
const password = ref();
const isRemember = ref();
const isShowPassword = ref(false);
function togglePasswordVisibility() {
isShowPassword.value = !isShowPassword.value;
function toggleLocale() {
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'
function login() {
// 在这里处理登录逻辑
usePageTitle('appname.login')
const router = useRouter()
const userStore = useUserStore()
const toast = useToastStore()
const { validate, errors, clearErrors } = useValidation()
const user = username.value?.value;
const pass = password.value?.value;
const remember = isRemember.value?.checked;
const form = ref({
username: '',
password: '',
remember: false,
})
const showPassword = ref(false)
const loading = ref(false)
username.value?.classList.remove("is-invalid");
password.value?.classList.remove("is-invalid");
async function handleLogin() {
clearErrors()
if (!user || !pass) {
if (!user) {
username.value?.classList.add("is-invalid");
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
loading.value = true
try {
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'))
const redirectPath = router.query.redirect || '/'
router.push(redirectPath)
break
case -42:
toast.danger(t('message.username_or_password_incorrect'))
break
default:
toast.error(t('message.server_error'))
}
if (!pass) {
password.value?.classList.add("is-invalid");
}
mos.value?.showAlert(
"info",
t("message.please_enter_username_and_password"),
5000
);
return;
} catch {
// 拦截器已处理
} finally {
loading.value = false
}
//console.log("登录信息:", { user, pass, remember });
my_network_func.postJson(
"/users/login",
{
username: user,
userpass: pass,
remember: remember,
},
(r) => {
//console.log(r)
switch (r.statusCode) {
case 200:
switch (r.data.err_code) {
case -41:
username.value?.classList.add("is-invalid");
mos.value?.showAlert(
"warning",
t("message.user_not_found"),
5000
);
break;
case -42:
username.value?.classList.add("is-invalid");
password.value?.classList.add("is-invalid");
mos.value?.showAlert(
"warning",
t("message.username_or_password_incorrect"),
5000
);
break;
case 0:
//登录成功,载入cookie
//临时保存cookie
userStore.cookieUpdata(r.data.return.cookie)
//更新用户信息
userStore.login(r.data.return.cookie)
mos.value?.showAlert(
"success",
t("message.login_successful"),
1000,
() => {
router.back()
}
);
break;
default:
mos.value?.showAlert("danger", t("message.server_error"), 5000);
break;
}
break;
default:
mos.value?.showAlert("danger", t("message.network_err"), 5000);
break;
}
}
);
}
onMounted(() => {
functionupdataTitle();
if (userStore.isLoggedIn) {
router.push("/");
}
});
function functionupdataTitle() {
document.title = "Operations." + t("appname.login");
}
// 监听语言变化,更新标题
watch(locale, () => {
functionupdataTitle();
});
</script>
<template>
<div class="page page-center">
<div class="container container-normal py-6">
<div class="row align-items-center g-4">
<div class="col-lg">
<div class="container-tight">
<div class="card card-md">
<div class="card-body">
<h2 class="h2 text-center mb-4">
{{ t("message.login_to_your_account") }}
</h2>
<div class="mb-3">
<label class="form-label">{{ t("message.user_name") }}</label>
<input
ref="username"
type="text"
maxlength="64"
class="form-control"
:placeholder="t('message.your_user_name')"
autocomplete="off"
/>
</div>
<div class="mb-2">
<label class="form-label">
{{ t("message.password") }}
<span class="form-label-description">
<router-link to="/forgot_password">{{
t("message.i_forgot_password")
}}</router-link>
</span>
</label>
<div class="input-group input-group-flat">
<input
ref="password"
:type="isShowPassword ? 'text' : 'password'"
class="form-control"
:placeholder="t('message.your_password')"
autocomplete="off"
/>
<span class="input-group-text">
<div
class="link-secondary"
:title="
isShowPassword
? t('message.hidden_Password')
: t('message.show_password')
"
data-bs-toggle="tooltip"
>
<!-- Download SVG icon from http://tabler-icons.io/i/eye -->
<svg
v-if="!isShowPassword"
@click="togglePasswordVisibility"
xmlns="http://www.w3.org/2000/svg"
class="icon"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
<path
d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6"
/>
</svg>
<svg
v-if="isShowPassword"
@click="togglePasswordVisibility"
xmlns="http://www.w3.org/2000/svg"
class="icon"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" />
<path
d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87"
/>
<path d="M3 3l18 18" />
</svg>
</div>
</span>
</div>
</div>
<div class="mb-2">
<label class="form-check">
<input
ref="isRemember"
type="checkbox"
class="form-check-input"
/>
<span class="form-check-label">{{
t("message.remember_me_on_this_device")
}}</span>
</label>
</div>
<div class="form-footer">
<button @click="login" class="btn btn-primary w-100">
{{ t("button.sign_in") }}
</button>
</div>
</div>
</div>
<div class="text-center text-secondary mt-3">
{{ t("message.dont_have_account_yet") }}
<router-link to="/register">{{
t("message.register_now")
}}</router-link>
</div>
</div>
</div>
<div class="col-lg d-none d-lg-block">
<img
src="/static/illustrations/undraw_secure_login_pdn4.svg"
height="300"
class="d-block mx-auto"
alt=""
/>
</div>
</div>
<div class="mx-auto max-w-sm px-8">
<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>
</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>
</div>
</div>
<MyOffcanvas ref="mos" />
<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>
<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'"
: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>
</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>
<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'"
: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">
<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>
</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>
<!-- Submit -->
<button
class="flex w-full items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-blue-500/20 focus:outline-none disabled:active:scale-100"
: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>
{{ 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>
</p>
</div>
</template>
@@ -0,0 +1,184 @@
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useToastStore } from '@/stores/toast'
import { usePageTitle } from '@/composables/usePageTitle'
import { purchaseApi } from '@/api/purchase'
import { IconPlus, IconChevronLeftPipe, IconChevronRightPipe, IconChevronsLeft, IconChevronsRight, IconSearch } from '@tabler/icons-vue'
usePageTitle('appname.purchase')
const { t, locale } = useI18n()
const router = useRouter()
const toast = useToastStore()
const orders = ref([])
const totalCount = ref(0)
const pageSize = ref(10)
const currentPage = ref(1)
const searchQuery = ref('')
const loading = ref(false)
const totalPages = computed(() => Math.ceil(totalCount.value / pageSize.value) || 1)
const pageRange = computed(() => {
const total = totalPages.value
const cur = currentPage.value
let start = Math.max(1, cur - 2)
let end = Math.min(cur + 4, total)
if (end - start < 4) start = Math.max(1, end - 4)
return Array.from({ length: end - start + 1 }, (_, i) => start + i)
})
async function fetchOrders() {
loading.value = true
try {
const { errCode, data } = await purchaseApi.getOrders({
keyword: searchQuery.value,
page: pageSize.value,
page_num: currentPage.value,
})
if (errCode === 0) {
orders.value = data.all_orders ?? []
totalCount.value = data.all_count ?? 0
} else {
toast.error(t('message.server_error'))
}
} catch {
// 拦截器已处理
} finally {
loading.value = false
}
}
function goToPage(page) {
if (page < 1 || page > totalPages.value) return
currentPage.value = page
fetchOrders()
}
function jumpToOrder(id) {
const resolved = router.resolve({ path: `/purchase/showorder/${id}` })
window.open(resolved.href, '_blank')
}
function formatDate(dateStr) {
if (!dateStr) return ''
return new Intl.DateTimeFormat(locale.value, {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false,
}).format(new Date(dateStr))
}
function handlePageSizeInput(e) {
let val = parseInt(e.target.value) || 10
if (val > 300) val = 300
if (val < 1) val = 1
pageSize.value = val
currentPage.value = 1
fetchOrders()
}
function handleJumpPageInput(e) {
const val = parseInt(e.target.value)
if (val > 0 && val <= totalPages.value) {
currentPage.value = val
fetchOrders()
}
}
onMounted(fetchOrders)
</script>
<template>
<div class="mx-auto max-w-6xl px-6 py-6">
<div class="flex flex-col gap-6 rounded-xl border border-gray-200 bg-white shadow-lg dark:border-dk-muted dark:bg-dk-card">
<!-- Header -->
<div class="flex items-center justify-between border-b border-gray-100 px-6 py-4 dark:border-dk-muted">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">{{ t('purchase.purchase_list') }}</h3>
</div>
<!-- Toolbar -->
<div class="flex flex-col gap-3 px-6 py-3 sm:flex-row sm:items-center">
<div class="flex gap-2">
<RouterLink to="/purchase/addorder" class="inline-flex items-center gap-1.5 rounded-lg bg-blue-600 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-blue-700">
<IconPlus :size="16" />
{{ t('purchase.add_part') }}
</RouterLink>
<button class="rounded-lg border border-gray-300 px-3 py-1.5 text-sm text-gray-600 transition-colors hover:bg-gray-50 dark:border-dk-muted dark:text-gray-400">{{ t('purchase.exp_report') }}</button>
</div>
<div class="flex items-center gap-2">
<label class="text-sm text-gray-500">{{ t('purchase.search') }}</label>
<input v-model="searchQuery" type="text" class="w-48 rounded-lg border border-gray-300 bg-white px-3 py-1.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" @keydown.enter="currentPage=1;fetchOrders()" />
</div>
</div>
<!-- Table -->
<div class="overflow-x-auto px-0">
<table class="w-full text-left text-sm text-gray-900">
<thead>
<tr class="border-b border-gray-200 bg-gray-50 text-gray-500 dark:border-dk-muted dark:bg-dk-base">
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400">No.</th>
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400">{{ t('purchase.item_name') }}</th>
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400">{{ t('purchase.purpose') }}</th>
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400">{{ t('purchase.quantity') }}</th>
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 whitespace-nowrap">{{ t('purchase.unit_price') }}</th>
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400 whitespace-nowrap">{{ t('purchase.total_price') }}</th>
<th class="px-6 py-3 font-medium text-gray-500 dark:text-gray-400">{{ t('purchase.status') }}</th>
</tr>
</thead>
<tbody>
<tr v-if="loading">
<td colspan="7" class="px-6 py-8 text-center text-gray-400">
<svg class="mx-auto mb-2 h-5 w-5 animate-spin text-gray-400" 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>
Loading...
</td>
</tr>
<tr
v-for="order in orders"
:key="order.ID"
class="border-b border-gray-100 transition-colors hover:bg-blue-50/50 dark:border-dk-muted/50 dark:bg-dk-card dark:hover:bg-dk-base/50"
@click="jumpToOrder(order.ID)"
>
<td class="px-6 py-3 text-gray-400">{{ order.ID }}</td>
<td class="px-6 py-3 font-medium text-gray-900 dark:text-white">{{ order.Title }}</td>
<td class="px-6 py-3 text-gray-600 dark:text-gray-300">{{ order.Remark }}</td>
<td class="px-6 py-3 text-gray-600 dark:text-gray-300">1</td>
<td class="px-6 py-3 whitespace-nowrap text-gray-500">{{ formatDate(order.UnitPriceAt) }}</td>
<td class="px-6 py-3 whitespace-nowrap text-gray-500">{{ formatDate(order.TotalPriceAt) }}</td>
<td class="px-6 py-3 text-gray-600 dark:text-gray-300">1</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="flex flex-col items-center justify-between gap-3 border-t border-gray-200 px-6 py-3 sm:flex-row dark:border-dk-muted">
<div class="flex items-center gap-1.5 text-sm text-gray-500">
<label>{{ t('purchase.show') }}</label>
<input type="text" class="w-14 rounded border border-gray-300 px-2 py-1 text-center text-sm text-gray-900 dark:border-dk-muted dark:bg-dk-base dark:text-white" :value="pageSize" @change="handlePageSizeInput" />
<label>{{ t('purchase.entries') }}</label>
<span class="ml-1">{{ t('purchase.There_are_a_total_of') }} {{ totalCount }} {{ t('purchase.items') }}</span>
</div>
<div class="flex items-center gap-1">
<button class="rounded p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 disabled:opacity-40 dark:hover:bg-dk-card" :disabled="currentPage <= 1" @click="goToPage(1)"><IconChevronsLeft :size="16" /></button>
<button class="rounded p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 disabled:opacity-40 dark:hover:bg-dk-card" :disabled="currentPage <= 1" @click="goToPage(currentPage - 1)"><IconChevronLeftPipe :size="16" /></button>
<template v-for="a in pageRange" :key="a">
<button
class="min-w-[32px] rounded px-2 py-1 text-sm font-medium transition-colors"
:class="a === currentPage ? 'bg-blue-600 text-white' : 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-dk-card dark:text-gray-400 dark:hover:bg-dk-card'"
@click="goToPage(a)"
>{{ a }}</button>
</template>
<button class="rounded p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 disabled:opacity-40 dark:hover:bg-dk-card" :disabled="currentPage >= totalPages" @click="goToPage(currentPage + 1)"><IconChevronRightPipe :size="16" /></button>
<button class="rounded p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 disabled:opacity-40 dark:hover:bg-dk-card" :disabled="currentPage >= totalPages" @click="goToPage(totalPages)"><IconChevronsRight :size="16" /></button>
<input type="text" class="ml-2 w-14 rounded border border-gray-300 px-2 py-1 text-center text-sm text-gray-900 dark:border-dk-muted dark:bg-dk-base dark:text-white" @change="handleJumpPageInput" />
</div>
</div>
</div>
</div>
</template>
@@ -1,481 +1,296 @@
<script setup>
import { onMounted, watch, ref, reactive } from "vue";
import { useI18n } from "vue-i18n";
<script setup>
import { reactive, ref, computed, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useToastStore } from '@/stores/toast'
import { usePageTitle } from '@/composables/usePageTitle'
import { useValidation } from '@/composables'
import { purchaseApi } from '@/api/purchase'
import tagadder from '@/components/tagadder.vue'
import datePicker from '@/components/datePicker.vue'
import useDropzone from '@/components/useDropzone.vue'
import MyOffcanvas from "@/components/MyOffcanvas.vue";
usePageTitle('purchase.add_part')
const { t, locale } = useI18n()
const toast = useToastStore()
const { validate, errors, clearErrors } = useValidation()
import tagadder from "@/components/tagadder.vue";
import dateTimePicker from "@/components/dateTimePicker.vue";
const textMaxLen = 256
const photosRef = ref(null)
import useDropzone from "@/components/useDropzone.vue";
const currencyOptions = { 1: 'RMB', 2: 'MOA', 3: 'HKD', 4: 'USD' }
import { useUserStore } from "@/stores/user";
const userStore = useUserStore();
const costType = computed(() => ({
1: t('cost_type.unit_price'),
2: t('cost_type.freight'),
}))
import { useRouter } from "vue-router";
const router = useRouter();
const orderStatus = computed(() => ({
1: t('order_status.pending_order'),
2: t('order_status.order_placed'),
3: t('order_status.in_transit'),
4: t('order_status.completed'),
5: t('order_status.refund_requested'),
6: t('order_status.returning'),
7: t('order_status.refunded'),
8: t('order_status.lost_package'),
}))
import "tom-select/dist/css/tom-select.css";
import { my_network_func } from "@/my_network_func";
const costEntries = reactive([])
const newCost = reactive({
type: '1', qty: 1, cost: 0, currencyType: '1',
})
const textarea_maxlen = 256;
const newCostTotal = computed(() =>
parseFloat((newCost.qty * newCost.cost).toFixed(2))
)
const title_input_dom = ref();
const photos_hash = ref();
const mos = ref();
const { t, locale } = useI18n();
//货币类型
const currency_type = reactive({
1: "RMB",
2: "MOP",
3: "HKD",
4: "USD",
});
//成本类型
const cost_type = reactive({
1: t("cost_type.unit_price"),
2: t("cost_type.freight"),
});
function update_cost_type() {
cost_type["1"] = t("cost_type.unit_price");
cost_type["2"] = t("cost_type.freight");
function addCostEntry() {
if (newCost.cost <= 0) return
costEntries.push({
type: newCost.type,
qty: newCost.qty,
cost: newCost.cost,
cost_t: newCostTotal.value,
currency_type: newCost.currencyType,
})
newCost.type = '1'
newCost.qty = 1
newCost.cost = 0
newCost.currencyType = '1'
}
//订单状态
const order_status = reactive({
1: t("order_status.pending_order"),
2: t("order_status.order_placed"),
3: t("order_status.in_transit"),
4: t("order_status.compvared"),
5: t("order_status.refund_requested"),
6: t("order_status.returning"),
7: t("order_status.refunded"),
8: t("order_status.lost_package"),
});
function update_order_status() {
order_status["1"] = t("order_status.pending_order");
order_status["2"] = t("order_status.order_placed");
order_status["3"] = t("order_status.in_transit");
order_status["4"] = t("order_status.compvared");
order_status["5"] = t("order_status.refund_requested");
order_status["6"] = t("order_status.returning");
order_status["7"] = t("order_status.refunded");
order_status["8"] = t("order_status.lost_package");
function removeCostEntry(index) {
costEntries.splice(index, 1)
}
const cost_sheet_tab = reactive([]);
// 表单对象
const cost_sheet = reactive({
type: "1",
int: 1,
cost: 0.0,
cost_t: 0.0,
currency_type: "1",
});
watch(() => newCost.cost, (val) => {
const fixed = parseFloat(val).toFixed(2)
if (parseFloat(fixed) !== val) newCost.cost = parseFloat(fixed)
})
function del_cost(key) {
cost_sheet.type = cost_sheet_tab[key].type;
cost_sheet.int = cost_sheet_tab[key].int;
cost_sheet.cost = cost_sheet_tab[key].cost;
cost_sheet.cost_t = cost_sheet_tab[key].cost_t;
cost_sheet.currency_type = cost_sheet_tab[key].currency_type;
cost_sheet_tab.splice(key, 1);
}
function add_cost() {
if (cost_sheet.cost <= 0) {
} else {
// 四舍五入到2位小数
var t = parseFloat((cost_sheet.int * cost_sheet.cost).toFixed(2));
cost_sheet.cost_t = t;
cost_sheet_tab.push(JSON.parse(JSON.stringify(cost_sheet)));
cost_sheet.type = "1";
cost_sheet.int = 1;
cost_sheet.cost = 0.0;
cost_sheet.cost_t = 0.0;
cost_sheet.currency_type = "1";
}
}
const submit_sheet = reactive({
title: "",
remark: "",
const form = reactive({
title: '',
remark: '',
photos: [],
link: "",
partname: "",
styles: "",
link: '',
style_remarks: '',
notes: '',
costs: [],
updatetime: "",
trackingnumber: "",
orderstatus: "1",
});
tracking_number: '',
express_number: '',
order_status: '1',
})
function submit_order() {
if (submit_sheet.title == "") {
title_input_dom.value.classList.add("is-invalid");
title_input_dom.value.addEventListener("input", function () {
if (this.value.trim() !== "") {
this.classList.remove("is-invalid");
//this.removeEventListener('input');
}
});
const loading = ref(false)
mos.value?.showAlert("danger", t("purchase_addorder.title"), 1000);
return;
}
//载入图片哈希列表
submit_sheet.photos = [];
var photos = photos_hash.value.return_files();
for (var i = 0; i < photos.length; i++) {
submit_sheet.photos.push(photos[i].hash);
async function handleSubmit() {
clearErrors()
const err = validate('title', form.title, t('purchase_addorder.title'))
if (!err) return
form.photos = []
if (photosRef.value?.has_some_files) {
const result = photosRef.value.get_some_files()
form.photos = result.map(f => f.name)
}
//载入价格表
submit_sheet.costs = [];
for (var i = 0; i < cost_sheet_tab.length; i++) {
//var t=cost_sheet_tab[i]
submit_sheet.costs.push(JSON.parse(JSON.stringify(cost_sheet_tab[i])));
}
form.costs = costEntries.map(h => ({
...h,
cost: Math.round(h.cost * 100),
cost_t: Math.round(h.cost_t * 100),
}))
//修改价格表里的小数,将所有价值*100去掉小数
for (var i = 0; i < submit_sheet.costs.length; i++) {
submit_sheet.costs[i].cost *= 100;
submit_sheet.costs[i].cost_t *= 100;
}
console.log(submit_sheet);
my_network_func.postJson("/purchase/addorder", submit_sheet, (r) => {
console.log(r);
});
}
function functionupdataTitle() {
document.title = "Operations." + t("purchase.add_part");
}
onMounted(() => {
functionupdataTitle();
//sele_init();
if (!userStore.isLoggedIn) {
router.push("/login");
}
});
// 监听语言变化,更新标题
watch(locale, () => {
functionupdataTitle();
update_cost_type();
update_order_status();
});
// 监听 cost 变化,自动限制小数位
watch(
() => cost_sheet.cost,
(newVal) => {
if (newVal !== null && newVal !== undefined) {
// 四舍五入到2位小数
const fixed = parseFloat(newVal).toFixed(2);
if (parseFloat(fixed) !== newVal) {
cost_sheet.cost = parseFloat(fixed);
}
loading.value = true
try {
const { errCode } = await purchaseApi.addOrder(form)
if (errCode === 0) {
toast.success(t('message.save_ok'))
} else {
toast.error(t('message.server_error'))
}
},
);
} catch {
// interceptor handled
} finally {
loading.value = false
}
}
</script>
<template>
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<h2 class="page-title">{{ t("purchase_addorder.add_order") }}</h2>
<div class="mx-auto max-w-6xl px-6 py-6">
<div class="flex flex-col gap-6 rounded-xl border border-gray-200 bg-white shadow-lg dark:border-dk-muted dark:bg-dk-card">
<!-- Order Info -->
<div class="border-b border-gray-200 px-6 py-4 dark:border-dk-muted">
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">{{ t('purchase_addorder.order_info') }}</h4>
</div>
<div class="space-y-4 px-6 py-5">
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t('purchase_addorder.title') }} <span class="text-red-500">*</span>
</label>
<input
v-model="form.title"
type="text"
class="w-full rounded-lg border border-gray-300 bg-white px-3.5 py-2 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.title ? 'border-red-500' : 'border-gray-300'"
:placeholder="t('purchase_addorder.input_title')"
/>
<span v-if="errors.title" class="mt-1 block text-xs text-red-500">{{ errors.title }}</span>
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t('purchase_addorder.remarks') }}
<span class="text-gray-400">{{ form.remark.length }}/{{ textMaxLen }}</span>
</label>
<textarea
v-model="form.remark"
class="w-full rounded-lg border border-gray-300 bg-white px-3.5 py-2 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"
rows="4"
:placeholder="t('purchase_addorder.remarks_text')"
/>
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('purchase_addorder.photo_remarks') }}</label>
<useDropzone acceptFiles="image/*" uploadURL="/api/files/upload/image" maxFiles="10" ref="photosRef" />
</div>
</div>
</div>
</div>
<div class="page-body">
<div class="container-xl">
<div class="row row-cards">
<div class="col-12">
<div class="card">
<div class="card-header">
<h4 class="card-title">
{{ t("purchase_addorder.order_info") }}
</h4>
<!-- Purchase Channel -->
<div class="border-t border-gray-200 px-6 py-4 dark:border-dk-muted">
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">{{ t('purchase_addorder.purchase_channel') }}</h4>
</div>
<div class="space-y-4 px-6 py-5">
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('purchase_addorder.link') }}</label>
<textarea
v-model="form.link"
class="w-full rounded-lg border border-gray-300 bg-white px-3.5 py-2 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"
rows="2"
placeholder="url"
/>
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('purchase_addorder.part_name') }}</label>
<input
v-model="form.style_remarks"
type="text"
class="w-full rounded-lg border border-gray-300 bg-white px-3.5 py-2 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"
:placeholder="t('purchase_addorder.part_name')"
/>
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('purchase_addorder.style_remarks') }}</label>
<tagadder :placeholder="t('purchase_addorder.add_style')" v-model="form.notes" />
</div>
<!-- costs table -->
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('purchase_addorder.cost') }}</label>
<div v-if="costEntries.length" class="mb-4 overflow-x-auto">
<table class="w-full text-left text-sm text-gray-900">
<thead>
<tr class="border-b border-gray-200 bg-gray-50 text-gray-500 dark:border-dk-muted dark:bg-dk-base">
<th class="px-3 py-2 font-medium">{{ t('purchase_addorder.type') }}</th>
<th class="px-3 py-2 font-medium">{{ t('purchase_addorder.quantity') }}</th>
<th class="px-3 py-2 font-medium">{{ t('purchase_addorder.fee') }}</th>
<th class="px-3 py-2 font-medium">{{ t('purchase_addorder.total_price') }}</th>
<th class="px-3 py-2 font-medium">{{ t('purchase_addorder.currency') }}</th>
<th class="px-3 py-2 font-medium">{{ t('purchase_addorder.operation') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, idx) in costEntries" :key="idx" class="border-b border-gray-100 dark:border-dk-muted">
<td class="px-3 py-2 font-medium text-gray-900 dark:text-white">{{ costType[item.type] }}</td>
<td class="px-3 py-2 text-gray-500">{{ item.qty }}</td>
<td class="px-3 py-2 text-gray-500">{{ item.cost }}</td>
<td class="px-3 py-2 text-gray-500">{{ item.cost_t }}</td>
<td class="px-3 py-2 text-gray-500">{{ currencyOptions[item.currency_type] }}</td>
<td class="px-3 py-2">
<button class="rounded px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20" @click="removeCostEntry(idx)">{{ t('purchase_addorder.remove') }}</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="grid grid-cols-2 gap-4 sm:grid-cols-5">
<div>
<label class="mb-1 block text-xs font-medium text-gray-500">{{ t('purchase_addorder.fee_type') }}</label>
<select v-model="newCost.type" class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-dk-muted dark:bg-dk-base dark:text-white">
<template v-for="(label, key) in costType" :key="key">
<option :value="key">{{ label }}</option>
</template>
</select>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label required">{{
t("purchase_addorder.title")
}}</label>
<input
type="text"
class="form-control"
name="example-text-input"
:placeholder="t('purchase_addorder.title')"
v-model="submit_sheet.title"
ref="title_input_dom"
/>
</div>
<div class="mb-3">
<label class="form-label"
>{{ t("purchase_addorder.remarks") }}
<span class="form-label-description"
>{{ submit_sheet.remark.length }}/{{
textarea_maxlen
}}</span
></label
>
<textarea
class="form-control mt-2 mb-2"
name="example-textarea-input"
rows="6"
:placeholder="t('purchase_addorder.remarks_text')"
:maxlength="textarea_maxlen"
v-model="submit_sheet.remark"
></textarea>
</div>
<label class="form-label mb-0">{{
t("purchase_addorder.photo_remarks")
}}</label>
<useDropzone
acceptedFiles="image/*"
uploadURL="/api/files/upload/image"
maxFiles="10"
ref="photos_hash"
></useDropzone>
<div>
<label class="mb-1 block text-xs font-medium text-gray-500">{{ t('purchase_addorder.input_quantity') }}</label>
<input v-model.number="newCost.qty" type="number" class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-dk-muted dark:bg-dk-base dark:text-white" min="1" />
</div>
<div class="card-header">
<h4 class="card-title">
{{ t("purchase_addorder.purchase_channel") }}
</h4>
<div>
<label class="mb-1 block text-xs font-medium text-gray-500">{{ t('purchase_addorder.input_fee') }}</label>
<input v-model="newCost.cost" type="number" class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-dk-muted dark:bg-dk-base dark:text-white" step="0.01" min="0" />
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">{{
t("purchase_addorder.link")
}}</label>
<textarea
name="url"
type="url"
class="form-control"
placeholder="https"
v-model="submit_sheet.link"
></textarea>
<div class="mb-3 mt-3">
<label class="form-label">{{
t("purchase_addorder.part_name")
}}</label>
<input
type="text"
class="form-control"
name="example-text-input"
:placeholder="t('purchase_addorder.part_name')"
v-model="submit_sheet.partname"
/>
</div>
<div class="mt-3">
<label class="form-label">{{
t("purchase_addorder.style_remarks")
}}</label>
<tagadder
:placeholder="t('purchase_addorder.add_style')"
v-model="submit_sheet.styles"
></tagadder>
</div>
<div class="mt-3">
<label class="form-label">{{
t("purchase_addorder.cost")
}}</label>
<table
v-show="cost_sheet_tab.length"
class="table table-vcenter card-table table-striped"
>
<thead>
<tr>
<th>{{ t("purchase_addorder.type") }}</th>
<th>{{ t("purchase_addorder.quantity") }}</th>
<th>{{ t("purchase_addorder.fee") }}</th>
<th>{{ t("purchase_addorder.total_price") }}</th>
<th>{{ t("purchase_addorder.currency") }}</th>
<th class="w-1">
{{ t("purchase_addorder.operation") }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(value, key) in cost_sheet_tab">
<td>{{ cost_type[value.type] }}</td>
<td class="text-secondary">{{ value.int }}</td>
<td class="text-secondary">{{ value.cost }}</td>
<td class="text-secondary">
{{ value.cost_t }}
</td>
<td class="text-secondary">
{{ currency_type[value.currency_type] }}
</td>
<td>
<button
class="btn btn-outline-danger"
@click="del_cost(key)"
>
{{ t("purchase_addorder.change") }}
</button>
</td>
</tr>
<!-- <tr>
<td>运输</td>
<td class="text-secondary">1</td>
<td class="text-secondary">5</td>
<td class="text-secondary">MOP</td>
<td>
<button class="btn btn-outline-danger">Del</button>
</td>
</tr> -->
</tbody>
</table>
<div class="row g-5">
<div class="col-xl-2">
{{ t("purchase_addorder.fee_type") }}
<select
ref="select_type"
class="form-control"
autocompvare="off"
value="1"
v-model="cost_sheet.type"
>
<option v-for="(value, key) in cost_type" :value="key">
{{ value }}
</option>
</select>
</div>
<div class="col-xl-3">
{{ t("purchase_addorder.input_quantity") }}
<input
type="number"
class="form-control"
min="1"
value="1"
v-model="cost_sheet.int"
/>
</div>
<div class="col-xl-3">
{{ t("purchase_addorder.input_fee") }}
<input
type="number"
class="form-control"
step="0.01"
min="0.0"
value="0.0"
v-model="cost_sheet.cost"
/>
</div>
<div class="col-xl-2">
{{ t("purchase_addorder.select_currency") }}
<select
ref="select_beast"
class="form-control"
autocompvare="off"
value="1"
v-model="cost_sheet.currency_type"
>
<option
v-for="(value, key) in currency_type"
:value="key"
>
{{ value }}
</option>
</select>
</div>
<div class="col-xl-2">
{{ t("purchase_addorder.operation") }}
<button
class="form-control btn btn-outline-primary"
@click="add_cost"
>
{{ t("purchase_addorder.add") }}
</button>
</div>
</div>
</div>
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-500">{{ t('purchase_addorder.select_currency') }}</label>
<select v-model="newCost.currencyType" class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-dk-muted dark:bg-dk-base dark:text-white">
<template v-for="(label, key) in currencyOptions" :key="key">
<option :value="key">{{ label }}</option>
</template>
</select>
</div>
<div class="card-header">
<h4 class="card-title">
{{ t("purchase_addorder.other_status") }}
</h4>
</div>
<div class="card-body">
<div class="mb-3">
<div class="row g-5">
<div class="col-xl-4">
<label class="form-label required">{{
t("purchase_addorder.update_time")
}}</label>
<dateTimePicker
v-model="submit_sheet.updatetime"
></dateTimePicker>
</div>
<div class="col-xl-4">
<label class="form-label">{{
t("purchase_addorder.tracking_number")
}}</label>
<input
type="text"
class="form-control"
:placeholder="
t('purchase_addorder.input_tracking_number')
"
v-model="submit_sheet.trackingnumber"
/>
</div>
<div class="col-xl-4">
{{ t("purchase_addorder.order_status") }}
<select
ref="select_beast"
class="form-control"
autocompvare="off"
v-model="submit_sheet.orderstatus"
>
<option v-for="(value, key) in order_status" :value="key">
{{ value }}
</option>
</select>
</div>
</div>
</div>
</div>
<div class="card-footer text-end">
<div class="d-flex">
<button
type="submit"
class="btn btn-primary ms-auto"
@click="submit_order"
>
{{ t("purchase_addorder.submit") }}
</button>
</div>
<div class="flex items-end">
<button class="w-full rounded-lg border border-gray-300 bg-blue-600 px-3 py-2 text-sm font-semibold text-blue-100 transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-blue-500/20 dark:border-dk-muted dark:text-white dark:bg-blue-600" @click="addCostEntry">{{ t('purchase_addorder.add') }}</button>
</div>
</div>
</div>
</div>
<!-- Order Status -->
<div class="border-t border-gray-200 px-6 py-4 dark:border-dk-muted">
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">{{ t('purchase_addorder.order_status') }}</h4>
</div>
<div class="px-6 py-5">
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t('purchase_addorder.update_time') }} <span class="text-red-500">*</span>
</label>
<datePicker v-model="form.tracking_number" />
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('purchase_addorder.tracking_number') }}</label>
<input
v-model="form.express_number"
type="text"
class="w-full rounded-lg border border-gray-300 bg-white px-3.5 py-2 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"
:placeholder="t('purchase_addorder.input_tracking_number')"
/>
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('purchase_addorder.order_status') }}</label>
<select v-model="form.order_status" class="w-full rounded-lg border border-gray-300 bg-white px-3.5 py-2 text-sm dark:border-dk-muted dark:bg-dk-base dark:text-white">
<template v-for="(label, key) in orderStatus" :key="key">
<option :value="key">{{ label }}</option>
</template>
</select>
</div>
</div>
</div>
<!-- Footer -->
<div class="flex justify-end border-t border-gray-200 px-6 py-4 dark:border-dk-muted">
<button
class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-blue-500/20 focus:outline-none disabled:active:scale-100"
:disabled="loading"
@click="handleSubmit"
>
<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('purchase_addorder.submit') }}
</button>
</div>
</div>
</div>
<MyOffcanvas ref="mos" />
</template>
<style></style>
@@ -1,422 +0,0 @@
<script setup>
import { onMounted, watch, ref, reactive } from "vue";
import { useI18n } from "vue-i18n";
import MyOffcanvas from "@/components/MyOffcanvas.vue";
const mos = ref();
import { my_network_func } from "@/my_network_func";
import { myfuncs } from "@/myfunc";
import { useRouter } from "vue-router";
const router = useRouter();
const { t, locale } = useI18n();
const all_items = ref(0);
const all_pages = ref(0);
const page_items = ref(10);
const now_page = ref(1);
const all_orders = ref({});
const page_start = ref(0);
const page_end = ref(0);
const page_input = ref();
const page_items_items = ref("10");
function jump_to_order(order_id) {
//console.log(order_id);
var order_str=order_id.toString()
const resolved = router.resolve({
path: "/purchase/showorder/" + order_str,
});
window.open(resolved.href, "_blank");
}
//获取订单列表
function get_orders() {
my_network_func.postJson(
"/purchase/getorders",
{
search: "",
entries: page_items.value,
page: now_page.value,
},
(r) => {
//console.log(r);
switch (r.statusCode) {
case 200:
switch (r.data.err_code) {
case 0:
all_orders.value = r.data.return.all_orders;
all_items.value = r.data.return.all_count;
all_pages.value = Math.ceil(all_items.value / page_items.value);
if (now_page.value < 3) {
page_start.value = 1;
} else {
if (now_page.value > all_pages.value - 3) {
page_start.value = all_pages.value - 4;
if (page_start.value <= 0) {
page_start.value = 1;
}
} else {
page_start.value = now_page.value - 2;
}
}
if (now_page.value > all_pages.value - 3) {
page_end.value = all_pages.value;
} else {
if (now_page.value < 3) {
page_end.value = 5;
} else {
page_end.value = now_page.value + 2;
}
}
break;
default:
mos.value?.showAlert("danger", t("message.server_error"), 5000);
break;
}
break;
default:
mos.value?.showAlert("danger", t("message.network_err"), 5000);
break;
}
},
);
}
function change_page(page) {
now_page.value = page;
get_orders();
}
function functionupdataTitle() {
document.title = "Operations." + t("appname.purchase");
}
function range(start, end) {
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
}
function page_input_change(c) {
//console.log(page_input.value);
var t = parseInt(page_input.value);
if (t > 0) {
if (t <= all_pages.value) {
page_input.value = "";
change_page(t);
}
}
}
function page_input_input(c) {
page_input.value = page_input.value.replace(/[^\d]/g, "");
//console.log(c)
}
function page_items_input_change(c) {
var t = parseInt(page_items_items.value);
page_items.value = t;
now_page.value = 1;
get_orders();
//console.log(t)
}
function page_items_input_input(c) {
page_items_items.value = page_items_items.value.replace(/[^\d]/g, "");
var t = parseInt(page_items_items.value);
if (t > 300) {
page_items_items.value = "300";
}
//console.log(c)
}
onMounted(() => {
functionupdataTitle();
get_orders();
});
// 监听语言变化,更新标题
watch(locale, () => {
functionupdataTitle();
});
</script>
<template>
<div class="page-body">
<div class="container-xl">
<div class="card">
<div class="card-header">
<h3 class="card-title">{{ t("purchase.purchase_list") }}</h3>
</div>
<div class="card-body border-bottom py-3">
<div class="d-flex">
<div class="text-secondary">
<router-link to="/purchase/addorder" class="btn btn-info m-1">
{{ t("purchase.add_part") }}
</router-link>
<button class="btn m-1">
{{ t("purchase.exp_report") }}
</button>
</div>
<!-- //搜索dom -->
<div class="ms-auto text-secondary">
{{ t("purchase.search") }}
<div class="ms-2 d-inline-block mr-2">
<input
type="text"
class="form-control form-control-sm"
aria-label="Search invoice"
/>
</div>
</div>
<div class="ms-auto text-secondary"></div>
</div>
</div>
<div class="table-responsive">
<table class="table card-table table-vcenter text-nowrap datatable">
<thead>
<tr>
<th class="w-1">
<input
class="form-check-input m-0 align-middle"
type="checkbox"
aria-label="Select all invoices"
/>
</th>
<th class="col-1">
No.
<!-- Download SVG icon from http://tabler-icons.io/i/chevron-up -->
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-sm icon-thick"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M6 15l6 -6l6 6" />
</svg>
</th>
<th class="col-3">{{ t("purchase.item_name") }}</th>
<th class="col-3">{{ t("purchase.purpose") }}</th>
<th class="w-1">{{ t("purchase.quantity") }}</th>
<th class="w-1">{{ t("purchase.created_at") }}</th>
<th class="w-1">{{ t("purchase.updated_at") }}</th>
<th class="w-1">{{ t("purchase.status") }}</th>
</tr>
</thead>
<tbody>
<tr
v-for="value in all_orders"
class="element"
@click="jump_to_order(value.ID)"
>
<td>
<input
class="form-check-input m-0 align-middle"
type="checkbox"
aria-label="Select invoice"
/>
</td>
<td>
<span class="text-muted">{{ value.ID }}</span>
</td>
<td>
{{ value.Title }}
</td>
<td>{{ value.Remark }}</td>
<td>1</td>
<td>
{{ myfuncs.formatLocalizedDate(value.CreatedAt, locale) }}
</td>
<td>{{ myfuncs.formatLocalizedDate(value.UpdatedAt) }}</td>
<td>1</td>
</tr>
</tbody>
</table>
</div>
<div class="card-footer d-flex align-items-center">
<p class="m-0 text-secondary">
{{ t("purchase.show") }}
</p>
<div class="mx-2 d-inline-block">
<input
type="text"
class="form-control form-control-sm w-6"
v-model="page_items_items"
aria-label="Invoices count"
@change="page_items_input_change"
@input="page_items_input_input"
/>
</div>
<p class="m-0 text-secondary">
{{ t("purchase.entries") }}
{{ t("purchase.There_are_a_total_of") }} {{ all_items }}
{{ t("purchase.entries") }}
</p>
<ul class="pagination m-0 ms-auto">
<li class="page-item" :class="now_page == 1 ? 'disabled' : ''">
<div
class="page-link"
:tabindex="now_page == 1 ? '-1' : ''"
:aria-disabled="now_page == 1 ? 'true' : ''"
@click="change_page(1)"
>
<!-- Download SVG icon from http://tabler-icons.io/i/chevron-left -->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="icon icon-tabler icons-tabler-outline icon-tabler-arrow-bar-to-left"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M10 12l10 0" />
<path d="M10 12l4 4" />
<path d="M10 12l4 -4" />
<path d="M4 4l0 16" />
</svg>
</div>
</li>
<li class="page-item" :class="now_page == 1 ? 'disabled' : ''">
<div
class="page-link"
:tabindex="now_page == 1 ? '-1' : ''"
:aria-disabled="now_page == 1 ? 'true' : ''"
@click="change_page(now_page - 1)"
>
<!-- Download SVG icon from http://tabler-icons.io/i/chevron-left -->
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M15 6l-6 6l6 6" />
</svg>
<!-- prev -->
</div>
</li>
<li
v-for="value in range(page_start, page_end)"
class="page-item"
:class="value == now_page ? 'active' : ''"
>
<div class="page-link" @click="change_page(value)">
{{ value }}
</div>
</li>
<li
class="page-item"
:class="now_page == all_pages ? 'disabled' : ''"
>
<div
class="page-link"
:tabindex="now_page == all_pages ? '-1' : ''"
:aria-disabled="now_page == all_pages ? 'true' : ''"
@click="change_page(now_page + 1)"
>
<!-- next -->
<!-- Download SVG icon from http://tabler-icons.io/i/chevron-right -->
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 6l6 6l-6 6" />
</svg>
</div>
</li>
<li
class="page-item"
:class="now_page == all_pages ? 'disabled' : ''"
>
<div
class="page-link"
:tabindex="now_page == all_pages ? '-1' : ''"
:aria-disabled="now_page == all_pages ? 'true' : ''"
@click="change_page(all_pages)"
>
<!-- Download SVG icon from http://tabler-icons.io/i/chevron-right -->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="icon icon-tabler icons-tabler-outline icon-tabler-arrow-bar-to-right"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M14 12l-10 0" />
<path d="M14 12l-4 4" />
<path d="M14 12l-4 -4" />
<path d="M20 4l0 16" />
</svg>
</div>
</li>
<li>
<input
type="text"
class="form-control form-control-sm w-6"
@change="page_input_change"
@input="page_input_input"
v-model="page_input"
/>
</li>
</ul>
</div>
</div>
</div>
</div>
<MyOffcanvas ref="mos" />
</template>
<style lang="scss" scoped>
.element:hover {
background-color: #4299e11c;
}
</style>
@@ -1,14 +1,17 @@
<script setup>
import { onMounted, watch, ref, reactive } from "vue";
import { useRouter } from "vue-router";
const router = useRouter();
<script setup>
import { useRouter } from 'vue-router'
import { usePageTitle } from '@/composables/usePageTitle'
const dynamicParam = router
onMounted(() => {
console.log(dynamicParam);
});
usePageTitle('purchase.add_part')
const router = useRouter()
const orderId = router.params.id
</script>
<template>
<div class="mx-auto max-w-6xl px-6 py-6">
<h2 class="mb-6 text-2xl font-bold text-gray-900 dark:text-white">Order#{{ orderId }}</h2>
<div class="rounded-xl border border-gray-200 bg-white px-12 py-12 text-center shadow-lg dark:border-dk-muted dark:bg-dk-card">
<p class="text-gray-400">{{ $t('message.functionality_not_yet_developed') }}</p>
</div>
</div>
</template>
+129 -226
View File
@@ -1,243 +1,146 @@
<script setup>
import { onMounted, watch, ref } from "vue";
import { useRouter } from "vue-router";
import MyOffcanvas from "@/components/MyOffcanvas.vue";
import { myfuncs } from "@/myfunc.js";
import { my_network_func } from "@/my_network_func";
import { useI18n } from "vue-i18n";
// 使用 vue-i18n 的 Composition API
const { t, locale } = useI18n();
const mos = ref();
const isShowPassword = ref(false);
const username = ref();
const useremail = ref();
const userpassword = ref();
const router = useRouter();
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
function functionupdataTitle() {
document.title = "Operations." + t("appname.register");
const { t, locale } = useI18n()
function toggleLocale() {
locale.value = locale.value === 'zh-CN' ? 'en' : 'zh-CN'
}
function togglePasswordVisibility() {
isShowPassword.value = !isShowPassword.value;
}
function createAccount() {
// 在这里处理创建新账户的逻辑
const user = username.value?.value;
const email = useremail.value?.value;
const pass = userpassword.value?.value;
import { useToastStore } from '@/stores/toast'
import { usePageTitle } from '@/composables/usePageTitle'
import { useValidation, isValidEmail } from '@/composables'
import { authApi } from '@/api/auth'
import { IconEye, IconEyeOff } from '@tabler/icons-vue'
username.value?.classList.remove("is-invalid");
useremail.value?.classList.remove("is-invalid");
userpassword.value?.classList.remove("is-invalid");
usePageTitle('appname.register')
const router = useRouter()
const toast = useToastStore()
const { validate, errors, clearErrors } = useValidation()
let isDataErr = false;
const form = ref({
username: '',
email: '',
password: '',
})
const showPassword = ref(false)
const loading = ref(false)
if (!user) {
isDataErr = true;
username.value?.classList.add("is-invalid");
}
if (!email) {
isDataErr = true;
useremail.value?.classList.add("is-invalid");
}
if (!pass) {
isDataErr = true;
userpassword.value?.classList.add("is-invalid");
}
async function handleRegister() {
clearErrors()
if (isDataErr) {
mos.value?.showAlert(
"info",
t("message.please_enter_username_and_password"),
5000
);
return;
}
const err1 = validate('username', form.value.username, t('message.please_enter_username_and_password'))
const err2 = validate('email', form.value.email, t('message.please_enter_your_email'), isValidEmail)
const err3 = validate('password', form.value.password, t('message.please_enter_username_and_password'))
//判断长度
if (!err1 || !err2 || !err3) return
if (!myfuncs.isValidEmail(email)) {
useremail.value?.classList.add("is-invalid");
mos.value?.showAlert("warning", t("message.this_not_email"), 5000);
return;
}
// console.log("创建新账户信息:", {
// user: username.value?.value,
// email: useremail.value?.value,
// pass: userpassword.value?.value,
// });
loading.value = true
try {
const { errCode } = await authApi.register(form.value.username, form.value.email, form.value.password)
my_network_func.postJson(
"/users/register",
{
username: username.value?.value,
useremail: useremail.value?.value,
userpass: userpassword.value?.value,
},
(r) => {
//console.log(r);
switch (r.statusCode) {
case 200:
switch (r.data.err_code) {
case -4:
username.value?.classList.add("is-invalid");
mos.value?.showAlert("warning", t("message.username_dup"), 5000);
break;
case 0:
mos.value?.showAlert(
"success",
t("message.registration_successful"),
1000,
() => {
router.push("/login");
}
);
break;
default:
mos.value?.showAlert("danger", t("message.server_error"), 5000);
break;
}
break;
default:
mos.value?.showAlert("danger", t("message.network_err"), 5000);
break;
}
switch (errCode) {
case 0:
toast.success(t('message.registration_successful'), 1500)
setTimeout(() => router.push('/login'), 1500)
break
case -4:
toast.warning(t('message.username_dup'))
errors.username = t('message.username_dup')
break
default:
toast.error(t('message.server_error'))
}
);
} catch {
// 拦截器已处理
} finally {
loading.value = false
}
}
onMounted(() => {
functionupdataTitle();
});
// 监听语言变化,更新标题
watch(locale, () => {
functionupdataTitle();
});
</script>
<template>
<div class="page page-center">
<div class="container container-tight py-4">
<div class="text-center mb-4">
<router-link to="/" class="navbar-brand navbar-brand-autodark">
<img
src="/static/logo.svg"
width="110"
height="32"
alt="Tabler"
class="navbar-brand-image"
/>
</router-link>
</div>
<div class="card card-md">
<div class="card-body">
<h2 class="card-title text-center mb-4">
{{ t("message.create_new_account") }}
</h2>
<div class="mb-3">
<label class="form-label">{{ t("message.user_name") }}</label>
<input
ref="username"
type="text"
maxlength="64"
class="form-control"
:placeholder="t('message.your_user_name')"
/>
</div>
<div class="mb-3">
<label class="form-label">{{ t("message.email_address") }}</label>
<input
ref="useremail"
type="email"
maxlength="250"
class="form-control"
:placeholder="t('message.your_email_address')"
/>
</div>
<div class="mb-3">
<label class="form-label">{{ t("message.password") }}</label>
<div class="input-group input-group-flat">
<input
ref="userpassword"
:type="isShowPassword ? 'text' : 'password'"
class="form-control"
:placeholder="t('message.your_password')"
autocomplete="off"
/>
<span class="input-group-text">
<div
class="link-secondary"
:title="
isShowPassword
? t('message.hidden_Password')
: t('message.show_password')
"
data-bs-toggle="tooltip"
>
<!-- Download SVG icon from http://tabler-icons.io/i/eye -->
<svg
v-if="!isShowPassword"
@click="togglePasswordVisibility"
xmlns="http://www.w3.org/2000/svg"
class="icon"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
<path
d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6"
/>
</svg>
<svg
v-if="isShowPassword"
@click="togglePasswordVisibility"
xmlns="http://www.w3.org/2000/svg"
class="icon"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" />
<path
d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87"
/>
<path d="M3 3l18 18" />
</svg>
</div>
</span>
</div>
</div>
<!-- <div class="mb-3">
<label class="form-check">
<input type="checkbox" class="form-check-input"/>
<span class="form-check-label">Agree the <a href="./terms-of-service.html" tabindex="-1">terms and policy</a>.</span>
</label>
</div> -->
<div class="form-footer">
<button @click="createAccount" class="btn btn-primary w-100">
{{ t("message.create_new_account") }}
</button>
</div>
</div>
</div>
<div class="text-center text-secondary mt-3">
{{ t("message.already_have_an_account") }}
<router-link to="/login">{{ t("message.back_to_login") }}</router-link>
</div>
<div class="mx-auto max-w-sm px-8">
<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>
</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>
</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.create_new_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>
<input
v-model="form.username"
type="text"
maxlength="64"
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'"
:placeholder="t('message.your_user_name')"
/>
<span v-if="errors.username" class="mt-1 block text-xs text-red-500">{{ errors.username }}</span>
</div>
<!-- Email -->
<div class="mb-4">
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('message.your_email_address') }}</label>
<input
v-model="form.email"
type="email"
maxlength="250"
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.email ? 'border-red-500 focus:border-red-500 focus:ring-red-500/20' : 'border-gray-300'"
:placeholder="t('message.your_email_address')"
/>
<span v-if="errors.email" class="mt-1 block text-xs text-red-500">{{ errors.email }}</span>
</div>
<!-- Password -->
<div class="mb-6">
<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'"
:placeholder="t('message.your_password')"
autocomplete="new-password"
@keydown.enter="handleRegister"
/>
<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>
</div>
<!-- Submit -->
<button
class="flex w-full items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-blue-500/20 focus:outline-none disabled:active:scale-100"
:disabled="loading"
@click="handleRegister"
>
<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('message.create_new_account') }}
</button>
</div>
<p class="mt-6 text-center text-sm text-gray-500">
{{ t('message.already_have_an_account') }}
<RouterLink to="/login" class="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400">{{ t('message.back_to_login') }}</RouterLink>
</p>
</div>
<MyOffcanvas ref="mos" />
</template>
+56 -101
View File
@@ -1,134 +1,89 @@
<script setup>
import FullCalendar from "@fullcalendar/vue3";
import dayGridPlugin from "@fullcalendar/daygrid";
import timeGridPlugin from "@fullcalendar/timegrid";
import interactionPlugin from "@fullcalendar/interaction"; //拖动插件 需要用npm安装
import listPlugin from "@fullcalendar/list";
<script setup>
import { ref, watch } from 'vue'
import FullCalendar from '@fullcalendar/vue3'
import dayGridPlugin from '@fullcalendar/daygrid'
import timeGridPlugin from '@fullcalendar/timegrid'
import interactionPlugin from '@fullcalendar/interaction'
import listPlugin from '@fullcalendar/list'
import { useI18n } from 'vue-i18n'
import { usePageTitle } from '@/composables/usePageTitle'
import { onMounted, watch, ref } from "vue";
import { useI18n } from "vue-i18n";
const { t, locale } = useI18n();
const calendar = ref(null);
usePageTitle('appname.schedule')
const { t, locale } = useI18n()
const calendarRef = ref(null)
const calendarOptions = ref({
height: "auto",
height: 'auto',
locale: locale.value,
plugins: [
dayGridPlugin,
timeGridPlugin,
interactionPlugin, //导入拖动插件
listPlugin,
],
fixedWeekCount: false, //是否固定显示6行
weekNumbers: true,
initialView: "dayGridMonth", //默认月视图 dayGridMonth timeGridWeek listWeek
editable: true,
plugins: [dayGridPlugin, timeGridPlugin, interactionPlugin, listPlugin],
nowIndicator: true,
weekends: true,
initialView: 'dayGridMonth',
selectable: true,
editable: true,
dayMaxEvents: true,
navLinks: true,
firstDay: 1,
dayCellDidMount(info) {
switch (info.dow) {
case 0:
info.el.style.backgroundColor = "#ffb5b5";
break;
case 6:
info.el.style.backgroundColor = "#ffb5b5";
break;
if (info.date.getDay() === 0 || info.date.getDay() === 6) {
info.el.style.backgroundColor = '#f5f5f5'
}
if (info.isToday) {
//info.el.style.backgroundColor = '#ffff7f';
}
info.el.style.border = "1px solid #4b4b4b"; // 浅蓝色边框
info.el.style.border = '1px solid #e5e7eb'
},
headerToolbar: {
left: "prevYearCustom,prevMonthCustom,todayCustom,nextMonthCustom,nextYearCustom",
center: "title",
right: "", //,timeGridWeek,timeGridDay'
left: 'prevYear,prev,next,nextYear',
center: 'title',
right: '',
},
// 自定义按钮
customButtons: {
prevYearCustom: {
prevYear: {
text: t('schedule.previous_year'),
click: function () {
calendar.value.getApi().prevYear();
},
click() { calendarRef.value.getApi().prevYear() },
},
nextYearCustom: {
nextYear: {
text: t('schedule.next_year'),
click: function () {
calendar.value.getApi().nextYear();
},
click() { calendarRef.value.getApi().nextYear() },
},
prevMonthCustom: {
prevMonth: {
text: t('schedule.previous_month'),
click: function () {
calendar.value.getApi().prev();
},
click() { calendarRef.value.getApi().prev() },
},
nextMonthCustom: {
nextMonth: {
text: t('schedule.next_month'),
click: function () {
calendar.value.getApi().next();
},
click() { calendarRef.value.getApi().next() },
},
todayCustom: {
text: t('schedule.month'),
click: function () {
calendar.value.getApi().today();
},
week: {
text: t('schedule.week'),
click() { calendarRef.value.getApi().changeView('timeGridWeek') },
},
},
events: [
{ title: "事件 1", start: "2025-11-10" },
{ title: "事件 2", start: "2025-11-15", end: "2024-06-17" },
{
title: "事件 3",
start: "2025-11-20T10:30:00",
end: "2024-06-20T12:30:00",
},
{ title: 'Event1', date: '2025-11-10' },
{ title: 'Event2', date: '2025-11-15', end: '2025-11-17' },
{ title: 'Event3', date: '2025-11-20T10:30:00', end: '2025-11-20T12:30:00' },
],
});
})
function functionupdataTitle() {
document.title = "Operations." + t("appname.schedule");
}
// 监听语言变化,更新标题
watch(locale, () => {
functionupdataTitle();
calendarOptions.value.locale = locale.value;
// 更新自定义按钮文本
calendarOptions.value.customButtons.prevYearCustom.text = t('schedule.previous_year');
calendarOptions.value.customButtons.nextYearCustom.text = t('schedule.next_year');
calendarOptions.value.customButtons.prevMonthCustom.text = t('schedule.previous_month');
calendarOptions.value.customButtons.nextMonthCustom.text = t('schedule.next_month');
calendarOptions.value.customButtons.todayCustom.text = t('schedule.month');
});
onMounted(() => {
functionupdataTitle();
});
calendarOptions.value.locale = locale.value
calendarOptions.value.headerToolbar.customButtons.prevYear.text = t('schedule.previous_year')
calendarOptions.value.headerToolbar.customButtons.nextYear.text = t('schedule.next_year')
calendarOptions.value.headerToolbar.customButtons.prevMonth.text = t('schedule.previous_month')
calendarOptions.value.headerToolbar.customButtons.nextMonth.text = t('schedule.next_month')
calendarOptions.value.headerToolbar.customButtons.week.text = t('schedule.week')
})
</script>
<template>
<FullCalendar ref="calendar" :options="calendarOptions" />
<div class="mx-auto max-w-6xl px-6 py-6">
<h2 class="mb-6 text-2xl font-bold text-gray-900 dark:text-white">{{ t('schedule.my_schedule') }}</h2>
<div class="rounded-xl border border-gray-200 bg-white px-4 py-4 shadow-lg dark:border-dk-muted dark:bg-dk-card">
<FullCalendar ref="calendarRef" :options="calendarOptions" />
</div>
</div>
</template>
<style scoped>
/* .fc-prevYearCustom-button {
background-color: #4CAF50 !important;
color: white !important;
border: none !important;
border-radius: 5px !important;
padding: 8px 16px !important;
font-weight: bold !important;
} */
</style>
@@ -0,0 +1,163 @@
<script setup>
import { reactive, ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
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 SettingNav from '@/components/SettingNav.vue'
import ImageCropper from '@/components/imageCropper.vue'
usePageTitle('settings.account_settings')
const { t } = useI18n()
const userStore = useUserStore()
const toast = useToastStore()
const { validate, errors, clearErrors } = useValidation()
const form = reactive({
username: '',
remark: '',
birthday: '',
})
const avatarHasChanged = ref(false)
const avatarDataUrl = ref('')
const loading = ref(false)
onMounted(() => {
if (userStore.user) {
form.username = userStore.user.Username || ''
form.remark = userStore.user.FirstName || ''
form.birthday = userStore.birthday
}
})
function handleCrop(dataUrl) {
avatarHasChanged.value = true
avatarDataUrl.value = dataUrl
}
function cancelAvatar() {
avatarHasChanged.value = false
avatarDataUrl.value = ''
}
function base64ToFile(base64) {
const [info, data] = base64.split(',')
const mime = info.match(/:(.*?);/)[1]
const bytes = atob(data)
const arr = new Uint8Array(bytes.length)
for (let i = 0; i < bytes.length; i++) arr[i] = bytes.charCodeAt(i)
return new File([arr], 'avatar.png', { type: mime })
}
async function handleSave() {
clearErrors()
const err1 = validate('username', form.username, t('settings.name'))
const err2 = validate('remark', form.remark, t('settings.remark'))
const err3 = validate('birthday', form.birthday, t('settings.birthday'))
if (!err1 || !err2 || !err3) return
loading.value = true
try {
if (avatarHasChanged.value) {
const file = base64ToFile(avatarDataUrl.value)
await authApi.updateAvatar(file)
avatarHasChanged.value = false
}
const { errCode } = await authApi.updateInfo({
username: form.username,
remark: form.remark,
birthday: form.birthday,
})
if (errCode === 0) {
toast.success(t('message.save_ok'))
await userStore.fetchUserInfo()
} else {
toast.error(t('message.server_error'))
}
} catch {
// 拦截器已处理
} finally {
loading.value = false
}
}
</script>
<template>
<div class="mx-auto max-w-5xl px-6 py-6">
<h2 class="mb-6 text-2xl font-bold text-gray-900 dark:text-white">{{ t('settings.my_account') }}</h2>
<div class="flex flex-col gap-6 lg:flex-row lg:gap-8">
<SettingNav />
<div class="flex-1 space-y-6">
<!-- Avatar -->
<div class="mb-6 flex items-center gap-4">
<div>
<img
:src="avatarHasChanged ? avatarDataUrl : userStore.avatarUrl"
alt="Avatar"
class="h-16 w-16 rounded-full border-2 border-gray-200 object-cover dark:border-dk-muted"
/>
</div>
<div>
<ImageCropper @crop-data-url="handleCrop" />
<button v-if="avatarHasChanged" class="mt-2 rounded-lg border border-gray-300 px-3 py-1 text-xs text-gray-600 hover:bg-gray-50 dark:border-dk-muted dark:text-gray-400 dark:hover:bg-dk-card" @click="cancelAvatar">
{{ t('settings.cancel') }}
</button>
</div>
</div>
<h3 class="mb-4 text-sm font-semibold uppercase text-gray-400 tracking-wider dark:text-gray-500">Profile</h3>
<!-- Form -->
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('settings.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 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' : 'border-gray-300'"
/>
<span v-if="errors.username" class="mt-1 block text-xs text-red-500">{{ errors.username }}</span>
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('settings.remark') }}</label>
<input
v-model="form.remark"
type="text"
class="w-full rounded-lg border border-gray-300 bg-white px-3.5 py-2 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.remark ? 'border-red-500' : 'border-gray-300'"
/>
<span v-if="errors.remark" class="mt-1 block text-xs text-red-500">{{ errors.remark }}</span>
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('settings.birthday') }}</label>
<input
v-model="form.birthday"
type="date"
class="w-full rounded-lg border border-gray-300 bg-white px-3.5 py-2 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.birthday ? 'border-red-500' : 'border-gray-300'"
/>
<span v-if="errors.birthday" class="mt-1 block text-xs text-red-500">{{ errors.birthday }}</span>
</div>
</div>
<div class="mt-6">
<button
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-blue-500/20 focus:outline-none disabled:active:scale-100"
:disabled="loading"
@click="handleSave"
>
{{ t('settings.save_changes') }}
</button>
</div>
</div>
</div>
</div>
</template>
@@ -0,0 +1,81 @@
<script setup>
import { reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/stores/user'
import { useToastStore } from '@/stores/toast'
import { usePageTitle } from '@/composables/usePageTitle'
import { useValidation, isValidEmail } from '@/composables'
import { authApi } from '@/api/auth'
import SettingNav from '@/components/SettingNav.vue'
usePageTitle('settings.contact_information')
const { t } = useI18n()
const userStore = useUserStore()
const toast = useToastStore()
const { validate, errors, clearErrors } = useValidation()
const form = reactive({ email: '' })
const loading = ref(false)
async function handleChangeEmail() {
clearErrors()
const err = validate('email', form.email, t('message.please_enter_your_email'), isValidEmail)
if (!err) return
loading.value = true
try {
const { errCode } = await authApi.changeEmail(form.email)
switch (errCode) {
case 0:
toast.success(t('message.change_ok'))
await userStore.fetchUserInfo()
break
case -43:
form.email = t('message.this_not_email')
toast.error(t('message.this_not_email'))
break
default:
toast.error(t('message.server_error'))
}
} catch {
// 拦截器已处理
} finally {
loading.value = false
}
}
</script>
<template>
<div class="mx-auto max-w-5xl px-6 py-6">
<h2 class="mb-6 text-2xl font-bold text-gray-900 dark:text-white">{{ t('settings.my_account') }}</h2>
<div class="flex flex-col gap-6 lg:flex-row lg:gap-8">
<SettingNav />
<div class="flex-1 space-y-6">
<h3 class="mb-4 text-sm font-semibold uppercase text-gray-400 tracking-wider dark:text-gray-500">{{ t('settings.email') }}</h3>
<div class="flex flex-col gap-4 sm:flex-row sm:items-start">
<div class="flex-1">
<input
v-model="form.email"
type="email"
class="w-full rounded-lg border border-gray-300 bg-white px-3.5 py-2 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.email ? 'border-red-500' : 'border-gray-300'"
:placeholder="t('message.your_email_address')"
@keydown.enter="handleChangeEmail"
/>
<span v-if="errors.email" class="mt-1 block text-xs text-red-500">{{ errors.email }}</span>
</div>
<button
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-blue-500/20 focus:outline-none disabled:active:scale-100"
:disabled="loading"
@click="handleChangeEmail"
>
{{ t('settings.change_email') }}
</button>
</div>
</div>
</div>
</div>
</template>
@@ -0,0 +1,132 @@
<script setup>
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
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 SettingNav from '@/components/SettingNav.vue'
usePageTitle('settings.security_settings')
const { t } = useI18n()
const router = useRouter()
const userStore = useUserStore()
const toast = useToastStore()
const { validate, errors, clearErrors } = useValidation()
const form = reactive({
oldpass: '',
newpass: '',
confirm: '',
})
const showPassword = ref(false)
const loading = ref(false)
async function handleChangePassword() {
clearErrors()
const err1 = validate('oldpass', form.oldpass, t('message.type_old_pass'))
const err2 = validate('newpass', form.newpass, t('message.type_new_pass'))
const err3 = validate('confirm', form.confirm, t('message.type_cof_pass'))
if (form.newpass !== form.confirm) {
errors.confirm = t('message.confirm_password_incorrect')
toast.warning(t('message.confirm_password_incorrect'))
}
if (!err1 || !err2 || !err3 || errors.confirm) return
loading.value = true
try {
const { errCode } = await authApi.changePassword(form.oldpass, form.newpass)
switch (errCode) {
case 0:
form.oldpass = ''
form.newpass = ''
form.confirm = ''
toast.success(t('message.change_ok'), 2000)
setTimeout(() => {
userStore.logout()
router.push('/')
}, 2000)
break
case -42:
form.oldpass = t('message.old_pass_incorrect')
toast.error(t('message.old_pass_incorrect'))
break
default:
toast.error(t('message.server_error'))
}
} catch {
// 拦截器已处理
} finally {
loading.value = false
}
}
</script>
<template>
<div class="mx-auto max-w-5xl px-6 py-6">
<h2 class="mb-6 text-2xl font-bold text-gray-900 dark:text-white">{{ t('settings.my_account') }}</h2>
<div class="flex flex-col gap-6 lg:flex-row lg:gap-8">
<SettingNav />
<div class="flex-1 space-y-6">
<div class="mb-4 flex items-center gap-2">
<h3 class="text-sm font-semibold uppercase text-gray-400 tracking-wider dark:text-gray-500">{{ t('settings.password') }}</h3>
<button type="button" class="rounded p-1 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 disabled:hover:bg-transparent dark:text-gray-500 dark:hover:text-gray-300" @click="showPassword = !showPassword">
<IconEye v-if="!showPassword" :size="16" />
<IconEyeOff v-else :size="16" />
</button>
</div>
<div class="flex flex-col gap-4 sm:flex-row sm:items-start">
<div>
<input
v-model="form.oldpass"
:type="showPassword ? 'text' : 'password'"
class="w-full rounded-lg border border-gray-300 bg-white px-3.5 py-2 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.oldpass ? 'border-red-500' : 'border-gray-300'"
:placeholder="t('message.type_old_pass')"
/>
<span v-if="errors.oldpass" class="mt-1 block text-xs text-red-500">{{ errors.oldpass }}</span>
</div>
<div>
<input
v-model="form.newpass"
:type="showPassword ? 'text' : 'password'"
class="w-full rounded-lg border border-gray-300 bg-white px-3.5 py-2 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.newpass ? 'border-red-500' : 'border-gray-300'"
:placeholder="t('message.type_new_pass')"
/>
<span v-if="errors.newpass" class="mt-1 block text-xs text-red-500">{{ errors.newpass }}</span>
</div>
<div>
<input
v-model="form.confirm"
:type="showPassword ? 'text' : 'password'"
class="w-full rounded-lg border border-gray-300 bg-white px-3.5 py-2 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.confirm ? 'border-red-500' : 'border-gray-300'"
:placeholder="t('message.type_cof_pass')"
@keydown.enter="handleChangePassword"
/>
<span v-if="errors.confirm" class="mt-1 block text-xs text-red-500">{{ errors.confirm }}</span>
</div>
</div>
<div class="mt-4">
<button
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-blue-500/20 focus:outline-none disabled:active:scale-100"
:disabled="loading"
@click="handleChangePassword"
>
{{ t('settings.set_new_password') }}
</button>
</div>
</div>
</div>
</div>
</template>
@@ -1,251 +0,0 @@
<script setup>
import { onMounted, watch, ref } from "vue";
import settingNavigation from "@/components/settingNavigation.vue";
import { useI18n } from "vue-i18n";
import datePicker from "@/components/datePicker.vue";
import imageCropper from "@/components/imageCropper.vue";
import { useUserStore } from "@/stores/user";
import { my_network_func } from "@/my_network_func";
import MyOffcanvas from "@/components/MyOffcanvas.vue";
import { useRouter } from "vue-router";
const mos = ref();
const { t, locale } = useI18n();
const router = useRouter();
const birthday = ref();
const username = ref();
const userremark = ref();
const userStore = useUserStore();
const is_avatar_change = ref(false);
const avatar_temp_url = ref("");
const avatar_canvas=ref();
// 将 Base64 转换为 File 对象
function base64ToFile(base64Data, filename) {
const arr = base64Data.split(',');
const mime = arr[0].match(/:(.*?);/)[1];
const bstr = atob(arr[1] || base64Data);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, { type: mime });
}
function updataInfo() {
let isDataErr = false;
let birthdayValue = birthday.value.datepicker.value;
let usernameValue = username.value.value;
let userremarkValue = userremark.value.value;
username.value?.classList.remove("is-invalid");
userremark.value?.classList.remove("is-invalid");
birthday.value?.datepicker.classList.remove("is-invalid");
if (!usernameValue) {
isDataErr = true;
username.value?.classList.add("is-invalid");
}
if (!userremarkValue) {
isDataErr = true;
userremark.value?.classList.add("is-invalid");
}
if (!birthdayValue) {
isDataErr = true;
birthday.value?.datepicker.classList.add("is-invalid");
}
if (isDataErr) {
//console.log("用户信息有误,无法保存");
return;
}
//检查头像是否需要更新
if(is_avatar_change.value)
{
my_network_func.postflise("/users/updateAvatar",base64ToFile(avatar_temp_url.value,"avatar.png"),(r)=>{
is_avatar_change.value=false
//console.log(r)
switch (r.statusCode) {
case 200:
switch (r.data.err_code) {
case 0:
//mos.value?.showAlert("success", t("message.save_ok"), 1000);
is_avatar_change.value=false
// 更新用户信息到store
//userStore.getUserInfoFromCookie();
break;
default:
mos.value?.showAlert("danger", t("message.server_error"), 5000);
break;
}
break;
default:
mos.value?.showAlert("danger", t("message.network_err"), 5000);
break;
}
})
}
my_network_func.postJson(
"/users/updateInfo",
{
username: usernameValue,
remark: userremarkValue,
birthday: birthdayValue,
},
(r) => {
//console.log(r);
switch (r.statusCode) {
case 200:
switch (r.data.err_code) {
case 0:
mos.value?.showAlert("success", t("message.save_ok"), 1000);
// 更新用户信息到store
userStore.getUserInfoFromCookie();
break;
default:
mos.value?.showAlert("danger", t("message.server_error"), 5000);
break;
}
break;
default:
mos.value?.showAlert("danger", t("message.network_err"), 5000);
break;
}
}
);
}
function rev_avatar_canvas(canvas) {
is_avatar_change.value = true;
avatar_temp_url.value = canvas.toDataURL("image/png");
avatar_canvas.value=canvas
//console.log(url)
}
function cancel_change_avatar(){
is_avatar_change.value=false
}
function functionupdataTitle() {
document.title = "Operations." + t("settings.basic_information");
}
// 监听语言变化,更新标题
watch(locale, () => {
functionupdataTitle();
});
onMounted(() => {
//console.log("account mounted");
//username.value.value="Kevin";
functionupdataTitle();
if (!userStore.isLoggedIn) {
router.push("/login");
}
});
</script>
<template>
<div class="page-wrapper">
<div class="page-header d-print-none">
<div class="container-xl">
<h2 class="page-title">{{ t("settings.my_account") }}</h2>
</div>
</div>
<div class="page-body">
<div class="container-xl">
<div class="card">
<div class="row g-0">
<settingNavigation />
<div class="col-12 col-md-9 d-flex flex-column">
<div class="card-body">
<!-- <h2 class="mb-4">{{ t("settings.my_account") }}</h2> -->
<!-- <h3 class="card-title">
{{ t("settings.profile_information") }}
</h3> -->
<div class="row align-items-center">
<div class="col-auto">
<img
:src="
is_avatar_change
? avatar_temp_url
: userStore.getUserAvatarPath()
"
alt=""
class="avatar avatar-xl"
/>
</div>
<!-- <imageCropper /> -->
<div class="col-auto " >
<imageCropper @crop_to_canvas="rev_avatar_canvas"></imageCropper>
<button v-show="is_avatar_change" class="btn btn-outline-secondary " @click="cancel_change_avatar">
{{ t("settings.cancel") }}
</button>
</div>
</div>
<h3 class="card-title mt-4">-</h3>
<div class="row g-3">
<div class="col-md">
<div class="form-label">{{ t("settings.name") }}</div>
<input
ref="username"
type="text"
class="form-control"
:value="
userStore.userInfo ? userStore.userInfo.Username : ''
"
/>
</div>
<div class="col-md">
<div class="form-label">{{ t("settings.remark") }}</div>
<input
ref="userremark"
type="text"
class="form-control"
:value="
userStore.userInfo ? userStore.userInfo.FirstName : ''
"
/>
</div>
<div class="col-md">
<div class="form-label">{{ t("settings.birthday") }}</div>
<datePicker
ref="birthday"
:setdef="userStore.getUserBirthday()"
/>
</div>
<div>
<button class="btn" @click="updataInfo">
{{ t("settings.save_changes") }}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<MyOffcanvas ref="mos" />
</template>
@@ -1,135 +0,0 @@
<script setup>
import { onMounted, watch, ref } from "vue";
import settingNavigation from "@/components/settingNavigation.vue";
import { useI18n } from "vue-i18n";
import { useUserStore } from "@/stores/user";
import { my_network_func } from "@/my_network_func";
import MyOffcanvas from "@/components/MyOffcanvas.vue";
import { myfuncs } from "@/myfunc";
import { useRouter } from "vue-router";
const mos = ref();
const { t, locale } = useI18n();
const router = useRouter();
const emailInput = ref();
const userStore = useUserStore();
function changeEmail() {
if (emailInput.value.value == "") {
emailInput.value.classList.add("is-invalid");
mos.value?.showAlert("warning", t("message.please_enter_your_email"), 3000);
return;
} else {
emailInput.value.classList.remove("is-invalid");
}
//判断是否是合法邮箱
if (myfuncs.isValidEmail(emailInput.value.value) == false) {
emailInput.value.classList.add("is-invalid");
mos.value?.showAlert("danger", t("message.this_not_email"), 3000);
return;
} else {
emailInput.value.classList.remove("is-invalid");
}
my_network_func.postJson(
"/users/changeEmail",
{
newemail: emailInput.value.value,
},
(r) => {
switch (r.statusCode) {
case 200:
switch (r.data.err_code) {
case 0:
mos.value?.showAlert("success", t("message.change_ok"), 5000);
// 更新用户信息到store
userStore.getUserInfoFromCookie();
break;
case -43:
emailInput.value.classList.add("is-invalid");
mos.value?.showAlert("danger", t("message.this_not_email"), 3000);
break;
default:
mos.value?.showAlert("danger", t("message.server_error"), 5000);
break;
}
break;
default:
mos.value?.showAlert("danger", t("message.network_err"), 5000);
break;
}
}
);
}
function functionupdataTitle() {
document.title = "Operations." + t("settings.contact_information");
}
// 监听语言变化,更新标题
watch(locale, () => {
functionupdataTitle();
});
onMounted(() => {
//console.log("account mounted");
//username.value.value="Kevin";
functionupdataTitle();
if (!userStore.isLoggedIn) {
router.push("/login");
}
});
</script>
<template>
<div class="page-wrapper">
<div class="page-header d-print-none">
<div class="container-xl">
<h2 class="page-title">{{ t("settings.my_account") }}</h2>
</div>
</div>
<div class="page-body">
<div class="container-xl">
<div class="card">
<div class="row g-0">
<settingNavigation />
<div class="col-12 col-md-9 d-flex flex-column">
<div class="card-body">
<h3 class="card-title mt-4">{{ t("settings.email") }}</h3>
<div>
<div class="row g-2">
<div class="col-auto">
<input
ref="emailInput"
type="text"
class="form-control w-auto"
:value="userStore.user.Email"
:placeholder="t('message.your_email_address')"
/>
</div>
<div class="col-auto">
<button class="btn" @click="changeEmail">
{{ t("settings.change_email") }}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<MyOffcanvas ref="mos" />
</template>
@@ -1,230 +0,0 @@
<script setup>
import { onMounted, watch, ref } from "vue";
import settingNavigation from "@/components/settingNavigation.vue";
import { useI18n } from "vue-i18n";
import { useUserStore } from "@/stores/user";
import { my_network_func } from "@/my_network_func";
import MyOffcanvas from "@/components/MyOffcanvas.vue";
import { useRouter } from "vue-router";
const mos = ref();
const { t, locale } = useI18n();
const router = useRouter();
const userStore = useUserStore();
const oldPassInput = ref();
const newPassInput = ref();
const cnfPassInput = ref();
const isShowPassword = ref(false);
function togglePasswordVisibility() {
isShowPassword.value = !isShowPassword.value;
}
function changePassword() {
let isDataErr = false;
let oldPass = oldPassInput.value.value;
let newPass = newPassInput.value.value;
let cnfPass = cnfPassInput.value.value;
oldPassInput.value.classList.remove("is-invalid");
newPassInput.value.classList.remove("is-invalid");
cnfPassInput.value.classList.remove("is-invalid");
if (!oldPass) {
isDataErr = true;
oldPassInput.value.classList.add("is-invalid");
}
if (!newPass) {
isDataErr = true;
newPassInput.value.classList.add("is-invalid");
}
if (!cnfPass) {
isDataErr = true;
cnfPassInput.value.classList.add("is-invalid");
}
if (newPass !== cnfPass) {
isDataErr = true;
newPassInput.value.classList.add("is-invalid");
cnfPassInput.value.classList.add("is-invalid");
mos.value?.showAlert(
"warning",
t("message.confirm_password_incorrect"),
3000
);
}
if (isDataErr) {
return;
}
my_network_func.postJson(
"/users/changePassword",
{
oldpass: oldPass,
newpass: newPass,
},
(r) => {
switch (r.statusCode) {
case 200:
switch (r.data.err_code) {
case 0:
// 清空输入框
oldPassInput.value.value = "";
newPassInput.value.value = "";
cnfPassInput.value.value = "";
mos.value?.showAlert(
"success",
t("message.change_ok"),
2000,
() => {
userStore.logout();
router.push("/");
}
);
break;
case -42:
oldPassInput.value.classList.add("is-invalid");
mos.value?.showAlert(
"danger",
t("message.old_pass_incorrect"),
3000
);
break;
default:
mos.value?.showAlert("danger", t("message.server_error"), 5000);
break;
}
break;
default:
mos.value?.showAlert("danger", t("message.network_err"), 5000);
break;
}
}
);
}
function functionupdataTitle() {
document.title = "Operations." + t("settings.security_settings");
}
// 监听语言变化,更新标题
watch(locale, () => {
functionupdataTitle();
});
onMounted(() => {
//console.log("account mounted");
//username.value.value="Kevin";
functionupdataTitle();
if (!userStore.isLoggedIn) {
router.push("/login");
}
});
</script>
<template>
<div class="page-wrapper">
<div class="page-header d-print-none">
<div class="container-xl">
<h2 class="page-title">{{ t("settings.my_account") }}</h2>
</div>
</div>
<div class="page-body">
<div class="container-xl">
<div class="card">
<div class="row g-0">
<settingNavigation />
<div class="col-12 col-md-9 d-flex flex-column">
<div class="card-body">
<h3 class="card-title mt-4">
{{ t("settings.password") }}
<!-- Download SVG icon from http://tabler-icons.io/i/eye -->
<svg
v-if="!isShowPassword"
@click="togglePasswordVisibility"
xmlns="http://www.w3.org/2000/svg"
class="icon"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
<path
d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6"
/>
</svg>
<svg
v-if="isShowPassword"
@click="togglePasswordVisibility"
xmlns="http://www.w3.org/2000/svg"
class="icon"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" />
<path
d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87"
/>
<path d="M3 3l18 18" />
</svg>
</h3>
<input
ref="oldPassInput"
:type="isShowPassword ? 'text' : 'password'"
class="form-control w-auto mb-1"
:placeholder="t('message.type_old_pass')"
/>
<input
ref="newPassInput"
:type="isShowPassword ? 'text' : 'password'"
class="form-control w-auto mb-1"
:placeholder="t('message.type_new_pass')"
/>
<input
ref="cnfPassInput"
:type="isShowPassword ? 'text' : 'password'"
class="form-control w-auto mb-1"
:placeholder="t('message.type_cof_pass')"
/>
<div>
<button class="btn" @click="changePassword">
{{ t("settings.set_new_password") }}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<MyOffcanvas ref="mos" />
</template>
-11
View File
@@ -1,11 +0,0 @@
<script setup>
import useDropzone from '@/components/useDropzone.vue';
</script>
<template>
<useDropzone></useDropzone>
<!-- <useDropzoneBootstrap></useDropzoneBootstrap>
<useFilePond></useFilePond> -->
</template>
@@ -1,72 +0,0 @@
<template>
<div class="page-wrapper">
<!-- Page header -->
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<h2 class="page-title">Cards</h2>
</div>
</div>
</div>
</div>
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="row row-cards">
<div class="col-md-6 col-lg-3">
<div class="card">
<!-- Photo -->
<div
class="img-responsive img-responsive-21x9 card-img-top"
style="
background-image: url(./static/photos/home-office-desk-with-macbook-iphone-calendar-watch-and-organizer.jpg);
"
></div>
<div class="card-body">
<h3 class="card-title">Card with top image</h3>
<p class="text-secondary card_text">
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
</p>
</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="card">
<!-- Photo -->
<div
class="img-responsive img-responsive-21x9 card-img-top"
style="
background-image: url(./static/photos/home-office-desk-with-macbook-iphone-calendar-watch-and-organizer.jpg);
"
></div>
<div class="card-body">
<h3 class="card-title">Card with top image</h3>
<p class="text-secondary card_text">
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Aperiam deleniti fugit incidunt, iste, itaque minima neque
pariatur perferendis sed suscipit velit vitae voluptatem. Lorem, ipsum dolor sit amet consectetur adipisicing elit. Illo optio et fuga omnis ipsa, odit repellendus iste doloremque est, nam eius quisquam perspiciatis deserunt. Quasi tempore velit architecto corporis voluptatibus!
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.card_text{
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2; /* 限制显示4行 */
overflow: hidden;
line-height: 1.5;
min-height: calc(1.5em * 2); /* 最小高度 */
}
</style>