@ -2,124 +2,162 @@
< div class = "upload-page" >
< div class = "upload-page" >
< NavBar / >
< NavBar / >
< div class = "container" >
< div class = "container" >
< h1 > { { isEditMode ? '编辑项目' : '上传项目' } } < / h1 >
< div class = "page-header" >
< form @submit.prevent ="handleSubmit" class = "upload-form" >
< h1 class = "glitch-text" : data -text = " isEditMode ? ' UPDATE PROTOCOL ' : ' INITIATE UPLOAD ' " >
{ { isEditMode ? 'UPDATE PROTOCOL' : 'INITIATE UPLOAD' } }
< / h1 >
< p class = "subtitle" > { { isEditMode ? 'MODIFY EXISTING PARAMETERS' : 'DEPLOY NEW MODULE TO NETWORK' } } < / p >
< / div >
< form @submit.prevent ="handleSubmit" class = "upload-form acid-card" >
< div class = "form-section" >
< div class = "form-section" >
< h2 > 基本信息 < / h2 >
< div class = "section-header" >
< div class = "form-group" >
< span class = "section-icon" > 📝 < / span >
< label > 标题 * < / label >
< h2 > CORE_DATA < / h2 >
< input
v - model = "form.title"
type = "text"
required
placeholder = "请输入项目标题"
class = "form-input"
/ >
< / div >
< div class = "form-group" >
< label > 描述 < / label >
< textarea
v - model = "form.description"
placeholder = "请输入项目描述"
rows = "4"
class = "form-textarea"
> < / textarea >
< / div >
< div class = "form-group" >
< label > 分类 < / label >
< input
v - model = "form.category"
type = "text"
placeholder = "请输入分类"
class = "form-input"
/ >
< / div >
< / div >
< div class = "form-group" >
< div class = "form-grid" >
< label > 标签 ( 用逗号分隔 ) < / label >
< div class = "form-group full-width" >
< input
< label > PROJECT TITLE < span class = "required" > * < / span > < / label >
v - model = "form.tags"
< input
type = "text"
v - model = "form.title"
placeholder = "例如:vue, react, demo"
type = "text"
class = "form-input"
required
/ >
placeholder = "ENTER IDENTIFIER"
class = "form-input"
/ >
< / div >
< div class = "form-group full-width" >
< label > DESCRIPTION < / label >
< textarea
v - model = "form.description"
placeholder = "SYSTEM FUNCTIONS AND SPECS..."
rows = "4"
class = "form-textarea"
> < / textarea >
< / div >
< div class = "form-group" >
< label > CATEGORY < / label >
< div class = "input-wrapper" >
< input
v - model = "form.category"
type = "text"
placeholder = "CLASSIFICATION"
class = "form-input"
/ >
< / div >
< / div >
< div class = "form-group" >
< label > TAGS < / label >
< div class = "input-wrapper" >
< input
v - model = "form.tags"
type = "text"
placeholder = "KEYWORDS (COMMA SEPARATED)"
class = "form-input"
/ >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< div v-if ="!isEditMode" class="form-section" >
< div v-if ="!isEditMode" class="form-section" >
< h2 > 网页文件 * < / h2 >
< div class = "section-header" >
< div class = "file-upload" >
< span class = "section-icon" > 📦 < / span >
< h2 > SOURCE_FILE < span class = "required" > * < / span > < / h2 >
< / div >
< div class = "file-upload-area" : class = "{ 'has-file': form.file }" >
< input
< input
ref = "fileInput"
ref = "fileInput"
type = "file"
type = "file"
accept = ".html,.zip"
accept = ".html,.zip"
@ change = "handleFileChange"
@ change = "handleFileChange"
class = "file-input"
class = "file-input-hidden "
/ >
/ >
< div class = "file-display" >
< div class = "upload-content" @click ="$refs.fileInput.click()" >
< p v-if ="form.file" > {{ form.file.name }} < / p >
< div class = "upload-icon" > { { form . file ? '📄' : '☁️' } } < / div >
< p v -else class = "placeholder" > 请选择HTML文件或ZIP压缩包 < / p >
< div class = "upload-text" >
< p class = "main-text" > { { form . file ? form . file . name : 'DRAG & DROP OR CLICK' } } < / p >
< p class = "sub-text" > { { form . file ? ( form . file . size / 1024 ) . toFixed ( 2 ) + ' KB' : 'ACCEPTS .HTML OR .ZIP ARCHIVE' } } < / p >
< / div >
< button type = "button" class = "btn-select" >
{ { form . file ? 'REPLACE FILE' : 'SELECT FILE' } }
< / button >
< / div >
< / div >
< button type = "button" @click ="$refs.fileInput.click()" class = "btn-select" >
选择文件
< / button >
< / div >
< / div >
< / div >
< / div >
< div v-if ="!isEditMode" class="form-section" >
< div v-if ="!isEditMode" class="form-section" >
< h2 > 附件 ( 可选 ) < / h2 >
< div class = "section-header" >
< div class = "file-upload" >
< span class = "section-icon" > 📎 < / span >
< h2 > ATTACHMENTS < span class = "optional" > ( OPTIONAL ) < / span > < / h2 >
< / div >
< div class = "file-list-upload" >
< input
< input
ref = "attachmentInput"
ref = "attachmentInput"
type = "file"
type = "file"
multiple
multiple
@ change = "handleAttachmentChange"
@ change = "handleAttachmentChange"
class = "file-input"
class = "file-input-hidden "
/ >
/ >
< div class = "file-list" >
< div class = "file-list" v-if ="form.attachments.length > 0" >
< div v-for ="(file, index) in form.attachments" :key="index" class="file-item" >
< div v-for ="(file, index) in form.attachments" :key="index" class="file-item" >
< span > { { file . name } } < / span >
< span class = "file-icon" > 📎 < / span >
< span class = "file-name" > { { file . name } } < / span >
< span class = "file-size" > { { ( file . size / 1024 ) . toFixed ( 2 ) } } KB < / span >
< button type = "button" @click ="removeAttachment(index)" class = "btn-remove" > × < / button >
< button type = "button" @click ="removeAttachment(index)" class = "btn-remove" > × < / button >
< / div >
< / div >
< / div >
< / div >
< button type = "button" @click ="$refs.attachmentInput.click()" class = "btn-select " >
< button type = "button" @click ="$refs.attachmentInput.click()" class = "btn-add-file " >
添加附件
[ + ADD ATTACHMENT ]
< / button >
< / button >
< / div >
< / div >
< / div >
< / div >
< div v-if ="!isEditMode" class="form-section" >
< div v-if ="!isEditMode" class="form-section" >
< h2 > Markdown文档 ( 可选 ) < / h2 >
< div class = "section-header" >
< div class = "file-upload" >
< span class = "section-icon" > 📘 < / span >
< h2 > DOCUMENTATION < span class = "optional" > ( MARKDOWN ) < / span > < / h2 >
< / div >
< div class = "file-list-upload" >
< input
< input
ref = "documentInput"
ref = "documentInput"
type = "file"
type = "file"
accept = ".md,.markdown"
accept = ".md,.markdown"
multiple
multiple
@ change = "handleDocumentChange"
@ change = "handleDocumentChange"
class = "file-input"
class = "file-input-hidden "
/ >
/ >
< div class = "file-list" >
< div class = "file-list" v-if ="form.documents.length > 0" >
< div v-for ="(file, index) in form.documents" :key="index" class="file-item" >
< div v-for ="(file, index) in form.documents" :key="index" class="file-item document-type" >
< span > { { file . name } } < / span >
< span class = "file-icon" > 📝 < / span >
< span class = "file-name" > { { file . name } } < / span >
< button type = "button" @click ="removeDocument(index)" class = "btn-remove" > × < / button >
< button type = "button" @click ="removeDocument(index)" class = "btn-remove" > × < / button >
< / div >
< / div >
< / div >
< / div >
< button type = "button" @click ="$refs.documentInput.click()" class = "btn-select " >
< button type = "button" @click ="$refs.documentInput.click()" class = "btn-add-file " >
添加文档
[ + ADD DOC ]
< / button >
< / button >
< / div >
< / div >
< / div >
< / div >
< div v-if ="error" class="error-message" > {{ error }} < / div >
< div v-if ="error" class="error-message" >
< div v-if ="uploadProgress > 0 && uploadProgress < 100" class="progress" >
< span class = "error-icon" > ⚠ ️ < / span > { { error } }
< div class = "progress-bar" : style = "{ width: uploadProgress + '%' }" > < / div >
< / div >
< span class = "progress-text" > { { uploadProgress } } % < / span >
< div v-if ="uploadProgress > 0 && uploadProgress < 100" class="progress-container" >
< div class = "progress-info" >
< span > UPLOADING ... < / span >
< span > { { uploadProgress } } % < / span >
< / div >
< div class = "progress-track" >
< div class = "progress-bar" : style = "{ width: uploadProgress + '%' }" > < / div >
< / div >
< / div >
< / div >
< div class = "form-actions" >
< div class = "form-actions" >
< button type = "button" @click ="$router.back()" class = "btn-cancel" > 取消 < / button >
< button type = "button" @click ="$router.back()" class = "btn-cancel" > CANCEL < / button >
< button type = "submit" : disabled = "loading || (!isEditMode && !form.file)" class = "btn-submit" >
< button type = "submit" : disabled = "loading || (!isEditMode && !form.file)" class = "btn btn-primary btn -submit" >
{ { loading ? ( isEditMode ? '保存中...' : '上传中...' ) : ( isEditMode ? '保存' : '上传' ) } }
{ { loading ? ( isEditMode ? 'SAVING...' : 'UPLOADING ...' ) : ( isEditMode ? 'SAVE CHANGES' : 'DEPLOY NOW ' ) } }
< / button >
< / button >
< / div >
< / div >
< / form >
< / form >
@ -152,6 +190,9 @@ const form = ref({
const loading = ref ( false ) ;
const loading = ref ( false ) ;
const error = ref ( '' ) ;
const error = ref ( '' ) ;
const uploadProgress = ref ( 0 ) ;
const uploadProgress = ref ( 0 ) ;
const fileInput = ref < HTMLInputElement | null > ( null ) ;
const attachmentInput = ref < HTMLInputElement | null > ( null ) ;
const documentInput = ref < HTMLInputElement | null > ( null ) ;
const handleFileChange = ( e : Event ) => {
const handleFileChange = ( e : Event ) => {
const target = e . target as HTMLInputElement ;
const target = e . target as HTMLInputElement ;
@ -163,14 +204,14 @@ const handleFileChange = (e: Event) => {
const handleAttachmentChange = ( e : Event ) => {
const handleAttachmentChange = ( e : Event ) => {
const target = e . target as HTMLInputElement ;
const target = e . target as HTMLInputElement ;
if ( target . files ) {
if ( target . files ) {
form . value . attachments = Array . from ( target . files ) ;
form . value . attachments = [ ... form . value . attachments , ... Array . from ( target . files ) ] ;
}
}
} ;
} ;
const handleDocumentChange = ( e : Event ) => {
const handleDocumentChange = ( e : Event ) => {
const target = e . target as HTMLInputElement ;
const target = e . target as HTMLInputElement ;
if ( target . files ) {
if ( target . files ) {
form . value . documents = Array . from ( target . files ) ;
form . value . documents = [ ... form . value . documents , ... Array . from ( target . files ) ] ;
}
}
} ;
} ;
@ -189,15 +230,14 @@ const loadProjectForEdit = async (id: number) => {
form . value . description = project . description || '' ;
form . value . description = project . description || '' ;
form . value . category = project . category || '' ;
form . value . category = project . category || '' ;
form . value . tags = project . tags ? project . tags . join ( ', ' ) : '' ;
form . value . tags = project . tags ? project . tags . join ( ', ' ) : '' ;
/ / 编 辑 模 式 下 文 件 是 可 选 的
} catch ( err : any ) {
} catch ( err : any ) {
error . value = '加载项目失败 ' ;
error . value = 'LOAD FAILED ' ;
}
}
} ;
} ;
const handleSubmit = async ( ) => {
const handleSubmit = async ( ) => {
if ( ! isEditMode . value && ! form . value . file ) {
if ( ! isEditMode . value && ! form . value . file ) {
error . value = '请选择网页文件 ' ;
error . value = 'SOURCE FILE REQUIRED ' ;
return ;
return ;
}
}
@ -207,7 +247,6 @@ const handleSubmit = async () => {
try {
try {
if ( isEditMode . value && editProjectId . value ) {
if ( isEditMode . value && editProjectId . value ) {
/ / 编 辑 模 式 : 只 更 新 基 本 信 息
const updateData : any = {
const updateData : any = {
title : form . value . title ,
title : form . value . title ,
description : form . value . description ,
description : form . value . description ,
@ -215,19 +254,17 @@ const handleSubmit = async () => {
tags : form . value . tags
tags : form . value . tags
} ;
} ;
const response = await projectApi . update ( editProjectId . value , updateData ) ;
await projectApi . update ( editProjectId . value , updateData ) ;
router . push ( ` /project/ ${ editProjectId . value } ` ) ;
router . push ( ` /project/ ${ editProjectId . value } ` ) ;
return ;
return ;
}
}
/ / 创 建 模 式 : 上 传 文 件
const formData = new FormData ( ) ;
const formData = new FormData ( ) ;
formData . append ( 'title' , form . value . title ) ;
formData . append ( 'title' , form . value . title ) ;
if ( form . value . description ) formData . append ( 'description' , form . value . description ) ;
if ( form . value . description ) formData . append ( 'description' , form . value . description ) ;
if ( form . value . category ) formData . append ( 'category' , form . value . category ) ;
if ( form . value . category ) formData . append ( 'category' , form . value . category ) ;
if ( form . value . tags ) formData . append ( 'tags' , form . value . tags ) ;
if ( form . value . tags ) formData . append ( 'tags' , form . value . tags ) ;
/ / 判 断 是 H T M L 还 是 Z I P
const isZip = form . value . file ! . name . endsWith ( '.zip' ) ;
const isZip = form . value . file ! . name . endsWith ( '.zip' ) ;
if ( isZip ) {
if ( isZip ) {
formData . append ( 'file' , form . value . file ! ) ;
formData . append ( 'file' , form . value . file ! ) ;
@ -243,7 +280,6 @@ const handleSubmit = async () => {
formData . append ( 'documents' , file ) ;
formData . append ( 'documents' , file ) ;
} ) ;
} ) ;
/ / 模 拟 上 传 进 度
const progressInterval = setInterval ( ( ) => {
const progressInterval = setInterval ( ( ) => {
if ( uploadProgress . value < 90 ) {
if ( uploadProgress . value < 90 ) {
uploadProgress . value += 10 ;
uploadProgress . value += 10 ;
@ -258,11 +294,10 @@ const handleSubmit = async () => {
router . replace ( ` /project/ ${ response . project . id } ` ) ;
router . replace ( ` /project/ ${ response . project . id } ` ) ;
} , 500 ) ;
} , 500 ) ;
} catch ( err : any ) {
} catch ( err : any ) {
const errorMessage = err . response ? . data ? . error ? . message || '操作失败,请重试 ' ;
const errorMessage = err . response ? . data ? . error ? . message || 'OPERATION FAILED ' ;
error . value = errorMessage ;
error . value = errorMessage ;
uploadProgress . value = 0 ;
uploadProgress . value = 0 ;
/ / 如 果 是 4 0 1 错 误 , 不 显 示 错 误 信 息 , 让 拦 截 器 处 理 跳 转
if ( err . response ? . status === 401 ) {
if ( err . response ? . status === 401 ) {
return ;
return ;
}
}
@ -287,95 +322,217 @@ onMounted(() => {
< style scoped >
< style scoped >
. upload - page {
. upload - page {
min - height : 100 vh ;
min - height : 100 vh ;
background : # f5f5f5 ;
background : var ( -- bg - body ) ;
}
}
. container {
. container {
max - width : 8 00px ;
max - width : 9 00px ;
margin : 0 auto ;
margin : 0 auto ;
padding : 2 rem ;
padding : 2 rem ;
}
}
. container h1 {
. page - header {
margin - bottom : 2 rem ;
text - align : center ;
color : # 333 ;
margin - bottom : 3 rem ;
}
. page - header h1 {
font - size : 2.5 rem ;
color : white ;
margin - bottom : 0.5 rem ;
font - weight : 900 ;
letter - spacing : - 1 px ;
}
. subtitle {
color : var ( -- color - text - muted ) ;
font - size : 1 rem ;
font - family : var ( -- font - family - mono ) ;
letter - spacing : 2 px ;
}
}
. upload - form {
. upload - form {
background : white ;
padding : 2.5 rem ;
border - radius : 12 px ;
background : rgba ( var ( -- color - surface - rgb ) , 0.8 ) ;
padding : 2 rem ;
backdrop - filter : blur ( 10 px ) ;
box - shadow : 0 2 px 8 px rgba ( 0 , 0 , 0 , 0.1 ) ;
}
}
. form - section {
. form - section {
margin - bottom : 2 rem ;
margin - bottom : 2.5 rem ;
padding - bottom : 2 rem ;
padding - bottom : 2 rem ;
border - bottom : 1 px solid # eee ;
border - bottom : 1 px dashed var ( -- border - color ) ;
}
}
. form - section : last - child {
. form - section : last - of - type {
border - bottom : none ;
border - bottom : none ;
}
}
. form - section h2 {
. section - header {
font - size : 1.25 rem ;
display : flex ;
margin - bottom : 1 rem ;
align - items : center ;
color : # 333 ;
gap : 1 rem ;
margin - bottom : 1.5 rem ;
}
. section - icon {
font - size : 1.5 rem ;
filter : drop - shadow ( 0 0 5 px var ( -- secondary ) ) ;
}
. section - header h2 {
font - size : 1.2 rem ;
color : var ( -- secondary ) ;
margin : 0 ;
font - weight : 700 ;
font - family : var ( -- font - family - mono ) ;
letter - spacing : 1 px ;
}
. optional {
font - size : 0.8 rem ;
color : var ( -- color - text - muted ) ;
font - weight : normal ;
margin - left : 0.5 rem ;
}
. required {
color : var ( -- accent ) ;
margin - left : 4 px ;
}
. form - grid {
display : grid ;
grid - template - columns : 1 fr 1 fr ;
gap : 1.5 rem ;
}
. full - width {
grid - column : span 2 ;
}
}
. form - group {
. form - group {
margin - bottom : 1.5 rem ;
display : flex ;
flex - direction : column ;
gap : 0.5 rem ;
}
}
. form - group label {
. form - group label {
display : block ;
font - weight : 600 ;
margin - bottom : 0.5 rem ;
color : var ( -- primary ) ;
font - weight : 500 ;
font - size : 0.7 rem ;
color : # 666 ;
font - family : var ( -- font - family - mono ) ;
letter - spacing : 1 px ;
}
. input - wrapper {
position : relative ;
}
}
. form - input ,
. form - input ,
. form - textarea {
. form - textarea {
width : 100 % ;
width : 100 % ;
padding : 0.75 rem ;
padding : 0.8 rem 1 rem ;
border : 1 px solid # ddd ;
border : 1 px solid var ( -- border - color ) ;
border - radius : 8 px ;
background : rgba ( 0 , 0 , 0 , 0.3 ) ;
font - size : 1 rem ;
color : white ;
font - family : var ( -- font - family - mono ) ;
font - size : 0.9 rem ;
outline : none ;
outline : none ;
transition : border - color 0.2 s ;
transition : all var ( -- transition - fast ) ;
font - family : inherit ;
}
}
. form - input : focus ,
. form - input : focus ,
. form - textarea : focus {
. form - textarea : focus {
border - color : # 667 eea ;
border - color : var ( -- primary ) ;
box - shadow : 0 0 10 px rgba ( var ( -- color - primary - rgb ) , 0.2 ) ;
background : black ;
}
}
. file - upload {
/* File Upload Styling */
display : flex ;
. file - upload - area {
flex - direction : column ;
border : 1 px dashed var ( -- border - color ) ;
gap : 1 rem ;
padding : 3 rem ;
text - align : center ;
cursor : pointer ;
transition : all var ( -- transition - fast ) ;
background : rgba ( 0 , 0 , 0 , 0.2 ) ;
position : relative ;
overflow : hidden ;
}
. file - upload - area : hover {
border - color : var ( -- primary ) ;
background : rgba ( var ( -- color - primary - rgb ) , 0.05 ) ;
box - shadow : inset 0 0 20 px rgba ( var ( -- color - primary - rgb ) , 0.1 ) ;
}
}
. file - input {
. file - upload - area . has - file {
border - style : solid ;
border - color : var ( -- primary ) ;
background : rgba ( var ( -- color - primary - rgb ) , 0.05 ) ;
}
. file - input - hidden {
display : none ;
display : none ;
}
}
. file - display {
. upload - content {
padding : 1 rem ;
background : # f9f9f9 ;
border - radius : 8 px ;
min - height : 3 rem ;
display : flex ;
display : flex ;
flex - direction : column ;
align - items : center ;
align - items : center ;
gap : 1 rem ;
position : relative ;
z - index : 1 ;
}
. upload - icon {
font - size : 3 rem ;
margin - bottom : 0.5 rem ;
filter : drop - shadow ( 0 0 10 px var ( -- primary ) ) ;
}
}
. placeholder {
. upload - text . main - text {
color : # 999 ;
font - weight : 700 ;
color : white ;
margin - bottom : 0.2 rem ;
font - family : var ( -- font - family - mono ) ;
letter - spacing : 1 px ;
}
. upload - text . sub - text {
color : var ( -- color - text - muted ) ;
font - size : 0.8 rem ;
font - family : var ( -- font - family - mono ) ;
}
. btn - select {
padding : 0.6 rem 1.5 rem ;
background : transparent ;
border : 1 px solid var ( -- primary ) ;
font - weight : 600 ;
color : var ( -- primary ) ;
cursor : pointer ;
transition : all var ( -- transition - fast ) ;
font - family : var ( -- font - family - mono ) ;
margin - top : 1 rem ;
}
. file - upload - area : hover . btn - select {
background : var ( -- primary ) ;
color : black ;
box - shadow : 0 0 10 px var ( -- primary ) ;
}
/* File List */
. file - list - upload {
display : flex ;
flex - direction : column ;
gap : 1 rem ;
align - items : flex - start ;
}
}
. file - list {
. file - list {
width : 100 % ;
display : flex ;
display : flex ;
flex - direction : column ;
flex - direction : column ;
gap : 0.5 rem ;
gap : 0.5 rem ;
@ -383,109 +540,140 @@ onMounted(() => {
. file - item {
. file - item {
display : flex ;
display : flex ;
justify - content : space - between ;
align - items : center ;
align - items : center ;
padding : 0.5 rem ;
gap : 1 rem ;
background : # f9f9f9 ;
padding : 0.8 rem ;
border - radius : 6 px ;
background : rgba ( 255 , 255 , 255 , 0.05 ) ;
border : 1 px solid var ( -- border - color ) ;
transition : all var ( -- transition - fast ) ;
}
}
. btn - remove {
. file - item : hover {
background : # e74c3c ;
border - color : var ( -- secondary ) ;
color : white ;
background : rgba ( var ( -- color - secondary - rgb ) , 0.1 ) ;
border : none ;
border - radius : 4 px ;
width : 24 px ;
height : 24 px ;
cursor : pointer ;
font - size : 1.2 rem ;
line - height : 1 ;
}
}
. btn - select {
. file - name {
padding : 0.75 rem 1.5 rem ;
flex : 1 ;
background : # 667 eea ;
color : white ;
border : none ;
border - radius : 8 px ;
cursor : pointer ;
font - weight : 500 ;
font - weight : 500 ;
align - self : flex - start ;
color : white ;
font - family : var ( -- font - family - mono ) ;
font - size : 0.9 rem ;
}
}
. btn - select : hover {
. file - size {
background : # 5568 d3 ;
color : var ( -- color - text - muted ) ;
font - size : 0.8 rem ;
font - family : var ( -- font - family - mono ) ;
}
}
. progress {
. btn - remove {
margin : 1 rem 0 ;
width : 24 px ;
background : # f0f0f0 ;
height : 24 px ;
border - radius : 8 px ;
border - radius : 0 ;
height : 32 px ;
border : 1 px solid var ( -- accent ) ;
position : relative ;
background : transparent ;
overflow : hidden ;
color : var ( -- accent ) ;
cursor : pointer ;
display : flex ;
align - items : center ;
justify - content : center ;
font - size : 1 rem ;
line - height : 1 ;
transition : all var ( -- transition - fast ) ;
}
}
. progress - bar {
. btn - remove : hove r {
height : 100 % ;
background : var ( -- accent ) ;
background : linear - gradient ( 135 deg , # 667 eea 0 % , # 764 ba2 100 % ) ;
color : white ;
transition : width 0.3 s ;
box - shadow : 0 0 8 px var ( -- accent ) ;
}
}
. progress - text {
. btn - add - file {
position : absolute ;
padding : 0.6 rem 1.2 rem ;
top : 50 % ;
background : transparent ;
left : 50 % ;
border : 1 px dashed var ( -- border - color ) ;
transform : translate ( - 50 % , - 50 % ) ;
color : var ( -- color - text - muted ) ;
font - weight : 600 ;
font - weight : 600 ;
color : # 333 ;
cursor : pointer ;
transition : all var ( -- transition - fast ) ;
font - family : var ( -- font - family - mono ) ;
font - size : 0.8 rem ;
}
}
. error - message {
. btn - add - file : hover {
color : # e74c3c ;
border - color : var ( -- secondary ) ;
padding : 1 rem ;
color : var ( -- secondary ) ;
background : # fee ;
background : rgba ( var ( -- color - secondary - rgb ) , 0.1 ) ;
border - radius : 8 px ;
box - shadow : 0 0 10 px rgba ( var ( -- color - secondary - rgb ) , 0.2 ) ;
margin - bottom : 1 rem ;
}
}
/* Actions */
. form - actions {
. form - actions {
display : flex ;
display : flex ;
justify - content : flex - end ;
justify - content : flex - end ;
gap : 1 rem ;
gap : 1 rem ;
margin - top : 2 rem ;
margin - top : 3 rem ;
padding - top : 2 rem ;
border - top : 1 px solid var ( -- border - color ) ;
}
}
. btn - cancel {
. btn - cancel {
padding : 0.75 rem 2 rem ;
padding : 0.8 rem 2 rem ;
background : # f0f0f0 ;
background : transparent ;
border : none ;
border : 1 px solid var ( -- border - color ) ;
border - radius : 8 px ;
color : var ( -- color - text - muted ) ;
font - weight : 600 ;
cursor : pointer ;
cursor : pointer ;
font - weight : 500 ;
transition : all var ( -- transition - fast ) ;
font - family : var ( -- font - family - mono ) ;
}
}
. btn - cancel : hover {
. btn - cancel : hover {
background : # e0e0e0 ;
border - color : white ;
color : white ;
}
}
. btn - submit {
. btn - submit {
padding : 0.75 rem 2 rem ;
min - width : 160 px ;
background : linear - gradient ( 135 deg , # 667 eea 0 % , # 764 ba2 100 % ) ;
}
color : white ;
border : none ;
. error - message {
border - radius : 8 px ;
margin - top : 1.5 rem ;
cursor : pointer ;
background : rgba ( var ( -- color - accent - rgb ) , 0.1 ) ;
font - weight : 600 ;
color : var ( -- accent ) ;
transition : transform 0.2 s ;
padding : 1 rem ;
border : 1 px solid var ( -- accent ) ;
display : flex ;
align - items : center ;
gap : 0.5 rem ;
font - family : var ( -- font - family - mono ) ;
font - size : 0.9 rem ;
}
. progress - container {
margin - top : 1.5 rem ;
}
}
. btn - submit : hover : not ( : disabled ) {
. progress - info {
transform : translateY ( - 2 px ) ;
display : flex ;
justify - content : space - between ;
font - size : 0.8 rem ;
color : var ( -- primary ) ;
margin - bottom : 0.5 rem ;
font - family : var ( -- font - family - mono ) ;
}
}
. btn - submit : disabled {
. progress - track {
opacity : 0.6 ;
height : 4 px ;
cursor : not - allowed ;
background : # 333 ;
overflow : hidden ;
}
. progress - bar {
height : 100 % ;
background : var ( -- primary ) ;
transition : width 0.2 s ease ;
box - shadow : 0 0 10 px var ( -- primary ) ;
}
}
< / style >
< / style >