chore: 同步主分支本地改动与个人开发工具链

纳入 personal-dev/OpenSpec 配置、管理端与教育 Demo 界面调整、工作人员 JWT 默认 7 天,以及参考资料与代理文档更新。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
wuzilongandCursor committed 2026-11-10 17:14:52 +08:00
1 parent e8060287e0
commit 608afb88ea
217 files changed
+15370 -5890

No files matched your search

+21 -21
View File
@@ -1,21 +1,21 @@
MIT License
Copyright (c) 2025 SuperManTT
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MIT License
Copyright (c) 2025 SuperManTT
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+26
View File
@@ -32,6 +32,32 @@
line-height: var(--el-component-custom-height) !important;
}
// 提高 Tabs 标签文字对比度,避免浅灰难辨认
.el-tabs__item {
color: var(--art-gray-800) !important;
&.is-active {
color: var(--el-color-primary) !important;
}
&:hover {
color: var(--el-color-primary) !important;
}
}
// 列表列头默认走 secondary 灰,对比不足(需盖过 Element Plus 后载变量)
.el-table {
--el-table-header-text-color: var(--art-gray-800) !important;
thead {
color: var(--art-gray-800) !important;
}
th.el-table__cell > .cell {
color: var(--art-gray-800) !important;
}
}
// 日期选择器
.el-date-range-picker {
--el-datepicker-inrange-bg-color: var(--art-gray-200) !important;
+2 -1
View File
@@ -1,5 +1,5 @@
<template>
<div class="mb-3 flex flex-wrap items-center gap-2">
<div class="list-keyword-search-bar mb-3 flex flex-wrap items-center gap-2">
<ElInput
v-model="draft"
clearable
@@ -8,6 +8,7 @@
@keyup.enter="submit"
@clear="submit"
/>
<slot />
<ElButton type="primary" @click="submit">检索</ElButton>
<ElButton @click="reset">重置</ElButton>
</div>
@@ -10,19 +10,19 @@
<div
:class="
isClickable(item, index)
? 'c-p py-1 rounded tad-200 hover:bg-active-color hover:[&_span]:text-g-600'
? 'c-p py-1 rounded tad-200 hover:bg-active-color hover:[&_span]:text-g-900'
: ''
"
@click="handleBreadcrumbClick(item, index)"
>
<span
class="block max-w-46 overflow-hidden text-ellipsis whitespace-nowrap px-1.5 text-sm text-g-600 dark:text-g-800"
class="block max-w-46 overflow-hidden text-ellipsis whitespace-nowrap px-1.5 text-sm text-g-800 dark:text-g-800"
>{{ formatMenuTitle(item.meta?.title as string) }}</span
>
</div>
<div
v-if="!isLastItem(index) && item.meta?.title"
class="mx-1 text-sm not-italic text-g-500"
class="mx-1 text-sm not-italic text-g-600"
aria-hidden="true"
>
/
@@ -68,10 +68,10 @@
@click="openSearchDialog"
>
<div class="flex-c">
<ArtSvgIcon icon="ri:search-line" class="text-sm text-g-500" />
<span class="ml-1 text-xs font-normal text-g-500">{{ $t('topBar.search.title') }}</span>
<ArtSvgIcon icon="ri:search-line" class="text-sm text-g-700" />
<span class="ml-1 text-xs font-normal text-g-700">{{ $t('topBar.search.title') }}</span>
</div>
<div class="flex-c h-5 px-1.5 text-g-500/80 border border-g-400 rounded">
<div class="flex-c h-5 px-1.5 text-g-700 border border-g-400 rounded">
<ArtSvgIcon v-if="isWindows" icon="vaadin:ctrl-a" class="text-sm" />
<ArtSvgIcon v-else icon="ri:command-fill" class="text-xs" />
<span class="ml-0.5 text-xs">k</span>
@@ -21,7 +21,7 @@
<li
class="art-card-xs inline-flex flex-cc h-8 mr-1.5 text-xs c-p hover:text-theme group"
:class="[
item.path === activeTab ? 'activ-tab !text-theme' : 'text-g-600 dark:text-g-800',
item.path === activeTab ? 'activ-tab !text-theme' : 'text-g-800 dark:text-g-800',
tabStyle === 'tab-google' ? 'google-tab relative !h-8 !leading-8 !border-none' : ''
]"
:style="{
@@ -42,7 +42,7 @@
v-show="item.icon"
:icon="item.icon"
class="text-base mr-1 group-hover:text-theme"
:class="item.path === activeTab ? 'text-theme' : 'text-g-600'"
:class="item.path === activeTab ? 'text-theme' : 'text-g-800'"
/>
{{ item.customTitle || formatMenuTitle(item.title) }}
<span
@@ -50,7 +50,7 @@
class="inline-flex flex-cc relative ml-0.5 p-1 rounded-full tad-200 hover:bg-g-200"
@click.stop="closeWorktab('current', item.path)"
>
<ArtSvgIcon icon="ri:close-large-fill" class="text-[10px] text-g-600" />
<ArtSvgIcon icon="ri:close-large-fill" class="text-[10px] text-g-700" />
</span>
<div
v-if="tabStyle === 'tab-google'"
@@ -521,7 +521,7 @@
.google-tab:not(.activ-tab):hover {
box-sizing: border-box;
color: var(--art-gray-600) !important;
color: var(--art-gray-800) !important;
background-color: var(--art-gray-200) !important;
border-bottom: 1px solid var(--default-box-color) !important;
border-radius: calc(var(--custom-radius) / 2.5 + 4px) !important;
@@ -1,7 +1,7 @@
<!-- 按钮组件 -->
<template>
<div
class="size-8.5 inline-flex flex-cc c-p text-g-600 dark:text-g-800 text-xl rounded tad-300 hover:bg-hover-color"
class="size-8.5 inline-flex flex-cc c-p text-g-800 dark:text-g-800 text-xl rounded tad-300 hover:bg-hover-color"
:class="{ 'rounded-full': circle }"
>
<ArtSvgIcon :icon="icon"></ArtSvgIcon>
+105 -40
View File
@@ -2,52 +2,58 @@
<template>
<div class="file-page art-full-height">
<ElCard class="art-table-card">
<div class="flex flex-wrap items-center gap-3 mb-4">
<ElRadioGroup v-model="bucket">
<ElRadioButton value="public">公开</ElRadioButton>
<ElRadioButton value="private">私有</ElRadioButton>
</ElRadioGroup>
<ElButton v-auth="'file:upload'" type="primary" @click="pickFiles">选择文件</ElButton>
<span class="text-sm text-g-600">可多选,也可在本页粘贴图片</span>
<input ref="fileInput" class="hidden" type="file" multiple @change="onPick" />
</div>
<div class="file-toolbar">
<div class="file-toolbar__upload">
<span class="file-toolbar__label">上传到</span>
<ElRadioGroup v-model="bucket">
<ElRadioButton value="public">公开</ElRadioButton>
<ElRadioButton value="private">私有</ElRadioButton>
</ElRadioGroup>
<ElButton v-auth="'file:upload'" type="primary" @click="pickFiles">选择文件</ElButton>
<span class="file-toolbar__hint">可多选,也可在本页粘贴图片</span>
<input ref="fileInput" class="hidden" type="file" multiple @change="onPick" />
</div>
<div v-if="tasks.length" class="mb-4">
<div v-for="task in tasks" :key="task.id" class="mb-2">
<div class="flex justify-between text-sm mb-1">
<span>{{ task.name }}</span>
<span>{{ task.status === 'error' ? task.message : `${task.percent}%` }}</span>
<div v-if="tasks.length" class="file-toolbar__tasks">
<div v-for="task in tasks" :key="task.id" class="file-toolbar__task">
<div class="file-toolbar__task-meta">
<span>{{ task.name }}</span>
<span>{{ task.status === 'error' ? task.message : `${task.percent}%` }}</span>
</div>
<ElProgress
:percentage="task.percent"
:status="
task.status === 'error'
? 'exception'
: task.status === 'success'
? 'success'
: undefined
"
/>
</div>
<ElProgress
:percentage="task.percent"
:status="
task.status === 'error'
? 'exception'
: task.status === 'success'
? 'success'
: undefined
"
</div>
<div class="file-toolbar__filters">
<ListKeywordSearch v-model="keyword" placeholder="检索文件名" @search="searchFiles">
<ElSelect
v-model="listBucket"
clearable
placeholder="全部桶"
class="file-toolbar__bucket"
@change="reload"
>
<ElOption label="公开" value="public" />
<ElOption label="私有" value="private" />
</ElSelect>
</ListKeywordSearch>
<ArtTableHeader
v-model:columns="columnChecks"
:loading="loading"
@refresh="refreshData"
/>
</div>
</div>
<ListKeywordSearch v-model="keyword" placeholder="检索文件名" @search="searchFiles" />
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
<template #left>
<ElSelect
v-model="listBucket"
clearable
placeholder="全部桶"
style="width: 140px"
@change="reload"
>
<ElOption label="公开" value="public" />
<ElOption label="私有" value="private" />
</ElSelect>
</template>
</ArtTableHeader>
<ArtTable
:loading="loading"
:data="data"
@@ -277,3 +283,62 @@
}
}
</script>
<style scoped>
.file-toolbar {
display: flex;
flex-direction: column;
gap: 14px;
margin-bottom: 12px;
padding-bottom: 16px;
border-bottom: 1px solid var(--el-border-color-lighter);
}
.file-toolbar__upload {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px 12px;
}
.file-toolbar__label,
.file-toolbar__hint {
color: var(--el-text-color-secondary);
font-size: 13px;
line-height: 1.4;
}
.file-toolbar__tasks {
display: flex;
flex-direction: column;
gap: 8px;
}
.file-toolbar__task-meta {
display: flex;
justify-content: space-between;
gap: 12px;
margin-bottom: 4px;
font-size: 13px;
}
.file-toolbar__filters {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 12px;
}
.file-toolbar__filters :deep(.list-keyword-search-bar) {
margin-bottom: 0;
}
.file-toolbar__filters :deep(#art-table-header) {
flex: 1 1 220px;
min-width: 0;
}
.file-toolbar__bucket {
width: 140px;
}
</style>
+26 -24
View File
@@ -3,30 +3,32 @@
<div class="flex flex-col gap-3">
<ElCard>
<template #header>登录锁定</template>
<ElForm v-loading="loading" label-width="120px" class="max-w-lg">
<ElFormItem label="失败次数">
<ElInputNumber
v-model="form.loginMaxFailures"
:min="1"
:precision="0"
:step="1"
controls-position="right"
/>
</ElFormItem>
<ElFormItem label="锁定分钟数">
<ElInputNumber
v-model="form.loginLockMinutes"
:min="1"
:precision="0"
:step="1"
controls-position="right"
/>
</ElFormItem>
<ElFormItem>
<ElButton v-auth="'security:edit'" type="primary" :loading="saving" @click="save">
保存
</ElButton>
</ElFormItem>
<ElForm v-loading="loading" label-width="auto">
<div class="flex flex-wrap items-center gap-x-4 gap-y-2">
<ElFormItem label="失败次数" class="!mb-0">
<ElInputNumber
v-model="form.loginMaxFailures"
:min="1"
:precision="0"
:step="1"
controls-position="right"
/>
</ElFormItem>
<ElFormItem label="锁定分钟数" class="!mb-0">
<ElInputNumber
v-model="form.loginLockMinutes"
:min="1"
:precision="0"
:step="1"
controls-position="right"
/>
</ElFormItem>
<ElFormItem class="!mb-0">
<ElButton v-auth="'security:edit'" type="primary" :loading="saving" @click="save">
保存
</ElButton>
</ElFormItem>
</div>
</ElForm>
</ElCard>
+24 -2
View File
@@ -1,6 +1,7 @@
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import { existsSync, readdirSync } from 'fs'
import { fileURLToPath } from 'url'
import vueDevTools from 'vite-plugin-vue-devtools'
import viteCompression from 'vite-plugin-compression'
@@ -11,6 +12,28 @@ import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import tailwindcss from '@tailwindcss/vite'
// import { visualizer } from 'rollup-plugin-visualizer'
/**
* Element Plus 按需样式不会在启动扫描时全部可见;首次进入新页面会触发
* “发现新依赖 → 重预构建 → 旧 hash 请求 504 Outdated Optimize Dep”。
* 这里同步枚举全部 style 入口,避免路由切换中途重建。
*/
function elementPlusStyleEntries(): string[] {
const dir = path.resolve(__dirname, 'node_modules/element-plus/es/components')
if (!existsSync(dir)) return []
const entries: string[] = []
for (const name of readdirSync(dir)) {
const styleDir = path.resolve(dir, name, 'style')
if (existsSync(path.resolve(styleDir, 'css.mjs'))) {
entries.push(`element-plus/es/components/${name}/style/css`)
}
if (existsSync(path.resolve(styleDir, 'index.mjs'))) {
entries.push(`element-plus/es/components/${name}/style/index`)
}
}
return entries
}
export default ({ mode }: { mode: string }) => {
const root = process.cwd()
const env = loadEnv(mode, root)
@@ -119,8 +142,7 @@ export default ({ mode }: { mode: string }) => {
'file-saver',
'vue-img-cutter',
'element-plus/es',
'element-plus/es/components/*/style/css',
'element-plus/es/components/*/style/index'
...elementPlusStyleEntries()
]
},
css: {