Compare commits
5
Commits
910651ddba
...
27d58cfb5d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27d58cfb5d | ||
|
|
d0506b0d2e | ||
|
|
602664c122 | ||
|
|
b39173643d | ||
|
|
313d54a5ec |
@@ -1,7 +1,13 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
|
"image"
|
||||||
|
_ "image/gif"
|
||||||
|
_ "image/jpeg"
|
||||||
|
_ "image/png"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -9,6 +15,7 @@ import (
|
|||||||
|
|
||||||
"backend/internal/service"
|
"backend/internal/service"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
_ "golang.org/x/image/webp"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserGenerationHandler struct {
|
type UserGenerationHandler struct {
|
||||||
@@ -91,6 +98,45 @@ func (h *UserGenerationHandler) MyImages(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"data": items})
|
c.JSON(http.StatusOK, gin.H{"data": items})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UploadMyImage stores one user-supplied image (multipart field "file") under
|
||||||
|
// the caller's owner directory, so it can be placed on the infinite canvas and
|
||||||
|
// shows up in the 我的图片 picker alongside generated outputs. Returns the
|
||||||
|
// storage key ("owner/name") the frontend feeds to generatedUrl/thumbUrl.
|
||||||
|
func (h *UserGenerationHandler) UploadMyImage(c *gin.Context) {
|
||||||
|
user := currentUser(c)
|
||||||
|
if user == nil {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"detail": "未登录或会话已过期"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 20<<20)
|
||||||
|
f, _, err := c.Request.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"detail": "请选择图片文件"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
data, err := io.ReadAll(io.LimitReader(f, 15<<20+1))
|
||||||
|
if err != nil || len(data) == 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"detail": "读取文件失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(data) > 15<<20 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"detail": "图片不能超过 15MB"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, format, err := image.DecodeConfig(bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"detail": "不是有效的图片文件(支持 png/jpg/webp/gif)"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
key, err := h.admin.SaveOwnedImage(c.Request.Context(), service.OwnerDir(user), format, data)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"name": key})
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteMyFile removes ONE of the caller's own generated files (plus its
|
// DeleteMyFile removes ONE of the caller's own generated files (plus its
|
||||||
// thumbnail) and blanks the log rows referencing it, so the 画图台 grid and
|
// thumbnail) and blanks the log rows referencing it, so the 画图台 grid and
|
||||||
// 创作记录 gallery stop showing it. ?file= is the storage key (owner/name).
|
// 创作记录 gallery stop showing it. ?file= is the storage key (owner/name).
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ func New(cfg *config.Config, auth *service.AuthService, handlers Handlers) *gin.
|
|||||||
userAuthed.POST("/test", handlers.UserGen.Test)
|
userAuthed.POST("/test", handlers.UserGen.Test)
|
||||||
userAuthed.GET("/jobs/mine", handlers.UserGen.MyJobs)
|
userAuthed.GET("/jobs/mine", handlers.UserGen.MyJobs)
|
||||||
userAuthed.GET("/my-images", handlers.UserGen.MyImages)
|
userAuthed.GET("/my-images", handlers.UserGen.MyImages)
|
||||||
|
userAuthed.POST("/my-images", handlers.UserGen.UploadMyImage)
|
||||||
userAuthed.DELETE("/my-files", handlers.UserGen.DeleteMyFile)
|
userAuthed.DELETE("/my-files", handlers.UserGen.DeleteMyFile)
|
||||||
userAuthed.GET("/announcement", handlers.Announcement.Get)
|
userAuthed.GET("/announcement", handlers.Announcement.Get)
|
||||||
userAuthed.POST("/announcement/seen", handlers.Announcement.MarkSeen)
|
userAuthed.POST("/announcement/seen", handlers.Announcement.MarkSeen)
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
_ "image/gif"
|
||||||
|
_ "image/jpeg"
|
||||||
|
_ "image/png"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "golang.org/x/image/webp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SaveOwnedImage stores a user-uploaded image under their owner directory so it
|
||||||
|
// behaves like a generated output: it shows up in the 我的图片 picker, can be
|
||||||
|
// dropped onto the infinite canvas, and is served by /images/<owner>/<name>.
|
||||||
|
// format must be a decoder-registered image format ("png", "jpeg", ...).
|
||||||
|
func (s *AdminReadService) SaveOwnedImage(ctx context.Context, owner, format string, data []byte) (string, error) {
|
||||||
|
owner = strings.TrimSpace(owner)
|
||||||
|
if owner == "" || s.store == nil || !s.store.Configured() {
|
||||||
|
return "", errors.New("storage not configured")
|
||||||
|
}
|
||||||
|
ext := strings.ToLower(strings.TrimSpace(format))
|
||||||
|
contentType := ""
|
||||||
|
switch ext {
|
||||||
|
case "jpeg":
|
||||||
|
ext = "jpg"
|
||||||
|
contentType = "image/jpeg"
|
||||||
|
case "png", "gif", "webp":
|
||||||
|
contentType = "image/" + ext
|
||||||
|
default:
|
||||||
|
return "", errors.New("unsupported image format")
|
||||||
|
}
|
||||||
|
if _, _, err := image.DecodeConfig(bytes.NewReader(data)); err != nil {
|
||||||
|
return "", errors.New("invalid image data")
|
||||||
|
}
|
||||||
|
key := fmt.Sprintf("%s/up_%d%s.%s", owner, time.Now().Unix(), randomUpper(6), ext)
|
||||||
|
if err := s.store.Put(ctx, key, data, contentType); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// Best-effort thumbnail, same convention as generated outputs.
|
||||||
|
if thumb, terr := makeThumbnail(data); terr == nil {
|
||||||
|
_ = s.store.Put(ctx, ThumbKey(key), thumb, "image/jpeg")
|
||||||
|
}
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
@@ -13,22 +13,22 @@ const html = computed(() => marked.parse(announcement.content.trim() || '暂无
|
|||||||
<div v-if="announcement.show"
|
<div v-if="announcement.show"
|
||||||
class="fixed inset-0 z-[80] bg-slate-950/70 backdrop-blur-sm flex items-center justify-center p-4"
|
class="fixed inset-0 z-[80] bg-slate-950/70 backdrop-blur-sm flex items-center justify-center p-4"
|
||||||
@click.self="dismissAnnouncement">
|
@click.self="dismissAnnouncement">
|
||||||
<div class="w-full max-w-lg rounded-2xl bg-white text-slate-800 shadow-2xl overflow-hidden flex flex-col max-h-[80vh]">
|
<div class="w-full max-w-lg rounded-xl bg-white text-slate-800 border-[3px] border-[#111] shadow-[8px_8px_0_#111] overflow-hidden flex flex-col max-h-[80vh]">
|
||||||
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between gap-3 shrink-0">
|
<div class="px-6 py-4 border-b-[2.5px] border-b-[#111] flex items-center justify-between gap-3 shrink-0">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="w-7 h-7 rounded-lg bg-violet-500/15 text-violet-600 grid place-items-center">
|
<span class="w-7 h-7 rounded-lg bg-[#fef08a] text-[#111] border-2 border-[#111] grid place-items-center">
|
||||||
<Icon name="spark" class="w-4 h-4" />
|
<Icon name="spark" class="w-4 h-4" />
|
||||||
</span>
|
</span>
|
||||||
<h2 class="text-base font-semibold">公告</h2>
|
<h2 class="text-base font-extrabold">公告</h2>
|
||||||
</div>
|
</div>
|
||||||
<button @click="dismissAnnouncement" class="text-slate-400 hover:text-slate-700 transition-colors">
|
<button @click="dismissAnnouncement" class="text-slate-400 hover:text-slate-700 transition-colors">
|
||||||
<Icon name="close" class="w-5 h-5" />
|
<Icon name="close" class="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="ann-body px-6 py-5 overflow-y-auto" v-html="html"></div>
|
<div class="ann-body px-6 py-5 overflow-y-auto" v-html="html"></div>
|
||||||
<div class="px-6 py-4 border-t border-slate-100 flex justify-end shrink-0">
|
<div class="px-6 py-4 border-t-[2.5px] border-t-[#111] flex justify-end shrink-0">
|
||||||
<button @click="dismissAnnouncement"
|
<button @click="dismissAnnouncement"
|
||||||
class="rounded-lg bg-slate-900 text-white hover:bg-slate-700 px-5 py-2 text-sm font-medium transition-colors">
|
class="rounded-lg bg-[#ec5b13] text-white border-[2.5px] border-[#111] shadow-[3px_3px_0_#111] hover:-translate-x-0.5 hover:-translate-y-0.5 hover:shadow-[4px_4px_0_#111] active:translate-y-0.5 active:shadow-none px-5 py-2 text-sm font-extrabold transition-all">
|
||||||
我知道了
|
我知道了
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,12 +51,12 @@ const html = computed(() => marked.parse(announcement.content.trim() || '暂无
|
|||||||
.ann-body :deep(ul) { list-style: disc; }
|
.ann-body :deep(ul) { list-style: disc; }
|
||||||
.ann-body :deep(ol) { list-style: decimal; }
|
.ann-body :deep(ol) { list-style: decimal; }
|
||||||
.ann-body :deep(li) { margin: 0.2em 0; }
|
.ann-body :deep(li) { margin: 0.2em 0; }
|
||||||
.ann-body :deep(a) { color: rgb(124 58 237); text-decoration: underline; }
|
.ann-body :deep(a) { color: var(--m-orange); text-decoration: underline; }
|
||||||
.ann-body :deep(strong) { font-weight: 700; color: rgb(15 23 42); }
|
.ann-body :deep(strong) { font-weight: 700; color: rgb(15 23 42); }
|
||||||
.ann-body :deep(code) { background: rgb(241 245 249); padding: 0.1em 0.35em; border-radius: 0.3rem; font-size: 0.85em; }
|
.ann-body :deep(code) { background: rgb(254 240 138 / 0.5); padding: 0.1em 0.35em; border-radius: 0.3rem; font-size: 0.85em; }
|
||||||
.ann-body :deep(pre) { background: rgb(241 245 249); padding: 0.8em; border-radius: 0.5rem; overflow-x: auto; margin: 0.6em 0; }
|
.ann-body :deep(pre) { background: #111; color: #f5f5f5; padding: 0.8em; border-radius: 0.5rem; overflow-x: auto; margin: 0.6em 0; }
|
||||||
.ann-body :deep(pre code) { background: none; padding: 0; }
|
.ann-body :deep(pre code) { background: none; padding: 0; }
|
||||||
.ann-body :deep(blockquote) { border-left: 3px solid rgb(196 181 253); padding-left: 0.9em; color: rgb(100 116 139); margin: 0.6em 0; }
|
.ann-body :deep(blockquote) { border-left: 3px solid var(--m-orange); padding-left: 0.9em; color: rgb(100 116 139); margin: 0.6em 0; }
|
||||||
.ann-body :deep(hr) { border: none; border-top: 1px solid rgb(226 232 240); margin: 1em 0; }
|
.ann-body :deep(hr) { border: none; border-top: 1px solid rgb(226 232 240); margin: 1em 0; }
|
||||||
.ann-body :deep(img) { max-width: 100%; border-radius: 0.5rem; margin: 0.5em 0; }
|
.ann-body :deep(img) { max-width: 100%; border-radius: 0.5rem; margin: 0.5em 0; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ async function save() {
|
|||||||
<div class="flex flex-wrap gap-1.5">
|
<div class="flex flex-wrap gap-1.5">
|
||||||
<button v-for="r in RATIO_OPTS" :key="r" type="button" @click="toggleRatio(r)"
|
<button v-for="r in RATIO_OPTS" :key="r" type="button" @click="toggleRatio(r)"
|
||||||
class="px-2 py-1 rounded text-xs ring-1 transition-colors"
|
class="px-2 py-1 rounded text-xs ring-1 transition-colors"
|
||||||
:class="ratios.includes(r) ? 'bg-indigo-500/15 text-indigo-600 ring-indigo-300' : 'bg-slate-50 text-slate-500 ring-slate-200 hover:ring-slate-300'">{{ r }}</button>
|
:class="ratios.includes(r) ? 'bg-sky-500/15 text-sky-600 ring-sky-300' : 'bg-slate-50 text-slate-500 ring-slate-200 hover:ring-slate-300'">{{ r }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ async function doSmartImport() {
|
|||||||
<span v-if="detected.adobe" class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-rose-700 bg-rose-50 ring-1 ring-rose-200">
|
<span v-if="detected.adobe" class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-rose-700 bg-rose-50 ring-1 ring-rose-200">
|
||||||
Adobe · <span class="tabular-nums">{{ detected.adobe }}</span>
|
Adobe · <span class="tabular-nums">{{ detected.adobe }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="detected.runway" class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-violet-700 bg-violet-50 ring-1 ring-violet-200">
|
<span v-if="detected.runway" class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-orange-700 bg-orange-50 ring-1 ring-orange-200">
|
||||||
Runway · <span class="tabular-nums">{{ detected.runway }}</span>
|
Runway · <span class="tabular-nums">{{ detected.runway }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="detected.leonardo" class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-amber-700 bg-amber-50 ring-1 ring-amber-200">
|
<span v-if="detected.leonardo" class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-amber-700 bg-amber-50 ring-1 ring-amber-200">
|
||||||
@@ -146,7 +146,7 @@ async function doSmartImport() {
|
|||||||
<span v-if="detected.grok" class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-slate-700 bg-slate-100 ring-1 ring-slate-300">
|
<span v-if="detected.grok" class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-slate-700 bg-slate-100 ring-1 ring-slate-300">
|
||||||
Grok · <span class="tabular-nums">{{ detected.grok }}</span>
|
Grok · <span class="tabular-nums">{{ detected.grok }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="detected.cf" class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-fuchsia-700 bg-fuchsia-50 ring-1 ring-fuchsia-200">
|
<span v-if="detected.cf" class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-rose-700 bg-rose-50 ring-1 ring-rose-200">
|
||||||
Creative Fabrica · <span class="tabular-nums">{{ detected.cf }}</span>
|
Creative Fabrica · <span class="tabular-nums">{{ detected.cf }}</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -164,9 +164,9 @@ async function submit() {
|
|||||||
<div class="absolute inset-0 bg-black/70 backdrop-blur-md" @click="closeLogin"></div>
|
<div class="absolute inset-0 bg-black/70 backdrop-blur-md" @click="closeLogin"></div>
|
||||||
|
|
||||||
<div class="card relative z-10 w-full max-w-[26rem]">
|
<div class="card relative z-10 w-full max-w-[26rem]">
|
||||||
<!-- ambient brand glow -->
|
<!-- ambient memphis crayon washes -->
|
||||||
<div class="pointer-events-none absolute -top-24 -right-16 h-56 w-56 rounded-full bg-fuchsia-500/20 blur-3xl"></div>
|
<div class="pointer-events-none absolute -top-24 -right-16 h-56 w-56 rounded-full bg-[#fef08a]/60 blur-3xl"></div>
|
||||||
<div class="pointer-events-none absolute -bottom-24 -left-16 h-56 w-56 rounded-full bg-violet-500/20 blur-3xl"></div>
|
<div class="pointer-events-none absolute -bottom-24 -left-16 h-56 w-56 rounded-full bg-[#ffc1cc]/50 blur-3xl"></div>
|
||||||
|
|
||||||
<div class="relative p-7">
|
<div class="relative p-7">
|
||||||
<button class="absolute top-4 right-4 grid h-7 w-7 place-items-center rounded-lg text-[color:var(--fg-3)] hover:text-[color:var(--fg)] hover:bg-[var(--hover)] transition-colors"
|
<button class="absolute top-4 right-4 grid h-7 w-7 place-items-center rounded-lg text-[color:var(--fg-3)] hover:text-[color:var(--fg)] hover:bg-[var(--hover)] transition-colors"
|
||||||
@@ -195,7 +195,7 @@ async function submit() {
|
|||||||
<transition name="swap" mode="out-in">
|
<transition name="swap" mode="out-in">
|
||||||
<p v-if="mode === 'register' && auth.pendingInvite" class="invite-banner">
|
<p v-if="mode === 'register' && auth.pendingInvite" class="invite-banner">
|
||||||
<svg viewBox="0 0 24 24" class="h-4 w-4 shrink-0" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 12v10H4V12"/><path d="M2 7h20v5H2z"/><path d="M12 22V7"/><path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"/><path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"/></svg>
|
<svg viewBox="0 0 24 24" class="h-4 w-4 shrink-0" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 12v10H4V12"/><path d="M2 7h20v5H2z"/><path d="M12 22V7"/><path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"/><path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"/></svg>
|
||||||
<span>已应用邀请码 <span class="font-mono font-semibold text-emerald-200">{{ auth.pendingInvite }}</span>,完成首次生图后邀请人得 3 积分</span>
|
<span>已应用邀请码 <span class="font-mono font-bold text-emerald-800 dark:text-emerald-200">{{ auth.pendingInvite }}</span>,完成首次生图后邀请人得 3 积分</span>
|
||||||
</p>
|
</p>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
@@ -262,44 +262,44 @@ async function submit() {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.card {
|
.card {
|
||||||
border-radius: 1.25rem;
|
border-radius: 1.1rem;
|
||||||
background: linear-gradient(165deg, #ffffff 0%, #f6f6fb 100%);
|
background: var(--surface);
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
border: 1px solid var(--hairline);
|
border: 3px solid var(--ink);
|
||||||
box-shadow: 0 30px 70px -22px rgb(15 23 42 / 0.25);
|
box-shadow: 10px 10px 0 var(--shadow-ink);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
html.dark .card {
|
html.dark .card {
|
||||||
background: linear-gradient(165deg, #14161f 0%, #0d0f15 100%);
|
background: #1c1d24;
|
||||||
color: rgb(255 255 255 / 0.9);
|
color: rgb(255 255 255 / 0.9);
|
||||||
border: none;
|
|
||||||
box-shadow: 0 30px 70px -20px rgb(0 0 0 / 0.7);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-halo {
|
.logo-halo {
|
||||||
position: relative;
|
position: relative;
|
||||||
filter: drop-shadow(0 8px 18px rgb(168 85 247 / 0.45));
|
filter: drop-shadow(0 6px 0 rgb(236 91 19 / 0.35));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* segmented tab control */
|
/* segmented tab control */
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex; gap: 0.25rem; padding: 0.25rem;
|
display: flex; gap: 0.3rem; padding: 0.3rem;
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.7rem;
|
||||||
background: var(--surface-2);
|
background: var(--surface-2);
|
||||||
border: 1px solid var(--hairline);
|
border: 2px solid var(--ink);
|
||||||
}
|
}
|
||||||
.tab {
|
.tab {
|
||||||
flex: 1; padding: 0.45rem 0; border-radius: 0.55rem;
|
flex: 1; padding: 0.45rem 0; border-radius: 0.5rem;
|
||||||
font-size: 0.8125rem; color: var(--fg-3);
|
font-size: 0.8125rem; font-weight: 700; color: var(--fg-3);
|
||||||
transition: background 0.18s, color 0.18s, box-shadow 0.18s;
|
border: 2px solid transparent;
|
||||||
|
transition: background 0.18s, color 0.18s, box-shadow 0.18s, border-color 0.18s;
|
||||||
}
|
}
|
||||||
.tab:hover { color: var(--fg-2); }
|
.tab:hover { color: var(--fg); }
|
||||||
.tab-on {
|
.tab-on {
|
||||||
color: rgb(124 58 237);
|
color: #111;
|
||||||
background: linear-gradient(135deg, rgb(167 139 250 / 0.22), rgb(236 72 153 / 0.18));
|
background: var(--m-yellow);
|
||||||
box-shadow: 0 1px 0 rgb(255 255 255 / 0.08) inset, 0 4px 12px -4px rgb(168 85 247 / 0.5);
|
border-color: var(--ink);
|
||||||
|
box-shadow: 2px 2px 0 var(--shadow-ink);
|
||||||
}
|
}
|
||||||
html.dark .tab-on { color: white; }
|
html.dark .tab-on { color: #fff; background: var(--m-orange); }
|
||||||
|
|
||||||
/* icon-prefixed input */
|
/* icon-prefixed input */
|
||||||
.lm-field { position: relative; }
|
.lm-field { position: relative; }
|
||||||
@@ -308,21 +308,18 @@ html.dark .tab-on { color: white; }
|
|||||||
width: 1.05rem; height: 1.05rem; color: var(--fg-3);
|
width: 1.05rem; height: 1.05rem; color: var(--fg-3);
|
||||||
pointer-events: none; transition: color 0.18s;
|
pointer-events: none; transition: color 0.18s;
|
||||||
}
|
}
|
||||||
.lm-field:focus-within .ic { color: rgb(124 58 237 / 0.95); }
|
.lm-field:focus-within .ic { color: var(--m-orange); }
|
||||||
html.dark .lm-field:focus-within .ic { color: rgb(196 181 253 / 0.95); }
|
|
||||||
.fld {
|
.fld {
|
||||||
width: 100%; padding: 0.7rem 0.85rem 0.7rem 2.4rem; border-radius: 0.7rem;
|
width: 100%; padding: 0.7rem 0.85rem 0.7rem 2.4rem; border-radius: 0.6rem;
|
||||||
background: rgb(15 23 42 / 0.03); border: 1px solid var(--hairline);
|
background: var(--surface); border: 2px solid var(--ink-soft);
|
||||||
color: var(--fg); font-size: 0.875rem; outline: none;
|
color: var(--fg); font-size: 0.875rem; outline: none;
|
||||||
transition: border-color 0.18s, box-shadow 0.18s, background 0.18s;
|
transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
|
||||||
}
|
}
|
||||||
html.dark .fld { background: rgb(255 255 255 / 0.04); }
|
html.dark .fld { background: rgb(255 255 255 / 0.05); }
|
||||||
.fld:focus {
|
.fld:focus {
|
||||||
border-color: rgb(167 139 250 / 0.65);
|
border-color: var(--ink);
|
||||||
background: rgb(124 58 237 / 0.04);
|
box-shadow: 3px 3px 0 var(--m-orange);
|
||||||
box-shadow: 0 0 0 3px rgb(167 139 250 / 0.18);
|
|
||||||
}
|
}
|
||||||
html.dark .fld:focus { background: rgb(255 255 255 / 0.06); }
|
|
||||||
.fld::placeholder { color: var(--fg-faint); }
|
.fld::placeholder { color: var(--fg-faint); }
|
||||||
/* Keep autofill on-theme in dark (otherwise it paints white/yellow). */
|
/* Keep autofill on-theme in dark (otherwise it paints white/yellow). */
|
||||||
html.dark .fld:-webkit-autofill,
|
html.dark .fld:-webkit-autofill,
|
||||||
@@ -343,16 +340,18 @@ html.dark .fld:-webkit-autofill:focus {
|
|||||||
.eye:hover { color: var(--fg); background: var(--hover); }
|
.eye:hover { color: var(--fg); background: var(--hover); }
|
||||||
|
|
||||||
.code-btn {
|
.code-btn {
|
||||||
flex-shrink: 0; height: 2.7rem; padding: 0 0.85rem; border-radius: 0.7rem;
|
flex-shrink: 0; height: 2.7rem; padding: 0 0.85rem; border-radius: 0.6rem;
|
||||||
min-width: 5.5rem; /* keep width stable between text / spinner */
|
min-width: 5.5rem; /* keep width stable between text / spinner */
|
||||||
display: inline-flex; align-items: center; justify-content: center;
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
font-size: 0.75rem; white-space: nowrap;
|
font-size: 0.75rem; font-weight: 800; white-space: nowrap;
|
||||||
color: rgb(124 58 237); /* violet-600 — readable on the light card */
|
color: #111;
|
||||||
background: rgb(167 139 250 / 0.12); border: 1px solid rgb(167 139 250 / 0.4);
|
background: var(--m-yellow); border: 2px solid var(--ink);
|
||||||
transition: background 0.15s, opacity 0.15s;
|
box-shadow: 2px 2px 0 var(--shadow-ink);
|
||||||
|
transition: background 0.15s, box-shadow 0.15s, transform 0.15s, opacity 0.15s;
|
||||||
}
|
}
|
||||||
html.dark .code-btn { color: rgb(196 181 253 / 0.95); background: rgb(167 139 250 / 0.1); border-color: rgb(167 139 250 / 0.3); }
|
html.dark .code-btn { color: #111; }
|
||||||
.code-btn:hover:not(:disabled) { background: rgb(167 139 250 / 0.2); }
|
.code-btn:hover:not(:disabled) { transform: translate(-1px, -1px); box-shadow: 3px 3px 0 var(--shadow-ink); }
|
||||||
|
.code-btn:active:not(:disabled) { transform: scale(0.96) translateY(1px); box-shadow: none; }
|
||||||
/* Disabled (sending / countdown): just dim — keep the theme text color so it
|
/* Disabled (sending / countdown): just dim — keep the theme text color so it
|
||||||
never washes out to invisible white on the light card. */
|
never washes out to invisible white on the light card. */
|
||||||
.code-btn:disabled { opacity: 0.55; cursor: not-allowed; }
|
.code-btn:disabled { opacity: 0.55; cursor: not-allowed; }
|
||||||
@@ -361,14 +360,15 @@ html.dark .code-btn { color: rgb(196 181 253 / 0.95); background: rgb(167 139 25
|
|||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
display: flex; align-items: center; justify-content: center; gap: 0.5rem;
|
display: flex; align-items: center; justify-content: center; gap: 0.5rem;
|
||||||
padding: 0.72rem 0; border-radius: 0.7rem; font-size: 0.9rem; font-weight: 600;
|
padding: 0.72rem 0; border-radius: 0.65rem; font-size: 0.9rem; font-weight: 800;
|
||||||
letter-spacing: 0.02em; color: white; margin-top: 0.35rem;
|
letter-spacing: 0.02em; color: white; margin-top: 0.35rem;
|
||||||
background: linear-gradient(135deg, #a855f7 0%, #7c3aed 50%, #ec4899 100%);
|
background: var(--m-orange);
|
||||||
box-shadow: 0 10px 24px -8px rgb(168 85 247 / 0.6);
|
border: 2.5px solid var(--ink);
|
||||||
transition: transform 0.12s, box-shadow 0.18s, filter 0.18s;
|
box-shadow: 4px 4px 0 var(--shadow-ink);
|
||||||
|
transition: transform 0.13s, box-shadow 0.13s, filter 0.15s;
|
||||||
}
|
}
|
||||||
.btn-primary:hover:not(:disabled) { filter: brightness(1.08); box-shadow: 0 12px 28px -8px rgb(168 85 247 / 0.75); }
|
.btn-primary:hover:not(:disabled) { filter: brightness(1.06); transform: translate(-1px, -1px); box-shadow: 5px 5px 0 var(--shadow-ink); }
|
||||||
.btn-primary:active:not(:disabled) { transform: translateY(1px); }
|
.btn-primary:active:not(:disabled) { transform: scale(0.98) translateY(2px); box-shadow: 0 0 0 var(--shadow-ink); }
|
||||||
.btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }
|
.btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||||
|
|
||||||
.spinner {
|
.spinner {
|
||||||
@@ -380,18 +380,22 @@ html.dark .code-btn { color: rgb(196 181 253 / 0.95); background: rgb(167 139 25
|
|||||||
|
|
||||||
.invite-banner {
|
.invite-banner {
|
||||||
display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.85rem;
|
display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.85rem;
|
||||||
padding: 0.6rem 0.75rem; border-radius: 0.7rem; font-size: 0.75rem; line-height: 1.4;
|
padding: 0.6rem 0.75rem; border-radius: 0.6rem; font-size: 0.75rem; line-height: 1.4;
|
||||||
color: rgb(167 243 208);
|
color: #065f46; font-weight: 600;
|
||||||
background: rgb(16 185 129 / 0.1); border: 1px solid rgb(16 185 129 / 0.25);
|
background: var(--m-mint); border: 2px solid var(--ink);
|
||||||
}
|
}
|
||||||
.invite-banner svg { color: rgb(110 231 183); }
|
html.dark .invite-banner { color: rgb(167 243 208); background: rgb(16 185 129 / 0.15); }
|
||||||
|
.invite-banner svg { color: rgb(5 150 105); }
|
||||||
|
html.dark .invite-banner svg { color: rgb(110 231 183); }
|
||||||
|
|
||||||
.msg {
|
.msg {
|
||||||
display: flex; align-items: center; gap: 0.4rem; margin-top: 0.85rem;
|
display: flex; align-items: center; gap: 0.4rem; margin-top: 0.85rem;
|
||||||
font-size: 0.78rem; line-height: 1.4;
|
font-size: 0.78rem; font-weight: 600; line-height: 1.4;
|
||||||
}
|
}
|
||||||
.msg-err { color: rgb(253 164 175); }
|
.msg-err { color: rgb(220 38 38); }
|
||||||
.msg-ok { color: rgb(110 231 183); }
|
html.dark .msg-err { color: rgb(253 164 175); }
|
||||||
|
.msg-ok { color: rgb(5 150 105); }
|
||||||
|
html.dark .msg-ok { color: rgb(110 231 183); }
|
||||||
|
|
||||||
/* modal in/out */
|
/* modal in/out */
|
||||||
.modal-enter-active { transition: opacity 0.2s ease; }
|
.modal-enter-active { transition: opacity 0.2s ease; }
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
// Vivid mark: an open spectrum ring (lens/aperture) around a folded "V", with a
|
// Vivid mark: an open spectrum ring (lens/aperture) around a play triangle,
|
||||||
// shutter dot closing the gap — image + video in one glyph. Transparent
|
// with a shutter dot closing the gap — image + video in one glyph.
|
||||||
// background so it reads on both the dark and light shells.
|
// Transparent background so it reads on both the dark and light shells.
|
||||||
|
// Colors follow the memphis crayon palette (orange / pink / blue).
|
||||||
// Each instance gets unique gradient ids (useId) so multiple logos on one
|
// Each instance gets unique gradient ids (useId) so multiple logos on one
|
||||||
// page don't share/clobber defs.
|
// page don't share/clobber defs.
|
||||||
import { useId } from 'vue'
|
import { useId } from 'vue'
|
||||||
@@ -10,7 +11,6 @@ defineProps({ size: { type: [Number, String], default: 40 } })
|
|||||||
|
|
||||||
const uid = useId()
|
const uid = useId()
|
||||||
const gRing = `vring-${uid}`
|
const gRing = `vring-${uid}`
|
||||||
const gV = `vv-${uid}`
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -18,24 +18,21 @@ const gV = `vv-${uid}`
|
|||||||
xmlns="http://www.w3.org/2000/svg" class="shrink-0">
|
xmlns="http://www.w3.org/2000/svg" class="shrink-0">
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient :id="gRing" x1="6" y1="6" x2="42" y2="42" gradientUnits="userSpaceOnUse">
|
<linearGradient :id="gRing" x1="6" y1="6" x2="42" y2="42" gradientUnits="userSpaceOnUse">
|
||||||
<stop stop-color="#e879f9" />
|
<stop stop-color="#ec5b13" />
|
||||||
<stop offset="0.5" stop-color="#8b5cf6" />
|
<stop offset="0.5" stop-color="#f472b6" />
|
||||||
<stop offset="1" stop-color="#38bdf8" />
|
<stop offset="1" stop-color="#38bdf8" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient :id="gV" x1="14" y1="14" x2="34" y2="34" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#f0abfc" />
|
|
||||||
<stop offset="1" stop-color="#8b5cf6" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
<g transform="translate(24 24)">
|
<g transform="translate(24 24)">
|
||||||
<!-- open ring: dash gap leaves room for the shutter dot -->
|
<!-- open ring: dash gap leaves room for the shutter dot -->
|
||||||
<circle r="18.5" fill="none" :stroke="`url(#${gRing})`" stroke-width="3.2"
|
<circle r="17.5" fill="none" :stroke="`url(#${gRing})`" stroke-width="4.4"
|
||||||
stroke-linecap="round" stroke-dasharray="86 30" transform="rotate(-58)" />
|
stroke-linecap="round" stroke-dasharray="84 32" transform="rotate(-58)" />
|
||||||
<!-- folded V -->
|
<!-- play triangle = image + video -->
|
||||||
<path d="M-8 -7.5 L0 9 L8 -7.5" fill="none" :stroke="`url(#${gV})`" stroke-width="4.8"
|
<path d="M-3 -6.5 L8.5 0 L-3 6.5 Z" fill="#ec5b13" stroke="#111" stroke-width="1.4"
|
||||||
stroke-linecap="round" stroke-linejoin="round" />
|
stroke-linejoin="round" />
|
||||||
<circle cx="13" cy="-13" r="3.4" fill="#38bdf8" />
|
<!-- shutter dot -->
|
||||||
|
<circle cx="13" cy="-13" r="4.2" fill="#fef08a" stroke="#111" stroke-width="1.6" />
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
// Vivi — the platform's original chibi mascot. Pure inline SVG (no raster
|
|
||||||
// asset, no external dependency), so it scales crisply and inherits the
|
|
||||||
// pink/violet/sky brand palette. `size` sets the square px box; the halo of
|
|
||||||
// twin-tails + a camera-lens hair ring ties her to the B-ring logo.
|
|
||||||
import { computed } from 'vue'
|
|
||||||
const props = defineProps({
|
|
||||||
size: { type: [Number, String], default: 160 },
|
|
||||||
float: { type: Boolean, default: true },
|
|
||||||
uid: { type: String, default: () => 'm' + Math.random().toString(36).slice(2, 8) },
|
|
||||||
})
|
|
||||||
const px = computed(() => (typeof props.size === 'number' ? props.size + 'px' : props.size))
|
|
||||||
const g = (k) => `${props.uid}-${k}`
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<svg :width="px" :height="px" viewBox="0 0 200 200" fill="none"
|
|
||||||
:class="['mascot', { 'mascot-float': float }]" aria-hidden="true">
|
|
||||||
<defs>
|
|
||||||
<linearGradient :id="g('hair')" x1="0" y1="0" x2="1" y2="1">
|
|
||||||
<stop offset="0" stop-color="#f0abfc" />
|
|
||||||
<stop offset="0.55" stop-color="#c084fc" />
|
|
||||||
<stop offset="1" stop-color="#818cf8" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient :id="g('eye')" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0" stop-color="#7dd3fc" />
|
|
||||||
<stop offset="1" stop-color="#a855f7" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient :id="g('ring')" x1="0" y1="0" x2="1" y2="1">
|
|
||||||
<stop offset="0" stop-color="#f472b6" />
|
|
||||||
<stop offset="0.5" stop-color="#a78bfa" />
|
|
||||||
<stop offset="1" stop-color="#38bdf8" />
|
|
||||||
</linearGradient>
|
|
||||||
<radialGradient :id="g('glow')" cx="0.5" cy="0.5" r="0.5">
|
|
||||||
<stop offset="0" stop-color="#e879f9" stop-opacity="0.55" />
|
|
||||||
<stop offset="1" stop-color="#e879f9" stop-opacity="0" />
|
|
||||||
</radialGradient>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- soft aura -->
|
|
||||||
<circle cx="100" cy="104" r="86" :fill="`url(#${g('glow')})`" />
|
|
||||||
|
|
||||||
<!-- back twin-tail -->
|
|
||||||
<path d="M56 78 C22 84 20 140 44 168 C58 150 60 120 74 104 Z" :fill="`url(#${g('hair')})`" opacity="0.92" />
|
|
||||||
<path d="M144 78 C178 84 180 140 156 168 C142 150 140 120 126 104 Z" :fill="`url(#${g('hair')})`" opacity="0.92" />
|
|
||||||
|
|
||||||
<!-- hair back mass -->
|
|
||||||
<path d="M100 34 C60 34 44 66 46 100 C47 116 56 128 62 132 L138 132 C144 128 153 116 154 100 C156 66 140 34 100 34 Z"
|
|
||||||
:fill="`url(#${g('hair')})`" />
|
|
||||||
|
|
||||||
<!-- face -->
|
|
||||||
<path d="M64 92 C64 66 80 52 100 52 C120 52 136 66 136 92 C136 118 120 134 100 134 C80 134 64 118 64 92 Z"
|
|
||||||
fill="#fff5fb" />
|
|
||||||
<!-- fringe -->
|
|
||||||
<path d="M62 92 C60 66 78 50 100 50 C122 50 140 66 138 92 C132 78 120 74 118 90 C112 74 100 72 100 72 C100 72 88 74 82 90 C80 74 68 78 62 92 Z"
|
|
||||||
:fill="`url(#${g('hair')})`" />
|
|
||||||
|
|
||||||
<!-- ears blush -->
|
|
||||||
<circle cx="76" cy="106" r="7" fill="#fbb6ce" opacity="0.75" />
|
|
||||||
<circle cx="124" cy="106" r="7" fill="#fbb6ce" opacity="0.75" />
|
|
||||||
|
|
||||||
<!-- eyes -->
|
|
||||||
<g>
|
|
||||||
<ellipse cx="83" cy="98" rx="9" ry="12" :fill="`url(#${g('eye')})`" />
|
|
||||||
<ellipse cx="117" cy="98" rx="9" ry="12" :fill="`url(#${g('eye')})`" />
|
|
||||||
<circle cx="85.5" cy="93.5" r="3.2" fill="#fff" />
|
|
||||||
<circle cx="119.5" cy="93.5" r="3.2" fill="#fff" />
|
|
||||||
<circle cx="81" cy="101" r="1.6" fill="#fff" opacity="0.85" />
|
|
||||||
<circle cx="115" cy="101" r="1.6" fill="#fff" opacity="0.85" />
|
|
||||||
</g>
|
|
||||||
<!-- brows / smile -->
|
|
||||||
<path d="M74 84 q9 -5 18 -1" stroke="#c084fc" stroke-width="2.4" stroke-linecap="round" />
|
|
||||||
<path d="M108 83 q9 -4 18 1" stroke="#c084fc" stroke-width="2.4" stroke-linecap="round" />
|
|
||||||
<path d="M92 114 q8 7 16 0" stroke="#f472b6" stroke-width="2.6" stroke-linecap="round" fill="none" />
|
|
||||||
|
|
||||||
<!-- camera-lens hair ring (echoes the logo) -->
|
|
||||||
<g transform="translate(140 58)">
|
|
||||||
<circle r="15" fill="none" :stroke="`url(#${g('ring')})`" stroke-width="4"
|
|
||||||
stroke-linecap="round" stroke-dasharray="70 24" transform="rotate(-55)" />
|
|
||||||
<circle r="5.5" fill="#38bdf8" />
|
|
||||||
<circle r="2.2" fill="#fff" opacity="0.9" />
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<!-- sparkles -->
|
|
||||||
<g fill="#fef08a">
|
|
||||||
<path d="M46 60 l2.2 5 5 2.2 -5 2.2 -2.2 5 -2.2 -5 -5 -2.2 5 -2.2 Z" />
|
|
||||||
<path d="M158 128 l1.6 3.6 3.6 1.6 -3.6 1.6 -1.6 3.6 -1.6 -3.6 -3.6 -1.6 3.6 -1.6 Z" />
|
|
||||||
</g>
|
|
||||||
<circle cx="52" cy="150" r="2.4" fill="#7dd3fc" />
|
|
||||||
<circle cx="150" cy="44" r="2" fill="#f0abfc" />
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.mascot { display: block; }
|
|
||||||
.mascot-float { animation: mascotFloat 5s ease-in-out infinite; transform-origin: center; }
|
|
||||||
@keyframes mascotFloat {
|
|
||||||
0%, 100% { transform: translateY(0) rotate(-1deg); }
|
|
||||||
50% { transform: translateY(-8px) rotate(1.5deg); }
|
|
||||||
}
|
|
||||||
@media (prefers-reduced-motion: reduce) { .mascot-float { animation: none; } }
|
|
||||||
</style>
|
|
||||||
@@ -219,8 +219,8 @@ async function save() {
|
|||||||
<div v-if="entry" class="rounded-xl bg-white/[0.03] ring-1 ring-white/[0.06] p-3.5 space-y-2.5">
|
<div v-if="entry" class="rounded-xl bg-white/[0.03] ring-1 ring-white/[0.06] p-3.5 space-y-2.5">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-[11px] font-medium ring-1"
|
<span class="inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-[11px] font-medium ring-1"
|
||||||
:class="isVideo ? 'bg-fuchsia-500/10 text-fuchsia-300 ring-fuchsia-400/30'
|
:class="isVideo ? 'bg-rose-500/10 text-rose-300 ring-rose-400/30'
|
||||||
: 'bg-indigo-500/10 text-indigo-300 ring-indigo-400/30'">
|
: 'bg-sky-500/10 text-sky-300 ring-sky-400/30'">
|
||||||
{{ isVideo ? '生视频' : '生图' }}
|
{{ isVideo ? '生视频' : '生图' }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-[11px] text-white/45 capitalize">{{ entry.provider }}</span>
|
<span class="text-[11px] text-white/45 capitalize">{{ entry.provider }}</span>
|
||||||
|
|||||||
@@ -103,15 +103,15 @@ onUnmounted(clearTimers)
|
|||||||
<transition name="pay-fade">
|
<transition name="pay-fade">
|
||||||
<div v-if="payment.show" class="fixed inset-0 z-[85] bg-slate-950/70 backdrop-blur-sm flex items-center justify-center p-4"
|
<div v-if="payment.show" class="fixed inset-0 z-[85] bg-slate-950/70 backdrop-blur-sm flex items-center justify-center p-4"
|
||||||
@click.self="!paid && finish()">
|
@click.self="!paid && finish()">
|
||||||
<div class="w-full max-w-sm rounded-2xl bg-white text-slate-800 shadow-2xl overflow-hidden">
|
<div class="w-full max-w-sm rounded-xl bg-white text-slate-800 border-[3px] border-[#111] shadow-[8px_8px_0_#111] overflow-hidden">
|
||||||
<div class="px-5 py-4 border-b border-slate-100 flex items-center justify-between">
|
<div class="px-5 py-4 border-b-[2.5px] border-b-[#111] flex items-center justify-between">
|
||||||
<h2 class="text-base font-semibold">{{ paid ? '支付成功' : dead ? '支付超时' : (isJump ? '支付监控中' : '扫码支付') }}</h2>
|
<h2 class="text-base font-semibold">{{ paid ? '支付成功' : dead ? '支付超时' : (isJump ? '支付监控中' : '扫码支付') }}</h2>
|
||||||
<button v-if="!paid" @click="finish" class="text-slate-400 hover:text-slate-700"><Icon name="close" class="w-5 h-5" /></button>
|
<button v-if="!paid" @click="finish" class="text-slate-400 hover:text-slate-700"><Icon name="close" class="w-5 h-5" /></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- paid -->
|
<!-- paid -->
|
||||||
<div v-if="paid" class="px-6 py-10 text-center">
|
<div v-if="paid" class="px-6 py-10 text-center">
|
||||||
<div class="w-16 h-16 mx-auto rounded-full bg-emerald-100 text-emerald-600 grid place-items-center mb-4">
|
<div class="w-16 h-16 mx-auto rounded-full bg-[#a7f3d0] text-emerald-700 border-[2.5px] border-[#111] shadow-[4px_4px_0_#111] grid place-items-center mb-4">
|
||||||
<Icon name="spark" class="w-8 h-8" />
|
<Icon name="spark" class="w-8 h-8" />
|
||||||
</div>
|
</div>
|
||||||
<p class="text-lg font-semibold text-slate-800">充值成功</p>
|
<p class="text-lg font-semibold text-slate-800">充值成功</p>
|
||||||
@@ -127,7 +127,7 @@ onUnmounted(clearTimers)
|
|||||||
<p class="text-lg font-semibold text-slate-700">支付超时</p>
|
<p class="text-lg font-semibold text-slate-700">支付超时</p>
|
||||||
<p class="text-sm text-slate-500 mt-1">二维码已失效,订单已取消</p>
|
<p class="text-sm text-slate-500 mt-1">二维码已失效,订单已取消</p>
|
||||||
<p class="text-xs text-slate-400 mt-1">如需充值请重新下单</p>
|
<p class="text-xs text-slate-400 mt-1">如需充值请重新下单</p>
|
||||||
<button @click="finish" class="mt-5 rounded-lg bg-slate-900 text-white hover:bg-slate-700 px-5 py-2 text-sm font-medium transition-colors">关闭</button>
|
<button @click="finish" class="mt-5 rounded-lg bg-[#ec5b13] text-white border-[2.5px] border-[#111] shadow-[3px_3px_0_#111] hover:-translate-y-0.5 active:translate-y-0.5 active:shadow-none px-5 py-2 text-sm font-extrabold transition-all">关闭</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- pending -->
|
<!-- pending -->
|
||||||
@@ -135,7 +135,7 @@ onUnmounted(clearTimers)
|
|||||||
<!-- qrcode: scan to pay -->
|
<!-- qrcode: scan to pay -->
|
||||||
<template v-if="!isJump">
|
<template v-if="!isJump">
|
||||||
<div class="flex justify-center mb-3">
|
<div class="flex justify-center mb-3">
|
||||||
<div class="w-[228px] h-[228px] rounded-2xl ring-1 ring-slate-200 shadow-sm grid place-items-center overflow-hidden bg-white p-3.5">
|
<div class="w-[228px] h-[228px] rounded-xl border-[2.5px] border-[#111] shadow-[4px_4px_0_#111] grid place-items-center overflow-hidden bg-white p-3.5">
|
||||||
<img v-if="qrUrl" :src="qrUrl" alt="支付二维码" class="w-full h-full rounded-lg" />
|
<img v-if="qrUrl" :src="qrUrl" alt="支付二维码" class="w-full h-full rounded-lg" />
|
||||||
<span v-else class="text-xs text-slate-400">二维码生成中…</span>
|
<span v-else class="text-xs text-slate-400">二维码生成中…</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -145,7 +145,7 @@ onUnmounted(clearTimers)
|
|||||||
<!-- jump (no qrcode): the cashier opened in a new tab — just monitor -->
|
<!-- jump (no qrcode): the cashier opened in a new tab — just monitor -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="flex flex-col items-center justify-center py-7 mb-2">
|
<div class="flex flex-col items-center justify-center py-7 mb-2">
|
||||||
<div class="w-12 h-12 rounded-full border-2 border-violet-200 border-t-violet-500 animate-spin mb-3"></div>
|
<div class="w-12 h-12 rounded-full border-[3px] border-[#fecaca] border-t-[#ec5b13] animate-spin mb-3"></div>
|
||||||
<p class="text-sm font-medium text-slate-700">支付监控中…</p>
|
<p class="text-sm font-medium text-slate-700">支付监控中…</p>
|
||||||
<p class="text-xs text-slate-400 mt-1">已打开支付页面,完成后自动到账</p>
|
<p class="text-xs text-slate-400 mt-1">已打开支付页面,完成后自动到账</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -155,7 +155,7 @@ onUnmounted(clearTimers)
|
|||||||
<div class="flex justify-between"><dt class="text-slate-400">订单号</dt><dd class="font-mono text-xs text-slate-700">{{ order.id }}</dd></div>
|
<div class="flex justify-between"><dt class="text-slate-400">订单号</dt><dd class="font-mono text-xs text-slate-700">{{ order.id }}</dd></div>
|
||||||
<div class="flex justify-between"><dt class="text-slate-400">状态</dt><dd class="text-amber-600 font-medium">{{ statusLabel }}</dd></div>
|
<div class="flex justify-between"><dt class="text-slate-400">状态</dt><dd class="text-amber-600 font-medium">{{ statusLabel }}</dd></div>
|
||||||
<div class="flex justify-between"><dt class="text-slate-400">金额</dt><dd class="font-semibold text-slate-800">¥{{ order.amount }}</dd></div>
|
<div class="flex justify-between"><dt class="text-slate-400">金额</dt><dd class="font-semibold text-slate-800">¥{{ order.amount }}</dd></div>
|
||||||
<div class="flex justify-between"><dt class="text-slate-400">充值积分</dt><dd class="text-violet-600 font-semibold">{{ order.points }}</dd></div>
|
<div class="flex justify-between"><dt class="text-slate-400">充值积分</dt><dd class="text-[#ec5b13] font-semibold">{{ order.points }}</dd></div>
|
||||||
<div class="flex justify-between"><dt class="text-slate-400">下单时间</dt><dd class="text-xs text-slate-600">{{ fmtTime(order.created_at) }}</dd></div>
|
<div class="flex justify-between"><dt class="text-slate-400">下单时间</dt><dd class="text-xs text-slate-600">{{ fmtTime(order.created_at) }}</dd></div>
|
||||||
<div class="flex justify-between"><dt class="text-slate-400">支付倒计时</dt><dd class="tabular-nums font-medium" :class="remaining > 0 ? 'text-slate-700' : 'text-rose-500'">{{ remaining > 0 ? mmss : '已超时' }}</dd></div>
|
<div class="flex justify-between"><dt class="text-slate-400">支付倒计时</dt><dd class="tabular-nums font-medium" :class="remaining > 0 ? 'text-slate-700' : 'text-rose-500'">{{ remaining > 0 ? mmss : '已超时' }}</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
|
|||||||
@@ -90,17 +90,17 @@ onUnmounted(() => document.removeEventListener('mousedown', onDocClick))
|
|||||||
leave-from-class="opacity-100 translate-y-0"
|
leave-from-class="opacity-100 translate-y-0"
|
||||||
leave-to-class="opacity-0 -translate-y-1">
|
leave-to-class="opacity-0 -translate-y-1">
|
||||||
<div v-if="open" ref="panel"
|
<div v-if="open" ref="panel"
|
||||||
class="absolute z-30 mt-2 w-full max-h-64 overflow-auto rounded-xl border border-[color:var(--hairline)]
|
class="absolute z-30 mt-2 w-full max-h-64 overflow-auto rounded-xl border-[2.5px] border-[color:var(--ink)]
|
||||||
bg-[var(--menu-bg)] backdrop-blur-xl p-1.5 shadow-2xl shadow-black/20 ring-1 ring-[color:var(--hairline)]">
|
bg-[var(--menu-bg)] p-1.5 shadow-[5px_5px_0_var(--shadow-ink)]">
|
||||||
<button v-for="(o, i) in options" :key="o.value" type="button"
|
<button v-for="(o, i) in options" :key="o.value" type="button"
|
||||||
:data-idx="i" @click="pick(o)" @mouseenter="active = i"
|
:data-idx="i" @click="pick(o)" @mouseenter="active = i"
|
||||||
class="w-full flex items-center justify-between gap-2 rounded-lg px-3 py-2 text-left text-sm transition-colors"
|
class="w-full flex items-center justify-between gap-2 rounded-lg px-3 py-2 text-left text-sm font-medium transition-colors"
|
||||||
:class="[
|
:class="[
|
||||||
mono ? 'font-mono' : '',
|
mono ? 'font-mono' : '',
|
||||||
i === active ? 'bg-[var(--hover)] text-[color:var(--fg)]' : 'text-[color:var(--fg-2)]',
|
i === active ? 'bg-[var(--m-yellow)] text-[#111]' : 'text-[color:var(--fg-2)]',
|
||||||
]">
|
]">
|
||||||
<span class="truncate">{{ o.label }}</span>
|
<span class="truncate">{{ o.label }}</span>
|
||||||
<Icon v-if="o.value === modelValue" name="check" class="w-4 h-4 shrink-0 text-violet-400" />
|
<Icon v-if="o.value === modelValue" name="check" class="w-4 h-4 shrink-0 text-[var(--m-orange)]" />
|
||||||
</button>
|
</button>
|
||||||
<div v-if="!options.length" class="px-3 py-2 text-xs text-[color:var(--fg-3)]">无选项</div>
|
<div v-if="!options.length" class="px-3 py-2 text-xs text-[color:var(--fg-3)]">无选项</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -58,12 +58,12 @@ function remove(i) {
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
color: rgb(238 224 255); /* near-white violet */
|
color: rgb(255 237 213); /* near-white orange */
|
||||||
background: linear-gradient(135deg, rgb(167 139 250 / 0.18), rgb(236 72 153 / 0.14));
|
background: linear-gradient(135deg, rgb(253 186 116 / 0.18), rgb(251 113 133 / 0.14));
|
||||||
box-shadow: inset 0 0 0 1px rgb(167 139 250 / 0.3);
|
box-shadow: inset 0 0 0 1px rgb(253 186 116 / 0.3);
|
||||||
transition: background 0.15s ease;
|
transition: background 0.15s ease;
|
||||||
}
|
}
|
||||||
.chip:hover { background: linear-gradient(135deg, rgb(167 139 250 / 0.25), rgb(236 72 153 / 0.2)); }
|
.chip:hover { background: linear-gradient(135deg, rgb(253 186 116 / 0.25), rgb(251 113 133 / 0.2)); }
|
||||||
|
|
||||||
.chip-x {
|
.chip-x {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -98,8 +98,8 @@ async function submit() {
|
|||||||
<div v-else class="flex flex-wrap gap-1.5 max-h-44 overflow-y-auto rounded-lg ring-1 ring-slate-200 bg-slate-50/60 p-2">
|
<div v-else class="flex flex-wrap gap-1.5 max-h-44 overflow-y-auto rounded-lg ring-1 ring-slate-200 bg-slate-50/60 p-2">
|
||||||
<button v-for="m in allModels" :key="m.id" type="button" @click="toggle(m.id)"
|
<button v-for="m in allModels" :key="m.id" type="button" @click="toggle(m.id)"
|
||||||
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs ring-1 transition-colors"
|
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs ring-1 transition-colors"
|
||||||
:class="selected.includes(m.id) ? 'bg-indigo-500/15 text-indigo-700 ring-indigo-300 font-medium' : 'bg-white text-slate-600 ring-slate-200 hover:ring-slate-300'">
|
:class="selected.includes(m.id) ? 'bg-sky-500/15 text-sky-700 ring-sky-300 font-medium' : 'bg-white text-slate-600 ring-slate-200 hover:ring-slate-300'">
|
||||||
<span class="w-1.5 h-1.5 rounded-full" :class="m.type === 'video' ? 'bg-violet-400' : 'bg-emerald-400'"></span>{{ m.alias || m.id }}
|
<span class="w-1.5 h-1.5 rounded-full" :class="m.type === 'video' ? 'bg-orange-400' : 'bg-emerald-400'"></span>{{ m.alias || m.id }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -101,6 +101,14 @@ defineExpose({ startEdit, commitNote })
|
|||||||
>
|
>
|
||||||
<!-- 连线出口:拖到目标节点创建连线(舞台接管手势) -->
|
<!-- 连线出口:拖到目标节点创建连线(舞台接管手势) -->
|
||||||
<span v-if="node.type !== 'gen'" class="cv-port" data-port="out" />
|
<span v-if="node.type !== 'gen'" class="cv-port" data-port="out" />
|
||||||
|
<!-- 右下角缩放手柄 + 悬浮工具条:仅选中时显示,舞台接管 resize 手势 -->
|
||||||
|
<span v-if="selected && node.type !== 'gen'" class="cv-size" data-resize />
|
||||||
|
<div v-if="selected && node.type !== 'gen'" class="cv-tools" @dblclick.stop @pointerdown.stop>
|
||||||
|
<button v-if="node.type === 'image'" title="预览" @click.stop="emit('node-action', { type: 'preview' })">⤢</button>
|
||||||
|
<button title="复制 (Ctrl+D)" @click.stop="emit('node-action', { type: 'duplicate' })">⧉</button>
|
||||||
|
<button title="置顶" @click.stop="emit('node-action', { type: 'front' })">⤒</button>
|
||||||
|
<button class="cv-tools__del" title="删除" @click.stop="emit('node-action', { type: 'delete' })">✕</button>
|
||||||
|
</div>
|
||||||
<!-- 图片节点 -->
|
<!-- 图片节点 -->
|
||||||
<template v-if="node.type === 'image'">
|
<template v-if="node.type === 'image'">
|
||||||
<img
|
<img
|
||||||
@@ -144,17 +152,19 @@ defineExpose({ startEdit, commitNote })
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.cv-node {
|
.cv-node {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: 10px;
|
border-radius: 14px;
|
||||||
|
border: 2.5px solid var(--ink);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
contain: layout style paint;
|
contain: layout style paint;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
box-shadow: 0 2px 10px rgb(0 0 0 / 0.35);
|
background: var(--surface);
|
||||||
|
box-shadow: 5px 5px 0 var(--shadow-ink);
|
||||||
|
transition: box-shadow 0.15s ease;
|
||||||
}
|
}
|
||||||
.cv-node.is-selected {
|
.cv-node.is-selected {
|
||||||
outline: 2px solid #7c6cff;
|
box-shadow: 7px 7px 0 var(--m-orange);
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
}
|
||||||
.cv-node:active {
|
.cv-node:active {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
@@ -165,7 +175,7 @@ defineExpose({ startEdit, commitNote })
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
display: block;
|
display: block;
|
||||||
background: #14161d;
|
background: var(--surface-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cv-node__cap {
|
.cv-node__cap {
|
||||||
@@ -173,20 +183,22 @@ defineExpose({ startEdit, commitNote })
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
padding: 14px 8px 5px;
|
padding: 5px 9px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
color: #d6d9e2;
|
color: var(--fg);
|
||||||
background: linear-gradient(transparent, rgb(8 9 13 / 0.92));
|
background: var(--surface);
|
||||||
|
border-top: 2.5px solid var(--ink);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 笔记 = 孟菲斯黄贴纸 */
|
||||||
.cv-node--note {
|
.cv-node--note {
|
||||||
background: #23262f;
|
background: var(--m-yellow);
|
||||||
border: 1px solid #343947;
|
|
||||||
cursor: text;
|
cursor: text;
|
||||||
}
|
}
|
||||||
.cv-node--note:not(.is-editing) {
|
.cv-node--note:not(.is-editing) {
|
||||||
@@ -197,8 +209,8 @@ defineExpose({ startEdit, commitNote })
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.5;
|
line-height: 1.55;
|
||||||
color: #e8eaf0;
|
color: #1a1a1a;
|
||||||
outline: none;
|
outline: none;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
@@ -216,21 +228,23 @@ defineExpose({ startEdit, commitNote })
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: #1a1d26;
|
background: var(--surface);
|
||||||
border: 1.5px dashed #4a4f61;
|
border-style: dashed;
|
||||||
color: #9aa1b2;
|
color: var(--fg-2);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.cv-node--gen.is-failed {
|
.cv-node--gen.is-failed {
|
||||||
border-color: #6b3b44;
|
border-color: #e5484d;
|
||||||
color: #e58c96;
|
color: #e5484d;
|
||||||
|
background: #ffe4e6;
|
||||||
}
|
}
|
||||||
.cv-node__spin {
|
.cv-node__spin {
|
||||||
width: 22px;
|
width: 22px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 2.5px solid #3b4152;
|
border: 3px solid var(--ink-soft);
|
||||||
border-top-color: #7c6cff;
|
border-top-color: var(--m-orange);
|
||||||
animation: cv-spin 0.9s linear infinite;
|
animation: cv-spin 0.9s linear infinite;
|
||||||
}
|
}
|
||||||
@keyframes cv-spin {
|
@keyframes cv-spin {
|
||||||
@@ -246,21 +260,67 @@ defineExpose({ startEdit, commitNote })
|
|||||||
|
|
||||||
.cv-port {
|
.cv-port {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: -8px;
|
right: -5px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
width: 13px;
|
width: 15px;
|
||||||
height: 13px;
|
height: 15px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #7c6cff;
|
background: var(--m-orange);
|
||||||
border: 2.5px solid #0e1015;
|
border: 2.5px solid var(--ink);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.12s;
|
transition: opacity 0.12s;
|
||||||
cursor: crosshair;
|
cursor: crosshair;
|
||||||
z-index: 2;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
.cv-node:hover .cv-port,
|
.cv-node:hover .cv-port,
|
||||||
.cv-node.is-selected .cv-port {
|
.cv-node.is-selected .cv-port {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 右下角缩放方块(节点 contain:paint,只能放内侧) */
|
||||||
|
.cv-size {
|
||||||
|
position: absolute;
|
||||||
|
right: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--m-blue);
|
||||||
|
border: 2.5px solid var(--ink);
|
||||||
|
cursor: nwse-resize;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选中悬浮工具条:贴节点内上缘(节点 overflow:hidden,外侧会被裁) */
|
||||||
|
.cv-tools {
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
right: 6px;
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
.cv-tools button {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 7px;
|
||||||
|
border: 2.5px solid var(--ink);
|
||||||
|
background: var(--m-yellow);
|
||||||
|
color: #1a1a1a;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 2.5px 2.5px 0 var(--shadow-ink);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.cv-tools button:hover {
|
||||||
|
transform: translate(1px, 1px);
|
||||||
|
box-shadow: 1.5px 1.5px 0 var(--shadow-ink);
|
||||||
|
}
|
||||||
|
.cv-tools__del {
|
||||||
|
background: #ffb4ab !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const emit = defineEmits([
|
|||||||
'img-size', // 图片真实尺寸校正 -> { id, w, h }
|
'img-size', // 图片真实尺寸校正 -> { id, w, h }
|
||||||
'viewport-rest', // 视口静止 700ms -> viewport
|
'viewport-rest', // 视口静止 700ms -> viewport
|
||||||
'pan-zoom', // 视口变化(用于百分比显示,高频)
|
'pan-zoom', // 视口变化(用于百分比显示,高频)
|
||||||
|
'blank-dblclick', // 双击空白 -> { x, y } 世界坐标(父级建笔记)
|
||||||
])
|
])
|
||||||
|
|
||||||
const stageEl = ref(null)
|
const stageEl = ref(null)
|
||||||
@@ -186,6 +187,25 @@ function onPointerDown(e) {
|
|||||||
// 文本编辑中完全放行
|
// 文本编辑中完全放行
|
||||||
if (e.target.closest?.('.cv-node--note.is-editing')) return
|
if (e.target.closest?.('.cv-node--note.is-editing')) return
|
||||||
|
|
||||||
|
// 0) 缩放手柄:右下角墨边方块,拖动能改节点尺寸
|
||||||
|
const sizeGrip = e.target.closest?.('[data-resize]')
|
||||||
|
if (sizeGrip && e.button === 0) {
|
||||||
|
const node = hitNode(e)
|
||||||
|
if (node) {
|
||||||
|
clearEdgeSel()
|
||||||
|
setSelection(new Set([node.id]))
|
||||||
|
const start = screenToWorld(localXY(e).x, localXY(e).y, vp)
|
||||||
|
act = {
|
||||||
|
mode: 'resize', node,
|
||||||
|
startX: start.x, startY: start.y,
|
||||||
|
w0: node.w, h0: node.h,
|
||||||
|
aspect: node.h > 0 ? node.w / node.h : 1,
|
||||||
|
}
|
||||||
|
try { stageEl.value.setPointerCapture(e.pointerId) } catch { /* 合成/失效指针 */ }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 1) 连线出口:拖到目标节点建边
|
// 1) 连线出口:拖到目标节点建边
|
||||||
const port = e.target.closest?.('[data-port]')
|
const port = e.target.closest?.('[data-port]')
|
||||||
if (port && e.button === 0) {
|
if (port && e.button === 0) {
|
||||||
@@ -292,6 +312,20 @@ function applyMove(e) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (act.mode === 'resize') {
|
||||||
|
const cur = screenToWorld(p.x, p.y, vp)
|
||||||
|
const n = act.node
|
||||||
|
if (n.type === 'note') {
|
||||||
|
n.w = Math.max(140, Math.round(act.w0 + cur.x - act.startX))
|
||||||
|
n.h = Math.max(90, Math.round(act.h0 + cur.y - act.startY))
|
||||||
|
} else {
|
||||||
|
// 图片锁长宽比:以宽度为准推导高度
|
||||||
|
const w = Math.max(96, act.w0 + cur.x - act.startX)
|
||||||
|
n.w = Math.round(w)
|
||||||
|
n.h = Math.round(w / act.aspect)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
if (act.mode === 'link' && linkLine.value) {
|
if (act.mode === 'link' && linkLine.value) {
|
||||||
const cur = screenToWorld(p.x, p.y, vp)
|
const cur = screenToWorld(p.x, p.y, vp)
|
||||||
linkLine.value.x2 = cur.x
|
linkLine.value.x2 = cur.x
|
||||||
@@ -320,6 +354,7 @@ function onPointerUp(e) {
|
|||||||
panning.value = false
|
panning.value = false
|
||||||
try { stageEl.value.releasePointerCapture(e.pointerId) } catch { /* 已释放 */ }
|
try { stageEl.value.releasePointerCapture(e.pointerId) } catch { /* 已释放 */ }
|
||||||
if (a.mode === 'drag' && a.moved) emit('move-end')
|
if (a.mode === 'drag' && a.moved) emit('move-end')
|
||||||
|
if (a.mode === 'resize') emit('move-end') // 尺寸进了快照,可撤销
|
||||||
if (a.mode === 'link') {
|
if (a.mode === 'link') {
|
||||||
const target = hitNode(e)
|
const target = hitNode(e)
|
||||||
if (target && target.id !== a.fromId) emit('link-create', { from: a.fromId, to: target.id })
|
if (target && target.id !== a.fromId) emit('link-create', { from: a.fromId, to: target.id })
|
||||||
@@ -328,6 +363,7 @@ function onPointerUp(e) {
|
|||||||
}
|
}
|
||||||
function onPointerCancel() {
|
function onPointerCancel() {
|
||||||
if (act?.mode === 'drag' && act.moved) emit('move-end')
|
if (act?.mode === 'drag' && act.moved) emit('move-end')
|
||||||
|
if (act?.mode === 'resize') emit('move-end')
|
||||||
act = null
|
act = null
|
||||||
marquee.value = null
|
marquee.value = null
|
||||||
linkLine.value = null
|
linkLine.value = null
|
||||||
@@ -390,6 +426,14 @@ function setSelection(s) {
|
|||||||
selection.value = s
|
selection.value = s
|
||||||
emit('selection', [...s])
|
emit('selection', [...s])
|
||||||
}
|
}
|
||||||
|
// 父级复制/成组操作后同步舞台内部选择集
|
||||||
|
function selectIds(ids) {
|
||||||
|
setSelection(new Set(ids))
|
||||||
|
}
|
||||||
|
function bumpNode(id) {
|
||||||
|
const n = nodeById.value.get(id)
|
||||||
|
if (n) bumpZ(n)
|
||||||
|
}
|
||||||
|
|
||||||
// ---- 对外 API ----
|
// ---- 对外 API ----
|
||||||
function setViewport(v) {
|
function setViewport(v) {
|
||||||
@@ -412,6 +456,37 @@ function centerOn(x, y) {
|
|||||||
function clearSelection() {
|
function clearSelection() {
|
||||||
if (selection.value.size) setSelection(new Set())
|
if (selection.value.size) setSelection(new Set())
|
||||||
}
|
}
|
||||||
|
// 双击空白:交给父级在落点建笔记
|
||||||
|
function onDblClick(e) {
|
||||||
|
if (e.target.closest?.('[data-node-id]') || e.target.closest?.('.cv-edge-hit')) return
|
||||||
|
const p = localXY(e)
|
||||||
|
const w = screenToWorld(p.x, p.y, vp)
|
||||||
|
emit('blank-dblclick', { x: w.x, y: w.y })
|
||||||
|
}
|
||||||
|
// 适应内容:把所有节点缩放进视口(留 80px 边距,最大放到 1.2x)
|
||||||
|
function fitToContent() {
|
||||||
|
const r = stageEl.value?.getBoundingClientRect()
|
||||||
|
const sw = r ? r.width : 800
|
||||||
|
const sh = r ? r.height : 600
|
||||||
|
const nodes = props.nodes
|
||||||
|
if (!nodes.length) {
|
||||||
|
setViewport({ x: 0, y: 0, scale: 1 })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let x1 = Infinity, y1 = Infinity, x2 = -Infinity, y2 = -Infinity
|
||||||
|
for (const n of nodes) {
|
||||||
|
x1 = Math.min(x1, n.x); y1 = Math.min(y1, n.y)
|
||||||
|
x2 = Math.max(x2, n.x + n.w); y2 = Math.max(y2, n.y + n.h)
|
||||||
|
}
|
||||||
|
const pad = 80
|
||||||
|
const bw = Math.max(1, x2 - x1 + pad * 2)
|
||||||
|
const bh = Math.max(1, y2 - y1 + pad * 2)
|
||||||
|
const s = Math.min(sw / bw, sh / bh, 1.2)
|
||||||
|
vp.scale = s
|
||||||
|
vp.x = sw / 2 - (x1 - pad + bw / 2) * s
|
||||||
|
vp.y = sh / 2 - (y1 - pad + bh / 2) * s
|
||||||
|
notifyViewport()
|
||||||
|
}
|
||||||
function selectedIds() {
|
function selectedIds() {
|
||||||
return [...selection.value]
|
return [...selection.value]
|
||||||
}
|
}
|
||||||
@@ -430,7 +505,7 @@ function screenRectToWorld(r) {
|
|||||||
}
|
}
|
||||||
defineExpose({
|
defineExpose({
|
||||||
setViewport, zoomBy, centerOn, clearSelection, selectedIds, selectAll, syncZTop, vp,
|
setViewport, zoomBy, centerOn, clearSelection, selectedIds, selectAll, syncZTop, vp,
|
||||||
cancelLink, consumeSelectedEdges, hasSelectedEdges,
|
cancelLink, consumeSelectedEdges, hasSelectedEdges, fitToContent, selectIds, bumpNode,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -444,6 +519,7 @@ defineExpose({
|
|||||||
@pointermove="onPointerMove"
|
@pointermove="onPointerMove"
|
||||||
@pointerup="onPointerUp"
|
@pointerup="onPointerUp"
|
||||||
@pointercancel="onPointerCancel"
|
@pointercancel="onPointerCancel"
|
||||||
|
@dblclick="onDblClick"
|
||||||
@contextmenu.prevent
|
@contextmenu.prevent
|
||||||
>
|
>
|
||||||
<div class="cv-world" :style="worldStyle">
|
<div class="cv-world" :style="worldStyle">
|
||||||
@@ -484,8 +560,12 @@ defineExpose({
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
background-color: #0e1015;
|
background-color: var(--app-bg);
|
||||||
background-image: radial-gradient(circle, rgb(255 255 255 / 0.07) 1px, transparent 1px);
|
background-image: radial-gradient(
|
||||||
|
circle,
|
||||||
|
color-mix(in srgb, var(--ink) 18%, transparent) 1.3px,
|
||||||
|
transparent 1.3px
|
||||||
|
);
|
||||||
}
|
}
|
||||||
.cv-stage.is-panning {
|
.cv-stage.is-panning {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
@@ -509,17 +589,17 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
.cv-edge {
|
.cv-edge {
|
||||||
fill: none;
|
fill: none;
|
||||||
stroke: rgb(140 146 165 / 0.5);
|
stroke: var(--ink-soft);
|
||||||
stroke-width: 1.5;
|
|
||||||
}
|
|
||||||
.cv-edge--hot {
|
|
||||||
stroke: #7c6cff;
|
|
||||||
stroke-width: 2;
|
stroke-width: 2;
|
||||||
}
|
}
|
||||||
.cv-edge--sel {
|
.cv-edge--hot {
|
||||||
stroke: #ff7b8f;
|
stroke: var(--m-orange);
|
||||||
stroke-width: 2.5;
|
stroke-width: 2.5;
|
||||||
filter: drop-shadow(0 0 3px rgb(255 123 143 / 0.6));
|
}
|
||||||
|
.cv-edge--sel {
|
||||||
|
stroke: var(--m-orange);
|
||||||
|
stroke-width: 3.5;
|
||||||
|
stroke-dasharray: 10 6;
|
||||||
}
|
}
|
||||||
.cv-edge-hit {
|
.cv-edge-hit {
|
||||||
fill: none;
|
fill: none;
|
||||||
@@ -530,17 +610,17 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
.cv-linkline {
|
.cv-linkline {
|
||||||
fill: none;
|
fill: none;
|
||||||
stroke: #a99dff;
|
stroke: var(--m-orange);
|
||||||
stroke-width: 2;
|
stroke-width: 2.5;
|
||||||
stroke-dasharray: 7 6;
|
stroke-dasharray: 8 6;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.cv-marquee {
|
.cv-marquee {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border: 1px solid #7c6cff;
|
border: 2px dashed var(--ink);
|
||||||
background: rgb(124 108 255 / 0.12);
|
background: color-mix(in srgb, var(--m-yellow) 35%, transparent);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
border-radius: 2px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
.cv-hint {
|
.cv-hint {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -548,8 +628,17 @@ defineExpose({
|
|||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: rgb(255 255 255 / 0.28);
|
font-weight: 600;
|
||||||
|
color: var(--fg-2);
|
||||||
|
background: var(--surface);
|
||||||
|
border: 2px solid var(--ink);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 3px 14px;
|
||||||
|
box-shadow: 3px 3px 0 var(--shadow-ink);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
max-width: 92%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -47,15 +47,15 @@ watch(() => route.path, () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="theme-x h-screen flex bg-[var(--app-bg)] text-[color:var(--fg-2)] selection:bg-violet-400/30 overflow-hidden">
|
<div class="theme-x h-screen flex bg-[var(--app-bg)] text-[color:var(--fg-2)] selection:bg-orange-400/30 overflow-hidden">
|
||||||
<!-- ===== Sidebar ===== -->
|
<!-- ===== Sidebar ===== -->
|
||||||
<aside class="w-60 shrink-0 border-r border-[color:var(--hairline)] bg-[var(--surface)] backdrop-blur-md flex flex-col">
|
<aside class="w-60 shrink-0 border-r-[3px] border-r-[color:var(--ink)] bg-[var(--surface)] backdrop-blur-md flex flex-col">
|
||||||
<router-link to="/" class="h-16 flex items-center gap-2.5 px-5 border-b border-[color:var(--hairline)] group">
|
<router-link to="/" class="h-16 flex items-center gap-2.5 px-5 border-b-[3px] border-b-[color:var(--ink)] group">
|
||||||
<img v-if="site.logo" :src="site.logo" :alt="site.title" class="w-8 h-8 rounded-[10px] object-contain shadow-lg shadow-violet-500/20 ring-1 ring-white/10" />
|
<img v-if="site.logo" :src="site.logo" :alt="site.title" class="w-8 h-8 rounded-[10px] object-contain ring-2 ring-[color:var(--ink)]" />
|
||||||
<Logo v-else :size="32" />
|
<Logo v-else :size="32" />
|
||||||
<div class="leading-tight min-w-0">
|
<div class="leading-tight min-w-0">
|
||||||
<div class="text-sm font-semibold truncate tracking-tight text-[color:var(--fg)]">{{ site.title }}</div>
|
<div class="text-sm font-extrabold truncate tracking-tight text-[color:var(--fg)]">{{ site.title }}</div>
|
||||||
<div class="text-[11px] text-[color:var(--fg-3)] truncate">Admin</div>
|
<div class="text-[11px] font-bold uppercase tracking-widest text-[color:var(--m-orange)] truncate">Admin</div>
|
||||||
</div>
|
</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
@@ -64,16 +64,16 @@ watch(() => route.path, () => {
|
|||||||
<router-link v-if="!t.children" :to="t.to"
|
<router-link v-if="!t.children" :to="t.to"
|
||||||
class="admin-link group"
|
class="admin-link group"
|
||||||
active-class="active">
|
active-class="active">
|
||||||
<span class="absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-5 rounded-r-full opacity-0 transition-opacity"
|
<span class="absolute left-1 top-1/2 -translate-y-1/2 w-[6px] h-6 rounded-sm opacity-0 transition-opacity"
|
||||||
style="background: linear-gradient(180deg, #f0abfc, #a78bfa)"></span>
|
style="background: var(--m-orange)"></span>
|
||||||
<Icon :name="t.icon" class="w-4 h-4 shrink-0 opacity-70 group-hover:opacity-100 transition-opacity" />
|
<Icon :name="t.icon" class="w-4 h-4 shrink-0 opacity-70 group-hover:opacity-100 transition-opacity" />
|
||||||
<span class="text-sm">{{ t.label }}</span>
|
<span class="text-sm">{{ t.label }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<button type="button" @click="toggleGroup(t.label)"
|
<button type="button" @click="toggleGroup(t.label)"
|
||||||
class="admin-link group w-full text-left" :class="groupActive(t) && 'active'">
|
class="admin-link group w-full text-left" :class="groupActive(t) && 'active'">
|
||||||
<span class="absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-5 rounded-r-full opacity-0 transition-opacity"
|
<span class="absolute left-1 top-1/2 -translate-y-1/2 w-[6px] h-6 rounded-sm opacity-0 transition-opacity"
|
||||||
style="background: linear-gradient(180deg, #f0abfc, #a78bfa)"></span>
|
style="background: var(--m-orange)"></span>
|
||||||
<Icon :name="t.icon" class="w-4 h-4 shrink-0 opacity-70 group-hover:opacity-100 transition-opacity" />
|
<Icon :name="t.icon" class="w-4 h-4 shrink-0 opacity-70 group-hover:opacity-100 transition-opacity" />
|
||||||
<span class="text-sm">{{ t.label }}</span>
|
<span class="text-sm">{{ t.label }}</span>
|
||||||
<svg class="w-3 h-3 ml-auto transition-transform" :class="openGroups.has(t.label) && 'rotate-90'"
|
<svg class="w-3 h-3 ml-auto transition-transform" :class="openGroups.has(t.label) && 'rotate-90'"
|
||||||
@@ -89,7 +89,7 @@ watch(() => route.path, () => {
|
|||||||
</template>
|
</template>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="p-3 border-t border-[color:var(--hairline)] space-y-1">
|
<div class="p-3 border-t-[3px] border-t-[color:var(--ink)] space-y-1">
|
||||||
<button type="button" @click="toggleTheme"
|
<button type="button" @click="toggleTheme"
|
||||||
class="w-full flex items-center gap-2.5 rounded-lg px-3 py-2 text-xs text-[color:var(--fg-2)] hover:bg-[var(--hover)] hover:text-[color:var(--fg)] transition-colors">
|
class="w-full flex items-center gap-2.5 rounded-lg px-3 py-2 text-xs text-[color:var(--fg-2)] hover:bg-[var(--hover)] hover:text-[color:var(--fg)] transition-colors">
|
||||||
<svg v-if="isDark" class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
|
<svg v-if="isDark" class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
|
||||||
@@ -106,18 +106,19 @@ watch(() => route.path, () => {
|
|||||||
|
|
||||||
<!-- ===== Main ===== -->
|
<!-- ===== Main ===== -->
|
||||||
<div class="flex-1 min-w-0 flex flex-col relative">
|
<div class="flex-1 min-w-0 flex flex-col relative">
|
||||||
<!-- soft background mesh, mirrors the public shell -->
|
<!-- memphis backdrop: polka dots + flat crayon washes -->
|
||||||
<div aria-hidden="true" class="pointer-events-none absolute inset-0 overflow-hidden">
|
<div aria-hidden="true" class="pointer-events-none absolute inset-0 overflow-hidden">
|
||||||
<div class="absolute -top-32 left-1/3 w-[40rem] h-[40rem] rounded-full opacity-[0.12]"
|
<div class="memphis-dots absolute inset-0 opacity-50"></div>
|
||||||
style="background: radial-gradient(circle, #a855f7, transparent 60%); filter: blur(110px)"></div>
|
<div class="absolute -top-32 left-1/3 w-[40rem] h-[40rem] rounded-full opacity-[0.16]"
|
||||||
<div class="absolute top-1/2 -right-40 w-[36rem] h-[36rem] rounded-full opacity-[0.10]"
|
style="background: radial-gradient(circle, #fef08a, transparent 60%); filter: blur(100px)"></div>
|
||||||
style="background: radial-gradient(circle, #06b6d4, transparent 60%); filter: blur(110px)"></div>
|
<div class="absolute top-1/2 -right-40 w-[36rem] h-[36rem] rounded-full opacity-[0.14]"
|
||||||
<div class="absolute bottom-0 left-0 w-[32rem] h-[32rem] rounded-full opacity-[0.08]"
|
style="background: radial-gradient(circle, #bae6fd, transparent 60%); filter: blur(100px)"></div>
|
||||||
style="background: radial-gradient(circle, #f43f5e, transparent 60%); filter: blur(110px)"></div>
|
<div class="absolute bottom-0 left-0 w-[32rem] h-[32rem] rounded-full opacity-[0.12]"
|
||||||
|
style="background: radial-gradient(circle, #ffc1cc, transparent 60%); filter: blur(100px)"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<header class="relative z-10 h-14 shrink-0 border-b border-[color:var(--hairline)] bg-[var(--app-bg)]/70 backdrop-blur-md flex items-center px-8">
|
<header class="relative z-10 h-14 shrink-0 border-b-[3px] border-b-[color:var(--ink)] bg-[var(--app-bg)]/70 backdrop-blur-md flex items-center px-8">
|
||||||
<div class="text-[10px] uppercase tracking-[0.25em] text-[color:var(--fg-3)] font-medium mr-3">Admin</div>
|
<div class="text-[10px] uppercase tracking-[0.25em] text-[color:var(--m-orange)] font-extrabold mr-3">Admin</div>
|
||||||
<div class="text-[color:var(--fg-faint)] mr-3">/</div>
|
<div class="text-[color:var(--fg-faint)] mr-3">/</div>
|
||||||
<h1 class="text-sm font-semibold tracking-tight text-[color:var(--fg)]">{{ currentLabel }}</h1>
|
<h1 class="text-sm font-semibold tracking-tight text-[color:var(--fg)]">{{ currentLabel }}</h1>
|
||||||
</header>
|
</header>
|
||||||
@@ -143,12 +144,20 @@ watch(() => route.path, () => {
|
|||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
padding: 0.55rem 0.875rem;
|
padding: 0.55rem 0.875rem;
|
||||||
border-radius: 0.625rem;
|
border-radius: 0.625rem;
|
||||||
|
border: 2px solid transparent;
|
||||||
color: var(--fg-2);
|
color: var(--fg-2);
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
transition: background 0.15s ease, color 0.15s ease;
|
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
|
||||||
}
|
}
|
||||||
.admin-link:hover { background: var(--hover); color: var(--fg); }
|
.admin-link:hover { background: rgb(254 240 138 / 0.5); color: var(--fg); border-color: var(--ink); }
|
||||||
.admin-link.active { color: var(--fg); background: var(--hover); }
|
.admin-link.active {
|
||||||
|
color: var(--fg);
|
||||||
|
background: var(--m-yellow);
|
||||||
|
border-color: var(--ink);
|
||||||
|
box-shadow: 3px 3px 0 var(--shadow-ink);
|
||||||
|
}
|
||||||
|
html.dark .admin-link:hover { background: rgb(236 91 19 / 0.18); }
|
||||||
|
html.dark .admin-link.active { color: #fff; background: var(--m-orange); }
|
||||||
.admin-link.active > span:first-child { opacity: 1; }
|
.admin-link.active > span:first-child { opacity: 1; }
|
||||||
|
|
||||||
.admin-sublink {
|
.admin-sublink {
|
||||||
@@ -156,12 +165,15 @@ watch(() => route.path, () => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.45rem 0.875rem 0.45rem 2.6rem;
|
padding: 0.45rem 0.875rem 0.45rem 2.6rem;
|
||||||
border-radius: 0.625rem;
|
border-radius: 0.625rem;
|
||||||
|
border: 2px solid transparent;
|
||||||
color: var(--fg-2);
|
color: var(--fg-2);
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
transition: background 0.15s ease, color 0.15s ease;
|
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
||||||
}
|
}
|
||||||
.admin-sublink:hover { background: var(--hover); color: var(--fg); }
|
.admin-sublink:hover { background: rgb(254 240 138 / 0.5); color: var(--fg); border-color: var(--ink); }
|
||||||
.admin-sublink.active { color: var(--fg); background: var(--hover); }
|
.admin-sublink.active { color: var(--fg); background: var(--m-yellow); border-color: var(--ink); }
|
||||||
|
html.dark .admin-sublink:hover { background: rgb(236 91 19 / 0.18); }
|
||||||
|
html.dark .admin-sublink.active { color: #fff; background: var(--m-orange); }
|
||||||
|
|
||||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.15s ease, transform 0.15s ease; }
|
.fade-enter-active, .fade-leave-active { transition: opacity 0.15s ease, transform 0.15s ease; }
|
||||||
.fade-enter-from { opacity: 0; transform: translateY(4px); }
|
.fade-enter-from { opacity: 0; transform: translateY(4px); }
|
||||||
|
|||||||
@@ -67,9 +67,9 @@ async function onLogout() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="theme-x min-h-screen bg-[var(--app-bg)] text-[color:var(--fg-2)] selection:bg-violet-400/30">
|
<div class="theme-x min-h-screen bg-[var(--app-bg)] text-[color:var(--fg-2)] selection:bg-orange-400/30">
|
||||||
<!-- ===== Top bar ===== -->
|
<!-- ===== Top bar ===== -->
|
||||||
<header class="fixed top-0 inset-x-0 z-40 border-b border-transparent backdrop-blur-xl top-bar">
|
<header class="fixed top-0 inset-x-0 z-40 backdrop-blur-xl top-bar">
|
||||||
<div class="mx-auto max-w-[1400px] px-4 md:px-8 h-[58px] flex items-center gap-3">
|
<div class="mx-auto max-w-[1400px] px-4 md:px-8 h-[58px] flex items-center gap-3">
|
||||||
<!-- brand -->
|
<!-- brand -->
|
||||||
<router-link to="/" class="flex items-center gap-2.5 shrink-0 transition-transform hover:scale-[1.03]">
|
<router-link to="/" class="flex items-center gap-2.5 shrink-0 transition-transform hover:scale-[1.03]">
|
||||||
@@ -142,7 +142,7 @@ async function onLogout() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- mobile panel -->
|
<!-- mobile panel -->
|
||||||
<div v-if="mobileOpen" class="md:hidden border-t border-[color:var(--hairline)] px-3 py-3 bg-[var(--menu-bg)]">
|
<div v-if="mobileOpen" class="md:hidden border-t-2 border-[color:var(--ink)] px-3 py-3 bg-[var(--menu-bg)]">
|
||||||
<router-link v-for="n in nav" :key="n.to" :to="n.to"
|
<router-link v-for="n in nav" :key="n.to" :to="n.to"
|
||||||
:exact-active-class="n.to === '/' ? 'm-on' : ''"
|
:exact-active-class="n.to === '/' ? 'm-on' : ''"
|
||||||
:active-class="n.to === '/' ? '' : 'm-on'"
|
:active-class="n.to === '/' ? '' : 'm-on'"
|
||||||
@@ -164,14 +164,15 @@ async function onLogout() {
|
|||||||
|
|
||||||
<!-- ===== Main column ===== -->
|
<!-- ===== Main column ===== -->
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<!-- soft background mesh -->
|
<!-- memphis backdrop: polka dots + flat crayon washes -->
|
||||||
<div aria-hidden="true" class="pointer-events-none absolute inset-0 overflow-hidden">
|
<div aria-hidden="true" class="pointer-events-none absolute inset-0 overflow-hidden">
|
||||||
<div class="absolute -top-32 left-1/3 w-[40rem] h-[40rem] rounded-full opacity-[0.16]"
|
<div class="memphis-dots absolute inset-0 opacity-60"></div>
|
||||||
style="background: radial-gradient(circle, #a855f7, transparent 60%); filter: blur(100px)"></div>
|
<div class="absolute -top-32 left-1/3 w-[40rem] h-[40rem] rounded-full opacity-[0.22]"
|
||||||
<div class="absolute top-1/2 -right-40 w-[36rem] h-[36rem] rounded-full opacity-[0.14]"
|
style="background: radial-gradient(circle, #fef08a, transparent 60%); filter: blur(90px)"></div>
|
||||||
style="background: radial-gradient(circle, #06b6d4, transparent 60%); filter: blur(100px)"></div>
|
<div class="absolute top-1/2 -right-40 w-[36rem] h-[36rem] rounded-full opacity-[0.18]"
|
||||||
<div class="absolute bottom-0 left-0 w-[32rem] h-[32rem] rounded-full opacity-[0.10]"
|
style="background: radial-gradient(circle, #bae6fd, transparent 60%); filter: blur(90px)"></div>
|
||||||
style="background: radial-gradient(circle, #f43f5e, transparent 60%); filter: blur(100px)"></div>
|
<div class="absolute bottom-0 left-0 w-[32rem] h-[32rem] rounded-full opacity-[0.16]"
|
||||||
|
style="background: radial-gradient(circle, #ffc1cc, transparent 60%); filter: blur(90px)"></div>
|
||||||
<div class="anime-stars"></div>
|
<div class="anime-stars"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -195,42 +196,39 @@ async function onLogout() {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.top-bar {
|
.top-bar {
|
||||||
background: color-mix(in srgb, var(--app-bg) 72%, transparent);
|
background: color-mix(in srgb, var(--app-bg) 82%, transparent);
|
||||||
|
border-bottom: 2.5px solid var(--ink);
|
||||||
transition: border-color 0.2s ease;
|
transition: border-color 0.2s ease;
|
||||||
}
|
}
|
||||||
.top-bar:hover { border-bottom-color: var(--hairline); }
|
|
||||||
|
|
||||||
.top-link {
|
.top-link {
|
||||||
padding: 0.45rem 0.8rem;
|
padding: 0.45rem 0.8rem;
|
||||||
border-radius: 999px;
|
border-radius: 0.6rem;
|
||||||
font-size: 13.5px;
|
font-size: 13.5px;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
color: var(--fg-2);
|
color: var(--fg-2);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: 2px solid transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 0.15s ease, background 0.15s ease;
|
transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
|
||||||
}
|
}
|
||||||
.top-link:hover { color: var(--fg); background: var(--hover); }
|
.top-link:hover { color: var(--fg); background: var(--m-yellow); border-color: var(--ink); }
|
||||||
.top-link.on {
|
.top-link.on {
|
||||||
color: #a21caf;
|
color: #111;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
background: linear-gradient(135deg, rgb(244 114 182 / 0.16), rgb(129 140 248 / 0.16));
|
background: var(--m-yellow);
|
||||||
box-shadow: inset 0 0 0 1px rgb(217 70 239 / 0.3);
|
border-color: var(--ink);
|
||||||
|
box-shadow: 3px 3px 0 var(--shadow-ink);
|
||||||
}
|
}
|
||||||
html.dark .top-link.on {
|
html.dark .top-link.on {
|
||||||
color: #f5d0fe;
|
color: #fff;
|
||||||
background: linear-gradient(135deg, rgb(244 114 182 / 0.2), rgb(129 140 248 / 0.2));
|
background: var(--m-orange);
|
||||||
box-shadow: inset 0 0 0 1px rgb(217 70 239 / 0.35), 0 0 16px -6px rgb(217 70 239 / 0.55);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-spark {
|
.brand-spark {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
background: linear-gradient(135deg, #f472b6, #38bdf8);
|
color: var(--m-orange);
|
||||||
-webkit-background-clip: text;
|
|
||||||
background-clip: text;
|
|
||||||
color: transparent;
|
|
||||||
animation: brandSpark 3.5s ease-in-out infinite;
|
animation: brandSpark 3.5s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
@keyframes brandSpark {
|
@keyframes brandSpark {
|
||||||
@@ -242,34 +240,38 @@ html.dark .top-link.on {
|
|||||||
.m-link {
|
.m-link {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 0.6rem 0.75rem;
|
padding: 0.6rem 0.75rem;
|
||||||
border-radius: 0.85rem;
|
border-radius: 0.6rem;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
color: var(--fg-2);
|
color: var(--fg-2);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: 2px solid transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 0.15s ease, background 0.15s ease;
|
transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
|
||||||
}
|
}
|
||||||
.m-link:hover { color: var(--fg); background: var(--hover); }
|
.m-link:hover { color: var(--fg); background: var(--m-yellow); border-color: var(--ink); }
|
||||||
.m-link.m-on {
|
.m-link.m-on {
|
||||||
color: #a21caf;
|
color: #111;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
background: linear-gradient(135deg, rgb(244 114 182 / 0.16), rgb(129 140 248 / 0.16));
|
background: var(--m-yellow);
|
||||||
|
border-color: var(--ink);
|
||||||
|
box-shadow: 3px 3px 0 var(--shadow-ink);
|
||||||
}
|
}
|
||||||
html.dark .m-link.m-on { color: #f5d0fe; background: linear-gradient(135deg, rgb(244 114 182 / 0.2), rgb(129 140 248 / 0.2)); }
|
html.dark .m-link.m-on { color: #fff; background: var(--m-orange); }
|
||||||
|
|
||||||
.round-btn {
|
.round-btn {
|
||||||
width: 2.125rem;
|
width: 2.125rem;
|
||||||
height: 2.125rem;
|
height: 2.125rem;
|
||||||
border-radius: 999px;
|
border-radius: 0.6rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
color: var(--fg-3);
|
color: var(--fg-3);
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
box-shadow: inset 0 0 0 1px var(--hairline);
|
border: 2px solid var(--ink);
|
||||||
transition: color 0.15s ease, background 0.15s ease;
|
transition: color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
|
||||||
}
|
}
|
||||||
.round-btn:hover { color: var(--fg); background: var(--hover); }
|
.round-btn:hover { color: #111; background: var(--m-yellow); box-shadow: 3px 3px 0 var(--shadow-ink); transform: translate(-1px, -1px); }
|
||||||
|
.round-btn:active { transform: scale(0.95) translateY(1px); box-shadow: none; }
|
||||||
|
|
||||||
/* mobile-only: .round-btn's display:grid outranks Tailwind's md:hidden, so
|
/* mobile-only: .round-btn's display:grid outranks Tailwind's md:hidden, so
|
||||||
the breakpoint lives here instead */
|
the breakpoint lives here instead */
|
||||||
@@ -280,17 +282,18 @@ html.dark .m-link.m-on { color: #f5d0fe; background: linear-gradient(135deg, rgb
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
padding: 0.5rem 1.1rem;
|
padding: 0.5rem 1.1rem;
|
||||||
border-radius: 999px;
|
border-radius: 0.65rem;
|
||||||
font-size: 13.5px;
|
font-size: 13.5px;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: linear-gradient(135deg, #f472b6, #a855f7 55%, #38bdf8);
|
background: var(--m-orange);
|
||||||
border: none;
|
border: 2.5px solid var(--ink);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: 0 12px 26px -14px rgb(168 85 247 / 0.75);
|
box-shadow: 3px 3px 0 var(--shadow-ink);
|
||||||
transition: transform 0.15s ease, filter 0.15s ease, box-shadow 0.15s ease;
|
transition: transform 0.15s ease, filter 0.15s ease, box-shadow 0.15s ease;
|
||||||
}
|
}
|
||||||
.solid-btn:hover { filter: brightness(1.06); transform: translateY(-1px); box-shadow: 0 16px 30px -14px rgb(168 85 247 / 0.85); }
|
.solid-btn:hover { filter: brightness(1.06); transform: translate(-1px, -1px); box-shadow: 5px 5px 0 var(--shadow-ink); }
|
||||||
|
.solid-btn:active { transform: scale(0.97) translateY(2px); box-shadow: 0 0 0 var(--shadow-ink); }
|
||||||
|
|
||||||
.name-btn {
|
.name-btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -299,19 +302,19 @@ html.dark .m-link.m-on { color: #f5d0fe; background: linear-gradient(135deg, rgb
|
|||||||
max-width: 10rem;
|
max-width: 10rem;
|
||||||
height: 2.125rem;
|
height: 2.125rem;
|
||||||
padding: 0 0.7rem 0 0.85rem;
|
padding: 0 0.7rem 0 0.85rem;
|
||||||
border-radius: 999px;
|
border-radius: 0.6rem;
|
||||||
border: none;
|
border: 2px solid var(--ink);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
box-shadow: inset 0 0 0 1px var(--hairline);
|
transition: background 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
|
||||||
transition: background 0.15s ease, box-shadow 0.15s ease;
|
|
||||||
}
|
}
|
||||||
.name-btn:hover {
|
.name-btn:hover {
|
||||||
background: linear-gradient(135deg, rgb(244 114 182 / 0.14), rgb(129 140 248 / 0.14));
|
background: var(--m-yellow);
|
||||||
box-shadow: inset 0 0 0 1px rgb(217 70 239 / 0.3);
|
box-shadow: 3px 3px 0 var(--shadow-ink);
|
||||||
|
transform: translate(-1px, -1px);
|
||||||
}
|
}
|
||||||
.name-txt {
|
.name-txt {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -325,23 +328,25 @@ html.dark .m-link.m-on { color: #f5d0fe; background: linear-gradient(135deg, rgb
|
|||||||
top: 2.75rem;
|
top: 2.75rem;
|
||||||
width: 11.5rem;
|
width: 11.5rem;
|
||||||
padding: 0.4rem;
|
padding: 0.4rem;
|
||||||
border-radius: 0.9rem;
|
border-radius: 0.75rem;
|
||||||
background: var(--menu-bg);
|
background: var(--menu-bg);
|
||||||
box-shadow: inset 0 0 0 1px var(--hairline), 0 20px 44px rgb(0 0 0 / 0.24);
|
border: 2.5px solid var(--ink);
|
||||||
|
box-shadow: 6px 6px 0 var(--shadow-ink);
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
}
|
}
|
||||||
.menu-item {
|
.menu-item {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 0.5rem 0.65rem;
|
padding: 0.5rem 0.65rem;
|
||||||
border-radius: 0.55rem;
|
border-radius: 0.5rem;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
color: var(--fg-2);
|
color: var(--fg-2);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.menu-item:hover { background: var(--hover); color: var(--fg); }
|
.menu-item:hover { background: var(--m-yellow); color: #111; }
|
||||||
.menu-sep { height: 1px; background: var(--hairline); margin: 0.35rem 0.25rem; }
|
.menu-sep { height: 2px; background: var(--ink); opacity: 0.15; margin: 0.35rem 0.25rem; }
|
||||||
|
|
||||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.2s ease, transform 0.2s ease; }
|
.fade-enter-active, .fade-leave-active { transition: opacity 0.2s ease, transform 0.2s ease; }
|
||||||
.fade-enter-from { opacity: 0; transform: translateY(8px); }
|
.fade-enter-from { opacity: 0; transform: translateY(8px); }
|
||||||
|
|||||||
+252
-135
@@ -9,59 +9,72 @@ video {
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: "Inter", ui-sans-serif, system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
|
font-family: "Inter", ui-sans-serif, system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
--tw-ring-color: rgb(15 23 42 / 0.08);
|
--tw-ring-color: rgb(17 17 17 / 0.12);
|
||||||
|
|
||||||
/* ===== Theme palette — LIGHT (default) =====
|
/* ===== Theme palette — LIGHT (default): Memphis / neo-brutalism =====
|
||||||
Drives the app chrome (rails/header) and the public marketing pages.
|
Drives the app chrome (rails/header) and the public marketing pages.
|
||||||
The `.dark` class on <html> flips these to the dark values below.
|
The `.dark` class on <html> flips these to the dark values below.
|
||||||
Text over generated images keeps a literal text-white (handled in markup). */
|
Text over generated images keeps a literal text-white (handled in markup). */
|
||||||
--app-bg: rgb(250 250 251); /* page background */
|
--app-bg: #f8f6f2; /* warm paper */
|
||||||
--fg: rgb(15 23 42); /* primary heading/text */
|
--fg: #1a1a1a; /* primary heading/text */
|
||||||
--fg-2: rgb(15 23 42 / 0.62); /* body / secondary */
|
--fg-2: rgb(26 26 26 / 0.64); /* body / secondary */
|
||||||
--fg-3: rgb(15 23 42 / 0.45); /* muted */
|
--fg-3: rgb(26 26 26 / 0.48); /* muted */
|
||||||
--fg-faint: rgb(15 23 42 / 0.3); /* labels / hints */
|
--fg-faint: rgb(26 26 26 / 0.34); /* labels / hints */
|
||||||
--surface: rgb(255 255 255 / 0.7); /* raised card on page bg */
|
--surface: #ffffff; /* raised card on page bg */
|
||||||
--surface-2: rgb(15 23 42 / 0.035); /* subtle inset panel (counter cells) */
|
--surface-2: rgb(26 26 26 / 0.045); /* subtle inset panel (counter cells) */
|
||||||
--hairline: rgb(15 23 42 / 0.08); /* borders / rings */
|
--hairline: rgb(26 26 26 / 0.12); /* faint borders / separators */
|
||||||
--hover: rgb(15 23 42 / 0.05); /* hover wash */
|
--hover: rgb(254 240 138 / 0.55); /* hover wash — memphis yellow tint */
|
||||||
|
/* ink = the heavy outline color; shadow-ink = the offset "chunky" shadow */
|
||||||
|
--ink: #111111;
|
||||||
|
--ink-soft: rgb(17 17 17 / 0.45);
|
||||||
|
--shadow-ink: #111111;
|
||||||
|
/* memphis accent crayons */
|
||||||
|
--m-orange: #ec5b13;
|
||||||
|
--m-yellow: #fef08a;
|
||||||
|
--m-pink: #ffc1cc;
|
||||||
|
--m-blue: #bae6fd;
|
||||||
|
--m-mint: #a7f3d0;
|
||||||
/* solid CTA button that sits on the page bg — inverts per theme */
|
/* solid CTA button that sits on the page bg — inverts per theme */
|
||||||
--btn-solid-bg: rgb(15 23 42);
|
--btn-solid-bg: #1a1a1a;
|
||||||
--btn-solid-bg-h: rgb(30 41 59);
|
--btn-solid-bg-h: #000000;
|
||||||
--btn-solid-fg: #fff;
|
--btn-solid-fg: #fff;
|
||||||
--menu-bg: #ffffff; /* dropdown / popover panel */
|
--menu-bg: #ffffff; /* dropdown / popover panel */
|
||||||
}
|
}
|
||||||
|
|
||||||
html.dark {
|
html.dark {
|
||||||
--app-bg: #08090d;
|
--app-bg: #131318;
|
||||||
--fg: rgb(255 255 255 / 0.92);
|
--fg: rgb(255 255 255 / 0.92);
|
||||||
--fg-2: rgb(255 255 255 / 0.55);
|
--fg-2: rgb(255 255 255 / 0.55);
|
||||||
--fg-3: rgb(255 255 255 / 0.4);
|
--fg-3: rgb(255 255 255 / 0.4);
|
||||||
--fg-faint: rgb(255 255 255 / 0.3);
|
--fg-faint: rgb(255 255 255 / 0.3);
|
||||||
--surface: rgb(255 255 255 / 0.03);
|
--surface: rgb(255 255 255 / 0.05);
|
||||||
--surface-2: rgb(255 255 255 / 0.04);
|
--surface-2: rgb(255 255 255 / 0.05);
|
||||||
--hairline: rgb(255 255 255 / 0.06);
|
--hairline: rgb(255 255 255 / 0.1);
|
||||||
--hover: rgb(255 255 255 / 0.06);
|
--hover: rgb(236 91 19 / 0.2);
|
||||||
--btn-solid-bg: #fff;
|
--ink: rgb(244 244 245 / 0.85);
|
||||||
--btn-solid-bg-h: rgb(255 255 255 / 0.9);
|
--ink-soft: rgb(244 244 245 / 0.4);
|
||||||
--btn-solid-fg: rgb(2 6 23);
|
--shadow-ink: rgb(244 244 245 / 0.22);
|
||||||
--menu-bg: #15171f;
|
--btn-solid-bg: #ec5b13;
|
||||||
|
--btn-solid-bg-h: #f97316;
|
||||||
|
--btn-solid-fg: #fff;
|
||||||
|
--menu-bg: #1d1e26;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Smooth the flip so toggling doesn't hard-cut. */
|
/* Smooth the flip so toggling doesn't hard-cut. */
|
||||||
body, .theme-x { transition: background-color 0.25s ease, color 0.25s ease; }
|
body, .theme-x { transition: background-color 0.25s ease, color 0.25s ease; }
|
||||||
|
|
||||||
/* Footer CTA band — soft tinted panel that flips with the theme. */
|
/* Footer CTA band — flat memphis-yellow panel with a heavy ink outline. */
|
||||||
.cta-band {
|
.cta-band {
|
||||||
background:
|
background:
|
||||||
radial-gradient(at 30% 20%, rgb(168 85 247 / 0.16) 0%, transparent 50%),
|
radial-gradient(circle, rgb(17 17 17 / 0.1) 1.3px, transparent 1.3px) 0 0 / 20px 20px,
|
||||||
radial-gradient(at 80% 70%, rgb(244 114 182 / 0.14) 0%, transparent 55%),
|
var(--m-yellow);
|
||||||
linear-gradient(180deg, rgb(255 255 255), rgb(246 244 251));
|
border: 3px solid var(--ink);
|
||||||
|
box-shadow: 10px 10px 0 var(--shadow-ink);
|
||||||
}
|
}
|
||||||
html.dark .cta-band {
|
html.dark .cta-band {
|
||||||
background:
|
background:
|
||||||
radial-gradient(at 30% 20%, rgb(168 85 247 / 0.35) 0%, transparent 50%),
|
radial-gradient(circle, rgb(255 255 255 / 0.1) 1.3px, transparent 1.3px) 0 0 / 20px 20px,
|
||||||
radial-gradient(at 80% 70%, rgb(244 114 182 / 0.3) 0%, transparent 55%),
|
rgb(236 91 19 / 0.16);
|
||||||
linear-gradient(180deg, #0f1117, #080a10);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Light-mode rescue for hardcoded-dark pages =====
|
/* ===== Light-mode rescue for hardcoded-dark pages =====
|
||||||
@@ -88,9 +101,9 @@ html:not(.dark) .theme-text :is([class*="bg-slate-900"], [class*="bg-slate-800"]
|
|||||||
shell and washes out on white. In LIGHT mode, darken to the -600 tone. The
|
shell and washes out on white. In LIGHT mode, darken to the -600 tone. The
|
||||||
matcher also catches the /80 opacity variants. Badges keep their pale tinted
|
matcher also catches the /80 opacity variants. Badges keep their pale tinted
|
||||||
bg (bg-*-500/10) which reads fine on white. */
|
bg (bg-*-500/10) which reads fine on white. */
|
||||||
html:not(.dark) .theme-text :is([class*="text-indigo-300"], [class*="text-indigo-200"]) { color: rgb(79 70 229); }
|
html:not(.dark) .theme-text :is([class*="text-sky-300"], [class*="text-sky-200"]) { color: rgb(79 70 229); }
|
||||||
html:not(.dark) .theme-text :is([class*="text-violet-300"], [class*="text-violet-200"]) { color: rgb(124 58 237); }
|
html:not(.dark) .theme-text :is([class*="text-orange-300"], [class*="text-orange-200"]) { color: rgb(234 88 12); }
|
||||||
html:not(.dark) .theme-text :is([class*="text-fuchsia-300"], [class*="text-fuchsia-200"]) { color: rgb(192 38 211); }
|
html:not(.dark) .theme-text :is([class*="text-rose-300"], [class*="text-rose-200"]) { color: rgb(225 29 72); }
|
||||||
html:not(.dark) .theme-text :is([class*="text-emerald-300"], [class*="text-emerald-200"]) { color: rgb(5 150 105); }
|
html:not(.dark) .theme-text :is([class*="text-emerald-300"], [class*="text-emerald-200"]) { color: rgb(5 150 105); }
|
||||||
html:not(.dark) .theme-text :is([class*="text-sky-300"], [class*="text-sky-200"]) { color: rgb(2 132 199); }
|
html:not(.dark) .theme-text :is([class*="text-sky-300"], [class*="text-sky-200"]) { color: rgb(2 132 199); }
|
||||||
html:not(.dark) .theme-text :is([class*="text-amber-300"], [class*="text-amber-200"]) { color: rgb(217 119 6); }
|
html:not(.dark) .theme-text :is([class*="text-amber-300"], [class*="text-amber-200"]) { color: rgb(217 119 6); }
|
||||||
@@ -107,9 +120,9 @@ html:not(.dark) .theme-text :is(.fp, .pg, .act, .filter-pill, .kind-btn, .preset
|
|||||||
html:not(.dark) .theme-text :is(.fp, .pg, .act, .filter-pill, .kind-btn, .preset-btn, .opt, .chip-x):hover {
|
html:not(.dark) .theme-text :is(.fp, .pg, .act, .filter-pill, .kind-btn, .preset-btn, .opt, .chip-x):hover {
|
||||||
color: var(--fg); background: var(--hover);
|
color: var(--fg); background: var(--hover);
|
||||||
}
|
}
|
||||||
html:not(.dark) .theme-text :is(.fp-on, .pg-on, .seg-on, .opt-on) { background: rgb(15 23 42); color: #fff; box-shadow: none; }
|
html:not(.dark) .theme-text :is(.fp-on, .pg-on, .seg-on, .opt-on) { background: var(--fg); color: #fff; box-shadow: 2px 2px 0 var(--m-orange); }
|
||||||
/* Tag chips (TagInput) — near-white violet text is unreadable on white; keep
|
/* Tag chips (TagInput) — near-white orange text is unreadable on white; keep
|
||||||
the violet tint but switch the label to dark ink. */
|
the orange tint but switch the label to dark ink. */
|
||||||
html:not(.dark) .theme-text .chip { color: rgb(15 23 42 / 0.9); }
|
html:not(.dark) .theme-text .chip { color: rgb(15 23 42 / 0.9); }
|
||||||
html:not(.dark) .theme-text .chip .chip-x { color: rgb(15 23 42 / 0.55); background: transparent; box-shadow: none; }
|
html:not(.dark) .theme-text .chip .chip-x { color: rgb(15 23 42 / 0.55); background: transparent; box-shadow: none; }
|
||||||
html:not(.dark) .theme-text .chip .chip-x:hover { color: #fff; background: rgb(244 63 94 / 0.6); }
|
html:not(.dark) .theme-text .chip .chip-x:hover { color: #fff; background: rgb(244 63 94 / 0.6); }
|
||||||
@@ -117,17 +130,16 @@ html:not(.dark) .theme-text .chip .chip-x:hover { color: #fff; background: rgb(2
|
|||||||
`.filter-pill`/`.kind-btn` (not the `-on` class above). Without this, the
|
`.filter-pill`/`.kind-btn` (not the `-on` class above). Without this, the
|
||||||
neutral `.filter-pill`/`.kind-btn` rescue paints selected == unselected and
|
neutral `.filter-pill`/`.kind-btn` rescue paints selected == unselected and
|
||||||
the highlight vanishes on a white page. Equal-or-higher specificity, later. */
|
the highlight vanishes on a white page. Equal-or-higher specificity, later. */
|
||||||
html:not(.dark) .theme-text :is(.filter-pill, .kind-btn).on { background: rgb(15 23 42); color: #fff; box-shadow: none; }
|
html:not(.dark) .theme-text :is(.filter-pill, .kind-btn).on { background: var(--fg); color: #fff; box-shadow: 2px 2px 0 var(--m-orange); }
|
||||||
/* Selected COLORED filter pills — their scoped (dark) selected colors get
|
/* Selected COLORED filter pills — their scoped (dark) selected colors get
|
||||||
overridden by the neutral `.fp` rescue above, so re-state a light-mode
|
overridden by the neutral `.fp` rescue above, so re-state a light-mode
|
||||||
variant (tinted bg + dark-enough text + colored ring) with equal specificity
|
variant (tinted bg + dark-enough text + colored ring) with equal specificity
|
||||||
placed later, so the selection reads on a white page. */
|
placed later, so the selection reads on a white page. */
|
||||||
html:not(.dark) .theme-text .fp-emerald { background: rgb(16 185 129 / 0.14); color: rgb(4 120 87); box-shadow: inset 0 0 0 1px rgb(16 185 129 / 0.45); }
|
html:not(.dark) .theme-text .fp-emerald { background: rgb(16 185 129 / 0.14); color: rgb(4 120 87); box-shadow: inset 0 0 0 1px rgb(16 185 129 / 0.45); }
|
||||||
html:not(.dark) .theme-text .fp-rose { background: rgb(244 63 94 / 0.12); color: rgb(190 18 60); box-shadow: inset 0 0 0 1px rgb(244 63 94 / 0.45); }
|
html:not(.dark) .theme-text .fp-rose { background: rgb(244 63 94 / 0.12); color: rgb(190 18 60); box-shadow: inset 0 0 0 1px rgb(244 63 94 / 0.45); }
|
||||||
html:not(.dark) .theme-text .fp-fuchsia { background: rgb(217 70 239 / 0.12); color: rgb(162 28 175); box-shadow: inset 0 0 0 1px rgb(217 70 239 / 0.45); }
|
|
||||||
html:not(.dark) .theme-text .fp-amber { background: rgb(245 158 11 / 0.16); color: rgb(180 83 9); box-shadow: inset 0 0 0 1px rgb(245 158 11 / 0.5); }
|
html:not(.dark) .theme-text .fp-amber { background: rgb(245 158 11 / 0.16); color: rgb(180 83 9); box-shadow: inset 0 0 0 1px rgb(245 158 11 / 0.5); }
|
||||||
html:not(.dark) .theme-text .fp-violet { background: rgb(139 92 246 / 0.14); color: rgb(109 40 217); box-shadow: inset 0 0 0 1px rgb(139 92 246 / 0.45); }
|
html:not(.dark) .theme-text .fp-orange { background: rgb(249 115 22 / 0.14); color: rgb(194 65 12); box-shadow: inset 0 0 0 1px rgb(249 115 22 / 0.45); }
|
||||||
html:not(.dark) .theme-text .fp-purple { background: rgb(168 85 247 / 0.14); color: rgb(107 33 168); box-shadow: inset 0 0 0 1px rgb(168 85 247 / 0.45); }
|
html:not(.dark) .theme-text .fp-pink { background: rgb(244 114 182 / 0.14); color: rgb(190 24 93); box-shadow: inset 0 0 0 1px rgb(244 114 182 / 0.45); }
|
||||||
html:not(.dark) .theme-text .fp-slate { background: rgb(100 116 139 / 0.14); color: rgb(51 65 85); box-shadow: inset 0 0 0 1px rgb(100 116 139 / 0.45); }
|
html:not(.dark) .theme-text .fp-slate { background: rgb(100 116 139 / 0.14); color: rgb(51 65 85); box-shadow: inset 0 0 0 1px rgb(100 116 139 / 0.45); }
|
||||||
html:not(.dark) .theme-text .fp-sky { background: rgb(14 165 233 / 0.14); color: rgb(3 105 161); box-shadow: inset 0 0 0 1px rgb(14 165 233 / 0.45); }
|
html:not(.dark) .theme-text .fp-sky { background: rgb(14 165 233 / 0.14); color: rgb(3 105 161); box-shadow: inset 0 0 0 1px rgb(14 165 233 / 0.45); }
|
||||||
html:not(.dark) .theme-text .fp-teal { background: rgb(20 184 166 / 0.14); color: rgb(15 118 110); box-shadow: inset 0 0 0 1px rgb(20 184 166 / 0.45); }
|
html:not(.dark) .theme-text .fp-teal { background: rgb(20 184 166 / 0.14); color: rgb(15 118 110); box-shadow: inset 0 0 0 1px rgb(20 184 166 / 0.45); }
|
||||||
@@ -135,7 +147,7 @@ html:not(.dark) .theme-text .fp-teal { background: rgb(20 184 166 / 0.14); color
|
|||||||
/* Custom checkbox for admin tables (<input type="checkbox" class="chk">).
|
/* Custom checkbox for admin tables (<input type="checkbox" class="chk">).
|
||||||
Replaces the plain accent-color box with a rounded, branded one + white check.
|
Replaces the plain accent-color box with a rounded, branded one + white check.
|
||||||
Neutral border + transparent bg so it reads on both the dark admin shell and
|
Neutral border + transparent bg so it reads on both the dark admin shell and
|
||||||
light mode; fills fuchsia when checked. */
|
light mode; fills rose when checked. */
|
||||||
.chk {
|
.chk {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
@@ -151,10 +163,12 @@ html:not(.dark) .theme-text .fp-teal { background: rgb(20 184 166 / 0.14); color
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
transition: background 0.15s, border-color 0.15s;
|
transition: background 0.15s, border-color 0.15s;
|
||||||
}
|
}
|
||||||
.chk:hover:not(:checked) { border-color: rgb(217 70 239 / 0.7); }
|
.chk { border-color: var(--ink-soft); }
|
||||||
|
.chk:hover:not(:checked) { border-color: var(--m-orange); }
|
||||||
.chk:checked {
|
.chk:checked {
|
||||||
background: rgb(217 70 239);
|
background: var(--m-orange);
|
||||||
border-color: rgb(217 70 239);
|
border-color: var(--ink);
|
||||||
|
box-shadow: 2px 2px 0 var(--shadow-ink);
|
||||||
}
|
}
|
||||||
.chk:checked::after {
|
.chk:checked::after {
|
||||||
content: "";
|
content: "";
|
||||||
@@ -165,7 +179,7 @@ html:not(.dark) .theme-text .fp-teal { background: rgb(20 184 166 / 0.14); color
|
|||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
margin-top: -0.12rem;
|
margin-top: -0.12rem;
|
||||||
}
|
}
|
||||||
.chk:focus-visible { outline: 2px solid rgb(217 70 239 / 0.45); outline-offset: 1px; }
|
.chk:focus-visible { outline: 2px solid rgb(236 91 19 / 0.5); outline-offset: 1px; }
|
||||||
/* Account identity tags (AccountsView: 子号 / 母号 / 普号 / 限额). The colors live
|
/* Account identity tags (AccountsView: 子号 / 母号 / 普号 / 限额). The colors live
|
||||||
here rather than as inline dark-palette utilities, which were invisible on the
|
here rather than as inline dark-palette utilities, which were invisible on the
|
||||||
light theme: light values first, html.dark override after. */
|
light theme: light values first, html.dark override after. */
|
||||||
@@ -181,11 +195,11 @@ html:not(.dark) .theme-text .fp-teal { background: rgb(20 184 166 / 0.14); color
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.acct-tag-sub { background: rgb(14 165 233 / 0.14); color: rgb(3 105 161); box-shadow: inset 0 0 0 1px rgb(14 165 233 / 0.4); }
|
.acct-tag-sub { background: rgb(14 165 233 / 0.14); color: rgb(3 105 161); box-shadow: inset 0 0 0 1px rgb(14 165 233 / 0.4); }
|
||||||
.acct-tag-master { background: rgb(139 92 246 / 0.14); color: rgb(109 40 217); box-shadow: inset 0 0 0 1px rgb(139 92 246 / 0.4); }
|
.acct-tag-master { background: rgb(249 115 22 / 0.14); color: rgb(194 65 12); box-shadow: inset 0 0 0 1px rgb(249 115 22 / 0.4); }
|
||||||
.acct-tag-free { background: rgb(100 116 139 / 0.14); color: rgb(51 65 85); box-shadow: inset 0 0 0 1px rgb(100 116 139 / 0.4); }
|
.acct-tag-free { background: rgb(100 116 139 / 0.14); color: rgb(51 65 85); box-shadow: inset 0 0 0 1px rgb(100 116 139 / 0.4); }
|
||||||
.acct-tag-limit { background: rgb(245 158 11 / 0.16); color: rgb(180 83 9); box-shadow: inset 0 0 0 1px rgb(245 158 11 / 0.4); }
|
.acct-tag-limit { background: rgb(245 158 11 / 0.16); color: rgb(180 83 9); box-shadow: inset 0 0 0 1px rgb(245 158 11 / 0.4); }
|
||||||
html.dark .acct-tag-sub { background: rgb(14 165 233 / 0.18); color: rgb(125 211 252); box-shadow: inset 0 0 0 1px rgb(56 189 248 / 0.35); }
|
html.dark .acct-tag-sub { background: rgb(14 165 233 / 0.18); color: rgb(125 211 252); box-shadow: inset 0 0 0 1px rgb(56 189 248 / 0.35); }
|
||||||
html.dark .acct-tag-master { background: rgb(139 92 246 / 0.22); color: rgb(196 181 253); box-shadow: inset 0 0 0 1px rgb(167 139 250 / 0.35); }
|
html.dark .acct-tag-master { background: rgb(249 115 22 / 0.22); color: rgb(253 186 116); box-shadow: inset 0 0 0 1px rgb(253 186 116 / 0.35); }
|
||||||
html.dark .acct-tag-free { background: rgb(148 163 184 / 0.22); color: rgb(226 232 240); box-shadow: inset 0 0 0 1px rgb(148 163 184 / 0.35); }
|
html.dark .acct-tag-free { background: rgb(148 163 184 / 0.22); color: rgb(226 232 240); box-shadow: inset 0 0 0 1px rgb(148 163 184 / 0.35); }
|
||||||
html.dark .acct-tag-limit { background: rgb(245 158 11 / 0.18); color: rgb(252 211 77); box-shadow: inset 0 0 0 1px rgb(251 191 36 / 0.35); }
|
html.dark .acct-tag-limit { background: rgb(245 158 11 / 0.18); color: rgb(252 211 77); box-shadow: inset 0 0 0 1px rgb(251 191 36 / 0.35); }
|
||||||
/* Capability chips (ModelsView): all variants need bg + ring + color reset in light mode.
|
/* Capability chips (ModelsView): all variants need bg + ring + color reset in light mode.
|
||||||
@@ -240,9 +254,9 @@ html:not(.dark) .media-card :is([class*="border-white/"], [class*="ring-white/"]
|
|||||||
/* Accent text inside a media-card sits on dark imagery too — keep it at its
|
/* Accent text inside a media-card sits on dark imagery too — keep it at its
|
||||||
bright (dark-mode) shade instead of the darkened light-theme rescue above. */
|
bright (dark-mode) shade instead of the darkened light-theme rescue above. */
|
||||||
html:not(.dark) .media-card :is([class*="text-emerald-3"], [class*="text-emerald-2"]) { color: rgb(110 231 183) !important; }
|
html:not(.dark) .media-card :is([class*="text-emerald-3"], [class*="text-emerald-2"]) { color: rgb(110 231 183) !important; }
|
||||||
html:not(.dark) .media-card :is([class*="text-fuchsia-3"], [class*="text-fuchsia-2"]) { color: rgb(240 171 252) !important; }
|
html:not(.dark) .media-card :is([class*="text-rose-3"], [class*="text-rose-2"]) { color: rgb(251 207 232) !important; }
|
||||||
html:not(.dark) .media-card :is([class*="text-indigo-3"], [class*="text-indigo-2"]) { color: rgb(165 180 252) !important; }
|
html:not(.dark) .media-card :is([class*="text-sky-3"], [class*="text-sky-2"]) { color: rgb(147 197 253) !important; }
|
||||||
html:not(.dark) .media-card :is([class*="text-violet-3"], [class*="text-violet-2"]) { color: rgb(196 181 253) !important; }
|
html:not(.dark) .media-card :is([class*="text-orange-3"], [class*="text-orange-2"]) { color: rgb(253 186 116) !important; }
|
||||||
html:not(.dark) .media-card :is([class*="text-sky-3"], [class*="text-sky-2"]) { color: rgb(125 211 252) !important; }
|
html:not(.dark) .media-card :is([class*="text-sky-3"], [class*="text-sky-2"]) { color: rgb(125 211 252) !important; }
|
||||||
html:not(.dark) .media-card :is([class*="text-amber-3"], [class*="text-amber-2"]) { color: rgb(252 211 77) !important; }
|
html:not(.dark) .media-card :is([class*="text-amber-3"], [class*="text-amber-2"]) { color: rgb(252 211 77) !important; }
|
||||||
|
|
||||||
@@ -270,7 +284,7 @@ body {
|
|||||||
/* Subtle ambient gradient + grain (used by PublicLayout via .ambient-bg) */
|
/* Subtle ambient gradient + grain (used by PublicLayout via .ambient-bg) */
|
||||||
}
|
}
|
||||||
|
|
||||||
::selection { background: rgb(99 102 241 / 0.22); color: rgb(15 23 42); }
|
::selection { background: var(--m-yellow); color: #111; }
|
||||||
|
|
||||||
/* Tailwind v4 / preflight leaves <button> on the default arrow cursor, so
|
/* Tailwind v4 / preflight leaves <button> on the default arrow cursor, so
|
||||||
admins miss the affordance on icon-only actions (复制 / 删除 / 生成 等).
|
admins miss the affordance on icon-only actions (复制 / 删除 / 生成 等).
|
||||||
@@ -285,31 +299,37 @@ button:not(:disabled), [role="button"]:not([aria-disabled="true"]) { cursor: poi
|
|||||||
own utility so vertical clipping / rounded corners are preserved. */
|
own utility so vertical clipping / rounded corners are preserved. */
|
||||||
.card:has(> table) { overflow-x: auto; }
|
.card:has(> table) { overflow-x: auto; }
|
||||||
|
|
||||||
::-webkit-scrollbar { width: 8px; height: 8px; }
|
::-webkit-scrollbar { width: 9px; height: 9px; }
|
||||||
::-webkit-scrollbar-thumb { background: rgb(203 213 225); border-radius: 4px; }
|
::-webkit-scrollbar-thumb { background: rgb(26 26 26 / 0.28); border-radius: 5px; border: 2px solid transparent; background-clip: content-box; }
|
||||||
::-webkit-scrollbar-thumb:hover { background: rgb(148 163 184); }
|
::-webkit-scrollbar-thumb:hover { background: rgb(26 26 26 / 0.45); background-clip: content-box; }
|
||||||
::-webkit-scrollbar-track { background: transparent; }
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
html.dark ::-webkit-scrollbar-thumb { background: rgb(255 255 255 / 0.22); background-clip: content-box; }
|
||||||
|
html.dark ::-webkit-scrollbar-thumb:hover { background: rgb(255 255 255 / 0.35); background-clip: content-box; }
|
||||||
|
|
||||||
/* ===== Ambient background — used by the public shell ===== */
|
/* ===== Ambient background — used by the public shell ===== */
|
||||||
.ambient-bg {
|
.ambient-bg {
|
||||||
background:
|
background-color: var(--app-bg);
|
||||||
radial-gradient(at 15% 10%, rgb(238 242 255) 0%, transparent 45%),
|
background-image: radial-gradient(circle, rgb(17 17 17 / 0.07) 1.3px, transparent 1.3px);
|
||||||
radial-gradient(at 85% 25%, rgb(253 244 255) 0%, transparent 50%),
|
background-size: 22px 22px;
|
||||||
radial-gradient(at 50% 90%, rgb(236 254 255) 0%, transparent 50%),
|
}
|
||||||
rgb(250 250 251);
|
html.dark .ambient-bg {
|
||||||
|
background-image: radial-gradient(circle, rgb(255 255 255 / 0.06) 1.3px, transparent 1.3px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 二次元 (anime) design kit =====
|
/* ===== Memphis / neo-brutalist design kit =====
|
||||||
Soft pink/violet/sky gradients, rounded bubbly surfaces, sparkle overlays and
|
Heavy ink outlines, hard offset shadows, flat crayon accents (orange / yellow
|
||||||
sticker labels. Reusable across the workbench + user subpages + public pages
|
/ pink / blue / mint), rotated stickers and squishy press feedback. Reusable
|
||||||
so the whole front-end shares one cute, neon-tinged look. */
|
across the workbench + user subpages + public pages so the whole front-end
|
||||||
|
shares one punchy, print-like look. */
|
||||||
|
|
||||||
/* Gradient text used for eyebrows / accents. */
|
/* Highlighter text — black words "marked" with a yellow swipe. */
|
||||||
.anime-grad-text {
|
.anime-grad-text {
|
||||||
background: linear-gradient(100deg, #f472b6, #a855f7 46%, #38bdf8);
|
background: linear-gradient(transparent 55%, var(--m-yellow) 55%);
|
||||||
-webkit-background-clip: text;
|
padding: 0 0.1em;
|
||||||
background-clip: text;
|
color: inherit;
|
||||||
color: transparent;
|
}
|
||||||
|
html.dark .anime-grad-text {
|
||||||
|
background: linear-gradient(transparent 55%, rgb(236 91 19 / 0.55) 55%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Section eyebrow — tiny uppercase kicker with a leading star. */
|
/* Section eyebrow — tiny uppercase kicker with a leading star. */
|
||||||
@@ -318,71 +338,74 @@ button:not(:disabled), [role="button"]:not([aria-disabled="true"]) { cursor: poi
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
letter-spacing: 0.22em;
|
letter-spacing: 0.22em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
.anime-eyebrow::before { content: '✦'; font-size: 0.85em; }
|
.anime-eyebrow::before { content: '✦'; font-size: 0.85em; }
|
||||||
|
|
||||||
/* Sticker / kawaii pill label. */
|
/* Sticker label — rotated yellow chip with a hard shadow. */
|
||||||
.sticker {
|
.sticker {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.3rem;
|
gap: 0.3rem;
|
||||||
padding: 0.28rem 0.7rem;
|
padding: 0.3rem 0.75rem;
|
||||||
border-radius: 999px;
|
border-radius: 0.55rem;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
color: #a21caf;
|
color: #111;
|
||||||
background: linear-gradient(135deg, rgb(244 114 182 / 0.16), rgb(129 140 248 / 0.16));
|
background: var(--m-yellow);
|
||||||
box-shadow: inset 0 0 0 1px rgb(217 70 239 / 0.28);
|
border: 2.5px solid var(--ink);
|
||||||
}
|
box-shadow: 4px 4px 0 var(--shadow-ink);
|
||||||
html.dark .sticker {
|
transform: rotate(-1.5deg);
|
||||||
color: #f5d0fe;
|
|
||||||
background: linear-gradient(135deg, rgb(244 114 182 / 0.2), rgb(129 140 248 / 0.2));
|
|
||||||
box-shadow: inset 0 0 0 1px rgb(217 70 239 / 0.35), 0 0 18px -6px rgb(217 70 239 / 0.5);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bubbly gradient CTA — pill button with a squishy hover. */
|
/* Chunky CTA — flat orange with an offset ink shadow and a press feel. */
|
||||||
.bubble-btn {
|
.bubble-btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 0.7rem 1.4rem;
|
padding: 0.7rem 1.4rem;
|
||||||
border-radius: 999px;
|
border-radius: 0.8rem;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: none;
|
border: 2.5px solid var(--ink);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: linear-gradient(135deg, #f472b6, #a855f7 55%, #38bdf8);
|
background: var(--m-orange);
|
||||||
box-shadow: 0 14px 30px -12px rgb(168 85 247 / 0.7);
|
box-shadow: 4px 4px 0 var(--shadow-ink);
|
||||||
transition: transform 0.15s ease, filter 0.15s ease, box-shadow 0.15s ease;
|
transition: transform 0.15s ease, box-shadow 0.15s ease, filter 0.15s ease;
|
||||||
}
|
}
|
||||||
.bubble-btn:hover { transform: translateY(-2px) scale(1.02); filter: brightness(1.06); }
|
.bubble-btn:hover { transform: translate(-2px, -2px); box-shadow: 6px 6px 0 var(--shadow-ink); filter: brightness(1.05); }
|
||||||
.bubble-btn:active { transform: translateY(0) scale(0.99); }
|
.bubble-btn:active { transform: scale(0.97) translateY(2px); box-shadow: 1px 1px 0 var(--shadow-ink); }
|
||||||
|
|
||||||
/* Rounded anime card surface with a soft pastel wash + neon ring in dark. */
|
/* Memphis card — flat surface, heavy ink outline, hard offset shadow. */
|
||||||
.anime-card {
|
.anime-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 1.5rem;
|
border-radius: 1rem;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
box-shadow: inset 0 0 0 1px var(--hairline);
|
border: 2.5px solid var(--ink);
|
||||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
box-shadow: 5px 5px 0 var(--shadow-ink);
|
||||||
|
transition: transform 0.18s ease, box-shadow 0.18s ease;
|
||||||
}
|
}
|
||||||
.anime-card:hover {
|
.anime-card:hover {
|
||||||
transform: translateY(-3px);
|
transform: translate(-2px, -2px);
|
||||||
box-shadow: inset 0 0 0 1px rgb(217 70 239 / 0.4), 0 22px 44px -26px rgb(168 85 247 / 0.5);
|
box-shadow: 8px 8px 0 var(--shadow-ink);
|
||||||
}
|
}
|
||||||
html.dark .anime-card:hover {
|
html.dark .anime-card {
|
||||||
box-shadow: inset 0 0 0 1px rgb(217 70 239 / 0.5), 0 0 30px -10px rgb(217 70 239 / 0.45);
|
background: rgb(255 255 255 / 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Selected pill — replaces the old bg-slate-900 look with a candy gradient. */
|
/* Selected pill — solid ink with an orange offset shadow. */
|
||||||
.chip-on {
|
.chip-on {
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
background: linear-gradient(135deg, #e879f9, #8b5cf6) !important;
|
background: #1a1a1a !important;
|
||||||
box-shadow: 0 8px 20px -10px rgb(168 85 247 / 0.7) !important;
|
border: 2px solid var(--ink) !important;
|
||||||
|
box-shadow: 3px 3px 0 var(--m-orange) !important;
|
||||||
|
}
|
||||||
|
html.dark .chip-on {
|
||||||
|
background: var(--m-orange) !important;
|
||||||
|
box-shadow: 3px 3px 0 var(--shadow-ink) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Twinkling star field — drop into any relatively-positioned decorative layer.
|
/* Twinkling star field — drop into any relatively-positioned decorative layer.
|
||||||
@@ -392,9 +415,9 @@ html.dark .anime-card:hover {
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
background-image:
|
background-image:
|
||||||
radial-gradient(1.5px 1.5px at 12% 22%, rgb(217 70 239 / 0.7), transparent),
|
radial-gradient(1.5px 1.5px at 12% 22%, rgb(236 91 19 / 0.7), transparent),
|
||||||
radial-gradient(1.5px 1.5px at 78% 12%, rgb(56 189 248 / 0.7), transparent),
|
radial-gradient(1.5px 1.5px at 78% 12%, rgb(56 189 248 / 0.7), transparent),
|
||||||
radial-gradient(1px 1px at 42% 62%, rgb(168 85 247 / 0.65), transparent),
|
radial-gradient(1px 1px at 42% 62%, rgb(236 91 19 / 0.6), transparent),
|
||||||
radial-gradient(1.5px 1.5px at 88% 72%, rgb(244 114 182 / 0.6), transparent),
|
radial-gradient(1.5px 1.5px at 88% 72%, rgb(244 114 182 / 0.6), transparent),
|
||||||
radial-gradient(1px 1px at 24% 84%, rgb(56 189 248 / 0.55), transparent),
|
radial-gradient(1px 1px at 24% 84%, rgb(56 189 248 / 0.55), transparent),
|
||||||
radial-gradient(1px 1px at 62% 34%, rgb(250 204 21 / 0.55), transparent);
|
radial-gradient(1px 1px at 62% 34%, rgb(250 204 21 / 0.55), transparent);
|
||||||
@@ -422,46 +445,102 @@ html.dark .anime-card:hover {
|
|||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== Memphis utilities ===== */
|
||||||
|
/* Polka-dot texture layer (drop on a decorative absolutely-positioned div). */
|
||||||
|
.memphis-dots {
|
||||||
|
background-image: radial-gradient(circle, rgb(17 17 17 / 0.14) 1.4px, transparent 1.4px);
|
||||||
|
background-size: 22px 22px;
|
||||||
|
}
|
||||||
|
html.dark .memphis-dots {
|
||||||
|
background-image: radial-gradient(circle, rgb(255 255 255 / 0.1) 1.4px, transparent 1.4px);
|
||||||
|
}
|
||||||
|
/* Heavy outline + hard offset shadow helpers for one-off markup. */
|
||||||
|
.ink-card {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 2.5px solid var(--ink);
|
||||||
|
box-shadow: 6px 6px 0 var(--shadow-ink);
|
||||||
|
}
|
||||||
|
.ink-card-sm {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 2px solid var(--ink);
|
||||||
|
box-shadow: 4px 4px 0 var(--shadow-ink);
|
||||||
|
}
|
||||||
|
/* Lift on hover — the signature memphis card interaction. */
|
||||||
|
.chunky-hover { transition: transform 0.18s ease, box-shadow 0.18s ease; }
|
||||||
|
.chunky-hover:hover { transform: translate(-2px, -2px); box-shadow: 8px 8px 0 var(--shadow-ink); }
|
||||||
|
/* Squishy press for buttons (mirrors the reference site's .memphis-press). */
|
||||||
|
.memphis-press { transition: transform 0.15s ease; }
|
||||||
|
.memphis-press:active { transform: scale(0.97) translateY(2px); }
|
||||||
|
|
||||||
|
/* Empty-state badge — a rotated crayon tile with a star, replaces the old
|
||||||
|
mascot illustration in empty lists. */
|
||||||
|
.empty-badge {
|
||||||
|
width: 4.5rem;
|
||||||
|
height: 4.5rem;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #111;
|
||||||
|
background: var(--m-yellow);
|
||||||
|
border: 3px solid var(--ink);
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 5px 5px 0 var(--shadow-ink);
|
||||||
|
transform: rotate(-6deg);
|
||||||
|
animation: memphis-float 6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) { .empty-badge { animation: none; } }
|
||||||
|
|
||||||
|
@keyframes memphis-wiggle { 0%, 100% { transform: rotate(-3deg); } 50% { transform: rotate(3deg); } }
|
||||||
|
@keyframes memphis-float { 0%, 100% { transform: translateY(0) rotate(0deg); } 50% { transform: translateY(-10px) rotate(1.5deg); } }
|
||||||
|
@keyframes memphis-bounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } }
|
||||||
|
.animate-memphis-wiggle { animation: memphis-wiggle 2s ease-in-out infinite; }
|
||||||
|
.animate-memphis-float { animation: memphis-float 6s ease-in-out infinite; }
|
||||||
|
.animate-memphis-bounce { animation: memphis-bounce 2s ease-in-out infinite; }
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.animate-memphis-wiggle, .animate-memphis-float, .animate-memphis-bounce { animation: none; }
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== Dark public-shell theme overrides =====
|
/* ===== Dark public-shell theme overrides =====
|
||||||
Apply by adding `.public-dark` to a wrapper element. Views can keep their
|
Apply by adding `.public-dark` to a wrapper element. Views can keep their
|
||||||
existing Tailwind classes (text-slate-*, .card, .field) — these rules
|
existing Tailwind classes (text-slate-*, .card, .field) — these rules
|
||||||
remap them to the dark palette. */
|
remap them to the dark palette. */
|
||||||
.public-dark { color: rgb(255 255 255 / 0.9); }
|
.public-dark { color: rgb(255 255 255 / 0.9); }
|
||||||
.public-dark .card {
|
.public-dark .card {
|
||||||
background: rgb(255 255 255 / 0.03);
|
background: rgb(255 255 255 / 0.035);
|
||||||
border-color: rgb(255 255 255 / 0.07);
|
border-color: var(--ink);
|
||||||
color: rgb(255 255 255 / 0.92);
|
color: rgb(255 255 255 / 0.92);
|
||||||
box-shadow: 0 1px 0 rgb(255 255 255 / 0.04) inset, 0 8px 24px rgb(0 0 0 / 0.25);
|
box-shadow: 5px 5px 0 var(--shadow-ink);
|
||||||
}
|
}
|
||||||
.public-dark .field {
|
.public-dark .field {
|
||||||
background: rgb(255 255 255 / 0.04);
|
background: rgb(255 255 255 / 0.04);
|
||||||
border-color: rgb(255 255 255 / 0.1);
|
border-color: var(--ink-soft);
|
||||||
color: rgb(255 255 255 / 0.95);
|
color: rgb(255 255 255 / 0.95);
|
||||||
transition: border-color 0.18s, background 0.18s, box-shadow 0.18s;
|
transition: border-color 0.18s, background 0.18s, box-shadow 0.18s;
|
||||||
}
|
}
|
||||||
.public-dark .field::placeholder { color: rgb(255 255 255 / 0.3); }
|
.public-dark .field::placeholder { color: rgb(255 255 255 / 0.3); }
|
||||||
/* Single focus treatment shared by every <.field> across the admin shell —
|
/* Single focus treatment shared by every <.field> across the admin shell —
|
||||||
matches the per-component overrides in ConfigView/LoginModal so all
|
an orange offset shadow, matching the light theme's focus affordance. */
|
||||||
inputs glow with the same violet ring on focus. */
|
|
||||||
.public-dark .field:focus,
|
.public-dark .field:focus,
|
||||||
.public-dark .field:focus-visible {
|
.public-dark .field:focus-visible {
|
||||||
border-color: rgb(167 139 250 / 0.65);
|
border-color: var(--ink);
|
||||||
background: rgb(255 255 255 / 0.06);
|
background: rgb(255 255 255 / 0.06);
|
||||||
box-shadow: 0 0 0 3px rgb(167 139 250 / 0.15);
|
box-shadow: 3px 3px 0 rgb(236 91 19 / 0.6);
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
.public-dark .btn-soft {
|
.public-dark .btn-soft {
|
||||||
background: rgb(255 255 255 / 0.06);
|
background: rgb(255 255 255 / 0.06);
|
||||||
|
border-color: var(--ink-soft);
|
||||||
color: rgb(255 255 255 / 0.85);
|
color: rgb(255 255 255 / 0.85);
|
||||||
}
|
}
|
||||||
.public-dark .btn-soft:hover { background: rgb(255 255 255 / 0.12); color: white; }
|
.public-dark .btn-soft:hover { background: rgb(236 91 19 / 0.25); color: white; }
|
||||||
.public-dark .btn-ghost { color: rgb(255 255 255 / 0.65); }
|
.public-dark .btn-ghost { color: rgb(255 255 255 / 0.65); border-color: transparent; }
|
||||||
.public-dark .btn-ghost:hover { background: rgb(255 255 255 / 0.06); color: white; }
|
.public-dark .btn-ghost:hover { background: rgb(236 91 19 / 0.18); color: white; }
|
||||||
.public-dark .btn-primary {
|
.public-dark .btn-primary {
|
||||||
background: white; color: rgb(2 6 23); box-shadow: 0 1px 0 rgb(0 0 0 / 0.1);
|
background: var(--m-orange); color: #fff; box-shadow: 3px 3px 0 var(--shadow-ink);
|
||||||
}
|
}
|
||||||
.public-dark .btn-primary:hover { background: rgb(255 255 255 / 0.92); }
|
.public-dark .btn-primary:hover { background: #f97316; }
|
||||||
.public-dark .pill { background: rgb(255 255 255 / 0.06); color: rgb(255 255 255 / 0.85); }
|
.public-dark .pill { background: rgb(255 255 255 / 0.06); color: rgb(255 255 255 / 0.85); border-color: var(--ink-soft); }
|
||||||
|
|
||||||
/* Remap slate text tokens */
|
/* Remap slate text tokens */
|
||||||
.public-dark .text-slate-900,
|
.public-dark .text-slate-900,
|
||||||
@@ -511,34 +590,68 @@ html.dark .anime-card:hover {
|
|||||||
.public-dark .hover\:bg-slate-100:hover { background: rgb(255 255 255 / 0.06); }
|
.public-dark .hover\:bg-slate-100:hover { background: rgb(255 255 255 / 0.06); }
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
/* surfaces */
|
/* surfaces — flat white card with the memphis ink outline + offset shadow */
|
||||||
.card { @apply bg-white rounded-xl border border-slate-200 shadow-sm; }
|
.card {
|
||||||
|
@apply bg-white rounded-xl;
|
||||||
|
border: 2.5px solid var(--ink);
|
||||||
|
box-shadow: 5px 5px 0 var(--shadow-ink);
|
||||||
|
}
|
||||||
|
|
||||||
/* buttons — each variant is self-contained (Tailwind v4 can't @apply a custom class) */
|
/* buttons — each variant is self-contained (Tailwind v4 can't @apply a custom class) */
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
@apply inline-flex items-center justify-center gap-1.5 rounded-lg text-sm font-medium
|
@apply inline-flex items-center justify-center gap-1.5 rounded-lg text-sm font-bold
|
||||||
transition-colors disabled:opacity-50 disabled:cursor-not-allowed
|
disabled:opacity-50 disabled:cursor-not-allowed px-3.5 py-2;
|
||||||
bg-slate-900 text-white hover:bg-slate-700 px-3.5 py-2;
|
color: #fff;
|
||||||
|
background: var(--m-orange);
|
||||||
|
border: 2.5px solid var(--ink);
|
||||||
|
box-shadow: 3px 3px 0 var(--shadow-ink);
|
||||||
|
transition: transform 0.13s ease, box-shadow 0.13s ease, filter 0.13s ease;
|
||||||
}
|
}
|
||||||
|
.btn-primary:hover:not(:disabled) { transform: translate(-1px, -1px); box-shadow: 4px 4px 0 var(--shadow-ink); filter: brightness(1.05); }
|
||||||
|
.btn-primary:active:not(:disabled) { transform: scale(0.97) translateY(2px); box-shadow: 0 0 0 var(--shadow-ink); }
|
||||||
.btn-soft {
|
.btn-soft {
|
||||||
@apply inline-flex items-center justify-center gap-1.5 rounded-lg text-xs font-medium
|
@apply inline-flex items-center justify-center gap-1.5 rounded-lg text-xs font-semibold
|
||||||
transition-colors disabled:opacity-50 disabled:cursor-not-allowed
|
disabled:opacity-50 disabled:cursor-not-allowed px-3 py-1.5;
|
||||||
bg-slate-100 text-slate-700 hover:bg-slate-200 px-3 py-1.5;
|
color: var(--fg-2);
|
||||||
|
background: var(--surface);
|
||||||
|
border: 2px solid var(--ink-soft);
|
||||||
|
transition: transform 0.13s ease, box-shadow 0.13s ease, background 0.15s ease, color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
.btn-soft:hover:not(:disabled) {
|
||||||
|
color: var(--fg);
|
||||||
|
background: var(--m-yellow);
|
||||||
|
border-color: var(--ink);
|
||||||
|
box-shadow: 3px 3px 0 var(--shadow-ink);
|
||||||
|
}
|
||||||
|
.btn-soft:active:not(:disabled) { transform: scale(0.97) translateY(2px); box-shadow: none; }
|
||||||
.btn-ghost {
|
.btn-ghost {
|
||||||
@apply inline-flex items-center justify-center gap-1.5 rounded-lg text-xs font-medium
|
@apply inline-flex items-center justify-center gap-1.5 rounded-lg text-xs font-medium
|
||||||
transition-colors disabled:opacity-50 disabled:cursor-not-allowed
|
disabled:opacity-50 disabled:cursor-not-allowed px-3 py-1.5;
|
||||||
text-slate-600 hover:bg-slate-100 hover:text-slate-900 px-3 py-1.5;
|
color: var(--fg-2);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
||||||
|
}
|
||||||
|
.btn-ghost:hover:not(:disabled) {
|
||||||
|
color: var(--fg);
|
||||||
|
background: var(--m-yellow);
|
||||||
|
border-color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* text link / quiet action */
|
/* text link / quiet action */
|
||||||
.link { @apply text-slate-500 hover:text-slate-900 transition-colors cursor-pointer; }
|
.link { @apply hover:text-slate-900 transition-colors cursor-pointer; color: var(--fg-3); }
|
||||||
|
.link:hover { color: var(--m-orange); }
|
||||||
|
|
||||||
/* form controls */
|
/* form controls — heavy outline, orange offset shadow on focus */
|
||||||
.field {
|
.field {
|
||||||
@apply w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-800
|
@apply w-full rounded-lg bg-white px-3 py-2 text-sm
|
||||||
outline-none transition-colors placeholder:text-slate-400
|
outline-none placeholder:text-slate-400;
|
||||||
focus:border-slate-400 focus:ring-2;
|
color: var(--fg);
|
||||||
|
border: 2px solid var(--ink-soft);
|
||||||
|
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
}
|
||||||
|
.field:focus, .field:focus-visible {
|
||||||
|
border-color: var(--ink);
|
||||||
|
box-shadow: 3px 3px 0 var(--m-orange);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Native <select> using .field: drop the OS arrow for a custom chevron that
|
/* Native <select> using .field: drop the OS arrow for a custom chevron that
|
||||||
@@ -565,13 +678,17 @@ html.dark .anime-card:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* status pill */
|
/* status pill */
|
||||||
.pill { @apply inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium; }
|
.pill {
|
||||||
|
@apply inline-flex items-center rounded-md px-2 py-0.5 text-xs font-semibold;
|
||||||
|
border: 1.5px solid var(--ink-soft);
|
||||||
|
}
|
||||||
|
|
||||||
/* table primitives */
|
/* table primitives */
|
||||||
.th { @apply text-left px-4 py-3 text-[11px] font-semibold uppercase tracking-wider text-slate-400; }
|
.th { @apply text-left px-4 py-3 text-[11px] font-bold uppercase tracking-wider; color: var(--fg-faint); }
|
||||||
.td { @apply px-4 py-3.5 align-middle; }
|
.td { @apply px-4 py-3.5 align-middle; }
|
||||||
.row { @apply border-t border-slate-100 transition-colors hover:bg-slate-50; }
|
.row { @apply border-t transition-colors; border-color: var(--hairline); }
|
||||||
|
.row:hover { background: rgb(254 240 138 / 0.35); }
|
||||||
|
|
||||||
/* avatar chip for identity columns */
|
/* avatar chip for identity columns */
|
||||||
.avatar { @apply w-8 h-8 rounded-full grid place-items-center text-xs font-semibold shrink-0 ring-1 ring-black/5; }
|
.avatar { @apply w-8 h-8 rounded-full grid place-items-center text-xs font-bold shrink-0; border: 2px solid var(--ink); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +1,63 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { site } from '../site'
|
import { site } from '../site'
|
||||||
import Mascot from '../components/Mascot.vue'
|
|
||||||
|
|
||||||
const contact = computed(() => site.contact || {})
|
const contact = computed(() => site.contact || {})
|
||||||
|
|
||||||
const powers = [
|
const powers = [
|
||||||
{ icon: '🎨', title: '文字生图', desc: '一句话出图,画幅与分辨率随模型可选。', tint: 'from-fuchsia-400/20 to-pink-400/10' },
|
{ icon: '🎨', title: '文字生图', desc: '一句话出图,画幅与分辨率随模型可选。', bg: 'var(--m-mint)' },
|
||||||
{ icon: '🖼️', title: '参考图生图', desc: '上传参考换风格、改构图、做变体。', tint: 'from-violet-400/20 to-indigo-400/10' },
|
{ icon: '🖼️', title: '参考图生图', desc: '上传参考换风格、改构图、做变体。', bg: 'var(--m-pink)' },
|
||||||
{ icon: '🎬', title: '文字生视频', desc: '一段描述生成短片,时长分辨率可选。', tint: 'from-sky-400/20 to-cyan-400/10' },
|
{ icon: '🎬', title: '文字生视频', desc: '一段描述生成短片,时长分辨率可选。', bg: 'var(--m-blue)' },
|
||||||
{ icon: '✨', title: '图生视频', desc: '给张参考图,让静态画面动起来。', tint: 'from-amber-400/20 to-orange-400/10' },
|
{ icon: '✨', title: '图生视频', desc: '给张参考图,让静态画面动起来。', bg: 'var(--m-yellow)' },
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-16 pb-8">
|
<div class="space-y-16 pb-8">
|
||||||
<!-- ===== hero ===== -->
|
<!-- ===== hero ===== -->
|
||||||
<section class="relative overflow-hidden rounded-[2rem] anime-card px-6 py-10 md:px-12 md:py-14">
|
<section class="relative overflow-hidden rounded-[1.4rem] anime-card px-6 py-10 md:px-12 md:py-14">
|
||||||
<div class="anime-stars"></div>
|
<div class="memphis-dots absolute inset-0 opacity-40 pointer-events-none"></div>
|
||||||
<div class="relative grid md:grid-cols-[1.1fr_0.9fr] gap-8 items-center">
|
<div class="relative grid md:grid-cols-[1.1fr_0.9fr] gap-8 items-center">
|
||||||
<div>
|
<div>
|
||||||
<span class="sticker">✦ 关于我们</span>
|
<span class="sticker">✦ 关于我们</span>
|
||||||
<h1 class="mt-4 text-4xl md:text-5xl font-extrabold tracking-tight text-[color:var(--fg)]">
|
<h1 class="mt-4 text-4xl md:text-5xl font-black tracking-tight text-[color:var(--fg)]">
|
||||||
嗨,我是 <span class="anime-grad-text">薇薇</span>
|
把创作交给 <span class="anime-grad-text">{{ site.title }}</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-[color:var(--fg-2)] mt-5 text-base md:text-lg leading-relaxed max-w-xl">
|
<p class="text-[color:var(--fg-2)] mt-5 text-base md:text-lg leading-relaxed max-w-xl">
|
||||||
{{ site.title }} 是一个聚合 GPT、Gemini、Firefly、Flux 等顶级模型的 AI 生图与生视频平台。
|
{{ site.title }} 是一个聚合 GPT、Gemini、Firefly、Flux 等顶级模型的 AI 生图与生视频平台。
|
||||||
把脑海里的画面写成一句话,交给我替你变成图像与视频。
|
把脑海里的画面写成一句话,剩下的交给我们变成图像与视频。
|
||||||
</p>
|
</p>
|
||||||
<router-link to="/user" class="bubble-btn mt-7">开始创作 →</router-link>
|
<router-link to="/user" class="bubble-btn mt-7">开始创作 →</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid place-items-center">
|
<!-- memphis tile cluster standing in for the old mascot illustration -->
|
||||||
<Mascot :size="240" />
|
<div class="ab-cluster" aria-hidden="true">
|
||||||
|
<span class="ab-tile ab-1">🎨</span>
|
||||||
|
<span class="ab-tile ab-2">🖼️</span>
|
||||||
|
<span class="ab-tile ab-3">🎬</span>
|
||||||
|
<span class="ab-tile ab-4">✨</span>
|
||||||
|
<span class="ab-star">✦</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ===== powers ===== -->
|
<!-- ===== powers ===== -->
|
||||||
<section>
|
<section>
|
||||||
<div class="anime-eyebrow text-fuchsia-500 dark:text-fuchsia-300">能做什么</div>
|
<div class="anime-eyebrow text-[var(--m-orange)]">能做什么</div>
|
||||||
<h2 class="mt-3 text-2xl md:text-3xl font-extrabold tracking-tight text-[color:var(--fg)]">四种方式,把想象变成画面</h2>
|
<h2 class="mt-3 text-2xl md:text-3xl font-black tracking-tight text-[color:var(--fg)]">四种方式,把想象变成画面</h2>
|
||||||
<div class="mt-7 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
<div class="mt-7 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||||
<div v-for="p in powers" :key="p.title"
|
<div v-for="p in powers" :key="p.title"
|
||||||
class="anime-card p-5 bg-gradient-to-br" :class="p.tint">
|
class="power-card p-5" :style="{ background: p.bg }">
|
||||||
<div class="text-3xl">{{ p.icon }}</div>
|
<div class="text-3xl">{{ p.icon }}</div>
|
||||||
<div class="mt-3 text-base font-bold text-[color:var(--fg)]">{{ p.title }}</div>
|
<div class="mt-3 text-base font-extrabold text-[#111]">{{ p.title }}</div>
|
||||||
<div class="mt-1.5 text-[13px] leading-relaxed text-[color:var(--fg-3)]">{{ p.desc }}</div>
|
<div class="mt-1.5 text-[13px] font-medium leading-relaxed text-[#111]/60">{{ p.desc }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ===== contact ===== -->
|
<!-- ===== contact ===== -->
|
||||||
<section>
|
<section>
|
||||||
<div class="anime-eyebrow text-sky-500 dark:text-sky-300">联系我们</div>
|
<div class="anime-eyebrow text-sky-600">联系我们</div>
|
||||||
<h2 class="mt-3 text-2xl md:text-3xl font-extrabold tracking-tight text-[color:var(--fg)]">有问题?来找我们玩~</h2>
|
<h2 class="mt-3 text-2xl md:text-3xl font-black tracking-tight text-[color:var(--fg)]">有问题?来找我们玩~</h2>
|
||||||
<p class="text-[color:var(--fg-3)] mt-2 mb-6">合作、反馈,或想加入交流群,随时招呼。</p>
|
<p class="text-[color:var(--fg-3)] mt-2 mb-6">合作、反馈,或想加入交流群,随时招呼。</p>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 max-w-3xl">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 max-w-3xl">
|
||||||
@@ -61,7 +65,7 @@ const powers = [
|
|||||||
class="group anime-card flex items-center justify-between gap-3 p-5">
|
class="group anime-card flex items-center justify-between gap-3 p-5">
|
||||||
<div>
|
<div>
|
||||||
<div class="sticker">🛍️ 商店</div>
|
<div class="sticker">🛍️ 商店</div>
|
||||||
<div class="text-base font-semibold text-[color:var(--fg)] mt-2 group-hover:text-fuchsia-400 transition-colors">前往商店</div>
|
<div class="text-base font-semibold text-[color:var(--fg)] mt-2 group-hover:text-[var(--m-orange)] transition-colors">前往商店</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-[color:var(--fg-faint)] group-hover:translate-x-1 transition-transform">→</span>
|
<span class="text-[color:var(--fg-faint)] group-hover:translate-x-1 transition-transform">→</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -69,14 +73,14 @@ const powers = [
|
|||||||
<a v-if="contact.email" :href="`mailto:${contact.email}`"
|
<a v-if="contact.email" :href="`mailto:${contact.email}`"
|
||||||
class="group anime-card p-5">
|
class="group anime-card p-5">
|
||||||
<div class="sticker">✉️ 邮箱</div>
|
<div class="sticker">✉️ 邮箱</div>
|
||||||
<div class="text-base font-semibold text-[color:var(--fg)] mt-2 break-all group-hover:text-sky-400 transition-colors">{{ contact.email }}</div>
|
<div class="text-base font-semibold text-[color:var(--fg)] mt-2 break-all group-hover:text-sky-600 transition-colors">{{ contact.email }}</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a v-if="contact.qq && contact.qq_link" :href="contact.qq_link" target="_blank" rel="noopener"
|
<a v-if="contact.qq && contact.qq_link" :href="contact.qq_link" target="_blank" rel="noopener"
|
||||||
class="group anime-card flex items-center justify-between gap-3 p-5">
|
class="group anime-card flex items-center justify-between gap-3 p-5">
|
||||||
<div>
|
<div>
|
||||||
<div class="sticker">💬 QQ</div>
|
<div class="sticker">💬 QQ</div>
|
||||||
<div class="text-base font-semibold text-[color:var(--fg)] mt-2 tabular-nums break-all group-hover:text-sky-400 transition-colors">{{ contact.qq }}</div>
|
<div class="text-base font-semibold text-[color:var(--fg)] mt-2 tabular-nums break-all group-hover:text-sky-600 transition-colors">{{ contact.qq }}</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-[color:var(--fg-faint)] group-hover:translate-x-1 transition-transform">→</span>
|
<span class="text-[color:var(--fg-faint)] group-hover:translate-x-1 transition-transform">→</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -89,7 +93,7 @@ const powers = [
|
|||||||
class="group anime-card flex items-center justify-between gap-3 p-5">
|
class="group anime-card flex items-center justify-between gap-3 p-5">
|
||||||
<div>
|
<div>
|
||||||
<div class="sticker">🎀 QQ 群</div>
|
<div class="sticker">🎀 QQ 群</div>
|
||||||
<div class="text-base font-semibold text-[color:var(--fg)] mt-2 tabular-nums break-all group-hover:text-emerald-400 transition-colors">{{ contact.qq_group }}</div>
|
<div class="text-base font-semibold text-[color:var(--fg)] mt-2 tabular-nums break-all group-hover:text-emerald-600 transition-colors">{{ contact.qq_group }}</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-[color:var(--fg-faint)] group-hover:translate-x-1 transition-transform">→</span>
|
<span class="text-[color:var(--fg-faint)] group-hover:translate-x-1 transition-transform">→</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -104,3 +108,42 @@ const powers = [
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Crayon tile cluster — a loose collage of rotated ink-outlined squares. */
|
||||||
|
.ab-cluster {
|
||||||
|
position: relative;
|
||||||
|
min-height: 240px;
|
||||||
|
}
|
||||||
|
.ab-tile {
|
||||||
|
position: absolute;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 5.5rem;
|
||||||
|
height: 5.5rem;
|
||||||
|
font-size: 2.2rem;
|
||||||
|
border: 3px solid var(--ink);
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 6px 6px 0 var(--shadow-ink);
|
||||||
|
}
|
||||||
|
.ab-1 { left: 12%; top: 6%; background: var(--m-mint); transform: rotate(-9deg); animation: memphis-float 6s ease-in-out infinite; }
|
||||||
|
.ab-2 { right: 14%; top: 0; background: var(--m-pink); transform: rotate(7deg); animation: memphis-float 7s ease-in-out infinite reverse; }
|
||||||
|
.ab-3 { left: 26%; bottom: 4%; background: var(--m-blue); transform: rotate(5deg); animation: memphis-bounce 4.5s ease-in-out infinite; }
|
||||||
|
.ab-4 { right: 8%; bottom: 14%; background: var(--m-yellow); transform: rotate(-6deg); animation: memphis-float 5.5s ease-in-out infinite; }
|
||||||
|
.ab-star {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%; top: 44%;
|
||||||
|
font-size: 2.6rem;
|
||||||
|
color: var(--m-orange);
|
||||||
|
animation: memphis-wiggle 3.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.power-card {
|
||||||
|
border: 2.5px solid var(--ink);
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 5px 5px 0 var(--shadow-ink);
|
||||||
|
transition: transform 0.18s ease, box-shadow 0.18s ease;
|
||||||
|
}
|
||||||
|
.power-card:hover { transform: translate(-2px, -2px); box-shadow: 8px 8px 0 var(--shadow-ink); }
|
||||||
|
@media (max-width: 768px) { .ab-cluster { display: none; } }
|
||||||
|
@media (prefers-reduced-motion: reduce) { .ab-tile, .ab-star { animation: none; } }
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -65,11 +65,11 @@ function typePill(t) {
|
|||||||
return {
|
return {
|
||||||
adobe: 'bg-rose-500/10 text-rose-300 ring-rose-400/30',
|
adobe: 'bg-rose-500/10 text-rose-300 ring-rose-400/30',
|
||||||
openai: 'bg-emerald-500/10 text-emerald-300 ring-emerald-400/30',
|
openai: 'bg-emerald-500/10 text-emerald-300 ring-emerald-400/30',
|
||||||
runway: 'bg-violet-500/10 text-violet-300 ring-violet-400/30',
|
runway: 'bg-orange-500/10 text-orange-300 ring-orange-400/30',
|
||||||
leonardo: 'bg-amber-500/10 text-amber-300 ring-amber-400/30',
|
leonardo: 'bg-amber-500/10 text-amber-300 ring-amber-400/30',
|
||||||
krea: 'bg-sky-500/10 text-sky-300 ring-sky-400/30',
|
krea: 'bg-sky-500/10 text-sky-300 ring-sky-400/30',
|
||||||
imagine: 'bg-teal-500/10 text-teal-300 ring-teal-400/30',
|
imagine: 'bg-teal-500/10 text-teal-300 ring-teal-400/30',
|
||||||
creativefabrica: 'bg-fuchsia-500/10 text-fuchsia-300 ring-fuchsia-400/30',
|
creativefabrica: 'bg-rose-500/10 text-rose-300 ring-rose-400/30',
|
||||||
}[t] || 'bg-white/[0.06] text-white/70 ring-white/15'
|
}[t] || 'bg-white/[0.06] text-white/70 ring-white/15'
|
||||||
}
|
}
|
||||||
// 与后端 accountConcurrency 保持一致:adobe 普号固定 1 并发,会员号取配置值
|
// 与后端 accountConcurrency 保持一致:adobe 普号固定 1 并发,会员号取配置值
|
||||||
@@ -339,7 +339,7 @@ onMounted(() => { loadAccounts(); loadModelList() })
|
|||||||
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ stats.total }}</div>
|
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ stats.total }}</div>
|
||||||
<div class="text-[10px] text-white/35 mt-0.5">成功/失败/限额</div>
|
<div class="text-[10px] text-white/35 mt-0.5">成功/失败/限额</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="t in [['openai','OpenAI','text-emerald-300/80'],['adobe','Adobe','text-rose-300/80'],['runway','Runway','text-violet-300/80'],['leonardo','Leonardo','text-amber-300/80'],['krea','Krea','text-sky-300/80'],['imagine','Imagine','text-teal-300/80'],['grok','Grok','text-slate-300/80'],['creativefabrica','Creative Fabrica','text-fuchsia-300/80']]"
|
<div v-for="t in [['openai','OpenAI','text-emerald-300/80'],['adobe','Adobe','text-rose-300/80'],['runway','Runway','text-orange-300/80'],['leonardo','Leonardo','text-amber-300/80'],['krea','Krea','text-sky-300/80'],['imagine','Imagine','text-teal-300/80'],['grok','Grok','text-slate-300/80'],['creativefabrica','Creative Fabrica','text-rose-300/80']]"
|
||||||
:key="t[0]" class="card p-4">
|
:key="t[0]" class="card p-4">
|
||||||
<div class="text-[11px] uppercase tracking-wider" :class="t[2]">{{ t[1] }}</div>
|
<div class="text-[11px] uppercase tracking-wider" :class="t[2]">{{ t[1] }}</div>
|
||||||
<div class="text-2xl font-semibold mt-1 tabular-nums">
|
<div class="text-2xl font-semibold mt-1 tabular-nums">
|
||||||
@@ -359,8 +359,8 @@ onMounted(() => { loadAccounts(); loadModelList() })
|
|||||||
<button @click="setFilter(() => typeFilter = 'adobe')" class="fp" :class="typeFilter === 'adobe' && 'fp-rose'">
|
<button @click="setFilter(() => typeFilter = 'adobe')" class="fp" :class="typeFilter === 'adobe' && 'fp-rose'">
|
||||||
<span class="w-1.5 h-1.5 rounded-full bg-rose-400"></span>Adobe
|
<span class="w-1.5 h-1.5 rounded-full bg-rose-400"></span>Adobe
|
||||||
</button>
|
</button>
|
||||||
<button @click="setFilter(() => typeFilter = 'runway')" class="fp" :class="typeFilter === 'runway' && 'fp-violet'">
|
<button @click="setFilter(() => typeFilter = 'runway')" class="fp" :class="typeFilter === 'runway' && 'fp-orange'">
|
||||||
<span class="w-1.5 h-1.5 rounded-full bg-violet-400"></span>Runway
|
<span class="w-1.5 h-1.5 rounded-full bg-orange-400"></span>Runway
|
||||||
</button>
|
</button>
|
||||||
<button @click="setFilter(() => typeFilter = 'leonardo')" class="fp" :class="typeFilter === 'leonardo' && 'fp-amber'">
|
<button @click="setFilter(() => typeFilter = 'leonardo')" class="fp" :class="typeFilter === 'leonardo' && 'fp-amber'">
|
||||||
<span class="w-1.5 h-1.5 rounded-full bg-amber-400"></span>Leonardo
|
<span class="w-1.5 h-1.5 rounded-full bg-amber-400"></span>Leonardo
|
||||||
@@ -375,7 +375,7 @@ onMounted(() => { loadAccounts(); loadModelList() })
|
|||||||
<span class="w-1.5 h-1.5 rounded-full bg-slate-400"></span>Grok
|
<span class="w-1.5 h-1.5 rounded-full bg-slate-400"></span>Grok
|
||||||
</button>
|
</button>
|
||||||
<button @click="setFilter(() => typeFilter = 'creativefabrica')" class="fp" :class="typeFilter === 'creativefabrica' && 'fp-on'">
|
<button @click="setFilter(() => typeFilter = 'creativefabrica')" class="fp" :class="typeFilter === 'creativefabrica' && 'fp-on'">
|
||||||
<span class="w-1.5 h-1.5 rounded-full bg-fuchsia-400"></span>Creative Fabrica
|
<span class="w-1.5 h-1.5 rounded-full bg-rose-400"></span>Creative Fabrica
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-px h-5 bg-white/10"></div>
|
<div class="w-px h-5 bg-white/10"></div>
|
||||||
@@ -397,7 +397,7 @@ onMounted(() => { loadAccounts(); loadModelList() })
|
|||||||
<button @click="setFilter(() => planFilter = '')" class="fp" :class="planFilter === '' && 'fp-on'">全部账号</button>
|
<button @click="setFilter(() => planFilter = '')" class="fp" :class="planFilter === '' && 'fp-on'">全部账号</button>
|
||||||
<button @click="setFilter(() => planFilter = 'free')" class="fp" :class="planFilter === 'free' && 'fp-slate'">普号</button>
|
<button @click="setFilter(() => planFilter = 'free')" class="fp" :class="planFilter === 'free' && 'fp-slate'">普号</button>
|
||||||
<button @click="setFilter(() => planFilter = 'sub')" class="fp" :class="planFilter === 'sub' && 'fp-sky'">子号</button>
|
<button @click="setFilter(() => planFilter = 'sub')" class="fp" :class="planFilter === 'sub' && 'fp-sky'">子号</button>
|
||||||
<button @click="setFilter(() => planFilter = 'master')" class="fp" :class="planFilter === 'master' && 'fp-purple'">会员号</button>
|
<button @click="setFilter(() => planFilter = 'master')" class="fp" :class="planFilter === 'master' && 'fp-pink'">会员号</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="flex-1 min-w-[200px]">
|
<div class="flex-1 min-w-[200px]">
|
||||||
@@ -552,7 +552,7 @@ onMounted(() => { loadAccounts(); loadModelList() })
|
|||||||
<td class="px-3 py-3.5 align-middle">
|
<td class="px-3 py-3.5 align-middle">
|
||||||
<div class="flex items-center justify-center gap-1.5 text-xs tabular-nums">
|
<div class="flex items-center justify-center gap-1.5 text-xs tabular-nums">
|
||||||
<span class="px-1.5 py-0.5 rounded"
|
<span class="px-1.5 py-0.5 rounded"
|
||||||
:class="a.in_flight ? 'bg-indigo-500/15 text-indigo-300 font-semibold' : 'text-white/25'"
|
:class="a.in_flight ? 'bg-sky-500/15 text-sky-300 font-semibold' : 'text-white/25'"
|
||||||
title="在途">{{ a.in_flight || 0 }}</span>
|
title="在途">{{ a.in_flight || 0 }}</span>
|
||||||
<span class="text-white/20">/</span>
|
<span class="text-white/20">/</span>
|
||||||
<span class="px-1.5 py-0.5 rounded text-emerald-300 font-medium" title="成功">{{ a.success_total || 0 }}</span>
|
<span class="px-1.5 py-0.5 rounded text-emerald-300 font-medium" title="成功">{{ a.success_total || 0 }}</span>
|
||||||
@@ -645,10 +645,10 @@ onMounted(() => { loadAccounts(); loadModelList() })
|
|||||||
color: rgb(253 224 71);
|
color: rgb(253 224 71);
|
||||||
box-shadow: inset 0 0 0 1px rgb(253 224 71 / 0.4);
|
box-shadow: inset 0 0 0 1px rgb(253 224 71 / 0.4);
|
||||||
}
|
}
|
||||||
.fp-violet {
|
.fp-orange {
|
||||||
background: rgb(139 92 246 / 0.22);
|
background: rgb(249 115 22 / 0.22);
|
||||||
color: rgb(196 181 253);
|
color: rgb(253 186 116);
|
||||||
box-shadow: inset 0 0 0 1px rgb(196 181 253 / 0.45);
|
box-shadow: inset 0 0 0 1px rgb(253 186 116 / 0.45);
|
||||||
}
|
}
|
||||||
.fp-sky {
|
.fp-sky {
|
||||||
background: rgb(56 189 248 / 0.22);
|
background: rgb(56 189 248 / 0.22);
|
||||||
@@ -660,10 +660,10 @@ onMounted(() => { loadAccounts(); loadModelList() })
|
|||||||
color: rgb(94 234 212);
|
color: rgb(94 234 212);
|
||||||
box-shadow: inset 0 0 0 1px rgb(94 234 212 / 0.45);
|
box-shadow: inset 0 0 0 1px rgb(94 234 212 / 0.45);
|
||||||
}
|
}
|
||||||
.fp-purple {
|
.fp-pink {
|
||||||
background: rgb(168 85 247 / 0.22);
|
background: rgb(244 114 182 / 0.22);
|
||||||
color: rgb(216 180 254);
|
color: rgb(249 168 212);
|
||||||
box-shadow: inset 0 0 0 1px rgb(216 180 254 / 0.45);
|
box-shadow: inset 0 0 0 1px rgb(249 168 212 / 0.45);
|
||||||
}
|
}
|
||||||
.fp-slate {
|
.fp-slate {
|
||||||
background: rgb(148 163 184 / 0.22);
|
background: rgb(148 163 184 / 0.22);
|
||||||
@@ -725,10 +725,10 @@ onMounted(() => { loadAccounts(); loadModelList() })
|
|||||||
background: rgb(244 63 94 / 0.8);
|
background: rgb(244 63 94 / 0.8);
|
||||||
box-shadow: inset 0 0 0 1px rgb(244 63 94 / 0.6);
|
box-shadow: inset 0 0 0 1px rgb(244 63 94 / 0.6);
|
||||||
}
|
}
|
||||||
/* pending (import quota probe in flight) — neutral indigo, thumb stays left */
|
/* pending (import quota probe in flight) — neutral sky, thumb stays left */
|
||||||
.sw-pending {
|
.sw-pending {
|
||||||
background: rgb(99 102 241 / 0.45);
|
background: rgb(14 165 233 / 0.45);
|
||||||
box-shadow: inset 0 0 0 1px rgb(99 102 241 / 0.4);
|
box-shadow: inset 0 0 0 1px rgb(14 165 233 / 0.4);
|
||||||
}
|
}
|
||||||
/* quota exhausted — amber (NOT red/dead), thumb stays left, locked until reset */
|
/* quota exhausted — amber (NOT red/dead), thumb stays left, locked until reset */
|
||||||
.sw-quota {
|
.sw-quota {
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ function goPage(n) {
|
|||||||
<td class="px-3 py-3.5 align-middle text-xs text-white/55 whitespace-nowrap">{{ fmt(o.created_at) }}</td>
|
<td class="px-3 py-3.5 align-middle text-xs text-white/55 whitespace-nowrap">{{ fmt(o.created_at) }}</td>
|
||||||
<td class="px-3 py-3.5 align-middle text-xs text-white/55 whitespace-nowrap">{{ fmt(o.paid_at) }}</td>
|
<td class="px-3 py-3.5 align-middle text-xs text-white/55 whitespace-nowrap">{{ fmt(o.paid_at) }}</td>
|
||||||
<td class="px-3 py-3.5 align-middle text-right tabular-nums text-white/85">{{ o.source && o.source !== 'epay' ? '—' : '¥' + o.amount }}</td>
|
<td class="px-3 py-3.5 align-middle text-right tabular-nums text-white/85">{{ o.source && o.source !== 'epay' ? '—' : '¥' + o.amount }}</td>
|
||||||
<td class="px-3 py-3.5 align-middle text-right tabular-nums" :class="o.points < 0 ? 'text-rose-300' : 'text-violet-300'">{{ o.points }}</td>
|
<td class="px-3 py-3.5 align-middle text-right tabular-nums" :class="o.points < 0 ? 'text-rose-300' : 'text-orange-300'">{{ o.points }}</td>
|
||||||
<td class="px-3 py-3.5 align-middle">
|
<td class="px-3 py-3.5 align-middle">
|
||||||
<span class="chip" :class="chipClass(o.status)">{{ STATUS[o.status] }}<span class="opacity-50 ml-1">· {{ METHOD[o.pay_type] || o.pay_type }}</span></span>
|
<span class="chip" :class="chipClass(o.status)">{{ STATUS[o.status] }}<span class="opacity-50 ml-1">· {{ METHOD[o.pay_type] || o.pay_type }}</span></span>
|
||||||
</td>
|
</td>
|
||||||
@@ -148,7 +148,7 @@ function goPage(n) {
|
|||||||
.log-table { border-collapse: separate; border-spacing: 0; }
|
.log-table { border-collapse: separate; border-spacing: 0; }
|
||||||
.log-row td { border-bottom: 1px solid rgb(255 255 255 / 0.04); transition: background-color 0.15s ease, box-shadow 0.15s ease; }
|
.log-row td { border-bottom: 1px solid rgb(255 255 255 / 0.04); transition: background-color 0.15s ease, box-shadow 0.15s ease; }
|
||||||
.log-row:hover td { background: rgb(255 255 255 / 0.025); }
|
.log-row:hover td { background: rgb(255 255 255 / 0.025); }
|
||||||
.log-row:hover td:first-child { box-shadow: inset 2px 0 0 rgb(167 139 250 / 0.55); }
|
.log-row:hover td:first-child { box-shadow: inset 2px 0 0 rgb(253 186 116 / 0.55); }
|
||||||
.log-row:last-child td { border-bottom: none; }
|
.log-row:last-child td { border-bottom: none; }
|
||||||
.pg { min-width: 1.75rem; padding: 0.3rem 0.55rem; font-size: 0.72rem; font-weight: 500; text-align: center; border-radius: 0.45rem; color: rgb(255 255 255 / 0.7); background: rgb(255 255 255 / 0.04); box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.08); transition: background 0.15s, color 0.15s; }
|
.pg { min-width: 1.75rem; padding: 0.3rem 0.55rem; font-size: 0.72rem; font-weight: 500; text-align: center; border-radius: 0.45rem; color: rgb(255 255 255 / 0.7); background: rgb(255 255 255 / 0.04); box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.08); transition: background 0.15s, color 0.15s; }
|
||||||
.pg:hover:not(.pg-on) { background: rgb(255 255 255 / 0.1); color: white; }
|
.pg:hover:not(.pg-on) { background: rgb(255 255 255 / 0.1); color: white; }
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ onMounted(load)
|
|||||||
color: white;
|
color: white;
|
||||||
background: rgb(244 63 94 / 0.25);
|
background: rgb(244 63 94 / 0.25);
|
||||||
}
|
}
|
||||||
.chk { accent-color: rgb(217 70 239); width: 0.9rem; height: 0.9rem; cursor: pointer; }
|
.chk { accent-color: rgb(244 63 94); width: 0.9rem; height: 0.9rem; cursor: pointer; }
|
||||||
.pg { min-width: 1.75rem; padding: 0.3rem 0.55rem; font-size: 0.72rem; font-weight: 500; text-align: center; border-radius: 0.45rem; color: rgb(255 255 255 / 0.7); background: rgb(255 255 255 / 0.04); box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.08); transition: background 0.15s, color 0.15s; }
|
.pg { min-width: 1.75rem; padding: 0.3rem 0.55rem; font-size: 0.72rem; font-weight: 500; text-align: center; border-radius: 0.45rem; color: rgb(255 255 255 / 0.7); background: rgb(255 255 255 / 0.04); box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.08); transition: background 0.15s, color 0.15s; }
|
||||||
.pg:hover:not(.pg-on) { background: rgb(255 255 255 / 0.1); color: white; }
|
.pg:hover:not(.pg-on) { background: rgb(255 255 255 / 0.1); color: white; }
|
||||||
.pg-on { background: rgb(255 255 255 / 0.92); color: rgb(15 23 42); box-shadow: none; }
|
.pg-on { background: rgb(255 255 255 / 0.92); color: rgb(15 23 42); box-shadow: none; }
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ const selectedNotes = computed(() =>
|
|||||||
const pickerOpen = ref(false)
|
const pickerOpen = ref(false)
|
||||||
const pickerItems = ref([])
|
const pickerItems = ref([])
|
||||||
const pickerLoading = ref(false)
|
const pickerLoading = ref(false)
|
||||||
|
const fileInput = ref(null)
|
||||||
|
const uploadBusy = ref(false)
|
||||||
|
const dropHover = ref(false)
|
||||||
|
|
||||||
// ================= 文档加载 / 保存 =================
|
// ================= 文档加载 / 保存 =================
|
||||||
|
|
||||||
@@ -285,18 +288,21 @@ function redo() {
|
|||||||
// ================= 节点操作 =================
|
// ================= 节点操作 =================
|
||||||
|
|
||||||
function addNote() {
|
function addNote() {
|
||||||
const c = viewportCenterWorld()
|
addNoteAt(viewportCenterWorld())
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNoteAt(w) {
|
||||||
const n = {
|
const n = {
|
||||||
id: newId('t'), type: 'note', text: '',
|
id: newId('t'), type: 'note', text: '',
|
||||||
x: Math.round(c.x - 130), y: Math.round(c.y - 70), w: 260, h: 140, z: 0,
|
x: Math.round(w.x - 130), y: Math.round(w.y - 70), w: 260, h: 140, z: 0,
|
||||||
}
|
}
|
||||||
doc.nodes.push(n)
|
doc.nodes.push(n)
|
||||||
selectionIds.value = [n.id]
|
selectionIds.value = [n.id]
|
||||||
pushHistory()
|
pushHistory()
|
||||||
}
|
}
|
||||||
|
|
||||||
function addImageNode(src, prompt = '') {
|
function addImageNode(src, prompt = '', at = null) {
|
||||||
const c = viewportCenterWorld()
|
const c = at || viewportCenterWorld()
|
||||||
const n = {
|
const n = {
|
||||||
id: newId('i'), type: 'image', src, prompt,
|
id: newId('i'), type: 'image', src, prompt,
|
||||||
x: Math.round(c.x - 256 + cascade()), y: Math.round(c.y - 256 + cascade()),
|
x: Math.round(c.x - 256 + cascade()), y: Math.round(c.y - 256 + cascade()),
|
||||||
@@ -322,6 +328,32 @@ function viewportCenterWorld() {
|
|||||||
return screenToWorld(w / 2, h / 2, vp)
|
return screenToWorld(w / 2, h / 2, vp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 复制选中节点(带相互连线,偏移落位) ----
|
||||||
|
function duplicateSelection() {
|
||||||
|
const sel = doc.nodes.filter((n) => selectionIds.value.includes(n.id) && n.type !== 'gen')
|
||||||
|
if (!sel.length) return
|
||||||
|
const idMap = {}
|
||||||
|
const clones = sel.map((n) => {
|
||||||
|
const c = {
|
||||||
|
...n,
|
||||||
|
id: newId(n.type === 'note' ? 't' : 'i'),
|
||||||
|
x: n.x + 36,
|
||||||
|
y: n.y + 36,
|
||||||
|
}
|
||||||
|
idMap[n.id] = c.id
|
||||||
|
return c
|
||||||
|
})
|
||||||
|
doc.nodes.push(...clones)
|
||||||
|
for (const e of [...doc.edges]) {
|
||||||
|
if (idMap[e.from] && idMap[e.to]) {
|
||||||
|
doc.edges.push({ id: newId('e'), from: idMap[e.from], to: idMap[e.to] })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectionIds.value = clones.map((c) => c.id)
|
||||||
|
stage.value?.selectIds?.(selectionIds.value)
|
||||||
|
pushHistory()
|
||||||
|
}
|
||||||
|
|
||||||
function deleteSelection() {
|
function deleteSelection() {
|
||||||
const dead = new Set(selectionIds.value)
|
const dead = new Set(selectionIds.value)
|
||||||
const deadEdges = stage.value?.consumeSelectedEdges() || []
|
const deadEdges = stage.value?.consumeSelectedEdges() || []
|
||||||
@@ -403,6 +435,60 @@ async function openPicker() {
|
|||||||
pickerLoading.value = false
|
pickerLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 上传本地图片(按钮/拖拽/粘贴三条路径都汇到这里) ----
|
||||||
|
async function uploadImageFile(file, at = null) {
|
||||||
|
if (!file || !file.type?.startsWith('image/')) return
|
||||||
|
uploadBusy.value = true
|
||||||
|
try {
|
||||||
|
const fd = new FormData()
|
||||||
|
fd.append('file', file)
|
||||||
|
const r = await api('/my-images', { method: 'POST', body: fd })
|
||||||
|
if (r.ok && r.data?.name) {
|
||||||
|
pickerItems.value.unshift({ name: r.data.name })
|
||||||
|
addImageNode(r.data.name, '', at)
|
||||||
|
} else {
|
||||||
|
genMsg.value = r.data?.detail || `上传失败 (${r.status})`
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
genMsg.value = String(e)
|
||||||
|
} finally {
|
||||||
|
uploadBusy.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFilePicked(e) {
|
||||||
|
const files = [...(e.target.files || [])]
|
||||||
|
e.target.value = ''
|
||||||
|
files.forEach((f) => uploadImageFile(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拖拽本地图片落到画布:落点即拖放位置
|
||||||
|
function onDrop(e) {
|
||||||
|
dropHover.value = false
|
||||||
|
const files = [...(e.dataTransfer?.files || [])].filter((f) => f.type?.startsWith('image/'))
|
||||||
|
if (!files.length) return
|
||||||
|
const el = document.querySelector('.cv-stage')
|
||||||
|
const rect = el?.getBoundingClientRect()
|
||||||
|
const vp = stage.value?.vp
|
||||||
|
const at =
|
||||||
|
rect && vp ? screenToWorld(e.clientX - rect.left, e.clientY - rect.top, vp) : null
|
||||||
|
files.forEach((f) => uploadImageFile(f, at))
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPaste(e) {
|
||||||
|
if (isTyping(e.target)) return
|
||||||
|
for (const it of e.clipboardData?.items || []) {
|
||||||
|
if (it.type?.startsWith('image/')) {
|
||||||
|
const f = it.getAsFile()
|
||||||
|
if (f) {
|
||||||
|
e.preventDefault()
|
||||||
|
uploadImageFile(f)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ================= 生成 =================
|
// ================= 生成 =================
|
||||||
|
|
||||||
async function refToBase64(name) {
|
async function refToBase64(name) {
|
||||||
@@ -540,9 +626,20 @@ function onPanZoom(v) {
|
|||||||
scalePct.value = Math.round(v.scale * 100)
|
scalePct.value = Math.round(v.scale * 100)
|
||||||
}
|
}
|
||||||
function onNodeAction({ type, id }) {
|
function onNodeAction({ type, id }) {
|
||||||
if (type !== 'preview') return
|
|
||||||
const n = doc.nodes.find((x) => x.id === id)
|
const n = doc.nodes.find((x) => x.id === id)
|
||||||
if (n?.type === 'image') previewSrc.value = n.src
|
if (!n) return
|
||||||
|
if (type === 'preview') {
|
||||||
|
if (n.type === 'image') previewSrc.value = n.src
|
||||||
|
} else if (type === 'duplicate') {
|
||||||
|
selectionIds.value = [id]
|
||||||
|
duplicateSelection()
|
||||||
|
} else if (type === 'delete') {
|
||||||
|
selectionIds.value = [id]
|
||||||
|
deleteSelection()
|
||||||
|
} else if (type === 'front') {
|
||||||
|
stage.value?.bumpNode?.(id)
|
||||||
|
markDirty()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================= 快捷键 =================
|
// ================= 快捷键 =================
|
||||||
@@ -567,6 +664,9 @@ function onKeydown(e) {
|
|||||||
} else if (mod && e.key.toLowerCase() === 'a') {
|
} else if (mod && e.key.toLowerCase() === 'a') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
stage.value?.selectAll()
|
stage.value?.selectAll()
|
||||||
|
} else if (mod && e.key.toLowerCase() === 'd') {
|
||||||
|
e.preventDefault()
|
||||||
|
duplicateSelection()
|
||||||
} else if (e.key === 'Delete' || e.key === 'Backspace') {
|
} else if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
deleteSelection()
|
deleteSelection()
|
||||||
@@ -581,6 +681,8 @@ function onKeydown(e) {
|
|||||||
stage.value?.zoomBy(1 / 1.2)
|
stage.value?.zoomBy(1 / 1.2)
|
||||||
} else if (e.key === '0') {
|
} else if (e.key === '0') {
|
||||||
stage.value?.setViewport({ x: 0, y: 0, scale: 1 })
|
stage.value?.setViewport({ x: 0, y: 0, scale: 1 })
|
||||||
|
} else if (e.key === '1') {
|
||||||
|
stage.value?.fitToContent?.()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,6 +694,7 @@ function selectAll() {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
window.addEventListener('keydown', onKeydown)
|
window.addEventListener('keydown', onKeydown)
|
||||||
|
window.addEventListener('paste', onPaste)
|
||||||
window.addEventListener('beforeunload', flushSync)
|
window.addEventListener('beforeunload', flushSync)
|
||||||
document.addEventListener('visibilitychange', onVisibility)
|
document.addEventListener('visibilitychange', onVisibility)
|
||||||
const mm = await api('/managed-models')
|
const mm = await api('/managed-models')
|
||||||
@@ -610,6 +713,7 @@ function onVisibility() {
|
|||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener('keydown', onKeydown)
|
window.removeEventListener('keydown', onKeydown)
|
||||||
|
window.removeEventListener('paste', onPaste)
|
||||||
window.removeEventListener('beforeunload', flushSync)
|
window.removeEventListener('beforeunload', flushSync)
|
||||||
document.removeEventListener('visibilitychange', onVisibility)
|
document.removeEventListener('visibilitychange', onVisibility)
|
||||||
clearTimeout(saveTimer)
|
clearTimeout(saveTimer)
|
||||||
@@ -655,17 +759,28 @@ const saveLabel = computed(() => ({
|
|||||||
<button class="cv-btn" title="缩小" @click="stage?.zoomBy(1 / 1.2)">−</button>
|
<button class="cv-btn" title="缩小" @click="stage?.zoomBy(1 / 1.2)">−</button>
|
||||||
<span class="cv-zoom">{{ scalePct }}%</span>
|
<span class="cv-zoom">{{ scalePct }}%</span>
|
||||||
<button class="cv-btn" title="放大" @click="stage?.zoomBy(1.2)">+</button>
|
<button class="cv-btn" title="放大" @click="stage?.zoomBy(1.2)">+</button>
|
||||||
<button class="cv-btn" title="重置视图" @click="stage?.setViewport({ x: 0, y: 0, scale: 1 })">⌂</button>
|
<button class="cv-btn" title="重置视图 (0)" @click="stage?.setViewport({ x: 0, y: 0, scale: 1 })">⌂</button>
|
||||||
|
<button class="cv-btn" title="适应内容 (1)" @click="stage?.fitToContent?.()">⤢</button>
|
||||||
<span class="cv-sep" />
|
<span class="cv-sep" />
|
||||||
<button class="cv-btn" @click="addNote">+ 笔记</button>
|
<button class="cv-btn cv-btn--accent" @click="addNote">+ 笔记</button>
|
||||||
<button class="cv-btn" @click="openPicker">+ 图片</button>
|
<button class="cv-btn cv-btn--accent" @click="openPicker">+ 图片</button>
|
||||||
|
<button class="cv-btn" :disabled="uploadBusy" title="上传本地图片(也可直接拖入/粘贴)" @click="fileInput?.click()">
|
||||||
|
{{ uploadBusy ? '上传中…' : '⤒ 上传' }}
|
||||||
|
</button>
|
||||||
|
<input ref="fileInput" type="file" accept="image/*" multiple hidden @change="onFilePicked">
|
||||||
<span class="cv-save" :data-state="saveState">{{ saveLabel }}</span>
|
<span class="cv-save" :data-state="saveState">{{ saveLabel }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 舞台 + 生成面板 -->
|
<!-- 舞台 + 生成面板 -->
|
||||||
<div class="cv-body">
|
<div class="cv-body">
|
||||||
<div class="cv-stagewrap">
|
<div
|
||||||
|
class="cv-stagewrap"
|
||||||
|
@dragover.prevent="dropHover = true"
|
||||||
|
@dragleave="dropHover = false"
|
||||||
|
@drop.prevent="onDrop"
|
||||||
|
>
|
||||||
|
<div v-if="dropHover" class="cv-drop">松开把图片放进画布</div>
|
||||||
<div v-if="loading" class="cv-loading">加载中…</div>
|
<div v-if="loading" class="cv-loading">加载中…</div>
|
||||||
<div v-else-if="loadError" class="cv-error">{{ loadError }} <button class="cv-btn" @click="boot">重试</button></div>
|
<div v-else-if="loadError" class="cv-error">{{ loadError }} <button class="cv-btn" @click="boot">重试</button></div>
|
||||||
<CanvasStage
|
<CanvasStage
|
||||||
@@ -681,6 +796,7 @@ const saveLabel = computed(() => ({
|
|||||||
@viewport-rest="onViewportRest"
|
@viewport-rest="onViewportRest"
|
||||||
@pan-zoom="onPanZoom"
|
@pan-zoom="onPanZoom"
|
||||||
@node-action="onNodeAction"
|
@node-action="onNodeAction"
|
||||||
|
@blank-dblclick="addNoteAt"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -727,8 +843,8 @@ const saveLabel = computed(() => ({
|
|||||||
<div v-if="genMsg" class="cv-genmsg">{{ genMsg }}</div>
|
<div v-if="genMsg" class="cv-genmsg">{{ genMsg }}</div>
|
||||||
|
|
||||||
<div class="cv-tips">
|
<div class="cv-tips">
|
||||||
拖节点右侧圆点可连线(点击连线选中,Delete 删除);双击笔记编辑;
|
双击空白建笔记 · 拖右侧圆点连线 · 选中后右下角缩放 · Ctrl+D 复制 ·
|
||||||
生成结果会自动连线到来源节点;Ctrl+Z 撤销。
|
本地图片直接拖入或 Ctrl+V 粘贴 · 按 1 适应全部内容
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
@@ -743,8 +859,11 @@ const saveLabel = computed(() => ({
|
|||||||
<div class="cv-picker__box">
|
<div class="cv-picker__box">
|
||||||
<div class="cv-picker__head">
|
<div class="cv-picker__head">
|
||||||
<span>插入图片(点击放置到画布)</span>
|
<span>插入图片(点击放置到画布)</span>
|
||||||
|
<div class="cv-picker__acts">
|
||||||
|
<button class="cv-btn cv-btn--accent" @click="fileInput?.click()">⤒ 上传本地</button>
|
||||||
<button class="cv-btn" @click="pickerOpen = false">✕</button>
|
<button class="cv-btn" @click="pickerOpen = false">✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div v-if="pickerLoading" class="cv-loading">加载中…</div>
|
<div v-if="pickerLoading" class="cv-loading">加载中…</div>
|
||||||
<div v-else-if="!pickerItems.length" class="cv-loading">还没有生成过图片</div>
|
<div v-else-if="!pickerItems.length" class="cv-loading">还没有生成过图片</div>
|
||||||
<div v-else class="cv-picker__grid">
|
<div v-else class="cv-picker__grid">
|
||||||
@@ -764,41 +883,53 @@ const saveLabel = computed(() => ({
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* ===== 画布:Memphis/neo-brutalism 全屏工作台(随站点明暗主题走变量) ===== */
|
||||||
.cv-btn {
|
.cv-btn {
|
||||||
padding: 5px 10px;
|
padding: 5px 11px;
|
||||||
border-radius: 8px;
|
border-radius: 9px;
|
||||||
border: 1px solid rgb(255 255 255 / 0.14);
|
border: 2.5px solid var(--ink);
|
||||||
background: rgb(255 255 255 / 0.04);
|
background: var(--surface);
|
||||||
color: #c6cbd8;
|
color: var(--fg);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
box-shadow: 3px 3px 0 var(--shadow-ink);
|
||||||
|
transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.12s ease;
|
||||||
}
|
}
|
||||||
.cv-btn:hover:not(:disabled) {
|
.cv-btn:hover:not(:disabled) {
|
||||||
background: rgb(255 255 255 / 0.09);
|
background: var(--m-yellow);
|
||||||
color: #fff;
|
}
|
||||||
|
.cv-btn:active:not(:disabled) {
|
||||||
|
transform: translate(2px, 2px);
|
||||||
|
box-shadow: 1px 1px 0 var(--shadow-ink);
|
||||||
}
|
}
|
||||||
.cv-btn:disabled {
|
.cv-btn:disabled {
|
||||||
opacity: 0.4;
|
opacity: 0.45;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
.cv-btn--danger:hover:not(:disabled) {
|
.cv-btn--danger:hover:not(:disabled) {
|
||||||
border-color: #6b3b44;
|
background: #ffb4ab;
|
||||||
color: #e58c96;
|
}
|
||||||
|
.cv-btn--accent {
|
||||||
|
background: var(--m-blue);
|
||||||
|
}
|
||||||
|
.cv-btn--accent:hover:not(:disabled) {
|
||||||
|
background: var(--m-mint);
|
||||||
}
|
}
|
||||||
.cv-select,
|
.cv-select,
|
||||||
.cv-input {
|
.cv-input {
|
||||||
padding: 6px 9px;
|
padding: 6px 10px;
|
||||||
border-radius: 8px;
|
border-radius: 9px;
|
||||||
border: 1px solid rgb(255 255 255 / 0.14);
|
border: 2.5px solid var(--ink);
|
||||||
background: #0e1015;
|
background: var(--surface);
|
||||||
color: #e8eaf0;
|
color: var(--fg);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
.cv-select:focus,
|
.cv-select:focus,
|
||||||
.cv-input:focus {
|
.cv-input:focus {
|
||||||
border-color: #7c6cff;
|
box-shadow: 3px 3px 0 var(--m-orange);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cv-page {
|
.cv-page {
|
||||||
@@ -808,50 +939,62 @@ const saveLabel = computed(() => ({
|
|||||||
z-index: 50;
|
z-index: 50;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: #0e1015;
|
background: var(--app-bg);
|
||||||
}
|
}
|
||||||
.cv-top {
|
.cv-top {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 8px 14px;
|
padding: 9px 14px;
|
||||||
border-bottom: 1px solid rgb(255 255 255 / 0.08);
|
border-bottom: 3px solid var(--ink);
|
||||||
background: #12141b;
|
background: var(--surface);
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.cv-top__left,
|
.cv-top__left,
|
||||||
.cv-top__right {
|
.cv-top__right {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 7px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.cv-select {
|
.cv-select {
|
||||||
max-width: 220px;
|
max-width: 220px;
|
||||||
}
|
}
|
||||||
.cv-sep {
|
.cv-sep {
|
||||||
width: 1px;
|
width: 2.5px;
|
||||||
height: 18px;
|
height: 20px;
|
||||||
background: rgb(255 255 255 / 0.12);
|
border-radius: 2px;
|
||||||
|
background: var(--ink);
|
||||||
|
opacity: 0.25;
|
||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
}
|
}
|
||||||
.cv-zoom {
|
.cv-zoom {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #9aa1b2;
|
font-weight: 700;
|
||||||
min-width: 44px;
|
color: var(--fg);
|
||||||
|
min-width: 46px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
background: var(--m-yellow);
|
||||||
|
border: 2px solid var(--ink);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 1px 8px;
|
||||||
}
|
}
|
||||||
.cv-save {
|
.cv-save {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #7f8798;
|
font-weight: 700;
|
||||||
|
color: var(--fg-3);
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
.cv-save[data-state='error'] {
|
|
||||||
color: #e58c96;
|
|
||||||
}
|
|
||||||
.cv-save[data-state='saved'] {
|
.cv-save[data-state='saved'] {
|
||||||
color: #6fae8c;
|
color: #15803d;
|
||||||
|
}
|
||||||
|
.cv-save[data-state='dirty'],
|
||||||
|
.cv-save[data-state='saving'] {
|
||||||
|
color: var(--m-orange);
|
||||||
|
}
|
||||||
|
.cv-save[data-state='error'] {
|
||||||
|
color: #dc2626;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cv-body {
|
.cv-body {
|
||||||
@@ -864,6 +1007,21 @@ const saveLabel = computed(() => ({
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
.cv-drop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 10px;
|
||||||
|
z-index: 6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--fg);
|
||||||
|
border: 3px dashed var(--ink);
|
||||||
|
border-radius: 18px;
|
||||||
|
background: color-mix(in srgb, var(--m-blue) 55%, transparent);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
.cv-loading,
|
.cv-loading,
|
||||||
.cv-error {
|
.cv-error {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -872,33 +1030,40 @@ const saveLabel = computed(() => ({
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
color: #9aa1b2;
|
color: var(--fg-2);
|
||||||
background: #0e1015;
|
font-weight: 600;
|
||||||
|
background: var(--app-bg);
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
}
|
}
|
||||||
.cv-error {
|
.cv-error {
|
||||||
color: #e58c96;
|
color: #dc2626;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 右侧生成面板 = 白纸卡片 */
|
||||||
.cv-panel {
|
.cv-panel {
|
||||||
width: 260px;
|
width: 264px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
border-left: 1px solid rgb(255 255 255 / 0.08);
|
border-left: 3px solid var(--ink);
|
||||||
background: #12141b;
|
background: var(--surface);
|
||||||
padding: 14px;
|
padding: 16px 14px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 9px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
.cv-panel__title {
|
.cv-panel__title {
|
||||||
font-size: 14px;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 800;
|
||||||
color: #e8eaf0;
|
color: var(--fg);
|
||||||
|
display: inline-block;
|
||||||
|
background: linear-gradient(transparent 55%, var(--m-yellow) 55%);
|
||||||
|
align-self: flex-start;
|
||||||
|
padding: 0 2px;
|
||||||
}
|
}
|
||||||
.cv-label {
|
.cv-label {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #7f8798;
|
font-weight: 700;
|
||||||
|
color: var(--fg-3);
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
.cv-input {
|
.cv-input {
|
||||||
@@ -914,35 +1079,49 @@ const saveLabel = computed(() => ({
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.cv-chip {
|
.cv-chip {
|
||||||
padding: 3px 10px;
|
padding: 3px 11px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
border: 1px solid rgb(255 255 255 / 0.14);
|
border: 2.5px solid var(--ink);
|
||||||
background: transparent;
|
background: var(--surface);
|
||||||
color: #b6bccb;
|
color: var(--fg);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.cv-chip:hover {
|
||||||
|
background: var(--m-blue);
|
||||||
|
}
|
||||||
.cv-chip.is-on {
|
.cv-chip.is-on {
|
||||||
border-color: #7c6cff;
|
background: var(--m-yellow);
|
||||||
color: #a99dff;
|
box-shadow: 2.5px 2.5px 0 var(--shadow-ink);
|
||||||
background: rgb(124 108 255 / 0.12);
|
|
||||||
}
|
}
|
||||||
.cv-refs {
|
.cv-refs {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #7f8798;
|
font-weight: 600;
|
||||||
border: 1px dashed rgb(255 255 255 / 0.14);
|
color: var(--fg-2);
|
||||||
border-radius: 8px;
|
border: 2px dashed var(--ink-soft);
|
||||||
|
border-radius: 9px;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
}
|
}
|
||||||
.cv-gen {
|
.cv-gen {
|
||||||
padding: 9px;
|
padding: 10px;
|
||||||
border-radius: 9px;
|
border-radius: 11px;
|
||||||
border: none;
|
border: 2.5px solid var(--ink);
|
||||||
background: linear-gradient(135deg, #7c6cff, #5d8bff);
|
background: var(--m-orange);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 800;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
box-shadow: 4px 4px 0 var(--shadow-ink);
|
||||||
|
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
||||||
|
}
|
||||||
|
.cv-gen:hover:not(:disabled) {
|
||||||
|
transform: translate(-1px, -1px);
|
||||||
|
box-shadow: 5px 5px 0 var(--shadow-ink);
|
||||||
|
}
|
||||||
|
.cv-gen:active:not(:disabled) {
|
||||||
|
transform: translate(3px, 3px);
|
||||||
|
box-shadow: 1px 1px 0 var(--shadow-ink);
|
||||||
}
|
}
|
||||||
.cv-gen:disabled {
|
.cv-gen:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
@@ -950,28 +1129,30 @@ const saveLabel = computed(() => ({
|
|||||||
}
|
}
|
||||||
.cv-genmsg {
|
.cv-genmsg {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #9aa1b2;
|
color: var(--fg-2);
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
.cv-nomodels {
|
.cv-nomodels {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #d8a657;
|
font-weight: 600;
|
||||||
|
color: #92400e;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
border: 1px dashed rgb(216 166 87 / 0.4);
|
border: 2px dashed #f59e0b;
|
||||||
border-radius: 8px;
|
border-radius: 9px;
|
||||||
|
background: var(--m-yellow);
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
}
|
}
|
||||||
.cv-tips {
|
.cv-tips {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #5f6675;
|
color: var(--fg-faint);
|
||||||
line-height: 1.6;
|
line-height: 1.7;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cv-lightbox {
|
.cv-lightbox {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: rgb(4 5 8 / 0.9);
|
background: rgb(17 17 17 / 0.75);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -981,13 +1162,15 @@ const saveLabel = computed(() => ({
|
|||||||
.cv-lightbox img {
|
.cv-lightbox img {
|
||||||
max-width: 92vw;
|
max-width: 92vw;
|
||||||
max-height: 92vh;
|
max-height: 92vh;
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
|
border: 3px solid #fff;
|
||||||
|
box-shadow: 10px 10px 0 var(--m-orange);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cv-picker {
|
.cv-picker {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: rgb(4 5 8 / 0.6);
|
background: rgb(17 17 17 / 0.45);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -996,36 +1179,46 @@ const saveLabel = computed(() => ({
|
|||||||
.cv-picker__box {
|
.cv-picker__box {
|
||||||
width: min(720px, 92vw);
|
width: min(720px, 92vw);
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
background: #161922;
|
background: var(--surface);
|
||||||
border: 1px solid rgb(255 255 255 / 0.1);
|
border: 3px solid var(--ink);
|
||||||
border-radius: 12px;
|
border-radius: 16px;
|
||||||
padding: 14px;
|
box-shadow: 8px 8px 0 var(--shadow-ink);
|
||||||
|
padding: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
.cv-picker__head {
|
.cv-picker__head {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 13px;
|
gap: 10px;
|
||||||
color: #e8eaf0;
|
font-size: 14px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
.cv-picker__acts {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.cv-picker__grid {
|
.cv-picker__grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
.cv-picker__grid img {
|
.cv-picker__grid img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border-radius: 8px;
|
border-radius: 10px;
|
||||||
|
border: 2.5px solid var(--ink);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: #0e1015;
|
background: var(--surface-2);
|
||||||
|
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
||||||
}
|
}
|
||||||
.cv-picker__grid img:hover {
|
.cv-picker__grid img:hover {
|
||||||
outline: 2px solid #7c6cff;
|
transform: translate(-2px, -2px);
|
||||||
|
box-shadow: 4px 4px 0 var(--m-orange);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -155,8 +155,8 @@ const pageNumbers = computed(() => {
|
|||||||
<div class="text-2xl font-semibold mt-1 tabular-nums text-white/60">{{ stats.redeemed }}</div>
|
<div class="text-2xl font-semibold mt-1 tabular-nums text-white/60">{{ stats.redeemed }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card p-4">
|
<div class="card p-4">
|
||||||
<div class="text-[11px] uppercase tracking-wider text-fuchsia-300/80">未使用面额</div>
|
<div class="text-[11px] uppercase tracking-wider text-rose-300/80">未使用面额</div>
|
||||||
<div class="text-2xl font-semibold mt-1 tabular-nums text-fuchsia-300">{{ Number(stats.active_amount || 0).toLocaleString('en-US') }}</div>
|
<div class="text-2xl font-semibold mt-1 tabular-nums text-rose-300">{{ Number(stats.active_amount || 0).toLocaleString('en-US') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -178,12 +178,12 @@ const pageNumbers = computed(() => {
|
|||||||
<label class="block text-xs text-white/55 mb-1.5">类型</label>
|
<label class="block text-xs text-white/55 mb-1.5">类型</label>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<button type="button" @click="form.type = 'normal'" class="fp" :class="form.type === 'normal' && 'fp-on'">普通</button>
|
<button type="button" @click="form.type = 'normal'" class="fp" :class="form.type === 'normal' && 'fp-on'">普通</button>
|
||||||
<button type="button" @click="form.type = 'marketing'" class="fp" :class="form.type === 'marketing' && 'fp-fuchsia'">营销</button>
|
<button type="button" @click="form.type = 'marketing'" class="fp" :class="form.type === 'marketing' && 'fp-rose'">营销</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button @click="generate" class="btn-primary"><Icon name="plus" class="w-3.5 h-3.5" /> 生成</button>
|
<button @click="generate" class="btn-primary"><Icon name="plus" class="w-3.5 h-3.5" /> 生成</button>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="form.type === 'marketing'" class="text-[11px] text-fuchsia-300/80 mt-2">营销兑换码:同一批次每个用户只能兑换一次。</p>
|
<p v-if="form.type === 'marketing'" class="text-[11px] text-rose-300/80 mt-2">营销兑换码:同一批次每个用户只能兑换一次。</p>
|
||||||
|
|
||||||
<!-- last batch -->
|
<!-- last batch -->
|
||||||
<div v-if="lastBatch.length" class="mt-4 rounded-xl bg-white/[0.04] ring-1 ring-white/10 p-4">
|
<div v-if="lastBatch.length" class="mt-4 rounded-xl bg-white/[0.04] ring-1 ring-white/10 p-4">
|
||||||
@@ -202,8 +202,8 @@ const pageNumbers = computed(() => {
|
|||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<button @click="setFilter(() => typeFilter = '')" class="fp" :class="typeFilter === '' && 'fp-on'">全部类型</button>
|
<button @click="setFilter(() => typeFilter = '')" class="fp" :class="typeFilter === '' && 'fp-on'">全部类型</button>
|
||||||
<button @click="setFilter(() => typeFilter = 'normal')" class="fp" :class="typeFilter === 'normal' && 'fp-on'">普通</button>
|
<button @click="setFilter(() => typeFilter = 'normal')" class="fp" :class="typeFilter === 'normal' && 'fp-on'">普通</button>
|
||||||
<button @click="setFilter(() => typeFilter = 'marketing')" class="fp" :class="typeFilter === 'marketing' && 'fp-fuchsia'">
|
<button @click="setFilter(() => typeFilter = 'marketing')" class="fp" :class="typeFilter === 'marketing' && 'fp-rose'">
|
||||||
<span class="w-1.5 h-1.5 rounded-full bg-fuchsia-400"></span>营销
|
<span class="w-1.5 h-1.5 rounded-full bg-rose-400"></span>营销
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-px h-5 bg-white/10"></div>
|
<div class="w-px h-5 bg-white/10"></div>
|
||||||
@@ -266,7 +266,7 @@ const pageNumbers = computed(() => {
|
|||||||
<td class="px-5 py-3.5 align-middle font-mono text-xs text-white/90 truncate" :title="c.code">
|
<td class="px-5 py-3.5 align-middle font-mono text-xs text-white/90 truncate" :title="c.code">
|
||||||
<span class="inline-flex items-center gap-2">
|
<span class="inline-flex items-center gap-2">
|
||||||
<span class="truncate">{{ c.code }}</span>
|
<span class="truncate">{{ c.code }}</span>
|
||||||
<span v-if="c.type === 'marketing'" class="shrink-0 inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-sans font-medium bg-fuchsia-500/15 text-fuchsia-300 ring-1 ring-fuchsia-400/25">营销</span>
|
<span v-if="c.type === 'marketing'" class="shrink-0 inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-sans font-medium bg-rose-500/15 text-rose-300 ring-1 ring-rose-400/25">营销</span>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-3.5 align-middle text-right tabular-nums text-white/85 whitespace-nowrap">
|
<td class="px-3 py-3.5 align-middle text-right tabular-nums text-white/85 whitespace-nowrap">
|
||||||
@@ -365,10 +365,10 @@ const pageNumbers = computed(() => {
|
|||||||
color: rgb(110 231 183);
|
color: rgb(110 231 183);
|
||||||
box-shadow: inset 0 0 0 1px rgb(110 231 183 / 0.45);
|
box-shadow: inset 0 0 0 1px rgb(110 231 183 / 0.45);
|
||||||
}
|
}
|
||||||
.fp-fuchsia {
|
.fp-rose {
|
||||||
background: rgb(217 70 239 / 0.22);
|
background: rgb(244 63 94 / 0.22);
|
||||||
color: rgb(245 208 254);
|
color: rgb(254 205 211);
|
||||||
box-shadow: inset 0 0 0 1px rgb(245 208 254 / 0.45);
|
box-shadow: inset 0 0 0 1px rgb(254 205 211 / 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pg {
|
.pg {
|
||||||
|
|||||||
@@ -81,8 +81,8 @@ onMounted(load)
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-3.5 align-middle text-right tabular-nums text-white/70">{{ g.user_count }}</td>
|
<td class="px-3 py-3.5 align-middle text-right tabular-nums text-white/70">{{ g.user_count }}</td>
|
||||||
<td class="px-3 py-3.5 align-middle">
|
<td class="px-3 py-3.5 align-middle">
|
||||||
<span v-if="g.is_default" class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium bg-fuchsia-500/10 text-fuchsia-300 ring-1 ring-fuchsia-400/30">
|
<span v-if="g.is_default" class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium bg-rose-500/10 text-rose-300 ring-1 ring-rose-400/30">
|
||||||
<span class="w-1.5 h-1.5 rounded-full bg-fuchsia-400"></span> 默认
|
<span class="w-1.5 h-1.5 rounded-full bg-rose-400"></span> 默认
|
||||||
</span>
|
</span>
|
||||||
<button v-else @click="setDefault(g)" class="btn-soft text-xs">设为默认</button>
|
<button v-else @click="setDefault(g)" class="btn-soft text-xs">设为默认</button>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ onMounted(() => { loadSite(); loadReg(); loadSmtp(); loadCredits(); loadAnnounce
|
|||||||
<div @click="pickLogo" @drop="onLogoDrop" @dragover.prevent="logoDragOver = true" @dragleave="logoDragOver = false"
|
<div @click="pickLogo" @drop="onLogoDrop" @dragover.prevent="logoDragOver = true" @dragleave="logoDragOver = false"
|
||||||
title="点击或拖拽图片替换"
|
title="点击或拖拽图片替换"
|
||||||
class="w-16 h-16 rounded-xl grid place-items-center overflow-hidden shrink-0 cursor-pointer transition-all"
|
class="w-16 h-16 rounded-xl grid place-items-center overflow-hidden shrink-0 cursor-pointer transition-all"
|
||||||
:class="logoDragOver ? 'ring-2 ring-indigo-400 bg-indigo-50/40' : ''">
|
:class="logoDragOver ? 'ring-2 ring-sky-400 bg-sky-50/40' : ''">
|
||||||
<img v-if="logoPreview" :src="logoPreview" class="w-full h-full object-cover" />
|
<img v-if="logoPreview" :src="logoPreview" class="w-full h-full object-cover" />
|
||||||
<Logo v-else :size="64" class="w-full h-full" />
|
<Logo v-else :size="64" class="w-full h-full" />
|
||||||
</div>
|
</div>
|
||||||
@@ -656,31 +656,29 @@ html.dark .hint b { color: rgb(253 164 175); } /* rose — visible warning, no
|
|||||||
box-shadow: 0 1px 2px rgb(15 23 42 / 0.2);
|
box-shadow: 0 1px 2px rgb(15 23 42 / 0.2);
|
||||||
transition: transform 0.18s ease;
|
transition: transform 0.18s ease;
|
||||||
}
|
}
|
||||||
.sw:checked { background: #4f46e5; } /* indigo-600 — matches btn-primary */
|
.sw:checked { background: #4f46e5; } /* sky-600 — matches btn-primary */
|
||||||
.sw:checked::after { transform: translateX(calc(2.25rem - 1.3rem)); }
|
.sw:checked::after { transform: translateX(calc(2.25rem - 1.3rem)); }
|
||||||
.sw:focus-visible { box-shadow: 0 0 0 3px rgb(99 102 241 / 0.25); }
|
.sw:focus-visible { box-shadow: 0 0 0 3px rgb(236 91 19 / 0.3); }
|
||||||
.sw:disabled { cursor: not-allowed; opacity: 0.55; }
|
.sw:disabled { cursor: not-allowed; opacity: 0.55; }
|
||||||
/* All three input variants now share the same dark-glass surface as the rest
|
/* All three input variants now share the same surface as the rest of the
|
||||||
of the admin shell. Background + border use white-alpha so they read
|
admin shell. Background + border use ink-alpha so they read against the
|
||||||
against the card; placeholders are tuned for legibility, not noise. */
|
card; placeholders are tuned for legibility, not noise. */
|
||||||
.num, .txt, .field {
|
.num, .txt, .field {
|
||||||
background: rgb(15 23 42 / 0.03);
|
background: rgb(15 23 42 / 0.03);
|
||||||
border: 1px solid var(--hairline);
|
border: 2px solid var(--ink-soft);
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
border-radius: 0.55rem;
|
border-radius: 0.55rem;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: border-color 0.18s, background 0.18s, box-shadow 0.18s;
|
transition: border-color 0.15s, background 0.15s, box-shadow 0.15s;
|
||||||
}
|
}
|
||||||
html.dark .num, html.dark .txt, html.dark .field {
|
html.dark .num, html.dark .txt, html.dark .field {
|
||||||
background: rgb(255 255 255 / 0.04);
|
background: rgb(255 255 255 / 0.04);
|
||||||
border-color: rgb(255 255 255 / 0.1);
|
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.num::placeholder, .txt::placeholder, .field::placeholder { color: var(--fg-faint); }
|
.num::placeholder, .txt::placeholder, .field::placeholder { color: var(--fg-faint); }
|
||||||
.num:focus, .txt:focus, .field:focus {
|
.num:focus, .txt:focus, .field:focus {
|
||||||
border-color: rgb(167 139 250 / 0.65);
|
border-color: var(--ink);
|
||||||
background: rgb(255 255 255 / 0.06);
|
box-shadow: 3px 3px 0 var(--m-orange);
|
||||||
box-shadow: 0 0 0 3px rgb(167 139 250 / 0.15);
|
|
||||||
}
|
}
|
||||||
.num:disabled, .txt:disabled, .field:disabled { opacity: 0.45; cursor: not-allowed; }
|
.num:disabled, .txt:disabled, .field:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||||
.num { width: 6rem; padding: 0.4rem 0.55rem; font-size: 0.8rem; text-align: right; -moz-appearance: textfield; appearance: textfield; }
|
.num { width: 6rem; padding: 0.4rem 0.55rem; font-size: 0.8rem; text-align: right; -moz-appearance: textfield; appearance: textfield; }
|
||||||
@@ -688,23 +686,24 @@ html.dark .num, html.dark .txt, html.dark .field {
|
|||||||
.txt { width: 16rem; max-width: 60%; padding: 0.4rem 0.65rem; font-size: 0.8rem; }
|
.txt { width: 16rem; max-width: 60%; padding: 0.4rem 0.65rem; font-size: 0.8rem; }
|
||||||
.field { width: 100%; padding: 0.55rem 0.75rem; font-size: 0.85rem; }
|
.field { width: 100%; padding: 0.55rem 0.75rem; font-size: 0.85rem; }
|
||||||
|
|
||||||
/* Section save buttons — solid violet to match brand. The global .btn-primary
|
/* Section save buttons — solid memphis orange with a hard offset shadow.
|
||||||
under .public-dark goes to white; we override here so the save action stays
|
The global .btn-primary under .public-dark already uses this palette; we
|
||||||
visually distinct as the primary action on a form. */
|
re-assert it here so the save action stays visually distinct. */
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
padding: 0.55rem 1.15rem; border-radius: 0.6rem;
|
padding: 0.55rem 1.15rem; border-radius: 0.6rem;
|
||||||
font-size: 0.8rem; font-weight: 600; color: white;
|
font-size: 0.8rem; font-weight: 800; color: white;
|
||||||
background: linear-gradient(135deg, #a855f7 0%, #7c3aed 50%, #ec4899 100%);
|
background: var(--m-orange);
|
||||||
box-shadow: 0 8px 20px -8px rgb(168 85 247 / 0.55);
|
border: 2px solid var(--ink);
|
||||||
transition: filter 0.15s, transform 0.12s, box-shadow 0.18s, opacity 0.15s;
|
box-shadow: 3px 3px 0 var(--shadow-ink);
|
||||||
|
transition: filter 0.15s, transform 0.12s, box-shadow 0.15s, opacity 0.15s;
|
||||||
}
|
}
|
||||||
/* Re-assert the gradient on hover for ALL states: the global
|
/* Re-assert the flat orange on hover for ALL states — the global
|
||||||
`.public-dark .btn-primary:hover` in style.css repaints the background white
|
`.public-dark .btn-primary:hover` in style.css repaints the background,
|
||||||
with no `:not(:disabled)` guard, so even disabled save buttons (注册与登录 /
|
so even disabled save buttons (注册与登录 / 邮件服务 default to disabled)
|
||||||
邮件服务 default to disabled) flashed white on hover. Keep this rule
|
would flash on hover. Keep this rule unconditional; only the
|
||||||
unconditional; only the brightness/lift below is gated on :not(:disabled). */
|
brightness/lift below is gated on :not(:disabled). */
|
||||||
.btn-primary:hover { background: linear-gradient(135deg, #a855f7 0%, #7c3aed 50%, #ec4899 100%); }
|
.btn-primary:hover { background: #f97316; }
|
||||||
.btn-primary:hover:not(:disabled) { filter: brightness(1.08); box-shadow: 0 10px 24px -8px rgb(168 85 247 / 0.7); }
|
.btn-primary:hover:not(:disabled) { transform: translate(-1px, -1px); box-shadow: 4px 4px 0 var(--shadow-ink); }
|
||||||
.btn-primary:active:not(:disabled) { transform: translateY(1px); }
|
.btn-primary:active:not(:disabled) { transform: translate(2px, 2px); box-shadow: none; }
|
||||||
.btn-primary:disabled { opacity: 0.45; cursor: not-allowed; box-shadow: none; }
|
.btn-primary:disabled { opacity: 0.45; cursor: not-allowed; box-shadow: none; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ async function copy(text) {
|
|||||||
<dt class="text-white/45">你的 Key</dt><dd class="font-mono text-white/70">{{ keyHint }}</dd>
|
<dt class="text-white/45">你的 Key</dt><dd class="font-mono text-white/70">{{ keyHint }}</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
<p class="text-[11px] text-white/40 mt-4">还没有 Key?去 <router-link to="/settings" class="text-violet-300 underline">设置 → API Key</router-link> 生成。</p>
|
<p class="text-[11px] text-white/40 mt-4">还没有 Key?去 <router-link to="/settings" class="text-orange-300 underline">设置 → API Key</router-link> 生成。</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card p-6">
|
<div class="card p-6">
|
||||||
@@ -528,15 +528,15 @@ html.dark .badge-err { background: rgb(244 63 94 / 0.15); color: rgb(253 164 175
|
|||||||
|
|
||||||
.cap { display: inline-flex; align-items: center; padding: 0.18rem 0.55rem; font-size: 0.7rem; border-radius: 9999px; white-space: nowrap; }
|
.cap { display: inline-flex; align-items: center; padding: 0.18rem 0.55rem; font-size: 0.7rem; border-radius: 9999px; white-space: nowrap; }
|
||||||
.cap-k { background: rgb(16 185 129 / 0.12); color: rgb(4 120 87); box-shadow: inset 0 0 0 1px rgb(16 185 129 / 0.3); }
|
.cap-k { background: rgb(16 185 129 / 0.12); color: rgb(4 120 87); box-shadow: inset 0 0 0 1px rgb(16 185 129 / 0.3); }
|
||||||
.cap-dur { background: rgb(99 102 241 / 0.12); color: rgb(55 48 163); box-shadow: inset 0 0 0 1px rgb(99 102 241 / 0.3); }
|
.cap-dur { background: rgb(14 165 233 / 0.12); color: rgb(55 48 163); box-shadow: inset 0 0 0 1px rgb(14 165 233 / 0.3); }
|
||||||
.cap-frame { background: rgb(236 72 153 / 0.12); color: rgb(159 18 57); box-shadow: inset 0 0 0 1px rgb(236 72 153 / 0.3); }
|
.cap-frame { background: rgb(251 113 133 / 0.12); color: rgb(159 18 57); box-shadow: inset 0 0 0 1px rgb(251 113 133 / 0.3); }
|
||||||
.cap-ref { background: rgb(16 185 129 / 0.12); color: rgb(4 120 87); box-shadow: inset 0 0 0 1px rgb(16 185 129 / 0.3); }
|
.cap-ref { background: rgb(16 185 129 / 0.12); color: rgb(4 120 87); box-shadow: inset 0 0 0 1px rgb(16 185 129 / 0.3); }
|
||||||
.cap-media { background: rgb(245 158 11 / 0.12); color: rgb(146 64 14); box-shadow: inset 0 0 0 1px rgb(245 158 11 / 0.3); }
|
.cap-media { background: rgb(245 158 11 / 0.12); color: rgb(146 64 14); box-shadow: inset 0 0 0 1px rgb(245 158 11 / 0.3); }
|
||||||
.cap-price { background: rgb(14 165 233 / 0.12); color: rgb(3 105 161); box-shadow: inset 0 0 0 1px rgb(14 165 233 / 0.3); font-variant-numeric: tabular-nums; }
|
.cap-price { background: rgb(14 165 233 / 0.12); color: rgb(3 105 161); box-shadow: inset 0 0 0 1px rgb(14 165 233 / 0.3); font-variant-numeric: tabular-nums; }
|
||||||
.cap-ratio { background: rgb(100 116 139 / 0.1); color: rgb(51 65 85); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; box-shadow: inset 0 0 0 1px rgb(100 116 139 / 0.25); }
|
.cap-ratio { background: rgb(100 116 139 / 0.1); color: rgb(51 65 85); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; box-shadow: inset 0 0 0 1px rgb(100 116 139 / 0.25); }
|
||||||
html.dark .cap-k { background: rgb(16 185 129 / 0.18); color: rgb(110 231 183); box-shadow: inset 0 0 0 1px rgb(52 211 153 / 0.4); }
|
html.dark .cap-k { background: rgb(16 185 129 / 0.18); color: rgb(110 231 183); box-shadow: inset 0 0 0 1px rgb(52 211 153 / 0.4); }
|
||||||
html.dark .cap-dur { background: rgb(99 102 241 / 0.18); color: rgb(165 180 252); box-shadow: inset 0 0 0 1px rgb(129 140 248 / 0.4); }
|
html.dark .cap-dur { background: rgb(14 165 233 / 0.18); color: rgb(147 197 253); box-shadow: inset 0 0 0 1px rgb(129 140 248 / 0.4); }
|
||||||
html.dark .cap-frame { background: rgb(236 72 153 / 0.18); color: rgb(244 114 182); box-shadow: inset 0 0 0 1px rgb(244 114 182 / 0.45); }
|
html.dark .cap-frame { background: rgb(251 113 133 / 0.18); color: rgb(244 114 182); box-shadow: inset 0 0 0 1px rgb(244 114 182 / 0.45); }
|
||||||
html.dark .cap-ref { background: rgb(16 185 129 / 0.18); color: rgb(110 231 183); box-shadow: inset 0 0 0 1px rgb(52 211 153 / 0.4); }
|
html.dark .cap-ref { background: rgb(16 185 129 / 0.18); color: rgb(110 231 183); box-shadow: inset 0 0 0 1px rgb(52 211 153 / 0.4); }
|
||||||
html.dark .cap-media { background: rgb(245 158 11 / 0.18); color: rgb(252 211 77); box-shadow: inset 0 0 0 1px rgb(252 211 77 / 0.45); }
|
html.dark .cap-media { background: rgb(245 158 11 / 0.18); color: rgb(252 211 77); box-shadow: inset 0 0 0 1px rgb(252 211 77 / 0.45); }
|
||||||
html.dark .cap-price { background: rgb(14 165 233 / 0.18); color: rgb(125 211 252); box-shadow: inset 0 0 0 1px rgb(56 189 248 / 0.45); }
|
html.dark .cap-price { background: rgb(14 165 233 / 0.18); color: rgb(125 211 252); box-shadow: inset 0 0 0 1px rgb(56 189 248 / 0.45); }
|
||||||
|
|||||||
+186
-103
@@ -4,8 +4,6 @@ import { useRouter } from 'vue-router'
|
|||||||
import { api, generatedUrl } from '../api'
|
import { api, generatedUrl } from '../api'
|
||||||
import { site } from '../site'
|
import { site } from '../site'
|
||||||
import { isAuthed, openLogin } from '../auth'
|
import { isAuthed, openLogin } from '../auth'
|
||||||
import Mascot from '../components/Mascot.vue'
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
// Navigate to a signed-in page, or pop the login modal (remembering the
|
// Navigate to a signed-in page, or pop the login modal (remembering the
|
||||||
@@ -123,32 +121,32 @@ const caps = computed(() => {
|
|||||||
{
|
{
|
||||||
key: 't2i', title: '文字生图', model: imageModels.value[0]?.id,
|
key: 't2i', title: '文字生图', model: imageModels.value[0]?.id,
|
||||||
desc: '一句话出图,画幅与分辨率按所选模型的可用范围给出。',
|
desc: '一句话出图,画幅与分辨率按所选模型的可用范围给出。',
|
||||||
tint: 'rgb(52 211 153 / 0.14)', color: 'rgb(52 211 153)',
|
tint: '#a7f3d0', color: '#065f46',
|
||||||
metas: [resRange.value, `${imageModels.value.length} 个模型`].filter(Boolean),
|
metas: [resRange.value, `${imageModels.value.length} 个模型`].filter(Boolean),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'i2i', title: '参考图生图', model: i2iModels[0]?.id,
|
key: 'i2i', title: '参考图生图', model: i2iModels[0]?.id,
|
||||||
desc: '上传参考图换风格、改构图、做变体,支持多张参考。',
|
desc: '上传参考图换风格、改构图、做变体,支持多张参考。',
|
||||||
tint: 'rgb(217 70 239 / 0.14)', color: 'rgb(232 121 249)',
|
tint: '#ffc1cc', color: '#9f1239',
|
||||||
metas: ['多图参考', `${i2iModels.length} 个模型`],
|
metas: ['多图参考', `${i2iModels.length} 个模型`],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 't2v', title: '文字生视频', model: videoModels.value[0]?.id,
|
key: 't2v', title: '文字生视频', model: videoModels.value[0]?.id,
|
||||||
desc: '一段描述生成短片,时长与分辨率按模型能力可选。',
|
desc: '一段描述生成短片,时长与分辨率按模型能力可选。',
|
||||||
tint: 'rgb(14 165 233 / 0.14)', color: 'rgb(56 189 248)',
|
tint: '#bae6fd', color: '#075985',
|
||||||
metas: [durationRange.value, videoRes.value, `${videoModels.value.length} 个模型`].filter(Boolean),
|
metas: [durationRange.value, videoRes.value, `${videoModels.value.length} 个模型`].filter(Boolean),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'i2v', title: '图生视频', model: i2vModels[0]?.id,
|
key: 'i2v', title: '图生视频', model: i2vModels[0]?.id,
|
||||||
desc: '给参考图或首帧,让静态画面动起来。',
|
desc: '给参考图或首帧,让静态画面动起来。',
|
||||||
tint: 'rgb(245 158 11 / 0.14)', color: 'rgb(251 191 36)',
|
tint: '#fef08a', color: '#92400e',
|
||||||
metas: ['参考图', `${i2vModels.length} 个模型`],
|
metas: ['参考图', `${i2vModels.length} 个模型`],
|
||||||
},
|
},
|
||||||
].filter((c) => c.model)
|
].filter((c) => c.model)
|
||||||
})
|
})
|
||||||
|
|
||||||
// ---- model cards: the admin-set alias wins, so no upstream naming leaks ----
|
// ---- model cards: the admin-set alias wins, so no upstream naming leaks ----
|
||||||
const MODEL_TINT = ['#a78bfa', '#f0abfc', '#fb7185', '#fbbf24', '#38bdf8', '#34d399', '#94a3b8']
|
const MODEL_TINT = ['#ec5b13', '#ffc1cc', '#bae6fd', '#a7f3d0', '#fef08a', '#f472b6', '#94a3b8']
|
||||||
const modelCards = computed(() => managed.value.map((m, i) => {
|
const modelCards = computed(() => managed.value.map((m, i) => {
|
||||||
const bits = []
|
const bits = []
|
||||||
if (m.resolutions?.length) bits.push(m.resolutions.join(' / '))
|
if (m.resolutions?.length) bits.push(m.resolutions.join(' / '))
|
||||||
@@ -199,38 +197,43 @@ function useExample(ex) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="px-4 md:px-8">
|
<div class="px-4 md:px-8">
|
||||||
<!-- ============ HERO (centered, mascot-forward) ============ -->
|
<!-- ============ HERO (centered, memphis deco) ============ -->
|
||||||
<section class="hero-band relative overflow-hidden pt-16 md:pt-24 pb-10">
|
<section class="hero-band relative overflow-hidden pt-14 md:pt-20 pb-12">
|
||||||
<div class="anime-stars"></div>
|
<div class="anime-stars"></div>
|
||||||
<div class="relative flex flex-col items-center text-center">
|
|
||||||
<!-- mascot centerpiece with soft aura -->
|
<!-- floating memphis shapes (desktop only) -->
|
||||||
<div class="relative mb-6">
|
<div aria-hidden="true" class="pointer-events-none absolute inset-0 hidden lg:block">
|
||||||
<span class="mascot-aura"></span>
|
<span class="deco deco-a">🎨</span>
|
||||||
<Mascot :size="184" class="relative" />
|
<span class="deco deco-b">⚡</span>
|
||||||
|
<span class="deco deco-c">4K</span>
|
||||||
|
<span class="deco deco-d">🎬</span>
|
||||||
|
<span class="deco deco-star">✦</span>
|
||||||
|
<span class="deco deco-star deco-star-2">✦</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="relative flex flex-col items-center text-center">
|
||||||
<span class="sticker">
|
<span class="sticker">
|
||||||
<span class="inline-block w-1.5 h-1.5 rounded-full bg-emerald-400 shadow-[0_0_8px_rgb(52_211_153)]"></span>
|
<span class="inline-block w-1.5 h-1.5 rounded-full bg-emerald-500 border border-[#111]"></span>
|
||||||
{{ modelCount }} 个模型在线 · 图像与视频一站生成
|
{{ modelCount }} 个模型在线 · 图像与视频一站生成
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<h1 class="mt-5 font-extrabold tracking-tight leading-[1.05] text-[clamp(2.6rem,6vw,4.6rem)] text-[color:var(--fg)]">
|
<h1 class="mt-5 font-black tracking-tight leading-[1.05] text-[clamp(2.6rem,6vw,4.6rem)] text-[color:var(--fg)]">
|
||||||
把想象 <span class="anime-grad-text">说给 AI 听</span>
|
把想象 <span class="anime-grad-text">说给 AI 听</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p class="mt-5 text-[15px] md:text-base text-[color:var(--fg-2)] max-w-xl leading-relaxed">
|
<p class="mt-5 text-[15px] md:text-base font-medium text-[color:var(--fg-2)] max-w-xl leading-relaxed">
|
||||||
写一句话,剩下的交给我们:自动选模型、自动排队,图像与视频都在同一个工作台里出片。
|
写一句话,剩下的交给我们:自动选模型、自动排队,图像与视频都在同一个工作台里出片。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- prompt composer — the shortest path into the workbench -->
|
<!-- prompt composer — the shortest path into the workbench -->
|
||||||
<form class="composer mt-8 !max-w-2xl w-full" @submit.prevent="submitPrompt">
|
<form class="composer mt-9 !max-w-2xl w-full" @submit.prevent="submitPrompt">
|
||||||
<input v-model="prompt" class="composer-input" type="text"
|
<input v-model="prompt" class="composer-input" type="text"
|
||||||
placeholder="描述你想要的画面,比如:雨夜的东京小巷,霓虹倒映在湿滑路面…" />
|
placeholder="描述你想要的画面,比如:雨夜的东京小巷,霓虹倒映在湿滑路面…" />
|
||||||
<button type="submit" class="composer-go">✦ 生成</button>
|
<button type="submit" class="composer-go">✦ 生成</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- quick starters come from the curated bento set -->
|
<!-- quick starters come from the curated bento set -->
|
||||||
<div v-if="bento.length" class="mt-4 flex flex-wrap justify-center gap-2 max-w-2xl">
|
<div v-if="bento.length" class="mt-5 flex flex-wrap justify-center gap-2 max-w-2xl">
|
||||||
<button v-for="ex in bento.slice(0, 5)" :key="ex.id" type="button" class="chip"
|
<button v-for="ex in bento.slice(0, 5)" :key="ex.id" type="button" class="chip"
|
||||||
@click="useExample(ex)">{{ ex.title }}</button>
|
@click="useExample(ex)">{{ ex.title }}</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -238,7 +241,7 @@ function useExample(ex) {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ============ GALLERY MARQUEE (hero + bento imagery) ============ -->
|
<!-- ============ GALLERY MARQUEE (hero + bento imagery) ============ -->
|
||||||
<section v-if="gallery.length" class="mt-2 -mx-4 md:-mx-8 overflow-hidden marquee-mask">
|
<section v-if="gallery.length" class="mt-6 -mx-4 md:-mx-8 overflow-hidden marquee-mask border-y-[3px] border-y-[color:var(--ink)] bg-[var(--surface)] py-4">
|
||||||
<div class="marquee flex gap-4 px-4 md:px-8 w-max">
|
<div class="marquee flex gap-4 px-4 md:px-8 w-max">
|
||||||
<button v-for="(card, i) in [...gallery, ...gallery]" :key="card.id + '-' + i" type="button"
|
<button v-for="(card, i) in [...gallery, ...gallery]" :key="card.id + '-' + i" type="button"
|
||||||
class="gcard" :style="cardBg(card)" :aria-label="card.title" @click="useExample(card)">
|
class="gcard" :style="cardBg(card)" :aria-label="card.title" @click="useExample(card)">
|
||||||
@@ -254,23 +257,23 @@ function useExample(ex) {
|
|||||||
<!-- ============ KPI STRIP ============ -->
|
<!-- ============ KPI STRIP ============ -->
|
||||||
<section class="mt-12 grid grid-cols-2 md:grid-cols-4 gap-3">
|
<section class="mt-12 grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||||
<div v-for="k in [
|
<div v-for="k in [
|
||||||
{ v: modelCount, l: '在线模型', e: '🧩' },
|
{ v: modelCount, l: '在线模型', e: '🧩', bg: 'var(--m-yellow)' },
|
||||||
{ v: caps.length, l: '创作方式', e: '🎨' },
|
{ v: caps.length, l: '创作方式', e: '🎨', bg: 'var(--m-pink)' },
|
||||||
{ v: resRange, l: '输出分辨率', e: '🖼️' },
|
{ v: resRange, l: '输出分辨率', e: '🖼️', bg: 'var(--m-blue)' },
|
||||||
{ v: avgLabel, l: '平均出片 · 近 24h', e: '⚡' },
|
{ v: avgLabel, l: '平均出片 · 近 24h', e: '⚡', bg: 'var(--m-mint)' },
|
||||||
]" :key="k.l"
|
]" :key="k.l"
|
||||||
class="anime-card px-5 py-4">
|
class="kpi-card px-5 py-4" :style="{ background: k.bg }">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-lg">{{ k.e }}</span>
|
<span class="text-lg">{{ k.e }}</span>
|
||||||
<span class="text-[26px] font-bold tabular-nums text-[color:var(--fg)]">{{ k.v }}</span>
|
<span class="text-[26px] font-black tabular-nums text-[#111]">{{ k.v }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1.5 text-[10px] uppercase tracking-[0.18em] text-[color:var(--fg-3)]">{{ k.l }}</div>
|
<div class="mt-1.5 text-[10px] font-bold uppercase tracking-[0.18em] text-[#111]/60">{{ k.l }}</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ============ CAPABILITIES ============ -->
|
<!-- ============ CAPABILITIES ============ -->
|
||||||
<section class="mt-24">
|
<section class="mt-24">
|
||||||
<div class="anime-eyebrow text-violet-500 dark:text-violet-300/80">能力</div>
|
<div class="anime-eyebrow text-[var(--m-orange)]">能力</div>
|
||||||
<h2 class="mt-3 text-[clamp(1.65rem,3vw,2.3rem)] font-extrabold tracking-tight text-[color:var(--fg)]">
|
<h2 class="mt-3 text-[clamp(1.65rem,3vw,2.3rem)] font-extrabold tracking-tight text-[color:var(--fg)]">
|
||||||
{{ caps.length }} 种方式开始创作
|
{{ caps.length }} 种方式开始创作
|
||||||
</h2>
|
</h2>
|
||||||
@@ -299,7 +302,7 @@ function useExample(ex) {
|
|||||||
|
|
||||||
<!-- ============ INSPIRATION (kind=bento) ============ -->
|
<!-- ============ INSPIRATION (kind=bento) ============ -->
|
||||||
<section v-if="bento.length" id="bento" class="mt-24">
|
<section v-if="bento.length" id="bento" class="mt-24">
|
||||||
<div class="anime-eyebrow text-fuchsia-500 dark:text-fuchsia-300/80">灵感</div>
|
<div class="anime-eyebrow text-rose-500">灵感</div>
|
||||||
<h2 class="mt-3 text-[clamp(1.65rem,3vw,2.3rem)] font-extrabold tracking-tight text-[color:var(--fg)]">点一下,套用同款提示词</h2>
|
<h2 class="mt-3 text-[clamp(1.65rem,3vw,2.3rem)] font-extrabold tracking-tight text-[color:var(--fg)]">点一下,套用同款提示词</h2>
|
||||||
<p class="mt-2.5 text-[13.5px] text-[color:var(--fg-3)] max-w-lg leading-relaxed">
|
<p class="mt-2.5 text-[13.5px] text-[color:var(--fg-3)] max-w-lg leading-relaxed">
|
||||||
任选一张,自动进入画图工作台并预填提示词。
|
任选一张,自动进入画图工作台并预填提示词。
|
||||||
@@ -320,7 +323,7 @@ function useExample(ex) {
|
|||||||
|
|
||||||
<!-- ============ WORKS (kind=work) ============ -->
|
<!-- ============ WORKS (kind=work) ============ -->
|
||||||
<section v-if="works.length" class="mt-24">
|
<section v-if="works.length" class="mt-24">
|
||||||
<div class="anime-eyebrow text-sky-500 dark:text-sky-300/80">作品</div>
|
<div class="anime-eyebrow text-sky-600">作品</div>
|
||||||
<h2 class="mt-3 text-[clamp(1.65rem,3vw,2.3rem)] font-extrabold tracking-tight text-[color:var(--fg)]">大家都在生成什么</h2>
|
<h2 class="mt-3 text-[clamp(1.65rem,3vw,2.3rem)] font-extrabold tracking-tight text-[color:var(--fg)]">大家都在生成什么</h2>
|
||||||
<p class="mt-2.5 text-[13.5px] text-[color:var(--fg-3)] max-w-lg leading-relaxed">
|
<p class="mt-2.5 text-[13.5px] text-[color:var(--fg-3)] max-w-lg leading-relaxed">
|
||||||
精选公开作品,点开即用同一条提示词继续创作。
|
精选公开作品,点开即用同一条提示词继续创作。
|
||||||
@@ -340,7 +343,7 @@ function useExample(ex) {
|
|||||||
|
|
||||||
<!-- ============ MODELS ============ -->
|
<!-- ============ MODELS ============ -->
|
||||||
<section v-if="modelCards.length" class="mt-24">
|
<section v-if="modelCards.length" class="mt-24">
|
||||||
<div class="anime-eyebrow text-emerald-500 dark:text-emerald-300/80">模型</div>
|
<div class="anime-eyebrow text-emerald-600">模型</div>
|
||||||
<h2 class="mt-3 text-[clamp(1.65rem,3vw,2.3rem)] font-extrabold tracking-tight text-[color:var(--fg)]">在线模型</h2>
|
<h2 class="mt-3 text-[clamp(1.65rem,3vw,2.3rem)] font-extrabold tracking-tight text-[color:var(--fg)]">在线模型</h2>
|
||||||
<p class="mt-2.5 text-[13.5px] text-[color:var(--fg-3)] max-w-lg leading-relaxed">
|
<p class="mt-2.5 text-[13.5px] text-[color:var(--fg-3)] max-w-lg leading-relaxed">
|
||||||
每个模型的分辨率、时长与参考图能力都写在卡片上,选之前就知道能做什么。
|
每个模型的分辨率、时长与参考图能力都写在卡片上,选之前就知道能做什么。
|
||||||
@@ -359,7 +362,7 @@ function useExample(ex) {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ============ CTA ============ -->
|
<!-- ============ CTA ============ -->
|
||||||
<section class="mt-24 cta-band relative overflow-hidden rounded-[2rem] ring-1 ring-[color:var(--hairline)] p-10 md:p-16">
|
<section class="mt-24 cta-band relative overflow-hidden rounded-[1.4rem] p-10 md:p-16">
|
||||||
<div class="relative max-w-xl">
|
<div class="relative max-w-xl">
|
||||||
<h3 class="text-[clamp(1.7rem,3.4vw,2.8rem)] font-extrabold tracking-tight leading-[1.2] text-[color:var(--fg)]">
|
<h3 class="text-[clamp(1.7rem,3.4vw,2.8rem)] font-extrabold tracking-tight leading-[1.2] text-[color:var(--fg)]">
|
||||||
与其继续想,<br />不如现在写一句。
|
与其继续想,<br />不如现在写一句。
|
||||||
@@ -368,15 +371,15 @@ function useExample(ex) {
|
|||||||
注册即可开始,图像与视频都能试;不满意就换个模型,额度按次计费。
|
注册即可开始,图像与视频都能试;不满意就换个模型,额度按次计费。
|
||||||
</p>
|
</p>
|
||||||
<button type="button" @click="go('/user')"
|
<button type="button" @click="go('/user')"
|
||||||
class="mt-7 group inline-flex items-center gap-3 rounded-full bg-[var(--btn-solid-bg)] text-[color:var(--btn-solid-fg)] hover:bg-[var(--btn-solid-bg-h)] pl-6 pr-3 py-3 text-sm font-semibold transition-all">
|
class="mt-7 group inline-flex items-center gap-3 rounded-xl bg-[var(--m-orange)] text-white border-[3px] border-[color:var(--ink)] shadow-[5px_5px_0_var(--shadow-ink)] hover:shadow-[7px_7px_0_var(--shadow-ink)] hover:-translate-x-0.5 hover:-translate-y-0.5 active:translate-y-0.5 active:shadow-none pl-6 pr-3 py-3 text-sm font-extrabold transition-all">
|
||||||
免费开始
|
免费开始
|
||||||
<span class="w-8 h-8 rounded-full bg-[var(--btn-solid-fg)] text-[color:var(--btn-solid-bg)] grid place-items-center group-hover:translate-x-1 transition-transform">→</span>
|
<span class="w-8 h-8 rounded-lg bg-white text-[#111] border-2 border-[#111] grid place-items-center font-black group-hover:translate-x-1 transition-transform">→</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- thin footer line -->
|
<!-- thin footer line -->
|
||||||
<footer class="mt-16 pt-10 border-t border-[color:var(--hairline)] flex flex-wrap items-center justify-between gap-4 text-xs text-[color:var(--fg-3)]">
|
<footer class="mt-16 pt-10 border-t-[3px] border-t-[color:var(--ink)] flex flex-wrap items-center justify-between gap-4 text-xs font-semibold text-[color:var(--fg-3)]">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="font-semibold text-[color:var(--fg-2)]">{{ site.title }}</span>
|
<span class="font-semibold text-[color:var(--fg-2)]">{{ site.title }}</span>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
@@ -398,13 +401,15 @@ function useExample(ex) {
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
max-width: 36rem;
|
max-width: 36rem;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
border-radius: 1.25rem;
|
border-radius: 1rem;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
box-shadow: inset 0 0 0 1px var(--hairline), 0 20px 50px -22px rgb(139 92 246 / 0.3);
|
border: 3px solid var(--ink);
|
||||||
transition: box-shadow 0.25s ease;
|
box-shadow: 7px 7px 0 var(--shadow-ink);
|
||||||
|
transition: box-shadow 0.2s ease, transform 0.2s ease;
|
||||||
}
|
}
|
||||||
.composer:focus-within {
|
.composer:focus-within {
|
||||||
box-shadow: inset 0 0 0 1px rgb(139 92 246 / 0.55), 0 20px 60px -20px rgb(139 92 246 / 0.45);
|
transform: translate(-2px, -2px);
|
||||||
|
box-shadow: 10px 10px 0 var(--m-orange);
|
||||||
}
|
}
|
||||||
.composer-input {
|
.composer-input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -414,45 +419,54 @@ function useExample(ex) {
|
|||||||
outline: none;
|
outline: none;
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
padding: 0.7rem 0.85rem;
|
padding: 0.7rem 0.85rem;
|
||||||
}
|
}
|
||||||
.composer-input::placeholder { color: var(--fg-faint); }
|
.composer-input::placeholder { color: var(--fg-faint); }
|
||||||
.composer-go {
|
.composer-go {
|
||||||
flex: none;
|
flex: none;
|
||||||
border: none;
|
border: 2.5px solid var(--ink);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 800;
|
||||||
padding: 0.7rem 1.25rem;
|
padding: 0.65rem 1.2rem;
|
||||||
border-radius: 0.9rem;
|
border-radius: 0.7rem;
|
||||||
background: linear-gradient(135deg, #e879f9, #8b5cf6);
|
background: var(--m-orange);
|
||||||
transition: transform 0.15s ease, filter 0.15s ease;
|
box-shadow: 3px 3px 0 var(--shadow-ink);
|
||||||
|
transition: transform 0.15s ease, box-shadow 0.15s ease, filter 0.15s ease;
|
||||||
}
|
}
|
||||||
.composer-go:hover { transform: translateY(-1px); filter: brightness(1.1); }
|
.composer-go:hover { transform: translate(-1px, -1px); box-shadow: 4px 4px 0 var(--shadow-ink); filter: brightness(1.06); }
|
||||||
|
.composer-go:active { transform: scale(0.96) translateY(2px); box-shadow: 0 0 0 var(--shadow-ink); }
|
||||||
|
|
||||||
.chip {
|
.chip {
|
||||||
font-size: 12.5px;
|
font-size: 12.5px;
|
||||||
|
font-weight: 600;
|
||||||
color: var(--fg-2);
|
color: var(--fg-2);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: none;
|
border: 2px solid var(--ink);
|
||||||
padding: 0.42rem 0.8rem;
|
padding: 0.4rem 0.8rem;
|
||||||
border-radius: 999px;
|
border-radius: 0.6rem;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
box-shadow: inset 0 0 0 1px var(--hairline);
|
transition: color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
|
||||||
transition: color 0.15s ease, box-shadow 0.15s ease;
|
|
||||||
}
|
}
|
||||||
.chip:hover { color: var(--fg); box-shadow: inset 0 0 0 1px rgb(139 92 246 / 0.5); }
|
.chip:hover {
|
||||||
|
color: #111;
|
||||||
|
background: var(--m-yellow);
|
||||||
|
box-shadow: 3px 3px 0 var(--shadow-ink);
|
||||||
|
transform: translate(-1px, -1px);
|
||||||
|
}
|
||||||
|
html.dark .chip:hover { color: #111; }
|
||||||
|
|
||||||
/* ---------- hero cards (kind=hero) ---------- */
|
/* ---------- hero cards (kind=hero) ---------- */
|
||||||
.fcard {
|
.fcard {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border: none;
|
border: 3px solid var(--ink);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 1.4rem;
|
border-radius: 1rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 30px 70px -24px rgb(0 0 0 / 0.55), inset 0 0 0 1px rgb(255 255 255 / 0.14);
|
box-shadow: 7px 7px 0 var(--shadow-ink);
|
||||||
transition: transform 0.45s cubic-bezier(0.2, 0.7, 0.2, 1);
|
transition: transform 0.45s cubic-bezier(0.2, 0.7, 0.2, 1);
|
||||||
animation: fcardIn 0.7s cubic-bezier(0.2, 0.7, 0.2, 1) backwards;
|
animation: fcardIn 0.7s cubic-bezier(0.2, 0.7, 0.2, 1) backwards;
|
||||||
}
|
}
|
||||||
@@ -497,23 +511,25 @@ function useExample(ex) {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: none;
|
border: 2.5px solid var(--ink);
|
||||||
padding: 1.35rem;
|
padding: 1.35rem;
|
||||||
border-radius: 1.25rem;
|
border-radius: 1rem;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
box-shadow: inset 0 0 0 1px var(--hairline);
|
box-shadow: 5px 5px 0 var(--shadow-ink);
|
||||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
transition: transform 0.18s ease, box-shadow 0.18s ease;
|
||||||
}
|
}
|
||||||
.cap-card:hover {
|
.cap-card:hover {
|
||||||
transform: translateY(-4px);
|
transform: translate(-3px, -3px);
|
||||||
box-shadow: inset 0 0 0 1px rgb(139 92 246 / 0.45), 0 20px 40px -24px rgb(139 92 246 / 0.45);
|
box-shadow: 9px 9px 0 var(--m-orange);
|
||||||
}
|
}
|
||||||
|
html.dark .cap-card { background: rgb(255 255 255 / 0.05); }
|
||||||
.cap-arrow { position: absolute; top: 1.1rem; right: 1.1rem; color: var(--fg-3); transition: color 0.15s ease; }
|
.cap-arrow { position: absolute; top: 1.1rem; right: 1.1rem; color: var(--fg-3); transition: color 0.15s ease; }
|
||||||
.cap-card:hover .cap-arrow { color: var(--fg); }
|
.cap-card:hover .cap-arrow { color: var(--m-orange); }
|
||||||
.cap-icon {
|
.cap-icon {
|
||||||
width: 2.6rem;
|
width: 2.6rem;
|
||||||
height: 2.6rem;
|
height: 2.6rem;
|
||||||
border-radius: 0.85rem;
|
border-radius: 0.7rem;
|
||||||
|
border: 2px solid var(--ink);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
margin-bottom: 0.9rem;
|
margin-bottom: 0.9rem;
|
||||||
@@ -522,9 +538,11 @@ function useExample(ex) {
|
|||||||
.cap-meta {
|
.cap-meta {
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
color: var(--fg-2);
|
color: var(--fg-2);
|
||||||
padding: 0.18rem 0.5rem;
|
padding: 0.18rem 0.5rem;
|
||||||
border-radius: 0.4rem;
|
border-radius: 0.4rem;
|
||||||
|
border: 1.5px solid var(--ink-soft);
|
||||||
background: var(--surface-2);
|
background: var(--surface-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,14 +550,14 @@ function useExample(ex) {
|
|||||||
.tile {
|
.tile {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: none;
|
border: 2.5px solid var(--ink);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 1.25rem;
|
border-radius: 1rem;
|
||||||
box-shadow: inset 0 0 0 1px var(--hairline);
|
box-shadow: 5px 5px 0 var(--shadow-ink);
|
||||||
transition: transform 0.35s ease;
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
.tile:hover { transform: translateY(-3px); }
|
.tile:hover { transform: translate(-2px, -2px); box-shadow: 8px 8px 0 var(--m-blue); }
|
||||||
.tile-veil {
|
.tile-veil {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -566,12 +584,14 @@ function useExample(ex) {
|
|||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: none;
|
border: 2.5px solid var(--ink);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 1.1rem;
|
border-radius: 1rem;
|
||||||
box-shadow: inset 0 0 0 1px var(--hairline);
|
box-shadow: 5px 5px 0 var(--shadow-ink);
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
.work:hover { transform: translate(-2px, -2px); box-shadow: 8px 8px 0 var(--m-pink); }
|
||||||
.work-img { display: block; width: 100%; aspect-ratio: 4 / 5; transition: transform 0.5s ease; }
|
.work-img { display: block; width: 100%; aspect-ratio: 4 / 5; transition: transform 0.5s ease; }
|
||||||
.work:hover .work-img { transform: scale(1.04); }
|
.work:hover .work-img { transform: scale(1.04); }
|
||||||
.work-overlay {
|
.work-overlay {
|
||||||
@@ -606,10 +626,11 @@ function useExample(ex) {
|
|||||||
margin-top: 0.7rem;
|
margin-top: 0.7rem;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 800;
|
||||||
color: rgb(11 13 19);
|
color: rgb(11 13 19);
|
||||||
background: #fff;
|
background: var(--m-yellow);
|
||||||
border-radius: 999px;
|
border: 2px solid #111;
|
||||||
|
border-radius: 0.5rem;
|
||||||
padding: 0.4rem 0.8rem;
|
padding: 0.4rem 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -622,12 +643,14 @@ function useExample(ex) {
|
|||||||
width: 14.5rem;
|
width: 14.5rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 1.25rem;
|
padding: 1.25rem;
|
||||||
border-radius: 1.25rem;
|
border-radius: 1rem;
|
||||||
|
border: 2.5px solid var(--ink);
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
box-shadow: inset 0 0 0 1px var(--hairline);
|
box-shadow: 5px 5px 0 var(--shadow-ink);
|
||||||
transition: box-shadow 0.2s ease;
|
transition: box-shadow 0.18s ease, transform 0.18s ease;
|
||||||
}
|
}
|
||||||
.model-card:hover { box-shadow: inset 0 0 0 1px rgb(139 92 246 / 0.42); }
|
.model-card:hover { transform: translate(-2px, -2px); box-shadow: 8px 8px 0 var(--m-mint); }
|
||||||
|
html.dark .model-card { background: rgb(255 255 255 / 0.05); }
|
||||||
.model-glow {
|
.model-glow {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -2.5rem;
|
top: -2.5rem;
|
||||||
@@ -638,31 +661,92 @@ function useExample(ex) {
|
|||||||
filter: blur(40px);
|
filter: blur(40px);
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
.model-tag { font-size: 10.5px; padding: 0.18rem 0.55rem; border-radius: 0.4rem; }
|
.model-tag {
|
||||||
.model-tag.is-image { background: rgb(52 211 153 / 0.14); color: rgb(52 211 153); }
|
font-size: 10.5px;
|
||||||
.model-tag.is-video { background: rgb(125 211 252 / 0.14); color: rgb(56 189 248); }
|
font-weight: 800;
|
||||||
|
padding: 0.18rem 0.55rem;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
border: 1.5px solid var(--ink);
|
||||||
|
}
|
||||||
|
.model-tag.is-image { background: var(--m-mint); color: #065f46; }
|
||||||
|
.model-tag.is-video { background: var(--m-blue); color: #075985; }
|
||||||
|
|
||||||
/* ---------- hero band + mascot aura ---------- */
|
/* ---------- hero band + memphis deco ---------- */
|
||||||
.hero-band {
|
.hero-band {
|
||||||
border-radius: 2rem;
|
border-radius: 2rem;
|
||||||
}
|
}
|
||||||
.mascot-aura {
|
/* Floating crayon chips scattered around the headline — each is a small
|
||||||
|
ink-outlined tile with a hard shadow and its own idle animation. */
|
||||||
|
.deco {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
display: grid;
|
||||||
top: 50%;
|
place-items: center;
|
||||||
width: 260px;
|
border: 3px solid var(--ink);
|
||||||
height: 260px;
|
box-shadow: 5px 5px 0 var(--shadow-ink);
|
||||||
transform: translate(-50%, -50%);
|
font-weight: 900;
|
||||||
border-radius: 50%;
|
color: #111;
|
||||||
background: radial-gradient(circle, rgb(217 70 239 / 0.35), rgb(56 189 248 / 0.12) 55%, transparent 72%);
|
|
||||||
filter: blur(14px);
|
|
||||||
animation: auraPulse 5s ease-in-out infinite;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
@keyframes auraPulse {
|
.deco-a {
|
||||||
0%, 100% { opacity: 0.8; transform: translate(-50%, -50%) scale(1); }
|
left: 7%; top: 22%;
|
||||||
50% { opacity: 1; transform: translate(-50%, -50%) scale(1.08); }
|
width: 3.6rem; height: 3.6rem;
|
||||||
|
border-radius: 0.9rem;
|
||||||
|
background: var(--m-yellow);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
transform: rotate(-8deg);
|
||||||
|
animation: memphis-float 6s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
.deco-b {
|
||||||
|
right: 8%; top: 16%;
|
||||||
|
width: 3rem; height: 3rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--m-pink);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
transform: rotate(6deg);
|
||||||
|
animation: memphis-wiggle 3.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.deco-c {
|
||||||
|
left: 13%; bottom: 16%;
|
||||||
|
width: 3.2rem; height: 2.4rem;
|
||||||
|
border-radius: 0.6rem;
|
||||||
|
background: var(--m-blue);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
transform: rotate(9deg);
|
||||||
|
animation: memphis-float 7s ease-in-out infinite reverse;
|
||||||
|
}
|
||||||
|
.deco-d {
|
||||||
|
right: 13%; bottom: 20%;
|
||||||
|
width: 3.4rem; height: 2.6rem;
|
||||||
|
border-radius: 0.7rem;
|
||||||
|
background: var(--m-mint);
|
||||||
|
font-size: 1.15rem;
|
||||||
|
transform: rotate(-7deg);
|
||||||
|
animation: memphis-bounce 4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.deco-star {
|
||||||
|
left: 22%; top: 10%;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
font-size: 1.7rem;
|
||||||
|
color: var(--m-orange);
|
||||||
|
animation: memphis-wiggle 4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.deco-star-2 {
|
||||||
|
left: auto; right: 22%; top: auto; bottom: 10%;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
color: var(--m-pink);
|
||||||
|
animation: memphis-wiggle 5s ease-in-out infinite reverse;
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) { .deco { animation: none; } }
|
||||||
|
|
||||||
|
/* KPI tiles — flat crayon fills, dark ink type (readable in both themes). */
|
||||||
|
.kpi-card {
|
||||||
|
border: 2.5px solid var(--ink);
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 5px 5px 0 var(--shadow-ink);
|
||||||
|
transition: transform 0.18s ease, box-shadow 0.18s ease;
|
||||||
|
}
|
||||||
|
.kpi-card:hover { transform: translate(-2px, -2px); box-shadow: 8px 8px 0 var(--shadow-ink); }
|
||||||
|
|
||||||
/* ---------- gallery marquee ---------- */
|
/* ---------- gallery marquee ---------- */
|
||||||
.marquee-mask {
|
.marquee-mask {
|
||||||
@@ -683,21 +767,20 @@ function useExample(ex) {
|
|||||||
width: 15rem;
|
width: 15rem;
|
||||||
height: 10rem;
|
height: 10rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: none;
|
border: 2.5px solid var(--ink);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 1.4rem;
|
border-radius: 1rem;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
box-shadow: inset 0 0 0 1px var(--hairline);
|
box-shadow: 4px 4px 0 var(--shadow-ink);
|
||||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
.gcard:hover {
|
.gcard:hover {
|
||||||
transform: translateY(-4px) scale(1.02);
|
transform: translate(-2px, -2px);
|
||||||
box-shadow: inset 0 0 0 1px rgb(217 70 239 / 0.5), 0 22px 44px -26px rgb(168 85 247 / 0.6);
|
box-shadow: 7px 7px 0 var(--m-orange);
|
||||||
}
|
}
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.marquee { animation: none; }
|
.marquee { animation: none; }
|
||||||
.mascot-aura { animation: none; }
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -215,12 +215,12 @@ onUnmounted(() => window.removeEventListener('keydown', onKey))
|
|||||||
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ stats.total }}</div>
|
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ stats.total }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card p-4">
|
<div class="card p-4">
|
||||||
<div class="text-[11px] uppercase tracking-wider text-indigo-300/80">图像</div>
|
<div class="text-[11px] uppercase tracking-wider text-sky-300/80">图像</div>
|
||||||
<div class="text-2xl font-semibold mt-1 tabular-nums text-indigo-300">{{ stats.image }}</div>
|
<div class="text-2xl font-semibold mt-1 tabular-nums text-sky-300">{{ stats.image }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card p-4">
|
<div class="card p-4">
|
||||||
<div class="text-[11px] uppercase tracking-wider text-fuchsia-300/80">视频</div>
|
<div class="text-[11px] uppercase tracking-wider text-rose-300/80">视频</div>
|
||||||
<div class="text-2xl font-semibold mt-1 tabular-nums text-fuchsia-300">{{ stats.video }}</div>
|
<div class="text-2xl font-semibold mt-1 tabular-nums text-rose-300">{{ stats.video }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card p-4">
|
<div class="card p-4">
|
||||||
<div class="text-[11px] uppercase tracking-wider text-amber-300/80">存储</div>
|
<div class="text-[11px] uppercase tracking-wider text-amber-300/80">存储</div>
|
||||||
@@ -293,7 +293,7 @@ onUnmounted(() => window.removeEventListener('keydown', onKey))
|
|||||||
<Icon name="check" class="w-3 h-3" />
|
<Icon name="check" class="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
<span class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider ring-1"
|
<span class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider ring-1"
|
||||||
:class="f.kind === 'video' ? 'bg-fuchsia-500/20 text-fuchsia-200 ring-fuchsia-400/30' : 'bg-indigo-500/20 text-indigo-200 ring-indigo-400/30'">
|
:class="f.kind === 'video' ? 'bg-rose-500/20 text-rose-200 ring-rose-400/30' : 'bg-sky-500/20 text-sky-200 ring-sky-400/30'">
|
||||||
{{ f.kind === 'video' ? '视频' : '图像' }}
|
{{ f.kind === 'video' ? '视频' : '图像' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -327,8 +327,8 @@ onUnmounted(() => window.removeEventListener('keydown', onKey))
|
|||||||
<span class="shrink-0 flex items-center gap-1">
|
<span class="shrink-0 flex items-center gap-1">
|
||||||
<span v-if="f.resolution" class="text-emerald-300/90">{{ f.resolution }}</span>
|
<span v-if="f.resolution" class="text-emerald-300/90">{{ f.resolution }}</span>
|
||||||
<span v-if="f.ratio" class="text-white/40">{{ f.ratio }}</span>
|
<span v-if="f.ratio" class="text-white/40">{{ f.ratio }}</span>
|
||||||
<span v-if="f.kind === 'video' && f.duration" class="text-fuchsia-300/80">{{ f.duration }}</span>
|
<span v-if="f.kind === 'video' && f.duration" class="text-rose-300/80">{{ f.duration }}</span>
|
||||||
<span v-if="f.deai" class="text-[#7c3aed]">去AI</span>
|
<span v-if="f.deai" class="text-[#ec5b13]">去AI</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[10px] text-white/35 mt-0.5 tabular-nums">{{ fmtSize(f.size) }} · {{ fmtTs(f.mtime) }}</div>
|
<div class="text-[10px] text-white/35 mt-0.5 tabular-nums">{{ fmtSize(f.size) }} · {{ fmtTs(f.mtime) }}</div>
|
||||||
@@ -438,6 +438,6 @@ html.dark .pg-on { background: rgb(255 255 255 / 0.92); color: rgb(15 23 42); }
|
|||||||
.pick svg { opacity: 0; transition: opacity 0.15s; }
|
.pick svg { opacity: 0; transition: opacity 0.15s; }
|
||||||
.pick:hover { background: rgb(0 0 0 / 0.65); }
|
.pick:hover { background: rgb(0 0 0 / 0.65); }
|
||||||
.pick:hover svg { opacity: 0.6; }
|
.pick:hover svg { opacity: 0.6; }
|
||||||
.pick-on { background: rgb(217 70 239); box-shadow: inset 0 0 0 1.5px rgb(255 255 255 / 0.9); }
|
.pick-on { background: rgb(244 63 94); box-shadow: inset 0 0 0 1.5px rgb(255 255 255 / 0.9); }
|
||||||
.pick-on svg { opacity: 1; }
|
.pick-on svg { opacity: 1; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { auth, refreshMe } from '../auth'
|
|||||||
import { api } from '../api'
|
import { api } from '../api'
|
||||||
import { fmtIso } from '../utils/format'
|
import { fmtIso } from '../utils/format'
|
||||||
import Icon from '../components/Icon.vue'
|
import Icon from '../components/Icon.vue'
|
||||||
import Mascot from '../components/Mascot.vue'
|
|
||||||
|
|
||||||
// Reward per completed invite — comes from the backend (credits.invite_reward),
|
// Reward per completed invite — comes from the backend (credits.invite_reward),
|
||||||
// falling back to 3 until the response lands.
|
// falling back to 3 until the response lands.
|
||||||
@@ -86,34 +85,34 @@ function toast(m) {
|
|||||||
<p class="text-white/45 mt-2">好友用你的链接注册,并完成首次生图后,你得 {{ INVITE_REWARD }} 积分。</p>
|
<p class="text-white/45 mt-2">好友用你的链接注册,并完成首次生图后,你得 {{ INVITE_REWARD }} 积分。</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="relative rounded-3xl ring-1 ring-white/[0.08] p-7 md:p-8 overflow-hidden"
|
<section class="relative rounded-2xl p-7 md:p-8 overflow-hidden ink-card">
|
||||||
style="background: radial-gradient(at 70% 30%,rgba(251,191,36,0.16) 0%, transparent 55%),linear-gradient(180deg,rgba(255,255,255,0.04),rgba(255,255,255,0.02))">
|
<div class="memphis-dots absolute inset-0 opacity-30 pointer-events-none"></div>
|
||||||
<div class="inline-grid w-10 h-10 rounded-xl bg-amber-500/15 ring-1 ring-amber-400/30 grid place-items-center text-amber-300">
|
<div class="relative inline-grid w-11 h-11 rounded-xl bg-[var(--m-yellow)] border-2 border-[color:var(--ink)] shadow-[3px_3px_0_var(--shadow-ink)] grid place-items-center text-[#111]">
|
||||||
<Icon name="accounts" class="w-4 h-4" />
|
<Icon name="accounts" class="w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- summary -->
|
<!-- summary -->
|
||||||
<div class="mt-6 grid grid-cols-2 gap-3">
|
<div class="relative mt-6 grid grid-cols-2 gap-3">
|
||||||
<div class="rounded-xl bg-white/[0.04] ring-1 ring-white/[0.06] px-4 py-3">
|
<div class="rounded-xl px-4 py-3 border-2 border-[color:var(--ink)] bg-[var(--m-mint)] shadow-[3px_3px_0_var(--shadow-ink)]">
|
||||||
<div class="text-2xl font-bold tabular-nums">{{ inviteCount }}</div>
|
<div class="text-2xl font-black tabular-nums text-[#111]">{{ inviteCount }}</div>
|
||||||
<div class="text-[10px] text-white/40 mt-1 uppercase tracking-widest">已邀请</div>
|
<div class="text-[10px] text-[#111]/55 mt-1 uppercase tracking-widest font-bold">已邀请</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-xl bg-white/[0.04] ring-1 ring-white/[0.06] px-4 py-3">
|
<div class="rounded-xl px-4 py-3 border-2 border-[color:var(--ink)] bg-[var(--m-blue)] shadow-[3px_3px_0_var(--shadow-ink)]">
|
||||||
<div class="text-2xl font-bold tabular-nums">{{ inviteEarned.toLocaleString('en-US') }}</div>
|
<div class="text-2xl font-black tabular-nums text-[#111]">{{ inviteEarned.toLocaleString('en-US') }}</div>
|
||||||
<div class="text-[10px] text-white/40 mt-1 uppercase tracking-widest">累计积分</div>
|
<div class="text-[10px] text-[#111]/55 mt-1 uppercase tracking-widest font-bold">累计积分</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- code -->
|
<!-- code -->
|
||||||
<div class="mt-6">
|
<div class="relative mt-6">
|
||||||
<label class="block text-xs text-white/50 mb-2">邀请码</label>
|
<label class="block text-xs font-bold text-white/50 mb-2">邀请码</label>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button @click="copyCode"
|
<button @click="copyCode"
|
||||||
class="flex-1 rounded-xl bg-white/[0.05] ring-1 ring-white/10 hover:ring-white/30 hover:bg-white/[0.08] px-4 py-3 text-sm font-mono text-left transition-all">
|
class="flex-1 rounded-xl border-2 border-[color:var(--ink)] bg-[var(--surface)] px-4 py-3 text-sm font-mono text-left shadow-[3px_3px_0_var(--shadow-ink)] hover:-translate-y-0.5 hover:shadow-[4px_4px_0_var(--shadow-ink)] transition-all">
|
||||||
{{ inviteCode || '—' }}
|
{{ inviteCode || '—' }}
|
||||||
</button>
|
</button>
|
||||||
<button @click="copyInvite"
|
<button @click="copyInvite"
|
||||||
class="rounded-xl bg-white text-black hover:bg-white/90 px-5 py-3 text-sm font-semibold transition-colors">
|
class="rounded-xl border-[2.5px] border-[color:var(--ink)] bg-[var(--m-orange)] text-white px-5 py-3 text-sm font-extrabold shadow-[3px_3px_0_var(--shadow-ink)] hover:-translate-y-0.5 hover:shadow-[4px_4px_0_var(--shadow-ink)] active:translate-y-0.5 active:shadow-none transition-all">
|
||||||
复制链接
|
复制链接
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -134,7 +133,7 @@ function toast(m) {
|
|||||||
<div v-if="loading && !records.length" class="text-center text-sm text-white/40 py-16">加载中…</div>
|
<div v-if="loading && !records.length" class="text-center text-sm text-white/40 py-16">加载中…</div>
|
||||||
<div v-else-if="!records.length" class="relative overflow-hidden flex flex-col items-center gap-3 text-white/40 py-14">
|
<div v-else-if="!records.length" class="relative overflow-hidden flex flex-col items-center gap-3 text-white/40 py-14">
|
||||||
<div class="anime-stars"></div>
|
<div class="anime-stars"></div>
|
||||||
<Mascot :size="110" />
|
<span class="empty-badge">✦</span>
|
||||||
<span class="text-sm">还没有人通过你的链接注册~快去分享吧</span>
|
<span class="text-sm">还没有人通过你的链接注册~快去分享吧</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ onMounted(load)
|
|||||||
<div class="text-2xl font-semibold mt-1 tabular-nums text-amber-300">{{ stats.pending }}</div>
|
<div class="text-2xl font-semibold mt-1 tabular-nums text-amber-300">{{ stats.pending }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card p-4">
|
<div class="card p-4">
|
||||||
<div class="text-[11px] uppercase tracking-wider text-fuchsia-300/80">已发奖励</div>
|
<div class="text-[11px] uppercase tracking-wider text-rose-300/80">已发奖励</div>
|
||||||
<div class="text-2xl font-semibold mt-1 tabular-nums text-fuchsia-300">{{ Number(stats.reward_paid || 0).toLocaleString('en-US') }}</div>
|
<div class="text-2xl font-semibold mt-1 tabular-nums text-rose-300">{{ Number(stats.reward_paid || 0).toLocaleString('en-US') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ const statusDot = (s) => ({
|
|||||||
// Source: backend stamps "v1" (API key), "user" (画图台), "admin" (后台测试模型).
|
// Source: backend stamps "v1" (API key), "user" (画图台), "admin" (后台测试模型).
|
||||||
const sourceLabel = (s) => ({ v1: 'API', user: '画图台', admin: '测试' }[s] || '画图台')
|
const sourceLabel = (s) => ({ v1: 'API', user: '画图台', admin: '测试' }[s] || '画图台')
|
||||||
const sourcePill = (s) => ({
|
const sourcePill = (s) => ({
|
||||||
v1: 'bg-violet-500/15 text-violet-300 ring-violet-400/30',
|
v1: 'bg-orange-500/15 text-orange-300 ring-orange-400/30',
|
||||||
admin: 'bg-amber-500/15 text-amber-300 ring-amber-400/30',
|
admin: 'bg-amber-500/15 text-amber-300 ring-amber-400/30',
|
||||||
user: 'bg-sky-500/15 text-sky-300 ring-sky-400/30',
|
user: 'bg-sky-500/15 text-sky-300 ring-sky-400/30',
|
||||||
}[s] || 'bg-sky-500/15 text-sky-300 ring-sky-400/30')
|
}[s] || 'bg-sky-500/15 text-sky-300 ring-sky-400/30')
|
||||||
@@ -254,7 +254,7 @@ const sourcePill = (s) => ({
|
|||||||
<td class="px-4 py-3.5 align-middle text-center">
|
<td class="px-4 py-3.5 align-middle text-center">
|
||||||
<button v-if="e.status === 'success' && e.file && e.source !== 'v1'"
|
<button v-if="e.status === 'success' && e.file && e.source !== 'v1'"
|
||||||
@click="openPreview(e)"
|
@click="openPreview(e)"
|
||||||
class="block w-12 h-12 mx-auto rounded-lg overflow-hidden ring-1 ring-white/10 hover:ring-fuchsia-400/60 transition-all">
|
class="block w-12 h-12 mx-auto rounded-lg overflow-hidden ring-1 ring-white/10 hover:ring-rose-400/60 transition-all">
|
||||||
<img v-if="e.kind !== 'video' || !thumbFail[e.id]" :src="thumbUrl(e.file)" loading="lazy"
|
<img v-if="e.kind !== 'video' || !thumbFail[e.id]" :src="thumbUrl(e.file)" loading="lazy"
|
||||||
class="w-full h-full object-cover"
|
class="w-full h-full object-cover"
|
||||||
@error="e.kind === 'video' && (thumbFail[e.id] = true)" />
|
@error="e.kind === 'video' && (thumbFail[e.id] = true)" />
|
||||||
@@ -291,7 +291,7 @@ const sourcePill = (s) => ({
|
|||||||
<div class="font-mono text-xs text-white/90 break-all" :title="e.model">{{ e.model }}</div>
|
<div class="font-mono text-xs text-white/90 break-all" :title="e.model">{{ e.model }}</div>
|
||||||
<div class="mt-1 flex items-center gap-1.5 min-w-0">
|
<div class="mt-1 flex items-center gap-1.5 min-w-0">
|
||||||
<span class="text-[10px] uppercase tracking-wider font-medium truncate min-w-0"
|
<span class="text-[10px] uppercase tracking-wider font-medium truncate min-w-0"
|
||||||
:class="e.kind === 'video' ? 'text-fuchsia-300/80' : 'text-indigo-300/80'">
|
:class="e.kind === 'video' ? 'text-rose-300/80' : 'text-sky-300/80'">
|
||||||
{{ e.kind === 'video' ? '视频' : '图像' }}
|
{{ e.kind === 'video' ? '视频' : '图像' }}
|
||||||
<span v-if="e.provider" class="text-white/30 ml-1">· {{ e.provider }}</span>
|
<span v-if="e.provider" class="text-white/30 ml-1">· {{ e.provider }}</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -327,7 +327,7 @@ const sourcePill = (s) => ({
|
|||||||
</template>
|
</template>
|
||||||
<template v-if="e.deai">
|
<template v-if="e.deai">
|
||||||
<span class="text-white/25 mx-1.5">·</span>
|
<span class="text-white/25 mx-1.5">·</span>
|
||||||
<span class="text-[#7c3aed]">去AI特征</span>
|
<span class="text-[#ec5b13]">去AI特征</span>
|
||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-3.5 text-right text-xs tabular-nums align-middle whitespace-nowrap">
|
<td class="px-3 py-3.5 text-right text-xs tabular-nums align-middle whitespace-nowrap">
|
||||||
@@ -437,7 +437,7 @@ const sourcePill = (s) => ({
|
|||||||
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||||||
|
|
||||||
/* --- log table: subtle row separators + a barely-there hover tint that
|
/* --- log table: subtle row separators + a barely-there hover tint that
|
||||||
extends a soft violet accent on the left of the row (read as a
|
extends a soft orange accent on the left of the row (read as a
|
||||||
focus indicator without being noisy). --- */
|
focus indicator without being noisy). --- */
|
||||||
.log-table { border-collapse: separate; border-spacing: 0; }
|
.log-table { border-collapse: separate; border-spacing: 0; }
|
||||||
.log-row td {
|
.log-row td {
|
||||||
@@ -446,7 +446,7 @@ const sourcePill = (s) => ({
|
|||||||
}
|
}
|
||||||
.log-row:hover td { background: rgb(255 255 255 / 0.025); }
|
.log-row:hover td { background: rgb(255 255 255 / 0.025); }
|
||||||
.log-row:hover td:first-child {
|
.log-row:hover td:first-child {
|
||||||
box-shadow: inset 2px 0 0 rgb(167 139 250 / 0.55);
|
box-shadow: inset 2px 0 0 rgb(253 186 116 / 0.55);
|
||||||
}
|
}
|
||||||
.log-row:last-child td { border-bottom: none; }
|
.log-row:last-child td { border-bottom: none; }
|
||||||
|
|
||||||
|
|||||||
@@ -96,12 +96,12 @@ onMounted(loadModels)
|
|||||||
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ stats.total }}</div>
|
<div class="text-2xl font-semibold mt-1 tabular-nums">{{ stats.total }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card p-4">
|
<div class="card p-4">
|
||||||
<div class="text-[11px] uppercase tracking-wider text-indigo-300/80">图像</div>
|
<div class="text-[11px] uppercase tracking-wider text-sky-300/80">图像</div>
|
||||||
<div class="text-2xl font-semibold mt-1 tabular-nums text-indigo-300">{{ stats.image }}</div>
|
<div class="text-2xl font-semibold mt-1 tabular-nums text-sky-300">{{ stats.image }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card p-4">
|
<div class="card p-4">
|
||||||
<div class="text-[11px] uppercase tracking-wider text-fuchsia-300/80">视频</div>
|
<div class="text-[11px] uppercase tracking-wider text-rose-300/80">视频</div>
|
||||||
<div class="text-2xl font-semibold mt-1 tabular-nums text-fuchsia-300">{{ stats.video }}</div>
|
<div class="text-2xl font-semibold mt-1 tabular-nums text-rose-300">{{ stats.video }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card p-4">
|
<div class="card p-4">
|
||||||
<div class="text-[11px] uppercase tracking-wider text-emerald-300/80">启用</div>
|
<div class="text-[11px] uppercase tracking-wider text-emerald-300/80">启用</div>
|
||||||
@@ -187,8 +187,8 @@ onMounted(loadModels)
|
|||||||
<td class="px-3 py-3.5 align-middle">
|
<td class="px-3 py-3.5 align-middle">
|
||||||
<span class="inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-[11px] font-medium ring-1"
|
<span class="inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-[11px] font-medium ring-1"
|
||||||
:class="m.type === 'video'
|
:class="m.type === 'video'
|
||||||
? 'bg-fuchsia-500/10 text-fuchsia-300 ring-fuchsia-400/30'
|
? 'bg-rose-500/10 text-rose-300 ring-rose-400/30'
|
||||||
: 'bg-indigo-500/10 text-indigo-300 ring-indigo-400/30'">
|
: 'bg-sky-500/10 text-sky-300 ring-sky-400/30'">
|
||||||
{{ TYPE_LABEL[m.type] || m.type }}
|
{{ TYPE_LABEL[m.type] || m.type }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -354,12 +354,12 @@ onMounted(loadModels)
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.cap-dur {
|
.cap-dur {
|
||||||
background: rgb(99 102 241 / 0.12);
|
background: rgb(14 165 233 / 0.12);
|
||||||
color: rgb(165 180 252);
|
color: rgb(147 197 253);
|
||||||
box-shadow: inset 0 0 0 1px rgb(129 140 248 / 0.3);
|
box-shadow: inset 0 0 0 1px rgb(129 140 248 / 0.3);
|
||||||
}
|
}
|
||||||
.cap-frame {
|
.cap-frame {
|
||||||
background: rgb(236 72 153 / 0.12);
|
background: rgb(251 113 133 / 0.12);
|
||||||
color: rgb(244 114 182);
|
color: rgb(244 114 182);
|
||||||
box-shadow: inset 0 0 0 1px rgb(244 114 182 / 0.35);
|
box-shadow: inset 0 0 0 1px rgb(244 114 182 / 0.35);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { api, jsonBody } from '../api'
|
|||||||
import { openPayment } from '../payment'
|
import { openPayment } from '../payment'
|
||||||
import { refreshMe } from '../auth'
|
import { refreshMe } from '../auth'
|
||||||
import Icon from '../components/Icon.vue'
|
import Icon from '../components/Icon.vue'
|
||||||
import Mascot from '../components/Mascot.vue'
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const items = ref([])
|
const items = ref([])
|
||||||
@@ -118,7 +117,7 @@ async function cont(o) {
|
|||||||
<div v-if="loading && !items.length" class="card text-center text-sm text-slate-400 py-24">加载中…</div>
|
<div v-if="loading && !items.length" class="card text-center text-sm text-slate-400 py-24">加载中…</div>
|
||||||
<div v-else-if="!total" class="anime-card relative overflow-hidden flex flex-col items-center gap-3 text-[color:var(--fg-3)] py-20">
|
<div v-else-if="!total" class="anime-card relative overflow-hidden flex flex-col items-center gap-3 text-[color:var(--fg-3)] py-20">
|
||||||
<div class="anime-stars"></div>
|
<div class="anime-stars"></div>
|
||||||
<Mascot :size="120" />
|
<span class="empty-badge">✦</span>
|
||||||
<span class="text-sm">还没有充值订单~</span>
|
<span class="text-sm">还没有充值订单~</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -141,7 +140,7 @@ async function cont(o) {
|
|||||||
<td class="px-3 py-3 align-middle text-xs text-slate-500 whitespace-nowrap">{{ fmt(o.created_at) }}</td>
|
<td class="px-3 py-3 align-middle text-xs text-slate-500 whitespace-nowrap">{{ fmt(o.created_at) }}</td>
|
||||||
<td class="px-3 py-3 align-middle text-xs text-slate-500 whitespace-nowrap">{{ fmt(o.paid_at) }}</td>
|
<td class="px-3 py-3 align-middle text-xs text-slate-500 whitespace-nowrap">{{ fmt(o.paid_at) }}</td>
|
||||||
<td class="px-3 py-3 align-middle text-right tabular-nums text-slate-800 font-medium">¥{{ o.amount }}</td>
|
<td class="px-3 py-3 align-middle text-right tabular-nums text-slate-800 font-medium">¥{{ o.amount }}</td>
|
||||||
<td class="px-3 py-3 align-middle text-right tabular-nums text-violet-600 font-medium">{{ o.points }}</td>
|
<td class="px-3 py-3 align-middle text-right tabular-nums text-orange-600 font-medium">{{ o.points }}</td>
|
||||||
<td class="px-3 py-3 align-middle">
|
<td class="px-3 py-3 align-middle">
|
||||||
<span class="inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] font-medium ring-1 whitespace-nowrap" :class="statusPill(o.status)">
|
<span class="inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] font-medium ring-1 whitespace-nowrap" :class="statusPill(o.status)">
|
||||||
<span class="w-1.5 h-1.5 rounded-full" :class="statusDot(o.status)"></span>{{ STATUS[o.status] }}
|
<span class="w-1.5 h-1.5 rounded-full" :class="statusDot(o.status)"></span>{{ STATUS[o.status] }}
|
||||||
@@ -150,7 +149,7 @@ async function cont(o) {
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 align-middle text-right">
|
<td class="px-4 py-3 align-middle text-right">
|
||||||
<button v-if="o.status === 'pending'" @click="cont(o)" :disabled="continuingId === o.id"
|
<button v-if="o.status === 'pending'" @click="cont(o)" :disabled="continuingId === o.id"
|
||||||
class="rounded-lg bg-violet-600 text-white hover:bg-violet-500 disabled:opacity-60 disabled:cursor-not-allowed px-3 py-1.5 text-xs font-medium transition-colors inline-flex items-center gap-1.5">
|
class="rounded-lg bg-orange-600 text-white hover:bg-orange-500 disabled:opacity-60 disabled:cursor-not-allowed px-3 py-1.5 text-xs font-medium transition-colors inline-flex items-center gap-1.5">
|
||||||
<span v-if="continuingId === o.id" class="w-3 h-3 rounded-full border-2 border-white/40 border-t-white animate-spin"></span>
|
<span v-if="continuingId === o.id" class="w-3 h-3 rounded-full border-2 border-white/40 border-t-white animate-spin"></span>
|
||||||
{{ continuingId === o.id ? '处理中…' : '继续支付' }}
|
{{ continuingId === o.id ? '处理中…' : '继续支付' }}
|
||||||
</button>
|
</button>
|
||||||
@@ -178,7 +177,7 @@ async function cont(o) {
|
|||||||
.log-table { border-collapse: separate; border-spacing: 0; }
|
.log-table { border-collapse: separate; border-spacing: 0; }
|
||||||
.log-row td { border-bottom: 1px solid rgb(15 23 42 / 0.06); transition: background-color 0.15s ease, box-shadow 0.15s ease; }
|
.log-row td { border-bottom: 1px solid rgb(15 23 42 / 0.06); transition: background-color 0.15s ease, box-shadow 0.15s ease; }
|
||||||
.log-row:hover td { background: rgb(15 23 42 / 0.025); }
|
.log-row:hover td { background: rgb(15 23 42 / 0.025); }
|
||||||
.log-row:hover td:first-child { box-shadow: inset 2px 0 0 rgb(124 58 237 / 0.6); }
|
.log-row:hover td:first-child { box-shadow: inset 2px 0 0 rgb(234 88 12 / 0.6); }
|
||||||
.log-row:last-child td { border-bottom: none; }
|
.log-row:last-child td { border-bottom: none; }
|
||||||
.pg {
|
.pg {
|
||||||
min-width: 1.75rem; padding: 0.3rem 0.55rem; font-size: 0.72rem; font-weight: 500; text-align: center;
|
min-width: 1.75rem; padding: 0.3rem 0.55rem; font-size: 0.72rem; font-weight: 500; text-align: center;
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ onUnmounted(() => clearInterval(timer))
|
|||||||
<div class="card p-4">
|
<div class="card p-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-xs text-white/55">用户</span>
|
<span class="text-xs text-white/55">用户</span>
|
||||||
<span class="w-7 h-7 rounded-lg bg-indigo-500/15 text-indigo-300 grid place-items-center ring-1 ring-indigo-400/20">
|
<span class="w-7 h-7 rounded-lg bg-sky-500/15 text-sky-300 grid place-items-center ring-1 ring-sky-400/20">
|
||||||
<Icon name="accounts" class="w-3.5 h-3.5" />
|
<Icon name="accounts" class="w-3.5 h-3.5" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -197,7 +197,7 @@ onUnmounted(() => clearInterval(timer))
|
|||||||
<div class="card p-4">
|
<div class="card p-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-xs text-white/55">近 24 小时生成</span>
|
<span class="text-xs text-white/55">近 24 小时生成</span>
|
||||||
<span class="w-7 h-7 rounded-lg bg-violet-500/15 text-violet-300 grid place-items-center ring-1 ring-violet-400/20">
|
<span class="w-7 h-7 rounded-lg bg-orange-500/15 text-orange-300 grid place-items-center ring-1 ring-orange-400/20">
|
||||||
<Icon name="spark" class="w-3.5 h-3.5" />
|
<Icon name="spark" class="w-3.5 h-3.5" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -223,7 +223,7 @@ onUnmounted(() => clearInterval(timer))
|
|||||||
<div class="card p-4">
|
<div class="card p-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-xs text-white/55">累计生成</span>
|
<span class="text-xs text-white/55">累计生成</span>
|
||||||
<span class="w-7 h-7 rounded-lg bg-indigo-500/15 text-indigo-300 grid place-items-center ring-1 ring-indigo-400/20">
|
<span class="w-7 h-7 rounded-lg bg-sky-500/15 text-sky-300 grid place-items-center ring-1 ring-sky-400/20">
|
||||||
<Icon name="overview" class="w-3.5 h-3.5" />
|
<Icon name="overview" class="w-3.5 h-3.5" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -317,7 +317,7 @@ onUnmounted(() => clearInterval(timer))
|
|||||||
<div class="card p-4">
|
<div class="card p-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-xs text-white/55">邀请</span>
|
<span class="text-xs text-white/55">邀请</span>
|
||||||
<span class="w-7 h-7 rounded-lg bg-fuchsia-500/15 text-fuchsia-300 grid place-items-center ring-1 ring-fuchsia-400/20">
|
<span class="w-7 h-7 rounded-lg bg-rose-500/15 text-rose-300 grid place-items-center ring-1 ring-rose-400/20">
|
||||||
<Icon name="accounts" class="w-3.5 h-3.5" />
|
<Icon name="accounts" class="w-3.5 h-3.5" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -387,8 +387,8 @@ onUnmounted(() => clearInterval(timer))
|
|||||||
<div class="px-5 py-3 border-b border-white/[0.06] flex items-baseline justify-between">
|
<div class="px-5 py-3 border-b border-white/[0.06] flex items-baseline justify-between">
|
||||||
<h2 class="text-sm font-semibold">24 小时生成趋势</h2>
|
<h2 class="text-sm font-semibold">24 小时生成趋势</h2>
|
||||||
<div class="text-[11px] text-white/45 flex items-center gap-3">
|
<div class="text-[11px] text-white/45 flex items-center gap-3">
|
||||||
<span class="flex items-center gap-1"><span class="w-2 h-2 rounded-sm bg-indigo-400/80"></span>图像</span>
|
<span class="flex items-center gap-1"><span class="w-2 h-2 rounded-sm bg-sky-400/80"></span>图像</span>
|
||||||
<span class="flex items-center gap-1"><span class="w-2 h-2 rounded-sm bg-fuchsia-400/80"></span>视频</span>
|
<span class="flex items-center gap-1"><span class="w-2 h-2 rounded-sm bg-rose-400/80"></span>视频</span>
|
||||||
<span class="tabular-nums">峰值 {{ hourMax }}/h</span>
|
<span class="tabular-nums">峰值 {{ hourMax }}/h</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -402,8 +402,8 @@ onUnmounted(() => clearInterval(timer))
|
|||||||
whitespace-nowrap rounded-md bg-black/90 ring-1 ring-white/10 px-2 py-1 text-[10px] text-white/90 tabular-nums">
|
whitespace-nowrap rounded-md bg-black/90 ring-1 ring-white/10 px-2 py-1 text-[10px] text-white/90 tabular-nums">
|
||||||
{{ 23 - i }}h 前 · 图 {{ b.image }} / 视 {{ b.video }}
|
{{ 23 - i }}h 前 · 图 {{ b.image }} / 视 {{ b.video }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="b.video" class="bg-fuchsia-400/80 group-hover/bar:bg-fuchsia-400" :style="{ flex: b.video }"></div>
|
<div v-if="b.video" class="bg-rose-400/80 group-hover/bar:bg-rose-400" :style="{ flex: b.video }"></div>
|
||||||
<div v-if="b.image" class="bg-indigo-400/80 group-hover/bar:bg-indigo-400" :style="{ flex: b.image }"></div>
|
<div v-if="b.image" class="bg-sky-400/80 group-hover/bar:bg-sky-400" :style="{ flex: b.image }"></div>
|
||||||
<div v-if="!b.image && !b.video" class="bg-white/[0.06] group-hover/bar:bg-white/15 flex-1 rounded-t"></div>
|
<div v-if="!b.image && !b.video" class="bg-white/[0.06] group-hover/bar:bg-white/15 flex-1 rounded-t"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -448,7 +448,7 @@ onUnmounted(() => clearInterval(timer))
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-1.5 rounded-full bg-white/[0.06] overflow-hidden">
|
<div class="h-1.5 rounded-full bg-white/[0.06] overflow-hidden">
|
||||||
<div class="h-full rounded-full bg-gradient-to-r from-violet-400 to-fuchsia-500"
|
<div class="h-full rounded-full bg-gradient-to-r from-orange-400 to-rose-500"
|
||||||
:style="{ width: ((m.count / usageMax) * 100) + '%' }"></div>
|
:style="{ width: ((m.count / usageMax) * 100) + '%' }"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -544,7 +544,7 @@ onUnmounted(() => clearInterval(timer))
|
|||||||
class="flex items-center gap-3 px-2 py-2 text-xs rounded-lg hover:bg-white/[0.03]">
|
class="flex items-center gap-3 px-2 py-2 text-xs rounded-lg hover:bg-white/[0.03]">
|
||||||
<span class="w-1.5 h-1.5 rounded-full shrink-0" :class="logDot(e.status)"></span>
|
<span class="w-1.5 h-1.5 rounded-full shrink-0" :class="logDot(e.status)"></span>
|
||||||
<span class="text-[10px] uppercase tracking-wider font-medium w-9"
|
<span class="text-[10px] uppercase tracking-wider font-medium w-9"
|
||||||
:class="e.kind === 'video' ? 'text-fuchsia-300' : 'text-indigo-300'">
|
:class="e.kind === 'video' ? 'text-rose-300' : 'text-sky-300'">
|
||||||
{{ e.kind === 'video' ? '视频' : '图像' }}
|
{{ e.kind === 'video' ? '视频' : '图像' }}
|
||||||
</span>
|
</span>
|
||||||
<span class="font-mono text-white/85 truncate w-40 shrink-0">{{ e.model }}</span>
|
<span class="font-mono text-white/85 truncate w-40 shrink-0">{{ e.model }}</span>
|
||||||
|
|||||||
@@ -889,7 +889,7 @@ onUnmounted(() => {
|
|||||||
<div v-if="perSecondRate && secsRange">
|
<div v-if="perSecondRate && secsRange">
|
||||||
<label class="block text-xs font-medium text-slate-500 mb-1.5">时长 <span class="text-slate-400 font-normal tabular-nums">{{ secs }}s</span></label>
|
<label class="block text-xs font-medium text-slate-500 mb-1.5">时长 <span class="text-slate-400 font-normal tabular-nums">{{ secs }}s</span></label>
|
||||||
<input type="range" v-model.number="secs" :min="secsRange.min" :max="secsRange.max" step="1"
|
<input type="range" v-model.number="secs" :min="secsRange.min" :max="secsRange.max" step="1"
|
||||||
class="w-full accent-indigo-500 h-2 rounded-lg appearance-none bg-slate-200 cursor-pointer" />
|
class="w-full accent-sky-500 h-2 rounded-lg appearance-none bg-slate-200 cursor-pointer" />
|
||||||
<div class="flex justify-between text-[10px] text-slate-400 mt-0.5"><span>{{ secsRange.min }}s</span><span>{{ secsRange.max }}s</span></div>
|
<div class="flex justify-between text-[10px] text-slate-400 mt-0.5"><span>{{ secsRange.min }}s</span><span>{{ secsRange.max }}s</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="mode === 'video' && durations.length > 0 && !perSecondRate">
|
<div v-if="mode === 'video' && durations.length > 0 && !perSecondRate">
|
||||||
@@ -922,7 +922,7 @@ onUnmounted(() => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 flex-wrap items-start rounded-lg transition-colors"
|
<div class="flex gap-2 flex-wrap items-start rounded-lg transition-colors"
|
||||||
:class="dragOver ? 'ring-2 ring-indigo-400 ring-offset-2 bg-indigo-50/40' : ''"
|
:class="dragOver ? 'ring-2 ring-sky-400 ring-offset-2 bg-sky-50/40' : ''"
|
||||||
@drop="onDrop" @dragover="onDragOver" @dragleave="onDragLeave">
|
@drop="onDrop" @dragover="onDragOver" @dragleave="onDragLeave">
|
||||||
<div v-for="(img, i) in tileRefs" :key="img.mediaUrl || img.url || img.name + i"
|
<div v-for="(img, i) in tileRefs" :key="img.mediaUrl || img.url || img.name + i"
|
||||||
class="relative w-20 h-20 rounded-lg overflow-hidden border border-slate-200 bg-slate-50 transition-all">
|
class="relative w-20 h-20 rounded-lg overflow-hidden border border-slate-200 bg-slate-50 transition-all">
|
||||||
@@ -1058,7 +1058,7 @@ onUnmounted(() => {
|
|||||||
<div class="pg-cap text-[11px] leading-tight font-medium line-clamp-2 transition-colors"
|
<div class="pg-cap text-[11px] leading-tight font-medium line-clamp-2 transition-colors"
|
||||||
:class="item.prompt ? 'pointer-events-auto cursor-pointer' : ''"
|
:class="item.prompt ? 'pointer-events-auto cursor-pointer' : ''"
|
||||||
:title="item.prompt ? '点击复制提示词' : ''" @click.stop="copyPrompt(item)">{{ item.prompt }}</div>
|
:title="item.prompt ? '点击复制提示词' : ''" @click.stop="copyPrompt(item)">{{ item.prompt }}</div>
|
||||||
<div class="pg-cap-sub text-[9px] mt-0.5 font-mono truncate">{{ item.model }}<span v-if="item.deai"> · <span class="text-[#7c3aed]">去AI</span></span><span v-if="item.elapsed_ms"> · {{ (item.elapsed_ms / 1000).toFixed(1) }}s</span></div>
|
<div class="pg-cap-sub text-[9px] mt-0.5 font-mono truncate">{{ item.model }}<span v-if="item.deai"> · <span class="text-[#ec5b13]">去AI</span></span><span v-if="item.elapsed_ms"> · {{ (item.elapsed_ms / 1000).toFixed(1) }}s</span></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<!-- pending / running -->
|
<!-- pending / running -->
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ async function recharge() {
|
|||||||
<!-- header -->
|
<!-- header -->
|
||||||
<header class="flex items-end justify-between flex-wrap gap-4">
|
<header class="flex items-end justify-between flex-wrap gap-4">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-[10px] uppercase tracking-[0.3em] text-violet-300/70 font-medium">账户</div>
|
<div class="text-[10px] uppercase tracking-[0.3em] text-orange-300/70 font-medium">账户</div>
|
||||||
<h1 class="mt-2 text-4xl md:text-5xl font-bold tracking-tight">设置</h1>
|
<h1 class="mt-2 text-4xl md:text-5xl font-bold tracking-tight">设置</h1>
|
||||||
<p class="text-white/45 mt-2">API Key、密码、积分、登录状态 — 都在这里。</p>
|
<p class="text-white/45 mt-2">API Key、密码、积分、登录状态 — 都在这里。</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -235,7 +235,7 @@ async function recharge() {
|
|||||||
<!-- ===== Account info ===== -->
|
<!-- ===== Account info ===== -->
|
||||||
<section class="card p-6">
|
<section class="card p-6">
|
||||||
<div class="flex items-center gap-2 mb-4">
|
<div class="flex items-center gap-2 mb-4">
|
||||||
<Icon name="accounts" class="w-4 h-4 text-violet-300" />
|
<Icon name="accounts" class="w-4 h-4 text-orange-300" />
|
||||||
<h2 class="text-sm font-semibold">账户信息</h2>
|
<h2 class="text-sm font-semibold">账户信息</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 lg:grid-cols-5 gap-4 text-sm">
|
<div class="grid grid-cols-2 lg:grid-cols-5 gap-4 text-sm">
|
||||||
@@ -273,10 +273,10 @@ async function recharge() {
|
|||||||
<section class="lg:col-span-2 card overflow-hidden">
|
<section class="lg:col-span-2 card overflow-hidden">
|
||||||
<div class="p-7 md:p-8 relative">
|
<div class="p-7 md:p-8 relative">
|
||||||
<div class="absolute -top-16 -right-16 w-48 h-48 rounded-full opacity-30 blur-3xl"
|
<div class="absolute -top-16 -right-16 w-48 h-48 rounded-full opacity-30 blur-3xl"
|
||||||
style="background: radial-gradient(circle,#a855f7,transparent 60%)"></div>
|
style="background: radial-gradient(circle,#f97316,transparent 60%)"></div>
|
||||||
<div class="relative grid md:grid-cols-[260px_1fr] gap-8 items-start">
|
<div class="relative grid md:grid-cols-[260px_1fr] gap-8 items-start">
|
||||||
<div>
|
<div>
|
||||||
<div class="inline-grid w-10 h-10 rounded-xl bg-violet-500/15 ring-1 ring-violet-400/30 grid place-items-center text-violet-300">
|
<div class="inline-grid w-10 h-10 rounded-xl bg-orange-500/15 ring-1 ring-orange-400/30 grid place-items-center text-orange-300">
|
||||||
<Icon name="plug" class="w-4 h-4" />
|
<Icon name="plug" class="w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-xl font-bold mt-4">API Key</h2>
|
<h2 class="text-xl font-bold mt-4">API Key</h2>
|
||||||
@@ -323,7 +323,7 @@ async function recharge() {
|
|||||||
|
|
||||||
<p class="text-[11px] text-white/40 mt-3">
|
<p class="text-[11px] text-white/40 mt-3">
|
||||||
Key 由系统随机生成,不能手动填写。重新生成会让旧 Key 立刻失效。完整调用示例见
|
Key 由系统随机生成,不能手动填写。重新生成会让旧 Key 立刻失效。完整调用示例见
|
||||||
<router-link to="/docs" class="text-violet-300 underline">接口文档</router-link>。
|
<router-link to="/docs" class="text-orange-300 underline">接口文档</router-link>。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -384,7 +384,7 @@ async function recharge() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-5 flex items-center justify-between gap-3">
|
<div class="mt-5 flex items-center justify-between gap-3">
|
||||||
<span class="text-sm text-white/60">到账 <strong class="text-violet-300 text-base tabular-nums">{{ pointsPreview }}</strong> 积分</span>
|
<span class="text-sm text-white/60">到账 <strong class="text-orange-300 text-base tabular-nums">{{ pointsPreview }}</strong> 积分</span>
|
||||||
<button @click="recharge" :disabled="recharging"
|
<button @click="recharge" :disabled="recharging"
|
||||||
class="rounded-xl bg-white text-black hover:bg-white/90 disabled:opacity-60 disabled:cursor-not-allowed px-6 py-2.5 text-sm font-semibold transition-colors inline-flex items-center gap-2">
|
class="rounded-xl bg-white text-black hover:bg-white/90 disabled:opacity-60 disabled:cursor-not-allowed px-6 py-2.5 text-sm font-semibold transition-colors inline-flex items-center gap-2">
|
||||||
<span v-if="recharging" class="w-3.5 h-3.5 rounded-full border-2 border-black/30 border-t-black animate-spin"></span>
|
<span v-if="recharging" class="w-3.5 h-3.5 rounded-full border-2 border-black/30 border-t-black animate-spin"></span>
|
||||||
@@ -476,7 +476,7 @@ async function recharge() {
|
|||||||
.fade-enter-from, .fade-leave-to { opacity: 0; transform: translateY(8px); }
|
.fade-enter-from, .fade-leave-to { opacity: 0; transform: translateY(8px); }
|
||||||
|
|
||||||
/* Recharge amount / method buttons — theme-aware (clean in light AND dark).
|
/* Recharge amount / method buttons — theme-aware (clean in light AND dark).
|
||||||
Selected uses the inverted solid-button color, not a harsh violet. */
|
Selected uses the inverted solid-button color, not a harsh orange. */
|
||||||
.amt {
|
.amt {
|
||||||
border-radius: 0.6rem;
|
border-radius: 0.6rem;
|
||||||
padding: 0.6rem 0;
|
padding: 0.6rem 0;
|
||||||
|
|||||||
@@ -208,8 +208,8 @@ onMounted(refresh)
|
|||||||
<div class="absolute inset-0 bg-gradient-to-t from-black/85 via-black/30 to-transparent"></div>
|
<div class="absolute inset-0 bg-gradient-to-t from-black/85 via-black/30 to-transparent"></div>
|
||||||
<div class="absolute top-3 left-3 flex items-center gap-1.5">
|
<div class="absolute top-3 left-3 flex items-center gap-1.5">
|
||||||
<span class="text-[10px] uppercase tracking-wider px-1.5 py-0.5 rounded ring-1"
|
<span class="text-[10px] uppercase tracking-wider px-1.5 py-0.5 rounded ring-1"
|
||||||
:class="rec.kind === 'hero' ? 'bg-fuchsia-500/20 text-fuchsia-200 ring-fuchsia-400/30'
|
:class="rec.kind === 'hero' ? 'bg-rose-500/20 text-rose-200 ring-rose-400/30'
|
||||||
: rec.kind === 'bento' ? 'bg-violet-500/20 text-violet-200 ring-violet-400/30'
|
: rec.kind === 'bento' ? 'bg-orange-500/20 text-orange-200 ring-orange-400/30'
|
||||||
: 'bg-sky-500/20 text-sky-200 ring-sky-400/30'">
|
: 'bg-sky-500/20 text-sky-200 ring-sky-400/30'">
|
||||||
{{ rec.kind }}
|
{{ rec.kind }}
|
||||||
</span>
|
</span>
|
||||||
@@ -265,7 +265,7 @@ onMounted(refresh)
|
|||||||
<div class="p-5 space-y-4">
|
<div class="p-5 space-y-4">
|
||||||
<!-- live preview — click or drag an image here to upload as 底图 -->
|
<!-- live preview — click or drag an image here to upload as 底图 -->
|
||||||
<div class="relative rounded-2xl overflow-hidden ring-1 ring-white/10 aspect-[5/2] bg-white/[0.04] cursor-pointer transition-all"
|
<div class="relative rounded-2xl overflow-hidden ring-1 ring-white/10 aspect-[5/2] bg-white/[0.04] cursor-pointer transition-all"
|
||||||
:class="scDragOver ? 'ring-2 ring-indigo-400' : ''"
|
:class="scDragOver ? 'ring-2 ring-sky-400' : ''"
|
||||||
:style="bgFor(form.image)"
|
:style="bgFor(form.image)"
|
||||||
@click="pickShowcaseImg" @drop="onScDrop" @dragover.prevent="scDragOver = true" @dragleave="scDragOver = false">
|
@click="pickShowcaseImg" @drop="onScDrop" @dragover.prevent="scDragOver = true" @dragleave="scDragOver = false">
|
||||||
<div class="absolute inset-0 bg-gradient-to-t from-black/85 via-black/30 to-transparent"></div>
|
<div class="absolute inset-0 bg-gradient-to-t from-black/85 via-black/30 to-transparent"></div>
|
||||||
@@ -368,7 +368,7 @@ onMounted(refresh)
|
|||||||
<div v-if="!recentFiles.length" class="text-center text-xs text-[color:var(--fg-faint)] py-10">尚未有生成过的图片</div>
|
<div v-if="!recentFiles.length" class="text-center text-xs text-[color:var(--fg-faint)] py-10">尚未有生成过的图片</div>
|
||||||
<div v-else class="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 gap-2">
|
<div v-else class="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 gap-2">
|
||||||
<button v-for="f in recentFiles" :key="f.name" type="button" @click="pickImage(f)"
|
<button v-for="f in recentFiles" :key="f.name" type="button" @click="pickImage(f)"
|
||||||
class="relative aspect-square rounded-lg overflow-hidden ring-1 ring-white/10 hover:ring-fuchsia-400/60 transition-all">
|
class="relative aspect-square rounded-lg overflow-hidden ring-1 ring-white/10 hover:ring-rose-400/60 transition-all">
|
||||||
<img :src="generatedUrl(f.name)" loading="lazy" class="w-full h-full object-cover" />
|
<img :src="generatedUrl(f.name)" loading="lazy" class="w-full h-full object-cover" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { copyText } from '../utils/clipboard'
|
|||||||
import { points } from '../credits'
|
import { points } from '../credits'
|
||||||
import Icon from '../components/Icon.vue'
|
import Icon from '../components/Icon.vue'
|
||||||
import MediaLightbox from '../components/MediaLightbox.vue'
|
import MediaLightbox from '../components/MediaLightbox.vue'
|
||||||
import Mascot from '../components/Mascot.vue'
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const items = ref([]) // current server page
|
const items = ref([]) // current server page
|
||||||
@@ -71,7 +70,7 @@ onMounted(load)
|
|||||||
const isApi = (e) => e.source === 'v1'
|
const isApi = (e) => e.source === 'v1'
|
||||||
const sourceLabel = (e) => (isApi(e) ? 'API' : '画图台')
|
const sourceLabel = (e) => (isApi(e) ? 'API' : '画图台')
|
||||||
const sourcePill = (e) => (isApi(e)
|
const sourcePill = (e) => (isApi(e)
|
||||||
? 'bg-violet-50 text-violet-700 ring-violet-200'
|
? 'bg-orange-50 text-orange-700 ring-orange-200'
|
||||||
: 'bg-sky-50 text-sky-700 ring-sky-200')
|
: 'bg-sky-50 text-sky-700 ring-sky-200')
|
||||||
|
|
||||||
// 搜索走服务端(跨页),直接展示服务端返回的当页结果。
|
// 搜索走服务端(跨页),直接展示服务端返回的当页结果。
|
||||||
@@ -184,7 +183,7 @@ const params = (e) => {
|
|||||||
<div v-if="loading && !items.length" class="card text-center text-sm text-slate-400 py-24">加载中…</div>
|
<div v-if="loading && !items.length" class="card text-center text-sm text-slate-400 py-24">加载中…</div>
|
||||||
<div v-else-if="!total" class="anime-card relative overflow-hidden flex flex-col items-center gap-3 text-[color:var(--fg-3)] py-20">
|
<div v-else-if="!total" class="anime-card relative overflow-hidden flex flex-col items-center gap-3 text-[color:var(--fg-3)] py-20">
|
||||||
<div class="anime-stars"></div>
|
<div class="anime-stars"></div>
|
||||||
<Mascot :size="120" />
|
<span class="empty-badge">✦</span>
|
||||||
<span class="text-sm">还没有生成日志~</span>
|
<span class="text-sm">还没有生成日志~</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -219,7 +218,7 @@ const params = (e) => {
|
|||||||
<!-- API(v1) videos are no-store: file is an external provider URL
|
<!-- API(v1) videos are no-store: file is an external provider URL
|
||||||
(not a RustFS path), so it can't be previewed in-browser — show —. -->
|
(not a RustFS path), so it can't be previewed in-browser — show —. -->
|
||||||
<button v-if="e.status === 'success' && e.file && !e.file.startsWith('http')" @click="lightbox = e"
|
<button v-if="e.status === 'success' && e.file && !e.file.startsWith('http')" @click="lightbox = e"
|
||||||
class="block w-11 h-11 mx-auto rounded-lg overflow-hidden ring-1 ring-slate-200 hover:ring-fuchsia-300 transition-all">
|
class="block w-11 h-11 mx-auto rounded-lg overflow-hidden ring-1 ring-slate-200 hover:ring-rose-300 transition-all">
|
||||||
<img v-if="e.kind !== 'video' || !thumbFail[e.id]" :src="thumbUrl(e.file)" loading="lazy" class="w-full h-full object-cover"
|
<img v-if="e.kind !== 'video' || !thumbFail[e.id]" :src="thumbUrl(e.file)" loading="lazy" class="w-full h-full object-cover"
|
||||||
@error="e.kind === 'video' && (thumbFail[e.id] = true)" />
|
@error="e.kind === 'video' && (thumbFail[e.id] = true)" />
|
||||||
<video v-else :src="generatedUrl(e.file)" muted preload="metadata" class="w-full h-full object-cover" />
|
<video v-else :src="generatedUrl(e.file)" muted preload="metadata" class="w-full h-full object-cover" />
|
||||||
@@ -242,7 +241,7 @@ const params = (e) => {
|
|||||||
<div class="font-mono text-xs text-slate-800 break-all" :title="e.model">{{ e.model }}</div>
|
<div class="font-mono text-xs text-slate-800 break-all" :title="e.model">{{ e.model }}</div>
|
||||||
<div class="mt-0.5 flex items-center gap-1.5">
|
<div class="mt-0.5 flex items-center gap-1.5">
|
||||||
<span class="text-[10px] uppercase tracking-wider font-medium"
|
<span class="text-[10px] uppercase tracking-wider font-medium"
|
||||||
:class="e.kind === 'video' ? 'text-fuchsia-600' : 'text-indigo-600'">
|
:class="e.kind === 'video' ? 'text-rose-600' : 'text-sky-600'">
|
||||||
{{ e.kind === 'video' ? '视频' : '图像' }}
|
{{ e.kind === 'video' ? '视频' : '图像' }}
|
||||||
</span>
|
</span>
|
||||||
<span class="inline-flex items-center rounded px-1.5 py-px text-[10px] font-medium ring-1 whitespace-nowrap"
|
<span class="inline-flex items-center rounded px-1.5 py-px text-[10px] font-medium ring-1 whitespace-nowrap"
|
||||||
@@ -258,7 +257,7 @@ const params = (e) => {
|
|||||||
:title="e.error + ' — 点击复制'"
|
:title="e.error + ' — 点击复制'"
|
||||||
@click.stop="copyError(e)">⚠ {{ e.error }}</div>
|
@click.stop="copyError(e)">⚠ {{ e.error }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-3 align-middle text-xs text-slate-500 tabular-nums">{{ params(e) || '—' }}<span v-if="e.deai"> · <span class="text-[#7c3aed]">去AI特征</span></span></td>
|
<td class="px-3 py-3 align-middle text-xs text-slate-500 tabular-nums">{{ params(e) || '—' }}<span v-if="e.deai"> · <span class="text-[#ec5b13]">去AI特征</span></span></td>
|
||||||
<td class="px-3 py-3 align-middle text-right text-xs text-slate-700 tabular-nums">{{ e.cost ? points(e.cost) : '—' }}</td>
|
<td class="px-3 py-3 align-middle text-right text-xs text-slate-700 tabular-nums">{{ e.cost ? points(e.cost) : '—' }}</td>
|
||||||
<td class="px-3 py-3 align-middle text-right text-xs text-slate-500 tabular-nums">{{ e.elapsed_ms ? (e.elapsed_ms / 1000).toFixed(1) + 's' : '—' }}</td>
|
<td class="px-3 py-3 align-middle text-right text-xs text-slate-500 tabular-nums">{{ e.elapsed_ms ? (e.elapsed_ms / 1000).toFixed(1) + 's' : '—' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -300,14 +299,14 @@ const params = (e) => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* Row hover — light-theme twin of the admin 日志 page's .log-row: a subtle
|
/* Row hover — light-theme twin of the admin 日志 page's .log-row: a subtle
|
||||||
tint plus a violet accent bar on the left edge of the hovered row. */
|
tint plus a orange accent bar on the left edge of the hovered row. */
|
||||||
.log-table { border-collapse: separate; border-spacing: 0; }
|
.log-table { border-collapse: separate; border-spacing: 0; }
|
||||||
.log-row td {
|
.log-row td {
|
||||||
border-bottom: 1px solid rgb(15 23 42 / 0.06);
|
border-bottom: 1px solid rgb(15 23 42 / 0.06);
|
||||||
transition: background-color 0.15s ease, box-shadow 0.15s ease;
|
transition: background-color 0.15s ease, box-shadow 0.15s ease;
|
||||||
}
|
}
|
||||||
.log-row:hover td { background: rgb(15 23 42 / 0.025); }
|
.log-row:hover td { background: rgb(15 23 42 / 0.025); }
|
||||||
.log-row:hover td:first-child { box-shadow: inset 2px 0 0 rgb(124 58 237 / 0.6); }
|
.log-row:hover td:first-child { box-shadow: inset 2px 0 0 rgb(234 88 12 / 0.6); }
|
||||||
.log-row:last-child td { border-bottom: none; }
|
.log-row:last-child td { border-bottom: none; }
|
||||||
|
|
||||||
/* Numbered pagination buttons — light-theme twin of the admin 日志 page's .pg */
|
/* Numbered pagination buttons — light-theme twin of the admin 日志 page's .pg */
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { copyText } from '../utils/clipboard'
|
|||||||
import { zipSync } from 'fflate'
|
import { zipSync } from 'fflate'
|
||||||
import Icon from '../components/Icon.vue'
|
import Icon from '../components/Icon.vue'
|
||||||
import MediaLightbox from '../components/MediaLightbox.vue'
|
import MediaLightbox from '../components/MediaLightbox.vue'
|
||||||
import Mascot from '../components/Mascot.vue'
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
@@ -277,7 +276,7 @@ onUnmounted(() => {
|
|||||||
<div v-else-if="!filtered.length"
|
<div v-else-if="!filtered.length"
|
||||||
class="anime-card relative overflow-hidden flex flex-col items-center gap-3 text-[color:var(--fg-3)] py-20">
|
class="anime-card relative overflow-hidden flex flex-col items-center gap-3 text-[color:var(--fg-3)] py-20">
|
||||||
<div class="anime-stars"></div>
|
<div class="anime-stars"></div>
|
||||||
<Mascot :size="120" />
|
<span class="empty-badge">✦</span>
|
||||||
<span class="text-sm">还没有创作记录,画一张试试吧~</span>
|
<span class="text-sm">还没有创作记录,画一张试试吧~</span>
|
||||||
<button @click="router.push('/user')" class="bubble-btn mt-1">开始第一张 ✦</button>
|
<button @click="router.push('/user')" class="bubble-btn mt-1">开始第一张 ✦</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -328,7 +327,7 @@ onUnmounted(() => {
|
|||||||
<Icon name="check" class="w-3 h-3" />
|
<Icon name="check" class="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
<span class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider ring-1"
|
<span class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider ring-1"
|
||||||
:class="e.kind === 'video' ? 'bg-fuchsia-500/20 text-fuchsia-200 ring-fuchsia-400/30' : 'bg-indigo-500/20 text-indigo-200 ring-indigo-400/30'">
|
:class="e.kind === 'video' ? 'bg-rose-500/20 text-rose-200 ring-rose-400/30' : 'bg-sky-500/20 text-sky-200 ring-sky-400/30'">
|
||||||
{{ e.kind === 'video' ? '视频' : '图像' }}
|
{{ e.kind === 'video' ? '视频' : '图像' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -361,8 +360,8 @@ onUnmounted(() => {
|
|||||||
<span class="shrink-0 flex items-center gap-1">
|
<span class="shrink-0 flex items-center gap-1">
|
||||||
<span v-if="e.resolution" class="text-emerald-300/90">{{ e.resolution }}</span>
|
<span v-if="e.resolution" class="text-emerald-300/90">{{ e.resolution }}</span>
|
||||||
<span v-if="e.ratio" class="text-white/40">{{ e.ratio }}</span>
|
<span v-if="e.ratio" class="text-white/40">{{ e.ratio }}</span>
|
||||||
<span v-if="e.kind === 'video' && e.duration" class="text-fuchsia-300/80">{{ e.duration }}</span>
|
<span v-if="e.kind === 'video' && e.duration" class="text-rose-300/80">{{ e.duration }}</span>
|
||||||
<span v-if="e.deai" class="text-[#7c3aed]">去AI</span>
|
<span v-if="e.deai" class="text-[#ec5b13]">去AI</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[10px] text-white/35 mt-0.5 tabular-nums">{{ fmtTs(e.ts) }}<span v-if="e.elapsed_ms"> · {{ fmtMs(e.elapsed_ms) }}</span></div>
|
<div class="text-[10px] text-white/35 mt-0.5 tabular-nums">{{ fmtTs(e.ts) }}<span v-if="e.elapsed_ms"> · {{ fmtMs(e.elapsed_ms) }}</span></div>
|
||||||
@@ -437,6 +436,6 @@ onUnmounted(() => {
|
|||||||
.pick svg { opacity: 0; transition: opacity 0.15s; }
|
.pick svg { opacity: 0; transition: opacity 0.15s; }
|
||||||
.pick:hover { background: rgb(0 0 0 / 0.65); }
|
.pick:hover { background: rgb(0 0 0 / 0.65); }
|
||||||
.pick:hover svg { opacity: 0.6; }
|
.pick:hover svg { opacity: 0.6; }
|
||||||
.pick-on { background: rgb(217 70 239); box-shadow: inset 0 0 0 1.5px rgb(255 255 255 / 0.9); }
|
.pick-on { background: rgb(244 63 94); box-shadow: inset 0 0 0 1.5px rgb(255 255 255 / 0.9); }
|
||||||
.pick-on svg { opacity: 1; }
|
.pick-on svg { opacity: 1; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -221,8 +221,8 @@ async function doRecharge() {
|
|||||||
<div class="text-2xl font-semibold mt-1 tabular-nums text-emerald-300">{{ stats.active }}</div>
|
<div class="text-2xl font-semibold mt-1 tabular-nums text-emerald-300">{{ stats.active }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card p-4">
|
<div class="card p-4">
|
||||||
<div class="text-[11px] uppercase tracking-wider text-fuchsia-300/80">管理员</div>
|
<div class="text-[11px] uppercase tracking-wider text-rose-300/80">管理员</div>
|
||||||
<div class="text-2xl font-semibold mt-1 tabular-nums text-fuchsia-300">{{ stats.admins }}</div>
|
<div class="text-2xl font-semibold mt-1 tabular-nums text-rose-300">{{ stats.admins }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card p-4">
|
<div class="card p-4">
|
||||||
<div class="text-[11px] uppercase tracking-wider text-amber-300/80">总积分</div>
|
<div class="text-[11px] uppercase tracking-wider text-amber-300/80">总积分</div>
|
||||||
@@ -234,8 +234,8 @@ async function doRecharge() {
|
|||||||
<div class="card p-3 flex items-center gap-3 flex-wrap">
|
<div class="card p-3 flex items-center gap-3 flex-wrap">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<button @click="setFilter(() => roleFilter = '')" class="fp" :class="roleFilter === '' && 'fp-on'">全部角色</button>
|
<button @click="setFilter(() => roleFilter = '')" class="fp" :class="roleFilter === '' && 'fp-on'">全部角色</button>
|
||||||
<button @click="setFilter(() => roleFilter = 'admin')" class="fp" :class="roleFilter === 'admin' && 'fp-fuchsia'">
|
<button @click="setFilter(() => roleFilter = 'admin')" class="fp" :class="roleFilter === 'admin' && 'fp-orange'">
|
||||||
<span class="w-1.5 h-1.5 rounded-full bg-fuchsia-400"></span>管理员
|
<span class="w-1.5 h-1.5 rounded-full bg-rose-400"></span>管理员
|
||||||
</button>
|
</button>
|
||||||
<button @click="setFilter(() => roleFilter = 'agent')" class="fp" :class="roleFilter === 'agent' && 'fp-amber'">
|
<button @click="setFilter(() => roleFilter = 'agent')" class="fp" :class="roleFilter === 'agent' && 'fp-amber'">
|
||||||
<span class="w-1.5 h-1.5 rounded-full bg-amber-400"></span>代理
|
<span class="w-1.5 h-1.5 rounded-full bg-amber-400"></span>代理
|
||||||
@@ -340,12 +340,12 @@ async function doRecharge() {
|
|||||||
<td class="px-3 py-3.5 align-middle">
|
<td class="px-3 py-3.5 align-middle">
|
||||||
<span class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium ring-1 whitespace-nowrap"
|
<span class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium ring-1 whitespace-nowrap"
|
||||||
:class="u.role === 'admin'
|
:class="u.role === 'admin'
|
||||||
? 'bg-fuchsia-500/10 text-fuchsia-300 ring-fuchsia-400/30'
|
? 'bg-rose-500/10 text-rose-300 ring-rose-400/30'
|
||||||
: u.role === 'agent'
|
: u.role === 'agent'
|
||||||
? 'bg-amber-500/10 text-amber-300 ring-amber-400/30'
|
? 'bg-amber-500/10 text-amber-300 ring-amber-400/30'
|
||||||
: 'bg-white/[0.06] text-white/70 ring-white/15'">
|
: 'bg-white/[0.06] text-white/70 ring-white/15'">
|
||||||
<span class="w-1.5 h-1.5 rounded-full"
|
<span class="w-1.5 h-1.5 rounded-full"
|
||||||
:class="u.role === 'admin' ? 'bg-fuchsia-400' : u.role === 'agent' ? 'bg-amber-400' : 'bg-slate-400'"></span>
|
:class="u.role === 'admin' ? 'bg-rose-400' : u.role === 'agent' ? 'bg-amber-400' : 'bg-slate-400'"></span>
|
||||||
{{ roleLabel(u.role) }}
|
{{ roleLabel(u.role) }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -596,10 +596,10 @@ async function doRecharge() {
|
|||||||
color: rgb(253 164 175);
|
color: rgb(253 164 175);
|
||||||
box-shadow: inset 0 0 0 1px rgb(253 164 175 / 0.45);
|
box-shadow: inset 0 0 0 1px rgb(253 164 175 / 0.45);
|
||||||
}
|
}
|
||||||
.fp-fuchsia {
|
.fp-orange {
|
||||||
background: rgb(217 70 239 / 0.22);
|
background: rgb(249 115 22 / 0.22);
|
||||||
color: rgb(245 208 254);
|
color: rgb(253 186 116);
|
||||||
box-shadow: inset 0 0 0 1px rgb(245 208 254 / 0.45);
|
box-shadow: inset 0 0 0 1px rgb(253 186 116 / 0.45);
|
||||||
}
|
}
|
||||||
.fp-amber {
|
.fp-amber {
|
||||||
background: rgb(245 158 11 / 0.22);
|
background: rgb(245 158 11 / 0.22);
|
||||||
|
|||||||
Reference in New Issue
Block a user