vue使用语法糖

This commit is contained in:
鼠子
2025-07-15 13:27:02 +08:00
parent 7b4cef5d35
commit a96efc9f07
12 changed files with 1030 additions and 1317 deletions

View File

@@ -1,4 +1,53 @@
<!-- src/App.vue --> <script setup>
import { ref, watch } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const isLoggedIn = ref(false)
const isLoading = ref(true)
// 检查登录状态
const checkLoginStatus = () => {
isLoggedIn.value = !!localStorage.getItem('user-token')
}
// 退出登录
const logout = () => {
localStorage.removeItem('user-token')
isLoggedIn.value = false
if (router.currentRoute.value.meta.requiresAuth) {
router.push('/')
}
}
// 路由变化时检查登录状态
watch(() => router.currentRoute.value, () => {
checkLoginStatus()
})
// 组件创建时检查登录状态
checkLoginStatus()
// 确保主题和样式已完全加载后再显示内容
nextTick(() => {
setTimeout(() => {
isLoading.value = false
}, 200)
})
// 路由变化时的加载状态
router.beforeEach(() => {
isLoading.value = true
})
router.afterEach(() => {
setTimeout(() => {
isLoading.value = false
}, 100)
})
</script>
<template> <template>
<v-app> <v-app>
<!-- 添加加载指示器 --> <!-- 添加加载指示器 -->
@@ -15,61 +64,6 @@
</v-app> </v-app>
</template> </template>
<script>
export default {
data() {
return {
isLoggedIn: false,
isLoading: true
}
},
created() {
this.checkLoginStatus()
},
mounted() {
// 确保主题和样式已完全加载后再显示内容
this.$nextTick(() => {
// 短暂延迟确保DOM完全渲染
setTimeout(() => {
this.isLoading = false
}, 200)
})
// 添加路由变化时的加载状态
this.$router.beforeEach((to, from, next) => {
this.isLoading = true
next()
})
this.$router.afterEach(() => {
// 路由加载完成后,短暂延迟以确保组件已渲染
setTimeout(() => {
this.isLoading = false
}, 100)
})
},
methods: {
checkLoginStatus() {
this.isLoggedIn = !!localStorage.getItem('user-token')
},
logout() {
localStorage.removeItem('user-token')
this.isLoggedIn = false
// 如果在管理页面退出,则重定向到首页
if (this.$route.meta.requiresAuth) {
this.$router.push('/')
}
}
},
watch: {
$route() {
this.checkLoginStatus()
}
}
}
</script>
<style> <style>
.loading-overlay { .loading-overlay {
position: fixed; position: fixed;

View File

@@ -1,3 +1,39 @@
<script setup>
const items = [
{
title: 'Vuetify Documentation',
icon: `$vuetify`,
href: 'https://vuetifyjs.com/',
},
{
title: 'Vuetify Support',
icon: 'mdi-shield-star-outline',
href: 'https://support.vuetifyjs.com/',
},
{
title: 'Vuetify X',
icon: ['M2.04875 3.00002L9.77052 13.3248L1.99998 21.7192H3.74882L10.5519 14.3697L16.0486 21.7192H22L13.8437 10.8137L21.0765 3.00002H19.3277L13.0624 9.76874L8.0001 3.00002H2.04875ZM4.62054 4.28821H7.35461L19.4278 20.4308H16.6937L4.62054 4.28821Z'],
href: 'https://x.com/vuetifyjs',
},
{
title: 'Vuetify GitHub',
icon: `mdi-github`,
href: 'https://github.com/vuetifyjs/vuetify',
},
{
title: 'Vuetify Discord',
icon: ['M22,24L16.75,19L17.38,21H4.5A2.5,2.5 0 0,1 2,18.5V3.5A2.5,2.5 0 0,1 4.5,1H19.5A2.5,2.5 0 0,1 22,3.5V24M12,6.8C9.32,6.8 7.44,7.95 7.44,7.95C8.47,7.03 10.27,6.5 10.27,6.5L10.1,6.33C8.41,6.36 6.88,7.53 6.88,7.53C5.16,11.12 5.27,14.22 5.27,14.22C6.67,16.03 8.75,15.9 8.75,15.9L9.46,15C8.21,14.73 7.42,13.62 7.42,13.62C7.42,13.62 9.3,14.9 12,14.9C14.7,14.9 16.58,13.62 16.58,13.62C16.58,13.62 15.79,14.73 14.54,15L15.25,15.9C15.25,15.9 17.33,16.03 18.73,14.22C18.73,14.22 18.84,11.12 17.12,7.53C17.12,7.53 15.59,6.36 13.9,6.33L13.73,6.5C13.73,6.5 15.53,7.03 16.56,7.95C16.56,7.95 14.68,6.8 12,6.8M9.93,10.59C10.58,10.59 11.11,11.16 11.1,11.86C11.1,12.55 10.58,13.13 9.93,13.13C9.29,13.13 8.77,12.55 8.77,11.86C8.77,11.16 9.28,10.59 9.93,10.59M14.1,10.59C14.75,10.59 15.27,11.16 15.27,11.86C15.27,12.55 14.75,13.13 14.1,13.13C13.46,13.13 12.94,12.55 12.94,11.86C12.94,11.16 13.45,10.59 14.1,10.59Z'],
href: 'https://community.vuetifyjs.com/',
},
{
title: 'Vuetify Reddit',
icon: `mdi-reddit`,
href: 'https://reddit.com/r/vuetifyjs',
},
]
</script>
<template> <template>
<v-footer <v-footer
app app
@@ -36,41 +72,6 @@
</v-footer> </v-footer>
</template> </template>
<script setup>
const items = [
{
title: 'Vuetify Documentation',
icon: `$vuetify`,
href: 'https://vuetifyjs.com/',
},
{
title: 'Vuetify Support',
icon: 'mdi-shield-star-outline',
href: 'https://support.vuetifyjs.com/',
},
{
title: 'Vuetify X',
icon: ['M2.04875 3.00002L9.77052 13.3248L1.99998 21.7192H3.74882L10.5519 14.3697L16.0486 21.7192H22L13.8437 10.8137L21.0765 3.00002H19.3277L13.0624 9.76874L8.0001 3.00002H2.04875ZM4.62054 4.28821H7.35461L19.4278 20.4308H16.6937L4.62054 4.28821Z'],
href: 'https://x.com/vuetifyjs',
},
{
title: 'Vuetify GitHub',
icon: `mdi-github`,
href: 'https://github.com/vuetifyjs/vuetify',
},
{
title: 'Vuetify Discord',
icon: ['M22,24L16.75,19L17.38,21H4.5A2.5,2.5 0 0,1 2,18.5V3.5A2.5,2.5 0 0,1 4.5,1H19.5A2.5,2.5 0 0,1 22,3.5V24M12,6.8C9.32,6.8 7.44,7.95 7.44,7.95C8.47,7.03 10.27,6.5 10.27,6.5L10.1,6.33C8.41,6.36 6.88,7.53 6.88,7.53C5.16,11.12 5.27,14.22 5.27,14.22C6.67,16.03 8.75,15.9 8.75,15.9L9.46,15C8.21,14.73 7.42,13.62 7.42,13.62C7.42,13.62 9.3,14.9 12,14.9C14.7,14.9 16.58,13.62 16.58,13.62C16.58,13.62 15.79,14.73 14.54,15L15.25,15.9C15.25,15.9 17.33,16.03 18.73,14.22C18.73,14.22 18.84,11.12 17.12,7.53C17.12,7.53 15.59,6.36 13.9,6.33L13.73,6.5C13.73,6.5 15.53,7.03 16.56,7.95C16.56,7.95 14.68,6.8 12,6.8M9.93,10.59C10.58,10.59 11.11,11.16 11.1,11.86C11.1,12.55 10.58,13.13 9.93,13.13C9.29,13.13 8.77,12.55 8.77,11.86C8.77,11.16 9.28,10.59 9.93,10.59M14.1,10.59C14.75,10.59 15.27,11.16 15.27,11.86C15.27,12.55 14.75,13.13 14.1,13.13C13.46,13.13 12.94,12.55 12.94,11.86C12.94,11.16 13.45,10.59 14.1,10.59Z'],
href: 'https://community.vuetifyjs.com/',
},
{
title: 'Vuetify Reddit',
icon: `mdi-reddit`,
href: 'https://reddit.com/r/vuetifyjs',
},
]
</script>
<style scoped lang="sass"> <style scoped lang="sass">
.social-link :deep(.v-icon) .social-link :deep(.v-icon)
color: rgba(var(--v-theme-on-background), var(--v-disabled-opacity)) color: rgba(var(--v-theme-on-background), var(--v-disabled-opacity))

View File

@@ -1,3 +1,75 @@
<script setup>
import { ref, onMounted } from 'vue'
import storageService from '@/services/storage_service'
const cachedItemsCount = ref(0)
const lastCleanTime = ref(null)
const cacheMessage = ref('')
const cacheMessageType = ref('info')
const clearing = ref(false)
/**
* 格式化日期显示
* @param {Date} date - 日期对象
* @returns {string} 格式化的日期字符串
*/
const formatDate = (date) => {
if (!date) return ''
try {
return new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).format(date)
} catch (e) {
return date.toString()
}
}
/**
* 更新缓存信息
*/
const updateCacheInfo = () => {
try {
const allItems = storageService.getAllCachedItems()
cachedItemsCount.value = Object.keys(allItems).length
const cleanTimeStr = localStorage.getItem('findreve-last-clean-time')
lastCleanTime.value = cleanTimeStr ? new Date(parseInt(cleanTimeStr)) : null
} catch (error) {
console.error('获取缓存信息失败', error)
}
}
/**
* 清除所有缓存
*/
const clearCache = async () => {
clearing.value = true
try {
await new Promise(resolve => setTimeout(resolve, 600))
storageService.clearAllCache()
localStorage.setItem('findreve-last-clean-time', Date.now().toString())
updateCacheInfo()
cacheMessage.value = '缓存已成功清除'
cacheMessageType.value = 'success'
} catch (error) {
console.error('清除缓存失败', error)
cacheMessage.value = '清除缓存失败: ' + error.message
cacheMessageType.value = 'error'
} finally {
clearing.value = false
}
}
onMounted(() => {
updateCacheInfo()
})
</script>
<template> <template>
<v-card class="my-3"> <v-card class="my-3">
<v-card-title class="d-flex align-center"> <v-card-title class="d-flex align-center">
@@ -41,96 +113,3 @@
</v-card-text> </v-card-text>
</v-card> </v-card>
</template> </template>
<script>
/**
* 缓存状态组件
*
* 显示当前本地缓存的状态信息,支持清除缓存
*/
import storageService from '@/services/storage_service';
export default {
name: 'CacheStatus',
data() {
return {
cachedItemsCount: 0,
lastCleanTime: null,
cacheMessage: '',
cacheMessageType: 'info',
clearing: false
}
},
created() {
this.updateCacheInfo();
},
methods: {
/**
* 更新缓存信息
*/
updateCacheInfo() {
try {
const allItems = storageService.getAllCachedItems();
this.cachedItemsCount = Object.keys(allItems).length;
// 获取上次清理时间这里需要在storage_service中添加记录
const cleanTimeStr = localStorage.getItem('findreve-last-clean-time');
this.lastCleanTime = cleanTimeStr ? new Date(parseInt(cleanTimeStr)) : null;
} catch (error) {
console.error('获取缓存信息失败', error);
}
},
/**
* 清除所有缓存
*/
async clearCache() {
this.clearing = true;
try {
// 小延迟以显示加载效果
await new Promise(resolve => setTimeout(resolve, 600));
storageService.clearAllCache();
localStorage.setItem('findreve-last-clean-time', Date.now().toString());
this.updateCacheInfo();
this.cacheMessage = '缓存已成功清除';
this.cacheMessageType = 'success';
} catch (error) {
console.error('清除缓存失败', error);
this.cacheMessage = '清除缓存失败: ' + error.message;
this.cacheMessageType = 'error';
} finally {
this.clearing = false;
}
},
/**
* 格式化日期显示
*
* @param {Date} date - 日期对象
* @returns {string} 格式化的日期字符串
*/
formatDate(date) {
if (!date) return '';
try {
return new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).format(date);
} catch (e) {
return date.toString();
}
}
}
}
</script>

View File

@@ -1,3 +1,12 @@
<script setup>
const systemInfo = {
version: '2.0.0 Alpha',
releaseDate: '2025-07-15',
framework: 'FastAPI + Vue'
}
</script>
<template> <template>
<div> <div>
<h2 class="text-h4 mb-4">关于 Findreve</h2> <h2 class="text-h4 mb-4">关于 Findreve</h2>
@@ -62,18 +71,3 @@
</v-card> </v-card>
</div> </div>
</template> </template>
<script>
export default {
name: 'AboutSystemComponent',
data() {
return {
systemInfo: {
version: '2.0.0 Alpha',
releaseDate: '2025-07-15',
framework: 'FastAPI + Vue'
}
}
}
};
</script>

View File

@@ -1,3 +1,45 @@
<script setup>
import { ref, watch, computed } from 'vue'
const props = defineProps({
items: {
type: Array,
default: () => []
}
})
// 仪表盘数据
const itemStats = ref({
total: 0,
normal: 0,
lost: 0,
scans: 0
})
/**
* 更新统计信息
*/
const updateStats = (items) => {
itemStats.value.total = items.length
itemStats.value.normal = items.filter(item => item.status === 'ok').length
itemStats.value.lost = items.filter(item => item.status === 'lost').length
itemStats.value.scans = items.reduce((sum, item) => sum + (item.views || 0), 0)
if (itemStats.value.scans === 0) itemStats.value.scans = Math.floor(Math.random() * 100) + 50
}
/**
* 计算百分比
*/
const getPercentage = (type) => {
if (itemStats.value.total === 0) return 0
return Math.round((itemStats.value[type] / itemStats.value.total) * 100)
}
watch(() => props.items, (newItems) => {
updateStats(newItems)
}, { immediate: true })
</script>
<template> <template>
<div> <div>
<h2 class="text-h4 mb-4">仪表盘</h2> <h2 class="text-h4 mb-4">仪表盘</h2>
@@ -41,73 +83,3 @@
</v-row> </v-row>
</div> </div>
</template> </template>
<script>
/**
* 仪表盘组件
*
* 显示物品统计信息和系统概览
*/
export default {
name: 'DashboardComponent',
props: {
items: {
type: Array,
default: () => []
}
},
data() {
return {
// 仪表盘数据
itemStats: {
total: 0,
normal: 0,
lost: 0,
scans: 0
}
};
},
watch: {
items: {
handler(newItems) {
this.updateStats(newItems);
},
immediate: true
}
},
methods: {
/**
* 更新统计信息
*
* @param {Array} items - 物品列表
*/
updateStats(items) {
// 计算物品总数
this.itemStats.total = items.length;
// 计算正常和丢失物品数量
this.itemStats.normal = items.filter(item => item.status === 'ok').length;
this.itemStats.lost = items.filter(item => item.status === 'lost').length;
// 假设扫描次数是从物品中累计的一个属性
this.itemStats.scans = items.reduce((sum, item) => sum + (item.views || 0), 0);
if (this.itemStats.scans === 0) this.itemStats.scans = Math.floor(Math.random() * 100) + 50; // 示例数据
},
/**
* 计算百分比
*
* @param {string} type - 物品类型normal或lost
* @returns {number} 百分比值
*/
getPercentage(type) {
if (this.itemStats.total === 0) return 0;
return Math.round((this.itemStats[type] / this.itemStats.total) * 100);
}
}
};
</script>

View File

@@ -1,3 +1,254 @@
<script setup>
import { ref, computed, watch } from 'vue'
import apiService from '@/services/api_service'
const props = defineProps({
items: {
type: Array,
required: true
}
})
const emit = defineEmits(['refresh'])
// 界面控制
const loading = ref(false)
const search = ref('')
const statusFilter = ref('all')
// 物品管理
const editItem = ref({
id: null,
key: '',
name: '',
icon: '',
phone: '',
status: 'ok',
context: ''
})
const defaultItem = {
id: null,
key: '',
name: '',
icon: '',
phone: '',
status: 'ok',
context: ''
}
// 对话框控制
const itemDialog = ref(false)
const deleteDialog = ref(false)
const qrDialog = ref(false)
const saving = ref(false)
const deleting = ref(false)
const formValid = ref(false)
// 选中的物品和删除项
const selectedItem = ref(null)
const deleteItem = ref(null)
// 表格配置
const headers = [
{ title: 'ID', key: 'id', sortable: true },
{ title: '物品名称', key: 'name', sortable: true },
{ title: '标识码', key: 'key', sortable: true },
{ title: '状态', key: 'status', sortable: true },
{ title: '创建时间', key: 'created_at', sortable: true },
{ title: '操作', key: 'actions', sortable: false }
]
const statusOptions = [
{ title: '全部状态', value: 'all' },
{ title: '正常', value: 'ok' },
{ title: '丢失', value: 'lost' }
]
/**
* 过滤后的物品列表
*/
const filteredItems = computed(() => {
let result = [...props.items]
if (statusFilter.value !== 'all') {
result = result.filter(item => item.status === statusFilter.value)
}
return result
})
/**
* 打开物品对话框
*/
const openItemDialog = (item = null) => {
editItem.value = item ? JSON.parse(JSON.stringify(item)) : JSON.parse(JSON.stringify(defaultItem))
if (!item) {
editItem.value.key = generateRandomKey()
}
itemDialog.value = true
}
/**
* 保存物品
*/
const saveItem = async () => {
if (!formValid.value) return
try {
saving.value = true
let data
if (editItem.value.id) {
const params = new URLSearchParams()
const { id, key, name, icon, phone, status, context } = editItem.value
params.append('id', id)
params.append('key', key)
params.append('name', name)
params.append('icon', icon || '')
params.append('phone', phone)
params.append('status', status)
if (status === 'lost' && context) {
params.append('context', context)
}
data = await apiService.patch(`/api/admin/items?${params.toString()}`, '')
} else {
const params = new URLSearchParams()
const { key, name, icon, phone } = editItem.value
params.append('key', key)
params.append('name', name)
params.append('icon', icon || '')
params.append('phone', phone)
data = await apiService.post(`/api/admin/items?${params.toString()}`, '')
}
if (data.code !== 0) {
throw new Error(data.msg || '保存物品失败')
}
itemDialog.value = false
emit('refresh')
} catch (error) {
console.error('保存物品错误:', error)
} finally {
saving.value = false
}
}
/**
* 确认删除物品
*/
const confirmDelete = (item) => {
deleteItem.value = item
deleteDialog.value = true
}
/**
* 确认删除物品
*/
const deleteItemConfirm = async () => {
if (!deleteItem.value?.id) return
try {
deleting.value = true
const data = await apiService.delete(`/api/admin/items?id=${encodeURIComponent(deleteItem.value.id)}`)
if (data.code !== 0) {
throw new Error(data.msg || '删除物品失败')
}
deleteDialog.value = false
emit('refresh')
} catch (error) {
console.error('删除物品错误:', error)
} finally {
deleting.value = false
}
}
/**
* 显示二维码
*/
const showQRCode = (item) => {
selectedItem.value = item
qrDialog.value = true
}
/**
* 获取二维码URL
*/
const getQRCodeUrl = (key) => {
const currentUrl = window.location.origin
const foundUrl = `${currentUrl}/found?key=${encodeURIComponent(key)}`
return `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(foundUrl)}`
}
/**
* 生成随机标识码
*/
const generateRandomKey = () => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
let result = ''
for (let i = 0; i < 8; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length))
}
return result
}
/**
* 获取状态对应的颜色
*/
const getStatusColor = (status) => {
const statusMap = {
ok: "success",
lost: "error",
default: "grey"
}
return statusMap[status] || statusMap.default
}
/**
* 获取状态对应的文本
*/
const getStatusText = (status) => {
const statusMap = {
ok: "正常",
lost: "丢失",
default: "未知"
}
return statusMap[status] || statusMap.default
}
/**
* 格式化日期显示
*/
const formatDate = (dateStr) => {
if (!dateStr) return "未知时间"
try {
const date = new Date(dateStr)
return new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).format(date)
} catch (e) {
return dateStr
}
}
/**
* 重置所有筛选条件
*/
const resetFilters = () => {
search.value = ''
statusFilter.value = 'all'
}
</script>
<template> <template>
<div> <div>
<div class="d-flex justify-space-between align-center mb-4"> <div class="d-flex justify-space-between align-center mb-4">
@@ -260,345 +511,6 @@
</div> </div>
</template> </template>
<script>
import apiService from '@/services/api_service';
export default {
name: 'ItemsManagementComponent',
props: {
items: {
type: Array,
required: true
}
},
data() {
return {
// 界面控制
loading: false,
search: '',
statusFilter: 'all',
// 物品管理
editItem: {
id: null,
key: '',
name: '',
icon: '',
phone: '',
status: 'ok',
context: ''
},
defaultItem: {
id: null,
key: '',
name: '',
icon: '',
phone: '',
status: 'ok',
context: ''
},
// 对话框控制
itemDialog: false,
deleteDialog: false,
qrDialog: false,
saving: false,
deleting: false,
formValid: false,
// 选中的物品和删除项
selectedItem: null,
deleteItem: null,
// 表格配置
headers: [
{ title: 'ID', key: 'id', sortable: true },
{ title: '物品名称', key: 'name', sortable: true },
{ title: '标识码', key: 'key', sortable: true },
{ title: '状态', key: 'status', sortable: true },
{ title: '创建时间', key: 'created_at', sortable: true },
{ title: '操作', key: 'actions', sortable: false }
],
statusOptions: [
{ title: '全部状态', value: 'all' },
{ title: '正常', value: 'ok' },
{ title: '丢失', value: 'lost' }
]
}
},
computed: {
/**
* 过滤后的物品列表
*
* 根据搜索文本和状态筛选条件过滤物品列表
* @returns {Array} 过滤后的物品数组
*/
filteredItems() {
let result = [...this.items];
// 应用状态筛选
if (this.statusFilter !== 'all') {
result = result.filter(item => item.status === this.statusFilter);
}
return result;
}
},
methods: {
/**
* 打开物品对话框
*
* @param {Object|null} item - 要编辑的物品为null时表示添加新物品
*/
openItemDialog(item = null) {
if (item) {
this.editItem = JSON.parse(JSON.stringify(item)); // 深拷贝
} else {
this.editItem = JSON.parse(JSON.stringify(this.defaultItem));
// 为新物品生成一个随机标识码
this.editItem.key = this.generateRandomKey();
}
this.$nextTick(() => {
if (this.$refs.itemForm) {
this.$refs.itemForm.resetValidation();
}
});
this.itemDialog = true;
},
/**
* 保存物品
*
* 根据是否有ID决定是添加新物品还是更新现有物品
*/
async saveItem() {
if (!this.formValid) return;
try {
this.saving = true;
let data;
if (this.editItem.id) {
// 更新现有物品
const params = new URLSearchParams();
const { id, key, name, icon, phone, status, context } = this.editItem;
params.append('id', id);
params.append('key', key);
params.append('name', name);
params.append('icon', icon || '');
params.append('phone', phone);
params.append('status', status);
// 只有在状态为lost且有context时才添加context参数
if (status === 'lost' && context) {
params.append('context', context);
}
data = await apiService.patch(`/api/admin/items?${params.toString()}`, '');
} else {
// 添加新物品
const params = new URLSearchParams();
const { key, name, icon, phone } = this.editItem;
params.append('key', key);
params.append('name', name);
params.append('icon', icon || '');
params.append('phone', phone);
data = await apiService.post(`/api/admin/items?${params.toString()}`, '');
}
if (data.code !== 0) {
throw new Error(data.msg || '保存物品失败');
}
this.$nextTick(() => {
this.$root.$emit('show-toast', {
color: 'success',
message: this.editItem.id ? '物品更新成功' : '物品添加成功'
});
});
this.itemDialog = false;
this.refreshItems(); // 刷新物品列表
} catch (error) {
console.error('保存物品错误:', error);
this.$nextTick(() => {
this.$root.$emit('show-toast', {
color: 'error',
message: error.message || '保存物品失败'
});
});
} finally {
this.saving = false;
}
},
/**
* 确认删除物品
*
* @param {Object} item - 要删除的物品
*/
confirmDelete(item) {
this.deleteItem = item;
this.deleteDialog = true;
},
/**
* 确认删除物品
*/
async deleteItemConfirm() {
if (!this.deleteItem || !this.deleteItem.id) return;
try {
this.deleting = true;
const data = await apiService.delete(`/api/admin/items?id=${encodeURIComponent(this.deleteItem.id)}`);
if (data.code !== 0) {
throw new Error(data.msg || '删除物品失败');
}
this.$nextTick(() => {
this.$root.$emit('show-toast', {
color: 'success',
message: '物品已成功删除'
});
});
this.deleteDialog = false;
this.refreshItems(); // 刷新物品列表
} catch (error) {
console.error('删除物品错误:', error);
this.$nextTick(() => {
this.$root.$emit('show-toast', {
color: 'error',
message: error.message || '删除物品失败'
});
});
} finally {
this.deleting = false;
}
},
/**
* 显示二维码
*
* @param {Object} item - 要显示二维码的物品
*/
showQRCode(item) {
this.selectedItem = item;
this.qrDialog = true;
},
/**
* 获取二维码URL
*
* @param {string} key - 物品标识码
* @returns {string} 二维码图片URL
*/
getQRCodeUrl(key) {
// 使用QR Server API生成二维码
const currentUrl = window.location.origin;
const foundUrl = `${currentUrl}/found?key=${encodeURIComponent(key)}`;
return `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(foundUrl)}`;
},
/**
* 生成随机标识码
*
* @returns {string} 随机生成的标识码
*/
generateRandomKey() {
// 生成一个8位的随机字母数字组合
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < 8; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
},
/**
* 获取状态对应的颜色
*
* @param {string} status - 物品状态
* @returns {string} 对应的颜色名称
*/
getStatusColor(status) {
const statusMap = {
ok: "success",
lost: "error",
default: "grey"
};
return statusMap[status] || statusMap.default;
},
/**
* 获取状态对应的文本
*
* @param {string} status - 物品状态
* @returns {string} 对应的状态文本
*/
getStatusText(status) {
const statusMap = {
ok: "正常",
lost: "丢失",
default: "未知"
};
return statusMap[status] || statusMap.default;
},
/**
* 格式化日期显示
*
* @param {string} create_time - 日期字符串
* @returns {string} 格式化的日期文本
*/
formatDate(dateStr) {
if (!dateStr) return "未知时间";
try {
const date = new Date(dateStr);
return new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).format(date);
} catch (e) {
return dateStr;
}
},
/**
* 重置所有筛选条件
*/
resetFilters() {
this.search = '';
this.statusFilter = 'all';
},
/**
* 刷新物品列表
*/
refreshItems() {
this.$emit('refresh');
}
}
};
</script>
<style scoped> <style scoped>
/* 确保数据表格在移动设备上响应式滚动 */ /* 确保数据表格在移动设备上响应式滚动 */
@media (max-width: 768px) { @media (max-width: 768px) {

View File

@@ -1,3 +1,18 @@
<script setup>
import CacheStatus from '@/components/CacheStatus.vue'
const userInfo = {
username: 'admin',
email: 'admin@example.com'
}
const settings = {
darkMode: false,
notifications: true
}
</script>
<template> <template>
<div> <div>
<h2 class="text-h4 mb-4">用户设置</h2> <h2 class="text-h4 mb-4">用户设置</h2>
@@ -68,26 +83,3 @@
<p class="text-caption text-center mt-4">更多设置功能正在开发中...</p> <p class="text-caption text-center mt-4">更多设置功能正在开发中...</p>
</div> </div>
</template> </template>
<script>
import CacheStatus from '@/components/CacheStatus.vue';
export default {
name: 'UserSettingsComponent',
components: {
CacheStatus
},
data() {
return {
userInfo: {
username: 'admin',
email: 'admin@example.com'
},
settings: {
darkMode: false,
notifications: true
}
}
}
};
</script>

View File

@@ -1,4 +1,68 @@
<!-- src/views/Admin.vue --> <script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import apiService from '@/services/api_service'
import Dashboard from '@/components/admin/Dashboard.vue'
import ItemsManagement from '@/components/admin/ItemsManagement.vue'
import UserSettings from '@/components/admin/UserSettings.vue'
import AboutSystem from '@/components/admin/AboutSystem.vue'
const router = useRouter()
// 界面控制
const drawer = ref(false)
const currentTab = ref('dashboard')
const items = ref([])
// 检查用户是否已登录
const checkAuth = () => {
const token = localStorage.getItem('user-token')
if (!token) {
router.push({
path: '/login',
query: { redirect: router.currentRoute.value.fullPath }
})
}
}
// 获取物品列表
const fetchItems = async () => {
try {
const data = await apiService.get('/api/admin/items')
if (data.code === 0 && Array.isArray(data.data)) {
items.value = data.data
} else {
throw new Error(data.msg || '获取物品列表失败')
}
} catch (error) {
console.error('获取物品列表错误:', error)
nextTick(() => {
emit('show-toast', {
color: 'error',
message: error.message || '加载物品数据失败'
})
})
}
}
// 退出登录
const logout = () => {
localStorage.removeItem('user-token')
router.push('/login')
nextTick(() => {
emit('show-toast', {
color: 'info',
message: '您已成功退出登录'
})
})
}
// 组件创建时执行
checkAuth()
fetchItems()
</script>
<template> <template>
<v-container fluid class="admin-container"> <v-container fluid class="admin-container">
<!-- 页面顶部应用栏 --> <!-- 页面顶部应用栏 -->
@@ -61,103 +125,6 @@
</v-container> </v-container>
</template> </template>
<script>
/**
* 管理面板组件
*
* 提供物品管理功能,包括添加、编辑、删除物品,
* 以及查看物品状态和生成二维码等功能。
*
* 此组件还包含仪表盘视图,显示物品统计信息和最近活动。
*/
import apiService from '@/services/api_service';
import Dashboard from '@/components/admin/Dashboard.vue';
import ItemsManagement from '@/components/admin/ItemsManagement.vue';
import UserSettings from '@/components/admin/UserSettings.vue';
import AboutSystem from '@/components/admin/AboutSystem.vue';
export default {
name: 'AdminView',
components: {
Dashboard,
ItemsManagement,
UserSettings,
AboutSystem
},
data() {
return {
// 界面控制
drawer: false,
currentTab: 'dashboard',
items: [], // 保存物品数据以便共享给子组件
}
},
created() {
// 检查用户是否已登录
this.checkAuth();
// 获取物品列表
this.fetchItems();
},
methods: {
/**
* 检查用户是否已登录
*
* 如果未登录,重定向到登录页面
*/
checkAuth() {
const token = localStorage.getItem('user-token');
if (!token) {
this.$router.push({
path: '/login',
query: { redirect: this.$route.fullPath }
});
}
},
/**
* 获取物品列表
*
* 从API获取所有物品数据
*/
async fetchItems() {
try {
const data = await apiService.get('/api/admin/items');
if (data.code === 0 && Array.isArray(data.data)) {
this.items = data.data;
} else {
throw new Error(data.msg || '获取物品列表失败');
}
} catch (error) {
console.error('获取物品列表错误:', error);
this.$nextTick(() => {
this.$root.$emit('show-toast', {
color: 'error',
message: error.message || '加载物品数据失败'
});
});
}
},
/**
* 退出登录
*/
logout() {
localStorage.removeItem('user-token');
this.$router.push('/login');
this.$nextTick(() => {
this.$root.$emit('show-toast', {
color: 'info',
message: '您已成功退出登录'
});
});
}
}
};
</script>
<style scoped> <style scoped>
.admin-container { .admin-container {
min-height: 100vh; min-height: 100vh;

View File

@@ -1,4 +1,142 @@
<!-- src/views/Found.vue --> <script setup>
import { ref, onMounted } from 'vue'
import apiService from '@/services/api_service'
import storageService from '@/services/storage_service'
// 响应式数据
const key = ref(null)
const item = ref(null)
const loading = ref(true)
const error = ref(null)
const showQRDialog = ref(false)
const isFromCache = ref(false)
/**
* 获取状态对应的颜色
* @param {string} status - 物品状态
* @returns {string} 对应的颜色名称
*/
const getStatusColor = (status) => {
const statusMap = {
ok: "success",
lost: "error",
default: "grey"
}
return statusMap[status] || statusMap.default
}
/**
* 获取状态对应的文本
* @param {string} status - 物品状态
* @returns {string} 对应的状态文本
*/
const getStatusText = (status) => {
const statusMap = {
ok: "正常",
lost: "丢失",
default: "未知"
}
return statusMap[status] || statusMap.default
}
/**
* 格式化日期显示
* @param {string} dateStr - 日期字符串
* @returns {string} 格式化的日期文本
*/
const formatDate = (dateStr) => {
if (!dateStr) return "未知时间"
try {
const date = new Date(dateStr)
return new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).format(date)
} catch (e) {
return dateStr
}
}
/**
* 从缓存加载数据并获取最新数据
* 优先显示本地缓存的数据同时从API获取最新数据
*/
const loadFromCacheAndFetch = async () => {
try {
// 先尝试从缓存获取数据
const cachedItem = storageService.getItemFromCache(key.value)
if (cachedItem) {
// 如果有缓存,立即显示
item.value = cachedItem
isFromCache.value = true
loading.value = true // 保持加载状态,同时获取最新数据
// 在后台获取最新数据
fetchItemDetails(true)
} else {
// 没有缓存,直接获取最新数据
loading.value = true
fetchItemDetails(false)
}
} catch (err) {
console.error("Error loading cached data:", err)
// 如果缓存加载失败,直接获取最新数据
fetchItemDetails(false)
}
}
/**
* 获取物品详情数据
* @param {boolean} isBackground - 是否在后台获取数据(已显示缓存数据)
*/
const fetchItemDetails = async (isBackground = false) => {
try {
if (!isBackground) {
loading.value = true
}
const data = await apiService.getObject(key.value)
// 更新本地缓存
storageService.saveItemToCache(key.value, data)
// 更新界面数据
item.value = data
isFromCache.value = false
} catch (err) {
console.error("Error fetching item details:", err)
// 如果是后台请求且已有缓存数据显示,则不显示错误
if (!isBackground || !item.value) {
error.value = "获取物品信息失败:" + err.message
}
} finally {
loading.value = false
}
}
// 组件挂载时执行
onMounted(() => {
// 从URL获取物品的key
const urlParams = new URLSearchParams(window.location.search)
key.value = urlParams.get('key')
if (key.value) {
// 尝试先从缓存获取数据
loadFromCacheAndFetch()
} else {
loading.value = false
error.value = "缺少物品标识,无法获取信息"
}
})
</script>
<template> <template>
<v-container class="found-container"> <v-container class="found-container">
<v-row justify="center"> <v-row justify="center">
@@ -115,160 +253,6 @@
</v-container> </v-container>
</template> </template>
<script>
/**
* 物品详情页面
*
* 显示找到的物品的详细信息,包括联系方式、物品状态等
* 支持从本地缓存加载数据,提高页面加载速度
*/
import apiService from '@/services/api_service';
import storageService from '@/services/storage_service';
export default {
name: "FoundView",
data() {
return {
key: null,
item: null,
loading: true,
error: null,
showQRDialog: false,
isFromCache: false, // 标识数据是否来自缓存
};
},
created() {
// 从URL获取物品的key
const urlParams = new URLSearchParams(window.location.search);
this.key = urlParams.get('key');
if (this.key) {
// 尝试先从缓存获取数据
this.loadFromCacheAndFetch();
} else {
this.loading = false;
this.error = "缺少物品标识,无法获取信息";
}
},
methods: {
/**
* 从缓存加载数据并获取最新数据
*
* 优先显示本地缓存的数据同时从API获取最新数据
*/
async loadFromCacheAndFetch() {
try {
// 先尝试从缓存获取数据
const cachedItem = storageService.getItemFromCache(this.key);
if (cachedItem) {
// 如果有缓存,立即显示
this.item = cachedItem;
this.isFromCache = true;
this.loading = true; // 保持加载状态,同时获取最新数据
// 在后台获取最新数据
this.fetchItemDetails(true);
} else {
// 没有缓存,直接获取最新数据
this.loading = true;
this.fetchItemDetails(false);
}
} catch (err) {
console.error("Error loading cached data:", err);
// 如果缓存加载失败,直接获取最新数据
this.fetchItemDetails(false);
}
},
/**
* 获取物品详情数据
*
* @param {boolean} isBackground - 是否在后台获取数据(已显示缓存数据)
*/
async fetchItemDetails(isBackground = false) {
try {
if (!isBackground) {
this.loading = true;
}
const data = await apiService.getObject(this.key);
// 更新本地缓存
storageService.saveItemToCache(this.key, data);
// 更新界面数据
this.item = data;
this.isFromCache = false;
} catch (err) {
console.error("Error fetching item details:", err);
// 如果是后台请求且已有缓存数据显示,则不显示错误
if (!isBackground || !this.item) {
this.error = "获取物品信息失败:" + err.message;
}
} finally {
this.loading = false;
}
},
/**
* 获取状态对应的颜色
*
* @param {string} status - 物品状态
* @returns {string} 对应的颜色名称
*/
getStatusColor(status) {
const statusMap = {
ok: "success",
lost: "error",
default: "grey"
};
return statusMap[status] || statusMap.default;
},
/**
* 获取状态对应的文本
*
* @param {string} status - 物品状态
* @returns {string} 对应的状态文本
*/
getStatusText(status) {
const statusMap = {
ok: "正常",
lost: "丢失",
default: "未知"
};
return statusMap[status] || statusMap.default;
},
/**
* 格式化日期显示
*
* @param {string} dateStr - 日期字符串
* @returns {string} 格式化的日期文本
*/
formatDate(dateStr) {
if (!dateStr) return "未知时间";
try {
const date = new Date(dateStr);
return new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).format(date);
} catch (e) {
return dateStr;
}
}
}
};
</script>
<style scoped> <style scoped>
.found-container { .found-container {
padding-top: 20px; padding-top: 20px;

View File

@@ -1,3 +1,243 @@
<script setup>
import { ref, onMounted, computed } from 'vue'
import storageService from '@/services/storage_service'
// 缓存相关状态
const isFromCache = ref(false)
const showCacheAlert = ref(false)
const refreshing = ref(false)
const cacheTimestamp = ref(null)
// 本地存储键名
const HOME_CACHE_KEY = 'home-data'
// 格式化缓存时间
const formatCacheTime = computed(() => {
if (!cacheTimestamp.value) return ''
try {
const date = new Date(cacheTimestamp.value)
return new Intl.DateTimeFormat('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).format(date)
} catch (e) {
return '未知时间'
}
})
// 从服务加载主页数据
const fetchHomeData = async () => {
await new Promise(resolve => setTimeout(resolve, 800))
return {
socialLinks,
devSkills,
designSkills,
musicSkills,
projects,
musicWorks,
timeline
}
}
// 从本地存储加载数据
const loadFromCache = () => {
try {
const cachedItem = storageService.getItemFromCache(HOME_CACHE_KEY)
if (cachedItem) {
const cachedTimestamp = storageService.getCacheTimestamp(HOME_CACHE_KEY)
if (cachedTimestamp) {
cacheTimestamp.value = cachedTimestamp
isFromCache.value = true
showCacheAlert.value = true
}
console.log('Using cached home page data')
return true
}
} catch (error) {
console.error('Error loading cached data:', error)
}
return false
}
// 保存数据到本地存储
const saveToCache = async () => {
try {
const currentData = await fetchHomeData()
storageService.saveItemToCache(HOME_CACHE_KEY, currentData)
console.log('Home page data cached')
} catch (error) {
console.error('Error saving data to cache:', error)
}
}
// 刷新数据
const refreshData = async () => {
refreshing.value = true
try {
const newData = await fetchHomeData()
storageService.saveItemToCache(HOME_CACHE_KEY, newData)
isFromCache.value = false
showCacheAlert.value = false
console.log('Home data refreshed successfully')
} catch (error) {
console.error('Error refreshing home data:', error)
} finally {
refreshing.value = false
}
}
// 组件挂载时执行
onMounted(async () => {
const hasCachedData = loadFromCache()
if (!hasCachedData) {
try {
await fetchHomeData()
saveToCache()
} catch (error) {
console.error('Error fetching initial home data:', error)
}
}
})
// 静态数据定义
// 社交媒体链接
const socialLinks = [
{ icon: 'mdi-github', url: 'https://github.com/Yuerchu' },
{ icon: 'mdi-music', url: 'https://music.163.com/#/artist?id=48986728' },
{ icon: 'mdi-web', url: 'https://www.yxqi.cn' },
{ icon: 'mdi-email', url: 'mailto:admin@yuxiaoqiu.cn' },
];
// 开发技能
const devSkills = [
{ name: 'Python', color: 'amber-darken-1' },
{ name: 'Kotlin', color: 'purple-lighten-2' },
{ name: 'Golang', color: 'light-blue' },
{ name: 'Lua', color: 'blue-darken-4' },
{ name: 'C', color: 'red' },
{ name: 'HTML/CSS', color: 'red-darken-3' },
{ name: 'JavaScript', color: 'lime-darken-3' },
{ name: 'Git', color: 'amber-darken-3' },
{ name: 'Docker', color: 'light-blue-darken-1' },
];
// 设计技能
const designSkills = [
{ name: 'Photoshop', color: 'blue-darken-4' },
{ name: 'Premiere', color: 'indigo-darken-3' },
{ name: 'After Effects', color: 'indigo-darken-4' },
{ name: 'Audition', color: 'purple-darken-4' },
{ name: 'Illustrator', color: 'amber-darken-3' },
{ name: 'UI/UX', color: 'pink-darken-2' },
{ name: 'SAI2', color: 'grey-darken-3' },
];
// 音乐技能
const musicSkills = [
{ name: 'FL Studio', color: 'orange-darken-2' },
{ name: '作曲', color: 'deep-purple' },
{ name: '编曲', color: 'indigo' },
{ name: '混音', color: 'blue' },
{ name: '母带处理', color: 'teal' },
{ name: 'Midi创作', color: 'cyan' },
];
// 项目作品
const projects = [
{
title: 'DiskNext',
tag: 'B端系统',
tagColor: 'primary',
image: 'https://cdn.vuetifyjs.com/images/cards/sunshine.jpg',
description: '基于 NiceGUI 打造的高性能网盘系统,提供快速、安全的文件存储与分享服务。',
link: 'https://pan.yxqi.cn',
tech: ['Python', 'NiceGUI', 'SQLite', 'Docker']
},
{
title: 'Findreve',
tag: 'C端应用',
tagColor: 'success',
image: 'https://cdn.vuetifyjs.com/images/cards/road.jpg',
description: '个人主页与物品丢失找回系统,帮助用户追踪和找回丢失物品。',
link: 'https://i.yxqi.cn',
tech: ['Vue', 'Vuetify', 'FastAPI', 'MySQL']
},
{
title: 'HeyAuth',
tag: 'B+C端系统',
tagColor: 'info',
image: 'https://cdn.vuetifyjs.com/images/cards/plane.jpg',
description: '多应用授权系统,提供统一的身份验证和权限管理服务。',
link: 'https://auth.yxqi.cn',
tech: ['Python', 'JWT', 'OAuth2', 'Redis']
}
];
// 音乐作品
const musicWorks = [
{
title: '与枫同奔 Run With Fun',
tag: '词曲',
description: '我愿如流星赶月那样飞奔,向着远方的梦想不断前行。',
link: 'https://music.163.com/#/song?id=2148944359',
cover: 'https://cdn.vuetifyjs.com/images/cards/foster.jpg'
},
{
title: 'HeyFun\'s Story',
tag: '自设印象曲',
description: '飞奔在星辰大海之间的少年,勇敢探索未知的世界。',
link: 'https://music.163.com/#/song?id=1889436124',
cover: 'https://cdn.vuetifyjs.com/images/cards/house.jpg'
},
{
title: '2020Fall',
tag: '年度纯音乐',
description: '耗时6个月完成的年度纯音乐作品记录2020年的回忆。',
link: 'https://music.163.com/#/song?id=1863630345',
cover: 'https://cdn.vuetifyjs.com/images/cards/store.jpg'
}
];
// 时间线
const timeline = [
{
title: '梦开始的地方',
date: '2022年1月21日',
content: '购买了第一台服务器,并搭建了第一个 Wordpress 站点,开始了我的网络创作之旅。',
color: 'primary',
icon: 'mdi-server'
},
{
title: '音乐作品发布',
date: '2023年10月29日',
content: '在网易云音乐发布了收官作《与枫同奔 Release》截止到 2025 年 4 月 21 日获得了 7000+ 播放。',
color: 'deep-purple',
icon: 'mdi-music'
},
{
title: '自建生态计划开始',
date: '2024年3月1日',
content: '从 Cloudreve 项目脱离,开始自建网盘系统 DiskNext ,迈出了建立个人技术生态的第一步。',
color: 'amber',
icon: 'mdi-cloud'
},
{
title: '当前进展',
date: '现在',
content: '目前正在开发 HeyAuth、Findreve、DiskNext 三个核心系统,构建完整的个人应用生态。',
color: 'success',
icon: 'mdi-rocket'
}
];
</script>
<template> <template>
<v-container fluid> <v-container fluid>
<!-- 顶部封面区 --> <!-- 顶部封面区 -->
@@ -279,303 +519,6 @@
</v-container> </v-container>
</template> </template>
<script setup lang="ts">
/**
* 首页视图组件
*
* 这是一个现代化的个人主页视图,包含个人介绍、技能展示、项目展示、
* 音乐作品、时间线和联系方式等部分,采用响应式设计适配各种设备。
* 支持本地数据缓存,提升用户体验。
*/
import { ref, onMounted, computed } from 'vue';
import storageService from '@/services/storage_service';
// 缓存相关状态
const isFromCache = ref(false);
const showCacheAlert = ref(false);
const refreshing = ref(false);
const cacheTimestamp = ref<number | null>(null);
// 本地存储键名
const HOME_CACHE_KEY = 'home-data';
// 格式化缓存时间
const formatCacheTime = computed(() => {
if (!cacheTimestamp.value) return '';
try {
const date = new Date(cacheTimestamp.value);
return new Intl.DateTimeFormat('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).format(date);
} catch (e) {
return '未知时间';
}
});
/**
* 从服务加载主页数据
*
* 在实际应用中这里应该是API请求
* 目前仅为模拟API请求的示例函数
* @returns {Promise<Object>} 首页数据对象
*/
const fetchHomeData = async (): Promise<any> => {
// 模拟API延迟
await new Promise(resolve => setTimeout(resolve, 800));
// 在真实应用中这里应该是API调用
// const response = await fetch('/api/home');
// return response.json();
// 这里使用静态数据模拟API返回
return {
socialLinks,
devSkills,
designSkills,
musicSkills,
projects,
musicWorks,
timeline
};
};
/**
* 从本地存储加载数据
*
* 使用统一的存储服务获取缓存的首页数据
* 如果数据存在且未过期,则使用缓存数据
*/
const loadFromCache = () => {
try {
// 从统一存储服务获取缓存数据
const cachedItem = storageService.getItemFromCache(HOME_CACHE_KEY);
if (cachedItem) {
// 找到有效缓存
// 在实际应用中,可以使用缓存数据更新组件状态
// updateComponentState(cachedItem);
// 获取缓存时间戳以显示在UI上
const cachedTimestamp = storageService.getCacheTimestamp(HOME_CACHE_KEY);
if (cachedTimestamp) {
cacheTimestamp.value = cachedTimestamp;
isFromCache.value = true;
showCacheAlert.value = true;
}
console.log('Using cached home page data');
return true;
}
} catch (error) {
console.error('Error loading cached data:', error);
}
return false;
};
/**
* 保存数据到本地存储
*
* 将首页当前状态保存到统一存储服务中
*/
const saveToCache = async () => {
try {
// 在实际应用中,这里应该获取当前组件的最新状态
// 或者直接缓存API返回的数据
const currentData = await fetchHomeData();
// 使用存储服务保存数据
storageService.saveItemToCache(HOME_CACHE_KEY, currentData);
console.log('Home page data cached');
} catch (error) {
console.error('Error saving data to cache:', error);
}
};
/**
* 刷新数据
*
* 从服务器获取最新数据并更新缓存
*/
const refreshData = async () => {
refreshing.value = true;
try {
// 从服务器获取最新数据
const newData = await fetchHomeData();
// 在实际应用中,需要用新数据更新组件状态
// updateComponentWithNewData(newData);
// 更新缓存
storageService.saveItemToCache(HOME_CACHE_KEY, newData);
// 更新UI状态
isFromCache.value = false;
showCacheAlert.value = false;
console.log('Home data refreshed successfully');
} catch (error) {
console.error('Error refreshing home data:', error);
} finally {
refreshing.value = false;
}
};
// 在组件挂载时尝试加载缓存数据,无论是否有缓存都保存当前状态
onMounted(async () => {
// 尝试加载缓存数据
const hasCachedData = loadFromCache();
// 如果没有缓存数据或缓存已过期,获取新数据并保存
if (!hasCachedData) {
try {
// 模拟API请求
await fetchHomeData();
// 保存到缓存
saveToCache();
} catch (error) {
console.error('Error fetching initial home data:', error);
}
}
});
// 社交媒体链接
const socialLinks = [
{ icon: 'mdi-github', url: 'https://github.com/Yuerchu' },
{ icon: 'mdi-music', url: 'https://music.163.com/#/artist?id=48986728' },
{ icon: 'mdi-web', url: 'https://www.yxqi.cn' },
{ icon: 'mdi-email', url: 'mailto:admin@yuxiaoqiu.cn' },
];
// 开发技能
const devSkills = [
{ name: 'Python', color: 'amber-darken-1' },
{ name: 'Kotlin', color: 'purple-lighten-2' },
{ name: 'Golang', color: 'light-blue' },
{ name: 'Lua', color: 'blue-darken-4' },
{ name: 'C', color: 'red' },
{ name: 'HTML/CSS', color: 'red-darken-3' },
{ name: 'JavaScript', color: 'lime-darken-3' },
{ name: 'Git', color: 'amber-darken-3' },
{ name: 'Docker', color: 'light-blue-darken-1' },
];
// 设计技能
const designSkills = [
{ name: 'Photoshop', color: 'blue-darken-4' },
{ name: 'Premiere', color: 'indigo-darken-3' },
{ name: 'After Effects', color: 'indigo-darken-4' },
{ name: 'Audition', color: 'purple-darken-4' },
{ name: 'Illustrator', color: 'amber-darken-3' },
{ name: 'UI/UX', color: 'pink-darken-2' },
{ name: 'SAI2', color: 'grey-darken-3' },
];
// 音乐技能
const musicSkills = [
{ name: 'FL Studio', color: 'orange-darken-2' },
{ name: '作曲', color: 'deep-purple' },
{ name: '编曲', color: 'indigo' },
{ name: '混音', color: 'blue' },
{ name: '母带处理', color: 'teal' },
{ name: 'Midi创作', color: 'cyan' },
];
// 项目作品
const projects = [
{
title: 'DiskNext',
tag: 'B端系统',
tagColor: 'primary',
image: 'https://cdn.vuetifyjs.com/images/cards/sunshine.jpg',
description: '基于 NiceGUI 打造的高性能网盘系统,提供快速、安全的文件存储与分享服务。',
link: 'https://pan.yxqi.cn',
tech: ['Python', 'NiceGUI', 'SQLite', 'Docker']
},
{
title: 'Findreve',
tag: 'C端应用',
tagColor: 'success',
image: 'https://cdn.vuetifyjs.com/images/cards/road.jpg',
description: '个人主页与物品丢失找回系统,帮助用户追踪和找回丢失物品。',
link: 'https://i.yxqi.cn',
tech: ['Vue', 'Vuetify', 'FastAPI', 'MySQL']
},
{
title: 'HeyAuth',
tag: 'B+C端系统',
tagColor: 'info',
image: 'https://cdn.vuetifyjs.com/images/cards/plane.jpg',
description: '多应用授权系统,提供统一的身份验证和权限管理服务。',
link: 'https://auth.yxqi.cn',
tech: ['Python', 'JWT', 'OAuth2', 'Redis']
}
];
// 音乐作品
const musicWorks = [
{
title: '与枫同奔 Run With Fun',
tag: '词曲',
description: '我愿如流星赶月那样飞奔,向着远方的梦想不断前行。',
link: 'https://music.163.com/#/song?id=2148944359',
cover: 'https://cdn.vuetifyjs.com/images/cards/foster.jpg'
},
{
title: 'HeyFun\'s Story',
tag: '自设印象曲',
description: '飞奔在星辰大海之间的少年,勇敢探索未知的世界。',
link: 'https://music.163.com/#/song?id=1889436124',
cover: 'https://cdn.vuetifyjs.com/images/cards/house.jpg'
},
{
title: '2020Fall',
tag: '年度纯音乐',
description: '耗时6个月完成的年度纯音乐作品记录2020年的回忆。',
link: 'https://music.163.com/#/song?id=1863630345',
cover: 'https://cdn.vuetifyjs.com/images/cards/store.jpg'
}
];
// 时间线
const timeline = [
{
title: '梦开始的地方',
date: '2022年1月21日',
content: '购买了第一台服务器,并搭建了第一个 Wordpress 站点,开始了我的网络创作之旅。',
color: 'primary',
icon: 'mdi-server'
},
{
title: '音乐作品发布',
date: '2023年10月29日',
content: '在网易云音乐发布了收官作《与枫同奔 Release》截止到 2025 年 4 月 21 日获得了 7000+ 播放。',
color: 'deep-purple',
icon: 'mdi-music'
},
{
title: '自建生态计划开始',
date: '2024年3月1日',
content: '从 Cloudreve 项目脱离,开始自建网盘系统 DiskNext ,迈出了建立个人技术生态的第一步。',
color: 'amber',
icon: 'mdi-cloud'
},
{
title: '当前进展',
date: '现在',
content: '目前正在开发 HeyAuth、Findreve、DiskNext 三个核心系统,构建完整的个人应用生态。',
color: 'success',
icon: 'mdi-rocket'
}
];
</script>
<style scoped> <style scoped>
.max-width-text { .max-width-text {
max-width: 700px; max-width: 700px;

View File

@@ -1,4 +1,102 @@
<!-- src/views/Login.vue --> <script setup>
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import apiService from '@/services/api_service'
const route = useRoute()
const router = useRouter()
// 表单数据
const username = ref('')
const password = ref('')
const errorMessage = ref('')
const loading = ref(false)
const formValid = ref(false)
const tokenExpired = ref(false)
const checkingToken = ref(true)
const loginForm = ref(null)
// 表单验证规则
const usernameRules = [
v => !!v || '用户名不能为空'
]
const passwordRules = [
v => !!v || '密码不能为空',
v => (v && v.length >= 6) || '密码长度不能小于6位'
]
// 验证现有令牌
const validateExistingToken = async () => {
try {
checkingToken.value = true
const token = localStorage.getItem('user-token')
if (token) {
const isValid = await apiService.validateToken()
if (isValid) {
console.log('令牌有效,正在重定向')
const redirectPath = route.query.redirect || '/'
router.push(redirectPath)
return
} else {
console.log('令牌无效,已清除')
localStorage.removeItem('user-token')
}
}
} catch (error) {
console.error('验证令牌时出错:', error)
} finally {
checkingToken.value = false
}
}
// 处理用户登录
const login = async () => {
const { valid } = await loginForm.value.validate()
if (!valid) return
loading.value = true
errorMessage.value = ''
try {
const result = await apiService.login(username.value, password.value)
if (result.success) {
// 登录成功
emit('show-toast', {
color: 'success',
message: '登录成功,正在跳转...'
})
// 登录成功后重定向
const redirectPath = route.query.redirect || '/'
router.push(redirectPath)
} else {
// 登录失败
errorMessage.value = result.error
}
} catch (error) {
console.error('登录错误:', error)
errorMessage.value = error.message || '登录过程中发生错误,请稍后再试'
} finally {
loading.value = false
}
}
onMounted(() => {
// 检查是否是因为令牌过期而重定向
tokenExpired.value = route.query.expired === 'true'
// 如果不是因为令牌过期重定向,则验证令牌
if (!tokenExpired.value) {
validateExistingToken()
} else {
checkingToken.value = false
}
})
</script>
<template> <template>
<v-container class="fill-height"> <v-container class="fill-height">
<v-row justify="center" align="center"> <v-row justify="center" align="center">
@@ -84,126 +182,6 @@
</v-container> </v-container>
</template> </template>
<script>
/**
* 登录页面组件
*
* 处理用户登录认证成功后保存token并重定向
* 包含表单验证、记住我功能和错误处理
* 支持显示令牌过期的提示
* 支持自动验证现有令牌并跳转
*/
import apiService from '@/services/api_service';
export default {
data() {
return {
username: '',
password: '',
errorMessage: '',
loading: false,
formValid: false,
tokenExpired: false,
checkingToken: true, // 是否正在验证令牌
usernameRules: [
v => !!v || '用户名不能为空'
],
passwordRules: [
v => !!v || '密码不能为空',
v => (v && v.length >= 6) || '密码长度不能小于6位'
]
}
},
created() {
// 检查是否是因为令牌过期而重定向
this.tokenExpired = this.$route.query.expired === 'true';
// 如果不是因为令牌过期重定向,则验证令牌
if (!this.tokenExpired) {
this.validateExistingToken();
} else {
this.checkingToken = false;
}
},
methods: {
/**
* 验证现有令牌
*
* 检查本地是否有JWT令牌如果有则验证其有效性
* 如果令牌有效,自动重定向到目标页面
*/
async validateExistingToken() {
try {
this.checkingToken = true;
const token = localStorage.getItem('user-token');
// 如果有令牌,验证其有效性
if (token) {
const isValid = await apiService.validateToken();
if (isValid) {
// 令牌有效,重定向到目标页面
console.log('令牌有效,正在重定向');
const redirectPath = this.$route.query.redirect || '/';
this.$router.push(redirectPath);
return;
} else {
// 令牌无效,清除
console.log('令牌无效,已清除');
localStorage.removeItem('user-token');
}
}
} catch (error) {
console.error('验证令牌时出错:', error);
} finally {
this.checkingToken = false;
}
},
/**
* 处理用户登录
*
* 发送登录请求到后端API处理成功和失败情况
* 支持表单验证
*/
async login() {
// 表单验证
const { valid } = await this.$refs.loginForm.validate();
if (!valid) return;
this.loading = true;
this.errorMessage = '';
try {
const result = await apiService.login(this.username, this.password);
if (result.success) {
// 登录成功
this.$root.$emit('show-toast', {
color: 'success',
message: '登录成功,正在跳转...'
});
// 登录成功后重定向
const redirectPath = this.$route.query.redirect || '/';
this.$router.push(redirectPath);
} else {
// 登录失败
this.errorMessage = result.error;
}
} catch (error) {
console.error('登录错误:', error);
this.errorMessage = error.message || '登录过程中发生错误,请稍后再试';
} finally {
this.loading = false;
}
}
}
}
</script>
<style scoped> <style scoped>
.login-card { .login-card {
border-radius: 8px; border-radius: 8px;

View File

@@ -1,3 +1,10 @@
<script setup>
/**
* 404错误页面组件
* 当用户访问不存在的路由时显示此页面
*/
</script>
<template> <template>
<v-container class="fill-height"> <v-container class="fill-height">
<v-row align="center" justify="center"> <v-row align="center" justify="center">
@@ -27,13 +34,3 @@
</v-row> </v-row>
</v-container> </v-container>
</template> </template>
<script>
/**
* 404错误页面组件
* 当用户访问不存在的路由时显示此页面
*/
export default {
name: 'NotFound'
}
</script>