12 Commits
Author SHA1 Message Date
Vicky dea414739e feat(tag): 标签管理支持配置语义码
web-ci / build (push) Canceled after 0s
客户端按语义码判断板块归属(如推荐页「上新」横滑),标签改名或换语言
都不再影响;新增/编辑弹窗增加语义码输入并做格式校验,留空则按名称兜底。
2026-10-29 15:44:42 +08:00
Zhou Weidong d13bdd5dc8 ci: 新增 Gitea Actions 流水线(测试/分析/构建门禁)
web-ci / build (push) Failing after 36s
2026-10-29 10:47:47 +08:00
Zhou Weidong 040ec3f382 fix(login): 退出登录不清除记住我的账号密码 2026-10-29 10:08:21 +08:00
Zhou Weidong 9a58b98f12 fix(login): 记住我同时记住密码并回填 2026-10-29 09:58:57 +08:00
Zhou Weidong e2618735ba chore(merge): 合并 main 的样式排序到 develop 2026-10-29 09:21:32 +08:00
Zhou Weidong b68361b037 chore(merge): 合并 phase1 UI prep 后台用户系统筛选与列表展示
合并后新增收藏/点赞列表页、用户单元格组件与系统筛选,
并统一观看时间格式与标签文案。
2026-10-27 20:20:31 +08:00
Zhou Weidong d846d2d864 Merge branch fix/icons-vue-deps 2026-10-26 13:06:45 +08:00
Zhou Weidong e6d1d6997b fix(deps): 补充 icons-vue 依赖声明 2026-10-26 13:06:38 +08:00
Zhou Weidong ada3b56d14 chore(merge): 将 release/v1.0.0 同步到 main 2026-10-23 13:08:30 +08:00
Zhou Weidong 7e8ae86d9f chore(merge): 对齐远程 main 2026-10-23 13:08:28 +08:00
Zhou Weidong 430ba59613 chore(merge): 将 develop 同步到 release/v1.0.0 2026-10-23 13:08:09 +08:00
Zhou Weidong e9a956f16c chore(web): 忽略运行时产物(日志/启动脚本) 2026-10-19 22:43:29 +08:00
9 changed files with 82 additions and 35 deletions

No files matched your search

+28
View File
@@ -0,0 +1,28 @@
# Author: fruit.com.cn
# ReelShortWeb 后台 CI:lint + 构建门禁
name: web-ci
on:
push:
branches: [develop, main]
pull_request:
branches: [develop, main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: pnpm
- name: 依赖
run: pnpm install --frozen-lockfile
- name: 类型检查
run: pnpm ts:check
- name: 构建
run: pnpm build:pro
+1
View File
@@ -24,6 +24,7 @@
"prepare": "husky install" "prepare": "husky install"
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "2.3.2",
"@iconify/iconify": "^3.1.1", "@iconify/iconify": "^3.1.1",
"@iconify/vue": "^4.1.1", "@iconify/vue": "^4.1.1",
"@vueuse/core": "^10.7.2", "@vueuse/core": "^10.7.2",
+1
View File
@@ -2,6 +2,7 @@
export type UserLoginType = { export type UserLoginType = {
username: string username: string
password?: string
} }
export type UserType = { export type UserType = {
+3 -13
View File
@@ -87,8 +87,8 @@ export const useUserStore = defineStore('user', {
this.setToken('') this.setToken('')
this.setUserInfo(undefined) this.setUserInfo(undefined)
this.setRoleRouters([]) this.setRoleRouters([])
this.setLoginInfo(undefined) // 注意:不能清 loginInfo / rememberMe —— 那是「记住我」存的账号密码,
this.setRememberMe(false) // 退出登录后回到登录页还要靠它来回填,清了就等于「记住我」没生效。
tagsViewStore.delAllViews() tagsViewStore.delAllViews()
// 菜单/路由一起清:否则换账号后 permissionStore 里还是上一个账号的路由表 // 菜单/路由一起清:否则换账号后 permissionStore 里还是上一个账号的路由表
permissionStore.clearRoutes() permissionStore.clearRoutes()
@@ -104,17 +104,7 @@ export const useUserStore = defineStore('user', {
this.loginInfo = loginInfo this.loginInfo = loginInfo
} }
}, },
persist: { persist: true
afterRestore: (context) => {
const loginInfo = context.store.loginInfo as
(UserLoginType & { password?: string }) | undefined
if (loginInfo?.password !== undefined) {
context.store.setLoginInfo(
loginInfo.username ? { username: loginInfo.username } : undefined
)
}
}
}
}) })
export const useUserStoreWithOut = () => { export const useUserStoreWithOut = () => {
+5 -3
View File
@@ -132,7 +132,7 @@ const remember = ref(userStore.getRememberMe)
const initLoginInfo = () => { const initLoginInfo = () => {
const loginInfo = userStore.getLoginInfo const loginInfo = userStore.getLoginInfo
if (loginInfo) { if (loginInfo) {
setValues({ username: loginInfo.username }) setValues({ username: loginInfo.username, password: loginInfo.password })
} }
} }
onMounted(() => { onMounted(() => {
@@ -169,10 +169,12 @@ const signIn = async () => {
const res = await loginApi(formData) const res = await loginApi(formData)
if (res) { if (res) {
// 是否记住我 // 是否记住我:勾选时把账号密码一起持久化到 localStorage,
// 下次打开登录页自动回填,免重复输入。
if (unref(remember)) { if (unref(remember)) {
userStore.setLoginInfo({ userStore.setLoginInfo({
username: formData.username username: formData.username,
password: formData.password
}) })
} else { } else {
userStore.setLoginInfo(undefined) userStore.setLoginInfo(undefined)
@@ -33,6 +33,7 @@ const DEFAULT_TAG_COLOR = '#E5484D'
const emptyForm = () => ({ const emptyForm = () => ({
locales: { zh: '' }, locales: { zh: '' },
bgColor: DEFAULT_TAG_COLOR, bgColor: DEFAULT_TAG_COLOR,
code: '',
sort: 1, sort: 1,
status: 1 status: 1
}) })
@@ -66,6 +67,7 @@ function openEdit(row) {
Object.assign(form, { Object.assign(form, {
locales, locales,
bgColor: row.bgColor || DEFAULT_TAG_COLOR, bgColor: row.bgColor || DEFAULT_TAG_COLOR,
code: row.code || '',
sort: row.sort ?? 0, sort: row.sort ?? 0,
status: row.status ?? 1 status: row.status ?? 1
}) })
@@ -83,12 +85,18 @@ async function save() {
ElMessage.warning(`请填写${languageLabel(missing)}名称`) ElMessage.warning(`请填写${languageLabel(missing)}名称`)
return return
} }
const code = (form.code || '').trim().toUpperCase()
if (code && !/^[A-Z][A-Z0-9_]{0,31}$/.test(code)) {
ElMessage.warning('语义码需为大写字母、数字或下划线,且以字母开头')
return
}
saving.value = true saving.value = true
try { try {
const payload = { const payload = {
name: locales.zh, name: locales.zh,
locales, locales,
bgColor: form.bgColor || DEFAULT_TAG_COLOR, bgColor: form.bgColor || DEFAULT_TAG_COLOR,
code,
sort: form.sort, sort: form.sort,
status: form.status status: form.status
} }
@@ -209,6 +217,16 @@ onMounted(load)
<el-form-item label="背景色"> <el-form-item label="背景色">
<el-color-picker v-model="form.bgColor" color-format="hex" /> <el-color-picker v-model="form.bgColor" color-format="hex" />
</el-form-item> </el-form-item>
<el-form-item label="语义码">
<el-input
v-model="form.code"
maxlength="32"
placeholder="如 NEW_ARRIVAL,留空则按名称匹配"
/>
<div class="form-hint">
客户端按语义码判断板块(如「上新」横滑),改名或换语言都不受影响。
</div>
</el-form-item>
<div class="form-row"> <div class="form-row">
<el-form-item label="排序"> <el-form-item label="排序">
<el-input-number v-model="form.sort" :min="0" :max="999" /> <el-input-number v-model="form.sort" :min="0" :max="999" />
@@ -236,4 +254,11 @@ onMounted(load)
height: 16px; height: 16px;
border-radius: 4px; border-radius: 4px;
} }
.form-hint {
margin-top: 4px;
font-size: 12px;
line-height: 1.5;
color: var(--el-text-color-secondary);
}
</style> </style>
@@ -326,17 +326,17 @@ function onEpisodesCreated(): void {
.batch-upload-hint { .batch-upload-hint {
margin: 0 0 12px; margin: 0 0 12px;
color: var(--el-text-color-secondary);
font-size: 13px; font-size: 13px;
color: var(--el-text-color-secondary);
} }
.batch-upload-rejected { .batch-upload-rejected {
margin-bottom: 12px;
padding: 8px 12px; padding: 8px 12px;
border-radius: 6px; margin-bottom: 12px;
background: color-mix(in srgb, var(--el-color-danger) 8%, transparent);
color: var(--el-color-danger);
font-size: 12px; font-size: 12px;
color: var(--el-color-danger);
background: color-mix(in srgb, var(--el-color-danger) 8%, transparent);
border-radius: 6px;
} }
.batch-upload-rejected p { .batch-upload-rejected p {
@@ -348,12 +348,12 @@ function onEpisodesCreated(): void {
} }
.batch-upload-summary { .batch-upload-summary {
margin: 0 0 12px;
padding: 8px 12px; padding: 8px 12px;
border-radius: 6px; margin: 0 0 12px;
background: color-mix(in srgb, var(--el-color-success) 10%, transparent);
color: var(--el-text-color-primary);
font-size: 13px; font-size: 13px;
color: var(--el-text-color-primary);
background: color-mix(in srgb, var(--el-color-success) 10%, transparent);
border-radius: 6px;
} }
.batch-upload-list { .batch-upload-list {
@@ -202,9 +202,9 @@ function hasFailed(): boolean {
<style scoped> <style scoped>
.create-episode-hint { .create-episode-hint {
margin: 0 0 12px; margin: 0 0 12px;
color: var(--el-text-color-secondary);
font-size: 13px; font-size: 13px;
line-height: 1.5; line-height: 1.5;
color: var(--el-text-color-secondary);
} }
.create-episode-head, .create-episode-head,
@@ -229,8 +229,8 @@ function hasFailed(): boolean {
.create-episode-head { .create-episode-head {
margin-bottom: 8px; margin-bottom: 8px;
color: var(--el-text-color-secondary);
font-size: 12px; font-size: 12px;
color: var(--el-text-color-secondary);
} }
.create-episode-list { .create-episode-list {
@@ -253,8 +253,8 @@ function hasFailed(): boolean {
} }
.create-episode-result { .create-episode-result {
color: var(--el-text-color-secondary);
font-size: 12px; font-size: 12px;
color: var(--el-text-color-secondary);
} }
.create-episode-row.is-saved .create-episode-result { .create-episode-row.is-saved .create-episode-result {
@@ -100,10 +100,10 @@ const canRetry = computed(() => props.item.status === 'failed')
} }
.upload-task-row__index { .upload-task-row__index {
flex: 0 0 auto;
min-width: 1.5em; min-width: 1.5em;
color: var(--el-text-color-secondary);
font-size: 13px; font-size: 13px;
color: var(--el-text-color-secondary);
flex: 0 0 auto;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
@@ -116,22 +116,22 @@ const canRetry = computed(() => props.item.status === 'failed')
.upload-task-row__error { .upload-task-row__error {
margin: 6px 0 0; margin: 6px 0 0;
color: var(--el-color-danger);
font-size: 12px; font-size: 12px;
color: var(--el-color-danger);
} }
.upload-task-row__actions { .upload-task-row__actions {
margin-top: 8px;
display: flex; display: flex;
margin-top: 8px;
margin-left: auto;
gap: 8px; gap: 8px;
justify-content: flex-end; justify-content: flex-end;
margin-left: auto;
} }
.upload-task-row.is-failed { .upload-task-row.is-failed {
padding-right: 8px;
padding-left: 8px;
background: color-mix(in srgb, var(--el-color-danger) 6%, transparent); background: color-mix(in srgb, var(--el-color-danger) 6%, transparent);
border-radius: 6px; border-radius: 6px;
padding-left: 8px;
padding-right: 8px;
} }
</style> </style>