إنتقل إلى المحتوى الرئيسي

ابنِ أول تطبيق استوديو لك

الهدف​

ابنِ تطبيقًا عاملًا من لا شيء: جدولًا يحمل السجلات، وشاشةً تعرضها، وشريط أوامر يعمل على السجل الذي تؤشِّره، ودرجًا يكتب سجلًّا جديدًا.

يستغرق نحو أربعين دقيقة في المرة الأولى. لا شيء هنا لعبة — كل خطوة هي الخطوة نفسها التي تخطوها لتطبيق حقيقي، وما ينتهي بين يديك هو تقريبًا نموذج مكتب الدعم.

اقرأ النسخة الجاهزة أولًا

studio-starter-app في samples/solutions هو هذا التطبيق مرسومًا. استورده من الحلول ← استيراد، وافتحه، واضغط أزراره. العودة إلى هنا بعد ذلك تُقصِّر كل خطوة أدناه.

قبل أن تبدأ​

  • أنت داخل بحساب صانع.
  • اختيرت بيئة وحل في المنتقيين أعلى الصفحة. لا شيء تصنعه أدناه يوجد خارجهما.
  • تعرف بادئة الناشر لحلّك — الحرفين أو الثلاثة قبل كل اسم منطقي. تكتب هذه الصفحة par؛ وقد تختلف بادئتك.

١. جدول يحمل السجلات​

تطبيق لا يعرض شيئًا هو رسم. ابدأ بالجدول.

الجداول ← جديد، واملأ:

الحقلالقيمة
الاسم المعروضCase
اسم الجمعCases
الاسم المنطقيpar_case (تُضاف البادئة تلقائيًا)
الحقل الأساسيSummary

احفظ، ثم أضف ثلاثة حقول في تبويب الحقول:

الاسم المعروضالاسم المنطقيالنوع
Prioritypar_priorityاختيار — مجموعة خيارات جديدة par_case_priority بقيم Low وNormal وHigh
Urgentpar_urgentنعم/لا
Notespar_notesنص متعدد الأسطر
حقل نعم/لا يبدأ بـ لا شيء، لا بـ لا

السجل الذي يُنشأ دون لمس Urgent لا يحمل له قيمة إطلاقًا — و«لا قيمة» ليست قيمة منطقية. لذا فإن تعبيرًا مثل record.par_urgent ? "Yes" : "No" يفشل على تلك السجلات بالذات برسالة Only booleans are allowed in this expression. اكتب (record.par_urgent ?? false) حيثما قرأت حقلًا اختياريًّا، فتزول المسألة. هذه أشيع مفاجأة في أول تطبيق.


٢. التطبيق​

تطبيقات الاستوديو ← جديد:

الحقلالقيمة
الاسم المعروضSupport desk
الاسم المنطقيpar_support
الوصفجملة واحدة. هي ما تعرضه بطاقة المشغِّل تحت الاسم.
الأيقونةاضغط المنتقي واختر واحدة — LifeBuoy تناسب تطبيق دعم
مُدرَج في القشرةمفعَّل

اضغط حفظ ثم تصميم. تُفتح اللوحة.


٣. الشاشة، وما تعنيه الشاشة​

ارسم إطارًا واحدًا يملأ اللوحة — نحو ١٢٠٠ × ٧٦٠ مقاس مريح للرسم — ومع تحديده افتح تبويب الشاشة في الفاحص على اليمين:

┌─ الفاحص ──────────────────────────┐
│ رسم │ الشاشة │ الشفرة │
├───────────────────────────────────┤
│ هذا العنصر … │
│ ┌─────────────────────────────┐ │
│ │ هو الشاشة ▾ │ │ ← اختر هذا
│ └─────────────────────────────┘ │
│ │
│ المحتوى الذي لا يتّسع │
│ ┌─────────────────────────────┐ │
│ │ أظهره — يفيض خارجه ▾ │ │
│ └─────────────────────────────┘ │
└───────────────────────────────────┘

سمِّ الإطار Cases في لوحة الطبقات. ذلك الاسم هو اسم الشاشة — وهو ما سيطلبه navigate("Cases") لاحقًا.

المقاس الذي رسمته ليس المقاس النهائي. كل ما بداخل الإطار مثبَّت إلى حوافه، والإطار يملأ أي نافذة يُفتح فيها التطبيق.


٤. ذاكرة الشاشة نفسها​

تتذكّر الشاشة أشياء: أيّ صفحة من القائمة أنت فيها، وما كُتب في صندوق البحث، وأي الصفوف مؤشَّرة. تلك الذاكرة هي حالة الشاشة، وتُعلَن مرة واحدة على إطار الشاشة.

مع تحديد إطار الشاشة، في تبويب الشاشة، أضف هذه تحت الحالة:

الاسمالقيمة الابتدائية
cases[]
caseTotal0
page0
pageSize20
picked""
pickedCount0
pickedId""
form{ par_name: "", par_urgent: false, par_notes: "" }

أعلِن شكل form ولا تتركه {}. الارتباط الذي يقرأ عضوًا لا يملكه الكائن يرفع خطأ، وأول تقييم للشاشة يجري قبل أن يملأ أي شيء النموذج.


٥. املأ القائمة​

ما زلت على إطار الشاشة، في تبويب الشاشة، جد المعالِجات وضع هذا في حُمِّلت:

loadRecords("par_case", "cases", {
top: getState("pageSize"),
skip: getState("page") * getState("pageSize"),
orderBy: "par_name",
countInto: "caseTotal"
});

سطر استعلام واحد هو ترقيم الصفحات كله: top وskip هما الصفحة، وcountInto يعيد كم صفًّا هناك في المجموع — وهو ما لا تستطيع صفحة من عشرين قوله ويجب أن يعرفه زر التالي. بقيّته في اعرض القائمة صفحةً صفحة.

لا شيء يظهر بعد، لأنه لا شيء مرسوم يعرضه.


٦. صفّ واحد، مرسوم مرّة​

هذه الخطوة تفاجئ الناس، فلنقلها صراحةً: أنت ترسم صفًّا واحدًا، والمنصّة ترسم واحدًا لكل سجل.

١. ارسم داخل الشاشة إطارًا للقائمة — بعرض المحتوى كاملًا وبضع مئات من البكسلات ارتفاعًا. سمِّه Rows. في تبويب الشاشة اضبط هذا العنصر ← قائمة، والمحتوى الذي لا يتّسع ← تمرير للوصول إليه، ثم اتجاه التمرير ← رأسيًا. صندوق رُسِم لستة صفوف يحمل مائة متى وُضع جدول حقيقي خلفه. ٢. أعطه تخطيطًا تلقائيًّا رأسيًّا في تبويب رسم. بدونه تسقط كل نسخة من الصف فوق الأولى. ٣. ارسم إطارًا داخل Rows، بعرض كامل وارتفاع ٦٤. سمِّه Case row واضبط هذا العنصر ← عنصر قائمة. ٤. مع تحديد Case row، جد كرِّر لكل في تبويب الشاشة واكتب:

state.cases

٥. ارسم نصًّا داخل الصف للملخّص. حدّده، وفي الارتباطات اضبط النص:

row.par_name

row هو سجلّ هذه النسخة. احفظ واضغط معاينة: صفّ لكل قضية، من الجدول، دون تكرار أي شيء يدويًّا.

┌─ Rows ────────────────────────────────────────────────┐
│ ☐ ١ Sample case 1 Normal │ ← مرسوم مرّة واحدة
│ ☐ ٢ Sample case 2 High Urgent │
│ ☐ ٣ Sample case 3 Low │
│ … │
└───────────────────────────────────────────────────────┘

٧. التأشير، وفكرة واحدة عن ما هو مختار​

ارسم إطارًا صغيرًا ٢٠×٢٠ في أول الصف واضبط عنصر تحكّم ← مربع تأشير. ثم:

  • الارتباطات ← القيمة: state.picked.includes(row.id)

  • المعالِجات ← تغيّرت:

    var ids = "";
    var count = 0;
    var firstId = "";
    var i = 0;
    for (i = 0; i < state.cases.length(); i = i + 1) {
    var id = state.cases[i].id;
    var on = state.picked.includes(id);
    if (id == row.id) { on = !on; }
    if (on) {
    ids = ids + id + "|";
    count = count + 1;
    if (count == 1) { firstId = id; }
    }
    }
    setState("picked", ids);
    setState("pickedCount", count);
    setState("pickedId", firstId);

القيمة مقارنة لا حقل. كل مربع تأشير يعيد قراءة القائمة نفسها، فمسحها يزيل التأشير عنها جميعًا دفعةً واحدة — وهو ما لم يكن ليقدر عليه علَمٌ لكل صف، لأن لا شيء يعرف أين بقية الصفوف.


٨. شريط أوامر يستيقظ​

ارسم شريطًا فوق القائمة — إطارًا بعرض كامل وارتفاع ٥٦، هذا العنصر ← منطقة — وضع فيه ثلاثة أزرار. لكل واحد: ارسم إطارًا، اضبط عنصر تحكّم ← زر، وارسم داخله نصًّا للعنوان.

ضع أيقونة على كل زر. حدّد الزر واستعمل منتقي الأيقونات في تبويب رسم — Plus وPencil وTrash2. ما ينزل مسارٌ مرسوم عادي داخل الزر، والزر النهائي يرتّب الأيقونة والفجوة والعنوان بنفسه.

الزرالارتباطات ← مُفعَّلالمعالِجات ← نُقر
New(اتركه فارغًا)setState("form", { par_name: "", par_urgent: false, par_notes: "" });
openEndDrawer("New case");
Editstate.pickedCount == 1openStartDrawer("Edit case");
Deletestate.pickedCount > 0showModal("Confirm delete");

اربط مُفعَّل بدل كتابة شفرة تُطفئ الأزرار. يُعاد تقييم الارتباط مع كل تغيّر في الحالة، فيستيقظ الشريط ويهدأ من تلقاء نفسه.

لوِّن نص زر Delete وحدّه بالرمز color.error، لا بأحمرٍ اخترته بيدك. الرمز يتبع وضع ألوان القارئ؛ أما اللون الست عشري فصحيح لمن رسمه وخاطئ لكل من يختلف سمته.


٩. الدرج، وما هي بطاقة التراكب​

التراكب ليس شاشة. هو مكوّن تؤشّره كبطاقة، والمعالِج هو من يقرّر أين يُفتح.

١. ارسم النموذج في مكان بعيد عن الشاشة — إطارًا بعرض نحو ٤٢٠، بعنوان وحقول وزرّين. لا يهم موضعه على اللوحة؛ تُترجم البطاقة عند نقطة الأصل ويُحتفظ بـعرضها وحده. ٢. حدّد الإطار، اضغط اصنع مكوّنًا، وسمِّه New case. ٣. في لوحة المكوّن، أشِّر بطاقة تراكب.

┌─ الفاحص · المكوّن ─────────────────┐
│ ☑ بطاقة تراكب │
│ اعرضها بـ showModal("New │
│ case") … عرضها المرسوم هو │
│ عرض اللوحة. │
│ │
│ ☐ يجب الإجابة عنها │
│ ما دامت مفتوحة لا يعمل النقر │
│ خارجها ولا Escape ولا زر │
│ الإغلاق. │
└───────────────────────────────────┘

داخلها، لكل حقل: ارسم إطارًا، اضبط عنصر تحكّم ← صندوق نص (أو منطقة نص للملاحظات)، واربط القيمة بمسار الحالة الذي يحرّره — state.form.par_name وstate.form.par_notes. عنصر التحكّم المرتبط بمسار حالة بسيط يكتب فيه رجوعًا، فلا شيء آخر يُوصَّل.

ثم نُقر لزر الحفظ:

if (getState("form").par_name == "") {
notifyError("A case needs a summary.");
} else {
createRecord("par_case", {
par_name: getState("form").par_name,
par_urgent: getState("form").par_urgent,
par_notes: getState("form").par_notes
});
notify("Case created.");
closeEndDrawer();
setState("page", 0);
loadRecords("par_case", "cases", { top: getState("pageSize"), skip: 0,
orderBy: "par_name", countInto: "caseTotal" });
}

كل كتابة تنتهي بإعادة قراءة القائمة. لا شيء يتحدّث من تلقاء نفسه — الشاشة تعرض ما تحمله حالتها، والحالة تحمل ما وضعه فيها آخر استعلام.

سؤال يجب أن يُجاب​

ارسم تأكيد الحذف بالطريقة نفسها، وأشِّر بطاقة تراكب، ثم أشِّر يجب الإجابة عنها أيضًا. النقر خارجها والضغط على Escape وزر الإغلاق كلها تتوقّف — فارسم مخرجك بنفسك: زر Cancel ينفّذ closeModal().

اترك هذا التأشير الثاني مطفأً في كل مكان آخر. التراكب الذي فتحه أحدهم بالخطأ ينبغي أن يُغلق كما فُتح؛ والوحيد الذي يجب أن يرفض هو ذاك الذي لا يكون النقر بجانبه إجابةً عنه.


١٠. احفظ، وافتحه على الحقيقة​

اضغط حفظ. تظهر التحذيرات أعلى الصفحة إن لاحظ المترجم شيئًا — زر بلا كلمات، إطار شاشة غائب، ارتباط ليس تعبيرًا نقيًّا. التحذير يسمّي العنصر، فأصلحه باختياره.

ثم غادر المصمّم وافتح التطبيق من قائمة تطبيقات القشرة. المعاينة ليست الشيء نفسه: ترسم الشاشات ذاتها دون بيئة خلفها، فما لا تستطيع أن تريك إياه هو بياناتك. أما البقية — الأدراج ومربعات التأشير وشريط الأوامر — فتتصرّف تصرّفًا واحدًا.


ما بعد ذلك​

  • شاشة ثانية. ارسم إطارًا آخر بجوار الأول، وسِمه الشاشة، وسمِّه. يظهر منتقي يُفتح عند في شريط العنوان فور وجود شاشتين. وnavigate("Board") ينقل بينهما.
  • التبويبات. شريط التبويبات ليس عنصر تحكّم: هو حقل حالة واحد ولوحة لكل قيمة. ارسم اللوحات في المستطيل نفسه، واربط ظاهر لكل واحدة بـ state.tab == "notes"، وأعطِ أزرار التبويب ارتباط مختار بالمقارنة نفسها. للوصول إلى لوحة تحت غيرها، استعمل ارسم على هذه وحدها في صفّ الطبقة.
  • عناصر تحكّم لم ترسمها. يشحن نموذج studio-component-library قائمةً منسدلة وبحثًا ومفتاحًا. أشِّر ينشر مكوّنات في ذلك التطبيق فتظهر في لوحتك — انظر شارك مكوّنًا بين الشاشات.
  • لوحة معلومات. اللوحة نفسها ترسم واحدة: انظر لوحة معلومات على شاشة.

استكشاف الأعطال​

ما تراهما هو
Only booleans are allowed in this expressionحقل اختياري قُرئ كشرط. لُفّه: (state.x.par_urgent ?? false).
ارتباط في صفّ يُقرأ فارغًاrow لا يوجد إلا داخل العنصر الحامل لـ كرِّر لكل وأبنائه.
الصفوف تتراكم فوق بعضهاالصندوق حول الصف المتكرر يحتاج تخطيطًا تلقائيًّا.
النص يفيض من أسفل الصندوقضعه في إطار على تمرير للوصول إليه ← رأسيًا، وثبّت الكلمات إلى الأعلى.
قائمة منسدلة تُغلق الدرج الذي فُتحت منهأُصلح في ٠٫٣١. حدِّث، أو انقل المنتقي خارج الدرج.
زر الحفظ معطَّللم يتغيّر شيء منذ آخر حفظ.